deploylifecycle

package
v0.16.2 Latest Latest
Warning

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

Go to latest
Published: Sep 29, 2026 License: Apache-2.0 Imports: 17 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 SourceFromGitInfo added in v0.15.0

func SourceFromGitInfo(info core_v1alpha.GitInfo) core_v1alpha.Source

SourceFromGitInfo converts the verbose, legacy build metadata into the small provenance record owned by AppVersion. Repository credentials and request decorations are deliberately discarded before the value becomes durable.

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
	AppID     entity.Id
	ClusterID string
	Operation Operation

	// 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
	Subject        string
	AuthMethod     string
	OrganizationID string

	// ParentDeploymentID records the deployment this attempt was based on.
	ParentDeploymentID 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 expiring deployment lock stored on an app.

The app is the single compare-and-swap point for acquisition. A revision-guarded patch ensures that two callers racing to deploy the same app cannot both become its lock holder.

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. An empty lock, an expired lock, and a lock whose deployment already finished all read as free.

func (*Locks) CommitActivation added in v0.15.0

func (l *Locks) CommitActivation(ctx context.Context, appName string, appID, versionID entity.Id, deploymentID string) error

CommitActivation swings the serving pointers while the same app revision still proves that deploymentID owns the lock. The lock deliberately stays in place until the attempt's terminal outcome is durable. That preserves a recovery witness if the process dies between these two entity writes.

func (*Locks) CommitActivationAtRevision added in v0.15.0

func (l *Locks) CommitActivationAtRevision(ctx context.Context, appName string, appID, versionID entity.Id,
	deploymentID string, expectedRevision int64) error

CommitActivationAtRevision preserves optimistic concurrency for a version derived from a particular app snapshot. Unlike a regular deployment, a configuration mutation must be rebuilt if that snapshot has moved.

func (*Locks) Get

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

Get returns the current holder, or cond.ErrNotFound if the app is unlocked.

func (*Locks) Owns added in v0.15.0

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

Owns reports whether deploymentID is the current, unexpired holder.

func (*Locks) Release

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

Release clears the canonical App lock, then its downgrade-compatible shadow, but only if deploymentID still holds them. Releasing in this order keeps old binaries excluded until the canonical lock is safely clear.

type Operation added in v0.15.0

type Operation string

Operation records why an attempt was started. Unlike a status, it never changes as work progresses.

const (
	OperationBuild        Operation = "build"
	OperationRedeploy     Operation = "redeploy"
	OperationRollback     Operation = "rollback"
	OperationConfigChange Operation = "config_change"
)

func (Operation) Valid added in v0.15.0

func (o Operation) Valid() bool

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
	// LegacyStatus selects the downgrade representation. It is used only for
	// compatibility bookkeeping such as settling the previously active row.
	LegacyStatus 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) AppID added in v0.15.0

func (r *Record) AppID() entity.Id

func (*Record) AppVersion

func (r *Record) AppVersion() string

AppVersion returns the canonical version reference, or the normalized legacy value for an unmigrated record.

func (*Record) Canonical added in v0.15.0

func (r *Record) Canonical() bool

Canonical reports whether any attempt-shaped field is present. Outcome cannot be the discriminator because it is deliberately absent while an attempt is in progress.

func (*Record) CompletedAt added in v0.15.0

func (r *Record) CompletedAt() time.Time

func (*Record) Operation added in v0.15.0

func (r *Record) Operation() Operation

func (*Record) ParentDeploymentID added in v0.15.0

func (r *Record) ParentDeploymentID() string

func (*Record) Phase added in v0.15.0

func (r *Record) Phase() Phase

func (*Record) StartedAt added in v0.15.0

func (r *Record) StartedAt() time.Time

func (*Record) Status

func (r *Record) Status() Status

Status returns the attempt's lifecycle state. A canonical attempt with no outcome is still in progress; outcomes exist only after it settles. Legacy serving-state statuses collapse to succeeded because serving state belongs to app.active_deployment.

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"
	StatusInterrupted Status = "interrupted"
)

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) Canonical added in v0.15.0

func (s Status) Canonical() Status

Canonical removes serving-state history from a legacy status.

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 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) AppByName added in v0.15.0

func (s *Store) AppByName(ctx context.Context, name string) (*core_v1alpha.App, int64, error)

func (*Store) AppVersionByID added in v0.15.0

func (s *Store) AppVersionByID(ctx context.Context, id string) (*core_v1alpha.AppVersion, error)

func (*Store) Create

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

Create writes a new deployment record. Ensure makes a fantastically unlikely ID collision explicit instead of updating an existing attempt.

func (*Store) Delete

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

Delete removes a deployment record.

func (*Store) EnsureApp added in v0.15.0

func (s *Store) EnsureApp(ctx context.Context, name string) (*core_v1alpha.App, int64, error)

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) PublishLockOwner added in v0.15.0

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

PublishLockOwner atomically replaces a lock-owner reservation with the full deployment record after both the compatibility and canonical locks are held.

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) ReserveLockOwner added in v0.15.0

func (s *Store) ReserveLockOwner(ctx context.Context, deploymentID entity.Id) (*lockOwnerReservation, error)

ReserveLockOwner creates a private transition entity for a deployment ID before lock acquisition. During a rolling upgrade, an older runtime treats a legacy lock whose deployment record is missing as abandoned and steals it. The transition entity closes that publication window without making an unsuccessful lock attempt appear in deployment history.

func (*Store) SetActivePointers added in v0.15.0

func (s *Store) SetActivePointers(ctx context.Context, appID entity.Id, revision int64, versionID, deploymentID entity.Id) error

SetActivePointers changes the two app pointers as one entity update. The revision guard prevents an unrelated stale app write from silently undoing an activation; callers retry after re-reading.

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 app lock can be reconciled against the record it names.

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) ActivateAtRevision added in v0.15.0

func (t *Tracker) ActivateAtRevision(ctx context.Context, deploymentID string, appRevision int64) error

ActivateAtRevision activates only if the app is still at the revision from which a configuration mutation was derived. A conflict lets the caller discard its speculative version and rebuild it from the winning state.

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 acquires the deploy lock and publishes a deployment record.

If another live deployment holds the lock, Begin returns a *LockHeldError (matching errors.Is(err, ErrLockHeld)) describing the blocker, and leaves no deployment in history. A private lock-owner reservation ensures that an older runtime cannot mistake the compatibility lock's not-yet-published owner for an abandoned deployment during a rolling upgrade.

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, failureSummary 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, failureSummary 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) Reconcile added in v0.15.0

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

Reconcile converges the crash window between the app pointer CAS and the attempt's terminal write, and marks abandoned attempts interrupted once their lock lease and grace period are both gone.

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