Compare commits

..

3 Commits

Author SHA1 Message Date
Suriya
0a8c3b0374 Fix deployment tooling: shell scripts and Terraform
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.
2026-07-18 16:08:15 +05:30
Suriya
91dd240431 Fix worker/gateway logic bugs and duplicate CORS headers
worker.py (both the ConfigMap copy and conf/worker.py):
- Set an explicit ack_wait=60s on the JetStream pull consumer. It was
  previously left at the implicit default (~30s), the same ballpark
  as the outbound HTTP timeout - a slow-but-legitimate external call
  could cause JetStream to redeliver the message to another worker
  while the first was still mid-request, double-processing a
  non-idempotent call (e.g. duplicate order creation).
- Track in-flight tasks and drain them (bounded wait) before closing
  the NATS/HTTP connections on shutdown, instead of cutting them off
  immediately - avoids dropped/duplicated messages on pod restarts.
- Generic exception handler now does nak(delay=5) instead of an
  undelayed nak(), avoiding a tight redelivery loop on a persistent
  bug.
- Missing 'data' field in a message now explicitly drops with a log
  line instead of silently forwarding the entire internal envelope.
- Removed the hardcoded NATS password fallback baked into the source
  (every deployment already supplies it via a Secret at runtime, so
  this was a redundant plaintext copy sitting in a ConfigMap).

app.py (both the ConfigMap copy and conf/app.py):
- Fixed "NATS by connected" typo -> "NATS not connected".
- Same hardcoded-password-fallback removal as worker.py.

CORS:
- conf/nginx-jupiter.conf and the in-cluster jupiter-cors-proxy nginx
  config both add their own CORS headers without stripping any the
  upstream might set, unlike nginx-queue-proxy.conf which does this
  correctly. Added proxy_hide_header for the ACA-* headers in both -
  browsers reject a response with duplicate Access-Control-* values.

docker-compose.yml:
- Added the missing doormile-proxy service (doormile.com -> :8206 ->
  NodePort 30830). nginx-doormile.conf existed but had no service
  wiring it into Traefik, unlike every other app.
2026-07-18 16:07:49 +05:30
Suriya
836c079a05 Fix Kubernetes manifest bugs, dedupe drifted files, harden security
- Rebuild manifests/doormile/miletruth.yaml (was corrupted since the
  initial commit - contained pasted AI/terminal output, truncated env
  var names/values, duplicate keys). Rebuilt from the confirmed-live
  config, secrets sourced via a Secret instead of plaintext values.
- Lock down the Kubernetes Dashboard: remove --enable-skip-login /
  --enable-insecure-login / --insecure-port=9090, remove the extra
  cluster-admin binding on the dashboard's own ServiceAccount, remove
  the now-dead insecure NodePort Service. Token-based login via the
  existing admin-user ServiceAccount is unaffected.
- Fix the duplicate `backendRefs` key under the same HTTPRoute rule in
  alaska.yaml (invalid/redundant YAML).
