k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/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 "k8s.io/apimachinery/pkg/api/meta/testrestmapper" 27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 "k8s.io/apimachinery/pkg/util/wait" 29 validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating" 30 "k8s.io/apiserver/pkg/cel/openapi/resolver" 31 "k8s.io/client-go/informers" 32 "k8s.io/client-go/kubernetes/fake" 33 "k8s.io/client-go/kubernetes/scheme" 34 "k8s.io/kubernetes/pkg/generated/openapi" 35 ) 36 37 func TestTypeChecking(t *testing.T) { 38 for _, tc := range []struct { 39 name string 40 policy *admissionregistrationv1.ValidatingAdmissionPolicy 41 assertFieldRef func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) // warning.fieldRef 42 assertWarnings func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) // warning.warning 43 }{ 44 { 45 name: "deployment with correct expression", 46 policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1.Validation{ 47 { 48 Expression: "object.spec.replicas > 1", 49 }, 50 }, makePolicy("replicated-deployment"))), 51 assertFieldRef: toHaveLengthOf(0), 52 assertWarnings: toHaveLengthOf(0), 53 }, 54 { 55 name: "deployment with type confusion", 56 policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1.Validation{ 57 { 58 Expression: "object.spec.replicas < 100", // this one passes 59 }, 60 { 61 Expression: "object.spec.replicas > '1'", // '1' should be int 62 }, 63 }, makePolicy("confused-deployment"))), 64 assertFieldRef: toBe("spec.validations[1].expression"), 65 assertWarnings: toHaveSubstring(`found no matching overload for '_>_' applied to '(int, string)'`), 66 }, 67 { 68 name: "two expressions different type checking errors", 69 policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1.Validation{ 70 { 71 Expression: "object.spec.nonExistingFirst > 1", 72 }, 73 { 74 Expression: "object.spec.replicas > '1'", // '1' should be int 75 }, 76 }, makePolicy("confused-deployment"))), 77 assertFieldRef: toBe("spec.validations[0].expression", "spec.validations[1].expression"), 78 assertWarnings: toHaveSubstring( 79 "undefined field 'nonExistingFirst'", 80 `found no matching overload for '_>_' applied to '(int, string)'`, 81 ), 82 }, 83 { 84 name: "one expression, two warnings", 85 policy: withGVRMatch([]string{"apps"}, []string{"v1"}, []string{"deployments"}, withValidations([]admissionregistrationv1.Validation{ 86 { 87 Expression: "object.spec.replicas < 100", // this one passes 88 }, 89 { 90 Expression: "object.spec.replicas > '1' && object.spec.nonExisting == 1", 91 }, 92 }, makePolicy("confused-deployment"))), 93 assertFieldRef: toBe("spec.validations[1].expression"), 94 assertWarnings: toHaveMultipleSubstrings([]string{"undefined field 'nonExisting'", `found no matching overload for '_>_' applied to '(int, string)'`}), 95 }, 96 } { 97 t.Run(tc.name, func(t *testing.T) { 98 ctx, cancel := context.WithTimeout(context.Background(), time.Minute) 99 defer cancel() 100 policy := tc.policy.DeepCopy() 101 policy.ObjectMeta.Generation = 1 // fake storage does not do this automatically 102 client := fake.NewSimpleClientset(policy) 103 informerFactory := informers.NewSharedInformerFactory(client, 0) 104 typeChecker := &validatingadmissionpolicy.TypeChecker{ 105 SchemaResolver: resolver.NewDefinitionsSchemaResolver(openapi.GetOpenAPIDefinitions, scheme.Scheme), 106 RestMapper: testrestmapper.TestOnlyStaticRESTMapper(scheme.Scheme), 107 } 108 controller, err := NewController( 109 informerFactory.Admissionregistration().V1().ValidatingAdmissionPolicies(), 110 client.AdmissionregistrationV1().ValidatingAdmissionPolicies(), 111 typeChecker, 112 ) 113 if err != nil { 114 t.Fatalf("cannot create controller: %v", err) 115 } 116 go informerFactory.Start(ctx.Done()) 117 go controller.Run(ctx, 1) 118 err = wait.PollUntilContextCancel(ctx, time.Second, false, func(ctx context.Context) (done bool, err error) { 119 name := policy.Name 120 // wait until the typeChecking is set, which means the type checking 121 // is complete. 122 updated, err := client.AdmissionregistrationV1().ValidatingAdmissionPolicies().Get(ctx, name, metav1.GetOptions{}) 123 if err != nil { 124 return false, err 125 } 126 if updated.Status.TypeChecking != nil { 127 policy = updated 128 return true, nil 129 } 130 return false, nil 131 }) 132 if err != nil { 133 t.Fatal(err) 134 } 135 tc.assertFieldRef(policy.Status.TypeChecking.ExpressionWarnings, t) 136 tc.assertWarnings(policy.Status.TypeChecking.ExpressionWarnings, t) 137 if err != nil { 138 t.Fatalf("failed to initialize controller: %v", err) 139 } 140 }) 141 } 142 143 } 144 145 func toBe(expected ...string) func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) { 146 return func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) { 147 if len(expected) != len(warnings) { 148 t.Fatalf("mismatched length, expect %d, got %d", len(expected), len(warnings)) 149 } 150 for i := range expected { 151 if expected[i] != warnings[i].FieldRef { 152 t.Errorf("expected %q but got %q", expected[i], warnings[i].FieldRef) 153 } 154 } 155 } 156 } 157 158 func toHaveSubstring(substrings ...string) func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) { 159 return func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) { 160 if len(substrings) != len(warnings) { 161 t.Fatalf("mismatched length, expect %d, got %d", len(substrings), len(warnings)) 162 } 163 for i := range substrings { 164 if !strings.Contains(warnings[i].Warning, substrings[i]) { 165 t.Errorf("missing expected substring %q in %v", substrings[i], warnings[i]) 166 } 167 } 168 } 169 } 170 171 func toHaveMultipleSubstrings(substrings ...[]string) func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) { 172 return func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) { 173 if len(substrings) != len(warnings) { 174 t.Fatalf("mismatched length, expect %d, got %d", len(substrings), len(warnings)) 175 } 176 for i, expectedSubstrings := range substrings { 177 for _, s := range expectedSubstrings { 178 if !strings.Contains(warnings[i].Warning, s) { 179 t.Errorf("missing expected substring %q in %v", substrings[i], warnings[i]) 180 } 181 } 182 } 183 } 184 } 185 186 func toHaveLengthOf(n int) func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) { 187 return func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T) { 188 if n != len(warnings) { 189 t.Fatalf("mismatched length, expect %d, got %d", n, len(warnings)) 190 } 191 } 192 } 193 194 func withGVRMatch(groups []string, versions []string, resources []string, policy *admissionregistrationv1.ValidatingAdmissionPolicy) *admissionregistrationv1.ValidatingAdmissionPolicy { 195 policy.Spec.MatchConstraints = &admissionregistrationv1.MatchResources{ 196 ResourceRules: []admissionregistrationv1.NamedRuleWithOperations{ 197 { 198 RuleWithOperations: admissionregistrationv1.RuleWithOperations{ 199 Operations: []admissionregistrationv1.OperationType{ 200 "*", 201 }, 202 Rule: admissionregistrationv1.Rule{ 203 APIGroups: groups, 204 APIVersions: versions, 205 Resources: resources, 206 }, 207 }, 208 }, 209 }, 210 } 211 return policy 212 } 213 214 func withValidations(validations []admissionregistrationv1.Validation, policy *admissionregistrationv1.ValidatingAdmissionPolicy) *admissionregistrationv1.ValidatingAdmissionPolicy { 215 policy.Spec.Validations = validations 216 return policy 217 } 218 219 func makePolicy(name string) *admissionregistrationv1.ValidatingAdmissionPolicy { 220 return &admissionregistrationv1.ValidatingAdmissionPolicy{ 221 ObjectMeta: metav1.ObjectMeta{Name: name}, 222 } 223 }