extension

package
v1.0.39 Latest Latest
Warning

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

Go to latest
Published: Aug 13, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package extension provides public interfaces that a private commercial module can implement to extend podman-api without modifying OSS internals.

Current extension points: BlobStore (backup artifact storage) and SidecarInjector (inject sidecar containers into rendered pod YAML). Future releases will add extension points for RBAC auth and custom ingress controllers.

Index

Constants

View Source
const BackupMarkerNone = "none"

BackupMarkerNone is the one `backup:` marker literal the core interprets: a volume declaring it is never exported by a backup, on any path. Every other marker string stays opaque and belongs to the commercial marker grammar.

It is exported here — the module's public seam — so a commercial consumer links against the literal at compile time instead of hardcoding "none" and silently drifting from the core. This is the CANONICAL definition; the internal render and instance packages alias it rather than declaring copies.

Variables

View Source
var ErrBackupDeferred = errors.New("backup deferred: the instance is recovering from an interrupted backup")

ErrBackupDeferred is returned by BackupController.EnqueueBackup when the instance is temporarily unable to accept a backup and the tick was NOT serviced by anything else. It is distinct from the "already covered by an in-flight run" answer (`"", nil`) precisely because that one means the window IS handled and this one means it is not: a scheduler that re-arms its interval gate on a deferral silently drops every window the deferral spans.

The correct response is to retry on the next sweep without treating the window as satisfied. It is not a misconfiguration and should not be alerted on as a failure — the condition (a crashed backup mid-reconcile) clears by itself.

Functions

func InstanceSecretName added in v1.0.10

func InstanceSecretName(template, slug, name string) string

InstanceSecretName returns the per-instance podman secret name the core creates for an InjectedSecret (and template-declared secret). Injectors must use this to build the secretKeyRef.name in the pod YAML they return.

func IsBackupMarkerNone added in v1.0.39

func IsBackupMarkerNone(marker string) bool

IsBackupMarkerNone reports whether a raw `backup:` marker is the veto.

The comparison fails CLOSED: a marker equal to "none" after trimming surrounding whitespace and case-folding (`None`, `NONE`, `"none "`) vetoes the volume. Registration rejects those spellings outright — an author who writes `None` is told rather than guessed at — but registration only runs on the write path, so a template row persisted before that validator existed still carries the defect. For a marker whose entire job is "never capture this", the wrong direction to fail on a near-miss is open: the volume would be exported into every blob against the operator's explicit veto, with no error and no warning. Every comparison site goes through this function so they cannot drift.

Types

type BackupController added in v1.0.11

type BackupController interface {
	// ListBackupInstances returns every live instance (across all known hosts)
	// that has at least one volume whose marker is not `none`, with those
	// markers attached.
	ListBackupInstances(ctx context.Context) ([]BackupInstance, error)

	// LastBackupAt returns the finish time of the newest successful (complete)
	// backup for an instance, or the zero time if none exists. A scheduler uses
	// this for its interval gate.
	LastBackupAt(ctx context.Context, host, template, slug string) (time.Time, error)

	// EnqueueBackup enqueues a backup job for one instance over the same path
	// the HTTP POST .../backup handler uses, returning the new job id.
	// opts.Volumes narrows what is captured; a nil scope means every declared
	// volume not marked `none`, and an explicitly EMPTY one is an error.
	//
	// It is authoritative for in-flight dedupe, and distinguishes two answers a
	// scheduler must NOT conflate:
	//
	//   - COVERED — a backup job for this instance is already queued or running
	//     AND its scope COVERS the requested one. It enqueues nothing and
	//     returns an empty jobID with a NIL error: the window is handled, and a
	//     scheduler may re-arm its interval gate on it. An in-flight unscoped
	//     job covers every request; a scoped one covers only requests whose
	//     volumes are a subset of its own. A request the in-flight job does not
	//     cover is enqueued normally — it asks for work that run will not do,
	//     and swallowing it would drop that window's snapshot with no error and
	//     no retry.
	//
	//   - DEFERRED — a backup job for this instance is RECONCILING, i.e. a
	//     crashed run whose recovery sweep has not yet failed the row and
	//     restarted the (currently stopped) pod. Reconciling exports nothing, so
	//     it is not coverage at ANY scope; but starting a second backup now
	//     would snapshot a stopped instance and leave it stopped. The call
	//     returns an empty jobID and an error wrapping ErrBackupDeferred.
	//     Nothing was captured and nothing is in flight that will capture it —
	//     retry on the next sweep, and do NOT record the window as satisfied.
	//
	// COST OF DISTINCT SCOPES. Because dedupe is by coverage rather than by
	// instance, two ticks for the same instance with scopes neither of which
	// covers the other BOTH enqueue. Each resulting job takes the instance lock,
	// stops the pod, exports, and restarts it — so a scheduler emitting per-
	// volume ticks buys one stop/start cycle PER SCOPE, serialized, not one for
	// the window. That is the honest cost of not silently dropping a snapshot
	// nobody else is taking, but it is a real outage multiplier: a scheduler
	// that wants one outage per window should coalesce its volumes into a single
	// scoped call rather than issuing one call per volume.
	EnqueueBackup(ctx context.Context, host, template, slug string, opts BackupOptions) (jobID string, err error)
}

