deploylifecycle

package
v0.13.0 Latest Latest
Warning

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

Go to latest
Published: Aug 4, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package deploylifecycle owns the deployment record state machine: the set of valid statuses and phases, and the transitions between them.

It exists as its own package because both servers/deployment and servers/build need it, and servers/build importing servers/deployment would be backwards.

Index

Constants

View Source
const DefaultLockTTL = 30 * time.Minute

DefaultLockTTL is how long a lock survives without its holder finishing. It backstops a deploy whose driver died without releasing; a holder that reaches a terminal status is stealable sooner than this.

Variables

View Source
var ErrLockHeld = errors.New("deployment lock held")

ErrLockHeld reports that a live deployment already holds the lock. It is an expected outcome, not a failure — match against it with errors.Is.

Functions

func CheckPhase

func CheckPhase(status Status, phase Phase) error

CheckPhase reports whether a phase may be recorded against a deployment in the given status. Phases describe work still in flight, so they are meaningless once a deployment has left in_progress.

func LockID

func LockID(appName string) entity.Id

LockID is the deterministic entity id for an app's deploy lock. Determinism is what makes create-if-absent a mutual exclusion primitive: every contender computes the same key.

The app name is the sole variable component, so it is used raw. It must not be rewritten (e.g. slashes to underscores): that would let two distinct names collide on one lock. As the last path segment the name is unambiguous even when it contains a slash.

func Transition

func Transition(from, to Status) error

Transition reports whether a deployment may move from one status to another. It is the single authority on that question: every write path goes through it rather than re-deriving the rules.

A rejected transition is a conflict, not a validation failure — the request is well-formed, it just lost a race or arrived against a record that has already finished.

Types

type BeginParams

type BeginParams struct {
	AppName   string
	ClusterID string

	// AppVersion is normally empty: a forward deploy does not know its version
	// until the build produces one. Rollback knows it up front.
	AppVersion string

	GitInfo    core_v1alpha.GitInfo
	DeployedBy core_v1alpha.DeployedBy

	// SourceDeploymentID records what this deployment was derived from, for
	// rollback and redeploy provenance.
	SourceDeploymentID string
}

BeginParams describes a deployment about to start.

type Holder

type Holder struct {
	AppName      string
	DeploymentID string
	AcquiredAt   time.Time
	ExpiresAt    time.Time
	Revision     int64
}

Holder describes the deployment currently holding a lock.

func HolderFrom

func HolderFrom(err error) (*Holder, bool)

HolderFrom extracts the blocking holder from an error returned by Acquire or Begin, reporting false for any other error.

func (*Holder) Expired

func (h *Holder) Expired(now time.Time) bool

Expired reports whether the lock has outlived its TTL.

type LockHeldError

type LockHeldError struct {
	Holder *Holder
}

LockHeldError carries the blocking holder along with the error, so a caller several layers up can render "who is in the way" without re-reading the lock and risking a different answer.

func (*LockHeldError) Error

func (e *LockHeldError) Error() string

func (*LockHeldError) Is

func (e *LockHeldError) Is(target error) bool

type Locks

type Locks struct {
	// contains filtered or unexported fields
}

Locks manages the deploy lock entity for an app.

Acquisition is a compare-and-create against the entity store, so two callers racing to start a deploy cannot both win. That is the whole point of the type: the previous scheme listed in-progress deployments and then created a record, which admitted an interleaving where both callers saw an empty list.

The lock is scoped to the app, not app+cluster: a coordinator's entity store is a loopback into its own etcd, so it only ever holds this cluster's deployments, and the client-supplied cluster_id is unreliable anyway (a manual deploy sends the cluster name, a CI/OIDC deploy sends the raw address). Keying on it would let those two deploys of the same app run concurrently. See MIR-1465.

func NewLocks

NewLocks builds a lock manager. status may be nil, in which case a held lock is only stealable once expired.

func (*Locks) Acquire

func (l *Locks) Acquire(ctx context.Context, appName, deploymentID string) (*Holder, error)

Acquire takes the deploy lock for deploymentID, returning the holder it established.

If another live deployment holds it, Acquire returns that holder along with ErrLockHeld. Re-acquiring a lock this same deployment already holds succeeds and refreshes the expiry, so a retried call is not an error.

func (*Locks) Blocking

func (l *Locks) Blocking(ctx context.Context, appName string) (*Holder, error)

