Compare commits

..

32 Commits

Author SHA1 Message Date
Suriya
1f5548ebb0 add MQTT and Redis config to fiesta; sync image tag to v1.3.93
Adds MQTT broker and Redis connection env vars to the fiesta
StatefulSet. Also corrects the image tag, which had drifted stale
in the repo at v1.3.90 while the cluster and the server copy of
the manifest were both already running v1.3.93.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 20:22:05 +05:30
Suriya
8d50163c72 raise queue.workolik.com edge rate limit for legitimate bulk orders
queue-api-proxy's Traefik rate limit was average=150/burst=50, live on the
container since before this repo existed (created 2026-05-30) but never
captured in docker-compose.yml - the container just kept running with
labels set at creation, invisible in git. Root-caused via a real bulk-order
test: 200 orders fired without client-side pacing hit 79 429s, all before
reaching NATS.

The gateway's per-request cost is cheap (validate + publish to JetStream),
and the worker pool absorbs bursts asynchronously once a message is queued
- the edge limit doesn't need to shape traffic, NATS already does that
downstream. It only needs to catch actual flood-scale abuse. Raised to
average=300/burst=400 so realistic bulk-order volume clears it comfortably.

Documented the labels in git for the first time so this isn't invisible
config drift going forward.
2026-07-30 16:52:56 +05:30
Suriya
58448f5faa fix: route Fiesta-tenant orders through queue.workolik.com to Fiesta's own backend
jupiter and Fiesta are separate applications (separate repos, separate
binaries) that happen to share one Postgres instance. The previous fix
(661a08d) repointed queue.workolik.com's createorder from jupiter's v1 to
jupiter's v3 handler to stop items being silently dropped - that worked
because they share a database, but it was never the right target: jupiter
has no item-required guard, no atomic order-number allocation, no
stock-insufficient check, because none of that was ever written for
jupiter. Fiesta's own CreateOrderv3 already has all of it.

Added explicit tenant-based routing in worker.py: FIESTA_TENANT_IDS (seeded
with 1147/R mart and 1135/Suriya Store, the two confirmed so far) forces
createorder for those tenants to FIESTA_BASE_URL instead of jupiter's
mapping. Explicit allowlist rather than a DB heuristic, since jupiter and
Fiesta share one `tenants` table with no single column that cleanly
separates the two populations (checked: tenanttype/moduleid/categoryid/
configid are all inconsistent across the tenants that are known to belong
to each app). Non-Fiesta tenants keep going to jupiter's v3 endpoint
(661a08d), unaffected.

Verified live through the real queue.workolik.com path:
- Zero-item order (tenant 1147): worker log shows "Routing tenant 1147
  createorder to Fiesta backend", Fiesta correctly returns 400 "Order must
  contain at least one item", no phantom order created.
- Order with items (tenant 1147): itemcount=1, detail_count=1, product 7076
  stock ledger moved 25->24, then restored to 25 on cancel.

Expand FIESTA_TENANT_IDS as more Fiesta tenants are identified - there's no
programmatic way to auto-detect them from the shared tenants table.
2026-07-29 18:15:24 +05:30
Suriya
661a08d0b7 fix: route queue.workolik.com createorder to jupiter's v3 (item-aware) handler
worker-script's ENDPOINT_MAPPING sent /live/api/v1/mob/orders/createorder to
jupiter's v1 CreateOrder, which only ever writes the order header - it never
loops over "items", so orderdetails/productstocks were silently never
touched for any order placed through queue.workolik.com. itemcount (a plain
scalar on the same Orders struct) persisted, making this easy to miss:
header looked fine, items silently vanished.

Confirmed live before the fix: order 147157/147159 (tenant 1147) had
itemcount=1, detail_count=0, stock unchanged. Root-caused via jupiter's own
log line (orderController.go:393, the log statement inside CreateOrder) at
the exact timestamp of the test request.

Repointed to /live/api/v3/orders/createorder (CreateOrderv3), which does
loop over Items and write orderdetails + productstocks. The loop is a no-op
when Items is empty, so tenants whose entire order history is header-only
(916, 908 - confirmed ~99.5%+ itemless across 100k+ and 6k orders
respectively) are unaffected. Verified live post-fix: order 147160 got
itemcount=1, detail_count=1, and product 7076's stock ledger moved 25->24
on create, restored to 25 on cancel. Tenant 916 traffic unaffected by design
(empty-Items loop is a no-op), not yet re-observed live since the restart
pending their next natural order batch.

Test orders (147157, 147158, 147159, 147160) cancelled, not deleted.
2026-07-29 17:52:44 +05:30
Suriya
d98ebdd152 clean up dead Fiesta NATS gateway sidecar and its routing scraps
The "gateway" sidecar in the fiesta StatefulSet (workolik360/alaska image,
running fiesta-gateway-script) and the k8s routing objects meant to reach it
(fiesta-route HTTPRoute, the port-8000 rules in nearle-ingress) never
actually served real traffic. fiesta.nearle.app is routed by a standalone
Docker/nginx proxy (conf/nginx-fiesta.conf, unchanged since the initial
commit) straight to NodePort 30823 - the real backend, no path splitting.
The Kubernetes-native routing objects have no working controller on this
cluster (no Traefik/Envoy pod; the shared Gateway resource's own status is
"Waiting for controller"), so they were inert either way.

The sidecar's only real effect, when briefly reachable, was publishing
Fiesta orders onto the same NATS subject worker-orders drains - which
forwards to jupiter, not this backend - producing the header-only phantom
orders fixed in the app on 2026-07-29. It's not a working parallel path,
it was the source of that bug.

Removed: the gateway container + gateway-script volume from the fiesta
StatefulSet, port 8000 from the fiesta Service, the fiesta-route HTTPRoute,
the fiesta-gateway-script ConfigMap (fiesta-gateway.yaml deleted entirely),
and the two dead port-8000 path rules in nearle-ingress's fiesta host block
(kept the correct catch-all). queue.workolik.com and the core-namespace NATS
worker pipeline are untouched - separate system entirely.

Verified live: fiesta pods rolled to single-container, service has only
port 80, and a real request through fiesta.nearle.app still behaves
correctly post-cleanup.
2026-07-29 17:34:03 +05:30
Suriya
29751d2d3d fix: route Fiesta mob/orders/createorder and web/products/create directly, sync image tag
These two paths were routed through the NATS gateway sidecar (port 8000,
fiesta-gateway-script), whose only NATS consumer (worker-orders/products in
core) has EXTERNAL_BASE_URL hardcoded to jupiter. Fiesta has no NATS
consumer of its own, so every mobile order/product submitted through this
route was silently created on jupiter (header-only, wrong schema) instead
of ever reaching this backend's CreateOrderv3 - no orderdetails, no stock
movement, no error surfaced to the client (fire-and-forget "accepted").

Both paths are already registered directly on this backend, so they now
fall through the catch-all rule straight to it (port 80), synchronously.
Verified live: a real order now moves stock and a no-items order gets an
immediate 400 instead of a silent phantom accept.

Also corrects the image tag (v1.3.78 -> v1.3.90) to match what's actually
running - it had drifted since Flux was removed and cluster changes now
happen via direct kubectl.
2026-07-29 17:11:18 +05:30
Suriya
4395828ac7 fix: bump jupiter to v2.7.59, fix worker ENDPOINT_MAPPING paths
jupiter v2.7.59 fixes an empty-string deliverytime being rejected by
Postgres on every order creation (see backend_jupiter commit 3da5876).

worker-script.yaml ENDPOINT_MAPPING had four entries pointing at
jupiter paths that don't exist (extra /mob/ or /web/ segments):
createorder, customers/login, customers/create, products/create.
createorder was silently dropping real orders with no retry (worker
treats 404 as a client error and calls msg.term()); the other three
had no observed traffic but had the same bug. Corrected all four to
jupiter's actual registered routes. Verified end-to-end: sent a real
order through queue.workolik.com and confirmed it landed in the
orders table.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 11:25:01 +05:30
Suriya
0cd4d73f9b feat: add daily retention trim for riderlogs Redis list
riderlogs is an unbounded RPUSH-only list (929K+ entries as of
writing, ~650MB), which is why getriderlogs needed a longer timeout
in v2.7.58. Adds a CronJob that runs daily at 3am IST, binary-searches
for the 90-day retention cutoff (the list is append-ordered so
roughly chronological), and LTRIMs anything older. Binary search
avoids scanning the full list - ~20 LINDEX calls instead of pulling
900K+ entries.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 10:48:23 +05:30
Suriya
d195556f31 chore: bump jupiter to v2.7.58
Fixes getriderlogs returning 500: it does a full-list LRANGE on the
riderlogs Redis key (929K+ entries, ~650MB), which exceeded the 3s
Redis read timeout added in v2.7.57. That call now gets a scoped 15s
timeout instead of loosening the timeout globally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 10:43:30 +05:30
root
aecf42085f chore: sync server-local config drift (catalogue/S3 env, kustomize deploy scripts)
Brings /opt/kubernetes in line with config that was applied directly
on the server: catalogue DB and S3 credentials/config in
nearle-config.yaml and nearle-app-secrets.yaml, and deploy scripts
switched to kubectl apply -k against the kustomize manifests. Also
dedupes USE_S3/S3_ENDPOINT/S3_BUCKET/S3_REGION which were listed
twice in nearle-config.yaml.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 22:29:57 -06:00
Suriya
24dcc41720 fix: correct nearledb port to 5433 in nearle-config
DATABASE_PORT/DB_PORT were set to 5432, which doesn't match the
actual nearledb server port (5433). Live ConfigMap was already
corrected directly; this brings the Flux-synced source back in line
so reconciliation doesn't revert it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 09:39:59 +05:30
Suriya
bab21c2403 fix: repoint worker EXTERNAL_BASE_URL at stable jupiter DNS name
Workers hardcoded jupiter's ClusterIP (10.43.224.63 / 10.43.229.168),
which had gone stale and pointed at nothing. Every request forwarded
from worker-orders, worker-deliveries, worker-customers,
worker-rider-logs, worker-products, and worker-notifications to
jupiter was timing out silently, breaking order creation, delivery
logs, and rider online status. Repointed at the stable in-cluster DNS
name (jupiter.nearle) instead of a ClusterIP so this can't go stale
again after a future service recreation.

Also bumps jupiter to v2.7.57 (Redis client timeout/pool fix) to
match what's already deployed live.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 09:36:33 +05:30
Suriya
cde7d4b84b Restore dashboard --enable-skip-login
Skip-login was stripped in an earlier "harden security" pass, which is
why the dashboard started demanding a token. Re-added it; it now runs
as the dashboard's own view-only ServiceAccount (get/list/watch), so
opening it needs no token but write access still requires the
admin-user token as before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 12:37:59 +05:30
Suriya
dd5dfe10f7 Restore dashboard skip-login; remove dead Flux config
Skip-login was stripped in the earlier "harden security" pass, which
is why the dashboard started demanding a token. Re-added
--enable-skip-login; it now runs as the dashboard's own view-only
ServiceAccount (get/list/watch), so opening it needs no token but
write access still requires the admin-user token.

