Compare commits

...

3 Commits

Author SHA1 Message Date
Suriya
0a8c3b0374 Fix deployment tooling: shell scripts and Terraform
Terraform (validated with the real terraform CLI - was never actually
run against this cluster, no state file existed):
- Delete main.tf: it declared a duplicate kubernetes_namespace.core
  (also in namespaces.tf) and a duplicate provider "kubernetes" block
  (also in providers.tf), both hard errors that would fail
  `terraform plan` immediately.
- Fix workloads.tf references to 6 files deleted in the manifest
  cleanup (jupiter-sts/svc, atlantis-sts/svc, fiesta-sts/svc) - now
  points at the canonical nearle-jupiter/atlantis/fiesta.yaml.
- Fix every kubernetes_manifest resource: they fed multi-document
  YAML (multiple '---'-separated docs per file) straight into
  yamldecode(), which only parses a single document. Rewrote using a
  split-on-'---' + for_each pattern, confirmed safe first by checking
  separator counts exactly match document counts for every affected
  file (no embedded '---' inside any script/config content).
- Add the doormile namespace; rename kubernetes_namespace to
  kubernetes_namespace_v1 (fixes a deprecation warning).
- `terraform validate` now passes clean.

Shell scripts:
- deploy-nearle-stack.sh only applied 4 of the ~13 files in
  manifests/nearle/ - missing the ConfigMap/Secrets fiesta/jupiter/
  titan/ariane need via envFrom, the fiesta gateway script ConfigMap,
  atlantis entirely, and the Gateway/ReferenceGrant/jupiter-cors-proxy
  resources. Now applies every file (verified by diffing the
  directory listing against the script).
- Added deploy-doormile.sh and deploy-ingress.sh - nothing previously
  applied ingress-unified.yaml or traefik-middlewares.yaml at all.
- Rewrote deploy.sh as an orchestrator calling all of the above in
  order (previously referenced a manifests/namespace.yaml layout that
  hasn't existed since before this repo's initial commit).
- Rewrote check-k8s-status.sh to check the real namespaces
  (core/nearle/alaska/doormile/kubernetes-dashboard) instead of a
  'nats-backend' namespace that never existed in this repo.