BackupController is handed to a registered BackupScheduler so it can drive scheduled backups without reaching into internal/ packages. It exposes only the three capabilities a scheduler needs: discover backup-eligible instances, learn when each last succeeded, and enqueue a backup job.

type BackupInstance added in v1.0.11

type BackupInstance struct {
	Host     string
	Template string
	Slug     string
	Volumes  []BackupVolumeMarker
}

BackupInstance is one live instance that has at least one backup-marked volume, projected for a commercial BackupScheduler to act on. Volumes carries only the backup-marked volumes, each with its raw marker string. The core interprets exactly one literal — `none`, meaning never back this volume up, which is filtered out before projection so a scheduler never sees one. Every other non-empty marker value is opaque and belongs to the commercial marker grammar (e.g. cadence, mode).

type BackupOptions added in v1.0.39

type BackupOptions struct {
	// Volumes lists the template's declared (short) volume names to snapshot,
	// e.g. ["sites"].
	//
	// It is a POINTER so absent and empty are distinguishable, exactly as the
	// HTTP layer distinguishes an absent `volumes` key from `"volumes": []`:
	//
	//	nil          -> unscoped: every declared volume not marked `none`
	//	&[]string{}  -> an error; it never escalates to a full-instance backup
	//	&[]string{…} -> exactly those volumes
	//
	// A scheduler that builds its scope by filtering (every tracked volume just
	// re-marked `none`, a config reload that emptied the list) would otherwise
	// pass an empty slice, receive a job id, and record a narrow request as
	// handled — while the job it actually got stopped the pod and captured
	// everything.
	//
	// Naming a volume the template does not declare, or one marked `none`,
	// fails the call — it never silently degrades to a smaller backup.
	Volumes *[]string
}

BackupOptions narrows what a backup job captures. It is a struct rather than a bare parameter because further knobs are planned (a per-volume mode, an opaque instance id): growing a struct is additive, growing a parameter list breaks the interface again each time.

type BackupScheduler added in v1.0.11

type BackupScheduler interface {
	Run(ctx context.Context, c BackupController) error
}

BackupScheduler is the commercial hook for scheduled volume backups. When one is registered via server.WithBackupScheduler, the server starts it after wiring and runs it until the server's run context is cancelled, passing a BackupController. The implementation owns all timing, interval, and marker-grammar policy; the core ships no scheduling behavior of its own.

type BackupVolumeMarker added in v1.0.11

type BackupVolumeMarker struct {
	Name   string
	Backup string // raw marker, e.g. "s3; interval=6h"; never empty here
}

