diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index fe635a0..3c56d68 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -6,7 +6,7 @@ "--security-opt", "seccomp=unconfined" ], "appPort": [ - 8080 + 80 ], "extensions": [ "pvasek.sourcekit-lsp--dev-unofficial", diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..7fe840d --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,25 @@ +# *Name of the PR* + +## :recycle: Current situation +*Describe the current situation (if possible with and exemplary (or real) code snippet and/or where this is used)* + +## :bulb: Proposed solution +*Describe the solution (if possible with and exemplary (or real) code snippet)* + +### Problem that is solved +*Provide a description and link issues that are solved* + +### Implications +*Describe the implications, e.g. refactoring* + +## :heavy_plus_sign: Additional Information +*Provide some additional information if possible* + +### Related PRs +*Reference the related PRs* + +### Testing +*Are there tests included? If yes, which situations are tested and which corner cases are missing?* + +### Reviewer Nudging +*Where should the reviewer start, where is a good entry point?* diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..b20e968 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,20 @@ +branches: [release] +name-template: '$NEXT_PATCH_VERSION' +tag-template: '$NEXT_PATCH_VERSION' +categories: + - title: '🚀 Features' + labels: + - 'feature' + - 'enhancement' + - title: '🐛 Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' + - title: '🧰 Maintenance' + label: 'chore' +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +template: | + ## Changes + + $CHANGES diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 1f31c51..341e525 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -4,47 +4,45 @@ on: push: branches: - develop - - release pull_request: branches: - develop - - release jobs: - linux: - name: Linux ${{ matrix.linux }} ${{ matrix.configuration }} - container: - image: swift:5.3-${{ matrix.linux }} + macoswebservices: + name: macOS + runs-on: macos-11 + steps: + - uses: actions/checkout@v2 + - uses: maxim-lobanov/setup-xcode@v1.1 + with: + xcode-version: latest + - name: Check Xcode version + run: xcodebuild -version + - name: Check Swift version + run: swift --version + - name: Build and test + run: swift test + linuxwebservices: + name: Linux ${{ matrix.linux }} runs-on: ubuntu-latest + container: + image: swift:${{ matrix.linux }} strategy: - fail-fast: false matrix: - linux: [bionic, xenial, focal, amazonlinux2, centos8] - configuration: [debug, release, release_testing] + linux: [focal, bionic, amazonlinux2, centos8] steps: - uses: actions/checkout@v2 - - name: Install libsqlite3, lsof and zsh + - name: Install libsqlite3 if: ${{ !(startsWith( matrix.linux, 'centos' ) || startsWith( matrix.linux, 'amazonlinux' )) }} - run: apt-get update && apt-get install -y --no-install-recommends libsqlite3-dev lsof zsh + run: apt-get update && apt-get install -y --no-install-recommends libsqlite3-dev - name: Install libsqlite3 if: startsWith( matrix.linux, 'amazonlinux' ) - run: yum update -y && yum install -y sqlite-devel lsof zsh + run: yum update -y && yum install -y sqlite-devel - name: Install libsqlite3 if: startsWith( matrix.linux, 'centos' ) - run: yum update -y --nobest && yum install -y sqlite-devel lsof zsh - - uses: actions/cache@v2 - with: - path: .build - key: ${{ runner.os }}-${{matrix.linux}}-spm-${{ hashFiles('Package.resolved') }} + run: yum update -y --nobest && yum install -y sqlite-devel - name: Check Swift version run: swift --version - - - name: Release Build # Ensuring release build runs successfully without -enable-testing flag - if: matrix.configuration == 'release' - run: swift build -c release - - name: Release Build & Test - if: matrix.configuration == 'release_testing' - run: swift test -c release --enable-test-discovery -Xswiftc -enable-testing -Xswiftc -DRELEASE_TESTING - - name: Debug Build & Test - if: matrix.configuration == 'debug' - run: swift test -c debug --enable-test-discovery \ No newline at end of file + - name: Build and test + run: swift test -Xswiftc -Xfrontend -Xswiftc -sil-verify-none diff --git a/.github/workflows/docker-compose.yml b/.github/workflows/docker-compose.yml new file mode 100644 index 0000000..ee092ee --- /dev/null +++ b/.github/workflows/docker-compose.yml @@ -0,0 +1,26 @@ +name: Build Docker Compose + +on: + push: + branches: + - develop + pull_request: + branches: + - develop + +jobs: + dockercompose: + name: Docker Compose Build and Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Start containers + run: docker-compose -f docker-compose-development.yml up -d --build + - name: Test web service + run: | + sleep 3 + curl --fail http://localhost/v1/ + - name: Stop containers + if: always() + run: docker-compose down diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..66875fa --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,41 @@ +name: Build Docker Image + +on: + push: + tags: + - '*.*.*' + +jobs: + docker: + name: Docker Build and Push Image + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + - name: Get latest tag + id: latesttag + uses: WyriHaximus/github-action-get-previous-tag@v1 + with: + fallback: latest + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - name: Set up docker buildx + uses: docker/setup-buildx-action@v1 + with: + install: true + - name: Log in to the container registry + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push docker image + id: buildandpush + uses: docker/build-push-action@v2 + with: + context: . + file: ./Dockerfile + push: true + tags: ghcr.io/apodini/template:latest,ghcr.io/apodini/template:${{ steps.latesttag.outputs.tag }} + - name: Image digest + run: echo ${{ steps.buildandpush.outputs.digest }} diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 0000000..bc6ee09 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,14 @@ +name: Release Drafter + +on: + push: + branches: + - release + +jobs: + update_release_draft: + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@v5 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e0f9849 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,15 @@ +name: Create Release + +on: + push: + tags: + - '*.*.*' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: GH Release + uses: softprops/action-gh-release@v0.1.5 + env: + GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} diff --git a/.github/workflows/swiftlint.yml b/.github/workflows/swiftlint.yml index 785c256..2a1f5b5 100644 --- a/.github/workflows/swiftlint.yml +++ b/.github/workflows/swiftlint.yml @@ -1,6 +1,11 @@ name: SwiftLint -on: pull_request +on: + pull_request: + paths: + - '.github/workflows/swiftlint.yml' + - '.swiftlint.yml' + - '**/*.swift' jobs: swiftlint: @@ -11,3 +16,5 @@ jobs: uses: norio-nomura/action-swiftlint@3.2.1 with: args: --strict + env: + DIFF_BASE: ${{ github.base_ref }} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 7530733..9fba386 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,7 +4,7 @@ { "label": "build", "type": "shell", - "command": "swift build", + "command": "swift build -Xswiftc -Xfrontend -Xswiftc -sil-verify-none", "group": { "kind": "build", "isDefault": true @@ -16,7 +16,7 @@ { "label": "buildtests", "type": "shell", - "command": "swift build --build-tests --enable-test-discovery" + "command": "swift build -Xswiftc -Xfrontend -Xswiftc -sil-verify-none --build-tests" } ] -} \ No newline at end of file +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f444b09 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,64 @@ +# ================================ +# Build image +# ================================ +FROM swift:5.4-focal 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 \ + && apt-get install -y libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Set up a build area +WORKDIR /build + +# Copy all source files +COPY . . + +# Build everything, with optimizations +RUN swift build -Xswiftc -Xfrontend -Xswiftc -sil-verify-none -c release + +# 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)/WebService" ./ + +# Copy resources from the resources directory if the directories exist +# Ensure that by default, neither the directory nor any of its contents are writable. +RUN [ -d "$(swift build --package-path /build -c release --show-bin-path)/Apodini_ApodiniOpenAPI.resources" ] \ + && mv "$(swift build --package-path /build -c release --show-bin-path)/Apodini_ApodiniOpenAPI.resources" ./ \ + && chmod -R a-w ./Apodini_ApodiniOpenAPI.resources \ + || echo No resources to copy +RUN [ -d "$(swift build --package-path /build -c release --show-bin-path)/WebService_ApodiniTemplate.resources" ] \ + && mv "$(swift build --package-path /build -c release --show-bin-path)/WebService_ApodiniTemplate.resources" ./ \ + && chmod -R a-w ./WebService_ApodiniTemplate.resources \ + || echo No resources to copy + +# ================================ +# Run image +# ================================ +FROM swift:5.4-focal-slim + +# Make sure all system packages are up to date. +RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ + && apt-get -q update \ + && apt-get -q dist-upgrade -y \ + && rm -r /var/lib/apt/lists/* + +# Create a apodini user and group with /app as its home directory +RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app apodini + +# Switch to the new home directory +WORKDIR /app + +# Copy built executable and any staged resources from builder +COPY --from=build --chown=apodini:apodini /staging /app + +# Ensure all further commands run as the apodini user +USER apodini:apodini + +# Start the Apodini service when the image is run. +# The default port is 80. Can be adapted using the `--port` argument +ENTRYPOINT ["./WebService"] diff --git a/Package.resolved b/Package.resolved index b00cb05..8532dbf 100644 --- a/Package.resolved +++ b/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/Apodini/Apodini.git", "state": { "branch": null, - "revision": "be6417b0b4ea7e4d7b77747099ad082bc45a2ad9", - "version": "0.2.0" + "revision": "c1193105f1a4b7320bbd3f5c404e6110746b31d8", + "version": "0.3.0" } }, { @@ -33,8 +33,8 @@ "repositoryURL": "https://github.com/swift-server/async-http-client.git", "state": { "branch": null, - "revision": "3fd0658dd91864099481b23740afd1757d1f0634", - "version": "1.4.0" + "revision": "8e4d51908dd49272667126403bf977c5c503f78f", + "version": "1.5.0" } }, { @@ -123,8 +123,8 @@ "repositoryURL": "https://github.com/vapor/fluent-postgres-driver.git", "state": { "branch": null, - "revision": "11d4fce0c5d8a027a5d131439c9c94dd3230cb8e", - "version": "2.1.2" + "revision": "afe159ce915e752ab5f5b76479dc78a17051ce73", + "version": "2.1.3" } }, { @@ -213,8 +213,8 @@ "repositoryURL": "https://github.com/OpenCombine/OpenCombine.git", "state": { "branch": null, - "revision": "9bba5081344163296ddf537048566b95513b8f39", - "version": "0.11.0" + "revision": "28993ae57de5a4ea7e164787636cafad442d568c", + "version": "0.12.0" } }, { @@ -255,11 +255,11 @@ }, { "package": "Runtime", - "repositoryURL": "https://github.com/wickwirew/Runtime.git", + "repositoryURL": "https://github.com/Supereg/Runtime.git", "state": { "branch": null, - "revision": "a848b81e1a0100801f572e5273ffe4352fd54088", - "version": "2.2.2" + "revision": "596e22a518bb3f8177cd796cd77e5386098a33f8", + "version": "2.2.3" } }, { @@ -294,8 +294,8 @@ "repositoryURL": "https://github.com/vapor/sql-kit.git", "state": { "branch": null, - "revision": "211ce34be6a2dcd8b9f0c04f6a561c642a940c86", - "version": "3.9.0" + "revision": "dd87127c7b005237b24ee24917c515ecae9ff0ef", + "version": "3.9.1" } }, { @@ -348,8 +348,17 @@ "repositoryURL": "https://github.com/swift-server/swift-backtrace.git", "state": { "branch": null, - "revision": "54a65d6391a1467a896d0d351ff2de6f469ee53c", - "version": "1.2.3" + "revision": "d3e04a9d4b3833363fb6192065b763310b156d54", + "version": "1.3.1" + } + }, + { + "package": "swift-collections", + "repositoryURL": "https://github.com/apple/swift-collections", + "state": { + "branch": null, + "revision": "3426dba9ee5c9f8e4981b0fc9d39a818d36eec28", + "version": "0.0.4" } }, { @@ -438,17 +447,17 @@ "repositoryURL": "https://github.com/vapor/vapor.git", "state": { "branch": null, - "revision": "605d8c86cfc51cf875b9e09b3c8f55e4137b0b55", - "version": "4.47.0" + "revision": "2ada54b7ce56cc6934cd2c0207bef2305bfbd7a1", + "version": "4.48.2" } }, { "package": "vapor-aws-lambda-runtime", - "repositoryURL": "https://github.com/vapor-community/vapor-aws-lambda-runtime", + "repositoryURL": "https://github.com/vapor-community/vapor-aws-lambda-runtime.git", "state": { "branch": null, - "revision": "eef25d71854e87ba7e6f0388b8bc38b6ca865cc5", - "version": "0.6.1" + "revision": "610aaed1cdd7ef434ed23bc7006000948a221527", + "version": "0.6.2" } }, { diff --git a/Package.swift b/Package.swift index b56cc79..95b49fd 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.4 import PackageDescription @@ -15,10 +15,10 @@ let package = Package( ) ], dependencies: [ - .package(url: "https://github.com/Apodini/Apodini.git", .upToNextMinor(from: "0.2.0")) + .package(url: "https://github.com/Apodini/Apodini.git", .upToNextMinor(from: "0.3.0")) ], targets: [ - .target( + .executableTarget( name: "WebService", dependencies: [ .product(name: "Apodini", package: "Apodini"), diff --git a/README.md b/README.md index e3c296d..7ed69b7 100644 --- a/README.md +++ b/README.md @@ -19,53 +19,77 @@ If you are not using macOS or don't want to use Xcode, you can use [Visual Studi 3. Press "Reopen in Container" and wait until the docker container is build 4. You can now build the code using the [build keyboard shortcut](https://code.visualstudio.com/docs/getstarted/keybindings#_tasks) and run and test the code within the docker container using the Run and Debug area. +### CLion on macOS and Windows + +You can use [CLion with the Swift plugin](https://www.jetbrains.com/help/clion/swift.html) which also works on Windows and [allows you to use the Swift plugin in CLion on Windows ](https://blog.jetbrains.com/objc/2021/03/swift-on-windows-in-clion/) + ## Structure The web service exposes a RESTful web API and an OpenAPI description: ```swift -struct ExampleWebService: WebService { - var configuration: Configuration { - REST { - OpenAPI() - } - } +@main +import Apodini +import ApodiniOpenAPI +import ApodiniREST +import ArgumentParser + - var content: some Component { - Greeter() +@main +struct ExampleWebService: WebService { + @Option(help: "The port the web service is offered at") + var port: Int = 80 + + + var configuration: Configuration { + HTTPConfiguration(port: port) + REST { + OpenAPI() } + } + + var content: some Component { + Greeter() + } } ``` The example web service exposes a single `Handler` named `Greeter`: ```swift struct Greeter: Handler { - @Parameter var name: String = "World" - - - func handle() -> String { - "Hello, \(name)! 👋" - } + @Parameter var name: String = "World" + + + func handle() -> String { + "Hello, \(name)! 👋" + } } ``` -## RESTful API +### RESTful API -You can access the `Greeter` `Handler` at `http://localhost:8080/v1`. -The `@Parameter` is exposed as a parameter in the URL. E.g., you can send a request to `localhost:8080/v1?name=Paul` to get the following response: +You can access the `Greeter` `Handler` at `http://localhost/v1`. +The `@Parameter` is exposed as a parameter in the URL. E.g., you can send a request to `localhost/v1?name=Paul` to get the following response: ```json { "data" : "Hello, Paul! 👋", "_links" : { - "self" : "http://127.0.0.1:8080/v1" + "self" : "http://127.0.0.1/v1" } } ``` -## OpenAPI +### OpenAPI -You can access the OpenAPI document at `http://localhost:8080/openapi`. -The Swagger UI is also automatically generated and accessible at `http://localhost:8080/openapi-ui`. +You can access the OpenAPI document at `http://localhost/openapi`. +The Swagger UI is also automatically generated and accessible at `http://localhost/openapi-ui`. ## Continous Integration The repository contains GitHub Actions to automatically build and test the example web service on a wide variety of platforms and configurations. + +### Docker + +The template includes docker files and docker compose files to start and deploy a web service. +In addition, the template includes a GitHub Action that builds a new docker image on every release and pushes the image to the GitHub package registry. +You can start up the web service using published docker images using `$ docker compose up` using the `docker-compose.yml` file. +The `docker-compose-development.yml` file can be used to test the setup by building the web service locally using `$ docker compose -f docker-compose-development.yml up`. diff --git a/Sources/WebService/main.swift b/Sources/WebService/ExampleWebService.swift similarity index 60% rename from Sources/WebService/main.swift rename to Sources/WebService/ExampleWebService.swift index 1ee0035..afd7869 100644 --- a/Sources/WebService/main.swift +++ b/Sources/WebService/ExampleWebService.swift @@ -1,19 +1,23 @@ import Apodini import ApodiniOpenAPI import ApodiniREST +import ArgumentParser +@main struct ExampleWebService: WebService { + @Option(help: "The port the web service is offered at") + var port: Int = 80 + + var configuration: Configuration { + HTTPConfiguration(port: port) REST { OpenAPI() } } - + var content: some Component { Greeter() } } - - -ExampleWebService.main() diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index 08ae2ac..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,7 +0,0 @@ -// A LinuxMain.swift file is no longer needed since `swift test --enable-test-discovery` is possible -// Provide an error message when testing on Linux with no automatic test discovery -#error(""" - ----------------------------------------------------- - Please test with `swift test --enable-test-discovery` - ----------------------------------------------------- - """) diff --git a/Tests/WebServiceTests/WebServiceTests.swift b/Tests/WebServiceTests/WebServiceTests.swift index a5bd82b..521ab9e 100644 --- a/Tests/WebServiceTests/WebServiceTests.swift +++ b/Tests/WebServiceTests/WebServiceTests.swift @@ -1,4 +1,3 @@ -@testable import WebService import XCTest diff --git a/docker-compose-development.yml b/docker-compose-development.yml new file mode 100644 index 0000000..d402894 --- /dev/null +++ b/docker-compose-development.yml @@ -0,0 +1,45 @@ +version: '3.9' + +services: + # An Apodini Example Web Service + example-web-service: + container_name: "template-web-service" + image: "apodini/template:latest" + build: + context: ./ + dockerfile: ./Dockerfile + expose: + - "80" + command: ["--port", "80"] + labels: + # The domain the service will respond to + - "traefik.http.routers.backend.rule=Host(`localhost`)" + # Allow request only from the predefined entry point named "web" + - "traefik.http.routers.backend.entrypoints=web" + # We need to define the a service and specify, on which port our server is reachable + - "traefik.http.services.backend-service.loadbalancer.server.port=80" + # We have to add this service to our router "backend". That's how the router knows where to forward the requests + - "traefik.http.routers.backend.service=backend-service" + # Reverse Proxy to protect our service from direct access + traefik: + container_name: "traefik" + image: "traefik:v2.4" + command: + # Enable Debug output + - "--log.level=DEBUG" + # Enable the api and the traefik dashboard for debugging purposes, which can be reached under 127.0.0.1:8080 + - "--api.insecure=true" + - "--api.dashboard=true" + # Enabling docker provider + - "--providers.docker=true" + # Traefik will listen to incoming request on the port 8090 + - "--entrypoints.web.address=:80" + ports: + # 8080 on the container is mapped to 8080 on the server/VM/your Machine. + # Port 8080 is where the api traefik dashboard is located + - "8080:8080" + # Port 80 is where our example-web-service is running + - "80:80" + # Traefik needs the docker.sock to detect new docker container + volumes: + - "/var/run/docker.sock:/var/run/docker.sock:ro" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bff38e0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,34 @@ +version: '3.9' + +services: + # An Apodini Example Web Service + example-web-service: + container_name: "example-web-service" + image: "ghcr.io/apodini/template:latest" + expose: + - "80" + command: ["--port", "80"] + labels: + # The domain the service will respond to + - "traefik.http.routers.backend.rule=Host(`localhost`)" + # Allow request only from the predefined entry point named "web" + - "traefik.http.routers.backend.entrypoints=web" + # We need to define the a service and specify, on which port our server is reachable + - "traefik.http.services.backend-service.loadbalancer.server.port=80" + # We have to add this service to our router "backend". That's how the router knows where to forward the requests + - "traefik.http.routers.backend.service=backend-service" + # Reverse Proxy to protect our service from direct access + traefik: + container_name: "traefik" + image: "traefik:v2.4" + command: + # Enabling docker provider + - "--providers.docker=true" + # Traefik will listen to incoming request on the port 80 (HTTP) + - "--entrypoints.web.address=:80" + ports: + # 80 on the container is mapped to 80 on the server/VM/your Machine. + - "80:80" + # Traefik needs the docker.sock to detect new docker container + volumes: + - "/var/run/docker.sock:/var/run/docker.sock:ro"