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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ D8 provides comprehensive cluster management capabilities:
| [**backup**](internal/backup/) | Backup operations | ETCD snapshots, configuration backups, data export |
| [**cr**](internal/cr/) | Container registry tool | Pull/push images, list tags & repos, inspect manifests/configs, browse image filesystems |
| [**mirror**](internal/mirror/) | Module mirroring | Registry operations, image synchronization, air-gapped deployments |
| [**system**](internal/system/) | System diagnostics | Debug info collection, logs analysis, troubleshooting |
| [**status**](internal/status/README.md) | Cluster status report | Read-only health snapshot: nodes, Deckhouse pods, releases, edition, settings, registry, alerts, CNI, queue |
| [**system**](internal/system/README.md) | Platform operations | Config get/edit, module lifecycle, package scans, queue dumps, controller logs, debug archives |
| **user-operation** | Local user operations | Request `UserOperation` in `user-authn` (ResetPassword/Reset2FA/Lock/Unlock) |

### 🚀 Module Management
Expand Down
30 changes: 17 additions & 13 deletions docs/mirror-pull-dry-run.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

## What dry-run does

`--dry-run` runs the full planning pipeline of `d8 mirror pull` — version resolution,
release-channel discovery, module filtering, installer tag lookup — then **prints the
complete list of images that would be downloaded** and exits without writing any bundle
output to the bundle directory.
`--dry-run` runs the full planning pipeline of `d8 mirror pull` — version resolution, release-channel discovery, module and package filtering, installer tag lookup — then **prints the images that would be downloaded** and exits without writing any bundle output to the bundle directory.

> The **platform** plan is complete (the full component digest list is streamed from the installer image). For **modules** and **packages**, the plan lists release-channel refs and per-version images, but does **not** resolve `extra_images.json` extra images or `.att` VEX attestations — those require a real pull, so the plan and its counts undercount what a real pull downloads for each module/package.

The key distinction from a no-op:

Expand All @@ -14,16 +13,16 @@ The key distinction from a no-op:
| Validate registry access | yes | yes |
| Resolve versions / channels | yes | yes |
| Read installer `images_digests.json` (platform) | via OCI layout in tmpDir | **streamed from the registry** (no layout) |
| Stage installer/security/module OCI layout dirs in tmpDir | yes (with blobs) | **scaffolding only** (no image blobs) |
| Stage installer/security/module/package OCI layout dirs in tmpDir | yes (with blobs) | **scaffolding only** (no image blobs) |
| Pull release-channel metadata | yes | yes |
| Download platform/module/security blobs | yes | **no** |
| Write `platform.tar`, `security.tar`, module tarballs | yes | **no** |
| Write `platform.tar`, `security.tar`, module/package tarballs, `package-versions.tar` | yes | **no** |
| Write `deckhousereleases.yaml` | yes | **no** |
| Compute GOST digests | yes | **no** |

