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..6e0eed2 100644 --- a/k8s/app/kustomization.yaml +++ b/k8s/app/kustomization.yaml @@ -1,13 +1,12 @@ 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) + # config.local.yaml # Optional: plaintext local/dev overrides + generators: - secret-generator.yaml