1
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)
42 assertWarnings func(warnings []admissionregistrationv1.ExpressionWarning, t *testing.T)
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",
59 },
60 {
61 Expression: "object.spec.replicas > '1'",
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'",
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",
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
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
121
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 }
224
View as plain text