Skip to content
Merged
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
30 changes: 22 additions & 8 deletions cmd/commands/delivery.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,39 @@ import (
// while it ships as a built-in command rather than a standalone plugin.
const DeliveryKitCommandName = "delivery-kit"

func NewDeliveryCommand() (*cobra.Command, context.Context) {
server.DefaultAddress = "https://delivery-sync.deckhouse.ru"
// NewRootContext returns the process-wide graceful-termination context that the
// whole d8 command tree runs under: it installs the SIGINT/SIGTERM handler that
// Execute's graceful.Terminate and telemetry shutdown depend on, and carries the
// werf logger.
//
// It is separate from NewDeliveryCommand because the context is root
// infrastructure, still required when an installed plugin serves `delivery-kit`
// and the built-in werf tree is never constructed.
func NewRootContext() context.Context {
return logging.WithLogger(graceful.WithTermination(context.Background()))
}

terminationCtx := graceful.WithTermination(context.Background())
defer graceful.Shutdown(terminationCtx, onShutdown)
// NewDeliveryCommand builds the built-in werf re-skin under the root context.
// It returns nil only on a path that has already asked for termination, which
// onShutdown turns into a process exit before the caller sees the nil.
func NewDeliveryCommand(ctx context.Context) *cobra.Command {
server.DefaultAddress = "https://delivery-sync.deckhouse.ru"

ctx := logging.WithLogger(terminationCtx)
// Drains a termination requested below - or a signal caught while werf builds
// its command tree - into the os.Exit that onShutdown performs.
defer graceful.Shutdown(ctx, onShutdown)

const werfAlias = "dk"

if err := setWerfSelfInvocationCommand(werfAlias); err != nil {
graceful.Terminate(ctx, err, 1)
return nil, ctx
return nil
}

werfRootCmd, err := werfroot.ConstructRootCmd(ctx)
if err != nil {
graceful.Terminate(ctx, err, 1)
return nil, ctx
return nil
}

werfRootCmd.Use = DeliveryKitCommandName
Expand All @@ -67,7 +81,7 @@ LICENSE NOTE: The Deckhouse Delivery Kit functionality is exclusively available

removeKubectlCmd(werfRootCmd)

return werfRootCmd, ctx
return werfRootCmd
}

// setWerfSelfInvocationCommand sets environment variables to ensure werf knows how to call itself
Expand Down
2 changes: 1 addition & 1 deletion cmd/commands/kubectl.go
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,7 @@ func NewKubectlCommand() *cobra.Command {
// Restore default OS signal handling for the kubectl subtree.
//
// The d8 root command installs a graceful-termination signal handler
// (see graceful.WithTermination in NewDeliveryCommand) that intercepts
// (see graceful.WithTermination in NewRootContext) that intercepts
// SIGINT/SIGTERM, cancels the root context and then resets the signal
// handlers. The kubectl subcommands (notably long-running ones such as
// `proxy`, `port-forward`, `exec`, `attach`, `logs -f`) do not observe
Expand Down
159 changes: 140 additions & 19 deletions cmd/d8/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package main

import (
"context"
"errors"
"fmt"
"log"
Expand Down Expand Up @@ -48,6 +49,7 @@ import (
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"
"github.com/deckhouse/deckhouse-cli/internal/plugins/layout"
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 @@ -98,45 +100,164 @@ func NewRootCommand() *RootCommand {
return rootCmd
}

// overridable is a top-level command that an installed plugin of the same name
// replaces, falling back to the built-in when no such plugin is installed.
//
// builtin is a thunk rather than a ready command: it runs only when the built-in
// wins, so a replaced command's construction cost - the werf and virtualization
// trees are each assembled eagerly - is never paid.
//
// Matching is on the canonical name only, never an alias: a plugin must be named
// exactly like the command it takes over. The built-in's aliases carry over to the
// wrapper, so `d8 dk` and `d8 s` keep working once a plugin serves the command.
type overridable struct {
name string
short string
aliases []string

// satisfiesPluginDep marks a name a plugin contract may depend on while the
// capability ships as a built-in command instead of a standalone plugin.
satisfiesPluginDep bool

builtin func() *cobra.Command
}

// overridableCommands lists the top-level commands a plugin may take over, in
// registration order. short duplicates the built-in's own Short because the thunk
// stays unevaluated when a plugin wins; it is only a fallback, as the wrapper
// prefers the description from the plugin's cached contract.
func (r *RootCommand) overridableCommands(ctx context.Context) []overridable {
return []overridable{
{
name: commands.DeliveryKitCommandName,
short: "A set of tools for building, distributing, and deploying containerized applications",
aliases: []string{"dk"},
satisfiesPluginDep: true,
builtin: func() *cobra.Command { return commands.NewDeliveryCommand(ctx) },
},
{
name: "data",
short: "Data operations (export/import)",
builtin: data.NewCommand,
},
{
name: "snapshot",
short: "Snapshot operations (create, delete, download, restore, upload, get)",
builtin: snapshot.NewCommand,
},
{
name: "iam",
short: "Manage Deckhouse users, groups, and access grants",
builtin: iam.NewCommand,
},
{
name: "network",
short: "A group of commands to operate network related tasks in The Deckhouse Ecosystem.",
aliases: []string{"n"},
builtin: network.NewCommand,
},
{
name: "v",
short: "Commands to work with Deckhouse Virtualization Platform.",
aliases: []string{"virtualization"},
builtin: commands.NewVirtualizationCommand,
},
{
name: "stronghold",
short: "Deckhouse Stronghold commands",
builtin: commands.NewStrongholdCommand,
},
{
name: pluginscmd.PackagePluginName,
short: "Package build and bootstrap tool for containerized packages",
satisfiesPluginDep: true,
builtin: packagecmd.NewCommand,
},
{
name: pluginscmd.SystemPluginName,
short: "Operate system options in DKP",
aliases: []string{"s", "p", "platform"},
builtin: system.NewCommand,
},
}
}

func (r *RootCommand) registerCommands() {
deliveryCMD, ctx := commands.NewDeliveryCommand()
r.cmd.AddCommand(deliveryCMD)
// The termination context is root infrastructure - Execute's graceful.Terminate
// and telemetry shutdown run on it - so it is established before any command is
// built, and regardless of whether a plugin ends up serving delivery-kit.
ctx := commands.NewRootContext()
r.cmd.SetContext(ctx)

installRoot, installed := r.installedPlugins()

// Names still served by a built-in once the override pass is done. A name an
// installed plugin took over drops off the list: the plugin itself now satisfies
// that dependency, with the version checking a built-in cannot offer.
var builtinDeps []string

for _, o := range r.overridableCommands(ctx) {
if _, override := installed[o.name]; override {
r.cmd.AddCommand(pluginscmd.NewPluginCommand(
o.name,
o.short,
o.aliases,
r.logger.Named(o.name+"-command"),
pluginscmd.WithInstallRoot(installRoot),
))

continue
}

// A nil command means the built-in already asked for termination; adding it
// would panic in cobra before the pending exit runs.
if cmd := o.builtin(); cmd != nil {
r.cmd.AddCommand(cmd)
}

if o.satisfiesPluginDep {
builtinDeps = append(builtinDeps, o.name)
}
}

r.cmd.AddCommand(backup.NewCommand())
r.cmd.AddCommand(data.NewCommand())
r.cmd.AddCommand(snapshot.NewCommand())
r.cmd.AddCommand(mirror.NewCommand())
r.cmd.AddCommand(cr.NewCommand())
r.cmd.AddCommand(status.NewCommand())
r.cmd.AddCommand(iam.NewCommand())
// Backward-compatibility shim for the four UserOperation commands that
// used to live at the top level (d8 user lock|unlock|reset-password|reset-2fa)
// before they moved under d8 iam user. Hidden from help; emits a stderr
// deprecation banner on each invocation pointing to the new path.
r.cmd.AddCommand(iamuser.NewDeprecatedTopLevelCommand())
r.cmd.AddCommand(network.NewCommand())
r.cmd.AddCommand(tools.NewCommand())
r.cmd.AddCommand(commands.NewVirtualizationCommand())
r.cmd.AddCommand(commands.NewKubectlCommand())
r.cmd.AddCommand(commands.NewLoginCommand())
r.cmd.AddCommand(commands.NewStrongholdCommand())
r.cmd.AddCommand(commands.NewHelpJSONCommand(r.cmd))

if os.Getenv("DECKHOUSE_PLUGINS_ENABLED") != "true" {
r.cmd.AddCommand(system.NewCommand())
} else {
r.cmd.AddCommand(pluginscmd.NewPluginCommand(pluginscmd.SystemPluginName, "Operate system options in DKP", []string{"s", "p", "platform"}, r.logger.Named("system-command")))
r.cmd.AddCommand(distcmd.NewCommand(r.logger.Named("dist-command"), builtinDeps))
}

// installedPlugins returns the plugins root actually holding installs and the set of
// plugin names in it, empty when nothing is installed.
//
// This runs at registration time, before flag parsing, so only the DECKHOUSE_CLI_PATH
// env override (applied in NewRootCommand) can retarget it: --plugins-dir is parsed
// far too late to decide which commands get registered.
func (r *RootCommand) installedPlugins() (string, map[string]struct{}) {
root, names, ok := layout.ResolveInstalled(flags.DeckhousePluginsDir)
if !ok {
return "", nil
}

set := make(map[string]struct{}, len(names))
for _, name := range names {
set[name] = struct{}{}
}

r.cmd.AddCommand(packagecmd.NewCommand())
r.logger.Debug("resolved installed plugins for command override",
slog.String("root", root), slog.Any("plugins", names))

// 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(distcmd.NewCommand(
r.logger.Named("dist-command"),
[]string{commands.DeliveryKitCommandName, pluginscmd.PackagePluginName},
))
return root, set
}

func (r *RootCommand) Execute() error {
Expand Down
26 changes: 18 additions & 8 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ Plugins are versioned binaries distributed through the cluster registry.
[Troubleshooting](#troubleshooting) · [Advanced](#advanced-hidden-flags)

> [!NOTE]
> The `d8 dist plugins` command group is hidden from `d8 dist --help` while
> the plugin ecosystem rolls out. The commands below are fully functional.
> Installing a plugin that is already present updates it. There is no separate
> `update` command; `d8 dist plugins install --all` updates every installed plugin.

## Plugin source

Expand Down Expand Up @@ -54,8 +54,8 @@ as described in
| `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 install --all` | updates every installed plugin, each within its own major |
| `d8 dist plugins list` | shows installed plugins, plus the published ones when the transport can enumerate them (`--source`) |
| `d8 dist plugins contract <name>` | shows a plugin's contract: version, description, requirements |
| `d8 dist plugins remove <name>` / `remove all` | removes plugins |

Expand Down Expand Up @@ -83,8 +83,9 @@ active one:

Rules that follow from this layout:

- `d8 dist plugins update` stays **within the installed major**. Crossing
majors is always an explicit decision: `--use-major N` or `--version X`.
- Installing a plugin that is already present **updates** it, staying **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.
- Installing the active version says so and does nothing; `--force`
Expand Down Expand Up @@ -125,7 +126,16 @@ modules) are only *verified* - d8 never changes the cluster for you.
`d8 mirror pull` mirrors plugins into the images bundle automatically: every
plugin whose contract names a mirrored module is selected (per bundled module
version, newest compatible), along with its mandatory plugin dependencies;
`--include-plugin <name>[@constraint]` adds more. After `d8 mirror push`, the
`--include-plugin <name>[@constraint]` adds more.

Mirroring the platform additionally pulls the plugins that ship with it -
**`package`** and **`system`** - unconditionally, with no module pairing. A
version still has to be one the mirrored platform can run, and a registry that
does not publish them yields a warning rather than a failed pull.

Their dependencies come along as usual, including ones named after a built-in d8
command (`package` depends on **`delivery-kit`**): the plugin is mirrored when the
registry has it, and the built-in command covers the dependency when it does not. After `d8 mirror push`, the
plugins live at `<target>/deckhouse-cli/plugins/<name>` - exactly where the
in-cluster registry-packages-proxy looks - so `d8 plugins install <name>`
works in the air-gapped cluster with no extra setup. See
Expand All @@ -146,7 +156,7 @@ is not supported. To see what a registry offers, use
| `--rpp-ca-file` | `D8_RPP_CA_FILE` | PEM CA bundle to verify the proxy TLS certificate |
| `--insecure-skip-tls-verify` | - | skip TLS verification of both the API server and the proxy (debugging only) |
| `--version X` *(install only)* | - | install an exact version; may be a pre-release |
| `--use-major N` *(install, update)* | - | cross to major `N`; by default operations stay within the installed major |
| `--use-major N` *(install only)* | - | 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 dist plugins` subcommand
Expand Down
16 changes: 4 additions & 12 deletions internal/dist/cmd/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ func writePluginsSection(b *strings.Builder, d summaryData) {
}

if outdated {
fmt.Fprintf(b, "%s\n", sumDim("Update a plugin with 'd8 dist plugins update <name>' or 'd8 dist plugins update all'."))
fmt.Fprintf(b, "%s\n", sumDim("Update a plugin with 'd8 dist plugins install <name>' or 'd8 dist plugins install --all'."))
}
}

Expand All @@ -254,16 +254,8 @@ func latestCell(p pluginRow) string {

// installedPluginsRoot reports the plugins root that actually holds an
// install: the configured root, or the home fallback (~/.deckhouse-cli) -
// the same resolution `plugins update all` uses. ok=false means no plugins
// are installed anywhere.
// the same resolution `plugins install --all` and the root command's plugin
// override use. ok=false means no plugins are installed anywhere.
func installedPluginsRoot() (string, bool) {
if layout.RootHasInstall(pluginflags.DeckhousePluginsDir) {
return pluginflags.DeckhousePluginsDir, true
}

if fallback, err := layout.HomeFallbackPath(); err == nil && layout.RootHasInstall(fallback) {
return fallback, true
}

return "", false
return layout.ResolveInstallRoot(pluginflags.DeckhousePluginsDir)
}
2 changes: 1 addition & 1 deletion internal/dist/cmd/status_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func TestRenderSummaryFull(t *testing.T) {
" NAME VERSION LATEST STATUS",
" dk 0.5.1 0.5.1 up to date",
" system 1.2.0 1.3.0 update available",
"Update a plugin with 'd8 dist plugins update <name>' or 'd8 dist plugins update all'.",
"Update a plugin with 'd8 dist plugins install <name>' or 'd8 dist plugins install --all'.",
"",
}, "\n"), out)
}
Expand Down
5 changes: 3 additions & 2 deletions internal/mirror/README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -398,10 +398,11 @@ If the source registry has no `packages` repository (some public/CE registries),

d8 CLI plugins are standalone binaries the CLI installs through the in-cluster registry-packages-proxy. A plugin declares its requirements (Deckhouse modules, other plugins, platform versions) in a contract - a base64-JSON annotation on its image manifest. The plugins phase runs **last**, after modules and packages, because it resolves against what the earlier phases actually put into the bundle.

Selection principle: **nothing extra**. A plugin enters the bundle only when:
Selection principle: **nothing extra**, with one standing exception. A plugin enters the bundle when:

- **the platform is mirrored** - `package` and `system` ship with the platform rather than with any module, so mirroring the platform mirrors them too, unconditionally and regardless of `--include-plugin`. A bundle without them can install the platform but not operate it. The version still has to be one the bundled platform can run (its `deckhouse` constraint is checked like any other), and a registry that does not publish them, or denies access, yields a warning rather than a failed pull. The list lives in `PlatformPlugins` (`internal/mirror/plugins/resolver.go`);
- a mirrored module needs it - the plugin's contract names that module in its `mandatory` or `anyOf` requirements. For **each bundled version** of the module, the newest plugin version whose contract the bundle satisfies is picked (so a bundle carrying module v1.0.0 and v1.5.0 may get two plugin versions, deduplicated);
- it is a mandatory plugin dependency of another selected plugin (resolved recursively; a version already picked is shared when it satisfies the constraint);
- it is a mandatory plugin dependency of another selected plugin (resolved recursively; a version already picked is shared when it satisfies the constraint). A dependency whose name matches a **built-in d8 command** (`delivery-kit`, `package`) is mirrored too when the registry publishes it - the built-in is only the fallback, because an air-gapped cluster cannot fetch the plugin later and, once installed, it takes the command over. When it is not published the built-in satisfies the dependency and only a note is recorded, so the dependent is never dropped over it;
- the user names it with `--include-plugin` (additive; an unmet explicit include fails the pull, unlike the automatic selection which skips with a reason in the summary).

Registry layout (at the **bare root**, outside the edition segment - like the installer):
Expand Down
Loading
Loading