fire-planner: add examples ingest Job (toggled) + weekly CronJob

Adds the K8s plumbing for the Reddit FIRE-examples ingest path:

- ExternalSecret fire-planner-examples-reddit (Reddit OAuth from
  Vault secret/viktor.trading_bot_reddit_{client_id,client_secret}).
- ExternalSecret fire-planner-examples-claude (claude-agent-service
  bearer from Vault secret/claude-agent-service.api_bearer_token).
- kubernetes_job_v1.examples_bulk_ingest — one-shot bulk Job toggled
  via var.run_examples_bulk_ingest (default false). Timestamp-named so
  each (true) transition creates a fresh Job; lifecycle ignores the
  name so re-plans don't propose phantom renames.
- kubernetes_cron_job_v1.examples_weekly_delta — Sunday 04:00 UTC
  --top=week --limit=200 incremental run.

Both runners share the env_from plumbing of the existing recompute
CronJob (fire-planner-secrets, fire-planner-db-creds,
wealthfolio-sync-db-creds) plus examples-specific vars
(REDDIT_USER_AGENT, LLAMA_CPP_BASE_URL, CLAUDE_AGENT_SERVICE_URL,
plus the three secret-backed env vars).

Plan-only this commit — actual apply lands in Task 17 after the
ingest image build.
This commit is contained in:
Viktor Barzin 2026-05-28 22:51:14 +00:00
parent 4dff834c8a
commit 0044c3a8ea

View file

