feat(iot): Этапы 2-7 — MQTT auth, IoT API, EMQX, mqtt-bridge, E2E demo

Этап 2+4: internal/api/handler/iot_device_handler.go
  - MQTTAuth: POST /internal/mqtt/auth (без JWT, для EMQX)
  - CreateIoTDevice, ListIoTDevices, GetIoTDevice (c password), DeleteIoTDevice, UpdateIoTDevice
  - crypto/subtle.ConstantTimeCompare против timing attacks

Этап 4: internal/api/router.go
  - /v1/namespaces/{ns}/iot/devices CRUD
  - /internal/mqtt/auth (без JWT middleware)

Этап 3: deployments/k8s/emqx.yaml
  - EMQX 5.5.1, emqx.conf (HOCON) с HTTP auth backend
  - Сервис exposure: 1883 (MQTT), 8083 (WS), 18083 (Dashboard)

Этап 3: iot/cmd/mqtt-bridge/main.go
  - paho.mqtt.golang: подписка на +/telemetry/+
  - amqp091-go: publish в iot.{namespace}.telemetry
  - deployments/k8s/iot-mqtt-bridge.yaml

Этап 7: examples/IOT/ — E2E demo (main.tf, handler.py, README.md)

go.mod: добавлен github.com/eclipse/paho.mqtt.golang v1.5.1
go build ./... — ошибок нет
This commit is contained in:
Naeel
2026-04-04 09:45:23 +03:00
parent 716efafda8
commit 1e53766c46
14 changed files with 1209 additions and 151 deletions
+21
View File
@@ -0,0 +1,21 @@
# Terraform provider plugins
.terraform/
.terraform.lock.hcl
# Terraform state
terraform.tfstate
terraform.tfstate.backup
*.tfstate
*.tfstate.backup
# Sensitive data
terraform.tfvars
!terraform.tfvars.example
# Backup files
*.bak
*.bak_db
*.bak_*
# Test artifacts
test_*.log
-94
View File
@@ -1,94 +0,0 @@
// 2026-04-01 — postgres.tf: Managed PostgreSQL инстанс, пользователь и база данных.
//
// Порядок создания:
// 1. nubes_postgres — сам инстанс PostgreSQL
// 2. nubes_postgres_user — пользователь; пароль автоматически попадает в vault_secrets
// 3. nubes_postgres_database — база данных с owner = созданный пользователь
//
// Важно: vault_secrets["users"] появляется только ПОСЛЕ первого apply (нет пользователя — нет ключа).
// try() в locals страхует от ошибки на первом прогоне.
// ── Locals: credentials из vault ─────────────────────────────────────────────
locals {
# Карта username→{password, username} из vault_secrets, который Nubes заполняет после
# создания пользователя. try() нужен для первого apply, когда ключа ещё нет.
pg_creds_map = try(
jsondecode(lookup(nubes_postgres.pg_test_instance.vault_secrets, "users", "{}")),
{}
)
pg_password = try(local.pg_creds_map[var.pg_username]["password"], "")
# Адрес master-ноды (внутренний — для подключения из кластера).
pg_host = nubes_postgres.pg_test_instance.state_out_flat["internalConnect.master"]
pg_port = 5432
}
// ── Инстанс PostgreSQL ────────────────────────────────────────────────────────
resource "nubes_postgres" "pg_test_instance" {
resource_name = var.pg_resource_name
s3_uid = var.s3_uid
resource_realm = var.realm
# Минимальные ресурсы — достаточно для тестирования.
resource_instances = 1
resource_memory = 512 # MiB
resource_c_p_u = 500 # millicores
resource_disk = "1" # GiB
app_version = "17"
# json_parameters убран — при передаче пустого объекта API возвращает "Invalid JSON String".
# Если нужны кастомные параметры PG — добавить после диагностики.
# Pooler не нужен для тестов — упрощает топологию.
enable_pg_pooler_master = false
enable_pg_pooler_slave = false
allow_no_s_s_l = false
auto_scale = false
auto_scale_percentage = 10
auto_scale_tech_window = 0
auto_scale_quota_gb = "1"
# Внешний адрес не нужен — подключаемся изнутри кластера.
need_external_address_master = false
operation_timeout = "11m"
# Позволяет импортировать уже существующий инстанс с тем же именем, не падая
# с "already exists" — удобно при повторном apply после ручного создания.
adopt_existing_on_create = true
}
// ── Пользователь ──────────────────────────────────────────────────────────────
resource "nubes_postgres_user" "pg_test_user" {
postgres_id = nubes_postgres.pg_test_instance.id
username = var.pg_username
role = var.pg_role
# Не падать если пользователь с таким именем уже существует.
adopt_existing_on_create = true
}
resource "nubes_postgres_user" "pg_test_user3" {
postgres_id = nubes_postgres.pg_test_instance.id
username = "u3"
role = var.pg_role
depends_on = [nubes_postgres_user.pg_test_user]
# Не падать если пользователь с таким именем уже существует.
adopt_existing_on_create = true
}
// ── База данных ───────────────────────────────────────────────────────────────
resource "nubes_postgres_database" "pg_test_db" {
postgres_id = nubes_postgres.pg_test_instance.id
db_name = var.pg_db_name
db_owner = nubes_postgres_user.pg_test_user.username
# Не падать если БД уже существует.
adopt_existing_on_create = true
}
-56
View File
@@ -1,56 +0,0 @@
// 2026-04-01 — postgres_extra.tf: дополнительные пользователи и базы данных для lifecycle-тестов.
//
// ВАЖНО: ресурсы создаются строго последовательно через depends_on.
// Параллельное создание нескольких пользователей в одном инстансе вызывает
// ошибки API ("Секрет не был создан", "key doesn't exist") — race condition на стороне Nubes.
//
// ИЗВЕСТНОЕ ОГРАНИЧЕНИЕ: если apply упал в середине создания пользователя —
// этот пользователь может "зависнуть" в промежуточном состоянии в Nubes API.
// adopt_existing_on_create не спасает. Решение: использовать имена без истории,
// либо ждать очистки на стороне Nubes.
// ── Пользователь 1 ───────────────────────────────────────────────────────────
// Первый в extra-цепочке. Ждёт pg_test_db (postgres.tf).
resource "nubes_postgres_user" "test_extra_user1" {
postgres_id = nubes_postgres.pg_test_instance.id
username = "test_eu1"
role = "ddl_user"
adopt_existing_on_create = true
# Ждём pg_test_db — иначе параллельный старт с созданием БД ломает API.
depends_on = [nubes_postgres_database.pg_test_db]
}
// ── Пользователь 2 ───────────────────────────────────────────────────────────
resource "nubes_postgres_user" "test_extra_user2" {
postgres_id = nubes_postgres.pg_test_instance.id
username = "test_eu2"
role = "ddl_user"
adopt_existing_on_create = true
depends_on = [nubes_postgres_user.test_extra_user1]
}
// ── База данных 1 (owner = test_eu1) ─────────────────────────────────────────
resource "nubes_postgres_database" "test_extra_db1" {
postgres_id = nubes_postgres.pg_test_instance.id
db_name = "test_edb1"
db_owner = nubes_postgres_user.test_extra_user1.username
adopt_existing_on_create = true
depends_on = [nubes_postgres_user.test_extra_user2]
}
// ── База данных 2 (owner = test_eu2) ─────────────────────────────────────────
resource "nubes_postgres_database" "test_extra_db2" {
postgres_id = nubes_postgres.pg_test_instance.id
db_name = "test_edb2"
db_owner = nubes_postgres_user.test_extra_user2.username
adopt_existing_on_create = true
depends_on = [nubes_postgres_database.test_extra_db1]
}