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
- func IsAlreadyReserved(err error) bool
- func IsAppAmbiguous(err error) bool
- func IsAppNotFound(err error) bool
- func IsAppNotSupported(err error) bool
- func IsClusterLocked(err error) bool
- func IsClusterNotEnabled(err error) bool
- func IsClusterNotFound(err error) bool
- func IsComponentNotFound(err error) bool
- func IsDirtyWorktree(err error) bool
- func IsInvalidConfig(err error) bool
- func IsInvalidDuration(err error) bool
- func IsNotGitOpsRepo(err error) bool
- func IsNotReserved(err error) bool
- func IsNothingToExtend(err error) bool
- func IsPush(err error) bool
- func IsPushFailed(err error) bool
- func IsRender(err error) bool
- func IsRenderAssertion(err error) bool
- func ParseDuration(s string) (time.Duration, error)
- func Push(ctx context.Context, dir string) error
- func PushWithRetry(ctx context.Context, dir string, dryRun bool, render func() error) error
- func SplitPullRequest(pullRequest string) (owner, repo string, number int, err error)
- func ValidateUser(user string) error
- type ExtendRequest
- type Extended
- type HeadBranchFunc
- type ListRequest
- type ReapRequest
- type Reaped
- type ReleaseAllRequest
- type ReleaseRequest
- type ReleaseResult
- type Released
- type Request
- type Reservation
- type Result
Constants ¶
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" )
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" )
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" )
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.
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.
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 ¶
IsAlreadyReserved asserts alreadyReservedError.
func IsAppNotSupported ¶
IsAppNotSupported asserts appNotSupportedError.
func IsClusterLocked ¶
IsClusterLocked asserts clusterLockedError.
func IsClusterNotEnabled ¶
IsClusterNotEnabled asserts clusterNotEnabledError.
func IsClusterNotFound ¶
IsClusterNotFound asserts clusterNotFoundError.
func IsComponentNotFound ¶
IsComponentNotFound asserts componentNotFoundError.
func IsDirtyWorktree ¶
IsDirtyWorktree asserts dirtyWorktreeError.
func IsInvalidConfig ¶
IsInvalidConfig asserts invalidConfigError.
func IsInvalidDuration ¶
IsInvalidDuration asserts invalidDurationError.
func IsNotGitOpsRepo ¶
IsNotGitOpsRepo asserts notGitOpsRepoError.
func IsNothingToExtend ¶
IsNothingToExtend asserts nothingToExtendError.
func IsRenderAssertion ¶
IsRenderAssertion asserts renderAssertionError.
func ParseDuration ¶
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 ¶
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 ¶
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 ¶
SplitPullRequest parses "owner/repo#number", the form every --pull-request flag stores and matches on.
func ValidateUser ¶
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 ¶
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.
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.