Compare commits

...
362 Commits
Author SHA1 Message Date
Kubernetes Prow Robot b49fd27d10 Merge pull request #1830 from davidandreoletti/patch-1
Change annotations condition to deploymentAnnotations for Deployment object annotations
2026-02-19 14:13:39 +05:30
Kubernetes Prow Robot ce6bf5b735 Merge pull request #1832 from a7i/v0.35.0-docs-manifests
[v0.35.0] update docs and manifests
2026-02-19 10:23:37 +05:30
Kubernetes Prow Robot 86e96b5b04 Merge pull request #1831 from a7i/amir/CVE-2024-44337
chore: upgrade github.com/gomarkdown/markdown to latest version
2026-02-19 09:37:37 +05:30
Amir Alavi 86070c62c6 [v0.35.0] update docs and manifests 2026-02-18 22:49:59 -05:00
Amir Alavi 118f466290 chore: upgrade github.com/gomarkdown/markdown to latest version
Upgrade github.com/gomarkdown/markdown from v0.0.0-20240328165702-4d01890c35c0
to v0.0.0-20260217112301-37c66b85d6ab (latest as of 2026-02-17)
2026-02-18 22:39:34 -05:00
Kubernetes Prow Robot 0de5bad232 Merge pull request #1827 from a7i/k8s-1.35
[v0.35.0] bump to kubernetes 1.35 deps
2026-02-18 16:31:37 +05:30
David Andreoletti 66075b069a Change annotations condition to deploymentAnnotations 2026-02-18 17:59:03 +08:00
Kubernetes Prow Robot f587486296 Merge pull request #1829 from ingvagabund/bump-golangci-lint
bump(golangci-lint): update and migrate
2026-02-18 15:03:40 +05:30
Jan Chaloupka a868c8d129 chore: update the code based on golangci-lint report 2026-02-17 22:01:25 +01:00
Jan Chaloupka 91c1297a54 bump(golangci-lint): update and migrate
golangci-lint migrate to make .golangci.yaml v2 compatible
2026-02-17 21:53:53 +01:00
Kubernetes Prow Robot 760e9cc2e1 Merge pull request #1828 from ingvagabund/extend-list-of-supported-go-versions
chore: extend the list of supported Go versions
2026-02-17 16:27:03 +05:30
Jan Chaloupka a4ac8447b6 chore: extend the list of supported Go versions 2026-02-17 11:27:24 +01:00
Amir Alavi a206a88d86 [v0.35.0] bump to kubernetes 1.35 deps
Signed-off-by: Amir Alavi <amiralavi7@gmail.com>
2026-02-15 19:50:50 -05:00
Kubernetes Prow Robot 7221fa7613 Merge pull request #1822 from sammedsingalkar09/master
Update go dependecies to fix vulnerabilities
2026-02-06 01:52:31 +05:30
Kubernetes Prow Robot 6e33b690d7 Merge pull request #1823 from ingvagabund/prom-client-controller
refactor: move prometheus client controller related code under a seperate file
2026-02-05 21:14:33 +05:30
Jan Chaloupka f4718bf928 refactor(prom client controllers): change the one letter receiver into ctrl 2026-02-05 15:54:49 +01:00
Jan Chaloupka f149f5a083 refactor: move prometheus client controller related code under a seperate file 2026-02-05 15:42:47 +01:00
Kubernetes Prow Robot 8de50a8a17 Merge pull request #1815 from ingvagabund/new-profile-under-new-descheduler
feat(pkg/descheduler): create profiles outside the descheduling cycle
2026-02-05 14:36:35 +05:30
Jan Chaloupka b214c14793 feat(pkg/descheduler): create profiles outside the descheduling cycle 2026-02-04 20:04:31 +01:00
Kubernetes Prow Robot fc863ff58d Merge pull request #1821 from ingvagabund/prom-client-controllers
refactor(promClientController): split it into two prom client controllers
2026-02-04 22:48:32 +05:30
sammedsingalkar09 f801f34346 update dependecies 2026-02-04 22:32:15 +05:30
Jan Chaloupka d262c7af44 refactor(TestPromClientControllerSync_EventHandler): be more verbose about the target expectations 2026-02-04 17:44:17 +01:00
Jan Chaloupka 4b5be0a772 feat(prometheus client reconciling): be more strict about clearing the previous connection
To avoid stalling connections that are not expected to be kept. E.g.
when an invalid secret is provided.
2026-02-04 17:16:44 +01:00
Jan Chaloupka 29e5a51cb5 refactor(newSecretBasedPromClientController): inline setupPrometheusProvider into newSecretBasedPromClientController 2026-02-04 17:16:15 +01:00
Jan Chaloupka a91a02cadc refactor(newSecretBasedPromClientController): move prometheus config validation under newSecretBasedPromClientController 2026-02-04 17:14:58 +01:00
Jan Chaloupka 964df4ce95 refactor(promClientController): split it into two prom client controllers 2026-02-04 16:59:13 +01:00
Kubernetes Prow Robot 5fb70c12c7 Merge pull request #1820 from ingvagabund/refactorings
test(token reconciling): have tests initialize the prom client reconciling through the descheduler's bootstraping entry too
2026-02-04 18:31:59 +05:30
Jan Chaloupka c357bc3d28 test(TestReconcileInClusterSAToken): have prometheus client tests build an entire descheduler as well 2026-02-04 13:26:00 +01:00
Jan Chaloupka f8a4efd4f0 format(TestReconcileInClusterSAToken): indent the code by two tabs 2026-02-04 13:18:53 +01:00
Jan Chaloupka 0822f6f99a test(TestPromClientControllerSync_EventHandler): have prometheus client tests build an entire descheduler as well 2026-02-04 13:18:07 +01:00
Jan Chaloupka 4361c133e9 format(TestPromClientControllerSync_EventHandler): indent the code by two tabs 2026-02-04 13:11:20 +01:00
Jan Chaloupka 818675f73e refactor(TestPromClientControllerSync_EventHandler): get secret name and namespace from the implicitly generated secret object 2026-02-04 12:47:41 +01:00
Jan Chaloupka a8f618c3fd test(TestPromClientControllerSync_ClientCreation): have prometheus client tests build an entire descheduler as well 2026-02-04 12:41:31 +01:00
Jan Chaloupka 760bacab34 format(TestPromClientControllerSync_ClientCreation): indent the code by two tabs 2026-02-04 12:40:12 +01:00
Jan Chaloupka aa21eed809 refactor(setupPromClientControllerTest): inject context instead of creating a stop channel 2026-02-04 12:33:53 +01:00
Kubernetes Prow Robot 8849882944 Merge pull request #1819 from ingvagabund/bootstrap-descheduler
Deduplicate descheduler initialization code so unit tests test more of the production code
2026-02-04 16:33:55 +05:30
Jan Chaloupka 807f580204 test(pkg/descheduler): TestPluginPrometheusClientAccess_Secret to test secret based token reconciliation
This test makes sure the secret is reconciled and available in plugins
while running the descheduler from its main entry point.
2026-02-04 11:31:18 +01:00
Jan Chaloupka 643179cdac feat(pkg/descheduler): setup prometheus client for the dry run mode descheduler as well
The secret is used for read-only operations so it can be safely wired.
2026-02-04 11:29:09 +01:00
Jan Chaloupka d1671034df refactor(pkg/descheduler): move the main descheduler run entry under a dedicated function 2026-02-03 23:31:29 +01:00
Jan Chaloupka f26ab740fe feat(pkg/descheduler): do not cancel the context when runDeschedulerLoop fails
When the descheduler is running in the dry mode the the kube client sandbox restoring may fail.
Which can be caused by timeouts when waiting for internal caches to sync.
The internal timeouts depend on the cluster size which changes in time.
No reason to cancel the context because of that.
2026-02-03 23:31:06 +01:00
Jan Chaloupka 0d05e5dd1f refactor(pkg/descheduler): dedicated function for setting token reconciliation 2026-02-03 23:31:01 +01:00
Jan Chaloupka e402adb9f1 refactor(pkg/descheduler): start factory informers and other descheduler parts during bootstrap
This will help with testing as the initDescheduler function under
descheduler_test.go is now much closer to the production invocation.
2026-02-03 21:33:28 +01:00
Jan Chaloupka 0c307cf7b9 refactor(pkg/descheduler): bootstrap a descheduler through a dedicated function 2026-02-03 21:26:16 +01:00
Jan Chaloupka a71c2ab331 refactor(pkg/descheduler): single ctx with cancel with the whole RunDeschedulerStrategies function for consistency 2026-02-03 21:15:13 +01:00
Kubernetes Prow Robot ffd05ff9c1 Merge pull request #1818 from ingvagabund/prom-client-testing
tests: Prom client testing
2026-02-02 19:04:28 +05:30
Kubernetes Prow Robot a60ab7b5ed Merge pull request #1786 from W1seKappa/add-namespace-label-selector
Add namespace label selector
2026-02-02 18:24:29 +05:30
Danila Bobkov 4361eefaf2 add namespaceLabelSelector
Signed-off-by: Danila Bobkov <danila.bobkov@flant.com>
2026-02-02 15:22:11 +03:00
Jan Chaloupka 5a53f16526 test(promClientController): unit test the implementation
Currently, there's a single prometheus client reconciler for both in
cluster and secret based strategies. The in cluster reconciling is run in
sync with each descheduling cycle. An in file token either changes or it
does not. If changed a new prometheus client is created. The secret
based reconciling is run async and watches for secret object changes. If a
secret changes a new client is created. The internal state of the
reconciler keeps previous connection data for clearing and checks.

The current reconciler implementation lacks mutually exclusive access.
So data races are possible. The prometheus configuration validation is
performed during every sync. The future refactorings is expected to move
the validation to the creation phase of the reconciler.

The extra unit testing is expected to cover the following scenarios:
- in cluster:
  - in file token is unchanged: no-op
  - in file token is changed: client is created or fails to be created
- secret:
  - no secret is not found: no client creation, internal state cleared
  - secret is found: if token changed a new client created, otherwise
    no-op
- prometheus config validation
- prometheus client injection

Any error during new prom client creation should be followed by closing
the previous connection and reseting the internal state. Yet, the error
handling is not that strict currently. So the current extra unit testing
keeps the incomplete testing cases as they are.

Other use of the tests is to make sure every time a new prometheus
client is created a descheduling cycle injects a new profile with the
updated prometheus clients. So the future refactoring does not introduce
a regression.
2026-02-01 18:13:18 +01:00
Jan Chaloupka 12b1ddfb52 refactor(pkg/descheduler): move prometheus client specific code under a dedicated promClientController
The underlying implementation is the same. Only moving the code under a
separate controller that can be unit test independently of the
descheduler type implementation.
2026-02-01 12:42:27 +01:00
Kubernetes Prow Robot 0b4fd3544c Merge pull request #1816 from ingvagabund/prometheus-client-update-test
test(pkg/descheduler): test a prometheus client update propagates to a plugin profile handle
2026-01-26 03:01:08 +05:30
Jan Chaloupka fe2fb603f4 test(pkg/descheduler): test a prometheus client update propagates to a plugin profile handle 2026-01-25 22:00:28 +01:00
Kubernetes Prow Robot a500ff9c64 Merge pull request #1814 from ingvagabund/refactorings
fix(kubeClientSandbox): do not wait for pods in the fake indexers if they are already deleted
2026-01-24 19:48:13 +05:30
Jan Chaloupka 263db33052 fix(kubeClientSandbox): do not wait for pods in the fake indexers if they are already deleted 2026-01-24 14:49:38 +01:00
Jan Chaloupka 45dc5a20d3 test(kubeClientSandbox): more unit tests 2026-01-24 14:47:17 +01:00
Jan Chaloupka f520856095 refactor(kubeClientSandbox): move the code under a separate file 2026-01-24 14:47:10 +01:00
Jan Chaloupka e53b3d5dce refactor(pkg/descheduler): drop unsed clientset parameter from descheduler.runProfiles 2026-01-23 22:27:59 +01:00
Kubernetes Prow Robot 72d61286eb Merge pull request #1812 from ingvagabund/register-fake-factory-only-once
refactor(pkg/descheduler): create fake shared informer factory only once
2026-01-24 02:43:28 +05:30
Jan Chaloupka 770ec5affa refactor(pkg/descheduler): create fake shared informer factory only once 2026-01-23 21:34:09 +01:00
Kubernetes Prow Robot 38d99dd0c3 Merge pull request #1813 from ingvagabund/refactorings
refactor(pkg/descheduler): more handlers and dropping unused code
2026-01-23 21:27:30 +05:30
Jan Chaloupka 8f5a83279e refactor(pkg/descheduler): drop unused fakeEvictedPods variables in the unit tests 2026-01-23 15:31:57 +01:00
Jan Chaloupka 4daa7e2fbf refactor(pkg/descheduler): move prometheus setup under a helper
Prometheus is not used anywhere in the tests so there's no need to setup
it there.
2026-01-23 15:31:57 +01:00
Jan Chaloupka 433f0dbb8c refactor(pkg/descheduler): define a helper for newKubeClientSandbox with the default list of resources 2026-01-23 15:31:51 +01:00
Kubernetes Prow Robot cc96a3ee7a Merge pull request #1811 from ingvagabund/kube-client-sandbox
refactor(pkg/operator): replace informerResource with a kubeClientSandbox
2026-01-22 19:59:28 +05:30
Jan Chaloupka ff580a0eff refactor(kubeClientSandbox): keep a cache of evicted pods and allow to reset it at the end of each descheduling cycle 2026-01-22 14:49:47 +01:00
Jan Chaloupka 4af097a806 refactor(pkg/operator): create a helper for registering indexer in the dry run mode 2026-01-22 14:44:41 +01:00
Jan Chaloupka b3f0184af8 refactor(kubeClientSandbox): helpers for creating a node selector and node selector indexer 2026-01-22 14:44:32 +01:00
Jan Chaloupka 881ead3ed2 refactor(kubeClientSandbox): set the create pods reactor in buildSandbox 2026-01-22 14:44:14 +01:00
Jan Chaloupka fc6d0d1132 refactor(pkg/operator): replace informerResource with a kubeClientSandbox 2026-01-22 14:41:48 +01:00
Kubernetes Prow Robot 85b1d97dda Merge pull request #1810 from ingvagabund/refactorings
chore(pkg/descheduler): make TestPodEvictorReset table driven
2026-01-20 19:08:49 +05:30
Jan Chaloupka b6aadc1643 chore(pkg/descheduler): make TestPodEvictorReset table driven 2026-01-20 12:51:58 +01:00
Kubernetes Prow Robot c4ec31684f Merge pull request #1802 from ingvagabund/global-node-selector-as-indexer
feat: register a node indexer for the global node selector instead of listing nodes with the selector
2026-01-12 15:08:13 +05:30
Kubernetes Prow Robot 7d2c31cd39 Merge pull request #1808 from ingvagabund/profile-instance-id
feat(profile): inject a plugin instance ID to each built plugin
2026-01-09 15:33:43 +05:30
Jan Chaloupka cf9edca33c feat(profile): inject a plugin instance ID to each built plugin 2026-01-06 12:26:35 +01:00
Kubernetes Prow Robot f164943257 Merge pull request #1807 from ingvagabund/docs
doc(Design Decisions FAQ): Why doesn't the framework provide helpers for registering and retrieving indexers for plugins
2026-01-05 21:20:39 +05:30
Jan Chaloupka 1fe9e2c345 doc(Design Decisions FAQ): Why doesn't the framework provide helpers for registering and retrieving indexers for plugins 2026-01-05 16:10:39 +01:00
Kubernetes Prow Robot 16ccff8ed8 Merge pull request #1806 from ingvagabund/profile-refactoring
refactor(pkg/framework/profile): dedup unit test code
2026-01-05 15:12:37 +05:30
Jan Chaloupka 38f0f15787 chore: make gen 2026-01-04 20:23:13 +01:00
Jan Chaloupka 52f2aea444 refactor(pkg/framework/profile): add registerDefaultEvictor helper function 2026-01-04 19:43:47 +01:00
Jan Chaloupka f3c63011cc refactor(pkg/framework/profile): add fake plugin registration helpers 2026-01-04 19:43:29 +01:00
Jan Chaloupka 47b939dd86 refactor(pkg/framework/profile): build a profile through a shared function to reduce code duplication 2026-01-04 19:42:30 +01:00
Kubernetes Prow Robot 89c88f483b Merge pull request #1800 from ingvagabund/readnodes-unit-test
fix(TestReadyNodesWithNodeSelector): make sure nodeLister.List always returns a non-empty list so the lister is always tested
2025-12-26 20:48:31 +05:30
Kubernetes Prow Robot d558fa3a5b Merge pull request #1804 from ingvagabund/refactorings
refactor(plugins): simplify the way pods are created
2025-12-21 14:38:30 -08:00
Jan Chaloupka 7ef3673d4c refactor: inline single-statement apply functions in BuildTestPod calls 2025-12-21 21:41:59 +01:00
Jan Chaloupka 988e0b8868 refactor: replace pod.Spec.Volumes with test.SetHostPathEmptyDirVolumeSource in plugin tests 2025-12-21 21:36:24 +01:00
Jan Chaloupka fc8ae3b4e8 refactor: replace pod.Spec.Priority with test.SetPodPriority in plugin tests 2025-12-21 21:36:05 +01:00
Kubernetes Prow Robot 6d7fedc982 Merge pull request #1803 from ingvagabund/refactor-defaultevictir-test
refactor(TestDefaultEvictor): de-dup code and use helpers
2025-12-20 06:54:31 -08:00
Jan Chaloupka 769ded35f1 make fmt 2025-12-20 15:24:37 +01:00
Jan Chaloupka 3283635149 refactor(defaultevictor_test): move newProtectedStorageClassesConfig to package level
Move the newProtectedStorageClassesConfig helper function from local scope
to package level so it can be reused by both TestDefaultEvictorFilter and
Test_protectedPVCStorageClasses, eliminating code duplication.
2025-12-20 15:17:06 +01:00
Jan Chaloupka 994ce3e2f7 refactor(TestDefaultEvictor): add setPodPVCVolumeWithFooClaimName helper function 2025-12-20 14:36:10 +01:00
Jan Chaloupka 90e4115b78 refactor(TestDefaultEvictor): add setPodLocalStorage helper function 2025-12-20 14:29:19 +01:00
Jan Chaloupka 8913d79d14 refactor(TestDefaultEvictor): replace mirror pod annotation assignments with test.SetMirrorPodAnnotation 2025-12-20 14:28:08 +01:00
Jan Chaloupka 9a5d7e8286 refactor(TestDefaultEvictor): replace system critical priority assignments with test.SetPodPriority 2025-12-20 14:27:18 +01:00
Jan Chaloupka 674e463bc2 refactor(TestDefaultEvictor): replace manual owner reference assignments with test utilities 2025-12-20 14:25:58 +01:00
Jan Chaloupka 1df3ef5030 refactor(TestDefaultEvictor): add setPodEvictAnnotation helper function 2025-12-20 14:22:28 +01:00
Jan Chaloupka 3068f8431a refactor(TestDefaultEvictor): add setPodNodeSelector helper function 2025-12-20 14:11:46 +01:00
Jan Chaloupka dfd2b95d2d refactor(TestDefaultEvictor): add setNodeLabel helper function 2025-12-20 14:08:11 +01:00
Jan Chaloupka 3bb4529c34 refactor(TestDefaultEvictor): use test.SetNormalOwnerRef 2025-12-20 14:00:40 +01:00
Jan Chaloupka b8765bd8ee refactor(TestDefaultEvictor): add setNodeTaint helper function 2025-12-20 13:54:50 +01:00
Jan Chaloupka d666e4b830 refactor(TestDefaultEvictor): add buildTestPod helper function 2025-12-20 13:43:00 +01:00
Jan Chaloupka 08f733863e refactor(TestDefaultEvictor): add buildTestNode helper function 2025-12-20 13:25:39 +01:00
Jan Chaloupka 93a516a58a feat: register a node indexer for the global node selector instead of listing nodes with the selector
To avoid iterating through every node every time a list of nodes is
requested. This is a prerequisition work for introducing profile level
node selectors.
2025-12-19 23:25:24 +01:00
Jan Chaloupka 60da931e0e fix(TestReadyNodesWithNodeSelector): make sure nodeLister.List always returns a non-empty list so the lister is always tested
The case of an empty list of nodes from the lister is not easy to catch.
This change makes sure one more initial condition is met.
2025-12-18 17:06:25 +01:00
Kubernetes Prow Robot 12a9db4da0 Merge pull request #1798 from ingvagabund/contriburing-descheduler
doc: introduce contributing guidelines specific to the project
2025-12-15 12:43:47 -08:00
Jan Chaloupka 41da7497c3 doc: introduce contributing guidelines specific to the project
The document is to be extended on the fly
2025-12-15 21:11:50 +01:00
Kubernetes Prow Robot b56f3cdae9 Merge pull request #1797 from ingvagabund/nodetaint-unit-test
refactor(TestDeletePodsViolatingNodeTaints): inline object creation
2025-12-15 09:51:45 -08:00
Jan Chaloupka 162a2d14b7 refactor(TestRemovePodsHavingTooManyRestarts): remove leftover comments 2025-12-15 18:20:38 +01:00
Jan Chaloupka 78788d72de refactor(node_taint_test): inline p15 2025-12-15 18:19:04 +01:00
Jan Chaloupka 956eeefede refactor(node_taint_test): inline p14 2025-12-15 18:18:59 +01:00
Jan Chaloupka 1f7bd1fba9 refactor(node_taint_test): inline p13 2025-12-15 18:18:56 +01:00
Jan Chaloupka 5fdf368593 refactor(node_taint_test): inline p12 2025-12-15 18:18:51 +01:00
Jan Chaloupka 50b6e178c1 refactor(node_taint_test): inline p11 2025-12-15 18:18:47 +01:00
Jan Chaloupka c1ad532c46 refactor(node_taint_test): inline p10 2025-12-15 18:18:43 +01:00
Jan Chaloupka 7e40aae2dc refactor(node_taint_test): inline p9 2025-12-15 18:18:39 +01:00
Jan Chaloupka e09bd976f5 refactor(node_taint_test): inline p8 2025-12-15 18:18:35 +01:00
Jan Chaloupka ffb1f44144 refactor(node_taint_test): inline p7 2025-12-15 18:18:24 +01:00
Jan Chaloupka cb595f2524 refactor(node_taint_test): inline p6 2025-12-15 18:17:34 +01:00
Jan Chaloupka c46817f6df refactor(node_taint_test): inline p5 2025-12-15 18:17:30 +01:00
Jan Chaloupka 032db38d6c refactor(node_taint_test): inline p4 2025-12-15 18:17:26 +01:00
Jan Chaloupka c1cd3ae794 refactor(node_taint_test): inline p3 2025-12-15 18:17:21 +01:00
Jan Chaloupka 060d9c8573 refactor(node_taint_test): inline p2 2025-12-15 18:17:17 +01:00
Jan Chaloupka 51bcf60ccf refactor(node_taint_test): inline p1 2025-12-15 18:17:12 +01:00
Jan Chaloupka b472549cf6 refactor(node_taint_test): add withKubeSystemCriticalPod helper 2025-12-15 18:16:58 +01:00
Jan Chaloupka c68e8a6d06 refactor(node_taint_test): add withTestTaintXToleration1 helper 2025-12-15 18:16:19 +01:00
Jan Chaloupka 68d9d4d044 refactor(node_taint_test): add datacenter label constants 2025-12-15 18:16:14 +01:00
Jan Chaloupka 452b1ff7d9 refactor(node_taint_test): add SetPodVolumes and withLocalStorageVolume helpers 2025-12-15 18:16:08 +01:00
Jan Chaloupka f123f78b44 refactor: add SetSystemCriticalPriority helper function 2025-12-15 18:15:31 +01:00
Jan Chaloupka ca0f7535fb refactor: add SetMirrorPodAnnotation helper function 2025-12-15 17:41:46 +01:00
Jan Chaloupka 78ff3fe92a refactor(node_taint_test): add withTestTaintToleration1 helper 2025-12-15 17:37:45 +01:00
Jan Chaloupka 0269283185 refactor(node_taint_test): add buildTestPodWithNormalOwnerRef helper 2025-12-15 17:34:18 +01:00
Jan Chaloupka 57ed329feb refactor(node_taint_test): inline node7 2025-12-15 17:26:04 +01:00
Jan Chaloupka b96a41a745 refactor(node_taint_test): inline node6 2025-12-15 17:25:27 +01:00
Jan Chaloupka 6b6f7ba5c7 refactor(node_taint_test): inline node5 2025-12-15 17:24:53 +01:00
Jan Chaloupka a3ca65ea14 refactor(node_taint_test): inline node4 2025-12-15 17:24:17 +01:00
Jan Chaloupka d81580c93e refactor(node_taint_test): inline node3 2025-12-15 17:23:42 +01:00
Jan Chaloupka 0f7ff8a2b7 refactor(node_taint_test): inline node2 2025-12-15 17:23:08 +01:00
Jan Chaloupka d27afd0319 refactor(node_taint_test): inline node1 2025-12-15 17:21:25 +01:00
Jan Chaloupka 3d48efdff4 refactor(node_taint_test): add dedicated functions for remaining nodes 2025-12-15 17:14:06 +01:00
Jan Chaloupka e5d5cf2229 refactor(node_taint_test): create dedicated functions for taint configurations 2025-12-15 17:11:54 +01:00
Jan Chaloupka f65209d4fa refactor(node_taint_test): inline addTaintsToNode 2025-12-15 17:08:50 +01:00
Jan Chaloupka b9ceb9144f refactor(node_taint_test): remove default false settings for evict flags 2025-12-15 17:01:43 +01:00
Jan Chaloupka 2bbec0cbc6 refactor(node_taint_test): apply pod single creation convention for p15 2025-12-15 16:55:21 +01:00
Jan Chaloupka a363da9806 refactor(node_taint_test): apply pod single creation convention for p14 2025-12-15 16:55:02 +01:00
Jan Chaloupka 63b3bd3b4d refactor(node_taint_test): apply pod single creation convention for p13 2025-12-15 16:54:44 +01:00
Jan Chaloupka 7fb935c650 refactor(node_taint_test): replace GetNormalPodOwnerRefList with SetNormalOwnerRef 2025-12-15 16:52:42 +01:00
Kubernetes Prow Robot f85b2f8d4d Merge pull request #1796 from ingvagabund/nodeaffinity-unit-test
refactor(TestRemovePodsViolatingNodeAffinity): inline object creation
2025-12-15 07:45:47 -08:00
Jan Chaloupka 0580b5942c refactor(node_taint_test): apply pod single creation convention for p12 2025-12-15 16:44:46 +01:00
Jan Chaloupka 4171af7e8a refactor(node_taint_test): apply pod single creation convention for p11 2025-12-15 16:44:19 +01:00
Jan Chaloupka a1678cd464 refactor(node_taint_test): apply pod single creation convention for p10 2025-12-15 16:44:01 +01:00
Jan Chaloupka 2f90d1dd01 refactor(node_taint_test): apply pod single creation convention for p9 2025-12-15 16:43:32 +01:00
Jan Chaloupka f0cda32b6e refactor(node_taint_test): apply pod single creation convention for p8 2025-12-15 16:43:01 +01:00
Jan Chaloupka 43523113ff refactor(node_taint_test): apply pod single creation convention for p7 2025-12-15 16:42:34 +01:00
Jan Chaloupka 1b7889f4a3 refactor(node_taint_test): apply pod single creation convention for p6 2025-12-15 16:42:00 +01:00
Jan Chaloupka b86315f097 refactor(node_taint_test): apply pod single creation convention for p5 2025-12-15 16:41:39 +01:00
Jan Chaloupka 0d496dfc5d refactor(node_taint_test): apply pod single creation convention for p4 2025-12-15 16:41:19 +01:00
Jan Chaloupka d6b35eaed6 refactor(node_taint_test): apply pod single creation convention for p3 2025-12-15 16:40:41 +01:00
Jan Chaloupka dc18f9f330 refactor(node_taint_test): apply pod single creation convention for p2 2025-12-15 16:39:54 +01:00
Jan Chaloupka 39212419e6 refactor(node_taint_test): apply pod single creation convention for p1 2025-12-15 16:38:35 +01:00
Jan Chaloupka 64f77ce6ee refactor(node_taint_test): apply node single creation convention for node7 2025-12-15 16:35:38 +01:00
Jan Chaloupka ca5326c5c4 refactor(node_taint_test): apply node single creation convention for node6 2025-12-15 16:35:12 +01:00
Jan Chaloupka 9cf075ffc4 refactor(node_taint_test): apply node single creation convention for node5 2025-12-15 16:34:47 +01:00
Jan Chaloupka 3325fe0b8b refactor(node_taint_test): apply node single creation convention for node2 2025-12-15 16:33:28 +01:00
Jan Chaloupka 6c41ebd8f3 refactor(node_taint_test): apply node single creation convention for node1 2025-12-15 16:33:12 +01:00
Jan Chaloupka ba034d6e0e refactor(node_taint_test): add node name constants 2025-12-15 16:23:38 +01:00
Jan Chaloupka 3289554f90 refactor(node_taint_test): add buildTestPod helper function 2025-12-15 16:20:01 +01:00
Jan Chaloupka 72575c2f23 refactor(node_taint_test): add buildTestNode helper function 2025-12-15 16:17:49 +01:00
Jan Chaloupka 07616c3fc0 refactor(TestRemovePodsHavingTooManyRestarts): make fmt 2025-12-15 16:14:50 +01:00
Jan Chaloupka cad120881f refactor(TestRemovePodsViolatingNodeAffinity): apply pod single creation convention 2025-12-15 16:02:37 +01:00
Jan Chaloupka aec4416099 refactor(TestRemovePodsViolatingNodeAffinity): add buildUnschedulableNodeWithLabels function 2025-12-15 15:59:01 +01:00
Jan Chaloupka 7b9d5d2539 refactor(TestRemovePodsViolatingNodeAffinity): inline nodeWithoutLabels 2025-12-15 15:50:25 +01:00
Jan Chaloupka 9f7629136f refactor(TestRemovePodsViolatingNodeAffinity): inline nodeWithLabels 2025-12-15 15:48:00 +01:00
Jan Chaloupka 42d255fd95 refactor(TestRemovePodsViolatingNodeAffinity): update addPodsToNode to accept nodeName 2025-12-15 15:33:58 +01:00
Jan Chaloupka 183a138d82 refactor(TestRemovePodsViolatingNodeAffinity): add constants for node names 2025-12-15 15:28:03 +01:00
Kubernetes Prow Robot f669c45892 Merge pull request #1795 from ingvagabund/podantiaffinity-unit-test
refactor(TestPodAntiAffinity): inline object creation
2025-12-15 05:53:45 -08:00
Jan Chaloupka a2ffbc1261 refactor(TestRemovePodsViolatingNodeAffinity): apply unit test convention for podWithNodeAffinity 2025-12-15 14:35:05 +01:00
Jan Chaloupka 2cda1bd89d refactor(TestRemovePodsViolatingNodeAffinity): deduplicate pod creation with buildTestPod helper 2025-12-15 14:35:03 +01:00
Jan Chaloupka 691a1da43b refactor(TestRemovePodsViolatingNodeAffinity): apply unit test convention for unschedulableNodeWithLabels 2025-12-15 14:34:59 +01:00
Jan Chaloupka 8fe74c7a0c refactor(TestRemovePodsViolatingNodeAffinity): apply unit test convention for nodeWithLabels 2025-12-15 14:34:54 +01:00
Jan Chaloupka 102bd6a91d refactor(TestRemovePodsViolatingNodeAffinity): deduplicate node creation with buildTestNode helper 2025-12-15 14:34:44 +01:00
Jan Chaloupka 3d1e15bb82 refactor(TestPodAntiAffinity): apply gofumpt formatting 2025-12-15 14:21:32 +01:00
Jan Chaloupka 3c02d9029c refactor(TestPodAntiAffinity): inline p1, p2, p4 builders 2025-12-15 14:13:51 +01:00
Jan Chaloupka 57a3e610a7 refactor(TestPodAntiAffinity): deduplicate p1, p3, p4 builders 2025-12-15 14:06:05 +01:00
Jan Chaloupka 7cec27d467 refactor(TestPodAntiAffinity): inline nonEvictablePod 2025-12-15 13:56:37 +01:00
Jan Chaloupka 688b45011a refactor(TestPodAntiAffinity): inline p11 2025-12-15 13:56:07 +01:00
Jan Chaloupka a96451030c refactor(TestPodAntiAffinity): inline p10 2025-12-15 13:55:36 +01:00
Jan Chaloupka a4930ebc83 refactor(TestPodAntiAffinity): inline p9 2025-12-15 13:55:11 +01:00
Jan Chaloupka ad872f8b77 refactor(TestPodAntiAffinity): inline p8 2025-12-15 13:54:40 +01:00
Jan Chaloupka a0654df270 refactor(TestPodAntiAffinity): inline p7 2025-12-15 13:54:07 +01:00
Jan Chaloupka 03b5a9a967 refactor(TestPodAntiAffinity): inline p6 2025-12-15 13:53:43 +01:00
Jan Chaloupka 9f2d22c1f7 refactor(TestPodAntiAffinity): inline p5 2025-12-15 13:53:19 +01:00
Jan Chaloupka cbe1c1e559 refactor(TestPodAntiAffinity): inline p4 2025-12-15 13:52:21 +01:00
Jan Chaloupka 87182c5e8f refactor(TestPodAntiAffinity): inline p3 2025-12-15 13:51:58 +01:00
Jan Chaloupka 2765e31048 refactor(TestPodAntiAffinity): inline p2 2025-12-15 13:51:00 +01:00
Jan Chaloupka 87f675a2cd refactor(TestPodAntiAffinity): inline p1 2025-12-15 13:48:01 +01:00
Jan Chaloupka a400a66d51 refactor(TestPodAntiAffinity): create dedicated builders for p1-p4 and nonEvictablePod 2025-12-15 13:43:11 +01:00
Jan Chaloupka fa427a2b37 refactor(TestPodAntiAffinity): deduplicate setting Labels for foo1-bar1 2025-12-15 13:38:58 +01:00
Jan Chaloupka 90672630da refactor(TestPodAntiAffinity): deduplicate setting Labels for foo-bar 2025-12-15 13:37:55 +01:00
Jan Chaloupka 6a00214457 refactor(TestPodAntiAffinity): deduplicate setPodAntiAffinity for foo1-bar1 2025-12-15 13:36:27 +01:00
Jan Chaloupka 9413b0c654 refactor(TestPodAntiAffinity): deduplicate setPodAntiAffinity for foo-bar 2025-12-15 13:34:05 +01:00
Jan Chaloupka 3072a59ea0 refactor(TestPodAntiAffinity): ensure nonEvictablePod is created only through apply argument 2025-12-15 13:25:04 +01:00
Jan Chaloupka 0e56823865 refactor(TestPodAntiAffinity): ensure p11 is created only through apply argument 2025-12-15 13:24:09 +01:00
Jan Chaloupka ea80f7d307 refactor(TestPodAntiAffinity): ensure p10 is created only through apply argument 2025-12-15 13:23:46 +01:00
Jan Chaloupka 6638b976ad refactor(TestPodAntiAffinity): ensure p9 is created only through apply argument 2025-12-15 13:23:10 +01:00
Jan Chaloupka 116385718f refactor(TestPodAntiAffinity): ensure p8 is created only through apply argument 2025-12-15 13:22:35 +01:00
Jan Chaloupka 5ad695166a refactor(TestPodAntiAffinity): ensure p7 is created only through apply argument 2025-12-15 13:22:12 +01:00
Jan Chaloupka d5e0ec597f refactor(TestPodAntiAffinity): ensure p6 is created only through apply argument 2025-12-15 13:21:32 +01:00
Jan Chaloupka 4b86cdd31a refactor(TestPodAntiAffinity): ensure p5 is created only through apply argument 2025-12-15 13:20:53 +01:00
Jan Chaloupka 99527292e0 refactor(TestPodAntiAffinity): ensure p4 is created only through apply argument 2025-12-15 13:20:06 +01:00
Jan Chaloupka cf79af6fba refactor(TestPodAntiAffinity): ensure p3 is created only through apply argument 2025-12-15 13:19:38 +01:00
Jan Chaloupka da55c779f2 refactor(TestPodAntiAffinity): ensure p2 is created only through apply argument 2025-12-15 13:18:50 +01:00
Kubernetes Prow Robot bc6500d917 Merge pull request #1794 from ingvagabund/toomanyrestarts-unit-test
refactor(TestRemovePodsHavingTooManyRestarts): inline object creation
2025-12-15 03:59:45 -08:00
Jan Chaloupka c5b9debe56 refactor(TestPodAntiAffinity): ensure p1 is created only through apply argument 2025-12-15 12:57:19 +01:00
Jan Chaloupka 18f847bbe8 refactor(TestPodAntiAffinity): create buildTestPodForNode1 to deduplicate nodeName1 2025-12-15 12:54:42 +01:00
Jan Chaloupka 6e753ac5fb refactor(TestPodAntiAffinity): create buildTestPod helper to deduplicate 100 and 0 literals 2025-12-15 12:52:02 +01:00
Jan Chaloupka b797ca6ba2 refactor(TestPodAntiAffinity): inline node2, node3, node4, and node5 2025-12-15 12:48:50 +01:00
Jan Chaloupka 4ffabad669 refactor(TestPodAntiAffinity): create buildTestNode1 and inline node1 2025-12-15 12:45:07 +01:00
Jan Chaloupka bba62ccb93 refactor(TestPodAntiAffinity): extract setNodeMainRegionLabel helper 2025-12-15 12:40:46 +01:00
Jan Chaloupka 1f856595f5 refactor(TestPodAntiAffinity): add nodeName constants
refactor(TestPodAntiAffinity): replace node.Name with nodeName constants
2025-12-15 12:36:23 +01:00
Jan Chaloupka 993162dd44 refactor(TestPodAntiAffinity): replace test.BuildTestNode with buildTestNode helper 2025-12-15 12:30:30 +01:00
Jan Chaloupka ee73336fd8 refactor(TestRemovePodsHavingTooManyRestarts): inline node5 2025-12-15 12:25:03 +01:00
Jan Chaloupka 75f655e271 refactor(TestRemovePodsHavingTooManyRestarts): inline node4 2025-12-15 12:24:46 +01:00
Jan Chaloupka 76895273f9 refactor(TestRemovePodsHavingTooManyRestarts): inline node3 2025-12-15 12:11:09 +01:00
Jan Chaloupka 35d2103fcf refactor(TestRemovePodsHavingTooManyRestarts): inline node2 2025-12-15 12:10:34 +01:00
Jan Chaloupka b069ae009a refactor(TestRemovePodsHavingTooManyRestarts): inline node1 2025-12-15 12:08:58 +01:00
Jan Chaloupka be275deea5 refactor(TestRemovePodsHavingTooManyRestarts): node3 as a constant 2025-12-15 12:06:47 +01:00
Jan Chaloupka a5d3241a54 refactor(TestRemovePodsHavingTooManyRestarts): replace test.BuildTestNode with buildTestNode helpers 2025-12-15 12:05:44 +01:00
Jan Chaloupka 2af9ea8449 refactor(TestRemovePodsHavingTooManyRestarts): remove applyFunc and apply modifications in initPods 2025-12-15 12:04:20 +01:00
Jan Chaloupka 60fa5aa228 refactor(TestRemovePodsHavingTooManyRestarts): create all the pods as part of a unit test definition 2025-12-15 12:04:10 +01:00
Jan Chaloupka a94d22fd1b refactor(TestRemovePodsHavingTooManyRestarts): create all testing pods under initPods 2025-12-15 12:03:59 +01:00
Jan Chaloupka 8c70b02088 refactor(TestRemovePodsHavingTooManyRestarts): single testing pods creation 2025-12-15 12:03:22 +01:00
Jan Chaloupka ec58fed521 refactor(TestRemovePodsHavingTooManyRestarts): create each init container through a single invocation 2025-12-15 12:03:08 +01:00
Jan Chaloupka bf9cf0ee1c refactor(TestRemovePodsHavingTooManyRestarts): use test.Set...OwnerRef instead 2025-12-15 11:03:45 +01:00
Jan Chaloupka 6ebb0b7aa7 refactor(TestRemovePodsHavingTooManyRestarts): extract setPodContainerStatusRestartCount helper 2025-12-15 00:23:03 +01:00
Kubernetes Prow Robot bb01360776 Merge pull request #1793 from ingvagabund/duplicates-unit-test
refactor(TestRemoveDuplicates): reduce test code duplication
2025-12-14 08:35:44 -08:00
Jan Chaloupka c8bc668e04 refactor(TestRemoveDuplicatesUniformly): reduce duplication in setTolerations 2025-12-14 17:05:42 +01:00
Jan Chaloupka b64426888b refactor(TestRemoveDuplicatesUniformly): reduce duplication in setWorkerLabelSelector 2025-12-14 17:05:21 +01:00
Jan Chaloupka 1306cf38a1 refactor(TestRemoveDuplicatesUniformly): reduce duplication in setNotMasterNodeSelector 2025-12-14 17:04:18 +01:00
Jan Chaloupka bc06d1be83 refactor: replace test.BuildTestPod with buildTestPodForNode 2025-12-14 16:40:55 +01:00
Jan Chaloupka c9e87bb97d Merge pull request #1792 from ingvagabund/duplicates-unit-test
refactor(TestFindDuplicatePods): reduce duplicates and inline
2025-12-14 17:27:11 +02:00
Jan Chaloupka 05b6d5e343 refactor(TestFindDuplicatePods): remove leftover comments 2025-12-14 16:16:24 +01:00
Jan Chaloupka 044f75dcec refactor(TestFindDuplicatePods): inline node6 creation 2025-12-14 16:16:22 +01:00
Jan Chaloupka 6e62af3dbf refactor(TestFindDuplicatePods): inline node5 creation 2025-12-14 16:16:20 +01:00
Jan Chaloupka 2fac727be3 refactor(TestFindDuplicatePods): inline node4 creation 2025-12-14 16:16:17 +01:00
Jan Chaloupka babc4137a4 refactor(TestFindDuplicatePods): inline node3 creation 2025-12-14 16:16:12 +01:00
Jan Chaloupka fc033caf21 refactor(TestFindDuplicatePods): inline node2 creation 2025-12-14 16:16:10 +01:00
Jan Chaloupka fd524f2172 refactor(TestFindDuplicatePods): inline node1 creation 2025-12-14 16:07:27 +01:00
Jan Chaloupka 47275831ab refactor(TestFindDuplicatePods): apply buildTestNode helper to node variables
refactor(TestRemoveDuplicatesUniformly): apply buildTestNode helper
2025-12-14 16:00:50 +01:00
Jan Chaloupka b8b0fa0565 refactor(TestFindDuplicatePods): inline p20 pod creation 2025-12-14 14:27:05 +01:00
Jan Chaloupka daaa5896a9 refactor(TestFindDuplicatePods): inline p19 pod creation 2025-12-14 14:26:23 +01:00
Jan Chaloupka e27864717d refactor(TestFindDuplicatePods): inline p17 pod creation 2025-12-14 14:25:26 +01:00
Jan Chaloupka e8cf01591e refactor(TestFindDuplicatePods): inline p16 pod creation 2025-12-14 14:24:36 +01:00
Jan Chaloupka d7766cccfd refactor(TestFindDuplicatePods): inline p15 pod creation 2025-12-14 14:22:22 +01:00
Jan Chaloupka 3ebffe5a86 refactor(TestFindDuplicatePods): inline p13 pod creation 2025-12-14 14:20:48 +01:00
Jan Chaloupka 4e758c18e8 refactor(TestFindDuplicatePods): inline p12 pod creation 2025-12-14 14:19:19 +01:00
Jan Chaloupka 1c494f9c44 refactor(TestFindDuplicatePods): inline p11 pod creation 2025-12-14 14:17:37 +01:00
Jan Chaloupka 45dfe3011c refactor(TestFindDuplicatePods): inline p10 pod creation 2025-12-14 14:11:13 +01:00
Jan Chaloupka eeb459d6d4 refactor(TestFindDuplicatePods): inline p9 pod creation 2025-12-14 14:10:18 +01:00
Jan Chaloupka f3d91fc69f refactor(TestFindDuplicatePods): inline p8 pod creation 2025-12-14 14:04:23 +01:00
Jan Chaloupka e9dcd4e54d refactor(TestFindDuplicatePods): inline p7 pod creation 2025-12-14 14:03:22 +01:00
Jan Chaloupka 8490ed9c8f refactor(TestFindDuplicatePods): inline p6 pod creation 2025-12-14 14:01:21 +01:00
Jan Chaloupka 01fb826bd3 refactor(TestFindDuplicatePods): inline p5 pod creation 2025-12-14 13:58:38 +01:00
Jan Chaloupka 9b50aa91f8 refactor(TestFindDuplicatePods): inline p4 pod creation 2025-12-14 13:44:24 +01:00
Jan Chaloupka 7a5bf8c2f0 refactor(TestFindDuplicatePods): inline p3 pod creation 2025-12-14 13:43:34 +01:00
Jan Chaloupka df06442830 refactor(TestFindDuplicatePods): inline p2 pod creation 2025-12-14 13:39:56 +01:00
Jan Chaloupka 180548cc1a refactor(TestFindDuplicatePods): inline p1 pod creation 2025-12-14 13:37:23 +01:00
Jan Chaloupka 0aee6cff48 refactor(TestFindDuplicatePods): introduce buildTestPodWithRSOwnerRefWithNamespaceForNode1 helper 2025-12-14 13:33:56 +01:00
Jan Chaloupka 7a0257a682 refactor(TestFindDuplicatePods): introduce buildTestPodWithRSOwnerRefForNode1 helper 2025-12-14 13:25:40 +01:00
Jan Chaloupka f5253faeb0 refactor(TestFindDuplicatePods): set owner refs through a dedicated function 2025-12-14 13:25:34 +01:00
Jan Chaloupka 59f499e2cd refactor(TestFindDuplicatePods): replace direct ownerRef assignment with test.SetRSOwnerRef 2025-12-14 13:25:14 +01:00
Jan Chaloupka 008265db9b refactor(TestFindDuplicatePods): consolidate ownerRef as all the cases produce the same owner reference 2025-12-14 13:25:09 +01:00
Jan Chaloupka 61190b805b refactor(TestFindDuplicatePods): buildTestPodForNode1 for default testing configuration 2025-12-14 13:24:03 +01:00
Jan Chaloupka 8c83840bf9 Merge pull request #1791 from ingvagabund/duplicates-unit-test
refactor(TestFindDuplicatePods): have a pod fully created through BuildTestPod without any edits
2025-12-14 14:05:17 +02:00
Jan Chaloupka e46b5db6d5 refactor(TestFindDuplicatePods): have a pod fully created through BuildTestPod without any edits 2025-12-14 12:49:35 +01:00
Jan Chaloupka b21fb4a655 refactor(TestFindDuplicatePods): have a pod fully created through BuildTestPod without any edits 2025-12-14 12:49:35 +01:00
Jan Chaloupka 8f3c5f4978 refactor(TestFindDuplicatePods): drop unused variable 2025-12-14 12:49:27 +01:00
Jan Chaloupka 6f94e19385 refactor(TestFindDuplicatePods): have a pod fully created through BuildTestPod without any edits 2025-12-14 12:49:27 +01:00
Jan Chaloupka 3bb99512d8 refactor(TestFindDuplicatePods): have a pod fully created through BuildTestPod without any edits 2025-12-14 12:49:27 +01:00
Jan Chaloupka 56f49bc78f refactor(TestFindDuplicatePods): have a pod fully created through BuildTestPod without any edits 2025-12-14 12:49:27 +01:00
Jan Chaloupka 800dd280cd refactor(TestFindDuplicatePods): drop unused variable 2025-12-14 12:49:17 +01:00
Jan Chaloupka 8dada79593 refactor(TestFindDuplicatePods): have a pod fully created through BuildTestPod without any edits 2025-12-14 12:46:05 +01:00
Jan Chaloupka 660e2dba40 refactor(TestFindDuplicatePods): have a pod fully created through BuildTestPod without any edits 2025-12-14 12:46:03 +01:00
Jan Chaloupka 294ce39231 refactor(TestFindDuplicatePods): have a pod fully created through BuildTestPod without any edits 2025-12-14 12:46:01 +01:00
Jan Chaloupka f2031ddcb0 refactor(TestFindDuplicatePods): have a pod fully created through BuildTestPod without any edits 2025-12-14 12:45:59 +01:00
Jan Chaloupka 7435b5d474 refactor(TestFindDuplicatePods): have a pod fully created through BuildTestPod without any edits 2025-12-14 12:45:56 +01:00
Jan Chaloupka b5f177efa0 refactor(TestFindDuplicatePods): have a pod fully created through BuildTestPod without any edits 2025-12-14 12:45:54 +01:00
Jan Chaloupka 4a4ec4afb7 refactor(TestFindDuplicatePods): have a pod fully created through BuildTestPod without any edits 2025-12-14 12:45:52 +01:00
Jan Chaloupka 0c33be962d refactor(TestFindDuplicatePods): have a pod fully created through BuildTestPod without any edits 2025-12-14 12:45:50 +01:00
Jan Chaloupka 511ed214b0 refactor(TestFindDuplicatePods): have a pod fully created through BuildTestPod without any edits 2025-12-14 12:45:48 +01:00
Jan Chaloupka 3d4263bf5e refactor(TestFindDuplicatePods): have a pod fully created through BuildTestPod without any edits 2025-12-14 12:45:45 +01:00
Jan Chaloupka 96171413ba refactor(TestFindDuplicatePods): have a pod fully created through BuildTestPod without any edits 2025-12-14 12:45:43 +01:00
Jan Chaloupka 5578211253 refactor(TestFindDuplicatePods): have a pod fully created through BuildTestPod without any edits 2025-12-14 12:45:38 +01:00
Jan Chaloupka 08c2fc7621 refactor(TestFindDuplicatePods): have a pod fully created through BuildTestPod without any edits 2025-12-14 12:45:35 +01:00
Kubernetes Prow Robot 9e45259399 Merge pull request #1790 from ingvagabund/podlifetime-unit-tests
refactor(TestPodLifeTime): split the unit tests into smaller semantically close groups
2025-12-14 02:19:43 -08:00
Jan Chaloupka e5bbedb602 refactor(TestPodLifeTime): extract generic filtering tests 2025-12-13 14:27:13 +01:00
Jan Chaloupka 2710fd3781 refactor(TestPodLifeTime): extract evictor configuration tests 2025-12-13 14:06:24 +01:00
Jan Chaloupka 2658864ac0 refactor(TestPodLifeTime): extract eviction limits tests 2025-12-13 14:06:18 +01:00
Jan Chaloupka e05de87368 refactor(TestPodLifeTime): extract pod status reason tests 2025-12-13 14:06:10 +01:00
Jan Chaloupka 293a9ca4b7 refactor(TestPodLifeTime): extract container waiting reason tests 2025-12-13 14:06:04 +01:00
Jan Chaloupka 83151219e7 refactor(TestPodLifeTime): extract pod phase state tests 2025-12-13 14:05:18 +01:00
Jan Chaloupka fb0bddf85d refactor(TestPodLifeTime): extract age threshold tests 2025-12-13 14:04:26 +01:00
Jan Chaloupka 286f2848fc refactor(TestPodLifeTime): add shared test infrastructure 2025-12-13 14:03:21 +01:00
Jan Chaloupka d8d997a25d refactor(TestPodLifeTime): extract helper functions to package level 2025-12-13 13:58:26 +01:00
Kubernetes Prow Robot 5d7a483dc8 Merge pull request #1789 from ingvagabund/refactorings
feat(PodLifeTime): document the plugin with details that can be used for reasoning during reviews and design discussions
2025-12-12 04:54:12 -08:00
Jan Chaloupka 58076dd162 feat(PodLifeTime): document the plugin with details that can be used for
reasoning during reviews and design discussions
2025-12-12 11:55:27 +01:00
Kubernetes Prow Robot b6e81fdd4b Merge pull request #1787 from ingvagabund/refactorings
feat(TestPodLifeTime): check only expected pods are evicted
2025-12-11 11:37:31 -08:00
Jan Chaloupka 59dfd041a8 feat(TestPodLifeTime): check only expected pods are evicted 2025-12-11 17:08:25 +01:00
Kubernetes Prow Robot 7bf29ce56d Merge pull request #1785 from ingvagabund/refactorings
refactor(TestPodLifeTime): update unit test names and simplify pod creation
2025-12-11 04:35:30 -08:00
Jan Chaloupka c77f1a4ed2 refactor(TestPodLifeTime): update test names to better correspond to their purpose 2025-12-11 13:04:33 +01:00
Jan Chaloupka 7e14c6c7c4 refactor(TestPodLifeTime): drop applyPodsFunc function 2025-12-11 13:03:47 +01:00
Jan Chaloupka b4a0b8dbac fix(TestPodLifeTime): rename dev ns to default 2025-12-11 13:03:40 +01:00
Kubernetes Prow Robot 680b10099d Merge pull request #1784 from ingvagabund/refactorings
refactor(TestPodLifeTime): inline pod creation in each unit test to avoid accidental pod spec updates
2025-12-10 09:15:41 -08:00
Jan Chaloupka e92dda1a37 Merge pull request #1783 from ingvagabund/refactorings
refactor(TestPodLifeTime): consolidations, simplifications and node instance for each unit test
2025-12-10 17:38:21 +02:00
Jan Chaloupka 07dc0c61c5 refactor(TestPodLifeTime): inline pod creation in each unit test to avoid accidental pod spec updates 2025-12-10 16:33:28 +01:00
Jan Chaloupka cab310e55c refactor(TestPodLifeTime): inline pod creation in each unit test to avoid accidental pod spec updates 2025-12-10 16:32:54 +01:00
Jan Chaloupka 822a1d4c40 refactor(TestPodLifeTime): inline pod creation in each unit test to avoid accidental pod spec updates 2025-12-10 16:32:30 +01:00
Jan Chaloupka 1d7368b58d refactor(TestPodLifeTime): inline pod creation in each unit test to avoid accidental pod spec updates 2025-12-10 16:32:05 +01:00
Jan Chaloupka 70a71f54bc refactor(TestPodLifeTime): inline pod creation in each unit test to avoid accidental pod spec updates 2025-12-10 16:31:31 +01:00
Jan Chaloupka 3ea0eadcb3 refactor(TestPodLifeTime): inline pod creation in each unit test to avoid accidental pod spec updates 2025-12-10 16:31:25 +01:00
Jan Chaloupka 41a0a9c994 refactor(TestPodLifeTime): inline pod creation in each unit test to avoid accidental pod spec updates 2025-12-10 16:30:30 +01:00
Jan Chaloupka c707f53cec refactor(TestPodLifeTime): inline pod creation in each unit test to avoid accidental pod spec updates 2025-12-10 16:28:20 +01:00
Jan Chaloupka 9be42e50cc refactor(TestPodLifeTime): inline pod creation in each unit test to avoid accidental pod spec updates 2025-12-10 15:55:12 +01:00
Jan Chaloupka bed39d70f0 refactor(TestPodLifeTime): inline pod creation in each unit test to avoid accidental pod spec updates 2025-12-10 15:54:04 +01:00
Jan Chaloupka 8a0fd10315 refactor(TestPodLifeTime): inline pod creation in each unit test to avoid accidental pod spec updates 2025-12-10 15:53:19 +01:00
Jan Chaloupka 5e6cd6057b refactor(TestPodLifeTime): inline pod creation in each unit test to avoid accidental pod spec updates 2025-12-10 15:52:24 +01:00
Jan Chaloupka b857869371 refactor(TestPodLifeTime): inline pod creation in each unit test to avoid accidental pod spec updates 2025-12-10 15:47:08 +01:00
Jan Chaloupka 3e764eb564 refactor(TestPodLifeTime): inline pod creation in each unit test to avoid accidental pod spec updates 2025-12-10 15:45:53 +01:00
Jan Chaloupka 2648749eb8 refactor(TestPodLifeTime): inline pod creation in each unit test to avoid accidental pod spec updates 2025-12-10 15:45:10 +01:00
Jan Chaloupka ff43002060 refactor(TestPodLifeTime): inline pod creation in each unit test to avoid accidental pod spec updates 2025-12-10 15:43:07 +01:00
Jan Chaloupka 4f42a7ae9b refactor(TestPodLifeTime): consolidate defaults when building a test pod with RS owner reference 2025-12-10 15:34:48 +01:00
Jan Chaloupka 7d84b68556 refactor(TestPodLifeTime): consolidate defaults when building a test pod 2025-12-10 15:29:12 +01:00
Jan Chaloupka 5b4719634c refactor(TestPodLifeTime): the default pod namespace will work the same way as the 'dev' one 2025-12-10 15:29:03 +01:00
Jan Chaloupka 94a0fbdcbb refactor(TestPodLifeTime): inline node creation in each unit test to avoid accidental node spec updates 2025-12-10 15:28:57 +01:00
Jan Chaloupka bbc3eef1c9 refactor(TestPodLifeTime): replace test.GetReplicaSetOwnerRefList with test.SetRSOwnerRef
To make the assigment shorter and unified
2025-12-10 15:28:51 +01:00
Jan Chaloupka 3a3e72e9c2 refactor(TestPodLifeTime): consolidate all owner references
test.GetReplicaSetOwnerRefList produces the same value everything it's
invoked.
2025-12-10 15:28:45 +01:00
Kubernetes Prow Robot e6c14a365f Merge pull request #1782 from ingvagabund/refactorings
refactor(TestPodLifeTime): have a pod fully created through BuildTestPod without any edits
2025-12-10 06:23:30 -08:00
Kubernetes Prow Robot 2b2ab0b9ad Merge pull request #1781 from ingvagabund/podlifetime-unittest-dry
refactor(TestPodLifeTime): remove ineffective owner references assignments
2025-12-10 05:39:30 -08:00
Jan Chaloupka 16b9311e9e refactor(TestPodLifeTime): have a pod fully created through BuildTestPod without any edits 2025-12-10 14:13:23 +01:00
Jan Chaloupka 1a61470e81 refactor(TestPodLifeTime): have a pod fully created through BuildTestPod without any edits 2025-12-10 14:13:14 +01:00
Jan Chaloupka c02779b6a5 refactor(TestPodLifeTime): have a pod fully created through BuildTestPod without any edits 2025-12-10 14:13:09 +01:00
Jan Chaloupka ff6363692c refactor(TestPodLifeTime): have a pod fully created through BuildTestPod without any edits 2025-12-10 14:13:04 +01:00
Jan Chaloupka 34540c3c95 refactor(TestPodLifeTime): have a pod fully created through BuildTestPod without any edits 2025-12-10 14:13:00 +01:00
Jan Chaloupka ee40f7ff30 refactor(TestPodLifeTime): have a pod fully created through BuildTestPod without any edits 2025-12-10 14:12:55 +01:00
Jan Chaloupka cece2ee3cc refactor(TestPodLifeTime): have a pod fully created through BuildTestPod without any edits 2025-12-10 14:12:50 +01:00
Jan Chaloupka fbdf86fdfd refactor(TestPodLifeTime): have a pod fully created through BuildTestPod without any edits 2025-12-10 14:12:39 +01:00
Jan Chaloupka 7bfd4088ce refactor(TestPodLifeTime): have a pod fully created through BuildTestPod without any edits 2025-12-10 14:12:05 +01:00
Jan Chaloupka 18f61b5e64 refactor(TestPodLifeTime): have a pod fully created through BuildTestPod without any edits 2025-12-10 14:11:12 +01:00
Jan Chaloupka 769b4fe34a refactor(TestPodLifeTime): have a pod fully created through BuildTestPod without any edits 2025-12-10 14:11:07 +01:00
Jan Chaloupka 6ffc7e3975 refactor(TestPodLifeTime): have a pod fully created through BuildTestPod without any edits 2025-12-10 14:11:03 +01:00
Jan Chaloupka 31af0d8223 refactor(TestPodLifeTime): have a pod fully created through BuildTestPod without any edits 2025-12-10 14:10:59 +01:00
Jan Chaloupka 0c80f3689d refactor(TestPodLifeTime): have a pod fully created through BuildTestPod without any edits 2025-12-10 14:10:53 +01:00
Jan Chaloupka 9722018847 refactor(TestPodLifeTime): have a pod fully created through BuildTestPod without any edits 2025-12-10 14:10:48 +01:00
Jan Chaloupka 47cfdf7057 refactor(TestPodLifeTime): remove ineffective owner references assignments
Pods p5 and p6 already have an owner assigned. Also,
test.GetReplicaSetOwnerRefList() produces the same list of owner references.
2025-12-10 14:08:06 +01:00
Kubernetes Prow Robot db6d460677 Merge pull request #1764 from abelfodil/master
Fix "Current requires cgo or $USER set in environment" error
2025-11-15 00:25:38 -08:00
Anes Belfodil 237d9c1a7b fix: provide USER env var to correctly initialize tracing
This is done to prevent "Current requires cgo or $USER set in environment" error during
tracing initialization.
2025-11-15 01:40:08 -05:00
Kubernetes Prow Robot 5b66733ada Merge pull request #1772 from Sycrosity/master
docs: fix README.md link to kubernetes bot commands
2025-11-07 20:52:51 -08:00
Kubernetes Prow Robot eb1b91d085 Merge pull request #1773 from petersalas/update-readme
docs: fix incorrect gracePeriodSeconds default in README.md
2025-11-07 20:22:52 -08:00
Peter Salas 058056d965 docs: fix incorrect gracePeriodSeconds default in README.md 2025-11-04 12:11:02 -08:00
Sycrosity f9aa969791 docs: fix link to kubernetes bot commands page 2025-11-03 00:29:17 +00:00
Sycrosity 4bbfa08dfb docs: Have kustomize suggested commands use latest release 2025-11-03 00:25:22 +00:00
Kubernetes Prow Robot 4b7c2c90ea Merge pull request #1771 from a7i/native-sidecar
fix: pod resource calculation to consider native sidecars
2025-11-02 02:06:02 -08:00
Amir Alavi 06cab8e2aa fix: pod resource calculation to consider native sidecars
previously, descheduler code had copied an old version of PodRequestsAndLimits which does not consider native sidecars
it will now rely on resourcehelper libs, which will continue to get upstream updates

Signed-off-by: Amir Alavi <amiralavi7@gmail.com>
2025-11-01 10:48:00 -04:00
Kubernetes Prow Robot 582641c2e9 Merge pull request #1752 from ricardomaraschini/create-protection-for-pods-using-storage-class
feat: enable pod protection based on storage classes
2025-10-30 14:18:03 -07:00
Ricardo Maraschini d9d6ca64e9 feat: enable pod protection based on storage classes
this commit introduces a new customization on the existing PodsWithPVC
protection. this new customization allow users to make pods that refer
to a given storage class unevictable.

for example, to protect pods referring to `storage-class-0` and
`storage-class-1` this configuration can be used:

```yaml
apiVersion: "descheduler/v1alpha2"
kind: "DeschedulerPolicy"
profiles:
- name: ProfileName
  pluginConfig:
  - name: "DefaultEvictor"
    args:
      podProtections:
        extraEnabled:
        - PodsWithPVC
        config:
          PodsWithPVC:
            protectedStorageClasses:
            - name: storage-class-0
            - name: storage-class-1
```

changes introduced by this pr:

1. the descheduler starts to observe persistent volume claims.
1. a new api field was introduced to allow per pod protection config.
1. rbac had to be adjusted (+persistentvolumeclaims).
2025-10-29 11:21:10 +01:00
2412 changed files with 160481 additions and 92583 deletions
+3 -3
View File
@@ -7,11 +7,11 @@ jobs:
deploy:
strategy:
matrix:
k8s-version: ["v1.34.0"]
descheduler-version: ["v0.34.0"]
k8s-version: ["v1.35.1"]
descheduler-version: ["v0.35.0"]
descheduler-api: ["v1alpha2"]
manifest: ["deployment"]
kind-version: ["v0.30.0"] # keep in sync with test/run-e2e-tests.sh
kind-version: ["v0.31.0"] # keep in sync with test/run-e2e-tests.sh
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
+32 -14
View File
@@ -1,18 +1,36 @@
run:
timeout: 5m
version: "2"
linters:
disable-all: true
default: none
enable:
- gocyclo
- govet
- misspell
# - staticcheck // TODO: enable later once the code is properly updated
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- gofumpt
- gosimple
- gocyclo
- misspell
- govet
linters-settings:
gofumpt:
extra-rules: true
goimports:
local-prefixes: sigs.k8s.io/descheduler
#- goimports // TODO: enable later once the code is properly updated
settings:
gofumpt:
extra-rules: true
goimports:
local-prefixes:
- sigs.k8s.io/descheduler
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
+30
View File
@@ -0,0 +1,30 @@
# Descheduler Design Constraints
This is a slowly growing document that lists good practices, conventions, and design decisions.
## Overview
TBD
## Code convention
* *formatting code*: running `make fmt` before committing each change to avoid ci failing
## Unit Test Conventions
These are the known conventions that are useful to practice whenever reasonable:
* *single pod creation*: each pod variable built using `test.BuildTestPod` is updated only through the `apply` argument of `BuildTestPod`
* *single node creation*: each node variable built using `test.BuildTestNode` is updated only through the `apply` argument of `BuildTestNode`
* *no object instance sharing*: each object built through `test.BuildXXX` functions is newly created in each unit test to avoid accidental object mutations
* *no object instance duplication*: avoid duplication by no creating two objects with the same passed values at two different places. E.g. two nodes created with the same memory, cpu and pods requests. Rather create a single function wrapping test.BuildTestNode and invoke this wrapper multiple times.
The aim is to reduce cognitive load when reading and debugging the test code.
## Design Decisions FAQ
This section documents common questions about design decisions in the descheduler codebase and the rationale behind them.
### Why doesn't the framework provide helpers for registering and retrieving indexers for plugins?
In general, each plugin can have many indexers—for example, for nodes, namespaces, pods, and other resources. Each plugin, depending on its internal optimizations, may choose a different indexing function. Indexers are currently used very rarely in the framework and default plugins. Therefore, extending the framework interface with additional helpers for registering and retrieving indexers might introduce an unnecessary and overly restrictive layer without first understanding how indexers will be used. For the moment, I suggest avoiding any restrictions on how many indexers can be registered or which ones can be registered. Instead, we should extend the framework handle to provide a unique ID for each profile, so that indexers within the same profile share a unique prefix. This avoids collisions when the same profile is instantiated more than once. Later, once we learn more about indexer usage, we can revisit whether it makes sense to impose additional restrictions.
+1 -1
View File
@@ -11,7 +11,7 @@
# 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.
FROM golang:1.24.6
FROM golang:1.25.1
WORKDIR /go/src/sigs.k8s.io/descheduler
COPY . .
+3 -1
View File
@@ -26,7 +26,7 @@ ARCHS = amd64 arm arm64
LDFLAGS=-ldflags "-X ${LDFLAG_LOCATION}.version=${VERSION} -X ${LDFLAG_LOCATION}.buildDate=${BUILD} -X ${LDFLAG_LOCATION}.gitbranch=${BRANCH} -X ${LDFLAG_LOCATION}.gitsha1=${SHA1}"
GOLANGCI_VERSION := v1.64.8
GOLANGCI_VERSION := v2.8.0
HAS_GOLANGCI := $(shell ls _output/bin/golangci-lint 2> /dev/null)
GOFUMPT_VERSION := v0.7.0
@@ -149,12 +149,14 @@ ifndef HAS_GOLANGCI
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b ./_output/bin ${GOLANGCI_VERSION}
endif
./_output/bin/golangci-lint run -v
./_output/bin/golangci-lint fmt -v
fmt:
ifndef HAS_GOFUMPT
go install mvdan.cc/gofumpt@${GOFUMPT_VERSION}
endif
gofumpt -w -extra .
goimports -w pkg metrics test
# helm
+35 -6
View File
@@ -33,11 +33,12 @@ but relies on the default scheduler for that.
## ⚠️ Documentation Versions by Release
If you are using a published release of Descheduler (such as
`registry.k8s.io/descheduler/descheduler:v0.34.0`), follow the documentation in
`registry.k8s.io/descheduler/descheduler:v0.35.0`), follow the documentation in
that version's release branch, as listed below:
|Descheduler Version|Docs link|
|---|---|
|v0.35.x|[`release-1.35`](https://github.com/kubernetes-sigs/descheduler/blob/release-1.35/README.md)|
|v0.34.x|[`release-1.34`](https://github.com/kubernetes-sigs/descheduler/blob/release-1.34/README.md)|
|v0.33.x|[`release-1.33`](https://github.com/kubernetes-sigs/descheduler/blob/release-1.33/README.md)|
|v0.32.x|[`release-1.32`](https://github.com/kubernetes-sigs/descheduler/blob/release-1.32/README.md)|
@@ -94,17 +95,17 @@ See the [resources | Kustomize](https://kubectl.docs.kubernetes.io/references/ku
Run As A Job
```
kustomize build 'github.com/kubernetes-sigs/descheduler/kubernetes/job?ref=release-1.33' | kubectl apply -f -
kustomize build 'github.com/kubernetes-sigs/descheduler/kubernetes/job?ref=release-1.34' | kubectl apply -f -
```
Run As A CronJob
```
kustomize build 'github.com/kubernetes-sigs/descheduler/kubernetes/cronjob?ref=release-1.33' | kubectl apply -f -
kustomize build 'github.com/kubernetes-sigs/descheduler/kubernetes/cronjob?ref=release-1.34' | kubectl apply -f -
```
Run As A Deployment
```
kustomize build 'github.com/kubernetes-sigs/descheduler/kubernetes/deployment?ref=release-1.33' | kubectl apply -f -
kustomize build 'github.com/kubernetes-sigs/descheduler/kubernetes/deployment?ref=release-1.34' | kubectl apply -f -
```
## User Guide
@@ -129,7 +130,7 @@ These are top level keys in the Descheduler Policy that you can use to configure
| `metricsCollector.enabled` | `bool` | `false` | Enables Kubernetes [Metrics Server](https://kubernetes-sigs.github.io/metrics-server/) collection. |
| `metricsProviders` | `[]object` | `nil` | Enables various metrics providers like Kubernetes [Metrics Server](https://kubernetes-sigs.github.io/metrics-server/) |
| `evictionFailureEventNotification` | `bool` | `false` | Enables eviction failure event notification. |
| `gracePeriodSeconds` | `int` | `0` | The duration in seconds before the object should be deleted. The value zero indicates delete immediately. |
| `gracePeriodSeconds` | `int` | `nil` | The duration in seconds before the object should be deleted. The value zero indicates delete immediately. If this value is nil, the default grace period for the specified type will be used. |
| `prometheus` |`object`| `nil` | Configures collection of Prometheus metrics for actual resource utilization |
| `prometheus.url` |`string`| `nil` | Points to a Prometheus server url |
| `prometheus.authToken` |`object`| `nil` | Sets Prometheus server authentication token. If not specified in cluster authentication token from the container's file system is read. |
@@ -158,6 +159,7 @@ The Default Evictor Plugin is used by default for filtering pods before processi
| `ignorePvcPods` | `bool` | `false` | **[Deprecated: Use `podProtections` with `"PodsWithPVC"` instead]**<br>Sets whether PVC pods should be evicted or ignored. |
| `evictFailedBarePods` | `bool` | `false` | **[Deprecated: Use `podProtections` with `"FailedBarePods"` instead]**<br>Allows eviction of pods without owner references and in a failed phase. |
| `ignorePodsWithoutPDB` | `bool` | `false` | **[Deprecated: Use `podProtections` with `"PodsWithoutPDB"` instead]**<br>Sets whether pods without PodDisruptionBudget should be evicted or ignored. |
| `namespaceLabelSelector` | `metav1.LabelSelector` | | limiting the pods which are processed by namespace (see [label filtering](#label-filtering)) |
| `labelSelector` | `metav1.LabelSelector` | | (See [label filtering](#label-filtering)) |
| `priorityThreshold` | `priorityThreshold` | | (See [priority filtering](#priority-filtering)) |
| `nodeFit` | `bool` | `false` | (See [node fit filtering](#node-fit-filtering)) |
@@ -189,6 +191,31 @@ The Default Evictor Plugin is used by default for filtering pods before processi
| `"PodsWithoutPDB"` | Prevents eviction of Pods without a PodDisruptionBudget (PDB). |
| `"PodsWithResourceClaims"` | Prevents eviction of Pods using ResourceClaims. |
#### Protecting pods using specific Storage Classes
With the `PodsWithPVC` protection enabled all pods using PVCs are protected from eviction by default, if needed you can restrict the protection by filtering by PVC storage class. When filtering out by storage class, only pods using PVCs with the specified storage classes are protected from eviction. For example:
```yaml
apiVersion: "descheduler/v1alpha2"
kind: "DeschedulerPolicy"
profiles:
- name: ProfileName
pluginConfig:
- name: "DefaultEvictor"
args:
podProtections:
extraEnabled:
- PodsWithPVC
config:
PodsWithPVC:
protectedStorageClasses:
- name: storage-class-0
- name: storage-class-1
```
This example will protect pods using PVCs with storage classes `storage-class-0` and `storage-class-1` from eviction.
### Example policy
As part of the policy, you will start deciding which top level configuration to use, then which Evictor plugin to use (if you have your own, the Default Evictor if not), followed by deciding the configuration passed to the Evictor Plugin. By default, the Default Evictor is enabled for both `filter` and `preEvictionFilter` extension points. After that you will enable/disable eviction strategies plugins and configure them properly.
@@ -229,6 +256,7 @@ profiles:
#- "PodsWithPVC"
#- "PodsWithoutPDB"
#- "PodsWithResourceClaims"
config: {}
nodeFit: true
minReplicas: 2
plugins:
@@ -1108,6 +1136,7 @@ packages that it is compiled with.
| Descheduler | Supported Kubernetes Version |
|-------------|------------------------------|
| v0.35 | v1.35 |
| v0.34 | v1.34 |
| v0.33 | v1.33 |
| v0.32 | v1.32 |
@@ -1152,7 +1181,7 @@ that the only people who can get things done around here are the "maintainers".
We also would love to add more "official" maintainers, so show us what you can
do!
This repository uses the Kubernetes bots. See a full list of the commands [here][prow].
This repository uses the Kubernetes bots. See a full list of the commands [here](https://go.k8s.io/bot-commands).
### Communicating With Contributors
+1 -1
View File
@@ -6,7 +6,7 @@ metadata:
namespace: {{ include "descheduler.namespace" . }}
labels:
{{- include "descheduler.labels" . | nindent 4 }}
{{- if .Values.annotations }}
{{- if .Values.deploymentAnnotations }}
annotations: {{- toYaml .Values.deploymentAnnotations | nindent 4 }}
{{- end }}
spec:
+1
View File
@@ -4,6 +4,7 @@ Starting with descheduler release v0.10.0 container images are available in the
Descheduler Version | Container Image | Architectures |
------------------- |-------------------------------------------------|-------------------------|
v0.35.0 | registry.k8s.io/descheduler/descheduler:v0.35.0 | AMD64<br>ARM64<br>ARMv7 |
v0.34.0 | registry.k8s.io/descheduler/descheduler:v0.34.0 | AMD64<br>ARM64<br>ARMv7 |
v0.33.0 | registry.k8s.io/descheduler/descheduler:v0.33.0 | AMD64<br>ARM64<br>ARMv7 |
v0.32.0 | registry.k8s.io/descheduler/descheduler:v0.32.0 | AMD64<br>ARM64<br>ARMv7 |
+58 -57
View File
@@ -1,43 +1,41 @@
module sigs.k8s.io/descheduler
go 1.24.0
go 1.25.0
toolchain go1.24.3
toolchain go1.25.1
godebug default=go1.24
godebug default=go1.25
require (
github.com/client9/misspell v0.3.4
github.com/google/go-cmp v0.7.0
github.com/prometheus/client_golang v1.22.0
github.com/prometheus/common v0.64.0
github.com/spf13/cobra v1.10.0
github.com/spf13/pflag v1.0.9
go.opentelemetry.io/otel v1.36.0
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/common v0.67.5
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
go.opentelemetry.io/otel v1.40.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0
go.opentelemetry.io/otel/sdk v1.36.0
go.opentelemetry.io/otel/trace v1.36.0
google.golang.org/grpc v1.72.2
k8s.io/api v0.34.0
k8s.io/apimachinery v0.34.0
k8s.io/apiserver v0.34.0
k8s.io/client-go v0.34.0
k8s.io/code-generator v0.34.0
k8s.io/component-base v0.34.0
k8s.io/component-helpers v0.34.0
go.opentelemetry.io/otel/sdk v1.38.0
go.opentelemetry.io/otel/trace v1.40.0
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
google.golang.org/grpc v1.78.0
k8s.io/api v0.35.1
k8s.io/apimachinery v0.35.1
k8s.io/apiserver v0.35.1
k8s.io/client-go v0.35.1
k8s.io/code-generator v0.35.1
k8s.io/component-base v0.35.1
k8s.io/component-helpers v0.35.1
k8s.io/klog/v2 v2.130.1
k8s.io/metrics v0.34.0
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
k8s.io/metrics v0.35.1
k8s.io/utils v0.0.0-20260108192941-914a6e750570
kubevirt.io/api v1.3.0
kubevirt.io/client-go v1.3.0
kubevirt.io/containerized-data-importer-api v1.60.1 // indirect; drops dependency on o/api
sigs.k8s.io/mdtoc v1.1.0
sigs.k8s.io/mdtoc v1.4.0
sigs.k8s.io/yaml v1.6.0
)
require golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
require (
cel.dev/expr v0.24.0 // indirect
github.com/BurntSushi/toml v0.3.1 // indirect
@@ -45,8 +43,9 @@ require (
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
@@ -58,23 +57,24 @@ require (
github.com/go-kit/kit v0.13.0 // indirect
github.com/go-kit/log v0.2.1 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v1.2.4 // indirect
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
github.com/golang/glog v1.2.5 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/gomarkdown/markdown v0.0.0-20210514010506-3b9f47219fe7 // indirect
github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/cel-go v0.26.0 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
@@ -86,53 +86,54 @@ require (
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
github.com/openshift/api v0.0.0-20230503133300-8bbcb7ca7183 // indirect
github.com/openshift/custom-resource-status v1.1.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.etcd.io/etcd/api/v3 v3.6.4 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect
go.etcd.io/etcd/client/v3 v3.6.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.etcd.io/etcd/api/v3 v3.6.5 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.5 // indirect
go.etcd.io/etcd/client/v3 v3.6.5 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.38.0 // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.25.0 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/term v0.39.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/time v0.9.0 // indirect
golang.org/x/tools v0.26.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.30.0 // indirect
k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f // indirect
k8s.io/kms v0.34.0 // indirect
k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b // indirect
k8s.io/kms v0.35.1 // indirect
k8s.io/kube-openapi v0.30.0 // indirect
kubevirt.io/containerized-data-importer-api v1.57.0-alpha1 // indirect
kubevirt.io/controller-lifecycle-operator-sdk/api v0.0.0-20220329064328-f3cc58c6ed90 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/release-utils v0.8.3 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
)
replace k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b
replace k8s.io/kube-openapi => k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4
+161 -122
View File
@@ -3,6 +3,8 @@ cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
@@ -13,8 +15,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -29,6 +31,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ=
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
@@ -73,8 +77,9 @@ github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV
github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
@@ -105,11 +110,11 @@ github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/K
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc=
github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I=
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -125,8 +130,8 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/gomarkdown/markdown v0.0.0-20210514010506-3b9f47219fe7 h1:oKYOfNR7Hp6XpZ4JqolL5u642Js5Z0n7psPVl+S5heo=
github.com/gomarkdown/markdown v0.0.0-20210514010506-3b9f47219fe7/go.mod h1:aii0r/K0ZnHv7G0KF7xy1v0A7s2Ljrb5byB7MO5p6TU=
github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab h1:VYNivV7P8IRHUam2swVUNkhIdp0LRRFKe4hXNnoZKTc=
github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI=
@@ -148,8 +153,8 @@ github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -164,8 +169,8 @@ github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0 h1:FbSCl+KggFl+Ocym490i/E
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
@@ -243,8 +248,8 @@ github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xl
github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs=
github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc=
github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
@@ -266,30 +271,31 @@ github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3ev
github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY=
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/openshift/api v0.0.0-20230503133300-8bbcb7ca7183 h1:t/CahSnpqY46sQR01SoS+Jt0jtjgmhgE6lFmRnO4q70=
github.com/openshift/api v0.0.0-20230503133300-8bbcb7ca7183/go.mod h1:4VWG+W22wrB4HfBL88P40DxLEpSOaiBVxUnfalfJo9k=
github.com/openshift/custom-resource-status v1.1.2 h1:C3DL44LEbvlbItfd8mT5jWrqPfHnSOQoQf/sypqA6A4=
github.com/openshift/custom-resource-status v1.1.2/go.mod h1:DB/Mf2oTeiAmVVX1gN+NEqweonAPY0TKUwADizj8+ZA=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
@@ -297,12 +303,12 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs
github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/cobra v1.10.0 h1:a5/WeUlSDCvV5a45ljW2ZFtV0bTDpkfSAj3uqB6Sc+0=
github.com/spf13/cobra v1.10.0/go.mod h1:9dhySC7dnTtEiqzmqfkLj47BslqLCUPMXjG2lj/NgoE=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.8/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
@@ -320,8 +326,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
@@ -333,54 +339,54 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/bbolt v1.4.2 h1:IrUHp260R8c+zYx/Tm8QZr04CX+qWS5PGfPdevhdm1I=
go.etcd.io/bbolt v1.4.2/go.mod h1:Is8rSHO/b4f3XigBC0lL0+4FwAQv3HXEEIgFMuKHceM=
go.etcd.io/etcd/api/v3 v3.6.4 h1:7F6N7toCKcV72QmoUKa23yYLiiljMrT4xCeBL9BmXdo=
go.etcd.io/etcd/api/v3 v3.6.4/go.mod h1:eFhhvfR8Px1P6SEuLT600v+vrhdDTdcfMzmnxVXXSbk=
go.etcd.io/etcd/client/pkg/v3 v3.6.4 h1:9HBYrjppeOfFjBjaMTRxT3R7xT0GLK8EJMVC4xg6ok0=
go.etcd.io/etcd/client/pkg/v3 v3.6.4/go.mod h1:sbdzr2cl3HzVmxNw//PH7aLGVtY4QySjQFuaCgcRFAI=
go.etcd.io/etcd/client/v3 v3.6.4 h1:YOMrCfMhRzY8NgtzUsHl8hC2EBSnuqbR3dh84Uryl7A=
go.etcd.io/etcd/client/v3 v3.6.4/go.mod h1:jaNNHCyg2FdALyKWnd7hxZXZxZANb0+KGY+YQaEMISo=
go.etcd.io/etcd/pkg/v3 v3.6.4 h1:fy8bmXIec1Q35/jRZ0KOes8vuFxbvdN0aAFqmEfJZWA=
go.etcd.io/etcd/pkg/v3 v3.6.4/go.mod h1:kKcYWP8gHuBRcteyv6MXWSN0+bVMnfgqiHueIZnKMtE=
go.etcd.io/etcd/server/v3 v3.6.4 h1:LsCA7CzjVt+8WGrdsnh6RhC0XqCsLkBly3ve5rTxMAU=
go.etcd.io/etcd/server/v3 v3.6.4/go.mod h1:aYCL/h43yiONOv0QIR82kH/2xZ7m+IWYjzRmyQfnCAg=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
go.etcd.io/etcd/api/v3 v3.6.5 h1:pMMc42276sgR1j1raO/Qv3QI9Af/AuyQUW6CBAWuntA=
go.etcd.io/etcd/api/v3 v3.6.5/go.mod h1:ob0/oWA/UQQlT1BmaEkWQzI0sJ1M0Et0mMpaABxguOQ=
go.etcd.io/etcd/client/pkg/v3 v3.6.5 h1:Duz9fAzIZFhYWgRjp/FgNq2gO1jId9Yae/rLn3RrBP8=
go.etcd.io/etcd/client/pkg/v3 v3.6.5/go.mod h1:8Wx3eGRPiy0qOFMZT/hfvdos+DjEaPxdIDiCDUv/FQk=
go.etcd.io/etcd/client/v3 v3.6.5 h1:yRwZNFBx/35VKHTcLDeO7XVLbCBFbPi+XV4OC3QJf2U=
go.etcd.io/etcd/client/v3 v3.6.5/go.mod h1:ZqwG/7TAFZ0BJ0jXRPoJjKQJtbFo/9NIY8uoFFKcCyo=
go.etcd.io/etcd/pkg/v3 v3.6.5 h1:byxWB4AqIKI4SBmquZUG1WGtvMfMaorXFoCcFbVeoxM=
go.etcd.io/etcd/pkg/v3 v3.6.5/go.mod h1:uqrXrzmMIJDEy5j00bCqhVLzR5jEJIwDp5wTlLwPGOU=
go.etcd.io/etcd/server/v3 v3.6.5 h1:4RbUb1Bd4y1WkBHmuF+cZII83JNQMuNXzyjwigQ06y0=
go.etcd.io/etcd/server/v3 v3.6.5/go.mod h1:PLuhyVXz8WWRhzXDsl3A3zv/+aK9e4A9lpQkqawIaH0=
go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -396,9 +402,12 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
@@ -420,9 +429,12 @@ golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -462,12 +474,15 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -481,9 +496,11 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -528,11 +545,15 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsOKwuR5X/m0QFOJpSZvAxFfkQT+Erd9D4=
golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488/go.mod h1:fGb/2+tgXXjhjHsTNdVEEMZNWA0quBnfrO+AfoDSAKw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -552,9 +573,11 @@ golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -571,9 +594,12 @@ golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -603,28 +629,39 @@ golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/tools v0.34.1-0.20250613162507-3f93fece84c7/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0=
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@@ -639,16 +676,16 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
@@ -659,7 +696,6 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -668,28 +704,29 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.23.3/go.mod h1:w258XdGyvCmnBj/vGzQMj6kzdufJZVUwEM1U2fRJwSQ=
k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE=
k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug=
k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q=
k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM=
k8s.io/apiextensions-apiserver v0.30.0 h1:jcZFKMqnICJfRxTgnC4E+Hpcq8UEhT8B2lhBcQ+6uAs=
k8s.io/apiextensions-apiserver v0.30.0/go.mod h1:N9ogQFGcrbWqAY9p2mUAL5mGxsLqwgtUce127VtRX5Y=
k8s.io/apimachinery v0.23.3/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM=
k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0=
k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/apiserver v0.34.0 h1:Z51fw1iGMqN7uJ1kEaynf2Aec1Y774PqU+FVWCFV3Jg=
k8s.io/apiserver v0.34.0/go.mod h1:52ti5YhxAvewmmpVRqlASvaqxt0gKJxvCeW7ZrwgazQ=
k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo=
k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY=
k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU=
k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/apiserver v0.35.1 h1:potxdhhTL4i6AYAa2QCwtlhtB1eCdWQFvJV6fXgJzxs=
k8s.io/apiserver v0.35.1/go.mod h1:BiL6Dd3A2I/0lBnteXfWmCFobHM39vt5+hJQd7Lbpi4=
k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM=
k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA=
k8s.io/code-generator v0.23.3/go.mod h1:S0Q1JVA+kSzTI1oUvbKAxZY/DYbA/ZUb4Uknog12ETk=
k8s.io/code-generator v0.34.0 h1:Ze2i1QsvUprIlX3oHiGv09BFQRLCz+StA8qKwwFzees=
k8s.io/code-generator v0.34.0/go.mod h1:Py2+4w2HXItL8CGhks8uI/wS3Y93wPKO/9mBQUYNua0=
k8s.io/component-base v0.34.0 h1:bS8Ua3zlJzapklsB1dZgjEJuJEeHjj8yTu1gxE2zQX8=
k8s.io/component-base v0.34.0/go.mod h1:RSCqUdvIjjrEm81epPcjQ/DS+49fADvGSCkIP3IC6vg=
k8s.io/component-helpers v0.34.0 h1:5T7P9XGMoUy1JDNKzHf0p/upYbeUf8ZaSf9jbx0QlIo=
k8s.io/component-helpers v0.34.0/go.mod h1:kaOyl5tdtnymriYcVZg4uwDBe2d1wlIpXyDkt6sVnt4=
k8s.io/code-generator v0.35.1 h1:yLKR2la7Z9cWT5qmk67ayx8xXLM4RRKQMnC8YPvTWRI=
k8s.io/code-generator v0.35.1/go.mod h1:F2Fhm7aA69tC/VkMXLDokdovltXEF026Tb9yfQXQWKg=
k8s.io/component-base v0.35.1 h1:XgvpRf4srp037QWfGBLFsYMUQJkE5yMa94UsJU7pmcE=
k8s.io/component-base v0.35.1/go.mod h1:HI/6jXlwkiOL5zL9bqA3en1Ygv60F03oEpnuU1G56Bs=
k8s.io/component-helpers v0.35.1 h1:vwQ/cAfnVwaPeSXTu4DdK3d3n11Lugc5vMb6EV809ZY=
k8s.io/component-helpers v0.35.1/go.mod h1:HQqMwUk68Yyxgj92dJ+J1w/qbx9M0QR0eZ680m/o+Rk=
k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
k8s.io/gengo v0.0.0-20211129171323-c02415ce4185/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f h1:SLb+kxmzfA87x4E4brQzB33VBbT2+x7Zq9ROIHmGn9Q=
k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU=
k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b h1:gMplByicHV/TJBizHd9aVEsTYoJBnnUAT5MHlTkbjhQ=
k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b/go.mod h1:CgujABENc3KuTrcsdpGmrrASjtQsWCT7R99mEV4U/fM=
k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
@@ -697,35 +734,37 @@ k8s.io/klog/v2 v2.40.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kms v0.34.0 h1:u+/rcxQ3Jr7gC9AY5nXuEnBcGEB7ZOIJ9cdLdyHyEjQ=
k8s.io/kms v0.34.0/go.mod h1:s1CFkLG7w9eaTYvctOxosx88fl4spqmixnNpys0JAtM=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
k8s.io/metrics v0.34.0 h1:nYSfG2+tnL6/MRC2I+sGHjtNEGoEoM/KktgGOoQFwws=
k8s.io/metrics v0.34.0/go.mod h1:KCuXmotE0v4AvoARKUP8NC4lUnbK/Du1mluGdor5h4M=
k8s.io/kms v0.35.1 h1:kjv2r9g1mY7uL+l1RhyAZvWVZIA/4qIfBHXyjFGLRhU=
k8s.io/kms v0.35.1/go.mod h1:VT+4ekZAdrZDMgShK37vvlyHUVhwI9t/9tvh0AyCWmQ=
k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 h1:HhDfevmPS+OalTjQRKbTHppRIz01AWi8s45TMXStgYY=
k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
k8s.io/metrics v0.35.1 h1:MUcrUcWlq81XiripkydzCGsY9zQawDXfP9IICNNcVVw=
k8s.io/metrics v0.35.1/go.mod h1:9x7xWOAOiWzHA0vaqLgSE4PXF3vyT5ts5XIbx8OSjiI=
k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/utils v0.0.0-20260108192941-914a6e750570 h1:JT4W8lsdrGENg9W+YwwdLJxklIuKWdRm+BC+xt33FOY=
k8s.io/utils v0.0.0-20260108192941-914a6e750570/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
kubevirt.io/api v1.3.0 h1:9sGElMmnRU50pGED+MPPD2OwQl4S5lvjCUjm+t0mI90=
kubevirt.io/api v1.3.0/go.mod h1:e6LkElYZZm8NcP2gKlFVHZS9pgNhIARHIjSBSfeiP1s=
kubevirt.io/client-go v1.3.0 h1:/HKn4exzwsctEVTwVtEFaeT9D2v4TgWr2SmxITVEZ/4=
kubevirt.io/client-go v1.3.0/go.mod h1:qmcJZvUjbmggY1pp7irO3zesBJj7wwGIWAdnYEoh3yc=
kubevirt.io/containerized-data-importer-api v1.60.1 h1:chmxuINvA7TPmIe8LpShCoKPxoegcKjkG9tYboFBs/U=
kubevirt.io/containerized-data-importer-api v1.60.1/go.mod h1:8mwrkZIdy8j/LmCyKt2wFXbiMavLUIqDaegaIF67CZs=
kubevirt.io/containerized-data-importer-api v1.57.0-alpha1 h1:IWo12+ei3jltSN5jQN1xjgakfvRSF3G3Rr4GXVOOy2I=
kubevirt.io/containerized-data-importer-api v1.57.0-alpha1/go.mod h1:Y/8ETgHS1GjO89bl682DPtQOYEU/1ctPFBz6Sjxm4DM=
kubevirt.io/controller-lifecycle-operator-sdk/api v0.0.0-20220329064328-f3cc58c6ed90 h1:QMrd0nKP0BGbnxTqakhDZAUhGKxPiPiN5gSDqKUmGGc=
kubevirt.io/controller-lifecycle-operator-sdk/api v0.0.0-20220329064328-f3cc58c6ed90/go.mod h1:018lASpFYBsYN6XwmA2TIrPCx6e0gviTd/ZNtSitKgc=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs=
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/mdtoc v1.1.0 h1:q3YtqYzmC2e0hgLXRIOm7/QLuPux1CX3ZHCwlbABxZo=
sigs.k8s.io/mdtoc v1.1.0/go.mod h1:QZLVEdHH2iNIR4uHAZyvFRtjloHgVItk8lo/mzCtq3w=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/mdtoc v1.4.0 h1:2pDEwJSjoVrGr5BPkG+LoLkYLKvgtGYurrBY8ul3SxQ=
sigs.k8s.io/mdtoc v1.4.0/go.mod h1:KVnRRtK1rX9aQ95qF0rt3x2ytTxf3r7W7N41H+0KF0k=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/release-utils v0.8.3 h1:KtOtA4qDmzJyeQ2zkDsFVI25+NViwms/o5eL2NftFdA=
sigs.k8s.io/release-utils v0.8.3/go.mod h1:fp82Fma06OXBhEJ+GUJKqvcplDBomruK1R/1fWJnsrQ=
sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4=
sigs.k8s.io/structured-merge-diff/v6 v6.2.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
+1 -1
View File
@@ -19,7 +19,7 @@
go::verify_version() {
GO_VERSION=($(go version))
if [[ -z $(echo "${GO_VERSION[2]}" | grep -E 'go1.22|go1.23|go1.24') ]]; then
if [[ -z $(echo "${GO_VERSION[2]}" | grep -E 'go1.23|go1.24|go1.25') ]]; then
echo "Unknown go version '${GO_VERSION[2]}', skipping gofmt."
exit 1
fi
+3
View File
@@ -35,6 +35,9 @@ rules:
- apiGroups: ["metrics.k8s.io"]
resources: ["nodes", "pods"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "watch", "list"]
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
+1 -1
View File
@@ -16,7 +16,7 @@ spec:
priorityClassName: system-cluster-critical
containers:
- name: descheduler
image: registry.k8s.io/descheduler/descheduler:v0.34.0
image: registry.k8s.io/descheduler/descheduler:v0.35.0
volumeMounts:
- mountPath: /policy-dir
name: policy-volume
+1 -1
View File
@@ -19,7 +19,7 @@ spec:
serviceAccountName: descheduler-sa
containers:
- name: descheduler
image: registry.k8s.io/descheduler/descheduler:v0.34.0
image: registry.k8s.io/descheduler/descheduler:v0.35.0
imagePullPolicy: IfNotPresent
command:
- "/bin/descheduler"
+1 -1
View File
@@ -14,7 +14,7 @@ spec:
priorityClassName: system-cluster-critical
containers:
- name: descheduler
image: registry.k8s.io/descheduler/descheduler:v0.34.0
image: registry.k8s.io/descheduler/descheduler:v0.35.0
volumeMounts:
- mountPath: /policy-dir
name: policy-volume
+1 -1
View File
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
+1 -1
View File
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
+1 -1
View File
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
+1 -1
View File
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
+1 -1
View File
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
+1 -1
View File
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
+1 -1
View File
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
+4 -4
View File
@@ -20,10 +20,10 @@ import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/url"
"os"
"time"
promapi "github.com/prometheus/client_golang/api"
@@ -109,7 +109,7 @@ func GetMasterFromKubeconfig(filename string) (string, error) {
}
func loadCAFile(filepath string) (*x509.CertPool, error) {
caCert, err := ioutil.ReadFile(filepath)
caCert, err := os.ReadFile(filepath)
if err != nil {
return nil, err
}
@@ -126,13 +126,13 @@ func CreatePrometheusClient(prometheusURL, authToken string) (promapi.Client, *h
// Retrieve Pod CA cert
caCertPool, err := loadCAFile(K8sPodCAFilePath)
if err != nil {
return nil, nil, fmt.Errorf("Error loading CA file: %v", err)
return nil, nil, fmt.Errorf("error loading CA file: %v", err)
}
// Get Prometheus Host
u, err := url.Parse(prometheusURL)
if err != nil {
return nil, nil, fmt.Errorf("Error parsing prometheus URL: %v", err)
return nil, nil, fmt.Errorf("error parsing prometheus URL: %v", err)
}
t := &http.Transport{
Proxy: http.ProxyFromEnvironment,
+239 -377
View File
@@ -20,7 +20,6 @@ import (
"context"
"fmt"
"math"
"net/http"
"strconv"
"time"
@@ -30,27 +29,15 @@ import (
"go.opentelemetry.io/otel/trace"
v1 "k8s.io/api/core/v1"
policy "k8s.io/api/policy/v1"
policyv1 "k8s.io/api/policy/v1"
schedulingv1 "k8s.io/api/scheduling/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
utilversion "k8s.io/apimachinery/pkg/util/version"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/discovery"
"k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes"
fakeclientset "k8s.io/client-go/kubernetes/fake"
corev1listers "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/rest"
core "k8s.io/client-go/testing"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/events"
"k8s.io/client-go/util/workqueue"
componentbaseconfig "k8s.io/component-base/config"
"k8s.io/klog/v2"
@@ -72,8 +59,7 @@ import (
)
const (
prometheusAuthTokenSecretKey = "prometheusAuthToken"
workQueueKey = "key"
indexerNodeSelectorGlobal = "indexer_node_selector_global"
)
type eprunner func(ctx context.Context, nodes []*v1.Node) *frameworktypes.Status
@@ -84,88 +70,62 @@ type profileRunner struct {
}
type descheduler struct {
rs *options.DeschedulerServer
ir *informerResources
getPodsAssignedToNode podutil.GetPodsAssignedToNodeFunc
sharedInformerFactory informers.SharedInformerFactory
namespacedSecretsLister corev1listers.SecretNamespaceLister
deschedulerPolicy *api.DeschedulerPolicy
eventRecorder events.EventRecorder
podEvictor *evictions.PodEvictor
podEvictionReactionFnc func(*fakeclientset.Clientset) func(action core.Action) (bool, runtime.Object, error)
metricsCollector *metricscollector.MetricsCollector
prometheusClient promapi.Client
previousPrometheusClientTransport *http.Transport
queue workqueue.RateLimitingInterface
currentPrometheusAuthToken string
metricsProviders map[api.MetricsSource]*api.MetricsProvider
rs *options.DeschedulerServer
client clientset.Interface
kubeClientSandbox *kubeClientSandbox
getPodsAssignedToNode podutil.GetPodsAssignedToNodeFunc
sharedInformerFactory informers.SharedInformerFactory
deschedulerPolicy *api.DeschedulerPolicy
eventRecorder events.EventRecorder
podEvictor *evictions.PodEvictor
metricsCollector *metricscollector.MetricsCollector
inClusterPromClientCtrl *inClusterPromClientController
secretBasedPromClientCtrl *secretBasedPromClientController
profileRunners []profileRunner
}
type informerResources struct {
sharedInformerFactory informers.SharedInformerFactory
resourceToInformer map[schema.GroupVersionResource]informers.GenericInformer
}
func newInformerResources(sharedInformerFactory informers.SharedInformerFactory) *informerResources {
return &informerResources{
sharedInformerFactory: sharedInformerFactory,
resourceToInformer: make(map[schema.GroupVersionResource]informers.GenericInformer),
}
}
func (ir *informerResources) Uses(resources ...schema.GroupVersionResource) error {
for _, resource := range resources {
informer, err := ir.sharedInformerFactory.ForResource(resource)
func nodeSelectorFromPolicy(deschedulerPolicy *api.DeschedulerPolicy) (labels.Selector, error) {
nodeSelector := labels.Everything()
if deschedulerPolicy.NodeSelector != nil {
sel, err := labels.Parse(*deschedulerPolicy.NodeSelector)
if err != nil {
return err
return nil, err
}
ir.resourceToInformer[resource] = informer
nodeSelector = sel
}
return nil
return nodeSelector, nil
}
// CopyTo Copy informer subscriptions to the new factory and objects to the fake client so that the backing caches are populated for when listers are used.
func (ir *informerResources) CopyTo(fakeClient *fakeclientset.Clientset, newFactory informers.SharedInformerFactory) error {
for resource, informer := range ir.resourceToInformer {
_, err := newFactory.ForResource(resource)
if err != nil {
return fmt.Errorf("error getting resource %s: %w", resource, err)
}
objects, err := informer.Lister().List(labels.Everything())
if err != nil {
return fmt.Errorf("error listing %s: %w", informer, err)
}
for _, object := range objects {
fakeClient.Tracker().Add(object)
}
}
return nil
func addNodeSelectorIndexer(sharedInformerFactory informers.SharedInformerFactory, nodeSelector labels.Selector) error {
return nodeutil.AddNodeSelectorIndexer(sharedInformerFactory.Core().V1().Nodes().Informer(), indexerNodeSelectorGlobal, nodeSelector)
}
func metricsProviderListToMap(providersList []api.MetricsProvider) map[api.MetricsSource]*api.MetricsProvider {
providersMap := make(map[api.MetricsSource]*api.MetricsProvider)
for _, provider := range providersList {
provider := provider
providersMap[provider.Source] = &provider
}
return providersMap
}
func newDescheduler(ctx context.Context, rs *options.DeschedulerServer, deschedulerPolicy *api.DeschedulerPolicy, evictionPolicyGroupVersion string, eventRecorder events.EventRecorder, sharedInformerFactory, namespacedSharedInformerFactory informers.SharedInformerFactory) (*descheduler, error) {
func getPrometheusConfig(providersList []api.MetricsProvider) *api.Prometheus {
prometheusProvider := metricsProviderListToMap(providersList)[api.PrometheusMetrics]
if prometheusProvider == nil {
return nil
}
return prometheusProvider.Prometheus
}
func configureSecretPromClientReconciler(prometheusConfig *api.Prometheus) bool {
return prometheusConfig.URL != "" && prometheusConfig.AuthToken != nil
}
func newDescheduler(ctx context.Context, rs *options.DeschedulerServer, deschedulerPolicy *api.DeschedulerPolicy, evictionPolicyGroupVersion string, eventRecorder events.EventRecorder, client clientset.Interface, sharedInformerFactory, namespacedSharedInformerFactory informers.SharedInformerFactory, kubeClientSandbox *kubeClientSandbox) (*descheduler, error) {
podInformer := sharedInformerFactory.Core().V1().Pods().Informer()
ir := newInformerResources(sharedInformerFactory)
ir.Uses(v1.SchemeGroupVersion.WithResource("pods"),
v1.SchemeGroupVersion.WithResource("nodes"),
// Future work could be to let each plugin declare what type of resources it needs; that way dry runs would stay
// consistent with the real runs without having to keep the list here in sync.
v1.SchemeGroupVersion.WithResource("namespaces"), // Used by the defaultevictor plugin
schedulingv1.SchemeGroupVersion.WithResource("priorityclasses"), // Used by the defaultevictor plugin
policyv1.SchemeGroupVersion.WithResource("poddisruptionbudgets"), // Used by the defaultevictor plugin
) // Used by the defaultevictor plugin
// Temporarily register the PVC because it is used by the DefaultEvictor plugin during
// the descheduling cycle, where informer registration is ignored.
_ = sharedInformerFactory.Core().V1().PersistentVolumeClaims().Informer()
getPodsAssignedToNode, err := podutil.BuildGetPodsAssignedToNodeFunc(podInformer)
if err != nil {
@@ -174,7 +134,7 @@ func newDescheduler(ctx context.Context, rs *options.DeschedulerServer, deschedu
podEvictor, err := evictions.NewPodEvictor(
ctx,
rs.Client,
client,
eventRecorder,
podInformer,
rs.DefaultFeatureGates,
@@ -193,159 +153,77 @@ func newDescheduler(ctx context.Context, rs *options.DeschedulerServer, deschedu
}
desch := &descheduler{
rs: rs,
ir: ir,
getPodsAssignedToNode: getPodsAssignedToNode,
sharedInformerFactory: sharedInformerFactory,
deschedulerPolicy: deschedulerPolicy,
eventRecorder: eventRecorder,
podEvictor: podEvictor,
podEvictionReactionFnc: podEvictionReactionFnc,
prometheusClient: rs.PrometheusClient,
queue: workqueue.NewRateLimitingQueueWithConfig(workqueue.DefaultControllerRateLimiter(), workqueue.RateLimitingQueueConfig{Name: "descheduler"}),
metricsProviders: metricsProviderListToMap(deschedulerPolicy.MetricsProviders),
rs: rs,
client: client,
kubeClientSandbox: kubeClientSandbox,
getPodsAssignedToNode: getPodsAssignedToNode,
sharedInformerFactory: sharedInformerFactory,
deschedulerPolicy: deschedulerPolicy,
eventRecorder: eventRecorder,
podEvictor: podEvictor,
}
if rs.MetricsClient != nil {
nodeSelector := labels.Everything()
if deschedulerPolicy.NodeSelector != nil {
sel, err := labels.Parse(*deschedulerPolicy.NodeSelector)
prometheusConfig := getPrometheusConfig(deschedulerPolicy.MetricsProviders)
if prometheusConfig != nil && prometheusConfig.URL != "" {
if configureSecretPromClientReconciler(prometheusConfig) {
// Secret-based mode
ctrl, err := newSecretBasedPromClientController(rs.PrometheusClient, prometheusConfig, namespacedSharedInformerFactory)
if err != nil {
return nil, err
}
nodeSelector = sel
desch.secretBasedPromClientCtrl = ctrl
} else {
// In-cluster mode
desch.inClusterPromClientCtrl = newInClusterPromClientController(rs.PrometheusClient, prometheusConfig)
}
}
nodeSelector, err := nodeSelectorFromPolicy(deschedulerPolicy)
if err != nil {
return nil, err
}
if err := addNodeSelectorIndexer(sharedInformerFactory, nodeSelector); err != nil {
return nil, err
}
if rs.MetricsClient != nil {
desch.metricsCollector = metricscollector.NewMetricsCollector(sharedInformerFactory.Core().V1().Nodes().Lister(), rs.MetricsClient, nodeSelector)
}
prometheusProvider := desch.metricsProviders[api.PrometheusMetrics]
if prometheusProvider != nil && prometheusProvider.Prometheus != nil && prometheusProvider.Prometheus.AuthToken != nil {
authTokenSecret := prometheusProvider.Prometheus.AuthToken.SecretReference
if authTokenSecret == nil || authTokenSecret.Namespace == "" {
return nil, fmt.Errorf("prometheus metrics source configuration is missing authentication token secret")
var profileRunners []profileRunner
for idx, profile := range deschedulerPolicy.Profiles {
var promClientGetter func() promapi.Client
if desch.inClusterPromClientCtrl != nil {
promClientGetter = desch.inClusterPromClientCtrl.prometheusClient
} else if desch.secretBasedPromClientCtrl != nil {
promClientGetter = desch.secretBasedPromClientCtrl.prometheusClient
}
if namespacedSharedInformerFactory == nil {
return nil, fmt.Errorf("namespacedSharedInformerFactory not configured")
currProfile, err := frameworkprofile.NewProfile(
ctx,
profile,
pluginregistry.PluginRegistry,
frameworkprofile.WithClientSet(desch.client),
frameworkprofile.WithSharedInformerFactory(desch.sharedInformerFactory),
frameworkprofile.WithPodEvictor(desch.podEvictor),
frameworkprofile.WithGetPodsAssignedToNodeFnc(desch.getPodsAssignedToNode),
frameworkprofile.WithMetricsCollector(desch.metricsCollector),
frameworkprofile.WithPrometheusClient(promClientGetter),
// Generate a unique instance ID using just the index to avoid long IDs
// when profile names are very long
frameworkprofile.WithProfileInstanceID(fmt.Sprintf("%d", idx)),
)
if err != nil {
return nil, fmt.Errorf("unable to create %q profile: %v", profile.Name, err)
}
namespacedSharedInformerFactory.Core().V1().Secrets().Informer().AddEventHandler(desch.eventHandler())
desch.namespacedSecretsLister = namespacedSharedInformerFactory.Core().V1().Secrets().Lister().Secrets(authTokenSecret.Namespace)
profileRunners = append(profileRunners, profileRunner{profile.Name, currProfile.RunDeschedulePlugins, currProfile.RunBalancePlugins})
}
desch.profileRunners = profileRunners
return desch, nil
}
func (d *descheduler) reconcileInClusterSAToken() error {
// Read the sa token and assume it has the sufficient permissions to authenticate
cfg, err := rest.InClusterConfig()
if err == nil {
if d.currentPrometheusAuthToken != cfg.BearerToken {
klog.V(2).Infof("Creating Prometheus client (with SA token)")
prometheusClient, transport, err := client.CreatePrometheusClient(d.metricsProviders[api.PrometheusMetrics].Prometheus.URL, cfg.BearerToken)
if err != nil {
return fmt.Errorf("unable to create a prometheus client: %v", err)
}
d.prometheusClient = prometheusClient
if d.previousPrometheusClientTransport != nil {
d.previousPrometheusClientTransport.CloseIdleConnections()
}
d.previousPrometheusClientTransport = transport
d.currentPrometheusAuthToken = cfg.BearerToken
}
return nil
}
if err == rest.ErrNotInCluster {
return nil
}
return fmt.Errorf("unexpected error when reading in cluster config: %v", err)
}
func (d *descheduler) runAuthenticationSecretReconciler(ctx context.Context) {
defer utilruntime.HandleCrash()
defer d.queue.ShutDown()
klog.Infof("Starting authentication secret reconciler")
defer klog.Infof("Shutting down authentication secret reconciler")
go wait.UntilWithContext(ctx, d.runAuthenticationSecretReconcilerWorker, time.Second)
<-ctx.Done()
}
func (d *descheduler) runAuthenticationSecretReconcilerWorker(ctx context.Context) {
for d.processNextWorkItem(ctx) {
}
}
func (d *descheduler) processNextWorkItem(ctx context.Context) bool {
dsKey, quit := d.queue.Get()
if quit {
return false
}
defer d.queue.Done(dsKey)
err := d.sync()
if err == nil {
d.queue.Forget(dsKey)
return true
}
utilruntime.HandleError(fmt.Errorf("%v failed with : %v", dsKey, err))
d.queue.AddRateLimited(dsKey)
return true
}
func (d *descheduler) sync() error {
prometheusConfig := d.metricsProviders[api.PrometheusMetrics].Prometheus
if prometheusConfig == nil || prometheusConfig.AuthToken == nil || prometheusConfig.AuthToken.SecretReference == nil {
return fmt.Errorf("prometheus metrics source configuration is missing authentication token secret")
}
ns := prometheusConfig.AuthToken.SecretReference.Namespace
name := prometheusConfig.AuthToken.SecretReference.Name
secretObj, err := d.namespacedSecretsLister.Get(name)
if err != nil {
// clear the token if the secret is not found
if apierrors.IsNotFound(err) {
d.currentPrometheusAuthToken = ""
if d.previousPrometheusClientTransport != nil {
d.previousPrometheusClientTransport.CloseIdleConnections()
}
d.previousPrometheusClientTransport = nil
d.prometheusClient = nil
}
return fmt.Errorf("unable to get %v/%v secret", ns, name)
}
authToken := string(secretObj.Data[prometheusAuthTokenSecretKey])
if authToken == "" {
return fmt.Errorf("prometheus authentication token secret missing %q data or empty", prometheusAuthTokenSecretKey)
}
if d.currentPrometheusAuthToken == authToken {
return nil
}
klog.V(2).Infof("authentication secret token updated, recreating prometheus client")
prometheusClient, transport, err := client.CreatePrometheusClient(prometheusConfig.URL, authToken)
if err != nil {
return fmt.Errorf("unable to create a prometheus client: %v", err)
}
d.prometheusClient = prometheusClient
if d.previousPrometheusClientTransport != nil {
d.previousPrometheusClientTransport.CloseIdleConnections()
}
d.previousPrometheusClientTransport = transport
d.currentPrometheusAuthToken = authToken
return nil
}
func (d *descheduler) eventHandler() cache.ResourceEventHandler {
return cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) { d.queue.Add(workQueueKey) },
UpdateFunc: func(old, new interface{}) { d.queue.Add(workQueueKey) },
DeleteFunc: func(obj interface{}) { d.queue.Add(workQueueKey) },
}
}
func (d *descheduler) runDeschedulerLoop(ctx context.Context, nodes []*v1.Node) error {
func (d *descheduler) runDeschedulerLoop(ctx context.Context) error {
var span trace.Span
ctx, span = tracing.Tracer().Start(ctx, "runDeschedulerLoop")
defer span.End()
@@ -354,52 +232,22 @@ func (d *descheduler) runDeschedulerLoop(ctx context.Context, nodes []*v1.Node)
metrics.LoopDuration.With(map[string]string{}).Observe(time.Since(loopStartDuration).Seconds())
}(time.Now())
// if len is still <= 1 error out
if len(nodes) <= 1 {
klog.InfoS("Skipping descheduling cycle: requires >=2 nodes", "found", len(nodes))
return nil // gracefully skip this cycle instead of aborting
}
var client clientset.Interface
// When the dry mode is enable, collect all the relevant objects (mostly pods) under a fake client.
// So when evicting pods while running multiple strategies in a row have the cummulative effect
// as is when evicting pods for real.
if d.rs.DryRun {
klog.V(3).Infof("Building a cached client from the cluster for the dry run")
// Create a new cache so we start from scratch without any leftovers
fakeClient := fakeclientset.NewSimpleClientset()
// simulate a pod eviction by deleting a pod
fakeClient.PrependReactor("create", "pods", d.podEvictionReactionFnc(fakeClient))
fakeSharedInformerFactory := informers.NewSharedInformerFactory(fakeClient, 0)
err := d.ir.CopyTo(fakeClient, fakeSharedInformerFactory)
if err != nil {
return err
}
// create a new instance of the shared informer factor from the cached client
// register the pod informer, otherwise it will not get running
d.getPodsAssignedToNode, err = podutil.BuildGetPodsAssignedToNodeFunc(fakeSharedInformerFactory.Core().V1().Pods().Informer())
if err != nil {
return fmt.Errorf("build get pods assigned to node function error: %v", err)
}
fakeCtx, cncl := context.WithCancel(context.TODO())
defer cncl()
fakeSharedInformerFactory.Start(fakeCtx.Done())
fakeSharedInformerFactory.WaitForCacheSync(fakeCtx.Done())
client = fakeClient
d.sharedInformerFactory = fakeSharedInformerFactory
} else {
client = d.rs.Client
}
klog.V(3).Infof("Setting up the pod evictor")
d.podEvictor.SetClient(client)
klog.V(3).Infof("Resetting pod evictor counters")
d.podEvictor.ResetCounters()
d.runProfiles(ctx, client, nodes)
d.runProfiles(ctx)
if d.rs.DryRun {
if d.kubeClientSandbox == nil {
return fmt.Errorf("kubeClientSandbox is nil in DryRun mode")
}
klog.V(3).Infof("Restoring evicted pods from cache")
if err := d.kubeClientSandbox.restoreEvictedPods(ctx); err != nil {
klog.ErrorS(err, "Failed to restore evicted pods")
return fmt.Errorf("failed to restore evicted pods: %w", err)
}
d.kubeClientSandbox.reset()
}
klog.V(1).InfoS("Number of evictions/requests", "totalEvicted", d.podEvictor.TotalEvicted(), "evictionRequests", d.podEvictor.TotalEvictionRequests())
@@ -409,31 +257,32 @@ func (d *descheduler) runDeschedulerLoop(ctx context.Context, nodes []*v1.Node)
// runProfiles runs all the deschedule plugins of all profiles and
// later runs through all balance plugins of all profiles. (All Balance plugins should come after all Deschedule plugins)
// see https://github.com/kubernetes-sigs/descheduler/issues/979
func (d *descheduler) runProfiles(ctx context.Context, client clientset.Interface, nodes []*v1.Node) {
func (d *descheduler) runProfiles(ctx context.Context) {
var span trace.Span
ctx, span = tracing.Tracer().Start(ctx, "runProfiles")
defer span.End()
var profileRunners []profileRunner
for _, profile := range d.deschedulerPolicy.Profiles {
currProfile, err := frameworkprofile.NewProfile(
ctx,
profile,
pluginregistry.PluginRegistry,
frameworkprofile.WithClientSet(client),
frameworkprofile.WithSharedInformerFactory(d.sharedInformerFactory),
frameworkprofile.WithPodEvictor(d.podEvictor),
frameworkprofile.WithGetPodsAssignedToNodeFnc(d.getPodsAssignedToNode),
frameworkprofile.WithMetricsCollector(d.metricsCollector),
frameworkprofile.WithPrometheusClient(d.prometheusClient),
)
if err != nil {
klog.ErrorS(err, "unable to create a profile", "profile", profile.Name)
continue
}
profileRunners = append(profileRunners, profileRunner{profile.Name, currProfile.RunDeschedulePlugins, currProfile.RunBalancePlugins})
nodesAsInterface, err := d.sharedInformerFactory.Core().V1().Nodes().Informer().GetIndexer().ByIndex(indexerNodeSelectorGlobal, indexerNodeSelectorGlobal)
if err != nil {
span.AddEvent("Failed to list nodes with global node selector", trace.WithAttributes(attribute.String("err", err.Error())))
klog.Error(err)
return
}
for _, profileR := range profileRunners {
nodes, err := nodeutil.ReadyNodesFromInterfaces(nodesAsInterface)
if err != nil {
span.AddEvent("Failed to convert node as interfaces into ready nodes", trace.WithAttributes(attribute.String("err", err.Error())))
klog.Error(err)
return
}
// if len is still <= 1 error out
if len(nodes) <= 1 {
klog.InfoS("Skipping descheduling cycle: requires >=2 nodes", "found", len(nodes))
return // gracefully skip this cycle instead of aborting
}
for _, profileR := range d.profileRunners {
// First deschedule
status := profileR.descheduleEPs(ctx, nodes)
if status != nil && status.Err != nil {
@@ -443,7 +292,7 @@ func (d *descheduler) runProfiles(ctx context.Context, client clientset.Interfac
}
}
for _, profileR := range profileRunners {
for _, profileR := range d.profileRunners {
// Balance Later
status := profileR.balanceEPs(ctx, nodes)
if status != nil && status.Err != nil {
@@ -551,47 +400,117 @@ func validateVersionCompatibility(discovery discovery.DiscoveryInterface, desche
return nil
}
func podEvictionReactionFnc(fakeClient *fakeclientset.Clientset) func(action core.Action) (bool, runtime.Object, error) {
return func(action core.Action) (bool, runtime.Object, error) {
if action.GetSubresource() == "eviction" {
createAct, matched := action.(core.CreateActionImpl)
if !matched {
return false, nil, fmt.Errorf("unable to convert action to core.CreateActionImpl")
}
eviction, matched := createAct.Object.(*policy.Eviction)
if !matched {
return false, nil, fmt.Errorf("unable to convert action object into *policy.Eviction")
}
if err := fakeClient.Tracker().Delete(action.GetResource(), eviction.GetNamespace(), eviction.GetName()); err != nil {
return false, nil, fmt.Errorf("unable to delete pod %v/%v: %v", eviction.GetNamespace(), eviction.GetName(), err)
}
return true, nil, nil
}
// fallback to the default reactor
return false, nil, nil
type runFncType func(context.Context) error
func bootstrapDescheduler(
ctx context.Context,
rs *options.DeschedulerServer,
deschedulerPolicy *api.DeschedulerPolicy,
evictionPolicyGroupVersion string,
sharedInformerFactory, namespacedSharedInformerFactory informers.SharedInformerFactory,
eventRecorder events.EventRecorder,
) (*descheduler, runFncType, error) {
// Always create descheduler with real client/factory first to register all informers
descheduler, err := newDescheduler(ctx, rs, deschedulerPolicy, evictionPolicyGroupVersion, eventRecorder, rs.Client, sharedInformerFactory, namespacedSharedInformerFactory, nil)
if err != nil {
return nil, nil, fmt.Errorf("failed to create new descheduler: %v", err)
}
// If in dry run mode, replace the descheduler with one using fake client/factory
if rs.DryRun {
// Create sandbox with resources to mirror from real client
kubeClientSandbox, err := newDefaultKubeClientSandbox(rs.Client, sharedInformerFactory)
if err != nil {
return nil, nil, fmt.Errorf("failed to create kube client sandbox: %v", err)
}
klog.V(3).Infof("Building a cached client from the cluster for the dry run")
// TODO(ingvagabund): drop the previous queue
// TODO(ingvagabund): stop the previous pod evictor
// Replace descheduler with one using fake client/factory
descheduler, err = newDescheduler(ctx, rs, deschedulerPolicy, evictionPolicyGroupVersion, eventRecorder, kubeClientSandbox.fakeClient(), kubeClientSandbox.fakeSharedInformerFactory(), namespacedSharedInformerFactory, kubeClientSandbox)
if err != nil {
return nil, nil, fmt.Errorf("failed to create dry run descheduler: %v", err)
}
}
// init is responsible for starting all informer factories, metrics providers
// and other parts that require to start before a first descheduling cycle is run
deschedulerInitFnc := func(ctx context.Context) error {
// In dry run mode, start and sync the fake shared informer factory so it can mirror
// events from the real factory. Reliable propagation depends on both factories being
// fully synced (see WaitForCacheSync calls below), not solely on startup order.
if rs.DryRun {
descheduler.kubeClientSandbox.fakeSharedInformerFactory().Start(ctx.Done())
descheduler.kubeClientSandbox.fakeSharedInformerFactory().WaitForCacheSync(ctx.Done())
}
sharedInformerFactory.Start(ctx.Done())
if descheduler.secretBasedPromClientCtrl != nil {
namespacedSharedInformerFactory.Start(ctx.Done())
}
sharedInformerFactory.WaitForCacheSync(ctx.Done())
if descheduler.secretBasedPromClientCtrl != nil {
namespacedSharedInformerFactory.WaitForCacheSync(ctx.Done())
}
descheduler.podEvictor.WaitForEventHandlersSync(ctx)
if descheduler.metricsCollector != nil {
go func() {
klog.V(2).Infof("Starting metrics collector")
descheduler.metricsCollector.Run(ctx)
klog.V(2).Infof("Stopped metrics collector")
}()
klog.V(2).Infof("Waiting for metrics collector to sync")
if err := wait.PollUntilContextTimeout(ctx, time.Second, time.Minute, true, func(context.Context) (done bool, err error) {
return descheduler.metricsCollector.HasSynced(), nil
}); err != nil {
return fmt.Errorf("unable to wait for metrics collector to sync: %v", err)
}
}
if descheduler.secretBasedPromClientCtrl != nil {
go descheduler.secretBasedPromClientCtrl.runAuthenticationSecretReconciler(ctx)
}
return nil
}
if err := deschedulerInitFnc(ctx); err != nil {
return nil, nil, err
}
runFnc := func(ctx context.Context) error {
if descheduler.inClusterPromClientCtrl != nil {
// Read the sa token and assume it has the sufficient permissions to authenticate
if err := descheduler.inClusterPromClientCtrl.reconcileInClusterSAToken(); err != nil {
return fmt.Errorf("unable to reconcile an in cluster SA token: %v", err)
}
}
err = descheduler.runDeschedulerLoop(ctx)
if err != nil {
return fmt.Errorf("failed to run descheduler loop: %v", err)
}
return nil
}
return descheduler, runFnc, nil
}
type tokenReconciliation int
const (
noReconciliation tokenReconciliation = iota
inClusterReconciliation
secretReconciliation
)
func RunDeschedulerStrategies(ctx context.Context, rs *options.DeschedulerServer, deschedulerPolicy *api.DeschedulerPolicy, evictionPolicyGroupVersion string) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var span trace.Span
ctx, span = tracing.Tracer().Start(ctx, "RunDeschedulerStrategies")
defer span.End()
sharedInformerFactory := informers.NewSharedInformerFactoryWithOptions(rs.Client, 0, informers.WithTransform(trimManagedFields))
var nodeSelector string
if deschedulerPolicy.NodeSelector != nil {
nodeSelector = *deschedulerPolicy.NodeSelector
}
var eventClient clientset.Interface
if rs.DryRun {
eventClient = fakeclientset.NewSimpleClientset()
@@ -602,82 +521,25 @@ func RunDeschedulerStrategies(ctx context.Context, rs *options.DeschedulerServer
defer eventBroadcaster.Shutdown()
var namespacedSharedInformerFactory informers.SharedInformerFactory
metricProviderTokenReconciliation := noReconciliation
prometheusProvider := metricsProviderListToMap(deschedulerPolicy.MetricsProviders)[api.PrometheusMetrics]
if prometheusProvider != nil && prometheusProvider.Prometheus != nil && prometheusProvider.Prometheus.URL != "" {
if prometheusProvider.Prometheus.AuthToken != nil {
// Will get reconciled
namespacedSharedInformerFactory = informers.NewSharedInformerFactoryWithOptions(rs.Client, 0, informers.WithTransform(trimManagedFields), informers.WithNamespace(prometheusProvider.Prometheus.AuthToken.SecretReference.Namespace))
metricProviderTokenReconciliation = secretReconciliation
} else {
// Use the sa token and assume it has the sufficient permissions to authenticate
metricProviderTokenReconciliation = inClusterReconciliation
}
prometheusConfig := getPrometheusConfig(deschedulerPolicy.MetricsProviders)
if prometheusConfig != nil && configureSecretPromClientReconciler(prometheusConfig) {
namespacedSharedInformerFactory = informers.NewSharedInformerFactoryWithOptions(rs.Client, 0, informers.WithTransform(trimManagedFields), informers.WithNamespace(prometheusConfig.AuthToken.SecretReference.Namespace))
}
descheduler, err := newDescheduler(ctx, rs, deschedulerPolicy, evictionPolicyGroupVersion, eventRecorder, sharedInformerFactory, namespacedSharedInformerFactory)
_, runLoop, err := bootstrapDescheduler(ctx, rs, deschedulerPolicy, evictionPolicyGroupVersion, sharedInformerFactory, namespacedSharedInformerFactory, eventRecorder)
if err != nil {
span.AddEvent("Failed to create new descheduler", trace.WithAttributes(attribute.String("err", err.Error())))
span.AddEvent("Failed to bootstrap a descheduler", trace.WithAttributes(attribute.String("err", err.Error())))
return err
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
sharedInformerFactory.Start(ctx.Done())
if metricProviderTokenReconciliation == secretReconciliation {
namespacedSharedInformerFactory.Start(ctx.Done())
}
sharedInformerFactory.WaitForCacheSync(ctx.Done())
descheduler.podEvictor.WaitForEventHandlersSync(ctx)
if metricProviderTokenReconciliation == secretReconciliation {
namespacedSharedInformerFactory.WaitForCacheSync(ctx.Done())
}
if descheduler.metricsCollector != nil {
go func() {
klog.V(2).Infof("Starting metrics collector")
descheduler.metricsCollector.Run(ctx)
klog.V(2).Infof("Stopped metrics collector")
}()
klog.V(2).Infof("Waiting for metrics collector to sync")
if err := wait.PollWithContext(ctx, time.Second, time.Minute, func(context.Context) (done bool, err error) {
return descheduler.metricsCollector.HasSynced(), nil
}); err != nil {
return fmt.Errorf("unable to wait for metrics collector to sync: %v", err)
}
}
if metricProviderTokenReconciliation == secretReconciliation {
go descheduler.runAuthenticationSecretReconciler(ctx)
}
wait.NonSlidingUntil(func() {
if metricProviderTokenReconciliation == inClusterReconciliation {
// Read the sa token and assume it has the sufficient permissions to authenticate
if err := descheduler.reconcileInClusterSAToken(); err != nil {
klog.ErrorS(err, "unable to reconcile an in cluster SA token")
return
}
}
// A next context is created here intentionally to avoid nesting the spans via context.
sCtx, sSpan := tracing.Tracer().Start(ctx, "NonSlidingUntil")
defer sSpan.End()
nodes, err := nodeutil.ReadyNodes(sCtx, rs.Client, descheduler.sharedInformerFactory.Core().V1().Nodes().Lister(), nodeSelector)
if err != nil {
sSpan.AddEvent("Failed to detect ready nodes", trace.WithAttributes(attribute.String("err", err.Error())))
if err := runLoop(sCtx); err != nil {
sSpan.AddEvent("Descheduling loop failed", trace.WithAttributes(attribute.String("err", err.Error())))
klog.Error(err)
cancel()
return
}
err = descheduler.runDeschedulerLoop(sCtx, nodes)
if err != nil {
sSpan.AddEvent("Failed to run descheduler loop", trace.WithAttributes(attribute.String("err", err.Error())))
klog.Error(err)
cancel()
return
}
// If there was no interval specified, send a signal to the stopChannel to end the wait.Until loop after 1 iteration
File diff suppressed because it is too large Load Diff
+1 -7
View File
@@ -403,12 +403,6 @@ func (pe *PodEvictor) ResetCounters() {
pe.totalPodCount = 0
}
func (pe *PodEvictor) SetClient(client clientset.Interface) {
pe.mu.Lock()
defer pe.mu.Unlock()
pe.client = client
}
func (pe *PodEvictor) evictionRequestsTotal() uint {
if pe.featureGates.Enabled(features.EvictionsInBackground) {
return pe.erCache.evictionRequestsTotal()
@@ -464,7 +458,7 @@ type EvictOptions struct {
func (pe *PodEvictor) EvictPod(ctx context.Context, pod *v1.Pod, opts EvictOptions) error {
if len(pod.UID) == 0 {
klog.InfoS("Ignoring pod eviction due to missing UID", "pod", pod)
return fmt.Errorf("Pod %v is missing UID", klog.KObj(pod))
return fmt.Errorf("pod %v is missing UID", klog.KObj(pod))
}
if pe.featureGates.Enabled(features.EvictionsInBackground) {
+2 -3
View File
@@ -32,7 +32,6 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake"
fakeclientset "k8s.io/client-go/kubernetes/fake"
core "k8s.io/client-go/testing"
"k8s.io/client-go/tools/events"
@@ -94,7 +93,7 @@ func TestEvictPod(t *testing.T) {
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
ctx := context.Background()
fakeClient := fake.NewClientset(test.pods...)
fakeClient := fakeclientset.NewClientset(test.pods...)
fakeClient.PrependReactor("create", "pods/eviction", func(action core.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, test.wantErr
})
@@ -326,7 +325,7 @@ func TestNewPodEvictor(t *testing.T) {
}
for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
fakeClient := fake.NewSimpleClientset(pod1)
fakeClient := fakeclientset.NewSimpleClientset(pod1)
fakeClient.PrependReactor("create", "pods/eviction", func(action core.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, test.expectedError
})
+445
View File
@@ -0,0 +1,445 @@
/*
Copyright 2026 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 descheduler
import (
"context"
"fmt"
"sync"
"time"
v1 "k8s.io/api/core/v1"
policyv1 "k8s.io/api/policy/v1"
schedulingv1 "k8s.io/api/scheduling/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes"
fakeclientset "k8s.io/client-go/kubernetes/fake"
core "k8s.io/client-go/testing"
"k8s.io/client-go/tools/cache"
"k8s.io/klog/v2"
)
// evictedPodInfo stores identifying information about a pod that was evicted during dry-run mode
type evictedPodInfo struct {
Namespace string
Name string
UID string
}
// evictedPodsCache is a thread-safe cache for tracking pods evicted during dry-run mode
type evictedPodsCache struct {
sync.RWMutex
pods map[string]*evictedPodInfo
}
func newEvictedPodsCache() *evictedPodsCache {
return &evictedPodsCache{
pods: make(map[string]*evictedPodInfo),
}
}
func (c *evictedPodsCache) add(pod *v1.Pod) {
c.Lock()
defer c.Unlock()
c.pods[string(pod.UID)] = &evictedPodInfo{
Namespace: pod.Namespace,
Name: pod.Name,
UID: string(pod.UID),
}
}
func (c *evictedPodsCache) list() []*evictedPodInfo {
c.RLock()
defer c.RUnlock()
pods := make([]*evictedPodInfo, 0, len(c.pods))
for _, pod := range c.pods {
podCopy := *pod
pods = append(pods, &podCopy)
}
return pods
}
func (c *evictedPodsCache) clear() {
c.Lock()
defer c.Unlock()
c.pods = make(map[string]*evictedPodInfo)
}
// kubeClientSandbox creates a sandbox environment with a fake client and informer factory
// that mirrors resources from a real client, useful for dry-run testing scenarios
type kubeClientSandbox struct {
fakeKubeClient *fakeclientset.Clientset
fakeFactory informers.SharedInformerFactory
resourceToInformer map[schema.GroupVersionResource]informers.GenericInformer
evictedPodsCache *evictedPodsCache
podEvictionReactionFnc func(*fakeclientset.Clientset, *evictedPodsCache) func(action core.Action) (bool, runtime.Object, error)
}
func newDefaultKubeClientSandbox(client clientset.Interface, sharedInformerFactory informers.SharedInformerFactory) (*kubeClientSandbox, error) {
return newKubeClientSandbox(client, sharedInformerFactory,
v1.SchemeGroupVersion.WithResource("pods"),
v1.SchemeGroupVersion.WithResource("nodes"),
v1.SchemeGroupVersion.WithResource("namespaces"),
schedulingv1.SchemeGroupVersion.WithResource("priorityclasses"),
policyv1.SchemeGroupVersion.WithResource("poddisruptionbudgets"),
v1.SchemeGroupVersion.WithResource("persistentvolumeclaims"),
)
}
func newKubeClientSandbox(client clientset.Interface, sharedInformerFactory informers.SharedInformerFactory, resources ...schema.GroupVersionResource) (*kubeClientSandbox, error) {
sandbox := &kubeClientSandbox{
resourceToInformer: make(map[schema.GroupVersionResource]informers.GenericInformer),
evictedPodsCache: newEvictedPodsCache(),
podEvictionReactionFnc: podEvictionReactionFnc,
}
sandbox.fakeKubeClient = fakeclientset.NewSimpleClientset()
// simulate a pod eviction by deleting a pod
sandbox.fakeKubeClient.PrependReactor("create", "pods", sandbox.podEvictionReactionFnc(sandbox.fakeKubeClient, sandbox.evictedPodsCache))
sandbox.fakeFactory = informers.NewSharedInformerFactory(sandbox.fakeKubeClient, 0)
for _, resource := range resources {
informer, err := sharedInformerFactory.ForResource(resource)
if err != nil {
return nil, err
}
sandbox.resourceToInformer[resource] = informer
}
// Register event handlers to sync changes from real client to fake client.
// These handlers will keep the fake client in sync with ongoing changes.
if err := sandbox.registerEventHandlers(); err != nil {
return nil, fmt.Errorf("error registering event handlers: %w", err)
}
return sandbox, nil
}
func (sandbox *kubeClientSandbox) registerEventHandlers() error {
for resource, informer := range sandbox.resourceToInformer {
// Create a local copy to avoid closure capture issue
resource := resource
_, err := sandbox.fakeFactory.ForResource(resource)
if err != nil {
return fmt.Errorf("error getting resource %s for fake factory: %w", resource, err)
}
_, err = informer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
runtimeObj, ok := obj.(runtime.Object)
if !ok {
klog.ErrorS(nil, "object is not a runtime.Object", "resource", resource)
return
}
if err := sandbox.fakeKubeClient.Tracker().Add(runtimeObj); err != nil {
if !apierrors.IsAlreadyExists(err) {
klog.ErrorS(err, "failed to add object to fake client", "resource", resource)
}
}
},
UpdateFunc: func(oldObj, newObj interface{}) {
runtimeObj, ok := newObj.(runtime.Object)
if !ok {
klog.ErrorS(nil, "object is not a runtime.Object", "resource", resource)
return
}
metaObj, err := meta.Accessor(runtimeObj)
if err != nil {
klog.ErrorS(err, "failed to get object metadata", "resource", resource)
return
}
if err := sandbox.fakeKubeClient.Tracker().Update(resource, runtimeObj, metaObj.GetNamespace()); err != nil {
klog.ErrorS(err, "failed to update object in fake client", "resource", resource)
}
},
DeleteFunc: func(obj interface{}) {
// Handle tombstone case where the object might be wrapped
if tombstone, ok := obj.(cache.DeletedFinalStateUnknown); ok {
obj = tombstone.Obj
}
runtimeObj, ok := obj.(runtime.Object)
if !ok {
klog.ErrorS(nil, "object is not a runtime.Object", "resource", resource)
return
}
metaObj, err := meta.Accessor(runtimeObj)
if err != nil {
klog.ErrorS(err, "failed to get object metadata", "resource", resource)
return
}
if err := sandbox.fakeKubeClient.Tracker().Delete(resource, metaObj.GetNamespace(), metaObj.GetName()); err != nil {
klog.ErrorS(err, "failed to delete object from fake client", "resource", resource)
}
},
})
if err != nil {
return fmt.Errorf("error adding event handler for resource %s: %w", resource, err)
}
}
return nil
}
func (sandbox *kubeClientSandbox) fakeClient() *fakeclientset.Clientset {
return sandbox.fakeKubeClient
}
func (sandbox *kubeClientSandbox) fakeSharedInformerFactory() informers.SharedInformerFactory {
return sandbox.fakeFactory
}
func (sandbox *kubeClientSandbox) reset() {
sandbox.evictedPodsCache.clear()
}
// hasObjectInIndexer checks if an object exists in the fake indexer for the specified resource
func (sandbox *kubeClientSandbox) hasObjectInIndexer(resource schema.GroupVersionResource, namespace, name, uid string) (bool, error) {
informer, err := sandbox.fakeFactory.ForResource(resource)
if err != nil {
return false, fmt.Errorf("error getting informer for resource %s: %w", resource, err)
}
key := cache.MetaObjectToName(&metav1.ObjectMeta{Namespace: namespace, Name: name}).String()
obj, exists, err := informer.Informer().GetIndexer().GetByKey(key)
if err != nil {
return false, err
}
if !exists {
return false, nil
}
metaObj, err := meta.Accessor(obj)
if err != nil {
return false, fmt.Errorf("failed to get object metadata: %w", err)
}
return string(metaObj.GetUID()) == uid, nil
}
// hasRuntimeObjectInIndexer checks if a runtime.Object exists in the fake indexer by detecting its resource type
func (sandbox *kubeClientSandbox) hasRuntimeObjectInIndexer(obj runtime.Object) (bool, error) {
// Get metadata accessor to extract namespace and name
metaObj, err := meta.Accessor(obj)
if err != nil {
return false, fmt.Errorf("failed to get object metadata: %w", err)
}
// Get the GVK from the object using TypeMeta
gvk := obj.GetObjectKind().GroupVersionKind()
if gvk.Empty() {
return false, fmt.Errorf("no GroupVersionKind found for object")
}
// Use the GVK to construct the GVR by pluralizing the kind
plural, _ := meta.UnsafeGuessKindToResource(gvk)
gvr := schema.GroupVersionResource{
Group: gvk.Group,
Version: gvk.Version,
Resource: plural.Resource,
}
return sandbox.hasObjectInIndexer(gvr, metaObj.GetNamespace(), metaObj.GetName(), string(metaObj.GetUID()))
}
// shouldSkipPodWait checks if a pod still exists in the fake client with the expected UID.
// Returns (shouldSkip, error). shouldSkip is true if we should skip waiting for this pod.
// Returns an error for unexpected failures (non-NotFound errors).
func (sandbox *kubeClientSandbox) shouldSkipPodWait(ctx context.Context, pod *evictedPodInfo) (bool, error) {
// Check if the pod still exists in the fake client (it could have been deleted from the real client
// and the deletion propagated via event handlers in the meantime)
fakePod, err := sandbox.fakeClient().CoreV1().Pods(pod.Namespace).Get(ctx, pod.Name, metav1.GetOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
klog.V(3).InfoS("Pod no longer exists in fake client, skipping wait", "namespace", pod.Namespace, "name", pod.Name)
return true, nil
}
return false, fmt.Errorf("error getting pod %s/%s from fake client: %w", pod.Namespace, pod.Name, err)
}
if string(fakePod.UID) != pod.UID {
klog.V(3).InfoS("Pod UID mismatch in fake client, skipping wait", "namespace", pod.Namespace, "name", pod.Name, "expectedUID", pod.UID, "actualUID", string(fakePod.UID))
return true, nil
}
return false, nil
}
func waitForPodsCondition(ctx context.Context, pods []*evictedPodInfo, checkFn func(*evictedPodInfo) (bool, error), successMsg string) error {
if len(pods) == 0 {
return nil
}
err := wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, 5*time.Second, true, func(ctx context.Context) (bool, error) {
for _, pod := range pods {
satisfied, err := checkFn(pod)
if err != nil {
return false, err
}
if !satisfied {
return false, nil
}
}
return true, nil
})
if err != nil {
return err
}
klog.V(4).InfoS(successMsg)
return nil
}
// restoreEvictedPods restores pods from the evicted pods cache back to the fake client
func (sandbox *kubeClientSandbox) restoreEvictedPods(ctx context.Context) error {
podInformer, ok := sandbox.resourceToInformer[v1.SchemeGroupVersion.WithResource("pods")]
if !ok {
return fmt.Errorf("pod informer not found in resourceToInformer")
}
evictedPods := sandbox.evictedPodsCache.list()
// First wait loop: Check that all evicted pods are cleared from the indexers.
// This ensures the eviction has fully propagated through the fake informer's indexer.
// We check both existence and UID match to handle cases where a pod was deleted and
// recreated with the same name but different UID.
if err := waitForPodsCondition(ctx, evictedPods, func(pod *evictedPodInfo) (bool, error) {
exists, err := sandbox.hasObjectInIndexer(v1.SchemeGroupVersion.WithResource("pods"), pod.Namespace, pod.Name, pod.UID)
if err != nil {
klog.V(4).InfoS("Error checking indexer for pod", "namespace", pod.Namespace, "name", pod.Name, "error", err)
return false, nil
}
if exists {
klog.V(4).InfoS("Pod with matching UID still exists in fake indexer, waiting", "namespace", pod.Namespace, "name", pod.Name, "uid", pod.UID)
return false, nil
}
klog.V(4).InfoS("Pod with matching UID no longer in fake indexer", "namespace", pod.Namespace, "name", pod.Name, "uid", pod.UID)
return true, nil
}, "All evicted pods removed from fake indexer"); err != nil {
return fmt.Errorf("timeout waiting for evicted pods to be removed from fake indexer: %w", err)
}
var restoredPods []*evictedPodInfo
for _, podInfo := range sandbox.evictedPodsCache.list() {
obj, err := podInformer.Lister().ByNamespace(podInfo.Namespace).Get(podInfo.Name)
if err != nil {
klog.V(3).InfoS("Pod not found in real client, skipping restoration", "namespace", podInfo.Namespace, "name", podInfo.Name, "error", err)
continue
}
pod, ok := obj.(*v1.Pod)
if !ok {
klog.ErrorS(nil, "Object is not a pod", "namespace", podInfo.Namespace, "name", podInfo.Name)
continue
}
if string(pod.UID) != podInfo.UID {
klog.V(3).InfoS("Pod UID mismatch, skipping restoration", "namespace", podInfo.Namespace, "name", podInfo.Name, "expectedUID", podInfo.UID, "actualUID", string(pod.UID))
continue
}
if err = sandbox.fakeKubeClient.Tracker().Add(pod); err != nil && !apierrors.IsAlreadyExists(err) {
return fmt.Errorf("failed to restore pod %s/%s to fake client: %w", podInfo.Namespace, podInfo.Name, err)
}
klog.V(4).InfoS("Successfully restored pod to fake client", "namespace", podInfo.Namespace, "name", podInfo.Name, "uid", podInfo.UID)
restoredPods = append(restoredPods, podInfo)
}
// Second wait loop: Make sure the evicted pods are added back to the fake client.
// This ensures the restored pods are accessible through the fake informer's lister.
if err := waitForPodsCondition(ctx, restoredPods, func(pod *evictedPodInfo) (bool, error) {
podObj, err := sandbox.fakeFactory.Core().V1().Pods().Lister().Pods(pod.Namespace).Get(pod.Name)
if err != nil {
klog.V(4).InfoS("Pod not yet accessible in fake informer, waiting", "namespace", pod.Namespace, "name", pod.Name)
shouldSkip, checkErr := sandbox.shouldSkipPodWait(ctx, pod)
if checkErr != nil {
return false, checkErr
}
if shouldSkip {
return true, nil
}
return false, nil
}
klog.V(4).InfoS("Pod accessible in fake informer", "namespace", pod.Namespace, "name", pod.Name, "node", podObj.Spec.NodeName)
return true, nil
}, "All restored pods are accessible in fake informer"); err != nil {
return fmt.Errorf("timeout waiting for pods to be accessible in fake informer: %w", err)
}
// Third wait loop: Make sure the indexers see the added pods.
// This is important to ensure each descheduling cycle can see all the restored pods.
// Without this wait, the next cycle might not see the restored pods in the indexer yet.
if err := waitForPodsCondition(ctx, restoredPods, func(pod *evictedPodInfo) (bool, error) {
exists, err := sandbox.hasObjectInIndexer(v1.SchemeGroupVersion.WithResource("pods"), pod.Namespace, pod.Name, pod.UID)
if err != nil {
klog.V(4).InfoS("Error checking indexer for restored pod", "namespace", pod.Namespace, "name", pod.Name, "error", err)
return false, nil
}
if !exists {
klog.V(4).InfoS("Restored pod not yet in fake indexer, waiting", "namespace", pod.Namespace, "name", pod.Name)
shouldSkip, checkErr := sandbox.shouldSkipPodWait(ctx, pod)
if checkErr != nil {
return false, checkErr
}
if shouldSkip {
return true, nil
}
return false, nil
}
klog.V(4).InfoS("Restored pod now in fake indexer", "namespace", pod.Namespace, "name", pod.Name)
return true, nil
}, "All restored pods are now in fake indexer"); err != nil {
return fmt.Errorf("timeout waiting for restored pods to appear in fake indexer: %w", err)
}
return nil
}
func podEvictionReactionFnc(fakeClient *fakeclientset.Clientset, evictedCache *evictedPodsCache) func(action core.Action) (bool, runtime.Object, error) {
return func(action core.Action) (bool, runtime.Object, error) {
if action.GetSubresource() == "eviction" {
createAct, matched := action.(core.CreateActionImpl)
if !matched {
return false, nil, fmt.Errorf("unable to convert action to core.CreateActionImpl")
}
eviction, matched := createAct.Object.(*policyv1.Eviction)
if !matched {
return false, nil, fmt.Errorf("unable to convert action object into *policyv1.Eviction")
}
podObj, err := fakeClient.Tracker().Get(action.GetResource(), eviction.GetNamespace(), eviction.GetName())
if err == nil {
if pod, ok := podObj.(*v1.Pod); ok {
evictedCache.add(pod)
} else {
return false, nil, fmt.Errorf("unable to convert object to *v1.Pod for %v/%v", eviction.GetNamespace(), eviction.GetName())
}
} else if !apierrors.IsNotFound(err) {
return false, nil, fmt.Errorf("unable to get pod %v/%v: %v", eviction.GetNamespace(), eviction.GetName(), err)
}
if err := fakeClient.Tracker().Delete(action.GetResource(), eviction.GetNamespace(), eviction.GetName()); err != nil {
return false, nil, fmt.Errorf("unable to delete pod %v/%v: %v", eviction.GetNamespace(), eviction.GetName(), err)
}
return true, nil, nil
}
// fallback to the default reactor
return false, nil, nil
}
}
+582
View File
@@ -0,0 +1,582 @@
/*
Copyright 2026 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 descheduler
import (
"context"
"testing"
"time"
v1 "k8s.io/api/core/v1"
policy "k8s.io/api/policy/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/informers"
clientset "k8s.io/client-go/kubernetes"
fakeclientset "k8s.io/client-go/kubernetes/fake"
"sigs.k8s.io/descheduler/test"
)
// setupTestSandbox creates and initializes a kubeClientSandbox for testing.
// It creates a fake client, informer factory, registers pod and node informers,
// creates the sandbox, and starts both factories.
func setupTestSandbox(ctx context.Context, t *testing.T, initialObjects ...runtime.Object) (*kubeClientSandbox, informers.SharedInformerFactory, clientset.Interface) {
// Create a "real" fake client to act as the source of truth
realClient := fakeclientset.NewSimpleClientset(initialObjects...)
realFactory := informers.NewSharedInformerFactoryWithOptions(realClient, 0)
// Register pods and nodes informers BEFORE creating the sandbox
_ = realFactory.Core().V1().Pods().Informer()
_ = realFactory.Core().V1().Nodes().Informer()
// Create the sandbox with only pods and nodes resources
sandbox, err := newKubeClientSandbox(realClient, realFactory,
v1.SchemeGroupVersion.WithResource("pods"),
v1.SchemeGroupVersion.WithResource("nodes"),
)
if err != nil {
t.Fatalf("Failed to create kubeClientSandbox: %v", err)
}
// fake factory created by newKubeClientSandbox needs to be started before
// the "real" fake client factory to have all handlers registered
// to get complete propagation
sandbox.fakeSharedInformerFactory().Start(ctx.Done())
sandbox.fakeSharedInformerFactory().WaitForCacheSync(ctx.Done())
realFactory.Start(ctx.Done())
realFactory.WaitForCacheSync(ctx.Done())
return sandbox, realFactory, realClient
}
func TestKubeClientSandboxEventHandlers(t *testing.T) {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
sandbox, realFactory, realClient := setupTestSandbox(ctx, t)
// Register a third resource (secrets) in the real factory AFTER sandbox creation
// This should NOT be synced to the fake client
_ = realFactory.Core().V1().Secrets().Informer()
// Create test objects
testPod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pod",
Namespace: "default",
UID: "pod-uid-12345",
},
Spec: v1.PodSpec{
NodeName: "test-node",
},
}
testNode := &v1.Node{
ObjectMeta: metav1.ObjectMeta{
Name: "test-node",
UID: "node-uid-67890",
},
}
testSecret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "test-secret",
Namespace: "default",
UID: "secret-uid-abcde",
},
Data: map[string][]byte{
"key": []byte("value"),
},
}
// Add objects to the real client
var err error
_, err = realClient.CoreV1().Pods(testPod.Namespace).Create(ctx, testPod, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Failed to create pod in real client: %v", err)
}
_, err = realClient.CoreV1().Nodes().Create(ctx, testNode, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Failed to create node in real client: %v", err)
}
_, err = realClient.CoreV1().Secrets(testSecret.Namespace).Create(ctx, testSecret, metav1.CreateOptions{})
if err != nil {
t.Fatalf("Failed to create secret in real client: %v", err)
}
// Helper function to wait for a resource to appear in the sandbox's fake client indexer
waitForResourceInIndexer := func(resourceType, namespace, name, uid, description string) error {
t.Logf("Waiting for %s to appear in fake client indexer...", description)
return wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, 5*time.Second, true, func(ctx context.Context) (bool, error) {
exists, err := sandbox.hasObjectInIndexer(
v1.SchemeGroupVersion.WithResource(resourceType),
namespace,
name,
uid,
)
if err != nil {
t.Logf("Error checking %s in indexer: %v", description, err)
return false, nil
}
if exists {
t.Logf("%s appeared in fake client indexer", description)
return true, nil
}
return false, nil
})
}
// Wait for the pod to appear in the sandbox's fake client indexer
if err := waitForResourceInIndexer("pods", testPod.Namespace, testPod.Name, string(testPod.UID), "pod"); err != nil {
t.Fatalf("Pod did not appear in fake client indexer within timeout: %v", err)
}
// Wait for the node to appear in the sandbox's fake client indexer
if err := waitForResourceInIndexer("nodes", "", testNode.Name, string(testNode.UID), "node"); err != nil {
t.Fatalf("Node did not appear in fake client indexer within timeout: %v", err)
}
// Verify the pod can be retrieved from the fake client
retrievedPod, err := sandbox.fakeClient().CoreV1().Pods(testPod.Namespace).Get(ctx, testPod.Name, metav1.GetOptions{})
if err != nil {
t.Fatalf("Failed to retrieve pod from fake client: %v", err)
}
if retrievedPod.Namespace != testPod.Namespace || retrievedPod.Name != testPod.Name || retrievedPod.UID != testPod.UID {
t.Errorf("Retrieved pod mismatch: got namespace=%s name=%s uid=%s, want namespace=%s name=%s uid=%s",
retrievedPod.Namespace, retrievedPod.Name, retrievedPod.UID, testPod.Namespace, testPod.Name, testPod.UID)
}
t.Logf("Successfully retrieved pod from fake client: %s/%s", retrievedPod.Namespace, retrievedPod.Name)
// Verify the node can be retrieved from the fake client
retrievedNode, err := sandbox.fakeClient().CoreV1().Nodes().Get(ctx, testNode.Name, metav1.GetOptions{})
if err != nil {
t.Fatalf("Failed to retrieve node from fake client: %v", err)
}
if retrievedNode.Name != testNode.Name || retrievedNode.UID != testNode.UID {
t.Errorf("Retrieved node mismatch: got name=%s uid=%s, want name=%s uid=%s",
retrievedNode.Name, retrievedNode.UID, testNode.Name, testNode.UID)
}
t.Logf("Successfully retrieved node from fake client: %s", retrievedNode.Name)
// Wait a bit longer and verify the secret does NOT appear in the fake client indexer
// because secrets were registered AFTER the sandbox was created
t.Log("Verifying secret does NOT appear in fake client indexer...")
time.Sleep(500 * time.Millisecond) // Give extra time to ensure it's not just a timing issue
// First, verify we can get the informer for secrets in the fake factory
secretInformer, err := sandbox.fakeSharedInformerFactory().ForResource(v1.SchemeGroupVersion.WithResource("secrets"))
if err != nil {
t.Logf("Expected: Cannot get secret informer from fake factory: %v", err)
} else {
// If we can get the informer, check if the secret exists in it
key := "default/test-secret"
_, exists, err := secretInformer.Informer().GetIndexer().GetByKey(key)
if err != nil {
t.Logf("Error checking secret in fake indexer: %v", err)
}
if exists {
t.Error("Secret should NOT exist in fake client indexer (it was registered after sandbox creation)")
} else {
t.Log("Correctly verified: Secret does not exist in fake client indexer")
}
}
// Also verify that attempting to get the secret directly from fake client should fail
_, err = sandbox.fakeClient().CoreV1().Secrets(testSecret.Namespace).Get(ctx, testSecret.Name, metav1.GetOptions{})
if err == nil {
t.Error("Secret should NOT be retrievable from fake client (it was not synced)")
} else {
t.Logf("Correctly verified: Secret not retrievable from fake client: %v", err)
}
// Verify the secret IS in the real client (sanity check)
_, err = realClient.CoreV1().Secrets(testSecret.Namespace).Get(ctx, testSecret.Name, metav1.GetOptions{})
if err != nil {
t.Fatalf("Secret should exist in real client but got error: %v", err)
}
t.Log("Sanity check passed: Secret exists in real client")
}
func TestKubeClientSandboxReset(t *testing.T) {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
node1 := test.BuildTestNode("n1", 2000, 3000, 10, nil)
p1 := test.BuildTestPod("p1", 100, 0, node1.Name, test.SetRSOwnerRef)
p2 := test.BuildTestPod("p2", 100, 0, node1.Name, test.SetRSOwnerRef)
sandbox, _, _ := setupTestSandbox(ctx, t, node1, p1, p2)
eviction1 := &policy.Eviction{
ObjectMeta: metav1.ObjectMeta{
Name: p1.Name,
Namespace: p1.Namespace,
},
}
eviction2 := &policy.Eviction{
ObjectMeta: metav1.ObjectMeta{
Name: p2.Name,
Namespace: p2.Namespace,
},
}
if err := sandbox.fakeClient().CoreV1().Pods(p1.Namespace).EvictV1(context.TODO(), eviction1); err != nil {
t.Fatalf("Error evicting p1: %v", err)
}
if err := sandbox.fakeClient().CoreV1().Pods(p2.Namespace).EvictV1(context.TODO(), eviction2); err != nil {
t.Fatalf("Error evicting p2: %v", err)
}
evictedPods := sandbox.evictedPodsCache.list()
if len(evictedPods) != 2 {
t.Fatalf("Expected 2 evicted pods in cache, but got %d", len(evictedPods))
}
t.Logf("Evicted pods in cache before reset: %d", len(evictedPods))
for _, evictedPod := range evictedPods {
if evictedPod.Namespace == "" || evictedPod.Name == "" || evictedPod.UID == "" {
t.Errorf("Evicted pod has empty fields: namespace=%s, name=%s, uid=%s", evictedPod.Namespace, evictedPod.Name, evictedPod.UID)
}
t.Logf("Evicted pod: %s/%s (UID: %s)", evictedPod.Namespace, evictedPod.Name, evictedPod.UID)
}
sandbox.reset()
evictedPodsAfterReset := sandbox.evictedPodsCache.list()
if len(evictedPodsAfterReset) != 0 {
t.Fatalf("Expected cache to be empty after reset, but found %d pods", len(evictedPodsAfterReset))
}
t.Logf("Successfully verified cache is empty after reset")
}
func TestKubeClientSandboxRestoreEvictedPods(t *testing.T) {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
node1 := test.BuildTestNode("n1", 2000, 3000, 10, nil)
// Create test pods
pod1 := test.BuildTestPod("pod1", 100, 0, node1.Name, test.SetRSOwnerRef)
pod2 := test.BuildTestPod("pod2", 100, 0, node1.Name, test.SetRSOwnerRef)
pod3 := test.BuildTestPod("pod3", 100, 0, node1.Name, test.SetRSOwnerRef)
pod4 := test.BuildTestPod("pod4", 100, 0, node1.Name, test.SetRSOwnerRef)
sandbox, _, realClient := setupTestSandbox(ctx, t, node1, pod1, pod2, pod3, pod4)
// Evict all pods
for _, pod := range []*v1.Pod{pod1, pod2, pod3, pod4} {
eviction := &policy.Eviction{
ObjectMeta: metav1.ObjectMeta{
Name: pod.Name,
Namespace: pod.Namespace,
},
}
if err := sandbox.fakeClient().CoreV1().Pods(pod.Namespace).EvictV1(ctx, eviction); err != nil {
t.Fatalf("Error evicting %s: %v", pod.Name, err)
}
}
// Delete pod2 from real client to simulate it being deleted (should skip restoration)
if err := realClient.CoreV1().Pods(pod2.Namespace).Delete(ctx, pod2.Name, metav1.DeleteOptions{}); err != nil {
t.Fatalf("Error deleting pod2 from real client: %v", err)
}
// Delete and recreate pod3 with different UID in real client (should skip restoration)
if err := realClient.CoreV1().Pods(pod3.Namespace).Delete(ctx, pod3.Name, metav1.DeleteOptions{}); err != nil {
t.Fatalf("Error deleting pod3 from real client: %v", err)
}
pod3New := test.BuildTestPod("pod3", 100, 0, node1.Name, test.SetRSOwnerRef)
// Ensure pod3New has a different UID from pod3
if pod3New.UID == pod3.UID {
pod3New.UID = "new-uid-for-pod3"
}
if _, err := realClient.CoreV1().Pods(pod3New.Namespace).Create(ctx, pod3New, metav1.CreateOptions{}); err != nil {
t.Fatalf("Error recreating pod3 in real client: %v", err)
}
// Verify evicted pods are in cache
evictedPods := sandbox.evictedPodsCache.list()
if len(evictedPods) != 4 {
t.Fatalf("Expected 4 evicted pods in cache, got %d", len(evictedPods))
}
t.Logf("Evicted pods in cache: %d", len(evictedPods))
// Call restoreEvictedPods
t.Log("Calling restoreEvictedPods...")
if err := sandbox.restoreEvictedPods(ctx); err != nil {
t.Fatalf("restoreEvictedPods failed: %v", err)
}
// Verify pod1 and pod4 were restored (exists in fake client with matching UID and accessible via indexer)
for _, pod := range []*v1.Pod{pod1, pod4} {
// Check restoration via fake client
restoredPod, err := sandbox.fakeClient().CoreV1().Pods(pod.Namespace).Get(ctx, pod.Name, metav1.GetOptions{})
if err != nil {
t.Errorf("%s should have been restored to fake client: %v", pod.Name, err)
} else {
if restoredPod.UID != pod.UID {
t.Errorf("Restored %s UID mismatch: got %s, want %s", pod.Name, restoredPod.UID, pod.UID)
}
t.Logf("Successfully verified %s restoration: %s/%s (UID: %s)", pod.Name, restoredPod.Namespace, restoredPod.Name, restoredPod.UID)
}
// Check accessibility via indexer
exists, err := sandbox.hasObjectInIndexer(v1.SchemeGroupVersion.WithResource("pods"), pod.Namespace, pod.Name, string(pod.UID))
if err != nil {
t.Errorf("Error checking %s in indexer: %v", pod.Name, err)
}
if !exists {
t.Errorf("%s should exist in fake indexer after restoration", pod.Name)
} else {
t.Logf("Successfully verified %s exists in fake indexer", pod.Name)
}
}
// Verify pod2 was NOT restored (deleted from real client)
_, err := sandbox.fakeClient().CoreV1().Pods(pod2.Namespace).Get(ctx, pod2.Name, metav1.GetOptions{})
if err == nil {
t.Error("pod2 should NOT have been restored (was deleted from real client)")
} else {
t.Logf("Correctly verified pod2 was not restored: %v", err)
}
// Verify pod3 was NOT restored with old UID (UID mismatch case)
// Note: pod3 may exist in fake client with NEW UID due to event handler syncing,
// but it should NOT have been restored with the OLD UID from evicted cache
pod3InFake, err := sandbox.fakeClient().CoreV1().Pods(pod3.Namespace).Get(ctx, pod3.Name, metav1.GetOptions{})
if err == nil {
// Pod3 exists, but it should have the NEW UID, not the old one
if pod3InFake.UID == pod3.UID {
t.Error("pod3 should NOT have been restored with old UID (UID mismatch should prevent restoration)")
} else {
t.Logf("Correctly verified pod3 has new UID (%s), not old UID (%s) - restoration was skipped", pod3InFake.UID, pod3.UID)
}
} else {
// Pod3 doesn't exist - this is also acceptable (event handlers haven't synced it yet)
t.Logf("pod3 not found in fake client: %v", err)
}
// Verify evicted pods cache is still intact (restoreEvictedPods doesn't clear it)
evictedPodsAfter := sandbox.evictedPodsCache.list()
if len(evictedPodsAfter) != 4 {
t.Errorf("Expected evicted pods cache to still have 4 entries, got %d", len(evictedPodsAfter))
}
}
func TestKubeClientSandboxRestoreEvictedPodsEmptyCache(t *testing.T) {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
node1 := test.BuildTestNode("n1", 2000, 3000, 10, nil)
sandbox, _, _ := setupTestSandbox(ctx, t, node1)
// Call restoreEvictedPods with empty cache - should be a no-op
t.Log("Calling restoreEvictedPods with empty cache...")
if err := sandbox.restoreEvictedPods(ctx); err != nil {
t.Fatalf("restoreEvictedPods should succeed with empty cache: %v", err)
}
t.Log("Successfully verified restoreEvictedPods handles empty cache")
}
func TestEvictedPodsCache(t *testing.T) {
t.Run("add single pod", func(t *testing.T) {
const (
podName = "pod1"
podNamespace = "default"
podUID = "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
)
cache := newEvictedPodsCache()
pod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: podName,
Namespace: podNamespace,
UID: podUID,
},
}
cache.add(pod)
pods := cache.list()
if len(pods) != 1 {
t.Fatalf("Expected 1 pod in cache, got %d", len(pods))
}
if pods[0].Name != podName || pods[0].Namespace != podNamespace || pods[0].UID != podUID {
t.Errorf("Pod data mismatch: got name=%s, namespace=%s, uid=%s", pods[0].Name, pods[0].Namespace, pods[0].UID)
}
})
t.Run("add multiple pods", func(t *testing.T) {
cache := newEvictedPodsCache()
pods := []*v1.Pod{
{ObjectMeta: metav1.ObjectMeta{Name: "pod1", Namespace: "default", UID: "11111111-1111-1111-1111-111111111111"}},
{ObjectMeta: metav1.ObjectMeta{Name: "pod2", Namespace: "kube-system", UID: "22222222-2222-2222-2222-222222222222"}},
{ObjectMeta: metav1.ObjectMeta{Name: "pod3", Namespace: "default", UID: "33333333-3333-3333-3333-333333333333"}},
}
for _, pod := range pods {
cache.add(pod)
}
cachedPods := cache.list()
if len(cachedPods) != 3 {
t.Fatalf("Expected 3 pods in cache, got %d", len(cachedPods))
}
podMap := make(map[string]*evictedPodInfo)
for _, cachedPod := range cachedPods {
podMap[cachedPod.UID] = cachedPod
}
for _, pod := range pods {
cached, ok := podMap[string(pod.UID)]
if !ok {
t.Errorf("Pod with UID %s not found in cache", pod.UID)
continue
}
if cached.Name != pod.Name || cached.Namespace != pod.Namespace {
t.Errorf("Pod data mismatch for UID %s: got name=%s, namespace=%s", pod.UID, cached.Name, cached.Namespace)
}
}
})
t.Run("add duplicate pod updates entry", func(t *testing.T) {
const (
duplicateUID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
updatedPodName = "pod1-new"
updatedPodNS = "kube-system"
)
cache := newEvictedPodsCache()
pod1 := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "pod1",
Namespace: "default",
UID: duplicateUID,
},
}
pod2 := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: updatedPodName,
Namespace: updatedPodNS,
UID: duplicateUID,
},
}
cache.add(pod1)
cache.add(pod2)
pods := cache.list()
if len(pods) != 1 {
t.Fatalf("Expected 1 pod in cache (duplicates should overwrite), got %d", len(pods))
}
if pods[0].Name != updatedPodName || pods[0].Namespace != updatedPodNS {
t.Errorf("Expected pod2 data, got name=%s, namespace=%s", pods[0].Name, pods[0].Namespace)
}
})
t.Run("list returns empty array for empty cache", func(t *testing.T) {
cache := newEvictedPodsCache()
pods := cache.list()
if pods == nil {
t.Fatal("Expected non-nil slice from list()")
}
if len(pods) != 0 {
t.Fatalf("Expected empty list, got %d pods", len(pods))
}
})
t.Run("list returns copies not references", func(t *testing.T) {
const originalPodName = "pod1"
cache := newEvictedPodsCache()
pod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: originalPodName,
Namespace: "default",
UID: "12345678-1234-1234-1234-123456789abc",
},
}
cache.add(pod)
pods1 := cache.list()
pods2 := cache.list()
if len(pods1) != 1 || len(pods2) != 1 {
t.Fatalf("Expected 1 pod in both lists")
}
pods1[0].Name = "modified"
if pods2[0].Name == "modified" {
t.Error("Modifying list result should not affect other list results (should be copies)")
}
pods3 := cache.list()
if pods3[0].Name != originalPodName {
t.Error("Cache data was modified, list() should return copies")
}
})
t.Run("clear empties the cache", func(t *testing.T) {
cache := newEvictedPodsCache()
cache.add(&v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod1", Namespace: "default", UID: "aaaa0000-0000-0000-0000-000000000001"}})
cache.add(&v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod2", Namespace: "kube-system", UID: "bbbb0000-0000-0000-0000-000000000002"}})
if len(cache.list()) != 2 {
t.Fatal("Expected 2 pods before clear")
}
cache.clear()
pods := cache.list()
if len(pods) != 0 {
t.Fatalf("Expected empty cache after clear, got %d pods", len(pods))
}
})
t.Run("clear on empty cache is safe", func(t *testing.T) {
cache := newEvictedPodsCache()
cache.clear()
pods := cache.list()
if len(pods) != 0 {
t.Fatalf("Expected empty cache, got %d pods", len(pods))
}
})
t.Run("add after clear works correctly", func(t *testing.T) {
cache := newEvictedPodsCache()
cache.add(&v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod1", Namespace: "default", UID: "00000001-0001-0001-0001-000000000001"}})
cache.clear()
cache.add(&v1.Pod{ObjectMeta: metav1.ObjectMeta{Name: "pod2", Namespace: "kube-system", UID: "00000002-0002-0002-0002-000000000002"}})
pods := cache.list()
if len(pods) != 1 {
t.Fatalf("Expected 1 pod after clear and add, got %d", len(pods))
}
if pods[0].Name != "pod2" {
t.Errorf("Expected pod2, got %s", pods[0].Name)
}
})
}
+42 -6
View File
@@ -28,6 +28,7 @@ import (
"k8s.io/apimachinery/pkg/labels"
clientset "k8s.io/client-go/kubernetes"
listersv1 "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
"sigs.k8s.io/descheduler/pkg/api"
@@ -78,6 +79,22 @@ func ReadyNodes(ctx context.Context, client clientset.Interface, nodeLister list
return readyNodes, nil
}
// ReadyNodesFromInterfaces converts a list of interface{} items to ready nodes.
// Each interface{} item is expected to be a *v1.Node. Only ready nodes are returned.
func ReadyNodesFromInterfaces(nodeInterfaces []interface{}) ([]*v1.Node, error) {
readyNodes := make([]*v1.Node, 0, len(nodeInterfaces))
for i, nodeInterface := range nodeInterfaces {
node, ok := nodeInterface.(*v1.Node)
if !ok {
return nil, fmt.Errorf("item at index %d is not a *v1.Node", i)
}
if IsReady(node) {
readyNodes = append(readyNodes, node)
}
}
return readyNodes, nil
}
// IsReady checks if the descheduler could run against given node.
func IsReady(node *v1.Node) bool {
for i := range node.Status.Conditions {
@@ -114,7 +131,7 @@ func IsReady(node *v1.Node) bool {
//
// The checks are ordered from fastest to slowest to reduce unnecessary computation,
// especially for nodes that are clearly unsuitable early in the evaluation process.
func NodeFit(nodeIndexer podutil.GetPodsAssignedToNodeFunc, pod *v1.Pod, node *v1.Node) error {
func NodeFit(ctx context.Context, nodeIndexer podutil.GetPodsAssignedToNodeFunc, pod *v1.Pod, node *v1.Node) error {
// Check if the node is marked as unschedulable.
if IsNodeUnschedulable(node) {
return errors.New("node is not schedulable")
@@ -128,7 +145,7 @@ func NodeFit(nodeIndexer podutil.GetPodsAssignedToNodeFunc, pod *v1.Pod, node *v
}
// Check taints on the node that have effect NoSchedule or NoExecute.
ok := utils.TolerationsTolerateTaintsWithFilter(pod.Spec.Tolerations, node.Spec.Taints, func(taint *v1.Taint) bool {
ok := utils.TolerationsTolerateTaintsWithFilter(ctx, pod.Spec.Tolerations, node.Spec.Taints, func(taint *v1.Taint) bool {
return taint.Effect == v1.TaintEffectNoSchedule || taint.Effect == v1.TaintEffectNoExecute
})
if !ok {
@@ -163,7 +180,7 @@ func podFitsNodes(nodeIndexer podutil.GetPodsAssignedToNodeFunc, pod *v1.Pod, no
if excludeFilter != nil && excludeFilter(pod, node) {
return
}
err := NodeFit(nodeIndexer, pod, node)
err := NodeFit(ctx, nodeIndexer, pod, node)
if err == nil {
klog.V(4).InfoS("Pod fits on node", "pod", klog.KObj(pod), "node", klog.KObj(node))
atomic.AddInt32(&filteredLen, 1)
@@ -195,8 +212,8 @@ func PodFitsAnyNode(nodeIndexer podutil.GetPodsAssignedToNodeFunc, pod *v1.Pod,
// PodFitsCurrentNode checks if the given pod will fit onto the given node. The predicates used
// to determine if the pod will fit can be found in the NodeFit function.
func PodFitsCurrentNode(nodeIndexer podutil.GetPodsAssignedToNodeFunc, pod *v1.Pod, node *v1.Node) bool {
err := NodeFit(nodeIndexer, pod, node)
func PodFitsCurrentNode(ctx context.Context, nodeIndexer podutil.GetPodsAssignedToNodeFunc, pod *v1.Pod, node *v1.Node) bool {
err := NodeFit(ctx, nodeIndexer, pod, node)
if err == nil {
klog.V(4).InfoS("Pod fits on node", "pod", klog.KObj(pod), "node", klog.KObj(node))
return true
@@ -241,7 +258,7 @@ func fitsRequest(nodeIndexer podutil.GetPodsAssignedToNodeFunc, pod *v1.Pod, nod
return false, fmt.Errorf("insufficient %v", resource)
}
}
// check pod num, at least one pod number is avaibalbe
// check pod num, at least one pod number is available
if quantity, ok := availableResources[v1.ResourcePods]; ok && quantity.MilliValue() <= 0 {
return false, fmt.Errorf("insufficient %v", v1.ResourcePods)
}
@@ -400,3 +417,22 @@ func podMatchesInterPodAntiAffinity(nodeIndexer podutil.GetPodsAssignedToNodeFun
return false, nil
}
// BuildGetPodsAssignedToNodeFunc establishes an indexer to map the pods and their assigned nodes.
// It returns a function to help us get all the pods that assigned to a node based on the indexer.
func AddNodeSelectorIndexer(nodeInformer cache.SharedIndexInformer, indexerName string, nodeSelector labels.Selector) error {
return nodeInformer.AddIndexers(cache.Indexers{
indexerName: func(obj interface{}) ([]string, error) {
node, ok := obj.(*v1.Node)
if !ok {
return []string{}, errors.New("unexpected object")
}
if nodeSelector.Matches(labels.Set(node.Labels)) {
return []string{indexerName}, nil
}
return []string{}, nil
},
})
}
+258 -3
View File
@@ -19,15 +19,20 @@ package node
import (
"context"
"errors"
"sort"
"strings"
"sync"
"testing"
"github.com/google/go-cmp/cmp"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/utils/ptr"
podutil "sigs.k8s.io/descheduler/pkg/descheduler/pod"
"sigs.k8s.io/descheduler/test"
)
@@ -78,13 +83,205 @@ func TestReadyNodesWithNodeSelector(t *testing.T) {
sharedInformerFactory.WaitForCacheSync(stopChannel)
defer close(stopChannel)
// First verify nodeLister returns non-empty list
allNodes, err := nodeLister.List(labels.Everything())
if err != nil {
t.Fatalf("Failed to list nodes from nodeLister: %v", err)
}
if len(allNodes) == 0 {
t.Fatal("Expected nodeLister to return non-empty list of nodes")
}
if len(allNodes) != 2 {
t.Errorf("Expected nodeLister to return 2 nodes, got %d", len(allNodes))
}
// Now test ReadyNodes
nodes, _ := ReadyNodes(ctx, fakeClient, nodeLister, nodeSelector)
if nodes[0].Name != "node1" {
if len(nodes) != 1 {
t.Errorf("Expected 1 node, got %d", len(nodes))
} else if nodes[0].Name != "node1" {
t.Errorf("Expected node1, got %s", nodes[0].Name)
}
}
func TestReadyNodesFromInterfaces(t *testing.T) {
node1 := test.BuildTestNode("node1", 1000, 2000, 9, nil)
node2 := test.BuildTestNode("node2", 1000, 2000, 9, nil)
node2.Status.Conditions = []v1.NodeCondition{{Type: v1.NodeReady, Status: v1.ConditionFalse}}
node3 := test.BuildTestNode("node3", 1000, 2000, 9, nil)
tests := []struct {
description string
nodeInterfaces []interface{}
expectedCount int
expectedNames []string
expectError bool
errorContains string
}{
{
description: "All nodes are ready",
nodeInterfaces: []interface{}{node1, node3},
expectedCount: 2,
expectedNames: []string{"node1", "node3"},
expectError: false,
},
{
description: "One node is not ready",
nodeInterfaces: []interface{}{node1, node2, node3},
expectedCount: 2,
expectedNames: []string{"node1", "node3"},
expectError: false,
},
{
description: "Empty list",
nodeInterfaces: []interface{}{},
expectedCount: 0,
expectedNames: []string{},
expectError: false,
},
{
description: "Invalid type in list",
nodeInterfaces: []interface{}{node1, "not a node", node3},
expectedCount: 0,
expectError: true,
errorContains: "item at index 1 is not a *v1.Node",
},
}
for _, tc := range tests {
t.Run(tc.description, func(t *testing.T) {
nodes, err := ReadyNodesFromInterfaces(tc.nodeInterfaces)
if tc.expectError {
if err == nil {
t.Errorf("Expected error but got none")
} else if tc.errorContains != "" && !strings.Contains(err.Error(), tc.errorContains) {
t.Errorf("Expected error to contain '%s', got '%s'", tc.errorContains, err.Error())
}
return
}
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if len(nodes) != tc.expectedCount {
t.Errorf("Expected %d nodes, got %d", tc.expectedCount, len(nodes))
}
for i, expectedName := range tc.expectedNames {
if i >= len(nodes) {
t.Errorf("Missing node at index %d, expected %s", i, expectedName)
continue
}
if nodes[i].Name != expectedName {
t.Errorf("Expected node at index %d to be %s, got %s", i, expectedName, nodes[i].Name)
}
}
})
}
}
func TestAddNodeSelectorIndexer(t *testing.T) {
node1 := test.BuildTestNode("node1", 1000, 2000, 9, nil)
node1.Labels = map[string]string{"type": "compute", "zone": "us-east-1"}
node2 := test.BuildTestNode("node2", 1000, 2000, 9, nil)
node2.Labels = map[string]string{"type": "infra", "zone": "us-west-1"}
node3 := test.BuildTestNode("node3", 1000, 2000, 9, nil)
node3.Labels = map[string]string{"type": "compute", "zone": "us-west-1"}
tests := []struct {
description string
indexerName string
selectorString string
expectedMatches []string
}{
{
description: "Index nodes by type=compute",
indexerName: "computeNodes",
selectorString: "type=compute",
expectedMatches: []string{"node1", "node3"},
},
{
description: "Index nodes by type=infra",
indexerName: "infraNodes",
selectorString: "type=infra",
expectedMatches: []string{"node2"},
},
{
description: "Index nodes by zone=us-west-1",
indexerName: "westZoneNodes",
selectorString: "zone=us-west-1",
expectedMatches: []string{"node2", "node3"},
},
{
description: "Index nodes with multiple labels",
indexerName: "computeEastNodes",
selectorString: "type=compute,zone=us-east-1",
expectedMatches: []string{"node1"},
},
{
description: "No matching nodes",
indexerName: "noMatchNodes",
selectorString: "type=storage",
expectedMatches: []string{},
},
}
for _, tc := range tests {
t.Run(tc.description, func(t *testing.T) {
fakeClient := fake.NewSimpleClientset(node1, node2, node3)
sharedInformerFactory := informers.NewSharedInformerFactory(fakeClient, 0)
nodeInformer := sharedInformerFactory.Core().V1().Nodes().Informer()
selector, err := labels.Parse(tc.selectorString)
if err != nil {
t.Fatalf("Failed to parse selector: %v", err)
}
err = AddNodeSelectorIndexer(nodeInformer, tc.indexerName, selector)
if err != nil {
t.Fatalf("AddNodeSelectorIndexer failed: %v", err)
}
stopChannel := make(chan struct{})
sharedInformerFactory.Start(stopChannel)
sharedInformerFactory.WaitForCacheSync(stopChannel)
defer close(stopChannel)
indexer := nodeInformer.GetIndexer()
objs, err := indexer.ByIndex(tc.indexerName, tc.indexerName)
if err != nil {
t.Errorf("Failed to query indexer: %v", err)
return
}
// Extract node names from the results
actualMatches := make([]string, 0, len(objs))
for _, obj := range objs {
node, ok := obj.(*v1.Node)
if !ok {
t.Errorf("Expected *v1.Node, got %T", obj)
continue
}
actualMatches = append(actualMatches, node.Name)
}
// Sort both slices for consistent comparison
sort.Strings(actualMatches)
expectedMatches := make([]string, len(tc.expectedMatches))
copy(expectedMatches, tc.expectedMatches)
sort.Strings(expectedMatches)
// Compare using cmp.Diff
if diff := cmp.Diff(expectedMatches, actualMatches); diff != "" {
t.Errorf("Node matches mismatch (-want +got):\n%s", diff)
}
})
}
}
func TestIsNodeUnschedulable(t *testing.T) {
tests := []struct {
description string
@@ -216,7 +413,7 @@ func TestPodFitsCurrentNode(t *testing.T) {
sharedInformerFactory.Start(ctx.Done())
sharedInformerFactory.WaitForCacheSync(ctx.Done())
actual := PodFitsCurrentNode(getPodsAssignedToNode, tc.pod, tc.node)
actual := PodFitsCurrentNode(ctx, getPodsAssignedToNode, tc.pod, tc.node)
if actual != tc.success {
t.Errorf("Test %#v failed", tc.description)
}
@@ -1020,6 +1217,64 @@ func TestNodeFit(t *testing.T) {
node: node,
podsOnNode: []*v1.Pod{},
},
{
description: "Pod with native sidecars with too much cpu does not fit on node",
pod: test.BuildTestPod("p1", 1, 100, "", func(pod *v1.Pod) {
pod.Spec.InitContainers = append(pod.Spec.InitContainers, v1.Container{
RestartPolicy: ptr.To(v1.ContainerRestartPolicyAlways), // native sidecar
Resources: v1.ResourceRequirements{
Requests: createResourceList(100000, 100*1000*1000, 0),
},
})
}),
node: node,
podsOnNode: []*v1.Pod{},
err: errors.New("insufficient cpu"),
},
{
description: "Pod with native sidecars with too much memory does not fit on node",
pod: test.BuildTestPod("p1", 1, 100, "", func(pod *v1.Pod) {
pod.Spec.InitContainers = append(pod.Spec.InitContainers, v1.Container{
RestartPolicy: ptr.To(v1.ContainerRestartPolicyAlways), // native sidecar
Resources: v1.ResourceRequirements{
Requests: createResourceList(100, 1000*1000*1000*1000, 0),
},
})
}),
node: node,
podsOnNode: []*v1.Pod{},
err: errors.New("insufficient memory"),
},
{
description: "Pod with small native sidecars fits on node",
pod: test.BuildTestPod("p1", 1, 100, "", func(pod *v1.Pod) {
pod.Spec.InitContainers = append(pod.Spec.InitContainers, v1.Container{
RestartPolicy: ptr.To(v1.ContainerRestartPolicyAlways), // native sidecar
Resources: v1.ResourceRequirements{
Requests: createResourceList(100, 100*1000*1000, 0),
},
})
}),
node: node,
podsOnNode: []*v1.Pod{},
},
{
description: "Pod with large overhead does not fit on node",
pod: test.BuildTestPod("p1", 1, 100, "", func(pod *v1.Pod) {
pod.Spec.Overhead = createResourceList(100000, 100*1000*1000, 0)
}),
node: node,
podsOnNode: []*v1.Pod{},
err: errors.New("insufficient cpu"),
},
{
description: "Pod with small overhead fits on node",
pod: test.BuildTestPod("p1", 1, 100, "", func(pod *v1.Pod) {
pod.Spec.Overhead = createResourceList(1, 1*1000*1000, 0)
}),
node: node,
podsOnNode: []*v1.Pod{},
},
}
for _, tc := range tests {
@@ -1043,7 +1298,7 @@ func TestNodeFit(t *testing.T) {
sharedInformerFactory.Start(ctx.Done())
sharedInformerFactory.WaitForCacheSync(ctx.Done())
err = NodeFit(getPodsAssignedToNode, tc.pod, tc.node)
err = NodeFit(ctx, getPodsAssignedToNode, tc.pod, tc.node)
if (err == nil && tc.err != nil) || (err != nil && err.Error() != tc.err.Error()) {
t.Errorf("Test %#v failed, got %v, expect %v", tc.description, err, tc.err)
}
+249
View File
@@ -0,0 +1,249 @@
/*
Copyright 2026 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 descheduler
import (
"context"
"fmt"
"net/http"
"sync"
"time"
promapi "github.com/prometheus/client_golang/api"
apierrors "k8s.io/apimachinery/pkg/api/errors"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/informers"
corev1listers "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue"
"k8s.io/klog/v2"
"sigs.k8s.io/descheduler/pkg/api"
"sigs.k8s.io/descheduler/pkg/descheduler/client"
)
const (
prometheusAuthTokenSecretKey = "prometheusAuthToken"
workQueueKey = "key"
)
// inClusterPromClientController manages prometheus client using in-cluster SA token
type inClusterPromClientController struct {
mu sync.RWMutex
promClient promapi.Client
previousPrometheusClientTransport *http.Transport
currentPrometheusAuthToken string
prometheusConfig *api.Prometheus
createPrometheusClient createPrometheusClientFunc
inClusterConfig inClusterConfigFunc
}
// secretBasedPromClientController manages prometheus client using Kubernetes secret
type secretBasedPromClientController struct {
mu sync.RWMutex
promClient promapi.Client
previousPrometheusClientTransport *http.Transport
queue workqueue.RateLimitingInterface
currentPrometheusAuthToken string
namespacedSecretsLister corev1listers.SecretNamespaceLister
prometheusConfig *api.Prometheus
createPrometheusClient createPrometheusClientFunc
}
type (
createPrometheusClientFunc func(url, token string) (promapi.Client, *http.Transport, error)
inClusterConfigFunc func() (*rest.Config, error)
)
func newInClusterPromClientController(prometheusClient promapi.Client, prometheusConfig *api.Prometheus) *inClusterPromClientController {
return &inClusterPromClientController{
promClient: prometheusClient,
prometheusConfig: prometheusConfig,
createPrometheusClient: client.CreatePrometheusClient,
inClusterConfig: rest.InClusterConfig,
}
}
func newSecretBasedPromClientController(prometheusClient promapi.Client, prometheusConfig *api.Prometheus, namespacedSharedInformerFactory informers.SharedInformerFactory) (*secretBasedPromClientController, error) {
if prometheusConfig == nil || prometheusConfig.AuthToken == nil || prometheusConfig.AuthToken.SecretReference == nil {
return nil, fmt.Errorf("prometheus metrics source configuration is missing authentication token secret")
}
authTokenSecret := prometheusConfig.AuthToken.SecretReference
if authTokenSecret.Name == "" || authTokenSecret.Namespace == "" {
return nil, fmt.Errorf("prometheus metrics source configuration is missing authentication token secret")
}
if namespacedSharedInformerFactory == nil {
return nil, fmt.Errorf("namespacedSharedInformerFactory not configured")
}
ctrl := &secretBasedPromClientController{
promClient: prometheusClient,
queue: workqueue.NewRateLimitingQueueWithConfig(workqueue.DefaultControllerRateLimiter(), workqueue.RateLimitingQueueConfig{Name: "descheduler"}),
prometheusConfig: prometheusConfig,
createPrometheusClient: client.CreatePrometheusClient,
}
namespacedSharedInformerFactory.Core().V1().Secrets().Informer().AddEventHandler(ctrl.eventHandler())
ctrl.namespacedSecretsLister = namespacedSharedInformerFactory.Core().V1().Secrets().Lister().Secrets(authTokenSecret.Namespace)
return ctrl, nil
}
func (ctrl *inClusterPromClientController) prometheusClient() promapi.Client {
ctrl.mu.RLock()
defer ctrl.mu.RUnlock()
return ctrl.promClient
}
func (ctrl *secretBasedPromClientController) prometheusClient() promapi.Client {
ctrl.mu.RLock()
defer ctrl.mu.RUnlock()
return ctrl.promClient
}
func (ctrl *inClusterPromClientController) reconcileInClusterSAToken() error {
ctrl.mu.Lock()
defer ctrl.mu.Unlock()
// Read the sa token and assume it has the sufficient permissions to authenticate
cfg, err := ctrl.inClusterConfig()
if err == nil {
if ctrl.currentPrometheusAuthToken != cfg.BearerToken {
klog.V(2).Infof("Creating Prometheus client (with SA token)")
prometheusClient, transport, err := ctrl.createPrometheusClient(ctrl.prometheusConfig.URL, cfg.BearerToken)
if err != nil {
ctrl.clearConnection()
return fmt.Errorf("unable to create a prometheus client: %v", err)
}
ctrl.promClient = prometheusClient
if ctrl.previousPrometheusClientTransport != nil {
ctrl.previousPrometheusClientTransport.CloseIdleConnections()
}
ctrl.previousPrometheusClientTransport = transport
ctrl.currentPrometheusAuthToken = cfg.BearerToken
}
return nil
}
if err == rest.ErrNotInCluster {
return nil
}
return fmt.Errorf("unexpected error when reading in cluster config: %v", err)
}
func clearPromClientConnection(currentPrometheusAuthToken *string, previousPrometheusClientTransport **http.Transport, promClient *promapi.Client) {
*currentPrometheusAuthToken = ""
if *previousPrometheusClientTransport != nil {
(*previousPrometheusClientTransport).CloseIdleConnections()
}
*previousPrometheusClientTransport = nil
*promClient = nil
}
func (ctrl *inClusterPromClientController) clearConnection() {
clearPromClientConnection(&ctrl.currentPrometheusAuthToken, &ctrl.previousPrometheusClientTransport, &ctrl.promClient)
}
func (ctrl *secretBasedPromClientController) clearConnection() {
clearPromClientConnection(&ctrl.currentPrometheusAuthToken, &ctrl.previousPrometheusClientTransport, &ctrl.promClient)
}
func (ctrl *secretBasedPromClientController) runAuthenticationSecretReconciler(ctx context.Context) {
defer utilruntime.HandleCrash()
defer ctrl.queue.ShutDown()
klog.Infof("Starting authentication secret reconciler")
defer klog.Infof("Shutting down authentication secret reconciler")
go wait.UntilWithContext(ctx, ctrl.runAuthenticationSecretReconcilerWorker, time.Second)
<-ctx.Done()
}
func (ctrl *secretBasedPromClientController) runAuthenticationSecretReconcilerWorker(ctx context.Context) {
for ctrl.processNextWorkItem(ctx) {
}
}
func (ctrl *secretBasedPromClientController) processNextWorkItem(ctx context.Context) bool {
dsKey, quit := ctrl.queue.Get()
if quit {
return false
}
defer ctrl.queue.Done(dsKey)
err := ctrl.sync()
if err == nil {
ctrl.queue.Forget(dsKey)
return true
}
utilruntime.HandleError(fmt.Errorf("%v failed with : %v", dsKey, err))
ctrl.queue.AddRateLimited(dsKey)
return true
}
func (ctrl *secretBasedPromClientController) sync() error {
ctrl.mu.Lock()
defer ctrl.mu.Unlock()
prometheusConfig := ctrl.prometheusConfig
ns := prometheusConfig.AuthToken.SecretReference.Namespace
name := prometheusConfig.AuthToken.SecretReference.Name
secretObj, err := ctrl.namespacedSecretsLister.Get(name)
if err != nil {
// clear the token if the secret is not found
if apierrors.IsNotFound(err) {
ctrl.clearConnection()
}
return fmt.Errorf("unable to get %v/%v secret", ns, name)
}
authToken := string(secretObj.Data[prometheusAuthTokenSecretKey])
if authToken == "" {
ctrl.clearConnection()
return fmt.Errorf("prometheus authentication token secret missing %q data or empty", prometheusAuthTokenSecretKey)
}
if ctrl.currentPrometheusAuthToken == authToken {
return nil
}
klog.V(2).Infof("authentication secret token updated, recreating prometheus client")
prometheusClient, transport, err := ctrl.createPrometheusClient(prometheusConfig.URL, authToken)
if err != nil {
ctrl.clearConnection()
return fmt.Errorf("unable to create a prometheus client: %v", err)
}
ctrl.promClient = prometheusClient
if ctrl.previousPrometheusClientTransport != nil {
ctrl.previousPrometheusClientTransport.CloseIdleConnections()
}
ctrl.previousPrometheusClientTransport = transport
ctrl.currentPrometheusAuthToken = authToken
return nil
}
func (ctrl *secretBasedPromClientController) eventHandler() cache.ResourceEventHandler {
return cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) { ctrl.queue.Add(workQueueKey) },
UpdateFunc: func(old, new interface{}) { ctrl.queue.Add(workQueueKey) },
DeleteFunc: func(obj interface{}) { ctrl.queue.Add(workQueueKey) },
}
}
@@ -0,0 +1,811 @@
/*
Copyright 2026 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 descheduler
import (
"context"
"fmt"
"net/http"
"sync"
"testing"
"time"
promapi "github.com/prometheus/client_golang/api"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
fakeclientset "k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/rest"
"sigs.k8s.io/descheduler/pkg/api"
)
func TestPromClientControllerSync_InvalidConfig(t *testing.T) {
testCases := []struct {
name string
objects []runtime.Object
prometheusConfig *api.Prometheus
expectedErr error
}{
{
name: "empty prometheus config",
prometheusConfig: nil,
expectedErr: fmt.Errorf("prometheus metrics source configuration is missing authentication token secret"),
},
{
name: "missing prometheus config",
prometheusConfig: &api.Prometheus{
URL: prometheusURL,
},
expectedErr: fmt.Errorf("prometheus metrics source configuration is missing authentication token secret"),
},
{
name: "missing auth token config",
prometheusConfig: &api.Prometheus{
URL: prometheusURL,
AuthToken: nil,
},
expectedErr: fmt.Errorf("prometheus metrics source configuration is missing authentication token secret"),
},
{
name: "missing secret reference",
prometheusConfig: &api.Prometheus{
URL: prometheusURL,
AuthToken: &api.AuthToken{
SecretReference: nil,
},
},
expectedErr: fmt.Errorf("prometheus metrics source configuration is missing authentication token secret"),
},
{
name: "missing secret reference name",
prometheusConfig: &api.Prometheus{
URL: prometheusURL,
AuthToken: &api.AuthToken{
SecretReference: &api.SecretReference{
Namespace: "kube-system",
},
},
},
expectedErr: fmt.Errorf("prometheus metrics source configuration is missing authentication token secret"),
},
{
name: "missing secret reference namespace",
prometheusConfig: &api.Prometheus{
URL: prometheusURL,
AuthToken: &api.AuthToken{
SecretReference: &api.SecretReference{
Name: "prom-token",
},
},
},
expectedErr: fmt.Errorf("prometheus metrics source configuration is missing authentication token secret"),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.TODO())
defer cancel()
_, err := setupPromClientControllerTest(ctx, t, tc.objects, tc.prometheusConfig, false)
// Verify error expectations
if tc.expectedErr != nil {
if err == nil {
t.Errorf("Expected error %q but got none", tc.expectedErr)
} else if err.Error() != tc.expectedErr.Error() {
t.Errorf("Expected error %q but got %q", tc.expectedErr, err.Error())
}
} else {
t.Errorf("Expected an error, got none")
}
})
}
}
func TestPromClientControllerSync_InvalidSecret(t *testing.T) {
testCases := []struct {
name string
objects []runtime.Object
prometheusConfig *api.Prometheus
expectedErr error
}{
{
name: "secret exists but empty token",
objects: []runtime.Object{newPrometheusAuthSecret(withToken(""))},
prometheusConfig: newPrometheusConfig(),
expectedErr: fmt.Errorf("prometheus authentication token secret missing \"prometheusAuthToken\" data or empty"),
},
{
name: "secret exists but missing token key",
objects: []runtime.Object{newPrometheusAuthSecret(func(s *v1.Secret) {
s.Data = map[string][]byte{}
})},
prometheusConfig: newPrometheusConfig(),
expectedErr: fmt.Errorf("prometheus authentication token secret missing \"prometheusAuthToken\" data or empty"),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.TODO())
defer cancel()
setup, err := setupPromClientControllerTest(ctx, t, tc.objects, tc.prometheusConfig, true)
if err != nil {
t.Fatal(err)
}
// Call sync
err = setup.ctrl.sync()
// Verify error expectations
if tc.expectedErr != nil {
if err == nil {
t.Errorf("Expected error %q but got none", tc.expectedErr)
} else if err.Error() != tc.expectedErr.Error() {
t.Errorf("Expected error %q but got %q", tc.expectedErr, err.Error())
}
} else {
t.Errorf("Expected an error, got none")
}
})
}
}
func TestPromClientControllerSync_ClientCreation(t *testing.T) {
testCases := []struct {
name string
objects []runtime.Object
currentAuthToken string
createPrometheusClientFunc func(url, token string) (promapi.Client, *http.Transport, error)
expectedErr error
expectClientCreated bool
expectCurrentTokenCleared bool
expectPreviousTransportCleared bool
}{
{
name: "secret not found",
currentAuthToken: "old-token",
expectedErr: fmt.Errorf("unable to get kube-system/prom-token secret"),
createPrometheusClientFunc: func(url, token string) (promapi.Client, *http.Transport, error) {
t.Fatalf("unexpected create client invocation")
return nil, nil, fmt.Errorf("unexpected create client invocation")
},
expectCurrentTokenCleared: true,
expectPreviousTransportCleared: true,
},
{
name: "token unchanged - no client creation",
objects: []runtime.Object{newPrometheusAuthSecret(withToken("same-token"))},
currentAuthToken: "same-token",
createPrometheusClientFunc: func(url, token string) (promapi.Client, *http.Transport, error) {
t.Fatalf("unexpected create client invocation")
return nil, nil, fmt.Errorf("unexpected create client invocation")
},
expectClientCreated: false,
},
{
name: "token changed - client created successfully",
objects: []runtime.Object{newPrometheusAuthSecret(withToken("new-token"))},
currentAuthToken: "old-token",
createPrometheusClientFunc: func(url, token string) (promapi.Client, *http.Transport, error) {
return &mockPrometheusClient{name: "new-client"}, &http.Transport{}, nil
},
expectClientCreated: true,
},
{
name: "token changed - client creation fails",
objects: []runtime.Object{newPrometheusAuthSecret(withToken("new-token"))},
currentAuthToken: "old-token",
createPrometheusClientFunc: func(url, token string) (promapi.Client, *http.Transport, error) {
return nil, nil, fmt.Errorf("failed to create client")
},
expectedErr: fmt.Errorf("unable to create a prometheus client: failed to create client"),
expectClientCreated: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
for _, setupMode := range []struct {
name string
setupFn func(context.Context, *testing.T, []runtime.Object) *secretBasedPromClientController
}{
{
name: "running with prom reconciler directly",
setupFn: func(ctx context.Context, t *testing.T, objects []runtime.Object) *secretBasedPromClientController {
setup, err := setupPromClientControllerTest(ctx, t, objects, newPrometheusConfig(), true)
if err != nil {
t.Fatal(err)
}
return setup.ctrl
},
},
{
name: "running with full descheduler",
setupFn: func(ctx context.Context, t *testing.T, objects []runtime.Object) *secretBasedPromClientController {
deschedulerPolicy := &api.DeschedulerPolicy{
MetricsProviders: []api.MetricsProvider{
{
Source: api.PrometheusMetrics,
Prometheus: newPrometheusConfig(),
},
},
}
_, descheduler, _, _ := initDescheduler(t, ctx, initFeatureGates(), deschedulerPolicy, nil, false, objects...)
return descheduler.secretBasedPromClientCtrl
},
},
} {
t.Run(setupMode.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.TODO())
defer cancel()
ctrl := setupMode.setupFn(ctx, t, tc.objects)
// Set additional test-specific fields
ctrl.currentPrometheusAuthToken = tc.currentAuthToken
if tc.currentAuthToken != "" {
ctrl.previousPrometheusClientTransport = &http.Transport{}
}
// Mock createPrometheusClient
clientCreated := false
if tc.createPrometheusClientFunc != nil {
ctrl.createPrometheusClient = func(url, token string) (promapi.Client, *http.Transport, error) {
client, transport, err := tc.createPrometheusClientFunc(url, token)
if err == nil {
clientCreated = true
}
return client, transport, err
}
}
// Call sync
err := ctrl.sync()
// Verify error expectations
if tc.expectedErr != nil {
if err == nil {
t.Errorf("Expected error %q but got none", tc.expectedErr)
} else if err.Error() != tc.expectedErr.Error() {
t.Errorf("Expected error %q but got %q", tc.expectedErr, err.Error())
}
} else {
if err != nil {
t.Errorf("Expected no error but got: %v", err)
}
}
// Verify client creation expectations
if tc.expectClientCreated && !clientCreated {
t.Errorf("Expected prometheus client to be created but it wasn't")
}
if !tc.expectClientCreated && clientCreated {
t.Errorf("Expected prometheus client not to be created but it was")
}
// Verify token cleared expectations
if tc.expectCurrentTokenCleared && ctrl.currentPrometheusAuthToken != "" {
t.Errorf("Expected current auth token to be cleared but it wasn't")
}
// Verify previous transport cleared expectations
if tc.expectPreviousTransportCleared && ctrl.previousPrometheusClientTransport != nil {
t.Errorf("Expected previous transport to be cleared but it wasn't")
}
// Verify promClient cleared when secret not found
if tc.expectPreviousTransportCleared && ctrl.prometheusClient() != nil {
t.Errorf("Expected promClient to be cleared but it wasn't")
}
// Verify token updated when client created
if tc.expectClientCreated && len(tc.objects) > 0 {
if secret, ok := tc.objects[0].(*v1.Secret); ok && secret.Data != nil {
expectedToken := string(secret.Data[prometheusAuthTokenSecretKey])
if ctrl.currentPrometheusAuthToken != expectedToken {
t.Errorf("Expected current auth token to be %q but got %q", expectedToken, ctrl.currentPrometheusAuthToken)
}
}
}
})
}
})
}
}
func TestPromClientControllerSync_EventHandler(t *testing.T) {
testCases := []struct {
name string
operation func(ctx context.Context, fakeClient *fakeclientset.Clientset) error
processItem bool
expectedPromClientSet bool
expectedCreatedClientsCount int
expectedCurrentToken string
expectedPreviousTransportCleared bool
expectDifferentClients bool
expectCreatePrometheusClientError bool
}{
// Check initial conditions
{
name: "no secret initially",
operation: func(ctx context.Context, fakeClient *fakeclientset.Clientset) error { return nil },
processItem: false,
expectedPromClientSet: false,
expectedCreatedClientsCount: 0,
expectedCurrentToken: "",
},
// Change conditions
{
name: "add secret",
operation: func(ctx context.Context, fakeClient *fakeclientset.Clientset) error {
secret := newPrometheusAuthSecret(withToken("token-1"))
_, err := fakeClient.CoreV1().Secrets(secret.Namespace).Create(ctx, secret, metav1.CreateOptions{})
return err
},
processItem: true,
expectedPromClientSet: true,
expectedCreatedClientsCount: 1,
expectedCurrentToken: "token-1",
},
{
name: "update secret",
operation: func(ctx context.Context, fakeClient *fakeclientset.Clientset) error {
secret := newPrometheusAuthSecret(withToken("token-2"))
_, err := fakeClient.CoreV1().Secrets(secret.Namespace).Update(ctx, secret, metav1.UpdateOptions{})
return err
},
processItem: true,
expectedPromClientSet: true,
expectedCreatedClientsCount: 2,
expectedCurrentToken: "token-2",
expectDifferentClients: true,
},
{
name: "update secret with invalid data",
operation: func(ctx context.Context, fakeClient *fakeclientset.Clientset) error {
secret := newPrometheusAuthSecret(withToken("token-3"))
secret.Data[prometheusAuthTokenSecretKey] = []byte{}
_, err := fakeClient.CoreV1().Secrets(secret.Namespace).Update(ctx, secret, metav1.UpdateOptions{})
return err
},
processItem: true,
expectedPromClientSet: false,
expectedCreatedClientsCount: 2,
expectedCurrentToken: "",
expectDifferentClients: true,
},
{
name: "update secret with valid data",
operation: func(ctx context.Context, fakeClient *fakeclientset.Clientset) error {
secret := newPrometheusAuthSecret(withToken("token-4"))
_, err := fakeClient.CoreV1().Secrets(secret.Namespace).Update(ctx, secret, metav1.UpdateOptions{})
return err
},
processItem: true,
expectedPromClientSet: true,
expectedCreatedClientsCount: 3,
expectedCurrentToken: "token-4",
expectDifferentClients: true,
},
{
name: "update secret with valid data but createPrometheusClient failing",
operation: func(ctx context.Context, fakeClient *fakeclientset.Clientset) error {
secret := newPrometheusAuthSecret(withToken("token-5"))
_, err := fakeClient.CoreV1().Secrets(secret.Namespace).Update(ctx, secret, metav1.UpdateOptions{})
return err
},
processItem: true,
expectedPromClientSet: false,
expectedCreatedClientsCount: 3,
expectedCurrentToken: "",
expectDifferentClients: true,
expectCreatePrometheusClientError: true,
},
{
name: "delete secret",
operation: func(ctx context.Context, fakeClient *fakeclientset.Clientset) error {
secret := newPrometheusAuthSecret(withToken("token-5"))
return fakeClient.CoreV1().Secrets(secret.Namespace).Delete(ctx, secret.Name, metav1.DeleteOptions{})
},
processItem: true,
expectedPromClientSet: false,
expectedCreatedClientsCount: 3,
expectedCurrentToken: "",
expectedPreviousTransportCleared: true,
},
}
for _, setupMode := range []struct {
name string
init func(t *testing.T, ctx context.Context) (ctrl *secretBasedPromClientController, fakeClient *fakeclientset.Clientset)
}{
{
name: "running with prom reconciler directly",
init: func(t *testing.T, ctx context.Context) (ctrl *secretBasedPromClientController, fakeClient *fakeclientset.Clientset) {
setup, err := setupPromClientControllerTest(ctx, t, nil, newPrometheusConfig(), true)
if err != nil {
t.Fatal(err)
}
// Start the reconciler to process queue items
go setup.ctrl.runAuthenticationSecretReconciler(ctx)
return setup.ctrl, setup.fakeClient
},
},
{
name: "running with full descheduler",
init: func(t *testing.T, ctx context.Context) (ctrl *secretBasedPromClientController, fakeClient *fakeclientset.Clientset) {
deschedulerPolicy := &api.DeschedulerPolicy{
MetricsProviders: []api.MetricsProvider{
{
Source: api.PrometheusMetrics,
Prometheus: newPrometheusConfig(),
},
},
}
_, descheduler, _, client := initDescheduler(t, ctx, initFeatureGates(), deschedulerPolicy, nil, false)
// The reconciler is already started by initDescheduler via bootstrapDescheduler
return descheduler.secretBasedPromClientCtrl, client
},
},
} {
t.Run(setupMode.name, func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ctrl, fakeClient := setupMode.init(t, ctx)
// Track created clients to verify different instances
var createdClients []promapi.Client
var createdClientsMu sync.Mutex
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctrl.mu.Lock()
ctrl.createPrometheusClient = func(url, token string) (promapi.Client, *http.Transport, error) {
if tc.expectCreatePrometheusClientError {
return nil, &http.Transport{}, fmt.Errorf("error creating a prometheus client")
}
client := &mockPrometheusClient{name: "client-" + token}
createdClientsMu.Lock()
createdClients = append(createdClients, client)
createdClientsMu.Unlock()
return client, &http.Transport{}, nil
}
ctrl.mu.Unlock()
if err := tc.operation(ctx, fakeClient); err != nil {
t.Fatalf("Failed to execute operation: %v", err)
}
if tc.processItem {
// Wait for event to be processed by the reconciler
err := wait.PollUntilContextTimeout(ctx, 10*time.Millisecond, 2*time.Second, true, func(ctx context.Context) (bool, error) {
// Check if all expected conditions are met (with mutex protection)
ctrl.mu.RLock()
promClient := ctrl.promClient
currentToken := ctrl.currentPrometheusAuthToken
previousTransport := ctrl.previousPrometheusClientTransport
ctrl.mu.RUnlock()
t.Logf("promClient: %v\n", promClient)
if tc.expectedPromClientSet {
if promClient == nil {
return false, nil
}
} else {
if promClient != nil {
return false, nil
}
}
createdClientsMu.Lock()
createdClientsLen := len(createdClients)
createdClientsMu.Unlock()
t.Logf("createdClientsLen: %v\n", createdClientsLen)
if createdClientsLen != tc.expectedCreatedClientsCount {
return false, nil
}
t.Logf("currentToken: %v\n", currentToken)
if currentToken != tc.expectedCurrentToken {
return false, nil
}
t.Logf("previousTransport: %v\n", previousTransport)
if tc.expectedPreviousTransportCleared {
if previousTransport != nil {
return false, nil
}
}
return true, nil
})
if err != nil {
t.Fatalf("Timed out waiting for expected conditions: %v", err)
}
// Log all expected conditions that were met
t.Logf("All expected conditions met: promClientSet=%v, createdClientsCount=%d, currentToken=%q, previousTransportCleared=%v",
tc.expectedPromClientSet, tc.expectedCreatedClientsCount, tc.expectedCurrentToken, tc.expectedPreviousTransportCleared)
}
// Validate post-conditions
if tc.expectedPromClientSet {
if ctrl.prometheusClient() == nil {
t.Error("Expected prometheus client to be set, but it was nil")
}
} else {
promClient := ctrl.prometheusClient()
if promClient != nil {
t.Errorf("Expected prometheus client to be nil, but got: %v", promClient)
}
}
createdClientsMu.Lock()
createdClientsLen := len(createdClients)
createdClientsMu.Unlock()
if createdClientsLen != tc.expectedCreatedClientsCount {
t.Errorf("Expected %d clients created, but got %d", tc.expectedCreatedClientsCount, len(createdClients))
}
if ctrl.currentPrometheusAuthToken != tc.expectedCurrentToken {
t.Errorf("Expected current token to be %q, got %q", tc.expectedCurrentToken, ctrl.currentPrometheusAuthToken)
}
if tc.expectedPreviousTransportCleared {
if ctrl.previousPrometheusClientTransport != nil {
t.Error("Expected previous transport to be cleared, but it was set")
}
}
if tc.expectDifferentClients && len(createdClients) >= 2 {
createdClientsMu.Lock()
defer createdClientsMu.Unlock()
if createdClients[0] == createdClients[1] {
t.Error("Expected different client instances")
}
}
})
}
})
}
}
func TestReconcileInClusterSAToken(t *testing.T) {
testCases := []struct {
name string
currentAuthToken string
inClusterConfigFunc func() (*rest.Config, error)
createPrometheusClientFunc func(url, token string) (promapi.Client, *http.Transport, error)
expectedErr error
expectClientCreated bool
expectCurrentToken string
expectPreviousTransportCleared bool
expectPromClientCleared bool
}{
{
name: "token unchanged - no client creation",
currentAuthToken: "same-token",
inClusterConfigFunc: func() (*rest.Config, error) {
return &rest.Config{BearerToken: "same-token"}, nil
},
expectClientCreated: false,
expectCurrentToken: "same-token",
},
{
name: "token changed - client created successfully",
currentAuthToken: "old-token",
inClusterConfigFunc: func() (*rest.Config, error) {
return &rest.Config{BearerToken: "new-token"}, nil
},
createPrometheusClientFunc: func(url, token string) (promapi.Client, *http.Transport, error) {
if token != "new-token" {
t.Errorf("Expected token to be %q, got %q", "new-token", token)
}
return &mockPrometheusClient{name: "new-client"}, &http.Transport{}, nil
},
expectClientCreated: true,
expectCurrentToken: "new-token",
},
{
name: "token changed - client creation fails",
currentAuthToken: "old-token",
inClusterConfigFunc: func() (*rest.Config, error) {
return &rest.Config{BearerToken: "new-token"}, nil
},
createPrometheusClientFunc: func(url, token string) (promapi.Client, *http.Transport, error) {
return nil, nil, fmt.Errorf("failed to create client")
},
expectedErr: fmt.Errorf("unable to create a prometheus client: failed to create client"),
expectClientCreated: false,
expectCurrentToken: "",
expectPreviousTransportCleared: false,
expectPromClientCleared: false,
},
{
name: "not in cluster - no error",
currentAuthToken: "current-token",
inClusterConfigFunc: func() (*rest.Config, error) {
return nil, rest.ErrNotInCluster
},
expectClientCreated: false,
expectCurrentToken: "current-token",
},
{
name: "unexpected error",
currentAuthToken: "current-token",
inClusterConfigFunc: func() (*rest.Config, error) {
return nil, fmt.Errorf("unexpected error")
},
expectedErr: fmt.Errorf("unexpected error when reading in cluster config: unexpected error"),
expectClientCreated: false,
expectCurrentToken: "current-token",
},
{
name: "first token - client created successfully",
currentAuthToken: "",
inClusterConfigFunc: func() (*rest.Config, error) {
return &rest.Config{BearerToken: "first-token"}, nil
},
createPrometheusClientFunc: func(url, token string) (promapi.Client, *http.Transport, error) {
return &mockPrometheusClient{name: "first-client"}, &http.Transport{}, nil
},
expectClientCreated: true,
expectCurrentToken: "first-token",
},
{
name: "token changed with previous transport - clears previous transport",
currentAuthToken: "old-token",
inClusterConfigFunc: func() (*rest.Config, error) {
return &rest.Config{BearerToken: "new-token"}, nil
},
createPrometheusClientFunc: func(url, token string) (promapi.Client, *http.Transport, error) {
return &mockPrometheusClient{name: "new-client"}, &http.Transport{}, nil
},
expectClientCreated: true,
expectCurrentToken: "new-token",
expectPreviousTransportCleared: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
for _, setupMode := range []struct {
name string
init func(t *testing.T) *inClusterPromClientController
}{
{
name: "running with prom reconciler directly",
init: func(t *testing.T) *inClusterPromClientController {
return &inClusterPromClientController{
currentPrometheusAuthToken: tc.currentAuthToken,
prometheusConfig: &api.Prometheus{
URL: prometheusURL,
},
inClusterConfig: tc.inClusterConfigFunc,
}
},
},
{
name: "running with full descheduler",
init: func(t *testing.T) *inClusterPromClientController {
ctx := context.Background()
prometheusConfig := &api.Prometheus{
URL: prometheusURL,
}
deschedulerPolicy := &api.DeschedulerPolicy{
MetricsProviders: []api.MetricsProvider{
{
Source: api.PrometheusMetrics,
Prometheus: prometheusConfig,
},
},
}
_, descheduler, _, _ := initDescheduler(t, ctx, initFeatureGates(), deschedulerPolicy, nil, false)
// Override the fields needed for this test (no need for a mutex
// since reconcileInClusterSAToken gets to run later)
descheduler.inClusterPromClientCtrl.currentPrometheusAuthToken = tc.currentAuthToken
descheduler.inClusterPromClientCtrl.inClusterConfig = tc.inClusterConfigFunc
return descheduler.inClusterPromClientCtrl
},
},
} {
t.Run(setupMode.name, func(t *testing.T) {
ctrl := setupMode.init(t)
// Set previous transport and client if test expects them to be cleared
if tc.expectPreviousTransportCleared {
ctrl.previousPrometheusClientTransport = &http.Transport{}
}
if tc.expectPromClientCleared {
ctrl.promClient = &mockPrometheusClient{name: "old-client"}
}
// Mock createPrometheusClient
clientCreated := false
if tc.createPrometheusClientFunc != nil {
ctrl.createPrometheusClient = func(url, token string) (promapi.Client, *http.Transport, error) {
client, transport, err := tc.createPrometheusClientFunc(url, token)
if err == nil {
clientCreated = true
}
return client, transport, err
}
}
// Call reconcileInClusterSAToken
err := ctrl.reconcileInClusterSAToken()
// Verify error expectations
if tc.expectedErr != nil {
if err == nil {
t.Errorf("Expected error %q but got none", tc.expectedErr)
} else if err.Error() != tc.expectedErr.Error() {
t.Errorf("Expected error %q but got %q", tc.expectedErr, err.Error())
}
} else {
if err != nil {
t.Errorf("Expected no error but got: %v", err)
}
}
// Verify client creation expectations
if tc.expectClientCreated && !clientCreated {
t.Errorf("Expected prometheus client to be created but it wasn't")
}
if !tc.expectClientCreated && clientCreated {
t.Errorf("Expected prometheus client not to be created but it was")
}
// Verify token expectations
if ctrl.currentPrometheusAuthToken != tc.expectCurrentToken {
t.Errorf("Expected current token to be %q but got %q", tc.expectCurrentToken, ctrl.currentPrometheusAuthToken)
}
// Verify previous transport cleared when expected
if tc.expectPreviousTransportCleared {
if tc.expectClientCreated {
// Success case: new transport should be set
if ctrl.previousPrometheusClientTransport == nil {
t.Error("Expected previous transport to be set to new transport, but it was nil")
}
} else if tc.expectedErr != nil {
// Failure case: transport should be nil
if ctrl.previousPrometheusClientTransport != nil {
t.Error("Expected previous transport to be cleared on error, but it was set")
}
}
}
// Verify promClient cleared when expected
if tc.expectPromClientCleared {
if ctrl.promClient != nil {
t.Error("Expected promClient to be cleared, but it was set")
}
}
})
}
})
}
}
+5
View File
@@ -23,6 +23,7 @@ type HandleImpl struct {
PodEvictorImpl *evictions.PodEvictor
MetricsCollectorImpl *metricscollector.MetricsCollector
PrometheusClientImpl promapi.Client
PluginInstanceIDImpl string
}
var _ frameworktypes.Handle = &HandleImpl{}
@@ -62,3 +63,7 @@ func (hi *HandleImpl) PreEvictionFilter(pod *v1.Pod) bool {
func (hi *HandleImpl) Evict(ctx context.Context, pod *v1.Pod, opts evictions.EvictOptions) error {
return hi.PodEvictorImpl.EvictPod(ctx, pod, opts)
}
func (hi *HandleImpl) PluginInstanceID() string {
return hi.PluginInstanceIDImpl
}
+68
View File
@@ -73,6 +73,22 @@ func NewPluginFncFromFake(fp *FakePlugin) pluginregistry.PluginBuilder {
}
}
func NewPluginFncFromFakeWithReactor(fp *FakePlugin, callback func(ActionImpl)) pluginregistry.PluginBuilder {
return func(ctx context.Context, args runtime.Object, handle frameworktypes.Handle) (frameworktypes.Plugin, error) {
fakePluginArgs, ok := args.(*FakePluginArgs)
if !ok {
return nil, fmt.Errorf("want args to be of type FakePluginArgs, got %T", args)
}
fp.handle = handle
fp.args = fakePluginArgs
callback(ActionImpl{handle: fp.handle})
return fp, nil
}
}
// New builds plugin from its arguments while passing a handle
func New(ctx context.Context, args runtime.Object, handle frameworktypes.Handle) (frameworktypes.Plugin, error) {
fakePluginArgs, ok := args.(*FakePluginArgs)
@@ -408,3 +424,55 @@ func (d *FakeFilterPlugin) handleBoolAction(action Action) bool {
}
panic(fmt.Errorf("unhandled %q action", action.GetExtensionPoint()))
}
// RegisterFakePlugin registers a FakePlugin with the given registry
func RegisterFakePlugin(name string, plugin *FakePlugin, registry pluginregistry.Registry) {
pluginregistry.Register(
name,
NewPluginFncFromFake(plugin),
&FakePlugin{},
&FakePluginArgs{},
ValidateFakePluginArgs,
SetDefaults_FakePluginArgs,
registry,
)
}
// RegisterFakeDeschedulePlugin registers a FakeDeschedulePlugin with the given registry
func RegisterFakeDeschedulePlugin(name string, plugin *FakeDeschedulePlugin, registry pluginregistry.Registry) {
pluginregistry.Register(
name,
NewFakeDeschedulePluginFncFromFake(plugin),
&FakeDeschedulePlugin{},
&FakeDeschedulePluginArgs{},
ValidateFakePluginArgs,
SetDefaults_FakePluginArgs,
registry,
)
}
// RegisterFakeBalancePlugin registers a FakeBalancePlugin with the given registry
func RegisterFakeBalancePlugin(name string, plugin *FakeBalancePlugin, registry pluginregistry.Registry) {
pluginregistry.Register(
name,
NewFakeBalancePluginFncFromFake(plugin),
&FakeBalancePlugin{},
&FakeBalancePluginArgs{},
ValidateFakePluginArgs,
SetDefaults_FakePluginArgs,
registry,
)
}
// RegisterFakeFilterPlugin registers a FakeFilterPlugin with the given registry
func RegisterFakeFilterPlugin(name string, plugin *FakeFilterPlugin, registry pluginregistry.Registry) {
pluginregistry.Register(
name,
NewFakeFilterPluginFncFromFake(plugin),
&FakeFilterPlugin{},
&FakeFilterPluginArgs{},
ValidateFakePluginArgs,
SetDefaults_FakePluginArgs,
registry,
)
}
+1 -1
View File
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
@@ -3,7 +3,7 @@ Copyright 2022 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
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.
@@ -17,15 +17,20 @@ import (
"context"
"errors"
"fmt"
"maps"
"slices"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/client-go/informers"
"k8s.io/client-go/tools/cache"
"k8s.io/klog/v2"
evictionutils "sigs.k8s.io/descheduler/pkg/descheduler/evictions/utils"
nodeutil "sigs.k8s.io/descheduler/pkg/descheduler/node"
podutil "sigs.k8s.io/descheduler/pkg/descheduler/pod"
frameworktypes "sigs.k8s.io/descheduler/pkg/framework/types"
@@ -33,8 +38,9 @@ import (
)
const (
PluginName = "DefaultEvictor"
evictPodAnnotationKey = "descheduler.alpha.kubernetes.io/evict"
PluginName = "DefaultEvictor"
evictPodAnnotationKey = "descheduler.alpha.kubernetes.io/evict"
namespaceWithLabelSelector = "namespaceWithLabelSelector-"
)
var _ frameworktypes.EvictorPlugin = &DefaultEvictor{}
@@ -83,9 +89,43 @@ func New(ctx context.Context, args runtime.Object, handle frameworktypes.Handle)
if err != nil {
return nil, err
}
if ev.args.NamespaceLabelSelector != nil && len(ev.args.NamespaceLabelSelector.MatchLabels) > 0 {
selector, nslErr := metav1.LabelSelectorAsSelector(ev.args.NamespaceLabelSelector)
if nslErr != nil {
return nil, fmt.Errorf("unable to convert namespaceLabelSelector to label selector: %w", nslErr)
}
indexName := namespaceWithLabelSelector + ev.handle.PluginInstanceID()
if nslErr := addNamespaceLabelSelectorIndexer(ev.handle.SharedInformerFactory().Core().V1().Namespaces().Informer(), indexName, selector); nslErr != nil {
return nil, fmt.Errorf("failed to add namespace label selector indexer: %w", nslErr)
}
}
return ev, nil
}
func addNamespaceLabelSelectorIndexer(informer cache.SharedIndexInformer, indexName string, selector labels.Selector) error {
indexer := informer.GetIndexer()
for name := range indexer.GetIndexers() {
if name == indexName {
return nil
}
}
return informer.AddIndexers(cache.Indexers{
indexName: func(obj interface{}) ([]string, error) {
ns, ok := obj.(*v1.Namespace)
if !ok {
return []string{}, errors.New("unexpected object")
}
if !selector.Empty() {
if !selector.Matches(labels.Set(ns.Labels)) {
return []string{}, nil
}
}
return []string{ns.GetName()}, nil
},
})
}
func (d *DefaultEvictor) addAllConstraints(logger klog.Logger, handle frameworktypes.Handle) error {
args := d.args
// Determine effective protected policies based on the provided arguments.
@@ -122,13 +162,67 @@ func applyEffectivePodProtections(d *DefaultEvictor, podProtections []PodProtect
applyFailedBarePodsProtection(d, protectionMap)
applyLocalStoragePodsProtection(d, protectionMap)
applyDaemonSetPodsProtection(d, protectionMap)
applyPvcPodsProtection(d, protectionMap)
applyPVCPodsProtection(d, protectionMap)
applyPodsWithoutPDBProtection(d, protectionMap, handle)
applyPodsWithResourceClaimsProtection(d, protectionMap)
return nil
}
// protectedPVCStorageClasses returns the list of storage classes that should
// be protected from eviction. If the list is empty or nil then all storage
// classes are protected (assuming PodsWithPVC protection is enabled).
func protectedPVCStorageClasses(d *DefaultEvictor) []ProtectedStorageClass {
protcfg := d.args.PodProtections.Config
if protcfg == nil {
return nil
}
scconfig := protcfg.PodsWithPVC
if scconfig == nil {
return nil
}
return scconfig.ProtectedStorageClasses
}
// podStorageClasses returns a list of storage classes referred by a pod. We
// need this when assessing if a pod should be protected because it refers to a
// protected storage class.
func podStorageClasses(inf informers.SharedInformerFactory, pod *v1.Pod) ([]string, error) {
lister := inf.Core().V1().PersistentVolumeClaims().Lister().PersistentVolumeClaims(
pod.Namespace,
)
referred := map[string]bool{}
for _, vol := range pod.Spec.Volumes {
if vol.PersistentVolumeClaim == nil {
continue
}
claim, err := lister.Get(vol.PersistentVolumeClaim.ClaimName)
if err != nil {
return nil, fmt.Errorf(
"failed to get persistent volume claim %q/%q: %w",
pod.Namespace, vol.PersistentVolumeClaim.ClaimName, err,
)
}
// this should never happen as once a pvc is created with a nil
// storageClass it is automatically picked up by the default
// storage class. By returning an error here we make the pod
// protected from eviction.
if claim.Spec.StorageClassName == nil || *claim.Spec.StorageClassName == "" {
return nil, fmt.Errorf(
"failed to resolve storage class for pod %q/%q",
pod.Namespace, claim.Name,
)
}
referred[*claim.Spec.StorageClassName] = true
}
return slices.Collect(maps.Keys(referred)), nil
}
func applyFailedBarePodsProtection(d *DefaultEvictor, protectionMap map[PodProtection]bool) {
isProtectionEnabled := protectionMap[FailedBarePods]
if !isProtectionEnabled {
@@ -206,16 +300,50 @@ func applyDaemonSetPodsProtection(d *DefaultEvictor, protectionMap map[PodProtec
}
}
func applyPvcPodsProtection(d *DefaultEvictor, protectionMap map[PodProtection]bool) {
isProtectionEnabled := protectionMap[PodsWithPVC]
if isProtectionEnabled {
d.constraints = append(d.constraints, func(pod *v1.Pod) error {
if utils.IsPodWithPVC(pod) {
return fmt.Errorf("pod with PVC is protected against eviction")
// applyPVCPodsProtection protects pods that refer to a PVC from eviction. If
// the user has specified a list of storage classes to protect then only pods
// referring to PVCs of those storage classes are protected.
func applyPVCPodsProtection(d *DefaultEvictor, enabledProtections map[PodProtection]bool) {
if !enabledProtections[PodsWithPVC] {
return
}
// if the user isn't filtering by storage classes we protect all pods
// referring to a PVC.
protected := protectedPVCStorageClasses(d)
if len(protected) == 0 {
d.constraints = append(
d.constraints,
func(pod *v1.Pod) error {
if utils.IsPodWithPVC(pod) {
return fmt.Errorf("pod with PVC is protected against eviction")
}
return nil
},
)
return
}
protectedsc := map[string]bool{}
for _, class := range protected {
protectedsc[class.Name] = true
}
d.constraints = append(
d.constraints, func(pod *v1.Pod) error {
classes, err := podStorageClasses(d.handle.SharedInformerFactory(), pod)
if err != nil {
return err
}
for _, class := range classes {
if !protectedsc[class] {
continue
}
return fmt.Errorf("pod using protected storage class %q", class)
}
return nil
})
}
},
)
}
func applyPodsWithoutPDBProtection(d *DefaultEvictor, protectionMap map[PodProtection]bool, handle frameworktypes.Handle) {
@@ -317,8 +445,21 @@ func (d *DefaultEvictor) PreEvictionFilter(pod *v1.Pod) bool {
logger.Info("pod does not fit on any other node because of nodeSelector(s), Taint(s), or nodes marked as unschedulable", "pod", klog.KObj(pod))
return false
}
}
if d.args.NamespaceLabelSelector == nil || len(d.args.NamespaceLabelSelector.MatchLabels) == 0 {
return true
}
indexName := namespaceWithLabelSelector + d.handle.PluginInstanceID()
objs, err := d.handle.SharedInformerFactory().Core().V1().Namespaces().Informer().GetIndexer().ByIndex(indexName, pod.Namespace)
if err != nil {
logger.Error(err, "unable to list namespaces for namespaceLabelSelector filter in the policy parameter", "pod", klog.KObj(pod))
return false
}
if len(objs) == 0 {
logger.Info("pod namespace do not match the namespaceLabelSelector filter in the policy parameter", "pod", klog.KObj(pod))
return false
}
return true
}
@@ -383,6 +524,5 @@ func getPodIndexerByOwnerRefs(indexName string, handle frameworktypes.Handle) (c
}); err != nil {
return nil, err
}
return indexer, nil
}
File diff suppressed because it is too large Load Diff
@@ -23,5 +23,4 @@ func addDefaultingFuncs(scheme *runtime.Scheme) error {
// SetDefaults_DefaultEvictorArgs sets the default values for the
// DefaultEvictorArgs configuration.
func SetDefaults_DefaultEvictorArgs(obj runtime.Object) {
}
func SetDefaults_DefaultEvictorArgs(obj runtime.Object) {}
+40 -7
View File
@@ -15,6 +15,7 @@ package defaultevictor
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/descheduler/pkg/api"
)
@@ -25,13 +26,14 @@ import (
type DefaultEvictorArgs struct {
metav1.TypeMeta `json:",inline"`
NodeSelector string `json:"nodeSelector,omitempty"`
LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"`
PriorityThreshold *api.PriorityThreshold `json:"priorityThreshold,omitempty"`
NodeFit bool `json:"nodeFit,omitempty"`
MinReplicas uint `json:"minReplicas,omitempty"`
MinPodAge *metav1.Duration `json:"minPodAge,omitempty"`
NoEvictionPolicy NoEvictionPolicy `json:"noEvictionPolicy,omitempty"`
NodeSelector string `json:"nodeSelector,omitempty"`
LabelSelector *metav1.LabelSelector `json:"labelSelector,omitempty"`
NamespaceLabelSelector *metav1.LabelSelector `json:"namespaceLabelSelector,omitempty"`
PriorityThreshold *api.PriorityThreshold `json:"priorityThreshold,omitempty"`
NodeFit bool `json:"nodeFit,omitempty"`
MinReplicas uint `json:"minReplicas,omitempty"`
MinPodAge *metav1.Duration `json:"minPodAge,omitempty"`
NoEvictionPolicy NoEvictionPolicy `json:"noEvictionPolicy,omitempty"`
// PodProtections holds the list of enabled and disabled protection policies.
// Users can selectively disable certain default protection rules or enable extra ones.
@@ -75,6 +77,37 @@ type PodProtections struct {
// DefaultDisabled specifies which default protection policies should be disabled.
// Supports: PodsWithLocalStorage, DaemonSetPods, SystemCriticalPods, FailedBarePods
DefaultDisabled []PodProtection `json:"defaultDisabled,omitempty"`
// Config holds configuration for pod protection policies. Depending on
// the enabled policies this may be required. For instance, when
// enabling the PodsWithPVC policy the user may specify which storage
// classes should be protected.
Config *PodProtectionsConfig `json:"config,omitempty"`
}
// PodProtectionsConfig holds configuration for pod protection policies. The
// name of the fields here must be equal to a protection name. This struct is
// meant to be extended as more protection policies are added.
// +k8s:deepcopy-gen=true
type PodProtectionsConfig struct {
PodsWithPVC *PodsWithPVCConfig `json:"PodsWithPVC,omitempty"`
}
// PodsWithPVCConfig holds configuration for the PodsWithPVC protection.
// +k8s:deepcopy-gen=true
type PodsWithPVCConfig struct {
// ProtectedStorageClasses is a list of storage classes that we want to
// protect. i.e. if a pod refers to one of these storage classes it is
// protected from being evicted. If none is provided then all pods with
// PVCs are protected from eviction.
ProtectedStorageClasses []ProtectedStorageClass `json:"protectedStorageClasses,omitempty"`
}
// ProtectedStorageClass is used to determine what storage classes are
// protected when the PodsWithPVC protection is enabled. This object exists
// so we can later on extend it with more configuration if needed.
type ProtectedStorageClass struct {
Name string `json:"name"`
}
// defaultPodProtections holds the list of protection policies that are enabled by default.
@@ -72,6 +72,17 @@ func ValidateDefaultEvictorArgs(obj runtime.Object) error {
if hasDuplicates(args.PodProtections.ExtraEnabled) {
allErrs = append(allErrs, fmt.Errorf("PodProtections.ExtraEnabled contains duplicate entries"))
}
if slices.Contains(args.PodProtections.ExtraEnabled, PodsWithPVC) {
if args.PodProtections.Config != nil && args.PodProtections.Config.PodsWithPVC != nil {
protectedsc := args.PodProtections.Config.PodsWithPVC.ProtectedStorageClasses
for i, sc := range protectedsc {
if sc.Name == "" {
allErrs = append(allErrs, fmt.Errorf("PodProtections.Config.PodsWithPVC.ProtectedStorageClasses[%d] name cannot be empty", i))
}
}
}
}
}
return utilerrors.NewAggregate(allErrs)
@@ -198,6 +198,33 @@ func TestValidateDefaultEvictorArgs(t *testing.T) {
},
errInfo: fmt.Errorf(`[noEvictionPolicy accepts only ["Preferred" "Mandatory"] values, invalid pod protection policy in DefaultDisabled: "PodsWithoutPDB". Valid options are: [PodsWithLocalStorage SystemCriticalPods FailedBarePods DaemonSetPods], PodProtections.DefaultDisabled contains duplicate entries, PodProtections.ExtraEnabled contains duplicate entries]`),
},
{
name: "Protected storage classes without storage class name",
args: &DefaultEvictorArgs{
PodProtections: PodProtections{
ExtraEnabled: []PodProtection{PodsWithPVC},
Config: &PodProtectionsConfig{
PodsWithPVC: &PodsWithPVCConfig{
ProtectedStorageClasses: []ProtectedStorageClass{
{
Name: "",
},
{
Name: "protected-storage-class-0",
},
{
Name: "",
},
{
Name: "protected-storage-class-1",
},
},
},
},
},
},
errInfo: fmt.Errorf(`[PodProtections.Config.PodsWithPVC.ProtectedStorageClasses[0] name cannot be empty, PodProtections.Config.PodsWithPVC.ProtectedStorageClasses[2] name cannot be empty]`),
},
}
for _, testCase := range tests {
+53 -1
View File
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
@@ -36,6 +36,11 @@ func (in *DefaultEvictorArgs) DeepCopyInto(out *DefaultEvictorArgs) {
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
if in.NamespaceLabelSelector != nil {
in, out := &in.NamespaceLabelSelector, &out.NamespaceLabelSelector
*out = new(v1.LabelSelector)
(*in).DeepCopyInto(*out)
}
if in.PriorityThreshold != nil {
in, out := &in.PriorityThreshold, &out.PriorityThreshold
*out = new(api.PriorityThreshold)
@@ -81,6 +86,11 @@ func (in *PodProtections) DeepCopyInto(out *PodProtections) {
*out = make([]PodProtection, len(*in))
copy(*out, *in)
}
if in.Config != nil {
in, out := &in.Config, &out.Config
*out = new(PodProtectionsConfig)
(*in).DeepCopyInto(*out)
}
return
}
@@ -93,3 +103,45 @@ func (in *PodProtections) DeepCopy() *PodProtections {
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PodProtectionsConfig) DeepCopyInto(out *PodProtectionsConfig) {
*out = *in
if in.PodsWithPVC != nil {
in, out := &in.PodsWithPVC, &out.PodsWithPVC
*out = new(PodsWithPVCConfig)
(*in).DeepCopyInto(*out)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodProtectionsConfig.
func (in *PodProtectionsConfig) DeepCopy() *PodProtectionsConfig {
if in == nil {
return nil
}
out := new(PodProtectionsConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PodsWithPVCConfig) DeepCopyInto(out *PodsWithPVCConfig) {
*out = *in
if in.ProtectedStorageClasses != nil {
in, out := &in.ProtectedStorageClasses, &out.ProtectedStorageClasses
*out = make([]ProtectedStorageClass, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PodsWithPVCConfig.
func (in *PodsWithPVCConfig) DeepCopy() *PodsWithPVCConfig {
if in == nil {
return nil
}
out := new(PodsWithPVCConfig)
in.DeepCopyInto(out)
return out
}
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
+1 -1
View File
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
+1 -1
View File
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
@@ -23,7 +23,6 @@ import (
v1 "k8s.io/api/core/v1"
policy "k8s.io/api/policy/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/fake"
core "k8s.io/client-go/testing"
@@ -97,17 +96,7 @@ func TestHighNodeUtilization(t *testing.T) {
test.BuildTestPod("p1", 400, 0, n1NodeName, func(pod *v1.Pod) {
// A pod with local storage.
test.SetNormalOwnerRef(pod)
pod.Spec.Volumes = []v1.Volume{
{
Name: "sample",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: "somePath"},
EmptyDir: &v1.EmptyDirVolumeSource{
SizeLimit: resource.NewQuantity(int64(10), resource.BinarySI),
},
},
},
}
test.SetHostPathEmptyDirVolumeSource(pod)
// A Mirror Pod.
pod.Annotations = test.GetMirrorPodAnnotation()
}),
@@ -115,8 +104,7 @@ func TestHighNodeUtilization(t *testing.T) {
// A Critical Pod.
test.SetNormalOwnerRef(pod)
pod.Namespace = "kube-system"
priority := utils.SystemCriticalPriority
pod.Spec.Priority = &priority
test.SetPodPriority(pod, utils.SystemCriticalPriority)
}),
// These won't be evicted.
test.BuildTestPod("p3", 400, 0, n2NodeName, test.SetDSOwnerRef),
@@ -168,8 +156,7 @@ func TestHighNodeUtilization(t *testing.T) {
// A Critical Pod.
test.SetNormalOwnerRef(pod)
pod.Namespace = "kube-system"
priority := utils.SystemCriticalPriority
pod.Spec.Priority = &priority
test.SetPodPriority(pod, utils.SystemCriticalPriority)
}),
// These won't be evicted.
test.BuildTestPod("p3", 400, 0, n2NodeName, test.SetRSOwnerRef),
@@ -249,9 +236,7 @@ func TestHighNodeUtilization(t *testing.T) {
test.SetRSOwnerRef(pod)
test.MakeBestEffortPod(pod)
}),
test.BuildTestPod("p2", 400, 0, n1NodeName, func(pod *v1.Pod) {
test.SetRSOwnerRef(pod)
}),
test.BuildTestPod("p2", 400, 0, n1NodeName, test.SetRSOwnerRef),
// These won't be evicted.
test.BuildTestPod("p3", 400, 0, n2NodeName, test.SetRSOwnerRef),
test.BuildTestPod("p4", 400, 0, n2NodeName, test.SetRSOwnerRef),
@@ -466,9 +451,7 @@ func TestHighNodeUtilization(t *testing.T) {
// pods in the other nodes must not be evicted
// because they do not have the extended
// resource defined in their requests.
test.BuildTestPod("p3", 500, 0, n2NodeName, func(pod *v1.Pod) {
test.SetRSOwnerRef(pod)
}),
test.BuildTestPod("p3", 500, 0, n2NodeName, test.SetRSOwnerRef),
test.BuildTestPod("p4", 500, 0, n2NodeName, func(pod *v1.Pod) {
test.SetRSOwnerRef(pod)
}),
@@ -23,7 +23,6 @@ import (
v1 "k8s.io/api/core/v1"
policy "k8s.io/api/policy/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/informers"
@@ -92,25 +91,14 @@ func TestLowNodeUtilization(t *testing.T) {
test.BuildTestPod("p5", 400, 0, n1NodeName, func(pod *v1.Pod) {
// A pod with local storage.
test.SetNormalOwnerRef(pod)
pod.Spec.Volumes = []v1.Volume{
{
Name: "sample",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: "somePath"},
EmptyDir: &v1.EmptyDirVolumeSource{
SizeLimit: resource.NewQuantity(int64(10), resource.BinarySI),
},
},
},
}
test.SetHostPathEmptyDirVolumeSource(pod)
// A Mirror Pod.
pod.Annotations = test.GetMirrorPodAnnotation()
}),
test.BuildTestPod("p6", 400, 0, n1NodeName, func(pod *v1.Pod) {
// A Critical Pod.
pod.Namespace = "kube-system"
priority := utils.SystemCriticalPriority
pod.Spec.Priority = &priority
test.SetPodPriority(pod, utils.SystemCriticalPriority)
}),
test.BuildTestPod("p9", 400, 0, n2NodeName, test.SetRSOwnerRef),
},
@@ -155,25 +143,14 @@ func TestLowNodeUtilization(t *testing.T) {
test.BuildTestPod("p7", 400, 0, n1NodeName, func(pod *v1.Pod) {
// A pod with local storage.
test.SetNormalOwnerRef(pod)
pod.Spec.Volumes = []v1.Volume{
{
Name: "sample",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: "somePath"},
EmptyDir: &v1.EmptyDirVolumeSource{
SizeLimit: resource.NewQuantity(int64(10), resource.BinarySI),
},
},
},
}
test.SetHostPathEmptyDirVolumeSource(pod)
// A Mirror Pod.
pod.Annotations = test.GetMirrorPodAnnotation()
}),
test.BuildTestPod("p8", 400, 0, n1NodeName, func(pod *v1.Pod) {
// A Critical Pod.
pod.Namespace = "kube-system"
priority := utils.SystemCriticalPriority
pod.Spec.Priority = &priority
test.SetPodPriority(pod, utils.SystemCriticalPriority)
}),
test.BuildTestPod("p9", 400, 0, n2NodeName, test.SetRSOwnerRef),
},
@@ -233,25 +210,14 @@ func TestLowNodeUtilization(t *testing.T) {
test.BuildTestPod("p7", 400, 0, n1NodeName, func(pod *v1.Pod) {
// A pod with local storage.
test.SetNormalOwnerRef(pod)
pod.Spec.Volumes = []v1.Volume{
{
Name: "sample",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: "somePath"},
EmptyDir: &v1.EmptyDirVolumeSource{
SizeLimit: resource.NewQuantity(int64(10), resource.BinarySI),
},
},
},
}
test.SetHostPathEmptyDirVolumeSource(pod)
// A Mirror Pod.
pod.Annotations = test.GetMirrorPodAnnotation()
}),
test.BuildTestPod("p8", 400, 0, n1NodeName, func(pod *v1.Pod) {
// A Critical Pod.
pod.Namespace = "kube-system"
priority := utils.SystemCriticalPriority
pod.Spec.Priority = &priority
test.SetPodPriority(pod, utils.SystemCriticalPriority)
}),
test.BuildTestPod("p9", 400, 0, n2NodeName, test.SetRSOwnerRef),
},
@@ -310,17 +276,7 @@ func TestLowNodeUtilization(t *testing.T) {
test.BuildTestPod("p7", 400, 0, n1NodeName, func(pod *v1.Pod) {
// A pod with local storage.
test.SetNormalOwnerRef(pod)
pod.Spec.Volumes = []v1.Volume{
{
Name: "sample",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: "somePath"},
EmptyDir: &v1.EmptyDirVolumeSource{
SizeLimit: resource.NewQuantity(int64(10), resource.BinarySI),
},
},
},
}
test.SetHostPathEmptyDirVolumeSource(pod)
// A Mirror Pod.
pod.Annotations = test.GetMirrorPodAnnotation()
}),
@@ -328,8 +284,7 @@ func TestLowNodeUtilization(t *testing.T) {
// A Critical Pod.
test.SetNormalOwnerRef(pod)
pod.Namespace = "kube-system"
priority := utils.SystemCriticalPriority
pod.Spec.Priority = &priority
test.SetPodPriority(pod, utils.SystemCriticalPriority)
}),
test.BuildTestPod("p9", 400, 0, n2NodeName, test.SetRSOwnerRef),
},
@@ -379,17 +334,7 @@ func TestLowNodeUtilization(t *testing.T) {
test.BuildTestPod("p7", 400, 0, n1NodeName, func(pod *v1.Pod) {
// A pod with local storage.
test.SetNormalOwnerRef(pod)
pod.Spec.Volumes = []v1.Volume{
{
Name: "sample",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: "somePath"},
EmptyDir: &v1.EmptyDirVolumeSource{
SizeLimit: resource.NewQuantity(int64(10), resource.BinarySI),
},
},
},
}
test.SetHostPathEmptyDirVolumeSource(pod)
// A Mirror Pod.
pod.Annotations = test.GetMirrorPodAnnotation()
}),
@@ -397,8 +342,7 @@ func TestLowNodeUtilization(t *testing.T) {
// A Critical Pod.
test.SetNormalOwnerRef(pod)
pod.Namespace = "kube-system"
priority := utils.SystemCriticalPriority
pod.Spec.Priority = &priority
test.SetPodPriority(pod, utils.SystemCriticalPriority)
}),
test.BuildTestPod("p9", 400, 0, n2NodeName, test.SetRSOwnerRef),
},
@@ -462,17 +406,7 @@ func TestLowNodeUtilization(t *testing.T) {
// A pod with local storage.
test.SetNormalOwnerRef(pod)
test.SetPodPriority(pod, lowPriority)
pod.Spec.Volumes = []v1.Volume{
{
Name: "sample",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: "somePath"},
EmptyDir: &v1.EmptyDirVolumeSource{
SizeLimit: resource.NewQuantity(int64(10), resource.BinarySI),
},
},
},
}
test.SetHostPathEmptyDirVolumeSource(pod)
// A Mirror Pod.
pod.Annotations = test.GetMirrorPodAnnotation()
}),
@@ -480,8 +414,7 @@ func TestLowNodeUtilization(t *testing.T) {
// A Critical Pod.
test.SetNormalOwnerRef(pod)
pod.Namespace = "kube-system"
priority := utils.SystemCriticalPriority
pod.Spec.Priority = &priority
test.SetPodPriority(pod, utils.SystemCriticalPriority)
}),
test.BuildTestPod("p9", 400, 0, n2NodeName, test.SetRSOwnerRef),
},
@@ -525,9 +458,7 @@ func TestLowNodeUtilization(t *testing.T) {
test.SetRSOwnerRef(pod)
test.MakeBestEffortPod(pod)
}),
test.BuildTestPod("p3", 400, 0, n1NodeName, func(pod *v1.Pod) {
test.SetRSOwnerRef(pod)
}),
test.BuildTestPod("p3", 400, 0, n1NodeName, test.SetRSOwnerRef),
test.BuildTestPod("p4", 400, 0, n1NodeName, func(pod *v1.Pod) {
test.SetRSOwnerRef(pod)
test.MakeBestEffortPod(pod)
@@ -537,23 +468,11 @@ func TestLowNodeUtilization(t *testing.T) {
test.MakeBestEffortPod(pod)
}),
// These won't be evicted.
test.BuildTestPod("p6", 400, 0, n1NodeName, func(pod *v1.Pod) {
test.SetDSOwnerRef(pod)
}),
test.BuildTestPod("p6", 400, 0, n1NodeName, test.SetDSOwnerRef),
test.BuildTestPod("p7", 400, 0, n1NodeName, func(pod *v1.Pod) {
// A pod with local storage.
test.SetNormalOwnerRef(pod)
pod.Spec.Volumes = []v1.Volume{
{
Name: "sample",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: "somePath"},
EmptyDir: &v1.EmptyDirVolumeSource{
SizeLimit: resource.NewQuantity(int64(10), resource.BinarySI),
},
},
},
}
test.SetHostPathEmptyDirVolumeSource(pod)
// A Mirror Pod.
pod.Annotations = test.GetMirrorPodAnnotation()
}),
@@ -561,8 +480,7 @@ func TestLowNodeUtilization(t *testing.T) {
// A Critical Pod.
test.SetNormalOwnerRef(pod)
pod.Namespace = "kube-system"
priority := utils.SystemCriticalPriority
pod.Spec.Priority = &priority
test.SetPodPriority(pod, utils.SystemCriticalPriority)
}),
test.BuildTestPod("p9", 400, 0, n2NodeName, test.SetRSOwnerRef),
},
@@ -632,17 +550,7 @@ func TestLowNodeUtilization(t *testing.T) {
// A pod with local storage.
test.SetNormalOwnerRef(pod)
test.SetPodExtendedResourceRequest(pod, extendedResource, 1)
pod.Spec.Volumes = []v1.Volume{
{
Name: "sample",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: "somePath"},
EmptyDir: &v1.EmptyDirVolumeSource{
SizeLimit: resource.NewQuantity(int64(10), resource.BinarySI),
},
},
},
}
test.SetHostPathEmptyDirVolumeSource(pod)
// A Mirror Pod.
pod.Annotations = test.GetMirrorPodAnnotation()
}),
@@ -651,8 +559,7 @@ func TestLowNodeUtilization(t *testing.T) {
test.SetNormalOwnerRef(pod)
test.SetPodExtendedResourceRequest(pod, extendedResource, 1)
pod.Namespace = "kube-system"
priority := utils.SystemCriticalPriority
pod.Spec.Priority = &priority
test.SetPodPriority(pod, utils.SystemCriticalPriority)
}),
test.BuildTestPod("p9", 0, 0, n2NodeName, func(pod *v1.Pod) {
test.SetRSOwnerRef(pod)
@@ -747,12 +654,8 @@ func TestLowNodeUtilization(t *testing.T) {
test.SetRSOwnerRef(pod)
test.SetPodExtendedResourceRequest(pod, extendedResource, 7)
}),
test.BuildTestPod("p3", 0, 0, n2NodeName, func(pod *v1.Pod) {
test.SetRSOwnerRef(pod)
}),
test.BuildTestPod("p8", 0, 0, n3NodeName, func(pod *v1.Pod) {
test.SetRSOwnerRef(pod)
}),
test.BuildTestPod("p3", 0, 0, n2NodeName, test.SetRSOwnerRef),
test.BuildTestPod("p8", 0, 0, n3NodeName, test.SetRSOwnerRef),
test.BuildTestPod("p9", 0, 0, n3NodeName, test.SetRSOwnerRef),
},
nodemetricses: []*v1beta1.NodeMetrics{
@@ -795,17 +698,7 @@ func TestLowNodeUtilization(t *testing.T) {
test.BuildTestPod("p7", 400, 0, n1NodeName, func(pod *v1.Pod) {
// A pod with local storage.
test.SetNormalOwnerRef(pod)
pod.Spec.Volumes = []v1.Volume{
{
Name: "sample",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: "somePath"},
EmptyDir: &v1.EmptyDirVolumeSource{
SizeLimit: resource.NewQuantity(int64(10), resource.BinarySI),
},
},
},
}
test.SetHostPathEmptyDirVolumeSource(pod)
// A Mirror Pod.
pod.Annotations = test.GetMirrorPodAnnotation()
}),
@@ -813,8 +706,7 @@ func TestLowNodeUtilization(t *testing.T) {
// A Critical Pod.
test.SetNormalOwnerRef(pod)
pod.Namespace = "kube-system"
priority := utils.SystemCriticalPriority
pod.Spec.Priority = &priority
test.SetPodPriority(pod, utils.SystemCriticalPriority)
}),
},
nodemetricses: []*v1beta1.NodeMetrics{
@@ -872,17 +764,7 @@ func TestLowNodeUtilization(t *testing.T) {
test.BuildTestPod("p7", 400, 0, n1NodeName, func(pod *v1.Pod) {
// A pod with local storage.
test.SetNormalOwnerRef(pod)
pod.Spec.Volumes = []v1.Volume{
{
Name: "sample",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: "somePath"},
EmptyDir: &v1.EmptyDirVolumeSource{
SizeLimit: resource.NewQuantity(int64(10), resource.BinarySI),
},
},
},
}
test.SetHostPathEmptyDirVolumeSource(pod)
// A Mirror Pod.
pod.Annotations = test.GetMirrorPodAnnotation()
}),
@@ -890,8 +772,7 @@ func TestLowNodeUtilization(t *testing.T) {
// A Critical Pod.
test.SetNormalOwnerRef(pod)
pod.Namespace = "kube-system"
priority := utils.SystemCriticalPriority
pod.Spec.Priority = &priority
test.SetPodPriority(pod, utils.SystemCriticalPriority)
}),
},
nodemetricses: []*v1beta1.NodeMetrics{
@@ -975,17 +856,7 @@ func TestLowNodeUtilization(t *testing.T) {
test.BuildTestPod("p7", 400, 0, n1NodeName, func(pod *v1.Pod) {
// A pod with local storage.
test.SetNormalOwnerRef(pod)
pod.Spec.Volumes = []v1.Volume{
{
Name: "sample",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: "somePath"},
EmptyDir: &v1.EmptyDirVolumeSource{
SizeLimit: resource.NewQuantity(int64(10), resource.BinarySI),
},
},
},
}
test.SetHostPathEmptyDirVolumeSource(pod)
// A Mirror Pod.
pod.Annotations = test.GetMirrorPodAnnotation()
}),
@@ -993,8 +864,7 @@ func TestLowNodeUtilization(t *testing.T) {
// A Critical Pod.
test.SetNormalOwnerRef(pod)
pod.Namespace = "kube-system"
priority := utils.SystemCriticalPriority
pod.Spec.Priority = &priority
test.SetPodPriority(pod, utils.SystemCriticalPriority)
}),
test.BuildTestPod("p9", 0, 0, n2NodeName, test.SetRSOwnerRef),
},
@@ -1037,17 +907,7 @@ func TestLowNodeUtilization(t *testing.T) {
test.BuildTestPod("p7", 400, 0, n1NodeName, func(pod *v1.Pod) {
// A pod with local storage.
test.SetNormalOwnerRef(pod)
pod.Spec.Volumes = []v1.Volume{
{
Name: "sample",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: "somePath"},
EmptyDir: &v1.EmptyDirVolumeSource{
SizeLimit: resource.NewQuantity(int64(10), resource.BinarySI),
},
},
},
}
test.SetHostPathEmptyDirVolumeSource(pod)
// A Mirror Pod.
pod.Annotations = test.GetMirrorPodAnnotation()
}),
@@ -1055,8 +915,7 @@ func TestLowNodeUtilization(t *testing.T) {
// A Critical Pod.
test.SetNormalOwnerRef(pod)
pod.Namespace = "kube-system"
priority := utils.SystemCriticalPriority
pod.Spec.Priority = &priority
test.SetPodPriority(pod, utils.SystemCriticalPriority)
}),
test.BuildTestPod("p9", 400, 0, n2NodeName, test.SetRSOwnerRef),
},
@@ -1106,17 +965,7 @@ func TestLowNodeUtilization(t *testing.T) {
test.BuildTestPod("p7", 400, 375, n1NodeName, func(pod *v1.Pod) {
// A pod with local storage.
test.SetNormalOwnerRef(pod)
pod.Spec.Volumes = []v1.Volume{
{
Name: "sample",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: "somePath"},
EmptyDir: &v1.EmptyDirVolumeSource{
SizeLimit: resource.NewQuantity(int64(10), resource.BinarySI),
},
},
},
}
test.SetHostPathEmptyDirVolumeSource(pod)
// A Mirror Pod.
pod.Annotations = test.GetMirrorPodAnnotation()
}),
@@ -1124,8 +973,7 @@ func TestLowNodeUtilization(t *testing.T) {
// A Critical Pod.
test.SetNormalOwnerRef(pod)
pod.Namespace = "kube-system"
priority := utils.SystemCriticalPriority
pod.Spec.Priority = &priority
test.SetPodPriority(pod, utils.SystemCriticalPriority)
}),
test.BuildTestPod("p9", 400, 3000, n2NodeName, test.SetRSOwnerRef),
},
@@ -1218,17 +1066,7 @@ func TestLowNodeUtilization(t *testing.T) {
test.BuildTestPod("p7", 400, 0, n1NodeName, func(pod *v1.Pod) {
// A pod with local storage.
test.SetNormalOwnerRef(pod)
pod.Spec.Volumes = []v1.Volume{
{
Name: "sample",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: "somePath"},
EmptyDir: &v1.EmptyDirVolumeSource{
SizeLimit: resource.NewQuantity(int64(10), resource.BinarySI),
},
},
},
}
test.SetHostPathEmptyDirVolumeSource(pod)
// A Mirror Pod.
pod.Annotations = test.GetMirrorPodAnnotation()
}),
@@ -1236,8 +1074,7 @@ func TestLowNodeUtilization(t *testing.T) {
// A Critical Pod.
test.SetNormalOwnerRef(pod)
pod.Namespace = "kube-system"
priority := utils.SystemCriticalPriority
pod.Spec.Priority = &priority
test.SetPodPriority(pod, utils.SystemCriticalPriority)
}),
test.BuildTestPod("p9", 0, 0, n2NodeName, test.SetRSOwnerRef),
},
@@ -1282,25 +1119,14 @@ func TestLowNodeUtilization(t *testing.T) {
test.BuildTestPod("p7", 400, 0, n1NodeName, func(pod *v1.Pod) {
// A pod with local storage.
test.SetNormalOwnerRef(pod)
pod.Spec.Volumes = []v1.Volume{
{
Name: "sample",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: "somePath"},
EmptyDir: &v1.EmptyDirVolumeSource{
SizeLimit: resource.NewQuantity(int64(10), resource.BinarySI),
},
},
},
}
test.SetHostPathEmptyDirVolumeSource(pod)
// A Mirror Pod.
pod.Annotations = test.GetMirrorPodAnnotation()
}),
test.BuildTestPod("p8", 400, 0, n1NodeName, func(pod *v1.Pod) {
// A Critical Pod.
pod.Namespace = "kube-system"
priority := utils.SystemCriticalPriority
pod.Spec.Priority = &priority
test.SetPodPriority(pod, utils.SystemCriticalPriority)
}),
test.BuildTestPod("p9", 400, 0, n2NodeName, test.SetRSOwnerRef),
},
@@ -1575,17 +1401,7 @@ func TestLowNodeUtilizationWithTaints(t *testing.T) {
func withLocalStorage(pod *v1.Pod) {
// A pod with local storage.
test.SetNormalOwnerRef(pod)
pod.Spec.Volumes = []v1.Volume{
{
Name: "sample",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: "somePath"},
EmptyDir: &v1.EmptyDirVolumeSource{
SizeLimit: resource.NewQuantity(int64(10), resource.BinarySI),
},
},
},
}
test.SetHostPathEmptyDirVolumeSource(pod)
// A Mirror Pod.
pod.Annotations = test.GetMirrorPodAnnotation()
}
@@ -1594,8 +1410,7 @@ func withCriticalPod(pod *v1.Pod) {
// A Critical Pod.
test.SetNormalOwnerRef(pod)
pod.Namespace = "kube-system"
priority := utils.SystemCriticalPriority
pod.Spec.Priority = &priority
test.SetPodPriority(pod, utils.SystemCriticalPriority)
}
func TestLowNodeUtilizationWithPrometheusMetrics(t *testing.T) {
@@ -32,7 +32,6 @@ import (
"k8s.io/utils/ptr"
"sigs.k8s.io/descheduler/pkg/descheduler/evictions"
nodeutil "sigs.k8s.io/descheduler/pkg/descheduler/node"
"sigs.k8s.io/descheduler/pkg/descheduler/pod"
podutil "sigs.k8s.io/descheduler/pkg/descheduler/pod"
"sigs.k8s.io/descheduler/pkg/framework/plugins/nodeutilization/normalizer"
frameworktypes "sigs.k8s.io/descheduler/pkg/framework/types"
@@ -283,7 +282,7 @@ func evictPods(
break
}
if !utils.PodToleratesTaints(pod, destinationTaints) {
if !utils.PodToleratesTaints(ctx, pod, destinationTaints) {
logger.V(3).Info(
"Skipping eviction for pod, doesn't tolerate node taint",
"pod", klog.KObj(pod),
@@ -757,7 +756,7 @@ func assessAvailableResourceInNodes(
// withResourceRequestForAny returns a filter function that checks if a pod
// has a resource request specified for any of the given resources names.
func withResourceRequestForAny(names ...v1.ResourceName) pod.FilterFunc {
func withResourceRequestForAny(names ...v1.ResourceName) podutil.FilterFunc {
return func(pod *v1.Pod) bool {
all := append(pod.Spec.Containers, pod.Spec.InitContainers...)
for _, name := range names {
@@ -281,10 +281,10 @@ func NodeUsageFromPrometheusMetrics(ctx context.Context, promClient promapi.Clie
for _, sample := range results.(model.Vector) {
nodeName, exists := sample.Metric["instance"]
if !exists {
return nil, fmt.Errorf("The collected metrics sample is missing 'instance' key")
return nil, fmt.Errorf("the collected metrics sample is missing 'instance' key")
}
if sample.Value < 0 || sample.Value > 1 {
return nil, fmt.Errorf("The collected metrics sample for %q has value %v outside of <0; 1> interval", string(nodeName), sample.Value)
return nil, fmt.Errorf("the collected metrics sample for %q has value %v outside of <0; 1> interval", string(nodeName), sample.Value)
}
nodeUsages[string(nodeName)] = map[v1.ResourceName]*resource.Quantity{
MetricResource: resource.NewQuantity(int64(sample.Value*100), resource.DecimalSI),
@@ -230,7 +230,7 @@ func TestPrometheusUsageClient(t *testing.T) {
Timestamp: 1728991761711,
},
},
err: fmt.Errorf("The collected metrics sample is missing 'instance' key"),
err: fmt.Errorf("the collected metrics sample is missing 'instance' key"),
},
{
name: "invalid data value out of range",
@@ -238,7 +238,7 @@ func TestPrometheusUsageClient(t *testing.T) {
result: model.Vector{
sample("instance:node_cpu:rate:sum", "ip-10-0-51-101.ec2.internal", 1.20381818181818104),
},
err: fmt.Errorf("The collected metrics sample for \"ip-10-0-51-101.ec2.internal\" has value 1.203818181818181 outside of <0; 1> interval"),
err: fmt.Errorf("the collected metrics sample for \"ip-10-0-51-101.ec2.internal\" has value 1.203818181818181 outside of <0; 1> interval"),
},
{
name: "invalid data not a vector",
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
+155
View File
@@ -0,0 +1,155 @@
# PodLifeTime Plugin
## What It Does
The PodLifeTime plugin evicts pods that have been running for too long. You can configure a maximum age threshold, and the plugin evicts pods older than that threshold. The oldest pods are evicted first.
## How It Works
The plugin examines all pods across your nodes and selects those that exceed the configured age threshold. You can further narrow down which pods are considered by specifying:
- Which namespaces to include or exclude
- Which labels pods must have
- Which states pods must be in (e.g., Running, Pending, CrashLoopBackOff)
Once pods are selected, they are sorted by age (oldest first) and evicted in that order. Eviction stops when limits are reached (per-node limits, total limits, or Pod Disruption Budget constraints).
## Use Cases
- **Resource Leakage Mitigation**: Restart long-running pods that may have accumulated memory leaks, stale cache, or resource leaks
```yaml
args:
maxPodLifeTimeSeconds: 604800 # 7 days
states: [Running]
```
- **Ephemeral Workload Cleanup**: Remove long-running batch jobs, test pods, or temporary workloads that have exceeded their expected lifetime
```yaml
args:
maxPodLifeTimeSeconds: 7200 # 2 hours
states: [Succeeded, Failed]
```
- **Node Hygiene**: Remove forgotten or stuck pods that are consuming resources but not making progress
```yaml
args:
maxPodLifeTimeSeconds: 3600 # 1 hour
states: [CrashLoopBackOff, ImagePullBackOff, ErrImagePull]
includingInitContainers: true
```
- **Config/Secret Update Pickup**: Force pod restart to pick up updated ConfigMaps, Secrets, or environment variables
```yaml
args:
maxPodLifeTimeSeconds: 86400 # 1 day
states: [Running]
labelSelector:
matchLabels:
config-refresh: enabled
```
- **Security Rotation**: Periodically refresh pods to pick up new security tokens, certificates, or patched container images
```yaml
args:
maxPodLifeTimeSeconds: 259200 # 3 days
states: [Running]
namespaces:
exclude: [kube-system]
```
- **Dev/Test Environment Cleanup**: Automatically clean up old pods in development or staging namespaces
```yaml
args:
maxPodLifeTimeSeconds: 86400 # 1 day
namespaces:
include: [dev, staging, test]
```
- **Cluster Health Freshness**: Ensure pods periodically restart to maintain cluster health and verify workloads can recover from restarts
```yaml
args:
maxPodLifeTimeSeconds: 604800 # 7 days
states: [Running]
namespaces:
exclude: [kube-system, production]
```
- **Rebalancing Assistance**: Work alongside other descheduler strategies by removing old pods to allow better pod distribution
```yaml
args:
maxPodLifeTimeSeconds: 1209600 # 14 days
states: [Running]
```
- **Non-Critical Stateful Refresh**: Occasionally reset tolerable stateful workloads that can handle data loss or have external backup mechanisms
```yaml
args:
maxPodLifeTimeSeconds: 2592000 # 30 days
labelSelector:
matchLabels:
stateful-tier: cache
```
## Configuration
| Parameter | Description | Type | Required | Default |
|-----------|-------------|------|----------|---------|
| `maxPodLifeTimeSeconds` | Pods older than this many seconds are evicted | `uint` | Yes | - |
| `namespaces` | Limit eviction to specific namespaces (or exclude specific namespaces) | `Namespaces` | No | `nil` |
| `labelSelector` | Only evict pods matching these labels | `metav1.LabelSelector` | No | `nil` |
| `states` | Only evict pods in specific states (e.g., Running, CrashLoopBackOff) | `[]string` | No | `nil` |
| `includingInitContainers` | When checking states, also check init container states | `bool` | No | `false` |
| `includingEphemeralContainers` | When checking states, also check ephemeral container states | `bool` | No | `false` |
### Discovering states
Each pod is checked for the following locations to discover its relevant state:
1. **Pod Phase** - The overall pod lifecycle phase:
- `Running` - Pod is running on a node
- `Pending` - Pod has been accepted but containers are not yet running
- `Succeeded` - All containers terminated successfully
- `Failed` - All containers terminated, at least one failed
- `Unknown` - Pod state cannot be determined
2. **Pod Status Reason** - Why the pod is in its current state:
- `NodeAffinity` - Pod cannot be scheduled due to node affinity rules
- `NodeLost` - Node hosting the pod is lost
- `Shutdown` - Pod terminated due to node shutdown
- `UnexpectedAdmissionError` - Pod admission failed unexpectedly
3. **Container Waiting Reason** - Why containers are waiting to start:
- `PodInitializing` - Pod is still initializing
- `ContainerCreating` - Container is being created
- `ImagePullBackOff` - Image pull is failing and backing off
- `CrashLoopBackOff` - Container is crashing repeatedly
- `CreateContainerConfigError` - Container configuration is invalid
- `ErrImagePull` - Image cannot be pulled
- `CreateContainerError` - Container creation failed
- `InvalidImageName` - Image name is invalid
By default, only regular containers are checked. Enable `includingInitContainers` or `includingEphemeralContainers` to also check those container types.
## Example
```yaml
apiVersion: descheduler/v1alpha2
kind: DeschedulerPolicy
profiles:
- name: default
plugins:
deschedule:
enabled:
- name: PodLifeTime
pluginConfig:
- name: PodLifeTime
args:
maxPodLifeTimeSeconds: 86400 # 1 day
namespaces:
include:
- default
states:
- Running
```
This configuration evicts Running pods in the `default` namespace that are older than 1 day.
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
+1 -1
View File
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
@@ -197,7 +197,7 @@ func (r *RemoveDuplicates) Balance(ctx context.Context, nodes []*v1.Node) *frame
// 1. how many pods can be evicted to respect uniform placement of pods among viable nodes?
for ownerKey, podNodes := range duplicatePods {
targetNodes := getTargetNodes(podNodes, nodes)
targetNodes := getTargetNodes(ctx, podNodes, nodes)
logger.V(2).Info("Adjusting feasible nodes", "owner", ownerKey, "from", nodeCount, "to", len(targetNodes))
if len(targetNodes) < 2 {
@@ -233,7 +233,7 @@ func (r *RemoveDuplicates) Balance(ctx context.Context, nodes []*v1.Node) *frame
return nil
}
func getTargetNodes(podNodes map[string][]*v1.Pod, nodes []*v1.Node) []*v1.Node {
func getTargetNodes(ctx context.Context, podNodes map[string][]*v1.Pod, nodes []*v1.Node) []*v1.Node {
// In order to reduce the number of pods processed, identify pods which have
// equal (tolerations, nodeselectors, node affinity) terms and considered them
// as identical. Identical pods wrt. (tolerations, nodeselectors, node affinity) terms
@@ -265,7 +265,7 @@ func getTargetNodes(podNodes map[string][]*v1.Pod, nodes []*v1.Node) []*v1.Node
for pod := range distinctPods {
matchingNodes := map[string]*v1.Node{}
for _, node := range nodes {
if !utils.TolerationsTolerateTaintsWithFilter(pod.Spec.Tolerations, node.Spec.Taints, func(taint *v1.Taint) bool {
if !utils.TolerationsTolerateTaintsWithFilter(ctx, pod.Spec.Tolerations, node.Spec.Taints, func(taint *v1.Taint) bool {
return taint.Effect == v1.TaintEffectNoSchedule || taint.Effect == v1.TaintEffectNoExecute
}) {
continue
@@ -21,7 +21,6 @@ import (
"testing"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/fake"
@@ -33,8 +32,25 @@ import (
"sigs.k8s.io/descheduler/test"
)
func buildTestPodWithImage(podName, node, image string) *v1.Pod {
pod := test.BuildTestPod(podName, 100, 0, node, test.SetRSOwnerRef)
const (
nodeName1 = "n1"
nodeName2 = "n2"
nodeName3 = "n3"
nodeName4 = "n4"
nodeName5 = "n5"
nodeName6 = "n6"
)
func buildTestNode(nodeName string, apply func(*v1.Node)) *v1.Node {
return test.BuildTestNode(nodeName, 2000, 3000, 10, apply)
}
func buildTestPodForNode(name, nodeName string, apply func(*v1.Pod)) *v1.Pod {
return test.BuildTestPod(name, 100, 0, nodeName, apply)
}
func buildTestPodWithImage(podName, image string) *v1.Pod {
pod := buildTestPodForNode(podName, nodeName1, test.SetRSOwnerRef)
pod.Spec.Containers = append(pod.Spec.Containers, v1.Container{
Name: image,
Image: image,
@@ -42,144 +58,25 @@ func buildTestPodWithImage(podName, node, image string) *v1.Pod {
return pod
}
func buildTestPodWithRSOwnerRefForNode1(name string, apply func(*v1.Pod)) *v1.Pod {
return buildTestPodForNode(name, nodeName1, func(pod *v1.Pod) {
test.SetRSOwnerRef(pod)
if apply != nil {
apply(pod)
}
})
}
func buildTestPodWithRSOwnerRefWithNamespaceForNode1(name, namespace string, apply func(*v1.Pod)) *v1.Pod {
return buildTestPodWithRSOwnerRefForNode1(name, func(pod *v1.Pod) {
pod.Namespace = namespace
if apply != nil {
apply(pod)
}
})
}
func TestFindDuplicatePods(t *testing.T) {
// first setup pods
node1 := test.BuildTestNode("n1", 2000, 3000, 10, nil)
node2 := test.BuildTestNode("n2", 2000, 3000, 10, nil)
node3 := test.BuildTestNode("n3", 2000, 3000, 10, func(node *v1.Node) {
node.Spec.Taints = []v1.Taint{
{
Key: "hardware",
Value: "gpu",
Effect: v1.TaintEffectNoSchedule,
},
}
})
node4 := test.BuildTestNode("n4", 2000, 3000, 10, func(node *v1.Node) {
node.ObjectMeta.Labels = map[string]string{
"datacenter": "east",
}
})
node5 := test.BuildTestNode("n5", 2000, 3000, 10, func(node *v1.Node) {
node.Spec = v1.NodeSpec{
Unschedulable: true,
}
})
node6 := test.BuildTestNode("n6", 200, 200, 10, nil)
p1 := test.BuildTestPod("p1", 100, 0, node1.Name, nil)
p1.Namespace = "dev"
p2 := test.BuildTestPod("p2", 100, 0, node1.Name, nil)
p2.Namespace = "dev"
p3 := test.BuildTestPod("p3", 100, 0, node1.Name, nil)
p3.Namespace = "dev"
p4 := test.BuildTestPod("p4", 100, 0, node1.Name, nil)
p5 := test.BuildTestPod("p5", 100, 0, node1.Name, nil)
p6 := test.BuildTestPod("p6", 100, 0, node1.Name, nil)
p7 := test.BuildTestPod("p7", 100, 0, node1.Name, nil)
p7.Namespace = "kube-system"
p8 := test.BuildTestPod("p8", 100, 0, node1.Name, nil)
p8.Namespace = "test"
p9 := test.BuildTestPod("p9", 100, 0, node1.Name, nil)
p9.Namespace = "test"
p10 := test.BuildTestPod("p10", 100, 0, node1.Name, nil)
p10.Namespace = "test"
p11 := test.BuildTestPod("p11", 100, 0, node1.Name, nil)
p11.Namespace = "different-images"
p12 := test.BuildTestPod("p12", 100, 0, node1.Name, nil)
p12.Namespace = "different-images"
p13 := test.BuildTestPod("p13", 100, 0, node1.Name, nil)
p13.Namespace = "different-images"
p14 := test.BuildTestPod("p14", 100, 0, node1.Name, nil)
p14.Namespace = "different-images"
p15 := test.BuildTestPod("p15", 100, 0, node1.Name, nil)
p15.Namespace = "node-fit"
p16 := test.BuildTestPod("NOT1", 100, 0, node1.Name, nil)
p16.Namespace = "node-fit"
p17 := test.BuildTestPod("NOT2", 100, 0, node1.Name, nil)
p17.Namespace = "node-fit"
p18 := test.BuildTestPod("TARGET", 100, 0, node1.Name, nil)
p18.Namespace = "node-fit"
// This pod sits on node6 and is used to take up CPU requests on the node
p19 := test.BuildTestPod("CPU-eater", 150, 150, node6.Name, nil)
p19.Namespace = "test"
// Dummy pod for node6 used to do the opposite of p19
p20 := test.BuildTestPod("CPU-saver", 100, 150, node6.Name, nil)
p20.Namespace = "test"
// ### Evictable Pods ###
// Three Pods in the "default" Namespace, bound to same ReplicaSet. 2 should be evicted.
ownerRef1 := test.GetReplicaSetOwnerRefList()
p1.ObjectMeta.OwnerReferences = ownerRef1
p2.ObjectMeta.OwnerReferences = ownerRef1
p3.ObjectMeta.OwnerReferences = ownerRef1
// Three Pods in the "test" Namespace, bound to same ReplicaSet. 2 should be evicted.
ownerRef2 := test.GetReplicaSetOwnerRefList()
p8.ObjectMeta.OwnerReferences = ownerRef2
p9.ObjectMeta.OwnerReferences = ownerRef2
p10.ObjectMeta.OwnerReferences = ownerRef2
// ### Non-evictable Pods ###
// A DaemonSet.
p4.ObjectMeta.OwnerReferences = test.GetDaemonSetOwnerRefList()
// A Pod with local storage.
p5.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
p5.Spec.Volumes = []v1.Volume{
{
Name: "sample",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: "somePath"},
EmptyDir: &v1.EmptyDirVolumeSource{
SizeLimit: resource.NewQuantity(int64(10), resource.BinarySI),
},
},
},
}
// A Mirror Pod.
p6.Annotations = test.GetMirrorPodAnnotation()
// A Critical Pod.
priority := utils.SystemCriticalPriority
p7.Spec.Priority = &priority
// Same owners, but different images
p11.Spec.Containers[0].Image = "foo"
p11.ObjectMeta.OwnerReferences = ownerRef1
p12.Spec.Containers[0].Image = "bar"
p12.ObjectMeta.OwnerReferences = ownerRef1
// Multiple containers
p13.ObjectMeta.OwnerReferences = ownerRef1
p13.Spec.Containers = append(p13.Spec.Containers, v1.Container{
Name: "foo",
Image: "foo",
})
// ### Pods Evictable Based On Node Fit ###
ownerRef3 := test.GetReplicaSetOwnerRefList()
p15.ObjectMeta.OwnerReferences = ownerRef3
p16.ObjectMeta.OwnerReferences = ownerRef3
p17.ObjectMeta.OwnerReferences = ownerRef3
p18.ObjectMeta.OwnerReferences = ownerRef3
p15.Spec.NodeSelector = map[string]string{
"datacenter": "west",
}
p16.Spec.NodeSelector = map[string]string{
"datacenter": "west",
}
p17.Spec.NodeSelector = map[string]string{
"datacenter": "west",
}
testCases := []struct {
description string
pods []*v1.Pod
@@ -189,92 +86,263 @@ func TestFindDuplicatePods(t *testing.T) {
nodefit bool
}{
{
description: "Three pods in the `dev` Namespace, bound to same ReplicaSet. 1 should be evicted.",
pods: []*v1.Pod{p1, p2, p3},
nodes: []*v1.Node{node1, node2},
description: "Three pods in the `dev` Namespace, bound to same ReplicaSet. 1 should be evicted.",
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p1", "dev", nil),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p2", "dev", nil),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p3", "dev", nil),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
buildTestNode(nodeName2, nil),
},
expectedEvictedPodCount: 1,
},
{
description: "Three pods in the `dev` Namespace, bound to same ReplicaSet, but ReplicaSet kind is excluded. 0 should be evicted.",
pods: []*v1.Pod{p1, p2, p3},
nodes: []*v1.Node{node1, node2},
description: "Three pods in the `dev` Namespace, bound to same ReplicaSet, but ReplicaSet kind is excluded. 0 should be evicted.",
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p1", "dev", nil),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p2", "dev", nil),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p3", "dev", nil),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
buildTestNode(nodeName2, nil),
},
expectedEvictedPodCount: 0,
excludeOwnerKinds: []string{"ReplicaSet"},
},
{
description: "Three Pods in the `test` Namespace, bound to same ReplicaSet. 1 should be evicted.",
pods: []*v1.Pod{p8, p9, p10},
nodes: []*v1.Node{node1, node2},
description: "Three Pods in the `test` Namespace, bound to same ReplicaSet. 1 should be evicted.",
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p8", "test", nil),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p9", "test", nil),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p10", "test", nil),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
buildTestNode(nodeName2, nil),
},
expectedEvictedPodCount: 1,
},
{
description: "Three Pods in the `dev` Namespace, three Pods in the `test` Namespace. Bound to ReplicaSet with same name. 4 should be evicted.",
pods: []*v1.Pod{p1, p2, p3, p8, p9, p10},
nodes: []*v1.Node{node1, node2},
description: "Three Pods in the `dev` Namespace, three Pods in the `test` Namespace. Bound to ReplicaSet with same name. 4 should be evicted.",
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p1", "dev", nil),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p2", "dev", nil),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p3", "dev", nil),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p8", "test", nil),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p9", "test", nil),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p10", "test", nil),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
buildTestNode(nodeName2, nil),
},
expectedEvictedPodCount: 2,
},
{
description: "Pods are: part of DaemonSet, with local storage, mirror pod annotation, critical pod annotation - none should be evicted.",
pods: []*v1.Pod{p4, p5, p6, p7},
nodes: []*v1.Node{node1, node2},
description: "Pods are: part of DaemonSet, with local storage, mirror pod annotation, critical pod annotation - none should be evicted.",
pods: []*v1.Pod{
buildTestPodForNode("p4", nodeName1, test.SetDSOwnerRef),
buildTestPodForNode("p5", nodeName1, func(pod *v1.Pod) {
test.SetNormalOwnerRef(pod)
test.SetHostPathEmptyDirVolumeSource(pod)
}),
buildTestPodForNode("p6", nodeName1, func(pod *v1.Pod) {
pod.Annotations = test.GetMirrorPodAnnotation()
}),
buildTestPodForNode("p7", nodeName1, func(pod *v1.Pod) {
pod.Namespace = "kube-system"
test.SetPodPriority(pod, utils.SystemCriticalPriority)
}),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
buildTestNode(nodeName2, nil),
},
expectedEvictedPodCount: 0,
},
{
description: "Test all Pods: 4 should be evicted.",
pods: []*v1.Pod{p1, p2, p3, p4, p5, p6, p7, p8, p9, p10},
nodes: []*v1.Node{node1, node2},
description: "Test all Pods: 4 should be evicted.",
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p1", "dev", nil),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p2", "dev", nil),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p3", "dev", nil),
buildTestPodForNode("p4", nodeName1, test.SetDSOwnerRef),
buildTestPodForNode("p5", nodeName1, func(pod *v1.Pod) {
test.SetNormalOwnerRef(pod)
test.SetHostPathEmptyDirVolumeSource(pod)
}),
buildTestPodForNode("p6", nodeName1, func(pod *v1.Pod) {
pod.Annotations = test.GetMirrorPodAnnotation()
}),
buildTestPodForNode("p7", nodeName1, func(pod *v1.Pod) {
pod.Namespace = "kube-system"
test.SetPodPriority(pod, utils.SystemCriticalPriority)
}),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p8", "test", nil),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p9", "test", nil),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p10", "test", nil),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
buildTestNode(nodeName2, nil),
},
expectedEvictedPodCount: 2,
},
{
description: "Pods with the same owner but different images should not be evicted",
pods: []*v1.Pod{p11, p12},
nodes: []*v1.Node{node1, node2},
description: "Pods with the same owner but different images should not be evicted",
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p11", "different-images", func(pod *v1.Pod) {
pod.Spec.Containers[0].Image = "foo"
}),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p12", "different-images", func(pod *v1.Pod) {
pod.Spec.Containers[0].Image = "bar"
}),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
buildTestNode(nodeName2, nil),
},
expectedEvictedPodCount: 0,
},
{
description: "Pods with multiple containers should not match themselves",
pods: []*v1.Pod{p13},
nodes: []*v1.Node{node1, node2},
description: "Pods with multiple containers should not match themselves",
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p13", "different-images", func(pod *v1.Pod) {
pod.Spec.Containers = append(pod.Spec.Containers, v1.Container{
Name: "foo",
Image: "foo",
})
}),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
buildTestNode(nodeName2, nil),
},
expectedEvictedPodCount: 0,
},
{
description: "Pods with matching ownerrefs and at not all matching image should not trigger an eviction",
pods: []*v1.Pod{p11, p13},
nodes: []*v1.Node{node1, node2},
description: "Pods with matching ownerrefs and at not all matching image should not trigger an eviction",
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p11", "different-images", func(pod *v1.Pod) {
pod.Spec.Containers[0].Image = "foo"
}),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p13", "different-images", func(pod *v1.Pod) {
pod.Spec.Containers = append(pod.Spec.Containers, v1.Container{
Name: "foo",
Image: "foo",
})
}),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
buildTestNode(nodeName2, nil),
},
expectedEvictedPodCount: 0,
},
{
description: "Three pods in the `dev` Namespace, bound to same ReplicaSet. Only node available has a taint, and nodeFit set to true. 0 should be evicted.",
pods: []*v1.Pod{p1, p2, p3},
nodes: []*v1.Node{node1, node3},
description: "Three pods in the `dev` Namespace, bound to same ReplicaSet. Only node available has a taint, and nodeFit set to true. 0 should be evicted.",
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p1", "dev", nil),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p2", "dev", nil),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p3", "dev", nil),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
buildTestNode(nodeName3, func(node *v1.Node) {
node.Spec.Taints = []v1.Taint{
{
Key: "hardware",
Value: "gpu",
Effect: v1.TaintEffectNoSchedule,
},
}
}),
},
expectedEvictedPodCount: 0,
nodefit: true,
},
{
description: "Three pods in the `node-fit` Namespace, bound to same ReplicaSet, all with a nodeSelector. Only node available has an incorrect node label, and nodeFit set to true. 0 should be evicted.",
pods: []*v1.Pod{p15, p16, p17},
nodes: []*v1.Node{node1, node4},
description: "Three pods in the `node-fit` Namespace, bound to same ReplicaSet, all with a nodeSelector. Only node available has an incorrect node label, and nodeFit set to true. 0 should be evicted.",
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p15", "node-fit", func(pod *v1.Pod) {
pod.Spec.NodeSelector = map[string]string{
"datacenter": "west",
}
}),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("NOT1", "node-fit", func(pod *v1.Pod) {
pod.Spec.NodeSelector = map[string]string{
"datacenter": "west",
}
}),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("NOT2", "node-fit", func(pod *v1.Pod) {
pod.Spec.NodeSelector = map[string]string{
"datacenter": "west",
}
}),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
buildTestNode(nodeName4, func(node *v1.Node) {
node.ObjectMeta.Labels = map[string]string{
"datacenter": "east",
}
}),
},
expectedEvictedPodCount: 0,
nodefit: true,
},
{
description: "Three pods in the `node-fit` Namespace, bound to same ReplicaSet. Only node available is not schedulable, and nodeFit set to true. 0 should be evicted.",
pods: []*v1.Pod{p1, p2, p3},
nodes: []*v1.Node{node1, node5},
description: "Three pods in the `node-fit` Namespace, bound to same ReplicaSet. Only node available is not schedulable, and nodeFit set to true. 0 should be evicted.",
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p1", "dev", nil),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p2", "dev", nil),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p3", "dev", nil),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
buildTestNode(nodeName5, func(node *v1.Node) {
node.Spec = v1.NodeSpec{
Unschedulable: true,
}
}),
},
expectedEvictedPodCount: 0,
nodefit: true,
},
{
description: "Three pods in the `node-fit` Namespace, bound to same ReplicaSet. Only node available does not have enough CPU, and nodeFit set to true. 0 should be evicted.",
pods: []*v1.Pod{p1, p2, p3, p19},
nodes: []*v1.Node{node1, node6},
description: "Three pods in the `node-fit` Namespace, bound to same ReplicaSet. Only node available does not have enough CPU, and nodeFit set to true. 0 should be evicted.",
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p1", "dev", nil),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p2", "dev", nil),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p3", "dev", nil),
test.BuildTestPod("CPU-eater", 150, 150, nodeName6, func(pod *v1.Pod) {
pod.Namespace = "test"
}),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
test.BuildTestNode(nodeName6, 200, 200, 10, nil),
},
expectedEvictedPodCount: 0,
nodefit: true,
},
{
description: "Three pods in the `node-fit` Namespace, bound to same ReplicaSet. Only node available has enough CPU, and nodeFit set to true. 1 should be evicted.",
pods: []*v1.Pod{p1, p2, p3, p20},
nodes: []*v1.Node{node1, node6},
description: "Three pods in the `node-fit` Namespace, bound to same ReplicaSet. Only node available has enough CPU, and nodeFit set to true. 1 should be evicted.",
pods: []*v1.Pod{
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p1", "dev", nil),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p2", "dev", nil),
buildTestPodWithRSOwnerRefWithNamespaceForNode1("p3", "dev", nil),
test.BuildTestPod("CPU-saver", 100, 150, nodeName6, func(pod *v1.Pod) {
pod.Namespace = "test"
}),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
test.BuildTestNode(nodeName6, 200, 200, 10, nil),
},
expectedEvictedPodCount: 1,
nodefit: true,
},
@@ -330,26 +398,17 @@ func TestRemoveDuplicatesUniformly(t *testing.T) {
}
}
setTolerationsK1 := func(pod *v1.Pod) {
test.SetRSOwnerRef(pod)
pod.Spec.Tolerations = []v1.Toleration{
{
Key: "k1",
Value: "v1",
Operator: v1.TolerationOpEqual,
Effect: v1.TaintEffectNoSchedule,
},
}
}
setTolerationsK2 := func(pod *v1.Pod) {
test.SetRSOwnerRef(pod)
pod.Spec.Tolerations = []v1.Toleration{
{
Key: "k2",
Value: "v2",
Operator: v1.TolerationOpEqual,
Effect: v1.TaintEffectNoSchedule,
},
setNoScheduleTolerations := func(key, value string) func(*v1.Pod) {
return func(pod *v1.Pod) {
test.SetRSOwnerRef(pod)
pod.Spec.Tolerations = []v1.Toleration{
{
Key: key,
Value: value,
Operator: v1.TolerationOpEqual,
Effect: v1.TaintEffectNoSchedule,
},
}
}
}
@@ -377,70 +436,42 @@ func TestRemoveDuplicatesUniformly(t *testing.T) {
node.ObjectMeta.Labels["node-role.kubernetes.io/worker"] = "k2"
}
setNotMasterNodeSelectorK1 := func(pod *v1.Pod) {
test.SetRSOwnerRef(pod)
pod.Spec.Affinity = &v1.Affinity{
NodeAffinity: &v1.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{
NodeSelectorTerms: []v1.NodeSelectorTerm{
{
MatchExpressions: []v1.NodeSelectorRequirement{
{
Key: "node-role.kubernetes.io/control-plane",
Operator: v1.NodeSelectorOpDoesNotExist,
},
{
Key: "k1",
Operator: v1.NodeSelectorOpDoesNotExist,
setNotMasterNodeSelector := func(key string) func(*v1.Pod) {
return func(pod *v1.Pod) {
test.SetRSOwnerRef(pod)
pod.Spec.Affinity = &v1.Affinity{
NodeAffinity: &v1.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{
NodeSelectorTerms: []v1.NodeSelectorTerm{
{
MatchExpressions: []v1.NodeSelectorRequirement{
{
Key: "node-role.kubernetes.io/control-plane",
Operator: v1.NodeSelectorOpDoesNotExist,
},
{
Key: key,
Operator: v1.NodeSelectorOpDoesNotExist,
},
},
},
},
},
},
},
}
}
}
setNotMasterNodeSelectorK2 := func(pod *v1.Pod) {
test.SetRSOwnerRef(pod)
pod.Spec.Affinity = &v1.Affinity{
NodeAffinity: &v1.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &v1.NodeSelector{
NodeSelectorTerms: []v1.NodeSelectorTerm{
{
MatchExpressions: []v1.NodeSelectorRequirement{
{
Key: "node-role.kubernetes.io/control-plane",
Operator: v1.NodeSelectorOpDoesNotExist,
},
{
Key: "k2",
Operator: v1.NodeSelectorOpDoesNotExist,
},
},
},
},
},
},
setWorkerLabelSelector := func(value string) func(*v1.Pod) {
return func(pod *v1.Pod) {
test.SetRSOwnerRef(pod)
if pod.Spec.NodeSelector == nil {
pod.Spec.NodeSelector = map[string]string{}
}
pod.Spec.NodeSelector["node-role.kubernetes.io/worker"] = value
}
}
setWorkerLabelSelectorK1 := func(pod *v1.Pod) {
test.SetRSOwnerRef(pod)
if pod.Spec.NodeSelector == nil {
pod.Spec.NodeSelector = map[string]string{}
}
pod.Spec.NodeSelector["node-role.kubernetes.io/worker"] = "k1"
}
setWorkerLabelSelectorK2 := func(pod *v1.Pod) {
test.SetRSOwnerRef(pod)
if pod.Spec.NodeSelector == nil {
pod.Spec.NodeSelector = map[string]string{}
}
pod.Spec.NodeSelector["node-role.kubernetes.io/worker"] = "k2"
}
testCases := []struct {
description string
pods []*v1.Pod
@@ -451,107 +482,107 @@ func TestRemoveDuplicatesUniformly(t *testing.T) {
description: "Evict pods uniformly",
pods: []*v1.Pod{
// (5,3,1) -> (3,3,3) -> 2 evictions
test.BuildTestPod("p1", 100, 0, "n1", test.SetRSOwnerRef),
test.BuildTestPod("p2", 100, 0, "n1", test.SetRSOwnerRef),
test.BuildTestPod("p3", 100, 0, "n1", test.SetRSOwnerRef),
test.BuildTestPod("p4", 100, 0, "n1", test.SetRSOwnerRef),
test.BuildTestPod("p5", 100, 0, "n1", test.SetRSOwnerRef),
test.BuildTestPod("p6", 100, 0, "n2", test.SetRSOwnerRef),
test.BuildTestPod("p7", 100, 0, "n2", test.SetRSOwnerRef),
test.BuildTestPod("p8", 100, 0, "n2", test.SetRSOwnerRef),
test.BuildTestPod("p9", 100, 0, "n3", test.SetRSOwnerRef),
buildTestPodForNode("p1", "n1", test.SetRSOwnerRef),
buildTestPodForNode("p2", "n1", test.SetRSOwnerRef),
buildTestPodForNode("p3", "n1", test.SetRSOwnerRef),
buildTestPodForNode("p4", "n1", test.SetRSOwnerRef),
buildTestPodForNode("p5", "n1", test.SetRSOwnerRef),
buildTestPodForNode("p6", "n2", test.SetRSOwnerRef),
buildTestPodForNode("p7", "n2", test.SetRSOwnerRef),
buildTestPodForNode("p8", "n2", test.SetRSOwnerRef),
buildTestPodForNode("p9", "n3", test.SetRSOwnerRef),
},
expectedEvictedPodCount: 2,
nodes: []*v1.Node{
test.BuildTestNode("n1", 2000, 3000, 10, nil),
test.BuildTestNode("n2", 2000, 3000, 10, nil),
test.BuildTestNode("n3", 2000, 3000, 10, nil),
buildTestNode(nodeName1, nil),
buildTestNode(nodeName2, nil),
buildTestNode(nodeName3, nil),
},
},
{
description: "Evict pods uniformly with one node left out",
pods: []*v1.Pod{
// (5,3,1) -> (4,4,1) -> 1 eviction
test.BuildTestPod("p1", 100, 0, "n1", test.SetRSOwnerRef),
test.BuildTestPod("p2", 100, 0, "n1", test.SetRSOwnerRef),
test.BuildTestPod("p3", 100, 0, "n1", test.SetRSOwnerRef),
test.BuildTestPod("p4", 100, 0, "n1", test.SetRSOwnerRef),
test.BuildTestPod("p5", 100, 0, "n1", test.SetRSOwnerRef),
test.BuildTestPod("p6", 100, 0, "n2", test.SetRSOwnerRef),
test.BuildTestPod("p7", 100, 0, "n2", test.SetRSOwnerRef),
test.BuildTestPod("p8", 100, 0, "n2", test.SetRSOwnerRef),
test.BuildTestPod("p9", 100, 0, "n3", test.SetRSOwnerRef),
buildTestPodForNode("p1", "n1", test.SetRSOwnerRef),
buildTestPodForNode("p2", "n1", test.SetRSOwnerRef),
buildTestPodForNode("p3", "n1", test.SetRSOwnerRef),
buildTestPodForNode("p4", "n1", test.SetRSOwnerRef),
buildTestPodForNode("p5", "n1", test.SetRSOwnerRef),
buildTestPodForNode("p6", "n2", test.SetRSOwnerRef),
buildTestPodForNode("p7", "n2", test.SetRSOwnerRef),
buildTestPodForNode("p8", "n2", test.SetRSOwnerRef),
buildTestPodForNode("p9", "n3", test.SetRSOwnerRef),
},
expectedEvictedPodCount: 1,
nodes: []*v1.Node{
test.BuildTestNode("n1", 2000, 3000, 10, nil),
test.BuildTestNode("n2", 2000, 3000, 10, nil),
buildTestNode(nodeName1, nil),
buildTestNode(nodeName2, nil),
},
},
{
description: "Evict pods uniformly with two replica sets",
pods: []*v1.Pod{
// (5,3,1) -> (3,3,3) -> 2 evictions
test.BuildTestPod("p11", 100, 0, "n1", setTwoRSOwnerRef),
test.BuildTestPod("p12", 100, 0, "n1", setTwoRSOwnerRef),
test.BuildTestPod("p13", 100, 0, "n1", setTwoRSOwnerRef),
test.BuildTestPod("p14", 100, 0, "n1", setTwoRSOwnerRef),
test.BuildTestPod("p15", 100, 0, "n1", setTwoRSOwnerRef),
test.BuildTestPod("p16", 100, 0, "n2", setTwoRSOwnerRef),
test.BuildTestPod("p17", 100, 0, "n2", setTwoRSOwnerRef),
test.BuildTestPod("p18", 100, 0, "n2", setTwoRSOwnerRef),
test.BuildTestPod("p19", 100, 0, "n3", setTwoRSOwnerRef),
buildTestPodForNode("p11", "n1", setTwoRSOwnerRef),
buildTestPodForNode("p12", "n1", setTwoRSOwnerRef),
buildTestPodForNode("p13", "n1", setTwoRSOwnerRef),
buildTestPodForNode("p14", "n1", setTwoRSOwnerRef),
buildTestPodForNode("p15", "n1", setTwoRSOwnerRef),
buildTestPodForNode("p16", "n2", setTwoRSOwnerRef),
buildTestPodForNode("p17", "n2", setTwoRSOwnerRef),
buildTestPodForNode("p18", "n2", setTwoRSOwnerRef),
buildTestPodForNode("p19", "n3", setTwoRSOwnerRef),
},
expectedEvictedPodCount: 4,
nodes: []*v1.Node{
test.BuildTestNode("n1", 2000, 3000, 10, nil),
test.BuildTestNode("n2", 2000, 3000, 10, nil),
test.BuildTestNode("n3", 2000, 3000, 10, nil),
buildTestNode(nodeName1, nil),
buildTestNode(nodeName2, nil),
buildTestNode(nodeName3, nil),
},
},
{
description: "Evict pods uniformly with two owner references",
pods: []*v1.Pod{
// (5,3,1) -> (3,3,3) -> 2 evictions
test.BuildTestPod("p11", 100, 0, "n1", test.SetRSOwnerRef),
test.BuildTestPod("p12", 100, 0, "n1", test.SetRSOwnerRef),
test.BuildTestPod("p13", 100, 0, "n1", test.SetRSOwnerRef),
test.BuildTestPod("p14", 100, 0, "n1", test.SetRSOwnerRef),
test.BuildTestPod("p15", 100, 0, "n1", test.SetRSOwnerRef),
test.BuildTestPod("p16", 100, 0, "n2", test.SetRSOwnerRef),
test.BuildTestPod("p17", 100, 0, "n2", test.SetRSOwnerRef),
test.BuildTestPod("p18", 100, 0, "n2", test.SetRSOwnerRef),
test.BuildTestPod("p19", 100, 0, "n3", test.SetRSOwnerRef),
buildTestPodForNode("p11", "n1", test.SetRSOwnerRef),
buildTestPodForNode("p12", "n1", test.SetRSOwnerRef),
buildTestPodForNode("p13", "n1", test.SetRSOwnerRef),
buildTestPodForNode("p14", "n1", test.SetRSOwnerRef),
buildTestPodForNode("p15", "n1", test.SetRSOwnerRef),
buildTestPodForNode("p16", "n2", test.SetRSOwnerRef),
buildTestPodForNode("p17", "n2", test.SetRSOwnerRef),
buildTestPodForNode("p18", "n2", test.SetRSOwnerRef),
buildTestPodForNode("p19", "n3", test.SetRSOwnerRef),
// (1,3,5) -> (3,3,3) -> 2 evictions
test.BuildTestPod("p21", 100, 0, "n1", setRSOwnerRef2),
test.BuildTestPod("p22", 100, 0, "n2", setRSOwnerRef2),
test.BuildTestPod("p23", 100, 0, "n2", setRSOwnerRef2),
test.BuildTestPod("p24", 100, 0, "n2", setRSOwnerRef2),
test.BuildTestPod("p25", 100, 0, "n3", setRSOwnerRef2),
test.BuildTestPod("p26", 100, 0, "n3", setRSOwnerRef2),
test.BuildTestPod("p27", 100, 0, "n3", setRSOwnerRef2),
test.BuildTestPod("p28", 100, 0, "n3", setRSOwnerRef2),
test.BuildTestPod("p29", 100, 0, "n3", setRSOwnerRef2),
buildTestPodForNode("p21", "n1", setRSOwnerRef2),
buildTestPodForNode("p22", "n2", setRSOwnerRef2),
buildTestPodForNode("p23", "n2", setRSOwnerRef2),
buildTestPodForNode("p24", "n2", setRSOwnerRef2),
buildTestPodForNode("p25", "n3", setRSOwnerRef2),
buildTestPodForNode("p26", "n3", setRSOwnerRef2),
buildTestPodForNode("p27", "n3", setRSOwnerRef2),
buildTestPodForNode("p28", "n3", setRSOwnerRef2),
buildTestPodForNode("p29", "n3", setRSOwnerRef2),
},
expectedEvictedPodCount: 4,
nodes: []*v1.Node{
test.BuildTestNode("n1", 2000, 3000, 10, nil),
test.BuildTestNode("n2", 2000, 3000, 10, nil),
test.BuildTestNode("n3", 2000, 3000, 10, nil),
buildTestNode(nodeName1, nil),
buildTestNode(nodeName2, nil),
buildTestNode(nodeName3, nil),
},
},
{
description: "Evict pods with number of pods less than nodes",
pods: []*v1.Pod{
// (2,0,0) -> (1,1,0) -> 1 eviction
test.BuildTestPod("p1", 100, 0, "n1", test.SetRSOwnerRef),
test.BuildTestPod("p2", 100, 0, "n1", test.SetRSOwnerRef),
buildTestPodForNode("p1", "n1", test.SetRSOwnerRef),
buildTestPodForNode("p2", "n1", test.SetRSOwnerRef),
},
expectedEvictedPodCount: 1,
nodes: []*v1.Node{
test.BuildTestNode("n1", 2000, 3000, 10, nil),
test.BuildTestNode("n2", 2000, 3000, 10, nil),
test.BuildTestNode("n3", 2000, 3000, 10, nil),
buildTestNode(nodeName1, nil),
buildTestNode(nodeName2, nil),
buildTestNode(nodeName3, nil),
},
},
{
@@ -560,125 +591,125 @@ func TestRemoveDuplicatesUniformly(t *testing.T) {
// (1, 0, 0) for "bar","baz" images -> no eviction, even with a matching ownerKey
// (2, 0, 0) for "foo" image -> (1,1,0) - 1 eviction
// In this case the only "real" duplicates are p1 and p4, so one of those should be evicted
buildTestPodWithImage("p1", "n1", "foo"),
buildTestPodWithImage("p2", "n1", "bar"),
buildTestPodWithImage("p3", "n1", "baz"),
buildTestPodWithImage("p4", "n1", "foo"),
buildTestPodWithImage("p1", "foo"),
buildTestPodWithImage("p2", "bar"),
buildTestPodWithImage("p3", "baz"),
buildTestPodWithImage("p4", "foo"),
},
expectedEvictedPodCount: 1,
nodes: []*v1.Node{
test.BuildTestNode("n1", 2000, 3000, 10, nil),
test.BuildTestNode("n2", 2000, 3000, 10, nil),
test.BuildTestNode("n3", 2000, 3000, 10, nil),
buildTestNode(nodeName1, nil),
buildTestNode(nodeName2, nil),
buildTestNode(nodeName3, nil),
},
},
{
description: "Evict pods with a single pod with three nodes",
pods: []*v1.Pod{
// (2,0,0) -> (1,1,0) -> 1 eviction
test.BuildTestPod("p1", 100, 0, "n1", test.SetRSOwnerRef),
buildTestPodForNode("p1", "n1", test.SetRSOwnerRef),
},
expectedEvictedPodCount: 0,
nodes: []*v1.Node{
test.BuildTestNode("n1", 2000, 3000, 10, nil),
test.BuildTestNode("n2", 2000, 3000, 10, nil),
test.BuildTestNode("n3", 2000, 3000, 10, nil),
buildTestNode(nodeName1, nil),
buildTestNode(nodeName2, nil),
buildTestNode(nodeName3, nil),
},
},
{
description: "Evict pods uniformly respecting taints",
pods: []*v1.Pod{
// (5,3,1,0,0,0) -> (3,3,3,0,0,0) -> 2 evictions
test.BuildTestPod("p1", 100, 0, "worker1", setTolerationsK1),
test.BuildTestPod("p2", 100, 0, "worker1", setTolerationsK2),
test.BuildTestPod("p3", 100, 0, "worker1", setTolerationsK1),
test.BuildTestPod("p4", 100, 0, "worker1", setTolerationsK2),
test.BuildTestPod("p5", 100, 0, "worker1", setTolerationsK1),
test.BuildTestPod("p6", 100, 0, "worker2", setTolerationsK2),
test.BuildTestPod("p7", 100, 0, "worker2", setTolerationsK1),
test.BuildTestPod("p8", 100, 0, "worker2", setTolerationsK2),
test.BuildTestPod("p9", 100, 0, "worker3", setTolerationsK1),
buildTestPodForNode("p1", "worker1", setNoScheduleTolerations("k1", "v1")),
buildTestPodForNode("p2", "worker1", setNoScheduleTolerations("k2", "v2")),
buildTestPodForNode("p3", "worker1", setNoScheduleTolerations("k1", "v1")),
buildTestPodForNode("p4", "worker1", setNoScheduleTolerations("k2", "v2")),
buildTestPodForNode("p5", "worker1", setNoScheduleTolerations("k1", "v1")),
buildTestPodForNode("p6", "worker2", setNoScheduleTolerations("k2", "v2")),
buildTestPodForNode("p7", "worker2", setNoScheduleTolerations("k1", "v1")),
buildTestPodForNode("p8", "worker2", setNoScheduleTolerations("k2", "v2")),
buildTestPodForNode("p9", "worker3", setNoScheduleTolerations("k1", "v1")),
},
expectedEvictedPodCount: 2,
nodes: []*v1.Node{
test.BuildTestNode("worker1", 2000, 3000, 10, nil),
test.BuildTestNode("worker2", 2000, 3000, 10, nil),
test.BuildTestNode("worker3", 2000, 3000, 10, nil),
test.BuildTestNode("master1", 2000, 3000, 10, setMasterNoScheduleTaint),
test.BuildTestNode("master2", 2000, 3000, 10, setMasterNoScheduleTaint),
test.BuildTestNode("master3", 2000, 3000, 10, setMasterNoScheduleTaint),
buildTestNode("worker1", nil),
buildTestNode("worker2", nil),
buildTestNode("worker3", nil),
buildTestNode("master1", setMasterNoScheduleTaint),
buildTestNode("master2", setMasterNoScheduleTaint),
buildTestNode("master3", setMasterNoScheduleTaint),
},
},
{
description: "Evict pods uniformly respecting RequiredDuringSchedulingIgnoredDuringExecution node affinity",
pods: []*v1.Pod{
// (5,3,1,0,0,0) -> (3,3,3,0,0,0) -> 2 evictions
test.BuildTestPod("p1", 100, 0, "worker1", setNotMasterNodeSelectorK1),
test.BuildTestPod("p2", 100, 0, "worker1", setNotMasterNodeSelectorK2),
test.BuildTestPod("p3", 100, 0, "worker1", setNotMasterNodeSelectorK1),
test.BuildTestPod("p4", 100, 0, "worker1", setNotMasterNodeSelectorK2),
test.BuildTestPod("p5", 100, 0, "worker1", setNotMasterNodeSelectorK1),
test.BuildTestPod("p6", 100, 0, "worker2", setNotMasterNodeSelectorK2),
test.BuildTestPod("p7", 100, 0, "worker2", setNotMasterNodeSelectorK1),
test.BuildTestPod("p8", 100, 0, "worker2", setNotMasterNodeSelectorK2),
test.BuildTestPod("p9", 100, 0, "worker3", setNotMasterNodeSelectorK1),
buildTestPodForNode("p1", "worker1", setNotMasterNodeSelector("k1")),
buildTestPodForNode("p2", "worker1", setNotMasterNodeSelector("k2")),
buildTestPodForNode("p3", "worker1", setNotMasterNodeSelector("k1")),
buildTestPodForNode("p4", "worker1", setNotMasterNodeSelector("k2")),
buildTestPodForNode("p5", "worker1", setNotMasterNodeSelector("k1")),
buildTestPodForNode("p6", "worker2", setNotMasterNodeSelector("k2")),
buildTestPodForNode("p7", "worker2", setNotMasterNodeSelector("k1")),
buildTestPodForNode("p8", "worker2", setNotMasterNodeSelector("k2")),
buildTestPodForNode("p9", "worker3", setNotMasterNodeSelector("k1")),
},
expectedEvictedPodCount: 2,
nodes: []*v1.Node{
test.BuildTestNode("worker1", 2000, 3000, 10, nil),
test.BuildTestNode("worker2", 2000, 3000, 10, nil),
test.BuildTestNode("worker3", 2000, 3000, 10, nil),
test.BuildTestNode("master1", 2000, 3000, 10, setMasterNoScheduleLabel),
test.BuildTestNode("master2", 2000, 3000, 10, setMasterNoScheduleLabel),
test.BuildTestNode("master3", 2000, 3000, 10, setMasterNoScheduleLabel),
buildTestNode("worker1", nil),
buildTestNode("worker2", nil),
buildTestNode("worker3", nil),
buildTestNode("master1", setMasterNoScheduleLabel),
buildTestNode("master2", setMasterNoScheduleLabel),
buildTestNode("master3", setMasterNoScheduleLabel),
},
},
{
description: "Evict pods uniformly respecting node selector",
pods: []*v1.Pod{
// (5,3,1,0,0,0) -> (3,3,3,0,0,0) -> 2 evictions
test.BuildTestPod("p1", 100, 0, "worker1", setWorkerLabelSelectorK1),
test.BuildTestPod("p2", 100, 0, "worker1", setWorkerLabelSelectorK2),
test.BuildTestPod("p3", 100, 0, "worker1", setWorkerLabelSelectorK1),
test.BuildTestPod("p4", 100, 0, "worker1", setWorkerLabelSelectorK2),
test.BuildTestPod("p5", 100, 0, "worker1", setWorkerLabelSelectorK1),
test.BuildTestPod("p6", 100, 0, "worker2", setWorkerLabelSelectorK2),
test.BuildTestPod("p7", 100, 0, "worker2", setWorkerLabelSelectorK1),
test.BuildTestPod("p8", 100, 0, "worker2", setWorkerLabelSelectorK2),
test.BuildTestPod("p9", 100, 0, "worker3", setWorkerLabelSelectorK1),
buildTestPodForNode("p1", "worker1", setWorkerLabelSelector("k1")),
buildTestPodForNode("p2", "worker1", setWorkerLabelSelector("k2")),
buildTestPodForNode("p3", "worker1", setWorkerLabelSelector("k1")),
buildTestPodForNode("p4", "worker1", setWorkerLabelSelector("k2")),
buildTestPodForNode("p5", "worker1", setWorkerLabelSelector("k1")),
buildTestPodForNode("p6", "worker2", setWorkerLabelSelector("k2")),
buildTestPodForNode("p7", "worker2", setWorkerLabelSelector("k1")),
buildTestPodForNode("p8", "worker2", setWorkerLabelSelector("k2")),
buildTestPodForNode("p9", "worker3", setWorkerLabelSelector("k1")),
},
expectedEvictedPodCount: 2,
nodes: []*v1.Node{
test.BuildTestNode("worker1", 2000, 3000, 10, setWorkerLabel),
test.BuildTestNode("worker2", 2000, 3000, 10, setWorkerLabel),
test.BuildTestNode("worker3", 2000, 3000, 10, setWorkerLabel),
test.BuildTestNode("master1", 2000, 3000, 10, nil),
test.BuildTestNode("master2", 2000, 3000, 10, nil),
test.BuildTestNode("master3", 2000, 3000, 10, nil),
buildTestNode("worker1", setWorkerLabel),
buildTestNode("worker2", setWorkerLabel),
buildTestNode("worker3", setWorkerLabel),
buildTestNode("master1", nil),
buildTestNode("master2", nil),
buildTestNode("master3", nil),
},
},
{
description: "Evict pods uniformly respecting node selector with zero target nodes",
pods: []*v1.Pod{
// (5,3,1,0,0,0) -> (3,3,3,0,0,0) -> 2 evictions
test.BuildTestPod("p1", 100, 0, "worker1", setWorkerLabelSelectorK1),
test.BuildTestPod("p2", 100, 0, "worker1", setWorkerLabelSelectorK2),
test.BuildTestPod("p3", 100, 0, "worker1", setWorkerLabelSelectorK1),
test.BuildTestPod("p4", 100, 0, "worker1", setWorkerLabelSelectorK2),
test.BuildTestPod("p5", 100, 0, "worker1", setWorkerLabelSelectorK1),
test.BuildTestPod("p6", 100, 0, "worker2", setWorkerLabelSelectorK2),
test.BuildTestPod("p7", 100, 0, "worker2", setWorkerLabelSelectorK1),
test.BuildTestPod("p8", 100, 0, "worker2", setWorkerLabelSelectorK2),
test.BuildTestPod("p9", 100, 0, "worker3", setWorkerLabelSelectorK1),
buildTestPodForNode("p1", "worker1", setWorkerLabelSelector("k1")),
buildTestPodForNode("p2", "worker1", setWorkerLabelSelector("k2")),
buildTestPodForNode("p3", "worker1", setWorkerLabelSelector("k1")),
buildTestPodForNode("p4", "worker1", setWorkerLabelSelector("k2")),
buildTestPodForNode("p5", "worker1", setWorkerLabelSelector("k1")),
buildTestPodForNode("p6", "worker2", setWorkerLabelSelector("k2")),
buildTestPodForNode("p7", "worker2", setWorkerLabelSelector("k1")),
buildTestPodForNode("p8", "worker2", setWorkerLabelSelector("k2")),
buildTestPodForNode("p9", "worker3", setWorkerLabelSelector("k1")),
},
expectedEvictedPodCount: 0,
nodes: []*v1.Node{
test.BuildTestNode("worker1", 2000, 3000, 10, nil),
test.BuildTestNode("worker2", 2000, 3000, 10, nil),
test.BuildTestNode("worker3", 2000, 3000, 10, nil),
test.BuildTestNode("master1", 2000, 3000, 10, nil),
test.BuildTestNode("master2", 2000, 3000, 10, nil),
test.BuildTestNode("master3", 2000, 3000, 10, nil),
buildTestNode("worker1", nil),
buildTestNode("worker2", nil),
buildTestNode("worker3", nil),
buildTestNode("master1", nil),
buildTestNode("master2", nil),
buildTestNode("master3", nil),
},
},
}
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
@@ -22,7 +22,6 @@ import (
"testing"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/fake"
@@ -33,73 +32,93 @@ import (
"sigs.k8s.io/descheduler/test"
)
func initPods(node *v1.Node) []*v1.Pod {
pods := make([]*v1.Pod, 0)
const (
nodeName1 = "node1"
nodeName2 = "node2"
nodeName3 = "node3"
nodeName4 = "node4"
nodeName5 = "node5"
)
for i := int32(0); i <= 9; i++ {
pod := test.BuildTestPod(fmt.Sprintf("pod-%d", i), 100, 0, node.Name, nil)
pod.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
func buildTestNode(nodeName string, apply func(*v1.Node)) *v1.Node {
return test.BuildTestNode(nodeName, 2000, 3000, 10, apply)
}
// pod at index i will have 25 * i restarts.
pod.Status = v1.PodStatus{
InitContainerStatuses: []v1.ContainerStatus{
{
RestartCount: 5 * i,
},
func setPodContainerStatusRestartCount(pod *v1.Pod, base int32) {
pod.Status = v1.PodStatus{
InitContainerStatuses: []v1.ContainerStatus{
{
RestartCount: 5 * base,
},
ContainerStatuses: []v1.ContainerStatus{
{
RestartCount: 10 * i,
},
{
RestartCount: 10 * i,
},
},
ContainerStatuses: []v1.ContainerStatus{
{
RestartCount: 10 * base,
},
}
pods = append(pods, pod)
}
// The following 3 pods won't get evicted.
// A daemonset.
pods[6].ObjectMeta.OwnerReferences = test.GetDaemonSetOwnerRefList()
// A pod with local storage.
pods[7].ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
pods[7].Spec.Volumes = []v1.Volume{
{
Name: "sample",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: "somePath"},
EmptyDir: &v1.EmptyDirVolumeSource{
SizeLimit: resource.NewQuantity(int64(10), resource.BinarySI),
},
{
RestartCount: 10 * base,
},
},
}
// A Mirror Pod.
pods[8].Annotations = test.GetMirrorPodAnnotation()
}
func initPodContainersWithStatusRestartCount(name string, base int32, apply func(pod *v1.Pod)) *v1.Pod {
return test.BuildTestPod(name, 100, 0, nodeName1, func(pod *v1.Pod) {
test.SetNormalOwnerRef(pod)
// pod at index i will have 25 * i restarts, 5 for init container, 20 for other two containers
setPodContainerStatusRestartCount(pod, base)
if apply != nil {
apply(pod)
}
})
}
func initPods(apply func(pod *v1.Pod)) []*v1.Pod {
pods := make([]*v1.Pod, 0)
for i := int32(0); i <= 9; i++ {
switch i {
default:
pods = append(pods, initPodContainersWithStatusRestartCount(fmt.Sprintf("pod-%d", i), i, apply))
// The following 3 pods won't get evicted.
// A daemonset.
case 6:
pods = append(pods, initPodContainersWithStatusRestartCount(fmt.Sprintf("pod-%d", i), i, func(pod *v1.Pod) {
test.SetDSOwnerRef(pod)
if apply != nil {
apply(pod)
}
}))
// A pod with local storage.
case 7:
pods = append(pods, initPodContainersWithStatusRestartCount(fmt.Sprintf("pod-%d", i), i, func(pod *v1.Pod) {
test.SetNormalOwnerRef(pod)
test.SetHostPathEmptyDirVolumeSource(pod)
if apply != nil {
apply(pod)
}
}))
// A Mirror Pod.
case 8:
pods = append(pods, initPodContainersWithStatusRestartCount(fmt.Sprintf("pod-%d", i), i, func(pod *v1.Pod) {
pod.Annotations = test.GetMirrorPodAnnotation()
if apply != nil {
apply(pod)
}
}))
}
}
pods = append(
pods,
test.BuildTestPod("CPU-consumer-1", 150, 100, nodeName4, test.SetNormalOwnerRef),
test.BuildTestPod("CPU-consumer-2", 150, 100, nodeName5, test.SetNormalOwnerRef),
)
return pods
}
func TestRemovePodsHavingTooManyRestarts(t *testing.T) {
node1 := test.BuildTestNode("node1", 2000, 3000, 10, nil)
node2 := test.BuildTestNode("node2", 2000, 3000, 10, func(node *v1.Node) {
node.Spec.Taints = []v1.Taint{
{
Key: "hardware",
Value: "gpu",
Effect: v1.TaintEffectNoSchedule,
},
}
})
node3 := test.BuildTestNode("node3", 2000, 3000, 10, func(node *v1.Node) {
node.Spec = v1.NodeSpec{
Unschedulable: true,
}
})
node4 := test.BuildTestNode("node4", 200, 3000, 10, nil)
node5 := test.BuildTestNode("node5", 2000, 3000, 10, nil)
createRemovePodsHavingTooManyRestartsAgrs := func(
podRestartThresholds int32,
includingInitContainers bool,
@@ -114,207 +133,261 @@ func TestRemovePodsHavingTooManyRestarts(t *testing.T) {
tests := []struct {
description string
pods []*v1.Pod
nodes []*v1.Node
args RemovePodsHavingTooManyRestartsArgs
expectedEvictedPodCount uint
maxPodsToEvictPerNode *uint
maxNoOfPodsToEvictPerNamespace *uint
nodeFit bool
applyFunc func([]*v1.Pod)
}{
{
description: "All pods have total restarts under threshold, no pod evictions",
args: createRemovePodsHavingTooManyRestartsAgrs(10000, true),
nodes: []*v1.Node{node1},
description: "All pods have total restarts under threshold, no pod evictions",
args: createRemovePodsHavingTooManyRestartsAgrs(10000, true),
pods: initPods(nil),
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
},
expectedEvictedPodCount: 0,
},
{
description: "Some pods have total restarts bigger than threshold",
args: createRemovePodsHavingTooManyRestartsAgrs(1, true),
nodes: []*v1.Node{node1},
description: "Some pods have total restarts bigger than threshold",
args: createRemovePodsHavingTooManyRestartsAgrs(1, true),
pods: initPods(nil),
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
},
expectedEvictedPodCount: 6,
},
{
description: "Nine pods have total restarts equals threshold(includingInitContainers=true), 6 pod evictions",
args: createRemovePodsHavingTooManyRestartsAgrs(1*25, true),
nodes: []*v1.Node{node1},
description: "Nine pods have total restarts equals threshold(includingInitContainers=true), 6 pod evictions",
args: createRemovePodsHavingTooManyRestartsAgrs(1*25, true),
pods: initPods(nil),
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
},
expectedEvictedPodCount: 6,
},
{
description: "Nine pods have total restarts equals threshold(includingInitContainers=false), 5 pod evictions",
args: createRemovePodsHavingTooManyRestartsAgrs(1*25, false),
nodes: []*v1.Node{node1},
description: "Nine pods have total restarts equals threshold(includingInitContainers=false), 5 pod evictions",
args: createRemovePodsHavingTooManyRestartsAgrs(1*25, false),
pods: initPods(nil),
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
},
expectedEvictedPodCount: 5,
},
{
description: "All pods have total restarts equals threshold(includingInitContainers=true), 6 pod evictions",
args: createRemovePodsHavingTooManyRestartsAgrs(1*20, true),
nodes: []*v1.Node{node1},
description: "All pods have total restarts equals threshold(includingInitContainers=true), 6 pod evictions",
args: createRemovePodsHavingTooManyRestartsAgrs(1*20, true),
pods: initPods(nil),
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
},
expectedEvictedPodCount: 6,
},
{
description: "Nine pods have total restarts equals threshold(includingInitContainers=false), 6 pod evictions",
args: createRemovePodsHavingTooManyRestartsAgrs(1*20, false),
nodes: []*v1.Node{node1},
description: "Nine pods have total restarts equals threshold(includingInitContainers=false), 6 pod evictions",
args: createRemovePodsHavingTooManyRestartsAgrs(1*20, false),
pods: initPods(nil),
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
},
expectedEvictedPodCount: 6,
},
{
description: "Five pods have total restarts bigger than threshold(includingInitContainers=true), but only 1 pod eviction",
args: createRemovePodsHavingTooManyRestartsAgrs(5*25+1, true),
nodes: []*v1.Node{node1},
description: "Five pods have total restarts bigger than threshold(includingInitContainers=true), but only 1 pod eviction",
args: createRemovePodsHavingTooManyRestartsAgrs(5*25+1, true),
pods: initPods(nil),
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
},
expectedEvictedPodCount: 1,
},
{
description: "Five pods have total restarts bigger than threshold(includingInitContainers=false), but only 1 pod eviction",
args: createRemovePodsHavingTooManyRestartsAgrs(5*20+1, false),
nodes: []*v1.Node{node1},
description: "Five pods have total restarts bigger than threshold(includingInitContainers=false), but only 1 pod eviction",
args: createRemovePodsHavingTooManyRestartsAgrs(5*20+1, false),
pods: initPods(nil),
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
},
expectedEvictedPodCount: 1,
nodeFit: false,
},
{
description: "All pods have total restarts equals threshold(maxPodsToEvictPerNode=3), 3 pod evictions",
args: createRemovePodsHavingTooManyRestartsAgrs(1, true),
nodes: []*v1.Node{node1},
description: "All pods have total restarts equals threshold(maxPodsToEvictPerNode=3), 3 pod evictions",
args: createRemovePodsHavingTooManyRestartsAgrs(1, true),
pods: initPods(nil),
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
},
expectedEvictedPodCount: 3,
maxPodsToEvictPerNode: &uint3,
},
{
description: "All pods have total restarts equals threshold(maxNoOfPodsToEvictPerNamespace=3), 3 pod evictions",
args: createRemovePodsHavingTooManyRestartsAgrs(1, true),
nodes: []*v1.Node{node1},
description: "All pods have total restarts equals threshold(maxNoOfPodsToEvictPerNamespace=3), 3 pod evictions",
args: createRemovePodsHavingTooManyRestartsAgrs(1, true),
pods: initPods(nil),
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
},
expectedEvictedPodCount: 3,
maxNoOfPodsToEvictPerNamespace: &uint3,
},
{
description: "All pods have total restarts equals threshold(maxPodsToEvictPerNode=3) but the only other node is tainted, 0 pod evictions",
args: createRemovePodsHavingTooManyRestartsAgrs(1, true),
nodes: []*v1.Node{node1, node2},
description: "All pods have total restarts equals threshold(maxPodsToEvictPerNode=3) but the only other node is tainted, 0 pod evictions",
args: createRemovePodsHavingTooManyRestartsAgrs(1, true),
pods: initPods(nil),
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
buildTestNode(nodeName2, func(node *v1.Node) {
node.Spec.Taints = []v1.Taint{
{
Key: "hardware",
Value: "gpu",
Effect: v1.TaintEffectNoSchedule,
},
}
}),
},
expectedEvictedPodCount: 0,
maxPodsToEvictPerNode: &uint3,
nodeFit: true,
},
{
description: "All pods have total restarts equals threshold(maxPodsToEvictPerNode=3) but the only other node is not schedulable, 0 pod evictions",
args: createRemovePodsHavingTooManyRestartsAgrs(1, true),
nodes: []*v1.Node{node1, node3},
description: "All pods have total restarts equals threshold(maxPodsToEvictPerNode=3) but the only other node is not schedulable, 0 pod evictions",
args: createRemovePodsHavingTooManyRestartsAgrs(1, true),
pods: initPods(nil),
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
buildTestNode(nodeName3, func(node *v1.Node) {
node.Spec = v1.NodeSpec{
Unschedulable: true,
}
}),
},
expectedEvictedPodCount: 0,
maxPodsToEvictPerNode: &uint3,
nodeFit: true,
},
{
description: "All pods have total restarts equals threshold(maxPodsToEvictPerNode=3) but the only other node does not have enough CPU, 0 pod evictions",
args: createRemovePodsHavingTooManyRestartsAgrs(1, true),
nodes: []*v1.Node{node1, node4},
description: "All pods have total restarts equals threshold(maxPodsToEvictPerNode=3) but the only other node does not have enough CPU, 0 pod evictions",
args: createRemovePodsHavingTooManyRestartsAgrs(1, true),
pods: initPods(nil),
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
test.BuildTestNode(nodeName4, 200, 3000, 10, nil),
},
expectedEvictedPodCount: 0,
maxPodsToEvictPerNode: &uint3,
nodeFit: true,
},
{
description: "All pods have total restarts equals threshold(maxPodsToEvictPerNode=3) but the only other node has enough CPU, 3 pod evictions",
args: createRemovePodsHavingTooManyRestartsAgrs(1, true),
nodes: []*v1.Node{node1, node5},
description: "All pods have total restarts equals threshold(maxPodsToEvictPerNode=3) but the only other node has enough CPU, 3 pod evictions",
args: createRemovePodsHavingTooManyRestartsAgrs(1, true),
pods: initPods(nil),
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
buildTestNode(nodeName5, nil),
},
expectedEvictedPodCount: 3,
maxPodsToEvictPerNode: &uint3,
nodeFit: true,
},
{
description: "pods are in CrashLoopBackOff with states=CrashLoopBackOff, 3 pod evictions",
args: RemovePodsHavingTooManyRestartsArgs{PodRestartThreshold: 1, States: []string{"CrashLoopBackOff"}},
nodes: []*v1.Node{node1, node5},
description: "pods are in CrashLoopBackOff with states=CrashLoopBackOff, 3 pod evictions",
args: RemovePodsHavingTooManyRestartsArgs{PodRestartThreshold: 1, States: []string{"CrashLoopBackOff"}},
pods: initPods(func(pod *v1.Pod) {
if len(pod.Status.ContainerStatuses) > 0 {
pod.Status.ContainerStatuses[0].State = v1.ContainerState{
Waiting: &v1.ContainerStateWaiting{Reason: "CrashLoopBackOff"},
}
}
}),
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
buildTestNode(nodeName5, nil),
},
expectedEvictedPodCount: 3,
maxPodsToEvictPerNode: &uint3,
nodeFit: true,
applyFunc: func(pods []*v1.Pod) {
for _, pod := range pods {
if len(pod.Status.ContainerStatuses) > 0 {
pod.Status.ContainerStatuses[0].State = v1.ContainerState{
},
{
description: "pods without CrashLoopBackOff with states=CrashLoopBackOff, 0 pod evictions",
args: RemovePodsHavingTooManyRestartsArgs{PodRestartThreshold: 1, States: []string{"CrashLoopBackOff"}},
pods: initPods(nil),
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
buildTestNode(nodeName5, nil),
},
expectedEvictedPodCount: 0,
maxPodsToEvictPerNode: &uint3,
nodeFit: true,
},
{
description: "pods running with state=Running, 3 pod evictions",
args: RemovePodsHavingTooManyRestartsArgs{PodRestartThreshold: 1, States: []string{string(v1.PodRunning)}},
pods: initPods(func(pod *v1.Pod) {
pod.Status.Phase = v1.PodRunning
}),
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
},
expectedEvictedPodCount: 3,
maxPodsToEvictPerNode: &uint3,
},
{
description: "pods pending with state=Running, 0 pod evictions",
args: RemovePodsHavingTooManyRestartsArgs{PodRestartThreshold: 1, States: []string{string(v1.PodRunning)}},
pods: initPods(func(pod *v1.Pod) {
pod.Status.Phase = v1.PodPending
}),
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
},
expectedEvictedPodCount: 0,
maxPodsToEvictPerNode: &uint3,
},
{
description: "pods pending with initContainer with states=CrashLoopBackOff threshold(includingInitContainers=true), 3 pod evictions",
args: RemovePodsHavingTooManyRestartsArgs{PodRestartThreshold: 1, States: []string{"CrashLoopBackOff"}, IncludingInitContainers: true},
pods: initPods(func(pod *v1.Pod) {
pod.Status.InitContainerStatuses = []v1.ContainerStatus{
{
State: v1.ContainerState{
Waiting: &v1.ContainerStateWaiting{Reason: "CrashLoopBackOff"},
}
}
},
},
}
}),
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
},
},
{
description: "pods without CrashLoopBackOff with states=CrashLoopBackOff, 0 pod evictions",
args: RemovePodsHavingTooManyRestartsArgs{PodRestartThreshold: 1, States: []string{"CrashLoopBackOff"}},
nodes: []*v1.Node{node1, node5},
expectedEvictedPodCount: 0,
maxPodsToEvictPerNode: &uint3,
nodeFit: true,
},
{
description: "pods running with state=Running, 3 pod evictions",
args: RemovePodsHavingTooManyRestartsArgs{PodRestartThreshold: 1, States: []string{string(v1.PodRunning)}},
nodes: []*v1.Node{node1},
expectedEvictedPodCount: 3,
maxPodsToEvictPerNode: &uint3,
applyFunc: func(pods []*v1.Pod) {
for _, pod := range pods {
pod.Status.Phase = v1.PodRunning
}
},
},
{
description: "pods pending with state=Running, 0 pod evictions",
args: RemovePodsHavingTooManyRestartsArgs{PodRestartThreshold: 1, States: []string{string(v1.PodRunning)}},
nodes: []*v1.Node{node1},
description: "pods pending with initContainer with states=CrashLoopBackOff threshold(includingInitContainers=false), 0 pod evictions",
args: RemovePodsHavingTooManyRestartsArgs{PodRestartThreshold: 1, States: []string{"CrashLoopBackOff"}, IncludingInitContainers: false},
pods: initPods(func(pod *v1.Pod) {
pod.Status.InitContainerStatuses = []v1.ContainerStatus{
{
State: v1.ContainerState{
Waiting: &v1.ContainerStateWaiting{Reason: "CrashLoopBackOff"},
},
},
}
}),
nodes: []*v1.Node{
buildTestNode(nodeName1, nil),
},
expectedEvictedPodCount: 0,
maxPodsToEvictPerNode: &uint3,
applyFunc: func(pods []*v1.Pod) {
for _, pod := range pods {
pod.Status.Phase = v1.PodPending
}
},
},
{
description: "pods pending with initContainer with states=CrashLoopBackOff threshold(includingInitContainers=true), 3 pod evictions",
args: RemovePodsHavingTooManyRestartsArgs{PodRestartThreshold: 1, States: []string{"CrashLoopBackOff"}, IncludingInitContainers: true},
nodes: []*v1.Node{node1},
expectedEvictedPodCount: 3,
maxPodsToEvictPerNode: &uint3,
applyFunc: func(pods []*v1.Pod) {
for _, pod := range pods {
pod.Status.InitContainerStatuses = []v1.ContainerStatus{
{
State: v1.ContainerState{
Waiting: &v1.ContainerStateWaiting{Reason: "CrashLoopBackOff"},
},
},
}
}
},
},
{
description: "pods pending with initContainer with states=CrashLoopBackOff threshold(includingInitContainers=false), 0 pod evictions",
args: RemovePodsHavingTooManyRestartsArgs{PodRestartThreshold: 1, States: []string{"CrashLoopBackOff"}, IncludingInitContainers: false},
nodes: []*v1.Node{node1},
expectedEvictedPodCount: 0,
maxPodsToEvictPerNode: &uint3,
applyFunc: func(pods []*v1.Pod) {
for _, pod := range pods {
pod.Status.InitContainerStatuses = []v1.ContainerStatus{
{
State: v1.ContainerState{
Waiting: &v1.ContainerStateWaiting{Reason: "CrashLoopBackOff"},
},
},
}
}
},
},
}
for _, tc := range tests {
t.Run(tc.description, func(t *testing.T) {
pods := append(
initPods(node1),
test.BuildTestPod("CPU-consumer-1", 150, 100, node4.Name, test.SetNormalOwnerRef),
test.BuildTestPod("CPU-consumer-2", 150, 100, node5.Name, test.SetNormalOwnerRef),
)
if tc.applyFunc != nil {
tc.applyFunc(pods)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -322,7 +395,7 @@ func TestRemovePodsHavingTooManyRestarts(t *testing.T) {
for _, node := range tc.nodes {
objs = append(objs, node)
}
for _, pod := range pods {
for _, pod := range tc.pods {
objs = append(objs, pod)
}
fakeClient := fake.NewSimpleClientset(objs...)
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
@@ -33,84 +33,75 @@ import (
"sigs.k8s.io/descheduler/test"
)
func TestPodAntiAffinity(t *testing.T) {
node1 := test.BuildTestNode("n1", 2000, 3000, 10, func(node *v1.Node) {
node.ObjectMeta.Labels = map[string]string{
"region": "main-region",
}
})
node2 := test.BuildTestNode("n2", 2000, 3000, 10, func(node *v1.Node) {
node.ObjectMeta.Labels = map[string]string{
"datacenter": "east",
}
})
node3 := test.BuildTestNode("n3", 2000, 3000, 10, func(node *v1.Node) {
node.Spec = v1.NodeSpec{
Unschedulable: true,
}
})
node4 := test.BuildTestNode("n4", 2, 2, 1, nil)
node5 := test.BuildTestNode("n5", 200, 3000, 10, func(node *v1.Node) {
node.ObjectMeta.Labels = map[string]string{
"region": "main-region",
}
})
const (
nodeName1 = "n1"
nodeName2 = "n2"
nodeName3 = "n3"
nodeName4 = "n4"
nodeName5 = "n5"
)
p1 := test.BuildTestPod("p1", 100, 0, node1.Name, nil)
p2 := test.BuildTestPod("p2", 100, 0, node1.Name, nil)
p3 := test.BuildTestPod("p3", 100, 0, node1.Name, nil)
p4 := test.BuildTestPod("p4", 100, 0, node1.Name, nil)
p5 := test.BuildTestPod("p5", 100, 0, node1.Name, nil)
p6 := test.BuildTestPod("p6", 100, 0, node1.Name, nil)
p7 := test.BuildTestPod("p7", 100, 0, node1.Name, nil)
p8 := test.BuildTestPod("p8", 100, 0, node1.Name, nil)
p9 := test.BuildTestPod("p9", 100, 0, node1.Name, nil)
p10 := test.BuildTestPod("p10", 100, 0, node1.Name, nil)
p11 := test.BuildTestPod("p11", 100, 0, node5.Name, nil)
p9.DeletionTimestamp = &metav1.Time{}
p10.DeletionTimestamp = &metav1.Time{}
func buildTestNode(name string, apply func(*v1.Node)) *v1.Node {
return test.BuildTestNode(name, 2000, 3000, 10, apply)
}
criticalPriority := utils.SystemCriticalPriority
nonEvictablePod := test.BuildTestPod("non-evict", 100, 0, node1.Name, func(pod *v1.Pod) {
pod.Spec.Priority = &criticalPriority
})
p2.Labels = map[string]string{"foo": "bar"}
p5.Labels = map[string]string{"foo": "bar"}
p6.Labels = map[string]string{"foo": "bar"}
p7.Labels = map[string]string{"foo1": "bar1"}
p11.Labels = map[string]string{"foo": "bar"}
nonEvictablePod.Labels = map[string]string{"foo": "bar"}
test.SetNormalOwnerRef(p1)
test.SetNormalOwnerRef(p2)
test.SetNormalOwnerRef(p3)
test.SetNormalOwnerRef(p4)
test.SetNormalOwnerRef(p5)
test.SetNormalOwnerRef(p6)
test.SetNormalOwnerRef(p7)
test.SetNormalOwnerRef(p9)
test.SetNormalOwnerRef(p10)
test.SetNormalOwnerRef(p11)
// set pod anti affinity
test.SetPodAntiAffinity(p1, "foo", "bar")
test.SetPodAntiAffinity(p3, "foo", "bar")
test.SetPodAntiAffinity(p4, "foo", "bar")
test.SetPodAntiAffinity(p5, "foo1", "bar1")
test.SetPodAntiAffinity(p6, "foo1", "bar1")
test.SetPodAntiAffinity(p7, "foo", "bar")
test.SetPodAntiAffinity(p9, "foo", "bar")
test.SetPodAntiAffinity(p10, "foo", "bar")
// set pod priority
test.SetPodPriority(p5, 100)
test.SetPodPriority(p6, 50)
test.SetPodPriority(p7, 0)
// Set pod node selectors
p8.Spec.NodeSelector = map[string]string{
"datacenter": "west",
func setNodeMainRegionLabel(node *v1.Node) {
node.ObjectMeta.Labels = map[string]string{
"region": "main-region",
}
}
func buildTestNode1() *v1.Node {
return buildTestNode(nodeName1, setNodeMainRegionLabel)
}
func buildTestPod(name, nodeName string, apply func(*v1.Pod)) *v1.Pod {
return test.BuildTestPod(name, 100, 0, nodeName, apply)
}
func buildTestPodForNode1(name string, apply func(*v1.Pod)) *v1.Pod {
return buildTestPod(name, nodeName1, apply)
}
func setPodAntiAffinityFooBar(pod *v1.Pod) {
test.SetPodAntiAffinity(pod, "foo", "bar")
}
func setPodAntiAffinityFoo1Bar1(pod *v1.Pod) {
test.SetPodAntiAffinity(pod, "foo1", "bar1")
}
func setLabelsFooBar(pod *v1.Pod) {
pod.Labels = map[string]string{"foo": "bar"}
}
func setLabelsFoo1Bar1(pod *v1.Pod) {
pod.Labels = map[string]string{"foo1": "bar1"}
}
func buildTestPodWithAntiAffinityForNode1(name string) *v1.Pod {
return buildTestPodForNode1(name, func(pod *v1.Pod) {
test.SetNormalOwnerRef(pod)
setPodAntiAffinityFooBar(pod)
})
}
func buildTestPodP2ForNode1() *v1.Pod {
return buildTestPodForNode1("p2", func(pod *v1.Pod) {
test.SetNormalOwnerRef(pod)
setLabelsFooBar(pod)
})
}
func buildTestPodNonEvictableForNode1() *v1.Pod {
criticalPriority := utils.SystemCriticalPriority
return buildTestPodForNode1("non-evict", func(pod *v1.Pod) {
test.SetPodPriority(pod, criticalPriority)
setLabelsFooBar(pod)
})
}
func TestPodAntiAffinity(t *testing.T) {
var uint1 uint = 1
var uint3 uint = 3
@@ -125,87 +116,204 @@ func TestPodAntiAffinity(t *testing.T) {
nodes []*v1.Node
}{
{
description: "Maximum pods to evict - 0",
pods: []*v1.Pod{p1, p2, p3, p4},
nodes: []*v1.Node{node1},
description: "Maximum pods to evict - 0",
pods: []*v1.Pod{
buildTestPodWithAntiAffinityForNode1("p1"),
buildTestPodP2ForNode1(),
buildTestPodWithAntiAffinityForNode1("p3"),
buildTestPodWithAntiAffinityForNode1("p4"),
},
nodes: []*v1.Node{
buildTestNode1(),
},
expectedEvictedPodCount: 3,
},
{
description: "Maximum pods to evict - 3",
maxPodsToEvictPerNode: &uint3,
pods: []*v1.Pod{p1, p2, p3, p4},
nodes: []*v1.Node{node1},
description: "Maximum pods to evict - 3",
maxPodsToEvictPerNode: &uint3,
pods: []*v1.Pod{
buildTestPodWithAntiAffinityForNode1("p1"),
buildTestPodP2ForNode1(),
buildTestPodWithAntiAffinityForNode1("p3"),
buildTestPodWithAntiAffinityForNode1("p4"),
},
nodes: []*v1.Node{
buildTestNode1(),
},
expectedEvictedPodCount: 3,
},
{
description: "Maximum pods to evict (maxPodsToEvictPerNamespace=3) - 3",
maxNoOfPodsToEvictPerNamespace: &uint3,
pods: []*v1.Pod{p1, p2, p3, p4},
nodes: []*v1.Node{node1},
expectedEvictedPodCount: 3,
pods: []*v1.Pod{
buildTestPodWithAntiAffinityForNode1("p1"),
buildTestPodP2ForNode1(),
buildTestPodWithAntiAffinityForNode1("p3"),
buildTestPodWithAntiAffinityForNode1("p4"),
},
nodes: []*v1.Node{
buildTestNode1(),
},
expectedEvictedPodCount: 3,
},
{
description: "Maximum pods to evict (maxNoOfPodsToEvictTotal)",
maxNoOfPodsToEvictPerNamespace: &uint3,
maxNoOfPodsToEvictTotal: &uint1,
pods: []*v1.Pod{p1, p2, p3, p4},
nodes: []*v1.Node{node1},
expectedEvictedPodCount: 1,
},
{
description: "Evict only 1 pod after sorting",
pods: []*v1.Pod{p5, p6, p7},
nodes: []*v1.Node{node1},
pods: []*v1.Pod{
buildTestPodWithAntiAffinityForNode1("p1"),
buildTestPodP2ForNode1(),
buildTestPodWithAntiAffinityForNode1("p3"),
buildTestPodWithAntiAffinityForNode1("p4"),
},
nodes: []*v1.Node{
buildTestNode1(),
},
expectedEvictedPodCount: 1,
},
{
description: "Evicts pod that conflicts with critical pod (but does not evict critical pod)",
maxPodsToEvictPerNode: &uint1,
pods: []*v1.Pod{p1, nonEvictablePod},
nodes: []*v1.Node{node1},
description: "Evict only 1 pod after sorting",
pods: []*v1.Pod{
buildTestPodForNode1("p5", func(pod *v1.Pod) {
test.SetNormalOwnerRef(pod)
setLabelsFooBar(pod)
setPodAntiAffinityFoo1Bar1(pod)
test.SetPodPriority(pod, 100)
}),
buildTestPodForNode1("p6", func(pod *v1.Pod) {
test.SetNormalOwnerRef(pod)
setLabelsFooBar(pod)
setPodAntiAffinityFoo1Bar1(pod)
test.SetPodPriority(pod, 50)
}),
buildTestPodForNode1("p7", func(pod *v1.Pod) {
test.SetNormalOwnerRef(pod)
setLabelsFoo1Bar1(pod)
setPodAntiAffinityFooBar(pod)
test.SetPodPriority(pod, 0)
}),
},
nodes: []*v1.Node{
buildTestNode1(),
},
expectedEvictedPodCount: 1,
},
{
description: "Evicts pod that conflicts with critical pod (but does not evict critical pod)",
maxPodsToEvictPerNode: &uint1,
pods: []*v1.Pod{p1, nonEvictablePod},
nodes: []*v1.Node{node1},
description: "Evicts pod that conflicts with critical pod (but does not evict critical pod)",
maxPodsToEvictPerNode: &uint1,
pods: []*v1.Pod{
buildTestPodWithAntiAffinityForNode1("p1"),
buildTestPodNonEvictableForNode1(),
},
nodes: []*v1.Node{
buildTestNode1(),
},
expectedEvictedPodCount: 1,
},
{
description: "Won't evict pods because node selectors don't match available nodes",
maxPodsToEvictPerNode: &uint1,
pods: []*v1.Pod{p8, nonEvictablePod},
nodes: []*v1.Node{node1, node2},
description: "Evicts pod that conflicts with critical pod (but does not evict critical pod)",
maxPodsToEvictPerNode: &uint1,
pods: []*v1.Pod{
buildTestPodWithAntiAffinityForNode1("p1"),
buildTestPodNonEvictableForNode1(),
},
nodes: []*v1.Node{
buildTestNode1(),
},
expectedEvictedPodCount: 1,
},
{
description: "Won't evict pods because node selectors don't match available nodes",
maxPodsToEvictPerNode: &uint1,
pods: []*v1.Pod{
buildTestPodForNode1("p8", func(pod *v1.Pod) {
pod.Spec.NodeSelector = map[string]string{
"datacenter": "west",
}
}),
buildTestPodNonEvictableForNode1(),
},
nodes: []*v1.Node{
buildTestNode1(),
buildTestNode(nodeName2, func(node *v1.Node) {
node.ObjectMeta.Labels = map[string]string{
"datacenter": "east",
}
}),
},
expectedEvictedPodCount: 0,
nodeFit: true,
},
{
description: "Won't evict pods because only other node is not schedulable",
maxPodsToEvictPerNode: &uint1,
pods: []*v1.Pod{p8, nonEvictablePod},
nodes: []*v1.Node{node1, node3},
description: "Won't evict pods because only other node is not schedulable",
maxPodsToEvictPerNode: &uint1,
pods: []*v1.Pod{
buildTestPodForNode1("p8", func(pod *v1.Pod) {
pod.Spec.NodeSelector = map[string]string{
"datacenter": "west",
}
}),
buildTestPodNonEvictableForNode1(),
},
nodes: []*v1.Node{
buildTestNode1(),
buildTestNode(nodeName3, func(node *v1.Node) {
node.Spec = v1.NodeSpec{
Unschedulable: true,
}
}),
},
expectedEvictedPodCount: 0,
nodeFit: true,
},
{
description: "No pod to evicted since all pod terminating",
pods: []*v1.Pod{p9, p10},
nodes: []*v1.Node{node1},
description: "No pod to evicted since all pod terminating",
pods: []*v1.Pod{
buildTestPodForNode1("p9", func(pod *v1.Pod) {
test.SetNormalOwnerRef(pod)
setPodAntiAffinityFooBar(pod)
pod.DeletionTimestamp = &metav1.Time{}
}),
buildTestPodForNode1("p10", func(pod *v1.Pod) {
test.SetNormalOwnerRef(pod)
setPodAntiAffinityFooBar(pod)
pod.DeletionTimestamp = &metav1.Time{}
}),
},
nodes: []*v1.Node{
buildTestNode1(),
},
expectedEvictedPodCount: 0,
},
{
description: "Won't evict pods because only other node doesn't have enough resources",
maxPodsToEvictPerNode: &uint3,
pods: []*v1.Pod{p1, p2, p3, p4},
nodes: []*v1.Node{node1, node4},
description: "Won't evict pods because only other node doesn't have enough resources",
maxPodsToEvictPerNode: &uint3,
pods: []*v1.Pod{
buildTestPodWithAntiAffinityForNode1("p1"),
buildTestPodP2ForNode1(),
buildTestPodWithAntiAffinityForNode1("p3"),
buildTestPodWithAntiAffinityForNode1("p4"),
},
nodes: []*v1.Node{
buildTestNode1(),
test.BuildTestNode(nodeName4, 2, 2, 1, nil),
},
expectedEvictedPodCount: 0,
nodeFit: true,
},
{
description: "Evict pod violating anti-affinity among different node (all pods have anti-affinity)",
pods: []*v1.Pod{p1, p11},
nodes: []*v1.Node{node1, node5},
description: "Evict pod violating anti-affinity among different node (all pods have anti-affinity)",
pods: []*v1.Pod{
buildTestPodWithAntiAffinityForNode1("p1"),
buildTestPod("p11", nodeName5, func(pod *v1.Pod) {
test.SetNormalOwnerRef(pod)
setLabelsFooBar(pod)
}),
},
nodes: []*v1.Node{
buildTestNode1(),
test.BuildTestNode(nodeName5, 200, 3000, 10, setNodeMainRegionLabel),
},
expectedEvictedPodCount: 1,
nodeFit: false,
},
@@ -244,6 +352,9 @@ func TestPodAntiAffinity(t *testing.T) {
&RemovePodsViolatingInterPodAntiAffinityArgs{},
handle,
)
if err != nil {
t.Fatalf("Unable to initialize a plugin: %v", err)
}
plugin.(frameworktypes.DeschedulePlugin).Deschedule(ctx, test.nodes)
podsEvicted := podEvictor.TotalEvicted()
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
@@ -32,73 +32,91 @@ import (
"sigs.k8s.io/descheduler/test"
)
const (
nodeWithLabelsName = "nodeWithLabels"
nodeWithoutLabelsName = "nodeWithoutLabels"
unschedulableNodeWithLabelsName = "unschedulableNodeWithLabels"
nodeLabelKey = "kubernetes.io/desiredNode"
nodeLabelValue = "yes"
)
func buildTestNode(name string, apply func(*v1.Node)) *v1.Node {
return test.BuildTestNode(name, 2000, 3000, 10, apply)
}
func setNodeDesiredNodeLabel(node *v1.Node) {
node.Labels[nodeLabelKey] = nodeLabelValue
}
func buildTestPod(name, nodeName string, apply func(*v1.Pod)) *v1.Pod {
return test.BuildTestPod(name, 100, 0, nodeName, apply)
}
func buildUnschedulableNodeWithLabels() *v1.Node {
return buildTestNode(unschedulableNodeWithLabelsName, func(node *v1.Node) {
setNodeDesiredNodeLabel(node)
node.Spec.Unschedulable = true
})
}
func TestRemovePodsViolatingNodeAffinity(t *testing.T) {
nodeLabelKey := "kubernetes.io/desiredNode"
nodeLabelValue := "yes"
nodeWithLabels := test.BuildTestNode("nodeWithLabels", 2000, 3000, 10, nil)
nodeWithLabels.Labels[nodeLabelKey] = nodeLabelValue
addPodsToNode := func(nodeName string, deletionTimestamp *metav1.Time, affinityType string) []*v1.Pod {
podWithNodeAffinity := buildTestPod("podWithNodeAffinity", nodeName, func(pod *v1.Pod) {
pod.Spec.Affinity = &v1.Affinity{
NodeAffinity: &v1.NodeAffinity{},
}
nodeWithoutLabels := test.BuildTestNode("nodeWithoutLabels", 2000, 3000, 10, nil)
unschedulableNodeWithLabels := test.BuildTestNode("unschedulableNodeWithLabels", 2000, 3000, 10, nil)
unschedulableNodeWithLabels.Labels[nodeLabelKey] = nodeLabelValue
unschedulableNodeWithLabels.Spec.Unschedulable = true
addPodsToNode := func(node *v1.Node, deletionTimestamp *metav1.Time, affinityType string) []*v1.Pod {
podWithNodeAffinity := test.BuildTestPod("podWithNodeAffinity", 100, 0, node.Name, nil)
podWithNodeAffinity.Spec.Affinity = &v1.Affinity{
NodeAffinity: &v1.NodeAffinity{},
}
switch affinityType {
case "requiredDuringSchedulingIgnoredDuringExecution":
podWithNodeAffinity.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution = &v1.NodeSelector{
NodeSelectorTerms: []v1.NodeSelectorTerm{
switch affinityType {
case "requiredDuringSchedulingIgnoredDuringExecution":
pod.Spec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution = &v1.NodeSelector{
NodeSelectorTerms: []v1.NodeSelectorTerm{
{
MatchExpressions: []v1.NodeSelectorRequirement{
{
Key: nodeLabelKey,
Operator: "In",
Values: []string{
nodeLabelValue,
},
},
},
},
},
}
case "preferredDuringSchedulingIgnoredDuringExecution":
pod.Spec.Affinity.NodeAffinity.PreferredDuringSchedulingIgnoredDuringExecution = []v1.PreferredSchedulingTerm{
{
MatchExpressions: []v1.NodeSelectorRequirement{
{
Key: nodeLabelKey,
Operator: "In",
Values: []string{
nodeLabelValue,
Weight: 10,
Preference: v1.NodeSelectorTerm{
MatchExpressions: []v1.NodeSelectorRequirement{
{
Key: nodeLabelKey,
Operator: "In",
Values: []string{
nodeLabelValue,
},
},
},
},
},
},
}
case "requiredDuringSchedulingRequiredDuringExecution":
default:
t.Fatalf("Invalid affinity type %s", affinityType)
}
case "preferredDuringSchedulingIgnoredDuringExecution":
podWithNodeAffinity.Spec.Affinity.NodeAffinity.PreferredDuringSchedulingIgnoredDuringExecution = []v1.PreferredSchedulingTerm{
{
Weight: 10,
Preference: v1.NodeSelectorTerm{
MatchExpressions: []v1.NodeSelectorRequirement{
{
Key: nodeLabelKey,
Operator: "In",
Values: []string{
nodeLabelValue,
},
},
},
},
},
}
case "requiredDuringSchedulingRequiredDuringExecution":
default:
t.Fatalf("Invalid affinity type %s", affinityType)
}
pod1 := test.BuildTestPod("pod1", 100, 0, node.Name, nil)
pod2 := test.BuildTestPod("pod2", 100, 0, node.Name, nil)
pod.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
pod.DeletionTimestamp = deletionTimestamp
})
podWithNodeAffinity.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
pod1.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
pod2.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
podWithNodeAffinity.DeletionTimestamp = deletionTimestamp
pod1.DeletionTimestamp = deletionTimestamp
pod2.DeletionTimestamp = deletionTimestamp
pod1 := buildTestPod("pod1", nodeName, func(pod *v1.Pod) {
pod.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
pod.DeletionTimestamp = deletionTimestamp
})
pod2 := buildTestPod("pod2", nodeName, func(pod *v1.Pod) {
pod.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
pod.DeletionTimestamp = deletionTimestamp
})
return []*v1.Pod{
podWithNodeAffinity,
@@ -126,8 +144,11 @@ func TestRemovePodsViolatingNodeAffinity(t *testing.T) {
NodeAffinityType: []string{"requiredDuringSchedulingRequiredDuringExecution"},
},
expectedEvictedPodCount: 0,
pods: addPodsToNode(nodeWithoutLabels, nil, "requiredDuringSchedulingRequiredDuringExecution"),
nodes: []*v1.Node{nodeWithoutLabels, nodeWithLabels},
pods: addPodsToNode(nodeWithoutLabelsName, nil, "requiredDuringSchedulingRequiredDuringExecution"),
nodes: []*v1.Node{
buildTestNode(nodeWithoutLabelsName, nil),
buildTestNode(nodeWithLabelsName, setNodeDesiredNodeLabel),
},
},
{
description: "Pod is correctly scheduled on node, no eviction expected [required affinity]",
@@ -135,8 +156,10 @@ func TestRemovePodsViolatingNodeAffinity(t *testing.T) {
NodeAffinityType: []string{"requiredDuringSchedulingIgnoredDuringExecution"},
},
expectedEvictedPodCount: 0,
pods: addPodsToNode(nodeWithLabels, nil, "requiredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{nodeWithLabels},
pods: addPodsToNode(nodeWithLabelsName, nil, "requiredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{
buildTestNode(nodeWithLabelsName, setNodeDesiredNodeLabel),
},
},
{
description: "Pod is correctly scheduled on node, no eviction expected [preferred affinity]",
@@ -144,8 +167,10 @@ func TestRemovePodsViolatingNodeAffinity(t *testing.T) {
NodeAffinityType: []string{"preferredDuringSchedulingIgnoredDuringExecution"},
},
expectedEvictedPodCount: 0,
pods: addPodsToNode(nodeWithLabels, nil, "preferredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{nodeWithLabels},
pods: addPodsToNode(nodeWithLabelsName, nil, "preferredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{
buildTestNode(nodeWithLabelsName, setNodeDesiredNodeLabel),
},
},
{
description: "Pod is scheduled on node without matching labels, another schedulable node available, should be evicted",
@@ -153,8 +178,11 @@ func TestRemovePodsViolatingNodeAffinity(t *testing.T) {
args: RemovePodsViolatingNodeAffinityArgs{
NodeAffinityType: []string{"requiredDuringSchedulingIgnoredDuringExecution"},
},
pods: addPodsToNode(nodeWithoutLabels, nil, "requiredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{nodeWithoutLabels, nodeWithLabels},
pods: addPodsToNode(nodeWithoutLabelsName, nil, "requiredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{
buildTestNode(nodeWithoutLabelsName, nil),
buildTestNode(nodeWithLabelsName, setNodeDesiredNodeLabel),
},
},
{
description: "Pod is scheduled on node without matching labels, another schedulable node available with better fit, should be evicted",
@@ -162,8 +190,11 @@ func TestRemovePodsViolatingNodeAffinity(t *testing.T) {
args: RemovePodsViolatingNodeAffinityArgs{
NodeAffinityType: []string{"preferredDuringSchedulingIgnoredDuringExecution"},
},
pods: addPodsToNode(nodeWithoutLabels, nil, "preferredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{nodeWithoutLabels, nodeWithLabels},
pods: addPodsToNode(nodeWithoutLabelsName, nil, "preferredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{
buildTestNode(nodeWithoutLabelsName, nil),
buildTestNode(nodeWithLabelsName, setNodeDesiredNodeLabel),
},
},
{
description: "Pod is scheduled on node without matching labels, another schedulable node available, maxPodsToEvictPerNode set to 1, should be evicted [required affinity]",
@@ -171,8 +202,11 @@ func TestRemovePodsViolatingNodeAffinity(t *testing.T) {
args: RemovePodsViolatingNodeAffinityArgs{
NodeAffinityType: []string{"requiredDuringSchedulingIgnoredDuringExecution"},
},
pods: addPodsToNode(nodeWithoutLabels, nil, "requiredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{nodeWithoutLabels, nodeWithLabels},
pods: addPodsToNode(nodeWithoutLabelsName, nil, "requiredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{
buildTestNode(nodeWithoutLabelsName, nil),
buildTestNode(nodeWithLabelsName, setNodeDesiredNodeLabel),
},
maxPodsToEvictPerNode: &uint1,
},
{
@@ -181,8 +215,11 @@ func TestRemovePodsViolatingNodeAffinity(t *testing.T) {
args: RemovePodsViolatingNodeAffinityArgs{
NodeAffinityType: []string{"preferredDuringSchedulingIgnoredDuringExecution"},
},
pods: addPodsToNode(nodeWithoutLabels, nil, "preferredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{nodeWithoutLabels, nodeWithLabels},
pods: addPodsToNode(nodeWithoutLabelsName, nil, "preferredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{
buildTestNode(nodeWithoutLabelsName, nil),
buildTestNode(nodeWithLabelsName, setNodeDesiredNodeLabel),
},
maxPodsToEvictPerNode: &uint1,
},
{
@@ -191,8 +228,11 @@ func TestRemovePodsViolatingNodeAffinity(t *testing.T) {
args: RemovePodsViolatingNodeAffinityArgs{
NodeAffinityType: []string{"requiredDuringSchedulingIgnoredDuringExecution"},
},
pods: addPodsToNode(nodeWithoutLabels, nil, "requiredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{nodeWithoutLabels, nodeWithLabels},
pods: addPodsToNode(nodeWithoutLabelsName, nil, "requiredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{
buildTestNode(nodeWithoutLabelsName, nil),
buildTestNode(nodeWithLabelsName, setNodeDesiredNodeLabel),
},
maxPodsToEvictPerNode: &uint0,
},
{
@@ -201,8 +241,11 @@ func TestRemovePodsViolatingNodeAffinity(t *testing.T) {
args: RemovePodsViolatingNodeAffinityArgs{
NodeAffinityType: []string{"preferredDuringSchedulingIgnoredDuringExecution"},
},
pods: addPodsToNode(nodeWithoutLabels, nil, "preferredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{nodeWithoutLabels, nodeWithLabels},
pods: addPodsToNode(nodeWithoutLabelsName, nil, "preferredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{
buildTestNode(nodeWithoutLabelsName, nil),
buildTestNode(nodeWithLabelsName, setNodeDesiredNodeLabel),
},
maxPodsToEvictPerNode: &uint0,
},
{
@@ -211,8 +254,11 @@ func TestRemovePodsViolatingNodeAffinity(t *testing.T) {
args: RemovePodsViolatingNodeAffinityArgs{
NodeAffinityType: []string{"requiredDuringSchedulingIgnoredDuringExecution"},
},
pods: addPodsToNode(nodeWithoutLabels, &metav1.Time{}, "requiredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{nodeWithoutLabels, nodeWithLabels},
pods: addPodsToNode(nodeWithoutLabelsName, &metav1.Time{}, "requiredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{
buildTestNode(nodeWithoutLabelsName, nil),
buildTestNode(nodeWithLabelsName, setNodeDesiredNodeLabel),
},
maxPodsToEvictPerNode: &uint1,
},
{
@@ -221,8 +267,11 @@ func TestRemovePodsViolatingNodeAffinity(t *testing.T) {
args: RemovePodsViolatingNodeAffinityArgs{
NodeAffinityType: []string{"preferredDuringSchedulingIgnoredDuringExecution"},
},
pods: addPodsToNode(nodeWithoutLabels, &metav1.Time{}, "preferredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{nodeWithoutLabels, nodeWithLabels},
pods: addPodsToNode(nodeWithoutLabelsName, &metav1.Time{}, "preferredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{
buildTestNode(nodeWithoutLabelsName, nil),
buildTestNode(nodeWithLabelsName, setNodeDesiredNodeLabel),
},
maxPodsToEvictPerNode: &uint1,
},
{
@@ -231,8 +280,11 @@ func TestRemovePodsViolatingNodeAffinity(t *testing.T) {
args: RemovePodsViolatingNodeAffinityArgs{
NodeAffinityType: []string{"requiredDuringSchedulingIgnoredDuringExecution"},
},
pods: addPodsToNode(nodeWithoutLabels, nil, "requiredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{nodeWithoutLabels, nodeWithLabels},
pods: addPodsToNode(nodeWithoutLabelsName, nil, "requiredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{
buildTestNode(nodeWithoutLabelsName, nil),
buildTestNode(nodeWithLabelsName, setNodeDesiredNodeLabel),
},
maxNoOfPodsToEvictPerNamespace: &uint1,
},
{
@@ -241,8 +293,11 @@ func TestRemovePodsViolatingNodeAffinity(t *testing.T) {
args: RemovePodsViolatingNodeAffinityArgs{
NodeAffinityType: []string{"requiredDuringSchedulingIgnoredDuringExecution"},
},
pods: addPodsToNode(nodeWithoutLabels, nil, "requiredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{nodeWithoutLabels, nodeWithLabels},
pods: addPodsToNode(nodeWithoutLabelsName, nil, "requiredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{
buildTestNode(nodeWithoutLabelsName, nil),
buildTestNode(nodeWithLabelsName, setNodeDesiredNodeLabel),
},
maxNoOfPodsToEvictPerNamespace: &uint1,
maxNoOfPodsToEvictTotal: &uint0,
},
@@ -252,8 +307,11 @@ func TestRemovePodsViolatingNodeAffinity(t *testing.T) {
args: RemovePodsViolatingNodeAffinityArgs{
NodeAffinityType: []string{"preferredDuringSchedulingIgnoredDuringExecution"},
},
pods: addPodsToNode(nodeWithoutLabels, nil, "preferredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{nodeWithoutLabels, nodeWithLabels},
pods: addPodsToNode(nodeWithoutLabelsName, nil, "preferredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{
buildTestNode(nodeWithoutLabelsName, nil),
buildTestNode(nodeWithLabelsName, setNodeDesiredNodeLabel),
},
maxNoOfPodsToEvictPerNamespace: &uint1,
},
{
@@ -262,8 +320,11 @@ func TestRemovePodsViolatingNodeAffinity(t *testing.T) {
args: RemovePodsViolatingNodeAffinityArgs{
NodeAffinityType: []string{"requiredDuringSchedulingIgnoredDuringExecution"},
},
pods: addPodsToNode(nodeWithoutLabels, nil, "requiredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{nodeWithoutLabels, nodeWithLabels},
pods: addPodsToNode(nodeWithoutLabelsName, nil, "requiredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{
buildTestNode(nodeWithoutLabelsName, nil),
buildTestNode(nodeWithLabelsName, setNodeDesiredNodeLabel),
},
maxNoOfPodsToEvictPerNamespace: &uint0,
},
{
@@ -272,8 +333,11 @@ func TestRemovePodsViolatingNodeAffinity(t *testing.T) {
args: RemovePodsViolatingNodeAffinityArgs{
NodeAffinityType: []string{"preferredDuringSchedulingIgnoredDuringExecution"},
},
pods: addPodsToNode(nodeWithoutLabels, nil, "preferredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{nodeWithoutLabels, nodeWithLabels},
pods: addPodsToNode(nodeWithoutLabelsName, nil, "preferredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{
buildTestNode(nodeWithoutLabelsName, nil),
buildTestNode(nodeWithLabelsName, setNodeDesiredNodeLabel),
},
maxNoOfPodsToEvictPerNamespace: &uint0,
},
{
@@ -282,8 +346,11 @@ func TestRemovePodsViolatingNodeAffinity(t *testing.T) {
args: RemovePodsViolatingNodeAffinityArgs{
NodeAffinityType: []string{"requiredDuringSchedulingIgnoredDuringExecution"},
},
pods: addPodsToNode(nodeWithoutLabels, &metav1.Time{}, "requiredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{nodeWithoutLabels, nodeWithLabels},
pods: addPodsToNode(nodeWithoutLabelsName, &metav1.Time{}, "requiredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{
buildTestNode(nodeWithoutLabelsName, nil),
buildTestNode(nodeWithLabelsName, setNodeDesiredNodeLabel),
},
maxNoOfPodsToEvictPerNamespace: &uint1,
},
{
@@ -292,8 +359,11 @@ func TestRemovePodsViolatingNodeAffinity(t *testing.T) {
args: RemovePodsViolatingNodeAffinityArgs{
NodeAffinityType: []string{"preferredDuringSchedulingIgnoredDuringExecution"},
},
pods: addPodsToNode(nodeWithoutLabels, &metav1.Time{}, "preferredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{nodeWithoutLabels, nodeWithLabels},
pods: addPodsToNode(nodeWithoutLabelsName, &metav1.Time{}, "preferredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{
buildTestNode(nodeWithoutLabelsName, nil),
buildTestNode(nodeWithLabelsName, setNodeDesiredNodeLabel),
},
maxNoOfPodsToEvictPerNamespace: &uint1,
},
{
@@ -302,8 +372,11 @@ func TestRemovePodsViolatingNodeAffinity(t *testing.T) {
args: RemovePodsViolatingNodeAffinityArgs{
NodeAffinityType: []string{"requiredDuringSchedulingIgnoredDuringExecution"},
},
pods: addPodsToNode(nodeWithoutLabels, nil, "requiredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{nodeWithoutLabels, unschedulableNodeWithLabels},
pods: addPodsToNode(nodeWithoutLabelsName, nil, "requiredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{
buildTestNode(nodeWithoutLabelsName, nil),
buildUnschedulableNodeWithLabels(),
},
nodefit: true,
},
{
@@ -312,8 +385,11 @@ func TestRemovePodsViolatingNodeAffinity(t *testing.T) {
args: RemovePodsViolatingNodeAffinityArgs{
NodeAffinityType: []string{"preferredDuringSchedulingIgnoredDuringExecution"},
},
pods: addPodsToNode(nodeWithoutLabels, nil, "preferredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{nodeWithoutLabels, unschedulableNodeWithLabels},
pods: addPodsToNode(nodeWithoutLabelsName, nil, "preferredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{
buildTestNode(nodeWithoutLabelsName, nil),
buildUnschedulableNodeWithLabels(),
},
nodefit: true,
},
{
@@ -322,8 +398,11 @@ func TestRemovePodsViolatingNodeAffinity(t *testing.T) {
args: RemovePodsViolatingNodeAffinityArgs{
NodeAffinityType: []string{"requiredDuringSchedulingIgnoredDuringExecution"},
},
pods: addPodsToNode(nodeWithoutLabels, nil, "requiredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{nodeWithLabels, unschedulableNodeWithLabels},
pods: addPodsToNode(nodeWithoutLabelsName, nil, "requiredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{
buildTestNode(nodeWithLabelsName, setNodeDesiredNodeLabel),
buildUnschedulableNodeWithLabels(),
},
maxPodsToEvictPerNode: &uint1,
nodefit: true,
},
@@ -333,8 +412,11 @@ func TestRemovePodsViolatingNodeAffinity(t *testing.T) {
args: RemovePodsViolatingNodeAffinityArgs{
NodeAffinityType: []string{"preferredDuringSchedulingIgnoredDuringExecution"},
},
pods: addPodsToNode(nodeWithoutLabels, nil, "preferredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{nodeWithLabels, unschedulableNodeWithLabels},
pods: addPodsToNode(nodeWithoutLabelsName, nil, "preferredDuringSchedulingIgnoredDuringExecution"),
nodes: []*v1.Node{
buildTestNode(nodeWithLabelsName, setNodeDesiredNodeLabel),
buildUnschedulableNodeWithLabels(),
},
maxPodsToEvictPerNode: &uint1,
nodefit: true,
},
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
@@ -107,7 +107,8 @@ func (d *RemovePodsViolatingNodeTaints) Name() string {
// Deschedule extension point implementation for the plugin
func (d *RemovePodsViolatingNodeTaints) Deschedule(ctx context.Context, nodes []*v1.Node) *frameworktypes.Status {
logger := klog.FromContext(klog.NewContext(ctx, d.logger)).WithValues("ExtensionPoint", frameworktypes.DescheduleExtensionPoint)
ctx = klog.NewContext(ctx, d.logger)
logger := klog.FromContext(ctx).WithValues("ExtensionPoint", frameworktypes.DescheduleExtensionPoint)
for _, node := range nodes {
pods, err := podutil.ListPodsOnANode(node.Name, d.handle.GetPodsAssignedToNodeFunc(), d.podFilter)
logger.V(1).Info("Processing node", "node", klog.KObj(node))
@@ -120,11 +121,7 @@ func (d *RemovePodsViolatingNodeTaints) Deschedule(ctx context.Context, nodes []
totalPods := len(pods)
loop:
for i := 0; i < totalPods; i++ {
if !utils.TolerationsTolerateTaintsWithFilter(
pods[i].Spec.Tolerations,
node.Spec.Taints,
d.taintFilterFnc,
) {
if !utils.TolerationsTolerateTaintsWithFilter(ctx, pods[i].Spec.Tolerations, node.Spec.Taints, d.taintFilterFnc) {
logger.V(2).Info("Not all taints with NoSchedule effect are tolerated after update for pod on node", "pod", klog.KObj(pods[i]), "node", klog.KObj(node))
err := d.handle.Evictor().Evict(ctx, pods[i], evictions.EvictOptions{StrategyName: PluginName})
if err == nil {
@@ -22,9 +22,9 @@ import (
"testing"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/klog/v2"
"sigs.k8s.io/descheduler/pkg/descheduler/evictions"
"sigs.k8s.io/descheduler/pkg/framework/plugins/defaultevictor"
@@ -34,6 +34,37 @@ import (
"sigs.k8s.io/descheduler/test"
)
const (
nodeName1 = "n1"
nodeName2 = "n2"
nodeName3 = "n3"
nodeName4 = "n4"
nodeName5 = "n5"
nodeName6 = "n6"
nodeName7 = "n7"
datacenterLabel = "datacenter"
datacenterEast = "east"
datacenterWest = "west"
)
func buildTestNode(name string, apply func(*v1.Node)) *v1.Node {
return test.BuildTestNode(name, 2000, 3000, 10, apply)
}
func buildTestPod(name, nodeName string, apply func(*v1.Pod)) *v1.Pod {
return test.BuildTestPod(name, 100, 0, nodeName, apply)
}
func buildTestPodWithNormalOwnerRef(name, nodeName string, apply func(*v1.Pod)) *v1.Pod {
return buildTestPod(name, nodeName, func(pod *v1.Pod) {
test.SetNormalOwnerRef(pod)
if apply != nil {
apply(pod)
}
})
}
func createNoScheduleTaint(key, value string, index int) v1.Taint {
return v1.Taint{
Key: "testTaint" + fmt.Sprintf("%v", index),
@@ -50,13 +81,39 @@ func createPreferNoScheduleTaint(key, value string, index int) v1.Taint {
}
}
func addTaintsToNode(node *v1.Node, key, value string, indices []int) *v1.Node {
taints := []v1.Taint{}
for _, index := range indices {
taints = append(taints, createNoScheduleTaint(key, value, index))
func withTestTaint1(node *v1.Node) {
node.Spec.Taints = []v1.Taint{
createNoScheduleTaint("testTaint", "test", 1),
}
}
func withTestingTaint1(node *v1.Node) {
node.Spec.Taints = []v1.Taint{
createNoScheduleTaint("testingTaint", "testing", 1),
}
}
func withBothTaints1(node *v1.Node) {
node.Spec.Taints = []v1.Taint{
createNoScheduleTaint("testTaint", "test", 1),
createNoScheduleTaint("testingTaint", "testing", 1),
}
}
func withDatacenterEastLabel(node *v1.Node) {
node.ObjectMeta.Labels = map[string]string{
datacenterLabel: datacenterEast,
}
}
func withUnschedulable(node *v1.Node) {
node.Spec.Unschedulable = true
}
func withPreferNoScheduleTestTaint1(node *v1.Node) {
node.Spec.Taints = []v1.Taint{
createPreferNoScheduleTaint("testTaint", "test", 1),
}
node.Spec.Taints = taints
return node
}
func addTolerationToPod(pod *v1.Pod, key, value string, index int, effect v1.TaintEffect) *v1.Pod {
@@ -69,111 +126,24 @@ func addTolerationToPod(pod *v1.Pod, key, value string, index int, effect v1.Tai
return pod
}
func withTestTaintToleration1(pod *v1.Pod) {
addTolerationToPod(pod, "testTaint", "test", 1, v1.TaintEffectNoSchedule)
}
func withTestTaintXToleration1(pod *v1.Pod) {
addTolerationToPod(pod, "testTaintX", "testX", 1, v1.TaintEffectNoSchedule)
}
func withLocalStorageVolume(pod *v1.Pod) {
test.SetHostPathEmptyDirVolumeSource(pod)
}
func withKubeSystemCriticalPod(pod *v1.Pod) {
pod.Namespace = "kube-system"
test.SetPodPriority(pod, utils.SystemCriticalPriority)
}
func TestDeletePodsViolatingNodeTaints(t *testing.T) {
node1 := test.BuildTestNode("n1", 2000, 3000, 10, nil)
node1 = addTaintsToNode(node1, "testTaint", "test", []int{1})
node2 := test.BuildTestNode("n2", 2000, 3000, 10, nil)
node2 = addTaintsToNode(node2, "testingTaint", "testing", []int{1})
node3 := test.BuildTestNode("n3", 2000, 3000, 10, func(node *v1.Node) {
node.ObjectMeta.Labels = map[string]string{
"datacenter": "east",
}
})
node4 := test.BuildTestNode("n4", 2000, 3000, 10, func(node *v1.Node) {
node.Spec = v1.NodeSpec{
Unschedulable: true,
}
})
node5 := test.BuildTestNode("n5", 2000, 3000, 10, nil)
node5.Spec.Taints = []v1.Taint{
createPreferNoScheduleTaint("testTaint", "test", 1),
}
node6 := test.BuildTestNode("n6", 1, 1, 1, nil)
node6.Spec.Taints = []v1.Taint{
createPreferNoScheduleTaint("testTaint", "test", 1),
}
node7 := test.BuildTestNode("n7", 2000, 3000, 10, nil)
node7 = addTaintsToNode(node7, "testTaint", "test", []int{1})
node7 = addTaintsToNode(node7, "testingTaint", "testing", []int{1})
p1 := test.BuildTestPod("p1", 100, 0, node1.Name, nil)
p2 := test.BuildTestPod("p2", 100, 0, node1.Name, nil)
p3 := test.BuildTestPod("p3", 100, 0, node1.Name, nil)
p4 := test.BuildTestPod("p4", 100, 0, node1.Name, nil)
p5 := test.BuildTestPod("p5", 100, 0, node1.Name, nil)
p6 := test.BuildTestPod("p6", 100, 0, node1.Name, nil)
p7 := test.BuildTestPod("p7", 100, 0, node2.Name, nil)
p8 := test.BuildTestPod("p8", 100, 0, node2.Name, nil)
p9 := test.BuildTestPod("p9", 100, 0, node2.Name, nil)
p10 := test.BuildTestPod("p10", 100, 0, node2.Name, nil)
p11 := test.BuildTestPod("p11", 100, 0, node2.Name, nil)
p12 := test.BuildTestPod("p11", 100, 0, node2.Name, nil)
p1.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
p2.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
p3.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
p4.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
p5.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
p6.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
p7.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
p8.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
p9.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
p10.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
p11.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
p12.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
// The following 4 pods won't get evicted.
// A Critical Pod.
p7.Namespace = "kube-system"
priority := utils.SystemCriticalPriority
p7.Spec.Priority = &priority
p7.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
// A daemonset.
p8.ObjectMeta.OwnerReferences = test.GetDaemonSetOwnerRefList()
// A pod with local storage.
p9.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
p9.Spec.Volumes = []v1.Volume{
{
Name: "sample",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: "somePath"},
EmptyDir: &v1.EmptyDirVolumeSource{
SizeLimit: resource.NewQuantity(int64(10), resource.BinarySI),
},
},
},
}
// A Mirror Pod.
p10.Annotations = test.GetMirrorPodAnnotation()
p1 = addTolerationToPod(p1, "testTaint", "test", 1, v1.TaintEffectNoSchedule)
p3 = addTolerationToPod(p3, "testTaint", "test", 1, v1.TaintEffectNoSchedule)
p4 = addTolerationToPod(p4, "testTaintX", "testX", 1, v1.TaintEffectNoSchedule)
p12.Spec.NodeSelector = map[string]string{
"datacenter": "west",
}
p13 := test.BuildTestPod("p13", 100, 0, node5.Name, nil)
p13.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
// node5 has PreferNoSchedule:testTaint1=test1, so the p13 has to have
// PreferNoSchedule:testTaint0=test0 so the pod is not tolarated
p13 = addTolerationToPod(p13, "testTaint", "test", 0, v1.TaintEffectPreferNoSchedule)
p14 := test.BuildTestPod("p14", 100, 0, node7.Name, nil)
p14.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
p14 = addTolerationToPod(p14, "testTaint", "test", 1, v1.TaintEffectNoSchedule)
p15 := test.BuildTestPod("p15", 100, 0, node7.Name, nil)
p15.ObjectMeta.OwnerReferences = test.GetNormalPodOwnerRefList()
p15 = addTolerationToPod(p15, "testTaint", "test", 1, v1.TaintEffectNoSchedule)
p15 = addTolerationToPod(p15, "testingTaint", "testing", 1, v1.TaintEffectNoSchedule)
var uint1, uint2 uint = 1, 2
tests := []struct {
@@ -192,203 +162,306 @@ func TestDeletePodsViolatingNodeTaints(t *testing.T) {
includedTaints []string
}{
{
description: "Pods not tolerating node taint should be evicted",
pods: []*v1.Pod{p1, p2, p3},
nodes: []*v1.Node{node1},
evictLocalStoragePods: false,
evictSystemCriticalPods: false,
description: "Pods not tolerating node taint should be evicted",
pods: []*v1.Pod{
buildTestPodWithNormalOwnerRef("p1", nodeName1, withTestTaintToleration1),
buildTestPodWithNormalOwnerRef("p2", nodeName1, nil),
buildTestPodWithNormalOwnerRef("p3", nodeName1, withTestTaintToleration1),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, withTestTaint1),
},
expectedEvictedPodCount: 1, // p2 gets evicted
},
{
description: "Pods with tolerations but not tolerating node taint should be evicted",
pods: []*v1.Pod{p1, p3, p4},
nodes: []*v1.Node{node1},
evictLocalStoragePods: false,
evictSystemCriticalPods: false,
description: "Pods with tolerations but not tolerating node taint should be evicted",
pods: []*v1.Pod{
buildTestPodWithNormalOwnerRef("p1", nodeName1, withTestTaintToleration1),
buildTestPodWithNormalOwnerRef("p3", nodeName1, withTestTaintToleration1),
buildTestPodWithNormalOwnerRef("p4", nodeName1, withTestTaintXToleration1),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, withTestTaint1),
},
expectedEvictedPodCount: 1, // p4 gets evicted
},
{
description: "Only <maxNoOfPodsToEvictTotal> number of Pods not tolerating node taint should be evicted",
pods: []*v1.Pod{p1, p5, p6},
nodes: []*v1.Node{node1},
evictLocalStoragePods: false,
evictSystemCriticalPods: false,
description: "Only <maxNoOfPodsToEvictTotal> number of Pods not tolerating node taint should be evicted",
pods: []*v1.Pod{
buildTestPodWithNormalOwnerRef("p1", nodeName1, withTestTaintToleration1),
buildTestPodWithNormalOwnerRef("p5", nodeName1, nil),
buildTestPodWithNormalOwnerRef("p6", nodeName1, nil),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, withTestTaint1),
},
maxPodsToEvictPerNode: &uint2,
maxNoOfPodsToEvictTotal: &uint1,
expectedEvictedPodCount: 1, // p5 or p6 gets evicted
},
{
description: "Only <maxPodsToEvictPerNode> number of Pods not tolerating node taint should be evicted",
pods: []*v1.Pod{p1, p5, p6},
nodes: []*v1.Node{node1},
evictLocalStoragePods: false,
evictSystemCriticalPods: false,
description: "Only <maxPodsToEvictPerNode> number of Pods not tolerating node taint should be evicted",
pods: []*v1.Pod{
buildTestPodWithNormalOwnerRef("p1", nodeName1, withTestTaintToleration1),
buildTestPodWithNormalOwnerRef("p5", nodeName1, nil),
buildTestPodWithNormalOwnerRef("p6", nodeName1, nil),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, withTestTaint1),
},
maxPodsToEvictPerNode: &uint1,
expectedEvictedPodCount: 1, // p5 or p6 gets evicted
},
{
description: "Only <maxNoOfPodsToEvictPerNamespace> number of Pods not tolerating node taint should be evicted",
pods: []*v1.Pod{p1, p5, p6},
nodes: []*v1.Node{node1},
evictLocalStoragePods: false,
evictSystemCriticalPods: false,
description: "Only <maxNoOfPodsToEvictPerNamespace> number of Pods not tolerating node taint should be evicted",
pods: []*v1.Pod{
buildTestPodWithNormalOwnerRef("p1", nodeName1, withTestTaintToleration1),
buildTestPodWithNormalOwnerRef("p5", nodeName1, nil),
buildTestPodWithNormalOwnerRef("p6", nodeName1, nil),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, withTestTaint1),
},
maxNoOfPodsToEvictPerNamespace: &uint1,
expectedEvictedPodCount: 1, // p5 or p6 gets evicted
},
{
description: "Only <maxNoOfPodsToEvictPerNamespace> number of Pods not tolerating node taint should be evicted",
pods: []*v1.Pod{p1, p5, p6},
nodes: []*v1.Node{node1},
evictLocalStoragePods: false,
evictSystemCriticalPods: false,
description: "Only <maxNoOfPodsToEvictPerNamespace> number of Pods not tolerating node taint should be evicted",
pods: []*v1.Pod{
buildTestPodWithNormalOwnerRef("p1", nodeName1, withTestTaintToleration1),
buildTestPodWithNormalOwnerRef("p5", nodeName1, nil),
buildTestPodWithNormalOwnerRef("p6", nodeName1, nil),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, withTestTaint1),
},
maxNoOfPodsToEvictPerNamespace: &uint1,
expectedEvictedPodCount: 1, // p5 or p6 gets evicted
},
{
description: "Critical pods not tolerating node taint should not be evicted",
pods: []*v1.Pod{p7, p8, p9, p10},
nodes: []*v1.Node{node2},
evictLocalStoragePods: false,
evictSystemCriticalPods: false,
description: "Critical pods not tolerating node taint should not be evicted",
pods: []*v1.Pod{
buildTestPodWithNormalOwnerRef("p7", nodeName2, withKubeSystemCriticalPod),
buildTestPod("p8", nodeName2, test.SetDSOwnerRef),
buildTestPodWithNormalOwnerRef("p9", nodeName2, withLocalStorageVolume),
buildTestPodWithNormalOwnerRef("p10", nodeName2, test.SetMirrorPodAnnotation),
},
nodes: []*v1.Node{
buildTestNode(nodeName2, withTestingTaint1),
},
expectedEvictedPodCount: 0, // nothing is evicted
},
{
description: "Critical pods except storage pods not tolerating node taint should not be evicted",
pods: []*v1.Pod{p7, p8, p9, p10},
nodes: []*v1.Node{node2},
description: "Critical pods except storage pods not tolerating node taint should not be evicted",
pods: []*v1.Pod{
buildTestPodWithNormalOwnerRef("p7", nodeName2, withKubeSystemCriticalPod),
buildTestPod("p8", nodeName2, test.SetDSOwnerRef),
buildTestPodWithNormalOwnerRef("p9", nodeName2, withLocalStorageVolume),
buildTestPodWithNormalOwnerRef("p10", nodeName2, test.SetMirrorPodAnnotation),
},
nodes: []*v1.Node{
buildTestNode(nodeName2, withTestingTaint1),
},
evictLocalStoragePods: true,
evictSystemCriticalPods: false,
expectedEvictedPodCount: 1, // p9 gets evicted
},
{
description: "Critical and non critical pods, only non critical pods not tolerating node taint should be evicted",
pods: []*v1.Pod{p7, p8, p10, p11},
nodes: []*v1.Node{node2},
evictLocalStoragePods: false,
evictSystemCriticalPods: false,
description: "Critical and non critical pods, only non critical pods not tolerating node taint should be evicted",
pods: []*v1.Pod{
buildTestPodWithNormalOwnerRef("p7", nodeName2, withKubeSystemCriticalPod),
buildTestPod("p8", nodeName2, test.SetDSOwnerRef),
buildTestPodWithNormalOwnerRef("p10", nodeName2, test.SetMirrorPodAnnotation),
buildTestPodWithNormalOwnerRef("p11", nodeName2, nil),
},
nodes: []*v1.Node{
buildTestNode(nodeName2, withTestingTaint1),
},
expectedEvictedPodCount: 1, // p11 gets evicted
},
{
description: "Critical and non critical pods, pods not tolerating node taint should be evicted even if they are critical",
pods: []*v1.Pod{p2, p7, p9, p10},
nodes: []*v1.Node{node1, node2},
evictLocalStoragePods: false,
description: "Critical and non critical pods, pods not tolerating node taint should be evicted even if they are critical",
pods: []*v1.Pod{
buildTestPodWithNormalOwnerRef("p2", nodeName1, nil),
buildTestPodWithNormalOwnerRef("p7", nodeName2, withKubeSystemCriticalPod),
buildTestPodWithNormalOwnerRef("p9", nodeName2, withLocalStorageVolume),
buildTestPodWithNormalOwnerRef("p10", nodeName2, test.SetMirrorPodAnnotation),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, withTestTaint1),
buildTestNode(nodeName2, withTestingTaint1),
},
evictSystemCriticalPods: true,
expectedEvictedPodCount: 2, // p2 and p7 are evicted
},
{
description: "Pod p2 doesn't tolerate taint on it's node, but also doesn't tolerate taints on other nodes",
pods: []*v1.Pod{p1, p2, p3},
nodes: []*v1.Node{node1, node2},
evictLocalStoragePods: false,
evictSystemCriticalPods: false,
description: "Pod p2 doesn't tolerate taint on it's node, but also doesn't tolerate taints on other nodes",
pods: []*v1.Pod{
buildTestPodWithNormalOwnerRef("p1", nodeName1, withTestTaintToleration1),
buildTestPodWithNormalOwnerRef("p2", nodeName1, nil),
buildTestPodWithNormalOwnerRef("p3", nodeName1, withTestTaintToleration1),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, withTestTaint1),
buildTestNode(nodeName2, withTestingTaint1),
},
expectedEvictedPodCount: 0, // p2 gets evicted
nodeFit: true,
},
{
description: "Pod p12 doesn't tolerate taint on it's node, but other nodes don't match it's selector",
pods: []*v1.Pod{p1, p3, p12},
nodes: []*v1.Node{node1, node3},
evictLocalStoragePods: false,
evictSystemCriticalPods: false,
description: "Pod p12 doesn't tolerate taint on it's node, but other nodes don't match it's selector",
pods: []*v1.Pod{
buildTestPodWithNormalOwnerRef("p1", nodeName1, withTestTaintToleration1),
buildTestPodWithNormalOwnerRef("p3", nodeName1, withTestTaintToleration1),
buildTestPodWithNormalOwnerRef("p11", nodeName2, func(pod *v1.Pod) {
pod.Spec.NodeSelector = map[string]string{
datacenterLabel: datacenterWest,
}
}),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, withTestTaint1),
buildTestNode(nodeName3, withDatacenterEastLabel),
},
expectedEvictedPodCount: 0, // p2 gets evicted
nodeFit: true,
},
{
description: "Pod p2 doesn't tolerate taint on it's node, but other nodes are unschedulable",
pods: []*v1.Pod{p1, p2, p3},
nodes: []*v1.Node{node1, node4},
evictLocalStoragePods: false,
evictSystemCriticalPods: false,
description: "Pod p2 doesn't tolerate taint on it's node, but other nodes are unschedulable",
pods: []*v1.Pod{
buildTestPodWithNormalOwnerRef("p1", nodeName1, withTestTaintToleration1),
buildTestPodWithNormalOwnerRef("p2", nodeName1, nil),
buildTestPodWithNormalOwnerRef("p3", nodeName1, withTestTaintToleration1),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, withTestTaint1),
buildTestNode(nodeName4, withUnschedulable),
},
expectedEvictedPodCount: 0, // p2 gets evicted
nodeFit: true,
},
{
description: "Pods not tolerating PreferNoSchedule node taint should not be evicted when not enabled",
pods: []*v1.Pod{p13},
nodes: []*v1.Node{node5},
evictLocalStoragePods: false,
evictSystemCriticalPods: false,
description: "Pods not tolerating PreferNoSchedule node taint should not be evicted when not enabled",
pods: []*v1.Pod{
buildTestPodWithNormalOwnerRef("p13", nodeName5, func(pod *v1.Pod) {
addTolerationToPod(pod, "testTaint", "test", 0, v1.TaintEffectPreferNoSchedule)
}),
},
nodes: []*v1.Node{
buildTestNode(nodeName5, withPreferNoScheduleTestTaint1),
},
expectedEvictedPodCount: 0,
},
{
description: "Pods not tolerating PreferNoSchedule node taint should be evicted when enabled",
pods: []*v1.Pod{p13},
nodes: []*v1.Node{node5},
evictLocalStoragePods: false,
evictSystemCriticalPods: false,
description: "Pods not tolerating PreferNoSchedule node taint should be evicted when enabled",
pods: []*v1.Pod{
buildTestPodWithNormalOwnerRef("p13", nodeName5, func(pod *v1.Pod) {
addTolerationToPod(pod, "testTaint", "test", 0, v1.TaintEffectPreferNoSchedule)
}),
},
nodes: []*v1.Node{
buildTestNode(nodeName5, withPreferNoScheduleTestTaint1),
},
includePreferNoSchedule: true,
expectedEvictedPodCount: 1, // p13 gets evicted
},
{
description: "Pods not tolerating excluded node taints (by key) should not be evicted",
pods: []*v1.Pod{p2},
nodes: []*v1.Node{node1},
evictLocalStoragePods: false,
evictSystemCriticalPods: false,
description: "Pods not tolerating excluded node taints (by key) should not be evicted",
pods: []*v1.Pod{
buildTestPodWithNormalOwnerRef("p2", nodeName1, nil),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, withTestTaint1),
},
excludedTaints: []string{"excludedTaint1", "testTaint1"},
expectedEvictedPodCount: 0, // nothing gets evicted, as one of the specified excludedTaints matches the key of node1's taint
},
{
description: "Pods not tolerating excluded node taints (by key and value) should not be evicted",
pods: []*v1.Pod{p2},
nodes: []*v1.Node{node1},
evictLocalStoragePods: false,
evictSystemCriticalPods: false,
description: "Pods not tolerating excluded node taints (by key and value) should not be evicted",
pods: []*v1.Pod{
buildTestPodWithNormalOwnerRef("p2", nodeName1, nil),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, withTestTaint1),
},
excludedTaints: []string{"testTaint1=test1"},
expectedEvictedPodCount: 0, // nothing gets evicted, as both the key and value of the excluded taint match node1's taint
},
{
description: "The excluded taint matches the key of node1's taint, but does not match the value",
pods: []*v1.Pod{p2},
nodes: []*v1.Node{node1},
evictLocalStoragePods: false,
evictSystemCriticalPods: false,
description: "The excluded taint matches the key of node1's taint, but does not match the value",
pods: []*v1.Pod{
buildTestPodWithNormalOwnerRef("p2", nodeName1, nil),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, withTestTaint1),
},
excludedTaints: []string{"testTaint1=test2"},
expectedEvictedPodCount: 1, // pod gets evicted, as excluded taint value does not match node1's taint value
},
{
description: "Critical and non critical pods, pods not tolerating node taint can't be evicted because the only available node does not have enough resources.",
pods: []*v1.Pod{p2, p7, p9, p10},
nodes: []*v1.Node{node1, node6},
evictLocalStoragePods: false,
description: "Critical and non critical pods, pods not tolerating node taint can't be evicted because the only available node does not have enough resources.",
pods: []*v1.Pod{
buildTestPodWithNormalOwnerRef("p2", nodeName1, nil),
buildTestPodWithNormalOwnerRef("p7", nodeName2, withKubeSystemCriticalPod),
buildTestPodWithNormalOwnerRef("p9", nodeName2, withLocalStorageVolume),
buildTestPodWithNormalOwnerRef("p10", nodeName2, test.SetMirrorPodAnnotation),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, withTestTaint1),
test.BuildTestNode(nodeName6, 1, 1, 1, withPreferNoScheduleTestTaint1),
},
evictSystemCriticalPods: true,
expectedEvictedPodCount: 0, // p2 and p7 can't be evicted
nodeFit: true,
},
{
description: "Pods tolerating included taints should not get evicted even with other taints present",
pods: []*v1.Pod{p1},
nodes: []*v1.Node{node7},
evictLocalStoragePods: false,
evictSystemCriticalPods: false,
description: "Pods tolerating included taints should not get evicted even with other taints present",
pods: []*v1.Pod{
buildTestPodWithNormalOwnerRef("p1", nodeName1, withTestTaintToleration1),
},
nodes: []*v1.Node{
buildTestNode(nodeName7, withBothTaints1),
},
includedTaints: []string{"testTaint1=test1"},
expectedEvictedPodCount: 0, // nothing gets evicted, as p1 tolerates the included taint, and taint "testingTaint1=testing1" is not included
},
{
description: "Pods not tolerating not included taints should not get evicted",
pods: []*v1.Pod{p1, p2, p4},
nodes: []*v1.Node{node1},
evictLocalStoragePods: false,
evictSystemCriticalPods: false,
description: "Pods not tolerating not included taints should not get evicted",
pods: []*v1.Pod{
buildTestPodWithNormalOwnerRef("p1", nodeName1, withTestTaintToleration1),
buildTestPodWithNormalOwnerRef("p2", nodeName1, nil),
buildTestPodWithNormalOwnerRef("p4", nodeName1, withTestTaintXToleration1),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, withTestTaint1),
},
includedTaints: []string{"testTaint2=test2"},
expectedEvictedPodCount: 0, // nothing gets evicted, as taint is not included, even though the pods' p2 and p4 tolerations do not match node1's taint
},
{
description: "Pods tolerating includedTaint should not get evicted. Pods not tolerating includedTaints should get evicted",
pods: []*v1.Pod{p1, p2, p3},
nodes: []*v1.Node{node1},
evictLocalStoragePods: false,
evictSystemCriticalPods: false,
description: "Pods tolerating includedTaint should not get evicted. Pods not tolerating includedTaints should get evicted",
pods: []*v1.Pod{
buildTestPodWithNormalOwnerRef("p1", nodeName1, withTestTaintToleration1),
buildTestPodWithNormalOwnerRef("p2", nodeName1, nil),
buildTestPodWithNormalOwnerRef("p3", nodeName1, withTestTaintToleration1),
},
nodes: []*v1.Node{
buildTestNode(nodeName1, withTestTaint1),
},
includedTaints: []string{"testTaint1=test1"},
expectedEvictedPodCount: 1, // node1 taint is included. p1 and p3 tolerate the included taint, p2 gets evicted
},
{
description: "Pods not tolerating all taints are evicted when includedTaints is empty",
pods: []*v1.Pod{p14, p15},
nodes: []*v1.Node{node7},
evictLocalStoragePods: false,
evictSystemCriticalPods: false,
description: "Pods not tolerating all taints are evicted when includedTaints is empty",
pods: []*v1.Pod{
buildTestPodWithNormalOwnerRef("p14", nodeName7, withTestTaintToleration1),
buildTestPodWithNormalOwnerRef("p15", nodeName7, func(pod *v1.Pod) {
withTestTaintToleration1(pod)
addTolerationToPod(pod, "testingTaint", "testing", 1, v1.TaintEffectNoSchedule)
}),
},
nodes: []*v1.Node{
buildTestNode(nodeName7, withBothTaints1),
},
expectedEvictedPodCount: 1, // includedTaints is empty so all taints are included. p15 tolerates both node taints and does not get evicted. p14 tolerate only one and gets evicted
},
}
@@ -539,8 +612,9 @@ func TestToleratesTaint(t *testing.T) {
expectTolerated: false,
},
}
logger := klog.FromContext(t.Context())
for _, tc := range testCases {
if tolerated := tc.toleration.ToleratesTaint(&tc.taint); tc.expectTolerated != tolerated {
if tolerated := tc.toleration.ToleratesTaint(logger, &tc.taint, true); tc.expectTolerated != tolerated {
t.Errorf("[%s] expect %v, got %v: toleration %+v, taint %s", tc.description, tc.expectTolerated, tolerated, tc.toleration, tc.taint.ToString())
}
}
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
@@ -185,7 +185,7 @@ func (d *RemovePodsViolatingTopologySpreadConstraint) Balance(ctx context.Contex
// (we can't just build it from existing pods' nodes because a topology may have 0 pods)
for _, node := range nodeMap {
if val, ok := node.Labels[tsc.TopologyKey]; ok {
if matchNodeInclusionPolicies(tsc, node) {
if matchNodeInclusionPolicies(ctx, tsc, node) {
constraintTopologies[topologyPair{key: tsc.TopologyKey, value: val}] = make([]*v1.Pod, 0)
}
}
@@ -224,7 +224,7 @@ func (d *RemovePodsViolatingTopologySpreadConstraint) Balance(ctx context.Contex
logger.V(2).Info("Skipping topology constraint because it is already balanced", "constraint", tsc)
continue
}
d.balanceDomains(podsForEviction, tsc, constraintTopologies, sumPods, nodes)
d.balanceDomains(ctx, podsForEviction, tsc, constraintTopologies, sumPods, nodes)
}
}
@@ -305,20 +305,14 @@ func topologyIsBalanced(topology map[topologyPair][]*v1.Pod, tsc topologySpreadC
// Following this, the above topology domains end up "sorted" as:
// [5, 5, 5, 5, 5, 5]
// (assuming even distribution by the scheduler of the evicted pods)
func (d *RemovePodsViolatingTopologySpreadConstraint) balanceDomains(
podsForEviction map[*v1.Pod]struct{},
tsc topologySpreadConstraint,
constraintTopologies map[topologyPair][]*v1.Pod,
sumPods float64,
nodes []*v1.Node,
) {
func (d *RemovePodsViolatingTopologySpreadConstraint) balanceDomains(ctx context.Context, podsForEviction map[*v1.Pod]struct{}, tsc topologySpreadConstraint, constraintTopologies map[topologyPair][]*v1.Pod, sumPods float64, nodes []*v1.Node) {
idealAvg := sumPods / float64(len(constraintTopologies))
isEvictable := d.handle.Evictor().Filter
sortedDomains := sortDomains(constraintTopologies, isEvictable)
getPodsAssignedToNode := d.handle.GetPodsAssignedToNodeFunc()
topologyBalanceNodeFit := utilptr.Deref(d.args.TopologyBalanceNodeFit, true)
eligibleNodes := filterEligibleNodes(nodes, tsc)
eligibleNodes := filterEligibleNodes(ctx, nodes, tsc)
nodesBelowIdealAvg := filterNodesBelowIdealAvg(eligibleNodes, sortedDomains, tsc.TopologyKey, idealAvg)
// i is the index for belowOrEqualAvg
@@ -490,17 +484,17 @@ func doNotScheduleTaintsFilterFunc() func(t *v1.Taint) bool {
}
}
func filterEligibleNodes(nodes []*v1.Node, tsc topologySpreadConstraint) []*v1.Node {
func filterEligibleNodes(ctx context.Context, nodes []*v1.Node, tsc topologySpreadConstraint) []*v1.Node {
var eligibleNodes []*v1.Node
for _, node := range nodes {
if matchNodeInclusionPolicies(tsc, node) {
if matchNodeInclusionPolicies(ctx, tsc, node) {
eligibleNodes = append(eligibleNodes, node)
}
}
return eligibleNodes
}
func matchNodeInclusionPolicies(tsc topologySpreadConstraint, node *v1.Node) bool {
func matchNodeInclusionPolicies(ctx context.Context, tsc topologySpreadConstraint, node *v1.Node) bool {
if tsc.NodeAffinityPolicy == v1.NodeInclusionPolicyHonor {
// We ignore parsing errors here for backwards compatibility.
if match, _ := tsc.PodNodeAffinity.Match(node); !match {
@@ -508,8 +502,11 @@ func matchNodeInclusionPolicies(tsc topologySpreadConstraint, node *v1.Node) boo
}
}
logger := klog.FromContext(ctx)
if tsc.NodeTaintsPolicy == v1.NodeInclusionPolicyHonor {
if _, untolerated := v1helper.FindMatchingUntoleratedTaint(node.Spec.Taints, tsc.PodTolerations, doNotScheduleTaintsFilterFunc()); untolerated {
if _, untolerated := v1helper.FindMatchingUntoleratedTaint(
logger, node.Spec.Taints, tsc.PodTolerations, doNotScheduleTaintsFilterFunc(), true,
); untolerated {
return false
}
}
@@ -9,12 +9,10 @@ import (
appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
policy "k8s.io/api/policy/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/kubernetes/fake"
core "k8s.io/client-go/testing"
utilptr "k8s.io/utils/ptr"
"sigs.k8s.io/descheduler/pkg/api"
@@ -1490,18 +1488,7 @@ func TestTopologySpreadConstraint(t *testing.T) {
}
var evictedPods []string
fakeClient.PrependReactor("create", "pods", func(action core.Action) (bool, runtime.Object, error) {
if action.GetSubresource() == "eviction" {
createAct, matched := action.(core.CreateActionImpl)
if !matched {
return false, nil, fmt.Errorf("unable to convert action to core.CreateActionImpl")
}
if eviction, matched := createAct.Object.(*policy.Eviction); matched {
evictedPods = append(evictedPods, eviction.GetName())
}
}
return false, nil, nil // fallback to the default reactor
})
test.RegisterEvictedPodsCollector(fakeClient, &evictedPods)
SetDefaults_RemovePodsViolatingTopologySpreadConstraintArgs(&tc.args)
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
@@ -2,7 +2,7 @@
// +build !ignore_autogenerated
/*
Copyright 2025 The Kubernetes Authors.
Copyright 2026 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.
+53 -14
View File
@@ -69,7 +69,7 @@ func (ei *evictorImpl) Evict(ctx context.Context, pod *v1.Pod, opts evictions.Ev
// handleImpl implements the framework handle which gets passed to plugins
type handleImpl struct {
clientSet clientset.Interface
prometheusClient promapi.Client
prometheusClientGetter func() promapi.Client
metricsCollector *metricscollector.MetricsCollector
getPodsAssignedToNodeFunc podutil.GetPodsAssignedToNodeFunc
sharedInformerFactory informers.SharedInformerFactory
@@ -78,13 +78,25 @@ type handleImpl struct {
var _ frameworktypes.Handle = &handleImpl{}
// pluginHandle wraps a shared handleImpl and adds a plugin-specific instance ID
type pluginHandle struct {
*handleImpl
pluginInstanceID string
}
var _ frameworktypes.Handle = &pluginHandle{}
// ClientSet retrieves kube client set
func (hi *handleImpl) ClientSet() clientset.Interface {
return hi.clientSet
}
func (hi *handleImpl) PrometheusClient() promapi.Client {
return hi.prometheusClient
if hi.prometheusClientGetter == nil {
klog.V(3).Info("prometheusClientGetter is nil, returning nil Prometheus client")
return nil
}
return hi.prometheusClientGetter()
}
func (hi *handleImpl) MetricsCollector() *metricscollector.MetricsCollector {
@@ -106,6 +118,17 @@ func (hi *handleImpl) Evictor() frameworktypes.Evictor {
return hi.evictor
}
// PluginInstanceID returns an empty string for the base handle.
// Plugins should receive a pluginHandle which has a specific instance ID.
func (hi *handleImpl) PluginInstanceID() string {
panic(fmt.Errorf("not implemented"))
}
// PluginInstanceID returns a unique identifier for this plugin instance.
func (ph *pluginHandle) PluginInstanceID() string {
return ph.pluginInstanceID
}
type filterPlugin interface {
frameworktypes.Plugin
Filter(pod *v1.Pod) bool
@@ -137,11 +160,12 @@ type Option func(*handleImplOpts)
type handleImplOpts struct {
clientSet clientset.Interface
prometheusClient promapi.Client
prometheusClientGetter func() promapi.Client
sharedInformerFactory informers.SharedInformerFactory
getPodsAssignedToNodeFunc podutil.GetPodsAssignedToNodeFunc
podEvictor *evictions.PodEvictor
metricsCollector *metricscollector.MetricsCollector
profileInstanceID string
}
// WithClientSet sets clientSet for the scheduling frameworkImpl.
@@ -151,10 +175,10 @@ func WithClientSet(clientSet clientset.Interface) Option {
}
}
// WithPrometheusClient sets Prometheus client for the scheduling frameworkImpl.
func WithPrometheusClient(prometheusClient promapi.Client) Option {
// WithPrometheusClient sets Prometheus client getter for the scheduling frameworkImpl.
func WithPrometheusClient(prometheusClientGetter func() promapi.Client) Option {
return func(o *handleImplOpts) {
o.prometheusClient = prometheusClient
o.prometheusClientGetter = prometheusClientGetter
}
}
@@ -182,6 +206,14 @@ func WithMetricsCollector(metricsCollector *metricscollector.MetricsCollector) O
}
}
// WithProfileInstanceID sets the profile instance ID for the handle.
// This will be used to construct unique plugin instance IDs.
func WithProfileInstanceID(profileInstanceID string) Option {
return func(o *handleImplOpts) {
o.profileInstanceID = profileInstanceID
}
}
func getPluginConfig(pluginName string, pluginConfigs []api.PluginConfig) (*api.PluginConfig, int) {
for idx, pluginConfig := range pluginConfigs {
if pluginConfig.Name == pluginName {
@@ -191,7 +223,7 @@ func getPluginConfig(pluginName string, pluginConfigs []api.PluginConfig) (*api.
return nil, 0
}
func buildPlugin(ctx context.Context, config api.DeschedulerProfile, pluginName string, handle *handleImpl, reg pluginregistry.Registry) (frameworktypes.Plugin, error) {
func buildPlugin(ctx context.Context, config api.DeschedulerProfile, pluginName string, handle frameworktypes.Handle, reg pluginregistry.Registry) (frameworktypes.Plugin, error) {
pc, _ := getPluginConfig(pluginName, config.PluginConfigs)
if pc == nil {
klog.ErrorS(fmt.Errorf("unable to get plugin config"), "skipping plugin", "plugin", pluginName, "profile", config.Name)
@@ -272,6 +304,7 @@ func NewProfile(ctx context.Context, config api.DeschedulerProfile, reg pluginre
return nil, fmt.Errorf("profile %q configures preEvictionFilter extension point of non-existing plugins: %v", config.Name, sets.New(config.Plugins.PreEvictionFilter.Enabled...).Difference(pi.preEvictionFilter))
}
// Create a base handle that will be used as a template for plugin-specific handles
handle := &handleImpl{
clientSet: hOpts.clientSet,
getPodsAssignedToNodeFunc: hOpts.getPodsAssignedToNodeFunc,
@@ -280,24 +313,30 @@ func NewProfile(ctx context.Context, config api.DeschedulerProfile, reg pluginre
profileName: config.Name,
podEvictor: hOpts.podEvictor,
},
metricsCollector: hOpts.metricsCollector,
prometheusClient: hOpts.prometheusClient,
metricsCollector: hOpts.metricsCollector,
prometheusClientGetter: hOpts.prometheusClientGetter,
}
// Collect all unique plugin names across all extension points
pluginNames := append(config.Plugins.Deschedule.Enabled, config.Plugins.Balance.Enabled...)
pluginNames = append(pluginNames, config.Plugins.Filter.Enabled...)
pluginNames = append(pluginNames, config.Plugins.PreEvictionFilter.Enabled...)
// Build each unique plugin only once with a unique plugin instance ID
plugins := make(map[string]frameworktypes.Plugin)
for _, plugin := range sets.New(pluginNames...).UnsortedList() {
pg, err := buildPlugin(ctx, config, plugin, handle, reg)
for idx, pluginName := range sets.New(pluginNames...).UnsortedList() {
ph := &pluginHandle{
handleImpl: handle,
pluginInstanceID: fmt.Sprintf("%s-%d", hOpts.profileInstanceID, idx),
}
pg, err := buildPlugin(ctx, config, pluginName, ph, reg)
if err != nil {
return nil, fmt.Errorf("unable to build %v plugin: %v", plugin, err)
return nil, fmt.Errorf("unable to build %v plugin: %v", pluginName, err)
}
if pg == nil {
return nil, fmt.Errorf("got empty %v plugin build", plugin)
return nil, fmt.Errorf("got empty %v plugin build", pluginName)
}
plugins[plugin] = pg
plugins[pluginName] = pg
}
// Later, when a default list of plugins and their extension points is established,
+394 -209
View File
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"sort"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
@@ -26,7 +27,60 @@ import (
testutils "sigs.k8s.io/descheduler/test"
)
// registerDefaultEvictor registers the DefaultEvictor plugin with the given registry
func registerDefaultEvictor(registry pluginregistry.Registry) {
pluginregistry.Register(
defaultevictor.PluginName,
defaultevictor.New,
&defaultevictor.DefaultEvictor{},
&defaultevictor.DefaultEvictorArgs{},
defaultevictor.ValidateDefaultEvictorArgs,
defaultevictor.SetDefaults_DefaultEvictorArgs,
registry,
)
}
func TestProfileDescheduleBalanceExtensionPointsEviction(t *testing.T) {
// Helper to build profile config with default Filter and PreEvictionFilter
buildProfileConfig := func(name string, descheduleEnabled, balanceEnabled bool) api.DeschedulerProfile {
config := api.DeschedulerProfile{
Name: name,
PluginConfigs: []api.PluginConfig{
{
Name: defaultevictor.PluginName,
Args: &defaultevictor.DefaultEvictorArgs{
PriorityThreshold: &api.PriorityThreshold{
Value: nil,
},
},
},
{
Name: "FakePlugin",
Args: &fakeplugin.FakePluginArgs{},
},
},
Plugins: api.Plugins{
Filter: api.PluginSet{
Enabled: []string{defaultevictor.PluginName},
},
PreEvictionFilter: api.PluginSet{
Enabled: []string{defaultevictor.PluginName},
},
},
}
if descheduleEnabled {
config.Plugins.Deschedule = api.PluginSet{
Enabled: []string{"FakePlugin"},
}
}
if balanceEnabled {
config.Plugins.Balance = api.PluginSet{
Enabled: []string{"FakePlugin"},
}
}
return config
}
tests := []struct {
name string
config api.DeschedulerProfile
@@ -34,134 +88,26 @@ func TestProfileDescheduleBalanceExtensionPointsEviction(t *testing.T) {
expectedEviction bool
}{
{
name: "profile with deschedule extension point enabled single eviction",
config: api.DeschedulerProfile{
Name: "strategy-test-profile-with-deschedule",
PluginConfigs: []api.PluginConfig{
{
Name: defaultevictor.PluginName,
Args: &defaultevictor.DefaultEvictorArgs{
PriorityThreshold: &api.PriorityThreshold{
Value: nil,
},
},
},
{
Name: "FakePlugin",
Args: &fakeplugin.FakePluginArgs{},
},
},
Plugins: api.Plugins{
Deschedule: api.PluginSet{
Enabled: []string{"FakePlugin"},
},
Filter: api.PluginSet{
Enabled: []string{defaultevictor.PluginName},
},
PreEvictionFilter: api.PluginSet{
Enabled: []string{defaultevictor.PluginName},
},
},
},
name: "profile with deschedule extension point enabled single eviction",
config: buildProfileConfig("strategy-test-profile-with-deschedule", true, false),
extensionPoint: frameworktypes.DescheduleExtensionPoint,
expectedEviction: true,
},
{
name: "profile with balance extension point enabled single eviction",
config: api.DeschedulerProfile{
Name: "strategy-test-profile-with-balance",
PluginConfigs: []api.PluginConfig{
{
Name: defaultevictor.PluginName,
Args: &defaultevictor.DefaultEvictorArgs{
PriorityThreshold: &api.PriorityThreshold{
Value: nil,
},
},
},
{
Name: "FakePlugin",
Args: &fakeplugin.FakePluginArgs{},
},
},
Plugins: api.Plugins{
Balance: api.PluginSet{
Enabled: []string{"FakePlugin"},
},
Filter: api.PluginSet{
Enabled: []string{defaultevictor.PluginName},
},
PreEvictionFilter: api.PluginSet{
Enabled: []string{defaultevictor.PluginName},
},
},
},
name: "profile with balance extension point enabled single eviction",
config: buildProfileConfig("strategy-test-profile-with-balance", false, true),
extensionPoint: frameworktypes.BalanceExtensionPoint,
expectedEviction: true,
},
{
name: "profile with deschedule extension point balance enabled no eviction",
config: api.DeschedulerProfile{
Name: "strategy-test-profile-with-deschedule",
PluginConfigs: []api.PluginConfig{
{
Name: defaultevictor.PluginName,
Args: &defaultevictor.DefaultEvictorArgs{
PriorityThreshold: &api.PriorityThreshold{
Value: nil,
},
},
},
{
Name: "FakePlugin",
Args: &fakeplugin.FakePluginArgs{},
},
},
Plugins: api.Plugins{
Balance: api.PluginSet{
Enabled: []string{"FakePlugin"},
},
Filter: api.PluginSet{
Enabled: []string{defaultevictor.PluginName},
},
PreEvictionFilter: api.PluginSet{
Enabled: []string{defaultevictor.PluginName},
},
},
},
name: "profile with deschedule extension point balance enabled no eviction",
config: buildProfileConfig("strategy-test-profile-with-balance", false, true),
extensionPoint: frameworktypes.DescheduleExtensionPoint,
expectedEviction: false,
},
{
name: "profile with balance extension point deschedule enabled no eviction",
config: api.DeschedulerProfile{
Name: "strategy-test-profile-with-deschedule",
PluginConfigs: []api.PluginConfig{
{
Name: defaultevictor.PluginName,
Args: &defaultevictor.DefaultEvictorArgs{
PriorityThreshold: &api.PriorityThreshold{
Value: nil,
},
},
},
{
Name: "FakePlugin",
Args: &fakeplugin.FakePluginArgs{},
},
},
Plugins: api.Plugins{
Deschedule: api.PluginSet{
Enabled: []string{"FakePlugin"},
},
Filter: api.PluginSet{
Enabled: []string{defaultevictor.PluginName},
},
PreEvictionFilter: api.PluginSet{
Enabled: []string{defaultevictor.PluginName},
},
},
},
name: "profile with balance extension point deschedule enabled no eviction",
config: buildProfileConfig("strategy-test-profile-with-deschedule", true, false),
extensionPoint: frameworktypes.BalanceExtensionPoint,
expectedEviction: false,
},
@@ -206,25 +152,9 @@ func TestProfileDescheduleBalanceExtensionPointsEviction(t *testing.T) {
}
pluginregistry.PluginRegistry = pluginregistry.NewRegistry()
pluginregistry.Register(
"FakePlugin",
fakeplugin.NewPluginFncFromFake(&fakePlugin),
&fakeplugin.FakePlugin{},
&fakeplugin.FakePluginArgs{},
fakeplugin.ValidateFakePluginArgs,
fakeplugin.SetDefaults_FakePluginArgs,
pluginregistry.PluginRegistry,
)
fakeplugin.RegisterFakePlugin("FakePlugin", &fakePlugin, pluginregistry.PluginRegistry)
pluginregistry.Register(
defaultevictor.PluginName,
defaultevictor.New,
&defaultevictor.DefaultEvictor{},
&defaultevictor.DefaultEvictorArgs{},
defaultevictor.ValidateDefaultEvictorArgs,
defaultevictor.SetDefaults_DefaultEvictorArgs,
pluginregistry.PluginRegistry,
)
registerDefaultEvictor(pluginregistry.PluginRegistry)
client := fakeclientset.NewSimpleClientset(n1, n2, p1)
var evictedPods []string
@@ -319,56 +249,13 @@ func TestProfileExtensionPoints(t *testing.T) {
fakeBalancePlugin := &fakeplugin.FakeBalancePlugin{PluginName: balancePluginName}
fakeFilterPlugin := &fakeplugin.FakeFilterPlugin{PluginName: filterPluginName}
pluginregistry.Register(
fakePluginName,
fakeplugin.NewPluginFncFromFake(fakePlugin),
&fakeplugin.FakePlugin{},
&fakeplugin.FakePluginArgs{},
fakeplugin.ValidateFakePluginArgs,
fakeplugin.SetDefaults_FakePluginArgs,
pluginregistry.PluginRegistry,
)
pluginregistry.Register(
deschedulePluginName,
fakeplugin.NewFakeDeschedulePluginFncFromFake(fakeDeschedulePlugin),
&fakeplugin.FakeDeschedulePlugin{},
&fakeplugin.FakeDeschedulePluginArgs{},
fakeplugin.ValidateFakePluginArgs,
fakeplugin.SetDefaults_FakePluginArgs,
pluginregistry.PluginRegistry,
)
pluginregistry.Register(
balancePluginName,
fakeplugin.NewFakeBalancePluginFncFromFake(fakeBalancePlugin),
&fakeplugin.FakeBalancePlugin{},
&fakeplugin.FakeBalancePluginArgs{},
fakeplugin.ValidateFakePluginArgs,
fakeplugin.SetDefaults_FakePluginArgs,
pluginregistry.PluginRegistry,
)
pluginregistry.Register(
filterPluginName,
fakeplugin.NewFakeFilterPluginFncFromFake(fakeFilterPlugin),
&fakeplugin.FakeFilterPlugin{},
&fakeplugin.FakeFilterPluginArgs{},
fakeplugin.ValidateFakePluginArgs,
fakeplugin.SetDefaults_FakePluginArgs,
pluginregistry.PluginRegistry,
)
fakeplugin.RegisterFakePlugin(fakePluginName, fakePlugin, pluginregistry.PluginRegistry)
fakeplugin.RegisterFakeDeschedulePlugin(deschedulePluginName, fakeDeschedulePlugin, pluginregistry.PluginRegistry)
fakeplugin.RegisterFakeBalancePlugin(balancePluginName, fakeBalancePlugin, pluginregistry.PluginRegistry)
fakeplugin.RegisterFakeFilterPlugin(filterPluginName, fakeFilterPlugin, pluginregistry.PluginRegistry)
}
pluginregistry.Register(
defaultevictor.PluginName,
defaultevictor.New,
&defaultevictor.DefaultEvictor{},
&defaultevictor.DefaultEvictorArgs{},
defaultevictor.ValidateDefaultEvictorArgs,
defaultevictor.SetDefaults_DefaultEvictorArgs,
pluginregistry.PluginRegistry,
)
registerDefaultEvictor(pluginregistry.PluginRegistry)
client := fakeclientset.NewSimpleClientset(n1, n2, p1)
var evictedPods []string
@@ -524,15 +411,7 @@ func TestProfileExtensionPointOrdering(t *testing.T) {
})
// plugin implementing Filter extension point
pluginregistry.Register(
pluginName,
fakeplugin.NewFakeFilterPluginFncFromFake(fakeFilterPlugin),
&fakeplugin.FakeFilterPlugin{},
&fakeplugin.FakeFilterPluginArgs{},
fakeplugin.ValidateFakePluginArgs,
fakeplugin.SetDefaults_FakePluginArgs,
pluginregistry.PluginRegistry,
)
fakeplugin.RegisterFakeFilterPlugin(pluginName, fakeFilterPlugin, pluginregistry.PluginRegistry)
fakePluginName := fmt.Sprintf("FakePlugin_%v", i)
fakePlugin := fakeplugin.FakePlugin{}
@@ -557,26 +436,10 @@ func TestProfileExtensionPointOrdering(t *testing.T) {
return true, false, nil
})
pluginregistry.Register(
fakePluginName,
fakeplugin.NewPluginFncFromFake(&fakePlugin),
&fakeplugin.FakePlugin{},
&fakeplugin.FakePluginArgs{},
fakeplugin.ValidateFakePluginArgs,
fakeplugin.SetDefaults_FakePluginArgs,
pluginregistry.PluginRegistry,
)
fakeplugin.RegisterFakePlugin(fakePluginName, &fakePlugin, pluginregistry.PluginRegistry)
}
pluginregistry.Register(
defaultevictor.PluginName,
defaultevictor.New,
&defaultevictor.DefaultEvictor{},
&defaultevictor.DefaultEvictorArgs{},
defaultevictor.ValidateDefaultEvictorArgs,
defaultevictor.SetDefaults_DefaultEvictorArgs,
pluginregistry.PluginRegistry,
)
registerDefaultEvictor(pluginregistry.PluginRegistry)
client := fakeclientset.NewSimpleClientset(n1, n2, p1)
var evictedPods []string
@@ -680,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())
}
})
}
}
+3 -3
View File
@@ -29,7 +29,7 @@ func InitFrameworkHandle(
podInformer := sharedInformerFactory.Core().V1().Pods().Informer()
podsAssignedToNode, err := podutil.BuildGetPodsAssignedToNodeFunc(podInformer)
if err != nil {
return nil, nil, fmt.Errorf("Build get pods assigned to node function error: %v", err)
return nil, nil, fmt.Errorf("build get pods assigned to node function error: %v", err)
}
var getPodsAssignedToNode func(s string, filterFunc podutil.FilterFunc) ([]*v1.Pod, error)
@@ -52,7 +52,7 @@ func InitFrameworkHandle(
})
podEvictor, err := evictions.NewPodEvictor(ctx, client, eventRecorder, podInformer, featureGates, evictionOptions)
if err != nil {
return nil, nil, fmt.Errorf("Unable to initialize pod evictor: %v", err)
return nil, nil, fmt.Errorf("unable to initialize pod evictor: %v", err)
}
evictorFilter, err := defaultevictor.New(
ctx,
@@ -64,7 +64,7 @@ func InitFrameworkHandle(
},
)
if err != nil {
return nil, nil, fmt.Errorf("Unable to initialize the plugin: %v", err)
return nil, nil, fmt.Errorf("unable to initialize the plugin: %v", err)
}
return &frameworkfake.HandleImpl{
ClientsetImpl: client,
+3
View File
@@ -41,6 +41,9 @@ type Handle interface {
GetPodsAssignedToNodeFunc() podutil.GetPodsAssignedToNodeFunc
SharedInformerFactory() informers.SharedInformerFactory
MetricsCollector() *metricscollector.MetricsCollector
// PluginInstanceID returns a unique identifier for this plugin instance.
// The ID is unique across all plugin instances in a configuration.
PluginInstanceID() string
}
// Evictor defines an interface for filtering and evicting pods
+13 -6
View File
@@ -27,8 +27,9 @@ import (
"go.opentelemetry.io/otel/propagation"
sdkresource "go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
semconv "go.opentelemetry.io/otel/semconv/v1.37.0"
"go.opentelemetry.io/otel/trace"
"go.opentelemetry.io/otel/trace/noop"
"google.golang.org/grpc/credentials"
"k8s.io/klog/v2"
)
@@ -56,7 +57,7 @@ var (
)
func init() {
provider = trace.NewNoopTracerProvider()
provider = noop.NewTracerProvider()
tracer = provider.Tracer(TracerName)
}
@@ -73,7 +74,7 @@ func NewTracerProvider(ctx context.Context, endpoint, caCert, name, namespace st
if err != nil && fallbackToNoOpTracer {
klog.ErrorS(err, "ran into an error trying to setup a trace provider. Falling back to NoOp provider")
err = nil
provider = trace.NewNoopTracerProvider()
provider = noop.NewTracerProvider()
}
otel.SetTextMapPropagator(propagation.TraceContext{})
otel.SetTracerProvider(provider)
@@ -85,8 +86,8 @@ func NewTracerProvider(ctx context.Context, endpoint, caCert, name, namespace st
if endpoint == "" {
klog.V(2).Info("Did not find a trace collector endpoint defined. Switching to NoopTraceProvider")
provider = trace.NewNoopTracerProvider()
return
provider = noop.NewTracerProvider()
return nil
}
var opts []otlptracegrpc.Option
@@ -110,6 +111,12 @@ func NewTracerProvider(ctx context.Context, endpoint, caCert, name, namespace st
opts = append(opts, otlptracegrpc.WithInsecure())
}
if os.Getenv("USER") == "" {
if err := os.Setenv("USER", "descheduler"); err != nil {
klog.ErrorS(err, "failed to set USER environment variable")
}
}
client := otlptracegrpc.NewClient(opts...)
exporter, err := otlptrace.New(ctx, client)
@@ -138,7 +145,7 @@ func NewTracerProvider(ctx context.Context, endpoint, caCert, name, namespace st
sdktrace.WithResource(resource),
)
klog.V(2).Info("Successfully setup trace provider")
return
return nil
}
func defaultResourceOpts(name string) []sdkresource.Option {
+1 -2
View File
@@ -1,14 +1,13 @@
package tracing
import (
"context"
"testing"
sdkresource "go.opentelemetry.io/otel/sdk/resource"
)
func TestCreateTraceableResource(t *testing.T) {
ctx := context.TODO()
ctx := t.Context()
resourceOpts := defaultResourceOpts("descheduler")
_, err := sdkresource.New(ctx, resourceOpts...)
if err != nil {
+10 -91
View File
@@ -1,6 +1,7 @@
package utils
import (
"context"
"fmt"
policy "k8s.io/api/policy/v1"
@@ -11,24 +12,10 @@ import (
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
resourcehelper "k8s.io/component-helpers/resource"
"k8s.io/klog/v2"
)
// GetResourceRequest finds and returns the request value for a specific resource.
func GetResourceRequest(pod *v1.Pod, resource v1.ResourceName) int64 {
if resource == v1.ResourcePods {
return 1
}
requestQuantity := GetResourceRequestQuantity(pod, resource)
if resource == v1.ResourceCPU {
return requestQuantity.MilliValue()
}
return requestQuantity.Value()
}
// GetResourceRequestQuantity finds and returns the request quantity for a specific resource.
func GetResourceRequestQuantity(pod *v1.Pod, resourceName v1.ResourceName) resource.Quantity {
requestQuantity := resource.Quantity{}
@@ -42,26 +29,8 @@ func GetResourceRequestQuantity(pod *v1.Pod, resourceName v1.ResourceName) resou
requestQuantity = resource.Quantity{Format: resource.DecimalSI}
}
for _, container := range pod.Spec.Containers {
if rQuantity, ok := container.Resources.Requests[resourceName]; ok {
requestQuantity.Add(rQuantity)
}
}
for _, container := range pod.Spec.InitContainers {
if rQuantity, ok := container.Resources.Requests[resourceName]; ok {
if requestQuantity.Cmp(rQuantity) < 0 {
requestQuantity = rQuantity.DeepCopy()
}
}
}
// We assume pod overhead feature gate is enabled.
// We can't import the scheduler settings so we will inherit the default.
if pod.Spec.Overhead != nil {
if podOverhead, ok := pod.Spec.Overhead[resourceName]; ok && !requestQuantity.IsZero() {
requestQuantity.Add(podOverhead)
}
if rQuantity, ok := resourcehelper.PodRequests(pod, resourcehelper.PodResourcesOptions{})[resourceName]; ok {
requestQuantity.Add(rQuantity)
}
return requestQuantity
@@ -171,73 +140,23 @@ func GetPodSource(pod *v1.Pod) (string, error) {
// containers of the pod. If PodOverhead feature is enabled, pod overhead is added to the
// total container resource requests and to the total container limits which have a
// non-zero quantity.
func PodRequestsAndLimits(pod *v1.Pod) (reqs, limits v1.ResourceList) {
reqs, limits = v1.ResourceList{}, v1.ResourceList{}
for _, container := range pod.Spec.Containers {
addResourceList(reqs, container.Resources.Requests)
addResourceList(limits, container.Resources.Limits)
}
// init containers define the minimum of any resource
for _, container := range pod.Spec.InitContainers {
maxResourceList(reqs, container.Resources.Requests)
maxResourceList(limits, container.Resources.Limits)
}
// We assume pod overhead feature gate is enabled.
// We can't import the scheduler settings so we will inherit the default.
if pod.Spec.Overhead != nil {
addResourceList(reqs, pod.Spec.Overhead)
for name, quantity := range pod.Spec.Overhead {
if value, ok := limits[name]; ok && !value.IsZero() {
value.Add(quantity)
limits[name] = value
}
}
}
return
}
// addResourceList adds the resources in newList to list
func addResourceList(list, newList v1.ResourceList) {
for name, quantity := range newList {
if value, ok := list[name]; !ok {
list[name] = quantity.DeepCopy()
} else {
value.Add(quantity)
list[name] = value
}
}
}
// maxResourceList sets list to the greater of list/newList for every resource
// either list
func maxResourceList(list, new v1.ResourceList) {
for name, quantity := range new {
if value, ok := list[name]; !ok {
list[name] = quantity.DeepCopy()
continue
} else {
if quantity.Cmp(value) > 0 {
list[name] = quantity.DeepCopy()
}
}
}
func PodRequestsAndLimits(pod *v1.Pod) (v1.ResourceList, v1.ResourceList) {
opts := resourcehelper.PodResourcesOptions{}
return resourcehelper.PodRequests(pod, opts), resourcehelper.PodLimits(pod, opts)
}
// PodToleratesTaints returns true if a pod tolerates one node's taints
func PodToleratesTaints(pod *v1.Pod, taintsOfNodes map[string][]v1.Taint) bool {
func PodToleratesTaints(ctx context.Context, pod *v1.Pod, taintsOfNodes map[string][]v1.Taint) bool {
for nodeName, taintsForNode := range taintsOfNodes {
if len(pod.Spec.Tolerations) >= len(taintsForNode) {
if TolerationsTolerateTaintsWithFilter(pod.Spec.Tolerations, taintsForNode, nil) {
if TolerationsTolerateTaintsWithFilter(ctx, pod.Spec.Tolerations, taintsForNode, nil) {
return true
}
if klog.V(5).Enabled() {
for i := range taintsForNode {
if !TolerationsTolerateTaint(pod.Spec.Tolerations, &taintsForNode[i]) {
if !TolerationsTolerateTaint(ctx, pod.Spec.Tolerations, &taintsForNode[i]) {
klog.V(5).InfoS("Pod doesn't tolerate node taint",
"pod", klog.KObj(pod),
"nodeName", nodeName,
+6 -4
View File
@@ -17,6 +17,7 @@ limitations under the License.
package utils
import (
"context"
"fmt"
"reflect"
"sort"
@@ -176,9 +177,10 @@ func NodeSelectorsEqual(n1, n2 *v1.NodeSelector) bool {
}
// TolerationsTolerateTaint checks if taint is tolerated by any of the tolerations.
func TolerationsTolerateTaint(tolerations []v1.Toleration, taint *v1.Taint) bool {
func TolerationsTolerateTaint(ctx context.Context, tolerations []v1.Toleration, taint *v1.Taint) bool {
logger := klog.FromContext(ctx)
for i := range tolerations {
if tolerations[i].ToleratesTaint(taint) {
if tolerations[i].ToleratesTaint(logger, taint, true) {
return true
}
}
@@ -189,13 +191,13 @@ type taintsFilterFunc func(*v1.Taint) bool
// TolerationsTolerateTaintsWithFilter checks if given tolerations tolerates
// all the taints that apply to the filter in given taint list.
func TolerationsTolerateTaintsWithFilter(tolerations []v1.Toleration, taints []v1.Taint, applyFilter taintsFilterFunc) bool {
func TolerationsTolerateTaintsWithFilter(ctx context.Context, tolerations []v1.Toleration, taints []v1.Taint, applyFilter taintsFilterFunc) bool {
for i := range taints {
if applyFilter != nil && !applyFilter(&taints[i]) {
continue
}
if !TolerationsTolerateTaint(tolerations, &taints[i]) {
if !TolerationsTolerateTaint(ctx, tolerations, &taints[i]) {
return false
}
}
+1 -1
View File
@@ -41,5 +41,5 @@ func GetPriorityValueFromPriorityThreshold(ctx context.Context, client clientset
if priority > SystemCriticalPriority {
return 0, fmt.Errorf("priority threshold can't be greater than %d", SystemCriticalPriority)
}
return
return priority, nil
}
+2 -3
View File
@@ -10,7 +10,6 @@ import (
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -71,8 +70,8 @@ func virtualMachineInstance(idx int) *kvcorev1.VirtualMachineInstance {
Rng: &kvcorev1.Rng{},
},
Resources: kvcorev1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceMemory: resource.MustParse("1024M"),
Requests: corev1.ResourceList{
corev1.ResourceMemory: resource.MustParse("1024M"),
},
},
},
+5 -1
View File
@@ -201,8 +201,12 @@ func TestLowNodeUtilizationKubernetesMetrics(t *testing.T) {
waitForPodsRunning(ctx, t, clientSet, deploymentObj.Labels, tc.replicasNum, deploymentObj.Namespace)
// wait until workerNodes[0].Name has the right actual cpu utilization and all the testing pods are running
// and producing ~12 cores in total
wait.PollUntilWithContext(ctx, 5*time.Second, func(context.Context) (done bool, err error) {
wait.PollUntilContextCancel(ctx, 5*time.Second, true, func(context.Context) (done bool, err error) {
item, err := metricsClient.MetricsV1beta1().NodeMetricses().Get(ctx, workerNodes[0].Name, metav1.GetOptions{})
if err != nil {
t.Logf("unable to list nodemetricses: %v", err)
return false, nil
}
t.Logf("Waiting for %q nodemetrics cpu utilization to get over 12, currently %v", workerNodes[0].Name, item.Usage.Cpu().Value())
if item.Usage.Cpu().Value() < 12 {
return false, nil
+373
View File
@@ -0,0 +1,373 @@
/*
Copyright 2021 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 e2e
import (
"context"
"fmt"
"os"
"strings"
"testing"
"time"
appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/component-base/config"
"k8s.io/utils/ptr"
storagev1 "k8s.io/api/storage/v1"
"sigs.k8s.io/descheduler/cmd/descheduler/app/options"
"sigs.k8s.io/descheduler/pkg/api"
apiv1alpha2 "sigs.k8s.io/descheduler/pkg/api/v1alpha2"
"sigs.k8s.io/descheduler/pkg/descheduler/client"
"sigs.k8s.io/descheduler/pkg/framework/plugins/defaultevictor"
"sigs.k8s.io/descheduler/pkg/framework/plugins/removepodshavingtoomanyrestarts"
)
// protectPodsWithPVCPolicy returns a descheduler policy that protects pods
// using PVCs of specific storage classes from eviction while, at the same
// time, evicting pods that have restarted more than 3 times.
func protectPodsWithPVCPolicy(namespace string, protectedsc []defaultevictor.ProtectedStorageClass) *apiv1alpha2.DeschedulerPolicy {
return &apiv1alpha2.DeschedulerPolicy{
Profiles: []apiv1alpha2.DeschedulerProfile{
{
Name: "ProtectPodsWithPVCPolicy",
PluginConfigs: []apiv1alpha2.PluginConfig{
{
Name: removepodshavingtoomanyrestarts.PluginName,
Args: runtime.RawExtension{
Object: &removepodshavingtoomanyrestarts.RemovePodsHavingTooManyRestartsArgs{
PodRestartThreshold: 3,
IncludingInitContainers: true,
Namespaces: &api.Namespaces{
Include: []string{namespace},
},
},
},
},
{
Name: defaultevictor.PluginName,
Args: runtime.RawExtension{
Object: &defaultevictor.DefaultEvictorArgs{
PodProtections: defaultevictor.PodProtections{
DefaultDisabled: []defaultevictor.PodProtection{
defaultevictor.PodsWithLocalStorage,
},
ExtraEnabled: []defaultevictor.PodProtection{
defaultevictor.PodsWithPVC,
},
Config: &defaultevictor.PodProtectionsConfig{
PodsWithPVC: &defaultevictor.PodsWithPVCConfig{
ProtectedStorageClasses: protectedsc,
},
},
},
},
},
},
},
Plugins: apiv1alpha2.Plugins{
Filter: apiv1alpha2.PluginSet{
Enabled: []string{
defaultevictor.PluginName,
},
},
Deschedule: apiv1alpha2.PluginSet{
Enabled: []string{
removepodshavingtoomanyrestarts.PluginName,
},
},
},
},
},
}
}
// TestProtectPodsWithPVC tests that pods using PVCs are protected.
func TestProtectPodsWithPVC(t *testing.T) {
ctx := context.Background()
initPluginRegistry()
cli, err := client.CreateClient(
config.ClientConnectionConfiguration{
Kubeconfig: os.Getenv("KUBECONFIG"),
}, "",
)
if err != nil {
t.Fatalf("error during kubernetes client creation with %v", err)
}
// start by finding out what is the default storage class in the
// cluster. if none is found then this test can't run.
scs, err := cli.StorageV1().StorageClasses().List(ctx, metav1.ListOptions{})
if err != nil {
t.Fatalf("error listing storage classes: %v", err)
}
var defclass *storagev1.StorageClass
for _, sc := range scs.Items {
if _, ok := sc.Annotations["storageclass.kubernetes.io/is-default-class"]; ok {
defclass = &sc
break
}
}
if defclass == nil {
t.Fatalf("no default storage class found, unable to run the test")
}
// now we replicate the default storage class so we have two different
// storage classes in the cluster. this is useful to test protected vs
// unprotected pods using PVCs.
unprotectedsc := defclass.DeepCopy()
delete(unprotectedsc.Annotations, "storageclass.kubernetes.io/is-default-class")
unprotectedsc.ResourceVersion = ""
unprotectedsc.Name = "unprotected"
if _, err = cli.StorageV1().StorageClasses().Create(ctx, unprotectedsc, metav1.CreateOptions{}); err != nil {
t.Fatalf("error creating unprotected storage class: %v", err)
}
defer cli.StorageV1().StorageClasses().Delete(ctx, unprotectedsc.Name, metav1.DeleteOptions{})
// this is the namespace we are going to use for all testing
t.Logf("creating testing namespace %v", t.Name())
namespace := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("e2e-%s", strings.ToLower(t.Name())),
},
}
if _, err := cli.CoreV1().Namespaces().Create(ctx, namespace, metav1.CreateOptions{}); err != nil {
t.Fatalf("Unable to create ns %v", namespace.Name)
}
defer cli.CoreV1().Namespaces().Delete(ctx, namespace.Name, metav1.DeleteOptions{})
for _, tc := range []struct {
name string
policy *apiv1alpha2.DeschedulerPolicy
enableGracePeriod bool
expectedEvictedPodCount uint
pvcs []*v1.PersistentVolumeClaim
volumes []v1.Volume
}{
{
name: "evict pods from unprotected storage class",
policy: protectPodsWithPVCPolicy(
namespace.Name, []defaultevictor.ProtectedStorageClass{
{
Name: defclass.Name,
},
},
),
expectedEvictedPodCount: 4,
pvcs: []*v1.PersistentVolumeClaim{
{
ObjectMeta: metav1.ObjectMeta{
Name: "test-unprotected-claim",
Namespace: namespace.Name,
},
Spec: v1.PersistentVolumeClaimSpec{
StorageClassName: ptr.To(unprotectedsc.Name),
AccessModes: []v1.PersistentVolumeAccessMode{
v1.ReadWriteOnce,
},
Resources: v1.VolumeResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceStorage: resource.MustParse("1Gi"),
},
},
},
},
},
volumes: []v1.Volume{
{
Name: "test-unprotected-volume",
VolumeSource: v1.VolumeSource{
PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
ClaimName: "test-unprotected-claim",
},
},
},
},
},
{
name: "preserve pods from protected storage class",
policy: protectPodsWithPVCPolicy(
namespace.Name, []defaultevictor.ProtectedStorageClass{
{
Name: defclass.Name,
},
},
),
expectedEvictedPodCount: 0,
pvcs: []*v1.PersistentVolumeClaim{
{
ObjectMeta: metav1.ObjectMeta{
Name: "test-protected-claim",
Namespace: namespace.Name,
},
Spec: v1.PersistentVolumeClaimSpec{
AccessModes: []v1.PersistentVolumeAccessMode{
v1.ReadWriteOnce,
},
Resources: v1.VolumeResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceStorage: resource.MustParse("1Gi"),
},
},
},
},
},
volumes: []v1.Volume{
{
Name: "test-protected-volume",
VolumeSource: v1.VolumeSource{
PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
ClaimName: "test-protected-claim",
},
},
},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
t.Logf("creating testing pvcs in namespace %v", namespace.Name)
for _, pvc := range tc.pvcs {
if _, err = cli.CoreV1().PersistentVolumeClaims(namespace.Name).Create(ctx, pvc, metav1.CreateOptions{}); err != nil {
t.Fatalf("error creating PVC: %v", err)
}
defer cli.CoreV1().PersistentVolumeClaims(namespace.Name).Delete(ctx, pvc.Name, metav1.DeleteOptions{})
}
deploy := buildTestDeployment(
"restart-pod",
namespace.Name,
4,
map[string]string{"test": "restart-pod", "name": "test-toomanyrestarts"},
func(deployment *appsv1.Deployment) {
deployment.Spec.Template.Spec.Containers[0].Command = []string{"/bin/sh"}
deployment.Spec.Template.Spec.Containers[0].Args = []string{"-c", "sleep 1s && exit 1"}
},
)
deploy.Spec.Template.Spec.Volumes = tc.volumes
t.Logf("creating deployment %v", deploy.Name)
if _, err := cli.AppsV1().Deployments(deploy.Namespace).Create(ctx, deploy, metav1.CreateOptions{}); err != nil {
t.Fatalf("error creating deployment: %v", err)
}
defer cli.AppsV1().Deployments(deploy.Namespace).Delete(ctx, deploy.Name, metav1.DeleteOptions{})
// wait for 3 restarts
waitPodRestartCount(ctx, cli, namespace.Name, t, 3)
rs, err := options.NewDeschedulerServer()
if err != nil {
t.Fatalf("unable to initialize server: %v\n", err)
}
rs.Client, rs.EventClient, rs.DefaultFeatureGates = cli, cli, initFeatureGates()
preRunNames := sets.NewString(getCurrentPodNames(ctx, cli, namespace.Name, t)...)
// deploy the descheduler with the configured policy
policycm, err := deschedulerPolicyConfigMap(tc.policy)
if err != nil {
t.Fatalf("Error creating %q CM: %v", policycm.Name, err)
}
t.Logf("creating %q policy CM with PodsWithPVC protection enabled...", policycm.Name)
if _, err = cli.CoreV1().ConfigMaps(policycm.Namespace).Create(
ctx, policycm, metav1.CreateOptions{},
); err != nil {
t.Fatalf("error creating %q CM: %v", policycm.Name, err)
}
defer func() {
t.Logf("deleting %q CM...", policycm.Name)
if err := cli.CoreV1().ConfigMaps(policycm.Namespace).Delete(
ctx, policycm.Name, metav1.DeleteOptions{},
); err != nil {
t.Fatalf("unable to delete %q CM: %v", policycm.Name, err)
}
}()
desdep := deschedulerDeployment(namespace.Name)
t.Logf("creating descheduler deployment %v", desdep.Name)
if _, err := cli.AppsV1().Deployments(desdep.Namespace).Create(
ctx, desdep, metav1.CreateOptions{},
); err != nil {
t.Fatalf("error creating %q deployment: %v", desdep.Name, err)
}
deschedulerPodName := ""
defer func() {
if deschedulerPodName != "" {
printPodLogs(ctx, t, cli, deschedulerPodName)
}
t.Logf("deleting %q deployment...", desdep.Name)
if err := cli.AppsV1().Deployments(desdep.Namespace).Delete(
ctx, desdep.Name, metav1.DeleteOptions{},
); err != nil {
t.Fatalf("unable to delete %q deployment: %v", desdep.Name, err)
}
waitForPodsToDisappear(ctx, t, cli, desdep.Labels, desdep.Namespace)
}()
t.Logf("waiting for the descheduler pod running")
deschedulerPods := waitForPodsRunning(ctx, t, cli, desdep.Labels, 1, desdep.Namespace)
if len(deschedulerPods) != 0 {
deschedulerPodName = deschedulerPods[0].Name
}
if err := wait.PollUntilContextTimeout(
ctx, 5*time.Second, time.Minute, true,
func(ctx context.Context) (bool, error) {
podList, err := cli.CoreV1().Pods(namespace.Name).List(
ctx, metav1.ListOptions{},
)
if err != nil {
t.Fatalf("error listing pods: %v", err)
}
names := []string{}
for _, item := range podList.Items {
names = append(names, item.Name)
}
currentRunNames := sets.NewString(names...)
actualEvictedPod := preRunNames.Difference(currentRunNames)
actualEvictedPodCount := uint(actualEvictedPod.Len())
if actualEvictedPodCount < tc.expectedEvictedPodCount {
t.Logf(
"expecting %v number of pods evicted, got %v instead",
tc.expectedEvictedPodCount, actualEvictedPodCount,
)
return false, nil
}
return true, nil
},
); err != nil {
t.Fatalf("error waiting for descheduler running: %v", err)
}
waitForTerminatingPodsToDisappear(ctx, t, cli, namespace.Name)
})
}
}
+5 -6
View File
@@ -47,7 +47,6 @@ import (
"sigs.k8s.io/yaml"
"sigs.k8s.io/descheduler/cmd/descheduler/app/options"
"sigs.k8s.io/descheduler/pkg/api"
deschedulerapi "sigs.k8s.io/descheduler/pkg/api"
deschedulerapiv1alpha2 "sigs.k8s.io/descheduler/pkg/api/v1alpha2"
"sigs.k8s.io/descheduler/pkg/descheduler"
@@ -459,7 +458,7 @@ func runPodLifetimePlugin(
defaultevictor.DefaultEvictorArgs{
EvictSystemCriticalPods: evictCritical,
EvictDaemonSetPods: evictDaemonSet,
PriorityThreshold: &api.PriorityThreshold{
PriorityThreshold: &deschedulerapi.PriorityThreshold{
Value: &thresholdPriority,
},
},
@@ -640,10 +639,10 @@ func TestLowNodeUtilization(t *testing.T) {
t.Log("Running LowNodeUtilization plugin")
plugin, err := nodeutilization.NewLowNodeUtilization(ctx, &nodeutilization.LowNodeUtilizationArgs{
Thresholds: api.ResourceThresholds{
Thresholds: deschedulerapi.ResourceThresholds{
v1.ResourceCPU: 70,
},
TargetThresholds: api.ResourceThresholds{
TargetThresholds: deschedulerapi.ResourceThresholds{
v1.ResourceCPU: 80,
},
}, handle)
@@ -1340,7 +1339,7 @@ func TestPodLifeTimeOldestEvicted(t *testing.T) {
}
waitForRCPodsRunning(ctx, t, clientSet, rc)
podList, err = clientSet.CoreV1().Pods(rc.Namespace).List(ctx, metav1.ListOptions{LabelSelector: labels.SelectorFromSet(rc.Spec.Template.Labels).String()})
_, err = clientSet.CoreV1().Pods(rc.Namespace).List(ctx, metav1.ListOptions{LabelSelector: labels.SelectorFromSet(rc.Spec.Template.Labels).String()})
if err != nil {
t.Fatalf("Unable to list pods: %v", err)
}
@@ -1584,7 +1583,7 @@ func createBalancedPodForNodes(
}
// find the max, if the node has the max,use the one, if not,use the ratio parameter
var maxCPUFraction, maxMemFraction float64 = ratio, ratio
maxCPUFraction, maxMemFraction := ratio, ratio
cpuFractionMap := make(map[string]float64)
memFractionMap := make(map[string]float64)
+3 -3
View File
@@ -23,7 +23,7 @@ SKIP_INSTALL=${SKIP_INSTALL:-}
KIND_E2E=${KIND_E2E:-}
CONTAINER_ENGINE=${CONTAINER_ENGINE:-docker}
KIND_SUDO=${KIND_SUDO:-}
KIND_VERSION=${KIND_VERSION:-v0.30.0}
KIND_VERSION=${KIND_VERSION:-v0.31.0}
SKIP_KUBECTL_INSTALL=${SKIP_KUBECTL_INSTALL:-}
SKIP_KIND_INSTALL=${SKIP_KIND_INSTALL:-}
SKIP_KUBEVIRT_INSTALL=${SKIP_KUBEVIRT_INSTALL:-}
@@ -39,7 +39,7 @@ echo "DESCHEDULER_IMAGE: ${DESCHEDULER_IMAGE}"
# This just runs e2e tests.
if [ -n "$KIND_E2E" ]; then
K8S_VERSION=${KUBERNETES_VERSION:-v1.34.0}
K8S_VERSION=${KUBERNETES_VERSION:-v1.35.1}
if [ -z "${SKIP_KUBECTL_INSTALL}" ]; then
curl -Lo kubectl https://dl.k8s.io/release/${K8S_VERSION}/bin/linux/amd64/kubectl && chmod +x kubectl && mv kubectl /usr/local/bin/
fi
@@ -100,7 +100,7 @@ if [ -z "${SKIP_KUBEVIRT_INSTALL}" ]; then
kubectl -n kubevirt patch kubevirt kubevirt --type=merge --patch '{"spec":{"configuration":{"developerConfiguration":{"useEmulation":true}}}}'
fi
METRICS_SERVER_VERSION="v0.5.0"
METRICS_SERVER_VERSION="v0.8.1"
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/download/${METRICS_SERVER_VERSION}/components.yaml
kubectl patch -n kube-system deployment metrics-server --type=json \
-p '[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'
+96
View File
@@ -29,17 +29,40 @@ import (
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/uuid"
"k8s.io/apimachinery/pkg/util/wait"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
clientgotesting "k8s.io/client-go/testing"
"k8s.io/metrics/pkg/apis/metrics/v1beta1"
utilptr "k8s.io/utils/ptr"
)
// BuildTestNamespace creates a test namespace with given parameters.
func BuildTestNamespace(name string) *v1.Namespace {
namespace := &v1.Namespace{
TypeMeta: metav1.TypeMeta{
Kind: "Namespace",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
UID: uuid.NewUUID(),
Labels: map[string]string{"kubernetes.io/metadata.name": name},
},
}
return namespace
}
// BuildTestPod creates a test pod with given parameters.
func BuildTestPod(name string, cpu, memory int64, nodeName string, apply func(*v1.Pod)) *v1.Pod {
pod := &v1.Pod{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Pod",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: name,
@@ -73,6 +96,10 @@ func BuildTestPod(name string, cpu, memory int64, nodeName string, apply func(*v
func BuildTestPDB(name, appLabel string) *policyv1.PodDisruptionBudget {
maxUnavailable := intstr.FromInt32(1)
pdb := &policyv1.PodDisruptionBudget{
TypeMeta: metav1.TypeMeta{
APIVersion: "policy/v1",
Kind: "PodDisruptionBudget",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: name,
@@ -89,9 +116,36 @@ func BuildTestPDB(name, appLabel string) *policyv1.PodDisruptionBudget {
return pdb
}
func BuildTestPVC(name, storageClass string) *v1.PersistentVolumeClaim {
pvc := &v1.PersistentVolumeClaim{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "PersistentVolumeClaim",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: name,
},
Spec: v1.PersistentVolumeClaimSpec{
AccessModes: []v1.PersistentVolumeAccessMode{v1.ReadWriteOnce},
StorageClassName: &storageClass,
Resources: v1.VolumeResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceStorage: resource.MustParse("1Gi"),
},
},
},
}
return pvc
}
// BuildPodMetrics creates a test podmetrics with given parameters.
func BuildPodMetrics(name string, millicpu, mem int64) *v1beta1.PodMetrics {
return &v1beta1.PodMetrics{
TypeMeta: metav1.TypeMeta{
APIVersion: "metrics.k8s.io/v1beta1",
Kind: "PodMetrics",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "default",
@@ -149,6 +203,10 @@ func GetDaemonSetOwnerRefList() []metav1.OwnerReference {
// BuildTestNode creates a node with specified capacity.
func BuildTestNode(name string, millicpu, mem, pods int64, apply func(*v1.Node)) *v1.Node {
node := &v1.Node{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Node",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
SelfLink: fmt.Sprintf("/api/v1/nodes/%s", name),
@@ -179,6 +237,10 @@ func BuildTestNode(name string, millicpu, mem, pods int64, apply func(*v1.Node))
func BuildNodeMetrics(name string, millicpu, mem int64) *v1beta1.NodeMetrics {
return &v1beta1.NodeMetrics{
TypeMeta: metav1.TypeMeta{
APIVersion: "metrics.k8s.io/v1beta1",
Kind: "NodeMetrics",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
@@ -230,11 +292,30 @@ func SetNormalOwnerRef(pod *v1.Pod) {
pod.ObjectMeta.OwnerReferences = GetNormalPodOwnerRefList()
}
// SetMirrorPodAnnotation sets the given pod's annotations to mirror pod annotations
func SetMirrorPodAnnotation(pod *v1.Pod) {
pod.Annotations = GetMirrorPodAnnotation()
}
// SetPodPriority sets the given pod's priority
func SetPodPriority(pod *v1.Pod, priority int32) {
pod.Spec.Priority = &priority
}
func SetHostPathEmptyDirVolumeSource(pod *v1.Pod) {
pod.Spec.Volumes = []v1.Volume{
{
Name: "sample",
VolumeSource: v1.VolumeSource{
HostPath: &v1.HostPathVolumeSource{Path: "somePath"},
EmptyDir: &v1.EmptyDirVolumeSource{
SizeLimit: resource.NewQuantity(int64(10), resource.BinarySI),
},
},
},
}
}
// SetNodeUnschedulable sets the given node unschedulable
func SetNodeUnschedulable(node *v1.Node) {
node.Spec.Unschedulable = true
@@ -322,3 +403,18 @@ func PodWithPodAntiAffinity(inputPod *v1.Pod, labelKey, labelValue string) *v1.P
inputPod.Labels = map[string]string{labelKey: labelValue}
return inputPod
}
func RegisterEvictedPodsCollector(fakeClient *fake.Clientset, evictedPods *[]string) {
fakeClient.PrependReactor("create", "pods", func(action clientgotesting.Action) (bool, runtime.Object, error) {
if action.GetSubresource() == "eviction" {
createAct, matched := action.(clientgotesting.CreateActionImpl)
if !matched {
return false, nil, fmt.Errorf("unable to convert action to core.CreateActionImpl")
}
if eviction, matched := createAct.Object.(*policyv1.Eviction); matched {
*evictedPods = append(*evictedPods, eviction.GetName())
}
}
return false, nil, nil // fallback to the default reactor
})
}
+2 -9
View File
@@ -1,7 +1,7 @@
package backoff
import (
"math/rand"
"math/rand/v2"
"time"
)
@@ -28,13 +28,7 @@ multiplied by the exponential, that is, between 2 and 6 seconds.
Note: MaxInterval caps the RetryInterval and not the randomized interval.
If the time elapsed since an ExponentialBackOff instance is created goes past the
MaxElapsedTime, then the method NextBackOff() starts returning backoff.Stop.
The elapsed time can be reset by calling Reset().
Example: Given the following default arguments, for 10 tries the sequence will be,
and assuming we go over the MaxElapsedTime on the 10th try:
Example: Given the following default arguments, for 9 tries the sequence will be:
Request # RetryInterval (seconds) Randomized Interval (seconds)
@@ -47,7 +41,6 @@ and assuming we go over the MaxElapsedTime on the 10th try:
7 5.692 [2.846, 8.538]
8 8.538 [4.269, 12.807]
9 12.807 [6.403, 19.210]
10 19.210 backoff.Stop
Note: Implementation is not thread-safe.
*/
+2 -2
View File
@@ -47,7 +47,7 @@ func WithNotify(n Notify) RetryOption {
}
}
// WithMaxTries limits the number of retry attempts.
// WithMaxTries limits the number of all attempts.
func WithMaxTries(n uint) RetryOption {
return func(args *retryOptions) {
args.MaxTries = n
@@ -97,7 +97,7 @@ func Retry[T any](ctx context.Context, operation Operation[T], opts ...RetryOpti
// Handle permanent errors without retrying.
var permanent *PermanentError
if errors.As(err, &permanent) {
return res, err
return res, permanent.Unwrap()
}
// Stop retrying if context is cancelled.
+1
View File
@@ -0,0 +1 @@
language: go
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Daniel Deutsch
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Some files were not shown because too many files have changed in this diff Show More