Merge pull request #137133 from johankj/kep-4563/eviction-request-controller

EvictionRequest controller
This commit is contained in:
kubernetes-prow[bot] authored and GitHub committed 2026-10-22 09:13:46 +00:00
commit 8bc4bbffb0
41 files changed
+6584 -3

No files matched your search

+2
View File
@@ -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"
)
+4
View File
@@ -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
}
+1
View File
@@ -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
+10
View File
@@ -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
}
+19
View File
@@ -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
View File
@@ -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
}
@@ -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
@@ -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
@@ -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"
+10
View File
@@ -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)
}