feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
Этот коммит содержится в:
240
app/models/deployment.go
Обычный файл
240
app/models/deployment.go
Обычный файл
@@ -0,0 +1,240 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"rsgit.ru/rsmon/rsmon/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
|
||||
}
|
||||
Ссылка в новой задаче
Block a user