Help us shape the future of Go! Take the 2026 Go Developer Survey

reservation

package
v8.158.1 Latest Latest
Warning

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

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

Documentation

Overview

Package reservation implements the GitOps side of a management cluster reservation: it points one management cluster's copy of one collection app at the dev builds of one branch, as a single commit in the GitOps repo that holds that cluster.

Index

Constants

View Source
const (
	// ReasonExpired means the reservation's Until had already passed.
	ReasonExpired = "expired"
	// ReasonRenamed means the reservation's stored branch no longer matches its
	// pull request's current head branch: the old branch builds nothing.
	ReasonRenamed = "renamed"
)
View Source
const (
	// Namespace is the namespace every collection app object lives in.
	Namespace = "giantswarm"
	// ConfigMapFile is the per-cluster reservation state file. Its presence,
	// wired into the cluster root kustomization, is the opt-in.
	ConfigMapFile = "configmap-reservations.yaml"
	// DefaultDuration is how long a reservation lasts when nobody says.
	DefaultDuration = 10 * time.Hour
	// MaxDuration is the longest reservation any cluster allows. A cluster can
	// set a lower one of its own.
	MaxDuration = 7 * 24 * time.Hour
	// ScopeApp is a reservation that locks only its own app: it fails when the
	// same app already holds one, or when an exclusive reservation is active,
	// but leaves every other app on the cluster free. It is the default.
	ScopeApp = "app"
	// ScopeExclusive is a reservation that locks the whole cluster: it fails
	// against any other active reservation, unless it promotes the requester's
	// own sole one, but it still patches only the requester's own app.
	ScopeExclusive = "exclusive"

	// DefaultGitOpsRepository holds the configuration of every Giant Swarm
	// management cluster.
	DefaultGitOpsRepository = "giantswarm/giantswarm-management-clusters"
)
View Source
const (
	// SourceNameSuffix is appended to the app name to name the reservation's own
	// source object. The app's own source object is never touched.
	SourceNameSuffix = "-dev-reservation"
	// AnnotationPrefix prefixes the reservation annotations stamped on the new
	// source object, so `kubectl` can answer "why is this version running".
	AnnotationPrefix = "reservation.giantswarm.io/"
	// SourceInterval is how often Flux re-checks the reservation source. A dev
	// build has to land quickly, unlike a release.
	SourceInterval = "1m"
)
View Source
const AcceptedDurations = "30m, 4h, 4h30m or 2d: a positive whole number of minutes in Go duration form (m, h), or a whole number of days (d)"

AcceptedDurations is the list every duration refusal carries, so the fix is in the message and nobody has to go and read documentation for it.

View Source
const MaxDurationAnnotation = "reservations.giantswarm.io/max-duration"

MaxDurationAnnotation is the annotation a cluster's owners put on the reservations ConfigMap to lower the maximum reservation length below MaxDuration.

View Source
const MaxPushAttempts = 5

MaxPushAttempts caps how many times PushWithRetry redoes render before it gives up and reports the failure.

Variables

This section is empty.

Functions

func IsAlreadyReserved

func IsAlreadyReserved(err error) bool

IsAlreadyReserved asserts alreadyReservedError.

func IsAppAmbiguous

func IsAppAmbiguous(err error) bool

IsAppAmbiguous asserts appAmbiguousError.

func IsAppNotFound

func IsAppNotFound(err error) bool

IsAppNotFound asserts appNotFoundError.

func IsAppNotSupported

func IsAppNotSupported(err error) bool

IsAppNotSupported asserts appNotSupportedError.

func IsClusterLocked

func IsClusterLocked(err error) bool

IsClusterLocked asserts clusterLockedError.

func IsClusterNotEnabled

func IsClusterNotEnabled(err error) bool

IsClusterNotEnabled asserts clusterNotEnabledError.

func IsClusterNotFound

func IsClusterNotFound(err error) bool

IsClusterNotFound asserts clusterNotFoundError.

func IsComponentNotFound

func IsComponentNotFound(err error) bool

IsComponentNotFound asserts componentNotFoundError.

func IsDirtyWorktree

func IsDirtyWorktree(err error) bool

IsDirtyWorktree asserts dirtyWorktreeError.

func IsInvalidConfig

func IsInvalidConfig(err error) bool

IsInvalidConfig asserts invalidConfigError.

func IsInvalidDuration

func IsInvalidDuration(err error) bool

IsInvalidDuration asserts invalidDurationError.

func IsNotGitOpsRepo

func IsNotGitOpsRepo(err error) bool

IsNotGitOpsRepo asserts notGitOpsRepoError.

func IsNotReserved

func IsNotReserved(err error) bool

IsNotReserved asserts notReservedError.

func IsNothingToExtend

func IsNothingToExtend(err error) bool

