fix(iot): created_at в RFC3339 + перечитывание после Create

- deviceToResponse: created_at теперь RFC3339 (был «2006-01-02 15:04:05 UTC»)
  — единый формат с last_connected, без drift для Terraform.
- CreateIoTDevice: после INSERT перечитываем устройство из БД (GetByName),
  чтобы в 201 отдавался реальный created_at (in-memory структура его не имеет).
This commit is contained in:
Repinoid
2026-10-10 18:48:52 +03:00
parent 76f0230f5d
commit e2bb642df2
+17 -4
View File
@@ -70,7 +70,9 @@ func deviceToResponse(d *store.Device, password string) iotDeviceResponse {
resp.LastConnected = d.LastConnected.UTC().Format(time.RFC3339) resp.LastConnected = d.LastConnected.UTC().Format(time.RFC3339)
} }
if !d.CreatedAt.IsZero() { if !d.CreatedAt.IsZero() {
resp.CreatedAt = d.CreatedAt.UTC().Format("2006-01-02 15:04:05 UTC") // RFC3339 — как last_connected: единый формат для Terraform (drift между
// POST/GET из-за формата «2006-01-02 15:04:05 UTC» устранён).
resp.CreatedAt = d.CreatedAt.UTC().Format(time.RFC3339)
} }
return resp return resp
} }
@@ -149,13 +151,24 @@ func (h *Handler) CreateIoTDevice(w http.ResponseWriter, r *http.Request) {
return return
} }
// Перечитываем устройство из БД: после INSERT нужны БД-поля created_at
// (и phase из БД), которых нет в in-memory структуре. Иначе POST отдал бы
// пустой created_at → drift между POST и GET (ревью 2026-10-10, «Что
// стабилизировать в REST для Terraform»).
created, err := h.Devices.GetByName(r.Context(), ns, req.Name)
if err != nil {
h.Log.Error("reload device after create", "namespace", ns, "name", req.Name, "err", err)
writeJSON(w, http.StatusInternalServerError, errResp("failed to reload device"))
return
}
// Пароль отдаётся ТОЛЬКО здесь, один раз в ответе 201. Дальше (GET/PATCH/ // Пароль отдаётся ТОЛЬКО здесь, один раз в ответе 201. Дальше (GET/PATCH/
// LIST) он не возвращается — иначе пароль утекал бы через чтение и в // LIST) он не возвращается — иначе пароль утекал бы через чтение и в
// историю/логи. Это блокер для Terraform: без пароля устройство нельзя // историю/логи. Это блокер для Terraform: без пароля устройство нельзя
// подключить к MQTT, значит ресурс не создать. Именно поэтому пароль // подключить к MQTT, значит ресурс не создать. Именно поэтому пароль
// пробрасывается в deviceToResponse только в этой точке, а не хранится // пробрасывается в deviceToResponse только в этой точке (GetByName его
// в response-структуре навсегда. // не отдаёт — см. P0-3 ч.2).
writeJSON(w, http.StatusCreated, deviceToResponse(device, password)) writeJSON(w, http.StatusCreated, deviceToResponse(created, password))
} }
// ListIoTDevices — GET /v1/namespaces/{ns}/iot/devices (без паролей). // ListIoTDevices — GET /v1/namespaces/{ns}/iot/devices (без паролей).