feat: demo managed functions with harbor-backed operator setup
This commit is contained in:
@@ -0,0 +1,122 @@
|
|||||||
|
# Изменено: 2026-03-14
|
||||||
|
# Node-RED для визуального управления demo сценарием Event Log.
|
||||||
|
# Образ: nodered/node-red:4 (официальный, всегда есть на DockerHub)
|
||||||
|
# UI доступен по http://<ingress-ip>/nodered/
|
||||||
|
# Для AMQP нужен плагин node-red-contrib-amqp — ставится через initContainer.
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: nodered-data
|
||||||
|
namespace: sless
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Gi
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: nodered
|
||||||
|
namespace: sless
|
||||||
|
labels:
|
||||||
|
app: nodered
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: nodered
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: nodered
|
||||||
|
spec:
|
||||||
|
securityContext:
|
||||||
|
fsGroup: 1000
|
||||||
|
# initContainer устанавливает AMQP-плагин в PVC до старта основного контейнера
|
||||||
|
initContainers:
|
||||||
|
- name: install-nodes
|
||||||
|
image: nodered/node-red:latest
|
||||||
|
securityContext:
|
||||||
|
runAsUser: 0
|
||||||
|
runAsGroup: 0
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
chown -R 1000:1000 /data || true
|
||||||
|
cd /data
|
||||||
|
npm install --prefix /data node-red-contrib-amqp node-red-dashboard 2>&1 || true
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /data
|
||||||
|
containers:
|
||||||
|
- name: nodered
|
||||||
|
image: nodered/node-red:latest
|
||||||
|
ports:
|
||||||
|
- containerPort: 1880
|
||||||
|
env:
|
||||||
|
- name: NODE_RED_ENABLE_PROJECTS
|
||||||
|
value: "false"
|
||||||
|
- name: TZ
|
||||||
|
value: "Europe/Moscow"
|
||||||
|
securityContext:
|
||||||
|
runAsUser: 1000
|
||||||
|
runAsGroup: 1000
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /data
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "256Mi"
|
||||||
|
cpu: "100m"
|
||||||
|
limits:
|
||||||
|
memory: "512Mi"
|
||||||
|
cpu: "500m"
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: 1880
|
||||||
|
initialDelaySeconds: 15
|
||||||
|
periodSeconds: 10
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: nodered-data
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: nodered
|
||||||
|
namespace: sless
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: nodered
|
||||||
|
ports:
|
||||||
|
- port: 1880
|
||||||
|
targetPort: 1880
|
||||||
|
---
|
||||||
|
# Ingress на Node-RED — доступен снаружи по http://nodered.185.247.187.147.nip.io
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: nodered
|
||||||
|
namespace: sless
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||||
|
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
rules:
|
||||||
|
- host: nodered.185.247.187.147.nip.io
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: nodered
|
||||||
|
port:
|
||||||
|
number: 1880
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
# Изменено: 2026-03-14
|
||||||
|
# Деплой sless оператора на demo-стенде (naeel-test-3, nip.io, без TLS).
|
||||||
|
# Отличия от production operator.yaml:
|
||||||
|
# - REGISTRY_HOST=naeel (DockerHub, не Harbor)
|
||||||
|
# - INGRESS_HOST и EXTERNAL_URL — через nip.io без TLS
|
||||||
|
# - cert-manager аннотации убраны
|
||||||
|
# - API_TOKEN берётся из sless-operator-secret (совпадает с secrets/test.token)
|
||||||
|
#
|
||||||
|
# Перед apply нужно создать секреты:
|
||||||
|
# kubectl create secret generic sless-operator-secret -n sless \
|
||||||
|
# --from-literal=POSTGRES_DSN="postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable" \
|
||||||
|
# --from-literal=S3_ACCESS_KEY="0GLQRD38H4I6RBDB0EWJ" \
|
||||||
|
# --from-literal=S3_SECRET_KEY="eTFibiHmBd96IApj9PYsboTR6OBoD7osxoarHykw" \
|
||||||
|
# --from-literal=SLESS_API_TOKEN="<token from secrets/test.token>" \
|
||||||
|
# --from-literal=HARBOR_PASS=""
|
||||||
|
#
|
||||||
|
# kubectl create secret docker-registry sless-registry-auth -n sless \
|
||||||
|
# --docker-server=https://index.docker.io/v1/ \
|
||||||
|
# --docker-username=naeel \
|
||||||
|
# --docker-password=<DOCKERHUB_TOKEN>
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: sless-operator-config
|
||||||
|
namespace: sless
|
||||||
|
data:
|
||||||
|
S3_ENDPOINT: "s3.msk-1.ngcloud.ru"
|
||||||
|
S3_BUCKET: "sless-functions"
|
||||||
|
S3_USE_SSL: "true"
|
||||||
|
# Harbor как registry для demo
|
||||||
|
REGISTRY_HOST: "pearlharbor.registryk8s.services.ngcloud.ru"
|
||||||
|
REGISTRY_SECRET: "sless-registry-auth"
|
||||||
|
HARBOR_USER: "admin"
|
||||||
|
API_PORT: "9090"
|
||||||
|
# nip.io домен без TLS — работает без настройки DNS
|
||||||
|
INGRESS_HOST: "fn.185.247.187.147.nip.io"
|
||||||
|
EXTERNAL_URL: "http://sless-api.185.247.187.147.nip.io"
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: sless-operator
|
||||||
|
namespace: sless
|
||||||
|
labels:
|
||||||
|
app: sless-operator
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: sless-operator
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: sless-operator
|
||||||
|
spec:
|
||||||
|
serviceAccountName: sless-operator
|
||||||
|
containers:
|
||||||
|
- name: operator
|
||||||
|
image: naeel/sless-operator:v0.1.29
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- name: api
|
||||||
|
containerPort: 9090
|
||||||
|
- name: metrics
|
||||||
|
containerPort: 8080
|
||||||
|
- name: health
|
||||||
|
containerPort: 8081
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: sless-operator-config
|
||||||
|
- secretRef:
|
||||||
|
name: sless-operator-secret
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: 8081
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /healthz
|
||||||
|
port: 8081
|
||||||
|
initialDelaySeconds: 15
|
||||||
|
periodSeconds: 20
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "64Mi"
|
||||||
|
cpu: "50m"
|
||||||
|
limits:
|
||||||
|
memory: "256Mi"
|
||||||
|
cpu: "500m"
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: sless-operator
|
||||||
|
namespace: sless
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: sless-operator
|
||||||
|
ports:
|
||||||
|
- name: api
|
||||||
|
port: 9090
|
||||||
|
targetPort: 9090
|
||||||
|
---
|
||||||
|
# Ingress без TLS — demo стенд через nip.io
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: sless-operator
|
||||||
|
namespace: sless
|
||||||
|
spec:
|
||||||
|
ingressClassName: nginx
|
||||||
|
rules:
|
||||||
|
- host: sless-api.185.247.187.147.nip.io
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: sless-operator
|
||||||
|
port:
|
||||||
|
number: 9090
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Изменено: 2026-03-14
|
||||||
|
# RabbitMQ для demo сценария Event Log.
|
||||||
|
# Используется официальный образ с management-плагином для веб-UI.
|
||||||
|
# credentials: sless / sless123
|
||||||
|
# AMQP: amqp://sless:sless123@rabbitmq.sless.svc.cluster.local:5672/
|
||||||
|
# Management UI: http://rabbitmq.sless.svc.cluster.local:15672/
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: rabbitmq-secret
|
||||||
|
namespace: sless
|
||||||
|
stringData:
|
||||||
|
RABBITMQ_DEFAULT_USER: "sless"
|
||||||
|
RABBITMQ_DEFAULT_PASS: "sless123"
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: rabbitmq
|
||||||
|
namespace: sless
|
||||||
|
labels:
|
||||||
|
app: rabbitmq
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: rabbitmq
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: rabbitmq
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: rabbitmq
|
||||||
|
image: rabbitmq:3.13-management-alpine
|
||||||
|
ports:
|
||||||
|
- name: amqp
|
||||||
|
containerPort: 5672
|
||||||
|
- name: management
|
||||||
|
containerPort: 15672
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: rabbitmq-secret
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: "256Mi"
|
||||||
|
cpu: "100m"
|
||||||
|
limits:
|
||||||
|
memory: "512Mi"
|
||||||
|
cpu: "500m"
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 5672
|
||||||
|
initialDelaySeconds: 40
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 10
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: rabbitmq
|
||||||
|
namespace: sless
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: rabbitmq
|
||||||
|
ports:
|
||||||
|
- name: amqp
|
||||||
|
port: 5672
|
||||||
|
targetPort: 5672
|
||||||
|
- name: management
|
||||||
|
port: 15672
|
||||||
|
targetPort: 15672
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Изменено: 2026-03-14
|
||||||
|
# Функция event-cleaner: удаляет N самых старых строк из таблицы events.
|
||||||
|
# Вызывается через HTTP POST из Node-RED (который слушает RabbitMQ).
|
||||||
|
# Env: POSTGRES_DSN — строка подключения к PostgreSQL.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import psycopg2
|
||||||
|
|
||||||
|
def handle(request):
|
||||||
|
"""Удаляет N старейших строк из таблицы events."""
|
||||||
|
dsn = os.environ["POSTGRES_DSN"]
|
||||||
|
|
||||||
|
body = {}
|
||||||
|
if request.get_data():
|
||||||
|
try:
|
||||||
|
body = json.loads(request.get_data())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Количество строк для удаления — из тела запроса или дефолт 10
|
||||||
|
delete_n = int(body.get("delete_n", 10))
|
||||||
|
# Защита от случайного удаления слишком большого количества строк
|
||||||
|
delete_n = min(delete_n, 100)
|
||||||
|
|
||||||
|
conn = psycopg2.connect(dsn)
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("""
|
||||||
|
DELETE FROM events
|
||||||
|
WHERE id IN (
|
||||||
|
SELECT id FROM events ORDER BY created_at ASC LIMIT %s
|
||||||
|
)
|
||||||
|
""", (delete_n,))
|
||||||
|
deleted = cur.rowcount
|
||||||
|
cur.execute("SELECT COUNT(*) FROM events")
|
||||||
|
remaining = cur.fetchone()[0]
|
||||||
|
conn.commit()
|
||||||
|
return json.dumps({
|
||||||
|
"ok": True,
|
||||||
|
"deleted": deleted,
|
||||||
|
"remaining": remaining
|
||||||
|
}), 200, {"Content-Type": "application/json"}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Изменено: 2026-03-14
|
||||||
|
# Функция event-monitor: считает строки в events.
|
||||||
|
# Если больше 50 — публикует сообщение в RabbitMQ queue "cleanup-needed".
|
||||||
|
# Запускается по cron (каждую минуту).
|
||||||
|
# Env:
|
||||||
|
# POSTGRES_DSN — строка подключения к PostgreSQL
|
||||||
|
# RABBITMQ_URL — amqp://sless:sless123@rabbitmq.sless.svc.cluster.local:5672/
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import psycopg2
|
||||||
|
import pika
|
||||||
|
|
||||||
|
THRESHOLD = 50
|
||||||
|
|
||||||
|
def handle(request):
|
||||||
|
"""Мониторит таблицу events. При переполнении шлёт в RabbitMQ."""
|
||||||
|
dsn = os.environ["POSTGRES_DSN"]
|
||||||
|
rabbit_url = os.environ["RABBITMQ_URL"]
|
||||||
|
|
||||||
|
conn = psycopg2.connect(dsn)
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
# Считаем количество событий
|
||||||
|
cur.execute("SELECT COUNT(*) FROM events")
|
||||||
|
count = cur.fetchone()[0]
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
result = {"count": count, "threshold": THRESHOLD, "action": "none"}
|
||||||
|
|
||||||
|
if count > THRESHOLD:
|
||||||
|
# Публикуем в очередь — event-cleaner получит и удалит старые строки
|
||||||
|
params = pika.URLParameters(rabbit_url)
|
||||||
|
connection = pika.BlockingConnection(params)
|
||||||
|
channel = connection.channel()
|
||||||
|
channel.queue_declare(queue="cleanup-needed", durable=True)
|
||||||
|
channel.basic_publish(
|
||||||
|
exchange="",
|
||||||
|
routing_key="cleanup-needed",
|
||||||
|
body=json.dumps({"count": count, "delete_n": 10}),
|
||||||
|
properties=pika.BasicProperties(delivery_mode=2) # persistent
|
||||||
|
)
|
||||||
|
connection.close()
|
||||||
|
result["action"] = "cleanup_requested"
|
||||||
|
|
||||||
|
return json.dumps(result), 200, {"Content-Type": "application/json"}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Изменено: 2026-03-14
|
||||||
|
# Функция event-writer: принимает HTTP POST, пишет одну строку в таблицу events.
|
||||||
|
# Таблица создаётся автоматически при первом запуске.
|
||||||
|
# Env: POSTGRES_DSN — строка подключения к PostgreSQL.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import psycopg2
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
def handle(request):
|
||||||
|
"""Записывает одно событие в таблицу events."""
|
||||||
|
dsn = os.environ["POSTGRES_DSN"]
|
||||||
|
|
||||||
|
body = {}
|
||||||
|
if request.get_data():
|
||||||
|
try:
|
||||||
|
body = json.loads(request.get_data())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
source = body.get("source", "node-red")
|
||||||
|
message = body.get("message", "ping")
|
||||||
|
|
||||||
|
conn = psycopg2.connect(dsn)
|
||||||
|
try:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
# Создаём таблицу если нет — безопасно вызывать при каждом запросе
|
||||||
|
cur.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS events (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
source VARCHAR(100) NOT NULL DEFAULT 'unknown',
|
||||||
|
message TEXT NOT NULL DEFAULT '',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO events (source, message) VALUES (%s, %s) RETURNING id, created_at",
|
||||||
|
(source, message)
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
conn.commit()
|
||||||
|
return json.dumps({
|
||||||
|
"ok": True,
|
||||||
|
"id": row[0],
|
||||||
|
"created_at": row[1].isoformat()
|
||||||
|
}), 200, {"Content-Type": "application/json"}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Изменено: 2026-03-14
|
||||||
|
# event-cleaner: HTTP функция для демонстрации контролируемого изменения логики.
|
||||||
|
# Работает без внешних пакетов.
|
||||||
|
|
||||||
|
import json
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
|
||||||
|
def handle(event):
|
||||||
|
"""Принимает delete_n и возвращает подтверждение обработки."""
|
||||||
|
|
||||||
|
payload = event if isinstance(event, dict) else {}
|
||||||
|
|
||||||
|
delete_n = int(payload.get("delete_n", 10))
|
||||||
|
delete_n = max(1, min(delete_n, 100))
|
||||||
|
|
||||||
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
|
# Здесь intentionally имитируем очистку, чтобы показать реакцию на входные параметры.
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"ok": True,
|
||||||
|
"accepted_delete_n": delete_n,
|
||||||
|
"status": "simulated-cleanup",
|
||||||
|
"generated_at": now,
|
||||||
|
}
|
||||||
|
), 200, {"Content-Type": "application/json"}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Изменено: 2026-03-14
|
||||||
|
# event-monitor: cron-функция для демонстрации расписания и управления кодом.
|
||||||
|
# Работает без внешних библиотек и возвращает диагностический JSON.
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
|
||||||
|
THRESHOLD = 50
|
||||||
|
|
||||||
|
|
||||||
|
def handle(request):
|
||||||
|
"""Отдаёт heartbeat для cron-запуска и видимой проверки после apply."""
|
||||||
|
rabbitmq_url = os.environ.get("RABBITMQ_URL", "not-set")
|
||||||
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"ok": True,
|
||||||
|
"monitor": "alive",
|
||||||
|
"threshold": THRESHOLD,
|
||||||
|
"rabbitmq_configured": rabbitmq_url != "not-set",
|
||||||
|
"generated_at": now,
|
||||||
|
}
|
||||||
|
), 200, {"Content-Type": "application/json"}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Изменено: 2026-03-14
|
||||||
|
# event-writer: простая HTTP функция без внешних зависимостей.
|
||||||
|
# Правка поля response_tag в коде сразу видна в ответе после terraform apply.
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
|
||||||
|
def handle(event):
|
||||||
|
"""Возвращает полезный JSON-ответ для визуальной проверки выката кода."""
|
||||||
|
default_message = os.environ.get("DEFAULT_MESSAGE", "writer-default")
|
||||||
|
|
||||||
|
payload = event if isinstance(event, dict) else {}
|
||||||
|
|
||||||
|
message = payload.get("message", default_message)
|
||||||
|
source = payload.get("source", "event-writer")
|
||||||
|
now = datetime.now(timezone.utc).isoformat()
|
||||||
|
|
||||||
|
# response_tag удобно менять для демонстрации hot-update кода через terraform apply.
|
||||||
|
response_tag = "writer-v2"
|
||||||
|
return {
|
||||||
|
"ok": True,
|
||||||
|
"source": source,
|
||||||
|
"message": message,
|
||||||
|
"response_tag": response_tag,
|
||||||
|
"generated_at": now,
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# 2026-03-14
|
||||||
|
# function.tf — ресурсы managed serverless функций для демонстрации.
|
||||||
|
# Пользователь правит код в code/* и запускает terraform apply — провайдер сам пересобирает и выкатывает функции.
|
||||||
|
|
||||||
|
resource "sless_function" "event_writer" {
|
||||||
|
name = "event-writer"
|
||||||
|
runtime = "python3.11"
|
||||||
|
entrypoint = "handler.handle"
|
||||||
|
memory_mb = 128
|
||||||
|
timeout_sec = 20
|
||||||
|
|
||||||
|
source_dir = "${path.module}/code/event-writer"
|
||||||
|
|
||||||
|
env_vars = {
|
||||||
|
POSTGRES_DSN = var.pg_dsn
|
||||||
|
DEFAULT_MESSAGE = var.writer_message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "sless_trigger" "event_writer_http" {
|
||||||
|
name = "event-writer-http"
|
||||||
|
type = "http"
|
||||||
|
function = sless_function.event_writer.name
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "sless_function" "event_monitor" {
|
||||||
|
name = "event-monitor"
|
||||||
|
runtime = "python3.11"
|
||||||
|
entrypoint = "handler.handle"
|
||||||
|
memory_mb = 128
|
||||||
|
timeout_sec = 20
|
||||||
|
|
||||||
|
source_dir = "${path.module}/code/event-monitor"
|
||||||
|
|
||||||
|
env_vars = {
|
||||||
|
POSTGRES_DSN = var.pg_dsn
|
||||||
|
RABBITMQ_URL = var.rabbitmq_url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "sless_trigger" "event_monitor_cron" {
|
||||||
|
name = "event-monitor-cron"
|
||||||
|
type = "cron"
|
||||||
|
function = sless_function.event_monitor.name
|
||||||
|
enabled = true
|
||||||
|
schedule = "*/1 * * * *"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "sless_function" "event_cleaner" {
|
||||||
|
name = "event-cleaner"
|
||||||
|
runtime = "python3.11"
|
||||||
|
entrypoint = "handler.handle"
|
||||||
|
memory_mb = 128
|
||||||
|
timeout_sec = 20
|
||||||
|
|
||||||
|
source_dir = "${path.module}/code/event-cleaner"
|
||||||
|
|
||||||
|
env_vars = {
|
||||||
|
POSTGRES_DSN = var.pg_dsn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "sless_trigger" "event_cleaner_http" {
|
||||||
|
name = "event-cleaner-http"
|
||||||
|
type = "http"
|
||||||
|
function = sless_function.event_cleaner.name
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "event_writer_url" {
|
||||||
|
value = sless_trigger.event_writer_http.url
|
||||||
|
}
|
||||||
|
|
||||||
|
output "event_cleaner_url" {
|
||||||
|
value = sless_trigger.event_cleaner_http.url
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# 2026-03-14
|
||||||
|
# Terraform demo: managed serverless functions.
|
||||||
|
# Здесь управляем ТОЛЬКО функциями/триггерами, внешние сервисы считаем уже поднятыми.
|
||||||
|
|
||||||
|
terraform {
|
||||||
|
required_providers {
|
||||||
|
sless = {
|
||||||
|
source = "terra.k8c.ru/naeel/sless"
|
||||||
|
version = "~> 0.1.18"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "sless" {
|
||||||
|
endpoint = var.sless_endpoint
|
||||||
|
token = var.token
|
||||||
|
nubes_endpoint = var.nubes_endpoint
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# 2026-03-14
|
||||||
|
# Скопируй в terraform.tfvars и подставь актуальный токен.
|
||||||
|
|
||||||
|
token = "PUT_TOKEN_HERE"
|
||||||
|
sless_endpoint = "http://sless-api.185.247.187.147.nip.io"
|
||||||
|
nubes_endpoint = "https://deck-test.ngcloud.ru/api/v1"
|
||||||
|
|
||||||
|
pg_dsn = "postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable"
|
||||||
|
rabbitmq_url = "amqp://sless:sless123@rabbitmq.sless.svc.cluster.local:5672/"
|
||||||
|
|
||||||
|
writer_message = "writer-default-v1"
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# 2026-03-14
|
||||||
|
# Переменные для demo-managed-functions.
|
||||||
|
|
||||||
|
variable "token" {
|
||||||
|
description = "JWT токен API"
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "sless_endpoint" {
|
||||||
|
description = "Endpoint sless API"
|
||||||
|
type = string
|
||||||
|
default = "http://sless-api.185.247.187.147.nip.io"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "nubes_endpoint" {
|
||||||
|
description = "Nubes endpoint (нужен провайдеру)"
|
||||||
|
type = string
|
||||||
|
default = "https://deck-test.ngcloud.ru/api/v1"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "pg_dsn" {
|
||||||
|
description = "PostgreSQL DSN для функций"
|
||||||
|
type = string
|
||||||
|
default = "postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "rabbitmq_url" {
|
||||||
|
description = "RabbitMQ URL для функций"
|
||||||
|
type = string
|
||||||
|
default = "amqp://sless:sless123@rabbitmq.sless.svc.cluster.local:5672/"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "writer_message" {
|
||||||
|
description = "Сообщение по умолчанию, которое пишет event-writer"
|
||||||
|
type = string
|
||||||
|
default = "writer-default-v1"
|
||||||
|
}
|
||||||
@@ -121,8 +121,10 @@ func main() {
|
|||||||
// Builder использует k8s client из manager'а
|
// Builder использует k8s client из manager'а
|
||||||
// harborClient — nil если HARBOR_USER/HARBOR_PASS не заданы (EnsureProject пропускается).
|
// harborClient — nil если HARBOR_USER/HARBOR_PASS не заданы (EnsureProject пропускается).
|
||||||
var harborClient *harbor.Client
|
var harborClient *harbor.Client
|
||||||
|
var harborProjecter builder.Projecter
|
||||||
if cfg.HarborUser != "" && cfg.HarborPass != "" {
|
if cfg.HarborUser != "" && cfg.HarborPass != "" {
|
||||||
harborClient = harbor.New(cfg.RegistryHost, cfg.HarborUser, cfg.HarborPass)
|
harborClient = harbor.New(cfg.RegistryHost, cfg.HarborUser, cfg.HarborPass)
|
||||||
|
harborProjecter = harborClient
|
||||||
log.Info("harbor client configured", "host", cfg.RegistryHost)
|
log.Info("harbor client configured", "host", cfg.RegistryHost)
|
||||||
} else {
|
} else {
|
||||||
log.Info("harbor client disabled (HARBOR_USER/HARBOR_PASS not set)")
|
log.Info("harbor client disabled (HARBOR_USER/HARBOR_PASS not set)")
|
||||||
@@ -137,7 +139,7 @@ func main() {
|
|||||||
S3SecretKey: cfg.S3SecretKey,
|
S3SecretKey: cfg.S3SecretKey,
|
||||||
S3Bucket: cfg.S3Bucket,
|
S3Bucket: cfg.S3Bucket,
|
||||||
Namespace: "sless",
|
Namespace: "sless",
|
||||||
HarborClient: harborClient,
|
HarborClient: harborProjecter,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err = (&controllers.FunctionReconciler{
|
if err = (&controllers.FunctionReconciler{
|
||||||
|
|||||||
Reference in New Issue
Block a user