Skip to content

Commit ac7f72b

Browse files
authored
Merge pull request #5795 from fluxcd/rfc-cli-plugin-system
[RFC-0013] Flux CLI Plugin System
2 parents 3e19817 + 968beba commit ac7f72b

1 file changed

Lines changed: 334 additions & 0 deletions

File tree

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
# RFC-0013 Flux CLI Plugin System
2+
3+
**Status:** implementable
4+
5+
**Creation date:** 2026-03-30
6+
7+
**Last update:** 2026-04-13
8+
9+
## Summary
10+
11+
This RFC proposes a plugin system for the Flux CLI that allows external CLI tools to be
12+
discoverable and invocable as `flux <name>` subcommands. Plugins are installed from a
13+
centralized catalog hosted on GitHub, with SHA-256 checksum verification and automatic
14+
version updates. The design follows the established kubectl plugin pattern used across
15+
the Kubernetes ecosystem.
16+
17+
## Motivation
18+
19+
The Flux CLI currently has no mechanism for extending its functionality with external tools.
20+
Projects like [flux-operator](https://github.com/controlplaneio-fluxcd/flux-operator) and
21+
[flux-local](https://github.com/allenporter/flux-local) provide complementary CLI tools
22+
that users install and invoke separately. This creates a fragmented user experience where
23+
Flux-related workflows require switching between multiple binaries with different flag
24+
conventions and discovery mechanisms.
25+
26+
The Kubernetes ecosystem has a proven model for CLI extensibility: kubectl plugins are
27+
executables prefixed with `kubectl-` that can be discovered, installed via
28+
[krew](https://krew.sigs.k8s.io/), and invoked as `kubectl <name>`. This model has
29+
been widely adopted and is well understood by Kubernetes users.
30+
31+
### Goals
32+
33+
- Allow external CLI tools to be invoked as `flux <name>` subcommands without modifying
34+
the external binary.
35+
- Provide a `flux plugin install` command to download plugins from a centralized catalog
36+
with checksum verification.
37+
- Support shell completion for plugin subcommands by delegating to the plugin's own
38+
Cobra `__complete` command.
39+
- Support plugins written as scripts (Python, Bash, etc.) via symlinks into the
40+
plugin directory.
41+
- Ensure built-in commands always take priority over plugins.
42+
- Keep the plugin system lightweight with zero impact on non-plugin Flux commands.
43+
44+
### Non-Goals
45+
46+
- Plugin dependency management (plugins are standalone binaries).
47+
- Cosign/SLSA signature verification (SHA-256 only in v1beta1; signatures can be added later).
48+
- Automatic update checks on startup (users run `flux plugin update` explicitly).
49+
- Private catalog authentication (users can use `$FLUXCD_PLUGIN_CATALOG` with TLS).
50+
- Flag sharing between Flux and plugins (`--namespace`, `--context`, etc. are not
51+
forwarded; plugins manage their own flags).
52+
53+
## Proposal
54+
55+
### Plugin Discovery
56+
57+
Plugins are executables prefixed with `flux-` placed in a single plugin directory.
58+
The `flux-<name>` binary maps to the `flux <name>` command. For example,
59+
`flux-operator` becomes `flux operator`.
60+
61+
The default plugin directory is `~/.fluxcd/plugins/`. Users can override it with the
62+
`FLUXCD_PLUGINS` environment variable. Only this single directory is scanned.
63+
64+
When a plugin is discovered, it appears under a "Plugin Commands:" group in `flux --help`:
65+
66+
```
67+
Plugin Commands:
68+
operator Runs the operator plugin
69+
70+
Additional Commands:
71+
bootstrap Deploy Flux on a cluster the GitOps way.
72+
...
73+
```
74+
75+
### Plugin Execution
76+
77+
On macOS and Linux, `flux operator export report` replaces the current process with
78+
`flux-operator export report` via `syscall.Exec`, matching kubectl's behavior.
79+
On Windows, the plugin runs as a child process with full I/O passthrough.
80+
All arguments after the plugin name are passed through verbatim with
81+
`DisableFlagParsing: true`.
82+
83+
### Shell Completion
84+
85+
Shell completion is delegated to the plugin binary via Cobra's `__complete` protocol.
86+
When the user types `flux operator get <TAB>`, Flux runs
87+
`flux-operator __complete get ""` and returns the results. This works automatically
88+
for all Cobra-based plugins (like flux-operator). Non-Cobra plugins gracefully degrade
89+
to no completions.
90+
91+
### Plugin Catalog
92+
93+
A dedicated GitHub repository ([fluxcd/plugins](https://github.com/fluxcd/plugins))
94+
serves as the plugin catalog. Each plugin has a YAML manifest:
95+
96+
```yaml
97+
apiVersion: cli.fluxcd.io/v1beta1
98+
kind: Plugin
99+
name: operator
100+
description: Flux Operator CLI
101+
homepage: https://fluxoperator.dev/
102+
source: https://github.com/controlplaneio-fluxcd/flux-operator
103+
bin: flux-operator
104+
versions:
105+
- version: 0.45.0
106+
platforms:
107+
- os: darwin
108+
arch: arm64
109+
url: https://github.com/.../flux-operator_0.45.0_darwin_arm64.tar.gz
110+
checksum: sha256:cd85d5d84d264...
111+
- os: linux
112+
arch: amd64
113+
url: https://github.com/.../flux-operator_0.45.0_linux_amd64.tar.gz
114+
checksum: sha256:96198da969096...
115+
- os: windows
116+
arch: amd64
117+
url: https://github.com/.../flux-operator_0.45.0_windows_amd64.zip
118+
checksum: sha256:9712026094a5...
119+
```
120+
121+
The plugin manifest includes metadata (name, description, homepage, source repo), the binary name
122+
(`bin`), and a list of versions with platform-specific download URLs and checksums.
123+
124+
The download URLs can point to one of the following formats:
125+
126+
- An archive containing the binary (`tar`, `tar.gz` or `zip`), with the binary at the root of the archive. The binary name inside the archive must match the `bin` field in the manifest.
127+
- An optional `extractPath` field can be specified in a `platforms` entry to override this default, either because the binary has a different name on this platform, or because it is nested in a subfolder rather than at the root of the archive. It accepts an absolute path to a file within the archive (e.g., `bin/flux-operator`).
128+
- A direct binary URL. The binary is downloaded and saved without extraction. The `bin` field is used for naming the installed plugin, not for discovery in this case.
129+
- Note that when the OS is Windows, the binary name is composed by appending `.exe` to the `bin` field (e.g., `flux-operator.exe`).
130+
131+
The Flux Operator CLI detects if the URL points to an archive by checking the file extension.
132+
If no extension is present, it checks the content type by probing for archive magic bytes after downloading the file.
133+
If the content is not an archive, it treats it as a direct binary URL.
134+
135+
A generated `catalog.yaml` (`PluginCatalog` kind) contains static metadata for all
136+
plugins, enabling `flux plugin search` with a single HTTP fetch.
137+
138+
### CLI Commands
139+
140+
| Command | Description |
141+
|---------|-------------|
142+
| `flux plugin list` (alias: `ls`) | List installed plugins with versions and paths |
143+
| `flux plugin install <name>[@<version>]` | Install a plugin from the catalog |
144+
| `flux plugin uninstall <name>` | Remove a plugin binary and receipt |
145+
| `flux plugin update [name]` | Update one or all installed plugins |
146+
| `flux plugin search [query]` | Search the plugin catalog |
147+
148+
### Install Flow
149+
150+
1. Fetch `plugins/<name>.yaml` from the catalog URL
151+
2. Validate `apiVersion: cli.fluxcd.io/v1beta1` and `kind: Plugin`
152+
3. Resolve version (latest if unspecified, or match `@version`)
153+
4. Find platform entry matching `runtime.GOOS` / `runtime.GOARCH`
154+
5. Download archive to temp file with SHA-256 checksum verification
155+
6. Extract only the declared binary from the archive (tar.gz or zip), streaming
156+
directly to disk without buffering in memory
157+
7. Write binary to plugin directory as `flux-<name>` (mode `0755`)
158+
8. Write install receipt (`flux-<name>.yaml`) recording version, platform, download URL, checksum and timestamp
159+
160+
Install is idempotent -- reinstalling overwrites the binary and receipt.
161+
162+
### Install Receipts
163+
164+
When a plugin is installed via `flux plugin install`, a receipt file is written
165+
next to the binary:
166+
167+
```yaml
168+
name: operator
169+
version: "0.45.0"
170+
installedAt: "2026-03-30T10:00:00Z"
171+
platform:
172+
os: darwin
173+
arch: arm64
174+
url: https://github.com/.../flux-operator_0.45.0_darwin_arm64.tar.gz
175+
checksum: sha256:cd85d5d84d264...
176+
```
177+
178+
Receipts enable `flux plugin list` to show versions, `flux plugin update` to compare
179+
installed vs. latest, and provenance tracking. Manually installed plugins (no receipt)
180+
show `manual` in listings and are skipped by `flux plugin update`.
181+
182+
### User Stories
183+
184+
#### Flux User Installs a Plugin
185+
186+
As a Flux user, I want to install the Flux Operator CLI as a plugin so that I can
187+
manage Flux instances using `flux operator` instead of a separate `flux-operator` binary.
188+
189+
```bash
190+
flux plugin install operator
191+
flux operator get instance -n flux-system
192+
```
193+
194+
#### Flux User Updates Plugins
195+
196+
As a Flux user, I want to update all my installed plugins to the latest versions
197+
with a single command.
198+
199+
```bash
200+
flux plugin update
201+
```
202+
203+
#### Flux User Symlinks a Python Plugin
204+
205+
As a Flux user, I want to use [flux-local](https://github.com/allenporter/flux-local)
206+
(a Python tool) as a Flux CLI plugin by symlinking it into the plugin directory.
207+
Since flux-local is not a Go binary distributed via the catalog, I install it with
208+
pip and register it manually.
209+
210+
```bash
211+
uv venv
212+
source .venv/bin/activate
213+
uv pip install flux-local
214+
ln -s "$(pwd)/.venv/bin/flux-local" ~/.fluxcd/plugins/flux-local
215+
flux local test
216+
```
217+
218+
Manually symlinked plugins show `manual` in `flux plugin list` and are skipped by
219+
`flux plugin update`.
220+
221+
#### Flux User Discovers Available Plugins
222+
223+
As a Flux user, I want to search for available plugins so that I can extend my
224+
Flux CLI with community tools.
225+
226+
```bash
227+
flux plugin search
228+
```
229+
230+
#### Plugin Author Publishes a Plugin
231+
232+
As a plugin author, I want to submit my tool to the Flux plugin catalog so that
233+
Flux users can install it with `flux plugin install <name>`.
234+
235+
1. Release binary with GoReleaser (produces tarballs/zips + checksums)
236+
2. Submit a PR to `fluxcd/plugins` with `plugins/<name>.yaml`
237+
3. Subsequent releases are picked up by automated polling workflows
238+
239+
Plugin authors are responsible for maintaining their plugin definitions in the catalog,
240+
by responding to issues and approving PRs for updates.
241+
242+
### Alternatives
243+
244+
#### PATH-based Discovery (kubectl model)
245+
246+
kubectl discovers plugins by scanning `$PATH` for `kubectl-*` executables. This is
247+
simple but has drawbacks:
248+
249+
- Scanning the entire PATH is slow on some systems
250+
- No control over what's discoverable (any `flux-*` binary on PATH becomes a plugin)
251+
- No install/update mechanism built in (requires a separate tool like krew)
252+
253+
The single-directory approach is faster, more predictable, and integrates install/update
254+
directly into the CLI.
255+
256+
## Design Details
257+
258+
### Package Structure
259+
260+
```
261+
internal/plugin/
262+
discovery.go # Plugin dir scanning, DI-based Handler
263+
completion.go # Shell completion via Cobra __complete protocol
264+
exec_unix.go # syscall.Exec (//go:build !windows)
265+
exec_windows.go # os/exec fallback (//go:build windows)
266+
catalog.go # Catalog fetching, manifest parsing, version/platform resolution
267+
install.go # Download, verify, extract, receipts
268+
update.go # Compare receipts vs catalog, update check
269+
270+
cmd/flux/
271+
plugin.go # Cobra command registration, all plugin subcommands
272+
```
273+
274+
The `internal/plugin` package uses dependency injection (injectable `ReadDir`, `Stat`,
275+
`GetEnv`, `HomeDir` on a `Handler` struct) for testability. Tests mock these functions
276+
directly without filesystem fixtures.
277+
278+
### Plugin Directory
279+
280+
- **Default**: `~/.fluxcd/plugins/` -- auto-created by install/update commands
281+
(best-effort, no error if filesystem is read-only).
282+
- **Override**: `FLUXCD_PLUGINS` env var replaces the default directory path.
283+
When set, the CLI does not auto-create the directory.
284+
285+
### Startup Behavior
286+
287+
`registerPlugins()` is called in `main()` before `rootCmd.Execute()`. It scans the
288+
plugin directory and registers discovered plugins as Cobra subcommands. The scan is
289+
lightweight (a single `ReadDir` call) and only occurs if the plugin directory exists.
290+
Built-in commands always take priority.
291+
292+
### Manifest Validation
293+
294+
Both plugin manifests and the catalog are validated after fetching:
295+
296+
- `apiVersion` must be `cli.fluxcd.io/v1beta1`
297+
- `kind` must be `Plugin` or `PluginCatalog` respectively
298+
- Checksum format is `<algorithm>:<hex>` (currently `sha256:...`), allowing future
299+
algorithm migration without schema changes
300+
301+
### Security Considerations
302+
303+
- **Checksum verification**: All downloaded archives are verified against SHA-256
304+
checksums declared in the catalog manifest before extraction.
305+
- **Path traversal protection**: Archive extraction guards against tar traversal.
306+
- **Response size limits**: HTTP responses from the catalog are capped at 10 MiB to
307+
prevent unbounded memory allocation from malicious servers.
308+
- **No code execution during discovery**: Plugin directory scanning only reads directory
309+
entries and file metadata. No plugin binary is executed during startup.
310+
- **Retryable fetching**: All HTTP/S operations use automatic retries for transient network failures.
311+
312+
### Catalog Repository CI
313+
314+
The `fluxcd/plugins` repository includes CI workflows that:
315+
316+
1. Validate plugin manifests on every PR (schema, name consistency, URL reachability,
317+
checksum verification, binary presence in archives, no builtin collisions)
318+
2. Regenerate `catalog.yaml` when plugins are added or removed
319+
3. Automatically poll upstream repositories for new releases and create update PRs
320+
4. Plugin authors have to agree to maintain their plugin's definition by responding to issues and approving PRs in the catalog repo.
321+
322+
### Known Limitations (v1beta1)
323+
324+
1. **No cosign/SLSA verification** -- SHA-256 only. Signature verification can be added later.
325+
2. **No plugin dependencies** -- plugins are standalone binaries.
326+
3. **No automatic update checks** -- users run `flux plugin update` explicitly.
327+
4. **No private catalog auth** -- `$FLUXCD_PLUGIN_CATALOG` works for private URLs but no token injection.
328+
5. **No version constraints** -- no `>=0.44.0` ranges. Exact version or latest only.
329+
6. **Flag names differ between Flux and plugins** -- e.g., `--context` (flux) vs
330+
`--kube-context` (flux-operator). This is a plugin concern, not a system concern.
331+
332+
## Implementation History
333+
334+
- **2026-03-30** PoC plugin catalog repository with example manifests and CI validation workflows available at [fluxcd/plugins](https://github.com/fluxcd/plugins).

0 commit comments

Comments
 (0)