database

package
v1.6.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 23, 2026 License: AGPL-3.0 Imports: 22 Imported by: 0

Documentation

Overview

Package database provisions database server instances (PostgreSQL, MySQL, MariaDB, Redis) as containers and manages the logical databases they host, so a single instance can back many apps instead of one container per database.

Index

Constants

View Source
const (
	SecretOwnerDatabase = "database"    // a logical database (its scoped user)
	SecretOwnerInstance = "db_instance" // an instance (redis / admin)
)

Owner kinds for a managed database's Vault secrets.

View Source
const (
	PathInPlace     = "in-place"     // swap the image on the same data volume
	PathDumpRestore = "dump-restore" // dump all logical DBs and restore into a fresh volume
)

Upgrade paths: how the data is carried from the old version to the new one.

Variables

View Source
var (
	ErrUnsupportedEngine = errors.New("unsupported database engine")
	ErrSlugTaken         = errors.New("database name already taken in workspace")
	ErrNoLogicalDBs      = errors.New("this engine does not support multiple databases")
	ErrInstanceNotReady  = errors.New("database instance is not running yet")
	ErrNotFound          = errors.New("not found")
	ErrNameTaken         = errors.New("a database with this name already exists on the instance")
	ErrNoContainer       = errors.New("database has no container; re-provision it")
	ErrInstanceInUse     = errors.New("a database on this instance is attached to an application; detach it first")
	ErrInstanceRunning   = errors.New("stop the database before deleting it")
	ErrInstanceOwned     = errors.New("database is owned by another resource")
	// Upgrade errors.
	ErrInvalidVersion        = errors.New("invalid target version")
	ErrAlreadyOnVersion      = errors.New("instance is already on this version")
	ErrDowngrade             = errors.New("downgrade is not supported; data written by a newer version may be unreadable")
	ErrMongoMajorUpgrade     = errors.New("MongoDB major-version upgrades must be applied one major at a time and are not yet automated; create a new instance and migrate, or upgrade within the same major")
	ErrUpgradeInProgress     = errors.New("an upgrade is already in progress for this instance")
	ErrInstanceNotUpgradable = errors.New("instance must be running or stopped to upgrade")
	ErrDefaultNetwork        = errors.New("the workspace default network cannot be detached")
	ErrNoNetworkProvider     = errors.New("network management is not available")
)
View Source
var ErrNoSharedInstance = errors.New("no compatible running database instance to share")

ErrNoSharedInstance is returned when placement "shared" finds no compatible running instance to host the dependency's logical database.

Functions

func InstancePasswordSecretName

func InstancePasswordSecretName(inst *models.DatabaseInstance) string

InstancePasswordSecretName / InstanceURLSecretName are the names for an instance-level secret (Redis, which has no logical databases).

func InstanceURLSecretName

func InstanceURLSecretName(inst *models.DatabaseInstance) string

func PasswordSecretName

func PasswordSecretName(inst *models.DatabaseInstance, db *models.Database) string

PasswordSecretName / URLSecretName are the canonical Vault names for a logical database's scoped-user password and full connection URL. The instance slug is unique per workspace, so the names don't collide across instances.

func URLSecretName

func URLSecretName(inst *models.DatabaseInstance, db *models.Database) string

Types

type AppController

type AppController interface {
	StopByID(ctx context.Context, workspaceID, appID uint) error
	StartByID(ctx context.Context, workspaceID, appID uint) error
}

AppController stops/starts the applications that use a database while it is being upgraded, so writers are quiesced during a major (copy) upgrade. Wired over the application service by the composition root (an interface here keeps the database service free of an app-service dependency).

type AppDatabase

type AppDatabase struct {
	models.Database
	InstanceName string          `json:"instance_name"`
	Engine       models.DBEngine `json:"engine"`
	Host         string          `json:"host"`
	Port         int             `json:"port"`
}

AppDatabase is a logical database enriched with its instance's engine and network address, for display on an application.

type ConnectionInfo

type ConnectionInfo struct {
	Host     string `json:"host"`
	Port     int    `json:"port"`
	Username string `json:"username"`
	Password string `json:"password"`
	Database string `json:"database"`
	URI      string `json:"uri"`
}

ConnectionInfo is the (sensitive) connection detail for a database.

