feat(sense): implement Control API v1 [T-011]
This commit is contained in:
@@ -53,8 +53,8 @@ func (s *Postgres) Close() error {
|
||||
func (s *Postgres) verifySchemaAndPrivileges(ctx context.Context) error {
|
||||
var version sql.NullInt64
|
||||
if err := s.db.QueryRowContext(ctx,
|
||||
`SELECT MAX(version) FROM sense.schema_migrations`).Scan(&version); err != nil || !version.Valid || version.Int64 < 3 {
|
||||
return errors.New("postgres sense schema migration v3 is required")
|
||||
`SELECT MAX(version) FROM sense.schema_migrations`).Scan(&version); err != nil || !version.Valid || version.Int64 < 4 {
|
||||
return errors.New("postgres sense schema migration v4 is required")
|
||||
}
|
||||
var canReadQuotaView, canWriteQuotaView, canReadSiteSource, canWriteSiteSource bool
|
||||
var canReadAreaView, canWriteAreaView, canReadAreaSource, canWriteAreaSource bool
|
||||
@@ -77,6 +77,24 @@ func (s *Postgres) verifySchemaAndPrivileges(ctx context.Context) error {
|
||||
!canReadAreaView || canWriteAreaView || canReadAreaSource || canWriteAreaSource {
|
||||
return errors.New("postgres role violates Bell projection privilege boundary")
|
||||
}
|
||||
var canUseReceipts, canUseOperations, canUseOperationItems bool
|
||||
var publicReceipts, publicOperations, publicOperationItems bool
|
||||
if err := s.db.QueryRowContext(ctx, `SELECT
|
||||
has_table_privilege(current_user, 'sense.control_idempotency_receipts', 'SELECT,INSERT,UPDATE,DELETE'),
|
||||
has_table_privilege(current_user, 'sense.batch_operations', 'SELECT,INSERT,UPDATE,DELETE'),
|
||||
has_table_privilege(current_user, 'sense.batch_operation_items', 'SELECT,INSERT,UPDATE,DELETE'),
|
||||
has_table_privilege('public', 'sense.control_idempotency_receipts', 'SELECT,INSERT,UPDATE,DELETE'),
|
||||
has_table_privilege('public', 'sense.batch_operations', 'SELECT,INSERT,UPDATE,DELETE'),
|
||||
has_table_privilege('public', 'sense.batch_operation_items', 'SELECT,INSERT,UPDATE,DELETE')`).Scan(
|
||||
&canUseReceipts, &canUseOperations, &canUseOperationItems,
|
||||
&publicReceipts, &publicOperations, &publicOperationItems,
|
||||
); err != nil {
|
||||
return errors.New("verify postgres Control API state privileges")
|
||||
}
|
||||
if !canUseReceipts || !canUseOperations || !canUseOperationItems ||
|
||||
publicReceipts || publicOperations || publicOperationItems {
|
||||
return errors.New("postgres role violates Control API state privilege boundary")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -87,6 +105,9 @@ func (s *Postgres) CreateDevice(ctx context.Context, value device.Device) error
|
||||
if value.ActualState == "" {
|
||||
value.ActualState = device.ActualPending
|
||||
}
|
||||
if value.ResourceVersion == 0 {
|
||||
value.ResourceVersion = 1
|
||||
}
|
||||
if err := value.Validate(); err != nil {
|
||||
return fmt.Errorf("validate device: %w", err)
|
||||
}
|
||||
@@ -118,14 +139,15 @@ func (s *Postgres) CreateDevice(ctx context.Context, value device.Device) error
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `INSERT INTO sense.devices(
|
||||
id, tenant_id, site_id, area_id, serial_number, name, modality,
|
||||
desired_state, actual_state, endpoint_ref, credential_ref,
|
||||
path_name, generation, quota_source_version, area_policy_source_version,
|
||||
desired_state, actual_state, endpoint_ref, credential_ref, profile_token,
|
||||
path_name, generation, resource_version, quota_source_version, area_policy_source_version,
|
||||
created_at, updated_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)`,
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)`,
|
||||
value.ID, value.TenantID, value.SiteID, value.AreaID, value.SerialNumber, value.Name,
|
||||
value.Modality, value.DesiredState, value.ActualState, value.EndpointRef,
|
||||
value.CredentialRef, value.PathName, value.Generation, nullableVersion(quotaVersion),
|
||||
areaVersion, value.CreatedAt, value.UpdatedAt)
|
||||
value.CredentialRef, value.ProfileToken, value.PathName, value.Generation,
|
||||
value.ResourceVersion, nullableVersion(quotaVersion), areaVersion,
|
||||
value.CreatedAt, value.UpdatedAt)
|
||||
if err != nil {
|
||||
return errors.New("insert postgres device")
|
||||
}
|
||||
@@ -354,6 +376,7 @@ func (s *Postgres) SetDesiredState(ctx context.Context, id string, desired devic
|
||||
var updatedQuotaVersion, updatedAreaVersion sql.NullInt64
|
||||
err = tx.QueryRowContext(ctx, `UPDATE sense.devices SET
|
||||
desired_state = $1, actual_state = 'pending', generation = generation + 1,
|
||||
resource_version = resource_version + 1,
|
||||
quota_source_version = COALESCE($2, quota_source_version),
|
||||
area_policy_source_version = COALESCE($3, area_policy_source_version),
|
||||
updated_at = $4
|
||||
@@ -413,10 +436,10 @@ func (s *Postgres) ListDueReconcile(ctx context.Context, now time.Time, limit in
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT `+postgresDeviceColumns+`, r.failure_count, r.next_attempt_at
|
||||
FROM sense.devices d
|
||||
JOIN sense.reconcile_state r ON r.device_id = d.id
|
||||
WHERE d.desired_state = 'enabled'
|
||||
AND EXISTS (SELECT 1 FROM sense.device_capabilities c
|
||||
WHERE EXISTS (SELECT 1 FROM sense.device_capabilities c
|
||||
WHERE c.device_id = d.id AND c.capability = 'video_capture')
|
||||
AND (r.observed_generation < d.generation OR r.failure_count > 0)
|
||||
AND (r.observed_generation < d.generation
|
||||
OR (d.desired_state = 'enabled' AND r.failure_count > 0))
|
||||
AND (r.next_attempt_at IS NULL OR r.next_attempt_at <= $1)
|
||||
ORDER BY d.updated_at, d.id LIMIT $2`, now, limit)
|
||||
if err != nil {
|
||||
@@ -434,7 +457,8 @@ func (s *Postgres) ListDueReconcile(ctx context.Context, now time.Time, limit in
|
||||
&areaID, &candidate.Device.SerialNumber, &candidate.Device.Name, &candidate.Device.Modality,
|
||||
&candidate.Device.DesiredState, &candidate.Device.ActualState,
|
||||
&candidate.Device.EndpointRef, &candidate.Device.CredentialRef,
|
||||
&candidate.Device.PathName, &candidate.Device.Generation,
|
||||
&candidate.Device.ProfileToken, &candidate.Device.PathName,
|
||||
&candidate.Device.Generation, &candidate.Device.ResourceVersion,
|
||||
"aVersion, &areaVersion,
|
||||
&candidate.Device.CreatedAt, &candidate.Device.UpdatedAt,
|
||||
&candidate.FailureCount, &nextAttempt,
|
||||
@@ -511,7 +535,8 @@ func (s *Postgres) MarkReconciled(ctx context.Context, id string, generation int
|
||||
return ErrNotFound
|
||||
}
|
||||
if _, err := tx.ExecContext(ctx, `UPDATE sense.devices
|
||||
SET actual_state = 'pending', updated_at = $1 WHERE id = $2`, now, id); err != nil {
|
||||
SET actual_state = CASE WHEN desired_state = 'disabled' THEN 'offline' ELSE 'pending' END,
|
||||
updated_at = $1 WHERE id = $2`, now, id); err != nil {
|
||||
return errors.New("mark postgres reconciled device pending")
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
@@ -628,8 +653,8 @@ func (s *Postgres) ConvergenceSnapshot(ctx context.Context) (ConvergenceSnapshot
|
||||
}
|
||||
|
||||
const postgresDeviceColumns = `d.id, d.tenant_id, d.site_id, d.area_id, d.serial_number, d.name, d.modality,
|
||||
d.desired_state, d.actual_state, d.endpoint_ref, d.credential_ref,
|
||||
d.path_name, d.generation, d.quota_source_version, d.area_policy_source_version,
|
||||
d.desired_state, d.actual_state, d.endpoint_ref, d.credential_ref, d.profile_token,
|
||||
d.path_name, d.generation, d.resource_version, d.quota_source_version, d.area_policy_source_version,
|
||||
d.created_at, d.updated_at`
|
||||
|
||||
const postgresDeviceSelect = `SELECT ` + postgresDeviceColumns + ` FROM sense.devices d`
|
||||
@@ -641,8 +666,9 @@ func scanPostgresDevice(row scanner) (device.Device, error) {
|
||||
err := row.Scan(
|
||||
&value.ID, &value.TenantID, &value.SiteID, &areaID, &value.SerialNumber,
|
||||
&value.Name, &value.Modality, &value.DesiredState, &value.ActualState,
|
||||
&value.EndpointRef, &value.CredentialRef, &value.PathName,
|
||||
&value.Generation, "aVersion, &areaVersion, &value.CreatedAt, &value.UpdatedAt,
|
||||
&value.EndpointRef, &value.CredentialRef, &value.ProfileToken, &value.PathName,
|
||||
&value.Generation, &value.ResourceVersion, "aVersion, &areaVersion,
|
||||
&value.CreatedAt, &value.UpdatedAt,
|
||||
)
|
||||
value.AreaID = areaID.String
|
||||
value.QuotaSourceVersion = quotaVersion.Int64
|
||||
|
||||
Reference in New Issue
Block a user