mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-08-09 00:20:36 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
21
pkg/apis/lifecycle/doc.go
Normal 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
|
||||
38
pkg/apis/lifecycle/install/install.go
Normal file
38
pkg/apis/lifecycle/install/install.go
Normal 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))
|
||||
}
|
||||
56
pkg/apis/lifecycle/register.go
Normal file
56
pkg/apis/lifecycle/register.go
Normal 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
590
pkg/apis/lifecycle/types.go
Normal 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
|
||||
}
|
||||
26
pkg/apis/lifecycle/v1alpha1/doc.go
Normal file
26
pkg/apis/lifecycle/v1alpha1/doc.go
Normal 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
|
||||
46
pkg/apis/lifecycle/v1alpha1/register.go
Normal file
46
pkg/apis/lifecycle/v1alpha1/register.go
Normal 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)
|
||||
}
|
||||
37
pkg/apis/lifecycle/v1alpha1/validation.go
Normal file
37
pkg/apis/lifecycle/v1alpha1/validation.go
Normal 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
|
||||
}
|
||||
616
pkg/apis/lifecycle/validation/validation.go
Normal file
616
pkg/apis/lifecycle/validation/validation.go
Normal 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)
|
||||
})
|
||||
}
|
||||
1993
pkg/apis/lifecycle/validation/validation_test.go
Normal file
1993
pkg/apis/lifecycle/validation/validation_test.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
17
pkg/registry/lifecycle/eviction/doc.go
Normal file
17
pkg/registry/lifecycle/eviction/doc.go
Normal 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
|
||||
107
pkg/registry/lifecycle/eviction/storage/storage.go
Normal file
107
pkg/registry/lifecycle/eviction/storage/storage.go
Normal 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)
|
||||
}
|
||||
274
pkg/registry/lifecycle/eviction/storage/storage_test.go
Normal file
274
pkg/registry/lifecycle/eviction/storage/storage_test.go
Normal 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
|
||||
}
|
||||
182
pkg/registry/lifecycle/eviction/strategy.go
Normal file
182
pkg/registry/lifecycle/eviction/strategy.go
Normal 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
|
||||
}
|
||||
296
pkg/registry/lifecycle/eviction/strategy_test.go
Normal file
296
pkg/registry/lifecycle/eviction/strategy_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
17
pkg/registry/lifecycle/evictionrequest/doc.go
Normal file
17
pkg/registry/lifecycle/evictionrequest/doc.go
Normal 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
|
||||
106
pkg/registry/lifecycle/evictionrequest/storage/storage.go
Normal file
106
pkg/registry/lifecycle/evictionrequest/storage/storage.go
Normal 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)
|
||||
}
|
||||
263
pkg/registry/lifecycle/evictionrequest/storage/storage_test.go
Normal file
263
pkg/registry/lifecycle/evictionrequest/storage/storage_test.go
Normal 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
|
||||
}
|
||||
177
pkg/registry/lifecycle/evictionrequest/strategy.go
Normal file
177
pkg/registry/lifecycle/evictionrequest/strategy.go
Normal 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
|
||||
}
|
||||
271
pkg/registry/lifecycle/evictionrequest/strategy_test.go
Normal file
271
pkg/registry/lifecycle/evictionrequest/strategy_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
85
pkg/registry/lifecycle/rest/storage_coordination.go
Normal file
85
pkg/registry/lifecycle/rest/storage_coordination.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
25
staging/src/k8s.io/api/lifecycle/v1alpha1/doc.go
Normal file
25
staging/src/k8s.io/api/lifecycle/v1alpha1/doc.go
Normal 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
|
||||
55
staging/src/k8s.io/api/lifecycle/v1alpha1/register.go
Normal file
55
staging/src/k8s.io/api/lifecycle/v1alpha1/register.go
Normal 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
|
||||
}
|
||||
680
staging/src/k8s.io/api/lifecycle/v1alpha1/types.go
Normal file
680
staging/src/k8s.io/api/lifecycle/v1alpha1/types.go
Normal 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"`
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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": []}`,
|
||||
|
||||
Reference in New Issue
Block a user