Files
story/internal/saas/ops_firmware_service.go
T
Codex Local 0c2151ace9
ci / verify (push) Canceled after 0s
chore: checkpoint 第一版
2026-09-06 17:04:11 +08:00

572 lines
30 KiB
Go

package saas
import (
"context"
"crypto/ed25519"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"time"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/database"
"data-terminal-cloud/internal/identity"
"github.com/jackc/pgx/v5"
)
type opsFirmwareAccess struct {
tenantID, tenantPublicID, userID, userPublicID, role string
}
type FirmwareDownload struct {
ReleaseID string `json:"release_id"`
Version string `json:"version"`
ExpectedSize int64 `json:"size"`
SHA256 string `json:"sha256"`
Signature string `json:"signature"`
SigningKeyID string `json:"signing_key_id"`
DownloadURL string `json:"download_url"`
DownloadUntil time.Time `json:"expires_at"`
}
func resolveOpsFirmwareAccess(ctx context.Context, q queryRow, principal identity.Principal, tenantPublicID string, mutate bool) (opsFirmwareAccess, error) {
var access opsFirmwareAccess
err := q.QueryRow(ctx, `SELECT t.id::text,t.public_id::text,ua.id::text,ua.public_id::text,ua.platform_role
FROM tenant t JOIN user_account ua ON ua.tenant_id=t.id AND ua.status='active' AND ua.account_kind='operator'
WHERE t.public_id=$1::uuid AND t.status='active' AND ua.oidc_issuer=$2 AND ua.oidc_subject=$3`, tenantPublicID, principal.Issuer, principal.Subject).
Scan(&access.tenantID, &access.tenantPublicID, &access.userID, &access.userPublicID, &access.role)
if errors.Is(err, pgx.ErrNoRows) {
return opsFirmwareAccess{}, ErrAccessDenied
}
if err != nil {
return opsFirmwareAccess{}, fmt.Errorf("resolve operations firmware access: %w", err)
}
if mutate && access.role != "ops_operator" && access.role != "ops_admin" {
return opsFirmwareAccess{}, ErrAccessDenied
}
return access, nil
}
func (s *Service) ListOpsFirmwareSigningKeys(ctx context.Context, principal identity.Principal, tenantPublicID string) ([]FirmwareSigningKey, error) {
access, err := resolveOpsFirmwareAccess(ctx, s.pool, principal, tenantPublicID, false)
if err != nil {
return nil, err
}
return s.listFirmwareSigningKeys(ctx, access.tenantID)
}
func (s *Service) RegisterOpsFirmwareSigningKey(ctx context.Context, principal identity.Principal, tenantPublicID, requestID, keyID, publicKeyBase64 string) error {
decoded, err := base64.StdEncoding.DecodeString(publicKeyBase64)
if err != nil || len(decoded) != ed25519.PublicKeySize || base64.StdEncoding.EncodeToString(decoded) != publicKeyBase64 || keyID == "" || len([]rune(keyID)) > 120 {
return errors.New("invalid Ed25519 public key")
}
return database.Transact(ctx, s.pool, func(tx pgx.Tx) error {
access, err := resolveOpsFirmwareAccess(ctx, tx, principal, tenantPublicID, true)
if err != nil {
return err
}
var existingPublicKey, status string
err = tx.QueryRow(ctx, `SELECT public_key_base64,status FROM firmware_signing_key WHERE tenant_id=$1::uuid AND key_id=$2 FOR UPDATE`, access.tenantID, keyID).Scan(&existingPublicKey, &status)
if errors.Is(err, pgx.ErrNoRows) {
if _, err := tx.Exec(ctx, `INSERT INTO firmware_signing_key(tenant_id,key_id,public_key_base64,created_by) VALUES($1::uuid,$2,$3,$4::uuid)`, access.tenantID, keyID, publicKeyBase64, access.userID); err != nil {
return err
}
} else if err != nil {
return err
} else if existingPublicKey != publicKeyBase64 || status != "ACTIVE" {
return ErrFirmwareSigningKeyConflict
}
fingerprint := sha256.Sum256(decoded)
return s.audit.Record(ctx, tx, audit.Event{TenantID: &access.tenantID, ActorType: "user", ActorPublicID: &access.userPublicID, Action: "ops.firmware.signing_key.register", ResourceType: "firmware_signing_key", RequestID: requestID, Result: "success", Detail: map[string]any{"key_id": keyID, "fingerprint_sha256": hex.EncodeToString(fingerprint[:])}})
})
}
func (s *Service) RevokeOpsFirmwareSigningKey(ctx context.Context, principal identity.Principal, tenantPublicID, requestID, keyID string) (FirmwareSigningKey, error) {
var internalTenantID string
err := database.Transact(ctx, s.pool, func(tx pgx.Tx) error {
access, err := resolveOpsFirmwareAccess(ctx, tx, principal, tenantPublicID, true)
if err != nil {
return err
}
internalTenantID = access.tenantID
var status string
if err := tx.QueryRow(ctx, `SELECT status FROM firmware_signing_key WHERE tenant_id=$1::uuid AND key_id=$2 FOR UPDATE`, access.tenantID, keyID).Scan(&status); errors.Is(err, pgx.ErrNoRows) {
return ErrFirmwareSigningKeyConflict
} else if err != nil {
return err
}
if status == "REVOKED" {
return nil
}
var blocked bool
if err := tx.QueryRow(ctx, `SELECT EXISTS(
SELECT 1 FROM firmware_release fr WHERE fr.tenant_id=$1::uuid AND fr.signing_key_id=$2 AND fr.status='UPLOADING'
UNION ALL
SELECT 1 FROM firmware_rollout ro JOIN firmware_release fr ON fr.id=ro.release_id AND fr.tenant_id=ro.tenant_id WHERE ro.tenant_id=$1::uuid AND fr.signing_key_id=$2 AND ro.status='RUNNING'
)`, access.tenantID, keyID).Scan(&blocked); err != nil {
return err
}
if blocked {
return ErrFirmwareSigningKeyInUse
}
if _, err := tx.Exec(ctx, `UPDATE firmware_signing_key SET status='REVOKED',revoked_at=now() WHERE tenant_id=$1::uuid AND key_id=$2 AND status='ACTIVE'`, access.tenantID, keyID); err != nil {
return err
}
return s.audit.Record(ctx, tx, audit.Event{TenantID: &access.tenantID, ActorType: "user", ActorPublicID: &access.userPublicID, Action: "ops.firmware.signing_key.revoke", ResourceType: "firmware_signing_key", RequestID: requestID, Result: "success", Detail: map[string]any{"key_id": keyID}})
})
if err != nil {
return FirmwareSigningKey{}, err
}
items, err := s.listFirmwareSigningKeys(ctx, internalTenantID)
if err != nil {
return FirmwareSigningKey{}, err
}
for _, item := range items {
if item.KeyID == keyID {
return item, nil
}
}
return FirmwareSigningKey{}, ErrFirmwareSigningKeyConflict
}
func (s *Service) ListOpsFirmwareReleases(ctx context.Context, principal identity.Principal, tenantPublicID, model, status string) ([]FirmwareRelease, error) {
access, err := resolveOpsFirmwareAccess(ctx, s.pool, principal, tenantPublicID, false)
if err != nil {
return nil, err
}
rows, err := s.pool.Query(ctx, `SELECT public_id::text,COALESCE(household_id::text,''),model,COALESCE(hardware_revision,''),version,expected_size,expected_digest,signature_algorithm,signature,signing_key_id,COALESCE(minimum_security_version,''),COALESCE(release_notes,''),release_channel,mandatory,published_at,created_source,status,verified_at,created_at,updated_at,object_ref
FROM firmware_release WHERE tenant_id=$1::uuid AND (NULLIF($2,'') IS NULL OR model=$2) AND (NULLIF($3,'') IS NULL OR status=$3)
ORDER BY created_at DESC LIMIT 200`, access.tenantID, strings.TrimSpace(model), strings.ToUpper(strings.TrimSpace(status)))
if err != nil {
return nil, fmt.Errorf("list operations firmware releases: %w", err)
}
defer rows.Close()
items := make([]FirmwareRelease, 0)
for rows.Next() {
var item FirmwareRelease
if err := rows.Scan(firmwareReleaseTargets(&item)...); err != nil {
return nil, fmt.Errorf("scan operations firmware release: %w", err)
}
items = append(items, item)
}
return items, rows.Err()
}
func (s *Service) PrepareOpsFirmwareRelease(ctx context.Context, principal identity.Principal, tenantPublicID, requestID string, input PrepareFirmwareInput) (FirmwareRelease, error) {
if s.firmwareStore == nil {
return FirmwareRelease{}, ErrFirmwareStateConflict
}
var item FirmwareRelease
err := database.Transact(ctx, s.pool, func(tx pgx.Tx) error {
access, err := resolveOpsFirmwareAccess(ctx, tx, principal, tenantPublicID, true)
if err != nil {
return err
}
var signingKeyActive bool
if err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM firmware_signing_key WHERE tenant_id=$1::uuid AND key_id=$2 AND status='ACTIVE')`, access.tenantID, input.SigningKeyID).Scan(&signingKeyActive); err != nil {
return err
}
if !signingKeyActive {
return ErrFirmwareSigningKeyInactive
}
tenantHash := sha256.Sum256([]byte(access.tenantID))
objectRef := fmt.Sprintf("tenants/%s/firmware/%s/%s/%s.bin", hex.EncodeToString(tenantHash[:16]), safePath(input.Model), safePath(input.HardwareRevision), safePath(input.Version))
err = tx.QueryRow(ctx, `INSERT INTO firmware_release(tenant_id,household_id,model,hardware_revision,version,object_ref,expected_size,expected_digest,signature,signing_key_id,minimum_security_version,release_notes,release_channel,mandatory,created_source,created_by)
VALUES($1::uuid,NULL,$2,NULLIF($3,''),$4,$5,$6,$7,$8,$9,NULLIF($10,''),NULLIF($11,''),$12,$13,'OPS',$14::uuid)
RETURNING public_id::text,COALESCE(household_id::text,''),model,COALESCE(hardware_revision,''),version,expected_size,expected_digest,signature_algorithm,signature,signing_key_id,COALESCE(minimum_security_version,''),COALESCE(release_notes,''),release_channel,mandatory,published_at,created_source,status,verified_at,created_at,updated_at,object_ref`,
access.tenantID, input.Model, input.HardwareRevision, input.Version, objectRef, input.ExpectedSize, input.ExpectedDigest, input.Signature, input.SigningKeyID, input.MinimumSecurityVersion, input.ReleaseNotes, normalizeReleaseChannel(input.ReleaseChannel), input.Mandatory, access.userID).Scan(firmwareReleaseTargets(&item)...)
if err != nil {
return err
}
return s.audit.Record(ctx, tx, audit.Event{TenantID: &access.tenantID, ActorType: "user", ActorPublicID: &access.userPublicID, Action: "ops.firmware.release.prepare", ResourceType: "firmware_release", ResourceID: &item.ID, RequestID: requestID, Result: "success", Detail: map[string]any{"model": input.Model, "version": input.Version, "digest": input.ExpectedDigest, "release_channel": item.ReleaseChannel, "mandatory": item.Mandatory}})
})
if err != nil {
return FirmwareRelease{}, err
}
url, err := s.firmwareStore.PresignUpload(ctx, item.objectRef, 15*time.Minute)
if err != nil {
return FirmwareRelease{}, err
}
until := time.Now().Add(15 * time.Minute)
item.UploadURL, item.UploadUntil = url, &until
return item, nil
}
func (s *Service) resolveOpsFirmwareRelease(ctx context.Context, principal identity.Principal, tenantPublicID, releasePublicID string, mutate bool) (FirmwareRelease, opsFirmwareAccess, error) {
access, err := resolveOpsFirmwareAccess(ctx, s.pool, principal, tenantPublicID, mutate)
if err != nil {
return FirmwareRelease{}, opsFirmwareAccess{}, err
}
var item FirmwareRelease
err = s.pool.QueryRow(ctx, `SELECT public_id::text,COALESCE(household_id::text,''),model,COALESCE(hardware_revision,''),version,expected_size,expected_digest,signature_algorithm,signature,signing_key_id,COALESCE(minimum_security_version,''),COALESCE(release_notes,''),release_channel,mandatory,published_at,created_source,status,verified_at,created_at,updated_at,object_ref
FROM firmware_release WHERE tenant_id=$1::uuid AND public_id=$2::uuid`, access.tenantID, releasePublicID).Scan(firmwareReleaseTargets(&item)...)
if errors.Is(err, pgx.ErrNoRows) {
return FirmwareRelease{}, opsFirmwareAccess{}, ErrNotFound
}
if err != nil {
return FirmwareRelease{}, opsFirmwareAccess{}, err
}
return item, access, nil
}
func (s *Service) FinalizeOpsFirmwareRelease(ctx context.Context, principal identity.Principal, tenantPublicID, releasePublicID, requestID string) (FirmwareRelease, error) {
item, access, err := s.resolveOpsFirmwareRelease(ctx, principal, tenantPublicID, releasePublicID, true)
if err != nil {
return FirmwareRelease{}, err
}
if item.Status == "READY" {
return item, nil
}
if item.Status != "UPLOADING" || s.firmwareStore == nil {
return FirmwareRelease{}, ErrFirmwareStateConflict
}
if err := s.firmwareStore.VerifyDigest(ctx, item.objectRef, item.ExpectedDigest, item.ExpectedSize); err != nil {
return FirmwareRelease{}, err
}
var publicKeyBase64 string
if err := s.pool.QueryRow(ctx, `SELECT public_key_base64 FROM firmware_signing_key WHERE tenant_id=$1::uuid AND key_id=$2 AND status='ACTIVE'`, access.tenantID, item.SigningKeyID).Scan(&publicKeyBase64); err != nil {
return FirmwareRelease{}, ErrFirmwareSigningKeyInactive
}
publicKey, keyErr := base64.StdEncoding.DecodeString(publicKeyBase64)
signature, signatureErr := base64.StdEncoding.DecodeString(item.Signature)
if keyErr != nil || signatureErr != nil || len(publicKey) != ed25519.PublicKeySize || !ed25519.Verify(ed25519.PublicKey(publicKey), []byte(firmwareManifest(item.Model, item.HardwareRevision, item.Version, item.ExpectedDigest, item.ExpectedSize)), signature) {
return FirmwareRelease{}, ErrFirmwareStateConflict
}
err = database.Transact(ctx, s.pool, func(tx pgx.Tx) error {
command, err := tx.Exec(ctx, `UPDATE firmware_release SET status='READY',verified_at=now(),published_at=COALESCE(published_at,now()),updated_at=now() WHERE tenant_id=$1::uuid AND public_id=$2::uuid AND status='UPLOADING'`, access.tenantID, releasePublicID)
if err != nil {
return err
}
if command.RowsAffected() != 1 {
return ErrFirmwareStateConflict
}
return s.audit.Record(ctx, tx, audit.Event{TenantID: &access.tenantID, ActorType: "user", ActorPublicID: &access.userPublicID, Action: "ops.firmware.release.ready", ResourceType: "firmware_release", ResourceID: &releasePublicID, RequestID: requestID, Result: "success", Detail: map[string]any{"digest": item.ExpectedDigest, "size": item.ExpectedSize}})
})
if err != nil {
return FirmwareRelease{}, err
}
return s.currentOpsFirmwareRelease(ctx, principal, tenantPublicID, releasePublicID)
}
func (s *Service) RenewOpsFirmwareUpload(ctx context.Context, principal identity.Principal, tenantPublicID, releasePublicID, requestID string) (FirmwareRelease, error) {
item, access, err := s.resolveOpsFirmwareRelease(ctx, principal, tenantPublicID, releasePublicID, true)
if err != nil {
return FirmwareRelease{}, err
}
if item.Status != "UPLOADING" || s.firmwareStore == nil {
return FirmwareRelease{}, ErrFirmwareStateConflict
}
url, err := s.firmwareStore.PresignUpload(ctx, item.objectRef, 15*time.Minute)
if err != nil {
return FirmwareRelease{}, err
}
until := time.Now().Add(15 * time.Minute)
err = database.Transact(ctx, s.pool, func(tx pgx.Tx) error {
var currentStatus string
if err := tx.QueryRow(ctx, `SELECT status FROM firmware_release WHERE tenant_id=$1::uuid AND public_id=$2::uuid FOR SHARE`, access.tenantID, releasePublicID).Scan(&currentStatus); err != nil {
return err
}
if currentStatus != "UPLOADING" {
return ErrFirmwareStateConflict
}
return s.audit.Record(ctx, tx, audit.Event{TenantID: &access.tenantID, ActorType: "user", ActorPublicID: &access.userPublicID, Action: "ops.firmware.release.upload_renew", ResourceType: "firmware_release", ResourceID: &releasePublicID, RequestID: requestID, Result: "success", Detail: map[string]any{"upload_until": until}})
})
if err != nil {
return FirmwareRelease{}, err
}
item.UploadURL, item.UploadUntil = url, &until
return item, nil
}
func (s *Service) PauseOpsFirmwareRelease(ctx context.Context, principal identity.Principal, tenantPublicID, releasePublicID, requestID string) (FirmwareRelease, error) {
item, access, err := s.resolveOpsFirmwareRelease(ctx, principal, tenantPublicID, releasePublicID, true)
if err != nil {
return FirmwareRelease{}, err
}
if item.Status == "PAUSED" {
return item, nil
}
if item.Status != "READY" {
return FirmwareRelease{}, ErrFirmwareStateConflict
}
err = database.Transact(ctx, s.pool, func(tx pgx.Tx) error {
var releaseID string
if err := tx.QueryRow(ctx, `UPDATE firmware_release SET status='PAUSED',updated_at=now() WHERE tenant_id=$1::uuid AND public_id=$2::uuid AND status='READY' RETURNING id::text`, access.tenantID, releasePublicID).Scan(&releaseID); err != nil {
return ErrFirmwareStateConflict
}
paused, err := tx.Exec(ctx, `UPDATE firmware_rollout SET status='PAUSED',updated_at=now() WHERE release_id=$1::uuid AND status='RUNNING'`, releaseID)
if err != nil {
return err
}
cancelled, err := tx.Exec(ctx, `UPDATE device_command dc SET status='CANCELLED',completed_at=now(),version=version+1,updated_at=now() FROM firmware_assignment fa JOIN firmware_rollout ro ON ro.id=fa.rollout_id WHERE fa.command_id=dc.id AND ro.release_id=$1::uuid AND ro.status='PAUSED' AND dc.status='QUEUED'`, releaseID)
if err != nil {
return err
}
return s.audit.Record(ctx, tx, audit.Event{TenantID: &access.tenantID, ActorType: "user", ActorPublicID: &access.userPublicID, Action: "ops.firmware.release.pause", ResourceType: "firmware_release", ResourceID: &releasePublicID, RequestID: requestID, Result: "success", Detail: map[string]any{"paused_rollouts": paused.RowsAffected(), "cancelled_commands": cancelled.RowsAffected()}})
})
if err != nil {
return FirmwareRelease{}, err
}
return s.currentOpsFirmwareRelease(ctx, principal, tenantPublicID, releasePublicID)
}
func (s *Service) ResumeOpsFirmwareRelease(ctx context.Context, principal identity.Principal, tenantPublicID, releasePublicID, requestID string) (FirmwareRelease, error) {
item, access, err := s.resolveOpsFirmwareRelease(ctx, principal, tenantPublicID, releasePublicID, true)
if err != nil {
return FirmwareRelease{}, err
}
if item.Status == "READY" {
return item, nil
}
if item.Status != "PAUSED" {
return FirmwareRelease{}, ErrFirmwareStateConflict
}
err = database.Transact(ctx, s.pool, func(tx pgx.Tx) error {
command, err := tx.Exec(ctx, `UPDATE firmware_release fr SET status='READY',updated_at=now() FROM firmware_signing_key fsk WHERE fr.tenant_id=$1::uuid AND fr.public_id=$2::uuid AND fr.status='PAUSED' AND fsk.tenant_id=fr.tenant_id AND fsk.key_id=fr.signing_key_id AND fsk.status='ACTIVE'`, access.tenantID, releasePublicID)
if err != nil {
return err
}
if command.RowsAffected() != 1 {
return ErrFirmwareSigningKeyInactive
}
return s.audit.Record(ctx, tx, audit.Event{TenantID: &access.tenantID, ActorType: "user", ActorPublicID: &access.userPublicID, Action: "ops.firmware.release.resume", ResourceType: "firmware_release", ResourceID: &releasePublicID, RequestID: requestID, Result: "success"})
})
if err != nil {
return FirmwareRelease{}, err
}
return s.currentOpsFirmwareRelease(ctx, principal, tenantPublicID, releasePublicID)
}
func (s *Service) RetireOpsFirmwareRelease(ctx context.Context, principal identity.Principal, tenantPublicID, releasePublicID, requestID string) (FirmwareRelease, error) {
item, access, err := s.resolveOpsFirmwareRelease(ctx, principal, tenantPublicID, releasePublicID, true)
if err != nil {
return FirmwareRelease{}, err
}
if item.Status != "UPLOADING" && item.Status != "READY" && item.Status != "PAUSED" && item.Status != "RETIRED" {
return FirmwareRelease{}, ErrFirmwareStateConflict
}
removeObject := item.Status == "UPLOADING" || item.Status == "RETIRED" && item.VerifiedAt == nil
if item.Status != "RETIRED" {
err = database.Transact(ctx, s.pool, func(tx pgx.Tx) error {
var releaseID, status string
var verifiedAt *time.Time
if err := tx.QueryRow(ctx, `SELECT id::text,status,verified_at FROM firmware_release WHERE tenant_id=$1::uuid AND public_id=$2::uuid FOR UPDATE`, access.tenantID, releasePublicID).Scan(&releaseID, &status, &verifiedAt); err != nil {
return err
}
if status == "READY" || status == "PAUSED" {
var running bool
if err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM firmware_rollout WHERE release_id=$1::uuid AND status='RUNNING')`, releaseID).Scan(&running); err != nil {
return err
}
if running {
return ErrFirmwareReleaseInUse
}
removeObject = false
}
if _, err := tx.Exec(ctx, `UPDATE firmware_release SET status='RETIRED',updated_at=now() WHERE id=$1::uuid AND status=$2`, releaseID, status); err != nil {
return err
}
return s.audit.Record(ctx, tx, audit.Event{TenantID: &access.tenantID, ActorType: "user", ActorPublicID: &access.userPublicID, Action: "ops.firmware.release.retire", ResourceType: "firmware_release", ResourceID: &releasePublicID, RequestID: requestID, Result: "success", Detail: map[string]any{"from_status": status, "object_preserved": verifiedAt != nil}})
})
if err != nil {
return FirmwareRelease{}, err
}
item.Status, item.UpdatedAt = "RETIRED", time.Now()
}
if removeObject {
if s.firmwareStore == nil {
return FirmwareRelease{}, ErrFirmwareStateConflict
}
if err := s.firmwareStore.Remove(ctx, item.objectRef); err != nil {
return FirmwareRelease{}, fmt.Errorf("remove retired operations firmware object: %w", err)
}
}
return item, nil
}
func (s *Service) currentOpsFirmwareRelease(ctx context.Context, principal identity.Principal, tenantPublicID, releasePublicID string) (FirmwareRelease, error) {
item, _, err := s.resolveOpsFirmwareRelease(ctx, principal, tenantPublicID, releasePublicID, false)
return item, err
}
func (s *Service) OpsFirmwareDownload(ctx context.Context, principal identity.Principal, tenantPublicID, releasePublicID, requestID string) (FirmwareDownload, error) {
item, access, err := s.resolveOpsFirmwareRelease(ctx, principal, tenantPublicID, releasePublicID, false)
if err != nil {
return FirmwareDownload{}, err
}
if item.VerifiedAt == nil || (item.Status != "READY" && item.Status != "PAUSED" && item.Status != "RETIRED") || s.firmwareStore == nil {
return FirmwareDownload{}, ErrFirmwareStateConflict
}
ttl := s.firmwareURLTTL
if ttl <= 0 {
ttl = 5 * time.Minute
}
url, err := s.firmwareStore.PresignDownload(ctx, item.objectRef, ttl, "firmware-"+safePath(item.Model)+"-"+safePath(item.Version)+".bin")
if err != nil {
return FirmwareDownload{}, err
}
until := time.Now().Add(ttl)
err = database.Transact(ctx, s.pool, func(tx pgx.Tx) error {
return s.audit.Record(ctx, tx, audit.Event{TenantID: &access.tenantID, ActorType: "user", ActorPublicID: &access.userPublicID, Action: "ops.firmware.release.download", ResourceType: "firmware_release", ResourceID: &releasePublicID, RequestID: requestID, Result: "success", Detail: map[string]any{"expires_at": until}})
})
if err != nil {
return FirmwareDownload{}, err
}
return FirmwareDownload{ReleaseID: item.ID, Version: item.Version, ExpectedSize: item.ExpectedSize, SHA256: item.ExpectedDigest, Signature: item.Signature, SigningKeyID: item.SigningKeyID, DownloadURL: url, DownloadUntil: until}, nil
}
func (s *Service) StartOpsFirmwareRollout(ctx context.Context, principal identity.Principal, tenantPublicID, releasePublicID, requestID, name string, percentage int, deviceIDs []string) (FirmwareRollout, error) {
if strings.TrimSpace(name) == "" || percentage < 1 || percentage > 100 || len(deviceIDs) == 0 {
return FirmwareRollout{}, ErrFirmwareStateConflict
}
deviceIDs = append([]string(nil), deviceIDs...)
sort.Strings(deviceIDs)
uniqueDeviceIDs := deviceIDs[:0]
for _, deviceID := range deviceIDs {
if len(uniqueDeviceIDs) == 0 || uniqueDeviceIDs[len(uniqueDeviceIDs)-1] != deviceID {
uniqueDeviceIDs = append(uniqueDeviceIDs, deviceID)
}
}
deviceIDs = uniqueDeviceIDs
var result FirmwareRollout
err := database.Transact(ctx, s.pool, func(tx pgx.Tx) error {
access, err := resolveOpsFirmwareAccess(ctx, tx, principal, tenantPublicID, true)
if err != nil {
return err
}
var releaseID, model, hardware, version, digest, signature, keyID string
err = tx.QueryRow(ctx, `SELECT fr.id::text,fr.model,COALESCE(fr.hardware_revision,''),fr.version,fr.expected_digest,fr.signature,fr.signing_key_id
FROM firmware_release fr JOIN firmware_signing_key fsk ON fsk.tenant_id=fr.tenant_id AND fsk.key_id=fr.signing_key_id AND fsk.status='ACTIVE'
WHERE fr.tenant_id=$1::uuid AND fr.public_id=$2::uuid AND fr.status='READY' FOR SHARE OF fr,fsk`, access.tenantID, releasePublicID).
Scan(&releaseID, &model, &hardware, &version, &digest, &signature, &keyID)
if errors.Is(err, pgx.ErrNoRows) {
return ErrFirmwareStateConflict
}
if err != nil {
return err
}
var rolloutID string
err = tx.QueryRow(ctx, `INSERT INTO firmware_rollout(tenant_id,household_id,release_id,name,status,target_percentage,created_by,started_at)
VALUES($1::uuid,NULL,$2::uuid,$3,'RUNNING',$4,$5::uuid,now()) RETURNING id::text,public_id::text,created_at,updated_at`,
access.tenantID, releaseID, strings.TrimSpace(name), percentage, access.userID).Scan(&rolloutID, &result.ID, &result.CreatedAt, &result.UpdatedAt)
if err != nil {
return err
}
result.ReleaseID, result.Name, result.Status, result.TargetPercentage = releasePublicID, strings.TrimSpace(name), "RUNNING", percentage
selected := 0
for _, devicePublicID := range deviceIDs {
hash := sha256.Sum256([]byte(devicePublicID))
if int(hash[0])%100 >= percentage && selected > 0 {
continue
}
var deviceID, previous string
err := tx.QueryRow(ctx, `SELECT d.id::text,COALESCE(dt.firmware_version,'') FROM device d JOIN device_twin dt ON dt.device_id=d.id AND dt.tenant_id=d.tenant_id
WHERE d.tenant_id=$1::uuid AND d.public_id=$2::uuid AND d.status='registered' AND d.model=$3 AND ($4='' OR d.hardware_revision=$4)`,
access.tenantID, devicePublicID, model, hardware).Scan(&deviceID, &previous)
if err != nil {
return fmt.Errorf("%w: rollout target %s is incompatible or inaccessible", ErrFirmwareStateConflict, devicePublicID)
}
payload, _ := json.Marshal(map[string]any{"release_id": releasePublicID, "version": version, "sha256": digest, "signature": signature, "signing_key_id": keyID, "download_path": "/device/v2/firmware-releases/" + releasePublicID + "/download"})
requestDigest := sha256.Sum256(payload)
var commandID string
err = tx.QueryRow(ctx, `INSERT INTO device_command(tenant_id,device_id,command_type,payload,requested_by,idempotency_key,request_digest,expires_at)
VALUES($1::uuid,$2::uuid,'firmware_update',$3::jsonb,$4::uuid,$5,$6,now()+interval '24 hours') RETURNING id::text`,
access.tenantID, deviceID, payload, access.userID, "ops-firmware-rollout:"+rolloutID+":"+deviceID, hex.EncodeToString(requestDigest[:])).Scan(&commandID)
if err != nil {
return err
}
if _, err := tx.Exec(ctx, `INSERT INTO firmware_assignment(rollout_id,tenant_id,device_id,command_id,previous_version,status) VALUES($1::uuid,$2::uuid,$3::uuid,$4::uuid,NULLIF($5,''),'ASSIGNED')`, rolloutID, access.tenantID, deviceID, commandID, previous); err != nil {
return err
}
selected++
}
if selected == 0 {
return errors.New("rollout selected no devices")
}
result.Targeted = selected
return s.audit.Record(ctx, tx, audit.Event{TenantID: &access.tenantID, ActorType: "user", ActorPublicID: &access.userPublicID, Action: "ops.firmware.rollout.start", ResourceType: "firmware_rollout", ResourceID: &result.ID, RequestID: requestID, Result: "success", Detail: map[string]any{"release_id": releasePublicID, "targeted": selected, "percentage": percentage}})
})
return result, err
}
func (s *Service) ListOpsFirmwareRollouts(ctx context.Context, principal identity.Principal, tenantPublicID string) ([]FirmwareRollout, error) {
access, err := resolveOpsFirmwareAccess(ctx, s.pool, principal, tenantPublicID, false)
if err != nil {
return nil, err
}
rows, err := s.pool.Query(ctx, `SELECT ro.public_id::text,fr.public_id::text,COALESCE(h.public_id::text,''),ro.name,ro.status,ro.target_percentage,
count(fa.id),count(fa.id) FILTER(WHERE fa.status='CONFIRMED'),count(fa.id) FILTER(WHERE fa.status='FAILED'),ro.created_at,ro.updated_at
FROM firmware_rollout ro JOIN firmware_release fr ON fr.id=ro.release_id LEFT JOIN household h ON h.id=ro.household_id
LEFT JOIN firmware_assignment fa ON fa.rollout_id=ro.id WHERE ro.tenant_id=$1::uuid
GROUP BY ro.id,fr.public_id,h.public_id ORDER BY ro.created_at DESC LIMIT 200`, access.tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
items := make([]FirmwareRollout, 0)
for rows.Next() {
var item FirmwareRollout
var targeted, succeeded, failed int64
if err := rows.Scan(&item.ID, &item.ReleaseID, &item.HouseholdID, &item.Name, &item.Status, &item.TargetPercentage, &targeted, &succeeded, &failed, &item.CreatedAt, &item.UpdatedAt); err != nil {
return nil, err
}
item.Targeted, item.Succeeded, item.Failed = int(targeted), int(succeeded), int(failed)
items = append(items, item)
}
return items, rows.Err()
}
func (s *Service) GetOpsFirmwareRollout(ctx context.Context, principal identity.Principal, tenantPublicID, rolloutPublicID string) (FirmwareRollout, error) {
access, err := resolveOpsFirmwareAccess(ctx, s.pool, principal, tenantPublicID, false)
if err != nil {
return FirmwareRollout{}, err
}
var item FirmwareRollout
var targeted, succeeded, failed int64
err = s.pool.QueryRow(ctx, `SELECT ro.public_id::text,fr.public_id::text,COALESCE(h.public_id::text,''),ro.name,ro.status,ro.target_percentage,
count(fa.id),count(fa.id) FILTER(WHERE fa.status='CONFIRMED'),count(fa.id) FILTER(WHERE fa.status='FAILED'),ro.created_at,ro.updated_at
FROM firmware_rollout ro JOIN firmware_release fr ON fr.id=ro.release_id LEFT JOIN household h ON h.id=ro.household_id
LEFT JOIN firmware_assignment fa ON fa.rollout_id=ro.id WHERE ro.tenant_id=$1::uuid AND ro.public_id=$2::uuid
GROUP BY ro.id,fr.public_id,h.public_id`, access.tenantID, rolloutPublicID).
Scan(&item.ID, &item.ReleaseID, &item.HouseholdID, &item.Name, &item.Status, &item.TargetPercentage, &targeted, &succeeded, &failed, &item.CreatedAt, &item.UpdatedAt)
if errors.Is(err, pgx.ErrNoRows) {
return FirmwareRollout{}, ErrNotFound
}
if err != nil {
return FirmwareRollout{}, err
}
item.Targeted, item.Succeeded, item.Failed = int(targeted), int(succeeded), int(failed)
item.Assignments, err = s.listFirmwareAssignments(ctx, rolloutPublicID)
return item, err
}
func (s *Service) PauseOpsFirmwareRollout(ctx context.Context, principal identity.Principal, tenantPublicID, rolloutPublicID, requestID string) (FirmwareRollout, error) {
access, err := resolveOpsFirmwareAccess(ctx, s.pool, principal, tenantPublicID, true)
if err != nil {
return FirmwareRollout{}, err
}
err = database.Transact(ctx, s.pool, func(tx pgx.Tx) error {
command, err := tx.Exec(ctx, `UPDATE firmware_rollout SET status='PAUSED',updated_at=now() WHERE tenant_id=$1::uuid AND public_id=$2::uuid AND status='RUNNING'`, access.tenantID, rolloutPublicID)
if err != nil {
return err
}
if command.RowsAffected() != 1 {
return ErrFirmwareStateConflict
}
if _, err := tx.Exec(ctx, `UPDATE device_command dc SET status='CANCELLED',completed_at=now(),version=version+1,updated_at=now() FROM firmware_assignment fa JOIN firmware_rollout ro ON ro.id=fa.rollout_id WHERE fa.command_id=dc.id AND ro.public_id=$1::uuid AND dc.status='QUEUED'`, rolloutPublicID); err != nil {
return err
}
return s.audit.Record(ctx, tx, audit.Event{TenantID: &access.tenantID, ActorType: "user", ActorPublicID: &access.userPublicID, Action: "ops.firmware.rollout.pause", ResourceType: "firmware_rollout", ResourceID: &rolloutPublicID, RequestID: requestID, Result: "success"})
})
if err != nil {
return FirmwareRollout{}, err
}
return s.GetOpsFirmwareRollout(ctx, principal, tenantPublicID, rolloutPublicID)
}