[deckhouse-cli] Plugins / d8 self-update with requirements check#386
Open
Glitchy-Sheep wants to merge 48 commits into
Open
[deckhouse-cli] Plugins / d8 self-update with requirements check#386Glitchy-Sheep wants to merge 48 commits into
Glitchy-Sheep wants to merge 48 commits into
Conversation
- exclusive O_EXCL lock with identity-checked stale reclaim - shared lock dialect for plugin install and self-update Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
…, TLS) - HTTP client over the proxy /v1/images routes with kubeconfig bearer identity - TLS hardening: CA bundle / insecure flag, redirect ban, size limits, timeouts Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- discover the proxy endpoint from the cluster (public Ingress preferred, pod-IP fallback) - cluster client wiring and safe gzip/tar extraction with decompression-bomb limits Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- table-driven tests for client / transport / endpoint discovery / extraction over an httptest TLS proxy - drop a stale package-doc line about an HTTP HEAD stat the client no longer does Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- export UnmarshalContract so the rpp plugin source decodes contracts with the same user-actionable errors - correct the module-requirements doc: mandatory/conditional check "enabled", not merely "in the cluster" Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
…tract decoder - remove PluginService and its OCI methods (contract-from-annotation, image extract, tag/catalog listing); the registry-packages-proxy is the only plugin source - keep the contract decoder and DTO<->domain converters in contract.go, reused by the rpp source, validators and install Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- on-disk layout helpers, the registry-packages-proxy flag set, the PluginSource interface and the Manager struct - foundation the install/update/run/list machinery is built on; rpp is the only plugin source Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- one-shot ClusterState snapshot (Kubernetes/Deckhouse/module versions) and the ordered named checks against it - distinguishes a genuinely unmet requirement (selection may retry older) from an operational error that must propagate Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
…tion - validate a plugin contract (plugin-to-plugin deps + cluster-side checks) before install/run; read the cached contract - resolve missing mandatory plugin deps when asked, with the cluster snapshot cached per command run Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- pick the newest STABLE version whose cluster-side requirements hold, walking newest-to-oldest with a contract cache - a genuinely incompatible version demotes to an older one; an unreachable cluster or broken contract hard-stops Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- lock, staged download, smoke-test and atomic swap; validate requirements before any switch - relink-only path for an already-installed version; downgrade guard for the implicit/background update Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- adapts the registry-packages-proxy client to PluginSource: list tags, read the YAML contract, extract the binary - tolerates contract-less images; in-process HTTPS e2e covering list -> contract -> extract Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- RunInstalled: lazy install, contract requirement gate before run, env injection, SIGTERM grace on cancel - local help/version/completion args bypass the cluster gate so a plugin stays usable offline Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
…emove - InitPluginServices builds the registry-packages-proxy client from the kubeconfig identity - the only plugin source - list installed plugins, published versions, update-all within major (home-fallback aware), remove / remove-all Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- detached, throttled (6h TTL) spawn of the visible `d8 plugins update all`; never blocks the command, fails closed on marker write - skipped when disabled by env, on Windows, within the TTL, or with nothing installed; home-fallback aware Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- move the machinery into internal/plugins; cmd/ keeps only cobra wiring (list/versions/contract/install/update/remove + per-plugin wrapper) - drop the old in-cmd impl (validators, init, layout, flags); add versions command and cmd-level tests Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- per-user store (~/.deckhouse-cli/cli/versions/<tag>/d8) selected by a `current` symlink; switching is an atomic repoint, no sudo - staged install with smoke-test before an entry becomes visible; immutable entries; nil-safe best-effort reads Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- Updater: list/select latest stable, stage+smoke into the store, atomic current-symlink switch under a stale-reclaiming lock - migrate a plain-file install to the symlink layout (backup as <exe>.old, rollback on failure); rpp source normalizes per-platform tags Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- print a one-line "newer version available" notice from a per-user cache; refreshed synchronously by the root hook, at most once per 24h TTL - best-effort and silent (missing cache, non-semver dev build, disabled via env); correct the package doc to describe the refresh as synchronous Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- check/update/versions(list)/use over the registry-packages-proxy; use switches to a stored version offline, downloads otherwise - cron prints a copy-pasteable crontab line (d8 never edits crontab itself); RefreshNoticeCache feeds the root-hook notice Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- register `d8 plugins` and `d8 cli`; ExecuteC resolves the run command so the post-command hook gates by resolved name, not os.Args - recursion-safe background hook: synchronous self-update notice + detached plugin auto-update, skipped for cli/plugins/help/completion Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- plugins.md / self-update.md user guides and package READMEs: rpp-only sourcing, the store/symlink layout, requirements, auto-update - accurate TTLs (plugins 6h, notice 24h), the synchronous notice refresh, and the independent per-mechanism disable switches Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- platform service streams images_digests.json from the registry instead of pulling the installer to an OCI layout; installer/security/modules stage blob-less scaffolding - refresh the expected output, artifact table, call tree and test references to match Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
…onfig in the root hook - the flag-less root hook passed an empty kubeconfig path, but SetupK8sClientSet does not fall back to $KUBECONFIG / ~/.kube/config on "", so the notice refresh always failed with "no updater" and the notice never appeared - resolve the default path ($KUBECONFIG, else ~/.kube/config) in newDefaultUpdater; explicit `d8 cli ...` commands were unaffected (they pass the flag value) Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- d8 no longer does anything in the background after a command: no plugin auto-update, no self-update notice, no detached cache refresh. - Automation will be redesigned in a separate PR; only the manual commands stay for now. - `d8 cli` keeps `check`/`update`/`use`/`versions`, `d8 plugins` its install/update/remove set - all run only when invoked. - Drop the `d8 cli cron` helper, the `internal/bgproc` spawner, and the root-command gate that only guarded the background work. - Strip the `D8_DISABLE_*` switches and the auto-update sections from docs and READMEs. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
'd8 plugins list --available' listed the registry catalog through the direct-registry plugin service. That service is gone (RPP is the only source now), and the proxy has no catalog endpoint, so the listing could never return anything. Drop the dead path: ListPlugins, fetchAvailablePlugins, the --available/--installed flags. 'd8 plugins list' now shows installed plugins plus a hint to install by name. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
PullImage returned a Descriptor (manifest digest + size) that every caller discarded and nothing ever read - idempotency is version-based, not digest-based, and there is no digest-verification path. Return just the body stream and re-add the digest when artifact verification actually lands. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
The comments and README bullets promised a future per-artifact digest check, but a digest served by the same proxy proves nothing - real authenticity needs a publisher signature. Remove the notes; the trust model (TLS + kubeconfig identity, smoke test) stands without them. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
…dater The plugin install/update and self-update comments justified their guards by an 'unattended background update' that was removed in e9446bd. Reword them to the manual reality (implicit update / Ctrl-C'd install); no code change. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
Several identifiers in rpp/selfupdate/plugins were exported but used only within their own package: the rpp endpoint-discovery helpers, the selfupdate version-store methods, the plugins source interface and failed-constraints type, and the layout/flags path segment constants. Lowercase them to keep the internal API surface minimal. No behavior change. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
Remove/RemoveAll deleted a plugin directory with os.RemoveAll without holding the per-plugin install lock that installs take, so a remove could wipe the directory out from under a concurrent install of the same plugin and corrupt it. Acquire the same lock first (shared removeLocked helper); removing a plugin that is not installed stays a lock-free no-op. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
backupOldBinary renamed the live binary to .old before the new one was in place, leaving the binary path briefly absent while the 'current' symlink still pointed at it - a concurrent 'd8 <plugin>' in that window failed to exec. Copy the binary to .old instead and let the atomic rename of the new binary replace it, so the path is never absent. A failed swap now leaves the original untouched, so the former restore-from-.old becomes a simple cleanup. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
c7911ae to
648a3d2
Compare
…tion resolvePluginConflicts installs the latest cluster-compatible version of each unsatisfied dependency, ignoring the requiring plugin's recorded version constraint. validateAndResolveConflicts then returned nil without re-checking, so install reported success for a plugin the run-time gate blocks at first run. Re-run validateRequirements after resolution and fail if any requirement is still unmet, surfacing the conflict at install time. Add a regression test for the resolution path. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
…ailable NewPluginCommand returned nil when EnsureInstallRoot failed. With DECKHOUSE_PLUGINS_ENABLED=true, root.go's AddCommand(nil) then dereferenced the nil command and panicked, taking down every d8 invocation, not just the plugin. Warn and keep building the command instead; RunInstalled surfaces the root error at invocation time (and EnsureInstallRoot already falls back to the home root when the default one is unwritable). Add a regression test asserting a non-nil command when the install root cannot be created. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
… fails SwitchTo repointed the store 'current' symlink before migrating the PATH binary. For a plain-file install, a migration failure (e.g. no sudo on a root-owned directory) left current on the new tag while the PATH binary still ran the old one - a split state from a command that reported failure. Roll current back on migration failure: restore the previous tag, or remove the link on a first install where there was no previous current. Add a regression test asserting current is not left repointed after a failed migration. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
The branch removed the background plugin auto-updater (autoupdate/, updatecheck/) and the direct-registry plugin source, leaving rppPluginSource as the only source and update-all as a manual command. Several comments and the plugins README still described the deleted background child and a second 'registry source'. Reword them to the manual-only, RPP-only reality: no behavior change. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
backupOldBinary copied the live plugin binary to <binary>.old before the swap, but after the move to copy-staged + atomic rename-over (commit 648a3d2) nothing ever restored from .old: the atomic rename already leaves the original intact on failure. The .old copy was a full binary copy on every install/update and a stale file left behind on success - pure dead machinery. Drop backupOldBinary, removeOldBackup, copyFile and the .old logic (and the io import they needed); the install pipeline is now download staged -> smoke-test -> rename over -> cache contract -> relink current. Update the README step and drop the .old-specific tests. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- rpp: discoverEndpoints built a slice of every serving pod but only [0] was ever used (there is no failover). Return a single endpoint (discoverEndpoint). - selfupdate/cmd: inline buildUpdater into newUpdater - one caller, the (kubeconfig, context) seam was never reused. - plugins/cmd remove: drop the duplicate ValidatePluginName; Manager.Remove already validates the name before touching the filesystem. No behavior change. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
Installing a plugin with unmet plugin requirements printed a raw slog JSON warn and a bare "plugin requirements not satisfied" with no dependency name. Extract failedConstraints.describe() (shared with the run-time gate) so both paths name the missing or incompatible plugins, add a hint to install them or use --resolve-plugins-conflicts, drop the duplicate unsatisfiedNames helper, and lower the per-requirement warns to Debug so normal output stays clean. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
plugins/doc.go had no filesystem layout; add an On-disk layout section with the concrete paths and point to the layout package as the source of truth. selfupdate/doc.go now notes the PATH symlink and the <exe>.old backup. Doc-only, no logic change. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
…list The actionable requirement error ran name, items and hint together on one line. Make failedConstraints.describe() emit one indented bullet per requirement, separate the list with blank lines, and label the next step as an indented "Hint:" line, so install-time and run-time failures read cleanly. Output formatting only. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
9a1b7d3 to
f6186a9
Compare
Two coloring changes to the plugin install UX, both TTY-aware (plain under NO_COLOR / in pipes): - Unsatisfied requirements now return a *diagnostic.HelpfulError, so the top-level handler (cmd/d8/root.go) renders them with semantic color - bold-red header, yellow cause, cyan fix - one cause/fix pair per dependency. This also adds the blank line before the error and drops the generic "Error executing command:" prefix. Replace failedConstraints.describe() with a helpfulError builder; assert on the structured error in tests. - Color the install banner key labels (Installing plugin:/Tag:/Plugin:/Description:) cyan+bold via fatih/color, matching the repo's existing color conventions. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
…tput All TTY-aware (plain under NO_COLOR / in pipes): - A no-write-access failure during the PATH swap now returns a *diagnostic.HelpfulError, so the top-level handler renders it with color and actionable fixes (sudo, or install into a user-writable directory) instead of an inline sudo hint appended to the raw error. - check/update/use mark success with a green checkmark and highlight version numbers (green = target, cyan+bold = active, faint = superseded), matching d8 cli versions. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
…tics
Recognized proxy failures (401/403/404/5xx) were raw, wrapped error chains in d8 plugins and d8 cli. Give each command tree its own errdetect package (internal/selfupdate/cmd/errdetect and internal/plugins/cmd/errdetect) that maps the rpp sentinel errors to a *diagnostic.HelpfulError with concise, command-specific guidance: the 403 names the right ClusterRole (cli-download vs packages-download) and the 404 points at the right place ('d8 cli versions' vs the plugins publication path).
Each tree's constructor wraps its RunE and calls only its own Diagnose, so classification stays in the command (per pkg/diagnostic), not in root.go, and the two trees stay independent - easier to read, maintain, and less conflict-prone. internal/rpp keeps only the sentinel errors and no longer depends on pkg/diagnostic.
Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
8fe683e to
c0a02e0
Compare
- Tag kube-API discovery failures - unreachable, bad TLS cert, rejected identity - as `ErrEndpointDiscovery`. - Restrict the pod-IP fallback to an absent or host-less Ingress (`errIngressUnusable`), so a failing kube-API isn't masked by a pod listing that fails identically. - `d8 plugins` and `d8 cli` map the error to a colored hint pointing at the kubeconfig `server:` and the `--rpp-endpoint` / `D8_RPP_ENDPOINT` bypass. - Cover the absent and host-less Ingress fallbacks, and an API TLS failure surfacing even when a serving pod exists. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- Split run-on sentences and drop participial chains, so each comment reads one idea per line. - Reflow dense rationale into bullet lists where it aids scanning: `normalizedForConstraint`, `withTunedTransport`, `ErrEndpointDiscovery`, the `extract.go` path-traversal note. - Drop comments that just restate the code: `// check if version is specified`, `// CLI Parameters`, the conflict-resolution loop note. - Polish the two `ErrEndpointDiscovery` hints in the `d8 cli` discovery error - the only non-comment change. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds two ways to keep
d8current, both reaching the cluster registry only through the in-cluster registry-packages-proxy (RPP), authenticated by the caller's kubeconfig identity:d8 cli- update thed8binary itself.d8 plugins- install, update, run and remove plugins (standalone binaries that behave like native subcommands).No registry credentials are handed to users: access is an ordinary RBAC grant on the proxy, so an admin decides who may download and can revoke per-identity. Plugin operations validate the plugin's declared requirements before anything is downloaded or switched.
Why
d8core, while still installing as native-looking subcommands.d8itself compact - pull only what a user actually invokes.How a request reaches the registry
flowchart LR U["d8 cli update / d8 plugins install"] -->|"Bearer token from kubeconfig"| ING["Ingress (auto-discovered)<br/>pod-IP fallback"] ING --> KRP["kube-rbac-proxy<br/>TokenReview + SubjectAccessReview"] KRP -->|"cli-download RBAC role"| RPP["registry-packages-proxy"] RPP --> REG["cluster registry"]The endpoint is discovered from the cluster (public Ingress preferred, master pod-IP as fallback) or set explicitly with
--rpp-endpoint. TLS is verified against system roots or a supplied CA; redirects are refused so the Bearer token never leaves the proxy host.New commands
d8 cli- updated8itselfcheck- report whether a newer published version exists.versions(aliaslist) - list published versions newest-first; the active one is starred, locally installed ones are marked.update [--version X]- install a version into the per-user store, smoke-test it, then point the binary at it.--versionalso downgrades.use <version>- switch versions: an installed one is an instant offline symlink repoint (no cluster, no sudo); a missing one is downloaded first.Versions live in a per-user store and the active one is chosen by a
currentsymlink, so switching copies nothing:d8 plugins- manage pluginsinstall <name> [--version X | --use-major N | --force | --resolve-plugins-conflicts]- install or switch a plugin version; requirements are checked first.update <name>/update all- move to the newest cluster-compatible version within the installed major.versions <name>- list a plugin's published versions.contract <name>- show a plugin's contract (version, description, requirements) without installing.list- list installed plugins (the proxy serves no catalog, so available plugins are addressed by name).remove <name>/remove all- uninstall.A plugin image carries the binary plus a
contract.yamldescribing its env vars, flags and requirements. WhenDECKHOUSE_PLUGINS_ENABLED=true, the built-insystemcommand is served by a plugin wrapper that installs it on first use, then execs it with arguments forwarded verbatim and contract-requested env injected.Safe install and update
Every install/update runs under a per-component lock as an all-or-nothing pipeline, so a failure never leaves a half-swapped or broken binary:
flowchart TD A["acquire install lock"] --> B["validate requirements (before any download)"] B -->|"unmet"| Z["abort - nothing downloaded or changed"] B -->|"ok"| C["download to staged sibling .new"] C --> D["smoke test (--version)"] D -->|"fails"| Z2["abort - live binary untouched"] D -->|"passes"| E["atomic rename over the live binary"] E --> F["cache the contract"] F --> G["repoint the current symlink"]Self-update follows the same shape: a downloaded binary is smoke-tested before it can become
current, and a failed download leaves the running binary in place.Requirements model
Validated against a single cluster snapshot taken per command:
--skip-cluster-checksfor testing).--resolve-plugins-conflictsauto-installs missing ones and re-validates, failing if a resolved version still does not satisfy the constraint.Example usage
Self-update (
d8 cli)See what is published and which version is active:
Check whether a newer version exists:
A mandatory plugin dependency that is not installed (retry with
--resolve-plugins-conflictsto pull it in):A typo in plugins versions leads to a user-friendly error
A typo in version to update to is also leading to a user-friendly error
Configuration
--kubeconfig/--contextKUBECONFIG--rpp-endpointD8_RPP_ENDPOINT--rpp-ca-file/--rpp-insecure-skip-tls-verifyD8_RPP_CA_FILE--plugins-dirDECKHOUSE_CLI_PATH/opt/deckhouse/lib/deckhouse-cli, home fallback when not writable)--skip-cluster-checksD8_PLUGINS_SKIP_CLUSTER_CHECKS=1DECKHOUSE_PLUGINS_ENABLED=trueWhat is new under the hood
internal/rpp- the proxy client: endpoint discovery, kubeconfig-identity transport (TLS, redirect ban, size limits), routes, tar.gz extraction.internal/selfupdate- thed8 clitree, the versioned store and the atomic switch flow.internal/plugins- the plugin machinery (install / run / select / validate / update / remove) with thin cobra wrappers ininternal/plugins/cmd.internal/lockfile- cross-process exclusive locks shared by self-update and plugin installs.pkg/registry/service/contract.go- the shared contract decoder. The direct-registry plugin path is removed: the proxy is the only source.User docs:
docs/self-update.md,docs/plugins.md; package notes ininternal/selfupdate/README.md,internal/plugins/README.md.Tests
internal/rpp,internal/selfupdate,internal/plugins(+requirements,cmd),internal/lockfile,pkg/registry/service.rpp_source_test.goin plugins and selfupdate) drive the full path against an in-process TLS server: list tags, pull, extract, atomic swap, smoke test, platform-tag preference.Notes
cli croncommand; they were removed -d8downloads or replaces nothing without an explicit command.iam accessslice preallocation andsig-migrateformatting.