Некоторые проверки не удались
CI / test (push) Successful in 2m5s
Docker / Build and publish worker image (push) Failing after 31s
241 строка
8.4 KiB
Go
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
|
|
}
|