plugins

package
v0.32.3 Latest Latest
Warning

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

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

README

d8 plugins

The internal/plugins package manages d8 plugins: standalone binaries published to an OCI registry that d8 installs, updates, and runs as if they were native subcommands. The machinery lives in this package (the Manager); the d8 plugins cobra commands are a thin layer on top of it in internal/plugins/cmd (package pluginscmd), one file per command - the same split internal/selfupdate / internal/selfupdate/cmd uses.

Why

  • Isolate dependencies and let teams develop plugins independently of d8.
  • Keep d8 itself compact - heavy functionality ships as plugins.
  • Guarantee compatibility: a plugin declares requirements (Kubernetes, Deckhouse, modules, other plugins) and d8 enforces them both at install time and before every run.

Commands

Command What it does
d8 plugins install <name> [--version X] [--use-major N] [--force] install or switch a plugin version
d8 plugins update <name> [--use-major N] update to the newest cluster-compatible version within the current major
d8 plugins update all the same for every installed plugin
d8 plugins list list installed plugins (the proxy serves no catalog, so available plugins cannot be listed)
d8 plugins versions <name> list all published versions of one plugin (installed one marked; same verb as d8 cli versions)
d8 plugins contract <name> show a plugin's contract
d8 plugins remove <name> remove an installed plugin
d8 <plugin> ... (wrapper, with DECKHOUSE_PLUGINS_ENABLED=true) run an installed plugin; auto-installs it on first use

Plugin source

rppPluginSource (rpp_source.go) implements the PluginSource interface (source.go) and is the only source: plugins are pulled through the in-cluster registry-packages-proxy using the kubeconfig identity, with no registry credentials on the user side (ADR: deckhouse-cli reaches the registry exclusively through the proxy, so every command needs a reachable cluster). See internal/selfupdate/README.md for what RPP is and how authorization works; the plugin routes are /v1/images/deckhouse-cli/plugins/<name>/....

What a plugin image contains

  • plugin - the executable, in the image layers;
  • the contract - name, version, description, requested env vars, flags, and requirements (Kubernetes / Deckhouse / modules / plugins) - published as a base64-JSON contract annotation on the image manifest.

The RPP source fetches the raw image manifest over the proxy manifests/<ref> route (a single manifest fetch, no layer pull) and reads the contract from its base64-JSON contract annotation itself. The binary is pulled separately (full image, over the images/<version> route) only when a plugin is installed.

On-disk layout

<plugins-dir>/                       # /opt/deckhouse/lib/deckhouse-cli by default
├── plugins/<name>/
│   ├── v<major>/<name>             # one binary per major version
│   ├── current -> v<major>/<name>  # the active version (atomic symlink swap)
│   └── install.lock                # one install lock per plugin
└── cache/contracts/<name>.json     # contract of the installed version (atomic writes)
  • --plugins-dir / DECKHOUSE_CLI_PATH override the root; if it is not writable, installs fall back to ~/.deckhouse-cli.
  • "Installed" means "has a current symlink" - a leftover directory from a failed install is never treated as an installed plugin.

How install works (InstallPlugin)

  1. Validate the plugin name (a single OCI path component - nothing else may reach filesystem paths or registry routes).
  2. Pick the version (see policy below) and take the per-plugin lock.
  3. If the selected version is already current - nothing to do (--force re-pulls).
  4. Fetch the contract and validate ALL requirements BEFORE any switch - including the fast path that merely repoints current to an already installed version.
  5. Download into a staged file (<binary>.new) - the live binary keeps working for the whole download.
  6. Smoke-test the staged binary (--version, fallback version; only a clean exit is required) - a corrupt or wrong-platform artifact is rejected before it replaces anything.
  7. Atomically swap the new binary in (rename over the live one - the original is untouched on failure), write the contract cache, then atomically repoint current.

A failure at any step leaves the previous version installed and working.

Version selection policy

  • Default pick: the newest stable semver tag whose cluster-side requirements are satisfied AND whose plugin->plugin dependency chain is resolvable - versions are probed newest to oldest and the first that passes both wins (a too-new release, or one whose dependencies cannot be satisfied, does not block updates).
  • Updates stay within the installed major; crossing majors requires an explicit --use-major N. The major is read from disk (the current symlink), so a broken binary cannot drop the pin.
  • Downgrade guard: the implicit path never installs a version older than the installed one - e.g. when the newest tag's contract is temporarily unreadable. Downgrades are explicit only (--version, --use-major).
  • Pre-releases (rc/alpha/beta) are never picked by default; install them via --version.
  • An unreachable cluster or a malformed contract is a hard error, not a silent fallback to an older version.

