/* 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" "math" "testing" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" flowcontrolv1 "k8s.io/api/flowcontrol/v1" flowcontrolv1beta1 "k8s.io/api/flowcontrol/v1beta1" flowcontrolv1beta2 "k8s.io/api/flowcontrol/v1beta2" flowcontrolv1beta3 "k8s.io/api/flowcontrol/v1beta3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apiserver/pkg/authentication/user" "k8s.io/kubernetes/pkg/apis/flowcontrol" "k8s.io/kubernetes/pkg/apis/flowcontrol/internalbootstrap" "k8s.io/utils/pointer" ) func TestFlowSchemaValidation(t *testing.T) { badExempt := flowcontrol.FlowSchemaSpec{ MatchingPrecedence: 1, PriorityLevelConfiguration: flowcontrol.PriorityLevelConfigurationReference{ Name: flowcontrol.PriorityLevelConfigurationNameExempt, }, Rules: []flowcontrol.PolicyRulesWithSubjects{{ Subjects: []flowcontrol.Subject{{ Kind: flowcontrol.SubjectKindGroup, Group: &flowcontrol.GroupSubject{Name: "system:masters"}, }}, ResourceRules: []flowcontrol.ResourcePolicyRule{{ Verbs: []string{flowcontrol.VerbAll}, APIGroups: []string{flowcontrol.APIGroupAll}, Resources: []string{flowcontrol.ResourceAll}, ClusterScope: true, Namespaces: []string{flowcontrol.NamespaceEvery}, }}, NonResourceRules: []flowcontrol.NonResourcePolicyRule{{ Verbs: []string{flowcontrol.VerbAll}, NonResourceURLs: []string{"/"}, }}, }}, } badCatchAll := flowcontrol.FlowSchemaSpec{ MatchingPrecedence: flowcontrol.FlowSchemaMaxMatchingPrecedence, PriorityLevelConfiguration: flowcontrol.PriorityLevelConfigurationReference{ Name: flowcontrol.PriorityLevelConfigurationNameCatchAll, }, DistinguisherMethod: &flowcontrol.FlowDistinguisherMethod{Type: flowcontrol.FlowDistinguisherMethodByUserType}, Rules: []flowcontrol.PolicyRulesWithSubjects{{ Subjects: []flowcontrol.Subject{{ Kind: flowcontrol.SubjectKindGroup, Group: &flowcontrol.GroupSubject{Name: user.AllUnauthenticated}, }, { Kind: flowcontrol.SubjectKindGroup, Group: &flowcontrol.GroupSubject{Name: user.AllAuthenticated}, }}, ResourceRules: []flowcontrol.ResourcePolicyRule{{ Verbs: []string{flowcontrol.VerbAll}, APIGroups: []string{flowcontrol.APIGroupAll}, Resources: []string{flowcontrol.ResourceAll}, ClusterScope: true, Namespaces: []string{flowcontrol.NamespaceEvery}, }}, NonResourceRules: []flowcontrol.NonResourcePolicyRule{{ Verbs: []string{flowcontrol.VerbAll}, NonResourceURLs: []string{"/"}, }}, }}, } testCases := []struct { name string flowSchema *flowcontrol.FlowSchema expectedErrors field.ErrorList }{{ name: "missing both 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}, Namespaces: []string{flowcontrol.NamespaceEvery}, }}, }}, }, }, expectedErrors: field.ErrorList{}, }, { name: "malformed Subject union in ServiceAccount case", 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, User: &flowcontrol.UserSubject{Name: "fred"}, Group: &flowcontrol.GroupSubject{Name: "fred"}, }}, NonResourceRules: []flowcontrol.NonResourcePolicyRule{{ Verbs: []string{flowcontrol.VerbAll}, NonResourceURLs: []string{"*"}, }}, }}, }, }, expectedErrors: field.ErrorList{ field.Required(field.NewPath("spec").Child("rules").Index(0).Child("subjects").Index(0).Child("serviceAccount"), "serviceAccount is required when subject kind is 'ServiceAccount'"), field.Forbidden(field.NewPath("spec").Child("rules").Index(0).Child("subjects").Index(0).Child("user"), "user is forbidden when subject kind is not 'User'"), field.Forbidden(field.NewPath("spec").Child("rules").Index(0).Child("subjects").Index(0).Child("group"), "group is forbidden when subject kind is not 'Group'"), }, }, { name: "Subject union malformed in User case", 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, Group: &flowcontrol.GroupSubject{Name: "fred"}, ServiceAccount: &flowcontrol.ServiceAccountSubject{Namespace: "s", Name: "n"}, }}, NonResourceRules: []flowcontrol.NonResourcePolicyRule{{ Verbs: []string{flowcontrol.VerbAll}, NonResourceURLs: []string{"*"}, }}, }}, }, }, expectedErrors: field.ErrorList{ field.Forbidden(field.NewPath("spec").Child("rules").Index(0).Child("subjects").Index(0).Child("serviceAccount"), "serviceAccount is forbidden when subject kind is not 'ServiceAccount'"), field.Required(field.NewPath("spec").Child("rules").Index(0).Child("subjects").Index(0).Child("user"), "user is required when subject kind is 'User'"), field.Forbidden(field.NewPath("spec").Child("rules").Index(0).Child("subjects").Index(0).Child("group"), "group is forbidden when subject kind is not 'Group'"), }, }, { name: "malformed Subject union in Group case", 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, User: &flowcontrol.UserSubject{Name: "fred"}, ServiceAccount: &flowcontrol.ServiceAccountSubject{Namespace: "s", Name: "n"}, }}, NonResourceRules: []flowcontrol.NonResourcePolicyRule{{ Verbs: []string{flowcontrol.VerbAll}, NonResourceURLs: []string{"*"}, }}, }}, }, }, expectedErrors: field.ErrorList{ field.Forbidden(field.NewPath("spec").Child("rules").Index(0).Child("subjects").Index(0).Child("serviceAccount"), "serviceAccount is forbidden when subject kind is not 'ServiceAccount'"), field.Forbidden(field.NewPath("spec").Child("rules").Index(0).Child("subjects").Index(0).Child("user"), "user is forbidden when subject kind is not 'User'"), field.Required(field.NewPath("spec").Child("rules").Index(0).Child("subjects").Index(0).Child("group"), "group is required when subject kind is 'Group'"), }, }, { name: "exempt flow-schema should work", flowSchema: &flowcontrol.FlowSchema{ ObjectMeta: metav1.ObjectMeta{ Name: flowcontrol.FlowSchemaNameExempt, }, Spec: flowcontrol.FlowSchemaSpec{ MatchingPrecedence: 1, PriorityLevelConfiguration: flowcontrol.PriorityLevelConfigurationReference{ Name: flowcontrol.PriorityLevelConfigurationNameExempt, }, Rules: []flowcontrol.PolicyRulesWithSubjects{{ Subjects: []flowcontrol.Subject{{ Kind: flowcontrol.SubjectKindGroup, Group: &flowcontrol.GroupSubject{Name: "system:masters"}, }}, ResourceRules: []flowcontrol.ResourcePolicyRule{{ Verbs: []string{flowcontrol.VerbAll}, APIGroups: []string{flowcontrol.APIGroupAll}, Resources: []string{flowcontrol.ResourceAll}, ClusterScope: true, Namespaces: []string{flowcontrol.NamespaceEvery}, }}, NonResourceRules: []flowcontrol.NonResourcePolicyRule{{ Verbs: []string{flowcontrol.VerbAll}, NonResourceURLs: []string{"*"}, }}, }}, }, }, expectedErrors: field.ErrorList{}, }, { name: "bad exempt flow-schema should fail", flowSchema: &flowcontrol.FlowSchema{ ObjectMeta: metav1.ObjectMeta{ Name: flowcontrol.FlowSchemaNameExempt, }, Spec: badExempt, }, expectedErrors: field.ErrorList{field.Invalid(field.NewPath("spec"), badExempt, "spec of 'exempt' must equal the fixed value")}, }, { name: "bad catch-all flow-schema should fail", flowSchema: &flowcontrol.FlowSchema{ ObjectMeta: metav1.ObjectMeta{ Name: flowcontrol.FlowSchemaNameCatchAll, }, Spec: badCatchAll, }, expectedErrors: field.ErrorList{field.Invalid(field.NewPath("spec"), badCatchAll, "spec of 'catch-all' must equal the fixed value")}, }, { name: "catch-all flow-schema should work", flowSchema: &flowcontrol.FlowSchema{ ObjectMeta: metav1.ObjectMeta{ Name: flowcontrol.FlowSchemaNameCatchAll, }, Spec: flowcontrol.FlowSchemaSpec{ MatchingPrecedence: 10000, PriorityLevelConfiguration: flowcontrol.PriorityLevelConfigurationReference{ Name: flowcontrol.PriorityLevelConfigurationNameCatchAll, }, DistinguisherMethod: &flowcontrol.FlowDistinguisherMethod{Type: flowcontrol.FlowDistinguisherMethodByUserType}, Rules: []flowcontrol.PolicyRulesWithSubjects{{ Subjects: []flowcontrol.Subject{{ Kind: flowcontrol.SubjectKindGroup, Group: &flowcontrol.GroupSubject{Name: user.AllUnauthenticated}, }, { Kind: flowcontrol.SubjectKindGroup, Group: &flowcontrol.GroupSubject{Name: user.AllAuthenticated}, }}, ResourceRules: []flowcontrol.ResourcePolicyRule{{ Verbs: []string{flowcontrol.VerbAll}, APIGroups: []string{flowcontrol.APIGroupAll}, Resources: []string{flowcontrol.ResourceAll}, ClusterScope: true, Namespaces: []string{flowcontrol.NamespaceEvery}, }}, NonResourceRules: []flowcontrol.NonResourcePolicyRule{{ Verbs: []string{flowcontrol.VerbAll}, NonResourceURLs: []string{"*"}, }}, }}, }, }, expectedErrors: field.ErrorList{}, }, { name: "non-exempt flow-schema with matchingPrecedence==1 should fail", flowSchema: &flowcontrol.FlowSchema{ ObjectMeta: metav1.ObjectMeta{ Name: "fred", }, Spec: flowcontrol.FlowSchemaSpec{ MatchingPrecedence: 1, PriorityLevelConfiguration: flowcontrol.PriorityLevelConfigurationReference{ Name: "exempt", }, Rules: []flowcontrol.PolicyRulesWithSubjects{{ Subjects: []flowcontrol.Subject{{ Kind: flowcontrol.SubjectKindGroup, Group: &flowcontrol.GroupSubject{Name: "gorp"}, }}, NonResourceRules: []flowcontrol.NonResourcePolicyRule{{ Verbs: []string{flowcontrol.VerbAll}, NonResourceURLs: []string{"*"}, }}, }}, }, }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec").Child("matchingPrecedence"), int32(1), "only the schema named 'exempt' may have matchingPrecedence 1")}, }, { 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"}, Namespaces: []string{flowcontrol.NamespaceEvery}, }}, }}, }, }, 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}, Namespaces: []string{flowcontrol.NamespaceEvery}, }}, 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}, Namespaces: []string{flowcontrol.NamespaceEvery}, }}, }}, }, }, 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}, Namespaces: []string{flowcontrol.NamespaceEvery}, }}, }}, }, }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec").Child("priorityLevelConfiguration").Child("name"), "system+++$$", `a lowercase RFC 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}, Namespaces: []string{flowcontrol.NamespaceEvery}, }}, }}, }, }, 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}, Namespaces: []string{flowcontrol.NamespaceEvery}, }}, }}, }, }, expectedErrors: field.ErrorList{ field.NotSupported(field.NewPath("spec").Child("rules").Index(0).Child("subjects").Index(0).Child("kind"), flowcontrol.SubjectKind(""), supportedSubjectKinds.List()), }, }, { name: "Omitted ResourceRule.Namespaces 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}, APIGroups: []string{flowcontrol.APIGroupAll}, Resources: []string{flowcontrol.ResourceAll}, Namespaces: nil, }}, }}, }, }, expectedErrors: field.ErrorList{ field.Required(field.NewPath("spec").Child("rules").Index(0).Child("resourceRules").Index(0).Child("namespaces"), "resource rules that are not cluster scoped must supply at least one namespace"), }, }, { name: "ClusterScope is allowed, with no Namespaces", 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}, ClusterScope: true, }}, }}, }, }, expectedErrors: field.ErrorList{}, }, { name: "ClusterScope is allowed with NamespaceEvery", 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}, ClusterScope: true, Namespaces: []string{flowcontrol.NamespaceEvery}, }}, }}, }, }, expectedErrors: field.ErrorList{}, }, { name: "NamespaceEvery may not be combined with particulars", 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}, Namespaces: []string{"foo", flowcontrol.NamespaceEvery}, }}, }}, }, }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec").Child("rules").Index(0).Child("resourceRules").Index(0).Child("namespaces"), []string{"foo", flowcontrol.NamespaceEvery}, "if '*' is present, must not specify other namespaces"), }, }, { name: "ResourceRule.Namespaces must be well formed", 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}, Namespaces: []string{"-foo"}, }}, }}, }, }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec").Child("rules").Index(0).Child("resourceRules").Index(0).Child("namespaces").Index(0), "-foo", nsErrIntro+`a lowercase RFC 1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')`), }, }, { name: "MatchingPrecedence must not be greater than 10000", flowSchema: &flowcontrol.FlowSchema{ ObjectMeta: metav1.ObjectMeta{ Name: "system-foo", }, Spec: flowcontrol.FlowSchemaSpec{ MatchingPrecedence: 10001, 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}, Namespaces: []string{flowcontrol.NamespaceEvery}, }}, }}, }, }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec").Child("matchingPrecedence"), int32(10001), "must not be greater than 10000"), }, }} 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) { badSpec := flowcontrol.PriorityLevelConfigurationSpec{ Type: flowcontrol.PriorityLevelEnablementLimited, Limited: &flowcontrol.LimitedPriorityLevelConfiguration{ NominalConcurrencyShares: 42, LimitResponse: flowcontrol.LimitResponse{ Type: flowcontrol.LimitResponseTypeReject}, }, } badExemptSpec1 := flowcontrol.PriorityLevelConfigurationSpec{ Type: flowcontrol.PriorityLevelEnablementExempt, Exempt: &flowcontrol.ExemptPriorityLevelConfiguration{ NominalConcurrencyShares: pointer.Int32(-1), LendablePercent: pointer.Int32(101), }, } badExemptSpec2 := flowcontrol.PriorityLevelConfigurationSpec{ Type: flowcontrol.PriorityLevelEnablementExempt, Exempt: &flowcontrol.ExemptPriorityLevelConfiguration{ NominalConcurrencyShares: pointer.Int32(-1), LendablePercent: pointer.Int32(-1), }, } badExemptSpec3 := flowcontrol.PriorityLevelConfigurationSpec{ Type: flowcontrol.PriorityLevelEnablementExempt, Exempt: &flowcontrol.ExemptPriorityLevelConfiguration{}, Limited: &flowcontrol.LimitedPriorityLevelConfiguration{ NominalConcurrencyShares: 42, LimitResponse: flowcontrol.LimitResponse{ Type: flowcontrol.LimitResponseTypeReject}, }, } validChangesInExemptFieldOfExemptPLFn := func() flowcontrol.PriorityLevelConfigurationSpec { have, _ := internalbootstrap.MandatoryPriorityLevelConfigurations[flowcontrol.PriorityLevelConfigurationNameExempt] return flowcontrol.PriorityLevelConfigurationSpec{ Type: flowcontrol.PriorityLevelEnablementExempt, Exempt: &flowcontrol.ExemptPriorityLevelConfiguration{ NominalConcurrencyShares: pointer.Int32(*have.Spec.Exempt.NominalConcurrencyShares + 10), LendablePercent: pointer.Int32(*have.Spec.Exempt.LendablePercent + 10), }, } } exemptTypeRepurposed := &flowcontrol.PriorityLevelConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: flowcontrol.PriorityLevelConfigurationNameExempt, }, Spec: flowcontrol.PriorityLevelConfigurationSpec{ // changing the type from exempt to limited Type: flowcontrol.PriorityLevelEnablementLimited, Exempt: &flowcontrol.ExemptPriorityLevelConfiguration{}, Limited: &flowcontrol.LimitedPriorityLevelConfiguration{ NominalConcurrencyShares: 42, LimitResponse: flowcontrol.LimitResponse{ Type: flowcontrol.LimitResponseTypeReject}, }, }, } testCases := []struct { name string priorityLevelConfiguration *flowcontrol.PriorityLevelConfiguration requestGV *schema.GroupVersion expectedErrors field.ErrorList }{{ name: "exempt should work", priorityLevelConfiguration: &flowcontrol.PriorityLevelConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: flowcontrol.PriorityLevelConfigurationNameExempt, }, Spec: flowcontrol.PriorityLevelConfigurationSpec{ Type: flowcontrol.PriorityLevelEnablementExempt, Exempt: &flowcontrol.ExemptPriorityLevelConfiguration{ NominalConcurrencyShares: pointer.Int32(0), LendablePercent: pointer.Int32(0), }, }, }, expectedErrors: field.ErrorList{}, }, { name: "wrong exempt spec should fail", priorityLevelConfiguration: &flowcontrol.PriorityLevelConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: flowcontrol.PriorityLevelConfigurationNameExempt, }, Spec: badSpec, }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec").Child("type"), flowcontrol.PriorityLevelEnablementLimited, "type must be 'Exempt' if and only if name is 'exempt'"), field.Invalid(field.NewPath("spec"), badSpec, "spec of 'exempt' except the 'spec.exempt' field must equal the fixed value"), }, }, { name: "exempt priority level should have appropriate values for Exempt field", priorityLevelConfiguration: &flowcontrol.PriorityLevelConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: flowcontrol.PriorityLevelConfigurationNameExempt, }, Spec: badExemptSpec1, }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec").Child("exempt").Child("nominalConcurrencyShares"), int32(-1), "must be a non-negative integer"), field.Invalid(field.NewPath("spec").Child("exempt").Child("lendablePercent"), int32(101), "must be between 0 and 100, inclusive"), }, }, { name: "exempt priority level should have appropriate values for Exempt field", priorityLevelConfiguration: &flowcontrol.PriorityLevelConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: flowcontrol.PriorityLevelConfigurationNameExempt, }, Spec: badExemptSpec2, }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec").Child("exempt").Child("nominalConcurrencyShares"), int32(-1), "must be a non-negative integer"), field.Invalid(field.NewPath("spec").Child("exempt").Child("lendablePercent"), int32(-1), "must be between 0 and 100, inclusive"), }, }, { name: "admins are not allowed to repurpose the 'exempt' pl to a limited type", priorityLevelConfiguration: exemptTypeRepurposed, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec").Child("type"), flowcontrol.PriorityLevelEnablementLimited, "type must be 'Exempt' if and only if name is 'exempt'"), field.Forbidden(field.NewPath("spec").Child("exempt"), "must be nil if the type is Limited"), field.Invalid(field.NewPath("spec"), exemptTypeRepurposed.Spec, "spec of 'exempt' except the 'spec.exempt' field must equal the fixed value"), }, }, { name: "admins are not allowed to change any field of the 'exempt' pl except 'Exempt'", priorityLevelConfiguration: &flowcontrol.PriorityLevelConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: flowcontrol.PriorityLevelConfigurationNameExempt, }, Spec: badExemptSpec3, }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec"), badExemptSpec3, "spec of 'exempt' except the 'spec.exempt' field must equal the fixed value"), field.Forbidden(field.NewPath("spec").Child("limited"), "must be nil if the type is not Limited"), }, }, { name: "admins are allowed to change the Exempt field of the 'exempt' pl", priorityLevelConfiguration: &flowcontrol.PriorityLevelConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: flowcontrol.PriorityLevelConfigurationNameExempt, }, Spec: validChangesInExemptFieldOfExemptPLFn(), }, expectedErrors: field.ErrorList{}, }, { name: "limited must not set exempt priority level configuration for borrowing", priorityLevelConfiguration: &flowcontrol.PriorityLevelConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: "broken-limited", }, Spec: flowcontrol.PriorityLevelConfigurationSpec{ Type: flowcontrol.PriorityLevelEnablementLimited, Exempt: &flowcontrol.ExemptPriorityLevelConfiguration{}, }, }, expectedErrors: field.ErrorList{ field.Forbidden(field.NewPath("spec").Child("exempt"), "must be nil if the type is Limited"), field.Required(field.NewPath("spec").Child("limited"), "must not be empty when type is Limited"), }, }, { name: "limited requires more details", priorityLevelConfiguration: &flowcontrol.PriorityLevelConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: "broken-limited", }, Spec: flowcontrol.PriorityLevelConfigurationSpec{ Type: flowcontrol.PriorityLevelEnablementLimited, }, }, expectedErrors: field.ErrorList{field.Required(field.NewPath("spec").Child("limited"), "must not be empty when type is Limited")}, }, { name: "max-in-flight should work", priorityLevelConfiguration: &flowcontrol.PriorityLevelConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: "max-in-flight", }, Spec: flowcontrol.PriorityLevelConfigurationSpec{ Type: flowcontrol.PriorityLevelEnablementLimited, Limited: &flowcontrol.LimitedPriorityLevelConfiguration{ NominalConcurrencyShares: 42, LimitResponse: flowcontrol.LimitResponse{ Type: flowcontrol.LimitResponseTypeReject}, }, }, }, expectedErrors: field.ErrorList{}, }, { name: "forbid queuing details when not queuing", priorityLevelConfiguration: &flowcontrol.PriorityLevelConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: "system-foo", }, Spec: flowcontrol.PriorityLevelConfigurationSpec{ Type: flowcontrol.PriorityLevelEnablementLimited, Limited: &flowcontrol.LimitedPriorityLevelConfiguration{ NominalConcurrencyShares: 100, LimitResponse: flowcontrol.LimitResponse{ Type: flowcontrol.LimitResponseTypeReject, Queuing: &flowcontrol.QueuingConfiguration{ Queues: 512, HandSize: 4, QueueLengthLimit: 100, }}}}, }, expectedErrors: field.ErrorList{field.Forbidden(field.NewPath("spec").Child("limited").Child("limitResponse").Child("queuing"), "must be nil if limited.limitResponse.type is not Limited")}, }, { name: "wrong backstop spec should fail", priorityLevelConfiguration: &flowcontrol.PriorityLevelConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: flowcontrol.PriorityLevelConfigurationNameCatchAll, }, Spec: badSpec, }, expectedErrors: field.ErrorList{field.Invalid(field.NewPath("spec"), badSpec, "spec of 'catch-all' must equal the fixed value")}, }, { name: "backstop should work", priorityLevelConfiguration: &flowcontrol.PriorityLevelConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: flowcontrol.PriorityLevelConfigurationNameCatchAll, }, Spec: flowcontrol.PriorityLevelConfigurationSpec{ Type: flowcontrol.PriorityLevelEnablementLimited, Limited: &flowcontrol.LimitedPriorityLevelConfiguration{ NominalConcurrencyShares: 5, LendablePercent: pointer.Int32(0), LimitResponse: flowcontrol.LimitResponse{ Type: flowcontrol.LimitResponseTypeReject, }}}, }, expectedErrors: field.ErrorList{}, }, { name: "broken queuing level should fail", priorityLevelConfiguration: &flowcontrol.PriorityLevelConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: "system-foo", }, Spec: flowcontrol.PriorityLevelConfigurationSpec{ Type: flowcontrol.PriorityLevelEnablementLimited, Limited: &flowcontrol.LimitedPriorityLevelConfiguration{ NominalConcurrencyShares: 100, LimitResponse: flowcontrol.LimitResponse{ Type: flowcontrol.LimitResponseTypeQueue, }}}, }, expectedErrors: field.ErrorList{field.Required(field.NewPath("spec").Child("limited").Child("limitResponse").Child("queuing"), "must not be empty if limited.limitResponse.type is Limited")}, }, { name: "normal customized priority level should work", priorityLevelConfiguration: &flowcontrol.PriorityLevelConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: "system-foo", }, Spec: flowcontrol.PriorityLevelConfigurationSpec{ Type: flowcontrol.PriorityLevelEnablementLimited, Limited: &flowcontrol.LimitedPriorityLevelConfiguration{ NominalConcurrencyShares: 100, LimitResponse: flowcontrol.LimitResponse{ Type: flowcontrol.LimitResponseTypeQueue, Queuing: &flowcontrol.QueuingConfiguration{ Queues: 512, HandSize: 4, QueueLengthLimit: 100, }}}}, }, 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.PriorityLevelEnablementLimited, Limited: &flowcontrol.LimitedPriorityLevelConfiguration{ NominalConcurrencyShares: 100, LimitResponse: flowcontrol.LimitResponse{ Type: flowcontrol.LimitResponseTypeQueue, Queuing: &flowcontrol.QueuingConfiguration{ QueueLengthLimit: 100, Queues: 512, HandSize: 8, }}}}, }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec").Child("limited").Child("limitResponse").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.PriorityLevelEnablementLimited, Limited: &flowcontrol.LimitedPriorityLevelConfiguration{ NominalConcurrencyShares: 100, LimitResponse: flowcontrol.LimitResponse{ Type: flowcontrol.LimitResponseTypeQueue, Queuing: &flowcontrol.QueuingConfiguration{ QueueLengthLimit: 100, Queues: 128, HandSize: 10, }}}}, }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec").Child("limited").Child("limitResponse").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.PriorityLevelEnablementLimited, Limited: &flowcontrol.LimitedPriorityLevelConfiguration{ NominalConcurrencyShares: 100, LimitResponse: flowcontrol.LimitResponse{ Type: flowcontrol.LimitResponseTypeQueue, Queuing: &flowcontrol.QueuingConfiguration{ QueueLengthLimit: 100, Queues: math.MaxInt32, HandSize: 3, }}}}, }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec").Child("limited").Child("limitResponse").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("limited").Child("limitResponse").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.PriorityLevelEnablementLimited, Limited: &flowcontrol.LimitedPriorityLevelConfiguration{ NominalConcurrencyShares: 100, LimitResponse: flowcontrol.LimitResponse{ Type: flowcontrol.LimitResponseTypeQueue, Queuing: &flowcontrol.QueuingConfiguration{ 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.PriorityLevelEnablementLimited, Limited: &flowcontrol.LimitedPriorityLevelConfiguration{ NominalConcurrencyShares: 100, LimitResponse: flowcontrol.LimitResponse{ Type: flowcontrol.LimitResponseTypeQueue, Queuing: &flowcontrol.QueuingConfiguration{ QueueLengthLimit: 100, Queues: 7, HandSize: 8, }}}}, }, expectedErrors: field.ErrorList{ field.Invalid(field.NewPath("spec").Child("limited").Child("limitResponse").Child("queuing").Child("handSize"), int32(8), "should not be greater than queues (7)"), }, }, { name: "the roundtrip annotation is forbidden", priorityLevelConfiguration: &flowcontrol.PriorityLevelConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: "with-forbidden-annotation", Annotations: map[string]string{ flowcontrolv1beta3.PriorityLevelPreserveZeroConcurrencySharesKey: "", }, }, Spec: flowcontrol.PriorityLevelConfigurationSpec{ Type: flowcontrol.PriorityLevelEnablementLimited, Limited: &flowcontrol.LimitedPriorityLevelConfiguration{ NominalConcurrencyShares: 42, LimitResponse: flowcontrol.LimitResponse{ Type: flowcontrol.LimitResponseTypeReject}, }, }, }, // the internal object should never have the round trip annotation requestGV: &schema.GroupVersion{}, expectedErrors: field.ErrorList{ field.Forbidden(field.NewPath("metadata").Child("annotations"), fmt.Sprintf("annotation '%s' is forbidden", flowcontrolv1beta3.PriorityLevelPreserveZeroConcurrencySharesKey)), }, }} for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { gv := flowcontrolv1beta3.SchemeGroupVersion if testCase.requestGV != nil { gv = *testCase.requestGV } errs := ValidatePriorityLevelConfiguration(testCase.priorityLevelConfiguration, gv, PriorityLevelValidationOptions{}) 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)) } }) } } func TestValidateNonResourceURLPath(t *testing.T) { testCases := []struct { name string path string expectingError bool }{{ name: "empty string should fail", path: "", expectingError: true, }, { name: "no slash should fail", path: "foo", expectingError: true, }, { name: "single slash should work", path: "/", expectingError: false, }, { name: "continuous slash should fail", path: "//", expectingError: true, }, { name: "/foo slash should work", path: "/foo", expectingError: false, }, { name: "multiple continuous slashes should fail", path: "/////", expectingError: true, }, { name: "ending up with slash should work", path: "/apis/", expectingError: false, }, { name: "ending up with wildcard should work", path: "/healthz/*", expectingError: false, }, { name: "single wildcard inside the path should fail", path: "/healthz/*/foo", expectingError: true, }, { name: "white-space in the path should fail", path: "/healthz/foo bar", expectingError: true, }, { name: "wildcard plus plain path should fail", path: "/health*", expectingError: true, }, { name: "wildcard plus plain path should fail 2", path: "/health*/foo", expectingError: true, }, { name: "multiple wildcard internal and suffix should fail", path: "/*/*", expectingError: true, }} for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { err := ValidateNonResourceURLPath(testCase.path, field.NewPath("")) assert.Equal(t, testCase.expectingError, err != nil, "actual error: %v", err) }) } } func TestValidateLimitedPriorityLevelConfiguration(t *testing.T) { errExpectedFn := func(fieldName string, v int32, msg string) field.ErrorList { return field.ErrorList{ field.Invalid(field.NewPath("spec").Child("limited").Child(fieldName), int32(v), msg), } } tests := []struct { requestVersion schema.GroupVersion allowZero bool concurrencyShares int32 errExpected field.ErrorList }{{ requestVersion: flowcontrolv1beta1.SchemeGroupVersion, concurrencyShares: 0, errExpected: errExpectedFn("assuredConcurrencyShares", 0, "must be positive"), }, { requestVersion: flowcontrolv1beta2.SchemeGroupVersion, concurrencyShares: 0, errExpected: errExpectedFn("assuredConcurrencyShares", 0, "must be positive"), }, { requestVersion: flowcontrolv1beta3.SchemeGroupVersion, concurrencyShares: 0, errExpected: errExpectedFn("nominalConcurrencyShares", 0, "must be positive"), }, { requestVersion: flowcontrolv1.SchemeGroupVersion, concurrencyShares: 0, errExpected: errExpectedFn("nominalConcurrencyShares", 0, "must be positive"), }, { requestVersion: flowcontrolv1beta3.SchemeGroupVersion, concurrencyShares: 100, errExpected: nil, }, { requestVersion: flowcontrolv1beta3.SchemeGroupVersion, allowZero: true, concurrencyShares: 0, errExpected: nil, }, { requestVersion: flowcontrolv1beta3.SchemeGroupVersion, allowZero: true, concurrencyShares: -1, errExpected: errExpectedFn("nominalConcurrencyShares", -1, "must be a non-negative integer"), }, { requestVersion: flowcontrolv1beta3.SchemeGroupVersion, allowZero: true, concurrencyShares: 1, errExpected: nil, }, { requestVersion: flowcontrolv1.SchemeGroupVersion, allowZero: true, concurrencyShares: 0, errExpected: nil, }, { requestVersion: flowcontrolv1.SchemeGroupVersion, allowZero: true, concurrencyShares: -1, errExpected: errExpectedFn("nominalConcurrencyShares", -1, "must be a non-negative integer"), }, { requestVersion: flowcontrolv1.SchemeGroupVersion, allowZero: true, concurrencyShares: 1, errExpected: nil, }, { // this should never really happen in real life, the request // context should always contain the request {group, version} requestVersion: schema.GroupVersion{}, concurrencyShares: 0, errExpected: errExpectedFn("nominalConcurrencyShares", 0, "must be positive"), }} for _, test := range tests { t.Run(test.requestVersion.String(), func(t *testing.T) { configuration := &flowcontrol.LimitedPriorityLevelConfiguration{ NominalConcurrencyShares: test.concurrencyShares, LimitResponse: flowcontrol.LimitResponse{ Type: flowcontrol.LimitResponseTypeReject, }, } specPath := field.NewPath("spec").Child("limited") errGot := ValidateLimitedPriorityLevelConfiguration(configuration, test.requestVersion, specPath, PriorityLevelValidationOptions{AllowZeroLimitedNominalConcurrencyShares: test.allowZero}) if !cmp.Equal(test.errExpected, errGot) { t.Errorf("Expected error: %v, diff: %s", test.errExpected, cmp.Diff(test.errExpected, errGot)) } }) } } func TestValidateLimitedPriorityLevelConfigurationWithBorrowing(t *testing.T) { errLendablePercentFn := func(v int32) field.ErrorList { return field.ErrorList{ field.Invalid(field.NewPath("spec").Child("limited").Child("lendablePercent"), v, "must be between 0 and 100, inclusive"), } } errBorrowingLimitPercentFn := func(v int32) field.ErrorList { return field.ErrorList{ field.Invalid(field.NewPath("spec").Child("limited").Child("borrowingLimitPercent"), v, "if specified, must be a non-negative integer"), } } makeTestNameFn := func(lendablePercent *int32, borrowingLimitPercent *int32) string { formatFn := func(v *int32) string { if v == nil { return "" } return fmt.Sprintf("%d", *v) } return fmt.Sprintf("lendablePercent %s, borrowingLimitPercent %s", formatFn(lendablePercent), formatFn(borrowingLimitPercent)) } tests := []struct { lendablePercent *int32 borrowingLimitPercent *int32 errExpected field.ErrorList }{{ lendablePercent: nil, errExpected: nil, }, { lendablePercent: pointer.Int32(0), errExpected: nil, }, { lendablePercent: pointer.Int32(100), errExpected: nil, }, { lendablePercent: pointer.Int32(101), errExpected: errLendablePercentFn(101), }, { lendablePercent: pointer.Int32(-1), errExpected: errLendablePercentFn(-1), }, { borrowingLimitPercent: nil, errExpected: nil, }, { borrowingLimitPercent: pointer.Int32(1), errExpected: nil, }, { borrowingLimitPercent: pointer.Int32(100), errExpected: nil, }, { borrowingLimitPercent: pointer.Int32(0), errExpected: nil, }, { borrowingLimitPercent: pointer.Int32(-1), errExpected: errBorrowingLimitPercentFn(-1), }} for _, test := range tests { t.Run(makeTestNameFn(test.lendablePercent, test.borrowingLimitPercent), func(t *testing.T) { configuration := &flowcontrol.LimitedPriorityLevelConfiguration{ NominalConcurrencyShares: 1, LimitResponse: flowcontrol.LimitResponse{ Type: flowcontrol.LimitResponseTypeReject, }, LendablePercent: test.lendablePercent, BorrowingLimitPercent: test.borrowingLimitPercent, } specPath := field.NewPath("spec").Child("limited") errGot := ValidateLimitedPriorityLevelConfiguration(configuration, flowcontrolv1.SchemeGroupVersion, specPath, PriorityLevelValidationOptions{}) if !cmp.Equal(test.errExpected, errGot) { t.Errorf("Expected error: %v, diff: %s", test.errExpected, cmp.Diff(test.errExpected, errGot)) } }) } }