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
276 changes: 276 additions & 0 deletions internal/mirror/PROXY-REGISTRY.md

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions internal/mirror/README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ d8 mirror pull <images-bundle-path> [flags]
| `--force` | | Overwrite existing bundle packages if they conflict with current pull operation |
| `--no-pull-resume` | | Do not continue last unfinished pull operation; start from scratch |

#### Proxy/Cache Registry Discovery

| Flag | Description |
|------|-------------|
| `--proxy-registry` | Pull from a proxy/caching registry that does not implement the registry catalog API. Discovers tags by probing individual semver versions instead of listing. Requires `--include-platform` and/or `--include-module`. See [Proxy Registry Mode](#proxy-registry-mode) for a summary and [PROXY-REGISTRY.md](./PROXY-REGISTRY.md) for the full reference. Cannot be combined with `--deckhouse-tag` or `--since-version` |

#### Connection Options

| Flag | Description |
Expand Down Expand Up @@ -139,6 +145,27 @@ d8 mirror pull /tmp/d8-bundle \

---

### Proxy Registry Mode

`--proxy-registry` adapts the pull command to source registries that act as a transparent proxy or cache in front of another registry and do **not** implement the registry catalog API (`/v2/_catalog`, `/v2/<name>/tags/list`). Instead of calling `ListTags`, the pull walks individual semver tags forward from a starting point you supply via `--include-platform` / `--include-module` and probes each one with a single manifest `HEAD` request.

Minimal example:

```bash
d8 mirror pull /tmp/d8-bundle \
--source proxy.internal.company.com/deckhouse/ee \
--license $LICENSE_TOKEN \
--proxy-registry \
--include-platform ">=1.64.0 <=1.68.0" \
--include-module prometheus@^1.0.0
```

Full documentation — end-to-end pipeline diagram, walk algorithm, worked example with a step-by-step HEAD-request trace, HTTP response handling, per-component behaviour table, required flag combinations, performance characteristics, known limitations and more examples — lives in a dedicated document:

**→ [PROXY-REGISTRY.md](./PROXY-REGISTRY.md)**

---

### Module Filtering

The `--include-module` and `--exclude-module` flags support version constraints for fine-grained control over which module versions to include.
Expand Down Expand Up @@ -494,6 +521,8 @@ By default, the platform service discovers releases between the current `rock-so

`--include-platform` replaces this window with a user-supplied semver constraint, applying the same latest-patch-per-minor and inclusive-anchor rules as module version filtering. Channel snapshots outside the constraint are pruned so the bundle stays internally consistent.

`--proxy-registry` replaces the catalog-based `ListTags` call entirely with a sequential forward-probe walk seeded from the constraint's lower bound. The latest-patch-per-minor and inclusive-anchor rules still apply to the result; only the way the candidate tag list is obtained changes. Full reference: [PROXY-REGISTRY.md](./PROXY-REGISTRY.md).

### Module Filtering Logic

- **Whitelist Mode:** Only specified modules are included
Expand Down
22 changes: 22 additions & 0 deletions internal/mirror/cmd/pull/flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ var (
OnlyExtraImages bool
SkipVexImages bool

// ProxyRegistry switches platform/module release discovery from the
// catalog-based ListTags path to a sequential probe of explicit
// version tags. It exists for proxy/caching registries that do NOT
// implement the registry catalog API but DO serve manifests for tags
// they have cached. Requires --include-platform and/or --include-module
// so the probe has a defined entry point — without those flags the
// probe would have to start from 0.0.0 and the bundle would always
// come back empty.
ProxyRegistry bool

DryRun bool

MirrorTimeout time.Duration = -1
Expand Down Expand Up @@ -257,6 +267,18 @@ module-name@=v1.3.0+stable → exact tag match: include only v1.3.0 and and publ
false,
"Do not pull VEX images.",
)
flagSet.BoolVar(
&ProxyRegistry,
"proxy-registry",
false,
`Pull from a proxy/caching registry that does not implement the registry catalog API.

Instead of calling the registry's "list tags" endpoint (which proxy registries typically return empty), this mode probes individual tags by incrementing patch -> minor -> major from the version explicitly named via --include-platform / --include-module. The probe stops once both a new patch and a new minor of the current major fail to resolve, then attempts the next major; if that also fails the probe terminates and downloads what was discovered.

Requires --include-platform when platform is not skipped via --no-platform, and at least one --include-module when modules are not skipped via --no-modules. --exclude-module and --no-platform are respected.

Cannot be combined with --deckhouse-tag or --since-version (use --include-platform's lower bound instead).`,
)
flagSet.BoolVar(
&DryRun,
"dry-run",
Expand Down
3 changes: 3 additions & 0 deletions internal/mirror/cmd/pull/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ func NewCommand() *cobra.Command {
pullCmd.MarkFlagsMutuallyExclusive("include-module", "exclude-module")
pullCmd.MarkFlagsMutuallyExclusive("include-platform", "deckhouse-tag")
pullCmd.MarkFlagsMutuallyExclusive("include-platform", "since-version")
pullCmd.MarkFlagsMutuallyExclusive("proxy-registry", "deckhouse-tag")
pullCmd.MarkFlagsMutuallyExclusive("proxy-registry", "since-version")
pullflags.ParseEnvironmentVariables()

return pullCmd
Expand Down Expand Up @@ -297,6 +299,7 @@ func (p *Puller) Execute(ctx context.Context) error {
BundleChunkSize: pullflags.ImagesBundleChunkSizeGB * 1000 * 1000 * 1000,
Timeout: pullflags.MirrorTimeout,
DryRun: pullflags.DryRun,
ProxyRegistry: pullflags.ProxyRegistry,
},
logger.Named("pull"),
p.logger,
Expand Down
65 changes: 65 additions & 0 deletions internal/mirror/cmd/pull/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"net/url"
"os"
"path/filepath"
"strings"

"github.com/Masterminds/semver/v3"
"github.com/google/go-containerregistry/pkg/name"
Expand All @@ -39,6 +40,9 @@ func parseAndValidateParameters(_ *cobra.Command, args []string) error {
if err = parseAndValidateVersionFlags(); err != nil {
return err
}
if err = validateProxyRegistryFlag(); err != nil {
return err
}
if err = validateImagesBundlePathArg(args); err != nil {
return err
}
Expand Down Expand Up @@ -143,6 +147,67 @@ func parseAndValidateVersionFlags() error {
return nil
}

// validateProxyRegistryFlag enforces the combinations the proxy-registry
// probe needs to work: each component that is actually being pulled (i.e.
// not switched off via --no-platform / --no-modules / --only-extra-images)
// must come with an explicit lower bound, because the probe cannot rely
// on the registry's tag catalog and has to be told where to start
// incrementing from.
//
// Notes:
// - --deckhouse-tag / --since-version conflicts: --deckhouse-tag asks
// for one tag (a tag-existence check is enough, no probe needed) and
// --since-version asks for "everything from X upward without an upper
// bound", which a probe cannot terminate safely. Both should use the
// non-proxy path instead.
// - --exclude-module is allowed but only meaningful when at least one
// --include-module remains, because the probe still needs concrete
// module names to walk.
func validateProxyRegistryFlag() error {
if !pullflags.ProxyRegistry {
return nil
}

if pullflags.DeckhouseTag != "" {
return errors.New("--proxy-registry cannot be combined with --deckhouse-tag: pulling a single tag does not need a list-based discovery and uses the direct check-tag-exists path already")
}
if pullflags.SinceVersionString != "" {
return errors.New("--proxy-registry cannot be combined with --since-version: --since-version has no upper bound, so the probe cannot terminate. Use --include-platform with an explicit lower bound (and optional upper bound) instead")
}

needPlatform := !pullflags.NoPlatform
needModules := !pullflags.NoModules || pullflags.OnlyExtraImages

// At least one component must actually be pulled — otherwise the
// flag is a no-op against a registry that probably already failed
// to satisfy the user.
if !needPlatform && !needModules {
return errors.New("--proxy-registry has nothing to do: both --no-platform and --no-modules are set")
}

if needPlatform && pullflags.PlatformConstraintString == "" {
return errors.New("--proxy-registry requires --include-platform (or --no-platform to skip platform mirroring): the probe needs an explicit lower bound to start incrementing from")
}
if needModules {
if len(pullflags.ModulesWhitelist) == 0 {
return errors.New("--proxy-registry requires --include-module (or --no-modules to skip module mirroring): the probe needs explicit module names and version anchors to start incrementing from")
}
// Every --include-module entry must come with an explicit
// version part. The implicit ">=0.0.0" fallback used by the
// regular pull mode is poisonous for the probe: it starts at
// v0.0.0 and stops on the first not-found, silently skipping
// any module whose lowest tag is above v0.0.0 / v0.1.0 /
// v1.0.0. Bail out loudly so the user picks a real anchor.
for _, entry := range pullflags.ModulesWhitelist {
if !strings.Contains(entry, "@") {
return fmt.Errorf("--proxy-registry requires every --include-module entry to specify an explicit version constraint (e.g. %q@^1.0.0); without it the probe would start at v0.0.0 and miss everything", strings.TrimSpace(entry))
}
}
}

return nil
}

func validateChunkSizeFlag() error {
if pullflags.ImagesBundleChunkSizeGB < 0 {
return errors.New("Chunk size cannot be less than zero GB")
Expand Down
159 changes: 159 additions & 0 deletions internal/mirror/cmd/pull/validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,165 @@ func TestValidationParseAndValidateVersionFlags(t *testing.T) {
}
}

func TestValidationValidateProxyRegistryFlag(t *testing.T) {
tests := []struct {
name string
proxyRegistry bool
platformConstraintString string
deckhouseTag string
sinceVersionString string
modulesWhitelist []string
noPlatform bool
noModules bool
onlyExtraImages bool
expectError bool
errorMsg string
}{
{
name: "flag off — every other combination is a no-op",
proxyRegistry: false,
expectError: false,
},
{
name: "happy path: platform constraint + module whitelist with explicit version",
proxyRegistry: true,
platformConstraintString: ">=1.64.0 <=1.68.0",
modulesWhitelist: []string{"prometheus@^1.0.0"},
expectError: false,
},
{
name: "missing --include-platform when platform is on",
proxyRegistry: true,
platformConstraintString: "",
modulesWhitelist: []string{"prometheus@^1.0.0"},
expectError: true,
errorMsg: "requires --include-platform",
},
{
name: "missing --include-module when modules are on",
proxyRegistry: true,
platformConstraintString: "^1.64.0",
modulesWhitelist: nil,
expectError: true,
errorMsg: "requires --include-module",
},
{
name: "--include-module without explicit version is rejected",
proxyRegistry: true,
platformConstraintString: "^1.64.0",
modulesWhitelist: []string{"prometheus"},
expectError: true,
errorMsg: "explicit version constraint",
},
{
name: "--no-platform skips the include-platform requirement",
proxyRegistry: true,
noPlatform: true,
modulesWhitelist: []string{"prometheus@^1.0.0"},
expectError: false,
},
{
name: "--no-modules skips the include-module requirement",
proxyRegistry: true,
platformConstraintString: "^1.64.0",
noModules: true,
modulesWhitelist: nil,
expectError: false,
},
{
name: "--no-platform and --no-modules together is a no-op",
proxyRegistry: true,
noPlatform: true,
noModules: true,
expectError: true,
errorMsg: "nothing to do",
},
{
name: "--only-extra-images still requires --include-module even with --no-modules",
proxyRegistry: true,
platformConstraintString: "^1.64.0",
noModules: true,
onlyExtraImages: true,
modulesWhitelist: nil,
expectError: true,
errorMsg: "requires --include-module",
},
{
name: "conflicts with --deckhouse-tag",
proxyRegistry: true,
platformConstraintString: "^1.64.0",
modulesWhitelist: []string{"prometheus@^1.0.0"},
deckhouseTag: "v1.65.0",
expectError: true,
errorMsg: "deckhouse-tag",
},
{
name: "conflicts with --since-version",
proxyRegistry: true,
platformConstraintString: "^1.64.0",
modulesWhitelist: []string{"prometheus@^1.0.0"},
sinceVersionString: "1.50.0",
expectError: true,
errorMsg: "since-version",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Snapshot global state — these flags are package-level.
originals := struct {
proxyRegistry bool
platformConstraintString string
deckhouseTag string
sinceVersionString string
modulesWhitelist []string
noPlatform bool
noModules bool
onlyExtraImages bool
}{
proxyRegistry: pullflags.ProxyRegistry,
platformConstraintString: pullflags.PlatformConstraintString,
deckhouseTag: pullflags.DeckhouseTag,
sinceVersionString: pullflags.SinceVersionString,
modulesWhitelist: pullflags.ModulesWhitelist,
noPlatform: pullflags.NoPlatform,
noModules: pullflags.NoModules,
onlyExtraImages: pullflags.OnlyExtraImages,
}
defer func() {
pullflags.ProxyRegistry = originals.proxyRegistry
pullflags.PlatformConstraintString = originals.platformConstraintString
pullflags.DeckhouseTag = originals.deckhouseTag
pullflags.SinceVersionString = originals.sinceVersionString
pullflags.ModulesWhitelist = originals.modulesWhitelist
pullflags.NoPlatform = originals.noPlatform
pullflags.NoModules = originals.noModules
pullflags.OnlyExtraImages = originals.onlyExtraImages
}()

pullflags.ProxyRegistry = tt.proxyRegistry
pullflags.PlatformConstraintString = tt.platformConstraintString
pullflags.DeckhouseTag = tt.deckhouseTag
pullflags.SinceVersionString = tt.sinceVersionString
pullflags.ModulesWhitelist = tt.modulesWhitelist
pullflags.NoPlatform = tt.noPlatform
pullflags.NoModules = tt.noModules
pullflags.OnlyExtraImages = tt.onlyExtraImages

err := validateProxyRegistryFlag()

if tt.expectError {
assert.Error(t, err)
if tt.errorMsg != "" {
assert.Contains(t, err.Error(), tt.errorMsg)
}
} else {
assert.NoError(t, err)
}
})
}
}

func TestValidationValidateChunkSizeFlag(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading
Loading