mirror of
https://github.com/kubernetes/kubernetes.git
synced 2026-11-06 05:17:08 +00:00
366 lines
13 KiB
Go
366 lines
13 KiB
Go
/*
|
|
Copyright The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package evictionrequest
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
lifecyclev1alpha1 "k8s.io/api/lifecycle/v1alpha1"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/util/validation"
|
|
metav1ac "k8s.io/client-go/applyconfigurations/meta/v1"
|
|
"k8s.io/kubernetes/pkg/apis/lifecycle"
|
|
validation2 "k8s.io/kubernetes/pkg/apis/lifecycle/validation"
|
|
)
|
|
|
|
var evictionNameRegex = regexp.MustCompile("^([a-z]+)-([0-9]+)-(.*)$")
|
|
|
|
func extractEvictionNameParts(name string) (string, int, string, bool) {
|
|
if subMatches := evictionNameRegex.FindStringSubmatch(name); len(subMatches) > 3 {
|
|
targetTypePart := subMatches[1]
|
|
counterPart := subMatches[2]
|
|
namePart := subMatches[3]
|
|
counter, err := strconv.Atoi(counterPart)
|
|
if err != nil {
|
|
return targetTypePart, -1, namePart, false
|
|
}
|
|
return targetTypePart, counter, namePart, true
|
|
}
|
|
return "", -1, "", false
|
|
}
|
|
|
|
// evictionNameForCounter returns the Eviction name for the target and counter,
|
|
// truncated to a valid DNS1123 subdomain.
|
|
func evictionNameForCounter(target targetInfo, counter int) string {
|
|
name := fmt.Sprintf("%s-%d-%s", target.targetType().String(), counter, target.targetName())
|
|
if len(name) > validation.DNS1123SubdomainMaxLength {
|
|
name = strings.TrimRight(name[:validation.DNS1123SubdomainMaxLength], "-.")
|
|
}
|
|
return name
|
|
}
|
|
|
|
// newEvictionName returns the name in pod-1-foo format. If there are Evictions with a conflicting name,
|
|
// the counter is increased and pod-2-foo, etc. name is returned.
|
|
func newEvictionName(target targetInfo, allEvictions []*lifecyclev1alpha1.Eviction) string {
|
|
nameCounter := 0
|
|
|
|
if len(target.targetName()) == 0 {
|
|
return ""
|
|
}
|
|
|
|
for _, eviction := range allEvictions {
|
|
if targetType, counter, _, ok := extractEvictionNameParts(eviction.Name); ok && targetType == target.targetType().String() {
|
|
// Compare against the (possibly truncated) name we would generate for this
|
|
// counter. Two different very long pod names can share a counter space.
|
|
if eviction.Name == evictionNameForCounter(target, counter) && counter > nameCounter {
|
|
nameCounter = counter
|
|
}
|
|
}
|
|
}
|
|
return evictionNameForCounter(target, nameCounter+1)
|
|
}
|
|
|
|
func compareEvictionNames(nameA, nameB string) int {
|
|
targetTypeA, counterA, namePartA, okA := extractEvictionNameParts(nameA)
|
|
targetTypeB, counterB, namePartB, okB := extractEvictionNameParts(nameB)
|
|
|
|
if okA && !okB {
|
|
return -1
|
|
}
|
|
if !okA && okB {
|
|
return 1
|
|
}
|
|
if okA && okB {
|
|
targetCmp := strings.Compare(targetTypeA, targetTypeB)
|
|
if targetCmp != 0 {
|
|
return targetCmp
|
|
}
|
|
namePartCmp := strings.Compare(namePartA, namePartB)
|
|
if namePartCmp != 0 {
|
|
return namePartCmp
|
|
}
|
|
if counterA != counterB {
|
|
return counterA - counterB
|
|
}
|
|
}
|
|
|
|
return strings.Compare(nameA, nameB)
|
|
}
|
|
|
|
// findRelevantEviction finds the most relevant Eviction for the EvictionRequest
|
|
// 1. an Eviction with TargetEvicted=True condition
|
|
// 2. the oldest active Eviction
|
|
// 3. an Eviction with Failed=True condition
|
|
// 4. nil if no Eviction matches
|
|
func findRelevantEviction(evictions []*lifecyclev1alpha1.Eviction) (*lifecyclev1alpha1.Eviction, bool) {
|
|
var active, succeeded, failed *lifecyclev1alpha1.Eviction
|
|
|
|
for _, eviction := range evictions {
|
|
switch {
|
|
case hasEvictionSucceeded(eviction):
|
|
// Succeeded eviction with the latest transition time.
|
|
if succeeded == nil || compareConditionTransitionTime(succeeded, eviction, lifecyclev1alpha1.EvictionConditionTargetEvicted) < 0 {
|
|
succeeded = eviction
|
|
}
|
|
case hasEvictionFailed(eviction):
|
|
// Failed eviction with the latest transition time.
|
|
if failed == nil || compareConditionTransitionTime(failed, eviction, lifecyclev1alpha1.EvictionConditionFailed) < 0 {
|
|
failed = eviction
|
|
}
|
|
default:
|
|
// Oldest active eviction.
|
|
if active == nil {
|
|
active = eviction
|
|
} else {
|
|
timeCmp := active.CreationTimestamp.Time.Compare(eviction.CreationTimestamp.Time)
|
|
if timeCmp > 0 {
|
|
active = eviction
|
|
} else if timeCmp == 0 && compareEvictionNames(active.Name, eviction.Name) > 0 {
|
|
active = eviction
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if succeeded != nil {
|
|
return succeeded, false
|
|
}
|
|
if active != nil {
|
|
return active, false
|
|
}
|
|
return failed, true
|
|
}
|
|
|
|
func convertToEvictionRequestConditions(now time.Time, evictionRequest *lifecyclev1alpha1.EvictionRequest, eviction *lifecyclev1alpha1.Eviction, conditionType lifecyclev1alpha1.EvictionConditionType) (*metav1ac.ConditionApplyConfiguration, bool) {
|
|
if eviction != nil {
|
|
if condition := meta.FindStatusCondition(eviction.Status.Conditions, string(conditionType)); condition != nil {
|
|
return setCondition(now, evictionRequest.Status.Conditions, conditionType,
|
|
condition.Status, lifecyclev1alpha1.EvictionConditionReason(condition.Reason), condition.Message)
|
|
}
|
|
}
|
|
return setCondition(now, evictionRequest.Status.Conditions, conditionType,
|
|
metav1.ConditionFalse, lifecyclev1alpha1.EvictionConditionReasonAwaitingEviction, "waiting for an Eviction to report on progress")
|
|
}
|
|
|
|
func compareConditionTransitionTime(a, b *lifecyclev1alpha1.Eviction, conditionType lifecyclev1alpha1.EvictionConditionType) int {
|
|
aCond := meta.FindStatusCondition(a.Status.Conditions, string(conditionType))
|
|
bCond := meta.FindStatusCondition(b.Status.Conditions, string(conditionType))
|
|
if aCond == nil && bCond == nil {
|
|
return 0
|
|
}
|
|
if aCond == nil && bCond != nil {
|
|
return -1
|
|
}
|
|
if aCond != nil && bCond == nil {
|
|
return 1
|
|
}
|
|
return aCond.LastTransitionTime.Time.Compare(bCond.LastTransitionTime.Time)
|
|
}
|
|
|
|
// setCondition creates or updates a condition in the apply configuration,
|
|
// preserving the LastTransitionTime when the status has not changed.
|
|
// Inspired by k8s.io/apimachinery/pkg/api/meta.SetStatusCondition.
|
|
func setCondition(
|
|
now time.Time,
|
|
existingConditions []metav1.Condition,
|
|
conditionType lifecyclev1alpha1.EvictionConditionType,
|
|
status metav1.ConditionStatus,
|
|
reason lifecyclev1alpha1.EvictionConditionReason,
|
|
message string,
|
|
) (*metav1ac.ConditionApplyConfiguration, bool) {
|
|
transitionTime := metav1.Time{Time: now}
|
|
|
|
existing := meta.FindStatusCondition(existingConditions, string(conditionType))
|
|
if existing != nil && existing.Status == status && !existing.LastTransitionTime.IsZero() {
|
|
// Status unchanged, so we preserve the original transition time
|
|
transitionTime = existing.LastTransitionTime
|
|
}
|
|
hasChanged := existing == nil ||
|
|
existing.Status != status ||
|
|
existing.Type != string(conditionType) ||
|
|
existing.Reason != string(reason) ||
|
|
existing.Message != message
|
|
|
|
return metav1ac.Condition().
|
|
WithType(string(conditionType)).
|
|
WithStatus(status).
|
|
WithReason(string(reason)).
|
|
WithMessage(message).
|
|
WithLastTransitionTime(transitionTime), hasChanged
|
|
}
|
|
|
|
// hasCompleted returns true if the Eviction has reached
|
|
// a terminal state (Canceled or Evicted condition is True).
|
|
func hasEvictionRequestCompleted(evictionRequest *lifecyclev1alpha1.EvictionRequest) bool {
|
|
if meta.IsStatusConditionTrue(evictionRequest.Status.Conditions, string(lifecyclev1alpha1.EvictionConditionTargetEvicted)) {
|
|
return true
|
|
}
|
|
failedCondition := meta.FindStatusCondition(evictionRequest.Status.Conditions, string(lifecyclev1alpha1.EvictionConditionFailed))
|
|
if failedCondition != nil && failedCondition.Status == metav1.ConditionTrue &&
|
|
failedCondition.Reason == string(lifecyclev1alpha1.EvictionConditionReasonEvictionInvalid) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func hasEvictionIntent(targetEvictionRequests []*lifecyclev1alpha1.EvictionRequest) bool {
|
|
for _, request := range targetEvictionRequests {
|
|
// only non deleted requests are considered active
|
|
if request.DeletionTimestamp == nil && request.Spec.Intent == lifecyclev1alpha1.EvictionRequestIntentEviction {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func evictionRequestAsOwnerReference(evictionRequest *lifecyclev1alpha1.EvictionRequest) *metav1ac.OwnerReferenceApplyConfiguration {
|
|
gvk := lifecyclev1alpha1.SchemeGroupVersion.WithKind("EvictionRequest")
|
|
return metav1ac.OwnerReference().
|
|
WithKind(gvk.Kind).
|
|
WithAPIVersion(gvk.GroupVersion().String()).
|
|
WithName(evictionRequest.Name).
|
|
WithUID(evictionRequest.UID)
|
|
}
|
|
|
|
func computeEvictionLabels(eviction *lifecyclev1alpha1.Eviction, target targetInfo, targetEvictionRequests []*lifecyclev1alpha1.EvictionRequest) map[string]string {
|
|
labels := map[string]string{}
|
|
|
|
// refresh requester and responder labels
|
|
for _, request := range targetEvictionRequests {
|
|
labels[request.Spec.Requester] = string(lifecyclev1alpha1.EvictionParticipantRoleRequester)
|
|
}
|
|
|
|
if target.isGone() {
|
|
// if the target is gone we have to preserve our managed responder labels
|
|
for k, v := range eviction.Labels {
|
|
switch v {
|
|
case string(lifecyclev1alpha1.EvictionParticipantRoleRequesterResponder):
|
|
if len(labels[k]) > 0 {
|
|
labels[k] = v
|
|
} else {
|
|
// demote to a responder
|
|
labels[k] = string(lifecyclev1alpha1.EvictionParticipantRoleResponder)
|
|
}
|
|
case string(lifecyclev1alpha1.EvictionParticipantRoleResponder):
|
|
if len(labels[k]) > 0 {
|
|
// promote a responder
|
|
labels[k] = string(lifecyclev1alpha1.EvictionParticipantRoleRequesterResponder)
|
|
} else {
|
|
labels[k] = v
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
for _, responder := range target.evictionResponders(true) {
|
|
if len(labels[responder.Name]) > 0 {
|
|
labels[responder.Name] = string(lifecyclev1alpha1.EvictionParticipantRoleRequesterResponder)
|
|
} else {
|
|
labels[responder.Name] = string(lifecyclev1alpha1.EvictionParticipantRoleResponder)
|
|
}
|
|
}
|
|
}
|
|
return labels
|
|
}
|
|
|
|
// evictionLabelsNeedSSAUpdate reports label changes that are interesting for us: additions, changes,
|
|
// and removal of only a single label.
|
|
func evictionLabelsNeedSSAUpdate(oldLabels, newLabels map[string]string) bool {
|
|
for k, v := range newLabels {
|
|
if oldLabels == nil || oldLabels[k] != v {
|
|
// new label added or updated
|
|
return true
|
|
}
|
|
}
|
|
for k, v := range oldLabels {
|
|
// Only requester labels can be removed for now, as responders are immutable.
|
|
if v == string(lifecyclev1alpha1.EvictionParticipantRoleRequester) {
|
|
if len(newLabels[k]) == 0 {
|
|
// requester removed
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// shouldDeferCompletion returns how long to wait before setting the completion
|
|
// condition, giving the active responder time to report its final status
|
|
// before the eviction is finalized. Returns 0 when no deferral is needed.
|
|
func shouldDeferCompletion(now time.Time, activeResponderStatus *lifecyclev1alpha1.ResponderStatus, terminalTime *time.Time) *time.Duration {
|
|
if activeResponderStatus == nil || activeResponderStatus.CompletionTime != nil || terminalTime == nil {
|
|
return nil
|
|
}
|
|
|
|
if remaining := GracefulCompletionDelay - now.Sub(*terminalTime); remaining > 0 {
|
|
return new(remaining)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func findTargetResponderIdx(targetResponders []lifecyclev1alpha1.TargetResponder, state lifecyclev1alpha1.ResponderStateType) int {
|
|
for i, responder := range targetResponders {
|
|
if responder.State == state {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// findResponderStatus finds the status for a given responder name.
|
|
func findResponderStatus(statuses []lifecyclev1alpha1.ResponderStatus, name string) *lifecyclev1alpha1.ResponderStatus {
|
|
for i := range statuses {
|
|
if statuses[i].Name == name {
|
|
return &statuses[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SortTargetResponders returns highest priority responders on a lower index.
|
|
// If there are responders with the same priority, the responder whose domain name comes first in the
|
|
// alphabetical higher domain order, will be picked. This means that the top domain labels are compared
|
|
// alphabetically first, followed by the lower domain labels. The key is compared last.
|
|
//
|
|
// NOTE: this is a wrapper around validation.SortTargetResponders in order not to duplicate the sorting logic
|
|
func sortTargetResponders(targetResponders []lifecyclev1alpha1.TargetResponder) []lifecyclev1alpha1.TargetResponder {
|
|
if len(targetResponders) == 0 {
|
|
return nil
|
|
}
|
|
input := make([]lifecycle.TargetResponder, 0, len(targetResponders))
|
|
for _, responder := range targetResponders {
|
|
input = append(input, lifecycle.TargetResponder{
|
|
Name: responder.Name,
|
|
Priority: responder.Priority,
|
|
State: lifecycle.ResponderStateType(responder.State),
|
|
})
|
|
}
|
|
validation2.SortTargetResponders(input)
|
|
output := make([]lifecyclev1alpha1.TargetResponder, 0, len(targetResponders))
|
|
for _, responder := range input {
|
|
output = append(output, lifecyclev1alpha1.TargetResponder{
|
|
Name: responder.Name,
|
|
Priority: responder.Priority,
|
|
State: lifecyclev1alpha1.ResponderStateType(responder.State),
|
|
})
|
|
}
|
|
return output
|
|
}
|