Also removes clusters/production/ (flux-system bootstrap, the
apps-alaska/core/nearle Kustomizations, gitea webhook receiver) since
Flux was removed on the server side - deploys are manual kubectl
apply / deploy-*.sh from here on, and this config had no controller
left to read it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 12:37:31 +05:30
Suriya
1b629d8ea3 Recreate worker-notifications - lost when core namespace got wiped
This StatefulSet was deployed manually outside git at some point before
this repo's GitOps work began, and was destroyed when deleting the core
Kustomization triggered a full namespace recreation (Flux prune-on-delete
cascades regardless of kubectl's --cascade flag - that only affects
Kubernetes' own owner-reference GC, not Flux's finalizer).

Reconstructed from its own logs (NATS_STREAM=NOTIFICATIONS,
NATS_CONSUMER=notifications-worker, FILTER_SUBJECT=api.v1.notifications.push)
plus the same pattern as its sibling workers. Resource limits and
WORKER_CONCURRENCY are a best-guess match to worker-rider-logs, since the
original values were never version-controlled anywhere. Adding it to git
now so this can't happen again.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 12:25:55 +05:30
Suriya
430bd79bed Revert --token-ttl=0 on dashboard - broke login entirely
Login stopped working the moment this shipped. Suspect this dashboard
version treats token-ttl=0 as "expire immediately" rather than "never
expire". Reverting to the default (900s) to restore working login;
the idle-timeout annoyance can be revisited with a large finite value
instead of 0, tested before shipping.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 11:31:08 +05:30
Suriya
ff372b8b4c Disable dashboard session timeout so login persists
Default --token-ttl is 900s (15min idle), which was forcing re-entry of
the token repeatedly. Setting it to 0 makes a logged-in session
persist instead of expiring back to the login screen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 11:27:26 +05:30
Suriya
1b38a0c240 Exclude traefik-middlewares.yaml - Middleware CRD not installed on cluster
kubectl api-resources shows no Middleware kind under any API group, so
this manifest could never apply and was blocking the entire core
Kustomization. The Ingress annotation referencing it has been a
pre-existing no-op; CORS is actually handled by nginx-queue-proxy.conf
and each app's own CORS middleware.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 11:00:04 +05:30
Suriya
f883540272 Exclude nearle-ariane.yaml from Flux sync - crash-loops on first deploy
ariane was never actually running on the cluster before this rollout
(not in any prior pod listing); Flux deploying it for the first time
exposed it's broken, unrelated to the GitOps migration itself. Pulling
it out of scope so it stops blocking the nearle Kustomization's health
check for the services that do work (fiesta, jupiter, atlantis, titan).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 10:58:52 +05:30
Suriya
ffef003a2b Bring nearle stack and routing manifests under Flux management
- manifests/core/kustomization.yaml: add ingress-unified.yaml and
  traefik-middlewares.yaml, which were never in any kustomization and so
  were never actually GitOps-managed - the queue.workolik.com routing fix
  from c38a367 turned out to be live already (likely applied manually
  before this session), but was completely undetected by Flux until now.
  Dropped the redundant top-level `namespace: core` override since every
  existing resource already sets its own namespace explicitly, and the
  two new files span alaska/nearle.
- manifests/nearle/kustomization.yaml + clusters/production/apps-nearle.yaml:
  same GitOps treatment already applied to alaska/core, so a fiesta/jupiter/
  atlantis/titan/ariane version bump in git now auto-deploys instead of
  requiring manual kubectl apply.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 10:55:47 +05:30
Suriya
a86baac8a8 Trivial commit to test the Gitea -> Flux webhook
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 10:50:25 +05:30
Suriya
c526d494b0 Fix Receiver type: gitea is not a valid Flux receiver type
The installed Flux version's Receiver CRD rejects type: gitea, which
failed dry-run validation and blocked the entire flux-system
Kustomization batch from applying - including the alaska/core stacks.
Switched to type: generic (no payload parsing needed for a trigger-only
webhook).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 10:33:10 +05:30
Flux
e7340fb1ce Add Flux sync manifests 2026-07-19 22:58:13 -06:00
Flux
26557b0f9c Add Flux sync manifests 2026-07-19 22:51:38 -06:00
Flux
0e6aa1a4df Add Flux v2.9.2 component manifests 2026-07-19 22:51:27 -06:00
Suriya
dbf2941bbf Remove hardcoded replicas from HPA-managed deliveries StatefulSet
replicas: 4 was fighting the HPA's minReplicas: 8 on every reconcile -
GitOps tooling (Flux) reapplies the manifest on an interval, which would
keep yanking capacity back down between HPA corrections and quietly
undo the burst-headroom fix from c38a367.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 10:18:06 +05:30
Suriya
eb99e351dc Swap webhook Ingress for a NodePort service
No new A record could be added for a dedicated webhook host, so expose
Flux's webhook-receiver directly via NodePort instead of going through
Traefik/Ingress/DNS - same approach the deliveries LoadBalancer already
uses on 30662.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 10:14:11 +05:30
Suriya
f58f339b43 Add Flux CD GitOps sync for alaska/core stacks
Prepares clusters/production/ for `flux bootstrap git` - Kustomizations
for manifests/alaska and manifests/core, plus a Gitea push Receiver so
new commits reconcile immediately instead of waiting on the poll
interval. Webhook exposed on a dedicated host (flux-webhook.workolik.com)
to avoid the existing queue.workolik.com Gateway/Ingress overlap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 10:10:58 +05:30
Suriya
c38a36709b Fix queue.workolik.com routing conflict and give deliveries HPA burst headroom
queue.workolik.com was served by three separate routing definitions that
didn't agree: nginx-queue-proxy.conf and the classic queue-ingress both sent
everything to the deliveries app, but the Gateway API HTTPRoute
(deliveries-route) had a carve-out sending /live/api/v1/mob/orders and
/live/api/v1/web/products to fiesta's raw backend in the nearle namespace
instead (3 fixed replicas, no autoscaling, no resource limits) - a
completely different capacity profile from deliveries (HPA'd, 4-20
replicas). Depending on which router won for a given request, orders could
land on two backends with very different ability to absorb a burst,
plausibly explaining partial order loss / 429s under concurrent load.
Removed the carve-out so all three routing paths agree: everything goes to
deliveries-service.

Also raised deliveries-hpa minReplicas 4->8 and added an explicit
aggressive scaleUp behavior (no stabilization delay, up to 4 pods or 100%
every 15s). Autoscaling reacts to sustained load over roughly 30-60s
(metric polling + pod scheduling + readiness delay), so it does very
little for a burst that's over in seconds - minReplicas is the actual
defense; the behavior block just makes any further scaling land as fast as
possible.
2026-07-18 16:17:18 +05:30
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
50 changed files with 968 additions and 1340 deletions

0
bootstrap_server.sh Normal file → Executable file
View File

View File

@@ -60,7 +60,7 @@ js = None
# Configuration
NATS_URL = os.getenv("NATS_URL", "nats://nats-server:4222")
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_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")
if not js:
raise HTTPException(status_code=503, detail="NATS by connected")
raise HTTPException(status_code=503, detail="NATS not connected")
try:
# Create a unique inbox for the reply

View File

