/
githubmirror
/
kubernetes
Обзор
Документация
Войти
/
githubmirror
/
kubernetes
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
pkg/apis/lifecycle/validation/validation.go
616 строк
28 KB
Filip Křepinský
introduce EvictionRequest and Eviction API
22 июл 2026, 22:32
22 июл 2026, 22:32
fc558fb
Код
Авторство
О чём код?
/* 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) }) }