feat: worker crate, cargo-generate config, liquid templates, docker
This commit is contained in:
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,3 +1,9 @@
|
|||||||
/target
|
/target
|
||||||
|
**/*.rs.bk
|
||||||
.env
|
.env
|
||||||
*.db
|
data.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
**/dev.db
|
||||||
|
|||||||
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -2486,6 +2486,16 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "worker"
|
name = "worker"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"adapters-sqlite",
|
||||||
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
|
"domain",
|
||||||
|
"dotenvy",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "writeable"
|
name = "writeable"
|
||||||
|
|||||||
26
Dockerfile
26
Dockerfile
@@ -1,27 +1,11 @@
|
|||||||
FROM rust:1.92 AS builder
|
FROM rust:1.85-slim AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . .
|
COPY . .
|
||||||
|
RUN cargo build --release -p bootstrap
|
||||||
# Build the release binary
|
|
||||||
RUN cargo build --release -p api
|
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
COPY --from=builder /app/target/release/server ./server
|
||||||
# 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
|
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
CMD ["./server"]
|
||||||
CMD ["./api"]
|
|
||||||
|
|||||||
@@ -1,33 +1,35 @@
|
|||||||
[template]
|
[template]
|
||||||
cargo_generate_version = ">=0.21.0"
|
cargo_generate_version = ">=0.21.0"
|
||||||
ignore = [".git", "target", ".idea", ".vscode", "data.db"]
|
ignore = [".git", "target", ".idea", ".vscode", "data.db", "*.liquid", "**/.sqlx", "**/dev.db"]
|
||||||
|
|
||||||
[filenames]
|
|
||||||
"api/Cargo.toml.template" = "api/Cargo.toml"
|
|
||||||
"infra/Cargo.toml.template" = "infra/Cargo.toml"
|
|
||||||
|
|
||||||
[placeholders.project_name]
|
[placeholders.project_name]
|
||||||
type = "string"
|
type = "string"
|
||||||
prompt = "Project name"
|
prompt = "Project name (snake_case)"
|
||||||
|
|
||||||
[placeholders.database]
|
[placeholders.database]
|
||||||
type = "string"
|
type = "string"
|
||||||
prompt = "Database type"
|
prompt = "Database backend"
|
||||||
choices = ["sqlite", "postgres"]
|
choices = ["sqlite", "postgres"]
|
||||||
default = "sqlite"
|
default = "sqlite"
|
||||||
|
|
||||||
[placeholders.auth_jwt]
|
[placeholders.worker]
|
||||||
type = "bool"
|
type = "bool"
|
||||||
prompt = "Enable JWT authentication (Bearer tokens)?"
|
prompt = "Include background worker binary?"
|
||||||
default = true
|
default = false
|
||||||
|
|
||||||
[placeholders.auth_oidc]
|
[placeholders.auth_oidc]
|
||||||
type = "bool"
|
type = "bool"
|
||||||
prompt = "Enable OIDC integration (Login with Google, etc.)?"
|
prompt = "Include OIDC/OAuth2 adapter stub?"
|
||||||
default = true
|
default = false
|
||||||
|
|
||||||
[conditional.'database == "sqlite"']
|
[conditional.'database == "sqlite"']
|
||||||
ignore = ["migrations_postgres"]
|
ignore = ["crates/adapters/postgres"]
|
||||||
|
|
||||||
[conditional.'database == "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"]
|
||||||
|
|||||||
85
compose.yml
85
compose.yml
@@ -1,89 +1,14 @@
|
|||||||
services:
|
services:
|
||||||
backend:
|
app:
|
||||||
build: .
|
build: .
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
environment:
|
environment:
|
||||||
- SESSION_SECRET=dev_secret_key_12345
|
DATABASE_URL: sqlite:///data/app.db
|
||||||
- DATABASE_URL=sqlite:///app/data/notes.db
|
JWT_SECRET: change-me-in-production
|
||||||
- CORS_ALLOWED_ORIGINS=http://localhost:8080,http://localhost:5173
|
RUST_LOG: info
|
||||||
- HOST=0.0.0.0
|
|
||||||
- PORT=3000
|
|
||||||
- DB_MAX_CONNECTIONS=5
|
|
||||||
- DB_MIN_CONNECTIONS=1
|
|
||||||
- SECURE_COOKIE=true
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/app/data
|
- db_data:/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"
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db_data:
|
db_data:
|
||||||
zitadel_db_data:
|
|
||||||
19
crates/adapters/auth/Cargo.toml.liquid
Normal file
19
crates/adapters/auth/Cargo.toml.liquid
Normal file
@@ -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 %}
|
||||||
27
crates/bootstrap/Cargo.toml.liquid
Normal file
27
crates/bootstrap/Cargo.toml.liquid
Normal file
@@ -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 }
|
||||||
18
crates/worker/Cargo.toml
Normal file
18
crates/worker/Cargo.toml
Normal file
@@ -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 }
|
||||||
23
crates/worker/Cargo.toml.liquid
Normal file
23
crates/worker/Cargo.toml.liquid
Normal file
@@ -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 }
|
||||||
18
crates/worker/src/config.rs
Normal file
18
crates/worker/src/config.rs
Normal file
@@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
crates/worker/src/job.rs
Normal file
7
crates/worker/src/job.rs
Normal file
@@ -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<()>;
|
||||||
|
}
|
||||||
14
crates/worker/src/jobs/example.rs
Normal file
14
crates/worker/src/jobs/example.rs
Normal file
@@ -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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
2
crates/worker/src/jobs/mod.rs
Normal file
2
crates/worker/src/jobs/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod example;
|
||||||
|
pub use example::ExampleJob;
|
||||||
34
crates/worker/src/main.rs
Normal file
34
crates/worker/src/main.rs
Normal file
@@ -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(())
|
||||||
|
}
|
||||||
34
crates/worker/src/runner.rs
Normal file
34
crates/worker/src/runner.rs
Normal file
@@ -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<dyn Job>, Duration)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JobRunner {
|
||||||
|
pub fn new() -> Self { Self { jobs: vec![] } }
|
||||||
|
|
||||||
|
pub fn register(mut self, job: Arc<dyn Job>, 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() } }
|
||||||
Reference in New Issue
Block a user