k8s.io/kubernetes@v1.29.3/pkg/controller/validatingadmissionpolicystatus/controller_test.go (about) 1 /* 2 Copyright 2023 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package validatingadmissionpolicystatus 18 19 import ( 20 "context" 21 "strings" 22 "testing" 23 "time" 24 25 admissionregistrationv1 "k8s.io/api/admissionregistration/v1" 26 admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" 27 "k8s.io/apimachinery/pkg/api/meta/testrestmapper" 28 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/apimachinery/pkg/util/wait" 30 "k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy" 31 "k8s.io/apiserver/pkg/cel/openapi/resolver" 32 "k8s.io/client-go/informers" 33 "k8s.io/client-go/kubernetes/fake" 34 "k8s.io/client-go/kubernetes/scheme" 35 "k8s.io/kubernetes/pkg/generated/openapi" 36 ) 37 38 func TestTypeChecking(t *testing.T) { 39 for _, tc := range []struct { 40 name string 41 policy *admissionregistrationv1beta1.ValidatingAdmissionPolicy 42 assertFieldRef func(warnings []admissionregistrationv1beta1.ExpressionWarning, t *testing.T) // warning.fieldRef 43 assertWarnings func(warnings []admissionregistrationv1beta1.ExpressionWarning, t *testing.T) // warning.warning 44 }{ 45 { 46 name: "deployment with correct expression", 47 policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1beta1.Validation{ 48 { 49 Expression: "object.spec.replicas > 1", 50 }, 51 }, makePolicy("replicated-deployment"))), 52 assertFieldRef: toHaveLengthOf(0), 53 assertWarnings: toHaveLengthOf(0), 54 }, 55 { 56 name: "deployment with type confusion", 57 policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1beta1.Validation{ 58 { 59 Expression: "object.spec.replicas < 100", // this one passes 60 }, 61 { 62 Expression: "object.spec.replicas > '1'", // '1' should be int 63 }, 64 }, makePolicy("confused-deployment"))), 65 assertFieldRef: toBe("spec.validations[1].expression"), 66 assertWarnings: toHaveSubstring(`found no matching overload for '_>_' applied to '(int, string)'`), 67 }, 68 { 69 name: "two expressions different type checking errors", 70 policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1beta1.Validation{ 71 { 72 Expression: "object.spec.nonExistingFirst > 1", 73 }, 74 { 75 Expression: "object.spec.replicas > '1'", // '1' should be int 76 }, 77 }, makePolicy("confused-deployment"))), 78 assertFieldRef: toBe("spec.validations[0].expression", "spec.validations[1].expression"), 79 assertWarnings: toHaveSubstring( 80 "undefined field 'nonExistingFirst'", 81 `found no matching overload for '_>_' applied to '(int, string)'`, 82 ), 83 }, 84 { 85 name: "one expression, two warnings", 86 policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1beta1.Validation{ 87 { 88 Expression: "object.spec.replicas < 100", // this one passes 89 }, 90 { 91 Expression: "object.spec.replicas > '1' && object.spec.nonExisting == 1", 92 }, 93 }, makePolicy("confused-deployment"))), 94 assertFieldRef: toBe("spec.validations[1].expression"), 95 assertWarnings: toHaveMultipleSubstrings([]string{"undefined field 'nonExisting'", `found no matching overload for '_>_' applied to '(int, string)'`}), 96 }, 97 } { 98 t.Run(tc.name, func(t *testing.T) { 99 ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 100 defer cancel() 101 policy := tc.policy.DeepCopy() 102 policy.ObjectMeta.Generation = 1 // fake storage does not do this automatically 103 client := fake.NewSimpleClientset(policy) 104 informerFactory := informers.NewSharedInformerFactory(client, 0) 105 typeChecker := &validatingadmissionpolicy.TypeChecker{ 106 SchemaResolver: resolver.NewDefinitionsSchemaResolver(openapi.GetOpenAPIDefinitions, scheme.Scheme), 107 RestMapper: testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme), 108 } 109 controller, err := NewController( 110 informerFactory.Admissionregistration().V1beta1().ValidatingAdmissionPolicies(), 111 client.AdmissionregistrationV1beta1().ValidatingAdmissionPolicies(), 112 typeChecker, 113 ) 114 if err != nil { 115 t.Fatalf("cannot create controller: %v", err) 116 } 117 go informerFactory.Start(ctx.Done()) 118 go controller.Run(ctx, 1) 119 err = wait.PollUntilContextCancel(ctx, time.Second, false, func(ctx context.Context) (done bool, err error) { 120 name := policy.Name 121 // wait until the typeChecking is set, which means the type checking 122 // is complete. 123 updated, err := client.AdmissionregistrationV1beta1().ValidatingAdmissionPolicies().Get(ctx, name, metav1.GetOptions{}) 124 if err != nil { 125 return false, err 126 } 127 if updated.Status.TypeChecking != nil { 128 policy = updated 129 return true, nil 130 } 131 return false, nil 132 }) 133 if err != nil { 134 t.Fatal(err) 135 } 136 tc.assertFieldRef(policy.Status.TypeChecking.ExpressionWarnings, t) 137 tc.assertWarnings(policy.Status.TypeChecking.ExpressionWarnings, t) 138 if err != nil { 139 t.Fatalf("failed to initialize controller: %v", err) 140 } 141 }) 142 } 143 144 } 145 146 func toBe(expected ...string) func(warnings []admissionregistrationv1beta1.ExpressionWarning, t *testing.T) { 147 return func(warnings []admissionregistrationv1beta1.ExpressionWarning, t *testing.T) { 148 if len(expected) != len(warnings) { 149 t.Fatalf("mismatched length, expect %d, got %d", len(expected), len(warnings)) 150 } 151 for i := range expected { 152 if expected[i] != warnings[i].FieldRef { 153 t.Errorf("expected %q but got %q", expected[i], warnings[i].FieldRef) 154 } 155 } 156 } 157 } 158 159 func toHaveSubstring(substrings ...string) func(warnings []admissionregistrationv1beta1.ExpressionWarning, t *testing.T) { 160 return func(warnings []admissionregistrationv1beta1.ExpressionWarning, t *testing.T) { 161 if len(substrings) != len(warnings) { 162 t.Fatalf("mismatched length, expect %d, got %d", len(substrings), len(warnings)) 163 } 164 for i := range substrings { 165 if !strings.Contains(warnings[i].Warning, substrings[i]) { 166 t.Errorf("missing expected substring %q in %v", substrings[i], warnings[i]) 167 } 168 } 169 } 170 } 171 172 func toHaveMultipleSubstrings(substrings ...[]string) func(warnings []admissionregistrationv1beta1.ExpressionWarning, t *testing.T) { 173 return func(warnings []admissionregistrationv1beta1.ExpressionWarning, t *testing.T) { 174 if len(substrings) != len(warnings) { 175 t.Fatalf("mismatched length, expect %d, got %d", len(substrings), len(warnings)) 176 } 177 for i, expectedSubstrings := range substrings { 178 for _, s := range expectedSubstrings { 179 if !strings.Contains(warnings[i].Warning, s) { 180 t.Errorf("missing expected substring %q in %v", substrings[i], warnings[i]) 181 } 182 } 183 } 184 } 185 } 186 187 func toHaveLengthOf(n int) func(warnings []admissionregistrationv1beta1.ExpressionWarning, t *testing.T) { 188 return func(warnings []admissionregistrationv1beta1.ExpressionWarning, t *testing.T) { 189 if n != len(warnings) { 190 t.Fatalf("mismatched length, expect %d, got %d", n, len(warnings)) 191 } 192 } 193 } 194 195 func withGVRMatch(groups []string, versions []string, resources []string, policy *admissionregistrationv1beta1.ValidatingAdmissionPolicy) *admissionregistrationv1beta1.ValidatingAdmissionPolicy { 196 policy.Spec.MatchConstraints = &admissionregistrationv1beta1.MatchResources{ 197 ResourceRules: []admissionregistrationv1beta1.NamedRuleWithOperations{ 198 { 199 RuleWithOperations: admissionregistrationv1beta1.RuleWithOperations{ 200 Operations: []admissionregistrationv1.OperationType{ 201 "*", 202 }, 203 Rule: admissionregistrationv1.Rule{ 204 APIGroups: groups, 205 APIVersions: versions, 206 Resources: resources, 207 }, 208 }, 209 }, 210 }, 211 } 212 return policy 213 } 214 215 func withValidations(validations []admissionregistrationv1beta1.Validation, policy *admissionregistrationv1beta1.ValidatingAdmissionPolicy) *admissionregistrationv1beta1.ValidatingAdmissionPolicy { 216 policy.Spec.Validations = validations 217 return policy 218 } 219 220 func makePolicy(name string) *admissionregistrationv1beta1.ValidatingAdmissionPolicy { 221 return &admissionregistrationv1beta1.ValidatingAdmissionPolicy{ 222 ObjectMeta: metav1.ObjectMeta{Name: name}, 223 } 224 }