Blocking returns the holder that would block a new deployment for this app, or nil if nothing would.

This is the question callers actually have — a pre-flight check before uploading a build context, or the lock info rendered in a "deployment blocked" message — and it is not the same as "a lock entity exists". A released tombstone, an expired lock, and a lock whose deployment already finished all read as free.

func (*Locks) Get

func (l *Locks) Get(ctx context.Context, appName string) (*Holder, error)

Get returns the current holder, or cond.ErrNotFound if the lock is free.

func (*Locks) Release

func (l *Locks) Release(ctx context.Context, appName, deploymentID string) error

Release drops the lock, but only if deploymentID still holds it.

The release is a revision-guarded write rather than a delete: between reading the holder and acting on it, another deployment may legitimately steal the lock — a deployment that has just finished is exactly what makes a lock stealable — and an unguarded delete would then remove the successor's lock, letting a third deployment start alongside it. The entity server's delete takes no caller revision, so the write has to be a Replace to be safe.

The released lock is left behind as a tombstone: owned by nobody, already expired. Acquire treats that as free.

type Phase

type Phase string

Phase is the fine-grained progress of a deployment that is still in_progress.

const (
	PhasePreparing  Phase = "preparing"
	PhaseBuilding   Phase = "building"
	PhasePushing    Phase = "pushing"
	PhaseActivating Phase = "activating"
)

func ParsePhase

func ParsePhase(s string) (Phase, error)

ParsePhase converts a wire/stored string into a Phase, rejecting unknown values with a validation failure.

func (Phase) String

func (p Phase) String() string

func (Phase) Valid

func (p Phase) Valid() bool

Valid reports whether p is a phase this system recognizes.

type Query

type Query struct {
	AppName string
	Status  Status

	// Limit caps the result after sorting newest-first. Zero means no cap.
	Limit int
}

Query selects deployments. An empty field means "any".

There is deliberately no cluster filter: a coordinator's store only holds its own cluster's deployments, and the client-supplied cluster_id is unreliable, so filtering on it would hide legitimate deploys (see MIR-1465).

type Record

type Record struct {
	Deployment *core_v1alpha.Deployment
	Entity     *entityserver_v1alpha.Entity
	Revision   int64
}

Record is a deployment entity together with what a caller needs to write it back: the revision it was read at, and the raw entity for short-id rendering.

func (*Record) AppVersion

func (r *Record) AppVersion() string

AppVersion returns the recorded app version, with legacy placeholder values reported as empty — they never named a real version.

func (*Record) Status

func (r *Record) Status() Status

Status returns the record's status as a typed value.

type Status

type Status string

Status is the lifecycle state of a deployment record.

const (
	StatusInProgress Status = "in_progress"
	StatusActive     Status = "active"
	StatusSucceeded  Status = "succeeded"
	StatusFailed     Status = "failed"
	StatusRolledBack Status = "rolled_back"
	StatusCancelled  Status = "cancelled"
)

func ParseStatus

func ParseStatus(s string) (Status, error)

ParseStatus converts a wire/stored string into a Status, rejecting unknown values with a validation failure.

func (Status) String

func (s Status) String() string

func (Status) Terminal

func (s Status) Terminal() bool

Terminal reports whether s admits no further transitions. A deployment in a terminal status holds no lock and will not change again.

func (Status) Valid

func (s Status) Valid() bool

Valid reports whether s is a status this system recognizes. Records read back from storage can carry anything, so callers validating stored data should use this rather than assuming.

type StatusLookup

type StatusLookup func(ctx context.Context, deploymentID string) (Status, error)

StatusLookup reports the stored status of a deployment record. Acquire uses it so a lock whose holder has already finished — the record says failed, the lock says running — can be taken immediately instead of waiting out the TTL.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store reads and writes deployment records.

Every query goes through an index when the filters allow one. The previous implementation listed every deployment ever created and filtered in memory on each history query, lock check, and activation.

func (*Store) Create

func (s *Store) Create(ctx context.Context, dep *core_v1alpha.Deployment) (*Record, error)

Create writes a new deployment record and returns it with its assigned id.

func (*Store) Delete

func (s *Store) Delete(ctx context.Context, deploymentID string) error

Delete removes a deployment record.

func (*Store) Get

func (s *Store) Get(ctx context.Context, deploymentID string) (*Record, error)

Get reads one deployment record.

func (*Store) List

