introduce EvictionRequest and Eviction API

to new lifecycle group

- EvictionRequest is managed by a requester
- Eviction is managed by the evictionrequest-controller
- add storage, strategy, printers and validation
This commit is contained in:
Filip Křepinský
2026-02-17 17:37:51 +01:00
parent 4dd63255fc
commit fc558fb4e9
44 changed files with 7987 additions and 1 deletions

View File

@@ -56,6 +56,7 @@ var apiVersionPriorities = merge(controlplaneapiserver.DefaultGenericAPIServiceP
{Group: "resource.k8s.io", Version: "v1beta2"}: {Group: 16200, Version: 15},
{Group: "resource.k8s.io", Version: "v1beta1"}: {Group: 16200, Version: 9},
{Group: "resource.k8s.io", Version: "v1alpha3"}: {Group: 16200, Version: 1},
{Group: "lifecycle.k8s.io", Version: "v1alpha1"}: {Group: 15700, Version: 1},
// Append a new group to the end of the list if unsure.
// You can use min(existing group)-100 as the initial value for a group.
// Version can be set to 9 (to have space around) for a new group.

View File

@@ -31,6 +31,7 @@ import (
_ "k8s.io/kubernetes/pkg/apis/core/install"
_ "k8s.io/kubernetes/pkg/apis/events/install"
_ "k8s.io/kubernetes/pkg/apis/extensions/install"
_ "k8s.io/kubernetes/pkg/apis/lifecycle/install"
_ "k8s.io/kubernetes/pkg/apis/policy/install"
_ "k8s.io/kubernetes/pkg/apis/rbac/install"
_ "k8s.io/kubernetes/pkg/apis/resource/install"

View File

@@ -93,6 +93,7 @@ extensions/v1beta1 \
events.k8s.io/v1 \
events.k8s.io/v1beta1 \
imagepolicy.k8s.io/v1alpha1 \
lifecycle.k8s.io/v1alpha1 \
networking.k8s.io/v1 \
networking.k8s.io/v1beta1 \
node.k8s.io/v1 \

View File

@@ -33,6 +33,7 @@ import (
_ "k8s.io/kubernetes/pkg/apis/extensions/install"
_ "k8s.io/kubernetes/pkg/apis/flowcontrol/install"
_ "k8s.io/kubernetes/pkg/apis/imagepolicy/install"
_ "k8s.io/kubernetes/pkg/apis/lifecycle/install"
_ "k8s.io/kubernetes/pkg/apis/networking/install"
_ "k8s.io/kubernetes/pkg/apis/node/install"
_ "k8s.io/kubernetes/pkg/apis/policy/install"

21
pkg/apis/lifecycle/doc.go Normal file
View File

@@ -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
// +groupName=lifecycle.k8s.io
package lifecycle

View File

@@ -0,0 +1,38 @@
/*
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 install installs the lifecycle API group, making it available as
// an option to all of the API encoding/decoding machinery.
package install
import (
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/apis/lifecycle"
"k8s.io/kubernetes/pkg/apis/lifecycle/v1alpha1"
)
func init() {
Install(legacyscheme.Scheme)
}
// Install registers the API group and adds types to a scheme
func Install(scheme *runtime.Scheme) {
utilruntime.Must(lifecycle.AddToScheme(scheme))
utilruntime.Must(v1alpha1.AddToScheme(scheme))
utilruntime.Must(scheme.SetVersionPriority(v1alpha1.SchemeGroupVersion))
}

View File

@@ -0,0 +1,56 @@
/*
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 lifecycle
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// GroupName is the group name use in this package
const GroupName = "lifecycle.k8s.io"
// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal}
// Kind takes an unqualified kind and returns a Group qualified GroupKind
func Kind(kind string) schema.GroupKind {
return SchemeGroupVersion.WithKind(kind).GroupKind()
}
// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}
var (
// SchemeBuilder points to a list of functions added to Scheme.
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
// AddToScheme applies all the stored functions to the scheme.
AddToScheme = SchemeBuilder.AddToScheme
)
// Adds the list of known types to the given scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Eviction{},
&EvictionList{},
&EvictionRequest{},
&EvictionRequestList{},
)
return nil
}

590
pkg/apis/lifecycle/types.go Normal file
View File

@@ -0,0 +1,590 @@
/*
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 lifecycle
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apimachinerytypes "k8s.io/apimachinery/pkg/types"
)
const (
// EvictionResponderImperativeEviction is a default responder with a priority of 100 that will evict
// pods using the imperative Eviction API (pods/<name>/eviction subresource) with a backoff.
EvictionResponderImperativeEviction string = "imperative-eviction.k8s.io/evictor"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// EvictionRequest defines a request that should ideally result in a graceful eviction of a
// .spec.target (e.g. termination of a pod).
//
// The evictionrequest-controller observes intents of all EvictionRequests and transforms them into
// Evictions.
// - .spec.requester is set as a label on the Eviction for easier lookup.
// - Each target can have a set of responders assigned to it. Eviction objects are observed by
// these responders, who implement the eviction logic and update the Eviction's status with
// progress.
//
// There is many-to-many relationship between EvictionRequests and Evictions in general.
// And many-to-one if the target is a pod.
//
// If all requesters withdraw their eviction intent for a common target, the eviction will be
// canceled. Deleting an EvictionRequest also counts as a withdrawal.
// Once all EvictionRequest of a target are removed, the corresponding Evictions are eventually
// garbage collected.
type EvictionRequest struct {
metav1.TypeMeta
// metadata is the standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata.
// +optional
metav1.ObjectMeta
// spec defines the eviction request specification.
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
// +required
Spec EvictionRequestSpec
// status represents the most recently observed status of the eviction request.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
// +optional
Status EvictionRequestStatus
}
// EvictionRequestSpec is a specification of an EvictionRequest.
type EvictionRequestSpec struct {
// target contains a reference to an object (e.g. a pod) that should be evicted.
// This field is required and immutable.
// +required
Target EvictionRequestTarget
// requester allows you to identify the entity, that requested the eviction of the target.
//
// It must be a valid domain-prefixed key (such as "acme.io/foo").
// Domain names *.k8s.io and *.kubernetes.io are reserved.
// This field is required and immutable.
// +required
Requester string
// intent specifies the action that should be taken for the specified target.
//
// - Eviction means that the requester is interested in the eviction of the target.
// - Withdrawn means that the requester is no longer interested in the eviction of the target.
// If all requesters' intents are withdrawn for a common target, the eviction will be canceled.
// Cancellation consequences:
// - Inactive responders will never run.
// - Active responders are expected to cancel the eviction.
// - Completed or Interrupted responders should not take any action.
// +required
Intent EvictionRequestIntent
}
// EvictionRequestTarget contains a reference to an object that should be evicted.
type EvictionRequestTarget struct {
// pod references a pod that is subject to eviction/termination.
// Pods that are part of a PodGroup (.spec.schedulingGroup is set) are not supported.
// +optional
Pod *EvictionRequestPodReference
}
// EvictionRequestPodReference contains enough information to locate the referenced pod inside the
// same namespace.
type EvictionRequestPodReference struct {
// name of the target.
// This field is required.
// +required
Name string
// uid of the target.
// It can be found in .metadata.uid of the target and is a lowercase UUID in 8-4-4-4-12 format.
// This field is required.
// +required
UID apimachinerytypes.UID
}
// EvictionRequestIntent specifies a requester intent.
type EvictionRequestIntent string
// These are intents that can be set by each requester.
const (
// EvictionRequestIntentEviction means that the requester is interested in the eviction of the target.
EvictionRequestIntentEviction EvictionRequestIntent = "Eviction"
// EvictionRequestIntentWithdrawn means that the requester is no longer interested in the eviction of the target.
// If all requesters' intents are withdrawn for a common target, the eviction will be canceled.
// Cancellation consequences:
// - Inactive responders will never run.
// - Active responders are expected to cancel the eviction.
// - Completed or Interrupted responders should not take any action.
EvictionRequestIntentWithdrawn EvictionRequestIntent = "Withdrawn"
)
// EvictionRequestStatus represents the last observed status of the eviction request.
type EvictionRequestStatus struct {
// conditions contain information about the eviction request.
//
// EvictionRequest specific conditions are: TargetEvicted or Failed (managed by evictionrequest-controller).
// - Failed means that the eviction request is no longer being processed
// by any eviction responder. This can happen if the request is canceled or if no responder
// managed to evict the target (e.g. terminate or delete a pod).
// - TargetEvicted means that the target has been evicted (e.g. a pod has been terminated or deleted).
//
// These conditions can be reset if the eviction was unsuccessful and a new Eviction intent has
// been submitted.
//
// The maximum length of the conditions list is 100.
// +optional
// +patchMergeKey=type
// +patchStrategy=merge
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition
// observedGeneration is EvictionRequest's .metadata.generation observed by the evictionrequest-controller.
// The observed generation value cannot be negative and can only be incremented.
// The minimum value is 1.
// This field is managed by evictionrequest-controller.
// +optional
ObservedGeneration *int64
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// EvictionRequestList contains a list of EvictionRequests resources.
type EvictionRequestList struct {
metav1.TypeMeta
// metadata is the standard list metadata.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
// +optional
metav1.ListMeta
// items is the list of EvictionRequests.
Items []EvictionRequest
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// Eviction initiates an eviction process, which should ideally result in a graceful eviction of a
// .spec.target (e.g. termination of a pod).
//
// The evictionrequest-controller observes intents of all EvictionRequests and transforms them into
// Evictions. It manages the Eviction lifecycle.
// Requesters are preserved in .status.requesters even after they have withdrawn their request.
// If all requesters withdraw their eviction intent for a common target, the eviction will be
// canceled. Once all EvictionRequest corresponding to this Eviction .spec.target have been
// removed, this Eviction object will eventually be garbage collected.
//
// If the target is a pod, the .status.targetResponders is populated from Pod's
// .spec.evictionResponders.
//
// Responders should observe and communicate through the .status to help with the eviction
// of the target when they see their state == Active in .status.targetResponders. ResponderStatus
// struct should then be periodically updated to indicate the progress or completion of the eviction
// process by each responder in .status.responders. If .status.responders[].heartbeatTime is not
// updated within the heartbeat deadline defined by the Eviction API (currently 20 minutes), the
// eviction is passed over to the next responder with a lower priority.
//
// If there are no other responders and the target is a pod, the last default
// imperative-eviction.k8s.io/evictor responder with a priority of 100 will evict the pod using the
// imperative Eviction API (pods/<name>/eviction subresource).
type Eviction struct {
metav1.TypeMeta
// metadata is the standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata.
// .metadata.name set by the evictionrequest-controller is purely informative and subject to change.
// .spec.target field should be used to identify the target precisesly.
//
// The requester and responder names will be used as label keys and added to the labels of the
// eviction in one of the following formats:
// 1. acme.io/foo: "requester"
// 2. acme.io/foo: "responder"
// 3. acme.io/foo: "requester-responder"
//
// Please see EvictionParticipantRole for available role label values.
// +optional
metav1.ObjectMeta
// spec defines the eviction specification.
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
// +required
Spec EvictionSpec
// status represents the most recently observed status of the eviction.
// Populated by responders and evictionrequest-controller.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
// +optional
Status EvictionStatus
}
// EvictionParticipantRole specifies a role of an eviction participant intent.
type EvictionParticipantRole string
const (
// EvictionParticipantRoleRequester identifies a requester which creates EvictionRequests.
EvictionParticipantRoleRequester EvictionParticipantRole = "requester"
// EvictionParticipantRoleResponder identifies a responder which responds to an Eviction.
EvictionParticipantRoleResponder EvictionParticipantRole = "responder"
// EvictionParticipantRoleRequesterResponder is both a "requester" and a "responder" at the same time.
EvictionParticipantRoleRequesterResponder EvictionParticipantRole = "requester-responder"
)
// EvictionSpec is a specification of an Eviction.
type EvictionSpec struct {
// target contains a reference to an object (e.g. a pod) that should be evicted.
// This field is required and immutable.
// +required
Target EvictionTarget
}
// EvictionTarget contains a reference to an object that should be evicted.
// +union
type EvictionTarget struct {
// pod references a pod that is subject to eviction/termination.
// Pods that are part of a PodGroup (.spec.schedulingGroup is set) are not supported.
// +optional
Pod *EvictionPodReference
}
// EvictionPodReference contains enough information to locate the referenced pod inside the same
// namespace.
type EvictionPodReference struct {
// name of the target.
// This field is required.
// +required
Name string
// uid of the target.
// It can be found in .metadata.uid of the target and is a lowercase UUID in 8-4-4-4-12 format.
// This field is required.
// +required
UID apimachinerytypes.UID
}
// EvictionStatus represents the last observed status of the eviction request.
type EvictionStatus struct {
// conditions contain information about the eviction request.
//
// Eviction specific conditions are: TargetEvicted or Failed (managed by evictionrequest-controller).
// - Failed means that the eviction request is no longer being processed
// by any eviction responder. This can happen if the request is canceled or if no responder
// managed to evict the target (e.g. terminate or delete a pod).
// - TargetEvicted means that the target has been evicted (e.g. a pod has been terminated or deleted).
//
// The maximum length of the conditions list is 100.
// +optional
// +patchMergeKey=type
// +patchStrategy=merge
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition
// observedGeneration is Eviction's .metadata.generation observed by the evictionrequest-controller.
// The observed generation value cannot be negative and can only be incremented.
// The minimum value is 1.
// This field is managed by evictionrequest-controller.
// +optional
ObservedGeneration *int64
// requesters allow you to identify the entities, that requested the eviction of the target.
// If all the requesters withdraw their eviction intent, the eviction will be canceled.
//
// The maximum length of the requesters list is 100.
// If this limit is exceeded, requesters with Withdrawn intent should be dropped first.
// +optional
// +patchMergeKey=name
// +patchStrategy=merge
// +listType=map
// +listMapKey=name
Requesters []Requester
// targetResponders reference responders that should eventually respond to this eviction
// to help with the graceful eviction of a target. These responders are selected sequentially,
// according to their specified priority by setting the Active state to the TargetResponder
// .state field. The maximum number of active responders allowed is 1.
// Eventually each responder can end up in an Interrupted, Canceled or, Completed state.
// Responders should observe these states in order to navigate their lifecycle.
//
// If the target is a pod, the field is populated from Pod's .spec.evictionResponders. Default
// responders may be added to the list according to the target.
//
// Default responders:
// - imperative-eviction.k8s.io/evictor responder with a priority of 100 is added to the list if the
// target is a pod. It will call the imperative Eviction API (pods/<name>/eviction subresource).
// This call may not succeed due to PodDisruptionBudgets, which may block the pod termination.
// It will update the responder message and try again with a backoff.
//
// The maximum length of the responders list is 11.
// The length and keys of the list cannot change once set.
// This field is managed by evictionrequest-controller.
// +optional
// +patchMergeKey=name
// +patchStrategy=merge
// +listType=map
// +listMapKey=name
TargetResponders []TargetResponder
// responders represents the eviction process status of each declared responder.
//
// The responder list should be the same length and have the same .name fields as
// .status.targetResponders. Only responders with .name that have Active state in
// .targetResponders[].state should be updated and can be mutated. First initialization
// of the list is allowed.
//
// Each ResponderStatus is initialized by evictionrequest-controller and then managed by
// the designated responder.
// +optional
// +patchMergeKey=name
// +patchStrategy=merge
// +listType=map
// +listMapKey=name
Responders []ResponderStatus
}
type EvictionConditionType string
// These are built-in conditions of an eviction request.
const (
// EvictionConditionFailed means that the eviction request is no longer being processed
// by any eviction responder. This can happen if the request is canceled or if no responder
// managed to evict the target (e.g. terminate or delete a pod).
EvictionConditionFailed EvictionConditionType = "Failed"
// EvictionConditionTargetEvicted means that the target has been evicted (e.g. a pod has been
// terminated or deleted).
EvictionConditionTargetEvicted EvictionConditionType = "TargetEvicted"
)
type EvictionConditionReason string
// These are built-in condition reasons of an eviction request.
const (
// EvictionConditionReasonAwaitingEviction means that this Eviction works as expected and the target
// is scheduled for an eviction.
// This reason is set for the Failed and TargetEvicted condition.
EvictionConditionReasonAwaitingEviction EvictionConditionReason = "AwaitingEviction"
// EvictionConditionReasonEvictionInvalid means that the Eviction is not accepted because the
// initial configuration is not valid.
// This reason is set for the Failed condition.
EvictionConditionReasonEvictionInvalid EvictionConditionReason = "EvictionInvalid"
// EvictionConditionReasonCanceledDueToNoRequesters means that the Eviction is canceled because there is no
// EvictionRequest with the same target and Eviction intent in .spec.intent.
// This reason is set for the Failed condition.
EvictionConditionReasonCanceledDueToNoRequesters EvictionConditionReason = "CanceledDueToNoRequesters"
// EvictionConditionReasonSucceeded means that the Eviction has successfully evicted the target.
// This reason is set for the Failed condition.
EvictionConditionReasonSucceeded EvictionConditionReason = "Succeeded"
// EvictionConditionReasonNoFurtherResponder means that the Eviction responders failed to evict
// the target and that no further responder is available.
// This reason is set for the Failed condition.
EvictionConditionReasonNoFurtherResponder EvictionConditionReason = "NoFurtherResponder"
// EvictionConditionReasonPodDeleted means that the target pod has been deleted.
// This reason is set for the TargetEvicted condition.
EvictionConditionReasonPodDeleted EvictionConditionReason = "PodDeleted"
// EvictionConditionReasonPodTerminal means that the target pod has reached a terminal state.
// This reason is set for the TargetEvicted condition.
EvictionConditionReasonPodTerminal EvictionConditionReason = "PodTerminal"
// EvictionConditionReasonEvictionFailed means that the eviction of the target was unsuccessful.
// This reason is set for the TargetEvicted condition.
EvictionConditionReasonEvictionFailed EvictionConditionReason = "EvictionFailed"
)
// Requester allows you to identify the entity, that requested the eviction of the target.
// +structType=atomic
type Requester struct {
// name allows you to identify the entity, that requested the eviction of the target.
//
// It must be a valid domain-prefixed key (such as "acme.io/foo").
// This field must be unique for each requester.
// This field is required.
// +required
Name string
// intent specifies the action that should be taken for the specified target.
//
// - Eviction means that the requester is interested in the eviction of the target.
// - Withdrawn means that the requester is no longer interested in the eviction of the target.
// If all requesters' intents are withdrawn, the eviction will be canceled.
// Cancellation consequences:
// - Inactive responders will never run.
// - Active responders are expected to cancel the eviction.
// - Completed or Interrupted responders should not take any action.
// +required
Intent RequesterIntent
}
// RequesterIntent specifies a requester intent.
type RequesterIntent string
// These are intents that can be set by each requester.
const (
// RequesterIntentEviction means that the requester is interested in the eviction of the target.
RequesterIntentEviction RequesterIntent = "Eviction"
// RequesterIntentWithdrawn means that the requester is no longer interested in the eviction of the target.
// If all requesters' intents are withdrawn, the eviction will be canceled.
// Cancellation consequences:
// - Inactive responders will never run.
// - Active responders are expected to cancel the eviction.
// - Completed or Interrupted responders should not take any action.
RequesterIntentWithdrawn RequesterIntent = "Withdrawn"
)
// TargetResponder allows you to specify the responder reacting to the Eviction.
// Responders should observe and communicate through the Eviction API (see .state) to help
// with the graceful eviction of a target (e.g. termination of a pod).
// +structType=atomic
type TargetResponder struct {
// name allows you to identify the responder reacting to the Eviction.
//
// It must be a valid domain-prefixed key (such as "acme.io/foo").
// This field must be unique for each responder.
// This field is required.
// +required
Name string
// priority for this responder. Higher priorities are selected first by the evictionrequest-controller.
// 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.
//
// The responder that is the managing controller of the pod should set the value of
// this field to 10000 to allow both for preemption or fallback registration by other
// responders.
//
// The minimum value is 0 and the maximum value is 100000.
// The interval 0-999 is reserved for responders with *.k8s.io suffix.
// This field is required and immutable.
// +required
Priority *int32
// state specifies a state that is assigned by the evictionrequest-controller. Responders should observe
// this state in order to navigate their lifecycle.
// - Inactive means that the responder should not yet process this eviction request.
// - Active means that the responder is either running or expected to start soon.
// Also, startTime has been set in the ResponderStatus by the evictionrequest-controller.
//
// An active responder should currently interact with the eviction process by updating
// .status.responders, where .name is the active responder name. ResponderStatus fields
// should be periodically updated to indicate the progress or completion of the eviction process.
// If .status.responders[].heartbeatTime field is not updated within the heartbeat deadline defined
// by the Eviction API (currently 20 minutes), the eviction is passed over to the next responder
// with a lower priority. Only one responder can be active at a time.
// - Interrupted means that the responder has failed to start or failed to update
// heartbeatTime in ResponderStatus in a timely manner.
// - Canceled means that the responder has been canceled. In other words, there is no
// EvictionRequest with the same target and Eviction intent in .spec.intent.
// - Completed means that the responder has successfully completed and set completionTime
// in ResponderStatus.
//
// Please refer to the ResponderStatus in .status.responders for more details on each responder.
// +required
State ResponderStateType
}
// ResponderStateType specifies a state that is assigned by the evictionrequest-controller.
type ResponderStateType string
const (
// ResponderStateInactive means that the responder should not yet process this eviction request.
ResponderStateInactive ResponderStateType = "Inactive"
// ResponderStateActive means that the responder is either running or expected to start soon.
// Also, startTime has been set in the ResponderStatus by the evictionrequest-controller.
//
// An active responder should currently interact with the eviction process by updating
// .status.responders, where .name is the active responder name. ResponderStatus fields
// should be periodically updated to indicate the progress or completion of the eviction process.
// If .status.responders[].heartbeatTime field is not updated within the heartbeat deadline defined
// by the Eviction API (currently 20 minutes), the eviction is passed over to the next responder
// with a lower priority. Only one responder can be active at a time.
ResponderStateActive ResponderStateType = "Active"
// ResponderStateInterrupted means that the responder has failed to start or failed to update
// heartbeatTime in ResponderStatus in a timely manner.
ResponderStateInterrupted ResponderStateType = "Interrupted"
// ResponderStateCanceled means that the responder has been canceled. In other words, there
// is no EvictionRequest with the same target and Eviction intent in .spec.intent.
ResponderStateCanceled ResponderStateType = "Canceled"
// ResponderStateCompleted means that the responder has successfully completed and set completionTime
// in ResponderStatus.
ResponderStateCompleted ResponderStateType = "Completed"
)
// ResponderStatus represents the last observed status of the eviction process of the responder.
// It should be only updated by the designated responder whose name is .name field.
// +structType=granular
type ResponderStatus struct {
// name allows you to identify the responder reacting to the Eviction.
//
// It must be a valid domain-prefixed key (such as "acme.io/foo").
// This field is initialized by Kubernetes and must be unique for each responder.
// This field is required.
// +required
Name string
// startTime tracks the time at which this responder was designated as active and should start
// processing the eviction request.
// It should reflect the present time when set.
// This field is initialized by Kubernetes when this responder becomes active.
// This field becomes immutable once set.
// +optional
StartTime *metav1.Time
// heartbeatTime is the last time at which the eviction process was reported to be in progress
// by the responder.
// It should reflect the present time when set.
// Responders should avoid heartbeats more frequent than 20 seconds to avoid overloading the
// control-plane.
// +optional
HeartbeatTime *metav1.Time
// expectedCompletionTime is the time at which the eviction process step is expected to end for the
// responder.
// The time cannot be set to the past.
// May be omitted if no estimate can be made.
// +optional
ExpectedCompletionTime *metav1.Time
// completionTime tracks the time at which the Responder stopped processing the eviction request.
// Completion means that the responders has either fully or partially completed the
// eviction process, which may have resulted in target eviction (e.g. pod termination).
// It should reflect the present time when set.
// This field becomes immutable once set.
// +optional
CompletionTime *metav1.Time
// message provides human-readable details about the state of the responder and the eviction
// process.
// Maximum length is 4000 characters.
// +optional
Message *string
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// EvictionList contains a list of Eviction resources.
type EvictionList struct {
metav1.TypeMeta
// metadata is the standard list metadata.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
// +optional
metav1.ListMeta
// items is the list of Evictions.
Items []Eviction
}

View File

@@ -0,0 +1,26 @@
/*
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:conversion-gen=k8s.io/kubernetes/pkg/apis/lifecycle
// +k8s:conversion-gen-external-types=k8s.io/api/lifecycle/v1alpha1
// +k8s:defaulter-gen=TypeMeta
// +k8s:defaulter-gen-input=k8s.io/api/lifecycle/v1alpha1
// +k8s:validation-gen=TypesWithField=TypeMeta
// +k8s:validation-gen-input=k8s.io/api/lifecycle/v1alpha1
// +groupName=lifecycle.k8s.io
package v1alpha1

View File

@@ -0,0 +1,46 @@
/*
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 (
lifecyclev1alpha1 "k8s.io/api/lifecycle/v1alpha1"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// GroupName is the group name use in this package
const GroupName = "lifecycle.k8s.io"
// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}
var (
localSchemeBuilder = &lifecyclev1alpha1.SchemeBuilder
// AddToScheme is a common registration function for mapping packaged scoped group & version keys to a scheme
AddToScheme = localSchemeBuilder.AddToScheme
)
func init() {
// We only register manually written functions here. The registration of the
// generated functions takes place in the generated files. The separation
// makes the code compile even when the generated files are missing.
localSchemeBuilder.Register(RegisterDefaults)
}

View File

@@ -0,0 +1,37 @@
/*
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 (
"context"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/util/validation/field"
apiscorev1 "k8s.io/kubernetes/pkg/apis/core/v1"
)
// ValidateCustom_EvictionRequestSpec_Requester is wired into the generated declarative validation by
// +k8s:customValidation on corev1.EvictionRequestSpec.Requester. It enforces that the
// k8s-prefixed-label-key value, is not prefixed with a k8s.io or kubernetes.io domain.
func ValidateCustom_EvictionRequestSpec_Requester(ctx context.Context, op operation.Operation, fldPath *field.Path, value, oldValue *string) field.ErrorList {
if value != nil {
// Once, we have k8s requesters, we need to check a set of supported requesters that can bypass this validation.
return apiscorev1.ValidateForbiddenReservedDomainSuffixes(fldPath, *value, []string{".k8s.io", ".kubernetes.io"})
}
return nil
}

View File

@@ -0,0 +1,616 @@
/*
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 validation
import (
"context"
"fmt"
"slices"
"strings"
"time"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/operation"
"k8s.io/apimachinery/pkg/api/validate"
"k8s.io/apimachinery/pkg/api/validation"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
validation2 "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation/field"
apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
"k8s.io/kubernetes/pkg/apis/lifecycle"
"k8s.io/utils/clock"
"k8s.io/utils/ptr"
)
const defaultEvictionResponderCount = 1 // EvictionResponderImperativeEviction
// more than .pod.spec.evictionResponders to account for default responders
const maxEvictionResponders = apivalidation.MaxPodEvictionResponders + defaultEvictionResponderCount
// ValidateEvictionRequest validates an EvictionRequest.
func ValidateEvictionRequest(evictionRequest *lifecycle.EvictionRequest) field.ErrorList {
validateLongName := func(fldPath *field.Path, name string) field.ErrorList {
return validate.LongName(context.Background(), operation.Operation{}, fldPath, &name, nil).MarkCoveredByDeclarative()
}
allErrs := apivalidation.ValidateObjectMetaWithOpts(&evictionRequest.ObjectMeta, true, validateLongName, field.NewPath("metadata"))
// Spec validation is covered by DV (Validate_EvictionRequestSpec)
return allErrs
}
// ValidateEvictionRequestUpdate validates an EvictionRequest.
func ValidateEvictionRequestUpdate(evictionRequest, oldEvictionRequest *lifecycle.EvictionRequest) field.ErrorList {
allErrs := apivalidation.ValidateObjectMetaUpdate(&evictionRequest.ObjectMeta, &oldEvictionRequest.ObjectMeta, field.NewPath("metadata"))
// Spec validation is covered by DV (Validate_EvictionRequestSpec)
return allErrs
}
// ValidateEvictionRequestStatusUpdate validates an EvictionRequest Status update.
func ValidateEvictionRequestStatusUpdate(evictionRequest, oldEvictionRequest *lifecycle.EvictionRequest) field.ErrorList {
allErrs := apivalidation.ValidateObjectMetaUpdate(&evictionRequest.ObjectMeta, &oldEvictionRequest.ObjectMeta, field.NewPath("metadata"))
allErrs = append(allErrs, ValidateEvictionRequestStatus(&evictionRequest.Status, &oldEvictionRequest.Status, field.NewPath("status"))...)
return allErrs
}
// ValidateEvictionRequestStatus validates an EvictionRequest Status.
func ValidateEvictionRequestStatus(status, oldStatus *lifecycle.EvictionRequestStatus, fldPath *field.Path) field.ErrorList {
var allErrs field.ErrorList
// observedGeneration covered by DV
// conditions
conditionsPath := fldPath.Child("conditions")
// conditions validation covered by DV
allErrs = append(allErrs, validation2.ValidateConditions(status.Conditions, conditionsPath)...)
return allErrs
}
// ValidateEviction validates an Eviction.
func ValidateEviction(eviction *lifecycle.Eviction) field.ErrorList {
validateLongName := func(fldPath *field.Path, name string) field.ErrorList {
return validate.LongName(context.Background(), operation.Operation{}, fldPath, &name, nil).MarkCoveredByDeclarative()
}
allErrs := apivalidation.ValidateObjectMetaWithOpts(&eviction.ObjectMeta, true, validateLongName, field.NewPath("metadata"))
// Spec validation is covered by DV (Validate_EvictionSpec)
return allErrs
}
// ValidateEvictionUpdate validates an Eviction.
func ValidateEvictionUpdate(eviction, oldEviction *lifecycle.Eviction) field.ErrorList {
allErrs := apivalidation.ValidateObjectMetaUpdate(&eviction.ObjectMeta, &oldEviction.ObjectMeta, field.NewPath("metadata"))
// Spec validation is covered by DV (Validate_EvictionSpec)
return allErrs
}
type EvictionStatusValidationOptions struct {
Clock clock.PassiveClock
}
// ValidateEvictionStatusUpdate validates an Eviction Status update.
func ValidateEvictionStatusUpdate(eviction, oldEviction *lifecycle.Eviction, opts EvictionStatusValidationOptions) field.ErrorList {
allErrs := apivalidation.ValidateObjectMetaUpdate(&eviction.ObjectMeta, &oldEviction.ObjectMeta, field.NewPath("metadata"))
allErrs = append(allErrs, ValidateEvictionStatus(&eviction.Status, &oldEviction.Status, field.NewPath("status"), opts)...)
return allErrs
}
// ValidateEvictionStatus validates an Eviction Status.
func ValidateEvictionStatus(status, oldStatus *lifecycle.EvictionStatus, fldPath *field.Path, opts EvictionStatusValidationOptions) field.ErrorList {
var allErrs field.ErrorList
// observedGeneration covered by DV
// conditions
conditionsPath := fldPath.Child("conditions")
// conditions validation covered by DV
allErrs = append(allErrs, validation2.ValidateConditions(status.Conditions, conditionsPath)...)
isEvicted := meta.IsStatusConditionTrue(status.Conditions, string(lifecycle.EvictionConditionTargetEvicted))
isFailed := meta.IsStatusConditionTrue(status.Conditions, string(lifecycle.EvictionConditionFailed))
for _, oldCondition := range oldStatus.Conditions {
if oldCondition.Type == string(lifecycle.EvictionConditionTargetEvicted) || oldCondition.Type == string(lifecycle.EvictionConditionFailed) {
newCondition := meta.FindStatusCondition(status.Conditions, oldCondition.Type)
if oldCondition.Status == v1.ConditionTrue && (newCondition == nil || newCondition.Status == v1.ConditionFalse || oldCondition.Reason != newCondition.Reason) {
allErrs = append(allErrs, field.Invalid(conditionsPath, status.Conditions, fmt.Sprintf("%s condition status cannot be reverted", oldCondition.Type)))
if oldCondition.Type == string(lifecycle.EvictionConditionTargetEvicted) {
isEvicted = true // do not use invalid condition state for next validations
}
if oldCondition.Type == string(lifecycle.EvictionConditionFailed) {
isFailed = true // do not use invalid condition state for next validations
}
}
}
}
// requesters covered by DV (Validate_EvictionStatus)
// targetResponders, and responders
allErrs = append(allErrs, ValidateAllEvictionStatusResponderFields(status, oldStatus, fldPath, EvictionStatusRespondersValidationOptions{
Clock: opts.Clock,
IsEvicted: isEvicted,
IsFailed: isFailed,
})...)
return allErrs
}
type EvictionStatusRespondersValidationOptions struct {
Clock clock.PassiveClock
IsEvicted bool
IsFailed bool
}
// ValidateAllEvictionStatusResponderFields validates .status.targetResponders and .status.responders
//
// Multiple actors are expected to update the status (eviction-controller, responders and requesters updating conditions).
// We have to emulate the eviction-controller behavior, to prevent invalid updates by other misbehaving/malicious actors.
// 1. .status.targetResponders and .status.responders should be set first.
// 2. Responders' state transitions from Inactive to Active, and from Active to Interrupted, Canceled or, Complete
// gradually in the order of the responder priorities This is not reversible.
// 3. Responders' state transition from Active to Interrupted, Canceled or, Complete when
// .status.responders[].completionTime is set or when heartbeat is exceeded. This is not reversible.
// 4. .status.responders items cannot be removed once set. Only active responders can mutate it.
// 5. The controller can mark an Eviction as Evicted or Canceled via conditions. We then have to allow removal
// from activeResponders and prevent any new ones.
func ValidateAllEvictionStatusResponderFields(status, oldStatus *lifecycle.EvictionStatus, fldPath *field.Path, opts EvictionStatusRespondersValidationOptions) field.ErrorList {
var allErrs field.ErrorList
heartbeatDeadline := time.Minute * 20
allowedTimeSkew := time.Second * 30
allowedMaxExpectedCompletionTime := time.Hour * 24 * 365 * 10 // 10 years
statusResponders := append(make([]lifecycle.ResponderStatus, 0, len(status.Responders)), status.Responders...)
// targetResponders
allErrs = append(allErrs, ValidateEvictionTargetResponders(status.TargetResponders, oldStatus.TargetResponders, fldPath.Child("targetResponders"), ValidateEvictionTargetRespondersOptions{
Clock: opts.Clock,
IsFailed: opts.IsFailed,
IsEvicted: opts.IsEvicted,
StatusResponders: statusResponders,
HeartbeatDeadline: heartbeatDeadline,
AllowedTimeSkew: allowedTimeSkew,
})...)
// responders
allErrs = append(allErrs, ValidateEvictionStatusResponders(status.Responders, oldStatus.Responders, fldPath.Child("responders"), ValidateEvictionStatusRespondersOptions{
Clock: opts.Clock,
TargetResponders: status.TargetResponders,
AllowedTimeSkew: allowedTimeSkew,
MaxExpectedCompletionTime: allowedMaxExpectedCompletionTime,
})...)
return allErrs
}
type ValidateEvictionTargetRespondersOptions struct {
Clock clock.PassiveClock
IsEvicted bool
IsFailed bool
StatusResponders []lifecycle.ResponderStatus
HeartbeatDeadline time.Duration
AllowedTimeSkew time.Duration
}
func ValidateEvictionTargetResponders(targetResponders, oldTargetResponders []lifecycle.TargetResponder, fldPath *field.Path, opts ValidateEvictionTargetRespondersOptions) field.ErrorList {
var allErrs field.ErrorList
if len(oldTargetResponders) != 0 {
if len(targetResponders) != len(oldTargetResponders) {
return append(allErrs, field.Invalid(fldPath, targetResponders, "must preserve the same length and the same keys"))
}
targetRespondersNames := sets.New[string]()
for _, responder := range targetResponders {
targetRespondersNames.Insert(responder.Name)
}
for _, oldTargetResponder := range oldTargetResponders {
if !targetRespondersNames.Has(oldTargetResponder.Name) {
return append(allErrs, field.Invalid(fldPath, targetResponders, "must preserve the same keys"))
}
}
}
if len(targetResponders) == 0 {
// not initialized yet
return allErrs
}
if len(targetResponders) > maxEvictionResponders {
// covered by declarative validation; exit early
return allErrs
}
// simulate declarative error for early exit
uniqueErrors := validate.ValSliceUnique(context.TODO(), operation.Operation{}, fldPath, targetResponders, nil,
func(a *lifecycle.TargetResponder, b *lifecycle.TargetResponder) bool { return a.Name == b.Name })
if len(uniqueErrors) > 0 {
// does not make sense to check for state transition with duplicates
return allErrs
}
statusResponders := map[string]*lifecycle.ResponderStatus{}
for _, responder := range opts.StatusResponders {
statusResponders[responder.Name] = &responder
}
targetRespondersOriginalIdx := map[string]int{}
for i, responder := range targetResponders {
targetRespondersOriginalIdx[responder.Name] = i
}
// Sort responders first, then use ordered progression to simplify the processing.
// This means lower indexes are Activated first, and the last index is processed last.
sortedOldTargetResponders := append([]lifecycle.TargetResponder{}, oldTargetResponders...)
SortTargetResponders(sortedOldTargetResponders)
sortedTargetResponders := append([]lifecycle.TargetResponder{}, targetResponders...)
SortTargetResponders(sortedTargetResponders)
lastActiveIdx := -1
for i, responder := range sortedOldTargetResponders {
if responder.State == lifecycle.ResponderStateActive {
lastActiveIdx = i
break
}
}
hasNeverBeenActive := len(sortedOldTargetResponders) == 0 // we should Activate during the first sync
hasFoundLastActive := lastActiveIdx != -1
activeChanged := hasFoundLastActive && sortedTargetResponders[lastActiveIdx].State != lifecycle.ResponderStateActive
isFinal := opts.IsEvicted || opts.IsFailed
lastOldResponderState := lifecycle.ResponderStateInactive
if len(sortedOldTargetResponders) > 0 {
lastOldResponderState = sortedOldTargetResponders[len(sortedOldTargetResponders)-1].State
}
isFinalOrAllProcessed := isFinal || lastOldResponderState == lifecycle.ResponderStateCompleted ||
lastOldResponderState == lifecycle.ResponderStateCanceled ||
lastOldResponderState == lifecycle.ResponderStateInterrupted
for i, responder := range sortedTargetResponders {
expectedStates := sets.New[lifecycle.ResponderStateType]()
expectedStatesReason := ""
switch {
//
case i < lastActiveIdx ||
(len(sortedOldTargetResponders) > 0 && !hasFoundLastActive && isFinalOrAllProcessed):
// Processed responders must preserve their state.
expectedStates.Insert(sortedOldTargetResponders[i].State)
expectedStatesReason = "final state is immutable"
case i == lastActiveIdx && activeChanged:
// If Active responder changes, it must have a final state.
expectedStates.Insert(lifecycle.ResponderStateInterrupted,
lifecycle.ResponderStateCanceled,
lifecycle.ResponderStateCompleted)
expectedStatesReason = "this responder must reach a final state"
case i == lastActiveIdx:
// Unchanged Active responders can stay Active.
expectedStates.Insert(lifecycle.ResponderStateActive)
expectedStatesReason = "the eviction request stays active"
if isFinal {
// Last Responder must finish before setting the final condition.
expectedStates.Clear().Insert(lifecycle.ResponderStateInterrupted,
lifecycle.ResponderStateCanceled,
lifecycle.ResponderStateCompleted)
expectedStatesReason = "the eviction request has finished processing"
}
case i == 0 && hasNeverBeenActive,
i == lastActiveIdx+1 && activeChanged:
// Next responder must move to an Active state, when the old one is final.
expectedStates.Insert(lifecycle.ResponderStateActive)
expectedStatesReason = "this responder is next in line"
if isFinal {
// Do not active next responder if we have finished.
expectedStates.Clear().Insert(lifecycle.ResponderStateInactive)
expectedStatesReason = "the eviction request has finished processing"
}
default:
expectedStates.Insert(lifecycle.ResponderStateInactive)
expectedStatesReason = "the previously active has not finished processing"
}
allErrs = append(allErrs, ValidateTargetResponder(responder, fldPath.Index(targetRespondersOriginalIdx[responder.Name]), ValidateEvictionTargetResponderOptions{
Clock: opts.Clock,
IsEvicted: opts.IsEvicted,
IsFailed: opts.IsFailed,
responderStatus: statusResponders[responder.Name],
expectedStates: expectedStates,
expectedStatesReason: expectedStatesReason,
HeartbeatDeadline: opts.HeartbeatDeadline,
AllowedTimeSkew: opts.AllowedTimeSkew,
})...)
}
return allErrs
}
type ValidateEvictionTargetResponderOptions struct {
Clock clock.PassiveClock
IsEvicted bool
IsFailed bool
responderStatus *lifecycle.ResponderStatus
expectedStates sets.Set[lifecycle.ResponderStateType]
expectedStatesReason string
HeartbeatDeadline time.Duration
AllowedTimeSkew time.Duration
}
func ValidateTargetResponder(evictionResponder lifecycle.TargetResponder, fldPath *field.Path, opts ValidateEvictionTargetResponderOptions) field.ErrorList {
var allErrs field.ErrorList
// name covered by DV
statusResponderPath := field.NewPath("status", "responders")
if opts.responderStatus == nil {
msg := fmt.Sprintf("%q has to be tracked in %s first", evictionResponder.Name, statusResponderPath)
return append(allErrs, field.Invalid(fldPath, evictionResponder, msg))
}
// priority
const reservedKubernetesRespondersPriority = 1000
priorityAssignedToK8sResponders := map[string]int32{
lifecycle.EvictionResponderImperativeEviction: 100,
}
priorityPath := fldPath.Child("priority")
// validate.Maximum covered by DV
// validate.Minimum covered by DV
if evictionResponder.Priority != nil && *evictionResponder.Priority >= 0 {
if expectedPriority, ok := priorityAssignedToK8sResponders[evictionResponder.Name]; ok {
if *evictionResponder.Priority != expectedPriority {
allErrs = append(allErrs, field.Invalid(priorityPath, *evictionResponder.Priority, fmt.Sprintf("core k8s responder is expected to have priority: %d", expectedPriority)))
}
} else if *evictionResponder.Priority < reservedKubernetesRespondersPriority {
// Each core responder should have priority assigned - see priorityAssignedToK8sResponders
allErrs = append(allErrs, field.Invalid(priorityPath, *evictionResponder.Priority, "priorities 0-999 are reserved for responders with *.k8s.io suffix"))
}
}
// state
statePath := fldPath.Child("state")
// validate.RequiredValue covered by DV
// validate.Enum covered by DV
// check that the state transition is allowed
if !opts.expectedStates.Has(evictionResponder.State) {
var expectedValues []string
for _, ev := range sets.List(opts.expectedStates) {
expectedValues = append(expectedValues, string(ev))
}
msg := fmt.Sprintf("must be one of: %s", strings.Join(expectedValues, ", "))
if len(opts.expectedStatesReason) > 0 {
msg = fmt.Sprintf("%s, because %s", msg, opts.expectedStatesReason)
}
allErrs = append(allErrs, field.Invalid(statePath, evictionResponder.State, msg))
// short circuit as the validations below depend on a correct state
return allErrs
}
// must be present in status responders
// check the heartbeat deadline if the responder is not active anymore (.status.responders)
// we can skip the check if the Eviction is final (failed or evicted).
// StartTime presence is validated in ValidateEvictionStatusResponder
if evictionResponder.State != lifecycle.ResponderStateActive && opts.responderStatus.StartTime != nil &&
opts.responderStatus.CompletionTime == nil && !opts.IsEvicted && !opts.IsFailed {
heartbeat := opts.responderStatus.StartTime
if opts.responderStatus.HeartbeatTime != nil {
heartbeat = opts.responderStatus.HeartbeatTime
}
if opts.Clock.Now().Before(heartbeat.Add(opts.HeartbeatDeadline).Add(-opts.AllowedTimeSkew)) {
msg := fmt.Sprintf("must stay Active because the responder is in progress and it should report %s or %s", statusResponderPath.Child("heartbeatTime"), statusResponderPath.Child("completionTime"))
allErrs = append(allErrs, field.Forbidden(statePath, msg))
}
}
if evictionResponder.State == lifecycle.ResponderStateCompleted && opts.responderStatus.CompletionTime == nil {
msg := fmt.Sprintf("cannot become Completed because the responder didn't report %s", statusResponderPath.Child("completionTime"))
allErrs = append(allErrs, field.Invalid(statePath, evictionResponder.State, msg))
}
return allErrs
}
type ValidateEvictionStatusRespondersOptions struct {
Clock clock.PassiveClock
TargetResponders []lifecycle.TargetResponder
AllowedTimeSkew time.Duration
MaxExpectedCompletionTime time.Duration
}
func ValidateEvictionStatusResponders(statusResponders, oldStatusResponders []lifecycle.ResponderStatus, fldPath *field.Path, opts ValidateEvictionStatusRespondersOptions) field.ErrorList {
var allErrs field.ErrorList
if len(opts.TargetResponders) != len(statusResponders) {
return append(allErrs, field.Invalid(fldPath, statusResponders, "must be the same length as status.targetResponders and contain the same keys"))
}
statusRespondersNames := sets.New[string]()
for _, responder := range statusResponders {
statusRespondersNames.Insert(responder.Name)
}
targetResponders := map[string]*lifecycle.TargetResponder{}
for i, targetResponder := range opts.TargetResponders {
targetResponders[targetResponder.Name] = &opts.TargetResponders[i]
if !statusRespondersNames.Has(targetResponder.Name) {
return append(allErrs, field.Invalid(fldPath, statusResponders, "must contain the same keys as status.targetResponders"))
}
}
if len(statusResponders) == 0 {
// statusResponders and TargetResponders are not initialized yet
return allErrs
}
if len(statusResponders) > maxEvictionResponders {
// TooMany is handled by declarative validation - detect early return
return allErrs
}
// validate.ValSliceUnique covered by DV - simulate an error
uniqueErrors := validate.ValSliceUnique(context.TODO(), operation.Operation{}, fldPath, statusResponders, nil,
func(a *lifecycle.ResponderStatus, b *lifecycle.ResponderStatus) bool { return a.Name == b.Name })
if len(uniqueErrors) > 0 {
// does not make sense to check each responder status with duplicates as we depend on the target responder and an oldStatusResponder
return allErrs
}
oldStatusRespondersMap := map[string]*lifecycle.ResponderStatus{}
for i := range oldStatusResponders {
oldStatusRespondersMap[oldStatusResponders[i].Name] = &oldStatusResponders[i] // +k8s:verify-mutation:reason=clone
}
for i, responder := range statusResponders {
targetResponder := targetResponders[responder.Name] // key presence checked above
responderPath := fldPath.Index(i)
allErrs = append(allErrs, ValidateEvictionStatusResponder(&responder, oldStatusRespondersMap[responder.Name], responderPath,
EvictionStatusResponderValidationOptions{
Clock: opts.Clock,
responderState: targetResponder.State,
AllowedTimeSkew: opts.AllowedTimeSkew,
MaxExpectedCompletionTime: opts.MaxExpectedCompletionTime,
},
)...)
}
return allErrs
}
type EvictionStatusResponderValidationOptions struct {
responderState lifecycle.ResponderStateType
AllowedTimeSkew time.Duration
MaxExpectedCompletionTime time.Duration
Clock clock.PassiveClock
}
func ValidateEvictionStatusResponder(status, oldStatus *lifecycle.ResponderStatus, fldPath *field.Path, opts EvictionStatusResponderValidationOptions) field.ErrorList {
var allErrs field.ErrorList
oldDefaultedStatus := lifecycle.ResponderStatus{}
if oldStatus != nil {
oldDefaultedStatus = *oldStatus // +k8s:verify-mutation:reason=clone
}
if oldStatus != nil && !validate.SemanticDeepEqual(status, oldStatus) && opts.responderState != lifecycle.ResponderStateActive {
// immutable; changes to the ResponderStatus are only allowed by the active responder or during initialization
return append(allErrs, field.Invalid(fldPath, status, validation.FieldImmutableErrorMsg).WithOrigin("immutable"))
}
// name covered by DV
// The existence of targetResponder with the same name is done in ValidateEvictionStatusResponders
// startTime
startTimePath := fldPath.Child("startTime")
// immutable once set
if oldDefaultedStatus.StartTime != nil && !oldDefaultedStatus.StartTime.Equal(status.StartTime) {
// validate.NoUnset and validate.NoModify checks covered by DV
} else if status.StartTime == nil && opts.responderState == lifecycle.ResponderStateActive {
allErrs = append(allErrs, field.Required(startTimePath, "is required for an active responder"))
} else if status.StartTime != nil && !oldDefaultedStatus.StartTime.Equal(status.StartTime) && !timeNear(status.StartTime.Time, opts.Clock.Now(), opts.AllowedTimeSkew) {
allErrs = append(allErrs, field.Invalid(startTimePath, status.StartTime, "must be set to the present time"))
}
// heartbeatTime
heartbeatTimePath := fldPath.Child("heartbeatTime")
if oldDefaultedStatus.HeartbeatTime != nil && status.HeartbeatTime == nil {
allErrs = append(allErrs, field.Required(heartbeatTimePath, "is required once set"))
}
if !oldDefaultedStatus.HeartbeatTime.Equal(status.HeartbeatTime) && status.HeartbeatTime != nil {
if status.StartTime == nil {
allErrs = append(allErrs, field.Invalid(heartbeatTimePath, status.HeartbeatTime, fmt.Sprintf("cannot be set before %s is set", startTimePath.String())))
} else if status.HeartbeatTime.Before(oldDefaultedStatus.HeartbeatTime) {
// this could still happen since we allow for the skew
allErrs = append(allErrs, field.Invalid(heartbeatTimePath, status.HeartbeatTime, "cannot be decreased"))
} else if status.HeartbeatTime.Before(status.StartTime) {
// this could still happen since we allow for the skew
allErrs = append(allErrs, field.Invalid(heartbeatTimePath, status.HeartbeatTime, fmt.Sprintf("must occur after %s", startTimePath.String())))
} else if !timeNear(status.HeartbeatTime.Time, opts.Clock.Now(), opts.AllowedTimeSkew) {
allErrs = append(allErrs, field.Invalid(heartbeatTimePath, status.HeartbeatTime, "must be set to the present time"))
}
}
// expectedCompletionTime
expectedCompletionTimePath := fldPath.Child("expectedCompletionTime")
if !oldDefaultedStatus.ExpectedCompletionTime.Equal(status.ExpectedCompletionTime) && status.ExpectedCompletionTime != nil {
if status.StartTime == nil {
allErrs = append(allErrs, field.Invalid(expectedCompletionTimePath, status.ExpectedCompletionTime, fmt.Sprintf("cannot be set before %s is set", startTimePath.String())))
} else if status.ExpectedCompletionTime.Before(status.StartTime) {
// this could still happen since we allow for the skew
allErrs = append(allErrs, field.Invalid(expectedCompletionTimePath, status.ExpectedCompletionTime, fmt.Sprintf("must occur after %s", startTimePath.String())))
} else if status.ExpectedCompletionTime.Time.Before(opts.Clock.Now().Add(-opts.AllowedTimeSkew)) {
allErrs = append(allErrs, field.Invalid(expectedCompletionTimePath, status.ExpectedCompletionTime, "cannot be set to the past time"))
} else if status.ExpectedCompletionTime.Time.After(opts.Clock.Now().Add(opts.MaxExpectedCompletionTime)) {
allErrs = append(allErrs, field.Invalid(expectedCompletionTimePath, status.ExpectedCompletionTime, "must complete within 10 years")) // sanity check
}
}
// completionTime
completionTimePath := fldPath.Child("completionTime")
// immutable once set
if oldDefaultedStatus.CompletionTime != nil && !oldDefaultedStatus.CompletionTime.Equal(status.CompletionTime) {
// validate.NoUnset and validate.NoModify checks covered by DV
} else if status.CompletionTime != nil {
if status.StartTime == nil {
allErrs = append(allErrs, field.Invalid(completionTimePath, status.CompletionTime, fmt.Sprintf("cannot be set before %s is set", startTimePath.String())))
} else if status.CompletionTime.Before(status.StartTime) {
allErrs = append(allErrs, field.Invalid(completionTimePath, status.CompletionTime, fmt.Sprintf("must occur after %s", startTimePath.String())))
} else if !oldDefaultedStatus.CompletionTime.Equal(status.CompletionTime) && !timeNear(status.CompletionTime.Time, opts.Clock.Now(), opts.AllowedTimeSkew) {
allErrs = append(allErrs, field.Invalid(completionTimePath, status.CompletionTime, "must be set to the present time"))
}
}
// message covered by DV
return allErrs
}
func timeNear(a, b time.Time, skew time.Duration) bool {
return a.After(b.Add(-skew)) && a.Before(b.Add(skew))
}
// SortTargetResponders returns highest priority responders on a lower indes.
// 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.
func SortTargetResponders(responders []lifecycle.TargetResponder) {
getComponents := func(domainPrefixedKey string) ([]string, string) {
segments := strings.SplitN(domainPrefixedKey, "/", 2)
if len(segments) == 0 {
return nil, ""
}
var domainComponents []string
var key string
if len(segments) == 2 {
key = segments[1]
}
domainComponents = strings.Split(segments[0], ".")
slices.Reverse(domainComponents)
return domainComponents, key
}
slices.SortFunc(responders, func(a, b lifecycle.TargetResponder) int {
if !ptr.Equal(a.Priority, b.Priority) {
if a.Priority == nil {
return 1
}
if b.Priority == nil {
return -1
}
return int(ptr.Deref(b.Priority, -1) - ptr.Deref(a.Priority, -1))
}
aComponents, aKey := getComponents(a.Name)
bComponents, bKey := getComponents(b.Name)
for i, aComponent := range aComponents {
if i > len(bComponents)-1 {
// b has a higher priority if no component left for b
return 1
}
bComponent := bComponents[i]
cmp := strings.Compare(aComponent, bComponent)
if cmp != 0 {
return cmp
}
}
if len(bComponents) > len(aComponents) {
// a has a higher priority if no component left for a
return -1
}
return strings.Compare(aKey, bKey)
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -34,6 +34,7 @@ import (
_ "k8s.io/kubernetes/pkg/apis/extensions/install"
_ "k8s.io/kubernetes/pkg/apis/flowcontrol/install"
_ "k8s.io/kubernetes/pkg/apis/imagepolicy/install"
_ "k8s.io/kubernetes/pkg/apis/lifecycle/install"
_ "k8s.io/kubernetes/pkg/apis/networking/install"
_ "k8s.io/kubernetes/pkg/apis/node/install"
_ "k8s.io/kubernetes/pkg/apis/policy/install"

View File

@@ -44,6 +44,7 @@ import (
apiv1 "k8s.io/api/core/v1"
discoveryv1 "k8s.io/api/discovery/v1"
eventsv1 "k8s.io/api/events/v1"
lifecyclev1alpha1 "k8s.io/api/lifecycle/v1alpha1"
networkingapiv1 "k8s.io/api/networking/v1"
networkingapiv1beta1 "k8s.io/api/networking/v1beta1"
nodev1 "k8s.io/api/node/v1"
@@ -99,6 +100,7 @@ import (
discoveryrest "k8s.io/kubernetes/pkg/registry/discovery/rest"
eventsrest "k8s.io/kubernetes/pkg/registry/events/rest"
flowcontrolrest "k8s.io/kubernetes/pkg/registry/flowcontrol/rest"
lifecyclerest "k8s.io/kubernetes/pkg/registry/lifecycle/rest"
networkingrest "k8s.io/kubernetes/pkg/registry/networking/rest"
noderest "k8s.io/kubernetes/pkg/registry/node/rest"
policyrest "k8s.io/kubernetes/pkg/registry/policy/rest"
@@ -427,6 +429,7 @@ func (c CompletedConfig) StorageProviders(client *kubernetes.Clientset) ([]contr
certificatesrest.RESTStorageProvider{Authorizer: c.ControlPlane.Generic.Authorization.Authorizer},
coordinationrest.RESTStorageProvider{},
discoveryrest.StorageProvider{},
lifecyclerest.RESTStorageProvider{},
networkingrest.RESTStorageProvider{},
noderest.RESTStorageProvider{},
policyrest.RESTStorageProvider{},
@@ -513,6 +516,7 @@ var (
}
// alphaAPIGroupVersionsDisabledByDefault holds the alpha APIs we have for additional API groups only provided in kube-apiserver. They are always disabled by default.
alphaAPIGroupVersionsDisabledByDefault = []schema.GroupVersion{
lifecyclev1alpha1.SchemeGroupVersion,
resourcev1alpha3.SchemeGroupVersion,
schedulingapiv1alpha3.SchemeGroupVersion,
storageapiv1alpha1.SchemeGroupVersion,

View File

@@ -78,6 +78,7 @@ import (
certificatesrest "k8s.io/kubernetes/pkg/registry/certificates/rest"
corerest "k8s.io/kubernetes/pkg/registry/core/rest"
discoveryrest "k8s.io/kubernetes/pkg/registry/discovery/rest"
lifecyclerest "k8s.io/kubernetes/pkg/registry/lifecycle/rest"
networkingrest "k8s.io/kubernetes/pkg/registry/networking/rest"
noderest "k8s.io/kubernetes/pkg/registry/node/rest"
policyrest "k8s.io/kubernetes/pkg/registry/policy/rest"
@@ -560,7 +561,8 @@ func TestGenericStorageProviders(t *testing.T) {
schedulingrest.RESTStorageProvider,
storagerest.RESTStorageProvider,
appsrest.StorageProvider,
resourcerest.RESTStorageProvider:
resourcerest.RESTStorageProvider,
lifecyclerest.RESTStorageProvider:
// all these are non-generic, but kube specific
continue
default:

View File

@@ -34,6 +34,7 @@ import (
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/events"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/apis/lifecycle"
"k8s.io/kubernetes/pkg/apis/networking"
"k8s.io/kubernetes/pkg/apis/policy"
"k8s.io/kubernetes/pkg/apis/resource"
@@ -88,6 +89,8 @@ func NewStorageFactoryConfigEffectiveVersion(effectiveVersion basecompatibility.
scheduling.Resource("workloads").WithVersion("v1beta1"),
scheduling.Resource("podgroups").WithVersion("v1beta1"),
scheduling.Resource("compositepodgroups").WithVersion("v1alpha3"),
lifecycle.Resource("evictions").WithVersion("v1alpha1"),
lifecycle.Resource("evictionrequests").WithVersion("v1alpha1"),
}
return &StorageFactoryConfig{
Serializer: legacyscheme.Codecs,

View File

@@ -34,6 +34,7 @@ import (
_ "k8s.io/kubernetes/pkg/apis/events/install"
_ "k8s.io/kubernetes/pkg/apis/flowcontrol/install"
_ "k8s.io/kubernetes/pkg/apis/imagepolicy/install"
_ "k8s.io/kubernetes/pkg/apis/lifecycle/install"
_ "k8s.io/kubernetes/pkg/apis/networking/install"
_ "k8s.io/kubernetes/pkg/apis/node/install"
_ "k8s.io/kubernetes/pkg/apis/policy/install"

View File

@@ -30,6 +30,7 @@ import (
_ "k8s.io/kubernetes/pkg/apis/discovery/install"
_ "k8s.io/kubernetes/pkg/apis/events/install"
_ "k8s.io/kubernetes/pkg/apis/extensions/install"
_ "k8s.io/kubernetes/pkg/apis/lifecycle/install"
_ "k8s.io/kubernetes/pkg/apis/policy/install"
_ "k8s.io/kubernetes/pkg/apis/rbac/install"
_ "k8s.io/kubernetes/pkg/apis/resource/install"

View File

@@ -20,6 +20,7 @@ import (
"bytes"
"fmt"
"net"
"slices"
"sort"
"strconv"
"strings"
@@ -37,6 +38,7 @@ import (
apiv1 "k8s.io/api/core/v1"
discoveryv1 "k8s.io/api/discovery/v1"
flowcontrolv1 "k8s.io/api/flowcontrol/v1"
lifecyclev1alpha1 "k8s.io/api/lifecycle/v1alpha1"
networkingv1 "k8s.io/api/networking/v1"
rbacv1beta1 "k8s.io/api/rbac/v1beta1"
resourceapi "k8s.io/api/resource/v1"
@@ -44,6 +46,7 @@ import (
schedulingv1alpha3 "k8s.io/api/scheduling/v1alpha3"
schedulingv1beta1 "k8s.io/api/scheduling/v1beta1"
storagev1 "k8s.io/api/storage/v1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
@@ -51,6 +54,7 @@ import (
"k8s.io/apimachinery/pkg/util/duration"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/util/certificate/csr"
"k8s.io/kubernetes/pkg/apis/lifecycle"
"k8s.io/utils/ptr"
podutil "k8s.io/kubernetes/pkg/api/pod"
@@ -780,6 +784,34 @@ func AddHandlers(h printers.PrintHandler) {
}
_ = h.TableHandler(compositePodGroupColumnDefinitions, printCompositePodGroup)
_ = h.TableHandler(compositePodGroupColumnDefinitions, printCompositePodGroupList)
evictionRequestColumnDefinitions := []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
{Name: "Target", Type: "string", Description: lifecyclev1alpha1.EvictionRequestSpec{}.SwaggerDoc()["target"]},
{Name: "Target Type", Type: "string", Description: lifecyclev1alpha1.EvictionRequestSpec{}.SwaggerDoc()["target"]},
{Name: "Status", Type: "string", Description: lifecyclev1alpha1.EvictionRequestStatus{}.SwaggerDoc()["conditions"]},
{Name: "Requester", Type: "string", Description: lifecyclev1alpha1.EvictionRequestSpec{}.SwaggerDoc()["requester"]},
{Name: "Intent", Type: "string", Description: lifecyclev1alpha1.EvictionRequestSpec{}.SwaggerDoc()["intent"]},
{Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]},
}
_ = h.TableHandler(evictionRequestColumnDefinitions, printEvictionRequest)
_ = h.TableHandler(evictionRequestColumnDefinitions, printEvictionRequestList)
evictionColumnDefinitions := []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
{Name: "Target", Type: "string", Description: lifecyclev1alpha1.EvictionSpec{}.SwaggerDoc()["target"]},
{Name: "Target Type", Type: "string", Description: lifecyclev1alpha1.EvictionSpec{}.SwaggerDoc()["target"]},
{Name: "Status", Type: "string", Description: lifecyclev1alpha1.EvictionStatus{}.SwaggerDoc()["conditions"]},
{Name: "Active Responder", Type: "string", Description: lifecyclev1alpha1.TargetResponder{}.SwaggerDoc()["state"]},
{Name: "Responder Status", Type: "string", Description: lifecyclev1alpha1.ResponderStatus{}.SwaggerDoc()[""]},
{Name: "Responder Expected Finish", Type: "string", Description: lifecyclev1alpha1.ResponderStatus{}.SwaggerDoc()["expectedCompletionTime"]},
{Name: "Requesters", Type: "string", Description: lifecyclev1alpha1.EvictionStatus{}.SwaggerDoc()["requesters"]},
{Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]},
{Name: "Responder Heartbeat", Type: "string", Priority: 1, Description: lifecyclev1alpha1.ResponderStatus{}.SwaggerDoc()["heartbeatTime"]},
{Name: "Responder Status Message", Type: "string", Priority: 1, Description: lifecyclev1alpha1.ResponderStatus{}.SwaggerDoc()["message"]},
}
_ = h.TableHandler(evictionColumnDefinitions, printEviction)
_ = h.TableHandler(evictionColumnDefinitions, printEvictionList)
}
// Pass ports=nil for all ports.
@@ -3575,6 +3607,190 @@ func printCompositePodGroupList(list *scheduling.CompositePodGroupList, options
return rows, nil
}
func printEvictionRequest(obj *lifecycle.EvictionRequest, options printers.GenerateOptions) ([]metav1.TableRow, error) {
row := metav1.TableRow{
Object: runtime.RawExtension{Object: obj},
}
var wideCells []interface{}
row.Cells = append(row.Cells, obj.Name)
// resolve target
target := "<unset>"
targetType := "<unset>"
if obj.Spec.Target.Pod != nil {
target = obj.Spec.Target.Pod.Name
targetType = "Pod"
}
row.Cells = append(row.Cells, target, targetType)
// resolve status
evictionStatus := resolveEvictionStatusConditions(obj.Status.ObservedGeneration, obj.Status.Conditions)
row.Cells = append(row.Cells, evictionStatus)
// resolve requester, intent, age
row.Cells = append(row.Cells, obj.Spec.Requester, string(obj.Spec.Intent), translateTimestampSince(obj.CreationTimestamp))
row.Cells = append(row.Cells, wideCells...)
return []metav1.TableRow{row}, nil
}
func printEvictionRequestList(list *lifecycle.EvictionRequestList, options printers.GenerateOptions) ([]metav1.TableRow, error) {
rows := make([]metav1.TableRow, 0, len(list.Items))
for i := range list.Items {
r, err := printEvictionRequest(&list.Items[i], options)
if err != nil {
return nil, err
}
rows = append(rows, r...)
}
return rows, nil
}
func printEviction(obj *lifecycle.Eviction, options printers.GenerateOptions) ([]metav1.TableRow, error) {
row := metav1.TableRow{
Object: runtime.RawExtension{Object: obj},
}
var wideCells []interface{}
row.Cells = append(row.Cells, obj.Name)
// resolve target
target := "<unset>"
targetType := "<unset>"
if obj.Spec.Target.Pod != nil {
target = obj.Spec.Target.Pod.Name
targetType = "Pod"
}
row.Cells = append(row.Cells, target, targetType)
// resolve status
evictionStatus := resolveEvictionStatusConditions(obj.Status.ObservedGeneration, obj.Status.Conditions)
row.Cells = append(row.Cells, evictionStatus)
// resolve responder progress and find an active responder
totalResponders := len(obj.Status.TargetResponders)
processedOrActiveResponders := 0
var lastActiveResponder, lastProcessedResponder *lifecycle.TargetResponder
for _, targetResponder := range obj.Status.TargetResponders {
switch targetResponder.State {
case lifecycle.ResponderStateCanceled, lifecycle.ResponderStateInterrupted, lifecycle.ResponderStateCompleted:
processedOrActiveResponders++
lastProcessedResponder = &targetResponder
case lifecycle.ResponderStateActive:
processedOrActiveResponders++
lastActiveResponder = &targetResponder
}
}
if lastActiveResponder == nil {
// present last completed one if all have been processed
lastActiveResponder = lastProcessedResponder
}
activeResponder := "<unset>"
if lastActiveResponder != nil {
activeResponder = lastActiveResponder.Name
}
activeResponderWithCount := fmt.Sprintf("%s (%d/%d)", activeResponder, processedOrActiveResponders, totalResponders)
row.Cells = append(row.Cells, activeResponderWithCount)
// resolve responder status and finish time
activeResponderExpectedCompletionTime := "<unknown>"
responderStatus := "<unset>"
heartbeat := "<unset>"
message := "<unset>"
if lastActiveResponder != nil {
for _, responder := range obj.Status.Responders {
if responder.Name != lastActiveResponder.Name {
continue
}
responderStatus = string(lastActiveResponder.State)
switch lastActiveResponder.State {
case lifecycle.ResponderStateActive:
if responder.StartTime != nil {
responderStatus = fmt.Sprintf("Started (%s ago)", translateTimestampSince(*responder.StartTime))
if responder.ExpectedCompletionTime != nil {
if responder.ExpectedCompletionTime.After(time.Now()) {
activeResponderExpectedCompletionTime = fmt.Sprintf("in %s", translateTimestampUntil(*responder.ExpectedCompletionTime))
} else {
// in case the estimate is wrong, or a kubelet is slow
activeResponderExpectedCompletionTime = fmt.Sprintf("%s ago", translateTimestampSince(*responder.ExpectedCompletionTime))
}
}
}
case lifecycle.ResponderStateCanceled, lifecycle.ResponderStateInterrupted:
activeResponderExpectedCompletionTime = "-"
case lifecycle.ResponderStateCompleted:
if responder.CompletionTime != nil {
responderStatus = fmt.Sprintf("%s (%s ago)", lastActiveResponder.State, translateTimestampSince(*responder.CompletionTime))
}
activeResponderExpectedCompletionTime = "-"
}
if options.Wide {
if responder.HeartbeatTime != nil {
heartbeat = fmt.Sprintf("%s ago", translateTimestampSince(*responder.HeartbeatTime))
}
if msg := strings.TrimSpace(ptr.Deref(responder.Message, "")); len(msg) > 0 {
message = msg
}
}
break
}
}
if options.Wide {
wideCells = append(wideCells, heartbeat, message)
}
row.Cells = append(row.Cells, responderStatus, activeResponderExpectedCompletionTime)
// resolve requesters
var requesters []string
for _, requester := range obj.Status.Requesters {
if requester.Intent != lifecycle.RequesterIntentWithdrawn {
requesters = append(requesters, requester.Name)
}
}
slices.Sort(requesters)
requestersStr := "<none>"
if len(obj.Status.Requesters) > 0 {
requestersStr = "<withdrawn>"
}
if len(requesters) > 0 {
requestersStr = listWithMoreString(requesters[:1], len(requesters) > 1, len(requesters), 1)
}
row.Cells = append(row.Cells, requestersStr, translateTimestampSince(obj.CreationTimestamp))
row.Cells = append(row.Cells, wideCells...)
return []metav1.TableRow{row}, nil
}
func printEvictionList(list *lifecycle.EvictionList, options printers.GenerateOptions) ([]metav1.TableRow, error) {
rows := make([]metav1.TableRow, 0, len(list.Items))
for i := range list.Items {
r, err := printEviction(&list.Items[i], options)
if err != nil {
return nil, err
}
rows = append(rows, r...)
}
return rows, nil
}
func resolveEvictionStatusConditions(observedGeneration *int64, conditions []metav1.Condition) string {
evictionStatus := "Pending"
if ptr.Deref(observedGeneration, 0) > 0 {
evictionStatus = "Progressing"
}
evicted := meta.FindStatusCondition(conditions, string(lifecycle.EvictionConditionTargetEvicted))
failed := meta.FindStatusCondition(conditions, string(lifecycle.EvictionConditionFailed))
isFailed := failed != nil && failed.Status == metav1.ConditionTrue
if isFailed {
evictionStatus = fmt.Sprintf("%s (%s)", failed.Type, failed.Reason)
}
if evicted != nil && evicted.Status == metav1.ConditionTrue {
evictionStatus = fmt.Sprintf("%s (%s)", evicted.Type, evicted.Reason)
}
return evictionStatus
}
func printBoolPtr(value *bool) string {
if value != nil {
return printBool(*value)

View File

@@ -43,6 +43,7 @@ import (
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/discovery"
"k8s.io/kubernetes/pkg/apis/flowcontrol"
"k8s.io/kubernetes/pkg/apis/lifecycle"
"k8s.io/kubernetes/pkg/apis/networking"
nodeapi "k8s.io/kubernetes/pkg/apis/node"
"k8s.io/kubernetes/pkg/apis/policy"
@@ -8617,3 +8618,657 @@ func TestPrintResourcePoolStatusRequest(t *testing.T) {
}
}
}
func TestPrintEvictionRequest(t *testing.T) {
now := time.Now().UTC().AddDate(0, 0, -3)
twoDaysAgo := now.AddDate(0, 0, -2)
eviction := lifecycle.EvictionRequest{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-1-pod1",
Namespace: "ns1",
CreationTimestamp: metav1.Time{Time: twoDaysAgo},
},
Spec: lifecycle.EvictionRequestSpec{
Target: lifecycle.EvictionRequestTarget{
Pod: &lifecycle.EvictionRequestPodReference{
Name: "pod1",
UID: "3d7fdff1-3fe5-48b9-b106-1ee24b0277f6",
},
},
Requester: "drain.foo.com/bar",
Intent: lifecycle.EvictionRequestIntentEviction,
},
Status: lifecycle.EvictionRequestStatus{
ObservedGeneration: new(int64(1)),
Conditions: []metav1.Condition{},
},
}
tests := []struct {
expected []metav1.TableRow
}{
{
expected: []metav1.TableRow{
// Columns: Name, Target, Target Type, Status, Requester, Intent, Age
{Cells: []interface{}{"pod-1-pod1", "pod1", "Pod", "Progressing", "drain.foo.com/bar", "Eviction", "5d"}},
},
},
}
for _, test := range tests {
rows, err := printEvictionRequest(&eviction, printers.GenerateOptions{})
if err != nil {
t.Fatalf("Error generating table rows for EvictionRequest: %#v", err)
}
rows[0].Object.Object = nil
if !reflect.DeepEqual(test.expected, rows) {
t.Errorf("mismatch: %s", cmp.Diff(test.expected, rows))
}
}
}
func TestPrintEvictionRequestList(t *testing.T) {
now := time.Now().UTC().AddDate(0, 0, -3)
dayAgo := now.AddDate(0, 0, -1)
twoDaysAgo := now.AddDate(0, 0, -2)
evictionRequestList := lifecycle.EvictionRequestList{
Items: []lifecycle.EvictionRequest{
{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-1-pod1",
Namespace: "ns1",
CreationTimestamp: metav1.Time{Time: twoDaysAgo},
},
Spec: lifecycle.EvictionRequestSpec{
Target: lifecycle.EvictionRequestTarget{
Pod: &lifecycle.EvictionRequestPodReference{
Name: "pod1",
UID: "bc134542-aa19-4361-8fe2-cf18f78848c0",
},
},
Requester: "drain.foo.com/bar",
Intent: lifecycle.EvictionRequestIntentEviction,
},
Status: lifecycle.EvictionRequestStatus{
ObservedGeneration: new(int64(1)),
Conditions: []metav1.Condition{
{Type: string(lifecycle.EvictionConditionTargetEvicted), Status: metav1.ConditionTrue, ObservedGeneration: 1, LastTransitionTime: metav1.Time{Time: dayAgo}, Reason: string(lifecycle.EvictionConditionReasonPodDeleted), Message: "Pod was successfully deleted."},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-1-pod2",
Namespace: "ns2",
CreationTimestamp: metav1.Time{Time: twoDaysAgo},
},
Spec: lifecycle.EvictionRequestSpec{
Target: lifecycle.EvictionRequestTarget{
Pod: &lifecycle.EvictionRequestPodReference{
Name: "pod2",
UID: "5e153f5b-e420-41b4-9040-8f3b51cf2162",
},
},
Requester: "drain.foo.com/bar",
Intent: lifecycle.EvictionRequestIntentEviction,
},
Status: lifecycle.EvictionRequestStatus{
ObservedGeneration: new(int64(1)),
Conditions: []metav1.Condition{
{Type: string(lifecycle.EvictionConditionFailed), Status: metav1.ConditionTrue, ObservedGeneration: 1, LastTransitionTime: metav1.Time{Time: now}, Reason: string(lifecycle.EvictionConditionReasonNoFurtherResponder), Message: "Canceled because there is no further responder."},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-1-pod3",
Namespace: "ns3",
CreationTimestamp: metav1.Time{Time: twoDaysAgo},
},
Spec: lifecycle.EvictionRequestSpec{
Target: lifecycle.EvictionRequestTarget{
Pod: &lifecycle.EvictionRequestPodReference{
Name: "pod3",
UID: "68018c4c-bcca-4465-964a-66d8a1626686",
},
},
Requester: "drain.foo.com/bar",
Intent: lifecycle.EvictionRequestIntentWithdrawn,
},
Status: lifecycle.EvictionRequestStatus{
ObservedGeneration: new(int64(1)),
Conditions: []metav1.Condition{
{Type: string(lifecycle.EvictionConditionFailed), Status: metav1.ConditionTrue, ObservedGeneration: 1, LastTransitionTime: metav1.Time{Time: now}, Reason: string(lifecycle.EvictionConditionReasonCanceledDueToNoRequesters), Message: "Canceled due to no requesters."},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-1-pod4",
Namespace: "ns4",
CreationTimestamp: metav1.Time{Time: twoDaysAgo},
},
Spec: lifecycle.EvictionRequestSpec{
Target: lifecycle.EvictionRequestTarget{
Pod: &lifecycle.EvictionRequestPodReference{
Name: "pod4",
UID: "3d7fdff1-3fe5-48b9-b106-1ee24b0277f6",
},
},
Requester: "drain.foo.com/bar",
Intent: lifecycle.EvictionRequestIntentEviction,
},
Status: lifecycle.EvictionRequestStatus{
ObservedGeneration: new(int64(1)),
Conditions: []metav1.Condition{
{Type: string(lifecycle.EvictionConditionTargetEvicted), Status: metav1.ConditionFalse, ObservedGeneration: 1, LastTransitionTime: metav1.Time{Time: now}, Reason: "EvictionInProgress", Message: "Pod is being evicted."},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-1-pod5",
Namespace: "ns5",
CreationTimestamp: metav1.Time{Time: twoDaysAgo},
},
Spec: lifecycle.EvictionRequestSpec{
Target: lifecycle.EvictionRequestTarget{
Pod: &lifecycle.EvictionRequestPodReference{
Name: "pod5",
UID: "9f4a1620-0e97-4246-b304-5969b2104377",
},
},
Requester: "drain.foo.com/bar",
Intent: lifecycle.EvictionRequestIntentEviction,
},
Status: lifecycle.EvictionRequestStatus{
ObservedGeneration: new(int64(1)),
Conditions: []metav1.Condition{
{Type: string(lifecycle.EvictionConditionTargetEvicted), Status: metav1.ConditionFalse, ObservedGeneration: 1, LastTransitionTime: metav1.Time{Time: now}, Reason: "Canceled", Message: "Eviction failed due to cancelation."},
{Type: string(lifecycle.EvictionConditionFailed), Status: metav1.ConditionTrue, ObservedGeneration: 1, LastTransitionTime: metav1.Time{Time: now}, Reason: string(lifecycle.EvictionConditionReasonEvictionInvalid), Message: "Pod pod6 was not found."},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-1-pod6",
Namespace: "ns6",
CreationTimestamp: metav1.Time{Time: twoDaysAgo},
},
Spec: lifecycle.EvictionRequestSpec{
Target: lifecycle.EvictionRequestTarget{
Pod: &lifecycle.EvictionRequestPodReference{
Name: "pod6",
UID: "e968f679-523b-4e63-9417-0cd9ae13a0b2",
},
},
Requester: "drain.foo.com/bar",
Intent: lifecycle.EvictionRequestIntentEviction,
},
},
},
}
// Columns: Name, Target, Target Type, Status, Requester, Intent, Age
expected := []metav1.TableRow{
{Cells: []interface{}{"pod-1-pod1", "pod1", "Pod", "TargetEvicted (PodDeleted)", "drain.foo.com/bar", "Eviction", "5d"}},
{Cells: []interface{}{"pod-1-pod2", "pod2", "Pod", "Failed (NoFurtherResponder)", "drain.foo.com/bar", "Eviction", "5d"}},
{Cells: []interface{}{"pod-1-pod3", "pod3", "Pod", "Failed (CanceledDueToNoRequesters)", "drain.foo.com/bar", "Withdrawn", "5d"}},
{Cells: []interface{}{"pod-1-pod4", "pod4", "Pod", "Progressing", "drain.foo.com/bar", "Eviction", "5d"}},
{Cells: []interface{}{"pod-1-pod5", "pod5", "Pod", "Failed (EvictionInvalid)", "drain.foo.com/bar", "Eviction", "5d"}},
{Cells: []interface{}{"pod-1-pod6", "pod6", "Pod", "Pending", "drain.foo.com/bar", "Eviction", "5d"}},
}
rows, err := printEvictionRequestList(&evictionRequestList, printers.GenerateOptions{})
if err != nil {
t.Fatalf("Error generating table rows for EvictionRequestList: %#v", err)
}
for i := range rows {
rows[i].Object.Object = nil
}
if !reflect.DeepEqual(expected, rows) {
t.Errorf("mismatch: %s", cmp.Diff(expected, rows))
}
}
func TestPrintEviction(t *testing.T) {
now := time.Now().UTC().AddDate(0, 0, -3)
daysLater := now.AddDate(0, 0, 5).Add(time.Minute)
dayAgo := now.AddDate(0, 0, -1)
twoDaysAgo := now.AddDate(0, 0, -2)
eviction := lifecycle.Eviction{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-1-pod1",
Namespace: "ns1",
CreationTimestamp: metav1.Time{Time: twoDaysAgo},
},
Spec: lifecycle.EvictionSpec{
Target: lifecycle.EvictionTarget{
Pod: &lifecycle.EvictionPodReference{
Name: "pod1",
UID: "3d7fdff1-3fe5-48b9-b106-1ee24b0277f6",
},
},
},
Status: lifecycle.EvictionStatus{
ObservedGeneration: new(int64(1)),
TargetResponders: []lifecycle.TargetResponder{
{Name: "responder1", State: lifecycle.ResponderStateCompleted},
{Name: "responder2", State: lifecycle.ResponderStateActive},
},
Responders: []lifecycle.ResponderStatus{
{Name: "responder1", HeartbeatTime: &metav1.Time{Time: dayAgo}, ExpectedCompletionTime: nil, StartTime: &metav1.Time{Time: twoDaysAgo}, CompletionTime: &metav1.Time{Time: dayAgo}, Message: new("completed message")},
{Name: "responder2", HeartbeatTime: &metav1.Time{Time: now}, ExpectedCompletionTime: &metav1.Time{Time: daysLater}, StartTime: &metav1.Time{Time: now}, CompletionTime: nil, Message: new("migrating pod1 message")},
},
Requesters: []lifecycle.Requester{
{
Name: "drain.foo.com/bar",
Intent: lifecycle.RequesterIntentEviction,
},
},
Conditions: []metav1.Condition{},
},
}
tests := []struct {
options printers.GenerateOptions
expected []metav1.TableRow
}{
{
options: printers.GenerateOptions{Wide: false},
expected: []metav1.TableRow{
// Columns: Name, Target, Target Type, Status, Active Responder, Responder Status, Responder Expected Finish, Requesters, Age
{Cells: []interface{}{"pod-1-pod1", "pod1", "Pod", "Progressing", "responder2 (2/2)", "Started (3d ago)", "in 2d", "drain.foo.com/bar", "5d"}},
},
},
{
options: printers.GenerateOptions{Wide: true},
expected: []metav1.TableRow{
// Columns: Name, Target, Target Type, Status, Active Responder, Responder Status, Responder Expected Finish, Requesters, Age, Responder Status Message, Responder Heartbeat
{Cells: []interface{}{"pod-1-pod1", "pod1", "Pod", "Progressing", "responder2 (2/2)", "Started (3d ago)", "in 2d", "drain.foo.com/bar", "5d", "3d ago", "migrating pod1 message"}},
},
},
}
for _, test := range tests {
rows, err := printEviction(&eviction, test.options)
if err != nil {
t.Fatalf("Error generating table rows for Eviction: %#v", err)
}
rows[0].Object.Object = nil
if !reflect.DeepEqual(test.expected, rows) {
t.Errorf("mismatch: %s", cmp.Diff(test.expected, rows))
}
}
}
func TestPrintEvictionList(t *testing.T) {
now := time.Now().UTC().AddDate(0, 0, -3)
daysLater := now.AddDate(0, 0, 5).Add(time.Minute)
dayAgo := now.AddDate(0, 0, -1)
twoDaysAgo := now.AddDate(0, 0, -2)
evictionList := lifecycle.EvictionList{
Items: []lifecycle.Eviction{
{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-1-pod1",
Namespace: "ns1",
CreationTimestamp: metav1.Time{Time: twoDaysAgo},
},
Spec: lifecycle.EvictionSpec{
Target: lifecycle.EvictionTarget{
Pod: &lifecycle.EvictionPodReference{
Name: "pod1",
UID: "bc134542-aa19-4361-8fe2-cf18f78848c0",
},
},
},
Status: lifecycle.EvictionStatus{
ObservedGeneration: new(int64(1)),
Requesters: []lifecycle.Requester{
{
Name: "drain.foo.com/bar",
Intent: lifecycle.RequesterIntentEviction,
},
},
TargetResponders: []lifecycle.TargetResponder{
{Name: "responder1", State: lifecycle.ResponderStateCompleted},
},
Responders: []lifecycle.ResponderStatus{
{Name: "responder1", HeartbeatTime: &metav1.Time{Time: dayAgo}, ExpectedCompletionTime: &metav1.Time{Time: dayAgo}, StartTime: &metav1.Time{Time: twoDaysAgo}, CompletionTime: &metav1.Time{Time: dayAgo}, Message: new("migrated1")},
},
Conditions: []metav1.Condition{
{Type: string(lifecycle.EvictionConditionTargetEvicted), Status: metav1.ConditionTrue, ObservedGeneration: 1, LastTransitionTime: metav1.Time{Time: dayAgo}, Reason: string(lifecycle.EvictionConditionReasonPodDeleted), Message: "Pod was successfully deleted."},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-1-pod2",
Namespace: "ns2",
CreationTimestamp: metav1.Time{Time: twoDaysAgo},
},
Spec: lifecycle.EvictionSpec{
Target: lifecycle.EvictionTarget{
Pod: &lifecycle.EvictionPodReference{
Name: "pod2",
UID: "5e153f5b-e420-41b4-9040-8f3b51cf2162",
},
},
},
Status: lifecycle.EvictionStatus{
ObservedGeneration: new(int64(1)),
Requesters: []lifecycle.Requester{
{
Name: "drain.foo.com/bar",
Intent: lifecycle.RequesterIntentEviction,
},
},
TargetResponders: []lifecycle.TargetResponder{
{Name: "responder1", State: lifecycle.ResponderStateInterrupted},
},
Responders: []lifecycle.ResponderStatus{
{Name: "responder1", HeartbeatTime: &metav1.Time{Time: dayAgo}, ExpectedCompletionTime: &metav1.Time{Time: dayAgo}, StartTime: &metav1.Time{Time: twoDaysAgo}, CompletionTime: nil, Message: new("running for a long time")}},
Conditions: []metav1.Condition{
{Type: string(lifecycle.EvictionConditionFailed), Status: metav1.ConditionTrue, ObservedGeneration: 1, LastTransitionTime: metav1.Time{Time: now}, Reason: string(lifecycle.EvictionConditionReasonNoFurtherResponder), Message: "Canceled because there is no further responder."},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-1-pod3",
Namespace: "ns3",
CreationTimestamp: metav1.Time{Time: twoDaysAgo},
},
Spec: lifecycle.EvictionSpec{
Target: lifecycle.EvictionTarget{
Pod: &lifecycle.EvictionPodReference{
Name: "pod3",
UID: "a4ea9433-da6a-4bfc-9033-296488ba12be",
},
},
},
Status: lifecycle.EvictionStatus{
ObservedGeneration: new(int64(1)),
Requesters: []lifecycle.Requester{
{
Name: "drain.foo.com/bar",
Intent: lifecycle.RequesterIntentEviction,
},
},
TargetResponders: []lifecycle.TargetResponder{
{Name: "responder1", State: lifecycle.ResponderStateCompleted},
},
Responders: []lifecycle.ResponderStatus{
{Name: "responder1", HeartbeatTime: &metav1.Time{Time: dayAgo}, ExpectedCompletionTime: nil, StartTime: &metav1.Time{Time: twoDaysAgo}, CompletionTime: &metav1.Time{Time: dayAgo}, Message: new("completed message")}},
Conditions: []metav1.Condition{
{Type: string(lifecycle.EvictionConditionFailed), Status: metav1.ConditionTrue, ObservedGeneration: 1, LastTransitionTime: metav1.Time{Time: now}, Reason: string(lifecycle.EvictionConditionReasonNoFurtherResponder), Message: "Canceled because there is no further responder."},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-1-pod4",
Namespace: "ns4",
CreationTimestamp: metav1.Time{Time: twoDaysAgo},
},
Spec: lifecycle.EvictionSpec{
Target: lifecycle.EvictionTarget{
Pod: &lifecycle.EvictionPodReference{
Name: "pod4",
UID: "68018c4c-bcca-4465-964a-66d8a1626686",
},
},
},
Status: lifecycle.EvictionStatus{
ObservedGeneration: new(int64(1)),
Requesters: []lifecycle.Requester{
{
Name: "drain.foo.com/bar",
Intent: lifecycle.RequesterIntentWithdrawn,
},
},
TargetResponders: []lifecycle.TargetResponder{
{Name: "responder3", State: lifecycle.ResponderStateCanceled},
{Name: "responder4", State: lifecycle.ResponderStateInactive},
},
Responders: []lifecycle.ResponderStatus{
{Name: "responder3", HeartbeatTime: &metav1.Time{Time: now}, ExpectedCompletionTime: nil, StartTime: &metav1.Time{Time: now}, CompletionTime: nil, Message: new("running3")},
{Name: "responder4", HeartbeatTime: nil, ExpectedCompletionTime: nil, StartTime: nil, CompletionTime: nil, Message: nil},
},
Conditions: []metav1.Condition{
{Type: string(lifecycle.EvictionConditionFailed), Status: metav1.ConditionTrue, ObservedGeneration: 1, LastTransitionTime: metav1.Time{Time: now}, Reason: string(lifecycle.EvictionConditionReasonCanceledDueToNoRequesters), Message: "Canceled due to no requesters."},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-1-pod5",
Namespace: "ns5",
CreationTimestamp: metav1.Time{Time: twoDaysAgo},
},
Spec: lifecycle.EvictionSpec{
Target: lifecycle.EvictionTarget{
Pod: &lifecycle.EvictionPodReference{
Name: "pod5",
UID: "3d7fdff1-3fe5-48b9-b106-1ee24b0277f6",
},
},
},
Status: lifecycle.EvictionStatus{
ObservedGeneration: new(int64(1)),
Requesters: []lifecycle.Requester{
{
Name: "drain.foo.com/bar",
Intent: lifecycle.RequesterIntentEviction,
},
},
TargetResponders: []lifecycle.TargetResponder{
{Name: "responder1", State: lifecycle.ResponderStateCompleted},
{Name: "responder2", State: lifecycle.ResponderStateActive},
},
Responders: []lifecycle.ResponderStatus{
{Name: "responder1", HeartbeatTime: &metav1.Time{Time: dayAgo}, ExpectedCompletionTime: nil, StartTime: &metav1.Time{Time: twoDaysAgo}, CompletionTime: &metav1.Time{Time: dayAgo}, Message: new("completed2")},
{Name: "responder2", HeartbeatTime: &metav1.Time{Time: now}, ExpectedCompletionTime: &metav1.Time{Time: daysLater}, StartTime: &metav1.Time{Time: now}, CompletionTime: nil, Message: new("running2")},
},
Conditions: []metav1.Condition{
{Type: string(lifecycle.EvictionConditionTargetEvicted), Status: metav1.ConditionFalse, ObservedGeneration: 1, LastTransitionTime: metav1.Time{Time: now}, Reason: "EvictionInProgress", Message: "Pod is being evicted."},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-1-pod6",
Namespace: "ns6",
CreationTimestamp: metav1.Time{Time: twoDaysAgo},
},
Spec: lifecycle.EvictionSpec{
Target: lifecycle.EvictionTarget{
Pod: &lifecycle.EvictionPodReference{
Name: "pod6",
UID: "aac2a982-2176-4770-8c8c-bcdf51b24fde",
},
},
},
Status: lifecycle.EvictionStatus{
ObservedGeneration: new(int64(1)),
Requesters: []lifecycle.Requester{
{
Name: "drain.foo.com/bar",
Intent: lifecycle.RequesterIntentEviction,
},
},
TargetResponders: []lifecycle.TargetResponder{
{Name: "responder1", State: lifecycle.ResponderStateCompleted},
{Name: "responder2", State: lifecycle.ResponderStateActive},
},
Responders: []lifecycle.ResponderStatus{
{Name: "responder1", HeartbeatTime: &metav1.Time{Time: dayAgo}, ExpectedCompletionTime: nil, StartTime: &metav1.Time{Time: twoDaysAgo}, CompletionTime: &metav1.Time{Time: dayAgo}, Message: new("completed2")},
{Name: "responder2", HeartbeatTime: &metav1.Time{Time: now}, ExpectedCompletionTime: &metav1.Time{Time: dayAgo}, StartTime: &metav1.Time{Time: now}, CompletionTime: nil, Message: new("running2")},
},
Conditions: []metav1.Condition{
{Type: string(lifecycle.EvictionConditionTargetEvicted), Status: metav1.ConditionFalse, ObservedGeneration: 1, LastTransitionTime: metav1.Time{Time: now}, Reason: "EvictionInProgress", Message: "Pod is being evicted."},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-1-pod7",
Namespace: "ns7",
CreationTimestamp: metav1.Time{Time: twoDaysAgo},
},
Spec: lifecycle.EvictionSpec{
Target: lifecycle.EvictionTarget{
Pod: &lifecycle.EvictionPodReference{
Name: "pod7",
UID: "707ba618-ffd6-4797-b503-17af1c7f4d98",
},
},
},
Status: lifecycle.EvictionStatus{
ObservedGeneration: new(int64(1)),
Requesters: []lifecycle.Requester{
{
Name: "rescheduler.bar.com",
Intent: lifecycle.RequesterIntentEviction,
},
{
Name: "drain.foo.com/bar",
Intent: lifecycle.RequesterIntentEviction,
},
},
TargetResponders: []lifecycle.TargetResponder{
{Name: "responder3", State: lifecycle.ResponderStateActive},
{Name: "responder4", State: lifecycle.ResponderStateInactive},
},
Responders: []lifecycle.ResponderStatus{
{Name: "responder3", HeartbeatTime: &metav1.Time{Time: now}, ExpectedCompletionTime: nil, StartTime: &metav1.Time{Time: now}, CompletionTime: nil, Message: new("running3")},
{Name: "responder4", HeartbeatTime: nil, ExpectedCompletionTime: nil, StartTime: nil, CompletionTime: nil, Message: nil},
},
Conditions: []metav1.Condition{
{Type: string(lifecycle.EvictionConditionTargetEvicted), Status: metav1.ConditionFalse, ObservedGeneration: 1, LastTransitionTime: metav1.Time{Time: now}, Reason: "EvictionInProgress", Message: "Pod is being evicted."},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-1-pod8",
Namespace: "ns8",
CreationTimestamp: metav1.Time{Time: twoDaysAgo},
},
Spec: lifecycle.EvictionSpec{
Target: lifecycle.EvictionTarget{
Pod: &lifecycle.EvictionPodReference{
Name: "pod8",
UID: "30741366-8d1f-4385-84ff-0f2e7ba0305f",
},
},
},
Status: lifecycle.EvictionStatus{
ObservedGeneration: new(int64(1)),
Requesters: []lifecycle.Requester{
{
Name: "drain.foo.com/bar",
Intent: lifecycle.RequesterIntentEviction,
},
},
TargetResponders: []lifecycle.TargetResponder{
{Name: "responder3", State: lifecycle.ResponderStateInactive},
{Name: "responder4", State: lifecycle.ResponderStateInactive},
},
Responders: []lifecycle.ResponderStatus{
{Name: "responder3", HeartbeatTime: nil, ExpectedCompletionTime: nil, StartTime: nil, CompletionTime: nil, Message: nil},
{Name: "responder4", HeartbeatTime: nil, ExpectedCompletionTime: nil, StartTime: nil, CompletionTime: nil, Message: nil},
},
Conditions: []metav1.Condition{
{Type: string(lifecycle.EvictionConditionTargetEvicted), Status: metav1.ConditionFalse, ObservedGeneration: 1, LastTransitionTime: metav1.Time{Time: now}, Reason: "WaitingForResponder", Message: "Waiting for an responder."},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-1-pod9",
Namespace: "ns9",
CreationTimestamp: metav1.Time{Time: twoDaysAgo},
},
Spec: lifecycle.EvictionSpec{
Target: lifecycle.EvictionTarget{
Pod: &lifecycle.EvictionPodReference{
Name: "pod9",
UID: "9f4a1620-0e97-4246-b304-5969b2104377",
},
},
},
Status: lifecycle.EvictionStatus{
ObservedGeneration: new(int64(1)),
Requesters: []lifecycle.Requester{
{
Name: "drain.foo.com/bar",
Intent: lifecycle.RequesterIntentEviction,
},
},
TargetResponders: []lifecycle.TargetResponder{
{Name: "responder3", State: lifecycle.ResponderStateInactive},
{Name: "responder4", State: lifecycle.ResponderStateInactive},
},
Responders: []lifecycle.ResponderStatus{
{Name: "responder3", HeartbeatTime: nil, ExpectedCompletionTime: nil, StartTime: nil, CompletionTime: nil, Message: nil},
{Name: "responder4", HeartbeatTime: nil, ExpectedCompletionTime: nil, StartTime: nil, CompletionTime: nil, Message: nil},
},
Conditions: []metav1.Condition{
{Type: string(lifecycle.EvictionConditionTargetEvicted), Status: metav1.ConditionFalse, ObservedGeneration: 1, LastTransitionTime: metav1.Time{Time: now}, Reason: "Canceled", Message: "Eviction failed due to cancelation."},
{Type: string(lifecycle.EvictionConditionFailed), Status: metav1.ConditionTrue, ObservedGeneration: 1, LastTransitionTime: metav1.Time{Time: now}, Reason: string(lifecycle.EvictionConditionReasonEvictionInvalid), Message: "Pod pod6 was not found."},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-1-pod10",
Namespace: "ns10",
CreationTimestamp: metav1.Time{Time: twoDaysAgo},
},
Spec: lifecycle.EvictionSpec{
Target: lifecycle.EvictionTarget{
Pod: &lifecycle.EvictionPodReference{
Name: "pod10",
UID: "e968f679-523b-4e63-9417-0cd9ae13a0b2",
},
},
},
},
},
}
// Columns: Name, Target, Target Type, Status, Active Responder, Responder Status, Responder Expected Finish, Requesters, Age
expected := []metav1.TableRow{
{Cells: []interface{}{"pod-1-pod1", "pod1", "Pod", "TargetEvicted (PodDeleted)", "responder1 (1/1)", "Completed (4d ago)", "-", "drain.foo.com/bar", "5d"}},
{Cells: []interface{}{"pod-1-pod2", "pod2", "Pod", "Failed (NoFurtherResponder)", "responder1 (1/1)", "Interrupted", "-", "drain.foo.com/bar", "5d"}},
{Cells: []interface{}{"pod-1-pod3", "pod3", "Pod", "Failed (NoFurtherResponder)", "responder1 (1/1)", "Completed (4d ago)", "-", "drain.foo.com/bar", "5d"}},
{Cells: []interface{}{"pod-1-pod4", "pod4", "Pod", "Failed (CanceledDueToNoRequesters)", "responder3 (1/2)", "Canceled", "-", "<withdrawn>", "5d"}},
{Cells: []interface{}{"pod-1-pod5", "pod5", "Pod", "Progressing", "responder2 (2/2)", "Started (3d ago)", "in 2d", "drain.foo.com/bar", "5d"}},
{Cells: []interface{}{"pod-1-pod6", "pod6", "Pod", "Progressing", "responder2 (2/2)", "Started (3d ago)", "4d ago", "drain.foo.com/bar", "5d"}},
{Cells: []interface{}{"pod-1-pod7", "pod7", "Pod", "Progressing", "responder3 (1/2)", "Started (3d ago)", "<unknown>", "drain.foo.com/bar + 1 more...", "5d"}},
{Cells: []interface{}{"pod-1-pod8", "pod8", "Pod", "Progressing", "<unset> (0/2)", "<unset>", "<unknown>", "drain.foo.com/bar", "5d"}},
{Cells: []interface{}{"pod-1-pod9", "pod9", "Pod", "Failed (EvictionInvalid)", "<unset> (0/2)", "<unset>", "<unknown>", "drain.foo.com/bar", "5d"}},
{Cells: []interface{}{"pod-1-pod10", "pod10", "Pod", "Pending", "<unset> (0/0)", "<unset>", "<unknown>", "<none>", "5d"}},
}
rows, err := printEvictionList(&evictionList, printers.GenerateOptions{})
if err != nil {
t.Fatalf("Error generating table rows for EvictionList: %#v", err)
}
for i := range rows {
rows[i].Object.Object = nil
}
if !reflect.DeepEqual(expected, rows) {
t.Errorf("mismatch: %s", cmp.Diff(expected, rows))
}
}

View File

@@ -0,0 +1,17 @@
/*
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 eviction

View File

@@ -0,0 +1,107 @@
/*
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 storage
import (
"context"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/registry/rest"
lifecycleapi "k8s.io/kubernetes/pkg/apis/lifecycle"
"k8s.io/kubernetes/pkg/printers"
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
printerstorage "k8s.io/kubernetes/pkg/printers/storage"
"k8s.io/kubernetes/pkg/registry/lifecycle/eviction"
"k8s.io/utils/clock"
)
// REST implements a RESTStorage for evictions against etcd
type REST struct {
*genericregistry.Store
}
// NewREST returns a RESTStorage object that will work against evictions.
func NewREST(optsGetter generic.RESTOptionsGetter, clock clock.PassiveClock) (*REST, *StatusREST, error) {
strategy := eviction.NewStrategy(clock)
store := &genericregistry.Store{
NewFunc: func() runtime.Object { return &lifecycleapi.Eviction{} },
NewListFunc: func() runtime.Object { return &lifecycleapi.EvictionList{} },
DefaultQualifiedResource: lifecycleapi.Resource("evictions"),
SingularQualifiedResource: lifecycleapi.Resource("eviction"),
CreateStrategy: strategy,
UpdateStrategy: strategy,
DeleteStrategy: strategy,
ResetFieldsStrategy: strategy,
TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)},
}
options := &generic.StoreOptions{RESTOptions: optsGetter}
if err := store.CompleteWithOptions(options); err != nil {
return nil, nil, err
}
statusStrategy := eviction.NewStatusStrategy(strategy)
statusStore := *store
statusStore.UpdateStrategy = statusStrategy
statusStore.ResetFieldsStrategy = statusStrategy
return &REST{store}, &StatusREST{store: &statusStore}, nil
}
// StatusREST implements the REST endpoint for changing the status of evictions.
type StatusREST struct {
store *genericregistry.Store
}
// New creates a new Eviction object.
func (r *StatusREST) New() runtime.Object {
return &lifecycleapi.Eviction{}
}
// Destroy cleans up resources on shutdown.
func (r *StatusREST) Destroy() {
// Given that underlying store is shared with REST,
// we don't destroy it here explicitly.
}
// Get retrieves the object from the storage. It is required to support Patch.
func (r *StatusREST) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
return r.store.Get(ctx, name, options)
}
// Update alters the status subset of an object.
func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
// We are explicitly setting forceAllowCreate to false in the call to the underlying storage because
// subresources should never allow create on update.
return r.store.Update(ctx, name, objInfo, createValidation, updateValidation, false, options)
}
// GetResetFields implements rest.ResetFieldsStrategy
func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set {
return r.store.GetResetFields()
}
func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
return r.store.ConvertToTable(ctx, object, tableOptions)
}

View File

@@ -0,0 +1,274 @@
/*
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 storage
import (
"context"
"testing"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/generic"
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
"k8s.io/apiserver/pkg/registry/rest"
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
"k8s.io/kubernetes/pkg/apis/lifecycle"
"k8s.io/kubernetes/pkg/registry/registrytest"
testing2 "k8s.io/utils/clock/testing"
)
const validUID = "8057f54d-455d-4b25-90c6-92a919cff10a"
func newStorage(t *testing.T) (*REST, *StatusREST, *etcd3testing.EtcdTestServer) {
clock := testing2.NewFakePassiveClock(time.Now())
etcdStorage, server := registrytest.NewEtcdStorageForResource(t, lifecycle.SchemeGroupVersion.WithResource("evictions").GroupResource())
restOptions := generic.RESTOptions{
StorageConfig: etcdStorage,
Decorator: generic.UndecoratedStorage,
DeleteCollectionWorkers: 1,
ResourcePrefix: "evictions",
}
evictionStorage, evictionStatusStorage, err := NewREST(restOptions, clock)
if err != nil {
t.Fatalf("unexpected error from REST storage: %v", err)
}
return evictionStorage, evictionStatusStorage, server
}
func tester(t *testing.T, storage *REST) *genericregistrytest.Tester {
test := genericregistrytest.New(t, storage.Store)
requestInfo := &genericapirequest.RequestInfo{
APIGroup: "lifecycle.k8s.io",
APIVersion: "v1alpha1",
Resource: "evictions",
}
test.SetRequestInfo(requestInfo)
return test
}
func newValidEviction() *lifecycle.Eviction {
return &lifecycle.Eviction{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: metav1.NamespaceDefault,
},
Spec: lifecycle.EvictionSpec{
Target: lifecycle.EvictionTarget{
Pod: &lifecycle.EvictionPodReference{
UID: validUID,
Name: "foo.pod",
},
},
},
Status: lifecycle.EvictionStatus{
ObservedGeneration: new(int64(1)),
Requesters: []lifecycle.Requester{
{Name: "requester-1.example.com/bar", Intent: lifecycle.RequesterIntentEviction},
{Name: "requester-2.example.com/bar", Intent: lifecycle.RequesterIntentEviction},
},
TargetResponders: []lifecycle.TargetResponder{
{Name: "responder1.example.com/bar", Priority: new(int32(1000)), State: lifecycle.ResponderStateInactive},
},
Responders: []lifecycle.ResponderStatus{
{Name: "responder1.example.com/bar"},
},
},
}
}
func TestCreate(t *testing.T) {
storage, _, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := tester(t, storage)
validEviction := newValidEviction()
validEviction.ObjectMeta = metav1.ObjectMeta{}
invalidEviction := newValidEviction()
invalidEviction.ObjectMeta = metav1.ObjectMeta{Name: "-foo"}
test.TestCreate(
validEviction,
invalidEviction,
)
}
func TestUpdate(t *testing.T) {
storage, _, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := tester(t, storage)
validEviction := newValidEviction()
test.TestUpdate(
validEviction,
func(obj runtime.Object) runtime.Object {
object := obj.(*lifecycle.Eviction)
object.ObjectMeta.Annotations = map[string]string{"foo": "bar"}
return object
},
// invalid updateFunc
func(obj runtime.Object) runtime.Object {
object := obj.(*lifecycle.Eviction)
object.Spec.Target.Pod.Name = "bar"
return object
},
)
}
func TestDelete(t *testing.T) {
storage, _, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := tester(t, storage)
test.TestDelete(newValidEviction())
}
func TestGet(t *testing.T) {
storage, _, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := tester(t, storage)
test.TestGet(newValidEviction())
}
func TestList(t *testing.T) {
storage, _, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := tester(t, storage)
test.TestList(newValidEviction())
}
func TestWatch(t *testing.T) {
storage, _, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := tester(t, storage)
test.TestWatch(
newValidEviction(),
// matching labels
[]labels.Set{},
// not matching labels
[]labels.Set{
{"x": "y"},
},
// matching fields
[]fields.Set{},
// not matching fields
[]fields.Set{
{"metadata.name": "xyz"},
{"name": "foo"},
},
)
}
func TestStatusUpdate(t *testing.T) {
storage, statusStorage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
eviction := newValidEviction()
eviction.Status = lifecycle.EvictionStatus{}
ctx := evictionContext()
key, err := storage.KeyFunc(ctx, "foo")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
result := &lifecycle.Eviction{}
if err := storage.Storage.Create(ctx, key, eviction, result, 0, false); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(result.Status.TargetResponders) != 0 {
t.Errorf("we expected .status.targetResponders to be empty but it was %v", result.Status.TargetResponders)
}
evictionUpdate := newValidEviction()
evictionUpdate.ObjectMeta = result.ObjectMeta
evictionUpdate.Labels = map[string]string{"foo": "bar"}
evictionUpdate.Spec.Target.Pod.Name = "bax"
evictionUpdate.Status.TargetResponders = []lifecycle.TargetResponder{
{Name: "responder1.example.com/bar", Priority: new(int32(1000)), State: lifecycle.ResponderStateActive},
}
evictionUpdate.Status.Responders = []lifecycle.ResponderStatus{
{Name: "responder1.example.com/bar", StartTime: new(metav1.Now())},
}
if _, _, err := statusStorage.Update(ctx, evictionUpdate.Name, rest.DefaultUpdatedObjectInfo(evictionUpdate), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
obj, err := storage.Get(ctx, evictionUpdate.Name, &metav1.GetOptions{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
result = obj.(*lifecycle.Eviction)
if len(result.Labels) != 0 {
t.Errorf("we expected .status.labels to be empty but it was %v", result.Labels)
}
if result.Spec.Target.Pod.Name != "foo.pod" {
t.Errorf("we expected .spec.target.pod.name to not be updated but it was updated to %v", result.Spec.Target.Pod.Name)
}
if len(result.Status.TargetResponders) != 1 {
t.Errorf("we expected .status.targetResponders to be updated to but it was %v", result.Status.TargetResponders)
}
}
func TestGenerationNumber(t *testing.T) {
storage, statusStorage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
eviction := newValidEviction()
eviction.Generation = 100
eviction.Status.ObservedGeneration = new(int64(10))
ctx := evictionContext()
resultObj, err := storage.Create(ctx, eviction, rest.ValidateAllObjectFunc, &metav1.CreateOptions{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
result, _ := resultObj.(*lifecycle.Eviction)
// Generation initialization
if result.Generation != 1 || result.Status.ObservedGeneration != nil {
t.Fatalf("Unexpected generation number %v, status generation %v", result.Generation, result.Status.ObservedGeneration)
}
// Updates to status should not increment either spec or status generation numbers
result.Status.Conditions = append(result.Status.Conditions, metav1.Condition{Type: "Test", Status: "True", LastTransitionTime: metav1.Now(), Reason: "Reason"})
if _, _, err := statusStorage.Update(ctx, result.Name, rest.DefaultUpdatedObjectInfo(result), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil {
t.Errorf("unexpected error: %v", err)
}
resultObj, err = storage.Get(ctx, result.Name, &metav1.GetOptions{})
if err != nil {
t.Errorf("unexpected error: %v", err)
}
result, _ = resultObj.(*lifecycle.Eviction)
if result.Generation != 1 || result.Status.ObservedGeneration != nil {
t.Fatalf("Unexpected generation number, spec: %v, status: %v", result.Generation, result.Status.ObservedGeneration)
}
}
func evictionContext() context.Context {
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
ctx = genericapirequest.WithRequestInfo(ctx, &genericapirequest.RequestInfo{
APIGroup: "lifecycle.k8s.io",
APIVersion: "v1alpha1",
Resource: "evictions",
})
return ctx
}

View File

@@ -0,0 +1,182 @@
/*
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 eviction
import (
"context"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/apiserver/pkg/storage/names"
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/apis/lifecycle"
"k8s.io/kubernetes/pkg/apis/lifecycle/validation"
"k8s.io/utils/clock"
)
// evictionStrategy is the default logic that applies when creating and updating Eviction objects.
type evictionStrategy struct {
rest.DeclarativeValidation
names.NameGenerator
clock clock.PassiveClock
}
func NewStrategy(clock clock.PassiveClock) *evictionStrategy {
return &evictionStrategy{
rest.DeclarativeValidation{Scheme: legacyscheme.Scheme},
names.SimpleNameGenerator,
clock,
}
}
var _ = rest.ResetFieldsStrategy(&evictionStrategy{})
func (*evictionStrategy) NamespaceScoped() bool {
return true
}
// GetResetFields returns the set of fields that get reset by the strategy
// and should not be modified by the user.
func (*evictionStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set {
fields := map[fieldpath.APIVersion]*fieldpath.Set{
"lifecycle.k8s.io/v1alpha1": fieldpath.NewSet(
fieldpath.MakePathOrDie("status"),
),
}
return fields
}
// PrepareForCreate clears fields that are not allowed to be set by end users on creation.
func (*evictionStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
eviction := obj.(*lifecycle.Eviction)
eviction.Status = lifecycle.EvictionStatus{}
eviction.Generation = 1
}
// PrepareForUpdate clears fields that are not allowed to be set by end users on update.
func (*evictionStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
oldEviction := old.(*lifecycle.Eviction)
newEviction := obj.(*lifecycle.Eviction)
newEviction.Status = oldEviction.Status
// Spec updates bump the generation.
if !apiequality.Semantic.DeepEqual(oldEviction.Spec, newEviction.Spec) {
newEviction.Generation = oldEviction.Generation + 1
}
}
// Validate validates a new Eviction.
func (s *evictionStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
eviction := obj.(*lifecycle.Eviction)
allErrs := validation.ValidateEviction(eviction)
return allErrs
}
func (*evictionStrategy) DeclarativeValidationConfig(ctx context.Context, obj, oldObj runtime.Object) rest.DeclarativeValidationConfig {
return rest.DeclarativeValidationConfig{}
}
func (*evictionStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
return nil
}
func (*evictionStrategy) Canonicalize(obj runtime.Object) {
}
func (*evictionStrategy) AllowCreateOnUpdate(ctx context.Context) bool {
return false
}
// ValidateUpdate is the default update validation for an end user.
func (s *evictionStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
var allErrs field.ErrorList
eviction := obj.(*lifecycle.Eviction)
oldEviction := old.(*lifecycle.Eviction)
allErrs = validation.ValidateEvictionUpdate(eviction, oldEviction)
return allErrs
}
func (*evictionStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
return nil
}
func (*evictionStrategy) AllowUnconditionalUpdate(ctx context.Context) bool {
return false
}
// evictionStatusStrategy is the default logic invoked when updating object status.
type evictionStatusStrategy struct {
*evictionStrategy
}
var _ = rest.ResetFieldsStrategy(&evictionStatusStrategy{})
func NewStatusStrategy(strategy *evictionStrategy) *evictionStatusStrategy {
return &evictionStatusStrategy{
strategy,
}
}
// GetResetFields returns the set of fields that get reset by the strategy
// and should not be modified by the user.
func (*evictionStatusStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set {
return map[fieldpath.APIVersion]*fieldpath.Set{
"lifecycle.k8s.io/v1alpha1": fieldpath.NewSet(
fieldpath.MakePathOrDie("spec"),
fieldpath.MakePathOrDie("metadata"),
),
}
}
// PrepareForUpdate clears fields that are not allowed to be set by end users on update of status
func (*evictionStatusStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
newEviction := obj.(*lifecycle.Eviction)
oldEviction := old.(*lifecycle.Eviction)
newEviction.Spec = oldEviction.Spec
// Status updates should not include metadata update privileges, also,
// the eviction-controller should be responsible for the labels
// and not the responders - let's not promote label updates.
metav1.ResetObjectMetaForStatus(&newEviction.ObjectMeta, &oldEviction.ObjectMeta)
}
func (*evictionStatusStrategy) AllowCreateOnUpdate(ctx context.Context) bool {
return false
}
// ValidateUpdate is the default update validation for an end user updating status
func (s *evictionStatusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
allErrs := validation.ValidateEvictionStatusUpdate(obj.(*lifecycle.Eviction), old.(*lifecycle.Eviction), validation.EvictionStatusValidationOptions{
Clock: s.clock,
})
return allErrs
}
// WarningsOnUpdate returns warnings for the given update.
func (*evictionStatusStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
return nil
}
func (*evictionStatusStrategy) AllowUnconditionalUpdate(ctx context.Context) bool {
return false
}

View File

@@ -0,0 +1,296 @@
/*
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 eviction
import (
"testing"
"time"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/authentication/user"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/kubernetes/pkg/apis/lifecycle"
_ "k8s.io/kubernetes/pkg/apis/lifecycle/install"
testing2 "k8s.io/utils/clock/testing"
)
const validUID = "e02c09a7-9226-4ce8-bdd2-fd69c87ae9ed"
func TestEvictionStrategy_ResetFields(t *testing.T) {
strategy := NewStrategy(nil)
for _, fields := range strategy.GetResetFields() {
if !fields.Has(fieldpath.MakePathOrDie("status")) {
t.Errorf("status should be reset on creation and update")
}
}
}
func TestEvictionStrategy(t *testing.T) {
clock := testing2.NewFakePassiveClock(time.Now())
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
APIGroup: "lifecycle.k8s.io",
APIVersion: "v1alpha1",
Resource: "evictions",
IsResourceRequest: true,
Verb: "create",
})
strategy := NewStrategy(clock)
if !strategy.NamespaceScoped() {
t.Errorf("Eviction must be namespace scoped")
}
if strategy.AllowCreateOnUpdate(ctx) {
t.Errorf("Eviction should not allow create on update")
}
if len(strategy.GenerateName("test")) <= len("test") {
t.Errorf("Eviction should implement name generation")
}
if len(strategy.WarningsOnCreate(ctx, nil)) != 0 {
t.Errorf("Eviction warnings on create are expected to be empty")
}
eviction := &lifecycle.Eviction{
ObjectMeta: metav1.ObjectMeta{Name: "bar", Namespace: "foo"},
Spec: lifecycle.EvictionSpec{
Target: lifecycle.EvictionTarget{
Pod: &lifecycle.EvictionPodReference{
UID: validUID,
Name: "foo.pod",
},
},
},
Status: lifecycle.EvictionStatus{
ObservedGeneration: new(int64(1)),
Requesters: []lifecycle.Requester{
{Name: "requester.example.com/bar", Intent: lifecycle.RequesterIntentEviction},
},
TargetResponders: []lifecycle.TargetResponder{
{Name: "test", Priority: new(int32(1000)), State: lifecycle.ResponderStateInactive},
},
},
}
strategy.PrepareForCreate(ctx, eviction)
if eviction.Generation != int64(1) {
t.Error("Eviction metadata.generation should be set to 1")
}
if len(eviction.Status.TargetResponders) != 0 {
t.Error("Eviction should not allow setting status.targetResponders on create")
}
if eviction.Status.ObservedGeneration != nil {
t.Error("Eviction should not allow setting status.observedGeneration on create")
}
errs := strategy.Validate(ctx, eviction)
if len(errs) != 0 {
t.Errorf("Unexpected error validating %v", errs)
}
}
func TestEvictionStrategy_Update(t *testing.T) {
clock := testing2.NewFakePassiveClock(time.Now())
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
APIGroup: "lifecycle.k8s.io",
APIVersion: "v1alpha1",
Resource: "evictions",
IsResourceRequest: true,
Verb: "update",
})
ctx = genericapirequest.WithUser(ctx, &user.DefaultInfo{Name: "other-user"})
strategy := NewStrategy(clock)
if len(strategy.WarningsOnUpdate(ctx, nil, nil)) != 0 {
t.Errorf("Eviction warnings on update are expected to be empty")
}
oldEviction := &lifecycle.Eviction{
ObjectMeta: metav1.ObjectMeta{Name: "bar", Namespace: "foo", Generation: 1, ResourceVersion: "2", UID: "3fa927b6-a79d-43e8-9e49-f4f488c275ad"},
Spec: lifecycle.EvictionSpec{
Target: lifecycle.EvictionTarget{
Pod: &lifecycle.EvictionPodReference{
UID: validUID,
Name: "foo.pod",
},
},
},
Status: lifecycle.EvictionStatus{
ObservedGeneration: new(int64(1)),
Requesters: []lifecycle.Requester{
{Name: "requester-1.example.com/bar", Intent: lifecycle.RequesterIntentEviction},
{Name: "requester-2.example.com/bar", Intent: lifecycle.RequesterIntentWithdrawn},
},
},
}
newEviction := &lifecycle.Eviction{
ObjectMeta: metav1.ObjectMeta{Name: "bar", Namespace: "foo", ResourceVersion: "2"},
Spec: lifecycle.EvictionSpec{
Target: lifecycle.EvictionTarget{
Pod: &lifecycle.EvictionPodReference{
UID: validUID,
Name: "bar.pod",
},
},
},
Status: lifecycle.EvictionStatus{
ObservedGeneration: new(int64(10)),
Requesters: []lifecycle.Requester{
{Name: "requester-1.example.com/bar", Intent: lifecycle.RequesterIntentEviction},
{Name: "requester-2.example.com/bar", Intent: lifecycle.RequesterIntentEviction},
},
TargetResponders: []lifecycle.TargetResponder{
{Name: "test", Priority: new(int32(1000)), State: lifecycle.ResponderStateCanceled},
},
},
}
strategy.PrepareForUpdate(ctx, newEviction, oldEviction)
if newEviction.Generation != int64(2) {
t.Error("Eviction metadata.generation should be set to 2")
}
if len(newEviction.Status.TargetResponders) != 0 {
t.Error("Eviction should not allow setting status.targetResponders on update")
}
errs := strategy.ValidateUpdate(ctx, newEviction, oldEviction)
if len(errs) == 0 {
t.Errorf("Expected a validation error")
}
newEviction.UID = oldEviction.UID
errs = strategy.ValidateUpdate(ctx, newEviction, oldEviction)
if len(errs) != 0 {
t.Errorf("Unexpected error validating %v", errs)
}
}
func TestEvictionStatusStrategy_ResetFields(t *testing.T) {
strategy := NewStrategy(nil)
statusStrategy := NewStatusStrategy(strategy)
for _, fields := range statusStrategy.GetResetFields() {
if !fields.Has(fieldpath.MakePathOrDie("spec")) {
t.Errorf("spec should be reset on status update")
}
if !fields.Has(fieldpath.MakePathOrDie("metadata")) {
t.Errorf("metadata should be reset on status update")
}
}
}
func TestEvictionStatusStrategy(t *testing.T) {
clock := testing2.NewFakePassiveClock(time.Now())
strategy := NewStatusStrategy(NewStrategy(clock))
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
APIGroup: "lifecycle.k8s.io",
APIVersion: "v1alpha1",
Resource: "evictions",
IsResourceRequest: true,
Verb: "update",
Subresource: "status",
})
oldEviction := &lifecycle.Eviction{
ObjectMeta: metav1.ObjectMeta{Name: "bar", Namespace: "foo", Generation: 2, ResourceVersion: "2",
Annotations: map[string]string{"test": "true"},
Labels: map[string]string{"foo": "bar"}},
Spec: lifecycle.EvictionSpec{
Target: lifecycle.EvictionTarget{
Pod: &lifecycle.EvictionPodReference{
UID: validUID,
Name: "foo.pod",
},
},
},
Status: lifecycle.EvictionStatus{
ObservedGeneration: new(int64(2)),
Conditions: []metav1.Condition{
{
Type: "Failed",
Status: metav1.ConditionFalse,
LastTransitionTime: metav1.Now(),
Reason: "reason",
Message: "message",
ObservedGeneration: 1,
},
},
},
}
newEviction := &lifecycle.Eviction{
ObjectMeta: metav1.ObjectMeta{Name: "bar", Namespace: "foo", Generation: 2, ResourceVersion: "2",
Annotations: map[string]string{"test": "false"},
Labels: map[string]string{"foo": "baz"}},
Spec: lifecycle.EvictionSpec{
Target: lifecycle.EvictionTarget{
Pod: &lifecycle.EvictionPodReference{
UID: validUID,
Name: "foo.update",
},
},
},
Status: lifecycle.EvictionStatus{
ObservedGeneration: new(int64(2)),
Requesters: []lifecycle.Requester{
{Name: "requester-1.example.com/bar", Intent: lifecycle.RequesterIntentEviction},
{Name: "requester-2.example.com/bar", Intent: lifecycle.RequesterIntentEviction},
},
TargetResponders: []lifecycle.TargetResponder{
{Name: "responder1.example.com/bar", Priority: new(int32(1000)), State: lifecycle.ResponderStateActive},
},
Responders: []lifecycle.ResponderStatus{
{Name: "responder1.example.com/bar", Message: new("test message"), StartTime: new(metav1.Now())},
},
Conditions: []metav1.Condition{
{
Type: "Failed",
Status: metav1.ConditionFalse,
LastTransitionTime: metav1.Now(),
Reason: "reason",
Message: "message",
ObservedGeneration: 1,
},
{
Type: "Failed",
Status: metav1.ConditionFalse,
LastTransitionTime: metav1.Now(),
Reason: "reason",
Message: "message",
ObservedGeneration: 1,
},
},
},
}
strategy.PrepareForUpdate(ctx, newEviction, oldEviction)
if newEviction.Spec.Target.Pod.Name != "foo.pod" {
t.Error("Eviction spec.target.pod.name should not be updated")
}
if newEviction.Labels["foo"] != "bar" {
t.Error("Eviction should not allow changing labels")
}
if newEviction.Annotations["test"] != "true" {
t.Error("Eviction should not allow changing annotations")
}
errs := strategy.ValidateUpdate(ctx, newEviction, oldEviction)
if len(errs) == 0 {
t.Errorf("Expected a validation error")
}
newEviction.Status.Conditions = newEviction.Status.Conditions[:1]
errs = strategy.ValidateUpdate(ctx, newEviction, oldEviction)
if len(errs) != 0 {
t.Errorf("Unexpected error validating %v", errs)
}
}

View File

@@ -0,0 +1,17 @@
/*
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

View File

@@ -0,0 +1,106 @@
/*
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 storage
import (
"context"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/registry/rest"
lifecycleapi "k8s.io/kubernetes/pkg/apis/lifecycle"
"k8s.io/kubernetes/pkg/printers"
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
printerstorage "k8s.io/kubernetes/pkg/printers/storage"
"k8s.io/kubernetes/pkg/registry/lifecycle/evictionrequest"
)
// REST implements a RESTStorage for evictionrequests against etcd
type REST struct {
*genericregistry.Store
}
// NewREST returns a RESTStorage object that will work against evictionrequests.
func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, *StatusREST, error) {
strategy := evictionrequest.NewStrategy()
store := &genericregistry.Store{
NewFunc: func() runtime.Object { return &lifecycleapi.EvictionRequest{} },
NewListFunc: func() runtime.Object { return &lifecycleapi.EvictionRequestList{} },
DefaultQualifiedResource: lifecycleapi.Resource("evictionrequests"),
SingularQualifiedResource: lifecycleapi.Resource("evictionrequest"),
CreateStrategy: strategy,
UpdateStrategy: strategy,
DeleteStrategy: strategy,
ResetFieldsStrategy: strategy,
TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)},
}
options := &generic.StoreOptions{RESTOptions: optsGetter}
if err := store.CompleteWithOptions(options); err != nil {
return nil, nil, err
}
statusStrategy := evictionrequest.NewStatusStrategy(strategy)
statusStore := *store
statusStore.UpdateStrategy = statusStrategy
statusStore.ResetFieldsStrategy = statusStrategy
return &REST{store}, &StatusREST{store: &statusStore}, nil
}
// StatusREST implements the REST endpoint for changing the status of evictionrequests.
type StatusREST struct {
store *genericregistry.Store
}
// New creates a new EvictionRequest object.
func (r *StatusREST) New() runtime.Object {
return &lifecycleapi.EvictionRequest{}
}
// Destroy cleans up resources on shutdown.
func (r *StatusREST) Destroy() {
// Given that underlying store is shared with REST,
// we don't destroy it here explicitly.
}
// Get retrieves the object from the storage. It is required to support Patch.
func (r *StatusREST) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
return r.store.Get(ctx, name, options)
}
// Update alters the status subset of an object.
func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
// We are explicitly setting forceAllowCreate to false in the call to the underlying storage because
// subresources should never allow create on update.
return r.store.Update(ctx, name, objInfo, createValidation, updateValidation, false, options)
}
// GetResetFields implements rest.ResetFieldsStrategy
func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set {
return r.store.GetResetFields()
}
func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
return r.store.ConvertToTable(ctx, object, tableOptions)
}

View File

@@ -0,0 +1,263 @@
/*
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 storage
import (
"context"
"testing"
"k8s.io/utils/ptr"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/authentication/user"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/generic"
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
"k8s.io/apiserver/pkg/registry/rest"
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
"k8s.io/kubernetes/pkg/apis/lifecycle"
"k8s.io/kubernetes/pkg/registry/registrytest"
)
const validUID = "c88f9680-e6bc-4b18-9a4c-eed4292c4de9"
func newStorage(t *testing.T) (*REST, *StatusREST, *etcd3testing.EtcdTestServer) {
etcdStorage, server := registrytest.NewEtcdStorageForResource(t, lifecycle.SchemeGroupVersion.WithResource("evictionrequests").GroupResource())
restOptions := generic.RESTOptions{
StorageConfig: etcdStorage,
Decorator: generic.UndecoratedStorage,
DeleteCollectionWorkers: 1,
ResourcePrefix: "evictionrequests",
}
evictionRequestStorage, evictionRequestStatusStorage, err := NewREST(restOptions)
if err != nil {
t.Fatalf("unexpected error from REST storage: %v", err)
}
return evictionRequestStorage, evictionRequestStatusStorage, server
}
func tester(t *testing.T, storage *REST) *genericregistrytest.Tester {
test := genericregistrytest.New(t, storage.Store)
requestInfo := &genericapirequest.RequestInfo{
APIGroup: "lifecycle.k8s.io",
APIVersion: "v1alpha1",
Resource: "evictionrequests",
}
test.SetRequestInfo(requestInfo)
test.SetUserInfo(&user.DefaultInfo{Name: "test"})
return test
}
func newValidEvictionRequest() *lifecycle.EvictionRequest {
return &lifecycle.EvictionRequest{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: metav1.NamespaceDefault,
},
Spec: lifecycle.EvictionRequestSpec{
Target: lifecycle.EvictionRequestTarget{
Pod: &lifecycle.EvictionRequestPodReference{
UID: validUID,
Name: "foo.pod",
},
},
Requester: "requester-1.example.com/bar",
Intent: lifecycle.EvictionRequestIntentEviction,
},
Status: lifecycle.EvictionRequestStatus{
ObservedGeneration: new(int64(1)),
},
}
}
func TestCreate(t *testing.T) {
storage, _, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := tester(t, storage)
validEvictionRequest := newValidEvictionRequest()
validEvictionRequest.ObjectMeta = metav1.ObjectMeta{}
invalidEvictionRequest := newValidEvictionRequest()
invalidEvictionRequest.ObjectMeta = metav1.ObjectMeta{Name: "-foo"}
test.TestCreate(
validEvictionRequest,
invalidEvictionRequest,
)
}
func TestUpdate(t *testing.T) {
storage, _, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := tester(t, storage)
validEvictionRequest := newValidEvictionRequest()
test.TestUpdate(
validEvictionRequest,
func(obj runtime.Object) runtime.Object {
object := obj.(*lifecycle.EvictionRequest)
object.ObjectMeta.Annotations = map[string]string{"foo": "bar"}
return object
},
// invalid updateFunc
func(obj runtime.Object) runtime.Object {
object := obj.(*lifecycle.EvictionRequest)
object.Spec.Target.Pod.Name = "bar"
return object
},
)
}
func TestDelete(t *testing.T) {
storage, _, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := tester(t, storage)
test.TestDelete(newValidEvictionRequest())
}
func TestGet(t *testing.T) {
storage, _, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := tester(t, storage)
test.TestGet(newValidEvictionRequest())
}
func TestList(t *testing.T) {
storage, _, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := tester(t, storage)
test.TestList(newValidEvictionRequest())
}
func TestWatch(t *testing.T) {
storage, _, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := tester(t, storage)
test.TestWatch(
newValidEvictionRequest(),
// matching labels
[]labels.Set{},
// not matching labels
[]labels.Set{
{"x": "y"},
},
// matching fields
[]fields.Set{},
// not matching fields
[]fields.Set{
{"metadata.name": "xyz"},
{"name": "foo"},
},
)
}
func TestStatusUpdate(t *testing.T) {
storage, statusStorage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
evictionRequest := newValidEvictionRequest()
evictionRequest.Status = lifecycle.EvictionRequestStatus{}
ctx := evictionRequestContext()
key, err := storage.KeyFunc(ctx, "foo")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
result := &lifecycle.EvictionRequest{}
if err := storage.Storage.Create(ctx, key, evictionRequest, result, 0, false); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Status.ObservedGeneration != nil {
t.Errorf("we expected .status.observedGeneration to be nil but it was %v", *result.Status.ObservedGeneration)
}
evictionRequestUpdate := newValidEvictionRequest()
evictionRequestUpdate.ObjectMeta = result.ObjectMeta
evictionRequestUpdate.Labels = map[string]string{"foo": "bar"}
evictionRequestUpdate.Spec.Target.Pod.Name = "bax"
evictionRequestUpdate.Status.ObservedGeneration = new(int64(1))
if _, _, err := statusStorage.Update(ctx, evictionRequestUpdate.Name, rest.DefaultUpdatedObjectInfo(evictionRequestUpdate), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
obj, err := storage.Get(ctx, evictionRequestUpdate.Name, &metav1.GetOptions{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
result = obj.(*lifecycle.EvictionRequest)
if len(result.Labels) != 0 {
t.Errorf("we expected .status.labels to be empty but it was %v", result.Labels)
}
if result.Spec.Target.Pod.Name != "foo.pod" {
t.Errorf("we expected .spec.target.pod.name to not be updated but it was updated to %v", result.Spec.Target.Pod.Name)
}
if ptr.Deref(result.Status.ObservedGeneration, 0) != 1 {
t.Errorf("we expected .status.observedGeneration to be updated to but it was %v", ptr.Deref(result.Status.ObservedGeneration, -1))
}
}
func TestGenerationNumber(t *testing.T) {
storage, statusStorage, server := newStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
evictionRequest := newValidEvictionRequest()
evictionRequest.Generation = 100
evictionRequest.Status.ObservedGeneration = new(int64(10))
ctx := evictionRequestContext()
resultObj, err := storage.Create(ctx, evictionRequest, rest.ValidateAllObjectFunc, &metav1.CreateOptions{})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
result, _ := resultObj.(*lifecycle.EvictionRequest)
// Generation initialization
if result.Generation != 1 || result.Status.ObservedGeneration != nil {
t.Fatalf("Unexpected generation number %v, status generation %v", result.Generation, result.Status.ObservedGeneration)
}
// Updates to status should not increment either spec or status generation numbers
result.Status.Conditions = append(result.Status.Conditions, metav1.Condition{Type: "Test", Status: "True", LastTransitionTime: metav1.Now(), Reason: "Reason"})
if _, _, err := statusStorage.Update(ctx, result.Name, rest.DefaultUpdatedObjectInfo(result), rest.ValidateAllObjectFunc, rest.ValidateAllObjectUpdateFunc, false, &metav1.UpdateOptions{}); err != nil {
t.Errorf("unexpected error: %v", err)
}
resultObj, err = storage.Get(ctx, result.Name, &metav1.GetOptions{})
if err != nil {
t.Errorf("unexpected error: %v", err)
}
result, _ = resultObj.(*lifecycle.EvictionRequest)
if result.Generation != 1 || result.Status.ObservedGeneration != nil {
t.Fatalf("Unexpected generation number, spec: %v, status: %v", result.Generation, result.Status.ObservedGeneration)
}
}
func evictionRequestContext() context.Context {
ctx := genericapirequest.WithNamespace(genericapirequest.NewContext(), metav1.NamespaceDefault)
ctx = genericapirequest.WithRequestInfo(ctx, &genericapirequest.RequestInfo{
APIGroup: "lifecycle.k8s.io",
APIVersion: "v1alpha1",
Resource: "evictionrequests",
})
ctx = genericapirequest.WithUser(ctx, &user.DefaultInfo{Name: "test"})
return ctx
}

View File

@@ -0,0 +1,177 @@
/*
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 (
"context"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/apiserver/pkg/storage/names"
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/apis/lifecycle"
"k8s.io/kubernetes/pkg/apis/lifecycle/validation"
)
// evictionRequestStrategy is the default logic that applies when creating and updating EvictionRequest objects.
type evictionRequestStrategy struct {
rest.DeclarativeValidation
names.NameGenerator
}
func NewStrategy() *evictionRequestStrategy {
return &evictionRequestStrategy{
rest.DeclarativeValidation{Scheme: legacyscheme.Scheme},
names.SimpleNameGenerator,
}
}
var _ = rest.ResetFieldsStrategy(&evictionRequestStrategy{})
func (*evictionRequestStrategy) NamespaceScoped() bool {
return true
}
// GetResetFields returns the set of fields that get reset by the strategy
// and should not be modified by the user.
func (*evictionRequestStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set {
fields := map[fieldpath.APIVersion]*fieldpath.Set{
"lifecycle.k8s.io/v1alpha1": fieldpath.NewSet(
fieldpath.MakePathOrDie("status"),
),
}
return fields
}
// PrepareForCreate clears fields that are not allowed to be set by end users on creation.
func (*evictionRequestStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
evictionRequest := obj.(*lifecycle.EvictionRequest)
evictionRequest.Status = lifecycle.EvictionRequestStatus{}
evictionRequest.Generation = 1
}
// PrepareForUpdate clears fields that are not allowed to be set by end users on update.
func (*evictionRequestStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
oldEvictionRequest := old.(*lifecycle.EvictionRequest)
newEvictionRequest := obj.(*lifecycle.EvictionRequest)
newEvictionRequest.Status = oldEvictionRequest.Status
// Spec updates bump the generation.
if !apiequality.Semantic.DeepEqual(oldEvictionRequest.Spec, newEvictionRequest.Spec) {
newEvictionRequest.Generation = oldEvictionRequest.Generation + 1
}
}
// Validate validates a new EvictionRequest.
func (s *evictionRequestStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
evictionRequest := obj.(*lifecycle.EvictionRequest)
allErrs := validation.ValidateEvictionRequest(evictionRequest)
return allErrs
}
func (*evictionRequestStrategy) DeclarativeValidationConfig(ctx context.Context, obj, oldObj runtime.Object) rest.DeclarativeValidationConfig {
return rest.DeclarativeValidationConfig{}
}
func (*evictionRequestStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
return nil
}
func (*evictionRequestStrategy) Canonicalize(obj runtime.Object) {
}
func (*evictionRequestStrategy) AllowCreateOnUpdate(ctx context.Context) bool {
return false
}
// ValidateUpdate is the default update validation for an end user.
func (s *evictionRequestStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
var allErrs field.ErrorList
evictionRequest := obj.(*lifecycle.EvictionRequest)
oldEvictionRequest := old.(*lifecycle.EvictionRequest)
allErrs = validation.ValidateEvictionRequestUpdate(evictionRequest, oldEvictionRequest)
return allErrs
}
func (*evictionRequestStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
return nil
}
func (*evictionRequestStrategy) AllowUnconditionalUpdate(ctx context.Context) bool {
return false
}
// evictionRequestStatusStrategy is the default logic invoked when updating object status.
type evictionRequestStatusStrategy struct {
*evictionRequestStrategy
}
var _ = rest.ResetFieldsStrategy(&evictionRequestStatusStrategy{})
func NewStatusStrategy(strategy *evictionRequestStrategy) *evictionRequestStatusStrategy {
return &evictionRequestStatusStrategy{
strategy,
}
}
// GetResetFields returns the set of fields that get reset by the strategy
// and should not be modified by the user.
func (*evictionRequestStatusStrategy) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set {
return map[fieldpath.APIVersion]*fieldpath.Set{
"lifecycle.k8s.io/v1alpha1": fieldpath.NewSet(
fieldpath.MakePathOrDie("spec"),
fieldpath.MakePathOrDie("metadata"),
),
}
}
// PrepareForUpdate clears fields that are not allowed to be set by end users on update of status
func (*evictionRequestStatusStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
newEvictionRequest := obj.(*lifecycle.EvictionRequest)
oldEvictionRequest := old.(*lifecycle.EvictionRequest)
// Status updates should not include metadata update privileges, also,
// the evictionrequest-controller should be responsible for the labels
// and not the responders - let's not promote label updates.
metav1.ResetObjectMetaForStatus(&newEvictionRequest.ObjectMeta, &oldEvictionRequest.ObjectMeta)
newEvictionRequest.Spec = oldEvictionRequest.Spec
}
func (*evictionRequestStatusStrategy) AllowCreateOnUpdate(ctx context.Context) bool {
return false
}
// ValidateUpdate is the default update validation for an end user updating status
func (s *evictionRequestStatusStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
allErrs := validation.ValidateEvictionRequestStatusUpdate(obj.(*lifecycle.EvictionRequest), old.(*lifecycle.EvictionRequest))
return allErrs
}
// WarningsOnUpdate returns warnings for the given update.
func (*evictionRequestStatusStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
return nil
}
func (*evictionRequestStatusStrategy) AllowUnconditionalUpdate(ctx context.Context) bool {
return false
}

View File

@@ -0,0 +1,271 @@
/*
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"
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiserver/pkg/authentication/user"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/kubernetes/pkg/apis/lifecycle"
_ "k8s.io/kubernetes/pkg/apis/lifecycle/install"
)
const validUID = "f27eb9ee-c41a-4afd-96a1-a80b4675a8c7"
func TestEvictionRequestStrategy_ResetFields(t *testing.T) {
strategy := NewStrategy()
for _, fields := range strategy.GetResetFields() {
if !fields.Has(fieldpath.MakePathOrDie("status")) {
t.Errorf("status should be reset on creation and update")
}
}
}
func TestEvictionRequestStrategy(t *testing.T) {
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
APIGroup: "lifecycle.k8s.io",
APIVersion: "v1alpha1",
Resource: "evictionrequests",
IsResourceRequest: true,
Verb: "create",
})
ctx = genericapirequest.WithUser(ctx, &user.DefaultInfo{Name: "test"})
strategy := NewStrategy()
if !strategy.NamespaceScoped() {
t.Errorf("EvictionRequest must be namespace scoped")
}
if strategy.AllowCreateOnUpdate(ctx) {
t.Errorf("EvictionRequest should not allow create on update")
}
if len(strategy.GenerateName("test")) <= len("test") {
t.Errorf("Eviction should implement name generation")
}
if len(strategy.WarningsOnCreate(ctx, nil)) != 0 {
t.Errorf("EvictionRequest warnings on create are expected to be empty")
}
evictionRequest := &lifecycle.EvictionRequest{
ObjectMeta: metav1.ObjectMeta{Name: "bar", Namespace: "foo"},
Spec: lifecycle.EvictionRequestSpec{
Target: lifecycle.EvictionRequestTarget{
Pod: &lifecycle.EvictionRequestPodReference{
UID: validUID,
Name: "foo.pod",
},
},
Requester: "requester.domain/requester1",
Intent: lifecycle.EvictionRequestIntentEviction,
},
Status: lifecycle.EvictionRequestStatus{
ObservedGeneration: new(int64(1)),
},
}
strategy.PrepareForCreate(ctx, evictionRequest)
if evictionRequest.Generation != int64(1) {
t.Error("EvictionRequest metadata.generation should be set to 1")
}
if evictionRequest.Status.ObservedGeneration != nil {
t.Error("EvictionRequest should not allow setting status.observedGeneration on create")
}
errs := strategy.Validate(ctx, evictionRequest)
if len(errs) != 0 {
t.Errorf("Unexpected error validating %v", errs)
}
}
func TestEvictionRequestStrategy_Update(t *testing.T) {
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
APIGroup: "lifecycle.k8s.io",
APIVersion: "v1alpha1",
Resource: "evictionrequests",
IsResourceRequest: true,
Verb: "update",
})
ctx = genericapirequest.WithUser(ctx, &user.DefaultInfo{Name: "test"})
strategy := NewStrategy()
if len(strategy.WarningsOnUpdate(ctx, nil, nil)) != 0 {
t.Errorf("EvictionRequest warnings on update are expected to be empty")
}
oldEvictionRequest := &lifecycle.EvictionRequest{
ObjectMeta: metav1.ObjectMeta{Name: "bar", Namespace: "foo", Generation: 1, ResourceVersion: "2", UID: "8fcabaef-2e66-459a-b6dc-c5b4c295b89d"},
Spec: lifecycle.EvictionRequestSpec{
Target: lifecycle.EvictionRequestTarget{
Pod: &lifecycle.EvictionRequestPodReference{
UID: validUID,
Name: "foo.pod",
},
},
Requester: "requester.domain/requester1",
Intent: lifecycle.EvictionRequestIntentWithdrawn,
},
Status: lifecycle.EvictionRequestStatus{
ObservedGeneration: new(int64(1)),
},
}
newEvictionRequest := &lifecycle.EvictionRequest{
ObjectMeta: metav1.ObjectMeta{Name: "bar", Namespace: "foo", ResourceVersion: "2"},
Spec: lifecycle.EvictionRequestSpec{
Target: lifecycle.EvictionRequestTarget{
Pod: &lifecycle.EvictionRequestPodReference{
UID: validUID,
Name: "bar.pod",
},
},
Requester: "requester.domain/requester1",
Intent: lifecycle.EvictionRequestIntentEviction,
},
Status: lifecycle.EvictionRequestStatus{
ObservedGeneration: new(int64(10)),
},
}
strategy.PrepareForUpdate(ctx, newEvictionRequest, oldEvictionRequest)
if newEvictionRequest.Generation != int64(2) {
t.Error("EvictionRequest metadata.generation should be set to 2")
}
errs := strategy.ValidateUpdate(ctx, newEvictionRequest, oldEvictionRequest)
if len(errs) == 0 {
t.Errorf("Expected a validation error")
}
newEvictionRequest.UID = oldEvictionRequest.UID
errs = strategy.ValidateUpdate(ctx, newEvictionRequest, oldEvictionRequest)
if len(errs) != 0 {
t.Errorf("Unexpected error validating %v", errs)
}
}
func TestEvictionRequestStatusStrategy_ResetFields(t *testing.T) {
strategy := NewStrategy()
statusStrategy := NewStatusStrategy(strategy)
for _, fields := range statusStrategy.GetResetFields() {
if !fields.Has(fieldpath.MakePathOrDie("spec")) {
t.Errorf("spec should be reset on status update")
}
if !fields.Has(fieldpath.MakePathOrDie("metadata")) {
t.Errorf("metadata should be reset on status update")
}
}
}
func TestEvictionRequestStatusStrategy(t *testing.T) {
strategy := NewStatusStrategy(NewStrategy())
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
APIGroup: "lifecycle.k8s.io",
APIVersion: "v1alpha1",
Resource: "evictionrequests",
IsResourceRequest: true,
Verb: "update",
Subresource: "status",
})
oldEvictionRequest := &lifecycle.EvictionRequest{
ObjectMeta: metav1.ObjectMeta{Name: "bar", Namespace: "foo", Generation: 2, ResourceVersion: "2",
Annotations: map[string]string{"test": "true"},
Labels: map[string]string{"foo": "bar"}},
Spec: lifecycle.EvictionRequestSpec{
Target: lifecycle.EvictionRequestTarget{
Pod: &lifecycle.EvictionRequestPodReference{
UID: validUID,
Name: "foo.pod",
},
},
Requester: "requester.domain/requester1",
Intent: lifecycle.EvictionRequestIntentEviction,
},
Status: lifecycle.EvictionRequestStatus{
ObservedGeneration: new(int64(1)),
Conditions: []metav1.Condition{
{
Type: "Failed",
Status: metav1.ConditionFalse,
LastTransitionTime: metav1.Now(),
Reason: "reason",
Message: "message",
ObservedGeneration: 1,
},
},
},
}
newEvictionRequest := &lifecycle.EvictionRequest{
ObjectMeta: metav1.ObjectMeta{Name: "bar", Namespace: "foo", Generation: 2, ResourceVersion: "2",
Annotations: map[string]string{"test": "false"},
Labels: map[string]string{"foo": "baz"}},
Spec: lifecycle.EvictionRequestSpec{
Target: lifecycle.EvictionRequestTarget{
Pod: &lifecycle.EvictionRequestPodReference{
UID: validUID,
Name: "foo.pod",
},
},
Requester: "requester.domain/requester1",
Intent: lifecycle.EvictionRequestIntentWithdrawn,
},
Status: lifecycle.EvictionRequestStatus{
ObservedGeneration: new(int64(2)),
Conditions: []metav1.Condition{
{
Type: "Failed",
Status: metav1.ConditionFalse,
LastTransitionTime: metav1.Now(),
Reason: "reason",
Message: "message",
ObservedGeneration: 1,
},
{
Type: "Failed",
Status: metav1.ConditionFalse,
LastTransitionTime: metav1.Now(),
Reason: "reason",
Message: "message",
ObservedGeneration: 1,
},
},
},
}
strategy.PrepareForUpdate(ctx, newEvictionRequest, oldEvictionRequest)
if newEvictionRequest.Spec.Intent != lifecycle.EvictionRequestIntentEviction {
t.Error("EvictionRequest spec.intent should not be updated and have a non Eviction intent")
}
if newEvictionRequest.Labels["foo"] != "bar" {
t.Error("EvictionRequest should not allow changing labels")
}
if newEvictionRequest.Annotations["test"] != "true" {
t.Error("EvictionRequest should not allow changing annotations")
}
errs := strategy.ValidateUpdate(ctx, newEvictionRequest, oldEvictionRequest)
if len(errs) == 0 {
t.Errorf("Expected a validation error")
}
newEvictionRequest.Status.Conditions = newEvictionRequest.Status.Conditions[:1]
errs = strategy.ValidateUpdate(ctx, newEvictionRequest, oldEvictionRequest)
if len(errs) != 0 {
t.Errorf("Unexpected error validating %v", errs)
}
}

View File

@@ -0,0 +1,85 @@
/*
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 rest
import (
lifecyclev1alpha1 "k8s.io/api/lifecycle/v1alpha1"
"k8s.io/apiserver/pkg/registry/generic"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
serverstorage "k8s.io/apiserver/pkg/server/storage"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/klog/v2"
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/apis/lifecycle"
"k8s.io/kubernetes/pkg/features"
evictionstorage "k8s.io/kubernetes/pkg/registry/lifecycle/eviction/storage"
evictionrequeststorage "k8s.io/kubernetes/pkg/registry/lifecycle/evictionrequest/storage"
"k8s.io/utils/clock"
)
type RESTStorageProvider struct {
}
func (p RESTStorageProvider) NewRESTStorage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (genericapiserver.APIGroupInfo, error) {
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(lifecycle.GroupName, legacyscheme.Scheme, legacyscheme.ParameterCodec, legacyscheme.Codecs)
// If you add a version here, be sure to add an entry in `k8s.io/kubernetes/cmd/kube-apiserver/app/aggregator.go with specific priorities.
// TODO refactor the plumbing to provide the information in the APIGroupInfo
if storageMap, err := p.v1alpha1Storage(apiResourceConfigSource, restOptionsGetter); err != nil {
return genericapiserver.APIGroupInfo{}, err
} else if len(storageMap) > 0 {
apiGroupInfo.VersionedResourcesStorageMap[lifecyclev1alpha1.SchemeGroupVersion.Version] = storageMap
}
return apiGroupInfo, nil
}
func (p RESTStorageProvider) v1alpha1Storage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (map[string]rest.Storage, error) {
storage := map[string]rest.Storage{}
if resource := "evictions"; apiResourceConfigSource.ResourceEnabled(lifecyclev1alpha1.SchemeGroupVersion.WithResource(resource)) {
if utilfeature.DefaultFeatureGate.Enabled(features.EvictionRequestAPI) {
evictionStorage, evictionStatusStorage, err := evictionstorage.NewREST(restOptionsGetter, clock.RealClock{})
if err != nil {
return storage, err
}
storage[resource] = evictionStorage
storage[resource+"/status"] = evictionStatusStorage
} else {
klog.Warning("Eviction storage is disabled because the EvictionRequestAPI feature gate is disabled")
}
}
if resource := "evictionrequests"; apiResourceConfigSource.ResourceEnabled(lifecyclev1alpha1.SchemeGroupVersion.WithResource(resource)) {
if utilfeature.DefaultFeatureGate.Enabled(features.EvictionRequestAPI) {
evictionRequestStorage, evictionRequestStatusStorage, err := evictionrequeststorage.NewREST(restOptionsGetter)
if err != nil {
return storage, err
}
storage[resource] = evictionRequestStorage
storage[resource+"/status"] = evictionRequestStatusStorage
} else {
klog.Warning("EvictionRequest storage is disabled because the EvictionRequestAPI feature gate is disabled")
}
}
return storage, nil
}
func (p RESTStorageProvider) GroupName() string {
return lifecycle.GroupName
}

View File

@@ -54,6 +54,7 @@ const (
coordinationGroup = "coordination.k8s.io"
discoveryGroup = "discovery.k8s.io"
extensionsGroup = "extensions"
lifecycleGroup = "lifecycle.k8s.io"
policyGroup = "policy"
rbacGroup = "rbac.authorization.k8s.io"
resourceGroup = "resource.k8s.io"
@@ -152,6 +153,9 @@ func viewRules() []rbacv1.PolicyRule {
if utilfeature.DefaultFeatureGate.Enabled(features.CompositePodGroup) {
rules = append(rules, rbacv1helpers.NewRule(Read...).Groups(schedulingGroup).Resources("compositepodgroups", "compositepodgroups/status").RuleOrDie())
}
if utilfeature.DefaultFeatureGate.Enabled(features.EvictionRequestAPI) {
rules = append(rules, rbacv1helpers.NewRule(Read...).Groups(lifecycleGroup).Resources("evictionrequests", "evictions").RuleOrDie())
}
return rules
}
@@ -199,6 +203,10 @@ func editRules() []rbacv1.PolicyRule {
if utilfeature.DefaultFeatureGate.Enabled(features.CompositePodGroup) {
rules = append(rules, rbacv1helpers.NewRule(Write...).Groups(schedulingGroup).Resources("compositepodgroups").RuleOrDie())
}
if utilfeature.DefaultFeatureGate.Enabled(features.EvictionRequestAPI) {
// "evictions" are in the domain of the system and should be only updated by the evictionrequest-controller. "evictionrequests" are user-scoped.
rules = append(rules, rbacv1helpers.NewRule(Write...).Groups(lifecycleGroup).Resources("evictionrequests").RuleOrDie())
}
return rules
}

View File

@@ -281,6 +281,16 @@ items:
- deletecollection
- patch
- update
- apiGroups:
- lifecycle.k8s.io
resources:
- evictionrequests
verbs:
- create
- delete
- deletecollection
- patch
- update
- apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
@@ -454,6 +464,15 @@ items:
- get
- list
- watch
- apiGroups:
- lifecycle.k8s.io
resources:
- evictionrequests
- evictions
verbs:
- get
- list
- watch
- apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:

View File

@@ -0,0 +1,25 @@
/*
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:protobuf-gen=package
// +k8s:openapi-gen=true
// +k8s:prerelease-lifecycle-gen=true
// +k8s:openapi-model-package=io.k8s.api.lifecycle.v1alpha1
// +groupName=lifecycle.k8s.io
package v1alpha1

View File

@@ -0,0 +1,55 @@
/*
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 (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// GroupName is the group name use in this package
const GroupName = "lifecycle.k8s.io"
// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}
var (
// TODO: move SchemeBuilder with zz_generated.deepcopy.go to k8s.io/api.
// localSchemeBuilder and AddToScheme will stay in k8s.io/kubernetes.
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
localSchemeBuilder = &SchemeBuilder
AddToScheme = localSchemeBuilder.AddToScheme
)
// Adds the list of known types to api.Scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Eviction{},
&EvictionList{},
&EvictionRequest{},
&EvictionRequestList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}

View File

@@ -0,0 +1,680 @@
/*
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 (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apimachinerytypes "k8s.io/apimachinery/pkg/types"
)
const (
// EvictionResponderImperativeEviction is a default responder that will evict pods using the imperative
// Eviction API (pods/<name>/eviction subresource) with a backoff.
EvictionResponderImperativeEviction string = "imperative-eviction.k8s.io/evictor"
)
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.37
// +k8s:supportsSubresource="/status"
// EvictionRequest defines a request that should ideally result in a graceful eviction of a
// .spec.target (e.g. termination of a pod).
//
// The evictionrequest-controller observes intents of all EvictionRequests and transforms them into
// Evictions.
// - .spec.requester is set as a label on the Eviction for easier lookup.
// - Each target can have a set of responders assigned to it. Eviction objects are observed by
// these responders, who implement the eviction logic and update the Eviction's status with
// progress.
//
// There is many-to-many relationship between EvictionRequests and Evictions in general.
// And many-to-one if the target is a pod.
//
// If all requesters withdraw their eviction intent for a common target, the eviction will be
// canceled. Deleting an EvictionRequest also counts as a withdrawal.
// Once all EvictionRequest of a target are removed, the corresponding Evictions are eventually
// garbage collected.
//
// +k8s:validation-gen-nolint // Note: remove this when the API got GA
type EvictionRequest struct {
metav1.TypeMeta `json:",inline"`
// metadata is the standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata.
// +optional
// +k8s:beta(since: "1.37")=+k8s:subfield(name)=+k8s:format=k8s-long-name
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
// spec defines the eviction request specification.
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
// +required
Spec EvictionRequestSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"`
// status represents the most recently observed status of the eviction request.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
// +optional
Status EvictionRequestStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
}
// EvictionRequestSpec is a specification of an EvictionRequest.
type EvictionRequestSpec struct {
// target contains a reference to an object (e.g. a pod) that should be evicted.
// This field is required and immutable.
// +required
// +k8s:immutable
Target EvictionRequestTarget `json:"target" protobuf:"bytes,1,opt,name=target"`
// requester allows you to identify the entity, that requested the eviction of the target.
//
// It must be a valid domain-prefixed key (such as "acme.io/foo").
// Domain names *.k8s.io and *.kubernetes.io are reserved.
// This field is required and immutable.
// +required
// +k8s:required
// +k8s:immutable
// +k8s:format=k8s-prefixed-label-key
// +k8s:customValidation
Requester string `json:"requester" protobuf:"bytes,2,opt,name=requester"`
// intent specifies the action that should be taken for the specified target.
//
// - Eviction means that the requester is interested in the eviction of the target.
// - Withdrawn means that the requester is no longer interested in the eviction of the target.
// If all requesters' intents are withdrawn for a common target, the eviction will be canceled.
// Cancellation consequences:
// - Inactive responders will never run.
// - Active responders are expected to cancel the eviction.
// - Completed or Interrupted responders should not take any action.
// +required
// +k8s:required
Intent EvictionRequestIntent `json:"intent" protobuf:"bytes,3,opt,name=intent,casttype=EvictionRequestIntent"`
}
// EvictionRequestTarget contains a reference to an object that should be evicted.
// +union
type EvictionRequestTarget struct {
// pod references a pod that is subject to eviction/termination.
// Pods that are part of a PodGroup (.spec.schedulingGroup is set) are not supported.
// +optional
// +k8s:optional
// +k8s:unionMember
Pod *EvictionRequestPodReference `json:"pod,omitempty" protobuf:"bytes,1,opt,name=pod"`
}
// EvictionRequestPodReference contains enough information to locate the referenced pod inside the
// same namespace.
type EvictionRequestPodReference struct {
// name of the target.
// This field is required.
// +required
// +k8s:required
// +k8s:format=k8s-long-name
Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
// uid of the target.
// It can be found in .metadata.uid of the target and is a lowercase UUID in 8-4-4-4-12 format.
// This field is required.
// +required
// +k8s:required
// +k8s:format=k8s-uuid
UID apimachinerytypes.UID `json:"uid" protobuf:"bytes,2,opt,name=uid,casttype=k8s.io/apimachinery/pkg/types.UID"`
}
// EvictionRequestIntent specifies a requester intent.
// +enum
// +k8s:enum
type EvictionRequestIntent string
// These are intents that can be set by each requester.
const (
// EvictionRequestIntentEviction means that the requester is interested in the eviction of the target.
EvictionRequestIntentEviction EvictionRequestIntent = "Eviction"
// EvictionRequestIntentWithdrawn means that the requester is no longer interested in the eviction of the target.
// If all requesters' intents are withdrawn for a common target, the eviction will be canceled.
// Cancellation consequences:
// - Inactive responders will never run.
// - Active responders are expected to cancel the eviction.
// - Completed or Interrupted responders should not take any action.
EvictionRequestIntentWithdrawn EvictionRequestIntent = "Withdrawn"
)
// EvictionRequestStatus represents the last observed status of the eviction request.
type EvictionRequestStatus struct {
// conditions contain information about the eviction request.
//
// EvictionRequest specific conditions are: TargetEvicted or Failed (managed by evictionrequest-controller).
// - Failed means that the eviction request is no longer being processed
// by any eviction responder. This can happen if the request is canceled or if no responder
// managed to evict the target (e.g. terminate or delete a pod).
// - TargetEvicted means that the target has been evicted (e.g. a pod has been terminated or deleted).
//
// These conditions can be reset if the eviction was unsuccessful and a new Eviction intent has
// been submitted.
//
// The maximum length of the conditions list is 100.
// +optional
// +patchMergeKey=type
// +patchStrategy=merge
// +listType=map
// +listMapKey=type
// +k8s:optional
// +k8s:alpha(since: "1.37")=+k8s:listType=map
// +k8s:alpha(since: "1.37")=+k8s:listMapKey=type
// +k8s:maxItems=100
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
// observedGeneration is EvictionRequest's .metadata.generation observed by the evictionrequest-controller.
// The observed generation value cannot be negative and can only be incremented.
// The minimum value is 1.
// This field is managed by evictionrequest-controller.
// +optional
// +k8s:optional
// +k8s:minimum=1
// +k8s:update=NoUnset
// +k8s:monotonic
ObservedGeneration *int64 `json:"observedGeneration,omitempty" protobuf:"varint,2,opt,name=observedGeneration"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.37
// EvictionRequestList contains a list of EvictionRequests resources.
type EvictionRequestList struct {
metav1.TypeMeta `json:",inline"`
// metadata is the standard list metadata.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
// +optional
metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
// items is the list of EvictionRequests.
Items []EvictionRequest `json:"items" protobuf:"bytes,2,rep,name=items"`
}
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.37
// +k8s:supportsSubresource="/status"
// Eviction initiates an eviction process, which should ideally result in a graceful eviction of a
// .spec.target (e.g. termination of a pod).
//
// The evictionrequest-controller observes intents of all EvictionRequests and transforms them into
// Evictions. It manages the Eviction lifecycle.
// Requesters are preserved in .status.requesters even after they have withdrawn their request.
// If all requesters withdraw their eviction intent for a common target, the eviction will be
// canceled. Once all EvictionRequest corresponding to this Eviction .spec.target have been
// removed, this Eviction object will eventually be garbage collected.
//
// If the target is a pod, the .status.targetResponders is populated from Pod's
// .spec.evictionResponders.
//
// Responders should observe and communicate through the .status to help with the eviction
// of the target when they see their state == Active in .status.targetResponders. ResponderStatus
// struct should then be periodically updated to indicate the progress or completion of the eviction
// process by each responder in .status.responders. If .status.responders[].heartbeatTime is not
// updated within the heartbeat deadline defined by the Eviction API (currently 20 minutes), the
// eviction is passed over to the next responder with a lower priority.
//
// If there are no other responders and the target is a pod, the last default
// imperative-eviction.k8s.io/evictor responder with a priority of 100 will evict the pod using the
// imperative Eviction API (pods/<name>/eviction subresource).
// +k8s:validation-gen-nolint // Note: remove this when the API got GA
type Eviction struct {
metav1.TypeMeta `json:",inline"`
// metadata is the standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata.
// .metadata.name set by the evictionrequest-controller is purely informative and subject to change.
// .spec.target field should be used to identify the target precisesly.
//
// The requester and responder names will be used as label keys and added to the labels of the
// eviction in one of the following formats:
// 1. acme.io/foo: "requester"
// 2. acme.io/foo: "responder"
// 3. acme.io/foo: "requester-responder"
//
// Please see EvictionParticipantRole for available role label values.
// +optional
// +k8s:beta(since: "1.37")=+k8s:subfield(name)=+k8s:format=k8s-long-name
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
// spec defines the eviction specification.
// https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
// +required
Spec EvictionSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"`
// status represents the most recently observed status of the eviction.
// Populated by responders and evictionrequest-controller.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
// +optional
Status EvictionStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
}
// EvictionParticipantRole specifies a role of an eviction participant intent.
type EvictionParticipantRole string
const (
// EvictionParticipantRoleRequester identifies a requester which creates EvictionRequests.
EvictionParticipantRoleRequester EvictionParticipantRole = "requester"
// EvictionParticipantRoleResponder identifies a responder which responds to an Eviction.
EvictionParticipantRoleResponder EvictionParticipantRole = "responder"
// EvictionParticipantRoleRequesterResponder is both a "requester" and a "responder" at the same time.
EvictionParticipantRoleRequesterResponder EvictionParticipantRole = "requester-responder"
)
// EvictionSpec is a specification of an Eviction.
type EvictionSpec struct {
// target contains a reference to an object (e.g. a pod) that should be evicted.
// This field is required and immutable.
// +required
// +k8s:immutable
Target EvictionTarget `json:"target" protobuf:"bytes,1,opt,name=target"`
}
// EvictionTarget contains a reference to an object that should be evicted.
// +union
type EvictionTarget struct {
// pod references a pod that is subject to eviction/termination.
// Pods that are part of a PodGroup (.spec.schedulingGroup is set) are not supported.
// +optional
// +k8s:optional
// +k8s:unionMember
Pod *EvictionPodReference `json:"pod,omitempty" protobuf:"bytes,1,opt,name=pod"`
}
// EvictionPodReference contains enough information to locate the referenced pod inside the same
// namespace.
type EvictionPodReference struct {
// name of the target.
// This field is required.
// +required
// +k8s:required
// +k8s:format=k8s-long-name
Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
// uid of the target.
// It can be found in .metadata.uid of the target and is a lowercase UUID in 8-4-4-4-12 format.
// This field is required.
// +required
// +k8s:required
// +k8s:format=k8s-uuid
UID apimachinerytypes.UID `json:"uid" protobuf:"bytes,2,opt,name=uid,casttype=k8s.io/apimachinery/pkg/types.UID"`
}
// EvictionStatus represents the last observed status of the eviction request.
type EvictionStatus struct {
// conditions contain information about the eviction request.
//
// Eviction specific conditions are: TargetEvicted or Failed (managed by evictionrequest-controller).
// - Failed means that the eviction request is no longer being processed
// by any eviction responder. This can happen if the request is canceled or if no responder
// managed to evict the target (e.g. terminate or delete a pod).
// - TargetEvicted means that the target has been evicted (e.g. a pod has been terminated or deleted).
//
// The maximum length of the conditions list is 100.
// +optional
// +patchMergeKey=type
// +patchStrategy=merge
// +listType=map
// +listMapKey=type
// +k8s:optional
// +k8s:alpha(since: "1.37")=+k8s:listType=map
// +k8s:alpha(since: "1.37")=+k8s:listMapKey=type
// +k8s:maxItems=100
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
// observedGeneration is Eviction's .metadata.generation observed by the evictionrequest-controller.
// The observed generation value cannot be negative and can only be incremented.
// The minimum value is 1.
// This field is managed by evictionrequest-controller.
// +optional
// +k8s:optional
// +k8s:minimum=1
// +k8s:monotonic
// +k8s:update=NoUnset
ObservedGeneration *int64 `json:"observedGeneration,omitempty" protobuf:"varint,2,opt,name=observedGeneration"`
// requesters allow you to identify the entities, that requested the eviction of the target.
// If all the requesters withdraw their eviction intent, the eviction will be canceled.
//
// The maximum length of the requesters list is 100.
// If this limit is exceeded, requesters with Withdrawn intent should be dropped first.
// +optional
// +patchMergeKey=name
// +patchStrategy=merge
// +listType=map
// +listMapKey=name
// +k8s:optional
// +k8s:listType=map
// +k8s:listMapKey=name
// +k8s:maxItems=100
Requesters []Requester `json:"requesters,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,3,rep,name=requesters"`
// targetResponders reference responders that should eventually respond to this eviction
// to help with the graceful eviction of a target. These responders are selected sequentially,
// according to their specified priority by setting the Active state to the TargetResponder
// .state field. The maximum number of active responders allowed is 1.
// Eventually each responder can end up in an Interrupted, Canceled or, Completed state.
// Responders should observe these states in order to navigate their lifecycle.
//
// If the target is a pod, the field is populated from Pod's .spec.evictionResponders. Default
// responders may be added to the list according to the target.
//
// Default responders:
// - imperative-eviction.k8s.io/evictor responder with a priority of 100 is added to the list if the
// target is a pod. It will call the imperative Eviction API (pods/<name>/eviction subresource).
// This call may not succeed due to PodDisruptionBudgets, which may block the pod termination.
// It will update the responder message and try again with a backoff.
//
// The maximum length of the responders list is 11.
// The length and keys of the list cannot change once set.
// This field is managed by evictionrequest-controller.
// +optional
// +patchMergeKey=name
// +patchStrategy=merge
// +listType=map
// +listMapKey=name
// +k8s:optional
// +k8s:listType=map
// +k8s:listMapKey=name
// +k8s:maxItems=11
TargetResponders []TargetResponder `json:"targetResponders,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,4,rep,name=targetResponders"`
// responders represents the eviction process status of each declared responder.
//
// The responder list should be the same length and have the same .name fields as
// .status.targetResponders. Only responders with .name that have Active state in
// .targetResponders[].state should be updated and can be mutated. First initialization
// of the list is allowed.
//
// Each ResponderStatus is initialized by evictionrequest-controller and then managed by
// the designated responder.
// +optional
// +patchMergeKey=name
// +patchStrategy=merge
// +listType=map
// +listMapKey=name
// +k8s:optional
// +k8s:listType=map
// +k8s:listMapKey=name
// +k8s:maxItems=11
Responders []ResponderStatus `json:"responders,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,5,rep,name=responders"`
}
type EvictionConditionType string
// These are built-in conditions of an eviction request.
const (
// EvictionConditionFailed means that the eviction request is no longer being processed
// by any eviction responder. This can happen if the request is canceled or if no responder
// managed to evict the target (e.g. terminate or delete a pod).
EvictionConditionFailed EvictionConditionType = "Failed"
// EvictionConditionTargetEvicted means that the target has been evicted (e.g. a pod has been
// terminated or deleted).
EvictionConditionTargetEvicted EvictionConditionType = "TargetEvicted"
)
type EvictionConditionReason string
// These are built-in condition reasons of an eviction request.
const (
// EvictionConditionReasonAwaitingEviction means that this Eviction works as expected and the target
// is scheduled for an eviction.
// This reason is set for the Failed and TargetEvicted condition.
EvictionConditionReasonAwaitingEviction EvictionConditionReason = "AwaitingEviction"
// EvictionConditionReasonEvictionInvalid means that the Eviction is not accepted because the
// initial configuration is not valid.
// This reason is set for the Failed condition.
EvictionConditionReasonEvictionInvalid EvictionConditionReason = "EvictionInvalid"
// EvictionConditionReasonCanceledDueToNoRequesters means that the Eviction is canceled because there is no
// EvictionRequest with the same target and Eviction intent in .spec.intent.
// This reason is set for the Failed condition.
EvictionConditionReasonCanceledDueToNoRequesters EvictionConditionReason = "CanceledDueToNoRequesters"
// EvictionConditionReasonSucceeded means that the Eviction has successfully evicted the target.
// This reason is set for the Failed condition.
EvictionConditionReasonSucceeded EvictionConditionReason = "Succeeded"
// EvictionConditionReasonNoFurtherResponder means that the Eviction responders failed to evict
// the target and that no further responder is available.
// This reason is set for the Failed condition.
EvictionConditionReasonNoFurtherResponder EvictionConditionReason = "NoFurtherResponder"
// EvictionConditionReasonPodDeleted means that the target pod has been deleted.
// This reason is set for the TargetEvicted condition.
EvictionConditionReasonPodDeleted EvictionConditionReason = "PodDeleted"
// EvictionConditionReasonPodTerminal means that the target pod has reached a terminal state.
// This reason is set for the TargetEvicted condition.
EvictionConditionReasonPodTerminal EvictionConditionReason = "PodTerminal"
// EvictionConditionReasonEvictionFailed means that the eviction of the target was unsuccessful.
// This reason is set for the TargetEvicted condition.
EvictionConditionReasonEvictionFailed EvictionConditionReason = "EvictionFailed"
)
// Requester allows you to identify the entity, that requested the eviction of the target.
// +structType=atomic
type Requester struct {
// name allows you to identify the entity, that requested the eviction of the target.
//
// It must be a valid domain-prefixed key (such as "acme.io/foo").
// This field must be unique for each requester.
// This field is required.
// +required
// +k8s:required
// +k8s:format=k8s-prefixed-label-key
Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
// intent specifies the action that should be taken for the specified target.
//
// - Eviction means that the requester is interested in the eviction of the target.
// - Withdrawn means that the requester is no longer interested in the eviction of the target.
// If all requesters' intents are withdrawn, the eviction will be canceled.
// Cancellation consequences:
// - Inactive responders will never run.
// - Active responders are expected to cancel the eviction.
// - Completed or Interrupted responders should not take any action.
// +required
// +k8s:required
Intent RequesterIntent `json:"intent" protobuf:"bytes,2,opt,name=intent,casttype=RequesterIntent"`
}
// RequesterIntent specifies a requester intent.
// +enum
// +k8s:enum
type RequesterIntent string
// These are intents that can be set by each requester.
const (
// RequesterIntentEviction means that the requester is interested in the eviction of the target.
RequesterIntentEviction RequesterIntent = "Eviction"
// RequesterIntentWithdrawn means that the requester is no longer interested in the eviction of the target.
// If all requesters' intents are withdrawn, the eviction will be canceled.
// Cancellation consequences:
// - Inactive responders will never run.
// - Active responders are expected to cancel the eviction.
// - Completed or Interrupted responders should not take any action.
RequesterIntentWithdrawn RequesterIntent = "Withdrawn"
)
// TargetResponder allows you to specify the responder reacting to the Eviction.
// Responders should observe and communicate through the Eviction API (see .state) to help
// with the graceful eviction of a target (e.g. termination of a pod).
// +structType=atomic
type TargetResponder struct {
// name allows you to identify the responder reacting to the Eviction.
//
// It must be a valid domain-prefixed key (such as "acme.io/foo").
// This field must be unique for each responder.
// This field is required.
// +required
// +k8s:required
// +k8s:format=k8s-prefixed-label-key
Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
// priority for this responder. Higher priorities are selected first by the evictionrequest-controller.
// 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.
//
// The responder that is the managing controller of the pod should set the value of
// this field to 10000 to allow both for preemption or fallback registration by other
// responders.
//
// The minimum value is 0 and the maximum value is 100000.
// The interval 0-999 is reserved for responders with *.k8s.io suffix.
// This field is required and immutable.
// +required
// +k8s:required
// +k8s:minimum=0
// +k8s:maximum=100000
// +k8s:update=NoModify
// +k8s:update=NoUnset
Priority *int32 `json:"priority" protobuf:"varint,2,opt,name=priority"`
// state specifies a state that is assigned by the evictionrequest-controller. Responders should observe
// this state in order to navigate their lifecycle.
// - Inactive means that the responder should not yet process this eviction request.
// - Active means that the responder is either running or expected to start soon.
// Also, startTime has been set in the ResponderStatus by the evictionrequest-controller.
//
// An active responder should currently interact with the eviction process by updating
// .status.responders, where .name is the active responder name. ResponderStatus fields
// should be periodically updated to indicate the progress or completion of the eviction process.
// If .status.responders[].heartbeatTime field is not updated within the heartbeat deadline defined
// by the Eviction API (currently 20 minutes), the eviction is passed over to the next responder
// with a lower priority. Only one responder can be active at a time.
// - Interrupted means that the responder has failed to start or failed to update
// heartbeatTime in ResponderStatus in a timely manner.
// - Canceled means that the responder has been canceled. In other words, there is no
// EvictionRequest with the same target and Eviction intent in .spec.intent.
// - Completed means that the responder has successfully completed and set completionTime
// in ResponderStatus.
//
// Please refer to the ResponderStatus in .status.responders for more details on each responder.
// +required
// +k8s:required
State ResponderStateType `json:"state" protobuf:"bytes,3,opt,name=state,casttype=ResponderStateType"`
}
// ResponderStateType specifies a state that is assigned by the evictionrequest-controller.
// +enum
// +k8s:enum
type ResponderStateType string
const (
// ResponderStateInactive means that the responder should not yet process this eviction request.
ResponderStateInactive ResponderStateType = "Inactive"
// ResponderStateActive means that the responder is either running or expected to start soon.
// Also, startTime has been set in the ResponderStatus by the evictionrequest-controller.
//
// An active responder should currently interact with the eviction process by updating
// .status.responders, where .name is the active responder name. ResponderStatus fields
// should be periodically updated to indicate the progress or completion of the eviction process.
// If .status.responders[].heartbeatTime field is not updated within the heartbeat deadline defined
// by the Eviction API (currently 20 minutes), the eviction is passed over to the next responder
// with a lower priority. Only one responder can be active at a time.
ResponderStateActive ResponderStateType = "Active"
// ResponderStateInterrupted means that the responder has failed to start or failed to update
// heartbeatTime in ResponderStatus in a timely manner.
ResponderStateInterrupted ResponderStateType = "Interrupted"
// ResponderStateCanceled means that the responder has been canceled. In other words, there
// is no EvictionRequest with the same target and Eviction intent in .spec.intent.
ResponderStateCanceled ResponderStateType = "Canceled"
// ResponderStateCompleted means that the responder has successfully completed and set completionTime
// in ResponderStatus.
ResponderStateCompleted ResponderStateType = "Completed"
)
// ResponderStatus represents the last observed status of the eviction process of the responder.
// It should be only updated by the designated responder whose name is .name field.
// +structType=granular
type ResponderStatus struct {
// name allows you to identify the responder reacting to the Eviction.
//
// It must be a valid domain-prefixed key (such as "acme.io/foo").
// This field is initialized by Kubernetes and must be unique for each responder.
// This field is required.
// +required
// +k8s:required
// +k8s:format=k8s-prefixed-label-key
Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
// startTime tracks the time at which this responder was designated as active and should start
// processing the eviction request.
// It should reflect the present time when set.
// This field is initialized by Kubernetes when this responder becomes active.
// This field becomes immutable once set.
// +optional
// +k8s:optional
// +k8s:update=NoModify
// +k8s:update=NoUnset
StartTime *metav1.Time `json:"startTime,omitempty" protobuf:"bytes,2,opt,name=startTime"`
// heartbeatTime is the last time at which the eviction process was reported to be in progress
// by the responder.
// It should reflect the present time when set.
// Responders should avoid heartbeats more frequent than 20 seconds to avoid overloading the
// control-plane.
// +optional
// +k8s:optional
HeartbeatTime *metav1.Time `json:"heartbeatTime,omitempty" protobuf:"bytes,3,opt,name=heartbeatTime"`
// expectedCompletionTime is the time at which the eviction process step is expected to end for the
// responder.
// The time cannot be set to the past.
// May be omitted if no estimate can be made.
// +optional
// +k8s:optional
ExpectedCompletionTime *metav1.Time `json:"expectedCompletionTime,omitempty" protobuf:"bytes,4,opt,name=expectedCompletionTime"`
// completionTime tracks the time at which the Responder stopped processing the eviction request.
// Completion means that the responders has either fully or partially completed the
// eviction process, which may have resulted in target eviction (e.g. pod termination).
// It should reflect the present time when set.
// This field becomes immutable once set.
// +optional
// +k8s:optional
// +k8s:update=NoModify
// +k8s:update=NoUnset
CompletionTime *metav1.Time `json:"completionTime,omitempty" protobuf:"bytes,5,opt,name=completionTime"`
// message provides human-readable details about the state of the responder and the eviction
// process.
// Maximum length is 4000 characters.
// +optional
// +k8s:optional
// +k8s:maxLength=4000
Message *string `json:"message,omitempty" protobuf:"bytes,6,opt,name=message"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.37
// EvictionList contains a list of Eviction resources.
type EvictionList struct {
metav1.TypeMeta `json:",inline"`
// metadata is the standard list metadata.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
// +optional
metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
// items is the list of Evictions.
Items []Eviction `json:"items" protobuf:"bytes,2,rep,name=items"`
}

View File

@@ -56,6 +56,7 @@ import (
flowcontrolv1beta2 "k8s.io/api/flowcontrol/v1beta2"
flowcontrolv1beta3 "k8s.io/api/flowcontrol/v1beta3"
imagepolicyv1alpha1 "k8s.io/api/imagepolicy/v1alpha1"
lifecyclev1alpha1 "k8s.io/api/lifecycle/v1alpha1"
networkingv1 "k8s.io/api/networking/v1"
networkingv1beta1 "k8s.io/api/networking/v1beta1"
nodev1 "k8s.io/api/node/v1"
@@ -123,6 +124,7 @@ var groups = []runtime.SchemeBuilder{
flowcontrolv1beta3.SchemeBuilder,
flowcontrolv1.SchemeBuilder,
imagepolicyv1alpha1.SchemeBuilder,
lifecyclev1alpha1.SchemeBuilder,
networkingv1.SchemeBuilder,
networkingv1beta1.SchemeBuilder,
nodev1.SchemeBuilder,

View File

@@ -39,6 +39,7 @@ import (
certificatesv1alpha1 "k8s.io/api/certificates/v1alpha1"
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
coordinationv1 "k8s.io/api/coordination/v1"
coordinationv1alpha1 "k8s.io/api/coordination/v1alpha1"
coordinationv1alpha2 "k8s.io/api/coordination/v1alpha2"
coordinationv1beta1 "k8s.io/api/coordination/v1beta1"
corev1 "k8s.io/api/core/v1"
@@ -103,6 +104,7 @@ var localSchemeBuilder = runtime.SchemeBuilder{
certificatesv1.AddToScheme,
certificatesv1beta1.AddToScheme,
certificatesv1alpha1.AddToScheme,
coordinationv1alpha1.AddToScheme,
coordinationv1alpha2.AddToScheme,
coordinationv1beta1.AddToScheme,
coordinationv1.AddToScheme,

View File

@@ -39,6 +39,7 @@ import (
certificatesv1alpha1 "k8s.io/api/certificates/v1alpha1"
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
coordinationv1 "k8s.io/api/coordination/v1"
coordinationv1alpha1 "k8s.io/api/coordination/v1alpha1"
coordinationv1alpha2 "k8s.io/api/coordination/v1alpha2"
coordinationv1beta1 "k8s.io/api/coordination/v1beta1"
corev1 "k8s.io/api/core/v1"
@@ -103,6 +104,7 @@ var localSchemeBuilder = runtime.SchemeBuilder{
certificatesv1.AddToScheme,
certificatesv1beta1.AddToScheme,
certificatesv1alpha1.AddToScheme,
coordinationv1alpha1.AddToScheme,
coordinationv1alpha2.AddToScheme,
coordinationv1beta1.AddToScheme,
coordinationv1.AddToScheme,

View File

@@ -1,3 +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 pod
import (

View File

@@ -0,0 +1,653 @@
/*
Copyright 2025 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 eviction
import (
"fmt"
"strings"
"testing"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apimachinerytypes "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/validation/field"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
apitesting "k8s.io/kubernetes/pkg/api/testing"
"k8s.io/kubernetes/pkg/apis/lifecycle"
registry "k8s.io/kubernetes/pkg/registry/lifecycle/eviction"
"k8s.io/kubernetes/test/declarative_validation/meta"
utilsclock "k8s.io/utils/clock"
testing2 "k8s.io/utils/clock/testing"
// Ensure all API groups are registered with the scheme
_ "k8s.io/kubernetes/pkg/apis/lifecycle/install"
)
const validUID = "a2ee91f4-e13c-44db-9edc-4240e7383ab9"
func TestDeclarativeValidate(t *testing.T) {
apiVersions := []string{"v1alpha1"} // Eviction is currently only in v1alpha1
for _, apiVersion := range apiVersions {
t.Run(apiVersion, func(t *testing.T) {
testDeclarativeValidate(t, apiVersion)
})
}
}
func testDeclarativeValidate(t *testing.T, apiVersion string) {
testCases := map[string]struct {
input *lifecycle.Eviction
errors field.ErrorList
}{
"valid": {
input: mkValidEviction(),
},
"name is not valid": {
input: mkValidEviction(setName("-invalid-name")),
errors: []*field.Error{
field.Invalid(field.NewPath("metadata", "name"), "", "").WithOrigin("format=k8s-long-name").MarkBeta(),
},
},
"missing target": {
input: mkValidEviction(clearTarget()),
errors: field.ErrorList{
field.Invalid(field.NewPath("spec", "target"), "", "").WithOrigin("union"),
},
},
"missing target name": {
input: mkValidEviction(setTarget("", validUID)),
errors: field.ErrorList{
field.Required(field.NewPath("spec", "target", "pod", "name"), ""),
},
},
"invalid target name": {
input: mkValidEviction(setTarget("_test", validUID)),
errors: []*field.Error{
field.Invalid(field.NewPath("spec", "target", "pod", "name"), "", "").WithOrigin("format=k8s-long-name"),
},
},
"missing target uid": {
input: mkValidEviction(setTarget("bar", "")),
errors: field.ErrorList{
field.Required(field.NewPath("spec", "target", "pod", "uid"), ""),
},
},
"invalid target uid": {
input: mkValidEviction(setTarget("bar", "invalid-uid")),
errors: field.ErrorList{
field.Invalid(field.NewPath("spec", "target", "pod", "uid"), "", "").WithOrigin("format=k8s-uuid"),
},
},
}
clock := testing2.NewFakePassiveClock(time.Now())
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
APIGroup: "lifecycle.k8s.io",
APIVersion: apiVersion,
Resource: "evictions",
IsResourceRequest: true,
Verb: "create",
})
strategy := registry.NewStrategy(clock)
for k, tc := range testCases {
t.Run(k, func(t *testing.T) {
apitesting.VerifyValidationEquivalence(t, ctx, tc.input, strategy, tc.errors)
})
}
obj := mkValidEviction()
meta.RunObjectMetaTestCases(t, ctx, obj, strategy, meta.WithStringentFinalizerValidation())
}
func TestDeclarativeValidateUpdate(t *testing.T) {
apiVersions := []string{"v1alpha1"} // Eviction is currently only in v1alpha1
for _, apiVersion := range apiVersions {
t.Run(apiVersion, func(t *testing.T) {
testDeclarativeValidateUpdate(t, apiVersion)
})
}
}
func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) {
testCases := map[string]struct {
input *lifecycle.Eviction
oldInput *lifecycle.Eviction
errors field.ErrorList
}{
"valid": {
oldInput: mkValidEviction(),
input: mkValidEviction(func(obj *lifecycle.Eviction) {
obj.ObjectMeta.Labels = map[string]string{"foo": "bar"}
}),
},
"clear target": {
oldInput: mkValidEviction(),
input: mkValidEviction(clearTarget()),
errors: field.ErrorList{
field.Invalid(field.NewPath("spec", "target"), "", "").WithOrigin("immutable"),
},
},
"change target name": {
oldInput: mkValidEviction(),
input: mkValidEviction(setTarget("change", validUID)),
errors: field.ErrorList{
field.Invalid(field.NewPath("spec", "target"), "", "").WithOrigin("immutable"),
},
},
"change target uid": {
oldInput: mkValidEviction(),
input: mkValidEviction(setTarget("bar", "")),
errors: field.ErrorList{
field.Invalid(field.NewPath("spec", "target"), "", "").WithOrigin("immutable"),
},
},
}
clock := testing2.NewFakePassiveClock(time.Now())
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
APIGroup: "lifecycle.k8s.io",
APIVersion: apiVersion,
Resource: "evictions",
IsResourceRequest: true,
Verb: "update",
})
strategy := registry.NewStrategy(clock)
for k, tc := range testCases {
t.Run(k, func(t *testing.T) {
tc.oldInput.ResourceVersion = "0"
tc.input.ResourceVersion = "1"
apitesting.VerifyUpdateValidationEquivalence(t, ctx, tc.input, tc.oldInput, strategy, tc.errors)
})
}
obj := mkValidEviction()
meta.RunObjectMetaUpdateTestCases(t, ctx, obj, strategy, meta.WithStringentFinalizerValidation())
}
func TestDeclarativeValidateStatusUpdate(t *testing.T) {
apiVersions := []string{"v1alpha1"} // Eviction is currently only in v1alpha1
for _, apiVersion := range apiVersions {
t.Run(apiVersion, func(t *testing.T) {
testDeclarativeValidateStatusUpdate(t, apiVersion)
})
}
}
func testDeclarativeValidateStatusUpdate(t *testing.T, apiVersion string) {
clock := testing2.NewFakePassiveClock(time.Now())
clockAfter := func(duration time.Duration) utilsclock.PassiveClock {
return testing2.NewFakePassiveClock(clock.Now().Add(duration))
}
testCases := map[string]struct {
input *lifecycle.EvictionStatus
oldInput *lifecycle.EvictionStatus
errors field.ErrorList
}{
"valid": {
oldInput: mkValidEvictionStatus(0),
input: mkValidEvictionStatus(0, setObservedGeneration(new(int64(5)))),
},
// conditions
"too many conditions": {
oldInput: mkValidEvictionStatus(0),
input: mkValidEvictionStatus(0, addConditionsCount(clock, 101)),
errors: []*field.Error{
field.TooMany(field.NewPath("status", "conditions"), 101, 100).WithOrigin("maxItems"),
},
},
// observedGeneration
"clear generation": {
oldInput: mkValidEvictionStatus(0, setObservedGeneration(new(int64(1)))),
input: mkValidEvictionStatus(0, setObservedGeneration(nil)),
errors: []*field.Error{
field.Invalid(field.NewPath("status", "observedGeneration"), nil, "").WithOrigin("update"),
},
},
"set generation to 0": {
oldInput: mkValidEvictionStatus(0, setObservedGeneration(nil)),
input: mkValidEvictionStatus(0, setObservedGeneration(new(int64(0)))),
errors: []*field.Error{
field.Invalid(field.NewPath("status", "observedGeneration"), 0, "").WithOrigin("minimum"),
},
},
"set generation to negative": {
oldInput: mkValidEvictionStatus(0, setObservedGeneration(nil)),
input: mkValidEvictionStatus(0, setObservedGeneration(new(int64(-1)))),
errors: []*field.Error{
field.Invalid(field.NewPath("status", "observedGeneration"), -1, "").WithOrigin("minimum"),
},
},
"decrease generation": {
oldInput: mkValidEvictionStatus(0, setObservedGeneration(new(int64(2)))),
input: mkValidEvictionStatus(0, setObservedGeneration(new(int64(1)))),
errors: []*field.Error{
field.Invalid(field.NewPath("status", "observedGeneration"), 1, "").WithOrigin("monotonic"),
},
},
// requesters
"too many requesters": {
oldInput: mkValidEvictionStatus(0),
input: mkValidEvictionStatus(0, addRequestersCount(lifecycle.RequesterIntentEviction, 40),
addRequestersCount(lifecycle.RequesterIntentWithdrawn, 40),
addRequestersCount(lifecycle.RequesterIntentEviction, 21)),
errors: []*field.Error{
field.TooMany(field.NewPath("status", "requesters"), 101, 100).WithOrigin("maxItems"),
},
},
"add a duplicate requesters": {
oldInput: mkValidEvictionStatus(0, addRequesters(lifecycle.RequesterIntentEviction, "foo.example.com/baz")),
input: mkValidEvictionStatus(0, addRequesters(lifecycle.RequesterIntentEviction, "foo.example.com/baz", "foo.example.com/baz")),
errors: field.ErrorList{
field.Duplicate(field.NewPath("status", "requesters").Index(1), ""),
},
},
"add a requester without a name": {
oldInput: mkValidEvictionStatus(0),
input: mkValidEvictionStatus(0, addRequesters(lifecycle.RequesterIntentEviction, "")),
errors: field.ErrorList{
field.Required(field.NewPath("status", "requesters").Index(0).Child("name"), ""),
},
},
"add a requester with invalid name": {
oldInput: mkValidEvictionStatus(0),
input: mkValidEvictionStatus(0, addRequesters(lifecycle.RequesterIntentEviction, "foo")),
errors: field.ErrorList{
field.Invalid(field.NewPath("status", "requesters").Index(0).Child("name"), "", "").WithOrigin("format=k8s-prefixed-label-key"),
},
},
"add a requester without an intent": {
oldInput: mkValidEvictionStatus(0),
input: mkValidEvictionStatus(0, addRequesters("", "foo.example.com/baz")),
errors: field.ErrorList{
field.Required(field.NewPath("status", "requesters").Index(0).Child("intent"), ""),
},
},
"add a requester with invalid intent": {
oldInput: mkValidEvictionStatus(0),
input: mkValidEvictionStatus(0, addRequesters("Invalid", "foo.example.com/bar")),
errors: field.ErrorList{
field.NotSupported(field.NewPath("status", "requesters").Index(0).Child("intent"), "", []lifecycle.RequesterIntent{lifecycle.RequesterIntentEviction, lifecycle.RequesterIntentWithdrawn}),
},
},
"change a valid requester to an invalid one": {
oldInput: mkValidEvictionStatus(0, addRequesters(lifecycle.RequesterIntentEviction, "foo.example.com/baz")),
input: mkValidEvictionStatus(0, addRequesters("Invalid", "foo.example.com/baz")),
errors: field.ErrorList{
field.NotSupported(field.NewPath("status", "requesters").Index(0).Child("intent"), "", []lifecycle.RequesterIntent{lifecycle.RequesterIntentEviction, lifecycle.RequesterIntentWithdrawn}),
},
},
// targetResponders and responders
"duplicate targetResponders and responders": {
oldInput: mkValidEvictionStatus(0),
input: mkValidEvictionStatus(0, addTargetResponders("example.com/baz", "example.com/baz"), addStatusResponders("example.com/baz", "example.com/baz")),
errors: []*field.Error{
field.Duplicate(field.NewPath("status", "targetResponders").Index(1), ""),
field.Duplicate(field.NewPath("status", "responders").Index(1), ""),
},
},
"required targetResponder and responder name": {
oldInput: mkValidEvictionStatus(0),
input: mkValidEvictionStatus(0,
addTargetResponders(""),
setStateFor(lifecycle.ResponderStateActive, 0),
addStatusResponders(""),
setRespondersStartTime(clock, 0, 1),
),
errors: []*field.Error{
field.Required(field.NewPath("status", "targetResponders").Index(0).Child("name"), ""),
field.Required(field.NewPath("status", "responders").Index(0).Child("name"), ""),
},
},
"invalid targetResponder and responder name": {
oldInput: mkValidEvictionStatus(0),
input: mkValidEvictionStatus(0,
addTargetResponders("foo"),
setStateFor(lifecycle.ResponderStateActive, 0),
addStatusResponders("foo"),
setRespondersStartTime(clock, 0, 1),
),
errors: []*field.Error{
field.Invalid(field.NewPath("status", "targetResponders").Index(0).Child("name"), "", "").WithOrigin("format=k8s-prefixed-label-key"),
field.Invalid(field.NewPath("status", "responders").Index(0).Child("name"), "", "").WithOrigin("format=k8s-prefixed-label-key"),
},
},
// targetResponders
"too many targetResponders": {
oldInput: mkValidEvictionStatus(0),
input: mkValidEvictionStatusWithStatuses(12, 0),
errors: []*field.Error{
field.Invalid(field.NewPath("status", "responders"), "", "must be the same length as status.targetResponders and contain the same keys in the same order").MarkFromImperative(),
field.TooMany(field.NewPath("status", "targetResponders"), 12, 11).WithOrigin("maxItems"),
},
},
"required targetResponder priority": {
oldInput: mkValidEvictionStatus(0),
input: mkValidEvictionStatus(1, setPriorityFor(nil, 0)),
errors: []*field.Error{
field.Required(field.NewPath("status", "targetResponders").Index(0).Child("priority"), ""),
},
},
"negative targetResponder priority": {
oldInput: mkValidEvictionStatus(0),
input: mkValidEvictionStatus(1, setPriorityFor(new(int32(-1)), 0)),
errors: []*field.Error{
field.Invalid(field.NewPath("status", "targetResponders").Index(0).Child("priority"), "", "").WithOrigin("minimum"),
},
},
"too high targetResponder priority": {
oldInput: mkValidEvictionStatus(0),
input: mkValidEvictionStatus(1, setPriorityFor(new(int32(100001)), 0)),
errors: []*field.Error{
field.Invalid(field.NewPath("status", "targetResponders").Index(0).Child("priority"), "", "").WithOrigin("maximum"),
},
},
"cannot change targetResponder priority": {
oldInput: mkValidEvictionStatus(0,
addTargetResponders(responderName(0)),
setStateFor(lifecycle.ResponderStateActive, 0),
addStatusResponders(responderName(0)),
setRespondersStartTime(clock, 0, 1),
setPriorityFor(new(int32(3000)), 0)),
input: mkValidEvictionStatus(0,
addTargetResponders(responderName(0)),
setStateFor(lifecycle.ResponderStateActive, 0),
addStatusResponders(responderName(0)),
setRespondersStartTime(clock, 0, 1),
setPriorityFor(new(int32(3005)), 0)),
errors: []*field.Error{
field.Invalid(field.NewPath("status", "targetResponders").Index(0).Child("priority"), "", "").WithOrigin("update"),
},
},
"required targetResponder state": {
oldInput: mkValidEvictionStatus(0),
input: mkValidEvictionStatus(1, setStateFor("", 0)),
errors: []*field.Error{
field.Required(field.NewPath("status", "targetResponders").Index(0).Child("state"), ""),
field.Invalid(field.NewPath("status", "targetResponders").Index(0).Child("state"), "", "must be one of: Canceled, Completed, Interrupted").MarkFromImperative(),
},
},
"invalid targetResponder state": {
oldInput: mkValidEvictionStatus(0),
input: mkValidEvictionStatus(1, setStateFor("Invalid", 0)),
errors: []*field.Error{
field.NotSupported(field.NewPath("status", "targetResponders").Index(0).Child("state"), "", []string(nil)),
field.Invalid(field.NewPath("status", "targetResponders").Index(0).Child("state"), "", "must be one of: Canceled, Completed, Interrupted").MarkFromImperative(),
},
},
// responders
"too many status responders": {
oldInput: mkValidEvictionStatusWithStatuses(18, 17),
input: mkValidEvictionStatus(18),
errors: []*field.Error{
field.TooMany(field.NewPath("status", "responders"), 18, 17).WithOrigin("maxItems"),
},
},
// status responder name
// startTime
"startTime cannot be removed once set": {
oldInput: mkValidEvictionStatus(2,
setRespondersStartTime(clock, 0, 1)),
input: mkValidEvictionStatus(2),
errors: []*field.Error{
field.Invalid(field.NewPath("status", "responders").Index(0).Child("startTime"), nil, "").WithOrigin("update"),
},
},
"startTime cannot be changed once set": {
oldInput: mkValidEvictionStatus(2,
setRespondersStartTime(clock, 0, 1)),
input: mkValidEvictionStatus(2,
setRespondersStartTime(clockAfter(15*time.Second), 0, 1)),
errors: []*field.Error{
field.Invalid(field.NewPath("status", "responders").Index(0).Child("startTime"), nil, "").WithOrigin("update"),
},
},
// heartbeatTime
// expectedCompletionTime
// completionTime
"completionTime cannot be changed once set": {
oldInput: mkValidEvictionStatus(2,
setRespondersStartTime(clock, 0, 1),
setRespondersCompletionTime(clockAfter(5*time.Minute), 0, 1)),
input: mkValidEvictionStatus(2,
setRespondersStartTime(clock, 0, 1),
setRespondersCompletionTime(clockAfter(4*time.Minute), 0, 1)),
errors: []*field.Error{
field.Invalid(field.NewPath("status", "responders").Index(0).Child("completionTime"), nil, "").WithOrigin("update"),
},
},
"completionTime cannot be removed once set": {
oldInput: mkValidEvictionStatus(2,
setRespondersStartTime(clock, 0, 1),
setRespondersCompletionTime(clockAfter(5*time.Minute), 0, 1)),
input: mkValidEvictionStatus(2,
setRespondersStartTime(clock, 0, 1)),
errors: []*field.Error{
field.Invalid(field.NewPath("status", "responders").Index(0).Child("completionTime"), nil, "").WithOrigin("update"),
},
},
// message
"too long message": {
oldInput: mkValidEvictionStatus(2,
setRespondersStartTime(clock, 0, 1),
setRespondersHeartBeatTime(clock, 0, 1)),
input: mkValidEvictionStatus(2,
setRespondersStartTime(clock, 0, 1),
setRespondersHeartBeatTime(clock, 0, 1),
setRespondersMessage(0, 1, strings.Repeat("a", 4000))),
errors: []*field.Error{
field.TooLongCharacters(field.NewPath("status", "responders").Index(0).Child("message"), "", 4000).WithOrigin("maxLength"),
},
},
}
strategy := registry.NewStatusStrategy(registry.NewStrategy(clock))
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
APIGroup: "lifecycle.k8s.io",
APIVersion: apiVersion,
Resource: "evictions",
IsResourceRequest: true,
Verb: "update",
Subresource: "status",
})
for k, tc := range testCases {
t.Run(k, func(t *testing.T) {
oldEviction := mkValidEviction()
oldEviction.ResourceVersion = "0"
oldEviction.Status = *tc.oldInput
eviction := mkValidEviction()
eviction.ResourceVersion = "1"
eviction.Status = *tc.input
apitesting.VerifyUpdateValidationEquivalence(t, ctx, eviction, oldEviction, strategy, tc.errors, apitesting.WithSubResources("status"))
})
}
meta.RunConditionTestCases(t, ctx, field.NewPath("status", "conditions"), &lifecycle.Eviction{}, strategy, func(obj *lifecycle.Eviction, c []metav1.Condition) {
*obj = *mkValidEviction(func(r *lifecycle.Eviction) { r.Status.Conditions = c })
})
}
func mkValidEviction(tweaks ...func(obj *lifecycle.Eviction)) *lifecycle.Eviction {
obj := lifecycle.Eviction{
ObjectMeta: metav1.ObjectMeta{Name: "evict-pod-1-foo.pod", Namespace: "foo"},
Spec: lifecycle.EvictionSpec{
Target: lifecycle.EvictionTarget{
Pod: &lifecycle.EvictionPodReference{
UID: validUID,
Name: "foo.pod",
},
},
},
}
for _, tweak := range tweaks {
tweak(&obj)
}
return &obj
}
func setName(name string) func(obj *lifecycle.Eviction) {
return func(obj *lifecycle.Eviction) {
obj.Name = name
}
}
func clearTarget() func(obj *lifecycle.Eviction) {
return func(obj *lifecycle.Eviction) {
obj.Spec.Target.Pod = nil
}
}
func setTarget(name, uid string) func(obj *lifecycle.Eviction) {
return func(obj *lifecycle.Eviction) {
obj.Spec.Target.Pod = &lifecycle.EvictionPodReference{
UID: apimachinerytypes.UID(uid),
Name: name,
}
}
}
func responderName(i int) string {
return fmt.Sprintf("responder.example.com/bar%d", i)
}
func mkValidEvictionStatus(responders int, tweaks ...func(obj *lifecycle.EvictionStatus)) *lifecycle.EvictionStatus {
return mkValidEvictionStatusWithStatuses(responders, responders, tweaks...)
}
func mkValidEvictionStatusWithStatuses(responders, statuses int, tweaks ...func(obj *lifecycle.EvictionStatus)) *lifecycle.EvictionStatus {
obj := lifecycle.EvictionStatus{
ObservedGeneration: new(int64(1)),
}
for i := range responders {
obj.TargetResponders = append(obj.TargetResponders, lifecycle.TargetResponder{
Name: responderName(i),
Priority: new(5000 - int32(i)),
State: lifecycle.ResponderStateInactive,
})
if i == 0 {
obj.TargetResponders[i].State = lifecycle.ResponderStateActive
}
}
for i := range statuses {
obj.Responders = append(obj.Responders, lifecycle.ResponderStatus{
Name: responderName(i),
})
if i == 0 {
obj.Responders[i].StartTime = new(metav1.Now())
}
}
for _, tweak := range tweaks {
tweak(&obj)
}
return &obj
}
func addCondition(clock utilsclock.PassiveClock, name lifecycle.EvictionConditionType, status metav1.ConditionStatus) func(obj *lifecycle.EvictionStatus) {
return func(obj *lifecycle.EvictionStatus) {
newCond := metav1.Condition{
Type: string(name),
Status: status,
Reason: string(name) + "Reason",
LastTransitionTime: metav1.Time{Time: clock.Now()},
}
obj.Conditions = append(obj.Conditions, newCond)
}
}
func addConditionsCount(clock utilsclock.PassiveClock, count int) func(obj *lifecycle.EvictionStatus) {
return func(obj *lifecycle.EvictionStatus) {
for i := range count {
addCondition(clock, lifecycle.EvictionConditionType(fmt.Sprintf("Condition%d", i)), metav1.ConditionTrue)(obj)
}
}
}
func setObservedGeneration(generation *int64) func(obj *lifecycle.EvictionStatus) {
return func(obj *lifecycle.EvictionStatus) {
obj.ObservedGeneration = generation
}
}
func addRequesters(intent lifecycle.RequesterIntent, names ...string) func(obj *lifecycle.EvictionStatus) {
return func(obj *lifecycle.EvictionStatus) {
for _, name := range names {
obj.Requesters = append(obj.Requesters, lifecycle.Requester{Name: name, Intent: intent})
}
}
}
func addRequestersCount(intent lifecycle.RequesterIntent, count int) func(obj *lifecycle.EvictionStatus) {
return func(obj *lifecycle.EvictionStatus) {
for i := range count {
obj.Requesters = append(obj.Requesters, lifecycle.Requester{Name: fmt.Sprintf("foo.example.com/bar-%d", i), Intent: intent})
}
}
}
func addTargetResponders(responders ...string) func(obj *lifecycle.EvictionStatus) {
return func(obj *lifecycle.EvictionStatus) {
lastPriority := len(obj.Responders)
for i, name := range responders {
obj.TargetResponders = append(obj.TargetResponders, lifecycle.TargetResponder{
Name: name,
Priority: new(5000 - int32(lastPriority+i)),
State: lifecycle.ResponderStateInactive,
})
}
}
}
func setStateFor(state lifecycle.ResponderStateType, idx int) func(obj *lifecycle.EvictionStatus) {
return func(obj *lifecycle.EvictionStatus) {
obj.TargetResponders[idx].State = state
}
}
func setPriorityFor(priority *int32, idx int) func(obj *lifecycle.EvictionStatus) {
return func(obj *lifecycle.EvictionStatus) {
obj.TargetResponders[idx].Priority = priority
}
}
func addStatusResponders(responders ...string) func(obj *lifecycle.EvictionStatus) {
return func(obj *lifecycle.EvictionStatus) {
for _, responder := range responders {
obj.Responders = append(obj.Responders, lifecycle.ResponderStatus{Name: responder})
}
}
}
func setRespondersStartTime(clock utilsclock.PassiveClock, from, to int) func(obj *lifecycle.EvictionStatus) {
return func(obj *lifecycle.EvictionStatus) {
for i := from; i < to; i++ {
obj.Responders[i].StartTime = &metav1.Time{Time: clock.Now().Add(time.Duration(i) * time.Second)}
}
}
}
func setRespondersHeartBeatTime(clock utilsclock.PassiveClock, from, to int) func(obj *lifecycle.EvictionStatus) {
return func(obj *lifecycle.EvictionStatus) {
for i := from; i < to; i++ {
obj.Responders[i].HeartbeatTime = &metav1.Time{Time: clock.Now().Add(time.Duration(i) * time.Second)}
}
}
}
func setRespondersCompletionTime(clock utilsclock.PassiveClock, from, to int) func(obj *lifecycle.EvictionStatus) {
return func(obj *lifecycle.EvictionStatus) {
for i := from; i < to; i++ {
obj.Responders[i].CompletionTime = &metav1.Time{Time: clock.Now().Add(time.Duration(i) * time.Second)}
}
}
}
func setRespondersMessage(from, to int, suffixes ...string) func(obj *lifecycle.EvictionStatus) {
return func(obj *lifecycle.EvictionStatus) {
for i := from; i < to; i++ {
msg := fmt.Sprintf("message %d", i)
for _, suffix := range suffixes {
msg += suffix
}
obj.Responders[i].Message = new(msg)
}
}
}

View File

@@ -0,0 +1,402 @@
/*
Copyright 2025 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"
"testing"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
apimachinerytypes "k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/validation/field"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
apitesting "k8s.io/kubernetes/pkg/api/testing"
"k8s.io/kubernetes/pkg/apis/lifecycle"
registry "k8s.io/kubernetes/pkg/registry/lifecycle/evictionrequest"
"k8s.io/kubernetes/test/declarative_validation/meta"
utilsclock "k8s.io/utils/clock"
testing2 "k8s.io/utils/clock/testing"
// Ensure all API groups are registered with the scheme
_ "k8s.io/kubernetes/pkg/apis/lifecycle/install"
)
const validUID = "5477c2ff-f59f-4eb9-a0be-e54232323faa"
func TestDeclarativeValidate(t *testing.T) {
apiVersions := []string{"v1alpha1"} // EvictionRequest is currently only in v1alpha1
for _, apiVersion := range apiVersions {
t.Run(apiVersion, func(t *testing.T) {
testDeclarativeValidate(t, apiVersion)
})
}
}
func testDeclarativeValidate(t *testing.T, apiVersion string) {
testCases := map[string]struct {
input *lifecycle.EvictionRequest
errors field.ErrorList
}{
"valid": {
input: mkValidEvictionRequest(),
},
"name is not valid": {
input: mkValidEvictionRequest(setName("-invalid-name")),
errors: []*field.Error{
field.Invalid(field.NewPath("metadata", "name"), "", "").WithOrigin("format=k8s-long-name").MarkBeta(),
},
},
"missing target": {
input: mkValidEvictionRequest(clearTarget()),
errors: field.ErrorList{
field.Invalid(field.NewPath("spec", "target"), "", "").WithOrigin("union"),
},
},
"missing target name": {
input: mkValidEvictionRequest(setTarget("", validUID)),
errors: field.ErrorList{
field.Required(field.NewPath("spec", "target", "pod", "name"), ""),
},
},
"invalid target name": {
input: mkValidEvictionRequest(setTarget("_test", validUID)),
errors: []*field.Error{
field.Invalid(field.NewPath("spec", "target", "pod", "name"), "", "").WithOrigin("format=k8s-long-name"),
},
},
"missing target uid": {
input: mkValidEvictionRequest(setTarget("bar", "")),
errors: field.ErrorList{
field.Required(field.NewPath("spec", "target", "pod", "uid"), ""),
},
},
"invalid target uid": {
input: mkValidEvictionRequest(setTarget("bar", "invalid-uid")),
errors: field.ErrorList{
field.Invalid(field.NewPath("spec", "target", "pod", "uid"), "", "").WithOrigin("format=k8s-uuid"),
},
},
"missing requester name": {
input: mkValidEvictionRequest(setRequester("")),
errors: field.ErrorList{
field.Required(field.NewPath("spec", "requester"), ""),
},
},
"invalid requester name": {
input: mkValidEvictionRequest(setRequester("foo")),
errors: field.ErrorList{
field.Invalid(field.NewPath("spec", "requester"), "", "").WithOrigin("format=k8s-prefixed-label-key"),
},
},
"invalid requester name - reserved k8s.io domain": {
input: mkValidEvictionRequest(setRequester("k8s.io/key")),
errors: field.ErrorList{
field.Invalid(field.NewPath("spec", "requester"), "", "domain names *.k8s.io, *.kubernetes.io are reserved"),
},
},
"invalid requester name - reserved kubernetes.io domain": {
input: mkValidEvictionRequest(setRequester("dev.kubernetes.io/key")),
errors: field.ErrorList{
field.Invalid(field.NewPath("spec", "requester"), "", "domain names *.k8s.io, *.kubernetes.io are reserved"),
},
},
"requester without an intent": {
input: mkValidEvictionRequest(setRequesterIntent("")),
errors: field.ErrorList{
field.Required(field.NewPath("spec", "intent"), ""),
},
},
"requester with invalid intent": {
input: mkValidEvictionRequest(setRequesterIntent("Invalid")),
errors: field.ErrorList{
field.NotSupported(field.NewPath("spec", "intent"), "", []lifecycle.RequesterIntent{lifecycle.RequesterIntentEviction, lifecycle.RequesterIntentWithdrawn}),
},
},
}
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
APIGroup: "lifecycle.k8s.io",
APIVersion: apiVersion,
Resource: "evictionrequests",
IsResourceRequest: true,
Verb: "create",
})
strategy := registry.NewStrategy()
for k, tc := range testCases {
t.Run(k, func(t *testing.T) {
apitesting.VerifyValidationEquivalence(t, ctx, tc.input, strategy, tc.errors)
})
}
obj := mkValidEvictionRequest()
meta.RunObjectMetaTestCases(t, ctx, obj, strategy, meta.WithStringentFinalizerValidation())
}
func TestDeclarativeValidateUpdate(t *testing.T) {
apiVersions := []string{"v1alpha1"} // EvictionRequest is currently only in v1alpha1
for _, apiVersion := range apiVersions {
t.Run(apiVersion, func(t *testing.T) {
testDeclarativeValidateUpdate(t, apiVersion)
})
}
}
func testDeclarativeValidateUpdate(t *testing.T, apiVersion string) {
testCases := map[string]struct {
input *lifecycle.EvictionRequest
oldInput *lifecycle.EvictionRequest
errors field.ErrorList
}{
"valid": {
oldInput: mkValidEvictionRequest(),
input: mkValidEvictionRequest(setRequesterIntent(lifecycle.EvictionRequestIntentWithdrawn)),
},
"clear target": {
oldInput: mkValidEvictionRequest(),
input: mkValidEvictionRequest(clearTarget()),
errors: field.ErrorList{
field.Invalid(field.NewPath("spec", "target"), "", "").WithOrigin("immutable"),
},
},
"change target name": {
oldInput: mkValidEvictionRequest(),
input: mkValidEvictionRequest(setTarget("change", validUID)),
errors: field.ErrorList{
field.Invalid(field.NewPath("spec", "target"), "", "").WithOrigin("immutable"),
},
},
"change target uid": {
oldInput: mkValidEvictionRequest(),
input: mkValidEvictionRequest(setTarget("bar", "")),
errors: field.ErrorList{
field.Invalid(field.NewPath("spec", "target"), "", "").WithOrigin("immutable"),
},
},
"change requester name": {
oldInput: mkValidEvictionRequest(),
input: mkValidEvictionRequest(setRequester("foo.example.com/baz")),
errors: field.ErrorList{
field.Invalid(field.NewPath("spec", "requester"), "", "").WithOrigin("immutable"),
},
},
"change to a requester without an intent": {
oldInput: mkValidEvictionRequest(),
input: mkValidEvictionRequest(setRequesterIntent("")),
errors: field.ErrorList{
field.Required(field.NewPath("spec", "intent"), ""),
},
},
"change to a requester with invalid intent": {
oldInput: mkValidEvictionRequest(),
input: mkValidEvictionRequest(setRequesterIntent("Invalid")),
errors: field.ErrorList{
field.NotSupported(field.NewPath("spec", "intent"), "", []lifecycle.RequesterIntent{lifecycle.RequesterIntentEviction, lifecycle.RequesterIntentWithdrawn}),
},
},
}
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
APIGroup: "lifecycle.k8s.io",
APIVersion: apiVersion,
Resource: "evictionrequests",
IsResourceRequest: true,
Verb: "update",
})
strategy := registry.NewStrategy()
for k, tc := range testCases {
t.Run(k, func(t *testing.T) {
tc.oldInput.ResourceVersion = "0"
tc.input.ResourceVersion = "1"
apitesting.VerifyUpdateValidationEquivalence(t, ctx, tc.input, tc.oldInput, strategy, tc.errors)
})
}
obj := mkValidEvictionRequest()
meta.RunObjectMetaUpdateTestCases(t, ctx, obj, strategy, meta.WithStringentFinalizerValidation())
}
func TestDeclarativeValidateStatusUpdate(t *testing.T) {
apiVersions := []string{"v1alpha1"} // EvictionRequest is currently only in v1alpha1
for _, apiVersion := range apiVersions {
t.Run(apiVersion, func(t *testing.T) {
testDeclarativeValidateStatusUpdate(t, apiVersion)
})
}
}
func testDeclarativeValidateStatusUpdate(t *testing.T, apiVersion string) {
clock := testing2.NewFakePassiveClock(time.Now())
testCases := map[string]struct {
input *lifecycle.EvictionRequestStatus
oldInput *lifecycle.EvictionRequestStatus
errors field.ErrorList
}{
"valid": {
oldInput: mkValidEvictionRequestStatus(0),
input: mkValidEvictionRequestStatus(0, setObservedGeneration(new(int64(5)))),
},
// conditions
"too many conditions": {
oldInput: mkValidEvictionRequestStatus(0),
input: mkValidEvictionRequestStatus(0, addConditionsCount(clock, 101)),
errors: []*field.Error{
field.TooMany(field.NewPath("status", "conditions"), 101, 100).WithOrigin("maxItems"),
},
},
// observedGeneration
"clear generation": {
oldInput: mkValidEvictionRequestStatus(0, setObservedGeneration(new(int64(1)))),
input: mkValidEvictionRequestStatus(0, setObservedGeneration(nil)),
errors: []*field.Error{
field.Invalid(field.NewPath("status", "observedGeneration"), nil, "").WithOrigin("update"),
},
},
"set generation to 0": {
oldInput: mkValidEvictionRequestStatus(0, setObservedGeneration(nil)),
input: mkValidEvictionRequestStatus(0, setObservedGeneration(new(int64(0)))),
errors: []*field.Error{
field.Invalid(field.NewPath("status", "observedGeneration"), 0, "").WithOrigin("minimum"),
},
},
"set generation to negative": {
oldInput: mkValidEvictionRequestStatus(0, setObservedGeneration(nil)),
input: mkValidEvictionRequestStatus(0, setObservedGeneration(new(int64(-1)))),
errors: []*field.Error{
field.Invalid(field.NewPath("status", "observedGeneration"), -1, "").WithOrigin("minimum"),
},
},
"decrease generation": {
oldInput: mkValidEvictionRequestStatus(0, setObservedGeneration(new(int64(2)))),
input: mkValidEvictionRequestStatus(0, setObservedGeneration(new(int64(1)))),
errors: []*field.Error{
field.Invalid(field.NewPath("status", "observedGeneration"), 1, "").WithOrigin("monotonic"),
},
},
}
strategy := registry.NewStatusStrategy(registry.NewStrategy())
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
APIGroup: "lifecycle.k8s.io",
APIVersion: apiVersion,
Resource: "evictionrequests",
IsResourceRequest: true,
Verb: "update",
Subresource: "status",
})
for k, tc := range testCases {
t.Run(k, func(t *testing.T) {
oldEvictionRequest := mkValidEvictionRequest()
oldEvictionRequest.ResourceVersion = "0"
oldEvictionRequest.Status = *tc.oldInput
evictionRequest := mkValidEvictionRequest()
evictionRequest.ResourceVersion = "1"
evictionRequest.Status = *tc.input
apitesting.VerifyUpdateValidationEquivalence(t, ctx, evictionRequest, oldEvictionRequest, strategy, tc.errors, apitesting.WithSubResources("status"))
})
}
meta.RunConditionTestCases(t, ctx, field.NewPath("status", "conditions"), &lifecycle.EvictionRequest{}, strategy, func(obj *lifecycle.EvictionRequest, c []metav1.Condition) {
*obj = *mkValidEvictionRequest(func(r *lifecycle.EvictionRequest) { r.Status.Conditions = c })
})
}
func mkValidEvictionRequest(tweaks ...func(obj *lifecycle.EvictionRequest)) *lifecycle.EvictionRequest {
obj := lifecycle.EvictionRequest{
ObjectMeta: metav1.ObjectMeta{Name: "bar", Namespace: "foo"},
Spec: lifecycle.EvictionRequestSpec{
Target: lifecycle.EvictionRequestTarget{
Pod: &lifecycle.EvictionRequestPodReference{
UID: validUID,
Name: "foo.pod",
},
},
Requester: "foo.example.com/bar",
Intent: lifecycle.EvictionRequestIntentEviction,
},
}
for _, tweak := range tweaks {
tweak(&obj)
}
return &obj
}
func setName(name string) func(obj *lifecycle.EvictionRequest) {
return func(obj *lifecycle.EvictionRequest) {
obj.Name = name
}
}
func clearTarget() func(obj *lifecycle.EvictionRequest) {
return func(obj *lifecycle.EvictionRequest) {
obj.Spec.Target.Pod = nil
}
}
func setTarget(name, uid string) func(obj *lifecycle.EvictionRequest) {
return func(obj *lifecycle.EvictionRequest) {
obj.Spec.Target.Pod = &lifecycle.EvictionRequestPodReference{
UID: apimachinerytypes.UID(uid),
Name: name,
}
}
}
func setRequester(requester string) func(obj *lifecycle.EvictionRequest) {
return func(obj *lifecycle.EvictionRequest) {
obj.Spec.Requester = requester
}
}
func setRequesterIntent(intent lifecycle.EvictionRequestIntent) func(obj *lifecycle.EvictionRequest) {
return func(obj *lifecycle.EvictionRequest) {
obj.Spec.Intent = intent
}
}
func mkValidEvictionRequestStatus(responders int, tweaks ...func(obj *lifecycle.EvictionRequestStatus)) *lifecycle.EvictionRequestStatus {
obj := lifecycle.EvictionRequestStatus{
ObservedGeneration: new(int64(1)),
}
for _, tweak := range tweaks {
tweak(&obj)
}
return &obj
}
func addCondition(clock utilsclock.PassiveClock, name lifecycle.EvictionConditionType, status metav1.ConditionStatus) func(obj *lifecycle.EvictionRequestStatus) {
return func(obj *lifecycle.EvictionRequestStatus) {
newCond := metav1.Condition{
Type: string(name),
Status: status,
Reason: string(name) + "Reason",
LastTransitionTime: metav1.Time{Time: clock.Now()},
}
obj.Conditions = append(obj.Conditions, newCond)
}
}
func addConditionsCount(clock utilsclock.PassiveClock, count int) func(obj *lifecycle.EvictionRequestStatus) {
return func(obj *lifecycle.EvictionRequestStatus) {
for i := range count {
addCondition(clock, lifecycle.EvictionConditionType(fmt.Sprintf("Condition%d", i)), metav1.ConditionTrue)(obj)
}
}
}
func setObservedGeneration(generation *int64) func(obj *lifecycle.EvictionRequestStatus) {
return func(obj *lifecycle.EvictionRequestStatus) {
obj.ObservedGeneration = generation
}
}

View File

@@ -330,6 +330,23 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
},
// --
// k8s.io/kubernetes/pkg/apis/lifecycle/v1alpha1
gvr("lifecycle.k8s.io", "v1alpha1", "evictionrequests"): {
Stub: `{"metadata": {"name": "pod-eviction-request"}, "spec": {"target": {"pod": {"name": "my-workload", "uid": "3d7fdff1-3fe5-48b9-b106-1ee24b0277f6"}}, "requester": "drain.foo.com/bar", "intent": "Eviction"}}`,
ExpectedEtcdPath: "/registry/evictionrequests/" + namespace + "/pod-eviction-request",
ExpectedGVK: gvkP("lifecycle.k8s.io", "v1alpha1", "EvictionRequest"),
IntroducedVersion: "1.37",
RemovedVersion: "1.43",
},
gvr("lifecycle.k8s.io", "v1alpha1", "evictions"): {
Stub: `{"metadata": {"name": "pod-1-my-workload"}, "spec": {"target": {"pod": {"name": "my-workload", "uid": "3d7fdff1-3fe5-48b9-b106-1ee24b0277f6"}}}}`,
ExpectedEtcdPath: "/registry/evictions/" + namespace + "/pod-1-my-workload",
ExpectedGVK: gvkP("lifecycle.k8s.io", "v1alpha1", "Eviction"),
IntroducedVersion: "1.37",
RemovedVersion: "1.43",
},
// --
// k8s.io/kubernetes/pkg/apis/discovery/v1
gvr("discovery.k8s.io", "v1", "endpointslices"): {
Stub: `{"metadata": {"name": "slicev1"}, "addressType": "IPv4", "protocol": "TCP", "ports": [], "endpoints": []}`,