Skip to content

[deckhouse-cli] Plugins / d8 self-update with requirements check#386

Open
Glitchy-Sheep wants to merge 48 commits into
mainfrom
feat/cli-and-plugins-autoupdate
Open

[deckhouse-cli] Plugins / d8 self-update with requirements check#386
Glitchy-Sheep wants to merge 48 commits into
mainfrom
feat/cli-and-plugins-autoupdate

Conversation

@Glitchy-Sheep

@Glitchy-Sheep Glitchy-Sheep commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds two ways to keep d8 current, both reaching the cluster registry only through the in-cluster registry-packages-proxy (RPP), authenticated by the caller's kubeconfig identity:

  • d8 cli - update the d8 binary 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

  • Let utilities (system, delivery, ...) ship and version independently of the d8 core, while still installing as native-looking subcommands.
  • Keep d8 itself compact - pull only what a user actually invokes.
  • Replace per-user registry secrets with the existing cluster identity: the proxy holds the registry credentials, the user presents a kubeconfig Bearer token.

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"]
Loading

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 - update d8 itself

  • check - report whether a newer published version exists.
  • versions (alias list) - 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. --version also 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 current symlink, so switching copies nothing:

<d8 in PATH>  ->  ~/.deckhouse-cli/cli/current  ->  versions/<tag>/d8
   (migrated once to a symlink; the original is kept as <exe>.old)

d8 plugins - manage plugins

  • install <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.yaml describing its env vars, flags and requirements. When DECKHOUSE_PLUGINS_ENABLED=true, the built-in system command 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"]
Loading

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:

  • Kubernetes / Deckhouse / module constraints - checked only when the contract declares them; a snapshot that cannot be built is a hard error (override with --skip-cluster-checks for testing).
  • Plugin-to-plugin dependencies:
    • mandatory - the dependency must be installed and satisfy the constraint; --resolve-plugins-conflicts auto-installs missing ones and re-validates, failing if a resolved version still does not satisfy the constraint.
    • conditional - enforced only when the dependency is already installed.
  • Version selection picks the newest stable semver compatible with the cluster; pre-releases are excluded by default, an implicit pick never downgrades a newer installed version, and requirements are re-checked before every plugin run.

Example usage

Self-update (d8 cli)

See what is published and which version is active:

image

Check whether a newer version exists:

image

A mandatory plugin dependency that is not installed (retry with --resolve-plugins-conflicts to pull it in):

image

A typo in plugins versions leads to a user-friendly error

image

A typo in version to update to is also leading to a user-friendly error

image

Configuration

Flag Env Purpose
--kubeconfig / --context KUBECONFIG cluster identity (Bearer token)
--rpp-endpoint D8_RPP_ENDPOINT proxy base URL (auto-discovered if unset)
--rpp-ca-file / --rpp-insecure-skip-tls-verify D8_RPP_CA_FILE proxy TLS (custom CA / skip)
--plugins-dir DECKHOUSE_CLI_PATH plugins root (default /opt/deckhouse/lib/deckhouse-cli, home fallback when not writable)
--skip-cluster-checks D8_PLUGINS_SKIP_CLUSTER_CHECKS=1 downgrade cluster-side requirement checks to warnings
- DECKHOUSE_PLUGINS_ENABLED=true enable the plugin subsystem (staged rollout)

What 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 - the d8 cli tree, the versioned store and the atomic switch flow.
  • internal/plugins - the plugin machinery (install / run / select / validate / update / remove) with thin cobra wrappers in internal/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 in internal/selfupdate/README.md, internal/plugins/README.md.

Tests

  • Unit coverage across internal/rpp, internal/selfupdate, internal/plugins (+ requirements, cmd), internal/lockfile, pkg/registry/service.
  • Proxy-emulating tests (rpp_source_test.go in plugins and selfupdate) drive the full path against an in-process TLS server: list tags, pull, extract, atomic swap, smoke test, platform-tag preference.
  • Targeted regression tests for the install pipeline: requirements gate before download, conflict-resolution re-validation, smoke-failure rollback, and self-update PATH-migration rollback.

Notes

  • This branch deliberately ships manual-only delivery. Earlier commits added background auto-update, a startup notice and a cli cron command; they were removed - d8 downloads or replaces nothing without an explicit command.
  • The branch also carries two small unrelated tidy-ups: iam access slice preallocation and sig-migrate formatting.

- 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>
@Glitchy-Sheep Glitchy-Sheep self-assigned this Jun 15, 2026
@Glitchy-Sheep Glitchy-Sheep added the enhancement New feature or request label Jun 15, 2026
'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>
@Glitchy-Sheep Glitchy-Sheep force-pushed the feat/cli-and-plugins-autoupdate branch from c7911ae to 648a3d2 Compare June 15, 2026 12:49
…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>
@Glitchy-Sheep Glitchy-Sheep force-pushed the feat/cli-and-plugins-autoupdate branch from 9a1b7d3 to f6186a9 Compare June 16, 2026 11:08
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>
@Glitchy-Sheep Glitchy-Sheep force-pushed the feat/cli-and-plugins-autoupdate branch from 8fe683e to c0a02e0 Compare June 16, 2026 13:08
- 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>
@Glitchy-Sheep Glitchy-Sheep marked this pull request as ready for review June 17, 2026 08:59
@Glitchy-Sheep Glitchy-Sheep requested a review from ldmonster as a code owner June 17, 2026 08:59
- 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant