diff --git a/.gitignore b/.gitignore index 7e3b6f7..43ac389 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,7 @@ event-dispatcher # build artifacts /sless +/iot-mqtt-bridge examples/POSTGRES/stress_log*.txt examples/VM/vm_key examples/VM/vm_key.pub diff --git a/deployments/k8s/iot-mqtt-bridge.yaml b/deployments/k8s/iot-mqtt-bridge.yaml index 6050c98..23c2e5f 100644 --- a/deployments/k8s/iot-mqtt-bridge.yaml +++ b/deployments/k8s/iot-mqtt-bridge.yaml @@ -1,4 +1,5 @@ # Создано: 2026-04-04 +# Изменено: 2026-04-05 (добавлен IOT_PG_DSN, версия v0.1.59) # Deployment iot-mqtt-bridge — MQTT→RabbitMQ мост для IoT. # # Получает MQTT сообщения от EMQX (подписка на "+/telemetry/+") @@ -44,7 +45,7 @@ spec: - name: mqtt-bridge # Тот же образ что и оператор — оба бинаря в одном слое (manager + iot-mqtt-bridge). # При смене версии оператора — менять тег и здесь. - image: pearlharbor.registryk8s.services.ngcloud.ru/naeel/sless-operator:v0.1.53 + image: pearlharbor.registryk8s.services.ngcloud.ru/naeel/sless-operator:v0.1.59 imagePullPolicy: Always command: ["/iot-mqtt-bridge"] env: @@ -59,6 +60,10 @@ spec: envFrom: - secretRef: name: iot-bridge-credentials + # IOT_PG_DSN — сохранение телеметрии в Postgres (опционально) + - secretRef: + name: iot-postgres-secret + optional: true resources: requests: memory: "32Mi" diff --git a/deployments/k8s/iot-postgres.yaml b/deployments/k8s/iot-postgres.yaml new file mode 100644 index 0000000..3f36573 --- /dev/null +++ b/deployments/k8s/iot-postgres.yaml @@ -0,0 +1,66 @@ +# Создано: 2026-04-05 +# Postgres для IoT телеметрии — отдельный от sless postgres (тот для invocations логов). +# Deployment (не StatefulSet) — для dev/demo. В prod заменить на managed Postgres. +# +# Суперюзер iot_admin используется оператором для: +# - CREATE USER tenant_{ns} + CREATE DATABASE tenant_{ns} +# - CREATE TABLE iot_telemetry в tenant DB +# Клиенты НЕ имеют прямого доступа — только через REST API платформы. +--- +apiVersion: v1 +kind: Secret +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 diff --git a/deployments/k8s/operator.yaml b/deployments/k8s/operator.yaml index 16b4026..0156f45 100644 --- a/deployments/k8s/operator.yaml +++ b/deployments/k8s/operator.yaml @@ -1,4 +1,4 @@ -# Изменено: 2026-03-21 +# Изменено: 2026-04-05 (добавлен IOT_PG_DSN, версия v0.1.59) # Деплой sless оператора в кластер. # Состав: # - ConfigMap: не-секретные env vars (S3_ENDPOINT, REGISTRY_HOST и т.д.) @@ -74,8 +74,8 @@ spec: containers: - name: operator # При обновлении версии оператора — менять тег здесь (не latest!) - # v0.1.50 — добавлены IoT controller, IoT REST API, MQTT auth endpoint - image: pearlharbor.registryk8s.services.ngcloud.ru/naeel/sless-operator:v0.1.58 + # v0.1.59 — добавлено сохранение телеметрии в IoT Postgres (per-tenant DB) + image: pearlharbor.registryk8s.services.ngcloud.ru/naeel/sless-operator:v0.1.59 # Always — чтобы всегда тянуть по точному тегу (не кешировать старый) imagePullPolicy: Always ports: @@ -90,6 +90,10 @@ spec: name: sless-operator-config - secretRef: name: sless-operator-secret + # IOT_PG_DSN — опциональный ключ: если не задан, IoT Postgres отключён + - secretRef: + name: iot-postgres-secret + optional: true readinessProbe: httpGet: path: /healthz diff --git a/internal/api/handler/handler.go b/internal/api/handler/handler.go index 6bd3fec..dde0f62 100644 --- a/internal/api/handler/handler.go +++ b/internal/api/handler/handler.go @@ -1,4 +1,4 @@ -// Изменено: 2026-03-11 +// Изменено: 2026-04-05 (добавлено поле IoTPG для IoT телеметрии) // Handler — общий контейнер зависимостей для всех REST handlers. // Все handlers получают доступ к k8s, S3 и Postgres через эту структуру. // Логирование через slog, маршрутизация через gorilla/mux. @@ -28,6 +28,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/storage/iotpg" "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/storage/postgres" "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/storage/s3" ) @@ -43,7 +44,9 @@ type Handler struct { Scheme *runtime.Scheme S3 *s3.Client PG *postgres.Store - Log *slog.Logger + // IoTPG — хранилище IoT телеметрии (per-tenant Postgres). nil если IOT_PG_DSN не задан. + IoTPG *iotpg.IoTPostgresStore + Log *slog.Logger } // writeJSON отправляет JSON-ответ с указанным статусом. diff --git a/internal/api/handler/iot_telemetry_handler.go b/internal/api/handler/iot_telemetry_handler.go new file mode 100644 index 0000000..9c70bf2 --- /dev/null +++ b/internal/api/handler/iot_telemetry_handler.go @@ -0,0 +1,55 @@ +// Создано: 2026-04-05 +// iot_telemetry_handler.go — REST handler для чтения IoT телеметрии. +// +// Endpoint: +// GET /v1/namespaces/{namespace}/iot/telemetry?device={id}&limit={n} +// +// Авторизация: Bearer JWT → namespace validation (как все /v1/ маршруты). +// Данные берутся из per-tenant Postgres DB через IoTPostgresStore. +// Если IoTPG не инициализирован (IOT_PG_DSN не задан) — возвращает 503. + +package handler + +import ( + "net/http" + "strconv" + + "github.com/gorilla/mux" + + "gitea-naeel.giteak8s.services.ngcloud.ru/naeel/sless/internal/storage/iotpg" +) + +// ListIoTTelemetry обрабатывает GET /v1/namespaces/{namespace}/iot/telemetry. +// Параметры: device (опционально), limit (default 50, max 1000). +func (h *Handler) ListIoTTelemetry(w http.ResponseWriter, r *http.Request) { + if h.IoTPG == nil { + writeJSON(w, http.StatusServiceUnavailable, errResp("IoT telemetry storage not configured")) + return + } + + ns := mux.Vars(r)["namespace"] + deviceID := r.URL.Query().Get("device") + limit := 50 + if ls := r.URL.Query().Get("limit"); ls != "" { + if n, err := strconv.Atoi(ls); err == nil && n > 0 { + limit = n + } + } + + rows, err := h.IoTPG.QueryTelemetry(r.Context(), ns, deviceID, limit) + if err != nil { + h.Log.Error("query IoT telemetry", "namespace", ns, "device", deviceID, "err", err) + writeJSON(w, http.StatusInternalServerError, errResp("failed to query telemetry")) + return + } + + // Возвращаем пустой массив вместо null — удобнее для JS + if rows == nil { + rows = []iotpg.TelemetryRow{} + } + + writeJSON(w, http.StatusOK, map[string]any{ + "items": rows, + "count": len(rows), + }) +} diff --git a/internal/api/router.go b/internal/api/router.go index 18ea616..7472b82 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -1,4 +1,4 @@ -// Изменено: 2026-04-04 (tls: CORS origin обновлён с http → https для iot.kube5s.ru) +// Изменено: 2026-04-05 (добавлен route GET /iot/telemetry) // router.go — регистрация всех REST-маршрутов через gorilla/mux. // Все маршруты /v1/ защищены Bearer-токеном (middleware.Auth). // /console — публичный маршрут (статический HTML без auth). @@ -100,6 +100,9 @@ func NewRouter(h *handler.Handler, log *slog.Logger) http.Handler { v1.HandleFunc("/namespaces/{namespace}/iot/devices/{name}", h.DeleteIoTDevice).Methods(http.MethodDelete) v1.HandleFunc("/namespaces/{namespace}/iot/devices/{name}", h.UpdateIoTDevice).Methods(http.MethodPatch) + // IoT Telemetry — чтение сырых данных от устройств (Postgres per-tenant) + v1.HandleFunc("/namespaces/{namespace}/iot/telemetry", h.ListIoTTelemetry).Methods(http.MethodGet) + // MQTT Auth — БЕЗ JWT. Вызывается EMQX при MQTT CONNECT из кластера. // /internal/ недоступен снаружи (Ingress не проксирует /internal/). r.HandleFunc("/internal/mqtt/auth", h.MQTTAuth).Methods(http.MethodPost) diff --git a/internal/api/ui/iot-console.html b/internal/api/ui/iot-console.html index 4a498b0..e7cd9eb 100644 --- a/internal/api/ui/iot-console.html +++ b/internal/api/ui/iot-console.html @@ -267,6 +267,13 @@ const S = { mqttConnecting: false, mqttLog: [], autoTimer: null, + + // Телеметрия + telemetryRows: [], // загруженные строки [{id, device_id, ts, payload}] + telemetryLoading: false, + telemetryAutoRefresh: false, + telemetryRefreshTimer: null, + telemetryDeviceFilter: '', // '' = все устройства }; // Сохраняем auth-параметры в localStorage @@ -838,6 +845,12 @@ function switchTab(tab) { // Останавливаем авто-отправку при уходе с вкладки эмулятора mqttStopAuto(); } + if (tab !== 'telemetry') { + // Останавливаем авто-рефреш при уходе с вкладки телеметрии + clearInterval(S.telemetryRefreshTimer); + S.telemetryRefreshTimer = null; + S.telemetryAutoRefresh = false; + } S.activeTab = tab; // Обновляем active-класс на вкладках @@ -849,6 +862,11 @@ function switchTab(tab) { if (body && S.deviceDetail) { body.innerHTML = tabContent(S.deviceDetail, tab); } + + // При переходе на телеметрию — сразу загружаем данные + if (tab === 'telemetry') { + loadTelemetry(); + } } function tabContent(d, tab) { @@ -989,6 +1007,10 @@ function emulatorTab(d) {
Скоро — хранение данных и API чтения
-- В разработке — появится в следующем обновлении -
+| Время | +Устройство | +Данные | +
|---|