From 73182055562715a14656f5bbbb1208593bf668e6 Mon Sep 17 00:00:00 2001 From: Roman Berezkin Date: Tue, 13 Oct 2026 19:56:10 +0300 Subject: [PATCH] [mirror] Backport --since-version fix to 0.30 Signed-off-by: Roman Berezkin --- docs/mirror-pull-dry-run.md | 9 +- internal/mirror/PROXY-REGISTRY.md | 2 +- internal/mirror/README.MD | 8 +- internal/mirror/cmd/pull/pull.go | 1 + internal/mirror/cmd/pull/pull_dryrun_test.go | 22 +++ .../cmd/pull/pull_since_version_test.go | 155 ++++++++++++++++++ internal/mirror/cmd/pull/validation.go | 7 +- internal/mirror/platform/platform.go | 8 +- internal/mirror/platform/platform_test.go | 12 +- .../mirror/platform/pull_platform_test.go | 141 ++++++++++------ .../mirror/platform/validate_access_test.go | 7 +- internal/mirror/pull.go | 5 + pkg/fake/deckhouse_stub.go | 13 +- pkg/fake/deckhouse_stub_test.go | 4 +- 14 files changed, 316 insertions(+), 78 deletions(-) create mode 100644 internal/mirror/cmd/pull/pull_since_version_test.go diff --git a/docs/mirror-pull-dry-run.md b/docs/mirror-pull-dry-run.md index 724d2095a..19300d071 100644 --- a/docs/mirror-pull-dry-run.md +++ b/docs/mirror-pull-dry-run.md @@ -77,15 +77,17 @@ d8 mirror pull --dry-run /tmp/bundle \ This resolves platform images, installer, security databases, and all modules. No blobs are downloaded; the bundle directory stays empty. -### Since a minimum version (channel-based pull) +### Include releases older than Rock Solid ```bash d8 mirror pull --dry-run /tmp/bundle \ --source registry.deckhouse.io/deckhouse/fe \ --license \ - --since-version 1.68.0 + --since-version 1.67.0 ``` +Default discovery spans current Rock Solid through Alpha, inclusively. `--since-version` extends this range only when the requested version is older than Rock Solid. The lower bound is `MIN(rock-solid, since-version)`; equal or newer values leave the default versions and channels unchanged. Numeric tags retain the latest patch per major.minor together with channel snapshots in the range, so the exact patch named by `--since-version` is not pinned. + ### Module whitelist ```bash @@ -119,8 +121,7 @@ access, no credentials needed. The stub registry is activated via: STUB_REGISTRY_CLIENT=true ``` -The stub seeds versions `v1.68.0`–`v1.72.10`, channels `alpha`/`beta`/`early-access`/ -`stable`/`rock-solid`, and several module tags. +The stub seeds versions `v1.66.0` through `v1.72.10` and channels `alpha`/`beta`/`early-access`/`stable`/`rock-solid`. Rock Solid points to `v1.68.0`, so the default selection excludes `v1.66.0` and `v1.67.0`. With `--since-version 1.67.0`, the plan adds `v1.67.0` but still excludes `v1.66.0`. #### Run all dry-run unit tests diff --git a/internal/mirror/PROXY-REGISTRY.md b/internal/mirror/PROXY-REGISTRY.md index 31da955ca..39943b125 100644 --- a/internal/mirror/PROXY-REGISTRY.md +++ b/internal/mirror/PROXY-REGISTRY.md @@ -207,7 +207,7 @@ The reason this works: every operation other than the three `ListTags` calls is | `--no-modules` is set | `--include-module` is **not** required | | `--exclude-module` | Honoured (subtracts from the include list) | | `--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 | +| `--since-version` | **Conflict**: requires channel-based discovery with registry tag listings. Use `--include-platform` with an explicit lower bound instead | | `--dry-run` | Honoured — runs the probe and prints the plan without downloading any blobs | --- diff --git a/internal/mirror/README.MD b/internal/mirror/README.MD index 3498ecfd7..5ffc59ef5 100644 --- a/internal/mirror/README.MD +++ b/internal/mirror/README.MD @@ -52,7 +52,7 @@ d8 mirror pull [flags] | Flag | Description | |------|-------------| -| `--since-version` | Minimal Deckhouse release to pull (lower bound, inclusive). Ignored if above current Rock Solid release. Conflicts with `--deckhouse-tag` and `--include-platform` | +| `--since-version` | Extend discovery to older Deckhouse releases (inclusive). The lower bound is `MIN(rock-solid, since-version)`; equal or newer values keep the default range. Conflicts with `--deckhouse-tag` and `--include-platform` | | `--include-platform` | Select platform releases by semver constraint (e.g. `">=1.64 <=1.68"`). Uses the same constraint dialect as `--include-module`. Conflicts with `--deckhouse-tag` and `--since-version`. See [Platform Version Filtering](#platform-version-filtering) | | `--deckhouse-tag` | Specific Deckhouse build tag to pull. Conflicts with `--since-version` and `--include-platform`. If the registry contains a release channel image for the specified tag, all release channels in the bundle will point to it | @@ -106,7 +106,7 @@ The `--include-platform` flag accepts a semver constraint expression to pull onl | Goal | Recommended flag | |------|-----------------| -| Pull all releases from a given minimum upward | `--since-version 1.64.0` | +| Extend the default range to older releases | `--since-version 1.64.0` | | Pull a bounded range (e.g. for an incremental update) | `--include-platform ">=1.64 <=1.68"` | | Pull every release of a single minor | `--include-platform "~1.65.0"` | | Pin a single exact release (same as `--deckhouse-tag`) | `--include-platform "=v1.65.3"` | @@ -494,7 +494,7 @@ d8 mirror pull /tmp/bundle --license $LICENSE_TOKEN ### Version Management -1. **Use `--since-version`:** To pull all releases from a given minimum upward, reducing bundle size relative to a full pull +1. **Use `--since-version`:** To add releases older than the current Rock Solid release. Equal or newer values leave the default range unchanged 2. **Use `--include-platform`:** To pull a bounded window of releases (e.g. `">=1.64 <=1.68"`) — useful for staged incremental updates where you want to move up a few minors at a time without pulling the entire channel history 3. **Specific Versions:** Use `--deckhouse-tag` (or `--include-platform "=vX.Y.Z"`) for fully controlled, single-release deployments 4. **Module Versioning:** Leverage semver constraints in `--include-module` for flexible module version management; `--include-platform` speaks the same dialect @@ -523,7 +523,7 @@ The mirror system handles three primary component types: By default, the platform service discovers releases between the current `rock-solid` channel version (lower bound) and the current `alpha` channel version (upper bound), keeping the highest patch per `(major, minor)`. -`--since-version` raises the lower bound above `rock-solid` when the user wants to skip older minors. +`--since-version` extends the range to older releases using the inclusive lower bound `MIN(rock-solid, since-version)`. Values equal to or above Rock Solid leave the default versions and channels unchanged. Numeric tags keep the latest patch per major.minor together with channel snapshots in the range, so the exact patch named by `--since-version` is not pinned. The upper bound remains the current Alpha release. `--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. diff --git a/internal/mirror/cmd/pull/pull.go b/internal/mirror/cmd/pull/pull.go index 332313fee..7e1721219 100644 --- a/internal/mirror/cmd/pull/pull.go +++ b/internal/mirror/cmd/pull/pull.go @@ -388,6 +388,7 @@ func (p *Puller) buildPullService() (*mirror.PullService, error) { pullflags.TempDir, pullflags.DeckhouseTag, &mirror.PullServiceOptions{ + SinceVersion: p.params.SinceVersion, SkipPlatform: pullflags.NoPlatform, SkipSecurity: pullflags.NoSecurityDB, SkipModules: pullflags.NoModules, diff --git a/internal/mirror/cmd/pull/pull_dryrun_test.go b/internal/mirror/cmd/pull/pull_dryrun_test.go index 19fda85dd..525dac9d2 100644 --- a/internal/mirror/cmd/pull/pull_dryrun_test.go +++ b/internal/mirror/cmd/pull/pull_dryrun_test.go @@ -22,10 +22,12 @@ import ( "path/filepath" "testing" + "github.com/Masterminds/semver/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" pullflags "github.com/deckhouse/deckhouse-cli/internal/mirror/cmd/pull/flags" + "github.com/deckhouse/deckhouse-cli/internal/mirror/modules" ) // saveFlagsAndRestore captures the current state of all relevant pull flags and @@ -34,6 +36,13 @@ func saveFlagsAndRestore(t *testing.T) func() { t.Helper() saved := struct { + NoPackages bool + SinceVersionString string + SinceVersion *semver.Version + PlatformConstraintString string + PlatformConstraint modules.VersionConstraint + ProxyRegistry bool + TempDir string ImagesBundlePath string SourceRegistryRepo string @@ -56,6 +65,13 @@ func saveFlagsAndRestore(t *testing.T) func() { IgnoreSuspend bool ImagesBundleChunkSizeGB int64 }{ + NoPackages: pullflags.NoPackages, + SinceVersionString: pullflags.SinceVersionString, + SinceVersion: pullflags.SinceVersion, + PlatformConstraintString: pullflags.PlatformConstraintString, + PlatformConstraint: pullflags.PlatformConstraint, + ProxyRegistry: pullflags.ProxyRegistry, + TempDir: pullflags.TempDir, ImagesBundlePath: pullflags.ImagesBundlePath, SourceRegistryRepo: pullflags.SourceRegistryRepo, @@ -80,6 +96,12 @@ func saveFlagsAndRestore(t *testing.T) func() { } return func() { + pullflags.NoPackages = saved.NoPackages + pullflags.SinceVersionString = saved.SinceVersionString + pullflags.SinceVersion = saved.SinceVersion + pullflags.PlatformConstraintString = saved.PlatformConstraintString + pullflags.PlatformConstraint = saved.PlatformConstraint + pullflags.ProxyRegistry = saved.ProxyRegistry pullflags.TempDir = saved.TempDir pullflags.ImagesBundlePath = saved.ImagesBundlePath pullflags.SourceRegistryRepo = saved.SourceRegistryRepo diff --git a/internal/mirror/cmd/pull/pull_since_version_test.go b/internal/mirror/cmd/pull/pull_since_version_test.go new file mode 100644 index 000000000..eaf62830f --- /dev/null +++ b/internal/mirror/cmd/pull/pull_since_version_test.go @@ -0,0 +1,155 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package pull + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/fatih/color" + "github.com/stretchr/testify/require" + + pullflags "github.com/deckhouse/deckhouse-cli/internal/mirror/cmd/pull/flags" +) + +func TestPullCommandSinceVersion(t *testing.T) { + t.Setenv("STUB_REGISTRY_CLIENT", "true") + for _, key := range []string{ + "D8_MIRROR_LICENSE_TOKEN", "D8_MIRROR_SOURCE_LOGIN", "D8_MIRROR_SOURCE_PASSWORD", + "D8_MIRROR_TIMEOUT", "MIRROR_DEBUG_LOG", + } { + t.Setenv(key, "") + } + noColor := color.NoColor + color.NoColor = true + t.Cleanup(func() { color.NoColor = noColor }) + + const baseline = "v1.72.10, v1.71.0, v1.70.0, v1.69.0, v1.68.0 (5 channels)" + const withOlderRelease = "v1.72.10, v1.71.0, v1.70.0, v1.69.0, v1.68.0, v1.67.0 (5 channels)" + + tests := []struct { + name string + args []string + wantPlatform string + wantError string + }{ + { + name: "without since excludes releases below Rock Solid", + wantPlatform: baseline, + }, + { + name: "older since includes requested release but not earlier releases", + args: []string{"--since-version", "1.67.0"}, + wantPlatform: withOlderRelease, + }, + { + name: "since equal to Rock Solid preserves versions and channels", + args: []string{"--since-version", "1.68.0"}, + wantPlatform: baseline, + }, + { + name: "since newer than Rock Solid preserves versions and channels", + args: []string{"--since-version", "1.70.0"}, + wantPlatform: baseline, + }, + { + name: "since equal to Alpha preserves versions and channels", + args: []string{"--since-version", "1.72.10"}, + wantPlatform: baseline, + }, + { + name: "include platform selects older release", + args: []string{"--include-platform", ">=1.67.0"}, + wantPlatform: withOlderRelease, + }, + { + name: "deckhouse tag selects only the requested release", + args: []string{"--deckhouse-tag", "v1.67.0"}, + wantPlatform: "v1.67.0", + }, + { + name: "since conflicts with deckhouse tag", + args: []string{"--since-version", "1.67.0", "--deckhouse-tag", "v1.67.0"}, + wantError: "Using both --deckhouse-tag and --since-version", + }, + { + name: "since conflicts with include platform", + args: []string{"--since-version", "1.67.0", "--include-platform", ">=1.67.0"}, + wantError: "Using both --since-version and --include-platform", + }, + { + name: "since conflicts with proxy registry", + args: []string{"--since-version", "1.67.0", "--proxy-registry"}, + wantError: "--proxy-registry cannot be combined with --since-version", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + output, err := executeDryRunCommand(t, tt.args) + if tt.wantError != "" { + require.ErrorContains(t, err, tt.wantError) + return + } + require.NoError(t, err, output) + require.Contains(t, output, "Pull plan (dry-run)") + + var platformLines []string + for _, line := range strings.Split(output, "\n") { + if _, content, ok := strings.Cut(line, "Platform:"); ok { + platformLines = append(platformLines, strings.TrimSpace(content)) + } + } + require.Equal(t, []string{tt.wantPlatform}, platformLines, output) + }) + } +} + +func executeDryRunCommand(t *testing.T, extraArgs []string) (string, error) { + t.Helper() + defer saveFlagsAndRestore(t)() + pullflags.SinceVersion = nil + pullflags.PlatformConstraint = nil + + dir := t.TempDir() + output, err := os.Create(filepath.Join(dir, "output.log")) + require.NoError(t, err) + defer output.Close() + + stdout := os.Stdout + os.Stdout = output + defer func() { os.Stdout = stdout }() + + cmd := NewCommand() + cmd.SetOut(output) + cmd.SetErr(output) + cmd.SetArgs(append([]string{ + filepath.Join(dir, "bundle"), + "--source", "registry.deckhouse.ru/deckhouse/fe", + "--tmp-dir", filepath.Join(dir, "work"), + "--dry-run", "--no-modules", "--no-packages", "--no-installer", + "--no-security-db", "--skip-vex-images", "--gost-digest=false", + }, extraArgs...)) + commandErr := cmd.ExecuteContext(context.Background()) + + body, err := os.ReadFile(output.Name()) + require.NoError(t, err) + return string(body), commandErr +} diff --git a/internal/mirror/cmd/pull/validation.go b/internal/mirror/cmd/pull/validation.go index 5e3836bea..0f4a90246 100644 --- a/internal/mirror/cmd/pull/validation.go +++ b/internal/mirror/cmd/pull/validation.go @@ -168,9 +168,8 @@ func parseAndValidateVersionFlags() error { // 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. +// --since-version extends channel-based discovery using registry tag +// listings. Both use the non-proxy path. // - --exclude-module is allowed but only meaningful when at least one // --include-module remains, because the probe still needs concrete // module names to walk. @@ -184,7 +183,7 @@ func validateProxyRegistryFlag() error { } 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") + return errors.New("--proxy-registry cannot be combined with --since-version: use --include-platform with an explicit lower bound instead") } needPlatform := !pullflags.NoPlatform diff --git a/internal/mirror/platform/platform.go b/internal/mirror/platform/platform.go index 250b1dede..cc1c1ca61 100644 --- a/internal/mirror/platform/platform.go +++ b/internal/mirror/platform/platform.go @@ -48,7 +48,7 @@ import ( // Options contains configuration options for the platform service type Options struct { - // SinceVersion specifies the minimum version to start mirroring from (optional) + // SinceVersion extends release discovery below the current rock-solid release. SinceVersion *semver.Version // IncludeConstraint narrows platform release discovery to a user-supplied // semver constraint expression, mirroring the per-module constraint syntax @@ -782,12 +782,10 @@ func (svc *Service) determineMinimumVersion(channelVersions channelVersions) *se return nil } - // Use rock-solid as baseline minVersion := rockSolidVersion - // If SinceVersion is provided and is newer than rock-solid, start from SinceVersion - // (user wants to mirror from a later version than rock-solid) - if svc.options.SinceVersion != nil && svc.options.SinceVersion.GreaterThan(minVersion) { + // Older releases extend the range; newer releases keep the rock-solid baseline. + if svc.options.SinceVersion != nil && svc.options.SinceVersion.LessThan(minVersion) { minVersion = svc.options.SinceVersion } diff --git a/internal/mirror/platform/platform_test.go b/internal/mirror/platform/platform_test.go index 04bcb7589..9eaf42d7d 100644 --- a/internal/mirror/platform/platform_test.go +++ b/internal/mirror/platform/platform_test.go @@ -118,7 +118,7 @@ func TestService_versionsToMirror(t *testing.T) { wantErr: false, }, { - name: "no strict tags with SinceVersion", + name: "no strict tags with SinceVersion above Rock Solid keeps the default range", strictTags: []string{}, options: &Options{ SinceVersion: semver.MustParse("v1.69.0"), @@ -129,9 +129,10 @@ func TestService_versionsToMirror(t *testing.T) { "v1.71.0", // beta "v1.70.0", // early-access "v1.69.0", // stable + "v1.68.0", // rock-solid }, wantChannels: []string{ - "alpha", "beta", "early-access", "stable", + "alpha", "beta", "early-access", "stable", "rock-solid", }, wantCustomTags: []string{}, wantErr: false, @@ -312,3 +313,10 @@ func TestService_versionsToMirror_Deduplication(t *testing.T) { // No custom tags assert.Empty(t, result.CustomTags) } + +func TestService_determineMinimumVersion_WithoutRockSolid(t *testing.T) { + svc := &Service{options: &Options{SinceVersion: semver.MustParse("1.67.0")}} + versions := channelVersions{"alpha": semver.MustParse("1.72.10")} + + assert.Nil(t, svc.determineMinimumVersion(versions)) +} diff --git a/internal/mirror/platform/pull_platform_test.go b/internal/mirror/platform/pull_platform_test.go index c9d73a7dc..b7d192694 100644 --- a/internal/mirror/platform/pull_platform_test.go +++ b/internal/mirror/platform/pull_platform_test.go @@ -401,7 +401,7 @@ func TestPullPlatform_DryRun_FullDiscovery_AllVersionsAndChannels(t *testing.T) require.NoError(t, err) rootURL := stubRootURL - // Every semver tag from the stub must appear. + // Every version between Rock Solid and Alpha must appear. for _, ver := range []string{"v1.72.10", "v1.71.0", "v1.70.0", "v1.69.0", "v1.68.0"} { assert.Contains(t, svc.downloadList.Deckhouse, rootURL+":"+ver, "expected version %s in Deckhouse list", ver) @@ -427,39 +427,102 @@ func TestPullPlatform_DryRun_FullDiscovery_AllVersionsAndChannels(t *testing.T) assert.Len(t, svc.downloadList.DeckhouseInstallStandalone, 5) } -// ---- SinceVersion filter ---- +func TestPullPlatform_DryRun_SinceVersion(t *testing.T) { + for _, tt := range []struct { + name string + since string + wantOld bool + }{ + {name: "default range"}, + {name: "below Rock Solid extends range", since: "1.67.0", wantOld: true}, + {name: "equal to Rock Solid keeps range", since: "1.68.0"}, + {name: "above Rock Solid keeps range", since: "1.70.0"}, + {name: "equal to Alpha keeps range", since: "1.72.10"}, + } { + t.Run(tt.name, func(t *testing.T) { + logger := dkplog.NewLogger(dkplog.WithLevel(slog.LevelWarn)) + userLogger := log.NewSLogger(slog.LevelWarn) + options := &Options{} + if tt.since != "" { + options.SinceVersion = semver.MustParse(tt.since) + } + + svc := newDryRunService(localfake.NewRegistryClientStub(), options, logger, userLogger) + require.NoError(t, svc.PullPlatform(context.Background())) + + versions := []string{"v1.68.0", "v1.69.0", "v1.70.0", "v1.71.0", "v1.72.10"} + if tt.wantOld { + versions = append(versions, "v1.67.0") + } + assert.Len(t, svc.downloadList.Deckhouse, len(versions)) + assert.Len(t, svc.downloadList.DeckhouseInstall, len(versions)) + assert.Len(t, svc.downloadList.DeckhouseInstallStandalone, len(versions)) + for _, version := range versions { + assert.Contains(t, svc.downloadList.Deckhouse, stubRootURL+":"+version) + assert.Contains(t, svc.downloadList.DeckhouseInstall, stubRootURL+"/install:"+version) + assert.Contains(t, svc.downloadList.DeckhouseInstallStandalone, stubRootURL+"/install-standalone:"+version) + } + assert.NotContains(t, svc.downloadList.Deckhouse, stubRootURL+":v1.66.0") + if !tt.wantOld { + assert.NotContains(t, svc.downloadList.Deckhouse, stubRootURL+":v1.67.0") + } + for _, channel := range []string{"alpha", "beta", "early-access", "stable", "rock-solid"} { + assert.Contains(t, svc.downloadList.DeckhouseReleaseChannel, stubRootURL+"/release-channel:"+channel) + assert.NotContains(t, svc.downloadList.Deckhouse, stubRootURL+":"+channel) + } + }) + } +} + +func TestService_versionsToMirror_SinceVersion_KeepsLatestPatchesAndChannelSnapshots(t *testing.T) { + reg := upfake.NewRegistry(stubRootURL) + channels := map[string]string{ + "alpha": "v1.72.10", "beta": "v1.71.0", "early-access": "v1.70.0", + "stable": "v1.69.0", "rock-solid": "v1.68.0", + } + for channel, version := range channels { + img := upfake.NewImageBuilder().WithFile("version.json", fmt.Sprintf(`{"version":%q}`, version)).MustBuild() + reg.MustAddImage("release-channel", channel, img) + reg.MustAddImage("release-channel", version, img) + } + for _, version := range []string{"v1.66.0", "v1.67.0", "v1.67.4", "v1.68.4"} { + img := upfake.NewImageBuilder().WithFile("version.json", fmt.Sprintf(`{"version":%q}`, version)).MustBuild() + reg.MustAddImage("release-channel", version, img) + } -func TestPullPlatform_DryRun_SinceVersion_FiltersOlderChannels(t *testing.T) { - // SinceVersion=1.70.0 means stable(v1.69.0) and rock-solid(v1.68.0) are excluded. logger := dkplog.NewLogger(dkplog.WithLevel(slog.LevelWarn)) userLogger := log.NewSLogger(slog.LevelWarn) - - since := semver.MustParse("1.70.0") - svc := newDryRunService(localfake.NewRegistryClientStub(), &Options{SinceVersion: since}, logger, userLogger) - err := svc.PullPlatform(context.Background()) + svc := newDryRunService(pkgclient.Adapt(upfake.NewClient(reg)), &Options{SinceVersion: semver.MustParse("1.67.0")}, logger, userLogger) + result, err := svc.versionsToMirror(context.Background(), nil) require.NoError(t, err) - rootURL := stubRootURL - // Versions at or above 1.70.0 must be present. - for _, ver := range []string{"v1.72.10", "v1.71.0", "v1.70.0"} { - assert.Contains(t, svc.downloadList.Deckhouse, rootURL+":"+ver) + var versions []string + for _, version := range result.Versions { + versions = append(versions, "v"+version.String()) } - // Versions below 1.70.0 must be absent. - for _, ver := range []string{"v1.69.0", "v1.68.0"} { - assert.NotContains(t, svc.downloadList.Deckhouse, rootURL+":"+ver, -"version %s is below SinceVersion and must be excluded", ver) + assert.ElementsMatch(t, []string{"v1.67.4", "v1.68.0", "v1.68.4", "v1.69.0", "v1.70.0", "v1.71.0", "v1.72.10"}, versions) + assert.Len(t, result.ChannelVersions, len(channels)) + for channel, version := range channels { + assert.Contains(t, result.ChannelVersions, ChannelVersionTag{Channel: channel, Tag: version}) } - // Channel-aliased release channels below SinceVersion must be absent. - assert.NotContains(t, svc.downloadList.DeckhouseReleaseChannel, - rootURL+"/release-channel:stable") - assert.NotContains(t, svc.downloadList.DeckhouseReleaseChannel, - rootURL+"/release-channel:rock-solid") - // Channel-aliased release channels at or above SinceVersion must be present. - for _, ch := range []string{"alpha", "beta", "early-access"} { - assert.Contains(t, svc.downloadList.DeckhouseReleaseChannel, - rootURL+"/release-channel:"+ch) - assert.NotContains(t, svc.downloadList.Deckhouse, rootURL+":"+ch, -"Deckhouse repo must not carry channel alias %s", ch) +} + +func TestPullPlatform_DryRun_SinceVersion_LTSOnly(t *testing.T) { + for _, since := range []string{"", "1.67.0", "1.70.0"} { + t.Run("since="+since, func(t *testing.T) { + logger := dkplog.NewLogger(dkplog.WithLevel(slog.LevelWarn)) + userLogger := log.NewSLogger(slog.LevelWarn) + options := &Options{} + if since != "" { + options.SinceVersion = semver.MustParse(since) + } + + svc := newDryRunService(ltsOnlySourceStub("v1.68.0"), options, logger, userLogger) + require.NoError(t, svc.PullPlatform(context.Background())) + assert.Len(t, svc.downloadList.Deckhouse, 1) + assert.Contains(t, svc.downloadList.Deckhouse, stubRootURL+":v1.68.0") + assert.Contains(t, svc.downloadList.DeckhouseReleaseChannel, stubRootURL+"/release-channel:lts") + }) } } @@ -598,30 +661,6 @@ func TestPullPlatform_DryRun_IncludePlatform_ExactTagBehavesLikeTargetTag(t *tes "=v1.69.0 must propagate to stable because v1.69.0 is the stable channel version in the stub") } -func TestPullPlatform_DryRun_SinceVersion_EqualToAlpha_OnlyAlpha(t *testing.T) { - // SinceVersion=1.72.10 means only the alpha version (== newest) survives. - logger := dkplog.NewLogger(dkplog.WithLevel(slog.LevelWarn)) - userLogger := log.NewSLogger(slog.LevelWarn) - - since := semver.MustParse("1.72.10") - svc := newDryRunService(localfake.NewRegistryClientStub(), &Options{SinceVersion: since}, logger, userLogger) - err := svc.PullPlatform(context.Background()) - require.NoError(t, err) - - rootURL := stubRootURL - assert.Contains(t, svc.downloadList.Deckhouse, rootURL+":v1.72.10") - // alpha must be present in release-channel only. - assert.Contains(t, svc.downloadList.DeckhouseReleaseChannel, - rootURL+"/release-channel:alpha") - assert.NotContains(t, svc.downloadList.Deckhouse, rootURL+":alpha") - // All older channels must be gone everywhere. - for _, ch := range []string{"beta", "early-access", "stable", "rock-solid"} { - assert.NotContains(t, svc.downloadList.Deckhouse, rootURL+":"+ch) - assert.NotContains(t, svc.downloadList.DeckhouseReleaseChannel, - rootURL+"/release-channel:"+ch) - } -} - // ---- LTS channel fallback ---- func TestPullPlatform_DryRun_LTSRegistry_WithSemverTargetTag(t *testing.T) { diff --git a/internal/mirror/platform/validate_access_test.go b/internal/mirror/platform/validate_access_test.go index e1684cea0..524ea7b8a 100644 --- a/internal/mirror/platform/validate_access_test.go +++ b/internal/mirror/platform/validate_access_test.go @@ -232,17 +232,18 @@ func TestService_findTagsToMirror(t *testing.T) { wantErr: false, }, { - name: "empty TargetTag with SinceVersion filters old channels", + name: "empty TargetTag with SinceVersion above Rock Solid keeps all channels", options: &Options{ SinceVersion: mustParseSemver("1.70.0"), }, - // rock-solid (v1.68.0) and stable (v1.69.0) are below SinceVersion 1.70.0 wantVersions: []string{ "v1.72.10", "v1.71.0", "v1.70.0", + "v1.69.0", + "v1.68.0", }, - wantChannels: []string{"alpha", "beta", "early-access"}, + wantChannels: []string{"alpha", "beta", "early-access", "stable", "rock-solid"}, wantErr: false, }, } diff --git a/internal/mirror/pull.go b/internal/mirror/pull.go index db99ce8e5..00f9fc492 100644 --- a/internal/mirror/pull.go +++ b/internal/mirror/pull.go @@ -21,6 +21,8 @@ import ( "fmt" "time" + "github.com/Masterminds/semver/v3" + dkplog "github.com/deckhouse/deckhouse/pkg/log" "github.com/deckhouse/deckhouse-cli/internal/mirror/installer" @@ -34,6 +36,8 @@ import ( // PullServiceOptions contains configuration options for PullService type PullServiceOptions struct { + // SinceVersion extends release discovery below the current rock-solid release. + SinceVersion *semver.Version // SkipPlatform skips pulling platform images SkipPlatform bool // SkipSecurity skips pulling security databases @@ -117,6 +121,7 @@ func NewPullService( registryService, tmpDir, &platform.Options{ + SinceVersion: options.SinceVersion, TargetTag: targetTag, IncludeConstraint: options.PlatformConstraint, BundleDir: options.BundleDir, diff --git a/pkg/fake/deckhouse_stub.go b/pkg/fake/deckhouse_stub.go index ba0144203..74b326723 100644 --- a/pkg/fake/deckhouse_stub.go +++ b/pkg/fake/deckhouse_stub.go @@ -58,11 +58,12 @@ const imagesDigestsJSON = `{}` // ("registry.deckhouse.ru/deckhouse/fe") with the following structure: // // - root repository (empty path): tags alpha, beta, early-access, stable, -// rock-solid, v1.72.10, v1.71.0, v1.70.0, v1.69.0, v1.68.0, pr12345. +// rock-solid, v1.72.10, v1.71.0, v1.70.0, v1.69.0, v1.68.0, v1.67.0, +// v1.66.0, pr12345. // // - "release-channel" repository: tags alpha, beta, early-access, stable, -// rock-solid. Each image carries version.json with the channel's current -// version (e.g. alpha → v1.72.10). +// rock-solid, plus numeric version tags for each release. Each image +// carries version.json with its release version (e.g. alpha → v1.72.10). // // - "install" and "install-standalone" repositories: same tags as root. func NewRegistryClientStub() localreg.Client { @@ -76,6 +77,10 @@ func NewRegistryClientStub() localreg.Client { reg.MustAddImage("release-channel", version, img) } + for _, version := range []string{"v1.67.0", "v1.66.0"} { + reg.MustAddImage("release-channel", version, releaseChannelImage(version)) + } + // ---- root-level and installer repositories ---- rootTags := []struct { tag string @@ -91,6 +96,8 @@ func NewRegistryClientStub() localreg.Client { {"v1.70.0", "v1.70.0"}, {"v1.69.0", "v1.69.0"}, {"v1.68.0", "v1.68.0"}, + {"v1.67.0", "v1.67.0"}, + {"v1.66.0", "v1.66.0"}, {"pr12345", "v1.72.10"}, // custom non-semver tag } diff --git a/pkg/fake/deckhouse_stub_test.go b/pkg/fake/deckhouse_stub_test.go index 10f741ee8..054a336ff 100644 --- a/pkg/fake/deckhouse_stub_test.go +++ b/pkg/fake/deckhouse_stub_test.go @@ -30,7 +30,7 @@ import ( ) // TestNewRegistryClientStub_ReleaseChannelTags verifies that the Deckhouse -// stub exposes all five release channels and their version tags under the +// stub exposes all five release channels and the available version tags under the // "release-channel" repo. func TestNewRegistryClientStub_ReleaseChannelTags(t *testing.T) { client := fake.NewRegistryClientStub() @@ -42,6 +42,7 @@ func TestNewRegistryClientStub_ReleaseChannelTags(t *testing.T) { assert.ElementsMatch(t, []string{ "alpha", "beta", "early-access", "stable", "rock-solid", "v1.72.10", "v1.71.0", "v1.70.0", "v1.69.0", "v1.68.0", + "v1.67.0", "v1.66.0", }, tags) } @@ -109,6 +110,7 @@ func TestNewRegistryClientStub_RootTags(t *testing.T) { wantTags := []string{ "alpha", "beta", "early-access", "stable", "rock-solid", "v1.72.10", "v1.71.0", "v1.70.0", "v1.69.0", "v1.68.0", + "v1.67.0", "v1.66.0", "pr12345", } assert.ElementsMatch(t, wantTags, tags)