From 150146edba84155dda4e5f655c774de2a86832f4 Mon Sep 17 00:00:00 2001 From: Naeel Date: Sun, 12 Apr 2026 16:07:41 +0300 Subject: [PATCH] deploy: EMQX, iot-operator, managed Postgres, Docker Hub image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлен EMQX deployment + WS ingress (адаптирован из sless) - Добавлен iot-operator deployment с RBAC (ServiceAccount, ClusterRole) - Postgres: заменён self-hosted на managed (dc5db45d namespace) - Image: naeel/iot-operator:v0.2.0 (Docker Hub) - Убран imagePullSecrets (Docker Hub публичный) --- cmd/iot-operator/main.go | 230 +++++++++++++------------- deployments/k8s/emqx-ws-ingress.yaml | 68 ++++++++ deployments/k8s/emqx.yaml | 193 +++++++++++++++++++++ deployments/k8s/iot-mqtt-bridge.yaml | 4 +- deployments/k8s/iot-operator.yaml | 141 ++++++++++++++++ deployments/k8s/iot-postgres.yaml | 68 ++------ deployments/k8s/iot-sqs-consumer.yaml | 4 +- internal/api/handler/handler.go | 42 ++--- 8 files changed, 554 insertions(+), 196 deletions(-) create mode 100644 deployments/k8s/emqx-ws-ingress.yaml create mode 100644 deployments/k8s/emqx.yaml create mode 100644 deployments/k8s/iot-operator.yaml diff --git a/cmd/iot-operator/main.go b/cmd/iot-operator/main.go index 6fcf4c2..9705c45 100644 --- a/cmd/iot-operator/main.go +++ b/cmd/iot-operator/main.go @@ -13,131 +13,131 @@ package main import ( -"context" -"fmt" -"log/slog" -"net/http" -"os" -"os/signal" -"syscall" -"time" + "context" + "fmt" + "log/slog" + "net/http" + "os" + "os/signal" + "syscall" + "time" -"k8s.io/apimachinery/pkg/runtime" -utilruntime "k8s.io/apimachinery/pkg/util/runtime" -clientgoscheme "k8s.io/client-go/kubernetes/scheme" -ctrl "sigs.k8s.io/controller-runtime" -"sigs.k8s.io/controller-runtime/pkg/healthz" -"sigs.k8s.io/controller-runtime/pkg/log/zap" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" -iotv1alpha1 "gitea.services.ngcloud.ru/Nail/IoT/api/v1alpha1" -iotcontrollers "gitea.services.ngcloud.ru/Nail/IoT/controllers" -iotapi "gitea.services.ngcloud.ru/Nail/IoT/internal/api" -"gitea.services.ngcloud.ru/Nail/IoT/internal/api/handler" -"gitea.services.ngcloud.ru/Nail/IoT/internal/storage/iotpg" + iotv1alpha1 "gitea.services.ngcloud.ru/Nail/IoT/api/v1alpha1" + iotcontrollers "gitea.services.ngcloud.ru/Nail/IoT/controllers" + iotapi "gitea.services.ngcloud.ru/Nail/IoT/internal/api" + "gitea.services.ngcloud.ru/Nail/IoT/internal/api/handler" + "gitea.services.ngcloud.ru/Nail/IoT/internal/storage/iotpg" ) var scheme = runtime.NewScheme() func init() { -utilruntime.Must(clientgoscheme.AddToScheme(scheme)) -// IoT API group iot.kube5s.ru/v1alpha1 -utilruntime.Must(iotv1alpha1.AddToScheme(scheme)) + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + // IoT API group iot.kube5s.ru/v1alpha1 + utilruntime.Must(iotv1alpha1.AddToScheme(scheme)) } func main() { -log := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) -ctrl.SetLogger(zap.New(zap.UseDevMode(true))) + log := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})) + ctrl.SetLogger(zap.New(zap.UseDevMode(true))) -log.Info("starting IoT managed service operator") + log.Info("starting IoT managed service operator") -// Controller Manager — управляет reconcile loop для IoTDevice CRD -mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ -Scheme: scheme, -HealthProbeBindAddress: ":8081", -LeaderElection: false, -}) -if err != nil { -log.Error("unable to create controller manager", "err", err) -os.Exit(1) -} - -// Регистрируем IoTDevice controller -if err = (&iotcontrollers.IoTDeviceReconciler{ -Client: mgr.GetClient(), -Scheme: mgr.GetScheme(), -}).SetupWithManager(mgr); err != nil { -log.Error("unable to create IoTDevice controller", "err", err) -os.Exit(1) -} - -// Health/ready пробы для k8s -if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { -log.Error("unable to set up health check", "err", err) -os.Exit(1) -} -if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { -log.Error("unable to set up ready check", "err", err) -os.Exit(1) -} - -// IoT Postgres — опционален (если IOT_PG_DSN не задан — телеметрия отключена) -iotPGStore, err := iotpg.NewFromEnv(log) -if err != nil { -log.Error("failed to init IoT Postgres", "err", err) -os.Exit(1) -} -if iotPGStore != nil { -defer iotPGStore.Close() -} - -// REST API handler -h := &handler.Handler{ -K8s: mgr.GetClient(), -Scheme: mgr.GetScheme(), -IoTPG: iotPGStore, -Log: log, -} - -router := iotapi.NewRouter(h, log) - -// HTTP API сервер на порту 9090 -apiServer := &http.Server{ -Addr: ":9090", -Handler: router, -ReadTimeout: 30 * time.Second, -WriteTimeout: 60 * time.Second, -} - -// Запускаем API сервер в горутине -go func() { -log.Info("starting REST API server", "addr", ":9090") -if err := apiServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { -log.Error("API server failed", "err", err) -os.Exit(1) -} -}() - -// Graceful shutdown -ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) -defer cancel() - -// Запускаем controller-manager (блокирует до ctx.Done) -go func() { -log.Info("starting controller manager") -if err := mgr.Start(ctx); err != nil { -log.Error("controller manager failed", "err", err) -os.Exit(1) -} -}() - -<-ctx.Done() -log.Info("shutting down IoT operator") - -shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) -defer shutdownCancel() -if err := apiServer.Shutdown(shutdownCtx); err != nil { -log.Error("API server shutdown error", "err", err) -} - -fmt.Println("IoT operator stopped") + // Controller Manager — управляет reconcile loop для IoTDevice CRD + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + HealthProbeBindAddress: ":8081", + LeaderElection: false, + }) + if err != nil { + log.Error("unable to create controller manager", "err", err) + os.Exit(1) + } + + // Регистрируем IoTDevice controller + if err = (&iotcontrollers.IoTDeviceReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + log.Error("unable to create IoTDevice controller", "err", err) + os.Exit(1) + } + + // Health/ready пробы для k8s + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + log.Error("unable to set up health check", "err", err) + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + log.Error("unable to set up ready check", "err", err) + os.Exit(1) + } + + // IoT Postgres — опционален (если IOT_PG_DSN не задан — телеметрия отключена) + iotPGStore, err := iotpg.NewFromEnv(log) + if err != nil { + log.Error("failed to init IoT Postgres", "err", err) + os.Exit(1) + } + if iotPGStore != nil { + defer iotPGStore.Close() + } + + // REST API handler + h := &handler.Handler{ + K8s: mgr.GetClient(), + Scheme: mgr.GetScheme(), + IoTPG: iotPGStore, + Log: log, + } + + router := iotapi.NewRouter(h, log) + + // HTTP API сервер на порту 9090 + apiServer := &http.Server{ + Addr: ":9090", + Handler: router, + ReadTimeout: 30 * time.Second, + WriteTimeout: 60 * time.Second, + } + + // Запускаем API сервер в горутине + go func() { + log.Info("starting REST API server", "addr", ":9090") + if err := apiServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Error("API server failed", "err", err) + os.Exit(1) + } + }() + + // Graceful shutdown + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) + defer cancel() + + // Запускаем controller-manager (блокирует до ctx.Done) + go func() { + log.Info("starting controller manager") + if err := mgr.Start(ctx); err != nil { + log.Error("controller manager failed", "err", err) + os.Exit(1) + } + }() + + <-ctx.Done() + log.Info("shutting down IoT operator") + + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10*time.Second) + defer shutdownCancel() + if err := apiServer.Shutdown(shutdownCtx); err != nil { + log.Error("API server shutdown error", "err", err) + } + + fmt.Println("IoT operator stopped") } diff --git a/deployments/k8s/emqx-ws-ingress.yaml b/deployments/k8s/emqx-ws-ingress.yaml new file mode 100644 index 0000000..aa49cfe --- /dev/null +++ b/deployments/k8s/emqx-ws-ingress.yaml @@ -0,0 +1,68 @@ +# Создано: 2026-04-12 +# MQTT over WebSocket через Ingress с TLS termination. +# Причина: порт 1883 заблокирован NSX-T Edge firewall на уровне облака. +# Решение: EMQX WebSocket listener (8083) проксируется через nginx-ingress с TLS. +# +# IoT устройство подключается: wss://iot.kube5s.ru/mqtt +# IoT Консоль (UI): https://iot.kube5s.ru/console +# +# DNS A-запись: iot.kube5s.ru → 185.247.187.147 +# TLS: cert-manager + letsencrypt-prod, secret=iot-kube5s-ru-tls +# +# Применение: kubectl apply -f deployments/k8s/emqx-ws-ingress.yaml + +--- +apiVersion: v1 +kind: Service +metadata: + name: emqx-ws + namespace: sless + # Отдельный Service — WebSocket порт для Ingress +spec: + selector: + app: emqx + ports: + - name: mqtt-ws + port: 8083 + targetPort: 8083 + protocol: TCP +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: emqx-mqtt-websocket + namespace: sless + annotations: + kubernetes.io/ingress.class: nginx + cert-manager.io/cluster-issuer: letsencrypt-prod + nginx.ingress.kubernetes.io/ssl-redirect: "true" + # WebSocket: nginx-ingress добавляет Upgrade/Connection при proxy-http-version=1.1 + nginx.ingress.kubernetes.io/proxy-http-version: "1.1" + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" +spec: + ingressClassName: nginx + tls: + - hosts: + - iot.kube5s.ru + secretName: iot-kube5s-ru-tls + rules: + - host: iot.kube5s.ru + http: + paths: + # MQTT over WebSocket — wss://iot.kube5s.ru/mqtt + - path: /mqtt + pathType: Exact + backend: + service: + name: emqx-ws + port: + number: 8083 + # IoT Консоль (UI) — https://iot.kube5s.ru/console + - path: /console + pathType: Exact + backend: + service: + name: iot-operator + port: + number: 9090 diff --git a/deployments/k8s/emqx.yaml b/deployments/k8s/emqx.yaml new file mode 100644 index 0000000..eb9a391 --- /dev/null +++ b/deployments/k8s/emqx.yaml @@ -0,0 +1,193 @@ +# Создано: 2026-04-12 +# EMQX MQTT-брокер для IoT-сервиса (namespace: sless). +# +# Архитектура: +# IoT Device → MQTT CONNECT → EMQX (HTTP auth → iot-operator:9090/internal/mqtt/auth) +# EMQX → MQTT PUBLISH → iot-mqtt-bridge (paho subscriber) → shared-SQS queue iot-telemetry +# SQS → iot-sqs-consumer → Postgres +# +# EMQX 5.x конфиг через emqx.conf (HOCON формат), монтируется как ConfigMap volume. +# +# Порты: +# 1883 — MQTT (plaintext) +# 8083 — MQTT over WebSocket +# 18083 — EMQX Dashboard (admin/public по умолчанию — менять в prod!) +# +# Применение: kubectl apply -f deployments/k8s/emqx.yaml + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: emqx-config + namespace: sless +data: + # emqx.conf — HOCON конфиг для EMQX 5.5.x + # Раздел authentication: HTTP Backend для проверки MQTT credentials IoT-устройств. + # iot-operator ищет Secret iot-{deviceId} и сравнивает пароль. + emqx.conf: | + ## EMQX 5.x configuration (HOCON format) + ## Создано: 2026-04-12 + + ## node — без них EMQX 5.x падает при старте + ## node.cookie — секрет кластерного Erlang-соединения, для single-node любая строка + ## node.data_dir — директория данных (mnesia, конфиги) + node { + name = "emqx@127.0.0.1" + cookie = "iot-emqx-cookie-mvp" + data_dir = "/opt/emqx/data" + } + + ## HTTP Auth Backend для IoT-устройств + ## EMQX посылает POST с {username, password, clientid} → iot-operator отвечает {"result":"allow"|"deny"} + authentication = [ + { + mechanism = password_based + backend = http + enable = true + method = post + url = "http://iot-operator.sless.svc:9090/internal/mqtt/auth" + body { + username = "${username}" + password = "${password}" + clientid = "${clientid}" + } + headers { + "content-type" = "application/json" + } + connect_timeout = 5s + request_timeout = 5s + pool_size = 8 + } + ] + + ## Authorization (ACL) — HTTP backend для изоляции топиков по устройству. + ## no_match = deny: если HTTP backend недоступен или не ответил — запрещаем. + ## Endpoint /internal/mqtt/acl возвращает allow только для топиков {ns}/{deviceId}/# + authorization { + no_match = deny + deny_action = disconnect + cache { + enable = true + max_size = 32 + ttl = 1m + } + sources = [ + { + type = http + enable = true + method = post + url = "http://iot-operator.sless.svc:9090/internal/mqtt/acl" + body { + username = "${username}" + clientid = "${clientid}" + action = "${action}" + topic = "${topic}" + } + headers { + "content-type" = "application/json" + } + connect_timeout = 5s + request_timeout = 5s + pool_size = 8 + } + ] + } + + ## MQTT настройки + mqtt { + max_packet_size = 1MB + max_topic_levels = 10 + retain_available = false + } + + ## Listeners — plaintext MQTT + WebSocket + listeners.tcp.default { + bind = "0.0.0.0:1883" + max_connections = 1024 + } + + listeners.ws.default { + bind = "0.0.0.0:8083" + max_connections = 512 + } + + ## Dashboard + dashboard { + listeners.http { + bind = 18083 + } + } +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: emqx + namespace: sless + labels: + app: emqx +spec: + replicas: 1 + selector: + matchLabels: + app: emqx + template: + metadata: + labels: + app: emqx + spec: + containers: + - name: emqx + image: emqx/emqx:5.5.1 + ports: + - name: mqtt + containerPort: 1883 + - name: ws + containerPort: 8083 + - name: dashboard + containerPort: 18083 + volumeMounts: + - name: emqx-conf + mountPath: /opt/emqx/etc/emqx.conf + subPath: emqx.conf + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + readinessProbe: + tcpSocket: + port: 1883 + initialDelaySeconds: 20 + periodSeconds: 10 + timeoutSeconds: 5 + livenessProbe: + tcpSocket: + port: 1883 + initialDelaySeconds: 40 + periodSeconds: 20 + volumes: + - name: emqx-conf + configMap: + name: emqx-config +--- +apiVersion: v1 +kind: Service +metadata: + name: emqx + namespace: sless +spec: + selector: + app: emqx + ports: + - name: mqtt + port: 1883 + targetPort: 1883 + - name: ws + port: 8083 + targetPort: 8083 + - name: dashboard + port: 18083 + targetPort: 18083 diff --git a/deployments/k8s/iot-mqtt-bridge.yaml b/deployments/k8s/iot-mqtt-bridge.yaml index e0ddf7b..4d1ab67 100644 --- a/deployments/k8s/iot-mqtt-bridge.yaml +++ b/deployments/k8s/iot-mqtt-bridge.yaml @@ -38,7 +38,7 @@ spec: - name: mqtt-bridge # Тот же образ что и оператор — оба бинаря в одном слое (manager + iot-mqtt-bridge). # При смене версии оператора — менять тег и здесь. - image: pearlharbor.registryk8s.services.ngcloud.ru/naeel/sless-operator:v0.1.69 + image: naeel/iot-operator:v0.2.0 imagePullPolicy: Always command: ["/mqtt-bridge"] env: @@ -61,5 +61,3 @@ spec: limits: memory: "64Mi" cpu: "100m" - imagePullSecrets: - - name: sless-registry-auth diff --git a/deployments/k8s/iot-operator.yaml b/deployments/k8s/iot-operator.yaml new file mode 100644 index 0000000..086665b --- /dev/null +++ b/deployments/k8s/iot-operator.yaml @@ -0,0 +1,141 @@ +# Создано: 2026-04-12 +# Deployment iot-operator — controller-manager (IoTDevice CRD) + REST API на :9090. +# +# Компоненты: +# - ServiceAccount + ClusterRole + ClusterRoleBinding (RBAC для CRD controller) +# - Deployment: naeel/iot-operator:v0.2.0 +# - Service: ClusterIP :9090 (REST API, MQTT auth/acl, admin UI) +# +# iot-operator обслуживает: +# - IoTDevice CRD reconcilation (controller-runtime) +# - REST API: устройства, телеметрия, MQTT auth/acl +# - Admin UI: /iot-admin +# - Console UI: /console +# +# Секреты: +# iot-postgres-secret — IOT_PG_DSN для managed Postgres +# iot-sqs-credentials — SQS_ENDPOINT, SQS_ACCESS_KEY, SQS_SECRET_KEY +# +# Применение: kubectl apply -f deployments/k8s/iot-operator.yaml + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: iot-operator + namespace: sless +--- +# ClusterRole — права на IoTDevice CRD + Secrets (для MQTT auth) +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: iot-operator-role +rules: + # IoTDevice CRD + - apiGroups: ["iot.kube5s.ru"] + resources: ["iotdevices", "iotdevices/status", "iotdevices/finalizers"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + # Secrets — для MQTT auth (чтение device credentials) + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + # Events — controller-runtime записывает events + - apiGroups: [""] + resources: ["events"] + verbs: ["create", "patch"] + # Namespaces — для per-tenant DB provisioning + - apiGroups: [""] + resources: ["namespaces"] + verbs: ["get", "list", "watch"] + # Leases — leader election (controller-runtime) + - apiGroups: ["coordination.k8s.io"] + resources: ["leases"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: iot-operator-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: iot-operator-role +subjects: + - kind: ServiceAccount + name: iot-operator + namespace: sless +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: iot-operator + namespace: sless + labels: + app: iot-operator +spec: + replicas: 1 + selector: + matchLabels: + app: iot-operator + template: + metadata: + labels: + app: iot-operator + spec: + serviceAccountName: iot-operator + containers: + - name: operator + image: naeel/iot-operator:v0.2.0 + imagePullPolicy: Always + ports: + - name: api + containerPort: 9090 + - name: metrics + containerPort: 8080 + - name: health + containerPort: 8081 + envFrom: + # IOT_PG_DSN — managed Postgres + - secretRef: + name: iot-postgres-secret + # SQS_ENDPOINT, SQS_ACCESS_KEY, SQS_SECRET_KEY — для admin stats + - secretRef: + name: iot-sqs-credentials + env: + - name: API_PORT + value: "9090" + # ADMIN_STATS_TOKEN — токен доступа к /iot-admin/stats + - name: ADMIN_STATS_TOKEN + value: "iot-admin-2026" + 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: iot-operator + namespace: sless +spec: + selector: + app: iot-operator + ports: + - name: api + port: 9090 + targetPort: 9090 diff --git a/deployments/k8s/iot-postgres.yaml b/deployments/k8s/iot-postgres.yaml index 3f36573..9f3edb9 100644 --- a/deployments/k8s/iot-postgres.yaml +++ b/deployments/k8s/iot-postgres.yaml @@ -1,11 +1,16 @@ # Создано: 2026-04-05 -# Postgres для IoT телеметрии — отдельный от sless postgres (тот для invocations логов). -# Deployment (не StatefulSet) — для dev/demo. В prod заменить на managed Postgres. +# Изменено: 2026-04-12 (заменён self-hosted на managed Postgres через оператор) # -# Суперюзер iot_admin используется оператором для: +# Managed PostgreSQL 17 — тот же инстанс что использует shared-SQS. +# Namespace: dc5db45d-f8b4-4fd0-ad33-ec4dd017f2d5 (managed оператором) +# Host: postgresqlk8s-master.dc5db45d-f8b4-4fd0-ad33-ec4dd017f2d5.svc.cluster.local +# +# IoT оператор использует суперюзер для: # - CREATE USER tenant_{ns} + CREATE DATABASE tenant_{ns} # - CREATE TABLE iot_telemetry в tenant DB # Клиенты НЕ имеют прямого доступа — только через REST API платформы. +# +# Deployment и Service удалены — Postgres managed, только Secret с DSN. --- apiVersion: v1 kind: Secret @@ -13,54 +18,9 @@ metadata: name: iot-postgres-secret namespace: sless stringData: - # Суперпользователь — для управления tenant databases - POSTGRES_USER: "iot_admin" - POSTGRES_PASSWORD: "iot-pg-super-2026" - POSTGRES_DB: "iot_platform" - # DSN для оператора и mqtt-bridge (superuser к management DB) - IOT_PG_DSN: "postgresql://iot_admin:iot-pg-super-2026@iot-postgres.sless.svc:5432/iot_platform?sslmode=disable" ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: iot-postgres - namespace: sless - labels: - app: iot-postgres -spec: - replicas: 1 - selector: - matchLabels: - app: iot-postgres - template: - metadata: - labels: - app: iot-postgres - spec: - containers: - - name: postgres - image: postgres:16-alpine - ports: - - containerPort: 5432 - envFrom: - - secretRef: - name: iot-postgres-secret - resources: - requests: - memory: "128Mi" - cpu: "100m" - limits: - memory: "512Mi" - cpu: "500m" ---- -apiVersion: v1 -kind: Service -metadata: - name: iot-postgres - namespace: sless -spec: - selector: - app: iot-postgres - ports: - - port: 5432 - targetPort: 5432 + # Суперпользователь managed Postgres + POSTGRES_USER: "super" + POSTGRES_PASSWORD: "BQUF5ruECa1ZFlq4wYt3gPJUEmtBMkA9QNK4MM5Sd8al4ArMDlmT16DIKHYBPyif" + POSTGRES_DB: "sqsdb" + # DSN для iot-operator и sqs-consumer (superuser к managed DB) + IOT_PG_DSN: "postgresql://super:BQUF5ruECa1ZFlq4wYt3gPJUEmtBMkA9QNK4MM5Sd8al4ArMDlmT16DIKHYBPyif@postgresqlk8s-master.dc5db45d-f8b4-4fd0-ad33-ec4dd017f2d5.svc.cluster.local:5432/sqsdb?sslmode=disable" diff --git a/deployments/k8s/iot-sqs-consumer.yaml b/deployments/k8s/iot-sqs-consumer.yaml index c7219be..763a44a 100644 --- a/deployments/k8s/iot-sqs-consumer.yaml +++ b/deployments/k8s/iot-sqs-consumer.yaml @@ -36,7 +36,7 @@ spec: containers: - name: sqs-consumer # Тот же образ что и оператор — все IoT бинари в одном образе. - image: pearlharbor.registryk8s.services.ngcloud.ru/naeel/sless-operator:v0.1.69 + image: naeel/iot-operator:v0.2.0 imagePullPolicy: Always command: ["/sqs-consumer"] env: @@ -58,5 +58,3 @@ spec: limits: memory: "64Mi" cpu: "100m" - imagePullSecrets: - - name: sless-registry-auth diff --git a/internal/api/handler/handler.go b/internal/api/handler/handler.go index 1ba31e0..cefe23e 100644 --- a/internal/api/handler/handler.go +++ b/internal/api/handler/handler.go @@ -10,15 +10,15 @@ package handler import ( -"encoding/json" -"log/slog" -"net/http" + "encoding/json" + "log/slog" + "net/http" -"github.com/gorilla/mux" -"k8s.io/apimachinery/pkg/runtime" -"sigs.k8s.io/controller-runtime/pkg/client" + "github.com/gorilla/mux" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" -"gitea.services.ngcloud.ru/Nail/IoT/internal/storage/iotpg" + "gitea.services.ngcloud.ru/Nail/IoT/internal/storage/iotpg" ) // defaultNamespace — fallback namespace для dev/тестов. @@ -26,34 +26,34 @@ const defaultNamespace = "default" // Handler содержит зависимости для всех IoT REST-обработчиков. type Handler struct { -K8s client.Client -Scheme *runtime.Scheme -// IoTPG — хранилище IoT телеметрии (per-tenant Postgres). nil если IOT_PG_DSN не задан. -IoTPG *iotpg.IoTPostgresStore -Log *slog.Logger + K8s client.Client + Scheme *runtime.Scheme + // IoTPG — хранилище IoT телеметрии (per-tenant Postgres). nil если IOT_PG_DSN не задан. + IoTPG *iotpg.IoTPostgresStore + Log *slog.Logger } // writeJSON отправляет JSON-ответ с указанным статусом. func writeJSON(w http.ResponseWriter, status int, v any) { -w.Header().Set("Content-Type", "application/json") -w.WriteHeader(status) -_ = json.NewEncoder(w).Encode(v) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(v) } // errResp возвращает структуру ошибки для JSON. func errResp(msg string) map[string]string { -return map[string]string{"error": msg} + return map[string]string{"error": msg} } // pathVar читает переменную пути из gorilla/mux. func pathVar(r *http.Request, key string) string { -return mux.Vars(r)[key] + return mux.Vars(r)[key] } // namespace читает {namespace} из пути URL. func namespace(r *http.Request) string { -if ns := mux.Vars(r)["namespace"]; ns != "" { -return ns -} -return defaultNamespace + if ns := mux.Vars(r)["namespace"]; ns != "" { + return ns + } + return defaultNamespace }