Documentation
¶
Overview ¶
Package application defines the Application spec, its observed status, and the JSON both serialise to. It is the wire contract shared by the config loader, the reconciler, the HTTP API and the CLI.
Per D3 the API is designed UI-first, and these types are that design: a shape that is awkward to render is a bug here rather than in a handler.
The package is deliberately top-level rather than internal. Per D6 a private companion repository imports this one, and Go's internal rule is per-module, so anything the companion touches cannot live under internal/ — the same reason Eldara-Tech/swarmcli has no internal/ directory at all.
It depends on nothing but the standard library and a YAML decoder: not on Docker, not on the reconciler, not on the CE charts package. The types that mirror charts concepts are re-expressed here so that the API's shape does not move when CE's does.
Index ¶
- Constants
- func AppFromOwnerID(controller, id string) (string, bool)
- func OwnerID(controller, app string) string
- func ValidateControllerID(id string) error
- type AppSetStatus
- type ChartSource
- type Compat
- type CompatState
- type ControllerStatus
- type Destination
- type DriftDetection
- type Duration
- type Health
- type HealthState
- type History
- type ReleaseDiff
- type ReleaseHistory
- type ReleaseStatus
- type RepositorySpec
- type Revision
- type ServiceCounts
- type ServiceStatus
- type Source
- type Spec
- type Status
- type Sync
- type SyncAction
- type SyncPolicy
- type SyncResult
- type SyncState
- type SyncSummary
- type View
Constants ¶
const DefaultControllerID = "default"
DefaultControllerID is the identity a controller stamps with when the deployment does not choose one.
It is a real default rather than a required flag because the single-controller case is the overwhelmingly common one and should not need ceremony. Two controllers sharing a swarm must be given distinct ids — see OwnerID.
Variables ¶
This section is empty.
Functions ¶
func AppFromOwnerID ¶
AppFromOwnerID reports which of this controller's applications an owner id names, and whether the id belongs to this controller at all.
False for everything else on the swarm: an "apply/" stamp from the command line, another tool's id, a bare prefix naming no application, and — the case this exists for — an id belonging to a different swarmcli-cd. Prune treats false as "not mine", which is what stops it deleting a release it did not install.
A stamp in the pre-controller-id format ("cd/<app>") is likewise not this controller's. That is deliberate — it reads as unmanaged, so prune leaves it alone, and the migration errs towards not deleting.
It does not heal on its own, though, and it is worth being exact about why. Ownership plays no part in planning a release the file declares, so a reconcile that deploys one re-stamps it; but a release whose chart, values and manifest are unchanged is planned as unchanged and skipped, and skipping is what charts does instead of writing a revision that would say nothing new. So a stale stamp survives until something actually redeploys that release, which under the default sync policy is until an operator asks. What keeps that safe is not the stamp but prune's second signal: a release an application still declares is never swept, whatever it is stamped with (#62).
func OwnerID ¶
OwnerID is the id this controller stamps a release with and classifies deployed releases against: "cd/<controller>/<application>".
Both halves are load-bearing and for different reasons.
The application half is what keeps sibling applications apart. Several applications share one swarm, and an id that named only the controller would make each of them report the others' releases as its own orphans.
The controller half is what keeps whole controllers apart, and exists because prune acts on the difference. A sweep asks "which releases on this swarm belong to an application my app set no longer declares", and without a controller in the id, a second swarmcli-cd on the same swarm answers that question about the first one's applications — and deletes them. Two controllers sharing a swarm must therefore be given distinct ids, or each will treat the other's work as departed.
It lives here, in the wire contract, rather than in the reconciler that writes it, because it is also what prune reads back off the swarm to decide what may be deleted. Two copies of this format that drifted apart would not fail loudly — the reconciler would keep stamping and prune would quietly stop recognising, which is a deletion bug in whichever direction it broke.
func ValidateControllerID ¶
ValidateControllerID refuses an id that would produce an unparseable stamp or one that cannot be told apart from another.
A slash would make "cd/<controller>/<application>" ambiguous about where the controller ends, and a colon is what the chart engine itself rejects. Space is refused because an id that differs from another only by trailing whitespace is the kind of distinction an operator cannot see and prune would act on.
Types ¶
type AppSetStatus ¶
type AppSetStatus struct {
// Mode is how the set is sourced, as the deployment configured it —
// "static", "git" or "path". It is a label passed in rather than something
// the source infers about itself, so the bootstrap's own vocabulary is what
// an operator sees reflected back.
Mode string `json:"mode"`
// Source is what the bootstrap was pointed at, for a human: the repository,
// the revision it tracks and the file within it, or the directory and file
// an external process keeps current. Mode alone cannot be checked against
// anything, and "which branch is this controller following" is the first
// question a set that does not look right raises — and the one thing nothing
// in git can change (D-f of #47).
Source string `json:"source,omitempty"`
// Revision is the commit the running set was loaded from. Empty when the
// set does not come from a repository.
Revision string `json:"revision,omitempty"`
// LoadedAt is when the running set last loaded successfully — not when it
// was last checked. A load that changed nothing does not move it, so the
// pair with Error answers "how old is what I am running".
LoadedAt time.Time `json:"loadedAt"`
// Error is why the last attempt failed: a load that was refused, or the
// applications a diff could not apply. Cleared by the next attempt that
// succeeds.
Error string `json:"error,omitempty"`
// Stale reports that the running set is a last-good one and a newer version
// is being refused. It is the field a UI colours; Error is what it shows
// beside it. An error with Stale false is one of two different problems: a
// set that loaded but could not be fully applied, or — with Applications at
// zero and LoadedAt unset — a controller that has never managed to load one,
// which is the louder of the three.
Stale bool `json:"stale"`
// Orphaned names applications that left the set. Their loops are stopped and
// their stacks are still deployed and no longer reconciled by anyone —
// reported rather than removed, unless prune is enabled.
//
// This list is what the running loop watched leave, so a restart empties it.
// That is a gap in the reporting and not in the cleanup: the owner stamps
// the releases carry are the durable record, so a controller with prune
// enabled still finds and removes an application that departed before it
// started. With prune disabled a restart does forget, and the swarm is the
// only place left that knows.
Orphaned []string `json:"orphaned,omitempty"`
// Pruned names applications whose resources this controller has deleted,
// most recent last. Empty whenever prune is disabled, which is the default.
//
// It exists because a departed application otherwise leaves no trace at all
// once prune has run: it is gone from the app set, gone from Orphaned, and
// gone from the swarm. "Did it actually go, or did the controller never
// notice" is then only answerable from the logs.
//
// Like Orphaned, in memory: a restart empties it. What it reports is this
// process's own deletions, not an audit log.
Pruned []string `json:"pruned,omitempty"`
// PruneHeldBy names the applications that have not reconciled yet and are
// therefore holding the sweep back. Empty whenever prune is disabled, and
// on any controller whose applications have all planned at least once —
// which after a settled startup is every controller.
//
// The sweep deletes what no application declares, so it cannot run while an
// application has not said what it declares; it would read that silence as
// a departure. Waiting is the safe half of that trade and this is the other
// half: an operator who enabled prune and sees nothing being pruned has to
// be able to find out which application is holding it, or the safety
// measure is indistinguishable from a broken feature.
PruneHeldBy []string `json:"pruneHeldBy,omitempty"`
}
AppSetStatus is where the set of applications came from and how that is going.
type ChartSource ¶
type ChartSource struct {
Release string `json:"release" yaml:"release"`
Path string `json:"path,omitempty" yaml:"path,omitempty"` // chart directory within the repo
Ref string `json:"ref,omitempty" yaml:"ref,omitempty"` // repo/chart
Version string `json:"version,omitempty" yaml:"version,omitempty"`
Values []string `json:"values,omitempty" yaml:"values,omitempty"` // paths within the repo
Repositories []RepositorySpec `json:"repositories,omitempty" yaml:"repositories,omitempty"`
}
ChartSource is the "one application, one chart" case that does not deserve a release file in the repository. Its rules are the ones charts itself enforces: Version is required for a repository reference and forbidden for a path, because a floating pin would silently upgrade production on the next reconcile.
type Compat ¶
type Compat struct {
Status CompatState `json:"status"`
Required string `json:"required,omitempty"`
Engine string `json:"engine,omitempty"`
Reason string `json:"reason,omitempty"`
}
Compat is a chart's declared swarmcliVersion verdict. Planning records it and never enforces it, so an unattended controller has to surface it or the operator never learns the chart wanted a newer engine.
type CompatState ¶
type CompatState string
CompatState is a chart's swarmcliVersion verdict against the engine this controller embeds.
const ( CompatUnknown CompatState = "" CompatOK CompatState = "ok" CompatIncompatible CompatState = "incompatible" )
func (CompatState) MarshalJSON ¶
func (c CompatState) MarshalJSON() ([]byte, error)
MarshalJSON implements json.Marshaler.
func (*CompatState) UnmarshalJSON ¶
func (c *CompatState) UnmarshalJSON(data []byte) error
UnmarshalJSON implements json.Unmarshaler.
type ControllerStatus ¶
type ControllerStatus struct {
AppSet AppSetStatus `json:"appSet"`
Applications int `json:"applications"`
}
ControllerStatus is the controller itself as last observed, as distinct from the applications it reconciles.
It exists because the app set became something that can fail on its own: once the set is pulled from git rather than mounted at deploy time, "every application looks fine" and "the controller has been refusing every commit for an hour" are both true at the same time, and nothing in the per-application views can say the second. This is where that is said.
Applications counts what is actually being reconciled, which is not necessarily what the last-loaded file declares: an application the set added but that could not be started is in the file and not in this count.
type Destination ¶
type Destination struct {
Swarm string `json:"swarm,omitempty" yaml:"swarm,omitempty"`
}
Destination names the swarm, resolved through the SwarmRegistry seam. Empty means the local swarm, which is the only one Phase 1 can resolve.
type DriftDetection ¶
type DriftDetection string
DriftDetection is how an application's drift is decided. Phase 1 has one mode: manifest, which compares the rendered manifest against what was last applied. Comparing the desired ServiceSpec against the live one is Phase 2.
const ( DriftUnknown DriftDetection = "" DriftManifest DriftDetection = "manifest" )
func (DriftDetection) MarshalJSON ¶
func (d DriftDetection) MarshalJSON() ([]byte, error)
MarshalJSON implements json.Marshaler.
func (*DriftDetection) UnmarshalJSON ¶
func (d *DriftDetection) UnmarshalJSON(data []byte) error
UnmarshalJSON implements json.Unmarshaler.
func (DriftDetection) Valid ¶
func (d DriftDetection) Valid() bool
Valid reports whether d names a mode this build implements. It is what the config loader checks: unlike the wire, applications.yaml gets no leniency.
type Duration ¶
Duration is a time.Duration that reads and writes as a string ("30s", "10m") rather than as a count of nanoseconds. These values are written by hand in applications.yaml and read by humans in API output, and 600000000000 is neither writable nor readable.
func (Duration) MarshalJSON ¶
MarshalJSON implements json.Marshaler.
func (Duration) MarshalYAML ¶
MarshalYAML implements yaml.Marshaler.
func (*Duration) UnmarshalJSON ¶
UnmarshalJSON implements json.Unmarshaler.
type Health ¶
type Health struct {
State HealthState `json:"state"`
Message string `json:"message,omitempty"`
Services ServiceCounts `json:"services"`
}
Health answers "is what is running actually working". It is a separate axis from Sync — a stack can be synced and degraded at once, and collapsing the two loses the distinction that makes the view useful. Services carries the counts a list row renders without descending into Releases.
type HealthState ¶
type HealthState string
HealthState is whether what is running is working. Missing — declared but not present — is deliberately distinct from Degraded, which is present and unhealthy: a UI needs to tell those apart and so does an operator.
const ( HealthUnknown HealthState = "" HealthHealthy HealthState = "healthy" HealthProgressing HealthState = "progressing" HealthDegraded HealthState = "degraded" HealthMissing HealthState = "missing" )
func (HealthState) MarshalJSON ¶
func (h HealthState) MarshalJSON() ([]byte, error)
MarshalJSON implements json.Marshaler.
func (*HealthState) UnmarshalJSON ¶
func (h *HealthState) UnmarshalJSON(data []byte) error
UnmarshalJSON implements json.Unmarshaler.
type History ¶
type History struct {
Releases []ReleaseHistory `json:"releases"`
}
History is one application's release history: every release it declares, with the revisions recorded for each.
It is served by its own endpoint rather than carried on Status for the same reason ReleaseDiff is — a list view rendering twenty applications must not drag their histories along.
type ReleaseDiff ¶
type ReleaseDiff struct {
Release string `json:"release"`
Action SyncAction `json:"action"`
Diff string `json:"diff"`
}
ReleaseDiff is the manifest change one release would undergo.
It is deliberately not part of ReleaseStatus: a diff carries whole manifests, and a list view rendering twenty applications must not drag them along. It is served by its own endpoint, for one application at a time.
type ReleaseHistory ¶
ReleaseHistory is one release's revisions, newest first.
A release the repository declares but that has never been deployed has an empty Revisions rather than being absent: a history view shows the release with nothing under it, which is the honest answer and a different one from "no such release".
type ReleaseStatus ¶
type ReleaseStatus struct {
Name string `json:"name"`
Chart string `json:"chart"`
Version string `json:"version"`
Revision int `json:"revision"` // charts revision number; 0 when never installed
Action SyncAction `json:"action"`
Sync SyncState `json:"sync"`
Health Health `json:"health"`
Services []ServiceStatus `json:"services,omitempty"`
Compat *Compat `json:"compat,omitempty"`
}
ReleaseStatus is one release of one application.
type RepositorySpec ¶
type RepositorySpec struct {
Name string `json:"name" yaml:"name"`
URL string `json:"url" yaml:"url"`
}
RepositorySpec names a chart repository a ChartSource resolves Ref against.
type Revision ¶
type Revision struct {
Revision int `json:"revision"`
Chart string `json:"chart"`
Version string `json:"version"`
// Status is the engine's derived status: the highest revision keeps its
// stored status and every lower deployed one reads "superseded".
Status string `json:"status"`
// Created is RFC3339, as the engine recorded it.
Created string `json:"created,omitempty"`
// Owner is the stamp naming what produced the revision, empty when
// unclaimed. A history view showing a revision this controller did not
// install is telling the operator something worth knowing.
Owner string `json:"owner,omitempty"`
}
Revision is one recorded revision of one release.
Deliberately not charts.Release. That type carries the rendered manifest and the merged values of every revision, and a history response covering six releases at ten revisions each would then be sixty manifests — for a view that renders a table of numbers, dates and chart versions. The manifest of a specific revision is a different request, if it is ever wanted.
type ServiceCounts ¶
ServiceCounts is the "3/4" a list row shows.
type ServiceStatus ¶
type ServiceStatus struct {
Name string `json:"name"`
Mode string `json:"mode"`
Running int `json:"running"`
Desired int `json:"desired"`
Completed int `json:"completed,omitempty"`
Health HealthState `json:"health"`
UpdateState string `json:"updateState,omitempty"` // "" means never updated, not finished
Message string `json:"message,omitempty"`
}
ServiceStatus is one Swarm service under a release.
type Source ¶
type Source struct {
RepoURL string `json:"repoURL" yaml:"repoURL"`
Revision string `json:"revision" yaml:"revision"` // branch, tag or SHA, as written
ReleaseFile string `json:"releaseFile,omitempty" yaml:"releaseFile,omitempty"` // path within the repo
Chart *ChartSource `json:"chart,omitempty" yaml:"chart,omitempty"`
}
Source locates the desired state in git. Exactly one of ReleaseFile and Chart is set, and which one is present is the source type — a separate discriminator field would be a second thing to keep consistent with it.
type Spec ¶
type Spec struct {
Name string `json:"name" yaml:"name"`
Source Source `json:"source" yaml:"source"`
// RegistryAuth names a Docker secret holding a docker config.json
// ({"auths":{...}}). The controller uses only this application's secret to
// authenticate the pulls its images need, so one application cannot pull
// another's private images even though both credentials are mounted in the
// same controller. Empty means the application's images are public.
//
// It is a secret name, not a path: the controller reads it from the default
// mount /run/secrets/<name>, so the secret must be mounted there — the
// short form `secrets: [<name>]` in stack.yml does exactly that.
RegistryAuth string `json:"registryAuth,omitempty" yaml:"registryAuth,omitempty"`
Destination Destination `json:"destination" yaml:"destination"`
SyncPolicy SyncPolicy `json:"syncPolicy" yaml:"syncPolicy"`
DriftDetection DriftDetection `json:"driftDetection" yaml:"driftDetection"`
}
Spec is what an operator declares in applications.yaml. It is read-only over the API: the file is the only source of truth, whether it is mounted at deploy time or committed to git, and the API serves it rather than owning it.
type Status ¶
type Status struct {
Sync Sync `json:"sync"`
Health Health `json:"health"`
Releases []ReleaseStatus `json:"releases,omitempty"`
Error string `json:"error,omitempty"` // last reconcile error; not a failed sync
ObservedAt time.Time `json:"observedAt"`
}
Status is one application as last observed. The list view renders it without Releases and the detail view renders it with. Releases is never legitimately empty once populated — charts rejects a release file declaring no releases — so its absence unambiguously means "not requested" rather than "none".
type Sync ¶
type Sync struct {
State SyncState `json:"state"`
Revision string `json:"revision,omitempty"` // resolved SHA, never a branch name
Summary SyncSummary `json:"summary"`
LastSync *SyncResult `json:"lastSync,omitempty"`
}
Sync answers "does the swarm match git".
Revision is the commit the assessment was made against; LastSync.Revision is what was actually deployed. When the two differ there is a newer commit that has not been applied, which is a different condition from being OutOfSync, and a UI shows both.
type SyncAction ¶
type SyncAction string
SyncAction is what a sync would do to one release. The names mirror the chart engine's own vocabulary value-for-value; diverging from it would mean translating in both directions for no gain.
const ( ActionUnknown SyncAction = "" ActionUnchanged SyncAction = "unchanged" ActionInstall SyncAction = "install" ActionUpgrade SyncAction = "upgrade" )
func (SyncAction) MarshalJSON ¶
func (a SyncAction) MarshalJSON() ([]byte, error)
MarshalJSON implements json.Marshaler.
func (*SyncAction) UnmarshalJSON ¶
func (a *SyncAction) UnmarshalJSON(data []byte) error
UnmarshalJSON implements json.Unmarshaler.
type SyncPolicy ¶
type SyncPolicy struct {
Automated bool `json:"automated" yaml:"automated"`
Interval Duration `json:"interval,omitempty" yaml:"interval,omitempty"`
Wait bool `json:"wait,omitempty" yaml:"wait,omitempty"`
Timeout Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"`
HistoryMax int `json:"historyMax,omitempty" yaml:"historyMax,omitempty"`
// Prune deletes the resources of a release this application used to declare
// and no longer does. Off by default: reporting an orphan is safe and
// deleting one is not, so it is a deliberate choice per application.
//
// It governs only this application's own releases — those carrying its
// owner stamp. A release another application or the command line installed
// is unmanaged here and is never touched, whatever this says.
//
// Not to be confused with HistoryMax, which prunes an individual release's
// revision history rather than the release itself. Two senses of the word,
// one of which is the chart engine's; see the prune package.
Prune bool `json:"prune,omitempty" yaml:"prune,omitempty"`
// PruneVolumes extends Prune to the named volumes of what it deletes, and
// means nothing without it — a config declaring one and not the other is
// refused rather than half-obeyed.
//
// Separate from Prune because it is the one irreversible part. Everything
// else prune removes can be recreated from git on the next reconcile; the
// data in a volume cannot be recreated from anything.
PruneVolumes bool `json:"pruneVolumes,omitempty" yaml:"pruneVolumes,omitempty"`
// PruneFirst deletes before installing, instead of after.
//
// The default order applies and then prunes, so a failed apply leaves the
// old release running rather than nothing at all. The cost is that a
// renamed release briefly coexists with the name it replaced: the new name
// is an install and the old one an orphan, and between the two steps both
// are deployed.
//
// For a workload where two instances running at once is worse than none —
// a blockchain validator that would double-sign and be slashed, a job
// runner that must not process a queue twice — that trade is the wrong way
// round. This inverts it: the departing release is deleted before its
// replacement is installed, so they never overlap, and a failed apply
// leaves a gap instead.
//
// It bounds the overlap this controller creates deliberately; it is not a
// distributed lock. Nothing here can prevent two instances during a network
// partition or a node recovering with stale state, so a workload that
// cannot tolerate that at all needs an external guard — a remote signer
// with an anti-slashing record, or a lease.
//
// Means nothing without Prune, and is refused rather than ignored.
PruneFirst bool `json:"pruneFirst,omitempty" yaml:"pruneFirst,omitempty"`
}
SyncPolicy governs when and how a plan is applied. Wait, Timeout and HistoryMax map onto charts.InstallOptions; Interval overrides the controller-wide poll interval for one application.
type SyncResult ¶
type SyncResult struct {
Revision string `json:"revision"`
StartedAt time.Time `json:"startedAt"`
FinishedAt time.Time `json:"finishedAt"`
Succeeded bool `json:"succeeded"`
Error string `json:"error,omitempty"`
}
SyncResult records the outcome of the last sync that was actually attempted.
type SyncState ¶
type SyncState string
SyncState is whether the swarm matches git.
func (SyncState) MarshalJSON ¶
MarshalJSON implements json.Marshaler.
func (*SyncState) UnmarshalJSON ¶
UnmarshalJSON implements json.Unmarshaler.
type SyncSummary ¶
type SyncSummary struct {
Install int `json:"install"`
Upgrade int `json:"upgrade"`
Unchanged int `json:"unchanged"`
}
SyncSummary is the plan that made the state OutOfSync, counted by action.