selfupdate

package
v0.33.2 Latest Latest
Warning

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

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

README

d8 self-update (d8 cli)

The internal/selfupdate package updates the d8 binary itself through the cluster.

Why

  • d8 is a single binary; before this, updating meant "download and replace by hand".
  • The cluster itself knows which CLI versions are published for it - its registry is the source of updates.
  • Access to updates is controlled by the user's ordinary RBAC permissions (kubeconfig), with no registry credentials handed out.

Commands

Command What it does
d8 cli check reports whether a version newer than the current one is available
d8 cli update [--version X] installs the version into the store and repoints current at it
d8 cli use <version> switches to a version: an installed one is a pure symlink repoint (instant, offline), a missing one is downloaded first
d8 cli versions (alias list) lists published versions newest-first; the current one is starred, locally installed ones are marked installed

Versions live in a per-user store with the same versions-directory-plus-symlink layout the plugin installer uses:

/opt/deckhouse/bin/d8 -> ~/.deckhouse-cli/cli/current -> versions/v0.13.1/d8
     (PATH entry,           (stable symlink,               (the store)
      migrated once)         atomic repoint)
  • Switching = repointing current (atomic: staged symlink + rename). The PATH binary is never rewritten after migration, so switching needs no elevated privileges and copies no files.
  • The store is addressed by its own well-known paths, never through os.Executable(): on Linux /proc/self/exe resolves to the symlink target, so "replace whatever the executable resolves to" would overwrite a stored version in place. The two-level layout (PATH -> current -> version) is what makes the symlink scheme safe - the updater always knows where the switchable link lives.
  • Migration: when the running binary is a plain file outside the store (the pre-store layout, or a binary dropped in by external tooling), the first update/use seeds the store with it (under its own version, when semver), backs it up as <exe>.old and replaces the PATH entry with a symlink to current. If external tooling later overwrites the symlink with a real file, the next update/use simply migrates again - self-healing.
  • d8 cli use <version> resolves the request against the store by semver value (0.13.1 finds v0.13.1); a hit needs no network and no kubeconfig, a miss falls back to the regular download path and stays installed afterwards.
  • Store entries are immutable (re-installing an existing tag is a no-op) and are smoke-tested while staged (.staged suffix), so a corrupt artifact never becomes a visible entry. Foreign content in the store directory is ignored. Dev builds (non-semver versions) are never archived - use cannot address them.
  • The store is per-user (~/.deckhouse-cli/cli): after migration the PATH entry points into the home of the user who ran it. On shared machines each user who manages d8 gets their own store; on cluster masters that user is root.
  • d8 cli use <TAB> shell-completes from the store (newest-first, prefix filtered). Completion never touches the network - the offline-switchable versions are exactly the ones worth suggesting.

What RPP is and how this package talks to it

RPP (registry-packages-proxy) is the in-cluster HTTP proxy in front of the platform's container registry (module registry-packages-proxy, ns d8-cloud-instance-manager):

  • Why it exists: users do not have (and should not have) registry credentials. The proxy serves artifacts based on the kubeconfig identity - the same token the user already presents to kube-apiserver.
  • Transport: kube-rbac-proxy on :4219 of the master nodes; the public Ingress registry-packages-proxy.<publicDomain> (valid TLS, the default path).
  • Per-request authorization: the Bearer token from kubeconfig -> TokenReview ("who") + SubjectAccessReview ("is it allowed"). Access is granted by the ClusterRole d8:registry-packages-proxy:cli-download; it is NOT bound to anyone by default - the cluster administrator decides who may download the CLI.
  • API used by this package (the HTTP client lives in internal/rpp):
    • GET /v1/images/deckhouse-cli/tags -> {"name": ..., "tags": [...]} - the version list;
    • GET /v1/images/deckhouse-cli/tags/<tag> -> gzip-tar of the image contents (containing the d8 file).
  • Endpoint is discovered automatically (Ingress -> pod-IP fallback) or set explicitly (--rpp-endpoint / D8_RPP_ENDPOINT).

How an update works (Updater.Apply -> SwitchTo)

  1. A lock in the store (install.lock, internal/lockfile) - two switches cannot run in parallel; a lock orphaned by a kill is reclaimed.
  2. A plain-file install is seeded into the store first (best-effort, semver versions only), so the displaced version stays switchable.
  3. The requested version is installed into the store unless already present: download to versions/<tag>/d8.staged, smoke test (--version must exit cleanly - a corrupt artifact or one built for another platform is rejected while staged), atomic rename to its final name.
  4. Pre-existing store entries are smoke-tested again before they become active.
  5. current is atomically repointed at the version.
  6. A plain-file install is migrated: the original binary becomes <exe>.old and the PATH entry becomes a symlink to current (rolled back if the link cannot be created). Store-managed installs skip this step entirely.

