1
16
17 package node
18
19 import (
20 "context"
21 "fmt"
22 "math/rand"
23 "os"
24 "runtime"
25 "runtime/pprof"
26 "sync/atomic"
27 "testing"
28 "time"
29
30 corev1 "k8s.io/api/core/v1"
31 resourcev1alpha2 "k8s.io/api/resource/v1alpha2"
32 storagev1 "k8s.io/api/storage/v1"
33 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
34 "k8s.io/apiserver/pkg/authentication/user"
35 "k8s.io/apiserver/pkg/authorization/authorizer"
36 utilfeature "k8s.io/apiserver/pkg/util/feature"
37 "k8s.io/component-base/featuregate"
38 "k8s.io/kubernetes/pkg/auth/nodeidentifier"
39 "k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac/bootstrappolicy"
40 )
41
42 func TestAuthorizer(t *testing.T) {
43 g := NewGraph()
44
45 opts := &sampleDataOpts{
46 nodes: 2,
47 namespaces: 2,
48 podsPerNode: 2,
49 attachmentsPerNode: 1,
50 sharedConfigMapsPerPod: 0,
51 uniqueConfigMapsPerPod: 1,
52 sharedSecretsPerPod: 1,
53 uniqueSecretsPerPod: 1,
54 sharedPVCsPerPod: 0,
55 uniquePVCsPerPod: 1,
56 uniqueResourceClaimsPerPod: 1,
57 uniqueResourceClaimTemplatesPerPod: 1,
58 uniqueResourceClaimTemplatesWithClaimPerPod: 1,
59 nodeResourceCapacitiesPerNode: 2,
60 }
61 nodes, pods, pvs, attachments, slices := generate(opts)
62 populate(g, nodes, pods, pvs, attachments, slices)
63
64 identifier := nodeidentifier.NewDefaultNodeIdentifier()
65 authz := NewAuthorizer(g, identifier, bootstrappolicy.NodeRules())
66
67 node0 := &user.DefaultInfo{Name: "system:node:node0", Groups: []string{"system:nodes"}}
68
69 tests := []struct {
70 name string
71 attrs authorizer.AttributesRecord
72 expect authorizer.Decision
73 features featuregate.FeatureGate
74 }{
75 {
76 name: "allowed configmap",
77 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "configmaps", Name: "configmap0-pod0-node0", Namespace: "ns0"},
78 expect: authorizer.DecisionAllow,
79 },
80 {
81 name: "allowed secret via pod",
82 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "secrets", Name: "secret0-pod0-node0", Namespace: "ns0"},
83 expect: authorizer.DecisionAllow,
84 },
85 {
86 name: "list allowed secret via pod",
87 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "secrets", Name: "secret0-pod0-node0", Namespace: "ns0"},
88 expect: authorizer.DecisionAllow,
89 },
90 {
91 name: "watch allowed secret via pod",
92 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "secrets", Name: "secret0-pod0-node0", Namespace: "ns0"},
93 expect: authorizer.DecisionAllow,
94 },
95 {
96 name: "disallowed list many secrets",
97 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "secrets", Name: "", Namespace: "ns0"},
98 expect: authorizer.DecisionNoOpinion,
99 },
100 {
101 name: "disallowed watch many secrets",
102 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "secrets", Name: "", Namespace: "ns0"},
103 expect: authorizer.DecisionNoOpinion,
104 },
105 {
106 name: "disallowed list secrets from all namespaces with name",
107 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "secrets", Name: "secret0-pod0-node0", Namespace: ""},
108 expect: authorizer.DecisionNoOpinion,
109 },
110 {
111 name: "allowed shared secret via pod",
112 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "secrets", Name: "secret0-shared", Namespace: "ns0"},
113 expect: authorizer.DecisionAllow,
114 },
115 {
116 name: "allowed shared secret via pvc",
117 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "secrets", Name: "secret-pv0-pod0-node0-ns0", Namespace: "ns0"},
118 expect: authorizer.DecisionAllow,
119 },
120 {
121 name: "allowed pvc",
122 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "persistentvolumeclaims", Name: "pvc0-pod0-node0", Namespace: "ns0"},
123 expect: authorizer.DecisionAllow,
124 },
125 {
126 name: "allowed resource claim",
127 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "resourceclaims", APIGroup: "resource.k8s.io", Name: "claim0-pod0-node0-ns0", Namespace: "ns0"},
128 expect: authorizer.DecisionAllow,
129 },
130 {
131 name: "allowed resource claim with template",
132 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "resourceclaims", APIGroup: "resource.k8s.io", Name: "generated-claim-pod0-node0-ns0-0", Namespace: "ns0"},
133 expect: authorizer.DecisionAllow,
134 },
135 {
136 name: "allowed pv",
137 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "persistentvolumes", Name: "pv0-pod0-node0-ns0", Namespace: ""},
138 expect: authorizer.DecisionAllow,
139 },
140 {
141 name: "disallowed configmap",
142 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "configmaps", Name: "configmap0-pod0-node1", Namespace: "ns0"},
143 expect: authorizer.DecisionNoOpinion,
144 },
145 {
146 name: "disallowed secret via pod",
147 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "secrets", Name: "secret0-pod0-node1", Namespace: "ns0"},
148 expect: authorizer.DecisionNoOpinion,
149 },
150 {
151 name: "disallowed shared secret via pvc",
152 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "secrets", Name: "secret-pv0-pod0-node1-ns0", Namespace: "ns0"},
153 expect: authorizer.DecisionNoOpinion,
154 },
155 {
156 name: "disallowed pvc",
157 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "persistentvolumeclaims", Name: "pvc0-pod0-node1", Namespace: "ns0"},
158 expect: authorizer.DecisionNoOpinion,
159 },
160 {
161 name: "disallowed resource claim, other node",
162 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "resourceclaims", APIGroup: "resource.k8s.io", Name: "claim0-pod0-node1-ns0", Namespace: "ns0"},
163 expect: authorizer.DecisionNoOpinion,
164 },
165 {
166 name: "disallowed resource claim with template",
167 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "resourceclaims", APIGroup: "resource.k8s.io", Name: "pod0-node1-claimtemplate0", Namespace: "ns0"},
168 expect: authorizer.DecisionNoOpinion,
169 },
170 {
171 name: "disallowed pv",
172 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "persistentvolumes", Name: "pv0-pod0-node1-ns0", Namespace: ""},
173 expect: authorizer.DecisionNoOpinion,
174 },
175 {
176 name: "allowed attachment",
177 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "volumeattachments", APIGroup: "storage.k8s.io", Name: "attachment0-node0"},
178 expect: authorizer.DecisionAllow,
179 },
180 {
181 name: "allowed svcacct token create",
182 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "create", Resource: "serviceaccounts", Subresource: "token", Name: "svcacct0-node0", Namespace: "ns0"},
183 expect: authorizer.DecisionAllow,
184 },
185 {
186 name: "disallowed svcacct token create - serviceaccount not attached to node",
187 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "create", Resource: "serviceaccounts", Subresource: "token", Name: "svcacct0-node1", Namespace: "ns0"},
188 expect: authorizer.DecisionNoOpinion,
189 },
190 {
191 name: "disallowed svcacct token create - no subresource",
192 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "create", Resource: "serviceaccounts", Name: "svcacct0-node0", Namespace: "ns0"},
193 expect: authorizer.DecisionNoOpinion,
194 },
195 {
196 name: "disallowed svcacct token create - non create",
197 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "update", Resource: "serviceaccounts", Subresource: "token", Name: "svcacct0-node0", Namespace: "ns0"},
198 expect: authorizer.DecisionNoOpinion,
199 },
200 {
201 name: "disallowed get lease in namespace other than kube-node-lease - feature enabled",
202 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "leases", APIGroup: "coordination.k8s.io", Name: "node0", Namespace: "foo"},
203 expect: authorizer.DecisionNoOpinion,
204 },
205 {
206 name: "disallowed create lease in namespace other than kube-node-lease - feature enabled",
207 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "create", Resource: "leases", APIGroup: "coordination.k8s.io", Name: "node0", Namespace: "foo"},
208 expect: authorizer.DecisionNoOpinion,
209 },
210 {
211 name: "disallowed update lease in namespace other than kube-node-lease - feature enabled",
212 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "update", Resource: "leases", APIGroup: "coordination.k8s.io", Name: "node0", Namespace: "foo"},
213 expect: authorizer.DecisionNoOpinion,
214 },
215 {
216 name: "disallowed patch lease in namespace other than kube-node-lease - feature enabled",
217 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "patch", Resource: "leases", APIGroup: "coordination.k8s.io", Name: "node0", Namespace: "foo"},
218 expect: authorizer.DecisionNoOpinion,
219 },
220 {
221 name: "disallowed delete lease in namespace other than kube-node-lease - feature enabled",
222 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "delete", Resource: "leases", APIGroup: "coordination.k8s.io", Name: "node0", Namespace: "foo"},
223 expect: authorizer.DecisionNoOpinion,
224 },
225 {
226 name: "disallowed get another node's lease - feature enabled",
227 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "leases", APIGroup: "coordination.k8s.io", Name: "node1", Namespace: corev1.NamespaceNodeLease},
228 expect: authorizer.DecisionNoOpinion,
229 },
230 {
231 name: "disallowed update another node's lease - feature enabled",
232 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "update", Resource: "leases", APIGroup: "coordination.k8s.io", Name: "node1", Namespace: corev1.NamespaceNodeLease},
233 expect: authorizer.DecisionNoOpinion,
234 },
235 {
236 name: "disallowed patch another node's lease - feature enabled",
237 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "patch", Resource: "leases", APIGroup: "coordination.k8s.io", Name: "node1", Namespace: corev1.NamespaceNodeLease},
238 expect: authorizer.DecisionNoOpinion,
239 },
240 {
241 name: "disallowed delete another node's lease - feature enabled",
242 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "delete", Resource: "leases", APIGroup: "coordination.k8s.io", Name: "node1", Namespace: corev1.NamespaceNodeLease},
243 expect: authorizer.DecisionNoOpinion,
244 },
245 {
246 name: "disallowed list node leases - feature enabled",
247 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "leases", APIGroup: "coordination.k8s.io", Namespace: corev1.NamespaceNodeLease},
248 expect: authorizer.DecisionNoOpinion,
249 },
250 {
251 name: "disallowed watch node leases - feature enabled",
252 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "leases", APIGroup: "coordination.k8s.io", Namespace: corev1.NamespaceNodeLease},
253 expect: authorizer.DecisionNoOpinion,
254 },
255 {
256 name: "allowed get node lease - feature enabled",
257 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "leases", APIGroup: "coordination.k8s.io", Name: "node0", Namespace: corev1.NamespaceNodeLease},
258 expect: authorizer.DecisionAllow,
259 },
260 {
261 name: "allowed create node lease - feature enabled",
262 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "create", Resource: "leases", APIGroup: "coordination.k8s.io", Name: "node0", Namespace: corev1.NamespaceNodeLease},
263 expect: authorizer.DecisionAllow,
264 },
265 {
266 name: "allowed update node lease - feature enabled",
267 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "update", Resource: "leases", APIGroup: "coordination.k8s.io", Name: "node0", Namespace: corev1.NamespaceNodeLease},
268 expect: authorizer.DecisionAllow,
269 },
270 {
271 name: "allowed patch node lease - feature enabled",
272 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "patch", Resource: "leases", APIGroup: "coordination.k8s.io", Name: "node0", Namespace: corev1.NamespaceNodeLease},
273 expect: authorizer.DecisionAllow,
274 },
275 {
276 name: "allowed delete node lease - feature enabled",
277 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "delete", Resource: "leases", APIGroup: "coordination.k8s.io", Name: "node0", Namespace: corev1.NamespaceNodeLease},
278 expect: authorizer.DecisionAllow,
279 },
280
281 {
282 name: "disallowed CSINode with subresource - feature enabled",
283 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "csinodes", Subresource: "csiDrivers", APIGroup: "storage.k8s.io", Name: "node0"},
284 expect: authorizer.DecisionNoOpinion,
285 },
286 {
287 name: "disallowed get another node's CSINode",
288 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "csinodes", APIGroup: "storage.k8s.io", Name: "node1"},
289 expect: authorizer.DecisionNoOpinion,
290 },
291 {
292 name: "disallowed update another node's CSINode",
293 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "update", Resource: "csinodes", APIGroup: "storage.k8s.io", Name: "node1"},
294 expect: authorizer.DecisionNoOpinion,
295 },
296 {
297 name: "disallowed patch another node's CSINode",
298 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "patch", Resource: "csinodes", APIGroup: "storage.k8s.io", Name: "node1"},
299 expect: authorizer.DecisionNoOpinion,
300 },
301 {
302 name: "disallowed delete another node's CSINode",
303 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "delete", Resource: "csinodes", APIGroup: "storage.k8s.io", Name: "node1"},
304 expect: authorizer.DecisionNoOpinion,
305 },
306 {
307 name: "disallowed list CSINodes",
308 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "csinodes", APIGroup: "storage.k8s.io"},
309 expect: authorizer.DecisionNoOpinion,
310 },
311 {
312 name: "disallowed watch CSINodes",
313 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "csinodes", APIGroup: "storage.k8s.io"},
314 expect: authorizer.DecisionNoOpinion,
315 },
316 {
317 name: "allowed get CSINode",
318 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "csinodes", APIGroup: "storage.k8s.io", Name: "node0"},
319 expect: authorizer.DecisionAllow,
320 },
321 {
322 name: "allowed create CSINode",
323 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "create", Resource: "csinodes", APIGroup: "storage.k8s.io", Name: "node0"},
324 expect: authorizer.DecisionAllow,
325 },
326 {
327 name: "allowed update CSINode",
328 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "update", Resource: "csinodes", APIGroup: "storage.k8s.io", Name: "node0"},
329 expect: authorizer.DecisionAllow,
330 },
331 {
332 name: "allowed patch CSINode",
333 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "patch", Resource: "csinodes", APIGroup: "storage.k8s.io", Name: "node0"},
334 expect: authorizer.DecisionAllow,
335 },
336 {
337 name: "allowed delete CSINode",
338 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "delete", Resource: "csinodes", APIGroup: "storage.k8s.io", Name: "node0"},
339 expect: authorizer.DecisionAllow,
340 },
341
342 {
343 name: "disallowed ResourceSlice with subresource",
344 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "resourceslices", Subresource: "status", APIGroup: "resource.k8s.io", Name: "slice0-node0"},
345 expect: authorizer.DecisionNoOpinion,
346 },
347 {
348 name: "disallowed get another node's ResourceSlice",
349 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "resourceslices", APIGroup: "resource.k8s.io", Name: "slice0-node1"},
350 expect: authorizer.DecisionNoOpinion,
351 },
352 {
353 name: "disallowed update another node's ResourceSlice",
354 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "update", Resource: "resourceslices", APIGroup: "resource.k8s.io", Name: "slice0-node1"},
355 expect: authorizer.DecisionNoOpinion,
356 },
357 {
358 name: "disallowed patch another node's ResourceSlice",
359 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "patch", Resource: "resourceslices", APIGroup: "resource.k8s.io", Name: "slice0-node1"},
360 expect: authorizer.DecisionNoOpinion,
361 },
362 {
363 name: "disallowed delete another node's ResourceSlice",
364 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "delete", Resource: "resourceslices", APIGroup: "resource.k8s.io", Name: "slice0-node1"},
365 expect: authorizer.DecisionNoOpinion,
366 },
367 {
368 name: "allowed list ResourceSlices",
369 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "list", Resource: "resourceslices", APIGroup: "resource.k8s.io"},
370 expect: authorizer.DecisionAllow,
371 },
372 {
373 name: "allowed watch ResourceSlices",
374 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "watch", Resource: "resourceslices", APIGroup: "resource.k8s.io"},
375 expect: authorizer.DecisionAllow,
376 },
377 {
378 name: "allowed get ResourceSlice",
379 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "resourceslices", APIGroup: "resource.k8s.io", Name: "slice0-node0"},
380 expect: authorizer.DecisionAllow,
381 },
382 {
383 name: "allowed create ResourceSlice",
384 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "create", Resource: "resourceslices", APIGroup: "resource.k8s.io", Name: "slice0-node0"},
385 expect: authorizer.DecisionAllow,
386 },
387 {
388 name: "allowed update ResourceSlice",
389 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "update", Resource: "resourceslices", APIGroup: "resource.k8s.io", Name: "slice0-node0"},
390 expect: authorizer.DecisionAllow,
391 },
392 {
393 name: "allowed patch ResourceSlice",
394 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "patch", Resource: "resourceslices", APIGroup: "resource.k8s.io", Name: "slice0-node0"},
395 expect: authorizer.DecisionAllow,
396 },
397 {
398 name: "allowed delete ResourceSlice",
399 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "delete", Resource: "resourceslices", APIGroup: "resource.k8s.io", Name: "slice0-node0"},
400 expect: authorizer.DecisionAllow,
401 },
402 }
403
404 for _, tc := range tests {
405 t.Run(tc.name, func(t *testing.T) {
406 if tc.features == nil {
407 authz.features = utilfeature.DefaultFeatureGate
408 } else {
409 authz.features = tc.features
410 }
411 decision, _, _ := authz.Authorize(context.Background(), tc.attrs)
412 if decision != tc.expect {
413 t.Errorf("expected %v, got %v", tc.expect, decision)
414 }
415 })
416 }
417 }
418
419 func TestAuthorizerSharedResources(t *testing.T) {
420 g := NewGraph()
421 g.destinationEdgeThreshold = 1
422 identifier := nodeidentifier.NewDefaultNodeIdentifier()
423 authz := NewAuthorizer(g, identifier, bootstrappolicy.NodeRules())
424
425 node1 := &user.DefaultInfo{Name: "system:node:node1", Groups: []string{"system:nodes"}}
426 node2 := &user.DefaultInfo{Name: "system:node:node2", Groups: []string{"system:nodes"}}
427 node3 := &user.DefaultInfo{Name: "system:node:node3", Groups: []string{"system:nodes"}}
428
429 g.AddPod(&corev1.Pod{
430 ObjectMeta: metav1.ObjectMeta{Name: "pod1-node1", Namespace: "ns1"},
431 Spec: corev1.PodSpec{
432 NodeName: "node1",
433 Volumes: []corev1.Volume{
434 {VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: "node1-only"}}},
435 {VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: "node1-node2-only"}}},
436 {VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: "shared-all"}}},
437 },
438 },
439 })
440 g.AddPod(&corev1.Pod{
441 ObjectMeta: metav1.ObjectMeta{Name: "pod2-node2", Namespace: "ns1"},
442 Spec: corev1.PodSpec{
443 NodeName: "node2",
444 Volumes: []corev1.Volume{
445 {VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: "node1-node2-only"}}},
446 {VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: "shared-all"}}},
447 },
448 },
449 })
450
451 pod3 := &corev1.Pod{
452 ObjectMeta: metav1.ObjectMeta{Name: "pod3-node3", Namespace: "ns1"},
453 Spec: corev1.PodSpec{
454 NodeName: "node3",
455 Volumes: []corev1.Volume{
456 {VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: "shared-all"}}},
457 },
458 },
459 }
460 g.AddPod(pod3)
461
462 testcases := []struct {
463 User user.Info
464 Secret string
465 ConfigMap string
466 Decision authorizer.Decision
467 }{
468 {User: node1, Decision: authorizer.DecisionAllow, Secret: "node1-only"},
469 {User: node1, Decision: authorizer.DecisionAllow, Secret: "node1-node2-only"},
470 {User: node1, Decision: authorizer.DecisionAllow, Secret: "shared-all"},
471
472 {User: node2, Decision: authorizer.DecisionNoOpinion, Secret: "node1-only"},
473 {User: node2, Decision: authorizer.DecisionAllow, Secret: "node1-node2-only"},
474 {User: node2, Decision: authorizer.DecisionAllow, Secret: "shared-all"},
475
476 {User: node3, Decision: authorizer.DecisionNoOpinion, Secret: "node1-only"},
477 {User: node3, Decision: authorizer.DecisionNoOpinion, Secret: "node1-node2-only"},
478 {User: node3, Decision: authorizer.DecisionAllow, Secret: "shared-all"},
479 }
480
481 for i, tc := range testcases {
482 var (
483 decision authorizer.Decision
484 err error
485 )
486
487 if len(tc.Secret) > 0 {
488 decision, _, err = authz.Authorize(context.Background(), authorizer.AttributesRecord{User: tc.User, ResourceRequest: true, Verb: "get", Resource: "secrets", Namespace: "ns1", Name: tc.Secret})
489 if err != nil {
490 t.Errorf("%d: unexpected error: %v", i, err)
491 continue
492 }
493 } else if len(tc.ConfigMap) > 0 {
494 decision, _, err = authz.Authorize(context.Background(), authorizer.AttributesRecord{User: tc.User, ResourceRequest: true, Verb: "get", Resource: "configmaps", Namespace: "ns1", Name: tc.ConfigMap})
495 if err != nil {
496 t.Errorf("%d: unexpected error: %v", i, err)
497 continue
498 }
499 } else {
500 t.Fatalf("test case must include a request for a Secret or ConfigMap")
501 }
502
503 if decision != tc.Decision {
504 t.Errorf("%d: expected %v, got %v", i, tc.Decision, decision)
505 }
506 }
507
508 {
509 node3SharedSecretGet := authorizer.AttributesRecord{User: node3, ResourceRequest: true, Verb: "get", Resource: "secrets", Namespace: "ns1", Name: "shared-all"}
510
511 decision, _, err := authz.Authorize(context.Background(), node3SharedSecretGet)
512 if err != nil {
513 t.Errorf("unexpected error: %v", err)
514 }
515 if decision != authorizer.DecisionAllow {
516 t.Error("expected allowed")
517 }
518
519
520 pod3.Spec.Volumes = nil
521 g.AddPod(pod3)
522
523 decision, _, err = authz.Authorize(context.Background(), node3SharedSecretGet)
524 if err != nil {
525 t.Errorf("unexpected error: %v", err)
526 }
527 if decision == authorizer.DecisionAllow {
528 t.Errorf("unexpectedly allowed")
529 }
530 }
531 }
532
533 type sampleDataOpts struct {
534 nodes int
535 namespaces int
536 podsPerNode int
537
538 attachmentsPerNode int
539
540
541
542
543 sharedConfigMapsPerNamespace int
544
545
546
547 sharedSecretsPerNamespace int
548
549
550
551 sharedPVCsPerNamespace int
552
553 sharedConfigMapsPerPod int
554 sharedSecretsPerPod int
555 sharedPVCsPerPod int
556
557 uniqueSecretsPerPod int
558 uniqueConfigMapsPerPod int
559 uniquePVCsPerPod int
560 uniqueResourceClaimsPerPod int
561 uniqueResourceClaimTemplatesPerPod int
562 uniqueResourceClaimTemplatesWithClaimPerPod int
563
564 nodeResourceCapacitiesPerNode int
565 }
566
567 func BenchmarkPopulationAllocation(b *testing.B) {
568 opts := &sampleDataOpts{
569 nodes: 500,
570 namespaces: 200,
571 podsPerNode: 200,
572 attachmentsPerNode: 20,
573 sharedConfigMapsPerPod: 0,
574 uniqueConfigMapsPerPod: 1,
575 sharedSecretsPerPod: 1,
576 uniqueSecretsPerPod: 1,
577 sharedPVCsPerPod: 0,
578 uniquePVCsPerPod: 1,
579 }
580
581 nodes, pods, pvs, attachments, slices := generate(opts)
582 b.ResetTimer()
583
584 for i := 0; i < b.N; i++ {
585 g := NewGraph()
586 populate(g, nodes, pods, pvs, attachments, slices)
587 }
588 }
589
590 func BenchmarkPopulationRetention(b *testing.B) {
591
592
593
594
595
596
597
598
599 opts := &sampleDataOpts{
600 nodes: 500,
601 namespaces: 200,
602 podsPerNode: 200,
603 attachmentsPerNode: 20,
604 sharedConfigMapsPerPod: 0,
605 uniqueConfigMapsPerPod: 1,
606 sharedSecretsPerPod: 1,
607 uniqueSecretsPerPod: 1,
608 sharedPVCsPerPod: 0,
609 uniquePVCsPerPod: 1,
610 }
611
612 nodes, pods, pvs, attachments, slices := generate(opts)
613
614 runtime.GC()
615 b.ResetTimer()
616
617 for i := 0; i < b.N; i++ {
618 g := NewGraph()
619 populate(g, nodes, pods, pvs, attachments, slices)
620
621 if i == 0 {
622 f, _ := os.Create("BenchmarkPopulationRetention.profile")
623 runtime.GC()
624 pprof.WriteHeapProfile(f)
625 f.Close()
626
627 _ = fmt.Sprintf("%T\n", g)
628 }
629 }
630 }
631
632 func BenchmarkWriteIndexMaintenance(b *testing.B) {
633
634
635
636
637 opts := &sampleDataOpts{
638
639 nodes: 5000,
640 namespaces: 1,
641 podsPerNode: 1,
642 attachmentsPerNode: 20,
643 sharedConfigMapsPerPod: 0,
644 uniqueConfigMapsPerPod: 1,
645 sharedSecretsPerPod: 1,
646 uniqueSecretsPerPod: 1,
647 sharedPVCsPerPod: 0,
648 uniquePVCsPerPod: 1,
649 }
650 nodes, pods, pvs, attachments, slices := generate(opts)
651 g := NewGraph()
652 populate(g, nodes, pods, pvs, attachments, slices)
653
654 runtime.GC()
655 b.ResetTimer()
656
657 b.SetParallelism(100)
658 b.RunParallel(func(pb *testing.PB) {
659 for pb.Next() {
660 g.AddPod(pods[0])
661 }
662 })
663 }
664
665 func BenchmarkAuthorization(b *testing.B) {
666 g := NewGraph()
667
668 opts := &sampleDataOpts{
669
670
671
672
673 nodes: 500,
674 namespaces: 200,
675 podsPerNode: 200,
676 attachmentsPerNode: 20,
677 sharedConfigMapsPerPod: 0,
678 uniqueConfigMapsPerPod: 1,
679 sharedSecretsPerPod: 1,
680 uniqueSecretsPerPod: 1,
681 sharedPVCsPerPod: 0,
682 uniquePVCsPerPod: 1,
683 }
684 nodes, pods, pvs, attachments, slices := generate(opts)
685 populate(g, nodes, pods, pvs, attachments, slices)
686
687 identifier := nodeidentifier.NewDefaultNodeIdentifier()
688 authz := NewAuthorizer(g, identifier, bootstrappolicy.NodeRules())
689
690 node0 := &user.DefaultInfo{Name: "system:node:node0", Groups: []string{"system:nodes"}}
691
692 tests := []struct {
693 name string
694 attrs authorizer.AttributesRecord
695 expect authorizer.Decision
696 features featuregate.FeatureGate
697 }{
698 {
699 name: "allowed configmap",
700 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "configmaps", Name: "configmap0-pod0-node0", Namespace: "ns0"},
701 expect: authorizer.DecisionAllow,
702 },
703 {
704 name: "allowed secret via pod",
705 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "secrets", Name: "secret0-pod0-node0", Namespace: "ns0"},
706 expect: authorizer.DecisionAllow,
707 },
708 {
709 name: "allowed shared secret via pod",
710 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "secrets", Name: "secret0-shared", Namespace: "ns0"},
711 expect: authorizer.DecisionAllow,
712 },
713 {
714 name: "disallowed configmap",
715 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "configmaps", Name: "configmap0-pod0-node1", Namespace: "ns0"},
716 expect: authorizer.DecisionNoOpinion,
717 },
718 {
719 name: "disallowed secret via pod",
720 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "secrets", Name: "secret0-pod0-node1", Namespace: "ns0"},
721 expect: authorizer.DecisionNoOpinion,
722 },
723 {
724 name: "disallowed shared secret via pvc",
725 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "secrets", Name: "secret-pv0-pod0-node1-ns0", Namespace: "ns0"},
726 expect: authorizer.DecisionNoOpinion,
727 },
728 {
729 name: "disallowed pvc",
730 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "persistentvolumeclaims", Name: "pvc0-pod0-node1", Namespace: "ns0"},
731 expect: authorizer.DecisionNoOpinion,
732 },
733 {
734 name: "disallowed pv",
735 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "persistentvolumes", Name: "pv0-pod0-node1-ns0", Namespace: ""},
736 expect: authorizer.DecisionNoOpinion,
737 },
738 {
739 name: "disallowed attachment - no relationship",
740 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "volumeattachments", APIGroup: "storage.k8s.io", Name: "attachment0-node1"},
741 expect: authorizer.DecisionNoOpinion,
742 },
743 {
744 name: "allowed attachment",
745 attrs: authorizer.AttributesRecord{User: node0, ResourceRequest: true, Verb: "get", Resource: "volumeattachments", APIGroup: "storage.k8s.io", Name: "attachment0-node0"},
746 expect: authorizer.DecisionAllow,
747 },
748 }
749
750 podToAdd, _ := generatePod("testwrite", "ns0", "node0", "default", opts)
751
752 b.ResetTimer()
753 for _, testWriteContention := range []bool{false, true} {
754
755 shouldWrite := int32(1)
756 writes := int64(0)
757 _1ms := int64(0)
758 _10ms := int64(0)
759 _25ms := int64(0)
760 _50ms := int64(0)
761 _100ms := int64(0)
762 _250ms := int64(0)
763 _500ms := int64(0)
764 _1000ms := int64(0)
765 _1s := int64(0)
766
767 contentionPrefix := ""
768 if testWriteContention {
769 contentionPrefix = "contentious "
770
771 go func() {
772 for shouldWrite == 1 {
773 go func() {
774 start := time.Now()
775 authz.graph.AddPod(podToAdd)
776 diff := time.Since(start)
777 atomic.AddInt64(&writes, 1)
778 switch {
779 case diff < time.Millisecond:
780 atomic.AddInt64(&_1ms, 1)
781 case diff < 10*time.Millisecond:
782 atomic.AddInt64(&_10ms, 1)
783 case diff < 25*time.Millisecond:
784 atomic.AddInt64(&_25ms, 1)
785 case diff < 50*time.Millisecond:
786 atomic.AddInt64(&_50ms, 1)
787 case diff < 100*time.Millisecond:
788 atomic.AddInt64(&_100ms, 1)
789 case diff < 250*time.Millisecond:
790 atomic.AddInt64(&_250ms, 1)
791 case diff < 500*time.Millisecond:
792 atomic.AddInt64(&_500ms, 1)
793 case diff < 1000*time.Millisecond:
794 atomic.AddInt64(&_1000ms, 1)
795 default:
796 atomic.AddInt64(&_1s, 1)
797 }
798 }()
799 time.Sleep(10 * time.Millisecond)
800 }
801 }()
802 }
803
804 for _, tc := range tests {
805 if tc.features == nil {
806 authz.features = utilfeature.DefaultFeatureGate
807 } else {
808 authz.features = tc.features
809 }
810 b.Run(contentionPrefix+tc.name, func(b *testing.B) {
811
812 b.SetParallelism(5000)
813 b.RunParallel(func(pb *testing.PB) {
814 for pb.Next() {
815 decision, _, _ := authz.Authorize(context.Background(), tc.attrs)
816 if decision != tc.expect {
817 b.Errorf("expected %v, got %v", tc.expect, decision)
818 }
819 }
820 })
821 })
822 }
823
824 atomic.StoreInt32(&shouldWrite, 0)
825 if testWriteContention {
826 b.Logf("graph modifications during contention test: %d", writes)
827 b.Logf("<1ms=%d, <10ms=%d, <25ms=%d, <50ms=%d, <100ms=%d, <250ms=%d, <500ms=%d, <1000ms=%d, >1000ms=%d", _1ms, _10ms, _25ms, _50ms, _100ms, _250ms, _500ms, _1000ms, _1s)
828 } else {
829 b.Logf("graph modifications during non-contention test: %d", writes)
830 }
831 }
832 }
833
834 func populate(graph *Graph, nodes []*corev1.Node, pods []*corev1.Pod, pvs []*corev1.PersistentVolume, attachments []*storagev1.VolumeAttachment, slices []*resourcev1alpha2.ResourceSlice) {
835 p := &graphPopulator{}
836 p.graph = graph
837 for _, pod := range pods {
838 p.addPod(pod)
839 }
840 for _, pv := range pvs {
841 p.addPV(pv)
842 }
843 for _, attachment := range attachments {
844 p.addVolumeAttachment(attachment)
845 }
846 for _, slice := range slices {
847 p.addResourceSlice(slice)
848 }
849 }
850
851 func randomSubset(a, b int) []int {
852 if b < a {
853 b = a
854 }
855 return rand.Perm(b)[:a]
856 }
857
858
859
860
861
862 func generate(opts *sampleDataOpts) ([]*corev1.Node, []*corev1.Pod, []*corev1.PersistentVolume, []*storagev1.VolumeAttachment, []*resourcev1alpha2.ResourceSlice) {
863 nodes := make([]*corev1.Node, 0, opts.nodes)
864 pods := make([]*corev1.Pod, 0, opts.nodes*opts.podsPerNode)
865 pvs := make([]*corev1.PersistentVolume, 0, (opts.nodes*opts.podsPerNode*opts.uniquePVCsPerPod)+(opts.sharedPVCsPerPod*opts.namespaces))
866 attachments := make([]*storagev1.VolumeAttachment, 0, opts.nodes*opts.attachmentsPerNode)
867 slices := make([]*resourcev1alpha2.ResourceSlice, 0, opts.nodes*opts.nodeResourceCapacitiesPerNode)
868
869 rand.Seed(12345)
870
871 for n := 0; n < opts.nodes; n++ {
872 nodeName := fmt.Sprintf("node%d", n)
873 for p := 0; p < opts.podsPerNode; p++ {
874 name := fmt.Sprintf("pod%d-%s", p, nodeName)
875 namespace := fmt.Sprintf("ns%d", p%opts.namespaces)
876 svcAccountName := fmt.Sprintf("svcacct%d-%s", p, nodeName)
877
878 pod, podPVs := generatePod(name, namespace, nodeName, svcAccountName, opts)
879 pods = append(pods, pod)
880 pvs = append(pvs, podPVs...)
881 }
882 for a := 0; a < opts.attachmentsPerNode; a++ {
883 attachment := &storagev1.VolumeAttachment{}
884 attachment.Name = fmt.Sprintf("attachment%d-%s", a, nodeName)
885 attachment.Spec.NodeName = nodeName
886 attachments = append(attachments, attachment)
887 }
888
889 nodes = append(nodes, &corev1.Node{
890 ObjectMeta: metav1.ObjectMeta{Name: nodeName},
891 Spec: corev1.NodeSpec{},
892 })
893
894 for p := 0; p <= opts.nodeResourceCapacitiesPerNode; p++ {
895 name := fmt.Sprintf("slice%d-%s", p, nodeName)
896 slice := &resourcev1alpha2.ResourceSlice{
897 ObjectMeta: metav1.ObjectMeta{Name: name},
898 NodeName: nodeName,
899 }
900 slices = append(slices, slice)
901 }
902 }
903 return nodes, pods, pvs, attachments, slices
904 }
905
906 func generatePod(name, namespace, nodeName, svcAccountName string, opts *sampleDataOpts) (*corev1.Pod, []*corev1.PersistentVolume) {
907 pvs := make([]*corev1.PersistentVolume, 0, opts.uniquePVCsPerPod+opts.sharedPVCsPerPod)
908
909 pod := &corev1.Pod{}
910 pod.Name = name
911 pod.Namespace = namespace
912 pod.Spec.NodeName = nodeName
913 pod.Spec.ServiceAccountName = svcAccountName
914
915 for i := 0; i < opts.uniqueSecretsPerPod; i++ {
916 pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{VolumeSource: corev1.VolumeSource{
917 Secret: &corev1.SecretVolumeSource{SecretName: fmt.Sprintf("secret%d-%s", i, pod.Name)},
918 }})
919 }
920
921 subset := randomSubset(opts.sharedSecretsPerPod, opts.sharedSecretsPerNamespace)
922 for _, i := range subset {
923 pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{VolumeSource: corev1.VolumeSource{
924 Secret: &corev1.SecretVolumeSource{SecretName: fmt.Sprintf("secret%d-shared", i)},
925 }})
926 }
927
928 for i := 0; i < opts.uniqueConfigMapsPerPod; i++ {
929 pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{VolumeSource: corev1.VolumeSource{
930 ConfigMap: &corev1.ConfigMapVolumeSource{LocalObjectReference: corev1.LocalObjectReference{Name: fmt.Sprintf("configmap%d-%s", i, pod.Name)}},
931 }})
932 }
933
934 subset = randomSubset(opts.sharedConfigMapsPerPod, opts.sharedConfigMapsPerNamespace)
935 for _, i := range subset {
936 pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{VolumeSource: corev1.VolumeSource{
937 ConfigMap: &corev1.ConfigMapVolumeSource{LocalObjectReference: corev1.LocalObjectReference{Name: fmt.Sprintf("configmap%d-shared", i)}},
938 }})
939 }
940
941 for i := 0; i < opts.uniquePVCsPerPod; i++ {
942 pv := &corev1.PersistentVolume{}
943 pv.Name = fmt.Sprintf("pv%d-%s-%s", i, pod.Name, pod.Namespace)
944 pv.Spec.FlexVolume = &corev1.FlexPersistentVolumeSource{SecretRef: &corev1.SecretReference{Name: fmt.Sprintf("secret-%s", pv.Name)}}
945 pv.Spec.ClaimRef = &corev1.ObjectReference{Name: fmt.Sprintf("pvc%d-%s", i, pod.Name), Namespace: pod.Namespace}
946 pvs = append(pvs, pv)
947
948 pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{VolumeSource: corev1.VolumeSource{
949 PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ClaimName: pv.Spec.ClaimRef.Name},
950 }})
951 }
952 for i := 0; i < opts.uniqueResourceClaimsPerPod; i++ {
953 claimName := fmt.Sprintf("claim%d-%s-%s", i, pod.Name, pod.Namespace)
954 pod.Spec.ResourceClaims = append(pod.Spec.ResourceClaims, corev1.PodResourceClaim{
955 Name: fmt.Sprintf("claim%d", i),
956 Source: corev1.ClaimSource{
957 ResourceClaimName: &claimName,
958 },
959 })
960 }
961 for i := 0; i < opts.uniqueResourceClaimTemplatesPerPod; i++ {
962 claimTemplateName := fmt.Sprintf("claimtemplate%d-%s-%s", i, pod.Name, pod.Namespace)
963 podClaimName := fmt.Sprintf("claimtemplate%d", i)
964 pod.Spec.ResourceClaims = append(pod.Spec.ResourceClaims, corev1.PodResourceClaim{
965 Name: podClaimName,
966 Source: corev1.ClaimSource{
967 ResourceClaimTemplateName: &claimTemplateName,
968 },
969 })
970 }
971 for i := 0; i < opts.uniqueResourceClaimTemplatesWithClaimPerPod; i++ {
972 claimTemplateName := fmt.Sprintf("claimtemplate%d-%s-%s", i, pod.Name, pod.Namespace)
973 podClaimName := fmt.Sprintf("claimtemplate-with-claim%d", i)
974 claimName := fmt.Sprintf("generated-claim-%s-%s-%d", pod.Name, pod.Namespace, i)
975 pod.Spec.ResourceClaims = append(pod.Spec.ResourceClaims, corev1.PodResourceClaim{
976 Name: podClaimName,
977 Source: corev1.ClaimSource{
978 ResourceClaimTemplateName: &claimTemplateName,
979 },
980 })
981 pod.Status.ResourceClaimStatuses = append(pod.Status.ResourceClaimStatuses, corev1.PodResourceClaimStatus{
982 Name: podClaimName,
983 ResourceClaimName: &claimName,
984 })
985 }
986
987 subset = randomSubset(opts.sharedPVCsPerPod, opts.sharedPVCsPerNamespace)
988 for _, i := range subset {
989 pv := &corev1.PersistentVolume{}
990 pv.Name = fmt.Sprintf("pv%d-shared-%s", i, pod.Namespace)
991 pv.Spec.FlexVolume = &corev1.FlexPersistentVolumeSource{SecretRef: &corev1.SecretReference{Name: fmt.Sprintf("secret-%s", pv.Name)}}
992 pv.Spec.ClaimRef = &corev1.ObjectReference{Name: fmt.Sprintf("pvc%d-shared", i), Namespace: pod.Namespace}
993 pvs = append(pvs, pv)
994
995 pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{VolumeSource: corev1.VolumeSource{
996 PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ClaimName: pv.Spec.ClaimRef.Name},
997 }})
998 }
999
1000 return pod, pvs
1001 }
1002
View as plain text