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
- Variables
- func CheckPhase(status Status, phase Phase) error
- func LockID(appName string) entity.Id
- func Transition(from, to Status) error
- type BeginParams
- type Holder
- type LockHeldError
- type Locks
- func (l *Locks) Acquire(ctx context.Context, appName, deploymentID string) (*Holder, error)
- func (l *Locks) Blocking(ctx context.Context, appName string) (*Holder, error)
- func (l *Locks) Get(ctx context.Context, appName string) (*Holder, error)
- func (l *Locks) Release(ctx context.Context, appName, deploymentID string) error
- type Phase
- type Query
- type Record
- type Status
- type StatusLookup
- type Store
- func (s *Store) Create(ctx context.Context, dep *core_v1alpha.Deployment) (*Record, error)
- func (s *Store) Delete(ctx context.Context, deploymentID string) error
- func (s *Store) Get(ctx context.Context, deploymentID string) (*Record, error)
- func (s *Store) List(ctx context.Context, q Query) ([]*Record, error)
- func (s *Store) MarkPreviousActiveAs(ctx context.Context, appName, exceptID string, target Status) error
- func (s *Store) Put(ctx context.Context, rec *Record) error
- func (s *Store) Status(ctx context.Context, deploymentID string) (Status, error)
- type Tracker
- func (t *Tracker) Activate(ctx context.Context, deploymentID string) error
- func (t *Tracker) ActivateRollback(ctx context.Context, deploymentID string) error
- func (t *Tracker) Begin(ctx context.Context, params BeginParams) (*Record, error)
- func (t *Tracker) Cancel(ctx context.Context, deploymentID, reason string) error
- func (t *Tracker) Fail(ctx context.Context, deploymentID, errorMessage, buildLogs string) error
- func (t *Tracker) FailIfUnsettled(ctx context.Context, deploymentID, errorMessage, buildLogs string) error
- func (t *Tracker) Locks() *Locks
- func (t *Tracker) ReleaseLock(ctx context.Context, deploymentID string) error
- func (t *Tracker) SetAppVersion(ctx context.Context, deploymentID, appVersionID string) error
- func (t *Tracker) SetPhase(ctx context.Context, deploymentID string, phase Phase) error
- func (t *Tracker) Store() *Store
Constants ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
HolderFrom extracts the blocking holder from an error returned by Acquire or Begin, reporting false for any other error.
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 ¶
func NewLocks(log *slog.Logger, eac *entityserver_v1alpha.EntityAccessClient, status StatusLookup) *Locks
NewLocks builds a lock manager. status may be nil, in which case a held lock is only stealable once expired.
func (*Locks) Acquire ¶
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 ¶
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) Release ¶
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.
func ParsePhase ¶
ParsePhase converts a wire/stored string into a Phase, rejecting unknown values with a validation failure.
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 ¶
AppVersion returns the recorded app version, with legacy placeholder values reported as empty — they never named a real version.
type Status ¶
type Status string
Status is the lifecycle state of a deployment record.
func ParseStatus ¶
ParseStatus converts a wire/stored string into a Status, rejecting unknown values with a validation failure.
type StatusLookup ¶
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 NewStore ¶
func NewStore(log *slog.Logger, eac *entityserver_v1alpha.EntityAccessClient) *Store
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) 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.
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 ¶
func NewTracker(log *slog.Logger, eac *entityserver_v1alpha.EntityAccessClient) *Tracker
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 ¶
Activate marks the deployment live and settles the one it replaced as succeeded. The lock is released: the deploy is over.
func (*Tracker) ActivateRollback ¶
ActivateRollback is Activate for a rollback, which settles the deployment it replaced as rolled_back rather than succeeded.
func (*Tracker) Begin ¶
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) Fail ¶
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 ¶
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 ¶
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 ¶
SetAppVersion records the version the build produced. It replaces the "pending-build" placeholder the client used to write at creation time.