type DumpRef

type DumpRef struct {
	Filename string
	Volume   string
}

DumpRef is an opaque handle to a logical-database dump produced by Dump and consumed by Load (the artifact filename + the node volume it lives on).

type EngineDefault

type EngineDefault struct {
	Engine  models.DBEngine `json:"engine"`
	Image   string          `json:"image"`   // effective default image ref
	Version string          `json:"version"` // default tag
}

EngineDefault is the resolved default image/version for an engine, used to prefill the create form so it reflects the admin's deployment-config tags.

type Enqueuer

type Enqueuer interface {
	EnqueueProvisionDB(instanceID, serverID uint) error
	EnqueueUpgradeDB(instanceID, serverID uint, target, path string, stopApps bool) error
}

Enqueuer schedules background provisioning. Implemented by the worker producer; an interface here avoids a database->worker import cycle.

type ImageResolver

type ImageResolver interface {
	Ref(key string) string
}

ImageResolver resolves a deployment-config catalog key to an image ref.

type LiveStatus

type LiveStatus struct {
	Status         string                  `json:"status"` // headline: stored lifecycle, or live-derived when running
	StoredStatus   models.DBStatus         `json:"stored_status"`
	ContainerState string                  `json:"container_state,omitempty"`
	Health         string                  `json:"health,omitempty"`
	Running        bool                    `json:"running"`
	Restarting     bool                    `json:"restarting"`
	RestartCount   int                     `json:"restart_count"`
	ExitCode       int                     `json:"exit_code"`
	StartedAt      string                  `json:"started_at,omitempty"`
	UptimeSeconds  int64                   `json:"uptime_seconds"`
	HasContainer   bool                    `json:"has_container"`
	Upgrade        *models.UpgradeProgress `json:"upgrade,omitempty"`
	Stats          *docker.StatsSample     `json:"stats,omitempty"`
}

LiveStatus is a lightweight, pollable view of an instance's state: the stored lifecycle status (provisioning/running/stopped/failed/upgrading) plus, when a container exists, its live Docker state and a stats snapshot. The detail page polls this so a user sees provisioning finish — or a crash — without a manual refresh. Mirrors the application LiveStatus endpoint.

type LogicalBackup

type LogicalBackup interface {
	Dump(ctx context.Context, inst *models.DatabaseInstance, db *models.Database) (DumpRef, error)
	Load(ctx context.Context, inst *models.DatabaseInstance, db *models.Database, ref DumpRef, force bool) error
}

LogicalBackup dumps and restores a single logical database. Wired over the backup service by the composition root (keeps the database service decoupled from the backup package and its types).

type NetworkProvider

type NetworkProvider interface {
	EnsureDefault(ctx context.Context, workspaceID uint) (*models.Network, error)
	Get(workspaceID, id uint) (*models.Network, error)
}

NetworkProvider resolves workspace Docker networks so a database joins the same networks its applications do — the default network always, plus any custom networks the user attaches. Implemented by the network service.

type NodeDocker

type NodeDocker interface {
	For(serverID uint) (docker.Client, error)
	LocalID() uint
}

NodeDocker resolves the Docker client for a node id (0 = local).

type NodeGuard

type NodeGuard interface {
	Placeable(serverID uint) error
}

NodeGuard validates that a node can accept a new placement (exists, not cordoned). Implemented by the node service; injected after construction.

type OwnerExister

type OwnerExister func(kind string, id, workspaceID uint) bool

OwnerExister reports whether an owning resource of the given kind/id still exists in the workspace, letting Delete refuse to orphan a database that still backs an app/stack. Wired by the composition root to avoid depending on the app/stack repositories directly.

type Placement

type Placement string

Placement decides how a database dependency is satisfied against the workspace. It is shared by the marketplace installer and the declarative (GitOps) apply so both honor the same semantics.

const (
	// PlacementAuto reuses a compatible running instance when one exists (creating
	// a dedicated logical database on it), otherwise provisions a fresh instance.
	PlacementAuto Placement = "auto"
	// PlacementDedicated always provisions a fresh instance for the dependency.
	PlacementDedicated Placement = "dedicated"
	// PlacementShared requires an existing running compatible instance and creates
	// a dedicated logical database on it; it never provisions a new instance.
	PlacementShared Placement = "shared"
)

