feat: support custom scheduler config for in-tree schedulr plugins (without extenders)
Signed-off-by: vadasambar <surajrbanakar@gmail.com> refactor: rename `--scheduler-config` -> `--scheduler-config-file` to avoid confusion Signed-off-by: vadasambar <surajrbanakar@gmail.com> fix: `goto` causing infinite loop - abstract out running extenders in a separate function Signed-off-by: vadasambar <surajrbanakar@gmail.com> refactor: remove code around extenders - we decided not to use scheduler extenders for checking if a pod would fit on a node Signed-off-by: vadasambar <surajrbanakar@gmail.com> refactor: move scheduler config to a `utils/scheduler` package` - use default config as a fallback Signed-off-by: vadasambar <surajrbanakar@gmail.com> test: fix static_autoscaler test Signed-off-by: vadasambar <surajrbanakar@gmail.com> refactor: `GetSchedulerConfiguration` fn - remove falling back - add mechanism to detect if the scheduler config file flag was set - Signed-off-by: vadasambar <surajrbanakar@gmail.com> test: wip add tests for `GetSchedulerConfig` - tests are failing now Signed-off-by: vadasambar <surajrbanakar@gmail.com> test: add tests for `GetSchedulerConfig` - abstract error messages so that we can use them in the tests - set api version explicitly (this is what upstream does as well) Signed-off-by: vadasambar <surajrbanakar@gmail.com> refactor: do a round of cleanup to make PR ready for review - make import names consistent Signed-off-by: vadasambar <surajrbanakar@gmail.com> fix: use `pflag` to check if the `--scheduler-config-file` flag was set Signed-off-by: vadasambar <surajrbanakar@gmail.com> docs: add comments for exported error constants Signed-off-by: vadasambar <surajrbanakar@gmail.com> refactor: don't export error messages - exporting is not needed Signed-off-by: vadasambar <surajrbanakar@gmail.com> fix: add underscore in test file name Signed-off-by: vadasambar <surajrbanakar@gmail.com> test: fix test failing because of no comment on exported `SchedulerConfigFileFlag` Signed-off-by: vadasambar <surajrbanakar@gmail.com> refacotr: change name of flag variable `schedulerConfig` -> `schedulerConfigFile` - avoids confusion Signed-off-by: vadasambar <surajrbanakar@gmail.com> test: add extra test cases for predicate checker - where the predicate checker uses custom scheduler config Signed-off-by: vadasambar <surajrbanakar@gmail.com> refactor: remove `setFlags` variable - not needed anymore Signed-off-by: vadasambar <surajrbanakar@gmail.com> refactor: abstract custom scheduler configs into `conifg` package - make them constants Signed-off-by: vadasambar <surajrbanakar@gmail.com> test: fix linting error Signed-off-by: vadasambar <surajrbanakar@gmail.com> refactor: introduce a new custom test predicate checker - instead of adding a param to the current one - this is so that we don't have to pass `nil` to the existing test predicate checker in many places Signed-off-by: vadasambar <surajrbanakar@gmail.com> refactor: rename `NewCustomPredicateChecker` -> `NewTestPredicateCheckerWithCustomConfig` - latter narrows down meaning of the function better than former Signed-off-by: vadasambar <surajrbanakar@gmail.com> refactor: rename `GetSchedulerConfig` -> `ConfigFromPath` - `scheduler.ConfigFromPath` is shorter and feels less vague than `scheduler.GetSchedulerConfig` - move test config to a new package `test` under `config` package Signed-off-by: vadasambar <surajrbanakar@gmail.com> docs: add `TODO` for replacing code to parse scheduler config - with upstream function Signed-off-by: vadasambar <surajrbanakar@gmail.com>
This commit is contained in:
		
							parent
							
								
									da96d89b17
								
							
						
					
					
						commit
						23f03e112e
					
				|  | @ -18,6 +18,8 @@ package config | |||
| 
 | ||||
| import ( | ||||
| 	"time" | ||||
| 
 | ||||
| 	scheduler_config "k8s.io/kubernetes/pkg/scheduler/apis/config" | ||||
| ) | ||||
| 
 | ||||
| // GpuLimits define lower and upper bound on GPU instances of given type in cluster
 | ||||
|  | @ -166,6 +168,9 @@ type AutoscalingOptions struct { | |||
| 	// ScaleDownSimulationTimeout defines the maximum time that can be
 | ||||
| 	// spent on scale down simulation.
 | ||||
| 	ScaleDownSimulationTimeout time.Duration | ||||
| 	// SchedulerConfig allows changing configuration of in-tree
 | ||||
| 	// scheduler plugins acting on PreFilter and Filter extension points
 | ||||
| 	SchedulerConfig *scheduler_config.KubeSchedulerConfiguration | ||||
| 	// NodeDeletionDelayTimeout is maximum time CA waits for removing delay-deletion.cluster-autoscaler.kubernetes.io/ annotations before deleting the node.
 | ||||
| 	NodeDeletionDelayTimeout time.Duration | ||||
| 	// WriteStatusConfigMap tells if the status information should be written to a ConfigMap
 | ||||
|  |  | |||
|  | @ -19,6 +19,10 @@ package config | |||
| import "time" | ||||
| 
 | ||||
| const ( | ||||
| 	// SchedulerConfigFileFlag is the name of the flag
 | ||||
| 	// for passing in custom scheduler config for in-tree scheduelr plugins
 | ||||
| 	SchedulerConfigFileFlag = "scheduler-config-file" | ||||
| 
 | ||||
| 	// DefaultMaxClusterCores is the default maximum number of cores in the cluster.
 | ||||
| 	DefaultMaxClusterCores = 5000 * 64 | ||||
| 	// DefaultMaxClusterMemory is the default maximum number of gigabytes of memory in cluster.
 | ||||
|  |  | |||
|  | @ -0,0 +1,68 @@ | |||
| /* | ||||
| Copyright 2023 The Kubernetes Authors. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| package test | ||||
| 
 | ||||
| const ( | ||||
| 	// Custom scheduler configs for testing
 | ||||
| 
 | ||||
| 	// SchedulerConfigNodeResourcesFitDisabled is scheduler config
 | ||||
| 	// with `NodeResourcesFit` plugin disabled
 | ||||
| 	SchedulerConfigNodeResourcesFitDisabled = ` | ||||
| apiVersion: kubescheduler.config.k8s.io/v1 | ||||
| kind: KubeSchedulerConfiguration | ||||
| profiles: | ||||
| - pluginConfig: | ||||
|   plugins: | ||||
|     multiPoint: | ||||
|       disabled: | ||||
|       - name: NodeResourcesFit | ||||
|         weight: 1 | ||||
|   schedulerName: custom-scheduler` | ||||
| 
 | ||||
| 	// SchedulerConfigTaintTolerationDisabled is scheduler config
 | ||||
| 	// with `TaintToleration` plugin disabled
 | ||||
| 	SchedulerConfigTaintTolerationDisabled = ` | ||||
| apiVersion: kubescheduler.config.k8s.io/v1 | ||||
| kind: KubeSchedulerConfiguration | ||||
| profiles: | ||||
| - pluginConfig: | ||||
|   plugins: | ||||
|     multiPoint: | ||||
|       disabled: | ||||
|       - name: TaintToleration | ||||
|         weight: 1 | ||||
|   schedulerName: custom-scheduler` | ||||
| 
 | ||||
| 	// SchedulerConfigMinimalCorrect is the minimal
 | ||||
| 	// correct scheduler config
 | ||||
| 	SchedulerConfigMinimalCorrect = ` | ||||
| apiVersion: kubescheduler.config.k8s.io/v1 | ||||
| kind: KubeSchedulerConfiguration` | ||||
| 
 | ||||
| 	// SchedulerConfigDecodeErr is the scheduler config
 | ||||
| 	// which throws decoding error when we try to load it
 | ||||
| 	SchedulerConfigDecodeErr = ` | ||||
| kind: KubeSchedulerConfiguration` | ||||
| 
 | ||||
| 	// SchedulerConfigInvalid is invalid scheduler config
 | ||||
| 	// because we specify percentageOfNodesToScore > 100
 | ||||
| 	SchedulerConfigInvalid = ` | ||||
| apiVersion: kubescheduler.config.k8s.io/v1 | ||||
| kind: KubeSchedulerConfiguration | ||||
| # percentageOfNodesToScore has to be between 0 and 100 | ||||
| percentageOfNodesToScore: 130` | ||||
| ) | ||||
|  | @ -56,6 +56,7 @@ import ( | |||
| 	"k8s.io/autoscaler/cluster-autoscaler/simulator/clustersnapshot" | ||||
| 	"k8s.io/autoscaler/cluster-autoscaler/utils/errors" | ||||
| 	kube_util "k8s.io/autoscaler/cluster-autoscaler/utils/kubernetes" | ||||
| 	scheduler_util "k8s.io/autoscaler/cluster-autoscaler/utils/scheduler" | ||||
| 	"k8s.io/autoscaler/cluster-autoscaler/utils/units" | ||||
| 	"k8s.io/autoscaler/cluster-autoscaler/version" | ||||
| 	kube_client "k8s.io/client-go/kubernetes" | ||||
|  | @ -68,6 +69,7 @@ import ( | |||
| 	"k8s.io/component-base/config/options" | ||||
| 	"k8s.io/component-base/metrics/legacyregistry" | ||||
| 	"k8s.io/klog/v2" | ||||
| 	scheduler_config "k8s.io/kubernetes/pkg/scheduler/apis/config" | ||||
| ) | ||||
| 
 | ||||
| // MultiStringFlag is a flag for passing multiple parameters using same flag
 | ||||
|  | @ -133,6 +135,7 @@ var ( | |||
| 			"for scale down when some candidates from previous iteration are no longer valid."+ | ||||
| 			"When calculating the pool size for additional candidates we take"+ | ||||
| 			"max(#nodes * scale-down-candidates-pool-ratio, scale-down-candidates-pool-min-count).") | ||||
| 	schedulerConfigFile         = flag.String(config.SchedulerConfigFileFlag, "", "scheduler-config allows changing configuration of in-tree scheduler plugins acting on PreFilter and Filter extension points") | ||||
| 	nodeDeletionDelayTimeout    = flag.Duration("node-deletion-delay-timeout", 2*time.Minute, "Maximum time CA waits for removing delay-deletion.cluster-autoscaler.kubernetes.io/ annotations before deleting the node.") | ||||
| 	nodeDeletionBatcherInterval = flag.Duration("node-deletion-batcher-interval", 0*time.Second, "How long CA ScaleDown gather nodes to delete them in batch.") | ||||
| 	scanInterval                = flag.Duration("scan-interval", 10*time.Second, "How often cluster is reevaluated for scale up or down") | ||||
|  | @ -274,6 +277,15 @@ func createAutoscalingOptions() config.AutoscalingOptions { | |||
| 		*maxEmptyBulkDeleteFlag = *maxScaleDownParallelismFlag | ||||
| 	} | ||||
| 
 | ||||
| 	var parsedSchedConfig *scheduler_config.KubeSchedulerConfiguration | ||||
| 	// if scheduler config flag was set by the user
 | ||||
| 	if pflag.CommandLine.Changed(config.SchedulerConfigFileFlag) { | ||||
| 		parsedSchedConfig, err = scheduler_util.ConfigFromPath(*schedulerConfigFile) | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		klog.Fatalf("Failed to get scheduler config: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	return config.AutoscalingOptions{ | ||||
| 		NodeGroupDefaults: config.NodeGroupAutoscalingOptions{ | ||||
| 			ScaleDownUtilizationThreshold:    *scaleDownUtilizationThreshold, | ||||
|  | @ -316,6 +328,7 @@ func createAutoscalingOptions() config.AutoscalingOptions { | |||
| 		ScaleDownNonEmptyCandidatesCount: *scaleDownNonEmptyCandidatesCount, | ||||
| 		ScaleDownCandidatesPoolRatio:     *scaleDownCandidatesPoolRatio, | ||||
| 		ScaleDownCandidatesPoolMinCount:  *scaleDownCandidatesPoolMinCount, | ||||
| 		SchedulerConfig:                  parsedSchedConfig, | ||||
| 		WriteStatusConfigMap:             *writeStatusConfigMapFlag, | ||||
| 		StatusConfigMapName:              *statusConfigMapName, | ||||
| 		BalanceSimilarNodeGroups:         *balanceSimilarNodeGroupsFlag, | ||||
|  | @ -422,7 +435,8 @@ func buildAutoscaler(debuggingSnapshotter debuggingsnapshot.DebuggingSnapshotter | |||
| 
 | ||||
| 	eventsKubeClient := createKubeClient(getKubeConfig()) | ||||
| 
 | ||||
| 	predicateChecker, err := predicatechecker.NewSchedulerBasedPredicateChecker(kubeClient, make(chan struct{})) | ||||
| 	predicateChecker, err := predicatechecker.NewSchedulerBasedPredicateChecker(kubeClient, | ||||
| 		autoscalingOptions.SchedulerConfig, make(chan struct{})) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  |  | |||
|  | @ -27,6 +27,7 @@ import ( | |||
| 	kube_client "k8s.io/client-go/kubernetes" | ||||
| 	v1listers "k8s.io/client-go/listers/core/v1" | ||||
| 	klog "k8s.io/klog/v2" | ||||
| 	"k8s.io/kubernetes/pkg/scheduler/apis/config" | ||||
| 	scheduler_config "k8s.io/kubernetes/pkg/scheduler/apis/config/latest" | ||||
| 	schedulerframework "k8s.io/kubernetes/pkg/scheduler/framework" | ||||
| 	scheduler_plugins "k8s.io/kubernetes/pkg/scheduler/framework/plugins" | ||||
|  | @ -44,21 +45,26 @@ type SchedulerBasedPredicateChecker struct { | |||
| } | ||||
| 
 | ||||
| // NewSchedulerBasedPredicateChecker builds scheduler based PredicateChecker.
 | ||||
| func NewSchedulerBasedPredicateChecker(kubeClient kube_client.Interface, stop <-chan struct{}) (*SchedulerBasedPredicateChecker, error) { | ||||
| func NewSchedulerBasedPredicateChecker(kubeClient kube_client.Interface, schedConfig *config.KubeSchedulerConfiguration, stop <-chan struct{}) (*SchedulerBasedPredicateChecker, error) { | ||||
| 	informerFactory := informers.NewSharedInformerFactory(kubeClient, 0) | ||||
| 	config, err := scheduler_config.Default() | ||||
| 
 | ||||
| 	if schedConfig == nil { | ||||
| 		var err error | ||||
| 		schedConfig, err = scheduler_config.Default() | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("couldn't create scheduler config: %v", err) | ||||
| 		} | ||||
| 	if len(config.Profiles) != 1 || config.Profiles[0].SchedulerName != apiv1.DefaultSchedulerName { | ||||
| 		return nil, fmt.Errorf("unexpected scheduler config: expected default scheduler profile only (found %d profiles)", len(config.Profiles)) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(schedConfig.Profiles) != 1 { | ||||
| 		return nil, fmt.Errorf("unexpected scheduler config: expected one scheduler profile only (found %d profiles)", len(schedConfig.Profiles)) | ||||
| 	} | ||||
| 	sharedLister := NewDelegatingSchedulerSharedLister() | ||||
| 
 | ||||
| 	framework, err := schedulerframeworkruntime.NewFramework( | ||||
| 		context.TODO(), | ||||
| 		scheduler_plugins.NewInTreeRegistry(), | ||||
| 		&config.Profiles[0], | ||||
| 		&schedConfig.Profiles[0], | ||||
| 		schedulerframeworkruntime.WithInformerFactory(informerFactory), | ||||
| 		schedulerframeworkruntime.WithSnapshotSharedLister(sharedLister), | ||||
| 	) | ||||
|  | @ -181,6 +187,7 @@ func (p *SchedulerBasedPredicateChecker) CheckPredicates(clusterSnapshot cluster | |||
| 			filterReasons, | ||||
| 			p.buildDebugInfo(filterName, nodeInfo)) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -17,10 +17,14 @@ limitations under the License. | |||
| package predicatechecker | ||||
| 
 | ||||
| import ( | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| 	testconfig "k8s.io/autoscaler/cluster-autoscaler/config/test" | ||||
| 	"k8s.io/autoscaler/cluster-autoscaler/simulator/clustersnapshot" | ||||
| 	scheduler "k8s.io/autoscaler/cluster-autoscaler/utils/scheduler" | ||||
| 	. "k8s.io/autoscaler/cluster-autoscaler/utils/test" | ||||
| 
 | ||||
| 	"github.com/stretchr/testify/assert" | ||||
|  | @ -36,52 +40,114 @@ func TestCheckPredicate(t *testing.T) { | |||
| 
 | ||||
| 	n1000 := BuildTestNode("n1000", 1000, 2000000) | ||||
| 	SetNodeReadyState(n1000, true, time.Time{}) | ||||
| 	n1000Unschedulable := BuildTestNode("n1000", 1000, 2000000) | ||||
| 	SetNodeReadyState(n1000Unschedulable, true, time.Time{}) | ||||
| 
 | ||||
| 	defaultPredicateChecker, err := NewTestPredicateChecker() | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// temp dir
 | ||||
| 	tmpDir, err := os.MkdirTemp("", "scheduler-configs") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	defer os.RemoveAll(tmpDir) | ||||
| 
 | ||||
| 	customConfigFile := filepath.Join(tmpDir, "custom_config.yaml") | ||||
| 	if err := os.WriteFile(customConfigFile, | ||||
| 		[]byte(testconfig.SchedulerConfigNodeResourcesFitDisabled), | ||||
| 		os.FileMode(0600)); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	customConfig, err := scheduler.ConfigFromPath(customConfigFile) | ||||
| 	assert.NoError(t, err) | ||||
| 	customPredicateChecker, err := NewTestPredicateCheckerWithCustomConfig(customConfig) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	tests := []struct { | ||||
| 		name             string | ||||
| 		node             *apiv1.Node | ||||
| 		scheduledPods    []*apiv1.Pod | ||||
| 		testPod          *apiv1.Pod | ||||
| 		predicateChecker PredicateChecker | ||||
| 		expectError      bool | ||||
| 	}{ | ||||
| 		// default predicate checker test cases
 | ||||
| 		{ | ||||
| 			name:          "other pod - insuficient cpu", | ||||
| 			name:             "default - other pod - insuficient cpu", | ||||
| 			node:             n1000, | ||||
| 			scheduledPods:    []*apiv1.Pod{p450}, | ||||
| 			testPod:          p600, | ||||
| 			expectError:      true, | ||||
| 			predicateChecker: defaultPredicateChecker, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:          "other pod - ok", | ||||
| 			name:             "default - other pod - ok", | ||||
| 			node:             n1000, | ||||
| 			scheduledPods:    []*apiv1.Pod{p450}, | ||||
| 			testPod:          p500, | ||||
| 			expectError:      false, | ||||
| 			predicateChecker: defaultPredicateChecker, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:          "empty - insuficient cpu", | ||||
| 			name:             "default - empty - insuficient cpu", | ||||
| 			node:             n1000, | ||||
| 			scheduledPods:    []*apiv1.Pod{}, | ||||
| 			testPod:          p8000, | ||||
| 			expectError:      true, | ||||
| 			predicateChecker: defaultPredicateChecker, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:          "empty - ok", | ||||
| 			name:             "default - empty - ok", | ||||
| 			node:             n1000, | ||||
| 			scheduledPods:    []*apiv1.Pod{}, | ||||
| 			testPod:          p600, | ||||
| 			expectError:      false, | ||||
| 			predicateChecker: defaultPredicateChecker, | ||||
| 		}, | ||||
| 		// custom predicate checker test cases
 | ||||
| 		{ | ||||
| 			name:             "custom - other pod - ok", | ||||
| 			node:             n1000, | ||||
| 			scheduledPods:    []*apiv1.Pod{p450}, | ||||
| 			testPod:          p600, | ||||
| 			expectError:      false, | ||||
| 			predicateChecker: customPredicateChecker, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:             "custom -other pod - ok", | ||||
| 			node:             n1000, | ||||
| 			scheduledPods:    []*apiv1.Pod{p450}, | ||||
| 			testPod:          p500, | ||||
| 			expectError:      false, | ||||
| 			predicateChecker: customPredicateChecker, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:             "custom -empty - ok", | ||||
| 			node:             n1000, | ||||
| 			scheduledPods:    []*apiv1.Pod{}, | ||||
| 			testPod:          p8000, | ||||
| 			expectError:      false, | ||||
| 			predicateChecker: customPredicateChecker, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:             "custom -empty - ok", | ||||
| 			node:             n1000, | ||||
| 			scheduledPods:    []*apiv1.Pod{}, | ||||
| 			testPod:          p600, | ||||
| 			expectError:      false, | ||||
| 			predicateChecker: customPredicateChecker, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			var err error | ||||
| 			predicateChecker, err := NewTestPredicateChecker() | ||||
| 			clusterSnapshot := clustersnapshot.NewBasicClusterSnapshot() | ||||
| 			err = clusterSnapshot.AddNodeWithPods(tt.node, tt.scheduledPods) | ||||
| 			assert.NoError(t, err) | ||||
| 
 | ||||
| 			predicateError := predicateChecker.CheckPredicates(clusterSnapshot, tt.testPod, tt.node.Name) | ||||
| 			predicateError := tt.predicateChecker.CheckPredicates(clusterSnapshot, tt.testPod, tt.node.Name) | ||||
| 			if tt.expectError { | ||||
| 				assert.NotNil(t, predicateError) | ||||
| 				assert.Equal(t, NotSchedulablePredicateError, predicateError.ErrorType()) | ||||
|  | @ -102,7 +168,80 @@ func TestFitsAnyNode(t *testing.T) { | |||
| 	n1000 := BuildTestNode("n1000", 1000, 2000000) | ||||
| 	n2000 := BuildTestNode("n2000", 2000, 2000000) | ||||
| 
 | ||||
| 	var err error | ||||
| 	defaultPredicateChecker, err := NewTestPredicateChecker() | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// temp dir
 | ||||
| 	tmpDir, err := os.MkdirTemp("", "scheduler-configs") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	defer os.RemoveAll(tmpDir) | ||||
| 
 | ||||
| 	customConfigFile := filepath.Join(tmpDir, "custom_config.yaml") | ||||
| 	if err := os.WriteFile(customConfigFile, | ||||
| 		[]byte(testconfig.SchedulerConfigNodeResourcesFitDisabled), | ||||
| 		os.FileMode(0600)); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	customConfig, err := scheduler.ConfigFromPath(customConfigFile) | ||||
| 	assert.NoError(t, err) | ||||
| 	customPredicateChecker, err := NewTestPredicateCheckerWithCustomConfig(customConfig) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	testCases := []struct { | ||||
| 		name             string | ||||
| 		predicateChecker PredicateChecker | ||||
| 		pod              *apiv1.Pod | ||||
| 		expectedNodes    []string | ||||
| 		expectError      bool | ||||
| 	}{ | ||||
| 		// default predicate checker test cases
 | ||||
| 		{ | ||||
| 			name:             "default - small pod - no error", | ||||
| 			predicateChecker: defaultPredicateChecker, | ||||
| 			pod:              p900, | ||||
| 			expectedNodes:    []string{"n1000", "n2000"}, | ||||
| 			expectError:      false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:             "default - medium pod - no error", | ||||
| 			predicateChecker: defaultPredicateChecker, | ||||
| 			pod:              p1900, | ||||
| 			expectedNodes:    []string{"n2000"}, | ||||
| 			expectError:      false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:             "default - large pod - insufficient cpu", | ||||
| 			predicateChecker: defaultPredicateChecker, | ||||
| 			pod:              p2100, | ||||
| 			expectError:      true, | ||||
| 		}, | ||||
| 
 | ||||
| 		// custom predicate checker test cases
 | ||||
| 		{ | ||||
| 			name:             "custom - small pod - no error", | ||||
| 			predicateChecker: customPredicateChecker, | ||||
| 			pod:              p900, | ||||
| 			expectedNodes:    []string{"n1000", "n2000"}, | ||||
| 			expectError:      false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:             "custom - medium pod - no error", | ||||
| 			predicateChecker: customPredicateChecker, | ||||
| 			pod:              p1900, | ||||
| 			expectedNodes:    []string{"n1000", "n2000"}, | ||||
| 			expectError:      false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:             "custom - large pod - insufficient cpu", | ||||
| 			predicateChecker: customPredicateChecker, | ||||
| 			pod:              p2100, | ||||
| 			expectedNodes:    []string{"n1000", "n2000"}, | ||||
| 			expectError:      false, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	clusterSnapshot := clustersnapshot.NewBasicClusterSnapshot() | ||||
| 	err = clusterSnapshot.AddNode(n1000) | ||||
|  | @ -110,19 +249,18 @@ func TestFitsAnyNode(t *testing.T) { | |||
| 	err = clusterSnapshot.AddNode(n2000) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	predicateChecker, err := NewTestPredicateChecker() | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	nodeName, err := predicateChecker.FitsAnyNode(clusterSnapshot, p900) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.True(t, nodeName == "n1000" || nodeName == "n2000") | ||||
| 
 | ||||
| 	nodeName, err = predicateChecker.FitsAnyNode(clusterSnapshot, p1900) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, "n2000", nodeName) | ||||
| 
 | ||||
| 	nodeName, err = predicateChecker.FitsAnyNode(clusterSnapshot, p2100) | ||||
| 	for _, tc := range testCases { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			nodeName, err := tc.predicateChecker.FitsAnyNode(clusterSnapshot, tc.pod) | ||||
| 			if tc.expectError { | ||||
| 				assert.Error(t, err) | ||||
| 			} else { | ||||
| 				assert.NoError(t, err) | ||||
| 				assert.Contains(t, tc.expectedNodes, nodeName) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| func TestDebugInfo(t *testing.T) { | ||||
|  | @ -142,16 +280,39 @@ func TestDebugInfo(t *testing.T) { | |||
| 	} | ||||
| 	SetNodeReadyState(node1, true, time.Time{}) | ||||
| 
 | ||||
| 	predicateChecker, err := NewTestPredicateChecker() | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	clusterSnapshot := clustersnapshot.NewBasicClusterSnapshot() | ||||
| 
 | ||||
| 	err = clusterSnapshot.AddNode(node1) | ||||
| 	err := clusterSnapshot.AddNode(node1) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	predicateErr := predicateChecker.CheckPredicates(clusterSnapshot, p1, "n1") | ||||
| 	// with default predicate checker
 | ||||
| 	defaultPredicateChecker, err := NewTestPredicateChecker() | ||||
| 	assert.NoError(t, err) | ||||
| 	predicateErr := defaultPredicateChecker.CheckPredicates(clusterSnapshot, p1, "n1") | ||||
| 	assert.NotNil(t, predicateErr) | ||||
| 	assert.Equal(t, "node(s) had untolerated taint {SomeTaint: WhyNot?}", predicateErr.Message()) | ||||
| 	assert.Contains(t, predicateErr.VerboseMessage(), "RandomTaint") | ||||
| 
 | ||||
| 	// with custom predicate checker
 | ||||
| 
 | ||||
| 	// temp dir
 | ||||
| 	tmpDir, err := os.MkdirTemp("", "scheduler-configs") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	defer os.RemoveAll(tmpDir) | ||||
| 
 | ||||
| 	customConfigFile := filepath.Join(tmpDir, "custom_config.yaml") | ||||
| 	if err := os.WriteFile(customConfigFile, | ||||
| 		[]byte(testconfig.SchedulerConfigTaintTolerationDisabled), | ||||
| 		os.FileMode(0600)); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	customConfig, err := scheduler.ConfigFromPath(customConfigFile) | ||||
| 	assert.NoError(t, err) | ||||
| 	customPredicateChecker, err := NewTestPredicateCheckerWithCustomConfig(customConfig) | ||||
| 	assert.NoError(t, err) | ||||
| 	predicateErr = customPredicateChecker.CheckPredicates(clusterSnapshot, p1, "n1") | ||||
| 	assert.Nil(t, predicateErr) | ||||
| } | ||||
|  |  | |||
|  | @ -18,10 +18,27 @@ package predicatechecker | |||
| 
 | ||||
| import ( | ||||
| 	clientsetfake "k8s.io/client-go/kubernetes/fake" | ||||
| 	"k8s.io/kubernetes/pkg/scheduler/apis/config" | ||||
| 	scheduler_config_latest "k8s.io/kubernetes/pkg/scheduler/apis/config/latest" | ||||
| ) | ||||
| 
 | ||||
| // NewTestPredicateChecker builds test version of PredicateChecker.
 | ||||
| func NewTestPredicateChecker() (PredicateChecker, error) { | ||||
| 	schedConfig, err := scheduler_config_latest.Default() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// just call out to NewSchedulerBasedPredicateChecker but use fake kubeClient
 | ||||
| 	return NewSchedulerBasedPredicateChecker(clientsetfake.NewSimpleClientset(), make(chan struct{})) | ||||
| 	return NewSchedulerBasedPredicateChecker(clientsetfake.NewSimpleClientset(), schedConfig, make(chan struct{})) | ||||
| } | ||||
| 
 | ||||
| // NewTestPredicateCheckerWithCustomConfig builds test version of PredicateChecker with custom scheduler config.
 | ||||
| func NewTestPredicateCheckerWithCustomConfig(schedConfig *config.KubeSchedulerConfiguration) (PredicateChecker, error) { | ||||
| 	if schedConfig != nil { | ||||
| 		// just call out to NewSchedulerBasedPredicateChecker but use fake kubeClient
 | ||||
| 		return NewSchedulerBasedPredicateChecker(clientsetfake.NewSimpleClientset(), schedConfig, make(chan struct{})) | ||||
| 	} | ||||
| 
 | ||||
| 	return NewTestPredicateChecker() | ||||
| } | ||||
|  |  | |||
|  | @ -18,14 +18,25 @@ package scheduler | |||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"strings" | ||||
| 
 | ||||
| 	apiv1 "k8s.io/api/core/v1" | ||||
| 	"k8s.io/apimachinery/pkg/api/resource" | ||||
| 	"k8s.io/apimachinery/pkg/util/uuid" | ||||
| 	scheduler_config "k8s.io/kubernetes/pkg/scheduler/apis/config" | ||||
| 	scheduler_scheme "k8s.io/kubernetes/pkg/scheduler/apis/config/scheme" | ||||
| 	scheduler_validation "k8s.io/kubernetes/pkg/scheduler/apis/config/validation" | ||||
| 	schedulerframework "k8s.io/kubernetes/pkg/scheduler/framework" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| 	schedulerConfigDecodeErr   = "couldn't decode scheduler config" | ||||
| 	schedulerConfigLoadErr     = "couldn't load scheduler config" | ||||
| 	schedulerConfigTypeCastErr = "couldn't assert type as KubeSchedulerConfiguration" | ||||
| 	schedulerConfigInvalidErr  = "invalid KubeSchedulerConfiguration" | ||||
| ) | ||||
| 
 | ||||
| // CreateNodeNameToInfoMap obtains a list of pods and pivots that list into a map where the keys are node names
 | ||||
| // and the values are the aggregated information for that node. Pods waiting lower priority pods preemption
 | ||||
| // (pod.Status.NominatedNodeName is set) are also added to list of pods for a node.
 | ||||
|  | @ -106,3 +117,33 @@ func ResourceToResourceList(r *schedulerframework.Resource) apiv1.ResourceList { | |||
| 	} | ||||
| 	return result | ||||
| } | ||||
| 
 | ||||
| // ConfigFromPath loads scheduler config from a path.
 | ||||
| // TODO(vadasambar): replace code to parse scheduler config with upstream function
 | ||||
| // once https://github.com/kubernetes/kubernetes/pull/119057 is merged
 | ||||
| func ConfigFromPath(path string) (*scheduler_config.KubeSchedulerConfiguration, error) { | ||||
| 	data, err := os.ReadFile(path) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("%s: %v", schedulerConfigLoadErr, err) | ||||
| 	} | ||||
| 
 | ||||
| 	obj, gvk, err := scheduler_scheme.Codecs.UniversalDecoder().Decode(data, nil, nil) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("%s: %v", schedulerConfigDecodeErr, err) | ||||
| 	} | ||||
| 
 | ||||
| 	cfgObj, ok := obj.(*scheduler_config.KubeSchedulerConfiguration) | ||||
| 	if !ok { | ||||
| 		return nil, fmt.Errorf("%s, gvk: %s", schedulerConfigTypeCastErr, gvk) | ||||
| 	} | ||||
| 
 | ||||
| 	// this needs to be set explicitly because config's api version is empty after decoding
 | ||||
| 	// check kubernetes/cmd/kube-scheduler/app/options/configfile.go for more info
 | ||||
| 	cfgObj.TypeMeta.APIVersion = gvk.GroupVersion().String() | ||||
| 
 | ||||
| 	if err := scheduler_validation.ValidateKubeSchedulerConfiguration(cfgObj); err != nil { | ||||
| 		return nil, fmt.Errorf("%s: %v", schedulerConfigInvalidErr, err) | ||||
| 	} | ||||
| 
 | ||||
| 	return cfgObj, nil | ||||
| } | ||||
|  |  | |||
|  | @ -18,11 +18,15 @@ package scheduler | |||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"reflect" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"k8s.io/apimachinery/pkg/api/resource" | ||||
| 	testconfig "k8s.io/autoscaler/cluster-autoscaler/config/test" | ||||
| 	. "k8s.io/autoscaler/cluster-autoscaler/utils/test" | ||||
| 	"k8s.io/kubernetes/pkg/scheduler/apis/config" | ||||
| 	schedulerframework "k8s.io/kubernetes/pkg/scheduler/framework" | ||||
| 
 | ||||
| 	apiv1 "k8s.io/api/core/v1" | ||||
|  | @ -102,3 +106,86 @@ func TestResourceList(t *testing.T) { | |||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestConfigFromPath(t *testing.T) { | ||||
| 	// temp dir
 | ||||
| 	tmpDir, err := os.MkdirTemp("", "scheduler-configs") | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 	defer os.RemoveAll(tmpDir) | ||||
| 
 | ||||
| 	// Note that even if we are passing minimal config like below
 | ||||
| 	// `ConfigFromPath` will set the rest of the default fields
 | ||||
| 	// on its own (including default profile and default plugins)
 | ||||
| 	correctConfigFile := filepath.Join(tmpDir, "correct_config.yaml") | ||||
| 	if err := os.WriteFile(correctConfigFile, | ||||
| 		[]byte(testconfig.SchedulerConfigMinimalCorrect), | ||||
| 		os.FileMode(0600)); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	decodeErrConfigFile := filepath.Join(tmpDir, "decode_err_no_version_config.yaml") | ||||
| 	if err := os.WriteFile(decodeErrConfigFile, | ||||
| 		[]byte(testconfig.SchedulerConfigDecodeErr), | ||||
| 		os.FileMode(0600)); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	validationErrConfigFile := filepath.Join(tmpDir, "invalid_percent_node_score_config.yaml") | ||||
| 	if err := os.WriteFile(validationErrConfigFile, | ||||
| 		[]byte(testconfig.SchedulerConfigInvalid), | ||||
| 		os.FileMode(0600)); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	tests := []struct { | ||||
| 		name           string | ||||
| 		path           string | ||||
| 		expectedErr    error | ||||
| 		expectedConfig *config.KubeSchedulerConfiguration | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:           "Empty scheduler config file path", | ||||
| 			path:           "", | ||||
| 			expectedErr:    fmt.Errorf(schedulerConfigLoadErr), | ||||
| 			expectedConfig: nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Correct scheduler config", | ||||
| 			path:           correctConfigFile, | ||||
| 			expectedErr:    nil, | ||||
| 			expectedConfig: &config.KubeSchedulerConfiguration{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Scheduler config with decode error", | ||||
| 			path:           decodeErrConfigFile, | ||||
| 			expectedErr:    fmt.Errorf(schedulerConfigDecodeErr), | ||||
| 			expectedConfig: nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:           "Invalid scheduler config", | ||||
| 			path:           validationErrConfigFile, | ||||
| 			expectedErr:    fmt.Errorf(schedulerConfigInvalidErr), | ||||
| 			expectedConfig: nil, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for i, test := range tests { | ||||
| 		t.Run(fmt.Sprintf("case_%d: %s", i, test.name), func(t *testing.T) { | ||||
| 			cfg, err := ConfigFromPath(test.path) | ||||
| 			if test.expectedConfig == nil { | ||||
| 				assert.Nil(t, cfg) | ||||
| 			} else { | ||||
| 				assert.NotNil(t, cfg) | ||||
| 			} | ||||
| 
 | ||||
| 			if test.expectedErr == nil { | ||||
| 				assert.NoError(t, err) | ||||
| 			} else { | ||||
| 				assert.ErrorContains(t, err, test.expectedErr.Error()) | ||||
| 			} | ||||
| 		}) | ||||
| 
 | ||||
| 	} | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue