From 5b0d5bf15d03ff4ab79197ea67aaa2a767387098 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Mon, 18 May 2026 00:26:17 +0200 Subject: [PATCH] feat: worker crate, cargo-generate config, liquid templates, docker --- .gitignore | 8 ++- Cargo.lock | 10 +++ Dockerfile | 26 ++------ cargo-generate.toml | 30 ++++----- compose.yml | 85 ++------------------------ crates/adapters/auth/Cargo.toml.liquid | 19 ++++++ crates/bootstrap/Cargo.toml.liquid | 27 ++++++++ crates/worker/Cargo.toml | 18 ++++++ crates/worker/Cargo.toml.liquid | 23 +++++++ crates/worker/src/config.rs | 18 ++++++ crates/worker/src/job.rs | 7 +++ crates/worker/src/jobs/example.rs | 14 +++++ crates/worker/src/jobs/mod.rs | 2 + crates/worker/src/main.rs | 34 +++++++++++ crates/worker/src/runner.rs | 34 +++++++++++ 15 files changed, 239 insertions(+), 116 deletions(-) create mode 100644 crates/adapters/auth/Cargo.toml.liquid create mode 100644 crates/bootstrap/Cargo.toml.liquid create mode 100644 crates/worker/Cargo.toml create mode 100644 crates/worker/Cargo.toml.liquid create mode 100644 crates/worker/src/config.rs create mode 100644 crates/worker/src/job.rs create mode 100644 crates/worker/src/jobs/example.rs create mode 100644 crates/worker/src/jobs/mod.rs create mode 100644 crates/worker/src/main.rs create mode 100644 crates/worker/src/runner.rs diff --git a/.gitignore b/.gitignore index 0707444..23fe3c9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ /target +**/*.rs.bk .env -*.db \ No newline at end of file +data.db +*.db-shm +*.db-wal +.idea/ +.vscode/ +**/dev.db diff --git a/Cargo.lock b/Cargo.lock index 96f6b25..50fe469 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2486,6 +2486,16 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "worker" version = "0.1.0" +dependencies = [ + "adapters-sqlite", + "anyhow", + "async-trait", + "domain", + "dotenvy", + "tokio", + "tracing", + "tracing-subscriber", +] [[package]] name = "writeable" diff --git a/Dockerfile b/Dockerfile index 3d34f64..def3d45 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,27 +1,11 @@ -FROM rust:1.92 AS builder - +FROM rust:1.85-slim AS builder WORKDIR /app COPY . . - -# Build the release binary -RUN cargo build --release -p api +RUN cargo build --release -p bootstrap FROM debian:bookworm-slim - +RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* WORKDIR /app - -# Install OpenSSL (required for many Rust networking crates) and CA certificates -RUN apt-get update && apt-get install -y libssl3 ca-certificates && rm -rf /var/lib/apt/lists/* - -COPY --from=builder /app/target/release/api . - - -# Create data directory for SQLite -RUN mkdir -p /app/data - -ENV DATABASE_URL=sqlite:///app/data/template.db -ENV SESSION_SECRET=supersecretchangeinproduction - +COPY --from=builder /app/target/release/server ./server EXPOSE 3000 - -CMD ["./api"] +CMD ["./server"] diff --git a/cargo-generate.toml b/cargo-generate.toml index e499676..fd14dc3 100644 --- a/cargo-generate.toml +++ b/cargo-generate.toml @@ -1,33 +1,35 @@ [template] cargo_generate_version = ">=0.21.0" -ignore = [".git", "target", ".idea", ".vscode", "data.db"] - -[filenames] -"api/Cargo.toml.template" = "api/Cargo.toml" -"infra/Cargo.toml.template" = "infra/Cargo.toml" +ignore = [".git", "target", ".idea", ".vscode", "data.db", "*.liquid", "**/.sqlx", "**/dev.db"] [placeholders.project_name] type = "string" -prompt = "Project name" +prompt = "Project name (snake_case)" [placeholders.database] type = "string" -prompt = "Database type" +prompt = "Database backend" choices = ["sqlite", "postgres"] default = "sqlite" -[placeholders.auth_jwt] +[placeholders.worker] type = "bool" -prompt = "Enable JWT authentication (Bearer tokens)?" -default = true +prompt = "Include background worker binary?" +default = false [placeholders.auth_oidc] type = "bool" -prompt = "Enable OIDC integration (Login with Google, etc.)?" -default = true +prompt = "Include OIDC/OAuth2 adapter stub?" +default = false [conditional.'database == "sqlite"'] -ignore = ["migrations_postgres"] +ignore = ["crates/adapters/postgres"] [conditional.'database == "postgres"'] -ignore = ["migrations_sqlite"] +ignore = ["crates/adapters/sqlite"] + +[conditional.'!worker'] +ignore = ["crates/worker"] + +[conditional.'!auth_oidc'] +ignore = ["crates/adapters/auth/src/oidc.rs"] diff --git a/compose.yml b/compose.yml index ba6ca5b..a4980f1 100644 --- a/compose.yml +++ b/compose.yml @@ -1,89 +1,14 @@ services: - backend: + app: build: . ports: - "3000:3000" environment: - - SESSION_SECRET=dev_secret_key_12345 - - DATABASE_URL=sqlite:///app/data/notes.db - - CORS_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5173 - - HOST=0.0.0.0 - - PORT=3000 - - DB_MAX_CONNECTIONS=5 - - DB_MIN_CONNECTIONS=1 - - SECURE_COOKIE=true + DATABASE_URL: sqlite:///data/app.db + JWT_SECRET: change-me-in-production + RUST_LOG: info volumes: - - ./data:/app/data - - # nats: - # image: nats:alpine - # ports: - # - "4222:4222" - # - "6222:6222" - # - "8222:8222" - # restart: unless-stopped - - db: - image: postgres:15-alpine - environment: - POSTGRES_USER: user - POSTGRES_PASSWORD: password - POSTGRES_DB: k_template_db - ports: - - "5439:5432" - volumes: - - db_data:/var/lib/postgresql/data - - zitadel-db: - image: postgres:16-alpine - container_name: zitadel_db - environment: - POSTGRES_USER: zitadel - POSTGRES_PASSWORD: zitadel_password - POSTGRES_DB: zitadel - healthcheck: - test: ["CMD-SHELL", "pg_isready -U zitadel -d zitadel"] - interval: 10s - timeout: 5s - retries: 5 - volumes: - - zitadel_db_data:/var/lib/postgresql/data - - zitadel: - image: ghcr.io/zitadel/zitadel:latest - container_name: zitadel_local - depends_on: - zitadel-db: - condition: service_healthy - ports: - - "8086:8080" - # USE start-from-init (Fixes the "relation does not exist" bug) - command: 'start-from-init --masterkey "MasterkeyNeedsToBeExactly32Bytes"' - environment: - # Database Connection - ZITADEL_DATABASE_POSTGRES_HOST: zitadel-db - ZITADEL_DATABASE_POSTGRES_PORT: 5432 - ZITADEL_DATABASE_POSTGRES_DATABASE: zitadel - - # APPLICATION USER (Zitadel uses this to run) - ZITADEL_DATABASE_POSTGRES_USER_USERNAME: zitadel - ZITADEL_DATABASE_POSTGRES_USER_PASSWORD: zitadel_password - ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE: disable - - # ADMIN USER (Zitadel uses this to create tables/migrations) - # We use 'zitadel' because it is the owner of the DB in your postgres container. - ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME: zitadel - ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD: zitadel_password - ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE: disable - - # General Config - ZITADEL_EXTERNALDOMAIN: localhost - ZITADEL_EXTERNALPORT: 8086 - ZITADEL_EXTERNALSECURE: "false" - ZITADEL_TLS_ENABLED: "false" - - ZITADEL_DEFAULTINSTANCE_FEATURES_LOGINV2_REQUIRED: "false" + - db_data:/data volumes: db_data: - zitadel_db_data: \ No newline at end of file diff --git a/crates/adapters/auth/Cargo.toml.liquid b/crates/adapters/auth/Cargo.toml.liquid new file mode 100644 index 0000000..81c91e6 --- /dev/null +++ b/crates/adapters/auth/Cargo.toml.liquid @@ -0,0 +1,19 @@ +[package] +name = "adapters-auth" +version = "0.1.0" +edition = "2024" + +[dependencies] +domain = { workspace = true } +async-trait = { workspace = true } +anyhow = { workspace = true } +jsonwebtoken = { workspace = true } +bcrypt = { workspace = true } +serde = { workspace = true } +uuid = { workspace = true } +chrono = { workspace = true } +tokio = { workspace = true } +{% if auth_oidc %} +openidconnect = "3" +reqwest = { version = "0.12", features = ["json"] } +{% endif %} diff --git a/crates/bootstrap/Cargo.toml.liquid b/crates/bootstrap/Cargo.toml.liquid new file mode 100644 index 0000000..abeeb73 --- /dev/null +++ b/crates/bootstrap/Cargo.toml.liquid @@ -0,0 +1,27 @@ +[package] +name = "bootstrap" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "{{project_name}}" +path = "src/main.rs" + +[dependencies] +domain = { workspace = true } +application = { workspace = true } +adapters-auth = { workspace = true } +presentation = { workspace = true } +{% if database == "sqlite" %} +adapters-sqlite = { path = "../adapters/sqlite" } +{% endif %} +{% if database == "postgres" %} +adapters-postgres = { path = "../adapters/postgres" } +{% endif %} +tokio = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +dotenvy = { workspace = true } +tower-http = { workspace = true } +axum = { workspace = true } diff --git a/crates/worker/Cargo.toml b/crates/worker/Cargo.toml new file mode 100644 index 0000000..8d5c697 --- /dev/null +++ b/crates/worker/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "worker" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "worker" +path = "src/main.rs" + +[dependencies] +domain = { workspace = true } +adapters-sqlite = { path = "../adapters/sqlite" } +tokio = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +dotenvy = { workspace = true } +async-trait = { workspace = true } diff --git a/crates/worker/Cargo.toml.liquid b/crates/worker/Cargo.toml.liquid new file mode 100644 index 0000000..e386486 --- /dev/null +++ b/crates/worker/Cargo.toml.liquid @@ -0,0 +1,23 @@ +[package] +name = "worker" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "{{project_name}}-worker" +path = "src/main.rs" + +[dependencies] +domain = { workspace = true } +{% if database == "sqlite" %} +adapters-sqlite = { path = "../adapters/sqlite" } +{% endif %} +{% if database == "postgres" %} +adapters-postgres = { path = "../adapters/postgres" } +{% endif %} +tokio = { workspace = true } +anyhow = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +dotenvy = { workspace = true } +async-trait = { workspace = true } diff --git a/crates/worker/src/config.rs b/crates/worker/src/config.rs new file mode 100644 index 0000000..4a176fe --- /dev/null +++ b/crates/worker/src/config.rs @@ -0,0 +1,18 @@ +#[derive(Debug, Clone)] +pub struct WorkerConfig { + pub database_url: String, + pub example_job_interval_secs: u64, +} + +impl WorkerConfig { + pub fn from_env() -> Self { + dotenvy::dotenv().ok(); + Self { + database_url: std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"), + example_job_interval_secs: std::env::var("EXAMPLE_JOB_INTERVAL_SECS") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(60), + } + } +} diff --git a/crates/worker/src/job.rs b/crates/worker/src/job.rs new file mode 100644 index 0000000..19da8a8 --- /dev/null +++ b/crates/worker/src/job.rs @@ -0,0 +1,7 @@ +use async_trait::async_trait; + +#[async_trait] +pub trait Job: Send + Sync { + fn name(&self) -> &str; + async fn run(&self) -> anyhow::Result<()>; +} diff --git a/crates/worker/src/jobs/example.rs b/crates/worker/src/jobs/example.rs new file mode 100644 index 0000000..ba9784b --- /dev/null +++ b/crates/worker/src/jobs/example.rs @@ -0,0 +1,14 @@ +use async_trait::async_trait; +use tracing::info; +use crate::job::Job; + +pub struct ExampleJob; + +#[async_trait] +impl Job for ExampleJob { + fn name(&self) -> &str { "example" } + async fn run(&self) -> anyhow::Result<()> { + info!("example job ran — replace with real work"); + Ok(()) + } +} diff --git a/crates/worker/src/jobs/mod.rs b/crates/worker/src/jobs/mod.rs new file mode 100644 index 0000000..c03205c --- /dev/null +++ b/crates/worker/src/jobs/mod.rs @@ -0,0 +1,2 @@ +pub mod example; +pub use example::ExampleJob; diff --git a/crates/worker/src/main.rs b/crates/worker/src/main.rs new file mode 100644 index 0000000..d0244f9 --- /dev/null +++ b/crates/worker/src/main.rs @@ -0,0 +1,34 @@ +use std::sync::Arc; +use std::time::Duration; +use tracing::info; + +mod config; +mod job; +mod jobs; +mod runner; + +use jobs::ExampleJob; +use runner::JobRunner; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive("worker=info".parse()?), + ) + .init(); + + let config = config::WorkerConfig::from_env(); + info!("Worker starting"); + + let _pool = adapters_sqlite::connect(&config.database_url).await?; + adapters_sqlite::run_migrations(&_pool).await?; + + let interval = Duration::from_secs(config.example_job_interval_secs); + let runner = JobRunner::new().register(Arc::new(ExampleJob), interval); + + info!("Worker running"); + runner.run().await; + Ok(()) +} diff --git a/crates/worker/src/runner.rs b/crates/worker/src/runner.rs new file mode 100644 index 0000000..23b6bc5 --- /dev/null +++ b/crates/worker/src/runner.rs @@ -0,0 +1,34 @@ +use std::sync::Arc; +use std::time::Duration; +use tracing::{error, info}; +use crate::job::Job; + +pub struct JobRunner { + jobs: Vec<(Arc, Duration)>, +} + +impl JobRunner { + pub fn new() -> Self { Self { jobs: vec![] } } + + pub fn register(mut self, job: Arc, interval: Duration) -> Self { + self.jobs.push((job, interval)); + self + } + + pub async fn run(self) { + let handles: Vec<_> = self.jobs.into_iter().map(|(job, interval)| { + tokio::spawn(async move { + loop { + info!(job = job.name(), "running job"); + if let Err(e) = job.run().await { + error!(job = job.name(), error = %e, "job failed"); + } + tokio::time::sleep(interval).await; + } + }) + }).collect(); + for handle in handles { let _ = handle.await; } + } +} + +impl Default for JobRunner { fn default() -> Self { Self::new() } }