Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ bin/

# Release assets
d8
dist/
/dist/
/build/
tmp/

Expand Down
8 changes: 3 additions & 5 deletions cmd/d8/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ import (
backup "github.com/deckhouse/deckhouse-cli/internal/backup/cmd"
cr "github.com/deckhouse/deckhouse-cli/internal/cr/cmd"
data "github.com/deckhouse/deckhouse-cli/internal/data/cmd"
distcmd "github.com/deckhouse/deckhouse-cli/internal/dist/cmd"
iam "github.com/deckhouse/deckhouse-cli/internal/iam/cmd"
iamuser "github.com/deckhouse/deckhouse-cli/internal/iam/user/cmd"
mirror "github.com/deckhouse/deckhouse-cli/internal/mirror/cmd"
network "github.com/deckhouse/deckhouse-cli/internal/network"
packagecmd "github.com/deckhouse/deckhouse-cli/internal/packagecmd"
pluginscmd "github.com/deckhouse/deckhouse-cli/internal/plugins/cmd"
"github.com/deckhouse/deckhouse-cli/internal/plugins/flags"
selfupdatecmd "github.com/deckhouse/deckhouse-cli/internal/selfupdate/cmd"
snapshot "github.com/deckhouse/deckhouse-cli/internal/snapshot/cmd"
status "github.com/deckhouse/deckhouse-cli/internal/status/cmd"
system "github.com/deckhouse/deckhouse-cli/internal/system/cmd"
Expand Down Expand Up @@ -133,12 +133,10 @@ func (r *RootCommand) registerCommands() {

// delivery-kit and package ship as built-in commands, not as plugins. Declaring
// them here satisfies a plugin's dependency on either name without a registry lookup.
r.cmd.AddCommand(pluginscmd.NewCommand(
r.logger.Named("plugins-command"),
r.cmd.AddCommand(distcmd.NewCommand(
r.logger.Named("dist-command"),
[]string{commands.DeliveryKitCommandName, pluginscmd.PackagePluginName},
))

r.cmd.AddCommand(selfupdatecmd.NewCommand(r.logger.Named("cli-command")))
}

func (r *RootCommand) Execute() error {
Expand Down
45 changes: 24 additions & 21 deletions docs/plugins.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# d8 Plugins (`d8 plugins`)
# d8 Plugins (`d8 dist plugins`)

Plugins are versioned binaries distributed through the cluster registry.
`d8` installs, updates, and removes them for you.
Expand All @@ -10,14 +10,15 @@ Plugins are versioned binaries distributed through the cluster registry.
[Troubleshooting](#troubleshooting) · [Advanced](#advanced-hidden-flags)

> [!NOTE]
> The `d8 plugins` command group is hidden from the root `--help` while the
> plugin ecosystem rolls out. The commands below are fully functional.
> The `d8 dist plugins` command group is hidden from `d8 dist --help` while
> the plugin ecosystem rolls out. The commands below are fully functional.

## Plugin source

Plugins are pulled from the in-cluster **registry-packages-proxy**, the same
channel as d8 self-update. This is the only supported path: every `d8 plugins`
command reaches the registry through the proxy, so a reachable cluster is
channel as d8 self-update. This is the only supported path: every
`d8 dist plugins` command reaches the registry through the proxy, so a
reachable cluster is
required. (A hidden, temporary `--source` flag pulls straight from a registry
repo instead - see [Advanced](#advanced-hidden-flags) - but it bypasses the
cluster and is not the intended flow.) The access model:
Expand All @@ -41,22 +42,22 @@ plugins need `packages-download`, CLI self-update needs `cli-download`.

| Command | What it does |
|---|---|
| `d8 plugins versions <name>` | lists all published versions of one plugin |
| `d8 plugins install <name>` | installs the newest version compatible with your cluster |
| `d8 plugins install <name> --version X` | installs an exact version |
| `d8 plugins install <name> --use-major N` | switches majors explicitly |
| `d8 plugins update <name>` / `update all` | updates within the current major |
| `d8 plugins list` | shows installed plugins (the proxy serves no catalog, so available plugins are not listed) |
| `d8 plugins contract <name>` | shows a plugin's contract: version, description, requirements |
| `d8 plugins remove <name>` / `remove all` | removes plugins |
| `d8 dist plugins versions <name>` | lists all published versions of one plugin |
| `d8 dist plugins install <name>` | installs the newest version compatible with your cluster |
| `d8 dist plugins install <name> --version X` | installs an exact version |
| `d8 dist plugins install <name> --use-major N` | switches majors explicitly |
| `d8 dist plugins update <name>` / `update all` | updates within the current major |
| `d8 dist plugins list` | shows installed plugins (the proxy serves no catalog, so available plugins are not listed) |
| `d8 dist plugins contract <name>` | shows a plugin's contract: version, description, requirements |
| `d8 dist plugins remove <name>` / `remove all` | removes plugins |

```console
$ d8 plugins versions package
$ d8 dist plugins versions package
v0.1.2 newer
* v0.0.21 current
v0.0.20

$ d8 plugins install package
$ d8 dist plugins install package
Installing plugin: package
Tag: v0.0.21
...
Expand All @@ -74,7 +75,7 @@ active one:

Rules that follow from this layout:

- `d8 plugins update` stays **within the installed major**. Crossing majors is
- `d8 dist plugins update` stays **within the installed major**. Crossing majors is
always an explicit decision: `--use-major N` or `--version X`.
- Installing a version that is already on disk just repoints the symlink - no
download.
Expand All @@ -98,7 +99,7 @@ is downloaded or switched:
enforced only if that plugin is already installed.

```console
$ d8 plugins install package
$ d8 dist plugins install package
...
Error: plugin requirements not satisfied # e.g. requires plugin delivery-kit
```
Expand All @@ -125,17 +126,19 @@ modules) are only *verified* - d8 never changes the cluster for you.
| `--use-major N` *(install, update)* | - | cross to major `N`; by default operations stay within the installed major |
| `--force` *(install only)* | - | reinstall even if already current (re-pull and re-verify) |

The persistent flags above are shared by every `d8 plugins` subcommand; the
`--source*` family is hidden - see [Advanced](#advanced-hidden-flags).
The persistent flags above are shared by every `d8 dist plugins` subcommand
(the kubeconfig and `--rpp-*` flags live on the parent `d8 dist` group and are
inherited); the `--source*` family is hidden - see
[Advanced](#advanced-hidden-flags).

## Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| `image or tag not found` (404) | that plugin - or that specific version - is not published in this cluster's registry | check with `d8 plugins versions <name>`; publishing is the plugin CI's job |
| `image or tag not found` (404) | that plugin - or that specific version - is not published in this cluster's registry | check with `d8 dist plugins versions <name>`; publishing is the plugin CI's job |
| `... unauthorized (401)` | no accepted Bearer token (a client-certificate kubeconfig is not enough) | use an OIDC-token kubeconfig (Kubeconfig Generator or `d8 login`) |
| `... forbidden (403)` | your identity may not download plugins | ask an admin to bind the ClusterRole `d8:registry-packages-proxy:packages-download`; authorization is cached ~5 min, so retry with a fresh token |
| `... requirements not satisfied` | mandatory **plugin** dependencies are missing or version-incompatible | run `d8 plugins contract <name>`; on `install` deps auto-install, but at plugin *run* time install them manually as the hint says (`d8 plugins install <dep>`) |
| `... requirements not satisfied` | mandatory **plugin** dependencies are missing or version-incompatible | run `d8 dist plugins contract <name>`; on `install` deps auto-install, but at plugin *run* time install them manually as the hint says (`d8 dist plugins install <dep>`) |
| `... requires Kubernetes/Deckhouse/module ...` | a **cluster-side** requirement is unmet (a different message from the row above) | upgrade the cluster/module, or pass `--skip-cluster-checks` to bypass verification |
| `... upstream error (5xx)` | the proxy could not reach the backing registry | retry shortly, or check the `registry-packages-proxy` pods in `d8-cloud-instance-manager` |
| `endpoint discovery ... failed`, `x509:` to the API server | endpoint discovery goes through your kubeconfig's **API server** (not the proxy), which was unreachable or had an invalid certificate | confirm the API server is reachable with a valid cert, or skip discovery with `--rpp-endpoint https://registry-packages-proxy.<domain>` (`D8_RPP_ENDPOINT`) |
Expand Down
50 changes: 30 additions & 20 deletions docs/self-update.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# d8 Self-Update (`d8 cli`)
# d8 Self-Update (`d8 dist`)

`d8` updates itself **through the cluster**. No registry credentials needed:

Expand All @@ -14,13 +14,13 @@
[Flags & env](#flags-and-environment-variables) ·
[Troubleshooting](#troubleshooting)

> Plugin management (`d8 plugins`) uses the same access model and is covered
> in [plugins.md](plugins.md).
> Plugin management (`d8 dist plugins`) uses the same access model and is
> covered in [plugins.md](plugins.md).

## How access works

```
d8 cli update
d8 dist update
│ Bearer token from your kubeconfig
registry-packages-proxy.<publicDomain> (found automatically via Ingress)
Expand Down Expand Up @@ -63,24 +63,34 @@ kubectl create clusterrolebinding d8-cli-download \

| Command | What it does |
|---|---|
| `d8 cli check` | reports whether a newer version is available |
| `d8 cli versions` (alias: `list`) | lists published versions, newest first |
| `d8 cli update [--version X]` | installs a version and switches to it |
| `d8 cli use <version>` | switches to a version; instant if it is already installed |
| `d8 dist status` | prints a distribution summary: the d8 version, installed plugins, what is outdated (local data only when the cluster is unreachable) |
| `d8 dist check` | reports whether a newer version is available |
| `d8 dist versions` (alias: `list`) | lists published versions, newest first |
| `d8 dist update [--version X]` | installs a version and switches to it |
| `d8 dist use <version>` | switches to a version; instant if it is already installed |

```console
$ d8 cli check
A newer deckhouse-cli is available: v0.14.0 (current: v0.13.1). Run 'd8 cli update' to upgrade.
$ d8 dist status
deckhouse-cli (d8)
Version: v0.13.1
Latest: v0.14.0 update available - run 'd8 dist update'

$ d8 cli versions
Plugins (1 installed):
NAME VERSION LATEST STATUS
system 1.2.0 1.2.0 up to date

$ d8 dist check
A newer deckhouse-cli is available: v0.14.0 (current: v0.13.1). Run 'd8 dist update' to upgrade.

$ d8 dist versions
v0.14.0 newer
* v0.13.1 current installed
v0.13.0 installed

$ d8 cli update
$ d8 dist update
Updating deckhouse-cli to v0.14.0...
deckhouse-cli updated to v0.14.0.
Previous version v0.13.1 remains installed - switch back with 'd8 cli use v0.13.1'.
Previous version v0.13.1 remains installed - switch back with 'd8 dist use v0.13.1'.
```

## How versions are stored
Expand Down Expand Up @@ -108,17 +118,17 @@ What this gives you:
## Switching and rollback

```console
$ d8 cli use v0.13.1 # already installed: instant, no cluster access
$ d8 dist use v0.13.1 # already installed: instant, no cluster access
Switched deckhouse-cli to v0.13.1 (installed locally).
Previous version v0.14.0 remains installed - switch back with 'd8 cli use v0.14.0'.
Previous version v0.14.0 remains installed - switch back with 'd8 dist use v0.14.0'.

$ d8 cli use 0.13.0 # the "v" prefix is optional
$ d8 cli use v0.13.0 # repeated: "deckhouse-cli is already at v0.13.0."
$ d8 dist use 0.13.0 # the "v" prefix is optional
$ d8 dist use v0.13.0 # repeated: "deckhouse-cli is already at v0.13.0."
```

- Rollback after an update: `d8 cli use <previous>` - the previous version
- Rollback after an update: `d8 dist use <previous>` - the previous version
stays installed.
- `d8 cli use <TAB>` completes the locally installed versions (enable shell
- `d8 dist use <TAB>` completes the locally installed versions (enable shell
completion with `d8 completion`).

## Flags and environment variables
Expand All @@ -140,4 +150,4 @@ $ d8 cli use v0.13.0 # repeated: "deckhouse-cli is already at v0.13.0
| `x509: certificate signed by unknown authority` | the proxy endpoint uses a CA your system does not trust | pass `--rpp-ca-file <ca.pem>` |
| `x509: ... doesn't contain any IP SANs` | you are connecting to a pod IP instead of the Ingress host | set `--rpp-endpoint https://registry-packages-proxy.<publicDomain>` |
| `deckhouse-cli is already up to date` | you run the latest version | use `--version X` to install an exact (older) one |
| `d8 cli use X` downloads although X was installed before | the local store was cleaned, or X was installed on another machine/user | it will download once and stay installed |
| `d8 dist use X` downloads although X was installed before | the local store was cleaned, or X was installed on another machine/user | it will download once and stay installed |
55 changes: 55 additions & 0 deletions internal/dist/cmd/check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
Copyright 2026 Flant JSC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package distcmd

import (
"fmt"

"github.com/spf13/cobra"

dkplog "github.com/deckhouse/deckhouse/pkg/log"

"github.com/deckhouse/deckhouse-cli/internal/version"
)

func newCheckCommand(logger *dkplog.Logger) *cobra.Command {
return &cobra.Command{
Use: "check",
Short: "Report whether a newer deckhouse-cli version is available",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
updater, err := newUpdater(cmd.Context(), cmd, logger)
if err != nil {
return err
}

latest, newer, err := updater.LatestVersion(cmd.Context(), version.Version)
if err != nil {
return err
}

if newer {
fmt.Printf("A newer deckhouse-cli is available: %s (current: %s). Run 'd8 dist update' to upgrade.\n",
verNew.Sprint(latest), verOld.Sprint(version.Version))
} else {
fmt.Printf("deckhouse-cli is up to date (%s).\n", verCur.Sprint(version.Version))
}

return nil
},
}
}
98 changes: 98 additions & 0 deletions internal/dist/cmd/dist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
Copyright 2026 Flant JSC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package distcmd

import (
"github.com/spf13/cobra"

dkplog "github.com/deckhouse/deckhouse/pkg/log"

"github.com/deckhouse/deckhouse-cli/internal/dist/cmd/errdetect"
pluginscmd "github.com/deckhouse/deckhouse-cli/internal/plugins/cmd"
pluginflags "github.com/deckhouse/deckhouse-cli/internal/plugins/flags"
rppflags "github.com/deckhouse/deckhouse-cli/internal/rpp/flags"
)

// NewCommand returns the `d8 dist` command tree - management of the d8
// distribution: the deckhouse-cli binary itself and its plugins. It reaches
// the registry-packages-proxy with the caller's kubeconfig identity.
// builtinCommands are built-in command names that satisfy a plugin dependency
// of the same name (see pluginscmd.NewCommand).
func NewCommand(logger *dkplog.Logger, builtinCommands []string) *cobra.Command {
cmd := &cobra.Command{
Use: "dist",
Short: "Manage the d8 distribution: the deckhouse-cli binary and its plugins",
Long: "Manage the d8 distribution - the deckhouse-cli binary and its plugins.\n\n" +
"See the state of the distribution with 'd8 dist status': the d8 version, installed\n" +
"plugins, and what is outdated.\n\n" +
"Versions are served by the in-cluster registry-packages-proxy, authenticated by the\n" +
"current kubeconfig identity.\n\n" +
"Update the binary with 'd8 dist update'; manage plugins under 'd8 dist plugins'.\n\n" +
"Environment variables:\n" +
" " + rppflags.EnvEndpoint + " registry-packages-proxy base URL (otherwise discovered from the cluster)\n" +
" " + rppflags.EnvCAFile + " PEM CA bundle to verify the proxy TLS certificate\n" +
" KUBECONFIG path to the kubeconfig file",
// Print help when called bare, like the d8 root. The explicit Run makes
// Args effective: without it cobra shows help before validating args,
// and a stray argument ('d8 dist junk') would pass silently.
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, _ []string) {
_ = cmd.Help()
},
}

cmd.AddCommand(newStatusCommand(logger))
cmd.AddCommand(newCheckCommand(logger))
cmd.AddCommand(newUpdateCommand(logger))
cmd.AddCommand(newUseCommand(logger))
cmd.AddCommand(newVersionsCommand(logger))

// Cluster access flags (kubeconfig/context, rpp-*) are owned by the dist
// root for the whole tree; the plugins subtree adds only its own flags.
pluginflags.AddKubeFlags(cmd.PersistentFlags())
rppflags.AddFlags(cmd.PersistentFlags())

// Wrap before mounting plugins: the plugins subtree classifies its
// failures with its own errdetect.
wrapProxyDiagnostics(cmd)

cmd.AddCommand(pluginscmd.NewCommand(logger.Named("plugins"), builtinCommands))

return cmd
}

// wrapProxyDiagnostics turns recognized registry-packages-proxy failures into
// colored diagnostics at the command level (per pkg/diagnostic: classify in the
// command, never in root.go). It wraps every RunE in the tree; errdetect.Diagnose
// returns nil for non-proxy and already-diagnosed errors, leaving them untouched.
func wrapProxyDiagnostics(cmd *cobra.Command) {
if cmd.RunE != nil {
inner := cmd.RunE
cmd.RunE = func(c *cobra.Command, args []string) error {
err := inner(c, args)
if diag := errdetect.Diagnose(err); diag != nil {
return diag
}

return err
}
}

for _, sub := range cmd.Commands() {
wrapProxyDiagnostics(sub)
}
}
Loading
Loading