type SecretWriter

type SecretWriter interface {
	UpsertOwned(workspaceID uint, ownerKind string, ownerID uint, name, value, description string) (*models.Secret, error)
	DeleteOwned(workspaceID uint, ownerKind string, ownerID uint) ([]models.Application, error)
}

SecretWriter manages the Vault secrets owned by a managed database. Implemented by the secret service; injected after construction (optional — nil disables auto-provisioning and the cascade, falling back to plaintext env injection).

type ServerInfo

type ServerInfo interface {
	Get(id uint) (*models.Server, error)
}

ServerInfo resolves a node's display metadata by id. Implemented by the node service; injected after construction (optional — read paths degrade to an empty server name when unset, e.g. in the worker).

type Service

type Service struct {
	// contains filtered or unexported fields
}

func NewService

func NewService(repo *repositories.DatabaseRepository, clients NodeDocker, enqueuer Enqueuer) *Service

func (*Service) AttachNetwork

func (s *Service) AttachNetwork(ctx context.Context, workspaceID, instanceID, networkID uint) (*models.DatabaseInstance, error)

AttachNetwork connects the instance to an additional workspace network and, when its container exists, attaches it live (no restart). Idempotent.

func (*Service) AttachToApp

func (s *Service) AttachToApp(workspaceID, dbID, appID uint, envPrefix string) (*models.Database, error)

AttachToApp links an existing logical database to an application, recording the env prefix used for its injected connection vars. Returns the updated database. Node-affinity and cross-app checks are the caller's responsibility.

func (*Service) CreateDatabase

func (s *Service) CreateDatabase(ctx context.Context, workspaceID, instanceID uint, name string, appID *uint) (*models.Database, error)

CreateDatabase provisions a new logical database (with its own scoped user) on a running SQL instance by running the engine client as a one-shot container.

func (*Service) DatabaseConnection

func (s *Service) DatabaseConnection(inst *models.DatabaseInstance, d *models.Database) (ConnectionInfo, error)

DatabaseConnection returns the decrypted connection info for a logical database (sensitive — admin only).

func (*Service) DatabaseConnectionForApp

func (s *Service) DatabaseConnectionForApp(workspaceID, appID, dbID uint) (ConnectionInfo, error)

