/* Copyright 2023 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 validatingadmissionpolicystatus import ( "context" "strings" "testing" "time" admissionregistrationv1 "k8s.io/api/admissionregistration/v1" "k8s.io/apimachinery/pkg/api/meta/testrestmapper" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/wait" validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating" "k8s.io/apiserver/pkg/cel/openapi/resolver" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/scheme" "k8s.io/kubernetes/pkg/generated/openapi" ) func TestTypeChecking(t *testing.T) { for _, tc := range []struct { name string policy *admissionregistrationv1.ValidatingAdmissionPolicy assertFieldRef func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) // warning.fieldRef assertWarnings func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) // warning.warning }{ { name: "deployment with correct expression", policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1.Validation{ { Expression: "object.spec.replicas > 1", }, }, makePolicy("replicated-deployment"))), assertFieldRef: toHaveLengthOf(0), assertWarnings: toHaveLengthOf(0), }, { name: "deployment with type confusion", policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1.Validation{ { Expression: "object.spec.replicas < 100", // this one passes }, { Expression: "object.spec.replicas > '1'", // '1' should be int }, }, makePolicy("confused-deployment"))), assertFieldRef: toBe("spec.validations[1].expression"), assertWarnings: toHaveSubstring(`found no matching overload for '_>_' applied to '(int, string)'`), }, { name: "two expressions different type checking errors", policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1.Validation{ { Expression: "object.spec.nonExistingFirst > 1", }, { Expression: "object.spec.replicas > '1'", // '1' should be int }, }, makePolicy("confused-deployment"))), assertFieldRef: toBe("spec.validations[0].expression", "spec.validations[1].expression"), assertWarnings: toHaveSubstring( "undefined field 'nonExistingFirst'", `found no matching overload for '_>_' applied to '(int, string)'`, ), }, { name: "one expression, two warnings", policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1.Validation{ { Expression: "object.spec.replicas < 100", // this one passes }, { Expression: "object.spec.replicas > '1' && object.spec.nonExisting == 1", }, }, makePolicy("confused-deployment"))), assertFieldRef: toBe("spec.validations[1].expression"), assertWarnings: toHaveMultipleSubstrings([]string{"undefined field 'nonExisting'", `found no matching overload for '_>_' applied to '(int, string)'`}), }, } { t.Run(tc.name, func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() policy := tc.policy.DeepCopy() policy.ObjectMeta.Generation = 1 // fake storage does not do this automatically client := fake.NewSimpleClientset(policy) informerFactory := informers.NewSharedInformerFactory(client, 0) typeChecker := &validatingadmissionpolicy.TypeChecker{ SchemaResolver: resolver.NewDefinitionsSchemaResolver(openapi.GetOpenAPIDefinitions, scheme.Scheme), RestMapper: testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme), } controller, err := NewController( informerFactory.Admissionregistration().V1().ValidatingAdmissionPolicies(), client.AdmissionregistrationV1().ValidatingAdmissionPolicies(), typeChecker, ) if err != nil { t.Fatalf("cannot create controller: %v", err) } go informerFactory.Start(ctx.Done()) go controller.Run(ctx, 1) err = wait.PollUntilContextCancel(ctx, time.Second, false, func(ctx context.Context) (done bool, err error) { name := policy.Name // wait until the typeChecking is set, which means the type checking // is complete. updated, err := client.AdmissionregistrationV1().ValidatingAdmissionPolicies().Get(ctx, name, metav1.GetOptions{}) if err != nil { return false, err } if updated.Status.TypeChecking != nil { policy = updated return true, nil } return false, nil }) if err != nil { t.Fatal(err) } tc.assertFieldRef(policy.Status.TypeChecking.ExpressionWarnings, t) tc.assertWarnings(policy.Status.TypeChecking.ExpressionWarnings, t) if err != nil { t.Fatalf("failed to initialize controller: %v", err) } }) } } func toBe(expected ...string) func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) { return func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) { if len(expected) != len(warnings) { t.Fatalf("mismatched length, expect %d, got %d", len(expected), len(warnings)) } for i := range expected { if expected[i] != warnings[i].FieldRef { t.Errorf("expected %q but got %q", expected[i], warnings[i].FieldRef) } } } } func toHaveSubstring(substrings ...string) func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) { return func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) { if len(substrings) != len(warnings) { t.Fatalf("mismatched length, expect %d, got %d", len(substrings), len(warnings)) } for i := range substrings { if !strings.Contains(warnings[i].Warning, substrings[i]) { t.Errorf("missing expected substring %q in %v", substrings[i], warnings[i]) } } } } func toHaveMultipleSubstrings(substrings ...[]string) func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) { return func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) { if len(substrings) != len(warnings) { t.Fatalf("mismatched length, expect %d, got %d", len(substrings), len(warnings)) } for i, expectedSubstrings := range substrings { for _, s := range expectedSubstrings { if !strings.Contains(warnings[i].Warning, s) { t.Errorf("missing expected substring %q in %v", substrings[i], warnings[i]) } } } } } func toHaveLengthOf(n int) func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) { return func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) { if n != len(warnings) { t.Fatalf("mismatched length, expect %d, got %d", n, len(warnings)) } } } func withGVRMatch(groups []string, versions []string, resources []string, policy *admissionregistrationv1.ValidatingAdmissionPolicy) *admissionregistrationv1.ValidatingAdmissionPolicy { policy.Spec.MatchConstraints = &admissionregistrationv1.MatchResources{ ResourceRules: []admissionregistrationv1.NamedRuleWithOperations{ { RuleWithOperations: admissionregistrationv1.RuleWithOperations{ Operations: []admissionregistrationv1.OperationType{ "*", }, Rule: admissionregistrationv1.Rule{ APIGroups: groups, APIVersions: versions, Resources: resources, }, }, }, }, } return policy } func withValidations(validations []admissionregistrationv1.Validation, policy *admissionregistrationv1.ValidatingAdmissionPolicy) *admissionregistrationv1.ValidatingAdmissionPolicy { policy.Spec.Validations = validations return policy } func makePolicy(name string) *admissionregistrationv1.ValidatingAdmissionPolicy { return &admissionregistrationv1.ValidatingAdmissionPolicy{ ObjectMeta: metav1.ObjectMeta{Name: name}, } }