mirror of
https://github.com/kubernetes-sigs/descheduler.git
synced 2026-01-26 13:29:11 +01:00
feat(profile): inject a plugin instance ID to each built plugin
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@@ -542,3 +543,325 @@ func TestProfileExtensionPointOrdering(t *testing.T) {
|
||||
t.Errorf("check for balance invocation order failed. Results are not deep equal. mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
// verifyInstanceIDsMatch verifies that instance IDs captured at creation, deschedule, and balance match
|
||||
func verifyInstanceIDsMatch(t *testing.T, profileInstanceID string, pluginNames []string, creationIDs, descheduleIDs, balanceIDs map[string]string) {
|
||||
for _, pluginName := range pluginNames {
|
||||
creationID := creationIDs[pluginName]
|
||||
descheduleID := descheduleIDs[pluginName]
|
||||
balanceID := balanceIDs[pluginName]
|
||||
|
||||
if creationID == "" {
|
||||
t.Errorf("Profile %s, plugin %s: plugin creation did not capture instance ID", profileInstanceID, pluginName)
|
||||
}
|
||||
if descheduleID == "" {
|
||||
t.Errorf("Profile %s, plugin %s: deschedule extension point did not capture instance ID", profileInstanceID, pluginName)
|
||||
}
|
||||
if balanceID == "" {
|
||||
t.Errorf("Profile %s, plugin %s: balance extension point did not capture instance ID", profileInstanceID, pluginName)
|
||||
}
|
||||
|
||||
// Verify all IDs match
|
||||
if creationID != descheduleID {
|
||||
t.Errorf("Profile %s, plugin %s: instance ID mismatch - creation: %s, deschedule: %s", profileInstanceID, pluginName, creationID, descheduleID)
|
||||
}
|
||||
if creationID != balanceID {
|
||||
t.Errorf("Profile %s, plugin %s: instance ID mismatch - creation: %s, balance: %s", profileInstanceID, pluginName, creationID, balanceID)
|
||||
}
|
||||
if descheduleID != balanceID {
|
||||
t.Errorf("Profile %s, plugin %s: instance ID mismatch - deschedule: %s, balance: %s", profileInstanceID, pluginName, descheduleID, balanceID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// verifyInstanceIDFormat verifies that instance IDs have correct format and sequential indices
|
||||
func verifyInstanceIDFormat(t *testing.T, profileInstanceID string, pluginNames []string, pluginIDs map[string]string) sets.Set[string] {
|
||||
if len(pluginIDs) != len(pluginNames) {
|
||||
t.Errorf("Profile %s: expected %d plugins to be invoked, got %d", profileInstanceID, len(pluginNames), len(pluginIDs))
|
||||
}
|
||||
|
||||
// Collect all instance IDs for this profile
|
||||
profileInstanceIDs := sets.New[string]()
|
||||
for pluginName, instanceID := range pluginIDs {
|
||||
if instanceID == "" {
|
||||
t.Errorf("Profile %s, plugin %s: expected instance ID to be set, got empty string", profileInstanceID, pluginName)
|
||||
}
|
||||
profileInstanceIDs.Insert(instanceID)
|
||||
}
|
||||
|
||||
// Verify all IDs within this profile are unique
|
||||
if profileInstanceIDs.Len() != len(pluginIDs) {
|
||||
t.Errorf("Profile %s: duplicate instance IDs found", profileInstanceID)
|
||||
}
|
||||
|
||||
// Verify all IDs match the expected format: "{profileInstanceID}-{index}"
|
||||
// and contain sequential indices from 0 to n-1
|
||||
expectedIndices := sets.New[int]()
|
||||
for i := 0; i < len(pluginNames); i++ {
|
||||
expectedIndices.Insert(i)
|
||||
}
|
||||
actualIndices := sets.New[int]()
|
||||
for pluginName, instanceID := range pluginIDs {
|
||||
var idx int
|
||||
expectedPrefix := profileInstanceID + "-"
|
||||
if !strings.HasPrefix(instanceID, expectedPrefix) {
|
||||
t.Errorf("Profile %s, plugin %s: instance ID %s does not start with %s", profileInstanceID, pluginName, instanceID, expectedPrefix)
|
||||
continue
|
||||
}
|
||||
_, err := fmt.Sscanf(instanceID, profileInstanceID+"-%d", &idx)
|
||||
if err != nil {
|
||||
t.Errorf("Profile %s, plugin %s: instance ID %s does not match expected format", profileInstanceID, pluginName, instanceID)
|
||||
continue
|
||||
}
|
||||
actualIndices.Insert(idx)
|
||||
}
|
||||
// Verify we have indices 0 through n-1
|
||||
diff := cmp.Diff(expectedIndices, actualIndices)
|
||||
if diff != "" {
|
||||
t.Errorf("Profile %s: instance ID indices mismatch (-want +got):\n%s", profileInstanceID, diff)
|
||||
}
|
||||
|
||||
return profileInstanceIDs
|
||||
}
|
||||
|
||||
func TestPluginInstanceIDs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
profiles []struct {
|
||||
profileInstanceID string
|
||||
pluginNames []string
|
||||
}
|
||||
}{
|
||||
{
|
||||
name: "single plugin gets instance ID",
|
||||
profiles: []struct {
|
||||
profileInstanceID string
|
||||
pluginNames []string
|
||||
}{
|
||||
{
|
||||
profileInstanceID: "0",
|
||||
pluginNames: []string{"TestPlugin"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "two plugins get different instance IDs",
|
||||
profiles: []struct {
|
||||
profileInstanceID string
|
||||
pluginNames []string
|
||||
}{
|
||||
{
|
||||
profileInstanceID: "0",
|
||||
pluginNames: []string{"Plugin_0", "Plugin_1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "three profiles with two plugins each get unique instance IDs",
|
||||
profiles: []struct {
|
||||
profileInstanceID string
|
||||
pluginNames []string
|
||||
}{
|
||||
{
|
||||
profileInstanceID: "0",
|
||||
pluginNames: []string{"Plugin_A", "Plugin_B"},
|
||||
},
|
||||
{
|
||||
profileInstanceID: "1",
|
||||
pluginNames: []string{"Plugin_C", "Plugin_D"},
|
||||
},
|
||||
{
|
||||
profileInstanceID: "2",
|
||||
pluginNames: []string{"Plugin_E", "Plugin_F"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "three profiles with same plugin names get different instance IDs per profile",
|
||||
profiles: []struct {
|
||||
profileInstanceID string
|
||||
pluginNames []string
|
||||
}{
|
||||
{
|
||||
profileInstanceID: "0",
|
||||
pluginNames: []string{"CommonPlugin_X", "CommonPlugin_Y"},
|
||||
},
|
||||
{
|
||||
profileInstanceID: "1",
|
||||
pluginNames: []string{"CommonPlugin_X", "CommonPlugin_Y"},
|
||||
},
|
||||
{
|
||||
profileInstanceID: "2",
|
||||
pluginNames: []string{"CommonPlugin_X", "CommonPlugin_Y"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.TODO())
|
||||
defer cancel()
|
||||
|
||||
n1 := testutils.BuildTestNode("n1", 2000, 3000, 10, nil)
|
||||
n2 := testutils.BuildTestNode("n2", 2000, 3000, 10, nil)
|
||||
nodes := []*v1.Node{n1, n2}
|
||||
|
||||
// Track instance IDs by profile from different stages
|
||||
profileDescheduleIDs := make(map[string]map[string]string) // profileInstanceID -> pluginName -> instanceID (from Deschedule execution)
|
||||
profileBalanceIDs := make(map[string]map[string]string) // profileInstanceID -> pluginName -> instanceID (from Balance execution)
|
||||
profileCreationIDs := make(map[string]map[string]string) // profileInstanceID -> pluginName -> instanceID (from plugin creation)
|
||||
registry := pluginregistry.NewRegistry()
|
||||
|
||||
// Collect all distinct plugin names across all profiles
|
||||
allPluginNames := sets.New[string]()
|
||||
for _, profileCfg := range test.profiles {
|
||||
allPluginNames.Insert(profileCfg.pluginNames...)
|
||||
}
|
||||
|
||||
// Helper function to validate and store instance ID
|
||||
captureInstanceID := func(instanceID, pluginName string, targetMap map[string]map[string]string) {
|
||||
parts := strings.Split(instanceID, "-")
|
||||
if len(parts) < 2 {
|
||||
t.Fatalf("Plugin %s: instance ID %s does not have expected format 'profileID-index'", pluginName, instanceID)
|
||||
}
|
||||
profileID := parts[0]
|
||||
if targetMap[profileID] == nil {
|
||||
targetMap[profileID] = make(map[string]string)
|
||||
}
|
||||
targetMap[profileID][pluginName] = instanceID
|
||||
}
|
||||
|
||||
// Register all plugins before creating profiles
|
||||
for _, pluginName := range allPluginNames.UnsortedList() {
|
||||
// Capture plugin name for closure
|
||||
name := pluginName
|
||||
|
||||
pluginregistry.Register(
|
||||
pluginName,
|
||||
func(ctx context.Context, args runtime.Object, handle frameworktypes.Handle) (frameworktypes.Plugin, error) {
|
||||
fakePlugin := &fakeplugin.FakePlugin{PluginName: name}
|
||||
|
||||
fakePlugin.AddReactor(string(frameworktypes.DescheduleExtensionPoint), func(action fakeplugin.Action) (handled, filter bool, err error) {
|
||||
if dAction, ok := action.(fakeplugin.DescheduleAction); ok {
|
||||
captureInstanceID(dAction.Handle().PluginInstanceID(), name, profileDescheduleIDs)
|
||||
return true, false, nil
|
||||
}
|
||||
return false, false, nil
|
||||
})
|
||||
|
||||
fakePlugin.AddReactor(string(frameworktypes.BalanceExtensionPoint), func(action fakeplugin.Action) (handled, filter bool, err error) {
|
||||
if bAction, ok := action.(fakeplugin.BalanceAction); ok {
|
||||
captureInstanceID(bAction.Handle().PluginInstanceID(), name, profileBalanceIDs)
|
||||
return true, false, nil
|
||||
}
|
||||
return false, false, nil
|
||||
})
|
||||
|
||||
// Use NewPluginFncFromFakeWithReactor to wrap and capture instance ID at creation
|
||||
builder := fakeplugin.NewPluginFncFromFakeWithReactor(fakePlugin, func(action fakeplugin.ActionImpl) {
|
||||
captureInstanceID(action.Handle().PluginInstanceID(), name, profileCreationIDs)
|
||||
})
|
||||
|
||||
return builder(ctx, args, handle)
|
||||
},
|
||||
&fakeplugin.FakePlugin{},
|
||||
&fakeplugin.FakePluginArgs{},
|
||||
fakeplugin.ValidateFakePluginArgs,
|
||||
fakeplugin.SetDefaults_FakePluginArgs,
|
||||
registry,
|
||||
)
|
||||
}
|
||||
|
||||
client := fakeclientset.NewSimpleClientset(n1, n2)
|
||||
handle, podEvictor, err := frameworktesting.InitFrameworkHandle(
|
||||
ctx,
|
||||
client,
|
||||
nil,
|
||||
defaultevictor.DefaultEvictorArgs{},
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Unable to initialize a framework handle: %v", err)
|
||||
}
|
||||
|
||||
// Create all profiles
|
||||
var profiles []*profileImpl
|
||||
for _, profileCfg := range test.profiles {
|
||||
|
||||
var pluginConfigs []api.PluginConfig
|
||||
for _, pluginName := range profileCfg.pluginNames {
|
||||
pluginConfigs = append(pluginConfigs, api.PluginConfig{
|
||||
Name: pluginName,
|
||||
Args: &fakeplugin.FakePluginArgs{},
|
||||
})
|
||||
}
|
||||
|
||||
prfl, err := NewProfile(
|
||||
ctx,
|
||||
api.DeschedulerProfile{
|
||||
Name: "test-profile",
|
||||
PluginConfigs: pluginConfigs,
|
||||
Plugins: api.Plugins{
|
||||
Deschedule: api.PluginSet{
|
||||
Enabled: profileCfg.pluginNames,
|
||||
},
|
||||
Balance: api.PluginSet{
|
||||
Enabled: profileCfg.pluginNames,
|
||||
},
|
||||
},
|
||||
},
|
||||
registry,
|
||||
WithClientSet(client),
|
||||
WithSharedInformerFactory(handle.SharedInformerFactoryImpl),
|
||||
WithPodEvictor(podEvictor),
|
||||
WithGetPodsAssignedToNodeFnc(handle.GetPodsAssignedToNodeFuncImpl),
|
||||
WithProfileInstanceID(profileCfg.profileInstanceID),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create profile: %v", err)
|
||||
}
|
||||
profiles = append(profiles, prfl)
|
||||
}
|
||||
|
||||
// Run deschedule and balance plugins for all profiles
|
||||
for _, prfl := range profiles {
|
||||
prfl.RunDeschedulePlugins(ctx, nodes)
|
||||
prfl.RunBalancePlugins(ctx, nodes)
|
||||
}
|
||||
|
||||
// Verify creation, deschedule, and balance IDs all match
|
||||
for _, profileCfg := range test.profiles {
|
||||
verifyInstanceIDsMatch(
|
||||
t,
|
||||
profileCfg.profileInstanceID,
|
||||
profileCfg.pluginNames,
|
||||
profileCreationIDs[profileCfg.profileInstanceID],
|
||||
profileDescheduleIDs[profileCfg.profileInstanceID],
|
||||
profileBalanceIDs[profileCfg.profileInstanceID],
|
||||
)
|
||||
}
|
||||
|
||||
// Verify all plugins were invoked and have correct instance IDs
|
||||
allInstanceIDs := sets.New[string]()
|
||||
for _, profileCfg := range test.profiles {
|
||||
profileInstanceIDs := verifyInstanceIDFormat(
|
||||
t,
|
||||
profileCfg.profileInstanceID,
|
||||
profileCfg.pluginNames,
|
||||
profileDescheduleIDs[profileCfg.profileInstanceID],
|
||||
)
|
||||
allInstanceIDs = allInstanceIDs.Union(profileInstanceIDs)
|
||||
}
|
||||
|
||||
// Verify all instance IDs are unique across all profiles
|
||||
totalExpectedPlugins := 0
|
||||
for _, profileCfg := range test.profiles {
|
||||
totalExpectedPlugins += len(profileCfg.pluginNames)
|
||||
}
|
||||
if allInstanceIDs.Len() != totalExpectedPlugins {
|
||||
t.Errorf("Expected %d unique instance IDs across all profiles, got %d", totalExpectedPlugins, allInstanceIDs.Len())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user