IsNothingToExtend asserts nothingToExtendError.

func IsPush

func IsPush(err error) bool

IsPush asserts pushError.

func IsPushFailed

func IsPushFailed(err error) bool

IsPushFailed asserts pushFailedError.

func IsRender

func IsRender(err error) bool

IsRender asserts renderError.

func IsRenderAssertion

func IsRenderAssertion(err error) bool

IsRenderAssertion asserts renderAssertionError.

func ParseDuration

func ParseDuration(s string) (time.Duration, error)

ParseDuration reads a reservation duration: a whole number of days, or a time.ParseDuration string that is positive and a whole number of minutes, so "1ns" and "-4h" are not reservations. An empty string is the default, so a caller never has to special-case a duration nobody gave.

func Push

func Push(ctx context.Context, dir string) error

Push pushes dir's HEAD to main on origin, using whatever credentials are already configured on the remote: the checkout's own git credentials (SSH or a credential helper on a laptop, the token actions/checkout persists in CI). Reserve and Release share it, so there is one way to push, not two. Like the sync before every command, it names main and origin explicitly instead of relying on a tracking branch.

func PushWithRetry

func PushWithRetry(ctx context.Context, dir string, dryRun bool, render func() error) error

PushWithRetry calls render, which must render, assert and commit exactly as Reserve and Release do, and pushes the commit it made. When the push is rejected, it fetches and hard-resets dir to origin/main's new tip -- discarding the commit render just made, not replaying it -- and calls render again. A rebase that only replayed the old commit would carry whatever it rendered against the old tip; this reruns render against whatever landed there first, so a reservation that collided on the same kustomization.yaml or ConfigMap is folded in instead of corrupted.

When it gives up -- every attempt rejected, or a push error that is not a rejection (no credentials, no network) -- the last commit stays on local main and the error is a pushFailedError, so the caller can tell the user the change is committed but not pushed. A render error is returned as is.

With dryRun it calls render once and pushes nothing: the commit stays on local main for the caller to show and drop.

func SplitPullRequest

func SplitPullRequest(pullRequest string) (owner, repo string, number int, err error)

SplitPullRequest parses "owner/repo#number", the form every --pull-request flag stores and matches on.

func ValidateUser

func ValidateUser(user string) error

ValidateUser checks the form every --user flag takes. The login is written into YAML, so anything else would corrupt the reservation's source object.

Types

type ExtendRequest

type ExtendRequest struct {
	// RepoDir is the working tree of the GitOps repo to scan. Extend commits
	// and pushes to it directly, exactly as Reap does: no clone.
	RepoDir string
	// PullRequest is the key the scan matches on, e.g.
	// "giantswarm/hello-world#123".
	PullRequest string
	// User becomes the holder of each reservation Extend renews, and attributes
	// the commits. It need not be the original holder: extend hands it over.
	User string
	// Now is the new start of every reservation Extend resets. Zero means
	// time.Now().
	Now time.Time
	// DryRun commits each extension on local main and pushes nothing.
	DryRun bool
}

ExtendRequest resets the expiry of every reservation a pull request holds.

type Extended

type Extended struct {
	// Cluster is the management cluster the reservation lives on.
	Cluster string `json:"cluster"`
	// App is the reservation's chart.
	App string `json:"app"`
	// From and Until bound the new window, in UTC.
	From  time.Time `json:"from"`
	Until time.Time `json:"until"`
}

Extended reports one reservation Extend reset.

func Extend

func Extend(ctx context.Context, req ExtendRequest) ([]Extended, error)

Extend resets the expiry of every reservation req.PullRequest holds across every enabled cluster, keeping each reservation's own stored cluster, app, scope and duration -- only the window moves, starting now and lasting as long as the existing record already did.

A reservation whose Until has already passed is treated as if it did not exist: reviving it could silently break an exclusive lock somebody else legally took over the same cluster while the dead record sat unswept (see checkCollision). Only `reserve` brings a dead reservation back.

It refuses when the pull request holds no matching, unexpired record on any cluster, naming `devctl reservation reserve` as the way to create one. A single reservation whose stored duration now exceeds its cluster's cap is refused on its own and does not stop the rest of the sweep, exactly as Reap behaves: Extend never returns a bare nil, err when some reservations did get extended.

type HeadBranchFunc

type HeadBranchFunc func(ctx context.Context, pullRequest string) (string, error)

HeadBranchFunc resolves a pull request, e.g. "giantswarm/hello-world#123", to its current head branch. Reap calls it only for a reservation that is not already expired and names a pull request.

type ListRequest

type ListRequest struct {
	// RepoDir is the working tree of the GitOps repo holding Cluster.
	RepoDir string
	// Cluster is the management cluster name, e.g. "graveler".
	Cluster string
}

