From 4038c1e916f229869902a42f1ca087cf03257a17 Mon Sep 17 00:00:00 2001 From: Moritz Ellerbrock Date: Thu, 30 Mar 2023 00:51:12 +0200 Subject: [PATCH 01/17] add vapor implementation --- .gitignore | 7 +- bookstore-vapor/.dockerignore | 2 + bookstore-vapor/.gitignore | 10 + bookstore-vapor/Dockerfile | 77 ++++++ bookstore-vapor/Package.resolved | 239 ++++++++++++++++++ bookstore-vapor/Package.swift | 41 +++ .../Sources/App/Controllers/.gitkeep | 0 .../App/Controllers/BookController.swift | 44 ++++ .../Sources/App/Migrations/CreateBook.swift | 17 ++ .../Sources/App/Models/BookContent.swift | 19 ++ .../Sources/App/Models/BookModel.swift | 41 +++ .../App/Models/PaginationContent.swift | 5 + .../Repository/BookModelRepository.swift | 73 ++++++ .../Registration/RepositoryRegistration.swift | 23 ++ bookstore-vapor/Sources/App/configure.swift | 34 +++ bookstore-vapor/Sources/App/routes.swift | 6 + .../Application+Repository.swift | 15 ++ .../Sources/RepositoryPattern/Config.swift | 50 ++++ .../RepositoryPattern/Repository.swift | 6 + .../RepositoryPattern/RepositoryFactory.swift | 15 ++ .../RepositoryPattern/RepositoryId.swift | 10 + .../RepositoryRegistry.swift | 27 ++ .../Request+RepositoryFactory.swift | 7 + bookstore-vapor/Sources/Run/main.swift | 9 + bookstore-vapor/Tests/AppTests/AppTests.swift | 10 + 25 files changed, 786 insertions(+), 1 deletion(-) create mode 100644 bookstore-vapor/.dockerignore create mode 100644 bookstore-vapor/.gitignore create mode 100644 bookstore-vapor/Dockerfile create mode 100644 bookstore-vapor/Package.resolved create mode 100644 bookstore-vapor/Package.swift create mode 100644 bookstore-vapor/Sources/App/Controllers/.gitkeep create mode 100644 bookstore-vapor/Sources/App/Controllers/BookController.swift create mode 100644 bookstore-vapor/Sources/App/Migrations/CreateBook.swift create mode 100644 bookstore-vapor/Sources/App/Models/BookContent.swift create mode 100644 bookstore-vapor/Sources/App/Models/BookModel.swift create mode 100644 bookstore-vapor/Sources/App/Models/PaginationContent.swift create mode 100644 bookstore-vapor/Sources/App/Models/Repository/BookModelRepository.swift create mode 100644 bookstore-vapor/Sources/App/Registration/RepositoryRegistration.swift create mode 100644 bookstore-vapor/Sources/App/configure.swift create mode 100644 bookstore-vapor/Sources/App/routes.swift create mode 100644 bookstore-vapor/Sources/RepositoryPattern/Application+Repository.swift create mode 100644 bookstore-vapor/Sources/RepositoryPattern/Config.swift create mode 100644 bookstore-vapor/Sources/RepositoryPattern/Repository.swift create mode 100644 bookstore-vapor/Sources/RepositoryPattern/RepositoryFactory.swift create mode 100644 bookstore-vapor/Sources/RepositoryPattern/RepositoryId.swift create mode 100644 bookstore-vapor/Sources/RepositoryPattern/RepositoryRegistry.swift create mode 100644 bookstore-vapor/Sources/RepositoryPattern/Request+RepositoryFactory.swift create mode 100644 bookstore-vapor/Sources/Run/main.swift create mode 100644 bookstore-vapor/Tests/AppTests/AppTests.swift diff --git a/.gitignore b/.gitignore index 7167192..82b5037 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,9 @@ nb-configuration.xml *.tfstate.backup # Fleet -.fleet \ No newline at end of file +.fleet + +# Swift +.build/ +.swiftpm/ +.derivedData \ No newline at end of file diff --git a/bookstore-vapor/.dockerignore b/bookstore-vapor/.dockerignore new file mode 100644 index 0000000..2d9f16e --- /dev/null +++ b/bookstore-vapor/.dockerignore @@ -0,0 +1,2 @@ +.build/ +.swiftpm/ diff --git a/bookstore-vapor/.gitignore b/bookstore-vapor/.gitignore new file mode 100644 index 0000000..8f1c94f --- /dev/null +++ b/bookstore-vapor/.gitignore @@ -0,0 +1,10 @@ +Packages +.build +xcuserdata +*.xcodeproj +DerivedData/ +.derivedData/ +.DS_Store +db.sqlite +.swiftpm +.env diff --git a/bookstore-vapor/Dockerfile b/bookstore-vapor/Dockerfile new file mode 100644 index 0000000..087e712 --- /dev/null +++ b/bookstore-vapor/Dockerfile @@ -0,0 +1,77 @@ +# ================================ +# Build image +# ================================ +FROM swift:5.7-jammy as build + +# Install OS updates and, if needed, sqlite3 +RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ + && apt-get -q update \ + && apt-get -q dist-upgrade -y\ + && rm -rf /var/lib/apt/lists/* + +# Set up a build area +WORKDIR /build + +# First just resolve dependencies. +# This creates a cached layer that can be reused +# as long as your Package.swift/Package.resolved +# files do not change. +COPY ./Package.* ./ +RUN swift package resolve + +# Copy entire repo into container +COPY . . + +# Build everything, with optimizations +RUN swift build -c release --static-swift-stdlib + +# Switch to the staging area +WORKDIR /staging + +# Copy main executable to staging area +RUN cp "$(swift build --package-path /build -c release --show-bin-path)/Run" ./ + +# Copy resources bundled by SPM to staging area +RUN find -L "$(swift build --package-path /build -c release --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \; + +# Copy any resources from the public directory and views directory if the directories exist +# Ensure that by default, neither the directory nor any of its contents are writable. +RUN [ -d /build/Public ] && { mv /build/Public ./Public && chmod -R a-w ./Public; } || true +RUN [ -d /build/Resources ] && { mv /build/Resources ./Resources && chmod -R a-w ./Resources; } || true + +# ================================ +# Run image +# ================================ +FROM ubuntu:jammy + +# Make sure all system packages are up to date, and install only essential packages. +RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ + && apt-get -q update \ + && apt-get -q dist-upgrade -y \ + && apt-get -q install -y \ + ca-certificates \ + tzdata \ +# If your app or its dependencies import FoundationNetworking, also install `libcurl4`. + # libcurl4 \ +# If your app or its dependencies import FoundationXML, also install `libxml2`. + # libxml2 \ + && rm -r /var/lib/apt/lists/* + +# Create a vapor user and group with /app as its home directory +RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app vapor + +# Switch to the new home directory +WORKDIR /app + +# Copy built executable and any staged resources from builder +COPY --from=build --chown=vapor:vapor /staging /app + +# Ensure all further commands run as the vapor user +USER vapor:vapor + +# Let Docker bind to port 8080 +EXPOSE 8080 + +# Start the Vapor service when the image is run, default to listening on 8080 in production environment +ENTRYPOINT ["./Run"] +CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] diff --git a/bookstore-vapor/Package.resolved b/bookstore-vapor/Package.resolved new file mode 100644 index 0000000..59f0ac9 --- /dev/null +++ b/bookstore-vapor/Package.resolved @@ -0,0 +1,239 @@ +{ + "pins" : [ + { + "identity" : "async-http-client", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/async-http-client.git", + "state" : { + "revision" : "864c8d9e0ead5de7ba70b61c8982f89126710863", + "version" : "1.15.0" + } + }, + { + "identity" : "async-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/async-kit.git", + "state" : { + "revision" : "9acea4c92f51a5885c149904f0d11db4712dda80", + "version" : "1.16.0" + } + }, + { + "identity" : "console-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/console-kit.git", + "state" : { + "revision" : "447f1046fb4e9df40973fe426ecb24a6f0e8d3b4", + "version" : "4.6.0" + } + }, + { + "identity" : "fluent", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/fluent.git", + "state" : { + "revision" : "8d6015096200b4c2f17b884eb966defc42a2ad1d", + "version" : "4.7.1" + } + }, + { + "identity" : "fluent-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/fluent-kit.git", + "state" : { + "revision" : "21d99b0c66cf86867a779bac831d800aac22f5e6", + "version" : "1.41.0" + } + }, + { + "identity" : "fluent-postgres-driver", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/fluent-postgres-driver.git", + "state" : { + "revision" : "f2b084417472e6a153d555905796f0ef0bb6fc37", + "version" : "2.5.1" + } + }, + { + "identity" : "multipart-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/multipart-kit.git", + "state" : { + "revision" : "3a31859efeb054cdcd407fe152802ddc5c58bbce", + "version" : "4.5.3" + } + }, + { + "identity" : "postgres-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/postgres-kit.git", + "state" : { + "revision" : "8ea9274633374d01bdef10da3206db74f3e261d6", + "version" : "2.9.1" + } + }, + { + "identity" : "postgres-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/postgres-nio.git", + "state" : { + "revision" : "5d93f3e05f0493441ad46f6d7a76109ff685329c", + "version" : "1.13.0" + } + }, + { + "identity" : "routing-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/routing-kit.git", + "state" : { + "revision" : "bdc9c25adbf77ba2b02113077ae8f355d87df83e", + "version" : "4.7.1" + } + }, + { + "identity" : "sql-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/sql-kit.git", + "state" : { + "revision" : "fcc29f543b3de7b661cbe7540805974234cb9740", + "version" : "3.24.0" + } + }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms.git", + "state" : { + "revision" : "b14b7f4c528c942f121c8b860b9410b2bf57825e", + "version" : "1.0.0" + } + }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "ff3d2212b6b093db7f177d0855adbc4ef9c5f036", + "version" : "1.0.3" + } + }, + { + "identity" : "swift-backtrace", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/swift-backtrace.git", + "state" : { + "revision" : "f25620d5d05e2f1ba27154b40cafea2b67566956", + "version" : "1.3.3" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", + "version" : "1.0.4" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "da0fe44138ab86e380f40a2acbd8a611b07d3f64", + "version" : "2.4.0" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "32e8d724467f8fe623624570367e3d50c5638e46", + "version" : "1.5.2" + } + }, + { + "identity" : "swift-metrics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-metrics.git", + "state" : { + "revision" : "e8bced74bc6d747745935e469f45d03f048d6cbd", + "version" : "2.3.4" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "9b2848d76f5caad08b97e71a04345aa5bdb23a06", + "version" : "2.49.0" + } + }, + { + "identity" : "swift-nio-extras", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-extras.git", + "state" : { + "revision" : "cc1e5275079380c859417dbea8588531f1a90ec3", + "version" : "1.18.0" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "38feec96bcd929028939107684073554bf01abeb", + "version" : "1.25.2" + } + }, + { + "identity" : "swift-nio-ssl", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-ssl.git", + "state" : { + "revision" : "4fb7ead803e38949eb1d6fabb849206a72c580f3", + "version" : "2.23.0" + } + }, + { + "identity" : "swift-nio-transport-services", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-transport-services.git", + "state" : { + "revision" : "c0d9a144cfaec8d3d596aadde3039286a266c15c", + "version" : "1.15.0" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics", + "state" : { + "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", + "version" : "1.0.2" + } + }, + { + "identity" : "vapor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/vapor.git", + "state" : { + "revision" : "f39218ec4998654c59251b282cbf4bb9b2cf2629", + "version" : "4.74.2" + } + }, + { + "identity" : "websocket-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/websocket-kit.git", + "state" : { + "revision" : "2b8885974e8d9f522e787805000553f4f7cce8a0", + "version" : "2.7.0" + } + } + ], + "version" : 2 +} diff --git a/bookstore-vapor/Package.swift b/bookstore-vapor/Package.swift new file mode 100644 index 0000000..d9dc68c --- /dev/null +++ b/bookstore-vapor/Package.swift @@ -0,0 +1,41 @@ +// swift-tools-version:5.7 +import PackageDescription + +let package = Package( + name: "bookstore", + platforms: [ + .macOS(.v13) + ], + dependencies: [ + .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"), + .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"), + .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"), + ], + targets: [ + .target( + name: "App", + dependencies: [ + .product(name: "Fluent", package: "fluent"), + .product(name: "Vapor", package: "vapor"), + .target(name: "RepositoryPattern"), + ], + swiftSettings: [ + .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release)) + ] + ), + .executableTarget(name: "Run", dependencies: [.target(name: "App")]), + .target( + name: "RepositoryPattern", + dependencies: [ + .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"), + .product(name: "Fluent", package: "fluent"), + .product(name: "Vapor", package: "vapor"), + ] + ), + // Testing targets + .testTarget(name: "AppTests", dependencies: [ + .target(name: "App"), + .product(name: "XCTVapor", package: "vapor"), + ]), + ] +) diff --git a/bookstore-vapor/Sources/App/Controllers/.gitkeep b/bookstore-vapor/Sources/App/Controllers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/bookstore-vapor/Sources/App/Controllers/BookController.swift b/bookstore-vapor/Sources/App/Controllers/BookController.swift new file mode 100644 index 0000000..59bb97e --- /dev/null +++ b/bookstore-vapor/Sources/App/Controllers/BookController.swift @@ -0,0 +1,44 @@ +import Fluent +import Vapor + +struct BookController: RouteCollection { + func boot(routes: RoutesBuilder) throws { + let books = routes.grouped("books") + books.post(use: createBook) + books.delete(use: deleteAllBooks) + books.get(use: getAllBooks) + + let bookId = books.grouped(":bookId") + bookId.get(use: getBookById) + bookId.delete(use: deleteBookById) + } + + func createBook(req: Request) async throws -> String { + let dto = try req.content.decode(BookContent.self) + return try await req.repositories.books.create(book: dto) + } + + func deleteAllBooks(req: Request) async throws -> Response { + try await req.repositories.books.deleteAll() + return Response(status: .ok) + } + + func getAllBooks(req: Request) async throws -> Page { + return try await req.repositories.books.list() + } + + func getBookById(req: Request) async throws -> BookContent { + guard let bookId = req.parameters.get("bookId") else { + throw Abort(.badRequest) + } + return try await req.repositories.books.getBook(by: bookId) + } + + func deleteBookById(req: Request) async throws -> Response { + guard let bookId = req.parameters.get("bookId") else { + throw Abort(.badRequest) + } + try await req.repositories.books.delete(bookId: bookId) + return Response(status: .ok) + } +} diff --git a/bookstore-vapor/Sources/App/Migrations/CreateBook.swift b/bookstore-vapor/Sources/App/Migrations/CreateBook.swift new file mode 100644 index 0000000..4a887ab --- /dev/null +++ b/bookstore-vapor/Sources/App/Migrations/CreateBook.swift @@ -0,0 +1,17 @@ +import Fluent + +struct CreateBooks: AsyncMigration { + func prepare(on database: Database) async throws { + try await database.schema(BookModel.schema) + .id() + .field("title", .string, .required) + .field("author", .string, .required) + .field("release_date", .date, .required) + .field("publisher", .string, .required) + .create() + } + + func revert(on database: Database) async throws { + try await database.schema("books").delete() + } +} diff --git a/bookstore-vapor/Sources/App/Models/BookContent.swift b/bookstore-vapor/Sources/App/Models/BookContent.swift new file mode 100644 index 0000000..f2be7ba --- /dev/null +++ b/bookstore-vapor/Sources/App/Models/BookContent.swift @@ -0,0 +1,19 @@ +import Vapor + +struct BookContent: Content { + let id: UUID? + let title: String + let author: String + let releaseDate: Date + let publisher: String +} + +extension BookContent { + func toModel() -> BookModel { + BookModel(id: self.id, + title: self.title, + author: self.author, + releaseDate: self.releaseDate, + publisher: self.publisher) + } +} diff --git a/bookstore-vapor/Sources/App/Models/BookModel.swift b/bookstore-vapor/Sources/App/Models/BookModel.swift new file mode 100644 index 0000000..5c99888 --- /dev/null +++ b/bookstore-vapor/Sources/App/Models/BookModel.swift @@ -0,0 +1,41 @@ +import Fluent +import Vapor + +final class BookModel: Model { + static let schema = "books" + + @ID(key: .id) + var id: UUID? + + @Field(key: "title") + var title: String + + @Field(key: "author") + var author: String + + @Field(key: "release_date") + var releaseDate: Date + + @Field(key: "publisher") + var publisher: String + + init() { } + + init(id: UUID? = nil, title: String, author: String, releaseDate: Date, publisher: String) { + self.id = id + self.title = title + self.author = author + self.releaseDate = releaseDate + self.publisher = publisher + } +} + +extension BookModel { + func toContent() -> BookContent { + BookContent(id: self.id, + title: self.title, + author: self.author, + releaseDate: self.releaseDate, + publisher: self.publisher) + } +} diff --git a/bookstore-vapor/Sources/App/Models/PaginationContent.swift b/bookstore-vapor/Sources/App/Models/PaginationContent.swift new file mode 100644 index 0000000..f72e37f --- /dev/null +++ b/bookstore-vapor/Sources/App/Models/PaginationContent.swift @@ -0,0 +1,5 @@ +import Vapor + +struct PaginationContent: Content { + let limit: Int +} diff --git a/bookstore-vapor/Sources/App/Models/Repository/BookModelRepository.swift b/bookstore-vapor/Sources/App/Models/Repository/BookModelRepository.swift new file mode 100644 index 0000000..0f2795a --- /dev/null +++ b/bookstore-vapor/Sources/App/Models/Repository/BookModelRepository.swift @@ -0,0 +1,73 @@ +import Vapor +import Fluent +import RepositoryPattern + +enum BookModelRepositoryError: Error { + case noBookFound + case idInvalid +} + +protocol BookModelRepository: Repository { + func list() async throws -> [BookContent] + + func create(book: BookContent) async throws -> String + + func getBook(by bookId: String) async throws -> BookContent + + func delete(bookId: String) async throws -> Void + + func deleteAll() async throws -> Void +} + +struct BookModelRepositoryImpl: BookModelRepository { + var req: Request + + init(_ req: Request) { + self.req = req + } + + private func query() -> QueryBuilder { + BookModel.query(on: req.db) + } + + private func query(_ idString: String) throws -> QueryBuilder { + guard let id = UUID(uuidString: idString) else { + throw BookModelRepositoryError.idInvalid + } + return query().filter(\.$id == id) + } + + func list() async throws -> [BookContent] { + let builder: QueryBuilder + if let limit = try? req.query.decode(PaginationContent.self).limit { + builder = query().limit(limit) + } else { + builder = query() + } + + return try await builder.all().map { model in + model.toContent() + } + } + + func create(book: BookContent) async throws -> String { + let bookModel = book.toModel() + try await bookModel.create(on: req.db) + return bookModel.id?.uuidString ?? "" + } + + func getBook(by bookId: String) async throws -> BookContent { + guard let book = try await query(bookId).first() else { + throw BookModelRepositoryError.noBookFound + } + return book.toContent() + } + + func delete(bookId: String) async throws -> Void { + try await query(bookId).delete() + } + + func deleteAll() async throws -> Void { + try await query().delete() + } +} diff --git a/bookstore-vapor/Sources/App/Registration/RepositoryRegistration.swift b/bookstore-vapor/Sources/App/Registration/RepositoryRegistration.swift new file mode 100644 index 0000000..d727649 --- /dev/null +++ b/bookstore-vapor/Sources/App/Registration/RepositoryRegistration.swift @@ -0,0 +1,23 @@ +import Vapor +import RepositoryPattern + +extension RepositoryId { + static let book = RepositoryId("book") +} + +enum RepositoryRegistration { + static func bootstrap(for app: Application) async throws { + app.repositories.register(.book) { req in + BookModelRepositoryImpl(req) + } + } +} + +extension RepositoryFactory { + var books: BookModelRepository { + guard let result = make(.book) as? BookModelRepository else { + fatalError("Book repository is not configured") + } + return result + } +} diff --git a/bookstore-vapor/Sources/App/configure.swift b/bookstore-vapor/Sources/App/configure.swift new file mode 100644 index 0000000..0430a42 --- /dev/null +++ b/bookstore-vapor/Sources/App/configure.swift @@ -0,0 +1,34 @@ +import Fluent +import Vapor +import RepositoryPattern + +// configures your application +public func configure(_ app: Application) async throws { + // register repository + try RepositoryPattern.configure(app) + // add migrations + app.migrations.add(CreateBooks()) + + try await app.autoMigrate() + + try await RepositoryRegistration.bootstrap(for: app) + + setupCustomDateCoder() + + // register routes + try routes(app) +} + +func setupCustomDateCoder() { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd" + + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .formatted(dateFormatter) + ContentConfiguration.global.use(encoder: encoder, for: .json) + + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .formatted(dateFormatter) + ContentConfiguration.global.use(decoder: decoder, for: .json) +} diff --git a/bookstore-vapor/Sources/App/routes.swift b/bookstore-vapor/Sources/App/routes.swift new file mode 100644 index 0000000..2a31d1a --- /dev/null +++ b/bookstore-vapor/Sources/App/routes.swift @@ -0,0 +1,6 @@ +import Fluent +import Vapor + +func routes(_ app: Application) throws { + try app.register(collection: BookController()) +} diff --git a/bookstore-vapor/Sources/RepositoryPattern/Application+Repository.swift b/bookstore-vapor/Sources/RepositoryPattern/Application+Repository.swift new file mode 100644 index 0000000..2875c7a --- /dev/null +++ b/bookstore-vapor/Sources/RepositoryPattern/Application+Repository.swift @@ -0,0 +1,15 @@ +import Vapor + +public extension Application { + + private struct Key: StorageKey { + typealias Value = RepositoryRegistry + } + + var repositories: RepositoryRegistry { + if storage[Key.self] == nil { + storage[Key.self] = .init(self) + } + return storage[Key.self]! + } +} diff --git a/bookstore-vapor/Sources/RepositoryPattern/Config.swift b/bookstore-vapor/Sources/RepositoryPattern/Config.swift new file mode 100644 index 0000000..d80f76c --- /dev/null +++ b/bookstore-vapor/Sources/RepositoryPattern/Config.swift @@ -0,0 +1,50 @@ +import Vapor +import FluentPostgresDriver +import Fluent + +public func configure(_ app: Application) throws { + try DatabaseConfiguration.setup(in: app) +} + +struct DatabaseConfiguration { + enum DatabaseConfigurationError: LocalizedError { + case usernameMissing, passwordMissing, databaseNameMissing + } + + let hostname: String + let port: Int + let username: String + let password: String + let database: String + + init(_ app: Application) throws { + guard let username = Environment.get("DATABASE_USERNAME") else { + throw DatabaseConfigurationError.usernameMissing + } + guard let password = Environment.get("DATABASE_PASSWORD") else { + throw DatabaseConfigurationError.passwordMissing + } + + let databaseName = app.environment == .testing ? "testing" : Environment.get("DATABASE_NAME") + guard let databaseName else { + throw DatabaseConfigurationError.databaseNameMissing + } + + self.port = Environment.get("DATABASE_PORT").flatMap(Int.init(_:)) ?? PostgresConfiguration.ianaPortNumber + self.hostname = Environment.get("DATABASE_HOST") ?? "db" + self.username = username + self.password = password + self.database = databaseName + } + + static func setup(in app: Application) throws { + let dbConfig = try DatabaseConfiguration(app) + let factoryConfig: DatabaseConfigurationFactory = .postgres(hostname: dbConfig.hostname, + port: dbConfig.port, + username: dbConfig.username, + password: dbConfig.password, + database: dbConfig.database) + + app.databases.use(factoryConfig, as: .psql) + } +} diff --git a/bookstore-vapor/Sources/RepositoryPattern/Repository.swift b/bookstore-vapor/Sources/RepositoryPattern/Repository.swift new file mode 100644 index 0000000..d99ad56 --- /dev/null +++ b/bookstore-vapor/Sources/RepositoryPattern/Repository.swift @@ -0,0 +1,6 @@ +import Vapor + +public protocol Repository { + var req: Request { get set } + init(_ req: Request) +} diff --git a/bookstore-vapor/Sources/RepositoryPattern/RepositoryFactory.swift b/bookstore-vapor/Sources/RepositoryPattern/RepositoryFactory.swift new file mode 100644 index 0000000..2b74ef1 --- /dev/null +++ b/bookstore-vapor/Sources/RepositoryPattern/RepositoryFactory.swift @@ -0,0 +1,15 @@ +import Vapor + +public struct RepositoryFactory { + private var registry: RepositoryRegistry + private var req: Request + + init(_ req: Request, _ registry: RepositoryRegistry) { + self.req = req + self.registry = registry + } + + public func make(_ id: RepositoryId) -> Repository { + registry.make(id, req) + } +} diff --git a/bookstore-vapor/Sources/RepositoryPattern/RepositoryId.swift b/bookstore-vapor/Sources/RepositoryPattern/RepositoryId.swift new file mode 100644 index 0000000..f8b4e92 --- /dev/null +++ b/bookstore-vapor/Sources/RepositoryPattern/RepositoryId.swift @@ -0,0 +1,10 @@ +import Vapor + +public struct RepositoryId: Hashable, Codable { + + public let string: String + + public init(_ string: String) { + self.string = string + } +} diff --git a/bookstore-vapor/Sources/RepositoryPattern/RepositoryRegistry.swift b/bookstore-vapor/Sources/RepositoryPattern/RepositoryRegistry.swift new file mode 100644 index 0000000..8c7c4a4 --- /dev/null +++ b/bookstore-vapor/Sources/RepositoryPattern/RepositoryRegistry.swift @@ -0,0 +1,27 @@ +import Vapor + +public final class RepositoryRegistry { + + private let app: Application + private var builders: [RepositoryId: ((Request) -> Repository)] + + init(_ app: Application) { + self.app = app + self.builders = [:] + } + + func builder(_ req: Request) -> RepositoryFactory { + .init(req, self) + } + + func make(_ id: RepositoryId, _ req: Request) -> Repository { + guard let builder = builders[id] else { + fatalError("Repository for id `\(id.string)` is not configured.") + } + return builder(req) + } + + public func register(_ id: RepositoryId, _ builder: @escaping (Request) -> Repository) { + builders[id] = builder + } +} diff --git a/bookstore-vapor/Sources/RepositoryPattern/Request+RepositoryFactory.swift b/bookstore-vapor/Sources/RepositoryPattern/Request+RepositoryFactory.swift new file mode 100644 index 0000000..d099fec --- /dev/null +++ b/bookstore-vapor/Sources/RepositoryPattern/Request+RepositoryFactory.swift @@ -0,0 +1,7 @@ +import Vapor + +public extension Request { + var repositories: RepositoryFactory { + application.repositories.builder(self) + } +} diff --git a/bookstore-vapor/Sources/Run/main.swift b/bookstore-vapor/Sources/Run/main.swift new file mode 100644 index 0000000..cf54feb --- /dev/null +++ b/bookstore-vapor/Sources/Run/main.swift @@ -0,0 +1,9 @@ +import App +import Vapor + +var env = try Environment.detect() +try LoggingSystem.bootstrap(from: &env) +let app = Application(env) +defer { app.shutdown() } +try await configure(app) +try app.run() diff --git a/bookstore-vapor/Tests/AppTests/AppTests.swift b/bookstore-vapor/Tests/AppTests/AppTests.swift new file mode 100644 index 0000000..8fbfda7 --- /dev/null +++ b/bookstore-vapor/Tests/AppTests/AppTests.swift @@ -0,0 +1,10 @@ +@testable import App +import XCTVapor + +final class AppTests: XCTestCase { + func testHelloWorld() async throws { + let app = Application(.testing) + defer { app.shutdown() } + try await configure(app) + } +} From fced3544b7aee56e6c3e37048524ac5369b0e247 Mon Sep 17 00:00:00 2001 From: Moritz Ellerbrock Date: Mon, 3 Apr 2023 20:39:27 +0200 Subject: [PATCH 02/17] add github action to publish vapor image --- .github/workflows/publish-vapor-image.yml | 63 +++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 .github/workflows/publish-vapor-image.yml diff --git a/.github/workflows/publish-vapor-image.yml b/.github/workflows/publish-vapor-image.yml new file mode 100644 index 0000000..b119714 --- /dev/null +++ b/.github/workflows/publish-vapor-image.yml @@ -0,0 +1,63 @@ +name: Publish Vapor image to ECR + +on: + pull_request: + branches: + - main + +jobs: + publish: + name: Publish Vapor Image + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + + - name: Docker meta + id: meta + uses: docker/metadata-action@57396166ad8aefe6098280995947635806a0e6ea + with: + images: | + name=bookstore-vapor + tags: | + # minimal (short sha) + type=sha + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1-node16 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: eu-west-1 + + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@261a7de32bda11ba01f4d75c4ed6caf3739e54be + + - name: Resolve Dependencies + run: swift package resolve + working-directory: ./bookstore-vapor + + - name: Build + run: swift build -c release -Xswiftc -O --static-swift-stdlib + working-directory: ./bookstore-vapor + + - name: Create staging directory + run: | + mkdir staging + cp "$(swift build -c release --show-bin-path)/Run" ./staging + find -L "$(swift build -c release --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./staging/ \; + [ -d ./Public ] && { mv ./Public ./staging/Public && chmod -R a-w ./staging/Public; } || true + [ -d ./Resources ] && { mv ./Resources ./staging/Resources && chmod -R a-w ./staging/Resources; } || true + working-directory: ./bookstore-vapor + + - name: Build, tag, and push image to Amazon ECR + env: + ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} + ECR_REPOSITORY: bookstore-vapor + IMAGE_TAGS: ${{ steps.meta.outputs.tags }} + run: | + docker build -t $ECR_REGISTRY/$IMAGE_TAGS -t $ECR_REGISTRY/$ECR_REPOSITORY . + docker push -a $ECR_REGISTRY/$ECR_REPOSITORY + working-directory: ./bookstore-vapor From 1de20243de19821464cde72a4e96120223070955 Mon Sep 17 00:00:00 2001 From: Moritz Ellerbrock Date: Mon, 3 Apr 2023 21:05:39 +0200 Subject: [PATCH 03/17] fix build --- bookstore-vapor/Sources/App/Controllers/BookController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookstore-vapor/Sources/App/Controllers/BookController.swift b/bookstore-vapor/Sources/App/Controllers/BookController.swift index 59bb97e..5c90d60 100644 --- a/bookstore-vapor/Sources/App/Controllers/BookController.swift +++ b/bookstore-vapor/Sources/App/Controllers/BookController.swift @@ -23,7 +23,7 @@ struct BookController: RouteCollection { return Response(status: .ok) } - func getAllBooks(req: Request) async throws -> Page { + func getAllBooks(req: Request) async throws -> [BookContent] { return try await req.repositories.books.list() } From bdf218302eea719729d2cc85bf2d08cd7d9e4e61 Mon Sep 17 00:00:00 2001 From: Moritz Ellerbrock Date: Mon, 3 Apr 2023 21:29:03 +0200 Subject: [PATCH 04/17] reduce Dockerfile to minimum --- bookstore-vapor/Dockerfile | 50 +++----------------------------------- 1 file changed, 3 insertions(+), 47 deletions(-) diff --git a/bookstore-vapor/Dockerfile b/bookstore-vapor/Dockerfile index 087e712..097382a 100644 --- a/bookstore-vapor/Dockerfile +++ b/bookstore-vapor/Dockerfile @@ -1,58 +1,14 @@ -# ================================ -# Build image -# ================================ -FROM swift:5.7-jammy as build - -# Install OS updates and, if needed, sqlite3 -RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ - && apt-get -q update \ - && apt-get -q dist-upgrade -y\ - && rm -rf /var/lib/apt/lists/* - -# Set up a build area -WORKDIR /build - -# First just resolve dependencies. -# This creates a cached layer that can be reused -# as long as your Package.swift/Package.resolved -# files do not change. -COPY ./Package.* ./ -RUN swift package resolve - -# Copy entire repo into container -COPY . . - -# Build everything, with optimizations -RUN swift build -c release --static-swift-stdlib - -# Switch to the staging area -WORKDIR /staging - -# Copy main executable to staging area -RUN cp "$(swift build --package-path /build -c release --show-bin-path)/Run" ./ - -# Copy resources bundled by SPM to staging area -RUN find -L "$(swift build --package-path /build -c release --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \; - -# Copy any resources from the public directory and views directory if the directories exist -# Ensure that by default, neither the directory nor any of its contents are writable. -RUN [ -d /build/Public ] && { mv /build/Public ./Public && chmod -R a-w ./Public; } || true -RUN [ -d /build/Resources ] && { mv /build/Resources ./Resources && chmod -R a-w ./Resources; } || true - -# ================================ -# Run image -# ================================ FROM ubuntu:jammy # Make sure all system packages are up to date, and install only essential packages. RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ && apt-get -q update \ - && apt-get -q dist-upgrade -y \ && apt-get -q install -y \ ca-certificates \ tzdata \ + libc6 \ # If your app or its dependencies import FoundationNetworking, also install `libcurl4`. - # libcurl4 \ + libcurl4 \ # If your app or its dependencies import FoundationXML, also install `libxml2`. # libxml2 \ && rm -r /var/lib/apt/lists/* @@ -64,7 +20,7 @@ RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app WORKDIR /app # Copy built executable and any staged resources from builder -COPY --from=build --chown=vapor:vapor /staging /app +COPY ./staging /app # Ensure all further commands run as the vapor user USER vapor:vapor From 9df5a5eafbf2125ce2e666644e7cbecf0ddb23f4 Mon Sep 17 00:00:00 2001 From: Moritz Ellerbrock Date: Fri, 12 May 2023 16:39:26 +0200 Subject: [PATCH 05/17] rename RepositoryPattern -> Repository --- bookstore-vapor/Package.swift | 4 ++-- .../Sources/App/Models/Repository/BookModelRepository.swift | 4 ++-- .../Sources/App/Registration/RepositoryRegistration.swift | 2 +- bookstore-vapor/Sources/App/configure.swift | 4 ++-- .../Application+Repository.swift | 0 .../Sources/{RepositoryPattern => Repository}/Config.swift | 0 .../RepositoryFactory.swift | 2 +- .../{RepositoryPattern => Repository}/RepositoryId.swift | 0 .../RepositoryProtocol.swift} | 2 +- .../RepositoryRegistry.swift | 6 +++--- .../Request+RepositoryFactory.swift | 0 11 files changed, 12 insertions(+), 12 deletions(-) rename bookstore-vapor/Sources/{RepositoryPattern => Repository}/Application+Repository.swift (100%) rename bookstore-vapor/Sources/{RepositoryPattern => Repository}/Config.swift (100%) rename bookstore-vapor/Sources/{RepositoryPattern => Repository}/RepositoryFactory.swift (81%) rename bookstore-vapor/Sources/{RepositoryPattern => Repository}/RepositoryId.swift (100%) rename bookstore-vapor/Sources/{RepositoryPattern/Repository.swift => Repository/RepositoryProtocol.swift} (66%) rename bookstore-vapor/Sources/{RepositoryPattern => Repository}/RepositoryRegistry.swift (83%) rename bookstore-vapor/Sources/{RepositoryPattern => Repository}/Request+RepositoryFactory.swift (100%) diff --git a/bookstore-vapor/Package.swift b/bookstore-vapor/Package.swift index d9dc68c..3cf64f9 100644 --- a/bookstore-vapor/Package.swift +++ b/bookstore-vapor/Package.swift @@ -17,7 +17,7 @@ let package = Package( dependencies: [ .product(name: "Fluent", package: "fluent"), .product(name: "Vapor", package: "vapor"), - .target(name: "RepositoryPattern"), + .target(name: "Repository"), ], swiftSettings: [ .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release)) @@ -25,7 +25,7 @@ let package = Package( ), .executableTarget(name: "Run", dependencies: [.target(name: "App")]), .target( - name: "RepositoryPattern", + name: "Repository", dependencies: [ .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"), .product(name: "Fluent", package: "fluent"), diff --git a/bookstore-vapor/Sources/App/Models/Repository/BookModelRepository.swift b/bookstore-vapor/Sources/App/Models/Repository/BookModelRepository.swift index 0f2795a..b4fc3bc 100644 --- a/bookstore-vapor/Sources/App/Models/Repository/BookModelRepository.swift +++ b/bookstore-vapor/Sources/App/Models/Repository/BookModelRepository.swift @@ -1,13 +1,13 @@ import Vapor import Fluent -import RepositoryPattern +import Repository enum BookModelRepositoryError: Error { case noBookFound case idInvalid } -protocol BookModelRepository: Repository { +protocol BookModelRepository: RepositoryProtocol { func list() async throws -> [BookContent] func create(book: BookContent) async throws -> String diff --git a/bookstore-vapor/Sources/App/Registration/RepositoryRegistration.swift b/bookstore-vapor/Sources/App/Registration/RepositoryRegistration.swift index d727649..4a58295 100644 --- a/bookstore-vapor/Sources/App/Registration/RepositoryRegistration.swift +++ b/bookstore-vapor/Sources/App/Registration/RepositoryRegistration.swift @@ -1,5 +1,5 @@ import Vapor -import RepositoryPattern +import Repository extension RepositoryId { static let book = RepositoryId("book") diff --git a/bookstore-vapor/Sources/App/configure.swift b/bookstore-vapor/Sources/App/configure.swift index 0430a42..bc18fd4 100644 --- a/bookstore-vapor/Sources/App/configure.swift +++ b/bookstore-vapor/Sources/App/configure.swift @@ -1,11 +1,11 @@ import Fluent import Vapor -import RepositoryPattern +import Repository // configures your application public func configure(_ app: Application) async throws { // register repository - try RepositoryPattern.configure(app) + try Repository.configure(app) // add migrations app.migrations.add(CreateBooks()) diff --git a/bookstore-vapor/Sources/RepositoryPattern/Application+Repository.swift b/bookstore-vapor/Sources/Repository/Application+Repository.swift similarity index 100% rename from bookstore-vapor/Sources/RepositoryPattern/Application+Repository.swift rename to bookstore-vapor/Sources/Repository/Application+Repository.swift diff --git a/bookstore-vapor/Sources/RepositoryPattern/Config.swift b/bookstore-vapor/Sources/Repository/Config.swift similarity index 100% rename from bookstore-vapor/Sources/RepositoryPattern/Config.swift rename to bookstore-vapor/Sources/Repository/Config.swift diff --git a/bookstore-vapor/Sources/RepositoryPattern/RepositoryFactory.swift b/bookstore-vapor/Sources/Repository/RepositoryFactory.swift similarity index 81% rename from bookstore-vapor/Sources/RepositoryPattern/RepositoryFactory.swift rename to bookstore-vapor/Sources/Repository/RepositoryFactory.swift index 2b74ef1..48c2c7b 100644 --- a/bookstore-vapor/Sources/RepositoryPattern/RepositoryFactory.swift +++ b/bookstore-vapor/Sources/Repository/RepositoryFactory.swift @@ -9,7 +9,7 @@ public struct RepositoryFactory { self.registry = registry } - public func make(_ id: RepositoryId) -> Repository { + public func make(_ id: RepositoryId) -> RepositoryProtocol { registry.make(id, req) } } diff --git a/bookstore-vapor/Sources/RepositoryPattern/RepositoryId.swift b/bookstore-vapor/Sources/Repository/RepositoryId.swift similarity index 100% rename from bookstore-vapor/Sources/RepositoryPattern/RepositoryId.swift rename to bookstore-vapor/Sources/Repository/RepositoryId.swift diff --git a/bookstore-vapor/Sources/RepositoryPattern/Repository.swift b/bookstore-vapor/Sources/Repository/RepositoryProtocol.swift similarity index 66% rename from bookstore-vapor/Sources/RepositoryPattern/Repository.swift rename to bookstore-vapor/Sources/Repository/RepositoryProtocol.swift index d99ad56..22d360d 100644 --- a/bookstore-vapor/Sources/RepositoryPattern/Repository.swift +++ b/bookstore-vapor/Sources/Repository/RepositoryProtocol.swift @@ -1,6 +1,6 @@ import Vapor -public protocol Repository { +public protocol RepositoryProtocol { var req: Request { get set } init(_ req: Request) } diff --git a/bookstore-vapor/Sources/RepositoryPattern/RepositoryRegistry.swift b/bookstore-vapor/Sources/Repository/RepositoryRegistry.swift similarity index 83% rename from bookstore-vapor/Sources/RepositoryPattern/RepositoryRegistry.swift rename to bookstore-vapor/Sources/Repository/RepositoryRegistry.swift index 8c7c4a4..980bfa0 100644 --- a/bookstore-vapor/Sources/RepositoryPattern/RepositoryRegistry.swift +++ b/bookstore-vapor/Sources/Repository/RepositoryRegistry.swift @@ -3,7 +3,7 @@ import Vapor public final class RepositoryRegistry { private let app: Application - private var builders: [RepositoryId: ((Request) -> Repository)] + private var builders: [RepositoryId: ((Request) -> RepositoryProtocol)] init(_ app: Application) { self.app = app @@ -14,14 +14,14 @@ public final class RepositoryRegistry { .init(req, self) } - func make(_ id: RepositoryId, _ req: Request) -> Repository { + func make(_ id: RepositoryId, _ req: Request) -> RepositoryProtocol { guard let builder = builders[id] else { fatalError("Repository for id `\(id.string)` is not configured.") } return builder(req) } - public func register(_ id: RepositoryId, _ builder: @escaping (Request) -> Repository) { + public func register(_ id: RepositoryId, _ builder: @escaping (Request) -> RepositoryProtocol) { builders[id] = builder } } diff --git a/bookstore-vapor/Sources/RepositoryPattern/Request+RepositoryFactory.swift b/bookstore-vapor/Sources/Repository/Request+RepositoryFactory.swift similarity index 100% rename from bookstore-vapor/Sources/RepositoryPattern/Request+RepositoryFactory.swift rename to bookstore-vapor/Sources/Repository/Request+RepositoryFactory.swift From b6333246bbb483a176859ce57532d43fecc96265 Mon Sep 17 00:00:00 2001 From: Moritz Ellerbrock Date: Sat, 13 May 2023 17:11:30 +0200 Subject: [PATCH 06/17] add terraform configuration --- README.md | 1 + terraform/deployment/main.tf | 77 +++++++++++++++++++++++++ terraform/deployment/variables.tf | 5 ++ terraform/ecr_repositories/variables.tf | 3 +- 4 files changed, 85 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b0ba48c..cb0d879 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Currently you can find the following implementations of the Book Store REST API: 1) [Quarkus](./bookstore-quarkus) 2) [Spring Boot](./bookstore-springboot) 3) [NestJS](./bookstore-nestjs) +4) [Vapor](./bookstore-vapor) ## Contribute diff --git a/terraform/deployment/main.tf b/terraform/deployment/main.tf index 6ddc815..5922c19 100644 --- a/terraform/deployment/main.tf +++ b/terraform/deployment/main.tf @@ -346,6 +346,71 @@ module "ecs_nestjs_app" { } } +module "ecs_vapor_app" { + source = "./modules/ecs" + alb_listener = module.public_alb.alb_listener + alb = { + target_group = "vapor-tg" + target_group_paths = ["/vapor/*"] + arn = module.public_alb.alb_listener_http_tcp_arn + rule_priority = 2 + } + aws_region = var.aws_region + cluster_id = aws_ecs_cluster.main.id + cluster_name = aws_ecs_cluster.main.name + fargate_cpu = "1024" + fargate_memory = "2048" + iam_role_ecs_task_execution_role = aws_iam_role.ecs_task_execution_role + iam_role_policy_ecs_task_execution_role = aws_iam_role_policy_attachment.ecs_task_execution_role + logs_retention_in_days = 30 + service_security_groups_ids = [module.ecs_tasks_sg.security_group_id] + subnet_ids = module.vpc.private_subnet_ids + vpc_id = module.vpc.vpc_id + service = { + name = "bookstore-vapor" + desired_count = 1 + max_count = 1 + } + autoscaling_settings = merge(local.autoscaling_settings, { + autoscaling_name = "vapor_scaling" + }) + task_definition = { + name = "bookstore-vapor" + image = var.nestjs_bookstore_image + aws_logs_group = "ecs/bookstore-vapor" + host_port = 3000 + container_port = 3000 + container_name = "bookstore-vapor" + health_check_path = "/vapor/health" + family = "bookstore-vapor-task" + env_vars = [ + # Check how to configure writer and reader endpoints + { + "name" : "DB_HOST", + "value" : tostring(module.books-database-vapor.db_endpoint), + }, + { + "name" : "DB_NAME", + "value" : tostring(module.books-database-vapor.db_name), + }, + { + "name" : "DB_PORT", + "value" : tostring(module.books-database-vapor.db_port), + } + ] + secret_vars = [ + { + "name" : "DB_USER", + "valueFrom" : module.database_secrets.db_username_secret_arn, + }, + { + "name" : "DB_PASSWORD", + "valueFrom" : module.database_secrets.db_password_secret_arn, + } + ] + } +} + ################################################################################ # Database ################################################################################ @@ -422,6 +487,18 @@ module "books-database-nestjs" { database_username = module.database_secrets.db_username_secret_value } +module "books-database-vapor" { + source = "./modules/db" + aws_region = var.aws_region + name = "booksdb-vapor" + database_name = "booksdb" + subnet_ids = module.vpc.private_subnet_ids + security_groups = [module.private_database_sg.security_group_id] + vpc_id = module.vpc.vpc_id + database_password = module.database_secrets.db_password_secret_value + database_username = module.database_secrets.db_username_secret_value +} + ################################################################################ # VPC Flow Logs IAM ################################################################################ diff --git a/terraform/deployment/variables.tf b/terraform/deployment/variables.tf index 2e714cc..ad8cfca 100644 --- a/terraform/deployment/variables.tf +++ b/terraform/deployment/variables.tf @@ -29,4 +29,9 @@ variable "springboot_bookstore_image" { variable "nestjs_bookstore_image" { description = "Defines container image" default = "143441946271.dkr.ecr.eu-west-1.amazonaws.com/bookstore-nestjs" +} + +variable "vapor_bookstore_image" { + description = "Defines container image" + default = "143441946271.dkr.ecr.eu-west-1.amazonaws.com/bookstore-vapor" } \ No newline at end of file diff --git a/terraform/ecr_repositories/variables.tf b/terraform/ecr_repositories/variables.tf index e86b976..b138ae5 100644 --- a/terraform/ecr_repositories/variables.tf +++ b/terraform/ecr_repositories/variables.tf @@ -17,6 +17,7 @@ variable "repositories" { default = [ "bookstore-quarkus", "bookstore-springboot", - "bookstore-nestjs" + "bookstore-nestjs", + "bookstore-vapor" ] } \ No newline at end of file From 67f6cb4b0935e5c7cb37581a7e9dcc94a788fc02 Mon Sep 17 00:00:00 2001 From: Moritz Ellerbrock Date: Sat, 13 May 2023 17:11:58 +0200 Subject: [PATCH 07/17] change server to align with terraform config --- bookstore-vapor/Sources/App/configure.swift | 3 +++ bookstore-vapor/Sources/Repository/Config.swift | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/bookstore-vapor/Sources/App/configure.swift b/bookstore-vapor/Sources/App/configure.swift index bc18fd4..2099567 100644 --- a/bookstore-vapor/Sources/App/configure.swift +++ b/bookstore-vapor/Sources/App/configure.swift @@ -4,6 +4,9 @@ import Repository // configures your application public func configure(_ app: Application) async throws { + // set port to 3000 + app.http.server.configuration.port = 3000 + // register repository try Repository.configure(app) // add migrations diff --git a/bookstore-vapor/Sources/Repository/Config.swift b/bookstore-vapor/Sources/Repository/Config.swift index d80f76c..81abd89 100644 --- a/bookstore-vapor/Sources/Repository/Config.swift +++ b/bookstore-vapor/Sources/Repository/Config.swift @@ -18,20 +18,20 @@ struct DatabaseConfiguration { let database: String init(_ app: Application) throws { - guard let username = Environment.get("DATABASE_USERNAME") else { + guard let username = Environment.get("DB_USER") else { throw DatabaseConfigurationError.usernameMissing } - guard let password = Environment.get("DATABASE_PASSWORD") else { + guard let password = Environment.get("DB_PASSWORD") else { throw DatabaseConfigurationError.passwordMissing } - let databaseName = app.environment == .testing ? "testing" : Environment.get("DATABASE_NAME") + let databaseName = app.environment == .testing ? "testing" : Environment.get("DB_NAME") guard let databaseName else { throw DatabaseConfigurationError.databaseNameMissing } - self.port = Environment.get("DATABASE_PORT").flatMap(Int.init(_:)) ?? PostgresConfiguration.ianaPortNumber - self.hostname = Environment.get("DATABASE_HOST") ?? "db" + self.port = Environment.get("DB_PORT").flatMap(Int.init(_:)) ?? PostgresConfiguration.ianaPortNumber + self.hostname = Environment.get("DB_HOST") ?? "db" self.username = username self.password = password self.database = databaseName From bf9fb3d5db3dcf6604388e02478a638d96bdbe6e Mon Sep 17 00:00:00 2001 From: Moritz Ellerbrock Date: Fri, 13 Oct 2023 11:50:29 +0200 Subject: [PATCH 08/17] update to swift version 5.9 --- bookstore-vapor/Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookstore-vapor/Package.swift b/bookstore-vapor/Package.swift index 3cf64f9..043e4ea 100644 --- a/bookstore-vapor/Package.swift +++ b/bookstore-vapor/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.7 +// swift-tools-version:5.9 import PackageDescription let package = Package( From 05188b5c803c75d5652cb12875abca1bdd4a2b48 Mon Sep 17 00:00:00 2001 From: Moritz Ellerbrock Date: Fri, 13 Oct 2023 11:50:44 +0200 Subject: [PATCH 09/17] use multistage docker image --- bookstore-vapor/Dockerfile | 46 +++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/bookstore-vapor/Dockerfile b/bookstore-vapor/Dockerfile index 097382a..2eba989 100644 --- a/bookstore-vapor/Dockerfile +++ b/bookstore-vapor/Dockerfile @@ -1,3 +1,47 @@ +# ================================ +# Build image +# ================================ +FROM swift:5.9-focal-slim as build + +# Install OS updates and, if needed, sqlite3 +RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ + && apt-get -q update \ + && apt-get -q dist-upgrade -y\ + && rm -rf /var/lib/apt/lists/* + +# Set up a build area +WORKDIR /build + +# First just resolve dependencies. +# This creates a cached layer that can be reused +# as long as your Package.swift/Package.resolved +# files do not change. +COPY ./Package.* ./ +RUN swift package resolve + +# Copy entire repo into container +COPY . . + +# Build everything, with optimizations +RUN swift build -c release --static-swift-stdlib + +# Switch to the staging area +WORKDIR /staging + +# Copy main executable to staging area +RUN cp "$(swift build --package-path /build -c release --show-bin-path)/Run" ./ + +# Copy resources bundled by SPM to staging area +RUN find -L "$(swift build --package-path /build -c release --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \; + +# Copy any resources from the public directory and views directory if the directories exist +# Ensure that by default, neither the directory nor any of its contents are writable. +RUN [ -d /build/Public ] && { mv /build/Public ./Public && chmod -R a-w ./Public; } || true +RUN [ -d /build/Resources ] && { mv /build/Resources ./Resources && chmod -R a-w ./Resources; } || true + +# ================================ +# Run image +# ================================ FROM ubuntu:jammy # Make sure all system packages are up to date, and install only essential packages. @@ -20,7 +64,7 @@ RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app WORKDIR /app # Copy built executable and any staged resources from builder -COPY ./staging /app +COPY --from=build --chown=vapor:vapor /staging /app # Ensure all further commands run as the vapor user USER vapor:vapor From 7a0268fa7d6a642d0eed47a998bf81ab7f102a80 Mon Sep 17 00:00:00 2001 From: Moritz Ellerbrock Date: Fri, 13 Oct 2023 11:50:57 +0200 Subject: [PATCH 10/17] update dependencies --- bookstore-vapor/Package.resolved | 105 ++++++++++++++----------------- 1 file changed, 48 insertions(+), 57 deletions(-) diff --git a/bookstore-vapor/Package.resolved b/bookstore-vapor/Package.resolved index 59f0ac9..8e7bcf0 100644 --- a/bookstore-vapor/Package.resolved +++ b/bookstore-vapor/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swift-server/async-http-client.git", "state" : { - "revision" : "864c8d9e0ead5de7ba70b61c8982f89126710863", - "version" : "1.15.0" + "revision" : "16f7e62c08c6969899ce6cc277041e868364e5cf", + "version" : "1.19.0" } }, { @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/async-kit.git", "state" : { - "revision" : "9acea4c92f51a5885c149904f0d11db4712dda80", - "version" : "1.16.0" + "revision" : "eab9edff78e8ace20bd7cb6e792ab46d54f59ab9", + "version" : "1.18.0" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/console-kit.git", "state" : { - "revision" : "447f1046fb4e9df40973fe426ecb24a6f0e8d3b4", - "version" : "4.6.0" + "revision" : "ccd0773b3ad3c67a19918aaef6903678592bb087", + "version" : "4.9.0" } }, { @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/fluent.git", "state" : { - "revision" : "8d6015096200b4c2f17b884eb966defc42a2ad1d", - "version" : "4.7.1" + "revision" : "4b4d8bf15a06fd60137e9c543e5503c4b842654e", + "version" : "4.8.0" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/fluent-kit.git", "state" : { - "revision" : "21d99b0c66cf86867a779bac831d800aac22f5e6", - "version" : "1.41.0" + "revision" : "e0bb2b060249b7a501249b1612807b2eaaec28c6", + "version" : "1.45.0" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/fluent-postgres-driver.git", "state" : { - "revision" : "f2b084417472e6a153d555905796f0ef0bb6fc37", - "version" : "2.5.1" + "revision" : "a538fc647f82d915eb84e0a12ca9b08c513e57c4", + "version" : "2.8.0" } }, { @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/multipart-kit.git", "state" : { - "revision" : "3a31859efeb054cdcd407fe152802ddc5c58bbce", - "version" : "4.5.3" + "revision" : "1adfd69df2da08f7931d4281b257475e32c96734", + "version" : "4.5.4" } }, { @@ -68,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/postgres-kit.git", "state" : { - "revision" : "8ea9274633374d01bdef10da3206db74f3e261d6", - "version" : "2.9.1" + "revision" : "80ab7737dac4fccd4a8ad38743828dcb71ba7ac8", + "version" : "2.12.2" } }, { @@ -77,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/postgres-nio.git", "state" : { - "revision" : "5d93f3e05f0493441ad46f6d7a76109ff685329c", - "version" : "1.13.0" + "revision" : "abca6b390235ae337999d367c40cc40c99629385", + "version" : "1.18.1" } }, { @@ -86,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/routing-kit.git", "state" : { - "revision" : "bdc9c25adbf77ba2b02113077ae8f355d87df83e", - "version" : "4.7.1" + "revision" : "e0539da5b60a60d7381f44cdcf04036f456cee2f", + "version" : "4.8.0" } }, { @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/sql-kit.git", "state" : { - "revision" : "fcc29f543b3de7b661cbe7540805974234cb9740", - "version" : "3.24.0" + "revision" : "b2f128cb62a3abfbb1e3b2893ff3ee69e70f4f0f", + "version" : "3.28.0" } }, { @@ -104,8 +104,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-algorithms.git", "state" : { - "revision" : "b14b7f4c528c942f121c8b860b9410b2bf57825e", - "version" : "1.0.0" + "revision" : "bcd4f369ac962bc3e5244c9df778739f8f5bdbf1", + "version" : "1.1.0" } }, { @@ -113,17 +113,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-atomics.git", "state" : { - "revision" : "ff3d2212b6b093db7f177d0855adbc4ef9c5f036", - "version" : "1.0.3" - } - }, - { - "identity" : "swift-backtrace", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swift-server/swift-backtrace.git", - "state" : { - "revision" : "f25620d5d05e2f1ba27154b40cafea2b67566956", - "version" : "1.3.3" + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" } }, { @@ -131,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", - "version" : "1.0.4" + "revision" : "a902f1823a7ff3c9ab2fba0f992396b948eda307", + "version" : "1.0.5" } }, { @@ -140,8 +131,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-crypto.git", "state" : { - "revision" : "da0fe44138ab86e380f40a2acbd8a611b07d3f64", - "version" : "2.4.0" + "revision" : "60f13f60c4d093691934dc6cfdf5f508ada1f894", + "version" : "2.6.0" } }, { @@ -149,8 +140,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "32e8d724467f8fe623624570367e3d50c5638e46", - "version" : "1.5.2" + "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", + "version" : "1.5.3" } }, { @@ -158,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-metrics.git", "state" : { - "revision" : "e8bced74bc6d747745935e469f45d03f048d6cbd", - "version" : "2.3.4" + "revision" : "971ba26378ab69c43737ee7ba967a896cb74c0d1", + "version" : "2.4.1" } }, { @@ -167,8 +158,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "9b2848d76f5caad08b97e71a04345aa5bdb23a06", - "version" : "2.49.0" + "revision" : "3db5c4aeee8100d2db6f1eaf3864afdad5dc68fd", + "version" : "2.59.0" } }, { @@ -176,8 +167,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-extras.git", "state" : { - "revision" : "cc1e5275079380c859417dbea8588531f1a90ec3", - "version" : "1.18.0" + "revision" : "fb70a0f5e984f23be48b11b4f1909f3bee016178", + "version" : "1.19.1" } }, { @@ -185,8 +176,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-http2.git", "state" : { - "revision" : "38feec96bcd929028939107684073554bf01abeb", - "version" : "1.25.2" + "revision" : "9c22e4f810ce780453f563fba98e1a1039f83d56", + "version" : "1.28.1" } }, { @@ -194,8 +185,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-ssl.git", "state" : { - "revision" : "4fb7ead803e38949eb1d6fabb849206a72c580f3", - "version" : "2.23.0" + "revision" : "320bd978cceb8e88c125dcbb774943a92f6286e9", + "version" : "2.25.0" } }, { @@ -203,8 +194,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-transport-services.git", "state" : { - "revision" : "c0d9a144cfaec8d3d596aadde3039286a266c15c", - "version" : "1.15.0" + "revision" : "e7403c35ca6bb539a7ca353b91cc2d8ec0362d58", + "version" : "1.19.0" } }, { @@ -221,8 +212,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/vapor.git", "state" : { - "revision" : "f39218ec4998654c59251b282cbf4bb9b2cf2629", - "version" : "4.74.2" + "revision" : "288d73a368ac0d0e228dd9b81eecc576b50b9d6d", + "version" : "4.84.6" } }, { @@ -230,8 +221,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/websocket-kit.git", "state" : { - "revision" : "2b8885974e8d9f522e787805000553f4f7cce8a0", - "version" : "2.7.0" + "revision" : "53fe0639a98903858d0196b699720decb42aee7b", + "version" : "2.14.0" } } ], From cc554673f1aecebfb07553d45547b853c5026241 Mon Sep 17 00:00:00 2001 From: Moritz Ellerbrock Date: Fri, 13 Oct 2023 11:55:18 +0200 Subject: [PATCH 11/17] remove unecessary steps in github workflow --- .github/workflows/publish-vapor-image.yml | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/.github/workflows/publish-vapor-image.yml b/.github/workflows/publish-vapor-image.yml index b119714..8075b92 100644 --- a/.github/workflows/publish-vapor-image.yml +++ b/.github/workflows/publish-vapor-image.yml @@ -34,23 +34,6 @@ jobs: - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@261a7de32bda11ba01f4d75c4ed6caf3739e54be - - - name: Resolve Dependencies - run: swift package resolve - working-directory: ./bookstore-vapor - - - name: Build - run: swift build -c release -Xswiftc -O --static-swift-stdlib - working-directory: ./bookstore-vapor - - - name: Create staging directory - run: | - mkdir staging - cp "$(swift build -c release --show-bin-path)/Run" ./staging - find -L "$(swift build -c release --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./staging/ \; - [ -d ./Public ] && { mv ./Public ./staging/Public && chmod -R a-w ./staging/Public; } || true - [ -d ./Resources ] && { mv ./Resources ./staging/Resources && chmod -R a-w ./staging/Resources; } || true - working-directory: ./bookstore-vapor - name: Build, tag, and push image to Amazon ECR env: From 28e727dd692f882947f16e7b0888575709b6926f Mon Sep 17 00:00:00 2001 From: Moritz Ellerbrock Date: Sun, 15 Oct 2023 16:10:59 +0200 Subject: [PATCH 12/17] fix multistage docker image --- bookstore-vapor/.dockerignore | 2 ++ bookstore-vapor/Dockerfile | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/bookstore-vapor/.dockerignore b/bookstore-vapor/.dockerignore index 2d9f16e..07a3bb3 100644 --- a/bookstore-vapor/.dockerignore +++ b/bookstore-vapor/.dockerignore @@ -1,2 +1,4 @@ .build/ .swiftpm/ +.derivedData +DerivedData \ No newline at end of file diff --git a/bookstore-vapor/Dockerfile b/bookstore-vapor/Dockerfile index 2eba989..a1f2381 100644 --- a/bookstore-vapor/Dockerfile +++ b/bookstore-vapor/Dockerfile @@ -1,7 +1,7 @@ # ================================ # Build image # ================================ -FROM swift:5.9-focal-slim as build +FROM swift:5.9.0-slim as build # Install OS updates and, if needed, sqlite3 RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ From 1d3b1ef175b11a79f0a5ff2111fd15f1f52e487d Mon Sep 17 00:00:00 2001 From: Moritz Ellerbrock Date: Sun, 15 Oct 2023 16:12:51 +0200 Subject: [PATCH 13/17] use correct swift image --- bookstore-vapor/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookstore-vapor/Dockerfile b/bookstore-vapor/Dockerfile index a1f2381..8684135 100644 --- a/bookstore-vapor/Dockerfile +++ b/bookstore-vapor/Dockerfile @@ -1,7 +1,7 @@ # ================================ # Build image # ================================ -FROM swift:5.9.0-slim as build +FROM swift:5.9.0 as build # Install OS updates and, if needed, sqlite3 RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ From f521cae5ec833b5918174dd065cd70e5fdd41695 Mon Sep 17 00:00:00 2001 From: Moritz Ellerbrock Date: Fri, 12 Apr 2024 13:51:56 +0200 Subject: [PATCH 14/17] use slim image for docker container --- bookstore-vapor/Dockerfile | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/bookstore-vapor/Dockerfile b/bookstore-vapor/Dockerfile index 8684135..f97ee80 100644 --- a/bookstore-vapor/Dockerfile +++ b/bookstore-vapor/Dockerfile @@ -1,12 +1,11 @@ # ================================ # Build image # ================================ -FROM swift:5.9.0 as build +FROM swift:5.9-slim as build # Install OS updates and, if needed, sqlite3 RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ && apt-get -q update \ - && apt-get -q dist-upgrade -y\ && rm -rf /var/lib/apt/lists/* # Set up a build area @@ -17,13 +16,17 @@ WORKDIR /build # as long as your Package.swift/Package.resolved # files do not change. COPY ./Package.* ./ -RUN swift package resolve +RUN swift package resolve --skip-update \ + "$([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true)" # Copy entire repo into container COPY . . # Build everything, with optimizations -RUN swift build -c release --static-swift-stdlib +RUN swift build -c release --static-swift-stdlib \ + # Workaround for https://github.com/apple/swift/pull/68669 + # This can be removed as soon as 5.9.1 is released, but is harmless if left in. + -Xlinker -u -Xlinker _swift_backtrace_isThunkFunction # Switch to the staging area WORKDIR /staging @@ -50,9 +53,8 @@ RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ && apt-get -q install -y \ ca-certificates \ tzdata \ - libc6 \ # If your app or its dependencies import FoundationNetworking, also install `libcurl4`. - libcurl4 \ + # libcurl4 \ # If your app or its dependencies import FoundationXML, also install `libxml2`. # libxml2 \ && rm -r /var/lib/apt/lists/* @@ -66,6 +68,9 @@ WORKDIR /app # Copy built executable and any staged resources from builder COPY --from=build --chown=vapor:vapor /staging /app +# Provide configuration needed by the built-in crash reporter and some sensible default behaviors. +ENV SWIFT_ROOT=/usr SWIFT_BACKTRACE=enable=yes,sanitize=yes,threads=all,images=all,interactive=no + # Ensure all further commands run as the vapor user USER vapor:vapor From 337c7c81f9c6548c299fbc4f8e2f07fb17edc285 Mon Sep 17 00:00:00 2001 From: Moritz Ellerbrock Date: Fri, 12 Apr 2024 22:27:43 +0200 Subject: [PATCH 15/17] update swift --- bookstore-vapor/Dockerfile | 2 +- bookstore-vapor/Package.resolved | 119 ++++++++++++------ bookstore-vapor/Package.swift | 8 +- .../Sources/Repository/Config.swift | 19 +-- 4 files changed, 95 insertions(+), 53 deletions(-) diff --git a/bookstore-vapor/Dockerfile b/bookstore-vapor/Dockerfile index f97ee80..9f0c41d 100644 --- a/bookstore-vapor/Dockerfile +++ b/bookstore-vapor/Dockerfile @@ -1,7 +1,7 @@ # ================================ # Build image # ================================ -FROM swift:5.9-slim as build +FROM swift:5.10-slim as build # Install OS updates and, if needed, sqlite3 RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ diff --git a/bookstore-vapor/Package.resolved b/bookstore-vapor/Package.resolved index 8e7bcf0..ab6be6c 100644 --- a/bookstore-vapor/Package.resolved +++ b/bookstore-vapor/Package.resolved @@ -1,12 +1,13 @@ { + "originHash" : "cde2226e8c9b5a5d8d0d6049152bdfd562a8dcf76197cb05b9a5db0febf63722", "pins" : [ { "identity" : "async-http-client", "kind" : "remoteSourceControl", "location" : "https://github.com/swift-server/async-http-client.git", "state" : { - "revision" : "16f7e62c08c6969899ce6cc277041e868364e5cf", - "version" : "1.19.0" + "revision" : "fb308ee72f3d4c082a507033f94afa7395963ef3", + "version" : "1.21.0" } }, { @@ -14,8 +15,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/async-kit.git", "state" : { - "revision" : "eab9edff78e8ace20bd7cb6e792ab46d54f59ab9", - "version" : "1.18.0" + "revision" : "7ece208cd401687641c88367a00e3ea2b04311f1", + "version" : "1.19.0" } }, { @@ -23,8 +24,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/console-kit.git", "state" : { - "revision" : "ccd0773b3ad3c67a19918aaef6903678592bb087", - "version" : "4.9.0" + "revision" : "a31f44ebfbd15a2cc0fda705279676773ac16355", + "version" : "4.14.1" } }, { @@ -32,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/fluent.git", "state" : { - "revision" : "4b4d8bf15a06fd60137e9c543e5503c4b842654e", - "version" : "4.8.0" + "revision" : "a586a5d4164f23a0ee4e02e1f467b9bbef0c9f1c", + "version" : "4.9.0" } }, { @@ -41,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/fluent-kit.git", "state" : { - "revision" : "e0bb2b060249b7a501249b1612807b2eaaec28c6", - "version" : "1.45.0" + "revision" : "5f0938a3f5f1a751ff7a411117bfce4efe713526", + "version" : "1.47.2" } }, { @@ -59,8 +60,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/multipart-kit.git", "state" : { - "revision" : "1adfd69df2da08f7931d4281b257475e32c96734", - "version" : "4.5.4" + "revision" : "12ee56f25bd3fc4c2d09c2aa16e69de61dc786e8", + "version" : "4.6.0" } }, { @@ -68,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/postgres-kit.git", "state" : { - "revision" : "80ab7737dac4fccd4a8ad38743828dcb71ba7ac8", - "version" : "2.12.2" + "revision" : "e26763a6cb8d852f7ce01b1cd5925b3d8d084801", + "version" : "2.13.1" } }, { @@ -77,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/postgres-nio.git", "state" : { - "revision" : "abca6b390235ae337999d367c40cc40c99629385", - "version" : "1.18.1" + "revision" : "e345cbb9cf6052b37b27c0c4f976134fc01dbe15", + "version" : "1.21.1" } }, { @@ -86,8 +87,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/routing-kit.git", "state" : { - "revision" : "e0539da5b60a60d7381f44cdcf04036f456cee2f", - "version" : "4.8.0" + "revision" : "2a92a7eac411a82fb3a03731be5e76773ebe1b3e", + "version" : "4.9.0" } }, { @@ -104,8 +105,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-algorithms.git", "state" : { - "revision" : "bcd4f369ac962bc3e5244c9df778739f8f5bdbf1", - "version" : "1.1.0" + "revision" : "f6919dfc309e7f1b56224378b11e28bab5bccc42", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-async-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-async-algorithms.git", + "state" : { + "revision" : "da4e36f86544cdf733a40d59b3a2267e3a7bbf36", + "version" : "1.0.0" } }, { @@ -122,8 +132,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections.git", "state" : { - "revision" : "a902f1823a7ff3c9ab2fba0f992396b948eda307", - "version" : "1.0.5" + "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", + "version" : "1.1.0" } }, { @@ -131,8 +141,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-crypto.git", "state" : { - "revision" : "60f13f60c4d093691934dc6cfdf5f508ada1f894", - "version" : "2.6.0" + "revision" : "f0525da24dc3c6cbb2b6b338b65042bc91cbc4bb", + "version" : "3.3.0" + } + }, + { + "identity" : "swift-http-types", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-http-types", + "state" : { + "revision" : "12358d55a3824bd5fed310b999ea8cf83a9a1a65", + "version" : "1.0.3" } }, { @@ -140,8 +159,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "532d8b529501fb73a2455b179e0bbb6d49b652ed", - "version" : "1.5.3" + "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", + "version" : "1.5.4" } }, { @@ -158,8 +177,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "3db5c4aeee8100d2db6f1eaf3864afdad5dc68fd", - "version" : "2.59.0" + "revision" : "fc63f0cf4e55a4597407a9fc95b16a2bc44b4982", + "version" : "2.64.0" } }, { @@ -167,8 +186,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-extras.git", "state" : { - "revision" : "fb70a0f5e984f23be48b11b4f1909f3bee016178", - "version" : "1.19.1" + "revision" : "a3b640d7dc567225db7c94386a6e71aded1bfa63", + "version" : "1.22.0" } }, { @@ -176,8 +195,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-http2.git", "state" : { - "revision" : "9c22e4f810ce780453f563fba98e1a1039f83d56", - "version" : "1.28.1" + "revision" : "0904bf0feb5122b7e5c3f15db7df0eabe623dd87", + "version" : "1.30.0" } }, { @@ -185,8 +204,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-ssl.git", "state" : { - "revision" : "320bd978cceb8e88c125dcbb774943a92f6286e9", - "version" : "2.25.0" + "revision" : "7c381eb6083542b124a6c18fae742f55001dc2b5", + "version" : "2.26.0" } }, { @@ -194,8 +213,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio-transport-services.git", "state" : { - "revision" : "e7403c35ca6bb539a7ca353b91cc2d8ec0362d58", - "version" : "1.19.0" + "revision" : "6cbe0ed2b394f21ab0d46b9f0c50c6be964968ce", + "version" : "1.20.1" } }, { @@ -207,13 +226,31 @@ "version" : "1.0.2" } }, + { + "identity" : "swift-service-lifecycle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/swift-service-lifecycle.git", + "state" : { + "revision" : "d7fe0e731499a8dcce53bf4cbbc812c8e565d3a7", + "version" : "2.4.1" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "025bcb1165deab2e20d4eaba79967ce73013f496", + "version" : "1.2.1" + } + }, { "identity" : "vapor", "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/vapor.git", "state" : { - "revision" : "288d73a368ac0d0e228dd9b81eecc576b50b9d6d", - "version" : "4.84.6" + "revision" : "8409c3c296e7f8965df14e906dbcd44eb68f54a9", + "version" : "4.92.6" } }, { @@ -221,10 +258,10 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/websocket-kit.git", "state" : { - "revision" : "53fe0639a98903858d0196b699720decb42aee7b", - "version" : "2.14.0" + "revision" : "4232d34efa49f633ba61afde365d3896fc7f8740", + "version" : "2.15.0" } } ], - "version" : 2 + "version" : 3 } diff --git a/bookstore-vapor/Package.swift b/bookstore-vapor/Package.swift index 043e4ea..ef39db7 100644 --- a/bookstore-vapor/Package.swift +++ b/bookstore-vapor/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.9 +// swift-tools-version:5.10 import PackageDescription let package = Package( @@ -7,9 +7,9 @@ let package = Package( .macOS(.v13) ], dependencies: [ - .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"), - .package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"), - .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.0.0"), + .package(url: "https://github.com/vapor/vapor.git", exact: "4.92.6"), + .package(url: "https://github.com/vapor/fluent.git", exact: "4.9.0"), + .package(url: "https://github.com/vapor/fluent-postgres-driver.git", exact: "2.8.0"), ], targets: [ .target( diff --git a/bookstore-vapor/Sources/Repository/Config.swift b/bookstore-vapor/Sources/Repository/Config.swift index 81abd89..b00e274 100644 --- a/bookstore-vapor/Sources/Repository/Config.swift +++ b/bookstore-vapor/Sources/Repository/Config.swift @@ -30,21 +30,26 @@ struct DatabaseConfiguration { throw DatabaseConfigurationError.databaseNameMissing } - self.port = Environment.get("DB_PORT").flatMap(Int.init(_:)) ?? PostgresConfiguration.ianaPortNumber + self.port = Environment.get("DB_PORT").flatMap(Int.init(_:)) ?? SQLPostgresConfiguration.ianaPortNumber self.hostname = Environment.get("DB_HOST") ?? "db" self.username = username self.password = password self.database = databaseName } + var config: SQLPostgresConfiguration { + let tls: PostgresConnection.Configuration.TLS = .disable + return .init(hostname: hostname, + port: port, + username: username, + password: password, + database: database, + tls: tls) + } + static func setup(in app: Application) throws { let dbConfig = try DatabaseConfiguration(app) - let factoryConfig: DatabaseConfigurationFactory = .postgres(hostname: dbConfig.hostname, - port: dbConfig.port, - username: dbConfig.username, - password: dbConfig.password, - database: dbConfig.database) - + let factoryConfig: DatabaseConfigurationFactory = .postgres(configuration: dbConfig.config) app.databases.use(factoryConfig, as: .psql) } } From e49475518fa350947b24f7a233b232627ff35d15 Mon Sep 17 00:00:00 2001 From: Moritz Ellerbrock Date: Sat, 20 Jul 2024 19:01:46 +0200 Subject: [PATCH 16/17] update project --- bookstore-vapor/Package.resolved | 30 ++-- bookstore-vapor/Package.swift | 9 +- bookstore-vapor/Sources/Common/Secrets.swift | 149 +++++++++++++++++++ 3 files changed, 170 insertions(+), 18 deletions(-) create mode 100644 bookstore-vapor/Sources/Common/Secrets.swift diff --git a/bookstore-vapor/Package.resolved b/bookstore-vapor/Package.resolved index ab6be6c..463f70f 100644 --- a/bookstore-vapor/Package.resolved +++ b/bookstore-vapor/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "cde2226e8c9b5a5d8d0d6049152bdfd562a8dcf76197cb05b9a5db0febf63722", + "originHash" : "5774f338d58f9ed20229f16a661a3e0025751fb8a6a059403586af71997fe136", "pins" : [ { "identity" : "async-http-client", @@ -33,8 +33,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/fluent.git", "state" : { - "revision" : "a586a5d4164f23a0ee4e02e1f467b9bbef0c9f1c", - "version" : "4.9.0" + "revision" : "dfcbeba27a576c20ff181d496f21ecd45d2c1a71", + "version" : "4.11.0" } }, { @@ -42,8 +42,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/fluent-kit.git", "state" : { - "revision" : "5f0938a3f5f1a751ff7a411117bfce4efe713526", - "version" : "1.47.2" + "revision" : "d69efce21242ad4dba6935cc1b8d5637281604d5", + "version" : "1.48.5" } }, { @@ -51,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/fluent-postgres-driver.git", "state" : { - "revision" : "a538fc647f82d915eb84e0a12ca9b08c513e57c4", - "version" : "2.8.0" + "revision" : "e2988a8c960196eca2891f3a0bb1caad9044e7ea", + "version" : "2.9.2" } }, { @@ -69,8 +69,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/postgres-kit.git", "state" : { - "revision" : "e26763a6cb8d852f7ce01b1cd5925b3d8d084801", - "version" : "2.13.1" + "revision" : "0b72fa83b1023c4b82072e4049a3db6c29781fff", + "version" : "2.13.5" } }, { @@ -96,8 +96,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/sql-kit.git", "state" : { - "revision" : "b2f128cb62a3abfbb1e3b2893ff3ee69e70f4f0f", - "version" : "3.28.0" + "revision" : "f697d3289c628acd241e3b2c7d3ff068adcc52d1", + "version" : "3.31.1" } }, { @@ -177,8 +177,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-nio.git", "state" : { - "revision" : "fc63f0cf4e55a4597407a9fc95b16a2bc44b4982", - "version" : "2.64.0" + "revision" : "e5a216ba89deba84356bad9d4c2eab99071c745b", + "version" : "2.67.0" } }, { @@ -249,8 +249,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/vapor.git", "state" : { - "revision" : "8409c3c296e7f8965df14e906dbcd44eb68f54a9", - "version" : "4.92.6" + "revision" : "ebbe71c89aa1b76a0920277760b12be7a2ec7c70", + "version" : "4.102.0" } }, { diff --git a/bookstore-vapor/Package.swift b/bookstore-vapor/Package.swift index ef39db7..afc9069 100644 --- a/bookstore-vapor/Package.swift +++ b/bookstore-vapor/Package.swift @@ -7,9 +7,9 @@ let package = Package( .macOS(.v13) ], dependencies: [ - .package(url: "https://github.com/vapor/vapor.git", exact: "4.92.6"), - .package(url: "https://github.com/vapor/fluent.git", exact: "4.9.0"), - .package(url: "https://github.com/vapor/fluent-postgres-driver.git", exact: "2.8.0"), + .package(url: "https://github.com/vapor/vapor.git", exact: "4.102.0"), + .package(url: "https://github.com/vapor/fluent.git", exact: "4.11.0"), + .package(url: "https://github.com/vapor/fluent-postgres-driver.git", exact: "2.9.2"), ], targets: [ .target( @@ -18,6 +18,7 @@ let package = Package( .product(name: "Fluent", package: "fluent"), .product(name: "Vapor", package: "vapor"), .target(name: "Repository"), + .target(name: "Common"), ], swiftSettings: [ .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release)) @@ -32,6 +33,8 @@ let package = Package( .product(name: "Vapor", package: "vapor"), ] ), + .target(name: "Common", + path: "Sources/Common"), // Testing targets .testTarget(name: "AppTests", dependencies: [ .target(name: "App"), diff --git a/bookstore-vapor/Sources/Common/Secrets.swift b/bookstore-vapor/Sources/Common/Secrets.swift new file mode 100644 index 0000000..7eb85b5 --- /dev/null +++ b/bookstore-vapor/Sources/Common/Secrets.swift @@ -0,0 +1,149 @@ +// +// File.swift +// +// +// Created by Moritz Ellerbrock on 30.06.24. +// + +import Foundation + +public enum MandatorySecretError: Error { + case setupMissing(String) + case decryptionFailed(String) +} + +public enum FeauterSecretError: Error { + case setupMissing(String) + case decryptionFailed(String) +} + +@dynamicMemberLookup +public struct Secrets: Encodable { + // mandatory + public let dbUsername = "DATABASE_USERNAME" + public let dbPassword = "DATABASE_PASSWORD" + public let dbName = "DATABASE_NAME" // newsletter + public let dbPort = "DATABASE_PORT" // 5432 + public let dbHost = "DATABASE_HOST" // db + + // Encodable + private enum CodingKeys: String, CodingKey { + case dbUsername, dbPassword, dbName, dbPort, dbHost + } + + private static let mandatory: [String] = { + [ + "DATABASE_USERNAME", + "DATABASE_PASSWORD", + "DATABASE_NAME", + "DATABASE_PORT", + "DATABASE_HOST", + ] + }() + + private func isMandatory(_ key: String) -> Bool { + Self.mandatory.contains(key) + } + + private func storedValue(_ value: String) -> String? { + if value == dbHost { + return Environment.get(value) ?? "localhost" + } else if value == dbName { + return Environment.get(value) ?? "postgres" + } else if value == dbUsername { + return Environment.get(value) ?? "postgres" + } else if value == dbPassword { + return Environment.get(value) ?? "My unsafe Secret" + } else if value == dbPort { + return Environment.get(value) ?? "5432" + } else { + return Environment.get(value) + } + } + + public static subscript(dynamicMember keyPath: KeyPath) -> T { + let obj = Secrets() + let key = obj[keyPath: keyPath] as! String + let secret = obj.storedValue(key)! + return secret as! T + } + + public static func verifyValue(for keyPath: KeyPath) -> Bool { + let obj = Secrets() + let key = obj[keyPath: keyPath] as! String + return obj.storedValue(key) != nil + } + + public static func verifySetup() throws { + let object = Secrets() + let dict = object.toDict() + for key in dict.values { + if object.isMandatory(key) { + guard let _ = object.storedValue(key) else { + let error = MandatorySecretError.setupMissing(key) + throw error + } + } else { + do { + guard let _ = object.storedValue(key) else { + throw FeauterSecretError.setupMissing(key) + } + } catch { + print(String(reflecting: error)) + } + } + } + } +} + + +enum Environment { + case testing, develop, staging, production + + static func getEnvironment() -> Environment { + environment(from: Self.get("environment")) + } + + private static func environment(from text: String?, fallback: Environment = .develop) -> Environment { + guard let environment = text?.lowercased() else { return fallback } + if environment.hasPrefix("dev") { + return .develop + } else if environment.hasPrefix("stag") { + return .staging + } else if environment.hasPrefix("prod") { + return .production + } else if environment.hasPrefix("test") { + return .production + } else { + return .develop + } + } +} + +extension Environment { + static func get(_ key: String) -> String? { + let env = ProcessInfo.processInfo.environment + return env[key] + } +} + +extension Encodable { + func toDict() -> [String: String] { + guard let data = try? JSONEncoder().encode(self), + let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []), + let dictionary = jsonObject as? [String: Any] + else { + return [:] + } + + var stringDict: [String: String] = [:] + for (key, value) in dictionary { + if let stringValue = value as? String { + stringDict[key] = stringValue + } else { + stringDict[key] = "\(value)" + } + } + return stringDict + } +} From 30722d9d261125912ef7e3ffa9b536a53739d65d Mon Sep 17 00:00:00 2001 From: Moritz Ellerbrock Date: Sat, 20 Jul 2024 19:19:44 +0200 Subject: [PATCH 17/17] update github action --- .github/workflows/publish-vapor-image.yml | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/.github/workflows/publish-vapor-image.yml b/.github/workflows/publish-vapor-image.yml index 8075b92..72988f0 100644 --- a/.github/workflows/publish-vapor-image.yml +++ b/.github/workflows/publish-vapor-image.yml @@ -1,7 +1,7 @@ name: Publish Vapor image to ECR on: - pull_request: + push: branches: - main @@ -14,33 +14,20 @@ jobs: - name: Checkout code uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 - - name: Docker meta - id: meta - uses: docker/metadata-action@57396166ad8aefe6098280995947635806a0e6ea - with: - images: | - name=bookstore-vapor - tags: | - # minimal (short sha) - type=sha - - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v1-node16 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: eu-west-1 - - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@261a7de32bda11ba01f4d75c4ed6caf3739e54be - - name: Build, tag, and push image to Amazon ECR env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} ECR_REPOSITORY: bookstore-vapor - IMAGE_TAGS: ${{ steps.meta.outputs.tags }} run: | - docker build -t $ECR_REGISTRY/$IMAGE_TAGS -t $ECR_REGISTRY/$ECR_REPOSITORY . + docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$GITHUB_SHA -t $ECR_REGISTRY/$ECR_REPOSITORY . docker push -a $ECR_REGISTRY/$ECR_REPOSITORY working-directory: ./bookstore-vapor