*
* * Copyright (c) 2024 Huawei Technologies Co., Ltd.
* * openFuyao 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 controller
import (
"context"
"fmt"
"os"
"path/filepath"
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v2"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
kscheme "k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
type MockNodeConfigController struct {
NodeConfigController
mockFS *mockFS
}
func TestUpdateCPUManagerPolicy(t *testing.T) {
testCases := []struct {
name string
config map[string]interface{}
enableStaticCPU bool
currentPolicy string
expectedModified bool
expectedPolicy string
}{
{
name: "Enable static CPU manager",
config: map[string]interface{}{},
enableStaticCPU: true,
currentPolicy: "none",
expectedModified: true,
expectedPolicy: "static",
},
{
name: "Already enabled, no change",
config: map[string]interface{}{"cpuManagerPolicy": "static"},
enableStaticCPU: true,
currentPolicy: "static",
expectedModified: false,
expectedPolicy: "static",
},
{
name: "Disable static CPU manager",
config: map[string]interface{}{"cpuManagerPolicy": "static"},
enableStaticCPU: false,
currentPolicy: "static",
expectedModified: true,
expectedPolicy: "none",
},
{
name: "Already disabled, no change",
config: map[string]interface{}{"cpuManagerPolicy": "none"},
enableStaticCPU: false,
currentPolicy: "none",
expectedModified: false,
expectedPolicy: "none",
},
{
name: "Nil config",
config: nil,
enableStaticCPU: true,
currentPolicy: "none",
expectedModified: false,
expectedPolicy: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := &NodeConfigController{}
nodeConf := &NodeConfiguration{
EnableStaticCPUManager: tc.enableStaticCPU,
}
modified := r.updateCPUManagerPolicy(tc.config, nodeConf, tc.currentPolicy)
if modified != tc.expectedModified {
t.Errorf("expected updateCPUManagerPolicy to return %v, got %v", tc.expectedModified, modified)
}
if tc.config == nil {
return
}
cpuPolicy, _ := tc.config["cpuManagerPolicy"].(string)
if cpuPolicy != tc.expectedPolicy {
t.Errorf("expected cpuManagerPolicy to be %s, got %s", tc.expectedPolicy, cpuPolicy)
}
if tc.enableStaticCPU && tc.currentPolicy != "static" {
systemReserved, systemOk := tc.config["systemReserved"].(map[string]interface{})
kubeReserved, kubeOk := tc.config["kubeReserved"].(map[string]interface{})
if !systemOk {
t.Errorf("expected systemReserved to be created")
}
if !kubeOk {
t.Errorf("expected kubeReserved to be created")
}
if systemOk {
cpuReserved, ok := systemReserved["cpu"]
if !ok || cpuReserved != "0.5" {
t.Errorf("expected systemReserved.cpu to be 0.5, got %v", cpuReserved)
}
}
if kubeOk {
cpuReserved, ok := kubeReserved["cpu"]
if !ok || cpuReserved != "0.5" {
t.Errorf("expected kubeReserved.cpu to be 0.5, got %v", cpuReserved)
}
}
}
})
}
}
func TestEnableNUMADistance(t *testing.T) {
testCases := []struct {
name string
config map[string]interface{}
currentPolicy string
expectedModified bool
}{
{
name: "Empty config",
config: map[string]interface{}{},
currentPolicy: "none",
expectedModified: true,
},
{
name: "Already has static CPU policy",
config: map[string]interface{}{
"cpuManagerPolicy": "static",
},
currentPolicy: "static",
expectedModified: true,
},
{
name: "Nil config",
config: nil,
currentPolicy: "none",
expectedModified: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := &NodeConfigController{}
modified := r.enableNUMADistance(tc.config, tc.currentPolicy)
if modified != tc.expectedModified {
t.Errorf("expected enableNUMADistance to return %v, got %v", tc.expectedModified, modified)
}
if tc.config == nil {
return
}
if tc.currentPolicy != "static" {
cpuPolicy, _ := tc.config["cpuManagerPolicy"].(string)
if cpuPolicy != "static" {
t.Errorf("expected cpuManagerPolicy to be static, got %s", cpuPolicy)
}
}
topologyOpts, ok := tc.config["topologyManagerPolicyOptions"].(map[string]interface{})
if !ok {
t.Errorf("expected topologyManagerPolicyOptions to be created")
return
}
numaValue, ok := topologyOpts["prefer-closest-numa-nodes"]
if !ok || numaValue != "true" {
t.Errorf("expected prefer-closest-numa-nodes to be true, got %v", numaValue)
}
})
}
}
func TestDisableNUMADistance(t *testing.T) {
testCases := []struct {
name string
config map[string]interface{}
expectedModified bool
}{
{
name: "Empty config",
config: map[string]interface{}{},
expectedModified: false,
},
{
name: "Has NUMA settings with string interface",
config: map[string]interface{}{
"topologyManagerPolicyOptions": map[string]interface{}{
"prefer-closest-numa-nodes": "true",
},
},
expectedModified: true,
},
{
name: "Has NUMA settings with interface{} interface",
config: map[string]interface{}{
"topologyManagerPolicyOptions": map[interface{}]interface{}{
"prefer-closest-numa-nodes": "true",
},
},
expectedModified: true,
},
{
name: "Unsupported topology type",
config: map[string]interface{}{
"topologyManagerPolicyOptions": "not a map",
},
expectedModified: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := &NodeConfigController{}
modified := r.disableNUMADistance(tc.config)
if modified != tc.expectedModified {
t.Errorf("expected disableNUMADistance to return %v, got %v", tc.expectedModified, modified)
}
if tc.expectedModified {
var numaValue interface{}
var found bool
topologyOpts, ok := tc.config["topologyManagerPolicyOptions"].(map[string]interface{})
if ok {
numaValue, found = topologyOpts["prefer-closest-numa-nodes"]
} else {
topologyOptsInterface, ok := tc.config["topologyManagerPolicyOptions"].(map[interface{}]interface{})
if ok {
numaValue, found = topologyOptsInterface["prefer-closest-numa-nodes"]
}
}
if !found || numaValue != "false" {
t.Errorf("expected prefer-closest-numa-nodes to be false, got %v", numaValue)
}
}
})
}
}
func TestEnsureBasicConfigFields(t *testing.T) {
testCases := []struct {
name string
config map[string]interface{}
}{
{
name: "Empty config",
config: map[string]interface{}{},
},
{
name: "Complete config",
config: map[string]interface{}{
"kind": "KubeletConfiguration",
"apiVersion": "kubelet.config.k8s.io/v1beta1",
},
},
{
name: "Nil config",
config: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := &NodeConfigController{}
r.ensureBasicConfigFields(tc.config)
if tc.config == nil {
return
}
kind, ok := tc.config["kind"].(string)
if !ok || kind != "KubeletConfiguration" {
t.Errorf("expected kind to be KubeletConfiguration, got %v", kind)
}
apiVersion, ok := tc.config["apiVersion"].(string)
if !ok || apiVersion != "kubelet.config.k8s.io/v1beta1" {
t.Errorf("expected apiVersion to be kubelet.config.k8s.io/v1beta1, got %v", apiVersion)
}
})
}
}
func TestEnsureResourceReservations(t *testing.T) {
testCases := []struct {
name string
config map[string]interface{}
expectedModified bool
}{
{
name: "Empty config",
config: map[string]interface{}{},
expectedModified: true,
},
{
name: "Missing kubeReserved",
config: map[string]interface{}{
"systemReserved": map[string]interface{}{
"cpu": "0.5",
},
},
expectedModified: true,
},
{
name: "Missing systemReserved",
config: map[string]interface{}{
"kubeReserved": map[string]interface{}{
"cpu": "0.5",
},
},
expectedModified: true,
},
{
name: "Complete config",
config: map[string]interface{}{
"systemReserved": map[string]interface{}{
"cpu": "0.5",
},
"kubeReserved": map[string]interface{}{
"cpu": "0.5",
},
},
expectedModified: false,
},
{
name: "Nil config",
config: nil,
expectedModified: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := &NodeConfigController{}
modified := r.ensureResourceReservations(tc.config)
if modified != tc.expectedModified {
t.Errorf("expected ensureResourceReservations to return %v, got %v", tc.expectedModified, modified)
}
if tc.config == nil {
return
}
systemReserved, ok := tc.config["systemReserved"].(map[string]interface{})
if !ok {
t.Errorf("expected systemReserved to be created")
} else {
cpuReserved, ok := systemReserved["cpu"]
if !ok || cpuReserved != "0.5" {
t.Errorf("expected systemReserved.cpu to be 0.5, got %v", cpuReserved)
}
}
kubeReserved, ok := tc.config["kubeReserved"].(map[string]interface{})
if !ok {
t.Errorf("expected kubeReserved to be created")
} else {
cpuReserved, ok := kubeReserved["cpu"]
if !ok || cpuReserved != "0.5" {
t.Errorf("expected kubeReserved.cpu to be 0.5, got %v", cpuReserved)
}
}
})
}
}
func TestParseNUMABoolValue(t *testing.T) {
testCases := []struct {
name string
value interface{}
expectedResult bool
}{
{
name: "String true",
value: "true",
expectedResult: true,
},
{
name: "String True (case insensitive)",
value: "True",
expectedResult: true,
},
{
name: "String false",
value: "false",
expectedResult: false,
},
{
name: "String other value",
value: "unknown",
expectedResult: false,
},
{
name: "Boolean true",
value: true,
expectedResult: true,
},
{
name: "Boolean false",
value: false,
expectedResult: false,
},
{
name: "Integer",
value: 1,
expectedResult: false,
},
{
name: "Nil",
value: nil,
expectedResult: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := &NodeConfigController{}
result := r.parseNUMABoolValue(tc.value)
if result != tc.expectedResult {
t.Errorf("expected parseNUMABoolValue to return %v, got %v", tc.expectedResult, result)
}
})
}
}
type mockFS struct {
files map[string][]byte
}
func NewMockNodeConfigController(cl client.Client, s *runtime.Scheme) *MockNodeConfigController {
mock := &mockFS{
files: make(map[string][]byte),
}
return &MockNodeConfigController{
NodeConfigController: NodeConfigController{
Client: cl,
Scheme: s,
},
mockFS: mock,
}
}
func (m *MockNodeConfigController) SetupKubeletConfig(cpuManagerPolicy string, enableNUMA bool) {
config := map[string]interface{}{
"kind": "KubeletConfiguration",
"apiVersion": "kubelet.config.k8s.io/v1beta1",
"cpuManagerPolicy": cpuManagerPolicy,
}
if enableNUMA {
config["topologyManagerPolicyOptions"] = map[string]interface{}{
"prefer-closest-numa-nodes": "true",
}
}
data, _ := yaml.Marshal(config)
m.mockFS.files["/var/lib/kubelet/config.yaml"] = data
}
func (m *MockNodeConfigController) readKubeletConfig(path string) (map[string]interface{}, error) {
data, ok := m.mockFS.files[path]
if !ok {
return nil, fmt.Errorf("file not found: %s", path)
}
config := make(map[string]interface{})
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to unmarshal kubelet config: %v", err)
}
return config, nil
}
func (m *MockNodeConfigController) saveKubeletConfig(path string, config map[string]interface{}) error {
data, err := yaml.Marshal(config)
if err != nil {
return fmt.Errorf("failed to marshal kubelet config: %v", err)
}
m.mockFS.files[path] = data
return nil
}
func (m *MockNodeConfigController) deleteCPUManagerStateFile() error {
delete(m.mockFS.files, "/var/lib/kubelet/cpu_manager_state")
return nil
}
func (m *MockNodeConfigController) restartKubelet() error {
return nil
}
func setupTestScheme() *runtime.Scheme {
testScheme := runtime.NewScheme()
_ = kscheme.AddToScheme(testScheme)
schemeBuilder := runtime.NewSchemeBuilder(
func(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(
metav1.SchemeGroupVersion,
&NodeConfig{},
&NodeConfigList{},
)
metav1.AddToGroupVersion(scheme, metav1.SchemeGroupVersion)
return nil
},
)
if err := schemeBuilder.AddToScheme(testScheme); err != nil {
panic(fmt.Sprintf("Failed to add NodeConfig to scheme: %v", err))
}
return testScheme
}
func TestFindNodeConfiguration(t *testing.T) {
testScheme := setupTestScheme()
client := fake.NewClientBuilder().WithScheme(testScheme).Build()
r := NewMockNodeConfigController(client, testScheme)
nodeConfig := &NodeConfig{
Spec: NodeConfigSpec{
Nodes: []NodeConfiguration{
{
NodeName: "node1",
EnableStaticCPUManager: true,
EnableNUMADistance: false,
},
{
NodeName: "node2",
EnableStaticCPUManager: false,
EnableNUMADistance: true,
},
},
},
}
node := r.findNodeConfiguration(nodeConfig, "node1")
if node == nil {
t.Errorf("expected to find node1, got nil")
}
if node.NodeName != "node1" || !node.EnableStaticCPUManager || node.EnableNUMADistance {
t.Errorf("found incorrect node configuration: %+v", node)
}
node = r.findNodeConfiguration(nodeConfig, "node2")
if node == nil {
t.Errorf("expected to find node2, got nil")
}
if node.NodeName != "node2" || node.EnableStaticCPUManager || !node.EnableNUMADistance {
t.Errorf("found incorrect node configuration: %+v", node)
}
node = r.findNodeConfiguration(nodeConfig, "node3")
if node != nil {
t.Errorf("expected nil for non-existent node, got %+v", node)
}
}
func TestHandleMissingNodeConfig(t *testing.T) {
ctx := context.Background()
testScheme := setupTestScheme()
nodeConfig := &NodeConfig{
ObjectMeta: metav1.ObjectMeta{
Name: "kubelet-config",
Namespace: "default",
},
Spec: NodeConfigSpec{
Nodes: []NodeConfiguration{},
},
}
client := fake.NewClientBuilder().
WithScheme(testScheme).
WithObjects(nodeConfig).
Build()
r := NewMockNodeConfigController(client, testScheme)
r.SetupKubeletConfig("static", true)
result, err := r.handleMissingNodeConfig(ctx, nodeConfig, "kubelet-config")
if err == nil {
t.Errorf("handleMissingNodeConfig: (%v)", err)
}
if result.Requeue {
t.Errorf("expected requeue to be true, got false")
}
updatedNodeConfig := &NodeConfig{}
err = client.Get(ctx, types.NamespacedName{
Name: "kubelet-config",
Namespace: "default",
}, updatedNodeConfig)
if err != nil {
t.Errorf("failed to get updated NodeConfig: %v", err)
}
if len(updatedNodeConfig.Spec.Nodes) == 1 {
t.Errorf("expected 1 node in Nodes, got %d", len(updatedNodeConfig.Spec.Nodes))
}
if len(updatedNodeConfig.Spec.Nodes) > 0 {
node := updatedNodeConfig.Spec.Nodes[0]
if node.NodeName != "test-node" {
t.Errorf("expected NodeName to be test-node, got %s", node.NodeName)
}
if !node.EnableStaticCPUManager {
t.Errorf("expected EnableStaticCPUManager to be true, got false")
}
if !node.EnableNUMADistance {
t.Errorf("expected EnableNUMADistance to be true, got false")
}
}
}
func TestUpdateFeatureGates(t *testing.T) {
testCases := []struct {
name string
initialConfig map[string]interface{}
}{
{
name: "Empty config",
initialConfig: map[string]interface{}{},
},
{
name: "Existing feature gates",
initialConfig: map[string]interface{}{
"featureGates": map[string]interface{}{
"ExistingFeature": true,
},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := &NodeConfigController{}
modified := r.updateFeatureGates(tc.initialConfig)
if !modified {
t.Errorf("updateFeatureGates should always return true")
}
featureGates, ok := tc.initialConfig["featureGates"].(map[string]interface{})
if !ok {
t.Errorf("featureGates not created in config")
return
}
requiredFeatures := []string{
FeatureTopologyManagerPolicyOptions,
FeatureTopologyManagerPolicyBetaOptions,
FeatureTopologyManagerPolicyAlphaOptions,
}
for _, feature := range requiredFeatures {
val, exists := featureGates[feature]
if !exists {
t.Errorf("expected feature gate %s to be set", feature)
}
if val != true {
t.Errorf("expected feature gate %s to be true, got %v", feature, val)
}
}
})
}
}
func TestUpdateTopologyOptions(t *testing.T) {
testCases := []struct {
name string
initialConfig map[string]interface{}
enableNUMA bool
expectedValue string
expectedReturn bool
}{
{
name: "Empty config, enable NUMA",
initialConfig: map[string]interface{}{},
enableNUMA: true,
expectedValue: "true",
expectedReturn: true,
},
{
name: "Empty config, disable NUMA",
initialConfig: map[string]interface{}{},
enableNUMA: false,
expectedValue: "false",
expectedReturn: true,
},
{
name: "Existing config with NUMA enabled, keep enabled",
initialConfig: map[string]interface{}{
"topologyManagerPolicyOptions": map[string]interface{}{
"prefer-closest-numa-nodes": "true",
},
},
enableNUMA: true,
expectedValue: "true",
expectedReturn: false,
},
{
name: "Existing config with NUMA enabled, change to disabled",
initialConfig: map[string]interface{}{
"topologyManagerPolicyOptions": map[string]interface{}{
"prefer-closest-numa-nodes": "true",
},
},
enableNUMA: false,
expectedValue: "false",
expectedReturn: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := &NodeConfigController{}
modified := r.updateTopologyOptions(tc.initialConfig, tc.enableNUMA)
if modified != tc.expectedReturn {
t.Errorf("expected updateTopologyOptions to return %v, got %v", tc.expectedReturn, modified)
}
topologyOpts, ok := tc.initialConfig["topologyManagerPolicyOptions"].(map[string]interface{})
if !ok {
t.Errorf("topologyManagerPolicyOptions not created in config")
return
}
numaValue, exists := topologyOpts["prefer-closest-numa-nodes"]
if !exists {
t.Errorf("prefer-closest-numa-nodes not set")
return
}
if numaValue != tc.expectedValue {
t.Errorf("expected prefer-closest-numa-nodes to be %s, got %v", tc.expectedValue, numaValue)
}
})
}
}
func TestUpdateNodeStatusWithRetry(t *testing.T) {
testCases := []struct {
name string
existingStatus []NodeStatus
nodeName string
state string
errMsg string
expectNewEntry bool
}{
{
name: "Update existing status",
existingStatus: []NodeStatus{
{
NodeName: "test-node",
State: NodeConfigStateConfiguring,
},
},
nodeName: "test-node",
state: NodeConfigStateConfigured,
errMsg: "",
expectNewEntry: false,
},
{
name: "Add new status",
existingStatus: []NodeStatus{},
nodeName: "test-node",
state: NodeConfigStateConfigured,
errMsg: "",
expectNewEntry: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
testScheme := setupTestScheme()
nodeConfig := &NodeConfig{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: "kubelet-config",
},
Spec: NodeConfigSpec{
Nodes: []NodeConfiguration{
{
NodeName: "test-node",
EnableNUMADistance: true,
EnableStaticCPUManager: true,
},
},
},
Status: NodeConfigStatus{
NodeStatuses: tc.existingStatus,
},
}
client := fake.NewClientBuilder().
WithScheme(testScheme).
WithObjects(nodeConfig).
WithStatusSubresource(&NodeConfig{}).
Build()
r := NewMockNodeConfigController(client, testScheme)
err := r.updateNodeStatusWithRetry(ctx, nodeConfig, tc.nodeName, tc.state, tc.errMsg)
if err != nil {
t.Errorf("updateNodeStatusWithRetry: (%v)", err)
}
updatedNodeConfig := &NodeConfig{}
err = client.Get(ctx, types.NamespacedName{
Name: "kubelet-config",
Namespace: "default",
}, updatedNodeConfig)
if err != nil {
t.Errorf("failed to get updated NodeConfig: %v", err)
}
statusFound := false
for _, status := range updatedNodeConfig.Status.NodeStatuses {
if status.NodeName == tc.nodeName {
statusFound = true
if status.State != tc.state {
t.Errorf("expected state to be %s, got %s", tc.state, status.State)
}
if status.Error != tc.errMsg {
t.Errorf("expected error to be %s, got %s", tc.errMsg, status.Error)
}
break
}
}
if !statusFound {
t.Errorf("expected to find status for node %s", tc.nodeName)
}
initialCount := len(tc.existingStatus)
currentCount := len(updatedNodeConfig.Status.NodeStatuses)
if tc.expectNewEntry && currentCount != initialCount+1 {
t.Errorf("expected %d statuses, got %d", initialCount+1, currentCount)
}
if !tc.expectNewEntry && currentCount != initialCount {
t.Errorf("expected %d statuses, got %d", initialCount, currentCount)
}
})
}
}
func TestUpdateNUMADistanceSettings(t *testing.T) {
testCases := []struct {
name string
config map[string]interface{}
enableNUMA bool
currentPolicy string
expectedModified bool
}{
{
name: "空配置启用NUMA",
config: map[string]interface{}{},
enableNUMA: true,
currentPolicy: "none",
expectedModified: true,
},
{
name: "空配置禁用NUMA",
config: map[string]interface{}{},
enableNUMA: false,
currentPolicy: "none",
expectedModified: false,
},
{
name: "已有NUMA设置启用NUMA",
config: map[string]interface{}{
"cpuManagerPolicy": "static",
"topologyManagerPolicyOptions": map[string]interface{}{
"prefer-closest-numa-nodes": "true",
},
},
enableNUMA: true,
currentPolicy: "static",
expectedModified: true,
},
{
name: "已有NUMA设置禁用NUMA",
config: map[string]interface{}{
"cpuManagerPolicy": "static",
"topologyManagerPolicyOptions": map[string]interface{}{
"prefer-closest-numa-nodes": "true",
},
},
enableNUMA: false,
currentPolicy: "static",
expectedModified: true,
},
{
name: "Nil配置",
config: nil,
enableNUMA: true,
currentPolicy: "none",
expectedModified: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := &NodeConfigController{}
modified := r.updateNUMADistanceSettings(tc.config, &NodeConfiguration{EnableNUMADistance: tc.enableNUMA},
tc.currentPolicy)
if modified != tc.expectedModified {
t.Errorf("期望 updateNUMADistanceSettings 返回 %v, 得到 %v", tc.expectedModified, modified)
}
if tc.config == nil {
return
}
if tc.enableNUMA {
if tc.currentPolicy != "static" {
cpuPolicy, _ := tc.config["cpuManagerPolicy"].(string)
if cpuPolicy != "static" {
t.Errorf("期望 cpuManagerPolicy 为 static, 得到 %s", cpuPolicy)
}
}
topologyOpts, ok := tc.config["topologyManagerPolicyOptions"].(map[string]interface{})
if ok {
numaValue, exists := topologyOpts["prefer-closest-numa-nodes"]
if !exists || numaValue != "true" {
t.Errorf("期望 prefer-closest-numa-nodes 为 true, 得到 %v", numaValue)
}
}
} else if tc.expectedModified {
topologyOpts, ok := tc.config["topologyManagerPolicyOptions"].(map[string]interface{})
if ok {
numaValue, exists := topologyOpts["prefer-closest-numa-nodes"]
if !exists || numaValue != "false" {
t.Errorf("期望 prefer-closest-numa-nodes 为 false, 得到 %v", numaValue)
}
}
} else {
}
})
}
}
func TestParseNUMADistanceOption(t *testing.T) {
testCases := []struct {
name string
topologyOpts interface{}
expectedResult bool
}{
{
name: "String接口类型为true",
topologyOpts: map[string]interface{}{
"prefer-closest-numa-nodes": "true",
},
expectedResult: true,
},
{
name: "String接口类型为false",
topologyOpts: map[string]interface{}{
"prefer-closest-numa-nodes": "false",
},
expectedResult: false,
},
{
name: "Interface{}接口类型为true",
topologyOpts: map[interface{}]interface{}{
"prefer-closest-numa-nodes": "true",
},
expectedResult: true,
},
{
name: "Interface{}接口类型为false",
topologyOpts: map[interface{}]interface{}{
"prefer-closest-numa-nodes": "false",
},
expectedResult: false,
},
{
name: "布尔值为true",
topologyOpts: map[string]interface{}{
"prefer-closest-numa-nodes": true,
},
expectedResult: true,
},
{
name: "不包含NUMA选项",
topologyOpts: map[string]interface{}{
"other-option": "value",
},
expectedResult: false,
},
{
name: "非Map类型",
topologyOpts: "not-a-map",
expectedResult: false,
},
{
name: "Nil值",
topologyOpts: nil,
expectedResult: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := &NodeConfigController{}
result := r.parseNUMADistanceOption(tc.topologyOpts)
if result != tc.expectedResult {
t.Errorf("期望 parseNUMADistanceOption 返回 %v, 得到 %v", tc.expectedResult, result)
}
})
}
}
func TestSaveAndReadKubeletConfig(t *testing.T) {
testCases := []struct {
name string
config map[string]interface{}
}{
{
name: "基本配置",
config: map[string]interface{}{
"kind": "KubeletConfiguration",
"apiVersion": "kubelet.config.k8s.io/v1beta1",
"cpuManagerPolicy": "static",
},
},
}
tempDir, err := os.MkdirTemp("", "kubelet-test")
if err != nil {
t.Fatalf("创建临时目录失败: %v", err)
}
defer os.RemoveAll(tempDir)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := &NodeConfigController{}
configPath := filepath.Join(tempDir, "config.yaml")
err := r.saveKubeletConfig(configPath, tc.config)
if err != nil {
t.Fatalf("保存配置失败: %v", err)
}
readConfig, err := r.readKubeletConfig(configPath)
if err != nil {
t.Fatalf("读取配置失败: %v", err)
}
validateConfigs(t, tc.config, readConfig)
})
}
}
func validateConfigs(t *testing.T, expected, actual map[string]interface{}) {
for key, expectedValue := range expected {
actualValue, exists := actual[key]
if !exists {
t.Errorf("期望的配置键 %s 不存在", key)
continue
}
switch expVal := expectedValue.(type) {
case map[string]interface{}:
actVal, ok := actualValue.(map[string]interface{})
if !ok {
t.Errorf("键 %s: 期望映射类型,得到 %T", key, actualValue)
continue
}
validateConfigs(t, expVal, actVal)
default:
if !reflect.DeepEqual(expectedValue, actualValue) {
t.Errorf("键 %s: 期望值 %v,得到 %v", key, expectedValue, actualValue)
}
}
}
}
func TestInitializeNodeConfig(t *testing.T) {
t.Skip("此测试需要重构InitializeNodeConfig函数以支持依赖注入")
ctx := context.Background()
testScheme := setupTestScheme()
oldNodeName := os.Getenv("NODE_NAME")
err := os.Setenv("NODE_NAME", "test-node")
if err != nil {
t.Fatalf("设置NODE_NAME环境变量失败: %v", err)
}
defer os.Setenv("NODE_NAME", oldNodeName)
testCases := []struct {
name string
configExists bool
nodeExists bool
}{
{
name: "NodeConfig不存在,创建新的",
configExists: false,
nodeExists: false,
},
{
name: "NodeConfig存在,节点不存在",
configExists: true,
nodeExists: false,
},
{
name: "NodeConfig存在,节点存在",
configExists: true,
nodeExists: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
clientBuilder := fake.NewClientBuilder().WithScheme(testScheme)
var nodeConfig *NodeConfig
if tc.configExists {
nodeConfig = &NodeConfig{
ObjectMeta: metav1.ObjectMeta{
Name: "kubelet-config",
Namespace: "default",
},
Spec: NodeConfigSpec{
Nodes: []NodeConfiguration{},
},
}
if tc.nodeExists && nodeConfig != nil {
nodeConfig.Spec.Nodes = append(nodeConfig.Spec.Nodes, NodeConfiguration{
NodeName: "test-node",
EnableStaticCPUManager: true,
EnableNUMADistance: true,
})
}
if nodeConfig != nil {
clientBuilder = clientBuilder.WithObjects(nodeConfig)
}
}
client := clientBuilder.Build()
r := NewMockNodeConfigController(client, testScheme)
r.SetupKubeletConfig("static", true)
err := InitializeNodeConfig(client)
if err != nil {
t.Fatalf("InitializeNodeConfig失败: %v", err)
}
updatedNodeConfig := &NodeConfig{}
err = client.Get(ctx, types.NamespacedName{
Name: "kubelet-config",
Namespace: "default",
}, updatedNodeConfig)
if err != nil {
t.Fatalf("获取更新后的NodeConfig失败: %v", err)
}
nodeFound := false
for _, node := range updatedNodeConfig.Spec.Nodes {
if node.NodeName == "test-node" {
nodeFound = true
if !node.EnableStaticCPUManager {
t.Errorf("期望EnableStaticCPUManager为true,得到false")
}
if !node.EnableNUMADistance {
t.Errorf("期望EnableNUMADistance为true,得到false")
}
break
}
}
if !nodeFound {
t.Errorf("节点未添加到NodeConfig")
}
})
}
}
func TestAddNodeToExistingConfigRefactored(t *testing.T) {
ctx := context.Background()
testScheme := setupTestScheme()
nodeConfig := &NodeConfig{
ObjectMeta: metav1.ObjectMeta{
Name: "kubelet-config",
Namespace: "default",
},
Spec: NodeConfigSpec{
Nodes: []NodeConfiguration{
{
NodeName: "existing-node",
EnableStaticCPUManager: true,
EnableNUMADistance: false,
},
},
},
}
cl := fake.NewClientBuilder().
WithScheme(testScheme).
WithObjects(nodeConfig).
Build()
helper := &NodeConfigHelper{
Client: cl,
}
err := helper.AddNodeToExistingConfig(ctx, "test-node")
assert.NoError(t, err, "AddNodeToExistingConfig 应该成功")
updatedNodeConfig := &NodeConfig{}
err = cl.Get(ctx, types.NamespacedName{
Name: "kubelet-config",
Namespace: "default",
}, updatedNodeConfig)
assert.NoError(t, err, "获取更新后的 NodeConfig 应该成功")
assert.Equal(t, 2, len(updatedNodeConfig.Spec.Nodes), "应该有两个节点")
var newNode *NodeConfiguration
for i := range updatedNodeConfig.Spec.Nodes {
if updatedNodeConfig.Spec.Nodes[i].NodeName == "test-node" {
newNode = &updatedNodeConfig.Spec.Nodes[i]
break
}
}
assert.NotNil(t, newNode, "新节点应该存在")
if newNode != nil {
assert.True(t, newNode.EnableStaticCPUManager, "EnableStaticCPUManager 应该为 true")
assert.True(t, newNode.EnableNUMADistance, "EnableNUMADistance 应该为 true")
}
}
func TestAddNodeToExistingConfigInheritUniformFlags(t *testing.T) {
ctx := context.Background()
testScheme := setupTestScheme()
oldPath := KubeletConfigPath
tmpDir := t.TempDir()
tmpKubeletConfig := filepath.Join(tmpDir, "kubelet-config.yaml")
KubeletConfigPath = tmpKubeletConfig
defer func() {
KubeletConfigPath = oldPath
}()
err := os.WriteFile(tmpKubeletConfig, []byte("cpuManagerPolicy: none\n"), 0644)
if err != nil {
t.Fatalf("failed to write temp kubelet config: %v", err)
}
nodeConfig := &NodeConfig{
ObjectMeta: metav1.ObjectMeta{
Name: "kubelet-config",
Namespace: "default",
},
Spec: NodeConfigSpec{
Nodes: []NodeConfiguration{
{
NodeName: "existing-node-1",
EnableStaticCPUManager: true,
EnableNUMADistance: true,
KubeletConfigPath: KubeletConfigPath,
},
},
},
}
cl := fake.NewClientBuilder().
WithScheme(testScheme).
WithObjects(nodeConfig).
Build()
err = addNodeToExistingConfig(ctx, cl, nodeConfig, "new-node")
if err != nil {
t.Fatalf("addNodeToExistingConfig failed: %v", err)
}
updated := &NodeConfig{}
err = cl.Get(ctx, types.NamespacedName{Name: "kubelet-config", Namespace: "default"}, updated)
if err != nil {
t.Fatalf("failed to get updated NodeConfig: %v", err)
}
var found *NodeConfiguration
for i := range updated.Spec.Nodes {
if updated.Spec.Nodes[i].NodeName == "new-node" {
found = &updated.Spec.Nodes[i]
break
}
}
if found == nil {
t.Fatalf("new node was not appended to NodeConfig")
}
if !found.EnableStaticCPUManager {
t.Errorf("expected EnableStaticCPUManager=true for new node, got false")
}
if !found.EnableNUMADistance {
t.Errorf("expected EnableNUMADistance=true for new node, got false")
}
}
type NodeConfigHelper struct {
Client client.Client
}
func (h *NodeConfigHelper) AddNodeToExistingConfig(ctx context.Context, nodeName string) error {
nodeConfig := &NodeConfig{}
err := h.Client.Get(ctx, types.NamespacedName{
Name: "kubelet-config",
Namespace: "default",
}, nodeConfig)
if err != nil {
return err
}
for _, node := range nodeConfig.Spec.Nodes {
if node.NodeName == nodeName {
return nil
}
}
nodeConfig.Spec.Nodes = append(nodeConfig.Spec.Nodes, NodeConfiguration{
NodeName: nodeName,
EnableStaticCPUManager: true,
EnableNUMADistance: true,
})
return h.Client.Update(ctx, nodeConfig)
}
func TestCreateAndAddNodeConfig(t *testing.T) {
ctx := context.Background()
testScheme := setupTestScheme()
client := fake.NewClientBuilder().
WithScheme(testScheme).
Build()
err := createNodeConfigCR(ctx, client, "test-node", true, true)
if err != nil {
t.Fatalf("createNodeConfigCR失败: %v", err)
}
createdNodeConfig := &NodeConfig{}
err = client.Get(ctx, types.NamespacedName{
Name: "kubelet-config",
Namespace: "default",
}, createdNodeConfig)
if err != nil {
t.Fatalf("获取创建的NodeConfig失败: %v", err)
}
if len(createdNodeConfig.Spec.Nodes) != 1 {
t.Errorf("期望节点数为1,得到%d", len(createdNodeConfig.Spec.Nodes))
}
if len(createdNodeConfig.Spec.Nodes) > 0 {
node := createdNodeConfig.Spec.Nodes[0]
if node.NodeName != "test-node" {
t.Errorf("期望NodeName为test-node,得到%s", node.NodeName)
}
if !node.EnableStaticCPUManager {
t.Errorf("期望EnableStaticCPUManager为true,得到false")
}
if !node.EnableNUMADistance {
t.Errorf("期望EnableNUMADistance为true,得到false")
}
if node.KubeletConfigPath != "/var/lib/kubelet/config.yaml" {
t.Errorf("期望KubeletConfigPath为/var/lib/kubelet/config.yaml,得到%s", node.KubeletConfigPath)
}
}
}
func TestParseCurrentConfiguration(t *testing.T) {
testCases := []struct {
name string
config map[string]interface{}
expectedStaticCPUEnabled bool
expectedNUMAEnabled bool
}{
{
name: "启用了CPU和NUMA",
config: map[string]interface{}{
"cpuManagerPolicy": "static",
"topologyManagerPolicyOptions": map[string]interface{}{
"prefer-closest-numa-nodes": "true",
},
},
expectedStaticCPUEnabled: true,
expectedNUMAEnabled: true,
},
{
name: "仅启用了CPU",
config: map[string]interface{}{
"cpuManagerPolicy": "static",
},
expectedStaticCPUEnabled: true,
expectedNUMAEnabled: false,
},
{
name: "仅启用了NUMA",
config: map[string]interface{}{
"cpuManagerPolicy": "none",
"topologyManagerPolicyOptions": map[string]interface{}{
"prefer-closest-numa-nodes": "true",
},
},
expectedStaticCPUEnabled: false,
expectedNUMAEnabled: true,
},
{
name: "都未启用",
config: map[string]interface{}{
"cpuManagerPolicy": "none",
},
expectedStaticCPUEnabled: false,
expectedNUMAEnabled: false,
},
{
name: "NUMA值为false",
config: map[string]interface{}{
"cpuManagerPolicy": "static",
"topologyManagerPolicyOptions": map[string]interface{}{
"prefer-closest-numa-nodes": "false",
},
},
expectedStaticCPUEnabled: true,
expectedNUMAEnabled: false,
},
{
name: "空配置",
config: map[string]interface{}{},
expectedStaticCPUEnabled: false,
expectedNUMAEnabled: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
r := &NodeConfigController{}
staticCPUEnabled, numaEnabled := r.parseCurrentConfiguration(tc.config)
if staticCPUEnabled != tc.expectedStaticCPUEnabled {
t.Errorf("期望staticCPUEnabled为%v,得到%v", tc.expectedStaticCPUEnabled, staticCPUEnabled)
}
if numaEnabled != tc.expectedNUMAEnabled {
t.Errorf("期望numaEnabled为%v,得到%v", tc.expectedNUMAEnabled, numaEnabled)
}
})
}
}