prune

package
v1.0.0-rc5 Latest Latest
Warning

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

Go to latest
Published: Aug 3, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package prune deletes what git no longer declares: the resources of an application that has left the app set, and the rule deciding which services a release still in it has stopped declaring.

The two halves sit here together because they answer the same question at different scopes — "is this provably ours, and provably obsolete" — and get it right the same way, by refusing to act on a single forgeable signal. The sweeping of departed applications is below; the service rule is Undeclared and Claim at the foot of the file, kept as pure functions so that the reconciler owns the reads and this owns the policy.

App-of-apps (#47, decision D-e) is deliberately report-only: an application dropped from the git app set stops being reconciled and its stack is left running and surfaced as orphaned. This package is the acting half (#54), behind its own opt-in, because reporting an orphan is safe and deleting one is not.

Two senses of "prune"

The chart engine already has Engine.Prune, and it means something else: trimming an individual release's revision history to the last N, which this repository exposes as SyncPolicy.HistoryMax. Nothing here touches revision history. "Prune" in this package always means deleting a release's deployed resources.

Why the swarm is the source of truth

The obvious signal — the list of applications the running loop watched leave — does not survive a restart, and a controller that forgot its orphans on every restart would leave exactly the stacks nobody is looking at. So the departure is not remembered; it is rediscovered. Every release this controller installs carries the owner stamp application.OwnerID writes, and that stamp is stored on the swarm. Any release stamped for an application the app set no longer declares has, by definition, been left behind.

The stamp is also what makes deletion safe. A release without one, or with one another tool wrote, is unmanaged here and is never a candidate — the same distinction charts.Plan draws between Orphaned and Unmanaged, for the same reason.

The stamp is not enough on its own

A stamp records who installed a release, and it is only rewritten by a reconcile that deploys. Rename an application without changing anything else and the release is now planned under an owner that contradicts its stamp, which the plan does notice (swarmcli#511) — but noticing corrects the stamp no earlier than the next reconcile that gets that far, and a sweep can run first. Until it does, the release still carries the departed name's stamp, and reading the stamp alone would delete a stack that a current application is reconciling (#62). So the sweep is given the releases the current applications declare as well, and spares anything in that list. The stamp says who installed it; that list says whether anybody is still responsible for it.

Two controllers on one swarm

The stamp names a controller as well as an application, and the sweep only considers its own. Without that, a second swarmcli-cd sharing the swarm would answer "which releases belong to an application my app set no longer declares" about the *first* controller's applications, and delete them — a silent, swarm-wide deletion of somebody else's work, triggered by nothing more than starting a second controller.

Two controllers on one swarm must therefore be given distinct ids. The default is shared, so two controllers that both take it will still collide; that is a deployment mistake this package cannot detect from the inside, because a controller cannot tell "my own releases from before a restart" from "another controller using my id".

One swarm

The sweep covers the swarm this controller runs in, because swarms.Registry resolves exactly that one and cannot enumerate others (D2). That is complete for the OSS build.

swarms.Lister is now the shape a multi-swarm companion enumerates through, and this does not yet use it. The missing piece is not here: the sweep spares any release the current applications still declare, and it matches those by release name, which is unique within a swarm and not across them. Handed several swarms and one unqualified list, it would spare a release on swarm B because a different one shares its name on swarm A — and delete one on B that nothing on B declares. Making that safe is a change to what the app-set loop hands this, and it belongs with the companion that can test it.

One node

The releases are found through the swarm's manager API, but a stack's named volumes are not: the daemon's volume list answers from the node's own store, and an ordinary named volume is created by the engine that ran the task mounting it. swarms.NodeReach is how a registry says it can reach the nodes themselves, and purgeVolumes uses it when it is there. The OSS default is not — see swarms.NodeReach for why that is a deployment fact rather than an omission — so purgeThisNode deletes what the controller's own node holds and says exactly that.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Claim

func Claim(candidates, declared []string) (claimed, rest []string)

Claim splits candidates by whether declared names them: what this revision proves the repository once asked for, and what is still unaccounted for.

Taking one revision at a time rather than the union of a whole history is what lets a caller walk revisions newest first and stop as soon as nothing is left unaccounted for — so the ordinary case, a resource dropped last week, reads one revision rather than every revision ever recorded. A caller sweeping several kinds converts each revision once and calls this per kind, so the walk costs what one kind costs.

A candidate no revision of ours ever claims is not ours to delete. It may be another tool's, or ours from before the retained history; from here those are the same thing, which is that ownership cannot be proved.

func Owner

func Owner(rel charts.Release, controller string) (string, bool)

Owner reports which of this controller's applications installed a release, and whether this controller installed it at all. It reads a stored revision the same way, since a revision carries the stamp its release does.

Both halves of the stamp have to agree, which is the check charts makes before it will call a release orphaned. An id-only comparison would treat a stamp copied onto a second release as proof of ownership, and prune would then delete a release nobody installed under that name. A stamp that does not parse is not evidence of anything either, so it counts as unowned — and unowned is never pruned.

func Release

func Release(ctx context.Context, log *slog.Logger, backend charts.Backend, engine Uninstaller, release string, volumes VolumePurge) (*charts.UninstallResult, error)

Release deletes one release's deployed resources and then its records, in that order, and reports the networks the engine left behind.

The order is the whole point. charts.Engine.Uninstall aggregates its errors and deletes the release-history configs regardless of whether its own stack removal worked — and those configs carry the owner stamp, which is the only thing that lets a later sweep find this release again. Calling it directly on a stack that will not come down would delete the evidence and strand the resources permanently, invisible to every future prune. So the resources come down first, through the backend, and the records are only deleted once there is nothing left for them to point at.

This is shared with the per-application prune in reconcile, which faces the same hazard for the same reason.

log is taken rather than returned-to, because what a volume purge managed is not a value the two callers would do anything different with — it is a thing an operator has to be told, and only this knows what was and was not covered.

func Undeclared

func Undeclared(running, declared []string) []string

Undeclared names the resources of one kind carrying a release's namespace that its rendered manifest does not declare.

Both sides are namespace-scoped names — "<release>_<name>" — because that is what a live resource carries and the only key the two sides can be matched on.

On its own this is not permission to delete anything; it is the candidate list for the ownership check. Sorted, so what gets logged and reported does not reshuffle between sweeps.

Types

type Engine

type Engine interface {
	Uninstaller
	List(ctx context.Context) ([]charts.Release, error)
}

Engine is the part of the chart engine a sweep uses. *charts.Engine implements it.

Declared here rather than reused from reconcile so that prune does not import the reconciler for an interface it can state itself.

type Options

type Options struct {
	Swarms swarms.Registry
	Engine func(charts.Backend) Engine
	// Volumes extends the deletion to the named volumes of what it removes.
	// Off by default, and the only irreversible part: everything else prune
	// deletes is recreated from git the moment the application comes back.
	Volumes bool
	// ControllerID is this controller's half of the owner stamp, and bounds
	// what the sweep will consider at all. Empty is
	// application.DefaultControllerID — which is correct for the single
	// controller case and wrong the moment there are two, so a second
	// controller on the swarm must be given its own.
	ControllerID string
	Log          *slog.Logger
}

Options configures a Pruner. Everything has a working default.

type Pruner

type Pruner struct {
	// contains filtered or unexported fields
}

Pruner deletes the releases of applications that have left the app set.

func New

func New(o Options) *Pruner

New returns a Pruner. A nil Pruner is the report-only default, so callers construct one only when prune is enabled.

func (*Pruner) Departed

func (p *Pruner) Departed(ctx context.Context, desired, declared []string) ([]string, error)

Departed deletes the releases of every application this controller owns that is absent from desired, and returns the applications it emptied.

desired must be an app set that has actually loaded. The caller vouches for that: this cannot tell "the set declares nothing" apart from "the set could not be read", and the two want opposite things done. See appset.Loop, which only calls this after a load and an apply have both succeeded.

declared names the releases the applications still in the set hold, and no release named in it is ever deleted whatever its stamp says. That is what makes renaming an application a handover rather than a teardown (#62): the release keeps the departed application's stamp until a reconcile deploys it under the new one, which is the next pass for an automated application and not until it is asked for a manual one — either way, later than a sweep that runs in between. The stamp answers "who installed this"; declared answers "is anybody still responsible for it", and only the second is a safe basis for deleting.

Every application is attempted and the failures are collected rather than returned at the first. They are independent, and because the next sweep rediscovers whatever is still stamped on the swarm, a failure costs a reconcile interval rather than an orphan nobody looks at again.

type Uninstaller

type Uninstaller interface {
	Uninstall(ctx context.Context, release string, purgeVolumes bool) (*charts.UninstallResult, error)
}

Uninstaller deletes a release's recorded revisions once its resources are gone. *charts.Engine implements it.

type VolumePurge

type VolumePurge struct {
	// Enabled is the opt-in, and the zero value is off. Volumes are the only
	// irreversible thing a prune deletes: everything else it removes is
	// recreated from git the moment the application comes back.
	Enabled bool
	// Registry is what the purge asks for a handle to each node of the
	// destination. A registry implementing swarms.NodeReach makes the purge
	// cover every node of the swarm; one that does not — the OSS default, for
	// the deployment reasons swarms.NodeReach gives — leaves it covering the
	// node the controller talks to, which is what it has always covered. Nil is
	// the same as one that cannot.
	Registry swarms.Registry
	// Target is the destination whose nodes to reach. It is ignored by a
	// registry that cannot reach any.
	Target swarms.Target
}

VolumePurge says whether to delete a release's named volumes, and how far the caller can reach to find them.

The reach is here rather than taken from the seam inside the purge because prune owns policy and not wiring: a package that resolved a process-global registry behind its caller's back would be untestable without registering one, and Release's two callers would stop being able to say what they meant.

A struct rather than the bool this replaced, so that the answer to "how far can you reach" can grow — a node allowlist, a per-node deadline — without a third parameter appearing on Release each time.

Jump to

Keyboard shortcuts

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