*
* 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 containerd
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"testing"
"text/template"
"time"
"github.com/agiledragon/gomonkey/v2"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/wait"
"gopkg.openfuyao.cn/bkeadm/pkg/config"
"gopkg.openfuyao.cn/bkeadm/pkg/executor/containerd"
"gopkg.openfuyao.cn/bkeadm/pkg/executor/exec"
"gopkg.openfuyao.cn/bkeadm/pkg/executor/k8s"
"gopkg.openfuyao.cn/bkeadm/pkg/global"
"gopkg.openfuyao.cn/bkeadm/utils"
"gopkg.openfuyao.cn/bkeadm/utils/log"
)
const (
testFileModeReadWrite = os.FileMode(0755)
testTwoValue = 2
testFiveValue = 5
)
const (
testTwoMinutes = 2 * time.Minute
testFiveSeconds = 5 * time.Second
)
func TestApplyContainerdCrd(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(k8s.NewKubernetesClient, func(kubeConfig string) (k8s.KubernetesClient, error) {
return &k8s.Client{}, nil
})
patches.ApplyFunc(os.WriteFile, func(filename string, data []byte, perm os.FileMode) error {
assert.Contains(t, filename, "containerd_crd.yaml")
assert.Equal(t, containerdCrd, data)
return nil
})
patches.ApplyFunc(time.Sleep, func(d time.Duration) {})
patches.ApplyFunc(log.SteppedInfo, func(stepName string, args ...any) {})
patches.ApplyFunc((*k8s.Client).InstallYaml, func(k *k8s.Client, filename string, variable map[string]string, ns string) error {
assert.Contains(t, filename, "containerd_crd.yaml")
return nil
})
originalK8s := global.K8s
global.K8s = nil
defer func() {
global.K8s = originalK8s
}()
err := applyContainerdCrd()
assert.NoError(t, err)
}
func TestApplyContainerdCrdError(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(k8s.NewKubernetesClient, func(kubeConfig string) (k8s.KubernetesClient, error) {
return nil, fmt.Errorf("client creation failed")
})
err := applyContainerdCrd()
assert.Error(t, err)
assert.Contains(t, err.Error(), "client creation failed")
}
func TestApplyContainerdDefault(t *testing.T) {
domain := "registry.example.com"
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(config.GenerateControllerParam, func(domain string) (string, string) {
return "sandbox-image", "false"
})
patches.ApplyFunc((*template.Template).Execute, func(t *template.Template, wr io.Writer, data interface{}) error {
_, err := wr.Write([]byte("mock template output"))
return err
})
patches.ApplyFunc(os.WriteFile, func(filename string, data []byte, perm os.FileMode) error {
return nil
})
patches.ApplyFunc(time.Sleep, func(d time.Duration) {})
patches.ApplyFunc((*k8s.MockK8sClient).InstallYaml, func(k *k8s.MockK8sClient, filename string, variable map[string]string, ns string) error {
return nil
})
patches.ApplyFunc((*k8s.MockK8sClient).CreateNamespace, func(k *k8s.MockK8sClient, namespace *corev1.Namespace) error {
return nil
})
global.K8s = &k8s.MockK8sClient{}
err := applyContainerdDefault(domain)
assert.Error(t, err)
}
func TestApplyContainerdDefaultTemplateError(t *testing.T) {
domain := "registry.example.com"
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(config.GenerateControllerParam, func(domain string) (string, string) {
return "sandbox-image", "false"
})
patches.ApplyFunc(template.New, func(name string) *template.Template {
return &template.Template{}
})
patches.ApplyFunc((*template.Template).Parse, func(tpl *template.Template, text string) (*template.Template, error) {
return nil, fmt.Errorf("template parse error: invalid syntax")
})
err := applyContainerdDefault(domain)
assert.Error(t, err)
}
func TestApplyContainerdCfg(t *testing.T) {
domain := "registry.example.com"
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(applyContainerdCrd, func() error {
return nil
})
patches.ApplyFunc(applyContainerdDefault, func(domain string) error {
assert.Equal(t, domain, domain)
return nil
})
patches.ApplyFunc(log.SteppedInfo, func(stepName string, args ...any) {})
err := ApplyContainerdCfg(domain)
assert.NoError(t, err)
}
func TestApplyContainerdCfgError(t *testing.T) {
domain := "registry.example.com"
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(applyContainerdCrd, func() error {
return fmt.Errorf("CRD apply failed")
})
err := ApplyContainerdCfg(domain)
assert.Error(t, err)
assert.Contains(t, err.Error(), "apply containerd crd failed")
}
func TestGetPlatform(t *testing.T) {
result := getPlatform()
assert.Equal(t, "linux/amd64", result)
}
func TestGetPlatformCoverage(t *testing.T) {
platforms := map[string]string{
"amd64": "linux/amd64",
"arm64": "linux/arm64",
"arm": "linux/arm/v7",
}
for arch, expected := range platforms {
t.Run(fmt.Sprintf("arch_%s", arch), func(t *testing.T) {
assert.Contains(t, platforms, arch)
assert.Equal(t, expected, platforms[arch])
})
}
}
func TestExecuteTemplateWithFile(t *testing.T) {
tempDir := t.TempDir()
testFile := filepath.Join(tempDir, "test-template.txt")
file, err := os.Create(testFile)
assert.NoError(t, err)
defer file.Close()
templateContent := "Hello {{.Name}}, welcome to {{.Place}}!"
templateName := "test-template"
data := map[string]string{
"Name": "John",
"Place": "GoLand",
}
err = executeTemplateWithFile(templateContent, templateName, data, file)
assert.NoError(t, err)
content, err := os.ReadFile(testFile)
assert.NoError(t, err)
assert.Equal(t, "Hello John, welcome to GoLand!", string(content))
}
func TestExecuteTemplateWithFileParseError(t *testing.T) {
tempDir := t.TempDir()
testFile := filepath.Join(tempDir, "test-template.txt")
file, err := os.Create(testFile)
assert.NoError(t, err)
defer file.Close()
templateContent := "Hello {{.Name}}, {{.InvalidSyntax{{}}"
templateName := "test-template"
data := map[string]string{
"Name": "John",
}
err = executeTemplateWithFile(templateContent, templateName, data, file)
assert.Error(t, err)
assert.Contains(t, err.Error(), "parse template")
}
func TestExecuteTemplateWithFileExecuteError(t *testing.T) {
file, err := os.CreateTemp("", "test-execute-error")
assert.NoError(t, err)
file.Close()
templateContent := "Hello {{.Name}}!"
templateName := "test-template"
data := map[string]string{
"Name": "John",
}
err = executeTemplateWithFile(templateContent, templateName, data, file)
assert.Error(t, err)
assert.Contains(t, err.Error(), "execute template")
}
func TestInstall(t *testing.T) {
domain := "registry.example.com"
port := "5000"
runtimeStorage := "/var/lib/containerd"
containerdFile := "/path/to/containerd.tar.gz"
caFile := "/path/to/ca.crt"
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(utils.UnTar, func(archive, target string) error {
assert.Equal(t, containerdFile, archive)
assert.Equal(t, defaultInstallDirectory, target)
return nil
})
patches.ApplyFunc(config.GenerateControllerParam, func(domain string) (string, string) {
return "sandbox-image", "false"
})
patches.ApplyFunc(getPlatform, func() string {
return "linux/amd64"
})
patches.ApplyFunc(log.SteppedInfo, func(stepName string, args ...any) {})
patches.ApplyFunc(writeConfigToDisk, func(runtimeParam map[string]string) error {
return nil
})
patches.ApplyFunc(createHostsTOML, func(runtimeParam map[string]string) error {
return nil
})
patches.ApplyFunc((*exec.CommandExecutor).ExecuteCommand, func(c *exec.CommandExecutor, command string, args ...string) error {
if command == "systemctl" {
if args[0] == "enable" && args[1] == "containerd" {
return nil
} else if args[0] == "start" && args[1] == "containerd" {
return nil
}
}
return fmt.Errorf("unexpected command")
})
patches.ApplyFunc(waitContainerdReady, func() error {
return nil
})
originalCommand := global.Command
global.Command = &exec.CommandExecutor{}
defer func() {
global.Command = originalCommand
}()
err := Install(domain, port, runtimeStorage, containerdFile, caFile)
assert.NoError(t, err)
}
func TestInstallUntarError(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(utils.UnTar, func(archive, target string) error {
return fmt.Errorf("untar error")
})
err := Install("registry.example.com", "5000", "/var/lib/containerd", "/path/to/containerd.tar.gz", "/path/to/ca.crt")
assert.Error(t, err)
}
func TestInstallWriteConfigError(t *testing.T) {
domain := "registry.example.com"
port := "5000"
runtimeStorage := "/var/lib/containerd"
containerdFile := "/path/to/containerd.tar.gz"
caFile := "/path/to/ca.crt"
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(utils.UnTar, func(archive, target string) error {
return nil
})
patches.ApplyFunc(config.GenerateControllerParam, func(domain string) (string, string) {
return "sandbox-image", "false"
})
patches.ApplyFunc(getPlatform, func() string {
return "linux/amd64"
})
patches.ApplyFunc(log.SteppedInfo, func(stepName string, args ...any) {})
patches.ApplyFunc(writeConfigToDisk, func(runtimeParam map[string]string) error {
return fmt.Errorf("write config error")
})
originalCommand := global.Command
global.Command = &exec.CommandExecutor{}
defer func() {
global.Command = originalCommand
}()
err := Install(domain, port, runtimeStorage, containerdFile, caFile)
assert.Error(t, err)
}
func TestInstallCreateHostsError(t *testing.T) {
domain := "registry.example.com"
port := "5000"
runtimeStorage := "/var/lib/containerd"
containerdFile := "/path/to/containerd.tar.gz"
caFile := "/path/to/ca.crt"
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(utils.UnTar, func(archive, target string) error {
return nil
})
patches.ApplyFunc(config.GenerateControllerParam, func(domain string) (string, string) {
return "sandbox-image", "false"
})
patches.ApplyFunc(getPlatform, func() string {
return "linux/amd64"
})
patches.ApplyFunc(log.SteppedInfo, func(stepName string, args ...any) {})
patches.ApplyFunc(writeConfigToDisk, func(runtimeParam map[string]string) error {
return nil
})
patches.ApplyFunc(createHostsTOML, func(runtimeParam map[string]string) error {
return fmt.Errorf("create hosts error")
})
originalCommand := global.Command
global.Command = &exec.CommandExecutor{}
defer func() {
global.Command = originalCommand
}()
err := Install(domain, port, runtimeStorage, containerdFile, caFile)
assert.Error(t, err)
}
func TestInstallSystemctlEnableError(t *testing.T) {
domain := "registry.example.com"
port := "5000"
runtimeStorage := "/var/lib/containerd"
containerdFile := "/path/to/containerd.tar.gz"
caFile := "/path/to/ca.crt"
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(utils.UnTar, func(archive, target string) error {
return nil
})
patches.ApplyFunc(config.GenerateControllerParam, func(domain string) (string, string) {
return "sandbox-image", "false"
})
patches.ApplyFunc(getPlatform, func() string {
return "linux/amd64"
})
patches.ApplyFunc(log.SteppedInfo, func(stepName string, args ...any) {})
patches.ApplyFunc(writeConfigToDisk, func(runtimeParam map[string]string) error {
return nil
})
patches.ApplyFunc(createHostsTOML, func(runtimeParam map[string]string) error {
return nil
})
patches.ApplyFunc((*exec.CommandExecutor).ExecuteCommand, func(c *exec.CommandExecutor, command string, args ...string) error {
if command == "systemctl" && args[0] == "enable" {
return fmt.Errorf("systemctl enable error")
}
return nil
})
originalCommand := global.Command
global.Command = &exec.CommandExecutor{}
defer func() {
global.Command = originalCommand
}()
err := Install(domain, port, runtimeStorage, containerdFile, caFile)
assert.Error(t, err)
}
func TestInstallWaitContainerdError(t *testing.T) {
domain := "registry.example.com"
port := "5000"
runtimeStorage := "/var/lib/containerd"
containerdFile := "/path/to/containerd.tar.gz"
caFile := "/path/to/ca.crt"
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(utils.UnTar, func(archive, target string) error {
return nil
})
patches.ApplyFunc(config.GenerateControllerParam, func(domain string) (string, string) {
return "sandbox-image", "false"
})
patches.ApplyFunc(getPlatform, func() string {
return "linux/amd64"
})
patches.ApplyFunc(log.SteppedInfo, func(stepName string, args ...any) {})
patches.ApplyFunc(writeConfigToDisk, func(runtimeParam map[string]string) error {
return nil
})
patches.ApplyFunc(createHostsTOML, func(runtimeParam map[string]string) error {
return nil
})
patches.ApplyFunc((*exec.CommandExecutor).ExecuteCommand, func(c *exec.CommandExecutor, command string, args ...string) error {
return nil
})
patches.ApplyFunc(waitContainerdReady, func() error {
return fmt.Errorf("wait containerd error")
})
originalCommand := global.Command
global.Command = &exec.CommandExecutor{}
defer func() {
global.Command = originalCommand
}()
err := Install(domain, port, runtimeStorage, containerdFile, caFile)
assert.Error(t, err)
}
func TestWaitContainerdReady(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(wait.PollImmediateUntilWithContext, func(ctx context.Context, interval time.Duration, condition wait.ConditionWithContextFunc) error {
_, err := condition(ctx)
return err
})
patches.ApplyFunc(containerd.NewContainedClient, func() (containerd.ContainerdClient, error) {
return &containerd.Client{}, nil
})
patches.ApplyFunc(log.SteppedInfo, func(stepName string, args ...any) {})
err := waitContainerdReady()
assert.NoError(t, err)
}
func TestWaitContainerdReadyTimeout(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(wait.PollImmediateUntilWithContext, func(ctx context.Context, interval time.Duration, condition wait.ConditionWithContextFunc) error {
return fmt.Errorf("timed out waiting for condition")
})
err := waitContainerdReady()
assert.Error(t, err)
assert.Contains(t, err.Error(), "timed out waiting for condition")
}
func TestWriteConfigToDisk(t *testing.T) {
runtimeParam := map[string]string{
"param1": "value1",
"param2": "value2",
}
tempDir := t.TempDir()
configDir := filepath.Join(tempDir, "etc", "containerd")
err := os.MkdirAll(configDir, testFileModeReadWrite)
assert.NoError(t, err)
originalDir := defaultInstallDirectory
defaultInstallDirectory = tempDir + string(filepath.Separator)
defer func() {
defaultInstallDirectory = originalDir
}()
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(log.SteppedInfo, func(stepName string, args ...any) {})
err = writeConfigToDisk(runtimeParam)
assert.NoError(t, err)
configPath := filepath.Join(tempDir, "etc", "containerd", "config.toml")
assert.True(t, utils.Exists(configPath))
}
func TestWriteConfigToDiskError(t *testing.T) {
runtimeParam := map[string]string{
"param1": "value1",
}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(os.OpenFile, func(name string, flag int, perm os.FileMode) (*os.File, error) {
return nil, fmt.Errorf("open file error")
})
err := writeConfigToDisk(runtimeParam)
assert.Error(t, err)
assert.Contains(t, err.Error(), "open file error")
}
func TestWriteConfigToDiskTemplateExecuteError(t *testing.T) {
runtimeParam := map[string]string{
"param1": "value1",
}
tempDir := t.TempDir()
configDir := filepath.Join(tempDir, "etc", "containerd")
err := os.MkdirAll(configDir, testFileModeReadWrite)
assert.NoError(t, err)
originalDir := defaultInstallDirectory
defaultInstallDirectory = tempDir
defer func() {
defaultInstallDirectory = originalDir
}()
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(log.SteppedInfo, func(stepName string, args ...any) {})
err = writeConfigToDisk(runtimeParam)
assert.Error(t, err)
}
func TestCreateOfflineSpecialHostsTOML(t *testing.T) {
certsDir := t.TempDir()
port := "5000"
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(createOfflineSpecialHostsTOML, func(certsDir, port string) error {
return nil
})
err := createOfflineSpecialHostsTOML(certsDir, port)
assert.NoError(t, err)
}
func TestCreateOfflineSpecialHostsTOMLError(t *testing.T) {
port := "5000"
certsDir := t.TempDir()
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(os.MkdirAll, func(path string, perm os.FileMode) error {
return fmt.Errorf("mkdir failed")
})
err := createOfflineSpecialHostsTOML(certsDir, port)
assert.Error(t, err)
}
func TestGetRegistryList(t *testing.T) {
tests := []struct {
name string
repo string
repoWithNoPort string
offline string
expected []string
}{
{
name: "offline mode enabled",
repo: "registry.example.com:5000",
repoWithNoPort: "registry.example.com",
offline: "true",
expected: []string{
"registry.example.com:5000",
"registry.example.com",
"docker.io", "registry.k8s.io", "k8s.gcr.io", "ghcr.io", "quay.io", "gcr.io", "cr.openfuyao.cn", "hub.oepkgs.net",
},
},
{
name: "offline mode disabled",
repo: "registry.example.com:5000",
repoWithNoPort: "registry.example.com",
offline: "false",
expected: []string{
"registry.example.com:5000",
"registry.example.com",
},
},
{
name: "offline mode disabled with different values",
repo: "private.registry.com",
repoWithNoPort: "private.registry.com",
offline: "",
expected: []string{
"private.registry.com",
"private.registry.com",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getRegistryList(tt.repo, tt.repoWithNoPort, tt.offline)
assert.Equal(t, tt.expected, result)
})
}
}
func TestCreateRegistryHostsTOML(t *testing.T) {
certsDir := t.TempDir()
registry := "test-registry.com"
repo := "registry.example.com:5000"
offline := "false"
caFile := ""
err := createRegistryHostsTOML(registry, repo, offline, caFile, certsDir)
assert.NoError(t, err)
hostsPath := filepath.Join(certsDir, registry, "hosts.toml")
assert.True(t, utils.Exists(hostsPath))
content, err := os.ReadFile(hostsPath)
assert.NoError(t, err)
assert.Contains(t, string(content), fmt.Sprintf("server = \"https://%s\"", registry))
assert.Contains(t, string(content), fmt.Sprintf("[host.\"https://%s\"]", repo))
assert.Contains(t, string(content), "skip_verify = true")
}
func TestCreateRegistryHostsTOMLError(t *testing.T) {
certsDir := t.TempDir()
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(os.MkdirAll, func(path string, perm os.FileMode) error {
return fmt.Errorf("mkdir failed")
})
err := createRegistryHostsTOML("test-registry.com", "registry.example.com:5000", "false", "", certsDir)
assert.Error(t, err)
}
func TestCreateHostsTOML(t *testing.T) {
runtimeParam := map[string]string{
"repo": "registry.example.com:5000",
"offline": "false",
"caFile": "",
"port": "5000",
}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(getRegistryList, func(repo, repoWithNoPort, offline string) []string {
return []string{"registry.example.com:5000", "registry.example.com"}
})
patches.ApplyFunc(createOfflineSpecialHostsTOML, func(certsDir, port string) error {
return nil
})
patches.ApplyFunc(createRegistryHostsTOML, func(registry, repo, offline, caFile, certsDir string) error {
return nil
})
err := createHostsTOML(runtimeParam)
assert.NoError(t, err)
}
func TestCreateHostsTOMLError(t *testing.T) {
runtimeParam := map[string]string{
"repo": "registry.example.com:5000",
"offline": "false",
"caFile": "",
}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(createOfflineSpecialHostsTOML, func(certsDir string) error {
return fmt.Errorf("special hosts TOML creation failed")
})
err := createHostsTOML(runtimeParam)
assert.Error(t, err)
assert.Contains(t, err.Error(), "offline special registry config failed")
}
func TestCniPluginInstall(t *testing.T) {
cniPluginFile := "/path/to/cni-plugin.tar.gz"
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(utils.Exists, func(path string) bool {
return path == "/opt/cni/bin/bridge"
})
patches.ApplyFunc(os.MkdirAll, func(path string, perm os.FileMode) error {
return nil
})
patches.ApplyFunc(utils.UnTar, func(archive, target string) error {
return nil
})
patches.ApplyFunc(log.SteppedInfo, func(stepName string, args ...any) {})
err := CniPluginInstall(cniPluginFile)
assert.NoError(t, err)
}
func TestCniPluginInstallAlreadyExists(t *testing.T) {
cniPluginFile := "/path/to/cni-plugin.tar.gz"
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(utils.Exists, func(path string) bool {
return path == "/opt/cni/bin/bridge"
})
patches.ApplyFunc(utils.UnTar, func(archive, target string) error {
return nil
})
err := CniPluginInstall(cniPluginFile)
assert.NoError(t, err)
}
func TestCniPluginInstallError(t *testing.T) {
cniPluginFile := "/path/to/cni-plugin.tar.gz"
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(utils.Exists, func(path string) bool {
return false
})
patches.ApplyFunc(os.MkdirAll, func(path string, perm os.FileMode) error {
return nil
})
patches.ApplyFunc(utils.UnTar, func(archive, target string) error {
return fmt.Errorf("untar failed")
})
err := CniPluginInstall(cniPluginFile)
assert.Error(t, err)
}