Compare commits
1337 Commits
release-1.
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
259926ad99 | ||
|
|
d0b54e6b8f | ||
|
|
6054497bbc | ||
|
|
3a2cceea4f | ||
|
|
e7d1553cfc | ||
|
|
3c629baf09 | ||
|
|
f46333ea82 | ||
|
|
5f897118e1 | ||
|
|
a94a8eb422 | ||
|
|
a37a50b808 | ||
|
|
468ed0a1c2 | ||
|
|
0fdef4861e | ||
|
|
eafbf929da | ||
|
|
a552df5625 | ||
|
|
2c551ee4be | ||
|
|
7af770b11f | ||
|
|
36bade7a2d | ||
|
|
5754bd3357 | ||
|
|
095c5d616b | ||
|
|
c3b3a0fc58 | ||
|
|
20d56730ff | ||
|
|
ffc99354d2 | ||
|
|
e845478a96 | ||
|
|
28dd7167d0 | ||
|
|
faee66888e | ||
|
|
fc753ac489 | ||
|
|
88a76022c1 | ||
|
|
31df9d11a9 | ||
|
|
7ed6c18762 | ||
|
|
81d5b13083 | ||
|
|
02624bf3a6 | ||
|
|
842a3d12f6 | ||
|
|
14f1725f53 | ||
|
|
9b142c799a | ||
|
|
d77072b534 | ||
|
|
8a77075cff | ||
|
|
f5b600d4af | ||
|
|
4ae70139d9 | ||
|
|
7660e40fdc | ||
|
|
310b40aa20 | ||
|
|
0b0c72a80b | ||
|
|
14188656c8 | ||
|
|
76bacfdea4 | ||
|
|
ee44e489b5 | ||
|
|
89774fef17 | ||
|
|
acc2369dc7 | ||
|
|
9de06e35eb | ||
|
|
4575adca86 | ||
|
|
ca42a375c2 | ||
|
|
0214eb8d92 | ||
|
|
949518e680 | ||
|
|
81e4bb1fef | ||
|
|
35b40cdb22 | ||
|
|
fa47fe00a2 | ||
|
|
743371ed4f | ||
|
|
75d9f23adb | ||
|
|
e05ebc4990 | ||
|
|
6ffd34dcf9 | ||
|
|
16c6eaf9ae | ||
|
|
e67a55463f | ||
|
|
476cb655b7 | ||
|
|
6c1ccf4d60 | ||
|
|
05f5a79923 | ||
|
|
c2c19f42ad | ||
|
|
3dbc330cf0 | ||
|
|
8ad928330f | ||
|
|
355e77e56f | ||
|
|
376d79a454 | ||
|
|
b74a9461ed | ||
|
|
b4236e1dc7 | ||
|
|
6ecc25edbd | ||
|
|
56ca1fb4ea | ||
|
|
3629346085 | ||
|
|
092072a281 | ||
|
|
e1142f9759 | ||
|
|
fbd3fe69ff | ||
|
|
f05f84979c | ||
|
|
3d6ff9e903 | ||
|
|
4c812576c1 | ||
|
|
6753fc9743 | ||
|
|
8d082cc46e | ||
|
|
f586385c8b | ||
|
|
466c73fa3b | ||
|
|
9e6c78d55c | ||
|
|
c6af4cca10 | ||
|
|
5448466ddc | ||
|
|
0eec2fee71 | ||
|
|
8a3b36bd28 | ||
|
|
35a7350b74 | ||
|
|
241e6d0a6e | ||
|
|
dbd6b406ac | ||
|
|
a069639135 | ||
|
|
76241f9d3b | ||
|
|
e5452ff70e | ||
|
|
9fdd782854 | ||
|
|
053ae28ed5 | ||
|
|
6eb4f41343 | ||
|
|
d9072d8200 | ||
|
|
238abbf771 | ||
|
|
aa4fb9ab4a | ||
|
|
55bc144410 | ||
|
|
c428e091ab | ||
|
|
c13bf422f8 | ||
|
|
b4bbe60b8c | ||
|
|
4fdf573fd1 | ||
|
|
d326daef62 | ||
|
|
98337065ae | ||
|
|
bdda410463 | ||
|
|
1b1df76ef2 | ||
|
|
7f40739b97 | ||
|
|
2faa5c89d1 | ||
|
|
9db879f68f | ||
|
|
e4235dabb8 | ||
|
|
ff07b112b1 | ||
|
|
53e618a4c0 | ||
|
|
a12b7bdb74 | ||
|
|
eae0527839 | ||
|
|
2d79dbb0bb | ||
|
|
a501cdbb56 | ||
|
|
d2c1821148 | ||
|
|
00d44910b8 | ||
|
|
9b21c25783 | ||
|
|
e1deca2a9e | ||
|
|
7ae204d426 | ||
|
|
62621428b1 | ||
|
|
6411958be5 | ||
|
|
ab1f9e4658 | ||
|
|
36d1b42d5c | ||
|
|
8b9d25f6e3 | ||
|
|
323af4d562 | ||
|
|
a946b70b5e | ||
|
|
a7f6866344 | ||
|
|
c71bfc62ba | ||
|
|
c4d6fde1c4 | ||
|
|
306a84193a | ||
|
|
b02f7f14a7 | ||
|
|
ac8ac14545 | ||
|
|
cdb8758b34 | ||
|
|
521f87fe5f | ||
|
|
27141ff083 | ||
|
|
7599516f68 | ||
|
|
e3a18b9cd7 | ||
|
|
eef35a32ab | ||
|
|
e26dace64d | ||
|
|
1520346369 | ||
|
|
702d4358d1 | ||
|
|
f9f1bdaabe | ||
|
|
e66b6109f7 | ||
|
|
0162971ea0 | ||
|
|
53897e5019 | ||
|
|
7e0d8a490c | ||
|
|
7fd7999e49 | ||
|
|
03f773d0ff | ||
|
|
c4bc740fb7 | ||
|
|
5934bc4699 | ||
|
|
3f0d26ec17 | ||
|
|
7665e58613 | ||
|
|
3684a10332 | ||
|
|
ab80a8126b | ||
|
|
6905196665 | ||
|
|
4e283c14fb | ||
|
|
1b5925a494 | ||
|
|
d500b27f1d | ||
|
|
868b4c4c7c | ||
|
|
4bbce1cb22 | ||
|
|
127f50d697 | ||
|
|
e51aab8d1f | ||
|
|
85a746f861 | ||
|
|
3c2be61827 | ||
|
|
42d572306d | ||
|
|
b3f8e7a02c | ||
|
|
476b09cbbf | ||
|
|
487d6647d5 | ||
|
|
0378819c54 | ||
|
|
bbb925cb63 | ||
|
|
9d1a378ce8 | ||
|
|
e2358cabc9 | ||
|
|
5cd12a3943 | ||
|
|
ebb06b8c89 | ||
|
|
d5d01eca3e | ||
|
|
e13bb79578 | ||
|
|
a8b6282b15 | ||
|
|
fc00d73cf5 | ||
|
|
303d46e67c | ||
|
|
a00798bc5e | ||
|
|
1c4a15129b | ||
|
|
0ca35ef26c | ||
|
|
b38a9aacb2 | ||
|
|
5b239fc1d1 | ||
|
|
fdf7566bb7 | ||
|
|
389858b6df | ||
|
|
20adad76ef | ||
|
|
f37ae1c1f6 | ||
|
|
6edd18bb89 | ||
|
|
4d23fe8108 | ||
|
|
7eeefb003c | ||
|
|
3ae5b2bfe4 | ||
|
|
cdebd26ab4 | ||
|
|
3a088c7c86 | ||
|
|
5a363e9d9f | ||
|
|
57ea24281c | ||
|
|
94d7c10baa | ||
|
|
28027897aa | ||
|
|
0c610f91e5 | ||
|
|
990d9ef92b | ||
|
|
6592773a35 | ||
|
|
beee4de10e | ||
|
|
7fde387dd6 | ||
|
|
64c8ac70fb | ||
|
|
f230df938e | ||
|
|
c9b0fdf1d7 | ||
|
|
ea57d15a80 | ||
|
|
ebc048167c | ||
|
|
3b8405a89b | ||
|
|
66d496d1ef | ||
|
|
9d71ae5ad6 | ||
|
|
85d660f0b9 | ||
|
|
d5286296eb | ||
|
|
916d4aed57 | ||
|
|
59d7b7d2b4 | ||
|
|
18c8716f0a | ||
|
|
a1afe44066 | ||
|
|
1695457f9c | ||
|
|
06bc4064c1 | ||
|
|
d67b4f6c36 | ||
|
|
d2ff5887ac | ||
|
|
205926fa80 | ||
|
|
c4dd9d19c2 | ||
|
|
74fe4af98e | ||
|
|
949808f0b2 | ||
|
|
9ef80ef1f7 | ||
|
|
1801212ac7 | ||
|
|
722d5b02d9 | ||
|
|
9f8505205f | ||
|
|
9e81c38c13 | ||
|
|
ff40297bdc | ||
|
|
c8d74d1a7f | ||
|
|
a3f8ec33f4 | ||
|
|
c7718242f9 | ||
|
|
57eeaa4231 | ||
|
|
7edcf47a03 | ||
|
|
f561f22caa | ||
|
|
dcea620ca6 | ||
|
|
9790a5da9c | ||
|
|
b1d281e7bb | ||
|
|
2e7fa935c4 | ||
|
|
8b69efcdb9 | ||
|
|
8b08a337c5 | ||
|
|
e22f946415 | ||
|
|
3c6715a6f9 | ||
|
|
4cf02fd813 | ||
|
|
17d217c2d6 | ||
|
|
32d5a05aef | ||
|
|
e3f3688227 | ||
|
|
ad715565a6 | ||
|
|
d7cad4ac6d | ||
|
|
cc6c67d343 | ||
|
|
6ada626dda | ||
|
|
5bc59003af | ||
|
|
fe583c2f5d | ||
|
|
539281f89e | ||
|
|
e5ea3fe1fb | ||
|
|
f36ea4646d | ||
|
|
ddcdbaa990 | ||
|
|
857ce87f00 | ||
|
|
4643a1c26d | ||
|
|
3bf8dc6fb0 | ||
|
|
f62559128d | ||
|
|
63fe5f32ba | ||
|
|
a6cb9987a9 | ||
|
|
802edf3202 | ||
|
|
6c1e6b1e72 | ||
|
|
3396a604fe | ||
|
|
574446b3c0 | ||
|
|
07e35b1839 | ||
|
|
0989faf4bd | ||
|
|
85bda0f793 | ||
|
|
ac4191ff8e | ||
|
|
08e50d4eb3 | ||
|
|
9c0db45331 | ||
|
|
76da1529d9 | ||
|
|
da6da2a229 | ||
|
|
3cbe3483ca | ||
|
|
bc33f19333 | ||
|
|
189eaf2705 | ||
|
|
cd27e55711 | ||
|
|
6d612b47f6 | ||
|
|
e07953bf74 | ||
|
|
af212ce6b9 | ||
|
|
3911cd48ca | ||
|
|
d4c0ee80ee | ||
|
|
6a0bb821cc | ||
|
|
9a23823f32 | ||
|
|
aea81373eb | ||
|
|
2354965b6f | ||
|
|
c7b48e3f7c | ||
|
|
4e579da8e9 | ||
|
|
04097a1383 | ||
|
|
dbcfccbfc0 | ||
|
|
7e8723d664 | ||
|
|
316cf5c61c | ||
|
|
b1b443543b | ||
|
|
90769d47ed | ||
|
|
f253fcfa86 | ||
|
|
aea2a51eeb | ||
|
|
d0530a6c39 | ||
|
|
21f1c6c977 | ||
|
|
e1c65a0190 | ||
|
|
9783e7e2bf | ||
|
|
e9ae87ae67 | ||
|
|
cda1be72c0 | ||
|
|
66169ba982 | ||
|
|
35adaf04bf | ||
|
|
fca87b7884 | ||
|
|
54b3786f32 | ||
|
|
899abd37b5 | ||
|
|
73f0437ef3 | ||
|
|
f815facf71 | ||
|
|
7ab781d309 | ||
|
|
566a690813 | ||
|
|
2707008c1a | ||
|
|
c4eeb8d3a6 | ||
|
|
3c24681907 | ||
|
|
20bdfeca81 | ||
|
|
0a5023d08e | ||
|
|
0bf38bb93f | ||
|
|
9670f406a3 | ||
|
|
575f7604d6 | ||
|
|
97b45c7a74 | ||
|
|
0ea0404044 | ||
|
|
0fa02b0a55 | ||
|
|
7c787a3bb0 | ||
|
|
ef2501f4b1 | ||
|
|
f56e084bb9 | ||
|
|
c3ed032c79 | ||
|
|
14d0e76f12 | ||
|
|
cc2b7b149e | ||
|
|
9ffba90a73 | ||
|
|
659b16f5f8 | ||
|
|
adc2e9ba54 | ||
|
|
3a7f6d06b1 | ||
|
|
4fa47ce6cd | ||
|
|
fb0aef3d73 | ||
|
|
8cd34503a1 | ||
|
|
e40a046504 | ||
|
|
6710ac15f4 | ||
|
|
026cfc5efd | ||
|
|
880ee7fbaf | ||
|
|
6bf943c156 | ||
|
|
78dfbd37b4 | ||
|
|
03acca8ef7 | ||
|
|
2032688f36 | ||
|
|
3ad462a112 | ||
|
|
9b679ffa75 | ||
|
|
cb4898acb5 | ||
|
|
5077be9482 | ||
|
|
89a5bc87b9 | ||
|
|
a5f22d3841 | ||
|
|
50ac3fd6c5 | ||
|
|
91b0cd0a47 | ||
|
|
9fa1886c02 | ||
|
|
148b90b5b5 | ||
|
|
0715d05733 | ||
|
|
3258f2deee | ||
|
|
72b90f6890 | ||
|
|
addf397b53 | ||
|
|
aeb48b0a69 | ||
|
|
a6ccf924b5 | ||
|
|
e416547192 | ||
|
|
60aa7fb71e | ||
|
|
cbe94440df | ||
|
|
bfe05e4755 | ||
|
|
848a576a05 | ||
|
|
a6da0ca65b | ||
|
|
af195f36f0 | ||
|
|
31e30fbf6e | ||
|
|
f994926487 | ||
|
|
c8ae89f953 | ||
|
|
257c27677a | ||
|
|
73a1a7ce76 | ||
|
|
c2ff8e856b | ||
|
|
5980b604a6 | ||
|
|
d3b670937a | ||
|
|
9593f8d3b3 | ||
|
|
0cfe1cdedf | ||
|
|
39ea6444f9 | ||
|
|
620d956038 | ||
|
|
be77f468a3 | ||
|
|
24eb0b2409 | ||
|
|
b233563e29 | ||
|
|
455837f3e8 | ||
|
|
09808b5016 | ||
|
|
f01df4e686 | ||
|
|
a6db07ff72 | ||
|
|
048e787668 | ||
|
|
3d6c77e3a8 | ||
|
|
11f00c88b5 | ||
|
|
6cbc43e2ae | ||
|
|
84f24cdb6c | ||
|
|
0d8011da8e | ||
|
|
0ff2533ba0 | ||
|
|
7982a19966 | ||
|
|
f4400b9493 | ||
|
|
4facca0ae7 | ||
|
|
8ac09c9ca9 | ||
|
|
cb6d7eaad2 | ||
|
|
4ccb02375f | ||
|
|
329b845f55 | ||
|
|
de29c9d0f5 | ||
|
|
7065229a45 | ||
|
|
68a81854c1 | ||
|
|
d06260ebbb | ||
|
|
cc7b83adf3 | ||
|
|
45270ec03f | ||
|
|
42804dbbac | ||
|
|
00eebce95a | ||
|
|
e00607b722 | ||
|
|
8575e3942c | ||
|
|
da04075120 | ||
|
|
0b684db148 | ||
|
|
1d5e6a1b90 | ||
|
|
0fab3707cc | ||
|
|
212ca9e37e | ||
|
|
89b33a1442 | ||
|
|
6930ecc947 | ||
|
|
430b933869 | ||
|
|
e328a5d144 | ||
|
|
70ec0d8b29 | ||
|
|
fdea6e2edf | ||
|
|
c096341772 | ||
|
|
424f1e9a3d | ||
|
|
4d795ac381 | ||
|
|
7610a8b2dd | ||
|
|
b8bac1e688 | ||
|
|
afda10bc8f | ||
|
|
a9a28b7e42 | ||
|
|
9d784e7e3f | ||
|
|
a53950e5a8 | ||
|
|
134469c5f0 | ||
|
|
aeb5223169 | ||
|
|
73590e1a39 | ||
|
|
a0f3903418 | ||
|
|
21cc1ec89b | ||
|
|
0675ff2fb2 | ||
|
|
c72160f681 | ||
|
|
513b0eb51b | ||
|
|
4169697302 | ||
|
|
9c3c2f3f14 | ||
|
|
99426ce659 | ||
|
|
7e4cb92fb8 | ||
|
|
a55087b6fd | ||
|
|
2d73fea0a5 | ||
|
|
5706a17155 | ||
|
|
bbfb96cb01 | ||
|
|
500730ef6c | ||
|
|
078f5ccccf | ||
|
|
cf5d9db5bb | ||
|
|
6c93047367 | ||
|
|
e7b5007361 | ||
|
|
4cb84b37ce | ||
|
|
572d376dab | ||
|
|
941ccda32e | ||
|
|
8d5939f128 | ||
|
|
e8c21ab010 | ||
|
|
0f2a88102d | ||
|
|
e3edd2ced3 | ||
|
|
37641cf2d0 | ||
|
|
2148b593ee | ||
|
|
5ec5aeb002 | ||
|
|
bbdbe364b0 | ||
|
|
8df3bad4c8 | ||
|
|
f44681693a | ||
|
|
656167fceb | ||
|
|
87cb4987c9 | ||
|
|
549503c87a | ||
|
|
ae959ec575 | ||
|
|
2c88adc2ed | ||
|
|
8e60bdfe59 | ||
|
|
803b346caf | ||
|
|
4bd81730ef | ||
|
|
df56d036ff | ||
|
|
213ba364d7 | ||
|
|
d06df7438f | ||
|
|
9ebb12cf50 | ||
|
|
cb99c9c0ee | ||
|
|
1374107bea | ||
|
|
6e60762067 | ||
|
|
15cf89535e | ||
|
|
88e9cf0e18 | ||
|
|
402ce43804 | ||
|
|
f75984fbf5 | ||
|
|
f72a5c76a0 | ||
|
|
9aa1b18610 | ||
|
|
2e05e50fcb | ||
|
|
138ec15834 | ||
|
|
e422d33fec | ||
|
|
d925b73bf8 | ||
|
|
f94495ab82 | ||
|
|
c2dce9c981 | ||
|
|
f6545cd56a | ||
|
|
5abe863ea9 | ||
|
|
963300a7c9 | ||
|
|
f45991c8a7 | ||
|
|
61d1c1c722 | ||
|
|
d59a8b5b78 | ||
|
|
cf3436dcb3 | ||
|
|
afce1abbfb | ||
|
|
5bea7c32dc | ||
|
|
cc6be7f6f4 | ||
|
|
34a94bb060 | ||
|
|
53bf214207 | ||
|
|
abf6888dd5 | ||
|
|
f116bd3588 | ||
|
|
dd21ab92ad | ||
|
|
f185137ed1 | ||
|
|
c3479b886a | ||
|
|
9df130938e | ||
|
|
16e645b268 | ||
|
|
e9b2a6212a | ||
|
|
399a022099 | ||
|
|
039d81a8fc | ||
|
|
9d1a65d6a9 | ||
|
|
b5025559ac | ||
|
|
f5faeb888b | ||
|
|
e6e4751326 | ||
|
|
9a367da4f7 | ||
|
|
01ce567b66 | ||
|
|
8099a8807b | ||
|
|
96764c4d49 | ||
|
|
0b6c02391f | ||
|
|
886e1d3a14 | ||
|
|
a82d708dd8 | ||
|
|
7aa647080f | ||
|
|
9fb8d05591 | ||
|
|
b609a8ae85 | ||
|
|
a1b7a41705 | ||
|
|
bd2c4f1161 | ||
|
|
0f7e9a1d28 | ||
|
|
32f825e779 | ||
|
|
41a440f23d | ||
|
|
706b413353 | ||
|
|
c4203f7989 | ||
|
|
0e0a6e726e | ||
|
|
a345f349bc | ||
|
|
af8e41d6f8 | ||
|
|
ad24165eef | ||
|
|
4ff56493e6 | ||
|
|
a57e37a666 | ||
|
|
06bab51ee2 | ||
|
|
affbfe9020 | ||
|
|
1e5c78e35f | ||
|
|
70a97c0db8 | ||
|
|
5ef5ebcf19 | ||
|
|
e1954e3eaf | ||
|
|
d2af2faa52 | ||
|
|
06267d74f2 | ||
|
|
047d06f16f | ||
|
|
300b9b5fae | ||
|
|
bc226dcd6f | ||
|
|
dfb44373bf | ||
|
|
dc322f8a1f | ||
|
|
41b1b0a2d5 | ||
|
|
12b45116ed | ||
|
|
fb5bc58c56 | ||
|
|
62f029af5f | ||
|
|
c0084ebfe8 | ||
|
|
e0dd4b107c | ||
|
|
5516316cd2 | ||
|
|
72ea7912eb | ||
|
|
010dd02ba8 | ||
|
|
02cc6b100c | ||
|
|
8e3e79b5fc | ||
|
|
5d606cae2f | ||
|
|
39cb8db3e1 | ||
|
|
f4f291302c | ||
|
|
a1e7618d0f | ||
|
|
b85785726c | ||
|
|
4e9772e19b | ||
|
|
b37be09d6b | ||
|
|
b42389a021 | ||
|
|
cbf9585d84 | ||
|
|
e322750265 | ||
|
|
fe90744ea3 | ||
|
|
75cc094b88 | ||
|
|
3fa91729cc | ||
|
|
ea9b8c8b27 | ||
|
|
858823d911 | ||
|
|
58c32833ed | ||
|
|
7d9b8e60cc | ||
|
|
06b2fec68c | ||
|
|
21bc70be05 | ||
|
|
9d4a32e94f | ||
|
|
476682ba8c | ||
|
|
94b0a79d94 | ||
|
|
81aa3fb786 | ||
|
|
44a69e8a73 | ||
|
|
608361ce20 | ||
|
|
8b29b9c8c2 | ||
|
|
43a333d3a6 | ||
|
|
16883df273 | ||
|
|
459402b569 | ||
|
|
318a9251bd | ||
|
|
38b0f9d21f | ||
|
|
a10dd3f184 | ||
|
|
f9286cfab9 | ||
|
|
b45b3e807c | ||
|
|
7ac9e6f23c | ||
|
|
fc934fd4d7 | ||
|
|
133dfb76fe | ||
|
|
b79b388425 | ||
|
|
5930a8a04d | ||
|
|
57db0188cc | ||
|
|
adc6afd011 | ||
|
|
be56670519 | ||
|
|
83f58f2652 | ||
|
|
49a1a77c69 | ||
|
|
c9eb111d8a | ||
|
|
575dcc1697 | ||
|
|
b96a3aa401 | ||
|
|
b7377a1080 | ||
|
|
cdeff93e3a | ||
|
|
57aa8901e2 | ||
|
|
24b04be335 | ||
|
|
9fc6185436 | ||
|
|
aa0f9a3aa7 | ||
|
|
b85d3e59fa | ||
|
|
eb7f942acb | ||
|
|
ff71377546 | ||
|
|
97a6ebfdc8 | ||
|
|
066a083c62 | ||
|
|
ec142a1031 | ||
|
|
db6146a25c | ||
|
|
5b4c132e36 | ||
|
|
e94999b07d | ||
|
|
9a2e7ca190 | ||
|
|
bad2e91039 | ||
|
|
fdbe926aa8 | ||
|
|
a69c664e38 | ||
|
|
690a3cae16 | ||
|
|
cc7862bbb8 | ||
|
|
f2a341550d | ||
|
|
65ae3c2a32 | ||
|
|
9a59e9ac28 | ||
|
|
e682056ffc | ||
|
|
b17f330b88 | ||
|
|
9d0824beb6 | ||
|
|
9c8ab50d60 | ||
|
|
d2e98df607 | ||
|
|
de4fbcdf5b | ||
|
|
3f312a9e92 | ||
|
|
28be15de56 | ||
|
|
103609794c | ||
|
|
818524694d | ||
|
|
e3e4ae5d2e | ||
|
|
c47cc25690 | ||
|
|
c2ee30f49f | ||
|
|
ea8b7c6ac9 | ||
|
|
9bc5f26b41 | ||
|
|
1e81f2a163 | ||
|
|
f6240f0dea | ||
|
|
52e7a839d6 | ||
|
|
31ec4cf615 | ||
|
|
3a9034e68f | ||
|
|
583be34815 | ||
|
|
bc77e8ce12 | ||
|
|
5f2dc0e9e0 | ||
|
|
b49a0ce7d5 | ||
|
|
5d5c8168d4 | ||
|
|
5e0f3fc81c | ||
|
|
daad4306bd | ||
|
|
cdfa8c1561 | ||
|
|
4e15036cad | ||
|
|
2780f81fbb | ||
|
|
2114189bef | ||
|
|
588d30268b | ||
|
|
399b33df48 | ||
|
|
2742ead047 | ||
|
|
dacdeb6025 | ||
|
|
29b72ca695 | ||
|
|
355e949a87 | ||
|
|
47804103d9 | ||
|
|
df6a66e580 | ||
|
|
4257031c3f | ||
|
|
8da326926b | ||
|
|
935ffc167e | ||
|
|
5d27eaab9b | ||
|
|
4a00634ac1 | ||
|
|
d6b97c93f5 | ||
|
|
c51645be35 | ||
|
|
a24184bb7a | ||
|
|
3f34667dc7 | ||
|
|
4ec2ed3fe6 | ||
|
|
488bdcf0e3 | ||
|
|
2cac22bc55 | ||
|
|
351e964e4e | ||
|
|
5e4f327edd | ||
|
|
47e5285544 | ||
|
|
3445f448ac | ||
|
|
3cf00496b3 | ||
|
|
81719d3150 | ||
|
|
d9bdc3b125 | ||
|
|
0fb8680da6 | ||
|
|
2e523bfb93 | ||
|
|
0ef5061e11 | ||
|
|
ab8b435de3 | ||
|
|
e27568fa8c | ||
|
|
41650ed043 | ||
|
|
5953080c96 | ||
|
|
754e4754eb | ||
|
|
8d0744cdb0 | ||
|
|
da29f43a2b | ||
|
|
7dc19630fe | ||
|
|
1b2bdb1e09 | ||
|
|
1b3ec9d578 | ||
|
|
282de00d80 | ||
|
|
017f9c06fb | ||
|
|
be5ea7e219 | ||
|
|
33bc349280 | ||
|
|
d1d783f24d | ||
|
|
8bedda2314 | ||
|
|
8c8b552485 | ||
|
|
6956dcb6dc | ||
|
|
57e2c5d7e7 | ||
|
|
d58339561c | ||
|
|
a6cf2c5145 | ||
|
|
1cd016d7eb | ||
|
|
c11c2a617f | ||
|
|
6031d7f830 | ||
|
|
604ac4f0e7 | ||
|
|
2438c0de91 | ||
|
|
abeb301c2b | ||
|
|
db5f795be8 | ||
|
|
fe86b57a2c | ||
|
|
4dc959f3e5 | ||
|
|
a657ceb59d | ||
|
|
26df57e5c7 | ||
|
|
09dd89a468 | ||
|
|
ce861fe366 | ||
|
|
3f4bacdbba | ||
|
|
cd3ff90e0d | ||
|
|
8f3a6047b2 | ||
|
|
6049a49114 | ||
|
|
43721515d8 | ||
|
|
5c07473e60 | ||
|
|
1524aed25e | ||
|
|
a182e0c306 | ||
|
|
a9fc89e8a5 | ||
|
|
8ea7b4ac3b | ||
|
|
d50fbccaf1 | ||
|
|
d3c850b8e7 | ||
|
|
958239fffb | ||
|
|
22dfc7e066 | ||
|
|
9a76a06f39 | ||
|
|
a607731d86 | ||
|
|
ba731ee507 | ||
|
|
e93b0a0a8d | ||
|
|
377fa5532a | ||
|
|
9cf744f435 | ||
|
|
641e344c7f | ||
|
|
722aefd1c9 | ||
|
|
f40ffdf81e | ||
|
|
647275cf46 | ||
|
|
933426a5e6 | ||
|
|
7162100415 | ||
|
|
85b6defbc8 | ||
|
|
a03f257205 | ||
|
|
2a8807161a | ||
|
|
51e340cff6 | ||
|
|
03aade0267 | ||
|
|
afdfabd71b | ||
|
|
b2131e4a06 | ||
|
|
a786caa73d | ||
|
|
43d2848c12 | ||
|
|
a123f0bd37 | ||
|
|
11afe8d723 | ||
|
|
8665997d89 | ||
|
|
4296a87e6b | ||
|
|
37b72caf37 | ||
|
|
37ee096322 | ||
|
|
a2c9ed21ff | ||
|
|
7d86b51246 | ||
|
|
b387a3a1e1 | ||
|
|
5f798583db | ||
|
|
9f8693a6a1 | ||
|
|
5ee346c266 | ||
|
|
f79b49e706 | ||
|
|
24f68835d3 | ||
|
|
92d3173077 | ||
|
|
f555da6536 | ||
|
|
71cf80b44b | ||
|
|
5be6db4689 | ||
|
|
c26c07d938 | ||
|
|
46204e4a44 | ||
|
|
fb5e85d7fa | ||
|
|
4f9e81f6c4 | ||
|
|
809f742249 | ||
|
|
b0aa80f26f | ||
|
|
c76ee06cd8 | ||
|
|
f0d3a042a3 | ||
|
|
bbad449ac6 | ||
|
|
640c139fdc | ||
|
|
0f40a0b843 | ||
|
|
1e502fb5c5 | ||
|
|
825a815dd0 | ||
|
|
00134d2faf | ||
|
|
b53a3db971 | ||
|
|
8d2ab47494 | ||
|
|
6c5ccca4cd | ||
|
|
3815570294 | ||
|
|
c46e3f979e | ||
|
|
d55ac4fe92 | ||
|
|
7950a6e0e5 | ||
|
|
9ff0cbf6ff | ||
|
|
60fedf0985 | ||
|
|
469455ef00 | ||
|
|
94116d17d8 | ||
|
|
e5f9f9df14 | ||
|
|
933877a179 | ||
|
|
f8a4d662f2 | ||
|
|
059275a37f | ||
|
|
261c6885f8 | ||
|
|
8a6c18f3c9 | ||
|
|
d2647e85a9 | ||
|
|
024dee09f5 | ||
|
|
1b55b1f1d4 | ||
|
|
a223cbb96d | ||
|
|
882584202c | ||
|
|
2dfa59ac3a | ||
|
|
d9f087fe6c | ||
|
|
4660456ad9 | ||
|
|
b2066afee4 | ||
|
|
d6ff62e19c | ||
|
|
620b3faa8f | ||
|
|
65783410b6 | ||
|
|
fac9d6d38d | ||
|
|
120502097a | ||
|
|
d8272908e9 | ||
|
|
d3268f11f9 | ||
|
|
9457c65feb | ||
|
|
75dc92ff5f | ||
|
|
030acb0f56 | ||
|
|
784f73c0fe | ||
|
|
7bc8bc9846 | ||
|
|
6e6fef8d2f | ||
|
|
83db6a68f3 | ||
|
|
569aec60a3 | ||
|
|
4d008602cd | ||
|
|
ff00be9a7c | ||
|
|
4927943595 | ||
|
|
fbf2e9e128 | ||
|
|
770832bcb9 | ||
|
|
40ca1e731d | ||
|
|
87ac100e77 | ||
|
|
05097f3307 | ||
|
|
19f0af6169 | ||
|
|
bb53a8edff | ||
|
|
b7f1639016 | ||
|
|
e57fa0c32e | ||
|
|
8729c093c8 | ||
|
|
bbe800dbac | ||
|
|
1d9cd061b1 | ||
|
|
65cceaf224 | ||
|
|
88231bc93b | ||
|
|
611323b5ce | ||
|
|
4dc102af3f | ||
|
|
3f14f75e51 | ||
|
|
3256e6c29e | ||
|
|
604954561a | ||
|
|
9f60933a6e | ||
|
|
893f142345 | ||
|
|
03b7d24216 | ||
|
|
4860f2ce21 | ||
|
|
00889551e7 | ||
|
|
bdabd5b75c | ||
|
|
ac51f66829 | ||
|
|
bc7bbb9dbc | ||
|
|
fcf9f82da0 | ||
|
|
8275200c82 | ||
|
|
85ff669b66 | ||
|
|
b16c485a2a | ||
|
|
23ad098aa9 | ||
|
|
0f2fe76027 | ||
|
|
a5d957ec06 | ||
|
|
251cbfa99e | ||
|
|
243378b035 | ||
|
|
0dd80f9d8e | ||
|
|
f380deaf86 | ||
|
|
bb5b78e94e | ||
|
|
a234894d01 | ||
|
|
4c1cbbcdfc | ||
|
|
10cf1482ab | ||
|
|
89afb5cac2 | ||
|
|
34f0f286d6 | ||
|
|
b645589ed5 | ||
|
|
d09388bc97 | ||
|
|
64a1ea9e81 | ||
|
|
0fd10be9de | ||
|
|
c214ed9546 | ||
|
|
8a7c870f1c | ||
|
|
556f12fd59 | ||
|
|
ecdf94232f | ||
|
|
c2b6e0f34a | ||
|
|
32bfad21f8 | ||
|
|
2777910d1f | ||
|
|
a49314be07 | ||
|
|
9f9a076433 | ||
|
|
4c41f82d18 | ||
|
|
18b62f9bbe | ||
|
|
b9700b760f | ||
|
|
894b150ac9 | ||
|
|
5515b8ce9d | ||
|
|
24006300e5 | ||
|
|
8cd7d590e0 | ||
|
|
38b5b242b3 | ||
|
|
9f330348ec | ||
|
|
303737c0b0 | ||
|
|
71a8eb1697 | ||
|
|
71f3351d2b | ||
|
|
b93143381f | ||
|
|
5ed0b1a6bf | ||
|
|
847b7f5e11 | ||
|
|
7568b099ee | ||
|
|
7fdd865d5c | ||
|
|
d0d4d593cf | ||
|
|
20810e98f2 | ||
|
|
97ab061ab5 | ||
|
|
edf8a0ede2 | ||
|
|
1c6bb4386f | ||
|
|
ee00a0e049 | ||
|
|
e85eb01831 | ||
|
|
e6697274f4 | ||
|
|
b3ade6159e | ||
|
|
6c0e21780c | ||
|
|
0b945ef616 | ||
|
|
41cad56991 | ||
|
|
f8283a1014 | ||
|
|
da29c05662 | ||
|
|
5bf834e14e | ||
|
|
5c353a12f2 | ||
|
|
5ec5301680 | ||
|
|
d06303c432 | ||
|
|
f268f82780 | ||
|
|
8ea785892f | ||
|
|
5f81dc0d51 | ||
|
|
6ca654294c | ||
|
|
96d0beeaaf | ||
|
|
3f913c0c3f | ||
|
|
31a8e07cec | ||
|
|
fc6df01b8e | ||
|
|
bcefc34287 | ||
|
|
e6fe4f0e05 | ||
|
|
e20e693d70 | ||
|
|
686fab7fec | ||
|
|
1ee6e1c7fa | ||
|
|
444b65ecac | ||
|
|
a12124512e | ||
|
|
e5e1308852 | ||
|
|
7beae2beac | ||
|
|
d9345c99e3 | ||
|
|
d222b935e6 | ||
|
|
8577114e2e | ||
|
|
e3a120b1d8 | ||
|
|
00c12d9a25 | ||
|
|
33353417df | ||
|
|
b667cef4a8 | ||
|
|
5134ca37a7 | ||
|
|
ae23af7061 | ||
|
|
9686a2f16b | ||
|
|
8f658108f2 | ||
|
|
3db5c36e60 | ||
|
|
e803969442 | ||
|
|
5be580c105 | ||
|
|
a0ae6dd32f | ||
|
|
1bbd8f038b | ||
|
|
e7bde586d8 | ||
|
|
5540c9b9aa | ||
|
|
02c81851a8 | ||
|
|
01dad77d44 | ||
|
|
018ce4e9f0 | ||
|
|
0e89b744ec | ||
|
|
6aa12887b3 | ||
|
|
e5d6e9a21a | ||
|
|
3f9d361d4f | ||
|
|
0565dd3df1 | ||
|
|
c8e8c2dc32 | ||
|
|
9c5c420483 | ||
|
|
911425c1c1 | ||
|
|
3d0f85c188 | ||
|
|
ba43a01669 | ||
|
|
f5833da4cd | ||
|
|
67882a9dff | ||
|
|
c03bd896d8 | ||
|
|
8ee3c93c84 | ||
|
|
159a30fdc7 | ||
|
|
56ca350ed2 | ||
|
|
781a9ab627 | ||
|
|
d0ecaed401 | ||
|
|
f28d11bf90 | ||
|
|
7091585dbe | ||
|
|
8d55e72dfa | ||
|
|
cd87a1436b | ||
|
|
27b23f6a00 | ||
|
|
3540859074 | ||
|
|
1b41aba841 | ||
|
|
f787828712 | ||
|
|
471dac48be | ||
|
|
2675367400 | ||
|
|
f120c1dedb | ||
|
|
a54dc192d7 | ||
|
|
0850db530f | ||
|
|
0a1a579714 | ||
|
|
af3a766304 | ||
|
|
d7b1ffd014 | ||
|
|
0a6028e116 | ||
|
|
61173d7e70 | ||
|
|
3ae30c9028 | ||
|
|
fa62cdf127 | ||
|
|
d715ac9e53 | ||
|
|
8fa0d9c4fc | ||
|
|
5d2304b18f | ||
|
|
a886a58421 | ||
|
|
2f5549e0c8 | ||
|
|
915514e37b | ||
|
|
e776d64b6f | ||
|
|
d3c41395bc | ||
|
|
7cc55c078f | ||
|
|
a1edbb5972 | ||
|
|
6fe6a603d7 | ||
|
|
2a9a9884cf | ||
|
|
a5fedca016 | ||
|
|
98caad1ff7 | ||
|
|
b38485e169 | ||
|
|
b92e0a6d0f | ||
|
|
8b366ed5c2 | ||
|
|
7b1bf35b8c | ||
|
|
c631589306 | ||
|
|
138233e97d | ||
|
|
f09e213202 | ||
|
|
960a51853e | ||
|
|
906ac8f987 | ||
|
|
6bd8dea088 | ||
|
|
9f1a1f0f5e | ||
|
|
c4952fe81e | ||
|
|
abad42fcd5 | ||
|
|
fc87fa0630 | ||
|
|
59c5c6276d | ||
|
|
9d81e923b9 | ||
|
|
4d402c1223 | ||
|
|
ce18509697 | ||
|
|
e9990767fa | ||
|
|
cf4896bb3a | ||
|
|
adcac7f7b4 | ||
|
|
4bcef1bc67 | ||
|
|
b1b5ce211e | ||
|
|
150c69bb1d | ||
|
|
6006254716 | ||
|
|
2658cdfa5d | ||
|
|
eb79239e6e | ||
|
|
5c0c5a8446 | ||
|
|
fbc2021ed8 | ||
|
|
564413df01 | ||
|
|
5f32cae938 | ||
|
|
198e4fe520 | ||
|
|
3c2febf8b4 | ||
|
|
0d4c10bd45 | ||
|
|
d4e4d7e4b4 | ||
|
|
943bf8c69c | ||
|
|
be732210a4 | ||
|
|
d60ef39f82 | ||
|
|
23121b3528 | ||
|
|
135dce436e | ||
|
|
f38a3ac6cd | ||
|
|
a1382e107f | ||
|
|
3367c879bd | ||
|
|
ba8005740a | ||
|
|
0d225965ff | ||
|
|
c22aff33ce | ||
|
|
eb73d5c372 | ||
|
|
2bc9995b61 | ||
|
|
7188823ade | ||
|
|
10f4a22192 | ||
|
|
07111fa952 | ||
|
|
adf522454e | ||
|
|
d7b89f5a7c | ||
|
|
39f8662beb | ||
|
|
bf157fd794 | ||
|
|
4c62c19230 | ||
|
|
6f0f9ec1ba | ||
|
|
3c8da80fa4 | ||
|
|
a9f18abb41 | ||
|
|
4a1590c0bd | ||
|
|
2b89f6fb71 | ||
|
|
e5fd75cdd2 | ||
|
|
59eb3ab749 | ||
|
|
d67fd59f65 | ||
|
|
6a90de738c | ||
|
|
cf757831b6 | ||
|
|
c48b9f8edd | ||
|
|
dfb3451000 | ||
|
|
419a40beac | ||
|
|
15032dd3b9 | ||
|
|
649152c97a | ||
|
|
c387dca4fb | ||
|
|
d9b0e6b234 | ||
|
|
8372d751fd | ||
|
|
0a752fb61f | ||
|
|
16be7e708f | ||
|
|
09e4c32832 | ||
|
|
7298289f3a | ||
|
|
136cf5be52 | ||
|
|
d5023bc195 | ||
|
|
e94a551ec2 | ||
|
|
127cf77db4 | ||
|
|
4c2d4d11ef | ||
|
|
89690b1e97 | ||
|
|
ddbb39bb22 | ||
|
|
a775f48cf0 | ||
|
|
8e10610173 | ||
|
|
9356994d6a | ||
|
|
36b8abe601 | ||
|
|
c570186a6f | ||
|
|
db686b67ec | ||
|
|
40e04ab639 | ||
|
|
1891d7cde7 | ||
|
|
98d224a5ec | ||
|
|
f62bd58fae | ||
|
|
1ff4548a2c | ||
|
|
ba36b3f63b | ||
|
|
f353236c8a | ||
|
|
1bcc4d3991 | ||
|
|
b06ae9ea47 | ||
|
|
7fd326eb21 | ||
|
|
2a8fccc6cd | ||
|
|
1295a89911 | ||
|
|
3e2f205045 | ||
|
|
eff5421ce4 | ||
|
|
857ac806ae | ||
|
|
0f7ae16eb6 | ||
|
|
c31a756517 | ||
|
|
e09453d6e4 | ||
|
|
9177011abd | ||
|
|
9e45d5c8db | ||
|
|
fca687f5fb | ||
|
|
1223955cba | ||
|
|
636c896b90 | ||
|
|
2fa93fd694 | ||
|
|
7893e6461b | ||
|
|
1bad5b3179 | ||
|
|
a0330d439c | ||
|
|
3d831c1db7 | ||
|
|
613e294f15 | ||
|
|
013d37f23a | ||
|
|
a38f293246 | ||
|
|
f9c39fbc3b | ||
|
|
a85ff52115 | ||
|
|
c359a24017 | ||
|
|
7816430fd7 | ||
|
|
3e4ed83112 | ||
|
|
617d7be300 | ||
|
|
108dbb8efd | ||
|
|
40fdda3f5a | ||
|
|
c451919511 | ||
|
|
4f6b686ed7 | ||
|
|
455993b164 | ||
|
|
ec47a07195 | ||
|
|
9cbfc37774 | ||
|
|
1928548346 | ||
|
|
bc90faa69f | ||
|
|
29563434df | ||
|
|
eca1789ad1 | ||
|
|
7c60ff0201 | ||
|
|
a930b4fdca | ||
|
|
9c6125deef | ||
|
|
26d390e2bd | ||
|
|
363ca3febb | ||
|
|
6c648ef0d8 | ||
|
|
3d9943c7b3 | ||
|
|
a48b1bcbae | ||
|
|
1483ee4c8c | ||
|
|
ab505fddcd | ||
|
|
62158a0c06 | ||
|
|
83d0c4b084 | ||
|
|
95b237bdc5 | ||
|
|
2e1db8f69b | ||
|
|
66a182e743 | ||
|
|
28580b09c3 | ||
|
|
72bcad4810 | ||
|
|
6862fe3551 | ||
|
|
71b02e3bcd | ||
|
|
af88064c2a | ||
|
|
658a16fb78 | ||
|
|
bc2c2a5189 | ||
|
|
7ea4d5a957 | ||
|
|
1db0fbdedc | ||
|
|
3a25697349 | ||
|
|
4c80d6bc34 | ||
|
|
49f342ad43 | ||
|
|
e849321f62 | ||
|
|
6ded5c5cfe | ||
|
|
191f737d5f | ||
|
|
4a03d1120f | ||
|
|
5bbc94188c | ||
|
|
bf9f634613 | ||
|
|
73452f7b10 | ||
|
|
eb92001626 | ||
|
|
5b5fadce77 | ||
|
|
87f706aa1e | ||
|
|
4b36f0e211 | ||
|
|
b575f45c11 | ||
|
|
3900d11454 | ||
|
|
3434f5e601 | ||
|
|
8381581821 | ||
|
|
e7ef4dbc4f | ||
|
|
42778b5a91 | ||
|
|
ac89d49bea | ||
|
|
9e43ed4293 | ||
|
|
d37b09b6bc | ||
|
|
5e60a65fc6 | ||
|
|
92125c51b6 | ||
|
|
4301fc6b58 | ||
|
|
da1223aa57 | ||
|
|
4330130017 | ||
|
|
db8528c037 | ||
|
|
5cad0db347 | ||
|
|
658f72fe84 | ||
|
|
ee375a0224 | ||
|
|
e3a912a46f | ||
|
|
b94f3895db | ||
|
|
4404df3903 | ||
|
|
a502d5215a | ||
|
|
b64143d314 | ||
|
|
0148112676 | ||
|
|
ceb838d559 | ||
|
|
a30aff9454 | ||
|
|
47c756b243 | ||
|
|
afe84768a5 | ||
|
|
6da644b669 | ||
|
|
a02941cb99 | ||
|
|
4c8f02e35d | ||
|
|
a7d2fddd07 | ||
|
|
01a3ce70cb | ||
|
|
92adcf107c | ||
|
|
cbf7b70a8d | ||
|
|
579c230969 | ||
|
|
561843d006 | ||
|
|
f83ae97fbd | ||
|
|
35afec5884 | ||
|
|
ae41dba29f | ||
|
|
ebf808b0f9 | ||
|
|
8a284f1726 | ||
|
|
530320ca6e | ||
|
|
e71bdcfdd6 | ||
|
|
fc49ca3438 | ||
|
|
a688d38165 | ||
|
|
a3379dceec | ||
|
|
f5ad24f352 | ||
|
|
ae8834a6f2 | ||
|
|
ccd6863ad4 | ||
|
|
57ad86a222 | ||
|
|
d8d32ec1f5 | ||
|
|
56d06482fe | ||
|
|
309f44a079 | ||
|
|
9401f94b78 | ||
|
|
fab12da4e7 | ||
|
|
4bd49b0bf6 | ||
|
|
a972f76224 | ||
|
|
f28cd3f709 | ||
|
|
c87d6ec182 | ||
|
|
d4e781d48f | ||
|
|
6e5efa1e09 | ||
|
|
326489ff60 | ||
|
|
62a7c160ab | ||
|
|
13937ac7f9 | ||
|
|
9128daf883 | ||
|
|
6caa019231 | ||
|
|
4055960757 | ||
|
|
88fa8bb8b2 | ||
|
|
269fcbb091 | ||
|
|
a97ac8fadf | ||
|
|
93cbef4aeb | ||
|
|
929f30c58b | ||
|
|
e59f5b1ba4 | ||
|
|
20c8b0cec9 | ||
|
|
9c0dc4e865 | ||
|
|
28e68a2a3c | ||
|
|
a39d3f28e2 | ||
|
|
2ba7eb83d2 | ||
|
|
d8129ba59f | ||
|
|
360c7e051e | ||
|
|
9a3425cfcd | ||
|
|
f5b0af521c | ||
|
|
658126b7bc | ||
|
|
196d168b65 | ||
|
|
d71927a006 | ||
|
|
8e8017531a | ||
|
|
8be2660994 | ||
|
|
a34bae8905 | ||
|
|
01aaae9774 | ||
|
|
9e7a02e2b2 | ||
|
|
470d4f1dec | ||
|
|
65c2c6bb78 | ||
|
|
62b68a8892 | ||
|
|
c4c9ee4427 | ||
|
|
44790ad1e1 | ||
|
|
9e3727a037 | ||
|
|
6721909257 | ||
|
|
26ffea9bed | ||
|
|
d8fb318253 | ||
|
|
bb5dde23b8 | ||
|
|
94b2b0c208 | ||
|
|
e7a9f311c7 | ||
|
|
8156680b70 | ||
|
|
4d74e57bb6 | ||
|
|
8ba3bf1e5f | ||
|
|
e16b3a25b3 | ||
|
|
972d5ff493 | ||
|
|
059f4e0748 | ||
|
|
9ae501c7ca | ||
|
|
dffac4069d | ||
|
|
5527b3a852 | ||
|
|
537e28a0ce | ||
|
|
5382968864 | ||
|
|
994474aead | ||
|
|
f38c1b3106 | ||
|
|
ff3b5cc3c4 | ||
|
|
c9242b84f8 | ||
|
|
94c8ff5e1b | ||
|
|
e2e5a7715c | ||
|
|
7fc6628934 | ||
|
|
f834803946 | ||
|
|
172aa7e47c | ||
|
|
6ea5b671e7 | ||
|
|
80f373bc59 | ||
|
|
7de1908f48 | ||
|
|
d8ff73b702 | ||
|
|
a0880c58a9 | ||
|
|
eab17ce9fb |
163
.argo-ci/ci.yaml
@@ -1,163 +0,0 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Workflow
|
||||
metadata:
|
||||
generateName: argo-cd-ci-
|
||||
spec:
|
||||
entrypoint: argo-cd-ci
|
||||
arguments:
|
||||
parameters:
|
||||
- name: revision
|
||||
value: master
|
||||
- name: repo
|
||||
value: https://github.com/argoproj/argo-cd.git
|
||||
volumes:
|
||||
- name: k3setc
|
||||
emptyDir: {}
|
||||
- name: k3svar
|
||||
emptyDir: {}
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
|
||||
templates:
|
||||
- name: argo-cd-ci
|
||||
steps:
|
||||
- - name: build-e2e
|
||||
template: build-e2e
|
||||
|
||||
- name: test
|
||||
template: ci-builder
|
||||
arguments:
|
||||
parameters:
|
||||
- name: cmd
|
||||
value: "dep ensure && make lint test && bash <(curl -s https://codecov.io/bash) -f coverage.out"
|
||||
|
||||
# The step builds argo cd image, deploy argo cd components into throw-away kubernetes cluster provisioned using k3s and run e2e tests against it.
|
||||
- name: build-e2e
|
||||
inputs:
|
||||
artifacts:
|
||||
- name: code
|
||||
path: /go/src/github.com/argoproj/argo-cd
|
||||
git:
|
||||
repo: "{{workflow.parameters.repo}}"
|
||||
revision: "{{workflow.parameters.revision}}"
|
||||
container:
|
||||
image: argoproj/argo-cd-ci-builder:v0.13.1
|
||||
imagePullPolicy: Always
|
||||
command: [sh, -c]
|
||||
# Main contains build argocd image. The image is saved it into k3s agent images directory so it could be preloaded by the k3s cluster.
|
||||
args: ["
|
||||
dep ensure && until docker ps; do sleep 3; done && \
|
||||
make image DEV_IMAGE=true && mkdir -p /var/lib/rancher/k3s/agent/images && \
|
||||
docker save argocd:latest > /var/lib/rancher/k3s/agent/images/argocd.tar && \
|
||||
touch /var/lib/rancher/k3s/ready && until ls /etc/rancher/k3s/k3s.yaml; do sleep 3; done && \
|
||||
kubectl create ns argocd-e2e && kustomize build ./test/manifests/ci | kubectl apply -n argocd-e2e -f - && \
|
||||
kubectl rollout status deployment -n argocd-e2e argocd-application-controller && kubectl rollout status deployment -n argocd-e2e argocd-server && \
|
||||
git config --global user.email \"test@example.com\" && \
|
||||
export ARGOCD_SERVER=$(kubectl get service argocd-server -o=jsonpath={.spec.clusterIP} -n argocd-e2e):443 && make test-e2e"
|
||||
]
|
||||
workingDir: /go/src/github.com/argoproj/argo-cd
|
||||
env:
|
||||
- name: USER
|
||||
value: argocd
|
||||
- name: DOCKER_HOST
|
||||
value: 127.0.0.1
|
||||
- name: DOCKER_BUILDKIT
|
||||
value: "1"
|
||||
- name: KUBECONFIG
|
||||
value: /etc/rancher/k3s/k3s.yaml
|
||||
volumeMounts:
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: k3setc
|
||||
mountPath: /etc/rancher/k3s
|
||||
- name: k3svar
|
||||
mountPath: /var/lib/rancher/k3s
|
||||
sidecars:
|
||||
- name: dind
|
||||
image: docker:18.09-dind
|
||||
securityContext:
|
||||
privileged: true
|
||||
resources:
|
||||
requests:
|
||||
memory: 2048Mi
|
||||
cpu: 500m
|
||||
mirrorVolumeMounts: true
|
||||
|
||||
# Steps waits for file /var/lib/rancher/k3s/ready which indicates that all required images are ready, then starts the cluster.
|
||||
- name: k3s
|
||||
image: rancher/k3s:v0.3.0-rc1
|
||||
imagePullPolicy: Always
|
||||
command: [sh, -c]
|
||||
args: ["until ls /var/lib/rancher/k3s/ready; do sleep 3; done && k3s server || true"]
|
||||
securityContext:
|
||||
privileged: true
|
||||
volumeMounts:
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: k3setc
|
||||
mountPath: /etc/rancher/k3s
|
||||
- name: k3svar
|
||||
mountPath: /var/lib/rancher/k3s
|
||||
|
||||
- name: ci-builder
|
||||
inputs:
|
||||
parameters:
|
||||
- name: cmd
|
||||
artifacts:
|
||||
- name: code
|
||||
path: /go/src/github.com/argoproj/argo-cd
|
||||
git:
|
||||
repo: "{{workflow.parameters.repo}}"
|
||||
revision: "{{workflow.parameters.revision}}"
|
||||
container:
|
||||
image: argoproj/argo-cd-ci-builder:v0.13.1
|
||||
imagePullPolicy: Always
|
||||
command: [bash, -c]
|
||||
args: ["{{inputs.parameters.cmd}}"]
|
||||
workingDir: /go/src/github.com/argoproj/argo-cd
|
||||
env:
|
||||
- name: CODECOV_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: codecov-token
|
||||
key: codecov-token
|
||||
resources:
|
||||
requests:
|
||||
memory: 1024Mi
|
||||
cpu: 200m
|
||||
archiveLocation:
|
||||
archiveLogs: true
|
||||
|
||||
- name: ci-dind
|
||||
inputs:
|
||||
parameters:
|
||||
- name: cmd
|
||||
artifacts:
|
||||
- name: code
|
||||
path: /go/src/github.com/argoproj/argo-cd
|
||||
git:
|
||||
repo: "{{workflow.parameters.repo}}"
|
||||
revision: "{{workflow.parameters.revision}}"
|
||||
container:
|
||||
image: argoproj/argo-cd-ci-builder:v0.13.1
|
||||
imagePullPolicy: Always
|
||||
command: [sh, -c]
|
||||
args: ["until docker ps; do sleep 3; done && {{inputs.parameters.cmd}}"]
|
||||
workingDir: /go/src/github.com/argoproj/argo-cd
|
||||
env:
|
||||
- name: DOCKER_HOST
|
||||
value: 127.0.0.1
|
||||
- name: DOCKER_BUILDKIT
|
||||
value: "1"
|
||||
resources:
|
||||
requests:
|
||||
memory: 1024Mi
|
||||
cpu: 200m
|
||||
sidecars:
|
||||
- name: dind
|
||||
image: docker:18.09-dind
|
||||
securityContext:
|
||||
privileged: true
|
||||
mirrorVolumeMounts: true
|
||||
archiveLocation:
|
||||
archiveLogs: true
|
||||
365
.circleci/config.yml
Normal file
@@ -0,0 +1,365 @@
|
||||
version: 2.1
|
||||
commands:
|
||||
prepare_environment:
|
||||
steps:
|
||||
- run:
|
||||
name: Configure environment
|
||||
command: |
|
||||
set -x
|
||||
echo "export GOCACHE=/tmp/go-build-cache" | tee -a $BASH_ENV
|
||||
echo "export ARGOCD_TEST_VERBOSE=true" | tee -a $BASH_ENV
|
||||
echo "export ARGOCD_TEST_PARALLELISM=8" | tee -a $BASH_ENV
|
||||
echo "export ARGOCD_SONAR_VERSION=4.2.0.1873" | tee -a $BASH_ENV
|
||||
configure_git:
|
||||
steps:
|
||||
- run:
|
||||
name: Configure Git
|
||||
command: |
|
||||
set -x
|
||||
# must be configured for tests to run
|
||||
git config --global user.email you@example.com
|
||||
git config --global user.name "Your Name"
|
||||
echo "export PATH=/home/circleci/.go_workspace/src/github.com/argoproj/argo-cd/hack:\$PATH" | tee -a $BASH_ENV
|
||||
echo "export GIT_ASKPASS=git-ask-pass.sh" | tee -a $BASH_ENV
|
||||
restore_vendor:
|
||||
steps:
|
||||
- restore_cache:
|
||||
name: Restore vendor cache
|
||||
keys:
|
||||
- vendor-v2-{{ checksum "Gopkg.lock" }}
|
||||
save_vendor:
|
||||
steps:
|
||||
- save_cache:
|
||||
name: Save vendor cache
|
||||
key: vendor-v2-{{ checksum "Gopkg.lock" }}
|
||||
paths:
|
||||
- ./vendor
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- vendor
|
||||
save_coverage_info:
|
||||
steps:
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- coverage.out
|
||||
save_node_modules:
|
||||
steps:
|
||||
- persist_to_workspace:
|
||||
root: ~/argo-cd
|
||||
paths:
|
||||
- ui/node_modules
|
||||
attach_vendor:
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: .
|
||||
install_golang:
|
||||
steps:
|
||||
- run:
|
||||
name: Install Golang v1.14.1
|
||||
command: |
|
||||
go get golang.org/dl/go1.14.1
|
||||
[ -e /home/circleci/sdk/go1.14.1 ] || go1.14.1 download
|
||||
go env
|
||||
echo "export GOPATH=/home/circleci/.go_workspace" | tee -a $BASH_ENV
|
||||
echo "export PATH=/home/circleci/sdk/go1.14.1/bin:\$PATH" | tee -a $BASH_ENV
|
||||
save_go_cache:
|
||||
steps:
|
||||
- save_cache:
|
||||
key: go-v2-{{ .Branch }}-{{ .Environment.CIRCLE_JOB }}
|
||||
# https://circleci.com/docs/2.0/language-go/
|
||||
paths:
|
||||
- /home/circleci/.cache/go-build
|
||||
- /home/circleci/sdk/go1.14.1
|
||||
restore_go_cache:
|
||||
steps:
|
||||
- restore_cache:
|
||||
keys:
|
||||
- go-v2-{{ .Branch }}-{{ .Environment.CIRCLE_JOB }}
|
||||
|
||||
save_go_cache_docker:
|
||||
steps:
|
||||
- save_cache:
|
||||
name: Save Go build cache
|
||||
key: go-docker-v2-{{ .Branch }}-{{ .Environment.CIRCLE_WORKFLOW_ID }}
|
||||
# https://circleci.com/docs/2.0/language-go/
|
||||
paths:
|
||||
- /tmp/go-build-cache
|
||||
restore_go_cache_docker:
|
||||
steps:
|
||||
- restore_cache:
|
||||
name: Restore Go build cache
|
||||
keys:
|
||||
- go-docker-v2-{{ .Branch }}-{{ .Environment.CIRCLE_WORKFLOW_ID }}
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: argoproj/argocd-test-tools:v0.5.0
|
||||
working_directory: /go/src/github.com/argoproj/argo-cd
|
||||
steps:
|
||||
- prepare_environment
|
||||
- checkout
|
||||
- restore_vendor
|
||||
- run:
|
||||
name: Ensuring Gopkg.lock is up-to-date
|
||||
command: make dep-check-local
|
||||
- run:
|
||||
name: Syncing vendor dependencies
|
||||
command: dep ensure -v
|
||||
- run: make build-local
|
||||
- run: chmod -R 777 vendor
|
||||
- run: chmod -R 777 ${GOCACHE}
|
||||
- save_vendor
|
||||
- save_go_cache_docker
|
||||
|
||||
codegen:
|
||||
docker:
|
||||
- image: argoproj/argocd-test-tools:v0.5.0
|
||||
working_directory: /go/src/github.com/argoproj/argo-cd
|
||||
steps:
|
||||
- prepare_environment
|
||||
- checkout
|
||||
- restore_vendor
|
||||
- run: helm2 init --client-only
|
||||
- run: make codegen-local
|
||||
- run:
|
||||
name: Check nothing has changed
|
||||
command: |
|
||||
set -xo pipefail
|
||||
# This makes sure you ran `make pre-commit` before you pushed.
|
||||
# We exclude the Swagger resources; CircleCI doesn't generate them correctly.
|
||||
# When this fails, it will, create a patch file you can apply locally to fix it.
|
||||
# To troubleshoot builds: https://argoproj.github.io/argo-cd/developer-guide/ci/
|
||||
git diff --exit-code -- . ':!Gopkg.lock' ':!assets/swagger.json' | tee codegen.patch
|
||||
- store_artifacts:
|
||||
path: codegen.patch
|
||||
destination: .
|
||||
test:
|
||||
working_directory: /go/src/github.com/argoproj/argo-cd
|
||||
docker:
|
||||
- image: argoproj/argocd-test-tools:v0.5.0
|
||||
steps:
|
||||
- prepare_environment
|
||||
- checkout
|
||||
- configure_git
|
||||
- restore_vendor
|
||||
- run: dep ensure -v
|
||||
- restore_go_cache_docker
|
||||
- run: make test-local
|
||||
- run:
|
||||
name: Uploading code coverage
|
||||
command: bash <(curl -s https://codecov.io/bash) -f coverage.out
|
||||
- run:
|
||||
name: Output of test-results
|
||||
command: |
|
||||
ls -l test-results || true
|
||||
cat test-results/junit.xml || true
|
||||
- save_coverage_info
|
||||
- store_test_results:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
destination: .
|
||||
|
||||
lint:
|
||||
working_directory: /go/src/github.com/argoproj/argo-cd
|
||||
docker:
|
||||
- image: argoproj/argocd-test-tools:v0.5.0
|
||||
steps:
|
||||
- prepare_environment
|
||||
- checkout
|
||||
- configure_git
|
||||
- restore_vendor
|
||||
- run: dep ensure -v
|
||||
- restore_go_cache_docker
|
||||
- run:
|
||||
name: Run golangci-lint
|
||||
command: ARGOCD_LINT_GOGC=10 make lint-local
|
||||
- run:
|
||||
name: Check that nothing has changed
|
||||
command: |
|
||||
gDiff=$(git diff)
|
||||
if test "$gDiff" != ""; then
|
||||
echo
|
||||
echo "###############################################################################"
|
||||
echo "golangci-lint has made automatic corrections to your code. Please check below"
|
||||
echo "diff output and commit this to your local branch, or run make lint locally."
|
||||
echo "###############################################################################"
|
||||
echo
|
||||
git diff
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sonarcloud:
|
||||
working_directory: /go/src/github.com/argoproj/argo-cd
|
||||
docker:
|
||||
- image: argoproj/argocd-test-tools:v0.5.0
|
||||
environment:
|
||||
NODE_MODULES: /go/src/github.com/argoproj/argo-cd/ui/node_modules
|
||||
steps:
|
||||
- prepare_environment
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
command: mkdir -p /tmp/cache/scanner
|
||||
name: Create cache directory if it doesn't exist
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-sonarcloud-scanner-4.2.0.1873
|
||||
- run:
|
||||
command: |
|
||||
set -e
|
||||
VERSION=4.2.0.1873
|
||||
SONAR_TOKEN=$SONAR_TOKEN
|
||||
SCANNER_DIRECTORY=/tmp/cache/scanner
|
||||
export SONAR_USER_HOME=$SCANNER_DIRECTORY/.sonar
|
||||
OS="linux"
|
||||
echo $SONAR_USER_HOME
|
||||
|
||||
if [[ ! -x "$SCANNER_DIRECTORY/sonar-scanner-$VERSION-$OS/bin/sonar-scanner" ]]; then
|
||||
curl -Ol https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$VERSION-$OS.zip
|
||||
unzip -qq -o sonar-scanner-cli-$VERSION-$OS.zip -d $SCANNER_DIRECTORY
|
||||
fi
|
||||
|
||||
chmod +x $SCANNER_DIRECTORY/sonar-scanner-$VERSION-$OS/bin/sonar-scanner
|
||||
chmod +x $SCANNER_DIRECTORY/sonar-scanner-$VERSION-$OS/jre/bin/java
|
||||
|
||||
# Workaround for a possible bug in CircleCI
|
||||
if ! echo $CIRCLE_PULL_REQUEST | grep https://github.com/argoproj; then
|
||||
unset CIRCLE_PULL_REQUEST
|
||||
unset CIRCLE_PULL_REQUESTS
|
||||
fi
|
||||
|
||||
# Explicitly set NODE_MODULES
|
||||
export NODE_MODULES=/go/src/github.com/argoproj/argo-cd/ui/node_modules
|
||||
export NODE_PATH=/go/src/github.com/argoproj/argo-cd/ui/node_modules
|
||||
|
||||
$SCANNER_DIRECTORY/sonar-scanner-$VERSION-$OS/bin/sonar-scanner
|
||||
name: SonarCloud
|
||||
- save_cache:
|
||||
key: v1-sonarcloud-scanner-4.2.0.1873
|
||||
paths:
|
||||
- /tmp/cache/scanner
|
||||
|
||||
e2e:
|
||||
working_directory: /home/circleci/.go_workspace/src/github.com/argoproj/argo-cd
|
||||
machine:
|
||||
image: ubuntu-1604:201903-01
|
||||
environment:
|
||||
ARGOCD_FAKE_IN_CLUSTER: "true"
|
||||
ARGOCD_SSH_DATA_PATH: "/tmp/argo-e2e/app/config/ssh"
|
||||
ARGOCD_TLS_DATA_PATH: "/tmp/argo-e2e/app/config/tls"
|
||||
ARGOCD_E2E_K3S: "true"
|
||||
steps:
|
||||
- run:
|
||||
name: Install and start K3S v0.5.0
|
||||
command: |
|
||||
curl -sfL https://get.k3s.io | sh -
|
||||
sudo chmod -R a+rw /etc/rancher/k3s
|
||||
kubectl version
|
||||
environment:
|
||||
INSTALL_K3S_EXEC: --docker
|
||||
INSTALL_K3S_VERSION: v0.5.0
|
||||
- prepare_environment
|
||||
- restore_go_cache_docker
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys: [e2e-dl-v2]
|
||||
- run: sudo ./hack/install.sh dep-linux
|
||||
- save_cache:
|
||||
key: e2e-dl-v2
|
||||
paths: [/tmp/dl]
|
||||
- attach_vendor
|
||||
- run: dep ensure -v
|
||||
- run:
|
||||
name: Update kubectl configuration for container
|
||||
command: |
|
||||
ipaddr=$(ifconfig $IFACE |grep "inet " | awk '{print $2}')
|
||||
if echo $ipaddr | grep -q 'addr:'; then
|
||||
ipaddr=$(echo $ipaddr | awk -F ':' '{print $2}')
|
||||
fi
|
||||
test -d $HOME/.kube || mkdir -p $HOME/.kube
|
||||
kubectl config view --raw | sed -e "s/127.0.0.1:6443/${ipaddr}:6443/g" -e "s/localhost:6443/${ipaddr}:6443/g" > $HOME/.kube/config
|
||||
environment:
|
||||
IFACE: ens4
|
||||
- run:
|
||||
name: Start E2E test server
|
||||
command: make start-e2e
|
||||
background: true
|
||||
environment:
|
||||
DOCKER_SRCDIR: /home/circleci/.go_workspace/src
|
||||
ARGOCD_E2E_TEST: "true"
|
||||
ARGOCD_IN_CI: "true"
|
||||
- run:
|
||||
name: Wait for API server to become available
|
||||
command: |
|
||||
count=1
|
||||
until curl -v http://localhost:8080/healthz; do
|
||||
sleep 10;
|
||||
if test $count -ge 60; then
|
||||
echo "Timeout"
|
||||
exit 1
|
||||
fi
|
||||
count=$((count+1))
|
||||
done
|
||||
- run:
|
||||
name: Run E2E tests
|
||||
command: |
|
||||
make test-e2e
|
||||
environment:
|
||||
ARGOCD_OPTS: "--plaintext"
|
||||
ARGOCD_E2E_K3S: "true"
|
||||
IFACE: ens4
|
||||
DOCKER_SRCDIR: /home/circleci/.go_workspace/src
|
||||
- store_test_results:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
destination: .
|
||||
ui:
|
||||
docker:
|
||||
- image: node:11.15.0
|
||||
working_directory: ~/argo-cd/ui
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/argo-cd/
|
||||
- restore_cache:
|
||||
keys:
|
||||
- yarn-packages-v4-{{ checksum "yarn.lock" }}
|
||||
- run: yarn install --frozen-lockfile --ignore-optional --non-interactive
|
||||
- save_cache:
|
||||
key: yarn-packages-v4-{{ checksum "yarn.lock" }}
|
||||
paths: [~/.cache/yarn, node_modules]
|
||||
- run: yarn test
|
||||
- run: ./node_modules/.bin/codecov -p ..
|
||||
- run: NODE_ENV='production' yarn build
|
||||
- run: yarn lint
|
||||
- save_node_modules
|
||||
|
||||
orbs:
|
||||
sonarcloud: sonarsource/sonarcloud@1.0.1
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
workflow:
|
||||
jobs:
|
||||
- build
|
||||
- test:
|
||||
requires:
|
||||
- build
|
||||
- codegen:
|
||||
requires:
|
||||
- build
|
||||
- ui:
|
||||
requires:
|
||||
- build
|
||||
- sonarcloud:
|
||||
context: SonarCloud
|
||||
requires:
|
||||
- test
|
||||
- ui
|
||||
- e2e:
|
||||
requires:
|
||||
- build
|
||||
14
.codecov.yml
@@ -1,7 +1,17 @@
|
||||
ignore:
|
||||
- "**/*.pb.go"
|
||||
- "**/*.pb.gw.go"
|
||||
- "**/*generated.go"
|
||||
- "**/*generated.deepcopy.go"
|
||||
- "**/*_test.go"
|
||||
- "pkg/apis/.*"
|
||||
- "pkg/apis/client/.*"
|
||||
- "pkg/client/.*"
|
||||
- "test/.*"
|
||||
- "vendor/.*"
|
||||
coverage:
|
||||
status:
|
||||
# we've found this not to be useful
|
||||
patch: off
|
||||
project:
|
||||
default:
|
||||
# allow test coverage to drop by 2%, assume that it's typically due to CI problems
|
||||
threshold: 2
|
||||
@@ -10,3 +10,4 @@ dist/
|
||||
cmd/**/debug
|
||||
debug.test
|
||||
coverage.out
|
||||
ui/node_modules/
|
||||
|
||||
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -4,24 +4,41 @@ about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
If you are trying to resolve an environment-specific issue or have a one-off question about the edge case that does not require a feature then please consider asking a
|
||||
question in argocd slack [channel](https://argoproj.github.io/community/join-slack).
|
||||
|
||||
Checklist:
|
||||
|
||||
* [ ] I've searched in the docs and FAQ for my answer: http://bit.ly/argocd-faq.
|
||||
* [ ] I've included steps to reproduce the bug.
|
||||
* [ ] I've pasted the output of `argocd version`.
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
A list of the steps required to reproduce the issue. Best of all, give us the URL to a repository that exhibits this issue.
|
||||
|
||||
**Expected behavior**
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
**Version**
|
||||
|
||||
```shell
|
||||
Paste the output from `argocd version` here.
|
||||
```
|
||||
|
||||
**Logs**
|
||||
|
||||
```
|
||||
Paste any relevant application logs here.
|
||||
```
|
||||
|
||||
18
.github/ISSUE_TEMPLATE/enhancement_proposal.md
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
name: Enhancement proposal
|
||||
about: Propose an enhancement for this project
|
||||
title: ''
|
||||
labels: 'enhancement'
|
||||
assignees: ''
|
||||
---
|
||||
# Summary
|
||||
|
||||
What change you think needs making.
|
||||
|
||||
# Motivation
|
||||
|
||||
Please give examples of your use case, e.g. when would you use this.
|
||||
|
||||
# Proposal
|
||||
|
||||
How do you think this should be implemented?
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: 'enhancement'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
7
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
Checklist:
|
||||
|
||||
* [ ] Either (a) I've created an [enhancement proposal](https://github.com/argoproj/argo-cd/issues/new/choose) and discussed it with the community, (b) this is a bug fix, or (c) this does not need to be in the release notes.
|
||||
* [ ] The title of the PR states what changed and the related issues number (used for the release note).
|
||||
* [ ] I've updated both the CLI and UI to expose my feature, or I plan to submit a second PR with them.
|
||||
* [ ] Optional. My organization is added to USERS.md.
|
||||
* [ ] I've signed the CLA and my build is green ([troubleshooting builds](https://argoproj.github.io/argo-cd/developer-guide/ci/)).
|
||||
3
.github/stale.yml
vendored
@@ -1 +1,4 @@
|
||||
# See https://github.com/probot/stale
|
||||
# See https://github.com/probot/stale
|
||||
exemptLabels:
|
||||
- backlog
|
||||
|
||||
27
.github/workflows/gh-pages.yaml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.x
|
||||
- name: build
|
||||
run: |
|
||||
pip install mkdocs==1.0.4 mkdocs_material==4.1.1
|
||||
mkdocs build
|
||||
mkdir ./site/.circleci && echo '{version: 2, jobs: {build: {branches: {ignore: gh-pages}}}}' > ./site/.circleci/config.yml
|
||||
- name: deploy
|
||||
uses: peaceiris/actions-gh-pages@v2.5.0
|
||||
env:
|
||||
PERSONAL_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
PUBLISH_BRANCH: gh-pages
|
||||
PUBLISH_DIR: ./site
|
||||
59
.github/workflows/image.yaml
vendored
Normal file
@@ -0,0 +1,59 @@
|
||||
name: Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GOPATH: /home/runner/work/argo-cd/argo-cd
|
||||
steps:
|
||||
- uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: '1.14.1'
|
||||
- uses: actions/checkout@master
|
||||
with:
|
||||
path: src/github.com/argoproj/argo-cd
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: src/github.com/argoproj/argo-cd/vendor
|
||||
key: ${{ runner.os }}-go-dep-${{ hashFiles('**/Gopkg.lock') }}
|
||||
# download dependencies
|
||||
- run: mkdir -p $GITHUB_WORKSPACE/bin && curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
|
||||
working-directory: src/github.com/argoproj/argo-cd
|
||||
- run: $GOPATH/bin/dep ensure -v
|
||||
working-directory: ./src/github.com/argoproj/argo-cd
|
||||
|
||||
# get image tag
|
||||
- run: echo ::set-output name=tag::$(cat ./VERSION)-${GITHUB_SHA::8}
|
||||
working-directory: ./src/github.com/argoproj/argo-cd
|
||||
id: image
|
||||
|
||||
# build
|
||||
- run: |
|
||||
docker images -a --format "{{.ID}}" | xargs -I {} docker rmi {}
|
||||
make image DEV_IMAGE=true DOCKER_PUSH=false IMAGE_NAMESPACE=docker.pkg.github.com/argoproj/argo-cd IMAGE_TAG=${{ steps.image.outputs.tag }}
|
||||
working-directory: ./src/github.com/argoproj/argo-cd
|
||||
|
||||
# publish
|
||||
- run: |
|
||||
docker login docker.pkg.github.com --username $USERNAME --password $PASSWORD
|
||||
docker push docker.pkg.github.com/argoproj/argo-cd/argocd:${{ steps.image.outputs.tag }}
|
||||
env:
|
||||
USERNAME: ${{ secrets.USERNAME }}
|
||||
PASSWORD: ${{ secrets.TOKEN }}
|
||||
|
||||
# deploy
|
||||
- run: git clone "https://$TOKEN@github.com/argoproj/argoproj-deployments"
|
||||
env:
|
||||
TOKEN: ${{ secrets.TOKEN }}
|
||||
- run: |
|
||||
docker run -v $(pwd):/src -w /src --rm -t lyft/kustomizer:v3.3.0 kustomize edit set image argoproj/argocd=docker.pkg.github.com/argoproj/argo-cd/argocd:${{ steps.image.outputs.tag }}
|
||||
git config --global user.email 'ci@argoproj.com'
|
||||
git config --global user.name 'CI'
|
||||
git diff --exit-code && echo 'Already deployed' || (git commit -am 'Upgrade argocd to ${{ steps.image.outputs.tag }}' && git push)
|
||||
working-directory: argoproj-deployments/argocd
|
||||
# TODO: clean up old images once github supports it: https://github.community/t5/How-to-use-Git-and-GitHub/Deleting-images-from-Github-Package-Registry/m-p/41202/thread-id/9811
|
||||
3
.gitignore
vendored
@@ -9,3 +9,6 @@ site/
|
||||
cmd/**/debug
|
||||
debug.test
|
||||
coverage.out
|
||||
test-results
|
||||
.scannerwork
|
||||
.scratch
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
run:
|
||||
deadline: 8m
|
||||
timeout: 2m
|
||||
skip-files:
|
||||
- ".*\\.pb\\.go"
|
||||
skip-dirs:
|
||||
- pkg/client
|
||||
- vendor
|
||||
linter-settings:
|
||||
goimports:
|
||||
local-prefixes: github.com/argoproj/argo-cd
|
||||
- pkg/client/
|
||||
- vendor/
|
||||
linters:
|
||||
enable:
|
||||
- vet
|
||||
- gofmt
|
||||
- goimports
|
||||
- deadcode
|
||||
- goimports
|
||||
- varcheck
|
||||
- structcheck
|
||||
- ineffassign
|
||||
- unconvert
|
||||
- misspell
|
||||
- unparam
|
||||
linters-settings:
|
||||
goimports:
|
||||
local-prefixes: github.com/argoproj/argo-cd
|
||||
service:
|
||||
golangci-lint-version: 1.21.0
|
||||
|
||||
818
CHANGELOG.md
@@ -1,38 +1,804 @@
|
||||
# Changelog
|
||||
|
||||
## v1.0.0
|
||||
## v1.5.3 (Unreleased)
|
||||
|
||||
This patch release introduces a set of enhancements and bug fixes. Here are most notable changes:
|
||||
|
||||
#### Multiple Kustomize Versions
|
||||
|
||||
The bundled Kustomize version had been upgraded to v3.5.4. Argo CD allows changing bundled version using
|
||||
[custom image or init container](https://argoproj.github.io/argo-cd/operator-manual/custom_tools/).
|
||||
This [feature](https://argoproj.github.io/argo-cd/user-guide/kustomize/#custom-kustomize-versions)
|
||||
enables bundling multiple Kustomize versions at the same time and allows end-users to specify the required version per application.
|
||||
|
||||
#### Custom Root Path
|
||||
|
||||
The feature allows accessing Argo CD UI and API using a custom root path(for example https://myhostname/argocd).
|
||||
This enables running Argo CD behind a proxy that takes care of user authentication (such as Ambassador) or hosting
|
||||
multiple Argo CD using the same hostname. A set of bug fixes and enhancements had been implemented to makes it easier.
|
||||
Use new `--rootpath` [flag](https://argoproj.github.io/argo-cd/operator-manual/ingress/#argocd-server-and-ui-root-path-v153) to enable the feature.
|
||||
|
||||
### Login Rate Limiting
|
||||
|
||||
The feature prevents a built-in user password brute force attack and addresses the known
|
||||
[vulnerability](https://argoproj.github.io/argo-cd/security_considerations/#cve-2020-8827-insufficient-anti-automationanti-brute-force).
|
||||
|
||||
### Settings Management Tools
|
||||
|
||||
A new set of [CLI commands](https://argoproj.github.io/argo-cd/operator-manual/troubleshooting/) that simplify configuring Argo CD.
|
||||
Using the CLI you can test settings changes offline without affecting running Argo CD instance and have ability to troubleshot diffing
|
||||
customizations, custom resource health checks, and more.
|
||||
|
||||
### Other
|
||||
|
||||
* New Project and Application CRD settings ([#2900](https://github.com/argoproj/argo-cd/issues/2900), [#2873](https://github.com/argoproj/argo-cd/issues/2873)) that allows customizing Argo CD behavior.
|
||||
* Upgraded Dex (v2.22.0) enables seamless [SSO integration](https://www.openshift.com/blog/openshift-authentication-integration-with-argocd) with Openshift.
|
||||
|
||||
|
||||
#### Enhancements
|
||||
|
||||
* feat: added --grpc-web-root-path for CLI. (#3483)
|
||||
* feat: limit the maximum number of concurrent login attempts (#3467)
|
||||
* feat: upgrade kustomize version to 3.5.4 (#3472)
|
||||
* feat: upgrade dex to 2.22.0 (#3468)
|
||||
* feat: support user specified account token ids (#3425)
|
||||
* feat: support separate Kustomize version per application (#3414)
|
||||
* feat: add support for dex prometheus metrics (#3249)
|
||||
* feat: add settings troubleshooting commands to the 'argocd-util' binary (#3398)
|
||||
* feat: Let user to define meaningful unique JWT token name (#3388)
|
||||
* feat: Display link between OLM ClusterServiceVersion and it's OperatorGroup (#3390)
|
||||
* feat: Introduce sync-option SkipDryRunOnMissingResource=true (#2873) (#3247)
|
||||
* feat: support normalizing CRD fields that use known built-in K8S types (#3357)
|
||||
* feat: Whitelisted namespace resources (#2900)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
* fix: added path to cookie (#3501)
|
||||
* fix: 'argocd sync' does not take into account IgnoreExtraneous annotation (#3486)
|
||||
* fix: CLI renders flipped diff results (#3480)
|
||||
* fix: GetApplicationSyncWindows API should not validate project permissions (#3456)
|
||||
* fix: argocd-util kubeconfig should use RawRestConfig to export config (#3447)
|
||||
* fix: javascript error on accounts list page (#3453)
|
||||
* fix: support both <group>/<kind> as well as <kind> as a resource override key (#3433)
|
||||
* fix: Updating to jsonnet v1.15.0 fix issue #3277 (#3431)
|
||||
* fix for helm repo add with flag --insecure-skip-server-verification (#3420)
|
||||
* fix: app diff --local support for helm repo. #3151 (#3407)
|
||||
* fix: Syncing apps incorrectly states "app synced", but this is not true (#3286)
|
||||
* fix: for jsonnet when it is localed in nested subdirectory and uses import (#3372)
|
||||
* fix: Update 4.5.3 redis-ha helm manifest (#3370)
|
||||
* fix: return 401 error code if username does not exist (#3369)
|
||||
* fix: Do not panic while running hooks with short revision (#3368)
|
||||
|
||||
## v1.5.2 (2020-04-20)
|
||||
|
||||
#### Critical security fix
|
||||
|
||||
This release contains a critical security fix. Please refer to the
|
||||
[security document](https://argoproj.github.io/argo-cd/security_considerations/#CVE-2020-5260-possible-git-credential-leak)
|
||||
for more information.
|
||||
|
||||
**Upgrading is strongly recommended**
|
||||
|
||||
## v1.4.3 (2020-04-20)
|
||||
|
||||
#### Critical security fix
|
||||
|
||||
This release contains a critical security fix. Please refer to the
|
||||
[security document](https://argoproj.github.io/argo-cd/security_considerations/#CVE-2020-5260-possible-git-credential-leak)
|
||||
for more information.
|
||||
|
||||
## v1.5.1 (2020-04-06)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
* fix: return 401 error code if username does not exist (#3369)
|
||||
* fix: Do not panic while running hooks with short revision (#3368)
|
||||
* fix: Increase HAProxy check interval to prevent intermittent failures (#3356)
|
||||
* fix: Helm v3 CRD are not deployed (#3345)
|
||||
|
||||
## v1.5.0 (2020-04-02)
|
||||
|
||||
#### Helm Integration Enhancements - Helm 3 Support And More
|
||||
|
||||
Introduced native support Helm3 charts. For backward compatibility Helm 2 charts are still rendered using Helm 2 CLI. Argo CD inspects the
|
||||
Charts.yaml file and choose the right binary based on `apiVersion` value.
|
||||
|
||||
Following enhancement were implemented in addition to Helm 3:
|
||||
* The `--api-version` flag is passed to the `helm template` command during manifest generation.
|
||||
* The `--set-file` flag can be specified in the application specification.
|
||||
* Fixed bug that prevents automatically update Helm chart when new version is published (#3193)
|
||||
|
||||
#### Better Performance and Improved Metrics
|
||||
|
||||
If you are running Argo CD instances with several hundred applications on it, you should see a
|
||||
huge performance boost and significantly less Kubernetes API server load.
|
||||
|
||||
The Argo CD controller Prometheus metrics have been reworked to enable a richer Grafana dashboard.
|
||||
The improved dashboard is available at [examples/dashboard.json](https://github.com/argoproj/argo-cd/blob/master/examples/dashboard.json).
|
||||
You can set `ARGOCD_LEGACY_CONTROLLER_METRICS=true` environment variable and use [examples/dashboard-legacy.json](https://github.com/argoproj/argo-cd/blob/master/examples/dashboard-legacy.json)
|
||||
to keep using old dashboard.
|
||||
|
||||
#### Local accounts
|
||||
|
||||
The local accounts had been introduced additional to `admin` user and SSO integration. The feature is useful for creating authentication
|
||||
tokens with limited permissions to automate Argo CD management. Local accounts also could be used small by teams when SSO integration is overkill.
|
||||
This enhancement also allows to disable admin user and enforce only SSO logins.
|
||||
|
||||
#### Redis HA Proxy mode
|
||||
|
||||
As part of this release, the bundled Redis was upgraded to version 4.3.4 with enabled HAProxy.
|
||||
The HA proxy replaced the sentinel and provides more reliable Redis connection.
|
||||
|
||||
> After publishing 1.5.0 release we've discovered that default HAProxy settings might cause intermittent failures.
|
||||
> See [argo-cd#3358](https://github.com/argoproj/argo-cd/issues/3358)
|
||||
|
||||
#### Windows CLI
|
||||
|
||||
Windows users deploy to Kubernetes too! Now you can use Argo CD CLI on Linux, Mac OS, and Windows. The Windows compatible binary is available
|
||||
in the release details page as well as on the Argo CD Help page.
|
||||
|
||||
#### Breaking Changes
|
||||
|
||||
The `argocd_app_sync_status`, `argocd_app_health_status` and `argocd_app_created_time` prometheus metrics are deprecated in favor of additional labels
|
||||
to `argocd_app_info` metric. The deprecated labels are still available can be re-enabled using `ARGOCD_LEGACY_CONTROLLER_METRICS=true` environment variable.
|
||||
The legacy example Grafana dashboard is available at [examples/dashboard-legacy.json](https://github.com/argoproj/argo-cd/blob/master/examples/dashboard-legacy.json).
|
||||
|
||||
#### Known issues
|
||||
Last-minute bugs that will be addressed in 1.5.1 shortly:
|
||||
|
||||
* https://github.com/argoproj/argo-cd/issues/3336
|
||||
* https://github.com/argoproj/argo-cd/issues/3319
|
||||
* https://github.com/argoproj/argo-cd/issues/3339
|
||||
* https://github.com/argoproj/argo-cd/issues/3358
|
||||
|
||||
#### Enhancements
|
||||
* feat: support helm3 (#2383) (#3178)
|
||||
* feat: Argo CD Service Account / Local Users #3185
|
||||
* feat: Disable Admin Login (fixes #3019) (#3179)
|
||||
* feat(ui): add docs to sync policy options present in create application panel (Close #3098) (#3203)
|
||||
* feat: add "service-account" flag to "cluster add" command (#3183) (#3184)
|
||||
* feat: Supports the validate-false option at an app level. Closes #1063 (#2542)
|
||||
* feat: add dest cluster and namespace in the Events (#3093)
|
||||
* feat: Rollback disables auto sync issue #2441 (#2591)
|
||||
* feat: allow ssh and http repository references in bitbucketserver webhook #2773 (#3036)
|
||||
* feat: Add helm --set-file support (#2751)
|
||||
* feat: Include resource group for Event's InvolvedObject.APIVersion
|
||||
* feat: Add argocd cmd for Windows #2121 (#3015)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- fix: app reconciliation fails with panic: index out of (#3233)
|
||||
- fix: upgrade argoproj/pkg version to fix leaked sensitive information in logs (#3230)
|
||||
- fix: set MaxCallSendMsgSize to MaxGRPCMessageSize for the GRPC caller (#3117)
|
||||
- fix: stop caching helm index (#3193)
|
||||
- fix: dex proxy should forward request to dex preserving the basehref (#3165)
|
||||
- fix: set default login redirect to baseHRef (#3164)
|
||||
- fix: don't double-prepend basehref to redirect URLs (fixes #3137)
|
||||
- fix: ui referring to /api/version using absolute path (#3092)
|
||||
- fix: Unhang UI on long app info items by using more sane URL match pattern (#3159)
|
||||
- fix: Allow multiple hostnames per SSH known hosts entry and also allow IPv6 (#2814) (#3074)
|
||||
- fix: argocd-util backup produced truncated backups. import app status (#3096)
|
||||
- fix: upgrade redis-ha chart and enable haproxy (#3147)
|
||||
- fix: make dex server deployment init container resilient to restarts (#3136)
|
||||
- fix: reduct secret values of manifests stored in git (#3088)
|
||||
- fix: labels not being deleted via UI (#3081)
|
||||
- fix: HTTP|HTTPS|NO_PROXY env variable reading #3055 (#3063)
|
||||
- fix: Correct usage text for repo add command regarding insecure repos (#3068)
|
||||
- fix: Ensure SSH private key is written out with a final newline character (#2890) (#3064)
|
||||
- fix: Handle SSH URLs in 'git@server:org/repo' notation correctly (#3062)
|
||||
- fix sso condition when several sso connectors has been configured (#3057)
|
||||
- fix: Fix bug where the same pointer is used. (#3059)
|
||||
- fix: Opening in new tab bad key binding on Linux (#3020)
|
||||
- fix: K8s secrets for repository credential templates are not deleted when credential template is deleted (#3028)
|
||||
- fix: SSH credential template not working #3016
|
||||
- fix: Unable to parse kubectl pre-release version strings (#3034)
|
||||
- fix: Jsonnet TLA parameters of same type are overwritten (#3022)
|
||||
- fix: Replace aws-iam-authenticator to support IRSA (#3010)
|
||||
- fix: Hide bindPW in dex config (#3025)
|
||||
- fix: SSH repo URL with a user different from `git` is not matched correctly when resolving a webhook (#2988)
|
||||
- fix: JWT invalid => Password for superuser has changed since token issued (#2108)
|
||||
|
||||
#### Contributors
|
||||
* alexandrfox
|
||||
* alexec
|
||||
* alexmt
|
||||
* bergur88
|
||||
* CBytelabs
|
||||
* dbeal-wiser
|
||||
* dnascimento
|
||||
* Elgarni
|
||||
* eSamS
|
||||
* gpaul
|
||||
* jannfis
|
||||
* jdmulloy
|
||||
* machgo
|
||||
* masa213f
|
||||
* matthyx
|
||||
* rayanebel
|
||||
* shelby-moore
|
||||
* tomcruise81
|
||||
* wecger
|
||||
* zeph
|
||||
|
||||
## v1.4.2 (2020-01-24)
|
||||
|
||||
- fix: correctly replace cache in namespace isolation mode (#3023)
|
||||
|
||||
## v1.4.1 (2020-01-23)
|
||||
|
||||
- fix: impossible to config RBAC if group name includes ',' (#3013)
|
||||
|
||||
## v1.4.0 (2020-01-17)
|
||||
|
||||
The v1.4.0 is a stability release that brings multiple bug fixes, security, performance enhancements, and multiple usability improvements.
|
||||
|
||||
#### New Features
|
||||
|
||||
#### Security
|
||||
A number of security enhancements and features have been implemented (thanks to [@jannfis](https://github.com/jannfis) for driving it! ):
|
||||
* **Repository Credential Templates Management UI/CLI**. Now you can use Argo CD CLI or UI to configure
|
||||
[credentials template](https://argoproj.github.io/argo-cd/user-guide/private-repositories/#credential-templates) for multiple repositories!
|
||||
* **X-Frame-Options header on serving static assets**. The X-Frame-Options prevents third party sites to trick users into interacting with the application.
|
||||
* **Tighten AppProject RBAC enforcement**. We've improved the enforcement of access rules specified in the
|
||||
[application project](https://argoproj.github.io/argo-cd/operator-manual/declarative-setup/#projects) configuration.
|
||||
|
||||
#### Namespace Isolation
|
||||
With the namespace isolation feature, you are no longer have to give full read-only cluster access to the Argo CD. Instead, you can give access only to selected namespaces with-in
|
||||
the cluster:
|
||||
|
||||
```bash
|
||||
argocd cluster add <mycluster> --namespace <mynamespace1> --namespace <mynamespace2>
|
||||
```
|
||||
|
||||
This feature is useful if you don't have full cluster access but still want to use Argo CD to manage some cluster namespaces. The feature also improves performance if Argo CD is
|
||||
used to manage a few namespaces of a large cluster.
|
||||
|
||||
#### Reconciliation Performance
|
||||
The Argo CD no longer fork/exec `kubectl` to apply resource changes in the target cluster or convert resource manifest to the required manifest version. This reduces
|
||||
CPU and Memory usage of large Argo CD instances.
|
||||
|
||||
#### Resources Health based Hook Status
|
||||
The existing Argo CD [resource hooks](https://argoproj.github.io/argo-cd/user-guide/resource_hooks/) feature allows running custom logic during the syncing process. You can mark
|
||||
any Kubernetes resource as a hook and Argo CD assess hook status if resource is a `Pod`, `Job` or `Argo Workflow`. In the v1.4.0 release Argo CD is going to leverage resource
|
||||
[health assessment](https://argoproj.github.io/argo-cd/operator-manual/health/) to get sync hook status. This allows using any custom CRD as a sync hook and leverage custom health
|
||||
check logic.
|
||||
|
||||
#### Manifest Generation
|
||||
* **Track Helm Charts By Semantic Version**. You've been able to track charts hosted in Git repositories using branches to tags. This is now possible for Helm charts. You no longer
|
||||
need to choose the exact version, such as v1.4.0 ,instead you can use a semantic version constraint such as v1.4.* and the latest version that matches will be installed.
|
||||
* **Build Environment Variables**. Feature allows config management tool to get access to app details during manifest generation via
|
||||
[environment variables](https://argoproj.github.io/argo-cd/user-guide/build-environment/).
|
||||
* **Git submodules**. Argo CD is going to automatically fetch sub-modules if your repository has `.gitmodules` directory.
|
||||
|
||||
#### UI and CLI
|
||||
* **Improved Resource Tree View**. The Application details page got even prettier. The resource view was tuned to fit more resources into the screen, include more information about
|
||||
each resource and don't lose usability at the same time.
|
||||
* **New Account Management CLI Command**. The CLI allows to check which actions are allowed for your account: `argocd account can-i sync applications '*'`
|
||||
|
||||
#### Maintenance Tools
|
||||
The team put more effort into building tools that help to maintain Argo CD itself:
|
||||
* **Bulk Project Editing**. The `argocd-util` allows to add and remove permissions defined in multiple project roles using one command.
|
||||
* **More Prometheus Metrics**. A set of additional metrics that contains useful information managed clusters is exposed by application controller.
|
||||
|
||||
More documentation and tools are coming in patch releases.
|
||||
|
||||
#### Breaking Changes
|
||||
|
||||
The Argo CD deletes all **in-flight** hooks if you terminate running sync operation. The hook state assessment change implemented in this release the Argo CD enables detection of
|
||||
an in-flight state for all Kubernetes resources including `Deployment`, `PVC`, `StatefulSet`, `ReplicaSet` etc. So if you terminate the sync operation that has, for example,
|
||||
`StatefulSet` hook that is `Progressing` it will be deleted. The long-running jobs are not supposed to be used as a sync hook and you should consider using
|
||||
[Sync Waves](https://argoproj.github.io/argo-cd/user-guide/sync-waves/) instead.
|
||||
|
||||
#### Enhancements
|
||||
* feat: Add custom healthchecks for cert-manager v0.11.0 (#2689)
|
||||
* feat: add git submodule support (#2495)
|
||||
* feat: Add repository credential management API and CLI (addresses #2136) (#2207)
|
||||
* feat: add support for --additional-headers cli flag (#2467)
|
||||
* feat: Add support for ssh-with-port repo url (#2866) (#2948)
|
||||
* feat: Add Time to ApplicationCondition. (#2417)
|
||||
* feat: Adds `argocd auth can-i` command. Close #2255
|
||||
* feat: Adds revision history limit. Closes #2790 (#2818)
|
||||
* feat: Adds support for ARGO_CD_[TARGET_REVISION|REVISION] and pass to Custom Tool/Helm/Jsonnet
|
||||
* feat: Adds support for Helm charts to be a semver range. Closes #2552 (#2606)
|
||||
* feat: Adds tracing to key external invocations. (#2811)
|
||||
* feat: argocd-util should allow editing project policies in bulk (#2615)
|
||||
* feat: Displays controllerrevsion's revision in the UI. Closes #2306 (#2702)
|
||||
* feat: Issue #2559 - Add gauge Prometheus metric which represents the number of pending manifest requests. (#2658)
|
||||
* feat: Make ConvertToVersion maybe 1090% faster on average (#2820)
|
||||
* feat: namespace isolation (#2839)
|
||||
* feat: removes redundant mutex usage in controller cache and adds cluster cache metrics (#2898)
|
||||
* feat: Set X-Frame-Options on serving static assets (#2706) (#2711)
|
||||
* feat: Simplify using Argo CD without users/SSO/UI (#2688)
|
||||
* feat: Template Out Data Source in Grafana Dashboard (#2859)
|
||||
* feat: Updates UI icons. Closes #2625 and #2757 (#2653)
|
||||
* feat: use editor arguments in InteractiveEditor (#2833)
|
||||
* feat: Use kubectl apply library instead of forking binary (#2861)
|
||||
* feat: use resource health for hook status evaluation (#2938)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- fix: Adds support for /api/v1/account* via HTTP. Fixes #2664 (#2701)
|
||||
- fix: Allow '@'-character in SSH usernames when connecting a repository (#2612)
|
||||
- fix: Allow dot in project policy. Closes #2724 (#2755)
|
||||
- fix: Allow you to sync local Helm apps. Fixes #2741 (#2747)
|
||||
- fix: Allows Helm parameters that contains arrays or maps. (#2525)
|
||||
- fix: application-controller doesn't deal with rm/add same cluster gracefully (x509 unknown) (#2389)
|
||||
- fix: diff local ignore kustomize build options (#2942)
|
||||
- fix: Ensures that Helm charts are correctly resolved before sync. Fixes #2758 (#2760)
|
||||
- fix: Fix 'Open application' link when using basehref (#2729)
|
||||
- fix: fix a bug with cluster add when token secret is not first in list. (#2744)
|
||||
- fix: fix bug where manifests are not cached. Fixes #2770 (#2771)
|
||||
- fix: Fixes bug whereby retry does not work for CLI. Fixes #2767 (#2768)
|
||||
- fix: git contention leads applications into Unknown state (#2877)
|
||||
- fix: Issue #1944 - Gracefully handle missing cached app state (#2464)
|
||||
- fix: Issue #2668 - Delete a specified context (#2669)
|
||||
- fix: Issue #2683 - Make sure app update don't fail due to concurrent modification (#2852)
|
||||
- fix: Issue #2721 Optimize helm repo querying (#2816)
|
||||
- fix: Issue #2853 - Improve application env variables/labels editing (#2856)
|
||||
- fix: Issue 2848 - Application Deployment history panel shows incorrect info for recent releases (#2849)
|
||||
- fix: Make BeforeHookCreation the default. Fixes #2754 (#2759)
|
||||
- fix: No error on `argocd app create` in CLI if `--revision` is omitted #2665
|
||||
- fix: Only delete resources during app delete cascade if permitted to (fixes #2693) (#2695)
|
||||
- fix: prevent user from seeing/deleting resources not permitted in project (#2908) (#2910)
|
||||
- fix: self-heal should retry syncing an application after specified delay
|
||||
- fix: stop logging dex config secrets #(2904) (#2937)
|
||||
- fix: stop using jsondiffpatch on clientside to render resource difference (#2869)
|
||||
- fix: Target Revision truncated #2736
|
||||
- fix: UI should re-trigger SSO login if SSO JWT token expires (#2891)
|
||||
- fix: update argocd-util import was not working properly (#2939)
|
||||
|
||||
#### Contributors
|
||||
|
||||
* Aalok Ahluwalia
|
||||
* Aananth K
|
||||
* Abhishek Jaisingh
|
||||
* Adam Johnson
|
||||
* Alan Tang
|
||||
* Alex Collins
|
||||
* Alexander Matyushentsev
|
||||
* Andrew Waters
|
||||
* Byungjin Park
|
||||
* Christine Banek
|
||||
* Daniel Helfand
|
||||
* David Hong
|
||||
* David J. M. Karlsen
|
||||
* David Maciel
|
||||
* Devan Goodwin
|
||||
* Devin Stein
|
||||
* dthomson25
|
||||
* Gene Liverman
|
||||
* Gregor Krmelj
|
||||
* Guido Maria Serra
|
||||
* Ilir Bekteshi
|
||||
* Imran Ismail
|
||||
* INOUE BANJI
|
||||
* Isaac Gaskin
|
||||
* jannfis
|
||||
* Jeff Hastings
|
||||
* Jesse Suen
|
||||
* John Girvan
|
||||
* Konstantin
|
||||
* Lev Aminov
|
||||
* Manatsawin Hanmongkolchai
|
||||
* Marco Schmid
|
||||
* Masayuki Ishii
|
||||
* Michael Bridgen
|
||||
* Naoki Oketani
|
||||
* niqdev
|
||||
* nitinpatil1992
|
||||
* Olivier Boukili
|
||||
* Olivier Lemasle
|
||||
* Omer Kahani
|
||||
* Paul Brit
|
||||
* Qingbo Zhou
|
||||
* Saradhi Sreegiriraju
|
||||
* Scott Cabrinha
|
||||
* shlo
|
||||
* Simon Behar
|
||||
* stgarf
|
||||
* Yujun Zhang
|
||||
* Zoltán Reegn
|
||||
|
||||
## v1.3.4 (2019-12-05)
|
||||
- #2819 Fixes logging of tracing option in CLI
|
||||
|
||||
## v1.3.3 (2019-12-05)
|
||||
- #2721 High CPU utilisation (5 cores) and spammy logs
|
||||
|
||||
## v1.3.2 (2019-12-03)
|
||||
- #2797 Fix directory traversal edge case and enhance tests
|
||||
|
||||
## v1.3.1 (2019-12-02)
|
||||
- #2664 update account password from API resulted 404
|
||||
- #2724 Can't use `DNS-1123` compliant app name when creating project role
|
||||
- #2726 App list does not show chart for Helm app
|
||||
- #2741 argocd local sync cannot parse kubernetes version
|
||||
- #2754 BeforeHookCreation should be the default hook
|
||||
- #2767 Fix bug whereby retry does not work for CLI
|
||||
- #2770 Always cache miss for manifests
|
||||
- #1345 argocd-application-controller: can not retrieve list of objects using index : Index with name namespace does not exist
|
||||
|
||||
## v1.3.0 (2019-11-13)
|
||||
|
||||
#### New Features
|
||||
|
||||
##### Helm 1st-Class Support
|
||||
|
||||
We know that for many of our users, they want to deploy existing Helm charts using Argo CD. Up until now that has required you to create an Argo CD app in a Git repo that does nothing but point to that chart. Now you can use a Helm chart repository is the same way as a Git repository.
|
||||
|
||||
On top of that, we've improved support for Helm apps. The most common types of Helm hooks such as `pre-install` and `post-install` are supported as well as a the delete policy `before-hook-creation` which makes it easier to work with hooks.
|
||||
|
||||
https://youtu.be/GP7xtrnNznw
|
||||
|
||||
##### Orphan Resources
|
||||
|
||||
Some users would like to make sure that resources in a namespace are managed only by Argo CD. So we've introduced the concept of an "orphan resource" - any resource that is in namespace associated with an app, but not managed by Argo CD. This is enabled in the project settings. Once enabled, Argo CD will show in the app view any resources in the app's namepspace that is not mananged by Argo CD.
|
||||
|
||||
https://youtu.be/9ZoTevVQf5I
|
||||
|
||||
##### Sync Windows
|
||||
|
||||
There may be instances when you want to control the times during which an Argo CD app can sync. Sync Windows now gives you the capability to create windows of time in which apps are either allowed or denied the ability to sync. This can apply to both manual and auto-sync, or just auto-sync. The windows are configured at the project level and assigned to apps using app name, namespace or cluster. Wildcards are supported for all fields.
|
||||
|
||||
#### Enhancements
|
||||
|
||||
* [UI] Add application labels to Applications list and Applications details page (#1099)
|
||||
* Helm repository as first class Argo CD Application source (#1145)
|
||||
* Ability to generate a warn/alert when a namespace deviates from the expected state (#1167)
|
||||
* Improve diff support for resource requests/limits (#1615)
|
||||
* HTTP API should allow JWT to be passed via Authorization header (#1642)
|
||||
* Ability to create & upsert projects from spec (#1852)
|
||||
* Support for in-line block from helm chart values (#1930)
|
||||
* Request OIDC groups claim if groups scope is not supported (#1956)
|
||||
* Add a maintenance window for Applications with automated syncing (#1995)
|
||||
* Support `argocd.argoproj.io/hook-delete-policy: BeforeHookCreation` (#2036)
|
||||
* Support setting Helm string parameters using CLI/UI (#2078)
|
||||
* Config management plugin environment variable UI/CLI support (#2203)
|
||||
* Helm: auto-detect URLs (#2260)
|
||||
* Helm: UI improvements (#2261)
|
||||
* Support `helm template --kube-version ` (#2275)
|
||||
* Use community icons for resources (#2277)
|
||||
* Make `group` optional for `ignoreDifferences` config (#2298)
|
||||
* Update Helm docs (#2315)
|
||||
* Add cluster information into Splunk (#2354)
|
||||
* argocd list command should have filter options like by project (#2396)
|
||||
* Add target/current revision to status badge (#2445)
|
||||
* Update tooling to use Kustomize v3 (#2487)
|
||||
* Update root `Dockerfile` to use the `hack/install.sh` (#2488)
|
||||
* Support and document using HPA for repo-server (#2559)
|
||||
* Upgrade Helm (#2587)
|
||||
* UI fixes for "Sync Apps" panel. (#2604)
|
||||
* Upgrade kustomize from v3.1.0 to v3.2.1 (#2609)
|
||||
* Map helm lifecycle hooks to ArgoCD pre/post/sync hooks (#355)
|
||||
* [UI] Enhance app creation page with Helm parameters overrides (#1059)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- failed parsing on parameters with comma (#1660)
|
||||
- Statefuleset with OnDelete Update Strategy stuck progressing (#1881)
|
||||
- Warning during secret diffing (#1923)
|
||||
- Error message "Unable to load data: key is missing" is confusing (#1944)
|
||||
- OIDC group bindings are truncated (#2006)
|
||||
- Multiple parallel app syncs causes OOM (#2022)
|
||||
- Unknown error when setting params with argocd app set on helm app (#2046)
|
||||
- Endpoint is no longer shown as a child of services (#2060)
|
||||
- SSH known hosts entry cannot be deleted if contains shell pattern in name (#2099)
|
||||
- Application 404s on names with periods (#2114)
|
||||
- Adding certs for hostnames ending with a dot (.) is not possible (#2116)
|
||||
- Fix `TestHookDeleteBeforeCreation` (#2141)
|
||||
- v1.2.0-rc1 nil pointer dereference when syncing (#2146)
|
||||
- Replacing services failure (#2150)
|
||||
- 1.2.0-rc1 - Authentication Required error in Repo Server (#2152)
|
||||
- v1.2.0-rc1 Applications List View doesn't work (#2174)
|
||||
- Manual sync does not trigger Presync hooks (#2185)
|
||||
- SyncError app condition disappears during app reconciliation (#2192)
|
||||
- argocd app wait\sync prints 'Unknown' for resources without health (#2198)
|
||||
- 1.2.0-rc2 Warning during secret diffing (#2206)
|
||||
- SSO redirect url is incorrect if configured Argo CD URL has trailing slash (#2212)
|
||||
- Application summary diff page shows hooks (#2215)
|
||||
- An app with a single resource and Sync hook remains progressing (#2216)
|
||||
- CONTRIBUTING documentation outdated (#2231)
|
||||
- v1.2.0-rc2 does not retrieve http(s) based git repository behind the proxy (#2243)
|
||||
- Intermittent "git ls-remote" request failures should not fail app reconciliation (#2245)
|
||||
- Result of ListApps operation for Git repo is cached incorrectly (#2263)
|
||||
- ListApps does not utilize cache (#2287)
|
||||
- Controller panics due to nil pointer error (#2290)
|
||||
- The Helm --kube-version support does not work on GKE: (#2303)
|
||||
- Fixes bug that prevents you creating repos via UI/CLI. (#2308)
|
||||
- The 'helm.repositories' settings is dropped without migration path (#2316)
|
||||
- Badge response does not contain cache control header (#2317)
|
||||
- Inconsistent sync result from UI and CLI (#2321)
|
||||
- Failed edit application with plugin type requiring environment (#2330)
|
||||
- AutoSync doesn't work anymore (#2339)
|
||||
- End-to-End tests not working with Kubernetes v1.16 (#2371)
|
||||
- Creating an application from Helm repository should select "Helm" as source type (#2378)
|
||||
- The parameters of ValidateAccess GRPC method should not be logged (#2386)
|
||||
- Maintenance window meaning is confusing (#2398)
|
||||
- UI bug when targetRevision is ommited (#2407)
|
||||
- Too many vulnerabilities in Docker image (#2425)
|
||||
- proj windows commands not consistent with other commands (#2443)
|
||||
- Custom resource actions cannot be executed from the UI (#2448)
|
||||
- Application controller sometimes accidentally removes duplicated/excluded resource warning condition (#2453)
|
||||
- Logic that checks sync windows state in the cli is incorrect (#2455)
|
||||
- UI don't allow to create window with `* * * * *` schedule (#2475)
|
||||
- Helm Hook is executed twice if annotated with both pre-install and pre-upgrade annotations (#2480)
|
||||
- Impossible to edit chart name using App details page (#2484)
|
||||
- ArgoCD does not provide CSRF protection (#2496)
|
||||
- ArgoCD failing to install CRDs in master from Helm Charts (#2497)
|
||||
- Timestamp in Helm package file name causes error in Application with Helm source (#2549)
|
||||
- Attempting to create a repo with password but not username panics (#2567)
|
||||
- UI incorrectly mark resources as `Required Pruning` (#2577)
|
||||
- argocd app diff prints only first difference (#2616)
|
||||
- Bump min client cache version (#2619)
|
||||
- Cluster list page fails if any cluster is not reachable (#2620)
|
||||
- Repository type should be mandatory for repo add command in CLI (#2622)
|
||||
- Repo server executes unnecessary ls-remotes (#2626)
|
||||
- Application list page incorrectly filter apps by label selector (#2633)
|
||||
- Custom actions are disabled in Argo CD UI (#2635)
|
||||
- Failure of `argocd version` in the self-building container image (#2645)
|
||||
- Application list page is not updated automatically anymore (#2655)
|
||||
- Login regression issues (#2659)
|
||||
- Regression: Cannot return Kustomize version for 3.1.0 (#2662)
|
||||
- API server does not allow creating role with action `action/*` (#2670)
|
||||
- Application controller `kubectl-parallelism-limit` flag is broken (#2673)
|
||||
- Annoying toolbar flickering (#2691)
|
||||
|
||||
## v1.2.5 (2019-10-29)
|
||||
|
||||
- Issue #2339 - Don't update `status.reconciledAt` unless compared with latest git version (#2581)
|
||||
|
||||
## v1.2.4 (2019-10-23)
|
||||
|
||||
- Issue #2185 - Manual sync don't trigger hooks (#2477)
|
||||
- Issue #2339 - Controller should compare with latest git revision if app has changed (#2543)
|
||||
- Unknown child app should not affect app health (#2544)
|
||||
- Redact secrets in dex logs (#2538)
|
||||
|
||||
## v1.2.3 (2019-10-1)
|
||||
* Make argo-cd docker images openshift friendly (#2362) (@duboisf)
|
||||
* Add dest-server and dest-namespace field to reconciliation logs (#2354)
|
||||
- Stop loggin /repository.RepositoryService/ValidateAccess parameters (#2386)
|
||||
|
||||
## v1.2.2 (2019-09-26)
|
||||
+ Resource action equivalent to `kubectl rollout restart` (#2177)
|
||||
- Badge response does not contain cache-control header (#2317) (@greenstatic)
|
||||
- Make sure the controller uses the latest git version if app reconciliation result expired (#2339)
|
||||
|
||||
## v1.2.1 (2019-09-12)
|
||||
+ Support limiting number of concurrent kubectl fork/execs (#2022)
|
||||
+ Add --self-heal flag to argocd cli (#2296)
|
||||
- Fix degraded proxy support for http(s) git repository (#2243)
|
||||
- Fix nil pointer dereference in application controller (#2290)
|
||||
|
||||
## v1.2.0 (2019-09-05)
|
||||
|
||||
### New Features
|
||||
|
||||
#### Server Certificate And Known Hosts Management
|
||||
|
||||
The Server Certificate And Known Hosts Management feature makes it really easy to connect private Git repositories to Argo CD. Now Argo CD provides UI and CLI which
|
||||
enables managing certificates and known hosts which are used to access Git repositories. It is also possible to configure both hosts and certificates in a declarative manner using
|
||||
[argocd-ssh-known-hosts-cm](https://github.com/argoproj/argo-cd/blob/master/docs/operator-manual/argocd-ssh-known-hosts-cm.yaml) and
|
||||
[argocd-tls-certs-cm.yaml](https://github.com/argoproj/argo-cd/blob/master/docs/operator-manual/argocd-tls-certs-cm.yaml) config maps.
|
||||
|
||||
#### Self-Healing
|
||||
|
||||
The existing Automatic Sync feature allows to automatically apply any new changes in Git to the target Kubernetes cluster. However, Automatic Sync does not cover the case when the
|
||||
application is out of sync due to the unexpected change in the target cluster. The Self-Healing feature fills this gap. With Self-Healing enabled Argo CD automatically pushes the desired state from Git into the cluster every time when state deviation is detected.
|
||||
|
||||
**Anonymous access** - enable read-only access without authentication to anyone in your organization.
|
||||
|
||||
Support for Git LFS enabled repositories - now you can store Helm charts as tar files and enable Git LFS in your repository.
|
||||
|
||||
**Compact diff view** - compact diff summary of the whole application in a single view.
|
||||
|
||||
**Badge for application status** - add badge with the health and sync status of your application into README.md of your deployment repo.
|
||||
|
||||
**Allow configuring google analytics tracking** - use Google Analytics to check how many users are visiting UI or your Argo CD instance.
|
||||
|
||||
#### Backward Incompatible Changes
|
||||
- Kustomize v1 support is removed. All kustomize charts are built using the same Kustomize version
|
||||
- Kustomize v2.0.3 upgraded to v3.1.0 . We've noticed one backward incompatible change: https://github.com/kubernetes-sigs/kustomize/issues/42 . Starting v2.1.0 namespace prefix feature works with CRD ( which might cause renaming of generated resource definitions)
|
||||
- Argo CD config maps must be annotated with `app.kubernetes.io/part-of: argocd` label. Make sure to apply updated `install.yaml` manifest in addition to changing image version.
|
||||
|
||||
|
||||
#### Enhancements
|
||||
+ Adds a floating action button with help and chat links to every page.… (#2124)
|
||||
+ Enhances cookie warning with actual length to help users fix their co… (#2134)
|
||||
+ Added 'SyncFail' to possible HookTypes in UI (#2147)
|
||||
+ Support for Git LFS enabled repositories (#1853)
|
||||
+ Server certificate and known hosts management (#1514)
|
||||
+ Client HTTPS certifcates for private git repositories (#1945)
|
||||
+ Badge for application status (#1435)
|
||||
+ Make the health check for APIService a built in (#1841)
|
||||
+ Bitbucket Server and Gogs webhook providers (#1269)
|
||||
+ Jsonnet TLA arguments in ArgoCD CLI (#1626)
|
||||
+ Self Healing (#1736)
|
||||
+ Compact diff view (#1831)
|
||||
+ Allow Helm parameters to force ambiguously-typed values to be strings (#1846)
|
||||
+ Support anonymous argocd access (#1620)
|
||||
+ Allow configuring google analytics tracking (#738)
|
||||
+ Bash autocompletion for argocd (#1798)
|
||||
+ Additional commit metadata (#1219)
|
||||
+ Displays targetRevision in app dashboards. (#1239)
|
||||
+ Local path syncing (#839)
|
||||
+ System level `kustomize build` options (#1789)
|
||||
+ Adds support for `argocd app set` for Kustomize. (#1843)
|
||||
+ Allow users to create tokens for projects where they have any role. (#1977)
|
||||
+ Add Refresh button to applications table and card view (#1606)
|
||||
+ Adds CLI support for adding and removing groups from project roles. (#1851)
|
||||
+ Support dry run and hook vs. apply strategy during sync (#798)
|
||||
+ UI should remember most recent selected tab on resource info panel (#2007)
|
||||
+ Adds link to the project from the app summary page. (#1911)
|
||||
+ Different icon for resources which require pruning (#1159)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- Do not panic if the type is not api.Status (an error scenario) (#2105)
|
||||
- Make sure endpoint is shown as a child of service (#2060)
|
||||
- Word-wraps app info in the table and list views. (#2004)
|
||||
- Project source/destination removal should consider wildcards (#1780)
|
||||
- Repo whitelisting in UI does not support wildcards (#2000)
|
||||
- Wait for CRD creation during sync process (#1940)
|
||||
- Added a button to select out of sync items in the sync panel (#1902)
|
||||
- Proper handling of an excluded resource in an application (#1621)
|
||||
- Stop repeating logs on stoped container (#1614)
|
||||
- Fix git repo url parsing on application list view (#2174)
|
||||
- Fix nil pointer dereference error during app reconciliation (#2146)
|
||||
- Fix history api fallback implementation to support app names with dots (#2114)
|
||||
- Fixes some code issues related to Kustomize build options. (#2146)
|
||||
- Adds checks around valid paths for apps (#2133)
|
||||
- Enpoint incorrectly considered top level managed resource (#2060)
|
||||
- Allow adding certs for hostnames ending on a dot (#2116)
|
||||
|
||||
#### Other
|
||||
* Upgrade kustomize to v3.1.0 (#2068)
|
||||
* Remove support for Kustomize 1. (#1573)
|
||||
|
||||
#### Contributors
|
||||
|
||||
* [alexec](https://github.com/alexec)
|
||||
* [alexmt](https://github.com/alexmt)
|
||||
* [dmizelle](https://github.com/dmizelle)
|
||||
* [lcostea](https://github.com/lcostea)
|
||||
* [jutley](https://github.com/jutley)
|
||||
* [masa213f](https://github.com/masa213f)
|
||||
* [Rayyis](https://github.com/Rayyis)
|
||||
* [simster7](https://github.com/simster7)
|
||||
* [dthomson25](https://github.com/dthomson25)
|
||||
* [jannfis](https://github.com/jannfis)
|
||||
* [naynasiddharth](https://github.com/naynasiddharth)
|
||||
* [stgarf](https://github.com/stgarf)
|
||||
|
||||
|
||||
## v1.1.2 (2019-07-30)
|
||||
- 'argocd app wait' should print correct sync status (#2049)
|
||||
- Check that TLS is enabled when registering DEX Handlers (#2047)
|
||||
- Do not ignore Argo hooks when there is a Helm hook. (#1952)
|
||||
|
||||
## v1.1.1 (2019-07-25)
|
||||
+ Support 'override' action in UI/API (#1984)
|
||||
- Fix argocd app wait message (#1982)
|
||||
|
||||
## v1.1.0 (2019-07-24)
|
||||
|
||||
### New Features
|
||||
|
||||
#### Sync Waves
|
||||
|
||||
Sync waves feature allows executing a sync operation in a number of steps or waves. Within each synchronization phase (pre-sync, sync, post-sync) you can have one or more waves,
|
||||
than allows you to ensure certain resources are healthy before subsequent resources are synced.
|
||||
|
||||
#### Optimized Interaction With Git
|
||||
|
||||
Argo CD needs to execute `git fetch` operation to access application manifests and `git ls-remote` to resolve ambiguous git revision. The `git ls-remote` is executed very frequently
|
||||
and although the operation is very lightweight it adds unnecessary load on Git server and might cause performance issues. In v1.1 release, the application reconciliation process was
|
||||
optimized which significantly reduced the number of Git requests. With v1.1 release, Argo CD should send 3x ~ 5x fewer Git requests.
|
||||
|
||||
#### User Defined Application Metadata
|
||||
|
||||
User-defined Application metadata enables the user to define a list of useful URLs for their specific application and expose those links on the UI
|
||||
(e.g. reference tp a CI pipeline or an application-specific management tool). These links should provide helpful shortcuts that make easier to integrate Argo CD into existing
|
||||
systems by making it easier to find other components inside and outside Argo CD.
|
||||
|
||||
### Deprecation Notice
|
||||
|
||||
* Kustomize v1.0 is deprecated and support will be removed in the Argo CD v1.2 release.
|
||||
|
||||
#### Enhancements
|
||||
|
||||
- Sync waves [#1544](https://github.com/argoproj/argo-cd/issues/1544)
|
||||
- Adds Prune=false and IgnoreExtraneous options [#1629](https://github.com/argoproj/argo-cd/issues/1629)
|
||||
- Forward Git credentials to config management plugins [#1628](https://github.com/argoproj/argo-cd/issues/1628)
|
||||
- Improve Kustomize 2 parameters UI [#1609](https://github.com/argoproj/argo-cd/issues/1609)
|
||||
- Adds `argocd logout` [#1210](https://github.com/argoproj/argo-cd/issues/1210)
|
||||
- Make it possible to set Helm release name different from Argo CD app name. [#1066](https://github.com/argoproj/argo-cd/issues/1066)
|
||||
- Add ability to specify system namespace during cluster add operation [#1661](https://github.com/argoproj/argo-cd/pull/1661)
|
||||
- Make listener and metrics ports configurable [#1647](https://github.com/argoproj/argo-cd/pull/1647)
|
||||
- Using SSH keys to authenticate kustomize bases from git [#827](https://github.com/argoproj/argo-cd/issues/827)
|
||||
- Adds `argocd app sync APPNAME --async` [#1728](https://github.com/argoproj/argo-cd/issues/1728)
|
||||
- Allow users to define app specific urls to expose in the UI [#1677](https://github.com/argoproj/argo-cd/issues/1677)
|
||||
- Error view instead of blank page in UI [#1375](https://github.com/argoproj/argo-cd/issues/1375)
|
||||
- Project Editor: Whitelisted Cluster Resources doesn't strip whitespace [#1693](https://github.com/argoproj/argo-cd/issues/1693)
|
||||
- Eliminate unnecessary git interactions for top-level resource changes (#1919)
|
||||
- Ability to rotate the bearer token used to manage external clusters (#1084)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- Project Editor: Whitelisted Cluster Resources doesn't strip whitespace [#1693](https://github.com/argoproj/argo-cd/issues/1693)
|
||||
- \[ui small bug\] menu position outside block [#1711](https://github.com/argoproj/argo-cd/issues/1711)
|
||||
- UI will crash when create application without destination namespace [#1701](https://github.com/argoproj/argo-cd/issues/1701)
|
||||
- ArgoCD synchronization failed due to internal error [#1697](https://github.com/argoproj/argo-cd/issues/1697)
|
||||
- Replicasets ordering is not stable on app tree view [#1668](https://github.com/argoproj/argo-cd/issues/1668)
|
||||
- Stuck processor on App Controller after deleting application with incomplete operation [#1665](https://github.com/argoproj/argo-cd/issues/1665)
|
||||
- Role edit page fails with JS error [#1662](https://github.com/argoproj/argo-cd/issues/1662)
|
||||
- failed parsing on parameters with comma [#1660](https://github.com/argoproj/argo-cd/issues/1660)
|
||||
- Handle nil obj when processing custom actions [#1700](https://github.com/argoproj/argo-cd/pull/1700)
|
||||
- Account for missing fields in Rollout HealthStatus [#1699](https://github.com/argoproj/argo-cd/pull/1699)
|
||||
- Sync operation unnecessary waits for a healthy state of all resources [#1715](https://github.com/argoproj/argo-cd/issues/1715)
|
||||
- failed parsing on parameters with comma [#1660](https://github.com/argoproj/argo-cd/issues/1660)
|
||||
- argocd app sync hangs when cluster is not configured (#1935)
|
||||
- Do not allow app-of-app child app's Missing status to affect parent (#1954)
|
||||
- Argo CD don't handle well k8s objects which size exceeds 1mb (#1685)
|
||||
- Secret data not redacted in last-applied-configuration (#897)
|
||||
- Running app actions requires only read privileges (#1827)
|
||||
- UI should allow editing repo URL (#1763)
|
||||
- Make status fields as optional fields (#1779)
|
||||
- Use correct healthcheck for Rollout with empty steps list (#1776)
|
||||
|
||||
#### Other
|
||||
|
||||
- Add Prometheus metrics for git repo interactions (#1912)
|
||||
- App controller should log additional information during app syncing (#1909)
|
||||
- Make sure api server to repo server grpc calls have timeout (#1820)
|
||||
- Forked tool processes should timeout (#1821)
|
||||
- Add health check to the controller deployment (#1785)
|
||||
|
||||
#### Contributors
|
||||
|
||||
* [Aditya Gupta](https://github.com/AdityaGupta1)
|
||||
* [Alex Collins](https://github.com/alexec)
|
||||
* [Alex Matyushentsev](https://github.com/alexmt)
|
||||
* [Danny Thomson](https://github.com/dthomson25)
|
||||
* [jannfis](https://github.com/jannfis)
|
||||
* [Jesse Suen](https://github.com/jessesuen)
|
||||
* [Liviu Costea](https://github.com/lcostea)
|
||||
* [narg95](https://github.com/narg95)
|
||||
* [Simon Behar](https://github.com/simster7)
|
||||
|
||||
See also [milestone v1.1](https://github.com/argoproj/argo-cd/milestone/13)
|
||||
|
||||
## v1.0.0 (2019-05-16)
|
||||
|
||||
### New Features
|
||||
|
||||
#### Network View
|
||||
|
||||
TODO
|
||||
A new way to visual application resources had been introduced to the Application Details page. The Network View visualizes connections between Ingresses, Services and Pods
|
||||
based on ingress reference service, service's label selectors and labels. The new view is useful to understand the application traffic flow and troubleshot connectivity issues.
|
||||
|
||||
#### Custom Actions
|
||||
|
||||
Argo CD introduces Custom Resource Actions to allow users to provide their own Lua scripts to modify existing Kubernetes resources in their applications. These actions are exposed in the UI to allow easy, safe, and reliable changes to their resources. This functionality can be used to introduce functionality such as suspending and enabling a Kubernetes cronjob, continue a BlueGreen deployment with Argo Rollouts, or scaling a deployment.
|
||||
|
||||
#### UI Enhancements
|
||||
#### UI Enhancements & Usability Enhancements
|
||||
|
||||
* New color palette intended to highlight unhealthily and out-of-sync resources more clearly.
|
||||
* The health of more resources is displayed, so it easier to quickly zoom to unhealthy pods, replica-sets, etc.
|
||||
* Resources that do not have health no longer appear to be healthy.
|
||||
* Support for configuring Git repo credentials at a domain/org level
|
||||
* Support for configuring requested OIDC provider scopes and enforced RBAC scopes
|
||||
* Support for configuring monitored resources whitelist in addition to excluded resources
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Remove deprecated componentParameterOverrides field #1372
|
||||
|
||||
### Changes since v0.12.1
|
||||
|
||||
#### New Features
|
||||
|
||||
+ Issue #357 - Expose application nodes networking information (#1333)
|
||||
+ Support for customizable resource actions as Lua scripts #86
|
||||
+ Surface Service/Ingress external IPs, hostname to application #908
|
||||
+ Update argocd-util import/export to support proper backup and restore (#1328)
|
||||
### Changes since v0.12.2
|
||||
|
||||
#### Enhancements
|
||||
|
||||
* `argocd app wait` should have `--resource` flag like sync #1206
|
||||
* Adds support for `kustomize edit set image`. Closes #1275 (#1324)
|
||||
* Allow wait to return on health or suspended (#1392)
|
||||
* Application warning when a manifest is defined twice #1070
|
||||
@@ -41,31 +807,49 @@ Argo CD introduces Custom Resource Actions to allow users to provide their own L
|
||||
* Display number of errors on resource tab #1477
|
||||
* Displays resources that are being deleted as "Progressing". Closes #1410 (#1426)
|
||||
* Generate random name for grpc proxy unix socket file instead of time stamp (#1455)
|
||||
* Issue #357 - Expose application nodes networking information (#1333)
|
||||
* Issue #1404 - App controller unnecessary set namespace to cluster level resources (#1405)
|
||||
* Nils health if the resource does not provide it. Closes #1383 (#1408)
|
||||
* Perform health assessments on all resource nodes in the tree. Closes #1382 (#1422)
|
||||
* Remove deprecated componentParameterOverrides field #1372
|
||||
* Shows the health of the application. Closes #1433 (#1434)
|
||||
* Surface Service/Ingress external IPs, hostname to application #908
|
||||
* Surface pod status to tree view #1358
|
||||
* Support for customizable resource actions as Lua scripts #86
|
||||
* UI / API Errors Truncated, Time Out #1386
|
||||
* UI Enhancement Proposals Quick Wins #1274
|
||||
* Update argocd-util import/export to support proper backup and restore (#1328)
|
||||
* Whitelisting repos/clusters in projects should consider repo/cluster permissions #1432
|
||||
* Adds support for configuring repo creds at a domain/org level. (#1332)
|
||||
* Implement whitelist option analogous to `resource.exclusions` (#1490)
|
||||
* Added ability to sync specific labels from the command line (#1241)
|
||||
* Improve rendering app image information (#1552)
|
||||
* Add liveness probe to repo server/api servers (#1546)
|
||||
* Support configuring requested OIDC provider scopes and enforced RBAC scopes (#1471)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- "bind: address already in use" after switching to gRPC-Web #1451
|
||||
- Annoying warning while using `--grpc-web` flag #1420
|
||||
- Don't compare secrets in the CLI, since argo-cd doesn't have access to their data (#1459)
|
||||
- Dropdown menu should not have sync item for unmanaged resources #1357
|
||||
- Fixes goroutine leak. Closes #1381 (#1457)
|
||||
- Improve input style #1217
|
||||
- Issue #1389 - Fix null pointer exception in secret normalization function (#1428)
|
||||
- Issue #1425 - Argo CD should not delete CRDs (#1428)
|
||||
- Issue #1446 - Delete helm temp directories (#1449)
|
||||
- Issue #908 - Surface Service/Ingress external IPs, hostname to application (#1347)
|
||||
- kustomization fields are all mandatory #1504
|
||||
- Resource node details is crashing if live resource is missing $1505
|
||||
- Rollback UI is not showing correct ksonnet parameters in preview #1326
|
||||
- See details of applications fails with "r.nodes is undefined" #1371
|
||||
- Unable to create app from private repo: x509: certificate signed by unknown authority #1171
|
||||
- UI fails to load custom actions is resource is not deployed #1502
|
||||
- Unable to create app from private repo: x509: certificate signed by unknown authority (#1171)
|
||||
- Fix hardcoded 'git' user in `util/git.NewClient` (#1555)
|
||||
- Application controller becomes unresponsive (#1476)
|
||||
- Load target resource using K8S if conversion fails (#1414)
|
||||
- Can't ignore a non-existent pointer anymore (#1586)
|
||||
- Impossible to sync to HEAD from UI if auto-sync is enabled (#1579)
|
||||
- Application controller is unable to delete self-referenced app (#1570)
|
||||
- Prevent reconciliation loop for self-managed apps (#1533)
|
||||
- Controller incorrectly report health state of self managed application (#1557)
|
||||
- Fix kustomize manifest generation crash is manifest has image without version (#1540)
|
||||
- Supply resourceVersion to watch request to prevent reading of stale cache (#1605)
|
||||
|
||||
## v0.12.2 (2019-04-22)
|
||||
|
||||
|
||||
138
Dockerfile
@@ -1,12 +1,19 @@
|
||||
ARG BASE_IMAGE=debian:10-slim
|
||||
####################################################################################################
|
||||
# Builder image
|
||||
# Initial stage which pulls prepares build dependencies and CLI tooling we need for our final image
|
||||
# Also used as the image in CI jobs so needs all dependencies
|
||||
####################################################################################################
|
||||
FROM golang:1.11.4 as builder
|
||||
FROM golang:1.14.1 as builder
|
||||
|
||||
RUN echo 'deb http://deb.debian.org/debian buster-backports main' >> /etc/apt/sources.list
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
openssh-server \
|
||||
nginx \
|
||||
fcgiwrap \
|
||||
git \
|
||||
git-lfs \
|
||||
make \
|
||||
wget \
|
||||
gcc \
|
||||
@@ -16,102 +23,54 @@ RUN apt-get update && apt-get install -y \
|
||||
|
||||
WORKDIR /tmp
|
||||
|
||||
# Install docker
|
||||
ENV DOCKER_CHANNEL stable
|
||||
ENV DOCKER_VERSION 18.09.1
|
||||
RUN wget -O docker.tgz "https://download.docker.com/linux/static/${DOCKER_CHANNEL}/x86_64/docker-${DOCKER_VERSION}.tgz" && \
|
||||
tar --extract --file docker.tgz --strip-components 1 --directory /usr/local/bin/ && \
|
||||
rm docker.tgz
|
||||
ADD hack/install.sh .
|
||||
ADD hack/installers installers
|
||||
|
||||
# Install dep
|
||||
ENV DEP_VERSION=0.5.0
|
||||
RUN wget https://github.com/golang/dep/releases/download/v${DEP_VERSION}/dep-linux-amd64 -O /usr/local/bin/dep && \
|
||||
chmod +x /usr/local/bin/dep
|
||||
|
||||
# Install gometalinter
|
||||
ENV GOMETALINTER_VERSION=2.0.12
|
||||
RUN curl -sLo- https://github.com/alecthomas/gometalinter/releases/download/v${GOMETALINTER_VERSION}/gometalinter-${GOMETALINTER_VERSION}-linux-amd64.tar.gz | \
|
||||
tar -xzC "$GOPATH/bin" --exclude COPYING --exclude README.md --strip-components 1 -f- && \
|
||||
ln -s $GOPATH/bin/gometalinter $GOPATH/bin/gometalinter.v2
|
||||
|
||||
# Install packr
|
||||
ENV PACKR_VERSION=1.21.9
|
||||
RUN wget https://github.com/gobuffalo/packr/releases/download/v${PACKR_VERSION}/packr_${PACKR_VERSION}_linux_amd64.tar.gz && \
|
||||
tar -vxf packr*.tar.gz -C /tmp/ && \
|
||||
mv /tmp/packr /usr/local/bin/packr
|
||||
|
||||
# Install kubectl
|
||||
# NOTE: keep the version synced with https://storage.googleapis.com/kubernetes-release/release/stable.txt
|
||||
ENV KUBECTL_VERSION=1.14.0
|
||||
RUN curl -L -o /usr/local/bin/kubectl -LO https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl && \
|
||||
chmod +x /usr/local/bin/kubectl && \
|
||||
kubectl version --client
|
||||
|
||||
# Install ksonnet
|
||||
ENV KSONNET_VERSION=0.13.1
|
||||
RUN wget https://github.com/ksonnet/ksonnet/releases/download/v${KSONNET_VERSION}/ks_${KSONNET_VERSION}_linux_amd64.tar.gz && \
|
||||
tar -C /tmp/ -xf ks_${KSONNET_VERSION}_linux_amd64.tar.gz && \
|
||||
mv /tmp/ks_${KSONNET_VERSION}_linux_amd64/ks /usr/local/bin/ks && \
|
||||
ks version
|
||||
|
||||
# Install helm
|
||||
ENV HELM_VERSION=2.12.1
|
||||
RUN wget https://storage.googleapis.com/kubernetes-helm/helm-v${HELM_VERSION}-linux-amd64.tar.gz && \
|
||||
tar -C /tmp/ -xf helm-v${HELM_VERSION}-linux-amd64.tar.gz && \
|
||||
mv /tmp/linux-amd64/helm /usr/local/bin/helm && \
|
||||
helm version --client
|
||||
|
||||
# Install kustomize
|
||||
ENV KUSTOMIZE1_VERSION=1.0.11
|
||||
RUN curl -L -o /usr/local/bin/kustomize1 https://github.com/kubernetes-sigs/kustomize/releases/download/v${KUSTOMIZE1_VERSION}/kustomize_${KUSTOMIZE1_VERSION}_linux_amd64 && \
|
||||
chmod +x /usr/local/bin/kustomize1 && \
|
||||
kustomize1 version
|
||||
|
||||
|
||||
ENV KUSTOMIZE_VERSION=2.0.3
|
||||
RUN curl -L -o /usr/local/bin/kustomize https://github.com/kubernetes-sigs/kustomize/releases/download/v${KUSTOMIZE_VERSION}/kustomize_${KUSTOMIZE_VERSION}_linux_amd64 && \
|
||||
chmod +x /usr/local/bin/kustomize && \
|
||||
kustomize version
|
||||
|
||||
# Install AWS IAM Authenticator
|
||||
ENV AWS_IAM_AUTHENTICATOR_VERSION=0.4.0-alpha.1
|
||||
RUN curl -L -o /usr/local/bin/aws-iam-authenticator https://github.com/kubernetes-sigs/aws-iam-authenticator/releases/download/${AWS_IAM_AUTHENTICATOR_VERSION}/aws-iam-authenticator_${AWS_IAM_AUTHENTICATOR_VERSION}_linux_amd64 && \
|
||||
chmod +x /usr/local/bin/aws-iam-authenticator
|
||||
|
||||
# Install golangci-lint
|
||||
RUN wget https://install.goreleaser.com/github.com/golangci/golangci-lint.sh && \
|
||||
chmod +x ./golangci-lint.sh && \
|
||||
./golangci-lint.sh -b $GOPATH/bin && \
|
||||
golangci-lint linters
|
||||
|
||||
COPY .golangci.yml ${GOPATH}/src/dummy/.golangci.yml
|
||||
|
||||
RUN cd ${GOPATH}/src/dummy && \
|
||||
touch dummy.go \
|
||||
golangci-lint run
|
||||
RUN ./install.sh dep-linux
|
||||
RUN ./install.sh packr-linux
|
||||
RUN ./install.sh kubectl-linux
|
||||
RUN ./install.sh ksonnet-linux
|
||||
RUN ./install.sh helm2-linux
|
||||
RUN ./install.sh helm-linux
|
||||
RUN ./install.sh kustomize-linux
|
||||
|
||||
####################################################################################################
|
||||
# Argo CD Base - used as the base for both the release and dev argocd images
|
||||
####################################################################################################
|
||||
FROM debian:9.5-slim as argocd-base
|
||||
FROM $BASE_IMAGE as argocd-base
|
||||
|
||||
USER root
|
||||
|
||||
RUN echo 'deb http://deb.debian.org/debian buster-backports main' >> /etc/apt/sources.list
|
||||
|
||||
RUN groupadd -g 999 argocd && \
|
||||
useradd -r -u 999 -g argocd argocd && \
|
||||
mkdir -p /home/argocd && \
|
||||
chown argocd:argocd /home/argocd && \
|
||||
chown argocd:0 /home/argocd && \
|
||||
chmod g=u /home/argocd && \
|
||||
chmod g=u /etc/passwd && \
|
||||
apt-get update && \
|
||||
apt-get install -y git && \
|
||||
apt-get install -y git git-lfs python3-pip && \
|
||||
apt-get clean && \
|
||||
pip3 install awscli==1.18.80 && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
COPY hack/ssh_known_hosts /etc/ssh/ssh_known_hosts
|
||||
COPY hack/git-ask-pass.sh /usr/local/bin/git-ask-pass.sh
|
||||
COPY --from=builder /usr/local/bin/ks /usr/local/bin/ks
|
||||
COPY --from=builder /usr/local/bin/helm2 /usr/local/bin/helm2
|
||||
COPY --from=builder /usr/local/bin/helm /usr/local/bin/helm
|
||||
COPY --from=builder /usr/local/bin/kubectl /usr/local/bin/kubectl
|
||||
COPY --from=builder /usr/local/bin/kustomize1 /usr/local/bin/kustomize1
|
||||
COPY --from=builder /usr/local/bin/kustomize /usr/local/bin/kustomize
|
||||
COPY --from=builder /usr/local/bin/aws-iam-authenticator /usr/local/bin/aws-iam-authenticator
|
||||
# script to add current (possibly arbitrary) user to /etc/passwd at runtime
|
||||
# (if it's not already there, to be openshift friendly)
|
||||
COPY uid_entrypoint.sh /usr/local/bin/uid_entrypoint.sh
|
||||
|
||||
# support for mounting configuration from a configmap
|
||||
RUN mkdir -p /app/config/ssh && \
|
||||
touch /app/config/ssh/ssh_known_hosts && \
|
||||
ln -s /app/config/ssh/ssh_known_hosts /etc/ssh/ssh_known_hosts
|
||||
|
||||
RUN mkdir -p /app/config/tls
|
||||
|
||||
# workaround ksonnet issue https://github.com/ksonnet/ksonnet/issues/298
|
||||
ENV USER=argocd
|
||||
@@ -119,11 +78,26 @@ ENV USER=argocd
|
||||
USER argocd
|
||||
WORKDIR /home/argocd
|
||||
|
||||
####################################################################################################
|
||||
# Argo CD UI stage
|
||||
####################################################################################################
|
||||
FROM node:11.15.0 as argocd-ui
|
||||
|
||||
WORKDIR /src
|
||||
ADD ["ui/package.json", "ui/yarn.lock", "./"]
|
||||
|
||||
RUN yarn install
|
||||
|
||||
ADD ["ui/", "."]
|
||||
|
||||
ARG ARGO_VERSION=latest
|
||||
ENV ARGO_VERSION=$ARGO_VERSION
|
||||
RUN NODE_ENV='production' yarn build
|
||||
|
||||
####################################################################################################
|
||||
# Argo CD Build stage which performs the actual build of Argo CD binaries
|
||||
####################################################################################################
|
||||
FROM golang:1.11.4 as argocd-build
|
||||
FROM golang:1.14.1 as argocd-build
|
||||
|
||||
COPY --from=builder /usr/local/bin/dep /usr/local/bin/dep
|
||||
COPY --from=builder /usr/local/bin/packr /usr/local/bin/packr
|
||||
@@ -142,7 +116,8 @@ RUN cd ${GOPATH}/src/dummy && \
|
||||
WORKDIR /go/src/github.com/argoproj/argo-cd
|
||||
COPY . .
|
||||
RUN make cli server controller repo-server argocd-util && \
|
||||
make CLI_NAME=argocd-darwin-amd64 GOOS=darwin cli
|
||||
make CLI_NAME=argocd-darwin-amd64 GOOS=darwin cli && \
|
||||
make CLI_NAME=argocd-windows-amd64.exe GOOS=windows cli
|
||||
|
||||
|
||||
####################################################################################################
|
||||
@@ -150,3 +125,4 @@ RUN make cli server controller repo-server argocd-util && \
|
||||
####################################################################################################
|
||||
FROM argocd-base
|
||||
COPY --from=argocd-build /go/src/github.com/argoproj/argo-cd/dist/argocd* /usr/local/bin/
|
||||
COPY --from=argocd-ui ./src/dist/app /shared/app
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
####################################################################################################
|
||||
FROM argocd-base
|
||||
COPY argocd* /usr/local/bin/
|
||||
COPY --from=argocd-ui ./src/dist/app /shared/app
|
||||
|
||||
686
Gopkg.lock
generated
80
Gopkg.toml
@@ -19,7 +19,7 @@ required = [
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gogo/protobuf"
|
||||
version = "1.1.1"
|
||||
version = "1.3.1"
|
||||
|
||||
# override github.com/grpc-ecosystem/go-grpc-middleware's constraint on master
|
||||
[[override]]
|
||||
@@ -35,21 +35,58 @@ required = [
|
||||
name = "github.com/prometheus/client_golang"
|
||||
revision = "7858729281ec582767b20e0d696b6041d995d5e0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "release-1.12"
|
||||
[[override]]
|
||||
branch = "release-1.16"
|
||||
name = "k8s.io/api"
|
||||
|
||||
[[constraint]]
|
||||
branch = "release-1.12"
|
||||
[[override]]
|
||||
branch = "release-1.16"
|
||||
name = "k8s.io/kubernetes"
|
||||
|
||||
[[override]]
|
||||
branch = "release-1.16"
|
||||
name = "k8s.io/code-generator"
|
||||
|
||||
[[constraint]]
|
||||
branch = "release-9.0"
|
||||
[[override]]
|
||||
branch = "release-1.16"
|
||||
name = "k8s.io/apimachinery"
|
||||
|
||||
[[override]]
|
||||
branch = "release-1.16"
|
||||
name = "k8s.io/apiextensions-apiserver"
|
||||
|
||||
[[override]]
|
||||
branch = "release-1.16"
|
||||
name = "k8s.io/apiserver"
|
||||
|
||||
[[override]]
|
||||
branch = "release-1.16"
|
||||
name = "k8s.io/kubectl"
|
||||
|
||||
[[override]]
|
||||
branch = "release-1.16"
|
||||
name = "k8s.io/cli-runtime"
|
||||
|
||||
[[override]]
|
||||
version = "2.0.3"
|
||||
name = "sigs.k8s.io/kustomize"
|
||||
|
||||
# ASCIIRenderer does not implement blackfriday.Renderer
|
||||
[[override]]
|
||||
name = "github.com/russross/blackfriday"
|
||||
version = "1.5.2"
|
||||
|
||||
[[override]]
|
||||
branch = "release-13.0"
|
||||
name = "k8s.io/client-go"
|
||||
|
||||
[[override]]
|
||||
name = "github.com/casbin/casbin"
|
||||
version = "1.9.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "1.2.2"
|
||||
version = "1.5.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gobuffalo/packr"
|
||||
@@ -63,6 +100,31 @@ required = [
|
||||
branch = "master"
|
||||
name = "github.com/yudai/gojsondiff"
|
||||
|
||||
# Fixes: Could not introduce sigs.k8s.io/kustomize@v2.0.3, as it has a dependency on github.com/spf13/cobra with constraint ^0.0.2, which has no overlap with existing constraint 0.0.5 from (root)
|
||||
[[override]]
|
||||
revision = "master"
|
||||
name = "github.com/spf13/cobra"
|
||||
revision = "0.0.5"
|
||||
|
||||
# TODO: move off of k8s.io/kube-openapi and use controller-tools for CRD spec generation
|
||||
# (override argoproj/argo contraint on master)
|
||||
[[override]]
|
||||
revision = "30be4d16710ac61bce31eb28a01054596fe6a9f1"
|
||||
name = "k8s.io/kube-openapi"
|
||||
|
||||
# jsonpatch replace operation does not apply: doc is missing key: /metadata/annotations
|
||||
[[override]]
|
||||
name = "github.com/evanphx/json-patch"
|
||||
version = "v4.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/google/uuid"
|
||||
version = "1.1.1"
|
||||
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/alicebob/miniredis"
|
||||
version = "2.7.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/bsm/redislock"
|
||||
version = "0.4.3"
|
||||
|
||||
325
Makefile
@@ -1,4 +1,4 @@
|
||||
PACKAGE=github.com/argoproj/argo-cd
|
||||
PACKAGE=github.com/argoproj/argo-cd/common
|
||||
CURRENT_DIR=$(shell pwd)
|
||||
DIST_DIR=${CURRENT_DIR}/dist
|
||||
CLI_NAME=argocd
|
||||
@@ -9,15 +9,93 @@ GIT_COMMIT=$(shell git rev-parse HEAD)
|
||||
GIT_TAG=$(shell if [ -z "`git status --porcelain`" ]; then git describe --exact-match --tags HEAD 2>/dev/null; fi)
|
||||
GIT_TREE_STATE=$(shell if [ -z "`git status --porcelain`" ]; then echo "clean" ; else echo "dirty"; fi)
|
||||
PACKR_CMD=$(shell if [ "`which packr`" ]; then echo "packr"; else echo "go run vendor/github.com/gobuffalo/packr/packr/main.go"; fi)
|
||||
TEST_CMD=$(shell [ "`which gotestsum`" != "" ] && echo gotestsum -- || echo go test)
|
||||
VOLUME_MOUNT=$(shell if test "$(go env GOOS)"=="darwin"; then echo ":delegated"; elif test selinuxenabled; then echo ":Z"; else echo ""; fi)
|
||||
|
||||
GOPATH?=$(shell if test -x `which go`; then go env GOPATH; else echo "$(HOME)/go"; fi)
|
||||
GOCACHE?=$(HOME)/.cache/go-build
|
||||
|
||||
DOCKER_SRCDIR?=$(GOPATH)/src
|
||||
DOCKER_WORKDIR?=/go/src/github.com/argoproj/argo-cd
|
||||
|
||||
ARGOCD_PROCFILE?=Procfile
|
||||
|
||||
# Configuration for building argocd-test-tools image
|
||||
TEST_TOOLS_NAMESPACE?=argoproj
|
||||
TEST_TOOLS_IMAGE=argocd-test-tools
|
||||
TEST_TOOLS_TAG?=v0.5.0
|
||||
ifdef TEST_TOOLS_NAMESPACE
|
||||
TEST_TOOLS_PREFIX=${TEST_TOOLS_NAMESPACE}/
|
||||
endif
|
||||
|
||||
# You can change the ports where ArgoCD components will be listening on by
|
||||
# setting the appropriate environment variables before running make.
|
||||
ARGOCD_E2E_APISERVER_PORT?=8080
|
||||
ARGOCD_E2E_REPOSERVER_PORT?=8081
|
||||
ARGOCD_E2E_REDIS_PORT?=6379
|
||||
ARGOCD_E2E_DEX_PORT?=5556
|
||||
ARGOCD_E2E_YARN_HOST?=localhost
|
||||
|
||||
ARGOCD_IN_CI?=false
|
||||
ARGOCD_TEST_E2E?=true
|
||||
|
||||
ARGOCD_LINT_GOGC?=20
|
||||
|
||||
# Runs any command in the argocd-test-utils container in server mode
|
||||
# Server mode container will start with uid 0 and drop privileges during runtime
|
||||
define run-in-test-server
|
||||
docker run --rm -it \
|
||||
--name argocd-test-server \
|
||||
-e USER_ID=$(shell id -u) \
|
||||
-e HOME=/home/user \
|
||||
-e GOPATH=/go \
|
||||
-e GOCACHE=/tmp/go-build-cache \
|
||||
-e ARGOCD_IN_CI=$(ARGOCD_IN_CI) \
|
||||
-e ARGOCD_E2E_TEST=$(ARGOCD_E2E_TEST) \
|
||||
-e ARGOCD_E2E_YARN_HOST=$(ARGOCD_E2E_YARN_HOST) \
|
||||
-v ${DOCKER_SRCDIR}:/go/src${VOLUME_MOUNT} \
|
||||
-v ${GOCACHE}:/tmp/go-build-cache${VOLUME_MOUNT} \
|
||||
-v ${HOME}/.kube:/home/user/.kube${VOLUME_MOUNT} \
|
||||
-v /tmp:/tmp${VOLUME_MOUNT} \
|
||||
-w ${DOCKER_WORKDIR} \
|
||||
-p ${ARGOCD_E2E_APISERVER_PORT}:8080 \
|
||||
-p 4000:4000 \
|
||||
$(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE):$(TEST_TOOLS_TAG) \
|
||||
bash -c "$(1)"
|
||||
endef
|
||||
|
||||
# Runs any command in the argocd-test-utils container in client mode
|
||||
define run-in-test-client
|
||||
docker run --rm -it \
|
||||
--name argocd-test-client \
|
||||
-u $(shell id -u) \
|
||||
-e HOME=/home/user \
|
||||
-e GOPATH=/go \
|
||||
-e ARGOCD_E2E_K3S=$(ARGOCD_E2E_K3S) \
|
||||
-e GOCACHE=/tmp/go-build-cache \
|
||||
-e ARGOCD_LINT_GOGC=$(ARGOCD_LINT_GOGC) \
|
||||
-v ${DOCKER_SRCDIR}:/go/src${VOLUME_MOUNT} \
|
||||
-v ${GOCACHE}:/tmp/go-build-cache${VOLUME_MOUNT} \
|
||||
-v ${HOME}/.kube:/home/user/.kube${VOLUME_MOUNT} \
|
||||
-v /tmp:/tmp${VOLUME_MOUNT} \
|
||||
-w ${DOCKER_WORKDIR} \
|
||||
$(TEST_TOOLS_NAMESPACE)/$(TEST_TOOLS_IMAGE):$(TEST_TOOLS_TAG) \
|
||||
bash -c "$(1)"
|
||||
endef
|
||||
|
||||
#
|
||||
define exec-in-test-server
|
||||
docker exec -it -u $(shell id -u) -e ARGOCD_E2E_K3S=$(ARGOCD_E2E_K3S) argocd-test-server $(1)
|
||||
endef
|
||||
|
||||
PATH:=$(PATH):$(PWD)/hack
|
||||
|
||||
# docker image publishing options
|
||||
DOCKER_PUSH=false
|
||||
IMAGE_TAG=latest
|
||||
DOCKER_PUSH?=false
|
||||
IMAGE_NAMESPACE?=
|
||||
# perform static compilation
|
||||
STATIC_BUILD=true
|
||||
STATIC_BUILD?=true
|
||||
# build development images
|
||||
DEV_IMAGE=false
|
||||
DEV_IMAGE?=false
|
||||
|
||||
override LDFLAGS += \
|
||||
-X ${PACKAGE}.version=${VERSION} \
|
||||
@@ -32,6 +110,8 @@ endif
|
||||
ifneq (${GIT_TAG},)
|
||||
IMAGE_TAG=${GIT_TAG}
|
||||
LDFLAGS += -X ${PACKAGE}.gitTag=${GIT_TAG}
|
||||
else
|
||||
IMAGE_TAG?=latest
|
||||
endif
|
||||
|
||||
ifeq (${DOCKER_PUSH},true)
|
||||
@@ -47,6 +127,10 @@ endif
|
||||
.PHONY: all
|
||||
all: cli image argocd-util
|
||||
|
||||
.PHONY: gogen
|
||||
gogen:
|
||||
go generate ./util/argo/...
|
||||
|
||||
.PHONY: protogen
|
||||
protogen:
|
||||
./hack/generate-proto.sh
|
||||
@@ -59,38 +143,61 @@ openapigen:
|
||||
clientgen:
|
||||
./hack/update-codegen.sh
|
||||
|
||||
.PHONY: codegen-local
|
||||
codegen-local: gogen protogen clientgen openapigen manifests-local
|
||||
|
||||
.PHONY: codegen
|
||||
codegen: protogen clientgen openapigen
|
||||
codegen:
|
||||
$(call run-in-test-client,make codegen-local)
|
||||
|
||||
.PHONY: cli
|
||||
cli: clean-debug
|
||||
CGO_ENABLED=0 ${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/${CLI_NAME} ./cmd/argocd
|
||||
|
||||
.PHONY: cli-docker
|
||||
go build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/${CLI_NAME} ./cmd/argocd
|
||||
|
||||
.PHONY: release-cli
|
||||
release-cli: clean-debug image
|
||||
docker create --name tmp-argocd-linux $(IMAGE_PREFIX)argocd:$(IMAGE_TAG)
|
||||
docker cp tmp-argocd-linux:/usr/local/bin/argocd ${DIST_DIR}/argocd-linux-amd64
|
||||
docker cp tmp-argocd-linux:/usr/local/bin/argocd-darwin-amd64 ${DIST_DIR}/argocd-darwin-amd64
|
||||
docker cp tmp-argocd-linux:/usr/local/bin/argocd-windows-amd64.exe ${DIST_DIR}/argocd-windows-amd64.exe
|
||||
docker rm tmp-argocd-linux
|
||||
|
||||
.PHONY: argocd-util
|
||||
argocd-util: clean-debug
|
||||
# Build argocd-util as a statically linked binary, so it could run within the alpine-based dex container (argoproj/argo-cd#844)
|
||||
CGO_ENABLED=0 go build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-util ./cmd/argocd-util
|
||||
CGO_ENABLED=0 ${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-util ./cmd/argocd-util
|
||||
|
||||
# .PHONY: dev-tools-image
|
||||
# dev-tools-image:
|
||||
# docker build -t $(DEV_TOOLS_PREFIX)$(DEV_TOOLS_IMAGE) . -f hack/Dockerfile.dev-tools
|
||||
# docker tag $(DEV_TOOLS_PREFIX)$(DEV_TOOLS_IMAGE) $(DEV_TOOLS_PREFIX)$(DEV_TOOLS_IMAGE):$(DEV_TOOLS_VERSION)
|
||||
|
||||
.PHONY: test-tools-image
|
||||
test-tools-image:
|
||||
docker build -t $(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE) -f test/container/Dockerfile .
|
||||
docker tag $(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE) $(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE):$(TEST_TOOLS_TAG)
|
||||
|
||||
.PHONY: manifests-local
|
||||
manifests-local:
|
||||
./hack/update-manifests.sh
|
||||
|
||||
.PHONY: manifests
|
||||
manifests:
|
||||
./hack/update-manifests.sh
|
||||
manifests: test-tools-image
|
||||
$(call run-in-test-client,make manifests-local IMAGE_TAG='${IMAGE_TAG}')
|
||||
|
||||
|
||||
# NOTE: we use packr to do the build instead of go, since we embed swagger files and policy.csv
|
||||
# files into the go binary
|
||||
.PHONY: server
|
||||
server: clean-debug
|
||||
CGO_ENABLED=0 ${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-server ./cmd/argocd-server
|
||||
|
||||
|
||||
.PHONY: repo-server
|
||||
repo-server:
|
||||
CGO_ENABLED=0 go build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-repo-server ./cmd/argocd-repo-server
|
||||
CGO_ENABLED=0 ${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-repo-server ./cmd/argocd-repo-server
|
||||
|
||||
.PHONY: controller
|
||||
controller:
|
||||
@@ -105,14 +212,17 @@ ifeq ($(DEV_IMAGE), true)
|
||||
# The "dev" image builds the binaries from the users desktop environment (instead of in Docker)
|
||||
# which speeds up builds. Dockerfile.dev needs to be copied into dist to perform the build, since
|
||||
# the dist directory is under .dockerignore.
|
||||
IMAGE_TAG="dev-$(shell git describe --always --dirty)"
|
||||
image: packr
|
||||
docker build -t argocd-base --target argocd-base .
|
||||
docker build -t argocd-ui --target argocd-ui .
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-server ./cmd/argocd-server
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-application-controller ./cmd/argocd-application-controller
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-repo-server ./cmd/argocd-repo-server
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-util ./cmd/argocd-util
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd ./cmd/argocd
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-darwin-amd64 ./cmd/argocd
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-windows-amd64.exe ./cmd/argocd
|
||||
cp Dockerfile.dev dist
|
||||
docker build -t $(IMAGE_PREFIX)argocd:$(IMAGE_TAG) -f dist/Dockerfile.dev dist
|
||||
else
|
||||
@@ -124,34 +234,134 @@ endif
|
||||
.PHONY: builder-image
|
||||
builder-image:
|
||||
docker build -t $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG) --target builder .
|
||||
docker push $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG)
|
||||
@if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG) ; fi
|
||||
|
||||
# Pulls in all vendor dependencies
|
||||
.PHONY: dep
|
||||
dep:
|
||||
$(call run-in-test-client,dep ensure -v)
|
||||
|
||||
# Pulls in all vendor dependencies (local version)
|
||||
.PHONY: dep-local
|
||||
dep-local:
|
||||
dep ensure -v
|
||||
|
||||
# Pulls in all unvendored dependencies
|
||||
.PHONY: dep-ensure
|
||||
dep-ensure:
|
||||
$(call run-in-test-client,dep ensure -no-vendor -v)
|
||||
|
||||
# Pulls in all unvendored dependencies (local version)
|
||||
.PHONY: dep-ensure-local
|
||||
dep-ensure-local:
|
||||
dep ensure -no-vendor
|
||||
|
||||
# Runs dep check in a container to ensure Gopkg.lock is up-to-date with dependencies
|
||||
.PHONY: dep-check
|
||||
dep-check:
|
||||
$(call run-in-test-client,make dep-check-local)
|
||||
|
||||
# Runs dep check locally to ensure Gopkg.lock is up-to-date with dependencies
|
||||
.PHONY: dep-check-local
|
||||
dep-check-local:
|
||||
if ! dep check -skip-vendor; then echo "Please make sure Gopkg.lock is up-to-date - see https://argoproj.github.io/argo-cd/developer-guide/faq/#why-does-the-build-step-fail"; exit 1; fi
|
||||
|
||||
# Deprecated - replace by install-local-tools
|
||||
.PHONY: install-lint-tools
|
||||
install-lint-tools:
|
||||
./hack/install.sh lint-tools
|
||||
|
||||
# Run linter on the code
|
||||
.PHONY: lint
|
||||
lint:
|
||||
golangci-lint run --fix
|
||||
$(call run-in-test-client,make lint-local)
|
||||
|
||||
# Run linter on the code (local version)
|
||||
.PHONY: lint-local
|
||||
lint-local:
|
||||
golangci-lint --version
|
||||
# NOTE: If you get a "Killed" OOM message, try reducing the value of GOGC
|
||||
# See https://github.com/golangci/golangci-lint#memory-usage-of-golangci-lint
|
||||
GOGC=$(ARGOCD_LINT_GOGC) GOMAXPROCS=2 golangci-lint run --fix --verbose --timeout 300s
|
||||
|
||||
.PHONY: lint-ui
|
||||
lint-ui:
|
||||
$(call run-in-test-client,make lint-ui-local)
|
||||
|
||||
.PHONY: lint-ui-local
|
||||
lint-ui-local:
|
||||
cd ui && yarn lint
|
||||
|
||||
# Build all Go code
|
||||
.PHONY: build
|
||||
build:
|
||||
go build `go list ./... | grep -v resource_customizations`
|
||||
mkdir -p $(GOCACHE)
|
||||
$(call run-in-test-client, make build-local)
|
||||
|
||||
# Build all Go code (local version)
|
||||
.PHONY: build-local
|
||||
build-local:
|
||||
go build -p 1 -v `go list ./... | grep -v 'resource_customizations\|test/e2e'`
|
||||
|
||||
# Run all unit tests
|
||||
#
|
||||
# If TEST_MODULE is set (to fully qualified module name), only this specific
|
||||
# module will be tested.
|
||||
.PHONY: test
|
||||
test:
|
||||
$(TEST_CMD) -covermode=count -coverprofile=coverage.out `go list ./... | grep -v "github.com/argoproj/argo-cd/test/e2e"`
|
||||
mkdir -p $(GOCACHE)
|
||||
$(call run-in-test-client,make TEST_MODULE=$(TEST_MODULE) test-local)
|
||||
|
||||
# Run all unit tests (local version)
|
||||
.PHONY: test-local
|
||||
test-local:
|
||||
if test "$(TEST_MODULE)" = ""; then \
|
||||
./hack/test.sh -coverprofile=coverage.out `go list ./... | grep -v 'test/e2e'`; \
|
||||
else \
|
||||
./hack/test.sh -coverprofile=coverage.out "$(TEST_MODULE)"; \
|
||||
fi
|
||||
|
||||
# Run the E2E test suite. E2E test servers (see start-e2e target) must be
|
||||
# started before.
|
||||
.PHONY: test-e2e
|
||||
test-e2e: cli
|
||||
$(TEST_CMD) -v -failfast -timeout 20m ./test/e2e
|
||||
test-e2e:
|
||||
$(call exec-in-test-server,make test-e2e-local)
|
||||
|
||||
# Run the E2E test suite (local version)
|
||||
.PHONY: test-e2e-local
|
||||
test-e2e-local: cli
|
||||
# NO_PROXY ensures all tests don't go out through a proxy if one is configured on the test system
|
||||
NO_PROXY=* ./hack/test.sh -timeout 15m -v ./test/e2e
|
||||
|
||||
# Spawns a shell in the test server container for debugging purposes
|
||||
debug-test-server:
|
||||
$(call run-in-test-server,/bin/bash)
|
||||
|
||||
# Spawns a shell in the test client container for debugging purposes
|
||||
debug-test-client:
|
||||
$(call run-in-test-client,/bin/bash)
|
||||
|
||||
# Starts e2e server in a container
|
||||
.PHONY: start-e2e
|
||||
start-e2e: cli
|
||||
start-e2e:
|
||||
docker version
|
||||
mkdir -p ${GOCACHE}
|
||||
$(call run-in-test-server,make ARGOCD_PROCFILE=test/container/Procfile start-e2e-local)
|
||||
|
||||
# Starts e2e server locally (or within a container)
|
||||
.PHONY: start-e2e-local
|
||||
start-e2e-local:
|
||||
kubectl create ns argocd-e2e || true
|
||||
kubens argocd-e2e
|
||||
kubectl config set-context --current --namespace=argocd-e2e
|
||||
kustomize build test/manifests/base | kubectl apply -f -
|
||||
make start
|
||||
# set paths for locally managed ssh known hosts and tls certs data
|
||||
ARGOCD_SSH_DATA_PATH=/tmp/argo-e2e/app/config/ssh \
|
||||
ARGOCD_TLS_DATA_PATH=/tmp/argo-e2e/app/config/tls \
|
||||
ARGOCD_E2E_DISABLE_AUTH=false \
|
||||
ARGOCD_ZJWT_FEATURE_FLAG=always \
|
||||
ARGOCD_IN_CI=$(ARGOCD_IN_CI) \
|
||||
ARGOCD_E2E_TEST=true \
|
||||
goreman -f $(ARGOCD_PROCFILE) start
|
||||
|
||||
# Cleans VSCode debug.test files from sub-dirs to prevent them from being included in packr boxes
|
||||
.PHONY: clean-debug
|
||||
@@ -164,13 +374,28 @@ clean: clean-debug
|
||||
|
||||
.PHONY: start
|
||||
start:
|
||||
killall goreman || true
|
||||
kubens argocd
|
||||
goreman start
|
||||
docker version
|
||||
$(call run-in-test-server,make ARGOCD_PROCFILE=test/container/Procfile start-local ARGOCD_START=${ARGOCD_START})
|
||||
|
||||
# Starts a local instance of ArgoCD
|
||||
.PHONY: start-local
|
||||
start-local:
|
||||
# check we can connect to Docker to start Redis
|
||||
killall goreman || true
|
||||
kubectl create ns argocd || true
|
||||
ARGOCD_ZJWT_FEATURE_FLAG=always \
|
||||
ARGOCD_IN_CI=false \
|
||||
ARGOCD_E2E_TEST=false \
|
||||
goreman -f $(ARGOCD_PROCFILE) start ${ARGOCD_START}
|
||||
|
||||
# Runs pre-commit validaiton with the virtualized toolchain
|
||||
.PHONY: pre-commit
|
||||
pre-commit: dep-ensure codegen build lint test
|
||||
|
||||
# Runs pre-commit validation with the local toolchain
|
||||
.PHONY: pre-commit-local
|
||||
pre-commit-local: dep-ensure-local codegen-local build-local lint-local test-local
|
||||
|
||||
.PHONY: release-precheck
|
||||
release-precheck: manifests
|
||||
@if [ "$(GIT_TREE_STATE)" != "clean" ]; then echo 'git tree state is $(GIT_TREE_STATE)' ; exit 1; fi
|
||||
@@ -178,4 +403,54 @@ release-precheck: manifests
|
||||
@if [ "$(GIT_TAG)" != "v`cat VERSION`" ]; then echo 'VERSION does not match git tag'; exit 1; fi
|
||||
|
||||
.PHONY: release
|
||||
release: release-precheck precheckin image release-cli
|
||||
release: pre-commit release-precheck image release-cli
|
||||
|
||||
.PHONY: build-docs
|
||||
build-docs:
|
||||
mkdocs build
|
||||
|
||||
.PHONY: serve-docs
|
||||
serve-docs:
|
||||
mkdocs serve
|
||||
|
||||
.PHONY: lint-docs
|
||||
lint-docs:
|
||||
# https://github.com/dkhamsing/awesome_bot
|
||||
find docs -name '*.md' -exec grep -l http {} + | xargs docker run --rm -v $(PWD):/mnt:ro dkhamsing/awesome_bot -t 3 --allow-dupe --allow-redirect --white-list `cat white-list | grep -v "#" | tr "\n" ','` --skip-save-results --
|
||||
|
||||
.PHONY: publish-docs
|
||||
publish-docs: lint-docs
|
||||
mkdocs gh-deploy
|
||||
|
||||
# Verify that kubectl can connect to your K8s cluster from Docker
|
||||
.PHONY: verify-kube-connect
|
||||
verify-kube-connect:
|
||||
$(call run-in-test-client,kubectl version)
|
||||
|
||||
# Show the Go version of local and virtualized environments
|
||||
.PHONY: show-go-version
|
||||
show-go-version:
|
||||
@echo -n "Local Go version: "
|
||||
@go version
|
||||
@echo -n "Docker Go version: "
|
||||
$(call run-in-test-client,go version)
|
||||
|
||||
# Installs all tools required to build and test ArgoCD locally
|
||||
.PHONY: install-tools-local
|
||||
install-tools-local:
|
||||
./hack/install.sh dep-linux
|
||||
./hack/install.sh packr-linux
|
||||
./hack/install.sh kubectl-linux
|
||||
./hack/install.sh ksonnet-linux
|
||||
./hack/install.sh helm2-linux
|
||||
./hack/install.sh helm-linux
|
||||
./hack/install.sh codegen-tools
|
||||
./hack/install.sh codegen-go-tools
|
||||
./hack/install.sh lint-tools
|
||||
|
||||
.PHONY: dep-ui
|
||||
dep-ui:
|
||||
$(call run-in-test-client,make dep-ui-local)
|
||||
|
||||
dep-ui-local:
|
||||
cd ui && yarn install
|
||||
|
||||
6
OWNERS
@@ -3,6 +3,10 @@ owners:
|
||||
- jessesuen
|
||||
|
||||
approvers:
|
||||
- alexec
|
||||
- alexmt
|
||||
- dthomson25
|
||||
- jannfis
|
||||
- jessesuen
|
||||
- merenbach
|
||||
- mayzhang2000
|
||||
- rachelwang20
|
||||
|
||||
12
Procfile
@@ -1,5 +1,7 @@
|
||||
controller: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true go run ./cmd/argocd-application-controller/main.go --loglevel debug --redis localhost:6379 --repo-server localhost:8081"
|
||||
api-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true go run ./cmd/argocd-server/main.go --loglevel debug --redis localhost:6379 --disable-auth --insecure --dex-server http://localhost:5556 --repo-server localhost:8081 --staticassets ../argo-cd-ui/dist/app"
|
||||
repo-server: sh -c "FORCE_LOG_COLORS=1 go run ./cmd/argocd-repo-server/main.go --loglevel debug --redis localhost:6379"
|
||||
dex: sh -c "go run ./cmd/argocd-util/main.go gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p 5556:5556 -v `pwd`/dist/dex.yaml:/dex.yaml quay.io/dexidp/dex:v2.14.0 serve /dex.yaml"
|
||||
redis: docker run --rm -i -p 6379:6379 redis:5.0.3-alpine --save "" --appendonly no
|
||||
controller: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true go run ./cmd/argocd-application-controller/main.go --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081}"
|
||||
api-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true go run ./cmd/argocd-server/main.go --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --disable-auth=${ARGOCD_E2E_DISABLE_AUTH:-'true'} --insecure --dex-server http://localhost:${ARGOCD_E2E_DEX_PORT:-5556} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --port ${ARGOCD_E2E_APISERVER_PORT:-8080} --staticassets ui/dist/app"
|
||||
dex: sh -c "go run github.com/argoproj/argo-cd/cmd/argocd-util gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p ${ARGOCD_E2E_DEX_PORT:-5556}:${ARGOCD_E2E_DEX_PORT:-5556} -v `pwd`/dist/dex.yaml:/dex.yaml quay.io/dexidp/dex:v2.22.0 serve /dex.yaml"
|
||||
redis: docker run --rm --name argocd-redis -i -p ${ARGOCD_E2E_REDIS_PORT:-6379}:${ARGOCD_E2E_REDIS_PORT:-6379} redis:5.0.3-alpine --save "" --appendonly no --port ${ARGOCD_E2E_REDIS_PORT:-6379}
|
||||
repo-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true go run ./cmd/argocd-repo-server/main.go --loglevel debug --port ${ARGOCD_E2E_REPOSERVER_PORT:-8081} --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379}"
|
||||
ui: sh -c 'cd ui && ${ARGOCD_E2E_YARN_CMD:-yarn} start'
|
||||
git-server: test/fixture/testrepos/start-git.sh
|
||||
|
||||
30
README.md
@@ -1,5 +1,6 @@
|
||||
[](https://argoproj.github.io/community/join-slack)
|
||||
[](https://codecov.io/gh/argoproj/argo-cd)
|
||||
[](https://github.com/argoproj/argo-cd/releases/latest)
|
||||
|
||||
# Argo CD - Declarative Continuous Delivery for Kubernetes
|
||||
|
||||
@@ -11,22 +12,27 @@ Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes.
|
||||
|
||||
## Why Argo CD?
|
||||
|
||||
Application definitions, configurations, and environments should be declarative and version controlled.
|
||||
Application deployment and lifecycle management should be automated, auditable, and easy to understand.
|
||||
|
||||
1. Application definitions, configurations, and environments should be declarative and version controlled.
|
||||
1. Application deployment and lifecycle management should be automated, auditable, and easy to understand.
|
||||
|
||||
## Who uses Argo CD?
|
||||
|
||||
Organizations below are **officially** using Argo CD. Please send a PR with your organization name if you are using Argo CD.
|
||||
|
||||
1. [Intuit](https://www.intuit.com/)
|
||||
2. [KompiTech GmbH](https://www.kompitech.com/)
|
||||
3. [Yieldlab](https://www.yieldlab.de/)
|
||||
4. [Ticketmaster](https://ticketmaster.com)
|
||||
5. [CyberAgent](https://www.cyberagent.co.jp/en/)
|
||||
6. [OpenSaaS Studio](https://opensaas.studio)
|
||||
7. [Riskified](https://www.riskified.com/)
|
||||
[Official Argo CD user list](USERS.md)
|
||||
|
||||
## Documentation
|
||||
|
||||
To learn more about Argo CD [go to the complete documentation](https://argoproj.github.io/argo-cd/).
|
||||
Check live demo at https://cd.apps.argoproj.io/.
|
||||
|
||||
## Community Blogs and Presentations
|
||||
|
||||
1. [Tutorial: Everything You Need To Become A GitOps Ninja](https://www.youtube.com/watch?v=r50tRQjisxw) 90m tutorial on GitOps and Argo CD.
|
||||
1. [Comparison of Argo CD, Spinnaker, Jenkins X, and Tekton](https://www.inovex.de/blog/spinnaker-vs-argo-cd-vs-tekton-vs-jenkins-x/)
|
||||
1. [Simplify and Automate Deployments Using GitOps with IBM Multicloud Manager 3.1.2](https://medium.com/ibm-cloud/simplify-and-automate-deployments-using-gitops-with-ibm-multicloud-manager-3-1-2-4395af317359)
|
||||
1. [GitOps for Kubeflow using Argo CD](https://www.kubeflow.org/docs/use-cases/gitops-for-kubeflow/)
|
||||
1. [GitOps Toolsets on Kubernetes with CircleCI and Argo CD](https://www.digitalocean.com/community/tutorials/webinar-series-gitops-tool-sets-on-kubernetes-with-circleci-and-argo-cd)
|
||||
1. [Simplify and Automate Deployments Using GitOps with IBM Multicloud Manager](https://www.ibm.com/blogs/bluemix/2019/02/simplify-and-automate-deployments-using-gitops-with-ibm-multicloud-manager-3-1-2/)
|
||||
1. [CI/CD in Light Speed with K8s and Argo CD](https://www.youtube.com/watch?v=OdzH82VpMwI&feature=youtu.be)
|
||||
1. [Machine Learning as Code](https://www.youtube.com/watch?v=VXrGp5er1ZE&t=0s&index=135&list=PLj6h78yzYM2PZf9eA7bhWnIh_mK1vyOfU). Among other things, describes how Kubeflow uses Argo CD to implement GitOPs for ML
|
||||
1. [Argo CD - GitOps Continuous Delivery for Kubernetes](https://www.youtube.com/watch?v=aWDIQMbp1cc&feature=youtu.be&t=1m4s)
|
||||
1. [Introduction to Argo CD : Kubernetes DevOps CI/CD](https://www.youtube.com/watch?v=2WSJF7d8dUg&feature=youtu.be)
|
||||
|
||||
8
SECURITY_CONTACTS
Normal file
@@ -0,0 +1,8 @@
|
||||
# Defined below are the security contacts for this repo.
|
||||
#
|
||||
# DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE
|
||||
# INSTRUCTIONS AT https://argoproj.github.io/argo-cd/security_considerations/#reporting-vulnerabilities
|
||||
|
||||
alexmt
|
||||
edlee2121
|
||||
jessesuen
|
||||
60
USERS.md
Normal file
@@ -0,0 +1,60 @@
|
||||
## Who uses Argo CD?
|
||||
|
||||
As the Argo Community grows, we'd like to keep track of our users. Please send a PR with your organization name if you are using Argo CD.
|
||||
|
||||
Currently, the following organizations are **officially** using Argo CD:
|
||||
|
||||
1. [127Labs](https://127labs.com/)
|
||||
1. [Adevinta](https://www.adevinta.com/)
|
||||
1. [ANSTO - Australian Synchrotron](https://www.synchrotron.org.au/)
|
||||
1. [ARZ Allgemeines Rechenzentrum GmbH ](https://www.arz.at/)
|
||||
1. [Baloise](https://www.baloise.com)
|
||||
1. [BioBox Analytics](https://biobox.io)
|
||||
1. [CARFAX](https://www.carfax.com)
|
||||
1. [Celonis](https://www.celonis.com/)
|
||||
1. [Codility](https://www.codility.com/)
|
||||
1. [Commonbond](https://commonbond.co/)
|
||||
1. [CyberAgent](https://www.cyberagent.co.jp/en/)
|
||||
1. [Cybozu](https://cybozu-global.com)
|
||||
1. [EDF Renewables](https://www.edf-re.com/)
|
||||
1. [Elium](https://www.elium.com)
|
||||
1. [END.](https://www.endclothing.com/)
|
||||
1. [Fave](https://myfave.com)
|
||||
1. [Future PLC](https://www.futureplc.com/)
|
||||
1. [GMETRI](https://gmetri.com/)
|
||||
1. [Healy](https://www.healyworld.net)
|
||||
1. [hipages](https://hipages.com.au/)
|
||||
1. [Intuit](https://www.intuit.com/)
|
||||
1. [KintoHub](https://www.kintohub.com/)
|
||||
1. [KompiTech GmbH](https://www.kompitech.com/)
|
||||
1. [Lytt](https://www.lytt.co/)
|
||||
1. [Major League Baseball](https://mlb.com)
|
||||
1. [Mambu](https://www.mambu.com/)
|
||||
1. [Max Kelsen](https://www.maxkelsen.com/)
|
||||
1. [Mirantis](https://mirantis.com/)
|
||||
1. [MOO Print](https://www.moo.com/)
|
||||
1. [OpenSaaS Studio](https://opensaas.studio)
|
||||
1. [Optoro](https://www.optoro.com/)
|
||||
1. [Peloton Interactive](https://www.onepeloton.com/)
|
||||
1. [Pipefy](https://www.pipefy.com/)
|
||||
1. [Prudential](https://prudential.com.sg)
|
||||
1. [Red Hat](https://www.redhat.com/)
|
||||
1. [Robotinfra](https://www.robotinfra.com)
|
||||
1. [Riskified](https://www.riskified.com/)
|
||||
1. [Saildrone](https://www.saildrone.com/)
|
||||
1. [Saloodo! GmbH](https://www.saloodo.com)
|
||||
1. [Syncier](https://syncier.com/)
|
||||
1. [Tesla](https://tesla.com/)
|
||||
1. [ThousandEyes](https://www.thousandeyes.com/)
|
||||
1. [Ticketmaster](https://ticketmaster.com)
|
||||
1. [Tiger Analytics](https://www.tigeranalytics.com/)
|
||||
1. [Twilio SendGrid](https://sendgrid.com)
|
||||
1. [tZERO](https://www.tzero.com/)
|
||||
1. [UBIO](https://ub.io/)
|
||||
1. [Universidad Mesoamericana](https://www.umes.edu.gt/)
|
||||
1. [Viaduct](https://www.viaduct.ai/)
|
||||
1. [Volvo Cars](https://www.volvocars.com/)
|
||||
1. [Walkbase](https://www.walkbase.com/)
|
||||
1. [Whitehat Berlin](https://whitehat.berlin) by Guido Maria Serra +Fenaroli
|
||||
1. [Yieldlab](https://www.yieldlab.de/)
|
||||
1. [MTN Group](https://www.mtn.com/)
|
||||
24
assets/badge.svg
Normal file
@@ -0,0 +1,24 @@
|
||||
<svg width="131" height="20" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" >
|
||||
<defs>
|
||||
<filter id="dropShadow">
|
||||
<feDropShadow dx="0.2" dy="0.4" stdDeviation="0.2" flood-color="#333" flood-opacity="0.5"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<clipPath id="roundedCorners">
|
||||
<rect width="100%" height="100%" rx="3" opacity="1" />
|
||||
</clipPath>
|
||||
|
||||
<g clip-path="url(#roundedCorners)">
|
||||
<rect id="leftRect" fill="#555" x="0" y="0" width="74" height="20" />
|
||||
<rect id="rightRect" fill="#4c1" x="74" y="0" width="57" height="20" />
|
||||
<rect id="revisionRect" fill="#4c1" x="131" y="0" width="62" height="20" display="none"/>
|
||||
</g>
|
||||
|
||||
<g fill="#fff" style="filter: url(#dropShadow);" text-anchor="middle" font-family="DejaVu Sans, sans-serif" font-size="90">
|
||||
<image x="5" y="3" width="14" height="14" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB8AAAAeCAYAAADU8sWcAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAABPAAAATwFjiv3XAAACC2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOlJlc29sdXRpb25Vbml0PjI8L3RpZmY6UmVzb2x1dGlvblVuaXQ+CiAgICAgICAgIDx0aWZmOkNvbXByZXNzaW9uPjE8L3RpZmY6Q29tcHJlc3Npb24+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDx0aWZmOlBob3RvbWV0cmljSW50ZXJwcmV0YXRpb24+MjwvdGlmZjpQaG90b21ldHJpY0ludGVycHJldGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KD0UqkwAACpZJREFUSA11VnmQFcUZ//qY4527by92WWTXwHKqEJDDg0MQUioKSWopK4oaS9RAlMofUSPGbFlBSaViKomgQSNmIdECkUS8ophdozEJArKoKCsgIFlY2Pu9N29mero7X78FxCR2Vb8309PTv+/4fb9vCHz1II2Nm+jmzYul2bJsdlMyO7LyPGlZtYqQck1ZHCgwLZWiROeJgB7bDzpYb/7o0y/emzXv4Pts8+ZGBUC0uf/vQf57wdw3NTVRnOYFfXvj6pJcadkUmbDGRoxVSEItSYAQQrUmRBP81VoRpkFTJUMuZA/3g/28q3dn89b7u885D4348vgf8NPAxY033LJmSlDizlSOU94f6UhoHaZdC/3QCHWON2gDYBhAYyRAWgyD4QSi1815f29++vv/+CoD2Lm2nAFGl8mndzyxMMgk5/iW6xzPi/xVk+oyE8+vLNt14FR/0uYW14ox0IwUJ4ZAaUpAaaBEYByiyLbikWWNnnThvPIxu+L717auVeb81tbWsyk4C34u8I3LnlhcSCameMzykg7TwzOJGIYWQj+M+voLYcSo/hxY1E65FECUq4G4RFP0nSjMCMVAIIKUnOHkw90L6qq/vXvbh02trV8yoBh2E0NMY9GiG+58fIGXTF6epyw7JOnamZQbz/tCnDrVVzgqqdwTT0ZTdWDN0wPpMh3ZPZqHLSw98C7Y4RwtnAodsTwGAg0ppkcxrlkYJFID+Z0bn/zelsFImzQRXfScNJFiOG689dcT/FT6GzlqedVJTHRpPH6yz/PyfTlx2IrLCpvSdWrv5OXkX9fOJB/Mnaz3XzItap+yMDoyZgEE7F1SceIghryeRNwnTBkEqhRIxiLF6bCpDXNybW2v/ruxcTzbt2+zZmfCvWT+zxNhbek3cxaPlyYsXVWSSHT25rxC3o+OWK6aDn7sZ3r79ZNY28w45Esxuhw5RjmVvEQPlIwgR8bOiE7VdrPaA2+TeGEEhFbhtAEmG5pzk5WqqbWTPv7jC8sLpgxNSRWZrUeWTAgor7EY9ytTiURvzg8GskGU5xxKkc33yDcX1yc7x3ijLxF+zZgoEljgCrSiXOWIG/WGrjifHxl1V9iyeKwW1lHNo5SKmDmcYinivwhjVpkor55sQj9+/D4MO9bpLW8RN6zJzMvbPDUkHaecE368N1+IUUJfIm5hPf1w2kTedlm/LgntmqEWEQGT3ScJYUh2NIxiiWupSUFasor1VgzXvPATfv6BC3Roq0E9MLqAXKQcAe1rqi/dv+q3DwQUw6djY6tiAaG1jLEw7nAn64WhcStPqZqpA6chap8aage077GwbQcJDx8Awq3T3DHlbeijUHWwytHFuuizCTergdQ+YocxLRHVPNcESRAJxqo60nbSvExvvvWx23MCVvucDyRt5hitwDRHHG09pZiaT7MlSUdUhtQGZttYRdwA4WnFQjFnDA4LX7UdIlgMYpaouBgGMnuBKVN/ZhgBRCMVynNAHOfRm777q4UcrW3EZxcEnD/kclYlpVaFKFIZrIRtluvdls/G9InnLZ9fpLXoJjwZAzZkDJ5mjkIj8HTCKETH9oD0eoikMSDQ5cTrLrUg4QoaFlxUH5MdYy6yAFAaYBHed6ODkMPOkMcFZggZKa0SkaL/jMeDpq5D1Vez9693V/wNUg1jQRY8yLX8BcSba8AaOgyBOdqtQBz7DNj8lZCeNY9SN6aiD3bTOc89s2iliDVvKa3uGxf6doQAxYE2oBbm0HQPG5IxqKhOg9ZpSSQj+pDU+jv5t7913spV5c70GcqLJwgMGw6ZJUuBLbgHPW0D4rgQdewDvnAllN58B0D918AvzVBn3jWi/p4Hq2/s+et1URgBRlVRMGVvBtITfwyuyYPAoIRmWSIrEqjZW1Ml/qN7dtRWzptfBzXDoaeri6TicWhes8Zsg9iMOaAyU0D3fw4qdSEkrryquP77xx+HZCwG3T09HEZfCBUXTxh125H9VZ9SHp1OfZF4aEWI9MM1AhmiVCmaIwV6a2F5ge2q8s7OBB8ytPiOZduw6pFHYOS4cUUQGsNWXj4MVLYdaGU9UAQ0o2H8eHh49WpAITReKqdmGC0PsvF2JB4Gu+i5RqKg5JSi82mOnu9GYGEhwbFjSOXadJII7Pa6Ed3hwU8igAU8lU7D/ffdZ1pFEUT19YI+sR9oZgLIjo8h6usDu2oIzJpxOc4ZphKQtoqFxw6LzxIV/RMxkabSUPAJRz3A5oc50x9T7Lf3xkR0R0xGSS+IRIFQOUUUkqvGXdR5tGXnLmjbYXI0OLCJykIBvFe2ABVHgCQqgMpjkH9xE9IY7cSgGd0w+8mOVjiy68DOh+sauusiwSM8F11nlhCm39/SvP7ux4qNZVfba/0TJ183Is9opWtxEXe5E/cC+Y5bf+CilmczqaC/JigEOjjYTrwtzwB8tAn4kJFGMIEmy0B98gb4B7tAYimHncd19ObLrP25rW0/rb76pQqsoKTEkKJgoT64biCObVx35xvGGz67qYW3Nl0RWYH/ftxmo7pyfpSoKFFjEiLxhE73jXaueHXlGw+PZ68rS8pQ01QlodUjTqcAfcQo8qGjQB36EwQfbFAuF8wjZbmNtStefZ4nC0uibKIHez92AeZEUln5wocGuLGxyUbg2cVEFp5a9pFz11MH/ZCP6M0V8gmbxWZL35K2paFmUmjDgCXAMQkjqv8wgFMGBFVSCw8g1wWsrB5IBrTDAvB0eaAkhVHCeGzSQDUm3bE9/1hs/d7dBhy5aSqAaNNWN6Mvdn9+e0KGfl8hZCd6vBwqABmISKSkCgkqr/a6QMWrwLpsKaYWhfrwi1ikfWBNvQmUhXItAlznICMlcopFFNXcEA25wBw/EIms2L4O1onZs5u46abFUsILDWhAc/OKo7H+/OtuEFqaUcsmIE8CR47zEMsSuJvRQcceODVmOqR/uAHKftAC6R+9AKcmzgVxci9qexwLTIDZ34+MR6FVErXECXw7kc+3/G7j8gPG0dbWJmQnum1+zDj3U2rJ0rWzvFRynu/YskMTb5vetmwI7xyeg7R0g072ynEGwa0PwZTRDbDn0GGI1j0Ii2pCKPCyqJTl+eei9tOl5Kr1eU2seuG5ZMBv2fjUIMkQymAWC6jouQE3jdlYZa43PLnsLbcn++eYVwi6FCQktTCEmO3QAx2rhOllAl6bOwsahg2FDTMvhRklWVSUMkxcWEyHJFbUqyCW9AtRSV//K18AF4XmbOWeBTegCF78ujTXf3hm+XvTeo49G8/67Sg+A0a0OGc6CDzIlFbDL3+8GPYuuxLWP7AYysprIQx9YNjdzAglyVkD3qGvd3dsWvv0infM2qBj53zr49rZsJsNZwa2WeTTFxtP3L1oe1VCzh3AWqOM2FJJsFBwbMuCUAgQyAqGrVVJ7Zdwyz2RZS/X/GbrguJ5eNYgyhfnncH5v+DmYcft18Y1T5S7fl9jPMN+4aM6IpeQPmgxThNA5AnuJFhIeI2tHafiNqEUW1XQL+8NrfRznENP1drNuTOA5/5/KezmAZ4zaJBSdVaUvQk5PsNTeqfrMsKQOui5LGKibBAjmKgSRk/RIMlc8BiWCLbI95Dx05jybrMkfsh+xfgPf+hH0AC4OlsAAAAASUVORK5CYII="/>
|
||||
<text id="leftText" x="435" y="140" transform="scale(.1)" textLength="470"></text>
|
||||
<text id="rightText" x="995" y="140" transform="scale(.1)" textLength="470"></text>
|
||||
<text id="revisionText" x="1550" y="140" font-family="monospace" transform="scale(.1)" font-size="110" display="none"></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
@@ -7,14 +7,21 @@
|
||||
# p, <user/group>, <resource>, <action>, <object>
|
||||
|
||||
p, role:readonly, applications, get, */*, allow
|
||||
p, role:readonly, certificates, get, *, allow
|
||||
p, role:readonly, clusters, get, *, allow
|
||||
p, role:readonly, repositories, get, *, allow
|
||||
p, role:readonly, projects, get, *, allow
|
||||
p, role:readonly, accounts, get, *, allow
|
||||
|
||||
p, role:admin, applications, create, */*, allow
|
||||
p, role:admin, applications, update, */*, allow
|
||||
p, role:admin, applications, delete, */*, allow
|
||||
p, role:admin, applications, sync, */*, allow
|
||||
p, role:admin, applications, override, */*, allow
|
||||
p, role:admin, applications, action/*, */*, allow
|
||||
p, role:admin, certificates, create, *, allow
|
||||
p, role:admin, certificates, update, *, allow
|
||||
p, role:admin, certificates, delete, *, allow
|
||||
p, role:admin, clusters, create, *, allow
|
||||
p, role:admin, clusters, update, *, allow
|
||||
p, role:admin, clusters, delete, *, allow
|
||||
@@ -24,6 +31,7 @@ p, role:admin, repositories, delete, *, allow
|
||||
p, role:admin, projects, create, *, allow
|
||||
p, role:admin, projects, update, *, allow
|
||||
p, role:admin, projects, delete, *, allow
|
||||
p, role:admin, accounts, update, *, allow
|
||||
|
||||
g, role:admin, role:readonly
|
||||
g, admin, role:admin
|
||||
|
||||
|
1843
assets/swagger.json
@@ -6,6 +6,8 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/pkg/stats"
|
||||
"github.com/go-redis/redis"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
@@ -16,16 +18,18 @@ import (
|
||||
// load the oidc plugin (required to authenticate with OpenID Connect).
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
|
||||
|
||||
argocd "github.com/argoproj/argo-cd"
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/controller"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
"github.com/argoproj/argo-cd/reposerver"
|
||||
"github.com/argoproj/argo-cd/util/cache"
|
||||
"github.com/argoproj/argo-cd/reposerver/apiclient"
|
||||
cacheutil "github.com/argoproj/argo-cd/util/cache"
|
||||
appstatecache "github.com/argoproj/argo-cd/util/cache/appstate"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/diff"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -37,14 +41,19 @@ const (
|
||||
|
||||
func newCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
appResyncPeriod int64
|
||||
repoServerAddress string
|
||||
statusProcessors int
|
||||
operationProcessors int
|
||||
logLevel string
|
||||
glogLevel int
|
||||
cacheSrc func() (*cache.Cache, error)
|
||||
clientConfig clientcmd.ClientConfig
|
||||
appResyncPeriod int64
|
||||
repoServerAddress string
|
||||
repoServerTimeoutSeconds int
|
||||
selfHealTimeoutSeconds int
|
||||
statusProcessors int
|
||||
operationProcessors int
|
||||
logLevel string
|
||||
glogLevel int
|
||||
metricsPort int
|
||||
kubectlParallelismLimit int64
|
||||
cacheSrc func() (*appstatecache.Cache, error)
|
||||
redisClient *redis.Client
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: cliName,
|
||||
@@ -54,9 +63,8 @@ func newCommand() *cobra.Command {
|
||||
cli.SetGLogLevel(glogLevel)
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
config.QPS = common.K8sClientConfigQPS
|
||||
config.Burst = common.K8sClientConfigBurst
|
||||
errors.CheckError(err)
|
||||
errors.CheckError(v1alpha1.SetK8SConfigDefaults(config))
|
||||
|
||||
kubeClient := kubernetes.NewForConfigOrDie(config)
|
||||
appClient := appclientset.NewForConfigOrDie(config)
|
||||
@@ -65,7 +73,7 @@ func newCommand() *cobra.Command {
|
||||
errors.CheckError(err)
|
||||
|
||||
resyncDuration := time.Duration(appResyncPeriod) * time.Second
|
||||
repoClientset := reposerver.NewRepoServerClientset(repoServerAddress)
|
||||
repoClientset := apiclient.NewRepoServerClientset(repoServerAddress, repoServerTimeoutSeconds)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
@@ -73,6 +81,9 @@ func newCommand() *cobra.Command {
|
||||
errors.CheckError(err)
|
||||
|
||||
settingsMgr := settings.NewSettingsManager(ctx, kubeClient, namespace)
|
||||
kubectl := &kube.KubectlCmd{}
|
||||
legacyDiffDisabled := os.Getenv("ARGOCD_ENABLE_LEGACY_DIFF") == "false"
|
||||
diff.SetPopulateLegacyDiff(!legacyDiffDisabled)
|
||||
appController, err := controller.NewApplicationController(
|
||||
namespace,
|
||||
settingsMgr,
|
||||
@@ -80,10 +91,16 @@ func newCommand() *cobra.Command {
|
||||
appClient,
|
||||
repoClientset,
|
||||
cache,
|
||||
resyncDuration)
|
||||
kubectl,
|
||||
resyncDuration,
|
||||
time.Duration(selfHealTimeoutSeconds)*time.Second,
|
||||
metricsPort,
|
||||
kubectlParallelismLimit)
|
||||
errors.CheckError(err)
|
||||
cacheutil.CollectMetrics(redisClient, appController.GetMetricsServer())
|
||||
|
||||
log.Infof("Application Controller (version: %s) starting (namespace: %s)", argocd.GetVersion(), namespace)
|
||||
vers := common.GetVersion()
|
||||
log.Infof("Application Controller (version: %s, built: %s) starting (namespace: %s)", vers.Version, vers.BuildDate, namespace)
|
||||
stats.RegisterStackDumper()
|
||||
stats.StartStatsTicker(10 * time.Minute)
|
||||
stats.RegisterHeapDumper("memprofile")
|
||||
@@ -98,11 +115,17 @@ func newCommand() *cobra.Command {
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(&command)
|
||||
command.Flags().Int64Var(&appResyncPeriod, "app-resync", defaultAppResyncPeriod, "Time period in seconds for application resync.")
|
||||
command.Flags().StringVar(&repoServerAddress, "repo-server", common.DefaultRepoServerAddr, "Repo server address.")
|
||||
command.Flags().IntVar(&repoServerTimeoutSeconds, "repo-server-timeout-seconds", 60, "Repo server RPC call timeout seconds.")
|
||||
command.Flags().IntVar(&statusProcessors, "status-processors", 1, "Number of application status processors")
|
||||
command.Flags().IntVar(&operationProcessors, "operation-processors", 1, "Number of application operation processors")
|
||||
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
|
||||
command.Flags().IntVar(&glogLevel, "gloglevel", 0, "Set the glog logging level")
|
||||
cacheSrc = cache.AddCacheFlagsToCmd(&command)
|
||||
command.Flags().IntVar(&metricsPort, "metrics-port", common.DefaultPortArgoCDMetrics, "Start metrics server on given port")
|
||||
command.Flags().IntVar(&selfHealTimeoutSeconds, "self-heal-timeout-seconds", 5, "Specifies timeout between application self heal attempts")
|
||||
command.Flags().Int64Var(&kubectlParallelismLimit, "kubectl-parallelism-limit", 20, "Number of allowed concurrent kubectl fork/execs. Any value less the 1 means no limit.")
|
||||
cacheSrc = appstatecache.AddCacheFlagsToCmd(&command, func(client *redis.Client) {
|
||||
redisClient = client
|
||||
})
|
||||
return &command
|
||||
}
|
||||
|
||||
|
||||
@@ -7,18 +7,18 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/argoproj/pkg/stats"
|
||||
"github.com/go-redis/redis"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
argocd "github.com/argoproj/argo-cd"
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/reposerver"
|
||||
"github.com/argoproj/argo-cd/util/cache"
|
||||
reposervercache "github.com/argoproj/argo-cd/reposerver/cache"
|
||||
"github.com/argoproj/argo-cd/reposerver/metrics"
|
||||
cacheutil "github.com/argoproj/argo-cd/util/cache"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
"github.com/argoproj/argo-cd/util/tls"
|
||||
)
|
||||
|
||||
@@ -31,8 +31,11 @@ func newCommand() *cobra.Command {
|
||||
var (
|
||||
logLevel string
|
||||
parallelismLimit int64
|
||||
cacheSrc func() (*cache.Cache, error)
|
||||
listenPort int
|
||||
metricsPort int
|
||||
cacheSrc func() (*reposervercache.Cache, error)
|
||||
tlsConfigCustomizerSrc func() (tls.ConfigCustomizer, error)
|
||||
redisClient *redis.Client
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: cliName,
|
||||
@@ -46,16 +49,19 @@ func newCommand() *cobra.Command {
|
||||
cache, err := cacheSrc()
|
||||
errors.CheckError(err)
|
||||
|
||||
server, err := reposerver.NewServer(git.NewFactory(), cache, tlsConfigCustomizer, parallelismLimit)
|
||||
metricsServer := metrics.NewMetricsServer()
|
||||
cacheutil.CollectMetrics(redisClient, metricsServer)
|
||||
server, err := reposerver.NewServer(metricsServer, cache, tlsConfigCustomizer, parallelismLimit)
|
||||
errors.CheckError(err)
|
||||
|
||||
grpc := server.CreateGRPC()
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", common.PortRepoServer))
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", listenPort))
|
||||
errors.CheckError(err)
|
||||
|
||||
http.Handle("/metrics", promhttp.Handler())
|
||||
go func() { errors.CheckError(http.ListenAndServe(fmt.Sprintf(":%d", common.PortRepoServerMetrics), nil)) }()
|
||||
http.Handle("/metrics", metricsServer.GetHandler())
|
||||
go func() { errors.CheckError(http.ListenAndServe(fmt.Sprintf(":%d", metricsPort), nil)) }()
|
||||
|
||||
log.Infof("argocd-repo-server %s serving on %s", argocd.GetVersion(), listener.Addr())
|
||||
log.Infof("argocd-repo-server %s serving on %s", common.GetVersion(), listener.Addr())
|
||||
stats.RegisterStackDumper()
|
||||
stats.StartStatsTicker(10 * time.Minute)
|
||||
stats.RegisterHeapDumper("memprofile")
|
||||
@@ -67,8 +73,12 @@ func newCommand() *cobra.Command {
|
||||
|
||||
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
|
||||
command.Flags().Int64Var(¶llelismLimit, "parallelismlimit", 0, "Limit on number of concurrent manifests generate requests. Any value less the 1 means no limit.")
|
||||
command.Flags().IntVar(&listenPort, "port", common.DefaultPortRepoServer, "Listen on given port for incoming connections")
|
||||
command.Flags().IntVar(&metricsPort, "metrics-port", common.DefaultPortRepoServerMetrics, "Start metrics server on given port")
|
||||
tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(&command)
|
||||
cacheSrc = cache.AddCacheFlagsToCmd(&command)
|
||||
cacheSrc = reposervercache.AddCacheFlagsToCmd(&command, func(client *redis.Client) {
|
||||
redisClient = client
|
||||
})
|
||||
return &command
|
||||
}
|
||||
|
||||
|
||||
@@ -4,35 +4,44 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/pkg/stats"
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
"github.com/argoproj/argo-cd/reposerver"
|
||||
"github.com/argoproj/argo-cd/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/server"
|
||||
"github.com/argoproj/argo-cd/util/cache"
|
||||
servercache "github.com/argoproj/argo-cd/server/cache"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
"github.com/argoproj/argo-cd/util/tls"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// NewCommand returns a new instance of an argocd command
|
||||
func NewCommand() *cobra.Command {
|
||||
var (
|
||||
insecure bool
|
||||
logLevel string
|
||||
glogLevel int
|
||||
clientConfig clientcmd.ClientConfig
|
||||
staticAssetsDir string
|
||||
baseHRef string
|
||||
repoServerAddress string
|
||||
dexServerAddress string
|
||||
disableAuth bool
|
||||
tlsConfigCustomizerSrc func() (tls.ConfigCustomizer, error)
|
||||
cacheSrc func() (*cache.Cache, error)
|
||||
redisClient *redis.Client
|
||||
insecure bool
|
||||
listenPort int
|
||||
metricsPort int
|
||||
logLevel string
|
||||
glogLevel int
|
||||
clientConfig clientcmd.ClientConfig
|
||||
repoServerTimeoutSeconds int
|
||||
staticAssetsDir string
|
||||
baseHRef string
|
||||
rootPath string
|
||||
repoServerAddress string
|
||||
dexServerAddress string
|
||||
disableAuth bool
|
||||
tlsConfigCustomizerSrc func() (tls.ConfigCustomizer, error)
|
||||
cacheSrc func() (*servercache.Cache, error)
|
||||
frameOptions string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: cliName,
|
||||
@@ -44,8 +53,7 @@ func NewCommand() *cobra.Command {
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
config.QPS = common.K8sClientConfigQPS
|
||||
config.Burst = common.K8sClientConfigBurst
|
||||
errors.CheckError(v1alpha1.SetK8SConfigDefaults(config))
|
||||
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
@@ -57,13 +65,23 @@ func NewCommand() *cobra.Command {
|
||||
|
||||
kubeclientset := kubernetes.NewForConfigOrDie(config)
|
||||
appclientset := appclientset.NewForConfigOrDie(config)
|
||||
repoclientset := reposerver.NewRepoServerClientset(repoServerAddress)
|
||||
repoclientset := apiclient.NewRepoServerClientset(repoServerAddress, repoServerTimeoutSeconds)
|
||||
|
||||
if rootPath != "" {
|
||||
if baseHRef != "" && baseHRef != rootPath {
|
||||
log.Warnf("--basehref and --rootpath had conflict: basehref: %s rootpath: %s", baseHRef, rootPath)
|
||||
}
|
||||
baseHRef = rootPath
|
||||
}
|
||||
|
||||
argoCDOpts := server.ArgoCDServerOpts{
|
||||
Insecure: insecure,
|
||||
ListenPort: listenPort,
|
||||
MetricsPort: metricsPort,
|
||||
Namespace: namespace,
|
||||
StaticAssetsDir: staticAssetsDir,
|
||||
BaseHRef: baseHRef,
|
||||
RootPath: rootPath,
|
||||
KubeClientset: kubeclientset,
|
||||
AppClientset: appclientset,
|
||||
RepoClientset: repoclientset,
|
||||
@@ -71,6 +89,8 @@ func NewCommand() *cobra.Command {
|
||||
DisableAuth: disableAuth,
|
||||
TLSConfigCustomizer: tlsConfigCustomizer,
|
||||
Cache: cache,
|
||||
XFrameOptions: frameOptions,
|
||||
RedisClient: redisClient,
|
||||
}
|
||||
|
||||
stats.RegisterStackDumper()
|
||||
@@ -81,7 +101,7 @@ func NewCommand() *cobra.Command {
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
argocd := server.NewServer(ctx, argoCDOpts)
|
||||
argocd.Run(ctx, common.PortAPIServer)
|
||||
argocd.Run(ctx, listenPort, metricsPort)
|
||||
cancel()
|
||||
}
|
||||
},
|
||||
@@ -91,13 +111,20 @@ func NewCommand() *cobra.Command {
|
||||
command.Flags().BoolVar(&insecure, "insecure", false, "Run server without TLS")
|
||||
command.Flags().StringVar(&staticAssetsDir, "staticassets", "", "Static assets directory path")
|
||||
command.Flags().StringVar(&baseHRef, "basehref", "/", "Value for base href in index.html. Used if Argo CD is running behind reverse proxy under subpath different from /")
|
||||
command.Flags().StringVar(&rootPath, "rootpath", "", "Used if Argo CD is running behind reverse proxy under subpath different from /")
|
||||
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
|
||||
command.Flags().IntVar(&glogLevel, "gloglevel", 0, "Set the glog logging level")
|
||||
command.Flags().StringVar(&repoServerAddress, "repo-server", common.DefaultRepoServerAddr, "Repo server address")
|
||||
command.Flags().StringVar(&dexServerAddress, "dex-server", common.DefaultDexServerAddr, "Dex server address")
|
||||
command.Flags().BoolVar(&disableAuth, "disable-auth", false, "Disable client authentication")
|
||||
command.AddCommand(cli.NewVersionCmd(cliName))
|
||||
command.Flags().IntVar(&listenPort, "port", common.DefaultPortAPIServer, "Listen on given port")
|
||||
command.Flags().IntVar(&metricsPort, "metrics-port", common.DefaultPortArgoCDAPIServerMetrics, "Start metrics on given port")
|
||||
command.Flags().IntVar(&repoServerTimeoutSeconds, "repo-server-timeout-seconds", 60, "Repo server RPC call timeout seconds.")
|
||||
command.Flags().StringVar(&frameOptions, "x-frame-options", "sameorigin", "Set X-Frame-Options header in HTTP responses to `value`. To disable, set to \"\".")
|
||||
tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(command)
|
||||
cacheSrc = cache.AddCacheFlagsToCmd(command)
|
||||
cacheSrc = servercache.AddCacheFlagsToCmd(command, func(client *redis.Client) {
|
||||
redisClient = client
|
||||
})
|
||||
return command
|
||||
}
|
||||
|
||||
192
cmd/argocd-util/commands/projects.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
appclient "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/typed/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/diff"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
func NewProjectsCommand() *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "projects",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
},
|
||||
}
|
||||
|
||||
command.AddCommand(NewUpdatePolicyRuleCommand())
|
||||
return command
|
||||
}
|
||||
|
||||
func globMatch(pattern string, val string) bool {
|
||||
if pattern == "*" {
|
||||
return true
|
||||
}
|
||||
if ok, err := filepath.Match(pattern, val); ok && err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getModification(modification string, resource string, scope string, permission string) (func(string, string) string, error) {
|
||||
switch modification {
|
||||
case "set":
|
||||
if scope == "" {
|
||||
return nil, fmt.Errorf("Flag --group cannot be empty if permission should be set in role")
|
||||
}
|
||||
if permission == "" {
|
||||
return nil, fmt.Errorf("Flag --permission cannot be empty if permission should be set in role")
|
||||
}
|
||||
return func(proj string, action string) string {
|
||||
return fmt.Sprintf("%s, %s, %s/%s, %s", resource, action, proj, scope, permission)
|
||||
}, nil
|
||||
case "remove":
|
||||
return func(proj string, action string) string {
|
||||
return ""
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("modification %s is not supported", modification)
|
||||
}
|
||||
|
||||
func saveProject(updated v1alpha1.AppProject, orig v1alpha1.AppProject, projectsIf appclient.AppProjectInterface, dryRun bool) error {
|
||||
fmt.Printf("===== %s ======\n", updated.Name)
|
||||
target, err := kube.ToUnstructured(&updated)
|
||||
errors.CheckError(err)
|
||||
live, err := kube.ToUnstructured(&orig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = diff.PrintDiff(updated.Name, target, live)
|
||||
if !dryRun {
|
||||
_, err = projectsIf.Update(&updated)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatPolicy(proj string, role string, permission string) string {
|
||||
return fmt.Sprintf("p, proj:%s:%s, %s", proj, role, permission)
|
||||
}
|
||||
|
||||
func split(input string, delimiter string) []string {
|
||||
parts := strings.Split(input, delimiter)
|
||||
for i := range parts {
|
||||
parts[i] = strings.TrimSpace(parts[i])
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
func NewUpdatePolicyRuleCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
resource string
|
||||
scope string
|
||||
rolePattern string
|
||||
permission string
|
||||
dryRun bool
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "update-role-policy PROJECT_GLOB MODIFICATION ACTION",
|
||||
Short: "Implement bulk project role update. Useful to back-fill existing project policies or remove obsolete actions.",
|
||||
Example: ` # Add policy that allows executing any action (action/*) to roles which name matches to *deployer* in all projects
|
||||
argocd-util projects update-role-policy '*' set 'action/*' --role '*deployer*' --resource applications --scope '*' --permission allow
|
||||
|
||||
# Remove policy that which manages running (action/*) from all roles which name matches *deployer* in all projects
|
||||
argocd-util projects update-role-policy '*' remove override --role '*deployer*'
|
||||
`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projectGlob := args[0]
|
||||
modificationType := args[1]
|
||||
action := args[2]
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
config.QPS = 100
|
||||
config.Burst = 50
|
||||
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
appclients := appclientset.NewForConfigOrDie(config)
|
||||
|
||||
modification, err := getModification(modificationType, resource, scope, permission)
|
||||
errors.CheckError(err)
|
||||
projIf := appclients.ArgoprojV1alpha1().AppProjects(namespace)
|
||||
|
||||
err = updateProjects(projIf, projectGlob, rolePattern, action, modification, dryRun)
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&resource, "resource", "", "Resource e.g. 'applications'")
|
||||
command.Flags().StringVar(&scope, "scope", "", "Resource scope e.g. '*'")
|
||||
command.Flags().StringVar(&rolePattern, "role", "*", "Role name pattern e.g. '*deployer*'")
|
||||
command.Flags().StringVar(&permission, "permission", "", "Action permission")
|
||||
command.Flags().BoolVar(&dryRun, "dry-run", true, "Dry run")
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(command)
|
||||
return command
|
||||
}
|
||||
|
||||
func updateProjects(projIf appclient.AppProjectInterface, projectGlob string, rolePattern string, action string, modification func(string, string) string, dryRun bool) error {
|
||||
projects, err := projIf.List(v1.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, proj := range projects.Items {
|
||||
if !globMatch(projectGlob, proj.Name) {
|
||||
continue
|
||||
}
|
||||
origProj := proj.DeepCopy()
|
||||
updated := false
|
||||
for i, role := range proj.Spec.Roles {
|
||||
if !globMatch(rolePattern, role.Name) {
|
||||
continue
|
||||
}
|
||||
actionPolicyIndex := -1
|
||||
for i := range role.Policies {
|
||||
parts := split(role.Policies[i], ",")
|
||||
if len(parts) != 6 || parts[3] != action {
|
||||
continue
|
||||
}
|
||||
actionPolicyIndex = i
|
||||
break
|
||||
}
|
||||
policyPermission := modification(proj.Name, action)
|
||||
if actionPolicyIndex == -1 && policyPermission != "" {
|
||||
updated = true
|
||||
role.Policies = append(role.Policies, formatPolicy(proj.Name, role.Name, policyPermission))
|
||||
} else if actionPolicyIndex > -1 && policyPermission == "" {
|
||||
updated = true
|
||||
role.Policies = append(role.Policies[:actionPolicyIndex], role.Policies[actionPolicyIndex+1:]...)
|
||||
} else if actionPolicyIndex > -1 && policyPermission != "" {
|
||||
updated = true
|
||||
role.Policies[actionPolicyIndex] = formatPolicy(proj.Name, role.Name, policyPermission)
|
||||
}
|
||||
proj.Spec.Roles[i] = role
|
||||
}
|
||||
if updated {
|
||||
err = saveProject(proj, *origProj, projIf, dryRun)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
78
cmd/argocd-util/commands/projects_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/pkg/client/clientset/versioned/fake"
|
||||
)
|
||||
|
||||
const (
|
||||
namespace = "default"
|
||||
)
|
||||
|
||||
func newProj(name string, roleNames ...string) *v1alpha1.AppProject {
|
||||
var roles []v1alpha1.ProjectRole
|
||||
for i := range roleNames {
|
||||
roles = append(roles, v1alpha1.ProjectRole{Name: roleNames[i]})
|
||||
}
|
||||
return &v1alpha1.AppProject{ObjectMeta: v1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
}, Spec: v1alpha1.AppProjectSpec{
|
||||
Roles: roles,
|
||||
}}
|
||||
}
|
||||
|
||||
func TestUpdateProjects_FindMatchingProject(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(newProj("foo", "test"), newProj("bar", "test"))
|
||||
|
||||
modification, err := getModification("set", "*", "*", "allow")
|
||||
assert.NoError(t, err)
|
||||
err = updateProjects(clientset.ArgoprojV1alpha1().AppProjects(namespace), "ba*", "*", "set", modification, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
fooProj, err := clientset.ArgoprojV1alpha1().AppProjects(namespace).Get("foo", v1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, fooProj.Spec.Roles[0].Policies, 0)
|
||||
|
||||
barProj, err := clientset.ArgoprojV1alpha1().AppProjects(namespace).Get("bar", v1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, barProj.Spec.Roles[0].Policies, []string{"p, proj:bar:test, *, set, bar/*, allow"})
|
||||
}
|
||||
|
||||
func TestUpdateProjects_FindMatchingRole(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(newProj("proj", "foo", "bar"))
|
||||
|
||||
modification, err := getModification("set", "*", "*", "allow")
|
||||
assert.NoError(t, err)
|
||||
err = updateProjects(clientset.ArgoprojV1alpha1().AppProjects(namespace), "*", "fo*", "set", modification, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
proj, err := clientset.ArgoprojV1alpha1().AppProjects(namespace).Get("proj", v1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, proj.Spec.Roles[0].Policies, []string{"p, proj:proj:foo, *, set, proj/*, allow"})
|
||||
assert.Len(t, proj.Spec.Roles[1].Policies, 0)
|
||||
}
|
||||
|
||||
func TestGetModification_SetPolicy(t *testing.T) {
|
||||
modification, err := getModification("set", "*", "*", "allow")
|
||||
assert.NoError(t, err)
|
||||
policy := modification("proj", "myaction")
|
||||
assert.Equal(t, "*, myaction, proj/*, allow", policy)
|
||||
}
|
||||
|
||||
func TestGetModification_RemovePolicy(t *testing.T) {
|
||||
modification, err := getModification("remove", "*", "*", "allow")
|
||||
assert.NoError(t, err)
|
||||
policy := modification("proj", "myaction")
|
||||
assert.Equal(t, "", policy)
|
||||
}
|
||||
|
||||
func TestGetModification_NotSupported(t *testing.T) {
|
||||
_, err := getModification("bar", "*", "*", "allow")
|
||||
assert.Errorf(t, err, "modification bar is not supported")
|
||||
}
|
||||
501
cmd/argocd-util/commands/settings.go
Normal file
@@ -0,0 +1,501 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/argo/normalizers"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/diff"
|
||||
"github.com/argoproj/argo-cd/util/health"
|
||||
"github.com/argoproj/argo-cd/util/lua"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
type settingsOpts struct {
|
||||
argocdCMPath string
|
||||
argocdSecretPath string
|
||||
loadClusterSettings bool
|
||||
clientConfig clientcmd.ClientConfig
|
||||
}
|
||||
|
||||
type commandContext interface {
|
||||
createSettingsManager() (*settings.SettingsManager, error)
|
||||
}
|
||||
|
||||
func collectLogs(callback func()) string {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
out := bytes.Buffer{}
|
||||
log.SetOutput(&out)
|
||||
defer log.SetLevel(log.FatalLevel)
|
||||
callback()
|
||||
return out.String()
|
||||
}
|
||||
|
||||
func setSettingsMeta(obj v1.Object) {
|
||||
obj.SetNamespace("default")
|
||||
labels := obj.GetLabels()
|
||||
if labels == nil {
|
||||
labels = make(map[string]string)
|
||||
}
|
||||
labels["app.kubernetes.io/part-of"] = "argocd"
|
||||
obj.SetLabels(labels)
|
||||
}
|
||||
|
||||
func (opts *settingsOpts) createSettingsManager() (*settings.SettingsManager, error) {
|
||||
var argocdCM *corev1.ConfigMap
|
||||
if opts.argocdCMPath == "" && !opts.loadClusterSettings {
|
||||
return nil, fmt.Errorf("either --argocd-cm-path must be provided or --load-cluster-settings must be set to true")
|
||||
} else if opts.argocdCMPath == "" {
|
||||
realClientset, ns, err := opts.getK8sClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
argocdCM, err = realClientset.CoreV1().ConfigMaps(ns).Get(common.ArgoCDConfigMapName, v1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
data, err := ioutil.ReadFile(opts.argocdCMPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = yaml.Unmarshal(data, &argocdCM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
setSettingsMeta(argocdCM)
|
||||
|
||||
var argocdSecret *corev1.Secret
|
||||
if opts.argocdSecretPath != "" {
|
||||
data, err := ioutil.ReadFile(opts.argocdSecretPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = yaml.Unmarshal(data, &argocdSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setSettingsMeta(argocdSecret)
|
||||
} else if opts.loadClusterSettings {
|
||||
realClientset, ns, err := opts.getK8sClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
argocdSecret, err = realClientset.CoreV1().Secrets(ns).Get(common.ArgoCDSecretName, v1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
argocdSecret = &corev1.Secret{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: common.ArgoCDSecretName,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"admin.password": []byte("test"),
|
||||
"server.secretkey": []byte("test"),
|
||||
},
|
||||
}
|
||||
}
|
||||
setSettingsMeta(argocdSecret)
|
||||
clientset := fake.NewSimpleClientset(argocdSecret, argocdCM)
|
||||
|
||||
manager := settings.NewSettingsManager(context.Background(), clientset, "default")
|
||||
errors.CheckError(manager.ResyncInformers())
|
||||
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
func (opts *settingsOpts) getK8sClient() (*kubernetes.Clientset, string, error) {
|
||||
namespace, _, err := opts.clientConfig.Namespace()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
restConfig, err := opts.clientConfig.ClientConfig()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
realClientset, err := kubernetes.NewForConfig(restConfig)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return realClientset, namespace, nil
|
||||
}
|
||||
|
||||
func NewSettingsCommand() *cobra.Command {
|
||||
var (
|
||||
opts settingsOpts
|
||||
)
|
||||
|
||||
var command = &cobra.Command{
|
||||
Use: "settings",
|
||||
Short: "Provides set of commands for settings validation and troubleshooting",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
},
|
||||
}
|
||||
log.SetLevel(log.FatalLevel)
|
||||
|
||||
command.AddCommand(NewValidateSettingsCommand(&opts))
|
||||
command.AddCommand(NewResourceOverridesCommand(&opts))
|
||||
|
||||
opts.clientConfig = cli.AddKubectlFlagsToCmd(command)
|
||||
command.PersistentFlags().StringVar(&opts.argocdCMPath, "argocd-cm-path", "", "Path to local argocd-cm.yaml file")
|
||||
command.PersistentFlags().StringVar(&opts.argocdSecretPath, "argocd-secret-path", "", "Path to local argocd-secret.yaml file")
|
||||
command.PersistentFlags().BoolVar(&opts.loadClusterSettings, "load-cluster-settings", false,
|
||||
"Indicates that config map and secret should be loaded from cluster unless local file path is provided")
|
||||
return command
|
||||
}
|
||||
|
||||
type settingValidator func(manager *settings.SettingsManager) (string, error)
|
||||
|
||||
func joinValidators(validators ...settingValidator) settingValidator {
|
||||
return func(manager *settings.SettingsManager) (string, error) {
|
||||
var errorStrs []string
|
||||
var summaries []string
|
||||
for i := range validators {
|
||||
summary, err := validators[i](manager)
|
||||
if err != nil {
|
||||
errorStrs = append(errorStrs, err.Error())
|
||||
}
|
||||
if summary != "" {
|
||||
summaries = append(summaries, summary)
|
||||
}
|
||||
}
|
||||
if len(errorStrs) > 0 {
|
||||
return "", fmt.Errorf("%s", strings.Join(errorStrs, "\n"))
|
||||
}
|
||||
return strings.Join(summaries, "\n"), nil
|
||||
}
|
||||
}
|
||||
|
||||
var validatorsByGroup = map[string]settingValidator{
|
||||
"general": joinValidators(func(manager *settings.SettingsManager) (string, error) {
|
||||
general, err := manager.GetSettings()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ssoProvider := ""
|
||||
if general.DexConfig != "" {
|
||||
if _, err := settings.UnmarshalDexConfig(general.DexConfig); err != nil {
|
||||
return "", fmt.Errorf("invalid dex.config: %v", err)
|
||||
}
|
||||
ssoProvider = "Dex"
|
||||
} else if general.OIDCConfigRAW != "" {
|
||||
if _, err := settings.UnmarshalOIDCConfig(general.OIDCConfigRAW); err != nil {
|
||||
return "", fmt.Errorf("invalid oidc.config: %v", err)
|
||||
}
|
||||
ssoProvider = "OIDC"
|
||||
}
|
||||
var summary string
|
||||
if ssoProvider != "" {
|
||||
summary = fmt.Sprintf("%s is configured", ssoProvider)
|
||||
if general.URL == "" {
|
||||
summary = summary + " ('url' field is missing)"
|
||||
}
|
||||
} else if ssoProvider != "" && general.URL != "" {
|
||||
|
||||
} else {
|
||||
summary = "SSO is not configured"
|
||||
}
|
||||
return summary, nil
|
||||
}, func(manager *settings.SettingsManager) (string, error) {
|
||||
_, err := manager.GetAppInstanceLabelKey()
|
||||
return "", err
|
||||
}, func(manager *settings.SettingsManager) (string, error) {
|
||||
_, err := manager.GetHelp()
|
||||
return "", err
|
||||
}, func(manager *settings.SettingsManager) (string, error) {
|
||||
_, err := manager.GetGoogleAnalytics()
|
||||
return "", err
|
||||
}),
|
||||
"plugins": func(manager *settings.SettingsManager) (string, error) {
|
||||
plugins, err := manager.GetConfigManagementPlugins()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%d plugins", len(plugins)), nil
|
||||
},
|
||||
"kustomize": func(manager *settings.SettingsManager) (string, error) {
|
||||
opts, err := manager.GetKustomizeSettings()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
summary := "default options"
|
||||
if opts.BuildOptions != "" {
|
||||
summary = opts.BuildOptions
|
||||
}
|
||||
if len(opts.Versions) > 0 {
|
||||
summary = fmt.Sprintf("%s (%d versions)", summary, len(opts.Versions))
|
||||
}
|
||||
return summary, err
|
||||
},
|
||||
"repositories": joinValidators(func(manager *settings.SettingsManager) (string, error) {
|
||||
repos, err := manager.GetRepositories()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%d repositories", len(repos)), nil
|
||||
}, func(manager *settings.SettingsManager) (string, error) {
|
||||
creds, err := manager.GetRepositoryCredentials()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%d repository credentials", len(creds)), nil
|
||||
}),
|
||||
"accounts": func(manager *settings.SettingsManager) (string, error) {
|
||||
accounts, err := manager.GetAccounts()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%d accounts", len(accounts)), nil
|
||||
},
|
||||
"resource-overrides": func(manager *settings.SettingsManager) (string, error) {
|
||||
overrides, err := manager.GetResourceOverrides()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%d resource overrides", len(overrides)), nil
|
||||
},
|
||||
}
|
||||
|
||||
func NewValidateSettingsCommand(cmdCtx commandContext) *cobra.Command {
|
||||
var (
|
||||
groups []string
|
||||
)
|
||||
|
||||
var allGroups []string
|
||||
for k := range validatorsByGroup {
|
||||
allGroups = append(allGroups, k)
|
||||
}
|
||||
sort.Slice(allGroups, func(i, j int) bool {
|
||||
return allGroups[i] < allGroups[j]
|
||||
})
|
||||
|
||||
var command = &cobra.Command{
|
||||
Use: "validate",
|
||||
Short: "Validate settings",
|
||||
Long: "Validates settings specified in 'argocd-cm' ConfigMap and 'argocd-secret' Secret",
|
||||
Example: `
|
||||
#Validates all settings in the specified YAML file
|
||||
argocd-util settings validate --argocd-cm-path ./argocd-cm.yaml
|
||||
|
||||
#Validates accounts and plugins settings in Kubernetes cluster of current kubeconfig context
|
||||
argocd-util settings validate --group accounts --group plugins --load-cluster-settings`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
settingsManager, err := cmdCtx.createSettingsManager()
|
||||
errors.CheckError(err)
|
||||
|
||||
if len(groups) == 0 {
|
||||
groups = allGroups
|
||||
}
|
||||
for i, group := range groups {
|
||||
validator := validatorsByGroup[group]
|
||||
|
||||
logs := collectLogs(func() {
|
||||
summary, err := validator(settingsManager)
|
||||
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(os.Stdout, "❌ %s\n", group)
|
||||
_, _ = fmt.Fprintf(os.Stdout, "%s\n", err.Error())
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(os.Stdout, "✅ %s\n", group)
|
||||
if summary != "" {
|
||||
_, _ = fmt.Fprintf(os.Stdout, "%s\n", summary)
|
||||
}
|
||||
}
|
||||
})
|
||||
if logs != "" {
|
||||
_, _ = fmt.Fprintf(os.Stdout, "%s\n", logs)
|
||||
}
|
||||
if i != len(groups)-1 {
|
||||
_, _ = fmt.Fprintf(os.Stdout, "\n")
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
command.Flags().StringArrayVar(&groups, "group", nil, fmt.Sprintf(
|
||||
"Optional list of setting groups that have to be validated ( one of: %s)", strings.Join(allGroups, ", ")))
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func NewResourceOverridesCommand(cmdCtx commandContext) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "resource-overrides",
|
||||
Short: "Troubleshoot resource overrides",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
},
|
||||
}
|
||||
command.AddCommand(NewResourceIgnoreDifferencesCommand(cmdCtx))
|
||||
command.AddCommand(NewResourceActionCommand(cmdCtx))
|
||||
command.AddCommand(NewResourceHealthCommand(cmdCtx))
|
||||
return command
|
||||
}
|
||||
|
||||
func executeResourceOverrideCommand(cmdCtx commandContext, args []string, callback func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride)) {
|
||||
data, err := ioutil.ReadFile(args[0])
|
||||
errors.CheckError(err)
|
||||
|
||||
res := unstructured.Unstructured{}
|
||||
errors.CheckError(yaml.Unmarshal(data, &res))
|
||||
|
||||
settingsManager, err := cmdCtx.createSettingsManager()
|
||||
errors.CheckError(err)
|
||||
|
||||
overrides, err := settingsManager.GetResourceOverrides()
|
||||
errors.CheckError(err)
|
||||
gvk := res.GroupVersionKind()
|
||||
key := gvk.Kind
|
||||
if gvk.Group != "" {
|
||||
key = fmt.Sprintf("%s/%s", gvk.Group, gvk.Kind)
|
||||
}
|
||||
override, hasOverride := overrides[key]
|
||||
if !hasOverride {
|
||||
_, _ = fmt.Printf("No overrides configured for '%s/%s'\n", gvk.Group, gvk.Kind)
|
||||
return
|
||||
}
|
||||
callback(res, override, overrides)
|
||||
}
|
||||
|
||||
func NewResourceIgnoreDifferencesCommand(cmdCtx commandContext) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "ignore-differences RESOURCE_YAML_PATH",
|
||||
Short: "Renders fields excluded from diffing",
|
||||
Long: "Renders ignored fields using the 'ignoreDifferences' setting specified in the 'resource.customizations' field of 'argocd-cm' ConfigMap",
|
||||
Example: `
|
||||
argocd-util settings resource-overrides ignore-differences ./deploy.yaml --argocd-cm-path ./argocd-cm.yaml`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) < 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
executeResourceOverrideCommand(cmdCtx, args, func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride) {
|
||||
gvk := res.GroupVersionKind()
|
||||
if override.IgnoreDifferences == "" {
|
||||
_, _ = fmt.Printf("Ignore differences are not configured for '%s/%s'\n", gvk.Group, gvk.Kind)
|
||||
return
|
||||
}
|
||||
|
||||
normalizer, err := normalizers.NewIgnoreNormalizer(nil, overrides)
|
||||
errors.CheckError(err)
|
||||
|
||||
normalizedRes := res.DeepCopy()
|
||||
logs := collectLogs(func() {
|
||||
errors.CheckError(normalizer.Normalize(normalizedRes))
|
||||
})
|
||||
if logs != "" {
|
||||
_, _ = fmt.Println(logs)
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(&res, normalizedRes) {
|
||||
_, _ = fmt.Printf("No fields are ignored by ignoreDifferences settings: \n%s\n", override.IgnoreDifferences)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = fmt.Printf("Following fields are ignored:\n\n")
|
||||
_ = diff.PrintDiff(res.GetName(), &res, normalizedRes)
|
||||
})
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
func NewResourceHealthCommand(cmdCtx commandContext) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "health RESOURCE_YAML_PATH",
|
||||
Short: "Assess resource health",
|
||||
Long: "Assess resource health using the lua script configured in the 'resource.customizations' field of 'argocd-cm' ConfigMap",
|
||||
Example: `
|
||||
argocd-util settings resource-overrides health ./deploy.yaml --argocd-cm-path ./argocd-cm.yaml`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) < 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
executeResourceOverrideCommand(cmdCtx, args, func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride) {
|
||||
gvk := res.GroupVersionKind()
|
||||
if override.HealthLua == "" {
|
||||
_, _ = fmt.Printf("Health script is not configured for '%s/%s'\n", gvk.Group, gvk.Kind)
|
||||
return
|
||||
}
|
||||
|
||||
resHealth, err := health.GetResourceHealth(&res, overrides)
|
||||
errors.CheckError(err)
|
||||
|
||||
_, _ = fmt.Printf("STATUS: %s\n", resHealth.Status)
|
||||
_, _ = fmt.Printf("MESSAGE: %s\n", resHealth.Message)
|
||||
})
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
func NewResourceActionCommand(cmdCtx commandContext) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "action RESOURCE_YAML_PATH ACTION",
|
||||
Short: "Executes resource action",
|
||||
Long: "Executes resource action using the lua script configured in the 'resource.customizations' field of 'argocd-cm' ConfigMap and outputs updated fields",
|
||||
Example: `
|
||||
argocd-util settings resource-overrides action /tmp/deploy.yaml restart --argocd-cm-path ./argocd-cm.yaml`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) < 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
action := args[1]
|
||||
|
||||
executeResourceOverrideCommand(cmdCtx, args, func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride) {
|
||||
gvk := res.GroupVersionKind()
|
||||
if override.Actions == "" {
|
||||
_, _ = fmt.Printf("Actions are not configured for '%s/%s'\n", gvk.Group, gvk.Kind)
|
||||
return
|
||||
}
|
||||
|
||||
luaVM := lua.VM{ResourceOverrides: overrides}
|
||||
action, err := luaVM.GetResourceAction(&res, action)
|
||||
errors.CheckError(err)
|
||||
|
||||
modifiedRes, err := luaVM.ExecuteResourceAction(&res, action.ActionLua)
|
||||
errors.CheckError(err)
|
||||
|
||||
if reflect.DeepEqual(&res, modifiedRes) {
|
||||
_, _ = fmt.Printf("No fields had been changed by action: \n%s\n", action.Name)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = fmt.Printf("Following fields have been changed:\n\n")
|
||||
_ = diff.PrintDiff(res.GetName(), &res, modifiedRes)
|
||||
})
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
367
cmd/argocd-util/commands/settings_test.go
Normal file
@@ -0,0 +1,367 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func captureStdout(callback func()) (string, error) {
|
||||
oldStdout := os.Stdout
|
||||
oldStderr := os.Stderr
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
os.Stdout = w
|
||||
defer func() {
|
||||
os.Stdout = oldStdout
|
||||
os.Stderr = oldStderr
|
||||
}()
|
||||
|
||||
callback()
|
||||
util.Close(w)
|
||||
|
||||
data, err := ioutil.ReadAll(r)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), err
|
||||
}
|
||||
|
||||
func newSettingsManager(data map[string]string) *settings.SettingsManager {
|
||||
clientset := fake.NewSimpleClientset(&v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: common.ArgoCDConfigMapName,
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/part-of": "argocd",
|
||||
},
|
||||
},
|
||||
Data: data,
|
||||
}, &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: common.ArgoCDSecretName,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"admin.password": []byte("test"),
|
||||
"server.secretkey": []byte("test"),
|
||||
},
|
||||
})
|
||||
return settings.NewSettingsManager(context.Background(), clientset, "default")
|
||||
}
|
||||
|
||||
type fakeCmdContext struct {
|
||||
mgr *settings.SettingsManager
|
||||
// nolint:unused,structcheck
|
||||
out bytes.Buffer
|
||||
}
|
||||
|
||||
func newCmdContext(data map[string]string) *fakeCmdContext {
|
||||
return &fakeCmdContext{mgr: newSettingsManager(data)}
|
||||
}
|
||||
|
||||
func (ctx *fakeCmdContext) createSettingsManager() (*settings.SettingsManager, error) {
|
||||
return ctx.mgr, nil
|
||||
}
|
||||
|
||||
type validatorTestCase struct {
|
||||
validator string
|
||||
data map[string]string
|
||||
containsSummary string
|
||||
containsError string
|
||||
}
|
||||
|
||||
func TestCreateSettingsManager(t *testing.T) {
|
||||
f, closer, err := tempFile(`apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-cm
|
||||
data:
|
||||
url: https://myargocd.com`)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
defer util.Close(closer)
|
||||
|
||||
opts := settingsOpts{argocdCMPath: f}
|
||||
settingsManager, err := opts.createSettingsManager()
|
||||
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
argoCDSettings, err := settingsManager.GetSettings()
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, "https://myargocd.com", argoCDSettings.URL)
|
||||
}
|
||||
|
||||
func TestValidator(t *testing.T) {
|
||||
testCases := map[string]validatorTestCase{
|
||||
"General_SSOIsNotConfigured": {
|
||||
validator: "general", containsSummary: "SSO is not configured",
|
||||
},
|
||||
"General_DexInvalidConfig": {
|
||||
validator: "general",
|
||||
data: map[string]string{"dex.config": "abcdefg"},
|
||||
containsError: "invalid dex.config",
|
||||
},
|
||||
"General_OIDCConfigured": {
|
||||
validator: "general",
|
||||
data: map[string]string{
|
||||
"url": "https://myargocd.com",
|
||||
"oidc.config": `
|
||||
name: Okta
|
||||
issuer: https://dev-123456.oktapreview.com
|
||||
clientID: aaaabbbbccccddddeee
|
||||
clientSecret: aaaabbbbccccddddeee`,
|
||||
},
|
||||
containsSummary: "OIDC is configured",
|
||||
},
|
||||
"General_DexConfiguredMissingURL": {
|
||||
validator: "general",
|
||||
data: map[string]string{
|
||||
"dex.config": `connectors:
|
||||
- type: github
|
||||
name: GitHub
|
||||
config:
|
||||
clientID: aabbccddeeff00112233
|
||||
clientSecret: aabbccddeeff00112233`,
|
||||
},
|
||||
containsSummary: "Dex is configured ('url' field is missing)",
|
||||
},
|
||||
"Plugins_ValidConfig": {
|
||||
validator: "plugins",
|
||||
data: map[string]string{
|
||||
"configManagementPlugins": `[{"name": "test1"}, {"name": "test2"}]`,
|
||||
},
|
||||
containsSummary: "2 plugins",
|
||||
},
|
||||
"Kustomize_ModifiedOptions": {
|
||||
validator: "kustomize",
|
||||
containsSummary: "default options",
|
||||
},
|
||||
"Kustomize_DefaultOptions": {
|
||||
validator: "kustomize",
|
||||
data: map[string]string{
|
||||
"kustomize.buildOptions": "updated-options (2 versions)",
|
||||
"kustomize.versions.v123": "binary-123",
|
||||
"kustomize.versions.v321": "binary-321",
|
||||
},
|
||||
containsSummary: "updated-options",
|
||||
},
|
||||
"Repositories": {
|
||||
validator: "repositories",
|
||||
data: map[string]string{
|
||||
"repositories": `
|
||||
- url: https://github.com/argoproj/my-private-repository1
|
||||
- url: https://github.com/argoproj/my-private-repository2`,
|
||||
},
|
||||
containsSummary: "2 repositories",
|
||||
},
|
||||
"Accounts": {
|
||||
validator: "accounts",
|
||||
data: map[string]string{
|
||||
"accounts.user1": "apiKey, login",
|
||||
"accounts.user2": "login",
|
||||
"accounts.user3": "apiKey",
|
||||
},
|
||||
containsSummary: "4 accounts",
|
||||
},
|
||||
"ResourceOverrides": {
|
||||
validator: "resource-overrides",
|
||||
data: map[string]string{
|
||||
"resource.customizations": `
|
||||
admissionregistration.k8s.io/MutatingWebhookConfiguration:
|
||||
ignoreDifferences: |
|
||||
jsonPointers:
|
||||
- /webhooks/0/clientConfig/caBundle`,
|
||||
},
|
||||
containsSummary: "1 resource overrides",
|
||||
},
|
||||
}
|
||||
for name := range testCases {
|
||||
tc := testCases[name]
|
||||
t.Run(name, func(t *testing.T) {
|
||||
validator, ok := validatorsByGroup[tc.validator]
|
||||
if !assert.True(t, ok) {
|
||||
return
|
||||
}
|
||||
summary, err := validator(newSettingsManager(tc.data))
|
||||
if tc.containsSummary != "" {
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, summary, tc.containsSummary)
|
||||
} else if tc.containsError != "" {
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), tc.containsError)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
testDeploymentYAML = `apiVersion: v1
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 0`
|
||||
)
|
||||
|
||||
func tempFile(content string) (string, io.Closer, error) {
|
||||
f, err := ioutil.TempFile("", "*.yaml")
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
_, err = f.Write([]byte(content))
|
||||
if err != nil {
|
||||
_ = os.Remove(f.Name())
|
||||
return "", nil, err
|
||||
}
|
||||
return f.Name(), util.NewCloser(func() error {
|
||||
return os.Remove(f.Name())
|
||||
}), nil
|
||||
}
|
||||
|
||||
func TestValidateSettingsCommand_NoErrors(t *testing.T) {
|
||||
cmd := NewValidateSettingsCommand(newCmdContext(map[string]string{}))
|
||||
out, err := captureStdout(func() {
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
for k := range validatorsByGroup {
|
||||
assert.Contains(t, out, fmt.Sprintf("✅ %s", k))
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceOverrideIgnoreDifferences(t *testing.T) {
|
||||
f, closer, err := tempFile(testDeploymentYAML)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
defer util.Close(closer)
|
||||
|
||||
t.Run("NoOverridesConfigured", func(t *testing.T) {
|
||||
cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{}))
|
||||
out, err := captureStdout(func() {
|
||||
cmd.SetArgs([]string{"ignore-differences", f})
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, "No overrides configured")
|
||||
})
|
||||
|
||||
t.Run("DataIgnored", func(t *testing.T) {
|
||||
cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
|
||||
"resource.customizations": `apps/Deployment:
|
||||
ignoreDifferences: |
|
||||
jsonPointers:
|
||||
- /spec`}))
|
||||
out, err := captureStdout(func() {
|
||||
cmd.SetArgs([]string{"ignore-differences", f})
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, "< spec:")
|
||||
})
|
||||
}
|
||||
|
||||
func TestResourceOverrideHealth(t *testing.T) {
|
||||
f, closer, err := tempFile(testDeploymentYAML)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
defer util.Close(closer)
|
||||
|
||||
t.Run("NoHealthAssessment", func(t *testing.T) {
|
||||
cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
|
||||
"resource.customizations": `apps/Deployment: {}`}))
|
||||
out, err := captureStdout(func() {
|
||||
cmd.SetArgs([]string{"health", f})
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, "Health script is not configured")
|
||||
})
|
||||
|
||||
t.Run("HealthAssessmentConfigured", func(t *testing.T) {
|
||||
cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
|
||||
"resource.customizations": `apps/Deployment:
|
||||
health.lua: |
|
||||
return { status = "Progressing" }
|
||||
`}))
|
||||
out, err := captureStdout(func() {
|
||||
cmd.SetArgs([]string{"health", f})
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, "Progressing")
|
||||
})
|
||||
}
|
||||
|
||||
func TestResourceOverrideAction(t *testing.T) {
|
||||
f, closer, err := tempFile(testDeploymentYAML)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
defer util.Close(closer)
|
||||
|
||||
t.Run("NoActions", func(t *testing.T) {
|
||||
cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
|
||||
"resource.customizations": `apps/Deployment: {}`}))
|
||||
out, err := captureStdout(func() {
|
||||
cmd.SetArgs([]string{"action", f, "test"})
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, "Actions are not configured")
|
||||
})
|
||||
|
||||
t.Run("HealthAssessmentConfigured", func(t *testing.T) {
|
||||
cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
|
||||
"resource.customizations": `apps/Deployment:
|
||||
actions: |
|
||||
definitions:
|
||||
- name: test
|
||||
action.lua: |
|
||||
obj.metadata.labels["test"] = 'updated'
|
||||
return obj
|
||||
`}))
|
||||
out, err := captureStdout(func() {
|
||||
cmd.SetArgs([]string{"action", f, "test"})
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, "test: updated")
|
||||
})
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"syscall"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
@@ -23,9 +24,8 @@ import (
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/argoproj/argo-cd/cmd/argocd-util/commands"
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
@@ -73,6 +73,8 @@ func NewCommand() *cobra.Command {
|
||||
command.AddCommand(NewImportCommand())
|
||||
command.AddCommand(NewExportCommand())
|
||||
command.AddCommand(NewClusterConfig())
|
||||
command.AddCommand(commands.NewProjectsCommand())
|
||||
command.AddCommand(commands.NewSettingsCommand())
|
||||
|
||||
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
|
||||
return command
|
||||
@@ -108,7 +110,7 @@ func NewRunDexCommand() *cobra.Command {
|
||||
} else {
|
||||
err = ioutil.WriteFile("/tmp/dex.yaml", dexCfgBytes, 0644)
|
||||
errors.CheckError(err)
|
||||
log.Info(string(dexCfgBytes))
|
||||
log.Debug(redactor(string(dexCfgBytes)))
|
||||
cmd = exec.Command("dex", "serve", "/tmp/dex.yaml")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
@@ -219,6 +221,7 @@ func NewImportCommand() *cobra.Command {
|
||||
os.Exit(1)
|
||||
}
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
config.QPS = 100
|
||||
config.Burst = 50
|
||||
errors.CheckError(err)
|
||||
@@ -241,43 +244,49 @@ func NewImportCommand() *cobra.Command {
|
||||
// pruneObjects tracks live objects and it's current resource version. any remaining
|
||||
// items in this map indicates the resource should be pruned since it no longer appears
|
||||
// in the backup
|
||||
pruneObjects := make(map[kube.ResourceKey]string)
|
||||
pruneObjects := make(map[kube.ResourceKey]unstructured.Unstructured)
|
||||
configMaps, err := acdClients.configMaps.List(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
// referencedSecrets holds any secrets referenced in the argocd-cm configmap. These
|
||||
// secrets need to be imported too
|
||||
var referencedSecrets map[string]bool
|
||||
for _, cm := range configMaps.Items {
|
||||
cmName := cm.GetName()
|
||||
if cmName == common.ArgoCDConfigMapName || cmName == common.ArgoCDRBACConfigMapName {
|
||||
pruneObjects[kube.ResourceKey{Group: "", Kind: "ConfigMap", Name: cm.GetName()}] = cm.GetResourceVersion()
|
||||
if isArgoCDConfigMap(cm.GetName()) {
|
||||
pruneObjects[kube.ResourceKey{Group: "", Kind: "ConfigMap", Name: cm.GetName()}] = cm
|
||||
}
|
||||
if cm.GetName() == common.ArgoCDConfigMapName {
|
||||
referencedSecrets = getReferencedSecrets(cm)
|
||||
}
|
||||
}
|
||||
|
||||
secrets, err := acdClients.secrets.List(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, secret := range secrets.Items {
|
||||
if isArgoCDSecret(nil, secret) {
|
||||
pruneObjects[kube.ResourceKey{Group: "", Kind: "Secret", Name: secret.GetName()}] = secret.GetResourceVersion()
|
||||
if isArgoCDSecret(referencedSecrets, secret) {
|
||||
pruneObjects[kube.ResourceKey{Group: "", Kind: "Secret", Name: secret.GetName()}] = secret
|
||||
}
|
||||
}
|
||||
applications, err := acdClients.applications.List(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, app := range applications.Items {
|
||||
pruneObjects[kube.ResourceKey{Group: "argoproj.io", Kind: "Application", Name: app.GetName()}] = app.GetResourceVersion()
|
||||
pruneObjects[kube.ResourceKey{Group: "argoproj.io", Kind: "Application", Name: app.GetName()}] = app
|
||||
}
|
||||
projects, err := acdClients.projects.List(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, proj := range projects.Items {
|
||||
pruneObjects[kube.ResourceKey{Group: "argoproj.io", Kind: "AppProject", Name: proj.GetName()}] = proj.GetResourceVersion()
|
||||
pruneObjects[kube.ResourceKey{Group: "argoproj.io", Kind: "AppProject", Name: proj.GetName()}] = proj
|
||||
}
|
||||
|
||||
// Create or replace existing object
|
||||
objs, err := kube.SplitYAML(string(input))
|
||||
backupObjects, err := kube.SplitYAML(string(input))
|
||||
errors.CheckError(err)
|
||||
for _, obj := range objs {
|
||||
gvk := obj.GroupVersionKind()
|
||||
key := kube.ResourceKey{Group: gvk.Group, Kind: gvk.Kind, Name: obj.GetName()}
|
||||
resourceVersion, exists := pruneObjects[key]
|
||||
for _, bakObj := range backupObjects {
|
||||
gvk := bakObj.GroupVersionKind()
|
||||
key := kube.ResourceKey{Group: gvk.Group, Kind: gvk.Kind, Name: bakObj.GetName()}
|
||||
liveObj, exists := pruneObjects[key]
|
||||
delete(pruneObjects, key)
|
||||
var dynClient dynamic.ResourceInterface
|
||||
switch obj.GetKind() {
|
||||
switch bakObj.GetKind() {
|
||||
case "Secret":
|
||||
dynClient = acdClients.secrets
|
||||
case "ConfigMap":
|
||||
@@ -289,17 +298,19 @@ func NewImportCommand() *cobra.Command {
|
||||
}
|
||||
if !exists {
|
||||
if !dryRun {
|
||||
_, err = dynClient.Create(obj, metav1.CreateOptions{})
|
||||
_, err = dynClient.Create(bakObj, metav1.CreateOptions{})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
fmt.Printf("%s/%s %s created%s\n", gvk.Group, gvk.Kind, obj.GetName(), dryRunMsg)
|
||||
fmt.Printf("%s/%s %s created%s\n", gvk.Group, gvk.Kind, bakObj.GetName(), dryRunMsg)
|
||||
} else if specsEqual(*bakObj, liveObj) {
|
||||
fmt.Printf("%s/%s %s unchanged%s\n", gvk.Group, gvk.Kind, bakObj.GetName(), dryRunMsg)
|
||||
} else {
|
||||
if !dryRun {
|
||||
obj.SetResourceVersion(resourceVersion)
|
||||
_, err = dynClient.Update(obj, metav1.UpdateOptions{})
|
||||
newLive := updateLive(bakObj, &liveObj)
|
||||
_, err = dynClient.Update(newLive, metav1.UpdateOptions{})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
fmt.Printf("%s/%s %s replaced%s\n", gvk.Group, gvk.Kind, obj.GetName(), dryRunMsg)
|
||||
fmt.Printf("%s/%s %s updated%s\n", gvk.Group, gvk.Kind, bakObj.GetName(), dryRunMsg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,8 +386,14 @@ func NewExportCommand() *cobra.Command {
|
||||
} else {
|
||||
f, err := os.Create(out)
|
||||
errors.CheckError(err)
|
||||
defer util.Close(f)
|
||||
writer = bufio.NewWriter(f)
|
||||
bw := bufio.NewWriter(f)
|
||||
writer = bw
|
||||
defer func() {
|
||||
err = bw.Flush()
|
||||
errors.CheckError(err)
|
||||
err = f.Close()
|
||||
errors.CheckError(err)
|
||||
}()
|
||||
}
|
||||
|
||||
acdClients := newArgoCDClientsets(config, namespace)
|
||||
@@ -386,6 +403,12 @@ func NewExportCommand() *cobra.Command {
|
||||
acdRBACConfigMap, err := acdClients.configMaps.Get(common.ArgoCDRBACConfigMapName, metav1.GetOptions{})
|
||||
errors.CheckError(err)
|
||||
export(writer, *acdRBACConfigMap)
|
||||
acdKnownHostsConfigMap, err := acdClients.configMaps.Get(common.ArgoCDKnownHostsConfigMapName, metav1.GetOptions{})
|
||||
errors.CheckError(err)
|
||||
export(writer, *acdKnownHostsConfigMap)
|
||||
acdTLSCertsConfigMap, err := acdClients.configMaps.Get(common.ArgoCDTLSCertsConfigMapName, metav1.GetOptions{})
|
||||
errors.CheckError(err)
|
||||
export(writer, *acdTLSCertsConfigMap)
|
||||
|
||||
referencedSecrets := getReferencedSecrets(*acdConfigMap)
|
||||
secrets, err := acdClients.secrets.List(metav1.ListOptions{})
|
||||
@@ -421,11 +444,13 @@ func getReferencedSecrets(un unstructured.Unstructured) map[string]bool {
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, &cm)
|
||||
errors.CheckError(err)
|
||||
referencedSecrets := make(map[string]bool)
|
||||
|
||||
// Referenced repository secrets
|
||||
if reposRAW, ok := cm.Data["repositories"]; ok {
|
||||
repoCreds := make([]settings.RepoCredentials, 0)
|
||||
err := yaml.Unmarshal([]byte(reposRAW), &repoCreds)
|
||||
repos := make([]settings.Repository, 0)
|
||||
err := yaml.Unmarshal([]byte(reposRAW), &repos)
|
||||
errors.CheckError(err)
|
||||
for _, cred := range repoCreds {
|
||||
for _, cred := range repos {
|
||||
if cred.PasswordSecret != nil {
|
||||
referencedSecrets[cred.PasswordSecret.Name] = true
|
||||
}
|
||||
@@ -435,27 +460,35 @@ func getReferencedSecrets(un unstructured.Unstructured) map[string]bool {
|
||||
if cred.UsernameSecret != nil {
|
||||
referencedSecrets[cred.UsernameSecret.Name] = true
|
||||
}
|
||||
if cred.TLSClientCertDataSecret != nil {
|
||||
referencedSecrets[cred.TLSClientCertDataSecret.Name] = true
|
||||
}
|
||||
if cred.TLSClientCertKeySecret != nil {
|
||||
referencedSecrets[cred.TLSClientCertKeySecret.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if helmReposRAW, ok := cm.Data["helm.repositories"]; ok {
|
||||
helmRepoCreds := make([]settings.HelmRepoCredentials, 0)
|
||||
err := yaml.Unmarshal([]byte(helmReposRAW), &helmRepoCreds)
|
||||
|
||||
// Referenced repository credentials secrets
|
||||
if reposRAW, ok := cm.Data["repository.credentials"]; ok {
|
||||
creds := make([]settings.RepositoryCredentials, 0)
|
||||
err := yaml.Unmarshal([]byte(reposRAW), &creds)
|
||||
errors.CheckError(err)
|
||||
for _, cred := range helmRepoCreds {
|
||||
if cred.CASecret != nil {
|
||||
referencedSecrets[cred.CASecret.Name] = true
|
||||
for _, cred := range creds {
|
||||
if cred.PasswordSecret != nil {
|
||||
referencedSecrets[cred.PasswordSecret.Name] = true
|
||||
}
|
||||
if cred.CertSecret != nil {
|
||||
referencedSecrets[cred.CertSecret.Name] = true
|
||||
}
|
||||
if cred.KeySecret != nil {
|
||||
referencedSecrets[cred.KeySecret.Name] = true
|
||||
if cred.SSHPrivateKeySecret != nil {
|
||||
referencedSecrets[cred.SSHPrivateKeySecret.Name] = true
|
||||
}
|
||||
if cred.UsernameSecret != nil {
|
||||
referencedSecrets[cred.UsernameSecret.Name] = true
|
||||
}
|
||||
if cred.PasswordSecret != nil {
|
||||
referencedSecrets[cred.PasswordSecret.Name] = true
|
||||
if cred.TLSClientCertDataSecret != nil {
|
||||
referencedSecrets[cred.TLSClientCertDataSecret.Name] = true
|
||||
}
|
||||
if cred.TLSClientCertKeySecret != nil {
|
||||
referencedSecrets[cred.TLSClientCertKeySecret.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -487,6 +520,73 @@ func isArgoCDSecret(repoSecretRefs map[string]bool, un unstructured.Unstructured
|
||||
return false
|
||||
}
|
||||
|
||||
// isArgoCDConfigMap returns true if the configmap name is one of argo cd's well known configmaps
|
||||
func isArgoCDConfigMap(name string) bool {
|
||||
switch name {
|
||||
case common.ArgoCDConfigMapName, common.ArgoCDRBACConfigMapName, common.ArgoCDKnownHostsConfigMapName, common.ArgoCDTLSCertsConfigMapName:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
// specsEqual returns if the spec, data, labels, annotations, and finalizers of the two
|
||||
// supplied objects are equal, indicating that no update is necessary during importing
|
||||
func specsEqual(left, right unstructured.Unstructured) bool {
|
||||
if !reflect.DeepEqual(left.GetAnnotations(), right.GetAnnotations()) {
|
||||
return false
|
||||
}
|
||||
if !reflect.DeepEqual(left.GetLabels(), right.GetLabels()) {
|
||||
return false
|
||||
}
|
||||
if !reflect.DeepEqual(left.GetFinalizers(), right.GetFinalizers()) {
|
||||
return false
|
||||
}
|
||||
switch left.GetKind() {
|
||||
case "Secret", "ConfigMap":
|
||||
leftData, _, _ := unstructured.NestedMap(left.Object, "data")
|
||||
rightData, _, _ := unstructured.NestedMap(right.Object, "data")
|
||||
return reflect.DeepEqual(leftData, rightData)
|
||||
case "AppProject":
|
||||
leftSpec, _, _ := unstructured.NestedMap(left.Object, "spec")
|
||||
rightSpec, _, _ := unstructured.NestedMap(right.Object, "spec")
|
||||
return reflect.DeepEqual(leftSpec, rightSpec)
|
||||
case "Application":
|
||||
leftSpec, _, _ := unstructured.NestedMap(left.Object, "spec")
|
||||
rightSpec, _, _ := unstructured.NestedMap(right.Object, "spec")
|
||||
leftStatus, _, _ := unstructured.NestedMap(left.Object, "status")
|
||||
rightStatus, _, _ := unstructured.NestedMap(right.Object, "status")
|
||||
// reconciledAt and observedAt are constantly changing and we ignore any diff there
|
||||
delete(leftStatus, "reconciledAt")
|
||||
delete(rightStatus, "reconciledAt")
|
||||
delete(leftStatus, "observedAt")
|
||||
delete(rightStatus, "observedAt")
|
||||
return reflect.DeepEqual(leftSpec, rightSpec) && reflect.DeepEqual(leftStatus, rightStatus)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// updateLive replaces the live object's finalizers, spec, annotations, labels, and data from the
|
||||
// backup object but leaves all other fields intact (status, other metadata, etc...)
|
||||
func updateLive(bak, live *unstructured.Unstructured) *unstructured.Unstructured {
|
||||
newLive := live.DeepCopy()
|
||||
newLive.SetAnnotations(bak.GetAnnotations())
|
||||
newLive.SetLabels(bak.GetLabels())
|
||||
newLive.SetFinalizers(bak.GetFinalizers())
|
||||
switch live.GetKind() {
|
||||
case "Secret", "ConfigMap":
|
||||
newLive.Object["data"] = bak.Object["data"]
|
||||
case "AppProject":
|
||||
newLive.Object["spec"] = bak.Object["spec"]
|
||||
case "Application":
|
||||
newLive.Object["spec"] = bak.Object["spec"]
|
||||
if _, ok := bak.Object["status"]; ok {
|
||||
newLive.Object["status"] = bak.Object["status"]
|
||||
}
|
||||
}
|
||||
return newLive
|
||||
}
|
||||
|
||||
// export writes the unstructured object and removes extraneous cruft from output before writing
|
||||
func export(w io.Writer, un unstructured.Unstructured) {
|
||||
name := un.GetName()
|
||||
@@ -534,7 +634,7 @@ func NewClusterConfig() *cobra.Command {
|
||||
|
||||
cluster, err := db.NewDB(namespace, settings.NewSettingsManager(context.Background(), kubeclientset, namespace), kubeclientset).GetCluster(context.Background(), serverUrl)
|
||||
errors.CheckError(err)
|
||||
err = kube.WriteKubeConfig(cluster.RESTConfig(), namespace, output)
|
||||
err = kube.WriteKubeConfig(cluster.RawRestConfig(), namespace, output)
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
@@ -542,6 +642,38 @@ func NewClusterConfig() *cobra.Command {
|
||||
return command
|
||||
}
|
||||
|
||||
func iterateStringFields(obj interface{}, callback func(name string, val string) string) {
|
||||
if mapField, ok := obj.(map[string]interface{}); ok {
|
||||
for field, val := range mapField {
|
||||
if strVal, ok := val.(string); ok {
|
||||
mapField[field] = callback(field, strVal)
|
||||
} else {
|
||||
iterateStringFields(val, callback)
|
||||
}
|
||||
}
|
||||
} else if arrayField, ok := obj.([]interface{}); ok {
|
||||
for i := range arrayField {
|
||||
iterateStringFields(arrayField[i], callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func redactor(dirtyString string) string {
|
||||
config := make(map[string]interface{})
|
||||
err := yaml.Unmarshal([]byte(dirtyString), &config)
|
||||
errors.CheckError(err)
|
||||
iterateStringFields(config, func(name string, val string) string {
|
||||
if name == "clientSecret" || name == "secret" || name == "bindPW" {
|
||||
return "********"
|
||||
} else {
|
||||
return val
|
||||
}
|
||||
})
|
||||
data, err := yaml.Marshal(config)
|
||||
errors.CheckError(err)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := NewCommand().Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
|
||||
94
cmd/argocd-util/main_test.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var textToRedact = `
|
||||
connectors:
|
||||
- config:
|
||||
clientID: aabbccddeeff00112233
|
||||
clientSecret: |
|
||||
theSecret
|
||||
orgs:
|
||||
- name: your-github-org
|
||||
redirectURI: https://argocd.example.com/api/dex/callback
|
||||
id: github
|
||||
name: GitHub
|
||||
type: github
|
||||
- config:
|
||||
bindDN: uid=serviceaccount,cn=users,dc=example,dc=com
|
||||
bindPW: theSecret
|
||||
host: ldap.example.com:636
|
||||
id: ldap
|
||||
name: LDAP
|
||||
type: ldap
|
||||
grpc:
|
||||
addr: 0.0.0.0:5557
|
||||
telemetry:
|
||||
http: 0.0.0.0:5558
|
||||
issuer: https://argocd.example.com/api/dex
|
||||
oauth2:
|
||||
skipApprovalScreen: true
|
||||
staticClients:
|
||||
- id: argo-cd
|
||||
name: Argo CD
|
||||
redirectURIs:
|
||||
- https://argocd.example.com/auth/callback
|
||||
secret: Dis9M-GA11oTwZVQQWdDklPQw-sWXZkWJFyyEhMs
|
||||
- id: argo-cd-cli
|
||||
name: Argo CD CLI
|
||||
public: true
|
||||
redirectURIs:
|
||||
- http://localhost
|
||||
storage:
|
||||
type: memory
|
||||
web:
|
||||
http: 0.0.0.0:5556`
|
||||
|
||||
var expectedRedaction = `connectors:
|
||||
- config:
|
||||
clientID: aabbccddeeff00112233
|
||||
clientSecret: '********'
|
||||
orgs:
|
||||
- name: your-github-org
|
||||
redirectURI: https://argocd.example.com/api/dex/callback
|
||||
id: github
|
||||
name: GitHub
|
||||
type: github
|
||||
- config:
|
||||
bindDN: uid=serviceaccount,cn=users,dc=example,dc=com
|
||||
bindPW: '********'
|
||||
host: ldap.example.com:636
|
||||
id: ldap
|
||||
name: LDAP
|
||||
type: ldap
|
||||
grpc:
|
||||
addr: 0.0.0.0:5557
|
||||
issuer: https://argocd.example.com/api/dex
|
||||
oauth2:
|
||||
skipApprovalScreen: true
|
||||
staticClients:
|
||||
- id: argo-cd
|
||||
name: Argo CD
|
||||
redirectURIs:
|
||||
- https://argocd.example.com/auth/callback
|
||||
secret: '********'
|
||||
- id: argo-cd-cli
|
||||
name: Argo CD CLI
|
||||
public: true
|
||||
redirectURIs:
|
||||
- http://localhost
|
||||
storage:
|
||||
type: memory
|
||||
telemetry:
|
||||
http: 0.0.0.0:5558
|
||||
web:
|
||||
http: 0.0.0.0:5556
|
||||
`
|
||||
|
||||
func TestSecretsRedactor(t *testing.T) {
|
||||
assert.Equal(t, expectedRedaction, redactor(textToRedact))
|
||||
}
|
||||
@@ -2,19 +2,29 @@ package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
timeutil "github.com/argoproj/pkg/time"
|
||||
"github.com/ghodss/yaml"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/server/account"
|
||||
accountpkg "github.com/argoproj/argo-cd/pkg/apiclient/account"
|
||||
"github.com/argoproj/argo-cd/pkg/apiclient/session"
|
||||
"github.com/argoproj/argo-cd/server/rbacpolicy"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
sessionutil "github.com/argoproj/argo-cd/util/session"
|
||||
)
|
||||
|
||||
func NewAccountCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
@@ -27,11 +37,18 @@ func NewAccountCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
},
|
||||
}
|
||||
command.AddCommand(NewAccountUpdatePasswordCommand(clientOpts))
|
||||
command.AddCommand(NewAccountGetUserInfoCommand(clientOpts))
|
||||
command.AddCommand(NewAccountCanICommand(clientOpts))
|
||||
command.AddCommand(NewAccountListCommand(clientOpts))
|
||||
command.AddCommand(NewAccountGenerateTokenCommand(clientOpts))
|
||||
command.AddCommand(NewAccountGetCommand(clientOpts))
|
||||
command.AddCommand(NewAccountDeleteTokenCommand(clientOpts))
|
||||
return command
|
||||
}
|
||||
|
||||
func NewAccountUpdatePasswordCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
account string
|
||||
currentPassword string
|
||||
newPassword string
|
||||
)
|
||||
@@ -43,53 +60,341 @@ func NewAccountUpdatePasswordCommand(clientOpts *argocdclient.ClientOptions) *co
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
acdClient := argocdclient.NewClientOrDie(clientOpts)
|
||||
conn, usrIf := acdClient.NewAccountClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
if currentPassword == "" {
|
||||
userInfo := getCurrentAccount(acdClient)
|
||||
|
||||
if userInfo.Iss == sessionutil.SessionManagerClaimsIssuer && currentPassword == "" {
|
||||
fmt.Print("*** Enter current password: ")
|
||||
password, err := terminal.ReadPassword(syscall.Stdin)
|
||||
password, err := terminal.ReadPassword(int(os.Stdin.Fd()))
|
||||
errors.CheckError(err)
|
||||
currentPassword = string(password)
|
||||
fmt.Print("\n")
|
||||
}
|
||||
|
||||
if newPassword == "" {
|
||||
var err error
|
||||
newPassword, err = cli.ReadAndConfirmPassword()
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
updatePasswordRequest := account.UpdatePasswordRequest{
|
||||
updatePasswordRequest := accountpkg.UpdatePasswordRequest{
|
||||
NewPassword: newPassword,
|
||||
CurrentPassword: currentPassword,
|
||||
Name: account,
|
||||
}
|
||||
|
||||
acdClient := argocdclient.NewClientOrDie(clientOpts)
|
||||
conn, usrIf := acdClient.NewAccountClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
ctx := context.Background()
|
||||
_, err := usrIf.UpdatePassword(ctx, &updatePasswordRequest)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Password updated\n")
|
||||
|
||||
// Get a new JWT token after updating the password
|
||||
localCfg, err := localconfig.ReadLocalConfig(clientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
configCtx, err := localCfg.ResolveContext(clientOpts.Context)
|
||||
errors.CheckError(err)
|
||||
claims, err := configCtx.User.Claims()
|
||||
errors.CheckError(err)
|
||||
tokenString := passwordLogin(acdClient, claims.Subject, newPassword)
|
||||
localCfg.UpsertUser(localconfig.User{
|
||||
Name: localCfg.CurrentContext,
|
||||
AuthToken: tokenString,
|
||||
})
|
||||
err = localconfig.WriteLocalConfig(*localCfg, clientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Context '%s' updated\n", localCfg.CurrentContext)
|
||||
if account == "" || account == userInfo.Username {
|
||||
// Get a new JWT token after updating the password
|
||||
localCfg, err := localconfig.ReadLocalConfig(clientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
configCtx, err := localCfg.ResolveContext(clientOpts.Context)
|
||||
errors.CheckError(err)
|
||||
claims, err := configCtx.User.Claims()
|
||||
errors.CheckError(err)
|
||||
tokenString := passwordLogin(acdClient, claims.Subject, newPassword)
|
||||
localCfg.UpsertUser(localconfig.User{
|
||||
Name: localCfg.CurrentContext,
|
||||
AuthToken: tokenString,
|
||||
})
|
||||
err = localconfig.WriteLocalConfig(*localCfg, clientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Context '%s' updated\n", localCfg.CurrentContext)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
command.Flags().StringVar(¤tPassword, "current-password", "", "current password you wish to change")
|
||||
command.Flags().StringVar(&newPassword, "new-password", "", "new password you want to update to")
|
||||
command.Flags().StringVar(&account, "account", "", "an account name that should be updated. Defaults to current user account")
|
||||
return command
|
||||
}
|
||||
|
||||
func NewAccountGetUserInfoCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "get-user-info",
|
||||
Short: "Get user info",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
conn, client := argocdclient.NewClientOrDie(clientOpts).NewSessionClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
ctx := context.Background()
|
||||
response, err := client.GetUserInfo(ctx, &session.GetUserInfoRequest{})
|
||||
errors.CheckError(err)
|
||||
|
||||
switch output {
|
||||
case "yaml":
|
||||
yamlBytes, err := yaml.Marshal(response)
|
||||
errors.CheckError(err)
|
||||
fmt.Println(string(yamlBytes))
|
||||
case "json":
|
||||
jsonBytes, err := json.MarshalIndent(response, "", " ")
|
||||
errors.CheckError(err)
|
||||
fmt.Println(string(jsonBytes))
|
||||
case "":
|
||||
fmt.Printf("Logged In: %v\n", response.LoggedIn)
|
||||
if response.LoggedIn {
|
||||
fmt.Printf("Username: %s\n", response.Username)
|
||||
fmt.Printf("Issuer: %s\n", response.Iss)
|
||||
fmt.Printf("Groups: %v\n", strings.Join(response.Groups, ","))
|
||||
}
|
||||
default:
|
||||
log.Fatalf("Unknown output format: %s", output)
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "", "Output format. One of: yaml, json")
|
||||
return command
|
||||
}
|
||||
|
||||
func NewAccountCanICommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "can-i ACTION RESOURCE SUBRESOURCE",
|
||||
Short: "Can I",
|
||||
Example: fmt.Sprintf(`
|
||||
# Can I sync any app?
|
||||
argocd account can-i sync applications '*'
|
||||
|
||||
# Can I update a project?
|
||||
argocd account can-i update projects 'default'
|
||||
|
||||
# Can I create a cluster?
|
||||
argocd account can-i create clusters '*'
|
||||
|
||||
Actions: %v
|
||||
Resources: %v
|
||||
`, rbacpolicy.Resources, rbacpolicy.Actions),
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
conn, client := argocdclient.NewClientOrDie(clientOpts).NewAccountClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
ctx := context.Background()
|
||||
response, err := client.CanI(ctx, &accountpkg.CanIRequest{
|
||||
Action: args[0],
|
||||
Resource: args[1],
|
||||
Subresource: args[2],
|
||||
})
|
||||
errors.CheckError(err)
|
||||
fmt.Println(response.Value)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func printAccountNames(accounts []*accountpkg.Account) {
|
||||
for _, p := range accounts {
|
||||
fmt.Println(p.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func printAccountsTable(items []*accountpkg.Account) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "NAME\tENABLED\tCAPABILITIES\n")
|
||||
for _, a := range items {
|
||||
fmt.Fprintf(w, "%s\t%v\t%s\n", a.Name, a.Enabled, strings.Join(a.Capabilities, ", "))
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
func NewAccountListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List accounts",
|
||||
Example: "argocd account list",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
|
||||
conn, client := argocdclient.NewClientOrDie(clientOpts).NewAccountClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
ctx := context.Background()
|
||||
response, err := client.ListAccounts(ctx, &accountpkg.ListAccountRequest{})
|
||||
|
||||
errors.CheckError(err)
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(response.Items, output, false)
|
||||
errors.CheckError(err)
|
||||
case "name":
|
||||
printAccountNames(response.Items)
|
||||
case "wide", "":
|
||||
printAccountsTable(response.Items)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|name")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func getCurrentAccount(clientset argocdclient.Client) session.GetUserInfoResponse {
|
||||
conn, client := clientset.NewSessionClientOrDie()
|
||||
defer util.Close(conn)
|
||||
userInfo, err := client.GetUserInfo(context.Background(), &session.GetUserInfoRequest{})
|
||||
errors.CheckError(err)
|
||||
return *userInfo
|
||||
}
|
||||
|
||||
func NewAccountGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
account string
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Get account details",
|
||||
Example: `# Get the currently logged in account details
|
||||
argocd account get
|
||||
|
||||
# Get details for an account by name
|
||||
argocd account get --account <account-name>`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
clientset := argocdclient.NewClientOrDie(clientOpts)
|
||||
|
||||
if account == "" {
|
||||
account = getCurrentAccount(clientset).Username
|
||||
}
|
||||
|
||||
conn, client := clientset.NewAccountClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
acc, err := client.GetAccount(context.Background(), &accountpkg.GetAccountRequest{Name: account})
|
||||
|
||||
errors.CheckError(err)
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(acc, output, true)
|
||||
errors.CheckError(err)
|
||||
case "name":
|
||||
fmt.Println(acc.Name)
|
||||
case "wide", "":
|
||||
printAccountDetails(acc)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|name")
|
||||
cmd.Flags().StringVarP(&account, "account", "a", "", "Account name. Defaults to the current account.")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func printAccountDetails(acc *accountpkg.Account) {
|
||||
fmt.Printf(printOpFmtStr, "Name:", acc.Name)
|
||||
fmt.Printf(printOpFmtStr, "Enabled:", strconv.FormatBool(acc.Enabled))
|
||||
fmt.Printf(printOpFmtStr, "Capabilities:", strings.Join(acc.Capabilities, ", "))
|
||||
fmt.Println("\nTokens:")
|
||||
if len(acc.Tokens) == 0 {
|
||||
fmt.Println("NONE")
|
||||
} else {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "ID\tISSUED AT\tEXPIRING AT\n")
|
||||
for _, t := range acc.Tokens {
|
||||
expiresAtFormatted := "never"
|
||||
if t.ExpiresAt > 0 {
|
||||
expiresAt := time.Unix(t.ExpiresAt, 0)
|
||||
expiresAtFormatted = expiresAt.Format(time.RFC3339)
|
||||
if expiresAt.Before(time.Now()) {
|
||||
expiresAtFormatted = fmt.Sprintf("%s (expired)", expiresAtFormatted)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", t.Id, time.Unix(t.IssuedAt, 0).Format(time.RFC3339), expiresAtFormatted)
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func NewAccountGenerateTokenCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
account string
|
||||
expiresIn string
|
||||
id string
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "generate-token",
|
||||
Short: "Generate account token",
|
||||
Example: `# Generate token for the currently logged in account
|
||||
argocd account generate-token
|
||||
|
||||
# Generate token for the account with the specified name
|
||||
argocd account generate-token --account <account-name>`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
|
||||
clientset := argocdclient.NewClientOrDie(clientOpts)
|
||||
conn, client := clientset.NewAccountClientOrDie()
|
||||
defer util.Close(conn)
|
||||
if account == "" {
|
||||
account = getCurrentAccount(clientset).Username
|
||||
}
|
||||
expiresIn, err := timeutil.ParseDuration(expiresIn)
|
||||
errors.CheckError(err)
|
||||
response, err := client.CreateToken(context.Background(), &accountpkg.CreateTokenRequest{
|
||||
Name: account,
|
||||
ExpiresIn: int64(expiresIn.Seconds()),
|
||||
Id: id,
|
||||
})
|
||||
errors.CheckError(err)
|
||||
fmt.Println(response.Token)
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&account, "account", "a", "", "Account name. Defaults to the current account.")
|
||||
cmd.Flags().StringVarP(&expiresIn, "expires-in", "e", "0s", "Duration before the token will expire. (Default: No expiration)")
|
||||
cmd.Flags().StringVar(&id, "id", "", "Optional token id. Fallback to uuid if not value specified.")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewAccountDeleteTokenCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
account string
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete-token",
|
||||
Short: "Deletes account token",
|
||||
Example: `# Delete token of the currently logged in account
|
||||
argocd account delete-token ID
|
||||
|
||||
# Delete token of the account with the specified name
|
||||
argocd account generate-token --account <account-name>`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
id := args[0]
|
||||
|
||||
clientset := argocdclient.NewClientOrDie(clientOpts)
|
||||
conn, client := clientset.NewAccountClientOrDie()
|
||||
defer util.Close(conn)
|
||||
if account == "" {
|
||||
account = getCurrentAccount(clientset).Username
|
||||
}
|
||||
_, err := client.DeleteToken(context.Background(), &accountpkg.DeleteTokenRequest{Name: account, Id: id})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&account, "account", "a", "", "Account name. Defaults to the current account.")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -2,20 +2,30 @@ package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/server/application"
|
||||
applicationpkg "github.com/argoproj/argo-cd/pkg/apiclient/application"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
)
|
||||
|
||||
type DisplayedAction struct {
|
||||
Group string
|
||||
Kind string
|
||||
Name string
|
||||
Action string
|
||||
Disabled bool
|
||||
}
|
||||
|
||||
// NewApplicationResourceActionsCommand returns a new instance of an `argocd app actions` command
|
||||
func NewApplicationResourceActionsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
@@ -37,7 +47,7 @@ func NewApplicationResourceActionsListCommand(clientOpts *argocdclient.ClientOpt
|
||||
var kind string
|
||||
var group string
|
||||
var resourceName string
|
||||
var all bool
|
||||
var output string
|
||||
var command = &cobra.Command{
|
||||
Use: "list APPNAME",
|
||||
Short: "Lists available actions on a resource",
|
||||
@@ -51,14 +61,14 @@ func NewApplicationResourceActionsListCommand(clientOpts *argocdclient.ClientOpt
|
||||
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
|
||||
defer util.Close(conn)
|
||||
ctx := context.Background()
|
||||
resources, err := appIf.ManagedResources(ctx, &application.ResourcesQuery{ApplicationName: &appName})
|
||||
resources, err := appIf.ManagedResources(ctx, &applicationpkg.ResourcesQuery{ApplicationName: &appName})
|
||||
errors.CheckError(err)
|
||||
filteredObjects := filterResources(command, resources.Items, group, kind, namespace, resourceName, all)
|
||||
availableActions := make(map[string][]argoappv1.ResourceAction)
|
||||
filteredObjects := filterResources(command, resources.Items, group, kind, namespace, resourceName, true)
|
||||
var availableActions []DisplayedAction
|
||||
for i := range filteredObjects {
|
||||
obj := filteredObjects[i]
|
||||
gvk := obj.GroupVersionKind()
|
||||
availActionsForResource, err := appIf.ListResourceActions(ctx, &application.ApplicationResourceRequest{
|
||||
availActionsForResource, err := appIf.ListResourceActions(ctx, &applicationpkg.ApplicationResourceRequest{
|
||||
Name: &appName,
|
||||
Namespace: obj.GetNamespace(),
|
||||
ResourceName: obj.GetName(),
|
||||
@@ -66,34 +76,42 @@ func NewApplicationResourceActionsListCommand(clientOpts *argocdclient.ClientOpt
|
||||
Kind: gvk.Kind,
|
||||
})
|
||||
errors.CheckError(err)
|
||||
availableActions[obj.GetName()] = availActionsForResource.Actions
|
||||
}
|
||||
|
||||
var keys []string
|
||||
for key := range availableActions {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "RESOURCE\tACTION\n")
|
||||
fmt.Println()
|
||||
for key := range availableActions {
|
||||
for i := range availableActions[key] {
|
||||
action := availableActions[key][i]
|
||||
fmt.Fprintf(w, "%s\t%s\n", key, action.Name)
|
||||
|
||||
for _, action := range availActionsForResource.Actions {
|
||||
displayAction := DisplayedAction{
|
||||
Group: gvk.Group,
|
||||
Kind: gvk.Kind,
|
||||
Name: obj.GetName(),
|
||||
Action: action.Name,
|
||||
Disabled: action.Disabled,
|
||||
}
|
||||
availableActions = append(availableActions, displayAction)
|
||||
}
|
||||
}
|
||||
_ = w.Flush()
|
||||
|
||||
switch output {
|
||||
case "yaml":
|
||||
yamlBytes, err := yaml.Marshal(availableActions)
|
||||
errors.CheckError(err)
|
||||
fmt.Println(string(yamlBytes))
|
||||
case "json":
|
||||
jsonBytes, err := json.MarshalIndent(availableActions, "", " ")
|
||||
errors.CheckError(err)
|
||||
fmt.Println(string(jsonBytes))
|
||||
case "":
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "GROUP\tKIND\tNAME\tACTION\tDISABLED\n")
|
||||
fmt.Println()
|
||||
for _, action := range availableActions {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", action.Group, action.Kind, action.Name, action.Action, strconv.FormatBool(action.Disabled))
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
}
|
||||
command.Flags().StringVar(&resourceName, "resource-name", "", "Name of resource")
|
||||
command.Flags().StringVar(&kind, "kind", "", "Kind")
|
||||
err := command.MarkFlagRequired("kind")
|
||||
errors.CheckError(err)
|
||||
command.Flags().StringVar(&group, "group", "", "Group")
|
||||
command.Flags().StringVar(&namespace, "namespace", "", "Namespace")
|
||||
command.Flags().BoolVar(&all, "all", false, "Indicates whether to list actions on multiple matching resources")
|
||||
command.Flags().StringVarP(&output, "out", "o", "", "Output format. One of: yaml, json")
|
||||
|
||||
return command
|
||||
}
|
||||
@@ -101,9 +119,9 @@ func NewApplicationResourceActionsListCommand(clientOpts *argocdclient.ClientOpt
|
||||
// NewApplicationResourceActionsRunCommand returns a new instance of an `argocd app actions run` command
|
||||
func NewApplicationResourceActionsRunCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var namespace string
|
||||
var resourceName string
|
||||
var kind string
|
||||
var group string
|
||||
var resourceName string
|
||||
var all bool
|
||||
var command = &cobra.Command{
|
||||
Use: "run APPNAME ACTION",
|
||||
@@ -111,11 +129,10 @@ func NewApplicationResourceActionsRunCommand(clientOpts *argocdclient.ClientOpti
|
||||
}
|
||||
|
||||
command.Flags().StringVar(&resourceName, "resource-name", "", "Name of resource")
|
||||
command.Flags().StringVar(&kind, "kind", "", "Kind")
|
||||
err := command.MarkFlagRequired("kind")
|
||||
errors.CheckError(err)
|
||||
command.Flags().StringVar(&group, "group", "", "Group")
|
||||
command.Flags().StringVar(&namespace, "namespace", "", "Namespace")
|
||||
command.Flags().StringVar(&kind, "kind", "", "Kind")
|
||||
command.Flags().StringVar(&group, "group", "", "Group")
|
||||
errors.CheckError(command.MarkFlagRequired("kind"))
|
||||
command.Flags().BoolVar(&all, "all", false, "Indicates whether to run the action on multiple matching resources")
|
||||
|
||||
command.Run = func(c *cobra.Command, args []string) {
|
||||
@@ -125,17 +142,25 @@ func NewApplicationResourceActionsRunCommand(clientOpts *argocdclient.ClientOpti
|
||||
}
|
||||
appName := args[0]
|
||||
actionName := args[1]
|
||||
|
||||
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
|
||||
defer util.Close(conn)
|
||||
ctx := context.Background()
|
||||
resources, err := appIf.ManagedResources(ctx, &application.ResourcesQuery{ApplicationName: &appName})
|
||||
resources, err := appIf.ManagedResources(ctx, &applicationpkg.ResourcesQuery{ApplicationName: &appName})
|
||||
errors.CheckError(err)
|
||||
filteredObjects := filterResources(command, resources.Items, group, kind, namespace, resourceName, all)
|
||||
var resGroup = filteredObjects[0].GroupVersionKind().Group
|
||||
for i := range filteredObjects[1:] {
|
||||
if filteredObjects[i].GroupVersionKind().Group != resGroup {
|
||||
log.Fatal("Ambiguous resource group. Use flag --group to specify resource group explicitly.")
|
||||
}
|
||||
}
|
||||
|
||||
for i := range filteredObjects {
|
||||
obj := filteredObjects[i]
|
||||
gvk := obj.GroupVersionKind()
|
||||
objResourceName := obj.GetName()
|
||||
_, err := appIf.RunResourceAction(context.Background(), &application.ResourceActionRunRequest{
|
||||
_, err := appIf.RunResourceAction(context.Background(), &applicationpkg.ResourceActionRunRequest{
|
||||
Name: &appName,
|
||||
Namespace: obj.GetNamespace(),
|
||||
ResourceName: objResourceName,
|
||||
|
||||
104
cmd/argocd/commands/app_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
func Test_setHelmOpt(t *testing.T) {
|
||||
t.Run("Zero", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setHelmOpt(&src, helmOpts{})
|
||||
assert.Nil(t, src.Helm)
|
||||
})
|
||||
t.Run("ValueFiles", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setHelmOpt(&src, helmOpts{valueFiles: []string{"foo"}})
|
||||
assert.Equal(t, []string{"foo"}, src.Helm.ValueFiles)
|
||||
})
|
||||
t.Run("ReleaseName", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setHelmOpt(&src, helmOpts{releaseName: "foo"})
|
||||
assert.Equal(t, "foo", src.Helm.ReleaseName)
|
||||
})
|
||||
t.Run("HelmSets", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setHelmOpt(&src, helmOpts{helmSets: []string{"foo=bar"}})
|
||||
assert.Equal(t, []v1alpha1.HelmParameter{{Name: "foo", Value: "bar"}}, src.Helm.Parameters)
|
||||
})
|
||||
t.Run("HelmSetStrings", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setHelmOpt(&src, helmOpts{helmSetStrings: []string{"foo=bar"}})
|
||||
assert.Equal(t, []v1alpha1.HelmParameter{{Name: "foo", Value: "bar", ForceString: true}}, src.Helm.Parameters)
|
||||
})
|
||||
t.Run("HelmSetFiles", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setHelmOpt(&src, helmOpts{helmSetFiles: []string{"foo=bar"}})
|
||||
assert.Equal(t, []v1alpha1.HelmFileParameter{{Name: "foo", Path: "bar"}}, src.Helm.FileParameters)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_setJsonnetOpt(t *testing.T) {
|
||||
t.Run("TlaSets", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setJsonnetOpt(&src, []string{"foo=bar"}, false)
|
||||
assert.Equal(t, []v1alpha1.JsonnetVar{{Name: "foo", Value: "bar"}}, src.Directory.Jsonnet.TLAs)
|
||||
setJsonnetOpt(&src, []string{"bar=baz"}, false)
|
||||
assert.Equal(t, []v1alpha1.JsonnetVar{{Name: "foo", Value: "bar"}, {Name: "bar", Value: "baz"}}, src.Directory.Jsonnet.TLAs)
|
||||
})
|
||||
t.Run("ExtSets", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setJsonnetOptExtVar(&src, []string{"foo=bar"}, false)
|
||||
assert.Equal(t, []v1alpha1.JsonnetVar{{Name: "foo", Value: "bar"}}, src.Directory.Jsonnet.ExtVars)
|
||||
setJsonnetOptExtVar(&src, []string{"bar=baz"}, false)
|
||||
assert.Equal(t, []v1alpha1.JsonnetVar{{Name: "foo", Value: "bar"}, {Name: "bar", Value: "baz"}}, src.Directory.Jsonnet.ExtVars)
|
||||
})
|
||||
}
|
||||
|
||||
type appOptionsFixture struct {
|
||||
spec *v1alpha1.ApplicationSpec
|
||||
command *cobra.Command
|
||||
options *appOptions
|
||||
}
|
||||
|
||||
func (f *appOptionsFixture) SetFlag(key, value string) error {
|
||||
err := f.command.Flags().Set(key, value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = setAppSpecOptions(f.command.Flags(), f.spec, f.options)
|
||||
return err
|
||||
}
|
||||
|
||||
func newAppOptionsFixture() *appOptionsFixture {
|
||||
fixture := &appOptionsFixture{
|
||||
spec: &v1alpha1.ApplicationSpec{},
|
||||
command: &cobra.Command{},
|
||||
options: &appOptions{},
|
||||
}
|
||||
addAppFlags(fixture.command, fixture.options)
|
||||
return fixture
|
||||
}
|
||||
|
||||
func Test_setAppSpecOptions(t *testing.T) {
|
||||
f := newAppOptionsFixture()
|
||||
t.Run("SyncPolicy", func(t *testing.T) {
|
||||
assert.NoError(t, f.SetFlag("sync-policy", "automated"))
|
||||
assert.NotNil(t, f.spec.SyncPolicy.Automated)
|
||||
|
||||
assert.NoError(t, f.SetFlag("sync-policy", "none"))
|
||||
assert.Nil(t, f.spec.SyncPolicy)
|
||||
})
|
||||
t.Run("SyncOptions", func(t *testing.T) {
|
||||
assert.NoError(t, f.SetFlag("sync-option", "a=1"))
|
||||
assert.True(t, f.spec.SyncPolicy.SyncOptions.HasOption("a=1"))
|
||||
|
||||
// remove the options using !
|
||||
assert.NoError(t, f.SetFlag("sync-option", "!a=1"))
|
||||
assert.Nil(t, f.spec.SyncPolicy)
|
||||
})
|
||||
}
|
||||
322
cmd/argocd/commands/cert.go
Normal file
@@ -0,0 +1,322 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
certificatepkg "github.com/argoproj/argo-cd/pkg/apiclient/certificate"
|
||||
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
certutil "github.com/argoproj/argo-cd/util/cert"
|
||||
|
||||
"crypto/x509"
|
||||
)
|
||||
|
||||
// NewCertCommand returns a new instance of an `argocd repo` command
|
||||
func NewCertCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "cert",
|
||||
Short: "Manage repository certificates and SSH known hosts entries",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
},
|
||||
Example: ` # Add a TLS certificate for cd.example.com to ArgoCD cert store from a file
|
||||
argocd cert add-tls --from ~/mycert.pem cd.example.com
|
||||
|
||||
# Add a TLS certificate for cd.example.com to ArgoCD via stdin
|
||||
cat ~/mycert.pem | argocd cert add-tls cd.example.com
|
||||
|
||||
# Add SSH known host entries for cd.example.com to ArgoCD by scanning host
|
||||
ssh-keyscan cd.example.com | argocd cert add-ssh --batch
|
||||
|
||||
# List all known TLS certificates
|
||||
argocd cert list --cert-type https
|
||||
|
||||
# Remove all TLS certificates for cd.example.com
|
||||
argocd cert rm --cert-type https cd.example.com
|
||||
|
||||
# Remove all certificates and SSH known host entries for cd.example.com
|
||||
argocd cert rm cd.example.com
|
||||
`,
|
||||
}
|
||||
|
||||
command.AddCommand(NewCertAddSSHCommand(clientOpts))
|
||||
command.AddCommand(NewCertAddTLSCommand(clientOpts))
|
||||
command.AddCommand(NewCertListCommand(clientOpts))
|
||||
command.AddCommand(NewCertRemoveCommand(clientOpts))
|
||||
return command
|
||||
}
|
||||
|
||||
func NewCertAddTLSCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
fromFile string
|
||||
upsert bool
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "add-tls SERVERNAME",
|
||||
Short: "Add TLS certificate data for connecting to repository server SERVERNAME",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
conn, certIf := argocdclient.NewClientOrDie(clientOpts).NewCertClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var certificateArray []string
|
||||
var err error
|
||||
|
||||
if fromFile != "" {
|
||||
fmt.Printf("Reading TLS certificate data in PEM format from '%s'\n", fromFile)
|
||||
certificateArray, err = certutil.ParseTLSCertificatesFromPath(fromFile)
|
||||
} else {
|
||||
fmt.Println("Enter TLS certificate data in PEM format. Press CTRL-D when finished.")
|
||||
certificateArray, err = certutil.ParseTLSCertificatesFromStream(os.Stdin)
|
||||
}
|
||||
|
||||
errors.CheckError(err)
|
||||
|
||||
certificateList := make([]appsv1.RepositoryCertificate, 0)
|
||||
|
||||
subjectMap := make(map[string]*x509.Certificate)
|
||||
|
||||
for _, entry := range certificateArray {
|
||||
// We want to make sure to only send valid certificate data to the
|
||||
// server, so we decode the certificate into X509 structure before
|
||||
// further processing it.
|
||||
x509cert, err := certutil.DecodePEMCertificateToX509(entry)
|
||||
errors.CheckError(err)
|
||||
|
||||
// TODO: We need a better way to detect duplicates sent in the stream,
|
||||
// maybe by using fingerprints? For now, no two certs with the same
|
||||
// subject may be sent.
|
||||
if subjectMap[x509cert.Subject.String()] != nil {
|
||||
fmt.Printf("ERROR: Cert with subject '%s' already seen in the input stream.\n", x509cert.Subject.String())
|
||||
continue
|
||||
} else {
|
||||
subjectMap[x509cert.Subject.String()] = x509cert
|
||||
}
|
||||
}
|
||||
|
||||
serverName := args[0]
|
||||
|
||||
if len(certificateArray) > 0 {
|
||||
certificateList = append(certificateList, appsv1.RepositoryCertificate{
|
||||
ServerName: serverName,
|
||||
CertType: "https",
|
||||
CertData: []byte(strings.Join(certificateArray, "\n")),
|
||||
})
|
||||
certificates, err := certIf.CreateCertificate(context.Background(), &certificatepkg.RepositoryCertificateCreateRequest{
|
||||
Certificates: &appsv1.RepositoryCertificateList{
|
||||
Items: certificateList,
|
||||
},
|
||||
Upsert: upsert,
|
||||
})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Created entry with %d PEM certificates for repository server %s\n", len(certificates.Items), serverName)
|
||||
} else {
|
||||
fmt.Printf("No valid certificates have been detected in the stream.\n")
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&fromFile, "from", "", "read TLS certificate data from file (default is to read from stdin)")
|
||||
command.Flags().BoolVar(&upsert, "upsert", false, "Replace existing TLS certificate if certificate is different in input")
|
||||
return command
|
||||
}
|
||||
|
||||
// NewCertAddCommand returns a new instance of an `argocd cert add` command
|
||||
func NewCertAddSSHCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
fromFile string
|
||||
batchProcess bool
|
||||
upsert bool
|
||||
certificates []appsv1.RepositoryCertificate
|
||||
)
|
||||
|
||||
var command = &cobra.Command{
|
||||
Use: "add-ssh --batch",
|
||||
Short: "Add SSH known host entries for repository servers",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
|
||||
conn, certIf := argocdclient.NewClientOrDie(clientOpts).NewCertClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
var sshKnownHostsLists []string
|
||||
var err error
|
||||
|
||||
// --batch is a flag, but it is mandatory for now.
|
||||
if batchProcess {
|
||||
if fromFile != "" {
|
||||
fmt.Printf("Reading SSH known hosts entries from file '%s'\n", fromFile)
|
||||
sshKnownHostsLists, err = certutil.ParseSSHKnownHostsFromPath(fromFile)
|
||||
} else {
|
||||
fmt.Println("Enter SSH known hosts entries, one per line. Press CTRL-D when finished.")
|
||||
sshKnownHostsLists, err = certutil.ParseSSHKnownHostsFromStream(os.Stdin)
|
||||
}
|
||||
} else {
|
||||
err = fmt.Errorf("You need to specify --batch or specify --help for usage instructions")
|
||||
}
|
||||
|
||||
errors.CheckError(err)
|
||||
|
||||
if len(sshKnownHostsLists) == 0 {
|
||||
errors.CheckError(fmt.Errorf("No valid SSH known hosts data found."))
|
||||
}
|
||||
|
||||
for _, knownHostsEntry := range sshKnownHostsLists {
|
||||
_, certSubType, certData, err := certutil.TokenizeSSHKnownHostsEntry(knownHostsEntry)
|
||||
errors.CheckError(err)
|
||||
hostnameList, _, err := certutil.KnownHostsLineToPublicKey(knownHostsEntry)
|
||||
errors.CheckError(err)
|
||||
// Each key could be valid for multiple hostnames
|
||||
for _, hostname := range hostnameList {
|
||||
certificate := appsv1.RepositoryCertificate{
|
||||
ServerName: hostname,
|
||||
CertType: "ssh",
|
||||
CertSubType: certSubType,
|
||||
CertData: certData,
|
||||
}
|
||||
certificates = append(certificates, certificate)
|
||||
}
|
||||
}
|
||||
|
||||
certList := &appsv1.RepositoryCertificateList{Items: certificates}
|
||||
response, err := certIf.CreateCertificate(context.Background(), &certificatepkg.RepositoryCertificateCreateRequest{
|
||||
Certificates: certList,
|
||||
Upsert: upsert,
|
||||
})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Successfully created %d SSH known host entries\n", len(response.Items))
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&fromFile, "from", "", "Read SSH known hosts data from file (default is to read from stdin)")
|
||||
command.Flags().BoolVar(&batchProcess, "batch", false, "Perform batch processing by reading in SSH known hosts data (mandatory flag)")
|
||||
command.Flags().BoolVar(&upsert, "upsert", false, "Replace existing SSH server public host keys if key is different in input")
|
||||
return command
|
||||
}
|
||||
|
||||
// NewCertRemoveCommand returns a new instance of an `argocd cert rm` command
|
||||
func NewCertRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
certType string
|
||||
certSubType string
|
||||
certQuery certificatepkg.RepositoryCertificateQuery
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "rm REPOSERVER",
|
||||
Short: "Remove certificate of TYPE for REPOSERVER",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) < 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
conn, certIf := argocdclient.NewClientOrDie(clientOpts).NewCertClientOrDie()
|
||||
defer util.Close(conn)
|
||||
hostNamePattern := args[0]
|
||||
|
||||
// Prevent the user from specifying a wildcard as hostname as precaution
|
||||
// measure -- the user could still use "?*" or any other pattern to
|
||||
// remove all certificates, but it's less likely that it happens by
|
||||
// accident.
|
||||
if hostNamePattern == "*" {
|
||||
err := fmt.Errorf("A single wildcard is not allowed as REPOSERVER name.")
|
||||
errors.CheckError(err)
|
||||
}
|
||||
certQuery = certificatepkg.RepositoryCertificateQuery{
|
||||
HostNamePattern: hostNamePattern,
|
||||
CertType: certType,
|
||||
CertSubType: certSubType,
|
||||
}
|
||||
removed, err := certIf.DeleteCertificate(context.Background(), &certQuery)
|
||||
errors.CheckError(err)
|
||||
if len(removed.Items) > 0 {
|
||||
for _, cert := range removed.Items {
|
||||
fmt.Printf("Removed cert for '%s' of type '%s' (subtype '%s')\n", cert.ServerName, cert.CertType, cert.CertSubType)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("No certificates were removed (none matched the given patterns)")
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&certType, "cert-type", "", "Only remove certs of given type (ssh, https)")
|
||||
command.Flags().StringVar(&certSubType, "cert-sub-type", "", "Only remove certs of given sub-type (only for ssh)")
|
||||
return command
|
||||
}
|
||||
|
||||
// NewCertListCommand returns a new instance of an `argocd cert rm` command
|
||||
func NewCertListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
certType string
|
||||
hostNamePattern string
|
||||
sortOrder string
|
||||
output string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List configured certificates",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if certType != "" {
|
||||
switch certType {
|
||||
case "ssh":
|
||||
case "https":
|
||||
default:
|
||||
fmt.Println("cert-type must be either ssh or https")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
conn, certIf := argocdclient.NewClientOrDie(clientOpts).NewCertClientOrDie()
|
||||
defer util.Close(conn)
|
||||
certificates, err := certIf.ListCertificates(context.Background(), &certificatepkg.RepositoryCertificateQuery{HostNamePattern: hostNamePattern, CertType: certType})
|
||||
errors.CheckError(err)
|
||||
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(certificates.Items, output, false)
|
||||
errors.CheckError(err)
|
||||
case "wide", "":
|
||||
printCertTable(certificates.Items, sortOrder)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide")
|
||||
command.Flags().StringVar(&sortOrder, "sort", "", "set display sort order for output format wide. One of: hostname|type")
|
||||
command.Flags().StringVar(&certType, "cert-type", "", "only list certificates of given type, valid: 'ssh','https'")
|
||||
command.Flags().StringVar(&hostNamePattern, "hostname-pattern", "", "only list certificates for hosts matching given glob-pattern")
|
||||
return command
|
||||
}
|
||||
|
||||
// Print table of certificate info
|
||||
func printCertTable(certs []appsv1.RepositoryCertificate, sortOrder string) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "HOSTNAME\tTYPE\tSUBTYPE\tINFO\n")
|
||||
|
||||
if sortOrder == "hostname" || sortOrder == "" {
|
||||
sort.Slice(certs, func(i, j int) bool {
|
||||
return certs[i].ServerName < certs[j].ServerName
|
||||
})
|
||||
} else if sortOrder == "type" {
|
||||
sort.Slice(certs, func(i, j int) bool {
|
||||
return certs[i].CertType < certs[j].CertType
|
||||
})
|
||||
}
|
||||
|
||||
for _, c := range certs {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", c.ServerName, c.CertType, c.CertSubType, c.CertInfo)
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
@@ -19,9 +18,10 @@ import (
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
clusterpkg "github.com/argoproj/argo-cd/pkg/apiclient/cluster"
|
||||
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/server/cluster"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/clusterauth"
|
||||
)
|
||||
|
||||
// NewClusterCommand returns a new instance of an `argocd cluster` command
|
||||
@@ -33,25 +33,41 @@ func NewClusterCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clientc
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
},
|
||||
Example: ` # List all known clusters in JSON format:
|
||||
argocd cluster list -o json
|
||||
|
||||
# Add a target cluster configuration to ArgoCD. The context must exist in your kubectl config:
|
||||
argocd cluster add example-cluster
|
||||
|
||||
# Get specific details about a cluster in plain text (wide) format:
|
||||
argocd cluster get example-cluster -o wide
|
||||
|
||||
# Remove a target cluster context from ArgoCD
|
||||
argocd cluster rm example-cluster
|
||||
`,
|
||||
}
|
||||
|
||||
command.AddCommand(NewClusterAddCommand(clientOpts, pathOpts))
|
||||
command.AddCommand(NewClusterGetCommand(clientOpts))
|
||||
command.AddCommand(NewClusterListCommand(clientOpts))
|
||||
command.AddCommand(NewClusterRemoveCommand(clientOpts))
|
||||
command.AddCommand(NewClusterRotateAuthCommand(clientOpts))
|
||||
return command
|
||||
}
|
||||
|
||||
// NewClusterAddCommand returns a new instance of an `argocd cluster add` command
|
||||
func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clientcmd.PathOptions) *cobra.Command {
|
||||
var (
|
||||
inCluster bool
|
||||
upsert bool
|
||||
awsRoleArn string
|
||||
awsClusterName string
|
||||
inCluster bool
|
||||
upsert bool
|
||||
serviceAccount string
|
||||
awsRoleArn string
|
||||
awsClusterName string
|
||||
systemNamespace string
|
||||
namespaces []string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "add",
|
||||
Use: "add CONTEXT",
|
||||
Short: fmt.Sprintf("%s cluster add CONTEXT", cliName),
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
var configAccess clientcmd.ConfigAccess = pathOpts
|
||||
@@ -62,9 +78,10 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
}
|
||||
config, err := configAccess.GetStartingConfig()
|
||||
errors.CheckError(err)
|
||||
clstContext := config.Contexts[args[0]]
|
||||
contextName := args[0]
|
||||
clstContext := config.Contexts[contextName]
|
||||
if clstContext == nil {
|
||||
log.Fatalf("Context %s does not exist in kubeconfig", args[0])
|
||||
log.Fatalf("Context %s does not exist in kubeconfig", contextName)
|
||||
}
|
||||
|
||||
overrides := clientcmd.ConfigOverrides{
|
||||
@@ -85,29 +102,36 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
// Install RBAC resources for managing the cluster
|
||||
clientset, err := kubernetes.NewForConfig(conf)
|
||||
errors.CheckError(err)
|
||||
managerBearerToken, err = common.InstallClusterManagerRBAC(clientset)
|
||||
if serviceAccount != "" {
|
||||
managerBearerToken, err = clusterauth.GetServiceAccountBearerToken(clientset, systemNamespace, serviceAccount)
|
||||
} else {
|
||||
managerBearerToken, err = clusterauth.InstallClusterManagerRBAC(clientset, systemNamespace, namespaces)
|
||||
}
|
||||
errors.CheckError(err)
|
||||
}
|
||||
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
|
||||
defer util.Close(conn)
|
||||
clst := NewCluster(args[0], conf, managerBearerToken, awsAuthConf)
|
||||
clst := newCluster(contextName, namespaces, conf, managerBearerToken, awsAuthConf)
|
||||
if inCluster {
|
||||
clst.Server = common.KubernetesInternalAPIServerAddr
|
||||
}
|
||||
clstCreateReq := cluster.ClusterCreateRequest{
|
||||
clstCreateReq := clusterpkg.ClusterCreateRequest{
|
||||
Cluster: clst,
|
||||
Upsert: upsert,
|
||||
}
|
||||
clst, err = clusterIf.Create(context.Background(), &clstCreateReq)
|
||||
_, err = clusterIf.Create(context.Background(), &clstCreateReq)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Cluster '%s' added\n", clst.Name)
|
||||
fmt.Printf("Cluster '%s' added\n", clst.Server)
|
||||
},
|
||||
}
|
||||
command.PersistentFlags().StringVar(&pathOpts.LoadingRules.ExplicitPath, pathOpts.ExplicitFileFlag, pathOpts.LoadingRules.ExplicitPath, "use a particular kubeconfig file")
|
||||
command.Flags().BoolVar(&inCluster, "in-cluster", false, "Indicates Argo CD resides inside this cluster and should connect using the internal k8s hostname (kubernetes.default.svc)")
|
||||
command.Flags().BoolVar(&upsert, "upsert", false, "Override an existing cluster with the same name even if the spec differs")
|
||||
command.Flags().StringVar(&awsClusterName, "aws-cluster-name", "", "AWS Cluster name if set then aws-iam-authenticator will be used to access cluster")
|
||||
command.Flags().StringVar(&serviceAccount, "service-account", "", fmt.Sprintf("System namespace service account to use for kubernetes resource management. If not set then default \"%s\" SA will be created", clusterauth.ArgoCDManagerServiceAccount))
|
||||
command.Flags().StringVar(&awsClusterName, "aws-cluster-name", "", "AWS Cluster name if set then aws cli eks token command will be used to access cluster")
|
||||
command.Flags().StringVar(&awsRoleArn, "aws-role-arn", "", "Optional AWS role arn. If set then AWS IAM Authenticator assume a role to perform cluster operations instead of the default AWS credential provider chain.")
|
||||
command.Flags().StringVar(&systemNamespace, "system-namespace", common.DefaultSystemNamespace, "Use different system namespace")
|
||||
command.Flags().StringArrayVar(&namespaces, "namespace", nil, "List of namespaces which are allowed to manage")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -150,32 +174,21 @@ func printKubeContexts(ca clientcmd.ConfigAccess) {
|
||||
}
|
||||
}
|
||||
|
||||
func NewCluster(name string, conf *rest.Config, managerBearerToken string, awsAuthConf *argoappv1.AWSAuthConfig) *argoappv1.Cluster {
|
||||
func newCluster(name string, namespaces []string, conf *rest.Config, managerBearerToken string, awsAuthConf *argoappv1.AWSAuthConfig) *argoappv1.Cluster {
|
||||
tlsClientConfig := argoappv1.TLSClientConfig{
|
||||
Insecure: conf.TLSClientConfig.Insecure,
|
||||
ServerName: conf.TLSClientConfig.ServerName,
|
||||
CertData: conf.TLSClientConfig.CertData,
|
||||
KeyData: conf.TLSClientConfig.KeyData,
|
||||
CAData: conf.TLSClientConfig.CAData,
|
||||
}
|
||||
if len(conf.TLSClientConfig.CertData) == 0 && conf.TLSClientConfig.CertFile != "" {
|
||||
data, err := ioutil.ReadFile(conf.TLSClientConfig.CertFile)
|
||||
errors.CheckError(err)
|
||||
tlsClientConfig.CertData = data
|
||||
}
|
||||
if len(conf.TLSClientConfig.KeyData) == 0 && conf.TLSClientConfig.KeyFile != "" {
|
||||
data, err := ioutil.ReadFile(conf.TLSClientConfig.KeyFile)
|
||||
errors.CheckError(err)
|
||||
tlsClientConfig.KeyData = data
|
||||
}
|
||||
if len(conf.TLSClientConfig.CAData) == 0 && conf.TLSClientConfig.CAFile != "" {
|
||||
data, err := ioutil.ReadFile(conf.TLSClientConfig.CAFile)
|
||||
errors.CheckError(err)
|
||||
tlsClientConfig.CAData = data
|
||||
}
|
||||
clst := argoappv1.Cluster{
|
||||
Server: conf.Host,
|
||||
Name: name,
|
||||
Server: conf.Host,
|
||||
Name: name,
|
||||
Namespaces: namespaces,
|
||||
Config: argoappv1.ClusterConfig{
|
||||
BearerToken: managerBearerToken,
|
||||
TLSClientConfig: tlsClientConfig,
|
||||
@@ -187,9 +200,13 @@ func NewCluster(name string, conf *rest.Config, managerBearerToken string, awsAu
|
||||
|
||||
// NewClusterGetCommand returns a new instance of an `argocd cluster get` command
|
||||
func NewClusterGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Get cluster information",
|
||||
Use: "get SERVER",
|
||||
Short: "Get cluster information",
|
||||
Example: `argocd cluster get https://12.34.567.89`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
@@ -197,23 +214,68 @@ func NewClusterGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
}
|
||||
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
|
||||
defer util.Close(conn)
|
||||
clusters := make([]argoappv1.Cluster, 0)
|
||||
for _, clusterName := range args {
|
||||
clst, err := clusterIf.Get(context.Background(), &cluster.ClusterQuery{Server: clusterName})
|
||||
clst, err := clusterIf.Get(context.Background(), &clusterpkg.ClusterQuery{Server: clusterName})
|
||||
errors.CheckError(err)
|
||||
yamlBytes, err := yaml.Marshal(clst)
|
||||
clusters = append(clusters, *clst)
|
||||
}
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(clusters, output, true)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("%v", string(yamlBytes))
|
||||
case "wide", "":
|
||||
printClusterDetails(clusters)
|
||||
case "server":
|
||||
printClusterServers(clusters)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
// we have yaml as default to not break backwards-compatibility
|
||||
command.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: json|yaml|wide|server")
|
||||
return command
|
||||
}
|
||||
|
||||
func strWithDefault(value string, def string) string {
|
||||
if value == "" {
|
||||
return def
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func formatNamespaces(cluster argoappv1.Cluster) string {
|
||||
if len(cluster.Namespaces) == 0 {
|
||||
return "all namespaces"
|
||||
}
|
||||
return strings.Join(cluster.Namespaces, ", ")
|
||||
}
|
||||
|
||||
func printClusterDetails(clusters []argoappv1.Cluster) {
|
||||
for _, cluster := range clusters {
|
||||
fmt.Printf("Cluster information\n\n")
|
||||
fmt.Printf(" Server URL: %s\n", cluster.Server)
|
||||
fmt.Printf(" Server Name: %s\n", strWithDefault(cluster.Name, "-"))
|
||||
fmt.Printf(" Server Version: %s\n", cluster.ServerVersion)
|
||||
fmt.Printf(" Namespaces: %s\n", formatNamespaces(cluster))
|
||||
fmt.Printf("\nTLS configuration\n\n")
|
||||
fmt.Printf(" Client cert: %v\n", string(cluster.Config.TLSClientConfig.CertData) != "")
|
||||
fmt.Printf(" Cert validation: %v\n", !cluster.Config.TLSClientConfig.Insecure)
|
||||
fmt.Printf("\nAuthentication\n\n")
|
||||
fmt.Printf(" Basic authentication: %v\n", cluster.Config.Username != "")
|
||||
fmt.Printf(" oAuth authentication: %v\n", cluster.Config.BearerToken != "")
|
||||
fmt.Printf(" AWS authentication: %v\n", cluster.Config.AWSAuthConfig != nil)
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
// NewClusterRemoveCommand returns a new instance of an `argocd cluster list` command
|
||||
func NewClusterRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "rm",
|
||||
Short: "Remove cluster credentials",
|
||||
Use: "rm SERVER",
|
||||
Short: "Remove cluster credentials",
|
||||
Example: `argocd cluster rm https://12.34.567.89`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
@@ -227,9 +289,9 @@ func NewClusterRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
|
||||
for _, clusterName := range args {
|
||||
// TODO(jessesuen): find the right context and remove manager RBAC artifacts
|
||||
// err := common.UninstallClusterManagerRBAC(clientset)
|
||||
// err := clusterauth.UninstallClusterManagerRBAC(clientset)
|
||||
// errors.CheckError(err)
|
||||
_, err := clusterIf.Delete(context.Background(), &cluster.ClusterQuery{Server: clusterName})
|
||||
_, err := clusterIf.Delete(context.Background(), &clusterpkg.ClusterQuery{Server: clusterName})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
@@ -237,22 +299,76 @@ func NewClusterRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
return command
|
||||
}
|
||||
|
||||
// Print table of cluster information
|
||||
func printClusterTable(clusters []argoappv1.Cluster) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintf(w, "SERVER\tNAME\tVERSION\tSTATUS\tMESSAGE\n")
|
||||
for _, c := range clusters {
|
||||
server := c.Server
|
||||
if len(c.Namespaces) > 0 {
|
||||
server = fmt.Sprintf("%s (%d namespaces)", c.Server, len(c.Namespaces))
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", server, c.Name, c.ServerVersion, c.ConnectionState.Status, c.ConnectionState.Message)
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
// Print list of cluster servers
|
||||
func printClusterServers(clusters []argoappv1.Cluster) {
|
||||
for _, c := range clusters {
|
||||
fmt.Println(c.Server)
|
||||
}
|
||||
}
|
||||
|
||||
// NewClusterListCommand returns a new instance of an `argocd cluster rm` command
|
||||
func NewClusterListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List configured clusters",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
|
||||
defer util.Close(conn)
|
||||
clusters, err := clusterIf.List(context.Background(), &cluster.ClusterQuery{})
|
||||
clusters, err := clusterIf.List(context.Background(), &clusterpkg.ClusterQuery{})
|
||||
errors.CheckError(err)
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "SERVER\tNAME\tSTATUS\tMESSAGE\n")
|
||||
for _, c := range clusters.Items {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", c.Server, c.Name, c.ConnectionState.Status, c.ConnectionState.Message)
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(clusters.Items, output, false)
|
||||
errors.CheckError(err)
|
||||
case "server":
|
||||
printClusterServers(clusters.Items)
|
||||
case "wide", "":
|
||||
printClusterTable(clusters.Items)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
_ = w.Flush()
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|server")
|
||||
return command
|
||||
}
|
||||
|
||||
// NewClusterRotateAuthCommand returns a new instance of an `argocd cluster rotate-auth` command
|
||||
func NewClusterRotateAuthCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "rotate-auth SERVER",
|
||||
Short: fmt.Sprintf("%s cluster rotate-auth SERVER", cliName),
|
||||
Example: fmt.Sprintf("%s cluster rotate-auth https://12.34.567.89", cliName),
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
|
||||
defer util.Close(conn)
|
||||
clusterQuery := clusterpkg.ClusterQuery{
|
||||
Server: args[0],
|
||||
}
|
||||
_, err := clusterIf.RotateAuth(context.Background(), &clusterQuery)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Cluster '%s' rotated auth\n", clusterQuery.Server)
|
||||
},
|
||||
}
|
||||
return command
|
||||
|
||||
31
cmd/argocd/commands/cluster_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
func Test_printClusterTable(t *testing.T) {
|
||||
printClusterTable([]v1alpha1.Cluster{
|
||||
{
|
||||
Server: "my-server",
|
||||
Name: "my-name",
|
||||
Config: v1alpha1.ClusterConfig{
|
||||
Username: "my-username",
|
||||
Password: "my-password",
|
||||
BearerToken: "my-bearer-token",
|
||||
TLSClientConfig: v1alpha1.TLSClientConfig{},
|
||||
AWSAuthConfig: nil,
|
||||
},
|
||||
ConnectionState: v1alpha1.ConnectionState{
|
||||
Status: "my-status",
|
||||
Message: "my-message",
|
||||
ModifiedAt: &metav1.Time{},
|
||||
},
|
||||
ServerVersion: "my-version",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,13 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
)
|
||||
|
||||
const (
|
||||
cliName = "argocd"
|
||||
|
||||
@@ -7,3 +15,58 @@ const (
|
||||
// the OAuth2 login flow.
|
||||
DefaultSSOLocalPort = 8085
|
||||
)
|
||||
|
||||
// PrintResource prints a single resource in YAML or JSON format to stdout according to the output format
|
||||
func PrintResource(resource interface{}, output string) error {
|
||||
switch output {
|
||||
case "json":
|
||||
jsonBytes, err := json.MarshalIndent(resource, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(jsonBytes))
|
||||
case "yaml":
|
||||
yamlBytes, err := yaml.Marshal(resource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Print(string(yamlBytes))
|
||||
default:
|
||||
return fmt.Errorf("unknown output format: %s", output)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrintResourceList marshals & prints a list of resources to stdout according to the output format
|
||||
func PrintResourceList(resources interface{}, output string, single bool) error {
|
||||
kt := reflect.ValueOf(resources)
|
||||
// Sometimes, we want to marshal the first resource of a slice or array as single item
|
||||
if kt.Kind() == reflect.Slice || kt.Kind() == reflect.Array {
|
||||
if single && kt.Len() == 1 {
|
||||
return PrintResource(kt.Index(0).Interface(), output)
|
||||
}
|
||||
|
||||
// If we have a zero len list, prevent printing "null"
|
||||
if kt.Len() == 0 {
|
||||
return PrintResource([]string{}, output)
|
||||
}
|
||||
}
|
||||
|
||||
switch output {
|
||||
case "json":
|
||||
jsonBytes, err := json.MarshalIndent(resources, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(jsonBytes))
|
||||
case "yaml":
|
||||
yamlBytes, err := yaml.Marshal(resources)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Print(string(yamlBytes))
|
||||
default:
|
||||
return fmt.Errorf("unknown output format: %s", output)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
142
cmd/argocd/commands/common_test.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Be careful with tabs vs. spaces in the following expected formats. Indents
|
||||
// should all be spaces, no tabs.
|
||||
const expectYamlSingle = `bar: ""
|
||||
baz: foo
|
||||
foo: bar
|
||||
`
|
||||
|
||||
const expectJsonSingle = `{
|
||||
"bar": "",
|
||||
"baz": "foo",
|
||||
"foo": "bar"
|
||||
}
|
||||
`
|
||||
const expectYamlList = `one:
|
||||
bar: ""
|
||||
baz: foo
|
||||
foo: bar
|
||||
two:
|
||||
bar: ""
|
||||
baz: foo
|
||||
foo: bar
|
||||
`
|
||||
|
||||
const expectJsonList = `{
|
||||
"one": {
|
||||
"bar": "",
|
||||
"baz": "foo",
|
||||
"foo": "bar"
|
||||
},
|
||||
"two": {
|
||||
"bar": "",
|
||||
"baz": "foo",
|
||||
"foo": "bar"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Rather dirty hack to capture stdout from PrintResource() and PrintResourceList()
|
||||
func captureOutput(f func() error) (string, error) {
|
||||
stdout := os.Stdout
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
os.Stdout = w
|
||||
err = f()
|
||||
w.Close()
|
||||
if err != nil {
|
||||
os.Stdout = stdout
|
||||
return "", err
|
||||
}
|
||||
str, err := ioutil.ReadAll(r)
|
||||
os.Stdout = stdout
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(str), err
|
||||
}
|
||||
|
||||
func Test_PrintResource(t *testing.T) {
|
||||
testResource := map[string]string{
|
||||
"foo": "bar",
|
||||
"bar": "",
|
||||
"baz": "foo",
|
||||
}
|
||||
|
||||
str, err := captureOutput(func() error {
|
||||
err := PrintResource(testResource, "yaml")
|
||||
return err
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, str, expectYamlSingle)
|
||||
|
||||
str, err = captureOutput(func() error {
|
||||
err := PrintResource(testResource, "json")
|
||||
return err
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, str, expectJsonSingle)
|
||||
|
||||
err = PrintResource(testResource, "unknown")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func Test_PrintResourceList(t *testing.T) {
|
||||
testResource := map[string]map[string]string{
|
||||
"one": {
|
||||
"foo": "bar",
|
||||
"bar": "",
|
||||
"baz": "foo",
|
||||
},
|
||||
"two": {
|
||||
"foo": "bar",
|
||||
"bar": "",
|
||||
"baz": "foo",
|
||||
},
|
||||
}
|
||||
|
||||
testResource2 := make([]map[string]string, 0)
|
||||
testResource2 = append(testResource2, testResource["one"])
|
||||
|
||||
str, err := captureOutput(func() error {
|
||||
err := PrintResourceList(testResource, "yaml", false)
|
||||
return err
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, str, expectYamlList)
|
||||
|
||||
str, err = captureOutput(func() error {
|
||||
err := PrintResourceList(testResource, "json", false)
|
||||
return err
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, str, expectJsonList)
|
||||
|
||||
str, err = captureOutput(func() error {
|
||||
err := PrintResourceList(testResource2, "yaml", true)
|
||||
return err
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, str, expectYamlSingle)
|
||||
|
||||
str, err = captureOutput(func() error {
|
||||
err := PrintResourceList(testResource2, "json", true)
|
||||
return err
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, str, expectJsonSingle)
|
||||
|
||||
err = PrintResourceList(testResource, "unknown", false)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
233
cmd/argocd/commands/completion.go
Normal file
@@ -0,0 +1,233 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
bashCompletionFunc = `
|
||||
__argocd_list_apps() {
|
||||
local -a argocd_out
|
||||
if argocd_out=($(argocd app list --output name 2>/dev/null)); then
|
||||
COMPREPLY+=( $( compgen -W "${argocd_out[*]}" -- "$cur" ) )
|
||||
fi
|
||||
}
|
||||
|
||||
__argocd_list_app_history() {
|
||||
local app=$1
|
||||
local -a argocd_out
|
||||
if argocd_out=($(argocd app history $app --output id 2>/dev/null)); then
|
||||
COMPREPLY+=( $( compgen -W "${argocd_out[*]}" -- "$cur" ) )
|
||||
fi
|
||||
}
|
||||
|
||||
__argocd_app_rollback() {
|
||||
local -a command
|
||||
for comp_word in "${COMP_WORDS[@]}"; do
|
||||
if [[ $comp_word =~ ^-.*$ ]]; then
|
||||
continue
|
||||
fi
|
||||
command+=($comp_word)
|
||||
done
|
||||
|
||||
# fourth arg is app (if present): e.g.- argocd app rollback guestbook
|
||||
local app=${command[3]}
|
||||
local id=${command[4]}
|
||||
if [[ -z $app || $app == $cur ]]; then
|
||||
__argocd_list_apps
|
||||
elif [[ -z $id || $id == $cur ]]; then
|
||||
__argocd_list_app_history $app
|
||||
fi
|
||||
}
|
||||
|
||||
__argocd_list_servers() {
|
||||
local -a argocd_out
|
||||
if argocd_out=($(argocd cluster list --output server 2>/dev/null)); then
|
||||
COMPREPLY+=( $( compgen -W "${argocd_out[*]}" -- "$cur" ) )
|
||||
fi
|
||||
}
|
||||
|
||||
__argocd_list_repos() {
|
||||
local -a argocd_out
|
||||
if argocd_out=($(argocd repo list --output url 2>/dev/null)); then
|
||||
COMPREPLY+=( $( compgen -W "${argocd_out[*]}" -- "$cur" ) )
|
||||
fi
|
||||
}
|
||||
|
||||
__argocd_list_projects() {
|
||||
local -a argocd_out
|
||||
if argocd_out=($(argocd proj list --output name 2>/dev/null)); then
|
||||
COMPREPLY+=( $( compgen -W "${argocd_out[*]}" -- "$cur" ) )
|
||||
fi
|
||||
}
|
||||
|
||||
__argocd_list_namespaces() {
|
||||
local -a argocd_out
|
||||
if argocd_out=($(kubectl get namespaces --no-headers 2>/dev/null | cut -f1 -d' ' 2>/dev/null)); then
|
||||
COMPREPLY+=( $( compgen -W "${argocd_out[*]}" -- "$cur" ) )
|
||||
fi
|
||||
}
|
||||
|
||||
__argocd_proj_server_namespace() {
|
||||
local -a command
|
||||
for comp_word in "${COMP_WORDS[@]}"; do
|
||||
if [[ $comp_word =~ ^-.*$ ]]; then
|
||||
continue
|
||||
fi
|
||||
command+=($comp_word)
|
||||
done
|
||||
|
||||
# expect something like this: argocd proj add-destination PROJECT SERVER NAMESPACE
|
||||
local project=${command[3]}
|
||||
local server=${command[4]}
|
||||
local namespace=${command[5]}
|
||||
if [[ -z $project || $project == $cur ]]; then
|
||||
__argocd_list_projects
|
||||
elif [[ -z $server || $server == $cur ]]; then
|
||||
__argocd_list_servers
|
||||
elif [[ -z $namespace || $namespace == $cur ]]; then
|
||||
__argocd_list_namespaces
|
||||
fi
|
||||
}
|
||||
|
||||
__argocd_list_project_role() {
|
||||
local project="$1"
|
||||
local -a argocd_out
|
||||
if argocd_out=($(argocd proj role list "$project" --output=name 2>/dev/null)); then
|
||||
COMPREPLY+=( $( compgen -W "${argocd_out[*]}" -- "$cur" ) )
|
||||
fi
|
||||
}
|
||||
|
||||
__argocd_proj_role(){
|
||||
local -a command
|
||||
for comp_word in "${COMP_WORDS[@]}"; do
|
||||
if [[ $comp_word =~ ^-.*$ ]]; then
|
||||
continue
|
||||
fi
|
||||
command+=($comp_word)
|
||||
done
|
||||
|
||||
# expect something like this: argocd proj role add-policy PROJECT ROLE-NAME
|
||||
local project=${command[4]}
|
||||
local role=${command[5]}
|
||||
if [[ -z $project || $project == $cur ]]; then
|
||||
__argocd_list_projects
|
||||
elif [[ -z $role || $role == $cur ]]; then
|
||||
__argocd_list_project_role $project
|
||||
fi
|
||||
}
|
||||
|
||||
__argocd_custom_func() {
|
||||
case ${last_command} in
|
||||
argocd_app_delete | \
|
||||
argocd_app_diff | \
|
||||
argocd_app_edit | \
|
||||
argocd_app_get | \
|
||||
argocd_app_history | \
|
||||
argocd_app_manifests | \
|
||||
argocd_app_patch-resource | \
|
||||
argocd_app_set | \
|
||||
argocd_app_sync | \
|
||||
argocd_app_terminate-op | \
|
||||
argocd_app_unset | \
|
||||
argocd_app_wait | \
|
||||
argocd_app_create)
|
||||
__argocd_list_apps
|
||||
return
|
||||
;;
|
||||
argocd_app_rollback)
|
||||
__argocd_app_rollback
|
||||
return
|
||||
;;
|
||||
argocd_cluster_get | \
|
||||
argocd_cluster_rm | \
|
||||
argocd_login | \
|
||||
argocd_cluster_add)
|
||||
__argocd_list_servers
|
||||
return
|
||||
;;
|
||||
argocd_repo_rm | \
|
||||
argocd_repo_add)
|
||||
__argocd_list_repos
|
||||
return
|
||||
;;
|
||||
argocd_proj_add-destination | \
|
||||
argocd_proj_remove-destination)
|
||||
__argocd_proj_server_namespace
|
||||
return
|
||||
;;
|
||||
argocd_proj_add-source | \
|
||||
argocd_proj_remove-source | \
|
||||
argocd_proj_allow-cluster-resource | \
|
||||
argocd_proj_allow-namespace-resource | \
|
||||
argocd_proj_deny-cluster-resource | \
|
||||
argocd_proj_deny-namespace-resource | \
|
||||
argocd_proj_delete | \
|
||||
argocd_proj_edit | \
|
||||
argocd_proj_get | \
|
||||
argocd_proj_set | \
|
||||
argocd_proj_role_list)
|
||||
__argocd_list_projects
|
||||
return
|
||||
;;
|
||||
argocd_proj_role_remove-policy | \
|
||||
argocd_proj_role_add-policy | \
|
||||
argocd_proj_role_create | \
|
||||
argocd_proj_role_delete | \
|
||||
argocd_proj_role_get | \
|
||||
argocd_proj_role_create-token | \
|
||||
argocd_proj_role_delete-token)
|
||||
__argocd_proj_role
|
||||
return
|
||||
;;
|
||||
*)
|
||||
;;
|
||||
esac
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
func NewCompletionCommand() *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "completion SHELL",
|
||||
Short: "output shell completion code for the specified shell (bash or zsh)",
|
||||
Long: `Write bash or zsh shell completion code to standard output.
|
||||
|
||||
For bash, ensure you have bash completions installed and enabled.
|
||||
To access completions in your current shell, run
|
||||
$ source <(argocd completion bash)
|
||||
Alternatively, write it to a file and source in .bash_profile
|
||||
|
||||
For zsh, output to a file in a directory referenced by the $fpath shell
|
||||
variable.
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
cmd.HelpFunc()(cmd, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
shell := args[0]
|
||||
rootCommand := NewCommand()
|
||||
rootCommand.BashCompletionFunction = bashCompletionFunc
|
||||
availableCompletions := map[string]func(io.Writer) error{
|
||||
"bash": rootCommand.GenBashCompletion,
|
||||
"zsh": rootCommand.GenZshCompletion,
|
||||
}
|
||||
completion, ok := availableCompletions[shell]
|
||||
if !ok {
|
||||
fmt.Printf("Invalid shell '%s'. The supported shells are bash and zsh.\n", shell)
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := completion(os.Stdout); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
@@ -18,16 +18,33 @@ import (
|
||||
|
||||
// NewContextCommand returns a new instance of an `argocd ctx` command
|
||||
func NewContextCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var delete bool
|
||||
var command = &cobra.Command{
|
||||
Use: "context",
|
||||
Use: "context [CONTEXT]",
|
||||
Aliases: []string{"ctx"},
|
||||
Short: "Switch between contexts",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
|
||||
localCfg, err := localconfig.ReadLocalConfig(clientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
|
||||
if delete {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
err := deleteContext(args[0], clientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
printArgoCDContexts(clientOpts.ConfigPath)
|
||||
return
|
||||
}
|
||||
|
||||
ctxName := args[0]
|
||||
|
||||
argoCDDir, err := localconfig.DefaultConfigDir()
|
||||
errors.CheckError(err)
|
||||
prevCtxFile := path.Join(argoCDDir, ".prev-ctx")
|
||||
@@ -37,8 +54,6 @@ func NewContextCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
errors.CheckError(err)
|
||||
ctxName = string(prevCtxBytes)
|
||||
}
|
||||
localCfg, err := localconfig.ReadLocalConfig(clientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
if localCfg.CurrentContext == ctxName {
|
||||
fmt.Printf("Already at context '%s'\n", localCfg.CurrentContext)
|
||||
return
|
||||
@@ -48,6 +63,7 @@ func NewContextCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
}
|
||||
prevCtx := localCfg.CurrentContext
|
||||
localCfg.CurrentContext = ctxName
|
||||
|
||||
err = localconfig.WriteLocalConfig(*localCfg, clientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
err = ioutil.WriteFile(prevCtxFile, []byte(prevCtx), 0644)
|
||||
@@ -55,9 +71,43 @@ func NewContextCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
fmt.Printf("Switched to context '%s'\n", localCfg.CurrentContext)
|
||||
},
|
||||
}
|
||||
command.Flags().BoolVar(&delete, "delete", false, "Delete the context instead of switching to it")
|
||||
return command
|
||||
}
|
||||
|
||||
func deleteContext(context, configPath string) error {
|
||||
|
||||
localCfg, err := localconfig.ReadLocalConfig(configPath)
|
||||
errors.CheckError(err)
|
||||
if localCfg == nil {
|
||||
return fmt.Errorf("Nothing to logout from")
|
||||
}
|
||||
|
||||
serverName, ok := localCfg.RemoveContext(context)
|
||||
if !ok {
|
||||
return fmt.Errorf("Context %s does not exist", context)
|
||||
}
|
||||
_ = localCfg.RemoveUser(context)
|
||||
_ = localCfg.RemoveServer(serverName)
|
||||
|
||||
if localCfg.IsEmpty() {
|
||||
err = localconfig.DeleteLocalConfig(configPath)
|
||||
errors.CheckError(err)
|
||||
} else {
|
||||
if localCfg.CurrentContext == context {
|
||||
localCfg.CurrentContext = ""
|
||||
}
|
||||
err = localconfig.ValidateLocalConfig(*localCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error in logging out")
|
||||
}
|
||||
err = localconfig.WriteLocalConfig(*localCfg, configPath)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
fmt.Printf("Context '%s' deleted\n", context)
|
||||
return nil
|
||||
}
|
||||
|
||||
func printArgoCDContexts(configPath string) {
|
||||
localCfg, err := localconfig.ReadLocalConfig(configPath)
|
||||
errors.CheckError(err)
|
||||
|
||||
81
cmd/argocd/commands/context_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
)
|
||||
|
||||
const testConfig = `contexts:
|
||||
- name: argocd1.example.com:443
|
||||
server: argocd1.example.com:443
|
||||
user: argocd1.example.com:443
|
||||
- name: argocd2.example.com:443
|
||||
server: argocd2.example.com:443
|
||||
user: argocd2.example.com:443
|
||||
- name: localhost:8080
|
||||
server: localhost:8080
|
||||
user: localhost:8080
|
||||
current-context: localhost:8080
|
||||
servers:
|
||||
- server: argocd1.example.com:443
|
||||
- server: argocd2.example.com:443
|
||||
- plain-text: true
|
||||
server: localhost:8080
|
||||
users:
|
||||
- auth-token: vErrYS3c3tReFRe$hToken
|
||||
name: argocd1.example.com:443
|
||||
refresh-token: vErrYS3c3tReFRe$hToken
|
||||
- auth-token: vErrYS3c3tReFRe$hToken
|
||||
name: argocd2.example.com:443
|
||||
refresh-token: vErrYS3c3tReFRe$hToken
|
||||
- auth-token: vErrYS3c3tReFRe$hToken
|
||||
name: localhost:8080`
|
||||
|
||||
const testConfigFilePath = "./testdata/config"
|
||||
|
||||
func TestContextDelete(t *testing.T) {
|
||||
|
||||
// Write the test config file
|
||||
err := ioutil.WriteFile(testConfigFilePath, []byte(testConfig), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
|
||||
localConfig, err := localconfig.ReadLocalConfig(testConfigFilePath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, localConfig.CurrentContext, "localhost:8080")
|
||||
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "localhost:8080", Server: "localhost:8080", User: "localhost:8080"})
|
||||
|
||||
// Delete a non-current context
|
||||
err = deleteContext("argocd1.example.com:443", testConfigFilePath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
localConfig, err = localconfig.ReadLocalConfig(testConfigFilePath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, localConfig.CurrentContext, "localhost:8080")
|
||||
assert.NotContains(t, localConfig.Contexts, localconfig.ContextRef{Name: "argocd1.example.com:443", Server: "argocd1.example.com:443", User: "argocd1.example.com:443"})
|
||||
assert.NotContains(t, localConfig.Servers, localconfig.Server{Server: "argocd1.example.com:443"})
|
||||
assert.NotContains(t, localConfig.Users, localconfig.User{AuthToken: "vErrYS3c3tReFRe$hToken", Name: "argocd1.example.com:443"})
|
||||
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "argocd2.example.com:443", Server: "argocd2.example.com:443", User: "argocd2.example.com:443"})
|
||||
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "localhost:8080", Server: "localhost:8080", User: "localhost:8080"})
|
||||
|
||||
// Delete the current context
|
||||
err = deleteContext("localhost:8080", testConfigFilePath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
localConfig, err = localconfig.ReadLocalConfig(testConfigFilePath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, localConfig.CurrentContext, "")
|
||||
assert.NotContains(t, localConfig.Contexts, localconfig.ContextRef{Name: "localhost:8080", Server: "localhost:8080", User: "localhost:8080"})
|
||||
assert.NotContains(t, localConfig.Servers, localconfig.Server{PlainText: true, Server: "localhost:8080"})
|
||||
assert.NotContains(t, localConfig.Users, localconfig.User{AuthToken: "vErrYS3c3tReFRe$hToken", Name: "localhost:8080"})
|
||||
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "argocd2.example.com:443", Server: "argocd2.example.com:443", User: "argocd2.example.com:443"})
|
||||
|
||||
// Write the file again so that no conflicts are made in git
|
||||
err = ioutil.WriteFile(testConfigFilePath, []byte(testConfig), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
|
||||
}
|
||||
@@ -6,10 +6,11 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -17,8 +18,8 @@ import (
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/server/session"
|
||||
"github.com/argoproj/argo-cd/server/settings"
|
||||
sessionpkg "github.com/argoproj/argo-cd/pkg/apiclient/session"
|
||||
settingspkg "github.com/argoproj/argo-cd/pkg/apiclient/settings"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
grpc_util "github.com/argoproj/argo-cd/util/grpc"
|
||||
@@ -64,11 +65,12 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
}
|
||||
}
|
||||
clientOpts := argocdclient.ClientOptions{
|
||||
ConfigPath: "",
|
||||
ServerAddr: server,
|
||||
Insecure: globalClientOpts.Insecure,
|
||||
PlainText: globalClientOpts.PlainText,
|
||||
GRPCWeb: globalClientOpts.GRPCWeb,
|
||||
ConfigPath: "",
|
||||
ServerAddr: server,
|
||||
Insecure: globalClientOpts.Insecure,
|
||||
PlainText: globalClientOpts.PlainText,
|
||||
GRPCWeb: globalClientOpts.GRPCWeb,
|
||||
GRPCWebRootPath: globalClientOpts.GRPCWebRootPath,
|
||||
}
|
||||
acdClient := argocdclient.NewClientOrDie(&clientOpts)
|
||||
setConn, setIf := acdClient.NewSettingsClientOrDie()
|
||||
@@ -76,6 +78,10 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
|
||||
if ctxName == "" {
|
||||
ctxName = server
|
||||
if globalClientOpts.GRPCWebRootPath != "" {
|
||||
rootPath := strings.TrimRight(strings.TrimLeft(globalClientOpts.GRPCWebRootPath, "/"), "/")
|
||||
ctxName = fmt.Sprintf("%s/%s", server, rootPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Perform the login
|
||||
@@ -88,11 +94,11 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
httpClient, err := acdClient.HTTPClient()
|
||||
errors.CheckError(err)
|
||||
ctx = oidc.ClientContext(ctx, httpClient)
|
||||
acdSet, err := setIf.Get(ctx, &settings.SettingsQuery{})
|
||||
acdSet, err := setIf.Get(ctx, &settingspkg.SettingsQuery{})
|
||||
errors.CheckError(err)
|
||||
oauth2conf, provider, err := acdClient.OIDCConfig(ctx, acdSet)
|
||||
errors.CheckError(err)
|
||||
tokenString, refreshToken = oauth2Login(ctx, ssoPort, oauth2conf, provider)
|
||||
tokenString, refreshToken = oauth2Login(ctx, ssoPort, acdSet.GetOIDCConfig(), oauth2conf, provider)
|
||||
}
|
||||
|
||||
parser := &jwt.Parser{
|
||||
@@ -110,10 +116,11 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
localCfg = &localconfig.LocalConfig{}
|
||||
}
|
||||
localCfg.UpsertServer(localconfig.Server{
|
||||
Server: server,
|
||||
PlainText: globalClientOpts.PlainText,
|
||||
Insecure: globalClientOpts.Insecure,
|
||||
GRPCWeb: globalClientOpts.GRPCWeb,
|
||||
Server: server,
|
||||
PlainText: globalClientOpts.PlainText,
|
||||
Insecure: globalClientOpts.Insecure,
|
||||
GRPCWeb: globalClientOpts.GRPCWeb,
|
||||
GRPCWebRootPath: globalClientOpts.GRPCWebRootPath,
|
||||
})
|
||||
localCfg.UpsertUser(localconfig.User{
|
||||
Name: ctxName,
|
||||
@@ -154,7 +161,7 @@ func userDisplayName(claims jwt.MapClaims) string {
|
||||
|
||||
// oauth2Login opens a browser, runs a temporary HTTP server to delegate OAuth2 login flow and
|
||||
// returns the JWT token and a refresh token (if supported)
|
||||
func oauth2Login(ctx context.Context, port int, oauth2conf *oauth2.Config, provider *oidc.Provider) (string, string) {
|
||||
func oauth2Login(ctx context.Context, port int, oidcSettings *settingspkg.OIDCConfig, oauth2conf *oauth2.Config, provider *oidc.Provider) (string, string) {
|
||||
oauth2conf.RedirectURL = fmt.Sprintf("http://localhost:%d/auth/callback", port)
|
||||
oidcConf, err := oidcutil.ParseConfig(provider)
|
||||
errors.CheckError(err)
|
||||
@@ -243,22 +250,28 @@ func oauth2Login(ctx context.Context, port int, oauth2conf *oauth2.Config, provi
|
||||
fmt.Printf("Opening browser for authentication\n")
|
||||
|
||||
var url string
|
||||
grantType := oidcutil.InferGrantType(oauth2conf, oidcConf)
|
||||
grantType := oidcutil.InferGrantType(oidcConf)
|
||||
opts := []oauth2.AuthCodeOption{oauth2.AccessTypeOffline}
|
||||
if claimsRequested := oidcSettings.GetIDTokenClaims(); claimsRequested != nil {
|
||||
opts = oidcutil.AppendClaimsAuthenticationRequestParameter(opts, claimsRequested)
|
||||
}
|
||||
|
||||
switch grantType {
|
||||
case oidcutil.GrantTypeAuthorizationCode:
|
||||
url = oauth2conf.AuthCodeURL(stateNonce, oauth2.AccessTypeOffline)
|
||||
url = oauth2conf.AuthCodeURL(stateNonce, opts...)
|
||||
case oidcutil.GrantTypeImplicit:
|
||||
url = oidcutil.ImplicitFlowURL(oauth2conf, stateNonce, oauth2.AccessTypeOffline)
|
||||
url = oidcutil.ImplicitFlowURL(oauth2conf, stateNonce, opts...)
|
||||
default:
|
||||
log.Fatalf("Unsupported grant type: %v", grantType)
|
||||
}
|
||||
fmt.Printf("Performing %s flow login: %s\n", grantType, url)
|
||||
time.Sleep(1 * time.Second)
|
||||
err = open.Run(url)
|
||||
err = open.Start(url)
|
||||
errors.CheckError(err)
|
||||
go func() {
|
||||
log.Debugf("Listen: %s", srv.Addr)
|
||||
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
|
||||
log.Fatalf("listen: %s\n", err)
|
||||
log.Fatalf("Temporary HTTP server failed: %s", err)
|
||||
}
|
||||
}()
|
||||
errMsg := <-completionChan
|
||||
@@ -278,7 +291,7 @@ func passwordLogin(acdClient argocdclient.Client, username, password string) str
|
||||
username, password = cli.PromptCredentials(username, password)
|
||||
sessConn, sessionIf := acdClient.NewSessionClientOrDie()
|
||||
defer util.Close(sessConn)
|
||||
sessionRequest := session.SessionCreateRequest{
|
||||
sessionRequest := sessionpkg.SessionCreateRequest{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
|
||||
50
cmd/argocd/commands/logout.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
)
|
||||
|
||||
// NewLogoutCommand returns a new instance of `argocd logout` command
|
||||
func NewLogoutCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "logout CONTEXT",
|
||||
Short: "Log out from Argo CD",
|
||||
Long: "Log out from Argo CD",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
context := args[0]
|
||||
|
||||
localCfg, err := localconfig.ReadLocalConfig(globalClientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
if localCfg == nil {
|
||||
log.Fatalf("Nothing to logout from")
|
||||
}
|
||||
|
||||
ok := localCfg.RemoveToken(context)
|
||||
if !ok {
|
||||
log.Fatalf("Context %s does not exist", context)
|
||||
}
|
||||
|
||||
err = localconfig.ValidateLocalConfig(*localCfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Error in logging out: %s", err)
|
||||
}
|
||||
err = localconfig.WriteLocalConfig(*localCfg, globalClientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
|
||||
fmt.Printf("Logged out from '%s'\n", context)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
41
cmd/argocd/commands/logout_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
)
|
||||
|
||||
func TestLogout(t *testing.T) {
|
||||
|
||||
// Write the test config file
|
||||
err := ioutil.WriteFile(testConfigFilePath, []byte(testConfig), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
|
||||
localConfig, err := localconfig.ReadLocalConfig(testConfigFilePath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, localConfig.CurrentContext, "localhost:8080")
|
||||
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "localhost:8080", Server: "localhost:8080", User: "localhost:8080"})
|
||||
|
||||
command := NewLogoutCommand(&apiclient.ClientOptions{ConfigPath: testConfigFilePath})
|
||||
command.Run(nil, []string{"localhost:8080"})
|
||||
|
||||
localConfig, err = localconfig.ReadLocalConfig(testConfigFilePath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, localConfig.CurrentContext, "localhost:8080")
|
||||
assert.NotContains(t, localConfig.Users, localconfig.User{AuthToken: "vErrYS3c3tReFRe$hToken", Name: "localhost:8080"})
|
||||
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "argocd1.example.com:443", Server: "argocd1.example.com:443", User: "argocd1.example.com:443"})
|
||||
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "argocd2.example.com:443", Server: "argocd2.example.com:443", User: "argocd2.example.com:443"})
|
||||
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "localhost:8080", Server: "localhost:8080", User: "localhost:8080"})
|
||||
|
||||
// Write the file again so that no conflicts are made in git
|
||||
err = ioutil.WriteFile(testConfigFilePath, []byte(testConfig), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
@@ -16,20 +18,24 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/utils/pointer"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
projectpkg "github.com/argoproj/argo-cd/pkg/apiclient/project"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/server/project"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/config"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
)
|
||||
|
||||
type projectOpts struct {
|
||||
description string
|
||||
destinations []string
|
||||
sources []string
|
||||
description string
|
||||
destinations []string
|
||||
sources []string
|
||||
orphanedResourcesEnabled bool
|
||||
orphanedResourcesWarn bool
|
||||
}
|
||||
|
||||
type policyOpts struct {
|
||||
@@ -79,6 +85,7 @@ func NewProjectCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
command.AddCommand(NewProjectDenyClusterResourceCommand(clientOpts))
|
||||
command.AddCommand(NewProjectAllowNamespaceResourceCommand(clientOpts))
|
||||
command.AddCommand(NewProjectDenyNamespaceResourceCommand(clientOpts))
|
||||
command.AddCommand(NewProjectWindowsCommand(clientOpts))
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -86,7 +93,21 @@ func addProjFlags(command *cobra.Command, opts *projectOpts) {
|
||||
command.Flags().StringVarP(&opts.description, "description", "", "", "Project description")
|
||||
command.Flags().StringArrayVarP(&opts.destinations, "dest", "d", []string{},
|
||||
"Permitted destination server and namespace (e.g. https://192.168.99.100:8443,default)")
|
||||
command.Flags().StringArrayVarP(&opts.sources, "src", "s", []string{}, "Permitted git source repository URL")
|
||||
command.Flags().StringArrayVarP(&opts.sources, "src", "s", []string{}, "Permitted source repository URL")
|
||||
command.Flags().BoolVar(&opts.orphanedResourcesEnabled, "orphaned-resources", false, "Enables orphaned resources monitoring")
|
||||
command.Flags().BoolVar(&opts.orphanedResourcesWarn, "orphaned-resources-warn", false, "Specifies if applications should be a warning condition when orphaned resources detected")
|
||||
}
|
||||
|
||||
func getOrphanedResourcesSettings(c *cobra.Command, opts projectOpts) *v1alpha1.OrphanedResourcesMonitorSettings {
|
||||
warnChanged := c.Flag("orphaned-resources-warn").Changed
|
||||
if opts.orphanedResourcesEnabled || warnChanged {
|
||||
settings := v1alpha1.OrphanedResourcesMonitorSettings{}
|
||||
if warnChanged {
|
||||
settings.Warn = pointer.BoolPtr(opts.orphanedResourcesWarn)
|
||||
}
|
||||
return &settings
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addPolicyFlags(command *cobra.Command, opts *policyOpts) {
|
||||
@@ -103,32 +124,63 @@ func humanizeTimestamp(epoch int64) string {
|
||||
// NewProjectCreateCommand returns a new instance of an `argocd proj create` command
|
||||
func NewProjectCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
opts projectOpts
|
||||
opts projectOpts
|
||||
fileURL string
|
||||
upsert bool
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "create PROJECT",
|
||||
Short: "Create a project",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
proj := v1alpha1.AppProject{
|
||||
ObjectMeta: v1.ObjectMeta{Name: projName},
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
Description: opts.description,
|
||||
Destinations: opts.GetDestinations(),
|
||||
SourceRepos: opts.sources,
|
||||
},
|
||||
var proj v1alpha1.AppProject
|
||||
if fileURL == "-" {
|
||||
// read stdin
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
err := config.UnmarshalReader(reader, &proj)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to read manifest from stdin: %v", err)
|
||||
}
|
||||
} else if fileURL != "" {
|
||||
// read uri
|
||||
parsedURL, err := url.ParseRequestURI(fileURL)
|
||||
if err != nil || !(parsedURL.Scheme == "http" || parsedURL.Scheme == "https") {
|
||||
err = config.UnmarshalLocalFile(fileURL, &proj)
|
||||
} else {
|
||||
err = config.UnmarshalRemoteFile(fileURL, &proj)
|
||||
}
|
||||
errors.CheckError(err)
|
||||
if len(args) == 1 && args[0] != proj.Name {
|
||||
log.Fatalf("project name '%s' does not match project spec metadata.name '%s'", args[0], proj.Name)
|
||||
}
|
||||
} else {
|
||||
// read arguments
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
proj = v1alpha1.AppProject{
|
||||
ObjectMeta: v1.ObjectMeta{Name: projName},
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
Description: opts.description,
|
||||
Destinations: opts.GetDestinations(),
|
||||
SourceRepos: opts.sources,
|
||||
OrphanedResources: getOrphanedResourcesSettings(c, opts),
|
||||
},
|
||||
}
|
||||
}
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
_, err := projIf.Create(context.Background(), &project.ProjectCreateRequest{Project: &proj})
|
||||
_, err := projIf.Create(context.Background(), &projectpkg.ProjectCreateRequest{Project: &proj, Upsert: upsert})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
command.Flags().BoolVar(&upsert, "upsert", false, "Allows to override a project with the same name even if supplied project spec is different from existing spec")
|
||||
command.Flags().StringVarP(&fileURL, "file", "f", "", "Filename or URL to Kubernetes manifests for the project")
|
||||
err := command.Flags().SetAnnotation("file", cobra.BashCompFilenameExt, []string{"json", "yaml", "yml"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
addProjFlags(command, &opts)
|
||||
return command
|
||||
}
|
||||
@@ -150,7 +202,7 @@ func NewProjectSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
visited := 0
|
||||
@@ -163,6 +215,8 @@ func NewProjectSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
proj.Spec.Destinations = opts.GetDestinations()
|
||||
case "src":
|
||||
proj.Spec.SourceRepos = opts.sources
|
||||
case "orphaned-resources", "orphaned-resources-warn":
|
||||
proj.Spec.OrphanedResources = getOrphanedResourcesSettings(c, opts)
|
||||
}
|
||||
})
|
||||
if visited == 0 {
|
||||
@@ -171,7 +225,7 @@ func NewProjectSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
@@ -195,7 +249,7 @@ func NewProjectAddDestinationCommand(clientOpts *argocdclient.ClientOptions) *co
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
for _, dest := range proj.Spec.Destinations {
|
||||
@@ -204,7 +258,7 @@ func NewProjectAddDestinationCommand(clientOpts *argocdclient.ClientOptions) *co
|
||||
}
|
||||
}
|
||||
proj.Spec.Destinations = append(proj.Spec.Destinations, v1alpha1.ApplicationDestination{Server: server, Namespace: namespace})
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
@@ -227,7 +281,7 @@ func NewProjectRemoveDestinationCommand(clientOpts *argocdclient.ClientOptions)
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
index := -1
|
||||
@@ -241,7 +295,7 @@ func NewProjectRemoveDestinationCommand(clientOpts *argocdclient.ClientOptions)
|
||||
log.Fatal("Specified destination does not exist in project")
|
||||
} else {
|
||||
proj.Spec.Destinations = append(proj.Spec.Destinations[:index], proj.Spec.Destinations[index+1:]...)
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
@@ -265,7 +319,7 @@ func NewProjectAddSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
for _, item := range proj.Spec.SourceRepos {
|
||||
@@ -279,15 +333,43 @@ func NewProjectAddSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
|
||||
}
|
||||
}
|
||||
proj.Spec.SourceRepos = append(proj.Spec.SourceRepos, url)
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
func modifyProjectResourceCmd(cmdUse, cmdDesc string, clientOpts *argocdclient.ClientOptions, action func(proj *v1alpha1.AppProject, group string, kind string) bool) *cobra.Command {
|
||||
func modifyClusterResourceCmd(cmdUse, cmdDesc string, clientOpts *argocdclient.ClientOptions, action func(proj *v1alpha1.AppProject, group string, kind string) bool) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: cmdUse,
|
||||
Short: cmdDesc,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
|
||||
if len(args) != 3 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName, group, kind := args[0], args[1], args[2]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
if action(proj, group, kind) {
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func modifyNamespaceResourceCmd(cmdUse, cmdDesc string, clientOpts *argocdclient.ClientOptions, action func(proj *v1alpha1.AppProject, group string, kind string, useWhitelist bool) bool) *cobra.Command {
|
||||
var (
|
||||
list string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: cmdUse,
|
||||
Short: cmdDesc,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
@@ -299,22 +381,39 @@ func modifyProjectResourceCmd(cmdUse, cmdDesc string, clientOpts *argocdclient.C
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
if action(proj, group, kind) {
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
var useWhitelist = false
|
||||
if list == "white" {
|
||||
useWhitelist = true
|
||||
}
|
||||
if action(proj, group, kind, useWhitelist) {
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&list, "list", "l", "black", "Use blacklist or whitelist. This can only be 'white' or 'black'")
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectAllowNamespaceResourceCommand returns a new instance of an `deny-cluster-resources` command
|
||||
func NewProjectAllowNamespaceResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
use := "allow-namespace-resource PROJECT GROUP KIND"
|
||||
desc := "Removes a namespaced API resource from the blacklist"
|
||||
return modifyProjectResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
|
||||
desc := "Removes a namespaced API resource from the blacklist or add a namespaced API resource to the whitelist"
|
||||
|
||||
return modifyNamespaceResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string, useWhitelist bool) bool {
|
||||
if useWhitelist {
|
||||
for _, item := range proj.Spec.NamespaceResourceWhitelist {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
fmt.Printf("Group '%s' and kind '%s' already present in whitelisted namespaced resources\n", group, kind)
|
||||
return false
|
||||
}
|
||||
}
|
||||
proj.Spec.NamespaceResourceWhitelist = append(proj.Spec.NamespaceResourceWhitelist, v1.GroupKind{Group: group, Kind: kind})
|
||||
fmt.Printf("Group '%s' and kind '%s' is added to whitelisted namespaced resources\n", group, kind)
|
||||
return true
|
||||
}
|
||||
index := -1
|
||||
for i, item := range proj.Spec.NamespaceResourceBlacklist {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
@@ -327,6 +426,7 @@ func NewProjectAllowNamespaceResourceCommand(clientOpts *argocdclient.ClientOpti
|
||||
return false
|
||||
}
|
||||
proj.Spec.NamespaceResourceBlacklist = append(proj.Spec.NamespaceResourceBlacklist[:index], proj.Spec.NamespaceResourceBlacklist[index+1:]...)
|
||||
fmt.Printf("Group '%s' and kind '%s' is removed from blacklisted namespaced resources\n", group, kind)
|
||||
return true
|
||||
})
|
||||
}
|
||||
@@ -334,8 +434,25 @@ func NewProjectAllowNamespaceResourceCommand(clientOpts *argocdclient.ClientOpti
|
||||
// NewProjectDenyNamespaceResourceCommand returns a new instance of an `argocd proj deny-namespace-resource` command
|
||||
func NewProjectDenyNamespaceResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
use := "deny-namespace-resource PROJECT GROUP KIND"
|
||||
desc := "Adds a namespaced API resource to the blacklist"
|
||||
return modifyProjectResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
|
||||
desc := "Adds a namespaced API resource to the blacklist or removes a namespaced API resource from the whitelist"
|
||||
return modifyNamespaceResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string, useWhitelist bool) bool {
|
||||
if useWhitelist {
|
||||
index := -1
|
||||
for i, item := range proj.Spec.NamespaceResourceWhitelist {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
fmt.Printf("Group '%s' and kind '%s' not in whitelisted namespaced resources\n", group, kind)
|
||||
return false
|
||||
}
|
||||
proj.Spec.NamespaceResourceWhitelist = append(proj.Spec.NamespaceResourceWhitelist[:index], proj.Spec.NamespaceResourceWhitelist[index+1:]...)
|
||||
fmt.Printf("Group '%s' and kind '%s' is removed from whitelisted namespaced resources\n", group, kind)
|
||||
return true
|
||||
}
|
||||
|
||||
for _, item := range proj.Spec.NamespaceResourceBlacklist {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
fmt.Printf("Group '%s' and kind '%s' already present in blacklisted namespaced resources\n", group, kind)
|
||||
@@ -343,6 +460,7 @@ func NewProjectDenyNamespaceResourceCommand(clientOpts *argocdclient.ClientOptio
|
||||
}
|
||||
}
|
||||
proj.Spec.NamespaceResourceBlacklist = append(proj.Spec.NamespaceResourceBlacklist, v1.GroupKind{Group: group, Kind: kind})
|
||||
fmt.Printf("Group '%s' and kind '%s' is added to blacklisted namespaced resources\n", group, kind)
|
||||
return true
|
||||
})
|
||||
}
|
||||
@@ -351,7 +469,7 @@ func NewProjectDenyNamespaceResourceCommand(clientOpts *argocdclient.ClientOptio
|
||||
func NewProjectDenyClusterResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
use := "deny-cluster-resource PROJECT GROUP KIND"
|
||||
desc := "Removes a cluster-scoped API resource from the whitelist"
|
||||
return modifyProjectResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
|
||||
return modifyClusterResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
|
||||
index := -1
|
||||
for i, item := range proj.Spec.ClusterResourceWhitelist {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
@@ -372,7 +490,7 @@ func NewProjectDenyClusterResourceCommand(clientOpts *argocdclient.ClientOptions
|
||||
func NewProjectAllowClusterResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
use := "allow-cluster-resource PROJECT GROUP KIND"
|
||||
desc := "Adds a cluster-scoped API resource to the whitelist"
|
||||
return modifyProjectResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
|
||||
return modifyClusterResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
|
||||
for _, item := range proj.Spec.ClusterResourceWhitelist {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
fmt.Printf("Group '%s' and kind '%s' already present in whitelisted cluster resources\n", group, kind)
|
||||
@@ -399,7 +517,7 @@ func NewProjectRemoveSourceCommand(clientOpts *argocdclient.ClientOptions) *cobr
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
index := -1
|
||||
@@ -413,7 +531,7 @@ func NewProjectRemoveSourceCommand(clientOpts *argocdclient.ClientOptions) *cobr
|
||||
fmt.Printf("Source repository '%s' does not exist in project\n", url)
|
||||
} else {
|
||||
proj.Spec.SourceRepos = append(proj.Spec.SourceRepos[:index], proj.Spec.SourceRepos[index+1:]...)
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
@@ -435,7 +553,7 @@ func NewProjectDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
for _, name := range args {
|
||||
_, err := projIf.Delete(context.Background(), &project.ProjectQuery{Name: name})
|
||||
_, err := projIf.Delete(context.Background(), &projectpkg.ProjectQuery{Name: name})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
@@ -443,27 +561,60 @@ func NewProjectDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
return command
|
||||
}
|
||||
|
||||
// Print list of project names
|
||||
func printProjectNames(projects []v1alpha1.AppProject) {
|
||||
for _, p := range projects {
|
||||
fmt.Println(p.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Print table of project info
|
||||
func printProjectTable(projects []v1alpha1.AppProject) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "NAME\tDESCRIPTION\tDESTINATIONS\tSOURCES\tCLUSTER-RESOURCE-WHITELIST\tNAMESPACE-RESOURCE-BLACKLIST\tORPHANED-RESOURCES\n")
|
||||
for _, p := range projects {
|
||||
printProjectLine(w, &p)
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
// NewProjectListCommand returns a new instance of an `argocd proj list` command
|
||||
func NewProjectListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List projects",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
projects, err := projIf.List(context.Background(), &project.ProjectQuery{})
|
||||
projects, err := projIf.List(context.Background(), &projectpkg.ProjectQuery{})
|
||||
errors.CheckError(err)
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "NAME\tDESCRIPTION\tDESTINATIONS\tSOURCES\tCLUSTER-RESOURCE-WHITELIST\tNAMESPACE-RESOURCE-BLACKLIST\n")
|
||||
for _, p := range projects.Items {
|
||||
printProjectLine(w, &p)
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(projects.Items, output, false)
|
||||
errors.CheckError(err)
|
||||
case "name":
|
||||
printProjectNames(projects.Items)
|
||||
case "wide", "":
|
||||
printProjectTable(projects.Items)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
_ = w.Flush()
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|name")
|
||||
return command
|
||||
}
|
||||
|
||||
func formatOrphanedResources(p *v1alpha1.AppProject) string {
|
||||
if p.Spec.OrphanedResources == nil {
|
||||
return "disabled"
|
||||
}
|
||||
return fmt.Sprintf("enabled (warn=%v)", p.Spec.OrphanedResources.IsWarn())
|
||||
}
|
||||
|
||||
func printProjectLine(w io.Writer, p *v1alpha1.AppProject) {
|
||||
var destinations, sourceRepos, clusterWhitelist, namespaceBlacklist string
|
||||
switch len(p.Spec.Destinations) {
|
||||
@@ -496,12 +647,63 @@ func printProjectLine(w io.Writer, p *v1alpha1.AppProject) {
|
||||
default:
|
||||
namespaceBlacklist = fmt.Sprintf("%d resources", len(p.Spec.NamespaceResourceBlacklist))
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%v\t%v\t%v\t%v\n", p.Name, p.Spec.Description, destinations, sourceRepos, clusterWhitelist, namespaceBlacklist)
|
||||
fmt.Fprintf(w, "%s\t%s\t%v\t%v\t%v\t%v\t%v\n", p.Name, p.Spec.Description, destinations, sourceRepos, clusterWhitelist, namespaceBlacklist, formatOrphanedResources(p))
|
||||
}
|
||||
|
||||
func printProject(p *v1alpha1.AppProject) {
|
||||
const printProjFmtStr = "%-34s%s\n"
|
||||
|
||||
fmt.Printf(printProjFmtStr, "Name:", p.Name)
|
||||
fmt.Printf(printProjFmtStr, "Description:", p.Spec.Description)
|
||||
|
||||
// Print destinations
|
||||
dest0 := "<none>"
|
||||
if len(p.Spec.Destinations) > 0 {
|
||||
dest0 = fmt.Sprintf("%s,%s", p.Spec.Destinations[0].Server, p.Spec.Destinations[0].Namespace)
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Destinations:", dest0)
|
||||
for i := 1; i < len(p.Spec.Destinations); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s,%s", p.Spec.Destinations[i].Server, p.Spec.Destinations[i].Namespace))
|
||||
}
|
||||
|
||||
// Print sources
|
||||
src0 := "<none>"
|
||||
if len(p.Spec.SourceRepos) > 0 {
|
||||
src0 = p.Spec.SourceRepos[0]
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Repositories:", src0)
|
||||
for i := 1; i < len(p.Spec.SourceRepos); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", p.Spec.SourceRepos[i])
|
||||
}
|
||||
|
||||
// Print whitelisted cluster resources
|
||||
cwl0 := "<none>"
|
||||
if len(p.Spec.ClusterResourceWhitelist) > 0 {
|
||||
cwl0 = fmt.Sprintf("%s/%s", p.Spec.ClusterResourceWhitelist[0].Group, p.Spec.ClusterResourceWhitelist[0].Kind)
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Whitelisted Cluster Resources:", cwl0)
|
||||
for i := 1; i < len(p.Spec.ClusterResourceWhitelist); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s/%s", p.Spec.ClusterResourceWhitelist[i].Group, p.Spec.ClusterResourceWhitelist[i].Kind))
|
||||
}
|
||||
|
||||
// Print blacklisted namespaced resources
|
||||
rbl0 := "<none>"
|
||||
if len(p.Spec.NamespaceResourceBlacklist) > 0 {
|
||||
rbl0 = fmt.Sprintf("%s/%s", p.Spec.NamespaceResourceBlacklist[0].Group, p.Spec.NamespaceResourceBlacklist[0].Kind)
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Blacklisted Namespaced Resources:", rbl0)
|
||||
for i := 1; i < len(p.Spec.NamespaceResourceBlacklist); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s/%s", p.Spec.NamespaceResourceBlacklist[i].Group, p.Spec.NamespaceResourceBlacklist[i].Kind))
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Orphaned Resources:", formatOrphanedResources(p))
|
||||
|
||||
}
|
||||
|
||||
// NewProjectGetCommand returns a new instance of an `argocd proj get` command
|
||||
func NewProjectGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
const printProjFmtStr = "%-34s%s\n"
|
||||
var (
|
||||
output string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "get PROJECT",
|
||||
Short: "Get project details",
|
||||
@@ -513,52 +715,21 @@ func NewProjectGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
projName := args[0]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
p, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
p, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf(printProjFmtStr, "Name:", p.Name)
|
||||
fmt.Printf(printProjFmtStr, "Description:", p.Spec.Description)
|
||||
|
||||
// Print destinations
|
||||
dest0 := "<none>"
|
||||
if len(p.Spec.Destinations) > 0 {
|
||||
dest0 = fmt.Sprintf("%s,%s", p.Spec.Destinations[0].Server, p.Spec.Destinations[0].Namespace)
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Destinations:", dest0)
|
||||
for i := 1; i < len(p.Spec.Destinations); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s,%s", p.Spec.Destinations[i].Server, p.Spec.Destinations[i].Namespace))
|
||||
}
|
||||
|
||||
// Print sources
|
||||
src0 := "<none>"
|
||||
if len(p.Spec.SourceRepos) > 0 {
|
||||
src0 = p.Spec.SourceRepos[0]
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Repositories:", src0)
|
||||
for i := 1; i < len(p.Spec.SourceRepos); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", p.Spec.SourceRepos[i])
|
||||
}
|
||||
|
||||
// Print whitelisted cluster resources
|
||||
cwl0 := "<none>"
|
||||
if len(p.Spec.ClusterResourceWhitelist) > 0 {
|
||||
cwl0 = fmt.Sprintf("%s/%s", p.Spec.ClusterResourceWhitelist[0].Group, p.Spec.ClusterResourceWhitelist[0].Kind)
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Whitelisted Cluster Resources:", cwl0)
|
||||
for i := 1; i < len(p.Spec.ClusterResourceWhitelist); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s/%s", p.Spec.ClusterResourceWhitelist[i].Group, p.Spec.ClusterResourceWhitelist[i].Kind))
|
||||
}
|
||||
|
||||
// Print blacklisted namespaced resources
|
||||
rbl0 := "<none>"
|
||||
if len(p.Spec.NamespaceResourceBlacklist) > 0 {
|
||||
rbl0 = fmt.Sprintf("%s/%s", p.Spec.NamespaceResourceBlacklist[0].Group, p.Spec.NamespaceResourceBlacklist[0].Kind)
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Blacklisted Namespaced Resources:", rbl0)
|
||||
for i := 1; i < len(p.Spec.NamespaceResourceBlacklist); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s/%s", p.Spec.NamespaceResourceBlacklist[i].Group, p.Spec.NamespaceResourceBlacklist[i].Kind))
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResource(p, output)
|
||||
errors.CheckError(err)
|
||||
case "wide", "":
|
||||
printProject(p)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -574,7 +745,7 @@ func NewProjectEditCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
projName := args[0]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
projData, err := json.Marshal(proj.Spec)
|
||||
errors.CheckError(err)
|
||||
@@ -591,12 +762,12 @@ func NewProjectEditCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
proj.Spec = updatedSpec
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to update project:\n%v", err)
|
||||
}
|
||||
|
||||
@@ -12,10 +12,9 @@ import (
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
projectpkg "github.com/argoproj/argo-cd/pkg/apiclient/project"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/server/project"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
projectutil "github.com/argoproj/argo-cd/util/project"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -40,6 +39,8 @@ func NewProjectRoleCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
roleCommand.AddCommand(NewProjectRoleDeleteTokenCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectRoleAddPolicyCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectRoleRemovePolicyCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectRoleAddGroupCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectRoleRemoveGroupCommand(clientOpts))
|
||||
return roleCommand
|
||||
}
|
||||
|
||||
@@ -61,16 +62,16 @@ func NewProjectRoleAddPolicyCommand(clientOpts *argocdclient.ClientOptions) *cob
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
role, roleIndex, err := projectutil.GetRoleByName(proj, roleName)
|
||||
role, roleIndex, err := proj.GetRoleByName(roleName)
|
||||
errors.CheckError(err)
|
||||
|
||||
policy := fmt.Sprintf(policyTemplate, proj.Name, role.Name, opts.action, proj.Name, opts.object, opts.permission)
|
||||
proj.Spec.Roles[roleIndex].Policies = append(role.Policies, policy)
|
||||
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
@@ -96,10 +97,10 @@ func NewProjectRoleRemovePolicyCommand(clientOpts *argocdclient.ClientOptions) *
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
role, roleIndex, err := projectutil.GetRoleByName(proj, roleName)
|
||||
role, roleIndex, err := proj.GetRoleByName(roleName)
|
||||
errors.CheckError(err)
|
||||
|
||||
policyToRemove := fmt.Sprintf(policyTemplate, proj.Name, role.Name, opts.action, proj.Name, opts.object, opts.permission)
|
||||
@@ -115,7 +116,7 @@ func NewProjectRoleRemovePolicyCommand(clientOpts *argocdclient.ClientOptions) *
|
||||
}
|
||||
role.Policies[duplicateIndex] = role.Policies[len(role.Policies)-1]
|
||||
proj.Spec.Roles[roleIndex].Policies = role.Policies[:len(role.Policies)-1]
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
@@ -141,17 +142,17 @@ func NewProjectRoleCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
_, _, err = projectutil.GetRoleByName(proj, roleName)
|
||||
_, _, err = proj.GetRoleByName(roleName)
|
||||
if err == nil {
|
||||
fmt.Printf("Role '%s' already exists\n", roleName)
|
||||
return
|
||||
}
|
||||
proj.Spec.Roles = append(proj.Spec.Roles, v1alpha1.ProjectRole{Name: roleName, Description: description})
|
||||
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Role '%s' created\n", roleName)
|
||||
},
|
||||
@@ -175,10 +176,10 @@ func NewProjectRoleDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
_, index, err := projectutil.GetRoleByName(proj, roleName)
|
||||
_, index, err := proj.GetRoleByName(roleName)
|
||||
if err != nil {
|
||||
fmt.Printf("Role '%s' does not exist in project\n", roleName)
|
||||
return
|
||||
@@ -186,7 +187,7 @@ func NewProjectRoleDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.
|
||||
proj.Spec.Roles[index] = proj.Spec.Roles[len(proj.Spec.Roles)-1]
|
||||
proj.Spec.Roles = proj.Spec.Roles[:len(proj.Spec.Roles)-1]
|
||||
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Role '%s' deleted\n", roleName)
|
||||
},
|
||||
@@ -213,7 +214,7 @@ func NewProjectRoleCreateTokenCommand(clientOpts *argocdclient.ClientOptions) *c
|
||||
defer util.Close(conn)
|
||||
duration, err := timeutil.ParseDuration(expiresIn)
|
||||
errors.CheckError(err)
|
||||
token, err := projIf.CreateToken(context.Background(), &project.ProjectTokenCreateRequest{Project: projName, Role: roleName, ExpiresIn: int64(duration.Seconds())})
|
||||
token, err := projIf.CreateToken(context.Background(), &projectpkg.ProjectTokenCreateRequest{Project: projName, Role: roleName, ExpiresIn: int64(duration.Seconds())})
|
||||
errors.CheckError(err)
|
||||
fmt.Println(token.Token)
|
||||
},
|
||||
@@ -241,15 +242,35 @@ func NewProjectRoleDeleteTokenCommand(clientOpts *argocdclient.ClientOptions) *c
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
_, err = projIf.DeleteToken(context.Background(), &project.ProjectTokenDeleteRequest{Project: projName, Role: roleName, Iat: issuedAt})
|
||||
_, err = projIf.DeleteToken(context.Background(), &projectpkg.ProjectTokenDeleteRequest{Project: projName, Role: roleName, Iat: issuedAt})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// Print list of project role names
|
||||
func printProjectRoleListName(roles []v1alpha1.ProjectRole) {
|
||||
for _, role := range roles {
|
||||
fmt.Println(role.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Print table of project roles
|
||||
func printProjectRoleListTable(roles []v1alpha1.ProjectRole) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "ROLE-NAME\tDESCRIPTION\n")
|
||||
for _, role := range roles {
|
||||
fmt.Fprintf(w, "%s\t%s\n", role.Name, role.Description)
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
// NewProjectRoleListCommand returns a new instance of an `argocd proj roles list` command
|
||||
func NewProjectRoleListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "list PROJECT",
|
||||
Short: "List all the roles in a project",
|
||||
@@ -262,16 +283,22 @@ func NewProjectRoleListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
project, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
project, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "ROLE-NAME\tDESCRIPTION\n")
|
||||
for _, role := range project.Spec.Roles {
|
||||
fmt.Fprintf(w, "%s\t%s\n", role.Name, role.Description)
|
||||
switch output {
|
||||
case "json", "yaml":
|
||||
err := PrintResourceList(project.Spec.Roles, output, false)
|
||||
errors.CheckError(err)
|
||||
case "name":
|
||||
printProjectRoleListName(project.Spec.Roles)
|
||||
case "wide", "":
|
||||
printProjectRoleListTable(project.Spec.Roles)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
_ = w.Flush()
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|name")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -290,10 +317,10 @@ func NewProjectRoleGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
role, _, err := projectutil.GetRoleByName(proj, roleName)
|
||||
role, _, err := proj.GetRoleByName(roleName)
|
||||
errors.CheckError(err)
|
||||
|
||||
printRoleFmtStr := "%-15s%s\n"
|
||||
@@ -322,24 +349,24 @@ func NewProjectRoleGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
|
||||
func NewProjectRoleAddGroupCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "add-group PROJECT ROLE-NAME GROUP-CLAIM",
|
||||
Short: "Add a policy to a project role",
|
||||
Short: "Add a group claim to a project role",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
if len(args) != 3 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName, roleName, groupName := args[0], args[1], args[2]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
updated, err := projectutil.AddGroupToRole(proj, roleName, groupName)
|
||||
updated, err := proj.AddGroupToRole(roleName, groupName)
|
||||
errors.CheckError(err)
|
||||
if updated {
|
||||
if !updated {
|
||||
fmt.Printf("Group '%s' already present in role '%s'\n", groupName, roleName)
|
||||
return
|
||||
}
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Group '%s' added to role '%s'\n", groupName, roleName)
|
||||
},
|
||||
@@ -360,15 +387,15 @@ func NewProjectRoleRemoveGroupCommand(clientOpts *argocdclient.ClientOptions) *c
|
||||
projName, roleName, groupName := args[0], args[1], args[2]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
updated, err := projectutil.RemoveGroupFromRole(proj, roleName, groupName)
|
||||
updated, err := proj.RemoveGroupFromRole(roleName, groupName)
|
||||
errors.CheckError(err)
|
||||
if !updated {
|
||||
fmt.Printf("Group '%s' not present in role '%s'\n", groupName, roleName)
|
||||
return
|
||||
}
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Group '%s' removed from role '%s'\n", groupName, roleName)
|
||||
},
|
||||
|
||||
322
cmd/argocd/commands/projectwindows.go
Normal file
@@ -0,0 +1,322 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"strconv"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
projectpkg "github.com/argoproj/argo-cd/pkg/apiclient/project"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
)
|
||||
|
||||
// NewProjectWindowsCommand returns a new instance of the `argocd proj windows` command
|
||||
func NewProjectWindowsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
roleCommand := &cobra.Command{
|
||||
Use: "windows",
|
||||
Short: "Manage a project's sync windows",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
},
|
||||
}
|
||||
roleCommand.AddCommand(NewProjectWindowsDisableManualSyncCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectWindowsEnableManualSyncCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectWindowsAddWindowCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectWindowsDeleteCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectWindowsListCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectWindowsUpdateCommand(clientOpts))
|
||||
return roleCommand
|
||||
}
|
||||
|
||||
// NewProjectSyncWindowsDisableManualSyncCommand returns a new instance of an `argocd proj windows disable-manual-sync` command
|
||||
func NewProjectWindowsDisableManualSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "disable-manual-sync PROJECT ID",
|
||||
Short: "Disable manual sync for a sync window",
|
||||
Long: "Disable manual sync for a sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
projName := args[0]
|
||||
id, err := strconv.Atoi(args[1])
|
||||
errors.CheckError(err)
|
||||
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
for i, window := range proj.Spec.SyncWindows {
|
||||
if id == i {
|
||||
window.ManualSync = false
|
||||
}
|
||||
}
|
||||
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectWindowsEnableManualSyncCommand returns a new instance of an `argocd proj windows enable-manual-sync` command
|
||||
func NewProjectWindowsEnableManualSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "enable-manual-sync PROJECT ID",
|
||||
Short: "Enable manual sync for a sync window",
|
||||
Long: "Enable manual sync for a sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
projName := args[0]
|
||||
id, err := strconv.Atoi(args[1])
|
||||
errors.CheckError(err)
|
||||
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
for i, window := range proj.Spec.SyncWindows {
|
||||
if id == i {
|
||||
window.ManualSync = true
|
||||
}
|
||||
}
|
||||
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectWindowsAddWindowCommand returns a new instance of an `argocd proj windows add` command
|
||||
func NewProjectWindowsAddWindowCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
kind string
|
||||
schedule string
|
||||
duration string
|
||||
applications []string
|
||||
namespaces []string
|
||||
clusters []string
|
||||
manualSync bool
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "add PROJECT",
|
||||
Short: "Add a sync window to a project",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
err = proj.Spec.AddWindow(kind, schedule, duration, applications, namespaces, clusters, manualSync)
|
||||
errors.CheckError(err)
|
||||
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&kind, "kind", "k", "", "Sync window kind, either allow or deny")
|
||||
command.Flags().StringVar(&schedule, "schedule", "", "Sync window schedule in cron format. (e.g. --schedule \"0 22 * * *\")")
|
||||
command.Flags().StringVar(&duration, "duration", "", "Sync window duration. (e.g. --duration 1h)")
|
||||
command.Flags().StringSliceVar(&applications, "applications", []string{}, "Applications that the schedule will be applied to. Comma separated, wildcards supported (e.g. --applications prod-\\*,website)")
|
||||
command.Flags().StringSliceVar(&namespaces, "namespaces", []string{}, "Namespaces that the schedule will be applied to. Comma separated, wildcards supported (e.g. --namespaces default,\\*-prod)")
|
||||
command.Flags().StringSliceVar(&clusters, "clusters", []string{}, "Clusters that the schedule will be applied to. Comma separated, wildcards supported (e.g. --clusters prod,staging)")
|
||||
command.Flags().BoolVar(&manualSync, "manual-sync", false, "Allow manual syncs for both deny and allow windows")
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectWindowsAddWindowCommand returns a new instance of an `argocd proj windows delete` command
|
||||
func NewProjectWindowsDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "delete PROJECT ID",
|
||||
Short: "Delete a sync window from a project. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
projName := args[0]
|
||||
id, err := strconv.Atoi(args[1])
|
||||
errors.CheckError(err)
|
||||
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
err = proj.Spec.DeleteWindow(id)
|
||||
errors.CheckError(err)
|
||||
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectWindowsUpdateCommand returns a new instance of an `argocd proj windows update` command
|
||||
func NewProjectWindowsUpdateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
schedule string
|
||||
duration string
|
||||
applications []string
|
||||
namespaces []string
|
||||
clusters []string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "update PROJECT ID",
|
||||
Short: "Update a project sync window",
|
||||
Long: "Update a project sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
projName := args[0]
|
||||
id, err := strconv.Atoi(args[1])
|
||||
errors.CheckError(err)
|
||||
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
for i, window := range proj.Spec.SyncWindows {
|
||||
if id == i {
|
||||
err := window.Update(schedule, duration, applications, namespaces, clusters)
|
||||
if err != nil {
|
||||
errors.CheckError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&schedule, "schedule", "", "Sync window schedule in cron format. (e.g. --schedule \"0 22 * * *\")")
|
||||
command.Flags().StringVar(&duration, "duration", "", "Sync window duration. (e.g. --duration 1h)")
|
||||
command.Flags().StringSliceVar(&applications, "applications", []string{}, "Applications that the schedule will be applied to. Comma separated, wildcards supported (e.g. --applications prod-\\*,website)")
|
||||
command.Flags().StringSliceVar(&namespaces, "namespaces", []string{}, "Namespaces that the schedule will be applied to. Comma separated, wildcards supported (e.g. --namespaces default,\\*-prod)")
|
||||
command.Flags().StringSliceVar(&clusters, "clusters", []string{}, "Clusters that the schedule will be applied to. Comma separated, wildcards supported (e.g. --clusters prod,staging)")
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectWindowsListCommand returns a new instance of an `argocd proj windows list` command
|
||||
func NewProjectWindowsListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "list PROJECT",
|
||||
Short: "List project sync windows",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(proj.Spec.SyncWindows, output, false)
|
||||
errors.CheckError(err)
|
||||
case "wide", "":
|
||||
printSyncWindows(proj)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide")
|
||||
return command
|
||||
}
|
||||
|
||||
// Print table of sync window data
|
||||
func printSyncWindows(proj *v1alpha1.AppProject) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
var fmtStr string
|
||||
headers := []interface{}{"ID", "STATUS", "KIND", "SCHEDULE", "DURATION", "APPLICATIONS", "NAMESPACES", "CLUSTERS", "MANUALSYNC"}
|
||||
fmtStr = "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n"
|
||||
fmt.Fprintf(w, fmtStr, headers...)
|
||||
if proj.Spec.SyncWindows.HasWindows() {
|
||||
for i, window := range proj.Spec.SyncWindows {
|
||||
vals := []interface{}{
|
||||
strconv.Itoa(i),
|
||||
formatBoolOutput(window.Active()),
|
||||
window.Kind,
|
||||
window.Schedule,
|
||||
window.Duration,
|
||||
formatListOutput(window.Applications),
|
||||
formatListOutput(window.Namespaces),
|
||||
formatListOutput(window.Clusters),
|
||||
formatManualOutput(window.ManualSync),
|
||||
}
|
||||
fmt.Fprintf(w, fmtStr, vals...)
|
||||
}
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
func formatListOutput(list []string) string {
|
||||
var o string
|
||||
if len(list) == 0 {
|
||||
o = "-"
|
||||
} else {
|
||||
o = strings.Join(list, ",")
|
||||
}
|
||||
return o
|
||||
}
|
||||
func formatBoolOutput(active bool) string {
|
||||
var o string
|
||||
if active {
|
||||
o = "Active"
|
||||
} else {
|
||||
o = "Inactive"
|
||||
}
|
||||
return o
|
||||
}
|
||||
func formatManualOutput(active bool) string {
|
||||
var o string
|
||||
if active {
|
||||
o = "Enabled"
|
||||
} else {
|
||||
o = "Disabled"
|
||||
}
|
||||
return o
|
||||
}
|
||||
@@ -5,13 +5,13 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
"github.com/coreos/go-oidc"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/server/settings"
|
||||
settingspkg "github.com/argoproj/argo-cd/pkg/apiclient/settings"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
"github.com/argoproj/argo-cd/util/session"
|
||||
@@ -43,11 +43,12 @@ func NewReloginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
var tokenString string
|
||||
var refreshToken string
|
||||
clientOpts := argocdclient.ClientOptions{
|
||||
ConfigPath: "",
|
||||
ServerAddr: configCtx.Server.Server,
|
||||
Insecure: configCtx.Server.Insecure,
|
||||
GRPCWeb: globalClientOpts.GRPCWeb,
|
||||
PlainText: configCtx.Server.PlainText,
|
||||
ConfigPath: "",
|
||||
ServerAddr: configCtx.Server.Server,
|
||||
Insecure: configCtx.Server.Insecure,
|
||||
GRPCWeb: globalClientOpts.GRPCWeb,
|
||||
GRPCWebRootPath: globalClientOpts.GRPCWebRootPath,
|
||||
PlainText: configCtx.Server.PlainText,
|
||||
}
|
||||
acdClient := argocdclient.NewClientOrDie(&clientOpts)
|
||||
claims, err := configCtx.User.Claims()
|
||||
@@ -63,11 +64,11 @@ func NewReloginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
httpClient, err := acdClient.HTTPClient()
|
||||
errors.CheckError(err)
|
||||
ctx = oidc.ClientContext(ctx, httpClient)
|
||||
acdSet, err := setIf.Get(ctx, &settings.SettingsQuery{})
|
||||
acdSet, err := setIf.Get(ctx, &settingspkg.SettingsQuery{})
|
||||
errors.CheckError(err)
|
||||
oauth2conf, provider, err := acdClient.OIDCConfig(ctx, acdSet)
|
||||
errors.CheckError(err)
|
||||
tokenString, refreshToken = oauth2Login(ctx, ssoPort, oauth2conf, provider)
|
||||
tokenString, refreshToken = oauth2Login(ctx, ssoPort, acdSet.GetOIDCConfig(), oauth2conf, provider)
|
||||
}
|
||||
|
||||
localCfg.UpsertUser(localconfig.User{
|
||||
|
||||
@@ -10,10 +10,11 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
repositorypkg "github.com/argoproj/argo-cd/pkg/apiclient/repository"
|
||||
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/server/repository"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
@@ -23,7 +24,7 @@ import (
|
||||
func NewRepoCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "repo",
|
||||
Short: "Manage git repository credentials",
|
||||
Short: "Manage repository connection parameters",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
@@ -39,58 +40,146 @@ func NewRepoCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
// NewRepoAddCommand returns a new instance of an `argocd repo add` command
|
||||
func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
repo appsv1.Repository
|
||||
upsert bool
|
||||
sshPrivateKeyPath string
|
||||
insecureIgnoreHostKey bool
|
||||
repo appsv1.Repository
|
||||
upsert bool
|
||||
sshPrivateKeyPath string
|
||||
insecureIgnoreHostKey bool
|
||||
insecureSkipServerVerification bool
|
||||
tlsClientCertPath string
|
||||
tlsClientCertKeyPath string
|
||||
enableLfs bool
|
||||
)
|
||||
|
||||
// For better readability and easier formatting
|
||||
var repoAddExamples = ` # Add a Git repository via SSH using a private key for authentication, ignoring the server's host key:
|
||||
argocd repo add git@git.example.com:repos/repo --insecure-ignore-host-key --ssh-private-key-path ~/id_rsa
|
||||
|
||||
# Add a Git repository via SSH on a non-default port - need to use ssh:// style URLs here
|
||||
argocd repo add ssh://git@git.example.com:2222/repos/repo --ssh-private-key-path ~/id_rsa
|
||||
|
||||
# Add a private Git repository via HTTPS using username/password and TLS client certificates:
|
||||
argocd repo add https://git.example.com/repos/repo --username git --password secret --tls-client-cert-path ~/mycert.crt --tls-client-cert-key-path ~/mycert.key
|
||||
|
||||
# Add a private Git repository via HTTPS using username/password without verifying the server's TLS certificate
|
||||
argocd repo add https://git.example.com/repos/repo --username git --password secret --insecure-skip-server-verification
|
||||
|
||||
# Add a public Helm repository named 'stable' via HTTPS
|
||||
argocd repo add https://kubernetes-charts.storage.googleapis.com --type helm --name stable
|
||||
|
||||
# Add a private Helm repository named 'stable' via HTTPS
|
||||
argocd repo add https://kubernetes-charts.storage.googleapis.com --type helm --name stable --username test --password test
|
||||
`
|
||||
|
||||
var command = &cobra.Command{
|
||||
Use: "add REPO",
|
||||
Short: "Add git repository credentials",
|
||||
Use: "add REPOURL",
|
||||
Short: "Add git repository connection parameters",
|
||||
Example: repoAddExamples,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Repository URL
|
||||
repo.Repo = args[0]
|
||||
|
||||
// Specifying ssh-private-key-path is only valid for SSH repositories
|
||||
if sshPrivateKeyPath != "" {
|
||||
keyData, err := ioutil.ReadFile(sshPrivateKeyPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
if ok, _ := git.IsSSHURL(repo.Repo); ok {
|
||||
keyData, err := ioutil.ReadFile(sshPrivateKeyPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
repo.SSHPrivateKey = string(keyData)
|
||||
} else {
|
||||
err := fmt.Errorf("--ssh-private-key-path is only supported for SSH repositories.")
|
||||
errors.CheckError(err)
|
||||
}
|
||||
repo.SSHPrivateKey = string(keyData)
|
||||
}
|
||||
|
||||
// tls-client-cert-path and tls-client-cert-key-key-path must always be
|
||||
// specified together
|
||||
if (tlsClientCertPath != "" && tlsClientCertKeyPath == "") || (tlsClientCertPath == "" && tlsClientCertKeyPath != "") {
|
||||
err := fmt.Errorf("--tls-client-cert-path and --tls-client-cert-key-path must be specified together")
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
// Specifying tls-client-cert-path is only valid for HTTPS repositories
|
||||
if tlsClientCertPath != "" {
|
||||
if git.IsHTTPSURL(repo.Repo) {
|
||||
tlsCertData, err := ioutil.ReadFile(tlsClientCertPath)
|
||||
errors.CheckError(err)
|
||||
tlsCertKey, err := ioutil.ReadFile(tlsClientCertKeyPath)
|
||||
errors.CheckError(err)
|
||||
repo.TLSClientCertData = string(tlsCertData)
|
||||
repo.TLSClientCertKey = string(tlsCertKey)
|
||||
} else {
|
||||
err := fmt.Errorf("--tls-client-cert-path is only supported for HTTPS repositories")
|
||||
errors.CheckError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Set repository connection properties only when creating repository, not
|
||||
// when creating repository credentials.
|
||||
// InsecureIgnoreHostKey is deprecated and only here for backwards compat
|
||||
repo.InsecureIgnoreHostKey = insecureIgnoreHostKey
|
||||
// First test the repo *without* username/password. This gives us a hint on whether this
|
||||
// is a private repo.
|
||||
// NOTE: it is important not to run git commands to test git credentials on the user's
|
||||
// system since it may mess with their git credential store (e.g. osx keychain).
|
||||
// See issue #315
|
||||
err := git.TestRepo(repo.Repo, "", "", repo.SSHPrivateKey, repo.InsecureIgnoreHostKey)
|
||||
if err != nil {
|
||||
if git.IsSSHURL(repo.Repo) {
|
||||
// If we failed using git SSH credentials, then the repo is automatically bad
|
||||
log.Fatal(err)
|
||||
}
|
||||
// If we can't test the repo, it's probably private. Prompt for credentials and
|
||||
// let the server test it.
|
||||
repo.Username, repo.Password = cli.PromptCredentials(repo.Username, repo.Password)
|
||||
repo.Insecure = insecureSkipServerVerification
|
||||
repo.EnableLFS = enableLfs
|
||||
|
||||
if repo.Type == "helm" && repo.Name == "" {
|
||||
errors.CheckError(fmt.Errorf("Must specify --name for repos of type 'helm'"))
|
||||
}
|
||||
|
||||
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoClientOrDie()
|
||||
defer util.Close(conn)
|
||||
repoCreateReq := repository.RepoCreateRequest{
|
||||
|
||||
// If the user set a username, but didn't supply password via --password,
|
||||
// then we prompt for it
|
||||
if repo.Username != "" && repo.Password == "" {
|
||||
repo.Password = cli.PromptPassword(repo.Password)
|
||||
}
|
||||
|
||||
// We let the server check access to the repository before adding it. If
|
||||
// it is a private repo, but we cannot access with with the credentials
|
||||
// that were supplied, we bail out.
|
||||
//
|
||||
// Skip validation if we are just adding credentials template, chances
|
||||
// are high that we do not have the given URL pointing to a valid Git
|
||||
// repo anyway.
|
||||
repoAccessReq := repositorypkg.RepoAccessQuery{
|
||||
Repo: repo.Repo,
|
||||
Type: repo.Type,
|
||||
Name: repo.Name,
|
||||
Username: repo.Username,
|
||||
Password: repo.Password,
|
||||
SshPrivateKey: repo.SSHPrivateKey,
|
||||
TlsClientCertData: repo.TLSClientCertData,
|
||||
TlsClientCertKey: repo.TLSClientCertKey,
|
||||
Insecure: repo.IsInsecure(),
|
||||
}
|
||||
_, err := repoIf.ValidateAccess(context.Background(), &repoAccessReq)
|
||||
errors.CheckError(err)
|
||||
|
||||
repoCreateReq := repositorypkg.RepoCreateRequest{
|
||||
Repo: &repo,
|
||||
Upsert: upsert,
|
||||
}
|
||||
|
||||
createdRepo, err := repoIf.Create(context.Background(), &repoCreateReq)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("repository '%s' added\n", createdRepo.Repo)
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&repo.Type, "type", common.DefaultRepoType, "type of the repository, \"git\" or \"helm\"")
|
||||
command.Flags().StringVar(&repo.Name, "name", "", "name of the repository, mandatory for repositories of type helm")
|
||||
command.Flags().StringVar(&repo.Username, "username", "", "username to the repository")
|
||||
command.Flags().StringVar(&repo.Password, "password", "", "password to the repository")
|
||||
command.Flags().StringVar(&sshPrivateKeyPath, "ssh-private-key-path", "", "path to the private ssh key (e.g. ~/.ssh/id_rsa)")
|
||||
command.Flags().BoolVar(&insecureIgnoreHostKey, "insecure-ignore-host-key", false, "disables SSH strict host key checking")
|
||||
command.Flags().StringVar(&tlsClientCertPath, "tls-client-cert-path", "", "path to the TLS client cert (must be PEM format)")
|
||||
command.Flags().StringVar(&tlsClientCertKeyPath, "tls-client-cert-key-path", "", "path to the TLS client cert's key path (must be PEM format)")
|
||||
command.Flags().BoolVar(&insecureIgnoreHostKey, "insecure-ignore-host-key", false, "disables SSH strict host key checking (deprecated, use --insecure-skip-server-verification instead)")
|
||||
command.Flags().BoolVar(&insecureSkipServerVerification, "insecure-skip-server-verification", false, "disables server certificate and host key checks")
|
||||
command.Flags().BoolVar(&enableLfs, "enable-lfs", false, "enable git-lfs (Large File Support) on this repository")
|
||||
command.Flags().BoolVar(&upsert, "upsert", false, "Override an existing repository with the same name even if the spec differs")
|
||||
return command
|
||||
}
|
||||
@@ -99,7 +188,7 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
func NewRepoRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "rm REPO",
|
||||
Short: "Remove git repository credentials",
|
||||
Short: "Remove repository credentials",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
@@ -108,7 +197,7 @@ func NewRepoRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoClientOrDie()
|
||||
defer util.Close(conn)
|
||||
for _, repoURL := range args {
|
||||
_, err := repoIf.Delete(context.Background(), &repository.RepoQuery{Repo: repoURL})
|
||||
_, err := repoIf.Delete(context.Background(), &repositorypkg.RepoQuery{Repo: repoURL})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
@@ -116,23 +205,71 @@ func NewRepoRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
return command
|
||||
}
|
||||
|
||||
// Print table of repo info
|
||||
func printRepoTable(repos appsv1.Repositories) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintf(w, "TYPE\tNAME\tREPO\tINSECURE\tLFS\tCREDS\tSTATUS\tMESSAGE\n")
|
||||
for _, r := range repos {
|
||||
var hasCreds string
|
||||
if !r.HasCredentials() {
|
||||
hasCreds = "false"
|
||||
} else {
|
||||
if r.InheritedCreds {
|
||||
hasCreds = "inherited"
|
||||
} else {
|
||||
hasCreds = "true"
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%v\t%s\t%s\t%s\n", r.Type, r.Name, r.Repo, r.IsInsecure(), r.EnableLFS, hasCreds, r.ConnectionState.Status, r.ConnectionState.Message)
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
// Print list of repo urls or url patterns for repository credentials
|
||||
func printRepoUrls(repos appsv1.Repositories) {
|
||||
for _, r := range repos {
|
||||
fmt.Println(r.Repo)
|
||||
}
|
||||
}
|
||||
|
||||
// NewRepoListCommand returns a new instance of an `argocd repo rm` command
|
||||
func NewRepoListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
refresh string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List configured repositories",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoClientOrDie()
|
||||
defer util.Close(conn)
|
||||
repos, err := repoIf.List(context.Background(), &repository.RepoQuery{})
|
||||
errors.CheckError(err)
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "REPO\tUSER\tSTATUS\tMESSAGE\n")
|
||||
for _, r := range repos.Items {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", r.Repo, r.Username, r.ConnectionState.Status, r.ConnectionState.Message)
|
||||
forceRefresh := false
|
||||
switch refresh {
|
||||
case "":
|
||||
case "hard":
|
||||
forceRefresh = true
|
||||
default:
|
||||
err := fmt.Errorf("--refresh must be one of: 'hard'")
|
||||
errors.CheckError(err)
|
||||
}
|
||||
repos, err := repoIf.List(context.Background(), &repositorypkg.RepoQuery{ForceRefresh: forceRefresh})
|
||||
errors.CheckError(err)
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(repos.Items, output, false)
|
||||
errors.CheckError(err)
|
||||
case "url":
|
||||
printRepoUrls(repos.Items)
|
||||
// wide is the default
|
||||
case "wide", "":
|
||||
printRepoTable(repos.Items)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
_ = w.Flush()
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|url")
|
||||
command.Flags().StringVar(&refresh, "refresh", "", "Force a cache refresh on connection status")
|
||||
return command
|
||||
}
|
||||
|
||||
203
cmd/argocd/commands/repocreds.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
repocredspkg "github.com/argoproj/argo-cd/pkg/apiclient/repocreds"
|
||||
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
)
|
||||
|
||||
// NewRepoCredsCommand returns a new instance of an `argocd repocreds` command
|
||||
func NewRepoCredsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "repocreds",
|
||||
Short: "Manage repository connection parameters",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
},
|
||||
}
|
||||
|
||||
command.AddCommand(NewRepoCredsAddCommand(clientOpts))
|
||||
command.AddCommand(NewRepoCredsListCommand(clientOpts))
|
||||
command.AddCommand(NewRepoCredsRemoveCommand(clientOpts))
|
||||
return command
|
||||
}
|
||||
|
||||
// NewRepoCredsAddCommand returns a new instance of an `argocd repocreds add` command
|
||||
func NewRepoCredsAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
repo appsv1.RepoCreds
|
||||
upsert bool
|
||||
sshPrivateKeyPath string
|
||||
tlsClientCertPath string
|
||||
tlsClientCertKeyPath string
|
||||
)
|
||||
|
||||
// For better readability and easier formatting
|
||||
var repocredsAddExamples = ` # Add credentials with user/pass authentication to use for all repositories under https://git.example.com/repos
|
||||
argocd repocreds add https://git.example.com/repos/ --username git --password secret
|
||||
|
||||
# Add credentials with SSH private key authentication to use for all repositories under ssh://git@git.example.com/repos
|
||||
argocd repocreds add ssh://git@git.example.com/repos/ --ssh-private-key-path ~/.ssh/id_rsa
|
||||
`
|
||||
|
||||
var command = &cobra.Command{
|
||||
Use: "add REPOURL",
|
||||
Short: "Add git repository connection parameters",
|
||||
Example: repocredsAddExamples,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Repository URL
|
||||
repo.URL = args[0]
|
||||
|
||||
// Specifying ssh-private-key-path is only valid for SSH repositories
|
||||
if sshPrivateKeyPath != "" {
|
||||
if ok, _ := git.IsSSHURL(repo.URL); ok {
|
||||
keyData, err := ioutil.ReadFile(sshPrivateKeyPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
repo.SSHPrivateKey = string(keyData)
|
||||
} else {
|
||||
err := fmt.Errorf("--ssh-private-key-path is only supported for SSH repositories.")
|
||||
errors.CheckError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// tls-client-cert-path and tls-client-cert-key-key-path must always be
|
||||
// specified together
|
||||
if (tlsClientCertPath != "" && tlsClientCertKeyPath == "") || (tlsClientCertPath == "" && tlsClientCertKeyPath != "") {
|
||||
err := fmt.Errorf("--tls-client-cert-path and --tls-client-cert-key-path must be specified together")
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
// Specifying tls-client-cert-path is only valid for HTTPS repositories
|
||||
if tlsClientCertPath != "" {
|
||||
if git.IsHTTPSURL(repo.URL) {
|
||||
tlsCertData, err := ioutil.ReadFile(tlsClientCertPath)
|
||||
errors.CheckError(err)
|
||||
tlsCertKey, err := ioutil.ReadFile(tlsClientCertKeyPath)
|
||||
errors.CheckError(err)
|
||||
repo.TLSClientCertData = string(tlsCertData)
|
||||
repo.TLSClientCertKey = string(tlsCertKey)
|
||||
} else {
|
||||
err := fmt.Errorf("--tls-client-cert-path is only supported for HTTPS repositories")
|
||||
errors.CheckError(err)
|
||||
}
|
||||
}
|
||||
|
||||
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoCredsClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
// If the user set a username, but didn't supply password via --password,
|
||||
// then we prompt for it
|
||||
if repo.Username != "" && repo.Password == "" {
|
||||
repo.Password = cli.PromptPassword(repo.Password)
|
||||
}
|
||||
|
||||
repoCreateReq := repocredspkg.RepoCredsCreateRequest{
|
||||
Creds: &repo,
|
||||
Upsert: upsert,
|
||||
}
|
||||
|
||||
createdRepo, err := repoIf.CreateRepositoryCredentials(context.Background(), &repoCreateReq)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("repository credentials for '%s' added\n", createdRepo.URL)
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&repo.Username, "username", "", "username to the repository")
|
||||
command.Flags().StringVar(&repo.Password, "password", "", "password to the repository")
|
||||
command.Flags().StringVar(&sshPrivateKeyPath, "ssh-private-key-path", "", "path to the private ssh key (e.g. ~/.ssh/id_rsa)")
|
||||
command.Flags().StringVar(&tlsClientCertPath, "tls-client-cert-path", "", "path to the TLS client cert (must be PEM format)")
|
||||
command.Flags().StringVar(&tlsClientCertKeyPath, "tls-client-cert-key-path", "", "path to the TLS client cert's key path (must be PEM format)")
|
||||
command.Flags().BoolVar(&upsert, "upsert", false, "Override an existing repository with the same name even if the spec differs")
|
||||
return command
|
||||
}
|
||||
|
||||
// NewRepoCredsRemoveCommand returns a new instance of an `argocd repocreds rm` command
|
||||
func NewRepoCredsRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "rm CREDSURL",
|
||||
Short: "Remove repository credentials",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoCredsClientOrDie()
|
||||
defer util.Close(conn)
|
||||
for _, repoURL := range args {
|
||||
_, err := repoIf.DeleteRepositoryCredentials(context.Background(), &repocredspkg.RepoCredsDeleteRequest{Url: repoURL})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// Print the repository credentials as table
|
||||
func printRepoCredsTable(repos []appsv1.RepoCreds) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "URL PATTERN\tUSERNAME\tSSH_CREDS\tTLS_CREDS\n")
|
||||
for _, r := range repos {
|
||||
if r.Username == "" {
|
||||
r.Username = "-"
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%v\t%v\n", r.URL, r.Username, r.SSHPrivateKey != "", r.TLSClientCertData != "")
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
// Print list of repo urls or url patterns for repository credentials
|
||||
func printRepoCredsUrls(repos []appsv1.RepoCreds) {
|
||||
for _, r := range repos {
|
||||
fmt.Println(r.URL)
|
||||
}
|
||||
}
|
||||
|
||||
// NewRepoCredsListCommand returns a new instance of an `argocd repo list` command
|
||||
func NewRepoCredsListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List configured repository credentials",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoCredsClientOrDie()
|
||||
defer util.Close(conn)
|
||||
repos, err := repoIf.ListRepositoryCredentials(context.Background(), &repocredspkg.RepoCredsQuery{})
|
||||
errors.CheckError(err)
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(repos.Items, output, false)
|
||||
errors.CheckError(err)
|
||||
case "url":
|
||||
printRepoCredsUrls(repos.Items)
|
||||
case "wide", "":
|
||||
printRepoCredsTable(repos.Items)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|url")
|
||||
return command
|
||||
}
|
||||
@@ -36,15 +36,19 @@ func NewCommand() *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
command.AddCommand(NewCompletionCommand())
|
||||
command.AddCommand(NewVersionCmd(&clientOpts))
|
||||
command.AddCommand(NewClusterCommand(&clientOpts, pathOpts))
|
||||
command.AddCommand(NewApplicationCommand(&clientOpts))
|
||||
command.AddCommand(NewLoginCommand(&clientOpts))
|
||||
command.AddCommand(NewReloginCommand(&clientOpts))
|
||||
command.AddCommand(NewRepoCommand(&clientOpts))
|
||||
command.AddCommand(NewRepoCredsCommand(&clientOpts))
|
||||
command.AddCommand(NewContextCommand(&clientOpts))
|
||||
command.AddCommand(NewProjectCommand(&clientOpts))
|
||||
command.AddCommand(NewAccountCommand(&clientOpts))
|
||||
command.AddCommand(NewLogoutCommand(&clientOpts))
|
||||
command.AddCommand(NewCertCommand(&clientOpts))
|
||||
|
||||
defaultLocalConfigPath, err := localconfig.DefaultLocalConfigPath()
|
||||
errors.CheckError(err)
|
||||
@@ -55,6 +59,10 @@ func NewCommand() *cobra.Command {
|
||||
command.PersistentFlags().StringVar(&clientOpts.CertFile, "server-crt", config.GetFlag("server-crt", ""), "Server certificate file")
|
||||
command.PersistentFlags().StringVar(&clientOpts.AuthToken, "auth-token", config.GetFlag("auth-token", ""), "Authentication token")
|
||||
command.PersistentFlags().BoolVar(&clientOpts.GRPCWeb, "grpc-web", config.GetBoolFlag("grpc-web"), "Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2.")
|
||||
command.PersistentFlags().StringVar(&clientOpts.GRPCWebRootPath, "grpc-web-root-path", config.GetFlag("grpc-web-root-path", ""), "Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2. Set web root.")
|
||||
command.PersistentFlags().StringVar(&logLevel, "loglevel", config.GetFlag("loglevel", "info"), "Set the logging level. One of: debug|info|warn|error")
|
||||
command.PersistentFlags().StringSliceVarP(&clientOpts.Headers, "header", "H", []string{}, "Sets additional header to all requests made by Argo CD CLI. (Can be repeated multiple times to add multiple headers, also supports comma separated headers)")
|
||||
command.PersistentFlags().BoolVar(&clientOpts.PortForward, "port-forward", config.GetBoolFlag("port-forward"), "Connect to a random argocd-server port using port forwarding")
|
||||
command.PersistentFlags().StringVar(&clientOpts.PortForwardNamespace, "port-forward-namespace", config.GetFlag("port-forward-namespace", ""), "Namespace name which should be used for port forwarding")
|
||||
return command
|
||||
}
|
||||
|
||||
25
cmd/argocd/commands/testdata/config
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
contexts:
|
||||
- name: argocd1.example.com:443
|
||||
server: argocd1.example.com:443
|
||||
user: argocd1.example.com:443
|
||||
- name: argocd2.example.com:443
|
||||
server: argocd2.example.com:443
|
||||
user: argocd2.example.com:443
|
||||
- name: localhost:8080
|
||||
server: localhost:8080
|
||||
user: localhost:8080
|
||||
current-context: localhost:8080
|
||||
servers:
|
||||
- server: argocd1.example.com:443
|
||||
- server: argocd2.example.com:443
|
||||
- plain-text: true
|
||||
server: localhost:8080
|
||||
users:
|
||||
- auth-token: vErrYS3c3tReFRe$hToken
|
||||
name: argocd1.example.com:443
|
||||
refresh-token: vErrYS3c3tReFRe$hToken
|
||||
- auth-token: vErrYS3c3tReFRe$hToken
|
||||
name: argocd2.example.com:443
|
||||
refresh-token: vErrYS3c3tReFRe$hToken
|
||||
- auth-token: vErrYS3c3tReFRe$hToken
|
||||
name: localhost:8080
|
||||
@@ -3,64 +3,121 @@ package commands
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
argocd "github.com/argoproj/argo-cd"
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/pkg/apiclient/version"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
)
|
||||
|
||||
// NewVersionCmd returns a new `version` command to be used as a sub-command to root
|
||||
func NewVersionCmd(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var short bool
|
||||
var client bool
|
||||
var (
|
||||
short bool
|
||||
client bool
|
||||
output string
|
||||
)
|
||||
|
||||
versionCmd := cobra.Command{
|
||||
Use: "version",
|
||||
Short: fmt.Sprintf("Print version information"),
|
||||
Example: ` # Print the full version of client and server to stdout
|
||||
argocd version
|
||||
|
||||
# Print only full version of the client - no connection to server will be made
|
||||
argocd version --client
|
||||
|
||||
# Print the full version of client and server in JSON format
|
||||
argocd version -o json
|
||||
|
||||
# Print only client and server core version strings in YAML format
|
||||
argocd version --short -o yaml
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
version := argocd.GetVersion()
|
||||
fmt.Printf("%s: %s\n", cliName, version)
|
||||
if !short {
|
||||
fmt.Printf(" BuildDate: %s\n", version.BuildDate)
|
||||
fmt.Printf(" GitCommit: %s\n", version.GitCommit)
|
||||
fmt.Printf(" GitTreeState: %s\n", version.GitTreeState)
|
||||
if version.GitTag != "" {
|
||||
fmt.Printf(" GitTag: %s\n", version.GitTag)
|
||||
var (
|
||||
versionIf version.VersionServiceClient
|
||||
serverVers *version.VersionMessage
|
||||
conn io.Closer
|
||||
err error
|
||||
)
|
||||
if !client {
|
||||
// Get Server version
|
||||
conn, versionIf = argocdclient.NewClientOrDie(clientOpts).NewVersionClientOrDie()
|
||||
defer util.Close(conn)
|
||||
serverVers, err = versionIf.Version(context.Background(), &empty.Empty{})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
clientVers := common.GetVersion()
|
||||
version := make(map[string]interface{})
|
||||
if !short {
|
||||
version["client"] = clientVers
|
||||
} else {
|
||||
version["client"] = map[string]string{cliName: clientVers.Version}
|
||||
}
|
||||
fmt.Printf(" GoVersion: %s\n", version.GoVersion)
|
||||
fmt.Printf(" Compiler: %s\n", version.Compiler)
|
||||
fmt.Printf(" Platform: %s\n", version.Platform)
|
||||
}
|
||||
if client {
|
||||
return
|
||||
}
|
||||
|
||||
// Get Server version
|
||||
conn, versionIf := argocdclient.NewClientOrDie(clientOpts).NewVersionClientOrDie()
|
||||
defer util.Close(conn)
|
||||
serverVers, err := versionIf.Version(context.Background(), &empty.Empty{})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("%s: %s\n", "argocd-server", serverVers.Version)
|
||||
if !short {
|
||||
fmt.Printf(" BuildDate: %s\n", serverVers.BuildDate)
|
||||
fmt.Printf(" GitCommit: %s\n", serverVers.GitCommit)
|
||||
fmt.Printf(" GitTreeState: %s\n", serverVers.GitTreeState)
|
||||
if version.GitTag != "" {
|
||||
fmt.Printf(" GitTag: %s\n", serverVers.GitTag)
|
||||
if !client {
|
||||
if !short {
|
||||
version["server"] = serverVers
|
||||
} else {
|
||||
version["server"] = map[string]string{"argocd-server": serverVers.Version}
|
||||
}
|
||||
}
|
||||
fmt.Printf(" GoVersion: %s\n", serverVers.GoVersion)
|
||||
fmt.Printf(" Compiler: %s\n", serverVers.Compiler)
|
||||
fmt.Printf(" Platform: %s\n", serverVers.Platform)
|
||||
fmt.Printf(" Ksonnet Version: %s\n", serverVers.KsonnetVersion)
|
||||
err := PrintResource(version, output)
|
||||
errors.CheckError(err)
|
||||
case "short":
|
||||
printVersion(serverVers, client, true)
|
||||
case "wide", "":
|
||||
// we use value of short for backward compatibility
|
||||
printVersion(serverVers, client, short)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
versionCmd.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|short")
|
||||
versionCmd.Flags().BoolVar(&short, "short", false, "print just the version number")
|
||||
versionCmd.Flags().BoolVar(&client, "client", false, "client version only (no server required)")
|
||||
return &versionCmd
|
||||
}
|
||||
|
||||
func printVersion(serverVers *version.VersionMessage, client bool, short bool) {
|
||||
version := common.GetVersion()
|
||||
fmt.Printf("%s: %s\n", cliName, version)
|
||||
if !short {
|
||||
fmt.Printf(" BuildDate: %s\n", version.BuildDate)
|
||||
fmt.Printf(" GitCommit: %s\n", version.GitCommit)
|
||||
fmt.Printf(" GitTreeState: %s\n", version.GitTreeState)
|
||||
if version.GitTag != "" {
|
||||
fmt.Printf(" GitTag: %s\n", version.GitTag)
|
||||
}
|
||||
fmt.Printf(" GoVersion: %s\n", version.GoVersion)
|
||||
fmt.Printf(" Compiler: %s\n", version.Compiler)
|
||||
fmt.Printf(" Platform: %s\n", version.Platform)
|
||||
}
|
||||
if client {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("%s: %s\n", "argocd-server", serverVers.Version)
|
||||
if !short {
|
||||
fmt.Printf(" BuildDate: %s\n", serverVers.BuildDate)
|
||||
fmt.Printf(" GitCommit: %s\n", serverVers.GitCommit)
|
||||
fmt.Printf(" GitTreeState: %s\n", serverVers.GitTreeState)
|
||||
if version.GitTag != "" {
|
||||
fmt.Printf(" GitTag: %s\n", serverVers.GitTag)
|
||||
}
|
||||
fmt.Printf(" GoVersion: %s\n", serverVers.GoVersion)
|
||||
fmt.Printf(" Compiler: %s\n", serverVers.Compiler)
|
||||
fmt.Printf(" Platform: %s\n", serverVers.Platform)
|
||||
fmt.Printf(" Ksonnet Version: %s\n", serverVers.KsonnetVersion)
|
||||
fmt.Printf(" Kustomize Version: %s\n", serverVers.KustomizeVersion)
|
||||
fmt.Printf(" Helm Version: %s\n", serverVers.HelmVersion)
|
||||
fmt.Printf(" Kubectl Version: %s\n", serverVers.KubectlVersion)
|
||||
}
|
||||
}
|
||||
|
||||
101
common/common.go
@@ -1,5 +1,11 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Default service addresses and URLS of Argo CD internal services
|
||||
const (
|
||||
// DefaultRepoServerAddr is the gRPC address of the Argo CD repo server
|
||||
@@ -15,14 +21,35 @@ const (
|
||||
ArgoCDConfigMapName = "argocd-cm"
|
||||
ArgoCDSecretName = "argocd-secret"
|
||||
ArgoCDRBACConfigMapName = "argocd-rbac-cm"
|
||||
// Contains SSH known hosts data for connecting repositories. Will get mounted as volume to pods
|
||||
ArgoCDKnownHostsConfigMapName = "argocd-ssh-known-hosts-cm"
|
||||
// Contains TLS certificate data for connecting repositories. Will get mounted as volume to pods
|
||||
ArgoCDTLSCertsConfigMapName = "argocd-tls-certs-cm"
|
||||
)
|
||||
|
||||
// Some default configurables
|
||||
const (
|
||||
PortAPIServer = 8080
|
||||
PortRepoServer = 8081
|
||||
PortArgoCDMetrics = 8082
|
||||
PortArgoCDAPIServerMetrics = 8083
|
||||
PortRepoServerMetrics = 8084
|
||||
DefaultSystemNamespace = "kube-system"
|
||||
DefaultRepoType = "git"
|
||||
)
|
||||
|
||||
// Default listener ports for ArgoCD components
|
||||
const (
|
||||
DefaultPortAPIServer = 8080
|
||||
DefaultPortRepoServer = 8081
|
||||
DefaultPortArgoCDMetrics = 8082
|
||||
DefaultPortArgoCDAPIServerMetrics = 8083
|
||||
DefaultPortRepoServerMetrics = 8084
|
||||
)
|
||||
|
||||
// Default paths on the pod's file system
|
||||
const (
|
||||
// The default path where TLS certificates for repositories are located
|
||||
DefaultPathTLSConfig = "/app/config/tls"
|
||||
// The default path where SSH known hosts are stored
|
||||
DefaultPathSSHConfig = "/app/config/ssh"
|
||||
// Default name for the SSH known hosts file
|
||||
DefaultSSHKnownHostsName = "ssh_known_hosts"
|
||||
)
|
||||
|
||||
// Argo CD application related constants
|
||||
@@ -39,10 +66,8 @@ const (
|
||||
AuthCookieName = "argocd.token"
|
||||
// RevisionHistoryLimit is the max number of successful sync to keep in history
|
||||
RevisionHistoryLimit = 10
|
||||
// K8sClientConfigQPS controls the QPS to be used in K8s REST client configs
|
||||
K8sClientConfigQPS = 25
|
||||
// K8sClientConfigBurst controls the burst to be used in K8s REST client configs
|
||||
K8sClientConfigBurst = 50
|
||||
// ChangePasswordSSOTokenMaxAge is the max token age for password change operation
|
||||
ChangePasswordSSOTokenMaxAge = time.Minute * 5
|
||||
)
|
||||
|
||||
// Dex related constants
|
||||
@@ -53,6 +78,8 @@ const (
|
||||
LoginEndpoint = "/auth/login"
|
||||
// CallbackEndpoint is Argo CD's final callback endpoint we reach after OAuth 2.0 login flow has been completed
|
||||
CallbackEndpoint = "/auth/callback"
|
||||
// DexCallbackEndpoint is Argo CD's final callback endpoint when Dex is configured
|
||||
DexCallbackEndpoint = "/api/dex/callback"
|
||||
// ArgoCDClientAppName is name of the Oauth client app used when registering our web app to dex
|
||||
ArgoCDClientAppName = "Argo CD"
|
||||
// ArgoCDClientAppID is the Oauth client ID we will use when registering our app to dex
|
||||
@@ -75,6 +102,12 @@ const (
|
||||
// LabelValueSecretTypeCluster indicates a secret type of cluster
|
||||
LabelValueSecretTypeCluster = "cluster"
|
||||
|
||||
// AnnotationCompareOptions is a comma-separated list of options for comparison
|
||||
AnnotationCompareOptions = "argocd.argoproj.io/compare-options"
|
||||
// AnnotationSyncOptions is a comma-separated list of options for syncing
|
||||
AnnotationSyncOptions = "argocd.argoproj.io/sync-options"
|
||||
// AnnotationSyncWave indicates which wave of the sync the resource or hook should be in
|
||||
AnnotationSyncWave = "argocd.argoproj.io/sync-wave"
|
||||
// AnnotationKeyHook contains the hook type of a resource
|
||||
AnnotationKeyHook = "argocd.argoproj.io/hook"
|
||||
// AnnotationKeyHookDeletePolicy is the policy of deleting a hook
|
||||
@@ -86,10 +119,6 @@ const (
|
||||
AnnotationKeyManagedBy = "managed-by"
|
||||
// AnnotationValueManagedByArgoCD is a 'managed-by' annotation value for resources managed by Argo CD
|
||||
AnnotationValueManagedByArgoCD = "argocd.argoproj.io"
|
||||
// AnnotationKeyHelmHook is the helm hook annotation
|
||||
AnnotationKeyHelmHook = "helm.sh/hook"
|
||||
// AnnotationValueHelmHookCRDInstall is a value of crd helm hook
|
||||
AnnotationValueHelmHookCRDInstall = "crd-install"
|
||||
// ResourcesFinalizerName the finalizer value which we inject to finalize deletion of an application
|
||||
ResourcesFinalizerName = "resources-finalizer.argocd.argoproj.io"
|
||||
)
|
||||
@@ -103,14 +132,58 @@ const (
|
||||
// EnvVarFakeInClusterConfig is an environment variable to fake an in-cluster RESTConfig using
|
||||
// the current kubectl context (for development purposes)
|
||||
EnvVarFakeInClusterConfig = "ARGOCD_FAKE_IN_CLUSTER"
|
||||
// Overrides the location where SSH known hosts for repo access data is stored
|
||||
EnvVarSSHDataPath = "ARGOCD_SSH_DATA_PATH"
|
||||
// Overrides the location where TLS certificate for repo access data is stored
|
||||
EnvVarTLSDataPath = "ARGOCD_TLS_DATA_PATH"
|
||||
// Specifies number of git remote operations attempts count
|
||||
EnvGitAttemptsCount = "ARGOCD_GIT_ATTEMPTS_COUNT"
|
||||
// Overrides git submodule support, true by default
|
||||
EnvGitSubmoduleEnabled = "ARGOCD_GIT_MODULES_ENABLED"
|
||||
// EnvK8sClientQPS is the QPS value used for the kubernetes client (default: 50)
|
||||
EnvK8sClientQPS = "ARGOCD_K8S_CLIENT_QPS"
|
||||
// EnvK8sClientBurst is the burst value used for the kubernetes client (default: twice the client QPS)
|
||||
EnvK8sClientBurst = "ARGOCD_K8S_CLIENT_BURST"
|
||||
// EnvK8sClientMaxIdleConnections is the number of max idle connections in K8s REST client HTTP transport (default: 500)
|
||||
EnvK8sClientMaxIdleConnections = "ARGOCD_K8S_CLIENT_MAX_IDLE_CONNECTIONS"
|
||||
)
|
||||
|
||||
const (
|
||||
// MinClientVersion is the minimum client version that can interface with this API server.
|
||||
// When introducing breaking changes to the API or datastructures, this number should be bumped.
|
||||
// The value here may be lower than the current value in VERSION
|
||||
MinClientVersion = "1.0.0"
|
||||
MinClientVersion = "1.4.0"
|
||||
// CacheVersion is a objects version cached using util/cache/cache.go.
|
||||
// Number should be bumped in case of backward incompatible change to make sure cache is invalidated after upgrade.
|
||||
CacheVersion = "1.0.0"
|
||||
)
|
||||
|
||||
var (
|
||||
// K8sClientConfigQPS controls the QPS to be used in K8s REST client configs
|
||||
K8sClientConfigQPS float32 = 50
|
||||
// K8sClientConfigBurst controls the burst to be used in K8s REST client configs
|
||||
K8sClientConfigBurst int = 100
|
||||
// K8sMaxIdleConnections controls the number of max idle connections in K8s REST client HTTP transport
|
||||
K8sMaxIdleConnections = 500
|
||||
)
|
||||
|
||||
func init() {
|
||||
if envQPS := os.Getenv(EnvK8sClientQPS); envQPS != "" {
|
||||
if qps, err := strconv.ParseFloat(envQPS, 32); err != nil {
|
||||
K8sClientConfigQPS = float32(qps)
|
||||
}
|
||||
}
|
||||
if envBurst := os.Getenv(EnvK8sClientBurst); envBurst != "" {
|
||||
if burst, err := strconv.Atoi(envBurst); err != nil {
|
||||
K8sClientConfigBurst = burst
|
||||
}
|
||||
} else {
|
||||
K8sClientConfigBurst = 2 * int(K8sClientConfigQPS)
|
||||
}
|
||||
|
||||
if envMaxConn := os.Getenv(EnvK8sClientMaxIdleConnections); envMaxConn != "" {
|
||||
if maxConn, err := strconv.Atoi(envMaxConn); err != nil {
|
||||
K8sMaxIdleConnections = maxConn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
apierr "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
// ArgoCDManagerServiceAccount is the name of the service account for managing a cluster
|
||||
const (
|
||||
ArgoCDManagerServiceAccount = "argocd-manager"
|
||||
ArgoCDManagerClusterRole = "argocd-manager-role"
|
||||
ArgoCDManagerClusterRoleBinding = "argocd-manager-role-binding"
|
||||
)
|
||||
|
||||
// ArgoCDManagerPolicyRules are the policies to give argocd-manager
|
||||
var ArgoCDManagerPolicyRules = []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{"*"},
|
||||
Resources: []string{"*"},
|
||||
Verbs: []string{"*"},
|
||||
},
|
||||
{
|
||||
NonResourceURLs: []string{"*"},
|
||||
Verbs: []string{"*"},
|
||||
},
|
||||
}
|
||||
|
||||
// CreateServiceAccount creates a service account
|
||||
func CreateServiceAccount(
|
||||
clientset kubernetes.Interface,
|
||||
serviceAccountName string,
|
||||
namespace string,
|
||||
) error {
|
||||
serviceAccount := apiv1.ServiceAccount{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
Kind: "ServiceAccount",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: serviceAccountName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
_, err := clientset.CoreV1().ServiceAccounts(namespace).Create(&serviceAccount)
|
||||
if err != nil {
|
||||
if !apierr.IsAlreadyExists(err) {
|
||||
return fmt.Errorf("Failed to create service account %q: %v", serviceAccountName, err)
|
||||
}
|
||||
log.Infof("ServiceAccount %q already exists", serviceAccountName)
|
||||
return nil
|
||||
}
|
||||
log.Infof("ServiceAccount %q created", serviceAccountName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateClusterRole creates a cluster role
|
||||
func CreateClusterRole(
|
||||
clientset kubernetes.Interface,
|
||||
clusterRoleName string,
|
||||
rules []rbacv1.PolicyRule,
|
||||
) error {
|
||||
clusterRole := rbacv1.ClusterRole{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
Kind: "ClusterRole",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: clusterRoleName,
|
||||
},
|
||||
Rules: rules,
|
||||
}
|
||||
crclient := clientset.RbacV1().ClusterRoles()
|
||||
_, err := crclient.Create(&clusterRole)
|
||||
if err != nil {
|
||||
if !apierr.IsAlreadyExists(err) {
|
||||
return fmt.Errorf("Failed to create ClusterRole %q: %v", clusterRoleName, err)
|
||||
}
|
||||
_, err = crclient.Update(&clusterRole)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to update ClusterRole %q: %v", clusterRoleName, err)
|
||||
}
|
||||
log.Infof("ClusterRole %q updated", clusterRoleName)
|
||||
} else {
|
||||
log.Infof("ClusterRole %q created", clusterRoleName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateClusterRoleBinding create a ClusterRoleBinding
|
||||
func CreateClusterRoleBinding(
|
||||
clientset kubernetes.Interface,
|
||||
clusterBindingRoleName,
|
||||
serviceAccountName,
|
||||
clusterRoleName string,
|
||||
namespace string,
|
||||
) error {
|
||||
roleBinding := rbacv1.ClusterRoleBinding{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
Kind: "ClusterRoleBinding",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: clusterBindingRoleName,
|
||||
},
|
||||
RoleRef: rbacv1.RoleRef{
|
||||
APIGroup: "rbac.authorization.k8s.io",
|
||||
Kind: "ClusterRole",
|
||||
Name: clusterRoleName,
|
||||
},
|
||||
Subjects: []rbacv1.Subject{
|
||||
{
|
||||
Kind: rbacv1.ServiceAccountKind,
|
||||
Name: serviceAccountName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
},
|
||||
}
|
||||
_, err := clientset.RbacV1().ClusterRoleBindings().Create(&roleBinding)
|
||||
if err != nil {
|
||||
if !apierr.IsAlreadyExists(err) {
|
||||
return fmt.Errorf("Failed to create ClusterRoleBinding %s: %v", clusterBindingRoleName, err)
|
||||
}
|
||||
log.Infof("ClusterRoleBinding %q already exists", clusterBindingRoleName)
|
||||
return nil
|
||||
}
|
||||
log.Infof("ClusterRoleBinding %q created, bound %q to %q", clusterBindingRoleName, serviceAccountName, clusterRoleName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// InstallClusterManagerRBAC installs RBAC resources for a cluster manager to operate a cluster. Returns a token
|
||||
func InstallClusterManagerRBAC(clientset kubernetes.Interface) (string, error) {
|
||||
const ns = "kube-system"
|
||||
|
||||
err := CreateServiceAccount(clientset, ArgoCDManagerServiceAccount, ns)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = CreateClusterRole(clientset, ArgoCDManagerClusterRole, ArgoCDManagerPolicyRules)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = CreateClusterRoleBinding(clientset, ArgoCDManagerClusterRoleBinding, ArgoCDManagerServiceAccount, ArgoCDManagerClusterRole, ns)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var serviceAccount *apiv1.ServiceAccount
|
||||
var secretName string
|
||||
err = wait.Poll(500*time.Millisecond, 30*time.Second, func() (bool, error) {
|
||||
serviceAccount, err = clientset.CoreV1().ServiceAccounts(ns).Get(ArgoCDManagerServiceAccount, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if len(serviceAccount.Secrets) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
secretName = serviceAccount.Secrets[0].Name
|
||||
return true, nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to wait for service account secret: %v", err)
|
||||
}
|
||||
secret, err := clientset.CoreV1().Secrets(ns).Get(secretName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to retrieve secret %q: %v", secretName, err)
|
||||
}
|
||||
token, ok := secret.Data["token"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("Secret %q for service account %q did not have a token", secretName, serviceAccount)
|
||||
}
|
||||
return string(token), nil
|
||||
}
|
||||
|
||||
// UninstallClusterManagerRBAC removes RBAC resources for a cluster manager to operate a cluster
|
||||
func UninstallClusterManagerRBAC(clientset kubernetes.Interface) error {
|
||||
return UninstallRBAC(clientset, "kube-system", ArgoCDManagerClusterRoleBinding, ArgoCDManagerClusterRole, ArgoCDManagerServiceAccount)
|
||||
}
|
||||
|
||||
// UninstallRBAC uninstalls RBAC related resources for a binding, role, and service account
|
||||
func UninstallRBAC(clientset kubernetes.Interface, namespace, bindingName, roleName, serviceAccount string) error {
|
||||
if err := clientset.RbacV1().ClusterRoleBindings().Delete(bindingName, &metav1.DeleteOptions{}); err != nil {
|
||||
if !apierr.IsNotFound(err) {
|
||||
return fmt.Errorf("Failed to delete ClusterRoleBinding: %v", err)
|
||||
}
|
||||
log.Infof("ClusterRoleBinding %q not found", bindingName)
|
||||
} else {
|
||||
log.Infof("ClusterRoleBinding %q deleted", bindingName)
|
||||
}
|
||||
|
||||
if err := clientset.RbacV1().ClusterRoles().Delete(roleName, &metav1.DeleteOptions{}); err != nil {
|
||||
if !apierr.IsNotFound(err) {
|
||||
return fmt.Errorf("Failed to delete ClusterRole: %v", err)
|
||||
}
|
||||
log.Infof("ClusterRole %q not found", roleName)
|
||||
} else {
|
||||
log.Infof("ClusterRole %q deleted", roleName)
|
||||
}
|
||||
|
||||
if err := clientset.CoreV1().ServiceAccounts(namespace).Delete(serviceAccount, &metav1.DeleteOptions{}); err != nil {
|
||||
if !apierr.IsNotFound(err) {
|
||||
return fmt.Errorf("Failed to delete ServiceAccount: %v", err)
|
||||
}
|
||||
log.Infof("ServiceAccount %q in namespace %q not found", serviceAccount, namespace)
|
||||
} else {
|
||||
log.Infof("ServiceAccount %q deleted", serviceAccount)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package argocd
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -2,6 +2,7 @@ package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -9,29 +10,41 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierr "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/schema"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
kubetesting "k8s.io/client-go/testing"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
mockstatecache "github.com/argoproj/argo-cd/controller/cache/mocks"
|
||||
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/fake"
|
||||
"github.com/argoproj/argo-cd/reposerver/apiclient"
|
||||
mockrepoclient "github.com/argoproj/argo-cd/reposerver/apiclient/mocks"
|
||||
mockreposerver "github.com/argoproj/argo-cd/reposerver/mocks"
|
||||
"github.com/argoproj/argo-cd/reposerver/repository"
|
||||
mockrepoclient "github.com/argoproj/argo-cd/reposerver/repository/mocks"
|
||||
"github.com/argoproj/argo-cd/test"
|
||||
utilcache "github.com/argoproj/argo-cd/util/cache"
|
||||
cacheutil "github.com/argoproj/argo-cd/util/cache"
|
||||
appstatecache "github.com/argoproj/argo-cd/util/cache/appstate"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/kube/kubetest"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
type namespacedResource struct {
|
||||
argoappv1.ResourceNode
|
||||
AppName string
|
||||
}
|
||||
|
||||
type fakeData struct {
|
||||
apps []runtime.Object
|
||||
manifestResponse *repository.ManifestResponse
|
||||
managedLiveObjs map[kube.ResourceKey]*unstructured.Unstructured
|
||||
apps []runtime.Object
|
||||
manifestResponse *apiclient.ManifestResponse
|
||||
managedLiveObjs map[kube.ResourceKey]*unstructured.Unstructured
|
||||
namespacedResources map[kube.ResourceKey]namespacedResource
|
||||
configMapData map[string]string
|
||||
}
|
||||
|
||||
func newFakeController(data *fakeData) *ApplicationController {
|
||||
@@ -61,19 +74,30 @@ func newFakeController(data *fakeData) *ApplicationController {
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "argocd-cm",
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/part-of": "argocd",
|
||||
},
|
||||
},
|
||||
Data: nil,
|
||||
Data: data.configMapData,
|
||||
}
|
||||
kubeClient := fake.NewSimpleClientset(&clust, &cm, &secret)
|
||||
settingsMgr := settings.NewSettingsManager(context.Background(), kubeClient, test.FakeArgoCDNamespace)
|
||||
kubectl := &kubetest.MockKubectlCmd{}
|
||||
ctrl, err := NewApplicationController(
|
||||
test.FakeArgoCDNamespace,
|
||||
settingsMgr,
|
||||
kubeClient,
|
||||
appclientset.NewSimpleClientset(data.apps...),
|
||||
&mockRepoClientset,
|
||||
utilcache.NewCache(utilcache.NewInMemoryCache(1*time.Hour)),
|
||||
appstatecache.NewCache(
|
||||
cacheutil.NewCache(cacheutil.NewInMemoryCache(1*time.Minute)),
|
||||
1*time.Minute,
|
||||
),
|
||||
kubectl,
|
||||
time.Minute,
|
||||
time.Minute,
|
||||
common.DefaultPortArgoCDMetrics,
|
||||
0,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -82,14 +106,26 @@ func newFakeController(data *fakeData) *ApplicationController {
|
||||
defer cancelProj()
|
||||
cancelApp := test.StartInformer(ctrl.appInformer)
|
||||
defer cancelApp()
|
||||
// Mock out call to GetManagedLiveObjs if fake data supplied
|
||||
if data.managedLiveObjs != nil {
|
||||
mockStateCache := mockstatecache.LiveStateCache{}
|
||||
mockStateCache.On("GetManagedLiveObjs", mock.Anything, mock.Anything).Return(data.managedLiveObjs, nil)
|
||||
mockStateCache.On("IsNamespaced", mock.Anything, mock.Anything).Return(true, nil)
|
||||
ctrl.stateCache = &mockStateCache
|
||||
ctrl.appStateManager.(*appStateManager).liveStateCache = &mockStateCache
|
||||
mockStateCache := mockstatecache.LiveStateCache{}
|
||||
ctrl.appStateManager.(*appStateManager).liveStateCache = &mockStateCache
|
||||
ctrl.stateCache = &mockStateCache
|
||||
mockStateCache.On("IsNamespaced", mock.Anything, mock.Anything).Return(true, nil)
|
||||
mockStateCache.On("GetManagedLiveObjs", mock.Anything, mock.Anything).Return(data.managedLiveObjs, nil)
|
||||
mockStateCache.On("GetVersionsInfo", mock.Anything).Return("v1.2.3", nil, nil)
|
||||
response := make(map[kube.ResourceKey]argoappv1.ResourceNode)
|
||||
for k, v := range data.namespacedResources {
|
||||
response[k] = v.ResourceNode
|
||||
}
|
||||
mockStateCache.On("GetNamespaceTopLevelResources", mock.Anything, mock.Anything).Return(response, nil)
|
||||
mockStateCache.On("IterateHierarchy", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
|
||||
key := args[1].(kube.ResourceKey)
|
||||
action := args[2].(func(child argoappv1.ResourceNode, appName string))
|
||||
appName := ""
|
||||
if res, ok := data.namespacedResources[key]; ok {
|
||||
appName = res.AppName
|
||||
}
|
||||
action(argoappv1.ResourceNode{ResourceRef: argoappv1.ResourceRef{Group: key.Group, Namespace: key.Namespace, Name: key.Name}}, appName)
|
||||
}).Return(nil)
|
||||
return ctrl
|
||||
}
|
||||
|
||||
@@ -119,6 +155,7 @@ var fakeApp = `
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
uid: "123"
|
||||
name: my-app
|
||||
namespace: ` + test.FakeArgoCDNamespace + `
|
||||
spec:
|
||||
@@ -155,6 +192,17 @@ status:
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps.git
|
||||
`
|
||||
|
||||
var fakeStrayResource = `
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: test-cm
|
||||
namespace: invalid
|
||||
labels:
|
||||
app.kubernetes.io/instance: my-app
|
||||
data:
|
||||
`
|
||||
|
||||
func newFakeApp() *argoappv1.Application {
|
||||
var app argoappv1.Application
|
||||
err := yaml.Unmarshal([]byte(fakeApp), &app)
|
||||
@@ -164,6 +212,15 @@ func newFakeApp() *argoappv1.Application {
|
||||
return &app
|
||||
}
|
||||
|
||||
func newFakeCM() map[string]interface{} {
|
||||
var cm map[string]interface{}
|
||||
err := yaml.Unmarshal([]byte(fakeStrayResource), &cm)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return cm
|
||||
}
|
||||
|
||||
func TestAutoSync(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
@@ -171,7 +228,7 @@ func TestAutoSync(t *testing.T) {
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus)
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: argoappv1.SyncStatusCodeOutOfSync}})
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
@@ -183,37 +240,37 @@ func TestAutoSync(t *testing.T) {
|
||||
func TestSkipAutoSync(t *testing.T) {
|
||||
// Verify we skip when we previously synced to it in our most recent history
|
||||
// Set current to 'aaaaa', desired to 'aaaa' and mark system OutOfSync
|
||||
{
|
||||
t.Run("PreviouslySyncedToRevision", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus := argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus)
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{})
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
}
|
||||
})
|
||||
|
||||
// Verify we skip when we are already Synced (even if revision is different)
|
||||
{
|
||||
t.Run("AlreadyInSyncedState", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus := argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeSynced,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus)
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{})
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
}
|
||||
})
|
||||
|
||||
// Verify we skip when auto-sync is disabled
|
||||
{
|
||||
t.Run("AutoSyncIsDisabled", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Spec.SyncPolicy = nil
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
@@ -221,15 +278,15 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus)
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{})
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
}
|
||||
})
|
||||
|
||||
// Verify we skip when application is marked for deletion
|
||||
{
|
||||
t.Run("ApplicationIsMarkedForDeletion", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
now := metav1.Now()
|
||||
app.DeletionTimestamp = &now
|
||||
@@ -238,16 +295,16 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus)
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{})
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
}
|
||||
})
|
||||
|
||||
// Verify we skip when previous sync attempt failed and return error condition
|
||||
// Set current to 'aaaaa', desired to 'bbbbb' and add 'bbbbb' to failure history
|
||||
{
|
||||
t.Run("PreviousSyncAttemptFailed", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Status.OperationState = &argoappv1.OperationState{
|
||||
Operation: argoappv1.Operation{
|
||||
@@ -264,12 +321,28 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus)
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: argoappv1.SyncStatusCodeOutOfSync}})
|
||||
assert.NotNil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NeedsToPruneResourcesOnlyButAutomatedPruneDisabled", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus := argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{
|
||||
{Name: "guestbook", Kind: kube.DeploymentKind, Status: argoappv1.SyncStatusCodeOutOfSync, RequiresPruning: true},
|
||||
})
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAutoSyncIndicateError verifies we skip auto-sync and return error condition if previous sync failed
|
||||
@@ -300,7 +373,7 @@ func TestAutoSyncIndicateError(t *testing.T) {
|
||||
Source: *app.Spec.Source.DeepCopy(),
|
||||
},
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus)
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: argoappv1.SyncStatusCodeOutOfSync}})
|
||||
assert.NotNil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
@@ -343,7 +416,7 @@ func TestAutoSyncParameterOverrides(t *testing.T) {
|
||||
Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
},
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus)
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: argoappv1.SyncStatusCodeOutOfSync}})
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
@@ -352,21 +425,118 @@ func TestAutoSyncParameterOverrides(t *testing.T) {
|
||||
|
||||
// TestFinalizeAppDeletion verifies application deletion
|
||||
func TestFinalizeAppDeletion(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
// Ensure app can be deleted cascading
|
||||
{
|
||||
defaultProj := argoappv1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
},
|
||||
Spec: argoappv1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []argoappv1.ApplicationDestination{
|
||||
{
|
||||
Server: "*",
|
||||
Namespace: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
app := newFakeApp()
|
||||
app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
|
||||
appObj := kube.MustToUnstructured(&app)
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}, managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
||||
kube.GetResourceKey(appObj): appObj,
|
||||
}})
|
||||
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
patched := false
|
||||
fakeAppCs.ReactionChain = nil
|
||||
fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
patched = true
|
||||
return true, nil, nil
|
||||
})
|
||||
err := ctrl.finalizeApplicationDeletion(app)
|
||||
// TODO: use an interface to fake out the calls to GetResourcesWithLabel and DeleteResourceWithLabel
|
||||
// For now just ensure we have an expected error condition
|
||||
assert.Error(t, err) // Change this to assert.Nil when we stub out GetResourcesWithLabel/DeleteResourceWithLabel
|
||||
assert.False(t, patched) // Change this to assert.True when we stub out GetResourcesWithLabel/DeleteResourceWithLabel
|
||||
patched := false
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
defaultReactor := fakeAppCs.ReactionChain[0]
|
||||
fakeAppCs.ReactionChain = nil
|
||||
fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
return defaultReactor.React(action)
|
||||
})
|
||||
fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
patched = true
|
||||
return true, nil, nil
|
||||
})
|
||||
_, err := ctrl.finalizeApplicationDeletion(app)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, patched)
|
||||
}
|
||||
|
||||
// Ensure any stray resources irregulary labeled with instance label of app are not deleted upon deleting,
|
||||
// when app project restriction is in place
|
||||
{
|
||||
defaultProj := argoappv1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
},
|
||||
Spec: argoappv1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []argoappv1.ApplicationDestination{
|
||||
{
|
||||
Server: "*",
|
||||
Namespace: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
restrictedProj := argoappv1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "restricted",
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
},
|
||||
Spec: argoappv1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []argoappv1.ApplicationDestination{
|
||||
{
|
||||
Server: "*",
|
||||
Namespace: "my-app",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
app := newFakeApp()
|
||||
app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
|
||||
app.Spec.Project = "restricted"
|
||||
appObj := kube.MustToUnstructured(&app)
|
||||
cm := newFakeCM()
|
||||
strayObj := kube.MustToUnstructured(&cm)
|
||||
ctrl := newFakeController(&fakeData{
|
||||
apps: []runtime.Object{app, &defaultProj, &restrictedProj},
|
||||
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
||||
kube.GetResourceKey(appObj): appObj,
|
||||
kube.GetResourceKey(strayObj): strayObj,
|
||||
},
|
||||
})
|
||||
|
||||
patched := false
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
defaultReactor := fakeAppCs.ReactionChain[0]
|
||||
fakeAppCs.ReactionChain = nil
|
||||
fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
return defaultReactor.React(action)
|
||||
})
|
||||
fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
patched = true
|
||||
return true, nil, nil
|
||||
})
|
||||
objs, err := ctrl.finalizeApplicationDeletion(app)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, patched)
|
||||
objsMap, err := ctrl.stateCache.GetManagedLiveObjs(app, []*unstructured.Unstructured{})
|
||||
if err != nil {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
// Managed objects must be empty
|
||||
assert.Empty(t, objsMap)
|
||||
// Loop through all deleted objects, ensure that test-cm is none of them
|
||||
for _, o := range objs {
|
||||
assert.NotEqual(t, "test-cm", o.GetName())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNormalizeApplication verifies we normalize an application during reconciliation
|
||||
@@ -391,7 +561,7 @@ func TestNormalizeApplication(t *testing.T) {
|
||||
app.Spec.Source.Kustomize = &argoappv1.ApplicationSourceKustomize{NamePrefix: "foo-"}
|
||||
data := fakeData{
|
||||
apps: []runtime.Object{app, &defaultProj},
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
@@ -442,3 +612,253 @@ func TestNormalizeApplication(t *testing.T) {
|
||||
assert.False(t, normalized)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleAppUpdated(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
|
||||
app.Spec.Destination.Server = common.KubernetesInternalAPIServerAddr
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
|
||||
ctrl.handleObjectUpdated(map[string]bool{app.Name: true}, kube.GetObjectRef(kube.MustToUnstructured(app)))
|
||||
isRequested, level := ctrl.isRefreshRequested(app.Name)
|
||||
assert.False(t, isRequested)
|
||||
assert.Equal(t, ComparisonWithNothing, level)
|
||||
|
||||
ctrl.handleObjectUpdated(map[string]bool{app.Name: true}, corev1.ObjectReference{UID: "test", Kind: kube.DeploymentKind, Name: "test", Namespace: "default"})
|
||||
isRequested, level = ctrl.isRefreshRequested(app.Name)
|
||||
assert.True(t, isRequested)
|
||||
assert.Equal(t, CompareWithRecent, level)
|
||||
}
|
||||
|
||||
func TestHandleOrphanedResourceUpdated(t *testing.T) {
|
||||
app1 := newFakeApp()
|
||||
app1.Name = "app1"
|
||||
app1.Spec.Destination.Namespace = test.FakeArgoCDNamespace
|
||||
app1.Spec.Destination.Server = common.KubernetesInternalAPIServerAddr
|
||||
|
||||
app2 := newFakeApp()
|
||||
app2.Name = "app2"
|
||||
app2.Spec.Destination.Namespace = test.FakeArgoCDNamespace
|
||||
app2.Spec.Destination.Server = common.KubernetesInternalAPIServerAddr
|
||||
|
||||
proj := defaultProj.DeepCopy()
|
||||
proj.Spec.OrphanedResources = &argoappv1.OrphanedResourcesMonitorSettings{}
|
||||
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app1, app2, proj}})
|
||||
|
||||
ctrl.handleObjectUpdated(map[string]bool{}, corev1.ObjectReference{UID: "test", Kind: kube.DeploymentKind, Name: "test", Namespace: test.FakeArgoCDNamespace})
|
||||
|
||||
isRequested, level := ctrl.isRefreshRequested(app1.Name)
|
||||
assert.True(t, isRequested)
|
||||
assert.Equal(t, ComparisonWithNothing, level)
|
||||
|
||||
isRequested, level = ctrl.isRefreshRequested(app2.Name)
|
||||
assert.True(t, isRequested)
|
||||
assert.Equal(t, ComparisonWithNothing, level)
|
||||
}
|
||||
|
||||
func TestSetOperationStateOnDeletedApp(t *testing.T) {
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{}})
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
fakeAppCs.ReactionChain = nil
|
||||
patched := false
|
||||
fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
patched = true
|
||||
return true, nil, apierr.NewNotFound(schema.GroupResource{}, "my-app")
|
||||
})
|
||||
ctrl.setOperationState(newFakeApp(), &argoappv1.OperationState{Phase: argoappv1.OperationSucceeded})
|
||||
assert.True(t, patched)
|
||||
}
|
||||
|
||||
func TestNeedRefreshAppStatus(t *testing.T) {
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{}})
|
||||
|
||||
app := newFakeApp()
|
||||
now := metav1.Now()
|
||||
app.Status.ReconciledAt = &now
|
||||
app.Status.Sync = argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeSynced,
|
||||
ComparedTo: argoappv1.ComparedTo{
|
||||
Source: app.Spec.Source,
|
||||
Destination: app.Spec.Destination,
|
||||
},
|
||||
}
|
||||
|
||||
// no need to refresh just reconciled application
|
||||
needRefresh, _, _ := ctrl.needRefreshAppStatus(app, 1*time.Hour)
|
||||
assert.False(t, needRefresh)
|
||||
|
||||
// refresh app using the 'deepest' requested comparison level
|
||||
ctrl.requestAppRefresh(app.Name, CompareWithRecent.Pointer(), nil)
|
||||
ctrl.requestAppRefresh(app.Name, ComparisonWithNothing.Pointer(), nil)
|
||||
|
||||
needRefresh, refreshType, compareWith := ctrl.needRefreshAppStatus(app, 1*time.Hour)
|
||||
assert.True(t, needRefresh)
|
||||
assert.Equal(t, argoappv1.RefreshTypeNormal, refreshType)
|
||||
assert.Equal(t, CompareWithRecent, compareWith)
|
||||
|
||||
// refresh application which status is not reconciled using latest commit
|
||||
app.Status.Sync = argoappv1.SyncStatus{Status: argoappv1.SyncStatusCodeUnknown}
|
||||
|
||||
needRefresh, refreshType, compareWith = ctrl.needRefreshAppStatus(app, 1*time.Hour)
|
||||
assert.True(t, needRefresh)
|
||||
assert.Equal(t, argoappv1.RefreshTypeNormal, refreshType)
|
||||
assert.Equal(t, CompareWithLatest, compareWith)
|
||||
|
||||
{
|
||||
// refresh app using the 'latest' level if comparison expired
|
||||
app := app.DeepCopy()
|
||||
ctrl.requestAppRefresh(app.Name, CompareWithRecent.Pointer(), nil)
|
||||
reconciledAt := metav1.NewTime(time.Now().UTC().Add(-1 * time.Hour))
|
||||
app.Status.ReconciledAt = &reconciledAt
|
||||
needRefresh, refreshType, compareWith = ctrl.needRefreshAppStatus(app, 1*time.Minute)
|
||||
assert.True(t, needRefresh)
|
||||
assert.Equal(t, argoappv1.RefreshTypeNormal, refreshType)
|
||||
assert.Equal(t, CompareWithLatest, compareWith)
|
||||
}
|
||||
|
||||
{
|
||||
app := app.DeepCopy()
|
||||
// execute hard refresh if app has refresh annotation
|
||||
reconciledAt := metav1.NewTime(time.Now().UTC().Add(-1 * time.Hour))
|
||||
app.Status.ReconciledAt = &reconciledAt
|
||||
app.Annotations = map[string]string{
|
||||
common.AnnotationKeyRefresh: string(argoappv1.RefreshTypeHard),
|
||||
}
|
||||
needRefresh, refreshType, compareWith = ctrl.needRefreshAppStatus(app, 1*time.Hour)
|
||||
assert.True(t, needRefresh)
|
||||
assert.Equal(t, argoappv1.RefreshTypeHard, refreshType)
|
||||
assert.Equal(t, CompareWithLatest, compareWith)
|
||||
}
|
||||
|
||||
{
|
||||
app := app.DeepCopy()
|
||||
// ensure that CompareWithLatest level is used if application source has changed
|
||||
ctrl.requestAppRefresh(app.Name, ComparisonWithNothing.Pointer(), nil)
|
||||
// sample app source change
|
||||
app.Spec.Source.Helm = &argoappv1.ApplicationSourceHelm{
|
||||
Parameters: []argoappv1.HelmParameter{{
|
||||
Name: "foo",
|
||||
Value: "bar",
|
||||
}},
|
||||
}
|
||||
|
||||
needRefresh, refreshType, compareWith = ctrl.needRefreshAppStatus(app, 1*time.Hour)
|
||||
assert.True(t, needRefresh)
|
||||
assert.Equal(t, argoappv1.RefreshTypeNormal, refreshType)
|
||||
assert.Equal(t, CompareWithLatest, compareWith)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshAppConditions(t *testing.T) {
|
||||
defaultProj := argoappv1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
},
|
||||
Spec: argoappv1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []argoappv1.ApplicationDestination{
|
||||
{
|
||||
Server: "*",
|
||||
Namespace: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("NoErrorConditions", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}})
|
||||
|
||||
_, hasErrors := ctrl.refreshAppConditions(app)
|
||||
assert.False(t, hasErrors)
|
||||
assert.Len(t, app.Status.Conditions, 0)
|
||||
})
|
||||
|
||||
t.Run("PreserveExistingWarningCondition", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Status.SetConditions([]argoappv1.ApplicationCondition{{Type: argoappv1.ApplicationConditionExcludedResourceWarning}}, nil)
|
||||
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}})
|
||||
|
||||
_, hasErrors := ctrl.refreshAppConditions(app)
|
||||
assert.False(t, hasErrors)
|
||||
assert.Len(t, app.Status.Conditions, 1)
|
||||
assert.Equal(t, argoappv1.ApplicationConditionExcludedResourceWarning, app.Status.Conditions[0].Type)
|
||||
})
|
||||
|
||||
t.Run("ReplacesSpecErrorCondition", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Spec.Project = "wrong project"
|
||||
app.Status.SetConditions([]argoappv1.ApplicationCondition{{Type: argoappv1.ApplicationConditionInvalidSpecError, Message: "old message"}}, nil)
|
||||
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}})
|
||||
|
||||
_, hasErrors := ctrl.refreshAppConditions(app)
|
||||
assert.True(t, hasErrors)
|
||||
assert.Len(t, app.Status.Conditions, 1)
|
||||
assert.Equal(t, argoappv1.ApplicationConditionInvalidSpecError, app.Status.Conditions[0].Type)
|
||||
assert.Equal(t, "Application referencing project wrong project which does not exist", app.Status.Conditions[0].Message)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateReconciledAt(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
reconciledAt := metav1.NewTime(time.Now().Add(-1 * time.Second))
|
||||
app.Status = argoappv1.ApplicationStatus{ReconciledAt: &reconciledAt}
|
||||
app.Status.Sync = argoappv1.SyncStatus{ComparedTo: argoappv1.ComparedTo{Source: app.Spec.Source, Destination: app.Spec.Destination}}
|
||||
ctrl := newFakeController(&fakeData{
|
||||
apps: []runtime.Object{app, &defaultProj},
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
})
|
||||
key, _ := cache.MetaNamespaceKeyFunc(app)
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
fakeAppCs.ReactionChain = nil
|
||||
receivedPatch := map[string]interface{}{}
|
||||
fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
if patchAction, ok := action.(kubetesting.PatchAction); ok {
|
||||
assert.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch))
|
||||
}
|
||||
return true, nil, nil
|
||||
})
|
||||
|
||||
t.Run("UpdatedOnFullReconciliation", func(t *testing.T) {
|
||||
receivedPatch = map[string]interface{}{}
|
||||
ctrl.requestAppRefresh(app.Name, CompareWithLatest.Pointer(), nil)
|
||||
ctrl.appRefreshQueue.Add(key)
|
||||
|
||||
ctrl.processAppRefreshQueueItem()
|
||||
|
||||
_, updated, err := unstructured.NestedString(receivedPatch, "status", "reconciledAt")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, updated)
|
||||
|
||||
_, updated, err = unstructured.NestedString(receivedPatch, "status", "observedAt")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, updated)
|
||||
})
|
||||
|
||||
t.Run("NotUpdatedOnPartialReconciliation", func(t *testing.T) {
|
||||
receivedPatch = map[string]interface{}{}
|
||||
ctrl.appRefreshQueue.Add(key)
|
||||
ctrl.requestAppRefresh(app.Name, CompareWithRecent.Pointer(), nil)
|
||||
|
||||
ctrl.processAppRefreshQueueItem()
|
||||
|
||||
_, updated, err := unstructured.NestedString(receivedPatch, "status", "reconciledAt")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, updated)
|
||||
|
||||
_, updated, err = unstructured.NestedString(receivedPatch, "status", "observedAt")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, updated)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
254
controller/cache/cache.go
vendored
@@ -2,14 +2,18 @@ package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
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/schema"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"github.com/argoproj/argo-cd/controller/metrics"
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
@@ -17,20 +21,33 @@ import (
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
type cacheSettings struct {
|
||||
ResourceOverrides map[string]appv1.ResourceOverride
|
||||
AppInstanceLabelKey string
|
||||
ResourcesFilter *settings.ResourcesFilter
|
||||
}
|
||||
|
||||
type LiveStateCache interface {
|
||||
IsNamespaced(server string, obj *unstructured.Unstructured) (bool, error)
|
||||
// Returns k8s server version
|
||||
GetVersionsInfo(serverURL string) (string, []metav1.APIGroup, error)
|
||||
// Returns true of given group kind is a namespaced resource
|
||||
IsNamespaced(server string, gk schema.GroupKind) (bool, error)
|
||||
// Executes give callback against resource specified by the key and all its children
|
||||
IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode)) error
|
||||
IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string)) error
|
||||
// Returns state of live nodes which correspond for target nodes of specified application.
|
||||
GetManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error)
|
||||
// Returns all top level resources (resources without owner references) of a specified namespace
|
||||
GetNamespaceTopLevelResources(server string, namespace string) (map[kube.ResourceKey]appv1.ResourceNode, error)
|
||||
// Starts watching resources of each controlled cluster.
|
||||
Run(ctx context.Context)
|
||||
// Deletes specified resource from cluster.
|
||||
Delete(server string, obj *unstructured.Unstructured) error
|
||||
Run(ctx context.Context) error
|
||||
// Invalidate invalidates the entire cluster state cache
|
||||
Invalidate()
|
||||
// Returns information about monitored clusters
|
||||
GetClustersInfo() []metrics.ClusterInfo
|
||||
}
|
||||
|
||||
type ObjectUpdatedHandler = func(managedByApp map[string]bool, ref v1.ObjectReference)
|
||||
|
||||
func GetTargetObjKey(a *appv1.Application, un *unstructured.Unstructured, isNamespaced bool) kube.ResourceKey {
|
||||
key := kube.GetResourceKey(un)
|
||||
if !isNamespaced {
|
||||
@@ -42,53 +59,98 @@ func GetTargetObjKey(a *appv1.Application, un *unstructured.Unstructured, isName
|
||||
return key
|
||||
}
|
||||
|
||||
func NewLiveStateCache(db db.ArgoDB, appInformer cache.SharedIndexInformer, settings *settings.ArgoCDSettings, kubectl kube.Kubectl, onAppUpdated func(appName string, fullRefresh bool)) LiveStateCache {
|
||||
func NewLiveStateCache(
|
||||
db db.ArgoDB,
|
||||
appInformer cache.SharedIndexInformer,
|
||||
settingsMgr *settings.SettingsManager,
|
||||
kubectl kube.Kubectl,
|
||||
metricsServer *metrics.MetricsServer,
|
||||
onObjectUpdated ObjectUpdatedHandler) LiveStateCache {
|
||||
|
||||
return &liveStateCache{
|
||||
appInformer: appInformer,
|
||||
db: db,
|
||||
clusters: make(map[string]*clusterInfo),
|
||||
lock: &sync.Mutex{},
|
||||
onAppUpdated: onAppUpdated,
|
||||
kubectl: kubectl,
|
||||
settings: settings,
|
||||
appInformer: appInformer,
|
||||
db: db,
|
||||
clusters: make(map[string]*clusterInfo),
|
||||
lock: &sync.RWMutex{},
|
||||
onObjectUpdated: onObjectUpdated,
|
||||
kubectl: kubectl,
|
||||
settingsMgr: settingsMgr,
|
||||
metricsServer: metricsServer,
|
||||
cacheSettingsLock: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
type liveStateCache struct {
|
||||
db db.ArgoDB
|
||||
clusters map[string]*clusterInfo
|
||||
lock *sync.Mutex
|
||||
appInformer cache.SharedIndexInformer
|
||||
onAppUpdated func(appName string, fullRefresh bool)
|
||||
kubectl kube.Kubectl
|
||||
settings *settings.ArgoCDSettings
|
||||
db db.ArgoDB
|
||||
clusters map[string]*clusterInfo
|
||||
lock *sync.RWMutex
|
||||
appInformer cache.SharedIndexInformer
|
||||
onObjectUpdated ObjectUpdatedHandler
|
||||
kubectl kube.Kubectl
|
||||
settingsMgr *settings.SettingsManager
|
||||
metricsServer *metrics.MetricsServer
|
||||
cacheSettingsLock *sync.Mutex
|
||||
cacheSettings *cacheSettings
|
||||
}
|
||||
|
||||
func (c *liveStateCache) loadCacheSettings() (*cacheSettings, error) {
|
||||
appInstanceLabelKey, err := c.settingsMgr.GetAppInstanceLabelKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resourcesFilter, err := c.settingsMgr.GetResourcesFilter()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resourceOverrides, err := c.settingsMgr.GetResourceOverrides()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cacheSettings{AppInstanceLabelKey: appInstanceLabelKey, ResourceOverrides: resourceOverrides, ResourcesFilter: resourcesFilter}, nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) getCluster(server string) (*clusterInfo, error) {
|
||||
c.lock.RLock()
|
||||
info, ok := c.clusters[server]
|
||||
c.lock.RUnlock()
|
||||
|
||||
if ok {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
info, ok := c.clusters[server]
|
||||
if !ok {
|
||||
cluster, err := c.db.GetCluster(context.Background(), server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info = &clusterInfo{
|
||||
apisMeta: make(map[schema.GroupKind]*apiMeta),
|
||||
lock: &sync.Mutex{},
|
||||
nodes: make(map[kube.ResourceKey]*node),
|
||||
nsIndex: make(map[string]map[kube.ResourceKey]*node),
|
||||
onAppUpdated: c.onAppUpdated,
|
||||
kubectl: c.kubectl,
|
||||
cluster: cluster,
|
||||
syncTime: nil,
|
||||
syncLock: &sync.Mutex{},
|
||||
log: log.WithField("server", cluster.Server),
|
||||
settings: c.settings,
|
||||
}
|
||||
|
||||
c.clusters[cluster.Server] = info
|
||||
info, ok = c.clusters[server]
|
||||
if ok {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
logCtx := log.WithField("server", server)
|
||||
logCtx.Info("initializing cluster")
|
||||
cluster, err := c.db.GetCluster(context.Background(), server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info = &clusterInfo{
|
||||
apisMeta: make(map[schema.GroupKind]*apiMeta),
|
||||
lock: &sync.RWMutex{},
|
||||
nodes: make(map[kube.ResourceKey]*node),
|
||||
nsIndex: make(map[string]map[kube.ResourceKey]*node),
|
||||
onObjectUpdated: c.onObjectUpdated,
|
||||
kubectl: c.kubectl,
|
||||
cluster: cluster,
|
||||
syncTime: nil,
|
||||
log: logCtx,
|
||||
cacheSettingsSrc: c.getCacheSettings,
|
||||
onEventReceived: func(event watch.EventType, un *unstructured.Unstructured) {
|
||||
gvk := un.GroupVersionKind()
|
||||
c.metricsServer.IncClusterEventsCount(cluster.Server, gvk.Group, gvk.Kind)
|
||||
},
|
||||
metricsServer: c.metricsServer,
|
||||
}
|
||||
c.clusters[cluster.Server] = info
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
@@ -106,33 +168,23 @@ func (c *liveStateCache) getSyncedCluster(server string) (*clusterInfo, error) {
|
||||
|
||||
func (c *liveStateCache) Invalidate() {
|
||||
log.Info("invalidating live state cache")
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.lock.RLock()
|
||||
defer c.lock.RLock()
|
||||
for _, clust := range c.clusters {
|
||||
clust.lock.Lock()
|
||||
clust.invalidate()
|
||||
clust.lock.Unlock()
|
||||
}
|
||||
log.Info("live state cache invalidated")
|
||||
}
|
||||
|
||||
func (c *liveStateCache) Delete(server string, obj *unstructured.Unstructured) error {
|
||||
clusterInfo, err := c.getSyncedCluster(server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return clusterInfo.delete(obj)
|
||||
}
|
||||
|
||||
func (c *liveStateCache) IsNamespaced(server string, obj *unstructured.Unstructured) (bool, error) {
|
||||
func (c *liveStateCache) IsNamespaced(server string, gk schema.GroupKind) (bool, error) {
|
||||
clusterInfo, err := c.getSyncedCluster(server)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return clusterInfo.isNamespaced(obj), nil
|
||||
return clusterInfo.isNamespaced(gk), nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode)) error {
|
||||
func (c *liveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string)) error {
|
||||
clusterInfo, err := c.getSyncedCluster(server)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -141,6 +193,14 @@ func (c *liveStateCache) IterateHierarchy(server string, key kube.ResourceKey, a
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) GetNamespaceTopLevelResources(server string, namespace string) (map[kube.ResourceKey]appv1.ResourceNode, error) {
|
||||
clusterInfo, err := c.getSyncedCluster(server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return clusterInfo.getNamespaceTopLevelResources(namespace), nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) GetManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
|
||||
clusterInfo, err := c.getSyncedCluster(a.Spec.Destination.Server)
|
||||
if err != nil {
|
||||
@@ -149,6 +209,14 @@ func (c *liveStateCache) GetManagedLiveObjs(a *appv1.Application, targetObjs []*
|
||||
return clusterInfo.getManagedLiveObjs(a, targetObjs)
|
||||
}
|
||||
|
||||
func (c *liveStateCache) GetVersionsInfo(serverURL string) (string, []metav1.APIGroup, error) {
|
||||
clusterInfo, err := c.getSyncedCluster(serverURL)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return clusterInfo.serverVersion, clusterInfo.apiGroups, nil
|
||||
}
|
||||
|
||||
func isClusterHasApps(apps []interface{}, cluster *appv1.Cluster) bool {
|
||||
for _, obj := range apps {
|
||||
if app, ok := obj.(*appv1.Application); ok && app.Spec.Destination.Server == cluster.Server {
|
||||
@@ -158,13 +226,59 @@ func isClusterHasApps(apps []interface{}, cluster *appv1.Cluster) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *liveStateCache) getCacheSettings() *cacheSettings {
|
||||
return c.cacheSettings
|
||||
}
|
||||
|
||||
func (c *liveStateCache) watchSettings(ctx context.Context) {
|
||||
updateCh := make(chan *settings.ArgoCDSettings, 1)
|
||||
c.settingsMgr.Subscribe(updateCh)
|
||||
|
||||
done := false
|
||||
for !done {
|
||||
select {
|
||||
case <-updateCh:
|
||||
nextCacheSettings, err := c.loadCacheSettings()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to read updated settings: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
c.cacheSettingsLock.Lock()
|
||||
needInvalidate := false
|
||||
if !reflect.DeepEqual(c.cacheSettings, nextCacheSettings) {
|
||||
c.cacheSettings = nextCacheSettings
|
||||
needInvalidate = true
|
||||
}
|
||||
c.cacheSettingsLock.Unlock()
|
||||
if needInvalidate {
|
||||
c.Invalidate()
|
||||
}
|
||||
case <-ctx.Done():
|
||||
done = true
|
||||
}
|
||||
}
|
||||
log.Info("shutting down settings watch")
|
||||
c.settingsMgr.Unsubscribe(updateCh)
|
||||
close(updateCh)
|
||||
}
|
||||
|
||||
// Run watches for resource changes annotated with application label on all registered clusters and schedule corresponding app refresh.
|
||||
func (c *liveStateCache) Run(ctx context.Context) {
|
||||
func (c *liveStateCache) Run(ctx context.Context) error {
|
||||
cacheSettings, err := c.loadCacheSettings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.cacheSettings = cacheSettings
|
||||
|
||||
go c.watchSettings(ctx)
|
||||
|
||||
util.RetryUntilSucceed(func() error {
|
||||
clusterEventCallback := func(event *db.ClusterEvent) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if cluster, ok := c.clusters[event.Cluster.Server]; ok {
|
||||
cluster, ok := c.clusters[event.Cluster.Server]
|
||||
if ok {
|
||||
defer c.lock.Unlock()
|
||||
if event.Type == watch.Deleted {
|
||||
cluster.invalidate()
|
||||
delete(c.clusters, event.Cluster.Server)
|
||||
@@ -172,11 +286,14 @@ func (c *liveStateCache) Run(ctx context.Context) {
|
||||
cluster.cluster = event.Cluster
|
||||
cluster.invalidate()
|
||||
}
|
||||
} else if event.Type == watch.Added && isClusterHasApps(c.appInformer.GetStore().List(), event.Cluster) {
|
||||
go func() {
|
||||
// warm up cache for cluster with apps
|
||||
_, _ = c.getSyncedCluster(event.Cluster.Server)
|
||||
}()
|
||||
} else {
|
||||
c.lock.Unlock()
|
||||
if event.Type == watch.Added && isClusterHasApps(c.appInformer.GetStore().List(), event.Cluster) {
|
||||
go func() {
|
||||
// warm up cache for cluster with apps
|
||||
_, _ = c.getSyncedCluster(event.Cluster.Server)
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,4 +302,15 @@ func (c *liveStateCache) Run(ctx context.Context) {
|
||||
}, "watch clusters", ctx, clusterRetryTimeout)
|
||||
|
||||
<-ctx.Done()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) GetClustersInfo() []metrics.ClusterInfo {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
res := make([]metrics.ClusterInfo, 0)
|
||||
for _, info := range c.clusters {
|
||||
res = append(res, info.getClusterInfo())
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
26
controller/cache/cache_test.go
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetServerVersion(t *testing.T) {
|
||||
now := time.Now()
|
||||
cache := &liveStateCache{
|
||||
lock: &sync.RWMutex{},
|
||||
clusters: map[string]*clusterInfo{
|
||||
"http://localhost": {
|
||||
syncTime: &now,
|
||||
lock: &sync.RWMutex{},
|
||||
serverVersion: "123",
|
||||
},
|
||||
}}
|
||||
|
||||
version, _, err := cache.GetVersionsInfo("http://localhost")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "123", version)
|
||||
}
|
||||
435
controller/cache/cluster.go
vendored
@@ -4,22 +4,28 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"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/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/dynamic"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/tools/pager"
|
||||
|
||||
"github.com/argoproj/argo-cd/controller/metrics"
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/health"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -35,45 +41,51 @@ type apiMeta struct {
|
||||
}
|
||||
|
||||
type clusterInfo struct {
|
||||
syncLock *sync.Mutex
|
||||
syncTime *time.Time
|
||||
syncError error
|
||||
apisMeta map[schema.GroupKind]*apiMeta
|
||||
syncTime *time.Time
|
||||
syncError error
|
||||
apisMeta map[schema.GroupKind]*apiMeta
|
||||
serverVersion string
|
||||
apiGroups []metav1.APIGroup
|
||||
// namespacedResources is a simple map which indicates a groupKind is namespaced
|
||||
namespacedResources map[schema.GroupKind]bool
|
||||
|
||||
lock *sync.Mutex
|
||||
// lock is a rw lock which protects the fields of clusterInfo
|
||||
lock *sync.RWMutex
|
||||
nodes map[kube.ResourceKey]*node
|
||||
nsIndex map[string]map[kube.ResourceKey]*node
|
||||
|
||||
onAppUpdated func(appName string, fullRefresh bool)
|
||||
kubectl kube.Kubectl
|
||||
cluster *appv1.Cluster
|
||||
log *log.Entry
|
||||
settings *settings.ArgoCDSettings
|
||||
onObjectUpdated ObjectUpdatedHandler
|
||||
onEventReceived func(event watch.EventType, un *unstructured.Unstructured)
|
||||
kubectl kube.Kubectl
|
||||
cluster *appv1.Cluster
|
||||
log *log.Entry
|
||||
cacheSettingsSrc func() *cacheSettings
|
||||
metricsServer *metrics.MetricsServer
|
||||
}
|
||||
|
||||
func (c *clusterInfo) replaceResourceCache(gk schema.GroupKind, resourceVersion string, objs []unstructured.Unstructured) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
func (c *clusterInfo) replaceResourceCache(gk schema.GroupKind, resourceVersion string, objs []unstructured.Unstructured, ns string) {
|
||||
info, ok := c.apisMeta[gk]
|
||||
if ok {
|
||||
objByKind := make(map[kube.ResourceKey]*unstructured.Unstructured)
|
||||
objByKey := make(map[kube.ResourceKey]*unstructured.Unstructured)
|
||||
for i := range objs {
|
||||
objByKind[kube.GetResourceKey(&objs[i])] = &objs[i]
|
||||
objByKey[kube.GetResourceKey(&objs[i])] = &objs[i]
|
||||
}
|
||||
|
||||
// update existing nodes
|
||||
for i := range objs {
|
||||
obj := &objs[i]
|
||||
key := kube.GetResourceKey(&objs[i])
|
||||
existingNode, exists := c.nodes[key]
|
||||
c.onNodeUpdated(exists, existingNode, obj, key)
|
||||
c.onNodeUpdated(exists, existingNode, obj)
|
||||
}
|
||||
|
||||
// remove existing nodes that a no longer exist
|
||||
for key, existingNode := range c.nodes {
|
||||
if key.Kind != gk.Kind || key.Group != gk.Group {
|
||||
if key.Kind != gk.Kind || key.Group != gk.Group || ns != "" && key.Namespace != ns {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := objByKind[key]; !ok {
|
||||
if _, ok := objByKey[key]; !ok {
|
||||
c.onNodeRemoved(key, existingNode)
|
||||
}
|
||||
}
|
||||
@@ -81,33 +93,82 @@ func (c *clusterInfo) replaceResourceCache(gk schema.GroupKind, resourceVersion
|
||||
}
|
||||
}
|
||||
|
||||
func isServiceAccountTokenSecret(un *unstructured.Unstructured) (bool, metav1.OwnerReference) {
|
||||
ref := metav1.OwnerReference{
|
||||
APIVersion: "v1",
|
||||
Kind: kube.ServiceAccountKind,
|
||||
}
|
||||
if un.GetKind() != kube.SecretKind || un.GroupVersionKind().Group != "" {
|
||||
return false, ref
|
||||
}
|
||||
|
||||
if typeVal, ok, err := unstructured.NestedString(un.Object, "type"); !ok || err != nil || typeVal != "kubernetes.io/service-account-token" {
|
||||
return false, ref
|
||||
}
|
||||
|
||||
annotations := un.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return false, ref
|
||||
}
|
||||
|
||||
id, okId := annotations["kubernetes.io/service-account.uid"]
|
||||
name, okName := annotations["kubernetes.io/service-account.name"]
|
||||
if okId && okName {
|
||||
ref.Name = name
|
||||
ref.UID = types.UID(id)
|
||||
}
|
||||
return ref.Name != "" && ref.UID != "", ref
|
||||
}
|
||||
|
||||
func (c *clusterInfo) createObjInfo(un *unstructured.Unstructured, appInstanceLabel string) *node {
|
||||
ownerRefs := un.GetOwnerReferences()
|
||||
gvk := un.GroupVersionKind()
|
||||
// Special case for endpoint. Remove after https://github.com/kubernetes/kubernetes/issues/28483 is fixed
|
||||
if un.GroupVersionKind().Group == "" && un.GetKind() == kube.EndpointsKind && len(un.GetOwnerReferences()) == 0 {
|
||||
if gvk.Group == "" && gvk.Kind == kube.EndpointsKind && len(un.GetOwnerReferences()) == 0 {
|
||||
ownerRefs = append(ownerRefs, metav1.OwnerReference{
|
||||
Name: un.GetName(),
|
||||
Kind: kube.ServiceKind,
|
||||
APIVersion: "",
|
||||
APIVersion: "v1",
|
||||
})
|
||||
}
|
||||
|
||||
// Special case for Operator Lifecycle Manager ClusterServiceVersion:
|
||||
if un.GroupVersionKind().Group == "operators.coreos.com" && un.GetKind() == "ClusterServiceVersion" {
|
||||
if un.GetAnnotations()["olm.operatorGroup"] != "" {
|
||||
ownerRefs = append(ownerRefs, metav1.OwnerReference{
|
||||
Name: un.GetAnnotations()["olm.operatorGroup"],
|
||||
Kind: "OperatorGroup",
|
||||
APIVersion: "operators.coreos.com/v1",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// edge case. Consider auto-created service account tokens as a child of service account objects
|
||||
if yes, ref := isServiceAccountTokenSecret(un); yes {
|
||||
ownerRefs = append(ownerRefs, ref)
|
||||
}
|
||||
|
||||
nodeInfo := &node{
|
||||
resourceVersion: un.GetResourceVersion(),
|
||||
ref: v1.ObjectReference{
|
||||
APIVersion: un.GetAPIVersion(),
|
||||
Kind: un.GetKind(),
|
||||
Name: un.GetName(),
|
||||
Namespace: un.GetNamespace(),
|
||||
},
|
||||
ownerRefs: ownerRefs,
|
||||
ref: kube.GetObjectRef(un),
|
||||
ownerRefs: ownerRefs,
|
||||
}
|
||||
|
||||
populateNodeInfo(un, nodeInfo)
|
||||
appName := kube.GetAppInstanceLabel(un, appInstanceLabel)
|
||||
if len(ownerRefs) == 0 && appName != "" {
|
||||
nodeInfo.appName = appName
|
||||
nodeInfo.resource = un
|
||||
} else {
|
||||
// edge case. we do not label CRDs, so they miss the tracking label we inject. But we still
|
||||
// want the full resource to be available in our cache (to diff), so we store all CRDs
|
||||
switch gvk.Kind {
|
||||
case kube.CustomResourceDefinitionKind:
|
||||
nodeInfo.resource = un
|
||||
}
|
||||
}
|
||||
nodeInfo.health, _ = health.GetResourceHealth(un, c.settings.ResourceOverrides)
|
||||
|
||||
nodeInfo.health, _ = health.GetResourceHealth(un, c.cacheSettingsSrc().ResourceOverrides)
|
||||
return nodeInfo
|
||||
}
|
||||
|
||||
@@ -133,63 +194,80 @@ func (c *clusterInfo) removeNode(key kube.ResourceKey) {
|
||||
}
|
||||
|
||||
func (c *clusterInfo) invalidate() {
|
||||
c.syncLock.Lock()
|
||||
defer c.syncLock.Unlock()
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.syncTime = nil
|
||||
for i := range c.apisMeta {
|
||||
c.apisMeta[i].watchCancel()
|
||||
}
|
||||
c.apisMeta = nil
|
||||
c.namespacedResources = nil
|
||||
c.log.Warnf("invalidated cluster")
|
||||
}
|
||||
|
||||
func (c *clusterInfo) synced() bool {
|
||||
if c.syncTime == nil {
|
||||
syncTime := c.syncTime
|
||||
if syncTime == nil {
|
||||
return false
|
||||
}
|
||||
if c.syncError != nil {
|
||||
return time.Now().Before(c.syncTime.Add(clusterRetryTimeout))
|
||||
return time.Now().Before(syncTime.Add(clusterRetryTimeout))
|
||||
}
|
||||
return time.Now().Before(c.syncTime.Add(clusterSyncTimeout))
|
||||
return time.Now().Before(syncTime.Add(clusterSyncTimeout))
|
||||
}
|
||||
|
||||
func (c *clusterInfo) stopWatching(gk schema.GroupKind) {
|
||||
c.syncLock.Lock()
|
||||
defer c.syncLock.Unlock()
|
||||
func (c *clusterInfo) stopWatching(gk schema.GroupKind, ns string) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if info, ok := c.apisMeta[gk]; ok {
|
||||
info.watchCancel()
|
||||
delete(c.apisMeta, gk)
|
||||
c.replaceResourceCache(gk, "", []unstructured.Unstructured{})
|
||||
log.Warnf("Stop watching %s not found on %s.", gk, c.cluster.Server)
|
||||
c.replaceResourceCache(gk, "", []unstructured.Unstructured{}, ns)
|
||||
c.log.Warnf("Stop watching: %s not found", gk)
|
||||
}
|
||||
}
|
||||
|
||||
// startMissingWatches lists supported cluster resources and start watching for changes unless watch is already running
|
||||
func (c *clusterInfo) startMissingWatches() error {
|
||||
config := c.cluster.RESTConfig()
|
||||
|
||||
apis, err := c.kubectl.GetAPIResources(c.cluster.RESTConfig(), c.settings)
|
||||
apis, err := c.kubectl.GetAPIResources(config, c.cacheSettingsSrc().ResourcesFilter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := c.kubectl.NewDynamicClient(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
namespacedResources := make(map[schema.GroupKind]bool)
|
||||
for i := range apis {
|
||||
api := apis[i]
|
||||
namespacedResources[api.GroupKind] = api.Meta.Namespaced
|
||||
if _, ok := c.apisMeta[api.GroupKind]; !ok {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
info := &apiMeta{namespaced: api.Meta.Namespaced, watchCancel: cancel}
|
||||
c.apisMeta[api.GroupKind] = info
|
||||
go c.watchEvents(ctx, api, info)
|
||||
|
||||
err = c.processApi(client, api, func(resClient dynamic.ResourceInterface, ns string) error {
|
||||
go c.watchEvents(ctx, api, info, resClient, ns)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
c.namespacedResources = namespacedResources
|
||||
return nil
|
||||
}
|
||||
|
||||
func runSynced(lock *sync.Mutex, action func() error) error {
|
||||
func runSynced(lock sync.Locker, action func() error) error {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
return action()
|
||||
}
|
||||
|
||||
func (c *clusterInfo) watchEvents(ctx context.Context, api kube.APIResourceInfo, info *apiMeta) {
|
||||
func (c *clusterInfo) watchEvents(ctx context.Context, api kube.APIResourceInfo, info *apiMeta, resClient dynamic.ResourceInterface, ns string) {
|
||||
util.RetryUntilSucceed(func() (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@@ -197,13 +275,28 @@ func (c *clusterInfo) watchEvents(ctx context.Context, api kube.APIResourceInfo,
|
||||
}
|
||||
}()
|
||||
|
||||
err = runSynced(c.syncLock, func() error {
|
||||
err = runSynced(c.lock, func() error {
|
||||
if info.resourceVersion == "" {
|
||||
list, err := api.Interface.List(metav1.ListOptions{})
|
||||
listPager := pager.New(func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) {
|
||||
res, err := resClient.List(opts)
|
||||
if err == nil {
|
||||
info.resourceVersion = res.GetResourceVersion()
|
||||
}
|
||||
return res, err
|
||||
})
|
||||
var items []unstructured.Unstructured
|
||||
err = listPager.EachListItem(ctx, metav1.ListOptions{}, func(obj runtime.Object) error {
|
||||
if un, ok := obj.(*unstructured.Unstructured); !ok {
|
||||
return fmt.Errorf("object %s/%s has an unexpected type", un.GroupVersionKind().String(), un.GetName())
|
||||
} else {
|
||||
items = append(items, *un)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to load initial state of resource %s: %v", api.GroupKind.String(), err)
|
||||
}
|
||||
c.replaceResourceCache(api.GroupKind, list.GetResourceVersion(), list.Items)
|
||||
c.replaceResourceCache(api.GroupKind, info.resourceVersion, items, ns)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -212,20 +305,15 @@ func (c *clusterInfo) watchEvents(ctx context.Context, api kube.APIResourceInfo,
|
||||
return err
|
||||
}
|
||||
|
||||
w, err := api.Interface.Watch(metav1.ListOptions{ResourceVersion: info.resourceVersion})
|
||||
w, err := resClient.Watch(metav1.ListOptions{ResourceVersion: info.resourceVersion})
|
||||
if errors.IsNotFound(err) {
|
||||
c.stopWatching(api.GroupKind)
|
||||
c.stopWatching(api.GroupKind, ns)
|
||||
return nil
|
||||
}
|
||||
|
||||
err = runSynced(c.syncLock, func() error {
|
||||
if errors.IsGone(err) {
|
||||
info.resourceVersion = ""
|
||||
log.Warnf("Resource version of %s on %s is too old.", api.GroupKind, c.cluster.Server)
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
if errors.IsGone(err) {
|
||||
info.resourceVersion = ""
|
||||
c.log.Warnf("Resource version of %s is too old", api.GroupKind)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -238,12 +326,7 @@ func (c *clusterInfo) watchEvents(ctx context.Context, api kube.APIResourceInfo,
|
||||
if ok {
|
||||
obj := event.Object.(*unstructured.Unstructured)
|
||||
info.resourceVersion = obj.GetResourceVersion()
|
||||
err = c.processEvent(event.Type, obj)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to process event %s %s/%s/%s: %v", event.Type, obj.GroupVersionKind(), obj.GetNamespace(), obj.GetName(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
c.processEvent(event.Type, obj)
|
||||
if kube.IsCRD(obj) {
|
||||
if event.Type == watch.Deleted {
|
||||
group, groupOk, groupErr := unstructured.NestedString(obj.Object, "spec", "group")
|
||||
@@ -251,17 +334,17 @@ func (c *clusterInfo) watchEvents(ctx context.Context, api kube.APIResourceInfo,
|
||||
|
||||
if groupOk && groupErr == nil && kindOk && kindErr == nil {
|
||||
gk := schema.GroupKind{Group: group, Kind: kind}
|
||||
c.stopWatching(gk)
|
||||
c.stopWatching(gk, ns)
|
||||
}
|
||||
} else {
|
||||
err = runSynced(c.syncLock, func() error {
|
||||
err = runSynced(c.lock, func() error {
|
||||
return c.startMissingWatches()
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Warnf("Failed to start missing watch: %v", err)
|
||||
c.log.Warnf("Failed to start missing watch: %v", err)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("Watch %s on %s has closed", api.GroupKind, c.cluster.Server)
|
||||
@@ -272,6 +355,25 @@ func (c *clusterInfo) watchEvents(ctx context.Context, api kube.APIResourceInfo,
|
||||
}, fmt.Sprintf("watch %s on %s", api.GroupKind, c.cluster.Server), ctx, watchResourcesRetryTimeout)
|
||||
}
|
||||
|
||||
func (c *clusterInfo) processApi(client dynamic.Interface, api kube.APIResourceInfo, callback func(resClient dynamic.ResourceInterface, ns string) error) error {
|
||||
resClient := client.Resource(api.GroupVersionResource)
|
||||
if len(c.cluster.Namespaces) == 0 {
|
||||
return callback(resClient, "")
|
||||
}
|
||||
|
||||
if !api.Meta.Namespaced {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ns := range c.cluster.Namespaces {
|
||||
err := callback(resClient.Namespace(ns), ns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *clusterInfo) sync() (err error) {
|
||||
|
||||
c.log.Info("Start syncing cluster")
|
||||
@@ -281,30 +383,69 @@ func (c *clusterInfo) sync() (err error) {
|
||||
}
|
||||
c.apisMeta = make(map[schema.GroupKind]*apiMeta)
|
||||
c.nodes = make(map[kube.ResourceKey]*node)
|
||||
c.namespacedResources = make(map[schema.GroupKind]bool)
|
||||
config := c.cluster.RESTConfig()
|
||||
version, err := c.kubectl.GetServerVersion(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.serverVersion = version
|
||||
groups, err := c.kubectl.GetAPIGroups(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.apiGroups = groups
|
||||
|
||||
apis, err := c.kubectl.GetAPIResources(c.cluster.RESTConfig(), c.settings)
|
||||
apis, err := c.kubectl.GetAPIResources(config, c.cacheSettingsSrc().ResourcesFilter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := c.kubectl.NewDynamicClient(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lock := sync.Mutex{}
|
||||
err = util.RunAllAsync(len(apis), func(i int) error {
|
||||
api := apis[i]
|
||||
list, err := api.Interface.List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lock.Lock()
|
||||
for i := range list.Items {
|
||||
c.setNode(c.createObjInfo(&list.Items[i], c.settings.GetAppInstanceLabelKey()))
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
info := &apiMeta{namespaced: api.Meta.Namespaced, watchCancel: cancel}
|
||||
c.apisMeta[api.GroupKind] = info
|
||||
c.namespacedResources[api.GroupKind] = api.Meta.Namespaced
|
||||
lock.Unlock()
|
||||
return nil
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
err = c.startMissingWatches()
|
||||
}
|
||||
return c.processApi(client, api, func(resClient dynamic.ResourceInterface, ns string) error {
|
||||
|
||||
listPager := pager.New(func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) {
|
||||
res, err := resClient.List(opts)
|
||||
if err == nil {
|
||||
lock.Lock()
|
||||
info.resourceVersion = res.GetResourceVersion()
|
||||
lock.Unlock()
|
||||
}
|
||||
return res, err
|
||||
})
|
||||
|
||||
err = listPager.EachListItem(context.Background(), metav1.ListOptions{}, func(obj runtime.Object) error {
|
||||
if un, ok := obj.(*unstructured.Unstructured); !ok {
|
||||
return fmt.Errorf("object %s/%s has an unexpected type", un.GroupVersionKind().String(), un.GetName())
|
||||
} else {
|
||||
lock.Lock()
|
||||
c.setNode(c.createObjInfo(un, c.cacheSettingsSrc().AppInstanceLabelKey))
|
||||
lock.Unlock()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load initial state of resource %s: %v", api.GroupKind.String(), err)
|
||||
}
|
||||
|
||||
go c.watchEvents(ctx, api, info, resClient, ns)
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Failed to sync cluster %s: %v", c.cluster.Server, err)
|
||||
@@ -316,12 +457,17 @@ func (c *clusterInfo) sync() (err error) {
|
||||
}
|
||||
|
||||
func (c *clusterInfo) ensureSynced() error {
|
||||
c.syncLock.Lock()
|
||||
defer c.syncLock.Unlock()
|
||||
// first check if cluster is synced *without lock*
|
||||
if c.synced() {
|
||||
return c.syncError
|
||||
}
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
// before doing any work, check once again now that we have the lock, to see if it got
|
||||
// synced between the first check and now
|
||||
if c.synced() {
|
||||
return c.syncError
|
||||
}
|
||||
|
||||
err := c.sync()
|
||||
syncTime := time.Now()
|
||||
c.syncTime = &syncTime
|
||||
@@ -329,31 +475,60 @@ func (c *clusterInfo) ensureSynced() error {
|
||||
return c.syncError
|
||||
}
|
||||
|
||||
func (c *clusterInfo) iterateHierarchy(key kube.ResourceKey, action func(child appv1.ResourceNode)) {
|
||||
func (c *clusterInfo) getNamespaceTopLevelResources(namespace string) map[kube.ResourceKey]appv1.ResourceNode {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
nodes := make(map[kube.ResourceKey]appv1.ResourceNode)
|
||||
for _, node := range c.nsIndex[namespace] {
|
||||
if len(node.ownerRefs) == 0 {
|
||||
nodes[node.resourceKey()] = node.asResourceNode()
|
||||
}
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
func (c *clusterInfo) iterateHierarchy(key kube.ResourceKey, action func(child appv1.ResourceNode, appName string)) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if objInfo, ok := c.nodes[key]; ok {
|
||||
action(objInfo.asResourceNode())
|
||||
nsNodes := c.nsIndex[key.Namespace]
|
||||
action(objInfo.asResourceNode(), objInfo.getApp(nsNodes))
|
||||
childrenByUID := make(map[types.UID][]*node)
|
||||
for _, child := range nsNodes {
|
||||
if objInfo.isParentOf(child) {
|
||||
action(child.asResourceNode())
|
||||
childrenByUID[child.ref.UID] = append(childrenByUID[child.ref.UID], child)
|
||||
}
|
||||
}
|
||||
// make sure children has no duplicates
|
||||
for _, children := range childrenByUID {
|
||||
if len(children) > 0 {
|
||||
// The object might have multiple children with the same UID (e.g. replicaset from apps and extensions group). It is ok to pick any object but we need to make sure
|
||||
// we pick the same child after every refresh.
|
||||
sort.Slice(children, func(i, j int) bool {
|
||||
key1 := children[i].resourceKey()
|
||||
key2 := children[j].resourceKey()
|
||||
return strings.Compare(key1.String(), key2.String()) < 0
|
||||
})
|
||||
child := children[0]
|
||||
action(child.asResourceNode(), child.getApp(nsNodes))
|
||||
child.iterateChildren(nsNodes, map[kube.ResourceKey]bool{objInfo.resourceKey(): true}, action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *clusterInfo) isNamespaced(obj *unstructured.Unstructured) bool {
|
||||
if api, ok := c.apisMeta[kube.GetResourceKey(obj).GroupKind()]; ok && !api.namespaced {
|
||||
return false
|
||||
func (c *clusterInfo) isNamespaced(gk schema.GroupKind) bool {
|
||||
// this is safe to access without a lock since we always replace the entire map instead of mutating keys
|
||||
if isNamespaced, ok := c.namespacedResources[gk]; ok {
|
||||
return isNamespaced
|
||||
}
|
||||
log.Warnf("group/kind %s scope is unknown (known objects: %d). assuming namespaced object", gk, len(c.namespacedResources))
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
|
||||
managedObjs := make(map[kube.ResourceKey]*unstructured.Unstructured)
|
||||
// iterate all objects in live state cache to find ones associated with app
|
||||
@@ -362,12 +537,13 @@ func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*uns
|
||||
managedObjs[key] = o.resource
|
||||
}
|
||||
}
|
||||
// iterate target objects and identify ones that already exist in the cluster,\
|
||||
config := metrics.AddMetricsTransportWrapper(c.metricsServer, a, c.cluster.RESTConfig())
|
||||
// iterate target objects and identify ones that already exist in the cluster,
|
||||
// but are simply missing our label
|
||||
lock := &sync.Mutex{}
|
||||
err := util.RunAllAsync(len(targetObjs), func(i int) error {
|
||||
targetObj := targetObjs[i]
|
||||
key := GetTargetObjKey(a, targetObj, c.isNamespaced(targetObj))
|
||||
key := GetTargetObjKey(a, targetObj, c.isNamespaced(targetObj.GroupVersionKind().GroupKind()))
|
||||
lock.Lock()
|
||||
managedObj := managedObjs[key]
|
||||
lock.Unlock()
|
||||
@@ -378,7 +554,7 @@ func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*uns
|
||||
managedObj = existingObj.resource
|
||||
} else {
|
||||
var err error
|
||||
managedObj, err = c.kubectl.GetResource(c.cluster.RESTConfig(), targetObj.GroupVersionKind(), existingObj.ref.Name, existingObj.ref.Namespace)
|
||||
managedObj, err = c.kubectl.GetResource(config, targetObj.GroupVersionKind(), existingObj.ref.Name, existingObj.ref.Namespace)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return nil
|
||||
@@ -386,13 +562,32 @@ func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*uns
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if _, watched := c.apisMeta[key.GroupKind()]; !watched {
|
||||
var err error
|
||||
managedObj, err = c.kubectl.GetResource(config, targetObj.GroupVersionKind(), targetObj.GetName(), targetObj.GetNamespace())
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if managedObj != nil {
|
||||
managedObj, err := c.kubectl.ConvertToVersion(managedObj, targetObj.GroupVersionKind().Group, targetObj.GroupVersionKind().Version)
|
||||
converted, err := c.kubectl.ConvertToVersion(managedObj, targetObj.GroupVersionKind().Group, targetObj.GroupVersionKind().Version)
|
||||
if err != nil {
|
||||
return err
|
||||
// fallback to loading resource from kubernetes if conversion fails
|
||||
log.Warnf("Failed to convert resource: %v", err)
|
||||
managedObj, err = c.kubectl.GetResource(config, targetObj.GroupVersionKind(), managedObj.GetName(), managedObj.GetNamespace())
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
managedObj = converted
|
||||
}
|
||||
lock.Lock()
|
||||
managedObjs[key] = managedObj
|
||||
@@ -407,32 +602,32 @@ func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*uns
|
||||
return managedObjs, nil
|
||||
}
|
||||
|
||||
func (c *clusterInfo) delete(obj *unstructured.Unstructured) error {
|
||||
return c.kubectl.DeleteResource(c.cluster.RESTConfig(), obj.GroupVersionKind(), obj.GetName(), obj.GetNamespace(), false)
|
||||
}
|
||||
|
||||
func (c *clusterInfo) processEvent(event watch.EventType, un *unstructured.Unstructured) error {
|
||||
func (c *clusterInfo) processEvent(event watch.EventType, un *unstructured.Unstructured) {
|
||||
if c.onEventReceived != nil {
|
||||
c.onEventReceived(event, un)
|
||||
}
|
||||
key := kube.GetResourceKey(un)
|
||||
if event == watch.Modified && skipAppRequeing(key) {
|
||||
return
|
||||
}
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
key := kube.GetResourceKey(un)
|
||||
existingNode, exists := c.nodes[key]
|
||||
if event == watch.Deleted {
|
||||
if exists {
|
||||
c.onNodeRemoved(key, existingNode)
|
||||
}
|
||||
} else if event != watch.Deleted {
|
||||
c.onNodeUpdated(exists, existingNode, un, key)
|
||||
c.onNodeUpdated(exists, existingNode, un)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *clusterInfo) onNodeUpdated(exists bool, existingNode *node, un *unstructured.Unstructured, key kube.ResourceKey) {
|
||||
func (c *clusterInfo) onNodeUpdated(exists bool, existingNode *node, un *unstructured.Unstructured) {
|
||||
nodes := make([]*node, 0)
|
||||
if exists {
|
||||
nodes = append(nodes, existingNode)
|
||||
}
|
||||
newObj := c.createObjInfo(un, c.settings.GetAppInstanceLabelKey())
|
||||
newObj := c.createObjInfo(un, c.cacheSettingsSrc().AppInstanceLabelKey)
|
||||
c.setNode(newObj)
|
||||
nodes = append(nodes, newObj)
|
||||
toNotify := make(map[string]bool)
|
||||
@@ -440,15 +635,13 @@ func (c *clusterInfo) onNodeUpdated(exists bool, existingNode *node, un *unstruc
|
||||
n := nodes[i]
|
||||
if ns, ok := c.nsIndex[n.ref.Namespace]; ok {
|
||||
app := n.getApp(ns)
|
||||
if app == "" || skipAppRequeing(key) {
|
||||
if app == "" {
|
||||
continue
|
||||
}
|
||||
toNotify[app] = n.isRootAppNode() || toNotify[app]
|
||||
}
|
||||
}
|
||||
for name, full := range toNotify {
|
||||
c.onAppUpdated(name, full)
|
||||
}
|
||||
c.onObjectUpdated(toNotify, newObj.ref)
|
||||
}
|
||||
|
||||
func (c *clusterInfo) onNodeRemoved(key kube.ResourceKey, n *node) {
|
||||
@@ -458,9 +651,11 @@ func (c *clusterInfo) onNodeRemoved(key kube.ResourceKey, n *node) {
|
||||
}
|
||||
|
||||
c.removeNode(key)
|
||||
managedByApp := make(map[string]bool)
|
||||
if appName != "" {
|
||||
c.onAppUpdated(appName, n.isRootAppNode())
|
||||
managedByApp[appName] = n.isRootAppNode()
|
||||
}
|
||||
c.onObjectUpdated(managedByApp, n.ref)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -469,6 +664,18 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func (c *clusterInfo) getClusterInfo() metrics.ClusterInfo {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
return metrics.ClusterInfo{
|
||||
APIsCount: len(c.apisMeta),
|
||||
K8SVersion: c.serverVersion,
|
||||
ResourcesCount: len(c.nodes),
|
||||
Server: c.cluster.Server,
|
||||
LastCacheSyncTime: c.syncTime,
|
||||
}
|
||||
}
|
||||
|
||||
// skipAppRequeing checks if the object is an API type which we want to skip requeuing against.
|
||||
// We ignore API types which have a high churn rate, and/or whose updates are irrelevant to the app
|
||||
func skipAppRequeing(key kube.ResourceKey) bool {
|
||||
|
||||
221
controller/cache/cluster_test.go
vendored
@@ -18,11 +18,11 @@ import (
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/dynamic/fake"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/kube/kubetest"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
func strToUnstructured(jsonStr string) *unstructured.Unstructured {
|
||||
@@ -43,24 +43,30 @@ var (
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
uid: "1"
|
||||
name: helm-guestbook-pod
|
||||
namespace: default
|
||||
ownerReferences:
|
||||
- apiVersion: extensions/v1beta1
|
||||
- apiVersion: apps/v1
|
||||
kind: ReplicaSet
|
||||
name: helm-guestbook-rs
|
||||
uid: "2"
|
||||
resourceVersion: "123"`)
|
||||
|
||||
testRS = strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: ReplicaSet
|
||||
metadata:
|
||||
uid: "2"
|
||||
name: helm-guestbook-rs
|
||||
namespace: default
|
||||
annotations:
|
||||
deployment.kubernetes.io/revision: "2"
|
||||
ownerReferences:
|
||||
- apiVersion: extensions/v1beta1
|
||||
- apiVersion: apps/v1beta1
|
||||
kind: Deployment
|
||||
name: helm-guestbook
|
||||
uid: "3"
|
||||
resourceVersion: "123"`)
|
||||
|
||||
testDeploy = strToUnstructured(`
|
||||
@@ -69,6 +75,7 @@ var (
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: helm-guestbook
|
||||
uid: "3"
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
resourceVersion: "123"`)
|
||||
@@ -80,6 +87,7 @@ var (
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
resourceVersion: "123"
|
||||
uid: "4"
|
||||
spec:
|
||||
selector:
|
||||
app: guestbook
|
||||
@@ -95,6 +103,7 @@ var (
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
uid: "4"
|
||||
spec:
|
||||
backend:
|
||||
serviceName: not-found-service
|
||||
@@ -107,6 +116,10 @@ var (
|
||||
serviceName: helm-guestbook
|
||||
servicePort: 443
|
||||
path: /
|
||||
- backend:
|
||||
serviceName: helm-guestbook
|
||||
servicePort: https
|
||||
path: /
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
@@ -122,46 +135,127 @@ func newCluster(objs ...*unstructured.Unstructured) *clusterInfo {
|
||||
client := fake.NewSimpleDynamicClient(scheme, runtimeObjs...)
|
||||
|
||||
apiResources := []kube.APIResourceInfo{{
|
||||
GroupKind: schema.GroupKind{Group: "", Kind: "Pod"},
|
||||
Interface: client.Resource(schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}),
|
||||
Meta: metav1.APIResource{Namespaced: true},
|
||||
GroupKind: schema.GroupKind{Group: "", Kind: "Pod"},
|
||||
GroupVersionResource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
|
||||
Meta: metav1.APIResource{Namespaced: true},
|
||||
}, {
|
||||
GroupKind: schema.GroupKind{Group: "apps", Kind: "ReplicaSet"},
|
||||
Interface: client.Resource(schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "replicasets"}),
|
||||
Meta: metav1.APIResource{Namespaced: true},
|
||||
GroupKind: schema.GroupKind{Group: "apps", Kind: "ReplicaSet"},
|
||||
GroupVersionResource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "replicasets"},
|
||||
Meta: metav1.APIResource{Namespaced: true},
|
||||
}, {
|
||||
GroupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"},
|
||||
Interface: client.Resource(schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}),
|
||||
Meta: metav1.APIResource{Namespaced: true},
|
||||
GroupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"},
|
||||
GroupVersionResource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"},
|
||||
Meta: metav1.APIResource{Namespaced: true},
|
||||
}}
|
||||
|
||||
return newClusterExt(kubetest.MockKubectlCmd{APIResources: apiResources})
|
||||
return newClusterExt(&kubetest.MockKubectlCmd{APIResources: apiResources, DynamicClient: client})
|
||||
}
|
||||
|
||||
func newClusterExt(kubectl kube.Kubectl) *clusterInfo {
|
||||
return &clusterInfo{
|
||||
lock: &sync.Mutex{},
|
||||
nodes: make(map[kube.ResourceKey]*node),
|
||||
onAppUpdated: func(appName string, fullRefresh bool) {},
|
||||
kubectl: kubectl,
|
||||
nsIndex: make(map[string]map[kube.ResourceKey]*node),
|
||||
cluster: &appv1.Cluster{},
|
||||
syncTime: nil,
|
||||
syncLock: &sync.Mutex{},
|
||||
apisMeta: make(map[schema.GroupKind]*apiMeta),
|
||||
log: log.WithField("cluster", "test"),
|
||||
settings: &settings.ArgoCDSettings{},
|
||||
lock: &sync.RWMutex{},
|
||||
nodes: make(map[kube.ResourceKey]*node),
|
||||
onObjectUpdated: func(managedByApp map[string]bool, reference corev1.ObjectReference) {},
|
||||
kubectl: kubectl,
|
||||
nsIndex: make(map[string]map[kube.ResourceKey]*node),
|
||||
cluster: &appv1.Cluster{},
|
||||
syncTime: nil,
|
||||
apisMeta: make(map[schema.GroupKind]*apiMeta),
|
||||
log: log.WithField("cluster", "test"),
|
||||
cacheSettingsSrc: func() *cacheSettings {
|
||||
return &cacheSettings{AppInstanceLabelKey: common.LabelKeyAppInstance}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getChildren(cluster *clusterInfo, un *unstructured.Unstructured) []appv1.ResourceNode {
|
||||
hierarchy := make([]appv1.ResourceNode, 0)
|
||||
cluster.iterateHierarchy(kube.GetResourceKey(un), func(child appv1.ResourceNode) {
|
||||
cluster.iterateHierarchy(kube.GetResourceKey(un), func(child appv1.ResourceNode, app string) {
|
||||
hierarchy = append(hierarchy, child)
|
||||
})
|
||||
return hierarchy[1:]
|
||||
}
|
||||
|
||||
func TestEnsureSynced(t *testing.T) {
|
||||
obj1 := strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: {"name": "helm-guestbook1", "namespace": "default1"}
|
||||
`)
|
||||
obj2 := strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: {"name": "helm-guestbook2", "namespace": "default2"}
|
||||
`)
|
||||
|
||||
cluster := newCluster(obj1, obj2)
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Len(t, cluster.nodes, 2)
|
||||
var names []string
|
||||
for k := range cluster.nodes {
|
||||
names = append(names, k.Name)
|
||||
}
|
||||
assert.ElementsMatch(t, []string{"helm-guestbook1", "helm-guestbook2"}, names)
|
||||
}
|
||||
|
||||
func TestEnsureSyncedSingleNamespace(t *testing.T) {
|
||||
obj1 := strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: {"name": "helm-guestbook1", "namespace": "default1"}
|
||||
`)
|
||||
obj2 := strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: {"name": "helm-guestbook2", "namespace": "default2"}
|
||||
`)
|
||||
|
||||
cluster := newCluster(obj1, obj2)
|
||||
cluster.cluster.Namespaces = []string{"default1"}
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Len(t, cluster.nodes, 1)
|
||||
var names []string
|
||||
for k := range cluster.nodes {
|
||||
names = append(names, k.Name)
|
||||
}
|
||||
assert.ElementsMatch(t, []string{"helm-guestbook1"}, names)
|
||||
}
|
||||
|
||||
func TestGetNamespaceResources(t *testing.T) {
|
||||
defaultNamespaceTopLevel1 := strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: {"name": "helm-guestbook1", "namespace": "default"}
|
||||
`)
|
||||
defaultNamespaceTopLevel2 := strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: {"name": "helm-guestbook2", "namespace": "default"}
|
||||
`)
|
||||
kubesystemNamespaceTopLevel2 := strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: {"name": "helm-guestbook3", "namespace": "kube-system"}
|
||||
`)
|
||||
|
||||
cluster := newCluster(defaultNamespaceTopLevel1, defaultNamespaceTopLevel2, kubesystemNamespaceTopLevel2)
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
resources := cluster.getNamespaceTopLevelResources("default")
|
||||
assert.Len(t, resources, 2)
|
||||
assert.Equal(t, resources[kube.GetResourceKey(defaultNamespaceTopLevel1)].Name, "helm-guestbook1")
|
||||
assert.Equal(t, resources[kube.GetResourceKey(defaultNamespaceTopLevel2)].Name, "helm-guestbook2")
|
||||
|
||||
resources = cluster.getNamespaceTopLevelResources("kube-system")
|
||||
assert.Len(t, resources, 1)
|
||||
assert.Equal(t, resources[kube.GetResourceKey(kubesystemNamespaceTopLevel2)].Name, "helm-guestbook3")
|
||||
}
|
||||
|
||||
func TestGetChildren(t *testing.T) {
|
||||
cluster := newCluster(testPod, testRS, testDeploy)
|
||||
err := cluster.ensureSynced()
|
||||
@@ -175,6 +269,7 @@ func TestGetChildren(t *testing.T) {
|
||||
Name: "helm-guestbook-pod",
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
UID: "1",
|
||||
},
|
||||
ParentRefs: []appv1.ResourceRef{{
|
||||
Group: "apps",
|
||||
@@ -182,6 +277,7 @@ func TestGetChildren(t *testing.T) {
|
||||
Kind: "ReplicaSet",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-rs",
|
||||
UID: "2",
|
||||
}},
|
||||
Health: &appv1.HealthStatus{Status: appv1.HealthStatusUnknown},
|
||||
NetworkingInfo: &appv1.ResourceNetworkingInfo{Labels: testPod.GetLabels()},
|
||||
@@ -197,11 +293,12 @@ func TestGetChildren(t *testing.T) {
|
||||
Name: "helm-guestbook-rs",
|
||||
Group: "apps",
|
||||
Version: "v1",
|
||||
UID: "2",
|
||||
},
|
||||
ResourceVersion: "123",
|
||||
Health: &appv1.HealthStatus{Status: appv1.HealthStatusHealthy},
|
||||
Info: []appv1.InfoItem{},
|
||||
ParentRefs: []appv1.ResourceRef{{Group: "apps", Version: "", Kind: "Deployment", Namespace: "default", Name: "helm-guestbook"}},
|
||||
Info: []appv1.InfoItem{{Name: "Revision", Value: "Rev:2"}},
|
||||
ParentRefs: []appv1.ResourceRef{{Group: "apps", Version: "", Kind: "Deployment", Namespace: "default", Name: "helm-guestbook", UID: "3"}},
|
||||
}}, rsChildren...), deployChildren)
|
||||
}
|
||||
|
||||
@@ -237,8 +334,7 @@ func TestChildDeletedEvent(t *testing.T) {
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = cluster.processEvent(watch.Deleted, testPod)
|
||||
assert.Nil(t, err)
|
||||
cluster.processEvent(watch.Deleted, testPod)
|
||||
|
||||
rsChildren := getChildren(cluster, testRS)
|
||||
assert.Equal(t, []appv1.ResourceNode{}, rsChildren)
|
||||
@@ -253,16 +349,17 @@ func TestProcessNewChildEvent(t *testing.T) {
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
uid: "4"
|
||||
name: helm-guestbook-pod2
|
||||
namespace: default
|
||||
ownerReferences:
|
||||
- apiVersion: extensions/v1beta1
|
||||
- apiVersion: apps/v1
|
||||
kind: ReplicaSet
|
||||
name: helm-guestbook-rs
|
||||
uid: "2"
|
||||
resourceVersion: "123"`)
|
||||
|
||||
err = cluster.processEvent(watch.Added, newPod)
|
||||
assert.Nil(t, err)
|
||||
cluster.processEvent(watch.Added, newPod)
|
||||
|
||||
rsChildren := getChildren(cluster, testRS)
|
||||
sort.Slice(rsChildren, func(i, j int) bool {
|
||||
@@ -275,6 +372,7 @@ func TestProcessNewChildEvent(t *testing.T) {
|
||||
Name: "helm-guestbook-pod",
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
UID: "1",
|
||||
},
|
||||
Info: []appv1.InfoItem{{Name: "Containers", Value: "0/0"}},
|
||||
Health: &appv1.HealthStatus{Status: appv1.HealthStatusUnknown},
|
||||
@@ -285,6 +383,7 @@ func TestProcessNewChildEvent(t *testing.T) {
|
||||
Kind: "ReplicaSet",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-rs",
|
||||
UID: "2",
|
||||
}},
|
||||
ResourceVersion: "123",
|
||||
}, {
|
||||
@@ -294,6 +393,7 @@ func TestProcessNewChildEvent(t *testing.T) {
|
||||
Name: "helm-guestbook-pod2",
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
UID: "4",
|
||||
},
|
||||
NetworkingInfo: &appv1.ResourceNetworkingInfo{Labels: testPod.GetLabels()},
|
||||
Info: []appv1.InfoItem{{Name: "Containers", Value: "0/0"}},
|
||||
@@ -304,6 +404,7 @@ func TestProcessNewChildEvent(t *testing.T) {
|
||||
Kind: "ReplicaSet",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-rs",
|
||||
UID: "2",
|
||||
}},
|
||||
ResourceVersion: "123",
|
||||
}}, rsChildren)
|
||||
@@ -339,8 +440,7 @@ func TestUpdateResourceTags(t *testing.T) {
|
||||
},
|
||||
}},
|
||||
}
|
||||
err = cluster.processEvent(watch.Modified, mustToUnstructured(pod))
|
||||
assert.Nil(t, err)
|
||||
cluster.processEvent(watch.Modified, mustToUnstructured(pod))
|
||||
|
||||
podNode = cluster.nodes[kube.GetResourceKey(mustToUnstructured(pod))]
|
||||
|
||||
@@ -351,15 +451,16 @@ func TestUpdateResourceTags(t *testing.T) {
|
||||
func TestUpdateAppResource(t *testing.T) {
|
||||
updatesReceived := make([]string, 0)
|
||||
cluster := newCluster(testPod, testRS, testDeploy)
|
||||
cluster.onAppUpdated = func(appName string, fullRefresh bool) {
|
||||
updatesReceived = append(updatesReceived, fmt.Sprintf("%s: %v", appName, fullRefresh))
|
||||
cluster.onObjectUpdated = func(managedByApp map[string]bool, _ corev1.ObjectReference) {
|
||||
for appName, fullRefresh := range managedByApp {
|
||||
updatesReceived = append(updatesReceived, fmt.Sprintf("%s: %v", appName, fullRefresh))
|
||||
}
|
||||
}
|
||||
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = cluster.processEvent(watch.Modified, mustToUnstructured(testPod))
|
||||
assert.Nil(t, err)
|
||||
cluster.processEvent(watch.Modified, mustToUnstructured(testPod))
|
||||
|
||||
assert.Contains(t, updatesReceived, "helm-guestbook: false")
|
||||
}
|
||||
@@ -403,7 +504,8 @@ func TestWatchCacheUpdated(t *testing.T) {
|
||||
|
||||
podGroupKind := testPod.GroupVersionKind().GroupKind()
|
||||
|
||||
cluster.replaceResourceCache(podGroupKind, "updated-list-version", []unstructured.Unstructured{*updated, *added})
|
||||
cluster.lock.Lock()
|
||||
cluster.replaceResourceCache(podGroupKind, "updated-list-version", []unstructured.Unstructured{*updated, *added}, "")
|
||||
|
||||
_, ok := cluster.nodes[kube.GetResourceKey(removed)]
|
||||
assert.False(t, ok)
|
||||
@@ -414,4 +516,45 @@ func TestWatchCacheUpdated(t *testing.T) {
|
||||
|
||||
_, ok = cluster.nodes[kube.GetResourceKey(added)]
|
||||
assert.True(t, ok)
|
||||
cluster.lock.Unlock()
|
||||
}
|
||||
|
||||
func TestNamespaceModeReplace(t *testing.T) {
|
||||
ns1Pod := testPod.DeepCopy()
|
||||
ns1Pod.SetNamespace("ns1")
|
||||
ns1Pod.SetName("pod1")
|
||||
|
||||
ns2Pod := testPod.DeepCopy()
|
||||
ns2Pod.SetNamespace("ns2")
|
||||
podGroupKind := testPod.GroupVersionKind().GroupKind()
|
||||
|
||||
cluster := newCluster(ns1Pod, ns2Pod)
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
cluster.replaceResourceCache(podGroupKind, "", nil, "ns1")
|
||||
|
||||
_, ok := cluster.nodes[kube.GetResourceKey(ns1Pod)]
|
||||
assert.False(t, ok)
|
||||
|
||||
_, ok = cluster.nodes[kube.GetResourceKey(ns2Pod)]
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestGetDuplicatedChildren(t *testing.T) {
|
||||
extensionsRS := testRS.DeepCopy()
|
||||
extensionsRS.SetGroupVersionKind(schema.GroupVersionKind{Group: "extensions", Kind: kube.ReplicaSetKind, Version: "v1beta1"})
|
||||
cluster := newCluster(testDeploy, testRS, extensionsRS)
|
||||
err := cluster.ensureSynced()
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Get children multiple times to make sure the right child is picked up every time.
|
||||
for i := 0; i < 5; i++ {
|
||||
children := getChildren(cluster, testDeploy)
|
||||
assert.Len(t, children, 1)
|
||||
assert.Equal(t, "apps", children[0].Group)
|
||||
assert.Equal(t, kube.ReplicaSetKind, children[0].Kind)
|
||||
assert.Equal(t, testRS.GetName(), children[0].Name)
|
||||
}
|
||||
}
|
||||
|
||||
86
controller/cache/info.go
vendored
@@ -9,12 +9,18 @@ import (
|
||||
k8snode "k8s.io/kubernetes/pkg/util/node"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/resource"
|
||||
)
|
||||
|
||||
func populateNodeInfo(un *unstructured.Unstructured, node *node) {
|
||||
|
||||
gvk := un.GroupVersionKind()
|
||||
revision := resource.GetRevision(un)
|
||||
if revision > 0 {
|
||||
node.info = append(node.info, v1alpha1.InfoItem{Name: "Revision", Value: fmt.Sprintf("Rev:%v", revision)})
|
||||
}
|
||||
switch gvk.Group {
|
||||
case "":
|
||||
switch gvk.Kind {
|
||||
@@ -25,14 +31,13 @@ func populateNodeInfo(un *unstructured.Unstructured, node *node) {
|
||||
populateServiceInfo(un, node)
|
||||
return
|
||||
}
|
||||
case "extensions":
|
||||
case "extensions", "networking.k8s.io":
|
||||
switch gvk.Kind {
|
||||
case kube.IngressKind:
|
||||
populateIngressInfo(un, node)
|
||||
return
|
||||
}
|
||||
}
|
||||
node.info = []v1alpha1.InfoItem{}
|
||||
}
|
||||
|
||||
func getIngress(un *unstructured.Unstructured) []v1.LoadBalancerIngress {
|
||||
@@ -63,14 +68,15 @@ func populateServiceInfo(un *unstructured.Unstructured, node *node) {
|
||||
}
|
||||
|
||||
func populateIngressInfo(un *unstructured.Unstructured, node *node) {
|
||||
targets := make([]v1alpha1.ResourceRef, 0)
|
||||
ingress := getIngress(un)
|
||||
targetsMap := make(map[v1alpha1.ResourceRef]bool)
|
||||
if backend, ok, err := unstructured.NestedMap(un.Object, "spec", "backend"); ok && err == nil {
|
||||
targets = append(targets, v1alpha1.ResourceRef{
|
||||
targetsMap[v1alpha1.ResourceRef{
|
||||
Group: "",
|
||||
Kind: kube.ServiceKind,
|
||||
Namespace: un.GetNamespace(),
|
||||
Name: fmt.Sprintf("%s", backend["serviceName"]),
|
||||
})
|
||||
}] = true
|
||||
}
|
||||
urlsSet := make(map[string]bool)
|
||||
if rules, ok, err := unstructured.NestedSlice(un.Object, "spec", "rules"); ok && err == nil {
|
||||
@@ -80,6 +86,14 @@ func populateIngressInfo(un *unstructured.Unstructured, node *node) {
|
||||
continue
|
||||
}
|
||||
host := rule["host"]
|
||||
if host == nil || host == "" {
|
||||
for i := range ingress {
|
||||
host = util.FirstNonEmpty(ingress[i].Hostname, ingress[i].IP)
|
||||
if host != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
paths, ok, err := unstructured.NestedSlice(rule, "http", "paths")
|
||||
if !ok || err != nil {
|
||||
continue
|
||||
@@ -91,39 +105,63 @@ func populateIngressInfo(un *unstructured.Unstructured, node *node) {
|
||||
}
|
||||
|
||||
if serviceName, ok, err := unstructured.NestedString(path, "backend", "serviceName"); ok && err == nil {
|
||||
targets = append(targets, v1alpha1.ResourceRef{
|
||||
targetsMap[v1alpha1.ResourceRef{
|
||||
Group: "",
|
||||
Kind: kube.ServiceKind,
|
||||
Namespace: un.GetNamespace(),
|
||||
Name: serviceName,
|
||||
})
|
||||
}] = true
|
||||
}
|
||||
|
||||
if port, ok, err := unstructured.NestedFieldNoCopy(path, "backend", "servicePort"); ok && err == nil && host != "" {
|
||||
switch fmt.Sprintf("%v", port) {
|
||||
case "80":
|
||||
urlsSet[fmt.Sprintf("http://%s", host)] = true
|
||||
case "443":
|
||||
urlsSet[fmt.Sprintf("https://%s", host)] = true
|
||||
if port, ok, err := unstructured.NestedFieldNoCopy(path, "backend", "servicePort"); ok && err == nil && host != "" && host != nil {
|
||||
stringPort := ""
|
||||
switch typedPod := port.(type) {
|
||||
case int64:
|
||||
stringPort = fmt.Sprintf("%d", typedPod)
|
||||
case float64:
|
||||
stringPort = fmt.Sprintf("%d", int64(typedPod))
|
||||
case string:
|
||||
stringPort = typedPod
|
||||
default:
|
||||
urlsSet[fmt.Sprintf("http://%s:%s", host, port)] = true
|
||||
stringPort = fmt.Sprintf("%v", port)
|
||||
}
|
||||
|
||||
var externalURL string
|
||||
switch stringPort {
|
||||
case "80", "http":
|
||||
externalURL = fmt.Sprintf("http://%s", host)
|
||||
case "443", "https":
|
||||
externalURL = fmt.Sprintf("https://%s", host)
|
||||
default:
|
||||
externalURL = fmt.Sprintf("http://%s:%s", host, stringPort)
|
||||
}
|
||||
|
||||
subPath := ""
|
||||
if nestedPath, ok, err := unstructured.NestedString(path, "path"); ok && err == nil {
|
||||
subPath = nestedPath
|
||||
}
|
||||
|
||||
externalURL += subPath
|
||||
urlsSet[externalURL] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
targets := make([]v1alpha1.ResourceRef, 0)
|
||||
for target := range targetsMap {
|
||||
targets = append(targets, target)
|
||||
}
|
||||
urls := make([]string, 0)
|
||||
for url := range urlsSet {
|
||||
urls = append(urls, url)
|
||||
}
|
||||
node.networkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetRefs: targets, Ingress: getIngress(un), ExternalURLs: urls}
|
||||
node.networkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetRefs: targets, Ingress: ingress, ExternalURLs: urls}
|
||||
}
|
||||
|
||||
func populatePodInfo(un *unstructured.Unstructured, node *node) {
|
||||
pod := v1.Pod{}
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, &pod)
|
||||
if err != nil {
|
||||
node.info = []v1alpha1.InfoItem{}
|
||||
return
|
||||
}
|
||||
restarts := 0
|
||||
@@ -135,13 +173,20 @@ func populatePodInfo(un *unstructured.Unstructured, node *node) {
|
||||
reason = pod.Status.Reason
|
||||
}
|
||||
|
||||
initializing := false
|
||||
|
||||
// note that I ignore initContainers
|
||||
imagesSet := make(map[string]bool)
|
||||
for _, container := range pod.Spec.InitContainers {
|
||||
imagesSet[container.Image] = true
|
||||
}
|
||||
for _, container := range pod.Spec.Containers {
|
||||
node.images = append(node.images, container.Image)
|
||||
imagesSet[container.Image] = true
|
||||
}
|
||||
|
||||
node.images = nil
|
||||
for image := range imagesSet {
|
||||
node.images = append(node.images, image)
|
||||
}
|
||||
|
||||
initializing := false
|
||||
for i := range pod.Status.InitContainerStatuses {
|
||||
container := pod.Status.InitContainerStatuses[i]
|
||||
restarts += int(container.RestartCount)
|
||||
@@ -204,7 +249,6 @@ func populatePodInfo(un *unstructured.Unstructured, node *node) {
|
||||
reason = "Terminating"
|
||||
}
|
||||
|
||||
node.info = make([]v1alpha1.InfoItem, 0)
|
||||
if reason != "" {
|
||||
node.info = append(node.info, v1alpha1.InfoItem{Name: "Status Reason", Value: reason})
|
||||
}
|
||||
|
||||
156
controller/cache/info_test.go
vendored
@@ -1,6 +1,8 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
@@ -50,6 +52,9 @@ func TestGetIngressInfo(t *testing.T) {
|
||||
node := &node{}
|
||||
populateNodeInfo(testIngress, node)
|
||||
assert.Equal(t, 0, len(node.info))
|
||||
sort.Slice(node.networkingInfo.TargetRefs, func(i, j int) bool {
|
||||
return strings.Compare(node.networkingInfo.TargetRefs[j].Name, node.networkingInfo.TargetRefs[i].Name) < 0
|
||||
})
|
||||
assert.Equal(t, &v1alpha1.ResourceNetworkingInfo{
|
||||
Ingress: []v1.LoadBalancerIngress{{IP: "107.178.210.11"}},
|
||||
TargetRefs: []v1alpha1.ResourceRef{{
|
||||
@@ -63,6 +68,155 @@ func TestGetIngressInfo(t *testing.T) {
|
||||
Kind: kube.ServiceKind,
|
||||
Name: "helm-guestbook",
|
||||
}},
|
||||
ExternalURLs: []string{"https://helm-guestbook.com"},
|
||||
ExternalURLs: []string{"https://helm-guestbook.com/"},
|
||||
}, node.networkingInfo)
|
||||
}
|
||||
|
||||
func TestGetIngressInfoNoHost(t *testing.T) {
|
||||
ingress := strToUnstructured(`
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
spec:
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: helm-guestbook
|
||||
servicePort: 443
|
||||
path: /
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
- ip: 107.178.210.11`)
|
||||
|
||||
node := &node{}
|
||||
populateNodeInfo(ingress, node)
|
||||
|
||||
assert.Equal(t, &v1alpha1.ResourceNetworkingInfo{
|
||||
Ingress: []v1.LoadBalancerIngress{{IP: "107.178.210.11"}},
|
||||
TargetRefs: []v1alpha1.ResourceRef{{
|
||||
Namespace: "default",
|
||||
Group: "",
|
||||
Kind: kube.ServiceKind,
|
||||
Name: "helm-guestbook",
|
||||
}},
|
||||
ExternalURLs: []string{"https://107.178.210.11/"},
|
||||
}, node.networkingInfo)
|
||||
}
|
||||
func TestExternalUrlWithSubPath(t *testing.T) {
|
||||
ingress := strToUnstructured(`
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
spec:
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: helm-guestbook
|
||||
servicePort: 443
|
||||
path: /my/sub/path/
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
- ip: 107.178.210.11`)
|
||||
|
||||
node := &node{}
|
||||
populateNodeInfo(ingress, node)
|
||||
|
||||
expectedExternalUrls := []string{"https://107.178.210.11/my/sub/path/"}
|
||||
assert.Equal(t, expectedExternalUrls, node.networkingInfo.ExternalURLs)
|
||||
}
|
||||
func TestExternalUrlWithMultipleSubPaths(t *testing.T) {
|
||||
ingress := strToUnstructured(`
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
spec:
|
||||
rules:
|
||||
- host: helm-guestbook.com
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: helm-guestbook
|
||||
servicePort: 443
|
||||
path: /my/sub/path/
|
||||
- backend:
|
||||
serviceName: helm-guestbook-2
|
||||
servicePort: 443
|
||||
path: /my/sub/path/2
|
||||
- backend:
|
||||
serviceName: helm-guestbook-3
|
||||
servicePort: 443
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
- ip: 107.178.210.11`)
|
||||
|
||||
node := &node{}
|
||||
populateNodeInfo(ingress, node)
|
||||
|
||||
expectedExternalUrls := []string{"https://helm-guestbook.com/my/sub/path/", "https://helm-guestbook.com/my/sub/path/2", "https://helm-guestbook.com"}
|
||||
actualURLs := node.networkingInfo.ExternalURLs
|
||||
sort.Strings(expectedExternalUrls)
|
||||
sort.Strings(actualURLs)
|
||||
assert.Equal(t, expectedExternalUrls, actualURLs)
|
||||
}
|
||||
func TestExternalUrlWithNoSubPath(t *testing.T) {
|
||||
ingress := strToUnstructured(`
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
spec:
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: helm-guestbook
|
||||
servicePort: 443
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
- ip: 107.178.210.11`)
|
||||
|
||||
node := &node{}
|
||||
populateNodeInfo(ingress, node)
|
||||
|
||||
expectedExternalUrls := []string{"https://107.178.210.11"}
|
||||
assert.Equal(t, expectedExternalUrls, node.networkingInfo.ExternalURLs)
|
||||
}
|
||||
|
||||
func TestExternalUrlWithNetworkingApi(t *testing.T) {
|
||||
ingress := strToUnstructured(`
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
spec:
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: helm-guestbook
|
||||
servicePort: 443
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
- ip: 107.178.210.11`)
|
||||
|
||||
node := &node{}
|
||||
populateNodeInfo(ingress, node)
|
||||
|
||||
expectedExternalUrls := []string{"https://107.178.210.11"}
|
||||
assert.Equal(t, expectedExternalUrls, node.networkingInfo.ExternalURLs)
|
||||
}
|
||||
|
||||
121
controller/cache/mocks/LiveStateCache.go
vendored
@@ -2,26 +2,39 @@
|
||||
|
||||
package mocks
|
||||
|
||||
import context "context"
|
||||
import kube "github.com/argoproj/argo-cd/util/kube"
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
import unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
import v1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
import (
|
||||
context "context"
|
||||
|
||||
metrics "github.com/argoproj/argo-cd/controller/metrics"
|
||||
kube "github.com/argoproj/argo-cd/util/kube"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
schema "k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
v1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
// LiveStateCache is an autogenerated mock type for the LiveStateCache type
|
||||
type LiveStateCache struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: server, obj
|
||||
func (_m *LiveStateCache) Delete(server string, obj *unstructured.Unstructured) error {
|
||||
ret := _m.Called(server, obj)
|
||||
// GetClustersInfo provides a mock function with given fields:
|
||||
func (_m *LiveStateCache) GetClustersInfo() []metrics.ClusterInfo {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, *unstructured.Unstructured) error); ok {
|
||||
r0 = rf(server, obj)
|
||||
var r0 []metrics.ClusterInfo
|
||||
if rf, ok := ret.Get(0).(func() []metrics.ClusterInfo); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]metrics.ClusterInfo)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
@@ -50,25 +63,78 @@ func (_m *LiveStateCache) GetManagedLiveObjs(a *v1alpha1.Application, targetObjs
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetNamespaceTopLevelResources provides a mock function with given fields: server, namespace
|
||||
func (_m *LiveStateCache) GetNamespaceTopLevelResources(server string, namespace string) (map[kube.ResourceKey]v1alpha1.ResourceNode, error) {
|
||||
ret := _m.Called(server, namespace)
|
||||
|
||||
var r0 map[kube.ResourceKey]v1alpha1.ResourceNode
|
||||
if rf, ok := ret.Get(0).(func(string, string) map[kube.ResourceKey]v1alpha1.ResourceNode); ok {
|
||||
r0 = rf(server, namespace)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[kube.ResourceKey]v1alpha1.ResourceNode)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, string) error); ok {
|
||||
r1 = rf(server, namespace)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetVersionsInfo provides a mock function with given fields: serverURL
|
||||
func (_m *LiveStateCache) GetVersionsInfo(serverURL string) (string, []v1.APIGroup, error) {
|
||||
ret := _m.Called(serverURL)
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func(string) string); ok {
|
||||
r0 = rf(serverURL)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
var r1 []v1.APIGroup
|
||||
if rf, ok := ret.Get(1).(func(string) []v1.APIGroup); ok {
|
||||
r1 = rf(serverURL)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).([]v1.APIGroup)
|
||||
}
|
||||
}
|
||||
|
||||
var r2 error
|
||||
if rf, ok := ret.Get(2).(func(string) error); ok {
|
||||
r2 = rf(serverURL)
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// Invalidate provides a mock function with given fields:
|
||||
func (_m *LiveStateCache) Invalidate() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// IsNamespaced provides a mock function with given fields: server, obj
|
||||
func (_m *LiveStateCache) IsNamespaced(server string, obj *unstructured.Unstructured) (bool, error) {
|
||||
ret := _m.Called(server, obj)
|
||||
// IsNamespaced provides a mock function with given fields: server, gk
|
||||
func (_m *LiveStateCache) IsNamespaced(server string, gk schema.GroupKind) (bool, error) {
|
||||
ret := _m.Called(server, gk)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(string, *unstructured.Unstructured) bool); ok {
|
||||
r0 = rf(server, obj)
|
||||
if rf, ok := ret.Get(0).(func(string, schema.GroupKind) bool); ok {
|
||||
r0 = rf(server, gk)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, *unstructured.Unstructured) error); ok {
|
||||
r1 = rf(server, obj)
|
||||
if rf, ok := ret.Get(1).(func(string, schema.GroupKind) error); ok {
|
||||
r1 = rf(server, gk)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
@@ -77,11 +143,11 @@ func (_m *LiveStateCache) IsNamespaced(server string, obj *unstructured.Unstruct
|
||||
}
|
||||
|
||||
// IterateHierarchy provides a mock function with given fields: server, key, action
|
||||
func (_m *LiveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(v1alpha1.ResourceNode)) error {
|
||||
func (_m *LiveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(v1alpha1.ResourceNode, string)) error {
|
||||
ret := _m.Called(server, key, action)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, kube.ResourceKey, func(v1alpha1.ResourceNode)) error); ok {
|
||||
if rf, ok := ret.Get(0).(func(string, kube.ResourceKey, func(v1alpha1.ResourceNode, string)) error); ok {
|
||||
r0 = rf(server, key, action)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
@@ -91,6 +157,15 @@ func (_m *LiveStateCache) IterateHierarchy(server string, key kube.ResourceKey,
|
||||
}
|
||||
|
||||
// Run provides a mock function with given fields: ctx
|
||||
func (_m *LiveStateCache) Run(ctx context.Context) {
|
||||
_m.Called(ctx)
|
||||
func (_m *LiveStateCache) Run(ctx context.Context) error {
|
||||
ret := _m.Called(ctx)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
|
||||
r0 = rf(ctx)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
20
controller/cache/node.go
vendored
@@ -35,9 +35,16 @@ func (n *node) resourceKey() kube.ResourceKey {
|
||||
}
|
||||
|
||||
func (n *node) isParentOf(child *node) bool {
|
||||
for _, ownerRef := range child.ownerRefs {
|
||||
ownerGvk := schema.FromAPIVersionAndKind(ownerRef.APIVersion, ownerRef.Kind)
|
||||
if kube.NewResourceKey(ownerGvk.Group, ownerRef.Kind, n.ref.Namespace, ownerRef.Name) == n.resourceKey() {
|
||||
for i, ownerRef := range child.ownerRefs {
|
||||
|
||||
// backfill UID of inferred owner child references
|
||||
if ownerRef.UID == "" && n.ref.Kind == ownerRef.Kind && n.ref.APIVersion == ownerRef.APIVersion && n.ref.Name == ownerRef.Name {
|
||||
ownerRef.UID = n.ref.UID
|
||||
child.ownerRefs[i] = ownerRef
|
||||
return true
|
||||
}
|
||||
|
||||
if n.ref.UID == ownerRef.UID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -100,10 +107,11 @@ func (n *node) asResourceNode() appv1.ResourceNode {
|
||||
for _, ownerRef := range n.ownerRefs {
|
||||
ownerGvk := schema.FromAPIVersionAndKind(ownerRef.APIVersion, ownerRef.Kind)
|
||||
ownerKey := kube.NewResourceKey(ownerGvk.Group, ownerRef.Kind, n.ref.Namespace, ownerRef.Name)
|
||||
parentRefs[0] = appv1.ResourceRef{Name: ownerRef.Name, Kind: ownerKey.Kind, Namespace: n.ref.Namespace, Group: ownerKey.Group}
|
||||
parentRefs[0] = appv1.ResourceRef{Name: ownerRef.Name, Kind: ownerKey.Kind, Namespace: n.ref.Namespace, Group: ownerKey.Group, UID: string(ownerRef.UID)}
|
||||
}
|
||||
return appv1.ResourceNode{
|
||||
ResourceRef: appv1.ResourceRef{
|
||||
UID: string(n.ref.UID),
|
||||
Name: n.ref.Name,
|
||||
Group: gv.Group,
|
||||
Version: gv.Version,
|
||||
@@ -119,14 +127,14 @@ func (n *node) asResourceNode() appv1.ResourceNode {
|
||||
}
|
||||
}
|
||||
|
||||
func (n *node) iterateChildren(ns map[kube.ResourceKey]*node, parents map[kube.ResourceKey]bool, action func(child appv1.ResourceNode)) {
|
||||
func (n *node) iterateChildren(ns map[kube.ResourceKey]*node, parents map[kube.ResourceKey]bool, action func(child appv1.ResourceNode, appName string)) {
|
||||
for childKey, child := range ns {
|
||||
if n.isParentOf(ns[childKey]) {
|
||||
if parents[childKey] {
|
||||
key := n.resourceKey()
|
||||
log.Warnf("Circular dependency detected. %s is child and parent of %s", childKey.String(), key.String())
|
||||
} else {
|
||||
action(child.asResourceNode())
|
||||
action(child.asResourceNode(), child.getApp(ns))
|
||||
child.iterateChildren(ns, newResourceKeySet(parents, n.resourceKey()), action)
|
||||
}
|
||||
}
|
||||
|
||||
62
controller/cache/node_test.go
vendored
@@ -3,12 +3,14 @@ package cache
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var c = &clusterInfo{settings: &settings.ArgoCDSettings{}}
|
||||
var c = &clusterInfo{cacheSettingsSrc: func() *cacheSettings {
|
||||
return &cacheSettings{AppInstanceLabelKey: common.LabelKeyAppInstance}
|
||||
}}
|
||||
|
||||
func TestIsParentOf(t *testing.T) {
|
||||
child := c.createObjInfo(testPod, "")
|
||||
@@ -19,11 +21,63 @@ func TestIsParentOf(t *testing.T) {
|
||||
assert.False(t, grandParent.isParentOf(child))
|
||||
}
|
||||
|
||||
func TestIsParentOfSameKindDifferentGroup(t *testing.T) {
|
||||
func TestIsParentOfSameKindDifferentGroupAndUID(t *testing.T) {
|
||||
rs := testRS.DeepCopy()
|
||||
rs.SetAPIVersion("somecrd.io/v1")
|
||||
rs.SetUID("123")
|
||||
child := c.createObjInfo(testPod, "")
|
||||
invalidParent := c.createObjInfo(rs, "")
|
||||
|
||||
assert.False(t, invalidParent.isParentOf(child))
|
||||
}
|
||||
|
||||
func TestIsServiceParentOfEndPointWithTheSameName(t *testing.T) {
|
||||
nonMatchingNameEndPoint := c.createObjInfo(strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: Endpoints
|
||||
metadata:
|
||||
name: not-matching-name
|
||||
namespace: default
|
||||
`), "")
|
||||
|
||||
matchingNameEndPoint := c.createObjInfo(strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: Endpoints
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
`), "")
|
||||
|
||||
parent := c.createObjInfo(testService, "")
|
||||
|
||||
assert.True(t, parent.isParentOf(matchingNameEndPoint))
|
||||
assert.Equal(t, parent.ref.UID, matchingNameEndPoint.ownerRefs[0].UID)
|
||||
assert.False(t, parent.isParentOf(nonMatchingNameEndPoint))
|
||||
}
|
||||
|
||||
func TestIsServiceAccoountParentOfSecret(t *testing.T) {
|
||||
serviceAccount := c.createObjInfo(strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: default
|
||||
namespace: default
|
||||
uid: '123'
|
||||
secrets:
|
||||
- name: default-token-123
|
||||
`), "")
|
||||
tokenSecret := c.createObjInfo(strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
annotations:
|
||||
kubernetes.io/service-account.name: default
|
||||
kubernetes.io/service-account.uid: '123'
|
||||
name: default-token-123
|
||||
namespace: default
|
||||
uid: '345'
|
||||
type: kubernetes.io/service-account-token
|
||||
`), "")
|
||||
|
||||
assert.True(t, serviceAccount.isParentOf(tokenSecret))
|
||||
}
|
||||
|
||||
99
controller/metrics/clustercollector.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
metricsCollectionInterval = 30 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
descClusterDefaultLabels = []string{"server"}
|
||||
|
||||
descClusterInfo = prometheus.NewDesc(
|
||||
"argocd_cluster_info",
|
||||
"Information about cluster.",
|
||||
append(descClusterDefaultLabels, "k8s_version"),
|
||||
nil,
|
||||
)
|
||||
descClusterCacheResources = prometheus.NewDesc(
|
||||
"argocd_cluster_api_resource_objects",
|
||||
"Number of k8s resource objects in the cache.",
|
||||
descClusterDefaultLabels,
|
||||
nil,
|
||||
)
|
||||
descClusterAPIs = prometheus.NewDesc(
|
||||
"argocd_cluster_api_resources",
|
||||
"Number of monitored kubernetes API resources.",
|
||||
descClusterDefaultLabels,
|
||||
nil,
|
||||
)
|
||||
descClusterCacheAgeSeconds = prometheus.NewDesc(
|
||||
"argocd_cluster_cache_age_seconds",
|
||||
"Cluster cache age in seconds.",
|
||||
descClusterDefaultLabels,
|
||||
nil,
|
||||
)
|
||||
)
|
||||
|
||||
type ClusterInfo struct {
|
||||
Server string
|
||||
K8SVersion string
|
||||
ResourcesCount int
|
||||
APIsCount int
|
||||
LastCacheSyncTime *time.Time
|
||||
}
|
||||
|
||||
type HasClustersInfo interface {
|
||||
GetClustersInfo() []ClusterInfo
|
||||
}
|
||||
|
||||
type clusterCollector struct {
|
||||
infoSource HasClustersInfo
|
||||
info []ClusterInfo
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func (c *clusterCollector) Run(ctx context.Context) {
|
||||
tick := time.Tick(metricsCollectionInterval)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break
|
||||
case <-tick:
|
||||
info := c.infoSource.GetClustersInfo()
|
||||
|
||||
c.lock.Lock()
|
||||
c.info = info
|
||||
c.lock.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Describe implements the prometheus.Collector interface
|
||||
func (c *clusterCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- descClusterInfo
|
||||
ch <- descClusterCacheResources
|
||||
ch <- descClusterAPIs
|
||||
ch <- descClusterCacheAgeSeconds
|
||||
}
|
||||
|
||||
func (c *clusterCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
now := time.Now()
|
||||
for _, c := range c.info {
|
||||
defaultValues := []string{c.Server}
|
||||
ch <- prometheus.MustNewConstMetric(descClusterInfo, prometheus.GaugeValue, 1, append(defaultValues, c.K8SVersion)...)
|
||||
ch <- prometheus.MustNewConstMetric(descClusterCacheResources, prometheus.GaugeValue, float64(c.ResourcesCount), defaultValues...)
|
||||
ch <- prometheus.MustNewConstMetric(descClusterAPIs, prometheus.GaugeValue, float64(c.APIsCount), defaultValues...)
|
||||
cacheAgeSeconds := -1
|
||||
if c.LastCacheSyncTime != nil {
|
||||
cacheAgeSeconds = int(now.Sub(*c.LastCacheSyncTime).Seconds())
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(descClusterCacheAgeSeconds, prometheus.GaugeValue, float64(cacheAgeSeconds), defaultValues...)
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
@@ -12,17 +15,27 @@ import (
|
||||
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
applister "github.com/argoproj/argo-cd/pkg/client/listers/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
"github.com/argoproj/argo-cd/util/healthz"
|
||||
)
|
||||
|
||||
type MetricsServer struct {
|
||||
*http.Server
|
||||
syncCounter *prometheus.CounterVec
|
||||
reconcileHistogram *prometheus.HistogramVec
|
||||
syncCounter *prometheus.CounterVec
|
||||
kubectlExecCounter *prometheus.CounterVec
|
||||
kubectlExecPendingGauge *prometheus.GaugeVec
|
||||
k8sRequestCounter *prometheus.CounterVec
|
||||
clusterEventsCounter *prometheus.CounterVec
|
||||
redisRequestCounter *prometheus.CounterVec
|
||||
reconcileHistogram *prometheus.HistogramVec
|
||||
redisRequestHistogram *prometheus.HistogramVec
|
||||
registry *prometheus.Registry
|
||||
}
|
||||
|
||||
const (
|
||||
// MetricsPath is the endpoint to collect application metrics
|
||||
MetricsPath = "/metrics"
|
||||
// EnvVarLegacyControllerMetrics is a env var to re-enable deprecated prometheus metrics
|
||||
EnvVarLegacyControllerMetrics = "ARGOCD_LEGACY_CONTROLLER_METRICS"
|
||||
)
|
||||
|
||||
// Follow Prometheus naming practices
|
||||
@@ -33,79 +46,185 @@ var (
|
||||
descAppInfo = prometheus.NewDesc(
|
||||
"argocd_app_info",
|
||||
"Information about application.",
|
||||
append(descAppDefaultLabels, "repo", "dest_server", "dest_namespace"),
|
||||
append(descAppDefaultLabels, "repo", "dest_server", "dest_namespace", "sync_status", "health_status", "operation"),
|
||||
nil,
|
||||
)
|
||||
// DEPRECATED
|
||||
descAppCreated = prometheus.NewDesc(
|
||||
"argocd_app_created_time",
|
||||
"Creation time in unix timestamp for an application.",
|
||||
descAppDefaultLabels,
|
||||
nil,
|
||||
)
|
||||
// DEPRECATED: superceded by sync_status label in argocd_app_info
|
||||
descAppSyncStatusCode = prometheus.NewDesc(
|
||||
"argocd_app_sync_status",
|
||||
"The application current sync status.",
|
||||
append(descAppDefaultLabels, "sync_status"),
|
||||
nil,
|
||||
)
|
||||
// DEPRECATED: superceded by health_status label in argocd_app_info
|
||||
descAppHealthStatus = prometheus.NewDesc(
|
||||
"argocd_app_health_status",
|
||||
"The application current health status.",
|
||||
append(descAppDefaultLabels, "health_status"),
|
||||
nil,
|
||||
)
|
||||
)
|
||||
|
||||
// NewMetricsServer returns a new prometheus server which collects application metrics
|
||||
func NewMetricsServer(addr string, appLister applister.ApplicationLister) *MetricsServer {
|
||||
mux := http.NewServeMux()
|
||||
appRegistry := NewAppRegistry(appLister)
|
||||
appRegistry.MustRegister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}))
|
||||
appRegistry.MustRegister(prometheus.NewGoCollector())
|
||||
mux.Handle(MetricsPath, promhttp.HandlerFor(appRegistry, promhttp.HandlerOpts{}))
|
||||
|
||||
syncCounter := prometheus.NewCounterVec(
|
||||
syncCounter = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "argocd_app_sync_total",
|
||||
Help: "Number of application syncs.",
|
||||
},
|
||||
append(descAppDefaultLabels, "phase"),
|
||||
append(descAppDefaultLabels, "dest_server", "phase"),
|
||||
)
|
||||
appRegistry.MustRegister(syncCounter)
|
||||
|
||||
reconcileHistogram := prometheus.NewHistogramVec(
|
||||
k8sRequestCounter = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "argocd_app_k8s_request_total",
|
||||
Help: "Number of kubernetes requests executed during application reconciliation.",
|
||||
},
|
||||
append(descAppDefaultLabels, "server", "response_code", "verb", "resource_kind", "resource_namespace"),
|
||||
)
|
||||
|
||||
kubectlExecCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "argocd_kubectl_exec_total",
|
||||
Help: "Number of kubectl executions",
|
||||
}, []string{"command"})
|
||||
|
||||
kubectlExecPendingGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "argocd_kubectl_exec_pending",
|
||||
Help: "Number of pending kubectl executions",
|
||||
}, []string{"command"})
|
||||
|
||||
reconcileHistogram = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "argocd_app_reconcile",
|
||||
Help: "Application reconciliation performance.",
|
||||
// Buckets chosen after observing a ~2100ms mean reconcile time
|
||||
Buckets: []float64{0.25, .5, 1, 2, 4, 8, 16},
|
||||
},
|
||||
append(descAppDefaultLabels),
|
||||
[]string{"namespace", "dest_server"},
|
||||
)
|
||||
|
||||
appRegistry.MustRegister(reconcileHistogram)
|
||||
clusterEventsCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "argocd_cluster_events_total",
|
||||
Help: "Number of processes k8s resource events.",
|
||||
}, append(descClusterDefaultLabels, "group", "kind"))
|
||||
|
||||
redisRequestCounter = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "argocd_redis_request_total",
|
||||
Help: "Number of kubernetes requests executed during application reconciliation.",
|
||||
},
|
||||
[]string{"initiator", "failed"},
|
||||
)
|
||||
|
||||
redisRequestHistogram = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "argocd_redis_request_duration",
|
||||
Help: "Redis requests duration.",
|
||||
Buckets: []float64{0.01, 0.05, 0.10, 0.25, .5, 1},
|
||||
},
|
||||
[]string{"initiator"},
|
||||
)
|
||||
)
|
||||
|
||||
// NewMetricsServer returns a new prometheus server which collects application metrics
|
||||
func NewMetricsServer(addr string, appLister applister.ApplicationLister, healthCheck func() error) *MetricsServer {
|
||||
mux := http.NewServeMux()
|
||||
registry := NewAppRegistry(appLister)
|
||||
mux.Handle(MetricsPath, promhttp.HandlerFor(prometheus.Gatherers{
|
||||
// contains app controller specific metrics
|
||||
registry,
|
||||
// contains process, golang and controller workqueues metrics
|
||||
prometheus.DefaultGatherer,
|
||||
}, promhttp.HandlerOpts{}))
|
||||
healthz.ServeHealthCheck(mux, healthCheck)
|
||||
|
||||
registry.MustRegister(syncCounter)
|
||||
registry.MustRegister(k8sRequestCounter)
|
||||
registry.MustRegister(kubectlExecCounter)
|
||||
registry.MustRegister(kubectlExecPendingGauge)
|
||||
registry.MustRegister(reconcileHistogram)
|
||||
registry.MustRegister(clusterEventsCounter)
|
||||
registry.MustRegister(redisRequestCounter)
|
||||
registry.MustRegister(redisRequestHistogram)
|
||||
|
||||
return &MetricsServer{
|
||||
registry: registry,
|
||||
Server: &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
},
|
||||
syncCounter: syncCounter,
|
||||
reconcileHistogram: reconcileHistogram,
|
||||
syncCounter: syncCounter,
|
||||
k8sRequestCounter: k8sRequestCounter,
|
||||
kubectlExecCounter: kubectlExecCounter,
|
||||
kubectlExecPendingGauge: kubectlExecPendingGauge,
|
||||
reconcileHistogram: reconcileHistogram,
|
||||
clusterEventsCounter: clusterEventsCounter,
|
||||
redisRequestCounter: redisRequestCounter,
|
||||
redisRequestHistogram: redisRequestHistogram,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MetricsServer) RegisterClustersInfoSource(ctx context.Context, source HasClustersInfo) {
|
||||
collector := &clusterCollector{infoSource: source}
|
||||
go collector.Run(ctx)
|
||||
m.registry.MustRegister(collector)
|
||||
}
|
||||
|
||||
// IncSync increments the sync counter for an application
|
||||
func (m *MetricsServer) IncSync(app *argoappv1.Application, state *argoappv1.OperationState) {
|
||||
if !state.Phase.Completed() {
|
||||
return
|
||||
}
|
||||
m.syncCounter.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject(), string(state.Phase)).Inc()
|
||||
m.syncCounter.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject(), app.Spec.Destination.Server, string(state.Phase)).Inc()
|
||||
}
|
||||
|
||||
func (m *MetricsServer) IncKubectlExec(command string) {
|
||||
m.kubectlExecCounter.WithLabelValues(command).Inc()
|
||||
}
|
||||
|
||||
func (m *MetricsServer) IncKubectlExecPending(command string) {
|
||||
m.kubectlExecPendingGauge.WithLabelValues(command).Inc()
|
||||
}
|
||||
|
||||
func (m *MetricsServer) DecKubectlExecPending(command string) {
|
||||
m.kubectlExecPendingGauge.WithLabelValues(command).Dec()
|
||||
}
|
||||
|
||||
// IncClusterEventsCount increments the number of cluster events
|
||||
func (m *MetricsServer) IncClusterEventsCount(server, group, kind string) {
|
||||
m.clusterEventsCounter.WithLabelValues(server, group, kind).Inc()
|
||||
}
|
||||
|
||||
// IncKubernetesRequest increments the kubernetes requests counter for an application
|
||||
func (m *MetricsServer) IncKubernetesRequest(app *argoappv1.Application, server, statusCode, verb, resourceKind, resourceNamespace string) {
|
||||
var namespace, name, project string
|
||||
if app != nil {
|
||||
namespace = app.Namespace
|
||||
name = app.Name
|
||||
project = app.Spec.GetProject()
|
||||
}
|
||||
m.k8sRequestCounter.WithLabelValues(
|
||||
namespace, name, project, server, statusCode,
|
||||
verb, resourceKind, resourceNamespace,
|
||||
).Inc()
|
||||
}
|
||||
|
||||
func (m *MetricsServer) IncRedisRequest(failed bool) {
|
||||
m.redisRequestCounter.WithLabelValues("argocd-application-controller", strconv.FormatBool(failed)).Inc()
|
||||
}
|
||||
|
||||
// ObserveRedisRequestDuration observes redis request duration
|
||||
func (m *MetricsServer) ObserveRedisRequestDuration(duration time.Duration) {
|
||||
m.redisRequestHistogram.WithLabelValues("argocd-application-controller").Observe(duration.Seconds())
|
||||
}
|
||||
|
||||
// IncReconcile increments the reconcile counter for an application
|
||||
func (m *MetricsServer) IncReconcile(app *argoappv1.Application, duration time.Duration) {
|
||||
m.reconcileHistogram.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject()).Observe(duration.Seconds())
|
||||
m.reconcileHistogram.WithLabelValues(app.Namespace, app.Spec.Destination.Server).Observe(duration.Seconds())
|
||||
}
|
||||
|
||||
type appCollector struct {
|
||||
@@ -129,7 +248,6 @@ func NewAppRegistry(appLister applister.ApplicationLister) *prometheus.Registry
|
||||
// Describe implements the prometheus.Collector interface
|
||||
func (c *appCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- descAppInfo
|
||||
ch <- descAppCreated
|
||||
ch <- descAppSyncStatusCode
|
||||
ch <- descAppHealthStatus
|
||||
}
|
||||
@@ -163,20 +281,36 @@ func collectApps(ch chan<- prometheus.Metric, app *argoappv1.Application) {
|
||||
addConstMetric(desc, prometheus.GaugeValue, v, lv...)
|
||||
}
|
||||
|
||||
addGauge(descAppInfo, 1, git.NormalizeGitURL(app.Spec.Source.RepoURL), app.Spec.Destination.Server, app.Spec.Destination.Namespace)
|
||||
|
||||
addGauge(descAppCreated, float64(app.CreationTimestamp.Unix()))
|
||||
|
||||
var operation string
|
||||
if app.DeletionTimestamp != nil {
|
||||
operation = "delete"
|
||||
} else if app.Operation != nil && app.Operation.Sync != nil {
|
||||
operation = "sync"
|
||||
}
|
||||
syncStatus := app.Status.Sync.Status
|
||||
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeSynced), string(argoappv1.SyncStatusCodeSynced))
|
||||
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeOutOfSync), string(argoappv1.SyncStatusCodeOutOfSync))
|
||||
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeUnknown || syncStatus == ""), string(argoappv1.SyncStatusCodeUnknown))
|
||||
|
||||
if syncStatus == "" {
|
||||
syncStatus = argoappv1.SyncStatusCodeUnknown
|
||||
}
|
||||
healthStatus := app.Status.Health.Status
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusUnknown || healthStatus == ""), argoappv1.HealthStatusUnknown)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusProgressing), argoappv1.HealthStatusProgressing)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusSuspended), argoappv1.HealthStatusSuspended)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusHealthy), argoappv1.HealthStatusHealthy)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusDegraded), argoappv1.HealthStatusDegraded)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusMissing), argoappv1.HealthStatusMissing)
|
||||
if healthStatus == "" {
|
||||
healthStatus = argoappv1.HealthStatusUnknown
|
||||
}
|
||||
|
||||
addGauge(descAppInfo, 1, git.NormalizeGitURL(app.Spec.Source.RepoURL), app.Spec.Destination.Server, app.Spec.Destination.Namespace, string(syncStatus), healthStatus, operation)
|
||||
|
||||
// Deprecated controller metrics
|
||||
if os.Getenv(EnvVarLegacyControllerMetrics) == "true" {
|
||||
addGauge(descAppCreated, float64(app.CreationTimestamp.Unix()))
|
||||
|
||||
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeSynced), string(argoappv1.SyncStatusCodeSynced))
|
||||
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeOutOfSync), string(argoappv1.SyncStatusCodeOutOfSync))
|
||||
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeUnknown || syncStatus == ""), string(argoappv1.SyncStatusCodeUnknown))
|
||||
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusUnknown || healthStatus == ""), argoappv1.HealthStatusUnknown)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusProgressing), argoappv1.HealthStatusProgressing)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusSuspended), argoappv1.HealthStatusSuspended)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusHealthy), argoappv1.HealthStatusHealthy)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusDegraded), argoappv1.HealthStatusDegraded)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusMissing), argoappv1.HealthStatusMissing)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -42,25 +43,52 @@ status:
|
||||
status: Healthy
|
||||
`
|
||||
|
||||
const expectedResponse = `# HELP argocd_app_created_time Creation time in unix timestamp for an application.
|
||||
# TYPE argocd_app_created_time gauge
|
||||
argocd_app_created_time{name="my-app",namespace="argocd",project="important-project"} -6.21355968e+10
|
||||
# HELP argocd_app_health_status The application current health status.
|
||||
# TYPE argocd_app_health_status gauge
|
||||
argocd_app_health_status{health_status="Degraded",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
argocd_app_health_status{health_status="Healthy",name="my-app",namespace="argocd",project="important-project"} 1
|
||||
argocd_app_health_status{health_status="Missing",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
argocd_app_health_status{health_status="Progressing",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
argocd_app_health_status{health_status="Suspended",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
argocd_app_health_status{health_status="Unknown",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
# HELP argocd_app_info Information about application.
|
||||
# TYPE argocd_app_info gauge
|
||||
argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",name="my-app",namespace="argocd",project="important-project",repo="https://github.com/argoproj/argocd-example-apps"} 1
|
||||
# HELP argocd_app_sync_status The application current sync status.
|
||||
# TYPE argocd_app_sync_status gauge
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="OutOfSync"} 0
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="Synced"} 1
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="Unknown"} 0
|
||||
const fakeApp2 = `
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: my-app-2
|
||||
namespace: argocd
|
||||
spec:
|
||||
destination:
|
||||
namespace: dummy-namespace
|
||||
server: https://localhost:6443
|
||||
project: important-project
|
||||
source:
|
||||
path: some/path
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps.git
|
||||
status:
|
||||
sync:
|
||||
status: Synced
|
||||
health:
|
||||
status: Healthy
|
||||
operation:
|
||||
sync:
|
||||
revision: 041eab7439ece92c99b043f0e171788185b8fc1d
|
||||
syncStrategy:
|
||||
hook: {}
|
||||
`
|
||||
|
||||
const fakeApp3 = `
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: my-app-3
|
||||
namespace: argocd
|
||||
deletionTimestamp: "2020-03-16T09:17:45Z"
|
||||
spec:
|
||||
destination:
|
||||
namespace: dummy-namespace
|
||||
server: https://localhost:6443
|
||||
project: important-project
|
||||
source:
|
||||
path: some/path
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps.git
|
||||
status:
|
||||
sync:
|
||||
status: OutOfSync
|
||||
health:
|
||||
status: Degraded
|
||||
`
|
||||
|
||||
const fakeDefaultApp = `
|
||||
@@ -83,42 +111,26 @@ status:
|
||||
status: Healthy
|
||||
`
|
||||
|
||||
const expectedDefaultResponse = `# HELP argocd_app_created_time Creation time in unix timestamp for an application.
|
||||
# TYPE argocd_app_created_time gauge
|
||||
argocd_app_created_time{name="my-app",namespace="argocd",project="default"} -6.21355968e+10
|
||||
# HELP argocd_app_health_status The application current health status.
|
||||
# TYPE argocd_app_health_status gauge
|
||||
argocd_app_health_status{health_status="Degraded",name="my-app",namespace="argocd",project="default"} 0
|
||||
argocd_app_health_status{health_status="Healthy",name="my-app",namespace="argocd",project="default"} 1
|
||||
argocd_app_health_status{health_status="Missing",name="my-app",namespace="argocd",project="default"} 0
|
||||
argocd_app_health_status{health_status="Progressing",name="my-app",namespace="argocd",project="default"} 0
|
||||
argocd_app_health_status{health_status="Suspended",name="my-app",namespace="argocd",project="default"} 0
|
||||
argocd_app_health_status{health_status="Unknown",name="my-app",namespace="argocd",project="default"} 0
|
||||
# HELP argocd_app_info Information about application.
|
||||
# TYPE argocd_app_info gauge
|
||||
argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",name="my-app",namespace="argocd",project="default",repo="https://github.com/argoproj/argocd-example-apps"} 1
|
||||
# HELP argocd_app_sync_status The application current sync status.
|
||||
# TYPE argocd_app_sync_status gauge
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="default",sync_status="OutOfSync"} 0
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="default",sync_status="Synced"} 1
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="default",sync_status="Unknown"} 0
|
||||
`
|
||||
var noOpHealthCheck = func() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newFakeApp(fakeApp string) *argoappv1.Application {
|
||||
func newFakeApp(fakeAppYAML string) *argoappv1.Application {
|
||||
var app argoappv1.Application
|
||||
err := yaml.Unmarshal([]byte(fakeApp), &app)
|
||||
err := yaml.Unmarshal([]byte(fakeAppYAML), &app)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &app
|
||||
}
|
||||
|
||||
func newFakeLister(fakeApp ...string) (context.CancelFunc, applister.ApplicationLister) {
|
||||
func newFakeLister(fakeAppYAMLs ...string) (context.CancelFunc, applister.ApplicationLister) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
var fakeApps []runtime.Object
|
||||
for _, name := range fakeApp {
|
||||
fakeApps = append(fakeApps, newFakeApp(name))
|
||||
for _, appYAML := range fakeAppYAMLs {
|
||||
a := newFakeApp(appYAML)
|
||||
fakeApps = append(fakeApps, a)
|
||||
}
|
||||
appClientset := appclientset.NewSimpleClientset(fakeApps...)
|
||||
factory := appinformer.NewFilteredSharedInformerFactory(appClientset, 0, "argocd", func(options *metav1.ListOptions) {})
|
||||
@@ -130,10 +142,10 @@ func newFakeLister(fakeApp ...string) (context.CancelFunc, applister.Application
|
||||
return cancel, factory.Argoproj().V1alpha1().Applications().Lister()
|
||||
}
|
||||
|
||||
func testApp(t *testing.T, fakeApp string, expectedResponse string) {
|
||||
cancel, appLister := newFakeLister(fakeApp)
|
||||
func testApp(t *testing.T, fakeAppYAMLs []string, expectedResponse string) {
|
||||
cancel, appLister := newFakeLister(fakeAppYAMLs...)
|
||||
defer cancel()
|
||||
metricsServ := NewMetricsServer("localhost:8082", appLister)
|
||||
metricsServ := NewMetricsServer("localhost:8082", appLister, noOpHealthCheck)
|
||||
req, err := http.NewRequest("GET", "/metrics", nil)
|
||||
assert.NoError(t, err)
|
||||
rr := httptest.NewRecorder()
|
||||
@@ -145,38 +157,74 @@ func testApp(t *testing.T, fakeApp string, expectedResponse string) {
|
||||
}
|
||||
|
||||
type testCombination struct {
|
||||
application string
|
||||
applications []string
|
||||
expectedResponse string
|
||||
}
|
||||
|
||||
func TestMetrics(t *testing.T) {
|
||||
combinations := []testCombination{
|
||||
{
|
||||
application: fakeApp,
|
||||
expectedResponse: expectedResponse,
|
||||
applications: []string{fakeApp, fakeApp2, fakeApp3},
|
||||
expectedResponse: `
|
||||
# HELP argocd_app_info Information about application.
|
||||
# TYPE argocd_app_info gauge
|
||||
argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",health_status="Degraded",name="my-app-3",namespace="argocd",operation="delete",project="important-project",repo="https://github.com/argoproj/argocd-example-apps",sync_status="OutOfSync"} 1
|
||||
argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",health_status="Healthy",name="my-app",namespace="argocd",operation="",project="important-project",repo="https://github.com/argoproj/argocd-example-apps",sync_status="Synced"} 1
|
||||
argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",health_status="Healthy",name="my-app-2",namespace="argocd",operation="sync",project="important-project",repo="https://github.com/argoproj/argocd-example-apps",sync_status="Synced"} 1
|
||||
`,
|
||||
},
|
||||
{
|
||||
application: fakeDefaultApp,
|
||||
expectedResponse: expectedDefaultResponse,
|
||||
applications: []string{fakeDefaultApp},
|
||||
expectedResponse: `
|
||||
# HELP argocd_app_info Information about application.
|
||||
# TYPE argocd_app_info gauge
|
||||
argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",health_status="Healthy",name="my-app",namespace="argocd",operation="",project="default",repo="https://github.com/argoproj/argocd-example-apps",sync_status="Synced"} 1
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, combination := range combinations {
|
||||
testApp(t, combination.application, combination.expectedResponse)
|
||||
testApp(t, combination.applications, combination.expectedResponse)
|
||||
}
|
||||
}
|
||||
|
||||
const appSyncTotal = `# HELP argocd_app_sync_total Number of application syncs.
|
||||
# TYPE argocd_app_sync_total counter
|
||||
argocd_app_sync_total{name="my-app",namespace="argocd",phase="Error",project="important-project"} 1
|
||||
argocd_app_sync_total{name="my-app",namespace="argocd",phase="Failed",project="important-project"} 1
|
||||
argocd_app_sync_total{name="my-app",namespace="argocd",phase="Succeeded",project="important-project"} 2
|
||||
func TestLegacyMetrics(t *testing.T) {
|
||||
os.Setenv(EnvVarLegacyControllerMetrics, "true")
|
||||
defer os.Unsetenv(EnvVarLegacyControllerMetrics)
|
||||
|
||||
expectedResponse := `
|
||||
# HELP argocd_app_created_time Creation time in unix timestamp for an application.
|
||||
# TYPE argocd_app_created_time gauge
|
||||
argocd_app_created_time{name="my-app",namespace="argocd",project="important-project"} -6.21355968e+10
|
||||
# HELP argocd_app_health_status The application current health status.
|
||||
# TYPE argocd_app_health_status gauge
|
||||
argocd_app_health_status{health_status="Degraded",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
argocd_app_health_status{health_status="Healthy",name="my-app",namespace="argocd",project="important-project"} 1
|
||||
argocd_app_health_status{health_status="Missing",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
argocd_app_health_status{health_status="Progressing",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
argocd_app_health_status{health_status="Suspended",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
argocd_app_health_status{health_status="Unknown",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
# HELP argocd_app_sync_status The application current sync status.
|
||||
# TYPE argocd_app_sync_status gauge
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="OutOfSync"} 0
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="Synced"} 1
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="Unknown"} 0
|
||||
`
|
||||
testApp(t, []string{fakeApp}, expectedResponse)
|
||||
}
|
||||
|
||||
func TestMetricsSyncCounter(t *testing.T) {
|
||||
cancel, appLister := newFakeLister()
|
||||
defer cancel()
|
||||
metricsServ := NewMetricsServer("localhost:8082", appLister)
|
||||
metricsServ := NewMetricsServer("localhost:8082", appLister, noOpHealthCheck)
|
||||
|
||||
appSyncTotal := `
|
||||
# HELP argocd_app_sync_total Number of application syncs.
|
||||
# TYPE argocd_app_sync_total counter
|
||||
argocd_app_sync_total{dest_server="https://localhost:6443",name="my-app",namespace="argocd",phase="Error",project="important-project"} 1
|
||||
argocd_app_sync_total{dest_server="https://localhost:6443",name="my-app",namespace="argocd",phase="Failed",project="important-project"} 1
|
||||
argocd_app_sync_total{dest_server="https://localhost:6443",name="my-app",namespace="argocd",phase="Succeeded",project="important-project"} 2
|
||||
`
|
||||
|
||||
fakeApp := newFakeApp(fakeApp)
|
||||
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: argoappv1.OperationRunning})
|
||||
@@ -198,27 +246,31 @@ func TestMetricsSyncCounter(t *testing.T) {
|
||||
// assertMetricsPrinted asserts every line in the expected lines appears in the body
|
||||
func assertMetricsPrinted(t *testing.T, expectedLines, body string) {
|
||||
for _, line := range strings.Split(expectedLines, "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
assert.Contains(t, body, line)
|
||||
}
|
||||
}
|
||||
|
||||
const appReconcileMetrics = `argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="0.25"} 0
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="0.5"} 0
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="1"} 0
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="2"} 0
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="4"} 0
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="8"} 1
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="16"} 1
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="+Inf"} 1
|
||||
argocd_app_reconcile_sum{name="my-app",namespace="argocd",project="important-project"} 5
|
||||
argocd_app_reconcile_count{name="my-app",namespace="argocd",project="important-project"} 1
|
||||
`
|
||||
|
||||
func TestReconcileMetrics(t *testing.T) {
|
||||
cancel, appLister := newFakeLister()
|
||||
defer cancel()
|
||||
metricsServ := NewMetricsServer("localhost:8082", appLister)
|
||||
|
||||
metricsServ := NewMetricsServer("localhost:8082", appLister, noOpHealthCheck)
|
||||
appReconcileMetrics := `
|
||||
# HELP argocd_app_reconcile Application reconciliation performance.
|
||||
# TYPE argocd_app_reconcile histogram
|
||||
argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="0.25"} 0
|
||||
argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="0.5"} 0
|
||||
argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="1"} 0
|
||||
argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="2"} 0
|
||||
argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="4"} 0
|
||||
argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="8"} 1
|
||||
argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="16"} 1
|
||||
argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="+Inf"} 1
|
||||
argocd_app_reconcile_sum{dest_server="https://localhost:6443",namespace="argocd"} 5
|
||||
argocd_app_reconcile_count{dest_server="https://localhost:6443",namespace="argocd"} 1
|
||||
`
|
||||
fakeApp := newFakeApp(fakeApp)
|
||||
metricsServ.IncReconcile(fakeApp, 5*time.Second)
|
||||
|
||||
|
||||
24
controller/metrics/transportwrapper.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/argoproj/pkg/kubeclientmetrics"
|
||||
"k8s.io/client-go/rest"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
// AddMetricsTransportWrapper adds a transport wrapper which increments 'argocd_app_k8s_request_total' counter on each kubernetes request
|
||||
func AddMetricsTransportWrapper(server *MetricsServer, app *v1alpha1.Application, config *rest.Config) *rest.Config {
|
||||
inc := func(resourceInfo kubeclientmetrics.ResourceInfo) error {
|
||||
namespace := resourceInfo.Namespace
|
||||
kind := resourceInfo.Kind
|
||||
statusCode := strconv.Itoa(resourceInfo.StatusCode)
|
||||
server.IncKubernetesRequest(app, resourceInfo.Server, statusCode, string(resourceInfo.Verb), kind, namespace)
|
||||
return nil
|
||||
}
|
||||
|
||||
newConfig := kubeclientmetrics.AddMetricsTransportWrapper(config, inc)
|
||||
return newConfig
|
||||
}
|
||||
@@ -7,18 +7,20 @@ import (
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/yudai/gojsondiff"
|
||||
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/client-go/tools/cache"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
statecache "github.com/argoproj/argo-cd/controller/cache"
|
||||
"github.com/argoproj/argo-cd/controller/metrics"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
"github.com/argoproj/argo-cd/reposerver"
|
||||
"github.com/argoproj/argo-cd/reposerver/repository"
|
||||
"github.com/argoproj/argo-cd/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/argo"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
@@ -26,7 +28,10 @@ import (
|
||||
"github.com/argoproj/argo-cd/util/health"
|
||||
hookutil "github.com/argoproj/argo-cd/util/hook"
|
||||
kubeutil "github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/resource"
|
||||
"github.com/argoproj/argo-cd/util/resource/ignore"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
)
|
||||
|
||||
type managedResource struct {
|
||||
@@ -50,45 +55,62 @@ func GetLiveObjs(res []managedResource) []*unstructured.Unstructured {
|
||||
}
|
||||
|
||||
type ResourceInfoProvider interface {
|
||||
IsNamespaced(server string, obj *unstructured.Unstructured) (bool, error)
|
||||
IsNamespaced(server string, gk schema.GroupKind) (bool, error)
|
||||
}
|
||||
|
||||
// AppStateManager defines methods which allow to compare application spec and actual application state.
|
||||
type AppStateManager interface {
|
||||
CompareAppState(app *v1alpha1.Application, revision string, source v1alpha1.ApplicationSource, noCache bool) (*comparisonResult, error)
|
||||
CompareAppState(app *v1alpha1.Application, project *appv1.AppProject, revision string, source v1alpha1.ApplicationSource, noCache bool, localObjects []string) *comparisonResult
|
||||
SyncAppState(app *v1alpha1.Application, state *v1alpha1.OperationState)
|
||||
}
|
||||
|
||||
type comparisonResult struct {
|
||||
reconciledAt metav1.Time
|
||||
syncStatus *v1alpha1.SyncStatus
|
||||
healthStatus *v1alpha1.HealthStatus
|
||||
resources []v1alpha1.ResourceStatus
|
||||
managedResources []managedResource
|
||||
conditions []v1alpha1.ApplicationCondition
|
||||
hooks []*unstructured.Unstructured
|
||||
diffNormalizer diff.Normalizer
|
||||
appSourceType v1alpha1.ApplicationSourceType
|
||||
// timings maps phases of comparison to the duration it took to complete (for statistical purposes)
|
||||
timings map[string]time.Duration
|
||||
}
|
||||
|
||||
func (cr *comparisonResult) targetObjs() []*unstructured.Unstructured {
|
||||
objs := cr.hooks
|
||||
for _, r := range cr.managedResources {
|
||||
if r.Target != nil {
|
||||
objs = append(objs, r.Target)
|
||||
}
|
||||
}
|
||||
return objs
|
||||
}
|
||||
|
||||
// appStateManager allows to compare applications to git
|
||||
type appStateManager struct {
|
||||
metricsServer *metrics.MetricsServer
|
||||
db db.ArgoDB
|
||||
settings *settings.ArgoCDSettings
|
||||
settingsMgr *settings.SettingsManager
|
||||
appclientset appclientset.Interface
|
||||
projInformer cache.SharedIndexInformer
|
||||
kubectl kubeutil.Kubectl
|
||||
repoClientset reposerver.Clientset
|
||||
repoClientset apiclient.Clientset
|
||||
liveStateCache statecache.LiveStateCache
|
||||
namespace string
|
||||
}
|
||||
|
||||
func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1.ApplicationSource, appLabelKey, revision string, noCache bool) ([]*unstructured.Unstructured, []*unstructured.Unstructured, *repository.ManifestResponse, error) {
|
||||
helmRepos, err := m.db.ListHelmRepos(context.Background())
|
||||
func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1.ApplicationSource, appLabelKey, revision string, noCache bool) ([]*unstructured.Unstructured, []*unstructured.Unstructured, *apiclient.ManifestResponse, error) {
|
||||
ts := stats.NewTimingStats()
|
||||
helmRepos, err := m.db.ListHelmRepositories(context.Background())
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
repo := m.getRepo(source.RepoURL)
|
||||
ts.AddCheckpoint("helm_ms")
|
||||
repo, err := m.db.GetRepository(context.Background(), source.RepoURL)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
ts.AddCheckpoint("repo_ms")
|
||||
conn, repoClient, err := m.repoClientset.NewRepoServerClient()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
@@ -99,14 +121,33 @@ func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1
|
||||
revision = source.TargetRevision
|
||||
}
|
||||
|
||||
tools := make([]*appv1.ConfigManagementPlugin, len(m.settings.ConfigManagementPlugins))
|
||||
for i := range m.settings.ConfigManagementPlugins {
|
||||
tools[i] = &m.settings.ConfigManagementPlugins[i]
|
||||
plugins, err := m.settingsMgr.GetConfigManagementPlugins()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
ts.AddCheckpoint("plugins_ms")
|
||||
tools := make([]*appv1.ConfigManagementPlugin, len(plugins))
|
||||
for i := range plugins {
|
||||
tools[i] = &plugins[i]
|
||||
}
|
||||
|
||||
manifestInfo, err := repoClient.GenerateManifest(context.Background(), &repository.ManifestRequest{
|
||||
kustomizeSettings, err := m.settingsMgr.GetKustomizeSettings()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
kustomizeOptions, err := kustomizeSettings.GetOptions(app.Spec.Source)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
ts.AddCheckpoint("build_options_ms")
|
||||
serverVersion, apiGroups, err := m.liveStateCache.GetVersionsInfo(app.Spec.Destination.Server)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
ts.AddCheckpoint("version_ms")
|
||||
manifestInfo, err := repoClient.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: repo,
|
||||
HelmRepos: helmRepos,
|
||||
Repos: helmRepos,
|
||||
Revision: revision,
|
||||
NoCache: noCache,
|
||||
AppLabelKey: appLabelKey,
|
||||
@@ -114,17 +155,38 @@ func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1
|
||||
Namespace: app.Spec.Destination.Namespace,
|
||||
ApplicationSource: &source,
|
||||
Plugins: tools,
|
||||
KustomizeOptions: kustomizeOptions,
|
||||
KubeVersion: serverVersion,
|
||||
ApiVersions: argo.APIGroupsToVersions(apiGroups),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
ts.AddCheckpoint("manifests_ms")
|
||||
targetObjs, hooks, err := unmarshalManifests(manifestInfo.Manifests)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
ts.AddCheckpoint("unmarshal_ms")
|
||||
logCtx := log.WithField("application", app.Name)
|
||||
for k, v := range ts.Timings() {
|
||||
logCtx = logCtx.WithField(k, v.Milliseconds())
|
||||
}
|
||||
logCtx = logCtx.WithField("time_ms", time.Since(ts.StartTime).Milliseconds())
|
||||
logCtx.Info("getRepoObjs stats")
|
||||
return targetObjs, hooks, manifestInfo, nil
|
||||
}
|
||||
|
||||
func unmarshalManifests(manifests []string) ([]*unstructured.Unstructured, []*unstructured.Unstructured, error) {
|
||||
targetObjs := make([]*unstructured.Unstructured, 0)
|
||||
hooks := make([]*unstructured.Unstructured, 0)
|
||||
for _, manifest := range manifestInfo.Manifests {
|
||||
for _, manifest := range manifests {
|
||||
obj, err := v1alpha1.UnmarshalToUnstructured(manifest)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
if obj == nil || ignore.Ignore(obj) {
|
||||
continue
|
||||
}
|
||||
if hookutil.IsHook(obj) {
|
||||
hooks = append(hooks, obj)
|
||||
@@ -132,7 +194,7 @@ func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1
|
||||
targetObjs = append(targetObjs, obj)
|
||||
}
|
||||
}
|
||||
return targetObjs, hooks, manifestInfo, nil
|
||||
return targetObjs, hooks, nil
|
||||
}
|
||||
|
||||
func DeduplicateTargetObjects(
|
||||
@@ -145,7 +207,7 @@ func DeduplicateTargetObjects(
|
||||
targetByKey := make(map[kubeutil.ResourceKey][]*unstructured.Unstructured)
|
||||
for i := range objs {
|
||||
obj := objs[i]
|
||||
isNamespaced, err := infoProvider.IsNamespaced(server, obj)
|
||||
isNamespaced, err := infoProvider.IsNamespaced(server, obj.GroupVersionKind().GroupKind())
|
||||
if err != nil {
|
||||
return objs, nil, err
|
||||
}
|
||||
@@ -161,9 +223,11 @@ func DeduplicateTargetObjects(
|
||||
result := make([]*unstructured.Unstructured, 0)
|
||||
for key, targets := range targetByKey {
|
||||
if len(targets) > 1 {
|
||||
now := metav1.Now()
|
||||
conditions = append(conditions, appv1.ApplicationCondition{
|
||||
Type: appv1.ApplicationConditionRepeatedResourceWarning,
|
||||
Message: fmt.Sprintf("Resource %s appeared %d times among application resources.", key.String(), len(targets)),
|
||||
Type: appv1.ApplicationConditionRepeatedResourceWarning,
|
||||
Message: fmt.Sprintf("Resource %s appeared %d times among application resources.", key.String(), len(targets)),
|
||||
LastTransitionTime: &now,
|
||||
})
|
||||
}
|
||||
result = append(result, targets[len(targets)-1])
|
||||
@@ -172,47 +236,152 @@ func DeduplicateTargetObjects(
|
||||
return result, conditions, nil
|
||||
}
|
||||
|
||||
// dedupLiveResources handles removes live resource duplicates with the same UID. Duplicates are created in a separate resource groups.
|
||||
// E.g. apps/Deployment produces duplicate in extensions/Deployment, authorization.openshift.io/ClusterRole produces duplicate in rbac.authorization.k8s.io/ClusterRole etc.
|
||||
// The method removes such duplicates unless it was defined in git ( exists in target resources list ). At least one duplicate stays.
|
||||
// If non of duplicates are in git at random one stays
|
||||
func dedupLiveResources(targetObjs []*unstructured.Unstructured, liveObjsByKey map[kubeutil.ResourceKey]*unstructured.Unstructured) {
|
||||
targetObjByKey := make(map[kubeutil.ResourceKey]*unstructured.Unstructured)
|
||||
for i := range targetObjs {
|
||||
targetObjByKey[kubeutil.GetResourceKey(targetObjs[i])] = targetObjs[i]
|
||||
}
|
||||
liveObjsById := make(map[types.UID][]*unstructured.Unstructured)
|
||||
for k := range liveObjsByKey {
|
||||
obj := liveObjsByKey[k]
|
||||
if obj != nil {
|
||||
liveObjsById[obj.GetUID()] = append(liveObjsById[obj.GetUID()], obj)
|
||||
}
|
||||
}
|
||||
for id := range liveObjsById {
|
||||
objs := liveObjsById[id]
|
||||
|
||||
if len(objs) > 1 {
|
||||
duplicatesLeft := len(objs)
|
||||
for i := range objs {
|
||||
obj := objs[i]
|
||||
resourceKey := kubeutil.GetResourceKey(obj)
|
||||
if _, ok := targetObjByKey[resourceKey]; !ok {
|
||||
delete(liveObjsByKey, resourceKey)
|
||||
duplicatesLeft--
|
||||
if duplicatesLeft == 1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *appStateManager) getComparisonSettings(app *appv1.Application) (string, map[string]v1alpha1.ResourceOverride, diff.Normalizer, *settings.ResourcesFilter, error) {
|
||||
resourceOverrides, err := m.settingsMgr.GetResourceOverrides()
|
||||
if err != nil {
|
||||
return "", nil, nil, nil, err
|
||||
}
|
||||
appLabelKey, err := m.settingsMgr.GetAppInstanceLabelKey()
|
||||
if err != nil {
|
||||
return "", nil, nil, nil, err
|
||||
}
|
||||
diffNormalizer, err := argo.NewDiffNormalizer(app.Spec.IgnoreDifferences, resourceOverrides)
|
||||
if err != nil {
|
||||
return "", nil, nil, nil, err
|
||||
}
|
||||
resFilter, err := m.settingsMgr.GetResourcesFilter()
|
||||
if err != nil {
|
||||
return "", nil, nil, nil, err
|
||||
}
|
||||
return appLabelKey, resourceOverrides, diffNormalizer, resFilter, nil
|
||||
}
|
||||
|
||||
// CompareAppState compares application git state to the live app state, using the specified
|
||||
// revision and supplied source. If revision or overrides are empty, then compares against
|
||||
// revision and overrides in the app spec.
|
||||
func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision string, source v1alpha1.ApplicationSource, noCache bool) (*comparisonResult, error) {
|
||||
diffNormalizer, err := argo.NewDiffNormalizer(app.Spec.IgnoreDifferences, m.settings.ResourceOverrides)
|
||||
func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *appv1.AppProject, revision string, source v1alpha1.ApplicationSource, noCache bool, localManifests []string) *comparisonResult {
|
||||
ts := stats.NewTimingStats()
|
||||
appLabelKey, resourceOverrides, diffNormalizer, resFilter, err := m.getComparisonSettings(app)
|
||||
ts.AddCheckpoint("settings_ms")
|
||||
|
||||
// return unknown comparison result if basic comparison settings cannot be loaded
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return &comparisonResult{
|
||||
syncStatus: &v1alpha1.SyncStatus{
|
||||
ComparedTo: appv1.ComparedTo{Source: source, Destination: app.Spec.Destination},
|
||||
Status: appv1.SyncStatusCodeUnknown,
|
||||
},
|
||||
healthStatus: &appv1.HealthStatus{Status: appv1.HealthStatusUnknown},
|
||||
}
|
||||
}
|
||||
logCtx := log.WithField("application", app.Name)
|
||||
logCtx.Infof("Comparing app state (cluster: %s, namespace: %s)", app.Spec.Destination.Server, app.Spec.Destination.Namespace)
|
||||
observedAt := metav1.Now()
|
||||
|
||||
// do best effort loading live and target state to present as much information about app state as possible
|
||||
failedToLoadObjs := false
|
||||
conditions := make([]v1alpha1.ApplicationCondition, 0)
|
||||
appLabelKey := m.settings.GetAppInstanceLabelKey()
|
||||
targetObjs, hooks, manifestInfo, err := m.getRepoObjs(app, source, appLabelKey, revision, noCache)
|
||||
if err != nil {
|
||||
targetObjs = make([]*unstructured.Unstructured, 0)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
failedToLoadObjs = true
|
||||
|
||||
logCtx := log.WithField("application", app.Name)
|
||||
logCtx.Infof("Comparing app state (cluster: %s, namespace: %s)", app.Spec.Destination.Server, app.Spec.Destination.Namespace)
|
||||
|
||||
var targetObjs []*unstructured.Unstructured
|
||||
var hooks []*unstructured.Unstructured
|
||||
var manifestInfo *apiclient.ManifestResponse
|
||||
now := metav1.Now()
|
||||
|
||||
if len(localManifests) == 0 {
|
||||
targetObjs, hooks, manifestInfo, err = m.getRepoObjs(app, source, appLabelKey, revision, noCache)
|
||||
if err != nil {
|
||||
targetObjs = make([]*unstructured.Unstructured, 0)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
|
||||
failedToLoadObjs = true
|
||||
}
|
||||
} else {
|
||||
targetObjs, hooks, err = unmarshalManifests(localManifests)
|
||||
if err != nil {
|
||||
targetObjs = make([]*unstructured.Unstructured, 0)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
|
||||
failedToLoadObjs = true
|
||||
}
|
||||
manifestInfo = nil
|
||||
}
|
||||
ts.AddCheckpoint("git_ms")
|
||||
|
||||
targetObjs, dedupConditions, err := DeduplicateTargetObjects(app.Spec.Destination.Server, app.Spec.Destination.Namespace, targetObjs, m.liveStateCache)
|
||||
if err != nil {
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
|
||||
}
|
||||
conditions = append(conditions, dedupConditions...)
|
||||
for i := len(targetObjs) - 1; i >= 0; i-- {
|
||||
targetObj := targetObjs[i]
|
||||
gvk := targetObj.GroupVersionKind()
|
||||
if resFilter.IsExcludedResource(gvk.Group, gvk.Kind, app.Spec.Destination.Server) {
|
||||
targetObjs = append(targetObjs[:i], targetObjs[i+1:]...)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{
|
||||
Type: v1alpha1.ApplicationConditionExcludedResourceWarning,
|
||||
Message: fmt.Sprintf("Resource %s/%s %s is excluded in the settings", gvk.Group, gvk.Kind, targetObj.GetName()),
|
||||
LastTransitionTime: &now,
|
||||
})
|
||||
}
|
||||
}
|
||||
ts.AddCheckpoint("dedup_ms")
|
||||
|
||||
logCtx.Debugf("Generated config manifests")
|
||||
liveObjByKey, err := m.liveStateCache.GetManagedLiveObjs(app, targetObjs)
|
||||
if err != nil {
|
||||
liveObjByKey = make(map[kubeutil.ResourceKey]*unstructured.Unstructured)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
|
||||
failedToLoadObjs = true
|
||||
}
|
||||
logCtx.Debugf("Retrieved lived manifests")
|
||||
dedupLiveResources(targetObjs, liveObjByKey)
|
||||
// filter out all resources which are not permitted in the application project
|
||||
for k, v := range liveObjByKey {
|
||||
if !project.IsLiveResourcePermitted(v, app.Spec.Destination.Server) {
|
||||
delete(liveObjByKey, k)
|
||||
}
|
||||
}
|
||||
|
||||
for _, liveObj := range liveObjByKey {
|
||||
if liveObj != nil {
|
||||
appInstanceName := kubeutil.GetAppInstanceLabel(liveObj, appLabelKey)
|
||||
if appInstanceName != "" && appInstanceName != app.Name {
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{
|
||||
Type: v1alpha1.ApplicationConditionSharedResourceWarning,
|
||||
Message: fmt.Sprintf("%s/%s is part of a different application: %s", liveObj.GetKind(), liveObj.GetName(), appInstanceName),
|
||||
Type: v1alpha1.ApplicationConditionSharedResourceWarning,
|
||||
Message: fmt.Sprintf("%s/%s is part of a different application: %s", liveObj.GetKind(), liveObj.GetName(), appInstanceName),
|
||||
LastTransitionTime: &now,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -222,7 +391,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
for i, obj := range targetObjs {
|
||||
gvk := obj.GroupVersionKind()
|
||||
ns := util.FirstNonEmpty(obj.GetNamespace(), app.Spec.Destination.Namespace)
|
||||
if namespaced, err := m.liveStateCache.IsNamespaced(app.Spec.Destination.Server, obj); err == nil && !namespaced {
|
||||
if namespaced, err := m.liveStateCache.IsNamespaced(app.Spec.Destination.Server, obj.GroupVersionKind().GroupKind()); err == nil && !namespaced {
|
||||
ns = ""
|
||||
}
|
||||
key := kubeutil.NewResourceKey(gvk.Group, gvk.Kind, ns, obj.GetName())
|
||||
@@ -233,7 +402,8 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
managedLiveObj[i] = nil
|
||||
}
|
||||
}
|
||||
logCtx.Debugf("built managed objects list")
|
||||
ts.AddCheckpoint("live_ms")
|
||||
|
||||
// Everything remaining in liveObjByKey are "extra" resources that aren't tracked in git.
|
||||
// The following adds all the extras to the managedLiveObj list and backfills the targetObj
|
||||
// list with nils, so that the lists are of equal lengths for comparison purposes.
|
||||
@@ -245,16 +415,20 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
// Do the actual comparison
|
||||
diffResults, err := diff.DiffArray(targetObjs, managedLiveObj, diffNormalizer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
diffResults = &diff.DiffResultList{}
|
||||
failedToLoadObjs = true
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
|
||||
}
|
||||
ts.AddCheckpoint("diff_ms")
|
||||
|
||||
syncCode := v1alpha1.SyncStatusCodeSynced
|
||||
managedResources := make([]managedResource, len(targetObjs))
|
||||
resourceSummaries := make([]v1alpha1.ResourceStatus, len(targetObjs))
|
||||
for i := 0; i < len(targetObjs); i++ {
|
||||
obj := managedLiveObj[i]
|
||||
for i, targetObj := range targetObjs {
|
||||
liveObj := managedLiveObj[i]
|
||||
obj := liveObj
|
||||
if obj == nil {
|
||||
obj = targetObjs[i]
|
||||
obj = targetObj
|
||||
}
|
||||
if obj == nil {
|
||||
continue
|
||||
@@ -262,35 +436,60 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
gvk := obj.GroupVersionKind()
|
||||
|
||||
resState := v1alpha1.ResourceStatus{
|
||||
Namespace: obj.GetNamespace(),
|
||||
Name: obj.GetName(),
|
||||
Kind: gvk.Kind,
|
||||
Version: gvk.Version,
|
||||
Group: gvk.Group,
|
||||
Hook: hookutil.IsHook(obj),
|
||||
Namespace: obj.GetNamespace(),
|
||||
Name: obj.GetName(),
|
||||
Kind: gvk.Kind,
|
||||
Version: gvk.Version,
|
||||
Group: gvk.Group,
|
||||
Hook: hookutil.IsHook(obj),
|
||||
RequiresPruning: targetObj == nil && liveObj != nil,
|
||||
}
|
||||
|
||||
diffResult := diffResults.Diffs[i]
|
||||
if resState.Hook {
|
||||
var diffResult diff.DiffResult
|
||||
if i < len(diffResults.Diffs) {
|
||||
diffResult = diffResults.Diffs[i]
|
||||
} else {
|
||||
diffResult = diff.DiffResult{
|
||||
Diff: gojsondiff.New().CompareObjects(map[string]interface{}{}, map[string]interface{}{}),
|
||||
Modified: false,
|
||||
NormalizedLive: []byte("{}"),
|
||||
PredictedLive: []byte("{}"),
|
||||
}
|
||||
}
|
||||
if resState.Hook || ignore.Ignore(obj) {
|
||||
// For resource hooks, don't store sync status, and do not affect overall sync status
|
||||
} else if diffResult.Modified || targetObjs[i] == nil || managedLiveObj[i] == nil {
|
||||
} else if diffResult.Modified || targetObj == nil || liveObj == nil {
|
||||
// Set resource state to OutOfSync since one of the following is true:
|
||||
// * target and live resource are different
|
||||
// * target resource not defined and live resource is extra
|
||||
// * target resource present but live resource is missing
|
||||
resState.Status = v1alpha1.SyncStatusCodeOutOfSync
|
||||
syncCode = v1alpha1.SyncStatusCodeOutOfSync
|
||||
// we ignore the status if the obj needs pruning AND we have the annotation
|
||||
needsPruning := targetObj == nil && liveObj != nil
|
||||
if !(needsPruning && resource.HasAnnotationOption(obj, common.AnnotationCompareOptions, "IgnoreExtraneous")) {
|
||||
syncCode = v1alpha1.SyncStatusCodeOutOfSync
|
||||
}
|
||||
} else {
|
||||
resState.Status = v1alpha1.SyncStatusCodeSynced
|
||||
}
|
||||
// set unknown status to all resource that are not permitted in the app project
|
||||
isNamespaced, err := m.liveStateCache.IsNamespaced(app.Spec.Destination.Server, gvk.GroupKind())
|
||||
if !project.IsGroupKindPermitted(gvk.GroupKind(), isNamespaced && err == nil) {
|
||||
resState.Status = v1alpha1.SyncStatusCodeUnknown
|
||||
}
|
||||
|
||||
// we can't say anything about the status if we were unable to get the target objects
|
||||
if failedToLoadObjs {
|
||||
resState.Status = v1alpha1.SyncStatusCodeUnknown
|
||||
}
|
||||
managedResources[i] = managedResource{
|
||||
Name: resState.Name,
|
||||
Namespace: resState.Namespace,
|
||||
Group: resState.Group,
|
||||
Kind: resState.Kind,
|
||||
Version: resState.Version,
|
||||
Live: managedLiveObj[i],
|
||||
Target: targetObjs[i],
|
||||
Live: liveObj,
|
||||
Target: targetObj,
|
||||
Diff: diffResult,
|
||||
Hook: resState.Hook,
|
||||
}
|
||||
@@ -310,35 +509,36 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
if manifestInfo != nil {
|
||||
syncStatus.Revision = manifestInfo.Revision
|
||||
}
|
||||
ts.AddCheckpoint("sync_ms")
|
||||
|
||||
healthStatus, err := health.SetApplicationHealth(resourceSummaries, GetLiveObjs(managedResources), resourceOverrides, func(obj *unstructured.Unstructured) bool {
|
||||
return !isSelfReferencedApp(app, kubeutil.GetObjectRef(obj))
|
||||
})
|
||||
|
||||
healthStatus, err := health.SetApplicationHealth(resourceSummaries, GetLiveObjs(managedResources), m.settings.ResourceOverrides)
|
||||
if err != nil {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
conditions = append(conditions, appv1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
|
||||
}
|
||||
|
||||
compRes := comparisonResult{
|
||||
reconciledAt: observedAt,
|
||||
syncStatus: &syncStatus,
|
||||
healthStatus: healthStatus,
|
||||
resources: resourceSummaries,
|
||||
managedResources: managedResources,
|
||||
conditions: conditions,
|
||||
hooks: hooks,
|
||||
diffNormalizer: diffNormalizer,
|
||||
}
|
||||
if manifestInfo != nil {
|
||||
compRes.appSourceType = v1alpha1.ApplicationSourceType(manifestInfo.SourceType)
|
||||
}
|
||||
return &compRes, nil
|
||||
}
|
||||
|
||||
func (m *appStateManager) getRepo(repoURL string) *v1alpha1.Repository {
|
||||
repo, err := m.db.GetRepository(context.Background(), repoURL)
|
||||
if err != nil {
|
||||
// If we couldn't retrieve from the repo service, assume public repositories
|
||||
repo = &v1alpha1.Repository{Repo: repoURL}
|
||||
}
|
||||
return repo
|
||||
app.Status.SetConditions(conditions, map[appv1.ApplicationConditionType]bool{
|
||||
appv1.ApplicationConditionComparisonError: true,
|
||||
appv1.ApplicationConditionSharedResourceWarning: true,
|
||||
appv1.ApplicationConditionRepeatedResourceWarning: true,
|
||||
appv1.ApplicationConditionExcludedResourceWarning: true,
|
||||
})
|
||||
ts.AddCheckpoint("health_ms")
|
||||
compRes.timings = ts.Timings()
|
||||
return &compRes
|
||||
}
|
||||
|
||||
func (m *appStateManager) persistRevisionHistory(app *v1alpha1.Application, revision string, source v1alpha1.ApplicationSource) error {
|
||||
@@ -346,20 +546,18 @@ func (m *appStateManager) persistRevisionHistory(app *v1alpha1.Application, revi
|
||||
if len(app.Status.History) > 0 {
|
||||
nextID = app.Status.History[len(app.Status.History)-1].ID + 1
|
||||
}
|
||||
history := append(app.Status.History, v1alpha1.RevisionHistory{
|
||||
app.Status.History = append(app.Status.History, v1alpha1.RevisionHistory{
|
||||
Revision: revision,
|
||||
DeployedAt: metav1.NewTime(time.Now().UTC()),
|
||||
ID: nextID,
|
||||
Source: source,
|
||||
})
|
||||
|
||||
if len(history) > common.RevisionHistoryLimit {
|
||||
history = history[1 : common.RevisionHistoryLimit+1]
|
||||
}
|
||||
app.Status.History = app.Status.History.Trunc(app.Spec.GetRevisionHistoryLimit())
|
||||
|
||||
patch, err := json.Marshal(map[string]map[string][]v1alpha1.RevisionHistory{
|
||||
"status": {
|
||||
"history": history,
|
||||
"history": app.Status.History,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -373,12 +571,13 @@ func (m *appStateManager) persistRevisionHistory(app *v1alpha1.Application, revi
|
||||
func NewAppStateManager(
|
||||
db db.ArgoDB,
|
||||
appclientset appclientset.Interface,
|
||||
repoClientset reposerver.Clientset,
|
||||
repoClientset apiclient.Clientset,
|
||||
namespace string,
|
||||
kubectl kubeutil.Kubectl,
|
||||
settings *settings.ArgoCDSettings,
|
||||
settingsMgr *settings.SettingsManager,
|
||||
liveStateCache statecache.LiveStateCache,
|
||||
projInformer cache.SharedIndexInformer,
|
||||
metricsServer *metrics.MetricsServer,
|
||||
) AppStateManager {
|
||||
return &appStateManager{
|
||||
liveStateCache: liveStateCache,
|
||||
@@ -387,7 +586,8 @@ func NewAppStateManager(
|
||||
kubectl: kubectl,
|
||||
repoClientset: repoClientset,
|
||||
namespace: namespace,
|
||||
settings: settings,
|
||||
settingsMgr: settingsMgr,
|
||||
projInformer: projInformer,
|
||||
metricsServer: metricsServer,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,14 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
v1 "k8s.io/api/apps/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/reposerver/repository"
|
||||
"github.com/argoproj/argo-cd/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/test"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
)
|
||||
@@ -19,7 +21,7 @@ import (
|
||||
func TestCompareAppStateEmpty(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
@@ -28,22 +30,22 @@ func TestCompareAppStateEmpty(t *testing.T) {
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
|
||||
assert.NoError(t, err)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Equal(t, 0, len(compRes.resources))
|
||||
assert.Equal(t, 0, len(compRes.managedResources))
|
||||
assert.Equal(t, 0, len(compRes.conditions))
|
||||
assert.Len(t, compRes.resources, 0)
|
||||
assert.Len(t, compRes.managedResources, 0)
|
||||
assert.Len(t, app.Status.Conditions, 0)
|
||||
}
|
||||
|
||||
// TestCompareAppStateMissing tests when there is a manifest defined in git which doesn't exist in live
|
||||
// TestCompareAppStateMissing tests when there is a manifest defined in the repo which doesn't exist in live
|
||||
func TestCompareAppStateMissing(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
apps: []runtime.Object{app},
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
Manifests: []string{string(test.PodManifest)},
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{test.PodManifest},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
@@ -51,13 +53,13 @@ func TestCompareAppStateMissing(t *testing.T) {
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
|
||||
assert.NoError(t, err)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeOutOfSync, compRes.syncStatus.Status)
|
||||
assert.Equal(t, 1, len(compRes.resources))
|
||||
assert.Equal(t, 1, len(compRes.managedResources))
|
||||
assert.Equal(t, 0, len(compRes.conditions))
|
||||
assert.Len(t, compRes.resources, 1)
|
||||
assert.Len(t, compRes.managedResources, 1)
|
||||
assert.Len(t, app.Status.Conditions, 0)
|
||||
}
|
||||
|
||||
// TestCompareAppStateExtra tests when there is an extra object in live but not defined in git
|
||||
@@ -67,7 +69,7 @@ func TestCompareAppStateExtra(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
key := kube.ResourceKey{Group: "", Kind: "Pod", Namespace: test.FakeDestNamespace, Name: app.Name}
|
||||
data := fakeData{
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
@@ -78,13 +80,12 @@ func TestCompareAppStateExtra(t *testing.T) {
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
|
||||
assert.NoError(t, err)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeOutOfSync, compRes.syncStatus.Status)
|
||||
assert.Equal(t, 1, len(compRes.resources))
|
||||
assert.Equal(t, 1, len(compRes.managedResources))
|
||||
assert.Equal(t, 0, len(compRes.conditions))
|
||||
assert.Equal(t, 0, len(app.Status.Conditions))
|
||||
}
|
||||
|
||||
// TestCompareAppStateHook checks that hooks are detected during manifest generation, and not
|
||||
@@ -96,7 +97,7 @@ func TestCompareAppStateHook(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
apps: []runtime.Object{app},
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{string(podBytes)},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
@@ -105,13 +106,39 @@ func TestCompareAppStateHook(t *testing.T) {
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
|
||||
assert.NoError(t, err)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Equal(t, 0, len(compRes.resources))
|
||||
assert.Equal(t, 0, len(compRes.managedResources))
|
||||
assert.Equal(t, 0, len(compRes.conditions))
|
||||
assert.Equal(t, 1, len(compRes.hooks))
|
||||
assert.Equal(t, 0, len(app.Status.Conditions))
|
||||
}
|
||||
|
||||
// checks that ignore resources are detected, but excluded from status
|
||||
func TestCompareAppStateCompareOptionIgnoreExtraneous(t *testing.T) {
|
||||
pod := test.NewPod()
|
||||
pod.SetAnnotations(map[string]string{common.AnnotationCompareOptions: "IgnoreExtraneous"})
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
apps: []runtime.Object{app},
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
|
||||
assert.NotNil(t, compRes)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Len(t, compRes.resources, 0)
|
||||
assert.Len(t, compRes.managedResources, 0)
|
||||
assert.Len(t, app.Status.Conditions, 0)
|
||||
}
|
||||
|
||||
// TestCompareAppStateExtraHook tests when there is an extra _hook_ object in live but not defined in git
|
||||
@@ -122,7 +149,7 @@ func TestCompareAppStateExtraHook(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
key := kube.ResourceKey{Group: "", Kind: "Pod", Namespace: test.FakeDestNamespace, Name: app.Name}
|
||||
data := fakeData{
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
@@ -133,13 +160,14 @@ func TestCompareAppStateExtraHook(t *testing.T) {
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
|
||||
assert.NoError(t, err)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
|
||||
assert.NotNil(t, compRes)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Equal(t, 1, len(compRes.resources))
|
||||
assert.Equal(t, 1, len(compRes.managedResources))
|
||||
assert.Equal(t, 0, len(compRes.conditions))
|
||||
assert.Equal(t, 0, len(compRes.hooks))
|
||||
assert.Equal(t, 0, len(app.Status.Conditions))
|
||||
}
|
||||
|
||||
func toJSON(t *testing.T, obj *unstructured.Unstructured) string {
|
||||
@@ -157,7 +185,7 @@ func TestCompareAppStateDuplicatedNamespacedResources(t *testing.T) {
|
||||
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{toJSON(t, obj1), toJSON(t, obj2), toJSON(t, obj3)},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
@@ -169,12 +197,244 @@ func TestCompareAppStateDuplicatedNamespacedResources(t *testing.T) {
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
|
||||
assert.NoError(t, err)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
|
||||
assert.NotNil(t, compRes)
|
||||
assert.Contains(t, compRes.conditions, argoappv1.ApplicationCondition{
|
||||
Message: "Resource /Pod/fake-dest-ns/my-pod appeared 2 times among application resources.",
|
||||
Type: argoappv1.ApplicationConditionRepeatedResourceWarning,
|
||||
})
|
||||
assert.Equal(t, 1, len(app.Status.Conditions))
|
||||
assert.NotNil(t, app.Status.Conditions[0].LastTransitionTime)
|
||||
assert.Equal(t, argoappv1.ApplicationConditionRepeatedResourceWarning, app.Status.Conditions[0].Type)
|
||||
assert.Equal(t, "Resource /Pod/fake-dest-ns/my-pod appeared 2 times among application resources.", app.Status.Conditions[0].Message)
|
||||
assert.Equal(t, 2, len(compRes.resources))
|
||||
}
|
||||
|
||||
var defaultProj = argoappv1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
},
|
||||
Spec: argoappv1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []argoappv1.ApplicationDestination{
|
||||
{
|
||||
Server: "*",
|
||||
Namespace: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestSetHealth(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
deployment := kube.MustToUnstructured(&v1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "apps/v1beta1",
|
||||
Kind: "Deployment",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "demo",
|
||||
Namespace: "default",
|
||||
},
|
||||
})
|
||||
ctrl := newFakeController(&fakeData{
|
||||
apps: []runtime.Object{app, &defaultProj},
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
||||
kube.GetResourceKey(deployment): deployment,
|
||||
},
|
||||
})
|
||||
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
|
||||
assert.Equal(t, compRes.healthStatus.Status, argoappv1.HealthStatusHealthy)
|
||||
}
|
||||
|
||||
func TestSetHealthSelfReferencedApp(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
unstructuredApp := kube.MustToUnstructured(app)
|
||||
deployment := kube.MustToUnstructured(&v1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "apps/v1beta1",
|
||||
Kind: "Deployment",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "demo",
|
||||
Namespace: "default",
|
||||
},
|
||||
})
|
||||
ctrl := newFakeController(&fakeData{
|
||||
apps: []runtime.Object{app, &defaultProj},
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
||||
kube.GetResourceKey(deployment): deployment,
|
||||
kube.GetResourceKey(unstructuredApp): unstructuredApp,
|
||||
},
|
||||
})
|
||||
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
|
||||
assert.Equal(t, compRes.healthStatus.Status, argoappv1.HealthStatusHealthy)
|
||||
}
|
||||
|
||||
func TestSetManagedResourcesWithOrphanedResources(t *testing.T) {
|
||||
proj := defaultProj.DeepCopy()
|
||||
proj.Spec.OrphanedResources = &argoappv1.OrphanedResourcesMonitorSettings{}
|
||||
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{
|
||||
apps: []runtime.Object{app, proj},
|
||||
namespacedResources: map[kube.ResourceKey]namespacedResource{
|
||||
kube.NewResourceKey("apps", kube.DeploymentKind, app.Namespace, "guestbook"): {
|
||||
ResourceNode: argoappv1.ResourceNode{
|
||||
ResourceRef: argoappv1.ResourceRef{Kind: kube.DeploymentKind, Name: "guestbook", Namespace: app.Namespace},
|
||||
},
|
||||
AppName: "",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
tree, err := ctrl.setAppManagedResources(app, &comparisonResult{managedResources: make([]managedResource, 0)})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(tree.OrphanedNodes), 1)
|
||||
assert.Equal(t, "guestbook", tree.OrphanedNodes[0].Name)
|
||||
assert.Equal(t, app.Namespace, tree.OrphanedNodes[0].Namespace)
|
||||
}
|
||||
|
||||
func TestSetManagedResourcesWithResourcesOfAnotherApp(t *testing.T) {
|
||||
proj := defaultProj.DeepCopy()
|
||||
proj.Spec.OrphanedResources = &argoappv1.OrphanedResourcesMonitorSettings{}
|
||||
|
||||
app1 := newFakeApp()
|
||||
app1.Name = "app1"
|
||||
app2 := newFakeApp()
|
||||
app2.Name = "app2"
|
||||
|
||||
ctrl := newFakeController(&fakeData{
|
||||
apps: []runtime.Object{app1, app2, proj},
|
||||
namespacedResources: map[kube.ResourceKey]namespacedResource{
|
||||
kube.NewResourceKey("apps", kube.DeploymentKind, app2.Namespace, "guestbook"): {
|
||||
ResourceNode: argoappv1.ResourceNode{
|
||||
ResourceRef: argoappv1.ResourceRef{Kind: kube.DeploymentKind, Name: "guestbook", Namespace: app2.Namespace},
|
||||
},
|
||||
AppName: "app2",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
tree, err := ctrl.setAppManagedResources(app1, &comparisonResult{managedResources: make([]managedResource, 0)})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(tree.OrphanedNodes), 0)
|
||||
}
|
||||
|
||||
func TestReturnUnknownComparisonStateOnSettingLoadError(t *testing.T) {
|
||||
proj := defaultProj.DeepCopy()
|
||||
proj.Spec.OrphanedResources = &argoappv1.OrphanedResourcesMonitorSettings{}
|
||||
|
||||
app := newFakeApp()
|
||||
|
||||
ctrl := newFakeController(&fakeData{
|
||||
apps: []runtime.Object{app, proj},
|
||||
configMapData: map[string]string{
|
||||
"resource.customizations": "invalid setting",
|
||||
},
|
||||
})
|
||||
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
|
||||
assert.Equal(t, argoappv1.HealthStatusUnknown, compRes.healthStatus.Status)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeUnknown, compRes.syncStatus.Status)
|
||||
}
|
||||
|
||||
func TestSetManagedResourcesKnownOrphanedResourceExceptions(t *testing.T) {
|
||||
proj := defaultProj.DeepCopy()
|
||||
proj.Spec.OrphanedResources = &argoappv1.OrphanedResourcesMonitorSettings{}
|
||||
|
||||
app := newFakeApp()
|
||||
app.Namespace = "default"
|
||||
|
||||
ctrl := newFakeController(&fakeData{
|
||||
apps: []runtime.Object{app, proj},
|
||||
namespacedResources: map[kube.ResourceKey]namespacedResource{
|
||||
kube.NewResourceKey("apps", kube.DeploymentKind, app.Namespace, "guestbook"): {
|
||||
ResourceNode: argoappv1.ResourceNode{ResourceRef: argoappv1.ResourceRef{Group: "apps", Kind: kube.DeploymentKind, Name: "guestbook", Namespace: app.Namespace}},
|
||||
},
|
||||
kube.NewResourceKey("", kube.ServiceAccountKind, app.Namespace, "default"): {
|
||||
ResourceNode: argoappv1.ResourceNode{ResourceRef: argoappv1.ResourceRef{Kind: kube.ServiceAccountKind, Name: "default", Namespace: app.Namespace}},
|
||||
},
|
||||
kube.NewResourceKey("", kube.ServiceKind, app.Namespace, "kubernetes"): {
|
||||
ResourceNode: argoappv1.ResourceNode{ResourceRef: argoappv1.ResourceRef{Kind: kube.ServiceAccountKind, Name: "kubernetes", Namespace: app.Namespace}},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
tree, err := ctrl.setAppManagedResources(app, &comparisonResult{managedResources: make([]managedResource, 0)})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, tree.OrphanedNodes, 1)
|
||||
assert.Equal(t, "guestbook", tree.OrphanedNodes[0].Name)
|
||||
}
|
||||
|
||||
func Test_comparisonResult_obs(t *testing.T) {
|
||||
assert.Len(t, (&comparisonResult{}).targetObjs(), 0)
|
||||
assert.Len(t, (&comparisonResult{managedResources: []managedResource{{}}}).targetObjs(), 0)
|
||||
assert.Len(t, (&comparisonResult{managedResources: []managedResource{{Target: test.NewPod()}}}).targetObjs(), 1)
|
||||
assert.Len(t, (&comparisonResult{hooks: []*unstructured.Unstructured{{}}}).targetObjs(), 1)
|
||||
}
|
||||
|
||||
func Test_appStateManager_persistRevisionHistory(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{
|
||||
apps: []runtime.Object{app},
|
||||
})
|
||||
manager := ctrl.appStateManager.(*appStateManager)
|
||||
setRevisionHistoryLimit := func(value int) {
|
||||
i := int64(value)
|
||||
app.Spec.RevisionHistoryLimit = &i
|
||||
}
|
||||
addHistory := func() {
|
||||
err := manager.persistRevisionHistory(app, "my-revision", argoappv1.ApplicationSource{})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 1)
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 2)
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 3)
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 4)
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 5)
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 6)
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 7)
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 8)
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 9)
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 10)
|
||||
// default limit is 10
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 10)
|
||||
// increase limit
|
||||
setRevisionHistoryLimit(11)
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 11)
|
||||
// decrease limit
|
||||
setRevisionHistoryLimit(9)
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 9)
|
||||
}
|
||||
|
||||
1078
controller/sync.go
@@ -1,66 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/test"
|
||||
"github.com/argoproj/argo-cd/util/kube/kubetest"
|
||||
)
|
||||
|
||||
var clusterRoleHook = `
|
||||
{
|
||||
"apiVersion": "rbac.authorization.k8s.io/v1",
|
||||
"kind": "ClusterRole",
|
||||
"metadata": {
|
||||
"name": "cluster-role-hook",
|
||||
"annotations": {
|
||||
"argocd.argoproj.io/hook": "PostSync"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
func TestSyncHookProjectPermissions(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx(&v1.APIResourceList{
|
||||
GroupVersion: "v1",
|
||||
APIResources: []v1.APIResource{
|
||||
{Name: "pod", Namespaced: true, Kind: "Pod", Group: "v1"},
|
||||
},
|
||||
}, &v1.APIResourceList{
|
||||
GroupVersion: "rbac.authorization.k8s.io/v1",
|
||||
APIResources: []v1.APIResource{
|
||||
{Name: "clusterroles", Namespaced: false, Kind: "ClusterRole", Group: "rbac.authorization.k8s.io"},
|
||||
},
|
||||
})
|
||||
|
||||
syncCtx.kubectl = kubetest.MockKubectlCmd{}
|
||||
crHook, _ := v1alpha1.UnmarshalToUnstructured(clusterRoleHook)
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
hooks: []*unstructured.Unstructured{
|
||||
crHook,
|
||||
},
|
||||
managedResources: []managedResource{{
|
||||
Target: test.NewPod(),
|
||||
}},
|
||||
}
|
||||
syncCtx.proj.Spec.ClusterResourceWhitelist = []v1.GroupKind{}
|
||||
|
||||
syncCtx.syncOp.SyncStrategy = nil
|
||||
syncCtx.sync()
|
||||
assert.Equal(t, v1alpha1.OperationFailed, syncCtx.opState.Phase)
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 0)
|
||||
assert.Contains(t, syncCtx.opState.Message, "not permitted in project")
|
||||
|
||||
// Now add the resource to the whitelist and try again. Resource should be created
|
||||
syncCtx.proj.Spec.ClusterResourceWhitelist = []v1.GroupKind{
|
||||
{Group: "rbac.authorization.k8s.io", Kind: "ClusterRole"},
|
||||
}
|
||||
syncCtx.syncOp.SyncStrategy = nil
|
||||
syncCtx.sync()
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 1)
|
||||
assert.Equal(t, v1alpha1.ResultCodeSynced, syncCtx.syncRes.Resources[0].Status)
|
||||
}
|
||||
@@ -2,559 +2,34 @@ package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
apierr "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/schema"
|
||||
"k8s.io/kubernetes/pkg/apis/batch"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
hookutil "github.com/argoproj/argo-cd/util/hook"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/health"
|
||||
)
|
||||
|
||||
// doHookSync initiates (or continues) a hook-based sync. This method will be invoked when there may
|
||||
// already be in-flight (potentially incomplete) jobs/workflows, and should be idempotent.
|
||||
func (sc *syncContext) doHookSync(syncTasks []syncTask, hooks []*unstructured.Unstructured) {
|
||||
if !sc.startedPreSyncPhase() {
|
||||
if !sc.verifyPermittedHooks(hooks) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// 1. Run PreSync hooks
|
||||
if !sc.runHooks(hooks, appv1.HookTypePreSync) {
|
||||
return
|
||||
}
|
||||
// getOperationPhase returns a hook status from an _live_ unstructured object
|
||||
func (sc *syncContext) getOperationPhase(hook *unstructured.Unstructured) (v1alpha1.OperationPhase, string, error) {
|
||||
phase := v1alpha1.OperationSucceeded
|
||||
message := fmt.Sprintf("%s created", hook.GetName())
|
||||
|
||||
// 2. Run Sync hooks (e.g. blue-green sync workflow)
|
||||
// Before performing Sync hooks, apply any normal manifests which aren't annotated with a hook.
|
||||
// We only want to do this once per operation.
|
||||
shouldContinue := true
|
||||
if !sc.startedSyncPhase() {
|
||||
if !sc.syncNonHookTasks(syncTasks) {
|
||||
sc.setOperationPhase(appv1.OperationFailed, "one or more objects failed to apply")
|
||||
return
|
||||
}
|
||||
shouldContinue = false
|
||||
}
|
||||
if !sc.runHooks(hooks, appv1.HookTypeSync) {
|
||||
shouldContinue = false
|
||||
}
|
||||
if !shouldContinue {
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Run PostSync hooks
|
||||
// Before running PostSync hooks, we want to make rollout is complete (app is healthy). If we
|
||||
// already started the post-sync phase, then we do not need to perform the health check.
|
||||
postSyncHooks, _ := sc.getHooks(appv1.HookTypePostSync)
|
||||
if len(postSyncHooks) > 0 && !sc.startedPostSyncPhase() {
|
||||
sc.log.Infof("PostSync application health check: %s", sc.compareResult.healthStatus.Status)
|
||||
if sc.compareResult.healthStatus.Status != appv1.HealthStatusHealthy {
|
||||
sc.setOperationPhase(appv1.OperationRunning, fmt.Sprintf("waiting for %s state to run %s hooks (current health: %s)",
|
||||
appv1.HealthStatusHealthy, appv1.HookTypePostSync, sc.compareResult.healthStatus.Status))
|
||||
return
|
||||
}
|
||||
}
|
||||
if !sc.runHooks(hooks, appv1.HookTypePostSync) {
|
||||
return
|
||||
}
|
||||
|
||||
// if we get here, all hooks successfully completed
|
||||
sc.setOperationPhase(appv1.OperationSucceeded, "successfully synced")
|
||||
}
|
||||
|
||||
// verifyPermittedHooks verifies all hooks are permitted in the project
|
||||
func (sc *syncContext) verifyPermittedHooks(hooks []*unstructured.Unstructured) bool {
|
||||
for _, hook := range hooks {
|
||||
gvk := hook.GroupVersionKind()
|
||||
serverRes, err := kube.ServerResourceForGroupVersionKind(sc.disco, gvk)
|
||||
if err != nil {
|
||||
sc.setOperationPhase(appv1.OperationError, fmt.Sprintf("unable to identify api resource type: %v", gvk))
|
||||
return false
|
||||
}
|
||||
if !sc.proj.IsResourcePermitted(metav1.GroupKind{Group: gvk.Group, Kind: gvk.Kind}, serverRes.Namespaced) {
|
||||
sc.setOperationPhase(appv1.OperationFailed, fmt.Sprintf("Hook resource %s:%s is not permitted in project %s", gvk.Group, gvk.Kind, sc.proj.Name))
|
||||
return false
|
||||
}
|
||||
|
||||
if serverRes.Namespaced && !sc.proj.IsDestinationPermitted(appv1.ApplicationDestination{Namespace: hook.GetNamespace(), Server: sc.server}) {
|
||||
gvk := hook.GroupVersionKind()
|
||||
sc.setResourceDetails(&appv1.ResourceResult{
|
||||
Name: hook.GetName(),
|
||||
Group: gvk.Group,
|
||||
Version: gvk.Version,
|
||||
Kind: hook.GetKind(),
|
||||
Namespace: hook.GetNamespace(),
|
||||
Message: fmt.Sprintf("namespace %v is not permitted in project '%s'", hook.GetNamespace(), sc.proj.Name),
|
||||
Status: appv1.ResultCodeSyncFailed,
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// getHooks returns all Argo CD hooks, optionally filtered by ones of the specific type(s)
|
||||
func (sc *syncContext) getHooks(hookTypes ...appv1.HookType) ([]*unstructured.Unstructured, error) {
|
||||
var hooks []*unstructured.Unstructured
|
||||
for _, hook := range sc.compareResult.hooks {
|
||||
if hook.GetNamespace() == "" {
|
||||
hook.SetNamespace(sc.namespace)
|
||||
}
|
||||
if !hookutil.IsArgoHook(hook) {
|
||||
// TODO: in the future, if we want to map helm hooks to Argo CD lifecycles, we should
|
||||
// include helm hooks in the returned list
|
||||
continue
|
||||
}
|
||||
if len(hookTypes) > 0 {
|
||||
match := false
|
||||
for _, desiredType := range hookTypes {
|
||||
if isHookType(hook, desiredType) {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
}
|
||||
hooks = append(hooks, hook)
|
||||
}
|
||||
return hooks, nil
|
||||
}
|
||||
|
||||
// runHooks iterates & filters the target manifests for resources of the specified hook type, then
|
||||
// creates the resource. Updates the sc.opRes.hooks with the current status. Returns whether or not
|
||||
// we should continue to the next hook phase.
|
||||
func (sc *syncContext) runHooks(hooks []*unstructured.Unstructured, hookType appv1.HookType) bool {
|
||||
shouldContinue := true
|
||||
for _, hook := range hooks {
|
||||
if hookType == appv1.HookTypeSync && isHookType(hook, appv1.HookTypeSkip) {
|
||||
// If we get here, we are invoking all sync hooks and reached a resource that is
|
||||
// annotated with the Skip hook. This will update the resource details to indicate it
|
||||
// was skipped due to annotation
|
||||
gvk := hook.GroupVersionKind()
|
||||
sc.setResourceDetails(&appv1.ResourceResult{
|
||||
Name: hook.GetName(),
|
||||
Group: gvk.Group,
|
||||
Version: gvk.Version,
|
||||
Kind: hook.GetKind(),
|
||||
Namespace: hook.GetNamespace(),
|
||||
Message: "Skipped",
|
||||
})
|
||||
continue
|
||||
}
|
||||
if !isHookType(hook, hookType) {
|
||||
continue
|
||||
}
|
||||
updated, err := sc.runHook(hook, hookType)
|
||||
if err != nil {
|
||||
sc.setOperationPhase(appv1.OperationError, fmt.Sprintf("%s hook error: %v", hookType, err))
|
||||
return false
|
||||
}
|
||||
if updated {
|
||||
// If the result of running a hook, caused us to modify hook resource state, we should
|
||||
// not proceed to the next hook phase. This is because before proceeding to the next
|
||||
// phase, we want a full health assessment to happen. By returning early, we allow
|
||||
// the application to get requeued into the controller workqueue, and on the next
|
||||
// process iteration, a new CompareAppState() will be performed to get the most
|
||||
// up-to-date live state. This enables us to accurately wait for an application to
|
||||
// become Healthy before proceeding to run PostSync tasks.
|
||||
shouldContinue = false
|
||||
}
|
||||
}
|
||||
if !shouldContinue {
|
||||
sc.log.Infof("Stopping after %s phase due to modifications to hook resource state", hookType)
|
||||
return false
|
||||
}
|
||||
completed, successful := areHooksCompletedSuccessful(hookType, sc.syncRes.Resources)
|
||||
if !completed {
|
||||
return false
|
||||
}
|
||||
if !successful {
|
||||
sc.setOperationPhase(appv1.OperationFailed, fmt.Sprintf("%s hook failed", hookType))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// syncNonHookTasks syncs or prunes the objects that are not handled by hooks using an apply sync.
|
||||
// returns true if the sync was successful
|
||||
func (sc *syncContext) syncNonHookTasks(syncTasks []syncTask) bool {
|
||||
var nonHookTasks []syncTask
|
||||
for _, task := range syncTasks {
|
||||
if task.targetObj == nil {
|
||||
nonHookTasks = append(nonHookTasks, task)
|
||||
} else {
|
||||
annotations := task.targetObj.GetAnnotations()
|
||||
if annotations != nil && annotations[common.AnnotationKeyHook] != "" {
|
||||
// we are doing a hook sync and this resource is annotated with a hook annotation
|
||||
continue
|
||||
}
|
||||
// if we get here, this resource does not have any hook annotation so we
|
||||
// should perform an `kubectl apply`
|
||||
nonHookTasks = append(nonHookTasks, task)
|
||||
}
|
||||
}
|
||||
return sc.doApplySync(nonHookTasks, false, sc.syncOp.SyncStrategy.Hook.Force, true)
|
||||
}
|
||||
|
||||
// runHook runs the supplied hook and updates the hook status. Returns true if the result of
|
||||
// invoking this method resulted in changes to any hook status
|
||||
func (sc *syncContext) runHook(hook *unstructured.Unstructured, hookType appv1.HookType) (bool, error) {
|
||||
// Hook resources names are deterministic, whether they are defined by the user (metadata.name),
|
||||
// or formulated at the time of the operation (metadata.generateName). If user specifies
|
||||
// metadata.generateName, then we will generate a formulated metadata.name before submission.
|
||||
if hook.GetName() == "" {
|
||||
postfix := strings.ToLower(fmt.Sprintf("%s-%s-%d", sc.syncRes.Revision[0:7], hookType, sc.opState.StartedAt.UTC().Unix()))
|
||||
generatedName := hook.GetGenerateName()
|
||||
hook = hook.DeepCopy()
|
||||
hook.SetName(fmt.Sprintf("%s%s", generatedName, postfix))
|
||||
}
|
||||
// Check our hook statuses to see if we already completed this hook.
|
||||
// If so, this method is a noop
|
||||
prevStatus := sc.getHookStatus(hook, hookType)
|
||||
if prevStatus != nil && prevStatus.HookPhase.Completed() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
gvk := hook.GroupVersionKind()
|
||||
apiResource, err := kube.ServerResourceForGroupVersionKind(sc.disco, gvk)
|
||||
resHealth, err := health.GetResourceHealth(hook, sc.resourceOverrides)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return "", "", err
|
||||
}
|
||||
resource := kube.ToGroupVersionResource(gvk.GroupVersion().String(), apiResource)
|
||||
resIf := kube.ToResourceInterface(sc.dynamicIf, apiResource, resource, hook.GetNamespace())
|
||||
|
||||
var liveObj *unstructured.Unstructured
|
||||
existing, err := resIf.Get(hook.GetName(), metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if !apierr.IsNotFound(err) {
|
||||
return false, fmt.Errorf("Failed to get status of %s hook %s '%s': %v", hookType, gvk, hook.GetName(), err)
|
||||
}
|
||||
_, err := sc.kubectl.ApplyResource(sc.config, hook, hook.GetNamespace(), false, false)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("Failed to create %s hook %s '%s': %v", hookType, gvk, hook.GetName(), err)
|
||||
}
|
||||
created, err := resIf.Get(hook.GetName(), metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return true, fmt.Errorf("Failed to get status of %s hook %s '%s': %v", hookType, gvk, hook.GetName(), err)
|
||||
}
|
||||
sc.log.Infof("%s hook %s '%s' created", hookType, gvk, created.GetName())
|
||||
sc.setOperationPhase(appv1.OperationRunning, fmt.Sprintf("running %s hooks", hookType))
|
||||
liveObj = created
|
||||
} else {
|
||||
liveObj = existing
|
||||
}
|
||||
hookStatus := newHookStatus(liveObj, hookType)
|
||||
if hookStatus.HookPhase.Completed() {
|
||||
if enforceHookDeletePolicy(hook, hookStatus.HookPhase) {
|
||||
err = sc.deleteHook(hook.GetName(), hook.GetNamespace(), hook.GroupVersionKind())
|
||||
if err != nil {
|
||||
hookStatus.HookPhase = appv1.OperationFailed
|
||||
hookStatus.Message = fmt.Sprintf("failed to delete %s hook: %v", hookStatus.HookPhase, err)
|
||||
}
|
||||
if resHealth != nil {
|
||||
switch resHealth.Status {
|
||||
case v1alpha1.HealthStatusUnknown, v1alpha1.HealthStatusDegraded:
|
||||
phase = v1alpha1.OperationFailed
|
||||
message = resHealth.Message
|
||||
case v1alpha1.HealthStatusProgressing, v1alpha1.HealthStatusSuspended:
|
||||
phase = v1alpha1.OperationRunning
|
||||
message = resHealth.Message
|
||||
case v1alpha1.HealthStatusHealthy:
|
||||
phase = v1alpha1.OperationSucceeded
|
||||
message = resHealth.Message
|
||||
}
|
||||
}
|
||||
return sc.updateHookStatus(hookStatus), nil
|
||||
}
|
||||
|
||||
// enforceHookDeletePolicy examines the hook deletion policy of a object and deletes it based on the status
|
||||
func enforceHookDeletePolicy(hook *unstructured.Unstructured, phase appv1.OperationPhase) bool {
|
||||
annotations := hook.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return false
|
||||
}
|
||||
deletePolicies := strings.Split(annotations[common.AnnotationKeyHookDeletePolicy], ",")
|
||||
for _, dp := range deletePolicies {
|
||||
policy := appv1.HookDeletePolicy(strings.TrimSpace(dp))
|
||||
if policy == appv1.HookDeletePolicyHookSucceeded && phase == appv1.OperationSucceeded {
|
||||
return true
|
||||
}
|
||||
if policy == appv1.HookDeletePolicyHookFailed && phase == appv1.OperationFailed {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isHookType tells whether or not the supplied object is a hook of the specified type
|
||||
func isHookType(hook *unstructured.Unstructured, hookType appv1.HookType) bool {
|
||||
annotations := hook.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return false
|
||||
}
|
||||
resHookTypes := strings.Split(annotations[common.AnnotationKeyHook], ",")
|
||||
for _, ht := range resHookTypes {
|
||||
if string(hookType) == strings.TrimSpace(ht) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// newHookStatus returns a hook status from an _live_ unstructured object
|
||||
func newHookStatus(hook *unstructured.Unstructured, hookType appv1.HookType) appv1.ResourceResult {
|
||||
gvk := hook.GroupVersionKind()
|
||||
hookStatus := appv1.ResourceResult{
|
||||
Name: hook.GetName(),
|
||||
Kind: hook.GetKind(),
|
||||
Group: gvk.Group,
|
||||
Version: gvk.Version,
|
||||
HookType: hookType,
|
||||
HookPhase: appv1.OperationRunning,
|
||||
Namespace: hook.GetNamespace(),
|
||||
}
|
||||
if isBatchJob(gvk) {
|
||||
updateStatusFromBatchJob(hook, &hookStatus)
|
||||
} else if isArgoWorkflow(gvk) {
|
||||
updateStatusFromArgoWorkflow(hook, &hookStatus)
|
||||
} else if isPod(gvk) {
|
||||
updateStatusFromPod(hook, &hookStatus)
|
||||
} else {
|
||||
hookStatus.HookPhase = appv1.OperationSucceeded
|
||||
hookStatus.Message = fmt.Sprintf("%s created", hook.GetName())
|
||||
}
|
||||
return hookStatus
|
||||
}
|
||||
|
||||
// isRunnable returns if the resource object is a runnable type which needs to be terminated
|
||||
func isRunnable(res *appv1.ResourceResult) bool {
|
||||
gvk := res.GroupVersionKind()
|
||||
return isBatchJob(gvk) || isArgoWorkflow(gvk) || isPod(gvk)
|
||||
}
|
||||
|
||||
func isBatchJob(gvk schema.GroupVersionKind) bool {
|
||||
return gvk.Group == "batch" && gvk.Kind == "Job"
|
||||
}
|
||||
|
||||
func updateStatusFromBatchJob(hook *unstructured.Unstructured, hookStatus *appv1.ResourceResult) {
|
||||
var job batch.Job
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(hook.Object, &job)
|
||||
if err != nil {
|
||||
hookStatus.HookPhase = appv1.OperationError
|
||||
hookStatus.Message = err.Error()
|
||||
return
|
||||
}
|
||||
failed := false
|
||||
var failMsg string
|
||||
complete := false
|
||||
var message string
|
||||
for _, condition := range job.Status.Conditions {
|
||||
switch condition.Type {
|
||||
case batch.JobFailed:
|
||||
failed = true
|
||||
complete = true
|
||||
failMsg = condition.Message
|
||||
case batch.JobComplete:
|
||||
complete = true
|
||||
message = condition.Message
|
||||
}
|
||||
}
|
||||
if !complete {
|
||||
hookStatus.HookPhase = appv1.OperationRunning
|
||||
hookStatus.Message = message
|
||||
} else if failed {
|
||||
hookStatus.HookPhase = appv1.OperationFailed
|
||||
hookStatus.Message = failMsg
|
||||
} else {
|
||||
hookStatus.HookPhase = appv1.OperationSucceeded
|
||||
hookStatus.Message = message
|
||||
}
|
||||
}
|
||||
|
||||
func isArgoWorkflow(gvk schema.GroupVersionKind) bool {
|
||||
return gvk.Group == "argoproj.io" && gvk.Kind == "Workflow"
|
||||
}
|
||||
|
||||
func updateStatusFromArgoWorkflow(hook *unstructured.Unstructured, hookStatus *appv1.ResourceResult) {
|
||||
var wf wfv1.Workflow
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(hook.Object, &wf)
|
||||
if err != nil {
|
||||
hookStatus.HookPhase = appv1.OperationError
|
||||
hookStatus.Message = err.Error()
|
||||
return
|
||||
}
|
||||
switch wf.Status.Phase {
|
||||
case wfv1.NodePending, wfv1.NodeRunning:
|
||||
hookStatus.HookPhase = appv1.OperationRunning
|
||||
case wfv1.NodeSucceeded:
|
||||
hookStatus.HookPhase = appv1.OperationSucceeded
|
||||
case wfv1.NodeFailed:
|
||||
hookStatus.HookPhase = appv1.OperationFailed
|
||||
case wfv1.NodeError:
|
||||
hookStatus.HookPhase = appv1.OperationError
|
||||
}
|
||||
hookStatus.Message = wf.Status.Message
|
||||
}
|
||||
|
||||
func isPod(gvk schema.GroupVersionKind) bool {
|
||||
return gvk.Group == "" && gvk.Kind == "Pod"
|
||||
}
|
||||
|
||||
func updateStatusFromPod(hook *unstructured.Unstructured, hookStatus *appv1.ResourceResult) {
|
||||
var pod apiv1.Pod
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(hook.Object, &pod)
|
||||
if err != nil {
|
||||
hookStatus.HookPhase = appv1.OperationError
|
||||
hookStatus.Message = err.Error()
|
||||
return
|
||||
}
|
||||
getFailMessage := func(ctr *apiv1.ContainerStatus) string {
|
||||
if ctr.State.Terminated != nil {
|
||||
if ctr.State.Terminated.Message != "" {
|
||||
return ctr.State.Terminated.Message
|
||||
}
|
||||
if ctr.State.Terminated.Reason == "OOMKilled" {
|
||||
return ctr.State.Terminated.Reason
|
||||
}
|
||||
if ctr.State.Terminated.ExitCode != 0 {
|
||||
return fmt.Sprintf("container %q failed with exit code %d", ctr.Name, ctr.State.Terminated.ExitCode)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
switch pod.Status.Phase {
|
||||
case apiv1.PodPending, apiv1.PodRunning:
|
||||
hookStatus.HookPhase = appv1.OperationRunning
|
||||
case apiv1.PodSucceeded:
|
||||
hookStatus.HookPhase = appv1.OperationSucceeded
|
||||
case apiv1.PodFailed:
|
||||
hookStatus.HookPhase = appv1.OperationFailed
|
||||
if pod.Status.Message != "" {
|
||||
// Pod has a nice error message. Use that.
|
||||
hookStatus.Message = pod.Status.Message
|
||||
return
|
||||
}
|
||||
for _, ctr := range append(pod.Status.InitContainerStatuses, pod.Status.ContainerStatuses...) {
|
||||
if msg := getFailMessage(&ctr); msg != "" {
|
||||
hookStatus.Message = msg
|
||||
return
|
||||
}
|
||||
}
|
||||
case apiv1.PodUnknown:
|
||||
hookStatus.HookPhase = appv1.OperationError
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *syncContext) getHookStatus(hookObj *unstructured.Unstructured, hookType appv1.HookType) *appv1.ResourceResult {
|
||||
for _, hr := range sc.syncRes.Resources {
|
||||
if !hr.IsHook() {
|
||||
continue
|
||||
}
|
||||
ns := util.FirstNonEmpty(hookObj.GetNamespace(), sc.namespace)
|
||||
if hookEqual(hr, hookObj.GroupVersionKind().Group, hookObj.GetKind(), ns, hookObj.GetName(), hookType) {
|
||||
return hr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hookEqual(hr *appv1.ResourceResult, group, kind, namespace, name string, hookType appv1.HookType) bool {
|
||||
return bool(
|
||||
hr.Group == group &&
|
||||
hr.Kind == kind &&
|
||||
hr.Namespace == namespace &&
|
||||
hr.Name == name &&
|
||||
hr.HookType == hookType)
|
||||
}
|
||||
|
||||
// updateHookStatus updates the status of a hook. Returns true if the hook was modified
|
||||
func (sc *syncContext) updateHookStatus(hookStatus appv1.ResourceResult) bool {
|
||||
sc.lock.Lock()
|
||||
defer sc.lock.Unlock()
|
||||
for i, prev := range sc.syncRes.Resources {
|
||||
if !prev.IsHook() {
|
||||
continue
|
||||
}
|
||||
if hookEqual(prev, hookStatus.Group, hookStatus.Kind, hookStatus.Namespace, hookStatus.Name, hookStatus.HookType) {
|
||||
if reflect.DeepEqual(prev, hookStatus) {
|
||||
return false
|
||||
}
|
||||
if prev.HookPhase != hookStatus.HookPhase {
|
||||
sc.log.Infof("Hook %s %s/%s hookPhase: %s -> %s", hookStatus.HookType, prev.Kind, prev.Name, prev.HookPhase, hookStatus.HookPhase)
|
||||
}
|
||||
if prev.Status != hookStatus.Status {
|
||||
sc.log.Infof("Hook %s %s/%s status: %s -> %s", hookStatus.HookType, prev.Kind, prev.Name, prev.Status, hookStatus.Status)
|
||||
}
|
||||
if prev.Message != hookStatus.Message {
|
||||
sc.log.Infof("Hook %s %s/%s message: '%s' -> '%s'", hookStatus.HookType, prev.Kind, prev.Name, prev.Message, hookStatus.Message)
|
||||
}
|
||||
sc.syncRes.Resources[i] = &hookStatus
|
||||
return true
|
||||
}
|
||||
}
|
||||
sc.syncRes.Resources = append(sc.syncRes.Resources, &hookStatus)
|
||||
sc.log.Infof("Set new hook %s %s/%s. phase: %s, message: %s", hookStatus.HookType, hookStatus.Kind, hookStatus.Name, hookStatus.HookPhase, hookStatus.Message)
|
||||
return true
|
||||
}
|
||||
|
||||
// areHooksCompletedSuccessful checks if all the hooks of the specified type are completed and successful
|
||||
func areHooksCompletedSuccessful(hookType appv1.HookType, hookStatuses []*appv1.ResourceResult) (bool, bool) {
|
||||
isSuccessful := true
|
||||
for _, hookStatus := range hookStatuses {
|
||||
if !hookStatus.IsHook() {
|
||||
continue
|
||||
}
|
||||
if hookStatus.HookType != hookType {
|
||||
continue
|
||||
}
|
||||
if !hookStatus.HookPhase.Completed() {
|
||||
return false, false
|
||||
}
|
||||
if !hookStatus.HookPhase.Successful() {
|
||||
isSuccessful = false
|
||||
}
|
||||
}
|
||||
return true, isSuccessful
|
||||
}
|
||||
|
||||
// terminate looks for any running jobs/workflow hooks and deletes the resource
|
||||
func (sc *syncContext) terminate() {
|
||||
terminateSuccessful := true
|
||||
for _, hookStatus := range sc.syncRes.Resources {
|
||||
if !hookStatus.IsHook() {
|
||||
continue
|
||||
}
|
||||
if hookStatus.HookPhase.Completed() {
|
||||
continue
|
||||
}
|
||||
if isRunnable(hookStatus) {
|
||||
hookStatus.HookPhase = appv1.OperationFailed
|
||||
err := sc.deleteHook(hookStatus.Name, hookStatus.Namespace, hookStatus.GroupVersionKind())
|
||||
if err != nil {
|
||||
hookStatus.Message = fmt.Sprintf("Failed to delete %s hook %s/%s: %v", hookStatus.HookType, hookStatus.Kind, hookStatus.Name, err)
|
||||
terminateSuccessful = false
|
||||
} else {
|
||||
hookStatus.Message = fmt.Sprintf("Deleted %s hook %s/%s", hookStatus.HookType, hookStatus.Kind, hookStatus.Name)
|
||||
}
|
||||
sc.updateHookStatus(*hookStatus)
|
||||
}
|
||||
}
|
||||
if terminateSuccessful {
|
||||
sc.setOperationPhase(appv1.OperationFailed, "Operation terminated")
|
||||
} else {
|
||||
sc.setOperationPhase(appv1.OperationError, "Operation termination had errors")
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *syncContext) deleteHook(name, namespace string, gvk schema.GroupVersionKind) error {
|
||||
apiResource, err := kube.ServerResourceForGroupVersionKind(sc.disco, gvk)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resource := kube.ToGroupVersionResource(gvk.GroupVersion().String(), apiResource)
|
||||
resIf := kube.ToResourceInterface(sc.dynamicIf, apiResource, resource, namespace)
|
||||
propagationPolicy := metav1.DeletePropagationForeground
|
||||
return resIf.Delete(name, &metav1.DeleteOptions{PropagationPolicy: &propagationPolicy})
|
||||
return phase, message, nil
|
||||
}
|
||||
|
||||
29
controller/sync_phase.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/hook"
|
||||
)
|
||||
|
||||
func syncPhases(obj *unstructured.Unstructured) []v1alpha1.SyncPhase {
|
||||
if hook.Skip(obj) {
|
||||
return nil
|
||||
} else if hook.IsHook(obj) {
|
||||
phasesMap := make(map[v1alpha1.SyncPhase]bool)
|
||||
for _, hookType := range hook.Types(obj) {
|
||||
switch hookType {
|
||||
case v1alpha1.HookTypePreSync, v1alpha1.HookTypeSync, v1alpha1.HookTypePostSync, v1alpha1.HookTypeSyncFail:
|
||||
phasesMap[v1alpha1.SyncPhase(hookType)] = true
|
||||
}
|
||||
}
|
||||
var phases []v1alpha1.SyncPhase
|
||||
for phase := range phasesMap {
|
||||
phases = append(phases, phase)
|
||||
}
|
||||
return phases
|
||||
} else {
|
||||
return []v1alpha1.SyncPhase{v1alpha1.SyncPhaseSync}
|
||||
}
|
||||
}
|
||||
57
controller/sync_phase_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
. "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/test"
|
||||
)
|
||||
|
||||
func TestSyncPhaseNone(t *testing.T) {
|
||||
assert.Equal(t, []SyncPhase{SyncPhaseSync}, syncPhases(&unstructured.Unstructured{}))
|
||||
}
|
||||
|
||||
func TestSyncPhasePreSync(t *testing.T) {
|
||||
assert.Equal(t, []SyncPhase{SyncPhasePreSync}, syncPhases(pod("PreSync")))
|
||||
}
|
||||
|
||||
func TestSyncPhaseSync(t *testing.T) {
|
||||
assert.Equal(t, []SyncPhase{SyncPhaseSync}, syncPhases(pod("Sync")))
|
||||
}
|
||||
|
||||
func TestSyncPhaseSkip(t *testing.T) {
|
||||
assert.Nil(t, syncPhases(pod("Skip")))
|
||||
}
|
||||
|
||||
// garbage hooks are still hooks, but have no phases, because some user spelled something wrong
|
||||
func TestSyncPhaseGarbage(t *testing.T) {
|
||||
assert.Nil(t, syncPhases(pod("Garbage")))
|
||||
}
|
||||
|
||||
func TestSyncPhasePost(t *testing.T) {
|
||||
assert.Equal(t, []SyncPhase{SyncPhasePostSync}, syncPhases(pod("PostSync")))
|
||||
}
|
||||
|
||||
func TestSyncPhaseFail(t *testing.T) {
|
||||
assert.Equal(t, []SyncPhase{SyncPhaseSyncFail}, syncPhases(pod("SyncFail")))
|
||||
}
|
||||
|
||||
func TestSyncPhaseTwoPhases(t *testing.T) {
|
||||
assert.ElementsMatch(t, []SyncPhase{SyncPhasePreSync, SyncPhasePostSync}, syncPhases(pod("PreSync,PostSync")))
|
||||
}
|
||||
|
||||
func TestSyncDuplicatedPhases(t *testing.T) {
|
||||
assert.ElementsMatch(t, []SyncPhase{SyncPhasePreSync}, syncPhases(pod("PreSync,PreSync")))
|
||||
assert.ElementsMatch(t, []SyncPhase{SyncPhasePreSync}, syncPhases(podWithHelmHook("pre-install,pre-upgrade")))
|
||||
}
|
||||
|
||||
func pod(hookType string) *unstructured.Unstructured {
|
||||
return test.Annotate(test.NewPod(), "argocd.argoproj.io/hook", hookType)
|
||||
}
|
||||
|
||||
func podWithHelmHook(hookType string) *unstructured.Unstructured {
|
||||
return test.Annotate(test.NewPod(), "helm.sh/hook", hookType)
|
||||
}
|
||||
130
controller/sync_task.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/hook"
|
||||
"github.com/argoproj/argo-cd/util/resource/syncwaves"
|
||||
)
|
||||
|
||||
// syncTask holds the live and target object. At least one should be non-nil. A targetObj of nil
|
||||
// indicates the live object needs to be pruned. A liveObj of nil indicates the object has yet to
|
||||
// be deployed
|
||||
type syncTask struct {
|
||||
phase v1alpha1.SyncPhase
|
||||
liveObj *unstructured.Unstructured
|
||||
targetObj *unstructured.Unstructured
|
||||
skipDryRun bool
|
||||
syncStatus v1alpha1.ResultCode
|
||||
operationState v1alpha1.OperationPhase
|
||||
message string
|
||||
}
|
||||
|
||||
func ternary(val bool, a, b string) string {
|
||||
if val {
|
||||
return a
|
||||
} else {
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
func (t *syncTask) String() string {
|
||||
return fmt.Sprintf("%s/%d %s %s/%s:%s/%s %s->%s (%s,%s,%s)",
|
||||
t.phase, t.wave(),
|
||||
ternary(t.isHook(), "hook", "resource"), t.group(), t.kind(), t.namespace(), t.name(),
|
||||
ternary(t.liveObj != nil, "obj", "nil"), ternary(t.targetObj != nil, "obj", "nil"),
|
||||
t.syncStatus, t.operationState, t.message,
|
||||
)
|
||||
}
|
||||
|
||||
func (t *syncTask) isPrune() bool {
|
||||
return t.targetObj == nil
|
||||
}
|
||||
|
||||
// return the target object (if this exists) otherwise the live object
|
||||
// some caution - often you explicitly want the live object not the target object
|
||||
func (t *syncTask) obj() *unstructured.Unstructured {
|
||||
return obj(t.targetObj, t.liveObj)
|
||||
}
|
||||
|
||||
func (t *syncTask) wave() int {
|
||||
return syncwaves.Wave(t.obj())
|
||||
}
|
||||
|
||||
func (t *syncTask) isHook() bool {
|
||||
return hook.IsHook(t.obj())
|
||||
}
|
||||
|
||||
func (t *syncTask) group() string {
|
||||
return t.groupVersionKind().Group
|
||||
}
|
||||
func (t *syncTask) kind() string {
|
||||
return t.groupVersionKind().Kind
|
||||
}
|
||||
|
||||
func (t *syncTask) version() string {
|
||||
return t.groupVersionKind().Version
|
||||
}
|
||||
|
||||
func (t *syncTask) groupVersionKind() schema.GroupVersionKind {
|
||||
return t.obj().GroupVersionKind()
|
||||
}
|
||||
|
||||
func (t *syncTask) name() string {
|
||||
return t.obj().GetName()
|
||||
}
|
||||
|
||||
func (t *syncTask) namespace() string {
|
||||
return t.obj().GetNamespace()
|
||||
}
|
||||
|
||||
func (t *syncTask) pending() bool {
|
||||
return t.operationState == ""
|
||||
}
|
||||
|
||||
func (t *syncTask) running() bool {
|
||||
return t.operationState.Running()
|
||||
}
|
||||
|
||||
func (t *syncTask) completed() bool {
|
||||
return t.operationState.Completed()
|
||||
}
|
||||
|
||||
func (t *syncTask) successful() bool {
|
||||
return t.operationState.Successful()
|
||||
}
|
||||
|
||||
func (t *syncTask) failed() bool {
|
||||
return t.operationState.Failed()
|
||||
}
|
||||
|
||||
func (t *syncTask) hookType() v1alpha1.HookType {
|
||||
if t.isHook() {
|
||||
return v1alpha1.HookType(t.phase)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (t *syncTask) hasHookDeletePolicy(policy v1alpha1.HookDeletePolicy) bool {
|
||||
// cannot have a policy if it is not a hook, it is meaningless
|
||||
if !t.isHook() {
|
||||
return false
|
||||
}
|
||||
for _, p := range hook.DeletePolicies(t.obj()) {
|
||||
if p == policy {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *syncTask) needsDeleting() bool {
|
||||
return t.liveObj != nil && (t.pending() && t.hasHookDeletePolicy(v1alpha1.HookDeletePolicyBeforeHookCreation) ||
|
||||
t.successful() && t.hasHookDeletePolicy(v1alpha1.HookDeletePolicyHookSucceeded) ||
|
||||
t.failed() && t.hasHookDeletePolicy(v1alpha1.HookDeletePolicyHookFailed))
|
||||
}
|
||||
66
controller/sync_task_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
. "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
. "github.com/argoproj/argo-cd/test"
|
||||
)
|
||||
|
||||
func Test_syncTask_hookType(t *testing.T) {
|
||||
type fields struct {
|
||||
phase SyncPhase
|
||||
liveObj *unstructured.Unstructured
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want HookType
|
||||
}{
|
||||
{"Empty", fields{SyncPhaseSync, NewPod()}, ""},
|
||||
{"PreSyncHook", fields{SyncPhasePreSync, NewHook(HookTypePreSync)}, HookTypePreSync},
|
||||
{"SyncHook", fields{SyncPhaseSync, NewHook(HookTypeSync)}, HookTypeSync},
|
||||
{"PostSyncHook", fields{SyncPhasePostSync, NewHook(HookTypePostSync)}, HookTypePostSync},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
task := &syncTask{
|
||||
phase: tt.fields.phase,
|
||||
liveObj: tt.fields.liveObj,
|
||||
}
|
||||
hookType := task.hookType()
|
||||
assert.EqualValues(t, tt.want, hookType)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_syncTask_hasHookDeletePolicy(t *testing.T) {
|
||||
assert.False(t, (&syncTask{targetObj: NewPod()}).hasHookDeletePolicy(HookDeletePolicyBeforeHookCreation))
|
||||
assert.False(t, (&syncTask{targetObj: NewPod()}).hasHookDeletePolicy(HookDeletePolicyHookSucceeded))
|
||||
assert.False(t, (&syncTask{targetObj: NewPod()}).hasHookDeletePolicy(HookDeletePolicyHookFailed))
|
||||
// must be hook
|
||||
assert.False(t, (&syncTask{targetObj: Annotate(NewPod(), "argocd.argoproj.io/hook-delete-policy", "BeforeHookCreation")}).hasHookDeletePolicy(HookDeletePolicyBeforeHookCreation))
|
||||
assert.True(t, (&syncTask{targetObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "BeforeHookCreation")}).hasHookDeletePolicy(HookDeletePolicyBeforeHookCreation))
|
||||
assert.True(t, (&syncTask{targetObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "HookSucceeded")}).hasHookDeletePolicy(HookDeletePolicyHookSucceeded))
|
||||
assert.True(t, (&syncTask{targetObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "HookFailed")}).hasHookDeletePolicy(HookDeletePolicyHookFailed))
|
||||
}
|
||||
|
||||
func Test_syncTask_needsDeleting(t *testing.T) {
|
||||
assert.False(t, (&syncTask{liveObj: NewPod()}).needsDeleting())
|
||||
// must be hook
|
||||
assert.False(t, (&syncTask{liveObj: Annotate(NewPod(), "argocd.argoproj.io/hook-delete-policy", "BeforeHookCreation")}).needsDeleting())
|
||||
// no need to delete if no live obj
|
||||
assert.False(t, (&syncTask{targetObj: Annotate(Annotate(NewPod(), "argoocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "BeforeHookCreation")}).needsDeleting())
|
||||
assert.True(t, (&syncTask{liveObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "BeforeHookCreation")}).needsDeleting())
|
||||
assert.True(t, (&syncTask{liveObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "BeforeHookCreation")}).needsDeleting())
|
||||
assert.True(t, (&syncTask{operationState: OperationSucceeded, liveObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "HookSucceeded")}).needsDeleting())
|
||||
assert.True(t, (&syncTask{operationState: OperationFailed, liveObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "HookFailed")}).needsDeleting())
|
||||
}
|
||||
|
||||
func Test_syncTask_wave(t *testing.T) {
|
||||
assert.Equal(t, 0, (&syncTask{targetObj: NewPod()}).wave())
|
||||
assert.Equal(t, 1, (&syncTask{targetObj: Annotate(NewPod(), "argocd.argoproj.io/sync-wave", "1")}).wave())
|
||||
}
|
||||
185
controller/sync_tasks.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
// kindOrder represents the correct order of Kubernetes resources within a manifest
|
||||
var syncPhaseOrder = map[v1alpha1.SyncPhase]int{
|
||||
v1alpha1.SyncPhasePreSync: -1,
|
||||
v1alpha1.SyncPhaseSync: 0,
|
||||
v1alpha1.SyncPhasePostSync: 1,
|
||||
v1alpha1.SyncPhaseSyncFail: 2,
|
||||
}
|
||||
|
||||
// kindOrder represents the correct order of Kubernetes resources within a manifest
|
||||
// https://github.com/helm/helm/blob/master/pkg/tiller/kind_sorter.go
|
||||
var kindOrder = map[string]int{}
|
||||
|
||||
func init() {
|
||||
kinds := []string{
|
||||
"Namespace",
|
||||
"ResourceQuota",
|
||||
"LimitRange",
|
||||
"PodSecurityPolicy",
|
||||
"PodDisruptionBudget",
|
||||
"Secret",
|
||||
"ConfigMap",
|
||||
"StorageClass",
|
||||
"PersistentVolume",
|
||||
"PersistentVolumeClaim",
|
||||
"ServiceAccount",
|
||||
"CustomResourceDefinition",
|
||||
"ClusterRole",
|
||||
"ClusterRoleBinding",
|
||||
"Role",
|
||||
"RoleBinding",
|
||||
"Service",
|
||||
"DaemonSet",
|
||||
"Pod",
|
||||
"ReplicationController",
|
||||
"ReplicaSet",
|
||||
"Deployment",
|
||||
"StatefulSet",
|
||||
"Job",
|
||||
"CronJob",
|
||||
"Ingress",
|
||||
"APIService",
|
||||
}
|
||||
for i, kind := range kinds {
|
||||
// make sure none of the above entries are zero, we need that for custom resources
|
||||
kindOrder[kind] = i - len(kinds)
|
||||
}
|
||||
}
|
||||
|
||||
type syncTasks []*syncTask
|
||||
|
||||
func (s syncTasks) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func (s syncTasks) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
// order is
|
||||
// 1. phase
|
||||
// 2. wave
|
||||
// 3. kind
|
||||
// 4. name
|
||||
func (s syncTasks) Less(i, j int) bool {
|
||||
|
||||
tA := s[i]
|
||||
tB := s[j]
|
||||
|
||||
d := syncPhaseOrder[tA.phase] - syncPhaseOrder[tB.phase]
|
||||
if d != 0 {
|
||||
return d < 0
|
||||
}
|
||||
|
||||
d = tA.wave() - tB.wave()
|
||||
if d != 0 {
|
||||
return d < 0
|
||||
}
|
||||
|
||||
a := tA.obj()
|
||||
b := tB.obj()
|
||||
|
||||
// we take advantage of the fact that if the kind is not in the kindOrder map,
|
||||
// then it will return the default int value of zero, which is the highest value
|
||||
d = kindOrder[a.GetKind()] - kindOrder[b.GetKind()]
|
||||
if d != 0 {
|
||||
return d < 0
|
||||
}
|
||||
|
||||
return a.GetName() < b.GetName()
|
||||
}
|
||||
|
||||
func (s syncTasks) Filter(predicate func(task *syncTask) bool) (tasks syncTasks) {
|
||||
for _, task := range s {
|
||||
if predicate(task) {
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
func (s syncTasks) Split(predicate func(task *syncTask) bool) (trueTasks, falseTasks syncTasks) {
|
||||
for _, task := range s {
|
||||
if predicate(task) {
|
||||
trueTasks = append(trueTasks, task)
|
||||
} else {
|
||||
falseTasks = append(falseTasks, task)
|
||||
}
|
||||
}
|
||||
return trueTasks, falseTasks
|
||||
}
|
||||
|
||||
func (s syncTasks) All(predicate func(task *syncTask) bool) bool {
|
||||
for _, task := range s {
|
||||
if !predicate(task) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s syncTasks) Any(predicate func(task *syncTask) bool) bool {
|
||||
for _, task := range s {
|
||||
if predicate(task) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s syncTasks) Find(predicate func(task *syncTask) bool) *syncTask {
|
||||
for _, task := range s {
|
||||
if predicate(task) {
|
||||
return task
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s syncTasks) String() string {
|
||||
var values []string
|
||||
for _, task := range s {
|
||||
values = append(values, task.String())
|
||||
}
|
||||
return "[" + strings.Join(values, ", ") + "]"
|
||||
}
|
||||
|
||||
func (s syncTasks) phase() v1alpha1.SyncPhase {
|
||||
if len(s) > 0 {
|
||||
return s[0].phase
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s syncTasks) wave() int {
|
||||
if len(s) > 0 {
|
||||
return s[0].wave()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s syncTasks) lastPhase() v1alpha1.SyncPhase {
|
||||
if len(s) > 0 {
|
||||
return s[len(s)-1].phase
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s syncTasks) lastWave() int {
|
||||
if len(s) > 0 {
|
||||
return s[len(s)-1].wave()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s syncTasks) multiStep() bool {
|
||||
return s.wave() != s.lastWave() || s.phase() != s.lastPhase()
|
||||
}
|
||||
392
controller/sync_tasks_test.go
Normal file
@@ -0,0 +1,392 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
. "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
. "github.com/argoproj/argo-cd/test"
|
||||
)
|
||||
|
||||
func Test_syncTasks_kindOrder(t *testing.T) {
|
||||
assert.Equal(t, -27, kindOrder["Namespace"])
|
||||
assert.Equal(t, -1, kindOrder["APIService"])
|
||||
assert.Equal(t, 0, kindOrder["MyCRD"])
|
||||
}
|
||||
|
||||
func TestSortSyncTask(t *testing.T) {
|
||||
sort.Sort(unsortedTasks)
|
||||
assert.Equal(t, sortedTasks, unsortedTasks)
|
||||
}
|
||||
|
||||
func TestAnySyncTasks(t *testing.T) {
|
||||
res := unsortedTasks.Any(func(task *syncTask) bool {
|
||||
return task.name() == "a"
|
||||
})
|
||||
assert.True(t, res)
|
||||
|
||||
res = unsortedTasks.Any(func(task *syncTask) bool {
|
||||
return task.name() == "does-not-exist"
|
||||
})
|
||||
assert.False(t, res)
|
||||
|
||||
}
|
||||
|
||||
func TestAllSyncTasks(t *testing.T) {
|
||||
res := unsortedTasks.All(func(task *syncTask) bool {
|
||||
return task.name() != ""
|
||||
})
|
||||
assert.False(t, res)
|
||||
|
||||
res = unsortedTasks.All(func(task *syncTask) bool {
|
||||
return task.name() == "a"
|
||||
})
|
||||
assert.False(t, res)
|
||||
}
|
||||
|
||||
func TestSplitSyncTasks(t *testing.T) {
|
||||
named, unnamed := sortedTasks.Split(func(task *syncTask) bool {
|
||||
return task.name() != ""
|
||||
})
|
||||
assert.Equal(t, named, namedObjTasks)
|
||||
assert.Equal(t, unnamed, unnamedTasks)
|
||||
}
|
||||
|
||||
var unsortedTasks = syncTasks{
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "Pod",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "Service",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "PersistentVolume",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
phase: SyncPhaseSyncFail, targetObj: &unstructured.Unstructured{},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"annotations": map[string]interface{}{
|
||||
"argocd.argoproj.io/sync-wave": "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "a",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"annotations": map[string]interface{}{
|
||||
"argocd.argoproj.io/sync-wave": "-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
phase: SyncPhasePreSync,
|
||||
targetObj: &unstructured.Unstructured{},
|
||||
},
|
||||
{
|
||||
phase: SyncPhasePostSync, targetObj: &unstructured.Unstructured{},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "ConfigMap",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var sortedTasks = syncTasks{
|
||||
{
|
||||
phase: SyncPhasePreSync,
|
||||
targetObj: &unstructured.Unstructured{},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"annotations": map[string]interface{}{
|
||||
"argocd.argoproj.io/sync-wave": "-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "ConfigMap",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "PersistentVolume",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "Service",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "Pod",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "a",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"annotations": map[string]interface{}{
|
||||
"argocd.argoproj.io/sync-wave": "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
phase: SyncPhasePostSync,
|
||||
targetObj: &unstructured.Unstructured{},
|
||||
},
|
||||
{
|
||||
phase: SyncPhaseSyncFail,
|
||||
targetObj: &unstructured.Unstructured{},
|
||||
},
|
||||
}
|
||||
|
||||
var namedObjTasks = syncTasks{
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "a",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var unnamedTasks = syncTasks{
|
||||
{
|
||||
phase: SyncPhasePreSync,
|
||||
targetObj: &unstructured.Unstructured{},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"annotations": map[string]interface{}{
|
||||
"argocd.argoproj.io/sync-wave": "-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "ConfigMap",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "PersistentVolume",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "Service",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "Pod",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"annotations": map[string]interface{}{
|
||||
"argocd.argoproj.io/sync-wave": "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
phase: SyncPhasePostSync,
|
||||
targetObj: &unstructured.Unstructured{},
|
||||
},
|
||||
{
|
||||
phase: SyncPhaseSyncFail,
|
||||
targetObj: &unstructured.Unstructured{},
|
||||
},
|
||||
}
|
||||
|
||||
func Test_syncTasks_Filter(t *testing.T) {
|
||||
tasks := syncTasks{{phase: SyncPhaseSync}, {phase: SyncPhasePostSync}}
|
||||
|
||||
assert.Equal(t, syncTasks{{phase: SyncPhaseSync}}, tasks.Filter(func(t *syncTask) bool {
|
||||
return t.phase == SyncPhaseSync
|
||||
}))
|
||||
}
|
||||
|
||||
func TestSyncNamespaceAgainstCRD(t *testing.T) {
|
||||
crd := &syncTask{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "Workflow",
|
||||
},
|
||||
}}
|
||||
namespace := &syncTask{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "Namespace",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
unsorted := syncTasks{crd, namespace}
|
||||
sort.Sort(unsorted)
|
||||
|
||||
assert.Equal(t, syncTasks{namespace, crd}, unsorted)
|
||||
}
|
||||
|
||||
func Test_syncTasks_multiStep(t *testing.T) {
|
||||
t.Run("Single", func(t *testing.T) {
|
||||
tasks := syncTasks{{liveObj: Annotate(NewPod(), common.AnnotationSyncWave, "-1"), phase: SyncPhaseSync}}
|
||||
assert.Equal(t, SyncPhaseSync, tasks.phase())
|
||||
assert.Equal(t, -1, tasks.wave())
|
||||
assert.Equal(t, SyncPhaseSync, tasks.lastPhase())
|
||||
assert.Equal(t, -1, tasks.lastWave())
|
||||
assert.False(t, tasks.multiStep())
|
||||
})
|
||||
t.Run("Double", func(t *testing.T) {
|
||||
tasks := syncTasks{
|
||||
{liveObj: Annotate(NewPod(), common.AnnotationSyncWave, "-1"), phase: SyncPhasePreSync},
|
||||
{liveObj: Annotate(NewPod(), common.AnnotationSyncWave, "1"), phase: SyncPhasePostSync},
|
||||
}
|
||||
assert.Equal(t, SyncPhasePreSync, tasks.phase())
|
||||
assert.Equal(t, -1, tasks.wave())
|
||||
assert.Equal(t, SyncPhasePostSync, tasks.lastPhase())
|
||||
assert.Equal(t, 1, tasks.lastWave())
|
||||
assert.True(t, tasks.multiStep())
|
||||
})
|
||||
}
|
||||
@@ -2,24 +2,25 @@ package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
fakedisco "k8s.io/client-go/discovery/fake"
|
||||
"k8s.io/client-go/dynamic/fake"
|
||||
"k8s.io/client-go/rest"
|
||||
testcore "k8s.io/client-go/testing"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/reposerver/repository"
|
||||
. "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/test"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/kube/kubetest"
|
||||
@@ -43,9 +44,12 @@ func newTestSyncCtx(resources ...*v1.APIResourceList) *syncContext {
|
||||
})
|
||||
sc := syncContext{
|
||||
config: &rest.Config{},
|
||||
rawConfig: &rest.Config{},
|
||||
namespace: test.FakeArgoCDNamespace,
|
||||
server: test.FakeClusterURL,
|
||||
syncRes: &v1alpha1.SyncOperationResult{},
|
||||
syncRes: &v1alpha1.SyncOperationResult{
|
||||
Revision: "FooBarBaz",
|
||||
},
|
||||
syncOp: &v1alpha1.SyncOperation{
|
||||
Prune: true,
|
||||
SyncStrategy: &v1alpha1.SyncStrategy{
|
||||
@@ -70,10 +74,21 @@ func newTestSyncCtx(resources ...*v1.APIResourceList) *syncContext {
|
||||
disco: fakeDisco,
|
||||
log: log.WithFields(log.Fields{"application": "fake-app"}),
|
||||
}
|
||||
sc.kubectl = kubetest.MockKubectlCmd{}
|
||||
sc.kubectl = &kubetest.MockKubectlCmd{}
|
||||
return &sc
|
||||
}
|
||||
|
||||
func newManagedResource(live *unstructured.Unstructured) managedResource {
|
||||
return managedResource{
|
||||
Live: live,
|
||||
Group: live.GroupVersionKind().Group,
|
||||
Version: live.GroupVersionKind().Version,
|
||||
Kind: live.GroupVersionKind().Kind,
|
||||
Namespace: live.GetNamespace(),
|
||||
Name: live.GetName(),
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncNotPermittedNamespace(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
targetPod := test.NewPod()
|
||||
@@ -104,18 +119,19 @@ func TestSyncCreateInSortedOrder(t *testing.T) {
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Equal(t, v1alpha1.OperationSucceeded, syncCtx.opState.Phase)
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 2)
|
||||
for i := range syncCtx.syncRes.Resources {
|
||||
if syncCtx.syncRes.Resources[i].Kind == "Pod" {
|
||||
assert.Equal(t, v1alpha1.ResultCodeSynced, syncCtx.syncRes.Resources[i].Status)
|
||||
} else if syncCtx.syncRes.Resources[i].Kind == "Service" {
|
||||
assert.Equal(t, v1alpha1.ResultCodeSynced, syncCtx.syncRes.Resources[i].Status)
|
||||
result := syncCtx.syncRes.Resources[i]
|
||||
if result.Kind == "Pod" {
|
||||
assert.Equal(t, v1alpha1.ResultCodeSynced, result.Status)
|
||||
assert.Equal(t, "", result.Message)
|
||||
} else if result.Kind == "Service" {
|
||||
assert.Equal(t, "", result.Message)
|
||||
} else {
|
||||
t.Error("Resource isn't a pod or a service")
|
||||
}
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Equal(t, syncCtx.opState.Phase, v1alpha1.OperationSucceeded)
|
||||
}
|
||||
|
||||
func TestSyncCreateNotWhitelistedClusterResources(t *testing.T) {
|
||||
@@ -136,7 +152,7 @@ func TestSyncCreateNotWhitelistedClusterResources(t *testing.T) {
|
||||
{Group: "argoproj.io", Kind: "*"},
|
||||
}
|
||||
|
||||
syncCtx.kubectl = kubetest.MockKubectlCmd{}
|
||||
syncCtx.kubectl = &kubetest.MockKubectlCmd{}
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: nil,
|
||||
@@ -147,8 +163,9 @@ func TestSyncCreateNotWhitelistedClusterResources(t *testing.T) {
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 1)
|
||||
assert.Equal(t, v1alpha1.ResultCodeSyncFailed, syncCtx.syncRes.Resources[0].Status)
|
||||
assert.Contains(t, syncCtx.syncRes.Resources[0].Message, "not permitted in project")
|
||||
result := syncCtx.syncRes.Resources[0]
|
||||
assert.Equal(t, v1alpha1.ResultCodeSyncFailed, result.Status)
|
||||
assert.Contains(t, result.Message, "not permitted in project")
|
||||
}
|
||||
|
||||
func TestSyncBlacklistedNamespacedResources(t *testing.T) {
|
||||
@@ -166,73 +183,83 @@ func TestSyncBlacklistedNamespacedResources(t *testing.T) {
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 1)
|
||||
assert.Equal(t, v1alpha1.ResultCodeSyncFailed, syncCtx.syncRes.Resources[0].Status)
|
||||
assert.Contains(t, syncCtx.syncRes.Resources[0].Message, "not permitted in project")
|
||||
result := syncCtx.syncRes.Resources[0]
|
||||
assert.Equal(t, v1alpha1.ResultCodeSyncFailed, result.Status)
|
||||
assert.Contains(t, result.Message, "not permitted in project")
|
||||
}
|
||||
|
||||
func TestSyncSuccessfully(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
pod := test.NewPod()
|
||||
pod.SetNamespace(test.FakeArgoCDNamespace)
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: nil,
|
||||
Target: test.NewService(),
|
||||
}, {
|
||||
Live: test.NewPod(),
|
||||
Live: pod,
|
||||
Target: nil,
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Equal(t, v1alpha1.OperationSucceeded, syncCtx.opState.Phase)
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 2)
|
||||
for i := range syncCtx.syncRes.Resources {
|
||||
if syncCtx.syncRes.Resources[i].Kind == "Pod" {
|
||||
assert.Equal(t, v1alpha1.ResultCodePruned, syncCtx.syncRes.Resources[i].Status)
|
||||
} else if syncCtx.syncRes.Resources[i].Kind == "Service" {
|
||||
assert.Equal(t, v1alpha1.ResultCodeSynced, syncCtx.syncRes.Resources[i].Status)
|
||||
result := syncCtx.syncRes.Resources[i]
|
||||
if result.Kind == "Pod" {
|
||||
assert.Equal(t, v1alpha1.ResultCodePruned, result.Status)
|
||||
assert.Equal(t, "pruned", result.Message)
|
||||
} else if result.Kind == "Service" {
|
||||
assert.Equal(t, v1alpha1.ResultCodeSynced, result.Status)
|
||||
assert.Equal(t, "", result.Message)
|
||||
} else {
|
||||
t.Error("Resource isn't a pod or a service")
|
||||
}
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Equal(t, syncCtx.opState.Phase, v1alpha1.OperationSucceeded)
|
||||
}
|
||||
|
||||
func TestSyncDeleteSuccessfully(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
svc := test.NewService()
|
||||
svc.SetNamespace(test.FakeArgoCDNamespace)
|
||||
pod := test.NewPod()
|
||||
pod.SetNamespace(test.FakeArgoCDNamespace)
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: test.NewService(),
|
||||
Live: svc,
|
||||
Target: nil,
|
||||
}, {
|
||||
Live: test.NewPod(),
|
||||
Live: pod,
|
||||
Target: nil,
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Equal(t, v1alpha1.OperationSucceeded, syncCtx.opState.Phase)
|
||||
for i := range syncCtx.syncRes.Resources {
|
||||
if syncCtx.syncRes.Resources[i].Kind == "Pod" {
|
||||
assert.Equal(t, v1alpha1.ResultCodePruned, syncCtx.syncRes.Resources[i].Status)
|
||||
} else if syncCtx.syncRes.Resources[i].Kind == "Service" {
|
||||
assert.Equal(t, v1alpha1.ResultCodePruned, syncCtx.syncRes.Resources[i].Status)
|
||||
result := syncCtx.syncRes.Resources[i]
|
||||
if result.Kind == "Pod" {
|
||||
assert.Equal(t, v1alpha1.ResultCodePruned, result.Status)
|
||||
assert.Equal(t, "pruned", result.Message)
|
||||
} else if result.Kind == "Service" {
|
||||
assert.Equal(t, v1alpha1.ResultCodePruned, result.Status)
|
||||
assert.Equal(t, "pruned", result.Message)
|
||||
} else {
|
||||
t.Error("Resource isn't a pod or a service")
|
||||
}
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Equal(t, syncCtx.opState.Phase, v1alpha1.OperationSucceeded)
|
||||
}
|
||||
|
||||
func TestSyncCreateFailure(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.kubectl = kubetest.MockKubectlCmd{
|
||||
testSvc := test.NewService()
|
||||
syncCtx.kubectl = &kubetest.MockKubectlCmd{
|
||||
Commands: map[string]kubetest.KubectlOutput{
|
||||
"test-service": {
|
||||
testSvc.GetName(): {
|
||||
Output: "",
|
||||
Err: fmt.Errorf("error: error validating \"test.yaml\": error validating data: apiVersion not set; if you choose to ignore these errors, turn validation off with --validate=false"),
|
||||
Err: fmt.Errorf("foo"),
|
||||
},
|
||||
},
|
||||
}
|
||||
testSvc := test.NewService()
|
||||
testSvc.SetAPIVersion("")
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: nil,
|
||||
@@ -241,21 +268,24 @@ func TestSyncCreateFailure(t *testing.T) {
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 1)
|
||||
assert.Equal(t, v1alpha1.ResultCodeSyncFailed, syncCtx.syncRes.Resources[0].Status)
|
||||
result := syncCtx.syncRes.Resources[0]
|
||||
assert.Equal(t, v1alpha1.ResultCodeSyncFailed, result.Status)
|
||||
assert.Equal(t, "foo", result.Message)
|
||||
}
|
||||
|
||||
func TestSyncPruneFailure(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.kubectl = kubetest.MockKubectlCmd{
|
||||
syncCtx.kubectl = &kubetest.MockKubectlCmd{
|
||||
Commands: map[string]kubetest.KubectlOutput{
|
||||
"test-service": {
|
||||
Output: "",
|
||||
Err: fmt.Errorf(" error: timed out waiting for \"test-service\" to be synced"),
|
||||
Err: fmt.Errorf("foo"),
|
||||
},
|
||||
},
|
||||
}
|
||||
testSvc := test.NewService()
|
||||
testSvc.SetName("test-service")
|
||||
testSvc.SetNamespace(test.FakeArgoCDNamespace)
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: testSvc,
|
||||
@@ -263,155 +293,11 @@ func TestSyncPruneFailure(t *testing.T) {
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Equal(t, v1alpha1.OperationFailed, syncCtx.opState.Phase)
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 1)
|
||||
assert.Equal(t, v1alpha1.ResultCodeSyncFailed, syncCtx.syncRes.Resources[0].Status)
|
||||
}
|
||||
|
||||
func unsortedManifest() []syncTask {
|
||||
return []syncTask{
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "Pod",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "Service",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "PersistentVolume",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "ConfigMap",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func sortedManifest() []syncTask {
|
||||
return []syncTask{
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "ConfigMap",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "PersistentVolume",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "Service",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "Pod",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortKubernetesResourcesSuccessfully(t *testing.T) {
|
||||
unsorted := unsortedManifest()
|
||||
ks := newKindSorter(unsorted, resourceOrder)
|
||||
sort.Sort(ks)
|
||||
|
||||
expectedOrder := sortedManifest()
|
||||
assert.Equal(t, len(unsorted), len(expectedOrder))
|
||||
for i, sorted := range unsorted {
|
||||
assert.Equal(t, expectedOrder[i], sorted)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestSortManifestHandleNil(t *testing.T) {
|
||||
task := syncTask{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "Service",
|
||||
},
|
||||
},
|
||||
}
|
||||
manifest := []syncTask{
|
||||
{},
|
||||
task,
|
||||
}
|
||||
ks := newKindSorter(manifest, resourceOrder)
|
||||
sort.Sort(ks)
|
||||
assert.Equal(t, task, manifest[0])
|
||||
assert.Nil(t, manifest[1].targetObj)
|
||||
}
|
||||
|
||||
func TestSyncNamespaceAgainstCRD(t *testing.T) {
|
||||
crd := syncTask{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": "argoproj.io/alpha1",
|
||||
"kind": "Workflow",
|
||||
},
|
||||
}}
|
||||
namespace := syncTask{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "Namespace",
|
||||
},
|
||||
},
|
||||
}
|
||||
unsorted := []syncTask{crd, namespace}
|
||||
ks := newKindSorter(unsorted, resourceOrder)
|
||||
sort.Sort(ks)
|
||||
|
||||
expectedOrder := []syncTask{namespace, crd}
|
||||
assert.Equal(t, len(unsorted), len(expectedOrder))
|
||||
for i, sorted := range unsorted {
|
||||
assert.Equal(t, expectedOrder[i], sorted)
|
||||
}
|
||||
result := syncCtx.syncRes.Resources[0]
|
||||
assert.Equal(t, v1alpha1.ResultCodeSyncFailed, result.Status)
|
||||
assert.Equal(t, "foo", result.Message)
|
||||
}
|
||||
|
||||
func TestDontSyncOrPruneHooks(t *testing.T) {
|
||||
@@ -421,23 +307,270 @@ func TestDontSyncOrPruneHooks(t *testing.T) {
|
||||
targetPod.SetAnnotations(map[string]string{common.AnnotationKeyHook: "PreSync"})
|
||||
liveSvc := test.NewService()
|
||||
liveSvc.SetName("dont-prune-me")
|
||||
liveSvc.SetNamespace(test.FakeArgoCDNamespace)
|
||||
liveSvc.SetAnnotations(map[string]string{common.AnnotationKeyHook: "PreSync"})
|
||||
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: nil,
|
||||
Target: targetPod,
|
||||
Hook: true,
|
||||
}, {
|
||||
Live: liveSvc,
|
||||
Target: nil,
|
||||
Hook: true,
|
||||
}},
|
||||
hooks: []*unstructured.Unstructured{targetPod, liveSvc},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 0)
|
||||
syncCtx.sync()
|
||||
assert.Equal(t, syncCtx.opState.Phase, v1alpha1.OperationSucceeded)
|
||||
assert.Equal(t, v1alpha1.OperationSucceeded, syncCtx.opState.Phase)
|
||||
}
|
||||
|
||||
// make sure that we do not prune resources with Prune=false
|
||||
func TestDontPrunePruneFalse(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
pod := test.NewPod()
|
||||
pod.SetAnnotations(map[string]string{common.AnnotationSyncOptions: "Prune=false"})
|
||||
pod.SetNamespace(test.FakeArgoCDNamespace)
|
||||
syncCtx.compareResult = &comparisonResult{managedResources: []managedResource{{Live: pod}}}
|
||||
|
||||
syncCtx.sync()
|
||||
|
||||
assert.Equal(t, v1alpha1.OperationSucceeded, syncCtx.opState.Phase)
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 1)
|
||||
assert.Equal(t, v1alpha1.ResultCodePruneSkipped, syncCtx.syncRes.Resources[0].Status)
|
||||
assert.Equal(t, "ignored (no prune)", syncCtx.syncRes.Resources[0].Message)
|
||||
|
||||
syncCtx.sync()
|
||||
|
||||
assert.Equal(t, v1alpha1.OperationSucceeded, syncCtx.opState.Phase)
|
||||
}
|
||||
|
||||
// make sure Validate=false means we don't validate
|
||||
func TestSyncOptionValidate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
annotationVal string
|
||||
want bool
|
||||
}{
|
||||
{"Empty", "", true},
|
||||
{"True", "Validate=true", true},
|
||||
{"False", "Validate=false", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
pod := test.NewPod()
|
||||
pod.SetAnnotations(map[string]string{common.AnnotationSyncOptions: tt.annotationVal})
|
||||
pod.SetNamespace(test.FakeArgoCDNamespace)
|
||||
syncCtx.compareResult = &comparisonResult{managedResources: []managedResource{{Target: pod, Live: pod}}}
|
||||
|
||||
syncCtx.sync()
|
||||
|
||||
kubectl, _ := syncCtx.kubectl.(*kubetest.MockKubectlCmd)
|
||||
assert.Equal(t, tt.want, kubectl.LastValidate)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// make sure Validate means we don't validate
|
||||
func TestSyncValidate(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
pod := test.NewPod()
|
||||
pod.SetNamespace(test.FakeArgoCDNamespace)
|
||||
syncCtx.compareResult = &comparisonResult{managedResources: []managedResource{{Target: pod, Live: pod}}}
|
||||
syncCtx.syncOp.SyncOptions = SyncOptions{"Validate=false"}
|
||||
|
||||
syncCtx.sync()
|
||||
|
||||
kubectl := syncCtx.kubectl.(*kubetest.MockKubectlCmd)
|
||||
assert.False(t, kubectl.LastValidate)
|
||||
}
|
||||
|
||||
func TestSelectiveSyncOnly(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
pod1 := test.NewPod()
|
||||
pod1.SetName("pod-1")
|
||||
pod2 := test.NewPod()
|
||||
pod2.SetName("pod-2")
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{Target: pod1}},
|
||||
}
|
||||
syncCtx.syncResources = []v1alpha1.SyncOperationResource{{Kind: "Pod", Name: "pod-1"}}
|
||||
|
||||
tasks, successful := syncCtx.getSyncTasks()
|
||||
|
||||
assert.True(t, successful)
|
||||
assert.Len(t, tasks, 1)
|
||||
assert.Equal(t, "pod-1", tasks[0].name())
|
||||
}
|
||||
|
||||
func TestUnnamedHooksGetUniqueNames(t *testing.T) {
|
||||
t.Run("Truncated revision", func(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.syncOp.SyncStrategy.Apply = nil
|
||||
pod := test.NewPod()
|
||||
pod.SetName("")
|
||||
pod.SetAnnotations(map[string]string{common.AnnotationKeyHook: "PreSync,PostSync"})
|
||||
syncCtx.compareResult = &comparisonResult{hooks: []*unstructured.Unstructured{pod}}
|
||||
|
||||
tasks, successful := syncCtx.getSyncTasks()
|
||||
|
||||
assert.True(t, successful)
|
||||
assert.Len(t, tasks, 2)
|
||||
assert.Contains(t, tasks[0].name(), "foobarb-presync-")
|
||||
assert.Contains(t, tasks[1].name(), "foobarb-postsync-")
|
||||
assert.Equal(t, "", pod.GetName())
|
||||
})
|
||||
|
||||
t.Run("Short revision", func(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.syncOp.SyncStrategy.Apply = nil
|
||||
pod := test.NewPod()
|
||||
pod.SetName("")
|
||||
pod.SetAnnotations(map[string]string{common.AnnotationKeyHook: "PreSync,PostSync"})
|
||||
syncCtx.compareResult = &comparisonResult{hooks: []*unstructured.Unstructured{pod}}
|
||||
syncCtx.syncRes.Revision = "foobar"
|
||||
tasks, successful := syncCtx.getSyncTasks()
|
||||
|
||||
assert.True(t, successful)
|
||||
assert.Len(t, tasks, 2)
|
||||
assert.Contains(t, tasks[0].name(), "foobar-presync-")
|
||||
assert.Contains(t, tasks[1].name(), "foobar-postsync-")
|
||||
assert.Equal(t, "", pod.GetName())
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestManagedResourceAreNotNamed(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
pod := test.NewPod()
|
||||
pod.SetName("")
|
||||
syncCtx.compareResult = &comparisonResult{managedResources: []managedResource{{Target: pod}}}
|
||||
|
||||
tasks, successful := syncCtx.getSyncTasks()
|
||||
|
||||
assert.True(t, successful)
|
||||
assert.Len(t, tasks, 1)
|
||||
assert.Equal(t, "", tasks[0].name())
|
||||
assert.Equal(t, "", pod.GetName())
|
||||
}
|
||||
|
||||
func TestDeDupingTasks(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.syncOp.SyncStrategy.Apply = nil
|
||||
pod := test.NewPod()
|
||||
pod.SetAnnotations(map[string]string{common.AnnotationKeyHook: "Sync"})
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{Target: pod}},
|
||||
hooks: []*unstructured.Unstructured{pod},
|
||||
}
|
||||
|
||||
tasks, successful := syncCtx.getSyncTasks()
|
||||
|
||||
assert.True(t, successful)
|
||||
assert.Len(t, tasks, 1)
|
||||
}
|
||||
|
||||
func TestObjectsGetANamespace(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
pod := test.NewPod()
|
||||
syncCtx.compareResult = &comparisonResult{managedResources: []managedResource{{Target: pod}}}
|
||||
|
||||
tasks, successful := syncCtx.getSyncTasks()
|
||||
|
||||
assert.True(t, successful)
|
||||
assert.Len(t, tasks, 1)
|
||||
assert.Equal(t, test.FakeArgoCDNamespace, tasks[0].namespace())
|
||||
assert.Equal(t, "", pod.GetNamespace())
|
||||
}
|
||||
|
||||
func TestSyncCustomResources(t *testing.T) {
|
||||
type fields struct {
|
||||
skipDryRunAnnotationPresent bool
|
||||
crdAlreadyPresent bool
|
||||
crdInSameSync bool
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
wantDryRun bool
|
||||
wantSuccess bool
|
||||
}{
|
||||
|
||||
{"unknown crd", fields{
|
||||
skipDryRunAnnotationPresent: false, crdAlreadyPresent: false, crdInSameSync: false,
|
||||
}, true, false},
|
||||
{"crd present in same sync", fields{
|
||||
skipDryRunAnnotationPresent: false, crdAlreadyPresent: false, crdInSameSync: true,
|
||||
}, false, true},
|
||||
{"crd is already present in cluster", fields{
|
||||
skipDryRunAnnotationPresent: false, crdAlreadyPresent: true, crdInSameSync: false,
|
||||
}, true, true},
|
||||
{"crd is already present in cluster, skip dry run annotated", fields{
|
||||
skipDryRunAnnotationPresent: true, crdAlreadyPresent: true, crdInSameSync: false,
|
||||
}, true, true},
|
||||
{"unknown crd, skip dry run annotated", fields{
|
||||
skipDryRunAnnotationPresent: true, crdAlreadyPresent: false, crdInSameSync: false,
|
||||
}, false, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
knownCustomResourceTypes := []v1.APIResource{}
|
||||
if tt.fields.crdAlreadyPresent {
|
||||
knownCustomResourceTypes = append(knownCustomResourceTypes, v1.APIResource{Kind: "TestCrd", Group: "argoproj.io", Version: "v1", Namespaced: true})
|
||||
}
|
||||
|
||||
syncCtx := newTestSyncCtx(
|
||||
&v1.APIResourceList{
|
||||
GroupVersion: "argoproj.io/v1",
|
||||
APIResources: knownCustomResourceTypes,
|
||||
},
|
||||
&v1.APIResourceList{
|
||||
GroupVersion: "apiextensions.k8s.io/v1beta1",
|
||||
APIResources: []v1.APIResource{
|
||||
{Kind: "CustomResourceDefinition", Group: "apiextensions.k8s.io", Version: "v1beta1", Namespaced: true},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
cr := test.Unstructured(`
|
||||
{
|
||||
"apiVersion": "argoproj.io/v1",
|
||||
"kind": "TestCrd",
|
||||
"metadata": {
|
||||
"name": "my-resource"
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
if tt.fields.skipDryRunAnnotationPresent {
|
||||
cr.SetAnnotations(map[string]string{common.AnnotationSyncOptions: "SkipDryRunOnMissingResource=true"})
|
||||
}
|
||||
|
||||
resources := []managedResource{{Target: cr}}
|
||||
if tt.fields.crdInSameSync {
|
||||
resources = append(resources, managedResource{Target: test.NewCRD()})
|
||||
}
|
||||
|
||||
syncCtx.compareResult = &comparisonResult{managedResources: resources}
|
||||
|
||||
tasks, successful := syncCtx.getSyncTasks()
|
||||
|
||||
if successful != tt.wantSuccess {
|
||||
t.Errorf("successful = %v, want: %v", successful, tt.wantSuccess)
|
||||
return
|
||||
}
|
||||
|
||||
skipDryRun := false
|
||||
for _, task := range tasks {
|
||||
if task.targetObj.GetKind() == cr.GetKind() {
|
||||
skipDryRun = task.skipDryRun
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if tt.wantDryRun != !skipDryRun {
|
||||
t.Errorf("dryRun = %v, want: %v", !skipDryRun, tt.wantDryRun)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPersistRevisionHistory(t *testing.T) {
|
||||
@@ -453,7 +586,7 @@ func TestPersistRevisionHistory(t *testing.T) {
|
||||
}
|
||||
data := fakeData{
|
||||
apps: []runtime.Object{app, defaultProject},
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
@@ -490,7 +623,7 @@ func TestPersistRevisionHistoryRollback(t *testing.T) {
|
||||
}
|
||||
data := fakeData{
|
||||
apps: []runtime.Object{app, defaultProject},
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
@@ -526,3 +659,171 @@ func TestPersistRevisionHistoryRollback(t *testing.T) {
|
||||
assert.Equal(t, source, updatedApp.Status.History[0].Source)
|
||||
assert.Equal(t, "abc123", updatedApp.Status.History[0].Revision)
|
||||
}
|
||||
|
||||
func TestSyncFailureHookWithSuccessfulSync(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.syncOp.SyncStrategy.Apply = nil
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{Target: test.NewPod()}},
|
||||
hooks: []*unstructured.Unstructured{test.NewHook(HookTypeSyncFail)},
|
||||
}
|
||||
|
||||
syncCtx.sync()
|
||||
|
||||
assert.Equal(t, OperationSucceeded, syncCtx.opState.Phase)
|
||||
// only one result, we did not run the failure failureHook
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 1)
|
||||
}
|
||||
|
||||
func TestSyncFailureHookWithFailedSync(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.syncOp.SyncStrategy.Apply = nil
|
||||
pod := test.NewPod()
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{Target: pod}},
|
||||
hooks: []*unstructured.Unstructured{test.NewHook(HookTypeSyncFail)},
|
||||
}
|
||||
syncCtx.kubectl = &kubetest.MockKubectlCmd{
|
||||
Commands: map[string]kubetest.KubectlOutput{pod.GetName(): {Err: fmt.Errorf("")}},
|
||||
}
|
||||
|
||||
syncCtx.sync()
|
||||
syncCtx.sync()
|
||||
|
||||
assert.Equal(t, OperationFailed, syncCtx.opState.Phase)
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 2)
|
||||
}
|
||||
|
||||
func TestBeforeHookCreation(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.syncOp.SyncStrategy.Apply = nil
|
||||
hook := test.Annotate(test.Annotate(test.NewPod(), common.AnnotationKeyHook, "Sync"), common.AnnotationKeyHookDeletePolicy, "BeforeHookCreation")
|
||||
hook.SetNamespace(test.FakeArgoCDNamespace)
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{newManagedResource(hook)},
|
||||
hooks: []*unstructured.Unstructured{hook},
|
||||
}
|
||||
syncCtx.dynamicIf = fake.NewSimpleDynamicClient(runtime.NewScheme())
|
||||
|
||||
syncCtx.sync()
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 1)
|
||||
assert.Empty(t, syncCtx.syncRes.Resources[0].Message)
|
||||
}
|
||||
|
||||
func TestRunSyncFailHooksFailed(t *testing.T) {
|
||||
// Tests that other SyncFail Hooks run even if one of them fail.
|
||||
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.syncOp.SyncStrategy.Apply = nil
|
||||
pod := test.NewPod()
|
||||
successfulSyncFailHook := test.NewHook(HookTypeSyncFail)
|
||||
successfulSyncFailHook.SetName("successful-sync-fail-hook")
|
||||
failedSyncFailHook := test.NewHook(HookTypeSyncFail)
|
||||
failedSyncFailHook.SetName("failed-sync-fail-hook")
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{Target: pod}},
|
||||
hooks: []*unstructured.Unstructured{successfulSyncFailHook, failedSyncFailHook},
|
||||
}
|
||||
|
||||
syncCtx.kubectl = &kubetest.MockKubectlCmd{
|
||||
Commands: map[string]kubetest.KubectlOutput{
|
||||
// Fail operation
|
||||
pod.GetName(): {Err: fmt.Errorf("")},
|
||||
// Fail a single SyncFail hook
|
||||
failedSyncFailHook.GetName(): {Err: fmt.Errorf("")}},
|
||||
}
|
||||
|
||||
syncCtx.sync()
|
||||
syncCtx.sync()
|
||||
|
||||
fmt.Println(syncCtx.syncRes.Resources)
|
||||
fmt.Println(syncCtx.opState.Phase)
|
||||
// Operation as a whole should fail
|
||||
assert.Equal(t, OperationFailed, syncCtx.opState.Phase)
|
||||
// failedSyncFailHook should fail
|
||||
assert.Equal(t, OperationFailed, syncCtx.syncRes.Resources[1].HookPhase)
|
||||
assert.Equal(t, ResultCodeSyncFailed, syncCtx.syncRes.Resources[1].Status)
|
||||
// successfulSyncFailHook should be synced running (it is an nginx pod)
|
||||
assert.Equal(t, OperationRunning, syncCtx.syncRes.Resources[2].HookPhase)
|
||||
assert.Equal(t, ResultCodeSynced, syncCtx.syncRes.Resources[2].Status)
|
||||
}
|
||||
|
||||
func Test_syncContext_isSelectiveSync(t *testing.T) {
|
||||
type fields struct {
|
||||
compareResult *comparisonResult
|
||||
syncResources []SyncOperationResource
|
||||
}
|
||||
oneSyncResource := []SyncOperationResource{{}}
|
||||
oneResource := func(group, kind, name string, hook bool) *comparisonResult {
|
||||
return &comparisonResult{resources: []v1alpha1.ResourceStatus{{Group: group, Kind: kind, Name: name, Hook: hook}}}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want bool
|
||||
}{
|
||||
{"Empty", fields{}, false},
|
||||
{"OneCompareResult", fields{oneResource("", "", "", false), []SyncOperationResource{}}, true},
|
||||
{"OneSyncResource", fields{&comparisonResult{}, oneSyncResource}, true},
|
||||
{"Equal", fields{oneResource("", "", "", false), oneSyncResource}, false},
|
||||
{"EqualOutOfOrder", fields{&comparisonResult{resources: []v1alpha1.ResourceStatus{{Group: "a"}, {Group: "b"}}}, []SyncOperationResource{{Group: "b"}, {Group: "a"}}}, false},
|
||||
{"KindDifferent", fields{oneResource("foo", "", "", false), oneSyncResource}, true},
|
||||
{"GroupDifferent", fields{oneResource("", "foo", "", false), oneSyncResource}, true},
|
||||
{"NameDifferent", fields{oneResource("", "", "foo", false), oneSyncResource}, true},
|
||||
{"HookIgnored", fields{oneResource("", "", "", true), []SyncOperationResource{}}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sc := &syncContext{
|
||||
compareResult: tt.fields.compareResult,
|
||||
syncResources: tt.fields.syncResources,
|
||||
}
|
||||
if got := sc.isSelectiveSync(); got != tt.want {
|
||||
t.Errorf("syncContext.isSelectiveSync() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_syncContext_liveObj(t *testing.T) {
|
||||
type fields struct {
|
||||
compareResult *comparisonResult
|
||||
}
|
||||
type args struct {
|
||||
obj *unstructured.Unstructured
|
||||
}
|
||||
obj := test.NewPod()
|
||||
obj.SetNamespace("my-ns")
|
||||
|
||||
found := test.NewPod()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *unstructured.Unstructured
|
||||
}{
|
||||
{"None", fields{compareResult: &comparisonResult{managedResources: []managedResource{}}}, args{obj: &unstructured.Unstructured{}}, nil},
|
||||
{"Found", fields{compareResult: &comparisonResult{managedResources: []managedResource{{Group: obj.GroupVersionKind().Group, Kind: obj.GetKind(), Namespace: obj.GetNamespace(), Name: obj.GetName(), Live: found}}}}, args{obj: obj}, found},
|
||||
{"EmptyNamespace", fields{compareResult: &comparisonResult{managedResources: []managedResource{{Group: obj.GroupVersionKind().Group, Kind: obj.GetKind(), Name: obj.GetName(), Live: found}}}}, args{obj: obj}, found},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sc := &syncContext{
|
||||
compareResult: tt.fields.compareResult,
|
||||
}
|
||||
if got := sc.liveObj(tt.args.obj); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("syncContext.liveObj() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_syncContext_hasCRDOfGroupKind(t *testing.T) {
|
||||
// target
|
||||
assert.False(t, (&syncContext{compareResult: &comparisonResult{managedResources: []managedResource{{Target: test.NewCRD()}}}}).hasCRDOfGroupKind("", ""))
|
||||
assert.True(t, (&syncContext{compareResult: &comparisonResult{managedResources: []managedResource{{Target: test.NewCRD()}}}}).hasCRDOfGroupKind("argoproj.io", "TestCrd"))
|
||||
// hook
|
||||
assert.False(t, (&syncContext{compareResult: &comparisonResult{hooks: []*unstructured.Unstructured{test.NewCRD()}}}).hasCRDOfGroupKind("", ""))
|
||||
assert.True(t, (&syncContext{compareResult: &comparisonResult{hooks: []*unstructured.Unstructured{test.NewCRD()}}}).hasCRDOfGroupKind("argoproj.io", "TestCrd"))
|
||||
}
|
||||
|
||||
@@ -1,186 +1 @@
|
||||
# Contributing
|
||||
## Before You Start
|
||||
|
||||
You must install and run the ArgoCD using a local Kubernetes (e.g. Docker for Desktop or Minikube) first. This will help you understand the application, but also get your local environment set-up.
|
||||
|
||||
Then, to get a good grounding in Go, try out [the tutorial](https://tour.golang.org/).
|
||||
|
||||
## Pre-requisites
|
||||
|
||||
Install:
|
||||
|
||||
* [docker](https://docs.docker.com/install/#supported-platforms)
|
||||
* [golang](https://golang.org/)
|
||||
* [dep](https://github.com/golang/dep)
|
||||
* [protobuf](https://developers.google.com/protocol-buffers/)
|
||||
* [ksonnet](https://github.com/ksonnet/ksonnet#install)
|
||||
* [helm](https://github.com/helm/helm/releases)
|
||||
* [kustomize](https://github.com/kubernetes-sigs/kustomize/releases)
|
||||
* [go-swagger](https://github.com/go-swagger/go-swagger/blob/master/docs/install.md)
|
||||
* [jq](https://stedolan.github.io/jq/)
|
||||
* [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
|
||||
* [kubectx](https://kubectx.dev)
|
||||
* [minikube](https://kubernetes.io/docs/setup/minikube/) or Docker for Desktop
|
||||
|
||||
Brew users can quickly install the lot:
|
||||
|
||||
```bash
|
||||
brew tap go-swagger/go-swagger
|
||||
brew install go dep protobuf kubectl kubectx ksonnet/tap/ks kubernetes-helm jq go-swagger
|
||||
```
|
||||
|
||||
!!! note "Kustomize"
|
||||
Since Argo CD supports Kustomize v1.0 and v2.0, you will need to install both versions in order for the unit tests to run. The Kustomize 1 unit test expects to find a `kustomize1` binary in the path. You can use this [link](https://github.com/argoproj/argo-cd/blob/master/Dockerfile#L66-L69) to find the Kustomize 1 currently used by Argo CD and modify the curl command to download the correct OS.
|
||||
|
||||
Set up environment variables (e.g. is `~/.bashrc`):
|
||||
|
||||
```bash
|
||||
export GOPATH=~/go
|
||||
export PATH=$PATH:$GOPATH/bin
|
||||
```
|
||||
|
||||
Install go dependencies:
|
||||
|
||||
```bash
|
||||
go get -u github.com/golang/protobuf/protoc-gen-go
|
||||
go get -u github.com/go-swagger/go-swagger/cmd/swagger
|
||||
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
|
||||
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
|
||||
go get -u github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||
go get -u github.com/mattn/goreman
|
||||
go get -u gotest.tools/gotestsum
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
go get -u github.com/argoproj/argo-cd
|
||||
dep ensure
|
||||
make
|
||||
```
|
||||
|
||||
The make command can take a while, and we recommend building the specific component you are working on
|
||||
|
||||
* `make codegen` - Builds protobuf and swagger files
|
||||
* `make cli` - Make the argocd CLI tool
|
||||
* `make server` - Make the API/repo/controller server
|
||||
* `make argocd-util` - Make the administrator's utility, used for certain tasks such as import/export
|
||||
|
||||
## Running Tests
|
||||
|
||||
To run unit tests:
|
||||
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
|
||||
Check out the following [documentation](https://github.com/argoproj/argo-cd/blob/master/docs/developer-guide/test-e2e.md) for instructions on running the e2e tests.
|
||||
|
||||
## Running Locally
|
||||
|
||||
It is much easier to run and debug if you run ArgoCD on your local machine than in the Kubernetes cluster.
|
||||
|
||||
You should scale the deployments to zero:
|
||||
|
||||
```bash
|
||||
kubectl -n argocd scale deployment.extensions/argocd-application-controller --replicas 0
|
||||
kubectl -n argocd scale deployment.extensions/argocd-dex-server --replicas 0
|
||||
kubectl -n argocd scale deployment.extensions/argocd-repo-server --replicas 0
|
||||
kubectl -n argocd scale deployment.extensions/argocd-server --replicas 0
|
||||
kubectl -n argocd scale deployment.extensions/argocd-redis --replicas 0
|
||||
```
|
||||
|
||||
Then checkout and build the UI next to your code
|
||||
|
||||
```
|
||||
cd ~/go/src/github.com/argoproj
|
||||
git clone git@github.com:argoproj/argo-cd-ui.git
|
||||
```
|
||||
|
||||
Follow the UI's [README](https://github.com/argoproj/argo-cd-ui/blob/master/README.md) to build it.
|
||||
|
||||
Note: you'll need to use the https://localhost:6443 cluster now.
|
||||
|
||||
Then start the services:
|
||||
|
||||
```bash
|
||||
cd ~/go/src/github.com/argoproj/argo-cd
|
||||
make start
|
||||
```
|
||||
|
||||
You can now execute `argocd` command against your locally running ArgoCD by appending `--server localhost:8080 --plaintext --insecure`, e.g.:
|
||||
|
||||
```bash
|
||||
argocd app set guestbook --path guestbook --repo https://github.com/argoproj/argocd-example-apps.git --dest-server https://localhost:6443 --dest-namespace default --server localhost:8080 --plaintext --insecure
|
||||
```
|
||||
|
||||
You can open the UI: http://localhost:8080
|
||||
|
||||
Note: you'll need to use the https://kubernetes.default.svc cluster now.
|
||||
|
||||
## Running Local Containers
|
||||
|
||||
You may need to run containers locally, so here's how:
|
||||
|
||||
Create login to Docker Hub, then login.
|
||||
|
||||
```bash
|
||||
docker login
|
||||
```
|
||||
|
||||
Add your username as the environment variable, e.g. to your `~/.bash_profile`:
|
||||
|
||||
```bash
|
||||
export IMAGE_NAMESPACE=alexcollinsintuit
|
||||
```
|
||||
|
||||
If you have not built the UI image (see [the UI README](https://github.com/argoproj/argo-cd-ui/blob/master/README.md)), then do the following:
|
||||
|
||||
```bash
|
||||
docker pull argoproj/argocd-ui:latest
|
||||
docker tag argoproj/argocd-ui:latest $IMAGE_NAMESPACE/argocd-ui:latest
|
||||
docker push $IMAGE_NAMESPACE/argocd-ui:latest
|
||||
```
|
||||
|
||||
Build the images:
|
||||
|
||||
```bash
|
||||
DOCKER_PUSH=true make image
|
||||
```
|
||||
|
||||
Update the manifests:
|
||||
|
||||
```bash
|
||||
make manifests
|
||||
```
|
||||
|
||||
Install the manifests:
|
||||
|
||||
```bash
|
||||
kubectl -n argocd apply --force -f manifests/install.yaml
|
||||
```
|
||||
|
||||
Scale your deployments up:
|
||||
|
||||
```bash
|
||||
kubectl -n argocd scale deployment.extensions/argocd-application-controller --replicas 1
|
||||
kubectl -n argocd scale deployment.extensions/argocd-dex-server --replicas 1
|
||||
kubectl -n argocd scale deployment.extensions/argocd-repo-server --replicas 1
|
||||
kubectl -n argocd scale deployment.extensions/argocd-server --replicas 1
|
||||
kubectl -n argocd scale deployment.extensions/argocd-redis --replicas 1
|
||||
```
|
||||
|
||||
Now you can set-up the port-forwarding and open the UI or CLI.
|
||||
|
||||
## Pre-commit Checks
|
||||
|
||||
Before you commit, make sure you've formatted and linted your code, or your PR will fail CI:
|
||||
|
||||
```bash
|
||||
STAGED_GO_FILES=$(git diff --cached --name-only | grep ".go$")
|
||||
|
||||
gofmt -w $STAGED_GO_FILES
|
||||
|
||||
make codgen
|
||||
make precommit ;# lint and test
|
||||
```
|
||||
Please refer to [the Contribution Guide](https://argoproj.github.io/argo-cd/developer-guide/contributing/)
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
1. Make sure you've read [understanding the basics](understand_the_basics.md) the [getting started guide](getting_started.md).
|
||||
2. Looked for an answer [the frequently asked questions](faq.md).
|
||||
3. Ask a question in [the Argo CD Slack channel ⧉](https://argoproj.slack.com/messages/CASHNF6MS).
|
||||
4. [Read issues, report a bug, or request a feature ⧉](https://github.com/argoproj/argo-cd/issues)
|
||||
3. Ask a question in [the Argo CD Slack channel ⧉](https://argoproj.github.io/community/join-slack).
|
||||
4. [Read issues, report a bug, or request a feature ⧉](https://github.com/argoproj/argo-cd/issues)
|
||||
|
||||
BIN
docs/assets/api-management.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/assets/app-ui-information.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/assets/application-of-applications.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
docs/assets/argo.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
docs/assets/azure-api-permissions.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
docs/assets/azure-token-configuration.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
docs/assets/cert-management-add-ssh.png
Normal file
|
After Width: | Height: | Size: 187 KiB |