*
* Copyright (c) 2025 Bocloud Technologies Co., Ltd.
* installer is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* <http://license.coscl.org.cn/MulanPSL2>
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*
*/
package build
import (
"fmt"
"testing"
"github.com/agiledragon/gomonkey/v2"
"github.com/stretchr/testify/assert"
reg "gopkg.openfuyao.cn/bkeadm/pkg/registry"
"gopkg.openfuyao.cn/bkeadm/pkg/server"
"gopkg.openfuyao.cn/bkeadm/utils/log"
)
func TestSyncRepo(t *testing.T) {
stopChan := make(chan struct{})
defer close(stopChan)
tests := []struct {
name string
mockRemoveImageRegistry func(string) error
mockStartImageRegistry func(string, string, string, string) error
mockProcessRepoImages func(Repo, chan struct{}, bool) error
mockPackImageAndCleanup func() error
cfg *BuildConfig
expectError bool
}{
{
name: "successful sync",
mockRemoveImageRegistry: func(name string) error { return nil },
mockStartImageRegistry: func(name, image, port, dir string) error { return nil },
mockProcessRepoImages: func(repo Repo, stopChan chan struct{}, defaultSkipTLSVerify bool) error { return nil },
mockPackImageAndCleanup: func() error { return nil },
cfg: &BuildConfig{
Repos: []Repo{
{
NeedDownload: true,
SubImages: []SubImage{
{
Images: []Image{
{
Name: "test-image",
Tag: []string{"v1.0"},
},
},
},
},
},
},
},
expectError: false,
},
{
name: "process repo images fails",
mockRemoveImageRegistry: func(name string) error { return nil },
mockStartImageRegistry: func(name, image, port, dir string) error { return nil },
mockProcessRepoImages: func(repo Repo, stopChan chan struct{}, defaultSkipTLSVerify bool) error {
return fmt.Errorf("process error")
},
cfg: &BuildConfig{
Repos: []Repo{
{
NeedDownload: true,
SubImages: []SubImage{
{
Images: []Image{
{
Name: "test-image",
Tag: []string{"v1.0"},
},
},
},
},
},
},
},
expectError: true,
},
{
name: "pack image and cleanup fails",
mockRemoveImageRegistry: func(name string) error { return nil },
mockStartImageRegistry: func(name, image, port, dir string) error { return nil },
mockProcessRepoImages: func(repo Repo, stopChan chan struct{}, defaultSkipTLSVerify bool) error { return nil },
mockPackImageAndCleanup: func() error { return fmt.Errorf("pack error") },
cfg: &BuildConfig{
Repos: []Repo{
{
NeedDownload: true,
SubImages: []SubImage{
{
Images: []Image{
{
Name: "test-image",
Tag: []string{"v1.0"},
},
},
},
},
},
},
},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(server.RemoveImageRegistry, tt.mockRemoveImageRegistry)
patches.ApplyFunc(server.StartImageRegistry, tt.mockStartImageRegistry)
patches.ApplyFunc(processRepoImages, tt.mockProcessRepoImages)
patches.ApplyFunc(packImageAndCleanup, tt.mockPackImageAndCleanup)
patches.ApplyFunc(log.SteppedInfo, func(stepName string, args ...any) {})
patches.ApplyGlobalVar(&tmpRegistry, "/tmp/registry")
err := syncRepo(tt.cfg, stopChan, false)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestProcessRepoImages(t *testing.T) {
stopChan := make(chan struct{})
defer close(stopChan)
tests := []struct {
name string
repo Repo
mockProcessSingleSubImage func(SubImage, []string, chan struct{}, bool) error
expectError bool
}{
{
name: "successful processing",
repo: Repo{
Architecture: []string{"amd64"},
SubImages: []SubImage{
{
Images: []Image{
{
Name: "test-image",
Tag: []string{"v1.0"},
},
},
},
},
},
mockProcessSingleSubImage: func(subImage SubImage, arch []string, stopChan chan struct{}, defaultSkipTLSVerify bool) error {
return nil
},
expectError: false,
},
{
name: "process single sub image fails",
repo: Repo{
Architecture: []string{"amd64"},
SubImages: []SubImage{
{
Images: []Image{
{
Name: "test-image",
Tag: []string{"v1.0"},
},
},
},
},
},
mockProcessSingleSubImage: func(subImage SubImage, arch []string, stopChan chan struct{}, defaultSkipTLSVerify bool) error {
return fmt.Errorf("process error")
},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(processSingleSubImage, tt.mockProcessSingleSubImage)
err := processRepoImages(tt.repo, stopChan, false)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestProcessSingleSubImage(t *testing.T) {
stopChan := make(chan struct{})
defer close(stopChan)
tests := []struct {
name string
subImage SubImage
architecture []string
mockProcessImageTags func(Image, SubImage, []string, bool) error
expectError bool
}{
{
name: "successful processing",
subImage: SubImage{
Images: []Image{
{
Name: "test-image",
Tag: []string{"v1.0"},
},
},
},
architecture: []string{"amd64"},
mockProcessImageTags: func(image Image, subImage SubImage, arch []string, defaultSkipTLSVerify bool) error { return nil },
expectError: false,
},
{
name: "process image tags fails",
subImage: SubImage{
Images: []Image{
{
Name: "test-image",
Tag: []string{"v1.0"},
},
},
},
architecture: []string{"amd64"},
mockProcessImageTags: func(image Image, subImage SubImage, arch []string, defaultSkipTLSVerify bool) error {
return fmt.Errorf("process error")
},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(processImageTags, tt.mockProcessImageTags)
err := processSingleSubImage(tt.subImage, tt.architecture, stopChan, false)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestProcessImageTags(t *testing.T) {
tests := []struct {
name string
image Image
subImage SubImage
architecture []string
mockImageTrack func(string, string, string, string, []string) (string, error)
mockSyncRepoImage func(string, string, []string, bool) error
expectError bool
}{
{
name: "successful processing",
image: Image{
Name: "test-image",
Tag: []string{"v1.0"},
},
subImage: SubImage{
SourceRepo: "source-repo",
TargetRepo: "target-repo",
},
architecture: []string{"amd64"},
mockImageTrack: func(sourceRepo, imageTrack, imageName, tag string, arch []string) (string, error) {
return "source:image", nil
},
mockSyncRepoImage: func(source, target string, arch []string, srcTLSVerify bool) error { return nil },
expectError: false,
},
{
name: "image track fails",
image: Image{
Name: "test-image",
Tag: []string{"v1.0"},
},
subImage: SubImage{
SourceRepo: "source-repo",
TargetRepo: "target-repo",
},
architecture: []string{"amd64"},
mockImageTrack: func(sourceRepo, imageTrack, imageName, tag string, arch []string) (string, error) {
return "", fmt.Errorf("track error")
},
expectError: true,
},
{
name: "sync repo image fails",
image: Image{
Name: "test-image",
Tag: []string{"v1.0"},
},
subImage: SubImage{
SourceRepo: "source-repo",
TargetRepo: "target-repo",
},
architecture: []string{"amd64"},
mockImageTrack: func(sourceRepo, imageTrack, imageName, tag string, arch []string) (string, error) {
return "source:image", nil
},
mockSyncRepoImage: func(source, target string, arch []string, srcTLSVerify bool) error { return fmt.Errorf("sync error") },
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(imageTrack, tt.mockImageTrack)
patches.ApplyFunc(syncRepoImage, tt.mockSyncRepoImage)
err := processImageTags(tt.image, tt.subImage, tt.architecture, false)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestProcessImageTagsSkipTLSVerifyPrecedence(t *testing.T) {
trueVal := true
falseVal := false
tests := []struct {
name string
subImage SubImage
defaultSkipTLSVerify bool
mockIsInsecureInDocker func(string) bool
expectedSrcTLSVerify bool
}{
{
name: "use cli default when yaml not configured",
subImage: SubImage{
SourceRepo: "source-repo",
TargetRepo: "target-repo",
},
defaultSkipTLSVerify: true,
mockIsInsecureInDocker: func(string) bool { return false },
expectedSrcTLSVerify: false,
},
{
name: "yaml false overrides cli true",
subImage: SubImage{
SourceRepo: "source-repo",
TargetRepo: "target-repo",
SkipTLSVerify: &falseVal,
},
defaultSkipTLSVerify: true,
mockIsInsecureInDocker: func(string) bool { return false },
expectedSrcTLSVerify: true,
},
{
name: "yaml true overrides cli false",
subImage: SubImage{
SourceRepo: "source-repo",
TargetRepo: "target-repo",
SkipTLSVerify: &trueVal,
},
defaultSkipTLSVerify: false,
mockIsInsecureInDocker: func(string) bool { return false },
expectedSrcTLSVerify: false,
},
{
name: "docker insecure fallback when yaml and cli both false",
subImage: SubImage{
SourceRepo: "10.17.31.217:5000",
TargetRepo: "target-repo",
},
defaultSkipTLSVerify: false,
mockIsInsecureInDocker: func(source string) bool { return true },
expectedSrcTLSVerify: false,
},
{
name: "docker insecure not matched when yaml and cli both false",
subImage: SubImage{
SourceRepo: "registry.example.com",
TargetRepo: "target-repo",
},
defaultSkipTLSVerify: false,
mockIsInsecureInDocker: func(source string) bool { return false },
expectedSrcTLSVerify: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(imageTrack, func(sourceRepo, imageTrack, imageName, tag string, arch []string) (string, error) {
return "source:image", nil
})
patches.ApplyFunc(isInsecureInDocker, tt.mockIsInsecureInDocker)
patches.ApplyFunc(syncRepoImage, func(source, target string, arch []string, srcTLSVerify bool) error {
assert.Equal(t, tt.expectedSrcTLSVerify, srcTLSVerify)
return nil
})
err := processImageTags(Image{Name: "test-image", Tag: []string{"v1.0"}}, tt.subImage, []string{"amd64"}, tt.defaultSkipTLSVerify)
assert.NoError(t, err)
})
}
}
func TestSyncRepoImage(t *testing.T) {
tests := []struct {
name string
source string
target string
arch []string
srcTLSVerify bool
mockSyncSingleArchImage func(string, string, string, bool) error
mockSyncMultiArchImage func(string, string, []string, bool) error
expectError bool
}{
{
name: "single architecture sync",
source: "source:image",
target: "target:image",
arch: []string{"amd64"},
srcTLSVerify: true,
mockSyncSingleArchImage: func(source, target, arch string, srcTLSVerify bool) error { return nil },
mockSyncMultiArchImage: func(source, target string, arch []string, srcTLSVerify bool) error { return nil },
expectError: false,
},
{
name: "multi architecture sync",
source: "source:image",
target: "target:image",
arch: []string{"amd64", "arm64"},
srcTLSVerify: true,
mockSyncSingleArchImage: func(source, target, arch string, srcTLSVerify bool) error { return nil },
mockSyncMultiArchImage: func(source, target string, arch []string, srcTLSVerify bool) error { return nil },
expectError: false,
},
{
name: "single arch sync fails",
source: "source:image",
target: "target:image",
arch: []string{"amd64"},
srcTLSVerify: true,
mockSyncSingleArchImage: func(source, target, arch string, srcTLSVerify bool) error { return fmt.Errorf("sync error") },
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(syncSingleArchImage, tt.mockSyncSingleArchImage)
patches.ApplyFunc(syncMultiArchImage, tt.mockSyncMultiArchImage)
err := syncRepoImage(tt.source, tt.target, tt.arch, tt.srcTLSVerify)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestSyncSingleArchImage(t *testing.T) {
tests := []struct {
name string
source string
target string
arch string
srcTLSVerify bool
mockCopyRegistry func(reg.Options) error
expectError bool
}{
{
name: "successful sync",
source: "source:image",
target: "target:image",
arch: "amd64",
srcTLSVerify: true,
mockCopyRegistry: func(opts reg.Options) error {
assert.Equal(t, false, opts.MultiArch)
assert.Equal(t, true, opts.SrcTLSVerify)
assert.Equal(t, false, opts.DestTLSVerify)
assert.Equal(t, "amd64", opts.Arch)
assert.Equal(t, "source:image", opts.Source)
assert.Equal(t, "target:image", opts.Target)
return nil
},
expectError: false,
},
{
name: "copy registry fails",
source: "source:image",
target: "target:image",
arch: "amd64",
srcTLSVerify: true,
mockCopyRegistry: func(opts reg.Options) error {
return fmt.Errorf("copy error")
},
expectError: true,
},
{
name: "retry succeeds",
source: "source:image",
target: "target:image",
arch: "amd64",
srcTLSVerify: true,
mockCopyRegistry: func(opts reg.Options) error {
if opts.Source == "source:image" {
return fmt.Errorf("first attempt fails")
}
return nil
},
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(reg.CopyRegistry, tt.mockCopyRegistry)
patches.ApplyFunc(log.SteppedInfo, func(stepName string, args ...any) {})
err := syncSingleArchImage(tt.source, tt.target, tt.arch, tt.srcTLSVerify)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestRetrySyncWithArchSuffix(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(reg.CopyRegistry, func(opts reg.Options) error {
assert.Equal(t, "source:image-amd64", opts.Source)
assert.Equal(t, "target:image", opts.Target)
return nil
})
patches.ApplyFunc(log.SteppedInfo, func(stepName string, args ...any) {})
op := reg.Options{
Source: "source:image",
Target: "target:image",
}
err := retrySyncWithArchSuffix("source:image", "target:image", "amd64", op)
assert.NoError(t, err)
}
func TestSyncMultiArchImage(t *testing.T) {
tests := []struct {
name string
source string
target string
arch []string
srcTLSVerify bool
mockTryDirectMultiArchSync func(string, string, bool) error
mockSyncArchImagesAndCreateManifest func(string, string, []string, bool) error
expectError bool
}{
{
name: "direct sync succeeds",
source: "source:image",
target: "target:image",
arch: []string{"amd64", "arm64"},
srcTLSVerify: true,
mockTryDirectMultiArchSync: func(source, target string, srcTLSVerify bool) error { return nil },
mockSyncArchImagesAndCreateManifest: func(source, target string, arch []string, srcTLSVerify bool) error { return nil },
expectError: false,
},
{
name: "fallback to arch images sync",
source: "source:image",
target: "target:image",
arch: []string{"amd64", "arm64"},
srcTLSVerify: true,
mockTryDirectMultiArchSync: func(source, target string, srcTLSVerify bool) error { return fmt.Errorf("direct sync fails") },
mockSyncArchImagesAndCreateManifest: func(source, target string, arch []string, srcTLSVerify bool) error { return nil },
expectError: false,
},
{
name: "both methods fail",
source: "source:image",
target: "target:image",
arch: []string{"amd64", "arm64"},
srcTLSVerify: true,
mockTryDirectMultiArchSync: func(source, target string, srcTLSVerify bool) error { return fmt.Errorf("direct sync fails") },
mockSyncArchImagesAndCreateManifest: func(source, target string, arch []string, srcTLSVerify bool) error {
return fmt.Errorf("arch sync fails")
},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(tryDirectMultiArchSync, tt.mockTryDirectMultiArchSync)
patches.ApplyFunc(syncArchImagesAndCreateManifest, tt.mockSyncArchImagesAndCreateManifest)
err := syncMultiArchImage(tt.source, tt.target, tt.arch, tt.srcTLSVerify)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestTryDirectMultiArchSync(t *testing.T) {
tests := []struct {
name string
source string
target string
srcTLSVerify bool
mockIsMultiArchManifests func(bool, string) bool
mockCopyRegistry func(reg.Options) error
expectError bool
}{
{
name: "multi-arch image sync succeeds",
source: "source:image",
target: "target:image",
srcTLSVerify: true,
mockIsMultiArchManifests: func(srcTLSVerify bool, imageAddress string) bool { return true },
mockCopyRegistry: func(opts reg.Options) error {
assert.Equal(t, true, opts.MultiArch)
return nil
},
expectError: false,
},
{
name: "not multi-arch image",
source: "source:image",
target: "target:image",
srcTLSVerify: true,
mockIsMultiArchManifests: func(srcTLSVerify bool, imageAddress string) bool { return false },
expectError: true,
},
{
name: "copy registry fails",
source: "source:image",
target: "target:image",
srcTLSVerify: true,
mockIsMultiArchManifests: func(srcTLSVerify bool, imageAddress string) bool { return true },
mockCopyRegistry: func(opts reg.Options) error { return fmt.Errorf("copy error") },
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(reg.IsMultiArchManifests, tt.mockIsMultiArchManifests)
patches.ApplyFunc(reg.CopyRegistry, tt.mockCopyRegistry)
patches.ApplyFunc(log.SteppedInfo, func(stepName string, args ...any) {})
err := tryDirectMultiArchSync(tt.source, tt.target, tt.srcTLSVerify)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestSyncArchImagesAndCreateManifest(t *testing.T) {
tests := []struct {
name string
source string
target string
arch []string
srcTLSVerify bool
mockSyncSingleArchVariant func(string, string, string, reg.Options) (reg.ImageArch, error)
mockCreateMultiArchImage func([]reg.ImageArch, string) error
expectError bool
}{
{
name: "successful sync and manifest creation",
source: "source:image",
target: "target:image",
arch: []string{"amd64", "arm64"},
srcTLSVerify: true,
mockSyncSingleArchVariant: func(source, target, arch string, op reg.Options) (reg.ImageArch, error) {
return reg.ImageArch{
Name: target + "-" + arch,
OS: "linux",
Architecture: arch,
}, nil
},
mockCreateMultiArchImage: func(img []reg.ImageArch, target string) error { return nil },
expectError: false,
},
{
name: "sync single arch variant fails",
source: "source:image",
target: "target:image",
arch: []string{"amd64"},
srcTLSVerify: true,
mockSyncSingleArchVariant: func(source, target, arch string, op reg.Options) (reg.ImageArch, error) {
return reg.ImageArch{}, fmt.Errorf("sync error")
},
expectError: true,
},
{
name: "create multi-arch image fails",
source: "source:image",
target: "target:image",
arch: []string{"amd64"},
srcTLSVerify: true,
mockSyncSingleArchVariant: func(source, target, arch string, op reg.Options) (reg.ImageArch, error) {
return reg.ImageArch{
Name: target + "-" + arch,
OS: "linux",
Architecture: arch,
}, nil
},
mockCreateMultiArchImage: func(img []reg.ImageArch, target string) error { return fmt.Errorf("create error") },
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(syncSingleArchVariant, tt.mockSyncSingleArchVariant)
patches.ApplyFunc(reg.CreateMultiArchImage, tt.mockCreateMultiArchImage)
patches.ApplyFunc(log.SteppedInfo, func(stepName string, args ...any) {})
err := syncArchImagesAndCreateManifest(tt.source, tt.target, tt.arch, tt.srcTLSVerify)
if tt.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestSyncSingleArchVariant(t *testing.T) {
op := reg.Options{
MultiArch: false,
SrcTLSVerify: true,
DestTLSVerify: false,
}
tests := []struct {
name string
source string
target string
arch string
mockCopyRegistry func(reg.Options) error
expectError bool
}{
{
name: "successful sync (no cut, use original source with arch choice)",
source: "source:image",
target: "target:image",
arch: "amd64",
mockCopyRegistry: func(opts reg.Options) error {
assert.Equal(t, false, opts.MultiArch)
assert.Equal(t, true, opts.SrcTLSVerify)
assert.Equal(t, false, opts.DestTLSVerify)
assert.Equal(t, "amd64", opts.Arch)
assert.Equal(t, "target:image-amd64", opts.Target)
assert.Equal(t, "source:image", opts.Source)
return nil
},
expectError: false,
},
{
name: "successful sync (has cut, replace cut with -arch-)",
source: "source:image-*-202112111112",
target: "target:image",
arch: "arm64",
mockCopyRegistry: func(opts reg.Options) error {
assert.Equal(t, false, opts.MultiArch)
assert.Equal(t, true, opts.SrcTLSVerify)
assert.Equal(t, false, opts.DestTLSVerify)
assert.Equal(t, "arm64", opts.Arch)
assert.Equal(t, "target:image-arm64", opts.Target)
assert.Equal(t, "source:image-arm64-202112111112", opts.Source)
return nil
},
expectError: false,
},
{
name: "copy registry fails",
source: "source:image",
target: "target:image",
arch: "amd64",
mockCopyRegistry: func(opts reg.Options) error {
return fmt.Errorf("copy error")
},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(reg.CopyRegistry, tt.mockCopyRegistry)
patches.ApplyFunc(log.Debugf, func(format string, args ...interface{}) {})
patches.ApplyFunc(log.SteppedInfo, func(stepName string, args ...any) {})
img, err := syncSingleArchVariant(tt.source, tt.target, tt.arch, op)
if tt.expectError {
assert.Error(t, err)
assert.Equal(t, reg.ImageArch{}, img)
} else {
assert.NoError(t, err)
assert.Equal(t, "target:image-"+tt.arch, img.Name)
assert.Equal(t, "linux", img.OS)
assert.Equal(t, tt.arch, img.Architecture)
}
})
}
}