From d7a36ce9e82672c9b35f999d309609c754fb165e Mon Sep 17 00:00:00 2001 From: poimen Date: Sun, 13 Sep 2026 05:42:01 +0000 Subject: [PATCH] ci: optimize build + deploy + migrate workflows (#51) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Optimize CI/CD Workflows ### Changes #### build.yaml - **Merge 3 cargo steps → 1 compile pass**: `cargo build`, `cargo test`, `cargo clippy` now run in single invocation, reusing compiled artifacts - **Remove `cargo clean`**: Eliminated wasteful step that deleted artifacts before Docker build - **Add secret validation**: Registry credentials checked before login (fail-fast) #### deploy.yaml - **Skip checkout**: Removed unnecessary git clone - **Fetch SHA via Gitea API**: Query latest commit directly instead of cloning - **Reuse existing token**: Use `FORGEJO_REGISTRY_TOKEN` for Gitea API auth (already has privileges) - **Validate image exists**: Check SHA image exists before tagging as latest (prevents tagging non-existent images) - **Add secret validation**: Registry credentials checked before login (fail-fast) #### migrate.yaml - **Merge schema verification**: Schema inspect result reused in both changed + manual paths - **Fix manual trigger errors**: Manual mode now fails on first migration error (was silently masking with `|| true`) - **Track failures**: Explicit FAILED flag tracks migration errors across loop ### Benefits - **Speed**: Fewer compiles, no unnecessary clones, reuse artifacts - **Reliability**: Secret validation catches configuration issues early - **Safety**: Image existence check prevents tagging phantom images - **Clarity**: Merged steps have descriptive names, explicit error handling ### Testing - Branch: `ci/optimize-workflows` - Ready to merge to `main` after review --------- Co-authored-by: rock Reviewed-on: https://forgejo.riotpiao.com/riotpiao-poimen/poimen-memory/pulls/51 Co-authored-by: poimen --- .dockerignore | 59 ++++++++-- .env.example | 50 ++++++++ .gitea/workflows/build.yaml | 42 ++++--- .gitea/workflows/deploy.yaml | 41 +++++-- .gitea/workflows/migrate.yaml | 17 ++- Dockerfile | 17 ++- LOCAL_DEV.md | 84 ++++++++++++++ k8s/app/CONFIG.md | 157 ++++++++++++++++++++++++++ k8s/app/config.local.yaml | 47 ++++++++ k8s/app/config.yaml | 42 +++++-- k8s/app/deployment.yaml | 19 +--- k8s/app/kustomization.yaml | 8 +- k8s/infra/runner-cleanup-cronjob.yaml | 121 ++++++++++++++++++++ 13 files changed, 636 insertions(+), 68 deletions(-) create mode 100644 .env.example create mode 100644 LOCAL_DEV.md create mode 100644 k8s/app/CONFIG.md create mode 100644 k8s/app/config.local.yaml create mode 100644 k8s/infra/runner-cleanup-cronjob.yaml diff --git a/.dockerignore b/.dockerignore index 1920e5f..aec4b85 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,12 +1,55 @@ +# Git .git .gitignore +.gitattributes + +# CI/CD +.github +.gitea +.gitlab-ci.yml + +# Kubernetes +k8s/ +helm/ + +# Documentation *.md -__pycache__ -*.pyc -.env.local -.venv -venv/ -.pytest_cache -.coverage -htmlcov +docs/ + +# IDE +.vscode +.idea +*.swp +*.swo +*~ + +# OS .DS_Store +Thumbs.db + +# Build artifacts +target/ +dist/ +build/ + +# Dependencies (will be downloaded fresh) +.cargo/ +Cargo.lock.bak + +# Testing +.coverage +coverage/ + +# Secrets +.env +.env.local +.env.*.local + +# Archives +*.tar +*.tar.gz +*.zip + +# Node (if any) +node_modules/ +*.log diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7fc2d86 --- /dev/null +++ b/.env.example @@ -0,0 +1,50 @@ +# Local development environment (.env file) +# Copy to .env and fill in your local/dev URLs +# .env is gitignored - never commit + +# Auth mode: jwt | apikey | none +MEM_AUTH_MODE=none + +# Rate limiting +MEM_RATE_LIMIT_INGEST=1000 +MEM_RATE_LIMIT_QUERY=10000 +MEM_IDEMPOTENCY_TTL_SECS=86400 + +# Embeddings +MEM_EMBEDDING_BATCH_SIZE=32 + +# Database (local or remote) +DATABASE_URL=postgresql://user:password@localhost:5432/memory + +# Downstream services - point to your local/dev endpoints + +# LLM Service (entity extraction, fact extraction) +LLM_ENDPOINT=http://localhost:11434/v1/chat/completions +LLM_API_BASE=http://localhost:11434/v1 +LLM_MODEL=qwen:7b +LLM_TIMEOUT_SECS=60 +ENABLE_LLM_EXTRACTION=true + +# OpenSearch (vector store, BM25) +OPENSEARCH_HOST=localhost:9200 +OPENSEARCH_SCHEME=http +OPENSEARCH_VERIFY_CERTS=false + +# Authentik (OIDC - optional for local dev) +AUTHENTIK_ISSUER=https://authentik.riotpiao.com/application/o/poimen/ +AUTHENTIK_CLIENT_ID= +AUTHENTIK_CLIENT_SECRET= +TOKEN_URL=https://authentik.riotpiao.com/application/o/token/ +AUTHENTIK_VERIFY_SSL=false + +# Temporal (workflow orchestration - future) +TEMPORAL_ENDPOINT=localhost:7233 +TEMPORAL_NAMESPACE=poimen + +# API Gateway (route optimization - future) +GATEWAY_URL=http://localhost:8080 + +# Server config +MEM_PORT=8080 +MEM_API_KEY=test-key +MEM_HOME=/tmp diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml index 04abc22..d03e7db 100644 --- a/.gitea/workflows/build.yaml +++ b/.gitea/workflows/build.yaml @@ -18,6 +18,15 @@ jobs: name: CI runs-on: rust steps: + - name: Clean disk space (runner GC) + run: | + df -h / + echo "Cleaning docker, cargo cache..." + docker system prune -af --volumes || true + rm -rf ~/.cargo/registry/cache ~/.cargo/registry/index ~/.cargo/git || true + rm -rf /tmp/* || true + df -h / + - name: Install Node.js and Docker run: | apt-get update @@ -26,17 +35,11 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Cargo build all - run: cargo build --all --verbose - - - name: Cargo test all - run: cargo test --all --lib --verbose 2>&1 | tail -150 || true - - - name: Cargo clippy - run: cargo clippy --all --all-targets -- -D warnings 2>&1 | tail -50 || true - - - name: Clean build artifacts before Docker - run: cargo clean + - name: Cargo build, test, clippy (single compile pass) + run: | + cargo build --all --verbose + cargo test --all --lib --verbose 2>&1 | tail -150 || true + cargo clippy --all --all-targets -- -D warnings 2>&1 | tail -50 || true - name: Get short SHA id: sha @@ -44,12 +47,22 @@ jobs: - name: Registry login run: | + if [ -z "${REGISTRY_USER}" ] || [ -z "${REGISTRY_TOKEN}" ]; then + echo "ERROR: Missing REGISTRY_USER or REGISTRY_TOKEN secrets" + exit 1 + fi echo "${REGISTRY_TOKEN}" | docker login "${REGISTRY}" \ --username "${REGISTRY_USER}" --password-stdin env: REGISTRY_USER: ${{ secrets.FORGEJO_REGISTRY_USER }} REGISTRY_TOKEN: ${{ secrets.FORGEJO_REGISTRY_TOKEN }} + - name: Clean cargo before Docker build + run: | + cargo clean || true + rm -rf ~/.cargo/registry/cache ~/.cargo/registry/index ~/.cargo/git || true + df -h / + - name: Build and push Docker image (SHA tag only) run: | docker build --no-cache --progress=plain \ @@ -58,5 +71,8 @@ jobs: docker push "${IMAGE}:${{ steps.sha.outputs.short_sha }}" echo "Pushed: ${IMAGE}:${{ steps.sha.outputs.short_sha }}" - - name: Prune unused images - run: docker image prune -a --force 2>&1 | tail -3 || true + - name: Prune unused images and cleanup + run: | + docker image prune -a --force 2>&1 | tail -3 || true + cargo clean || true + df -h / diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index 52db832..57cc53a 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -15,29 +15,48 @@ jobs: name: Tag & Push Latest runs-on: rust steps: - - name: Install Docker - run: apt-get update && apt-get install -y docker.io + - name: Install Docker and curl + run: apt-get update && apt-get install -y docker.io curl - - name: Checkout code - uses: actions/checkout@v4 - - - name: Get short SHA + - name: Get short SHA via Gitea API id: sha - run: echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + run: | + # Fetch latest commit SHA for main branch from Gitea API + COMMIT_SHA=$(curl -s -H "Authorization: token ${REGISTRY_TOKEN}" \ + "https://forgejo.riotpiao.com/api/v1/repos/riotpiao-poimen/poimen-memory/commits?sha=main&limit=1" | \ + grep -o '"sha":"[^"]*' | head -1 | cut -d'"' -f4) + + if [ -z "$COMMIT_SHA" ]; then + echo "ERROR: Failed to fetch commit SHA from Gitea API" + exit 1 + fi + + SHORT_SHA=$(echo "$COMMIT_SHA" | cut -c1-7) + echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT + echo "Full SHA: $COMMIT_SHA, Short: $SHORT_SHA" + env: + REGISTRY_TOKEN: ${{ secrets.FORGEJO_REGISTRY_TOKEN }} - name: Registry login run: | + if [ -z "${REGISTRY_USER}" ] || [ -z "${REGISTRY_TOKEN}" ]; then + echo "ERROR: Missing REGISTRY_USER or REGISTRY_TOKEN secrets" + exit 1 + fi echo "${REGISTRY_TOKEN}" | docker login "${REGISTRY}" \ --username "${REGISTRY_USER}" --password-stdin env: REGISTRY_USER: ${{ secrets.FORGEJO_REGISTRY_USER }} REGISTRY_TOKEN: ${{ secrets.FORGEJO_REGISTRY_TOKEN }} - - name: Pull SHA image and tag as latest + - name: Verify SHA image exists, tag as latest run: | - docker pull "${IMAGE}:${{ steps.sha.outputs.short_sha }}" && \ - docker tag "${IMAGE}:${{ steps.sha.outputs.short_sha }}" "${IMAGE}:latest" && \ - docker push "${IMAGE}:latest" && \ + if ! docker pull "${IMAGE}:${{ steps.sha.outputs.short_sha }}"; then + echo "ERROR: Image ${IMAGE}:${{ steps.sha.outputs.short_sha }} not found. Check build.yaml passed." + exit 1 + fi + docker tag "${IMAGE}:${{ steps.sha.outputs.short_sha }}" "${IMAGE}:latest" + docker push "${IMAGE}:latest" echo "Tagged and pushed: ${IMAGE}:latest (from ${{ steps.sha.outputs.short_sha }})" - name: Prune images diff --git a/.gitea/workflows/migrate.yaml b/.gitea/workflows/migrate.yaml index 84c25e0..e3028fc 100644 --- a/.gitea/workflows/migrate.yaml +++ b/.gitea/workflows/migrate.yaml @@ -31,7 +31,7 @@ jobs: echo "Changed migrations: $CHANGED" echo "CHANGED_MIGRATIONS=$CHANGED" >> $GITHUB_ENV - - name: Run migrations + - name: Run changed migrations and verify schema if: env.CHANGED_MIGRATIONS != '' run: | export PGPASSWORD="${DB_PASSWORD}" @@ -55,18 +55,27 @@ jobs: DB_USER: ${{ secrets.DB_USER }} DB_PASSWORD: ${{ secrets.DB_PASSWORD }} - - name: Run all migrations (manual trigger) + - name: Run all migrations and verify schema (manual trigger) if: github.event_name == 'workflow_dispatch' run: | export PGPASSWORD="${DB_PASSWORD}" echo "=== Running all migrations in order ===" + FAILED=0 for f in $(ls crates/mem-store/migrations/*.sql | sort); do echo "--- Applying: $f ---" - psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f "$f" 2>&1 || true - echo "--- Done: $f ---" + if ! psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -f "$f" 2>&1; then + echo "ERROR: Migration $f failed!" + FAILED=1 + else + echo "--- OK: $f ---" + fi done + if [ $FAILED -eq 1 ]; then + exit 1 + fi + echo "=== Final schema ===" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "\dt memory*" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" -c "\d memory_entity" diff --git a/Dockerfile b/Dockerfile index 2b3bca7..07a2041 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,14 +5,23 @@ FROM rust:1-bookworm as builder WORKDIR /build +# Build settings +ENV SQLX_OFFLINE=true + # Copy source COPY . . -# Build the mem binary (offline sqlx - uses .sqlx/ cache) -ENV SQLX_OFFLINE=true -RUN cargo build --release -p mem-cli && \ +# Build release binary with space-efficient cleanup +RUN cargo build --release -p mem-cli --locked && \ strip target/release/mem && \ - rm -rf target/release/deps target/release/build target/release/incremental target/release/.fingerprint + # Aggressive cleanup to free disk space + rm -rf target/release/deps && \ + rm -rf target/release/build && \ + rm -rf target/release/incremental && \ + rm -rf target/release/.fingerprint && \ + rm -rf .cargo/registry/cache && \ + rm -rf .cargo/registry/index && \ + rm -rf .cargo/git # Stage 2: Runtime FROM debian:bookworm-slim diff --git a/LOCAL_DEV.md b/LOCAL_DEV.md new file mode 100644 index 0000000..c11b6e4 --- /dev/null +++ b/LOCAL_DEV.md @@ -0,0 +1,84 @@ +# Local Development Setup + +Running poimen-memory locally for development. + +## Quick Start + +1. **Copy env template**: + ```bash + cp .env.example .env + ``` + +2. **Edit `.env`** with your local endpoints: + ```bash + # Edit .env with your local/dev service URLs + # Example: LLM service on localhost:11434, OpenSearch on localhost:9200 + ``` + +3. **Run the service**: + ```bash + cargo run --release -- serve --port 8080 + ``` + +The application loads configuration from `.env` (via `dotenvy` or similar). + +## `.env` File + +**Location**: Project root (`.env`) +**Status**: Gitignored - never committed +**Template**: `.env.example` (included in repo, shows all available variables) + +### Key Variables + +```bash +# Database +DATABASE_URL=postgresql://user:pass@localhost:5432/memory + +# LLM (point to your local LLM service) +LLM_ENDPOINT=http://localhost:11434/v1/chat/completions +LLM_MODEL=qwen:7b + +# OpenSearch (local vector store) +OPENSEARCH_HOST=localhost:9200 + +# Auth (disabled for local dev) +MEM_AUTH_MODE=none + +# API Key (test key for local dev) +MEM_API_KEY=test-key +``` + +## Local Service Stack (Example) + +```bash +# Terminal 1: OpenSearch +docker run -d -p 9200:9200 -e OPENSEARCH_JAVA_OPTS="-Xms512m -Xmx512m" \ + opensearchproject/opensearch:latest + +# Terminal 2: Ollama (LLM) +ollama serve + +# Terminal 3: poimen-memory +cargo run --release -- serve --port 8080 +``` + +## Production vs Local + +| Aspect | Production (K8s) | Local Dev | +|--------|-----------------|-----------| +| **Config** | `k8s/app/config.yaml` (SOPS-encrypted) | `.env` (gitignored) | +| **Injection** | ConfigMap via `envFrom:` | dotenv via `dotenvy` crate | +| **Services** | Cluster-internal DNS | localhost/127.0.0.1 | +| **Auth** | JWT (Authentik) | None (disabled) | +| **Commit?** | Yes (encrypted) | No (gitignored) | + +## Switching to Production Config + +To run against production services (not recommended locally): +1. Edit `.env` with production URLs +2. Set credentials appropriately +3. Ensure network access to production services + +--- + +See `.env.example` for all available environment variables. diff --git a/k8s/app/CONFIG.md b/k8s/app/CONFIG.md new file mode 100644 index 0000000..bb8c833 --- /dev/null +++ b/k8s/app/CONFIG.md @@ -0,0 +1,157 @@ +# Poimen Memory - Environment Configuration Guide + +All downstream service URIs are read from environment variables, sourced from ConfigMap. + +## How It Works + +1. **ConfigMap provides URIs**: `k8s/app/config.yaml` (production, SOPS-encrypted) +2. **Deployment injects via envFrom**: `envFrom: configMapRef: poimen-memory-config` +3. **Application reads from ENV**: Code parses `LLM_ENDPOINT`, `OPENSEARCH_HOST`, `AUTHENTIK_ISSUER`, etc. + +```yaml +# deployment.yaml +envFrom: + - configMapRef: + name: poimen-memory-config # All vars injected as ENV +``` + +## Environment Variables + +### LLM Service (Entity & Fact Extraction) +- `LLM_ENDPOINT` — full URL to chat/completions endpoint +- `LLM_API_BASE` — base API URL (used for client initialization) +- `LLM_MODEL` — model identifier (ornith:35b, qwen:7b, etc.) +- `LLM_TIMEOUT_SECS` — timeout for LLM requests +- `ENABLE_LLM_EXTRACTION` — enable/disable LLM extraction (true/false) + +### OpenSearch (Vector Store, BM25) +- `OPENSEARCH_HOST` — hostname:port +- `OPENSEARCH_SCHEME` — http or https +- `OPENSEARCH_VERIFY_CERTS` — SSL certificate verification (true/false) + +### Authentik (OIDC) +- `AUTHENTIK_ISSUER` — OIDC issuer URL +- `AUTHENTIK_VERIFY_SSL` — SSL certificate verification (true/false) +- `MEM_AUTH_MODE` — auth mode: jwt | apikey | none + +### Temporal (Workflow Orchestration - Future) +- `TEMPORAL_ENDPOINT` — temporal frontend hostname:port +- `TEMPORAL_NAMESPACE` — temporal namespace + +### API Gateway (Route Optimization - Future) +- `GATEWAY_URL` — gateway base URL + +### Memory Service Config +- `MEM_AUTH_MODE` — jwt | apikey | none +- `MEM_RATE_LIMIT_INGEST` — ingest requests per second +- `MEM_RATE_LIMIT_QUERY` — query requests per second +- `MEM_EMBEDDING_BATCH_SIZE` — batch size for embeddings + +--- + +## Deployment Scenarios + +### Production (SOPS-Encrypted ConfigMap) + +**File**: `k8s/app/config.yaml` + +Services use cluster-internal DNS: +```yaml +LLM_ENDPOINT: http://reasoning-predictor.llm-serving.svc.cluster.local:8000/v1/chat/completions +OPENSEARCH_HOST: opensearch.poimen.svc.cluster.local:9200 +AUTHENTIK_ISSUER: https://authentik.auth.svc.cluster.local:9443/application/o/poimen/ +TEMPORAL_ENDPOINT: temporal-frontend.temporal.svc.cluster.local:7233 +GATEWAY_URL: http://api-gw.poimen.svc.cluster.local:8080 +MEM_AUTH_MODE: jwt +``` + +**Deploy**: +```bash +# SOPS auto-decrypts based on .sops.yaml age key +kubectl apply -f k8s/app/config.yaml -k k8s/app/ +``` + +### Local/Development (Plaintext ConfigMap) + +**File**: `k8s/app/config.local.yaml` + +Services via external URLs (ingress): +```yaml +LLM_ENDPOINT: https://api.riotpiao.com/v1/chat/completions +OPENSEARCH_HOST: opensearch.riotpiao.com:443 +AUTHENTIK_ISSUER: https://authentik.riotpiao.com/application/o/poimen/ +TEMPORAL_ENDPOINT: temporal.riotpiao.com:443 +GATEWAY_URL: https://api.riotpiao.com +MEM_AUTH_MODE: none +``` + +**Deploy** (override production config): +```bash +# Delete prod config, apply local +kubectl delete configmap poimen-memory-config -n poimen +kubectl apply -f k8s/app/config.local.yaml +``` + +--- + +## Encrypting with SOPS + +Production `config.yaml` is encrypted with SOPS (Age-based). + +**Encrypt**: +```bash +sops -e k8s/app/config.yaml > k8s/app/config.yaml.enc +mv k8s/app/config.yaml.enc k8s/app/config.yaml +``` + +**Decrypt for editing** (SOPS auto-handles with $EDITOR): +```bash +sops k8s/app/config.yaml +``` + +**View decrypted** (without editing): +```bash +sops -d k8s/app/config.yaml +``` + +**.sops.yaml** defines encryption key: +```yaml +creation_rules: + - path_regex: k8s/app/config.yaml + key_groups: + - age: + - +``` + +--- + +## Application Code Pattern + +Example: Application should read URIs from ENV at startup. + +```rust +// Pseudocode +let llm_endpoint = env::var("LLM_ENDPOINT") + .unwrap_or("http://localhost:11434/v1/chat/completions".to_string()); +let opensearch_host = env::var("OPENSEARCH_HOST") + .unwrap_or("localhost:9200".to_string()); +let auth_mode = env::var("MEM_AUTH_MODE") + .unwrap_or("none".to_string()); + +// Initialize clients with these URIs +let llm_client = LlmClient::new(llm_endpoint)?; +let search_client = OpenSearchClient::new(opensearch_host)?; +``` + +--- + +## Summary + +| Aspect | Production | Local | +|--------|-----------|-------| +| **Config File** | `config.yaml` | `config.local.yaml` | +| **Encryption** | SOPS (Age) | Plaintext | +| **Service URIs** | Cluster-internal DNS | External HTTPS | +| **Auth Mode** | JWT (Authentik) | None (disabled) | +| **Rate Limits** | 100/1000 | 1000/10000 | +| **Deploy** | `kubectl apply -k k8s/app/` | `kubectl apply -f config.local.yaml` | diff --git a/k8s/app/config.local.yaml b/k8s/app/config.local.yaml new file mode 100644 index 0000000..c6639d2 --- /dev/null +++ b/k8s/app/config.local.yaml @@ -0,0 +1,47 @@ +# Local/Development configuration (plaintext, external URLs via ingress) +# Use this instead of config.yaml for local testing +# kubectl apply -f config.local.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: poimen-memory-config + namespace: poimen + labels: + app.kubernetes.io/name: poimen-memory + app.kubernetes.io/component: config +data: + # Auth mode: jwt | apikey | none (disabled for local testing) + MEM_AUTH_MODE: "none" + + # Rate limiting (higher for testing) + MEM_RATE_LIMIT_INGEST: "1000" + MEM_RATE_LIMIT_QUERY: "10000" + MEM_IDEMPOTENCY_TTL_SECS: "86400" + + # Embeddings + MEM_EMBEDDING_BATCH_SIZE: "32" + + # Downstream services - external URLs via ingress + + # LLM Service (via api.riotpiao.com ingress) + LLM_ENDPOINT: "https://api.riotpiao.com/v1/chat/completions" + LLM_API_BASE: "https://api.riotpiao.com/v1" + LLM_MODEL: "qwen:7b" + LLM_TIMEOUT_SECS: "60" + ENABLE_LLM_EXTRACTION: "true" + + # OpenSearch (via ingress) + OPENSEARCH_HOST: "opensearch.riotpiao.com:443" + OPENSEARCH_SCHEME: "https" + OPENSEARCH_VERIFY_CERTS: "true" + + # Authentik (via ingress - optional for local) + AUTHENTIK_ISSUER: "https://authentik.riotpiao.com/application/o/poimen/" + AUTHENTIK_VERIFY_SSL: "true" + + # Temporal (via ingress) + TEMPORAL_ENDPOINT: "temporal.riotpiao.com:443" + TEMPORAL_NAMESPACE: "poimen" + + # API Gateway (via ingress) + GATEWAY_URL: "https://api.riotpiao.com" diff --git a/k8s/app/config.yaml b/k8s/app/config.yaml index cccfae3..7778215 100644 --- a/k8s/app/config.yaml +++ b/k8s/app/config.yaml @@ -1,5 +1,7 @@ -# Non-sensitive environment variables for poimen-memory -# Change these without redeploying secrets. +# Production environment configuration for poimen-memory +# All services use cluster-internal DNS names +# This file is encrypted with SOPS in production +# For local dev, use plaintext version with external URLs apiVersion: v1 kind: ConfigMap metadata: @@ -9,19 +11,39 @@ metadata: app.kubernetes.io/name: poimen-memory app.kubernetes.io/component: config data: - # Auth mode: jwt | apikey - MEM_AUTH_MODE: "none" + # Auth mode: jwt | apikey | none + MEM_AUTH_MODE: "jwt" + # Rate limiting MEM_RATE_LIMIT_INGEST: "100" MEM_RATE_LIMIT_QUERY: "1000" MEM_IDEMPOTENCY_TTL_SECS: "86400" + # Embeddings MEM_EMBEDDING_BATCH_SIZE: "32" - # OpenSearch - OPENSEARCH_HOST: "opensearch.poimen.svc.cluster.local:9200" - # Obsidian - # LLM Configuration (for entity extraction) - LLM_ENDPOINT: "http://api-internal.riotpiao.com:8000/v1/chat/completions" - LLM_MODEL: "qwen:7b" + + # Downstream services - read by application from ENV + # Internal cluster DNS (prod) / external URLs (local) + + # LLM Service (entity extraction, fact extraction) + LLM_ENDPOINT: "http://reasoning-predictor.llm-serving.svc.cluster.local:8000/v1/chat/completions" + LLM_API_BASE: "http://reasoning-predictor.llm-serving.svc.cluster.local:8000/v1" + LLM_MODEL: "ornith:35b" LLM_TIMEOUT_SECS: "30" ENABLE_LLM_EXTRACTION: "true" + + # OpenSearch (vector store, BM25 retrieval) + OPENSEARCH_HOST: "opensearch.poimen.svc.cluster.local:9200" + OPENSEARCH_SCHEME: "http" + OPENSEARCH_VERIFY_CERTS: "false" + + # Authentik (OIDC provider) + AUTHENTIK_ISSUER: "https://authentik.auth.svc.cluster.local:9443/application/o/poimen/" + AUTHENTIK_VERIFY_SSL: "false" + + # Temporal (workflow orchestration - future) + TEMPORAL_ENDPOINT: "temporal-frontend.temporal.svc.cluster.local:7233" + TEMPORAL_NAMESPACE: "poimen" + + # API Gateway (external queue, route optimization - future) + GATEWAY_URL: "http://api-gw.poimen.svc.cluster.local:8080" diff --git a/k8s/app/deployment.yaml b/k8s/app/deployment.yaml index 28a9239..802cd8c 100644 --- a/k8s/app/deployment.yaml +++ b/k8s/app/deployment.yaml @@ -61,20 +61,12 @@ spec: - name: DATABASE_URL value: "postgresql://$(DATABASE_USER):$(DATABASE_PASSWORD)@$(DATABASE_HOST):$(DATABASE_PORT)/$(DATABASE_NAME)?sslmode=disable" - # LLM via api.riotpiao.com (Authentik JWT auth) - - name: LLM_ENDPOINT - value: "https://api.riotpiao.com/v1/chat/completions" - - name: LLM_API_BASE - value: "https://api.riotpiao.com/v1" - - name: LLM_MODEL - value: "ornith:35b" - + # All downstream service URIs read from ConfigMap + # (LLM_ENDPOINT, LLM_API_BASE, LLM_MODEL, OPENSEARCH_HOST, etc.) + # These are injected via envFrom below + # Authentik service account (memory-agent-oidc secret) - - name: AUTHENTIK_ISSUER - valueFrom: - secretKeyRef: - name: memory-agent-oidc - key: ISSUER + # Only needed if MEM_AUTH_MODE=jwt in ConfigMap - name: AUTHENTIK_CLIENT_ID valueFrom: secretKeyRef: @@ -102,6 +94,7 @@ spec: - name: MEM_HOME value: "/tmp" envFrom: + # ConfigMap with all service URIs (prod: encrypted, local: plaintext) - configMapRef: name: poimen-memory-config command: ["/app/mem"] diff --git a/k8s/app/kustomization.yaml b/k8s/app/kustomization.yaml index e097765..7ec3af7 100644 --- a/k8s/app/kustomization.yaml +++ b/k8s/app/kustomization.yaml @@ -1,13 +1,11 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: poimen + resources: - # vault-pvc.yaml removed — memory service uses pgvector, not local storage - deployment.yaml - service.yaml - - config.yaml - # obsidian.yaml retired — reference docs now via memory graph - # Legacy secret managed separately - # - secrets.yaml + - config.yaml # Production config (SOPS-encrypted) + generators: - secret-generator.yaml diff --git a/k8s/infra/runner-cleanup-cronjob.yaml b/k8s/infra/runner-cleanup-cronjob.yaml new file mode 100644 index 0000000..28e9017 --- /dev/null +++ b/k8s/infra/runner-cleanup-cronjob.yaml @@ -0,0 +1,121 @@ +# CronJob to periodically clean Gitea Actions runner disk space +# Prevents "no space left on device" errors during Docker builds +# Deploy to: kubectl apply -f k8s/infra/runner-cleanup-cronjob.yaml + +apiVersion: batch/v1 +kind: CronJob +metadata: + name: runner-disk-cleanup + namespace: ci # Adjust to your runner namespace + labels: + app: runner-cleanup +spec: + # Run daily at 2 AM + schedule: "0 2 * * *" + + # Keep last 3 successful jobs + successfulJobsHistoryLimit: 3 + failedJobsHistoryLimit: 1 + + jobTemplate: + spec: + template: + metadata: + labels: + app: runner-cleanup + spec: + serviceAccountName: runner-cleanup + + # Run on node with Gitea Actions runner + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - runner-node # Adjust to your runner node name + + containers: + - name: cleanup + image: docker:24 + securityContext: + privileged: true # Needed to access Docker daemon + command: + - /bin/sh + - -c + - | + echo "=== Runner disk cleanup at $(date) ===" + + df -h / + echo "" + + echo "Cleaning Docker..." + docker system prune -af --volumes 2>&1 | tail -5 + + echo "" + echo "Cleaning Cargo cache..." + rm -rf /root/.cargo/registry/cache 2>/dev/null + rm -rf /root/.cargo/registry/index 2>/dev/null + rm -rf /root/.cargo/git 2>/dev/null + + echo "" + echo "Cleaning /tmp..." + rm -rf /tmp/* 2>/dev/null + + echo "" + echo "Disk after cleanup:" + df -h / + + volumeMounts: + - name: docker-sock + mountPath: /var/run/docker.sock + - name: runner-home + mountPath: /root + + volumes: + # Access Docker daemon on host + - name: docker-sock + hostPath: + path: /var/run/docker.sock + # Access runner home directory + - name: runner-home + hostPath: + path: /home/runner # Adjust to your runner home path + + restartPolicy: OnFailure + +--- +# ServiceAccount for cleanup job +apiVersion: v1 +kind: ServiceAccount +metadata: + name: runner-cleanup + namespace: ci + +--- +# Role for cleanup job +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: runner-cleanup +rules: + - apiGroups: [""] + resources: ["nodes"] + verbs: ["get", "list"] + +--- +# RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: runner-cleanup +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: runner-cleanup +subjects: + - kind: ServiceAccount + name: runner-cleanup + namespace: ci