Rollback is d8 cli use <previous> - the previous version remains installed (the command prints it). The .old file exists only as the migration backup.

Version selection:

  • by default - the highest stable semver tag;
  • pre-releases (rc/alpha/beta) are installed only explicitly via --version (which also allows a downgrade).

Platforms (rpp_source.go):

  • releases are published as multi-platform OCI image indexes under plain version tags (v1.2.3);
  • ListTags returns those plain version tags as-is;
  • ExtractBinary pulls the plain tag; the client attaches ?platform=<os>-<arch> (see internal/rpp), so the proxy resolves the matching per-platform child manifest from the index. The proxy must be recent enough to honor the query - see internal/rpp.

Switches

Need How
explicit RPP endpoint --rpp-endpoint / D8_RPP_ENDPOINT
custom CA / skip TLS verification --rpp-ca-file / --rpp-insecure-skip-tls-verify
identity -k/--kubeconfig, --context

Boundaries and deliberate decisions

  • Windows is not supported: a running .exe cannot be replaced.
  • The client does not follow redirects: the Bearer token must never travel to a foreign host.

Package map

File Responsibility
cmd/command.go the d8 cli ... cobra commands; building the Updater
cmd/list.go d8 cli versions: rendering the version list
cmd/use.go d8 cli use: switching versions, store-first; shell completion
update.go Updater and SwitchTo: version selection, store install, repoint, migration
store.go the version store + current symlink (~/.deckhouse-cli/cli)
source.go / rpp_source.go the Source interface and its RPP implementation

Related:

  • internal/rpp - the HTTP client for the proxy (transport, discovery, tar extraction);
  • internal/lockfile - the file lock (shared with plugin installs).

Documentation

Overview

Package selfupdate lets the d8 binary update itself through the cluster (registry-packages-proxy, kubeconfig identity - no registry credentials).

It implements the `d8 cli` command tree:

  • check - is a newer version available
  • versions - list published versions (alias: list)
  • update - install a version and switch to it
  • use - switch to a version (instant if already installed)

How it works, in short:

  • Versions are kept in a per-user store (~/.deckhouse-cli/cli/versions/<tag>/d8); the active one is selected by the `current` symlink, so switching is an atomic repoint - no file copying, no sudo. The PATH entry (e.g. /opt/deckhouse/bin/d8) is a one-time-created symlink to that `current`, with the original binary kept as <exe>.old. Full layout: store.go.
  • Every downloaded binary is smoke-tested (`--version`) before it becomes active; a corrupt or wrong-platform artifact never replaces a working d8.

Wiring: the cobra commands live in the cmd subpackage; this package holds the update flow (update.go) and the store (store.go). Downloads go through the Source interface (source.go) backed by internal/rpp. Details, trade-offs, and the full file map are in README.md next to this file.

Index

Constants

View Source
const (
	// OldSuffix marks the backup of a plain-file install made when it is migrated
	// to the store-managed (symlink) layout.
	OldSuffix = ".old"
)

Variables

This section is empty.

Functions

func CurrentExecutable

func CurrentExecutable() (string, error)

CurrentExecutable resolves the running binary to a real file path (symlinks evaluated). For a store-managed install this lands inside the store (the `current` chain resolved), which is exactly how SwitchTo detects the mode.

Types

type Source

type Source interface {
	ListTags(ctx context.Context) ([]string, error)
	ExtractBinary(ctx context.Context, tag, destination string) error
}

Source lists available deckhouse-cli versions and extracts the binary for a tag.

func NewRPPSource

func NewRPPSource(client *rpp.Client) Source

NewRPPSource builds the proxy-backed release source.

type Store

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

Store is the local store of installed d8 versions plus the `current` symlink that selects the active one - the same versions-directory-plus-symlink layout the plugin installer uses:

<root>/current            -> versions/<version>/d8 (atomic repoint on switch)
<root>/versions/<version>/d8

The PATH entry (e.g. /opt/deckhouse/bin/d8) is a one-time-created symlink to <root>/current, so switching never touches root-owned directories.

Addressed by its own well-known paths, never via os.Executable(). On Linux /proc/self/exe resolves to the symlink TARGET: "replace what the executable resolves to" would overwrite a stored version in place instead of repointing the link.