BackupVolumeMarker pairs a volume name with its raw backup marker. The core interprets exactly one literal — `none`, meaning never back this volume up, which is filtered out before projection so it never reaches a scheduler. Every other value is opaque and belongs to the commercial marker grammar.

type BlobStore

type BlobStore interface {
	Put(ctx context.Context, key string) (BlobWriter, error)
	Get(ctx context.Context, key string) (io.ReadCloser, error)
	// DeleteAll removes every blob under the directory-like key prefix.
	// Removing an absent prefix is a no-op.
	DeleteAll(ctx context.Context, prefix string) error
}

BlobStore is where backup artifacts rest. The OSS implementation is a local directory (internal/backup.LocalDir); the commercial S3 backend implements the same seam. Keys are slash-separated relative paths (fs.ValidPath); Get returns an error satisfying errors.Is(err, fs.ErrNotExist) for a missing blob.

type BlobWriter

type BlobWriter interface {
	io.Writer
	Commit() error
	Abort() error
}

BlobWriter is one streamed blob write. Exactly one of Commit or Abort must be called; only Commit makes the blob visible to Get. This is the temp-file+rename contract: a failed backup never leaves a partial blob that looks complete.

type HostPortRequirer added in v1.0.36

type HostPortRequirer interface {
	// RequiredHostPorts returns the host ports this instance's sidecar(s)
	// would need exclusively, given its resolved render parameters. Called
	// after InjectSidecars, before the pod is applied to the host. An empty
	// result means this instance's configuration needs no dedicated host
	// port (e.g. no "vpn" parameter present) — not an error.
	RequiredHostPorts(params map[string]any) ([]PortSpec, error)
}

HostPortRequirer is an optional extension a SidecarInjector may also implement to declare host ports its injected sidecar(s) need exclusively, beyond whatever the rendered pod spec's own hostPort mappings already express. It exists so Apply can fail fast, before PlayKube, when a required port is already bound on the target host — instead of the pod starting silently with a sidecar that can never establish (the failure mode is invisible to the sidecar itself: a rootless pod's traffic is translated through the host's own network stack one layer below the sidecar's own process, so a colliding bind there produces no error the sidecar can see or log).

The core type-asserts a registered SidecarInjector against this interface; an injector that has no such requirement (the common case) simply does not implement it, and Apply's behavior is unchanged.

type InjectedSecret added in v1.0.9

type InjectedSecret struct {
	// Name is the short secret name (e.g. "litestream-s3-key"). The core
	// namespaces it to the instance as it does for template-declared secrets.
	Name string
	// Key is the data key within the Kubernetes Secret. The injected sidecar's
	// secretKeyRef.key references this value.
	Key string
	// Value is the plaintext secret value. The caller MUST NOT log or retain it.
	Value string
}

InjectedSecret is a secret declared by a SidecarInjector. The core creates it as a podman secret (using the same SecretCreate / wrapAsKubeSecret path used for template-declared secrets) before PlayKube and reaps it on instance Delete, so the injected sidecar can reference it via secretKeyRef instead of inlining a plaintext value.

Declaring a secret here is also what keeps it out of the instance API's env_summary: the core redacts any env value matching a secret it records for this instance — the values here plus any template-declared secrets in the stored spec — including env an injector added via secretKeyRef, which the template body cannot reveal (#198). That boundary is "values the core records for this instance", not "anything referenced via secretKeyRef": a secretKeyRef pointing at a pre-existing per-host secret (not part of the instance's stored spec) or an `envFrom: [{secretRef: …}]` bulk import (not parsed by the name pass at all) is NOT covered and will have its value returned in cleartext from GET /hosts/{host}/instances/{template}/{slug}. Prefer InjectedSecret for anything an injector needs redacted.

type PortSpec added in v1.0.36

type PortSpec struct {
	// Port is the host-level port number.
	Port int
	// Protocol is "tcp" or "udp", matching podman's PortMapping.Protocol.
	Protocol string
}

