Coordinated Leader Election Alpha API

This commit is contained in:
Jefftree
2024-07-21 20:02:00 +00:00
parent ab470aad01
commit 3999b98c88
100 changed files with 8329 additions and 100 deletions

View File

@@ -24,6 +24,7 @@ import (
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/apis/coordination"
v1 "k8s.io/kubernetes/pkg/apis/coordination/v1"
"k8s.io/kubernetes/pkg/apis/coordination/v1alpha1"
"k8s.io/kubernetes/pkg/apis/coordination/v1beta1"
)
@@ -34,7 +35,8 @@ func init() {
// Install registers the API group and adds types to a scheme
func Install(scheme *runtime.Scheme) {
utilruntime.Must(coordination.AddToScheme(scheme))
utilruntime.Must(v1alpha1.AddToScheme(scheme))
utilruntime.Must(v1beta1.AddToScheme(scheme))
utilruntime.Must(v1.AddToScheme(scheme))
utilruntime.Must(scheme.SetVersionPriority(v1.SchemeGroupVersion, v1beta1.SchemeGroupVersion))
utilruntime.Must(scheme.SetVersionPriority(v1.SchemeGroupVersion, v1beta1.SchemeGroupVersion, v1alpha1.SchemeGroupVersion))
}

View File

@@ -50,6 +50,8 @@ func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Lease{},
&LeaseList{},
&LeaseCandidate{},
&LeaseCandidateList{},
)
return nil
}

View File

