[deckhouse-cli] Add plugin mirroring to d8 mirror pull and push - #445
Merged
Conversation
- `PluginsService` lists plugin names from the catalog path, `Plugin(name)` scopes to one plugin repo with its version tags. - `ContractAnnotation` reads the base64 contract from the manifest: index annotation first, first child as fallback. - The catalog hangs off the bare registry root, outside the edition segment - same asymmetry as the installer, pinned by tests. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- `AddIndex` writes a whole image index into the layout: platform children, their descriptors, and index annotations stay as published. - Same (tag, digest) idempotency guard as `AddImage`, so retried pulls do not duplicate descriptors. - Needed for CLI plugins: their images are multi-platform and flattening to one platform would lose the rest. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- `pushManifest` picks the push call by descriptor media type: `PushIndex` for a nested index, `PushImage` for a single image. - Before, push failed on any layout with an index: `index.Image()` rejects index media types. - CLI plugin images are such indexes: platform children and the contract annotation must reach the target registry whole. - Round-trip test against an in-memory registry: index digest, both platform children, and the contract annotation survive the push. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- `Catalog` reads the plugins registry: names from the directory-as-tags index, stable semver versions newest first, contracts decoded from the manifest annotation. - Results are memoized per pull: one tags listing per plugin, one manifest fetch per version. - `ErrInvalidContract` marks broken published contracts, so the resolver can skip that version; transport errors stay plain and fail the pull. - Resolver input/output types: typed module versions in, selected plugin versions with provenance out. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- For each bundled version of a mirrored module, the newest plugin version the bundle satisfies is picked; picks are deduplicated. - Mandatory plugin dependencies resolve recursively: shared versions are reused, disjoint constraints get two versions, cycles and depth are guarded. - A plugin with no compatible version is skipped with the reason recorded; an unmet `--include-plugin` fails the pull. - Module and Deckhouse constraints go through `requirements.NormalizedForConstraint` (now exported), so mirror and install compare versions the same way. - Exact pins bypass the stable-version list, so pre-releases stay reachable. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- `PullPlugins` resolves plugin versions for the bundle, pulls them, and packs one `plugin-<name>.tar` per plugin. - Multi-platform indexes are pulled whole: children fetched by digest, so platform binaries and the contract annotation stay as published. - Tars carry the `deckhouse-cli/plugins/<name>` prefix - the path mirror push uploads verbatim and registry-packages-proxy serves. - A registry without a plugins catalog skips auto-selection quietly; dry-run prints the plan and fills stats without downloads. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- The plugins phase runs last: it resolves against the module and platform versions the earlier phases actually selected. - New `--include-plugin name[@constraint]` flag adds plugins on top of the automatic selection; with `--proxy-registry` an exact pin is required. - `PullSummary.Plugins` carries per-version provenance, skips, and advisories for the summary renderer. - Dependencies on built-in d8 commands (`delivery-kit`, `package`) are never mirrored. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- `createPluginsIndex` tags the deckhouse-cli/plugins path with one tag per plugin, so `ListTags` discovery works on the target registry. - The same directory-as-tags convention modules and packages use; matches what registry-bundle serves for bundle-backed registries. - `PushSummary.Plugins` counts pushed plugin repositories; the push summary prints a Plugins line. - Plugin layouts are not touched by `--modules-path-suffix`: their registry path is fixed, pinned by a test. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- The Plugins line shows the count with a provenance breakdown: how many serve modules, how many are dependencies, how many were explicit. - Verbose mode draws the tree: plugins grouped by the module they serve, dependencies nested under their dependents. - Skipped plugins with their reasons are always visible - losing a plugin in an air-gapped bundle should not hide behind a flag. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- Plugin Mirroring section in the mirror README: selection rules, registry layout, what mirror checks vs what install checks. - Air-gapped flow in docs/plugins.md: pull -> push -> `d8 plugins install` through the proxy. - Proxy-registry doc: auto-selection needs a catalog, so only exact `--include-plugin` pins work there. - The push layout doc-tree gains the deckhouse-cli/plugins branch. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- The e2e suite runs the whole `PullService.Pull`: module versions from release channels reach the plugin resolver and pick compatible plugin versions. - Checks summary reasons for picked and skipped plugins, the `deckhouse` constraint, exact pins and the `--only-extra-images` skip. - A pull-to-push roundtrip carries plugin tars into a target registry and checks the plugin name index. - `--modules-path-suffix` moves modules only - the roundtrip checks that plugins stay put. - Test stubs now carry a pullable module and a plugin catalog: the plugins phase is no longer a no-op in orchestration and command-level tests. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
- unparam: every call passed stubModuleVersion, so the helper now reads the constant itself. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Adds automatic CLI plugin mirroring to air-gapped pull/push workflows.
Changes:
- Resolves plugins from mirrored modules, explicit includes, and dependencies.
- Preserves and pushes multi-platform plugin indexes.
- Adds plugin summaries, validation, documentation, and extensive tests.
Reviewed changes
Copilot reviewed 41 out of 41 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
pkg/registry/service/service.go |
Wires plugin registry service. |
pkg/registry/service/plugin_service.go |
Adds plugin catalog operations. |
pkg/registry/service/plugin_service_test.go |
Tests catalog scoping and contracts. |
pkg/registry/image/layout.go |
Adds OCI index storage. |
pkg/registry/image/layout_test.go |
Tests index layout behavior. |
pkg/fake/deckhouse_stub.go |
Adds module/plugin fixtures. |
pkg/fake/deckhouse_stub_test.go |
Tests new fixtures. |
internal/plugins/requirements/checks.go |
Exports version normalization. |
internal/plugins/requirements/checks_test.go |
Updates normalization tests. |
internal/plugins/README.md |
Documents air-gapped delivery. |
internal/mirror/summary.go |
Adds plugin statistics. |
internal/mirror/README.MD |
Documents plugin mirroring. |
internal/mirror/pusher/pusher.go |
Pushes nested OCI indexes. |
internal/mirror/pusher/pusher_test.go |
Tests multi-platform pushes. |
internal/mirror/push.go |
Pushes plugins and catalog tags. |
internal/mirror/push_test.go |
Tests plugin push layout. |
internal/mirror/pull.go |
Integrates plugin pull phase. |
internal/mirror/pull_test.go |
Extends pull coverage. |
internal/mirror/pull_plugins_wiring_test.go |
Tests phase handoff. |
internal/mirror/pull_plugins_e2e_test.go |
Tests pull/push plugin workflows. |
internal/mirror/PROXY-REGISTRY.md |
Documents proxy restrictions. |
internal/mirror/plugins/types.go |
Defines resolver types. |
internal/mirror/plugins/stats.go |
Implements plugin accounting. |
internal/mirror/plugins/resolver.go |
Implements plugin resolution. |
internal/mirror/plugins/resolver_test.go |
Tests resolver scenarios. |
internal/mirror/plugins/pull_plugins_test.go |
Tests plugin pulling and packing. |
internal/mirror/plugins/plugins.go |
Implements plugin mirror service. |
internal/mirror/plugins/doc.go |
Documents the package. |
internal/mirror/plugins/catalog.go |
Implements registry catalog access. |
internal/mirror/plugins/catalog_test.go |
Tests catalog behavior. |
internal/mirror/cmd/push/summary.go |
Displays pushed plugin count. |
internal/mirror/cmd/push/summary_test.go |
Tests push summary output. |
internal/mirror/cmd/pull/validation.go |
Validates proxy plugin pins. |
internal/mirror/cmd/pull/validation_test.go |
Tests plugin pin validation. |
internal/mirror/cmd/pull/summary.go |
Renders plugin pull summary. |
internal/mirror/cmd/pull/summary_test.go |
Tests plugin summary output. |
internal/mirror/cmd/pull/pull.go |
Wires plugin CLI options. |
internal/mirror/cmd/pull/pull_plugins_stub_test.go |
Tests command-level plugin pulls. |
internal/mirror/cmd/pull/flags/flags.go |
Adds --include-plugin. |
internal/layout.go |
Defines plugin registry segments. |
docs/plugins.md |
Documents air-gapped installs. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
PluginsStats.Warnings was filled by the resolver but never rendered, so unmet gates and co-installation conflicts only appeared in the live log. Warnings now follow the skipped plugins, always visible: a plugin the target cluster cannot run is as much a surprise as a missing one. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
validateProxyRegistryFlag rejected --no-platform --no-modules as "nothing to do" before it reached the --include-plugin check, so exact plugin pins could never be mirrored on their own through a proxy registry. Plugins are now counted as a component in that guard. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
Mirror built layout directories, bundle tar names and registry routes from plugin names taken verbatim from the catalog, --include-plugin and contract dependencies. The registry client normalizes "../x" away while filepath.Join keeps it, so a hostile source registry could write outside the working directory. The single-component rule from internal/plugins now lives in internal/plugins/layout and is applied at every name source: catalog entries are dropped, explicit includes error out, dependency names reject the dependent. layoutFor guards the join as a last line. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
rebuildIndex re-marshals the top-level index locally and dropped the OCI 1.1 subject and per-child inline data, silently changing the digest of indexes that carry them. Both fields now travel with the index; per-child artifactType stays out because ggcr's mutate cannot set it. The multi-platform round-trip test now asserts the bundled index keeps the published digest, which its doc comment promised but never checked. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
--only-extra-images skips the plugins phase, so an explicit --include-plugin was accepted by validation and then dropped without a word: exit 0, no plugin tar in the bundle. Fail up front instead - an explicit include is either mirrored or an error. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
Explicit pins were resolved one by one in name order, so a dependency the user pinned was reused only when its name sorted first; otherwise a newer version was pulled beside it, and with --proxy-registry the pull failed outright because dependencies were looked up in a catalog the proxy does not serve. Pins are now all committed first, then their dependencies resolved; without a catalog an unpinned dependency is an error that names the pin to add. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
d8 mirror auto-selects plugins from the mirrored external modules; modules embedded in the platform image are not a supported dependency. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
A plugin the auto path passed over but the user then included explicitly was reported both as mirrored and as skipped in the summary. A skip is dropped once the plugin is in the final selection; the explicit path's warning about the unmet module requirement stays. Signed-off-by: Roman Berezkin <roman.berezkin@flant.com>
Glitchy-Sheep
marked this pull request as ready for review
August 18, 2026 07:14
ldmonster
approved these changes
Aug 19, 2026
4 tasks
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
d8 mirrornow carries CLI plugins into air-gapped installs.Pull picks the plugin versions the mirrored modules need, packs each plugin into its own tar, and push publishes them so
d8inside the closed network can install them.How it works
deckhouse-cli/plugins/<name>, next to the installer, outside the edition path.--include-plugin.Before / After
Before:
d8 mirrorcarried the platform, modules, packages and security databases; plugins could not reach closed networks at all.After: the bundle also holds
plugin-<name>.tarfor every plugin the modules need; push uploads them to the same paths and adds a name tag to the catalog.Tests
TestPullE2E_*(9 tests): the wholePullService.Pullon a fake registry, plus a pull-to-push roundtrip into a target registry.Puller.Executewith the registry stub produces plugin tars next to module tars; dry-run writes nothing.Notes
--include-pluginaccepts ranges and exact pins (name@=vX.Y.Z); only pins reach pre-releases. With--proxy-registry, only exact pins are allowed.