@ -546,7 +546,7 @@ module "ingress_api" {
ingress_path = ["/api/"]
tls_secret_name = var.tls_secret_name
# auth = "none": XHR-based API endpoints; forward-auth 302+cookie-dance breaks CORS preflight and browser fetch().
auth = "none"
auth = "none"
}
# Plan-time read of the ESO-created K8s Secret for Grafana datasource
@ -605,3 +605,318 @@ resource "kubernetes_config_map" "grafana_fire_planner_datasource" {
# CI retrigger 2026-05-16T13:42:57+00:00 bulk enrollment apply (pipeline #689 killed)
# CI retrigger v2 2026-05-16T13:46:35+00:00
# ----------------------------------------------------------------------
# Reddit FIRE examples ingest Job (bulk, toggled) + weekly CronJob
# Backs the fire_planner.examples module. See:
# ~/code/fire-planner/docs/plans/2026-05-28-reddit-examples-{design,plan}.md
# ----------------------------------------------------------------------
variable "llama_cpp_base_url" {
type = string
description = "llama-cpp /v1/chat/completions endpoint for primary LLM extraction"
default = "http://llama-cpp.llama-cpp.svc.cluster.local:8000/v1/chat/completions"
}
variable "claude_agent_service_url" {
type = string
description = "claude-agent-service /v1/chat/completions endpoint for Tier 2 fallback"
default = "http://claude-agent-service.claude-agent.svc.cluster.local:8080/v1/chat/completions"
}
variable "run_examples_bulk_ingest" {
type = bool
description = "Flip to true once to bulk-populate fire_example. Reset to false after."
default = false
}
# Reddit OAuth creds pulled from Vault secret/viktor.
resource "kubernetes_manifest" "external_secret_examples_reddit" {
manifest = {
apiVersion = "external-secrets.io/v1beta1"
kind = "ExternalSecret"
metadata = {
name = "fire-planner-examples-reddit"
namespace = local.namespace
}
spec = {
refreshInterval = "1h"
secretStoreRef = {
name = "vault-kv"
kind = "ClusterSecretStore"
}
target = {
name = "fire-planner-examples-reddit"
}
data = [
{
secretKey = "REDDIT_CLIENT_ID"
remoteRef = {
key = "viktor"
property = "trading_bot_reddit_client_id"
}
},
{
secretKey = "REDDIT_CLIENT_SECRET"
remoteRef = {
key = "viktor"
property = "trading_bot_reddit_client_secret"
}
},
]
}
}
depends_on = [kubernetes_namespace.fire_planner]
}
# claude-agent-service bearer pulled separately so its rotation cadence
# is decoupled from the Reddit creds.
resource "kubernetes_manifest" "external_secret_examples_claude" {
manifest = {
apiVersion = "external-secrets.io/v1beta1"
kind = "ExternalSecret"
metadata = {
name = "fire-planner-examples-claude"
namespace = local.namespace
}
spec = {
refreshInterval = "1h"
secretStoreRef = {
name = "vault-kv"
kind = "ClusterSecretStore"
}
target = {
name = "fire-planner-examples-claude"
}
data = [
{
secretKey = "CLAUDE_AGENT_BEARER"
remoteRef = {
key = "claude-agent-service"
property = "api_bearer_token"
}
},
]
}
}
depends_on = [kubernetes_namespace.fire_planner]
}
# Bulk one-shot Job toggled via var.run_examples_bulk_ingest. Flip to
# true once, apply, wait for completion, flip back. The timestamp() in
# the name ensures Terraform creates a fresh Job on each (true)
# transition rather than refusing to recreate an existing one.
resource "kubernetes_job_v1" "examples_bulk_ingest" {
count = var.run_examples_bulk_ingest ? 1 : 0
metadata {
name = "fire-planner-examples-bulk-${formatdate("YYYYMMDDhhmm", timestamp())}"
namespace = kubernetes_namespace.fire_planner.metadata[0].name
}
spec {
backoff_limit = 0
template {
metadata {
labels = local.labels
}
spec {
restart_policy = "OnFailure"
image_pull_secrets {
name = "registry-credentials"
}
container {
name = "ingest"
image = local.image
image_pull_policy = "IfNotPresent"
command = ["python", "-m", "fire_planner", "examples", "ingest",
"--top=all,year", "--limit=1000"]
# DB plumbing mirror the fire_planner_recompute CronJob.
env_from {
secret_ref {
name = "fire-planner-secrets"
}
}
env_from {
secret_ref {
name = "fire-planner-db-creds"
}
}
env_from {
secret_ref {
name = "wealthfolio-sync-db-creds"
}
}
# Examples-specific vars.
env {
name = "REDDIT_CLIENT_ID"
value_from {
secret_key_ref {
name = "fire-planner-examples-reddit"
key = "REDDIT_CLIENT_ID"
}
}
}
env {
name = "REDDIT_CLIENT_SECRET"
value_from {
secret_key_ref {
name = "fire-planner-examples-reddit"
key = "REDDIT_CLIENT_SECRET"
}
}
}
env {
name = "CLAUDE_AGENT_BEARER"
value_from {
secret_key_ref {
name = "fire-planner-examples-claude"
key = "CLAUDE_AGENT_BEARER"
}
}
}
env {
name = "REDDIT_USER_AGENT"
value = "fire-planner/0.1"
}
env {
name = "LLAMA_CPP_BASE_URL"
value = var.llama_cpp_base_url
}
env {
name = "CLAUDE_AGENT_SERVICE_URL"
value = var.claude_agent_service_url
}
}
}
}
}
lifecycle {
# The name embeds a timestamp so a re-plan after time has passed
# would otherwise propose a no-op rename. Ignore.
# KYVERNO_LIFECYCLE_V1
ignore_changes = [
metadata[0].name,
spec[0].template[0].spec[0].dns_config,
]
}
depends_on = [
kubernetes_manifest.external_secret,
kubernetes_manifest.db_external_secret,
kubernetes_manifest.wealthfolio_sync_db_external_secret,
kubernetes_manifest.external_secret_examples_reddit,
kubernetes_manifest.external_secret_examples_claude,
]
}
# Weekly delta top-of-week milestone posts. Sunday 04:00 UTC.
resource "kubernetes_cron_job_v1" "examples_weekly_delta" {
metadata {
name = "fire-planner-examples-weekly"
namespace = kubernetes_namespace.fire_planner.metadata[0].name
}
spec {
schedule = "0 4 * * 0"
concurrency_policy = "Forbid"
successful_jobs_history_limit = 3
failed_jobs_history_limit = 3
job_template {
metadata {
labels = local.labels
}
spec {
backoff_limit = 0
ttl_seconds_after_finished = 86400
template {
metadata {
labels = local.labels
}
spec {
restart_policy = "OnFailure"
image_pull_secrets {
name = "registry-credentials"
}
container {
name = "ingest"
image = local.image
image_pull_policy = "IfNotPresent"
command = ["python", "-m", "fire_planner", "examples", "ingest",
"--top=week", "--limit=200"]
# DB plumbing mirror the fire_planner_recompute CronJob.
env_from {
secret_ref {
name = "fire-planner-secrets"
}
}
env_from {
secret_ref {
name = "fire-planner-db-creds"
}
}
env_from {
secret_ref {
name = "wealthfolio-sync-db-creds"
}
}
# Examples-specific vars keep in sync with the bulk Job.
env {
name = "REDDIT_CLIENT_ID"
value_from {
secret_key_ref {
name = "fire-planner-examples-reddit"
key = "REDDIT_CLIENT_ID"
}
}
}
env {
name = "REDDIT_CLIENT_SECRET"
value_from {
secret_key_ref {
name = "fire-planner-examples-reddit"
key = "REDDIT_CLIENT_SECRET"
}
}
}
env {
name = "CLAUDE_AGENT_BEARER"
value_from {
secret_key_ref {
name = "fire-planner-examples-claude"
key = "CLAUDE_AGENT_BEARER"
}
}
}
env {
name = "REDDIT_USER_AGENT"
value = "fire-planner/0.1"
}
env {
name = "LLAMA_CPP_BASE_URL"
value = var.llama_cpp_base_url
}
env {
name = "CLAUDE_AGENT_SERVICE_URL"
value = var.claude_agent_service_url
}
}
}
}
}
}
}
lifecycle {
# KYVERNO_LIFECYCLE_V1
ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config]
}
depends_on = [
kubernetes_manifest.external_secret,
kubernetes_manifest.db_external_secret,
kubernetes_manifest.wealthfolio_sync_db_external_secret,
kubernetes_manifest.external_secret_examples_reddit,
kubernetes_manifest.external_secret_examples_claude,
]
}