@@ -20,6 +20,18 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type CoordinatedLeaseStrategy string
// CoordinatedLeaseStrategy defines the strategy for picking the leader for coordinated leader election.
const (
// OldestEmulationVersion picks the oldest LeaseCandidate, where "oldest" is defined as follows
// 1) Select the candidate(s) with the lowest emulation version
// 2) If multiple candidates have the same emulation version, select the candidate(s) with the lowest binary version. (Note that binary version must be greater or equal to emulation version)
// 3) If multiple candidates have the same binary version, select the candidate with the oldest creationTimestamp.
// If a candidate does not specify the emulationVersion and binaryVersion fields, it will not be considered a candidate for the lease.
OldestEmulationVersion CoordinatedLeaseStrategy = "OldestEmulationVersion"
)
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// Lease defines a lease concept.
@@ -36,6 +48,8 @@ type Lease struct {
// LeaseSpec is a specification of a Lease.
type LeaseSpec struct {
// holderIdentity contains the identity of the holder of a current lease.
// If Coordinated Leader Election is used, the holder identity must be
// equal to the elected LeaseCandidate.metadata.name field.
// +optional
HolderIdentity *string
// leaseDurationSeconds is a duration that candidates for a lease need
@@ -54,6 +68,18 @@ type LeaseSpec struct {
// holders.
// +optional
LeaseTransitions *int32
// Strategy indicates the strategy for picking the leader for coordinated leader election.
// If the field is not specified, there is no active coordination for this lease.
// (Alpha) Using this field requires the CoordinatedLeaderElection feature gate to be enabled.
// +featureGate=CoordinatedLeaderElection
// +optional
Strategy *CoordinatedLeaseStrategy
// PreferredHolder signals to a lease holder that the lease has a
// more optimal holder and should be given up.
// This field can only be set if Strategy is also set.
// +featureGate=CoordinatedLeaderElection
// +optional
PreferredHolder *string
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
@@ -67,3 +93,68 @@ type LeaseList struct {
// items is a list of schema objects.
Items []Lease
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// LeaseCandidate defines a candidate for a lease object.
// Candidates are created such that coordinated leader election will pick the best leader from the list of candidates.
type LeaseCandidate struct {
metav1.TypeMeta
// +optional
metav1.ObjectMeta
Spec LeaseCandidateSpec
}
// LeaseCandidateSpec is a specification of a Lease.
type LeaseCandidateSpec struct {
// LeaseName is the name of the lease for which this candidate is contending.
// This field is immutable.
LeaseName string
// PingTime is the last time that the server has requested the LeaseCandidate
// to renew. It is only done during leader election to check if any
// LeaseCandidates have become ineligible. When PingTime is updated, the
// LeaseCandidate will respond by updating RenewTime.
// +optional
PingTime *metav1.MicroTime
// RenewTime is the time that the LeaseCandidate was last updated.
// Any time a Lease needs to do leader election, the PingTime field
// is updated to signal to the LeaseCandidate that they should update
// the RenewTime.
// Old LeaseCandidate objects are also garbage collected if it has been hours since the last renew.
// +optional
RenewTime *metav1.MicroTime
// BinaryVersion is the binary version. It must be in a semver format without leadig `v`.
// This field is required when Strategy is "OldestEmulationVersion"
// +optional
BinaryVersion string
// EmulationVersion is the emulation version. It must be in a semver format without leading `v`.
// EmulationVersion must be less than or equal to BinaryVersion.
// This field is required when Strategy is "OldestEmulationVersion"
// +optional
EmulationVersion string
// PreferredStrategies indicates the list of strategies for picking the leader for coordinated leader election.
// The list is ordered, and the first strategy supersedes all other strategies. The list is used by coordinated
// leader election to make a decision about the final election strategy. This follows as
// - If all clients have strategy X as the first element in this list, strategy X will be used.
// - If a candidate has strategy [X] and another candidate has strategy [Y, X], Y supersedes X and strategy Y
// will be used
// - If a candidate has strategy [X, Y] and another candidate has strategy [Y, X], this is a user error and leader
// election will not operate the Lease until resolved.
// (Alpha) Using this field requires the CoordinatedLeaderElection feature gate to be enabled.
// +featureGate=CoordinatedLeaderElection
// +listType=atomic
// +required
PreferredStrategies []CoordinatedLeaseStrategy
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// LeaseCandidateList is a list of LeaseCandidate objects.
type LeaseCandidateList struct {
metav1.TypeMeta
// +optional
metav1.ListMeta
// items is a list of schema objects.
Items []LeaseCandidate
}

View File

@@ -125,6 +125,8 @@ func autoConvert_v1_LeaseSpec_To_coordination_LeaseSpec(in *v1.LeaseSpec, out *c
out.AcquireTime = (*metav1.MicroTime)(unsafe.Pointer(in.AcquireTime))
out.RenewTime = (*metav1.MicroTime)(unsafe.Pointer(in.RenewTime))
out.LeaseTransitions = (*int32)(unsafe.Pointer(in.LeaseTransitions))
out.Strategy = (*coordination.CoordinatedLeaseStrategy)(unsafe.Pointer(in.Strategy))
out.PreferredHolder = (*string)(unsafe.Pointer(in.PreferredHolder))
return nil
}
@@ -139,6 +141,8 @@ func autoConvert_coordination_LeaseSpec_To_v1_LeaseSpec(in *coordination.LeaseSp
out.AcquireTime = (*metav1.MicroTime)(unsafe.Pointer(in.AcquireTime))
out.RenewTime = (*metav1.MicroTime)(unsafe.Pointer(in.RenewTime))
out.LeaseTransitions = (*int32)(unsafe.Pointer(in.LeaseTransitions))
out.Strategy = (*v1.CoordinatedLeaseStrategy)(unsafe.Pointer(in.Strategy))
out.PreferredHolder = (*string)(unsafe.Pointer(in.PreferredHolder))
return nil
}

View File

@@ -0,0 +1,24 @@
/*
Copyright 2024 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/coordination
// +k8s:conversion-gen-external-types=k8s.io/api/coordination/v1alpha1
// +k8s:defaulter-gen=TypeMeta
// +k8s:defaulter-gen-input=k8s.io/api/coordination/v1alpha1
// +groupName=coordination.k8s.io
package v1alpha1 // import "k8s.io/kubernetes/pkg/apis/coordination/v1alpha1"

View File

@@ -0,0 +1,46 @@
/*
Copyright 2024 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 (
coordinationv1alpha1 "k8s.io/api/coordination/v1alpha1"
"k8s.io/apimachinery/pkg/runtime/schema"
)
// GroupName is the group name use in this package
const GroupName = "coordination.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 = &coordinationv1alpha1.SchemeBuilder
// AddToScheme is a common registration function for mapping packaged scoped group & version keys to a scheme
AddToScheme = localSchemeBuilder.AddToScheme
)
func init() {
// We only register manually written functions here. The registration of the
// generated functions takes place in the generated files. The separation
// makes the code compile even when the generated files are missing.
localSchemeBuilder.Register(RegisterDefaults)
}

View File

@@ -0,0 +1,151 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by conversion-gen. DO NOT EDIT.
package v1alpha1
import (
unsafe "unsafe"
coordinationv1 "k8s.io/api/coordination/v1"
v1alpha1 "k8s.io/api/coordination/v1alpha1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
conversion "k8s.io/apimachinery/pkg/conversion"
runtime "k8s.io/apimachinery/pkg/runtime"
coordination "k8s.io/kubernetes/pkg/apis/coordination"
)
func init() {
localSchemeBuilder.Register(RegisterConversions)
}
// RegisterConversions adds conversion functions to the given scheme.
// Public to allow building arbitrary schemes.
func RegisterConversions(s *runtime.Scheme) error {
if err := s.AddGeneratedConversionFunc((*v1alpha1.LeaseCandidate)(nil), (*coordination.LeaseCandidate)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_LeaseCandidate_To_coordination_LeaseCandidate(a.(*v1alpha1.LeaseCandidate), b.(*coordination.LeaseCandidate), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*coordination.LeaseCandidate)(nil), (*v1alpha1.LeaseCandidate)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_coordination_LeaseCandidate_To_v1alpha1_LeaseCandidate(a.(*coordination.LeaseCandidate), b.(*v1alpha1.LeaseCandidate), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*v1alpha1.LeaseCandidateList)(nil), (*coordination.LeaseCandidateList)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_LeaseCandidateList_To_coordination_LeaseCandidateList(a.(*v1alpha1.LeaseCandidateList), b.(*coordination.LeaseCandidateList), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*coordination.LeaseCandidateList)(nil), (*v1alpha1.LeaseCandidateList)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_coordination_LeaseCandidateList_To_v1alpha1_LeaseCandidateList(a.(*coordination.LeaseCandidateList), b.(*v1alpha1.LeaseCandidateList), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*v1alpha1.LeaseCandidateSpec)(nil), (*coordination.LeaseCandidateSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_v1alpha1_LeaseCandidateSpec_To_coordination_LeaseCandidateSpec(a.(*v1alpha1.LeaseCandidateSpec), b.(*coordination.LeaseCandidateSpec), scope)
}); err != nil {
return err
}
if err := s.AddGeneratedConversionFunc((*coordination.LeaseCandidateSpec)(nil), (*v1alpha1.LeaseCandidateSpec)(nil), func(a, b interface{}, scope conversion.Scope) error {
return Convert_coordination_LeaseCandidateSpec_To_v1alpha1_LeaseCandidateSpec(a.(*coordination.LeaseCandidateSpec), b.(*v1alpha1.LeaseCandidateSpec), scope)
}); err != nil {
return err
}
return nil
}
func autoConvert_v1alpha1_LeaseCandidate_To_coordination_LeaseCandidate(in *v1alpha1.LeaseCandidate, out *coordination.LeaseCandidate, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
if err := Convert_v1alpha1_LeaseCandidateSpec_To_coordination_LeaseCandidateSpec(&in.Spec, &out.Spec, s); err != nil {
return err
}
return nil
}
// Convert_v1alpha1_LeaseCandidate_To_coordination_LeaseCandidate is an autogenerated conversion function.
func Convert_v1alpha1_LeaseCandidate_To_coordination_LeaseCandidate(in *v1alpha1.LeaseCandidate, out *coordination.LeaseCandidate, s conversion.Scope) error {
return autoConvert_v1alpha1_LeaseCandidate_To_coordination_LeaseCandidate(in, out, s)
}
func autoConvert_coordination_LeaseCandidate_To_v1alpha1_LeaseCandidate(in *coordination.LeaseCandidate, out *v1alpha1.LeaseCandidate, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
if err := Convert_coordination_LeaseCandidateSpec_To_v1alpha1_LeaseCandidateSpec(&in.Spec, &out.Spec, s); err != nil {
return err
}
return nil
}
// Convert_coordination_LeaseCandidate_To_v1alpha1_LeaseCandidate is an autogenerated conversion function.
func Convert_coordination_LeaseCandidate_To_v1alpha1_LeaseCandidate(in *coordination.LeaseCandidate, out *v1alpha1.LeaseCandidate, s conversion.Scope) error {
return autoConvert_coordination_LeaseCandidate_To_v1alpha1_LeaseCandidate(in, out, s)
}
func autoConvert_v1alpha1_LeaseCandidateList_To_coordination_LeaseCandidateList(in *v1alpha1.LeaseCandidateList, out *coordination.LeaseCandidateList, s conversion.Scope) error {
out.ListMeta = in.ListMeta
out.Items = *(*[]coordination.LeaseCandidate)(unsafe.Pointer(&in.Items))
return nil
}
// Convert_v1alpha1_LeaseCandidateList_To_coordination_LeaseCandidateList is an autogenerated conversion function.
func Convert_v1alpha1_LeaseCandidateList_To_coordination_LeaseCandidateList(in *v1alpha1.LeaseCandidateList, out *coordination.LeaseCandidateList, s conversion.Scope) error {
return autoConvert_v1alpha1_LeaseCandidateList_To_coordination_LeaseCandidateList(in, out, s)
}
func autoConvert_coordination_LeaseCandidateList_To_v1alpha1_LeaseCandidateList(in *coordination.LeaseCandidateList, out *v1alpha1.LeaseCandidateList, s conversion.Scope) error {
out.ListMeta = in.ListMeta
out.Items = *(*[]v1alpha1.LeaseCandidate)(unsafe.Pointer(&in.Items))
return nil
}
// Convert_coordination_LeaseCandidateList_To_v1alpha1_LeaseCandidateList is an autogenerated conversion function.
func Convert_coordination_LeaseCandidateList_To_v1alpha1_LeaseCandidateList(in *coordination.LeaseCandidateList, out *v1alpha1.LeaseCandidateList, s conversion.Scope) error {
return autoConvert_coordination_LeaseCandidateList_To_v1alpha1_LeaseCandidateList(in, out, s)
}
func autoConvert_v1alpha1_LeaseCandidateSpec_To_coordination_LeaseCandidateSpec(in *v1alpha1.LeaseCandidateSpec, out *coordination.LeaseCandidateSpec, s conversion.Scope) error {
out.LeaseName = in.LeaseName
out.PingTime = (*v1.MicroTime)(unsafe.Pointer(in.PingTime))
out.RenewTime = (*v1.MicroTime)(unsafe.Pointer(in.RenewTime))
out.BinaryVersion = in.BinaryVersion
out.EmulationVersion = in.EmulationVersion
out.PreferredStrategies = *(*[]coordination.CoordinatedLeaseStrategy)(unsafe.Pointer(&in.PreferredStrategies))
return nil
}
// Convert_v1alpha1_LeaseCandidateSpec_To_coordination_LeaseCandidateSpec is an autogenerated conversion function.
func Convert_v1alpha1_LeaseCandidateSpec_To_coordination_LeaseCandidateSpec(in *v1alpha1.LeaseCandidateSpec, out *coordination.LeaseCandidateSpec, s conversion.Scope) error {
return autoConvert_v1alpha1_LeaseCandidateSpec_To_coordination_LeaseCandidateSpec(in, out, s)
}
func autoConvert_coordination_LeaseCandidateSpec_To_v1alpha1_LeaseCandidateSpec(in *coordination.LeaseCandidateSpec, out *v1alpha1.LeaseCandidateSpec, s conversion.Scope) error {
out.LeaseName = in.LeaseName
out.PingTime = (*v1.MicroTime)(unsafe.Pointer(in.PingTime))
out.RenewTime = (*v1.MicroTime)(unsafe.Pointer(in.RenewTime))
out.BinaryVersion = in.BinaryVersion
out.EmulationVersion = in.EmulationVersion
out.PreferredStrategies = *(*[]coordinationv1.CoordinatedLeaseStrategy)(unsafe.Pointer(&in.PreferredStrategies))
return nil
}
// Convert_coordination_LeaseCandidateSpec_To_v1alpha1_LeaseCandidateSpec is an autogenerated conversion function.
func Convert_coordination_LeaseCandidateSpec_To_v1alpha1_LeaseCandidateSpec(in *coordination.LeaseCandidateSpec, out *v1alpha1.LeaseCandidateSpec, s conversion.Scope) error {
return autoConvert_coordination_LeaseCandidateSpec_To_v1alpha1_LeaseCandidateSpec(in, out, s)
}

View File

@@ -0,0 +1,33 @@
//go:build !ignore_autogenerated
// +build !ignore_autogenerated
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by defaulter-gen. DO NOT EDIT.
package v1alpha1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// RegisterDefaults adds defaulters functions to the given scheme.
// Public to allow building arbitrary schemes.
// All generated defaulters are covering - they call all nested defaulters.
func RegisterDefaults(scheme *runtime.Scheme) error {
return nil
}

View File

@@ -24,6 +24,7 @@ package v1beta1
import (
unsafe "unsafe"
coordinationv1 "k8s.io/api/coordination/v1"
v1beta1 "k8s.io/api/coordination/v1beta1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
conversion "k8s.io/apimachinery/pkg/conversion"
@@ -125,6 +126,8 @@ func autoConvert_v1beta1_LeaseSpec_To_coordination_LeaseSpec(in *v1beta1.LeaseSp
out.AcquireTime = (*v1.MicroTime)(unsafe.Pointer(in.AcquireTime))
out.RenewTime = (*v1.MicroTime)(unsafe.Pointer(in.RenewTime))
out.LeaseTransitions = (*int32)(unsafe.Pointer(in.LeaseTransitions))
out.Strategy = (*coordination.CoordinatedLeaseStrategy)(unsafe.Pointer(in.Strategy))
out.PreferredHolder = (*string)(unsafe.Pointer(in.PreferredHolder))
return nil
}
@@ -139,6 +142,8 @@ func autoConvert_coordination_LeaseSpec_To_v1beta1_LeaseSpec(in *coordination.Le
out.AcquireTime = (*v1.MicroTime)(unsafe.Pointer(in.AcquireTime))
out.RenewTime = (*v1.MicroTime)(unsafe.Pointer(in.RenewTime))
out.LeaseTransitions = (*int32)(unsafe.Pointer(in.LeaseTransitions))
out.Strategy = (*coordinationv1.CoordinatedLeaseStrategy)(unsafe.Pointer(in.Strategy))
out.PreferredHolder = (*string)(unsafe.Pointer(in.PreferredHolder))
return nil
}

View File

@@ -17,11 +17,20 @@ limitations under the License.
package validation
import (
"slices"
"strings"
"github.com/blang/semver/v4"
"k8s.io/apimachinery/pkg/api/validation"
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/kubernetes/pkg/apis/coordination"
apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
)
var validLeaseStrategies = []coordination.CoordinatedLeaseStrategy{coordination.OldestEmulationVersion}
// ValidateLease validates a Lease.
func ValidateLease(lease *coordination.Lease) field.ErrorList {
allErrs := validation.ValidateObjectMeta(&lease.ObjectMeta, true, validation.NameIsDNSSubdomain, field.NewPath("metadata"))
@@ -48,5 +57,119 @@ func ValidateLeaseSpec(spec *coordination.LeaseSpec, fldPath *field.Path) field.
fld := fldPath.Child("leaseTransitions")
allErrs = append(allErrs, field.Invalid(fld, spec.LeaseTransitions, "must be greater than or equal to 0"))
}
if spec.Strategy != nil {
allErrs = append(allErrs, ValidateCoordinatedLeaseStrategy(*spec.Strategy, fldPath.Child("strategy"))...)
}
if spec.PreferredHolder != nil && *spec.PreferredHolder != "" && (spec.Strategy == nil || *spec.Strategy == "") {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("preferredHolder"), "may only be specified if `strategy` is defined"))
}
// spec.RenewTime is a MicroTime and doesn't need further validation
return allErrs
}
// ValidateLeaseCandidate validates a LeaseCandidate.
func ValidateLeaseCandidate(lease *coordination.LeaseCandidate) field.ErrorList {
allErrs := validation.ValidateObjectMeta(&lease.ObjectMeta, true, ValidLeaseCandidateName, field.NewPath("metadata"))
allErrs = append(allErrs, ValidateLeaseCandidateSpec(&lease.Spec, field.NewPath("spec"))...)
return allErrs
}
func ValidLeaseCandidateName(name string, prefix bool) []string {
// prefix is already handled by IsConfigMapKey, a trailing - is permitted.
return utilvalidation.IsConfigMapKey(name)
}
func ValidateLeaseCandidateSpecUpdate(leaseCandidateSpec, oldLeaseCandidateSpec *coordination.LeaseCandidateSpec) field.ErrorList {
allErrs := field.ErrorList{}
allErrs = append(allErrs, apivalidation.ValidateImmutableField(leaseCandidateSpec.LeaseName, oldLeaseCandidateSpec.LeaseName, field.NewPath("spec").Child("leaseName"))...)
return allErrs
}
// ValidateLeaseCandidateUpdate validates an update of LeaseCandidate object.
func ValidateLeaseCandidateUpdate(leaseCandidate, oldLeaseCandidate *coordination.LeaseCandidate) field.ErrorList {
allErrs := validation.ValidateObjectMetaUpdate(&leaseCandidate.ObjectMeta, &oldLeaseCandidate.ObjectMeta, field.NewPath("metadata"))
allErrs = append(allErrs, ValidateLeaseCandidateSpec(&leaseCandidate.Spec, field.NewPath("spec"))...)
allErrs = append(allErrs, ValidateLeaseCandidateSpecUpdate(&leaseCandidate.Spec, &oldLeaseCandidate.Spec)...)
return allErrs
}
// ValidateLeaseCandidateSpec validates spec of LeaseCandidate.
func ValidateLeaseCandidateSpec(spec *coordination.LeaseCandidateSpec, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if len(spec.LeaseName) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("leaseName"), ""))
}
ev := semver.Version{}
if spec.EmulationVersion != "" {
var err error
ev, err = semver.Parse(spec.EmulationVersion)
if err != nil {
fld := fldPath.Child("emulationVersion")
allErrs = append(allErrs, field.Invalid(fld, spec.EmulationVersion, "must be a valid semantic version"))
}
}
bv := semver.Version{}
if spec.BinaryVersion != "" {
var err error
bv, err = semver.Parse(spec.BinaryVersion)
if err != nil {
fld := fldPath.Child("binaryVersion")
allErrs = append(allErrs, field.Invalid(fld, spec.BinaryVersion, "must be a valid semantic version"))
}
}
if spec.BinaryVersion != "" && spec.EmulationVersion != "" && bv.LT(ev) {
fld := fldPath.Child("binaryVersion")
allErrs = append(allErrs, field.Invalid(fld, spec.BinaryVersion, "must be greater than or equal to `emulationVersion`"))
}
strategySeen := make(map[coordination.CoordinatedLeaseStrategy]bool)
if len(spec.PreferredStrategies) > 0 {
for i, strategy := range spec.PreferredStrategies {
fld := fldPath.Child("preferredStrategies").Index(i)
if _, ok := strategySeen[strategy]; ok {
allErrs = append(allErrs, field.Duplicate(fld, strategy))
} else {
strategySeen[strategy] = true
}
if strategy == coordination.OldestEmulationVersion {
zeroVersion := semver.Version{}
if bv.EQ(zeroVersion) {
allErrs = append(allErrs, field.Required(fldPath.Child("binaryVersion"), "must be specified when `strategy` is 'OldestEmulationVersion'"))
}
if ev.EQ(zeroVersion) {
allErrs = append(allErrs, field.Required(fldPath.Child("emulationVersion"), "must be specified when `strategy` is 'OldestEmulationVersion'"))
}
}
allErrs = append(allErrs, ValidateCoordinatedLeaseStrategy(strategy, fld)...)
}
}
// spec.PingTime is a MicroTime and doesn't need further validation
// spec.RenewTime is a MicroTime and doesn't need further validation
return allErrs
}
// ValidateLeaseStrategy validates the Strategy field in both the Lease and LeaseCandidate
func ValidateCoordinatedLeaseStrategy(strategy coordination.CoordinatedLeaseStrategy, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
parts := strings.Split(string(strategy), "/")
switch len(parts) {
case 1:
// Must be a Kubernetes-defined name.
if !slices.Contains(validLeaseStrategies, coordination.CoordinatedLeaseStrategy(parts[0])) {
allErrs = append(allErrs, field.NotSupported(fldPath.Child("strategy"), strategy, validLeaseStrategies))
}
default:
if msgs := utilvalidation.IsQualifiedName(string(strategy)); len(msgs) > 0 {
for _, msg := range msgs {
allErrs = append(allErrs, field.Invalid(fldPath.Child("strategy"), strategy, msg))
}
}
}
return allErrs
}

View File

@@ -22,6 +22,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/kubernetes/pkg/apis/coordination"
"k8s.io/utils/ptr"
)
func TestValidateLease(t *testing.T) {
@@ -41,14 +42,58 @@ func TestValidateLeaseSpec(t *testing.T) {
holder := "holder"
leaseDuration := int32(0)
leaseTransitions := int32(-1)
spec := &coordination.LeaseSpec{
HolderIdentity: &holder,
LeaseDurationSeconds: &leaseDuration,
LeaseTransitions: &leaseTransitions,
preferredHolder := "holder2"
testcases := []struct {
spec coordination.LeaseSpec
err bool
}{
{
// valid
coordination.LeaseSpec{
HolderIdentity: &holder,
LeaseDurationSeconds: ptr.To[int32](10),
LeaseTransitions: ptr.To[int32](1),
},
false,
},
{
// valid with PreferredHolder
coordination.LeaseSpec{
HolderIdentity: &holder,
LeaseDurationSeconds: ptr.To[int32](10),
LeaseTransitions: ptr.To[int32](1),
Strategy: ptr.To(coordination.OldestEmulationVersion),
PreferredHolder: ptr.To("someotherholder"),
},
false,
},
{
coordination.LeaseSpec{
HolderIdentity: &holder,
LeaseDurationSeconds: &leaseDuration,
LeaseTransitions: &leaseTransitions,
},
true,
},
{
coordination.LeaseSpec{
HolderIdentity: &holder,
LeaseDurationSeconds: &leaseDuration,
LeaseTransitions: &leaseTransitions,
PreferredHolder: &preferredHolder,
},
true,
},
}
errs := ValidateLeaseSpec(spec, field.NewPath("foo"))
if len(errs) != 2 {
t.Errorf("unexpected list of errors: %#v", errs.ToAggregate().Error())
for _, tc := range testcases {
errs := ValidateLeaseSpec(&tc.spec, field.NewPath("foo"))
if tc.err && len(errs) == 0 {
t.Error("Expected err, got no err")
} else if !tc.err && len(errs) != 0 {
t.Errorf("Expected no err, got err %v", errs)
}
}
}
@@ -102,3 +147,188 @@ func TestValidateLeaseSpecUpdate(t *testing.T) {
t.Errorf("unexpected list of errors for valid update: %#v", errs.ToAggregate().Error())
}
}
func TestValidateLeaseCandidate(t *testing.T) {
lease := &coordination.LeaseCandidate{
ObjectMeta: metav1.ObjectMeta{
Name: "invalidName++",
Namespace: "==invalid_Namespace==",
},
}
errs := ValidateLeaseCandidate(lease)
if len(errs) == 0 {
t.Errorf("expected invalid LeaseCandidate")
}
}
func TestValidateLeaseCandidateSpec(t *testing.T) {
testcases := []struct {
name string
shouldErr bool
spec *coordination.LeaseCandidateSpec
}{
{
"valid",
false,
&coordination.LeaseCandidateSpec{
BinaryVersion: "1.30.0",
EmulationVersion: "1.30.0",
LeaseName: "test",
PreferredStrategies: []coordination.CoordinatedLeaseStrategy{coordination.OldestEmulationVersion},
},
},
{
"valid custom strategy should not require binaryVersion and emulationVersion",
false,
&coordination.LeaseCandidateSpec{
LeaseName: "test",
PreferredStrategies: []coordination.CoordinatedLeaseStrategy{"custom.com/foo"},
},
},
{
"no lease name",
true,
&coordination.LeaseCandidateSpec{
EmulationVersion: "1.30.0",
},
},
{
"bad binaryVersion",
true,
&coordination.LeaseCandidateSpec{
BinaryVersion: "1.30.1.6",
LeaseName: "test",
},
},
{
"emulation should be greater than or equal to binary version",
true,
&coordination.LeaseCandidateSpec{
EmulationVersion: "1.30.0",
BinaryVersion: "1.29.0",
LeaseName: "test",
},
},
{
"preferredStrategies bad",
true,
&coordination.LeaseCandidateSpec{
BinaryVersion: "1.30.1",
EmulationVersion: "1.30.1",
LeaseName: "test",
PreferredStrategies: []coordination.CoordinatedLeaseStrategy{"foo"},
},
},
{
"preferredStrategies good but emulationVersion missing",
true,
&coordination.LeaseCandidateSpec{
BinaryVersion: "1.30.1",
LeaseName: "test",
PreferredStrategies: []coordination.CoordinatedLeaseStrategy{coordination.OldestEmulationVersion},
},
},
}
for _, tc := range testcases {
errs := ValidateLeaseCandidateSpec(tc.spec, field.NewPath("foo"))
if len(errs) > 0 && !tc.shouldErr {
t.Errorf("unexpected list of errors: %#v", errs.ToAggregate().Error())
} else if len(errs) == 0 && tc.shouldErr {
t.Errorf("Expected err, got no error for tc: %s", tc.name)
}
}
}
func TestValidateLeaseCandidateUpdate(t *testing.T) {
testcases := []struct {
name string
old coordination.LeaseCandidate
update coordination.LeaseCandidate
err bool
}{
{
name: "valid update",
old: coordination.LeaseCandidate{
Spec: coordination.LeaseCandidateSpec{
BinaryVersion: "1.30.0",
EmulationVersion: "1.30.0",
LeaseName: "test",
},
},
update: coordination.LeaseCandidate{
Spec: coordination.LeaseCandidateSpec{
BinaryVersion: "1.30.0",
EmulationVersion: "1.30.0",
LeaseName: "test",
},
},
err: false,
},
{
name: "update LeaseName should fail",
old: coordination.LeaseCandidate{
Spec: coordination.LeaseCandidateSpec{
BinaryVersion: "1.30.0",
EmulationVersion: "1.30.0",
LeaseName: "test",
},
},
update: coordination.LeaseCandidate{
Spec: coordination.LeaseCandidateSpec{
BinaryVersion: "1.30.0",
EmulationVersion: "1.30.0",
LeaseName: "test-update",
},
},
err: true,
},
}
for _, tc := range testcases {
tc.old.ResourceVersion = "1"
tc.update.ResourceVersion = "1"
errs := ValidateLeaseCandidateUpdate(&tc.update, &tc.old)
if tc.err && len(errs) == 0 {
t.Errorf("Expected err, got no err for tc: %s", tc.name)
} else if !tc.err && len(errs) != 0 {
t.Errorf("Expected no err, got err %v for tc: %s", errs, tc.name)
}
}
}
func TestValidateCoordinatedLeaseStrategy(t *testing.T) {
testcases := []struct {
strategy coordination.CoordinatedLeaseStrategy
err bool
}{
{
coordination.CoordinatedLeaseStrategy("foobar"),
true,
},
{
coordination.CoordinatedLeaseStrategy("example.com/foobar/toomanyslashes"),
true,
},
{
coordination.CoordinatedLeaseStrategy(coordination.OldestEmulationVersion),
false,
},
{
coordination.CoordinatedLeaseStrategy("example.com/foobar"),
false,
},
}
for _, tc := range testcases {
errs := ValidateCoordinatedLeaseStrategy(tc.strategy, field.NewPath("foo"))
if tc.err && len(errs) == 0 {
t.Error("Expected err, got no err")
} else if !tc.err && len(errs) != 0 {
t.Errorf("Expected no err, got err %v", errs)
}
}
}

View File

@@ -52,6 +52,95 @@ func (in *Lease) DeepCopyObject() runtime.Object {
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LeaseCandidate) DeepCopyInto(out *LeaseCandidate) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LeaseCandidate.
func (in *LeaseCandidate) DeepCopy() *LeaseCandidate {
if in == nil {
return nil
}
out := new(LeaseCandidate)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *LeaseCandidate) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LeaseCandidateList) DeepCopyInto(out *LeaseCandidateList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]LeaseCandidate, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LeaseCandidateList.
func (in *LeaseCandidateList) DeepCopy() *LeaseCandidateList {
if in == nil {
return nil
}
out := new(LeaseCandidateList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *LeaseCandidateList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LeaseCandidateSpec) DeepCopyInto(out *LeaseCandidateSpec) {
*out = *in
if in.PingTime != nil {
in, out := &in.PingTime, &out.PingTime
*out = (*in).DeepCopy()
}
if in.RenewTime != nil {
in, out := &in.RenewTime, &out.RenewTime
*out = (*in).DeepCopy()
}
if in.PreferredStrategies != nil {
in, out := &in.PreferredStrategies, &out.PreferredStrategies
*out = make([]CoordinatedLeaseStrategy, len(*in))
copy(*out, *in)
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LeaseCandidateSpec.
func (in *LeaseCandidateSpec) DeepCopy() *LeaseCandidateSpec {
if in == nil {
return nil
}
out := new(LeaseCandidateSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LeaseList) DeepCopyInto(out *LeaseList) {
*out = *in
@@ -111,6 +200,16 @@ func (in *LeaseSpec) DeepCopyInto(out *LeaseSpec) {
*out = new(int32)
**out = **in
}
if in.Strategy != nil {
in, out := &in.Strategy, &out.Strategy
*out = new(CoordinatedLeaseStrategy)
**out = **in
}
if in.PreferredHolder != nil {
in, out := &in.PreferredHolder, &out.PreferredHolder
*out = new(string)
**out = **in
}
return
}