func (s *Store) List(ctx context.Context, q Query) ([]*Record, error)

List returns matching deployments, newest first.

func (*Store) MarkPreviousActiveAs

func (s *Store) MarkPreviousActiveAs(ctx context.Context, appName, exceptID string, target Status) error

MarkPreviousActiveAs moves the deployments that were active for this app into a settled status, leaving the incoming deployment alone.

func (*Store) Put

func (s *Store) Put(ctx context.Context, rec *Record) error

Put writes a record back under its revision, so a concurrent writer causes a conflict rather than a silent overwrite.

func (*Store) Status

func (s *Store) Status(ctx context.Context, deploymentID string) (Status, error)

Status reports just the status of a deployment, and satisfies StatusLookup so the lock manager can tell a live holder from a finished one.

type Tracker

type Tracker struct {
	// contains filtered or unexported fields
}

Tracker is the deployment lifecycle as a set of operations, and the surface the build paths call. It exists so the record is a byproduct of the work actually happening rather than something a client narrates.

Every mutation goes through it, which is what makes the state machine and the lock unavoidable rather than merely available.

func NewTracker

NewTracker wires a tracker over the entity store. The lock manager is given the store's status lookup, so an abandoned lock can be reconciled against the record it claims to be holding for.

func (*Tracker) Activate

func (t *Tracker) Activate(ctx context.Context, deploymentID string) error

Activate marks the deployment live and settles the one it replaced as succeeded. The lock is released: the deploy is over.

func (*Tracker) ActivateRollback

func (t *Tracker) ActivateRollback(ctx context.Context, deploymentID string) error

ActivateRollback is Activate for a rollback, which settles the deployment it replaced as rolled_back rather than succeeded.

func (*Tracker) Begin

func (t *Tracker) Begin(ctx context.Context, params BeginParams) (*Record, error)

Begin creates the deployment record and takes the deploy lock for it.

If another live deployment holds the lock, Begin returns a *LockHeldError (matching errors.Is(err, ErrLockHeld)) describing the blocker, and leaves no record behind.

func (*Tracker) Cancel

func (t *Tracker) Cancel(ctx context.Context, deploymentID, reason string) error

Cancel stops an in-flight deployment and releases the lock.

func (*Tracker) Fail

func (t *Tracker) Fail(ctx context.Context, deploymentID, errorMessage, buildLogs string) error

Fail records a failed deployment and releases the lock.

A deployment that was cancelled stays cancelled: the operator's action is the more meaningful account of what happened, and the build failing afterwards is a consequence of it.

func (*Tracker) FailIfUnsettled

func (t *Tracker) FailIfUnsettled(ctx context.Context, deploymentID, errorMessage, buildLogs string) error

FailIfUnsettled records a failure only if the deployment has not already finished, reporting success either way.

It exists for deferred error handlers, which fire on every exit path including the ones that follow a successful activation. Such a handler should not have to reason about the state machine: "record a failure unless the deploy already finished" is exactly what a defer wants, and a deployment that is already active, succeeded or cancelled has a better account of itself than a late error does.

func (*Tracker) Locks

func (t *Tracker) Locks() *Locks

Locks exposes the lock manager for read-only inspection, such as the pre-flight check a client makes before uploading a build context.

func (*Tracker) ReleaseLock

func (t *Tracker) ReleaseLock(ctx context.Context, deploymentID string) error

ReleaseLock frees the deploy lock held by a deployment without changing the record, looking the app+cluster up from the record itself.

It is a backstop for a caller whose activation already made the version live but whose record settle failed: releasing the lock keeps a record that will never settle from stalling every later deploy of that app+cluster for the full lock TTL. Release is a no-op if a newer deployment already holds the lock.

func (*Tracker) SetAppVersion

func (t *Tracker) SetAppVersion(ctx context.Context, deploymentID, appVersionID string) error

SetAppVersion records the version the build produced. It replaces the "pending-build" placeholder the client used to write at creation time.

func (*Tracker) SetPhase

func (t *Tracker) SetPhase(ctx context.Context, deploymentID string, phase Phase) error

SetPhase records fine-grained progress. Phases only mean something while a deployment is in flight, so setting one on a settled record is a conflict.

func (*Tracker) Store

func (t *Tracker) Store() *Store

Store exposes the underlying store for read paths (history, lock inspection) that do not mutate the lifecycle.

Jump to

Keyboard shortcuts

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