diff --git a/.docker/Dockerfile.dev b/.docker/Dockerfile.dev index 900e73c..ce4e77c 100644 --- a/.docker/Dockerfile.dev +++ b/.docker/Dockerfile.dev @@ -1,44 +1,32 @@ -ARG DEBIAN_FRONTEND=noninteractive +# rust:1.80.1-alpine3.20 +FROM rust@sha256:1f5aff501e02c1384ec61bb47f89e3eebf60e287e6ed5d1c598077afc82e83d5 AS dev + ARG INIT_PATH=/usr/local/bin/dumb-init ARG INIT_URL=https://github.com/Yelp/dumb-init/releases/download/v1.2.5/dumb-init_1.2.5_x86_64 -ARG USERNAME=rust +ARG USER=rust ARG USER_UID=1000 -ARG USER_GID=$USER_UID +ARG USER_GID=${USER_UID} ARG WORK_DIR=/usr/src/app -FROM rust:1.80.1 AS dev - -ARG DEBIAN_FRONTEND -ARG INIT_PATH -ARG INIT_URL -ARG USERNAME -ARG USER_UID -ARG USER_GID -ARG WORK_DIR - ENV TZ=America/Sao_Paulo TERM=xterm-256color LANG=C.UTF-8 LC_ALL=C.UTF-8 -SHELL [ "/bin/bash", "-c" ] - -RUN set -euxo pipefail;\ - apt-get -qq update;\ - apt-get -qq install -y nano sudo apt-utils build-essential tzdata curl cmake g++ libpcre3-dev libssl-dev make openssl libgmp-dev git curl ca-certificates software-properties-common wget zip unzip busybox > /dev/null;\ - curl --fail --silent --show-error --location ${INIT_URL} --output ${INIT_PATH};\ - apt-get -qq clean;\ - chmod +x ${INIT_PATH};\ +RUN set -euxo pipefail; \ + apk add --no-cache nano sudo build-base tzdata curl cmake g++ pcre-dev openssl-dev make gmp-dev git ca-certificates wget zip unzip busybox; \ + curl --fail --silent --show-error --location ${INIT_URL} --output ${INIT_PATH}; \ + chmod +x ${INIT_PATH}; \ cargo install cargo-watch; -RUN set -euxo pipefail;\ - groupadd --gid $USER_GID $USERNAME;\ - useradd --uid $USER_UID --gid $USER_GID -m $USERNAME;\ - echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME;\ - chmod 0440 /etc/sudoers.d/$USERNAME; +RUN set -euxo pipefail; \ + addgroup -g ${USER_GID} ${USER}; \ + adduser -u ${USER_UID} -G ${USER} -h /home/${USER} -D ${USER}; \ + echo "${USER} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/${USER}; \ + chmod 0440 /etc/sudoers.d/${USER}; -USER ${USERNAME} +USER ${USER} -ENV CARGO_HOME="/home/${USERNAME}/.cargo" +ENV CARGO_HOME="/home/${USER}/.cargo" -WORKDIR $WORK_DIR +WORKDIR ${WORK_DIR} EXPOSE 3000 diff --git a/.docker/Dockerfile.prod b/.docker/Dockerfile.prod new file mode 100644 index 0000000..13f0dac --- /dev/null +++ b/.docker/Dockerfile.prod @@ -0,0 +1,66 @@ +ARG INIT_PATH=/usr/local/bin/dumb-init +ARG INIT_URL=https://github.com/Yelp/dumb-init/releases/download/v1.2.5/dumb-init_1.2.5_x86_64 +ARG USER=rust +ARG USER_UID=1000 +ARG USER_GID=${USER_UID} +ARG WORK_DIR=/usr/src/app + +# rust:1.80.1-alpine3.20 +FROM rust@sha256:1f5aff501e02c1384ec61bb47f89e3eebf60e287e6ed5d1c598077afc82e83d5 AS builder + +ARG INIT_PATH +ARG INIT_URL +ARG USER +ARG USER_UID +ARG USER_GID +ARG WORK_DIR + +ENV CI=true LANG=C.UTF-8 LC_ALL=C.UTF-8 + +WORKDIR ${WORK_DIR} + +RUN set -euxo pipefail; \ + apk add --no-cache build-base cmake g++ pcre-dev openssl-dev gmp-dev curl ca-certificates; \ + curl --fail --silent --show-error --location ${INIT_URL} --output ${INIT_PATH}; \ + chmod +x ${INIT_PATH}; + +COPY Cargo.toml Cargo.lock Makefile ${WORK_DIR}/ +COPY src ${WORK_DIR}/src + +RUN set -euxo pipefail; \ + make clean; \ + make release; + +# alpine:3.20 +FROM alpine@sha256:0a4eaa0eecf5f8c050e5bba433f58c052be7587ee8af3e8b3910ef9ab5fbe9f5 AS main + +ARG INIT_PATH +ARG INIT_URL +ARG USER +ARG USER_UID +ARG USER_GID +ARG WORK_DIR + +ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 + +RUN set -euxo pipefail; \ + addgroup -g ${USER_GID} ${USER}; \ + adduser -u ${USER_UID} -G ${USER} -D ${USER}; \ + apk update --no-cache; \ + apk upgrade --no-cache; + +COPY --from=builder --chown=${USER}:${USER} ${INIT_PATH} ${INIT_PATH} + +WORKDIR ${WORK_DIR} + +COPY --from=builder --chown=${USER}:${USER} ${WORK_DIR}/target/release/twitch-extension-api ${WORK_DIR}/twitch-extension-api + +COPY --chown=${USER}:${USER} static ${WORK_DIR}/static + +USER ${USER} + +EXPOSE 3000 + +ENTRYPOINT [ "/usr/local/bin/dumb-init", "--" ] + +CMD [ "/usr/src/app/twitch-extension-api" ] diff --git a/.env.docker b/.env.docker index 6ccd4dd..ead101f 100644 --- a/.env.docker +++ b/.env.docker @@ -1,5 +1,5 @@ # Rust -RUST_LOG=info +RUST_LOG=twitch_extension_api=debug RUST_BACKTRACE=1 # Http @@ -7,17 +7,18 @@ MAX_WORKERS=2 # App Config APP_NAME="Twitch Better Chat API" -APP_VERSION="0.1.0" -APP_URL="0.0.0.0" -APP_PORT="8001" -APP_TLS_ENABLED="false" -APP_TLS_CERT="/your/path/to/cert.pem" -APP_TLS_KEY="/your/path/to/key.pem" -APP_PLATFORM_SECRET="secret" +APP_VERSION=0.1.0 +APP_URL=0.0.0.0 +APP_PORT=3000 +APP_ENV=dev +APP_TLS_ENABLED=false +APP_TLS_CERT=/your/path/to/cert.pem +APP_TLS_KEY=/your/path/to/key.pem +APP_PLATFORM_SECRET=secret # Database Config -SCYLLA_NODES="host:port" +SCYLLA_NODES=scylla-1:9042 SCYLLA_USERNAME= SCYLLA_PASSWORD= -SCYLLA_CACHED_QUERIES="15" -SCYLLA_KEYSPACE="twitch" +SCYLLA_CACHED_QUERIES=15 +SCYLLA_KEYSPACE=twitch diff --git a/.env.example b/.env.example index e7c8f0b..8ccc8a7 100644 --- a/.env.example +++ b/.env.example @@ -1,23 +1,24 @@ # Rust -RUST_LOG=info +RUST_LOG=twitch_extension_api=info RUST_BACKTRACE=1 # Http MAX_WORKERS=4 # App Config -APP_NAME="Twitch Better Chat API" -APP_VERSION="0.1.0" -APP_URL="0.0.0.0" -APP_PORT="8001" -APP_TLS_ENABLED="false" -APP_TLS_CERT="/your/path/to/cert.pem" -APP_TLS_KEY="/your/path/to/key.pem" -APP_PLATFORM_SECRET="secret" +APP_NAME='Twitch Better Chat API' +APP_VERSION=0.1.0 +APP_URL=0.0.0.0 +APP_PORT=3000 +APP_ENV=dev +APP_TLS_ENABLED=false +APP_TLS_CERT=/your/path/to/cert.pem +APP_TLS_KEY=/your/path/to/key.pem +APP_PLATFORM_SECRET=secret # Database Config -SCYLLA_NODES="localhost:9042" -SCYLLA_USERNAME="scylla" +SCYLLA_NODES=localhost:9042 +SCYLLA_USERNAME= SCYLLA_PASSWORD= -SCYLLA_CACHED_QUERIES="15" -SCYLLA_KEYSPACE="twitch" +SCYLLA_CACHED_QUERIES=15 +SCYLLA_KEYSPACE=twitch diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..34884cb --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 + +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + target-branch: "develop" + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + target-branch: "develop" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cfd4708..e17b417 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,15 +10,19 @@ on: - main - develop +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: setup: name: Setup rust runs-on: ubuntu-24.04 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 - name: Setup rust - uses: actions-rust-lang/setup-rust-toolchain@v1 + uses: actions-rust-lang/setup-rust-toolchain@1fbea72663f6d4c03efaab13560c8a24cfd2a7cc # v1.9.0 with: toolchain: stable target: x86_64-unknown-linux-gnu @@ -34,10 +38,16 @@ jobs: - setup steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - name: Setup rust + uses: actions-rust-lang/setup-rust-toolchain@1fbea72663f6d4c03efaab13560c8a24cfd2a7cc # v1.9.0 + with: + toolchain: stable + target: x86_64-unknown-linux-gnu + components: clippy, rustfmt - name: Run formatter - uses: actions-rust-lang/rustfmt@v1 + uses: actions-rust-lang/rustfmt@2d1d4e9f72379428552fa1def0b898733fb8472d # v1.1.0 - name: Run linter - uses: clechasseur/rs-clippy-check@v3 + uses: clechasseur/rs-clippy-check@a2a93bdcf05de7909aabd62eca762179ad3dbe50 # v3.0.5 with: - args: --all-targets --all-features + args: --all-features diff --git a/Makefile b/Makefile index b5c72c4..c208497 100644 --- a/Makefile +++ b/Makefile @@ -37,28 +37,23 @@ release: clean ## Cleans up the project and compiles it for release. test: ## Runs the test suite. @$(CARGO) test -# Load specific variables from .env file -ifneq (,$(wildcard .env)) - export SCYLLA_NODES := $(shell grep -E '^SCYLLA_NODES=' .env | awk -F '=' '{gsub(/^[[:space:]]*|[[:space:]]*$$/, "", $$2); print substr($$2, 2, length($$2)-2)}') - export SCYLLA_KEYSPACE := $(shell grep -E '^SCYLLA_KEYSPACE=' .env | awk -F '=' '{gsub(/^[[:space:]]*|[[:space:]]*$$/, "", $$2); print substr($$2, 2, length($$2)-2)}') - export SCYLLA_USERNAME := $(shell grep -E '^SCYLLA_USERNAME=' .env | awk -F '=' '{gsub(/^[[:space:]]*|[[:space:]]*$$/, "", $$2); print substr($$2, 2, length($$2)-2)}') - export SCYLLA_PASSWORD := $(shell grep -E '^SCYLLA_PASSWORD=' .env | awk -F '=' '{gsub(/^[[:space:]]*|[[:space:]]*$$/, "", $$2); print substr($$2, 2, length($$2)-2)}') +# Load .env file +ifneq (,$(wildcard ./.env)) + include .env + export endif .PHONY: print-env print-env: ## Prints the loaded environment variables from the .env file. - @echo "SCYLLA_NODES=$(SCYLLA_NODES)" - @echo "SCYLLA_KEYSPACE=$(SCYLLA_KEYSPACE)" - @echo "SCYLLA_USERNAME=$(SCYLLA_USERNAME)" - @echo "SCYLLA_PASSWORD=$(SCYLLA_PASSWORD)" + $(foreach v, $(.VARIABLES), $(info $(v)=$($(v)))) .PHONY: migrate migrate: ## Runs database migrations - @migrate --host=$(SCYLLA_NODES) --keyspace=$(SCYLLA_KEYSPACE) $(if $(SCYLLA_USERNAME), --user=$(SCYLLA_USERNAME),) $(if $(SCYLLA_PASSWORD),--password=$(SCYLLA_PASSWORD),) + @migrate --host=$(SCYLLA_NODES) --keyspace=$(SCYLLA_KEYSPACE) $(if $(SCYLLA_USERNAME),--user=$(SCYLLA_USERNAME)) $(if $(SCYLLA_PASSWORD),--password=$(SCYLLA_PASSWORD)) .PHONY: keyspace keyspace: ## Configures the keyspace in the ScyllaDB - @toolkit keyspace --host=$(SCYLLA_NODES) --keyspace=$(SCYLLA_KEYSPACE) --replication-factor="1" $(if $(SCYLLA_USERNAME), --user=$(SCYLLA_USERNAME),) $(if $(SCYLLA_PASSWORD),--password=$(SCYLLA_PASSWORD),) + @toolkit keyspace --host=$(SCYLLA_NODES) --keyspace=$(SCYLLA_KEYSPACE) --replication-factor="1" $(if $(SCYLLA_USERNAME),--user=$(SCYLLA_USERNAME)) $(if $(SCYLLA_PASSWORD),--password=$(SCYLLA_PASSWORD)) .PHONY: watch watch: ## Watches for changes in the source files and runs the project on each change. diff --git a/current_schema.json b/current_schema.json deleted file mode 100644 index e69de29..0000000 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..989d11f --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,23 @@ +services: + tbp-server: + build: + context: . + dockerfile: .docker/Dockerfile.prod + image: tbp-consumer-api:${APP_VERSION} + hostname: prod + restart: on-failure + ports: + - "3001-3003:3000" + deploy: + replicas: 3 + resources: + limits: + cpus: '0.3' + memory: '300MB' + networks: + - tbp-consumer-api + stop_signal: SIGTERM +networks: + tbp-consumer-api: + name: tbp-consumer-api + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index ab4634d..d1edb55 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,15 +1,15 @@ services: - tbp-consumer-api: + tbp-server: build: context: . dockerfile: .docker/Dockerfile.dev tty: true stdin_open: true image: tbp-consumer-api:dev - container_name: tbp-consumer-api + container_name: tbp-server hostname: dev ports: - - "8001:8001" + - "3000:3000" env_file: - .env.docker volumes: diff --git a/src/config/app.rs b/src/config/app.rs index 7a3d9d0..a972512 100644 --- a/src/config/app.rs +++ b/src/config/app.rs @@ -1,5 +1,4 @@ use crate::config::Config; -use dotenvy::dotenv; use scylla::{CachingSession, Session, SessionBuilder}; use std::sync::Arc; use std::time::Duration; @@ -12,8 +11,6 @@ pub struct AppState { impl AppState { pub async fn new() -> Self { - dotenv().expect(".env file not found"); - let config = Config::new(); let session: Session = SessionBuilder::new() .known_nodes(config.database.nodes) diff --git a/src/main.rs b/src/main.rs index 44d2315..98c935a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use std::io::BufReader; use actix_cors::Cors; use actix_web::web::Data; use actix_web::{App, HttpServer}; +use dotenvy::dotenv; use log::debug; use self::http::v1; @@ -16,7 +17,7 @@ mod models; #[actix_web::main] async fn main() -> std::io::Result<()> { - dotenvy::dotenv().expect(".env file not found"); + dotenv().ok(); colog::init(); let app_data = AppState::new().await;