ListRequest identifies the cluster whose reservations to list.

type ReapRequest

type ReapRequest struct {
	// RepoDir is the working tree of the GitOps repo to sweep. Reap commits and
	// pushes to it directly, exactly as Release does: no clone.
	RepoDir string
	// User attributes each release commit Reap makes, e.g. "reservation-reaper".
	// It need not be the original holder, exactly like ReleaseRequest.User.
	User string
	// Now is the clock Reap checks expiry against. Zero means time.Now().
	Now time.Time
	// HeadBranch resolves a reservation's pull request to its current head
	// branch. Required only when some active reservation names one; Reap never
	// calls it for a reservation its expiry already condemns.
	HeadBranch HeadBranchFunc
	// DryRun commits each release on local main and pushes nothing.
	DryRun bool
}

ReapRequest sweeps every enabled cluster in a GitOps repo.

type Reaped

type Reaped struct {
	// Cluster is the management cluster the reservation was released on.
	Cluster string `json:"cluster"`
	// App is the chart Release resolved the reservation to.
	App string `json:"app"`
	// User is the GitHub login of the reservation's original holder -- not
	// ReapRequest.User, which only attributes the release commit.
	User string `json:"user"`
	// Branch is the reservation's stored branch: the one that makes no more
	// builds when Reason is "renamed".
	Branch string `json:"branch"`
	// PullRequest is the reservation's pull request, e.g.
	// "giantswarm/hello-world#123", or empty when it named none.
	PullRequest string `json:"pullRequest,omitempty"`
	// Reason is why Reap released it: ReasonExpired or ReasonRenamed.
	Reason string `json:"reason"`
	// Until is the reservation's expiry, in UTC.
	Until time.Time `json:"until"`
	// Commit is the hash of the commit Release made.
	Commit string `json:"commit"`
}

Reaped reports one reservation Reap released.

func Reap

func Reap(ctx context.Context, req ReapRequest) ([]Reaped, error)

Reap sweeps every enabled cluster under RepoDir's management-clusters directory and releases every reservation that ran out of time or whose stored branch no longer matches its pull request's current head branch. A cluster that fails -- an unparsable ConfigMap, a push that never lands -- does not stop the sweep from reaching the next one; its error is joined into the one Reap returns alongside whatever it did manage to release, so a caller can report every failure rather than just the first.

type ReleaseAllRequest

type ReleaseAllRequest struct {
	// RepoDir is the working tree of the GitOps repo to scan. ReleaseAll
	// commits and pushes to it directly, exactly as Reap and Extend do: no
	// clone.
	RepoDir string
	// PullRequest is the key the scan matches on, e.g.
	// "giantswarm/hello-world#123".
	PullRequest string
	// User attributes each release commit. It need not be the original holder,
	// exactly like ReapRequest.User.
	User string
	// DryRun commits each release on local main and pushes nothing.
	DryRun bool
}

ReleaseAllRequest releases every reservation one pull request holds, across every enabled cluster.

type ReleaseRequest

type ReleaseRequest struct {
	// RepoDir is the working tree of the GitOps repo holding Cluster.
	RepoDir string
	// Cluster is the management cluster name, e.g. "graveler".
	Cluster string
	// App names the chart to release. Empty means the chart the app repository
	// at AppDir builds. Release() keys the reservation on the resolved chart,
	// exactly as Reserve does, so this can be Result.App from the original
	// Reserve call.
	App string
	// AppDir is a working tree of the app repository, read only to take the
	// chart name from helm/*/Chart.yaml. It is unused when App is set.
	AppDir string
	// User is the GitHub login of whoever releases the reservation. It need not
	// be the original holder: a colleague or an on-call engineer can release for
	// someone else.
	User string
	// PullRequest, when set, restricts the release to a reservation that pull
	// request holds, e.g. "giantswarm/hello-world#123". Any other pull
	// request's reservation is refused, so releasing from another pull request cannot free a
	// cluster somebody else is testing on. Empty means no check: that is the
	// force-release a human runs from a laptop to clear a stuck lock, and it
	// is what Reap uses.
	PullRequest string
	// DryRun is read only by the sweeps that push: Release itself never
	// pushes.
	DryRun bool
}

ReleaseRequest identifies the reservation to release.

type ReleaseResult

type ReleaseResult struct {
	// App is the chart Release resolved the request to.
	App string `json:"app"`
	// Commit is the hash of the commit Release made.
	Commit string `json:"commit"`
	// Files are the repo-relative paths Release removed or edited, sorted.
	Files []string `json:"files,omitempty"`
}

ReleaseResult reports what Release removed.

func Release

func Release(req ReleaseRequest) (res ReleaseResult, err error)