In dry-run the **platform** service streams the built-in image digest list
(`images_tags.json` / `images_digests.json`) straight from the remote installer image,
layer by layer, without writing an OCI layout. The `installer`, `security` and `modules`
layer by layer, without writing an OCI layout. The `installer`, `security`, `modules` and `packages`
services still create their OCI layout directories under `--tmp-dir` (or
`<bundle-path>/.tmp`), but in dry-run they pull no image blobs (only layout scaffolding),
so `--tmp-dir` ends up non-empty while the **bundle directory** (first positional
Expand Down Expand Up @@ -230,10 +229,14 @@ Puller.Execute()
│ ├─ validateSecurityAccess()
│ ├─ downloadList.FillSecurityImages()
│ └─ [dry-run guard] print plan → return nil
└─ modules.Service.PullModules() [DryRun=true]
├─ discover modules (ListRepositories)
├─ per module: extractVersionsFromReleaseChannels()
└─ [dry-run guard] print plan → return nil
├─ modules.Service.PullModules() [DryRun=true]
│ ├─ discover modules (ListTags)
│ ├─ per module: extractVersionsFromReleaseChannels()
│ └─ [dry-run guard] print plan → return nil
├─ packages.Service.PullPackages() [DryRun=true]
│ └─ [dry-run guard] print per-package plan → return nil
└─ packages.Service.PullPackageVersions() [DryRun=true, ALWAYS runs]
└─ [dry-run guard] print package-versions plan → return nil

After Pull() returns:
if DryRun → print summary "No images were downloaded (dry-run)." → return nil
Expand All @@ -255,10 +258,11 @@ what a real pull actually downloads.
|----------|---------------------|-------------|
| `<tmpDir>/platform/...` | **no** | Platform digests are streamed; no platform OCI layout is written |
| `<tmpDir>/installer/` | **yes** | Installer OCI layout dir (scaffolding only, no blobs) |
| `<tmpDir>/security*/`, `<tmpDir>/modules*/` | **yes** | Security / module OCI layout dirs (scaffolding only, no blobs) |
| `<tmpDir>/security*/`, `<tmpDir>/modules*/`, `<tmpDir>/packages*/` | **yes** | Security / module / package OCI layout dirs (scaffolding only, no blobs) |
| `<bundleDir>/platform.tar` | no | Not created |
| `<bundleDir>/security.tar` | no | Not created |
| `<bundleDir>/modules-*.tar` | no | Not created |
| `<bundleDir>/module-*.tar` | no | Not created |
| `<bundleDir>/package-*.tar`, `package-versions.tar` | no | Not created |
| `<bundleDir>/deckhousereleases.yaml` | no | Not created |

`tmpDir` is cleaned up by a subsequent normal pull or can be removed manually.
88 changes: 66 additions & 22 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Plugins are versioned binaries distributed through the cluster registry.
[Versions & majors](#versions-majors-and-switching) ·
[Requirements](#requirements) ·
[Flags & env](#flags-and-environment-variables) ·
[Troubleshooting](#troubleshooting)
[Troubleshooting](#troubleshooting) · [Advanced](#advanced-hidden-flags)

> [!NOTE]
> The `d8 plugins` command group is hidden from the root `--help` while the
Expand All @@ -16,19 +16,26 @@ Plugins are versioned binaries distributed through the cluster registry.
## Plugin source

Plugins are pulled from the in-cluster **registry-packages-proxy**, the same
channel as d8 self-update. There is no direct-registry path: every `d8 plugins`
channel as d8 self-update. This is the only supported path: every `d8 plugins`
command reaches the registry through the proxy, so a reachable cluster is
required. The access model:
required. (A hidden, temporary `--source` flag pulls straight from a registry
repo instead - see [Advanced](#advanced-hidden-flags) - but it bypasses the
cluster and is not the intended flow.) The access model:

- Authentication: the **Bearer token** from your kubeconfig (client
certificates do not work).
- Authorization: the ClusterRole `d8:registry-packages-proxy:cli-download`,
bound by the cluster administrator.
- Endpoint: discovered automatically; override with `--rpp-endpoint` /
`D8_RPP_ENDPOINT`, pass a private CA with `--rpp-ca-file`.

See [self-update.md - How access works](self-update.md#how-access-works) for
the full picture (RBAC binding example, OIDC kubeconfig, endpoint discovery).
- Authorization: the ClusterRole
`d8:registry-packages-proxy:packages-download`, bound by the cluster
administrator. Authorization is cached for about 5 minutes, so after the
binding is created, retry with a fresh token.
- Endpoint: discovered automatically through your kubeconfig's API server;
override with `--rpp-endpoint` / `D8_RPP_ENDPOINT`, pass a private CA with
`--rpp-ca-file`.

The access model is shared with d8 self-update (see
[self-update.md - How access works](self-update.md#how-access-works) for the
OIDC-kubeconfig and endpoint-discovery details), but the ClusterRole differs:
plugins need `packages-download`, CLI self-update needs `cli-download`.

## Commands

Expand Down Expand Up @@ -78,24 +85,31 @@ Rules that follow from this layout:

## Requirements

A plugin's contract may declare requirements:

- other plugins;
- Kubernetes / Deckhouse versions;
- enabled modules.
A plugin's contract may declare requirements, all validated **before** anything
is downloaded or switched:

They are validated **before** anything is downloaded or switched:
- **Kubernetes / Deckhouse version** constraints (semver). A cluster whose
Deckhouse version is not a release semver - e.g. a `dev` build - skips the
Deckhouse check with a warning instead of failing.
- **Modules**: required-enabled (optionally with a version constraint),
conditional (checked only if the module is enabled), any-of (at least one of
a group must be enabled), and forbidden (a module that must *not* be enabled).
- **Other plugins**: mandatory dependencies, and conditional ones that are
enforced only if that plugin is already installed.

```console
$ d8 plugins install package
...
Error: plugin requirements not satisfied # e.g. requires plugin delivery-kit
```

Plugins this one depends on are installed/upgraded automatically.
Mandatory plugin dependencies are installed and upgraded automatically during
`install` / `update`. Cluster-side requirements (Kubernetes / Deckhouse /
modules) are only *verified* - d8 never changes the cluster for you.

- `--skip-cluster-checks` (or `D8_PLUGINS_SKIP_CLUSTER_CHECKS=1`) - skip
cluster-side checks, e.g. in air-gapped scenarios.
- `--skip-cluster-checks` (or `D8_PLUGINS_SKIP_CLUSTER_CHECKS=1`) downgrades the
cluster-side checks to a warning - useful when the cluster is unreachable or
air-gapped. Plugin-to-plugin requirements are still enforced.

## Flags and environment variables

Expand All @@ -107,11 +121,41 @@ Plugins this one depends on are installed/upgraded automatically.
| `--rpp-endpoint` | `D8_RPP_ENDPOINT` | proxy base URL; discovered from the cluster when empty |
| `--rpp-ca-file` | `D8_RPP_CA_FILE` | PEM CA bundle to verify the proxy TLS certificate |
| `--rpp-insecure-skip-tls-verify` | - | skip proxy TLS verification (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 |
| `--force` *(install only)* | - | reinstall even if already current (re-pull and re-verify) |

The persistent flags above are shared by every `d8 plugins` subcommand; the
`--source*` family is hidden - see [Advanced](#advanced-hidden-flags).

## Troubleshooting

| Symptom | Cause | Fix |
|---|---|---|
| `image or tag not found` (404) on a plugin | the plugin is not published in this cluster's registry | check with `d8 plugins versions <name>`; publishing is the plugin CI's job |
| `plugin requirements not satisfied` | the contract requires other plugins or cluster versions/modules | see `d8 plugins contract <name>`; plugin deps are auto-installed, cluster requirements are not |
| 401 / 403 / `x509: ...` reaching the proxy | access or TLS issue with the registry-packages-proxy | see [self-update.md - Troubleshooting](self-update.md#troubleshooting) - the access model is shared |
| `image or tag not found` (404) | that plugin - or that specific version - is not published in this cluster's registry | check with `d8 plugins versions <name>`; publishing is the plugin CI's job |
| `... unauthorized (401)` | no accepted Bearer token (a client-certificate kubeconfig is not enough) | use an OIDC-token kubeconfig (Kubeconfig Generator or `d8 login`) |
| `... forbidden (403)` | your identity may not download plugins | ask an admin to bind the ClusterRole `d8:registry-packages-proxy:packages-download`; authorization is cached ~5 min, so retry with a fresh token |
| `... requirements not satisfied` | mandatory **plugin** dependencies are missing or version-incompatible | run `d8 plugins contract <name>`; on `install` deps auto-install, but at plugin *run* time install them manually as the hint says (`d8 plugins install <dep>`) |
| `... requires Kubernetes/Deckhouse/module ...` | a **cluster-side** requirement is unmet (a different message from the row above) | upgrade the cluster/module, or pass `--skip-cluster-checks` to bypass verification |
| `... upstream error (5xx)` | the proxy could not reach the backing registry | retry shortly, or check the `registry-packages-proxy` pods in `d8-cloud-instance-manager` |
| `endpoint discovery ... failed`, `x509:` to the API server | endpoint discovery goes through your kubeconfig's **API server** (not the proxy), which was unreachable or had an invalid certificate | confirm the API server is reachable with a valid cert, or skip discovery with `--rpp-endpoint https://registry-packages-proxy.<domain>` (`D8_RPP_ENDPOINT`) |
| `cannot reach the cluster to ...` | the cluster is needed to verify requirements or select a version, but is unreachable | pass `--skip-cluster-checks` (`D8_PLUGINS_SKIP_CLUSTER_CHECKS=1`) |

The access model is shared with d8 self-update; see
[self-update.md - Troubleshooting](self-update.md#troubleshooting) for the
registry-packages-proxy side.

## Advanced (hidden flags)

These flags are hidden from `--help` and exist as a temporary escape hatch;
prefer the proxy flow above.

- `--source <registry-repo>` pulls plugins **directly from a registry
repository, bypassing the cluster and the proxy**. It automatically enables
`--skip-cluster-checks`, so cluster-side requirements are not verified.
Credentials come from `--source-login` / `--source-password`, or `--license`
(a shortcut for `--source-login=license-token`), or your
`~/.docker/config.json` - in that order. `--tls-skip-verify` and `--insecure`
relax TLS / allow HTTP for that registry.
- `--rpp-insecure-skip-tls-verify` skips registry-packages-proxy TLS
verification (debugging only).
14 changes: 8 additions & 6 deletions internal/mirror/PROXY-REGISTRY.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ The "(probe)" steps are the only network behaviour that changes — they're the

The walk never invents tags: only versions that the registry confirmed are written to the bundle, and the latest-patch-per-minor / inclusive-anchor rules described in [README.MD: Platform Version Filtering](./README.MD#platform-version-filtering) and [README.MD: Module Filtering](./README.MD#module-filtering) are applied to the result the same way they would be after a normal `ListTags`.

For **modules** whose version constraint is an OR of several ranges (e.g. `>=1 <2 || >=3 <4`), the walk above runs once per sub-range independently. Platform constraints are always a single range, so this applies to modules only.

---

## Worked example
Expand Down Expand Up @@ -143,10 +145,10 @@ The platform probe issues the following HEAD requests in order. The right column
| 10 | `release-channel:v1.67.0` | 404 | new-minor probe failed; jump to next major (2.0.0) |
| 11 | (skipped) `v2.0.0` is outside `<=1.68.0` | n/a | constraint excludes 2.0.0; **probe terminates** |

After the probe finishes, the downstream pipeline keeps only the highest patch per `(major, minor)` (so `v1.64.0` and `v1.64.1` are dropped because `v1.64.2` is newer in the same minor). The final platform set written to `platform.tar` is therefore:
After the probe finishes, the downstream pipeline keeps only the highest patch per `(major, minor)`, so `v1.64.1` is dropped in favour of `v1.64.2`. However, the lower bound `>=1.64.0` is an **inclusive anchor**: because the probe confirmed `v1.64.0` exists (step 1), it is preserved even though `v1.64.2` is a newer patch in the same minor — see the inclusive-anchor rule in [README.MD: Platform Version Filtering](./README.MD#platform-version-filtering). The final platform set written to `platform.tar` is therefore:

```
v1.64.2, v1.65.0, v1.66.1
v1.64.0, v1.64.2, v1.65.0, v1.66.1
```

…plus any version pinned by an existing release channel snapshot (alpha/beta/etc.) that also satisfies the constraint.
Expand All @@ -157,10 +159,10 @@ Note that `v1.67.x` and `v1.68.x` would have been pulled too if the proxy regist

## What "exists" and "not found" mean on the wire

The probe relies on the standard registry-v2 manifest endpoint:
The probe uses the standard registry-v2 manifest endpoint. It issues a `HEAD` first and, if the registry answers the `HEAD` with anything other than a clean success or a `404`, it retries the same tag with a `GET` before deciding:

```
HEAD /v2/<repo>/manifests/<tag>
HEAD /v2/<repo>/manifests/<tag> # retried as GET /v2/<repo>/manifests/<tag> on a non-404 error
```

The mapping from HTTP response to probe action is:
Expand All @@ -172,7 +174,7 @@ The mapping from HTTP response to probe action is:
| `401 Unauthorized`, `403 Forbidden` | Auth failure | Abort the entire pull with the error |
| `5xx`, network error, timeout | Real failure | Abort the entire pull with the error |

In other words: only an unambiguous "the registry does not have this tag" stops the probe — everything else is propagated so a transient network blip never gets silently mistaken for "release series ended". This is the same error policy used by `CheckImageExists` in the rest of the pull pipeline.
In other words: only an unambiguous "the registry does not have this tag" stops the probe — everything else is propagated so a transient network blip never gets silently mistaken for "release series ended". This is the same error policy used by `CheckImageExists` in the rest of the pull pipeline. Because a `HEAD` that fails with a non-404 error is retried as a `GET`, a proxy that refuses `HEAD` requests but serves `GET` still works.

If a proxy registry returns `200 OK` for tags it later refuses to serve the manifest of, the per-tag GET in the normal pull step (step 3d / 6d of the flow) will surface a clear error against that exact tag.

Expand Down Expand Up @@ -205,7 +207,7 @@ The reason this works: every operation other than the three `ListTags` calls is
| `--no-platform` is set | `--include-platform` is **not** required |
| modules are being pulled (default) or `--only-extra-images` | At least one `--include-module <name>@<constraint>`. Every entry **must** include `@<constraint>` — `--include-module foo` alone is rejected because the probe would otherwise start at `v0.0.0` and silently miss every real tag |
| `--no-modules` is set | `--include-module` is **not** required |
| `--exclude-module` | Honoured (subtracts from the include list) |
| `--exclude-module` | Cannot be used with the required `--include-module` (the two are mutually exclusive). Accepted only together with `--no-modules`, where it has no effect |
| `--deckhouse-tag` | **Conflict**: a single pinned tag is already a direct check; do not combine with `--proxy-registry` |
| `--since-version` | **Conflict**: `--since-version` has no upper bound and the probe cannot terminate. Use `--include-platform` with an explicit range instead |
| `--dry-run` | Honoured — runs the probe and prints the plan without downloading any blobs |
Expand Down
Loading
Loading