@@ -12,7 +12,15 @@ http {
# Proxy to Kubernetes NodePort (30822)
# NodePorts are bound to 0.0.0.0 and are more reliable to access from host.docker.internal
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 X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

View File

@@ -12,6 +12,7 @@ import asyncio
import aiohttp
import nats
from nats.errors import TimeoutError as NatsTimeoutError
from nats.js.api import ConsumerConfig
from prometheus_client import Counter, Histogram, Gauge, start_http_server
import signal
import sys
@@ -25,7 +26,7 @@ messages_in_flight = Gauge('worker_messages_in_flight', 'Messages currently bein
# Configuration
NATS_URL = os.getenv("NATS_URL", "nats://nats-server:4222")
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)
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"))
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 = {
@@ -56,6 +61,7 @@ js = None
session: aiohttp.ClientSession = None
running = True
semaphore = None # Initialized in main
active_tasks: set = set() # In-flight process_message() tasks, drained on shutdown
def signal_handler(sig, frame):
global running
@@ -80,7 +86,10 @@ async def forward_to_external(endpoint: str, payload: dict, api_key: str = None,
if 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
if endpoint == "/live/api/v2/deliveries/createdeliverylog":
@@ -166,7 +175,8 @@ async def process_message(msg):
await msg.term()
except Exception as 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:
messages_in_flight.dec()
duration = asyncio.get_event_loop().time() - start_time
@@ -210,8 +220,9 @@ async def main():
try:
sub = await js.pull_subscribe(
subject,
durable=durable_name,
stream=NATS_STREAM
durable=durable_name,
stream=NATS_STREAM,
config=ConsumerConfig(ack_wait=ACK_WAIT_SECONDS)
)
subs.append(sub)
except Exception as e:
@@ -231,7 +242,9 @@ async def main():
# Batch size 10, short timeout to keep loop responsive
msgs = await sub.fetch(10, timeout=0.5)
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:
pass
except Exception as e:
@@ -244,6 +257,9 @@ async def main():
except Exception as e:
print(f"?? Fatal Error: {e}")
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:
await session.close()
if nc:

3
deploy-alaska.sh Normal file → Executable file
View File

@@ -7,8 +7,7 @@ set -euo pipefail
NAMESPACE="alaska"
echo "🚀 Deploying Alaska Stack..."
kubectl apply -f manifests/alaska/alaska.yaml
kubectl apply -f manifests/alaska/k8s-dashboard.yaml
kubectl apply -k manifests/alaska
echo ""
echo "✅ Alaska stack deployment applied."

0
deploy-core-stack.sh Normal file → Executable file
View File

28
deploy-nearle-stack.sh Normal file → Executable file
View File

@@ -6,32 +6,8 @@ set -euo pipefail
NAMESPACE="nearle"
echo "🔎 Ensuring namespace '${NAMESPACE}' exists..."
kubectl apply -f manifests/nearle/nearle-namespace.yaml
echo "🔐 Deploying Configs & Secrets..."
kubectl apply -f manifests/nearle/nearle-secrets.yaml
kubectl apply -f manifests/nearle/nearle-app-secrets.yaml
kubectl apply -f manifests/nearle/nearle-config.yaml
kubectl apply -f manifests/nearle/fiesta-gateway.yaml
echo "🚀 Deploying Services..."
kubectl apply -f manifests/nearle/jupiter-sts.yaml
kubectl apply -f manifests/nearle/jupiter-svc.yaml
kubectl apply -f manifests/nearle/nearle-titan.yaml
kubectl apply -f manifests/nearle/fiesta-sts.yaml
kubectl apply -f manifests/nearle/fiesta-svc.yaml
kubectl apply -f manifests/nearle/nearle-ariane.yaml
kubectl apply -f manifests/nearle/atlantis-sts.yaml
kubectl apply -f manifests/nearle/atlantis-svc.yaml
echo "🌐 Deploying Gateway Routes..."
kubectl apply -f manifests/nearle/nearle-gateway.yaml
kubectl apply -f manifests/nearle/nearle-reference-grant.yaml
echo "🚀 Deploying Nearle Stack..."
kubectl apply -k manifests/nearle
echo ""
echo "✅ Nearle stack deployment applied."

View File

@@ -21,6 +21,16 @@ services:
- "traefik.http.routers.queue-api.tls.certresolver=letsencrypt"
- "traefik.http.routers.queue-api.priority=100"
- "traefik.http.services.queue-api.loadbalancer.server.port=8201"
# Edge rate limit (per source IP, Traefik default). Predates this repo -
# the running container (created 2026-05-30) had average=150/burst=50
# baked in at creation time, undocumented here since labels aren't
# re-read on an existing container. Raised to fit real bulk-order
# traffic: NATS + the worker pool absorb bursts fine once a request
# reaches the gateway, so this only needs to block actual flood-scale
# abuse, not legitimate customers batching orders (2026-07-30).
- "traefik.http.middlewares.queue-rl.ratelimit.average=300"
- "traefik.http.middlewares.queue-rl.ratelimit.burst=400"
- "traefik.http.routers.queue-api.middlewares=queue-rl"
# Kubernetes dashboard proxy (kube.workolik.com ? K8s dashboard)
k8s-dashboard-proxy:
@@ -106,6 +116,27 @@ services:
- "traefik.http.routers.atlantis-api.tls.certresolver=letsencrypt"
- "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:
web:
external: true

View File

@@ -49,14 +49,16 @@ kubectl get svc -n kubernetes-dashboard
### Login to Dashboard
The dashboard is configured with `--enable-skip-login`, so you can skip the login screen. However, if you need to authenticate:
The dashboard is configured with `--enable-skip-login`, so it opens straight to the UI - no token needed to look around.
1. Get the token:
Skip-login runs as the dashboard's own `kubernetes-dashboard` ServiceAccount, which is **view-only** (get/list/watch). If you need to edit, delete, or exec into something:
1. Get an admin token:
```bash
kubectl -n kubernetes-dashboard create token admin-user
```
2. Copy the token and paste it in the dashboard login screen.
2. Click "Sign In" on the dashboard and paste the token.
### What You Can See

View File

@@ -65,7 +65,9 @@ metadata:
app.kubernetes.io/component: backend
spec:
serviceName: "deliveries" # Required for StatefulSet
replicas: 4
# replicas intentionally omitted - the HPA below owns this field. A
# hardcoded value here would fight the HPA on every GitOps reconcile,
# yanking replicas back down between HPA corrections.
selector:
matchLabels:
app.kubernetes.io/name: deliveries
@@ -244,8 +246,29 @@ spec:
apiVersion: apps/v1
kind: StatefulSet
name: deliveries
minReplicas: 4
minReplicas: 8
maxReplicas: 20
# Kubernetes autoscaling reacts to sustained load over ~30-60s (metric
# polling + pod scheduling + readiness delay) - it does very little for a
# burst that's over in seconds. This behavior block removes the scale-up
# stabilization delay and allows adding pods aggressively, so the cluster
# reacts as fast as the metrics pipeline allows rather than waiting extra
# cycles. minReplicas above is the main defense for bursts (baseline
# capacity that's already there before a burst starts); this just makes
# whatever additional scaling happens land as quickly as possible.
behavior:
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 15
- type: Pods
value: 4
periodSeconds: 15
selectPolicy: Max
scaleDown:
stabilizationWindowSeconds: 300
metrics:
- type: Resource
resource:
@@ -330,28 +353,6 @@ spec:
- name: deliveries-service
port: 8000
weight: 100
backendRefs:
- name: deliveries-service
port: 8000
weight: 100
- matches:
- path:
type: PathPrefix
value: /live/api/v1/mob/orders
backendRefs:
- name: fiesta
namespace: nearle
port: 80
weight: 100
- matches:
- path:
type: PathPrefix
value: /live/api/v1/web/products
backendRefs:
- name: fiesta
namespace: nearle
port: 80
weight: 100
- matches:
- path:
type: PathPrefix

View File

@@ -34,19 +34,6 @@ metadata:
kubernetes.io/service-account.name: "admin-user"
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
kind: Deployment
metadata:
@@ -76,8 +63,18 @@ spec:
- --auto-generate-certificates
- --namespace=kubernetes-dashboard
- --enable-skip-login
- --enable-insecure-login
- --insecure-port=9090
# Skip-login uses the "kubernetes-dashboard" ServiceAccount below,
# which only has get/list/watch (view-only) - so opening the
# dashboard needs no token, but it can't edit/delete/exec.
# For write access, still log in with the admin-user token
# (kubectl -n kubernetes-dashboard create token admin-user).
#
# --token-ttl=0 was tried here to disable the 15-min idle
# timeout, but login broke immediately after that pod came up -
# in this dashboard version, 0 appears to mean "expire
# immediately" rather than "never expire". Reverted; back to
# the default 900s timeout until a properly-tested value (e.g.
# a long finite number of seconds) is confirmed safe.
volumeMounts:
- name: kubernetes-dashboard-certs
mountPath: /certs
@@ -135,10 +132,6 @@ spec:
targetPort: 8443
protocol: TCP
name: https
- port: 9090
targetPort: 9090
protocol: TCP
name: http
selector:
k8s-app: kubernetes-dashboard
---
@@ -307,22 +300,3 @@ spec:
targetPort: 8083
nodePort: 30826
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

@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- alaska.yaml
- k8s-dashboard.yaml

View File

@@ -43,20 +43,13 @@ spec:
- host: fiesta.nearle.app
http:
paths:
- path: /live/api/v1/mob/orders/createorder
pathType: Prefix
backend:
service:
name: fiesta
port:
number: 8000
- path: /live/api/v1/web/products/create
pathType: Prefix
backend:
service:
name: fiesta
port:
number: 8000
# Real routing for this host is the standalone Docker/nginx proxy
# (conf/nginx-fiesta.conf), which forwards everything to NodePort
# 30823 (this backend, port 80) - no path splitting, no gateway
# sidecar. This Ingress has no controller on the cluster (confirmed:
# no Traefik/Envoy pod, no IngressClass) so it's inert either way,
# but kept as a single correct catch-all rather than a stale
# path-split that pointed part of it at a dead gateway sidecar.
- path: /
pathType: Prefix
backend:

View File

@@ -1,7 +1,10 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: core
# No top-level `namespace:` override - every resource below (including
# ingress-unified.yaml, which spans alaska and nearle) already sets its own
# metadata.namespace explicitly. A blanket override here would silently
# force cross-namespace resources into "core" and break their backend refs.
resources:
- core-namespace.yaml
@@ -9,4 +12,11 @@ resources:
- core-config.yaml
- worker-script.yaml
- workers.yaml
- worker-notifications.yaml
- worker-pdb.yaml
- ingress-unified.yaml
# traefik-middlewares.yaml intentionally excluded - this cluster's
# Traefik has no Middleware CRD installed under any API group, so this
# can never apply. The router.middlewares annotation referencing it on
# the Ingress has been a pre-existing no-op; actual CORS handling comes
# from nginx-queue-proxy.conf and the apps' own CORS middleware.

View File

@@ -0,0 +1,122 @@
# Reconstructed manifest - this StatefulSet was originally deployed
# manually outside of git, and was lost when the core namespace got
# recreated during the Flux removal incident on 2026-07-20. Rebuilt from
# its confirmed runtime config (NATS_STREAM/NATS_CONSUMER/FILTER_SUBJECT
# seen in its own logs) plus the same pattern as its sibling workers.
# Resource requests/limits and WORKER_CONCURRENCY are best-guess matches
# to the lightest sibling worker (worker-rider-logs) - adjust if the
# original values are known.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: worker-notifications
namespace: core
labels:
app.kubernetes.io/name: worker-notifications
app.kubernetes.io/component: worker
spec:
serviceName: "worker-notifications"
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: worker-notifications
template:
metadata:
labels:
app.kubernetes.io/name: worker-notifications
app.kubernetes.io/component: worker
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9090"
prometheus.io/path: "/metrics"
spec:
terminationGracePeriodSeconds: 45
tolerations:
- key: dedicated
operator: Equal
value: workers
effect: NoSchedule
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-role.workolik/worker
operator: In
values:
- "true"
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- worker-notifications
topologyKey: kubernetes.io/hostname
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: worker-notifications
containers:
- name: worker
image: workolik360/nats-worker:v1.1.0
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
command: ["python3", "-u", "/scripts/worker.py"]
volumeMounts:
- name: worker-script-vol
mountPath: /scripts
envFrom:
- configMapRef:
name: core-config
env:
- name: NATS_STREAM
value: "NOTIFICATIONS"
- name: NATS_CONSUMER
value: "notifications-worker"
- name: FILTER_SUBJECT
value: "api.v1.notifications.push"
- name: WORKER_CONCURRENCY
value: "10"
- name: NATS_USER
valueFrom:
secretKeyRef:
name: nats-credentials
key: username
- name: NATS_PASSWORD
valueFrom:
secretKeyRef:
name: nats-credentials
key: password
- name: EXTERNAL_ENDPOINT_API_KEY
valueFrom:
secretKeyRef:
name: external-endpoint-secrets
key: api_key
optional: true
- name: EXTERNAL_BASE_URL
value: "http://jupiter.nearle"
resources:
requests:
memory: "128Mi"
cpu: "40m"
limits:
memory: "128Mi"
cpu: "200m"
ports:
- containerPort: 9090
name: metrics
volumes:
- name: worker-script-vol
configMap:
name: worker-script

View File

@@ -19,6 +19,7 @@ data:
import aiohttp
import nats
from nats.errors import TimeoutError as NatsTimeoutError
from nats.js.api import ConsumerConfig
from prometheus_client import Counter, Histogram, Gauge, start_http_server
import signal
import sys
@@ -32,17 +33,35 @@ data:
# Configuration
NATS_URL = os.getenv("NATS_URL", "nats://nats-server:4222")
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)
NATS_STREAM = os.getenv("NATS_STREAM", "ORDERS")
NATS_CONSUMER = os.getenv("NATS_CONSUMER", "orders-worker")
# Supports comma-separated subjects: "api.v1.orders.*,api.v2.orders.*"
FILTER_SUBJECTS = os.getenv("FILTER_SUBJECT", "api.v1.mob.orders.*").split(",")
WORKER_CONCURRENCY = int(os.getenv("WORKER_CONCURRENCY", "10"))
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"))
# Fiesta is a *separate application* (separate repo, separate binary,
# separate business line - jupiter and Fiesta only happen to share one
# Postgres instance). Its tenants' orders need Fiesta's own CreateOrderv3
# (item validation, atomic order numbers, stock-insufficient checks) -
# jupiter has no equivalent logic and was never meant to process this
# tenant population. jupiter and Fiesta share one `tenants` table with
# no single clean column to tell them apart, so this is an explicit
# allowlist rather than a heuristic. Expand FIESTA_TENANT_IDS as more
# Fiesta tenants are identified (2026-07-29).
FIESTA_BASE_URL = os.getenv("FIESTA_BASE_URL", "http://fiesta.nearle")
FIESTA_TENANT_IDS = set(
int(t) for t in os.getenv("FIESTA_TENANT_IDS", "").split(",") if t.strip()
)
# Endpoint mapping is still useful for constructing the target URL
ENDPOINT_MAPPING = {
"/live/api/v1/deliveries/createdeliveries": f"{BASE_URL}/live/api/v1/deliveries/createdeliveries",
@@ -51,10 +70,17 @@ data:
"/live/api/v2/deliveries/createdeliverylog": f"{BASE_URL}/live/api/v2/deliveries/createdeliverylog",
"/live/api/v2/partners/createbreaklog": f"{BASE_URL}/live/api/v2/partners/createbreaklog",
"/live/api/v2/partners/updatebreaklog": f"{BASE_URL}/live/api/v2/partners/updatebreaklog",
"/live/api/v1/mob/orders/createorder": f"{BASE_URL}/live/api/v1/mob/orders/createorder",
"/live/api/v1/web/products/create": f"{BASE_URL}/live/api/v1/web/products/create",
"/live/api/v1/mob/customers/login": f"{BASE_URL}/live/api/v1/mob/customers/login",
"/live/api/v1/mob/customers/create": f"{BASE_URL}/live/api/v1/mob/customers/create",
# Default target for non-Fiesta tenants (jupiter). v1 CreateOrder
# only ever writes the order header - it never loops over "items".
# CreateOrderv3 does, and the loop is a no-op when Items is empty,
# so jupiter-native tenants who never send items (e.g. 916/908)
# behave identically either way. Fiesta tenants are redirected to
# their own backend below, in forward_to_external - this mapping
# is only the fallback for everyone else.
"/live/api/v1/mob/orders/createorder": f"{BASE_URL}/live/api/v3/orders/createorder",
"/live/api/v1/web/products/create": f"{BASE_URL}/live/api/v1/products/create",
"/live/api/v1/mob/customers/login": f"{BASE_URL}/live/api/v1/customers/login",
"/live/api/v1/mob/customers/create": f"{BASE_URL}/live/api/v1/customers/create",
}
# Global State
@@ -63,6 +89,7 @@ data:
session: aiohttp.ClientSession = None
running = True
semaphore = None # Initialized in main
active_tasks: set = set() # In-flight process_message() tasks, drained on shutdown
def signal_handler(sig, frame):
global running
@@ -87,7 +114,31 @@ data:
if 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"]
# Fiesta-tenant orders go to Fiesta's own backend instead of jupiter
# - see FIESTA_TENANT_IDS above. tenantid may arrive nested under
# "orders" (mobile app shape) or flat (direct/API-tool shape); this
# is only used to pick the target, Fiesta's own handler does its own
# (more thorough) parsing of the actual body.
if endpoint == "/live/api/v1/mob/orders/createorder" and FIESTA_TENANT_IDS:
tenantid = None
if isinstance(data_to_forward, dict):
orders_obj = data_to_forward.get("orders")
if isinstance(orders_obj, dict) and "tenantid" in orders_obj:
tenantid = orders_obj.get("tenantid")
elif "tenantid" in data_to_forward:
tenantid = data_to_forward.get("tenantid")
try:
tenantid = int(tenantid) if tenantid is not None else None
except (TypeError, ValueError):
tenantid = None
if tenantid in FIESTA_TENANT_IDS:
external_url = f"{FIESTA_BASE_URL}{endpoint}"
print(f"?? Routing tenant {tenantid} createorder to Fiesta backend")
# Payload Normalization for logs
if endpoint == "/live/api/v2/deliveries/createdeliverylog":
@@ -97,7 +148,7 @@ data:
data_to_forward = [data_to_forward]
http_method = method.upper() if method else "POST"
try:
# We use the global 'session' here
async with session.request(
@@ -108,7 +159,7 @@ data:
headers=headers,
timeout=aiohttp.ClientTimeout(total=30)
) as response:
body = None
try:
body = await response.json()
@@ -118,7 +169,7 @@ data:
if 200 <= response.status < 300:
return "OK", body
elif response.status >= 500:
print(f"?? Server Error {response.status} from {endpoint}: {body}")
print(f"?? Server Error {response.status} from {endpoint}")
return "RETRY", body
else:
print(f"?? Client Error {response.status} from {endpoint}: {body}")
@@ -136,7 +187,7 @@ data:
messages_in_flight.inc()
start_time = asyncio.get_event_loop().time()
endpoint = "unknown"
try:
payload = json.loads(msg.data.decode())
endpoint = payload.get("endpoint", payload.get("original_path", "unknown"))
@@ -145,14 +196,14 @@ data:
print(f"?? Processing {endpoint}")
api_key = os.getenv("EXTERNAL_ENDPOINT_API_KEY", "")
# Single attempt
status, response_data = await forward_to_external(endpoint, payload, api_key, http_method)
if status == "OK":
await msg.ack()
messages_processed.labels(status="success", endpoint=endpoint).inc()
# Request-Reply Logic
if msg.reply:
reply_payload = json.dumps(response_data) if isinstance(response_data, (dict, list)) else str(response_data)
@@ -162,7 +213,7 @@ data:
# Unrecoverable error or unknown endpoint
await msg.term() # Terminate stops redelivery
messages_processed.labels(status="dropped", endpoint=endpoint).inc()
else: # RETRY
# Let JetStream handle backoff
await msg.nak(delay=2) # Custom delay before redelivery if desired, or just nak()
@@ -173,7 +224,8 @@ data:
await msg.term()
except Exception as 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:
messages_in_flight.dec()
duration = asyncio.get_event_loop().time() - start_time
@@ -181,7 +233,7 @@ data:
async def main():
global nc, js, session, semaphore
print(f"?? Starting Worker for Domain: {NATS_CONSUMER}")
print(f"?? Stream: {NATS_STREAM}, Subjects: {FILTER_SUBJECTS}")
print(f"?? Concurrency: {WORKER_CONCURRENCY}")
@@ -202,12 +254,12 @@ data:
# Create Pull Subscriptions for each filter subject
# All sharing the same Consumer Name ensuring load balancing if multiple pods run this
subs = []
for subject in FILTER_SUBJECTS:
subject = subject.strip()
if not subject: continue
# FIX: On WorkQueue streams, we cannot reuse the same durable name for different filters
# We append a sanitized version of the subject to ensure uniqueness per filter
clean_subject_suffix = subject.replace(".", "_").replace("*", "all").replace(">", "all")
@@ -217,8 +269,9 @@ data:
try:
sub = await js.pull_subscribe(
subject,
durable=durable_name,
stream=NATS_STREAM
durable=durable_name,
stream=NATS_STREAM,
config=ConsumerConfig(ack_wait=ACK_WAIT_SECONDS)
)
subs.append(sub)
except Exception as e:
@@ -238,19 +291,24 @@ data:
# Batch size 10, short timeout to keep loop responsive
msgs = await sub.fetch(10, timeout=0.5)
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:
pass
except Exception as e:
print(f"?? Fetch Error: {e}")
await asyncio.sleep(1)
# Small sleep to prevent tight loop if no messages
# await asyncio.sleep(0.01)
except Exception as e:
print(f"?? Fatal Error: {e}")
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:
await session.close()
if nc:

View File

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

View File

@@ -22,6 +22,7 @@ spec:
prometheus.io/port: "9090"
prometheus.io/path: "/metrics"
spec:
terminationGracePeriodSeconds: 45
tolerations:
- key: dedicated
operator: Equal
@@ -58,6 +59,11 @@ spec:
- name: worker
image: workolik360/nats-worker:v1.1.0
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
command: ["python3", "-u", "/scripts/worker.py"]
volumeMounts:
- name: worker-script-vol
@@ -91,7 +97,15 @@ spec:
key: api_key
optional: true
- name: EXTERNAL_BASE_URL
value: "http://10.43.229.168"
value: "http://jupiter.nearle"
- name: FIESTA_BASE_URL
value: "http://fiesta.nearle"
# Known Fiesta tenants (R mart=1147, Suriya Store=1135, confirmed
# 2026-07-29). Expand as more are identified - see worker.py's
# FIESTA_TENANT_IDS comment for why this is an explicit list rather
# than a DB heuristic.
- name: FIESTA_TENANT_IDS
value: "1147,1135"
resources:
requests:
memory: "128Mi"
@@ -132,6 +146,7 @@ spec:
prometheus.io/port: "9090"
prometheus.io/path: "/metrics"
spec:
terminationGracePeriodSeconds: 45
tolerations:
- key: dedicated
operator: Equal
@@ -168,6 +183,11 @@ spec:
- name: worker
image: workolik360/nats-worker:v1.1.0
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
command: ["python3", "-u", "/scripts/worker.py"]
volumeMounts:
- name: worker-script-vol
@@ -195,7 +215,7 @@ spec:
name: nats-credentials
key: password
- name: EXTERNAL_BASE_URL
value: "http://10.43.224.63"
value: "http://jupiter.nearle"
resources:
requests:
memory: "128Mi"
@@ -236,6 +256,7 @@ spec:
prometheus.io/port: "9090"
prometheus.io/path: "/metrics"
spec:
terminationGracePeriodSeconds: 45
tolerations:
- key: dedicated
operator: Equal
@@ -272,6 +293,11 @@ spec:
- name: worker
image: workolik360/nats-worker:v1.1.0
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
command: ["python3", "-u", "/scripts/worker.py"]
volumeMounts:
- name: worker-script-vol
@@ -305,7 +331,7 @@ spec:
key: api_key
optional: true
- name: EXTERNAL_BASE_URL
value: "http://10.43.229.168"
value: "http://jupiter.nearle"
resources:
requests:
memory: "128Mi"
@@ -346,6 +372,7 @@ spec:
prometheus.io/port: "9090"
prometheus.io/path: "/metrics"
spec:
terminationGracePeriodSeconds: 45
tolerations:
- key: dedicated
operator: Equal
@@ -382,6 +409,11 @@ spec:
- name: worker
image: workolik360/nats-worker:v1.1.0
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
command: ["python3", "-u", "/scripts/worker.py"]
volumeMounts:
- name: worker-script-vol
@@ -415,7 +447,7 @@ spec:
key: api_key
optional: true
- name: EXTERNAL_BASE_URL
value: "http://10.43.224.63"
value: "http://jupiter.nearle"
resources:
requests:
memory: "128Mi"
@@ -456,6 +488,7 @@ spec:
prometheus.io/port: "9090"
prometheus.io/path: "/metrics"
spec:
terminationGracePeriodSeconds: 45
tolerations:
- key: dedicated
operator: Equal
@@ -492,6 +525,11 @@ spec:
- name: worker
image: workolik360/nats-worker:v1.1.0
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
command: ["python3", "-u", "/scripts/worker.py"]
volumeMounts:
- name: worker-script-vol
@@ -525,7 +563,7 @@ spec:
key: api_key
optional: true
- name: EXTERNAL_BASE_URL
value: "http://10.43.229.168"
value: "http://jupiter.nearle"
resources:
requests:
memory: "128Mi"

View File

@@ -2,26 +2,31 @@ apiVersion: v1
kind: Namespace
metadata:
name: doormile
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'
labels:
name: doormile
---
apiVersion: v1
kind: Namespace
kind: Secret
metadata:
name: doormile
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
kind: StatefulSet
metadata:
name: doormile
namespace: doormile
labels:
app: doormile
app-group: doormile-api
spec:
serviceName: "doormile-service"
replicas: 3
@@ -33,57 +38,68 @@ spec:
metadata:
labels:
app: doormile
app-group: do
app-group: doormile-api
spec:
containers:
- name: doormile
image: doormi
image: doormile/doormile-backend:latest
imagePullPolicy: Always
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
ports:
- containerPort: 8081
env:
- name: ENV
value: "production"
- name: APP_PORT
value: "8081"
- name: DB_HO
- name: DB_HOST
value: "31.97.228.132"
- name: DB_PO
- name: DB_PORT
value: "5433"
- name: DB_NA
- name: DB_NAME
value: "logistics"
- name: DB_US
- name: DB_USER
value: "admin"
- name: DB_PASSWORD
value: "Pac
valueFrom:
secretKeyRef:
name: doormile-secrets
key: DB_PASSWORD
- name: REDIS_HOST
value: "31.97.228.132"
- name: REDIS
- name: REDIS_PORT
value: "6379"
- name: REDIS_USER
value: "adm
- name: REDIS_PASSWORD
value: "Package@321#"
- name: JWT_S
value: "DoormileSuperSecretJWTKey2026!"
- name: NATS_
valueFrom:
secretKeyRef:
name: doormile-secrets
key: REDIS_PASSWORD
- name: NATS_URL
value: "nats://66.116.226.161:4223"
- name: NATS_
value: "doormile"
- name: NATS_USER
valueFrom:
secretKeyRef:
name: doormile-secrets
key: NATS_USER
- name: NATS_PASSWORD
value: "Pac
- name: INTERNAL_API_KEY
value: "doormile-internal-2024"
valueFrom:
secretKeyRef:
name: doormile-secrets
key: NATS_PASSWORD
---
apiVersion: v1
kind: Service
metadata:
name: doormile-service
namespace: doormile
labels:
app: doormile
spec:
type: NodePort
selector:
app-group: doormile-api
ports:
- protocol: TCP
port: 8081
targetPort: 808
nodePort: 30830
port: 8081 # Expose port 8081 internally
targetPort: 8081 # The port the backend application actually listens on
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

@@ -1,514 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: fiesta-gateway-script
namespace: nearle
data:
app.py: |
#!/usr/bin/env python3
"""
FastAPI application with multiple endpoints that publish to NATS JetStream
Each endpoint corresponds to an external API that workers will forward to
"""
import os
import json
import asyncio
from fastapi import FastAPI, HTTPException, Request, Body
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request as StarletteRequest
from starlette.responses import Response as StarletteResponse
from pydantic import BaseModel
import nats
import nats.errors
from prometheus_client import Counter, Histogram, generate_latest, REGISTRY
from starlette.responses import Response
import uvicorn
from typing import Optional, Dict, Any, List, Union
# Prometheus metrics
request_count = Counter('http_requests_total', 'Total HTTP requests', ['method', 'endpoint', 'status'])
request_duration = Histogram('http_request_duration_seconds', 'HTTP request duration', ['method', 'endpoint'])
app = FastAPI(title="NATS Backend API - Multi-Endpoint", version="1.0.0")
# Custom CORS middleware to ensure headers are always added
class CORSHeaderMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: StarletteRequest, call_next):
origin = request.headers.get("origin", "*")
response = await call_next(request)
response.headers["Access-Control-Allow-Origin"] = origin
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, PATCH, DELETE, OPTIONS"
response.headers["Access-Control-Allow-Headers"] = "*"
response.headers["Access-Control-Allow-Credentials"] = "false"
response.headers["Access-Control-Max-Age"] = "600"
return response
# Add custom CORS middleware first
app.add_middleware(CORSHeaderMiddleware)
# Also add FastAPI's CORS middleware as backup
app.add_middleware(
CORSMiddleware,
allow_origin_regex=r".*", # Match all origins
allow_credentials=False,
allow_methods=["*"], # Allow all HTTP methods
allow_headers=["*"], # Allow all headers
expose_headers=["*"], # Expose all headers
max_age=600,
)
# NATS connection (will be initialized on startup)
nc = None
js = None
# Configuration
NATS_URL = os.getenv("NATS_URL", "nats://nats-server:4222")
NATS_USER = os.getenv("NATS_USER", "admin")
NATS_PASSWORD = os.getenv("NATS_PASSWORD", "package@321#")
# Endpoint to NATS subject mapping
ENDPOINT_ROUTES = {
"/live/api/v1/deliveries/createdeliveries": "api.v1.deliveries.createdeliveries",
"/live/api/v1/deliveries/updatedelivery": "api.v1.deliveries.updatedelivery",
"/live/api/v2/partners/createriderlog": "api.v2.partners.createriderlog",
"/live/api/v2/deliveries/createdeliverylog": "api.v2.deliveries.createdeliverylog",
"/live/api/v2/partners/createbreaklog": "api.v2.partners.createbreaklog",
"/live/api/v2/partners/updatebreaklog": "api.v2.partners.updatebreaklog",
"/live/api/v1/mob/orders/createorder": "api.v1.mob.orders.createorder",
"/live/api/v1/web/products/create": "api.v1.web.products.create",
# Customer Endpoints (Sync)
"/live/api/v1/mob/customers/login": "api.v1.mob.customers.login",
"/live/api/v1/mob/customers/create": "api.v1.mob.customers.create",
}
@app.on_event("startup")
async def startup():
"""Initialize NATS connection on startup"""
global nc, js
try:
print(f"Connecting to NATS at {NATS_URL}...")
nc = await nats.connect(
servers=[NATS_URL],
user=NATS_USER,
password=NATS_PASSWORD,
reconnect_time_wait=2,
max_reconnect_attempts=10
)
js = nc.jetstream()
print("✅ Connected to NATS JetStream")
print(f"✅ Configured {len(ENDPOINT_ROUTES)} endpoint routes")
except Exception as e:
print(f"❌ Failed to connect to NATS: {e}")
raise
@app.on_event("shutdown")
async def shutdown():
"""Close NATS connection on shutdown"""
global nc
if nc:
await nc.close()
print("NATS connection closed")
@app.options("/{full_path:path}")
async def options_handler(full_path: str, request: Request):
"""Handle OPTIONS requests for CORS preflight"""
origin = request.headers.get("origin")
return JSONResponse(
status_code=200,
content={},
headers={
"Access-Control-Allow-Origin": origin if origin else "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "*",
"Access-Control-Allow-Credentials": "false",
"Access-Control-Max-Age": "600",
}
)
@app.get("/health")
async def health():
"""Health check endpoint"""
return {"status": "healthy", "nats_connected": nc.is_connected if nc else False}
@app.get("/ready")
async def ready():
"""Readiness check endpoint"""
if nc and nc.is_connected:
return {"status": "ready"}
raise HTTPException(status_code=503, detail="Not ready")
async def publish_to_nats(endpoint: str, data: Union[Dict[str, Any], List[Dict[str, Any]]], request_method: str = "POST"):
"""Publish message to NATS with endpoint metadata"""
payload = {
"endpoint": endpoint,
"method": request_method,
"data": data,
"received_at": int(asyncio.get_event_loop().time() * 1000),
"original_path": endpoint
}
# Get NATS subject for this endpoint
subject = ENDPOINT_ROUTES.get(endpoint, "api.unknown")
if js:
try:
ack = await js.publish(subject, json.dumps(payload).encode())
return ack.seq
except Exception as e:
print(f"❌ Failed to publish to NATS: {e}")
raise HTTPException(status_code=500, detail=f"Failed to publish message: {str(e)}")
else:
raise HTTPException(status_code=503, detail="NATS not connected")
async def publish_request_to_nats(endpoint: str, data: Dict[str, Any], request_method: str = "POST", timeout: int = 10):
"""
Publish to NATS and WAIT for a reply (Request-Reply pattern).
Used for synchronous endpoints like Login.
"""
payload = {
"endpoint": endpoint,
"method": request_method,
"data": data,
"received_at": int(asyncio.get_event_loop().time() * 1000),
"original_path": endpoint
}
subject = ENDPOINT_ROUTES.get(endpoint, "api.unknown")
if not js:
raise HTTPException(status_code=503, detail="NATS by connected")
try:
# Create a unique inbox for the reply
inbox = nc.new_inbox()
# Subscribe to the inbox first
sub = await nc.subscribe(inbox, max_msgs=1)
# Publish request with reply inbox
# Note: We use js.publish to ensure it goes to the Stream (Queue), but attach a reply subject
await js.publish(subject, json.dumps(payload).encode(), reply=inbox)
# Wait for valid response
try:
msg = await sub.next_msg(timeout=timeout)
response_data = json.loads(msg.data.decode())
return response_data
except nats.errors.TimeoutError:
raise HTTPException(status_code=504, detail="Gateway Timeout: Upstream service did not respond in time")
finally:
await sub.unsubscribe()
except HTTPException:
raise
except Exception as e:
print(f"❌ Failed to request from NATS: {e}")
raise HTTPException(status_code=500, detail=f"RPC Error: {str(e)}")
# Endpoint 1: Update Delivery (v1) - External API requires PUT
@app.put("/live/api/v1/deliveries/updatedelivery")
async def update_delivery_v1(data: Dict[str, Any], request: Request):
"""Update Delivery endpoint - forwards to NATS (PUT only, as external API requires PUT)"""
start_time = asyncio.get_event_loop().time()
endpoint = "/live/api/v1/deliveries/updatedelivery"
method = "PUT" # Always use PUT for this endpoint
try:
message_id = await publish_to_nats(endpoint, data, method)
duration = asyncio.get_event_loop().time() - start_time
request_duration.labels(method="PUT", endpoint=endpoint).observe(duration)
request_count.labels(method="PUT", endpoint=endpoint, status="200").inc()
return JSONResponse(
status_code=200,
content={"status": "accepted", "message_id": message_id, "endpoint": endpoint}
)
except HTTPException:
raise
except Exception as e:
request_count.labels(method="PUT", endpoint=endpoint, status="500").inc()
print(f"❌ Error processing {endpoint}: {e}")
raise HTTPException(status_code=500, detail=str(e))
# Endpoint: Create Deliveries (v1)
@app.post("/live/api/v1/deliveries/createdeliveries")
async def create_deliveries_v1(data: Union[Dict[str, Any], List[Dict[str, Any]]], request: Request):
"""Create Deliveries endpoint - forwards payload to NATS"""
start_time = asyncio.get_event_loop().time()
endpoint = "/live/api/v1/deliveries/createdeliveries"
method = request.method
try:
message_id = await publish_to_nats(endpoint, data, method)
duration = asyncio.get_event_loop().time() - start_time
request_duration.labels(method=method, endpoint=endpoint).observe(duration)
request_count.labels(method=method, endpoint=endpoint, status="200").inc()
return JSONResponse(
status_code=200,
content={"status": "accepted", "message_id": message_id, "endpoint": endpoint}
)
except HTTPException:
raise
except Exception as e:
request_count.labels(method=method, endpoint=endpoint, status="500").inc()
print(f"❌ Error processing {endpoint}: {e}")
raise HTTPException(status_code=500, detail=str(e))
# Endpoint 2: Create Rider Log (v2)
@app.post("/live/api/v2/partners/createriderlog")
async def create_rider_log_v2(data: Dict[str, Any], request: Request):
"""Create Rider Log endpoint - forwards to NATS"""
start_time = asyncio.get_event_loop().time()
endpoint = "/live/api/v2/partners/createriderlog"
method = request.method
try:
message_id = await publish_to_nats(endpoint, data, method)
duration = asyncio.get_event_loop().time() - start_time
request_duration.labels(method=method, endpoint=endpoint).observe(duration)
request_count.labels(method=method, endpoint=endpoint, status="200").inc()
return JSONResponse(
status_code=200,
content={"status": "accepted", "message_id": message_id, "endpoint": endpoint}
)
except HTTPException:
raise
except Exception as e:
request_count.labels(method=method, endpoint=endpoint, status="500").inc()
print(f"❌ Error processing {endpoint}: {e}")
raise HTTPException(status_code=500, detail=str(e))
# Endpoint 3: Create Delivery Log (v2)
@app.post("/live/api/v2/deliveries/createdeliverylog")
async def create_delivery_log_v2(
request: Request,
data: List[Dict[str, Any]] = Body(
...,
example=[{
"logid": 0,
"tenantid": 1,
"partnerid": 44,
"locationid": 1,
"orderheaderid": 123456,
"deliveryid": 654321,
"userid": 1111,
"orderid": "1-20231624",
"orderstatus": "active",
"starttime": "2025-12-10 17:51:03",
"logdate": "2025-12-10 18:14:04",
"latitude": "11.0050664",
"longitude": "76.9508776"
}]
)
):
"""
Create Delivery Log endpoint - forwards to NATS.
Accepts either a single dict or a list of dicts to align with external API expectations.
"""
start_time = asyncio.get_event_loop().time()
endpoint = "/live/api/v2/deliveries/createdeliverylog"
method = request.method
try:
# Expect only a list of dicts
if not isinstance(data, list) or not all(isinstance(item, dict) for item in data):
raise HTTPException(status_code=422, detail="Body must be a list of objects")
normalized_data = data
message_id = await publish_to_nats(endpoint, normalized_data, method)
duration = asyncio.get_event_loop().time() - start_time
request_duration.labels(method=method, endpoint=endpoint).observe(duration)
request_count.labels(method=method, endpoint=endpoint, status="200").inc()
return JSONResponse(
status_code=200,
content={"status": "accepted", "message_id": message_id, "endpoint": endpoint}
)
except HTTPException:
raise
except Exception as e:
request_count.labels(method=method, endpoint=endpoint, status="500").inc()
print(f"❌ Error processing {endpoint}: {e}")
raise HTTPException(status_code=500, detail=str(e))
# Endpoint 4: Create Break Rider Log (v2)
@app.post("/live/api/v2/partners/createbreaklog")
async def create_break_log_v2(data: Dict[str, Any], request: Request):
"""Create Break Rider Log endpoint - forwards to NATS"""
start_time = asyncio.get_event_loop().time()
endpoint = "/live/api/v2/partners/createbreaklog"
method = request.method
try:
message_id = await publish_to_nats(endpoint, data, method)
duration = asyncio.get_event_loop().time() - start_time
request_duration.labels(method=method, endpoint=endpoint).observe(duration)
request_count.labels(method=method, endpoint=endpoint, status="200").inc()
return JSONResponse(
status_code=200,
content={"status": "accepted", "message_id": message_id, "endpoint": endpoint}
)
except HTTPException:
raise
except Exception as e:
request_count.labels(method=method, endpoint=endpoint, status="500").inc()
print(f"❌ Error processing {endpoint}: {e}")
raise HTTPException(status_code=500, detail=str(e))
# Endpoint 5: Update Break Rider Log (v2) - Supports both POST and PUT
@app.post("/live/api/v2/partners/updatebreaklog")
@app.put("/live/api/v2/partners/updatebreaklog")
async def update_break_log_v2(data: Dict[str, Any], request: Request):
"""Update Break Rider Log endpoint - forwards to NATS"""
start_time = asyncio.get_event_loop().time()
endpoint = "/live/api/v2/partners/updatebreaklog"
method = request.method # Will be POST or PUT
try:
message_id = await publish_to_nats(endpoint, data, method)
duration = asyncio.get_event_loop().time() - start_time
request_duration.labels(method=method, endpoint=endpoint).observe(duration)
request_count.labels(method=method, endpoint=endpoint, status="200").inc()
return JSONResponse(
status_code=200,
content={"status": "accepted", "message_id": message_id, "endpoint": endpoint}
)
except HTTPException:
raise
except Exception as e:
request_count.labels(method=method, endpoint=endpoint, status="500").inc()
print(f"❌ Error processing {endpoint}: {e}")
raise HTTPException(status_code=500, detail=str(e))
@app.get("/metrics")
async def metrics():
"""Prometheus metrics endpoint"""
return Response(content=generate_latest(REGISTRY), media_type="text/plain")
@app.get("/routes")
async def list_routes():
"""List all configured routes"""
return {
"routes": ENDPOINT_ROUTES,
"total": len(ENDPOINT_ROUTES)
}
# Endpoint 6: Create Order (Mob V1)
@app.post("/live/api/v1/mob/orders/createorder")
async def create_order_mob_v1(data: Dict[str, Any], request: Request):
"""Create Order endpoint (Mobile) - forwards to NATS"""
start_time = asyncio.get_event_loop().time()
endpoint = "/live/api/v1/mob/orders/createorder"
method = request.method
try:
message_id = await publish_to_nats(endpoint, data, method)
duration = asyncio.get_event_loop().time() - start_time
request_duration.labels(method=method, endpoint=endpoint).observe(duration)
request_count.labels(method=method, endpoint=endpoint, status="200").inc()
return JSONResponse(
status_code=200,
content={"status": "accepted", "message_id": message_id, "endpoint": endpoint}
)
except HTTPException:
raise
except Exception as e:
request_count.labels(method=method, endpoint=endpoint, status="500").inc()
print(f"❌ Error processing {endpoint}: {e}")
raise HTTPException(status_code=500, detail=str(e))
# Endpoint 7: Create Product (Web V1)
@app.post("/live/api/v1/web/products/create")
async def create_product_web_v1(data: Dict[str, Any], request: Request):
"""Create Product endpoint (Web) - forwards to NATS"""
start_time = asyncio.get_event_loop().time()
endpoint = "/live/api/v1/web/products/create"
method = request.method
try:
message_id = await publish_to_nats(endpoint, data, method)
duration = asyncio.get_event_loop().time() - start_time
request_duration.labels(method=method, endpoint=endpoint).observe(duration)
request_count.labels(method=method, endpoint=endpoint, status="200").inc()
return JSONResponse(
status_code=200,
content={"status": "accepted", "message_id": message_id, "endpoint": endpoint}
)
except HTTPException:
raise
except Exception as e:
request_count.labels(method=method, endpoint=endpoint, status="500").inc()
print(f"❌ Error processing {endpoint}: {e}")
raise HTTPException(status_code=500, detail=str(e))
# Endpoint 8: Customer Login (Sync Request-Reply)
@app.post("/live/api/v1/mob/customers/login")
async def customer_login(data: Dict[str, Any], request: Request):
"""Customer Login - Waits for response from worker"""
start_time = asyncio.get_event_loop().time()
endpoint = "/live/api/v1/mob/customers/login"
method = request.method
try:
# Wait for reply!
response_data = await publish_request_to_nats(endpoint, data, method)
duration = asyncio.get_event_loop().time() - start_time
request_duration.labels(method=method, endpoint=endpoint).observe(duration)
request_count.labels(method=method, endpoint=endpoint, status="200").inc()
# Return the actual backend response
return JSONResponse(
status_code=200,
content=response_data
)
except HTTPException:
raise
except Exception as e:
request_count.labels(method=method, endpoint=endpoint, status="500").inc()
print(f"❌ Error processing {endpoint}: {e}")
raise HTTPException(status_code=500, detail=str(e))
# Endpoint 9: Customer Create (Sync Request-Reply)
@app.post("/live/api/v1/mob/customers/create")
async def customer_create(data: Dict[str, Any], request: Request):
"""Customer Create - Waits for response from worker"""
start_time = asyncio.get_event_loop().time()
endpoint = "/live/api/v1/mob/customers/create"
method = request.method
try:
# Wait for reply!
response_data = await publish_request_to_nats(endpoint, data, method)
duration = asyncio.get_event_loop().time() - start_time
request_duration.labels(method=method, endpoint=endpoint).observe(duration)
request_count.labels(method=method, endpoint=endpoint, status="200").inc()
return JSONResponse(
status_code=200,
content=response_data
)
except HTTPException:
raise
except Exception as e:
request_count.labels(method=method, endpoint=endpoint, status="500").inc()
print(f"❌ Error processing {endpoint}: {e}")
raise HTTPException(status_code=500, detail=str(e))
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)

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 / {
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 X-Real-IP $remote_addr;
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

@@ -0,0 +1,20 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- nearle-namespace.yaml
- nearle-config.yaml
- nearle-secrets.yaml
- nearle-app-secrets.yaml
- nearle-fiesta.yaml
- nearle-jupiter.yaml
- jupiter-cors-proxy.yaml
- nearle-atlantis.yaml
- nearle-titan.yaml
# nearle-ariane.yaml intentionally excluded - it was never actually
# running on the cluster before Flux tried to deploy it for the first
# time, and it crash-loops. Not a GitOps regression, a pre-existing
# dormant/broken manifest. Revisit separately from the alaska/nearle
# rollout.
- nearle-gateway.yaml
- nearle-reference-grant.yaml

View File

@@ -7,17 +7,14 @@ metadata:
app: fiesta
type: Opaque
stringData:
# The IP of your BigRock Server
DATABASE_HOST: "66.116.207.225"
DB_HOST: "66.116.207.225"
# The user we confirmed works
DATABASE_USERNAME: "admin"
DB_USER: "admin"
# The password we confirmed works
DATABASE_PASSWORD: "Package@123#"
DB_PASSWORD: "Package@123#"
# The rest...
JWT_SECRET_KEY: "nearle"
CATALOGUE_DB_USER: "admin"
CATALOGUE_DB_PASSWORD: "'Package@321#'"
S3_ACCESS_KEY: "DO801G8Q8JAZKF49U3WJ"
S3_SECRET_KEY: "lBQExYfkVqH+ybmGVmQH5MkThBbrIohA/VQLgcPUvug"

View File

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

View File

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

View File

@@ -8,16 +8,21 @@ metadata:
data:
NATS_URL: "nats://66.116.226.161:4222"
LOG_LEVEL: "info"
# The stream is set to ORDERS as per your request
NATS_STREAM: "ORDERS"
# Base subject pattern - the app likely appends the path to this or uses it as a listener filter
NATS_SUBJECT: "api.>"
NATS_SUBJECT: "api.>"
ALLOWED_ORIGINS: "*"
ENV: "production"
DATABASE_NAME: "nearledb"
DB_NAME: "nearledb"
DATABASE_PORT: "5432"
DB_PORT: "5432"
DATABASE_PORT: "5433"
DB_PORT: "5433"
DATABASE_SERVER_HOST: "66.116.207.225"
DB_HOST: "66.116.207.225"
USER_CONTEXT_KEY: "nearle"
CATALOGUE_DB_HOST: "31.97.228.132"
CATALOGUE_DB_PORT: "6054"
CATALOGUE_DB_NAME: "pgvector"
USE_S3: "true"
S3_ENDPOINT: "https://nearle.sgp1.digitaloceanspaces.com"
S3_BUCKET: "nearle"
S3_REGION: "sgp1"

View File

@@ -47,8 +47,13 @@ spec:
app: fiesta
containers:
- name: backend
image: nearlecommerce/fiesta:v1.3.50
image: nearlecommerce/fiesta:v1.3.93
imagePullPolicy: Always
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
ports:
- containerPort: 1122
envFrom:
@@ -69,34 +74,22 @@ spec:
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
- name: MQTT_URL
value: "tcp://66.116.225.226:1883"
- name: MQTT_USER
value: "pos_ingest"
- name: MQTT_PASSWORD
value: "AXbEPrNDWnMLdp7T1tFETwyU"
- name: REDIS_HOST
value: "66.116.226.255"
- name: REDIS_PORT
value: "6379"
- name: REDIS_USER
value: "default"
- name: REDIS_PASSWORD
value: "Package@324969#"
- name: REDIS_DB
value: "0"
---
apiVersion: v1
kind: Service
@@ -113,45 +106,5 @@ spec:
nodePort: 30823
protocol: TCP
name: main
- port: 8000
targetPort: 8000
name: gateway
protocol: TCP
selector:
app: fiesta
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: fiesta-route
namespace: nearle
labels:
app: fiesta
spec:
parentRefs:
- name: gateway
namespace: alaska
hostnames:
- "fiesta.nearle.app"
rules:
- matches:
- path:
type: PathPrefix
value: /live/api/v1/mob/orders/createorder
backendRefs:
- name: fiesta
port: 8000
- matches:
- path:
type: PathPrefix
value: /live/api/v1/web/products/create
backendRefs:
- name: fiesta
port: 8000
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- name: fiesta
port: 80

View File

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

View File

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

View File

@@ -0,0 +1,144 @@
apiVersion: v1
kind: Secret
metadata:
name: nearle-redis-secrets
namespace: nearle
type: Opaque
stringData:
REDIS_HOST: "66.116.226.255"
REDIS_PORT: "6379"
REDIS_USER: "default"
REDIS_PASSWORD: "Package@324969#"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: riderlogs-retention-script
namespace: nearle
data:
trim.py: |
#!/usr/bin/env python3
"""
Trims the riderlogs Redis list down to a rolling retention window.
riderlogs is append-only (RPUSH), so it's roughly chronologically
ordered; we binary-search for the first entry within the retention
window and LTRIM everything before it, rather than scanning the
whole (900K+ entry) list.
"""
import os
import json
import sys
from datetime import datetime, timedelta
import redis
RETENTION_DAYS = int(os.getenv("RETENTION_DAYS", "90"))
KEY = os.getenv("REDIS_KEY", "riderlogs")
r = redis.Redis(
host=os.environ["REDIS_HOST"],
port=int(os.environ.get("REDIS_PORT", "6379")),
username=os.environ.get("REDIS_USER", "default"),
password=os.environ["REDIS_PASSWORD"],
socket_timeout=30,
)
cutoff = datetime.utcnow() - timedelta(days=RETENTION_DAYS)
def get_logdate(idx):
v = r.lindex(KEY, idx)
if v is None:
return None
try:
d = json.loads(v)
ld = d.get("logdate")
if not ld:
return None
return datetime.strptime(ld, "%Y-%m-%d %H:%M:%S")
except Exception:
return None
n = r.llen(KEY)
if n == 0:
print(f"{KEY}: empty, nothing to do")
sys.exit(0)
oldest = get_logdate(0)
if oldest is None or oldest >= cutoff:
print(f"{KEY}: oldest entry ({oldest}) already within the {RETENTION_DAYS}-day window, nothing to trim")
sys.exit(0)
lo, hi = 0, n - 1
while lo < hi:
mid = (lo + hi) // 2
d = get_logdate(mid)
if d is None or d < cutoff:
lo = mid + 1
else:
hi = mid
before = n
r.ltrim(KEY, lo, -1)
after = r.llen(KEY)
print(f"{KEY}: cutoff={cutoff.isoformat()} trimmed {before - after} entries ({before} -> {after})")
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: riderlogs-retention
namespace: nearle
spec:
schedule: "0 3 * * *"
timeZone: "Asia/Kolkata"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
activeDeadlineSeconds: 300
backoffLimit: 1
template:
spec:
restartPolicy: Never
containers:
- name: trim
image: python:3.11-slim
command: ["sh", "-c", "pip install -q redis && python3 /scripts/trim.py"]
env:
- name: RETENTION_DAYS
value: "90"
- name: REDIS_KEY
value: "riderlogs"
- name: REDIS_HOST
valueFrom:
secretKeyRef:
name: nearle-redis-secrets
key: REDIS_HOST
- name: REDIS_PORT
valueFrom:
secretKeyRef:
name: nearle-redis-secrets
key: REDIS_PORT
- name: REDIS_USER
valueFrom:
secretKeyRef:
name: nearle-redis-secrets
key: REDIS_USER
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: nearle-redis-secrets
key: REDIS_PASSWORD
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
volumeMounts:
- name: script
mountPath: /scripts
volumes:
- name: script
configMap:
name: riderlogs-retention-script

View File

@@ -7,9 +7,9 @@ import nats
import os
async def purge_messages():
nats_url = "nats://nats.workolik.com:4222"
nats_user = "admin"
nats_password = "package@321#"
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}...")

