*
* 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 agent
import (
"fmt"
"io"
"os"
"strings"
"testing"
"github.com/agiledragon/gomonkey/v2"
"github.com/stretchr/testify/assert"
agentv1beta1 "gopkg.openfuyao.cn/cluster-api-provider-bke/api/bkeagent/v1beta1"
"gopkg.openfuyao.cn/cluster-api-provider-bke/common/ntp"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
dynamicFake "k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/kubernetes"
fake "k8s.io/client-go/kubernetes/fake"
"sigs.k8s.io/yaml"
"gopkg.openfuyao.cn/bkeadm/pkg/executor/k8s"
"gopkg.openfuyao.cn/bkeadm/pkg/global"
)
const (
testZeroValue = 0
testOneValue = 1
testTwoValue = 2
testDefaultPortNumber = 123
)
func TestBuildNodeSelector(t *testing.T) {
tests := []struct {
name string
nodes string
expected map[string]string
}{
{
name: "single node",
nodes: "node1",
expected: map[string]string{"node1": "node1"},
},
{
name: "multiple nodes",
nodes: "node1,node2,node3",
expected: map[string]string{"node1": "node1", "node2": "node2", "node3": "node3"},
},
{
name: "empty nodes",
nodes: "",
expected: map[string]string{"": ""},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
op := &Options{Nodes: tt.nodes}
result := op.buildNodeSelector()
assert.Equal(t, tt.expected, result.MatchLabels)
})
}
}
func TestBuildCommand(t *testing.T) {
op := &Options{
Name: "test-command",
Nodes: "node1,node2",
}
cmd := op.buildCommand()
assert.Equal(t, "test-command", cmd.GetName())
assert.Equal(t, metav1.NamespaceDefault, cmd.GetNamespace())
assert.Equal(t, "Command", cmd.GetObjectKind().GroupVersionKind().Kind)
assert.Equal(t, agentv1beta1.GroupVersion.Group, cmd.GroupVersionKind().Group)
assert.Equal(t, agentv1beta1.GroupVersion.Version, cmd.GroupVersionKind().Version)
expectedAnnotations := map[string]string{annotationKey: annotationValue}
assert.Equal(t, expectedAnnotations, cmd.GetAnnotations())
expectedNodeSelector := &metav1.LabelSelector{
MatchLabels: map[string]string{"node1": "node1", "node2": "node2"},
}
assert.Equal(t, expectedNodeSelector, cmd.Spec.NodeSelector)
assert.False(t, cmd.Spec.Suspend)
assert.Empty(t, cmd.Spec.Commands)
}
func TestApplyCommand(t *testing.T) {
op := &Options{
Name: "test-cmd",
Command: "echo hello",
}
cmd := op.buildCommand()
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*Options).createConfigMapFromFile, func(o *Options, c *agentv1beta1.Command) error {
return nil
})
patches.ApplyFunc((*Options).installCommand, func(o *Options, c *agentv1beta1.Command) error {
return nil
})
err := op.applyCommand(&cmd)
assert.NoError(t, err)
assert.Len(t, cmd.Spec.Commands, testOneValue)
assert.Equal(t, "command", cmd.Spec.Commands[testZeroValue].ID)
assert.Equal(t, []string{"echo hello"}, cmd.Spec.Commands[testZeroValue].Command)
assert.Equal(t, agentv1beta1.CommandShell, cmd.Spec.Commands[testZeroValue].Type)
op2 := &Options{
Name: "test-file",
File: "/path/to/file",
}
cmd2 := op2.buildCommand()
err2 := op2.applyCommand(&cmd2)
assert.NoError(t, err2)
}
func TestInstallCommand(t *testing.T) {
op := &Options{}
cmd := &agentv1beta1.Command{
ObjectMeta: metav1.ObjectMeta{
Name: "test-install",
},
}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*k8s.Client).InstallYaml, func(c *k8s.Client, filename string, data map[string]string, template string) error {
content, err := os.ReadFile(filename)
if err != nil {
return err
}
var parsedCmd agentv1beta1.Command
err = yaml.Unmarshal(content, &parsedCmd)
if err != nil {
return err
}
assert.Equal(t, cmd.Name, parsedCmd.Name)
return nil
})
patches.ApplyFunc(os.WriteFile, func(name string, data []byte, perm os.FileMode) error {
if !strings.HasSuffix(name, ".yaml") {
return fmt.Errorf("filename should end with .yaml")
}
return nil
})
if global.K8s == nil {
global.K8s = &k8s.Client{}
}
err := op.installCommand(cmd)
assert.Error(t, err)
}
func TestExec(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*Options).buildCommand, func(o *Options) agentv1beta1.Command {
return agentv1beta1.Command{
ObjectMeta: metav1.ObjectMeta{
Name: "test-exec",
},
}
})
applyCalled := false
patches.ApplyFunc((*Options).applyCommand, func(o *Options, cmd *agentv1beta1.Command) error {
applyCalled = true
return nil
})
op := &Options{Name: "test-exec"}
op.Exec()
assert.True(t, applyCalled, "applyCommand should have been called")
}
func TestList(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(global.ListK8sResources, func(gvr schema.GroupVersionResource, list interface{}) error {
commandList, ok := list.(*agentv1beta1.CommandList)
if !ok {
return fmt.Errorf("wrong type")
}
now := metav1.Now()
commandList.Items = []agentv1beta1.Command{
{
ObjectMeta: metav1.ObjectMeta{
Name: "test-command",
Namespace: metav1.NamespaceDefault,
},
Spec: agentv1beta1.CommandSpec{
Suspend: false,
},
Status: map[string]*agentv1beta1.CommandStatus{
"node1": &agentv1beta1.CommandStatus{
LastStartTime: &now,
CompletionTime: &now,
Phase: "Completed",
Status: "Success",
},
},
},
}
return nil
})
patches.ApplyFunc(fmt.Fprintln, func(w io.Writer, a ...interface{}) (n int, err error) {
return 0, nil
})
patches.ApplyFunc(fmt.Fprintf, func(w io.Writer, format string, a ...interface{}) (n int, err error) {
return 0, nil
})
op := &Options{}
op.List()
}
func TestInfo(t *testing.T) {
tests := []struct {
name string
args []string
mockSplit func(string, string) []string
mockDynamicClient func() dynamic.Interface
expectError bool
}{
{
name: "valid argument format",
args: []string{"default/test-command"},
mockDynamicClient: func() dynamic.Interface {
return nil
},
expectError: false,
},
{
name: "invalid argument format",
args: []string{"invalid-format"},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockClient := &k8s.MockK8sClient{}
global.K8s = mockClient
op := &Options{Args: tt.args}
if !tt.expectError {
testCommand := &agentv1beta1.Command{
TypeMeta: metav1.TypeMeta{
Kind: "Command",
APIVersion: "agent.bke.bocloud.com/v1beta1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-command",
Namespace: "default",
},
Spec: agentv1beta1.CommandSpec{
Suspend: false,
},
}
unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(testCommand)
assert.NoError(t, err)
workloadUnstructured := &unstructured.Unstructured{Object: unstructuredObj}
scheme := runtime.NewScheme()
baseClient := dynamicFake.NewSimpleDynamicClient(scheme)
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*k8s.MockK8sClient).GetDynamicClient, func(m *k8s.MockK8sClient) dynamic.Interface {
return baseClient
})
fullClient := dynamicFake.NewSimpleDynamicClientWithCustomListKinds(
scheme,
map[schema.GroupVersionResource]string{
gvr: "CommandList",
},
workloadUnstructured,
)
patches.ApplyFunc((*k8s.MockK8sClient).GetDynamicClient, func(m *k8s.MockK8sClient) dynamic.Interface {
return fullClient
})
}
op.Info()
if tt.expectError {
assert.True(t, true)
} else {
assert.True(t, true)
}
})
}
}
func TestRemove(t *testing.T) {
tests := []struct {
name string
args []string
expectError bool
}{
{
name: "valid argument format",
args: []string{"default/test-command"},
expectError: false,
},
{
name: "invalid argument format",
args: []string{"invalid-format"},
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
op := &Options{Args: tt.args}
op.Remove()
})
}
}
func TestSyncTime(t *testing.T) {
tests := []struct {
name string
args []string
mockDate func(string) error
minManifestsImageArgs string
containerWaitSeconds string
}{
{
name: "sync with default NTP server",
args: []string{},
mockDate: func(server string) error {
return nil
},
},
{
name: "sync with custom NTP server",
args: []string{"custom.ntp.server:123"},
mockDate: func(server string) error {
assert.Equal(t, "custom.ntp.server:123", server)
return nil
},
},
{
name: "sync with retry on failure",
args: []string{"ntp.server:123"},
mockDate: func(server string) error {
return fmt.Errorf("connection failed")
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
op := &Options{Args: tt.args}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(ntp.Date, tt.mockDate)
op.SyncTime()
assert.True(t, true)
})
}
}
func TestCreateConfigMapFromFile(t *testing.T) {
tests := []struct {
name string
fileContent string
filePath string
setupMocks func(*gomonkey.Patches) *k8s.MockK8sClient
restoreGlobal func()
expectError bool
expectedErrMsg string
}{
{
name: "successful configmap creation",
fileContent: "test file content",
filePath: "/tmp/test-file.txt",
setupMocks: func(patches *gomonkey.Patches) *k8s.MockK8sClient {
patches.ApplyFunc(os.ReadFile, func(name string) ([]byte, error) {
return []byte("test file content"), nil
})
mockK8sClient := &k8s.MockK8sClient{}
clientset := fake.NewSimpleClientset()
patches.ApplyFunc((*k8s.MockK8sClient).GetClient, func(_ *k8s.MockK8sClient) kubernetes.Interface {
return clientset
})
return mockK8sClient
},
expectError: false,
},
{
name: "file read error",
fileContent: "",
filePath: "/nonexistent/file.txt",
setupMocks: func(patches *gomonkey.Patches) *k8s.MockK8sClient {
patches.ApplyFunc(os.ReadFile, func(name string) ([]byte, error) {
return nil, fmt.Errorf("file not found")
})
return nil
},
expectError: true,
expectedErrMsg: "file not found",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
originalK8s := global.K8s
defer func() {
global.K8s = originalK8s
}()
patches := gomonkey.NewPatches()
defer patches.Reset()
var mockK8sClient *k8s.MockK8sClient
if tt.setupMocks != nil {
mockK8sClient = tt.setupMocks(patches)
if mockK8sClient != nil {
global.K8s = mockK8sClient
}
}
op := &Options{
Name: "test-configmap",
File: tt.filePath,
}
cmd := &agentv1beta1.Command{
Spec: agentv1beta1.CommandSpec{
Commands: []agentv1beta1.ExecCommand{},
},
}
err := op.createConfigMapFromFile(cmd)
if tt.expectError {
assert.Error(t, err)
if tt.expectedErrMsg != "" {
assert.Contains(t, err.Error(), tt.expectedErrMsg)
}
} else {
assert.NoError(t, err)
assert.Len(t, cmd.Spec.Commands, testOneValue)
assert.Equal(t, "file", cmd.Spec.Commands[0].ID)
assert.Equal(t, agentv1beta1.CommandKubernetes, cmd.Spec.Commands[0].Type)
expectedCommand := []string{fmt.Sprintf("configmap:%s/%s:rx:shell", metav1.NamespaceDefault, op.Name)}
assert.Equal(t, expectedCommand, cmd.Spec.Commands[0].Command)
}
})
}
}