feat(iot): Этап 6 — Terraform ресурс sless_iot_device (провайдер v0.1.2)

This commit is contained in:
Naeel
2026-04-04 09:56:18 +03:00
parent 1e53766c46
commit b920dc5c9d
4 changed files with 390 additions and 3 deletions
@@ -522,6 +522,138 @@ func (c *Client) DeleteJob(ctx context.Context, ns, name string) error {
return nil
}
// --- IoTDevice CRUD ---
// Изменено: 2026-04-04
// IoTDeviceRequest — тело POST /v1/namespaces/{ns}/iot/devices
type IoTDeviceRequest struct {
Name string `json:"name"`
DeviceID string `json:"device_id"`
Enabled *bool `json:"enabled"`
Metadata map[string]string `json:"metadata,omitempty"`
}
// IoTDeviceUpdateRequest — тело PATCH /v1/namespaces/{ns}/iot/devices/{name}
type IoTDeviceUpdateRequest struct {
Enabled *bool `json:"enabled"`
}
// IoTDeviceResponse — ответ GET /v1/namespaces/{ns}/iot/devices/{name}
// mqtt_password заполняется только если устройство в фазе Active.
type IoTDeviceResponse struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
DeviceID string `json:"device_id"`
Enabled bool `json:"enabled"`
Phase string `json:"phase"`
MQTTUsername string `json:"mqtt_username"`
MQTTPassword string `json:"mqtt_password"`
SecretName string `json:"secret_name"`
TopicPrefix string `json:"topic_prefix"`
LastConnected string `json:"last_connected,omitempty"`
Message string `json:"message,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
}
// CreateIoTDevice — POST /v1/namespaces/{ns}/iot/devices → 201
func (c *Client) CreateIoTDevice(ctx context.Context, ns string, req IoTDeviceRequest) (*IoTDeviceResponse, error) {
url := fmt.Sprintf("%s/v1/namespaces/%s/iot/devices", c.endpoint, ns)
resp, err := c.doJSON(ctx, http.MethodPost, url, req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("create iot device: status %d: %s", resp.StatusCode, body)
}
var d IoTDeviceResponse
return &d, json.NewDecoder(resp.Body).Decode(&d)
}
// GetIoTDevice — GET /v1/namespaces/{ns}/iot/devices/{name} → nil если 404
// Возвращает mqtt_password из Secret. После создания ждать phase=Active.
func (c *Client) GetIoTDevice(ctx context.Context, ns, name string) (*IoTDeviceResponse, error) {
url := fmt.Sprintf("%s/v1/namespaces/%s/iot/devices/%s", c.endpoint, ns, name)
resp, err := c.doJSON(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, nil
}
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("get iot device: status %d: %s", resp.StatusCode, body)
}
var d IoTDeviceResponse
return &d, json.NewDecoder(resp.Body).Decode(&d)
}
// DeleteIoTDevice — DELETE /v1/namespaces/{ns}/iot/devices/{name} → 204
func (c *Client) DeleteIoTDevice(ctx context.Context, ns, name string) error {
url := fmt.Sprintf("%s/v1/namespaces/%s/iot/devices/%s", c.endpoint, ns, name)
resp, err := c.doJSON(ctx, http.MethodDelete, url, nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusNotFound {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("delete iot device: status %d: %s", resp.StatusCode, body)
}
return nil
}
// UpdateIoTDevice — PATCH /v1/namespaces/{ns}/iot/devices/{name} → 200
// Позволяет изменить enabled без пересоздания устройства.
func (c *Client) UpdateIoTDevice(ctx context.Context, ns, name string, req IoTDeviceUpdateRequest) (*IoTDeviceResponse, error) {
url := fmt.Sprintf("%s/v1/namespaces/%s/iot/devices/%s", c.endpoint, ns, name)
resp, err := c.doJSON(ctx, http.MethodPatch, url, req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("update iot device: status %d: %s", resp.StatusCode, body)
}
var d IoTDeviceResponse
return &d, json.NewDecoder(resp.Body).Decode(&d)
}
// WaitIoTDeviceActive опрашивает устройство каждые 2 секунды пока phase != Active/Error/Disabled.
// Нужен после CreateIoTDevice — контроллер асинхронно создаёт Secret с credentials.
func (c *Client) WaitIoTDeviceActive(ctx context.Context, ns, name string, timeout time.Duration) (*IoTDeviceResponse, error) {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
d, err := c.GetIoTDevice(ctx, ns, name)
if err != nil {
return nil, err
}
if d == nil {
return nil, fmt.Errorf("iot device %s/%s not found while waiting", ns, name)
}
switch d.Phase {
case "Active":
return d, nil
case "Error":
return nil, fmt.Errorf("iot device provisioning failed: %s", d.Message)
case "Disabled":
// enabled=false при создании — credentials не создаются, это ожидаемо
return d, nil
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(2 * time.Second):
}
}
return nil, fmt.Errorf("timeout waiting for iot device %s/%s to become Active", ns, name)
}
// EnsureNamespace — POST /v1/namespaces/{ns}/ensure
// Создаёт k8s namespace пользователя если не существует. Идемпотентен.
// Вызывается ОДИН РАЗ из provider.Configure() до любых ресурсных операций.