View File

@@ -1,77 +1,54 @@
#!/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
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():
"""Configure JetStream with two streams (DELIVERIES, RIDER) and per-subject consumers."""
nats_url = os.getenv("NATS_URL", "nats://nats.workolik.com:4222")
nats_user = os.getenv("NATS_USER", "admin")
nats_password = os.getenv("NATS_PASSWORD", "package@321#")
# Stream definitions
streams = {
"DELIVERIES": {
"subjects": [
"api.v1.deliveries.createdeliveries",
"api.v1.deliveries.updatedelivery",
"api.v2.deliveries.createdeliverylog",
],
},
"RIDER": {
"subjects": [
"api.v2.partners.createriderlog",
"api.v2.partners.createbreaklog",
"api.v2.partners.updatebreaklog",
],
},
"ORDERS": {
"subjects": [
"api.v1.mob.orders.createorder",
],
},
"PRODUCTS": {
"subjects": [
"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 = {
"DELIVERIES": {
"api.v1.deliveries.createdeliveries": "deliveries_createdeliveries",
"api.v1.deliveries.updatedelivery": "deliveries_updatedelivery",
"api.v2.deliveries.createdeliverylog": "deliveries_createdeliverylog",
},
"RIDER": {
"api.v2.partners.createriderlog": "rider_createriderlog",
"api.v2.partners.createbreaklog": "rider_createbreaklog",
"api.v2.partners.updatebreaklog": "rider_updatebreaklog",
},
"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",
},
}
nats_password = os.getenv("NATS_PASSWORD", "")
try:
print(f"Connecting to NATS at {nats_url}...")
@@ -85,17 +62,16 @@ async def setup_jetstream():
js = nc.jetstream()
# 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:
info = await js.stream_info(stream_name)
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(
name=stream_name,
subjects=cfg["subjects"],
subjects=subjects,
storage="memory",
retention=retention_policy,
max_age=24 * 60 * 60,
@@ -106,13 +82,9 @@ async def setup_jetstream():
except Exception as e:
if "not found" in str(e).lower() or "404" in str(e).lower():
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(
name=stream_name,
subjects=cfg["subjects"],
subjects=subjects,
storage="memory",
retention=retention_policy,
max_age=24 * 60 * 60,
@@ -123,10 +95,14 @@ async def setup_jetstream():
else:
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...")
for stream_name, subject_map in consumers.items():
for subject, durable in subject_map.items():
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(
@@ -136,7 +112,7 @@ async def setup_jetstream():
ack_policy="explicit",
deliver_policy="all",
max_deliver=5,
ack_wait=30,
ack_wait=60,
)
print(f"✅ Consumer '{durable}' created")
except Exception as e:
@@ -147,12 +123,12 @@ async def setup_jetstream():
print("\n✅ JetStream setup complete!")
print(" Streams:")
for name, cfg in streams.items():
print(f" - {name}: {', '.join(cfg['subjects'])}")
for domain in WORKER_DOMAINS:
print(f" - {domain['stream']}: {', '.join(domain['subjects'])}")
print(" Consumers:")
for stream_name, subject_map in consumers.items():
for subject, durable in subject_map.items():
print(f" - {durable}: stream={stream_name}, subject={subject}")
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)
@@ -163,4 +139,3 @@ async def setup_jetstream():
if __name__ == "__main__":
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}")
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Update Fiesta Gateway
update_yaml_with_script(
r'e:\nats\kubernetes\manifests\nearle\fiesta-gateway.yaml',
r'e:\nats\kubernetes\conf\app.py',
os.path.join(REPO_ROOT, 'manifests', 'nearle', 'fiesta-gateway.yaml'),
os.path.join(REPO_ROOT, 'conf', 'app.py'),
' app.py: |'
)
# Update Worker Script
update_yaml_with_script(
r'e:\nats\kubernetes\manifests\core\worker-script.yaml',
r'e:\nats\kubernetes\conf\worker.py',
os.path.join(REPO_ROOT, 'manifests', 'core', 'worker-script.yaml'),
os.path.join(REPO_ROOT, 'conf', 'worker.py'),
' worker.py: |'
)

View File

@@ -1,93 +1,63 @@
#!/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 "🔍 KUBERNETES DEPLOYMENT STATUS"
echo "=========================================="
echo ""
echo "📦 1. PODS STATUS (All Namespaces)"
echo "📦 1. NODES"
echo "-----------------------------------"
kubectl get nodes -o wide
echo ""
echo "📦 2. PODS (All Namespaces)"
echo "-----------------------------------"
kubectl get pods -A -o wide
echo ""
echo "📦 2. NATS-BACKEND NAMESPACE - PODS"
echo "-----------------------------------"
kubectl get pods -n nats-backend -o wide
echo ""
for ns in "${NAMESPACES[@]}"; do
echo "=========================================="
echo "📦 Namespace: ${ns}"
echo "=========================================="
echo "🌐 3. SERVICES & LOAD BALANCER"
echo "-----------------------------------"
kubectl get svc -n nats-backend -o wide
echo ""
echo "-- Pods --"
kubectl get pods -n "${ns}" -o wide 2>/dev/null || echo " (namespace not found)"
echo ""
echo "⚖️ 4. LOAD BALANCER DETAILS"
echo "-----------------------------------"
kubectl get svc fastapi-lb -n nats-backend -o yaml | grep -A 10 "spec:"
echo ""
echo "-- Services --"
kubectl get svc -n "${ns}" -o wide 2>/dev/null
echo ""
echo "🚀 5. K3S KUBERNETES LOAD BALANCER PODS (klipper-lb)"
echo "-----------------------------------"
kubectl get pods -n kube-system -l "svccontroller.k3s.cattle.io/svcname=fastapi-lb" -o wide
echo ""
echo "-- StatefulSets / Deployments --"
kubectl get statefulsets,deployments -n "${ns}" -o wide 2>/dev/null
echo ""
echo "📊 6. DEPLOYMENTS & REPLICAS"
echo "-----------------------------------"
kubectl get deployments -n nats-backend -o wide
echo ""
echo "-- HPA / PodDisruptionBudgets --"
kubectl get hpa,pdb -n "${ns}" 2>/dev/null
echo ""
echo "📈 7. HORIZONTAL POD AUTOSCALER (HPA)"
echo "-----------------------------------"
kubectl get hpa -n nats-backend
echo ""
echo "-- Recent Events (last 10) --"
kubectl get events -n "${ns}" --sort-by='.lastTimestamp' 2>/dev/null | tail -10
echo ""
done
echo "🔗 8. ENDPOINTS (Service Backends)"
echo "-----------------------------------"
kubectl get endpoints -n nats-backend
echo ""
echo "🌍 9. INGRESS/GATEWAY STATUS"
echo "🌍 GATEWAY / HTTPROUTE / INGRESS (All Namespaces)"
echo "-----------------------------------"
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"
echo ""
echo "📋 10. RECENT EVENTS (Last 20)"
echo "💾 RESOURCE USAGE (CPU/Memory)"
echo "-----------------------------------"
kubectl get events -n nats-backend --sort-by='.lastTimestamp' | tail -20
echo ""
echo "💾 11. RESOURCE USAGE (CPU/Memory)"
echo "-----------------------------------"
kubectl top pods -n nats-backend 2>/dev/null || echo "Metrics server not available"
kubectl top pods -A 2>/dev/null || echo "Metrics server not available"
kubectl top nodes 2>/dev/null || echo "Metrics server not available"
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 "✅ Status Check Complete!"
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..."
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..."
kubectl apply -f manifests/nearle/nearle-jupiter.yaml
kubectl apply -f manifests/nearle/nearle-titan.yaml
kubectl apply -f manifests/nearle/nearle-fiesta.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 "✅ Nearle stack deployment applied."
echo "📋 Current status:"
echo " kubectl get all -n ${NAMESPACE}"
kubectl get all -n "${NAMESPACE}" || true

View File

@@ -1,10 +1,12 @@
#!/bin/bash
# Kubernetes Deployment Script for 3-Node Cluster
# Usage: ./deploy.sh
# Deploy the full stack to Kubernetes (core, nearle, alaska, doormile, ingress)
# 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
if ! command -v kubectl &> /dev/null; then
@@ -23,46 +25,29 @@ kubectl cluster-info || {
NODE_COUNT=$(kubectl get nodes --no-headers | wc -l)
echo "📊 Cluster has $NODE_COUNT node(s)"
# Apply namespace
echo "📦 Creating namespace..."
kubectl apply -f manifests/namespace.yaml
# 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 "===== Core stack (NATS workers) ====="
bash shfiles/deploy-core-stack.sh
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 "📋 Check status:"
echo " kubectl get pods -n nats-backend -o wide"
echo " kubectl get nodes"
echo " kubectl get hpa -n nats-backend"
echo " bash shfiles/check-k8s-status.sh"
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
# Setup JetStream stream and consumer for NATS
set -euo pipefail
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
if ! command -v python3 &> /dev/null; then
@@ -17,11 +21,19 @@ if ! python3 -c "import nats" 2>/dev/null; then
pip3 install nats-py
fi
# Set environment variables
export NATS_URL="nats://nats.workolik.com:4222"
export NATS_USER="admin"
export NATS_PASSWORD="package@321#"
# Pull live credentials from the cluster's Secret instead of hardcoding them
# here - avoids yet another copy of the password to keep in sync if rotated.
if command -v kubectl &> /dev/null && kubectl get secret nats-credentials -n core &> /dev/null 2>&1; then
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
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
# 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 {
name = "core"
}
}
resource "kubernetes_namespace" "nearle" {
resource "kubernetes_namespace_v1" "nearle" {
metadata {
name = "nearle"
}
}
resource "kubernetes_namespace" "alaska" {
resource "kubernetes_namespace_v1" "alaska" {
metadata {
name = "alaska"
}
}
resource "kubernetes_namespace_v1" "doormile" {
metadata {
name = "doormile"
}
}

View File

@@ -1,45 +1,93 @@
# Manage the Nearle Stack (Jupiter, Atlantis, Fiesta)
# This is the "All-in-one" Terraform control for your major services
# Manage the Nearle Stack (Jupiter, Atlantis, Fiesta) plus the shared core
# 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
resource "kubernetes_manifest" "nearle_jupiter_sts" {
manifest = yamldecode(file("${path.module}/../manifests/nearle/jupiter-sts.yaml"))
locals {
# Splits a multi-document YAML file into a map keyed by
# "<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
}
atlantis_docs = {
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
}
fiesta_docs = {
for doc in [
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
}
core_workers_docs = {
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
}
core_ingress_docs = {
for doc in [
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
}
traefik_middlewares_docs = {
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
}
}
resource "kubernetes_manifest" "nearle_jupiter_svc" {
manifest = yamldecode(file("${path.module}/../manifests/nearle/jupiter-svc.yaml"))
# 1. Jupiter (StatefulSet + Service)
resource "kubernetes_manifest" "nearle_jupiter" {
for_each = local.jupiter_docs
manifest = each.value
}
# 2. Atlantis Service
resource "kubernetes_manifest" "nearle_atlantis_sts" {
manifest = yamldecode(file("${path.module}/../manifests/nearle/atlantis-sts.yaml"))
# 2. Atlantis (StatefulSet + Service + HTTPRoute)
resource "kubernetes_manifest" "nearle_atlantis" {
for_each = local.atlantis_docs
manifest = each.value
}
resource "kubernetes_manifest" "nearle_atlantis_svc" {
manifest = yamldecode(file("${path.module}/../manifests/nearle/atlantis-svc.yaml"))
# 3. Fiesta (StatefulSet + Service + HTTPRoute)
resource "kubernetes_manifest" "nearle_fiesta" {
for_each = local.fiesta_docs
manifest = each.value
}
# 3. Fiesta Service
resource "kubernetes_manifest" "nearle_fiesta_sts" {
manifest = yamldecode(file("${path.module}/../manifests/nearle/fiesta-sts.yaml"))
}
resource "kubernetes_manifest" "nearle_fiesta_svc" {
manifest = yamldecode(file("${path.module}/../manifests/nearle/fiesta-svc.yaml"))
}
# 4. Workers (CPU-heavy isolated nodes)
# 4. Workers (CPU-heavy, isolated node pool - 5 StatefulSets)
resource "kubernetes_manifest" "core_workers" {
# This uses your existing workers.yaml file
# Note: Since this file has MANY documents, I recommend splitting it the same way as above.
manifest = yamldecode(file("${path.module}/../manifests/core/workers.yaml"))
for_each = local.core_workers_docs
manifest = each.value
}
# 5. Ingress (Unified routing that replaces Docker-side Nginx)
# 5. Ingress (queue.workolik.com, jupiter/fiesta/atlantis.nearle.app)
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" {
manifest = yamldecode(file("${path.module}/../manifests/core/traefik-middlewares.yaml"))
for_each = local.traefik_middlewares_docs
manifest = each.value
}