diff --git a/pkg/apis/flowcontrol/validation/validation.go b/pkg/apis/flowcontrol/validation/validation.go new file mode 100644 index 00000000000..fcea4893f0d --- /dev/null +++ b/pkg/apis/flowcontrol/validation/validation.go @@ -0,0 +1,342 @@ +/* +Copyright 2019 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 ( + "fmt" + "k8s.io/apimachinery/pkg/api/validation" + apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/util/shufflesharding" + apivalidation "k8s.io/kubernetes/pkg/apis/core/validation" + "k8s.io/kubernetes/pkg/apis/flowcontrol" +) + +// ValidateFlowSchemaName validates name for flow-schema. +var ValidateFlowSchemaName = apimachineryvalidation.NameIsDNSSubdomain + +// ValidatePriorityLevelConfigurationName validates name for priority-level-configuration. +var ValidatePriorityLevelConfigurationName = apimachineryvalidation.NameIsDNSSubdomain + +var supportedDistinguisherMethods = sets.NewString( + string(flowcontrol.FlowDistinguisherMethodByNamespaceType), + string(flowcontrol.FlowDistinguisherMethodByUserType), +) + +var priorityLevelConfigurationQueuingMaxQueues int32 = 10 * 1000 * 1000 // 10^7 + +var supportedVerbs = sets.NewString( + "get", + "list", + "create", + "update", + "delete", + "deletecollection", + "patch", + "watch", + "proxy", +) + +var supportedSubjectKinds = sets.NewString( + string(flowcontrol.SubjectKindServiceAccount), + string(flowcontrol.SubjectKindGroup), + string(flowcontrol.SubjectKindUser), +) + +var supportedQueuingType = sets.NewString( + string(flowcontrol.PriorityLevelQueuingTypeQueueing), + string(flowcontrol.PriorityLevelQueuingTypeExempt), +) + +// ValidateFlowSchema validates the content of flow-schema +func ValidateFlowSchema(fs *flowcontrol.FlowSchema) field.ErrorList { + allErrs := apivalidation.ValidateObjectMeta(&fs.ObjectMeta, false, ValidateFlowSchemaName, field.NewPath("metadata")) + allErrs = append(allErrs, ValidateFlowSchemaSpec(&fs.Spec, field.NewPath("spec"))...) + allErrs = append(allErrs, ValidateFlowSchemaStatus(&fs.Status, field.NewPath("status"))...) + return allErrs +} + +// ValidateFlowSchemaSpec validates the content of flow-schema's spec +func ValidateFlowSchemaSpec(spec *flowcontrol.FlowSchemaSpec, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + if spec.MatchingPrecedence <= 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("matchingPrecedence"), spec.MatchingPrecedence, "must be positive value")) + } + if spec.DistinguisherMethod != nil { + if !supportedDistinguisherMethods.Has(string(spec.DistinguisherMethod.Type)) { + allErrs = append(allErrs, field.NotSupported(fldPath.Child("distinguisherMethod").Child("type"), spec.DistinguisherMethod, supportedDistinguisherMethods.List())) + } + } + if len(spec.PriorityLevelConfiguration.Name) > 0 { + for _, msg := range ValidatePriorityLevelConfigurationName(spec.PriorityLevelConfiguration.Name, false) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("priorityLevelConfiguration").Child("name"), spec.PriorityLevelConfiguration.Name, msg)) + } + } else { + allErrs = append(allErrs, field.Required(fldPath.Child("priorityLevelConfiguration").Child("name"), "must reference a priority level")) + } + for i, rule := range spec.Rules { + allErrs = append(allErrs, ValidateFlowSchemaPolicyRulesWithSubjects(&rule, fldPath.Child("rules").Index(i))...) + } + return allErrs +} + +// ValidateFlowSchemaPolicyRulesWithSubjects validates policy-rule-with-subjects object. +func ValidateFlowSchemaPolicyRulesWithSubjects(rule *flowcontrol.PolicyRulesWithSubjects, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + if len(rule.Subjects) > 0 { + for i, subject := range rule.Subjects { + allErrs = append(allErrs, ValidateFlowSchemaSubject(&subject, fldPath.Child("subjects").Index(i))...) + } + } else { + allErrs = append(allErrs, field.Required(fldPath.Child("subjects"), "subjects must contain at least one value")) + } + + if len(rule.ResourceRules) == 0 && len(rule.NonResourceRules) == 0 { + allErrs = append(allErrs, field.Required(fldPath, "at least one of resourceRules and nonResourceRules has to be non-empty")) + } + for i, resourceRule := range rule.ResourceRules { + allErrs = append(allErrs, ValidateFlowSchemaResourcePolicyRule(&resourceRule, fldPath.Child("resourceRules").Index(i))...) + } + for i, nonResourceRule := range rule.NonResourceRules { + allErrs = append(allErrs, ValidateFlowSchemaNonResourcePolicyRule(&nonResourceRule, fldPath.Child("nonResourceRules").Index(i))...) + } + return allErrs +} + +// ValidateFlowSchemaSubject validates flow-schema's subject object. +func ValidateFlowSchemaSubject(subject *flowcontrol.Subject, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + switch subject.Kind { + case flowcontrol.SubjectKindServiceAccount: + allErrs = append(allErrs, ValidateServiceAccountSubject(subject.ServiceAccount, fldPath.Child("serviceAccount"))...) + case flowcontrol.SubjectKindUser: + allErrs = append(allErrs, ValidateUserSubject(subject.User, fldPath.Child("user"))...) + case flowcontrol.SubjectKindGroup: + allErrs = append(allErrs, ValidateGroupSubject(subject.Group, fldPath.Child("group"))...) + default: + allErrs = append(allErrs, field.NotSupported(fldPath.Child("kind"), subject.Kind, supportedSubjectKinds.List())) + } + return allErrs +} + +// ValidateServiceAccountSubject validates subject of "ServiceAccount" kind +func ValidateServiceAccountSubject(subject *flowcontrol.ServiceAccountSubject, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + if len(subject.Name) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("name"), "")) + } else if subject.Name != flowcontrol.NameAll { + for _, msg := range validation.ValidateServiceAccountName(subject.Name, false) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("name"), subject.Name, msg)) + } + } + + if len(subject.Namespace) > 0 { + for _, msg := range apimachineryvalidation.ValidateNamespaceName(subject.Namespace, false) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("namespace"), subject.Namespace, msg)) + } + } else { + allErrs = append(allErrs, field.Required(fldPath.Child("namespace"), "must specify namespace for service account")) + } + + return allErrs +} + +// ValidateUserSubject validates subject of "User" kind +func ValidateUserSubject(subject *flowcontrol.UserSubject, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + if len(subject.Name) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("name"), "")) + } + return allErrs +} + +// ValidateGroupSubject validates subject of "Group" kind +func ValidateGroupSubject(subject *flowcontrol.GroupSubject, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + if len(subject.Name) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("name"), "")) + } + return allErrs +} + +// ValidateFlowSchemaNonResourcePolicyRule validates non-resource policy-rule in the flow-schema. +func ValidateFlowSchemaNonResourcePolicyRule(rule *flowcontrol.NonResourcePolicyRule, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + + if len(rule.Verbs) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("verbs"), "verbs must contain at least one value")) + } else if hasWildcard(rule.Verbs) { + if len(rule.Verbs) > 1 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("verbs"), rule.Verbs, "if '*' is present, must not specify other verbs")) + } + } else if !supportedVerbs.IsSuperset(sets.NewString(rule.Verbs...)) { + // only supported verbs are allowed + allErrs = append(allErrs, field.NotSupported(fldPath.Child("verbs"), rule.Verbs, supportedVerbs.List())) + } + + if len(rule.NonResourceURLs) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("nonResourceURLs"), "nonResourceURLs must contain at least one value")) + } else if len(rule.NonResourceURLs) > 1 && hasWildcard(rule.NonResourceURLs) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("nonResourceURLs"), rule.NonResourceURLs, "if '*' is present, must not specify other non-resource URLs")) + } + + return allErrs +} + +// ValidateFlowSchemaResourcePolicyRule validates resource policy-rule in the flow-schema. +func ValidateFlowSchemaResourcePolicyRule(rule *flowcontrol.ResourcePolicyRule, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + + if len(rule.Verbs) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("verbs"), "verbs must contain at least one value")) + } else if hasWildcard(rule.Verbs) { + if len(rule.Verbs) > 1 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("verbs"), rule.Verbs, "if '*' is present, must not specify other verbs")) + } + } else if !supportedVerbs.IsSuperset(sets.NewString(rule.Verbs...)) { + // only supported verbs are allowed + allErrs = append(allErrs, field.NotSupported(fldPath.Child("verbs"), rule.Verbs, supportedVerbs.List())) + } + + if len(rule.APIGroups) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("apiGroups"), "resource rules must supply at least one api group")) + } else if len(rule.APIGroups) > 1 && hasWildcard(rule.APIGroups) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("apiGroups"), rule.APIGroups, "if '*' is present, must not specify other api groups")) + } + + if len(rule.Resources) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("resources"), "resource rules must supply at least one resource")) + } else if len(rule.Resources) > 1 && hasWildcard(rule.Resources) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("resources"), rule.Resources, "if '*' is present, must not specify other resources")) + } + + return allErrs +} + +// ValidateFlowSchemaStatus validates status for the flow-schema. +func ValidateFlowSchemaStatus(status *flowcontrol.FlowSchemaStatus, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + keys := sets.NewString() + for i, condition := range status.Conditions { + if keys.Has(string(condition.Type)) { + allErrs = append(allErrs, field.Duplicate(fldPath.Child("conditions").Index(i).Child("type"), condition.Type)) + } + keys.Insert(string(condition.Type)) + allErrs = append(allErrs, ValidateFlowSchemaCondition(&condition, fldPath.Child("conditions").Index(i))...) + } + return allErrs +} + +// ValidateFlowSchemaCondition validates condition in the flow-schema's status. +func ValidateFlowSchemaCondition(condition *flowcontrol.FlowSchemaCondition, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + if len(condition.Type) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must not be empty")) + } + return allErrs +} + +// ValidatePriorityLevelConfiguration validates priority-level-configuration. +func ValidatePriorityLevelConfiguration(pl *flowcontrol.PriorityLevelConfiguration) field.ErrorList { + allErrs := apivalidation.ValidateObjectMeta(&pl.ObjectMeta, false, ValidatePriorityLevelConfigurationName, field.NewPath("metadata")) + allErrs = append(allErrs, ValidatePriorityLevelConfigurationSpec(&pl.Spec, pl.Name, field.NewPath("spec"))...) + allErrs = append(allErrs, ValidatePriorityLevelConfigurationStatus(&pl.Status, field.NewPath("status"))...) + return allErrs +} + +// ValidatePriorityLevelConfigurationSpec validates priority-level-configuration's spec. +func ValidatePriorityLevelConfigurationSpec(spec *flowcontrol.PriorityLevelConfigurationSpec, name string, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + switch spec.Type { + case flowcontrol.PriorityLevelQueuingTypeExempt: + if spec.Queuing != nil { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("queuing"), "must be nil if the type is not Queuing")) + } + case flowcontrol.PriorityLevelQueuingTypeQueueing: + if spec.Queuing == nil { + allErrs = append(allErrs, field.Required(fldPath.Child("queuing"), "must not be empty")) + } else { + allErrs = append(allErrs, ValidatePriorityLevelQueuingConfiguration(spec.Queuing, fldPath.Child("queuing"))...) + } + default: + allErrs = append(allErrs, field.NotSupported(fldPath.Child("type"), spec.Type, supportedQueuingType.List())) + } + return allErrs +} + +// ValidatePriorityLevelQueuingConfiguration validates queuing-configuration for a priority-level +func ValidatePriorityLevelQueuingConfiguration(queuing *flowcontrol.QueuingConfiguration, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + if queuing.AssuredConcurrencyShares <= 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("assuredConcurrencyShares"), queuing.AssuredConcurrencyShares, "must be positive")) + } + if queuing.QueueLengthLimit <= 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("queueLengthLimit"), queuing.QueueLengthLimit, "must be positive")) + } + + // validate input arguments for shuffle-sharding + if queuing.Queues <= 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("queues"), queuing.Queues, "must be positive")) + } else if queuing.Queues > priorityLevelConfigurationQueuingMaxQueues { + allErrs = append(allErrs, field.Invalid(fldPath.Child("queues"), queuing.Queues, + fmt.Sprintf("must not be greater than %d", priorityLevelConfigurationQueuingMaxQueues))) + } + + if queuing.HandSize <= 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("handSize"), queuing.HandSize, "must be positive")) + } else if queuing.HandSize > queuing.Queues { + allErrs = append(allErrs, field.Invalid(fldPath.Child("handSize"), queuing.HandSize, + fmt.Sprintf("should not be greater than queues (%d)", queuing.Queues))) + } else if entropy := shufflesharding.RequiredEntropyBits(int(queuing.Queues), int(queuing.HandSize)); entropy > shufflesharding.MaxHashBits { + allErrs = append(allErrs, field.Invalid(fldPath.Child("handSize"), queuing.HandSize, + fmt.Sprintf("required entropy bits of deckSize %d and handSize %d should not be greater than %d", queuing.Queues, queuing.HandSize, shufflesharding.MaxHashBits))) + } + return allErrs +} + +// ValidatePriorityLevelConfigurationStatus validates priority-level-configuration's status. +func ValidatePriorityLevelConfigurationStatus(status *flowcontrol.PriorityLevelConfigurationStatus, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + keys := sets.NewString() + for i, condition := range status.Conditions { + if keys.Has(string(condition.Type)) { + allErrs = append(allErrs, field.Duplicate(fldPath.Child("conditions").Index(i).Child("type"), condition.Type)) + } + keys.Insert(string(condition.Type)) + allErrs = append(allErrs, ValidatePriorityLevelConfigurationCondition(&condition, fldPath.Child("conditions").Index(i))...) + } + return allErrs +} + +// ValidatePriorityLevelConfigurationCondition validates condition in priority-level-configuration's status. +func ValidatePriorityLevelConfigurationCondition(condition *flowcontrol.PriorityLevelConfigurationCondition, fldPath *field.Path) field.ErrorList { + var allErrs field.ErrorList + if len(condition.Type) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must not be empty")) + } + return allErrs +} + +func hasWildcard(operations []string) bool { + for _, o := range operations { + if o == "*" { + return true + } + } + return false +} diff --git a/pkg/apis/flowcontrol/validation/validation_test.go b/pkg/apis/flowcontrol/validation/validation_test.go new file mode 100644 index 00000000000..42144ec2025 --- /dev/null +++ b/pkg/apis/flowcontrol/validation/validation_test.go @@ -0,0 +1,628 @@ +/* +Copyright 2019 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 ( + "math" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/kubernetes/pkg/apis/flowcontrol" +) + +func TestFlowSchemaValidation(t *testing.T) { + testCases := []struct { + name string + flowSchema *flowcontrol.FlowSchema + expectedErrors field.ErrorList + }{ + { + name: "missing neither resource and non-resource policy-rule should fail", + flowSchema: &flowcontrol.FlowSchema{ + ObjectMeta: metav1.ObjectMeta{ + Name: "system-foo", + }, + Spec: flowcontrol.FlowSchemaSpec{ + MatchingPrecedence: 50, + PriorityLevelConfiguration: flowcontrol.PriorityLevelConfigurationReference{ + Name: "system-bar", + }, + Rules: []flowcontrol.PolicyRulesWithSubjects{ + { + Subjects: []flowcontrol.Subject{ + { + Kind: flowcontrol.SubjectKindUser, + User: &flowcontrol.UserSubject{Name: "noxu"}, + }, + }, + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Required(field.NewPath("spec").Child("rules").Index(0), "at least one of resourceRules and nonResourceRules has to be non-empty"), + }, + }, + { + name: "normal flow-schema w/ * verbs/apiGroups/resources should work", + flowSchema: &flowcontrol.FlowSchema{ + ObjectMeta: metav1.ObjectMeta{ + Name: "system-foo", + }, + Spec: flowcontrol.FlowSchemaSpec{ + MatchingPrecedence: 50, + PriorityLevelConfiguration: flowcontrol.PriorityLevelConfigurationReference{ + Name: "system-bar", + }, + Rules: []flowcontrol.PolicyRulesWithSubjects{ + { + Subjects: []flowcontrol.Subject{ + { + Kind: flowcontrol.SubjectKindGroup, + Group: &flowcontrol.GroupSubject{Name: "noxu"}, + }, + }, + ResourceRules: []flowcontrol.ResourcePolicyRule{ + { + Verbs: []string{flowcontrol.VerbAll}, + APIGroups: []string{flowcontrol.APIGroupAll}, + Resources: []string{flowcontrol.ResourceAll}, + }, + }, + }, + }, + }, + }, + expectedErrors: field.ErrorList{}, + }, + { + name: "flow-schema mixes * verbs/apiGroups/resources should fail", + flowSchema: &flowcontrol.FlowSchema{ + ObjectMeta: metav1.ObjectMeta{ + Name: "system-foo", + }, + Spec: flowcontrol.FlowSchemaSpec{ + MatchingPrecedence: 50, + PriorityLevelConfiguration: flowcontrol.PriorityLevelConfigurationReference{ + Name: "system-bar", + }, + Rules: []flowcontrol.PolicyRulesWithSubjects{ + { + Subjects: []flowcontrol.Subject{ + { + Kind: flowcontrol.SubjectKindUser, + User: &flowcontrol.UserSubject{Name: "noxu"}, + }, + }, + ResourceRules: []flowcontrol.ResourcePolicyRule{ + { + Verbs: []string{flowcontrol.VerbAll, "create"}, + APIGroups: []string{flowcontrol.APIGroupAll, "tak"}, + Resources: []string{flowcontrol.ResourceAll, "tok"}, + }, + }, + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Invalid(field.NewPath("spec").Child("rules").Index(0).Child("resourceRules").Index(0).Child("verbs"), []string{"*", "create"}, "if '*' is present, must not specify other verbs"), + field.Invalid(field.NewPath("spec").Child("rules").Index(0).Child("resourceRules").Index(0).Child("apiGroups"), []string{"*", "tak"}, "if '*' is present, must not specify other api groups"), + field.Invalid(field.NewPath("spec").Child("rules").Index(0).Child("resourceRules").Index(0).Child("resources"), []string{"*", "tok"}, "if '*' is present, must not specify other resources"), + }, + }, + { + name: "flow-schema has both resource rules and non-resource rules should work", + flowSchema: &flowcontrol.FlowSchema{ + ObjectMeta: metav1.ObjectMeta{ + Name: "system-foo", + }, + Spec: flowcontrol.FlowSchemaSpec{ + MatchingPrecedence: 50, + PriorityLevelConfiguration: flowcontrol.PriorityLevelConfigurationReference{ + Name: "system-bar", + }, + Rules: []flowcontrol.PolicyRulesWithSubjects{ + { + Subjects: []flowcontrol.Subject{ + { + Kind: flowcontrol.SubjectKindUser, + User: &flowcontrol.UserSubject{Name: "noxu"}, + }, + }, + ResourceRules: []flowcontrol.ResourcePolicyRule{ + { + Verbs: []string{flowcontrol.VerbAll}, + APIGroups: []string{flowcontrol.APIGroupAll}, + Resources: []string{flowcontrol.ResourceAll}, + }, + }, + NonResourceRules: []flowcontrol.NonResourcePolicyRule{ + { + Verbs: []string{flowcontrol.VerbAll}, + NonResourceURLs: []string{"/apis/*"}, + }, + }, + }, + }, + }, + }, + expectedErrors: field.ErrorList{}, + }, + { + name: "flow-schema mixes * non-resource URLs should fail", + flowSchema: &flowcontrol.FlowSchema{ + ObjectMeta: metav1.ObjectMeta{ + Name: "system-foo", + }, + Spec: flowcontrol.FlowSchemaSpec{ + MatchingPrecedence: 50, + PriorityLevelConfiguration: flowcontrol.PriorityLevelConfigurationReference{ + Name: "system-bar", + }, + Rules: []flowcontrol.PolicyRulesWithSubjects{ + { + Subjects: []flowcontrol.Subject{ + { + Kind: flowcontrol.SubjectKindUser, + User: &flowcontrol.UserSubject{Name: "noxu"}, + }, + }, + NonResourceRules: []flowcontrol.NonResourcePolicyRule{ + { + Verbs: []string{"*"}, + NonResourceURLs: []string{flowcontrol.NonResourceAll, "tik"}, + }, + }, + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Invalid(field.NewPath("spec").Child("rules").Index(0).Child("nonResourceRules").Index(0).Child("nonResourceURLs"), []string{"*", "tik"}, "if '*' is present, must not specify other non-resource URLs"), + }, + }, + { + name: "invalid subject kind should fail", + flowSchema: &flowcontrol.FlowSchema{ + ObjectMeta: metav1.ObjectMeta{ + Name: "system-foo", + }, + Spec: flowcontrol.FlowSchemaSpec{ + MatchingPrecedence: 50, + PriorityLevelConfiguration: flowcontrol.PriorityLevelConfigurationReference{ + Name: "system-bar", + }, + Rules: []flowcontrol.PolicyRulesWithSubjects{ + { + Subjects: []flowcontrol.Subject{ + { + Kind: "FooKind", + }, + }, + NonResourceRules: []flowcontrol.NonResourcePolicyRule{ + { + Verbs: []string{"*"}, + NonResourceURLs: []string{flowcontrol.NonResourceAll}, + }, + }, + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.NotSupported(field.NewPath("spec").Child("rules").Index(0).Child("subjects").Index(0).Child("kind"), flowcontrol.SubjectKind("FooKind"), supportedSubjectKinds.List()), + }, + }, + { + name: "flow-schema w/ invalid verb should fail", + flowSchema: &flowcontrol.FlowSchema{ + ObjectMeta: metav1.ObjectMeta{ + Name: "system-foo", + }, + Spec: flowcontrol.FlowSchemaSpec{ + MatchingPrecedence: 50, + PriorityLevelConfiguration: flowcontrol.PriorityLevelConfigurationReference{ + Name: "system-bar", + }, + Rules: []flowcontrol.PolicyRulesWithSubjects{ + { + Subjects: []flowcontrol.Subject{ + { + Kind: flowcontrol.SubjectKindUser, + User: &flowcontrol.UserSubject{Name: "noxu"}, + }, + }, + ResourceRules: []flowcontrol.ResourcePolicyRule{ + { + Verbs: []string{"feed"}, + APIGroups: []string{flowcontrol.APIGroupAll}, + Resources: []string{flowcontrol.ResourceAll}, + }, + }, + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.NotSupported(field.NewPath("spec").Child("rules").Index(0).Child("resourceRules").Index(0).Child("verbs"), []string{"feed"}, supportedVerbs.List()), + }, + }, + { + name: "flow-schema w/ invalid priority level configuration name should fail", + flowSchema: &flowcontrol.FlowSchema{ + ObjectMeta: metav1.ObjectMeta{ + Name: "system-foo", + }, + Spec: flowcontrol.FlowSchemaSpec{ + MatchingPrecedence: 50, + PriorityLevelConfiguration: flowcontrol.PriorityLevelConfigurationReference{ + Name: "system+++$$", + }, + Rules: []flowcontrol.PolicyRulesWithSubjects{ + { + Subjects: []flowcontrol.Subject{ + { + Kind: flowcontrol.SubjectKindUser, + User: &flowcontrol.UserSubject{Name: "noxu"}, + }, + }, + ResourceRules: []flowcontrol.ResourcePolicyRule{ + { + Verbs: []string{flowcontrol.VerbAll}, + APIGroups: []string{flowcontrol.APIGroupAll}, + Resources: []string{flowcontrol.ResourceAll}, + }, + }, + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Invalid(field.NewPath("spec").Child("priorityLevelConfiguration").Child("name"), "system+++$$", `a DNS-1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`), + }, + }, + { + name: "flow-schema w/ service-account kind missing namespace should fail", + flowSchema: &flowcontrol.FlowSchema{ + ObjectMeta: metav1.ObjectMeta{ + Name: "system-foo", + }, + Spec: flowcontrol.FlowSchemaSpec{ + MatchingPrecedence: 50, + PriorityLevelConfiguration: flowcontrol.PriorityLevelConfigurationReference{ + Name: "system-bar", + }, + Rules: []flowcontrol.PolicyRulesWithSubjects{ + { + Subjects: []flowcontrol.Subject{ + { + Kind: flowcontrol.SubjectKindServiceAccount, + ServiceAccount: &flowcontrol.ServiceAccountSubject{ + Name: "noxu", + }, + }, + }, + ResourceRules: []flowcontrol.ResourcePolicyRule{ + { + Verbs: []string{flowcontrol.VerbAll}, + APIGroups: []string{flowcontrol.APIGroupAll}, + Resources: []string{flowcontrol.ResourceAll}, + }, + }, + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Required(field.NewPath("spec").Child("rules").Index(0).Child("subjects").Index(0).Child("serviceAccount").Child("namespace"), "must specify namespace for service account"), + }, + }, + { + name: "flow-schema missing kind should fail", + flowSchema: &flowcontrol.FlowSchema{ + ObjectMeta: metav1.ObjectMeta{ + Name: "system-foo", + }, + Spec: flowcontrol.FlowSchemaSpec{ + MatchingPrecedence: 50, + PriorityLevelConfiguration: flowcontrol.PriorityLevelConfigurationReference{ + Name: "system-bar", + }, + Rules: []flowcontrol.PolicyRulesWithSubjects{ + { + Subjects: []flowcontrol.Subject{ + { + Kind: "", + }, + }, + ResourceRules: []flowcontrol.ResourcePolicyRule{ + { + Verbs: []string{flowcontrol.VerbAll}, + APIGroups: []string{flowcontrol.APIGroupAll}, + Resources: []string{flowcontrol.ResourceAll}, + }, + }, + }, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.NotSupported(field.NewPath("spec").Child("rules").Index(0).Child("subjects").Index(0).Child("kind"), flowcontrol.SubjectKind(""), supportedSubjectKinds.List()), + }, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + errs := ValidateFlowSchema(testCase.flowSchema) + if !assert.ElementsMatch(t, testCase.expectedErrors, errs) { + t.Logf("mismatch: %v", cmp.Diff(testCase.expectedErrors, errs)) + } + }) + } +} + +func TestPriorityLevelConfigurationValidation(t *testing.T) { + testCases := []struct { + name string + priorityLevelConfiguration *flowcontrol.PriorityLevelConfiguration + expectedErrors field.ErrorList + }{ + { + name: "normal customized priority level should work", + priorityLevelConfiguration: &flowcontrol.PriorityLevelConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "system-foo", + }, + Spec: flowcontrol.PriorityLevelConfigurationSpec{ + Type: flowcontrol.PriorityLevelQueuingTypeQueueing, + Queuing: &flowcontrol.QueuingConfiguration{ + AssuredConcurrencyShares: 100, + Queues: 512, + HandSize: 4, + QueueLengthLimit: 100, + }, + }, + }, + expectedErrors: field.ErrorList{}, + }, + { + name: "system low priority level w/ exempt should work", + priorityLevelConfiguration: &flowcontrol.PriorityLevelConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: flowcontrol.PriorityLevelConfigurationNameExempt, + }, + Spec: flowcontrol.PriorityLevelConfigurationSpec{ + Type: flowcontrol.PriorityLevelQueuingTypeExempt, + }, + }, + expectedErrors: field.ErrorList{}, + }, + { + name: "customized priority level w/ overflowing handSize/queues should fail 1", + priorityLevelConfiguration: &flowcontrol.PriorityLevelConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "system-foo", + }, + Spec: flowcontrol.PriorityLevelConfigurationSpec{ + Type: flowcontrol.PriorityLevelQueuingTypeQueueing, + Queuing: &flowcontrol.QueuingConfiguration{ + AssuredConcurrencyShares: 100, + QueueLengthLimit: 100, + Queues: 512, + HandSize: 8, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Invalid(field.NewPath("spec").Child("queuing").Child("handSize"), int32(8), "required entropy bits of deckSize 512 and handSize 8 should not be greater than 60"), + }, + }, + { + name: "customized priority level w/ overflowing handSize/queues should fail 2", + priorityLevelConfiguration: &flowcontrol.PriorityLevelConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "system-foo", + }, + Spec: flowcontrol.PriorityLevelConfigurationSpec{ + Type: flowcontrol.PriorityLevelQueuingTypeQueueing, + Queuing: &flowcontrol.QueuingConfiguration{ + AssuredConcurrencyShares: 100, + QueueLengthLimit: 100, + Queues: 128, + HandSize: 10, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Invalid(field.NewPath("spec").Child("queuing").Child("handSize"), int32(10), "required entropy bits of deckSize 128 and handSize 10 should not be greater than 60"), + }, + }, + { + name: "customized priority level w/ overflowing handSize/queues should fail 3", + priorityLevelConfiguration: &flowcontrol.PriorityLevelConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "system-foo", + }, + Spec: flowcontrol.PriorityLevelConfigurationSpec{ + Type: flowcontrol.PriorityLevelQueuingTypeQueueing, + Queuing: &flowcontrol.QueuingConfiguration{ + AssuredConcurrencyShares: 100, + QueueLengthLimit: 100, + Queues: math.MaxInt32, + HandSize: 3, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Invalid(field.NewPath("spec").Child("queuing").Child("handSize"), int32(3), "required entropy bits of deckSize 2147483647 and handSize 3 should not be greater than 60"), + field.Invalid(field.NewPath("spec").Child("queuing").Child("queues"), int32(math.MaxInt32), "must not be greater than 10000000"), + }, + }, + { + name: "customized priority level w/ handSize=2 and queues=10^7 should work", + priorityLevelConfiguration: &flowcontrol.PriorityLevelConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "system-foo", + }, + Spec: flowcontrol.PriorityLevelConfigurationSpec{ + Type: flowcontrol.PriorityLevelQueuingTypeQueueing, + Queuing: &flowcontrol.QueuingConfiguration{ + AssuredConcurrencyShares: 100, + QueueLengthLimit: 100, + Queues: 10 * 1000 * 1000, // 10^7 + HandSize: 2, + }, + }, + }, + expectedErrors: field.ErrorList{}, + }, + { + name: "customized priority level w/ handSize greater than queues should fail", + priorityLevelConfiguration: &flowcontrol.PriorityLevelConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: "system-foo", + }, + Spec: flowcontrol.PriorityLevelConfigurationSpec{ + Type: flowcontrol.PriorityLevelQueuingTypeQueueing, + Queuing: &flowcontrol.QueuingConfiguration{ + AssuredConcurrencyShares: 100, + QueueLengthLimit: 100, + Queues: 7, + HandSize: 8, + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Invalid(field.NewPath("spec").Child("queuing").Child("handSize"), int32(8), "should not be greater than queues (7)"), + }, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + errs := ValidatePriorityLevelConfiguration(testCase.priorityLevelConfiguration) + if !assert.ElementsMatch(t, testCase.expectedErrors, errs) { + t.Logf("mismatch: %v", cmp.Diff(testCase.expectedErrors, errs)) + } + }) + } +} + +func TestValidateFlowSchemaStatus(t *testing.T) { + testCases := []struct { + name string + status *flowcontrol.FlowSchemaStatus + expectedErrors field.ErrorList + }{ + { + name: "empty status should work", + status: &flowcontrol.FlowSchemaStatus{}, + expectedErrors: field.ErrorList{}, + }, + { + name: "duplicate key should fail", + status: &flowcontrol.FlowSchemaStatus{ + Conditions: []flowcontrol.FlowSchemaCondition{ + { + Type: "1", + }, + { + Type: "1", + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Duplicate(field.NewPath("status").Child("conditions").Index(1).Child("type"), flowcontrol.FlowSchemaConditionType("1")), + }, + }, + { + name: "missing key should fail", + status: &flowcontrol.FlowSchemaStatus{ + Conditions: []flowcontrol.FlowSchemaCondition{ + { + Type: "", + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Required(field.NewPath("status").Child("conditions").Index(0).Child("type"), "must not be empty"), + }, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + errs := ValidateFlowSchemaStatus(testCase.status, field.NewPath("status")) + if !assert.ElementsMatch(t, testCase.expectedErrors, errs) { + t.Logf("mismatch: %v", cmp.Diff(testCase.expectedErrors, errs)) + } + }) + } +} + +func TestValidatePriorityLevelConfigurationStatus(t *testing.T) { + testCases := []struct { + name string + status *flowcontrol.PriorityLevelConfigurationStatus + expectedErrors field.ErrorList + }{ + { + name: "empty status should work", + status: &flowcontrol.PriorityLevelConfigurationStatus{}, + expectedErrors: field.ErrorList{}, + }, + { + name: "duplicate key should fail", + status: &flowcontrol.PriorityLevelConfigurationStatus{ + Conditions: []flowcontrol.PriorityLevelConfigurationCondition{ + { + Type: "1", + }, + { + Type: "1", + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Duplicate(field.NewPath("status").Child("conditions").Index(1).Child("type"), flowcontrol.PriorityLevelConfigurationConditionType("1")), + }, + }, + { + name: "missing key should fail", + status: &flowcontrol.PriorityLevelConfigurationStatus{ + Conditions: []flowcontrol.PriorityLevelConfigurationCondition{ + { + Type: "", + }, + }, + }, + expectedErrors: field.ErrorList{ + field.Required(field.NewPath("status").Child("conditions").Index(0).Child("type"), "must not be empty"), + }, + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + errs := ValidatePriorityLevelConfigurationStatus(testCase.status, field.NewPath("status")) + if !assert.ElementsMatch(t, testCase.expectedErrors, errs) { + t.Logf("mismatch: %v", cmp.Diff(testCase.expectedErrors, errs)) + } + }) + } +}