7d6719da12
Replace direct npm.Client usage throughout the codebase with the proxy.Provider interface, enabling pluggable proxy backends. The deployer, API layer, and proxy manager now use provider-agnostic route management (ConfigureRoute/DeleteRoute) instead of NPM-specific API calls. Adds ProxyRouteID (string) to Instance model and ProxyProvider setting to Settings, with SQLite migrations for backward compatibility.
189 lines
5.7 KiB
Go
189 lines
5.7 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// instanceColumns is the canonical column list for instance queries.
|
|
const instanceColumns = `id, stage_id, project_id, container_id, image_tag, subdomain, npm_proxy_id, proxy_route_id, status, port, last_alive_at, created_at, updated_at`
|
|
|
|
// scanInstance scans a row into an Instance struct using the canonical column order.
|
|
func scanInstance(scanner interface{ Scan(...any) error }) (Instance, error) {
|
|
var inst Instance
|
|
err := scanner.Scan(
|
|
&inst.ID, &inst.StageID, &inst.ProjectID, &inst.ContainerID, &inst.ImageTag,
|
|
&inst.Subdomain, &inst.NpmProxyID, &inst.ProxyRouteID, &inst.Status, &inst.Port,
|
|
&inst.LastAliveAt, &inst.CreatedAt, &inst.UpdatedAt,
|
|
)
|
|
return inst, err
|
|
}
|
|
|
|
// CreateInstance inserts a new instance record.
|
|
func (s *Store) CreateInstance(inst Instance) (Instance, error) {
|
|
inst.ID = uuid.New().String()
|
|
inst.CreatedAt = Now()
|
|
inst.UpdatedAt = inst.CreatedAt
|
|
|
|
_, err := s.db.Exec(
|
|
`INSERT INTO instances (`+instanceColumns+`)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
inst.ID, inst.StageID, inst.ProjectID, inst.ContainerID, inst.ImageTag,
|
|
inst.Subdomain, inst.NpmProxyID, inst.ProxyRouteID, inst.Status, inst.Port,
|
|
inst.LastAliveAt, inst.CreatedAt, inst.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return Instance{}, fmt.Errorf("insert instance: %w", err)
|
|
}
|
|
return inst, nil
|
|
}
|
|
|
|
// CreateInstanceWithID inserts a new instance using a pre-generated ID.
|
|
// Use this when the ID must be known before creation (e.g., for container labels).
|
|
func (s *Store) CreateInstanceWithID(inst Instance) (Instance, error) {
|
|
if inst.ID == "" {
|
|
return Instance{}, fmt.Errorf("instance ID is required")
|
|
}
|
|
inst.CreatedAt = Now()
|
|
inst.UpdatedAt = inst.CreatedAt
|
|
|
|
_, err := s.db.Exec(
|
|
`INSERT INTO instances (`+instanceColumns+`)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
inst.ID, inst.StageID, inst.ProjectID, inst.ContainerID, inst.ImageTag,
|
|
inst.Subdomain, inst.NpmProxyID, inst.ProxyRouteID, inst.Status, inst.Port,
|
|
inst.LastAliveAt, inst.CreatedAt, inst.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return Instance{}, fmt.Errorf("insert instance: %w", err)
|
|
}
|
|
return inst, nil
|
|
}
|
|
|
|
// GetInstanceByID returns a single instance by its ID.
|
|
func (s *Store) GetInstanceByID(id string) (Instance, error) {
|
|
inst, err := scanInstance(s.db.QueryRow(
|
|
`SELECT `+instanceColumns+` FROM instances WHERE id = ?`, id,
|
|
))
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return Instance{}, fmt.Errorf("instance %s: %w", id, ErrNotFound)
|
|
}
|
|
if err != nil {
|
|
return Instance{}, fmt.Errorf("query instance: %w", err)
|
|
}
|
|
return inst, nil
|
|
}
|
|
|
|
// GetInstancesByStageID returns all instances for a given stage.
|
|
func (s *Store) GetInstancesByStageID(stageID string) ([]Instance, error) {
|
|
rows, err := s.db.Query(
|
|
`SELECT `+instanceColumns+` FROM instances WHERE stage_id = ? ORDER BY created_at DESC`, stageID,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query instances: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
instances := []Instance{}
|
|
for rows.Next() {
|
|
inst, err := scanInstance(rows)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("scan instance: %w", err)
|
|
}
|
|
instances = append(instances, inst)
|
|
}
|
|
return instances, rows.Err()
|
|
}
|
|
|
|
// ListAllInstances returns all instances across all stages.
|
|
func (s *Store) ListAllInstances() ([]Instance, error) {
|
|
rows, err := s.db.Query(
|
|
`SELECT ` + instanceColumns + ` FROM instances ORDER BY created_at DESC`,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query all instances: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
instances := []Instance{}
|
|
for rows.Next() {
|
|
inst, err := scanInstance(rows)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("scan instance: %w", err)
|
|
}
|
|
instances = append(instances, inst)
|
|
}
|
|
return instances, rows.Err()
|
|
}
|
|
|
|
// UpdateInstance updates an existing instance's mutable fields.
|
|
func (s *Store) UpdateInstance(inst Instance) error {
|
|
inst.UpdatedAt = Now()
|
|
result, err := s.db.Exec(
|
|
`UPDATE instances SET stage_id=?, project_id=?, container_id=?, image_tag=?, subdomain=?, npm_proxy_id=?, proxy_route_id=?, status=?, port=?, last_alive_at=?, updated_at=?
|
|
WHERE id=?`,
|
|
inst.StageID, inst.ProjectID, inst.ContainerID, inst.ImageTag,
|
|
inst.Subdomain, inst.NpmProxyID, inst.ProxyRouteID, inst.Status, inst.Port,
|
|
inst.LastAliveAt, inst.UpdatedAt, inst.ID,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("update instance: %w", err)
|
|
}
|
|
n, _ := result.RowsAffected()
|
|
if n == 0 {
|
|
return fmt.Errorf("instance %s: %w", inst.ID, ErrNotFound)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UpdateInstanceStatus sets only the status field on an instance.
|
|
func (s *Store) UpdateInstanceStatus(id string, status string) error {
|
|
ts := Now()
|
|
result, err := s.db.Exec(
|
|
`UPDATE instances SET status=?, updated_at=? WHERE id=?`,
|
|
status, ts, id,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("update instance status: %w", err)
|
|
}
|
|
n, _ := result.RowsAffected()
|
|
if n == 0 {
|
|
return fmt.Errorf("instance %s: %w", id, ErrNotFound)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UpdateLastAliveAt sets the last_alive_at timestamp for an instance.
|
|
// Called when an instance is seen running.
|
|
func (s *Store) UpdateLastAliveAt(id string) error {
|
|
ts := Now()
|
|
result, err := s.db.Exec(
|
|
`UPDATE instances SET last_alive_at=?, updated_at=? WHERE id=?`,
|
|
ts, ts, id,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("update last_alive_at: %w", err)
|
|
}
|
|
n, _ := result.RowsAffected()
|
|
if n == 0 {
|
|
return fmt.Errorf("instance %s: %w", id, ErrNotFound)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DeleteInstance removes an instance by ID.
|
|
func (s *Store) DeleteInstance(id string) error {
|
|
result, err := s.db.Exec(`DELETE FROM instances WHERE id = ?`, id)
|
|
if err != nil {
|
|
return fmt.Errorf("delete instance: %w", err)
|
|
}
|
|
n, _ := result.RowsAffected()
|
|
if n == 0 {
|
|
return fmt.Errorf("instance %s: %w", id, ErrNotFound)
|
|
}
|
|
return nil
|
|
}
|