|
| 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