Release undoes exactly what Reserve wrote for one app on one cluster: deleting the reservation's Kustomize component, the one line that references it, and the reservation's entry in the cluster's ConfigMap. It restores the working tree byte for byte to the state a matching Reserve found it in, and commits the change. It works on a working tree only: like Reserve, it never clones and never pushes.

type Released

type Released struct {
	// Cluster is the management cluster the reservation lived on.
	Cluster string `json:"cluster"`
	// App is the chart Release resolved the reservation to.
	App string `json:"app"`
	// User is the GitHub login of the reservation's original holder -- not
	// ReleaseAllRequest.User, which only attributes the release commit.
	User string `json:"user"`
	// Branch is the reservation's stored branch.
	Branch string `json:"branch"`
	// Commit is the hash of the commit Release made.
	Commit string `json:"commit"`
}

Released reports one reservation ReleaseAll removed.

func ReleaseAll

func ReleaseAll(ctx context.Context, req ReleaseAllRequest) ([]Released, error)

ReleaseAll releases every reservation req.PullRequest holds across every enabled cluster. A merged or closed pull request knows neither the cluster nor the app it reserved, so it cannot call Release, which needs both.

type Request

type Request struct {
	// RepoDir is the working tree of the GitOps repo holding Cluster.
	RepoDir string
	// Cluster is the management cluster name, e.g. "graveler".
	Cluster string
	// App names the collection app to point at the dev builds. Empty means the
	// chart the app repository at AppDir builds. It is required when that
	// repository holds several charts, and it overrides the chart name when a
	// chart is named differently from the chart its URL serves.
	App string
	// AppDir is a working tree of the app repository, read only to take the
	// chart name from helm/*/Chart.yaml. It is unused when App is set.
	AppDir string
	// Branch is the app repo branch whose dev builds the cluster follows.
	Branch string
	// User is the GitHub login of the holder.
	User string
	// PullRequest is the holder's pull request, e.g. "giantswarm/app#123".
	PullRequest string
	// Now is the start of the reservation. Zero means time.Now().
	Now time.Time
	// Duration is how long the reservation lasts. Zero means DefaultDuration, or
	// the cluster's maximum when that is lower.
	Duration time.Duration
	// Scope is the reservation's lock: ScopeApp locks only App, ScopeExclusive
	// locks the whole cluster. Empty means ScopeApp.
	Scope string
}

Request is one reservation of one app on one management cluster.

type Reservation

type Reservation struct {
	// App is the chart the reservation moved. It is the ConfigMap entry's key.
	App string `json:"app"`
	// User is the GitHub login of the holder.
	User string `json:"user"`
	// Branch is the app repo branch the reservation follows.
	Branch string `json:"branch"`
	// PullRequest is the holder's pull request.
	PullRequest string `json:"pullRequest,omitempty"`
	// Scope is the reservation's scope, e.g. ScopeApp.
	Scope string `json:"scope"`
	// From and Until bound the reservation, in UTC. Until is the expiry.
	From  time.Time `json:"from"`
	Until time.Time `json:"until"`
}

Reservation is one reservation, active or expired, read back exactly as reservationEntry wrote it: never reconstructed from anything else.

func List

func List(req ListRequest) ([]Reservation, error)

List returns every reservation recorded on the cluster, sorted by app, read straight from its ConfigMap, including one whose Until has already passed but the reaper has not swept yet: Reap and checkCollision both rely on seeing those too, and do their own "active as of now" filtering. A caller that wants only the active ones, such as the list command, must filter Until against its own idea of now. Like Release, List works on an existing checkout: no render, no clone.

func (Reservation) Expired

func (r Reservation) Expired(now time.Time) bool

Expired reports whether the reservation no longer holds at now. An expired reservation is not a lock: a later Reserve of the same app replaces it.

type Result

type Result struct {
	// App is the chart Reserve resolved the request to. It is the reservation's
	// key on the cluster.
	App string `json:"app"`
	// Commit is the hash of the commit Reserve made.
	Commit string `json:"commit"`
	// SourceName is the name of the new OCIRepository.
	SourceName string `json:"sourceName"`
	// SemverFilter is the version selector the new OCIRepository carries.
	SemverFilter string `json:"semverFilter"`
	// Files are the repo-relative paths Reserve wrote, sorted.
	Files []string `json:"files,omitempty"`
	// From and Until bound the reservation, in UTC.
	From  time.Time `json:"from"`
	Until time.Time `json:"until"`
}

Result reports what Reserve wrote.

func Reserve

func Reserve(req Request) (res Result, err error)

Reserve points the cluster's copy of the app at the dev builds of the branch and commits the change to the working tree at req.RepoDir. It renders the cluster's collections and refuses to commit unless the rendered output really carries the reservation.

Jump to

Keyboard shortcuts

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