Compare commits
825 Commits
Author | SHA1 | Date |
---|---|---|
|
6016cdfb88 | |
|
6dc2a56d3a | |
|
dda2fcb797 | |
|
34f5472b35 | |
|
532260d25f | |
|
0a000a99f6 | |
|
e824ccb0fa | |
|
2f077fcbca | |
|
d7827fc7e8 | |
|
32cbb819fe | |
|
bf8c1a6d90 | |
|
c42cceba12 | |
|
d972fefbb0 | |
|
8cd1907cc9 | |
|
5e1c1db1da | |
|
03b538a8e8 | |
|
d0ee71ec17 | |
|
9b1ef94042 | |
|
f482a77223 | |
|
7364e005dc | |
|
d2a696650a | |
|
ab47d6f39c | |
|
c93fe8d40f | |
|
2898f40277 | |
|
3637a3a5aa | |
|
179348d538 | |
|
6bc5ac2a1a | |
|
b6975dfbe9 | |
|
61137484d0 | |
|
e26c3d7e23 | |
|
7710d9ff3e | |
|
08b1bbe4b9 | |
|
ff84ca3520 | |
|
2c9f20fd49 | |
|
3565462805 | |
|
09be259d76 | |
|
ee98ed2094 | |
|
6874062f41 | |
|
06b7bf2af7 | |
|
af5056a3db | |
|
8c685320dc | |
|
895f2b3947 | |
|
2ca55b4189 | |
|
67c4a8a9ee | |
|
df850b5a51 | |
|
b805c4d851 | |
|
19608fe81a | |
|
cf27dab482 | |
|
eaf6e48501 | |
|
07ec5c5838 | |
|
662ba26adb | |
|
a77ab41bc0 | |
|
1a56a2f375 | |
|
8ee91f3b4d | |
|
942549e2f9 | |
|
0d3a31d996 | |
|
3f2a0b72ba | |
|
75e69719a1 | |
|
389ff851fc | |
|
bab2fbdc6f | |
|
3d4aed0ded | |
|
0e36545f56 | |
|
a47cc30656 | |
|
0dd61ee79d | |
|
baef40d3ec | |
|
d16c916aad | |
|
fdccb8b2dc | |
|
7a9026cb3a | |
|
dd8cd79556 | |
|
d76c79a4e9 | |
|
3b43a1a7f8 | |
|
8162f5ea9d | |
|
deb30e35ad | |
|
9b695a5efa | |
|
6db9ead72a | |
|
1f47d05362 | |
|
081f25136a | |
|
89f1e0c9f7 | |
|
0e38ecf83f | |
|
a2f3ed4733 | |
|
e4c9db3825 | |
|
6bf08e15b6 | |
|
d9e31a74fe | |
|
65bd2fadfd | |
|
f294209f1e | |
|
dc0ee6eda8 | |
|
8aa4209508 | |
|
80cef81846 | |
|
c5cc58d0de | |
|
f8343759d9 | |
|
6b794dbd82 | |
|
8a7311dce3 | |
|
d9b6bd1d4c | |
|
ed9f8fce83 | |
|
f55aaff838 | |
|
eadbc19007 | |
|
13e6a3c329 | |
|
9388b76b21 | |
|
7cd49caffc | |
|
92c5638afd | |
|
2c9afbf9bc | |
|
053a8b2a4c | |
|
7d6aa8a395 | |
|
02450b6f98 | |
|
851e917827 | |
|
99e4fe9de1 | |
|
b50caf5d66 | |
|
73e127faf5 | |
|
255fa69bd1 | |
|
784d2892e6 | |
|
4b98f13de7 | |
|
b887c9ebec | |
|
7937de32ff | |
|
08959fe41f | |
|
2055ba24cd | |
|
df39bcd7dd | |
|
73a9ddd3c3 | |
|
d6a8a1af27 | |
|
2f8595c47b | |
|
5009571787 | |
|
a559cb8be1 | |
|
569dd0421b | |
|
062128c843 | |
|
2ce736f1ce | |
|
12fab5de10 | |
|
6764e31cac | |
|
9333a5f386 | |
|
80961690c3 | |
|
067a2139bb | |
|
057e0d26a0 | |
|
e80da78881 | |
|
9e1714f200 | |
|
bc489d7b1b | |
|
b45c4547ed | |
|
bda7e28bbb | |
|
9b509bf53b | |
|
2ffdf9039f | |
|
f2b80d742a | |
|
1ffdd2403f | |
|
2bc51918fe | |
|
46dd96ca03 | |
|
1527793495 | |
|
93c8ec73c8 | |
|
5f14a1e225 | |
|
007464aa0f | |
|
97250949cd | |
|
724cb010da | |
|
11e35f7e67 | |
|
71463feb30 | |
|
62514be921 | |
|
7e75755a5a | |
|
b6cfd80a0d | |
|
32e916b79d | |
|
1552f6cea3 | |
|
61451f57b1 | |
|
0de899d1ed | |
|
fd89bf895d | |
|
2bff744497 | |
|
8cf49c7c86 | |
|
8285a27f7b | |
|
0f91510ab0 | |
|
23913f0bf4 | |
|
eeced267ad | |
|
d8f5cf79ad | |
|
3dae57efb5 | |
|
73b2a2235b | |
|
f2b320dc52 | |
|
c199f9d392 | |
|
0d11839195 | |
|
b5e431bd82 | |
|
d22318d3d2 | |
|
8776678b52 | |
|
cb5a7a865d | |
|
a18370ff46 | |
|
de95d3511f | |
|
847f0cb7d3 | |
|
dbbb6a075e | |
|
f52dfa0eb4 | |
|
a50d13fe86 | |
|
201e7749f2 | |
|
3a062dc7cb | |
|
4a106441d6 | |
|
fc69168d19 | |
|
f541759309 | |
|
85b0cb4ae1 | |
|
42db0bf60a | |
|
fe06bdc873 | |
|
cab2303b49 | |
|
c160237b46 | |
|
492c873a80 | |
|
6da3566330 | |
|
213eed6ea1 | |
|
82f6fe39b0 | |
|
8181c85b8b | |
|
056896e970 | |
|
f5da7d2a32 | |
|
4c858d18eb | |
|
03ddc411f7 | |
|
2a113dbe6b | |
|
e87c9dbf8f | |
|
cd728bc91b | |
|
5a4ec0a797 | |
|
d4f2fc56b5 | |
|
beaef1d3ec | |
|
1776f0c3f2 | |
|
e228aeaf39 | |
|
29a5d82129 | |
|
30b60eb0a6 | |
|
aed144f141 | |
|
51e9232644 | |
|
4f42467c04 | |
|
b9e86eb851 | |
|
8ff7962ff8 | |
|
0710a6fd0a | |
|
81c5d13c4d | |
|
26bd744afc | |
|
37736ec2c1 | |
|
b1b5987c22 | |
|
6811fdeb2d | |
|
b1020cee14 | |
|
3a206e1457 | |
|
a11f874f2c | |
|
b94acad08e | |
|
8702b38eb1 | |
|
7d24a9eb1b | |
|
23be1a4749 | |
|
dfc7cb5fe1 | |
|
a581683e3c | |
|
fa558b198a | |
|
91724cfd41 | |
|
edd1d1f6ba | |
|
1a83f0ce07 | |
|
2566cd2659 | |
|
7db8aee1f8 | |
|
f50e0ed3bf | |
|
98865c84e6 | |
|
0f51ac53c8 | |
|
50f48ce4b3 | |
|
52e218fc61 | |
|
3482808f92 | |
|
aac1558d35 | |
|
3a95207b18 | |
|
67dfc24d13 | |
|
4da0062093 | |
|
d750e2f2bf | |
|
ba3fbbb53e | |
|
fa03ee3ea0 | |
|
544ce3df68 | |
|
cc5ef43352 | |
|
5df5358ff9 | |
|
2265efa65a | |
|
8aa636f1b5 | |
|
c59961a007 | |
|
05ed2e066d | |
|
b6404642e4 | |
|
3a2e1b53e5 | |
|
6ace22f694 | |
|
6f6da8e97b | |
|
a67992576e | |
|
84cc815637 | |
|
70820c9c98 | |
|
a91b0013af | |
|
543428d1c4 | |
|
d88392f1f6 | |
|
c31bd9905e | |
|
cba5376d4f | |
|
79feac10d8 | |
|
7b991eb8ee | |
|
24d512c91f | |
|
8e8b957b64 | |
|
6056c27674 | |
|
36c1a58d7e | |
|
ae901d5b33 | |
|
96b9726a3c | |
|
4fe686a430 | |
|
3cb2448d98 | |
|
6ed423348f | |
|
1623f6691b | |
|
3832c300e8 | |
|
7af25044da | |
|
1a6db11afb | |
|
9ff9bbbc47 | |
|
b2bc62b37f | |
|
67d2550df7 | |
|
a2c8b5531e | |
|
408f50382f | |
|
dccab55151 | |
|
7a33f524c6 | |
|
855c0d44c8 | |
|
92171c8c10 | |
|
782d90765e | |
|
73e72d16c5 | |
|
b8750e7396 | |
|
97b6cb1aeb | |
|
2b4f068bdb | |
|
4c88a5c5ae | |
|
f76f81aa6f | |
|
7ee837dd68 | |
|
d44a862c51 | |
|
bd0e774416 | |
|
01307e4b8d | |
|
23e9b2c9d4 | |
|
1b0427a576 | |
|
9641d30242 | |
|
c2e94ca503 | |
|
e36558fb20 | |
|
7377dfa3e5 | |
|
337542d65b | |
|
4305a14262 | |
|
d3e44dff6c | |
|
c5b8df417a | |
|
163865fa38 | |
|
0b0eaa35e1 | |
|
b12d7a1290 | |
|
5edb11aa43 | |
|
821b679880 | |
|
79ec45326d | |
|
345ad05c0c | |
|
7c0ba21066 | |
|
70fe4e2735 | |
|
9b3bebdbac | |
|
990bed7c73 | |
|
6a65641968 | |
|
f2b1ab6bbc | |
|
69cfb424ed | |
|
e1e2202f0a | |
|
205c0f56b5 | |
|
64791740ae | |
|
82c8af5dbe | |
|
e03f7e4910 | |
|
9e523945a2 | |
|
585eff559f | |
|
6faeace534 | |
|
3ad93853da | |
|
2ebe70a0a7 | |
|
0230ffb631 | |
|
a4587170bd | |
|
a64613cb08 | |
|
ed3efcd313 | |
|
1e7b28d116 | |
|
fcfce5aef7 | |
|
4a787e9b76 | |
|
710763dd43 | |
|
8dccd76acd | |
|
a3d85c236b | |
|
95d27c97c9 | |
|
afb79f83b3 | |
|
efe7a1b26d | |
|
c576a626de | |
|
09c0e7d0cd | |
|
d522031d97 | |
|
f422062046 | |
|
4831145b6d | |
|
10c2760fab | |
|
13a27b8dd7 | |
|
942f114031 | |
|
b6717882da | |
|
3a5523026c | |
|
644f8e67b2 | |
|
476d81d4ba | |
|
b9bc02fc91 | |
|
b9f760c579 | |
|
9da16fa551 | |
|
f98455cb25 | |
|
8dd4460107 | |
|
1e6e6f0a46 | |
|
b6fda29776 | |
|
938d466e86 | |
|
ff5a72e86f | |
|
52dde339a1 | |
|
35a4ee03bf | |
|
11288ef6c1 | |
|
2a04234bdc | |
|
08a05f9af7 | |
|
87cccf7779 | |
|
c4717788c4 | |
|
c533eff8e7 | |
|
4628bb89a6 | |
|
9bb5fd51d1 | |
|
7b8dc61f0b | |
|
0198fdbe95 | |
|
a9904eef9e | |
|
5a72bc815b | |
|
2ce508cc43 | |
|
69f7857a7a | |
|
c1a2d5992c | |
|
0167eb5d20 | |
|
66f788143e | |
|
55da1afddb | |
|
39a73cd3bd | |
|
3b2a820e45 | |
|
23e8a6cc2f | |
|
52f2fda3f7 | |
|
8a149c9296 | |
|
7e0cbb5924 | |
|
6c0d90b944 | |
|
2911f5b534 | |
|
0a703e3517 | |
|
ee59411d80 | |
|
f6d3d889a6 | |
|
a3f27ceda4 | |
|
e35b5a7595 | |
|
cf7237e44d | |
|
736476d22b | |
|
a2ab8dc0e3 | |
|
6e7ca2c0dc | |
|
6885e995e1 | |
|
0e1bb963f7 | |
|
b66aedfe5c | |
|
626adbf67c | |
|
ae92d91104 | |
|
2387b5d4a9 | |
|
2c1a1fa4ee | |
|
695a3a4c4e | |
|
e802cf6daf | |
|
cd99eadfc3 | |
|
37d4665f53 | |
|
5f060d93ca | |
|
7ca4de7334 | |
|
76f1672b11 | |
|
3a132bfa52 | |
|
30363fdd34 | |
|
5edcd3c1b7 | |
|
e62b626c0f | |
|
6c470468a0 | |
|
a6c0914bb9 | |
|
29de537bfa | |
|
a4a670721b | |
|
74be087390 | |
|
b43177846d | |
|
a2cdf2cd0f | |
|
2241ea0052 | |
|
3c47ed7b1d | |
|
9bb4aa730a | |
|
67ec836891 | |
|
ca7102a0c4 | |
|
38b01a1f78 | |
|
8161d73803 | |
|
c7ea66111e | |
|
893803dc85 | |
|
2cad252f0c | |
|
e6eb58901f | |
|
006dd593ee | |
|
7c924dc3c8 | |
|
825f36b139 | |
|
62c90b1dff | |
|
84b3c4c640 | |
|
a53d8f189c | |
|
494066f4a4 | |
|
15b54ba29c | |
|
c7fb780f6b | |
|
0daeb9f98b | |
|
b3597c01bb | |
|
f5b4a60379 | |
|
665a8df3fd | |
|
68ddbb0384 | |
|
0dcf3e9d26 | |
|
b62222be7c | |
|
6ecac9f4d1 | |
|
dbfbe37046 | |
|
988c0f82a8 | |
|
0a3c89578c | |
|
47024d970a | |
|
ea0b9ced4d | |
|
df1723ca5c | |
|
3658357fea | |
|
9add4d0573 | |
|
99abadede2 | |
|
79e6f1fbcf | |
|
aaeb01be6d | |
|
5047b8fe41 | |
|
b84662911c | |
|
753940e964 | |
|
b858201395 | |
|
c8d9720a72 | |
|
cccad306d6 | |
|
51b4b4971a | |
|
3423727e46 | |
|
b8357e72dd | |
|
0b01a72aa3 | |
|
f983148f8c | |
|
d513755309 | |
|
fbb5ab0d70 | |
|
fd21d18170 | |
|
8b8b5c0f78 | |
|
ae08979dcc | |
|
ada3ee1529 | |
|
a69b1ea2d5 | |
|
3aea7778be | |
|
026bd8791b | |
|
1f27993c44 | |
|
4ce1c97be7 | |
|
81ab7e0c68 | |
|
c9834df380 | |
|
c2262d518b | |
|
6898b8387d | |
|
5a059075db | |
|
d520e75de3 | |
|
43b9e23f22 | |
|
67b987f568 | |
|
a16a110639 | |
|
3a2942bdff | |
|
b5e6c92c1d | |
|
490aa4761a | |
|
6b49c2289e | |
|
accfd98e20 | |
|
ba14b9c42a | |
|
454f4fe2a7 | |
|
0b5852eb9a | |
|
88f051a068 | |
|
be81d72259 | |
|
882c7896e7 | |
|
dc2e401d84 | |
|
e906dc9707 | |
|
15884a5fd0 | |
|
cb02f4a386 | |
|
04fa4ade1a | |
|
7338177523 | |
|
4cb4306ed2 | |
|
982944365e | |
|
5fff35ea12 | |
|
4605e42ff2 | |
|
e3e5839136 | |
|
f7ccc75f9f | |
|
ca9c3d965e | |
|
cae3853011 | |
|
6df5cd720d | |
|
5d1ea16030 | |
|
0b45fb4d77 | |
|
97c3c25661 | |
|
1ae041730c | |
|
f1894425ee | |
|
0679efd5ba | |
|
9270d97c6f | |
|
2e7093fa33 | |
|
7d5891a801 | |
|
f0baf72379 | |
|
28f9eed685 | |
|
d93c90f116 | |
|
918f350339 | |
|
14c29cd5d7 | |
|
76c064061d | |
|
4bece4d457 | |
|
fb9f1a8075 | |
|
b91c4326ba | |
|
15adc430e7 | |
|
125611e8cd | |
|
30be0a3c20 | |
|
9843fd95fb | |
|
b738c82d70 | |
|
c259217340 | |
|
9da95682e0 | |
|
a5e93cb37c | |
|
0e6467b270 | |
|
9ead80d1bb | |
|
bd808a01a1 | |
|
8794780f6a | |
|
b09b8016b6 | |
|
78628824cd | |
|
00c7d5a4e1 | |
|
b432ca9bc1 | |
|
5913dac984 | |
|
781f771b86 | |
|
f27bb5491e | |
|
15220968e1 | |
|
cf0dbba4e6 | |
|
81796a85a6 | |
|
2bfdf1d736 | |
|
641a07c997 | |
|
9b88185823 | |
|
518e8ed25c | |
|
e85d45c0a4 | |
|
334b2d8b10 | |
|
eb7f648085 | |
|
de27d754e6 | |
|
fd6565d201 | |
|
fc6b2587cc | |
|
187d1ad94a | |
|
37f422396a | |
|
b43a2467d5 | |
|
7dd4904f18 | |
|
9d850afe6f | |
|
54b4c4f8a1 | |
|
c6b840aa24 | |
|
d0f54d5993 | |
|
ec228a4021 | |
|
1ea7befac3 | |
|
6600cfa976 | |
|
08766af90d | |
|
e784492b06 | |
|
b907ccabbe | |
|
08c82454ed | |
|
1e62dc23aa | |
|
d1d8d5e02d | |
|
aeae19662f | |
|
1f585b076f | |
|
608821ca42 | |
|
c8517e5aa2 | |
|
4f8e2cfe4c | |
|
54c7d50dd6 | |
|
402e1cdd43 | |
|
576854da2e | |
|
d089101190 | |
|
d52f63db87 | |
|
44ff1c1665 | |
|
6ad2d93d2e | |
|
e6b796812a | |
|
3ee34eb4ce | |
|
15f8ad59da | |
|
285603b2d8 | |
|
ebe463fe32 | |
|
b9c35c9189 | |
|
042b9763e0 | |
|
33a5883440 | |
|
23e33fe7dc | |
|
26cc27763e | |
|
f14df2d4fd | |
|
810761bf3d | |
|
664cde1f32 | |
|
965243019f | |
|
9da53c5ba3 | |
|
33cfd964ef | |
|
bf20cce32f | |
|
14881364b3 | |
|
4b46916a7b | |
|
f28acc6161 | |
|
bd937b2b8b | |
|
f45e42a091 | |
|
8602a60374 | |
|
50dafb8926 | |
|
c2070002ce | |
|
7202dc017c | |
|
f79926bf43 | |
|
3257d91a56 | |
|
5bdd80ce60 | |
|
d30adf3e28 | |
|
be240164d1 | |
|
0381d1eed6 | |
|
3a0975b22c | |
|
3e52ced45d | |
|
7b1424d930 | |
|
b2a5ee84cf | |
|
85ab93cd5f | |
|
b4437b251e | |
|
21560784da | |
|
10dc325a69 | |
|
2146712740 | |
|
df45eb6de5 | |
|
08c52b99b8 | |
|
2861287f95 | |
|
36e57697d1 | |
|
d7cbe7a5f2 | |
|
b16c96d150 | |
|
f900b71720 | |
|
60d1ca6725 | |
|
efdbeb541a | |
|
ad232ba084 | |
|
42502d245d | |
|
163594e62d | |
|
8c1054e51e | |
|
40680bab4c | |
|
38adb499b0 | |
|
326460d62e | |
|
e32a42cf11 | |
|
aaeee2a449 | |
|
17d29da911 | |
|
d9db30ee93 | |
|
afd0abad01 | |
|
18cfc4e4d4 | |
|
8cb411e993 | |
|
6dc6d8d7fa | |
|
11af6bbc46 | |
|
763c8220a6 | |
|
c34f5e1bba | |
|
bcc457994e | |
|
1e85cd3163 | |
|
de96004cf6 | |
|
8a3f4fc910 | |
|
b489c4ea4e | |
|
c6ba9681f7 | |
|
9996ba35fd | |
|
b09ab6e398 | |
|
4509919d9b | |
|
3f8a6ad61c | |
|
7d77d3c2dd | |
|
1a9bf77ab1 | |
|
cdd40a3d24 | |
|
eb3a20ad8b | |
|
743b5776f9 | |
|
87a5cdebbc | |
|
bafce892d3 | |
|
f5a73ba0a4 | |
|
e70087b968 | |
|
a84f81807f | |
|
7e60e39b7b | |
|
87a949c64a | |
|
71b1c7e730 | |
|
b0e4675775 | |
|
9a5bc0a561 | |
|
ac5c364aba | |
|
07be2984cd | |
|
d6db44ddd4 | |
|
17ab6c21d5 | |
|
7da1f042b3 | |
|
3abc312093 | |
|
6ab385d467 | |
|
b95faaac20 | |
|
438d2ea372 | |
|
14b3ab9aa0 | |
|
206e39c6f1 | |
|
c90ac4722a | |
|
0b2ba575d2 | |
|
d3a58ddf5a | |
|
91a4bf232d | |
|
d3f16fa83a | |
|
9f2124ae21 | |
|
fbad808f69 | |
|
8a764cf9c3 | |
|
f014f4a768 | |
|
5db4826844 | |
|
a096b0dd8d | |
|
197cd431b1 | |
|
8a5b9105e9 | |
|
fc23f87960 | |
|
b93ecaaa38 | |
|
4483cc97cf | |
|
a785076ef7 | |
|
1d107b22c5 | |
|
d076be3784 | |
|
f7eddd4bda | |
|
6999423628 | |
|
614f663b4a | |
|
0298bf965f | |
|
1ccea00339 | |
|
7896cd7b57 | |
|
33d4365d5b | |
|
d7c9221467 | |
|
535f09400e | |
|
cf15325096 | |
|
1bb7b2e6ab | |
|
e6df86ea96 | |
|
1bac1a3a1c | |
|
60f20c32c7 | |
|
b9e6a66c69 | |
|
3da25cf156 | |
|
90f5efd77b | |
|
e778ced9b7 | |
|
1b4b1fb1c0 | |
|
ec5e79c690 | |
|
6b4322243f | |
|
cfa44309dd | |
|
6bd08c5dc4 | |
|
af2142bfe4 | |
|
bbf28207e3 | |
|
13c06f3696 | |
|
bb8caf4ebf | |
|
bd063e0342 | |
|
a8c26a18b8 | |
|
94a5e3cd04 | |
|
784a83e3b4 | |
|
d9009c5253 | |
|
8beb81657f | |
|
f1fcc94e63 | |
|
daa75f8bec | |
|
6c201a977a | |
|
7198d4fe04 | |
|
a78241fd58 | |
|
12c4904eb5 | |
|
932b2589d5 | |
|
a03dae5dab | |
|
9c8c6ccc4d | |
|
158efa920d | |
|
586ab588eb | |
|
da8e2914b0 | |
|
e9e24680dc | |
|
27f5f2543a | |
|
2ed104b2e5 | |
|
491f6248d4 | |
|
a5bbfcdce4 | |
|
6830df5156 | |
|
66b4299b79 | |
|
43a56206a7 | |
|
5d131b7a78 | |
|
25d7e88901 | |
|
38586e5d94 | |
|
11b0e0730d | |
|
272e9eba82 | |
|
cd5bba1780 | |
|
b157511c42 | |
|
13f78e0e7e | |
|
be949676bf | |
|
cb239f8776 | |
|
c84ae4a3d4 | |
|
77331233f8 | |
|
27c3ca736b | |
|
fac4f5d2a0 | |
|
e312f49d45 | |
|
9d542feed9 | |
|
41e1af4df2 | |
|
fed75d52d6 | |
|
cc8ff8f965 | |
|
757565c389 | |
|
fcf807e7b4 | |
|
fb0703a685 | |
|
28d9c91abf | |
|
65a6ca8228 | |
|
cbc488649b | |
|
552e7d7170 | |
|
f434fbf0c7 | |
|
1d26753b4b | |
|
febd487238 | |
|
fed8dfe736 | |
|
3adae5fd46 | |
|
9aa7a6ac61 | |
|
bd44a99f50 | |
|
1b569bf504 | |
|
bebf7ae9a1 | |
|
c470f38c60 | |
|
788e7ee758 | |
|
6c5ee08ccf | |
|
c8097e3f30 | |
|
132d3e46d6 | |
|
de0559ec7b | |
|
72a449fe98 | |
|
644a2519ca |
3
OWNERS
3
OWNERS
|
@ -13,7 +13,6 @@ reviewers:
|
|||
- caesarxuchao
|
||||
- liggitt
|
||||
- sttts
|
||||
- ncdc
|
||||
- enj
|
||||
- hzxuzhonghu
|
||||
- apelisse
|
||||
|
@ -24,3 +23,5 @@ labels:
|
|||
- area/apiserver
|
||||
emeritus_approvers:
|
||||
- lavalamp
|
||||
emeritus_reviewers:
|
||||
- ncdc
|
||||
|
|
2
doc.go
2
doc.go
|
@ -14,4 +14,4 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package apiserver // import "k8s.io/apiserver"
|
||||
package apiserver
|
||||
|
|
161
go.mod
161
go.mod
|
@ -2,126 +2,125 @@
|
|||
|
||||
module k8s.io/apiserver
|
||||
|
||||
go 1.22.0
|
||||
go 1.24.0
|
||||
|
||||
godebug default=go1.24
|
||||
|
||||
require (
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||
github.com/blang/semver/v4 v4.0.0
|
||||
github.com/coreos/go-oidc v2.3.0+incompatible
|
||||
github.com/coreos/go-systemd/v22 v22.5.0
|
||||
github.com/emicklei/go-restful/v3 v3.11.0
|
||||
github.com/fsnotify/fsnotify v1.7.0
|
||||
github.com/emicklei/go-restful/v3 v3.12.2
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/go-logr/logr v1.4.2
|
||||
github.com/gogo/protobuf v1.3.2
|
||||
github.com/google/cel-go v0.20.1
|
||||
github.com/google/gnostic-models v0.6.8
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/google/gofuzz v1.2.0
|
||||
github.com/google/btree v1.1.3
|
||||
github.com/google/cel-go v0.25.0
|
||||
github.com/google/gnostic-models v0.7.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/stretchr/testify v1.9.0
|
||||
go.etcd.io/etcd/api/v3 v3.5.14
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.14
|
||||
go.etcd.io/etcd/client/v3 v3.5.14
|
||||
go.etcd.io/etcd/server/v3 v3.5.13
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0
|
||||
go.opentelemetry.io/otel v1.28.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0
|
||||
go.opentelemetry.io/otel/sdk v1.28.0
|
||||
go.opentelemetry.io/otel/trace v1.28.0
|
||||
go.uber.org/zap v1.26.0
|
||||
golang.org/x/crypto v0.24.0
|
||||
golang.org/x/net v0.26.0
|
||||
golang.org/x/sync v0.7.0
|
||||
golang.org/x/sys v0.21.0
|
||||
golang.org/x/time v0.3.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157
|
||||
google.golang.org/grpc v1.65.0
|
||||
google.golang.org/protobuf v1.34.2
|
||||
github.com/spf13/pflag v1.0.6
|
||||
github.com/stretchr/testify v1.10.0
|
||||
go.etcd.io/etcd/api/v3 v3.6.1
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.1
|
||||
go.etcd.io/etcd/client/v3 v3.6.1
|
||||
go.etcd.io/etcd/server/v3 v3.6.1
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0
|
||||
go.opentelemetry.io/otel v1.35.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0
|
||||
go.opentelemetry.io/otel/metric v1.35.0
|
||||
go.opentelemetry.io/otel/sdk v1.34.0
|
||||
go.opentelemetry.io/otel/trace v1.35.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.36.0
|
||||
golang.org/x/net v0.38.0
|
||||
golang.org/x/sync v0.12.0
|
||||
golang.org/x/sys v0.31.0
|
||||
golang.org/x/time v0.9.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb
|
||||
google.golang.org/grpc v1.72.1
|
||||
google.golang.org/protobuf v1.36.5
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0
|
||||
gopkg.in/go-jose/go-jose.v2 v2.6.3
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/square/go-jose.v2 v2.6.0
|
||||
k8s.io/api v0.0.0-20240724010313-f04ea0bc861d
|
||||
k8s.io/apimachinery v0.0.0-20240720202316-95b78024e3fe
|
||||
k8s.io/client-go v0.0.0-20240724010704-ac9204c6195b
|
||||
k8s.io/component-base v0.0.0-20240722183709-6cc953a9d440
|
||||
k8s.io/api v0.0.0-20250705010445-839e6c7fb630
|
||||
k8s.io/apimachinery v0.0.0-20250710005335-ed63805e81ef
|
||||
k8s.io/client-go v0.0.0-20250709010832-5439ef7b0c5e
|
||||
k8s.io/component-base v0.0.0-20250708051227-72837f691197
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
k8s.io/kms v0.0.0-20240707024556-6e3528fa4c33
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1
|
||||
sigs.k8s.io/yaml v1.4.0
|
||||
k8s.io/kms v0.0.0-20250701212550-c0cb85aa532f
|
||||
k8s.io/kube-openapi v0.0.0-20250628140032-d90c4fd18f59
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8
|
||||
sigs.k8s.io/randfill v1.0.0
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.7.0
|
||||
sigs.k8s.io/yaml v1.5.0
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.23.1 // indirect
|
||||
github.com/NYTimes/gziphandler v1.1.1 // indirect
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/coreos/go-semver v0.3.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.2 // indirect
|
||||
github.com/go-openapi/swag v0.22.4 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/go-openapi/swag v0.23.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
|
||||
github.com/imdario/mergo v0.3.6 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/moby/spdystream v0.4.0 // indirect
|
||||
github.com/moby/spdystream v0.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/pquerna/cachecontrol v0.1.0 // indirect
|
||||
github.com/prometheus/client_golang v1.19.1 // indirect
|
||||
github.com/prometheus/client_golang v1.22.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.55.0 // indirect
|
||||
github.com/prometheus/common v0.62.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/soheilhy/cmux v0.1.5 // indirect
|
||||
github.com/spf13/cobra v1.8.1 // indirect
|
||||
github.com/stoewer/go-strcase v1.2.0 // indirect
|
||||
github.com/spf13/cobra v1.9.1 // indirect
|
||||
github.com/stoewer/go-strcase v1.3.0 // indirect
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
|
||||
go.etcd.io/bbolt v1.3.9 // indirect
|
||||
go.etcd.io/etcd/client/v2 v2.305.13 // indirect
|
||||
go.etcd.io/etcd/pkg/v3 v3.5.13 // indirect
|
||||
go.etcd.io/etcd/raft/v3 v3.5.13 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
|
||||
github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 // indirect
|
||||
go.etcd.io/bbolt v1.4.0 // indirect
|
||||
go.etcd.io/etcd/pkg/v3 v3.6.1 // indirect
|
||||
go.etcd.io/raft/v3 v3.6.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect
|
||||
golang.org/x/oauth2 v0.21.0 // indirect
|
||||
golang.org/x/term v0.21.0 // indirect
|
||||
golang.org/x/text v0.16.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
|
||||
golang.org/x/oauth2 v0.27.0 // indirect
|
||||
golang.org/x/term v0.30.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace k8s.io/api => k8s.io/api v0.0.0-20240724010313-a789efa287e8
|
||||
|
|
401
go.sum
401
go.sum
|
@ -1,57 +1,42 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg=
|
||||
cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
||||
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
|
||||
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA=
|
||||
github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk=
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/coreos/go-oidc v2.3.0+incompatible h1:+5vEsrgprdLjjQ9FzIKAzQz1wwPD+83hQRfUIPh7rO0=
|
||||
github.com/coreos/go-oidc v2.3.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
|
||||
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
|
||||
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
|
||||
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
|
||||
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
|
@ -59,74 +44,60 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
|||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
|
||||
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
|
||||
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
|
||||
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
|
||||
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
|
||||
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
|
||||
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
|
||||
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
|
||||
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||
github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84=
|
||||
github.com/google/cel-go v0.20.1/go.mod h1:kWcIzTsPX0zmQ+H3TirHstLLf9ep5QTsZBN9u4dOYLg=
|
||||
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
|
||||
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY=
|
||||
github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI=
|
||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM=
|
||||
github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
|
||||
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0 h1:FbSCl+KggFl+Ocym490i/EyXF4lPgLoUtcSWquBM0Rs=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
|
||||
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
|
||||
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
|
||||
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
|
@ -134,263 +105,223 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/moby/spdystream v0.4.0 h1:Vy79D6mHeJJjiPdFEL2yku1kl0chZpJfZcPpb16BRl8=
|
||||
github.com/moby/spdystream v0.4.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
||||
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
|
||||
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
|
||||
github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
|
||||
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
|
||||
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
|
||||
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
|
||||
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8cTqKc=
|
||||
github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
|
||||
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
|
||||
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
|
||||
github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
|
||||
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
|
||||
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk=
|
||||
github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.etcd.io/bbolt v1.3.9 h1:8x7aARPEXiXbHmtUwAIv7eV2fQFHrLLavdiJ3uzJXoI=
|
||||
go.etcd.io/bbolt v1.3.9/go.mod h1:zaO32+Ti0PK1ivdPtgMESzuzL2VPoIG1PCQNvOdo/dE=
|
||||
go.etcd.io/etcd/api/v3 v3.5.14 h1:vHObSCxyB9zlF60w7qzAdTcGaglbJOpSj1Xj9+WGxq0=
|
||||
go.etcd.io/etcd/api/v3 v3.5.14/go.mod h1:BmtWcRlQvwa1h3G2jvKYwIQy4PkHlDej5t7uLMUdJUU=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.14 h1:SaNH6Y+rVEdxfpA2Jr5wkEvN6Zykme5+YnbCkxvuWxQ=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.14/go.mod h1:8uMgAokyG1czCtIdsq+AGyYQMvpIKnSvPjFMunkgeZI=
|
||||
go.etcd.io/etcd/client/v2 v2.305.13 h1:RWfV1SX5jTU0lbCvpVQe3iPQeAHETWdOTb6pxhd77C8=
|
||||
go.etcd.io/etcd/client/v2 v2.305.13/go.mod h1:iQnL7fepbiomdXMb3om1rHq96htNNGv2sJkEcZGDRRg=
|
||||
go.etcd.io/etcd/client/v3 v3.5.14 h1:CWfRs4FDaDoSz81giL7zPpZH2Z35tbOrAJkkjMqOupg=
|
||||
go.etcd.io/etcd/client/v3 v3.5.14/go.mod h1:k3XfdV/VIHy/97rqWjoUzrj9tk7GgJGH9J8L4dNXmAk=
|
||||
go.etcd.io/etcd/pkg/v3 v3.5.13 h1:st9bDWNsKkBNpP4PR1MvM/9NqUPfvYZx/YXegsYEH8M=
|
||||
go.etcd.io/etcd/pkg/v3 v3.5.13/go.mod h1:N+4PLrp7agI/Viy+dUYpX7iRtSPvKq+w8Y14d1vX+m0=
|
||||
go.etcd.io/etcd/raft/v3 v3.5.13 h1:7r/NKAOups1YnKcfro2RvGGo2PTuizF/xh26Z2CTAzA=
|
||||
go.etcd.io/etcd/raft/v3 v3.5.13/go.mod h1:uUFibGLn2Ksm2URMxN1fICGhk8Wu96EfDQyuLhAcAmw=
|
||||
go.etcd.io/etcd/server/v3 v3.5.13 h1:V6KG+yMfMSqWt+lGnhFpP5z5dRUj1BDRJ5k1fQ9DFok=
|
||||
go.etcd.io/etcd/server/v3 v3.5.13/go.mod h1:K/8nbsGupHqmr5MkgaZpLlH1QdX1pcNQLAkODy44XcQ=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
|
||||
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
|
||||
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 h1:qFffATk0X+HD+f1Z8lswGiOQYKHRlzfmdJm0wEaVrFA=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0/go.mod h1:MOiCmryaYtc+V0Ei+Tx9o5S1ZjA7kzLucuVuyzBZloQ=
|
||||
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
||||
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
||||
go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE=
|
||||
go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg=
|
||||
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
|
||||
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
|
||||
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
|
||||
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
|
||||
go.etcd.io/etcd/api/v3 v3.6.1 h1:yJ9WlDih9HT457QPuHt/TH/XtsdN2tubyxyQHSHPsEo=
|
||||
go.etcd.io/etcd/api/v3 v3.6.1/go.mod h1:lnfuqoGsXMlZdTJlact3IB56o3bWp1DIlXPIGKRArto=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.1 h1:CxDVv8ggphmamrXM4Of8aCC8QHzDM4tGcVr9p2BSoGk=
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.1/go.mod h1:aTkCp+6ixcVTZmrJGa7/Mc5nMNs59PEgBbq+HCmWyMc=
|
||||
go.etcd.io/etcd/client/v3 v3.6.1 h1:KelkcizJGsskUXlsxjVrSmINvMMga0VWwFF0tSPGEP0=
|
||||
go.etcd.io/etcd/client/v3 v3.6.1/go.mod h1:fCbPUdjWNLfx1A6ATo9syUmFVxqHH9bCnPLBZmnLmMY=
|
||||
go.etcd.io/etcd/pkg/v3 v3.6.1 h1:Qpshk3/SLra217k7FxcFGaH2niFAxFf1Dug57f0IUiw=
|
||||
go.etcd.io/etcd/pkg/v3 v3.6.1/go.mod h1:nS0ahQoZZ9qXjQAtYGDt80IEHKl9YOF7mv6J0lQmBoQ=
|
||||
go.etcd.io/etcd/server/v3 v3.6.1 h1:Y/mh94EeImzXyTBIMVgR0v5H+ANtRFDY4g1s5sxOZGE=
|
||||
go.etcd.io/etcd/server/v3 v3.6.1/go.mod h1:nCqJGTP9c2WlZluJB59j3bqxZEI/GYBfQxno0MguVjE=
|
||||
go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
|
||||
go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
|
||||
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
|
||||
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
|
||||
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU=
|
||||
golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
|
||||
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
|
||||
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
|
||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY=
|
||||
google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
||||
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb h1:TLPQVbx1GJ8VKZxz52VAxl1EBgKXXbTiU9Fc5fZeLn4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
|
||||
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
|
||||
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs=
|
||||
gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
|
||||
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
k8s.io/api v0.0.0-20240724010313-a789efa287e8 h1:TISAHWnfAdn420WpN+fEHG6snbLbfaCAp3kHDoAkxIc=
|
||||
k8s.io/api v0.0.0-20240724010313-a789efa287e8/go.mod h1:ytlEzqC2wOTwYET71W7+J+k7O2V7vrDuzmNLBSpgT+k=
|
||||
k8s.io/apimachinery v0.0.0-20240720202316-95b78024e3fe h1:V9MwpYUwbKlfLKVrhpVuKWiat/LBIhm1pGB9/xdHm5Q=
|
||||
k8s.io/apimachinery v0.0.0-20240720202316-95b78024e3fe/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
|
||||
k8s.io/client-go v0.0.0-20240724010704-ac9204c6195b h1:NTLYx38CAu+VstHvPLosqB6uSQUtSM+3Mqz2D/C5JpE=
|
||||
k8s.io/client-go v0.0.0-20240724010704-ac9204c6195b/go.mod h1:Y6CzOT21oLI4O66cjiV5oSSUgOL7gG/VCG9n8XI8OxU=
|
||||
k8s.io/component-base v0.0.0-20240722183709-6cc953a9d440 h1:14X+5sRQRsul6tLxIKTP0/DotvWlMd9DFCgMqHP1hZY=
|
||||
k8s.io/component-base v0.0.0-20240722183709-6cc953a9d440/go.mod h1:dj2Pl05aLcVMZi2NXcwv+M/WdUVPEkisFPjDze7rbSk=
|
||||
k8s.io/api v0.0.0-20250705010445-839e6c7fb630 h1:pnI9Db0bmtO4qa+X6jGK8WslPvzLwW8wrAe5B2//yGU=
|
||||
k8s.io/api v0.0.0-20250705010445-839e6c7fb630/go.mod h1:cQb0K/knyMnN0b7QfEoYB+YzMbFk6PMoa/XTGxEJ7iw=
|
||||
k8s.io/apimachinery v0.0.0-20250710005335-ed63805e81ef h1:87QT4Qmn87cD8y09BKqHmXrBC+eIZd9YTG2tDwkGhCc=
|
||||
k8s.io/apimachinery v0.0.0-20250710005335-ed63805e81ef/go.mod h1:Th679JJyaVRDNFk3vKPKY43ypziDeoGnbEiEgBCz8s4=
|
||||
k8s.io/client-go v0.0.0-20250709010832-5439ef7b0c5e h1:NGKcA9W2nQjLWlZrHfTcMPkPpjFci2M+SnmkusqoIHo=
|
||||
k8s.io/client-go v0.0.0-20250709010832-5439ef7b0c5e/go.mod h1:mSlS6FavM9KR26OMw/g4NsTn4BtRSGVKE0DlKfOoE2c=
|
||||
k8s.io/component-base v0.0.0-20250708051227-72837f691197 h1:HPpnKAvWicINIJb5H0yd7sMnvXjAcCeoN2j8NkIK+s8=
|
||||
k8s.io/component-base v0.0.0-20250708051227-72837f691197/go.mod h1:U4cTVU7iRxFIlHEFNsrq7AYzG4lxdaliabhy0qPB/Ww=
|
||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||
k8s.io/kms v0.0.0-20240707024556-6e3528fa4c33 h1:Wd/sRvKMgzuCdkZ/WQg2rg/j6NLW8eyw0RK8AhV9Hak=
|
||||
k8s.io/kms v0.0.0-20240707024556-6e3528fa4c33/go.mod h1:x2EJv5lkGE18ijjE04e8W0fVyRPfAx5flo9WdY7a1Hw=
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
|
||||
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98=
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
|
||||
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 h1:2770sDpzrjjsAtVhSeUFseziht227YAWYHLGNM8QPwY=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08=
|
||||
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
|
||||
k8s.io/kms v0.0.0-20250701212550-c0cb85aa532f h1:IiE5Db+Ha45v4GSmZtR4OLdkk8j7Nk/76SP1gvVAjnM=
|
||||
k8s.io/kms v0.0.0-20250701212550-c0cb85aa532f/go.mod h1:qCbYSZ7AgfskxlzZlEYmu4XfrmaR8oXyYSAiwvh/fTk=
|
||||
k8s.io/kube-openapi v0.0.0-20250628140032-d90c4fd18f59 h1:Jc4GiFTK2HHOpfQFoQEGXTBTs2pETwHukmoD4yoTqwo=
|
||||
k8s.io/kube-openapi v0.0.0-20250628140032-d90c4fd18f59/go.mod h1:GLOk5B+hDbRROvt0X2+hqX64v/zO3vXN7J78OUmBSKw=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y=
|
||||
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
|
||||
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI=
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
|
||||
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
|
||||
sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
|
||||
sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
|
||||
|
|
|
@ -30,13 +30,13 @@ func TestAddAnnotation(t *testing.T) {
|
|||
attr.AddAnnotation("foo.admission.k8s.io/key1", "value1")
|
||||
attr.AddAnnotation("foo.admission.k8s.io/key2", "value2")
|
||||
annotations := attr.getAnnotations(auditinternal.LevelMetadata)
|
||||
assert.Equal(t, annotations["foo.admission.k8s.io/key1"], "value1")
|
||||
assert.Equal(t, "value1", annotations["foo.admission.k8s.io/key1"])
|
||||
|
||||
// test overwrite
|
||||
assert.Error(t, attr.AddAnnotation("foo.admission.k8s.io/key1", "value1-overwrite"),
|
||||
"admission annotations should not be allowd to be overwritten")
|
||||
annotations = attr.getAnnotations(auditinternal.LevelMetadata)
|
||||
assert.Equal(t, annotations["foo.admission.k8s.io/key1"], "value1", "admission annotations should not be overwritten")
|
||||
assert.Equal(t, "value1", annotations["foo.admission.k8s.io/key1"], "admission annotations should not be overwritten")
|
||||
|
||||
// test invalid plugin names
|
||||
var testCases = map[string]string{
|
||||
|
@ -49,17 +49,16 @@ func TestAddAnnotation(t *testing.T) {
|
|||
err := attr.AddAnnotation(invalidKey, "value-foo")
|
||||
assert.Error(t, err)
|
||||
annotations = attr.getAnnotations(auditinternal.LevelMetadata)
|
||||
assert.Equal(t, annotations[invalidKey], "", name+": invalid pluginName is not allowed ")
|
||||
assert.Equal(t, "", annotations[invalidKey], name+": invalid pluginName is not allowed ")
|
||||
}
|
||||
|
||||
// test all saved annotations
|
||||
assert.Equal(
|
||||
t,
|
||||
annotations,
|
||||
map[string]string{
|
||||
"foo.admission.k8s.io/key1": "value1",
|
||||
"foo.admission.k8s.io/key2": "value2",
|
||||
},
|
||||
}, annotations,
|
||||
"unexpected final annotations",
|
||||
)
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@ func ensureAnnotationGetter(a Attributes) error {
|
|||
}
|
||||
|
||||
func (handler *auditHandler) logAnnotations(ctx context.Context, a Attributes) {
|
||||
ae := audit.AuditEventFrom(ctx)
|
||||
ae := audit.AuditContextFrom(ctx)
|
||||
if ae == nil {
|
||||
return
|
||||
}
|
||||
|
@ -91,9 +91,9 @@ func (handler *auditHandler) logAnnotations(ctx context.Context, a Attributes) {
|
|||
var annotations map[string]string
|
||||
switch a := a.(type) {
|
||||
case privateAnnotationsGetter:
|
||||
annotations = a.getAnnotations(ae.Level)
|
||||
annotations = a.getAnnotations(ae.GetEventLevel())
|
||||
case AnnotationsGetter:
|
||||
annotations = a.GetAnnotations(ae.Level)
|
||||
annotations = a.GetAnnotations(ae.GetEventLevel())
|
||||
default:
|
||||
// this will never happen, because we have already checked it in ensureAnnotationGetter
|
||||
}
|
||||
|
|
|
@ -144,8 +144,10 @@ func TestWithAudit(t *testing.T) {
|
|||
var handler Interface = fakeHandler{tc.admit, tc.admitAnnotations, tc.validate, tc.validateAnnotations, tc.handles}
|
||||
ctx := audit.WithAuditContext(context.Background())
|
||||
ac := audit.AuditContextFrom(ctx)
|
||||
ae := &ac.Event
|
||||
ae.Level = auditinternal.LevelMetadata
|
||||
if err := ac.Init(audit.RequestAuditConfig{Level: auditinternal.LevelMetadata}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
auditHandler := WithAudit(handler)
|
||||
a := attributes()
|
||||
|
||||
|
@ -171,9 +173,9 @@ func TestWithAudit(t *testing.T) {
|
|||
annotations[k] = v
|
||||
}
|
||||
if len(annotations) == 0 {
|
||||
assert.Nil(t, ae.Annotations, tcName+": unexptected annotations set in audit event")
|
||||
assert.Nil(t, ac.GetEventAnnotations(), tcName+": unexptected annotations set in audit event")
|
||||
} else {
|
||||
assert.Equal(t, annotations, ae.Annotations, tcName+": unexptected annotations set in audit event")
|
||||
assert.Equal(t, annotations, ac.GetEventAnnotations(), tcName+": unexptected annotations set in audit event")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -187,8 +189,6 @@ func TestWithAuditConcurrency(t *testing.T) {
|
|||
}
|
||||
var handler Interface = fakeHandler{admitAnnotations: admitAnnotations, handles: true}
|
||||
ctx := audit.WithAuditContext(context.Background())
|
||||
ac := audit.AuditContextFrom(ctx)
|
||||
ac.Event.Level = auditinternal.LevelMetadata
|
||||
auditHandler := WithAudit(handler)
|
||||
a := attributes()
|
||||
|
||||
|
@ -200,9 +200,15 @@ func TestWithAuditConcurrency(t *testing.T) {
|
|||
go func() {
|
||||
defer wg.Done()
|
||||
mutator, ok := handler.(MutationInterface)
|
||||
require.True(t, ok)
|
||||
if !ok {
|
||||
t.Error("handler is not an interface of type MutationInterface")
|
||||
return
|
||||
}
|
||||
auditMutator, ok := auditHandler.(MutationInterface)
|
||||
require.True(t, ok)
|
||||
if !ok {
|
||||
t.Error("handler is not an interface of type MutationInterface")
|
||||
return
|
||||
}
|
||||
assert.Equal(t, mutator.Admit(ctx, a, nil), auditMutator.Admit(ctx, a, nil), "WithAudit decorator should not effect the return value")
|
||||
}()
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ func ReadAdmissionConfiguration(pluginNames []string, configFilePath string, con
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to read admission control configuration from %q [%v]", configFilePath, err)
|
||||
}
|
||||
codecs := serializer.NewCodecFactory(configScheme)
|
||||
codecs := serializer.NewCodecFactory(configScheme, serializer.EnableStrict)
|
||||
decoder := codecs.UniversalDecoder()
|
||||
decodedObj, err := runtime.Decode(decoder, data)
|
||||
// we were able to decode the file successfully
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
@ -52,7 +53,28 @@ func TestReadAdmissionConfiguration(t *testing.T) {
|
|||
ConfigBody string
|
||||
ExpectedAdmissionConfig *apiserver.AdmissionConfiguration
|
||||
PluginNames []string
|
||||
ExpectedError string
|
||||
}{
|
||||
"duplicate field configuration error": {
|
||||
ConfigBody: `{
|
||||
"apiVersion": "apiserver.k8s.io/v1alpha1",
|
||||
"kind": "AdmissionConfiguration",
|
||||
"plugins": [
|
||||
{"name": "ImagePolicyWebhook-duplicate", "name": "ImagePolicyWebhook", "path": "image-policy-webhook.json"},
|
||||
{"name": "ResourceQuota"}
|
||||
]}`,
|
||||
ExpectedError: "strict decoding error: duplicate field",
|
||||
},
|
||||
"unknown field configuration error": {
|
||||
ConfigBody: `{
|
||||
"apiVersion": "apiserver.k8s.io/v1alpha1",
|
||||
"kind": "AdmissionConfiguration",
|
||||
"plugins": [
|
||||
{"foo": "bar", "name": "ImagePolicyWebhook", "path": "image-policy-webhook.json"},
|
||||
{"name": "ResourceQuota"}
|
||||
]}`,
|
||||
ExpectedError: "strict decoding error: unknown field",
|
||||
},
|
||||
"v1alpha1 configuration - path fixup": {
|
||||
ConfigBody: `{
|
||||
"apiVersion": "apiserver.k8s.io/v1alpha1",
|
||||
|
@ -192,12 +214,18 @@ func TestReadAdmissionConfiguration(t *testing.T) {
|
|||
t.Fatalf("unexpected err writing temp file: %v", err)
|
||||
}
|
||||
config, err := ReadAdmissionConfiguration(testCase.PluginNames, configFileName, scheme)
|
||||
if err != nil {
|
||||
if testCase.ExpectedError != "" {
|
||||
if err != nil {
|
||||
assert.Contains(t, err.Error(), testCase.ExpectedError)
|
||||
} else {
|
||||
t.Fatalf("expected error %q but received none", testCase.ExpectedError)
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
} else if !reflect.DeepEqual(config.(configProvider).config, testCase.ExpectedAdmissionConfig) {
|
||||
t.Fatalf("%s: Expected:\n\t%#v\nGot:\n\t%#v", testName, testCase.ExpectedAdmissionConfig, config.(configProvider).config)
|
||||
}
|
||||
if !reflect.DeepEqual(config.(configProvider).config, testCase.ExpectedAdmissionConfig) {
|
||||
t.Errorf("%s: Expected:\n\t%#v\nGot:\n\t%#v", testName, testCase.ExpectedAdmissionConfig, config.(configProvider).config)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -207,7 +207,7 @@ func newAdmissionMetrics() *AdmissionMetrics {
|
|||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "webhook_fail_open_count",
|
||||
Help: "Admission webhook fail open count, identified by name and broken out for each admission type (validating or mutating).",
|
||||
Help: "Admission webhook fail open count, identified by name and broken out for each admission type (validating or admit).",
|
||||
StabilityLevel: metrics.ALPHA,
|
||||
},
|
||||
[]string{"name", "type"})
|
||||
|
@ -217,7 +217,7 @@ func newAdmissionMetrics() *AdmissionMetrics {
|
|||
Namespace: namespace,
|
||||
Subsystem: subsystem,
|
||||
Name: "webhook_request_total",
|
||||
Help: "Admission webhook request total, identified by name and broken out for each admission type (validating or mutating) and operation. Additional labels specify whether the request was rejected or not and an HTTP status code. Codes greater than 600 are truncated to 600, to keep the metrics cardinality bounded.",
|
||||
Help: "Admission webhook request total, identified by name and broken out for each admission type (validating or admit) and operation. Additional labels specify whether the request was rejected or not and an HTTP status code. Codes greater than 600 are truncated to 600, to keep the metrics cardinality bounded.",
|
||||
StabilityLevel: metrics.ALPHA,
|
||||
},
|
||||
[]string{"name", "type", "operation", "code", "rejected"})
|
||||
|
@ -305,6 +305,11 @@ func (m *AdmissionMetrics) ObserveWebhookRejection(ctx context.Context, name, st
|
|||
m.webhookRejection.WithContext(ctx).WithLabelValues(name, stepType, operation, string(errorType), strconv.Itoa(rejectionCode)).Inc()
|
||||
}
|
||||
|
||||
// WebhookRejectionGathererForTest exposes admission webhook rejection metric for access by unit test.
|
||||
func (m *AdmissionMetrics) WebhookRejectionGathererForTest() *metrics.CounterVec {
|
||||
return m.webhookRejection
|
||||
}
|
||||
|
||||
// ObserveWebhookFailOpen records validating or mutating webhook that fail open.
|
||||
func (m *AdmissionMetrics) ObserveWebhookFailOpen(ctx context.Context, name, stepType string) {
|
||||
m.webhookFailOpen.WithContext(ctx).WithLabelValues(name, stepType).Inc()
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package validating
|
||||
package authorizer
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
@ -39,7 +39,10 @@ type cachingAuthorizer struct {
|
|||
decisions map[string]authzResult
|
||||
}
|
||||
|
||||
func newCachingAuthorizer(in authorizer.Authorizer) authorizer.Authorizer {
|
||||
// NewCachingAuthorizer returns an authorizer that caches decisions for the duration
|
||||
// of the authorizers use. Intended to be used for short-lived operations such as
|
||||
// the handling of a request in the admission chain, and then discarded.
|
||||
func NewCachingAuthorizer(in authorizer.Authorizer) authorizer.Authorizer {
|
||||
return &cachingAuthorizer{
|
||||
authorizer: in,
|
||||
decisions: make(map[string]authzResult),
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package validating
|
||||
package authorizer
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
@ -491,7 +491,7 @@ func TestCachingAuthorizer(t *testing.T) {
|
|||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var misses int
|
||||
frontend := newCachingAuthorizer(func() authorizer.Authorizer {
|
||||
frontend := NewCachingAuthorizer(func() authorizer.Authorizer {
|
||||
return authorizer.AuthorizerFunc(func(_ context.Context, attributes authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
if misses >= len(tc.backend) {
|
||||
t.Fatalf("got more than expected %d backend invocations", len(tc.backend))
|
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
Copyright 2024 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 cel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/google/cel-go/interpreter"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/library"
|
||||
)
|
||||
|
||||
// newActivation creates an activation for CEL admission plugins from the given request, admission chain and
|
||||
// variable binding information.
|
||||
func newActivation(compositionCtx CompositionContext, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, namespace *v1.Namespace) (*evaluationActivation, error) {
|
||||
oldObjectVal, err := objectToResolveVal(versionedAttr.VersionedOldObject)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare oldObject variable for evaluation: %w", err)
|
||||
}
|
||||
objectVal, err := objectToResolveVal(versionedAttr.VersionedObject)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare object variable for evaluation: %w", err)
|
||||
}
|
||||
var paramsVal, authorizerVal, requestResourceAuthorizerVal any
|
||||
if inputs.VersionedParams != nil {
|
||||
paramsVal, err = objectToResolveVal(inputs.VersionedParams)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare params variable for evaluation: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if inputs.Authorizer != nil {
|
||||
authorizerVal = library.NewAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer)
|
||||
requestResourceAuthorizerVal = library.NewResourceAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer, versionedAttr)
|
||||
}
|
||||
|
||||
requestVal, err := convertObjectToUnstructured(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare request variable for evaluation: %w", err)
|
||||
}
|
||||
namespaceVal, err := objectToResolveVal(namespace)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare namespace variable for evaluation: %w", err)
|
||||
}
|
||||
va := &evaluationActivation{
|
||||
object: objectVal,
|
||||
oldObject: oldObjectVal,
|
||||
params: paramsVal,
|
||||
request: requestVal.Object,
|
||||
namespace: namespaceVal,
|
||||
authorizer: authorizerVal,
|
||||
requestResourceAuthorizer: requestResourceAuthorizerVal,
|
||||
}
|
||||
|
||||
// composition is an optional feature that only applies for ValidatingAdmissionPolicy and MutatingAdmissionPolicy.
|
||||
if compositionCtx != nil {
|
||||
va.variables = compositionCtx.Variables(va)
|
||||
}
|
||||
return va, nil
|
||||
}
|
||||
|
||||
type evaluationActivation struct {
|
||||
object, oldObject, params, request, namespace, authorizer, requestResourceAuthorizer, variables interface{}
|
||||
}
|
||||
|
||||
// ResolveName returns a value from the activation by qualified name, or false if the name
|
||||
// could not be found.
|
||||
func (a *evaluationActivation) ResolveName(name string) (interface{}, bool) {
|
||||
switch name {
|
||||
case ObjectVarName:
|
||||
return a.object, true
|
||||
case OldObjectVarName:
|
||||
return a.oldObject, true
|
||||
case ParamsVarName:
|
||||
return a.params, true // params may be null
|
||||
case RequestVarName:
|
||||
return a.request, true
|
||||
case NamespaceVarName:
|
||||
return a.namespace, true
|
||||
case AuthorizerVarName:
|
||||
return a.authorizer, a.authorizer != nil
|
||||
case RequestResourceAuthorizerVarName:
|
||||
return a.requestResourceAuthorizer, a.requestResourceAuthorizer != nil
|
||||
case VariableVarName: // variables always present
|
||||
return a.variables, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
// Parent returns the parent of the current activation, may be nil.
|
||||
// If non-nil, the parent will be searched during resolve calls.
|
||||
func (a *evaluationActivation) Parent() interpreter.Activation {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Evaluate runs a compiled CEL admission plugin expression using the provided activation and CEL
|
||||
// runtime cost budget.
|
||||
func (a *evaluationActivation) Evaluate(ctx context.Context, compositionCtx CompositionContext, compilationResult CompilationResult, remainingBudget int64) (EvaluationResult, int64, error) {
|
||||
var evaluation = EvaluationResult{}
|
||||
if compilationResult.ExpressionAccessor == nil { // in case of placeholder
|
||||
return evaluation, remainingBudget, nil
|
||||
}
|
||||
|
||||
evaluation.ExpressionAccessor = compilationResult.ExpressionAccessor
|
||||
if compilationResult.Error != nil {
|
||||
evaluation.Error = &cel.Error{
|
||||
Type: cel.ErrorTypeInvalid,
|
||||
Detail: fmt.Sprintf("compilation error: %v", compilationResult.Error),
|
||||
Cause: compilationResult.Error,
|
||||
}
|
||||
return evaluation, remainingBudget, nil
|
||||
}
|
||||
if compilationResult.Program == nil {
|
||||
evaluation.Error = &cel.Error{
|
||||
Type: cel.ErrorTypeInternal,
|
||||
Detail: "unexpected internal error compiling expression",
|
||||
}
|
||||
return evaluation, remainingBudget, nil
|
||||
}
|
||||
t1 := time.Now()
|
||||
evalResult, evalDetails, err := compilationResult.Program.ContextEval(ctx, a)
|
||||
// budget may be spent due to lazy evaluation of composited variables
|
||||
if compositionCtx != nil {
|
||||
compositionCost := compositionCtx.GetAndResetCost()
|
||||
if compositionCost > remainingBudget {
|
||||
return evaluation, -1, &cel.Error{
|
||||
Type: cel.ErrorTypeInvalid,
|
||||
Detail: "validation failed due to running out of cost budget, no further validation rules will be run",
|
||||
Cause: cel.ErrOutOfBudget,
|
||||
}
|
||||
}
|
||||
remainingBudget -= compositionCost
|
||||
}
|
||||
elapsed := time.Since(t1)
|
||||
evaluation.Elapsed = elapsed
|
||||
if evalDetails == nil {
|
||||
return evaluation, -1, &cel.Error{
|
||||
Type: cel.ErrorTypeInternal,
|
||||
Detail: fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()),
|
||||
}
|
||||
} else {
|
||||
rtCost := evalDetails.ActualCost()
|
||||
if rtCost == nil {
|
||||
return evaluation, -1, &cel.Error{
|
||||
Type: cel.ErrorTypeInvalid,
|
||||
Detail: fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()),
|
||||
Cause: cel.ErrOutOfBudget,
|
||||
}
|
||||
} else {
|
||||
if *rtCost > math.MaxInt64 || int64(*rtCost) > remainingBudget {
|
||||
return evaluation, -1, &cel.Error{
|
||||
Type: cel.ErrorTypeInvalid,
|
||||
Detail: "validation failed due to running out of cost budget, no further validation rules will be run",
|
||||
Cause: cel.ErrOutOfBudget,
|
||||
}
|
||||
}
|
||||
remainingBudget -= int64(*rtCost)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
evaluation.Error = &cel.Error{
|
||||
Type: cel.ErrorTypeInvalid,
|
||||
Detail: fmt.Sprintf("expression '%v' resulted in error: %v", compilationResult.ExpressionAccessor.GetExpression(), err),
|
||||
}
|
||||
} else {
|
||||
evaluation.EvalResult = evalResult
|
||||
}
|
||||
return evaluation, remainingBudget, nil
|
||||
}
|
|
@ -24,8 +24,10 @@ import (
|
|||
"k8s.io/apimachinery/pkg/util/version"
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/common"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
"k8s.io/apiserver/pkg/cel/library"
|
||||
"k8s.io/apiserver/pkg/cel/mutation"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -186,7 +188,7 @@ func (c compiler) CompileCELExpression(expressionAccessor ExpressionAccessor, op
|
|||
found := false
|
||||
returnTypes := expressionAccessor.ReturnTypes()
|
||||
for _, returnType := range returnTypes {
|
||||
if ast.OutputType() == returnType || cel.AnyType == returnType {
|
||||
if ast.OutputType().IsExactType(returnType) || cel.AnyType.IsExactType(returnType) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
|
@ -194,9 +196,9 @@ func (c compiler) CompileCELExpression(expressionAccessor ExpressionAccessor, op
|
|||
if !found {
|
||||
var reason string
|
||||
if len(returnTypes) == 1 {
|
||||
reason = fmt.Sprintf("must evaluate to %v", returnTypes[0].String())
|
||||
reason = fmt.Sprintf("must evaluate to %v but got %v", returnTypes[0].String(), ast.OutputType().String())
|
||||
} else {
|
||||
reason = fmt.Sprintf("must evaluate to one of %v", returnTypes)
|
||||
reason = fmt.Sprintf("must evaluate to one of %v but got %v", returnTypes, ast.OutputType().String())
|
||||
}
|
||||
|
||||
return resultError(reason, apiservercel.ErrorTypeInvalid, nil)
|
||||
|
@ -226,46 +228,78 @@ func mustBuildEnvs(baseEnv *environment.EnvSet) variableDeclEnvs {
|
|||
envs := make(variableDeclEnvs, 8) // since the number of variable combinations is small, pre-build a environment for each
|
||||
for _, hasParams := range []bool{false, true} {
|
||||
for _, hasAuthorizer := range []bool{false, true} {
|
||||
var err error
|
||||
for _, strictCost := range []bool{false, true} {
|
||||
var envOpts []cel.EnvOption
|
||||
if hasParams {
|
||||
envOpts = append(envOpts, cel.Variable(ParamsVarName, cel.DynType))
|
||||
}
|
||||
if hasAuthorizer {
|
||||
envOpts = append(envOpts,
|
||||
cel.Variable(AuthorizerVarName, library.AuthorizerType),
|
||||
cel.Variable(RequestResourceAuthorizerVarName, library.ResourceCheckType))
|
||||
}
|
||||
envOpts = append(envOpts,
|
||||
cel.Variable(ObjectVarName, cel.DynType),
|
||||
cel.Variable(OldObjectVarName, cel.DynType),
|
||||
cel.Variable(NamespaceVarName, namespaceType.CelType()),
|
||||
cel.Variable(RequestVarName, requestType.CelType()))
|
||||
|
||||
extended, err := baseEnv.Extend(
|
||||
environment.VersionedOptions{
|
||||
// Feature epoch was actually 1.26, but we artificially set it to 1.0 because these
|
||||
// options should always be present.
|
||||
IntroducedVersion: version.MajorMinor(1, 0),
|
||||
EnvOptions: envOpts,
|
||||
DeclTypes: []*apiservercel.DeclType{
|
||||
namespaceType,
|
||||
requestType,
|
||||
},
|
||||
},
|
||||
)
|
||||
decl := OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer, StrictCost: strictCost}
|
||||
envs[decl], err = createEnvForOpts(baseEnv, namespaceType, requestType, decl)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("environment misconfigured: %v", err))
|
||||
panic(err)
|
||||
}
|
||||
if strictCost {
|
||||
extended, err = extended.Extend(environment.StrictCostOpt)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("environment misconfigured: %v", err))
|
||||
}
|
||||
}
|
||||
envs[OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer, StrictCost: strictCost}] = extended
|
||||
}
|
||||
// We only need this ObjectTypes where strict cost is true
|
||||
decl := OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer, StrictCost: true, HasPatchTypes: true}
|
||||
envs[decl], err = createEnvForOpts(baseEnv, namespaceType, requestType, decl)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return envs
|
||||
}
|
||||
|
||||
func createEnvForOpts(baseEnv *environment.EnvSet, namespaceType *apiservercel.DeclType, requestType *apiservercel.DeclType, opts OptionalVariableDeclarations) (*environment.EnvSet, error) {
|
||||
var envOpts []cel.EnvOption
|
||||
envOpts = append(envOpts,
|
||||
cel.Variable(ObjectVarName, cel.DynType),
|
||||
cel.Variable(OldObjectVarName, cel.DynType),
|
||||
cel.Variable(NamespaceVarName, namespaceType.CelType()),
|
||||
cel.Variable(RequestVarName, requestType.CelType()))
|
||||
if opts.HasParams {
|
||||
envOpts = append(envOpts, cel.Variable(ParamsVarName, cel.DynType))
|
||||
}
|
||||
if opts.HasAuthorizer {
|
||||
envOpts = append(envOpts,
|
||||
cel.Variable(AuthorizerVarName, library.AuthorizerType),
|
||||
cel.Variable(RequestResourceAuthorizerVarName, library.ResourceCheckType))
|
||||
}
|
||||
|
||||
extended, err := baseEnv.Extend(
|
||||
environment.VersionedOptions{
|
||||
// Feature epoch was actually 1.26, but we artificially set it to 1.0 because these
|
||||
// options should always be present.
|
||||
IntroducedVersion: version.MajorMinor(1, 0),
|
||||
EnvOptions: envOpts,
|
||||
DeclTypes: []*apiservercel.DeclType{
|
||||
namespaceType,
|
||||
requestType,
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("environment misconfigured: %w", err)
|
||||
}
|
||||
if opts.StrictCost {
|
||||
extended, err = extended.Extend(environment.StrictCostOpt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("environment misconfigured: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if opts.HasPatchTypes {
|
||||
extended, err = extended.Extend(hasPatchTypes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("environment misconfigured: %w", err)
|
||||
}
|
||||
}
|
||||
return extended, nil
|
||||
}
|
||||
|
||||
var hasPatchTypes = environment.VersionedOptions{
|
||||
// Feature epoch was actually 1.32, but we artificially set it to 1.0 because these
|
||||
// options should always be present.
|
||||
IntroducedVersion: version.MajorMinor(1, 0),
|
||||
EnvOptions: []cel.EnvOption{
|
||||
common.ResolverEnvOption(&mutation.DynamicTypeResolver{}),
|
||||
environment.UnversionedLib(library.JSONPatch), // for jsonPatch.escape() function
|
||||
},
|
||||
}
|
||||
|
|
|
@ -36,15 +36,27 @@ import (
|
|||
|
||||
const VariablesTypeName = "kubernetes.variables"
|
||||
|
||||
// CompositedCompiler compiles expressions with variable composition.
|
||||
type CompositedCompiler struct {
|
||||
Compiler
|
||||
FilterCompiler
|
||||
ConditionCompiler
|
||||
MutatingCompiler
|
||||
|
||||
CompositionEnv *CompositionEnv
|
||||
}
|
||||
|
||||
type CompositedFilter struct {
|
||||
Filter
|
||||
// CompositedConditionEvaluator provides evaluation of a condition expression with variable composition.
|
||||
// The expressions must return a boolean.
|
||||
type CompositedConditionEvaluator struct {
|
||||
ConditionEvaluator
|
||||
|
||||
compositionEnv *CompositionEnv
|
||||
}
|
||||
|
||||
// CompositedEvaluator provides evaluation of a single expression with variable composition.
|
||||
// The types that may returned by the expression is determined at compilation time.
|
||||
type CompositedEvaluator struct {
|
||||
MutatingEvaluator
|
||||
|
||||
compositionEnv *CompositionEnv
|
||||
}
|
||||
|
@ -64,11 +76,13 @@ func NewCompositedCompilerFromTemplate(context *CompositionEnv) *CompositedCompi
|
|||
CompiledVariables: map[string]CompilationResult{},
|
||||
}
|
||||
compiler := NewCompiler(context.EnvSet)
|
||||
filterCompiler := NewFilterCompiler(context.EnvSet)
|
||||
conditionCompiler := &conditionCompiler{compiler}
|
||||
mutation := &mutatingCompiler{compiler}
|
||||
return &CompositedCompiler{
|
||||
Compiler: compiler,
|
||||
FilterCompiler: filterCompiler,
|
||||
CompositionEnv: context,
|
||||
Compiler: compiler,
|
||||
ConditionCompiler: conditionCompiler,
|
||||
MutatingCompiler: mutation,
|
||||
CompositionEnv: context,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,11 +99,20 @@ func (c *CompositedCompiler) CompileAndStoreVariable(variable NamedExpressionAcc
|
|||
return result
|
||||
}
|
||||
|
||||
func (c *CompositedCompiler) Compile(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) Filter {
|
||||
filter := c.FilterCompiler.Compile(expressions, optionalDecls, envType)
|
||||
return &CompositedFilter{
|
||||
Filter: filter,
|
||||
compositionEnv: c.CompositionEnv,
|
||||
func (c *CompositedCompiler) CompileCondition(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) ConditionEvaluator {
|
||||
condition := c.ConditionCompiler.CompileCondition(expressions, optionalDecls, envType)
|
||||
return &CompositedConditionEvaluator{
|
||||
ConditionEvaluator: condition,
|
||||
compositionEnv: c.CompositionEnv,
|
||||
}
|
||||
}
|
||||
|
||||
// CompileEvaluator compiles an mutatingEvaluator for the given expression, options and environment.
|
||||
func (c *CompositedCompiler) CompileMutatingEvaluator(expression ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) MutatingEvaluator {
|
||||
mutation := c.MutatingCompiler.CompileMutatingEvaluator(expression, optionalDecls, envType)
|
||||
return &CompositedEvaluator{
|
||||
MutatingEvaluator: mutation,
|
||||
compositionEnv: c.CompositionEnv,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,9 +183,9 @@ func (c *compositionContext) Variables(activation any) ref.Val {
|
|||
return lazyMap
|
||||
}
|
||||
|
||||
func (f *CompositedFilter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) {
|
||||
func (f *CompositedConditionEvaluator) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) {
|
||||
ctx = f.compositionEnv.CreateContext(ctx)
|
||||
return f.Filter.ForInput(ctx, versionedAttr, request, optionalVars, namespace, runtimeCELCostBudget)
|
||||
return f.ConditionEvaluator.ForInput(ctx, versionedAttr, request, optionalVars, namespace, runtimeCELCostBudget)
|
||||
}
|
||||
|
||||
func (c *compositionContext) reportCost(cost int64) {
|
||||
|
|
|
@ -223,8 +223,8 @@ func TestCompositedPolicies(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
compiler.CompileAndStoreVariables(tc.variables, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: tc.strictCostEnforcement}, environment.NewExpressions)
|
||||
validations := []ExpressionAccessor{&condition{Expression: tc.expression}}
|
||||
f := compiler.Compile(validations, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: tc.strictCostEnforcement}, environment.NewExpressions)
|
||||
validations := []ExpressionAccessor{&testCondition{Expression: tc.expression}}
|
||||
f := compiler.CompileCondition(validations, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: tc.strictCostEnforcement}, environment.NewExpressions)
|
||||
versionedAttr, err := admission.NewVersionedAttributes(tc.attributes, tc.attributes.GetKind(), newObjectInterfacesForTest())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
|
|
@ -0,0 +1,216 @@
|
|||
/*
|
||||
Copyright 2022 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 cel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
)
|
||||
|
||||
// conditionCompiler implement the interface ConditionCompiler.
|
||||
type conditionCompiler struct {
|
||||
compiler Compiler
|
||||
}
|
||||
|
||||
func NewConditionCompiler(env *environment.EnvSet) ConditionCompiler {
|
||||
return &conditionCompiler{compiler: NewCompiler(env)}
|
||||
}
|
||||
|
||||
// CompileCondition compiles the cel expressions defined in the ExpressionAccessors into a ConditionEvaluator
|
||||
func (c *conditionCompiler) CompileCondition(expressionAccessors []ExpressionAccessor, options OptionalVariableDeclarations, mode environment.Type) ConditionEvaluator {
|
||||
compilationResults := make([]CompilationResult, len(expressionAccessors))
|
||||
for i, expressionAccessor := range expressionAccessors {
|
||||
if expressionAccessor == nil {
|
||||
continue
|
||||
}
|
||||
compilationResults[i] = c.compiler.CompileCELExpression(expressionAccessor, options, mode)
|
||||
}
|
||||
return NewCondition(compilationResults)
|
||||
}
|
||||
|
||||
// condition implements the ConditionEvaluator interface
|
||||
type condition struct {
|
||||
compilationResults []CompilationResult
|
||||
}
|
||||
|
||||
func NewCondition(compilationResults []CompilationResult) ConditionEvaluator {
|
||||
return &condition{
|
||||
compilationResults,
|
||||
}
|
||||
}
|
||||
|
||||
func convertObjectToUnstructured(obj interface{}) (*unstructured.Unstructured, error) {
|
||||
if obj == nil || reflect.ValueOf(obj).IsNil() {
|
||||
return &unstructured.Unstructured{Object: nil}, nil
|
||||
}
|
||||
ret, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &unstructured.Unstructured{Object: ret}, nil
|
||||
}
|
||||
|
||||
func objectToResolveVal(r runtime.Object) (interface{}, error) {
|
||||
if r == nil || reflect.ValueOf(r).IsNil() {
|
||||
return nil, nil
|
||||
}
|
||||
v, err := convertObjectToUnstructured(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v.Object, nil
|
||||
}
|
||||
|
||||
// ForInput evaluates the compiled CEL expressions converting them into CELEvaluations
|
||||
// errors per evaluation are returned on the Evaluation object
|
||||
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
|
||||
func (c *condition) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, namespace *v1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) {
|
||||
// TODO: replace unstructured with ref.Val for CEL variables when native type support is available
|
||||
evaluations := make([]EvaluationResult, len(c.compilationResults))
|
||||
var err error
|
||||
|
||||
// if this activation supports composition, we will need the compositionCtx. It may be nil.
|
||||
compositionCtx, _ := ctx.(CompositionContext)
|
||||
|
||||
activation, err := newActivation(compositionCtx, versionedAttr, request, inputs, namespace)
|
||||
if err != nil {
|
||||
return nil, -1, err
|
||||
}
|
||||
|
||||
remainingBudget := runtimeCELCostBudget
|
||||
for i, compilationResult := range c.compilationResults {
|
||||
evaluations[i], remainingBudget, err = activation.Evaluate(ctx, compositionCtx, compilationResult, remainingBudget)
|
||||
if err != nil {
|
||||
return nil, -1, err
|
||||
}
|
||||
}
|
||||
|
||||
return evaluations, remainingBudget, nil
|
||||
}
|
||||
|
||||
// TODO: to reuse https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request/admissionreview.go#L154
|
||||
func CreateAdmissionRequest(attr admission.Attributes, equivalentGVR metav1.GroupVersionResource, equivalentKind metav1.GroupVersionKind) *admissionv1.AdmissionRequest {
|
||||
// Attempting to use same logic as webhook for constructing resource
|
||||
// GVK, GVR, subresource
|
||||
// Use the GVK, GVR that the matcher decided was equivalent to that of the request
|
||||
// https://github.com/kubernetes/kubernetes/blob/90c362b3430bcbbf8f245fadbcd521dab39f1d7c/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go#L182-L210
|
||||
gvk := equivalentKind
|
||||
gvr := equivalentGVR
|
||||
subresource := attr.GetSubresource()
|
||||
|
||||
requestGVK := attr.GetKind()
|
||||
requestGVR := attr.GetResource()
|
||||
requestSubResource := attr.GetSubresource()
|
||||
|
||||
aUserInfo := attr.GetUserInfo()
|
||||
var userInfo authenticationv1.UserInfo
|
||||
if aUserInfo != nil {
|
||||
userInfo = authenticationv1.UserInfo{
|
||||
Extra: make(map[string]authenticationv1.ExtraValue),
|
||||
Groups: aUserInfo.GetGroups(),
|
||||
UID: aUserInfo.GetUID(),
|
||||
Username: aUserInfo.GetName(),
|
||||
}
|
||||
// Convert the extra information in the user object
|
||||
for key, val := range aUserInfo.GetExtra() {
|
||||
userInfo.Extra[key] = authenticationv1.ExtraValue(val)
|
||||
}
|
||||
}
|
||||
|
||||
dryRun := attr.IsDryRun()
|
||||
|
||||
return &admissionv1.AdmissionRequest{
|
||||
Kind: metav1.GroupVersionKind{
|
||||
Group: gvk.Group,
|
||||
Kind: gvk.Kind,
|
||||
Version: gvk.Version,
|
||||
},
|
||||
Resource: metav1.GroupVersionResource{
|
||||
Group: gvr.Group,
|
||||
Resource: gvr.Resource,
|
||||
Version: gvr.Version,
|
||||
},
|
||||
SubResource: subresource,
|
||||
RequestKind: &metav1.GroupVersionKind{
|
||||
Group: requestGVK.Group,
|
||||
Kind: requestGVK.Kind,
|
||||
Version: requestGVK.Version,
|
||||
},
|
||||
RequestResource: &metav1.GroupVersionResource{
|
||||
Group: requestGVR.Group,
|
||||
Resource: requestGVR.Resource,
|
||||
Version: requestGVR.Version,
|
||||
},
|
||||
RequestSubResource: requestSubResource,
|
||||
Name: attr.GetName(),
|
||||
Namespace: attr.GetNamespace(),
|
||||
Operation: admissionv1.Operation(attr.GetOperation()),
|
||||
UserInfo: userInfo,
|
||||
// Leave Object and OldObject unset since we don't provide access to them via request
|
||||
DryRun: &dryRun,
|
||||
Options: runtime.RawExtension{
|
||||
Object: attr.GetOperationOptions(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateNamespaceObject creates a Namespace object that is suitable for the CEL evaluation.
|
||||
// If the namespace is nil, CreateNamespaceObject returns nil
|
||||
func CreateNamespaceObject(namespace *v1.Namespace) *v1.Namespace {
|
||||
if namespace == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &v1.Namespace{
|
||||
Status: namespace.Status,
|
||||
Spec: namespace.Spec,
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespace.Name,
|
||||
GenerateName: namespace.GenerateName,
|
||||
Namespace: namespace.Namespace,
|
||||
UID: namespace.UID,
|
||||
ResourceVersion: namespace.ResourceVersion,
|
||||
Generation: namespace.Generation,
|
||||
CreationTimestamp: namespace.CreationTimestamp,
|
||||
DeletionTimestamp: namespace.DeletionTimestamp,
|
||||
DeletionGracePeriodSeconds: namespace.DeletionGracePeriodSeconds,
|
||||
Labels: namespace.Labels,
|
||||
Annotations: namespace.Annotations,
|
||||
Finalizers: namespace.Finalizers,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CompilationErrors returns a list of all the errors from the compilation of the mutatingEvaluator
|
||||
func (c *condition) CompilationErrors() []error {
|
||||
compilationErrors := []error{}
|
||||
for _, result := range c.compilationResults {
|
||||
if result.Error != nil {
|
||||
compilationErrors = append(compilationErrors, result.Error)
|
||||
}
|
||||
}
|
||||
return compilationErrors
|
||||
}
|
|
@ -28,8 +28,6 @@ import (
|
|||
celtypes "github.com/google/cel-go/common/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
pointer "k8s.io/utils/ptr"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
@ -48,17 +46,18 @@ import (
|
|||
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
pointer "k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
type condition struct {
|
||||
type testCondition struct {
|
||||
Expression string
|
||||
}
|
||||
|
||||
func (c *condition) GetExpression() string {
|
||||
return c.Expression
|
||||
func (tc *testCondition) GetExpression() string {
|
||||
return tc.Expression
|
||||
}
|
||||
|
||||
func (v *condition) ReturnTypes() []*celgo.Type {
|
||||
func (tc *testCondition) ReturnTypes() []*celgo.Type {
|
||||
return []*celgo.Type{celgo.BoolType}
|
||||
}
|
||||
|
||||
|
@ -71,10 +70,10 @@ func TestCompile(t *testing.T) {
|
|||
{
|
||||
name: "invalid syntax",
|
||||
validation: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "1 < 'asdf'",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "1 < 2",
|
||||
},
|
||||
},
|
||||
|
@ -85,13 +84,13 @@ func TestCompile(t *testing.T) {
|
|||
{
|
||||
name: "valid syntax",
|
||||
validation: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "1 < 2",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "object.spec.string.matches('[0-9]+')",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "request.kind.group == 'example.com' && request.kind.version == 'v1' && request.kind.kind == 'Fake'",
|
||||
},
|
||||
},
|
||||
|
@ -100,13 +99,13 @@ func TestCompile(t *testing.T) {
|
|||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := filterCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))}
|
||||
e := c.Compile(tc.validation, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: true}, environment.NewExpressions)
|
||||
c := conditionCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))}
|
||||
e := c.CompileCondition(tc.validation, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false, StrictCost: true}, environment.NewExpressions)
|
||||
if e == nil {
|
||||
t.Fatalf("unexpected nil validator")
|
||||
}
|
||||
validations := tc.validation
|
||||
CompilationResults := e.(*filter).compilationResults
|
||||
CompilationResults := e.(*condition).compilationResults
|
||||
require.Equal(t, len(validations), len(CompilationResults))
|
||||
|
||||
meets := make([]bool, len(validations))
|
||||
|
@ -131,7 +130,7 @@ func TestCompile(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestFilter(t *testing.T) {
|
||||
func TestCondition(t *testing.T) {
|
||||
simpleLabelSelector, err := labels.NewRequirement("apple", selection.Equals, []string{"banana"})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@ -185,6 +184,7 @@ func TestFilter(t *testing.T) {
|
|||
|
||||
v130 := version.MajorMinor(1, 30)
|
||||
v131 := version.MajorMinor(1, 31)
|
||||
v127 := version.MajorMinor(1, 27)
|
||||
|
||||
var nilUnstructured *unstructured.Unstructured
|
||||
cases := []struct {
|
||||
|
@ -201,11 +201,12 @@ func TestFilter(t *testing.T) {
|
|||
enableSelectors bool
|
||||
|
||||
compatibilityVersion *version.Version
|
||||
envType environment.Type
|
||||
}{
|
||||
{
|
||||
name: "valid syntax for object",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "has(object.subsets) && object.subsets.size() < 2",
|
||||
},
|
||||
},
|
||||
|
@ -220,7 +221,7 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "valid syntax for metadata",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "object.metadata.name == 'endpoints1'",
|
||||
},
|
||||
},
|
||||
|
@ -235,10 +236,10 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "valid syntax for oldObject",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "oldObject == null",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "object != null",
|
||||
},
|
||||
},
|
||||
|
@ -256,7 +257,7 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "valid syntax for request",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "request.operation == 'CREATE'",
|
||||
},
|
||||
},
|
||||
|
@ -271,7 +272,7 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "valid syntax for configMap",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "request.namespace != params.data.fakeString",
|
||||
},
|
||||
},
|
||||
|
@ -287,7 +288,7 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "test failure",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "object.subsets.size() > 2",
|
||||
},
|
||||
},
|
||||
|
@ -310,10 +311,10 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "test failure with multiple validations",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "has(object.subsets)",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "object.subsets.size() > 2",
|
||||
},
|
||||
},
|
||||
|
@ -332,10 +333,10 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "test failure policy with multiple failed validations",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "oldObject != null",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "object.subsets.size() > 2",
|
||||
},
|
||||
},
|
||||
|
@ -354,10 +355,10 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "test Object null in delete",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "oldObject != null",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "object == null",
|
||||
},
|
||||
},
|
||||
|
@ -376,7 +377,7 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "test runtime error",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "oldObject.x == 100",
|
||||
},
|
||||
},
|
||||
|
@ -392,7 +393,7 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "test against crd param",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "object.subsets.size() < params.spec.testSize",
|
||||
},
|
||||
},
|
||||
|
@ -408,10 +409,10 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "test compile failure",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "fail to compile test",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "object.subsets.size() > params.spec.testSize",
|
||||
},
|
||||
},
|
||||
|
@ -430,7 +431,7 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "test pod",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "object.spec.nodeName == 'testnode'",
|
||||
},
|
||||
},
|
||||
|
@ -446,7 +447,7 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "test deny paramKind without paramRef",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "params != null",
|
||||
},
|
||||
},
|
||||
|
@ -461,7 +462,7 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "test allow paramKind without paramRef",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "params == null",
|
||||
},
|
||||
},
|
||||
|
@ -477,10 +478,10 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "test authorizer allow resource check",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "authorizer.group('').resource('endpoints').check('create').allowed()",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "authorizer.group('').resource('endpoints').check('create').errored()",
|
||||
},
|
||||
},
|
||||
|
@ -503,7 +504,7 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "test authorizer error using fieldSelector with 1.30 compatibility",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "authorizer.group('apps').resource('deployments').fieldSelector('foo=bar').labelSelector('apple=banana').subresource('status').namespace('test').name('backend').check('create').allowed()",
|
||||
},
|
||||
},
|
||||
|
@ -529,13 +530,13 @@ func TestFilter(t *testing.T) {
|
|||
*simpleLabelSelector,
|
||||
},
|
||||
}),
|
||||
enableSelectors: true,
|
||||
enableSelectors: false,
|
||||
compatibilityVersion: v130,
|
||||
},
|
||||
{
|
||||
name: "test authorizer allow resource check with all fields",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "authorizer.group('apps').resource('deployments').fieldSelector('foo=bar').labelSelector('apple=banana').subresource('status').namespace('test').name('backend').check('create').allowed()",
|
||||
},
|
||||
},
|
||||
|
@ -567,7 +568,7 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "test authorizer allow resource check with parse failures",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "authorizer.group('apps').resource('deployments').fieldSelector('foo badoperator bar').labelSelector('apple badoperator banana').subresource('status').namespace('test').name('backend').check('create').allowed()",
|
||||
},
|
||||
},
|
||||
|
@ -595,14 +596,14 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "test authorizer allow resource check with all fields, without gate",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "authorizer.group('apps').resource('deployments').fieldSelector('foo=bar').labelSelector('apple=banana').subresource('status').namespace('test').name('backend').check('create').allowed()",
|
||||
},
|
||||
},
|
||||
attributes: newValidAttribute(&podObject, false),
|
||||
results: []EvaluationResult{
|
||||
{
|
||||
EvalResult: celtypes.True,
|
||||
Error: fmt.Errorf("fieldSelector"),
|
||||
},
|
||||
},
|
||||
authorizer: newAuthzAllowMatch(authorizer.AttributesRecord{
|
||||
|
@ -615,12 +616,13 @@ func TestFilter(t *testing.T) {
|
|||
Verb: "create",
|
||||
APIVersion: "*",
|
||||
}),
|
||||
enableSelectors: false,
|
||||
compatibilityVersion: v131,
|
||||
},
|
||||
{
|
||||
name: "test authorizer not allowed resource check one incorrect field",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
|
||||
Expression: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()",
|
||||
},
|
||||
|
@ -645,7 +647,7 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "test authorizer reason",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "authorizer.group('').resource('endpoints').check('create').reason() == 'fake reason'",
|
||||
},
|
||||
},
|
||||
|
@ -660,13 +662,13 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "test authorizer error",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "authorizer.group('').resource('endpoints').check('create').errored()",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "authorizer.group('').resource('endpoints').check('create').error() == 'fake authz error'",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "authorizer.group('').resource('endpoints').check('create').allowed()",
|
||||
},
|
||||
},
|
||||
|
@ -687,7 +689,7 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "test authorizer allow path check",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "authorizer.path('/healthz').check('get').allowed()",
|
||||
},
|
||||
},
|
||||
|
@ -705,7 +707,7 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "test authorizer decision is denied path check",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "authorizer.path('/healthz').check('get').allowed() == false",
|
||||
},
|
||||
},
|
||||
|
@ -720,7 +722,7 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "test request resource authorizer allow check",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "authorizer.requestResource.check('custom-verb').allowed()",
|
||||
},
|
||||
},
|
||||
|
@ -744,7 +746,7 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "test subresource request resource authorizer allow check",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "authorizer.requestResource.check('custom-verb').allowed()",
|
||||
},
|
||||
},
|
||||
|
@ -768,7 +770,7 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "test serviceAccount authorizer allow check",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "authorizer.serviceAccount('default', 'test-serviceaccount').group('').resource('endpoints').namespace('default').name('endpoints1').check('custom-verb').allowed()",
|
||||
},
|
||||
},
|
||||
|
@ -795,7 +797,7 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "test perCallLimit exceed",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "object.subsets.size() < params.spec.testSize",
|
||||
},
|
||||
},
|
||||
|
@ -812,28 +814,28 @@ func TestFilter(t *testing.T) {
|
|||
{
|
||||
name: "test namespaceObject",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "namespaceObject.metadata.name == 'test'",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "'env' in namespaceObject.metadata.labels && namespaceObject.metadata.labels.env == 'test'",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "('fake' in namespaceObject.metadata.labels) && namespaceObject.metadata.labels.fake == 'test'",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "namespaceObject.spec.finalizers[0] == 'kubernetes'",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "namespaceObject.status.phase == 'Active'",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "size(namespaceObject.metadata.managedFields) == 1",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "size(namespaceObject.metadata.ownerReferences) == 1",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "'env' in namespaceObject.metadata.annotations",
|
||||
},
|
||||
},
|
||||
|
@ -867,13 +869,58 @@ func TestFilter(t *testing.T) {
|
|||
hasParamKind: false,
|
||||
namespaceObject: nsObject,
|
||||
},
|
||||
{
|
||||
name: "cel lib not recognized in version earlier than introduced version",
|
||||
validations: []ExpressionAccessor{
|
||||
&testCondition{
|
||||
Expression: "isQuantity(\"20M\")",
|
||||
},
|
||||
},
|
||||
attributes: newValidAttribute(&podObject, false),
|
||||
results: []EvaluationResult{
|
||||
{
|
||||
Error: fmt.Errorf("isQuantity"),
|
||||
},
|
||||
},
|
||||
compatibilityVersion: v127,
|
||||
},
|
||||
{
|
||||
name: "cel lib recognized in version later than introduced version",
|
||||
validations: []ExpressionAccessor{
|
||||
&testCondition{
|
||||
Expression: "isQuantity(\"20M\")",
|
||||
},
|
||||
},
|
||||
results: []EvaluationResult{
|
||||
{
|
||||
EvalResult: celtypes.True,
|
||||
},
|
||||
},
|
||||
attributes: newValidAttribute(&podObject, false),
|
||||
compatibilityVersion: v130,
|
||||
},
|
||||
{
|
||||
name: "cel lib always recognized in stored expression",
|
||||
validations: []ExpressionAccessor{
|
||||
&testCondition{
|
||||
Expression: "isQuantity(\"20M\")",
|
||||
},
|
||||
},
|
||||
attributes: newValidAttribute(&podObject, false),
|
||||
results: []EvaluationResult{
|
||||
{
|
||||
EvalResult: celtypes.True,
|
||||
},
|
||||
},
|
||||
envType: environment.StoredExpressions,
|
||||
compatibilityVersion: version.MajorMinor(1, 2),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.enableSelectors {
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, true)
|
||||
}
|
||||
environment.DisableBaseEnvSetCachingForTests()
|
||||
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.AuthorizeWithSelectors, tc.enableSelectors)
|
||||
|
||||
if tc.testPerCallLimit == 0 {
|
||||
tc.testPerCallLimit = celconfig.PerCallLimit
|
||||
|
@ -891,13 +938,18 @@ func TestFilter(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c := NewFilterCompiler(env)
|
||||
f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: tc.authorizer != nil, StrictCost: tc.strictCost}, environment.NewExpressions)
|
||||
c := NewConditionCompiler(env)
|
||||
envType := tc.envType
|
||||
if envType == "" {
|
||||
envType = environment.NewExpressions
|
||||
}
|
||||
f := c.CompileCondition(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: tc.authorizer != nil, StrictCost: tc.strictCost}, envType)
|
||||
if f == nil {
|
||||
t.Fatalf("unexpected nil validator")
|
||||
}
|
||||
|
||||
validations := tc.validations
|
||||
CompilationResults := f.(*filter).compilationResults
|
||||
CompilationResults := f.(*condition).compilationResults
|
||||
require.Equal(t, len(validations), len(CompilationResults))
|
||||
|
||||
versionedAttr, err := admission.NewVersionedAttributes(tc.attributes, tc.attributes.GetKind(), newObjectInterfacesForTest())
|
||||
|
@ -913,8 +965,13 @@ func TestFilter(t *testing.T) {
|
|||
}
|
||||
require.Equal(t, len(evalResults), len(tc.results))
|
||||
for i, result := range tc.results {
|
||||
if result.Error != nil && evalResults[i].Error == nil {
|
||||
t.Errorf("Expected error result containing '%v' but got non-error", result.Error)
|
||||
continue
|
||||
}
|
||||
if result.Error != nil && !strings.Contains(evalResults[i].Error.Error(), result.Error.Error()) {
|
||||
t.Errorf("Expected result '%v' but got '%v'", result.Error, evalResults[i].Error)
|
||||
continue
|
||||
}
|
||||
if result.Error == nil && evalResults[i].Error != nil {
|
||||
t.Errorf("Expected result '%v' but got error '%v'", result.EvalResult, evalResults[i].Error)
|
||||
|
@ -954,10 +1011,10 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||
{
|
||||
name: "expression exceed RuntimeCELCostBudget at fist expression",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "has(object.subsets) && object.subsets.size() < 2",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "has(object.subsets)",
|
||||
},
|
||||
},
|
||||
|
@ -969,10 +1026,10 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||
{
|
||||
name: "expression exceed RuntimeCELCostBudget at last expression",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "has(object.subsets) && object.subsets.size() < 2",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "object.subsets.size() > 2",
|
||||
},
|
||||
},
|
||||
|
@ -985,10 +1042,10 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||
{
|
||||
name: "test RuntimeCELCostBudge is not exceed",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "oldObject != null",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "object.subsets.size() > 2",
|
||||
},
|
||||
},
|
||||
|
@ -1002,10 +1059,10 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||
{
|
||||
name: "test RuntimeCELCostBudge exactly covers",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "oldObject != null",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "object.subsets.size() > 2",
|
||||
},
|
||||
},
|
||||
|
@ -1019,13 +1076,13 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||
{
|
||||
name: "test RuntimeCELCostBudge exactly covers then constant",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "oldObject != null",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "object.subsets.size() > 2",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "true", // zero cost
|
||||
},
|
||||
},
|
||||
|
@ -1039,7 +1096,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||
{
|
||||
name: "Extended library cost: authz check",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()",
|
||||
},
|
||||
},
|
||||
|
@ -1053,7 +1110,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||
{
|
||||
name: "Extended library cost: isSorted()",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "[1,2,3,4].isSorted()",
|
||||
},
|
||||
},
|
||||
|
@ -1066,7 +1123,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||
{
|
||||
name: "Extended library cost: url",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "url('https:://kubernetes.io/').getHostname() == 'kubernetes.io'",
|
||||
},
|
||||
},
|
||||
|
@ -1079,7 +1136,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||
{
|
||||
name: "Extended library cost: split",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "size('abc 123 def 123'.split(' ')) > 0",
|
||||
},
|
||||
},
|
||||
|
@ -1092,7 +1149,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||
{
|
||||
name: "Extended library cost: join",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "size(['aa', 'bb', 'cc', 'd', 'e', 'f', 'g', 'h', 'i', 'j'].join(' ')) > 0",
|
||||
},
|
||||
},
|
||||
|
@ -1105,7 +1162,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||
{
|
||||
name: "Extended library cost: find",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "size('abc 123 def 123'.find('123')) > 0",
|
||||
},
|
||||
},
|
||||
|
@ -1118,7 +1175,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||
{
|
||||
name: "Extended library cost: quantity",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "quantity(\"200M\") == quantity(\"0.2G\") && quantity(\"0.2G\") == quantity(\"200M\")",
|
||||
},
|
||||
},
|
||||
|
@ -1131,10 +1188,10 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||
{
|
||||
name: "With StrictCostEnforcementForVAP enabled: expression exceed RuntimeCELCostBudget at fist expression",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "has(object.subsets)",
|
||||
},
|
||||
},
|
||||
|
@ -1148,10 +1205,10 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||
{
|
||||
name: "With StrictCostEnforcementForVAP enabled: expression exceed RuntimeCELCostBudget at last expression",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()",
|
||||
},
|
||||
},
|
||||
|
@ -1165,10 +1222,10 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||
{
|
||||
name: "With StrictCostEnforcementForVAP enabled: test RuntimeCELCostBudge is not exceed",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()",
|
||||
},
|
||||
},
|
||||
|
@ -1184,10 +1241,10 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||
{
|
||||
name: "With StrictCostEnforcementForVAP enabled: test RuntimeCELCostBudge exactly covers",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()",
|
||||
},
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed()",
|
||||
},
|
||||
},
|
||||
|
@ -1203,7 +1260,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||
{
|
||||
name: "With StrictCostEnforcementForVAP enabled: per call limit exceeds",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "!authorizer.group('').resource('endpoints').check('create').allowed() && !authorizer.group('').resource('endpoints').check('create').allowed() && !authorizer.group('').resource('endpoints').check('create').allowed()",
|
||||
},
|
||||
},
|
||||
|
@ -1218,7 +1275,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||
{
|
||||
name: "With StrictCostEnforcementForVAP enabled: Extended library cost: isSorted()",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "[1,2,3,4].isSorted()",
|
||||
},
|
||||
},
|
||||
|
@ -1232,7 +1289,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||
{
|
||||
name: "With StrictCostEnforcementForVAP enabled: Extended library cost: url",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "url('https:://kubernetes.io/').getHostname() == 'kubernetes.io'",
|
||||
},
|
||||
},
|
||||
|
@ -1246,7 +1303,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||
{
|
||||
name: "With StrictCostEnforcementForVAP enabled: Extended library cost: split",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "size('abc 123 def 123'.split(' ')) > 0",
|
||||
},
|
||||
},
|
||||
|
@ -1260,7 +1317,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||
{
|
||||
name: "With StrictCostEnforcementForVAP enabled: Extended library cost: join",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "size(['aa', 'bb', 'cc', 'd', 'e', 'f', 'g', 'h', 'i', 'j'].join(' ')) > 0",
|
||||
},
|
||||
},
|
||||
|
@ -1274,7 +1331,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||
{
|
||||
name: "With StrictCostEnforcementForVAP enabled: Extended library cost: find",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "size('abc 123 def 123'.find('123')) > 0",
|
||||
},
|
||||
},
|
||||
|
@ -1288,7 +1345,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||
{
|
||||
name: "With StrictCostEnforcementForVAP enabled: Extended library cost: quantity",
|
||||
validations: []ExpressionAccessor{
|
||||
&condition{
|
||||
&testCondition{
|
||||
Expression: "quantity(\"200M\") == quantity(\"0.2G\") && quantity(\"0.2G\") == quantity(\"200M\")",
|
||||
},
|
||||
},
|
||||
|
@ -1303,13 +1360,13 @@ func TestRuntimeCELCostBudget(t *testing.T) {
|
|||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
c := filterCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), tc.enableStrictCostEnforcement))}
|
||||
f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: true, StrictCost: tc.enableStrictCostEnforcement}, environment.NewExpressions)
|
||||
c := conditionCompiler{compiler: NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), tc.enableStrictCostEnforcement))}
|
||||
f := c.CompileCondition(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: true, StrictCost: tc.enableStrictCostEnforcement}, environment.NewExpressions)
|
||||
if f == nil {
|
||||
t.Fatalf("unexpected nil validator")
|
||||
}
|
||||
validations := tc.validations
|
||||
CompilationResults := f.(*filter).compilationResults
|
||||
CompilationResults := f.(*condition).compilationResults
|
||||
require.Equal(t, len(validations), len(CompilationResults))
|
||||
|
||||
versionedAttr, err := admission.NewVersionedAttributes(tc.attributes, tc.attributes.GetKind(), newObjectInterfacesForTest())
|
||||
|
@ -1470,7 +1527,7 @@ func TestCompilationErrors(t *testing.T) {
|
|||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
e := filter{
|
||||
e := condition{
|
||||
compilationResults: tc.results,
|
||||
}
|
||||
compilationErrors := e.CompilationErrors()
|
|
@ -1,361 +0,0 @@
|
|||
/*
|
||||
Copyright 2022 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 cel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/google/cel-go/interpreter"
|
||||
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
"k8s.io/apiserver/pkg/cel/library"
|
||||
)
|
||||
|
||||
// filterCompiler implement the interface FilterCompiler.
|
||||
type filterCompiler struct {
|
||||
compiler Compiler
|
||||
}
|
||||
|
||||
func NewFilterCompiler(env *environment.EnvSet) FilterCompiler {
|
||||
return &filterCompiler{compiler: NewCompiler(env)}
|
||||
}
|
||||
|
||||
type evaluationActivation struct {
|
||||
object, oldObject, params, request, namespace, authorizer, requestResourceAuthorizer, variables interface{}
|
||||
}
|
||||
|
||||
// ResolveName returns a value from the activation by qualified name, or false if the name
|
||||
// could not be found.
|
||||
func (a *evaluationActivation) ResolveName(name string) (interface{}, bool) {
|
||||
switch name {
|
||||
case ObjectVarName:
|
||||
return a.object, true
|
||||
case OldObjectVarName:
|
||||
return a.oldObject, true
|
||||
case ParamsVarName:
|
||||
return a.params, true // params may be null
|
||||
case RequestVarName:
|
||||
return a.request, true
|
||||
case NamespaceVarName:
|
||||
return a.namespace, true
|
||||
case AuthorizerVarName:
|
||||
return a.authorizer, a.authorizer != nil
|
||||
case RequestResourceAuthorizerVarName:
|
||||
return a.requestResourceAuthorizer, a.requestResourceAuthorizer != nil
|
||||
case VariableVarName: // variables always present
|
||||
return a.variables, true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
// Parent returns the parent of the current activation, may be nil.
|
||||
// If non-nil, the parent will be searched during resolve calls.
|
||||
func (a *evaluationActivation) Parent() interpreter.Activation {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compile compiles the cel expressions defined in the ExpressionAccessors into a Filter
|
||||
func (c *filterCompiler) Compile(expressionAccessors []ExpressionAccessor, options OptionalVariableDeclarations, mode environment.Type) Filter {
|
||||
compilationResults := make([]CompilationResult, len(expressionAccessors))
|
||||
for i, expressionAccessor := range expressionAccessors {
|
||||
if expressionAccessor == nil {
|
||||
continue
|
||||
}
|
||||
compilationResults[i] = c.compiler.CompileCELExpression(expressionAccessor, options, mode)
|
||||
}
|
||||
return NewFilter(compilationResults)
|
||||
}
|
||||
|
||||
// filter implements the Filter interface
|
||||
type filter struct {
|
||||
compilationResults []CompilationResult
|
||||
}
|
||||
|
||||
func NewFilter(compilationResults []CompilationResult) Filter {
|
||||
return &filter{
|
||||
compilationResults,
|
||||
}
|
||||
}
|
||||
|
||||
func convertObjectToUnstructured(obj interface{}) (*unstructured.Unstructured, error) {
|
||||
if obj == nil || reflect.ValueOf(obj).IsNil() {
|
||||
return &unstructured.Unstructured{Object: nil}, nil
|
||||
}
|
||||
ret, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &unstructured.Unstructured{Object: ret}, nil
|
||||
}
|
||||
|
||||
func objectToResolveVal(r runtime.Object) (interface{}, error) {
|
||||
if r == nil || reflect.ValueOf(r).IsNil() {
|
||||
return nil, nil
|
||||
}
|
||||
v, err := convertObjectToUnstructured(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v.Object, nil
|
||||
}
|
||||
|
||||
// ForInput evaluates the compiled CEL expressions converting them into CELEvaluations
|
||||
// errors per evaluation are returned on the Evaluation object
|
||||
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
|
||||
func (f *filter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, namespace *v1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) {
|
||||
// TODO: replace unstructured with ref.Val for CEL variables when native type support is available
|
||||
evaluations := make([]EvaluationResult, len(f.compilationResults))
|
||||
var err error
|
||||
|
||||
oldObjectVal, err := objectToResolveVal(versionedAttr.VersionedOldObject)
|
||||
if err != nil {
|
||||
return nil, -1, err
|
||||
}
|
||||
objectVal, err := objectToResolveVal(versionedAttr.VersionedObject)
|
||||
if err != nil {
|
||||
return nil, -1, err
|
||||
}
|
||||
var paramsVal, authorizerVal, requestResourceAuthorizerVal any
|
||||
if inputs.VersionedParams != nil {
|
||||
paramsVal, err = objectToResolveVal(inputs.VersionedParams)
|
||||
if err != nil {
|
||||
return nil, -1, err
|
||||
}
|
||||
}
|
||||
|
||||
if inputs.Authorizer != nil {
|
||||
authorizerVal = library.NewAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer)
|
||||
requestResourceAuthorizerVal = library.NewResourceAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer, versionedAttr)
|
||||
}
|
||||
|
||||
requestVal, err := convertObjectToUnstructured(request)
|
||||
if err != nil {
|
||||
return nil, -1, err
|
||||
}
|
||||
namespaceVal, err := objectToResolveVal(namespace)
|
||||
if err != nil {
|
||||
return nil, -1, err
|
||||
}
|
||||
va := &evaluationActivation{
|
||||
object: objectVal,
|
||||
oldObject: oldObjectVal,
|
||||
params: paramsVal,
|
||||
request: requestVal.Object,
|
||||
namespace: namespaceVal,
|
||||
authorizer: authorizerVal,
|
||||
requestResourceAuthorizer: requestResourceAuthorizerVal,
|
||||
}
|
||||
|
||||
// composition is an optional feature that only applies for ValidatingAdmissionPolicy.
|
||||
// check if the context allows composition
|
||||
var compositionCtx CompositionContext
|
||||
var ok bool
|
||||
if compositionCtx, ok = ctx.(CompositionContext); ok {
|
||||
va.variables = compositionCtx.Variables(va)
|
||||
}
|
||||
|
||||
remainingBudget := runtimeCELCostBudget
|
||||
for i, compilationResult := range f.compilationResults {
|
||||
var evaluation = &evaluations[i]
|
||||
if compilationResult.ExpressionAccessor == nil { // in case of placeholder
|
||||
continue
|
||||
}
|
||||
evaluation.ExpressionAccessor = compilationResult.ExpressionAccessor
|
||||
if compilationResult.Error != nil {
|
||||
evaluation.Error = &cel.Error{
|
||||
Type: cel.ErrorTypeInvalid,
|
||||
Detail: fmt.Sprintf("compilation error: %v", compilationResult.Error),
|
||||
Cause: compilationResult.Error,
|
||||
}
|
||||
continue
|
||||
}
|
||||
if compilationResult.Program == nil {
|
||||
evaluation.Error = &cel.Error{
|
||||
Type: cel.ErrorTypeInternal,
|
||||
Detail: fmt.Sprintf("unexpected internal error compiling expression"),
|
||||
}
|
||||
continue
|
||||
}
|
||||
t1 := time.Now()
|
||||
evalResult, evalDetails, err := compilationResult.Program.ContextEval(ctx, va)
|
||||
// budget may be spent due to lazy evaluation of composited variables
|
||||
if compositionCtx != nil {
|
||||
compositionCost := compositionCtx.GetAndResetCost()
|
||||
if compositionCost > remainingBudget {
|
||||
return nil, -1, &cel.Error{
|
||||
Type: cel.ErrorTypeInvalid,
|
||||
Detail: fmt.Sprintf("validation failed due to running out of cost budget, no further validation rules will be run"),
|
||||
Cause: cel.ErrOutOfBudget,
|
||||
}
|
||||
}
|
||||
remainingBudget -= compositionCost
|
||||
}
|
||||
elapsed := time.Since(t1)
|
||||
evaluation.Elapsed = elapsed
|
||||
if evalDetails == nil {
|
||||
return nil, -1, &cel.Error{
|
||||
Type: cel.ErrorTypeInternal,
|
||||
Detail: fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()),
|
||||
}
|
||||
} else {
|
||||
rtCost := evalDetails.ActualCost()
|
||||
if rtCost == nil {
|
||||
return nil, -1, &cel.Error{
|
||||
Type: cel.ErrorTypeInvalid,
|
||||
Detail: fmt.Sprintf("runtime cost could not be calculated for expression: %v, no further expression will be run", compilationResult.ExpressionAccessor.GetExpression()),
|
||||
Cause: cel.ErrOutOfBudget,
|
||||
}
|
||||
} else {
|
||||
if *rtCost > math.MaxInt64 || int64(*rtCost) > remainingBudget {
|
||||
return nil, -1, &cel.Error{
|
||||
Type: cel.ErrorTypeInvalid,
|
||||
Detail: fmt.Sprintf("validation failed due to running out of cost budget, no further validation rules will be run"),
|
||||
Cause: cel.ErrOutOfBudget,
|
||||
}
|
||||
}
|
||||
remainingBudget -= int64(*rtCost)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
evaluation.Error = &cel.Error{
|
||||
Type: cel.ErrorTypeInvalid,
|
||||
Detail: fmt.Sprintf("expression '%v' resulted in error: %v", compilationResult.ExpressionAccessor.GetExpression(), err),
|
||||
}
|
||||
} else {
|
||||
evaluation.EvalResult = evalResult
|
||||
}
|
||||
}
|
||||
|
||||
return evaluations, remainingBudget, nil
|
||||
}
|
||||
|
||||
// TODO: to reuse https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/request/admissionreview.go#L154
|
||||
func CreateAdmissionRequest(attr admission.Attributes, equivalentGVR metav1.GroupVersionResource, equivalentKind metav1.GroupVersionKind) *admissionv1.AdmissionRequest {
|
||||
// Attempting to use same logic as webhook for constructing resource
|
||||
// GVK, GVR, subresource
|
||||
// Use the GVK, GVR that the matcher decided was equivalent to that of the request
|
||||
// https://github.com/kubernetes/kubernetes/blob/90c362b3430bcbbf8f245fadbcd521dab39f1d7c/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/generic/webhook.go#L182-L210
|
||||
gvk := equivalentKind
|
||||
gvr := equivalentGVR
|
||||
subresource := attr.GetSubresource()
|
||||
|
||||
requestGVK := attr.GetKind()
|
||||
requestGVR := attr.GetResource()
|
||||
requestSubResource := attr.GetSubresource()
|
||||
|
||||
aUserInfo := attr.GetUserInfo()
|
||||
var userInfo authenticationv1.UserInfo
|
||||
if aUserInfo != nil {
|
||||
userInfo = authenticationv1.UserInfo{
|
||||
Extra: make(map[string]authenticationv1.ExtraValue),
|
||||
Groups: aUserInfo.GetGroups(),
|
||||
UID: aUserInfo.GetUID(),
|
||||
Username: aUserInfo.GetName(),
|
||||
}
|
||||
// Convert the extra information in the user object
|
||||
for key, val := range aUserInfo.GetExtra() {
|
||||
userInfo.Extra[key] = authenticationv1.ExtraValue(val)
|
||||
}
|
||||
}
|
||||
|
||||
dryRun := attr.IsDryRun()
|
||||
|
||||
return &admissionv1.AdmissionRequest{
|
||||
Kind: metav1.GroupVersionKind{
|
||||
Group: gvk.Group,
|
||||
Kind: gvk.Kind,
|
||||
Version: gvk.Version,
|
||||
},
|
||||
Resource: metav1.GroupVersionResource{
|
||||
Group: gvr.Group,
|
||||
Resource: gvr.Resource,
|
||||
Version: gvr.Version,
|
||||
},
|
||||
SubResource: subresource,
|
||||
RequestKind: &metav1.GroupVersionKind{
|
||||
Group: requestGVK.Group,
|
||||
Kind: requestGVK.Kind,
|
||||
Version: requestGVK.Version,
|
||||
},
|
||||
RequestResource: &metav1.GroupVersionResource{
|
||||
Group: requestGVR.Group,
|
||||
Resource: requestGVR.Resource,
|
||||
Version: requestGVR.Version,
|
||||
},
|
||||
RequestSubResource: requestSubResource,
|
||||
Name: attr.GetName(),
|
||||
Namespace: attr.GetNamespace(),
|
||||
Operation: admissionv1.Operation(attr.GetOperation()),
|
||||
UserInfo: userInfo,
|
||||
// Leave Object and OldObject unset since we don't provide access to them via request
|
||||
DryRun: &dryRun,
|
||||
Options: runtime.RawExtension{
|
||||
Object: attr.GetOperationOptions(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateNamespaceObject creates a Namespace object that is suitable for the CEL evaluation.
|
||||
// If the namespace is nil, CreateNamespaceObject returns nil
|
||||
func CreateNamespaceObject(namespace *v1.Namespace) *v1.Namespace {
|
||||
if namespace == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &v1.Namespace{
|
||||
Status: namespace.Status,
|
||||
Spec: namespace.Spec,
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: namespace.Name,
|
||||
GenerateName: namespace.GenerateName,
|
||||
Namespace: namespace.Namespace,
|
||||
UID: namespace.UID,
|
||||
ResourceVersion: namespace.ResourceVersion,
|
||||
Generation: namespace.Generation,
|
||||
CreationTimestamp: namespace.CreationTimestamp,
|
||||
DeletionTimestamp: namespace.DeletionTimestamp,
|
||||
DeletionGracePeriodSeconds: namespace.DeletionGracePeriodSeconds,
|
||||
Labels: namespace.Labels,
|
||||
Annotations: namespace.Annotations,
|
||||
Finalizers: namespace.Finalizers,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CompilationErrors returns a list of all the errors from the compilation of the evaluator
|
||||
func (e *filter) CompilationErrors() []error {
|
||||
compilationErrors := []error{}
|
||||
for _, result := range e.compilationResults {
|
||||
if result.Error != nil {
|
||||
compilationErrors = append(compilationErrors, result.Error)
|
||||
}
|
||||
}
|
||||
return compilationErrors
|
||||
}
|
|
@ -63,12 +63,15 @@ type OptionalVariableDeclarations struct {
|
|||
HasAuthorizer bool
|
||||
// StrictCost specifies if the CEL cost limitation is strict for extended libraries as well as native libraries.
|
||||
StrictCost bool
|
||||
// HasPatchTypes specifies if JSONPatch, Object, Object.metadata and similar types are available in CEL. These can be used
|
||||
// to initialize the typed objects in CEL required to create patches.
|
||||
HasPatchTypes bool
|
||||
}
|
||||
|
||||
// FilterCompiler contains a function to assist with converting types and values to/from CEL-typed values.
|
||||
type FilterCompiler interface {
|
||||
// Compile is used for the cel expression compilation
|
||||
Compile(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) Filter
|
||||
// ConditionCompiler contains a function to assist with converting types and values to/from CEL-typed values.
|
||||
type ConditionCompiler interface {
|
||||
// CompileCondition is used for the cel expression compilation
|
||||
CompileCondition(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) ConditionEvaluator
|
||||
}
|
||||
|
||||
// OptionalVariableBindings provides expression bindings for optional CEL variables.
|
||||
|
@ -82,16 +85,38 @@ type OptionalVariableBindings struct {
|
|||
Authorizer authorizer.Authorizer
|
||||
}
|
||||
|
||||
// Filter contains a function to evaluate compiled CEL-typed values
|
||||
// ConditionEvaluator contains the result of compiling a CEL expression
|
||||
// that evaluates to a condition. This is used both for validation and pre-conditions.
|
||||
// It expects the inbound object to already have been converted to the version expected
|
||||
// by the underlying CEL code (which is indicated by the match criteria of a policy definition).
|
||||
// versionedParams may be nil.
|
||||
type Filter interface {
|
||||
type ConditionEvaluator interface {
|
||||
// ForInput converts compiled CEL-typed values into evaluated CEL-typed value.
|
||||
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
|
||||
// If cost budget is calculated, the filter should return the remaining budget.
|
||||
// If cost budget is calculated, the condition should return the remaining budget.
|
||||
ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error)
|
||||
|
||||
// CompilationErrors returns a list of errors from the compilation of the evaluator
|
||||
// CompilationErrors returns a list of errors from the compilation of the mutatingEvaluator
|
||||
CompilationErrors() []error
|
||||
}
|
||||
|
||||
// MutatingCompiler contains a function to assist with converting types and values to/from CEL-typed values.
|
||||
type MutatingCompiler interface {
|
||||
// CompileMutatingEvaluator is used for the cel expression compilation
|
||||
CompileMutatingEvaluator(expression ExpressionAccessor, optionalDecls OptionalVariableDeclarations, envType environment.Type) MutatingEvaluator
|
||||
}
|
||||
|
||||
// MutatingEvaluator contains the result of compiling a CEL expression
|
||||
// that evaluates to a mutation.
|
||||
// It expects the inbound object to already have been converted to the version expected
|
||||
// by the underlying CEL code (which is indicated by the match criteria of a policy definition).
|
||||
// versionedParams may be nil.
|
||||
type MutatingEvaluator interface {
|
||||
// ForInput converts compiled CEL-typed values into a CEL-typed value representing a mutation.
|
||||
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
|
||||
// If cost budget is calculated, the condition should return the remaining budget.
|
||||
ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) (EvaluationResult, int64, error)
|
||||
|
||||
// CompilationErrors returns a list of errors from the compilation of the mutatingEvaluator
|
||||
CompilationErrors() []error
|
||||
}
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
Copyright 2024 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 cel
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
)
|
||||
|
||||
// mutatingCompiler provides a MutatingCompiler implementation.
|
||||
type mutatingCompiler struct {
|
||||
compiler Compiler
|
||||
}
|
||||
|
||||
// CompileMutatingEvaluator compiles a CEL expression for admission plugins and returns an MutatingEvaluator for executing the
|
||||
// compiled CEL expression.
|
||||
func (p *mutatingCompiler) CompileMutatingEvaluator(expressionAccessor ExpressionAccessor, options OptionalVariableDeclarations, mode environment.Type) MutatingEvaluator {
|
||||
compilationResult := p.compiler.CompileCELExpression(expressionAccessor, options, mode)
|
||||
return NewMutatingEvaluator(compilationResult)
|
||||
}
|
||||
|
||||
type mutatingEvaluator struct {
|
||||
compilationResult CompilationResult
|
||||
}
|
||||
|
||||
func NewMutatingEvaluator(compilationResult CompilationResult) MutatingEvaluator {
|
||||
return &mutatingEvaluator{compilationResult}
|
||||
}
|
||||
|
||||
// ForInput evaluates the compiled CEL expression and returns an evaluation result
|
||||
// errors per evaluation are returned in the evaluation result
|
||||
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
|
||||
func (p *mutatingEvaluator) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, namespace *v1.Namespace, runtimeCELCostBudget int64) (EvaluationResult, int64, error) {
|
||||
// if this activation supports composition, we will need the compositionCtx. It may be nil.
|
||||
compositionCtx, _ := ctx.(CompositionContext)
|
||||
|
||||
activation, err := newActivation(compositionCtx, versionedAttr, request, inputs, namespace)
|
||||
if err != nil {
|
||||
return EvaluationResult{}, -1, err
|
||||
}
|
||||
evaluation, remainingBudget, err := activation.Evaluate(ctx, compositionCtx, p.compilationResult, runtimeCELCostBudget)
|
||||
if err != nil {
|
||||
return evaluation, -1, err
|
||||
}
|
||||
return evaluation, remainingBudget, nil
|
||||
|
||||
}
|
||||
|
||||
// CompilationErrors returns a list of all the errors from the compilation of the mutatingEvaluator
|
||||
func (p *mutatingEvaluator) CompilationErrors() (compilationErrors []error) {
|
||||
if p.compilationResult.Error != nil {
|
||||
return []error{p.compilationResult.Error}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -26,6 +26,7 @@ type PolicyAccessor interface {
|
|||
GetNamespace() string
|
||||
GetParamKind() *v1.ParamKind
|
||||
GetMatchConstraints() *v1.MatchResources
|
||||
GetFailurePolicy() *v1.FailurePolicyType
|
||||
}
|
||||
|
||||
type BindingAccessor interface {
|
||||
|
|
|
@ -49,6 +49,9 @@ type Source[H Hook] interface {
|
|||
// Dispatcher dispatches evaluates an admission request against the currently
|
||||
// active hooks returned by the source.
|
||||
type Dispatcher[H Hook] interface {
|
||||
// Start the dispatcher. This method should be called only once at startup.
|
||||
Start(ctx context.Context) error
|
||||
|
||||
// Dispatch a request to the policies. Dispatcher may choose not to
|
||||
// call a hook, either because the rules of the hook does not match, or
|
||||
// the namespaceSelector or the objectSelector of the hook does not
|
||||
|
|
|
@ -36,8 +36,9 @@ import (
|
|||
)
|
||||
|
||||
// H is the Hook type generated by the source and consumed by the dispatcher.
|
||||
// !TODO: Just pass in a Plugin[H] with accessors to all this information
|
||||
type sourceFactory[H any] func(informers.SharedInformerFactory, kubernetes.Interface, dynamic.Interface, meta.RESTMapper) Source[H]
|
||||
type dispatcherFactory[H any] func(authorizer.Authorizer, *matching.Matcher) Dispatcher[H]
|
||||
type dispatcherFactory[H any] func(authorizer.Authorizer, *matching.Matcher, kubernetes.Interface) Dispatcher[H]
|
||||
|
||||
// admissionResources is the list of resources related to CEL-based admission
|
||||
// features.
|
||||
|
@ -170,7 +171,7 @@ func (c *Plugin[H]) ValidateInitialization() error {
|
|||
}
|
||||
|
||||
c.source = c.sourceFactory(c.informerFactory, c.client, c.dynamicClient, c.restMapper)
|
||||
c.dispatcher = c.dispatcherFactory(c.authorizer, c.matcher)
|
||||
c.dispatcher = c.dispatcherFactory(c.authorizer, c.matcher, c.client)
|
||||
|
||||
pluginContext, pluginContextCancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
|
@ -181,10 +182,15 @@ func (c *Plugin[H]) ValidateInitialization() error {
|
|||
go func() {
|
||||
err := c.source.Run(pluginContext)
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
utilruntime.HandleError(fmt.Errorf("policy source context unexpectedly closed: %v", err))
|
||||
utilruntime.HandleError(fmt.Errorf("policy source context unexpectedly closed: %w", err))
|
||||
}
|
||||
}()
|
||||
|
||||
err := c.dispatcher.Start(pluginContext)
|
||||
if err != nil && !errors.Is(err, context.Canceled) {
|
||||
utilruntime.HandleError(fmt.Errorf("policy dispatcher context unexpectedly closed: %w", err))
|
||||
}
|
||||
|
||||
c.SetReadyFunc(func() bool {
|
||||
return namespaceInformer.Informer().HasSynced() && c.source.HasSynced()
|
||||
})
|
||||
|
|
|
@ -36,7 +36,7 @@ import (
|
|||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
// A policy invocation is a single policy-binding-param tuple from a Policy Hook
|
||||
// PolicyInvocation is a single policy-binding-param tuple from a Policy Hook
|
||||
// in the context of a specific request. The params have already been resolved
|
||||
// and any error in configuration or setting up the invocation is stored in
|
||||
// the Error field.
|
||||
|
@ -62,10 +62,6 @@ type PolicyInvocation[P runtime.Object, B runtime.Object, E Evaluator] struct {
|
|||
|
||||
// Params fetched by the binding to use to evaluate the policy
|
||||
Param runtime.Object
|
||||
|
||||
// Error is set if there was an error with the policy or binding or its
|
||||
// params, etc
|
||||
Error error
|
||||
}
|
||||
|
||||
// dispatcherDelegate is called during a request with a pre-filtered list
|
||||
|
@ -76,7 +72,7 @@ type PolicyInvocation[P runtime.Object, B runtime.Object, E Evaluator] struct {
|
|||
//
|
||||
// The delegate provides the "validation" or "mutation" aspect of dispatcher functionality
|
||||
// (in contrast to generic.PolicyDispatcher which only selects active policies and params)
|
||||
type dispatcherDelegate[P, B runtime.Object, E Evaluator] func(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, versionedAttributes webhookgeneric.VersionedAttributeAccessor, invocations []PolicyInvocation[P, B, E]) error
|
||||
type dispatcherDelegate[P, B runtime.Object, E Evaluator] func(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, versionedAttributes webhookgeneric.VersionedAttributeAccessor, invocations []PolicyInvocation[P, B, E]) ([]PolicyError, *apierrors.StatusError)
|
||||
|
||||
type policyDispatcher[P runtime.Object, B runtime.Object, E Evaluator] struct {
|
||||
newPolicyAccessor func(P) PolicyAccessor
|
||||
|
@ -104,7 +100,10 @@ func NewPolicyDispatcher[P runtime.Object, B runtime.Object, E Evaluator](
|
|||
// request. It then resolves all params and creates an Invocation for each
|
||||
// matching policy-binding-param tuple. The delegate is then called with the
|
||||
// list of tuples.
|
||||
//
|
||||
func (d *policyDispatcher[P, B, E]) Start(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Note: MatchConditions expressions are not evaluated here. The dispatcher delegate
|
||||
// is expected to ignore the result of any policies whose match conditions dont pass.
|
||||
// This may be possible to refactor so matchconditions are checked here instead.
|
||||
|
@ -117,29 +116,33 @@ func (d *policyDispatcher[P, B, E]) Dispatch(ctx context.Context, a admission.At
|
|||
objectInterfaces: o,
|
||||
}
|
||||
|
||||
var policyErrors []PolicyError
|
||||
addConfigError := func(err error, definition PolicyAccessor, binding BindingAccessor) {
|
||||
var message error
|
||||
if binding == nil {
|
||||
message = fmt.Errorf("failed to configure policy: %w", err)
|
||||
} else {
|
||||
message = fmt.Errorf("failed to configure binding: %w", err)
|
||||
}
|
||||
|
||||
policyErrors = append(policyErrors, PolicyError{
|
||||
Policy: definition,
|
||||
Binding: binding,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
for _, hook := range hooks {
|
||||
policyAccessor := d.newPolicyAccessor(hook.Policy)
|
||||
matches, matchGVR, matchGVK, err := d.matcher.DefinitionMatches(a, o, policyAccessor)
|
||||
if err != nil {
|
||||
// There was an error evaluating if this policy matches anything.
|
||||
utilruntime.HandleError(err)
|
||||
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
|
||||
Policy: hook.Policy,
|
||||
Error: err,
|
||||
})
|
||||
addConfigError(err, policyAccessor, nil)
|
||||
continue
|
||||
} else if !matches {
|
||||
continue
|
||||
} else if hook.ConfigurationError != nil {
|
||||
// The policy matches but there is a configuration error with the
|
||||
// policy itself
|
||||
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
|
||||
Policy: hook.Policy,
|
||||
Error: hook.ConfigurationError,
|
||||
Resource: matchGVR,
|
||||
Kind: matchGVK,
|
||||
})
|
||||
utilruntime.HandleError(hook.ConfigurationError)
|
||||
addConfigError(hook.ConfigurationError, policyAccessor, nil)
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -148,19 +151,22 @@ func (d *policyDispatcher[P, B, E]) Dispatch(ctx context.Context, a admission.At
|
|||
matches, err = d.matcher.BindingMatches(a, o, bindingAccessor)
|
||||
if err != nil {
|
||||
// There was an error evaluating if this binding matches anything.
|
||||
utilruntime.HandleError(err)
|
||||
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
|
||||
Policy: hook.Policy,
|
||||
Binding: binding,
|
||||
Error: err,
|
||||
Resource: matchGVR,
|
||||
Kind: matchGVK,
|
||||
})
|
||||
addConfigError(err, policyAccessor, bindingAccessor)
|
||||
continue
|
||||
} else if !matches {
|
||||
continue
|
||||
}
|
||||
|
||||
// here the binding matches.
|
||||
// VersionedAttr result will be cached and reused later during parallel
|
||||
// hook calls.
|
||||
if _, err = versionedAttrAccessor.VersionedAttribute(matchGVK); err != nil {
|
||||
// VersionedAttr result will be cached and reused later during parallel
|
||||
// hook calls.
|
||||
addConfigError(err, policyAccessor, nil)
|
||||
continue
|
||||
}
|
||||
|
||||
// Collect params for this binding
|
||||
params, err := CollectParams(
|
||||
policyAccessor.GetParamKind(),
|
||||
|
@ -171,14 +177,7 @@ func (d *policyDispatcher[P, B, E]) Dispatch(ctx context.Context, a admission.At
|
|||
)
|
||||
if err != nil {
|
||||
// There was an error collecting params for this binding.
|
||||
utilruntime.HandleError(err)
|
||||
relevantHooks = append(relevantHooks, PolicyInvocation[P, B, E]{
|
||||
Policy: hook.Policy,
|
||||
Binding: binding,
|
||||
Error: err,
|
||||
Resource: matchGVR,
|
||||
Kind: matchGVK,
|
||||
})
|
||||
addConfigError(err, policyAccessor, bindingAccessor)
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -194,23 +193,72 @@ func (d *policyDispatcher[P, B, E]) Dispatch(ctx context.Context, a admission.At
|
|||
Evaluator: hook.Evaluator,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VersionedAttr result will be cached and reused later during parallel
|
||||
// hook calls
|
||||
_, err = versionedAttrAccessor.VersionedAttribute(matchGVK)
|
||||
if err != nil {
|
||||
return apierrors.NewInternalError(err)
|
||||
}
|
||||
if len(relevantHooks) > 0 {
|
||||
extraPolicyErrors, statusError := d.delegate(ctx, a, o, versionedAttrAccessor, relevantHooks)
|
||||
if statusError != nil {
|
||||
return statusError
|
||||
}
|
||||
policyErrors = append(policyErrors, extraPolicyErrors...)
|
||||
}
|
||||
|
||||
var filteredErrors []PolicyError
|
||||
for _, e := range policyErrors {
|
||||
// we always default the FailurePolicy if it is unset and validate it in API level
|
||||
var policy v1.FailurePolicyType
|
||||
if fp := e.Policy.GetFailurePolicy(); fp == nil {
|
||||
policy = v1.Fail
|
||||
} else {
|
||||
policy = *fp
|
||||
}
|
||||
|
||||
switch policy {
|
||||
case v1.Ignore:
|
||||
// TODO: add metrics for ignored error here
|
||||
continue
|
||||
case v1.Fail:
|
||||
filteredErrors = append(filteredErrors, e)
|
||||
default:
|
||||
filteredErrors = append(filteredErrors, e)
|
||||
}
|
||||
}
|
||||
|
||||
if len(relevantHooks) == 0 {
|
||||
// no matching hooks
|
||||
return nil
|
||||
if len(filteredErrors) > 0 {
|
||||
|
||||
forbiddenErr := admission.NewForbidden(a, fmt.Errorf("admission request denied by policy"))
|
||||
|
||||
// The forbiddenErr is always a StatusError.
|
||||
var err *apierrors.StatusError
|
||||
if !errors.As(forbiddenErr, &err) {
|
||||
// Should never happen.
|
||||
return apierrors.NewInternalError(fmt.Errorf("failed to create status error"))
|
||||
}
|
||||
err.ErrStatus.Message = ""
|
||||
|
||||
for _, policyError := range filteredErrors {
|
||||
message := policyError.Error()
|
||||
|
||||
// If this is the first denied decision, use its message and reason
|
||||
// for the status error message.
|
||||
if err.ErrStatus.Message == "" {
|
||||
err.ErrStatus.Message = message
|
||||
if policyError.Reason != "" {
|
||||
err.ErrStatus.Reason = policyError.Reason
|
||||
}
|
||||
}
|
||||
|
||||
// Add the denied decision's message to the status error's details
|
||||
err.ErrStatus.Details.Causes = append(
|
||||
err.ErrStatus.Details.Causes,
|
||||
metav1.StatusCause{Message: message})
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
return d.delegate(ctx, a, o, versionedAttrAccessor, relevantHooks)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns params to use to evaluate a policy-binding with given param
|
||||
|
@ -352,3 +400,18 @@ func (v *versionedAttributeAccessor) VersionedAttribute(gvk schema.GroupVersionK
|
|||
v.versionedAttrs[gvk] = versionedAttr
|
||||
return versionedAttr, nil
|
||||
}
|
||||
|
||||
type PolicyError struct {
|
||||
Policy PolicyAccessor
|
||||
Binding BindingAccessor
|
||||
Message error
|
||||
Reason metav1.StatusReason
|
||||
}
|
||||
|
||||
func (c PolicyError) Error() string {
|
||||
if c.Binding != nil {
|
||||
return fmt.Sprintf("policy '%s' with binding '%s' denied request: %s", c.Policy.GetName(), c.Binding.GetName(), c.Message.Error())
|
||||
}
|
||||
|
||||
return fmt.Sprintf("policy %q denied request: %s", c.Policy.GetName(), c.Message.Error())
|
||||
}
|
||||
|
|
|
@ -41,6 +41,13 @@ import (
|
|||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
// Interval for refreshing policies.
|
||||
// TODO: Consider reducing this to a shorter duration or replacing this entirely
|
||||
// with checks that detect when a policy change took effect.
|
||||
const policyRefreshIntervalDefault = 1 * time.Second
|
||||
|
||||
var policyRefreshInterval = policyRefreshIntervalDefault
|
||||
|
||||
type policySource[P runtime.Object, B runtime.Object, E Evaluator] struct {
|
||||
ctx context.Context
|
||||
policyInformer generic.Informer[P]
|
||||
|
@ -122,6 +129,15 @@ func NewPolicySource[P runtime.Object, B runtime.Object, E Evaluator](
|
|||
return res
|
||||
}
|
||||
|
||||
// SetPolicyRefreshIntervalForTests allows the refresh interval to be overridden during tests.
|
||||
// This should only be called from tests.
|
||||
func SetPolicyRefreshIntervalForTests(interval time.Duration) func() {
|
||||
policyRefreshInterval = interval
|
||||
return func() {
|
||||
policyRefreshInterval = policyRefreshIntervalDefault
|
||||
}
|
||||
}
|
||||
|
||||
func (s *policySource[P, B, E]) Run(ctx context.Context) error {
|
||||
if s.ctx != nil {
|
||||
return fmt.Errorf("policy source already running")
|
||||
|
@ -178,7 +194,7 @@ func (s *policySource[P, B, E]) Run(ctx context.Context) error {
|
|||
// and needs to be recompiled
|
||||
go func() {
|
||||
// Loop every 1 second until context is cancelled, refreshing policies
|
||||
wait.Until(s.refreshPolicies, 1*time.Second, ctx.Done())
|
||||
wait.Until(s.refreshPolicies, policyRefreshInterval, ctx.Done())
|
||||
}()
|
||||
|
||||
<-ctx.Done()
|
||||
|
|
|
@ -17,22 +17,35 @@ limitations under the License.
|
|||
package generic_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"k8s.io/api/admissionregistration/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
func makeTestDispatcher(authorizer.Authorizer, *matching.Matcher) generic.Dispatcher[generic.PolicyHook[*FakePolicy, *FakeBinding, generic.Evaluator]] {
|
||||
type fakeDispatcher struct{}
|
||||
|
||||
func (fd *fakeDispatcher) Dispatch(context.Context, admission.Attributes, admission.ObjectInterfaces, []generic.PolicyHook[*FakePolicy, *FakeBinding, generic.Evaluator]) error {
|
||||
return nil
|
||||
}
|
||||
func (fd *fakeDispatcher) Start(context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeTestDispatcher(authorizer.Authorizer, *matching.Matcher, kubernetes.Interface) generic.Dispatcher[generic.PolicyHook[*FakePolicy, *FakeBinding, generic.Evaluator]] {
|
||||
return &fakeDispatcher{}
|
||||
}
|
||||
|
||||
func TestPolicySourceHasSyncedEmpty(t *testing.T) {
|
||||
testContext, testCancel, err := generic.NewPolicyTestContext(
|
||||
|
@ -207,6 +220,10 @@ func (fb *FakePolicy) GetMatchConstraints() *v1.MatchResources {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (fb *FakePolicy) GetFailurePolicy() *v1.FailurePolicyType {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fb *FakeBinding) GetName() string {
|
||||
return fb.Name
|
||||
}
|
||||
|
|
|
@ -45,7 +45,6 @@ import (
|
|||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/initializer"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
)
|
||||
|
||||
// PolicyTestContext is everything you need to unit test a policy plugin
|
||||
|
@ -196,18 +195,6 @@ func NewPolicyTestContext[P, B runtime.Object, E Evaluator](
|
|||
plugin.SetEnabled(true)
|
||||
|
||||
featureGate := featuregate.NewFeatureGate()
|
||||
err = featureGate.Add(map[featuregate.Feature]featuregate.FeatureSpec{
|
||||
//!TODO: move this to validating specific tests
|
||||
features.ValidatingAdmissionPolicy: {
|
||||
Default: true, PreRelease: featuregate.Beta}})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
err = featureGate.SetFromMap(map[string]bool{string(features.ValidatingAdmissionPolicy): true})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
testContext, testCancel := context.WithCancel(context.Background())
|
||||
genericInitializer := initializer.New(
|
||||
nativeClient,
|
||||
|
|
|
@ -199,7 +199,9 @@ func TestReconcile(t *testing.T) {
|
|||
go func() {
|
||||
defer wg.Done()
|
||||
stopReason := myController.Run(testContext)
|
||||
require.ErrorIs(t, stopReason, context.Canceled)
|
||||
if !errors.Is(stopReason, context.Canceled) {
|
||||
t.Errorf("expected error to be context.Canceled, but got: %v", stopReason)
|
||||
}
|
||||
}()
|
||||
|
||||
// The controller is blocked because the reconcile function sends on an
|
||||
|
@ -255,7 +257,9 @@ func TestShutdown(t *testing.T) {
|
|||
go func() {
|
||||
defer wg.Done()
|
||||
stopReason := myController.Run(testContext)
|
||||
require.ErrorIs(t, stopReason, context.Canceled)
|
||||
if !errors.Is(stopReason, context.Canceled) {
|
||||
t.Errorf("expected error to be context.Canceled, but got: %v", stopReason)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for controller and informer to start up
|
||||
|
@ -287,7 +291,9 @@ func TestInformerNeverStarts(t *testing.T) {
|
|||
go func() {
|
||||
defer wg.Done()
|
||||
stopReason := myController.Run(testContext)
|
||||
require.ErrorIs(t, stopReason, context.DeadlineExceeded)
|
||||
if !errors.Is(stopReason, context.DeadlineExceeded) {
|
||||
t.Errorf("expected error to be context.Canceled, but got: %v", stopReason)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for deadline to pass without syncing the cache
|
||||
|
@ -335,7 +341,9 @@ func TestIgnoredUpdate(t *testing.T) {
|
|||
go func() {
|
||||
defer wg.Done()
|
||||
stopReason := myController.Run(testContext)
|
||||
require.ErrorIs(t, stopReason, context.Canceled)
|
||||
if !errors.Is(stopReason, context.Canceled) {
|
||||
t.Errorf("expected error to be context.Canceled, but got: %v", stopReason)
|
||||
}
|
||||
}()
|
||||
|
||||
// The controller is blocked because the reconcile function sends on an
|
||||
|
@ -392,7 +400,9 @@ func TestReconcileRetry(t *testing.T) {
|
|||
go func() {
|
||||
defer wg.Done()
|
||||
stopReason := myController.Run(testContext)
|
||||
require.ErrorIs(t, stopReason, context.Canceled)
|
||||
if !errors.Is(stopReason, context.Canceled) {
|
||||
t.Errorf("expected error to be context.Canceled, but got: %v", stopReason)
|
||||
}
|
||||
}()
|
||||
|
||||
// Add object to informer
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
Copyright 2024 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 mutating
|
||||
|
||||
import (
|
||||
v1 "k8s.io/api/admissionregistration/v1"
|
||||
"k8s.io/api/admissionregistration/v1alpha1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
|
||||
)
|
||||
|
||||
func NewMutatingAdmissionPolicyAccessor(obj *Policy) generic.PolicyAccessor {
|
||||
return &mutatingAdmissionPolicyAccessor{
|
||||
Policy: obj,
|
||||
}
|
||||
}
|
||||
|
||||
func NewMutatingAdmissionPolicyBindingAccessor(obj *PolicyBinding) generic.BindingAccessor {
|
||||
return &mutatingAdmissionPolicyBindingAccessor{
|
||||
PolicyBinding: obj,
|
||||
}
|
||||
}
|
||||
|
||||
type mutatingAdmissionPolicyAccessor struct {
|
||||
*Policy
|
||||
}
|
||||
|
||||
func (v *mutatingAdmissionPolicyAccessor) GetNamespace() string {
|
||||
return v.Namespace
|
||||
}
|
||||
|
||||
func (v *mutatingAdmissionPolicyAccessor) GetName() string {
|
||||
return v.Name
|
||||
}
|
||||
|
||||
func (v *mutatingAdmissionPolicyAccessor) GetParamKind() *v1.ParamKind {
|
||||
pk := v.Spec.ParamKind
|
||||
if pk == nil {
|
||||
return nil
|
||||
}
|
||||
return &v1.ParamKind{
|
||||
APIVersion: pk.APIVersion,
|
||||
Kind: pk.Kind,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *mutatingAdmissionPolicyAccessor) GetMatchConstraints() *v1.MatchResources {
|
||||
return convertV1alpha1ResourceRulesToV1(v.Spec.MatchConstraints)
|
||||
}
|
||||
|
||||
func (v *mutatingAdmissionPolicyAccessor) GetFailurePolicy() *v1.FailurePolicyType {
|
||||
return toV1FailurePolicy(v.Spec.FailurePolicy)
|
||||
}
|
||||
|
||||
func toV1FailurePolicy(failurePolicy *v1alpha1.FailurePolicyType) *v1.FailurePolicyType {
|
||||
if failurePolicy == nil {
|
||||
return nil
|
||||
}
|
||||
fp := v1.FailurePolicyType(*failurePolicy)
|
||||
return &fp
|
||||
}
|
||||
|
||||
type mutatingAdmissionPolicyBindingAccessor struct {
|
||||
*PolicyBinding
|
||||
}
|
||||
|
||||
func (v *mutatingAdmissionPolicyBindingAccessor) GetNamespace() string {
|
||||
return v.Namespace
|
||||
}
|
||||
|
||||
func (v *mutatingAdmissionPolicyBindingAccessor) GetName() string {
|
||||
return v.Name
|
||||
}
|
||||
|
||||
func (v *mutatingAdmissionPolicyBindingAccessor) GetPolicyName() types.NamespacedName {
|
||||
return types.NamespacedName{
|
||||
Namespace: "",
|
||||
Name: v.Spec.PolicyName,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *mutatingAdmissionPolicyBindingAccessor) GetMatchResources() *v1.MatchResources {
|
||||
return convertV1alpha1ResourceRulesToV1(v.Spec.MatchResources)
|
||||
}
|
||||
|
||||
func (v *mutatingAdmissionPolicyBindingAccessor) GetParamRef() *v1.ParamRef {
|
||||
if v.Spec.ParamRef == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var nfa *v1.ParameterNotFoundActionType
|
||||
if v.Spec.ParamRef.ParameterNotFoundAction != nil {
|
||||
nfa = new(v1.ParameterNotFoundActionType)
|
||||
*nfa = v1.ParameterNotFoundActionType(*v.Spec.ParamRef.ParameterNotFoundAction)
|
||||
}
|
||||
|
||||
return &v1.ParamRef{
|
||||
Name: v.Spec.ParamRef.Name,
|
||||
Namespace: v.Spec.ParamRef.Namespace,
|
||||
Selector: v.Spec.ParamRef.Selector,
|
||||
ParameterNotFoundAction: nfa,
|
||||
}
|
||||
}
|
||||
|
||||
func convertV1alpha1ResourceRulesToV1(mc *v1alpha1.MatchResources) *v1.MatchResources {
|
||||
if mc == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var res v1.MatchResources
|
||||
res.NamespaceSelector = mc.NamespaceSelector
|
||||
res.ObjectSelector = mc.ObjectSelector
|
||||
for _, ex := range mc.ExcludeResourceRules {
|
||||
res.ExcludeResourceRules = append(res.ExcludeResourceRules, v1.NamedRuleWithOperations{
|
||||
ResourceNames: ex.ResourceNames,
|
||||
RuleWithOperations: ex.RuleWithOperations,
|
||||
})
|
||||
}
|
||||
for _, ex := range mc.ResourceRules {
|
||||
res.ResourceRules = append(res.ResourceRules, v1.NamedRuleWithOperations{
|
||||
ResourceNames: ex.ResourceNames,
|
||||
RuleWithOperations: ex.RuleWithOperations,
|
||||
})
|
||||
}
|
||||
if mc.MatchPolicy != nil {
|
||||
mp := v1.MatchPolicyType(*mc.MatchPolicy)
|
||||
res.MatchPolicy = &mp
|
||||
}
|
||||
return &res
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
Copyright 2024 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 mutating
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/api/admissionregistration/v1alpha1"
|
||||
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
|
||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
)
|
||||
|
||||
// compilePolicy compiles the policy into a PolicyEvaluator
|
||||
// any error is stored and delayed until invocation.
|
||||
//
|
||||
// Each individual mutation is compiled into MutationEvaluationFunc and
|
||||
// returned is a PolicyEvaluator in the same order as the mutations appeared in the policy.
|
||||
func compilePolicy(policy *Policy) PolicyEvaluator {
|
||||
opts := plugincel.OptionalVariableDeclarations{HasParams: policy.Spec.ParamKind != nil, StrictCost: true, HasAuthorizer: true}
|
||||
compiler, err := plugincel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
|
||||
if err != nil {
|
||||
return PolicyEvaluator{Error: &apiservercel.Error{
|
||||
Type: apiservercel.ErrorTypeInternal,
|
||||
Detail: fmt.Sprintf("failed to initialize CEL compiler: %v", err),
|
||||
}}
|
||||
}
|
||||
|
||||
// Compile and store variables
|
||||
compiler.CompileAndStoreVariables(convertv1alpha1Variables(policy.Spec.Variables), opts, environment.StoredExpressions)
|
||||
|
||||
// Compile matchers
|
||||
var matcher matchconditions.Matcher = nil
|
||||
matchConditions := policy.Spec.MatchConditions
|
||||
if len(matchConditions) > 0 {
|
||||
matchExpressionAccessors := make([]plugincel.ExpressionAccessor, len(matchConditions))
|
||||
for i := range matchConditions {
|
||||
matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i])
|
||||
}
|
||||
matcher = matchconditions.NewMatcher(compiler.CompileCondition(matchExpressionAccessors, opts, environment.StoredExpressions), toV1FailurePolicy(policy.Spec.FailurePolicy), "policy", "validate", policy.Name)
|
||||
}
|
||||
|
||||
// Compiler patchers
|
||||
var patchers []patch.Patcher
|
||||
patchOptions := opts
|
||||
patchOptions.HasPatchTypes = true
|
||||
for _, m := range policy.Spec.Mutations {
|
||||
switch m.PatchType {
|
||||
case v1alpha1.PatchTypeJSONPatch:
|
||||
if m.JSONPatch != nil {
|
||||
accessor := &patch.JSONPatchCondition{Expression: m.JSONPatch.Expression}
|
||||
compileResult := compiler.CompileMutatingEvaluator(accessor, patchOptions, environment.StoredExpressions)
|
||||
patchers = append(patchers, patch.NewJSONPatcher(compileResult))
|
||||
}
|
||||
case v1alpha1.PatchTypeApplyConfiguration:
|
||||
if m.ApplyConfiguration != nil {
|
||||
accessor := &patch.ApplyConfigurationCondition{Expression: m.ApplyConfiguration.Expression}
|
||||
compileResult := compiler.CompileMutatingEvaluator(accessor, patchOptions, environment.StoredExpressions)
|
||||
patchers = append(patchers, patch.NewApplyConfigurationPatcher(compileResult))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return PolicyEvaluator{Matcher: matcher, Mutators: patchers, CompositionEnv: compiler.CompositionEnv}
|
||||
}
|
|
@ -0,0 +1,457 @@
|
|||
/*
|
||||
Copyright 2024 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 mutating
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/api/admissionregistration/v1alpha1"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch"
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/client-go/openapi/openapitest"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
// TestCompilation is an open-box test of mutatingEvaluator.compile
|
||||
// However, the result is a set of CEL programs, manually invoke them to assert
|
||||
// on the results.
|
||||
func TestCompilation(t *testing.T) {
|
||||
deploymentGVR := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
|
||||
deploymentGVK := schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}
|
||||
testCases := []struct {
|
||||
name string
|
||||
policy *Policy
|
||||
gvr schema.GroupVersionResource
|
||||
object runtime.Object
|
||||
oldObject runtime.Object
|
||||
params runtime.Object
|
||||
namespace *corev1.Namespace
|
||||
expectedErr string
|
||||
expectedResult runtime.Object
|
||||
}{
|
||||
{
|
||||
name: "applyConfiguration then jsonPatch",
|
||||
policy: mutations(policy("d1"), v1alpha1.Mutation{
|
||||
PatchType: v1alpha1.PatchTypeApplyConfiguration,
|
||||
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
|
||||
Expression: `Object{
|
||||
spec: Object.spec{
|
||||
replicas: object.spec.replicas + 100
|
||||
}
|
||||
}`,
|
||||
},
|
||||
},
|
||||
v1alpha1.Mutation{
|
||||
PatchType: v1alpha1.PatchTypeJSONPatch,
|
||||
JSONPatch: &v1alpha1.JSONPatch{
|
||||
Expression: `[
|
||||
JSONPatch{op: "replace", path: "/spec/replicas", value: object.spec.replicas + 10}
|
||||
]`,
|
||||
},
|
||||
}),
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
|
||||
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](111)}},
|
||||
},
|
||||
{
|
||||
name: "jsonPatch then applyConfiguration",
|
||||
policy: mutations(policy("d1"),
|
||||
v1alpha1.Mutation{
|
||||
PatchType: v1alpha1.PatchTypeJSONPatch,
|
||||
JSONPatch: &v1alpha1.JSONPatch{
|
||||
Expression: `[
|
||||
JSONPatch{op: "replace", path: "/spec/replicas", value: object.spec.replicas + 10}
|
||||
]`,
|
||||
},
|
||||
},
|
||||
v1alpha1.Mutation{
|
||||
PatchType: v1alpha1.PatchTypeApplyConfiguration,
|
||||
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
|
||||
Expression: `Object{
|
||||
spec: Object.spec{
|
||||
replicas: object.spec.replicas + 100
|
||||
}
|
||||
}`,
|
||||
},
|
||||
}),
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
|
||||
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](111)}},
|
||||
},
|
||||
{
|
||||
name: "jsonPatch with variable",
|
||||
policy: jsonPatches(variables(policy("d1"), v1alpha1.Variable{Name: "desired", Expression: "10"}), v1alpha1.JSONPatch{
|
||||
Expression: `[
|
||||
JSONPatch{op: "replace", path: "/spec/replicas", value: variables.desired + 1},
|
||||
]`,
|
||||
}),
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
|
||||
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](11)}},
|
||||
},
|
||||
{
|
||||
name: "apply configuration with variable",
|
||||
policy: applyConfigurations(variables(policy("d1"), v1alpha1.Variable{Name: "desired", Expression: "10"}),
|
||||
`Object{
|
||||
spec: Object.spec{
|
||||
replicas: variables.desired + 1
|
||||
}
|
||||
}`),
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
|
||||
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](11)}},
|
||||
},
|
||||
{
|
||||
name: "apply configuration with params",
|
||||
policy: paramKind(applyConfigurations(policy("d1"),
|
||||
`Object{
|
||||
spec: Object.spec{
|
||||
replicas: int(params.data['k1'])
|
||||
}
|
||||
}`), &v1alpha1.ParamKind{Kind: "ConfigMap", APIVersion: "v1"}),
|
||||
params: &corev1.ConfigMap{Data: map[string]string{"k1": "100"}},
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
|
||||
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](100)}},
|
||||
},
|
||||
{
|
||||
name: "jsonPatch with excessive cost",
|
||||
policy: jsonPatches(variables(policy("d1"), v1alpha1.Variable{Name: "list", Expression: "[0,1,2,3,4,5,6,7,8,9]"}), v1alpha1.JSONPatch{
|
||||
Expression: `[
|
||||
JSONPatch{op: "replace", path: "/spec/replicas",
|
||||
value: variables.list.all(x1, variables.list.all(x2, variables.list.all(x3, variables.list.all(x4, variables.list.all(x5, variables.list.all(x5, "0123456789" == "0123456789"))))))? 1 : 0
|
||||
}
|
||||
]`,
|
||||
}),
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
|
||||
expectedErr: "operation cancelled: actual cost limit exceeded",
|
||||
},
|
||||
{
|
||||
name: "applyConfiguration with excessive cost",
|
||||
policy: variables(applyConfigurations(policy("d1"),
|
||||
`Object{
|
||||
spec: Object.spec{
|
||||
replicas: variables.list.all(x1, variables.list.all(x2, variables.list.all(x3, variables.list.all(x4, variables.list.all(x5, variables.list.all(x5, "0123456789" == "0123456789"))))))? 1 : 0
|
||||
}
|
||||
}`), v1alpha1.Variable{Name: "list", Expression: "[0,1,2,3,4,5,6,7,8,9]"}),
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
|
||||
expectedErr: "operation cancelled: actual cost limit exceeded",
|
||||
},
|
||||
{
|
||||
name: "request variable",
|
||||
policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{
|
||||
Expression: `[
|
||||
JSONPatch{op: "replace", path: "/spec/replicas",
|
||||
value: request.kind.group == 'apps' && request.kind.version == 'v1' && request.kind.kind == 'Deployment' ? 10 : 0
|
||||
}
|
||||
]`}),
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
|
||||
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](10)}},
|
||||
},
|
||||
{
|
||||
name: "namespace request variable",
|
||||
policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{
|
||||
Expression: `[
|
||||
JSONPatch{op: "replace", path: "/spec/replicas",
|
||||
value: namespaceObject.metadata.name == 'ns1' ? 10 : 0
|
||||
}
|
||||
]`}),
|
||||
namespace: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "ns1"}},
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
|
||||
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](10)}},
|
||||
},
|
||||
{
|
||||
name: "authorizer check",
|
||||
policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{
|
||||
Expression: `[
|
||||
JSONPatch{op: "replace", path: "/spec/replicas",
|
||||
value: authorizer.group('').resource('endpoints').check('create').allowed() ? 10 : 0
|
||||
}
|
||||
]`,
|
||||
}),
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
|
||||
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](10)}},
|
||||
},
|
||||
{
|
||||
name: "object type has field access",
|
||||
policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{
|
||||
Expression: `[
|
||||
JSONPatch{
|
||||
op: "add", path: "/metadata/labels",
|
||||
value: {
|
||||
"value": Object{field: "fieldValue"}.field,
|
||||
}
|
||||
}
|
||||
]`,
|
||||
}),
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}},
|
||||
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{
|
||||
"value": "fieldValue",
|
||||
}}},
|
||||
},
|
||||
{
|
||||
name: "object type has field testing",
|
||||
policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{
|
||||
Expression: `[
|
||||
JSONPatch{
|
||||
op: "add", path: "/metadata/labels",
|
||||
value: {
|
||||
"field": string(has(Object{field: "fieldValue"}.field)),
|
||||
"field-unset": string(has(Object{}.field)),
|
||||
}
|
||||
}
|
||||
]`,
|
||||
}),
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}},
|
||||
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{
|
||||
"field": "true",
|
||||
"field-unset": "false",
|
||||
}}},
|
||||
},
|
||||
{
|
||||
name: "object type equality",
|
||||
policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{
|
||||
Expression: `[
|
||||
JSONPatch{
|
||||
op: "add", path: "/metadata/labels",
|
||||
value: {
|
||||
"empty": string(Object{} == Object{}),
|
||||
"same": string(Object{field: "x"} == Object{field: "x"}),
|
||||
"different": string(Object{field: "x"} == Object{field: "y"}),
|
||||
}
|
||||
}
|
||||
]`,
|
||||
}),
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}},
|
||||
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{
|
||||
"empty": "true",
|
||||
"same": "true",
|
||||
"different": "false",
|
||||
}}},
|
||||
},
|
||||
{
|
||||
// TODO: This test documents existing behavior that we should be fixed before
|
||||
// MutatingAdmissionPolicy graduates to beta.
|
||||
// It is possible to initialize invalid Object types because we do not yet perform
|
||||
// a full compilation pass with the types fully bound. Before beta, we should
|
||||
// recompile all expressions with fully bound types before evaluation and report
|
||||
// errors if invalid Object types like this are initialized.
|
||||
name: "object types are not fully type checked",
|
||||
policy: jsonPatches(policy("d1"), v1alpha1.JSONPatch{
|
||||
Expression: `[
|
||||
JSONPatch{
|
||||
op: "add", path: "/spec",
|
||||
value: Object.invalid{replicas: 1}
|
||||
}
|
||||
]`,
|
||||
}),
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}},
|
||||
expectedResult: &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Replicas: ptr.To[int32](1),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
err := appsv1.AddToScheme(scheme)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
tcManager := patch.NewTypeConverterManager(nil, openapitest.NewEmbeddedFileClient())
|
||||
go tcManager.Run(ctx)
|
||||
|
||||
err = wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, time.Second, true, func(context.Context) (done bool, err error) {
|
||||
converter := tcManager.GetTypeConverter(deploymentGVK)
|
||||
return converter != nil, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var gvk schema.GroupVersionKind
|
||||
gvks, _, err := scheme.ObjectKinds(tc.object)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(gvks) == 1 {
|
||||
gvk = gvks[0]
|
||||
} else {
|
||||
t.Fatalf("Failed to find gvk for type: %T", tc.object)
|
||||
}
|
||||
|
||||
policyEvaluator := compilePolicy(tc.policy)
|
||||
if policyEvaluator.CompositionEnv != nil {
|
||||
ctx = policyEvaluator.CompositionEnv.CreateContext(ctx)
|
||||
}
|
||||
obj := tc.object
|
||||
|
||||
typeAccessor, err := meta.TypeAccessor(obj)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
typeAccessor.SetKind(gvk.Kind)
|
||||
typeAccessor.SetAPIVersion(gvk.GroupVersion().String())
|
||||
typeConverter := tcManager.GetTypeConverter(gvk)
|
||||
|
||||
metaAccessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, patcher := range policyEvaluator.Mutators {
|
||||
attrs := admission.NewAttributesRecord(obj, tc.oldObject, gvk,
|
||||
metaAccessor.GetNamespace(), metaAccessor.GetName(), tc.gvr,
|
||||
"", admission.Create, &metav1.CreateOptions{}, false, nil)
|
||||
vAttrs := &admission.VersionedAttributes{
|
||||
Attributes: attrs,
|
||||
VersionedKind: gvk,
|
||||
VersionedObject: obj,
|
||||
VersionedOldObject: tc.oldObject,
|
||||
}
|
||||
r := patch.Request{
|
||||
MatchedResource: tc.gvr,
|
||||
VersionedAttributes: vAttrs,
|
||||
ObjectInterfaces: admission.NewObjectInterfacesFromScheme(scheme),
|
||||
OptionalVariables: cel.OptionalVariableBindings{VersionedParams: tc.params, Authorizer: fakeAuthorizer{}},
|
||||
Namespace: tc.namespace,
|
||||
TypeConverter: typeConverter,
|
||||
}
|
||||
obj, err = patcher.Patch(ctx, r, celconfig.RuntimeCELCostBudget)
|
||||
if len(tc.expectedErr) > 0 {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error: %s", tc.expectedErr)
|
||||
} else {
|
||||
if !strings.Contains(err.Error(), tc.expectedErr) {
|
||||
t.Fatalf("expected error: %s, got: %s", tc.expectedErr, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil && len(tc.expectedErr) == 0 {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
got, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wantTypeAccessor, err := meta.TypeAccessor(tc.expectedResult)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantTypeAccessor.SetKind(gvk.Kind)
|
||||
wantTypeAccessor.SetAPIVersion(gvk.GroupVersion().String())
|
||||
|
||||
want, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.expectedResult)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !equality.Semantic.DeepEqual(want, got) {
|
||||
t.Errorf("unexpected result, got diff:\n%s\n", cmp.Diff(want, got))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func policy(name string) *v1alpha1.MutatingAdmissionPolicy {
|
||||
return &v1alpha1.MutatingAdmissionPolicy{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicySpec{},
|
||||
}
|
||||
}
|
||||
|
||||
func variables(policy *v1alpha1.MutatingAdmissionPolicy, variables ...v1alpha1.Variable) *v1alpha1.MutatingAdmissionPolicy {
|
||||
policy.Spec.Variables = append(policy.Spec.Variables, variables...)
|
||||
return policy
|
||||
}
|
||||
|
||||
func jsonPatches(policy *v1alpha1.MutatingAdmissionPolicy, jsonPatches ...v1alpha1.JSONPatch) *v1alpha1.MutatingAdmissionPolicy {
|
||||
for _, jsonPatch := range jsonPatches {
|
||||
policy.Spec.Mutations = append(policy.Spec.Mutations, v1alpha1.Mutation{
|
||||
JSONPatch: &jsonPatch,
|
||||
PatchType: v1alpha1.PatchTypeJSONPatch,
|
||||
})
|
||||
}
|
||||
|
||||
return policy
|
||||
}
|
||||
|
||||
func applyConfigurations(policy *v1alpha1.MutatingAdmissionPolicy, expressions ...string) *v1alpha1.MutatingAdmissionPolicy {
|
||||
for _, expression := range expressions {
|
||||
policy.Spec.Mutations = append(policy.Spec.Mutations, v1alpha1.Mutation{
|
||||
ApplyConfiguration: &v1alpha1.ApplyConfiguration{Expression: expression},
|
||||
PatchType: v1alpha1.PatchTypeApplyConfiguration,
|
||||
})
|
||||
}
|
||||
return policy
|
||||
}
|
||||
|
||||
func paramKind(policy *v1alpha1.MutatingAdmissionPolicy, paramKind *v1alpha1.ParamKind) *v1alpha1.MutatingAdmissionPolicy {
|
||||
policy.Spec.ParamKind = paramKind
|
||||
return policy
|
||||
}
|
||||
|
||||
func mutations(policy *v1alpha1.MutatingAdmissionPolicy, mutations ...v1alpha1.Mutation) *v1alpha1.MutatingAdmissionPolicy {
|
||||
policy.Spec.Mutations = append(policy.Spec.Mutations, mutations...)
|
||||
return policy
|
||||
}
|
||||
|
||||
func matchConstraints(policy *v1alpha1.MutatingAdmissionPolicy, matchConstraints *v1alpha1.MatchResources) *v1alpha1.MutatingAdmissionPolicy {
|
||||
policy.Spec.MatchConstraints = matchConstraints
|
||||
return policy
|
||||
}
|
||||
|
||||
type fakeAuthorizer struct{}
|
||||
|
||||
func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
|
@ -0,0 +1,295 @@
|
|||
/*
|
||||
Copyright 2024 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 mutating
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/api/admissionregistration/v1alpha1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
admissionauthorizer "k8s.io/apiserver/pkg/admission/plugin/authorizer"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch"
|
||||
webhookgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
)
|
||||
|
||||
func NewDispatcher(a authorizer.Authorizer, m *matching.Matcher, tcm patch.TypeConverterManager) generic.Dispatcher[PolicyHook] {
|
||||
res := &dispatcher{
|
||||
matcher: m,
|
||||
authz: a,
|
||||
typeConverterManager: tcm,
|
||||
}
|
||||
res.Dispatcher = generic.NewPolicyDispatcher[*Policy, *PolicyBinding, PolicyEvaluator](
|
||||
NewMutatingAdmissionPolicyAccessor,
|
||||
NewMutatingAdmissionPolicyBindingAccessor,
|
||||
m,
|
||||
res.dispatchInvocations,
|
||||
)
|
||||
return res
|
||||
}
|
||||
|
||||
type dispatcher struct {
|
||||
matcher *matching.Matcher
|
||||
authz authorizer.Authorizer
|
||||
typeConverterManager patch.TypeConverterManager
|
||||
generic.Dispatcher[PolicyHook]
|
||||
}
|
||||
|
||||
func (d *dispatcher) Start(ctx context.Context) error {
|
||||
go d.typeConverterManager.Run(ctx)
|
||||
return d.Dispatcher.Start(ctx)
|
||||
}
|
||||
|
||||
func (d *dispatcher) dispatchInvocations(
|
||||
ctx context.Context,
|
||||
a admission.Attributes,
|
||||
o admission.ObjectInterfaces,
|
||||
versionedAttributes webhookgeneric.VersionedAttributeAccessor,
|
||||
invocations []generic.PolicyInvocation[*Policy, *PolicyBinding, PolicyEvaluator],
|
||||
) ([]generic.PolicyError, *k8serrors.StatusError) {
|
||||
var lastVersionedAttr *admission.VersionedAttributes
|
||||
|
||||
reinvokeCtx := a.GetReinvocationContext()
|
||||
var policyReinvokeCtx *policyReinvokeContext
|
||||
if v := reinvokeCtx.Value(PluginName); v != nil {
|
||||
policyReinvokeCtx = v.(*policyReinvokeContext)
|
||||
} else {
|
||||
policyReinvokeCtx = &policyReinvokeContext{}
|
||||
reinvokeCtx.SetValue(PluginName, policyReinvokeCtx)
|
||||
}
|
||||
|
||||
if reinvokeCtx.IsReinvoke() && policyReinvokeCtx.IsOutputChangedSinceLastPolicyInvocation(a.GetObject()) {
|
||||
// If the object has changed, we know the in-tree plugin re-invocations have mutated the object,
|
||||
// and we need to reinvoke all eligible policies.
|
||||
policyReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
|
||||
}
|
||||
defer func() {
|
||||
policyReinvokeCtx.SetLastPolicyInvocationOutput(a.GetObject())
|
||||
}()
|
||||
|
||||
var policyErrors []generic.PolicyError
|
||||
addConfigError := func(err error, invocation generic.PolicyInvocation[*Policy, *PolicyBinding, PolicyEvaluator], reason metav1.StatusReason) {
|
||||
policyErrors = append(policyErrors, generic.PolicyError{
|
||||
Message: err,
|
||||
Policy: NewMutatingAdmissionPolicyAccessor(invocation.Policy),
|
||||
Binding: NewMutatingAdmissionPolicyBindingAccessor(invocation.Binding),
|
||||
Reason: reason,
|
||||
})
|
||||
}
|
||||
|
||||
// There is at least one invocation to invoke. Make sure we have a namespace
|
||||
// object if the incoming object is not cluster scoped to pass into the evaluator.
|
||||
var namespace *v1.Namespace
|
||||
var err error
|
||||
namespaceName := a.GetNamespace()
|
||||
|
||||
// Special case, the namespace object has the namespace of itself (maybe a bug).
|
||||
// unset it if the incoming object is a namespace
|
||||
if gvk := a.GetKind(); gvk.Kind == "Namespace" && gvk.Version == "v1" && gvk.Group == "" {
|
||||
namespaceName = ""
|
||||
}
|
||||
|
||||
// if it is cluster scoped, namespaceName will be empty
|
||||
// Otherwise, get the Namespace resource.
|
||||
if namespaceName != "" {
|
||||
namespace, err = d.matcher.GetNamespace(namespaceName)
|
||||
if err != nil {
|
||||
return nil, k8serrors.NewNotFound(schema.GroupResource{Group: "", Resource: "namespaces"}, namespaceName)
|
||||
}
|
||||
}
|
||||
|
||||
authz := admissionauthorizer.NewCachingAuthorizer(d.authz)
|
||||
|
||||
// Should loop through invocations, handling possible error and invoking
|
||||
// evaluator to apply patch, also should handle re-invocations
|
||||
for _, invocation := range invocations {
|
||||
if invocation.Evaluator.CompositionEnv != nil {
|
||||
ctx = invocation.Evaluator.CompositionEnv.CreateContext(ctx)
|
||||
}
|
||||
if len(invocation.Evaluator.Mutators) != len(invocation.Policy.Spec.Mutations) {
|
||||
// This would be a bug. The compiler should always return exactly as
|
||||
// many evaluators as there are mutations
|
||||
return nil, k8serrors.NewInternalError(fmt.Errorf("expected %v compiled evaluators for policy %v, got %v",
|
||||
len(invocation.Policy.Spec.Mutations), invocation.Policy.Name, len(invocation.Evaluator.Mutators)))
|
||||
}
|
||||
|
||||
versionedAttr, err := versionedAttributes.VersionedAttribute(invocation.Kind)
|
||||
if err != nil {
|
||||
// This should never happen, we pre-warm versoined attribute
|
||||
// accessors before starting the dispatcher
|
||||
return nil, k8serrors.NewInternalError(err)
|
||||
}
|
||||
|
||||
if invocation.Evaluator.Matcher != nil {
|
||||
matchResults := invocation.Evaluator.Matcher.Match(ctx, versionedAttr, invocation.Param, authz)
|
||||
if matchResults.Error != nil {
|
||||
addConfigError(matchResults.Error, invocation, metav1.StatusReasonInvalid)
|
||||
continue
|
||||
}
|
||||
|
||||
// if preconditions are not met, then skip mutations
|
||||
if !matchResults.Matches {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
invocationKey, invocationKeyErr := keyFor(invocation)
|
||||
if invocationKeyErr != nil {
|
||||
// This should never happen. It occurs if there is a programming
|
||||
// error causing the Param not to be a valid object.
|
||||
return nil, k8serrors.NewInternalError(invocationKeyErr)
|
||||
}
|
||||
if reinvokeCtx.IsReinvoke() && !policyReinvokeCtx.ShouldReinvoke(invocationKey) {
|
||||
continue
|
||||
}
|
||||
|
||||
objectBeforeMutations := versionedAttr.VersionedObject
|
||||
// Mutations for a single invocation of a MutatingAdmissionPolicy are evaluated
|
||||
// in order.
|
||||
for mutationIndex := range invocation.Policy.Spec.Mutations {
|
||||
lastVersionedAttr = versionedAttr
|
||||
if versionedAttr.VersionedObject == nil { // Do not call patchers if there is no object to patch.
|
||||
continue
|
||||
}
|
||||
|
||||
patcher := invocation.Evaluator.Mutators[mutationIndex]
|
||||
optionalVariables := cel.OptionalVariableBindings{VersionedParams: invocation.Param, Authorizer: authz}
|
||||
err = d.dispatchOne(ctx, patcher, o, versionedAttr, namespace, invocation.Resource, optionalVariables)
|
||||
if err != nil {
|
||||
var statusError *k8serrors.StatusError
|
||||
if errors.As(err, &statusError) {
|
||||
return nil, statusError
|
||||
}
|
||||
|
||||
addConfigError(err, invocation, metav1.StatusReasonInvalid)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !apiequality.Semantic.DeepEqual(objectBeforeMutations, versionedAttr.VersionedObject) {
|
||||
// The mutation has changed the object. Prepare to reinvoke all previous mutations that are eligible for re-invocation.
|
||||
policyReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
|
||||
reinvokeCtx.SetShouldReinvoke()
|
||||
}
|
||||
if invocation.Policy.Spec.ReinvocationPolicy == v1alpha1.IfNeededReinvocationPolicy {
|
||||
policyReinvokeCtx.AddReinvocablePolicyToPreviouslyInvoked(invocationKey)
|
||||
}
|
||||
}
|
||||
|
||||
if lastVersionedAttr != nil && lastVersionedAttr.VersionedObject != nil && lastVersionedAttr.Dirty {
|
||||
policyReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
|
||||
reinvokeCtx.SetShouldReinvoke()
|
||||
if err := o.GetObjectConvertor().Convert(lastVersionedAttr.VersionedObject, lastVersionedAttr.Attributes.GetObject(), nil); err != nil {
|
||||
return nil, k8serrors.NewInternalError(fmt.Errorf("failed to convert object: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
return policyErrors, nil
|
||||
}
|
||||
|
||||
func (d *dispatcher) dispatchOne(
|
||||
ctx context.Context,
|
||||
patcher patch.Patcher,
|
||||
o admission.ObjectInterfaces,
|
||||
versionedAttributes *admission.VersionedAttributes,
|
||||
namespace *v1.Namespace,
|
||||
resource schema.GroupVersionResource,
|
||||
optionalVariables cel.OptionalVariableBindings,
|
||||
) (err error) {
|
||||
if patcher == nil {
|
||||
// internal error. this should not happen
|
||||
return k8serrors.NewInternalError(fmt.Errorf("policy evaluator is nil"))
|
||||
}
|
||||
|
||||
// Find type converter for the invoked Group-Version.
|
||||
typeConverter := d.typeConverterManager.GetTypeConverter(versionedAttributes.VersionedKind)
|
||||
if typeConverter == nil {
|
||||
// This can happen if the request is for a resource whose schema
|
||||
// has not been registered with the type converter manager.
|
||||
return k8serrors.NewServiceUnavailable(fmt.Sprintf("Resource kind %s not found. There can be a delay between when CustomResourceDefinitions are created and when they are available.", versionedAttributes.VersionedKind))
|
||||
}
|
||||
|
||||
patchRequest := patch.Request{
|
||||
MatchedResource: resource,
|
||||
VersionedAttributes: versionedAttributes,
|
||||
ObjectInterfaces: o,
|
||||
OptionalVariables: optionalVariables,
|
||||
Namespace: namespace,
|
||||
TypeConverter: typeConverter,
|
||||
}
|
||||
newVersionedObject, err := patcher.Patch(ctx, patchRequest, celconfig.RuntimeCELCostBudget)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch versionedAttributes.VersionedObject.(type) {
|
||||
case *unstructured.Unstructured:
|
||||
// No conversion needed before defaulting for the patch object if the admitted object is unstructured.
|
||||
default:
|
||||
// Before defaulting, if the admitted object is a typed object, convert unstructured patch result back to a typed object.
|
||||
newVersionedObject, err = o.GetObjectConvertor().ConvertToVersion(newVersionedObject, versionedAttributes.GetKind().GroupVersion())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
o.GetObjectDefaulter().Default(newVersionedObject)
|
||||
|
||||
versionedAttributes.Dirty = true
|
||||
versionedAttributes.VersionedObject = newVersionedObject
|
||||
return nil
|
||||
}
|
||||
|
||||
func keyFor(invocation generic.PolicyInvocation[*Policy, *PolicyBinding, PolicyEvaluator]) (key, error) {
|
||||
var paramUID types.NamespacedName
|
||||
if invocation.Param != nil {
|
||||
paramAccessor, err := meta.Accessor(invocation.Param)
|
||||
if err != nil {
|
||||
// This should never happen, as the param should have been validated
|
||||
// before being passed to the plugin.
|
||||
return key{}, err
|
||||
}
|
||||
paramUID = types.NamespacedName{
|
||||
Name: paramAccessor.GetName(),
|
||||
Namespace: paramAccessor.GetNamespace(),
|
||||
}
|
||||
}
|
||||
|
||||
return key{
|
||||
PolicyUID: types.NamespacedName{
|
||||
Name: invocation.Policy.GetName(),
|
||||
Namespace: invocation.Policy.GetNamespace(),
|
||||
},
|
||||
BindingUID: types.NamespacedName{
|
||||
Name: invocation.Binding.GetName(),
|
||||
Namespace: invocation.Binding.GetNamespace(),
|
||||
},
|
||||
ParamUID: paramUID,
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,715 @@
|
|||
/*
|
||||
Copyright 2024 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 mutating
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||
"k8s.io/api/admissionregistration/v1alpha1"
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/client-go/openapi/openapitest"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func TestDispatcher(t *testing.T) {
|
||||
deploymentGVK := schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}
|
||||
deploymentGVR := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
|
||||
testCases := []struct {
|
||||
name string
|
||||
object, oldObject runtime.Object
|
||||
gvk schema.GroupVersionKind
|
||||
gvr schema.GroupVersionResource
|
||||
params []runtime.Object // All params are expected to be ConfigMap for this test.
|
||||
policyHooks []PolicyHook
|
||||
expect runtime.Object
|
||||
}{
|
||||
{
|
||||
name: "simple patch",
|
||||
gvk: deploymentGVK,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Deployment",
|
||||
APIVersion: "apps/v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "d1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Replicas: ptr.To[int32](1),
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Volumes: []corev1.Volume{{Name: "x"}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
policyHooks: []generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]{
|
||||
{
|
||||
Policy: mutations(matchConstraints(policy("policy1"), &v1alpha1.MatchResources{
|
||||
MatchPolicy: ptr.To(v1alpha1.Equivalent),
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
ResourceRules: []v1alpha1.NamedRuleWithOperations{
|
||||
{
|
||||
RuleWithOperations: v1alpha1.RuleWithOperations{
|
||||
Rule: v1alpha1.Rule{
|
||||
APIGroups: []string{"apps"},
|
||||
APIVersions: []string{"v1"},
|
||||
Resources: []string{"deployments"},
|
||||
},
|
||||
Operations: []admissionregistrationv1.OperationType{"*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}), v1alpha1.Mutation{
|
||||
PatchType: v1alpha1.PatchTypeApplyConfiguration,
|
||||
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
|
||||
Expression: `Object{
|
||||
spec: Object.spec{
|
||||
replicas: object.spec.replicas + 100
|
||||
}
|
||||
}`,
|
||||
}}),
|
||||
Bindings: []*PolicyBinding{{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
|
||||
PolicyName: "policy1",
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
expect: &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "d1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Replicas: ptr.To[int32](101),
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Volumes: []corev1.Volume{{Name: "x"}},
|
||||
},
|
||||
},
|
||||
Strategy: appsv1.DeploymentStrategy{
|
||||
Type: appsv1.RollingUpdateDeploymentStrategyType,
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "with param",
|
||||
gvk: deploymentGVK,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Deployment",
|
||||
APIVersion: "apps/v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "d1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Replicas: ptr.To[int32](1),
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Volumes: []corev1.Volume{{Name: "x"}},
|
||||
},
|
||||
},
|
||||
Strategy: appsv1.DeploymentStrategy{
|
||||
Type: appsv1.RollingUpdateDeploymentStrategyType,
|
||||
},
|
||||
}},
|
||||
params: []runtime.Object{
|
||||
&corev1.ConfigMap{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "ConfigMap",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "cm1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Data: map[string]string{
|
||||
"key": "10",
|
||||
},
|
||||
},
|
||||
},
|
||||
policyHooks: []generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]{
|
||||
{
|
||||
Policy: paramKind(mutations(matchConstraints(policy("policy1"), &v1alpha1.MatchResources{
|
||||
MatchPolicy: ptr.To(v1alpha1.Equivalent),
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
ResourceRules: []v1alpha1.NamedRuleWithOperations{
|
||||
{
|
||||
RuleWithOperations: v1alpha1.RuleWithOperations{
|
||||
Rule: v1alpha1.Rule{
|
||||
APIGroups: []string{"apps"},
|
||||
APIVersions: []string{"v1"},
|
||||
Resources: []string{"deployments"},
|
||||
},
|
||||
Operations: []admissionregistrationv1.OperationType{"*"},
|
||||
},
|
||||
},
|
||||
}}),
|
||||
v1alpha1.Mutation{
|
||||
PatchType: v1alpha1.PatchTypeApplyConfiguration,
|
||||
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
|
||||
Expression: `Object{
|
||||
spec: Object.spec{
|
||||
replicas: object.spec.replicas + int(params.data['key'])
|
||||
}
|
||||
}`,
|
||||
}}),
|
||||
&v1alpha1.ParamKind{
|
||||
APIVersion: "v1",
|
||||
Kind: "ConfigMap",
|
||||
}),
|
||||
Bindings: []*PolicyBinding{{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
|
||||
PolicyName: "policy1",
|
||||
ParamRef: &v1alpha1.ParamRef{Name: "cm1", Namespace: "default"},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
expect: &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "d1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Replicas: ptr.To[int32](11),
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Volumes: []corev1.Volume{{Name: "x"}},
|
||||
},
|
||||
},
|
||||
Strategy: appsv1.DeploymentStrategy{
|
||||
Type: appsv1.RollingUpdateDeploymentStrategyType,
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "both policies reinvoked",
|
||||
gvk: deploymentGVK,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Deployment",
|
||||
APIVersion: "apps/v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "d1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Volumes: []corev1.Volume{{Name: "x"}},
|
||||
},
|
||||
},
|
||||
Strategy: appsv1.DeploymentStrategy{
|
||||
Type: appsv1.RollingUpdateDeploymentStrategyType,
|
||||
},
|
||||
}},
|
||||
policyHooks: []generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]{
|
||||
{
|
||||
Policy: mutations(matchConstraints(policy("policy1"), &v1alpha1.MatchResources{
|
||||
MatchPolicy: ptr.To(v1alpha1.Equivalent),
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
ResourceRules: []v1alpha1.NamedRuleWithOperations{
|
||||
{
|
||||
RuleWithOperations: v1alpha1.RuleWithOperations{
|
||||
Rule: v1alpha1.Rule{
|
||||
APIGroups: []string{"apps"},
|
||||
APIVersions: []string{"v1"},
|
||||
Resources: []string{"deployments"},
|
||||
},
|
||||
Operations: []admissionregistrationv1.OperationType{"*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}), v1alpha1.Mutation{
|
||||
PatchType: v1alpha1.PatchTypeApplyConfiguration,
|
||||
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
|
||||
Expression: `Object{
|
||||
metadata: Object.metadata{
|
||||
labels: {"policy1": string(int(object.?metadata.labels["count"].orValue("1")) + 1)}
|
||||
}
|
||||
}`,
|
||||
}}),
|
||||
Bindings: []*PolicyBinding{{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
|
||||
PolicyName: "policy1",
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
Policy: mutations(matchConstraints(policy("policy2"), &v1alpha1.MatchResources{
|
||||
MatchPolicy: ptr.To(v1alpha1.Equivalent),
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
ResourceRules: []v1alpha1.NamedRuleWithOperations{
|
||||
{
|
||||
RuleWithOperations: v1alpha1.RuleWithOperations{
|
||||
Rule: v1alpha1.Rule{
|
||||
APIGroups: []string{"apps"},
|
||||
APIVersions: []string{"v1"},
|
||||
Resources: []string{"deployments"},
|
||||
},
|
||||
Operations: []admissionregistrationv1.OperationType{"*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}), v1alpha1.Mutation{
|
||||
PatchType: v1alpha1.PatchTypeApplyConfiguration,
|
||||
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
|
||||
Expression: `Object{
|
||||
metadata: Object.metadata{
|
||||
labels: {"policy2": string(int(object.?metadata.labels["count"].orValue("1")) + 1)}
|
||||
}
|
||||
}`,
|
||||
}}),
|
||||
Bindings: []*PolicyBinding{{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
|
||||
PolicyName: "policy2",
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
expect: &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "d1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"policy1": "2",
|
||||
"policy2": "2",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Volumes: []corev1.Volume{{Name: "x"}},
|
||||
},
|
||||
},
|
||||
Strategy: appsv1.DeploymentStrategy{
|
||||
Type: appsv1.RollingUpdateDeploymentStrategyType,
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "1st policy sets match condition that 2nd policy matches",
|
||||
gvk: deploymentGVK,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Deployment",
|
||||
APIVersion: "apps/v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "d1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Volumes: []corev1.Volume{{Name: "x"}},
|
||||
},
|
||||
},
|
||||
Strategy: appsv1.DeploymentStrategy{
|
||||
Type: appsv1.RollingUpdateDeploymentStrategyType,
|
||||
},
|
||||
}},
|
||||
policyHooks: []generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]{
|
||||
{
|
||||
Policy: &v1alpha1.MutatingAdmissionPolicy{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "policy1",
|
||||
},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicySpec{
|
||||
MatchConstraints: &v1alpha1.MatchResources{
|
||||
MatchPolicy: ptr.To(v1alpha1.Equivalent),
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
ResourceRules: []v1alpha1.NamedRuleWithOperations{
|
||||
{
|
||||
RuleWithOperations: v1alpha1.RuleWithOperations{
|
||||
Rule: v1alpha1.Rule{
|
||||
APIGroups: []string{"apps"},
|
||||
APIVersions: []string{"v1"},
|
||||
Resources: []string{"deployments"},
|
||||
},
|
||||
Operations: []admissionregistrationv1.OperationType{"*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Mutations: []v1alpha1.Mutation{{
|
||||
PatchType: v1alpha1.PatchTypeApplyConfiguration,
|
||||
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
|
||||
Expression: `Object{
|
||||
metadata: Object.metadata{
|
||||
labels: {"environment": "production"}
|
||||
}
|
||||
}`}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Bindings: []*PolicyBinding{{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
|
||||
PolicyName: "policy1",
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
Policy: &v1alpha1.MutatingAdmissionPolicy{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "policy2",
|
||||
},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicySpec{
|
||||
MatchConstraints: &v1alpha1.MatchResources{
|
||||
MatchPolicy: ptr.To(v1alpha1.Equivalent),
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
ResourceRules: []v1alpha1.NamedRuleWithOperations{
|
||||
{
|
||||
RuleWithOperations: v1alpha1.RuleWithOperations{
|
||||
Rule: v1alpha1.Rule{
|
||||
APIGroups: []string{"apps"},
|
||||
APIVersions: []string{"v1"},
|
||||
Resources: []string{"deployments"},
|
||||
},
|
||||
Operations: []admissionregistrationv1.OperationType{"*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MatchConditions: []v1alpha1.MatchCondition{
|
||||
{
|
||||
Name: "prodonly",
|
||||
Expression: `object.?metadata.labels["environment"].orValue("") == "production"`,
|
||||
},
|
||||
},
|
||||
Mutations: []v1alpha1.Mutation{{
|
||||
PatchType: v1alpha1.PatchTypeApplyConfiguration,
|
||||
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
|
||||
Expression: `Object{
|
||||
metadata: Object.metadata{
|
||||
labels: {"policy1invoked": "true"}
|
||||
}
|
||||
}`}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Bindings: []*PolicyBinding{{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
|
||||
PolicyName: "policy2",
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
expect: &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "d1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"environment": "production",
|
||||
"policy1invoked": "true",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Volumes: []corev1.Volume{{Name: "x"}},
|
||||
},
|
||||
},
|
||||
Strategy: appsv1.DeploymentStrategy{
|
||||
Type: appsv1.RollingUpdateDeploymentStrategyType,
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
// TODO: This behavior pre-exists with webhook match conditions but should be reconsidered
|
||||
name: "1st policy still does not match after 2nd policy sets match condition",
|
||||
gvk: deploymentGVK,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Deployment",
|
||||
APIVersion: "apps/v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "d1",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Volumes: []corev1.Volume{{Name: "x"}},
|
||||
},
|
||||
},
|
||||
Strategy: appsv1.DeploymentStrategy{
|
||||
Type: appsv1.RollingUpdateDeploymentStrategyType,
|
||||
},
|
||||
}},
|
||||
policyHooks: []generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]{
|
||||
{
|
||||
Policy: &v1alpha1.MutatingAdmissionPolicy{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "policy1",
|
||||
},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicySpec{
|
||||
MatchConstraints: &v1alpha1.MatchResources{
|
||||
MatchPolicy: ptr.To(v1alpha1.Equivalent),
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
ResourceRules: []v1alpha1.NamedRuleWithOperations{
|
||||
{
|
||||
RuleWithOperations: v1alpha1.RuleWithOperations{
|
||||
Rule: v1alpha1.Rule{
|
||||
APIGroups: []string{"apps"},
|
||||
APIVersions: []string{"v1"},
|
||||
Resources: []string{"deployments"},
|
||||
},
|
||||
Operations: []admissionregistrationv1.OperationType{"*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
MatchConditions: []v1alpha1.MatchCondition{
|
||||
{
|
||||
Name: "prodonly",
|
||||
Expression: `object.?metadata.labels["environment"].orValue("") == "production"`,
|
||||
},
|
||||
},
|
||||
Mutations: []v1alpha1.Mutation{{
|
||||
PatchType: v1alpha1.PatchTypeApplyConfiguration,
|
||||
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
|
||||
Expression: `Object{
|
||||
metadata: Object.metadata{
|
||||
labels: {"policy1invoked": "true"}
|
||||
}
|
||||
}`}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Bindings: []*PolicyBinding{{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
|
||||
PolicyName: "policy1",
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
Policy: &v1alpha1.MutatingAdmissionPolicy{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "policy2",
|
||||
},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicySpec{
|
||||
MatchConstraints: &v1alpha1.MatchResources{
|
||||
MatchPolicy: ptr.To(v1alpha1.Equivalent),
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
ResourceRules: []v1alpha1.NamedRuleWithOperations{
|
||||
{
|
||||
RuleWithOperations: v1alpha1.RuleWithOperations{
|
||||
Rule: v1alpha1.Rule{
|
||||
APIGroups: []string{"apps"},
|
||||
APIVersions: []string{"v1"},
|
||||
Resources: []string{"deployments"},
|
||||
},
|
||||
Operations: []admissionregistrationv1.OperationType{"*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Mutations: []v1alpha1.Mutation{{
|
||||
PatchType: v1alpha1.PatchTypeApplyConfiguration,
|
||||
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
|
||||
Expression: `Object{
|
||||
metadata: Object.metadata{
|
||||
labels: {"environment": "production"}
|
||||
}
|
||||
}`}},
|
||||
},
|
||||
},
|
||||
},
|
||||
Bindings: []*PolicyBinding{{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
|
||||
PolicyName: "policy2",
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
expect: &appsv1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "d1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"environment": "production",
|
||||
},
|
||||
},
|
||||
Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Volumes: []corev1.Volume{{Name: "x"}},
|
||||
},
|
||||
},
|
||||
Strategy: appsv1.DeploymentStrategy{
|
||||
Type: appsv1.RollingUpdateDeploymentStrategyType,
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
tcManager := patch.NewTypeConverterManager(nil, openapitest.NewEmbeddedFileClient())
|
||||
go tcManager.Run(ctx)
|
||||
|
||||
err := wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, time.Second, true, func(context.Context) (done bool, err error) {
|
||||
converter := tcManager.GetTypeConverter(deploymentGVK)
|
||||
return converter != nil, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
err = appsv1.AddToScheme(scheme)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = corev1.AddToScheme(scheme)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Register a fake defaulter since registering the full defaulter adds noise
|
||||
// and creates dep cycles.
|
||||
scheme.AddTypeDefaultingFunc(&appsv1.Deployment{},
|
||||
func(obj interface{}) { fakeSetDefaultForDeployment(obj.(*appsv1.Deployment)) })
|
||||
|
||||
objectInterfaces := admission.NewObjectInterfacesFromScheme(scheme)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
client := fake.NewClientset(tc.params...)
|
||||
|
||||
// always include default namespace
|
||||
err := client.Tracker().Add(&corev1.Namespace{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "Namespace",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
},
|
||||
Spec: corev1.NamespaceSpec{},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
informerFactory := informers.NewSharedInformerFactory(client, 0)
|
||||
matcher := matching.NewMatcher(informerFactory.Core().V1().Namespaces().Lister(), client)
|
||||
paramInformer, err := informerFactory.ForResource(schema.GroupVersionResource{Version: "v1", Resource: "configmaps"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
informerFactory.Start(ctx.Done())
|
||||
informerFactory.WaitForCacheSync(ctx.Done())
|
||||
for i, h := range tc.policyHooks {
|
||||
tc.policyHooks[i].ParamInformer = paramInformer
|
||||
tc.policyHooks[i].ParamScope = testParamScope{}
|
||||
tc.policyHooks[i].Evaluator = compilePolicy(h.Policy)
|
||||
}
|
||||
|
||||
dispatcher := NewDispatcher(fakeAuthorizer{}, matcher, tcManager)
|
||||
err = dispatcher.Start(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("error starting dispatcher: %v", err)
|
||||
}
|
||||
|
||||
metaAccessor, err := meta.Accessor(tc.object)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
attrs := admission.NewAttributesRecord(tc.object, tc.oldObject, tc.gvk,
|
||||
metaAccessor.GetNamespace(), metaAccessor.GetName(), tc.gvr,
|
||||
"", admission.Create, &metav1.CreateOptions{}, false, nil)
|
||||
vAttrs := &admission.VersionedAttributes{
|
||||
Attributes: attrs,
|
||||
VersionedKind: tc.gvk,
|
||||
VersionedObject: tc.object,
|
||||
VersionedOldObject: tc.oldObject,
|
||||
}
|
||||
|
||||
err = dispatcher.Dispatch(ctx, vAttrs, objectInterfaces, tc.policyHooks)
|
||||
if err != nil {
|
||||
t.Fatalf("error dispatching policy hooks: %v", err)
|
||||
}
|
||||
|
||||
obj := vAttrs.VersionedObject
|
||||
if !equality.Semantic.DeepEqual(obj, tc.expect) {
|
||||
t.Errorf("unexpected result, got diff:\n%s\n", cmp.Diff(tc.expect, obj))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testParamScope struct{}
|
||||
|
||||
func (t testParamScope) Name() meta.RESTScopeName {
|
||||
return meta.RESTScopeNameNamespace
|
||||
}
|
||||
|
||||
var _ meta.RESTScope = testParamScope{}
|
||||
|
||||
func fakeSetDefaultForDeployment(obj *appsv1.Deployment) {
|
||||
// Just default strategy type so the tests have a defaulted field to observe
|
||||
strategy := &obj.Spec.Strategy
|
||||
if strategy.Type == "" {
|
||||
strategy.Type = appsv1.RollingUpdateDeploymentStrategyType
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
Copyright 2024 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 patch
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/managedfields"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
)
|
||||
|
||||
// Patcher provides a patch function to perform a mutation to an object in the admission chain.
|
||||
type Patcher interface {
|
||||
// Patch returns a copy of the object in the request, modified to change specified by the patch.
|
||||
// The original object in the request MUST NOT be modified in-place.
|
||||
Patch(ctx context.Context, request Request, runtimeCELCostBudget int64) (runtime.Object, error)
|
||||
}
|
||||
|
||||
// Request defines the arguments required by a patcher.
|
||||
type Request struct {
|
||||
MatchedResource schema.GroupVersionResource
|
||||
VersionedAttributes *admission.VersionedAttributes
|
||||
ObjectInterfaces admission.ObjectInterfaces
|
||||
OptionalVariables cel.OptionalVariableBindings
|
||||
Namespace *v1.Namespace
|
||||
TypeConverter managedfields.TypeConverter
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
Copyright 2024 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 patch
|
||||
|
||||
import (
|
||||
"context"
|
||||
gojson "encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
celgo "github.com/google/cel-go/cel"
|
||||
"reflect"
|
||||
"strconv"
|
||||
|
||||
"github.com/google/cel-go/common/types"
|
||||
"github.com/google/cel-go/common/types/traits"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
jsonpatch "gopkg.in/evanphx/json-patch.v4"
|
||||
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer/json"
|
||||
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
"k8s.io/apiserver/pkg/cel/mutation"
|
||||
"k8s.io/apiserver/pkg/cel/mutation/dynamic"
|
||||
pointer "k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
// JSONPatchCondition contains the inputs needed to compile and evaluate a cel expression
|
||||
// that returns a JSON patch value.
|
||||
type JSONPatchCondition struct {
|
||||
Expression string
|
||||
}
|
||||
|
||||
var _ plugincel.ExpressionAccessor = &JSONPatchCondition{}
|
||||
|
||||
func (v *JSONPatchCondition) GetExpression() string {
|
||||
return v.Expression
|
||||
}
|
||||
|
||||
func (v *JSONPatchCondition) ReturnTypes() []*celgo.Type {
|
||||
return []*celgo.Type{celgo.ListType(jsonPatchType)}
|
||||
}
|
||||
|
||||
var jsonPatchType = types.NewObjectType("JSONPatch")
|
||||
|
||||
// NewJSONPatcher creates a patcher that performs a JSON Patch mutation.
|
||||
func NewJSONPatcher(patchEvaluator plugincel.MutatingEvaluator) Patcher {
|
||||
return &jsonPatcher{patchEvaluator}
|
||||
}
|
||||
|
||||
type jsonPatcher struct {
|
||||
PatchEvaluator plugincel.MutatingEvaluator
|
||||
}
|
||||
|
||||
func (e *jsonPatcher) Patch(ctx context.Context, r Request, runtimeCELCostBudget int64) (runtime.Object, error) {
|
||||
admissionRequest := plugincel.CreateAdmissionRequest(
|
||||
r.VersionedAttributes.Attributes,
|
||||
metav1.GroupVersionResource(r.MatchedResource),
|
||||
metav1.GroupVersionKind(r.VersionedAttributes.VersionedKind))
|
||||
|
||||
compileErrors := e.PatchEvaluator.CompilationErrors()
|
||||
if len(compileErrors) > 0 {
|
||||
return nil, errors.Join(compileErrors...)
|
||||
}
|
||||
patchObj, _, err := e.evaluatePatchExpression(ctx, e.PatchEvaluator, runtimeCELCostBudget, r, admissionRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
o := r.ObjectInterfaces
|
||||
jsonSerializer := json.NewSerializerWithOptions(json.DefaultMetaFactory, o.GetObjectCreater(), o.GetObjectTyper(), json.SerializerOptions{Pretty: false, Strict: true})
|
||||
objJS, err := runtime.Encode(jsonSerializer, r.VersionedAttributes.VersionedObject)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create JSON patch: %w", err)
|
||||
}
|
||||
patchedJS, err := patchObj.Apply(objJS)
|
||||
if err != nil {
|
||||
if errors.Is(err, jsonpatch.ErrTestFailed) {
|
||||
// If a json patch fails a test operation, the patch must not be applied
|
||||
return r.VersionedAttributes.VersionedObject, nil
|
||||
}
|
||||
return nil, fmt.Errorf("JSON Patch: %w", err)
|
||||
}
|
||||
|
||||
var newVersionedObject runtime.Object
|
||||
if _, ok := r.VersionedAttributes.VersionedObject.(*unstructured.Unstructured); ok {
|
||||
newVersionedObject = &unstructured.Unstructured{}
|
||||
} else {
|
||||
newVersionedObject, err = o.GetObjectCreater().New(r.VersionedAttributes.VersionedKind)
|
||||
if err != nil {
|
||||
return nil, apierrors.NewInternalError(err)
|
||||
}
|
||||
}
|
||||
|
||||
if newVersionedObject, _, err = jsonSerializer.Decode(patchedJS, nil, newVersionedObject); err != nil {
|
||||
return nil, apierrors.NewInternalError(err)
|
||||
}
|
||||
|
||||
return newVersionedObject, nil
|
||||
}
|
||||
|
||||
func (e *jsonPatcher) evaluatePatchExpression(ctx context.Context, patchEvaluator plugincel.MutatingEvaluator, remainingBudget int64, r Request, admissionRequest *admissionv1.AdmissionRequest) (jsonpatch.Patch, int64, error) {
|
||||
var err error
|
||||
var eval plugincel.EvaluationResult
|
||||
eval, remainingBudget, err = patchEvaluator.ForInput(ctx, r.VersionedAttributes, admissionRequest, r.OptionalVariables, r.Namespace, remainingBudget)
|
||||
if err != nil {
|
||||
return nil, -1, err
|
||||
}
|
||||
if eval.Error != nil {
|
||||
return nil, -1, eval.Error
|
||||
}
|
||||
refVal := eval.EvalResult
|
||||
|
||||
// the return type can be any valid CEL value.
|
||||
// Scalars, maps and lists are used to set the value when the path points to a field of that type.
|
||||
// ObjectVal is used when the path points to a struct. A map like "{"field1": 1, "fieldX": bool}" is not
|
||||
// possible in Kubernetes CEL because maps and lists may not have mixed types.
|
||||
|
||||
iter, ok := refVal.(traits.Lister)
|
||||
if !ok {
|
||||
// Should never happen since compiler checks return type.
|
||||
return nil, -1, fmt.Errorf("type mismatch: JSONPatchType.expression should evaluate to array")
|
||||
}
|
||||
result := jsonpatch.Patch{}
|
||||
for it := iter.Iterator(); it.HasNext() == types.True; {
|
||||
v := it.Next()
|
||||
patchObj, err := v.ConvertToNative(reflect.TypeOf(&mutation.JSONPatchVal{}))
|
||||
if err != nil {
|
||||
// Should never happen since return type is checked by compiler.
|
||||
return nil, -1, fmt.Errorf("type mismatch: JSONPatchType.expression should evaluate to array of JSONPatch: %w", err)
|
||||
}
|
||||
op, ok := patchObj.(*mutation.JSONPatchVal)
|
||||
if !ok {
|
||||
// Should never happen since return type is checked by compiler.
|
||||
return nil, -1, fmt.Errorf("type mismatch: JSONPatchType.expression should evaluate to array of JSONPatch, got element of %T", patchObj)
|
||||
}
|
||||
|
||||
// Construct a JSON Patch from the evaluated CEL expression
|
||||
resultOp := jsonpatch.Operation{}
|
||||
resultOp["op"] = pointer.To(gojson.RawMessage(strconv.Quote(op.Op)))
|
||||
resultOp["path"] = pointer.To(gojson.RawMessage(strconv.Quote(op.Path)))
|
||||
if len(op.From) > 0 {
|
||||
resultOp["from"] = pointer.To(gojson.RawMessage(strconv.Quote(op.From)))
|
||||
}
|
||||
if op.Val != nil {
|
||||
if objVal, ok := op.Val.(*dynamic.ObjectVal); ok {
|
||||
// TODO: Object initializers are insufficiently type checked.
|
||||
// In the interim, we use this sanity check to detect type mismatches
|
||||
// between field names and Object initializers. For example,
|
||||
// "Object.spec{ selector: Object.spec.wrong{}}" is detected as a mismatch.
|
||||
// Before beta, attaching full type information both to Object initializers and
|
||||
// the "object" and "oldObject" variables is needed. This will allow CEL to
|
||||
// perform comprehensive runtime type checking.
|
||||
err := objVal.CheckTypeNamesMatchFieldPathNames()
|
||||
if err != nil {
|
||||
return nil, -1, fmt.Errorf("type mismatch: %w", err)
|
||||
}
|
||||
}
|
||||
// CEL data literals representing arbitrary JSON values can be serialized to JSON for use in
|
||||
// JSON Patch if first converted to pb.Value.
|
||||
v, err := op.Val.ConvertToNative(reflect.TypeOf(&structpb.Value{}))
|
||||
if err != nil {
|
||||
return nil, -1, fmt.Errorf("JSONPath valueExpression evaluated to a type that could not marshal to JSON: %w", err)
|
||||
}
|
||||
b, err := gojson.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, -1, fmt.Errorf("JSONPath valueExpression evaluated to a type that could not marshal to JSON: %w", err)
|
||||
}
|
||||
resultOp["value"] = pointer.To[gojson.RawMessage](b)
|
||||
}
|
||||
|
||||
result = append(result, resultOp)
|
||||
}
|
||||
|
||||
return result, remainingBudget, nil
|
||||
}
|
|
@ -0,0 +1,476 @@
|
|||
/*
|
||||
Copyright 2024 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 patch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func TestJSONPatch(t *testing.T) {
|
||||
deploymentGVR := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
|
||||
tests := []struct {
|
||||
name string
|
||||
expression string
|
||||
gvr schema.GroupVersionResource
|
||||
object, oldObject runtime.Object
|
||||
expectedResult runtime.Object
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "jsonPatch with false test operation",
|
||||
expression: `[
|
||||
JSONPatch{op: "test", path: "/spec/replicas", value: 100},
|
||||
JSONPatch{op: "replace", path: "/spec/replicas", value: 3},
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
|
||||
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
|
||||
},
|
||||
{
|
||||
name: "jsonPatch with true test operation",
|
||||
expression: `[
|
||||
JSONPatch{op: "test", path: "/spec/replicas", value: 1},
|
||||
JSONPatch{op: "replace", path: "/spec/replicas", value: 3},
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
|
||||
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](3)}},
|
||||
},
|
||||
{
|
||||
name: "jsonPatch remove to unset field",
|
||||
expression: `[
|
||||
JSONPatch{op: "remove", path: "/spec/replicas"},
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
|
||||
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{}},
|
||||
},
|
||||
{
|
||||
name: "jsonPatch remove map entry by key",
|
||||
expression: `[
|
||||
JSONPatch{op: "remove", path: "/metadata/labels/y"},
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1", "y": "1"}}, Spec: appsv1.DeploymentSpec{}},
|
||||
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1"}}, Spec: appsv1.DeploymentSpec{}},
|
||||
},
|
||||
{
|
||||
name: "jsonPatch remove element in list",
|
||||
expression: `[
|
||||
JSONPatch{op: "remove", path: "/spec/template/spec/containers/1"},
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{Name: "a"}, {Name: "b"}, {Name: "c"}},
|
||||
}}}},
|
||||
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{Name: "a"}, {Name: "c"}},
|
||||
}}}},
|
||||
},
|
||||
{
|
||||
name: "jsonPatch copy map entry by key",
|
||||
expression: `[
|
||||
JSONPatch{op: "copy", from: "/metadata/labels/x", path: "/metadata/labels/y"},
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1"}}, Spec: appsv1.DeploymentSpec{}},
|
||||
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1", "y": "1"}}, Spec: appsv1.DeploymentSpec{}},
|
||||
},
|
||||
{
|
||||
name: "jsonPatch copy first element to end of list",
|
||||
expression: `[
|
||||
JSONPatch{op: "copy", from: "/spec/template/spec/containers/0", path: "/spec/template/spec/containers/-"},
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{Name: "a"}, {Name: "b"}, {Name: "c"}},
|
||||
}}}},
|
||||
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{Name: "a"}, {Name: "b"}, {Name: "c"}, {Name: "a"}},
|
||||
}}}},
|
||||
},
|
||||
{
|
||||
name: "jsonPatch move map entry by key",
|
||||
expression: `[
|
||||
JSONPatch{op: "move", from: "/metadata/labels/x", path: "/metadata/labels/y"},
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1"}}, Spec: appsv1.DeploymentSpec{}},
|
||||
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "1"}}, Spec: appsv1.DeploymentSpec{}},
|
||||
},
|
||||
{
|
||||
name: "jsonPatch move first element to end of list",
|
||||
expression: `[
|
||||
JSONPatch{op: "move", from: "/spec/template/spec/containers/0", path: "/spec/template/spec/containers/-"},
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{Name: "a"}, {Name: "b"}, {Name: "c"}},
|
||||
}}}},
|
||||
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{Name: "b"}, {Name: "c"}, {Name: "a"}},
|
||||
}}}},
|
||||
},
|
||||
{
|
||||
name: "jsonPatch add map entry by key and value",
|
||||
expression: `[
|
||||
JSONPatch{op: "add", path: "/metadata/labels/x", value: "2"},
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "1"}}, Spec: appsv1.DeploymentSpec{}},
|
||||
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "1", "x": "2"}}, Spec: appsv1.DeploymentSpec{}},
|
||||
},
|
||||
{
|
||||
name: "jsonPatch add map value to field",
|
||||
expression: `[
|
||||
JSONPatch{op: "add", path: "/metadata/labels", value: {"y": "2"}},
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{}},
|
||||
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "2"}}, Spec: appsv1.DeploymentSpec{}},
|
||||
},
|
||||
{
|
||||
name: "jsonPatch add map to existing map", // performs a replacement
|
||||
expression: `[
|
||||
JSONPatch{op: "add", path: "/metadata/labels", value: {"y": "2"}},
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1"}}, Spec: appsv1.DeploymentSpec{}},
|
||||
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "2"}}, Spec: appsv1.DeploymentSpec{}},
|
||||
},
|
||||
{
|
||||
name: "jsonPatch add to start of list",
|
||||
expression: `[
|
||||
JSONPatch{op: "add", path: "/spec/template/spec/containers/0", value: {"name": "x"}},
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{Name: "a"}},
|
||||
}}}},
|
||||
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{Name: "x"}, {Name: "a"}},
|
||||
}}}},
|
||||
},
|
||||
{
|
||||
name: "jsonPatch add to end of list",
|
||||
expression: `[
|
||||
JSONPatch{op: "add", path: "/spec/template/spec/containers/-", value: {"name": "x"}},
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{Name: "a"}},
|
||||
}}}},
|
||||
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{Name: "a"}, {Name: "x"}},
|
||||
}}}},
|
||||
},
|
||||
{
|
||||
name: "jsonPatch replace key in map",
|
||||
expression: `[
|
||||
JSONPatch{op: "replace", path: "/metadata/labels/x", value: "2"},
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "1"}}, Spec: appsv1.DeploymentSpec{}},
|
||||
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "1", "x": "2"}}, Spec: appsv1.DeploymentSpec{}},
|
||||
},
|
||||
{
|
||||
name: "jsonPatch replace map value of unset field", // adds the field value
|
||||
expression: `[
|
||||
JSONPatch{op: "replace", path: "/metadata/labels", value: {"y": "2"}},
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{}},
|
||||
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "2"}}, Spec: appsv1.DeploymentSpec{}},
|
||||
},
|
||||
{
|
||||
name: "jsonPatch replace map value of set field",
|
||||
expression: `[
|
||||
JSONPatch{op: "replace", path: "/metadata/labels", value: {"y": "2"}},
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"x": "1"}}, Spec: appsv1.DeploymentSpec{}},
|
||||
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"y": "2"}}, Spec: appsv1.DeploymentSpec{}},
|
||||
},
|
||||
{
|
||||
name: "jsonPatch replace first element in list",
|
||||
expression: `[
|
||||
JSONPatch{op: "replace", path: "/spec/template/spec/containers/0", value: {"name": "x"}},
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{Name: "a"}},
|
||||
}}}},
|
||||
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{Name: "x"}},
|
||||
}}}},
|
||||
},
|
||||
{
|
||||
name: "jsonPatch add map entry by key and value",
|
||||
expression: `[
|
||||
JSONPatch{op: "add", path: "/spec", value: Object.spec{selector: Object.spec.selector{}, replicas: 10}}
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{}},
|
||||
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Selector: &metav1.LabelSelector{}, Replicas: ptr.To[int32](10)}},
|
||||
},
|
||||
{
|
||||
name: "JSONPatch patch type has field access",
|
||||
expression: `[
|
||||
JSONPatch{
|
||||
op: "add", path: "/metadata/labels",
|
||||
value: {
|
||||
"op": JSONPatch{op: "opValue"}.op,
|
||||
"path": JSONPatch{path: "pathValue"}.path,
|
||||
"from": JSONPatch{from: "fromValue"}.from,
|
||||
"value": string(JSONPatch{value: "valueValue"}.value),
|
||||
}
|
||||
}
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}},
|
||||
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{
|
||||
"op": "opValue",
|
||||
"path": "pathValue",
|
||||
"from": "fromValue",
|
||||
"value": "valueValue",
|
||||
}}},
|
||||
},
|
||||
{
|
||||
name: "JSONPatch patch type has field testing",
|
||||
expression: `[
|
||||
JSONPatch{
|
||||
op: "add", path: "/metadata/labels",
|
||||
value: {
|
||||
"op": string(has(JSONPatch{op: "opValue"}.op)),
|
||||
"path": string(has(JSONPatch{path: "pathValue"}.path)),
|
||||
"from": string(has(JSONPatch{from: "fromValue"}.from)),
|
||||
"value": string(has(JSONPatch{value: "valueValue"}.value)),
|
||||
"op-unset": string(has(JSONPatch{}.op)),
|
||||
"path-unset": string(has(JSONPatch{}.path)),
|
||||
"from-unset": string(has(JSONPatch{}.from)),
|
||||
"value-unset": string(has(JSONPatch{}.value)),
|
||||
}
|
||||
}
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}},
|
||||
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{
|
||||
"op": "true",
|
||||
"path": "true",
|
||||
"from": "true",
|
||||
"value": "true",
|
||||
"op-unset": "false",
|
||||
"path-unset": "false",
|
||||
"from-unset": "false",
|
||||
"value-unset": "false",
|
||||
}}},
|
||||
},
|
||||
{
|
||||
name: "JSONPatch patch type equality",
|
||||
expression: `[
|
||||
JSONPatch{
|
||||
op: "add", path: "/metadata/labels",
|
||||
value: {
|
||||
"empty": string(JSONPatch{} == JSONPatch{}),
|
||||
"partial": string(JSONPatch{op: "add"} == JSONPatch{op: "add"}),
|
||||
"same-all": string(JSONPatch{op: "add", path: "path", from: "from", value: 1} == JSONPatch{op: "add", path: "path", from: "from", value: 1}),
|
||||
"different-op": string(JSONPatch{op: "add"} == JSONPatch{op: "remove"}),
|
||||
"different-path": string(JSONPatch{op: "add", path: "x", from: "from", value: 1} == JSONPatch{op: "add", path: "path", from: "from", value: 1}),
|
||||
"different-from": string(JSONPatch{op: "add", path: "path", from: "x", value: 1} == JSONPatch{op: "add", path: "path", from: "from", value: 1}),
|
||||
"different-value": string(JSONPatch{op: "add", path: "path", from: "from", value: "1"} == JSONPatch{op: "add", path: "path", from: "from", value: 1}),
|
||||
}
|
||||
}
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}},
|
||||
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{
|
||||
"empty": "true",
|
||||
"partial": "true",
|
||||
"same-all": "true",
|
||||
"different-op": "false",
|
||||
"different-path": "false",
|
||||
"different-from": "false",
|
||||
"different-value": "false",
|
||||
}}},
|
||||
},
|
||||
{
|
||||
name: "JSONPatch key escaping",
|
||||
expression: `[
|
||||
JSONPatch{
|
||||
op: "add", path: "/metadata/labels", value: {}
|
||||
},
|
||||
JSONPatch{
|
||||
op: "add", path: "/metadata/labels/" + jsonpatch.escapeKey("k8s.io/x~y"), value: "true"
|
||||
}
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{}}},
|
||||
expectedResult: &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{
|
||||
"k8s.io/x~y": "true",
|
||||
}}},
|
||||
},
|
||||
{
|
||||
name: "jsonPatch with CEL initializer",
|
||||
expression: `[
|
||||
JSONPatch{op: "add", path: "/spec/template/spec/containers/-", value: Object.spec.template.spec.containers{
|
||||
name: "x",
|
||||
ports: [Object.spec.template.spec.containers.ports{containerPort: 8080}],
|
||||
}
|
||||
},
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{Name: "a"}},
|
||||
}}}},
|
||||
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{Name: "a"}, {Name: "x", Ports: []corev1.ContainerPort{{ContainerPort: 8080}}}},
|
||||
}}}},
|
||||
},
|
||||
{
|
||||
name: "jsonPatch invalid CEL initializer field",
|
||||
expression: `[
|
||||
JSONPatch{
|
||||
op: "add", path: "/spec/template/spec/containers/-",
|
||||
value: Object.spec.template.spec.containers{
|
||||
name: "x",
|
||||
ports: [Object.spec.template.spec.containers.ports{containerPortZ: 8080}]
|
||||
}
|
||||
}
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{Name: "a"}},
|
||||
}}}},
|
||||
expectedErr: "strict decoding error: unknown field \"spec.template.spec.containers[1].ports[0].containerPortZ\"",
|
||||
},
|
||||
{
|
||||
name: "jsonPatch invalid CEL initializer type",
|
||||
expression: `[
|
||||
JSONPatch{
|
||||
op: "add", path: "/spec/template/spec/containers/-",
|
||||
value: Object.spec.template.spec.containers{
|
||||
name: "x",
|
||||
ports: [Object.spec.template.spec.containers.portsZ{containerPort: 8080}]
|
||||
}
|
||||
}
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{Name: "a"}},
|
||||
}}}},
|
||||
expectedErr: " mismatch: unexpected type name \"Object.spec.template.spec.containers.portsZ\", expected \"Object.spec.template.spec.containers.ports\", which matches field name path from root Object type",
|
||||
},
|
||||
{
|
||||
name: "jsonPatch replace end of list with - not allowed",
|
||||
expression: `[
|
||||
JSONPatch{op: "replace", path: "/spec/template/spec/containers/-", value: {"name": "x"}},
|
||||
]`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{Name: "a"}},
|
||||
}}}},
|
||||
expectedErr: "JSON Patch: replace operation does not apply: doc is missing key: /spec/template/spec/containers/-: missing value",
|
||||
},
|
||||
}
|
||||
|
||||
compiler, err := cel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
accessor := &JSONPatchCondition{Expression: tc.expression}
|
||||
compileResult := compiler.CompileMutatingEvaluator(accessor, cel.OptionalVariableDeclarations{StrictCost: true, HasPatchTypes: true}, environment.StoredExpressions)
|
||||
|
||||
patcher := jsonPatcher{PatchEvaluator: compileResult}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
err := appsv1.AddToScheme(scheme)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var gvk schema.GroupVersionKind
|
||||
gvks, _, err := scheme.ObjectKinds(tc.object)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(gvks) == 1 {
|
||||
gvk = gvks[0]
|
||||
} else {
|
||||
t.Fatalf("Failed to find gvk for type: %T", tc.object)
|
||||
}
|
||||
|
||||
metaAccessor, err := meta.Accessor(tc.object)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
attrs := admission.NewAttributesRecord(tc.object, tc.oldObject, gvk,
|
||||
metaAccessor.GetNamespace(), metaAccessor.GetName(), tc.gvr,
|
||||
"", admission.Create, &metav1.CreateOptions{}, false, nil)
|
||||
vAttrs := &admission.VersionedAttributes{
|
||||
Attributes: attrs,
|
||||
VersionedKind: gvk,
|
||||
VersionedObject: tc.object,
|
||||
VersionedOldObject: tc.oldObject,
|
||||
}
|
||||
|
||||
r := Request{
|
||||
MatchedResource: tc.gvr,
|
||||
VersionedAttributes: vAttrs,
|
||||
ObjectInterfaces: admission.NewObjectInterfacesFromScheme(scheme),
|
||||
OptionalVariables: cel.OptionalVariableBindings{},
|
||||
}
|
||||
|
||||
got, err := patcher.Patch(context.Background(), r, celconfig.RuntimeCELCostBudget)
|
||||
if len(tc.expectedErr) > 0 {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error: %s", tc.expectedErr)
|
||||
} else {
|
||||
if !strings.Contains(err.Error(), tc.expectedErr) {
|
||||
t.Fatalf("expected error: %s, got: %s", tc.expectedErr, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil && len(tc.expectedErr) == 0 {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if !equality.Semantic.DeepEqual(tc.expectedResult, got) {
|
||||
t.Errorf("unexpected result, got diff:\n%s\n", cmp.Diff(tc.expectedResult, got))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,217 @@
|
|||
/*
|
||||
Copyright 2024 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 patch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
celgo "github.com/google/cel-go/cel"
|
||||
celtypes "github.com/google/cel-go/common/types"
|
||||
"strings"
|
||||
|
||||
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/schema"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/typed"
|
||||
"sigs.k8s.io/structured-merge-diff/v4/value"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/managedfields"
|
||||
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
"k8s.io/apiserver/pkg/cel/mutation/dynamic"
|
||||
)
|
||||
|
||||
// ApplyConfigurationCondition contains the inputs needed to compile and evaluate a cel expression
|
||||
// that returns an apply configuration
|
||||
type ApplyConfigurationCondition struct {
|
||||
Expression string
|
||||
}
|
||||
|
||||
var _ plugincel.ExpressionAccessor = &ApplyConfigurationCondition{}
|
||||
|
||||
func (v *ApplyConfigurationCondition) GetExpression() string {
|
||||
return v.Expression
|
||||
}
|
||||
|
||||
func (v *ApplyConfigurationCondition) ReturnTypes() []*celgo.Type {
|
||||
return []*celgo.Type{applyConfigObjectType}
|
||||
}
|
||||
|
||||
var applyConfigObjectType = celtypes.NewObjectType("Object")
|
||||
|
||||
// NewApplyConfigurationPatcher creates a patcher that performs an applyConfiguration mutation.
|
||||
func NewApplyConfigurationPatcher(expressionEvaluator plugincel.MutatingEvaluator) Patcher {
|
||||
return &applyConfigPatcher{expressionEvaluator: expressionEvaluator}
|
||||
}
|
||||
|
||||
type applyConfigPatcher struct {
|
||||
expressionEvaluator plugincel.MutatingEvaluator
|
||||
}
|
||||
|
||||
func (e *applyConfigPatcher) Patch(ctx context.Context, r Request, runtimeCELCostBudget int64) (runtime.Object, error) {
|
||||
admissionRequest := plugincel.CreateAdmissionRequest(
|
||||
r.VersionedAttributes.Attributes,
|
||||
metav1.GroupVersionResource(r.MatchedResource),
|
||||
metav1.GroupVersionKind(r.VersionedAttributes.VersionedKind))
|
||||
|
||||
compileErrors := e.expressionEvaluator.CompilationErrors()
|
||||
if len(compileErrors) > 0 {
|
||||
return nil, errors.Join(compileErrors...)
|
||||
}
|
||||
eval, _, err := e.expressionEvaluator.ForInput(ctx, r.VersionedAttributes, admissionRequest, r.OptionalVariables, r.Namespace, runtimeCELCostBudget)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if eval.Error != nil {
|
||||
return nil, eval.Error
|
||||
}
|
||||
v := eval.EvalResult
|
||||
|
||||
// The compiler ensures that the return type is an ObjectVal with type name of "Object".
|
||||
objVal, ok := v.(*dynamic.ObjectVal)
|
||||
if !ok {
|
||||
// Should not happen since the compiler type checks the return type.
|
||||
return nil, fmt.Errorf("unsupported return type from ApplyConfiguration expression: %v", v.Type())
|
||||
}
|
||||
// TODO: Object initializers are insufficiently type checked.
|
||||
// In the interim, we use this sanity check to detect type mismatches
|
||||
// between field names and Object initializers. For example,
|
||||
// "Object.spec{ selector: Object.spec.wrong{}}" is detected as a mismatch.
|
||||
// Before beta, attaching full type information both to Object initializers and
|
||||
// the "object" and "oldObject" variables is needed. This will allow CEL to
|
||||
// perform comprehensive runtime type checking.
|
||||
err = objVal.CheckTypeNamesMatchFieldPathNames()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("type mismatch: %w", err)
|
||||
}
|
||||
|
||||
value, ok := objVal.Value().(map[string]any)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid return type: %T", v)
|
||||
}
|
||||
|
||||
patchObject := unstructured.Unstructured{Object: value}
|
||||
patchObject.SetGroupVersionKind(r.VersionedAttributes.VersionedObject.GetObjectKind().GroupVersionKind())
|
||||
patched, err := ApplyStructuredMergeDiff(r.TypeConverter, r.VersionedAttributes.VersionedObject, &patchObject)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error applying patch: %w", err)
|
||||
}
|
||||
return patched, nil
|
||||
}
|
||||
|
||||
// ApplyStructuredMergeDiff applies a structured merge diff to an object and returns a copy of the object
|
||||
// with the patch applied.
|
||||
func ApplyStructuredMergeDiff(
|
||||
typeConverter managedfields.TypeConverter,
|
||||
originalObject runtime.Object,
|
||||
patch *unstructured.Unstructured,
|
||||
) (runtime.Object, error) {
|
||||
if patch.GroupVersionKind() != originalObject.GetObjectKind().GroupVersionKind() {
|
||||
return nil, fmt.Errorf("patch (%v) and original object (%v) are not of the same gvk", patch.GroupVersionKind().String(), originalObject.GetObjectKind().GroupVersionKind().String())
|
||||
} else if typeConverter == nil {
|
||||
return nil, fmt.Errorf("type converter must not be nil")
|
||||
}
|
||||
|
||||
patchObjTyped, err := typeConverter.ObjectToTyped(patch)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert patch object to typed object: %w", err)
|
||||
}
|
||||
|
||||
err = validatePatch(patchObjTyped)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid ApplyConfiguration: %w", err)
|
||||
}
|
||||
|
||||
liveObjTyped, err := typeConverter.ObjectToTyped(originalObject)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert original object to typed object: %w", err)
|
||||
}
|
||||
|
||||
newObjTyped, err := liveObjTyped.Merge(patchObjTyped)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to merge patch: %w", err)
|
||||
}
|
||||
|
||||
// Our mutating admission policy sets the fields but does not track ownership.
|
||||
// Newly introduced fields in the patch won't be tracked by a field manager
|
||||
// (so if the original object is updated again but the mutating policy is
|
||||
// not active, the fields will be dropped).
|
||||
//
|
||||
// This necessarily means that changes to an object by a mutating policy
|
||||
// are only preserved if the policy was active at the time of the change.
|
||||
// (If the policy is not active, the changes may be dropped.)
|
||||
|
||||
newObj, err := typeConverter.TypedToObject(newObjTyped)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert typed object to object: %w", err)
|
||||
}
|
||||
|
||||
return newObj, nil
|
||||
}
|
||||
|
||||
// validatePatch searches an apply configuration for any arrays, maps or structs elements that are atomic and returns
|
||||
// an error if any are found.
|
||||
// This prevents accidental removal of fields that can occur when the user intends to modify some
|
||||
// fields in an atomic type, not realizing that all fields not explicitly set in the new value
|
||||
// of the atomic will be removed.
|
||||
func validatePatch(v *typed.TypedValue) error {
|
||||
atomics := findAtomics(nil, v.Schema(), v.TypeRef(), v.AsValue())
|
||||
if len(atomics) > 0 {
|
||||
return fmt.Errorf("may not mutate atomic arrays, maps or structs: %v", strings.Join(atomics, ", "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findAtomics returns field paths for any atomic arrays, maps or structs found when traversing the given value.
|
||||
func findAtomics(path []fieldpath.PathElement, s *schema.Schema, tr schema.TypeRef, v value.Value) (atomics []string) {
|
||||
if a, ok := s.Resolve(tr); ok { // Validation pass happens before this and checks that all schemas can be resolved
|
||||
if v.IsMap() && a.Map != nil {
|
||||
if a.Map.ElementRelationship == schema.Atomic {
|
||||
atomics = append(atomics, pathString(path))
|
||||
}
|
||||
v.AsMap().Iterate(func(key string, val value.Value) bool {
|
||||
pe := fieldpath.PathElement{FieldName: &key}
|
||||
if sf, ok := a.Map.FindField(key); ok {
|
||||
tr = sf.Type
|
||||
atomics = append(atomics, findAtomics(append(path, pe), s, tr, val)...)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
if v.IsList() && a.List != nil {
|
||||
if a.List.ElementRelationship == schema.Atomic {
|
||||
atomics = append(atomics, pathString(path))
|
||||
}
|
||||
list := v.AsList()
|
||||
for i := 0; i < list.Length(); i++ {
|
||||
pe := fieldpath.PathElement{Index: &i}
|
||||
atomics = append(atomics, findAtomics(append(path, pe), s, a.List.ElementType, list.At(i))...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return atomics
|
||||
}
|
||||
|
||||
func pathString(path []fieldpath.PathElement) string {
|
||||
sb := strings.Builder{}
|
||||
for _, p := range path {
|
||||
sb.WriteString(p.String())
|
||||
}
|
||||
return sb.String()
|
||||
}
|
|
@ -0,0 +1,346 @@
|
|||
/*
|
||||
Copyright 2024 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 patch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
"k8s.io/client-go/openapi/openapitest"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func TestApplyConfiguration(t *testing.T) {
|
||||
deploymentGVR := schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
|
||||
deploymentGVK := schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}
|
||||
tests := []struct {
|
||||
name string
|
||||
expression string
|
||||
gvr schema.GroupVersionResource
|
||||
object, oldObject runtime.Object
|
||||
expectedResult runtime.Object
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "apply configuration add to listType=map",
|
||||
expression: `Object{
|
||||
spec: Object.spec{
|
||||
template: Object.spec.template{
|
||||
spec: Object.spec.template.spec{
|
||||
volumes: [Object.spec.template.spec.volumes{
|
||||
name: "y"
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Volumes: []corev1.Volume{{Name: "x"}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Volumes: []corev1.Volume{{Name: "x"}, {Name: "y"}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "apply configuration update listType=map entry",
|
||||
expression: `Object{
|
||||
spec: Object.spec{
|
||||
template: Object.spec.template{
|
||||
spec: Object.spec.template.spec{
|
||||
volumes: [Object.spec.template.spec.volumes{
|
||||
name: "y",
|
||||
hostPath: Object.spec.template.spec.volumes.hostPath{
|
||||
path: "a"
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Volumes: []corev1.Volume{{Name: "x"}, {Name: "y"}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{
|
||||
Template: corev1.PodTemplateSpec{
|
||||
Spec: corev1.PodSpec{
|
||||
Volumes: []corev1.Volume{{Name: "x"}, {Name: "y", VolumeSource: corev1.VolumeSource{HostPath: &corev1.HostPathVolumeSource{Path: "a"}}}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "apply configuration with conditionals",
|
||||
expression: `Object{
|
||||
spec: Object.spec{
|
||||
replicas: object.spec.replicas % 2 == 0?object.spec.replicas + 1:object.spec.replicas
|
||||
}
|
||||
}`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](2)}},
|
||||
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](3)}},
|
||||
},
|
||||
{
|
||||
name: "apply configuration with old object",
|
||||
expression: `Object{
|
||||
spec: Object.spec{
|
||||
replicas: oldObject.spec.replicas % 2 == 0?oldObject.spec.replicas + 1:oldObject.spec.replicas
|
||||
}
|
||||
}`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
|
||||
oldObject: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](2)}},
|
||||
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](3)}},
|
||||
},
|
||||
{
|
||||
name: "complex apply configuration initialization",
|
||||
expression: `Object{
|
||||
spec: Object.spec{
|
||||
replicas: 1,
|
||||
template: Object.spec.template{
|
||||
metadata: Object.spec.template.metadata{
|
||||
labels: {"app": "nginx"}
|
||||
},
|
||||
spec: Object.spec.template.spec{
|
||||
containers: [Object.spec.template.spec.containers{
|
||||
name: "nginx",
|
||||
image: "nginx:1.14.2",
|
||||
ports: [Object.spec.template.spec.containers.ports{
|
||||
containerPort: 80
|
||||
}],
|
||||
resources: Object.spec.template.spec.containers.resources{
|
||||
limits: {"cpu": "128M"},
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{}},
|
||||
expectedResult: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{
|
||||
Replicas: ptr.To[int32](1),
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": "nginx"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "nginx",
|
||||
Image: "nginx:1.14.2",
|
||||
Ports: []corev1.ContainerPort{
|
||||
{ContainerPort: 80},
|
||||
},
|
||||
Resources: corev1.ResourceRequirements{
|
||||
Limits: corev1.ResourceList{corev1.ResourceName("cpu"): resource.MustParse("128M")},
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "apply configuration with change to atomic",
|
||||
expression: `Object{
|
||||
spec: Object.spec{
|
||||
selector: Object.spec.selector{
|
||||
matchLabels: {"l": "v"}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
|
||||
expectedErr: "error applying patch: invalid ApplyConfiguration: may not mutate atomic arrays, maps or structs: .spec.selector",
|
||||
},
|
||||
{
|
||||
name: "apply configuration with invalid type name",
|
||||
expression: `Object{
|
||||
spec: Object.specx{
|
||||
replicas: 1
|
||||
}
|
||||
}`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
|
||||
expectedErr: "type mismatch: unexpected type name \"Object.specx\", expected \"Object.spec\", which matches field name path from root Object type",
|
||||
},
|
||||
{
|
||||
name: "apply configuration with invalid field name",
|
||||
expression: `Object{
|
||||
spec: Object.spec{
|
||||
replicasx: 1
|
||||
}
|
||||
}`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
|
||||
expectedErr: "error applying patch: failed to convert patch object to typed object: .spec.replicasx: field not declared in schema",
|
||||
},
|
||||
{
|
||||
name: "apply configuration with invalid return type",
|
||||
expression: `"I'm a teapot!"`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
|
||||
expectedErr: "must evaluate to Object but got string",
|
||||
},
|
||||
{
|
||||
name: "apply configuration with invalid initializer return type",
|
||||
expression: `Object.spec.metadata{}`,
|
||||
gvr: deploymentGVR,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Replicas: ptr.To[int32](1)}},
|
||||
expectedErr: "must evaluate to Object but got Object.spec.metadata",
|
||||
},
|
||||
}
|
||||
|
||||
compiler, err := cel.NewCompositedCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
tcManager := NewTypeConverterManager(nil, openapitest.NewEmbeddedFileClient())
|
||||
go tcManager.Run(ctx)
|
||||
|
||||
err = wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, time.Second, true, func(context.Context) (done bool, err error) {
|
||||
converter := tcManager.GetTypeConverter(deploymentGVK)
|
||||
return converter != nil, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
accessor := &ApplyConfigurationCondition{Expression: tc.expression}
|
||||
compileResult := compiler.CompileMutatingEvaluator(accessor, cel.OptionalVariableDeclarations{StrictCost: true, HasPatchTypes: true}, environment.StoredExpressions)
|
||||
|
||||
patcher := applyConfigPatcher{expressionEvaluator: compileResult}
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
err := appsv1.AddToScheme(scheme)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var gvk schema.GroupVersionKind
|
||||
gvks, _, err := scheme.ObjectKinds(tc.object)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(gvks) == 1 {
|
||||
gvk = gvks[0]
|
||||
} else {
|
||||
t.Fatalf("Failed to find gvk for type: %T", tc.object)
|
||||
}
|
||||
|
||||
metaAccessor, err := meta.Accessor(tc.object)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
typeAccessor, err := meta.TypeAccessor(tc.object)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
typeAccessor.SetKind(gvk.Kind)
|
||||
typeAccessor.SetAPIVersion(gvk.GroupVersion().String())
|
||||
|
||||
attrs := admission.NewAttributesRecord(tc.object, tc.oldObject, gvk,
|
||||
metaAccessor.GetNamespace(), metaAccessor.GetName(), tc.gvr,
|
||||
"", admission.Create, &metav1.CreateOptions{}, false, nil)
|
||||
vAttrs := &admission.VersionedAttributes{
|
||||
Attributes: attrs,
|
||||
VersionedKind: gvk,
|
||||
VersionedObject: tc.object,
|
||||
VersionedOldObject: tc.oldObject,
|
||||
}
|
||||
|
||||
r := Request{
|
||||
MatchedResource: tc.gvr,
|
||||
VersionedAttributes: vAttrs,
|
||||
ObjectInterfaces: admission.NewObjectInterfacesFromScheme(scheme),
|
||||
OptionalVariables: cel.OptionalVariableBindings{},
|
||||
TypeConverter: tcManager.GetTypeConverter(gvk),
|
||||
}
|
||||
|
||||
patched, err := patcher.Patch(ctx, r, celconfig.RuntimeCELCostBudget)
|
||||
if len(tc.expectedErr) > 0 {
|
||||
if err == nil {
|
||||
t.Fatalf("expected error: %s", tc.expectedErr)
|
||||
} else {
|
||||
if !strings.Contains(err.Error(), tc.expectedErr) {
|
||||
t.Fatalf("expected error: %s, got: %s", tc.expectedErr, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil && len(tc.expectedErr) == 0 {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
got, err := runtime.DefaultUnstructuredConverter.ToUnstructured(patched)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wantTypeAccessor, err := meta.TypeAccessor(tc.expectedResult)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantTypeAccessor.SetKind(gvk.Kind)
|
||||
wantTypeAccessor.SetAPIVersion(gvk.GroupVersion().String())
|
||||
|
||||
want, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.expectedResult)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !equality.Semantic.DeepEqual(want, got) {
|
||||
t.Errorf("unexpected result, got diff:\n%s\n", cmp.Diff(want, got))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
Copyright 2024 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 patch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/managedfields"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/openapi"
|
||||
"k8s.io/kube-openapi/pkg/spec3"
|
||||
)
|
||||
|
||||
type TypeConverterManager interface {
|
||||
// GetTypeConverter returns a type converter for the given GVK
|
||||
GetTypeConverter(gvk schema.GroupVersionKind) managedfields.TypeConverter
|
||||
Run(ctx context.Context)
|
||||
}
|
||||
|
||||
func NewTypeConverterManager(
|
||||
staticTypeConverter managedfields.TypeConverter,
|
||||
openapiClient openapi.Client,
|
||||
) TypeConverterManager {
|
||||
return &typeConverterManager{
|
||||
staticTypeConverter: staticTypeConverter,
|
||||
openapiClient: openapiClient,
|
||||
typeConverterMap: make(map[schema.GroupVersion]typeConverterCacheEntry),
|
||||
lastFetchedPaths: make(map[schema.GroupVersion]openapi.GroupVersion),
|
||||
}
|
||||
}
|
||||
|
||||
type typeConverterCacheEntry struct {
|
||||
typeConverter managedfields.TypeConverter
|
||||
entry openapi.GroupVersion
|
||||
}
|
||||
|
||||
// typeConverterManager helps us make sure we have an up to date schema and
|
||||
// type converter for our openapi models. It should be connfigured to use a
|
||||
// static type converter for natively typed schemas, and fetches the schema
|
||||
// for CRDs/other over the network on demand (trying to reduce network calls where necessary)
|
||||
type typeConverterManager struct {
|
||||
// schemaCache is used to cache the schema for a given GVK
|
||||
staticTypeConverter managedfields.TypeConverter
|
||||
|
||||
// discoveryClient is used to fetch the schema for a given GVK
|
||||
openapiClient openapi.Client
|
||||
|
||||
lock sync.RWMutex
|
||||
|
||||
typeConverterMap map[schema.GroupVersion]typeConverterCacheEntry
|
||||
lastFetchedPaths map[schema.GroupVersion]openapi.GroupVersion
|
||||
}
|
||||
|
||||
func (t *typeConverterManager) Run(ctx context.Context) {
|
||||
// Loop every 5s refershing the OpenAPI schema list to know which
|
||||
// schemas have been invalidated. This should use e-tags under the hood
|
||||
_ = wait.PollUntilContextCancel(ctx, 5*time.Second, true, func(_ context.Context) (done bool, err error) {
|
||||
paths, err := t.openapiClient.Paths()
|
||||
if err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("failed to fetch openapi paths: %w", err))
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// The /openapi/v3 endpoint contains a list of paths whose ServerRelativeURL
|
||||
// value changes every time the schema is updated. So we poll /openapi/v3
|
||||
// to get the "version number" for each schema, and invalidate our cache
|
||||
// if the version number has changed since we pulled it.
|
||||
parsedPaths := make(map[schema.GroupVersion]openapi.GroupVersion, len(paths))
|
||||
for path, entry := range paths {
|
||||
if !strings.HasPrefix(path, "apis/") && !strings.HasPrefix(path, "api/") {
|
||||
continue
|
||||
}
|
||||
path = strings.TrimPrefix(path, "apis/")
|
||||
path = strings.TrimPrefix(path, "api/")
|
||||
|
||||
gv, err := schema.ParseGroupVersion(path)
|
||||
if err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("failed to parse group version %q: %w", path, err))
|
||||
return false, nil
|
||||
}
|
||||
|
||||
parsedPaths[gv] = entry
|
||||
}
|
||||
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
t.lastFetchedPaths = parsedPaths
|
||||
return false, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (t *typeConverterManager) GetTypeConverter(gvk schema.GroupVersionKind) managedfields.TypeConverter {
|
||||
// Check to see if the static type converter handles this GVK
|
||||
if t.staticTypeConverter != nil {
|
||||
//!TODO: Add ability to check existence to type converter
|
||||
// working around for now but seeing if getting a typed version of an
|
||||
// empty object returns error
|
||||
stub := &unstructured.Unstructured{}
|
||||
stub.SetGroupVersionKind(gvk)
|
||||
|
||||
if _, err := t.staticTypeConverter.ObjectToTyped(stub); err == nil {
|
||||
return t.staticTypeConverter
|
||||
}
|
||||
}
|
||||
|
||||
gv := gvk.GroupVersion()
|
||||
|
||||
existing, entry, err := func() (managedfields.TypeConverter, openapi.GroupVersion, error) {
|
||||
t.lock.RLock()
|
||||
defer t.lock.RUnlock()
|
||||
|
||||
// If schema is not supported by static type converter, ask discovery
|
||||
// for the schema
|
||||
entry, ok := t.lastFetchedPaths[gv]
|
||||
if !ok {
|
||||
// If we can't get the schema, we can't do anything
|
||||
return nil, nil, fmt.Errorf("no schema for %v", gvk)
|
||||
}
|
||||
|
||||
// If the entry schema has not changed, used the same type converter
|
||||
if existing, ok := t.typeConverterMap[gv]; ok && existing.entry.ServerRelativeURL() == entry.ServerRelativeURL() {
|
||||
// If we have a type converter for this GVK, return it
|
||||
return existing.typeConverter, existing.entry, nil
|
||||
}
|
||||
|
||||
return nil, entry, nil
|
||||
}()
|
||||
if err != nil {
|
||||
utilruntime.HandleError(err)
|
||||
return nil
|
||||
} else if existing != nil {
|
||||
return existing
|
||||
}
|
||||
|
||||
schBytes, err := entry.Schema(runtime.ContentTypeJSON)
|
||||
if err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("failed to get schema for %v: %w", gvk, err))
|
||||
return nil
|
||||
}
|
||||
|
||||
var sch spec3.OpenAPI
|
||||
if err := json.Unmarshal(schBytes, &sch); err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("failed to unmarshal schema for %v: %w", gvk, err))
|
||||
return nil
|
||||
}
|
||||
|
||||
// The schema has changed, or there is no entry for it, generate
|
||||
// a new type converter for this GV
|
||||
tc, err := managedfields.NewTypeConverter(sch.Components.Schemas, false)
|
||||
if err != nil {
|
||||
utilruntime.HandleError(fmt.Errorf("failed to create type converter for %v: %w", gvk, err))
|
||||
return nil
|
||||
}
|
||||
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
|
||||
t.typeConverterMap[gv] = typeConverterCacheEntry{
|
||||
typeConverter: tc,
|
||||
entry: entry,
|
||||
}
|
||||
|
||||
return tc
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
Copyright 2024 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 patch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/openapi/openapitest"
|
||||
)
|
||||
|
||||
func TestTypeConverter(t *testing.T) {
|
||||
deploymentGVK := schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}
|
||||
tests := []struct {
|
||||
name string
|
||||
gvk schema.GroupVersionKind
|
||||
object runtime.Object
|
||||
}{
|
||||
{
|
||||
name: "simple round trip",
|
||||
gvk: deploymentGVK,
|
||||
object: &appsv1.Deployment{Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{Name: "a"}, {Name: "x", Ports: []corev1.ContainerPort{{ContainerPort: 8080}}}},
|
||||
}}}},
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
tcManager := NewTypeConverterManager(nil, openapitest.NewEmbeddedFileClient())
|
||||
go tcManager.Run(ctx)
|
||||
|
||||
err := wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, time.Second, true, func(context.Context) (done bool, err error) {
|
||||
converter := tcManager.GetTypeConverter(deploymentGVK)
|
||||
return converter != nil, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
typeAccessor, err := meta.TypeAccessor(tc.object)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
typeAccessor.SetKind(tc.gvk.Kind)
|
||||
typeAccessor.SetAPIVersion(tc.gvk.GroupVersion().String())
|
||||
|
||||
converter := tcManager.GetTypeConverter(tc.gvk)
|
||||
if converter == nil {
|
||||
t.Errorf("nil TypeConverter")
|
||||
}
|
||||
typedObject, err := converter.ObjectToTyped(tc.object)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
roundTripped, err := converter.TypedToObject(typedObject)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := runtime.DefaultUnstructuredConverter.ToUnstructured(roundTripped)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.object)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !equality.Semantic.DeepEqual(want, got) {
|
||||
t.Errorf("unexpected result, got diff:\n%s\n", cmp.Diff(want, got))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
Copyright 2024 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 mutating
|
||||
|
||||
import (
|
||||
"context"
|
||||
celgo "github.com/google/cel-go/cel"
|
||||
"io"
|
||||
|
||||
"k8s.io/api/admissionregistration/v1alpha1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/managedfields"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/component-base/featuregate"
|
||||
)
|
||||
|
||||
const (
|
||||
// PluginName indicates the name of admission plug-in
|
||||
PluginName = "MutatingAdmissionPolicy"
|
||||
)
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func(configFile io.Reader) (admission.Interface, error) {
|
||||
return NewPlugin(configFile), nil
|
||||
})
|
||||
}
|
||||
|
||||
type Policy = v1alpha1.MutatingAdmissionPolicy
|
||||
type PolicyBinding = v1alpha1.MutatingAdmissionPolicyBinding
|
||||
type PolicyMutation = v1alpha1.Mutation
|
||||
type PolicyHook = generic.PolicyHook[*Policy, *PolicyBinding, PolicyEvaluator]
|
||||
|
||||
type Mutator struct {
|
||||
}
|
||||
type MutationEvaluationFunc func(
|
||||
ctx context.Context,
|
||||
matchedResource schema.GroupVersionResource,
|
||||
versionedAttr *admission.VersionedAttributes,
|
||||
o admission.ObjectInterfaces,
|
||||
versionedParams runtime.Object,
|
||||
namespace *corev1.Namespace,
|
||||
typeConverter managedfields.TypeConverter,
|
||||
runtimeCELCostBudget int64,
|
||||
authorizer authorizer.Authorizer,
|
||||
) (runtime.Object, error)
|
||||
|
||||
type PolicyEvaluator struct {
|
||||
Matcher matchconditions.Matcher
|
||||
Mutators []patch.Patcher
|
||||
CompositionEnv *cel.CompositionEnv
|
||||
Error error
|
||||
}
|
||||
|
||||
// Plugin is an implementation of admission.Interface.
|
||||
type Plugin struct {
|
||||
*generic.Plugin[PolicyHook]
|
||||
}
|
||||
|
||||
var _ admission.Interface = &Plugin{}
|
||||
var _ admission.MutationInterface = &Plugin{}
|
||||
|
||||
// NewPlugin returns a generic admission webhook plugin.
|
||||
func NewPlugin(_ io.Reader) *Plugin {
|
||||
// There is no request body to mutate for DELETE, so this plugin never handles that operation.
|
||||
handler := admission.NewHandler(admission.Create, admission.Update, admission.Connect)
|
||||
res := &Plugin{}
|
||||
res.Plugin = generic.NewPlugin(
|
||||
handler,
|
||||
func(f informers.SharedInformerFactory, client kubernetes.Interface, dynamicClient dynamic.Interface, restMapper meta.RESTMapper) generic.Source[PolicyHook] {
|
||||
return generic.NewPolicySource(
|
||||
f.Admissionregistration().V1alpha1().MutatingAdmissionPolicies().Informer(),
|
||||
f.Admissionregistration().V1alpha1().MutatingAdmissionPolicyBindings().Informer(),
|
||||
NewMutatingAdmissionPolicyAccessor,
|
||||
NewMutatingAdmissionPolicyBindingAccessor,
|
||||
compilePolicy,
|
||||
//!TODO: Create a way to share param informers between
|
||||
// mutating/validating plugins
|
||||
f,
|
||||
dynamicClient,
|
||||
restMapper,
|
||||
)
|
||||
},
|
||||
func(a authorizer.Authorizer, m *matching.Matcher, client kubernetes.Interface) generic.Dispatcher[PolicyHook] {
|
||||
return NewDispatcher(a, m, patch.NewTypeConverterManager(nil, client.Discovery().OpenAPIV3()))
|
||||
},
|
||||
)
|
||||
return res
|
||||
}
|
||||
|
||||
// Admit makes an admission decision based on the request attributes.
|
||||
func (a *Plugin) Admit(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) error {
|
||||
return a.Plugin.Dispatch(ctx, attr, o)
|
||||
}
|
||||
|
||||
func (a *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
|
||||
a.Plugin.SetEnabled(featureGates.Enabled(features.MutatingAdmissionPolicy))
|
||||
}
|
||||
|
||||
// Variable is a named expression for composition.
|
||||
type Variable struct {
|
||||
Name string
|
||||
Expression string
|
||||
}
|
||||
|
||||
func (v *Variable) GetExpression() string {
|
||||
return v.Expression
|
||||
}
|
||||
|
||||
func (v *Variable) ReturnTypes() []*celgo.Type {
|
||||
return []*celgo.Type{celgo.AnyType, celgo.DynType}
|
||||
}
|
||||
|
||||
func (v *Variable) GetName() string {
|
||||
return v.Name
|
||||
}
|
||||
|
||||
func convertv1alpha1Variables(variables []v1alpha1.Variable) []cel.NamedExpressionAccessor {
|
||||
namedExpressions := make([]cel.NamedExpressionAccessor, len(variables))
|
||||
for i, variable := range variables {
|
||||
namedExpressions[i] = &Variable{Name: variable.Name, Expression: variable.Expression}
|
||||
}
|
||||
return namedExpressions
|
||||
}
|
|
@ -0,0 +1,354 @@
|
|||
/*
|
||||
Copyright 2024 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 mutating_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"k8s.io/api/admissionregistration/v1alpha1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/matching"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating/patch"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/openapi/openapitest"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func setupTest(
|
||||
t *testing.T,
|
||||
compiler func(*mutating.Policy) mutating.PolicyEvaluator,
|
||||
) *generic.PolicyTestContext[*mutating.Policy, *mutating.PolicyBinding, mutating.PolicyEvaluator] {
|
||||
|
||||
testContext, testCancel, err := generic.NewPolicyTestContext[*mutating.Policy, *mutating.PolicyBinding, mutating.PolicyEvaluator](
|
||||
mutating.NewMutatingAdmissionPolicyAccessor,
|
||||
mutating.NewMutatingAdmissionPolicyBindingAccessor,
|
||||
compiler,
|
||||
func(a authorizer.Authorizer, m *matching.Matcher, i kubernetes.Interface) generic.Dispatcher[mutating.PolicyHook] {
|
||||
// Use embedded schemas rather than discovery schemas
|
||||
return mutating.NewDispatcher(a, m, patch.NewTypeConverterManager(nil, openapitest.NewEmbeddedFileClient()))
|
||||
},
|
||||
nil,
|
||||
[]meta.RESTMapping{
|
||||
{
|
||||
Resource: schema.GroupVersionResource{
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Resource: "pods",
|
||||
},
|
||||
GroupVersionKind: schema.GroupVersionKind{
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Kind: "Pod",
|
||||
},
|
||||
Scope: meta.RESTScopeNamespace,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(testCancel)
|
||||
require.NoError(t, testContext.Start())
|
||||
return testContext
|
||||
}
|
||||
|
||||
// Show that a compiler that always sets an annotation on the object works
|
||||
func TestBasicPatch(t *testing.T) {
|
||||
expectedAnnotations := map[string]string{"foo": "bar"}
|
||||
|
||||
// Treat all policies as setting foo annotation to bar
|
||||
testContext := setupTest(t, func(p *mutating.Policy) mutating.PolicyEvaluator {
|
||||
return mutating.PolicyEvaluator{Mutators: []patch.Patcher{annotationPatcher{expectedAnnotations}}}
|
||||
})
|
||||
|
||||
// Set up a policy and binding that match, no params
|
||||
require.NoError(t, testContext.UpdateAndWait(
|
||||
&mutating.Policy{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "policy"},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicySpec{
|
||||
MatchConstraints: &v1alpha1.MatchResources{
|
||||
MatchPolicy: ptr.To(v1alpha1.Equivalent),
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
},
|
||||
Mutations: []v1alpha1.Mutation{
|
||||
{
|
||||
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
|
||||
Expression: "ignored, but required",
|
||||
},
|
||||
PatchType: v1alpha1.PatchTypeApplyConfiguration,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&mutating.PolicyBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
|
||||
PolicyName: "policy",
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
// Show that if we run an object through the policy, it gets the annotation
|
||||
testObject := &corev1.ConfigMap{}
|
||||
err := testContext.Dispatch(testObject, nil, admission.Create)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedAnnotations, testObject.Annotations)
|
||||
}
|
||||
|
||||
func TestJSONPatch(t *testing.T) {
|
||||
patchObj := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{
|
||||
"annotations": map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"myfield": "myvalue",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testContext := setupTest(t, func(p *mutating.Policy) mutating.PolicyEvaluator {
|
||||
return mutating.PolicyEvaluator{
|
||||
Mutators: []patch.Patcher{smdPatcher{patch: patchObj}},
|
||||
}
|
||||
})
|
||||
|
||||
// Set up a policy and binding that match, no params
|
||||
require.NoError(t, testContext.UpdateAndWait(
|
||||
&mutating.Policy{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "policy"},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicySpec{
|
||||
MatchConstraints: &v1alpha1.MatchResources{
|
||||
MatchPolicy: ptr.To(v1alpha1.Equivalent),
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
},
|
||||
Mutations: []v1alpha1.Mutation{
|
||||
{
|
||||
JSONPatch: &v1alpha1.JSONPatch{
|
||||
Expression: "ignored, but required",
|
||||
},
|
||||
PatchType: v1alpha1.PatchTypeApplyConfiguration,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&mutating.PolicyBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
|
||||
PolicyName: "policy",
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
// Show that if we run an object through the policy, it gets the annotation
|
||||
testObject := &corev1.ConfigMap{}
|
||||
err := testContext.Dispatch(testObject, nil, admission.Create)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"foo": "bar"},
|
||||
},
|
||||
Data: map[string]string{"myfield": "myvalue"},
|
||||
}, testObject)
|
||||
}
|
||||
|
||||
func TestSSAPatch(t *testing.T) {
|
||||
patchObj := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "ConfigMap",
|
||||
"metadata": map[string]interface{}{
|
||||
"annotations": map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"myfield": "myvalue",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testContext := setupTest(t, func(p *mutating.Policy) mutating.PolicyEvaluator {
|
||||
return mutating.PolicyEvaluator{
|
||||
Mutators: []patch.Patcher{smdPatcher{patch: patchObj}},
|
||||
}
|
||||
})
|
||||
|
||||
// Set up a policy and binding that match, no params
|
||||
require.NoError(t, testContext.UpdateAndWait(
|
||||
&mutating.Policy{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "policy"},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicySpec{
|
||||
MatchConstraints: &v1alpha1.MatchResources{
|
||||
MatchPolicy: ptr.To(v1alpha1.Equivalent),
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
},
|
||||
Mutations: []v1alpha1.Mutation{
|
||||
{
|
||||
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
|
||||
Expression: "ignored, but required",
|
||||
},
|
||||
PatchType: v1alpha1.PatchTypeApplyConfiguration,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&mutating.PolicyBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
|
||||
PolicyName: "policy",
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
// Show that if we run an object through the policy, it gets the annotation
|
||||
testObject := &corev1.ConfigMap{}
|
||||
err := testContext.Dispatch(testObject, nil, admission.Create)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"foo": "bar"},
|
||||
},
|
||||
Data: map[string]string{"myfield": "myvalue"},
|
||||
}, testObject)
|
||||
}
|
||||
|
||||
func TestSSAMapList(t *testing.T) {
|
||||
patchObj := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Pod",
|
||||
"metadata": map[string]interface{}{
|
||||
"annotations": map[string]interface{}{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"initContainers": []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "injected-init-container",
|
||||
"image": "injected-image",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testContext := setupTest(t, func(p *mutating.Policy) mutating.PolicyEvaluator {
|
||||
return mutating.PolicyEvaluator{
|
||||
Mutators: []patch.Patcher{smdPatcher{patch: patchObj}},
|
||||
}
|
||||
})
|
||||
|
||||
// Set up a policy and binding that match, no params
|
||||
require.NoError(t, testContext.UpdateAndWait(
|
||||
&mutating.Policy{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "policy"},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicySpec{
|
||||
MatchConstraints: &v1alpha1.MatchResources{
|
||||
MatchPolicy: ptr.To(v1alpha1.Equivalent),
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
},
|
||||
Mutations: []v1alpha1.Mutation{
|
||||
{
|
||||
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
|
||||
Expression: "ignored, but required",
|
||||
},
|
||||
PatchType: v1alpha1.PatchTypeApplyConfiguration,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&mutating.PolicyBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "binding"},
|
||||
Spec: v1alpha1.MutatingAdmissionPolicyBindingSpec{
|
||||
PolicyName: "policy",
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
// Show that if we run an object through the policy, it gets the annotation
|
||||
testObject := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{},
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "init-container",
|
||||
Image: "image",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
err := testContext.Dispatch(testObject, nil, admission.Create)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{"foo": "bar"},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
InitContainers: []corev1.Container{
|
||||
{
|
||||
Name: "init-container",
|
||||
Image: "image",
|
||||
},
|
||||
{
|
||||
Name: "injected-init-container",
|
||||
Image: "injected-image",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, testObject)
|
||||
}
|
||||
|
||||
type annotationPatcher struct {
|
||||
annotations map[string]string
|
||||
}
|
||||
|
||||
func (ap annotationPatcher) Patch(ctx context.Context, request patch.Request, runtimeCELCostBudget int64) (runtime.Object, error) {
|
||||
obj := request.VersionedAttributes.VersionedObject.DeepCopyObject()
|
||||
accessor, err := meta.Accessor(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accessor.SetAnnotations(ap.annotations)
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
type smdPatcher struct {
|
||||
patch *unstructured.Unstructured
|
||||
}
|
||||
|
||||
func (sp smdPatcher) Patch(ctx context.Context, request patch.Request, runtimeCELCostBudget int64) (runtime.Object, error) {
|
||||
return patch.ApplyStructuredMergeDiff(request.TypeConverter, request.VersionedAttributes.VersionedObject, sp.patch)
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
Copyright 2024 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 mutating
|
||||
|
||||
import (
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
)
|
||||
|
||||
type key struct {
|
||||
PolicyUID types.NamespacedName
|
||||
BindingUID types.NamespacedName
|
||||
ParamUID types.NamespacedName
|
||||
MutationIndex int
|
||||
}
|
||||
|
||||
type policyReinvokeContext struct {
|
||||
// lastPolicyOutput holds the result of the last Policy admission plugin call
|
||||
lastPolicyOutput runtime.Object
|
||||
// previouslyInvokedReinvocablePolicys holds the set of policies that have been invoked and
|
||||
// should be reinvoked if a later mutation occurs
|
||||
previouslyInvokedReinvocablePolicies sets.Set[key]
|
||||
// reinvokePolicies holds the set of Policies that should be reinvoked
|
||||
reinvokePolicies sets.Set[key]
|
||||
}
|
||||
|
||||
func (rc *policyReinvokeContext) ShouldReinvoke(policy key) bool {
|
||||
return rc.reinvokePolicies.Has(policy)
|
||||
}
|
||||
|
||||
func (rc *policyReinvokeContext) IsOutputChangedSinceLastPolicyInvocation(object runtime.Object) bool {
|
||||
return !apiequality.Semantic.DeepEqual(rc.lastPolicyOutput, object)
|
||||
}
|
||||
|
||||
func (rc *policyReinvokeContext) SetLastPolicyInvocationOutput(object runtime.Object) {
|
||||
if object == nil {
|
||||
rc.lastPolicyOutput = nil
|
||||
return
|
||||
}
|
||||
rc.lastPolicyOutput = object.DeepCopyObject()
|
||||
}
|
||||
|
||||
func (rc *policyReinvokeContext) AddReinvocablePolicyToPreviouslyInvoked(policy key) {
|
||||
if rc.previouslyInvokedReinvocablePolicies == nil {
|
||||
rc.previouslyInvokedReinvocablePolicies = sets.New[key]()
|
||||
}
|
||||
rc.previouslyInvokedReinvocablePolicies.Insert(policy)
|
||||
}
|
||||
|
||||
func (rc *policyReinvokeContext) RequireReinvokingPreviouslyInvokedPlugins() {
|
||||
if len(rc.previouslyInvokedReinvocablePolicies) > 0 {
|
||||
if rc.reinvokePolicies == nil {
|
||||
rc.reinvokePolicies = sets.New[key]()
|
||||
}
|
||||
for s := range rc.previouslyInvokedReinvocablePolicies {
|
||||
rc.reinvokePolicies.Insert(s)
|
||||
}
|
||||
rc.previouslyInvokedReinvocablePolicies = sets.New[key]()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
Copyright 2024 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 mutating
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
)
|
||||
|
||||
func TestFullReinvocation(t *testing.T) {
|
||||
key1 := key{PolicyUID: types.NamespacedName{Name: "p1"}, BindingUID: types.NamespacedName{Name: "b1"}}
|
||||
key2 := key{PolicyUID: types.NamespacedName{Name: "p2"}, BindingUID: types.NamespacedName{Name: "b2"}}
|
||||
key3 := key{PolicyUID: types.NamespacedName{Name: "p3"}, BindingUID: types.NamespacedName{Name: "b3"}}
|
||||
|
||||
cm1v1 := &v1.ConfigMap{Data: map[string]string{"v": "1"}}
|
||||
cm1v2 := &v1.ConfigMap{Data: map[string]string{"v": "2"}}
|
||||
|
||||
rc := policyReinvokeContext{}
|
||||
|
||||
// key1 is invoked and it updates the configmap
|
||||
rc.SetLastPolicyInvocationOutput(cm1v1)
|
||||
rc.RequireReinvokingPreviouslyInvokedPlugins()
|
||||
rc.AddReinvocablePolicyToPreviouslyInvoked(key1)
|
||||
|
||||
assert.True(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v2))
|
||||
|
||||
// key2 is invoked and it updates the configmap
|
||||
rc.SetLastPolicyInvocationOutput(cm1v2)
|
||||
rc.RequireReinvokingPreviouslyInvokedPlugins()
|
||||
rc.AddReinvocablePolicyToPreviouslyInvoked(key2)
|
||||
|
||||
assert.True(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v1))
|
||||
|
||||
// key3 is invoked but it does not change anything
|
||||
rc.AddReinvocablePolicyToPreviouslyInvoked(key3)
|
||||
|
||||
assert.False(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v2))
|
||||
|
||||
// key1 is reinvoked
|
||||
assert.True(t, rc.ShouldReinvoke(key1))
|
||||
rc.AddReinvocablePolicyToPreviouslyInvoked(key1)
|
||||
rc.SetLastPolicyInvocationOutput(cm1v1)
|
||||
|
||||
assert.True(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v2))
|
||||
rc.RequireReinvokingPreviouslyInvokedPlugins()
|
||||
|
||||
// key2 is reinvoked
|
||||
assert.True(t, rc.ShouldReinvoke(key2))
|
||||
rc.AddReinvocablePolicyToPreviouslyInvoked(key2)
|
||||
rc.SetLastPolicyInvocationOutput(cm1v2)
|
||||
|
||||
assert.True(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v1))
|
||||
rc.RequireReinvokingPreviouslyInvokedPlugins()
|
||||
|
||||
// key3 is reinvoked, because the reinvocations have changed the resource
|
||||
assert.True(t, rc.ShouldReinvoke(key3))
|
||||
}
|
||||
|
||||
func TestPartialReinvocation(t *testing.T) {
|
||||
key1 := key{PolicyUID: types.NamespacedName{Name: "p1"}, BindingUID: types.NamespacedName{Name: "b1"}}
|
||||
key2 := key{PolicyUID: types.NamespacedName{Name: "p2"}, BindingUID: types.NamespacedName{Name: "b2"}}
|
||||
key3 := key{PolicyUID: types.NamespacedName{Name: "p3"}, BindingUID: types.NamespacedName{Name: "b3"}}
|
||||
|
||||
cm1v1 := &v1.ConfigMap{Data: map[string]string{"v": "1"}}
|
||||
cm1v2 := &v1.ConfigMap{Data: map[string]string{"v": "2"}}
|
||||
|
||||
rc := policyReinvokeContext{}
|
||||
|
||||
// key1 is invoked and it updates the configmap
|
||||
rc.SetLastPolicyInvocationOutput(cm1v1)
|
||||
rc.RequireReinvokingPreviouslyInvokedPlugins()
|
||||
rc.AddReinvocablePolicyToPreviouslyInvoked(key1)
|
||||
|
||||
assert.True(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v2))
|
||||
|
||||
// key2 is invoked and it updates the configmap
|
||||
rc.SetLastPolicyInvocationOutput(cm1v2)
|
||||
rc.RequireReinvokingPreviouslyInvokedPlugins()
|
||||
rc.AddReinvocablePolicyToPreviouslyInvoked(key2)
|
||||
|
||||
assert.True(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v1))
|
||||
|
||||
// key3 is invoked but it does not change anything
|
||||
rc.AddReinvocablePolicyToPreviouslyInvoked(key3)
|
||||
|
||||
assert.False(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v2))
|
||||
|
||||
// key1 is reinvoked but does not change anything
|
||||
assert.True(t, rc.ShouldReinvoke(key1))
|
||||
|
||||
// key2 is not reinvoked because nothing changed since last invocation
|
||||
assert.False(t, rc.ShouldReinvoke(key2))
|
||||
|
||||
// key3 is not reinvoked because nothing changed since last invocation
|
||||
assert.False(t, rc.ShouldReinvoke(key3))
|
||||
}
|
||||
|
||||
func TestNoReinvocation(t *testing.T) {
|
||||
key1 := key{PolicyUID: types.NamespacedName{Name: "p1"}, BindingUID: types.NamespacedName{Name: "b1"}}
|
||||
key2 := key{PolicyUID: types.NamespacedName{Name: "p2"}, BindingUID: types.NamespacedName{Name: "b2"}}
|
||||
key3 := key{PolicyUID: types.NamespacedName{Name: "p3"}, BindingUID: types.NamespacedName{Name: "b3"}}
|
||||
|
||||
cm1v1 := &v1.ConfigMap{Data: map[string]string{"v": "1"}}
|
||||
|
||||
rc := policyReinvokeContext{}
|
||||
|
||||
// key1 is invoked and it updates the configmap
|
||||
rc.AddReinvocablePolicyToPreviouslyInvoked(key1)
|
||||
rc.SetLastPolicyInvocationOutput(cm1v1)
|
||||
|
||||
assert.False(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v1))
|
||||
|
||||
// key2 is invoked but does not change anything
|
||||
rc.AddReinvocablePolicyToPreviouslyInvoked(key2)
|
||||
rc.SetLastPolicyInvocationOutput(cm1v1)
|
||||
|
||||
assert.False(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v1))
|
||||
|
||||
// key3 is invoked but it does not change anything
|
||||
rc.AddReinvocablePolicyToPreviouslyInvoked(key3)
|
||||
rc.SetLastPolicyInvocationOutput(cm1v1)
|
||||
|
||||
assert.False(t, rc.IsOutputChangedSinceLastPolicyInvocation(cm1v1))
|
||||
|
||||
// no keys are reinvoked
|
||||
assert.False(t, rc.ShouldReinvoke(key1))
|
||||
assert.False(t, rc.ShouldReinvoke(key2))
|
||||
assert.False(t, rc.ShouldReinvoke(key3))
|
||||
|
||||
}
|
|
@ -54,6 +54,10 @@ func (v *validatingAdmissionPolicyAccessor) GetMatchConstraints() *v1.MatchResou
|
|||
return v.Spec.MatchConstraints
|
||||
}
|
||||
|
||||
func (v *validatingAdmissionPolicyAccessor) GetFailurePolicy() *v1.FailurePolicyType {
|
||||
return v.Spec.FailurePolicy
|
||||
}
|
||||
|
||||
type validatingAdmissionPolicyBindingAccessor struct {
|
||||
*v1.ValidatingAdmissionPolicyBinding
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ import (
|
|||
auditinternal "k8s.io/apiserver/pkg/apis/audit"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/warning"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -364,7 +365,7 @@ func setupTestCommon(
|
|||
func(p *validating.Policy) validating.Validator {
|
||||
return compiler.CompilePolicy(p)
|
||||
},
|
||||
func(a authorizer.Authorizer, m *matching.Matcher) generic.Dispatcher[validating.PolicyHook] {
|
||||
func(a authorizer.Authorizer, m *matching.Matcher, client kubernetes.Interface) generic.Dispatcher[validating.PolicyHook] {
|
||||
coolMatcher := matcher
|
||||
if coolMatcher == nil {
|
||||
coolMatcher = generic.NewPolicyMatcher(m)
|
||||
|
@ -602,7 +603,7 @@ func TestDefinitionDoesntMatch(t *testing.T) {
|
|||
nil, matchingParams,
|
||||
admission.Create), &admission.RuntimeObjectInterfaces{}),
|
||||
`Denied`)
|
||||
require.Equal(t, numCompiles, 1)
|
||||
require.Equal(t, 1, numCompiles)
|
||||
}
|
||||
|
||||
func TestReconfigureBinding(t *testing.T) {
|
||||
|
|
|
@ -30,6 +30,7 @@ import (
|
|||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utiljson "k8s.io/apimachinery/pkg/util/json"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
admissionauthorizer "k8s.io/apiserver/pkg/admission/plugin/authorizer"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/policy/generic"
|
||||
celmetrics "k8s.io/apiserver/pkg/admission/plugin/policy/validating/metrics"
|
||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||
|
@ -63,6 +64,10 @@ type policyDecisionWithMetadata struct {
|
|||
Binding *admissionregistrationv1.ValidatingAdmissionPolicyBinding
|
||||
}
|
||||
|
||||
func (c *dispatcher) Start(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Dispatch implements generic.Dispatcher.
|
||||
func (c *dispatcher) Dispatch(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces, hooks []PolicyHook) error {
|
||||
|
||||
|
@ -109,7 +114,7 @@ func (c *dispatcher) Dispatch(ctx context.Context, a admission.Attributes, o adm
|
|||
}
|
||||
}
|
||||
|
||||
authz := newCachingAuthorizer(c.authz)
|
||||
authz := admissionauthorizer.NewCachingAuthorizer(c.authz)
|
||||
|
||||
for _, hook := range hooks {
|
||||
// versionedAttributes will be set to non-nil inside of the loop, but
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cel
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cel
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package cel
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
|
@ -36,7 +36,6 @@ import (
|
|||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/component-base/featuregate"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -93,13 +92,12 @@ type Plugin struct {
|
|||
|
||||
var _ admission.Interface = &Plugin{}
|
||||
var _ admission.ValidationInterface = &Plugin{}
|
||||
var _ initializer.WantsFeatures = &Plugin{}
|
||||
var _ initializer.WantsExcludedAdmissionResources = &Plugin{}
|
||||
|
||||
func NewPlugin(_ io.Reader) *Plugin {
|
||||
handler := admission.NewHandler(admission.Connect, admission.Create, admission.Delete, admission.Update)
|
||||
|
||||
return &Plugin{
|
||||
p := &Plugin{
|
||||
Plugin: generic.NewPlugin(
|
||||
handler,
|
||||
func(f informers.SharedInformerFactory, client kubernetes.Interface, dynamicClient dynamic.Interface, restMapper meta.RESTMapper) generic.Source[PolicyHook] {
|
||||
|
@ -114,11 +112,13 @@ func NewPlugin(_ io.Reader) *Plugin {
|
|||
restMapper,
|
||||
)
|
||||
},
|
||||
func(a authorizer.Authorizer, m *matching.Matcher) generic.Dispatcher[PolicyHook] {
|
||||
func(a authorizer.Authorizer, m *matching.Matcher, client kubernetes.Interface) generic.Dispatcher[PolicyHook] {
|
||||
return NewDispatcher(a, generic.NewPolicyMatcher(m))
|
||||
},
|
||||
),
|
||||
}
|
||||
p.SetEnabled(true)
|
||||
return p
|
||||
}
|
||||
|
||||
// Validate makes an admission decision based on the request attributes.
|
||||
|
@ -126,10 +126,6 @@ func (a *Plugin) Validate(ctx context.Context, attr admission.Attributes, o admi
|
|||
return a.Plugin.Dispatch(ctx, attr, o)
|
||||
}
|
||||
|
||||
func (a *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
|
||||
a.Plugin.SetEnabled(featureGates.Enabled(features.ValidatingAdmissionPolicy))
|
||||
}
|
||||
|
||||
func compilePolicy(policy *Policy) Validator {
|
||||
hasParam := false
|
||||
if policy.Spec.ParamKind != nil {
|
||||
|
@ -155,13 +151,13 @@ func compilePolicy(policy *Policy) Validator {
|
|||
for i := range matchConditions {
|
||||
matchExpressionAccessors[i] = (*matchconditions.MatchCondition)(&matchConditions[i])
|
||||
}
|
||||
matcher = matchconditions.NewMatcher(filterCompiler.Compile(matchExpressionAccessors, optionalVars, environment.StoredExpressions), failurePolicy, "policy", "validate", policy.Name)
|
||||
matcher = matchconditions.NewMatcher(filterCompiler.CompileCondition(matchExpressionAccessors, optionalVars, environment.StoredExpressions), failurePolicy, "policy", "validate", policy.Name)
|
||||
}
|
||||
res := NewValidator(
|
||||
filterCompiler.Compile(convertv1Validations(policy.Spec.Validations), optionalVars, environment.StoredExpressions),
|
||||
filterCompiler.CompileCondition(convertv1Validations(policy.Spec.Validations), optionalVars, environment.StoredExpressions),
|
||||
matcher,
|
||||
filterCompiler.Compile(convertv1AuditAnnotations(policy.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions),
|
||||
filterCompiler.Compile(convertv1MessageExpressions(policy.Spec.Validations), expressionOptionalVars, environment.StoredExpressions),
|
||||
filterCompiler.CompileCondition(convertv1AuditAnnotations(policy.Spec.AuditAnnotations), optionalVars, environment.StoredExpressions),
|
||||
filterCompiler.CompileCondition(convertv1MessageExpressions(policy.Spec.Validations), expressionOptionalVars, environment.StoredExpressions),
|
||||
failurePolicy,
|
||||
)
|
||||
|
||||
|
|
|
@ -41,13 +41,13 @@ import (
|
|||
// validator implements the Validator interface
|
||||
type validator struct {
|
||||
celMatcher matchconditions.Matcher
|
||||
validationFilter cel.Filter
|
||||
auditAnnotationFilter cel.Filter
|
||||
messageFilter cel.Filter
|
||||
validationFilter cel.ConditionEvaluator
|
||||
auditAnnotationFilter cel.ConditionEvaluator
|
||||
messageFilter cel.ConditionEvaluator
|
||||
failPolicy *v1.FailurePolicyType
|
||||
}
|
||||
|
||||
func NewValidator(validationFilter cel.Filter, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.Filter, failPolicy *v1.FailurePolicyType) Validator {
|
||||
func NewValidator(validationFilter cel.ConditionEvaluator, celMatcher matchconditions.Matcher, auditAnnotationFilter, messageFilter cel.ConditionEvaluator, failPolicy *v1.FailurePolicyType) Validator {
|
||||
return &validator{
|
||||
celMatcher: celMatcher,
|
||||
validationFilter: validationFilter,
|
||||
|
@ -122,6 +122,7 @@ func (v *validator) Validate(ctx context.Context, matchedResource schema.GroupVe
|
|||
messageResults, _, err := v.messageFilter.ForInput(ctx, versionedAttr, admissionRequest, expressionOptionalVars, ns, remainingBudget)
|
||||
for i, evalResult := range evalResults {
|
||||
var decision = &decisions[i]
|
||||
decision.Elapsed = evalResult.Elapsed
|
||||
// TODO: move this to generics
|
||||
validation, ok := evalResult.ExpressionAccessor.(*ValidationCondition)
|
||||
if !ok {
|
||||
|
@ -146,6 +147,7 @@ func (v *validator) Validate(ctx context.Context, matchedResource schema.GroupVe
|
|||
decision.Message = fmt.Sprintf("failed messageExpression: %s", err)
|
||||
} else if evalResult.EvalResult != celtypes.True {
|
||||
decision.Action = ActionDeny
|
||||
decision.Evaluation = EvalDeny
|
||||
if validation.Reason == nil {
|
||||
decision.Reason = metav1.StatusReasonInvalid
|
||||
} else {
|
||||
|
@ -210,6 +212,7 @@ func (v *validator) Validate(ctx context.Context, matchedResource schema.GroupVe
|
|||
continue
|
||||
}
|
||||
var auditAnnotationResult = &auditAnnotationResults[i]
|
||||
auditAnnotationResult.Elapsed = evalResult.Elapsed
|
||||
// TODO: move this to generics
|
||||
validation, ok := evalResult.ExpressionAccessor.(*AuditAnnotationCondition)
|
||||
if !ok {
|
||||
|
|
|
@ -22,6 +22,7 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
celtypes "github.com/google/cel-go/common/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -41,7 +42,7 @@ import (
|
|||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
)
|
||||
|
||||
var _ cel.Filter = &fakeCelFilter{}
|
||||
var _ cel.ConditionEvaluator = &fakeCelFilter{}
|
||||
|
||||
type fakeCelFilter struct {
|
||||
evaluations []cel.EvaluationResult
|
||||
|
@ -105,11 +106,13 @@ func TestValidate(t *testing.T) {
|
|||
{
|
||||
EvalResult: celtypes.True,
|
||||
ExpressionAccessor: &ValidationCondition{},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
{
|
||||
Action: ActionAdmit,
|
||||
Action: ActionAdmit,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -119,18 +122,22 @@ func TestValidate(t *testing.T) {
|
|||
{
|
||||
EvalResult: celtypes.True,
|
||||
ExpressionAccessor: &ValidationCondition{},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
{
|
||||
EvalResult: celtypes.True,
|
||||
ExpressionAccessor: &ValidationCondition{},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
{
|
||||
Action: ActionAdmit,
|
||||
Action: ActionAdmit,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
{
|
||||
Action: ActionAdmit,
|
||||
Action: ActionAdmit,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -140,11 +147,13 @@ func TestValidate(t *testing.T) {
|
|||
{
|
||||
Error: errors.New(""),
|
||||
ExpressionAccessor: &ValidationCondition{},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
{
|
||||
Action: ActionAdmit,
|
||||
Action: ActionAdmit,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
failPolicy: &ignore,
|
||||
|
@ -155,11 +164,13 @@ func TestValidate(t *testing.T) {
|
|||
{
|
||||
Error: errors.New(""),
|
||||
ExpressionAccessor: &ValidationCondition{},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
{
|
||||
Action: ActionDeny,
|
||||
Action: ActionDeny,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -169,11 +180,13 @@ func TestValidate(t *testing.T) {
|
|||
{
|
||||
Error: errors.New(""),
|
||||
ExpressionAccessor: &ValidationCondition{},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
{
|
||||
Action: ActionDeny,
|
||||
Action: ActionDeny,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
failPolicy: &fail,
|
||||
|
@ -184,18 +197,22 @@ func TestValidate(t *testing.T) {
|
|||
{
|
||||
EvalResult: celtypes.True,
|
||||
ExpressionAccessor: &ValidationCondition{},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
{
|
||||
Error: errors.New(""),
|
||||
ExpressionAccessor: &ValidationCondition{},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
{
|
||||
Action: ActionAdmit,
|
||||
Action: ActionAdmit,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
{
|
||||
Action: ActionAdmit,
|
||||
Action: ActionAdmit,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
failPolicy: &ignore,
|
||||
|
@ -206,18 +223,22 @@ func TestValidate(t *testing.T) {
|
|||
{
|
||||
EvalResult: celtypes.True,
|
||||
ExpressionAccessor: &ValidationCondition{},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
{
|
||||
Error: errors.New(""),
|
||||
ExpressionAccessor: &ValidationCondition{},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
{
|
||||
Action: ActionAdmit,
|
||||
Action: ActionAdmit,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
{
|
||||
Action: ActionDeny,
|
||||
Action: ActionDeny,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -227,18 +248,22 @@ func TestValidate(t *testing.T) {
|
|||
{
|
||||
EvalResult: celtypes.True,
|
||||
ExpressionAccessor: &ValidationCondition{},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
{
|
||||
Error: errors.New(""),
|
||||
ExpressionAccessor: &ValidationCondition{},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
{
|
||||
Action: ActionAdmit,
|
||||
Action: ActionAdmit,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
{
|
||||
Action: ActionDeny,
|
||||
Action: ActionDeny,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
failPolicy: &fail,
|
||||
|
@ -249,18 +274,22 @@ func TestValidate(t *testing.T) {
|
|||
{
|
||||
Error: errors.New(""),
|
||||
ExpressionAccessor: &ValidationCondition{},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
{
|
||||
Error: errors.New(""),
|
||||
ExpressionAccessor: &ValidationCondition{},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
{
|
||||
Action: ActionAdmit,
|
||||
Action: ActionAdmit,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
{
|
||||
Action: ActionAdmit,
|
||||
Action: ActionAdmit,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
failPolicy: &ignore,
|
||||
|
@ -271,18 +300,22 @@ func TestValidate(t *testing.T) {
|
|||
{
|
||||
Error: errors.New(""),
|
||||
ExpressionAccessor: &ValidationCondition{},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
{
|
||||
Error: errors.New(""),
|
||||
ExpressionAccessor: &ValidationCondition{},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
{
|
||||
Action: ActionDeny,
|
||||
Action: ActionDeny,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
{
|
||||
Action: ActionDeny,
|
||||
Action: ActionDeny,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -292,18 +325,22 @@ func TestValidate(t *testing.T) {
|
|||
{
|
||||
Error: errors.New(""),
|
||||
ExpressionAccessor: &ValidationCondition{},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
{
|
||||
Error: errors.New(""),
|
||||
ExpressionAccessor: &ValidationCondition{},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
{
|
||||
Action: ActionDeny,
|
||||
Action: ActionDeny,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
{
|
||||
Action: ActionDeny,
|
||||
Action: ActionDeny,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
failPolicy: &fail,
|
||||
|
@ -316,6 +353,7 @@ func TestValidate(t *testing.T) {
|
|||
ExpressionAccessor: &ValidationCondition{
|
||||
Expression: "this.expression == unit.test",
|
||||
},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
|
@ -323,6 +361,7 @@ func TestValidate(t *testing.T) {
|
|||
Action: ActionDeny,
|
||||
Reason: metav1.StatusReasonInvalid,
|
||||
Message: "failed expression: this.expression == unit.test",
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
failPolicy: &fail,
|
||||
|
@ -336,6 +375,7 @@ func TestValidate(t *testing.T) {
|
|||
Reason: &forbiddenReason,
|
||||
Expression: "this.expression == unit.test",
|
||||
},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
|
@ -343,6 +383,7 @@ func TestValidate(t *testing.T) {
|
|||
Action: ActionDeny,
|
||||
Reason: metav1.StatusReasonForbidden,
|
||||
Message: "failed expression: this.expression == unit.test",
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
failPolicy: &fail,
|
||||
|
@ -356,6 +397,7 @@ func TestValidate(t *testing.T) {
|
|||
Reason: &forbiddenReason,
|
||||
Expression: "this.expression == unit.test",
|
||||
},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
{
|
||||
EvalResult: celtypes.False,
|
||||
|
@ -363,6 +405,7 @@ func TestValidate(t *testing.T) {
|
|||
Reason: &unauthorizedReason,
|
||||
Expression: "this.expression.2 == unit.test.2",
|
||||
},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
|
@ -370,11 +413,13 @@ func TestValidate(t *testing.T) {
|
|||
Action: ActionDeny,
|
||||
Reason: metav1.StatusReasonForbidden,
|
||||
Message: "failed expression: this.expression == unit.test",
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
{
|
||||
Action: ActionDeny,
|
||||
Reason: metav1.StatusReasonUnauthorized,
|
||||
Message: "failed expression: this.expression.2 == unit.test.2",
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
failPolicy: &fail,
|
||||
|
@ -389,6 +434,7 @@ func TestValidate(t *testing.T) {
|
|||
Expression: "this.expression == unit.test",
|
||||
Message: "test",
|
||||
},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
|
@ -396,6 +442,7 @@ func TestValidate(t *testing.T) {
|
|||
Action: ActionDeny,
|
||||
Reason: metav1.StatusReasonForbidden,
|
||||
Message: "test",
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
failPolicy: &fail,
|
||||
|
@ -410,6 +457,7 @@ func TestValidate(t *testing.T) {
|
|||
Expression: "this.expression == unit.test",
|
||||
Message: "test1",
|
||||
},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
{
|
||||
EvalResult: celtypes.False,
|
||||
|
@ -418,6 +466,7 @@ func TestValidate(t *testing.T) {
|
|||
Expression: "this.expression == unit.test",
|
||||
Message: "test2",
|
||||
},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
|
@ -425,11 +474,13 @@ func TestValidate(t *testing.T) {
|
|||
Action: ActionDeny,
|
||||
Reason: metav1.StatusReasonForbidden,
|
||||
Message: "test1",
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
{
|
||||
Action: ActionDeny,
|
||||
Reason: metav1.StatusReasonForbidden,
|
||||
Message: "test2",
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
failPolicy: &fail,
|
||||
|
@ -444,6 +495,7 @@ func TestValidate(t *testing.T) {
|
|||
Expression: "this.expression == unit.test",
|
||||
Message: "test1",
|
||||
},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
|
@ -465,6 +517,7 @@ func TestValidate(t *testing.T) {
|
|||
Expression: "this.expression == unit.test",
|
||||
Message: "test1",
|
||||
},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
{
|
||||
EvalResult: celtypes.False,
|
||||
|
@ -473,6 +526,7 @@ func TestValidate(t *testing.T) {
|
|||
Expression: "this.expression == unit.test",
|
||||
Message: "test2",
|
||||
},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
|
@ -492,13 +546,15 @@ func TestValidate(t *testing.T) {
|
|||
ExpressionAccessor: &AuditAnnotationCondition{
|
||||
ValueExpression: "'string value'",
|
||||
},
|
||||
Elapsed: 2 * time.Millisecond,
|
||||
},
|
||||
},
|
||||
failPolicy: &fail,
|
||||
auditAnnotations: []PolicyAuditAnnotation{
|
||||
{
|
||||
Action: AuditAnnotationActionPublish,
|
||||
Value: "string value",
|
||||
Action: AuditAnnotationActionPublish,
|
||||
Value: "string value",
|
||||
Elapsed: 2 * time.Millisecond,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -512,6 +568,7 @@ func TestValidate(t *testing.T) {
|
|||
Expression: "this.expression == unit.test",
|
||||
Message: "test1",
|
||||
},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
auditEvaluations: []cel.EvaluationResult{
|
||||
|
@ -520,17 +577,20 @@ func TestValidate(t *testing.T) {
|
|||
ExpressionAccessor: &AuditAnnotationCondition{
|
||||
ValueExpression: "'string value'",
|
||||
},
|
||||
Elapsed: 2 * time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
{
|
||||
Action: ActionAdmit,
|
||||
Action: ActionAdmit,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
auditAnnotations: []PolicyAuditAnnotation{
|
||||
{
|
||||
Action: AuditAnnotationActionPublish,
|
||||
Value: "string value",
|
||||
Action: AuditAnnotationActionPublish,
|
||||
Value: "string value",
|
||||
Elapsed: 2 * time.Millisecond,
|
||||
},
|
||||
},
|
||||
failPolicy: &fail,
|
||||
|
@ -543,21 +603,25 @@ func TestValidate(t *testing.T) {
|
|||
ExpressionAccessor: &AuditAnnotationCondition{
|
||||
ValueExpression: "null",
|
||||
},
|
||||
Elapsed: 2 * time.Millisecond,
|
||||
},
|
||||
{
|
||||
EvalResult: celtypes.String("string value"),
|
||||
ExpressionAccessor: &AuditAnnotationCondition{
|
||||
ValueExpression: "'string value'",
|
||||
},
|
||||
Elapsed: 2 * time.Millisecond,
|
||||
},
|
||||
},
|
||||
auditAnnotations: []PolicyAuditAnnotation{
|
||||
{
|
||||
Action: AuditAnnotationActionExclude,
|
||||
Action: AuditAnnotationActionExclude,
|
||||
Elapsed: 2 * time.Millisecond,
|
||||
},
|
||||
{
|
||||
Action: AuditAnnotationActionPublish,
|
||||
Value: "string value",
|
||||
Action: AuditAnnotationActionPublish,
|
||||
Value: "string value",
|
||||
Elapsed: 2 * time.Millisecond,
|
||||
},
|
||||
},
|
||||
failPolicy: &fail,
|
||||
|
@ -570,12 +634,14 @@ func TestValidate(t *testing.T) {
|
|||
ExpressionAccessor: &AuditAnnotationCondition{
|
||||
ValueExpression: "'this is not valid CEL",
|
||||
},
|
||||
Elapsed: 2 * time.Millisecond,
|
||||
},
|
||||
},
|
||||
auditAnnotations: []PolicyAuditAnnotation{
|
||||
{
|
||||
Action: AuditAnnotationActionError,
|
||||
Error: "valueExpression ''this is not valid CEL' resulted in error: <nil>",
|
||||
Action: AuditAnnotationActionError,
|
||||
Error: "valueExpression ''this is not valid CEL' resulted in error: <nil>",
|
||||
Elapsed: 2 * time.Millisecond,
|
||||
},
|
||||
},
|
||||
failPolicy: &fail,
|
||||
|
@ -588,12 +654,14 @@ func TestValidate(t *testing.T) {
|
|||
ExpressionAccessor: &AuditAnnotationCondition{
|
||||
ValueExpression: "'this is not valid CEL",
|
||||
},
|
||||
Elapsed: 2 * time.Millisecond,
|
||||
},
|
||||
},
|
||||
auditAnnotations: []PolicyAuditAnnotation{
|
||||
{
|
||||
Action: AuditAnnotationActionExclude, // TODO: is this right?
|
||||
Error: "valueExpression ''this is not valid CEL' resulted in error: <nil>",
|
||||
Action: AuditAnnotationActionExclude, // TODO: is this right?
|
||||
Error: "valueExpression ''this is not valid CEL' resulted in error: <nil>",
|
||||
Elapsed: 2 * time.Millisecond,
|
||||
},
|
||||
},
|
||||
failPolicy: &ignore,
|
||||
|
@ -607,11 +675,13 @@ func TestValidate(t *testing.T) {
|
|||
Reason: &forbiddenReason,
|
||||
Expression: "this.expression == unit.test",
|
||||
},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
messageEvaluations: []cel.EvaluationResult{
|
||||
{
|
||||
EvalResult: celtypes.String("evaluated message"),
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
|
@ -619,6 +689,7 @@ func TestValidate(t *testing.T) {
|
|||
Action: ActionDeny,
|
||||
Message: "evaluated message",
|
||||
Reason: forbiddenReason,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -632,6 +703,7 @@ func TestValidate(t *testing.T) {
|
|||
Expression: "this.expression == unit.test",
|
||||
Message: "I am not overwritten",
|
||||
},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
{
|
||||
EvalResult: celtypes.False,
|
||||
|
@ -640,12 +712,14 @@ func TestValidate(t *testing.T) {
|
|||
Expression: "this.expression == unit.test",
|
||||
Message: "I am overwritten",
|
||||
},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
messageEvaluations: []cel.EvaluationResult{
|
||||
{},
|
||||
{
|
||||
EvalResult: celtypes.String("evaluated message"),
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
|
@ -653,11 +727,13 @@ func TestValidate(t *testing.T) {
|
|||
Action: ActionDeny,
|
||||
Message: "I am not overwritten",
|
||||
Reason: forbiddenReason,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
{
|
||||
Action: ActionDeny,
|
||||
Message: "evaluated message",
|
||||
Reason: forbiddenReason,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -671,11 +747,13 @@ func TestValidate(t *testing.T) {
|
|||
Expression: "this.expression == unit.test",
|
||||
Message: "I am overwritten",
|
||||
},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
messageEvaluations: []cel.EvaluationResult{
|
||||
{
|
||||
EvalResult: celtypes.String("evaluated message"),
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
|
@ -683,6 +761,7 @@ func TestValidate(t *testing.T) {
|
|||
Action: ActionDeny,
|
||||
Message: "evaluated message",
|
||||
Reason: forbiddenReason,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -696,11 +775,13 @@ func TestValidate(t *testing.T) {
|
|||
Expression: "this.expression == unit.test",
|
||||
Message: "test1",
|
||||
},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
messageEvaluations: []cel.EvaluationResult{
|
||||
{
|
||||
Error: &apiservercel.Error{Type: apiservercel.ErrorTypeInvalid},
|
||||
Error: &apiservercel.Error{Type: apiservercel.ErrorTypeInvalid},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
|
@ -708,6 +789,7 @@ func TestValidate(t *testing.T) {
|
|||
Action: ActionDeny,
|
||||
Message: "test1", // original message used
|
||||
Reason: forbiddenReason,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -721,11 +803,13 @@ func TestValidate(t *testing.T) {
|
|||
Expression: "this.expression == unit.test",
|
||||
Message: "test1",
|
||||
},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
messageEvaluations: []cel.EvaluationResult{
|
||||
{
|
||||
EvalResult: celtypes.String(" "),
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
|
@ -733,6 +817,7 @@ func TestValidate(t *testing.T) {
|
|||
Action: ActionDeny,
|
||||
Message: "test1",
|
||||
Reason: forbiddenReason,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -746,11 +831,13 @@ func TestValidate(t *testing.T) {
|
|||
Expression: "this.expression == unit.test",
|
||||
Message: "test1",
|
||||
},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
messageEvaluations: []cel.EvaluationResult{
|
||||
{
|
||||
EvalResult: celtypes.String("hello\nthere"),
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
|
@ -758,6 +845,7 @@ func TestValidate(t *testing.T) {
|
|||
Action: ActionDeny,
|
||||
Message: "test1",
|
||||
Reason: forbiddenReason,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -771,6 +859,7 @@ func TestValidate(t *testing.T) {
|
|||
Expression: "this.expression == unit.test",
|
||||
Message: "test1",
|
||||
},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
messageEvaluations: []cel.EvaluationResult{
|
||||
|
@ -783,6 +872,7 @@ func TestValidate(t *testing.T) {
|
|||
Action: ActionDeny,
|
||||
Message: "test1",
|
||||
Reason: forbiddenReason,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -796,11 +886,13 @@ func TestValidate(t *testing.T) {
|
|||
Expression: "this.expression == unit.test",
|
||||
Message: "test1",
|
||||
},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
messageEvaluations: []cel.EvaluationResult{
|
||||
{
|
||||
EvalResult: celtypes.NullValue,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
|
@ -808,6 +900,7 @@ func TestValidate(t *testing.T) {
|
|||
Action: ActionDeny,
|
||||
Message: "test1",
|
||||
Reason: forbiddenReason,
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -821,6 +914,7 @@ func TestValidate(t *testing.T) {
|
|||
Expression: "this.expression == unit.test",
|
||||
Message: "test1",
|
||||
},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
messageEvaluations: []cel.EvaluationResult{
|
||||
|
@ -832,6 +926,7 @@ func TestValidate(t *testing.T) {
|
|||
{
|
||||
Action: ActionDeny,
|
||||
Message: "running out of cost budget",
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
costBudget: 1, // shared between expression and messageExpression, needs 1 + 1 = 2 in total
|
||||
|
@ -843,6 +938,7 @@ func TestValidate(t *testing.T) {
|
|||
{
|
||||
Error: errors.New("expected"),
|
||||
ExpressionAccessor: &ValidationCondition{},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{},
|
||||
|
@ -855,6 +951,7 @@ func TestValidate(t *testing.T) {
|
|||
{
|
||||
Error: errors.New("expected"),
|
||||
ExpressionAccessor: &ValidationCondition{},
|
||||
Elapsed: time.Millisecond,
|
||||
},
|
||||
},
|
||||
policyDecision: []PolicyDecision{
|
||||
|
@ -908,6 +1005,9 @@ func TestValidate(t *testing.T) {
|
|||
if policyDecision.Reason != validateResult.Decisions[i].Reason {
|
||||
t.Errorf("Expected policy decision reason '%v' but got '%v'", policyDecision.Reason, validateResult.Decisions[i].Reason)
|
||||
}
|
||||
if policyDecision.Elapsed != validateResult.Decisions[i].Elapsed {
|
||||
t.Errorf("Expected policy decision elapsed time '%v', but got '%v'", policyDecision.Elapsed, validateResult.Decisions[i].Elapsed)
|
||||
}
|
||||
}
|
||||
require.Equal(t, len(tc.auditEvaluations), len(validateResult.AuditAnnotations))
|
||||
|
||||
|
@ -922,6 +1022,9 @@ func TestValidate(t *testing.T) {
|
|||
if auditAnnotation.Value != actual.Value {
|
||||
t.Errorf("Expected policy audit annotation value '%v' but got '%v'", auditAnnotation.Value, actual.Value)
|
||||
}
|
||||
if auditAnnotation.Elapsed != actual.Elapsed {
|
||||
t.Errorf("Expected policy audit annotation elapsed time '%v', but got '%v'", auditAnnotation.Elapsed, actual.Elapsed)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -932,8 +1035,8 @@ func TestContextCanceled(t *testing.T) {
|
|||
|
||||
fakeAttr := admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "default", "foo", schema.GroupVersionResource{}, "", admission.Create, nil, false, nil)
|
||||
fakeVersionedAttr, _ := admission.NewVersionedAttributes(fakeAttr, schema.GroupVersionKind{}, nil)
|
||||
fc := cel.NewFilterCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
|
||||
f := fc.Compile([]cel.ExpressionAccessor{&ValidationCondition{Expression: "[1,2,3,4,5,6,7,8,9,10].map(x, [1,2,3,4,5,6,7,8,9,10].map(y, x*y)) == []"}}, cel.OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, environment.StoredExpressions)
|
||||
fc := cel.NewConditionCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), true))
|
||||
f := fc.CompileCondition([]cel.ExpressionAccessor{&ValidationCondition{Expression: "[1,2,3,4,5,6,7,8,9,10].map(x, [1,2,3,4,5,6,7,8,9,10].map(y, x*y)) == []"}}, cel.OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false}, environment.StoredExpressions)
|
||||
v := validator{
|
||||
failPolicy: &fail,
|
||||
celMatcher: &fakeCELMatcher{matches: true},
|
||||
|
|
|
@ -114,7 +114,9 @@ func (a *QuotaAdmission) SetExternalKubeClientSet(client kubernetes.Interface) {
|
|||
|
||||
// SetExternalKubeInformerFactory registers an informer factory into QuotaAdmission
|
||||
func (a *QuotaAdmission) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
|
||||
a.quotaAccessor.lister = f.Core().V1().ResourceQuotas().Lister()
|
||||
quotas := f.Core().V1().ResourceQuotas()
|
||||
a.quotaAccessor.lister = quotas.Lister()
|
||||
a.quotaAccessor.hasSynced = quotas.Informer().HasSynced
|
||||
}
|
||||
|
||||
// SetQuotaConfiguration assigns and initializes configuration and evaluator for QuotaAdmission
|
||||
|
@ -144,6 +146,9 @@ func (a *QuotaAdmission) ValidateInitialization() error {
|
|||
if a.quotaAccessor.lister == nil {
|
||||
return fmt.Errorf("missing quotaAccessor.lister")
|
||||
}
|
||||
if a.quotaAccessor.hasSynced == nil {
|
||||
return fmt.Errorf("missing quotaAccessor.hasSynced")
|
||||
}
|
||||
if a.quotaConfiguration == nil {
|
||||
return fmt.Errorf("missing quotaConfiguration")
|
||||
}
|
||||
|
@ -155,10 +160,6 @@ func (a *QuotaAdmission) ValidateInitialization() error {
|
|||
|
||||
// Validate makes admission decisions while enforcing quota
|
||||
func (a *QuotaAdmission) Validate(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces) (err error) {
|
||||
// ignore all operations that correspond to sub-resource actions
|
||||
if attr.GetSubresource() != "" {
|
||||
return nil
|
||||
}
|
||||
// ignore all operations that are not namespaced or creation of namespaces
|
||||
if attr.GetNamespace() == "" || isNamespaceCreation(attr) {
|
||||
return nil
|
||||
|
|
|
@ -143,10 +143,6 @@ func TestExcludedOperations(t *testing.T) {
|
|||
desc string
|
||||
attr admission.Attributes
|
||||
}{
|
||||
{
|
||||
"subresource",
|
||||
admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "namespace", "name", schema.GroupVersionResource{}, "subresource", admission.Create, nil, false, nil),
|
||||
},
|
||||
{
|
||||
"non-namespaced resource",
|
||||
admission.NewAttributesRecord(nil, nil, schema.GroupVersionKind{}, "", "namespace", schema.GroupVersionResource{}, "", admission.Create, nil, false, nil),
|
||||
|
|
|
@ -16,4 +16,4 @@ limitations under the License.
|
|||
|
||||
// +k8s:deepcopy-gen=package
|
||||
|
||||
package resourcequota // import "k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota"
|
||||
package resourcequota
|
||||
|
|
|
@ -20,4 +20,4 @@ limitations under the License.
|
|||
// +groupName=resourcequota.admission.k8s.io
|
||||
|
||||
// Package v1 is the v1 version of the API.
|
||||
package v1 // import "k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota/v1"
|
||||
package v1
|
||||
|
|
|
@ -20,4 +20,4 @@ limitations under the License.
|
|||
// +groupName=resourcequota.admission.k8s.io
|
||||
|
||||
// Package v1alpha1 is the v1alpha1 version of the API.
|
||||
package v1alpha1 // import "k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota/v1alpha1"
|
||||
package v1alpha1
|
||||
|
|
|
@ -20,4 +20,4 @@ limitations under the License.
|
|||
// +groupName=resourcequota.admission.k8s.io
|
||||
|
||||
// Package v1beta1 is the v1beta1 version of the API.
|
||||
package v1beta1 // import "k8s.io/apiserver/pkg/admission/plugin/resourcequota/apis/resourcequota/v1beta1"
|
||||
package v1beta1
|
||||
|
|
|
@ -29,7 +29,7 @@ import (
|
|||
|
||||
var (
|
||||
scheme = runtime.NewScheme()
|
||||
codecs = serializer.NewCodecFactory(scheme)
|
||||
codecs = serializer.NewCodecFactory(scheme, serializer.EnableStrict)
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
|
|
@ -43,6 +43,34 @@ func TestLoadConfiguration(t *testing.T) {
|
|||
input: `{"kind":"Unknown","apiVersion":"v1"}`,
|
||||
expectErr: `no kind "Unknown" is registered`,
|
||||
},
|
||||
{
|
||||
name: "duplicate field error; strict validation",
|
||||
input: `
|
||||
kind: ResourceQuotaConfiguration
|
||||
apiVersion: apiserver.config.k8s.io/v1
|
||||
limitedResources:
|
||||
- apiGroup: ""
|
||||
resource: persistentvolumeclaims
|
||||
resource: persistentvolumeclaims
|
||||
matchContains:
|
||||
- .storageclass.storage.k8s.io/requests.storage
|
||||
`,
|
||||
expectErr: `strict decoding error`,
|
||||
},
|
||||
{
|
||||
name: "unknown field error; strict validation",
|
||||
input: `
|
||||
kind: ResourceQuotaConfiguration
|
||||
apiVersion: apiserver.config.k8s.io/v1
|
||||
limitedResources:
|
||||
- apiGroup: ""
|
||||
foo: bar
|
||||
resource: persistentvolumeclaims
|
||||
matchContains:
|
||||
- .storageclass.storage.k8s.io/requests.storage
|
||||
`,
|
||||
expectErr: `strict decoding error`,
|
||||
},
|
||||
{
|
||||
name: "valid v1alpha1 config",
|
||||
input: `
|
||||
|
|
|
@ -492,16 +492,26 @@ func CheckRequest(quotas []corev1.ResourceQuota, a admission.Attributes, evaluat
|
|||
// as a result, we need to measure the usage of this object for quota
|
||||
// on updates, we need to subtract the previous measured usage
|
||||
// if usage shows no change, just return since it has no impact on quota
|
||||
deltaUsage, err := evaluator.Usage(inputObject)
|
||||
inputUsage, err := evaluator.Usage(inputObject)
|
||||
if err != nil {
|
||||
return quotas, err
|
||||
}
|
||||
|
||||
// ensure that usage for input object is never negative (this would mean a resource made a negative resource requirement)
|
||||
if negativeUsage := quota.IsNegative(deltaUsage); len(negativeUsage) > 0 {
|
||||
if negativeUsage := quota.IsNegative(inputUsage); len(negativeUsage) > 0 {
|
||||
return nil, admission.NewForbidden(a, fmt.Errorf("quota usage is negative for resource(s): %s", prettyPrintResourceNames(negativeUsage)))
|
||||
}
|
||||
|
||||
// initialize a map of delta usage for each interesting quota index.
|
||||
deltaUsageIndexMap := make(map[int]corev1.ResourceList, len(interestingQuotaIndexes))
|
||||
for _, index := range interestingQuotaIndexes {
|
||||
deltaUsageIndexMap[index] = inputUsage
|
||||
}
|
||||
var deltaUsageWhenNoInterestingQuota corev1.ResourceList
|
||||
if admission.Create == a.GetOperation() && len(interestingQuotaIndexes) == 0 {
|
||||
deltaUsageWhenNoInterestingQuota = inputUsage
|
||||
}
|
||||
|
||||
if admission.Update == a.GetOperation() {
|
||||
prevItem := a.GetOldObject()
|
||||
if prevItem == nil {
|
||||
|
@ -511,20 +521,55 @@ func CheckRequest(quotas []corev1.ResourceQuota, a admission.Attributes, evaluat
|
|||
// if we can definitively determine that this is not a case of "create on update",
|
||||
// then charge based on the delta. Otherwise, bill the maximum
|
||||
metadata, err := meta.Accessor(prevItem)
|
||||
if err == nil && len(metadata.GetResourceVersion()) > 0 {
|
||||
prevUsage, innerErr := evaluator.Usage(prevItem)
|
||||
if innerErr != nil {
|
||||
return quotas, innerErr
|
||||
if err == nil {
|
||||
if len(metadata.GetResourceVersion()) > 0 {
|
||||
prevUsage, innerErr := evaluator.Usage(prevItem)
|
||||
if innerErr != nil {
|
||||
return quotas, innerErr
|
||||
}
|
||||
|
||||
deltaUsage := quota.SubtractWithNonNegativeResult(inputUsage, prevUsage)
|
||||
if len(interestingQuotaIndexes) == 0 {
|
||||
deltaUsageWhenNoInterestingQuota = deltaUsage
|
||||
}
|
||||
|
||||
for _, index := range interestingQuotaIndexes {
|
||||
resourceQuota := quotas[index]
|
||||
match, err := evaluator.Matches(&resourceQuota, prevItem)
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "Error occurred while matching resource quota against the existing object",
|
||||
"resourceQuota", resourceQuota)
|
||||
return quotas, err
|
||||
}
|
||||
if match {
|
||||
deltaUsageIndexMap[index] = deltaUsage
|
||||
}
|
||||
}
|
||||
} else if len(interestingQuotaIndexes) == 0 {
|
||||
deltaUsageWhenNoInterestingQuota = inputUsage
|
||||
}
|
||||
deltaUsage = quota.SubtractWithNonNegativeResult(deltaUsage, prevUsage)
|
||||
}
|
||||
}
|
||||
|
||||
// ignore items in deltaUsage with zero usage
|
||||
deltaUsage = quota.RemoveZeros(deltaUsage)
|
||||
// ignore items in deltaUsageIndexMap with zero usage,
|
||||
// as they will not impact the quota.
|
||||
for index := range deltaUsageIndexMap {
|
||||
deltaUsageIndexMap[index] = quota.RemoveZeros(deltaUsageIndexMap[index])
|
||||
if len(deltaUsageIndexMap[index]) == 0 {
|
||||
delete(deltaUsageIndexMap, index)
|
||||
}
|
||||
}
|
||||
|
||||
// if there is no remaining non-zero usage, short-circuit and return
|
||||
if len(deltaUsage) == 0 {
|
||||
return quotas, nil
|
||||
if len(interestingQuotaIndexes) != 0 {
|
||||
if len(deltaUsageIndexMap) == 0 {
|
||||
return quotas, nil
|
||||
}
|
||||
} else {
|
||||
deltaUsage := quota.RemoveZeros(deltaUsageWhenNoInterestingQuota)
|
||||
if len(deltaUsage) == 0 {
|
||||
return quotas, nil
|
||||
}
|
||||
}
|
||||
|
||||
// verify that for every resource that had limited by default consumption
|
||||
|
@ -557,22 +602,29 @@ func CheckRequest(quotas []corev1.ResourceQuota, a admission.Attributes, evaluat
|
|||
|
||||
for _, index := range interestingQuotaIndexes {
|
||||
resourceQuota := outQuotas[index]
|
||||
deltaUsage, ok := deltaUsageIndexMap[index]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
hardResources := quota.ResourceNames(resourceQuota.Status.Hard)
|
||||
requestedUsage := quota.Mask(deltaUsage, hardResources)
|
||||
newUsage := quota.Add(resourceQuota.Status.Used, requestedUsage)
|
||||
maskedNewUsage := quota.Mask(newUsage, quota.ResourceNames(requestedUsage))
|
||||
|
||||
if allowed, exceeded := quota.LessThanOrEqual(maskedNewUsage, resourceQuota.Status.Hard); !allowed {
|
||||
failedRequestedUsage := quota.Mask(requestedUsage, exceeded)
|
||||
failedUsed := quota.Mask(resourceQuota.Status.Used, exceeded)
|
||||
failedHard := quota.Mask(resourceQuota.Status.Hard, exceeded)
|
||||
return nil, admission.NewForbidden(a,
|
||||
fmt.Errorf("exceeded quota: %s, requested: %s, used: %s, limited: %s",
|
||||
resourceQuota.Name,
|
||||
prettyPrint(failedRequestedUsage),
|
||||
prettyPrint(failedUsed),
|
||||
prettyPrint(failedHard)))
|
||||
if a.GetSubresource() != "status" {
|
||||
maskedNewUsage := quota.Mask(newUsage, quota.ResourceNames(requestedUsage))
|
||||
|
||||
if allowed, exceeded := quota.LessThanOrEqual(maskedNewUsage, resourceQuota.Status.Hard); !allowed {
|
||||
failedRequestedUsage := quota.Mask(requestedUsage, exceeded)
|
||||
failedUsed := quota.Mask(resourceQuota.Status.Used, exceeded)
|
||||
failedHard := quota.Mask(resourceQuota.Status.Hard, exceeded)
|
||||
return nil, admission.NewForbidden(a,
|
||||
fmt.Errorf("exceeded quota: %s, requested: %s, used: %s, limited: %s",
|
||||
resourceQuota.Name,
|
||||
prettyPrint(failedRequestedUsage),
|
||||
prettyPrint(failedUsed),
|
||||
prettyPrint(failedHard)))
|
||||
}
|
||||
}
|
||||
|
||||
// update to the new usage number
|
||||
|
|
|
@ -16,4 +16,4 @@ limitations under the License.
|
|||
|
||||
// Package resourcequota enforces all incoming requests against any applied quota
|
||||
// in the namespace context of the request
|
||||
package resourcequota // import "k8s.io/apiserver/pkg/admission/plugin/resourcequota"
|
||||
package resourcequota
|
||||
|
|
|
@ -48,6 +48,9 @@ type quotaAccessor struct {
|
|||
// lister can list/get quota objects from a shared informer's cache
|
||||
lister corev1listers.ResourceQuotaLister
|
||||
|
||||
// hasSynced indicates whether the lister has completed its initial sync
|
||||
hasSynced func() bool
|
||||
|
||||
// liveLookups holds the last few live lookups we've done to help ammortize cost on repeated lookup failures.
|
||||
// This lets us handle the case of latent caches, by looking up actual results for a namespace on cache miss/no results.
|
||||
// We track the lookup result here so that for repeated requests, we don't look it up very often.
|
||||
|
@ -112,8 +115,8 @@ func (e *quotaAccessor) GetQuotas(namespace string) ([]corev1.ResourceQuota, err
|
|||
return nil, fmt.Errorf("error resolving quota: %v", err)
|
||||
}
|
||||
|
||||
// if there are no items held in our indexer, check our live-lookup LRU, if that misses, do the live lookup to prime it.
|
||||
if len(items) == 0 {
|
||||
// if there are no items held in our unsynced lister, check our live-lookup LRU, if that misses, do the live lookup to prime it.
|
||||
if len(items) == 0 && !e.hasSynced() {
|
||||
lruItemObj, ok := e.liveLookupCache.Get(namespace)
|
||||
if !ok || lruItemObj.(liveLookupEntry).expiry.Before(time.Now()) {
|
||||
// use singleflight.Group to avoid flooding the apiserver with repeated
|
||||
|
|
|
@ -97,6 +97,7 @@ func TestLRUCacheLookup(t *testing.T) {
|
|||
accessor, _ := newQuotaAccessor()
|
||||
accessor.client = kubeClient
|
||||
accessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
|
||||
accessor.hasSynced = func() bool { return false }
|
||||
accessor.liveLookupCache = liveLookupCache
|
||||
|
||||
for _, q := range tc.cacheInput {
|
||||
|
@ -151,6 +152,7 @@ func TestGetQuotas(t *testing.T) {
|
|||
accessor, _ := newQuotaAccessor()
|
||||
accessor.client = kubeClient
|
||||
accessor.lister = informerFactory.Core().V1().ResourceQuotas().Lister()
|
||||
accessor.hasSynced = func() bool { return false }
|
||||
|
||||
kubeClient.AddReactor("list", "resourcequotas", func(action core.Action) (bool, runtime.Object, error) {
|
||||
switch action.GetNamespace() {
|
||||
|
|
|
@ -50,7 +50,7 @@ type WebhookAccessor interface {
|
|||
GetRESTClient(clientManager *webhookutil.ClientManager) (*rest.RESTClient, error)
|
||||
|
||||
// GetCompiledMatcher gets the compiled matcher object
|
||||
GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher
|
||||
GetCompiledMatcher(compiler cel.ConditionCompiler) matchconditions.Matcher
|
||||
|
||||
// GetName gets the webhook Name field. Note that the name is scoped to the webhook
|
||||
// configuration and does not provide a globally unique identity, if a unique identity is
|
||||
|
@ -132,7 +132,7 @@ func (m *mutatingWebhookAccessor) GetType() string {
|
|||
return "admit"
|
||||
}
|
||||
|
||||
func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher {
|
||||
func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.ConditionCompiler) matchconditions.Matcher {
|
||||
m.compileMatcher.Do(func() {
|
||||
expressions := make([]cel.ExpressionAccessor, len(m.MutatingWebhook.MatchConditions))
|
||||
for i, matchCondition := range m.MutatingWebhook.MatchConditions {
|
||||
|
@ -145,7 +145,7 @@ func (m *mutatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler
|
|||
if utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks) {
|
||||
strictCost = true
|
||||
}
|
||||
m.compiledMatcher = matchconditions.NewMatcher(compiler.Compile(
|
||||
m.compiledMatcher = matchconditions.NewMatcher(compiler.CompileCondition(
|
||||
expressions,
|
||||
cel.OptionalVariableDeclarations{
|
||||
HasParams: false,
|
||||
|
@ -265,7 +265,7 @@ func (v *validatingWebhookAccessor) GetRESTClient(clientManager *webhookutil.Cli
|
|||
return v.client, v.clientErr
|
||||
}
|
||||
|
||||
func (v *validatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher {
|
||||
func (v *validatingWebhookAccessor) GetCompiledMatcher(compiler cel.ConditionCompiler) matchconditions.Matcher {
|
||||
v.compileMatcher.Do(func() {
|
||||
expressions := make([]cel.ExpressionAccessor, len(v.ValidatingWebhook.MatchConditions))
|
||||
for i, matchCondition := range v.ValidatingWebhook.MatchConditions {
|
||||
|
@ -278,7 +278,7 @@ func (v *validatingWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompil
|
|||
if utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks) {
|
||||
strictCost = true
|
||||
}
|
||||
v.compiledMatcher = matchconditions.NewMatcher(compiler.Compile(
|
||||
v.compiledMatcher = matchconditions.NewMatcher(compiler.CompileCondition(
|
||||
expressions,
|
||||
cel.OptionalVariableDeclarations{
|
||||
HasParams: false,
|
||||
|
|
|
@ -22,16 +22,16 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
fuzz "github.com/google/gofuzz"
|
||||
v1 "k8s.io/api/admissionregistration/v1"
|
||||
"sigs.k8s.io/randfill"
|
||||
)
|
||||
|
||||
func TestMutatingWebhookAccessor(t *testing.T) {
|
||||
f := fuzz.New()
|
||||
f := randfill.New()
|
||||
for i := 0; i < 100; i++ {
|
||||
t.Run(fmt.Sprintf("Run %d/100", i), func(t *testing.T) {
|
||||
orig := &v1.MutatingWebhook{}
|
||||
f.Fuzz(orig)
|
||||
f.Fill(orig)
|
||||
|
||||
// zero out any accessor type specific fields not included in the accessor
|
||||
orig.ReinvocationPolicy = nil
|
||||
|
@ -72,11 +72,11 @@ func TestMutatingWebhookAccessor(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestValidatingWebhookAccessor(t *testing.T) {
|
||||
f := fuzz.New()
|
||||
f := randfill.New()
|
||||
for i := 0; i < 100; i++ {
|
||||
t.Run(fmt.Sprintf("Run %d/100", i), func(t *testing.T) {
|
||||
orig := &v1.ValidatingWebhook{}
|
||||
f.Fuzz(orig)
|
||||
f.Fill(orig)
|
||||
uid := fmt.Sprintf("test.configuration.admission/%s/0", orig.Name)
|
||||
accessor := NewValidatingWebhookAccessor(uid, "test.configuration.admission", orig)
|
||||
if uid != accessor.GetUID() {
|
||||
|
|
|
@ -16,4 +16,4 @@ limitations under the License.
|
|||
|
||||
// +k8s:deepcopy-gen=package
|
||||
|
||||
package webhookadmission // import "k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission"
|
||||
package webhookadmission
|
||||
|
|
|
@ -20,4 +20,4 @@ limitations under the License.
|
|||
// +groupName=apiserver.config.k8s.io
|
||||
|
||||
// Package v1 is the v1 version of the API.
|
||||
package v1 // import "k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1"
|
||||
package v1
|
||||
|
|
|
@ -20,4 +20,4 @@ limitations under the License.
|
|||
// +groupName=apiserver.config.k8s.io
|
||||
|
||||
// Package v1alpha1 is the v1alpha1 version of the API.
|
||||
package v1alpha1 // import "k8s.io/apiserver/pkg/admission/plugin/webhook/config/apis/webhookadmission/v1alpha1"
|
||||
package v1alpha1
|
||||
|
|
|
@ -15,4 +15,4 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
// Package errors contains utilities for admission webhook specific errors
|
||||
package errors // import "k8s.io/apiserver/pkg/admission/plugin/webhook/errors"
|
||||
package errors
|
||||
|
|
|
@ -21,6 +21,9 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
|
@ -38,9 +41,6 @@ import (
|
|||
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/rules"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
webhookutil "k8s.io/apiserver/pkg/util/webhook"
|
||||
"k8s.io/client-go/informers"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
|
@ -57,7 +57,7 @@ type Webhook struct {
|
|||
namespaceMatcher *namespace.Matcher
|
||||
objectMatcher *object.Matcher
|
||||
dispatcher Dispatcher
|
||||
filterCompiler cel.FilterCompiler
|
||||
filterCompiler cel.ConditionCompiler
|
||||
authorizer authorizer.Authorizer
|
||||
}
|
||||
|
||||
|
@ -102,7 +102,7 @@ func NewWebhook(handler *admission.Handler, configFile io.Reader, sourceFactory
|
|||
namespaceMatcher: &namespace.Matcher{},
|
||||
objectMatcher: &object.Matcher{},
|
||||
dispatcher: dispatcherFactory(&cm),
|
||||
filterCompiler: cel.NewFilterCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks))),
|
||||
filterCompiler: cel.NewConditionCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), utilfeature.DefaultFeatureGate.Enabled(features.StrictCostEnforcementForWebhooks))),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -76,7 +76,7 @@ type fakeWebhookAccessor struct {
|
|||
matchResult bool
|
||||
}
|
||||
|
||||
func (f *fakeWebhookAccessor) GetCompiledMatcher(compiler cel.FilterCompiler) matchconditions.Matcher {
|
||||
func (f *fakeWebhookAccessor) GetCompiledMatcher(compiler cel.ConditionCompiler) matchconditions.Matcher {
|
||||
return &fakeMatcher{
|
||||
throwError: f.throwError,
|
||||
matchResult: f.matchResult,
|
||||
|
|
|
@ -54,14 +54,14 @@ var _ Matcher = &matcher{}
|
|||
|
||||
// matcher evaluates compiled cel expressions and determines if they match the given request or not
|
||||
type matcher struct {
|
||||
filter celplugin.Filter
|
||||
filter celplugin.ConditionEvaluator
|
||||
failPolicy v1.FailurePolicyType
|
||||
matcherType string
|
||||
matcherKind string
|
||||
objectName string
|
||||
}
|
||||
|
||||
func NewMatcher(filter celplugin.Filter, failPolicy *v1.FailurePolicyType, matcherKind, matcherType, objectName string) Matcher {
|
||||
func NewMatcher(filter celplugin.ConditionEvaluator, failPolicy *v1.FailurePolicyType, matcherKind, matcherType, objectName string) Matcher {
|
||||
var f v1.FailurePolicyType
|
||||
if failPolicy == nil {
|
||||
f = v1.Fail
|
||||
|
|
|
@ -35,7 +35,7 @@ import (
|
|||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
)
|
||||
|
||||
var _ cel.Filter = &fakeCelFilter{}
|
||||
var _ cel.ConditionEvaluator = &fakeCelFilter{}
|
||||
|
||||
type fakeCelFilter struct {
|
||||
evaluations []cel.EvaluationResult
|
||||
|
|
|
@ -190,7 +190,7 @@ func (a *mutatingDispatcher) Dispatch(ctx context.Context, attr admission.Attrib
|
|||
admissionmetrics.Metrics.ObserveWebhook(ctx, hook.Name, time.Since(t), rejected, versionedAttr.Attributes, "admit", 200)
|
||||
}
|
||||
if changed {
|
||||
// Patch had changed the object. Prepare to reinvoke all previous webhooks that are eligible for re-invocation.
|
||||
// Patch had changed the object. Prepare to reinvoke all previous mutations that are eligible for re-invocation.
|
||||
webhookReinvokeCtx.RequireReinvokingPreviouslyInvokedPlugins()
|
||||
reinvokeCtx.SetShouldReinvoke()
|
||||
}
|
||||
|
@ -261,7 +261,7 @@ func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *admiss
|
|||
// Make the webhook request
|
||||
client, err := invocation.Webhook.GetRESTClient(a.cm)
|
||||
if err != nil {
|
||||
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("could not get REST client: %w", err), Status: apierrors.NewBadRequest("error getting REST client")}
|
||||
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("could not get REST client: %w", err), Status: apierrors.NewInternalError(err)}
|
||||
}
|
||||
ctx, span := tracing.Start(ctx, "Call mutating webhook",
|
||||
attribute.String("configuration", configurationName),
|
||||
|
@ -305,7 +305,7 @@ func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *admiss
|
|||
if se, ok := err.(*apierrors.StatusError); ok {
|
||||
status = se
|
||||
} else {
|
||||
status = apierrors.NewBadRequest("error calling webhook")
|
||||
status = apierrors.NewServiceUnavailable("error calling webhook")
|
||||
}
|
||||
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("failed to call webhook: %w", err), Status: status}
|
||||
}
|
||||
|
@ -335,7 +335,7 @@ func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *admiss
|
|||
}
|
||||
patchObj, err := jsonpatch.DecodePatch(result.Patch)
|
||||
if err != nil {
|
||||
return false, apierrors.NewInternalError(err)
|
||||
return false, &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("received undecodable patch in webhook response: %w", err), Status: apierrors.NewServiceUnavailable("error decoding patch in webhook response")}
|
||||
}
|
||||
|
||||
if len(patchObj) == 0 {
|
||||
|
@ -348,7 +348,7 @@ func (a *mutatingDispatcher) callAttrMutatingHook(ctx context.Context, h *admiss
|
|||
}
|
||||
|
||||
var patchedJS []byte
|
||||
jsonSerializer := json.NewSerializer(json.DefaultMetaFactory, o.GetObjectCreater(), o.GetObjectTyper(), false)
|
||||
jsonSerializer := json.NewSerializerWithOptions(json.DefaultMetaFactory, o.GetObjectCreater(), o.GetObjectTyper(), json.SerializerOptions{})
|
||||
switch result.PatchType {
|
||||
// VerifyAdmissionResponse normalizes to v1 patch types, regardless of the AdmissionReview version used
|
||||
case admissionv1.PatchTypeJSONPatch:
|
||||
|
|
|
@ -16,4 +16,4 @@ limitations under the License.
|
|||
|
||||
// Package mutating makes calls to mutating webhooks during the admission
|
||||
// process.
|
||||
package mutating // import "k8s.io/apiserver/pkg/admission/plugin/webhook/mutating"
|
||||
package mutating
|
||||
|
|
|
@ -26,14 +26,16 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
clocktesting "k8s.io/utils/clock/testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
|
||||
webhooktesting "k8s.io/apiserver/pkg/admission/plugin/webhook/testing"
|
||||
auditinternal "k8s.io/apiserver/pkg/apis/audit"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/component-base/metrics/testutil"
|
||||
clocktesting "k8s.io/utils/clock/testing"
|
||||
)
|
||||
|
||||
// BenchmarkAdmit tests the performance cost of invoking a mutating webhook
|
||||
|
@ -156,6 +158,9 @@ func TestAdmit(t *testing.T) {
|
|||
attr = webhooktesting.NewAttribute(ns, tt.AdditionalLabels, tt.IsDryRun)
|
||||
}
|
||||
|
||||
if len(tt.ExpectRejectionMetrics) > 0 {
|
||||
admissionmetrics.Metrics.WebhookRejectionGathererForTest().Reset()
|
||||
}
|
||||
err = wh.Admit(context.TODO(), attr, objectInterfaces)
|
||||
if tt.ExpectAllow != (err == nil) {
|
||||
t.Errorf("expected allowed=%v, but got err=%v", tt.ExpectAllow, err)
|
||||
|
@ -178,6 +183,15 @@ func TestAdmit(t *testing.T) {
|
|||
t.Errorf("expected status code %d, got %d", tt.ExpectStatusCode, statusErr.ErrStatus.Code)
|
||||
}
|
||||
}
|
||||
if len(tt.ExpectRejectionMetrics) > 0 {
|
||||
expectedMetrics := `
|
||||
# HELP apiserver_admission_webhook_rejection_count [ALPHA] Admission webhook rejection count, identified by name and broken out for each admission type (validating or admit) and operation. Additional labels specify an error type (calling_webhook_error or apiserver_internal_error if an error occurred; no_error otherwise) and optionally a non-zero rejection code if the webhook rejects the request with an HTTP status code (honored by the apiserver when the code is greater or equal to 400). Codes greater than 600 are truncated to 600, to keep the metrics cardinality bounded.
|
||||
# TYPE apiserver_admission_webhook_rejection_count counter
|
||||
` + tt.ExpectRejectionMetrics + "\n"
|
||||
if err := testutil.CollectAndCompare(admissionmetrics.Metrics.WebhookRejectionGathererForTest(), strings.NewReader(expectedMetrics), "apiserver_admission_webhook_rejection_count"); err != nil {
|
||||
t.Errorf("unexpected collecting result:\n%s", err)
|
||||
}
|
||||
}
|
||||
fakeAttr, ok := attr.(*webhooktesting.FakeAttributes)
|
||||
if !ok {
|
||||
t.Errorf("Unexpected error, failed to convert attr to webhooktesting.FakeAttributes")
|
||||
|
|
|
@ -17,4 +17,4 @@ limitations under the License.
|
|||
// Package namespace defines the utilities that are used by the webhook
|
||||
// plugin to decide if a webhook should be applied to an object based on its
|
||||
// namespace.
|
||||
package namespace // import "k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/namespace"
|
||||
package namespace
|
||||
|
|
|
@ -95,8 +95,8 @@ func (m *Matcher) GetNamespaceLabels(attr admission.Attributes) (map[string]stri
|
|||
return namespace.Labels, nil
|
||||
}
|
||||
|
||||
// MatchNamespaceSelector decideds whether the request matches the
|
||||
// namespaceSelctor of the webhook. Only when they match, the webhook is called.
|
||||
// MatchNamespaceSelector decides whether the request matches the
|
||||
// namespaceSelector of the webhook. Only when they match, the webhook is called.
|
||||
func (m *Matcher) MatchNamespaceSelector(p NamespaceSelectorProvider, attr admission.Attributes) (bool, *apierrors.StatusError) {
|
||||
namespaceName := attr.GetNamespace()
|
||||
if len(namespaceName) == 0 && attr.GetResource().Resource != "namespaces" {
|
||||
|
|
|
@ -17,4 +17,4 @@ limitations under the License.
|
|||
// Package object defines the utilities that are used by the webhook plugin to
|
||||
// decide if a webhook should run, as long as either the old object or the new
|
||||
// object has labels matching the webhook config's objectSelector.
|
||||
package object // import "k8s.io/apiserver/pkg/admission/plugin/webhook/predicates/object"
|
||||
package object
|
||||
|
|
|
@ -47,7 +47,7 @@ func matchObject(obj runtime.Object, selector labels.Selector) bool {
|
|||
|
||||
}
|
||||
|
||||
// MatchObjectSelector decideds whether the request matches the ObjectSelector
|
||||
// MatchObjectSelector decides whether the request matches the ObjectSelector
|
||||
// of the webhook. Only when they match, the webhook is called.
|
||||
func (m *Matcher) MatchObjectSelector(p ObjectSelectorProvider, attr admission.Attributes) (bool, *apierrors.StatusError) {
|
||||
selector, err := p.GetParsedObjectSelector()
|
||||
|
|
|
@ -121,7 +121,7 @@ func (r *Matcher) resource() bool {
|
|||
func IsExemptAdmissionConfigurationResource(attr admission.Attributes) bool {
|
||||
gvk := attr.GetKind()
|
||||
if gvk.Group == "admissionregistration.k8s.io" {
|
||||
if gvk.Kind == "ValidatingWebhookConfiguration" || gvk.Kind == "MutatingWebhookConfiguration" || gvk.Kind == "ValidatingAdmissionPolicy" || gvk.Kind == "ValidatingAdmissionPolicyBinding" {
|
||||
if gvk.Kind == "ValidatingWebhookConfiguration" || gvk.Kind == "MutatingWebhookConfiguration" || gvk.Kind == "ValidatingAdmissionPolicy" || gvk.Kind == "ValidatingAdmissionPolicyBinding" || gvk.Kind == "MutatingAdmissionPolicy" || gvk.Kind == "MutatingAdmissionPolicyBinding" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ import (
|
|||
"k8s.io/apiserver/pkg/admission/plugin/webhook"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
utilpointer "k8s.io/utils/pointer"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
func TestVerifyAdmissionResponse(t *testing.T) {
|
||||
|
@ -535,7 +535,7 @@ func TestCreateAdmissionObjects(t *testing.T) {
|
|||
},
|
||||
Object: runtime.RawExtension{Object: versionedObj},
|
||||
OldObject: runtime.RawExtension{Object: versionedObjOld},
|
||||
DryRun: utilpointer.BoolPtr(false),
|
||||
DryRun: ptr.To(false),
|
||||
Options: runtime.RawExtension{Object: &metav1.UpdateOptions{FieldManager: "foo"}},
|
||||
},
|
||||
}
|
||||
|
@ -578,7 +578,7 @@ func TestCreateAdmissionObjects(t *testing.T) {
|
|||
},
|
||||
Object: runtime.RawExtension{Object: versionedObj},
|
||||
OldObject: runtime.RawExtension{Object: versionedObjOld},
|
||||
DryRun: utilpointer.BoolPtr(false),
|
||||
DryRun: ptr.To(false),
|
||||
Options: runtime.RawExtension{Object: &metav1.UpdateOptions{FieldManager: "foo"}},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -15,4 +15,4 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
// Package request creates admissionReview request based on admission attributes.
|
||||
package request // import "k8s.io/apiserver/pkg/admission/plugin/webhook/request"
|
||||
package request
|
||||
|
|
|
@ -16,4 +16,4 @@ limitations under the License.
|
|||
|
||||
// Package testcerts contains generated key pairs used by the unit tests of
|
||||
// mutating and validating webhooks. They are for testing only.
|
||||
package testcerts // import "k8s.io/apiserver/pkg/admission/plugin/webhook/testcerts"
|
||||
package testcerts
|
||||
|
|
|
@ -38,6 +38,7 @@ import (
|
|||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
fakeclientset "k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
var matchEverythingRules = []registrationv1.RuleWithOperations{{
|
||||
|
@ -224,6 +225,7 @@ type ValidatingTest struct {
|
|||
ErrorContains string
|
||||
ExpectAnnotations map[string]string
|
||||
ExpectStatusCode int32
|
||||
ExpectRejectionMetrics string
|
||||
ExpectReinvokeWebhooks map[string]bool
|
||||
}
|
||||
|
||||
|
@ -241,6 +243,7 @@ type MutatingTest struct {
|
|||
ErrorContains string
|
||||
ExpectAnnotations map[string]string
|
||||
ExpectStatusCode int32
|
||||
ExpectRejectionMetrics string
|
||||
ExpectReinvokeWebhooks map[string]bool
|
||||
}
|
||||
|
||||
|
@ -288,7 +291,8 @@ func ConvertToMutatingTestCases(tests []ValidatingTest, configurationName string
|
|||
t.ExpectAnnotations[newKey] = value
|
||||
delete(t.ExpectAnnotations, key)
|
||||
}
|
||||
r[i] = MutatingTest{t.Name, ConvertToMutatingWebhooks(t.Webhooks), t.Path, t.IsCRD, t.IsDryRun, t.AdditionalLabels, t.SkipBenchmark, t.ExpectLabels, t.ExpectAllow, t.ErrorContains, t.ExpectAnnotations, t.ExpectStatusCode, t.ExpectReinvokeWebhooks}
|
||||
expectedMetrics := strings.ReplaceAll(t.ExpectRejectionMetrics, `type="validating"`, `type="admit"`)
|
||||
r[i] = MutatingTest{t.Name, ConvertToMutatingWebhooks(t.Webhooks), t.Path, t.IsCRD, t.IsDryRun, t.AdditionalLabels, t.SkipBenchmark, t.ExpectLabels, t.ExpectAllow, t.ErrorContains, t.ExpectAnnotations, t.ExpectStatusCode, expectedMetrics, t.ExpectReinvokeWebhooks}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
@ -506,6 +510,34 @@ func NewNonMutatingTestCases(url *url.URL) []ValidatingTest {
|
|||
ExpectStatusCode: http.StatusInternalServerError,
|
||||
ExpectAllow: false,
|
||||
},
|
||||
{
|
||||
Name: "match & invalid client config",
|
||||
Webhooks: []registrationv1.ValidatingWebhook{{
|
||||
Name: "invalidClientConfig",
|
||||
ClientConfig: registrationv1.WebhookClientConfig{},
|
||||
Rules: matchEverythingRules,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
AdmissionReviewVersions: []string{"v1beta1"},
|
||||
}},
|
||||
ExpectStatusCode: http.StatusInternalServerError,
|
||||
ExpectRejectionMetrics: `apiserver_admission_webhook_rejection_count{error_type="calling_webhook_error",name="invalidClientConfig",operation="UPDATE",rejection_code="500",type="validating"} 1`,
|
||||
ErrorContains: "could not get REST client",
|
||||
},
|
||||
{
|
||||
Name: "match & non-status error",
|
||||
Webhooks: []registrationv1.ValidatingWebhook{{
|
||||
Name: "nonStatusError",
|
||||
ClientConfig: ccfgSVC("nonStatusError"),
|
||||
Rules: matchEverythingRules,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
AdmissionReviewVersions: []string{"v1beta1"},
|
||||
}},
|
||||
ExpectStatusCode: http.StatusInternalServerError,
|
||||
ExpectRejectionMetrics: `apiserver_admission_webhook_rejection_count{error_type="calling_webhook_error",name="nonStatusError",operation="UPDATE",rejection_code="503",type="validating"} 1`,
|
||||
ErrorContains: "failed to call webhook",
|
||||
},
|
||||
{
|
||||
Name: "match & allow (url)",
|
||||
Webhooks: []registrationv1.ValidatingWebhook{{
|
||||
|
@ -897,6 +929,74 @@ func NewMutatingTestCases(url *url.URL, configurationName string) []MutatingTest
|
|||
"mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "invalidMutation", false),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "match & invalid patch",
|
||||
Webhooks: []registrationv1.MutatingWebhook{{
|
||||
Name: "invalidPatch",
|
||||
ClientConfig: ccfgSVC("invalidPatch"),
|
||||
Rules: matchEverythingRules,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
AdmissionReviewVersions: []string{"v1beta1"},
|
||||
}},
|
||||
ExpectStatusCode: http.StatusInternalServerError,
|
||||
ErrorContains: "unexpected end of JSON input",
|
||||
ExpectAnnotations: map[string]string{
|
||||
"mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "invalidPatch", false),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "match & invalid patch fail open",
|
||||
Webhooks: []registrationv1.MutatingWebhook{{
|
||||
Name: "invalidPatch",
|
||||
ClientConfig: ccfgSVC("invalidPatch"),
|
||||
Rules: matchEverythingRules,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
AdmissionReviewVersions: []string{"v1beta1"},
|
||||
FailurePolicy: ptr.To(registrationv1.Ignore),
|
||||
}},
|
||||
ExpectAllow: true,
|
||||
ExpectStatusCode: http.StatusOK,
|
||||
ExpectAnnotations: map[string]string{
|
||||
"failed-open.mutation.webhook.admission.k8s.io/round_0_index_0": "invalidPatch",
|
||||
"mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "invalidPatch", false),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "match & invalid client config",
|
||||
Webhooks: []registrationv1.MutatingWebhook{{
|
||||
Name: "invalidClientConfig",
|
||||
ClientConfig: registrationv1.WebhookClientConfig{},
|
||||
Rules: matchEverythingRules,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
AdmissionReviewVersions: []string{"v1beta1"},
|
||||
}},
|
||||
ExpectStatusCode: http.StatusInternalServerError,
|
||||
ExpectRejectionMetrics: `apiserver_admission_webhook_rejection_count{error_type="calling_webhook_error",name="invalidClientConfig",operation="UPDATE",rejection_code="500",type="admit"} 1`,
|
||||
ErrorContains: "could not get REST client",
|
||||
ExpectAnnotations: map[string]string{
|
||||
"mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "invalidClientConfig", false),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "match & non-status error",
|
||||
Webhooks: []registrationv1.MutatingWebhook{{
|
||||
Name: "nonStatusError",
|
||||
ClientConfig: ccfgSVC("nonStatusError"),
|
||||
Rules: matchEverythingRules,
|
||||
NamespaceSelector: &metav1.LabelSelector{},
|
||||
ObjectSelector: &metav1.LabelSelector{},
|
||||
AdmissionReviewVersions: []string{"v1beta1"},
|
||||
}},
|
||||
ExpectStatusCode: http.StatusInternalServerError,
|
||||
ExpectRejectionMetrics: `apiserver_admission_webhook_rejection_count{error_type="calling_webhook_error",name="nonStatusError",operation="UPDATE",rejection_code="503",type="admit"} 1`,
|
||||
ErrorContains: "failed to call webhook",
|
||||
ExpectAnnotations: map[string]string{
|
||||
"mutation.webhook.admission.k8s.io/round_0_index_0": mutationAnnotationValue(configurationName, "nonStatusError", false),
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "match & remove label dry run unsupported",
|
||||
Webhooks: []registrationv1.MutatingWebhook{{
|
||||
|
|
|
@ -137,6 +137,16 @@ func webhookHandler(w http.ResponseWriter, r *http.Request) {
|
|||
Patch: []byte(`[{"op": "add", "path": "/metadata/labels/added", "value": "test"}]`),
|
||||
},
|
||||
})
|
||||
case "/invalidPatch":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
pt := v1beta1.PatchTypeJSONPatch
|
||||
json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{
|
||||
Response: &v1beta1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
PatchType: &pt,
|
||||
Patch: []byte(`[{`),
|
||||
},
|
||||
})
|
||||
case "/invalidMutation":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
pt := v1beta1.PatchTypeJSONPatch
|
||||
|
@ -147,6 +157,11 @@ func webhookHandler(w http.ResponseWriter, r *http.Request) {
|
|||
Patch: []byte(`[{"op": "add", "CORRUPTED_KEY":}]`),
|
||||
},
|
||||
})
|
||||
case "/nonStatusError":
|
||||
hj, _ := w.(http.Hijacker)
|
||||
conn, _, _ := hj.Hijack()
|
||||
defer conn.Close() //nolint:errcheck
|
||||
conn.Write([]byte("bad-http")) //nolint:errcheck
|
||||
case "/nilResponse":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(&v1beta1.AdmissionReview{})
|
||||
|
|
|
@ -262,7 +262,7 @@ func (d *validatingDispatcher) callHook(ctx context.Context, h *v1.ValidatingWeb
|
|||
// Make the webhook request
|
||||
client, err := invocation.Webhook.GetRESTClient(d.cm)
|
||||
if err != nil {
|
||||
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("could not get REST client: %w", err), Status: apierrors.NewBadRequest("error getting REST client")}
|
||||
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("could not get REST client: %w", err), Status: apierrors.NewInternalError(err)}
|
||||
}
|
||||
ctx, span := tracing.Start(ctx, "Call validating webhook",
|
||||
attribute.String("configuration", invocation.Webhook.GetConfigurationName()),
|
||||
|
@ -306,7 +306,7 @@ func (d *validatingDispatcher) callHook(ctx context.Context, h *v1.ValidatingWeb
|
|||
if se, ok := err.(*apierrors.StatusError); ok {
|
||||
status = se
|
||||
} else {
|
||||
status = apierrors.NewBadRequest("error calling webhook")
|
||||
status = apierrors.NewServiceUnavailable("error calling webhook")
|
||||
}
|
||||
return &webhookutil.ErrCallingWebhook{WebhookName: h.Name, Reason: fmt.Errorf("failed to call webhook: %w", err), Status: status}
|
||||
}
|
||||
|
|
|
@ -16,4 +16,4 @@ limitations under the License.
|
|||
|
||||
// Package validating makes calls to validating (i.e., non-mutating) webhooks
|
||||
// during the admission process.
|
||||
package validating // import "k8s.io/apiserver/pkg/admission/plugin/webhook/validating"
|
||||
package validating
|
||||
|
|
|
@ -24,12 +24,14 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
clocktesting "k8s.io/utils/clock/testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
admissionmetrics "k8s.io/apiserver/pkg/admission/metrics"
|
||||
webhooktesting "k8s.io/apiserver/pkg/admission/plugin/webhook/testing"
|
||||
auditinternal "k8s.io/apiserver/pkg/apis/audit"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/component-base/metrics/testutil"
|
||||
clocktesting "k8s.io/utils/clock/testing"
|
||||
)
|
||||
|
||||
// BenchmarkValidate tests that ValidatingWebhook#Validate works as expected
|
||||
|
@ -135,6 +137,10 @@ func TestValidate(t *testing.T) {
|
|||
continue
|
||||
}
|
||||
|
||||
if len(tt.ExpectRejectionMetrics) > 0 {
|
||||
admissionmetrics.Metrics.WebhookRejectionGathererForTest().Reset()
|
||||
}
|
||||
|
||||
attr := webhooktesting.NewAttribute(ns, nil, tt.IsDryRun)
|
||||
err = wh.Validate(context.TODO(), attr, objectInterfaces)
|
||||
if tt.ExpectAllow != (err == nil) {
|
||||
|
@ -149,6 +155,15 @@ func TestValidate(t *testing.T) {
|
|||
if _, isStatusErr := err.(*errors.StatusError); err != nil && !isStatusErr {
|
||||
t.Errorf("%s: expected a StatusError, got %T", tt.Name, err)
|
||||
}
|
||||
if len(tt.ExpectRejectionMetrics) > 0 {
|
||||
expectedMetrics := `
|
||||
# HELP apiserver_admission_webhook_rejection_count [ALPHA] Admission webhook rejection count, identified by name and broken out for each admission type (validating or admit) and operation. Additional labels specify an error type (calling_webhook_error or apiserver_internal_error if an error occurred; no_error otherwise) and optionally a non-zero rejection code if the webhook rejects the request with an HTTP status code (honored by the apiserver when the code is greater or equal to 400). Codes greater than 600 are truncated to 600, to keep the metrics cardinality bounded.
|
||||
# TYPE apiserver_admission_webhook_rejection_count counter
|
||||
` + tt.ExpectRejectionMetrics + "\n"
|
||||
if err := testutil.CollectAndCompare(admissionmetrics.Metrics.WebhookRejectionGathererForTest(), strings.NewReader(expectedMetrics), "apiserver_admission_webhook_rejection_count"); err != nil {
|
||||
t.Errorf("unexpected collecting result:\n%s", err)
|
||||
}
|
||||
}
|
||||
fakeAttr, ok := attr.(*webhooktesting.FakeAttributes)
|
||||
if !ok {
|
||||
t.Errorf("Unexpected error, failed to convert attr to webhooktesting.FakeAttributes")
|
||||
|
|
|
@ -16,4 +16,4 @@ limitations under the License.
|
|||
|
||||
// +groupName=apidiscovery.k8s.io
|
||||
|
||||
package v2 // import "k8s.io/apiserver/pkg/apis/apidiscovery/v2"
|
||||
package v2
|
||||
|
|
|
@ -29,8 +29,8 @@ import (
|
|||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
fuzz "github.com/google/gofuzz"
|
||||
"github.com/stretchr/testify/require"
|
||||
"sigs.k8s.io/randfill"
|
||||
)
|
||||
|
||||
func TestConversionRoundTrip(t *testing.T) {
|
||||
|
@ -42,12 +42,12 @@ func TestConversionRoundTrip(t *testing.T) {
|
|||
err = v2scheme.RegisterConversions(scheme)
|
||||
require.NoError(t, err)
|
||||
|
||||
fuzzer := fuzz.NewWithSeed(2374375)
|
||||
fuzzer := randfill.NewWithSeed(2374375)
|
||||
|
||||
// v2 -> v2beta1 -> v2
|
||||
for i := 0; i < 100; i++ {
|
||||
expected := &v2.APIGroupDiscoveryList{}
|
||||
fuzzer.Fuzz(expected)
|
||||
fuzzer.Fill(expected)
|
||||
expected.TypeMeta = metav1.TypeMeta{
|
||||
Kind: "APIGroupDiscoveryList",
|
||||
APIVersion: "apidiscovery.k8s.io/v2",
|
||||
|
@ -68,7 +68,7 @@ func TestConversionRoundTrip(t *testing.T) {
|
|||
// v2beta1 -> v2 -> v2beta1
|
||||
for i := 0; i < 100; i++ {
|
||||
expected := &v2beta1.APIGroupDiscoveryList{}
|
||||
fuzzer.Fuzz(expected)
|
||||
fuzzer.Fill(expected)
|
||||
expected.TypeMeta = metav1.TypeMeta{
|
||||
Kind: "APIGroupDiscoveryList",
|
||||
APIVersion: "apidiscovery.k8s.io/v2beta1",
|
||||
|
|
|
@ -16,4 +16,4 @@ limitations under the License.
|
|||
|
||||
// +groupName=apidiscovery.k8s.io
|
||||
|
||||
package v2beta1 // import "k8s.io/apiserver/pkg/apis/apidiscovery/v2beta1"
|
||||
package v2beta1
|
||||
|
|
|
@ -18,4 +18,4 @@ limitations under the License.
|
|||
// +groupName=apiserver.k8s.io
|
||||
|
||||
// Package apiserver is the internal version of the API.
|
||||
package apiserver // import "k8s.io/apiserver/pkg/apis/apiserver"
|
||||
package apiserver
|
||||
|
|
|
@ -213,8 +213,10 @@ func TestLoadFromData(t *testing.T) {
|
|||
Type: "Webhook",
|
||||
Name: "default",
|
||||
Webhook: &api.WebhookConfiguration{
|
||||
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
||||
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
||||
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
||||
CacheAuthorizedRequests: true,
|
||||
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
||||
CacheUnauthorizedRequests: true,
|
||||
},
|
||||
}},
|
||||
},
|
||||
|
@ -252,8 +254,10 @@ authorizers:
|
|||
Type: "Webhook",
|
||||
Name: "default",
|
||||
Webhook: &api.WebhookConfiguration{
|
||||
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
||||
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
||||
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
||||
CacheAuthorizedRequests: true,
|
||||
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
||||
CacheUnauthorizedRequests: true,
|
||||
},
|
||||
}},
|
||||
},
|
||||
|
@ -265,6 +269,47 @@ apiVersion: apiserver.config.k8s.io/v1beta1
|
|||
kind: AuthorizationConfiguration
|
||||
authorizers:
|
||||
- type: Webhook
|
||||
`),
|
||||
expectConfig: &api.AuthorizationConfiguration{
|
||||
Authorizers: []api.AuthorizerConfiguration{{Type: "Webhook"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "v1 - json",
|
||||
data: []byte(`{
|
||||
"apiVersion":"apiserver.config.k8s.io/v1",
|
||||
"kind":"AuthorizationConfiguration",
|
||||
"authorizers":[{"type":"Webhook"}]}`),
|
||||
expectConfig: &api.AuthorizationConfiguration{
|
||||
Authorizers: []api.AuthorizerConfiguration{{Type: "Webhook"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "v1 - defaults",
|
||||
data: []byte(`{
|
||||
"apiVersion":"apiserver.config.k8s.io/v1",
|
||||
"kind":"AuthorizationConfiguration",
|
||||
"authorizers":[{"type":"Webhook","name":"default","webhook":{}}]}`),
|
||||
expectConfig: &api.AuthorizationConfiguration{
|
||||
Authorizers: []api.AuthorizerConfiguration{{
|
||||
Type: "Webhook",
|
||||
Name: "default",
|
||||
Webhook: &api.WebhookConfiguration{
|
||||
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
||||
CacheAuthorizedRequests: true,
|
||||
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
||||
CacheUnauthorizedRequests: true,
|
||||
},
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "v1 - yaml",
|
||||
data: []byte(`
|
||||
apiVersion: apiserver.config.k8s.io/v1
|
||||
kind: AuthorizationConfiguration
|
||||
authorizers:
|
||||
- type: Webhook
|
||||
`),
|
||||
expectConfig: &api.AuthorizationConfiguration{
|
||||
Authorizers: []api.AuthorizerConfiguration{{Type: "Webhook"}},
|
||||
|
|
|
@ -334,11 +334,21 @@ type WebhookConfiguration struct {
|
|||
// Same as setting `--authorization-webhook-cache-authorized-ttl` flag
|
||||
// Default: 5m0s
|
||||
AuthorizedTTL metav1.Duration
|
||||
// CacheAuthorizedRequests specifies whether authorized requests should be cached.
|
||||
// If set to true, the TTL for cached decisions can be configured via the
|
||||
// AuthorizedTTL field.
|
||||
// Default: true
|
||||
CacheAuthorizedRequests bool
|
||||
// The duration to cache 'unauthorized' responses from the webhook
|
||||
// authorizer.
|
||||
// Same as setting `--authorization-webhook-cache-unauthorized-ttl` flag
|
||||
// Default: 30s
|
||||
UnauthorizedTTL metav1.Duration
|
||||
// CacheUnauthorizedRequests specifies whether unauthorized requests should be cached.
|
||||
// If set to true, the TTL for cached decisions can be configured via the
|
||||
// UnauthorizedTTL field.
|
||||
// Default: true
|
||||
CacheUnauthorizedRequests bool
|
||||
// Timeout for the webhook request
|
||||
// Maximum allowed value is 30s.
|
||||
// Required, no default value.
|
||||
|
@ -401,6 +411,13 @@ type WebhookMatchCondition struct {
|
|||
// If version specified by subjectAccessReviewVersion in the request variable is v1beta1,
|
||||
// the contents would be converted to the v1 version before evaluating the CEL expression.
|
||||
//
|
||||
// - 'resourceAttributes' describes information for a resource access request and is unset for non-resource requests. e.g. has(request.resourceAttributes) && request.resourceAttributes.namespace == 'default'
|
||||
// - 'nonResourceAttributes' describes information for a non-resource access request and is unset for resource requests. e.g. has(request.nonResourceAttributes) && request.nonResourceAttributes.path == '/healthz'.
|
||||
// - 'user' is the user to test for. e.g. request.user == 'alice'
|
||||
// - 'groups' is the groups to test for. e.g. ('group1' in request.groups)
|
||||
// - 'extra' corresponds to the user.Info.GetExtra() method from the authenticator.
|
||||
// - 'uid' is the information about the requesting user. e.g. request.uid == '1'
|
||||
//
|
||||
// Documentation on CEL: https://kubernetes.io/docs/reference/using-api/cel/
|
||||
Expression string
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -48,3 +49,18 @@ func SetDefaults_KMSConfiguration(obj *KMSConfiguration) {
|
|||
obj.CacheSize = &defaultCacheSize
|
||||
}
|
||||
}
|
||||
|
||||
func SetDefaults_WebhookConfiguration(obj *WebhookConfiguration) {
|
||||
if obj.AuthorizedTTL.Duration == 0 {
|
||||
obj.AuthorizedTTL.Duration = 5 * time.Minute
|
||||
}
|
||||
if obj.CacheAuthorizedRequests == nil {
|
||||
obj.CacheAuthorizedRequests = ptr.To(true)
|
||||
}
|
||||
if obj.UnauthorizedTTL.Duration == 0 {
|
||||
obj.UnauthorizedTTL.Duration = 30 * time.Second
|
||||
}
|
||||
if obj.CacheUnauthorizedRequests == nil {
|
||||
obj.CacheUnauthorizedRequests = ptr.To(true)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,4 +20,4 @@ limitations under the License.
|
|||
// +groupName=apiserver.config.k8s.io
|
||||
|
||||
// Package v1 is the v1 version of the API.
|
||||
package v1 // import "k8s.io/apiserver/pkg/apis/apiserver/v1"
|
||||
package v1
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue