Terraform (validated with the real terraform CLI - was never actually
run against this cluster, no state file existed):
- Delete main.tf: it declared a duplicate kubernetes_namespace.core
(also in namespaces.tf) and a duplicate provider "kubernetes" block
(also in providers.tf), both hard errors that would fail
`terraform plan` immediately.
- Fix workloads.tf references to 6 files deleted in the manifest
cleanup (jupiter-sts/svc, atlantis-sts/svc, fiesta-sts/svc) - now
points at the canonical nearle-jupiter/atlantis/fiesta.yaml.
- Fix every kubernetes_manifest resource: they fed multi-document
YAML (multiple '---'-separated docs per file) straight into
yamldecode(), which only parses a single document. Rewrote using a
split-on-'---' + for_each pattern, confirmed safe first by checking
separator counts exactly match document counts for every affected
file (no embedded '---' inside any script/config content).
- Add the doormile namespace; rename kubernetes_namespace to
kubernetes_namespace_v1 (fixes a deprecation warning).
- `terraform validate` now passes clean.
Shell scripts:
- deploy-nearle-stack.sh only applied 4 of the ~13 files in
manifests/nearle/ - missing the ConfigMap/Secrets fiesta/jupiter/
titan/ariane need via envFrom, the fiesta gateway script ConfigMap,
atlantis entirely, and the Gateway/ReferenceGrant/jupiter-cors-proxy
resources. Now applies every file (verified by diffing the
directory listing against the script).
- Added deploy-doormile.sh and deploy-ingress.sh - nothing previously
applied ingress-unified.yaml or traefik-middlewares.yaml at all.
- Rewrote deploy.sh as an orchestrator calling all of the above in
order (previously referenced a manifests/namespace.yaml layout that
hasn't existed since before this repo's initial commit).
- Rewrote check-k8s-status.sh to check the real namespaces
(core/nearle/alaska/doormile/kubernetes-dashboard) instead of a
'nats-backend' namespace that never existed in this repo.
- Fixed a `cd` bug in setup-jetstream.sh that made it change into
shfiles/ and then look for scripts/setup_jetstream.py there (a
child directory that doesn't exist) - it could never have found its
own target file. Now pulls NATS credentials from the live
nats-credentials Secret instead of a third hardcoded copy.
Python scripts:
- sync_manifests.py had hardcoded Windows paths (e:\nats\kubernetes\...)
- replaced with paths relative to the script's own location so it
actually runs here (or anywhere). Verified by running it.
- setup_jetstream.py created durable consumers under different names
than worker.py computes at runtime ({NATS_CONSUMER}_{subject}), so
its max_deliver/ack_wait settings never actually reached the
consumers workers bind to. Naming now derived with the same logic
worker.py uses - verified all 10 derived names match workers.yaml
exactly.
- purge-old-messages.py had hardcoded NATS credentials with no env
var override at all - fixed to match the pattern used everywhere
else.
142 lines
5.5 KiB
Python
142 lines
5.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Setup JetStream streams and consumers for NATS
|
|
Run this after NATS is deployed and running
|
|
|
|
Durable consumer names are derived with the exact same logic worker.py uses
|
|
at runtime (NATS_CONSUMER + sanitized subject). WORKER_DOMAINS below must be
|
|
kept in sync with the NATS_STREAM / NATS_CONSUMER / FILTER_SUBJECT env vars
|
|
in manifests/core/workers.yaml - if they drift apart, the consumers created
|
|
here (with their max_deliver/ack_wait settings) will never be the ones the
|
|
workers actually bind to, and this script's config becomes a no-op.
|
|
"""
|
|
import asyncio
|
|
import nats
|
|
import os
|
|
import sys
|
|
|
|
WORKER_DOMAINS = [
|
|
{"stream": "ORDERS", "consumer": "orders-worker", "subjects": [
|
|
"api.v1.mob.orders.createorder",
|
|
]},
|
|
{"stream": "DELIVERIES", "consumer": "deliveries-worker", "subjects": [
|
|
"api.v1.deliveries.createdeliveries",
|
|
"api.v1.deliveries.updatedelivery",
|
|
"api.v2.deliveries.createdeliverylog",
|
|
]},
|
|
{"stream": "CUSTOMERS", "consumer": "customers-worker", "retention": "work", "subjects": [
|
|
"api.v1.mob.customers.login",
|
|
"api.v1.mob.customers.create",
|
|
]},
|
|
{"stream": "RIDER", "consumer": "rider-logs-worker", "subjects": [
|
|
"api.v2.partners.createriderlog",
|
|
"api.v2.partners.createbreaklog",
|
|
"api.v2.partners.updatebreaklog",
|
|
]},
|
|
{"stream": "PRODUCTS", "consumer": "products-worker", "subjects": [
|
|
"api.v1.web.products.create",
|
|
]},
|
|
]
|
|
|
|
|
|
def durable_name(consumer: str, subject: str) -> str:
|
|
"""Mirrors worker.py's durable-name derivation exactly."""
|
|
suffix = subject.replace(".", "_").replace("*", "all").replace(">", "all")
|
|
return f"{consumer}_{suffix}"
|
|
|
|
|
|
async def setup_jetstream():
|
|
nats_url = os.getenv("NATS_URL", "nats://nats.workolik.com:4222")
|
|
nats_user = os.getenv("NATS_USER", "admin")
|
|
nats_password = os.getenv("NATS_PASSWORD", "")
|
|
|
|
try:
|
|
print(f"Connecting to NATS at {nats_url}...")
|
|
nc = await nats.connect(
|
|
servers=[nats_url],
|
|
user=nats_user,
|
|
password=nats_password,
|
|
)
|
|
print("✅ Connected to NATS")
|
|
|
|
js = nc.jetstream()
|
|
|
|
# Create / recreate streams
|
|
for domain in WORKER_DOMAINS:
|
|
stream_name = domain["stream"]
|
|
subjects = domain["subjects"]
|
|
retention_policy = domain.get("retention", "limits")
|
|
try:
|
|
info = await js.stream_info(stream_name)
|
|
print(f"⚠️ Stream '{stream_name}' already exists with subjects={info.config.subjects}, updating...")
|
|
await js.update_stream(
|
|
name=stream_name,
|
|
subjects=subjects,
|
|
storage="memory",
|
|
retention=retention_policy,
|
|
max_age=24 * 60 * 60,
|
|
max_msgs=50000,
|
|
max_bytes=512 * 1024 * 1024,
|
|
)
|
|
print(f"✅ Stream '{stream_name}' updated")
|
|
except Exception as e:
|
|
if "not found" in str(e).lower() or "404" in str(e).lower():
|
|
print(f"Creating stream '{stream_name}'...")
|
|
await js.add_stream(
|
|
name=stream_name,
|
|
subjects=subjects,
|
|
storage="memory",
|
|
retention=retention_policy,
|
|
max_age=24 * 60 * 60,
|
|
max_msgs=50000,
|
|
max_bytes=512 * 1024 * 1024,
|
|
)
|
|
print(f"✅ Stream '{stream_name}' created")
|
|
else:
|
|
print(f"⚠️ Could not inspect stream '{stream_name}': {e}")
|
|
|
|
# Create durable consumers per subject - names match what worker.py
|
|
# computes at runtime, so max_deliver/ack_wait here actually apply.
|
|
print("\nConfiguring consumers...")
|
|
for domain in WORKER_DOMAINS:
|
|
stream_name = domain["stream"]
|
|
consumer = domain["consumer"]
|
|
for subject in domain["subjects"]:
|
|
durable = durable_name(consumer, subject)
|
|
try:
|
|
print(f"Creating consumer '{durable}' on stream '{stream_name}' for subject '{subject}'...")
|
|
await js.add_consumer(
|
|
stream_name,
|
|
durable_name=durable,
|
|
filter_subject=subject,
|
|
ack_policy="explicit",
|
|
deliver_policy="all",
|
|
max_deliver=5,
|
|
ack_wait=60,
|
|
)
|
|
print(f"✅ Consumer '{durable}' created")
|
|
except Exception as e:
|
|
if "already in use" in str(e).lower() or "already exists" in str(e).lower():
|
|
print(f"⚠️ Consumer '{durable}' already exists, skipping...")
|
|
else:
|
|
raise
|
|
|
|
print("\n✅ JetStream setup complete!")
|
|
print(" Streams:")
|
|
for domain in WORKER_DOMAINS:
|
|
print(f" - {domain['stream']}: {', '.join(domain['subjects'])}")
|
|
print(" Consumers:")
|
|
for domain in WORKER_DOMAINS:
|
|
for subject in domain["subjects"]:
|
|
print(f" - {durable_name(domain['consumer'], subject)}: stream={domain['stream']}, subject={subject}")
|
|
|
|
await nc.close()
|
|
sys.exit(0)
|
|
|
|
except Exception as e:
|
|
print(f"❌ Error: {e}")
|
|
sys.exit(1)
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(setup_jetstream())
|