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
- func NewRunID() (string, error)
- type Action
- type Check
- type Config
- type DBFact
- type DBStore
- func (s DBStore) Create(ctx context.Context, r Run) error
- func (s DBStore) Get(ctx context.Context, id string) (Run, error)
- func (s DBStore) List(ctx context.Context, app string, limit int) ([]Run, error)
- func (s DBStore) ListByState(ctx context.Context, states ...string) ([]Run, error)
- func (s DBStore) Save(ctx context.Context, r Run) error
- type DNS
- type DNSResult
- type Deps
- type DomainFact
- type DomainPlan
- type DomainRun
- type Facts
- type Fix
- type Host
- type Ingress
- type Plan
- type PlanRequest
- type Planner
- type ProbeResult
- type Proxy
- type Record
- type Run
- type RunRows
- type Runner
- func (r *Runner) AcknowledgeManual(ctx context.Context, id string) (Run, error)
- func (r *Runner) ConfirmManual(ctx context.Context, id string) (Run, error)
- func (r *Runner) Execute(ctx context.Context, id string) (Run, error)
- func (r *Runner) Recover(ctx context.Context, launch func(id string)) error
- func (r *Runner) Rollback(ctx context.Context, id, reason string) (Run, error)
- func (r *Runner) Start(ctx context.Context, req StartRequest) (Run, error)
- type StartRequest
- type Step
- type Store
- type Verifier
- type VolumeFact
Constants ¶
const ( ImageRegistry = "registry" ImageHostBuilt = "host_built" ImageGit = "git" )
Image sources an app can start from.
const ( ItemVerified = "verified" ItemRouted = "routed" )
Item states Facts.ItemState carries (the importer's own vocabulary).
const ( StepLinkImages = "images" StepLinkVerify = "verify" StepLinkVolumes = "volumes" StepLinkStage = "stage" )
Wizard steps a fix can link to.
const ( StatePlanning = "planning" StateReady = "ready" StateStarting = "starting" StateVerifying = "verifying" StateSwitching = "switching" StateLive = "live" StateRolledBack = "rolled_back" StateFailed = "failed" )
Run states.
const ( ModeDryRun = "dry_run" ModeSwitch = "switch" )
Run modes.
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.
const ( StatusPass = "pass" StatusWarn = "warn" StatusBlock = "block" )
Check statuses.
const ( VerdictReady = "ready" VerdictWarn = "warnings" VerdictBlocked = "blocked" )
Plan verdicts.
const ( StepRunning = "running" StepDone = "done" StepFailed = "failed" StepSkipped = "skipped" )
Step states.
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.
const ( CheckImage = "image" CheckEnv = "env" CheckDatabase = "database" CheckVolumes = "volumes" CheckHealth = "health" CheckDomains = "domains" CheckState = "state" )
Check ids in a readiness plan.
const ( ActionRequest = "request" ActionLink = "link" ActionCopy = "copy" )
Action kinds a fix can carry.
const AwaitingManualDNS = "manual_dns"
AwaitingManualDNS is what a paused run waits for: the operator's record change.
Variables ¶
This section is empty.
Functions ¶
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.
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) ListByState ¶
ListByState 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 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.
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 ¶
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) Rollbackable ¶
Rollbackable reports whether a rollback can still do something.
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 ¶
AcknowledgeManual records that the operator changed the DNS record by hand and persists it, without continuing the run.
func (*Runner) ConfirmManual ¶
ConfirmManual acknowledges the manual change, then continues with the post-switch verification.
func (*Runner) Execute ¶
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 ¶
Recover resumes or rolls back runs a restart interrupted. launch is called for each run to continue forward; stale ones are rolled back here.
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 ¶
VolumeFact is one persistent mount.