Coordinated Leader Election Alpha API
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user