A nil *Store is a valid no-op store (all read methods are nil-safe), so callers degrade gracefully when the home directory cannot be resolved.

func NewStore

func NewStore() (*Store, error)

NewStore returns the per-user version store at ~/.deckhouse-cli/cli (next to the plugins home-fallback layout).

func NewStoreAt

func NewStoreAt(root string) *Store

NewStoreAt returns a store rooted at an explicit directory (tests, tooling).

func (*Store) Archive

func (s *Store) Archive(ctx context.Context, srcPath, tag string) error

Archive copies the binary at srcPath into the store under tag (used to seed the store with the running binary during migration). Same immutability and semver rules as Install.

func (*Store) Contains

func (s *Store) Contains(path string) bool

Contains reports whether path (already symlink-resolved) lies inside the store - i.e. the running binary is store-managed.

func (*Store) CurrentTag

func (s *Store) CurrentTag() string

CurrentTag returns the version the `current` symlink points at, or "" when the link is absent or points outside the expected versions layout.

func (*Store) List

func (s *Store) List() []*semver.Version

List returns the stored versions newest-first. Foreign entries (non-semver directory names, entries without a binary) are skipped rather than reported: the store is best-effort by design.

func (*Store) Resolve

func (s *Store) Resolve(requested *semver.Version) string

Resolve returns the stored tag matching the requested version (semver comparison, so "0.13.1" finds an entry stored as "v0.13.1"), or "" when the version is not stored.

type SwitchResult

type SwitchResult struct {
	// PrevTag is the version `current` pointed at before the switch ("" when the
	// install was not store-managed yet).
	PrevTag string
	// Migrated reports that the PATH entry was converted from a plain file to a
	// symlink into the store (the original is backed up with OldSuffix).
	Migrated bool
}

SwitchResult describes what a switch did, so commands can tell the user what was left behind and how to undo it.

func SwitchTo

func SwitchTo(ctx context.Context, exePath, tag string, store *Store, logger *dkplog.Logger, stage func(dst string) error) (SwitchResult, error)

SwitchTo makes tag the active d8 version: ensures it is in the store (fetching via stage when missing; stage == nil demands a store hit), smoke-tests it and atomically repoints the store's `current` symlink. The PATH binary is never rewritten - it is a symlink into the store, so the switch happens entirely in the user's home, with no elevated privileges and no file copies.

exePath is the resolved path of the running binary. When it lies outside the store (a plain-file install), the switch MIGRATES it: the running binary is seeded into the store under its own version, and the PATH file is backed up as <exe>.old and replaced with a symlink to the store's `current`. The same path heals an install whose symlink was overwritten by external tooling.

type Updater

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

Updater checks for and installs newer deckhouse-cli releases from a Source.

func NewUpdater

func NewUpdater(source Source, store *Store, logger *dkplog.Logger) *Updater

NewUpdater builds an Updater over the given Source. The store is where versions are installed and switched; without it (nil) updates cannot proceed.

func (*Updater) Apply

func (u *Updater) Apply(ctx context.Context, tag string) (SwitchResult, error)

Apply downloads tag into the version store (unless already present) and makes it the active version by repointing the store's `current` symlink. A plain-file install is migrated to the symlink layout on the way (backup kept as <exe>.old).

func (*Updater) LatestVersion

func (u *Updater) LatestVersion(ctx context.Context, current string) (string, bool, error)

LatestVersion returns the highest available STABLE semver tag and whether it is newer than current (pre-releases are ignored - install them explicitly via --version). A non-semver current (e.g. a "dev" build) is treated as older than any real release, so an update is always offered.

func (*Updater) Versions

func (u *Updater) Versions(ctx context.Context) ([]*semver.Version, error)

Versions returns the published release versions sorted newest-first. Tags that are not valid semver are skipped. Tags are plain version strings (one multi-platform index per release); the proxy picks the platform at pull time.

Directories

Path Synopsis
cmd
Package selfupdatecmd implements the `d8 cli` command tree on top of the internal/selfupdate machinery (store, updater).
Package selfupdatecmd implements the `d8 cli` command tree on top of the internal/selfupdate machinery (store, updater).
errdetect
Package errdetect maps registry-packages-proxy failures from `d8 cli` to HelpfulErrors with CLI-specific guidance.
Package errdetect maps registry-packages-proxy failures from `d8 cli` to HelpfulErrors with CLI-specific guidance.

Jump to

Keyboard shortcuts

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