PortSpec names a single host-level port a sidecar needs to bind exclusively, for a reason the pod spec's own hostPort mappings cannot express — e.g. a rootless IPSEC sidecar whose IKE traffic is translated through pasta's host-side socket on the compiled-in port (UDP 500/4500), with no `ports:` entry in the rendered YAML for the core's own hostPort accounting to see.

type RestoreIntent added in v1.0.12

type RestoreIntent struct {
	// Timestamp is the opaque point-in-time selector, interpreted by the injector.
	Timestamp string
	// Volumes restricts the restore to these volume names; empty means all of the
	// instance's backup-marked volumes.
	Volumes []string
}

RestoreIntent expresses a one-shot point-in-time restore for an instance. It is supplied on a single Apply (via ApplyOptions.RestoreIntent) and is NEVER persisted into the stored spec — so the reconcile/boot-converge path always passes nil and never replays the restore. That non-persistence is what makes a point-in-time rollback fire exactly once instead of repeating on every pod restart.

The core ascribes no meaning to Timestamp: it projects the value verbatim to the injector, which owns the interpretation (the Litestream injector uses RFC3339). An empty Timestamp is rejected by the restore trigger, not here.

type SidecarInjection added in v1.0.9

type SidecarInjection struct {
	// YAML is the (possibly modified) pod manifest. Return the input unchanged
	// to pass through.
	YAML string
	// Secrets is an optional list of secrets the injector needs the core to
	// create as podman secrets (referenced via secretKeyRef in YAML). Nil/empty
	// means no extra secrets.
	Secrets []InjectedSecret
}

SidecarInjection is the return type of SidecarInjector.InjectSidecars.

type SidecarInjector added in v1.0.8

type SidecarInjector interface {
	InjectSidecars(ctx context.Context, renderedYAML string, meta TemplateMeta, params map[string]any, slug string, restore *RestoreIntent) (SidecarInjection, error)
}

SidecarInjector is a commercial extension point that injects sidecar containers into an instance's pod YAML after the template body has been rendered but before it is applied.

The implementation receives the rendered pod YAML, the projected template metadata, the resolved template parameters, the instance slug, and an optional one-shot RestoreIntent (nil on a normal apply and on every reconcile; non-nil only on an explicit point-in-time restore). It returns the (possibly modified) YAML plus any secrets the core must create before PlayKube and prune on delete. Return SidecarInjection{YAML: renderedYAML} to pass through without injection.

type TemplateMeta added in v1.0.8

type TemplateMeta struct {
	// ID is the template identifier (render.Meta.ID).
	ID string
	// Volumes lists the template's declared volumes and their backup targets.
	// The Backup marker is meta-only — it is not present in the rendered pod
	// YAML — so a backup/PITR sidecar needs it from here.
	Volumes []TemplateVolume
}

TemplateMeta is a read-only projection of a template's metadata, handed to a SidecarInjector. It is a public type so the commercial module can consume it: the internal render.Meta cannot cross the module boundary (it lives under internal/), so the OSS core projects the fields an injector needs into this stable struct. New fields are added here as injectors come to need them.

type TemplateVolume added in v1.0.8

type TemplateVolume struct {
	// Name is the volume name as referenced in the pod spec.
	Name string
	// Backup is the volume's `backup:` marker, passed through VERBATIM — this
	// projection filters nothing. Empty means the template declared no marker.
	//
	// A non-empty marker is NOT "back this volume up". The core interprets
	// exactly one literal, "none", as a veto: a volume marked that way is never
	// exported by a backup, on any path. That literal still arrives here
	// unchanged, deliberately — an injector's own marker grammar owns the
	// string, and filtering it in the projection would change what an existing
	// consumer receives.
	//
	// A consumer must therefore test the marker before acting on it. Reading
	// non-empty as "arm continuous backup/PITR for this volume" would arm it for
	// exactly the volume the operator vetoed.
	Backup string
}

TemplateVolume is one volume declared by a template.

Jump to

Keyboard shortcuts

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