From 854817e2e309bd126bbe43de11f07730c562b50c Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Fri, 22 May 2026 11:23:30 +0000 Subject: [PATCH] trading-bot: revive K8s stack + add meet-kevin-watcher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uncomment the trading-bot stack (disabled 2026-04-06 due to resource consumption) and add the new meet_kevin_watcher service container. Changes: - Uncomment the /* ... */ block enclosing the entire stack - Fix db_init job: add -d postgres to psql commands (root user has no root-named database — matches pattern used in claude-memory + others) - Remove 3 disabled containers from trading-bot-workers Pod spec: news-fetcher, sentiment-analyzer, trade-executor - Add new meet-kevin-watcher container (image viktorbarzin/trading-bot-service:latest, command python -m services.meet_kevin_watcher.main, mem 128Mi/256Mi) - Extend ExternalSecret with TRADING_OPENROUTER_API_KEY and TRADING_MEET_KEVIN_CHANNEL_ID keys (sourced from Vault secret/trading-bot) - Add 4 common_env entries for the Meet Kevin pipeline (poll interval, daily cost cap, model slug, prompt version) - Update lifecycle.ignore_changes to 4 image indices vault: re-enable pg-trading static role - Add pg-trading to vault_database_secret_backend_connection allowed_roles - Uncomment vault_database_secret_backend_static_role.pg_trading (was disabled 2026-04-06 with the rest of trading-bot stack) kyverno: add postgres* to trusted-registries allowlist - trading-bot db_init uses postgres:16-alpine (Docker Hub library image) - postgres* was not in the DockerHub bare-name allowlist (unlike mysql*, alpine*, nginx*, python* which were already there) Final workers Pod containers (in order): [0] signal-generator [1] learning-engine [2] market-data [3] meet-kevin-watcher (NEW) Co-Authored-By: Claude Opus 4.7 --- .../modules/kyverno/security-policies.tf | 2 +- stacks/trading-bot/backend.tf | 2 +- stacks/trading-bot/main.tf | 208 ++++++------------ stacks/trading-bot/providers.tf | 12 + stacks/vault/main.tf | 5 +- 5 files changed, 84 insertions(+), 145 deletions(-) diff --git a/stacks/kyverno/modules/kyverno/security-policies.tf b/stacks/kyverno/modules/kyverno/security-policies.tf index 6c65c188..7f11007d 100644 --- a/stacks/kyverno/modules/kyverno/security-policies.tf +++ b/stacks/kyverno/modules/kyverno/security-policies.tf @@ -329,7 +329,7 @@ resource "kubectl_manifest" "policy_require_trusted_registries" { # Private "forgejo.viktorbarzin.me/*", "10.0.20.10*", # DockerHub library (bare image names without slash) - "alpine*", "busybox*", "kong*", "mysql*", "nginx*", "python*", + "alpine*", "busybox*", "kong*", "mysql*", "nginx*", "postgres*", "python*", # DockerHub user repos (no registry prefix, has slash) — # enumerated from current cluster state. "actualbudget/*", "afadil/*", "binwiederhier/*", "bitnami/*", diff --git a/stacks/trading-bot/backend.tf b/stacks/trading-bot/backend.tf index 55941fa4..d9fff500 100644 --- a/stacks/trading-bot/backend.tf +++ b/stacks/trading-bot/backend.tf @@ -1,7 +1,7 @@ # Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa terraform { backend "pg" { - conn_str = "postgres://terraform_state:ZCcWMOLCTqb0aV-XyTAZ@10.0.20.200:5432/terraform_state?sslmode=disable" + conn_str = "postgres://terraform_state:LicuZK1nVl4ILE5HF-A9@10.0.20.200:5432/terraform_state?sslmode=disable" schema_name = "trading-bot" } } diff --git a/stacks/trading-bot/main.tf b/stacks/trading-bot/main.tf index e434101a..315fb967 100644 --- a/stacks/trading-bot/main.tf +++ b/stacks/trading-bot/main.tf @@ -1,8 +1,3 @@ -/* -# TRADING-BOT STACK COMMENTED OUT - 2026-04-06 -# Deployments scaled to 0, infrastructure disabled to prevent re-creation on apply -# To re-enable: uncomment this entire block - variable "tls_secret_name" { type = string sensitive = true @@ -12,21 +7,25 @@ variable "postgresql_host" { type = string } variable "redis_host" { type = string } locals { common_env = { - TRADING_REDIS_URL = "redis://${var.redis_host}:6379/4" - TRADING_LOG_LEVEL = "INFO" - TRADING_ALPACA_BASE_URL = "https://paper-api.alpaca.markets" - TRADING_PAPER_TRADING = "true" - TRADING_REDDIT_USER_AGENT = "trading-bot/0.1" - TRADING_WATCHLIST = "[\"AAPL\",\"TSLA\",\"NVDA\",\"MSFT\",\"GOOGL\"]" - TRADING_BAR_TIMEFRAME = "5Min" - TRADING_POLL_INTERVAL_SECONDS = "60" - TRADING_HISTORICAL_BARS = "100" - TRADING_SNAPSHOT_INTERVAL_SECONDS = "60" - TRADING_FUNDAMENTALS_CACHE_TTL_HOURS = "24" - TRADING_RP_ID = "trading.viktorbarzin.me" - TRADING_RP_NAME = "Trading Bot" - TRADING_RP_ORIGIN = "https://trading.viktorbarzin.me" - TRADING_CORS_ORIGINS = "[\"https://trading.viktorbarzin.me\"]" + TRADING_REDIS_URL = "redis://${var.redis_host}:6379/4" + TRADING_LOG_LEVEL = "INFO" + TRADING_ALPACA_BASE_URL = "https://paper-api.alpaca.markets" + TRADING_PAPER_TRADING = "true" + TRADING_REDDIT_USER_AGENT = "trading-bot/0.1" + TRADING_WATCHLIST = "[\"AAPL\",\"TSLA\",\"NVDA\",\"MSFT\",\"GOOGL\"]" + TRADING_BAR_TIMEFRAME = "5Min" + TRADING_POLL_INTERVAL_SECONDS = "60" + TRADING_HISTORICAL_BARS = "100" + TRADING_SNAPSHOT_INTERVAL_SECONDS = "60" + TRADING_FUNDAMENTALS_CACHE_TTL_HOURS = "24" + TRADING_RP_ID = "trading.viktorbarzin.me" + TRADING_RP_NAME = "Trading Bot" + TRADING_RP_ORIGIN = "https://trading.viktorbarzin.me" + TRADING_CORS_ORIGINS = "[\"https://trading.viktorbarzin.me\"]" + TRADING_MEET_KEVIN_POLL_INTERVAL_SECONDS = "10800" + TRADING_MEET_KEVIN_DAILY_COST_CAP_USD = "5" + TRADING_MEET_KEVIN_LLM_MODEL = "anthropic/claude-sonnet-4.5" + TRADING_MEET_KEVIN_PROMPT_VERSION = "v1" } } @@ -34,7 +33,7 @@ resource "kubernetes_namespace" "trading-bot" { metadata { name = "trading-bot" labels = { - tier = local.tiers.edge + tier = local.tiers.edge "keel.sh/enrolled" = "true" } } @@ -72,6 +71,8 @@ resource "kubernetes_manifest" "external_secret" { TRADING_ALPHA_VANTAGE_API_KEY = "{{ .alpha_vantage_api_key }}" TRADING_FMP_API_KEY = "{{ .fmp_api_key }}" DBAAS_ROOT_PASSWORD = "{{ .dbaas_root_password }}" + TRADING_OPENROUTER_API_KEY = "{{ .openrouter_api_key }}" + TRADING_MEET_KEVIN_CHANNEL_ID = "{{ .meet_kevin_channel_id }}" } } } @@ -84,6 +85,8 @@ resource "kubernetes_manifest" "external_secret" { { secretKey = "alpha_vantage_api_key", remoteRef = { key = "trading-bot", property = "alpha_vantage_api_key" } }, { secretKey = "fmp_api_key", remoteRef = { key = "trading-bot", property = "fmp_api_key" } }, { secretKey = "dbaas_root_password", remoteRef = { key = "trading-bot", property = "dbaas_root_password" } }, + { secretKey = "openrouter_api_key", remoteRef = { key = "trading-bot", property = "openrouter_api_key" } }, + { secretKey = "meet_kevin_channel_id", remoteRef = { key = "trading-bot", property = "meet_kevin_channel_id" } }, ] } } @@ -143,14 +146,16 @@ resource "kubernetes_job" "db_init" { "sh", "-c", <<-EOT set -e + # -d postgres: psql defaults database name to username; root user + # doesn't have a root-named database, so be explicit. # Create role if not exists - PGPASSWORD="$DBAAS_ROOT_PASSWORD" psql -h ${var.postgresql_host} -U root -tc "SELECT 1 FROM pg_roles WHERE rolname='trading'" | grep -q 1 || \ - PGPASSWORD="$DBAAS_ROOT_PASSWORD" psql -h ${var.postgresql_host} -U root -c "CREATE ROLE trading WITH LOGIN PASSWORD '$DB_PASSWORD'" + PGPASSWORD="$DBAAS_ROOT_PASSWORD" psql -h ${var.postgresql_host} -U root -d postgres -tc "SELECT 1 FROM pg_roles WHERE rolname='trading'" | grep -q 1 || \ + PGPASSWORD="$DBAAS_ROOT_PASSWORD" psql -h ${var.postgresql_host} -U root -d postgres -c "CREATE ROLE trading WITH LOGIN PASSWORD '$DB_PASSWORD'" # Create database if not exists - PGPASSWORD="$DBAAS_ROOT_PASSWORD" psql -h ${var.postgresql_host} -U root -tc "SELECT 1 FROM pg_database WHERE datname='trading'" | grep -q 1 || \ - PGPASSWORD="$DBAAS_ROOT_PASSWORD" psql -h ${var.postgresql_host} -U root -c "CREATE DATABASE trading OWNER trading" + PGPASSWORD="$DBAAS_ROOT_PASSWORD" psql -h ${var.postgresql_host} -U root -d postgres -tc "SELECT 1 FROM pg_database WHERE datname='trading'" | grep -q 1 || \ + PGPASSWORD="$DBAAS_ROOT_PASSWORD" psql -h ${var.postgresql_host} -U root -d postgres -c "CREATE DATABASE trading OWNER trading" # Grant privileges - PGPASSWORD="$DBAAS_ROOT_PASSWORD" psql -h ${var.postgresql_host} -U root -c "GRANT ALL PRIVILEGES ON DATABASE trading TO trading" + PGPASSWORD="$DBAAS_ROOT_PASSWORD" psql -h ${var.postgresql_host} -U root -d postgres -c "GRANT ALL PRIVILEGES ON DATABASE trading TO trading" # Try to enable timescaledb (allow failure) PGPASSWORD="$DBAAS_ROOT_PASSWORD" psql -h ${var.postgresql_host} -U root -d trading -c "CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE" || true echo "Database init complete" @@ -358,78 +363,6 @@ resource "kubernetes_deployment" "trading-bot-workers" { } } spec { - container { - name = "news-fetcher" - image = "viktorbarzin/trading-bot-service:latest" - image_pull_policy = "Always" - command = ["python", "-m", "services.news_fetcher.main"] - dynamic "env" { - for_each = local.common_env - content { - name = env.key - value = env.value - } - } - env { - name = "TRADING_OTEL_METRICS_PORT" - value = "9091" - } - env_from { - secret_ref { - name = "trading-bot-secrets" - } - } - env_from { - secret_ref { - name = "trading-bot-db-creds" - } - } - resources { - requests = { - cpu = "10m" - memory = "128Mi" - } - limits = { - memory = "256Mi" - } - } - } - container { - name = "sentiment-analyzer" - image = "viktorbarzin/trading-bot-service:latest" - image_pull_policy = "Always" - command = ["python", "-m", "services.sentiment_analyzer.main"] - dynamic "env" { - for_each = local.common_env - content { - name = env.key - value = env.value - } - } - env { - name = "TRADING_OTEL_METRICS_PORT" - value = "9092" - } - env_from { - secret_ref { - name = "trading-bot-secrets" - } - } - env_from { - secret_ref { - name = "trading-bot-db-creds" - } - } - resources { - requests = { - cpu = "100m" - memory = "512Mi" - } - limits = { - memory = "512Mi" - } - } - } container { name = "signal-generator" image = "viktorbarzin/trading-bot-service:latest" @@ -466,42 +399,6 @@ resource "kubernetes_deployment" "trading-bot-workers" { } } } - container { - name = "trade-executor" - image = "viktorbarzin/trading-bot-service:latest" - image_pull_policy = "Always" - command = ["python", "-m", "services.trade_executor.main"] - dynamic "env" { - for_each = local.common_env - content { - name = env.key - value = env.value - } - } - env { - name = "TRADING_OTEL_METRICS_PORT" - value = "9094" - } - env_from { - secret_ref { - name = "trading-bot-secrets" - } - } - env_from { - secret_ref { - name = "trading-bot-db-creds" - } - } - resources { - requests = { - cpu = "10m" - memory = "128Mi" - } - limits = { - memory = "256Mi" - } - } - } container { name = "learning-engine" image = "viktorbarzin/trading-bot-service:latest" @@ -574,18 +471,52 @@ resource "kubernetes_deployment" "trading-bot-workers" { } } } + container { + name = "meet-kevin-watcher" + image = "viktorbarzin/trading-bot-service:latest" + image_pull_policy = "Always" + command = ["python", "-m", "services.meet_kevin_watcher.main"] + dynamic "env" { + for_each = local.common_env + content { + name = env.key + value = env.value + } + } + env { + name = "TRADING_OTEL_METRICS_PORT" + value = "9097" + } + env_from { + secret_ref { + name = "trading-bot-secrets" + } + } + env_from { + secret_ref { + name = "trading-bot-db-creds" + } + } + resources { + requests = { + cpu = "10m" + memory = "128Mi" + } + limits = { + memory = "256Mi" + } + } + } } } } lifecycle { - # DRIFT_WORKAROUND: CI pipeline owns image tags for all 6 worker containers. Reviewed 2026-04-18. + # DRIFT_WORKAROUND: CI pipeline owns image tags for all 4 worker containers. Reviewed 2026-05-22. ignore_changes = [ spec[0].template[0].spec[0].container[0].image, spec[0].template[0].spec[0].container[1].image, spec[0].template[0].spec[0].container[2].image, spec[0].template[0].spec[0].container[3].image, - spec[0].template[0].spec[0].container[4].image, - spec[0].template[0].spec[0].container[5].image, spec[0].template[0].spec[0].dns_config, # KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2 ] } @@ -618,7 +549,7 @@ module "ingress" { name = "trading" service_name = "trading-bot-frontend" tls_secret_name = var.tls_secret_name - auth = "required" + auth = "required" extra_annotations = { "gethomepage.dev/enabled" = "true" "gethomepage.dev/name" = "Trading Bot" @@ -628,6 +559,5 @@ module "ingress" { "gethomepage.dev/pod-selector" = "" } } -*/ # CI retrigger v6 2026-05-16T23:18:58Z diff --git a/stacks/trading-bot/providers.tf b/stacks/trading-bot/providers.tf index 012af700..d5469984 100644 --- a/stacks/trading-bot/providers.tf +++ b/stacks/trading-bot/providers.tf @@ -13,6 +13,13 @@ terraform { source = "goauthentik/authentik" version = "~> 2024.10" } + # kubectl (gavinbunney) — workaround for hashicorp/kubernetes + # `kubernetes_manifest` panics on Kyverno CRDs. See beads code-e2dp. + # Declared for all stacks but only used where opted-in. + kubectl = { + source = "gavinbunney/kubectl" + version = "~> 1.14" + } } } @@ -35,3 +42,8 @@ provider "vault" { address = "https://vault.viktorbarzin.me" skip_child_token = true } + +provider "kubectl" { + config_path = var.kube_config_path + load_config_file = true +} diff --git a/stacks/vault/main.tf b/stacks/vault/main.tf index 36a7ffea..3c15465f 100644 --- a/stacks/vault/main.tf +++ b/stacks/vault/main.tf @@ -610,7 +610,7 @@ resource "vault_database_secret_backend_connection" "postgresql" { backend = vault_mount.database.path name = "postgresql" allowed_roles = [ - # "pg-trading", # Commented out 2026-04-06 - trading-bot disabled + "pg-trading", "pg-health", "pg-linkwarden", "pg-affine", "pg-woodpecker", "pg-claude-memory", "pg-terraform-state", "pg-payslip-ingest", "pg-job-hunter", @@ -696,8 +696,6 @@ resource "vault_database_secret_backend_static_role" "mysql_phpipam" { # --- PostgreSQL Static Roles --- -/* -# Commented out 2026-04-06 - trading-bot disabled resource "vault_database_secret_backend_static_role" "pg_trading" { backend = vault_mount.database.path db_name = vault_database_secret_backend_connection.postgresql.name @@ -705,7 +703,6 @@ resource "vault_database_secret_backend_static_role" "pg_trading" { username = "trading" rotation_period = 604800 } -*/ resource "vault_database_secret_backend_static_role" "pg_health" { backend = vault_mount.database.path