DatabaseConnectionForApp reveals the connection for a logical database that belongs to the given application (the app's own scoped credentials — not the instance admin credentials).

func (*Service) Delete

func (s *Service) Delete(ctx context.Context, inst *models.DatabaseInstance) error

Delete stops and removes the instance container, its data volume, and its logical database records (their data lives in the dropped container). Refuses while the instance is running, or when any of its databases is still attached to an application.

func (*Service) DeleteDatabase

func (s *Service) DeleteDatabase(ctx context.Context, workspaceID, id uint) error

DeleteDatabase drops a logical database and its user from the instance.

func (*Service) DetachFromApp

func (s *Service) DetachFromApp(workspaceID, dbID uint) (*models.Database, error)

DetachFromApp unlinks a logical database from its application, clearing the owner and env prefix. Returns the updated database (with its prior prefix so the caller can clean up injected env vars before it is cleared on the row).

func (*Service) DetachNetwork

func (s *Service) DetachNetwork(ctx context.Context, workspaceID, instanceID, networkID uint) (*models.DatabaseInstance, error)

DetachNetwork disconnects the instance from a network. The workspace default network (which carries the alias apps reach the instance by) cannot be removed.

func (*Service) EngineDefaults

func (s *Service) EngineDefaults() []EngineDefault

EngineDefaults returns the resolved default image+version per engine (from the deployment-config catalog), in a stable order.

func (*Service) FindDatabaseByDeclName

func (s *Service) FindDatabaseByDeclName(workspaceID uint, declName string) (*models.Database, *models.DatabaseInstance, bool)

FindDatabaseByDeclName returns the logical database stamped with the given declarative name and its hosting instance; ok is false when none exists.

func (*Service) FindReusableInstance

func (s *Service) FindReusableInstance(workspaceID uint, engine models.DBEngine) *models.DatabaseInstance

FindReusableInstance returns the running instance a shared/auto placement would reuse for engine, or nil. Exposed so a caller can pre-resolve a dependency's target node without performing the placement.

func (*Service) Get

func (s *Service) Get(workspaceID, id uint) (*models.DatabaseInstance, error)

func (*Service) GetDatabase

func (s *Service) GetDatabase(workspaceID, id uint) (*models.Database, error)

GetDatabase loads a logical database scoped to the workspace.

func (*Service) IDByUID

func (s *Service) IDByUID(uid string) (uint, error)

IDByUID resolves a database instance's portable uid to its numeric id.

func (*Service) InstanceConnection

func (s *Service) InstanceConnection(inst *models.DatabaseInstance) (ConnectionInfo, error)

InstanceConnection returns the admin/direct connection for an instance. Used for Redis (which has no logical databases) and for diagnostics.

func (*Service) List

func (s *Service) List(workspaceID uint) ([]models.DatabaseInstance, error)

func (*Service) ListByApp

func (s *Service) ListByApp(workspaceID, appID uint) ([]AppDatabase, error)

ListByApp returns the logical databases attached to an application, each with its instance's engine and address.

func (*Service) ListDatabases

func (s *Service) ListDatabases(workspaceID, instanceID uint) ([]models.Database, error)

ListDatabases returns an instance's logical databases.

func (*Service) ListDatabasesByWorkspace

func (s *Service) ListDatabasesByWorkspace(workspaceID uint) ([]models.Database, error)

ListDatabasesByWorkspace returns every logical database in the workspace.

func (*Service) LiveStatus

func (s *Service) LiveStatus(ctx context.Context, inst *models.DatabaseInstance) LiveStatus

LiveStatus inspects the instance's container and returns its real-time status. Falls back to the stored status when there is no container yet (provisioning), it was removed, or the node is unreachable — so polling always gets an answer.

func (*Service) MaybeRefreshSizes

func (s *Service) MaybeRefreshSizes(inst *models.DatabaseInstance)

MaybeRefreshSizes lazily refreshes an instance's sizes in the background when they are stale (older than sizeSyncTTL) and the instance is running. It never blocks the caller and debounces concurrent triggers per instance, so a detail-page load returns immediately with the current (possibly slightly stale) values and the next load shows fresh ones.

func (*Service) PlanUpgrade

func (s *Service) PlanUpgrade(workspaceID, instanceID uint, target string) (*UpgradePlan, error)

PlanUpgrade resolves (without mutating) how an upgrade to target would run, or an error for a same-version/downgrade/invalid target.

func (*Service) PrepareDatabase

func (s *Service) PrepareDatabase(workspaceID, instanceID uint, name string, appID *uint) (*models.Database, error)

PrepareDatabase reserves a logical database (record + scoped credentials) on an instance WITHOUT running DDL, so a caller can wire an application's connection env before the instance has finished provisioning. The actual CREATE DATABASE/USER runs later, when the instance comes up (applyPendingDatabases in RunProvision). For an already-running instance use CreateDatabase, which creates it immediately.

func (*Service) Provision

func (s *Service) Provision(ctx context.Context, workspaceID, serverID uint, name string, engine models.DBEngine, version string, volumeSizeBytes int64, meta, annotations models.Metadata) (*models.DatabaseInstance, error)

Provision creates the instance record (admin credentials and connection details known up front) and enqueues the container bring-up to the worker.

func (*Service) RecreateDatabase

func (s *Service) RecreateDatabase(ctx context.Context, inst *models.DatabaseInstance, db *models.Database) error

RecreateDatabase drops and recreates a logical database (and its user) with the same credentials — a clean slate for a force restore. Satisfies the backup service's DDLRunner.

func (*Service) Reencrypt

func (s *Service) Reencrypt(ctx context.Context, workspaceID uint) (int, error)

Reencrypt re-encrypts the workspace's database secrets (instance admin passwords + libSQL keys, and each logical database's password) under the workspace's active DEK. Idempotent; returns the number rewritten.

func (*Service) ResolveDependency

func (s *Service) ResolveDependency(ctx context.Context, workspaceID, serverID, pinInstance uint, base, declName string, engine models.DBEngine, version string, placement Placement, meta models.Metadata) (*models.DatabaseInstance, *models.Database, ConnectionInfo, bool, error)