- Fixed a `cd` bug in setup-jetstream.sh that made it change into
  shfiles/ and then look for scripts/setup_jetstream.py there (a
  child directory that doesn't exist) - it could never have found its
  own target file. Now pulls NATS credentials from the live
  nats-credentials Secret instead of a third hardcoded copy.

Python scripts:
- sync_manifests.py had hardcoded Windows paths (e:\nats\kubernetes\...)
  - replaced with paths relative to the script's own location so it
  actually runs here (or anywhere). Verified by running it.
- setup_jetstream.py created durable consumers under different names
  than worker.py computes at runtime ({NATS_CONSUMER}_{subject}), so
  its max_deliver/ack_wait settings never actually reached the
  consumers workers bind to. Naming now derived with the same logic
  worker.py uses - verified all 10 derived names match workers.yaml
  exactly.
- purge-old-messages.py had hardcoded NATS credentials with no env
  var override at all - fixed to match the pattern used everywhere
  else.
2026-07-18 16:08:15 +05:30
Suriya
91dd240431 Fix worker/gateway logic bugs and duplicate CORS headers
worker.py (both the ConfigMap copy and conf/worker.py):
- Set an explicit ack_wait=60s on the JetStream pull consumer. It was
  previously left at the implicit default (~30s), the same ballpark
  as the outbound HTTP timeout - a slow-but-legitimate external call
  could cause JetStream to redeliver the message to another worker
  while the first was still mid-request, double-processing a
  non-idempotent call (e.g. duplicate order creation).
- Track in-flight tasks and drain them (bounded wait) before closing
  the NATS/HTTP connections on shutdown, instead of cutting them off
  immediately - avoids dropped/duplicated messages on pod restarts.
- Generic exception handler now does nak(delay=5) instead of an
  undelayed nak(), avoiding a tight redelivery loop on a persistent
  bug.
- Missing 'data' field in a message now explicitly drops with a log
  line instead of silently forwarding the entire internal envelope.
- Removed the hardcoded NATS password fallback baked into the source
  (every deployment already supplies it via a Secret at runtime, so
  this was a redundant plaintext copy sitting in a ConfigMap).

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

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

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

View File

@@ -60,7 +60,7 @@ js = None
# Configuration
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

@@ -13,6 +13,14 @@ http {
# 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
@@ -211,7 +221,8 @@ async def main():
sub = await js.pull_subscribe(
subject,
durable=durable_name,
stream=NATS_STREAM
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:

View File

@@ -106,6 +106,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

@@ -330,10 +330,6 @@ spec:
- name: deliveries-service
port: 8000
weight: 100
backendRefs:
- name: deliveries-service
port: 8000
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:
@@ -75,9 +62,6 @@ spec:
args:
- --auto-generate-certificates
- --namespace=kubernetes-dashboard
- --enable-skip-login
- --enable-insecure-login
- --insecure-port=9090
volumeMounts:
- name: kubernetes-dashboard-certs
mountPath: /certs
@@ -135,10 +119,6 @@ spec:
targetPort: 8443
protocol: TCP
name: https
- port: 9090
targetPort: 9090
protocol: TCP
name: http
selector:
k8s-app: kubernetes-dashboard
---
@@ -307,22 +287,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

@@ -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,7 +33,7 @@ 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")
@@ -42,6 +43,10 @@ data:
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 = {
@@ -63,6 +68,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 +93,10 @@ 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"]
# Payload Normalization for logs
if endpoint == "/live/api/v2/deliveries/createdeliverylog":
@@ -118,7 +127,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}")
@@ -173,7 +182,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
@@ -218,7 +228,8 @@ data:
sub = await js.pull_subscribe(
subject,
durable=durable_name,
stream=NATS_STREAM
stream=NATS_STREAM,
config=ConsumerConfig(ack_wait=ACK_WAIT_SECONDS)
)
subs.append(sub)
except Exception as e:
@@ -238,7 +249,9 @@ 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:
@@ -251,6 +264,9 @@ data:
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
@@ -132,6 +138,7 @@ spec:
prometheus.io/port: "9090"
prometheus.io/path: "/metrics"
spec:
terminationGracePeriodSeconds: 45
tolerations:
- key: dedicated
operator: Equal
@@ -168,6 +175,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
@@ -236,6 +248,7 @@ spec:
prometheus.io/port: "9090"
prometheus.io/path: "/metrics"
spec:
terminationGracePeriodSeconds: 45
tolerations:
- key: dedicated
operator: Equal
@@ -272,6 +285,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
@@ -346,6 +364,7 @@ spec:
prometheus.io/port: "9090"
prometheus.io/path: "/metrics"
spec:
terminationGracePeriodSeconds: 45
tolerations:
- key: dedicated
operator: Equal
@@ -382,6 +401,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
@@ -456,6 +480,7 @@ spec:
prometheus.io/port: "9090"
prometheus.io/path: "/metrics"
spec:
terminationGracePeriodSeconds: 45
tolerations:
- key: dedicated
operator: Equal
@@ -492,6 +517,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

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'
apiVersion: v1
kind: Namespace
metadata:
labels:
name: doormile
---
apiVersion: v1
kind: Secret
metadata:
name: doormile-secrets
namespace: doormile
labels:
app: doormile
type: Opaque
stringData:
DB_PASSWORD: "Package@321#"
REDIS_PASSWORD: "Package@321#"
NATS_USER: "doormile"
NATS_PASSWORD: "Package@321#"
---
apiVersion: apps/v1
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

@@ -67,7 +67,7 @@ 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", "")
# Endpoint to NATS subject mapping
ENDPOINT_ROUTES = {
@@ -179,7 +179,7 @@ data:
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

@@ -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

@@ -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

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

View File

@@ -47,8 +47,13 @@ spec:
app: jupiter
containers:
- name: jupiter
image: nearlecommerce/jupiter:v2.7.31
image: nearlecommerce/jupiter:v2.7.55
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

@@ -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
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": [
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",
],
},
"RIDER": {
"subjects": [
]},
{"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",
],
},
"ORDERS": {
"subjects": [
"api.v1.mob.orders.createorder",
],
},
"PRODUCTS": {
"subjects": [
]},
{"stream": "PRODUCTS", "consumer": "products-worker", "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",
},
}
def durable_name(consumer: str, subject: str) -> str:
"""Mirrors worker.py's durable-name derivation exactly."""
suffix = subject.replace(".", "_").replace("*", "all").replace(">", "all")
return f"{consumer}_{suffix}"
async def setup_jetstream():
nats_url = os.getenv("NATS_URL", "nats://nats.workolik.com:4222")
nats_user = os.getenv("NATS_USER", "admin")
nats_password = os.getenv("NATS_PASSWORD", "")
try:
print(f"Connecting to NATS at {nats_url}...")
@@ -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
for ns in "${NAMESPACES[@]}"; do
echo "=========================================="
echo "📦 Namespace: ${ns}"
echo "=========================================="
echo "-- Pods --"
kubectl get pods -n "${ns}" -o wide 2>/dev/null || echo " (namespace not found)"
echo ""
echo "🌐 3. SERVICES & LOAD BALANCER"
echo "-----------------------------------"
kubectl get svc -n nats-backend -o wide
echo "-- Services --"
kubectl get svc -n "${ns}" -o wide 2>/dev/null
echo ""
echo "⚖️ 4. LOAD BALANCER DETAILS"
echo "-----------------------------------"
kubectl get svc fastapi-lb -n nats-backend -o yaml | grep -A 10 "spec:"
echo "-- StatefulSets / Deployments --"
kubectl get statefulsets,deployments -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 "-- HPA / PodDisruptionBudgets --"
kubectl get hpa,pdb -n "${ns}" 2>/dev/null
echo ""
echo "📊 6. DEPLOYMENTS & REPLICAS"
echo "-----------------------------------"
kubectl get deployments -n nats-backend -o wide
echo "-- Recent Events (last 10) --"
kubectl get events -n "${ns}" --sort-by='.lastTimestamp' 2>/dev/null | tail -10
echo ""
done
echo "📈 7. HORIZONTAL POD AUTOSCALER (HPA)"
echo "-----------------------------------"
kubectl get hpa -n nats-backend
echo ""
echo "🔗 8. ENDPOINTS (Service Backends)"
echo "-----------------------------------"
kubectl get endpoints -n nats-backend
echo ""
echo "🌍 9. INGRESS/GATEWAY STATUS"
echo "🌍 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
}
resource "kubernetes_manifest" "nearle_jupiter_svc" {
manifest = yamldecode(file("${path.module}/../manifests/nearle/jupiter-svc.yaml"))
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
}
# 2. Atlantis Service
resource "kubernetes_manifest" "nearle_atlantis_sts" {
manifest = yamldecode(file("${path.module}/../manifests/nearle/atlantis-sts.yaml"))
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
}
resource "kubernetes_manifest" "nearle_atlantis_svc" {
manifest = yamldecode(file("${path.module}/../manifests/nearle/atlantis-svc.yaml"))
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
}
# 3. Fiesta Service
resource "kubernetes_manifest" "nearle_fiesta_sts" {
manifest = yamldecode(file("${path.module}/../manifests/nearle/fiesta-sts.yaml"))
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
}
resource "kubernetes_manifest" "nearle_fiesta_svc" {
manifest = yamldecode(file("${path.module}/../manifests/nearle/fiesta-svc.yaml"))
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
}
}
# 4. Workers (CPU-heavy isolated nodes)
# 1. Jupiter (StatefulSet + Service)
resource "kubernetes_manifest" "nearle_jupiter" {
for_each = local.jupiter_docs
manifest = each.value
}
# 2. Atlantis (StatefulSet + Service + HTTPRoute)
resource "kubernetes_manifest" "nearle_atlantis" {
for_each = local.atlantis_docs
manifest = each.value
}
# 3. Fiesta (StatefulSet + Service + HTTPRoute)
resource "kubernetes_manifest" "nearle_fiesta" {
for_each = local.fiesta_docs
manifest = each.value
}
# 4. Workers (CPU-heavy, isolated node pool - 5 StatefulSets)
resource "kubernetes_manifest" "core_workers" {
# 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
}