Files
worker/app/models/deployment.go
Gleb Tv 2c884c5612
Некоторые проверки не удались
CI / test (push) Successful in 2m5s
Docker / Build and publish worker image (push) Failing after 31s
refactor: adopt worker module path
2026-07-13 17:56:12 +03:00

241 строка
8.4 KiB
Go

package models
import (
"database/sql/driver"
"fmt"
"time"
"github.com/lib/pq"
"gorm.io/datatypes"
"gorm.io/gorm"
"rocketgit.ru/rsmon/worker/app/models/concerns"
)
// DeploymentKind is the rstuff-mirrored lifecycle label for a
// deployment (production / production_prev / production_next /
// internal / staging / old). See
// docs/parity/rstuff-inventory.md §6.1 for the byte-stable numeric
// mapping (which Postgres stores alphabetically, not numerically).
type DeploymentKind string
// Lifecycle labels for a Deployment row. See DeploymentKind for
// the matching rstuff enum. The label set is closed; new values
// require adding a Postgres enum value via app/models/migrate.go.
const (
// DeploymentKindProduction is the customer-facing "live" deployment.
DeploymentKindProduction DeploymentKind = "production"
DeploymentKindProductionPrev DeploymentKind = "production_prev"
DeploymentKindProductionNext DeploymentKind = "production_next"
// DeploymentKindInternal is for ops/admin tooling (not customer-facing).
DeploymentKindInternal DeploymentKind = "internal"
DeploymentKindStaging DeploymentKind = "staging"
DeploymentKindOld DeploymentKind = "old"
)
// DeploymentMode is the host-side lifecycle label (kubernetes /
// compose / dedicated / vds / user). `dedicated` covers a single
// nginx vhost; `compose` covers a Docker Compose project.
type DeploymentMode string
// DeploymentMode values map onto rstuff's Deployment.mode enum.
// `dedicated` covers a single nginx vhost; `compose` covers a
// Docker Compose project; the others are reserved for future
// v2 surfaces (Kubernetes, VDS, user-owned).
const (
DeploymentModeKubernetes DeploymentMode = "kubernetes"
DeploymentModeCompose DeploymentMode = "compose"
DeploymentModeDedicated DeploymentMode = "dedicated"
DeploymentModeVDS DeploymentMode = "vds"
DeploymentModeUser DeploymentMode = "user"
)
// DeploymentAction is the reconciliation state. Updated by the
// deploymentd receiver on every POST and by the 90s reconcile sweep
// (see app/models/deployment.go ReconcileMissing).
type DeploymentAction string
// DeploymentAction values. Pending/PendingMove/PendingDrop are
// transient (operator or receiver-initiated); Deleted/Missing are
// sticky until the deployment shows up again on a future POST.
const (
DeploymentActionOk DeploymentAction = "ok"
DeploymentActionPending DeploymentAction = "pending"
DeploymentActionPendingMove DeploymentAction = "pending_move"
DeploymentActionPendingDrop DeploymentAction = "pending_drop"
DeploymentActionDeleted DeploymentAction = "deleted"
DeploymentActionMissing DeploymentAction = "missing"
)
// Deployment represents a single host-side binding: one nginx
// vhost, one Docker Compose service, or one Kubernetes service. The
// shape mirrors rstuff's `deployments` table. See
// docs/plans/inventory-management.md §4 / §6.1.
type Deployment struct {
concerns.Model
AccountID int64 `gorm:"type:bigint REFERENCES accounts(id);not null;index" json:"account_id"`
Account *Account `json:"-"`
ServerID *int64 `gorm:"type:bigint REFERENCES servers(id) ON DELETE SET NULL;index" json:"server_id,omitempty"`
Server *Server `json:"-"`
SiteID *int64 `gorm:"type:bigint REFERENCES sites(id) ON DELETE SET NULL;index" json:"site_id,omitempty"`
Site *Site `json:"site,omitempty"`
ExtID *string `gorm:"size:64" json:"ext_id,omitempty"`
ServiceName *string `gorm:"size:120" json:"service_name,omitempty"`
Kind DeploymentKind `gorm:"type:deployment_kind;not null;default:'production'" json:"kind"`
Mode DeploymentMode `gorm:"type:deployment_mode;not null;default:'dedicated'" json:"mode"`
Action DeploymentAction `gorm:"type:deployment_action;not null;default:'ok'" json:"action"`
URL *string `gorm:"type:text" json:"url,omitempty"`
SSHUser *string `gorm:"size:64" json:"ssh_user,omitempty"`
RootPath *string `gorm:"type:text" json:"root_path,omitempty"`
ConfigPath *string `gorm:"type:text" json:"config_path,omitempty"`
IP *string `gorm:"type:inet" json:"ip,omitempty"`
Listen pq.StringArray `gorm:"type:varchar(64)[];not null;default:'{}'" json:"listen"`
ServerName pq.StringArray `gorm:"type:varchar(255)[];not null;default:'{}'" json:"server_name"`
Auth bool `gorm:"not null;default:false" json:"auth"`
IsProxied bool `gorm:"not null;default:false" json:"is_proxied"`
LastSeenAt *time.Time `json:"last_seen_at,omitempty"`
Meta datatypes.JSON `gorm:"type:jsonb;not null;default:'{}'::jsonb" json:"meta"`
concerns.Timestamped
Audited
}
// TableName provides functionality.
func (Deployment) TableName() string { return "deployments" }
// Scan implements sql.Scanner so a Postgres enum value can land in
// our typed string alias without a code-generation step.
func (d *DeploymentKind) Scan(src any) error {
if src == nil {
*d = ""
return nil
}
switch v := src.(type) {
case string:
*d = DeploymentKind(v)
case []byte:
*d = DeploymentKind(string(v))
default:
return fmt.Errorf("deployment_kind: cannot scan %T", src)
}
return nil
}
// Value implements driver.Valuer for the inverse direction.
func (d DeploymentKind) Value() (driver.Value, error) {
if d == "" {
return nil, nil
}
return string(d), nil
}
// Scan implements sql.Scanner for DeploymentMode.
func (d *DeploymentMode) Scan(src any) error {
if src == nil {
*d = ""
return nil
}
switch v := src.(type) {
case string:
*d = DeploymentMode(v)
case []byte:
*d = DeploymentMode(string(v))
default:
return fmt.Errorf("deployment_mode: cannot scan %T", src)
}
return nil
}
// Value implements driver.Valuer for DeploymentMode.
func (d DeploymentMode) Value() (driver.Value, error) {
if d == "" {
return nil, nil
}
return string(d), nil
}
// Scan implements sql.Scanner for DeploymentAction.
func (d *DeploymentAction) Scan(src any) error {
if src == nil {
*d = ""
return nil
}
switch v := src.(type) {
case string:
*d = DeploymentAction(v)
case []byte:
*d = DeploymentAction(string(v))
default:
return fmt.Errorf("deployment_action: cannot scan %T", src)
}
return nil
}
// Value implements driver.Valuer for DeploymentAction.
func (d DeploymentAction) Value() (driver.Value, error) {
if d == "" {
return nil, nil
}
return string(d), nil
}
// ReconcileMissingDeployments flips action='missing' on every Deployment for the
// given server whose last_seen_at is older than cutoff. Called by
// the deploymentd receiver after every successful upsert so the
// "missing" badge appears within one POST cycle.
//
// Idempotent: re-running with the same cutoff is a no-op.
func ReconcileMissingDeployments(serverID int64, mode DeploymentMode, cutoff time.Time) (int64, error) {
res := DB().Model(&Deployment{}).
Where("server_id = ? AND mode = ? AND action NOT IN ?", serverID, mode,
[]DeploymentAction{DeploymentActionDeleted, DeploymentActionMissing, DeploymentActionPendingDrop}).
Where("last_seen_at IS NULL OR last_seen_at < ?", cutoff).
Update("action", DeploymentActionMissing)
return res.RowsAffected, res.Error
}
// UpsertNginxDeployment finds or creates a Deployment by
// (server_id, config_path) for an nginx vhost. The caller fills in
// the lifecycle fields (listen, server_name, etc.) after the upsert
// returns. The return value is the row to mutate; the caller MUST
// also touch last_seen_at and save.
func UpsertNginxDeployment(tx *gorm.DB, accountID int64, serverID int64, configPath string) (*Deployment, error) {
if tx == nil {
tx = DB()
}
var d Deployment
err := tx.Where("server_id = ? AND config_path = ?", serverID, configPath).First(&d).Error
if err == nil {
return &d, nil
}
if err != gorm.ErrRecordNotFound {
return nil, err
}
d = Deployment{
AccountID: accountID,
ServerID: &serverID,
Kind: DeploymentKindProduction,
Mode: DeploymentModeDedicated,
Action: DeploymentActionOk,
ConfigPath: &configPath,
}
if err := tx.Create(&d).Error; err != nil {
return nil, err
}
return &d, nil
}
// RotateServerToken sets a new random token for a server and returns
// the plaintext. Called by the operator-only
// POST /api/v1/servers/:id/rotate-token endpoint. The plaintext is
// returned exactly once — it is not stored anywhere recoverable.
func RotateServerToken(tx *gorm.DB, serverID int64, newToken string) error {
if tx == nil {
tx = DB()
}
return tx.Model(&Server{}).Where("id = ?", serverID).Update("token", newToken).Error
}