cutover

package
v0.5.0 Latest Latest
Warning

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

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

Documentation

Overview

Package cutover plans and runs a guided, reversible move of one staged import app: readiness rules, a persisted state machine, DNS undo data. It has no access to the source platform by construction: the only things it can touch are the app here, this node's ingress and the domain's DNS.

Index

Constants

View Source
const (
	ImageRegistry  = "registry"
	ImageHostBuilt = "host_built"
	ImageGit       = "git"
)

Image sources an app can start from.

View Source
const (
	ItemVerified = "verified"
	ItemRouted   = "routed"
)

Item states Facts.ItemState carries (the importer's own vocabulary).

View Source
const (
	StepLinkImages  = "images"
	StepLinkVerify  = "verify"
	StepLinkVolumes = "volumes"
	StepLinkStage   = "stage"
)

Wizard steps a fix can link to.

View Source
const (
	StatePlanning   = "planning"
	StateReady      = "ready"
	StateStarting   = "starting"
	StateVerifying  = "verifying"
	StateSwitching  = "switching"
	StateLive       = "live"
	StateRolledBack = "rolled_back"
	StateFailed     = "failed"
)

Run states.

View Source
const (
	ModeDryRun = "dry_run"
	ModeSwitch = "switch"
)

Run modes.

View Source
const (
	MethodDNS    = "dns"
	MethodProxy  = "proxy"
	MethodManual = "manual"
	// MethodNone means the domain already resolves here, nothing to change.
	MethodNone  = "none"
	MethodMixed = "mixed"
)

How a domain's traffic is switched.

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

Check statuses.

View Source
const (
	VerdictReady   = "ready"
	VerdictWarn    = "warnings"
	VerdictBlocked = "blocked"
)

Plan verdicts.

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

Step states.

View Source
const (
	StepReadiness   = "readiness"
	StepRoute       = "route"
	StepHealthy     = "healthy"
	StepIngress     = "ingress_probe"
	StepDNSPreview  = "dns_preview"
	StepSwitch      = "switch"
	StepPostVerify  = "post_verify"
	StepCleanup     = "cleanup"
	StepRollback    = "rollback"
	StepManualDNS   = "manual_dns"
	StepResumeCheck = "resume_check"
)

Step names in the run timeline.

View Source
const (
	CheckImage    = "image"
	CheckEnv      = "env"
	CheckDatabase = "database"
	CheckVolumes  = "volumes"
	CheckHealth   = "health"
	CheckDomains  = "domains"
	CheckState    = "state"
)

Check ids in a readiness plan.

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

Action kinds a fix can carry.

View Source
const AwaitingManualDNS = "manual_dns"

AwaitingManualDNS is what a paused run waits for: the operator's record change.

Variables

This section is empty.

Functions

func NewRunID

func NewRunID() (string, error)

NewRunID returns a fresh run id.

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 Check

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

Check is one readiness item.

type Config

type Config struct {
	HealthTimeout  time.Duration
	ProbeTimeout   time.Duration
	ProbeInterval  time.Duration
	VerifyTimeout  time.Duration
	VerifyInterval time.Duration
	// ResumeMaxAge is how old an interrupted run may be and still be resumed
	// forward after a restart. Older ones are rolled back instead.
	ResumeMaxAge time.Duration
}

Config bounds each wait in a run.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig is the production set of bounds.

type DBFact

type DBFact struct {
	Name string
	Host string
	// Target is the database here the data was moved to, "" when none.
	Target string
	// Verified is true when a recorded copy compared row counts.
	Verified   bool
	Checked    int
	Mismatched int
	// External is true when the database lives outside the source platform
	// and needs no copy.
	External bool
}

DBFact is one database the app points at.

type DBStore

type DBStore struct{ DB RunRows }

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, app string, 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 DNS

type DNS interface {
	Apply(ctx context.Context, domain string) (DNSResult, error)
	Restore(ctx context.Context, domain string, previous []Record, applied *Record) error
}

DNS changes a domain's record through a connected provider, or restores it.

type DNSResult

type DNSResult struct {
	Applied  *Record
	Previous []Record
	Provider string
	Zone     string
	Message  string
}

DNSResult is what an applied record change did, with its undo data.

type Deps

type Deps struct {
	Planner  Planner
	Host     Host
	Ingress  Ingress
	DNS      DNS
	Proxy    Proxy
	Verifier Verifier
}

Deps is everything a Runner acts through. There is deliberately no handle on the source platform here.

type DomainFact

type DomainFact struct {
	Domain string
	// Current is what the domain resolves to now (addresses or CNAME).
	Current []string
	// PointsHere is true when it already resolves to this server.
	PointsHere bool
	// TakenBy is another app already holding the domain.
	TakenBy string
	Plan    DomainPlan
}

DomainFact is the DNS side of one domain.

type DomainPlan

type DomainPlan struct {
	Domain string `json:"domain"`
	Method string `json:"method"`
	// Current is what the domain resolves to or the zone holds now.
	Current  []string `json:"current,omitempty"`
	Provider string   `json:"provider,omitempty"`
	Zone     string   `json:"zone,omitempty"`
	Desired  *Record  `json:"desired,omitempty"`
	// Replace lists the provider records a DNS switch removes first.
	Replace []Record `json:"replace,omitempty"`
	Message string   `json:"message,omitempty"`
}

DomainPlan is what switching one domain involves.

type DomainRun

type DomainRun struct {
	Domain string `json:"domain"`
	Method string `json:"method"`
	// Previous is what rollback restores, written before the switch.
	Previous []Record `json:"previous,omitempty"`
	Applied  *Record  `json:"applied,omitempty"`
	Provider string   `json:"provider,omitempty"`
	Zone     string   `json:"zone,omitempty"`
	// Manual is the exact record to set by hand.
	Manual       *Record `json:"manual,omitempty"`
	ProbeStatus  int     `json:"probe_status,omitempty"`
	ProbeDetail  string  `json:"probe_detail,omitempty"`
	Switched     bool    `json:"switched"`
	Restored     bool    `json:"restored"`
	Verified     bool    `json:"verified"`
	VerifyDetail string  `json:"verify_detail,omitempty"`
	Message      string  `json:"message,omitempty"`
}

DomainRun is one domain's progress and undo data inside a run.

type Facts

type Facts struct {
	App       string
	ItemState string

	ImageSource   string
	Image         string
	ImageVerified bool
	ImageDetail   string

	EnvPlain, EnvSecret, EnvEmptySecrets int

	Databases []DBFact
	Volumes   []VolumeFact

	HealthPath  string
	HealthKnown bool

	Domains []DomainFact
	Now     time.Time
}

Facts is everything the readiness rules read. It carries no secret value.

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 Host

type Host interface {
	// Route attaches the domains to the app here and starts it.
	Route(ctx context.Context, app string, domains []string) error
	// Unroute detaches the domains and stops the app again.
	Unroute(ctx context.Context, app string, domains []string) error
	// WaitHealthy returns false with a reason when the app is not ready in time.
	WaitHealthy(ctx context.Context, app string, timeout time.Duration) (ok bool, detail string, err error)
}

Host starts and stops the staged app here and waits for it.

type Ingress

type Ingress interface {
	Probe(ctx context.Context, domain, path string) (ProbeResult, error)
}

Ingress sends a request with the domain's Host header through the real ingress listener of this node, whatever DNS currently says.

type Plan

type Plan struct {
	App       string       `json:"app"`
	SessionID string       `json:"session_id,omitempty"`
	SourceID  string       `json:"source_id,omitempty"`
	Verdict   string       `json:"verdict"`
	Checks    []Check      `json:"checks"`
	Domains   []DomainPlan `json:"domains"`
	// HealthPath is probed through the ingress, "/" when none is defined.
	HealthPath string `json:"health_path"`
	CheckedAt  string `json:"checked_at"`
}

Plan is the readiness checklist for one app.

func Evaluate

func Evaluate(f Facts) Plan

Evaluate applies the readiness rules. It is pure: the same Facts always give the same Plan, so every rule has a table test.

type PlanRequest

type PlanRequest struct {
	SessionID string
	SourceID  string
	App       string
	// DNSWrite is whether the caller may change DNS records.
	DNSWrite bool
}

PlanRequest identifies the staged app a plan or run is about.

type Planner

type Planner interface {
	Plan(ctx context.Context, req PlanRequest) (Plan, error)
}

Planner gathers the facts for one staged app and returns its readiness plan.

type ProbeResult

type ProbeResult struct {
	OK     bool
	Status int
	Detail string
}

ProbeResult is one request through this node's own ingress.

type Proxy

type Proxy interface {
	WriteRoute(ctx context.Context, domain string) error
	RemoveRoute(ctx context.Context, domain string) error
}

Proxy writes and removes the managed proxy route of a domain.

type Record

type Record struct {
	Name  string `json:"name"`
	Type  string `json:"type"`
	Value string `json:"value"`
	TTL   int    `json:"ttl_seconds,omitempty"`
}

Record is one DNS record as the operator sees it.

type Run

type Run struct {
	ID         string      `json:"id"`
	SessionID  string      `json:"session_id"`
	SourceID   string      `json:"source_id"`
	App        string      `json:"app"`
	Mode       string      `json:"mode"`
	State      string      `json:"state"`
	Method     string      `json:"method,omitempty"`
	DNSWrite   bool        `json:"dns_write"`
	WasRouted  bool        `json:"was_routed"`
	AcceptWarn bool        `json:"accept_warnings"`
	Awaiting   string      `json:"awaiting,omitempty"`
	Domains    []DomainRun `json:"domains"`
	Steps      []Step      `json:"steps"`
	Plan       *Plan       `json:"plan,omitempty"`
	Error      string      `json:"error,omitempty"`
	CreatedAt  time.Time   `json:"created_at"`
	UpdatedAt  time.Time   `json:"updated_at"`
	FinishedAt time.Time   `json:"finished_at,omitzero"`
}

Run is one dry run or switch of one app.

func (Run) InFlight

func (r Run) InFlight() bool

InFlight reports whether a worker is expected to be driving the run.

func (Run) Rollbackable

func (r Run) Rollbackable() bool

Rollbackable reports whether a rollback can still do something.

func (Run) Terminal

func (r Run) Terminal() bool

Terminal reports whether the run can no longer change by itself.

type RunRows

type RunRows interface {
	CreateCutoverRun(ctx context.Context, r store.CutoverRun) error
	SaveCutoverRun(ctx context.Context, r store.CutoverRun) error
	GetCutoverRun(ctx context.Context, id string) (store.CutoverRun, error)
	ListCutoverRuns(ctx context.Context, app string, limit int) ([]store.CutoverRun, error)
	ListCutoverRunsByState(ctx context.Context, states ...string) ([]store.CutoverRun, error)
}

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

type Runner

type Runner struct {
	Store Store
	Deps  Deps
	Cfg   Config
	Log   *slog.Logger
	Now   func() time.Time
	Sleep func(ctx context.Context, d time.Duration) error
	// Hook runs after each persisted step. A non-nil error aborts Execute
	// with no cleanup, which is how tests simulate a process crash.
	Hook func(run Run, step string) error
}

Runner drives runs through the state machine. Every step is idempotent and persisted, so a run interrupted at any point resumes or rolls back.

func (*Runner) AcknowledgeManual

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

AcknowledgeManual records that the operator changed the DNS record by hand and persists it, without continuing the run.

func (*Runner) ConfirmManual

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

ConfirmManual acknowledges the manual change, then continues with the post-switch verification.

func (*Runner) Execute

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

Execute drives run id forward until it reaches a resting state: ready (dry run done), live, rolled_back, failed, or paused awaiting a manual DNS change. It returns the run as last persisted.

func (*Runner) Recover

func (r *Runner) Recover(ctx context.Context, launch func(id string)) error

Recover resumes or rolls back runs a restart interrupted. launch is called for each run to continue forward; stale ones are rolled back here.

func (*Runner) Rollback

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

Rollback restores every domain this run switched, then stops the staged app. It is safe to call again after a partial failure.

func (*Runner) Start

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

Start validates req and persists a new run in the planning state.

type StartRequest

type StartRequest struct {
	SessionID  string
	SourceID   string
	App        string
	Mode       string
	DNSWrite   bool
	WasRouted  bool
	AcceptWarn bool
}

StartRequest creates a run.

type Step

type Step struct {
	Name       string    `json:"name"`
	State      string    `json:"state"`
	Domain     string    `json:"domain,omitempty"`
	Detail     string    `json:"detail,omitempty"`
	StartedAt  time.Time `json:"started_at"`
	FinishedAt time.Time `json:"finished_at,omitzero"`
	DurationMS int64     `json:"duration_ms"`
}

Step is one timed entry of a run's timeline.

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, app string, limit int) ([]Run, error)
	ListByState(ctx context.Context, states ...string) ([]Run, error)
}

Store persists runs.

type Verifier

type Verifier interface {
	Verify(ctx context.Context, app, domain string) (ok bool, detail string, err error)
}

Verifier checks a domain through its public name, with the domain doctor.

type VolumeFact

type VolumeFact struct {
	Name   string
	Path   string
	Copied bool
}

VolumeFact is one persistent mount.

Jump to

Keyboard shortcuts

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