diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2d9f16e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +.build/ +.swiftpm/ diff --git a/.gitignore b/.gitignore index e6c4c4f..ea14afd 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ db.sqlite Public Resources ./feather +docker/datadir +ssh \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..70d7944 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,108 @@ +FROM swift:5.3.1-focal as builder +WORKDIR /opt/feather + +COPY . . + +## Install required dependencies for the build +RUN apt-get update && apt-get install libxml2-dev libsqlite3-dev -y + +## Allow the usage of private repository +### This part is for advanced user, ignore it if you don't use private repository in Swift package manager +## 1. Add SSH Hostname to known_host (This is use for Private repo in SPM) +## Here we add github.com hostname as when building the docker file we don't have an interactive shell +## It means it will be known from git, so won't request confirmation of the key +## You may want to add your own +## 2. It will use you private keys to clone private repository +## You private keys should be stored (Password less, decrypted) in the folder `ssh` +## for Continous integration, you will need a step to copy the decrypted key to the folder. Symbolic link won't work +RUN if [ -d "ssh" ]; then \ + mkdir -p ~/.ssh/; \ + eval `ssh-agent`; \ + ssh-keyscan -H github.com >> ~/.ssh/known_hosts; \ +fi + +## Build the application +RUN echo "---> Build application"; \ +if [ -d "ssh" ]; then \ + eval `ssh-agent`; \ + chmod 400 ssh/*; \ + ssh-add ssh/*; \ + swift build -c release --enable-test-discovery; \ +else \ + swift build -c release --enable-test-discovery; \ +fi + +RUN rm -rf */**/.DS_Store + +## Organizing things up +RUN mkdir -p /tmp/app + +## Check if the folder contains preexising DB/Folders. +## It means the user may have prepare Feather-CMS locally, in that case we will copy its content to the template folder +## So the user will keep what he prepared +RUN cd /opt/feather; if [ -f "db.sqlite" ]; then cp -pr db.sqlite /tmp/app; fi +RUN cd /opt/feather; if [ -d "Public" ]; then cp -pr Public /tmp/app; fi +RUN cd /opt/feather; if [ -d "Resources" ]; then cp -pr Resources /tmp/app; fi + +## Prepare entry point +RUN \ +echo '#!/bin/bash' > /tmp/app/start &&\ +echo "[[ ! -d \"\${BASE_PATH}/db.sqlite\" && -d /opt/feather/db.sqlite ]] && cp -pr /opt/feather/db.sqlite \${BASE_PATH}/" >> /tmp/app/start &&\ +echo "[[ ! -d \"\${BASE_PATH}/Public\" && -d /opt/feather/Public ]] && cp -pr /opt/feather/Public \${BASE_PATH}/" >> /tmp/app/start &&\ +echo "[[ ! -d \"\${BASE_PATH}/Resources\" && -d /opt/feather/Resources ]] && cp -pr /opt/feather/Resources \${BASE_PATH}/" >> /tmp/app/start &&\ +echo "Feather serve --hostname 0.0.0.0 --port \${BASE_PORT}" >> /tmp/app/start +RUN chmod 550 /tmp/app/start + +## Keep only executables & resources (will be available in /opt/feather/bin ) +RUN mv /opt/feather/.build/x86_64-unknown-linux-gnu/release /tmp/app/bin + +## Copy any custom scripts under the `customscripts` folder +## If the user decide to use a diffent enty point from start, he can create hsi own scripts. +## Ex. We could imagine, starting 10 servers in one docker. The script would set, different folders, env variables. It will not work with embeded DB here... +## docker run -d -p 8080-8090:8080-8090 feather start_10_apps +RUN if [ -d "/opt/feather/customscripts" ]; then \ + chmod 550 /opt/feather/customscripts/*; \ + cp -pr /opt/feather/customscripts/* /tmp/app/; \ +fi + +## Clean up & keep only executbale from the build +RUN cd /; rm -rf /opt/feather; mv /tmp/app /opt/feather + +## User feather +RUN groupadd -r feather && useradd --no-log-init -r -g feather feather +RUN chown -R feather:feather /opt/feather +RUN chmod 550 /opt/feather/bin/Feather + + + +## Slim version of the container +FROM swift:5.3.1-focal-slim +WORKDIR /var/feather +COPY --from=builder /opt/feather /opt/feather + +RUN \ + groupadd -r feather &&\ + useradd --no-log-init -r -g feather feather &&\ + mkdir -p /var/feather && chown -R feather:feather /var/feather &&\ + ln -s /opt/feather/bin/Feather /usr/local/bin/ + +USER feather + +ENV BASE_URL="http://localhost:8080" +ENV BASE_PORT=8080 +ENV BASE_PATH="/var/feather" + +## Default sqlite +ENV DB_TYPE="sqlite" +ENV MAX_BODYSIZE="10mb" +ENV USE_FILE_MIDDLEWARE="true" + +## Using mysql/mariadb/postgres +ENV DB_HOST="localhost" +# ENV DB_PORT=3306 or 5432 # The port will use the default one, if you use a custom port, set this env variable +ENV DB_USER="feather" +ENV DB_PASS="feather" +ENV DB_NAME="feather" + +CMD [ "bash", "/opt/feather/start" ] + diff --git a/Package.resolved b/Package.resolved index 6c2c1d1..15e8ac3 100644 --- a/Package.resolved +++ b/Package.resolved @@ -127,6 +127,24 @@ "version": "1.10.2" } }, + { + "package": "fluent-mysql-driver", + "repositoryURL": "https://github.com/vapor/fluent-mysql-driver", + "state": { + "branch": null, + "revision": "699e164880387349bf04c2329fe53097524a5904", + "version": "4.0.0" + } + }, + { + "package": "fluent-postgres-driver", + "repositoryURL": "https://github.com/vapor/fluent-postgres-driver", + "state": { + "branch": null, + "revision": "11d4fce0c5d8a027a5d131439c9c94dd3230cb8e", + "version": "2.1.2" + } + }, { "package": "fluent-sqlite-driver", "repositoryURL": "https://github.com/vapor/fluent-sqlite-driver", @@ -244,6 +262,42 @@ "version": "1.0.0-beta.3" } }, + { + "package": "mysql-kit", + "repositoryURL": "https://github.com/vapor/mysql-kit.git", + "state": { + "branch": null, + "revision": "fe836ecd52815ed1399e654b5365c108ff1638fd", + "version": "4.1.0" + } + }, + { + "package": "mysql-nio", + "repositoryURL": "https://github.com/vapor/mysql-nio.git", + "state": { + "branch": null, + "revision": "4cbef4c0903c9792ff1f8dc4b55532276fa70c99", + "version": "1.3.2" + } + }, + { + "package": "postgres-kit", + "repositoryURL": "https://github.com/vapor/postgres-kit.git", + "state": { + "branch": null, + "revision": "13f6c735cf9a9053011d03e77e2a81802ad6c680", + "version": "2.3.0" + } + }, + { + "package": "postgres-nio", + "repositoryURL": "https://github.com/vapor/postgres-nio.git", + "state": { + "branch": null, + "revision": "2808c4ff334c20073de92e735dac5587ba398b0d", + "version": "1.4.1" + } + }, { "package": "redirect-module", "repositoryURL": "https://github.com/FeatherCMS/redirect-module", @@ -411,8 +465,8 @@ "repositoryURL": "https://github.com/FeatherCMS/system-module", "state": { "branch": null, - "revision": "5d6804551621f154aeb11aaec88870b58bcdee80", - "version": "1.0.0-beta.6" + "revision": "4e10c15c3ed45cdcc2edc67f205daa29b5f23199", + "version": "1.0.0-beta.8" } }, { diff --git a/Package.swift b/Package.swift index 8df3424..e295bd1 100644 --- a/Package.swift +++ b/Package.swift @@ -15,6 +15,8 @@ let package = Package( .package(url: "https://github.com/binarybirds/spec.git", from: "1.2.0-beta"), /// drivers .package(url: "https://github.com/vapor/fluent-sqlite-driver", from: "4.0.0"), + .package(url: "https://github.com/vapor/fluent-mysql-driver", from: "4.0.0"), + .package(url: "https://github.com/vapor/fluent-postgres-driver", from: "2.0.0"), .package(url: "https://github.com/binarybirds/liquid-local-driver", from: "1.2.0-beta"), /// feather core .package(url: "https://github.com/FeatherCMS/feather-core", from: "1.0.0-beta"), @@ -38,6 +40,8 @@ let package = Package( .target(name: "Feather", dependencies: [ /// drivers .product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"), + .product(name: "FluentMySQLDriver", package: "fluent-mysql-driver"), + .product(name: "FluentPostgresDriver", package: "fluent-postgres-driver"), .product(name: "LiquidLocalDriver", package: "liquid-local-driver"), /// feather .product(name: "FeatherCore", package: "feather-core"), diff --git a/Sources/Feather/main.swift b/Sources/Feather/main.swift index 9eebe45..6a6a36e 100644 --- a/Sources/Feather/main.swift +++ b/Sources/Feather/main.swift @@ -7,6 +7,8 @@ import FeatherCore import FluentSQLiteDriver +import FluentMySQLDriver +import FluentPostgresDriver import LiquidLocalDriver import SystemModule @@ -27,15 +29,68 @@ import MarkdownModule /// setup metadata delegate object Feather.metadataDelegate = FrontendMetadataDelegate() +/// Detect the DB type from .env.development or the environement variables +/// # Required: +/// ~~~ +/// BASE_URL="http://127.0.0.1:8080" +/// BASE_PATH="/Repo/feather" +/// ~~~ +/// # Optional: +/// ~~~ +/// MAX_BODYSIZE="10mb" (default) - Required format: XXmb +/// USE_FILE_MIDDLEWARE="true" (default) - Required format: true/false +/// +/// DB_TYPE="mysql" # Available: sqlite (default) / mysql / postgres +/// DB_HOST="127.0.0.1" +/// DB_USER="feather" +/// DB_PASS="feather" +/// DB_NAME="feather" +/// +/// # Optional: For DB_TYPE = "mysql" | "postgres" +/// DB_PORT=3306 # mysql: 3306 (default) - postgres: 5432(default) +/// ~~~ +/// var env = try Environment.detect() try LoggingSystem.bootstrap(from: &env) let feather = try Feather(env: env) defer { feather.stop() } -try feather.configure(database: .sqlite(.file("db.sqlite")), - databaseId: .sqlite, +let dbconfig: DatabaseConfigurationFactory +let dbID: DatabaseID +switch Environment.get("DB_TYPE") { +case "mysql": + dbconfig = .mysql(hostname: Environment.fetch("DB_HOST"), + port: Int(Environment.get("DB_PORT") ?? "3306")!, + username: Environment.fetch("DB_USER"), + password: Environment.fetch("DB_PASS"), + database: Environment.fetch("DB_NAME"), + tlsConfiguration: .forClient(certificateVerification: .none)) + dbID = .mysql + break +case "postgres": + dbconfig = .postgres(hostname: Environment.fetch("DB_HOST"), + port: Int(Environment.get("DB_PORT") ?? "5432")!, + username: Environment.fetch("DB_USER"), + password: Environment.fetch("DB_PASS"), + database: Environment.fetch("DB_NAME")) + dbID = .psql + break +default: + dbconfig = .sqlite(.file("db.sqlite")) + dbID = .sqlite + break +} + +var middleWare = true +if let provideMiddleWare = Environment.get("USE_FILE_MIDDLEWARE") { + middleWare = Bool(provideMiddleWare) ?? true +} + +try feather.configure(database: dbconfig, + databaseId: dbID, fileStorage: .local(publicUrl: Application.baseUrl, publicPath: Application.Paths.public, workDirectory: "assets"), fileStorageId: .local, + maxUploadSize: ByteCount(stringLiteral: Environment.get("MAX_BODYSIZE") ?? "10mb"), modules: [ SystemBuilder(), UserBuilder(), @@ -51,7 +106,8 @@ try feather.configure(database: .sqlite(.file("db.sqlite")), SponsorBuilder(), SwiftyBuilder(), MarkdownBuilder(), - ]) + ], + usePublicFileMiddleware: middleWare) if feather.app.isDebug { try feather.reset(resourcesOnly: true) diff --git a/docker/README.md b/docker/README.md new file mode 100755 index 0000000..050484c --- /dev/null +++ b/docker/README.md @@ -0,0 +1,162 @@ +# Docker + Feather CMS 🪶 + +- Feather is a modern Swift-based content management system powered by Vapor 4. + +- Docker is a set of platform as a service products that use OS-level virtualization to deliver software in packages called containers. Containers are isolated from one another and bundle their own software, libraries and configuration files; they can communicate with each other through well-defined channels. + +## Requirements + +- Docker installed on your computer. [Get Docker.com](https://docs.docker.com/get-docker/) + +- Basic knowledge of [Docker Compose](https://docs.docker.com/compose/) + +- Basic knowledge of [Docker](https://docs.docker.com/) + + +## Installation + +- Clone or download the source files + +- Start the application using `docker-compose` + +## Usage + +### Testing the integrated database (sqlite) +the base url of your web server will be http://localhost:8080 + + +```bash +docker-compose up +``` + +### Testing mysql database +the base url of your web server will be http://localhost:8081 +& the database will be accessible on port `3306`. +- `root password`: root +- `TABLE`: feather - `USER`: feather - `PASS`: feather + +```bash +docker-compose -f mysql.yml up +``` + +### Testing postgres database +`⚠️ There is a pending issue with Postgres. It is unsuable for the moment` + +the base url of your web server will be http://localhost:8082 +& the database will be accessible on port `5432`. +- `root password`: postgres +- `TABLE`: feather - `USER`: feather - `PASS`: feather + +```bash +docker-compose -f postgres.yml up +``` + +## Custumazing the docker + +You can check the [Dockerfile](https://github.com/FeatherCMS/feather/blob/main/Dockerfile?raw=true) and the different step of the build. + +Here is a brief descritpion of the process + +1. We use `swift:5.3.1-focal` as our main builder +2. You can have your own `PRIVATE` custom module and cloning over `ssh` is supported: + - We need to add the SSH Hostname to `known_host` (This is use for Private repo in SPM) + In our example `Line 21` -> `ssh-keyscan -H github.com >> ~/.ssh/known_hosts;` + - At the root levele of the repository, create a folder `ssh` and copy your `unencrypted` private key in this folder. The Docker build will picked it up. + - The private key will not be part of the final build. So you can distribute the image. +4. You can also add some customs scripts/content to be part of the docker. They will be available at the path `/opt/feather` + - create a folder `customscripts` at the root lever of the repository + - add your content to it + - Please note that a `chmod 550` will be apply to it + +3. When the build complete, we will use `swift:5.3.1-focal-slim` to build the final result. + - Using this will drastycally decrease the final size of the docker image + - User details running the application inside the container:(`uid=999(feather) gid=999(feather) groups=999(feather)`. Please make sure you give read/write to this user + +## Available environement variable + +``` +BASE_URL="http://localhost:8080" +BASE_PATH="/var/feather" +BASE_PORT=8080 + +## Default sqlite +DB_TYPE="sqlite" +MAX_BODYSIZE="10mb" +USE_FILE_MIDDLEWARE="true" + +## Using mysql/mariadb/postgres +DB_HOST="localhost" +DB_PORT=3306 or 5432 # The port will use the default one, if you use a custom port, set this env variable +DB_USER="feather" +DB_PASS="feather" +``` +## Docker Working directory + +The default working directory is `/var/feather` you can mount a volume at this path. You may also use a different folder by setting the `BASE_PATH` environment variable. + + +## Docker entry point + +The default entry point is `/opt/feather/start`. You may want to custumize the start behaviour. In that case, add a script `start` to the `customscripts` folder, it will be overwriten durring the build. + +### Default provided entry point + +This script will check if you had previously prepared data and put then in the define `BASE_PATH`. It will then start `Feather` + +``` +#!/bin/bash + +[[ ! -d \"\${BASE_PATH}/db.sqlite\" && -d /opt/feather/db.sqlite ]] && cp -pr /opt/feather/db.sqlite \${BASE_PATH}/ + +[[ ! -d \"\${BASE_PATH}/Public\" && -d /opt/feather/Public ]] && cp -pr /opt/feather/Public \${BASE_PATH}/" + +[[ ! -d \"\${BASE_PATH}/Resources\" && -d /opt/feather/Resources ]] && cp -pr /opt/feather/Resources \${BASE_PATH}/ + +Feather serve --hostname 0.0.0.0 --port \${BASE_PORT} +``` + +## Examples + +``` +docker run -d -v :/var/feather -p 8080:8080 feather +``` + +``` +docker run -d \ + -v :/var/feather \ + -e BASE_URL="http://localhost:8081" \ + -e BASE_PORT=8081 \ + -e DB_TYPE=mysql \ + -e DB_HOST= \ + -e DB_PORT=3306 \ + -e DB_NAME=feather \ + -e DB_USER=feather \ + -e DB_PASS=feather \ + -e MAX_BODYSIZE="10mb" \ + -d feather + +``` + + +## Known issues + +### Postgress database + +When running the app using a Postgres database, the index lenght is limited to 63 characthers trucating some of feather indentifier: +``` +identifier "uq:user_permissions.module+user_permissions.context+user_permissions.action" will be truncated to "uq:user_permissions.module+user_permissions.context+user_permis" (truncate_identifier) +identifier "uq:frontend_metadatas.module+frontend_metadatas.model+frontend_metadatas.reference" will be truncated to "uq:frontend_metadatas.module+frontend_metadatas.model+frontend_" (truncate_identifier) +``` + +Until the issue is resolved, postgres databases are unusable. + +## Credits + +- [Vapor](https://vapor.codes) - underlying framework +- [Feather icons](https://feathericons.com) - feather icons + + +### License + +[WTFPL](LICENSE) + diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..6644ce1 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,9 @@ +version: "3.9" +## docker-compose up +services: + feather: + build: .. + ports: + - "8080:8080" + volumes: + - "./datadir/sqlite:/var/feather" \ No newline at end of file diff --git a/docker/mysql.yml b/docker/mysql.yml new file mode 100644 index 0000000..6c8ab29 --- /dev/null +++ b/docker/mysql.yml @@ -0,0 +1,43 @@ +version: "3.9" +## docker-compose -f mysql.yml up +services: + + ## Database + database: + container_name: mariadb + image: mariadb + command: --default-authentication-plugin=mysql_native_password + volumes: + - "./datadir/mysql/db:/var/lib/mysql" + - "./scripts/1-mysql-init.sql:/docker-entrypoint-initdb.d/1.sql" + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_ROOT_HOST: '%' + ports: + - "3306:3306" + expose: + - 3306 + + ## Application + feather: + depends_on: + - database + container_name: feather-mariadb + build: .. + ports: + - "8081:8081" + expose: + - 8081 + environment: + BASE_URL: "http://localhost:8081" + BASE_PORT: 8081 + DB_HOST: database + DB_PORT: 3306 + DB_TYPE: mysql + DB_USER: "feather" + DB_PASS: "feather" + DB_NAME: "feather" + volumes: + - "./datadir/mysql:/var/feather" + - "./scripts/waitfor:/opt/feather/waitfor" + command: sh -c "/opt/feather/waitfor database 30 && /opt/feather/start" \ No newline at end of file diff --git a/docker/postgres.yml b/docker/postgres.yml new file mode 100644 index 0000000..28981fe --- /dev/null +++ b/docker/postgres.yml @@ -0,0 +1,41 @@ +version: "3.9" +## docker-compose -f postgres.yml up +services: + + ## Database + database: + container_name: postgres + image: postgres + volumes: + - "./datadir/postgres/db:/var/lib/postgresql/data" + - "./scripts/1-postgres-init.sql:/docker-entrypoint-initdb.d/1.sql" + environment: + POSTGRES_PASSWORD: postgres + ports: + - "5432:5432" + expose: + - 5432 + + ## Application + feather: + depends_on: + - database + container_name: feather-postgres + build: .. + ports: + - "8082:8082" + expose: + - 8082 + environment: + BASE_URL: "http://localhost:8082" + BASE_PORT: 8082 + DB_HOST: database + DB_PORT: 5432 + DB_TYPE: postgres + DB_USER: "feather" + DB_PASS: "feather" + DB_NAME: "feather" + volumes: + - "./datadir/postgres:/var/feather" + - "./scripts/waitfor:/opt/feather/waitfor" + command: sh -c "/opt/feather/waitfor database 30 && /opt/feather/start" \ No newline at end of file diff --git a/docker/scripts/1-mysql-init.sql b/docker/scripts/1-mysql-init.sql new file mode 100644 index 0000000..9425de0 --- /dev/null +++ b/docker/scripts/1-mysql-init.sql @@ -0,0 +1,3 @@ +CREATE DATABASE feather; +CREATE USER 'feather'@'%' IDENTIFIED BY 'feather'; +GRANT ALL ON `feather`.* to 'feather'@'%'; diff --git a/docker/scripts/1-postgres-init.sql b/docker/scripts/1-postgres-init.sql new file mode 100644 index 0000000..8e1a289 --- /dev/null +++ b/docker/scripts/1-postgres-init.sql @@ -0,0 +1,3 @@ +CREATE DATABASE feather; +CREATE USER feather WITH PASSWORD 'feather'; +GRANT ALL PRIVILEGES ON DATABASE feather TO feather; \ No newline at end of file diff --git a/docker/scripts/waitfor b/docker/scripts/waitfor new file mode 100755 index 0000000..adc6620 --- /dev/null +++ b/docker/scripts/waitfor @@ -0,0 +1,12 @@ +#!/bin/sh + +set -eu +echo "Waiting for ${1}..." +i=0 +until [ $i -ge $2 ] +do + i=$(( i + 1 )) + echo "$i: Waiting for ${1} | ${i}/${2} seconds..." + sleep 1 +done +echo "${1} should be ready ..."