feat(iot): Этап 6 — Terraform ресурс sless_iot_device (провайдер v0.1.2)
This commit is contained in:
@@ -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() до любых ресурсных операций.
|
||||
|
||||
Reference in New Issue
Block a user