takeover

package
v0.8.6 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

Documentation

Overview

Package takeover moves a server's ports 80 and 443 from the proxy of the platform it is migrating off (Coolify's Traefik, a Caddy or nginx container) to this control plane's own ingress, with a recorded way back. It only acts through the Docker Engine API wrappers, the settings store and the existing restart: there is no host shell and no file writer here by construction.

Index

Constants

View Source
const (
	BootNone     = "none"
	BootApply    = "apply"
	BootRollback = "rollback"
)

Boot actions.

View Source
const (
	DefaultTargetHTTP  = ":80"
	DefaultTargetHTTPS = ":443"
	// DefaultWindow is how long a takeover may stay unverified before it rolls back by itself.
	DefaultWindow = 5 * time.Minute
	// PlanPath is the API path a re-check fix points at.
	PlanPath = "/api/v1/migration/takeover/plan"
)

Default listen addresses a takeover moves the ingress to.

View Source
const (
	StatePreparing   = "preparing"
	StateApplying    = "applying"
	StateVerifying   = "verifying"
	StateDone        = "done"
	StateRollingBack = "rolling_back"
	StateRolledBack  = "rolled_back"
	StateFailed      = "failed"
)

Run states.

View Source
const (
	StatusPass  = "pass"
	StatusWarn  = "warn"
	StatusBlock = "block"
)

Check statuses.

View Source
const (
	VerdictReady   = "ready"
	VerdictWarn    = "warnings"
	VerdictBlocked = "blocked"
	// VerdictDone means this server already listens on the target ports.
	VerdictDone = "done"
)

Plan verdicts.

View Source
const (
	StepPending = "pending"
	StepRunning = "running"
	StepDone    = "done"
	StepFailed  = "failed"
	StepSkipped = "skipped"
)

Step states.

View Source
const (
	StepStopProxy = "stop_old_proxy"
	StepTLS       = "tls_settings"
	StepListen    = "listen_addresses"
	StepRestart   = "restart"
	StepVerify    = "verify"
	StepRollback  = "rollback_record"
	StepRestore   = "rollback"
)

Step ids, in plan order.

View Source
const (
	CheckPorts      = "ports"
	CheckHealth     = "apps_healthy"
	CheckDNS        = "dns"
	CheckCerts      = "certificates"
	CheckACME       = "acme"
	CheckConflict   = "duplicate_domain"
	CheckUnmanaged  = "unmanaged_domains"
	CheckResources  = "resources"
	CheckRestart    = "restart"
	CheckDocker     = "docker"
	CheckSnippet    = "saved_not_enforced"
	CheckPlatform   = "platform_restarts_proxy"
	CheckRestartPol = "restart_policy"
	CheckSocket     = "socket_activation"
)

Check ids.

View Source
const (
	ActionRequest = "request"
	ActionLink    = "link"
	ActionCopy    = "copy"
)

Fix action kinds.

Variables

View Source
var (
	ErrBlocked         = errors.New("takeover: the preflight has blocking checks")
	ErrNotRollbackable = errors.New("takeover: nothing to roll back")
	ErrNotFound        = errors.New("takeover: run not found")
)

Errors a caller maps to a status.

Functions

func TraefikHosts

func TraefikHosts(labels map[string]string) []string

TraefikHosts returns the hostnames a container's Traefik router labels claim, sorted and without duplicates. Regex and wildcard rules are skipped.

Types

type Action

type Action struct {
	Kind  string `json:"kind"`
	Label string `json:"label"`
	API   string `json:"api,omitempty"`
	Value string `json:"value,omitempty"`
}

Action is the one primary thing a fix offers.

type BootDecision

type BootDecision struct {
	Action string
	Reason string
	Run    Run
}

BootDecision is what the process does with a stored run before it binds.

func DecideBoot

func DecideBoot(run Run) BootDecision

DecideBoot maps a stored run to a boot action. A run only gets one boot with the new addresses before verification ends it: a second start while it is still unverified means the first one failed, so the old addresses return.

func PrepareBoot

func PrepareBoot(ctx context.Context, st Store, now time.Time) (BootDecision, error)

PrepareBoot loads the latest unfinished or finished-active run, records the decision (a boot counted, or the rollback intent) and returns it. A nil store or no run is BootNone.

type Check

type Check struct {
	ID     string   `json:"id"`
	Title  string   `json:"title"`
	Status string   `json:"status"`
	Detail string   `json:"detail,omitempty"`
	Items  []string `json:"items,omitempty"`
	Fix    *Fix     `json:"fix,omitempty"`
}

Check is one preflight item.

type Config

type Config struct {
	Window         time.Duration
	VerifyInterval time.Duration
	StartAttempts  int
	StartWait      time.Duration
	Now            func() time.Time
}

Config bounds a run. Zero values use the defaults.

type Containers

type Containers interface {
	Stop(ctx context.Context, name string) error
	Start(ctx context.Context, name string) error
}

