Documentation
¶
Overview ¶
Package appset sources the set of applications the controller reconciles, so that the applications file is itself GitOps-managed (issue #47).
Two tiers ¶
The bootstrap config says where the app set lives and is the single anchor nothing in git can repoint. The app set — today's applications.yaml, the same config.File schema — lives wherever that says, and changing it is an ordinary commit rather than a controller redeploy.
Two modes, one rule ¶
The set arrives either from a repository the controller pulls itself, reusing the sourcer and the credential the applications already use, or from a path on a volume that an external process — a git-sync sidecar, the Flux source-controller pattern — keeps current. The two modes differ in nothing but how they produce bytes: parsing, validation, change detection and the swap are written once here, so the guarantee below is the same whichever is configured.
Validate before swap, keep last-good ¶
A load parses and fully validates before anything is swapped in. On any failure — unreachable remote, missing file, malformed YAML, an unknown key, a duplicate application name — the last set that validated stays current and the error is returned. The set is never partially applied, so a bad commit cannot break a running controller. Until the first successful load there is no current set and a failure is all the caller gets.
The path mode's one extra rule ¶
The writer must publish atomically: write a temporary file and rename it over the app-set file. Rename is atomic, so a reader sees one whole version or the other and never a half-written one. A writer that rewrites the file in place instead can be read mid-write; that read fails to parse and is therefore a failed load with the last-good set kept, which is the safe outcome but not a substitute for publishing atomically — a truncation that happens to still be valid YAML would be indistinguishable from a set an operator meant to shrink.
Not here ¶
No timer and no goroutine: Load is called by whoever owns the interval. What to do with a changed set — diffing it and driving per-application loops — is the reconcile loop's (issue #52), and the bootstrap flags that choose a mode are issue #53's.
Index ¶
Constants ¶
const DefaultInterval = 3 * time.Minute
DefaultInterval is how often the app set is re-read. It matches the reconciler's own default: an operator tuning "how quickly does a commit take effect" should not have to learn that the two tiers move at different speeds.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Fetcher ¶
type Fetcher interface {
Fetch(ctx context.Context, key string, src application.Source) (git.Checkout, error)
}
Fetcher brings a repository to a revision. *git.Sourcer implements it, which is the whole of this package's dependency on git: the app set is pulled by the same client, with the same credential, as the applications it declares.
type GitConfig ¶
type GitConfig struct {
// RepoURL is https:// or an absolute path, as the sourcer accepts.
RepoURL string
// Revision is a branch, tag or commit. A branch is the useful case here:
// the point of app-of-apps is that a commit to it is picked up.
Revision string
// Path is the app-set file within the repository.
Path string
}
GitConfig locates the app set in a repository the controller pulls itself.
type Loader ¶
type Loader struct {
// contains filtered or unexported fields
}
Loader loads the app set and keeps the last one that validated.
It is safe for concurrent use. The *config.File it hands out is shared with every other caller and with Current, and must be treated as immutable.
func NewGit ¶
NewGit returns a Loader that pulls the app set with the given fetcher.
The fetcher should cache under a root of its own rather than the one the applications clone into: the app set is the bootstrap tier, and keeping its clone separate is what makes its cache key unambiguous.
func NewPath ¶
func NewPath(cfg PathConfig) *Loader
NewPath returns a Loader that reads the app set from a directory an external process keeps current. That writer must publish atomically; see the package documentation.
func (*Loader) Current ¶
Current returns the last set that loaded successfully, or nil if none ever has. It is what keeps running while a load is failing.
func (*Loader) LastLoad ¶
LastLoad reports the commit the current set came from and when it was loaded. The revision is empty in path mode, where there is no commit to report, and the time is zero until the first successful load.
func (*Loader) Load ¶
Load reads, parses and validates the app set.
It returns the current set and whether it changed since the last successful load. A failure returns no set at all — not the last-good one — so that a caller cannot mistake a stale set for a fresh one; the last-good set is reached deliberately, through Current.
type Loop ¶
type Loop struct {
// contains filtered or unexported fields
}
Loop keeps the running set equal to the app set.
On each tick it loads the set and diffs it by application name against what is actually running, adding, replacing and removing accordingly. A load that fails changes nothing: the last-good set keeps running and the reason is reported.
It is deliberately not the thing that decides an application is out of sync — that is the reconciler's, per application, on its own schedule. This loop only decides which applications there are.
func NewLoop ¶
func NewLoop(src *Loader, rec Reconciler, o LoopOptions) *Loop
NewLoop returns a Loop driving rec from src.
func (*Loop) Once ¶
Once loads the app set and brings the running set to it. It is what Run calls, exported so a caller — a test, or a webhook later — can drive one pass without waiting for a tick.
The diff runs on every pass, not only when the file has changed. What is being reconciled towards is the desired state, not the last edit to it: an application that could not be started last tick is still missing, and a loop that only acted on changes would have recorded it as added once and never mentioned it again. Diffing an unchanged set costs a map and a comparison per application and almost always decides to do nothing.
func (*Loop) Run ¶
Run keeps the running set current until ctx is cancelled.
There is no backoff. A failing load is one small file read again in three minutes, and backing off would mean the fix an operator has just committed takes longer to land the longer the mistake went unnoticed — which is the wrong way round for the one loop whose job is to pick up corrections.
func (*Loop) Status ¶
func (l *Loop) Status() application.ControllerStatus
Status reports where the set came from and how the last attempt went.
type LoopOptions ¶
type LoopOptions struct {
// Mode labels how the set is sourced for the status endpoint: "static",
// "git" or "path". The source does not name itself — the bootstrap decides
// what to call the thing it configured.
Mode string
// Source is where that set lives, for the status endpoint to report: the
// repository and revision, or the directory. Like Mode it is passed in
// rather than derived, because it is the bootstrap anchor and describing it
// is the bootstrap's to do.
Source string
Interval time.Duration
Log *slog.Logger
// Credentials resolves the image-pull credential of an application joining
// or changing in the set. The default reads the Docker secret the
// application's registryAuth names, which is the same thing the controller
// does at startup for the applications it booted with.
Credentials func(spec application.Spec) (regauth.Resolver, error)
// Pruner deletes the resources of an application that has left the set.
// Nil is report-only — the default, and what every deployment that has not
// asked for prune gets.
Pruner Pruner
}
LoopOptions tunes a Loop. Everything has a working default.
type PathConfig ¶
PathConfig locates the app set on a volume an external process keeps current.
It is a directory plus a path within it rather than one file path, matching the git mode: what a sidecar syncs is a repository, its content is no more trusted than a checkout's, and both modes then resolve the file through the same containment check.
type Pruner ¶
type Pruner interface {
Departed(ctx context.Context, desired, declared []string) ([]string, error)
}
Pruner deletes the deployed resources of applications absent from the set it is given. *prune.Pruner implements it.
An interface rather than the concrete type because it is the one seam in this loop that deletes things, and a test of the loop's guards must be able to assert that it was never called.
type Reconciler ¶
type Reconciler interface {
Views() []application.View
Add(spec application.Spec) error
Replace(spec application.Spec) error
Remove(name string) error
SetRegistryAuth(app string, resolver regauth.Resolver)
}
Reconciler is the running set the loop steers. *reconcile.Reconciler implements it.
Add and Replace are separate rather than one upsert because the distinction is the loop's to make: replacing keeps an application's recorded status and its running loop, and adding starts one. A caller that could not tell them apart would silently restart every application on every change.