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
|
||||
**/*.rs.bk
|
||||
.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]]
|
||||
name = "worker"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"adapters-sqlite",
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"domain",
|
||||
"dotenvy",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
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
|
||||
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"]
|
||||
|
||||
@@ -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"]
|
||||
|
||||
85
compose.yml
85
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:
|
||||
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