Containers starts and stops the old proxy through the Docker Engine API.

type DBStore

type DBStore struct{ DB Rows }

DBStore persists runs in the control plane database.

func (DBStore) Create

func (s DBStore) Create(ctx context.Context, r Run) error

Create implements Store.

func (DBStore) Get

func (s DBStore) Get(ctx context.Context, id string) (Run, error)

Get implements Store.

func (DBStore) List

func (s DBStore) List(ctx context.Context, limit int) ([]Run, error)

List implements Store.

func (DBStore) ListByState

func (s DBStore) ListByState(ctx context.Context, states ...string) ([]Run, error)

ListByState implements Store.

func (DBStore) Save

func (s DBStore) Save(ctx context.Context, r Run) error

Save implements Store.

type Deps

type Deps struct {
	Containers Containers
	Settings   Settings
	Verifier   Verifier
	// Domains lists what verification must cover.
	Domains func(ctx context.Context) ([]string, error)
	// Restart asks the process to restart itself, false when it cannot.
	Restart func() bool
	// Audit records one entry; nil records nothing.
	Audit func(ctx context.Context, action, detail string)
}

Deps is everything a Runner acts through.

type DomainCheck

type DomainCheck struct {
	Domain    string `json:"domain"`
	OK        bool   `json:"ok"`
	Status    int    `json:"status,omitempty"`
	Delivered bool   `json:"delivered"`
	Detail    string `json:"detail,omitempty"`
}

DomainCheck is the outcome of verifying one domain over real HTTPS.

type DomainFact

type DomainFact struct {
	Domain       string
	App          string
	Healthy      bool
	HealthDetail string
	ResolvesTo   []string
	DNSErr       string
	CustomCert   bool
}

DomainFact is what the preflight learned about one domain.

type DomainRow

type DomainRow struct {
	Domain  string `json:"domain"`
	App     string `json:"app"`
	Healthy bool   `json:"healthy"`
	Detail  string `json:"detail,omitempty"`
	DNS     string `json:"dns"`
	Cert    string `json:"certificate"`
}

DomainRow is one domain the takeover has to keep serving.

type Facts

type Facts struct {
	Holders       []Holder
	HostListeners []int
	// HostProcess names the process per host listener port when known.
	HostProcess      map[int]string
	CurrentHTTP      string
	CurrentHTTPS     string
	TargetHTTP       string
	TargetHTTPS      string
	Domains          []DomainFact
	ServerIPs        []string
	LabelRouters     []LabelRouter
	OldDomains       []string
	ACMEEnabled      bool
	DiskFreeGB       float64
	DiskKnown        bool
	MemoryMB         int
	MinDiskGB        float64
	MinMemoryMB      int
	RestartSupported bool
	DockerAvailable  bool
	AcceptUnmanaged  bool
	// SocketActivation is true when systemd holds the ingress sockets.
	SocketActivation bool
	Window           time.Duration
	Now              time.Time
}

Facts is everything BuildPlan decides from. The caller gathers it, so the rules stay pure and table-testable.

type Fix

type Fix struct {
	Summary string  `json:"summary"`
	Action  *Action `json:"action,omitempty"`
}

Fix is the remedy for a warning or blocking check.

type Holder

type Holder struct {
	Port          int
	Container     string
	Image         string
	Kind          string
	RestartPolicy string
}

Holder is a running container publishing host port 80 or 443.

type LabelRouter

type LabelRouter struct {
	Domain    string
	Container string
}

LabelRouter is a Traefik label on a container that claims a domain.

type Plan

type Plan struct {
	Verdict string      `json:"verdict"`
	Checks  []Check     `json:"checks"`
	Steps   []PlanStep  `json:"steps"`
	Domains []DomainRow `json:"domains"`
	// Rollback says what the undo button restores.
	Rollback      []string `json:"rollback"`
	WindowSeconds int      `json:"window_seconds"`
	TargetHTTP    string   `json:"target_http"`
	TargetHTTPS   string   `json:"target_https"`
	CurrentHTTP   string   `json:"current_http"`
	CurrentHTTPS  string   `json:"current_https"`
	ProxyKind     string   `json:"proxy_kind,omitempty"`
	ProxyNames    []string `json:"proxy_containers"`
	CheckedAt     string   `json:"checked_at"`
}

Plan is the preflight result plus the exact steps a run would take.

func BuildPlan

func BuildPlan(f Facts) Plan

BuildPlan runs the preflight and lists the exact steps.

type PlanStep

type PlanStep struct {
	ID     string `json:"id"`
	Title  string `json:"title"`
	Detail string `json:"detail"`
	// Reversible is false only for steps rollback cannot undo on its own.
	Reversible bool `json:"reversible"`
}

PlanStep is one thing a run does, in order.

type Previous

type Previous struct {
	HTTPAddr              string  `json:"http_addr"`
	HTTPSAddr             string  `json:"https_addr"`
	TLSTerminatedUpstream bool    `json:"tls_terminated_upstream"`
	PublicHTTPSPort       int     `json:"public_https_port"`
	SettingsChanged       bool    `json:"settings_changed"`
	Proxies               []Proxy `json:"proxies"`
}