Requirements enforcement

  • Cluster-side (kubernetes, deckhouse, modules incl. mandatory/conditional/anyOf): verified against a one-shot cluster snapshot (the requirements/ package); the cluster is queried only when the plugin actually declares such requirements, so contract-less plugins install offline.
  • Plugin-to-plugin: a plugin's mandatory dependencies are installed and upgraded automatically (the resolution planner: constraint-aware, newest satisfying version, within each dependency's own major - or across it when --use-major cascades). Conflicts with already-installed plugins skip a candidate during selection.
  • At runtime: the wrapper re-validates requirements before EVERY plugin run (the gate is skipped for purely local queries: --help, --version, completion).
  • Escape hatch for air-gapped setups: --skip-cluster-checks / D8_PLUGINS_SKIP_CLUSTER_CHECKS=1 (downgrades the check to a warning).

Running a plugin (the wrapper)

  • All arguments are forwarded verbatim (the wrapper parses no flags itself).
  • Env requested by the contract is injected: KUBECONFIG (the path d8 uses) and PLUGINS_CALLER (the d8 executable); everything else passes through.
  • stdin/stdout/stderr are inherited; the plugin's exact exit code is propagated.
  • On d8's own termination the plugin gets SIGTERM and a grace period, not an instant SIGKILL.

Switches

Need How
install root --plugins-dir / DECKHOUSE_CLI_PATH
identity (rpp + cluster checks) -k/--kubeconfig, --context
RPP endpoint / TLS --rpp-endpoint, --rpp-ca-file, --rpp-insecure-skip-tls-verify
skip cluster-side requirement checks --skip-cluster-checks / D8_PLUGINS_SKIP_CLUSTER_CHECKS=1

Boundaries and deliberate decisions

  • Listing the full plugin catalog over RPP is not supported (the proxy has no catalog endpoint); install/update by name works.
  • Idempotency compares the version reported by the binary itself; a plugin that prints a non-semver banner is re-pulled on every explicit update.
  • Dependency resolution is dry-run during selection (a candidate whose chain cannot be resolved is skipped); the chain is actually installed only for the finally chosen version. Recursion has a cycle guard and a depth cap.
  • Dependencies are only upgraded, never downgraded, to satisfy a constraint.

Package map

File Responsibility
plugins.go the Manager: shared state of the plugin machinery
install.go the install pipeline: lock, staged download, smoke, atomic swap, idempotency
select.go newest-compatible version selection, contract memoization
update.go UpdateAll, installed-plugin discovery, home-fallback switch
remove.go Remove / RemoveAll
validators.go plugin-to-plugin requirement checks + the Manager glue over requirements/ (snapshot cache, kubeconfig clients, --skip-cluster-checks)
requirements/ cluster-side requirements: the one-shot cluster snapshot (k8s / Deckhouse / modules) and the named checks against it
run.go running an installed plugin: requirement gate, env injection, exec
list.go / versions.go data for the list / versions commands
source.go / rpp_source.go / init.go the PluginSource interface, its RPP implementation, source wiring
layout/ on-disk path layout
flags/ the d8 plugins flag set
cmd/ the d8 plugins ... command tree and the per-plugin wrapper command, one file per command

Related: internal/rpp (proxy HTTP client), internal/lockfile (install lock), internal/selfupdate (the same store-and-symlink update pattern for the d8 binary itself).

Documentation

Overview

Package plugins implements the d8-cli plugin system.

A d8 plugin is a standalone binary published to an OCI Registry. It is not part of d8-cli and can be developed independently.

Why plugins exist

  • Isolate dependencies.
  • Enable independent, parallel development.
  • Let different teams own different plugins (delivery, system, ...).
  • Keep d8 itself compact, with only the dependencies it actually needs.

What d8-cli can do with plugins

  • Download them.
  • Validate their dependencies (requirements the plugin declares in its contract).
  • Run them as if they were native subcommands.

Where a plugin lives

A plugin lives in the cluster's OCI registry, reached exclusively through the in-cluster registry-packages-proxy. The image carries the plugin binary in its layers and a contract, published as a manifest annotation, that describes the plugin:

  • name;
  • version;
  • description;
  • environment variables;
  • flags;
  • requirements.

How a plugin invocation works

  1. The user invokes a command through d8.
  2. The parent CLI checks whether the plugin is installed.
  3. If it is not, the image is pulled through the registry-packages-proxy.
  4. The binary is unpacked.
  5. Requirements are validated.
  6. A symlink is pointed at the current major version.
  7. The plugin is exec'd with the forwarded arguments.

