charts

package
v1.13.0-rc3 Latest Latest
Warning

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

Go to latest
Published: Jul 21, 2026 License: Apache-2.0 Imports: 28 Imported by: 0

README

SwarmCLI Charts

A Helm-inspired package manager for Docker Swarm. Charts package Docker Stack (Compose) templates with default values and metadata; installing a chart produces a release — a Docker stack whose revision history is stored in Docker Configs.

Implemented so far (issue #413): repository management, discovery, templating, the full release lifecycle — install, upgrade, rollback, uninstall, list, status, history, diff, get, and prune — and declarative releases (apply, outdated). Chart-dev tooling (create, lint, dependency) and subchart resolution are the remaining phase.

Invocation

Charts run through swarmcli's non-interactive CLI: when the binary is given arguments it executes a one-shot command and exits (a bare swarmcli still launches the TUI). This makes charts scriptable for CI/CD and GitOps.

swarmcli charts repo add eldara https://charts.example.com
swarmcli charts repo update
swarmcli charts search traefik
swarmcli charts show values eldara/traefik > values.yaml
swarmcli charts template my-traefik eldara/traefik -f values.yaml
swarmcli charts install  my-traefik eldara/traefik -f values.yaml
swarmcli charts status   my-traefik
swarmcli charts diff upgrade my-traefik eldara/traefik --set replicas=3
swarmcli charts upgrade  my-traefik eldara/traefik --set replicas=3
swarmcli charts history  my-traefik
swarmcli charts rollback my-traefik 1
swarmcli charts list
swarmcli charts uninstall my-traefik

A chart reference is either a configured repo/chart or a local path to a chart directory or packaged .tgz. --version selects a version from a repository index, so it applies only to a repo/chart reference — a local chart carries its version in its own Chart.yaml, and passing --version with one is an error rather than being silently ignored.

Declarative releases (GitOps)

The commands above are imperative, and release state lives in the swarm — so nothing in git says what should be running. charts apply closes that gap: it converges the swarm to a file you commit.

# swarmcli-release.yaml
apiVersion: v1
owner: prod-swarm              # optional; see Ownership below
repositories:
  - name: swarmcli-charts
    url: https://eldara-tech.github.io/swarmcli-charts
releases:
  - name: edge
    chart: swarmcli-charts/traefik
    version: "0.1.1"
    values: [./traefik.yaml]     # relative to THIS FILE, not the working directory
  - name: hello
    chart: swarmcli-charts/whoami
    version: "0.1.8"
swarmcli charts apply -f swarmcli-release.yaml --dry-run   # plan
swarmcli charts apply -f swarmcli-release.yaml --diff      # plan + manifest diffs
swarmcli charts apply -f swarmcli-release.yaml             # converge
swarmcli charts outdated                                   # what has a newer chart?
Behaviour
Missing release installed
Changed chart version, values, or rendered manifest upgraded
Identical skipped — no new revision
On the swarm but not in the file reported, never removed
Installed by this file, no longer in it reported as an orphan, still never removed

Two of those deserve the emphasis. Releases are never deleted — apply prints the uninstall command and leaves the decision to you. And an unchanged release is skipped entirely: history is one Docker Config per revision, so re-applying on every CI push would otherwise grow the swarm's config store without bound.

Ownership

owner: names the manifest, and every release it installs is stamped with that name — in the release-history Config's com.swarmcli.owner label and in the stored record — so a later apply can tell a release this file installed from one it has simply never seen. Dropping a release from the file then reports it as an orphan: provably obsolete, because the stamp says this manifest produced it and nothing else claims it. A release with no stamp, or another manifest's stamp, stays merely unmanaged.

Nothing is deleted either way. The distinction is the prerequisite for a prune that could be: it is what separates "this is obsolete" from "I do not recognise this", and only the first is ever safe to act on.

com.swarmcli.owner: apply/prod-swarm:release/hello
                    └──── id ─────┘ └── resource ──┘

The stamp names the resource as well as the owner, and both halves must match for it to count. A bare owner string cannot tell a release this file installed from a copy of one — ArgoCD shipped exactly that as app.kubernetes.io/instance and replaced it in 3.0 for the same reason. The apply/ prefix keeps a manifest applied from the command line from colliding with a controller that happened to pick the same name.

owner: is optional and has no default. A derived one would either change between a laptop and a CI checkout (a path hash) or be shared by every repository using the conventional filename (a basename) — and either would let two unrelated manifests claim each other's releases. Omit it and nothing is claimed, which is exactly the behaviour of every version before this one.

For a repo/chart, version is required — a floating pin would silently upgrade production on the next apply. For a local chart path (chart: ./charts/mine, resolved against the file) it must be omitted: the chart's own Chart.yaml sets the version, so there is nothing to select.

Unknown keys are rejected, so a typo fails loudly instead of quietly doing nothing. Releases are applied in file order; add --wait if a later release needs an earlier one live.

apply honours --wait, --timeout and --history-max. It rejects --set, --version, --reuse-values, --install, --purge-volumes, --requirements and --revision rather than ignoring them — the file is the only source of truth, so a value passed on the command line would be a lie. --diff implies --dry-run and never deploys.

Keeping it up to date automatically

If your charts come from swarmcli-charts, extend its Renovate preset — that is the whole configuration:

{ "extends": ["github>Eldara-Tech/swarmcli-charts"] }

For any other chart repository, one line does the same job:

{ "helmfile": { "managerFilePatterns": ["/(^|/)swarmcli-release\\.ya?ml$/"] } }

Both work because the file's key names match Helmfile's, so Renovate's built-in helmfile manager reads it — no custom regex to maintain. Renovate resolves each chart against the repositories you declared and opens a PR bumping version: when a new chart version is published, with the chart's release notes attached. Merge it, and swarmcli charts apply in CI rolls it out.

Chart format

mychart/
├── Chart.yaml          # apiVersion, name, version, appVersion, swarmcliVersion, …
├── values.yaml         # default values
├── values.schema.json  # optional JSON Schema validated before render
├── README.md
└── templates/          # Go-templated Compose fragments
    ├── stack.yaml
    ├── configs.yaml
    ├── secrets.yaml
    └── volumes.yaml

apiVersion is v1 (the only format this build reads; absent means v1).

Declaring the swarmcli a chart needs

A chart may state the chart engine it requires, as a SemVer constraint:

# Chart.yaml
swarmcliVersion: ">= 1.13.0"

install, upgrade and apply refuse a chart this build is too old for, so the failure names the version to upgrade to rather than surfacing as whatever error the missing feature happens to produce (function "toYamlPretty" not defined). install and upgrade ask before refusing when run interactively; apply never does — it is meant to run unattended. template, diff and show only warn: they change nothing, and show is how you find out what a chart wants. Pass --skip-compat-check to proceed anyway.

The constraint is checked against the chart engine's version, which is not necessarily the version the binary reports for itself — a binary embedding this package carries whichever engine it pinned. A build that reports no engine version (any go build) warns rather than refusing: not knowing is not evidence of incompatibility. Charts declaring nothing are unaffected.

Note that only builds carrying this check can honour it: Chart.yaml is parsed leniently, so an older swarmcli silently ignores swarmcliVersion — the same bootstrapping limit Helm's own apiVersion gate has.

Linting a chart
swarmcli charts lint ./mychart                       # against this build
swarmcli charts lint ./mychart -f ./ci/default-values.yaml
swarmcli charts lint ./mychart --for-version 1.12.0  # against another version

lint renders the chart and reports every problem it finds — a broken template, values that fail values.schema.json, a swarmcliVersion this build does not satisfy — rather than stopping at the first. It renders from the chart defaults, layering any -f/--set on top: a chart with a required, undefaulted input (a {{ required }} / {{ fail }} guard) cannot render from bare defaults, so lint it with the values a real install would supply. A chart that declares no swarmcliVersion gets a warning, not an error: the field is optional, but a chart naming no floor leaves an operator on an old build nothing to act on.

--for-version asks whether the chart's declared floor admits that version. It cannot tell you the chart runs on it: this binary carries one engine's behaviour and cannot emulate another's, so it checks the claim's shape, not its truth. Rendering with a real binary of that version is the only thing that settles it — which is what a chart repository's CI should do:

SWARMCLI_REF=v1.10.0 scripts/install-swarmcli.sh ./bin  # build the declared floor
./bin/swarmcli charts template t ./mychart              # prove the chart runs on it

Templates are rendered with Go text/template + Sprig, exposing:

  • .Values — merged values (defaults < -f files < --set)
  • .Release.Name, .Namespace, .Revision
  • .Chart.Name, .Version, .AppVersion

Each templates/*.yaml is rendered then deep-merged into a single Compose document (Compose is one document, unlike Helm's concatenated manifests). Files beginning with _ (e.g. _helpers.tpl) define named templates only. The merged manifest is validated as a Docker stack before use.

Repositories

A repository is an HTTP(S)-served index.yaml listing chart versions, each with a tarball URL (Helm repository format) — hostable on GitHub Pages/Releases, S3, or any static host. Configured repos and cached indexes live under $XDG_STATE_HOME/swarmcli/charts (default ~/.local/state/swarmcli/charts).

Integrity

The index is fetched over HTTPS from the repository, but a chart's tarball URL may point anywhere — a GitHub Release asset, a CDN. The digest the index publishes for each version is what binds the two together, so swarmcli verifies it:

Index entry Behaviour on install/upgrade/template
digest: sha256:<hex> matches the download installs
digest does not match fataldigest mismatch … refusing to install
digest uses an algorithm other than sha256 fatal — verification cannot be performed, so it is not skipped
entry publishes no digest installs, with a warning on stderr

An absent digest warns rather than fails because nothing was verified before this existed, so rejecting would break every repository that publishes none — including older and hand-written ones. Both the bare-hex form (helm repo index) and the sha256:-prefixed form are accepted.

Chart archives are also capped at 20 MiB on the wire (the decompressed contents have their own limits), so a hostile or truncated download cannot exhaust memory before it is hashed.

TLS is not sufficient on its own here: it authenticates the host you downloaded from, not that the bytes are the ones the repository index vouched for.

Release storage

Each release revision is stored as an immutable, gzipped Docker Config named swarmcli.release.<release>.v<N>, labeled com.swarmcli.*. The log is append-only: the highest revision is the current state; lower deployed revisions display as superseded. This gives HA (Swarm Raft) and rollback with no external database.

docker config ls --filter label=com.swarmcli.release=my-traefik

Pruning release history

Because every revision is its own Config, history grows without bound. Trim it with the retention window — keep only the newest N revisions:

# Apply a window inline, after the deploy:
swarmcli charts install my-traefik eldara/traefik --history-max 20
swarmcli charts upgrade my-traefik eldara/traefik --history-max 20

# Or prune existing history on demand:
swarmcli charts prune my-traefik --history-max 20   # one release
swarmcli charts prune --history-max 20              # every release
swarmcli charts prune my-traefik --history-max 20 --dry-run  # preview only

prune deletes the oldest revisions beyond the window and always keeps the current (highest) revision, so the live release and rollback targets inside the window are preserved. Without --history-max (or with 0) it keeps everything and reports that no window was given. --dry-run prints the keep/delete decision without touching Docker.

Use swarmcli charts prune, not raw Docker. docker config prune, docker system prune and docker config rm swarmcli.release.* are not SwarmCLI-aware and will corrupt release history, rollback targets and audit lineage. SwarmCLI is the only sanctioned way to delete a release's resources.

Per-revision protection labels (com.swarmcli.keep, com.swarmcli.protected) are a planned Phase-3 addition; today the current revision plus the newest-N window are the protections.

Notes & limitations

  • install --dry-run renders, validates, and computes the next revision but does not deploy. For fully offline rendering use charts template.
  • --purge-volumes removes volumes on the connected node only (the CE single-node volume scope); cross-node purge is a future extension.
  • Secrets: the rendered manifest is stored in a Docker Config, which is readable by anyone with Docker access. Do not inline secret material in templates — reference Docker secrets as separate objects instead. A redaction pass is planned before charts ship broadly.
  • Chart integrity is only as good as the index: a repository that publishes no digest gets a warning, not a refusal (see Integrity). Chart archives are capped at 20 MiB on the wire.
  • docker stack deploy is used under the hood, so only the Compose-on-Swarm subset is supported and the docker CLI must be on PATH.

Documentation

Overview

Package charts implements a Helm-inspired package manager for Docker Swarm.

A chart is a versioned package of Docker Stack (Compose) templates plus default values and metadata. Installing a chart produces a release: a Docker stack whose revision history is stored append-only in Docker Configs. The package is pure Go (no Bubble Tea / TUI) so it can back both the non-interactive CLI and, later, a TUI browser view.

Index

Constants

View Source
const (
	LabelType         = "com.swarmcli.type"          // always "release"
	LabelRelease      = "com.swarmcli.release"       // release name
	LabelChart        = "com.swarmcli.chart"         // chart name
	LabelChartVersion = "com.swarmcli.chart.version" // chart SemVer
	LabelRevision     = "com.swarmcli.revision"      // revision number
	LabelStatus       = "com.swarmcli.status"        // see Status* constants
	LabelCreated      = "com.swarmcli.created"       // RFC3339 timestamp
	// LabelOwner is the only optional one: "<id>:<kind>/<name>" (see OwnerRef),
	// absent entirely when nothing claimed the release.
	LabelOwner = "com.swarmcli.owner"

	TypeRelease = "release"
)

Label keys applied to release-history Docker Configs. They mirror the scheme documented in issue #413 and let list/status/history queries filter Configs by release without an external database.

View Source
const (
	StatusPendingInstall = "pending-install"
	StatusDeployed       = "deployed"
	StatusSuperseded     = "superseded"
	StatusFailed         = "failed"
	StatusUninstalled    = "uninstalled"
)

Release status values, following Helm's deploy/superseded/failed lifecycle.

View Source
const OwnerKindRelease = "release"

OwnerKindRelease is the resource kind of a release-history record, the only thing an owner stamp names today.

Variables

This section is empty.

Functions

func EngineVersion

func EngineVersion() string

EngineVersion reports the chart-engine version this binary embeds, or the empty string for an unstamped build.

func HasErrors

func HasErrors(findings []LintFinding) bool

HasErrors reports whether any finding is fatal, i.e. whether the lint failed.

func IsPathRef

func IsPathRef(ref string) bool

IsPathRef reports whether ref names a local chart path rather than a "<repo>/<chart>" reference. The test is deliberately SYNTACTIC: a release file is committed to git and must resolve the same way on every machine, so whether a reference is a path cannot depend on what happens to exist on the disk of whichever CI runner picked up the job.

func MergeValues

func MergeValues(defaults map[string]any, files [][]byte, sets []string) (map[string]any, error)

MergeValues computes the effective values for a render, applying Helm's precedence: chart defaults < each --values file (in order) < --set overrides. Maps are deep-merged; scalars and sequences replace.

func Render

func Render(ch *Chart, ctx RenderContext) (string, error)

Render evaluates every templates/*.yaml file with text/template + Sprig, deep-merges the resulting Compose fragments into a single document, and returns it as validated YAML. Files whose names start with "_" (e.g. templates/_helpers.tpl) define named templates only and emit no document.

func ValidateValues

func ValidateValues(schema []byte, values map[string]any) error

ValidateValues checks merged values against a chart's values.schema.json (JSON Schema). A nil/empty schema is a no-op.

Types

type Action

type Action string

Action is what apply will do to one release.

const (
	ActionInstall   Action = "install"
	ActionUpgrade   Action = "upgrade"
	ActionUnchanged Action = "unchanged"
)

type ApplyResult

type ApplyResult struct {
	Name     string `json:"name"`
	Action   Action `json:"action"`
	Revision int    `json:"revision,omitempty"` // 0 when unchanged (nothing was recorded)
}

ApplyResult is what Apply actually did to one release.

type Backend

type Backend interface {
	DeployStack(name, manifest string, resolve string) error
	RemoveStack(name string) error
	// RefreshSnapshot invalidates the shared Docker state cache after a mutation
	// so subsequent reads (status, convergence polling) do not see stale data.
	RefreshSnapshot() error
	CreateConfig(ctx context.Context, name string, data []byte, labels map[string]string) error
	ListConfigs(ctx context.Context) ([]ConfigMeta, error)
	InspectConfig(ctx context.Context, name string) ([]byte, error)
	DeleteConfig(ctx context.Context, name string) error
	StackServices(name string) []ServiceState
	StackVolumes(ctx context.Context, name string) ([]string, error)
	RemoveVolume(ctx context.Context, name string) error
	// NetworkScopes returns existing network names mapped to their scope
	// (e.g. "swarm", "local"), used to pre-flight a chart's external networks.
	NetworkScopes(ctx context.Context) (map[string]string, error)
	// CreateOverlayNetwork creates a swarm-scoped network with the given driver
	// and attachability (driver defaults to "overlay" when a chart does not
	// declare one in requirements.yaml).
	CreateOverlayNetwork(ctx context.Context, name, driver string, attachable bool) error
	// RemoveOverlayNetwork removes a network by name, used to roll back networks
	// auto-created for an install whose deploy then failed. A no-op if absent.
	RemoveOverlayNetwork(ctx context.Context, name string) error
	// SecretNames returns the set of existing swarm secret names, used to
	// pre-flight a chart's external secrets (which cannot be auto-created).
	SecretNames(ctx context.Context) (map[string]struct{}, error)
}

Backend abstracts the Docker operations the release engine needs, so the lifecycle logic is unit-testable without a live Swarm.

func NewDockerBackend

func NewDockerBackend(ctxName string) Backend

NewDockerBackend returns a Backend bound to an explicitly named Docker context, for callers that must address a specific swarm rather than the one the process happens to be pointed at.

Pair it with NewEngineWith. The three pieces of process-global state this avoids are the SDK client singleton, the `docker context show` lookup the exec-based stack commands do, and the shared snapshot cache — all three, not just the last: a backend that deployed to one swarm and read its history, networks and convergence from another would be worse than an honestly single-swarm one, because nothing would report the mismatch.

type Chart

type Chart struct {
	Metadata        Chartfile
	Values          map[string]any    // parsed values.yaml (defaults)
	ValuesRaw       []byte            // raw values.yaml bytes, nil if absent (preserves comments/order)
	Schema          []byte            // raw values.schema.json, nil if absent
	Templates       map[string]string // template path -> source, e.g. "templates/stack.yaml"
	Readme          string            // README.md, empty if absent
	Requirements    *Requirements     // parsed requirements.yaml (raw, unrendered), nil if absent
	RequirementsRaw []byte            // raw requirements.yaml bytes, nil if absent; re-rendered with values at pre-flight
}

Chart is a loaded chart: its metadata, default values, optional values schema, and raw template sources keyed by their path under templates/.

func LoadChartArchive

func LoadChartArchive(r io.Reader) (*Chart, error)

LoadChartArchive loads a chart from a gzipped tar (.tgz) stream. The archive is expected to contain a single top-level directory (the chart), as produced by chart packaging; the leading directory component is stripped.

func LoadChartDir

func LoadChartDir(dir string) (*Chart, error)

LoadChartDir loads a chart from a directory on disk.

type ChartHit

type ChartHit struct {
	Repo  string
	Entry IndexEntry
}

ChartHit is a search result: one chart version in a repository.

type ChartMeta

type ChartMeta struct {
	Name       string
	Version    string
	AppVersion string
}

ChartMeta is the .Chart object available to templates.

type ChartSource

type ChartSource interface {
	// Load returns the chart named by ref. ref is either a local path (a chart
	// directory or a .tgz) or a "<repo>/<chart>" reference resolved through the
	// configured repositories. version selects a repository chart version; it is
	// meaningless for a local path and rejected there rather than ignored.
	Load(ref, version string) (*Chart, error)
}

ChartSource resolves a chart reference to a loaded chart. It is the seam that lets release planning be unit-tested without a repository, a network or a filesystem: the whole of Engine.PlanApply depends on this interface and not on RepoStore.

func NewChartSource

func NewChartSource(store *RepoStore) ChartSource

NewChartSource returns the standard source, backed by the configured chart repositories for "<repo>/<chart>" references.

type Chartfile

type Chartfile struct {
	APIVersion string `yaml:"apiVersion"`
	Name       string `yaml:"name" json:"name"`
	Version    string `yaml:"version" json:"version"`
	AppVersion string `yaml:"appVersion,omitempty" json:"appVersion,omitempty"`
	// SwarmcliVersion is a SemVer constraint on the chart engine this chart
	// needs, e.g. ">= 1.13.0". Optional; absent means any. It constrains the
	// engine's version rather than the running binary's — see buildinfo.go.
	SwarmcliVersion string       `yaml:"swarmcliVersion,omitempty"`
	Description     string       `yaml:"description,omitempty"`
	Maintainers     []Maintainer `yaml:"maintainers,omitempty"`
	// Dependencies are parsed but not resolved in Phase 1 (subcharts are Phase 3).
	Dependencies []Dependency `yaml:"dependencies,omitempty"`
}

Chartfile is the parsed Chart.yaml metadata.

type CompatFinding

type CompatFinding struct {
	Chart    string       `json:"chart"`              // "<name> <version>", for messages
	Required string       `json:"required,omitempty"` // the chart's constraint as declared, e.g. ">= 1.13.0"
	Engine   string       `json:"engine,omitempty"`   // this build's chart-engine version; "" when unstamped
	Status   CompatStatus `json:"status"`
	Reason   string       `json:"reason,omitempty"` // why the check was skipped; set only with CompatUnknown
}

CompatFinding is the result of checking one chart against this build.

func CheckCompat

func CheckCompat(cf Chartfile) CompatFinding

CheckCompat classifies a chart's swarmcliVersion constraint against the chart engine this binary embeds.

func CheckCompatAgainst

func CheckCompatAgainst(cf Chartfile, engine string) CompatFinding

CheckCompatAgainst classifies a chart's swarmcliVersion against an arbitrary chart-engine version rather than this build's. It is what lets `charts lint --for-version` ask "does this chart's declared floor admit X?" without an X to hand.

Note what that question is NOT: whether the chart actually runs on X. This binary carries one engine's behaviour, so it cannot emulate another's — only a real X can prove that. This checks the claim's shape, not its truth.

It never returns an error. A constraint this build cannot make sense of yields CompatUnknown, not a failure: the check is a compatibility aid, not a security boundary — a chart already renders to an arbitrary stack — so failing open on our own inability to parse costs nothing, whereas failing closed would break working charts for a cosmetic reason.

func (CompatFinding) Message

func (f CompatFinding) Message(binaryVersion string) string

Message renders the finding as a one-line diagnostic naming what the chart wants and what this build has.

binaryVersion is the version the binary reports for itself. When it differs from the engine's, both are named: a binary embedding this module may carry its own version, and naming only the engine's would cite a release the user cannot map back to anything they installed. Pass "" to name only the engine.

type CompatStatus

type CompatStatus int

CompatStatus classifies a chart's declared engine requirement against this build.

const (
	// CompatUnknown means the chart declared no requirement, this build reports
	// no engine version, or the declared constraint could not be parsed.
	// Callers must not block on it: it is not evidence of an incompatible chart.
	CompatUnknown CompatStatus = iota
	// CompatOK means this build's chart engine satisfies the constraint.
	CompatOK
	// CompatIncompatible means it does not. This is the only status callers
	// block on.
	CompatIncompatible
)

func (CompatStatus) MarshalJSON

func (s CompatStatus) MarshalJSON() ([]byte, error)

MarshalJSON writes the name rather than the iota. A bare int in an API payload is a number whose meaning lives only in this file: a client cannot read it without a copy of the constant block, and appending a status later would silently renumber nothing but still leave every existing reader guessing. The names are the contract.

func (CompatStatus) String

func (s CompatStatus) String() string

String names the status for humans and for JSON. The zero value is deliberately the safe one, so an unset Status reads as "unknown" rather than as a claim about the chart.

func (*CompatStatus) UnmarshalJSON

func (s *CompatStatus) UnmarshalJSON(b []byte) error

UnmarshalJSON reads what MarshalJSON writes, so the type still round-trips. An unrecognised name decodes to CompatUnknown rather than failing: a newer producer naming a status this build does not know is not a reason to reject the whole document, and "unknown" is the status callers already must not block on.

type ConfigMeta

type ConfigMeta struct {
	Name   string
	Labels map[string]string
}

ConfigMeta is the edition-agnostic view of a stored release Config.

type Dependency

type Dependency struct {
	Name       string `yaml:"name"`
	Version    string `yaml:"version"`
	Repository string `yaml:"repository,omitempty"`
}

Dependency is a declared subchart requirement (resolution deferred to Phase 3).

type Engine

type Engine struct {
	Backend Backend
	// contains filtered or unexported fields
}

Engine drives release lifecycle operations against a Backend.

func NewEngine

func NewEngine() *Engine

NewEngine returns an Engine bound to the live Docker backend.

func NewEngineWith

func NewEngineWith(b Backend) *Engine

NewEngineWith returns an Engine bound to a custom backend (used in tests).

func (*Engine) Apply

func (e *Engine) Apply(ctx context.Context, plan *Plan, opts InstallOptions) ([]ApplyResult, error)

Apply converges the swarm to a plan, in file order.

It never deletes. A release on the swarm that is absent from the file is reported and left alone — either as Plan.Unmanaged, where nothing says which manifest produced it and so it may belong to a second file or to a human, or as Plan.Orphaned, where its owner stamp names this file and it is therefore provably obsolete. Only the second is safe to remove, which is what the stamp exists to establish; acting on it is a separate change.

Unchanged releases are skipped entirely. That is not an optimisation but a requirement: history is one Docker Config per revision, so an apply that recorded a revision even when nothing changed would grow the swarm's config store on every CI run, forever.

It stops at the first failure and returns the results completed so far alongside the error, so a partial apply still reports what it did. Re-running is safe: the successful releases become no-ops.

func (*Engine) GetRevision

func (e *Engine) GetRevision(ctx context.Context, release string, rev int) (*Release, error)

GetRevision returns a specific revision of a release, or the current one when rev <= 0.

func (*Engine) History

func (e *Engine) History(ctx context.Context, release string) ([]Release, error)

History returns every stored revision of a release, ascending, with derived display statuses.

func (*Engine) Install

func (e *Engine) Install(ctx context.Context, release string, chart ReleaseChart, values map[string]any, manifest string, opts InstallOptions) (*Release, error)

Install deploys a freshly rendered manifest as revision 1 of a new release and records it. It refuses to install over an existing, non-uninstalled release (use upgrade — Phase 2). manifest must already be rendered and validated.

func (*Engine) List

func (e *Engine) List(ctx context.Context) ([]Release, error)

List returns the current (highest) revision of every release.

func (*Engine) PlanApply

func (e *Engine) PlanApply(ctx context.Context, rf *ReleaseFile, src ChartSource) (*Plan, error)

PlanApply computes what Apply would do, without writing anything.

Every release is resolved, merged, schema-validated and rendered BEFORE any of them is deployed. A bad value in the third release therefore aborts the whole apply instead of leaving the swarm half-converged — and `--dry-run` is just "stop after planning".

func (*Engine) Prune

func (e *Engine) Prune(ctx context.Context, release string, keep int, dryRun bool) (PruneResult, error)

Prune deletes superseded revisions of one release beyond the keep window, always retaining the current (highest) revision. keep <= 0 keeps everything. On a dry run no Config is touched. Deletion failures are aggregated and returned, but pruning of the remaining revisions continues.

func (*Engine) PruneAll

func (e *Engine) PruneAll(ctx context.Context, keep int, dryRun bool) ([]PruneResult, error)

PruneAll prunes every release to the keep window, returning one result per release (sorted by name). Per-release errors are aggregated; a failing release does not stop the others.

func (*Engine) Rollback

func (e *Engine) Rollback(ctx context.Context, release string, targetRev int, opts InstallOptions) (*Release, error)

Rollback deploys a new revision whose content is copied from a previous revision (append-only, mirroring Helm). targetRev must be an existing, non-failed revision.

func (*Engine) Status

func (e *Engine) Status(ctx context.Context, release string) (*Release, []ServiceState, error)

Status returns the current release plus live service states for its stack.

func (*Engine) Uninstall

func (e *Engine) Uninstall(ctx context.Context, release string, purgeVolumes bool) (*UninstallResult, error)

Uninstall removes the release's stack and its recorded revisions, retaining volumes unless purgeVolumes is set. It returns an UninstallResult describing the auto-created external networks left in place so the caller can surface them; the networks themselves are not removed.

func (*Engine) Upgrade

func (e *Engine) Upgrade(ctx context.Context, release string, chart ReleaseChart, values map[string]any, manifest string, opts InstallOptions) (*Release, error)

Upgrade deploys a new revision of an existing release. When the release does not exist it errors unless opts.Install is set (the `upgrade --install` behavior). manifest must already be rendered and validated.

type Index

type Index struct {
	APIVersion string                  `yaml:"apiVersion"`
	Entries    map[string][]IndexEntry `yaml:"entries"`
}

Index is the parsed index.yaml of a chart repository, mapping a chart name to its available versions (Helm repository index format, subset).

type IndexEntry

type IndexEntry struct {
	Name        string   `yaml:"name"`
	Version     string   `yaml:"version"`
	AppVersion  string   `yaml:"appVersion,omitempty"`
	Description string   `yaml:"description,omitempty"`
	URLs        []string `yaml:"urls"` // tarball download URLs (absolute or index-relative)
	Digest      string   `yaml:"digest,omitempty"`
}

IndexEntry describes one published chart version within an Index.

type InstallOptions

type InstallOptions struct {
	DryRun     bool
	Wait       bool
	Install    bool // upgrade: create the release if it does not exist
	Timeout    time.Duration
	HistoryMax int // 0 = keep all
	// ResolveImage selects how the daemon resolves image tags at deploy time
	// ("always" | "changed" | "never"); empty leaves Docker's default of
	// "always". See docker.ResolveImage for why "changed" suits automation.
	ResolveImage string
	// Requirements is the chart's parsed requirements.yaml, when present. It
	// drives the external-resource pre-flight (auto-create vs validate-only, the
	// network driver/attachability, and remediation descriptions) and, when set,
	// every external resource the manifest references must be declared in it. Nil
	// falls back to manifest-driven pre-flight (auto-create attachable overlays).
	Requirements *Requirements
	// Owner claims the release for a manifest or controller, e.g.
	// "apply/prod-swarm" or "cd/edge". It is recorded on every revision this
	// call writes, as the id half of an OwnerRef naming the release.
	//
	// Empty leaves the revision unowned, which is what an imperative install
	// does and what keeps "never delete anything" the default: only a release
	// stamped with a caller's own owner can ever be a prune candidate, so a
	// release installed by hand or by somebody else's manifest is untouchable
	// no matter what a later prune is asked to do.
	Owner string
}

InstallOptions tune an install or upgrade.

type LintFinding

type LintFinding struct {
	Severity LintSeverity
	Message  string
}

LintFinding is one thing lint noticed about a chart.

func Lint

func Lint(ch *Chart, engine string, files [][]byte, sets []string) []LintFinding

Lint checks a loaded chart against the chart engine named by engine — this build's, or one the caller is asking about via --for-version.

files and sets are extra values layered over the chart defaults for the render check, exactly as `charts template -f/--set` would: a chart that requires an input it deliberately leaves undefaulted (a {{ required }} / {{ fail }} guard) cannot render from bare defaults, so linting it needs the same values a real install would supply. Pass nil for both to lint against defaults alone.

It reports everything it finds instead of stopping at the first problem: a chart author wants the list, not a game of whack-a-mole.

Structural validation already happened in LoadChartDir / LoadChartArchive, which refuse a chart with no name, version or templates, or an apiVersion this build cannot read. Lint covers what only becomes visible once a chart is rendered.

One thing it deliberately cannot do: prove a chart runs on the version it declares. This binary carries one engine's behaviour and cannot emulate another's — rendering with a real binary of that version is the only thing that settles it. See CheckCompatAgainst.

type LintSeverity

type LintSeverity int

LintSeverity ranks a lint finding. Only LintError fails a lint.

const (
	// LintWarning is advice: the chart works, but something is worth fixing.
	LintWarning LintSeverity = iota
	// LintError means the chart is broken — either outright, or for the engine
	// version it was linted against.
	LintError
)

func (LintSeverity) String

func (s LintSeverity) String() string

type Maintainer

type Maintainer struct {
	Name  string `yaml:"name"`
	Email string `yaml:"email,omitempty"`
	URL   string `yaml:"url,omitempty"`
}

Maintainer identifies a chart maintainer.

type NetworkRequirement

type NetworkRequirement struct {
	Name        string `yaml:"name"`
	Driver      string `yaml:"driver"`     // default "overlay"
	Attachable  *bool  `yaml:"attachable"` // default true
	AutoCreate  *bool  `yaml:"autoCreate"` // default true; false => validate-only
	Description string `yaml:"description"`
}

NetworkRequirement declares one external network a chart needs. AutoCreate and Attachable are pointers so an omitted key defaults to true (preserving the historical auto-create-as-attachable-overlay behaviour) while an explicit false is distinguishable. After parseRequirements they are always non-nil and Driver is non-empty.

type OutdatedEntry

type OutdatedEntry struct {
	Release   string
	Chart     string
	Repo      string
	Installed string
	Latest    string
}

OutdatedEntry is one installed release with a newer chart version available.

func Outdated

func Outdated(rels []Release, indexes map[string]*Index) []OutdatedEntry

Outdated joins installed releases against the newest version of their chart in any configured repository index. Releases already at the newest version, and those whose chart appears in no index (a local chart), are omitted.

A Release does not record which repository it came from, so a chart present in two repositories resolves to the highest version across them, reporting the repository that supplied it. That ambiguity is documented rather than designed away: recording the source repository is a change to persisted release state, which is not worth making for a case most users never hit.

type OwnerRef

type OwnerRef struct {
	ID   string // the manifest or controller that manages the resource
	Kind string // OwnerKindRelease
	Name string // the resource's name on the swarm
}

OwnerRef identifies both who manages a resource and which resource the stamp was written for. It is encoded as "<id>:<kind>/<name>", e.g.

apply/prod-swarm:release/whoami

The resource half is what makes the stamp verifiable, and it is there deliberately. A bare owner string cannot tell a resource this tool created from a copy of one: ArgoCD shipped exactly that as the app.kubernetes.io/instance label, and replaced it in 3.0 with an annotation naming the resource too. Reading a stamp back therefore means checking that it names the resource carrying it — one that does not is not evidence of ownership, so it is treated as unowned rather than trusted.

Docker label values have no length limit, which is the other half of what ArgoCD had to work around (its label truncated at 63 bytes).

func ParseOwner

func ParseOwner(s string) (OwnerRef, error)

ParseOwner decodes a stamp written by OwnerRef.String.

The id may itself contain "/" (the "apply/<name>" convention), so the id is cut at the first ":" and only the remainder is split into kind and name.

func (OwnerRef) String

func (o OwnerRef) String() string

String encodes the reference for storage in a label or a release payload.

type Plan

type Plan struct {
	// Owner is the owner id this plan was classified against, from the release
	// file's `owner:` key. Empty when the file declares none, in which case
	// nothing on the swarm is claimable and Orphaned is always empty.
	Owner string `json:"owner,omitempty"`
	// Releases, in file order.
	Releases []ReleasePlan `json:"releases"`
	// Unmanaged names releases that exist on the swarm, are absent from the
	// file, and carry no stamp saying this file produced them. Apply never
	// touches them — see Engine.Apply.
	Unmanaged []string `json:"unmanaged,omitempty"`
	// Orphaned names releases this file's own owner installed that the file no
	// longer declares. Unlike Unmanaged they are provably obsolete rather than
	// merely unrecognised, which is what makes deleting them safe. Apply still
	// does not delete them.
	Orphaned []string `json:"orphaned,omitempty"`
}

Plan is what apply would do to the whole swarm.

func (*Plan) Counts

func (p *Plan) Counts() (install, upgrade, unchanged int)

Counts summarises a plan.

type PruneAction

type PruneAction struct {
	Revision int
	Delete   bool
	Current  bool // the live (highest) revision; never deleted
}

PruneAction is the keep/delete decision for one revision in a prune.

type PruneResult

type PruneResult struct {
	Release string
	Actions []PruneAction
}

PruneResult reports what a prune did (or, for a dry run, would do) for one release. Actions are ascending by revision.

func (PruneResult) Deleted

func (r PruneResult) Deleted() []int

Deleted returns the revision numbers Prune removed (or would remove).

type Release

type Release struct {
	Name      string         `yaml:"release" json:"release"`
	Revision  int            `yaml:"revision" json:"revision"`
	Status    string         `yaml:"status" json:"status"`
	Chart     ReleaseChart   `yaml:"chart" json:"chart"`
	Values    map[string]any `yaml:"values" json:"values"`
	Manifest  string         `yaml:"manifest" json:"manifest"` // rendered Compose document
	Created   string         `yaml:"created" json:"created"`   // RFC3339
	Namespace string         `yaml:"namespace" json:"namespace"`
	// ManagedNetworks are the external networks swarmcli auto-created for this
	// revision. Persisted so uninstall can report what it left behind (it does
	// not remove them — they may be shared). Omitted for revisions that created
	// none and for records written before this field existed.
	ManagedNetworks []string `yaml:"managedNetworks,omitempty" json:"managedNetworks,omitempty"`
	// Owner is the stamp naming whichever manifest or controller produced this
	// revision, encoded as "<id>:<kind>/<name>" (see OwnerRef). Empty for a
	// release nothing claimed — an imperative install, or an apply from a file
	// declaring no owner — and an unowned release is never a prune candidate.
	Owner string `yaml:"owner,omitempty" json:"owner,omitempty"`
}

Release is the payload stored (gzipped) inside a release-history Config. It fully describes one deployed revision so it can be inspected or rolled back.

type ReleaseChart

type ReleaseChart struct {
	Name       string `yaml:"name" json:"name"`
	Version    string `yaml:"version" json:"version"`
	AppVersion string `yaml:"appVersion,omitempty" json:"appVersion,omitempty"`
}

ReleaseChart is the chart reference recorded in a Release.

func ReleaseChartOf

func ReleaseChartOf(ch *Chart) ReleaseChart

ReleaseChartOf projects a loaded chart into the metadata recorded on a release.

type ReleaseFile

type ReleaseFile struct {
	APIVersion   string     `yaml:"apiVersion,omitempty"`
	Repositories []RepoSpec `yaml:"repositories,omitempty"`
	// Owner names this manifest, claiming every release it installs. It is
	// optional and there is deliberately no default: a derived one would either
	// change between a laptop and a CI checkout (a path hash) or be shared by
	// every repository using the conventional filename (a basename), and either
	// would let two unrelated manifests claim each other's releases. Absent
	// means nothing is claimed, which is exactly today's behaviour.
	Owner    string        `yaml:"owner,omitempty"`
	Releases []ReleaseSpec `yaml:"releases"`

	// Dir is the directory containing the file. Values files and local chart
	// paths resolve against it, never the process working directory, so the
	// manifest is relocatable: a CI job gets the same result no matter where it
	// invoked swarmcli from.
	Dir string `yaml:"-"`
	// Path is the file as given, used to prefix error messages.
	Path string `yaml:"-"`
}

ReleaseFile is a declarative release manifest: the desired set of releases on a swarm, plus the repositories their charts come from. It is the GitOps entry point — `charts install` and `charts upgrade` are imperative and release state lives in the swarm, so without this file there is nothing in git for an automated updater (Renovate, Dependabot, a bot of your own) to edit.

The key names deliberately mirror Helmfile's. Renovate ships a `helmfile` manager that reads exactly `repositories[].{name,url}` and `releases[].{name,chart,version}`, so pointing it at this file needs one line of config and no hand-written regex:

{"helmfile": {"managerFilePatterns": ["/(^|/)swarmcli-release\\.ya?ml$/"]}}

Unknown keys are a hard error (see ParseReleaseFile), which also contains the obvious hazard of borrowing another tool's vocabulary: pasting real Helmfile syntax fails loudly and names the key, rather than silently doing half of what was meant.

func LoadReleaseFile

func LoadReleaseFile(path string) (*ReleaseFile, error)

LoadReleaseFile reads and validates a release manifest.

func ParseReleaseFile

func ParseReleaseFile(data []byte, path string) (*ReleaseFile, error)

ParseReleaseFile decodes and validates a release manifest. Unknown keys are rejected: this is a file an automated updater rewrites, so a misspelled `version:` must fail loudly rather than silently leave the release floating.

func (*ReleaseFile) ChartRef

func (rf *ReleaseFile) ChartRef(r ReleaseSpec) string

ChartRef resolves a release's chart reference, joining local paths against the manifest's directory and passing "<repo>/<chart>" references through.

func (*ReleaseFile) ValuesPaths

func (rf *ReleaseFile) ValuesPaths(r ReleaseSpec) []string

ValuesPaths resolves a release's values files against the manifest's directory.

type ReleaseMeta

type ReleaseMeta struct {
	Name      string
	Namespace string
	Revision  int
}

ReleaseMeta is the .Release object available to templates.

type ReleasePlan

type ReleasePlan struct {
	Name   string `json:"name"`
	Ref    string `json:"ref"`
	Action Action `json:"action"`
	// FromVersion is the currently deployed chart version, empty for an install.
	FromVersion string `json:"fromVersion,omitempty"`
	ToVersion   string `json:"toVersion"`

	Chart        ReleaseChart   `json:"chart"`
	Values       map[string]any `json:"values,omitempty"`
	Manifest     string         `json:"manifest"`
	Requirements *Requirements  `json:"requirements,omitempty"`
	// CurrentManifest is the deployed manifest, for diffing. Empty for an install.
	CurrentManifest string `json:"currentManifest,omitempty"`
	// Compat is the chart's engine requirement checked against this build.
	// Planning records it but never acts on it: apply's contract is to plan
	// every release before converging any, so the whole plan is gated at once
	// by the caller — which is also the layer that knows whether blocking is
	// appropriate for the verb being run.
	Compat CompatFinding `json:"compat"`
}

ReleasePlan is the computed desired state of one release.

type ReleaseSpec

type ReleaseSpec struct {
	Name    string   `yaml:"name"`
	Chart   string   `yaml:"chart"`
	Version string   `yaml:"version,omitempty"`
	Values  []string `yaml:"values,omitempty"`
}

ReleaseSpec is one desired release.

type RenderContext

type RenderContext struct {
	Values  map[string]any
	Release ReleaseMeta
	Chart   ChartMeta
}

RenderContext is the data exposed to chart templates, mirroring Helm's top-level objects: .Values, .Release, .Chart.

type RepoEntry

type RepoEntry struct {
	Name string `json:"name"`
	URL  string `json:"url"`
}

RepoEntry is a configured chart repository (name -> index URL).

type RepoSpec

type RepoSpec struct {
	Name string `yaml:"name"`
	URL  string `yaml:"url"`
}

RepoSpec is a chart repository the file depends on.

type RepoStore

type RepoStore struct {

	// Warnf, when set, receives non-fatal diagnostics: a chart whose index entry
	// publishes no digest, so its integrity could not be verified, and a
	// repository whose index could not be refreshed, so a cached one is in use.
	// charts is a library with no output of its own; nil is silent, and cli wires
	// this to stderr.
	Warnf func(format string, a ...any)
	// contains filtered or unexported fields
}

RepoStore persists configured repositories and caches their indexes under a base directory (default: the XDG state dir, ~/.local/state/swarmcli/charts).

func NewRepoStore

func NewRepoStore() (*RepoStore, error)

NewRepoStore returns a store rooted at the standard charts state directory.

func NewRepoStoreAt

func NewRepoStoreAt(dir string) *RepoStore

NewRepoStoreAt returns a store rooted at dir (used in tests).

func (*RepoStore) Add

func (s *RepoStore) Add(name, repoURL string) error

Add registers a repository and downloads its index. It rejects duplicate names and invalid URLs.

func (*RepoStore) EnsureRepos

func (s *RepoStore) EnsureRepos(specs []RepoSpec) error

EnsureRepos makes the repositories a release manifest declares available, adding those that are absent and refreshing the rest. It exists so that `charts apply -f file` is the only command a CI job needs to run.

What it writes is a name-to-URL mapping plus a cached index — a cache, not user data. The one thing it will not do is silently repoint an existing repository at a different origin.

func (*RepoStore) Indexes

func (s *RepoStore) Indexes() (map[string]*Index, error)

Indexes returns the cached index of every configured repository, keyed by repository name. Repositories with no cached index are skipped, as in Search.

func (*RepoStore) List

func (s *RepoStore) List() ([]RepoEntry, error)

List returns the configured repositories sorted by name.

func (*RepoStore) LoadIndex

func (s *RepoStore) LoadIndex(name string) (*Index, error)

LoadIndex returns the cached, parsed index for a repository.

func (*RepoStore) Pull

func (s *RepoStore) Pull(entry IndexEntry, baseURL string) (*Chart, error)

Pull downloads and loads the chart described by entry, resolving relative URLs against baseURL.

func (*RepoStore) Remove

func (s *RepoStore) Remove(name string) error

Remove deletes a repository and its cached index.

func (*RepoStore) Resolve

func (s *RepoStore) Resolve(ref, version string) (IndexEntry, string, error)

Resolve looks up a "repo/chart" reference and returns the chosen index entry (the requested version, or the latest when version is empty) plus the repo's base URL for resolving relative tarball URLs.

func (*RepoStore) Search

func (s *RepoStore) Search(keyword string) ([]ChartHit, error)

Search returns the latest version of every chart across all repos whose name or description contains keyword (case-insensitive). An empty keyword lists all charts. Results are sorted by "repo/name".

func (*RepoStore) Update

func (s *RepoStore) Update(name string) (changed, unchanged []string, err error)

Update downloads and caches the index for one repository (or all when name is empty) and returns the names refreshed.

type Requirements

type Requirements struct {
	Networks []NetworkRequirement  `yaml:"networks"`
	Secrets  []ResourceRequirement `yaml:"secrets"`
	Configs  []ResourceRequirement `yaml:"configs"`
}

Requirements is the parsed, defaulted requirements.yaml: the external networks/secrets/configs a chart needs. It is optional — a chart without it falls back to manifest-driven pre-flight. When present it is authoritative: every external resource the rendered manifest references must be declared.

func RenderRequirements

func RenderRequirements(ch *Chart, ctx RenderContext) (*Requirements, error)

RenderRequirements renders the chart's requirements.yaml through the same template engine and context as the manifest, then parses the result. This lets requirements.yaml reference .Values (e.g. an operator-chosen network name) while staying authoritative — the declared names are resolved against the same values that produced the manifest. Returns (nil, nil) when the chart ships no requirements.yaml.

Templated values must be quoted (name: "{{ .Values.x }}") so requirements.yaml still parses as YAML at chart-load time; the real value is resolved here, at the install/upgrade pre-flight.

type ResourceRequirement

type ResourceRequirement struct {
	Name        string `yaml:"name"`
	Description string `yaml:"description"`
}

ResourceRequirement declares one external secret or config a chart needs. These cannot be auto-created; Description enriches the remediation message.

type ServiceState

type ServiceState struct {
	Name     string
	Mode     string
	Replicas string // "running/desired" for replicated, "" otherwise — display only
	Status   string

	// Running counts tasks that are actually running on an active node.
	// Deliberately not derived from Replicas: that string counts tasks by
	// DESIRED state, so it reaches its target the moment Swarm schedules the
	// tasks rather than when they are up (see issue #480).
	Running int
	// Desired is the target task count over active nodes.
	Desired int
	// UpdateState is swarm's UpdateStatus.State, empty when the service has
	// never been updated. Empty means "no rollout has ever run" — NOT "the
	// rollout finished", which is why a fresh install cannot rely on it.
	UpdateState string
	// Monitor is UpdateConfig.Monitor: the window after a task is created in
	// which its failure still counts against the rollout.
	Monitor time.Duration
	// NewestTaskAge is how much of that window the newest running task has
	// already lived through, measured from task creation as swarm measures it.
	NewestTaskAge time.Duration
}

ServiceState is a live status line for a release's services, plus the facts --wait needs to decide whether the rollout is actually finished.

type UninstallResult

type UninstallResult struct {
	OrphanedNetworks []string
}

UninstallResult reports what an uninstall left behind. OrphanedNetworks are the external networks swarmcli auto-created for the release that still exist after the stack is removed — `docker stack rm` does not remove external networks, and swarmcli deliberately leaves them (they may be shared with other stacks) and reports them instead.

Jump to

Keyboard shortcuts

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