Previous is everything rollback restores. It holds no secrets.

type Proxy

type Proxy struct {
	Name       string `json:"name"`
	Image      string `json:"image"`
	WasRunning bool   `json:"was_running"`
	Stopped    bool   `json:"stopped"`
	Restored   bool   `json:"restored"`
}

Proxy is one container the run stopped, so rollback starts exactly it.

type Rows

type Rows interface {
	CreatePortTakeover(ctx context.Context, r store.PortTakeover) error
	SavePortTakeover(ctx context.Context, r store.PortTakeover) error
	GetPortTakeover(ctx context.Context, id string) (store.PortTakeover, error)
	ListPortTakeovers(ctx context.Context, limit int) ([]store.PortTakeover, error)
	ListPortTakeoversByState(ctx context.Context, states ...string) ([]store.PortTakeover, error)
}

Rows is the control plane database surface DBStore needs; *store.DB provides it.

type Run

type Run struct {
	ID            string        `json:"id"`
	State         string        `json:"state"`
	Actor         string        `json:"actor,omitempty"`
	Previous      Previous      `json:"previous"`
	TargetHTTP    string        `json:"target_http"`
	TargetHTTPS   string        `json:"target_https"`
	Steps         []Step        `json:"steps"`
	Verify        []DomainCheck `json:"verify"`
	Error         string        `json:"error,omitempty"`
	Boots         int           `json:"boots"`
	WindowSeconds int           `json:"window_seconds"`
	DeadlineAt    time.Time     `json:"deadline_at,omitzero"`
	CreatedAt     time.Time     `json:"created_at"`
	UpdatedAt     time.Time     `json:"updated_at"`
	FinishedAt    time.Time     `json:"finished_at,omitzero"`
}

Run is one takeover and, when it fails or is undone, its rollback.

func (Run) Active

func (r Run) Active() bool

Active reports whether the run's addresses are the ones this process must listen on at start.

func (Run) InFlight

func (r Run) InFlight() bool

InFlight reports whether the run is still moving by itself.

func (Run) Rollbackable

func (r Run) Rollbackable() bool

Rollbackable reports whether the undo button still does something.

type Runner

type Runner struct {
	Store Store
	Deps  Deps
	Cfg   Config
	Log   *slog.Logger
	// contains filtered or unexported fields
}

Runner drives takeovers and their rollbacks.

func (*Runner) Get

func (r *Runner) Get(ctx context.Context, id string) (Run, error)

Get returns one run.

func (*Runner) List

func (r *Runner) List(ctx context.Context, limit int) ([]Run, error)

List returns runs newest first.

func (*Runner) Resume

func (r *Runner) Resume(ctx context.Context)

Resume continues whatever a restart interrupted. Call it once after the router is built; it returns when the work is finished or ctx ends.

func (*Runner) Rollback

func (r *Runner) Rollback(ctx context.Context, id, reason, actor string) (Run, error)

Rollback undoes a takeover that is applying, verifying or done: it records the intent and restarts so the old addresses apply; Resume then starts the old proxy. Repeating it on a run already rolling back or rolled back is a no-op.

func (*Runner) Start

func (r *Runner) Start(ctx context.Context, req StartRequest) (run Run, created bool, err error)

Start runs the first half of a takeover and returns once the restart is requested. created is false when an earlier run is already in flight or finished, which makes a repeated request a no-op.

type Settings

type Settings interface {
	GetTLS(ctx context.Context) (TLSSettings, error)
	SetTLS(ctx context.Context, s TLSSettings) error
}

Settings reads and writes those settings.

type StartRequest

type StartRequest struct {
	Plan  Plan
	Actor string
}

StartRequest is what a run needs from its plan.

type Step

type Step struct {
	ID         string    `json:"id"`
	Title      string    `json:"title"`
	State      string    `json:"state"`
	Detail     string    `json:"detail,omitempty"`
	StartedAt  time.Time `json:"started_at,omitzero"`
	FinishedAt time.Time `json:"finished_at,omitzero"`
}

Step is one timed entry of a run.

type Store

type Store interface {
	Create(ctx context.Context, r Run) error
	Get(ctx context.Context, id string) (Run, error)
	Save(ctx context.Context, r Run) error
	List(ctx context.Context, limit int) ([]Run, error)
	ListByState(ctx context.Context, states ...string) ([]Run, error)
}

Store persists runs.

type TLSSettings

type TLSSettings struct {
	TerminatedUpstream bool
	PublicHTTPSPort    int
}

TLSSettings is the part of the ingress settings a takeover flips.

type Verifier

type Verifier interface {
	Verify(ctx context.Context, domain string) DomainCheck
}

Verifier requests one domain over real HTTPS through this server's ingress.

Jump to

Keyboard shortcuts

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