On-disk layout

Installed plugins live under the install root: /opt/deckhouse/lib/deckhouse-cli by default (override with --plugins-dir / DECKHOUSE_CLI_PATH; ~/.deckhouse-cli when the default is not writable). Concrete paths:

<root>/plugins/<name>/v<major>/<name>   plugin binary (one per major version)
<root>/plugins/<name>/current           symlink to the active major's binary
<root>/plugins/<name>/install.lock      install lock (one per plugin)
<root>/cache/contracts/<name>.json      cached contract

Versions are kept per major; the `current` symlink selects the active one, so switching is an atomic repoint - the same idea selfupdate uses for d8 itself. Package internal/plugins/layout holds the authoritative path builders.

What the plugin system is made of

  1. Discover - learn what plugins exist and what their contracts declare.
  2. Install - download and place the plugin in the right location with proper validation.
  3. Exec - run the plugin as part of d8 without losing argument context.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ProvidesEnv added in v0.32.0

func ProvidesEnv(name string) bool

ProvidesEnv reports whether d8 actually injects a value for a contract-requested env var (vs leaving it to pass through from the inherited environment). It is the single source of truth for both injection and help. MODULE_CONFIG_INFO is not yet provided (it needs a defined module mapping in the contract).

func ValidatePluginName added in v0.32.0

func ValidatePluginName(name string) error

ValidatePluginName guards every user-supplied plugin name before it reaches MkdirAll / RemoveAll / registry paths.

Types

type InstallOption added in v0.32.0

type InstallOption func(*installOptions)

func InstallWithForce added in v0.32.0

func InstallWithForce() InstallOption

func InstallWithMajorVersion added in v0.32.0

func InstallWithMajorVersion(majorVersion int) InstallOption

func InstallWithVersion added in v0.32.0

func InstallWithVersion(version string) InstallOption

type Manager added in v0.32.0

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

Manager is the plugin machinery shared by every `d8 plugins ...` subcommand and the per-plugin wrapper command (see internal/plugins/cmd): it installs, updates, removes, lists and runs plugins from the configured source.

func NewManager added in v0.32.0

func NewManager(logger *dkplog.Logger) *Manager

func (*Manager) EnsureInstallRoot added in v0.32.0

func (m *Manager) EnsureInstallRoot() error

EnsureInstallRoot creates <pluginDirectory>/plugins; on permission denied falls back to ~/.deckhouse-cli, updates m.pluginDirectory, and retries.

func (*Manager) InitPluginServices added in v0.32.0

func (m *Manager) InitPluginServices(ctx context.Context) error

InitPluginServices wires m.service to the in-cluster registry-packages-proxy, reaching the proxy by the user's kubeconfig identity. ctx bounds endpoint discovery, so a Ctrl-C during command startup is honored. The proxy is the only plugin source (ADR: deckhouse-cli reaches the registry exclusively through it).

func (*Manager) InstallPlugin added in v0.32.0

func (m *Manager) InstallPlugin(ctx context.Context, pluginName string, opts ...InstallOption) error

InstallPlugin installs or switches to a plugin version. Steps: select the version, validate requirements, lay out plugins/<name>/v<major>, swap the binary in, point `current` at it, cache the contract. Tune behaviour with the InstallWith* options (version, major, force). With no options it installs the newest cluster-compatible version within the installed major. A plugin's mandatory dependencies are always installed/upgraded first.

func (*Manager) InstalledPluginContract added in v0.32.0

func (m *Manager) InstalledPluginContract(pluginName string) (*internal.Plugin, error)

InstalledPluginContract reads the cached contract from <plugin-dir>/cache/contracts/<plugin>.json and converts it to a domain object.

func (*Manager) InstalledPluginNames added in v0.32.0

func (m *Manager) InstalledPluginNames() ([]string, error)

InstalledPluginNames returns the plugins that are actually installed under the plugins root - a directory with a `current` symlink. A leftover directory from a failed install has no symlink and is excluded, so it cannot become an install target for a plugin the user never had.

func (*Manager) InstalledVersionOrNil added in v0.32.0

func (m *Manager) InstalledVersionOrNil(pluginName string) *semver.Version

InstalledVersionOrNil returns the active installed version of the plugin, or nil when the plugin is not installed or its version cannot be probed - best-effort: a version listing then simply carries no "current" marker.

func (*Manager) LatestVersion added in v0.32.0

func (m *Manager) LatestVersion(ctx context.Context, pluginName string) (*semver.Version, error)

