mirror of https://github.com/interlegis/sapl.git
committed by
GitHub
14 changed files with 411 additions and 31 deletions
@ -0,0 +1,12 @@ |
|||
#!/bin/bash |
|||
|
|||
kubectl create namespace sapl |
|||
mkdir -p ./sapl-secret-data |
|||
kubectl -n sapl create secret generic sapl-secretkey --from-file=./sapl-secret-data/ |
|||
kubectl apply -f sapl-k8s.yaml |
|||
|
|||
kubectl rollout status deployment/sapl -n sapl |
|||
|
|||
POD=$(kubectl get pod -n sapl -l app=sapl -o jsonpath='{.items[0].metadata.name}') |
|||
kubectl exec -n sapl "$POD" -- ls -l /var/interlegis/sapl/data |
|||
|
@ -0,0 +1,218 @@ |
|||
apiVersion: v1 |
|||
kind: Namespace |
|||
metadata: |
|||
name: sapl |
|||
--- |
|||
apiVersion: v1 |
|||
kind: Service |
|||
metadata: |
|||
name: sapldb |
|||
namespace: sapl |
|||
spec: |
|||
selector: |
|||
app: sapldb |
|||
ports: |
|||
- name: postgres |
|||
port: 5432 |
|||
targetPort: 5432 |
|||
--- |
|||
apiVersion: v1 |
|||
kind: PersistentVolumeClaim |
|||
metadata: |
|||
name: sapldb-data |
|||
namespace: sapl |
|||
spec: |
|||
accessModes: ["ReadWriteOnce"] |
|||
storageClassName: local-path |
|||
resources: |
|||
requests: |
|||
storage: 5Gi # or 1Gi for solr-configsets |
|||
--- |
|||
apiVersion: apps/v1 |
|||
kind: StatefulSet |
|||
metadata: |
|||
name: sapldb |
|||
namespace: sapl |
|||
spec: |
|||
serviceName: sapldb |
|||
replicas: 1 |
|||
selector: |
|||
matchLabels: |
|||
app: sapldb |
|||
template: |
|||
metadata: |
|||
labels: |
|||
app: sapldb |
|||
spec: |
|||
containers: |
|||
- name: postgres |
|||
image: postgres:10.5-alpine |
|||
env: |
|||
- name: POSTGRES_PASSWORD |
|||
value: "sapl" |
|||
- name: POSTGRES_USER |
|||
value: "sapl" |
|||
- name: POSTGRES_DB |
|||
value: "sapl" |
|||
- name: PGDATA |
|||
value: /var/lib/postgresql/data/ |
|||
- name: TZ |
|||
value: UTC |
|||
- name: PG_TZ |
|||
value: UTC |
|||
ports: |
|||
- containerPort: 5432 |
|||
volumeMounts: |
|||
- name: sapldb-data |
|||
mountPath: /var/lib/postgresql/data/ |
|||
volumes: |
|||
- name: sapldb-data |
|||
persistentVolumeClaim: |
|||
claimName: sapldb-data |
|||
--- |
|||
apiVersion: v1 |
|||
kind: Service |
|||
metadata: |
|||
name: saplsolr |
|||
namespace: sapl |
|||
spec: |
|||
selector: |
|||
app: saplsolr |
|||
ports: |
|||
- name: solr |
|||
port: 8983 |
|||
targetPort: 8983 |
|||
--- |
|||
apiVersion: v1 |
|||
kind: PersistentVolumeClaim |
|||
metadata: |
|||
name: solr-data |
|||
namespace: sapl |
|||
spec: |
|||
accessModes: ["ReadWriteOnce"] |
|||
storageClassName: local-path |
|||
resources: |
|||
requests: |
|||
storage: 5Gi # or 1Gi for solr-configsets |
|||
--- |
|||
apiVersion: v1 |
|||
kind: PersistentVolumeClaim |
|||
metadata: |
|||
name: solr-configsets |
|||
namespace: sapl |
|||
spec: |
|||
accessModes: ["ReadWriteOnce"] |
|||
storageClassName: local-path |
|||
resources: |
|||
requests: |
|||
storage: 5Gi # or 1Gi for solr-configsets |
|||
--- |
|||
apiVersion: apps/v1 |
|||
kind: StatefulSet |
|||
metadata: |
|||
name: saplsolr |
|||
namespace: sapl |
|||
spec: |
|||
serviceName: saplsolr |
|||
replicas: 1 |
|||
selector: |
|||
matchLabels: |
|||
app: saplsolr |
|||
template: |
|||
metadata: |
|||
labels: |
|||
app: saplsolr |
|||
spec: |
|||
containers: |
|||
- name: solr |
|||
image: solr:8.11 |
|||
command: ["bash","-lc","bin/solr start -c -f"] |
|||
ports: |
|||
- containerPort: 8983 |
|||
volumeMounts: |
|||
- name: solr-data |
|||
mountPath: /var/solr |
|||
- name: solr-configsets |
|||
mountPath: /opt/solr/server/solr/configsets |
|||
volumes: |
|||
- name: solr-data |
|||
persistentVolumeClaim: |
|||
claimName: solr-data |
|||
- name: solr-configsets |
|||
persistentVolumeClaim: |
|||
claimName: solr-configsets |
|||
--- |
|||
apiVersion: v1 |
|||
kind: PersistentVolumeClaim |
|||
metadata: |
|||
name: sapl-media |
|||
namespace: sapl |
|||
spec: |
|||
accessModes: ["ReadWriteOnce"] |
|||
storageClassName: local-path |
|||
resources: |
|||
requests: |
|||
storage: 5Gi # or 1Gi for solr-configsets |
|||
--- |
|||
apiVersion: v1 |
|||
kind: Service |
|||
metadata: |
|||
name: sapl |
|||
namespace: sapl |
|||
spec: |
|||
selector: |
|||
app: sapl |
|||
type: NodePort |
|||
ports: |
|||
- name: http |
|||
port: 80 |
|||
targetPort: 80 |
|||
nodePort: 30080 |
|||
--- |
|||
apiVersion: apps/v1 |
|||
kind: Deployment |
|||
metadata: |
|||
name: sapl |
|||
namespace: sapl |
|||
spec: |
|||
replicas: 1 |
|||
selector: |
|||
matchLabels: |
|||
app: sapl |
|||
template: |
|||
metadata: |
|||
labels: |
|||
app: sapl |
|||
spec: |
|||
containers: |
|||
- name: sapl |
|||
image: eribeiro/sapl:debug-k8s-1 |
|||
ports: |
|||
- containerPort: 80 |
|||
volumeMounts: |
|||
- name: data |
|||
mountPath: /var/interlegis/sapl/data |
|||
readOnly: true # secrets are always mounted read-only |
|||
volumes: |
|||
- name: data |
|||
secret: |
|||
secretName: sapl-secretkey |
|||
defaultMode: 0440 # ensures read-only |
|||
env: |
|||
- name: ADMIN_PASSWORD |
|||
value: "interlegis" |
|||
- name: ADMIN_EMAIL |
|||
value: "email@dominio.net" |
|||
- name: DEBUG |
|||
value: "False" |
|||
- name: EMAIL_PORT |
|||
value: "587" |
|||
- name: EMAIL_USE_TLS |
|||
value: "False" |
|||
- name: EMAIL_HOST |
|||
value: "smtp.dominio.net" |
|||
- name: EMAIL_HOST_USER |
|||
value: "usuariosmtp" |
|||
- name: EMAIL_SEND_USER |
|||
|
|||
|
@ -0,0 +1,106 @@ |
|||
import logging |
|||
import traceback |
|||
|
|||
from django.http import HttpResponse, JsonResponse |
|||
from django.utils import timezone |
|||
from rest_framework.views import APIView |
|||
from rest_framework.response import Response |
|||
from rest_framework import status |
|||
from django.conf import settings |
|||
from sapl.health import check_app, check_db, check_cache |
|||
|
|||
COMMON_HEADERS = { |
|||
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0", |
|||
"Pragma": "no-cache", |
|||
} |
|||
|
|||
|
|||
def _format_plain(ok: bool) -> HttpResponse: |
|||
return HttpResponse("OK\n" if ok else "UNHEALTHY\n", |
|||
status=status.HTTP_200_OK if ok else status.HTTP_503_SERVICE_UNAVAILABLE, |
|||
content_type="text/plain") |
|||
|
|||
|
|||
class HealthzView(APIView): |
|||
authentication_classes = [] |
|||
permission_classes = [] |
|||
|
|||
logger = logging.getLogger(__name__) |
|||
|
|||
def get(self, request): |
|||
try: |
|||
ok, msg, ms = check_app() |
|||
payload = { |
|||
"status": "OK" if ok else "UNHEALTHY", |
|||
"checks": {"app": {"ok": ok, "latency_ms": round(ms, 1), "error": msg}}, |
|||
"version": settings.SAPL_VERSION, |
|||
"time": timezone.now().isoformat(), |
|||
} |
|||
if request.query_params.get("fmt") == "txt": |
|||
return _format_plain(ok) |
|||
return Response(payload, |
|||
status=status.HTTP_200_OK if ok else status.HTTP_503_SERVICE_UNAVAILABLE, |
|||
headers=COMMON_HEADERS) |
|||
except Exception as e: |
|||
self.logger.error(traceback.format_exc()) |
|||
return "An internal error has occurred!" |
|||
|
|||
|
|||
|
|||
class ReadyzView(APIView): |
|||
authentication_classes = [] |
|||
permission_classes = [] |
|||
|
|||
logger = logging.getLogger(__name__) |
|||
|
|||
def get(self, request): |
|||
try: |
|||
checks = { |
|||
"app": check_app(), |
|||
"db": check_db(), |
|||
"cache": check_cache(), |
|||
} |
|||
payload_checks = { |
|||
name: {"ok": r[0], "latency_ms": round(r[2], 1), "error": r[1]} |
|||
for name, r in checks.items() |
|||
} |
|||
ok = all(r[0] for r in checks.values()) |
|||
payload = { |
|||
"status": "ok" if ok else "unhealthy", |
|||
"checks": payload_checks, |
|||
"version": settings.SAPL_VERSION, |
|||
"time": timezone.now().isoformat(), |
|||
} |
|||
if request.query_params.get("fmt") == "txt": |
|||
return _format_plain(ok) |
|||
return Response(payload, |
|||
status=status.HTTP_200_OK if ok else status.HTTP_503_SERVICE_UNAVAILABLE, |
|||
headers=COMMON_HEADERS) |
|||
except Exception as e: |
|||
self.logger.error(traceback.format_exc()) |
|||
return "An internal error has occurred!" |
|||
|
|||
|
|||
class AppzVersionView(APIView): |
|||
authentication_classes = [] |
|||
permission_classes = [] |
|||
|
|||
logger = logging.getLogger(__name__) |
|||
|
|||
def get(self, request): |
|||
try: |
|||
payload = { |
|||
'name': 'SAPL', |
|||
'description': 'Sistema de Apoio ao Processo Legislativo', |
|||
'version': settings.SAPL_VERSION, |
|||
} |
|||
if request.query_params.get("fmt") == "txt": |
|||
return HttpResponse(f"{payload['version']} {payload['name']}", |
|||
status=status.HTTP_200_OK, |
|||
content_type="text/plain") |
|||
return Response(payload, |
|||
status=status.HTTP_200_OK, |
|||
headers=COMMON_HEADERS) |
|||
except Exception as e: |
|||
self.logger.error(traceback.format_exc()) |
|||
return "An internal error has occurred!" |
@ -0,0 +1,36 @@ |
|||
# core/health.py |
|||
import logging |
|||
import time |
|||
from typing import Tuple, Optional |
|||
from django.db import connection |
|||
from django.core.cache import cache |
|||
|
|||
logger = logging.getLogger(__name__) |
|||
|
|||
|
|||
def check_app() -> Tuple[bool, Optional[str], float]: |
|||
t0 = time.monotonic() |
|||
return True, None, (time.monotonic() - t0) * 1000 |
|||
|
|||
|
|||
def check_db() -> Tuple[bool, Optional[str], float]: |
|||
t0 = time.monotonic() |
|||
try: |
|||
with connection.cursor() as cur: |
|||
cur.execute("SELECT 1") |
|||
cur.fetchone() |
|||
return True, None, (time.monotonic() - t0) * 1000 |
|||
except Exception as e: |
|||
logging.error(e) |
|||
return False, "An internal error has occurred!", (time.monotonic() - t0) * 1000 |
|||
|
|||
|
|||
def check_cache() -> Tuple[bool, Optional[str], float]: |
|||
t0 = time.monotonic() |
|||
try: |
|||
cache.set("_hc", "1", 5) |
|||
ok = cache.get("_hc") == "1" |
|||
return ok, None if ok else "Cache get/set failed", (time.monotonic() - t0) * 1000 |
|||
except Exception as e: |
|||
logging.error(e) |
|||
return False, "An internal error has occurred!", (time.monotonic() - t0) * 1000 |
@ -0,0 +1,12 @@ |
|||
from prometheus_client import CollectorRegistry, Gauge, generate_latest, CONTENT_TYPE_LATEST |
|||
|
|||
|
|||
def health_registry(check_results: dict) -> bytes: |
|||
""" |
|||
check_results: {"app": True/False, "db": True/False, ...} |
|||
""" |
|||
reg = CollectorRegistry() |
|||
g = Gauge("app_health", "1 if healthy, 0 if unhealthy", ["component"], registry=reg) |
|||
for comp, ok in check_results.items(): |
|||
g.labels(component=comp).set(1 if ok else 0) |
|||
return generate_latest(reg) |
Loading…
Reference in new issue