mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-11-06 05:17:08 +00:00
Merge pull request #137133 from johankj/kep-4563/eviction-request-controller
EvictionRequest controller
This commit is contained in:
41 files changed
+6584
-3
No files matched your search
@@ -196,6 +196,7 @@ API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,E
|
||||
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,EndpointSliceMirroringControllerConfiguration,MirroringEndpointUpdatesBatchPeriod
|
||||
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,EndpointSliceMirroringControllerConfiguration,MirroringMaxEndpointsPerSubset
|
||||
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,EphemeralVolumeControllerConfiguration,ConcurrentEphemeralVolumeSyncs
|
||||
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,EvictionRequestControllerConfiguration,ConcurrentEvictionRequestSyncs
|
||||
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,GarbageCollectorControllerConfiguration,ConcurrentGCSyncs
|
||||
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,GarbageCollectorControllerConfiguration,EnableGarbageCollector
|
||||
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,GarbageCollectorControllerConfiguration,GCIgnoredResources
|
||||
@@ -220,6 +221,7 @@ API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,K
|
||||
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,KubeControllerManagerConfiguration,EndpointSliceController
|
||||
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,KubeControllerManagerConfiguration,EndpointSliceMirroringController
|
||||
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,KubeControllerManagerConfiguration,EphemeralVolumeController
|
||||
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,KubeControllerManagerConfiguration,EvictionRequestController
|
||||
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,KubeControllerManagerConfiguration,GarbageCollectorController
|
||||
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,KubeControllerManagerConfiguration,Generic
|
||||
API rule violation: names_match,k8s.io/kube-controller-manager/config/v1alpha1,KubeControllerManagerConfiguration,HPAController
|
||||
|
||||
@@ -223,7 +223,7 @@ func NewControllerDescriptors() map[string]*ControllerDescriptor {
|
||||
register(newServiceCIDRsControllerDescriptor())
|
||||
register(newStorageVersionMigratorControllerDescriptor())
|
||||
register(newSELinuxWarningControllerDescriptor())
|
||||
|
||||
register(newEvictionRequestControllerDescriptor())
|
||||
for _, alias := range aliases.UnsortedList() {
|
||||
if _, ok := controllers[alias]; ok {
|
||||
panic(fmt.Sprintf("alias %q conflicts with a controller name", alias))
|
||||
|
||||
@@ -98,6 +98,7 @@ func TestControllerNamesDeclaration(t *testing.T) {
|
||||
names.ServiceCIDRController,
|
||||
names.StorageVersionMigratorController,
|
||||
names.SELinuxWarningController,
|
||||
names.EvictionRequestController,
|
||||
)
|
||||
|
||||
for _, name := range KnownControllers() {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright 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 app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/component-base/featuregate"
|
||||
"k8s.io/kubernetes/cmd/kube-controller-manager/names"
|
||||
"k8s.io/kubernetes/pkg/controller/evictionrequest"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
)
|
||||
|
||||
func newEvictionRequestControllerDescriptor() *ControllerDescriptor {
|
||||
return &ControllerDescriptor{
|
||||
name: names.EvictionRequestController,
|
||||
constructor: newEvictionRequestController,
|
||||
requiredFeatureGates: []featuregate.Feature{
|
||||
features.EvictionRequestAPI,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newEvictionRequestController(ctx context.Context, controllerContext ControllerContext, controllerName string) (Controller, error) {
|
||||
client, err := controllerContext.NewClient(controllerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
evictionRequestController, err := evictionrequest.NewController(
|
||||
ctx,
|
||||
controllerName,
|
||||
controllerContext.InformerFactory.Lifecycle().V1alpha1().Evictions(),
|
||||
controllerContext.InformerFactory.Lifecycle().V1alpha1().EvictionRequests(),
|
||||
controllerContext.InformerFactory.Core().V1().Pods(),
|
||||
client,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newControllerLoop(func(ctx context.Context) {
|
||||
evictionRequestController.Run(ctx, int(controllerContext.ComponentConfig.EvictionRequestController.ConcurrentEvictionRequestSyncs))
|
||||
}, controllerName), nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
Copyright 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 options
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
evictionrequestconfig "k8s.io/kubernetes/pkg/controller/evictionrequest/config"
|
||||
)
|
||||
|
||||
const (
|
||||
minConcurrentEvictionRequestsSyncs = 1
|
||||
maxConcurrentEvictionRequestsSyncs = 50
|
||||
)
|
||||
|
||||
// EvictionRequestControllerOptions holds the EvictionRequestController options.
|
||||
type EvictionRequestControllerOptions struct {
|
||||
*evictionrequestconfig.EvictionRequestControllerConfiguration
|
||||
}
|
||||
|
||||
// AddFlags adds flags related to EvictionRequestController for controller manager to the specified FlagSet.
|
||||
func (o *EvictionRequestControllerOptions) AddFlags(fs *pflag.FlagSet) {
|
||||
if o == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fs.Int32Var(&o.ConcurrentEvictionRequestSyncs, "concurrent-eviction-request-syncs", o.ConcurrentEvictionRequestSyncs, "The number of eviction requests that are allowed to sync concurrently.")
|
||||
}
|
||||
|
||||
// ApplyTo fills up EvictionRequestController config with options.
|
||||
func (o *EvictionRequestControllerOptions) ApplyTo(cfg *evictionrequestconfig.EvictionRequestControllerConfiguration) error {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cfg.ConcurrentEvictionRequestSyncs = o.ConcurrentEvictionRequestSyncs
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate checks validation of EvictionRequestControllerOptions.
|
||||
func (o *EvictionRequestControllerOptions) Validate() []error {
|
||||
if o == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var errs []error
|
||||
|
||||
if o.ConcurrentEvictionRequestSyncs < minConcurrentEvictionRequestsSyncs {
|
||||
errs = append(errs, fmt.Errorf("concurrent-eviction-request-syncs must not be less than %d, but got %d", minConcurrentEvictionRequestsSyncs, o.ConcurrentEvictionRequestSyncs))
|
||||
} else if o.ConcurrentEvictionRequestSyncs > maxConcurrentEvictionRequestsSyncs {
|
||||
errs = append(errs, fmt.Errorf("concurrent-eviction-request-syncs must not be more than %d, but got %d", maxConcurrentEvictionRequestsSyncs, o.ConcurrentEvictionRequestSyncs))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
@@ -82,6 +82,7 @@ type KubeControllerManagerOptions struct {
|
||||
EndpointSliceController *EndpointSliceControllerOptions
|
||||
EndpointSliceMirroringController *EndpointSliceMirroringControllerOptions
|
||||
EphemeralVolumeController *EphemeralVolumeControllerOptions
|
||||
EvictionRequestController *EvictionRequestControllerOptions
|
||||
GarbageCollectorController *GarbageCollectorControllerOptions
|
||||
HPAController *HPAControllerOptions
|
||||
JobController *JobControllerOptions
|
||||
@@ -174,6 +175,9 @@ func NewKubeControllerManagerOptions() (*KubeControllerManagerOptions, error) {
|
||||
EphemeralVolumeController: &EphemeralVolumeControllerOptions{
|
||||
&componentConfig.EphemeralVolumeController,
|
||||
},
|
||||
EvictionRequestController: &EvictionRequestControllerOptions{
|
||||
&componentConfig.EvictionRequestController,
|
||||
},
|
||||
GarbageCollectorController: &GarbageCollectorControllerOptions{
|
||||
&componentConfig.GarbageCollectorController,
|
||||
},
|
||||
@@ -285,6 +289,7 @@ func (s *KubeControllerManagerOptions) Flags(allControllers []string, disabledBy
|
||||
s.EndpointSliceController.AddFlags(fss.FlagSet(names.EndpointSliceController))
|
||||
s.EndpointSliceMirroringController.AddFlags(fss.FlagSet(names.EndpointSliceMirroringController))
|
||||
s.EphemeralVolumeController.AddFlags(fss.FlagSet(names.EphemeralVolumeController))
|
||||
s.EvictionRequestController.AddFlags(fss.FlagSet(names.EvictionRequestController))
|
||||
s.GarbageCollectorController.AddFlags(fss.FlagSet(names.GarbageCollectorController))
|
||||
s.HPAController.AddFlags(fss.FlagSet(names.HorizontalPodAutoscalerController))
|
||||
s.JobController.AddFlags(fss.FlagSet(names.JobController))
|
||||
@@ -367,6 +372,9 @@ func (s *KubeControllerManagerOptions) ApplyTo(c *kubecontrollerconfig.Config, a
|
||||
if err := s.EphemeralVolumeController.ApplyTo(&c.ComponentConfig.EphemeralVolumeController); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.EvictionRequestController.ApplyTo(&c.ComponentConfig.EvictionRequestController); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.GarbageCollectorController.ApplyTo(&c.ComponentConfig.GarbageCollectorController); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -454,6 +462,7 @@ func (s *KubeControllerManagerOptions) Validate(allControllers []string, disable
|
||||
errs = append(errs, s.EndpointSliceController.Validate()...)
|
||||
errs = append(errs, s.EndpointSliceMirroringController.Validate()...)
|
||||
errs = append(errs, s.EphemeralVolumeController.Validate()...)
|
||||
errs = append(errs, s.EvictionRequestController.Validate()...)
|
||||
errs = append(errs, s.GarbageCollectorController.Validate()...)
|
||||
errs = append(errs, s.HPAController.Validate()...)
|
||||
errs = append(errs, s.JobController.Validate()...)
|
||||
|
||||
@@ -61,6 +61,7 @@ import (
|
||||
endpointconfig "k8s.io/kubernetes/pkg/controller/endpoint/config"
|
||||
endpointsliceconfig "k8s.io/kubernetes/pkg/controller/endpointslice/config"
|
||||
endpointslicemirroringconfig "k8s.io/kubernetes/pkg/controller/endpointslicemirroring/config"
|
||||
evictionrequestconfig "k8s.io/kubernetes/pkg/controller/evictionrequest/config"
|
||||
garbagecollectorconfig "k8s.io/kubernetes/pkg/controller/garbagecollector/config"
|
||||
jobconfig "k8s.io/kubernetes/pkg/controller/job/config"
|
||||
namespaceconfig "k8s.io/kubernetes/pkg/controller/namespace/config"
|
||||
@@ -109,6 +110,7 @@ var args = []string{
|
||||
"--concurrent-statefulset-syncs=15",
|
||||
"--concurrent-endpoint-syncs=10",
|
||||
"--concurrent-ephemeralvolume-syncs=10",
|
||||
"--concurrent-eviction-request-syncs=10",
|
||||
"--concurrent-service-endpoint-syncs=10",
|
||||
"--concurrent-gc-syncs=30",
|
||||
"--concurrent-namespace-syncs=20",
|
||||
@@ -317,6 +319,11 @@ func TestAddFlags(t *testing.T) {
|
||||
ConcurrentEphemeralVolumeSyncs: 10,
|
||||
},
|
||||
},
|
||||
EvictionRequestController: &EvictionRequestControllerOptions{
|
||||
&evictionrequestconfig.EvictionRequestControllerConfiguration{
|
||||
ConcurrentEvictionRequestSyncs: 10,
|
||||
},
|
||||
},
|
||||
GarbageCollectorController: &GarbageCollectorControllerOptions{
|
||||
&garbagecollectorconfig.GarbageCollectorControllerConfiguration{
|
||||
ConcurrentGCSyncs: 30,
|
||||
@@ -539,6 +546,13 @@ func TestValidateFlags(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "concurrent eviction request syncs set to 0",
|
||||
flags: []string{
|
||||
"--concurrent-eviction-request-syncs=0",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
@@ -663,6 +677,9 @@ func TestApplyTo(t *testing.T) {
|
||||
EphemeralVolumeController: ephemeralvolumeconfig.EphemeralVolumeControllerConfiguration{
|
||||
ConcurrentEphemeralVolumeSyncs: 10,
|
||||
},
|
||||
EvictionRequestController: evictionrequestconfig.EvictionRequestControllerConfiguration{
|
||||
ConcurrentEvictionRequestSyncs: 10,
|
||||
},
|
||||
GarbageCollectorController: garbagecollectorconfig.GarbageCollectorControllerConfiguration{
|
||||
ConcurrentGCSyncs: 30,
|
||||
GCIgnoredResources: []garbagecollectorconfig.GroupResource{
|
||||
@@ -1256,6 +1273,26 @@ func TestValidateControllersOptions(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "EvictionRequestControllerOptions ConcurrentEvictionRequestSyncs equal 0",
|
||||
expectErrors: true,
|
||||
expectedErrorSubString: "concurrent-eviction-request-syncs must not be less than 1",
|
||||
options: &EvictionRequestControllerOptions{
|
||||
&evictionrequestconfig.EvictionRequestControllerConfiguration{
|
||||
ConcurrentEvictionRequestSyncs: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "EvictionRequestControllerOptions ConcurrentEvictionRequestSyncs equal 0",
|
||||
expectErrors: true,
|
||||
expectedErrorSubString: "concurrent-eviction-request-syncs must not be more than 50",
|
||||
options: &EvictionRequestControllerOptions{
|
||||
&evictionrequestconfig.EvictionRequestControllerConfiguration{
|
||||
ConcurrentEvictionRequestSyncs: 51,
|
||||
},
|
||||
},
|
||||
},
|
||||
/* empty errs */
|
||||
{
|
||||
name: "CronJobControllerOptions",
|
||||
@@ -1302,6 +1339,15 @@ func TestValidateControllersOptions(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "EvictionRequestControllerOptions",
|
||||
expectErrors: false,
|
||||
options: &EvictionRequestControllerOptions{
|
||||
&evictionrequestconfig.EvictionRequestControllerConfiguration{
|
||||
ConcurrentEvictionRequestSyncs: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ResourceClaimControllerOptions",
|
||||
expectErrors: false,
|
||||
@@ -1439,6 +1485,30 @@ func TestValidateControllersOptions(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "EvictionRequestControllerOptions valid",
|
||||
expectErrors: false,
|
||||
options: &EvictionRequestControllerOptions{
|
||||
&evictionrequestconfig.EvictionRequestControllerConfiguration{
|
||||
ConcurrentEvictionRequestSyncs: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "EvictionRequestControllerOptions zero syncs",
|
||||
expectErrors: true,
|
||||
expectedErrorSubString: "concurrent-eviction-request-syncs must not be less than 1",
|
||||
options: &EvictionRequestControllerOptions{
|
||||
&evictionrequestconfig.EvictionRequestControllerConfiguration{
|
||||
ConcurrentEvictionRequestSyncs: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "EvictionRequestControllerOptions nil",
|
||||
expectErrors: false,
|
||||
options: (*EvictionRequestControllerOptions)(nil),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
||||
@@ -90,4 +90,5 @@ const (
|
||||
ServiceCIDRController = "service-cidr-controller"
|
||||
StorageVersionMigratorController = "storage-version-migrator-controller"
|
||||
SELinuxWarningController = "selinux-warning-controller"
|
||||
EvictionRequestController = "evictionrequest-controller"
|
||||
)
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
endpointconfig "k8s.io/kubernetes/pkg/controller/endpoint/config"
|
||||
endpointsliceconfig "k8s.io/kubernetes/pkg/controller/endpointslice/config"
|
||||
endpointslicemirroringconfig "k8s.io/kubernetes/pkg/controller/endpointslicemirroring/config"
|
||||
evictionrequestconfig "k8s.io/kubernetes/pkg/controller/evictionrequest/config"
|
||||
garbagecollectorconfig "k8s.io/kubernetes/pkg/controller/garbagecollector/config"
|
||||
jobconfig "k8s.io/kubernetes/pkg/controller/job/config"
|
||||
namespaceconfig "k8s.io/kubernetes/pkg/controller/namespace/config"
|
||||
@@ -97,6 +98,9 @@ type KubeControllerManagerConfiguration struct {
|
||||
// EphemeralVolumeControllerConfiguration holds configuration for EphemeralVolumeController
|
||||
// related features.
|
||||
EphemeralVolumeController ephemeralvolumeconfig.EphemeralVolumeControllerConfiguration
|
||||
// EvictionRequestControllerConfiguration holds configuration for
|
||||
// EvictionRequestController related features.
|
||||
EvictionRequestController evictionrequestconfig.EvictionRequestControllerConfiguration
|
||||
// GarbageCollectorControllerConfiguration holds configuration for
|
||||
// GarbageCollectorController related features.
|
||||
GarbageCollectorController garbagecollectorconfig.GarbageCollectorControllerConfiguration
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
endpointconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/endpoint/config/v1alpha1"
|
||||
endpointsliceconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/endpointslice/config/v1alpha1"
|
||||
endpointslicemirroringconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/endpointslicemirroring/config/v1alpha1"
|
||||
evictionrequestconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/evictionrequest/config/v1alpha1"
|
||||
garbagecollectorconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/garbagecollector/config/v1alpha1"
|
||||
jobconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/job/config/v1alpha1"
|
||||
namespaceconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/namespace/config/v1alpha1"
|
||||
@@ -90,6 +91,8 @@ func SetDefaults_KubeControllerManagerConfiguration(obj *kubectrlmgrconfigv1alph
|
||||
endpointslicemirroringconfigv1alpha1.RecommendedDefaultEndpointSliceMirroringControllerConfiguration(&obj.EndpointSliceMirroringController)
|
||||
// Use the default RecommendedDefaultEphemeralVolumeControllerConfiguration options
|
||||
ephemeralvolumeconfigv1alpha1.RecommendedDefaultEphemeralVolumeControllerConfiguration(&obj.EphemeralVolumeController)
|
||||
// Use the default RecommendedDefaultEvictionRequestControllerConfiguration options
|
||||
evictionrequestconfigv1alpha1.RecommendedDefaultEvictionRequestControllerConfiguration(&obj.EvictionRequestController)
|
||||
// Use the default RecommendedDefaultGarbageCollectorControllerConfiguration options
|
||||
garbagecollectorconfigv1alpha1.RecommendedDefaultGarbageCollectorControllerConfiguration(&obj.GarbageCollectorController)
|
||||
// Use the default RecommendedDefaultJobControllerConfiguration options
|
||||
|
||||
@@ -41,6 +41,7 @@ import (
|
||||
endpointconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/endpoint/config/v1alpha1"
|
||||
endpointsliceconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/endpointslice/config/v1alpha1"
|
||||
endpointslicemirroringconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/endpointslicemirroring/config/v1alpha1"
|
||||
evictionrequestconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/evictionrequest/config/v1alpha1"
|
||||
garbagecollectorconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/garbagecollector/config/v1alpha1"
|
||||
jobconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/job/config/v1alpha1"
|
||||
namespaceconfigv1alpha1 "k8s.io/kubernetes/pkg/controller/namespace/config/v1alpha1"
|
||||
@@ -181,6 +182,9 @@ func autoConvert_v1alpha1_KubeControllerManagerConfiguration_To_config_KubeContr
|
||||
if err := ephemeralconfigv1alpha1.Convert_v1alpha1_EphemeralVolumeControllerConfiguration_To_config_EphemeralVolumeControllerConfiguration(&in.EphemeralVolumeController, &out.EphemeralVolumeController, s); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := evictionrequestconfigv1alpha1.Convert_v1alpha1_EvictionRequestControllerConfiguration_To_config_EvictionRequestControllerConfiguration(&in.EvictionRequestController, &out.EvictionRequestController, s); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := garbagecollectorconfigv1alpha1.Convert_v1alpha1_GarbageCollectorControllerConfiguration_To_config_GarbageCollectorControllerConfiguration(&in.GarbageCollectorController, &out.GarbageCollectorController, s); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -289,6 +293,9 @@ func autoConvert_config_KubeControllerManagerConfiguration_To_v1alpha1_KubeContr
|
||||
if err := ephemeralconfigv1alpha1.Convert_config_EphemeralVolumeControllerConfiguration_To_v1alpha1_EphemeralVolumeControllerConfiguration(&in.EphemeralVolumeController, &out.EphemeralVolumeController, s); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := evictionrequestconfigv1alpha1.Convert_config_EvictionRequestControllerConfiguration_To_v1alpha1_EvictionRequestControllerConfiguration(&in.EvictionRequestController, &out.EvictionRequestController, s); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := garbagecollectorconfigv1alpha1.Convert_config_GarbageCollectorControllerConfiguration_To_v1alpha1_GarbageCollectorControllerConfiguration(&in.GarbageCollectorController, &out.GarbageCollectorController, s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ func (in *KubeControllerManagerConfiguration) DeepCopyInto(out *KubeControllerMa
|
||||
out.EndpointSliceController = in.EndpointSliceController
|
||||
out.EndpointSliceMirroringController = in.EndpointSliceMirroringController
|
||||
out.EphemeralVolumeController = in.EphemeralVolumeController
|
||||
out.EvictionRequestController = in.EvictionRequestController
|
||||
in.GarbageCollectorController.DeepCopyInto(&out.GarbageCollectorController)
|
||||
out.HPAController = in.HPAController
|
||||
out.JobController = in.JobController
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
approvers:
|
||||
- sig-node-approvers
|
||||
- atiratree
|
||||
reviewers:
|
||||
- atiratree
|
||||
labels:
|
||||
- sig/node
|
||||
- wg/node-lifecycle
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
Copyright 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.
|
||||
*/
|
||||
|
||||
// +k8s:deepcopy-gen=package
|
||||
|
||||
package config
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
Copyright 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 config
|
||||
|
||||
// EvictionRequestControllerConfiguration contains elements describing EvictionRequestController.
|
||||
type EvictionRequestControllerConfiguration struct {
|
||||
// ConcurrentEvictionRequestSyncs is the number of eviction requests and evictions that are
|
||||
// allowed to sync concurrently.
|
||||
ConcurrentEvictionRequestSyncs int32
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
Copyright 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 v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/conversion"
|
||||
"k8s.io/kube-controller-manager/config/v1alpha1"
|
||||
"k8s.io/kubernetes/pkg/controller/evictionrequest/config"
|
||||
)
|
||||
|
||||
// Important! The public back-and-forth conversion functions for the types in this package
|
||||
// with EvictionRequestControllerConfiguration types need to be manually exposed like this in order for
|
||||
// other packages that reference this package to be able to call these conversion functions
|
||||
// in an autogenerated manner.
|
||||
// TODO: Fix the bug in conversion-gen so it automatically discovers these Convert_* functions
|
||||
// in autogenerated code as well.
|
||||
|
||||
// Convert_v1alpha1_EvictionRequestControllerConfiguration_To_config_EvictionRequestControllerConfiguration is an autogenerated conversion function.
|
||||
func Convert_v1alpha1_EvictionRequestControllerConfiguration_To_config_EvictionRequestControllerConfiguration(in *v1alpha1.EvictionRequestControllerConfiguration, out *config.EvictionRequestControllerConfiguration, s conversion.Scope) error {
|
||||
return autoConvert_v1alpha1_EvictionRequestControllerConfiguration_To_config_EvictionRequestControllerConfiguration(in, out, s)
|
||||
}
|
||||
|
||||
// Convert_config_EvictionRequestControllerConfiguration_To_v1alpha1_EvictionRequestControllerConfiguration is an autogenerated conversion function.
|
||||
func Convert_config_EvictionRequestControllerConfiguration_To_v1alpha1_EvictionRequestControllerConfiguration(in *config.EvictionRequestControllerConfiguration, out *v1alpha1.EvictionRequestControllerConfiguration, s conversion.Scope) error {
|
||||
return autoConvert_config_EvictionRequestControllerConfiguration_To_v1alpha1_EvictionRequestControllerConfiguration(in, out, s)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
Copyright 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 v1alpha1
|
||||
|
||||
import (
|
||||
kubectrlmgrconfigv1alpha1 "k8s.io/kube-controller-manager/config/v1alpha1"
|
||||
)
|
||||
|
||||
// RecommendedDefaultEvictionRequestControllerConfiguration defaults a pointer to a
|
||||
// EvictionRequestControllerConfiguration struct. This will set the recommended default
|
||||
// values, but they may be subject to change between API versions. This function
|
||||
// is intentionally not registered in the scheme as a "normal" `SetDefaults_Foo`
|
||||
// function to allow consumers of this type to set whatever defaults for their
|
||||
// embedded configs. Forcing consumers to use these defaults would be problematic
|
||||
// as defaulting in the scheme is done as part of the conversion, and there would
|
||||
// be no easy way to opt-out. Instead, if you want to use this defaulting method
|
||||
// run it in your wrapper struct of this type in its `SetDefaults_` method.
|
||||
func RecommendedDefaultEvictionRequestControllerConfiguration(obj *kubectrlmgrconfigv1alpha1.EvictionRequestControllerConfiguration) {
|
||||
if obj.ConcurrentEvictionRequestSyncs == 0 {
|
||||
obj.ConcurrentEvictionRequestSyncs = 5
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
Copyright 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.
|
||||
*/
|
||||
|
||||
// +k8s:deepcopy-gen=package
|
||||
// +k8s:conversion-gen=k8s.io/kubernetes/pkg/controller/evictionrequest/config
|
||||
// +k8s:conversion-gen-external-types=k8s.io/kube-controller-manager/config/v1alpha1
|
||||
|
||||
package v1alpha1
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
Copyright 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 v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
var (
|
||||
// SchemeBuilder is the scheme builder with scheme init functions to run for this API package
|
||||
SchemeBuilder runtime.SchemeBuilder
|
||||
// localSchemeBuilder extends the SchemeBuilder instance with the external types. In this package,
|
||||
// defaulting and conversion init funcs are registered as well.
|
||||
localSchemeBuilder = &SchemeBuilder
|
||||
// AddToScheme is a global function that registers this API group & version to a scheme
|
||||
AddToScheme = localSchemeBuilder.AddToScheme
|
||||
)
|
||||
@@ -0,0 +1,92 @@
|
||||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 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.
|
||||
*/
|
||||
|
||||
// Code generated by conversion-gen. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
unsafe "unsafe"
|
||||
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
conversion "k8s.io/apimachinery/pkg/conversion"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
configv1alpha1 "k8s.io/kube-controller-manager/config/v1alpha1"
|
||||
config "k8s.io/kubernetes/pkg/controller/evictionrequest/config"
|
||||
)
|
||||
|
||||
func init() {
|
||||
localSchemeBuilder.Register(RegisterConversions)
|
||||
}
|
||||
|
||||
// RegisterConversions adds conversion functions to the given scheme.
|
||||
// Public to allow building arbitrary schemes.
|
||||
func RegisterConversions(s *runtime.Scheme) error {
|
||||
if err := s.AddGeneratedConversionFunc((*configv1alpha1.GroupResource)(nil), (*v1.GroupResource)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1alpha1_GroupResource_To_v1_GroupResource(a.(*configv1alpha1.GroupResource), b.(*v1.GroupResource), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddGeneratedConversionFunc((*v1.GroupResource)(nil), (*configv1alpha1.GroupResource)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1_GroupResource_To_v1alpha1_GroupResource(a.(*v1.GroupResource), b.(*configv1alpha1.GroupResource), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddConversionFunc((*config.EvictionRequestControllerConfiguration)(nil), (*configv1alpha1.EvictionRequestControllerConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_config_EvictionRequestControllerConfiguration_To_v1alpha1_EvictionRequestControllerConfiguration(a.(*config.EvictionRequestControllerConfiguration), b.(*configv1alpha1.EvictionRequestControllerConfiguration), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.AddConversionFunc((*configv1alpha1.EvictionRequestControllerConfiguration)(nil), (*config.EvictionRequestControllerConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error {
|
||||
return Convert_v1alpha1_EvictionRequestControllerConfiguration_To_config_EvictionRequestControllerConfiguration(a.(*configv1alpha1.EvictionRequestControllerConfiguration), b.(*config.EvictionRequestControllerConfiguration), scope)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha1_EvictionRequestControllerConfiguration_To_config_EvictionRequestControllerConfiguration(in *configv1alpha1.EvictionRequestControllerConfiguration, out *config.EvictionRequestControllerConfiguration, s conversion.Scope) error {
|
||||
out.ConcurrentEvictionRequestSyncs = in.ConcurrentEvictionRequestSyncs
|
||||
return nil
|
||||
}
|
||||
|
||||
func autoConvert_config_EvictionRequestControllerConfiguration_To_v1alpha1_EvictionRequestControllerConfiguration(in *config.EvictionRequestControllerConfiguration, out *configv1alpha1.EvictionRequestControllerConfiguration, s conversion.Scope) error {
|
||||
out.ConcurrentEvictionRequestSyncs = in.ConcurrentEvictionRequestSyncs
|
||||
return nil
|
||||
}
|
||||
|
||||
func autoConvert_v1alpha1_GroupResource_To_v1_GroupResource(in *configv1alpha1.GroupResource, out *v1.GroupResource, s conversion.Scope) error {
|
||||
*out = *(*v1.GroupResource)(unsafe.Pointer(in))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_v1alpha1_GroupResource_To_v1_GroupResource is an autogenerated conversion function.
|
||||
func Convert_v1alpha1_GroupResource_To_v1_GroupResource(in *configv1alpha1.GroupResource, out *v1.GroupResource, s conversion.Scope) error {
|
||||
return autoConvert_v1alpha1_GroupResource_To_v1_GroupResource(in, out, s)
|
||||
}
|
||||
|
||||
func autoConvert_v1_GroupResource_To_v1alpha1_GroupResource(in *v1.GroupResource, out *configv1alpha1.GroupResource, s conversion.Scope) error {
|
||||
*out = *(*configv1alpha1.GroupResource)(unsafe.Pointer(in))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert_v1_GroupResource_To_v1alpha1_GroupResource is an autogenerated conversion function.
|
||||
func Convert_v1_GroupResource_To_v1alpha1_GroupResource(in *v1.GroupResource, out *configv1alpha1.GroupResource, s conversion.Scope) error {
|
||||
return autoConvert_v1_GroupResource_To_v1alpha1_GroupResource(in, out, s)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
//go:build !ignore_autogenerated
|
||||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 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.
|
||||
*/
|
||||
|
||||
// Code generated by deepcopy-gen. DO NOT EDIT.
|
||||
|
||||
package config
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *EvictionRequestControllerConfiguration) DeepCopyInto(out *EvictionRequestControllerConfiguration) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EvictionRequestControllerConfiguration.
|
||||
func (in *EvictionRequestControllerConfiguration) DeepCopy() *EvictionRequestControllerConfiguration {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(EvictionRequestControllerConfiguration)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
Copyright 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 evictionrequest contains a controller that manages EvictionRequests
|
||||
// and Evictions, and coordinates the graceful eviction of pods.
|
||||
package evictionrequest
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
Copyright 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 evictionrequest
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
lifecyclev1alpha1 "k8s.io/api/lifecycle/v1alpha1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
)
|
||||
|
||||
// hasCompleted returns true if the Eviction has reached
|
||||
// a terminal state (Canceled or Evicted condition is True).
|
||||
func hasEvictionCompleted(eviction *lifecyclev1alpha1.Eviction) bool {
|
||||
return hasEvictionSucceeded(eviction) || hasEvictionFailed(eviction)
|
||||
}
|
||||
|
||||
func hasEvictionSucceeded(eviction *lifecyclev1alpha1.Eviction) bool {
|
||||
return meta.IsStatusConditionTrue(eviction.Status.Conditions, string(lifecyclev1alpha1.EvictionConditionTargetEvicted))
|
||||
}
|
||||
|
||||
func hasEvictionFailed(eviction *lifecyclev1alpha1.Eviction) bool {
|
||||
return meta.IsStatusConditionTrue(eviction.Status.Conditions, string(lifecyclev1alpha1.EvictionConditionFailed))
|
||||
}
|
||||
|
||||
// getOrInitializeTargetResponders initializers the target responders list.
|
||||
// Returns the existing list if already initialized, or a new list from the target's eviction responders.
|
||||
// The second value returns true if the list is initialized for the first time.
|
||||
func getOrInitializeTargetResponders(eviction *lifecyclev1alpha1.Eviction, target targetInfo) ([]lifecyclev1alpha1.TargetResponder, bool) {
|
||||
// TargetResponders entries cannot be added or removed after first initialization
|
||||
if len(eviction.Status.TargetResponders) > 0 {
|
||||
targets := make([]lifecyclev1alpha1.TargetResponder, len(eviction.Status.TargetResponders))
|
||||
copy(targets, eviction.Status.TargetResponders)
|
||||
return targets, false
|
||||
}
|
||||
|
||||
responders := target.evictionResponders(true)
|
||||
if len(responders) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
targets := make([]lifecyclev1alpha1.TargetResponder, 0, len(responders))
|
||||
for _, responder := range responders {
|
||||
targets = append(targets, lifecyclev1alpha1.TargetResponder{
|
||||
Name: responder.Name,
|
||||
Priority: responder.Priority,
|
||||
State: lifecyclev1alpha1.ResponderStateInactive,
|
||||
})
|
||||
}
|
||||
return targets, true
|
||||
}
|
||||
|
||||
// computeResponderProgression computes the progression of responder states.
|
||||
func computeResponderProgression(now time.Time, eviction *lifecyclev1alpha1.Eviction, targetResponders []lifecyclev1alpha1.TargetResponder, terminalTime *time.Time, targetIsGone, targetIsTerminal, evictionIsCanceled bool) (bool, *time.Duration) {
|
||||
// No target responders: nothing to process
|
||||
if len(targetResponders) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
switch targetResponders[len(targetResponders)-1].State {
|
||||
case lifecyclev1alpha1.ResponderStateInterrupted,
|
||||
lifecyclev1alpha1.ResponderStateCanceled,
|
||||
lifecyclev1alpha1.ResponderStateCompleted:
|
||||
// no other progression possible
|
||||
return true, nil
|
||||
}
|
||||
activeIdx := findTargetResponderIdx(targetResponders, lifecyclev1alpha1.ResponderStateActive)
|
||||
activeResponderNotFound := activeIdx == -1
|
||||
switch {
|
||||
case targetIsGone || targetIsTerminal:
|
||||
if activeResponderNotFound {
|
||||
// all responder work is done - do not start a new one
|
||||
return false, nil
|
||||
}
|
||||
activeResponderStatus := findResponderStatus(eviction.Status.Responders, targetResponders[activeIdx].Name)
|
||||
if activeResponderStatus != nil && activeResponderStatus.CompletionTime != nil {
|
||||
// successful completion
|
||||
targetResponders[activeIdx].State = lifecyclev1alpha1.ResponderStateCompleted
|
||||
return false, nil
|
||||
}
|
||||
if deferForResponderUpdate := shouldDeferCompletion(now, activeResponderStatus, terminalTime); deferForResponderUpdate != nil {
|
||||
// the responder might report status later
|
||||
return false, deferForResponderUpdate
|
||||
}
|
||||
// responder got stuck reporting the completion time
|
||||
targetResponders[activeIdx].State = lifecyclev1alpha1.ResponderStateInterrupted
|
||||
return false, nil
|
||||
case evictionIsCanceled:
|
||||
if activeResponderNotFound {
|
||||
// all responder work is done - do not start a new one
|
||||
return false, nil
|
||||
}
|
||||
// canceled
|
||||
targetResponders[activeIdx].State = lifecyclev1alpha1.ResponderStateCanceled
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// activate the first responder
|
||||
if activeResponderNotFound {
|
||||
activeIdx = findTargetResponderIdx(targetResponders, lifecyclev1alpha1.ResponderStateInactive)
|
||||
}
|
||||
|
||||
activeResponderStatus := findResponderStatus(eviction.Status.Responders, targetResponders[activeIdx].Name)
|
||||
assignedResponderState, resyncAfter := computeResponderStateAndNextResync(now, activeResponderStatus)
|
||||
targetResponders[activeIdx].State = assignedResponderState
|
||||
if assignedResponderState != lifecyclev1alpha1.ResponderStateActive && activeIdx+1 < len(targetResponders) {
|
||||
// activate the next one
|
||||
targetResponders[activeIdx+1].State = lifecyclev1alpha1.ResponderStateActive
|
||||
resyncAfter = new(ResponderHeartbeatTimeout)
|
||||
}
|
||||
return false, resyncAfter
|
||||
}
|
||||
|
||||
// computeResponderStateAndNextResync determines if we should advance from the current responder.
|
||||
// Returns (current responder state, resyncAfter). If not advancing, resyncAfter indicates when to check again.
|
||||
func computeResponderStateAndNextResync(now time.Time, status *lifecyclev1alpha1.ResponderStatus) (lifecyclev1alpha1.ResponderStateType, *time.Duration) {
|
||||
// First sync, advance as there is no current active responder
|
||||
if status == nil {
|
||||
return lifecyclev1alpha1.ResponderStateActive, new(ResponderHeartbeatTimeout)
|
||||
}
|
||||
|
||||
// Advance as responder has completed
|
||||
if status.CompletionTime != nil {
|
||||
return lifecyclev1alpha1.ResponderStateCompleted, nil
|
||||
}
|
||||
// If there is no startTime, we will set it during the same sync, so we can set now here.
|
||||
lastUpdate := now
|
||||
if status.StartTime != nil {
|
||||
lastUpdate = status.StartTime.Time
|
||||
}
|
||||
if status.HeartbeatTime != nil {
|
||||
lastUpdate = status.HeartbeatTime.Time
|
||||
}
|
||||
|
||||
elapsed := now.Sub(lastUpdate)
|
||||
// Advance as heartbeat timeout has been reached
|
||||
if elapsed >= ResponderHeartbeatTimeout {
|
||||
return lifecyclev1alpha1.ResponderStateInterrupted, nil
|
||||
}
|
||||
// Schedule resync when timeout would occur
|
||||
return lifecyclev1alpha1.ResponderStateActive, new(ResponderHeartbeatTimeout - elapsed)
|
||||
|
||||
}
|
||||
|
||||
func sortEvictionRequestsByRelevance(sortedRequests []*lifecyclev1alpha1.EvictionRequest) {
|
||||
slices.SortStableFunc(sortedRequests, func(a *lifecyclev1alpha1.EvictionRequest, b *lifecyclev1alpha1.EvictionRequest) int {
|
||||
aExists := !a.CreationTimestamp.IsZero()
|
||||
bExists := !b.CreationTimestamp.IsZero()
|
||||
aDeleted := !aExists || a.DeletionTimestamp != nil
|
||||
bDeleted := !bExists || b.DeletionTimestamp != nil
|
||||
// Prefer existing EvictionRequest objects over old eviction.Status.Requesters
|
||||
if aExists && !bExists {
|
||||
return -1
|
||||
}
|
||||
if !aExists && bExists {
|
||||
return 1
|
||||
}
|
||||
// Prefer non deleted EvictionRequest (deleted are considered withdrawn)
|
||||
if !aDeleted && bDeleted {
|
||||
return -1
|
||||
}
|
||||
if aDeleted && !bDeleted {
|
||||
return 1
|
||||
}
|
||||
// Prefer eviction intents over withdrawn.
|
||||
if a.Spec.Intent != b.Spec.Intent && a.Spec.Intent == lifecyclev1alpha1.EvictionRequestIntentEviction && !aDeleted { // Deleted default to Withdrawn.
|
||||
return -1
|
||||
}
|
||||
if a.Spec.Intent != b.Spec.Intent && b.Spec.Intent == lifecyclev1alpha1.EvictionRequestIntentEviction && !bDeleted { // Deleted default to Withdrawn.
|
||||
return 1
|
||||
}
|
||||
// Prefer oldest since they are already present in the status, so we don't do unnecessary updates
|
||||
cmp := a.CreationTimestamp.Time.Compare(b.CreationTimestamp.Time)
|
||||
if cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
// Compare names if the timestamp is the same.
|
||||
return strings.Compare(a.Spec.Requester, b.Spec.Requester)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,601 @@
|
||||
/*
|
||||
Copyright 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 evictionrequest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
lifecyclev1alpha1 "k8s.io/api/lifecycle/v1alpha1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
testingclock "k8s.io/utils/clock/testing"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
const (
|
||||
targetResponderA = "responder-a"
|
||||
targetResponderB = "responder-b"
|
||||
)
|
||||
|
||||
func TestEvictionCondition(t *testing.T) {
|
||||
now := time.Now()
|
||||
clock := testingclock.NewFakeClock(now)
|
||||
tests := []struct {
|
||||
name string
|
||||
eviction *lifecyclev1alpha1.Eviction
|
||||
expectSucceeded bool
|
||||
expectFailed bool
|
||||
}{
|
||||
{
|
||||
name: "failed",
|
||||
eviction: mkValidEviction("pod-1-foo", "foo", "uid-1",
|
||||
addConditionTrue(clock, lifecyclev1alpha1.EvictionConditionFailed, lifecyclev1alpha1.EvictionConditionReasonNoFurtherResponder),
|
||||
),
|
||||
expectFailed: true,
|
||||
},
|
||||
{
|
||||
name: "succeeded",
|
||||
eviction: mkValidEviction("pod-1-foo", "foo", "uid-1",
|
||||
addConditionTrue(clock, lifecyclev1alpha1.EvictionConditionTargetEvicted, lifecyclev1alpha1.EvictionConditionReasonPodDeleted),
|
||||
),
|
||||
expectSucceeded: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: "in_progress",
|
||||
eviction: mkValidEviction("pod-1-foo", "foo", "uid-1",
|
||||
addCondition(clock, lifecyclev1alpha1.EvictionConditionFailed, metav1.ConditionFalse, lifecyclev1alpha1.EvictionConditionReasonNoFurtherResponder),
|
||||
),
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := hasEvictionFailed(tc.eviction); tc.expectFailed != got {
|
||||
t.Errorf("got eviction failed %v, expected %v", got, tc.expectFailed)
|
||||
}
|
||||
if got := hasEvictionSucceeded(tc.eviction); tc.expectSucceeded != got {
|
||||
t.Errorf("got succeeded failed %v, expected %v", got, tc.expectSucceeded)
|
||||
}
|
||||
|
||||
expectCompleted := (tc.expectFailed || tc.expectSucceeded)
|
||||
if got := hasEvictionCompleted(tc.eviction); expectCompleted != got {
|
||||
t.Errorf("got completed failed %v, expected %v", got, expectCompleted)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOrInitializeTargetResponders(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
target targetInfo
|
||||
eviction lifecyclev1alpha1.Eviction
|
||||
want []lifecyclev1alpha1.TargetResponder
|
||||
expectChanged bool
|
||||
}{
|
||||
{
|
||||
name: "pod_with_responders_plus_default_ones",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"),
|
||||
mkValidPod("my-pod", "uid-1", setEvictionResponders(
|
||||
v1.EvictionResponder{Name: targetResponderA, Priority: new(int32(1000))},
|
||||
v1.EvictionResponder{Name: targetResponderB, Priority: new(int32(15000))},
|
||||
)),
|
||||
),
|
||||
want: []lifecyclev1alpha1.TargetResponder{
|
||||
{Name: targetResponderA, Priority: new(int32(1000)), State: lifecyclev1alpha1.ResponderStateInactive},
|
||||
{Name: targetResponderB, Priority: new(int32(15000)), State: lifecyclev1alpha1.ResponderStateInactive},
|
||||
targetResponder(lifecyclev1alpha1.EvictionResponderImperativeEviction, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
expectChanged: true,
|
||||
},
|
||||
{
|
||||
name: "pod_with_just_default_responders",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"), mkValidPod("my-pod", "uid-1")),
|
||||
want: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(lifecyclev1alpha1.EvictionResponderImperativeEviction, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
expectChanged: true,
|
||||
},
|
||||
{
|
||||
name: "pod_not_found",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"), nil),
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "pod_with_responders_plus_default_ones_changed_but_copied_from_the_last_status_instead",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"),
|
||||
mkValidPod("my-pod", "uid-1", setEvictionResponders(
|
||||
v1.EvictionResponder{Name: targetResponderA, Priority: new(int32(1000))},
|
||||
v1.EvictionResponder{Name: targetResponderB, Priority: new(int32(15000))},
|
||||
)),
|
||||
),
|
||||
eviction: *mkValidEviction("pod-1-my-pod", "my-pod", "uid-1",
|
||||
addTargetResponders("foo.example.com/bar", "foo.example.com/baz"),
|
||||
setStateFor(lifecyclev1alpha1.ResponderStateActive, 0)),
|
||||
want: []lifecyclev1alpha1.TargetResponder{
|
||||
{Name: "foo.example.com/bar", Priority: new(int32(5000)), State: lifecyclev1alpha1.ResponderStateActive},
|
||||
{Name: "foo.example.com/baz", Priority: new(int32(4999)), State: lifecyclev1alpha1.ResponderStateInactive},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, changed := getOrInitializeTargetResponders(&tc.eviction, tc.target)
|
||||
if diff := cmp.Diff(tc.want, got); len(diff) > 0 {
|
||||
t.Fatalf("unexpected targetResponders (-want +got):\n%s", diff)
|
||||
}
|
||||
if changed != tc.expectChanged {
|
||||
t.Errorf("got changed %v, want %v", changed, tc.expectChanged)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeResponderProgression(t *testing.T) {
|
||||
clock := testingclock.NewFakePassiveClock(time.Now())
|
||||
testCases := []struct {
|
||||
name string
|
||||
terminalTime *time.Time
|
||||
isGone bool
|
||||
isTerminal bool
|
||||
isCanceled bool
|
||||
statusResponders []lifecyclev1alpha1.ResponderStatus
|
||||
targetResponders []lifecyclev1alpha1.TargetResponder
|
||||
expectedTargetResponders []lifecyclev1alpha1.TargetResponder
|
||||
expectProgressionDone bool
|
||||
expectResync *time.Duration
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
targetResponders: []lifecyclev1alpha1.TargetResponder{},
|
||||
expectedTargetResponders: []lifecyclev1alpha1.TargetResponder{},
|
||||
expectProgressionDone: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: "activate_the_first_one",
|
||||
targetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateInactive),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateInactive),
|
||||
targetResponder(lifecyclev1alpha1.EvictionResponderImperativeEviction, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
expectedTargetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateActive),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateInactive),
|
||||
targetResponder(lifecyclev1alpha1.EvictionResponderImperativeEviction, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
expectResync: new(ResponderHeartbeatTimeout),
|
||||
},
|
||||
{
|
||||
name: "activate_the_second_one",
|
||||
statusResponders: []lifecyclev1alpha1.ResponderStatus{
|
||||
{Name: targetResponderA, StartTime: new(metav1.Now()), CompletionTime: new(metav1.Now())},
|
||||
{Name: targetResponderB},
|
||||
{Name: lifecyclev1alpha1.EvictionResponderImperativeEviction},
|
||||
},
|
||||
targetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateActive),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateInactive),
|
||||
targetResponder(lifecyclev1alpha1.EvictionResponderImperativeEviction, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
expectedTargetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateActive),
|
||||
targetResponder(lifecyclev1alpha1.EvictionResponderImperativeEviction, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
expectResync: new(ResponderHeartbeatTimeout),
|
||||
},
|
||||
{
|
||||
name: "activate_the_third_one",
|
||||
statusResponders: []lifecyclev1alpha1.ResponderStatus{
|
||||
{Name: targetResponderA, StartTime: new(metav1.Now()), CompletionTime: new(metav1.Now())},
|
||||
{Name: targetResponderB, StartTime: new(metav1.Now()), CompletionTime: new(metav1.Now())},
|
||||
{Name: lifecyclev1alpha1.EvictionResponderImperativeEviction},
|
||||
},
|
||||
targetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateActive),
|
||||
targetResponder(lifecyclev1alpha1.EvictionResponderImperativeEviction, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
expectedTargetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
targetResponder(lifecyclev1alpha1.EvictionResponderImperativeEviction, lifecyclev1alpha1.ResponderStateActive),
|
||||
},
|
||||
expectResync: new(ResponderHeartbeatTimeout),
|
||||
},
|
||||
{
|
||||
name: "complete_the_third_one",
|
||||
statusResponders: []lifecyclev1alpha1.ResponderStatus{
|
||||
{Name: targetResponderA, StartTime: new(metav1.Now()), CompletionTime: new(metav1.Now())},
|
||||
{Name: targetResponderB, StartTime: new(metav1.Now()), CompletionTime: new(metav1.Now())},
|
||||
{Name: lifecyclev1alpha1.EvictionResponderImperativeEviction, StartTime: new(metav1.Now()), CompletionTime: new(metav1.Now())},
|
||||
},
|
||||
targetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
targetResponder(lifecyclev1alpha1.EvictionResponderImperativeEviction, lifecyclev1alpha1.ResponderStateActive),
|
||||
},
|
||||
expectedTargetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
targetResponder(lifecyclev1alpha1.EvictionResponderImperativeEviction, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no_change_after_completion",
|
||||
statusResponders: []lifecyclev1alpha1.ResponderStatus{
|
||||
{Name: targetResponderA, StartTime: new(metav1.Now()), CompletionTime: new(metav1.Now())},
|
||||
{Name: targetResponderB, StartTime: new(metav1.Now()), CompletionTime: new(metav1.Now())},
|
||||
{Name: lifecyclev1alpha1.EvictionResponderImperativeEviction, StartTime: new(metav1.Now()), CompletionTime: new(metav1.Now())},
|
||||
},
|
||||
targetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
targetResponder(lifecyclev1alpha1.EvictionResponderImperativeEviction, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
},
|
||||
expectedTargetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
targetResponder(lifecyclev1alpha1.EvictionResponderImperativeEviction, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
},
|
||||
expectProgressionDone: true,
|
||||
},
|
||||
{
|
||||
name: "no_change_after_last_interruption",
|
||||
statusResponders: []lifecyclev1alpha1.ResponderStatus{
|
||||
{Name: targetResponderA, StartTime: new(metav1.Now()), CompletionTime: new(metav1.Now())},
|
||||
{Name: targetResponderB, StartTime: new(metav1.Now()), CompletionTime: new(metav1.Now())},
|
||||
{Name: lifecyclev1alpha1.EvictionResponderImperativeEviction, StartTime: new(metav1.Now())},
|
||||
},
|
||||
targetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
targetResponder(lifecyclev1alpha1.EvictionResponderImperativeEviction, lifecyclev1alpha1.ResponderStateInterrupted),
|
||||
},
|
||||
expectedTargetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
targetResponder(lifecyclev1alpha1.EvictionResponderImperativeEviction, lifecyclev1alpha1.ResponderStateInterrupted),
|
||||
},
|
||||
expectProgressionDone: true,
|
||||
},
|
||||
{
|
||||
name: "no_change_after_last_cancellation",
|
||||
statusResponders: []lifecyclev1alpha1.ResponderStatus{
|
||||
{Name: targetResponderA, StartTime: new(metav1.Now()), CompletionTime: new(metav1.Now())},
|
||||
{Name: targetResponderB, StartTime: new(metav1.Now()), CompletionTime: new(metav1.Now())},
|
||||
{Name: lifecyclev1alpha1.EvictionResponderImperativeEviction, StartTime: new(metav1.Now())},
|
||||
},
|
||||
targetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
targetResponder(lifecyclev1alpha1.EvictionResponderImperativeEviction, lifecyclev1alpha1.ResponderStateCanceled),
|
||||
},
|
||||
expectedTargetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
targetResponder(lifecyclev1alpha1.EvictionResponderImperativeEviction, lifecyclev1alpha1.ResponderStateCanceled),
|
||||
},
|
||||
expectProgressionDone: true,
|
||||
},
|
||||
{
|
||||
name: "reschedule_resync_according_to_the_start_time",
|
||||
statusResponders: []lifecyclev1alpha1.ResponderStatus{
|
||||
{Name: targetResponderA, StartTime: new(metav1.Time{Time: clock.Now().Add(-1 * time.Minute)})},
|
||||
{Name: targetResponderB},
|
||||
},
|
||||
targetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateActive),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
expectedTargetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateActive),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
expectResync: new(ResponderHeartbeatTimeout - 1*time.Minute),
|
||||
},
|
||||
{
|
||||
name: "reschedule_resync_according_to_the_heartbeat_time",
|
||||
statusResponders: []lifecyclev1alpha1.ResponderStatus{
|
||||
{
|
||||
Name: targetResponderA,
|
||||
StartTime: new(metav1.Time{Time: clock.Now().Add(-25 * time.Minute)}),
|
||||
HeartbeatTime: new(metav1.Time{Time: clock.Now().Add(-2 * time.Minute)}),
|
||||
},
|
||||
{Name: targetResponderB},
|
||||
},
|
||||
targetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateActive),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
expectedTargetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateActive),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
expectResync: new(ResponderHeartbeatTimeout - 2*time.Minute),
|
||||
},
|
||||
{
|
||||
name: "interrupt_the_first_after_the_heartbeat_has_elapsed_and_activate_the_second_one",
|
||||
statusResponders: []lifecyclev1alpha1.ResponderStatus{
|
||||
{
|
||||
Name: targetResponderA,
|
||||
StartTime: new(metav1.Time{Time: clock.Now().Add(-25 * time.Minute)}),
|
||||
HeartbeatTime: new(metav1.Time{Time: clock.Now().Add(-20 * time.Minute)}),
|
||||
},
|
||||
{Name: targetResponderB},
|
||||
},
|
||||
targetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateActive),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
expectedTargetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateInterrupted),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateActive),
|
||||
},
|
||||
expectResync: new(ResponderHeartbeatTimeout),
|
||||
},
|
||||
{
|
||||
name: "interrupt_the_first_after_the_heartbeat_(start_time_fallback_has_elapsed_and_activate_the_second_one)",
|
||||
statusResponders: []lifecyclev1alpha1.ResponderStatus{
|
||||
{Name: targetResponderA, StartTime: new(metav1.Time{Time: clock.Now().Add(-21 * time.Minute)})},
|
||||
{Name: targetResponderB},
|
||||
},
|
||||
targetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateActive),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
expectedTargetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateInterrupted),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateActive),
|
||||
},
|
||||
expectResync: new(ResponderHeartbeatTimeout),
|
||||
},
|
||||
{
|
||||
name: "complete_after_the_target_is_gone",
|
||||
isGone: true,
|
||||
statusResponders: []lifecyclev1alpha1.ResponderStatus{
|
||||
{Name: targetResponderA, StartTime: new(metav1.Now()), CompletionTime: new(metav1.Now())},
|
||||
{Name: targetResponderB},
|
||||
},
|
||||
targetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateActive),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
expectedTargetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "complete_after_the_target_is_terminal_with_completion_time_set",
|
||||
isTerminal: true,
|
||||
statusResponders: []lifecyclev1alpha1.ResponderStatus{
|
||||
{Name: targetResponderA, StartTime: new(metav1.Now()), CompletionTime: new(metav1.Now())},
|
||||
{Name: targetResponderB},
|
||||
},
|
||||
targetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateActive),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
expectedTargetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateCompleted),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "interrupt_after_the_target_is_gone_without_completion_time_set",
|
||||
isGone: true,
|
||||
statusResponders: []lifecyclev1alpha1.ResponderStatus{
|
||||
{Name: targetResponderA, StartTime: new(metav1.Now())},
|
||||
{Name: targetResponderB},
|
||||
},
|
||||
targetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateActive),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
expectedTargetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateInterrupted),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "interrupt_after_the_target_is_terminal_without_completion_time_set",
|
||||
isTerminal: true,
|
||||
terminalTime: new(clock.Now().Add(-6 * time.Second)),
|
||||
statusResponders: []lifecyclev1alpha1.ResponderStatus{
|
||||
{Name: targetResponderA, StartTime: new(metav1.Now())},
|
||||
{Name: targetResponderB},
|
||||
},
|
||||
targetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateActive),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
expectedTargetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateInterrupted),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wait_for_completionTime_after_the_target_is_terminal",
|
||||
isTerminal: true,
|
||||
terminalTime: new(clock.Now().Add(-4 * time.Second)),
|
||||
statusResponders: []lifecyclev1alpha1.ResponderStatus{
|
||||
{Name: targetResponderA, StartTime: new(metav1.Now())},
|
||||
{Name: targetResponderB},
|
||||
},
|
||||
targetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateActive),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
expectedTargetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateActive),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
expectResync: new(1 * time.Second),
|
||||
},
|
||||
{
|
||||
name: "cancel_active_responder",
|
||||
isCanceled: true,
|
||||
statusResponders: []lifecyclev1alpha1.ResponderStatus{
|
||||
{Name: targetResponderA, StartTime: new(metav1.Now())},
|
||||
{Name: targetResponderB},
|
||||
},
|
||||
targetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateActive),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
expectedTargetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateCanceled),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "cancel_non_active_responder",
|
||||
isCanceled: true,
|
||||
statusResponders: []lifecyclev1alpha1.ResponderStatus{
|
||||
{Name: targetResponderA, StartTime: new(metav1.Now())},
|
||||
{Name: targetResponderB},
|
||||
},
|
||||
targetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateInactive),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
expectedTargetResponders: []lifecyclev1alpha1.TargetResponder{
|
||||
targetResponder(targetResponderA, lifecyclev1alpha1.ResponderStateInactive),
|
||||
targetResponder(targetResponderB, lifecyclev1alpha1.ResponderStateInactive),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
eviction := mkValidEviction("foo", "foo", "foo-1")
|
||||
eviction.Status.Responders = tc.statusResponders
|
||||
// also tests computeResponderStateAndNextResync
|
||||
isProgressionDone, resyncAfter := computeResponderProgression(clock.Now(), eviction, tc.targetResponders, tc.terminalTime, tc.isGone, tc.isTerminal, tc.isCanceled)
|
||||
if isProgressionDone != tc.expectProgressionDone {
|
||||
t.Errorf("got isProgressionDone %v, want %v", isProgressionDone, tc.expectProgressionDone)
|
||||
}
|
||||
if !ptr.Equal(resyncAfter, tc.expectResync) {
|
||||
t.Errorf("got defer completion %v, expected %v", ptr.Deref(resyncAfter, -1), ptr.Deref(tc.expectResync, -1))
|
||||
}
|
||||
if diff := cmp.Diff(tc.expectedTargetResponders, tc.targetResponders); len(diff) > 0 {
|
||||
t.Fatalf("unexpected targetResponders (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortEvictionRequestsByRelevance(t *testing.T) {
|
||||
now := time.Now()
|
||||
clock := testingclock.NewFakeClock(now)
|
||||
metaNow := new(metav1.Time{Time: clock.Now()})
|
||||
testCases := []struct {
|
||||
name string
|
||||
requests []*lifecyclev1alpha1.EvictionRequest
|
||||
expectedRequests []*lifecyclev1alpha1.EvictionRequest
|
||||
}{
|
||||
{
|
||||
name: "no_requesters",
|
||||
},
|
||||
{
|
||||
name: "sort_requesters",
|
||||
requests: []*lifecyclev1alpha1.EvictionRequest{
|
||||
mkValidEvictionRequest("requester-2", "pod-2", setERRequester("foo.example.com/2"),
|
||||
setERCreationTimestamp(metav1.Time{})),
|
||||
mkValidEvictionRequest("requester-4", "pod-4", setERRequester("foo.example.com/4"),
|
||||
setERCreationTimestamp(metav1.Time{}),
|
||||
setERIntent(lifecyclev1alpha1.EvictionRequestIntentWithdrawn)),
|
||||
mkValidEvictionRequest("requester-1", "pod-1", setERRequester("foo.example.com/1"),
|
||||
setERCreationTimestamp(*metaNow)),
|
||||
mkValidEvictionRequest("requester-3", "pod-3", setERRequester("foo.example.com/3"),
|
||||
setERCreationTimestamp(*metaNow)),
|
||||
mkValidEvictionRequest("requester-5", "pod-5", setERRequester("foo.example.com/5"),
|
||||
setERCreationTimestamp(*metaNow),
|
||||
setERDeletionTimestamp(metaNow)),
|
||||
mkValidEvictionRequest("requester-7", "pod-7", setERRequester("foo.example.com/7"),
|
||||
setERCreationTimestamp(*metaNow)),
|
||||
mkValidEvictionRequest("requester-6", "pod-6", setERRequester("foo.example.com/6"),
|
||||
setERCreationTimestamp(*metaNow),
|
||||
setERIntent(lifecyclev1alpha1.EvictionRequestIntentWithdrawn)),
|
||||
mkValidEvictionRequest("requester-8", "pod-8", setERRequester("foo.example.com/8"),
|
||||
setERCreationTimestamp(*metaNow),
|
||||
setERDeletionTimestamp(metaNow)),
|
||||
mkValidEvictionRequest("requester-9", "pod-9", setERRequester("foo.example.com/9"),
|
||||
setERCreationTimestamp(*metaNow),
|
||||
setERDeletionTimestamp(metaNow),
|
||||
setERIntent(lifecyclev1alpha1.EvictionRequestIntentWithdrawn)),
|
||||
},
|
||||
expectedRequests: []*lifecyclev1alpha1.EvictionRequest{
|
||||
mkValidEvictionRequest("requester-1", "pod-1", setERRequester("foo.example.com/1"), // evictions always first
|
||||
setERCreationTimestamp(*metaNow)),
|
||||
mkValidEvictionRequest("requester-3", "pod-3", setERRequester("foo.example.com/3"),
|
||||
setERCreationTimestamp(*metaNow)),
|
||||
mkValidEvictionRequest("requester-7", "pod-7", setERRequester("foo.example.com/7"),
|
||||
setERCreationTimestamp(*metaNow)),
|
||||
mkValidEvictionRequest("requester-6", "pod-6", setERRequester("foo.example.com/6"), // non deleted
|
||||
setERCreationTimestamp(*metaNow),
|
||||
setERIntent(lifecyclev1alpha1.EvictionRequestIntentWithdrawn)),
|
||||
mkValidEvictionRequest("requester-5", "pod-5", setERRequester("foo.example.com/5"), // deletionTimestamp
|
||||
setERCreationTimestamp(*metaNow),
|
||||
setERDeletionTimestamp(metaNow)),
|
||||
mkValidEvictionRequest("requester-8", "pod-8", setERRequester("foo.example.com/8"),
|
||||
setERCreationTimestamp(*metaNow),
|
||||
setERDeletionTimestamp(metaNow)),
|
||||
mkValidEvictionRequest("requester-9", "pod-9", setERRequester("foo.example.com/9"),
|
||||
setERCreationTimestamp(*metaNow),
|
||||
setERDeletionTimestamp(metaNow),
|
||||
setERIntent(lifecyclev1alpha1.EvictionRequestIntentWithdrawn)),
|
||||
mkValidEvictionRequest("requester-2", "pod-2", setERRequester("foo.example.com/2"), // moved to the end, since it has no EvictionRequest assigned
|
||||
setERCreationTimestamp(metav1.Time{})),
|
||||
mkValidEvictionRequest("requester-4", "pod-4", setERRequester("foo.example.com/4"),
|
||||
setERCreationTimestamp(metav1.Time{}),
|
||||
setERIntent(lifecyclev1alpha1.EvictionRequestIntentWithdrawn)),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
sortEvictionRequestsByRelevance(tc.requests)
|
||||
if diff := cmp.Diff(tc.expectedRequests, tc.requests); len(diff) > 0 {
|
||||
t.Fatalf("unexpected EvictionRequests order (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func targetResponder(name string, state lifecyclev1alpha1.ResponderStateType) lifecyclev1alpha1.TargetResponder {
|
||||
var priority int32
|
||||
switch name {
|
||||
case targetResponderA:
|
||||
priority = 15000
|
||||
case targetResponderB:
|
||||
priority = 1000
|
||||
case lifecyclev1alpha1.EvictionResponderImperativeEviction:
|
||||
priority = 100
|
||||
default:
|
||||
panic("unknown responder")
|
||||
}
|
||||
return lifecyclev1alpha1.TargetResponder{
|
||||
Name: name,
|
||||
Priority: &priority,
|
||||
State: state,
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,605 @@
|
||||
/*
|
||||
Copyright 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 evictionrequest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
lifecyclev1alpha1 "k8s.io/api/lifecycle/v1alpha1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
lifecycleapply "k8s.io/client-go/applyconfigurations/lifecycle/v1alpha1"
|
||||
metav1ac "k8s.io/client-go/applyconfigurations/meta/v1"
|
||||
testingclock "k8s.io/utils/clock/testing"
|
||||
)
|
||||
|
||||
// TesValidate tests early validation
|
||||
func TestValidate(t *testing.T) {
|
||||
now := time.Now()
|
||||
clock := testingclock.NewFakePassiveClock(now)
|
||||
oldClock := testingclock.NewFakePassiveClock(now.Add(-time.Minute))
|
||||
tests := []struct {
|
||||
name string
|
||||
hasEvictions bool
|
||||
target lifecyclev1alpha1.EvictionTarget
|
||||
pod *v1.Pod
|
||||
conditions []metav1.Condition
|
||||
isDuplicate bool
|
||||
testEviction bool
|
||||
testEvictionRequest bool
|
||||
evictionRequestCreationTime time.Time
|
||||
expected []metav1ac.ConditionApplyConfiguration
|
||||
expectChanged bool
|
||||
expectAddAfter *time.Duration
|
||||
}{
|
||||
{
|
||||
name: "valid_pod",
|
||||
target: mkValidPodTarget("my-pod", "uid-1"),
|
||||
pod: mkValidPod("my-pod", "uid-1"),
|
||||
expected: nil,
|
||||
testEvictionRequest: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: "pod_not_found_after_eviction",
|
||||
hasEvictions: true,
|
||||
target: mkValidPodTarget("my-pod", "uid-1"),
|
||||
expected: nil,
|
||||
testEvictionRequest: true,
|
||||
},
|
||||
{
|
||||
name: "UID_mismatch_after_eviction",
|
||||
hasEvictions: true,
|
||||
target: mkValidPodTarget("my-pod", "uid-1"),
|
||||
pod: mkValidPod("my-pod", "uid-2"),
|
||||
expected: nil,
|
||||
testEvictionRequest: true,
|
||||
},
|
||||
{
|
||||
name: "pod_not_found_after_eviction_request_creation(race_prevention)",
|
||||
target: mkValidPodTarget("my-pod", "uid-1"),
|
||||
pod: nil,
|
||||
evictionRequestCreationTime: clock.Now().Add(-time.Second),
|
||||
expectAddAfter: new(2 * time.Second),
|
||||
},
|
||||
{
|
||||
name: "pod_not_found",
|
||||
target: mkValidPodTarget("my-pod", "uid-1"),
|
||||
pod: nil,
|
||||
expected: []metav1ac.ConditionApplyConfiguration{
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionFailed,
|
||||
metav1.ConditionTrue, lifecyclev1alpha1.EvictionConditionReasonEvictionInvalid,
|
||||
"Target pod not found."),
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionTargetEvicted,
|
||||
metav1.ConditionFalse, lifecyclev1alpha1.EvictionConditionReasonEvictionFailed, ""),
|
||||
},
|
||||
evictionRequestCreationTime: clock.Now().Add(3 * -time.Second),
|
||||
expectChanged: true,
|
||||
testEvictionRequest: true,
|
||||
},
|
||||
{
|
||||
name: "UID_mismatch",
|
||||
target: mkValidPodTarget("my-pod", "uid-1"),
|
||||
pod: mkValidPod("my-pod", "uid-2"),
|
||||
expected: []metav1ac.ConditionApplyConfiguration{
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionFailed,
|
||||
metav1.ConditionTrue, lifecyclev1alpha1.EvictionConditionReasonEvictionInvalid,
|
||||
"Target pod UID mismatch: expected uid-1, got uid-2."),
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionTargetEvicted,
|
||||
metav1.ConditionFalse, lifecyclev1alpha1.EvictionConditionReasonEvictionFailed, ""),
|
||||
},
|
||||
expectChanged: true,
|
||||
testEvictionRequest: true,
|
||||
},
|
||||
{
|
||||
name: "pod_with_PodGroup",
|
||||
target: mkValidPodTarget("my-pod", "uid-1"),
|
||||
pod: func() *v1.Pod {
|
||||
pod := mkValidPod("my-pod", "uid-1")
|
||||
pod.Spec.SchedulingGroup = &v1.PodSchedulingGroup{PodGroupName: new("my-podgroup")}
|
||||
return pod
|
||||
}(),
|
||||
expected: []metav1ac.ConditionApplyConfiguration{
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionFailed,
|
||||
metav1.ConditionTrue, lifecyclev1alpha1.EvictionConditionReasonEvictionInvalid,
|
||||
"Target pod references a SchedulingGroup. Eviction is currently not supported."),
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionTargetEvicted,
|
||||
metav1.ConditionFalse, lifecyclev1alpha1.EvictionConditionReasonEvictionFailed, ""),
|
||||
},
|
||||
expectChanged: true,
|
||||
testEviction: true,
|
||||
testEvictionRequest: true,
|
||||
},
|
||||
{
|
||||
name: "duplicate_eviction",
|
||||
target: mkValidPodTarget("my-pod", "uid-1"),
|
||||
pod: mkValidPod("my-pod", "uid-1"),
|
||||
isDuplicate: true,
|
||||
expected: []metav1ac.ConditionApplyConfiguration{
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionFailed,
|
||||
metav1.ConditionTrue, lifecyclev1alpha1.EvictionConditionReasonEvictionInvalid,
|
||||
"Active Eviction already exists for the same target."),
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionTargetEvicted,
|
||||
metav1.ConditionFalse, lifecyclev1alpha1.EvictionConditionReasonEvictionFailed, ""),
|
||||
},
|
||||
expectChanged: true,
|
||||
testEviction: true,
|
||||
},
|
||||
{
|
||||
name: "empty_target",
|
||||
target: lifecyclev1alpha1.EvictionTarget{},
|
||||
pod: nil,
|
||||
expected: []metav1ac.ConditionApplyConfiguration{
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionFailed,
|
||||
metav1.ConditionTrue, lifecyclev1alpha1.EvictionConditionReasonEvictionInvalid,
|
||||
"Unsupported target type."),
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionTargetEvicted,
|
||||
metav1.ConditionFalse, lifecyclev1alpha1.EvictionConditionReasonEvictionFailed, ""),
|
||||
},
|
||||
expectChanged: true,
|
||||
testEviction: true,
|
||||
testEvictionRequest: true,
|
||||
},
|
||||
{
|
||||
name: "empty_target(condition_already_set)",
|
||||
target: lifecyclev1alpha1.EvictionTarget{},
|
||||
pod: nil,
|
||||
conditions: []metav1.Condition{
|
||||
{
|
||||
Type: string(lifecyclev1alpha1.EvictionConditionTargetEvicted),
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: string(lifecyclev1alpha1.EvictionConditionReasonEvictionFailed),
|
||||
Message: "",
|
||||
},
|
||||
{
|
||||
Type: string(lifecyclev1alpha1.EvictionConditionFailed),
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: string(lifecyclev1alpha1.EvictionConditionReasonEvictionInvalid),
|
||||
Message: "Unsupported target type.",
|
||||
},
|
||||
},
|
||||
expected: []metav1ac.ConditionApplyConfiguration{
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionFailed,
|
||||
metav1.ConditionTrue, lifecyclev1alpha1.EvictionConditionReasonEvictionInvalid,
|
||||
"Unsupported target type."),
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionTargetEvicted,
|
||||
metav1.ConditionFalse, lifecyclev1alpha1.EvictionConditionReasonEvictionFailed, ""),
|
||||
},
|
||||
testEviction: true,
|
||||
testEvictionRequest: true,
|
||||
},
|
||||
{
|
||||
name: "empty_target(updates_existing_condition)",
|
||||
target: lifecyclev1alpha1.EvictionTarget{},
|
||||
pod: nil,
|
||||
conditions: []metav1.Condition{
|
||||
{
|
||||
Type: string(lifecyclev1alpha1.EvictionConditionFailed),
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: string(lifecyclev1alpha1.EvictionConditionReasonEvictionInvalid),
|
||||
Message: "Unsupported target type.",
|
||||
LastTransitionTime: metav1.Time{Time: oldClock.Now()},
|
||||
},
|
||||
},
|
||||
expected: []metav1ac.ConditionApplyConfiguration{
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionFailed,
|
||||
metav1.ConditionTrue, lifecyclev1alpha1.EvictionConditionReasonEvictionInvalid,
|
||||
"Unsupported target type."),
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionTargetEvicted,
|
||||
metav1.ConditionFalse, lifecyclev1alpha1.EvictionConditionReasonEvictionFailed, ""),
|
||||
},
|
||||
expectChanged: true,
|
||||
testEviction: true,
|
||||
testEvictionRequest: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// always test EvictionRequest, Eviction has only a subset of validations
|
||||
if tc.testEviction {
|
||||
eviction := &lifecyclev1alpha1.Eviction{
|
||||
Status: lifecyclev1alpha1.EvictionStatus{
|
||||
Conditions: tc.conditions,
|
||||
},
|
||||
}
|
||||
failed, evicted, changed := validateEviction(clock.Now(), eviction, newTargetInfoForEviction(tc.target, tc.pod), tc.isDuplicate)
|
||||
var got []metav1ac.ConditionApplyConfiguration
|
||||
if failed != nil {
|
||||
got = append(got, *failed)
|
||||
}
|
||||
if evicted != nil {
|
||||
got = append(got, *evicted)
|
||||
}
|
||||
if diff := cmp.Diff(tc.expected, got); diff != "" {
|
||||
t.Errorf("unexpected conditions update for validateEviction (-want +got):\n%s", diff)
|
||||
}
|
||||
if tc.expectChanged != changed {
|
||||
t.Errorf("got changed %v, want %v", changed, tc.expectChanged)
|
||||
}
|
||||
}
|
||||
if tc.testEvictionRequest {
|
||||
evictionRequest := &lifecyclev1alpha1.EvictionRequest{
|
||||
CreationTimestamp: metav1.Time{Time: tc.evictionRequestCreationTime},
|
||||
Status: lifecyclev1alpha1.EvictionRequestStatus{
|
||||
Conditions: tc.conditions,
|
||||
},
|
||||
}
|
||||
failed, evicted, changed, addAfter := validateEvictionRequest(clock.Now(), evictionRequest,
|
||||
newTargetInfoForEviction(tc.target, tc.pod), tc.hasEvictions)
|
||||
var got []metav1ac.ConditionApplyConfiguration
|
||||
if failed != nil {
|
||||
got = append(got, *failed)
|
||||
}
|
||||
if evicted != nil {
|
||||
got = append(got, *evicted)
|
||||
}
|
||||
if diff := cmp.Diff(tc.expected, got); diff != "" {
|
||||
t.Errorf("unexpected conditions update for validateEvictionRequest (-want +got):\n%s", diff)
|
||||
}
|
||||
if tc.expectChanged != changed {
|
||||
t.Errorf("got changed %v, want %v", changed, tc.expectChanged)
|
||||
}
|
||||
if diff := cmp.Diff(tc.expectAddAfter, addAfter); diff != "" {
|
||||
t.Errorf("unexpected addAfter for validateEvictionRequest (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeEvictionConditions(t *testing.T) {
|
||||
now := time.Now()
|
||||
clock := testingclock.NewFakePassiveClock(now)
|
||||
oldClock := testingclock.NewFakePassiveClock(now.Add(-time.Minute))
|
||||
tests := []struct {
|
||||
name string
|
||||
conditions []metav1.Condition
|
||||
isWaitingForResponderUpdate bool
|
||||
isGone bool
|
||||
isTerminal bool
|
||||
isCanceled bool
|
||||
isProgressionDone bool
|
||||
expected []metav1ac.ConditionApplyConfiguration
|
||||
expectChanged bool
|
||||
}{
|
||||
{
|
||||
name: "pod_deleted",
|
||||
isGone: true,
|
||||
expected: []metav1ac.ConditionApplyConfiguration{
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionFailed,
|
||||
metav1.ConditionFalse, lifecyclev1alpha1.EvictionConditionReasonSucceeded,
|
||||
""),
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionTargetEvicted,
|
||||
metav1.ConditionTrue, lifecyclev1alpha1.EvictionConditionReasonPodDeleted, "Target pod has been deleted."),
|
||||
},
|
||||
expectChanged: true,
|
||||
},
|
||||
{
|
||||
name: "pod_deleted(waiting)",
|
||||
isGone: true,
|
||||
isWaitingForResponderUpdate: true,
|
||||
expected: []metav1ac.ConditionApplyConfiguration{
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionFailed,
|
||||
metav1.ConditionFalse, lifecyclev1alpha1.EvictionConditionReasonAwaitingEviction,
|
||||
""),
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionTargetEvicted,
|
||||
metav1.ConditionFalse, lifecyclev1alpha1.EvictionConditionReasonAwaitingEviction, "Target is evicted, but waiting for the last active responder update."),
|
||||
},
|
||||
expectChanged: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: "pod_terminal",
|
||||
isTerminal: true,
|
||||
expected: []metav1ac.ConditionApplyConfiguration{
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionFailed,
|
||||
metav1.ConditionFalse, lifecyclev1alpha1.EvictionConditionReasonSucceeded, ""),
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionTargetEvicted,
|
||||
metav1.ConditionTrue, lifecyclev1alpha1.EvictionConditionReasonPodTerminal, "Pod has reached terminal state."),
|
||||
},
|
||||
expectChanged: true,
|
||||
},
|
||||
{
|
||||
name: "pod_terminal(waiting)",
|
||||
isTerminal: true,
|
||||
isWaitingForResponderUpdate: true,
|
||||
expected: []metav1ac.ConditionApplyConfiguration{
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionFailed,
|
||||
metav1.ConditionFalse, lifecyclev1alpha1.EvictionConditionReasonAwaitingEviction, ""),
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionTargetEvicted,
|
||||
metav1.ConditionFalse, lifecyclev1alpha1.EvictionConditionReasonAwaitingEviction, "Target is evicted, but waiting for the last active responder update."),
|
||||
},
|
||||
expectChanged: true,
|
||||
},
|
||||
{
|
||||
name: "is_canceled",
|
||||
isCanceled: true,
|
||||
expected: []metav1ac.ConditionApplyConfiguration{
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionFailed,
|
||||
metav1.ConditionTrue, lifecyclev1alpha1.EvictionConditionReasonCanceledDueToNoRequesters, "No active requesters with eviction intent."),
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionTargetEvicted,
|
||||
metav1.ConditionFalse, lifecyclev1alpha1.EvictionConditionReasonEvictionFailed, ""),
|
||||
},
|
||||
expectChanged: true,
|
||||
},
|
||||
{
|
||||
name: "no_progress",
|
||||
isProgressionDone: true,
|
||||
expected: []metav1ac.ConditionApplyConfiguration{
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionFailed,
|
||||
metav1.ConditionTrue, lifecyclev1alpha1.EvictionConditionReasonNoFurtherResponder, "All responders have completed without evicting the target."),
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionTargetEvicted,
|
||||
metav1.ConditionFalse, lifecyclev1alpha1.EvictionConditionReasonEvictionFailed, ""),
|
||||
},
|
||||
expectChanged: true,
|
||||
},
|
||||
{
|
||||
name: "pending",
|
||||
expected: []metav1ac.ConditionApplyConfiguration{
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionFailed,
|
||||
metav1.ConditionFalse, lifecyclev1alpha1.EvictionConditionReasonAwaitingEviction, ""),
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionTargetEvicted,
|
||||
metav1.ConditionFalse, lifecyclev1alpha1.EvictionConditionReasonAwaitingEviction, ""),
|
||||
},
|
||||
expectChanged: true,
|
||||
},
|
||||
{
|
||||
name: "no_progress(condition_already_set)",
|
||||
isProgressionDone: true,
|
||||
conditions: []metav1.Condition{
|
||||
{
|
||||
Type: string(lifecyclev1alpha1.EvictionConditionTargetEvicted),
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: string(lifecyclev1alpha1.EvictionConditionReasonEvictionFailed),
|
||||
Message: "",
|
||||
},
|
||||
{
|
||||
Type: string(lifecyclev1alpha1.EvictionConditionFailed),
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: string(lifecyclev1alpha1.EvictionConditionReasonNoFurtherResponder),
|
||||
Message: "All responders have completed without evicting the target.",
|
||||
},
|
||||
},
|
||||
expected: []metav1ac.ConditionApplyConfiguration{
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionFailed,
|
||||
metav1.ConditionTrue, lifecyclev1alpha1.EvictionConditionReasonNoFurtherResponder, "All responders have completed without evicting the target."),
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionTargetEvicted,
|
||||
metav1.ConditionFalse, lifecyclev1alpha1.EvictionConditionReasonEvictionFailed, ""),
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "no_progress(updates_existing_condition)",
|
||||
isProgressionDone: true,
|
||||
conditions: []metav1.Condition{
|
||||
{
|
||||
Type: string(lifecyclev1alpha1.EvictionConditionFailed),
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: string(lifecyclev1alpha1.EvictionConditionReasonAwaitingEviction),
|
||||
Message: "",
|
||||
LastTransitionTime: metav1.Time{Time: oldClock.Now()},
|
||||
},
|
||||
},
|
||||
expected: []metav1ac.ConditionApplyConfiguration{
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionFailed,
|
||||
metav1.ConditionTrue, lifecyclev1alpha1.EvictionConditionReasonNoFurtherResponder, "All responders have completed without evicting the target."),
|
||||
*conditionApplyConf(clock.Now(), lifecyclev1alpha1.EvictionConditionTargetEvicted,
|
||||
metav1.ConditionFalse, lifecyclev1alpha1.EvictionConditionReasonEvictionFailed, ""),
|
||||
},
|
||||
expectChanged: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
eviction := &lifecyclev1alpha1.Eviction{
|
||||
Status: lifecyclev1alpha1.EvictionStatus{
|
||||
Conditions: tc.conditions,
|
||||
},
|
||||
}
|
||||
failed, evicted, changed := computeEvictionConditions(clock.Now(), eviction,
|
||||
tc.isWaitingForResponderUpdate, tc.isGone, tc.isTerminal, tc.isCanceled, tc.isProgressionDone)
|
||||
var got []metav1ac.ConditionApplyConfiguration
|
||||
if failed != nil {
|
||||
got = append(got, *failed)
|
||||
}
|
||||
if evicted != nil {
|
||||
got = append(got, *evicted)
|
||||
}
|
||||
if diff := cmp.Diff(tc.expected, got); diff != "" {
|
||||
t.Errorf("unexpected conditions update (-want +got):\n%s", diff)
|
||||
}
|
||||
if tc.expectChanged != changed {
|
||||
t.Errorf("got changed %v, want %v", changed, tc.expectChanged)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateRequestersForEvictionStatusApply(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
existingRequesters []lifecyclev1alpha1.Requester
|
||||
requests []*lifecyclev1alpha1.EvictionRequest
|
||||
limit int
|
||||
expectedApplyConfiguration *lifecycleapply.EvictionStatusApplyConfiguration
|
||||
expectChanged bool
|
||||
}{
|
||||
{
|
||||
name: "no_requesters",
|
||||
expectedApplyConfiguration: lifecycleapply.EvictionStatus(),
|
||||
},
|
||||
{
|
||||
name: "keep_existing_requesters_and_withdraw_if_the_old_ones_are_missing",
|
||||
existingRequesters: []lifecyclev1alpha1.Requester{
|
||||
{Name: "foo.example.com/2", Intent: lifecyclev1alpha1.RequesterIntentEviction},
|
||||
{Name: "foo.example.com/1", Intent: lifecyclev1alpha1.RequesterIntentWithdrawn},
|
||||
},
|
||||
limit: 100,
|
||||
expectedApplyConfiguration: lifecycleapply.EvictionStatus().WithRequesters(
|
||||
lifecycleapply.Requester().WithName("foo.example.com/1").WithIntent(lifecyclev1alpha1.RequesterIntentWithdrawn),
|
||||
lifecycleapply.Requester().WithName("foo.example.com/2").WithIntent(lifecyclev1alpha1.RequesterIntentWithdrawn),
|
||||
),
|
||||
expectChanged: true,
|
||||
},
|
||||
{
|
||||
name: "keep_existing_requesters_and_withdraw_if_the_old_is_deleted",
|
||||
existingRequesters: []lifecyclev1alpha1.Requester{
|
||||
{Name: "foo.example.com/2", Intent: lifecyclev1alpha1.RequesterIntentEviction},
|
||||
{Name: "foo.example.com/1", Intent: lifecyclev1alpha1.RequesterIntentWithdrawn},
|
||||
},
|
||||
requests: []*lifecyclev1alpha1.EvictionRequest{
|
||||
mkValidEvictionRequest("requester-2", "pod-2", setERRequester("foo.example.com/2"),
|
||||
setERDeletionTimestamp(new(metav1.Now()))),
|
||||
},
|
||||
limit: 100,
|
||||
expectedApplyConfiguration: lifecycleapply.EvictionStatus().WithRequesters(
|
||||
lifecycleapply.Requester().WithName("foo.example.com/2").WithIntent(lifecyclev1alpha1.RequesterIntentWithdrawn),
|
||||
lifecycleapply.Requester().WithName("foo.example.com/1").WithIntent(lifecyclev1alpha1.RequesterIntentWithdrawn),
|
||||
),
|
||||
expectChanged: true,
|
||||
},
|
||||
{
|
||||
name: "add_and_delete_requesters_and_change_intents",
|
||||
existingRequesters: []lifecyclev1alpha1.Requester{
|
||||
{Name: "foo.example.com/2", Intent: lifecyclev1alpha1.RequesterIntentEviction},
|
||||
{Name: "foo.example.com/3", Intent: lifecyclev1alpha1.RequesterIntentEviction},
|
||||
{Name: "foo.example.com/5", Intent: lifecyclev1alpha1.RequesterIntentEviction},
|
||||
{Name: "foo.example.com/1", Intent: lifecyclev1alpha1.RequesterIntentWithdrawn},
|
||||
{Name: "foo.example.com/4", Intent: lifecyclev1alpha1.RequesterIntentWithdrawn},
|
||||
{Name: "foo.example.com/6", Intent: lifecyclev1alpha1.RequesterIntentWithdrawn},
|
||||
},
|
||||
requests: []*lifecyclev1alpha1.EvictionRequest{
|
||||
mkValidEvictionRequest("requester-1", "pod-1", setERRequester("foo.example.com/1")),
|
||||
mkValidEvictionRequest("requester-3", "pod-3", setERRequester("foo.example.com/3")),
|
||||
mkValidEvictionRequest("requester-5", "pod-5", setERRequester("foo.example.com/5"),
|
||||
setERDeletionTimestamp(new(metav1.Now()))),
|
||||
mkValidEvictionRequest("requester-7", "pod-7", setERRequester("foo.example.com/7")),
|
||||
mkValidEvictionRequest("requester-6", "pod-6", setERRequester("foo.example.com/6"),
|
||||
setERIntent(lifecyclev1alpha1.EvictionRequestIntentWithdrawn)),
|
||||
mkValidEvictionRequest("requester-8", "pod-8", setERRequester("foo.example.com/8"),
|
||||
setERDeletionTimestamp(new(metav1.Now()))),
|
||||
},
|
||||
limit: 100,
|
||||
expectedApplyConfiguration: lifecycleapply.EvictionStatus().WithRequesters(
|
||||
lifecycleapply.Requester().WithName("foo.example.com/1").WithIntent(lifecyclev1alpha1.RequesterIntentEviction), // evictions always first
|
||||
lifecycleapply.Requester().WithName("foo.example.com/3").WithIntent(lifecyclev1alpha1.RequesterIntentEviction),
|
||||
lifecycleapply.Requester().WithName("foo.example.com/7").WithIntent(lifecyclev1alpha1.RequesterIntentEviction),
|
||||
lifecycleapply.Requester().WithName("foo.example.com/6").WithIntent(lifecyclev1alpha1.RequesterIntentWithdrawn), // non deleted
|
||||
lifecycleapply.Requester().WithName("foo.example.com/5").WithIntent(lifecyclev1alpha1.RequesterIntentWithdrawn), // deletionTimestamp
|
||||
lifecycleapply.Requester().WithName("foo.example.com/8").WithIntent(lifecyclev1alpha1.RequesterIntentWithdrawn),
|
||||
lifecycleapply.Requester().WithName("foo.example.com/2").WithIntent(lifecyclev1alpha1.RequesterIntentWithdrawn), // moved to the end, since it has no EvictionRequest assigned
|
||||
lifecycleapply.Requester().WithName("foo.example.com/4").WithIntent(lifecyclev1alpha1.RequesterIntentWithdrawn),
|
||||
),
|
||||
expectChanged: true,
|
||||
},
|
||||
{
|
||||
name: "add_and_delete_requesters_and_change_intents_with_limit",
|
||||
existingRequesters: []lifecyclev1alpha1.Requester{
|
||||
{Name: "foo.example.com/2", Intent: lifecyclev1alpha1.RequesterIntentEviction},
|
||||
{Name: "foo.example.com/3", Intent: lifecyclev1alpha1.RequesterIntentEviction},
|
||||
{Name: "foo.example.com/5", Intent: lifecyclev1alpha1.RequesterIntentEviction},
|
||||
{Name: "foo.example.com/1", Intent: lifecyclev1alpha1.RequesterIntentWithdrawn},
|
||||
{Name: "foo.example.com/4", Intent: lifecyclev1alpha1.RequesterIntentWithdrawn},
|
||||
{Name: "foo.example.com/6", Intent: lifecyclev1alpha1.RequesterIntentWithdrawn},
|
||||
},
|
||||
requests: []*lifecyclev1alpha1.EvictionRequest{
|
||||
mkValidEvictionRequest("requester-1", "pod-1", setERRequester("foo.example.com/1")),
|
||||
mkValidEvictionRequest("requester-3", "pod-3", setERRequester("foo.example.com/3")),
|
||||
mkValidEvictionRequest("requester-5", "pod-5", setERRequester("foo.example.com/5"),
|
||||
setERDeletionTimestamp(new(metav1.Now()))),
|
||||
mkValidEvictionRequest("requester-7", "pod-7", setERRequester("foo.example.com/7")),
|
||||
mkValidEvictionRequest("requester-6", "pod-6", setERRequester("foo.example.com/6"),
|
||||
setERIntent(lifecyclev1alpha1.EvictionRequestIntentWithdrawn)),
|
||||
mkValidEvictionRequest("requester-8", "pod-8", setERRequester("foo.example.com/8"),
|
||||
setERDeletionTimestamp(new(metav1.Now()))),
|
||||
},
|
||||
limit: 5,
|
||||
expectedApplyConfiguration: lifecycleapply.EvictionStatus().WithRequesters(
|
||||
lifecycleapply.Requester().WithName("foo.example.com/1").WithIntent(lifecyclev1alpha1.RequesterIntentEviction), // evictions always first
|
||||
lifecycleapply.Requester().WithName("foo.example.com/3").WithIntent(lifecyclev1alpha1.RequesterIntentEviction),
|
||||
lifecycleapply.Requester().WithName("foo.example.com/7").WithIntent(lifecyclev1alpha1.RequesterIntentEviction),
|
||||
lifecycleapply.Requester().WithName("foo.example.com/6").WithIntent(lifecyclev1alpha1.RequesterIntentWithdrawn), // non deleted
|
||||
lifecycleapply.Requester().WithName("foo.example.com/5").WithIntent(lifecyclev1alpha1.RequesterIntentWithdrawn), // deletionTimestamp
|
||||
),
|
||||
expectChanged: true,
|
||||
},
|
||||
{
|
||||
name: "process_duplicates_with_limit",
|
||||
existingRequesters: []lifecyclev1alpha1.Requester{
|
||||
{Name: "foo.example.com/1", Intent: lifecyclev1alpha1.RequesterIntentEviction},
|
||||
{Name: "foo.example.com/2", Intent: lifecyclev1alpha1.RequesterIntentEviction},
|
||||
{Name: "foo.example.com/3", Intent: lifecyclev1alpha1.RequesterIntentEviction},
|
||||
},
|
||||
requests: []*lifecyclev1alpha1.EvictionRequest{
|
||||
mkValidEvictionRequest("requester-1", "pod-1", setERRequester("foo.example.com/1")),
|
||||
},
|
||||
limit: 2,
|
||||
expectedApplyConfiguration: lifecycleapply.EvictionStatus().WithRequesters(
|
||||
lifecycleapply.Requester().WithName("foo.example.com/1").WithIntent(lifecyclev1alpha1.RequesterIntentEviction),
|
||||
lifecycleapply.Requester().WithName("foo.example.com/2").WithIntent(lifecyclev1alpha1.RequesterIntentWithdrawn),
|
||||
),
|
||||
expectChanged: true,
|
||||
},
|
||||
{
|
||||
name: "no_changes_in_requesters",
|
||||
existingRequesters: []lifecyclev1alpha1.Requester{
|
||||
{Name: "foo.example.com/2", Intent: lifecyclev1alpha1.RequesterIntentEviction},
|
||||
{Name: "foo.example.com/1", Intent: lifecyclev1alpha1.RequesterIntentWithdrawn},
|
||||
},
|
||||
requests: []*lifecyclev1alpha1.EvictionRequest{
|
||||
mkValidEvictionRequest("requester-2", "pod-2", setERRequester("foo.example.com/2")),
|
||||
},
|
||||
limit: 100,
|
||||
expectedApplyConfiguration: lifecycleapply.EvictionStatus().WithRequesters(
|
||||
lifecycleapply.Requester().WithName("foo.example.com/2").WithIntent(lifecyclev1alpha1.RequesterIntentEviction),
|
||||
lifecycleapply.Requester().WithName("foo.example.com/1").WithIntent(lifecyclev1alpha1.RequesterIntentWithdrawn),
|
||||
),
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
eviction := mkValidEviction("foo", "foo", "foo-1")
|
||||
eviction.Status.Requesters = tc.existingRequesters
|
||||
result := lifecycleapply.EvictionStatus()
|
||||
changed := updateRequestersForEvictionStatusApply(eviction, tc.requests, tc.limit, result)
|
||||
if diff := cmp.Diff(tc.expectedApplyConfiguration, result); len(diff) > 0 {
|
||||
t.Fatalf("unexpected ApplyConfiguration (-want +got):\n%s", diff)
|
||||
}
|
||||
if tc.expectChanged != changed {
|
||||
t.Errorf("got changed %v, want %v", changed, tc.expectChanged)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func conditionApplyConf(
|
||||
now time.Time,
|
||||
conditionType lifecyclev1alpha1.EvictionConditionType,
|
||||
status metav1.ConditionStatus,
|
||||
reason lifecyclev1alpha1.EvictionConditionReason,
|
||||
message string,
|
||||
) *metav1ac.ConditionApplyConfiguration {
|
||||
return metav1ac.Condition().
|
||||
WithType(string(conditionType)).
|
||||
WithStatus(status).
|
||||
WithReason(string(reason)).
|
||||
WithMessage(message).
|
||||
WithLastTransitionTime(metav1.Time{Time: now})
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
/*
|
||||
Copyright 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 evictionrequest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
lifecyclev1alpha1 "k8s.io/api/lifecycle/v1alpha1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
metav1ac "k8s.io/client-go/applyconfigurations/meta/v1"
|
||||
"k8s.io/kubernetes/pkg/apis/lifecycle"
|
||||
validation2 "k8s.io/kubernetes/pkg/apis/lifecycle/validation"
|
||||
)
|
||||
|
||||
var evictionNameRegex = regexp.MustCompile("^([a-z]+)-([0-9]+)-(.*)$")
|
||||
|
||||
func extractEvictionNameParts(name string) (string, int, string, bool) {
|
||||
if subMatches := evictionNameRegex.FindStringSubmatch(name); len(subMatches) > 3 {
|
||||
targetTypePart := subMatches[1]
|
||||
counterPart := subMatches[2]
|
||||
namePart := subMatches[3]
|
||||
counter, err := strconv.Atoi(counterPart)
|
||||
if err != nil {
|
||||
return targetTypePart, -1, namePart, false
|
||||
}
|
||||
return targetTypePart, counter, namePart, true
|
||||
}
|
||||
return "", -1, "", false
|
||||
}
|
||||
|
||||
// evictionNameForCounter returns the Eviction name for the target and counter,
|
||||
// truncated to a valid DNS1123 subdomain.
|
||||
func evictionNameForCounter(target targetInfo, counter int) string {
|
||||
name := fmt.Sprintf("%s-%d-%s", target.targetType().String(), counter, target.targetName())
|
||||
if len(name) > validation.DNS1123SubdomainMaxLength {
|
||||
name = strings.TrimRight(name[:validation.DNS1123SubdomainMaxLength], "-.")
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// newEvictionName returns the name in pod-1-foo format. If there are Evictions with a conflicting name,
|
||||
// the counter is increased and pod-2-foo, etc. name is returned.
|
||||
func newEvictionName(target targetInfo, allEvictions []*lifecyclev1alpha1.Eviction) string {
|
||||
nameCounter := 0
|
||||
|
||||
if len(target.targetName()) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, eviction := range allEvictions {
|
||||
if targetType, counter, _, ok := extractEvictionNameParts(eviction.Name); ok && targetType == target.targetType().String() {
|
||||
// Compare against the (possibly truncated) name we would generate for this
|
||||
// counter. Two different very long pod names can share a counter space.
|
||||
if eviction.Name == evictionNameForCounter(target, counter) && counter > nameCounter {
|
||||
nameCounter = counter
|
||||
}
|
||||
}
|
||||
}
|
||||
return evictionNameForCounter(target, nameCounter+1)
|
||||
}
|
||||
|
||||
func compareEvictionNames(nameA, nameB string) int {
|
||||
targetTypeA, counterA, namePartA, okA := extractEvictionNameParts(nameA)
|
||||
targetTypeB, counterB, namePartB, okB := extractEvictionNameParts(nameB)
|
||||
|
||||
if okA && !okB {
|
||||
return -1
|
||||
}
|
||||
if !okA && okB {
|
||||
return 1
|
||||
}
|
||||
if okA && okB {
|
||||
targetCmp := strings.Compare(targetTypeA, targetTypeB)
|
||||
if targetCmp != 0 {
|
||||
return targetCmp
|
||||
}
|
||||
namePartCmp := strings.Compare(namePartA, namePartB)
|
||||
if namePartCmp != 0 {
|
||||
return namePartCmp
|
||||
}
|
||||
if counterA != counterB {
|
||||
return counterA - counterB
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Compare(nameA, nameB)
|
||||
}
|
||||
|
||||
// findRelevantEviction finds the most relevant Eviction for the EvictionRequest
|
||||
// 1. an Eviction with TargetEvicted=True condition
|
||||
// 2. the oldest active Eviction
|
||||
// 3. an Eviction with Failed=True condition
|
||||
// 4. nil if no Eviction matches
|
||||
func findRelevantEviction(evictions []*lifecyclev1alpha1.Eviction) (*lifecyclev1alpha1.Eviction, bool) {
|
||||
var active, succeeded, failed *lifecyclev1alpha1.Eviction
|
||||
|
||||
for _, eviction := range evictions {
|
||||
switch {
|
||||
case hasEvictionSucceeded(eviction):
|
||||
// Succeeded eviction with the latest transition time.
|
||||
if succeeded == nil || compareConditionTransitionTime(succeeded, eviction, lifecyclev1alpha1.EvictionConditionTargetEvicted) < 0 {
|
||||
succeeded = eviction
|
||||
}
|
||||
case hasEvictionFailed(eviction):
|
||||
// Failed eviction with the latest transition time.
|
||||
if failed == nil || compareConditionTransitionTime(failed, eviction, lifecyclev1alpha1.EvictionConditionFailed) < 0 {
|
||||
failed = eviction
|
||||
}
|
||||
default:
|
||||
// Oldest active eviction.
|
||||
if active == nil {
|
||||
active = eviction
|
||||
} else {
|
||||
timeCmp := active.CreationTimestamp.Time.Compare(eviction.CreationTimestamp.Time)
|
||||
if timeCmp > 0 {
|
||||
active = eviction
|
||||
} else if timeCmp == 0 && compareEvictionNames(active.Name, eviction.Name) > 0 {
|
||||
active = eviction
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if succeeded != nil {
|
||||
return succeeded, false
|
||||
}
|
||||
if active != nil {
|
||||
return active, false
|
||||
}
|
||||
return failed, true
|
||||
}
|
||||
|
||||
func convertToEvictionRequestConditions(now time.Time, evictionRequest *lifecyclev1alpha1.EvictionRequest, eviction *lifecyclev1alpha1.Eviction, conditionType lifecyclev1alpha1.EvictionConditionType) (*metav1ac.ConditionApplyConfiguration, bool) {
|
||||
if eviction != nil {
|
||||
if condition := meta.FindStatusCondition(eviction.Status.Conditions, string(conditionType)); condition != nil {
|
||||
return setCondition(now, evictionRequest.Status.Conditions, conditionType,
|
||||
condition.Status, lifecyclev1alpha1.EvictionConditionReason(condition.Reason), condition.Message)
|
||||
}
|
||||
}
|
||||
return setCondition(now, evictionRequest.Status.Conditions, conditionType,
|
||||
metav1.ConditionFalse, lifecyclev1alpha1.EvictionConditionReasonAwaitingEviction, "waiting for an Eviction to report on progress")
|
||||
}
|
||||
|
||||
func compareConditionTransitionTime(a, b *lifecyclev1alpha1.Eviction, conditionType lifecyclev1alpha1.EvictionConditionType) int {
|
||||
aCond := meta.FindStatusCondition(a.Status.Conditions, string(conditionType))
|
||||
bCond := meta.FindStatusCondition(b.Status.Conditions, string(conditionType))
|
||||
if aCond == nil && bCond == nil {
|
||||
return 0
|
||||
}
|
||||
if aCond == nil && bCond != nil {
|
||||
return -1
|
||||
}
|
||||
if aCond != nil && bCond == nil {
|
||||
return 1
|
||||
}
|
||||
return aCond.LastTransitionTime.Time.Compare(bCond.LastTransitionTime.Time)
|
||||
}
|
||||
|
||||
// setCondition creates or updates a condition in the apply configuration,
|
||||
// preserving the LastTransitionTime when the status has not changed.
|
||||
// Inspired by k8s.io/apimachinery/pkg/api/meta.SetStatusCondition.
|
||||
func setCondition(
|
||||
now time.Time,
|
||||
existingConditions []metav1.Condition,
|
||||
conditionType lifecyclev1alpha1.EvictionConditionType,
|
||||
status metav1.ConditionStatus,
|
||||
reason lifecyclev1alpha1.EvictionConditionReason,
|
||||
message string,
|
||||
) (*metav1ac.ConditionApplyConfiguration, bool) {
|
||||
transitionTime := metav1.Time{Time: now}
|
||||
|
||||
existing := meta.FindStatusCondition(existingConditions, string(conditionType))
|
||||
if existing != nil && existing.Status == status && !existing.LastTransitionTime.IsZero() {
|
||||
// Status unchanged, so we preserve the original transition time
|
||||
transitionTime = existing.LastTransitionTime
|
||||
}
|
||||
hasChanged := existing == nil ||
|
||||
existing.Status != status ||
|
||||
existing.Type != string(conditionType) ||
|
||||
existing.Reason != string(reason) ||
|
||||
existing.Message != message
|
||||
|
||||
return metav1ac.Condition().
|
||||
WithType(string(conditionType)).
|
||||
WithStatus(status).
|
||||
WithReason(string(reason)).
|
||||
WithMessage(message).
|
||||
WithLastTransitionTime(transitionTime), hasChanged
|
||||
}
|
||||
|
||||
// hasCompleted returns true if the Eviction has reached
|
||||
// a terminal state (Canceled or Evicted condition is True).
|
||||
func hasEvictionRequestCompleted(evictionRequest *lifecyclev1alpha1.EvictionRequest) bool {
|
||||
if meta.IsStatusConditionTrue(evictionRequest.Status.Conditions, string(lifecyclev1alpha1.EvictionConditionTargetEvicted)) {
|
||||
return true
|
||||
}
|
||||
failedCondition := meta.FindStatusCondition(evictionRequest.Status.Conditions, string(lifecyclev1alpha1.EvictionConditionFailed))
|
||||
if failedCondition != nil && failedCondition.Status == metav1.ConditionTrue &&
|
||||
failedCondition.Reason == string(lifecyclev1alpha1.EvictionConditionReasonEvictionInvalid) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasEvictionIntent(targetEvictionRequests []*lifecyclev1alpha1.EvictionRequest) bool {
|
||||
for _, request := range targetEvictionRequests {
|
||||
// only non deleted requests are considered active
|
||||
if request.DeletionTimestamp == nil && request.Spec.Intent == lifecyclev1alpha1.EvictionRequestIntentEviction {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func evictionRequestAsOwnerReference(evictionRequest *lifecyclev1alpha1.EvictionRequest) *metav1ac.OwnerReferenceApplyConfiguration {
|
||||
gvk := lifecyclev1alpha1.SchemeGroupVersion.WithKind("EvictionRequest")
|
||||
return metav1ac.OwnerReference().
|
||||
WithKind(gvk.Kind).
|
||||
WithAPIVersion(gvk.GroupVersion().String()).
|
||||
WithName(evictionRequest.Name).
|
||||
WithUID(evictionRequest.UID)
|
||||
}
|
||||
|
||||
func computeEvictionLabels(eviction *lifecyclev1alpha1.Eviction, target targetInfo, targetEvictionRequests []*lifecyclev1alpha1.EvictionRequest) map[string]string {
|
||||
labels := map[string]string{}
|
||||
|
||||
// refresh requester and responder labels
|
||||
for _, request := range targetEvictionRequests {
|
||||
labels[request.Spec.Requester] = string(lifecyclev1alpha1.EvictionParticipantRoleRequester)
|
||||
}
|
||||
|
||||
if target.isGone() {
|
||||
// if the target is gone we have to preserve our managed responder labels
|
||||
for k, v := range eviction.Labels {
|
||||
switch v {
|
||||
case string(lifecyclev1alpha1.EvictionParticipantRoleRequesterResponder):
|
||||
if len(labels[k]) > 0 {
|
||||
labels[k] = v
|
||||
} else {
|
||||
// demote to a responder
|
||||
labels[k] = string(lifecyclev1alpha1.EvictionParticipantRoleResponder)
|
||||
}
|
||||
case string(lifecyclev1alpha1.EvictionParticipantRoleResponder):
|
||||
if len(labels[k]) > 0 {
|
||||
// promote a responder
|
||||
labels[k] = string(lifecyclev1alpha1.EvictionParticipantRoleRequesterResponder)
|
||||
} else {
|
||||
labels[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, responder := range target.evictionResponders(true) {
|
||||
if len(labels[responder.Name]) > 0 {
|
||||
labels[responder.Name] = string(lifecyclev1alpha1.EvictionParticipantRoleRequesterResponder)
|
||||
} else {
|
||||
labels[responder.Name] = string(lifecyclev1alpha1.EvictionParticipantRoleResponder)
|
||||
}
|
||||
}
|
||||
}
|
||||
return labels
|
||||
}
|
||||
|
||||
// evictionLabelsNeedSSAUpdate reports label changes that are interesting for us: additions, changes,
|
||||
// and removal of only a single label.
|
||||
func evictionLabelsNeedSSAUpdate(oldLabels, newLabels map[string]string) bool {
|
||||
for k, v := range newLabels {
|
||||
if oldLabels == nil || oldLabels[k] != v {
|
||||
// new label added or updated
|
||||
return true
|
||||
}
|
||||
}
|
||||
for k, v := range oldLabels {
|
||||
// Only requester labels can be removed for now, as responders are immutable.
|
||||
if v == string(lifecyclev1alpha1.EvictionParticipantRoleRequester) {
|
||||
if len(newLabels[k]) == 0 {
|
||||
// requester removed
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// shouldDeferCompletion returns how long to wait before setting the completion
|
||||
// condition, giving the active responder time to report its final status
|
||||
// before the eviction is finalized. Returns 0 when no deferral is needed.
|
||||
func shouldDeferCompletion(now time.Time, activeResponderStatus *lifecyclev1alpha1.ResponderStatus, terminalTime *time.Time) *time.Duration {
|
||||
if activeResponderStatus == nil || activeResponderStatus.CompletionTime != nil || terminalTime == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if remaining := GracefulCompletionDelay - now.Sub(*terminalTime); remaining > 0 {
|
||||
return new(remaining)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findTargetResponderIdx(targetResponders []lifecyclev1alpha1.TargetResponder, state lifecyclev1alpha1.ResponderStateType) int {
|
||||
for i, responder := range targetResponders {
|
||||
if responder.State == state {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// findResponderStatus finds the status for a given responder name.
|
||||
func findResponderStatus(statuses []lifecyclev1alpha1.ResponderStatus, name string) *lifecyclev1alpha1.ResponderStatus {
|
||||
for i := range statuses {
|
||||
if statuses[i].Name == name {
|
||||
return &statuses[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SortTargetResponders returns highest priority responders on a lower index.
|
||||
// If there are responders with the same priority, the responder whose domain name comes first in the
|
||||
// alphabetical higher domain order, will be picked. This means that the top domain labels are compared
|
||||
// alphabetically first, followed by the lower domain labels. The key is compared last.
|
||||
//
|
||||
// NOTE: this is a wrapper around validation.SortTargetResponders in order not to duplicate the sorting logic
|
||||
func sortTargetResponders(targetResponders []lifecyclev1alpha1.TargetResponder) []lifecyclev1alpha1.TargetResponder {
|
||||
if len(targetResponders) == 0 {
|
||||
return nil
|
||||
}
|
||||
input := make([]lifecycle.TargetResponder, 0, len(targetResponders))
|
||||
for _, responder := range targetResponders {
|
||||
input = append(input, lifecycle.TargetResponder{
|
||||
Name: responder.Name,
|
||||
Priority: responder.Priority,
|
||||
State: lifecycle.ResponderStateType(responder.State),
|
||||
})
|
||||
}
|
||||
validation2.SortTargetResponders(input)
|
||||
output := make([]lifecyclev1alpha1.TargetResponder, 0, len(targetResponders))
|
||||
for _, responder := range input {
|
||||
output = append(output, lifecyclev1alpha1.TargetResponder{
|
||||
Name: responder.Name,
|
||||
Priority: responder.Priority,
|
||||
State: lifecyclev1alpha1.ResponderStateType(responder.State),
|
||||
})
|
||||
}
|
||||
return output
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
Copyright 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 evictionrequest
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
lifecyclev1alpha1 "k8s.io/api/lifecycle/v1alpha1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
apimachinerytypes "k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
lifecycleapply "k8s.io/client-go/applyconfigurations/lifecycle/v1alpha1"
|
||||
podutil "k8s.io/kubernetes/pkg/api/v1/pod"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
var _ metav1.ObjectMetaAccessor = &targetInfo{}
|
||||
|
||||
type targetType int
|
||||
|
||||
const (
|
||||
noTarget targetType = iota
|
||||
podTarget
|
||||
)
|
||||
|
||||
func (e targetType) String() string {
|
||||
if e == podTarget {
|
||||
return "pod"
|
||||
}
|
||||
return "unknown"
|
||||
|
||||
}
|
||||
|
||||
// targetInfo abstracts over the eviction target so that callers can ask
|
||||
// semantic questions ("is the target valid?", "is it gone?") rather than
|
||||
// reaching into pod-specific fields.
|
||||
type targetInfo struct {
|
||||
target lifecyclev1alpha1.EvictionRequestTarget
|
||||
pod *v1.Pod
|
||||
}
|
||||
|
||||
// newTargetInfo creates a targetInfo from the resolved target.
|
||||
// The caller is responsible for looking up the target object beforehand;
|
||||
// a nil pod with a non-nil spec.Pod means the pod was not found.
|
||||
func newTargetInfoForEviction(spec lifecyclev1alpha1.EvictionTarget, pod *v1.Pod) targetInfo {
|
||||
if spec.Pod != nil {
|
||||
return targetInfo{target: lifecyclev1alpha1.EvictionRequestTarget{
|
||||
Pod: &lifecyclev1alpha1.EvictionRequestPodReference{
|
||||
Name: spec.Pod.Name,
|
||||
UID: spec.Pod.UID,
|
||||
},
|
||||
}, pod: pod}
|
||||
}
|
||||
return targetInfo{pod: pod}
|
||||
|
||||
}
|
||||
|
||||
// newTargetInfo creates a targetInfo from the resolved target.
|
||||
// The caller is responsible for looking up the target object beforehand;
|
||||
// a nil pod with a non-nil spec.Pod means the pod was not found.
|
||||
func newTargetInfoForEvictionRequest(spec lifecyclev1alpha1.EvictionRequestTarget, pod *v1.Pod) targetInfo {
|
||||
return targetInfo{target: spec, pod: pod}
|
||||
}
|
||||
|
||||
// targetType returns the type of the eviction target (e.g. "pod").
|
||||
func (t targetInfo) targetType() targetType {
|
||||
if t.target.Pod != nil {
|
||||
return podTarget
|
||||
}
|
||||
return noTarget
|
||||
}
|
||||
|
||||
// targetType returns the type of the eviction target (e.g. "pod").
|
||||
func (t targetInfo) targetName() string {
|
||||
if t.target.Pod != nil {
|
||||
return t.target.Pod.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// targetType returns the type of the eviction target (e.g. "pod").
|
||||
func (t targetInfo) targetUID() apimachinerytypes.UID {
|
||||
if t.target.Pod != nil {
|
||||
return t.target.Pod.UID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// targetFoundByName returns true if the target object has been found
|
||||
func (t targetInfo) targetFoundByName() bool {
|
||||
if t.target.Pod != nil {
|
||||
return t.pod != nil
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetObjectMeta returns the target's ObjectMeta, or nil if the target is unavailable.
|
||||
func (t targetInfo) GetObjectMeta() metav1.Object {
|
||||
if t.target.Pod != nil {
|
||||
if t.pod != nil {
|
||||
return t.pod
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isGone reports whether the original target no longer exists. This is true
|
||||
// when the target is not found or when the found object has a different UID
|
||||
// (i.e., the original was deleted and a new object with the same name was created).
|
||||
func (t targetInfo) isGone() bool {
|
||||
if t.target.Pod != nil {
|
||||
if t.pod == nil {
|
||||
return true
|
||||
}
|
||||
return t.pod.UID != t.target.Pod.UID
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// hasSchedulingGroup reports whether the target belongs to a PodGroup.
|
||||
func (t targetInfo) hasSchedulingGroup() bool {
|
||||
if t.isGone() {
|
||||
return false
|
||||
}
|
||||
if t.pod != nil {
|
||||
return t.pod.Spec.SchedulingGroup != nil
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// hasCompleted reports whether the target has reached a terminal lifecycle state.
|
||||
func (t targetInfo) isTerminal() *bool {
|
||||
if t.isGone() {
|
||||
return nil
|
||||
}
|
||||
if t.pod != nil {
|
||||
return new(podutil.IsPodTerminal(t.pod))
|
||||
}
|
||||
return new(false)
|
||||
}
|
||||
|
||||
// terminalTime it reports the latest time this target has became terminal.
|
||||
func (t targetInfo) terminalTime() *time.Time {
|
||||
if t.isGone() {
|
||||
return nil
|
||||
}
|
||||
var terminalTime *time.Time
|
||||
if t.pod != nil && ptr.Deref(t.isTerminal(), false) {
|
||||
terminalTime = getPodFinishTime(t.pod)
|
||||
}
|
||||
objectMeta := t.GetObjectMeta()
|
||||
if objectMeta == nil || objectMeta.GetDeletionTimestamp() == nil {
|
||||
return terminalTime
|
||||
}
|
||||
if terminalTime == nil || objectMeta.GetDeletionTimestamp().Time.After(*terminalTime) {
|
||||
return new(objectMeta.GetDeletionTimestamp().Time)
|
||||
}
|
||||
return terminalTime
|
||||
}
|
||||
|
||||
// evictionResponders returns the responders declared on the target, or nil
|
||||
// if the target is unavailable.
|
||||
func (t targetInfo) evictionResponders(includeDefault bool) []v1.EvictionResponder {
|
||||
if t.isGone() {
|
||||
return nil
|
||||
}
|
||||
if t.pod != nil {
|
||||
responders := append([]v1.EvictionResponder(nil), t.pod.Spec.EvictionResponders...)
|
||||
if includeDefault {
|
||||
// Default imperative-eviction responder triggers imperative pod /eviction endpoint
|
||||
responders = append(responders, v1.EvictionResponder{Name: lifecyclev1alpha1.EvictionResponderImperativeEviction, Priority: new(int32(100))})
|
||||
}
|
||||
return responders
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t targetInfo) toEvictionTargetApply() *lifecycleapply.EvictionTargetApplyConfiguration {
|
||||
if t.target.Pod != nil {
|
||||
return lifecycleapply.EvictionTarget().WithPod(
|
||||
lifecycleapply.EvictionPodReference().
|
||||
WithName(t.target.Pod.Name).
|
||||
WithUID(t.target.Pod.UID),
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getPodFinishTime returns the pod finish time using
|
||||
// Similar tests in k8s.io/pkg/controller/job.getFinishedTime.
|
||||
func getPodFinishTime(p *v1.Pod) *time.Time {
|
||||
finishTime := latestFinishTimeFromContainers(nil, p.Status.ContainerStatuses, nil)
|
||||
// We need to check InitContainerStatuses here also,
|
||||
// because with the sidecar (restartable init) containers,
|
||||
// sidecar containers will always finish later than regular containers.
|
||||
names := sets.New[string]()
|
||||
for _, c := range p.Spec.InitContainers {
|
||||
if c.RestartPolicy != nil && *c.RestartPolicy == v1.ContainerRestartPolicyAlways {
|
||||
names.Insert(c.Name)
|
||||
}
|
||||
}
|
||||
finishTime = latestFinishTimeFromContainers(finishTime, p.Status.InitContainerStatuses, func(status v1.ContainerStatus) bool {
|
||||
return names.Has(status.Name)
|
||||
})
|
||||
|
||||
return finishTime
|
||||
}
|
||||
|
||||
func latestFinishTimeFromContainers(prevFinishTime *time.Time, cs []v1.ContainerStatus, check func(status v1.ContainerStatus) bool) *time.Time {
|
||||
var finishTime = prevFinishTime
|
||||
for _, containerState := range cs {
|
||||
if check != nil && !check(containerState) {
|
||||
continue
|
||||
}
|
||||
if containerState.State.Terminated == nil ||
|
||||
containerState.State.Terminated.FinishedAt.Time.IsZero() {
|
||||
return nil
|
||||
}
|
||||
if finishTime == nil || finishTime.Before(containerState.State.Terminated.FinishedAt.Time) {
|
||||
finishTime = new(containerState.State.Terminated.FinishedAt.Time)
|
||||
}
|
||||
}
|
||||
return finishTime
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
/*
|
||||
Copyright 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 evictionrequest
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
lifecyclev1alpha1 "k8s.io/api/lifecycle/v1alpha1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
lifecycleapply "k8s.io/client-go/applyconfigurations/lifecycle/v1alpha1"
|
||||
testing2 "k8s.io/utils/clock/testing"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func TestTargetInfo(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
target targetInfo
|
||||
expectedName string
|
||||
expectedTargetUID string
|
||||
expectedTargetType targetType
|
||||
expectedPodUID string
|
||||
expectedIsGone bool
|
||||
}{
|
||||
{
|
||||
name: "pod_found",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"), mkValidPod("my-pod", "uid-1")),
|
||||
expectedName: "my-pod",
|
||||
expectedTargetUID: "uid-1",
|
||||
expectedTargetType: podTarget,
|
||||
expectedPodUID: "uid-1",
|
||||
},
|
||||
{
|
||||
name: "pod_found_with_different_UID",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"), mkValidPod("my-pod", "uid-2")),
|
||||
expectedName: "my-pod",
|
||||
expectedTargetUID: "uid-1",
|
||||
expectedTargetType: podTarget,
|
||||
expectedPodUID: "uid-2",
|
||||
expectedIsGone: true,
|
||||
},
|
||||
{
|
||||
name: "pod_not_found",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"), nil),
|
||||
expectedName: "my-pod",
|
||||
expectedTargetUID: "uid-1",
|
||||
expectedTargetType: podTarget,
|
||||
expectedPodUID: "",
|
||||
expectedIsGone: true,
|
||||
},
|
||||
{
|
||||
name: "empty_target",
|
||||
target: newTargetInfoForEviction(lifecyclev1alpha1.EvictionTarget{}, nil),
|
||||
expectedName: "",
|
||||
expectedTargetUID: "",
|
||||
expectedTargetType: noTarget,
|
||||
expectedPodUID: "",
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := tc.target.targetType(); tc.expectedTargetType != got {
|
||||
t.Errorf("got targetType %v, expected %v", got, tc.expectedTargetType)
|
||||
}
|
||||
if got := tc.target.targetName(); tc.expectedName != got {
|
||||
t.Errorf("got targetName %v, expected %v", got, tc.expectedName)
|
||||
}
|
||||
if got := tc.target.targetUID(); tc.expectedTargetUID != string(got) {
|
||||
t.Errorf("got targetUID %v, expected %v", got, tc.expectedTargetUID)
|
||||
}
|
||||
isFound := len(tc.expectedPodUID) > 0 && len(tc.expectedTargetUID) > 0
|
||||
if got := tc.target.targetFoundByName(); isFound != got {
|
||||
t.Errorf("got targetFoundByName %v, expected %v", got, isFound)
|
||||
}
|
||||
expectedMeta := len(tc.expectedPodUID) > 0
|
||||
if got := tc.target.GetObjectMeta(); expectedMeta != (got != nil) ||
|
||||
(got != nil && string(got.GetUID()) != tc.expectedPodUID) {
|
||||
t.Errorf("got ObjectMeta %v, expected %v", got, expectedMeta)
|
||||
}
|
||||
if got := tc.target.isGone(); tc.expectedIsGone != got {
|
||||
t.Errorf("got isGone %v, expected %v", got, tc.expectedIsGone)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPartOfPodGroup(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
target targetInfo
|
||||
expectedHasSchedulingGroup bool
|
||||
}{
|
||||
{
|
||||
name: "pod_without_PodGroup",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"), mkValidPod("my-pod", "uid-1")),
|
||||
expectedHasSchedulingGroup: false,
|
||||
},
|
||||
{
|
||||
name: "pod_with_PodGroup",
|
||||
target: func() targetInfo {
|
||||
pod := mkValidPod("my-pod", "uid-1")
|
||||
pod.Spec.SchedulingGroup = &v1.PodSchedulingGroup{PodGroupName: new("my-podgroup")}
|
||||
return newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"), pod)
|
||||
}(),
|
||||
expectedHasSchedulingGroup: true,
|
||||
},
|
||||
{
|
||||
name: "pod_gone_but_new_with_PodGroup",
|
||||
target: func() targetInfo {
|
||||
pod := mkValidPod("my-pod", "uid-2")
|
||||
pod.Spec.SchedulingGroup = &v1.PodSchedulingGroup{PodGroupName: new("my-podgroup")}
|
||||
return newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"), pod)
|
||||
}(),
|
||||
expectedHasSchedulingGroup: false,
|
||||
},
|
||||
{
|
||||
name: "pod_not_found",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"), nil),
|
||||
expectedHasSchedulingGroup: false,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := tc.target.hasSchedulingGroup(); got != tc.expectedHasSchedulingGroup {
|
||||
t.Errorf("got hasSchedulingGroup %v, want %v", got, tc.expectedHasSchedulingGroup)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsTerminal(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
target targetInfo
|
||||
expectedIsTerminal *bool
|
||||
}{
|
||||
{
|
||||
name: "running_pod",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"), mkValidPod("my-pod", "uid-1")),
|
||||
expectedIsTerminal: new(false),
|
||||
},
|
||||
{
|
||||
name: "succeeded_pod",
|
||||
target: func() targetInfo {
|
||||
pod := mkValidPod("my-pod", "uid-1")
|
||||
pod.Status.Phase = v1.PodSucceeded
|
||||
return newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"), pod)
|
||||
}(),
|
||||
expectedIsTerminal: new(true),
|
||||
},
|
||||
{
|
||||
name: "failed_pod",
|
||||
target: func() targetInfo {
|
||||
pod := mkValidPod("my-pod", "uid-1")
|
||||
pod.Status.Phase = v1.PodFailed
|
||||
return newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"), pod)
|
||||
}(),
|
||||
expectedIsTerminal: new(true),
|
||||
},
|
||||
{
|
||||
name: "pod_not_found",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"), nil),
|
||||
expectedIsTerminal: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := tc.target.isTerminal(); !ptr.Equal(got, tc.expectedIsTerminal) {
|
||||
t.Errorf("got isTerminal %v, want %v", got, tc.expectedIsTerminal)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTerminalTime(t *testing.T) {
|
||||
clock := testing2.NewFakePassiveClock(time.Now())
|
||||
testCases := []struct {
|
||||
name string
|
||||
target targetInfo
|
||||
expectedTerminalTime *time.Time
|
||||
}{
|
||||
{
|
||||
name: "no_terminal_time_present",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"), mkValidPod("my-pod", "uid-1",
|
||||
setPodDeletionPhase(v1.PodSucceeded))),
|
||||
expectedTerminalTime: nil,
|
||||
},
|
||||
{
|
||||
name: "container_termination_time_present",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"), mkValidPod("my-pod", "uid-1",
|
||||
setPodDeletionPhase(v1.PodSucceeded), setPodContainerStatuses(v1.ContainerStatus{
|
||||
State: v1.ContainerState{
|
||||
Terminated: &v1.ContainerStateTerminated{FinishedAt: metav1.Time{Time: clock.Now()}},
|
||||
},
|
||||
}))),
|
||||
expectedTerminalTime: new(clock.Now()),
|
||||
},
|
||||
{
|
||||
name: "deletion_timestamp_present",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"), mkValidPod("my-pod", "uid-1",
|
||||
setPodDeletionPhase(v1.PodFailed), setPodDeletionTimestamp(&metav1.Time{Time: clock.Now()}))),
|
||||
expectedTerminalTime: new(clock.Now()),
|
||||
},
|
||||
{
|
||||
name: "deletion_timestamp_older_than_container_termination_time",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"), mkValidPod("my-pod", "uid-1",
|
||||
setPodDeletionPhase(v1.PodSucceeded),
|
||||
setPodDeletionTimestamp(&metav1.Time{Time: clock.Now().Add(-time.Second)}),
|
||||
setPodContainerStatuses(v1.ContainerStatus{
|
||||
State: v1.ContainerState{
|
||||
Terminated: &v1.ContainerStateTerminated{FinishedAt: metav1.Time{Time: clock.Now()}},
|
||||
},
|
||||
}))),
|
||||
expectedTerminalTime: new(clock.Now()),
|
||||
},
|
||||
{
|
||||
name: "deletion_timestamp_newer_than_container_termination_time",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"), mkValidPod("my-pod", "uid-1",
|
||||
setPodDeletionPhase(v1.PodFailed),
|
||||
setPodDeletionTimestamp(&metav1.Time{Time: clock.Now()}),
|
||||
setPodContainerStatuses(v1.ContainerStatus{
|
||||
State: v1.ContainerState{
|
||||
Terminated: &v1.ContainerStateTerminated{FinishedAt: metav1.Time{Time: clock.Now().Add(-time.Second)}},
|
||||
},
|
||||
}))),
|
||||
expectedTerminalTime: new(clock.Now()),
|
||||
},
|
||||
{
|
||||
name: "pod_gone_but_new_pod_container_termination_time_present",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"), mkValidPod("my-pod", "uid-2",
|
||||
setPodDeletionPhase(v1.PodSucceeded), setPodContainerStatuses(v1.ContainerStatus{
|
||||
State: v1.ContainerState{
|
||||
Terminated: &v1.ContainerStateTerminated{FinishedAt: metav1.Time{Time: clock.Now()}},
|
||||
},
|
||||
}))),
|
||||
expectedTerminalTime: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := tc.target.terminalTime(); !reflect.DeepEqual(got, tc.expectedTerminalTime) {
|
||||
t.Errorf("got terminalTime %v, want %v", got, tc.expectedTerminalTime)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvictionResponders(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
target targetInfo
|
||||
includeDefault bool
|
||||
want []v1.EvictionResponder
|
||||
}{
|
||||
{
|
||||
name: "pod_with_responders",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"),
|
||||
mkValidPod("my-pod", "uid-1", setEvictionResponders(
|
||||
v1.EvictionResponder{Name: "responder-a", Priority: new(int32(1000))},
|
||||
v1.EvictionResponder{Name: "responder-b", Priority: new(int32(15000))},
|
||||
)),
|
||||
),
|
||||
want: []v1.EvictionResponder{
|
||||
{Name: "responder-a", Priority: new(int32(1000))},
|
||||
{Name: "responder-b", Priority: new(int32(15000))},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pod_with_responders_plus_default_ones",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"),
|
||||
mkValidPod("my-pod", "uid-1", setEvictionResponders(
|
||||
v1.EvictionResponder{Name: "responder-a", Priority: new(int32(1000))},
|
||||
v1.EvictionResponder{Name: "responder-b", Priority: new(int32(15000))},
|
||||
)),
|
||||
),
|
||||
includeDefault: true,
|
||||
want: []v1.EvictionResponder{
|
||||
{Name: "responder-a", Priority: new(int32(1000))},
|
||||
{Name: "responder-b", Priority: new(int32(15000))},
|
||||
{Name: lifecyclev1alpha1.EvictionResponderImperativeEviction, Priority: new(int32(100))},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pod_without_responders",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"), mkValidPod("my-pod", "uid-1")),
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "pod_with_just_default_responders",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"), mkValidPod("my-pod", "uid-1")),
|
||||
includeDefault: true,
|
||||
want: []v1.EvictionResponder{
|
||||
{Name: lifecyclev1alpha1.EvictionResponderImperativeEviction, Priority: new(int32(100))},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pod_not_found",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"), nil),
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "pod_not_found_with_no_default_responders",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"), nil),
|
||||
includeDefault: true,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "no_target;_pod_not_found_with_no_default_responders",
|
||||
target: newTargetInfoForEviction(lifecyclev1alpha1.EvictionTarget{}, nil),
|
||||
includeDefault: true,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "pod_gone_but_new_pod_with_responders_plus_default_ones",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"),
|
||||
mkValidPod("my-pod", "uid-2", setEvictionResponders(
|
||||
v1.EvictionResponder{Name: "responder-a", Priority: new(int32(1000))},
|
||||
v1.EvictionResponder{Name: "responder-b", Priority: new(int32(15000))},
|
||||
)),
|
||||
),
|
||||
includeDefault: true,
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := tc.target.evictionResponders(tc.includeDefault)
|
||||
if diff := cmp.Diff(tc.want, got); len(diff) > 0 {
|
||||
t.Fatalf("unexpected evictionResponders (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToEvictionTargetApply(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
target targetInfo
|
||||
includeDefault bool
|
||||
expectedApplyConfiguration *lifecycleapply.EvictionTargetApplyConfiguration
|
||||
}{
|
||||
{
|
||||
name: "target_for_Eviction",
|
||||
target: newTargetInfoForEviction(mkValidPodTarget("my-pod", "uid-1"), nil),
|
||||
expectedApplyConfiguration: lifecycleapply.EvictionTarget().WithPod(
|
||||
lifecycleapply.EvictionPodReference().
|
||||
WithName("my-pod").
|
||||
WithUID("uid-1"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "target_for_EvictionRequest",
|
||||
target: newTargetInfoForEvictionRequest(mkValidEvictionRequestPodTarget("my-pod", "uid-2"), nil),
|
||||
expectedApplyConfiguration: lifecycleapply.EvictionTarget().WithPod(
|
||||
lifecycleapply.EvictionPodReference().
|
||||
WithName("my-pod").
|
||||
WithUID("uid-2"),
|
||||
),
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := tc.target.toEvictionTargetApply()
|
||||
if diff := cmp.Diff(tc.expectedApplyConfiguration, got); len(diff) > 0 {
|
||||
t.Fatalf("unexpected ApplyConfiguration (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Similar tests in k8s.io/pkg/controller/job.TestGetFinishedTime.
|
||||
func TestGetFinishedTime(t *testing.T) {
|
||||
defaultTestTime := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
|
||||
containerRestartPolicyAlways := v1.ContainerRestartPolicyAlways
|
||||
testCases := map[string]struct {
|
||||
pod v1.Pod
|
||||
wantFinishTime time.Time
|
||||
}{
|
||||
"Pod_with_multiple_containers_and_all_containers_terminated": {
|
||||
pod: v1.Pod{
|
||||
Status: v1.PodStatus{
|
||||
ContainerStatuses: []v1.ContainerStatus{
|
||||
{
|
||||
State: v1.ContainerState{
|
||||
Terminated: &v1.ContainerStateTerminated{FinishedAt: metav1.NewTime(defaultTestTime.Add(-1 * time.Second))},
|
||||
},
|
||||
},
|
||||
{
|
||||
State: v1.ContainerState{
|
||||
Terminated: &v1.ContainerStateTerminated{FinishedAt: metav1.NewTime(defaultTestTime)},
|
||||
},
|
||||
},
|
||||
{
|
||||
State: v1.ContainerState{
|
||||
Terminated: &v1.ContainerStateTerminated{FinishedAt: metav1.NewTime(defaultTestTime.Add(-2 * time.Second))},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantFinishTime: defaultTestTime,
|
||||
},
|
||||
// In this case, init container is stopped after the regular containers.
|
||||
// This is because with the sidecar (restartable init) containers,
|
||||
// sidecar containers will always finish later than regular containers.
|
||||
"Pod_with_sidecar_container_and_all_containers_terminated": {
|
||||
pod: v1.Pod{
|
||||
Spec: v1.PodSpec{
|
||||
InitContainers: []v1.Container{
|
||||
{
|
||||
Name: "sidecar",
|
||||
RestartPolicy: &containerRestartPolicyAlways,
|
||||
},
|
||||
},
|
||||
},
|
||||
Status: v1.PodStatus{
|
||||
ContainerStatuses: []v1.ContainerStatus{
|
||||
{
|
||||
State: v1.ContainerState{
|
||||
Terminated: &v1.ContainerStateTerminated{FinishedAt: metav1.NewTime(defaultTestTime.Add(-1 * time.Second))},
|
||||
},
|
||||
},
|
||||
},
|
||||
InitContainerStatuses: []v1.ContainerStatus{
|
||||
{
|
||||
Name: "sidecar",
|
||||
State: v1.ContainerState{
|
||||
Terminated: &v1.ContainerStateTerminated{FinishedAt: metav1.NewTime(defaultTestTime)},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantFinishTime: defaultTestTime,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
f := getPodFinishTime(&tc.pod)
|
||||
if !f.Equal(tc.wantFinishTime) {
|
||||
t.Errorf("Expected value of finishedTime %v; got %v", tc.wantFinishTime, f)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
Copyright 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 evictionrequest
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TerminalTimeCache struct {
|
||||
lock sync.RWMutex
|
||||
cache map[string]*time.Time
|
||||
}
|
||||
|
||||
func NewTerminalTimeCache() *TerminalTimeCache {
|
||||
return &TerminalTimeCache{
|
||||
cache: make(map[string]*time.Time),
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *TerminalTimeCache) recordTargetTerminalTime(targetHolderKey string, terminalTime time.Time) {
|
||||
tc.lock.Lock()
|
||||
defer tc.lock.Unlock()
|
||||
tc.cache[targetHolderKey] = &terminalTime
|
||||
}
|
||||
|
||||
func (tc *TerminalTimeCache) clearTargetTerminalTime(targetHolderKey string) {
|
||||
tc.lock.Lock()
|
||||
defer tc.lock.Unlock()
|
||||
delete(tc.cache, targetHolderKey)
|
||||
}
|
||||
|
||||
func (tc *TerminalTimeCache) getTargetTerminalTime(targetHolderKey string) *time.Time {
|
||||
tc.lock.RLock()
|
||||
defer tc.lock.RUnlock()
|
||||
return tc.cache[targetHolderKey]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
Copyright 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 evictionrequest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTerminalTimeCache(t *testing.T) {
|
||||
cache := NewTerminalTimeCache()
|
||||
|
||||
now := time.Now()
|
||||
cache.recordTargetTerminalTime("foo", now)
|
||||
cache.recordTargetTerminalTime("bar", now.Add(1*time.Minute))
|
||||
cache.recordTargetTerminalTime("baz", now.Add(2*time.Minute))
|
||||
|
||||
if got := cache.getTargetTerminalTime("foo"); !got.Equal(now) {
|
||||
t.Errorf("got terminal time %v, expected %v", got, now)
|
||||
}
|
||||
cache.clearTargetTerminalTime("foo")
|
||||
if got := cache.getTargetTerminalTime("foo"); got != nil {
|
||||
t.Errorf("got terminal time %v, expected nil", got)
|
||||
}
|
||||
if got := cache.getTargetTerminalTime("baz"); !got.Equal(now.Add(2 * time.Minute)) {
|
||||
t.Errorf("got terminal time %v, expected %v", got, now)
|
||||
}
|
||||
}
|
||||
+32
-2
@@ -1516,6 +1516,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA
|
||||
kubecontrollermanagerconfigv1alpha1.EndpointSliceControllerConfiguration{}.OpenAPIModelName(): schema_k8sio_kube_controller_manager_config_v1alpha1_EndpointSliceControllerConfiguration(ref),
|
||||
kubecontrollermanagerconfigv1alpha1.EndpointSliceMirroringControllerConfiguration{}.OpenAPIModelName(): schema_k8sio_kube_controller_manager_config_v1alpha1_EndpointSliceMirroringControllerConfiguration(ref),
|
||||
kubecontrollermanagerconfigv1alpha1.EphemeralVolumeControllerConfiguration{}.OpenAPIModelName(): schema_k8sio_kube_controller_manager_config_v1alpha1_EphemeralVolumeControllerConfiguration(ref),
|
||||
kubecontrollermanagerconfigv1alpha1.EvictionRequestControllerConfiguration{}.OpenAPIModelName(): schema_k8sio_kube_controller_manager_config_v1alpha1_EvictionRequestControllerConfiguration(ref),
|
||||
kubecontrollermanagerconfigv1alpha1.GarbageCollectorControllerConfiguration{}.OpenAPIModelName(): schema_k8sio_kube_controller_manager_config_v1alpha1_GarbageCollectorControllerConfiguration(ref),
|
||||
kubecontrollermanagerconfigv1alpha1.GroupResource{}.OpenAPIModelName(): schema_k8sio_kube_controller_manager_config_v1alpha1_GroupResource(ref),
|
||||
kubecontrollermanagerconfigv1alpha1.HPAControllerConfiguration{}.OpenAPIModelName(): schema_k8sio_kube_controller_manager_config_v1alpha1_HPAControllerConfiguration(ref),
|
||||
@@ -72061,6 +72062,28 @@ func schema_k8sio_kube_controller_manager_config_v1alpha1_EphemeralVolumeControl
|
||||
}
|
||||
}
|
||||
|
||||
func schema_k8sio_kube_controller_manager_config_v1alpha1_EvictionRequestControllerConfiguration(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "EvictionRequestControllerConfiguration contains elements describing EvictionRequestController.",
|
||||
Type: []string{"object"},
|
||||
Properties: map[string]spec.Schema{
|
||||
"ConcurrentEvictionRequestSyncs": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "ConcurrentEvictionRequestSyncs is the number of eviction requests and evictions that are allowed to sync concurrently.",
|
||||
Default: 0,
|
||||
Type: []string{"integer"},
|
||||
Format: "int32",
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"ConcurrentEvictionRequestSyncs"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func schema_k8sio_kube_controller_manager_config_v1alpha1_GarbageCollectorControllerConfiguration(ref common.ReferenceCallback) common.OpenAPIDefinition {
|
||||
return common.OpenAPIDefinition{
|
||||
Schema: spec.Schema{
|
||||
@@ -72325,6 +72348,13 @@ func schema_k8sio_kube_controller_manager_config_v1alpha1_KubeControllerManagerC
|
||||
Ref: ref(kubecontrollermanagerconfigv1alpha1.EphemeralVolumeControllerConfiguration{}.OpenAPIModelName()),
|
||||
},
|
||||
},
|
||||
"EvictionRequestController": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "EvictionRequestControllerConfiguration holds configuration for EvictionRequestController related features.",
|
||||
Default: map[string]interface{}{},
|
||||
Ref: ref(kubecontrollermanagerconfigv1alpha1.EvictionRequestControllerConfiguration{}.OpenAPIModelName()),
|
||||
},
|
||||
},
|
||||
"GarbageCollectorController": {
|
||||
SchemaProps: spec.SchemaProps{
|
||||
Description: "GarbageCollectorControllerConfiguration holds configuration for GarbageCollectorController related features.",
|
||||
@@ -72459,11 +72489,11 @@ func schema_k8sio_kube_controller_manager_config_v1alpha1_KubeControllerManagerC
|
||||
},
|
||||
},
|
||||
},
|
||||
Required: []string{"Generic", "KubeCloudShared", "AttachDetachController", "CSRSigningController", "DaemonSetController", "DeploymentController", "DisruptionController", "StatefulSetController", "DeprecatedController", "EndpointController", "EndpointSliceController", "EndpointSliceMirroringController", "EphemeralVolumeController", "GarbageCollectorController", "HPAController", "JobController", "CronJobController", "LegacySATokenCleaner", "NamespaceController", "NodeIPAMController", "NodeLifecycleController", "PersistentVolumeBinderController", "PodGCController", "ReplicaSetController", "ReplicationController", "ResourceQuotaController", "SAController", "ServiceController", "TTLAfterFinishedController", "ValidatingAdmissionPolicyStatusController", "DeviceTaintEvictionController", "ResourceClaimController"},
|
||||
Required: []string{"Generic", "KubeCloudShared", "AttachDetachController", "CSRSigningController", "DaemonSetController", "DeploymentController", "DisruptionController", "StatefulSetController", "DeprecatedController", "EndpointController", "EndpointSliceController", "EndpointSliceMirroringController", "EphemeralVolumeController", "EvictionRequestController", "GarbageCollectorController", "HPAController", "JobController", "CronJobController", "LegacySATokenCleaner", "NamespaceController", "NodeIPAMController", "NodeLifecycleController", "PersistentVolumeBinderController", "PodGCController", "ReplicaSetController", "ReplicationController", "ResourceQuotaController", "SAController", "ServiceController", "TTLAfterFinishedController", "ValidatingAdmissionPolicyStatusController", "DeviceTaintEvictionController", "ResourceClaimController"},
|
||||
},
|
||||
},
|
||||
Dependencies: []string{
|
||||
configv1alpha1.KubeCloudSharedConfiguration{}.OpenAPIModelName(), serviceconfigv1alpha1.ServiceControllerConfiguration{}.OpenAPIModelName(), controllermanagerconfigv1alpha1.GenericControllerManagerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.AttachDetachControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.CSRSigningControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.CronJobControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.DaemonSetControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.DeploymentControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.DeprecatedControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.DeviceTaintEvictionControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.DisruptionControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.EndpointControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.EndpointSliceControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.EndpointSliceMirroringControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.EphemeralVolumeControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.GarbageCollectorControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.HPAControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.JobControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.LegacySATokenCleanerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.NamespaceControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.NodeIPAMControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.NodeLifecycleControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.PersistentVolumeBinderControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.PodGCControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.ReplicaSetControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.ReplicationControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.ResourceClaimControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.ResourceQuotaControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.SAControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.StatefulSetControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.TTLAfterFinishedControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.ValidatingAdmissionPolicyStatusControllerConfiguration{}.OpenAPIModelName()},
|
||||
configv1alpha1.KubeCloudSharedConfiguration{}.OpenAPIModelName(), serviceconfigv1alpha1.ServiceControllerConfiguration{}.OpenAPIModelName(), controllermanagerconfigv1alpha1.GenericControllerManagerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.AttachDetachControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.CSRSigningControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.CronJobControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.DaemonSetControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.DeploymentControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.DeprecatedControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.DeviceTaintEvictionControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.DisruptionControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.EndpointControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.EndpointSliceControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.EndpointSliceMirroringControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.EphemeralVolumeControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.EvictionRequestControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.GarbageCollectorControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.HPAControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.JobControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.LegacySATokenCleanerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.NamespaceControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.NodeIPAMControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.NodeLifecycleControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.PersistentVolumeBinderControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.PodGCControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.ReplicaSetControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.ReplicationControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.ResourceClaimControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.ResourceQuotaControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.SAControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.StatefulSetControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.TTLAfterFinishedControllerConfiguration{}.OpenAPIModelName(), kubecontrollermanagerconfigv1alpha1.ValidatingAdmissionPolicyStatusControllerConfiguration{}.OpenAPIModelName()},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -605,6 +605,23 @@ func buildControllerRoles() ([]rbacv1.ClusterRole, []rbacv1.ClusterRoleBinding)
|
||||
})
|
||||
}
|
||||
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.EvictionRequestAPI) {
|
||||
addControllerRole(&controllerRoles, &controllerRoleBindings, rbacv1.ClusterRole{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: saRolePrefix + "evictionrequest-controller"},
|
||||
Rules: []rbacv1.PolicyRule{
|
||||
// Evictions: create, watch, update status and metadata
|
||||
rbacv1helpers.NewRule("get", "list", "watch", "create", "update", "patch").Groups(lifecycleGroup).Resources("evictions").RuleOrDie(),
|
||||
rbacv1helpers.NewRule("update", "patch").Groups(lifecycleGroup).Resources("evictions/status").RuleOrDie(),
|
||||
// EvictionRequests: watch, update status
|
||||
rbacv1helpers.NewRule("get", "list", "watch").Groups(lifecycleGroup).Resources("evictionrequests").RuleOrDie(),
|
||||
rbacv1helpers.NewRule("update", "patch").Groups(lifecycleGroup).Resources("evictionrequests/status").RuleOrDie(),
|
||||
// Pods: read pod info, labels, evictionResponders
|
||||
rbacv1helpers.NewRule("get", "list", "watch").Groups(legacyGroup).Resources("pods").RuleOrDie(),
|
||||
eventsRule(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return controllerRoles, controllerRoleBindings
|
||||
}
|
||||
|
||||
|
||||
Vendored
+59
@@ -684,6 +684,65 @@ items:
|
||||
- create
|
||||
- patch
|
||||
- update
|
||||
- apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
annotations:
|
||||
rbac.authorization.kubernetes.io/autoupdate: "true"
|
||||
labels:
|
||||
kubernetes.io/bootstrapping: rbac-defaults
|
||||
name: system:controller:evictionrequest-controller
|
||||
rules:
|
||||
- apiGroups:
|
||||
- lifecycle.k8s.io
|
||||
resources:
|
||||
- evictions
|
||||
verbs:
|
||||
- create
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- lifecycle.k8s.io
|
||||
resources:
|
||||
- evictions/status
|
||||
verbs:
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- lifecycle.k8s.io
|
||||
resources:
|
||||
- evictionrequests
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- lifecycle.k8s.io
|
||||
resources:
|
||||
- evictionrequests/status
|
||||
verbs:
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- pods
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
- events.k8s.io
|
||||
resources:
|
||||
- events
|
||||
verbs:
|
||||
- create
|
||||
- patch
|
||||
- update
|
||||
- apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
|
||||
@@ -125,6 +125,9 @@ type KubeControllerManagerConfiguration struct {
|
||||
// EphemeralVolumeControllerConfiguration holds configuration for EphemeralVolumeController
|
||||
// related features.
|
||||
EphemeralVolumeController EphemeralVolumeControllerConfiguration
|
||||
// EvictionRequestControllerConfiguration holds configuration for
|
||||
// EvictionRequestController related features.
|
||||
EvictionRequestController EvictionRequestControllerConfiguration
|
||||
// GarbageCollectorControllerConfiguration holds configuration for
|
||||
// GarbageCollectorController related features.
|
||||
GarbageCollectorController GarbageCollectorControllerConfiguration
|
||||
@@ -322,6 +325,13 @@ type EphemeralVolumeControllerConfiguration struct {
|
||||
ConcurrentEphemeralVolumeSyncs int32
|
||||
}
|
||||
|
||||
// EvictionRequestControllerConfiguration contains elements describing EvictionRequestController.
|
||||
type EvictionRequestControllerConfiguration struct {
|
||||
// ConcurrentEvictionRequestSyncs is the number of eviction requests and evictions that are
|
||||
// allowed to sync concurrently.
|
||||
ConcurrentEvictionRequestSyncs int32
|
||||
}
|
||||
|
||||
// GarbageCollectorControllerConfiguration contains elements describing GarbageCollectorController.
|
||||
type GarbageCollectorControllerConfiguration struct {
|
||||
// enables the generic garbage collector. MUST be synced with the
|
||||
|
||||
+17
@@ -242,6 +242,22 @@ func (in *EphemeralVolumeControllerConfiguration) DeepCopy() *EphemeralVolumeCon
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *EvictionRequestControllerConfiguration) DeepCopyInto(out *EvictionRequestControllerConfiguration) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EvictionRequestControllerConfiguration.
|
||||
func (in *EvictionRequestControllerConfiguration) DeepCopy() *EvictionRequestControllerConfiguration {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(EvictionRequestControllerConfiguration)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *GarbageCollectorControllerConfiguration) DeepCopyInto(out *GarbageCollectorControllerConfiguration) {
|
||||
*out = *in
|
||||
@@ -337,6 +353,7 @@ func (in *KubeControllerManagerConfiguration) DeepCopyInto(out *KubeControllerMa
|
||||
out.EndpointSliceController = in.EndpointSliceController
|
||||
out.EndpointSliceMirroringController = in.EndpointSliceMirroringController
|
||||
out.EphemeralVolumeController = in.EphemeralVolumeController
|
||||
out.EvictionRequestController = in.EvictionRequestController
|
||||
in.GarbageCollectorController.DeepCopyInto(&out.GarbageCollectorController)
|
||||
out.HPAController = in.HPAController
|
||||
out.JobController = in.JobController
|
||||
|
||||
+5
@@ -86,6 +86,11 @@ func (in EphemeralVolumeControllerConfiguration) OpenAPIModelName() string {
|
||||
return "io.k8s.kube-controller-manager.config.v1alpha1.EphemeralVolumeControllerConfiguration"
|
||||
}
|
||||
|
||||
// OpenAPIModelName returns the OpenAPI model name for this type.
|
||||
func (in EvictionRequestControllerConfiguration) OpenAPIModelName() string {
|
||||
return "io.k8s.kube-controller-manager.config.v1alpha1.EvictionRequestControllerConfiguration"
|
||||
}
|
||||
|
||||
// OpenAPIModelName returns the OpenAPI model name for this type.
|
||||
func (in GarbageCollectorControllerConfiguration) OpenAPIModelName() string {
|
||||
return "io.k8s.kube-controller-manager.config.v1alpha1.GarbageCollectorControllerConfiguration"
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# See the OWNERS docs at https://go.k8s.io/owners
|
||||
|
||||
approvers:
|
||||
- sig-node-approvers
|
||||
- atiratree
|
||||
reviewers:
|
||||
- atiratree
|
||||
labels:
|
||||
- sig/node
|
||||
- wg/node-lifecycle
|
||||
@@ -0,0 +1,953 @@
|
||||
/*
|
||||
Copyright 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 evictionrequest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/onsi/gomega"
|
||||
"github.com/onsi/gomega/gstruct"
|
||||
"github.com/onsi/gomega/types"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
lifecyclev1alpha1 "k8s.io/api/lifecycle/v1alpha1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
apimachinerytypes "k8s.io/apimachinery/pkg/types"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
lifecycleapply "k8s.io/client-go/applyconfigurations/lifecycle/v1alpha1"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/ktesting"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||
"k8s.io/kubernetes/cmd/kube-controller-manager/names"
|
||||
"k8s.io/kubernetes/pkg/controller/evictionrequest"
|
||||
"k8s.io/kubernetes/pkg/features"
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
const (
|
||||
pollInterval = 100 * time.Millisecond
|
||||
controllerResponseTimeout = 5 * time.Second
|
||||
|
||||
responderA = "a.example.com/foo"
|
||||
responderB = "b.example.com/bar"
|
||||
fieldManagerA = "a.example.com"
|
||||
fieldManagerB = "b.example.com"
|
||||
fieldManagerC = "c.example.com"
|
||||
)
|
||||
|
||||
func TestEvictionRequestValidation(t *testing.T) {
|
||||
testEvictionRequestValidation(ktesting.Init(t))
|
||||
}
|
||||
func testEvictionRequestValidation(tCtx ktesting.TContext) {
|
||||
featuregatetesting.SetFeatureGatesDuringTest(tCtx, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
|
||||
features.EvictionRequestAPI: true,
|
||||
features.GenericWorkload: true,
|
||||
})
|
||||
tCtx = apiServerSetup(tCtx)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
createPod bool
|
||||
pod *v1.Pod
|
||||
expectedConditions []metav1.Condition
|
||||
}{
|
||||
{
|
||||
name: "pod_not_found",
|
||||
pod: mkValidPod("", addPodUID("55fcee67-c0cb-409d-b610-c16d9d765b7b")),
|
||||
expectedConditions: failedConditions(lifecyclev1alpha1.EvictionConditionReasonEvictionInvalid, "not found"),
|
||||
},
|
||||
{
|
||||
name: "pod_UID_mismatch",
|
||||
pod: mkValidPod("", addPodUID("55fcee67-c0cb-409d-b610-c16d9d765b7b")),
|
||||
expectedConditions: failedConditions(lifecyclev1alpha1.EvictionConditionReasonEvictionInvalid, "UID mismatch"),
|
||||
createPod: true,
|
||||
},
|
||||
{
|
||||
name: "pod_with_schedulingGroup",
|
||||
pod: mkValidPod("", addSchedulingGroup("group")),
|
||||
expectedConditions: failedConditions(lifecyclev1alpha1.EvictionConditionReasonEvictionInvalid, "references a SchedulingGroup"),
|
||||
createPod: true,
|
||||
},
|
||||
}
|
||||
for i, tc := range testCases {
|
||||
tCtx.Run(tc.name, func(tCtx ktesting.TContext) {
|
||||
startEvictionRequestController(tCtx)
|
||||
ns := framework.CreateNamespaceOrDie(tCtx.Client(), fmt.Sprintf("test-eviction-request-validation-%d", i), tCtx)
|
||||
defer framework.DeleteNamespaceOrDie(tCtx.Client(), ns, tCtx)
|
||||
var err error
|
||||
|
||||
pod := tc.pod
|
||||
pod.Namespace = ns.Name
|
||||
if tc.createPod {
|
||||
var tmpUID apimachinerytypes.UID
|
||||
if len(pod.UID) > 0 {
|
||||
tmpUID = pod.UID
|
||||
pod.UID = ""
|
||||
}
|
||||
pod, err = tCtx.Client().CoreV1().Pods(pod.Namespace).Create(tCtx, pod, metav1.CreateOptions{})
|
||||
tCtx.ExpectNoError(err, "create pod")
|
||||
// Don't create the request immediately to prevent a race - so let's just observe it first
|
||||
pod, err = tCtx.Client().CoreV1().Pods(pod.Namespace).Get(tCtx, pod.Name, metav1.GetOptions{})
|
||||
tCtx.ExpectNoError(err, "get pod")
|
||||
pod, err = markPodRunning(tCtx, time.Now(), pod)
|
||||
tCtx.ExpectNoError(err, "mark pod running")
|
||||
if len(tmpUID) > 0 {
|
||||
pod.UID = tmpUID
|
||||
}
|
||||
}
|
||||
|
||||
evictionRequest := mkValidEvictionRequest(pod)
|
||||
evictionRequest, err = tCtx.Client().LifecycleV1alpha1().EvictionRequests(evictionRequest.Namespace).Create(tCtx, evictionRequest, metav1.CreateOptions{})
|
||||
tCtx.ExpectNoError(err, "create eviction request")
|
||||
|
||||
expectConditions := matchExpectedConditions(tc.expectedConditions...)
|
||||
expectGeneration := gomega.HaveField("Status.ObservedGeneration", gomega.Equal(new(int64(1))))
|
||||
|
||||
tCtx.Eventually(getEvictionRequest(evictionRequest)).WithTimeout(controllerResponseTimeout).WithPolling(pollInterval).Should(gomega.And(expectConditions, expectGeneration))
|
||||
evictionRequest, err = getEvictionRequest(evictionRequest)(tCtx)
|
||||
tCtx.ExpectNoError(err, "get eviction request")
|
||||
evictionRequest.Spec.Intent = lifecyclev1alpha1.EvictionRequestIntentWithdrawn
|
||||
// trigger update
|
||||
// observedGeneration should be managed even after final conditions are set
|
||||
expectGeneration = gomega.HaveField("Status.ObservedGeneration", gomega.Equal(new(int64(2))))
|
||||
evictionRequest, err = tCtx.Client().LifecycleV1alpha1().EvictionRequests(evictionRequest.Namespace).Update(tCtx, evictionRequest, metav1.UpdateOptions{})
|
||||
tCtx.ExpectNoError(err, "update eviction request")
|
||||
|
||||
tCtx.Eventually(getEvictionRequest(evictionRequest)).WithTimeout(controllerResponseTimeout).WithPolling(pollInterval).Should(gomega.And(expectConditions, expectGeneration))
|
||||
|
||||
// status should not change after
|
||||
tCtx.Consistently(getEvictionRequest(evictionRequest)).WithTimeout(300 * time.Millisecond).WithPolling(pollInterval).Should(gomega.And(expectConditions, expectGeneration))
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestEvictionValidation(t *testing.T) {
|
||||
testEvictionValidation(ktesting.Init(t))
|
||||
}
|
||||
func testEvictionValidation(tCtx ktesting.TContext) {
|
||||
featuregatetesting.SetFeatureGatesDuringTest(tCtx, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
|
||||
features.EvictionRequestAPI: true,
|
||||
features.GenericWorkload: true,
|
||||
})
|
||||
tCtx = apiServerSetup(tCtx)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
createPod bool
|
||||
pod *v1.Pod
|
||||
expectedConditions []metav1.Condition
|
||||
}{
|
||||
{
|
||||
name: "pod_with_schedulingGroup",
|
||||
pod: mkValidPod("", addSchedulingGroup("group")),
|
||||
expectedConditions: failedConditions(lifecyclev1alpha1.EvictionConditionReasonEvictionInvalid, "references a SchedulingGroup"),
|
||||
createPod: true,
|
||||
},
|
||||
}
|
||||
for i, tc := range testCases {
|
||||
tCtx.Run(tc.name, func(tCtx ktesting.TContext) {
|
||||
startEvictionRequestController(tCtx)
|
||||
ns := framework.CreateNamespaceOrDie(tCtx.Client(), fmt.Sprintf("test-eviction-validation-%d", i), tCtx)
|
||||
defer framework.DeleteNamespaceOrDie(tCtx.Client(), ns, tCtx)
|
||||
var err error
|
||||
|
||||
pod := tc.pod
|
||||
pod.Namespace = ns.Name
|
||||
if tc.createPod {
|
||||
pod, err = tCtx.Client().CoreV1().Pods(pod.Namespace).Create(tCtx, pod, metav1.CreateOptions{})
|
||||
tCtx.ExpectNoError(err, "create pod")
|
||||
// Don't create the request immediately to prevent a race - so let's just observe it first
|
||||
pod, err = tCtx.Client().CoreV1().Pods(pod.Namespace).Get(tCtx, pod.Name, metav1.GetOptions{})
|
||||
tCtx.ExpectNoError(err, "get pod")
|
||||
pod, err = markPodRunning(tCtx, time.Now(), pod)
|
||||
tCtx.ExpectNoError(err, "mark pod running")
|
||||
}
|
||||
|
||||
eviction := mkValidEviction(pod)
|
||||
eviction, err = tCtx.Client().LifecycleV1alpha1().Evictions(eviction.Namespace).Create(tCtx, eviction, metav1.CreateOptions{})
|
||||
tCtx.ExpectNoError(err, "create eviction")
|
||||
|
||||
expectConditions := matchExpectedConditions(tc.expectedConditions...)
|
||||
expectGeneration := gomega.HaveField("Status.ObservedGeneration", gomega.Equal(new(int64(1))))
|
||||
|
||||
tCtx.Eventually(getEviction(eviction)).WithTimeout(controllerResponseTimeout).WithPolling(pollInterval).Should(gomega.And(expectConditions, expectGeneration))
|
||||
// status should not change after
|
||||
tCtx.Consistently(getEviction(eviction)).WithTimeout(300 * time.Millisecond).WithPolling(pollInterval).Should(gomega.And(expectConditions, expectGeneration))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvictionRequestEvictionProcess(t *testing.T) {
|
||||
testEvictionRequestEvictionProcess(ktesting.Init(t))
|
||||
}
|
||||
func testEvictionRequestEvictionProcess(tCtx ktesting.TContext) {
|
||||
featuregatetesting.SetFeatureGatesDuringTest(tCtx, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
|
||||
features.EvictionRequestAPI: true,
|
||||
features.GenericWorkload: true,
|
||||
})
|
||||
tCtx = apiServerSetup(tCtx)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
deletePod bool
|
||||
terminatePod bool
|
||||
cancelEviction bool
|
||||
completeWithoutTermination bool
|
||||
responderLastUpdateMissing bool
|
||||
createNewRequester bool
|
||||
expectedConditions []metav1.Condition
|
||||
expectedFailedCondition metav1.Condition
|
||||
expectedTargetEvictedCondition metav1.Condition
|
||||
}{
|
||||
{
|
||||
name: "should_evict_for_pod_termination",
|
||||
terminatePod: true,
|
||||
expectedConditions: evictedConditions(lifecyclev1alpha1.EvictionConditionReasonPodTerminal, "terminal"),
|
||||
},
|
||||
{
|
||||
name: "should_evict_for_pod_deletion",
|
||||
deletePod: true,
|
||||
expectedConditions: evictedConditions(lifecyclev1alpha1.EvictionConditionReasonPodDeleted, "deleted"),
|
||||
},
|
||||
{
|
||||
name: "should_cancel_eviction,_and_allow_for_triggering_it_again_via_the_same_eviction_request",
|
||||
cancelEviction: true,
|
||||
deletePod: true,
|
||||
expectedConditions: evictedConditions(lifecyclev1alpha1.EvictionConditionReasonPodDeleted, "deleted"),
|
||||
},
|
||||
{
|
||||
name: "should_cancel_eviction,_and_allow_for_triggering_it_again_via_different_eviction_request",
|
||||
cancelEviction: true,
|
||||
deletePod: true,
|
||||
createNewRequester: true,
|
||||
expectedConditions: evictedConditions(lifecyclev1alpha1.EvictionConditionReasonPodDeleted, "deleted"),
|
||||
},
|
||||
{
|
||||
name: "complete_responders_without_pod_termination_fails_eviction,_but_recovers_with_a_new_eviction",
|
||||
terminatePod: true,
|
||||
completeWithoutTermination: true,
|
||||
expectedConditions: evictedConditions(lifecyclev1alpha1.EvictionConditionReasonPodTerminal, "terminal"),
|
||||
},
|
||||
{
|
||||
name: "should_evict_with_pod_termination,_but_last_responder_update_fails",
|
||||
terminatePod: true,
|
||||
responderLastUpdateMissing: true,
|
||||
expectedConditions: evictedConditions(lifecyclev1alpha1.EvictionConditionReasonPodTerminal, "terminal"),
|
||||
},
|
||||
}
|
||||
for i, tc := range testCases {
|
||||
tCtx.Run(tc.name, func(tCtx ktesting.TContext) {
|
||||
startEvictionRequestController(tCtx)
|
||||
ns := framework.CreateNamespaceOrDie(tCtx.Client(), fmt.Sprintf("test-eviction-request-processing-%d", i), tCtx)
|
||||
defer framework.DeleteNamespaceOrDie(tCtx.Client(), ns, tCtx)
|
||||
var err error
|
||||
|
||||
// Create Pod
|
||||
pod := mkValidPod("", addFinalizer("prevent/deletion"),
|
||||
addResponder(responderB, 1000),
|
||||
addResponder(responderA, 1001))
|
||||
pod.Namespace = ns.Name
|
||||
pod, err = tCtx.Client().CoreV1().Pods(pod.Namespace).Create(tCtx, pod, metav1.CreateOptions{})
|
||||
tCtx.ExpectNoError(err, "create pod")
|
||||
// Don't create the request immediately to prevent a race - so let's just observe it first
|
||||
pod, err = tCtx.Client().CoreV1().Pods(pod.Namespace).Get(tCtx, pod.Name, metav1.GetOptions{})
|
||||
tCtx.ExpectNoError(err, "get pod")
|
||||
pod, err = markPodRunning(tCtx, time.Now(), pod)
|
||||
tCtx.ExpectNoError(err, "mark pod running")
|
||||
|
||||
// Create EvictionRequest
|
||||
evictionRequest := mkValidEvictionRequest(pod)
|
||||
evictionRequest, err = tCtx.Client().LifecycleV1alpha1().EvictionRequests(evictionRequest.Namespace).Create(tCtx, evictionRequest, metav1.CreateOptions{})
|
||||
tCtx.ExpectNoError(err, "create eviction request")
|
||||
|
||||
// Check EvictionRequest
|
||||
expectAwaitingEvictionConditions := matchExpectedConditions(metav1.Condition{
|
||||
Type: string(lifecyclev1alpha1.EvictionConditionTargetEvicted),
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: string(lifecyclev1alpha1.EvictionConditionReasonAwaitingEviction),
|
||||
}, metav1.Condition{
|
||||
|
||||
Type: string(lifecyclev1alpha1.EvictionConditionFailed),
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: string(lifecyclev1alpha1.EvictionConditionReasonAwaitingEviction),
|
||||
})
|
||||
expectEvictionRequestGeneration := gomega.HaveField("Status.ObservedGeneration", gomega.Equal(new(int64(1))))
|
||||
|
||||
tCtx.Eventually(getEvictionRequest(evictionRequest)).WithTimeout(controllerResponseTimeout).WithPolling(pollInterval).Should(
|
||||
gomega.And(expectAwaitingEvictionConditions, expectEvictionRequestGeneration))
|
||||
|
||||
// Test Eviction creation
|
||||
eviction := &lifecyclev1alpha1.Eviction{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pod-1-" + pod.Name,
|
||||
Namespace: pod.Namespace,
|
||||
},
|
||||
}
|
||||
expectEvictionSpec := gomega.HaveField("Spec", gomega.Equal(lifecyclev1alpha1.EvictionSpec{
|
||||
Target: lifecyclev1alpha1.EvictionTarget{
|
||||
Pod: &lifecyclev1alpha1.EvictionPodReference{
|
||||
Name: pod.Name,
|
||||
UID: pod.UID,
|
||||
},
|
||||
},
|
||||
}))
|
||||
tCtx.Eventually(getEviction(eviction)).WithTimeout(controllerResponseTimeout).WithPolling(pollInterval).Should(expectEvictionSpec)
|
||||
evictions, err := tCtx.Client().LifecycleV1alpha1().Evictions(evictionRequest.Namespace).List(tCtx, metav1.ListOptions{})
|
||||
tCtx.ExpectNoError(err, "list evictions")
|
||||
tCtx.Expect(evictions.Items).To(gomega.HaveLen(1))
|
||||
eviction = new(evictions.Items[0])
|
||||
|
||||
// Test Eviction progress
|
||||
expectRequesters := gomega.HaveField("Status.Requesters", gomega.Equal([]lifecyclev1alpha1.Requester{
|
||||
{Name: evictionRequest.Spec.Requester, Intent: lifecyclev1alpha1.RequesterIntentEviction},
|
||||
}))
|
||||
expectEvictionGeneration := gomega.HaveField("Status.ObservedGeneration", gomega.Equal(new(int64(1))))
|
||||
expectEvictionLabels := gomega.HaveField("Labels", gomega.Equal(map[string]string{
|
||||
"foo.example.com/1": "requester",
|
||||
responderA: "responder",
|
||||
responderB: "responder",
|
||||
lifecyclev1alpha1.EvictionResponderImperativeEviction: "responder",
|
||||
}))
|
||||
expectEvictionOwnerRef := gomega.HaveField("OwnerReferences", gomega.Equal([]metav1.OwnerReference{{
|
||||
APIVersion: "lifecycle.k8s.io/v1alpha1",
|
||||
Kind: "EvictionRequest",
|
||||
Name: evictionRequest.Name,
|
||||
UID: evictionRequest.UID,
|
||||
}}))
|
||||
|
||||
// First Responder selected
|
||||
expectTargetRespondersMatcher := matchResponderState(
|
||||
lifecyclev1alpha1.ResponderStateActive,
|
||||
lifecyclev1alpha1.ResponderStateInactive,
|
||||
lifecyclev1alpha1.ResponderStateInactive)
|
||||
expectStatusResponders := matchExpectedStatusResponders([]string{responderA, responderB, lifecyclev1alpha1.EvictionResponderImperativeEviction}, responderA)
|
||||
|
||||
tCtx.Eventually(getEviction(eviction)).WithTimeout(controllerResponseTimeout).WithPolling(pollInterval).Should(gomega.And(
|
||||
expectEvictionLabels,
|
||||
expectEvictionOwnerRef,
|
||||
expectEvictionGeneration,
|
||||
expectRequesters,
|
||||
expectTargetRespondersMatcher,
|
||||
expectStatusResponders,
|
||||
expectAwaitingEvictionConditions,
|
||||
))
|
||||
// First responder progress and completion
|
||||
eviction, err = markResponderUpdate(tCtx, fieldManagerA, eviction, responderA, false)
|
||||
tCtx.ExpectNoError(err, "eviction responder update")
|
||||
|
||||
eviction, err = markResponderUpdate(tCtx, fieldManagerA, eviction, responderA, true)
|
||||
tCtx.ExpectNoError(err, "eviction responder update completion")
|
||||
|
||||
// Second Responder selected
|
||||
expectTargetRespondersMatcher = matchResponderState(
|
||||
lifecyclev1alpha1.ResponderStateCompleted,
|
||||
lifecyclev1alpha1.ResponderStateActive,
|
||||
lifecyclev1alpha1.ResponderStateInactive)
|
||||
expectStatusResponders = matchExpectedStatusResponders([]string{responderA, responderB, lifecyclev1alpha1.EvictionResponderImperativeEviction}, responderB)
|
||||
|
||||
tCtx.Eventually(getEviction(eviction)).WithTimeout(controllerResponseTimeout).WithPolling(pollInterval).Should(gomega.And(
|
||||
expectEvictionGeneration,
|
||||
expectRequesters,
|
||||
expectTargetRespondersMatcher,
|
||||
expectStatusResponders,
|
||||
expectAwaitingEvictionConditions,
|
||||
))
|
||||
|
||||
// Second responder progress
|
||||
eviction, err = markResponderUpdate(tCtx, fieldManagerB, eviction, responderB, false)
|
||||
tCtx.ExpectNoError(err, "eviction responder update")
|
||||
|
||||
switch {
|
||||
case tc.cancelEviction:
|
||||
// Cancel
|
||||
evictionRequest, err = getEvictionRequest(evictionRequest)(tCtx)
|
||||
tCtx.ExpectNoError(err, "get eviction request")
|
||||
evictionRequest.Spec.Intent = lifecyclev1alpha1.EvictionRequestIntentWithdrawn
|
||||
evictionRequest, err = tCtx.Client().LifecycleV1alpha1().EvictionRequests(evictionRequest.Namespace).Update(tCtx, evictionRequest, metav1.UpdateOptions{})
|
||||
tCtx.ExpectNoError(err, "update eviction request")
|
||||
tCtx.Eventually(getEviction(eviction)).WithTimeout(controllerResponseTimeout).WithPolling(pollInterval).Should(
|
||||
matchResponderState(
|
||||
lifecyclev1alpha1.ResponderStateCompleted,
|
||||
lifecyclev1alpha1.ResponderStateCanceled,
|
||||
lifecyclev1alpha1.ResponderStateInactive))
|
||||
// Observe failed conditions
|
||||
expectConditions := matchExpectedConditions(
|
||||
failedConditions(lifecyclev1alpha1.EvictionConditionReasonCanceledDueToNoRequesters, "No active requesters")...)
|
||||
tCtx.Eventually(getEviction(eviction)).WithTimeout(controllerResponseTimeout).WithPolling(pollInterval).Should(
|
||||
gomega.And(expectConditions, expectEvictionGeneration))
|
||||
// Same conditions are written to the EvictionRequest.
|
||||
expectEvictionRequestGeneration = gomega.HaveField("Status.ObservedGeneration", gomega.Equal(new(int64(2))))
|
||||
tCtx.Eventually(getEvictionRequest(evictionRequest)).WithTimeout(controllerResponseTimeout).WithPolling(pollInterval).Should(
|
||||
gomega.And(expectConditions, expectEvictionRequestGeneration))
|
||||
// Start eviction again
|
||||
if tc.createNewRequester {
|
||||
oldEvictionRequest := evictionRequest
|
||||
evictionRequest = mkValidEvictionRequest(pod, setERRequester("foo.example.com/2"))
|
||||
evictionRequest, err = tCtx.Client().LifecycleV1alpha1().EvictionRequests(evictionRequest.Namespace).Create(tCtx, evictionRequest, metav1.CreateOptions{})
|
||||
tCtx.ExpectNoError(err, "create eviction request")
|
||||
expectEvictionRequestGeneration = gomega.HaveField("Status.ObservedGeneration", gomega.Equal(new(int64(1))))
|
||||
expectEvictionLabels = gomega.HaveField("Labels", gomega.Equal(map[string]string{
|
||||
"foo.example.com/1": "requester",
|
||||
"foo.example.com/2": "requester",
|
||||
responderA: "responder",
|
||||
responderB: "responder",
|
||||
lifecyclev1alpha1.EvictionResponderImperativeEviction: "responder",
|
||||
}))
|
||||
expectEvictionOwnerRef = gomega.HaveField("OwnerReferences", gomega.And(gomega.ContainElement(gomega.Equal(metav1.OwnerReference{
|
||||
APIVersion: "lifecycle.k8s.io/v1alpha1",
|
||||
Kind: "EvictionRequest",
|
||||
Name: oldEvictionRequest.Name,
|
||||
UID: oldEvictionRequest.UID,
|
||||
})), gomega.ContainElement(gomega.Equal(metav1.OwnerReference{
|
||||
APIVersion: "lifecycle.k8s.io/v1alpha1",
|
||||
Kind: "EvictionRequest",
|
||||
Name: evictionRequest.Name,
|
||||
UID: evictionRequest.UID,
|
||||
}))),
|
||||
)
|
||||
} else {
|
||||
evictionRequest, err = tCtx.Client().LifecycleV1alpha1().EvictionRequests(evictionRequest.Namespace).Get(tCtx, evictionRequest.Name, metav1.GetOptions{})
|
||||
tCtx.ExpectNoError(err, "get eviction request")
|
||||
evictionRequest.Spec.Intent = lifecyclev1alpha1.EvictionRequestIntentEviction
|
||||
evictionRequest, err = tCtx.Client().LifecycleV1alpha1().EvictionRequests(evictionRequest.Namespace).Update(tCtx, evictionRequest, metav1.UpdateOptions{})
|
||||
tCtx.ExpectNoError(err, "update eviction request")
|
||||
expectEvictionRequestGeneration = gomega.HaveField("Status.ObservedGeneration", gomega.Equal(new(int64(3))))
|
||||
}
|
||||
case tc.completeWithoutTermination:
|
||||
// Complete 2nd responder
|
||||
eviction, err = markResponderUpdate(tCtx, fieldManagerB, eviction, responderB, true)
|
||||
tCtx.ExpectNoError(err, "eviction responder update")
|
||||
tCtx.Eventually(getEviction(eviction)).WithTimeout(controllerResponseTimeout).WithPolling(pollInterval).Should(
|
||||
matchResponderState(
|
||||
lifecyclev1alpha1.ResponderStateCompleted,
|
||||
lifecyclev1alpha1.ResponderStateCompleted,
|
||||
lifecyclev1alpha1.ResponderStateActive))
|
||||
// Complete 3rd responder
|
||||
eviction, err = markResponderUpdate(tCtx, fieldManagerC, eviction, lifecyclev1alpha1.EvictionResponderImperativeEviction, true)
|
||||
tCtx.ExpectNoError(err, "eviction responder update")
|
||||
tCtx.Eventually(getEviction(eviction)).WithTimeout(controllerResponseTimeout).WithPolling(pollInterval).Should(
|
||||
matchResponderState(
|
||||
lifecyclev1alpha1.ResponderStateCompleted,
|
||||
lifecyclev1alpha1.ResponderStateCompleted,
|
||||
lifecyclev1alpha1.ResponderStateCompleted))
|
||||
// Observe failed conditions
|
||||
expectConditions := matchExpectedConditions(
|
||||
failedConditions(lifecyclev1alpha1.EvictionConditionReasonNoFurtherResponder, "completed without evicting")...)
|
||||
tCtx.Eventually(getEviction(eviction)).WithTimeout(controllerResponseTimeout).WithPolling(pollInterval).Should(
|
||||
gomega.And(expectConditions, expectEvictionGeneration))
|
||||
// EvictionRequest recovers and immediately becomes AwaitingEviction
|
||||
}
|
||||
|
||||
// recover from a failed eviction
|
||||
if tc.cancelEviction || tc.completeWithoutTermination {
|
||||
// New Eviction should be immediately created by the controller
|
||||
eviction = &lifecyclev1alpha1.Eviction{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "pod-2-" + pod.Name,
|
||||
Namespace: pod.Namespace,
|
||||
},
|
||||
}
|
||||
// Ensure eviction active
|
||||
tCtx.Eventually(getEviction(eviction)).WithTimeout(controllerResponseTimeout).WithPolling(pollInterval).Should(expectEvictionSpec)
|
||||
eviction, err = tCtx.Client().LifecycleV1alpha1().Evictions(evictionRequest.Namespace).Get(tCtx, eviction.Name, metav1.GetOptions{})
|
||||
tCtx.ExpectNoError(err, "get second eviction")
|
||||
tCtx.Eventually(getEviction(eviction)).WithTimeout(controllerResponseTimeout).WithPolling(pollInterval).Should(
|
||||
gomega.And(expectEvictionLabels, expectEvictionOwnerRef, matchResponderState(
|
||||
lifecyclev1alpha1.ResponderStateActive,
|
||||
lifecyclev1alpha1.ResponderStateInactive,
|
||||
lifecyclev1alpha1.ResponderStateInactive)))
|
||||
// Complete first responder
|
||||
eviction, err = markResponderUpdate(tCtx, fieldManagerA, eviction, responderA, true)
|
||||
tCtx.ExpectNoError(err, "eviction responder update completion")
|
||||
}
|
||||
|
||||
tCtx.Eventually(getEviction(eviction)).WithTimeout(controllerResponseTimeout).WithPolling(pollInterval).Should(
|
||||
matchResponderState(
|
||||
lifecyclev1alpha1.ResponderStateCompleted,
|
||||
lifecyclev1alpha1.ResponderStateActive,
|
||||
lifecyclev1alpha1.ResponderStateInactive))
|
||||
|
||||
startDeletion := time.Now()
|
||||
if tc.deletePod {
|
||||
err = deletePod(tCtx, pod)
|
||||
tCtx.ExpectNoError(err, "delete pod")
|
||||
} else if tc.terminatePod {
|
||||
pod, err = markPodSucceeded(tCtx, pod)
|
||||
tCtx.ExpectNoError(err, "mark pod succeeded")
|
||||
} else {
|
||||
tCtx.Errorf("should complete by either pod termination or deletion")
|
||||
}
|
||||
|
||||
// It is possible for responder B to do a delayed completion update in order to observe pod deletion
|
||||
// simulate pod observation by the responder
|
||||
_, err = tCtx.Client().CoreV1().Pods(pod.Namespace).Get(tCtx, pod.Name, metav1.GetOptions{})
|
||||
if tc.deletePod {
|
||||
if err == nil || !apierrors.IsNotFound(err) {
|
||||
tCtx.Errorf("expected not found err: %v", err)
|
||||
}
|
||||
} else {
|
||||
tCtx.ExpectNoError(err, "get pod")
|
||||
}
|
||||
|
||||
// The following tests can fail if the termination takes too long.
|
||||
// See GracefulCompletionDelay in evictionrequest-controller (currently 5s).
|
||||
tCtx.Logf("Termination of pod %v took %v", klog.KObj(pod), time.Since(startDeletion))
|
||||
|
||||
if !tc.responderLastUpdateMissing {
|
||||
// Second responder complete
|
||||
eviction, err = markResponderUpdate(tCtx, fieldManagerB, eviction, responderB, true)
|
||||
tCtx.ExpectNoError(err, "eviction responder update")
|
||||
expectTargetRespondersMatcher = matchResponderState(
|
||||
lifecyclev1alpha1.ResponderStateCompleted,
|
||||
lifecyclev1alpha1.ResponderStateCompleted,
|
||||
lifecyclev1alpha1.ResponderStateInactive)
|
||||
} else {
|
||||
expectTargetRespondersMatcher = matchResponderState(
|
||||
lifecyclev1alpha1.ResponderStateCompleted,
|
||||
lifecyclev1alpha1.ResponderStateInterrupted,
|
||||
lifecyclev1alpha1.ResponderStateInactive)
|
||||
}
|
||||
|
||||
// Observe completion - controller finalizes status
|
||||
expectConditions := matchExpectedConditions(tc.expectedConditions...)
|
||||
|
||||
// 5s is the controller backoff if tc.responderLastUpdateMissing
|
||||
tCtx.Eventually(getEviction(eviction)).WithTimeout(10 * time.Second).WithPolling(pollInterval).Should(
|
||||
gomega.And(
|
||||
expectEvictionLabels,
|
||||
expectEvictionOwnerRef,
|
||||
expectConditions,
|
||||
expectTargetRespondersMatcher,
|
||||
expectEvictionGeneration,
|
||||
))
|
||||
// Same conditions are written to the EvictionRequest.
|
||||
tCtx.Eventually(getEvictionRequest(evictionRequest)).WithTimeout(controllerResponseTimeout).WithPolling(pollInterval).Should(
|
||||
gomega.And(expectConditions, expectEvictionRequestGeneration))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvictionTerminalTimeFallback(t *testing.T) {
|
||||
testEvictionTerminalTimeFallback(ktesting.Init(t))
|
||||
}
|
||||
func testEvictionTerminalTimeFallback(tCtx ktesting.TContext) {
|
||||
featuregatetesting.SetFeatureGatesDuringTest(tCtx, utilfeature.DefaultFeatureGate, featuregatetesting.FeatureOverrides{
|
||||
features.EvictionRequestAPI: true,
|
||||
features.GenericWorkload: true,
|
||||
})
|
||||
tCtx = apiServerSetup(tCtx)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
createPod bool
|
||||
pod *v1.Pod
|
||||
expectedConditions []metav1.Condition
|
||||
}{
|
||||
{
|
||||
name: "pod-gone-no-terminal-time-present",
|
||||
pod: mkValidPod(""),
|
||||
expectedConditions: evictedConditions(lifecyclev1alpha1.EvictionConditionReasonPodDeleted, "deleted"),
|
||||
},
|
||||
{
|
||||
name: "pod-present-no-terminal-time-present",
|
||||
pod: mkValidPod(""),
|
||||
expectedConditions: evictedConditions(lifecyclev1alpha1.EvictionConditionReasonPodTerminal, "terminal"),
|
||||
createPod: true,
|
||||
},
|
||||
}
|
||||
for i, tc := range testCases {
|
||||
tCtx.Run(tc.name, func(tCtx ktesting.TContext) {
|
||||
ns := framework.CreateNamespaceOrDie(tCtx.Client(), fmt.Sprintf("test-eviction-terminal-time-fallback-%d", i), tCtx)
|
||||
defer framework.DeleteNamespaceOrDie(tCtx.Client(), ns, tCtx)
|
||||
var err error
|
||||
|
||||
pod := tc.pod
|
||||
pod.Namespace = ns.Name
|
||||
if tc.createPod {
|
||||
pod, err = tCtx.Client().CoreV1().Pods(pod.Namespace).Create(tCtx, pod, metav1.CreateOptions{})
|
||||
tCtx.ExpectNoError(err, "create pod")
|
||||
// Don't create the request immediately to prevent a race - so let's just observe it first
|
||||
pod, err = tCtx.Client().CoreV1().Pods(pod.Namespace).Get(tCtx, pod.Name, metav1.GetOptions{})
|
||||
tCtx.ExpectNoError(err, "get pod")
|
||||
pod, err = markPodRunning(tCtx, time.Now(), pod)
|
||||
tCtx.ExpectNoError(err, "mark pod running")
|
||||
pod.Status.Phase = v1.PodSucceeded
|
||||
// We can generally depend on the kubelet to set the Terminated container state.
|
||||
// This serves as a broad test to fulfill the generic targetInfo.terminalTime() method behavior.
|
||||
pod, err = tCtx.Client().CoreV1().Pods(pod.Namespace).UpdateStatus(tCtx, pod, metav1.UpdateOptions{})
|
||||
tCtx.ExpectNoError(err, "mark pod succeeded")
|
||||
} else {
|
||||
pod.UID = "539bb197-7e4a-4698-8863-d4bacdb9ecf0"
|
||||
}
|
||||
|
||||
evictionRequest := mkValidEvictionRequest(pod)
|
||||
_, err = tCtx.Client().LifecycleV1alpha1().EvictionRequests(evictionRequest.Namespace).Create(tCtx, evictionRequest, metav1.CreateOptions{})
|
||||
tCtx.ExpectNoError(err, "create eviction request")
|
||||
eviction := mkValidEviction(pod)
|
||||
eviction, err = tCtx.Client().LifecycleV1alpha1().Evictions(eviction.Namespace).Create(tCtx, eviction, metav1.CreateOptions{})
|
||||
tCtx.ExpectNoError(err, "create eviction")
|
||||
|
||||
// Simulate the controller reconciliation; initialize responders of the missing pod.
|
||||
statusApply := lifecycleapply.EvictionStatus()
|
||||
targetRespondersApply := lifecycleapply.TargetResponder().
|
||||
WithName(lifecyclev1alpha1.EvictionResponderImperativeEviction).
|
||||
WithState(lifecyclev1alpha1.ResponderStateActive).
|
||||
WithPriority(100)
|
||||
statusApply.WithTargetResponders(targetRespondersApply)
|
||||
|
||||
responderStatusApply := lifecycleapply.ResponderStatus().
|
||||
WithName(lifecyclev1alpha1.EvictionResponderImperativeEviction).
|
||||
WithStartTime(metav1.Now())
|
||||
statusApply.WithResponders(responderStatusApply)
|
||||
|
||||
applyConfig := lifecycleapply.Eviction(eviction.Name, eviction.Namespace).
|
||||
WithStatus(statusApply)
|
||||
|
||||
eviction, err = tCtx.Client().LifecycleV1alpha1().
|
||||
Evictions(eviction.Namespace).
|
||||
ApplyStatus(tCtx, applyConfig, metav1.ApplyOptions{
|
||||
FieldManager: names.EvictionRequestController,
|
||||
})
|
||||
tCtx.ExpectNoError(err, "apply eviction status")
|
||||
|
||||
// Simulate pod deletion race by starting the controller after we have created the eviction request and the eviction.
|
||||
startEvictionRequestController(tCtx)
|
||||
|
||||
expectGeneration := gomega.HaveField("Status.ObservedGeneration", gomega.Equal(new(int64(1))))
|
||||
|
||||
// Check that the controller has accepted the half-baked status and is processing the status updates.
|
||||
tCtx.Eventually(getEviction(eviction)).WithTimeout(controllerResponseTimeout).WithPolling(pollInterval).Should(expectGeneration)
|
||||
// Status should not change for 4-5s (see GracefulCompletionDelay)
|
||||
tCtx.Consistently(getEviction(eviction)).WithTimeout(4 * time.Second).WithPolling(pollInterval).Should(gomega.And(
|
||||
matchExpectedConditions(awaitingConditions()...),
|
||||
expectGeneration),
|
||||
)
|
||||
tCtx.Eventually(getEviction(eviction)).WithTimeout(controllerResponseTimeout).WithPolling(pollInterval).Should(gomega.And(
|
||||
matchExpectedConditions(tc.expectedConditions...),
|
||||
expectGeneration),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func awaitingConditions() []metav1.Condition {
|
||||
return []metav1.Condition{
|
||||
{
|
||||
Type: string(lifecyclev1alpha1.EvictionConditionFailed),
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: string(lifecyclev1alpha1.EvictionConditionReasonAwaitingEviction),
|
||||
Message: "",
|
||||
},
|
||||
{
|
||||
Type: string(lifecyclev1alpha1.EvictionConditionTargetEvicted),
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: string(lifecyclev1alpha1.EvictionConditionReasonAwaitingEviction),
|
||||
Message: "",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func failedConditions(reason lifecyclev1alpha1.EvictionConditionReason, message string) []metav1.Condition {
|
||||
return []metav1.Condition{
|
||||
{
|
||||
Type: string(lifecyclev1alpha1.EvictionConditionFailed),
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: string(reason),
|
||||
Message: message,
|
||||
},
|
||||
{
|
||||
Type: string(lifecyclev1alpha1.EvictionConditionTargetEvicted),
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: string(lifecyclev1alpha1.EvictionConditionReasonEvictionFailed),
|
||||
},
|
||||
}
|
||||
}
|
||||
func evictedConditions(reason lifecyclev1alpha1.EvictionConditionReason, message string) []metav1.Condition {
|
||||
return []metav1.Condition{
|
||||
{
|
||||
Type: string(lifecyclev1alpha1.EvictionConditionFailed),
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: string(lifecyclev1alpha1.EvictionConditionReasonSucceeded),
|
||||
},
|
||||
{
|
||||
Type: string(lifecyclev1alpha1.EvictionConditionTargetEvicted),
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: string(reason),
|
||||
Message: message,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func matchExpectedConditions(conditions ...metav1.Condition,
|
||||
) types.GomegaMatcher {
|
||||
var containElems []types.GomegaMatcher
|
||||
for _, condition := range conditions {
|
||||
containElems = append(containElems, gomega.ContainElement(
|
||||
gstruct.MatchFields(gstruct.IgnoreExtras, gstruct.Fields{
|
||||
"Type": gomega.Equal(condition.Type),
|
||||
"Status": gomega.Equal(condition.Status),
|
||||
"Reason": gomega.Equal(condition.Reason),
|
||||
"Message": gomega.ContainSubstring(condition.Message),
|
||||
}),
|
||||
))
|
||||
}
|
||||
return gomega.HaveField("Status.Conditions", gomega.And(containElems...))
|
||||
}
|
||||
|
||||
func matchResponderState(a, b, c lifecyclev1alpha1.ResponderStateType) gomega.OmegaMatcher {
|
||||
return gomega.HaveField("Status.TargetResponders", gomega.Equal([]lifecyclev1alpha1.TargetResponder{
|
||||
{Name: responderA, Priority: new(int32(1001)), State: a},
|
||||
{Name: responderB, Priority: new(int32(1000)), State: b},
|
||||
{Name: lifecyclev1alpha1.EvictionResponderImperativeEviction, Priority: new(int32(100)), State: c},
|
||||
}))
|
||||
}
|
||||
|
||||
func matchExpectedStatusResponders(targetResponders []string, startedResponder string) types.GomegaMatcher {
|
||||
var matchers []types.GomegaMatcher
|
||||
for _, responder := range targetResponders {
|
||||
fields := gstruct.Fields{
|
||||
"Name": gomega.Equal(responder),
|
||||
}
|
||||
if responder == startedResponder {
|
||||
fields["StartTime"] = gomega.Not(gomega.BeNil())
|
||||
}
|
||||
matchers = append(matchers, gomega.ContainElement(
|
||||
gstruct.MatchFields(gstruct.IgnoreExtras, fields),
|
||||
))
|
||||
}
|
||||
return gomega.HaveField("Status.Responders", gomega.And(matchers...))
|
||||
}
|
||||
|
||||
func markPodRunning(tCtx ktesting.TContext, time time.Time, pod *v1.Pod) (*v1.Pod, error) {
|
||||
pod.Status.Phase = v1.PodRunning
|
||||
pod.Status.Conditions = []v1.PodCondition{{
|
||||
Type: v1.PodReady,
|
||||
Status: v1.ConditionTrue,
|
||||
LastTransitionTime: metav1.Time{Time: time},
|
||||
}}
|
||||
return tCtx.Client().CoreV1().Pods(pod.Namespace).UpdateStatus(tCtx, pod, metav1.UpdateOptions{})
|
||||
}
|
||||
|
||||
func markPodSucceeded(tCtx ktesting.TContext, pod *v1.Pod) (*v1.Pod, error) {
|
||||
pod.Status.Phase = v1.PodSucceeded
|
||||
pod.Status.ContainerStatuses = []v1.ContainerStatus{
|
||||
{
|
||||
State: v1.ContainerState{
|
||||
Terminated: &v1.ContainerStateTerminated{FinishedAt: metav1.Now()},
|
||||
},
|
||||
},
|
||||
}
|
||||
return tCtx.Client().CoreV1().Pods(pod.Namespace).UpdateStatus(tCtx, pod, metav1.UpdateOptions{})
|
||||
}
|
||||
|
||||
func deletePod(tCtx ktesting.TContext, pod *v1.Pod) error {
|
||||
err := tCtx.Client().CoreV1().Pods(pod.Namespace).Delete(tCtx, pod.Name, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Postpone deletion for the duration of these two calls
|
||||
pod, err = tCtx.Client().CoreV1().Pods(pod.Namespace).Get(tCtx, pod.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pod.Finalizers = nil
|
||||
_, err = tCtx.Client().CoreV1().Pods(pod.Namespace).Update(tCtx, pod, metav1.UpdateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
func markResponderUpdate(tCtx ktesting.TContext, fieldManager string, eviction *lifecyclev1alpha1.Eviction, responderName string, isCompleted bool) (*lifecyclev1alpha1.Eviction, error) {
|
||||
newResponderStatus := lifecycleapply.ResponderStatus().
|
||||
WithName(responderName).
|
||||
WithHeartbeatTime(metav1.Now()).
|
||||
WithExpectedCompletionTime(metav1.Time{Time: time.Now().Add(time.Hour)}).
|
||||
WithMessage(responderName)
|
||||
if isCompleted {
|
||||
newResponderStatus.WithCompletionTime(metav1.Now())
|
||||
}
|
||||
|
||||
statusApplyUpdate := lifecycleapply.Eviction(eviction.Name, eviction.Namespace).
|
||||
WithStatus(lifecycleapply.EvictionStatus().WithResponders(newResponderStatus))
|
||||
return tCtx.Client().LifecycleV1alpha1().Evictions(eviction.Namespace).ApplyStatus(tCtx, statusApplyUpdate, metav1.ApplyOptions{FieldManager: fieldManager})
|
||||
}
|
||||
|
||||
func getEviction(eviction *lifecyclev1alpha1.Eviction) func(tCtx ktesting.TContext) (*lifecyclev1alpha1.Eviction, error) {
|
||||
return func(tCtx ktesting.TContext) (*lifecyclev1alpha1.Eviction, error) {
|
||||
return tCtx.Client().LifecycleV1alpha1().Evictions(eviction.Namespace).Get(tCtx, eviction.Name, metav1.GetOptions{})
|
||||
}
|
||||
}
|
||||
|
||||
func getEvictionRequest(evictionRequest *lifecyclev1alpha1.EvictionRequest) func(tCtx ktesting.TContext) (*lifecyclev1alpha1.EvictionRequest, error) {
|
||||
return func(tCtx ktesting.TContext) (*lifecyclev1alpha1.EvictionRequest, error) {
|
||||
return tCtx.Client().LifecycleV1alpha1().EvictionRequests(evictionRequest.Namespace).Get(tCtx, evictionRequest.Name, metav1.GetOptions{})
|
||||
}
|
||||
}
|
||||
|
||||
func mkValidEvictionRequest(pod *v1.Pod, tweaks ...func(obj *lifecyclev1alpha1.EvictionRequest)) *lifecyclev1alpha1.EvictionRequest {
|
||||
obj := &lifecyclev1alpha1.EvictionRequest{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "foo-",
|
||||
Namespace: pod.Namespace,
|
||||
},
|
||||
Spec: lifecyclev1alpha1.EvictionRequestSpec{
|
||||
Target: lifecyclev1alpha1.EvictionRequestTarget{
|
||||
Pod: &lifecyclev1alpha1.EvictionRequestPodReference{
|
||||
UID: pod.UID,
|
||||
Name: pod.Name,
|
||||
},
|
||||
},
|
||||
Requester: "foo.example.com/1",
|
||||
Intent: lifecyclev1alpha1.EvictionRequestIntentEviction,
|
||||
},
|
||||
}
|
||||
for _, tweak := range tweaks {
|
||||
tweak(obj)
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
func setERRequester(requester string) func(obj *lifecyclev1alpha1.EvictionRequest) {
|
||||
return func(obj *lifecyclev1alpha1.EvictionRequest) {
|
||||
obj.Spec.Requester = requester
|
||||
}
|
||||
}
|
||||
|
||||
func mkValidEviction(pod *v1.Pod, tweaks ...func(obj *lifecyclev1alpha1.Eviction)) *lifecyclev1alpha1.Eviction {
|
||||
obj := &lifecyclev1alpha1.Eviction{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "evict-",
|
||||
Namespace: pod.Namespace,
|
||||
},
|
||||
Spec: lifecyclev1alpha1.EvictionSpec{
|
||||
Target: lifecyclev1alpha1.EvictionTarget{
|
||||
Pod: &lifecyclev1alpha1.EvictionPodReference{
|
||||
UID: pod.UID,
|
||||
Name: pod.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tweak := range tweaks {
|
||||
tweak(obj)
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
func mkValidPod(namespace string, tweaks ...func(obj *v1.Pod)) *v1.Pod {
|
||||
obj := &v1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-pod",
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: v1.PodSpec{
|
||||
Containers: []v1.Container{{
|
||||
Name: "test-container",
|
||||
Image: "busybox",
|
||||
}},
|
||||
},
|
||||
}
|
||||
for _, tweak := range tweaks {
|
||||
tweak(obj)
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
func addPodUID(uid string) func(obj *v1.Pod) {
|
||||
return func(obj *v1.Pod) {
|
||||
obj.UID = apimachinerytypes.UID(uid)
|
||||
}
|
||||
}
|
||||
|
||||
func addSchedulingGroup(groupName string) func(obj *v1.Pod) {
|
||||
return func(obj *v1.Pod) {
|
||||
obj.Spec.SchedulingGroup = &v1.PodSchedulingGroup{PodGroupName: new(groupName)}
|
||||
}
|
||||
}
|
||||
|
||||
func addResponder(name string, priority int32) func(obj *v1.Pod) {
|
||||
return func(obj *v1.Pod) {
|
||||
obj.Spec.EvictionResponders = append(obj.Spec.EvictionResponders, v1.EvictionResponder{Name: name, Priority: &priority})
|
||||
}
|
||||
}
|
||||
|
||||
func addFinalizer(name string) func(obj *v1.Pod) {
|
||||
return func(obj *v1.Pod) {
|
||||
obj.Finalizers = append(obj.Finalizers, name)
|
||||
}
|
||||
}
|
||||
|
||||
// apiServerSetup sets up apiserver for evictionrequest-controller integration test
|
||||
func apiServerSetup(tCtx ktesting.TContext) ktesting.TContext {
|
||||
// Disable ServiceAccount admission plugin as we don't have serviceaccount controller running.
|
||||
flags := framework.DefaultTestServerFlags()
|
||||
flags = append(flags, "--runtime-config=lifecycle.k8s.io/v1alpha1=true")
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.GenericWorkload) {
|
||||
flags = append(flags, "--runtime-config=scheduling.k8s.io/v1beta1=true")
|
||||
}
|
||||
|
||||
server, err := kubeapiservertesting.StartTestServer(tCtx, nil, flags, framework.SharedEtcd())
|
||||
tCtx.ExpectNoError(err, "start apiserver")
|
||||
tCtx.CleanupCtx(func(tCtx ktesting.TContext) {
|
||||
tCtx.Log("Stopping the apiserver...")
|
||||
server.TearDownFn()
|
||||
})
|
||||
config := restclient.CopyConfig(server.ClientConfig)
|
||||
tCtx = tCtx.WithRESTConfig(config)
|
||||
|
||||
return tCtx
|
||||
}
|
||||
|
||||
// startEvictionRequestController sets up informers and the main controller for the evictionrequest-controller integration test
|
||||
func startEvictionRequestController(tCtx ktesting.TContext) {
|
||||
informerFactory := informers.NewSharedInformerFactory(tCtx.Client(), 0)
|
||||
tCtx.CleanupCtx(func(tCtx ktesting.TContext) {
|
||||
tCtx.Log("stopping informers")
|
||||
informerFactory.Shutdown()
|
||||
})
|
||||
|
||||
responderController, err := evictionrequest.NewController(
|
||||
tCtx,
|
||||
names.EvictionRequestController,
|
||||
informerFactory.Lifecycle().V1alpha1().Evictions(),
|
||||
informerFactory.Lifecycle().V1alpha1().EvictionRequests(),
|
||||
informerFactory.Core().V1().Pods(),
|
||||
tCtx.Client(),
|
||||
)
|
||||
tCtx.ExpectNoError(err, "create evictionrequest-controller")
|
||||
|
||||
informerFactory.StartWithContext(tCtx)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Go(func() {
|
||||
responderController.Run(tCtx, 1) /* one worker to get more readable log output without interleaving */
|
||||
})
|
||||
tCtx.CleanupCtx(func(tCtx ktesting.TContext) {
|
||||
tCtx.Log("waiting for evictionrequest-controller to shut down")
|
||||
wg.Wait()
|
||||
})
|
||||
|
||||
// since this method starts the controller in a separate goroutine
|
||||
// and the tests don't check /readyz there is no way
|
||||
// the tests can tell it is safe to call the server and requests won't be rejected
|
||||
// thus we wait until caches have synced
|
||||
informerFactory.WaitForCacheSyncWithContext(tCtx)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
Copyright 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 evictionrequest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/test/integration/framework"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
framework.EtcdMain(m.Run)
|
||||
}
|
||||
Reference in new issue
Block a user