backend

package
v1.5.0-rc4 Latest Latest
Warning

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

Go to latest
Published: Oct 9, 2026 License: Apache-2.0 Imports: 33 Imported by: 0

Documentation

Overview

Package backend applies compose-derived Swarm specs to a swarm through the moby client. It is the half of charts.Backend that needs a daemon; turning a manifest into those specs is package compose.

Index

Constants

View Source
const (
	// ResolveAlways asks the registry to resolve the tag to a digest on every
	// deploy. Docker's default.
	ResolveAlways = "always"
	// ResolveChanged resolves only when the manifest names a different image
	// than the last deploy did, which is what suits automation: an unchanged
	// tag does not become a redeploy just because the registry moved.
	ResolveChanged = "changed"
	// ResolveNever leaves the tag as written.
	ResolveNever = "never"
)

Image resolution modes, matching charts.InstallOptions.ResolveImage and the daemon's own query parameter.

Variables

This section is empty.

Functions

func MountedSecretNames

func MountedSecretNames(dir string) (map[string]struct{}, error)

MountedSecretNames returns the names of the secrets Swarm has mounted into this controller, by listing dir (each secret is a file at /run/secrets/<name>). That set is exactly what a reconciled stack must not be allowed to mount: the admin token, the git token and every registryAuth are the controller's own credentials, and a stack mounting one by an `external` reference would read it.

A dir that does not exist yields an empty set and no error: a controller run outside a swarm has no mounted secrets to protect, and must still start.

Types

type Backend

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

Backend applies specs to one swarm.

func New

func New(api client.APIClient, o Options) *Backend

New returns a Backend applying to the swarm the client is connected to.

func (*Backend) ApplyServices

func (b *Backend) ApplyServices(ctx context.Context, stack *cdcompose.Stack, resolve string) error

ApplyServices creates the stack's services that do not exist and updates those that do, in the order the stack lists them.

It deletes nothing. Phase 1 is explicitly no prune, and charts.Apply itself never deletes either — a service the manifest no longer declares is left alone, not reaped.

It also cannot detect an out-of-band change. Swarm has no server-side apply, so `docker service update --replicas 10` produces no conflict signal at all: the next reconcile simply computes the same desired spec and writes it back, silently. The only conflict this can see is a write that races one of ours.

func (*Backend) CreateConfig

func (b *Backend) CreateConfig(ctx context.Context, name string, data []byte, labels map[string]string) error

CreateConfig stores one release revision.

The extra swarmcli.created label matches what the CE backend writes, so a release recorded by this controller and one recorded from the command line look the same to the TUI's config view.

func (*Backend) CreateOverlayNetwork

func (b *Backend) CreateOverlayNetwork(ctx context.Context, name, driver string, attachable bool) error

func (*Backend) DeclaredResources

func (b *Backend) DeclaredResources(ctx context.Context, req capability.ManifestRequest) (*cdcompose.Stack, error)

DeclaredResources converts a rendered manifest to the specs it declares, without resolving what its services mount to the ids Swarm addresses them by.

It is for the caller that wants names — the sweep proving a resource was declared by one of this application's own stored revisions. That caller is asking about the past, so it must not need any of it to still exist: DesiredServices resolves every mounted config and secret against the daemon, and a revision that mounted one a previous sweep has already deleted does not convert at all (#87).

Not a substitute for DesiredServices. What it returns is what ConvertUnresolved returns, so the config and secret references in it carry a placeholder id and nothing may be applied from it.

func (*Backend) DeleteConfig

func (b *Backend) DeleteConfig(ctx context.Context, name string) error

func (*Backend) DeployStack

func (b *Backend) DeployStack(ctx context.Context, req charts.DeployRequest) error

DeployStack converges the swarm to a rendered manifest.

Order matters and is the same order `docker stack deploy` uses: the things a service can reference have to exist before the service that references them. Volumes are absent from that list on purpose — Swarm creates a named volume on the node that first needs it, so there is nothing to pre-create.

The manifest is converted twice, and the first conversion is thrown away. It is what makes that order compatible with refusing a stack whole: converting a service resolves every config and secret it mounts to the id Swarm addresses it by, so the conversion that is applied cannot run until those exist — while the guard has to read what a service mounts before anything has been created (swarmcli-cd#84 for the first, #63 for the second). cdcompose.ConvertUnresolved answers that first read without needing anything to exist, and produces the same names for the guard to compare; the second conversion is the one whose specs are applied.

Nothing is deleted. Phase 1 is explicitly no prune.

req.Files is the content of every config the manifest sources with file:, keyed by the chart-relative path the manifest names, as the chart engine resolved it while the chart was still in scope. Both conversions take it, and it is the only place a config's content comes from: nothing is written to a directory and no path is opened. cdcompose.checkFileSources accepts a config's file: only when it names one of these keys from inside files/ or values/, and refuses a secret's file: and a service's env_file: whatever is here — swarmcli-cd#99, because the only filesystem a path in a rendered manifest could name is the one holding the Docker socket, the application set and /run/secrets.

func (*Backend) DesiredServices

func (b *Backend) DesiredServices(ctx context.Context, req capability.ManifestRequest) (*cdcompose.Stack, error)

DesiredServices converts a rendered manifest to the Swarm specs it declares.

It is the same conversion DeployStack does before applying, so what drift compares against is what a sync would write, rather than a second reading of the manifest that could disagree with it.

func (*Backend) InspectConfig

func (b *Backend) InspectConfig(ctx context.Context, name string) ([]byte, error)

func (*Backend) ListConfigs

func (b *Backend) ListConfigs(ctx context.Context) ([]charts.ConfigMeta, error)

ListConfigs returns every config's name and labels, and the payload of the ones holding release history.

One ConfigList call and nothing else. A config's payload comes back in the list response, not only on inspect, so passing it through means the engine decodes release history straight from this call instead of inspecting each stored revision (Eldara-Tech/swarmcli#510). That matters here more than anywhere: this runs several times per reconcile per application, against a store that grows by one config per release revision, and a controller reconciles on a timer whether or not anything changed.

Deliberately unfiltered, and that is a conclusion rather than an omission. The engine asks this one method two different questions — which configs hold release history, and which config names exist at all — and the second is asked about a chart's external configs, which carry no swarmcli label. Filtering on the release label server-side would answer the first cheaply and make the second report every external config as absent, which is a hard error refusing the deploy. Nor is there a projection to ask for instead: GET /configs is converted by the same daemon function as GET /configs/{id}, so a name cannot be fetched without its payload — unlike a secret, whose payload the list deliberately withholds — and the accepted filters are id, name, names and label, with no negation to list "everything else" with.

What can be bounded is what is *kept*. Only a release record is ever decoded — ConfigMeta.Data is documented as optional, and allRevisions skips anything not carrying the release label before it looks — so every other config contributes its name and labels and its payload is dropped here rather than held for the length of a plan. On a swarm whose stacks mount configs of their own that is the difference between retaining the release store and retaining the whole config store, on the manager node holding the raft log.

A config carrying a stack namespace is never reported as a record, whatever its other labels say — isReleaseRecord's rule, for the reason it gives: a stack deploy stamps that label on everything it creates, and a genuine record is written through CreateConfig without one. Such a config keeps its name and its other labels and loses the type label too, not only its payload, because the engine inspects a typed config whose payload the listing did not carry.

func (*Backend) LiveConfigs

func (b *Backend) LiveConfigs(ctx context.Context, stack string) (map[string]string, error)

LiveConfigs returns the configs this controller created under this stack's namespace, by scoped name.

A release-history config is never included. It carries com.swarmcli.* labels and no namespace, so the filter above cannot see one anyway — this is the same belt and braces RemoveStack applies, and for the same reason: a future change that did put a namespace label on them must not silently turn this sweep into "delete the history too".

func (*Backend) LiveNetworkNames

func (b *Backend) LiveNetworkNames(ctx context.Context) (map[string]string, error)

LiveNetworkNames returns every network on the swarm, by id.

It belongs to the live comparison rather than to the sweep, which is why it sits here beside LiveServices and not below with LiveNetworks and the other two scoped listers: it is deliberately *not* stack-scoped, and putting it in that group would contradict the one thing that group has in common.

A spec names the networks its service is attached to by id — the daemon rewrites each Target to one as it creates or updates the service (daemon/cluster.populateNetworkID) — while a manifest names a network. So comparing attachments is a resolution step before it is a comparison, and a stack-scoped listing cannot perform it: a service may be attached to an external network or to one of the swarm's predefined ones, and neither carries this stack's namespace label. An id that could not be named would have to be treated as opaque, which is precisely the attachment worth reporting.

Id to name, the opposite direction from LiveNetworks, because these are opposite jobs: the sweep matches on a name and deletes by id, and this reads an id and reports a name.

func (*Backend) LiveNetworks

func (b *Backend) LiveNetworks(ctx context.Context, stack string) (map[string]string, error)

LiveNetworks returns the networks carrying this stack's namespace label, by scoped name.

func (*Backend) LiveSecrets

func (b *Backend) LiveSecrets(ctx context.Context, stack string) (map[string]string, error)

LiveSecrets returns the secrets this controller created under this stack's namespace, by scoped name.

func (*Backend) LiveServices

func (b *Backend) LiveServices(ctx context.Context, stack string) (map[string]swarm.Service, error)

LiveServices returns the stack's running services with their full specs, by scoped name.

Named for what it returns rather than StackServices, which is taken by the charts.Backend method above it and answers a different question.

func (*Backend) NetworkScopes

func (b *Backend) NetworkScopes(ctx context.Context) (map[string]string, error)

NetworkScopes maps every network's name to its scope, for the engine's external-network pre-flight.

func (*Backend) PreservesOmittedServices added in v1.5.0

func (b *Backend) PreservesOmittedServices() bool

PreservesOmittedServices reports true: DeployStack creates and updates what the manifest declares and removes nothing it leaves out (see ApplyServices). Deleting a service is the reconciler's sweep, which goes by the release's whole declared manifest and this controller's revision records, so the partial manifests a sequential rollout deploys never make it remove the services held back.

func (*Backend) ReadStackServices

func (b *Backend) ReadStackServices(ctx context.Context, name string) ([]charts.ServiceState, error)

ReadStackServices is the same read with the failure kept, for the caller that has to tell "the swarm has no services under this release" from "the swarm could not be asked".

Those are one answer to StackServices and opposite findings to a health rollup, which reads an empty list as the positive assertion "deployed, but no services are present on the swarm". One slow daemon therefore flipped every release of an application from healthy to missing — the loudest state the rollup has, and the one anything alerting is watching for (#107).

It is a method beside StackServices rather than its signature because charts.Backend is CE's interface: widening it would change every implementation CE has, for a distinction only this repository's caller needs. The reconciler reaches this one through an optional-interface upgrade, the same way it reaches WithRegistryAuth, so a backend that cannot answer reads exactly as it did before.

func (*Backend) ReadStacks

func (b *Backend) ReadStacks(ctx context.Context, releases []string) (map[string][]charts.ServiceState, error)

ReadStacks answers for several releases from one look at the swarm.

It exists because the read above is not scoped to a release at all: the snapshot is the whole swarm — NodeList, ServiceList, TaskList and Info — and ServiceStatesFrom filters it afterwards. Asking per release therefore fetched the entire swarm per release and discarded all but one stack's worth of it each time, so an application declaring ten releases spent ten identical round trips on every reconcile, against the manager this controller runs on.

All-or-nothing, which is the same answer the single read gives: either the snapshot arrived and every release is in the map, or it did not and none are. An empty slice for a release therefore means the swarm really has no services under that name, which is precisely the distinction #107 turned on.

func (*Backend) RefreshSnapshot

func (b *Backend) RefreshSnapshot(context.Context) error

RefreshSnapshot is a no-op: this backend holds no cache to invalidate.

The method exists because the ambient CE backend reads through a process-wide snapshot with a 3s TTL and has to be told when it has gone stale. Here every read fetches, which is what makes one process able to serve several swarms without them evicting each other's state.

func (*Backend) RemoveConfig

func (b *Backend) RemoveConfig(ctx context.Context, id string) error

RemoveConfig deletes one config by id.

Distinct from DeleteConfig, which removes a release record by name on behalf of the chart engine. This one is the sweep's, takes an id, and tolerates a config that is already gone.

func (*Backend) RemoveNetwork

func (b *Backend) RemoveNetwork(ctx context.Context, id string) error

RemoveNetwork deletes one network by id.

Not RemoveOverlayNetwork, which resolves a name against an unfiltered list of every network on the swarm: that is the wrong cost on a path that runs every reconcile, and it reopens the window RemoveService closes by taking an id the caller has already read.

A network Swarm has already garbage-collected is not an error. The last task leaving an overlay network removes it, which routinely happens between the sweep reading the network and deciding to delete it.

Finding that out takes a second look, and classifying the error is not enough on its own — the premise RemoveStack states and answers with stackRemains, which this had no equivalent of. A swarm-scoped removal is proxied through swarmkit, and the helper that resolves the network there is the one of its five siblings that does *not* wrap "not found" in errdefs.NotFound (daemon/cluster/helpers.go:238, against :51, :87, :131, :167 and :274 which all do). An unclassified error is scored as Unknown and rendered 500 (api/server/httpstatus/status.go:16), which the client turns into ErrInternal (client/errors.go:124), so errdefs.IsNotFound is false for a network that had already gone. Reporting that as a failed prune, on the path whose whole purpose is honest reporting, is the defect.

So the state is the answer and the error is only a hint about where to look: a removal that failed is followed by a re-read, and a network that is no longer listed reached the state this was asking for whatever the call said.

func (*Backend) RemoveOverlayNetwork

func (b *Backend) RemoveOverlayNetwork(ctx context.Context, name string) error

RemoveOverlayNetwork removes a network by name, rolling back one this engine auto-created for an install whose deploy then failed. A network that is already gone is not an error: the caller is undoing, and undoing something that did not happen has succeeded.

func (*Backend) RemoveSecret

func (b *Backend) RemoveSecret(ctx context.Context, id string) error

RemoveSecret deletes one secret by id.

func (*Backend) RemoveService

func (b *Backend) RemoveService(ctx context.Context, id string) error

RemoveService deletes one service by id.

By id and not by name, because the caller has already read the service it means: resolving a name again here would open a window in which the name pointed at something else. A service already gone is not an error — a sweep that raced another deletion has still got the outcome it wanted.

It is the whole of the applier's delete surface, and deliberately narrow. RemoveStack takes a namespace and removes everything under it; this takes one service the caller has proved it may delete, and the proof lives in the reconciler rather than here.

func (*Backend) RemoveStack

func (b *Backend) RemoveStack(ctx context.Context, name string) error

RemoveStack deletes the services, networks, configs and secrets carrying the stack's namespace label — what `docker stack rm` removes, and nothing more.

Removal is idempotent, and it is decided by looking rather than by reading the error. A resource that has already gone between the list and the delete has reached the state this was asking for; a failure that leaves nothing behind is not a failure. That is not a rare race — Swarm garbage-collects an overlay network once the last task attached to it goes, which happens while the services removed a few lines above are still shutting down, so the network this listed is routinely gone before it is asked to remove it.

Classifying the error is not enough on its own. A swarm-scoped network removal is proxied through swarmkit and its "already gone" reply does not reliably arrive as a not-found the client recognises, so every failed removal is followed by a re-check of what is actually left. The state is the answer; the error is only a hint about where to look.

This is what makes the call safely repeatable, which prune's retry depends on: a pass that failed part-way is followed by another that re-lists and re-deletes, and treating the already-deleted half as an error would make that retry fail forever.

Volumes survive, as they do there: a stack's data outliving the stack is the whole point of a named volume, and charts has RemoveVolume for the caller that means it.

The engine's own release records are untouched. They are Docker configs, but they carry com.swarmcli.* labels rather than a stack namespace, so the filter below cannot see them — which is what lets a release be uninstalled and its history still be readable.

func (*Backend) RemoveStackVolume added in v1.5.0

func (b *Backend) RemoveStackVolume(ctx context.Context, v capability.StackVolume) error

RemoveStackVolume removes one of a stack's volumes while the name still answers with it: RemoveVolume, and only if the node-local volume carries the stack's namespace label. A volume of that name another stack has created since it was listed is left in place, and capability.ErrVolumeLeft says so.

func (*Backend) RemoveVolume

func (b *Backend) RemoveVolume(ctx context.Context, name string) error

RemoveVolume deletes one volume by name.

A volume that is already gone is not an error, as it is not for the four removals in live.go. The caller retries this for as long as the volume is still held by a container that has not finished shutting down, so a volume that vanished in between — another sweep, an operator, a `docker volume prune` — would otherwise burn the whole settle budget and then fail the prune naming "still in use" for something that had reached the asked-for state on the first attempt.

The classification is reliable here in a way it is not for a network: a missing volume is answered by the local volume store, or on a manager by getVolume, and both wrap in a not-found the client recognises (daemon/cluster/helpers.go:274).

Node-local only, as StackVolumes is. A name reaches a cluster volume when no node-local volume answers it — the daemon's delete and inspect both fall back to swarm (volume_routes.go:157-181 and :68-81), and the delete does so for any force=true request too, which is why force stays false — so the volume is inspected first and removed only if that answer is node-local. The inspect and the delete are two calls: a node-local volume removed by something else between them leaves the delete to that same fallback, and at a current API version the daemon offers no node-local-only delete to close it.

func (*Backend) SecretNames

func (b *Backend) SecretNames(ctx context.Context) (map[string]struct{}, error)

SecretNames is the set of existing secret names, for the engine's pre-flight. An external secret cannot be auto-created — its content is exactly what the manifest does not carry — so this only ever answers "is it there".

func (*Backend) ServiceLogs added in v1.4.0

ServiceLogs tails one service's container output.

Timestamps and details are always requested. The first is what makes a hundred lines of backlog a hundred instants rather than one; the second is the only place the daemon states which task and which node a line came from, which for a replicated service is the difference between "one node is crash-looping" and "this service is broken".

func (*Backend) StackServices

func (b *Backend) StackServices(ctx context.Context, name string) []charts.ServiceState

StackServices reads one stack's live service states.

Every rule in here belongs to the chart engine and is reached through its own exported mapping (Eldara-Tech/swarmcli#508): the running count by actual rather than desired state (#480), the target over active nodes (#481), a completed one-shot job counting toward its target instead of reading 0/N (#443, #494). A second copy would diverge silently, and both directions of that are wrong — reporting a release converged while the engine would still be waiting, or degraded on a stack that is fine.

A snapshot that cannot be read returns nil, matching the CE backend: the caller polls, so an unavailable daemon is "not converged yet" rather than a failure to report. That is true of awaitConverged and false of everything that asks once and then takes a view — see ReadStackServices.

func (*Backend) StackVolumes

func (b *Backend) StackVolumes(ctx context.Context, name string) ([]string, error)

StackVolumes names the volumes carrying this stack's namespace label **on the node this controller talks to**, which on a swarm of more than one node is not the same question as "this stack's volumes".

GET /volumes answers from the node-local volume store (api/server/router/volume/volume_routes.go:35 → volume/service/service.go:265): the daemon lists what this engine has, and only CSI *cluster* volumes are added from swarm, and only on a manager (volume_routes.go:41). An ordinary named volume is created by the engine that runs the task that mounts it, so a stack's volumes live on whichever nodes scheduled its tasks and are invisible from everywhere else.

There is no swarm-wide named-volume listing to ask instead, and swarms.Registry resolves one swarm rather than one node, so this is the whole of what the OSS build can see. SwarmNodes below is how a caller finds out whether that is everything.

Guarded like RemoveStack, and not only for symmetry: this is the list a purge deletes from, and the chart engine's own Uninstall reaches it even when the stack removal before it failed — it collects that error and carries on. A release named for the controller's stack would name the volume holding every application's git clone and chart cache, so the refusal has to be on the read that produces the list rather than only on the removal that precedes it (#102).

The label is checked here as well as sent as a filter, because on a manager the daemon applies the filter only to the node-local volumes: it appends every cluster volume unfiltered (volume_routes.go:41-51 and daemon/cluster/volumes.go:37, which sends an empty ListVolumesRequest). Only a node-local volume carrying this stack's namespace is returned. A cluster volume never is, labelled or not: a stack deploy neither creates nor labels one (docker/cli's convert gives a cluster mount empty ClusterOptions), so one was provisioned outside the release, and removing it deletes its storage.

A node-local volume whose name a cluster volume in the same listing also answers to is left out as well. Removal is by name, and if the node-local one is gone by then the daemon resolves the name in the swarm store instead — by a cluster volume's name, without regard to case, or as a prefix of its ID (daemon/cluster/helpers.go:248-270) — where a delete of an idle volume marks it for deletion for good. A declared cluster mount is named <stack>_<volume> exactly as a node-local volume is, so the two can share a name. A cluster volume created after this listing is not seen here.

func (*Backend) SwarmNodeRoster added in v1.4.0

func (b *Backend) SwarmNodeRoster(ctx context.Context) ([]application.SwarmNode, error)

SwarmNodeRoster describes every node of the swarm this backend applies to.

It reads the same whole-swarm snapshot ReadStacks does — nodes, services and tasks in one round trip — because the task counts are the point of the roster and fetching them separately would be two reads of a store that answers both at once.

Why an empty roster is an error

docker.SnapshotWith answers a locked swarm with an empty snapshot and a nil error, on purpose: the TUI it was written for prompts the operator to unlock rather than reporting a dead daemon. Passed through here that would serve a roster of no nodes, and a swarm with no nodes is not a thing — the API would answer 200 and the console would draw "the controller reported no swarm nodes" for a cluster that is merely locked. So both the locked flag and an empty list become errors, which is what keeps "could not read the roster" distinguishable from "read it, and the cluster is empty".

func (*Backend) SwarmNodes

func (b *Backend) SwarmNodes(ctx context.Context) (int, error)

SwarmNodes counts the swarm's nodes, which is the only thing that says whether StackVolumes could have seen everything: on a single-node swarm the node-local volume store *is* the swarm's, and on any larger one it is a fraction of unknown size.

A node that is not a manager cannot list nodes, and that failure is not smoothed over here — the caller decides what an unanswerable question means, and for a deletion it has to mean "assume not".

func (*Backend) UnpermittedNames added in v1.5.0

func (b *Backend) UnpermittedNames(ctx context.Context, req capability.AllowRequest) (application.Allow, error)

UnpermittedNames converts a manifest and returns the names in it that the allowlist would have to name for rejectForbiddenResources to let a deploy of it through: what its services reference external:, cluster mounts, a volume of its own with driver options (driverBacked), networks it joins or declares outside the release, and configs and secrets it declares that are not its own (ownDeclared). Each list is sorted, and names only.

What no allowlist can grant is left out, because naming it would not help: the controller's own secrets, configs, volumes and networks (for the self release they are its own), a name in the space of the release records, and a declared volume that is not the release's own (notOwnVolume). That last is warned about instead, with the refusal a deploy meets, since the release may be running unchanged until the next one.

func (*Backend) WithAllowedReferences

func (b *Backend) WithAllowedReferences(allow application.Allow) charts.Backend

WithAllowedReferences returns a copy of the backend that permits one application's charts to reach what allow enumerates — host paths on the nodes, and the secrets, configs, volumes and networks of some other stack.

Per application, unlike WithForbiddenSecrets, and applied through the same optional-interface upgrade as WithRegistryAuth so the reconciler need not depend on this concrete type. It is the only thing that carries an application's identity into this package: the guards here are name comparisons, and until now every set they compared against was the controller's own, which needed no caller to say who was deploying.

A backend nobody scoped this way permits nothing beyond what the release being deployed owns. That is the safe direction and it is what the sweep in package prune resolves for itself — a sweep removes rather than deploys, so it reaches neither this value's readers, but if it ever did it would refuse rather than wave something through.

func (*Backend) WithForbiddenSecrets

func (b *Backend) WithForbiddenSecrets(names map[string]struct{}) charts.Backend

WithForbiddenSecrets returns a copy of the backend that refuses to deploy a stack mounting any of the named secrets — the controller's own credentials. Controller-wide, not per application, but applied through the same optional-interface upgrade as WithRegistryAuth so the reconciler need not depend on this concrete type.

It is the startup-derived half only. The backend adds what Swarm reports it has mounted, and the chart engine's release records, at deploy time — see rejectForbiddenResources. Nothing an operator has to wire up can therefore be the difference between the guard being on and off.

func (*Backend) WithOutOfBandNotifier

func (b *Backend) WithOutOfBandNotifier(fn func(service string)) charts.Backend

WithOutOfBandNotifier returns a copy of the backend that reports a lost compare-and-swap to fn.

It is what makes Options.OnOutOfBandChange reachable at all from a reconcile: a backend is built once per swarm, by the swarms registry, which knows nothing about applications — and only the caller knows which application's sync a losing write belongs to. Applied through the same optional-interface upgrade as WithRegistryAuth, for the same reason.

A nil fn leaves the existing notifier in place rather than removing it, so a caller that does not care cannot accidentally silence one that does.

func (*Backend) WithRegistryAuth

func (b *Backend) WithRegistryAuth(auth regauth.Resolver) charts.Backend

WithRegistryAuth returns a copy of the backend that authenticates its image pulls with auth. The copy shares the client — one swarm's connection pool is not duplicated per application — and differs only by the resolver, so the per-swarm backend stays shared while the credential stays per application.

It returns charts.Backend so the reconciler can reach it through the swarms seam (which hands back that interface) with an optional-interface upgrade, rather than depending on this concrete type.

func (*Backend) WithSelfRelease added in v1.3.0

func (b *Backend) WithSelfRelease(hold capability.DeferSelf) charts.Backend

WithSelfRelease returns a copy of the backend that deploys the stack this controller itself runs as, handing hold the one write that would replace it.

The copy is what carries the answer to "is this release ours", and it is a copy for the same reason WithAllowedReferences is: a backend is built once per swarm and reused by every application, so a flag set on the shared one would make the next application's deploy the controller's own.

A nil hold returns the backend unchanged rather than a self copy with nowhere to put the write. The exemptions a self release gets are only defensible because the write replacing this controller is issued last, after the pass has been recorded; a copy that could not hand it back would deploy everything except the controller, report a successful sync, and have upgraded nothing. Refusing to be self is the failure that says so.

type Options

type Options struct {
	Log               *slog.Logger
	OnOutOfBandChange func(service string)
	Now               func() time.Time
}

Options tune a Backend. Every field has a working default.

Jump to

Keyboard shortcuts

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