- Delete 6 redundant duplicate manifests (fiesta-sts/svc,
  atlantis-sts/svc, jupiter-sts/svc) that were partial, stale subsets
  of nearle-fiesta/atlantis/jupiter.yaml - one pair disagreed on the
  fiesta image tag entirely (v1.3.50 vs v1.3.67, neither of which
  matched what's actually live).
- Reconcile nearle-fiesta.yaml and nearle-jupiter.yaml image tags to
  the confirmed-live versions (v1.3.78 / v2.7.55).
- Add allowPrivilegeEscalation:false + drop-all-capabilities to
  fiesta/atlantis/jupiter/titan/ariane and the 5 specialized core
  workers, which previously ran with no securityContext at all.
- Add terminationGracePeriodSeconds:45 to the worker StatefulSets so
  Kubernetes gives the new graceful-shutdown drain (see worker.py
  changes) enough time before SIGKILL.
2026-07-18 16:07:32 +05:30
37 changed files with 521 additions and 674 deletions

View File

@@ -60,7 +60,7 @@ js = None
# Configuration # Configuration
NATS_URL = os.getenv("NATS_URL", "nats://nats-server:4222") NATS_URL = os.getenv("NATS_URL", "nats://nats-server:4222")
NATS_USER = os.getenv("NATS_USER", "admin") NATS_USER = os.getenv("NATS_USER", "admin")
NATS_PASSWORD = os.getenv("NATS_PASSWORD", "package@321#") NATS_PASSWORD = os.getenv("NATS_PASSWORD", "")
# Endpoint to NATS subject mapping # Endpoint to NATS subject mapping
ENDPOINT_ROUTES = { ENDPOINT_ROUTES = {
@@ -172,7 +172,7 @@ async def publish_request_to_nats(endpoint: str, data: Dict[str, Any], request_m
subject = ENDPOINT_ROUTES.get(endpoint, "api.unknown") subject = ENDPOINT_ROUTES.get(endpoint, "api.unknown")
if not js: if not js:
raise HTTPException(status_code=503, detail="NATS by connected") raise HTTPException(status_code=503, detail="NATS not connected")
try: try:
# Create a unique inbox for the reply # Create a unique inbox for the reply

View File

@@ -13,6 +13,14 @@ http {
# NodePorts are bound to 0.0.0.0 and are more reliable to access from host.docker.internal # NodePorts are bound to 0.0.0.0 and are more reliable to access from host.docker.internal
proxy_pass http://jupiter_k8s; proxy_pass http://jupiter_k8s;
# Strip any CORS headers the backend may set itself, so we don't
# end up sending duplicate Access-Control-* headers (browsers
# reject a response that has more than one value for these).
proxy_hide_header 'Access-Control-Allow-Origin';
proxy_hide_header 'Access-Control-Allow-Methods';
proxy_hide_header 'Access-Control-Allow-Headers';
proxy_hide_header 'Access-Control-Max-Age';
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

View File

@@ -12,6 +12,7 @@ import asyncio
import aiohttp import aiohttp
import nats import nats
from nats.errors import TimeoutError as NatsTimeoutError from nats.errors import TimeoutError as NatsTimeoutError
from nats.js.api import ConsumerConfig
from prometheus_client import Counter, Histogram, Gauge, start_http_server from prometheus_client import Counter, Histogram, Gauge, start_http_server
import signal import signal
import sys import sys
@@ -25,7 +26,7 @@ messages_in_flight = Gauge('worker_messages_in_flight', 'Messages currently bein
# Configuration # Configuration
NATS_URL = os.getenv("NATS_URL", "nats://nats-server:4222") NATS_URL = os.getenv("NATS_URL", "nats://nats-server:4222")
NATS_USER = os.getenv("NATS_USER", "admin") NATS_USER = os.getenv("NATS_USER", "admin")
NATS_PASSWORD = os.getenv("NATS_PASSWORD", "package@321#") NATS_PASSWORD = os.getenv("NATS_PASSWORD", "")
# Domain Configuration (Env Vars from Deployment) # Domain Configuration (Env Vars from Deployment)
NATS_STREAM = os.getenv("NATS_STREAM", "ORDERS") NATS_STREAM = os.getenv("NATS_STREAM", "ORDERS")
@@ -35,6 +36,10 @@ FILTER_SUBJECTS = os.getenv("FILTER_SUBJECT", "api.v1.mob.orders.*").split(",")
WORKER_CONCURRENCY = int(os.getenv("WORKER_CONCURRENCY", "10")) WORKER_CONCURRENCY = int(os.getenv("WORKER_CONCURRENCY", "10"))
BASE_URL = os.getenv("EXTERNAL_BASE_URL", "https://jupiter.nearle.app") BASE_URL = os.getenv("EXTERNAL_BASE_URL", "https://jupiter.nearle.app")
# Must stay comfortably above the outbound HTTP timeout below (30s), otherwise
# JetStream can redeliver a message to another worker while this one is still
# waiting on a slow-but-legitimate response, causing a duplicate forward.
ACK_WAIT_SECONDS = int(os.getenv("ACK_WAIT_SECONDS", "60"))
# Endpoint mapping is still useful for constructing the target URL # Endpoint mapping is still useful for constructing the target URL
ENDPOINT_MAPPING = { ENDPOINT_MAPPING = {
@@ -56,6 +61,7 @@ js = None
session: aiohttp.ClientSession = None session: aiohttp.ClientSession = None
running = True running = True
semaphore = None # Initialized in main semaphore = None # Initialized in main
active_tasks: set = set() # In-flight process_message() tasks, drained on shutdown
def signal_handler(sig, frame): def signal_handler(sig, frame):
global running global running
@@ -80,7 +86,10 @@ async def forward_to_external(endpoint: str, payload: dict, api_key: str = None,
if api_key: if api_key:
headers["Authorization"] = f"Bearer {api_key}" headers["Authorization"] = f"Bearer {api_key}"
data_to_forward = payload.get("data", payload) if "data" not in payload:
print(f"?? Message for {endpoint} missing 'data' field, dropping.")
return "DROP", None
data_to_forward = payload["data"]
# Payload Normalization for logs # Payload Normalization for logs
if endpoint == "/live/api/v2/deliveries/createdeliverylog": if endpoint == "/live/api/v2/deliveries/createdeliverylog":
@@ -166,7 +175,8 @@ async def process_message(msg):
await msg.term() await msg.term()
except Exception as e: except Exception as e:
print(f"? Critical Worker Error: {e}") print(f"? Critical Worker Error: {e}")
await msg.nak() # Delay avoids a tight redelivery loop if this is a persistent bug
await msg.nak(delay=5)
finally: finally:
messages_in_flight.dec() messages_in_flight.dec()
duration = asyncio.get_event_loop().time() - start_time duration = asyncio.get_event_loop().time() - start_time
@@ -211,7 +221,8 @@ async def main():
sub = await js.pull_subscribe( sub = await js.pull_subscribe(
subject, subject,
durable=durable_name, durable=durable_name,
stream=NATS_STREAM stream=NATS_STREAM,
config=ConsumerConfig(ack_wait=ACK_WAIT_SECONDS)
) )
subs.append(sub) subs.append(sub)
except Exception as e: except Exception as e:
@@ -231,7 +242,9 @@ async def main():
# Batch size 10, short timeout to keep loop responsive # Batch size 10, short timeout to keep loop responsive
msgs = await sub.fetch(10, timeout=0.5) msgs = await sub.fetch(10, timeout=0.5)
for m in msgs: for m in msgs:
asyncio.create_task(process_message(m)) task = asyncio.create_task(process_message(m))
active_tasks.add(task)
task.add_done_callback(active_tasks.discard)
except NatsTimeoutError: except NatsTimeoutError:
pass pass
except Exception as e: except Exception as e:
@@ -244,6 +257,9 @@ async def main():
except Exception as e: except Exception as e:
print(f"?? Fatal Error: {e}") print(f"?? Fatal Error: {e}")
finally: finally:
if active_tasks:
print(f"?? Draining {len(active_tasks)} in-flight message(s) before shutdown...")
await asyncio.wait(active_tasks, timeout=25)
if session: if session:
await session.close() await session.close()
if nc: if nc:

View File

@@ -106,6 +106,27 @@ services:
- "traefik.http.routers.atlantis-api.tls.certresolver=letsencrypt" - "traefik.http.routers.atlantis-api.tls.certresolver=letsencrypt"
- "traefik.http.services.atlantis-api.loadbalancer.server.port=8205" - "traefik.http.services.atlantis-api.loadbalancer.server.port=8205"
# Doormile API Proxy (doormile.com -> K8s NodePort 30830)
doormile-proxy:
image: nginx:alpine
container_name: doormile-proxy
restart: unless-stopped
ports:
- "8206:8206"
volumes:
- ./conf/nginx-doormile.conf:/etc/nginx/nginx.conf:ro
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- web
labels:
- "traefik.enable=true"
- "traefik.docker.network=web"
- "traefik.http.routers.doormile-api.rule=Host(`doormile.com`)"
- "traefik.http.routers.doormile-api.tls=true"
- "traefik.http.routers.doormile-api.tls.certresolver=letsencrypt"
- "traefik.http.services.doormile-api.loadbalancer.server.port=8206"
networks: networks:
web: web:
external: true external: true

View File

@@ -330,10 +330,6 @@ spec:
- name: deliveries-service - name: deliveries-service
port: 8000 port: 8000
weight: 100 weight: 100
backendRefs:
- name: deliveries-service
port: 8000
weight: 100
- matches: - matches:
- path: - path:
type: PathPrefix type: PathPrefix

View File

@@ -34,19 +34,6 @@ metadata:
kubernetes.io/service-account.name: "admin-user" kubernetes.io/service-account.name: "admin-user"
type: kubernetes.io/service-account-token type: kubernetes.io/service-account-token
--- ---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: kubernetes-dashboard-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: kubernetes-dashboard
namespace: kubernetes-dashboard
---
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
@@ -75,9 +62,6 @@ spec:
args: args:
- --auto-generate-certificates - --auto-generate-certificates
- --namespace=kubernetes-dashboard - --namespace=kubernetes-dashboard
- --enable-skip-login
- --enable-insecure-login
- --insecure-port=9090
volumeMounts: volumeMounts:
- name: kubernetes-dashboard-certs - name: kubernetes-dashboard-certs
mountPath: /certs mountPath: /certs
@@ -135,10 +119,6 @@ spec:
targetPort: 8443 targetPort: 8443
protocol: TCP protocol: TCP
name: https name: https
- port: 9090
targetPort: 9090
protocol: TCP
name: http
selector: selector:
k8s-app: kubernetes-dashboard k8s-app: kubernetes-dashboard
--- ---
@@ -307,22 +287,3 @@ spec:
targetPort: 8083 targetPort: 8083
nodePort: 30826 nodePort: 30826
protocol: TCP protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: dashboard-loadbalancer
namespace: kubernetes-dashboard
labels:
app.kubernetes.io/name: kubernetes-dashboard
app.kubernetes.io/component: loadbalancer
spec:
type: NodePort
selector:
k8s-app: kubernetes-dashboard
ports:
- name: http
port: 9090
targetPort: 9090 # Dashboard HTTP port
nodePort: 30827 # Fixed NodePort for nginx proxy
protocol: TCP

View File

@@ -19,6 +19,7 @@ data:
import aiohttp import aiohttp
import nats import nats
from nats.errors import TimeoutError as NatsTimeoutError from nats.errors import TimeoutError as NatsTimeoutError
from nats.js.api import ConsumerConfig
from prometheus_client import Counter, Histogram, Gauge, start_http_server from prometheus_client import Counter, Histogram, Gauge, start_http_server
import signal import signal
import sys import sys
@@ -32,7 +33,7 @@ data:
# Configuration # Configuration
NATS_URL = os.getenv("NATS_URL", "nats://nats-server:4222") NATS_URL = os.getenv("NATS_URL", "nats://nats-server:4222")
NATS_USER = os.getenv("NATS_USER", "admin") NATS_USER = os.getenv("NATS_USER", "admin")
NATS_PASSWORD = os.getenv("NATS_PASSWORD", "package@321#") NATS_PASSWORD = os.getenv("NATS_PASSWORD", "")
# Domain Configuration (Env Vars from Deployment) # Domain Configuration (Env Vars from Deployment)
NATS_STREAM = os.getenv("NATS_STREAM", "ORDERS") NATS_STREAM = os.getenv("NATS_STREAM", "ORDERS")
@@ -42,6 +43,10 @@ data:
WORKER_CONCURRENCY = int(os.getenv("WORKER_CONCURRENCY", "10")) WORKER_CONCURRENCY = int(os.getenv("WORKER_CONCURRENCY", "10"))
BASE_URL = os.getenv("EXTERNAL_BASE_URL", "https://jupiter.nearle.app") BASE_URL = os.getenv("EXTERNAL_BASE_URL", "https://jupiter.nearle.app")
# Must stay comfortably above the outbound HTTP timeout below (30s), otherwise
# JetStream can redeliver a message to another worker while this one is still
# waiting on a slow-but-legitimate response, causing a duplicate forward.
ACK_WAIT_SECONDS = int(os.getenv("ACK_WAIT_SECONDS", "60"))
# Endpoint mapping is still useful for constructing the target URL # Endpoint mapping is still useful for constructing the target URL
ENDPOINT_MAPPING = { ENDPOINT_MAPPING = {
@@ -63,6 +68,7 @@ data:
session: aiohttp.ClientSession = None session: aiohttp.ClientSession = None
running = True running = True
semaphore = None # Initialized in main semaphore = None # Initialized in main
active_tasks: set = set() # In-flight process_message() tasks, drained on shutdown
def signal_handler(sig, frame): def signal_handler(sig, frame):
global running global running
@@ -87,7 +93,10 @@ data:
if api_key: if api_key:
headers["Authorization"] = f"Bearer {api_key}" headers["Authorization"] = f"Bearer {api_key}"
data_to_forward = payload.get("data", payload) if "data" not in payload:
print(f"?? Message for {endpoint} missing 'data' field, dropping.")
return "DROP", None
data_to_forward = payload["data"]
# Payload Normalization for logs # Payload Normalization for logs
if endpoint == "/live/api/v2/deliveries/createdeliverylog": if endpoint == "/live/api/v2/deliveries/createdeliverylog":
@@ -118,7 +127,7 @@ data:
if 200 <= response.status < 300: if 200 <= response.status < 300:
return "OK", body return "OK", body
elif response.status >= 500: elif response.status >= 500:
print(f"?? Server Error {response.status} from {endpoint}: {body}") print(f"?? Server Error {response.status} from {endpoint}")
return "RETRY", body return "RETRY", body
else: else:
print(f"?? Client Error {response.status} from {endpoint}: {body}") print(f"?? Client Error {response.status} from {endpoint}: {body}")
@@ -173,7 +182,8 @@ data:
await msg.term() await msg.term()
except Exception as e: except Exception as e:
print(f"? Critical Worker Error: {e}") print(f"? Critical Worker Error: {e}")
await msg.nak() # Delay avoids a tight redelivery loop if this is a persistent bug
await msg.nak(delay=5)
finally: finally:
messages_in_flight.dec() messages_in_flight.dec()
duration = asyncio.get_event_loop().time() - start_time duration = asyncio.get_event_loop().time() - start_time
@@ -218,7 +228,8 @@ data:
sub = await js.pull_subscribe( sub = await js.pull_subscribe(
subject, subject,
durable=durable_name, durable=durable_name,
stream=NATS_STREAM stream=NATS_STREAM,
config=ConsumerConfig(ack_wait=ACK_WAIT_SECONDS)
) )
subs.append(sub) subs.append(sub)
except Exception as e: except Exception as e:
@@ -238,7 +249,9 @@ data:
# Batch size 10, short timeout to keep loop responsive # Batch size 10, short timeout to keep loop responsive
msgs = await sub.fetch(10, timeout=0.5) msgs = await sub.fetch(10, timeout=0.5)
for m in msgs: for m in msgs:
asyncio.create_task(process_message(m)) task = asyncio.create_task(process_message(m))
active_tasks.add(task)
task.add_done_callback(active_tasks.discard)
except NatsTimeoutError: except NatsTimeoutError:
pass pass
except Exception as e: except Exception as e:
@@ -251,6 +264,9 @@ data:
except Exception as e: except Exception as e:
print(f"?? Fatal Error: {e}") print(f"?? Fatal Error: {e}")
finally: finally:
if active_tasks:
print(f"?? Draining {len(active_tasks)} in-flight message(s) before shutdown...")
await asyncio.wait(active_tasks, timeout=25)
if session: if session:
await session.close() await session.close()
if nc: if nc:

View File

@@ -27,6 +27,7 @@ spec:
prometheus.io/port: "9090" prometheus.io/port: "9090"
prometheus.io/path: "/metrics" prometheus.io/path: "/metrics"
spec: spec:
terminationGracePeriodSeconds: 45
securityContext: securityContext:
runAsUser: 1000 runAsUser: 1000
runAsGroup: 1000 runAsGroup: 1000

View File

@@ -22,6 +22,7 @@ spec:
prometheus.io/port: "9090" prometheus.io/port: "9090"
prometheus.io/path: "/metrics" prometheus.io/path: "/metrics"
spec: spec:
terminationGracePeriodSeconds: 45
tolerations: tolerations:
- key: dedicated - key: dedicated
operator: Equal operator: Equal
@@ -58,6 +59,11 @@ spec:
- name: worker - name: worker
image: workolik360/nats-worker:v1.1.0 image: workolik360/nats-worker:v1.1.0
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
command: ["python3", "-u", "/scripts/worker.py"] command: ["python3", "-u", "/scripts/worker.py"]
volumeMounts: volumeMounts:
- name: worker-script-vol - name: worker-script-vol
@@ -132,6 +138,7 @@ spec:
prometheus.io/port: "9090" prometheus.io/port: "9090"
prometheus.io/path: "/metrics" prometheus.io/path: "/metrics"
spec: spec:
terminationGracePeriodSeconds: 45
tolerations: tolerations:
- key: dedicated - key: dedicated
operator: Equal operator: Equal
@@ -168,6 +175,11 @@ spec:
- name: worker - name: worker
image: workolik360/nats-worker:v1.1.0 image: workolik360/nats-worker:v1.1.0
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
command: ["python3", "-u", "/scripts/worker.py"] command: ["python3", "-u", "/scripts/worker.py"]
volumeMounts: volumeMounts:
- name: worker-script-vol - name: worker-script-vol
@@ -236,6 +248,7 @@ spec:
prometheus.io/port: "9090" prometheus.io/port: "9090"
prometheus.io/path: "/metrics" prometheus.io/path: "/metrics"
spec: spec:
terminationGracePeriodSeconds: 45
tolerations: tolerations:
- key: dedicated - key: dedicated
operator: Equal operator: Equal
@@ -272,6 +285,11 @@ spec:
- name: worker - name: worker
image: workolik360/nats-worker:v1.1.0 image: workolik360/nats-worker:v1.1.0
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
command: ["python3", "-u", "/scripts/worker.py"] command: ["python3", "-u", "/scripts/worker.py"]
volumeMounts: volumeMounts:
- name: worker-script-vol - name: worker-script-vol
@@ -346,6 +364,7 @@ spec:
prometheus.io/port: "9090" prometheus.io/port: "9090"
prometheus.io/path: "/metrics" prometheus.io/path: "/metrics"
spec: spec:
terminationGracePeriodSeconds: 45
tolerations: tolerations:
- key: dedicated - key: dedicated
operator: Equal operator: Equal
@@ -382,6 +401,11 @@ spec:
- name: worker - name: worker
image: workolik360/nats-worker:v1.1.0 image: workolik360/nats-worker:v1.1.0
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
command: ["python3", "-u", "/scripts/worker.py"] command: ["python3", "-u", "/scripts/worker.py"]
volumeMounts: volumeMounts:
- name: worker-script-vol - name: worker-script-vol
@@ -456,6 +480,7 @@ spec:
prometheus.io/port: "9090" prometheus.io/port: "9090"
prometheus.io/path: "/metrics" prometheus.io/path: "/metrics"
spec: spec:
terminationGracePeriodSeconds: 45
tolerations: tolerations:
- key: dedicated - key: dedicated
operator: Equal operator: Equal
@@ -492,6 +517,11 @@ spec:
- name: worker - name: worker
image: workolik360/nats-worker:v1.1.0 image: workolik360/nats-worker:v1.1.0
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
command: ["python3", "-u", "/scripts/worker.py"] command: ["python3", "-u", "/scripts/worker.py"]
volumeMounts: volumeMounts:
- name: worker-script-vol - name: worker-script-vol

View File

@@ -2,26 +2,31 @@ apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
name: doormile name: doormile
labels:
Read 1 file
Found the critical issue — INTERNAL_API_KEY not set in the manifest means
all /internal/* endpoode explicitly rejects empty keys). That plus the NATS_URL duplicate.
Here's the corrected miletruth.yaml with all missing env vars added. Run
this on the server:
cat > /root/kuberneteh.yaml << 'EOF'
apiVersion: v1
kind: Namespace
metadata:
name: doormile name: doormile
--- ---
apiVersion: v1
kind: Secret
metadata:
name: doormile-secrets
namespace: doormile
labels:
app: doormile
type: Opaque
stringData:
DB_PASSWORD: "Package@321#"
REDIS_PASSWORD: "Package@321#"
NATS_USER: "doormile"
NATS_PASSWORD: "Package@321#"
---
apiVersion: apps/v1 apiVersion: apps/v1
kind: StatefulSet kind: StatefulSet
metadata: metadata:
name: doormile name: doormile
namespace: doormile namespace: doormile
labels:
app: doormile
app-group: doormile-api
spec: spec:
serviceName: "doormile-service" serviceName: "doormile-service"
replicas: 3 replicas: 3
@@ -33,57 +38,68 @@ spec:
metadata: metadata:
labels: labels:
app: doormile app: doormile
app-group: do app-group: doormile-api
spec: spec:
containers: containers:
- name: doormile - name: doormile
image: doormi image: doormile/doormile-backend:latest
imagePullPolicy: Always imagePullPolicy: Always
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
ports:
- containerPort: 8081
env: env:
- name: ENV - name: DB_HOST
value: "production"
- name: APP_PORT
value: "8081"
- name: DB_HO
value: "31.97.228.132" value: "31.97.228.132"
- name: DB_PO - name: DB_PORT
value: "5433" value: "5433"
- name: DB_NA - name: DB_NAME
value: "logistics" value: "logistics"
- name: DB_US - name: DB_USER
value: "admin" value: "admin"
- name: DB_PASSWORD - name: DB_PASSWORD
value: "Pac valueFrom:
secretKeyRef:
name: doormile-secrets
key: DB_PASSWORD
- name: REDIS_HOST - name: REDIS_HOST
value: "31.97.228.132" value: "31.97.228.132"
- name: REDIS - name: REDIS_PORT
value: "6379" value: "6379"
- name: REDIS_USER
value: "adm
- name: REDIS_PASSWORD - name: REDIS_PASSWORD
value: "Package@321#" valueFrom:
- name: JWT_S secretKeyRef:
value: "DoormileSuperSecretJWTKey2026!" name: doormile-secrets
- name: NATS_ key: REDIS_PASSWORD
- name: NATS_URL
value: "nats://66.116.226.161:4223" value: "nats://66.116.226.161:4223"
- name: NATS_ - name: NATS_USER
value: "doormile" valueFrom:
secretKeyRef:
name: doormile-secrets
key: NATS_USER
- name: NATS_PASSWORD - name: NATS_PASSWORD
value: "Pac valueFrom:
- name: INTERNAL_API_KEY secretKeyRef:
value: "doormile-internal-2024" name: doormile-secrets
key: NATS_PASSWORD
--- ---
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: doormile-service name: doormile-service
namespace: doormile namespace: doormile
labels:
app: doormile
spec: spec:
type: NodePort type: NodePort
selector: selector:
app-group: doormile-api app-group: doormile-api
ports: ports:
- protocol: TCP - protocol: TCP
port: 8081 port: 8081 # Expose port 8081 internally
targetPort: 808 targetPort: 8081 # The port the backend application actually listens on
nodePort: 30830 nodePort: 30830 # This must match what NGINX is looking for

View File

@@ -1,61 +0,0 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: atlantis
namespace: nearle
labels:
app: atlantis
spec:
serviceName: "atlantis"
replicas: 2
selector:
matchLabels:
app: atlantis
template:
metadata:
labels:
app: atlantis
spec:
tolerations:
- key: dedicated
operator: Equal
value: apps
effect: NoSchedule
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.workolik/app
operator: In
values:
- "true"
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: atlantis
topologyKey: kubernetes.io/hostname
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: atlantis
containers:
- name: backend
image: nearlecommerce/atlantis:v0.0.41
imagePullPolicy: Always
ports:
- containerPort: 3000
env:
- name: PORT
value: "3000"
envFrom:
- configMapRef:
name: nearle-config
- secretRef:
name: app-secrets

View File

@@ -1,16 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: atlantis
namespace: nearle
labels:
app: atlantis
spec:
type: NodePort
ports:
- port: 80
targetPort: 3000
nodePort: 30825
protocol: TCP
selector:
app: atlantis

View File

@@ -67,7 +67,7 @@ data:
# Configuration # Configuration
NATS_URL = os.getenv("NATS_URL", "nats://nats-server:4222") NATS_URL = os.getenv("NATS_URL", "nats://nats-server:4222")
NATS_USER = os.getenv("NATS_USER", "admin") NATS_USER = os.getenv("NATS_USER", "admin")
NATS_PASSWORD = os.getenv("NATS_PASSWORD", "package@321#") NATS_PASSWORD = os.getenv("NATS_PASSWORD", "")
# Endpoint to NATS subject mapping # Endpoint to NATS subject mapping
ENDPOINT_ROUTES = { ENDPOINT_ROUTES = {
@@ -179,7 +179,7 @@ data:
subject = ENDPOINT_ROUTES.get(endpoint, "api.unknown") subject = ENDPOINT_ROUTES.get(endpoint, "api.unknown")
if not js: if not js:
raise HTTPException(status_code=503, detail="NATS by connected") raise HTTPException(status_code=503, detail="NATS not connected")
try: try:
# Create a unique inbox for the reply # Create a unique inbox for the reply

View File

@@ -1,99 +0,0 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: fiesta
namespace: nearle
labels:
app: fiesta
spec:
serviceName: "fiesta"
replicas: 3
selector:
matchLabels:
app: fiesta
template:
metadata:
labels:
app: fiesta
spec:
tolerations:
- key: dedicated
operator: Equal
value: apps
effect: NoSchedule
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.workolik/app
operator: In
values:
- "true"
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: fiesta
topologyKey: kubernetes.io/hostname
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: fiesta
containers:
- name: backend
image: nearlecommerce/fiesta:v1.3.67
imagePullPolicy: Always
ports:
- containerPort: 1122
envFrom:
- configMapRef:
name: nearle-config
- secretRef:
name: app-secrets
env:
- name: PORT
value: "1122"
- name: NATS_USER
valueFrom:
secretKeyRef:
name: nats-credentials
key: username
- name: NATS_PASSWORD
valueFrom:
secretKeyRef:
name: nats-credentials
key: password
- name: gateway
image: workolik360/alaska:v1.2.0
imagePullPolicy: Always
ports:
- containerPort: 8000
name: http
volumeMounts:
- name: gateway-script
mountPath: /app/app.py
subPath: app.py
envFrom:
- configMapRef:
name: nearle-config
env:
- name: NATS_USER
valueFrom:
secretKeyRef:
name: nats-credentials
key: username
- name: NATS_PASSWORD
valueFrom:
secretKeyRef:
name: nats-credentials
key: password
volumes:
- name: gateway-script
configMap:
name: fiesta-gateway-script

View File

@@ -1,21 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: fiesta
namespace: nearle
labels:
app: fiesta
spec:
type: NodePort
ports:
- port: 80
targetPort: 1122
nodePort: 30823
protocol: TCP
name: main
- port: 8000
targetPort: 8000
name: gateway
protocol: TCP
selector:
app: fiesta

View File

@@ -16,6 +16,15 @@ data:
location / { location / {
proxy_pass http://jupiter_backend; proxy_pass http://jupiter_backend;
# Strip any CORS headers the backend may set itself, so we don't
# end up sending duplicate Access-Control-* headers (browsers
# reject a response that has more than one value for these).
proxy_hide_header 'Access-Control-Allow-Origin';
proxy_hide_header 'Access-Control-Allow-Methods';
proxy_hide_header 'Access-Control-Allow-Headers';
proxy_hide_header 'Access-Control-Max-Age';
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

View File

@@ -1,77 +0,0 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: jupiter
namespace: nearle
labels:
app: jupiter
spec:
serviceName: "jupiter"
replicas: 3
selector:
matchLabels:
app: jupiter
template:
metadata:
labels:
app: jupiter
spec:
tolerations:
- key: dedicated
operator: Equal
value: apps
effect: NoSchedule
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.workolik/app
operator: In
values:
- "true"
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: jupiter
topologyKey: kubernetes.io/hostname
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: jupiter
containers:
- name: jupiter
image: nearlecommerce/jupiter:v2.7.53
imagePullPolicy: Always
ports:
- containerPort: 1009
env:
- name: PORT
value: "1009"
- name: TZ
value: "Asia/Kolkata"
volumeMounts:
- name: tz-config
mountPath: /etc/localtime
readOnly: true
- name: tz-data
mountPath: /usr/share/zoneinfo
readOnly: true
envFrom:
- configMapRef:
name: nearle-config
- secretRef:
name: app-secrets
volumes:
- name: tz-config
hostPath:
path: /usr/share/zoneinfo/Asia/Kolkata
- name: tz-data
hostPath:
path: /usr/share/zoneinfo

View File

@@ -1,16 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: jupiter
namespace: nearle
labels:
app: jupiter
spec:
type: NodePort
ports:
- port: 80
targetPort: 1009
nodePort: 30822
protocol: TCP
selector:
app: jupiter

View File

@@ -49,6 +49,11 @@ spec:
- name: backend - name: backend
image: nearlecommerce/ariane:v1.0.22 image: nearlecommerce/ariane:v1.0.22
imagePullPolicy: Always imagePullPolicy: Always
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
ports: ports:
- containerPort: 1000 - containerPort: 1000
env: env:

View File

@@ -49,6 +49,11 @@ spec:
- name: backend - name: backend
image: nearlecommerce/atlantis:v0.0.41 image: nearlecommerce/atlantis:v0.0.41
imagePullPolicy: Always imagePullPolicy: Always
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
ports: ports:
- containerPort: 3000 - containerPort: 3000
env: env:

View File

@@ -47,8 +47,13 @@ spec:
app: fiesta app: fiesta
containers: containers:
- name: backend - name: backend
image: nearlecommerce/fiesta:v1.3.50 image: nearlecommerce/fiesta:v1.3.78
imagePullPolicy: Always imagePullPolicy: Always
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
ports: ports:
- containerPort: 1122 - containerPort: 1122
envFrom: envFrom:
@@ -72,6 +77,11 @@ spec:
- name: gateway - name: gateway
image: workolik360/alaska:v1.2.0 image: workolik360/alaska:v1.2.0
imagePullPolicy: Always imagePullPolicy: Always
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
ports: ports:
- containerPort: 8000 - containerPort: 8000
name: http name: http

View File

@@ -47,8 +47,13 @@ spec:
app: jupiter app: jupiter
containers: containers:
- name: jupiter - name: jupiter
image: nearlecommerce/jupiter:v2.7.31 image: nearlecommerce/jupiter:v2.7.55
imagePullPolicy: Always imagePullPolicy: Always
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
ports: ports:
- containerPort: 1009 - containerPort: 1009
env: env:

View File

@@ -49,6 +49,11 @@ spec:
- name: backend - name: backend
image: groomgear/groomgear:v1.0.41 image: groomgear/groomgear:v1.0.41
imagePullPolicy: Always imagePullPolicy: Always
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
ports: ports:
- containerPort: 1006 - containerPort: 1006
--- ---

View File

@@ -7,9 +7,9 @@ import nats
import os import os
async def purge_messages(): async def purge_messages():
nats_url = "nats://nats.workolik.com:4222" nats_url = os.getenv("NATS_URL", "nats://nats.workolik.com:4222")
nats_user = "admin" nats_user = os.getenv("NATS_USER", "admin")
nats_password = "package@321#" nats_password = os.getenv("NATS_PASSWORD", "")
try: try:
print(f"Connecting to NATS at {nats_url}...") print(f"Connecting to NATS at {nats_url}...")

View File

@@ -1,77 +1,54 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Setup JetStream stream and consumer for NATS Setup JetStream streams and consumers for NATS
Run this after NATS is deployed and running 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 asyncio
import nats import nats
import os import os
import sys import sys
async def setup_jetstream(): WORKER_DOMAINS = [
"""Configure JetStream with two streams (DELIVERIES, RIDER) and per-subject consumers.""" {"stream": "ORDERS", "consumer": "orders-worker", "subjects": [
nats_url = os.getenv("NATS_URL", "nats://nats.workolik.com:4222") "api.v1.mob.orders.createorder",
nats_user = os.getenv("NATS_USER", "admin") ]},
nats_password = os.getenv("NATS_PASSWORD", "package@321#") {"stream": "DELIVERIES", "consumer": "deliveries-worker", "subjects": [
# Stream definitions
streams = {
"DELIVERIES": {
"subjects": [
"api.v1.deliveries.createdeliveries", "api.v1.deliveries.createdeliveries",
"api.v1.deliveries.updatedelivery", "api.v1.deliveries.updatedelivery",
"api.v2.deliveries.createdeliverylog", "api.v2.deliveries.createdeliverylog",
], ]},
}, {"stream": "CUSTOMERS", "consumer": "customers-worker", "retention": "work", "subjects": [
"RIDER": { "api.v1.mob.customers.login",
"subjects": [ "api.v1.mob.customers.create",
]},
{"stream": "RIDER", "consumer": "rider-logs-worker", "subjects": [
"api.v2.partners.createriderlog", "api.v2.partners.createriderlog",
"api.v2.partners.createbreaklog", "api.v2.partners.createbreaklog",
"api.v2.partners.updatebreaklog", "api.v2.partners.updatebreaklog",
], ]},
}, {"stream": "PRODUCTS", "consumer": "products-worker", "subjects": [
"ORDERS": {
"subjects": [
"api.v1.mob.orders.createorder",
],
},
"PRODUCTS": {
"subjects": [
"api.v1.web.products.create", "api.v1.web.products.create",
], ]},
}, ]
"CUSTOMERS": {
"subjects": [
"api.v1.mob.customers.login",
"api.v1.mob.customers.create",
],
"retention": "work" # Special handling for Login queue: delete immediately after ack
},
}
# Per-subject durable consumers
consumers = { def durable_name(consumer: str, subject: str) -> str:
"DELIVERIES": { """Mirrors worker.py's durable-name derivation exactly."""
"api.v1.deliveries.createdeliveries": "deliveries_createdeliveries", suffix = subject.replace(".", "_").replace("*", "all").replace(">", "all")
"api.v1.deliveries.updatedelivery": "deliveries_updatedelivery", return f"{consumer}_{suffix}"
"api.v2.deliveries.createdeliverylog": "deliveries_createdeliverylog",
},
"RIDER": { async def setup_jetstream():
"api.v2.partners.createriderlog": "rider_createriderlog", nats_url = os.getenv("NATS_URL", "nats://nats.workolik.com:4222")
"api.v2.partners.createbreaklog": "rider_createbreaklog", nats_user = os.getenv("NATS_USER", "admin")
"api.v2.partners.updatebreaklog": "rider_updatebreaklog", nats_password = os.getenv("NATS_PASSWORD", "")
},
"ORDERS": {
"api.v1.mob.orders.createorder": "orders_createorder",
},
"PRODUCTS": {
"api.v1.web.products.create": "products_create",
},
"CUSTOMERS": {
"api.v1.mob.customers.login": "customers_login",
"api.v1.mob.customers.create": "customers_create",
},
}
try: try:
print(f"Connecting to NATS at {nats_url}...") print(f"Connecting to NATS at {nats_url}...")
@@ -85,17 +62,16 @@ async def setup_jetstream():
js = nc.jetstream() js = nc.jetstream()
# Create / recreate streams # Create / recreate streams
for stream_name, cfg in streams.items(): for domain in WORKER_DOMAINS:
stream_name = domain["stream"]
subjects = domain["subjects"]
retention_policy = domain.get("retention", "limits")
try: try:
info = await js.stream_info(stream_name) info = await js.stream_info(stream_name)
print(f"⚠️ Stream '{stream_name}' already exists with subjects={info.config.subjects}, updating...") print(f"⚠️ Stream '{stream_name}' already exists with subjects={info.config.subjects}, updating...")
# Determine retention policy
retention_policy = cfg.get("retention", "limits")
await js.update_stream( await js.update_stream(
name=stream_name, name=stream_name,
subjects=cfg["subjects"], subjects=subjects,
storage="memory", storage="memory",
retention=retention_policy, retention=retention_policy,
max_age=24 * 60 * 60, max_age=24 * 60 * 60,
@@ -106,13 +82,9 @@ async def setup_jetstream():
except Exception as e: except Exception as e:
if "not found" in str(e).lower() or "404" in str(e).lower(): if "not found" in str(e).lower() or "404" in str(e).lower():
print(f"Creating stream '{stream_name}'...") print(f"Creating stream '{stream_name}'...")
# Determine retention policy (use 'limits' by default, 'work' for queues)
retention_policy = cfg.get("retention", "limits")
await js.add_stream( await js.add_stream(
name=stream_name, name=stream_name,
subjects=cfg["subjects"], subjects=subjects,
storage="memory", storage="memory",
retention=retention_policy, retention=retention_policy,
max_age=24 * 60 * 60, max_age=24 * 60 * 60,
@@ -123,10 +95,14 @@ async def setup_jetstream():
else: else:
print(f"⚠️ Could not inspect stream '{stream_name}': {e}") print(f"⚠️ Could not inspect stream '{stream_name}': {e}")
# Create durable consumers per subject # Create durable consumers per subject - names match what worker.py
# computes at runtime, so max_deliver/ack_wait here actually apply.
print("\nConfiguring consumers...") print("\nConfiguring consumers...")
for stream_name, subject_map in consumers.items(): for domain in WORKER_DOMAINS:
for subject, durable in subject_map.items(): stream_name = domain["stream"]
consumer = domain["consumer"]
for subject in domain["subjects"]:
durable = durable_name(consumer, subject)
try: try:
print(f"Creating consumer '{durable}' on stream '{stream_name}' for subject '{subject}'...") print(f"Creating consumer '{durable}' on stream '{stream_name}' for subject '{subject}'...")
await js.add_consumer( await js.add_consumer(
@@ -136,7 +112,7 @@ async def setup_jetstream():
ack_policy="explicit", ack_policy="explicit",
deliver_policy="all", deliver_policy="all",
max_deliver=5, max_deliver=5,
ack_wait=30, ack_wait=60,
) )
print(f"✅ Consumer '{durable}' created") print(f"✅ Consumer '{durable}' created")
except Exception as e: except Exception as e:
@@ -147,12 +123,12 @@ async def setup_jetstream():
print("\n✅ JetStream setup complete!") print("\n✅ JetStream setup complete!")
print(" Streams:") print(" Streams:")
for name, cfg in streams.items(): for domain in WORKER_DOMAINS:
print(f" - {name}: {', '.join(cfg['subjects'])}") print(f" - {domain['stream']}: {', '.join(domain['subjects'])}")
print(" Consumers:") print(" Consumers:")
for stream_name, subject_map in consumers.items(): for domain in WORKER_DOMAINS:
for subject, durable in subject_map.items(): for subject in domain["subjects"]:
print(f" - {durable}: stream={stream_name}, subject={subject}") print(f" - {durable_name(domain['consumer'], subject)}: stream={domain['stream']}, subject={subject}")
await nc.close() await nc.close()
sys.exit(0) sys.exit(0)
@@ -163,4 +139,3 @@ async def setup_jetstream():
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(setup_jetstream()) asyncio.run(setup_jetstream())

View File

@@ -51,16 +51,18 @@ def update_yaml_with_script(yaml_path, script_path, key_line_start):
print(f"Successfully updated {yaml_path}") print(f"Successfully updated {yaml_path}")
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Update Fiesta Gateway # Update Fiesta Gateway
update_yaml_with_script( update_yaml_with_script(
r'e:\nats\kubernetes\manifests\nearle\fiesta-gateway.yaml', os.path.join(REPO_ROOT, 'manifests', 'nearle', 'fiesta-gateway.yaml'),
r'e:\nats\kubernetes\conf\app.py', os.path.join(REPO_ROOT, 'conf', 'app.py'),
' app.py: |' ' app.py: |'
) )
# Update Worker Script # Update Worker Script
update_yaml_with_script( update_yaml_with_script(
r'e:\nats\kubernetes\manifests\core\worker-script.yaml', os.path.join(REPO_ROOT, 'manifests', 'core', 'worker-script.yaml'),
r'e:\nats\kubernetes\conf\worker.py', os.path.join(REPO_ROOT, 'conf', 'worker.py'),
' worker.py: |' ' worker.py: |'
) )

View File

@@ -1,93 +1,63 @@
#!/bin/bash #!/bin/bash
# Status check across every namespace actually in use by this cluster.
# Usage: ./shfiles/check-k8s-status.sh
NAMESPACES=(core nearle alaska doormile kubernetes-dashboard)
echo "==========================================" echo "=========================================="
echo "🔍 KUBERNETES DEPLOYMENT STATUS" echo "🔍 KUBERNETES DEPLOYMENT STATUS"
echo "==========================================" echo "=========================================="
echo "" echo ""
echo "📦 1. PODS STATUS (All Namespaces)" echo "📦 1. NODES"
echo "-----------------------------------"
kubectl get nodes -o wide
echo ""
echo "📦 2. PODS (All Namespaces)"
echo "-----------------------------------" echo "-----------------------------------"
kubectl get pods -A -o wide kubectl get pods -A -o wide
echo "" echo ""
echo "📦 2. NATS-BACKEND NAMESPACE - PODS" for ns in "${NAMESPACES[@]}"; do
echo "-----------------------------------" echo "=========================================="
kubectl get pods -n nats-backend -o wide echo "📦 Namespace: ${ns}"
echo "=========================================="
echo "-- Pods --"
kubectl get pods -n "${ns}" -o wide 2>/dev/null || echo " (namespace not found)"
echo "" echo ""
echo "🌐 3. SERVICES & LOAD BALANCER" echo "-- Services --"
echo "-----------------------------------" kubectl get svc -n "${ns}" -o wide 2>/dev/null
kubectl get svc -n nats-backend -o wide
echo "" echo ""
echo "⚖️ 4. LOAD BALANCER DETAILS" echo "-- StatefulSets / Deployments --"
echo "-----------------------------------" kubectl get statefulsets,deployments -n "${ns}" -o wide 2>/dev/null
kubectl get svc fastapi-lb -n nats-backend -o yaml | grep -A 10 "spec:"
echo "" echo ""
echo "🚀 5. K3S KUBERNETES LOAD BALANCER PODS (klipper-lb)" echo "-- HPA / PodDisruptionBudgets --"
echo "-----------------------------------" kubectl get hpa,pdb -n "${ns}" 2>/dev/null
kubectl get pods -n kube-system -l "svccontroller.k3s.cattle.io/svcname=fastapi-lb" -o wide
echo "" echo ""
echo "📊 6. DEPLOYMENTS & REPLICAS" echo "-- Recent Events (last 10) --"
echo "-----------------------------------" kubectl get events -n "${ns}" --sort-by='.lastTimestamp' 2>/dev/null | tail -10
kubectl get deployments -n nats-backend -o wide
echo "" echo ""
done
echo "📈 7. HORIZONTAL POD AUTOSCALER (HPA)" echo "🌍 GATEWAY / HTTPROUTE / INGRESS (All Namespaces)"
echo "-----------------------------------"
kubectl get hpa -n nats-backend
echo ""
echo "🔗 8. ENDPOINTS (Service Backends)"
echo "-----------------------------------"
kubectl get endpoints -n nats-backend
echo ""
echo "🌍 9. INGRESS/GATEWAY STATUS"
echo "-----------------------------------" echo "-----------------------------------"
kubectl get gateway -A 2>/dev/null || echo "No Gateway API resources found" kubectl get gateway -A 2>/dev/null || echo "No Gateway API resources found"
kubectl get httproute -A 2>/dev/null || echo "No HTTPRoute resources found"
kubectl get ingress -A 2>/dev/null || echo "No Ingress resources found" kubectl get ingress -A 2>/dev/null || echo "No Ingress resources found"
echo "" echo ""
echo "📋 10. RECENT EVENTS (Last 20)" echo "💾 RESOURCE USAGE (CPU/Memory)"
echo "-----------------------------------" echo "-----------------------------------"
kubectl get events -n nats-backend --sort-by='.lastTimestamp' | tail -20 kubectl top pods -A 2>/dev/null || echo "Metrics server not available"
echo ""
echo "💾 11. RESOURCE USAGE (CPU/Memory)"
echo "-----------------------------------"
kubectl top pods -n nats-backend 2>/dev/null || echo "Metrics server not available"
kubectl top nodes 2>/dev/null || echo "Metrics server not available" kubectl top nodes 2>/dev/null || echo "Metrics server not available"
echo "" echo ""
echo "🔍 12. FASTAPI POD LOGS (Last 30 lines)"
echo "-----------------------------------"
kubectl logs -n nats-backend -l app=fastapi-backend --tail=30 2>/dev/null || echo "No FastAPI pods found"
echo ""
echo "🔍 13. WORKER POD LOGS (Last 30 lines)"
echo "-----------------------------------"
kubectl logs -n nats-backend -l app=nats-worker --tail=30 2>/dev/null || echo "No worker pods found"
echo ""
echo "🌐 14. NETWORK FLOW CHECK"
echo "-----------------------------------"
echo "Load Balancer External IP/Port:"
kubectl get svc fastapi-lb -n nats-backend -o jsonpath='{.status.loadBalancer.ingress[0].ip}:{.spec.ports[0].port}' 2>/dev/null || echo "Checking NodePort..."
kubectl get svc fastapi-lb -n nats-backend -o jsonpath='NodePort: {.spec.ports[0].nodePort}' 2>/dev/null
echo ""
echo "FastAPI Service ClusterIP:"
kubectl get svc fastapi-backend -n nats-backend -o jsonpath='{.spec.clusterIP}:{.spec.ports[0].port}' 2>/dev/null
echo ""
echo "✅ 15. HEALTH CHECK"
echo "-----------------------------------"
kubectl run health-check --rm -i --restart=Never --image=curlimages/curl -- curl -s http://fastapi-backend.nats-backend:8000/health 2>/dev/null || echo "Health check failed"
echo ""
echo "==========================================" echo "=========================================="
echo "✅ Status Check Complete!" echo "✅ Status Check Complete!"
echo "==========================================" echo "=========================================="

View File

@@ -0,0 +1,15 @@
#!/bin/bash
# Deploy "doormile" stack to Kubernetes
# Usage: ./deploy-doormile.sh
set -euo pipefail
NAMESPACE="doormile"
echo "🚀 Deploying Doormile Stack..."
kubectl apply -f manifests/doormile/miletruth.yaml
echo ""
echo "✅ Doormile deployment applied."
echo "📋 Current status:"
kubectl get all -n "${NAMESPACE}" || true

15
shfiles/deploy-ingress.sh Normal file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
# Deploy shared Ingress resources and Traefik CORS middlewares
# (queue.workolik.com, jupiter/fiesta/atlantis.nearle.app, alaska + nearle CORS)
# Usage: ./deploy-ingress.sh
set -euo pipefail
echo "🌐 Applying Ingress resources..."
kubectl apply -f manifests/core/ingress-unified.yaml
echo "🎛️ Applying Traefik CORS middlewares..."
kubectl apply -f manifests/core/traefik-middlewares.yaml
echo ""
echo "✅ Ingress & middlewares applied."

View File

@@ -9,16 +9,29 @@ NAMESPACE="nearle"
echo "🔎 Ensuring namespace '${NAMESPACE}' exists..." echo "🔎 Ensuring namespace '${NAMESPACE}' exists..."
kubectl apply -f manifests/nearle/nearle-namespace.yaml kubectl apply -f manifests/nearle/nearle-namespace.yaml
echo "🔐 Applying config & secrets..."
kubectl apply -f manifests/nearle/nearle-config.yaml
kubectl apply -f manifests/nearle/nearle-secrets.yaml
kubectl apply -f manifests/nearle/nearle-app-secrets.yaml
echo "📜 Applying fiesta gateway script ConfigMap..."
kubectl apply -f manifests/nearle/fiesta-gateway.yaml
echo "🌐 Applying Gateway API resources..."
kubectl apply -f manifests/nearle/nearle-gateway.yaml
kubectl apply -f manifests/nearle/nearle-reference-grant.yaml
echo "🚀 Deploying Services..." echo "🚀 Deploying Services..."
kubectl apply -f manifests/nearle/nearle-jupiter.yaml kubectl apply -f manifests/nearle/nearle-jupiter.yaml
kubectl apply -f manifests/nearle/nearle-titan.yaml kubectl apply -f manifests/nearle/nearle-titan.yaml
kubectl apply -f manifests/nearle/nearle-fiesta.yaml kubectl apply -f manifests/nearle/nearle-fiesta.yaml
kubectl apply -f manifests/nearle/nearle-ariane.yaml kubectl apply -f manifests/nearle/nearle-ariane.yaml
kubectl apply -f manifests/nearle/nearle-atlantis.yaml
echo "🧭 Deploying Jupiter CORS proxy..."
kubectl apply -f manifests/nearle/jupiter-cors-proxy.yaml
echo "" echo ""
echo "✅ Nearle stack deployment applied." echo "✅ Nearle stack deployment applied."
echo "📋 Current status:" echo "📋 Current status:"
echo " kubectl get all -n ${NAMESPACE}" kubectl get all -n "${NAMESPACE}" || true

View File

@@ -1,10 +1,12 @@
#!/bin/bash #!/bin/bash
# Kubernetes Deployment Script for 3-Node Cluster # Deploy the full stack to Kubernetes (core, nearle, alaska, doormile, ingress)
# Usage: ./deploy.sh # Usage: ./shfiles/deploy.sh (run from the repo root, or anywhere - it cd's there itself)
set -e set -euo pipefail
echo "🚀 Deploying to Kubernetes (3-Node Cluster)..." cd "$(dirname "$0")/.."
echo "🚀 Deploying full stack..."
# Check if kubectl is available # Check if kubectl is available
if ! command -v kubectl &> /dev/null; then if ! command -v kubectl &> /dev/null; then
@@ -23,46 +25,29 @@ kubectl cluster-info || {
NODE_COUNT=$(kubectl get nodes --no-headers | wc -l) NODE_COUNT=$(kubectl get nodes --no-headers | wc -l)
echo "📊 Cluster has $NODE_COUNT node(s)" echo "📊 Cluster has $NODE_COUNT node(s)"
# Apply namespace echo ""
echo "📦 Creating namespace..." echo "===== Core stack (NATS workers) ====="
kubectl apply -f manifests/namespace.yaml bash shfiles/deploy-core-stack.sh
# Apply secrets
echo "🔐 Creating secrets..."
kubectl apply -f manifests/secrets.yaml
# Apply FastAPI
echo "🐍 Deploying FastAPI backend..."
kubectl apply -f manifests/fastapi-deployment.yaml
kubectl apply -f manifests/fastapi-service.yaml
kubectl apply -f manifests/fastapi-hpa.yaml
# Apply Workers
echo "👷 Deploying NATS workers..."
kubectl apply -f manifests/worker-deployment.yaml
kubectl apply -f manifests/worker-hpa.yaml
# Apply Gateway (optional - only if Gateway API is installed)
read -p "Deploy Gateway API? (requires Gateway API controller) [y/N] " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "🚪 Deploying Gateway API..."
kubectl apply -f manifests/gateway.yaml
fi
echo "" echo ""
echo "✅ Deployment complete!" echo "===== Nearle stack (jupiter, titan, fiesta, ariane, atlantis) ====="
bash shfiles/deploy-nearle-stack.sh
echo ""
echo "===== Alaska stack (deliveries gateway + dashboard) ====="
bash shfiles/deploy-alaska.sh
echo ""
echo "===== Doormile stack ====="
bash shfiles/deploy-doormile.sh
echo ""
echo "===== Ingress & Traefik middlewares ====="
bash shfiles/deploy-ingress.sh
echo ""
echo "✅ Full deployment complete!"
echo "" echo ""
echo "📋 Check status:" echo "📋 Check status:"
echo " kubectl get pods -n nats-backend -o wide" echo " bash shfiles/check-k8s-status.sh"
echo " kubectl get nodes"
echo " kubectl get hpa -n nats-backend"
echo "" echo ""
echo "📊 View pod distribution across nodes:"
echo " kubectl get pods -n nats-backend -o wide | grep -E 'NAME|fastapi|worker'"
echo ""
echo "📝 View logs:"
echo " kubectl logs -f deployment/fastapi-backend -n nats-backend"
echo " kubectl logs -f deployment/nats-worker -n nats-backend"
echo ""

View File

@@ -1,9 +1,13 @@
#!/bin/bash #!/bin/bash
# Setup JetStream stream and consumer for NATS # Setup JetStream stream and consumer for NATS
set -euo pipefail
echo "🚀 Setting up NATS JetStream..." echo "🚀 Setting up NATS JetStream..."
cd "$(dirname "$0")" # scripts/ is a sibling of shfiles/, both under the repo root - cd there so
# the relative path below resolves regardless of where this is invoked from.
cd "$(dirname "$0")/.."
# Check if Python is available # Check if Python is available
if ! command -v python3 &> /dev/null; then if ! command -v python3 &> /dev/null; then
@@ -17,11 +21,19 @@ if ! python3 -c "import nats" 2>/dev/null; then
pip3 install nats-py pip3 install nats-py
fi fi
# Set environment variables # Pull live credentials from the cluster's Secret instead of hardcoding them
export NATS_URL="nats://nats.workolik.com:4222" # here - avoids yet another copy of the password to keep in sync if rotated.
export NATS_USER="admin" if command -v kubectl &> /dev/null && kubectl get secret nats-credentials -n core &> /dev/null 2>&1; then
export NATS_PASSWORD="package@321#" export NATS_USER
NATS_USER=$(kubectl get secret nats-credentials -n core -o jsonpath='{.data.username}' | base64 -d)
export NATS_PASSWORD
NATS_PASSWORD=$(kubectl get secret nats-credentials -n core -o jsonpath='{.data.password}' | base64 -d)
else
echo "⚠️ Could not read nats-credentials Secret from the cluster (kubectl not available or not connected)."
echo " Set NATS_USER / NATS_PASSWORD yourself before running this script."
fi
export NATS_URL="${NATS_URL:-nats://66.116.226.161:4222}"
# Run the setup script # Run the setup script
python3 scripts/setup_jetstream.py python3 scripts/setup_jetstream.py

4
terraform/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.terraform/
*.tfstate
*.tfstate.*
crash.log

22
terraform/.terraform.lock.hcl generated Normal file
View File

@@ -0,0 +1,22 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/kubernetes" {
version = "3.2.1"
constraints = ">= 2.10.0"
hashes = [
"h1:mcG69DdvaQvDNQzIo+SLVekECRLiNKavq5jbp/yieOU=",
"zh:067fe16a852d42e0f571712e36cb3e71855f917ea2041415e155f56ebc480d7f",
"zh:2815e174f8f0f032ea3a64f2196740ad000a39f88ae5646e7061bf15ed589f62",
"zh:2f94f6b689c59c43e596e724228f2861095d02c2a2ac2a257a4619667135ac75",
"zh:3e807310c84f11561b9ba06b978f03c46cfeca2e84ad0803d34d5d30a8a637cb",
"zh:5cba6f92202c60cac6898141356420709f5341b80ae4c360725cc647f86188ff",
"zh:72b841b6f0820d8f87c3d7c5a3611c35121ab9a4c1db4ea7a98b0319f209e474",
"zh:74770b892ee9b04829d92318d9e8ca96f8143b0c6c766e4141901908173fd01d",
"zh:7a723c8ebf9e218d0f7a0cfe6c0437f2b5eeb7ae015a14fad16e0f7fd9ef79ab",
"zh:a0f5073b2636a3894d4e9dd1b6853d5f324dd78728313bff79b842e5e9eca96f",
"zh:c13241cba993ef63a537beb6a1caf00e233bb045b50d197349530de0ee3276d5",
"zh:d52826f4b0227b7db99ea4a1d48f49a0bfb440563c92ebd2f8faec273c856c2d",
"zh:dc1cf5505a39a264a650b0830f74150ad02368787e5ead89e4007034f8f47831",
]
}

View File

@@ -1,34 +0,0 @@
terraform {
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
version = ">= 2.0.0"
}
}
}
provider "kubernetes" {
# This tells Terraform how to connect to your k3s cluster.
# Usually it looks for the file in ~/.kube/config.
config_path = "~/.kube/config"
}
# Example: Manage a Kubernetes Namespace using Terraform
resource "kubernetes_namespace" "core" {
metadata {
name = "core"
labels = {
name = "core"
environment = "production"
}
}
}
# Practical: Point Terraform to your updated .yaml files
# Instead of you running 'kubectl apply', Terraform will do it.
resource "kubernetes_manifest" "worker_orders" {
manifest = yamldecode(file("${path.module}/../manifests/core/workers.yaml"))
}
# (Add more resources here for nearle, alaska, etc.)

View File

@@ -1,20 +1,26 @@
# Create namespaces using Terraform # Create namespaces using Terraform
# This makes sure the zones 'core', 'nearle', 'alaska' are always present and correctly labeled. # This makes sure the zones 'core', 'nearle', 'alaska', 'doormile' are always present and correctly labeled.
resource "kubernetes_namespace" "core" { resource "kubernetes_namespace_v1" "core" {
metadata { metadata {
name = "core" name = "core"
} }
} }
resource "kubernetes_namespace" "nearle" { resource "kubernetes_namespace_v1" "nearle" {
metadata { metadata {
name = "nearle" name = "nearle"
} }
} }
resource "kubernetes_namespace" "alaska" { resource "kubernetes_namespace_v1" "alaska" {
metadata { metadata {
name = "alaska" name = "alaska"
} }
} }
resource "kubernetes_namespace_v1" "doormile" {
metadata {
name = "doormile"
}
}

View File

@@ -1,45 +1,93 @@
# Manage the Nearle Stack (Jupiter, Atlantis, Fiesta) # Manage the Nearle Stack (Jupiter, Atlantis, Fiesta) plus the shared core
# This is the "All-in-one" Terraform control for your major services # workers, Ingress and Traefik middlewares.
#
# NOTE: each of these manifest files contains MULTIPLE '---'-separated YAML
# documents (e.g. a StatefulSet + Service + HTTPRoute in one file). Terraform's
# built-in yamldecode() only parses a single document, so each file is split
# on the '---' separator first and turned into a for_each map, one
# kubernetes_manifest per document. This has been verified safe for these
# specific files (the number of '---' lines matches document-count minus one
# in each case - no embedded '---' inside any script/config content).
# 1. Jupiter Service locals {
resource "kubernetes_manifest" "nearle_jupiter_sts" { # Splits a multi-document YAML file into a map keyed by
manifest = yamldecode(file("${path.module}/../manifests/nearle/jupiter-sts.yaml")) # "<kind>/<namespace>/<name>" (namespace omitted for cluster-scoped
# resources), suitable for a kubernetes_manifest for_each.
jupiter_docs = {
for doc in [
for chunk in split("\n---\n", file("${path.module}/../manifests/nearle/nearle-jupiter.yaml")) :
yamldecode(chunk) if trimspace(chunk) != ""
] : "${doc.kind}/${lookup(doc.metadata, "namespace", "")}/${doc.metadata.name}" => doc
} }
resource "kubernetes_manifest" "nearle_jupiter_svc" { atlantis_docs = {
manifest = yamldecode(file("${path.module}/../manifests/nearle/jupiter-svc.yaml")) for doc in [
for chunk in split("\n---\n", file("${path.module}/../manifests/nearle/nearle-atlantis.yaml")) :
yamldecode(chunk) if trimspace(chunk) != ""
] : "${doc.kind}/${lookup(doc.metadata, "namespace", "")}/${doc.metadata.name}" => doc
} }
# 2. Atlantis Service fiesta_docs = {
resource "kubernetes_manifest" "nearle_atlantis_sts" { for doc in [
manifest = yamldecode(file("${path.module}/../manifests/nearle/atlantis-sts.yaml")) for chunk in split("\n---\n", file("${path.module}/../manifests/nearle/nearle-fiesta.yaml")) :
yamldecode(chunk) if trimspace(chunk) != ""
] : "${doc.kind}/${lookup(doc.metadata, "namespace", "")}/${doc.metadata.name}" => doc
} }
resource "kubernetes_manifest" "nearle_atlantis_svc" { core_workers_docs = {
manifest = yamldecode(file("${path.module}/../manifests/nearle/atlantis-svc.yaml")) for doc in [
for chunk in split("\n---\n", file("${path.module}/../manifests/core/workers.yaml")) :
yamldecode(chunk) if trimspace(chunk) != ""
] : "${doc.kind}/${lookup(doc.metadata, "namespace", "")}/${doc.metadata.name}" => doc
} }
# 3. Fiesta Service core_ingress_docs = {
resource "kubernetes_manifest" "nearle_fiesta_sts" { for doc in [
manifest = yamldecode(file("${path.module}/../manifests/nearle/fiesta-sts.yaml")) for chunk in split("\n---\n", file("${path.module}/../manifests/core/ingress-unified.yaml")) :
yamldecode(chunk) if trimspace(chunk) != ""
] : "${doc.kind}/${lookup(doc.metadata, "namespace", "")}/${doc.metadata.name}" => doc
} }
resource "kubernetes_manifest" "nearle_fiesta_svc" { traefik_middlewares_docs = {
manifest = yamldecode(file("${path.module}/../manifests/nearle/fiesta-svc.yaml")) for doc in [
for chunk in split("\n---\n", file("${path.module}/../manifests/core/traefik-middlewares.yaml")) :
yamldecode(chunk) if trimspace(chunk) != ""
] : "${doc.kind}/${lookup(doc.metadata, "namespace", "")}/${doc.metadata.name}" => doc
}
} }
# 4. Workers (CPU-heavy isolated nodes) # 1. Jupiter (StatefulSet + Service)
resource "kubernetes_manifest" "nearle_jupiter" {
for_each = local.jupiter_docs
manifest = each.value
}
# 2. Atlantis (StatefulSet + Service + HTTPRoute)
resource "kubernetes_manifest" "nearle_atlantis" {
for_each = local.atlantis_docs
manifest = each.value
}
# 3. Fiesta (StatefulSet + Service + HTTPRoute)
resource "kubernetes_manifest" "nearle_fiesta" {
for_each = local.fiesta_docs
manifest = each.value
}
# 4. Workers (CPU-heavy, isolated node pool - 5 StatefulSets)
resource "kubernetes_manifest" "core_workers" { resource "kubernetes_manifest" "core_workers" {
# This uses your existing workers.yaml file for_each = local.core_workers_docs
# Note: Since this file has MANY documents, I recommend splitting it the same way as above. manifest = each.value
manifest = yamldecode(file("${path.module}/../manifests/core/workers.yaml"))
} }
# 5. Ingress (Unified routing that replaces Docker-side Nginx) # 5. Ingress (queue.workolik.com, jupiter/fiesta/atlantis.nearle.app)
resource "kubernetes_manifest" "core_ingress" { resource "kubernetes_manifest" "core_ingress" {
manifest = yamldecode(file("${path.module}/../manifests/core/ingress-unified.yaml")) for_each = local.core_ingress_docs
manifest = each.value
} }
# 6. Traefik CORS middlewares (alaska + nearle)
resource "kubernetes_manifest" "traefik_middlewares" { resource "kubernetes_manifest" "traefik_middlewares" {
manifest = yamldecode(file("${path.module}/../manifests/core/traefik-middlewares.yaml")) for_each = local.traefik_middlewares_docs
manifest = each.value
} }