ResolveDependency satisfies a database dependency per its placement. It returns the hosting instance, the app-scoped logical database (nil for Redis/libSQL or the admin-connection fallback), a connection for the app to use, and whether a new instance was provisioned.

declName, when non-empty, is stamped on the logical database (with the managed-by / gitops-source provenance from meta) so a declarative reconcile can map the manifest Database name back to this exact logical database — rather than guessing by instance name or "first database on the instance". Marketplace installs pass an empty declName (their app env is materialized at install time, not reconciled).

pinInstance, when non-zero, forces a logical database onto that specific instance (the install wizard's explicit choice), ignoring placement.

Newly provisioned instances come up asynchronously, so their logical database is reserved (PrepareDatabase, deferred DDL); reused running instances get the database created immediately (CreateDatabase). Redis and libSQL host no user-managed logical databases, so the app receives the instance connection.

func (*Service) Restart

func (s *Service) Restart(ctx context.Context, inst *models.DatabaseInstance) error

Restart restarts the instance container and marks it running.

func (*Service) RunProvision

func (s *Service) RunProvision(ctx context.Context, instanceID uint) error

RunProvision performs the container bring-up for an instance (worker-invoked).

func (*Service) RunUpgradeJob

func (s *Service) RunUpgradeJob(ctx context.Context, instanceID uint, target, path string, stopApps bool) error

RunUpgradeJob executes a queued upgrade end-to-end (worker-invoked). It is panic-safe and always returns nil — failures are recorded on persisted progress rather than retried, since a half-applied upgrade must not be blindly re-run.

func (*Service) SetAppController

func (s *Service) SetAppController(a AppController)

SetAppController wires the app stop/start used to quiesce writers during an upgrade (nil-safe: when unset, apps are never auto-stopped).

func (*Service) SetEventBus

func (s *Service) SetEventBus(b *eventbus.Bus)

SetEventBus wires the in-process bus used to stream live instance status over SSE. Inject the SAME bus into the HTTP-facing and embedded-worker services so worker-driven phase changes reach an open stream (nil-safe).

func (*Service) SetImageResolver

func (s *Service) SetImageResolver(r ImageResolver)

SetImageResolver wires the deployment-config resolver used to pin engine images at provision time. Optional (request-side; the worker uses the pinned image).

func (*Service) SetLogicalBackup

func (s *Service) SetLogicalBackup(b LogicalBackup)

SetLogicalBackup wires the dump/restore engine used by the copy (major) upgrade path and to take a safety backup (nil-safe).

func (*Service) SetNetworkProvider

func (s *Service) SetNetworkProvider(p NetworkProvider)

SetNetworkProvider wires the resolver for workspace networks, where databases run alongside the workspace's applications. Optional (request-side): when unset, databases fall back to the shared gateway network.

func (*Service) SetNodeGuard

func (s *Service) SetNodeGuard(g NodeGuard)

SetNodeGuard wires the placement guard consulted when provisioning on a node.

func (*Service) SetOwner

func (s *Service) SetOwner(workspaceID, id uint, kind string, ownerID uint, name string) error

SetOwner records the owning resource (app/stack/user) on an existing instance's metadata. Used to back-link a template's database to the app/stack it backs once those have been created. Built-in keys are authoritative, so this overrides any prior owner.

func (*Service) SetOwnerExister

func (s *Service) SetOwnerExister(fn OwnerExister)

SetOwnerExister wires the owner-existence check used by Delete (nil-safe).

func (*Service) SetQuota

func (s *Service) SetQuota(q *quota.Service)

SetQuota wires the plan/quota enforcer (nil-safe; nil skips checks).

func (*Service) SetSecrets

func (s *Service) SetSecrets(w SecretWriter)

SetSecrets wires the Vault writer that auto-provisions a database's connection secrets and cascades their deletion. Optional (request-side only).

func (*Service) SetServerInfo

func (s *Service) SetServerInfo(si ServerInfo)

SetServerInfo wires the resolver used to annotate instances with their node's display name on read paths.

func (*Service) Start

func (s *Service) Start(ctx context.Context, inst *models.DatabaseInstance) error

Start (re)starts a stopped instance container and marks it running.

func (*Service) Stop

func (s *Service) Stop(ctx context.Context, inst *models.DatabaseInstance) error

Stop stops the instance container and marks it stopped. The data volume and records are retained, so the instance can be started again later.

func (*Service) StreamLogs

func (s *Service) StreamLogs(ctx context.Context, workspaceID, instanceID uint, follow bool, tail string, sink func(docker.LogLine) error) error

StreamLogs follows the instance container's logs, invoking sink for each line. tail bounds the initial backlog ("" = a sensible default / all). When follow is true it then follows live output until the context is cancelled; when false it returns after the tail.

func (*Service) StreamStatus

func (s *Service) StreamStatus(ctx context.Context, inst *models.DatabaseInstance, send func(eventbus.Event) error) error

StreamStatus sends the instance's current status, then live updates, until the context is cancelled (client disconnect). It backs the detail page's SSE. When no bus is wired it sends the initial snapshot and holds the connection open (the client still has its REST fallback).

func (*Service) StreamWorkspaceStatus

func (s *Service) StreamWorkspaceStatus(ctx context.Context, workspaceID uint, send func(eventbus.Event) error) error

StreamWorkspaceStatus streams lifecycle status changes for every instance in a workspace until the context is cancelled (client disconnect). It backs the Databases list's live updates, delivering {id,status} deltas. The list seeds its initial state via the REST list, so no snapshot is sent here. When no bus is wired the connection is held open and the client falls back to its periodic reconcile.

func (*Service) SyncSizes

func (s *Service) SyncSizes(ctx context.Context, inst *models.DatabaseInstance) error

SyncSizes refreshes the on-disk size of an instance and its logical databases by querying the engine. Requires the instance to be running.

func (*Service) UniqueDatabaseName

func (s *Service) UniqueDatabaseName(instanceID uint, base string) (string, error)

UniqueDatabaseName derives a sanitized logical-database name from base that is free on the instance, appending _2, _3, … only on collision. This lets callers (e.g. marketplace installs) use a meaningful, readable name — the install's name — without ever hitting ErrNameTaken.

func (*Service) Upgrade

func (s *Service) Upgrade(ctx context.Context, workspaceID, instanceID uint, target string, stopApps bool) (*models.DatabaseInstance, error)

Upgrade validates a version upgrade, marks the instance "upgrading", and enqueues the work onto the asynq worker (durable across restarts). Callers poll Get for the persisted progress.

stopApps quiesces the apps using the database before the upgrade and restarts them after — strongly recommended for a major (copy) upgrade to avoid losing writes made during the dump.

func (*Service) UpgradeOptions

func (s *Service) UpgradeOptions(workspaceID, instanceID uint) (*UpgradeOptions, error)

UpgradeOptions returns the suggested targets and affected apps for an instance.

type StatusEvent

type StatusEvent struct {
	Status  models.DBStatus         `json:"status"`
	Upgrade *models.UpgradeProgress `json:"upgrade,omitempty"`
}

StatusEvent is the snapshot pushed on the "status" stream: the instance's lifecycle status and any in-flight upgrade progress. The detail page replaces its state from this, so a status/phase change needs no extra request.

type UpgradeOptions

type UpgradeOptions struct {
	CurrentVersion string          `json:"current_version"`
	Engine         models.DBEngine `json:"engine"`
	Suggestions    []string        `json:"suggestions"`      // curated newer versions (hints; any version is accepted)
	AffectedAppIDs []uint          `json:"affected_app_ids"` // apps that would be stopped for a copy upgrade
}

UpgradeOptions describes what an instance can be upgraded to.

type UpgradePlan

type UpgradePlan struct {
	FromVersion    string `json:"from_version"`
	ToVersion      string `json:"to_version"`
	Path           string `json:"path"`  // in-place | dump-restore
	Major          bool   `json:"major"` // crosses a major version
	AffectedAppIDs []uint `json:"affected_app_ids"`
}

UpgradePlan is the resolved effect of upgrading to a specific target version.

type WorkspaceStatusEvent

type WorkspaceStatusEvent struct {
	ID      uint                    `json:"id"`
	Status  models.DBStatus         `json:"status"`
	Upgrade *models.UpgradeProgress `json:"upgrade,omitempty"`
}

WorkspaceStatusEvent is StatusEvent plus the instance id, so a workspace-wide subscriber (the list page) knows which row changed.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL