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
113 changes: 113 additions & 0 deletions internal/mirror/installer/installer_edition_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
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 installer

import (
"log/slog"
"strings"
"testing"

"github.com/stretchr/testify/assert"

dkplog "github.com/deckhouse/deckhouse/pkg/log"

"github.com/deckhouse/deckhouse-cli/internal"
"github.com/deckhouse/deckhouse-cli/pkg"
"github.com/deckhouse/deckhouse-cli/pkg/libmirror/util/log"
pkgclient "github.com/deckhouse/deckhouse-cli/pkg/registry/client"
registryservice "github.com/deckhouse/deckhouse-cli/pkg/registry/service"
)

// TestInstallerService_RootURL_StaysAtBareRoot_WithEdition is a regression test
// guarding the asymmetry in the registry service: while deckhouse, modules and
// security live UNDER the edition segment, the installer (and plugins) live
// OUTSIDE it (at <root>/installer). The fix that introduced GetEditionRoot()
// must NOT spill over into the installer wiring — installer.NewService must
// keep using registryService.GetRoot() (the bare root) for its downloadList.
//
// Without this regression test it would be easy to "uniformly" switch every
// service to GetEditionRoot() and silently break installer paths
// (<root>/<edition>/installer:tag does not exist).
func TestInstallerService_RootURL_StaysAtBareRoot_WithEdition(t *testing.T) {
logger := dkplog.NewLogger(dkplog.WithLevel(slog.LevelWarn))
userLogger := log.NewSLogger(slog.LevelWarn)

const bareRoot = "registry.deckhouse.ru/deckhouse"
c := pkgclient.NewFromOptions(bareRoot)
regSvc := registryservice.NewService(c, pkg.FEEdition, logger)

svc := NewService(
regSvc,
t.TempDir(),
&Options{BundleDir: t.TempDir(), DryRun: true},
logger,
userLogger,
)

const tag = "v1.75.7"
svc.downloadList.FillInstallerImages([]string{tag})

expected := bareRoot + "/" + internal.InstallerSegment + ":" + tag
_, ok := svc.downloadList.Installer[expected]
assert.Truef(t, ok,
"installer downloadList must contain %q (bare-root, no edition); actual keys: %v",
expected, svc.downloadList.Installer)

// The forbidden shape: <root>/<edition>/installer:tag. The installer
// repository does not exist under the edition sub-tree, so any such key
// would point at a 404 in production.
forbidden := bareRoot + "/fe/" + internal.InstallerSegment + ":" + tag
_, badFound := svc.downloadList.Installer[forbidden]
assert.Falsef(t, badFound,
"installer downloadList must NOT contain edition-scoped key %q; "+
"installer lives at <root>/installer regardless of edition",
forbidden)

for key := range svc.downloadList.Installer {
assert.Falsef(t,
strings.HasPrefix(key, bareRoot+"/fe/"),
"installer downloadList key %q must not start with the edition-scoped prefix", key)
}
}

// TestInstallerService_RootURL_NoEdition pins down the unchanged behaviour
// when edition is not set: installer paths still sit at <root>/installer.
func TestInstallerService_RootURL_NoEdition(t *testing.T) {
logger := dkplog.NewLogger(dkplog.WithLevel(slog.LevelWarn))
userLogger := log.NewSLogger(slog.LevelWarn)

const bareRoot = "registry.example.com/deckhouse"
c := pkgclient.NewFromOptions(bareRoot)
regSvc := registryservice.NewService(c, pkg.NoEdition, logger)

svc := NewService(
regSvc,
t.TempDir(),
&Options{BundleDir: t.TempDir(), DryRun: true},
logger,
userLogger,
)

const tag = "v1.75.7"
svc.downloadList.FillInstallerImages([]string{tag})

expected := bareRoot + "/" + internal.InstallerSegment + ":" + tag
_, ok := svc.downloadList.Installer[expected]
assert.Truef(t, ok,
"installer downloadList must contain %q; actual keys: %v",
expected, svc.downloadList.Installer)
}
7 changes: 6 additions & 1 deletion internal/mirror/modules/modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,12 @@ func NewService(
options.Filter = filter
}

rootURL := registryService.GetRoot()
// rootURL must include the edition segment (e.g. .../deckhouse/fe) so that
// per-module references like <rootURL>/modules/<name>:<tag> resolve to the
// same path served by registryService.ModuleService(). registryService.GetRoot()
// would return the non-edition root and produce <root>/modules/<name>:<tag>,
// which mismatches the actual ModulesService scope.
rootURL := registryService.GetEditionRoot()

return &Service{
workingDir: workingDir,
Expand Down
129 changes: 129 additions & 0 deletions internal/mirror/modules/modules_edition_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
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 modules

import (
"log/slog"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"

dkplog "github.com/deckhouse/deckhouse/pkg/log"

"github.com/deckhouse/deckhouse-cli/pkg"
"github.com/deckhouse/deckhouse-cli/pkg/libmirror/util/log"
pkgclient "github.com/deckhouse/deckhouse-cli/pkg/registry/client"
registryservice "github.com/deckhouse/deckhouse-cli/pkg/registry/service"
)

// TestModulesService_RootURL_UsesEditionSegment is the regression for the
// "missing edition segment in release-channel URLs" report applied to the
// modules service: the rootURL used to compose per-module registry paths
// must include the edition segment so that lookups land on the same path
// served by the edition-scoped ModulesService.
func TestModulesService_RootURL_UsesEditionSegment(t *testing.T) {
logger := dkplog.NewLogger(dkplog.WithLevel(slog.LevelWarn))
userLogger := log.NewSLogger(slog.LevelWarn)

const bareRoot = "registry.deckhouse.ru/deckhouse"
c := pkgclient.NewFromOptions(bareRoot)
regSvc := registryservice.NewService(c, pkg.FEEdition, logger)

svc := NewService(
regSvc,
t.TempDir(),
&Options{BundleDir: t.TempDir(), DryRun: true},
logger,
userLogger,
)

const editionRoot = bareRoot + "/fe"

assert.Equal(t, editionRoot, svc.rootURL,
"modules Service.rootURL must be the edition-scoped root so that "+
"per-module references resolve under <root>/<edition>/modules/<name>/...")

// pullSingleModule and the public Module model both compose paths as
// filepath.Join(svc.rootURL, "modules", <name>).
// Verify that the composed path carries the edition segment.
const moduleName = "console"
composed := filepath.Join(svc.rootURL, "modules", moduleName)
assert.Equal(t, editionRoot+"/modules/"+moduleName, composed,
"per-module registry path must live under the edition sub-tree")
}

// TestModulesService_RootURL_NoEdition pins down that with no edition both
// roots collapse to the bare host — so the new GetEditionRoot() plumbing is
// a true superset of the previous behaviour, not a different one.
func TestModulesService_RootURL_NoEdition(t *testing.T) {
logger := dkplog.NewLogger(dkplog.WithLevel(slog.LevelWarn))
userLogger := log.NewSLogger(slog.LevelWarn)

const bareRoot = "registry.example.com/deckhouse"
c := pkgclient.NewFromOptions(bareRoot)
regSvc := registryservice.NewService(c, pkg.NoEdition, logger)

svc := NewService(
regSvc,
t.TempDir(),
&Options{BundleDir: t.TempDir(), DryRun: true},
logger,
userLogger,
)

assert.Equal(t, bareRoot, svc.rootURL,
"modules Service.rootURL must equal the bare root when no edition is set")
}

// TestModulesService_RootURL_CoversAllEditions sweeps every Edition value
// so that adding a new edition to pkg.Edition without wiring it through
// registryservice.NewService surfaces here as an explicit mismatch.
func TestModulesService_RootURL_CoversAllEditions(t *testing.T) {
logger := dkplog.NewLogger(dkplog.WithLevel(slog.LevelWarn))
userLogger := log.NewSLogger(slog.LevelWarn)

const bareRoot = "registry.deckhouse.ru/deckhouse"

editions := []pkg.Edition{
pkg.FEEdition,
pkg.EEEdition,
pkg.SEEdition,
pkg.SEPlusEdition,
pkg.BEEdition,
pkg.CEEdition,
}

for _, edition := range editions {
t.Run("edition="+edition.String(), func(t *testing.T) {
c := pkgclient.NewFromOptions(bareRoot)
regSvc := registryservice.NewService(c, edition, logger)

svc := NewService(
regSvc,
t.TempDir(),
&Options{BundleDir: t.TempDir(), DryRun: true},
logger,
userLogger,
)

expected := bareRoot + "/" + edition.String()
assert.Equal(t, expected, svc.rootURL,
"modules Service.rootURL must include the %s edition segment", edition)
})
}
}
8 changes: 7 additions & 1 deletion internal/mirror/platform/platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,13 @@ func NewService(
}
}

rootURL := registryService.GetRoot()
// rootURL must point at the edition sub-tree so that references inside
// downloadList (e.g. release-channel:<channel>) line up with the actual
// repository served by deckhouseService.ReleaseChannels(). Using the
// bare registryService.GetRoot() here would drop the edition segment for
// FE/EE/SE sources and cause duplicate <root>/release-channel:<channel>
// entries to be enqueued alongside the correct <root>/<edition>/release-channel:<channel>.
rootURL := registryService.GetEditionRoot()

return &Service{
deckhouseService: registryService.DeckhouseService(),
Expand Down
Loading
Loading