LatestVersion lists tags from the registry for a plugin and returns the highest STABLE semver version - the same notion of "latest" that install selection uses (select.go), so `plugins list`/`contract` never advertise a pre-release that a default install would not pick.

func (*Manager) List added in v0.32.0

func (m *Manager) List() []PluginInfo

List returns the installed plugins. The registry-packages-proxy serves only allow-listed images by exact name and exposes no catalog endpoint, so the set of available plugins cannot be listed - a plugin is inspected by name with `d8 plugins versions <name>`.

func (*Manager) PluginContract added in v0.32.0

func (m *Manager) PluginContract(ctx context.Context, pluginName, tag string) (*internal.Plugin, error)

PluginContract fetches a plugin contract, memoizing it per name@tag for the duration of the command. Requirements-aware selection probes several versions' contracts, and the chosen one is fetched again by the install pipeline; the cache keeps that to a single pull per version.

func (*Manager) PublishedVersions added in v0.32.0

func (m *Manager) PublishedVersions(ctx context.Context, pluginName string) ([]*semver.Version, error)

PublishedVersions lists the plugin's published tags and returns them as semver versions, newest first (unparseable tags are dropped).

func (*Manager) Remove added in v0.32.0

func (m *Manager) Remove(pluginName string) error

Remove deletes an installed plugin from disk: its install directory and the cached contract. It is idempotent (removing a plugin that is not installed is a no-op) and holds the plugin's install lock so it cannot race a concurrent install of the same plugin.

func (*Manager) RemoveAll added in v0.32.0

func (m *Manager) RemoveAll() error

RemoveAll deletes every plugin found under the plugins root, each under its own install lock.

func (*Manager) RunInstalled added in v0.32.0

func (m *Manager) RunInstalled(ctx context.Context, pluginName string, args []string) error

RunInstalled ensures the plugin is installed, enforces its contract requirements, then execs its binary with args. stdin/stdout/stderr are inherited; the contract's requested env vars are injected.

func (*Manager) SetBuiltinCommands added in v0.32.0

func (m *Manager) SetBuiltinCommands(names []string)

SetBuiltinCommands records the d8 built-in command names that satisfy a plugin dependency of the same name. It bridges capabilities that ship as a built-in command but are not yet published as standalone plugins (e.g. delivery-kit): such a dependency counts as satisfied by the command's mere presence, with no on-disk install and no registry lookup.

func (*Manager) SetDirectory added in v0.32.0

func (m *Manager) SetDirectory(dir string)

SetDirectory retargets the manager at another install root. The command layer calls it after flag parsing, so --plugins-dir overrides the directory captured at construction time.

func (*Manager) UpdateAll added in v0.32.0

func (m *Manager) UpdateAll(ctx context.Context) error

UpdateAll updates every installed plugin to its newest cluster-compatible version within the current major. A per-plugin failure does not stop the others; the failures are reported together in the returned error.

type PluginInfo added in v0.32.0

type PluginInfo struct {
	Name        string
	Version     string
	Description string
}

PluginInfo is one installed plugin's name, version and description for display.

Directories

Path Synopsis
cmd
Package pluginscmd implements the `d8 plugins` command tree and the per-plugin wrapper command on top of the internal/plugins machinery.
Package pluginscmd implements the `d8 plugins` command tree and the per-plugin wrapper command on top of the internal/plugins machinery.
errdetect
Package errdetect maps registry-packages-proxy failures from `d8 plugins` to HelpfulErrors with plugin-specific guidance.
Package errdetect maps registry-packages-proxy failures from `d8 plugins` to HelpfulErrors with plugin-specific guidance.
Package flags defines the shared CLI flag set used by the d8 plugins management subcommands and consumed when building the registry-packages-proxy client and enforcing cluster-side requirements.
Package flags defines the shared CLI flag set used by the d8 plugins management subcommands and consumed when building the registry-packages-proxy client and enforcing cluster-side requirements.
Package layout centralizes the on-disk filesystem layout used by the d8 plugins subsystem: directory names, suffixes, and helpers that build concrete paths from the install root.
Package layout centralizes the on-disk filesystem layout used by the d8 plugins subsystem: directory names, suffixes, and helpers that build concrete paths from the install root.
Package requirements answers one question: does THIS cluster satisfy a plugin's cluster-side requirements (Kubernetes, Deckhouse and module versions)?
Package requirements answers one question: does THIS cluster satisfy a plugin's cluster-side requirements (Kubernetes, Deckhouse and module versions)?

Jump to

Keyboard shortcuts

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