Compare commits
1056 Commits
release-0.
...
v1.3.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5a65cdfe7 | ||
|
|
219fae8380 | ||
|
|
ba04a028c1 | ||
|
|
634e0d6323 | ||
|
|
962fb84fba | ||
|
|
0a8507ac6e | ||
|
|
3026882f17 | ||
|
|
e84c56b279 | ||
|
|
25a1b4ccc6 | ||
|
|
90bc83d1f7 | ||
|
|
130b1e6218 | ||
|
|
af5f1a7e69 | ||
|
|
be93e7918b | ||
|
|
4ea88b621d | ||
|
|
e1336f1f23 | ||
|
|
0af64eaf9a | ||
|
|
9f8608c9fc | ||
|
|
4c42c5fc70 | ||
|
|
1c01f9b7f5 | ||
|
|
24d43e45f7 | ||
|
|
22826baf46 | ||
|
|
58675b5266 | ||
|
|
786a94b03b | ||
|
|
79e364228a | ||
|
|
fba1c0d4f3 | ||
|
|
65e427eb4f | ||
|
|
a808fd2989 | ||
|
|
947b074d9a | ||
|
|
53252aa6a1 | ||
|
|
9df5e9560a | ||
|
|
7a9f7b01f8 | ||
|
|
52f7a66826 | ||
|
|
bcdaddcf69 | ||
|
|
593715038c | ||
|
|
4f84498265 | ||
|
|
8186ff0bb9 | ||
|
|
0432a85832 | ||
|
|
a0d37654b4 | ||
|
|
fe3b17322a | ||
|
|
59623b85fe | ||
|
|
905a8b0d75 | ||
|
|
30935e2019 | ||
|
|
f3e0e097de | ||
|
|
e1ff01cb56 | ||
|
|
e0de3300d2 | ||
|
|
40cb6fa9ce | ||
|
|
11de95cbac | ||
|
|
c7cd2e92bb | ||
|
|
a4f13f5f29 | ||
|
|
6460de9d03 | ||
|
|
6f6b03f74c | ||
|
|
36775ae1bb | ||
|
|
4a360cf9f4 | ||
|
|
6e56302fa4 | ||
|
|
3d82d5aab7 | ||
|
|
7df03b3c89 | ||
|
|
e059760906 | ||
|
|
8925b52bc8 | ||
|
|
8a43840f0b | ||
|
|
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 | ||
|
|
3b71bd05a4 | ||
|
|
e75a7a5dea | ||
|
|
5134ca37a7 | ||
|
|
60273ba84f | ||
|
|
ae23af7061 | ||
|
|
c33604f2ef | ||
|
|
9686a2f16b | ||
|
|
eea804b3f6 | ||
|
|
8f658108f2 | ||
|
|
25edf8ac3f | ||
|
|
3db5c36e60 | ||
|
|
3ed6dc91dd | ||
|
|
e803969442 | ||
|
|
5be580c105 | ||
|
|
13e5348177 | ||
|
|
90e44c092a | ||
|
|
8027882c1c | ||
|
|
ad9ed33f8d | ||
|
|
ddf5f0cf46 | ||
|
|
76811a992e | ||
|
|
2eac7bf457 | ||
|
|
53cbcd362d | ||
|
|
11c878b847 | ||
|
|
25d5333894 | ||
|
|
a0ae6dd32f | ||
|
|
1bbd8f038b | ||
|
|
e7bde586d8 | ||
|
|
4541ca664a | ||
|
|
97422b4148 | ||
|
|
4df07a278d | ||
|
|
efa418c58b | ||
|
|
5540c9b9aa | ||
|
|
be40dbc8cc | ||
|
|
0bd7023b66 | ||
|
|
db82456dde | ||
|
|
a51441546c | ||
|
|
0bd323140d | ||
|
|
ad22949925 | ||
|
|
0726ee8995 | ||
|
|
c120004084 | ||
|
|
02c81851a8 | ||
|
|
e15b97ee08 | ||
|
|
bbc7d39928 | ||
|
|
b53c34c3f7 | ||
|
|
d2928d5b31 | ||
|
|
7e76d6de33 | ||
|
|
3eac376a41 | ||
|
|
7084e3af5c | ||
|
|
ac3d12c746 | ||
|
|
41a3352516 | ||
|
|
01dad77d44 | ||
|
|
018ce4e9f0 | ||
|
|
311ff8caed | ||
|
|
197bbda02e | ||
|
|
56cd8fcc95 | ||
|
|
3c4b42de75 | ||
|
|
0e89b744ec | ||
|
|
6aa12887b3 | ||
|
|
e5d6e9a21a | ||
|
|
e4b8a9d895 | ||
|
|
76d25d3795 | ||
|
|
97a59ca753 | ||
|
|
544bd47e94 | ||
|
|
56916a0321 | ||
|
|
eff83a45cd | ||
|
|
3f9d361d4f | ||
|
|
0565dd3df1 | ||
|
|
c8e8c2dc32 | ||
|
|
9df1e27191 | ||
|
|
abe25f62d0 | ||
|
|
ad5d26f08a | ||
|
|
9c5c420483 | ||
|
|
dea731a6b2 | ||
|
|
1d19447e8e | ||
|
|
ac938c8738 | ||
|
|
88a1c2a593 | ||
|
|
1e8db87320 | ||
|
|
7382ebce27 | ||
|
|
911425c1c1 | ||
|
|
9988b3d8e6 | ||
|
|
3d0f85c188 | ||
|
|
6b69449175 | ||
|
|
85a5fb5a41 | ||
|
|
f5bc901dd7 | ||
|
|
ba43a01669 | ||
|
|
f5833da4cd | ||
|
|
67882a9dff | ||
|
|
c03bd896d8 | ||
|
|
8ee3c93c84 | ||
|
|
4ac062d09e | ||
|
|
159a30fdc7 | ||
|
|
a15ca7259c | ||
|
|
d4ee7972ca | ||
|
|
56ca350ed2 | ||
|
|
86f6b657e2 | ||
|
|
ac7906fdea | ||
|
|
781a9ab627 | ||
|
|
d0ecaed401 | ||
|
|
4d494f3a1b | ||
|
|
57ff5b25e4 | ||
|
|
28fa4a7571 | ||
|
|
723228598e | ||
|
|
f28d11bf90 | ||
|
|
7091585dbe | ||
|
|
790cdd1d45 | ||
|
|
81e21a551d | ||
|
|
7cf3f6cd19 | ||
|
|
506d95da10 | ||
|
|
36b4683e84 | ||
|
|
2becacd48d | ||
|
|
ae41425c77 | ||
|
|
59837cb513 | ||
|
|
66e5d51329 | ||
|
|
8d55e72dfa | ||
|
|
15dfa79708 | ||
|
|
896d46525e | ||
|
|
a8b70b411c | ||
|
|
cd25c4b3c9 | ||
|
|
b28d8361f5 | ||
|
|
b40ba175a3 | ||
|
|
cd87a1436b | ||
|
|
dfa91d87cf | ||
|
|
102c24cc29 | ||
|
|
7d3b6cc8e0 | ||
|
|
9ef7064cc4 | ||
|
|
56f0ff204e | ||
|
|
af896533df | ||
|
|
aa099f3fc0 | ||
|
|
27b23f6a00 | ||
|
|
e07a877e73 | ||
|
|
1f675f4bb9 | ||
|
|
e482d74d19 | ||
|
|
50bff3e540 | ||
|
|
0d7c42ba54 | ||
|
|
ec7cbf8e15 | ||
|
|
d60fb2b449 | ||
|
|
dc989dbebc | ||
|
|
09164cae6c | ||
|
|
80f0f779db | ||
|
|
c605e892b6 | ||
|
|
80fe3e1877 | ||
|
|
8f7a7ef6a4 | ||
|
|
2aad4d0ab5 | ||
|
|
df7b0c6682 | ||
|
|
ea1519de82 | ||
|
|
b60067af97 | ||
|
|
3540859074 | ||
|
|
22ddd53ea5 | ||
|
|
1b41aba841 | ||
|
|
cafe24da86 | ||
|
|
c33acf749c | ||
|
|
dab3b688f0 | ||
|
|
a34d2c750b | ||
|
|
5210c678b9 | ||
|
|
baf157901c | ||
|
|
e457dd6f6c | ||
|
|
f787828712 | ||
|
|
2724aeef32 | ||
|
|
1d3ec93ec7 | ||
|
|
471dac48be | ||
|
|
fea3899f26 | ||
|
|
f016acdade | ||
|
|
0c4d5009a2 | ||
|
|
815ba879e6 | ||
|
|
3df86a7918 | ||
|
|
2675367400 | ||
|
|
5e7b48c9a2 | ||
|
|
0f248e9149 | ||
|
|
461d8c980f | ||
|
|
f120c1dedb | ||
|
|
a54dc192d7 | ||
|
|
0850db530f | ||
|
|
0a1a579714 | ||
|
|
9a7fecef06 | ||
|
|
39c63371bf | ||
|
|
80b0e1138c | ||
|
|
3acc0b3af2 | ||
|
|
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 |
@@ -1,97 +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
|
||||
|
||||
templates:
|
||||
- name: argo-cd-ci
|
||||
steps:
|
||||
- - name: build
|
||||
template: ci-dind
|
||||
arguments:
|
||||
parameters:
|
||||
- name: cmd
|
||||
value: make image
|
||||
- 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"
|
||||
- name: test-e2e
|
||||
template: ci-builder
|
||||
arguments:
|
||||
parameters:
|
||||
- name: cmd
|
||||
value: "dep ensure && make test-e2e"
|
||||
|
||||
- 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.12.0
|
||||
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.12.0
|
||||
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
|
||||
214
.circleci/config.yml
Normal file
@@ -0,0 +1,214 @@
|
||||
version: 2.1
|
||||
commands:
|
||||
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
|
||||
dep_ensure:
|
||||
steps:
|
||||
- restore_cache:
|
||||
keys:
|
||||
- vendor-v4-{{ checksum "Gopkg.lock" }}
|
||||
- run:
|
||||
name: Run dep ensure
|
||||
command: dep ensure -v
|
||||
- save_cache:
|
||||
key: vendor-v4-{{ checksum "Gopkg.lock" }}
|
||||
paths:
|
||||
- vendor
|
||||
install_golang:
|
||||
steps:
|
||||
- run:
|
||||
name: Install Golang v1.12.6
|
||||
command: |
|
||||
go get golang.org/dl/go1.12.6
|
||||
[ -e /home/circleci/sdk/go1.12.6 ] || go1.12.6 download
|
||||
echo "export GOPATH=/home/circleci/.go_workspace" | tee -a $BASH_ENV
|
||||
echo "export PATH=/home/circleci/sdk/go1.12.6/bin:\$PATH" | tee -a $BASH_ENV
|
||||
save_go_cache:
|
||||
steps:
|
||||
- save_cache:
|
||||
key: go-v18-{{ .Branch }}
|
||||
paths:
|
||||
- /home/circleci/.go_workspace
|
||||
- /home/circleci/.cache/go-build
|
||||
- /home/circleci/sdk/go1.12.6
|
||||
restore_go_cache:
|
||||
steps:
|
||||
- restore_cache:
|
||||
keys:
|
||||
- go-v18-{{ .Branch }}
|
||||
- go-v18-master
|
||||
- go-v17-{{ .Branch }}
|
||||
- go-v17-master
|
||||
jobs:
|
||||
codegen:
|
||||
docker:
|
||||
- image: circleci/golang:1.12
|
||||
working_directory: /go/src/github.com/argoproj/argo-cd
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys: [codegen-v2]
|
||||
- run: ./hack/install.sh codegen-go-tools
|
||||
- run: sudo ./hack/install.sh codegen-tools
|
||||
- run: dep ensure
|
||||
- save_cache:
|
||||
key: codegen-v2
|
||||
paths: [vendor, /tmp/dl, /go/pkg]
|
||||
- run: helm 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: /home/circleci/.go_workspace/src/github.com/argoproj/argo-cd
|
||||
machine:
|
||||
image: circleci/classic:201808-01
|
||||
steps:
|
||||
- restore_go_cache
|
||||
- install_golang
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: test-dl-v1
|
||||
- run: sudo ./hack/install.sh kubectl-linux kubectx-linux dep-linux ksonnet-linux helm-linux kustomize-linux
|
||||
- save_cache:
|
||||
key: test-dl-v1
|
||||
paths: [/tmp/dl]
|
||||
- configure_git
|
||||
- run: go get github.com/jstemmer/go-junit-report
|
||||
- dep_ensure
|
||||
- save_go_cache
|
||||
- run: make test
|
||||
- run:
|
||||
name: Uploading code coverage
|
||||
command: bash <(curl -s https://codecov.io/bash) -f coverage.out
|
||||
- store_test_results:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
destination: .
|
||||
e2e:
|
||||
working_directory: /home/circleci/.go_workspace/src/github.com/argoproj/argo-cd
|
||||
machine:
|
||||
image: circleci/classic:201808-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"
|
||||
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
|
||||
background: true
|
||||
environment:
|
||||
INSTALL_K3S_EXEC: --docker
|
||||
INSTALL_K3S_VERSION: v0.5.0
|
||||
- restore_go_cache
|
||||
- install_golang
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys: [e2e-dl-v1]
|
||||
- run: sudo ./hack/install.sh kubectx-linux dep-linux ksonnet-linux helm-linux kustomize-linux
|
||||
- run: go get github.com/jstemmer/go-junit-report
|
||||
- save_cache:
|
||||
key: e2e-dl-v10
|
||||
paths: [/tmp/dl]
|
||||
- dep_ensure
|
||||
- configure_git
|
||||
- run: make cli
|
||||
- run:
|
||||
name: Create namespace
|
||||
command: |
|
||||
set -x
|
||||
kubectl create ns argocd-e2e
|
||||
kubens argocd-e2e
|
||||
# install the certificates (not 100% sure we need this)
|
||||
sudo cp /var/lib/rancher/k3s/server/tls/token-ca.crt /usr/local/share/ca-certificates/k3s.crt
|
||||
sudo update-ca-certificates
|
||||
# create the kubecfg, again - not sure we need this
|
||||
cat /etc/rancher/k3s/k3s.yaml | sed "s/localhost/`hostname`/" | tee ~/.kube/config
|
||||
echo "127.0.0.1 `hostname`" | sudo tee -a /etc/hosts
|
||||
- run:
|
||||
name: Apply manifests
|
||||
command: kustomize build test/manifests/base | kubectl apply -f -
|
||||
- run:
|
||||
name: Start Redis
|
||||
command: docker run --rm --name argocd-redis -i -p 6379:6379 redis:5.0.3-alpine --save "" --appendonly no
|
||||
background: true
|
||||
- run:
|
||||
name: Start repo server
|
||||
command: go run ./cmd/argocd-repo-server/main.go --loglevel debug --redis localhost:6379
|
||||
background: true
|
||||
- run:
|
||||
name: Start API server
|
||||
command: go run ./cmd/argocd-server/main.go --loglevel debug --redis localhost:6379 --insecure --dex-server http://localhost:5556 --repo-server localhost:8081 --staticassets ../argo-cd-ui/dist/app
|
||||
background: true
|
||||
- run:
|
||||
name: Start Test Git
|
||||
command: |
|
||||
test/fixture/testrepos/start-git.sh
|
||||
background: true
|
||||
- run: until curl -v http://localhost:8080/healthz; do sleep 10; done
|
||||
- run:
|
||||
name: Start controller
|
||||
command: go run ./cmd/argocd-application-controller/main.go --loglevel debug --redis localhost:6379 --repo-server localhost:8081 --kubeconfig ~/.kube/config
|
||||
background: true
|
||||
- run:
|
||||
command: PATH=dist:$PATH make test-e2e
|
||||
environment:
|
||||
ARGOCD_OPTS: "--server localhost:8080 --plaintext"
|
||||
ARGOCD_E2E_K3S: "true"
|
||||
- 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: yarn build
|
||||
- run: yarn lint
|
||||
workflows:
|
||||
version: 2
|
||||
workflow:
|
||||
jobs:
|
||||
- test
|
||||
- codegen:
|
||||
requires:
|
||||
- test
|
||||
- ui:
|
||||
requires:
|
||||
- codegen
|
||||
- e2e
|
||||
15
.codecov.yml
@@ -1,6 +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 1%, assume that it's typically due to CI problems
|
||||
threshold: 1
|
||||
@@ -10,3 +10,4 @@ dist/
|
||||
cmd/**/debug
|
||||
debug.test
|
||||
coverage.out
|
||||
ui/node_modules/
|
||||
|
||||
39
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
---
|
||||
Checklist:
|
||||
|
||||
* [ ] 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**
|
||||
|
||||
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.
|
||||
|
||||
**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?
|
||||
1
.github/no-response.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
# See https://github.com/probot/no-response
|
||||
5
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
Checklist:
|
||||
|
||||
* [ ] I've created an [enhancement proposal](https://github.com/argoproj/argo-cd/issues/new/choose) and I feel I've gotten a green light from the community.
|
||||
* [ ] My build is green ([troubleshooting builds](https://argoproj.github.io/argo-cd/developer-guide/ci/)).
|
||||
* [ ] Optional. My organisation is added to the README.
|
||||
4
.github/stale.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# See https://github.com/probot/stale
|
||||
# See https://github.com/probot/stale
|
||||
exemptLabels:
|
||||
- backlog
|
||||
2
.gitignore
vendored
@@ -3,8 +3,10 @@
|
||||
.DS_Store
|
||||
vendor/
|
||||
dist/
|
||||
site/
|
||||
*.iml
|
||||
# delve debug binaries
|
||||
cmd/**/debug
|
||||
debug.test
|
||||
coverage.out
|
||||
test-results
|
||||
22
.golangci.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
run:
|
||||
deadline: 2m
|
||||
skip-files:
|
||||
- ".*\\.pb\\.go"
|
||||
skip-dirs:
|
||||
- pkg/client/
|
||||
- vendor/
|
||||
linters:
|
||||
enable:
|
||||
- vet
|
||||
- deadcode
|
||||
- goimports
|
||||
- varcheck
|
||||
- structcheck
|
||||
- ineffassign
|
||||
- unconvert
|
||||
- unparam
|
||||
linters-settings:
|
||||
goimports:
|
||||
local-prefixes: github.com/argoproj/argo-cd
|
||||
service:
|
||||
golangci-lint-version: 1.21.0
|
||||
520
CHANGELOG.md
@@ -1,5 +1,525 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
|
||||
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 & 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.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
|
||||
* Create new documentation website #1390
|
||||
* Default view should resource view instead of diff view #1354
|
||||
* 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
|
||||
|
||||
- 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 #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
|
||||
- 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)
|
||||
|
||||
### Changes since v0.12.1
|
||||
|
||||
- Fix racing condition in controller cache (#1498)
|
||||
- "bind: address already in use" after switching to gRPC-Web (#1451)
|
||||
- Annoying warning while using --grpc-web flag (#1420)
|
||||
- Delete helm temp directories (#1446)
|
||||
- Fix null pointer exception in secret normalization function (#1389)
|
||||
- Argo CD should not delete CRDs(#1425)
|
||||
- UI is unable to load cluster level resource manifest (#1429)
|
||||
|
||||
## v0.12.1 (2019-04-09)
|
||||
|
||||
### Changes since v0.12.0
|
||||
|
||||
- [UI] applications view blows up when user does not have permissions (#1368)
|
||||
- Add k8s objects circular dependency protection to getApp method (#1374)
|
||||
- App controller unnecessary set namespace to cluster level resources (#1404)
|
||||
- Changing SSO login URL to be a relative link so it's affected by basehref (#101) (@arnarg)
|
||||
- CLI diff should take into account resource customizations (#1294)
|
||||
- Don't try deleting application resource if it already has `deletionTimestamp` (#1406)
|
||||
- Fix invalid group filtering in 'patch-resource' command (#1319)
|
||||
- Fix null pointer dereference error in 'argocd app wait' (#1366)
|
||||
- kubectl v1.13 fails to convert extensions/NetworkPolicy (#1012)
|
||||
- Patch APIs are not audited (#1397)
|
||||
|
||||
+ 'argocd app wait' should fail sooner if app transitioned to Degraded state (#733)
|
||||
+ Add mapping to new canonical Ingress API group - kubernetes 1.14 support (#1348) (@twz123)
|
||||
+ Adds support for `kustomize edit set image`. (#1275)
|
||||
+ Allow using any name for secrets which store cluster credentials (#1218)
|
||||
+ Update argocd-util import/export to support proper backup and restore (#1048)
|
||||
|
||||
## v0.12.0 (2019-03-20)
|
||||
|
||||
### New Features
|
||||
|
||||
#### Improved UI
|
||||
|
||||
Many improvements to the UI were made, including:
|
||||
|
||||
* Table view when viewing applications
|
||||
* Filters on applications
|
||||
* Table view when viewing application resources
|
||||
* YAML editor in UI
|
||||
* Switch to text-based diff instead of json diff
|
||||
* Ability to edit application specs
|
||||
|
||||
#### Custom Health Assessments (CRD Health)
|
||||
|
||||
Argo CD has long been able to perform health assessments on resources, however this could only
|
||||
assess the health for a few native kubernetes types (deployments, statefulsets, daemonsets, etc...).
|
||||
Now, Argo CD can be extended to gain understanding of any CRD health, in the form of Lua scripts.
|
||||
For example, using this feature, Argo CD now understands the CertManager Certificate CRD and will
|
||||
report a Degraded status when there are issues with the cert.
|
||||
|
||||
#### Configuration Management Plugins
|
||||
|
||||
Argo CD introduces Config Management Plugins to support custom configuration management tools other
|
||||
than the set that Argo CD provides out-of-the-box (Helm, Kustomize, Ksonnet, Jsonnet). Using config
|
||||
management plugins, Argo CD can be configured to run specified commands to render manifests. This
|
||||
makes it possible for Argo CD to support other config management tools (kubecfg, kapitan, shell
|
||||
scripts, etc...).
|
||||
|
||||
#### High Availability
|
||||
|
||||
Argo CD is now fully HA. A set HA of manifests are provided for users who wish to run Argo CD in
|
||||
a highly available manner. NOTE: The HA installation will require at least three different nodes due
|
||||
to pod anti-affinity roles in the specs.
|
||||
|
||||
#### Improved Application Source
|
||||
|
||||
* Support for Kustomize 2
|
||||
* YAML/JSON/Jsonnet Directories can now be recursed
|
||||
* Support for Jsonnet external variables and top-level arguments
|
||||
|
||||
#### Additional Prometheus Metrics
|
||||
|
||||
Argo CD provides the following additional prometheus metrics:
|
||||
* Sync counter to track sync activity and results over time
|
||||
* Application reconciliation (refresh) performance to track Argo CD performance and controller activity
|
||||
* Argo CD API Server metrics for monitoring HTTP/gRPC requests
|
||||
|
||||
#### Fuzzy Diff Logic
|
||||
|
||||
Argo CD can now be configured to ignore known differences for resource types by specifying a json
|
||||
pointer to the field path to ignore. This helps prevent OutOfSync conditions when a user has no
|
||||
control over the manifests. Ignored differences can be configured either at an application level,
|
||||
or a system level, based on a group/kind.
|
||||
|
||||
#### Resource Exclusions
|
||||
|
||||
Argo CD can now be configured to completely ignore entire classes of resources group/kinds.
|
||||
Excluding high-volume resources improves performance and memory usage, and reduces load and
|
||||
bandwidth to the Kubernetes API server. It also allows users to fine-tune the permissions that
|
||||
Argo CD needs to a cluster by preventing Argo CD from attempting to watch resources of that
|
||||
group/kind.
|
||||
|
||||
#### gRPC-Web Support
|
||||
|
||||
The argocd CLI can be now configured to communicate to the Argo CD API server using gRPC-Web
|
||||
(HTTP1.1) using a new CLI flag `--grpc-web`. This resolves some compatibility issues users were
|
||||
experiencing with ingresses and gRPC (HTTP2), and should enable argocd CLI to work with virtually
|
||||
any load balancer, ingress controller, or API gateway.
|
||||
|
||||
#### CLI features
|
||||
|
||||
Argo CD introduces some additional CLI commands:
|
||||
|
||||
* `argocd app edit APPNAME` - to edit an application spec using preferred EDITOR
|
||||
* `argocd proj edit PROJNAME` - to edit an project spec using preferred EDITOR
|
||||
* `argocd app patch APPNAME` - to patch an application spec
|
||||
* `argocd app patch-resource APPNAME` - to patch a specific resource which is part of an application
|
||||
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
#### Label selector changes, dex-server rename
|
||||
|
||||
The label selectors for deployments were been renamed to use kubernetes common labels
|
||||
(`app.kuberentes.io/name=NAME` instead of `app=NAME`). Since K8s deployment label selectors are
|
||||
immutable, during an upgrade from v0.11 to v0.12, the old deployments should be deleted using
|
||||
`--cascade=false` which allows the new deployments to be created without introducing downtime.
|
||||
Once the new deployments are ready, the older replicasets can be deleted. Use the following
|
||||
instructions to upgrade from v0.11 to v0.12 without introducing downtime:
|
||||
|
||||
```
|
||||
# delete the deployments with cascade=false. this orphan the replicasets, but leaves the pods running
|
||||
kubectl delete deploy --cascade=false argocd-server argocd-repo-server argocd-application-controller
|
||||
|
||||
# apply the new manifests and wait for them to finish rolling out
|
||||
kubectl apply <new install manifests>
|
||||
kubectl rollout status deploy/argocd-application-controller
|
||||
kubectl rollout status deploy/argocd-repo-server
|
||||
kubectl rollout status deploy/argocd-application-controller
|
||||
|
||||
# delete old replicasets which are using the legacy label
|
||||
kubectl delete rs -l app=argocd-server
|
||||
kubectl delete rs -l app=argocd-repo-server
|
||||
kubectl delete rs -l app=argocd-application-controller
|
||||
|
||||
# delete the legacy dex-server which was renamed
|
||||
kubectl delete deploy dex-server
|
||||
```
|
||||
|
||||
#### Deprecation of spec.source.componentParameterOverrides
|
||||
|
||||
For declarative application specs, the `spec.source.componentParameterOverrides` field is now
|
||||
deprecated in favor of application source specific config. They are replaced with new fields
|
||||
specific to their respective config management. For example, a Helm application spec using the
|
||||
legacy field:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
source:
|
||||
componentParameterOverrides:
|
||||
- name: image.tag
|
||||
value: v1.2
|
||||
```
|
||||
|
||||
should move to:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
source:
|
||||
helm:
|
||||
parameters:
|
||||
- name: image.tag
|
||||
value: v1.2
|
||||
```
|
||||
|
||||
Argo CD will automatically duplicate the legacy field values to the new locations (and vice versa)
|
||||
as part of automatic migration. The legacy `spec.source.componentParameterOverrides` field will be
|
||||
kept around for the v0.12 release (for migration purposes) and will be removed in the next Argo CD
|
||||
release.
|
||||
|
||||
#### Removal of spec.source.environment and spec.source.valuesFiles
|
||||
|
||||
The `spec.source.environment` and `spec.source.valuesFiles` fields, which were deprecated in v0.11,
|
||||
are now completely removed from the Application spec.
|
||||
|
||||
|
||||
#### API/CLI compatibility
|
||||
|
||||
Due to API spec changes related to the deprecation of componentParameterOverrides, Argo CD v0.12
|
||||
has a minimum client version of v0.12.0. Older CLI clients will be rejected.
|
||||
|
||||
|
||||
### Changes since v0.11:
|
||||
+ Improved UI
|
||||
+ Custom Health Assessments (CRD Health)
|
||||
+ Configuration Management Plugins
|
||||
+ High Availability
|
||||
+ Fuzzy Diff Logic
|
||||
+ Resource Exclusions
|
||||
+ gRPC-Web Support
|
||||
+ CLI features
|
||||
+ Additional prometheus metrics
|
||||
+ Sample Grafana dashboard (#1277) (@hartman17)
|
||||
+ Support for Kustomize 2
|
||||
+ YAML/JSON/Jsonnet Directories can now be recursed
|
||||
+ Support for Jsonnet external variables and top-level arguments
|
||||
+ Optimized reconciliation performance for applications with very active resources (#1267)
|
||||
+ Support a separate OAuth2 CLI clientID different from server (#1307)
|
||||
+ argocd diff: only print to stdout, if there is a diff + exit code (#1288) (@marcb1)
|
||||
+ Detection and handling of duplicated resource definitions (#1284)
|
||||
+ Support kustomize apps with remote bases in private repos in the same host (#1264)
|
||||
+ Support patching resource using REST API (#1186)
|
||||
* Deprecate componentParameterOverrides in favor of source specific config (#1207)
|
||||
* Support talking to Dex using local cluster address instead of public address (#1211)
|
||||
* Use Recreate deployment strategy for controller (#1315)
|
||||
* Honor os environment variables for helm commands (#1306) (@1337andre)
|
||||
* Disable CGO_ENABLED for server/controller binaries (#1286)
|
||||
* Documentation fixes and improvements (@twz123, @yann-soubeyrand, @OmerKahani, @dulltz)
|
||||
- Fix CRD creation/deletion handling (#1249)
|
||||
- Git cloning via SSH was not verifying host public key (#1276)
|
||||
- Fixed multiple goroutine leaks in controller and api-server
|
||||
- Fix isssue where `argocd app set -p` required repo privileges. (#1280)
|
||||
- Fix local diff of non-namespaced resources. Also handle duplicates in local diff (#1289)
|
||||
- Deprecated resource kinds from 'extensions' groups are not reconciled correctly (#1232)
|
||||
- Fix issue where CLI would panic after timeout when cli did not have get permissions (#1209)
|
||||
- invalidate repo cache on delete (#1182) (@narg95)
|
||||
|
||||
## v0.11.2 (2019-02-19)
|
||||
+ Adds client retry. Fixes #959 (#1119)
|
||||
- Prevent deletion hotloop (#1115)
|
||||
|
||||
185
CONTRIBUTING.md
@@ -1,185 +0,0 @@
|
||||
# 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/).
|
||||
* [minikube](https://kubernetes.io/docs/setup/minikube/) or Docker for Desktop
|
||||
|
||||
```
|
||||
brew tap go-swagger/go-swagger
|
||||
brew install go dep protobuf kubectl ksonnet/tap/ks kubernetes-helm jq go-swagger
|
||||
```
|
||||
|
||||
Set up environment variables (e.g. is `~/.bashrc`):
|
||||
|
||||
```
|
||||
export GOPATH=~/go
|
||||
export PATH=$PATH:$GOPATH/bin
|
||||
```
|
||||
|
||||
Install go dependencies:
|
||||
|
||||
```
|
||||
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 gopkg.in/alecthomas/gometalinter.v2
|
||||
go get -u github.com/mattn/goreman
|
||||
|
||||
gometalinter.v2 --install
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
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:
|
||||
|
||||
```
|
||||
make test
|
||||
```
|
||||
|
||||
To run e2e tests:
|
||||
|
||||
```
|
||||
make test-e2e
|
||||
```
|
||||
|
||||
## 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 deployemnts to zero:
|
||||
|
||||
```
|
||||
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:
|
||||
|
||||
```
|
||||
cd ~/go/src/github.com/argoproj/argo-cd
|
||||
goreman start
|
||||
```
|
||||
|
||||
You can now execute `argocd` command against your locally running ArgoCD by appending `--server localhost:8080 --plaintext --insecure`, e.g.:
|
||||
|
||||
```
|
||||
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.
|
||||
|
||||
```
|
||||
docker login
|
||||
```
|
||||
|
||||
Add your username as the environment variable, e.g. to your `~/.bash_profile`:
|
||||
|
||||
```
|
||||
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:
|
||||
|
||||
```
|
||||
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:
|
||||
|
||||
```
|
||||
DOCKER_PUSH=true make image
|
||||
```
|
||||
|
||||
Update the manifests:
|
||||
|
||||
```
|
||||
make manifests
|
||||
```
|
||||
|
||||
Install the manifests:
|
||||
|
||||
```
|
||||
kubectl -n argocd apply --force -f manifests/install.yaml
|
||||
```
|
||||
|
||||
Scale your deployments up:
|
||||
|
||||
```
|
||||
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 (see [README](README.md)) 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:
|
||||
|
||||
```
|
||||
STAGED_GO_FILES=$(git diff --cached --name-only | grep ".go$")
|
||||
|
||||
gofmt -w $STAGED_GO_FILES
|
||||
|
||||
make codgen
|
||||
make precommit ;# lint and test
|
||||
```
|
||||
123
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.12.6 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,90 +23,53 @@ 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
|
||||
|
||||
# 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
|
||||
# Keep version at 1.12.X until https://github.com/argoproj/argo-cd/issues/1012 is resolved
|
||||
ENV KUBECTL_VERSION=1.12.4
|
||||
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.2
|
||||
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
|
||||
ADD hack/install.sh .
|
||||
ADD hack/installers installers
|
||||
|
||||
RUN ./install.sh dep-linux
|
||||
RUN ./install.sh packr-linux
|
||||
RUN ./install.sh kubectl-linux
|
||||
RUN ./install.sh ksonnet-linux
|
||||
RUN ./install.sh helm-linux
|
||||
RUN ./install.sh kustomize-linux
|
||||
RUN ./install.sh aws-iam-authenticator-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 && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
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/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
|
||||
@@ -107,11 +77,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.12.6 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
|
||||
@@ -138,3 +123,5 @@ 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
|
||||
|
||||
325
Gopkg.lock
generated
@@ -1,6 +1,14 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
digest = "1:6d5a057da97a9dbdb10e7beedd2f43452b6bf7691001c0c8886e8dacf5610349"
|
||||
name = "bou.ke/monkey"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "bdf6dea004c6fd1cdf4b25da8ad45a606c09409a"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:9702dc153c9bb6ee7ee0587c248b7024700e89e4a7be284faaeeab9da32e1c6b"
|
||||
name = "cloud.google.com/go"
|
||||
@@ -48,47 +56,26 @@
|
||||
pruneopts = ""
|
||||
revision = "09c41003ee1d5015b75f331e52215512e7145b8d"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:a74730e052a45a3fab1d310fdef2ec17ae3d6af16228421e238320846f2aaec8"
|
||||
name = "github.com/alecthomas/template"
|
||||
packages = [
|
||||
".",
|
||||
"parse",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "a0175ee3bccc567396460bf5acd36800cb10c49c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:8483994d21404c8a1d489f6be756e25bfccd3b45d65821f25695577791a08e68"
|
||||
name = "github.com/alecthomas/units"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:0caf9208419fa5db5a0ca7112affaa9550c54291dda8e2abac0c0e76181c959e"
|
||||
name = "github.com/argoproj/argo"
|
||||
packages = [
|
||||
"pkg/apis/workflow",
|
||||
"pkg/apis/workflow/v1alpha1",
|
||||
"util",
|
||||
]
|
||||
packages = ["util"]
|
||||
pruneopts = ""
|
||||
revision = "7ef1cea68c94f7f0e1e2f8bd75bedc5a7df8af90"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:e8ec0abbf32fdcc9f7eb14c0656c1d0fc2fc7ec8f60dff4b7ac080c50afd8e49"
|
||||
digest = "1:4f6afcf4ebe041b3d4aa7926d09344b48d2f588e1f957526bbbe54f9cbb366a1"
|
||||
name = "github.com/argoproj/pkg"
|
||||
packages = [
|
||||
"errors",
|
||||
"exec",
|
||||
"rand",
|
||||
"time",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "88ab0e836a8e8c70bc297c5764669bd7da27afd1"
|
||||
revision = "38dba6e98495680ff1f8225642b63db10a96bb06"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:d8a2bb36a048d1571bcc1aee208b61f39dc16c6c53823feffd37449dde162507"
|
||||
@@ -360,6 +347,14 @@
|
||||
revision = "5ccd90ef52e1e632236f7326478d4faa74f99438"
|
||||
version = "v0.2.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:9a06e7365c6039daf4db9bbf79650e2933a2880982cbab8106cb74a36617f40d"
|
||||
name = "github.com/gogits/go-gogs-client"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "5a05380e4bc2440e0ec12f54f6f45648dbdd5e55"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:6e73003ecd35f4487a5e88270d3ca0a81bc80dc88053ac7e4dcfec5fba30d918"
|
||||
name = "github.com/gogo/protobuf"
|
||||
@@ -428,14 +423,6 @@
|
||||
revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:1e5b1e14524ed08301977b7b8e10c719ed853cbf3f24ecb66fae783a46f207a6"
|
||||
name = "github.com/google/btree"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "4030bb1f1f0c35b30ca7009e9ebd06849dd45306"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:14d826ee25139b4674e9768ac287a135f4e7c14e1134a5b15e4e152edfd49f41"
|
||||
name = "github.com/google/go-jsonnet"
|
||||
@@ -483,17 +470,6 @@
|
||||
revision = "66b9c49e59c6c48f0ffce28c2d8b8a5678502c6d"
|
||||
version = "v1.4.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:009a1928b8c096338b68b5822d838a72b4d8520715c1463614476359f3282ec8"
|
||||
name = "github.com/gregjones/httpcache"
|
||||
packages = [
|
||||
".",
|
||||
"diskcache",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "9cad4c3443a7200dd6400aef47183728de563a38"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:9dca8c981b8aed7448d94e78bc68a76784867a38b3036d5aabc0b32d92ffd1f4"
|
||||
@@ -691,22 +667,6 @@
|
||||
revision = "c37440a7cf42ac63b919c752ca73a85067e05992"
|
||||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:c24598ffeadd2762552269271b3b1510df2d83ee6696c1e543a0ff653af494bc"
|
||||
name = "github.com/petar/GoLLRB"
|
||||
packages = ["llrb"]
|
||||
pruneopts = ""
|
||||
revision = "53be0d36a84c2a886ca057d34b6aa4468df9ccb4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:b46305723171710475f2dd37547edd57b67b9de9f2a6267cafdd98331fd6897f"
|
||||
name = "github.com/peterbourgon/diskv"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "5f041e8faa004a95c88a202771f4cc3e991971e6"
|
||||
version = "v2.0.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:7365acd48986e205ccb8652cc746f09c8b7876030d53710ea6ef7d0bd0dcd7ca"
|
||||
name = "github.com/pkg/errors"
|
||||
@@ -760,7 +720,6 @@
|
||||
packages = [
|
||||
"expfmt",
|
||||
"internal/bitbucket.org/ww/goautoneg",
|
||||
"log",
|
||||
"model",
|
||||
]
|
||||
pruneopts = ""
|
||||
@@ -779,6 +738,14 @@
|
||||
pruneopts = ""
|
||||
revision = "05ee40e3a273f7245e8777337fc7b46e533a9a92"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:6bb048133650d1fb7fbff9fb3c35bd5c7e8653fc95c3bae6df94cd17d1580278"
|
||||
name = "github.com/robfig/cron"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "45fbe1491cdd47d74d1bf1396286d67faee8b8b5"
|
||||
version = "v3.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:5f47c69f85311c4dc292be6cc995a0a3fe8337a6ce38ef4f71e5b7efd5ad42e0"
|
||||
name = "github.com/rs/cors"
|
||||
@@ -798,7 +765,10 @@
|
||||
[[projects]]
|
||||
digest = "1:01d968ff6535945510c944983eee024e81f1c949043e9bbfe5ab206ebc3588a4"
|
||||
name = "github.com/sirupsen/logrus"
|
||||
packages = ["."]
|
||||
packages = [
|
||||
".",
|
||||
"hooks/test",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "a67f783a3814b8729bd2dac5780b5f78f8dbd64d"
|
||||
version = "v1.1.0"
|
||||
@@ -820,19 +790,19 @@
|
||||
version = "v0.1.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2208a80fc3259291e43b30f42f844d18f4218036dff510f42c653ec9890d460a"
|
||||
digest = "1:9ba49264cef4386aded205f9cb5b1f2d30f983d7dc37a21c780d9db3edfac9a7"
|
||||
name = "github.com/spf13/cobra"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "7b2c5ac9fc04fc5efafb60700713d4fa609b777b"
|
||||
version = "v0.0.1"
|
||||
revision = "fe5e611709b0c57fa4a89136deaa8e1d4004d053"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:261bc565833ef4f02121450d74eb88d5ae4bd74bfe5d0e862cddb8550ec35000"
|
||||
digest = "1:8e243c568f36b09031ec18dff5f7d2769dcf5ca4d624ea511c8e3197dc3d352d"
|
||||
name = "github.com/spf13/pflag"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
|
||||
revision = "583c0c0531f06d5278b7d917446061adc344b5cd"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:b1861b9a1aa0801b0b62945ed7477c1ab61a4bd03b55dfbc27f6d4f378110c8c"
|
||||
@@ -992,8 +962,6 @@
|
||||
packages = [
|
||||
"unix",
|
||||
"windows",
|
||||
"windows/registry",
|
||||
"windows/svc/eventlog",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "d0be0721c37eeb5299f245a996a483160fc36940"
|
||||
@@ -1047,6 +1015,39 @@
|
||||
pruneopts = ""
|
||||
revision = "5e776fee60db37e560cee3fb46db699d2f095386"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:e9e4b928898842a138bc345d42aae33741baa6d64f3ca69b0931f9c7a4fd0437"
|
||||
name = "gonum.org/v1/gonum"
|
||||
packages = [
|
||||
"blas",
|
||||
"blas/blas64",
|
||||
"blas/cblas128",
|
||||
"blas/gonum",
|
||||
"floats",
|
||||
"graph",
|
||||
"graph/internal/linear",
|
||||
"graph/internal/ordered",
|
||||
"graph/internal/set",
|
||||
"graph/internal/uid",
|
||||
"graph/iterator",
|
||||
"graph/simple",
|
||||
"graph/topo",
|
||||
"graph/traverse",
|
||||
"internal/asm/c128",
|
||||
"internal/asm/c64",
|
||||
"internal/asm/f32",
|
||||
"internal/asm/f64",
|
||||
"internal/cmplx64",
|
||||
"internal/math32",
|
||||
"lapack",
|
||||
"lapack/gonum",
|
||||
"lapack/lapack64",
|
||||
"mat",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "90b7154515874cee6c33cf56b29e257403a09a69"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:934fb8966f303ede63aa405e2c8d7f0a427a05ea8df335dfdc1833dd4d40756f"
|
||||
name = "google.golang.org/appengine"
|
||||
@@ -1116,25 +1117,18 @@
|
||||
version = "v1.15.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:15d017551627c8bb091bde628215b2861bed128855343fdd570c62d08871f6e1"
|
||||
name = "gopkg.in/alecthomas/kingpin.v2"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "947dcec5ba9c011838740e680966fd7087a71d0d"
|
||||
version = "v2.2.6"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:bf7444e1e6a36e633f4f1624a67b9e4734cfb879c27ac0a2082ac16aff8462ac"
|
||||
name = "gopkg.in/go-playground/webhooks.v3"
|
||||
digest = "1:adf5b0ae3467c3182757ecb86fbfe819939473bb870a42789dc1a3e7729397cd"
|
||||
name = "gopkg.in/go-playground/webhooks.v5"
|
||||
packages = [
|
||||
".",
|
||||
"bitbucket",
|
||||
"bitbucket-server",
|
||||
"github",
|
||||
"gitlab",
|
||||
"gogs",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "5580947e3ec83427ef5f6f2392eddca8dde5d99a"
|
||||
version = "v3.11.0"
|
||||
revision = "175186584584a83966dc9a7b8ec6c3d3a4ce6110"
|
||||
version = "v5.11.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:e5d1fb981765b6f7513f793a3fcaac7158408cca77f75f7311ac82cc88e9c445"
|
||||
@@ -1247,16 +1241,16 @@
|
||||
version = "v2.2.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "release-1.12"
|
||||
digest = "1:ed04c5203ecbf6358fb6a774b0ecd40ea992d6dcc42adc1d3b7cf9eceb66b6c8"
|
||||
branch = "release-1.14"
|
||||
digest = "1:d8a6f1ec98713e685346a2e4b46c6ec4a1792a5535f8b0dffe3b1c08c9d69b12"
|
||||
name = "k8s.io/api"
|
||||
packages = [
|
||||
"admission/v1beta1",
|
||||
"admissionregistration/v1alpha1",
|
||||
"admissionregistration/v1beta1",
|
||||
"apps/v1",
|
||||
"apps/v1beta1",
|
||||
"apps/v1beta2",
|
||||
"auditregistration/v1alpha1",
|
||||
"authentication/v1",
|
||||
"authentication/v1beta1",
|
||||
"authorization/v1",
|
||||
@@ -1268,16 +1262,21 @@
|
||||
"batch/v1beta1",
|
||||
"batch/v2alpha1",
|
||||
"certificates/v1beta1",
|
||||
"coordination/v1",
|
||||
"coordination/v1beta1",
|
||||
"core/v1",
|
||||
"events/v1beta1",
|
||||
"extensions/v1beta1",
|
||||
"imagepolicy/v1alpha1",
|
||||
"networking/v1",
|
||||
"networking/v1beta1",
|
||||
"node/v1alpha1",
|
||||
"node/v1beta1",
|
||||
"policy/v1beta1",
|
||||
"rbac/v1",
|
||||
"rbac/v1alpha1",
|
||||
"rbac/v1beta1",
|
||||
"scheduling/v1",
|
||||
"scheduling/v1alpha1",
|
||||
"scheduling/v1beta1",
|
||||
"settings/v1alpha1",
|
||||
@@ -1286,11 +1285,11 @@
|
||||
"storage/v1beta1",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "475331a8afff5587f47d0470a93f79c60c573c03"
|
||||
revision = "40a48860b5abbba9aa891b02b32da429b08d96a0"
|
||||
|
||||
[[projects]]
|
||||
branch = "release-1.12"
|
||||
digest = "1:39be82077450762b5e14b5268e679a14ac0e9c7d3286e2fcface437556a29e4c"
|
||||
branch = "master"
|
||||
digest = "1:49e0fcdcaeaf937c6c608d1da19eb80de74fe990021278d49d46e10288659be6"
|
||||
name = "k8s.io/apiextensions-apiserver"
|
||||
packages = [
|
||||
"pkg/apis/apiextensions",
|
||||
@@ -1298,14 +1297,13 @@
|
||||
"pkg/client/clientset/clientset",
|
||||
"pkg/client/clientset/clientset/scheme",
|
||||
"pkg/client/clientset/clientset/typed/apiextensions/v1beta1",
|
||||
"pkg/features",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "ca1024863b48cf0701229109df75ac5f0bb4907e"
|
||||
revision = "7f7d2b94eca3a7a1c49840e119a8bc03c3afb1e3"
|
||||
|
||||
[[projects]]
|
||||
branch = "release-1.12"
|
||||
digest = "1:5899da40e41bcc8c1df101b72954096bba9d85b763bc17efc846062ccc111c7b"
|
||||
branch = "release-1.14"
|
||||
digest = "1:a802c91b189a31200cfb66744441fe62dac961ec7c5c58c47716570de7da195c"
|
||||
name = "k8s.io/apimachinery"
|
||||
packages = [
|
||||
"pkg/api/equality",
|
||||
@@ -1357,34 +1355,22 @@
|
||||
"third_party/forked/golang/reflect",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "f71dbbc36e126f5a371b85f6cca96bc8c57db2b6"
|
||||
revision = "6a84e37a896db9780c75367af8d2ed2bb944022e"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:cb3ac215bfac54696f64a6e5c46524a7fc0f7a8f9b7a22cccb2e1e83ac2d013f"
|
||||
name = "k8s.io/apiserver"
|
||||
packages = [
|
||||
"pkg/features",
|
||||
"pkg/util/feature",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "19cf388d0a374e95329bf7d98e9bfd7da8853be0"
|
||||
|
||||
[[projects]]
|
||||
branch = "release-9.0"
|
||||
digest = "1:77bf3d9f18ec82e08ac6c4c7e2d9d1a2ef8d16b25d3ff72fcefcf9256d751573"
|
||||
branch = "release-11.0"
|
||||
digest = "1:794140b3ac07405646ea3d4a57e1f6155186e672aed8aa0c996779381cd92fe6"
|
||||
name = "k8s.io/client-go"
|
||||
packages = [
|
||||
"discovery",
|
||||
"discovery/fake",
|
||||
"dynamic",
|
||||
"dynamic/fake",
|
||||
"informers/core/v1",
|
||||
"informers/internalinterfaces",
|
||||
"kubernetes",
|
||||
"kubernetes/fake",
|
||||
"kubernetes/scheme",
|
||||
"kubernetes/typed/admissionregistration/v1alpha1",
|
||||
"kubernetes/typed/admissionregistration/v1alpha1/fake",
|
||||
"kubernetes/typed/admissionregistration/v1beta1",
|
||||
"kubernetes/typed/admissionregistration/v1beta1/fake",
|
||||
"kubernetes/typed/apps/v1",
|
||||
@@ -1393,6 +1379,8 @@
|
||||
"kubernetes/typed/apps/v1beta1/fake",
|
||||
"kubernetes/typed/apps/v1beta2",
|
||||
"kubernetes/typed/apps/v1beta2/fake",
|
||||
"kubernetes/typed/auditregistration/v1alpha1",
|
||||
"kubernetes/typed/auditregistration/v1alpha1/fake",
|
||||
"kubernetes/typed/authentication/v1",
|
||||
"kubernetes/typed/authentication/v1/fake",
|
||||
"kubernetes/typed/authentication/v1beta1",
|
||||
@@ -1415,6 +1403,8 @@
|
||||
"kubernetes/typed/batch/v2alpha1/fake",
|
||||
"kubernetes/typed/certificates/v1beta1",
|
||||
"kubernetes/typed/certificates/v1beta1/fake",
|
||||
"kubernetes/typed/coordination/v1",
|
||||
"kubernetes/typed/coordination/v1/fake",
|
||||
"kubernetes/typed/coordination/v1beta1",
|
||||
"kubernetes/typed/coordination/v1beta1/fake",
|
||||
"kubernetes/typed/core/v1",
|
||||
@@ -1425,6 +1415,12 @@
|
||||
"kubernetes/typed/extensions/v1beta1/fake",
|
||||
"kubernetes/typed/networking/v1",
|
||||
"kubernetes/typed/networking/v1/fake",
|
||||
"kubernetes/typed/networking/v1beta1",
|
||||
"kubernetes/typed/networking/v1beta1/fake",
|
||||
"kubernetes/typed/node/v1alpha1",
|
||||
"kubernetes/typed/node/v1alpha1/fake",
|
||||
"kubernetes/typed/node/v1beta1",
|
||||
"kubernetes/typed/node/v1beta1/fake",
|
||||
"kubernetes/typed/policy/v1beta1",
|
||||
"kubernetes/typed/policy/v1beta1/fake",
|
||||
"kubernetes/typed/rbac/v1",
|
||||
@@ -1433,6 +1429,8 @@
|
||||
"kubernetes/typed/rbac/v1alpha1/fake",
|
||||
"kubernetes/typed/rbac/v1beta1",
|
||||
"kubernetes/typed/rbac/v1beta1/fake",
|
||||
"kubernetes/typed/scheduling/v1",
|
||||
"kubernetes/typed/scheduling/v1/fake",
|
||||
"kubernetes/typed/scheduling/v1alpha1",
|
||||
"kubernetes/typed/scheduling/v1alpha1/fake",
|
||||
"kubernetes/typed/scheduling/v1beta1",
|
||||
@@ -1469,23 +1467,22 @@
|
||||
"tools/remotecommand",
|
||||
"transport",
|
||||
"transport/spdy",
|
||||
"util/buffer",
|
||||
"util/cert",
|
||||
"util/connrotation",
|
||||
"util/exec",
|
||||
"util/flowcontrol",
|
||||
"util/homedir",
|
||||
"util/integer",
|
||||
"util/jsonpath",
|
||||
"util/keyutil",
|
||||
"util/retry",
|
||||
"util/workqueue",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "13596e875accbd333e0b5bd5fd9462185acd9958"
|
||||
revision = "11646d1007e006f6f24995cb905c68bc62901c81"
|
||||
|
||||
[[projects]]
|
||||
branch = "release-1.12"
|
||||
digest = "1:e6fffdf0dfeb0d189a7c6d735e76e7564685d3b6513f8b19d3651191cb6b084b"
|
||||
branch = "release-1.14"
|
||||
digest = "1:742ce70d2c6de0f02b5331a25d4d549f55de6b214af22044455fd6e6b451cad9"
|
||||
name = "k8s.io/code-generator"
|
||||
packages = [
|
||||
"cmd/go-to-protobuf",
|
||||
@@ -1494,43 +1491,61 @@
|
||||
"third_party/forked/golang/reflect",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "3dcf91f64f638563e5106f21f50c31fa361c918d"
|
||||
revision = "50b561225d70b3eb79a1faafd3dfe7b1a62cbe73"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:15710582bd5ceff07eee4726884f75f97f90366fde9307b8dd09500c75722456"
|
||||
digest = "1:6a2a63e09a59caff3fd2d36d69b7b92c2fe7cf783390f0b7349fb330820f9a8e"
|
||||
name = "k8s.io/gengo"
|
||||
packages = [
|
||||
"args",
|
||||
"examples/set-gen/sets",
|
||||
"generator",
|
||||
"namer",
|
||||
"parser",
|
||||
"types",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "8394c995ab8fbe52216f38d0e1a37de36d820528"
|
||||
revision = "e17681d19d3ac4837a019ece36c2a0ec31ffe985"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:4f5eb833037cc0ba0bf8fe9cae6be9df62c19dd1c869415275c708daa8ccfda5"
|
||||
digest = "1:9eaf86f4f6fb4a8f177220d488ef1e3255d06a691cca95f14ef085d4cd1cef3c"
|
||||
name = "k8s.io/klog"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "a5bc97fbc634d635061f3146511332c7e313a55a"
|
||||
version = "v0.1.0"
|
||||
revision = "d98d8acdac006fb39831f1b25640813fef9c314f"
|
||||
version = "v0.3.3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:9a648ff9eb89673d2870c22fc011ec5db0fcff6c4e5174a650298e51be71bbf1"
|
||||
name = "k8s.io/kube-openapi"
|
||||
digest = "1:0d737d598e9db0a38d6ef6cba514c358b9fe7e1bc6b1128d02b2622700c75f2a"
|
||||
name = "k8s.io/kube-aggregator"
|
||||
packages = [
|
||||
"pkg/common",
|
||||
"pkg/util/proto",
|
||||
"pkg/apis/apiregistration",
|
||||
"pkg/apis/apiregistration/v1",
|
||||
"pkg/apis/apiregistration/v1beta1",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
|
||||
revision = "e80910364765199a4baebd4dec54c885fe52b680"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:6061aa42761235df375f20fa4a1aa6d1845cba3687575f3adb2ef3f3bc540af5"
|
||||
digest = "1:42ea993b351fdd39b9aad3c9ebe71f2fdb5d1f8d12eed24e71c3dff1a31b2a43"
|
||||
name = "k8s.io/kube-openapi"
|
||||
packages = [
|
||||
"cmd/openapi-gen",
|
||||
"cmd/openapi-gen/args",
|
||||
"pkg/common",
|
||||
"pkg/generators",
|
||||
"pkg/generators/rules",
|
||||
"pkg/util/proto",
|
||||
"pkg/util/sets",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "411b2483e5034420675ebcdd4a55fc76fe5e55cf"
|
||||
|
||||
[[projects]]
|
||||
branch = "release-1.14"
|
||||
digest = "1:78aa6079e011ece0d28513c7fe1bd64284fa9eb5d671760803a839ffdf0e9e38"
|
||||
name = "k8s.io/kubernetes"
|
||||
packages = [
|
||||
"pkg/api/v1/pod",
|
||||
@@ -1538,19 +1553,26 @@
|
||||
"pkg/apis/autoscaling",
|
||||
"pkg/apis/batch",
|
||||
"pkg/apis/core",
|
||||
"pkg/apis/extensions",
|
||||
"pkg/apis/networking",
|
||||
"pkg/apis/policy",
|
||||
"pkg/features",
|
||||
"pkg/kubectl/scheme",
|
||||
"pkg/kubectl/util/term",
|
||||
"pkg/kubelet/apis",
|
||||
"pkg/util/interrupt",
|
||||
"pkg/util/node",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "17c77c7898218073f14c8d573582e8d2313dc740"
|
||||
version = "v1.12.2"
|
||||
revision = "2d20b5759406ded89f8b25cf085ff4733b144ba5"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:4c5d39f7ca1c940d7e74dbc62d2221e2c59b3d35c54f1fa9c77f3fd3113bbcb1"
|
||||
name = "k8s.io/utils"
|
||||
packages = [
|
||||
"buffer",
|
||||
"integer",
|
||||
"pointer",
|
||||
"trace",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "c55fbcfc754a5b2ec2fbae8fb9dcac36bdba6a12"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -1560,14 +1582,23 @@
|
||||
pruneopts = ""
|
||||
revision = "97fed8db84274c421dbfffbb28ec859901556b97"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:321081b4a44256715f2b68411d8eda9a17f17ebfe6f0cc61d2cc52d11c08acfa"
|
||||
name = "sigs.k8s.io/yaml"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "fd68e9863619f6ec2fdd8625fe1f02e7c877e480"
|
||||
version = "v1.1.0"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
input-imports = [
|
||||
"bou.ke/monkey",
|
||||
"github.com/Masterminds/semver",
|
||||
"github.com/TomOnTime/utfutil",
|
||||
"github.com/argoproj/argo/pkg/apis/workflow/v1alpha1",
|
||||
"github.com/argoproj/argo/util",
|
||||
"github.com/argoproj/pkg/errors",
|
||||
"github.com/argoproj/pkg/exec",
|
||||
"github.com/argoproj/pkg/time",
|
||||
"github.com/casbin/casbin",
|
||||
@@ -1580,14 +1611,17 @@
|
||||
"github.com/ghodss/yaml",
|
||||
"github.com/go-openapi/loads",
|
||||
"github.com/go-openapi/runtime/middleware",
|
||||
"github.com/go-openapi/spec",
|
||||
"github.com/go-redis/cache",
|
||||
"github.com/go-redis/redis",
|
||||
"github.com/gobuffalo/packr",
|
||||
"github.com/gobwas/glob",
|
||||
"github.com/gogits/go-gogs-client",
|
||||
"github.com/gogo/protobuf/gogoproto",
|
||||
"github.com/gogo/protobuf/proto",
|
||||
"github.com/gogo/protobuf/protoc-gen-gofast",
|
||||
"github.com/gogo/protobuf/protoc-gen-gogofast",
|
||||
"github.com/gogo/protobuf/sortkeys",
|
||||
"github.com/golang/protobuf/proto",
|
||||
"github.com/golang/protobuf/protoc-gen-go",
|
||||
"github.com/golang/protobuf/ptypes/empty",
|
||||
@@ -1610,8 +1644,9 @@
|
||||
"github.com/pkg/errors",
|
||||
"github.com/prometheus/client_golang/prometheus",
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp",
|
||||
"github.com/prometheus/common/log",
|
||||
"github.com/robfig/cron",
|
||||
"github.com/sirupsen/logrus",
|
||||
"github.com/sirupsen/logrus/hooks/test",
|
||||
"github.com/skratchdot/open-golang/open",
|
||||
"github.com/soheilhy/cmux",
|
||||
"github.com/spf13/cobra",
|
||||
@@ -1624,6 +1659,7 @@
|
||||
"github.com/yuin/gopher-lua",
|
||||
"golang.org/x/crypto/bcrypt",
|
||||
"golang.org/x/crypto/ssh",
|
||||
"golang.org/x/crypto/ssh/knownhosts",
|
||||
"golang.org/x/crypto/ssh/terminal",
|
||||
"golang.org/x/net/context",
|
||||
"golang.org/x/oauth2",
|
||||
@@ -1637,23 +1673,27 @@
|
||||
"google.golang.org/grpc/metadata",
|
||||
"google.golang.org/grpc/reflection",
|
||||
"google.golang.org/grpc/status",
|
||||
"gopkg.in/go-playground/webhooks.v3",
|
||||
"gopkg.in/go-playground/webhooks.v3/bitbucket",
|
||||
"gopkg.in/go-playground/webhooks.v3/github",
|
||||
"gopkg.in/go-playground/webhooks.v3/gitlab",
|
||||
"gopkg.in/go-playground/webhooks.v5/bitbucket",
|
||||
"gopkg.in/go-playground/webhooks.v5/bitbucket-server",
|
||||
"gopkg.in/go-playground/webhooks.v5/github",
|
||||
"gopkg.in/go-playground/webhooks.v5/gitlab",
|
||||
"gopkg.in/go-playground/webhooks.v5/gogs",
|
||||
"gopkg.in/src-d/go-git.v4",
|
||||
"gopkg.in/src-d/go-git.v4/config",
|
||||
"gopkg.in/src-d/go-git.v4/plumbing",
|
||||
"gopkg.in/src-d/go-git.v4/plumbing/transport",
|
||||
"gopkg.in/src-d/go-git.v4/plumbing/transport/client",
|
||||
"gopkg.in/src-d/go-git.v4/plumbing/transport/http",
|
||||
"gopkg.in/src-d/go-git.v4/plumbing/transport/ssh",
|
||||
"gopkg.in/src-d/go-git.v4/storage/memory",
|
||||
"gopkg.in/src-d/go-git.v4/utils/ioutil",
|
||||
"gopkg.in/yaml.v2",
|
||||
"k8s.io/api/apps/v1",
|
||||
"k8s.io/api/batch/v1",
|
||||
"k8s.io/api/core/v1",
|
||||
"k8s.io/api/extensions/v1beta1",
|
||||
"k8s.io/api/rbac/v1",
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1",
|
||||
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset",
|
||||
"k8s.io/apimachinery/pkg/api/equality",
|
||||
"k8s.io/apimachinery/pkg/api/errors",
|
||||
@@ -1673,6 +1713,7 @@
|
||||
"k8s.io/client-go/discovery",
|
||||
"k8s.io/client-go/discovery/fake",
|
||||
"k8s.io/client-go/dynamic",
|
||||
"k8s.io/client-go/dynamic/fake",
|
||||
"k8s.io/client-go/informers/core/v1",
|
||||
"k8s.io/client-go/kubernetes",
|
||||
"k8s.io/client-go/kubernetes/fake",
|
||||
@@ -1687,6 +1728,11 @@
|
||||
"k8s.io/client-go/util/flowcontrol",
|
||||
"k8s.io/client-go/util/workqueue",
|
||||
"k8s.io/code-generator/cmd/go-to-protobuf",
|
||||
"k8s.io/klog",
|
||||
"k8s.io/kube-aggregator/pkg/apis/apiregistration/v1",
|
||||
"k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1",
|
||||
"k8s.io/kube-openapi/cmd/openapi-gen",
|
||||
"k8s.io/kube-openapi/pkg/common",
|
||||
"k8s.io/kubernetes/pkg/api/v1/pod",
|
||||
"k8s.io/kubernetes/pkg/apis/apps",
|
||||
"k8s.io/kubernetes/pkg/apis/batch",
|
||||
@@ -1694,6 +1740,7 @@
|
||||
"k8s.io/kubernetes/pkg/kubectl/scheme",
|
||||
"k8s.io/kubernetes/pkg/kubectl/util/term",
|
||||
"k8s.io/kubernetes/pkg/util/node",
|
||||
"k8s.io/utils/pointer",
|
||||
"layeh.com/gopher-json",
|
||||
]
|
||||
solver-name = "gps-cdcl"
|
||||
|
||||
33
Gopkg.toml
@@ -7,6 +7,7 @@ required = [
|
||||
"github.com/gogo/protobuf/protoc-gen-gofast",
|
||||
"github.com/gogo/protobuf/protoc-gen-gogofast",
|
||||
"k8s.io/code-generator/cmd/go-to-protobuf",
|
||||
"k8s.io/kube-openapi/cmd/openapi-gen",
|
||||
"github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway",
|
||||
"github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger",
|
||||
"golang.org/x/sync/errgroup",
|
||||
@@ -34,20 +35,24 @@ required = [
|
||||
name = "github.com/prometheus/client_golang"
|
||||
revision = "7858729281ec582767b20e0d696b6041d995d5e0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "release-1.12"
|
||||
[[override]]
|
||||
branch = "release-1.14"
|
||||
name = "k8s.io/api"
|
||||
|
||||
[[constraint]]
|
||||
name = "k8s.io/apiextensions-apiserver"
|
||||
branch = "release-1.12"
|
||||
[[override]]
|
||||
branch = "release-1.14"
|
||||
name = "k8s.io/kubernetes"
|
||||
|
||||
[[constraint]]
|
||||
branch = "release-1.12"
|
||||
[[override]]
|
||||
branch = "release-1.14"
|
||||
name = "k8s.io/code-generator"
|
||||
|
||||
[[constraint]]
|
||||
branch = "release-9.0"
|
||||
[[override]]
|
||||
branch = "release-1.14"
|
||||
name = "k8s.io/apimachinery"
|
||||
|
||||
[[override]]
|
||||
branch = "release-11.0"
|
||||
name = "k8s.io/client-go"
|
||||
|
||||
[[constraint]]
|
||||
@@ -65,3 +70,13 @@ required = [
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/yudai/gojsondiff"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/spf13/cobra"
|
||||
revision = "fe5e611709b0c57fa4a89136deaa8e1d4004d053"
|
||||
|
||||
# TODO: move off of k8s.io/kube-openapi and use controller-tools for CRD spec generation
|
||||
# (override argoproj/argo contraint on master)
|
||||
[[override]]
|
||||
revision = "411b2483e5034420675ebcdd4a55fc76fe5e55cf"
|
||||
name = "k8s.io/kube-openapi"
|
||||
|
||||
103
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,14 +9,21 @@ 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)
|
||||
VOLUME_MOUNT=$(shell [[ $(go env GOOS)=="darwin" ]] && echo ":delegated" || echo "")
|
||||
|
||||
define run-in-dev-tool
|
||||
docker run --rm -it -u $(shell id -u) -e HOME=/home/user -v ${CURRENT_DIR}:/go/src/github.com/argoproj/argo-cd${VOLUME_MOUNT} -w /go/src/github.com/argoproj/argo-cd argocd-dev-tools bash -c "GOPATH=/go $(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} \
|
||||
@@ -50,12 +57,20 @@ all: cli image argocd-util
|
||||
protogen:
|
||||
./hack/generate-proto.sh
|
||||
|
||||
.PHONY: openapigen
|
||||
openapigen:
|
||||
./hack/update-openapi.sh
|
||||
|
||||
.PHONY: clientgen
|
||||
clientgen:
|
||||
./hack/update-codegen.sh
|
||||
|
||||
.PHONY: codegen-local
|
||||
codegen-local: protogen clientgen openapigen manifests-local
|
||||
|
||||
.PHONY: codegen
|
||||
codegen: protogen clientgen format-code
|
||||
codegen: dev-tools-image
|
||||
$(call run-in-dev-tool,make codegen-local)
|
||||
|
||||
.PHONY: cli
|
||||
cli: clean-debug
|
||||
@@ -71,25 +86,34 @@ release-cli: clean-debug image
|
||||
.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:
|
||||
cd hack && docker build -t argocd-dev-tools . -f Dockerfile.dev-tools
|
||||
|
||||
.PHONY: manifests-local
|
||||
manifests-local:
|
||||
./hack/update-manifests.sh
|
||||
|
||||
.PHONY: manifests
|
||||
manifests:
|
||||
./hack/update-manifests.sh
|
||||
$(call run-in-dev-tool,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
|
||||
${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-server ./cmd/argocd-server
|
||||
|
||||
CGO_ENABLED=0 ${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-server ./cmd/argocd-server
|
||||
|
||||
.PHONY: repo-server
|
||||
repo-server:
|
||||
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:
|
||||
${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-application-controller ./cmd/argocd-application-controller
|
||||
CGO_ENABLED=0 ${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-application-controller ./cmd/argocd-application-controller
|
||||
|
||||
.PHONY: packr
|
||||
packr:
|
||||
@@ -100,8 +124,10 @@ 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
|
||||
@@ -119,26 +145,51 @@ endif
|
||||
.PHONY: builder-image
|
||||
builder-image:
|
||||
docker build -t $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG) --target builder .
|
||||
@if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG) ; fi
|
||||
|
||||
.PHONY: dep
|
||||
dep:
|
||||
dep ensure -v
|
||||
|
||||
.PHONY: dep-ensure
|
||||
dep-ensure:
|
||||
dep ensure
|
||||
dep ensure -no-vendor
|
||||
|
||||
.PHONY: format-code
|
||||
format-code:
|
||||
./hack/format-code.sh
|
||||
.PHONY: install-lint-tools
|
||||
install-lint-tools:
|
||||
./hack/install.sh lint-tools
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
gometalinter.v2 --config gometalinter.json ./...
|
||||
golangci-lint --version
|
||||
golangci-lint run --fix --verbose
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
go build -v `go list ./... | grep -v 'resource_customizations\|test/e2e'`
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test -covermode=count -coverprofile=coverage.out `go list ./... | grep -v "github.com/argoproj/argo-cd/test/e2e"`
|
||||
./hack/test.sh -coverprofile=coverage.out `go list ./... | grep -v 'test/e2e'`
|
||||
|
||||
.PHONY: test-e2e
|
||||
test-e2e:
|
||||
go test -v -failfast -timeout 20m ./test/e2e
|
||||
./hack/test.sh -timeout 15m ./test/e2e
|
||||
|
||||
.PHONY: start-e2e
|
||||
start-e2e: cli
|
||||
killall goreman || true
|
||||
# check we can connect to Docker to start Redis
|
||||
docker version
|
||||
kubectl create ns argocd-e2e || true
|
||||
kubectl config set-context --current --namespace=argocd-e2e
|
||||
kustomize build test/manifests/base | kubectl apply -f -
|
||||
# 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 \
|
||||
goreman start
|
||||
|
||||
# Cleans VSCode debug.test files from sub-dirs to prevent them from being included in packr boxes
|
||||
.PHONY: clean-debug
|
||||
@@ -149,8 +200,18 @@ clean-debug:
|
||||
clean: clean-debug
|
||||
-rm -rf ${CURRENT_DIR}/dist
|
||||
|
||||
.PHONY: start
|
||||
start:
|
||||
killall goreman || true
|
||||
# check we can connect to Docker to start Redis
|
||||
docker version
|
||||
kubectl create ns argocd || true
|
||||
kubens argocd
|
||||
ARGOCD_ZJWT_FEATURE_FLAG=always \
|
||||
goreman start
|
||||
|
||||
.PHONY: pre-commit
|
||||
pre-commit: dep-ensure codegen format-code test lint
|
||||
pre-commit: dep-ensure codegen build lint test
|
||||
|
||||
.PHONY: release-precheck
|
||||
release-precheck: manifests
|
||||
@@ -159,4 +220,4 @@ 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
|
||||
|
||||
6
OWNERS
@@ -1,8 +1,12 @@
|
||||
owners:
|
||||
- alexec
|
||||
- alexmt
|
||||
- jessesuen
|
||||
|
||||
reviewers:
|
||||
- jannfis
|
||||
|
||||
approvers:
|
||||
- alexec
|
||||
- alexmt
|
||||
- jessesuen
|
||||
- merenbach
|
||||
|
||||
12
Procfile
@@ -1,5 +1,7 @@
|
||||
controller: go run ./cmd/argocd-application-controller/main.go --redis localhost:6379 --repo-server localhost:8081
|
||||
api-server: go run ./cmd/argocd-server/main.go --redis localhost:6379 --disable-auth --insecure --dex-server http://localhost:5556 --repo-server localhost:8081 --staticassets ../argo-cd-ui/dist/app
|
||||
repo-server: 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 ./cmd/argocd-util/main.go 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.14.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
|
||||
|
||||
121
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
|
||||
|
||||
@@ -7,100 +8,54 @@
|
||||
|
||||
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.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Quickstart
|
||||
## Who uses Argo CD?
|
||||
|
||||
```bash
|
||||
kubectl create namespace argocd
|
||||
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
|
||||
```
|
||||
Organizations below are **officially** using Argo CD. Please send a PR with your organization name if you are using Argo CD.
|
||||
|
||||
Follow our [getting started guide](docs/getting_started.md). Further [documentation](docs/)
|
||||
is provided for additional features.
|
||||
1. [ANSTO - Australian Synchrotron](https://www.synchrotron.org.au/)
|
||||
1. [Codility](https://www.codility.com/)
|
||||
1. [Commonbond](https://commonbond.co/)
|
||||
1. [CyberAgent](https://www.cyberagent.co.jp/en/)
|
||||
1. [END.](https://www.endclothing.com/)
|
||||
1. [Future PLC](https://www.futureplc.com/)
|
||||
1. [GMETRI](https://gmetri.com/)
|
||||
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. [Mambu](https://www.mambu.com/)
|
||||
1. [Mirantis](https://mirantis.com/)
|
||||
1. [OpenSaaS Studio](https://opensaas.studio)
|
||||
1. [Optoro](https://www.optoro.com/)
|
||||
1. [Riskified](https://www.riskified.com/)
|
||||
1. [Saildrone](https://www.saildrone.com/)
|
||||
1. [Tesla](https://tesla.com/)
|
||||
1. [tZERO](https://www.tzero.com/)
|
||||
1. [Ticketmaster](https://ticketmaster.com)
|
||||
1. [Yieldlab](https://www.yieldlab.de/)
|
||||
1. [UBIO](https://ub.io/)
|
||||
1. [Volvo Cars](https://www.volvocars.com/)
|
||||
|
||||
## How it works
|
||||
## Documentation
|
||||
|
||||
Argo CD follows the **GitOps** pattern of using git repositories as the source of truth for defining
|
||||
the desired application state. Kubernetes manifests can be specified in several ways:
|
||||
* [kustomize](https://kustomize.io) applications
|
||||
* [helm](https://helm.sh) charts
|
||||
* [ksonnet](https://ksonnet.io) applications
|
||||
* [jsonnet](https://jsonnet.org) files
|
||||
* Plain directory of YAML/json manifests
|
||||
* Any custom config management tool configured as a config management plugin
|
||||
|
||||
Argo CD automates the deployment of the desired application states in the specified target environments.
|
||||
Application deployments can track updates to branches, tags, or pinned to a specific version of
|
||||
manifests at a git commit. See [tracking strategies](docs/tracking_strategies.md) for additional
|
||||
details about the different tracking strategies available.
|
||||
|
||||
For a quick 10 minute overview of Argo CD, check out the demo presented to the Sig Apps community
|
||||
meeting:
|
||||
[](https://youtu.be/aWDIQMbp1cc?t=1m4s)
|
||||
|
||||
|
||||
## Architecture
|
||||
|
||||

|
||||
|
||||
Argo CD is implemented as a kubernetes controller which continuously monitors running applications
|
||||
and compares the current, live state against the desired target state (as specified in the git repo).
|
||||
A deployed application whose live state deviates from the target state is considered `OutOfSync`.
|
||||
Argo CD reports & visualizes the differences, while providing facilities to automatically or
|
||||
manually sync the live state back to the desired target state. Any modifications made to the desired
|
||||
target state in the git repo can be automatically applied and reflected in the specified target
|
||||
environments.
|
||||
|
||||
For additional details, see [architecture overview](docs/architecture.md).
|
||||
|
||||
## Features
|
||||
|
||||
* Automated deployment of applications to specified target environments
|
||||
* Support for multiple config management/templating tools (Kustomize, Helm, Ksonnet, Jsonnet, plain-YAML)
|
||||
* Ability to manage and deploy to multiple clusters
|
||||
* SSO Integration (OIDC, OAuth2, LDAP, SAML 2.0, GitHub, GitLab, Microsoft, LinkedIn)
|
||||
* Multi-tenancy and RBAC policies for authorization
|
||||
* Rollback/Roll-anywhere to any application configuration committed in git repository
|
||||
* Health status analysis of application resources
|
||||
* Automated configuration drift detection and visualization
|
||||
* Automated or manual syncing of applications to its desired state
|
||||
* Web UI which provides real-time view of application activity
|
||||
* CLI for automation and CI integration
|
||||
* Webhook integration (GitHub, BitBucket, GitLab)
|
||||
* Access tokens for automation
|
||||
* PreSync, Sync, PostSync hooks to support complex application rollouts (e.g.blue/green & canary upgrades)
|
||||
* Audit trails for application events and API calls
|
||||
* Prometheus metrics
|
||||
* Parameter overrides for overriding ksonnet/helm parameters in git
|
||||
To learn more about Argo CD [go to the complete documentation](https://argoproj.github.io/argo-cd/).
|
||||
|
||||
## Community Blogs and Presentations
|
||||
* GitOps with Argo CD: [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/)
|
||||
* KubeCon talk: [CI/CD in Light Speed with K8s and Argo CD](https://www.youtube.com/watch?v=OdzH82VpMwI&feature=youtu.be)
|
||||
* KubeCon talk: [Machine Learning as Code](https://www.youtube.com/watch?v=VXrGp5er1ZE&t=0s&index=135&list=PLj6h78yzYM2PZf9eA7bhWnIh_mK1vyOfU)
|
||||
* Among other things, desribes how Kubeflow uses Argo CD to implement GitOPs for ML
|
||||
* SIG Apps demo: [Argo CD - GitOps Continuous Delivery for Kubernetes](https://www.youtube.com/watch?v=aWDIQMbp1cc&feature=youtu.be&t=1m4s)
|
||||
|
||||
## Project Resources
|
||||
* Argo GitHub: https://github.com/argoproj
|
||||
* Argo Slack: [click here to join](https://argoproj.github.io/community/join-slack)
|
||||
* Argo website: https://argoproj.github.io/
|
||||
|
||||
## Development Status
|
||||
* Argo CD is actively developed and is being used in production to deploy SaaS services at Intuit
|
||||
|
||||
## Roadmap
|
||||
|
||||
### v0.12
|
||||
* Support for custom K8S manifest templating engines
|
||||
* Support for custom health assessments (e.g. CRD health)
|
||||
* Improved prometheus metrics
|
||||
* Higher availability
|
||||
* UI improvements
|
||||
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)
|
||||
|
||||
22
assets/badge.svg
Normal file
@@ -0,0 +1,22 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="131" height="20">
|
||||
<linearGradient id="b" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
</linearGradient>
|
||||
<clipPath id="a">
|
||||
<rect width="131" height="20" rx="3" fill="#fff"/>
|
||||
</clipPath>
|
||||
<g clip-path="url(#a)">
|
||||
<path id="leftPath" fill="#555" d="M0 0h74v20H0z"/>
|
||||
<path id="rightPath" fill="#4c1" d="M74 0h57v20H74z"/>
|
||||
<path fill="url(#b)" d="M0 0h131v20H0z"/>
|
||||
</g>
|
||||
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,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="leftText1" x="435" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="470"></text>
|
||||
<text id="leftText2" x="435" y="140" transform="scale(.1)" textLength="470"></text>
|
||||
|
||||
<text id="rightText1" x="995" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="470"></text>
|
||||
<text id="rightText1" x="995" y="140" transform="scale(.1)" textLength="470"></text></g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.6 KiB |
@@ -7,6 +7,7 @@
|
||||
# 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
|
||||
@@ -15,6 +16,11 @@ 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
|
||||
|
||||
|
1316
assets/swagger.json
@@ -16,14 +16,14 @@ 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"
|
||||
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/util/cache"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
)
|
||||
@@ -37,14 +37,18 @@ 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() (*cache.Cache, error)
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: cliName,
|
||||
@@ -54,9 +58,9 @@ func newCommand() *cobra.Command {
|
||||
cli.SetGLogLevel(glogLevel)
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
config.QPS = common.K8sClientConfigQPS
|
||||
config.Burst = common.K8sClientConfigBurst
|
||||
errors.CheckError(err)
|
||||
|
||||
kubeClient := kubernetes.NewForConfigOrDie(config)
|
||||
appClient := appclientset.NewForConfigOrDie(config)
|
||||
@@ -65,7 +69,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 +77,7 @@ func newCommand() *cobra.Command {
|
||||
errors.CheckError(err)
|
||||
|
||||
settingsMgr := settings.NewSettingsManager(ctx, kubeClient, namespace)
|
||||
kubectl := &kube.KubectlCmd{}
|
||||
appController, err := controller.NewApplicationController(
|
||||
namespace,
|
||||
settingsMgr,
|
||||
@@ -80,10 +85,14 @@ func newCommand() *cobra.Command {
|
||||
appClient,
|
||||
repoClientset,
|
||||
cache,
|
||||
resyncDuration)
|
||||
kubectl,
|
||||
resyncDuration,
|
||||
time.Duration(selfHealTimeoutSeconds)*time.Second,
|
||||
metricsPort,
|
||||
kubectlParallelismLimit)
|
||||
errors.CheckError(err)
|
||||
|
||||
log.Infof("Application Controller (version: %s) starting (namespace: %s)", argocd.GetVersion(), namespace)
|
||||
log.Infof("Application Controller (version: %s) starting (namespace: %s)", common.GetVersion(), namespace)
|
||||
stats.RegisterStackDumper()
|
||||
stats.StartStatsTicker(10 * time.Minute)
|
||||
stats.RegisterHeapDumper("memprofile")
|
||||
@@ -98,10 +107,15 @@ 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")
|
||||
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 = cache.AddCacheFlagsToCmd(&command)
|
||||
return &command
|
||||
}
|
||||
|
||||
@@ -3,19 +3,19 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"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/reposerver/metrics"
|
||||
"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/ksonnet"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
"github.com/argoproj/argo-cd/util/tls"
|
||||
)
|
||||
@@ -23,13 +23,14 @@ import (
|
||||
const (
|
||||
// CLIName is the name of the CLI
|
||||
cliName = "argocd-repo-server"
|
||||
port = 8081
|
||||
)
|
||||
|
||||
func newCommand() *cobra.Command {
|
||||
var (
|
||||
logLevel string
|
||||
parallelismLimit int64
|
||||
listenPort int
|
||||
metricsPort int
|
||||
cacheSrc func() (*cache.Cache, error)
|
||||
tlsConfigCustomizerSrc func() (tls.ConfigCustomizer, error)
|
||||
)
|
||||
@@ -45,17 +46,18 @@ func newCommand() *cobra.Command {
|
||||
cache, err := cacheSrc()
|
||||
errors.CheckError(err)
|
||||
|
||||
server, err := reposerver.NewServer(git.NewFactory(), cache, tlsConfigCustomizer, parallelismLimit)
|
||||
metricsServer := metrics.NewMetricsServer()
|
||||
server, err := reposerver.NewServer(metricsServer, cache, tlsConfigCustomizer, parallelismLimit)
|
||||
errors.CheckError(err)
|
||||
|
||||
grpc := server.CreateGRPC()
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", listenPort))
|
||||
errors.CheckError(err)
|
||||
|
||||
ksVers, err := ksonnet.KsonnetVersion()
|
||||
errors.CheckError(err)
|
||||
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("ksonnet version: %s", ksVers)
|
||||
log.Infof("argocd-repo-server %s serving on %s", common.GetVersion(), listener.Addr())
|
||||
stats.RegisterStackDumper()
|
||||
stats.StartStatsTicker(10 * time.Minute)
|
||||
stats.RegisterHeapDumper("memprofile")
|
||||
@@ -67,6 +69,8 @@ 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)
|
||||
return &command
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
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"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
@@ -22,17 +22,20 @@ import (
|
||||
// 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)
|
||||
insecure bool
|
||||
listenPort int
|
||||
metricsPort int
|
||||
logLevel string
|
||||
glogLevel int
|
||||
clientConfig clientcmd.ClientConfig
|
||||
repoServerTimeoutSeconds int
|
||||
staticAssetsDir string
|
||||
baseHRef string
|
||||
repoServerAddress string
|
||||
dexServerAddress string
|
||||
disableAuth bool
|
||||
tlsConfigCustomizerSrc func() (tls.ConfigCustomizer, error)
|
||||
cacheSrc func() (*cache.Cache, error)
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: cliName,
|
||||
@@ -57,10 +60,12 @@ func NewCommand() *cobra.Command {
|
||||
|
||||
kubeclientset := kubernetes.NewForConfigOrDie(config)
|
||||
appclientset := appclientset.NewForConfigOrDie(config)
|
||||
repoclientset := reposerver.NewRepoServerClientset(repoServerAddress)
|
||||
repoclientset := apiclient.NewRepoServerClientset(repoServerAddress, repoServerTimeoutSeconds)
|
||||
|
||||
argoCDOpts := server.ArgoCDServerOpts{
|
||||
Insecure: insecure,
|
||||
ListenPort: listenPort,
|
||||
MetricsPort: metricsPort,
|
||||
Namespace: namespace,
|
||||
StaticAssetsDir: staticAssetsDir,
|
||||
BaseHRef: baseHRef,
|
||||
@@ -81,7 +86,7 @@ func NewCommand() *cobra.Command {
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
argocd := server.NewServer(ctx, argoCDOpts)
|
||||
argocd.Run(ctx, 8080)
|
||||
argocd.Run(ctx, listenPort, metricsPort)
|
||||
cancel()
|
||||
}
|
||||
},
|
||||
@@ -97,6 +102,9 @@ func NewCommand() *cobra.Command {
|
||||
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.")
|
||||
tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(command)
|
||||
cacheSrc = cache.AddCacheFlagsToCmd(command)
|
||||
return command
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"regexp"
|
||||
"syscall"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
@@ -14,13 +16,18 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
|
||||
"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/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/dex"
|
||||
@@ -36,9 +43,15 @@ import (
|
||||
const (
|
||||
// CLIName is the name of the CLI
|
||||
cliName = "argocd-util"
|
||||
|
||||
// YamlSeparator separates sections of a YAML file
|
||||
yamlSeparator = "\n---\n"
|
||||
yamlSeparator = "---\n"
|
||||
)
|
||||
|
||||
var (
|
||||
configMapResource = schema.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"}
|
||||
secretResource = schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"}
|
||||
applicationsResource = schema.GroupVersionResource{Group: "argoproj.io", Version: "v1alpha1", Resource: "applications"}
|
||||
appprojectsResource = schema.GroupVersionResource{Group: "argoproj.io", Version: "v1alpha1", Resource: "appprojects"}
|
||||
)
|
||||
|
||||
// NewCommand returns a new instance of an argocd command
|
||||
@@ -96,7 +109,7 @@ func NewRunDexCommand() *cobra.Command {
|
||||
} else {
|
||||
err = ioutil.WriteFile("/tmp/dex.yaml", dexCfgBytes, 0644)
|
||||
errors.CheckError(err)
|
||||
log.Info(string(dexCfgBytes))
|
||||
log.Info(redactor(string(dexCfgBytes)))
|
||||
cmd = exec.Command("dex", "serve", "/tmp/dex.yaml")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
@@ -177,7 +190,7 @@ func NewGenDexConfigCommand() *cobra.Command {
|
||||
errors.CheckError(err)
|
||||
maskedDexCfgBytes, err := yaml.Marshal(dexCfg)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf(string(maskedDexCfgBytes))
|
||||
fmt.Print(string(maskedDexCfgBytes))
|
||||
} else {
|
||||
err = ioutil.WriteFile(out, dexCfgBytes, 0644)
|
||||
errors.CheckError(err)
|
||||
@@ -195,94 +208,153 @@ func NewGenDexConfigCommand() *cobra.Command {
|
||||
func NewImportCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
prune bool
|
||||
dryRun bool
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: "import SOURCE",
|
||||
Short: "Import Argo CD data from stdin (specify `-') or a file",
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var (
|
||||
input []byte
|
||||
err error
|
||||
newSettings *settings.ArgoCDSettings
|
||||
newRepos []*v1alpha1.Repository
|
||||
newClusters []*v1alpha1.Cluster
|
||||
newApps []*v1alpha1.Application
|
||||
newRBACCM *apiv1.ConfigMap
|
||||
)
|
||||
|
||||
if in := args[0]; in == "-" {
|
||||
input, err = ioutil.ReadAll(os.Stdin)
|
||||
errors.CheckError(err)
|
||||
} else {
|
||||
input, err = ioutil.ReadFile(in)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
inputStrings := strings.Split(string(input), yamlSeparator)
|
||||
|
||||
err = yaml.Unmarshal([]byte(inputStrings[0]), &newSettings)
|
||||
errors.CheckError(err)
|
||||
|
||||
err = yaml.Unmarshal([]byte(inputStrings[1]), &newRepos)
|
||||
errors.CheckError(err)
|
||||
|
||||
err = yaml.Unmarshal([]byte(inputStrings[2]), &newClusters)
|
||||
errors.CheckError(err)
|
||||
|
||||
err = yaml.Unmarshal([]byte(inputStrings[3]), &newApps)
|
||||
errors.CheckError(err)
|
||||
|
||||
err = yaml.Unmarshal([]byte(inputStrings[4]), &newRBACCM)
|
||||
errors.CheckError(err)
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
config.QPS = 100
|
||||
config.Burst = 50
|
||||
errors.CheckError(err)
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
kubeClientset := kubernetes.NewForConfigOrDie(config)
|
||||
acdClients := newArgoCDClientsets(config, namespace)
|
||||
|
||||
settingsMgr := settings.NewSettingsManager(context.Background(), kubeClientset, namespace)
|
||||
err = settingsMgr.SaveSettings(newSettings)
|
||||
var input []byte
|
||||
if in := args[0]; in == "-" {
|
||||
input, err = ioutil.ReadAll(os.Stdin)
|
||||
} else {
|
||||
input, err = ioutil.ReadFile(in)
|
||||
}
|
||||
errors.CheckError(err)
|
||||
db := db.NewDB(namespace, settingsMgr, kubeClientset)
|
||||
var dryRunMsg string
|
||||
if dryRun {
|
||||
dryRunMsg = " (dry run)"
|
||||
}
|
||||
|
||||
_, err = kubeClientset.CoreV1().ConfigMaps(namespace).Create(newRBACCM)
|
||||
// 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)
|
||||
configMaps, err := acdClients.configMaps.List(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
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()
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
for _, repo := range newRepos {
|
||||
_, err := db.CreateRepository(context.Background(), repo)
|
||||
if err != nil {
|
||||
log.Warn(err)
|
||||
// Create or replace existing object
|
||||
objs, 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]
|
||||
delete(pruneObjects, key)
|
||||
var dynClient dynamic.ResourceInterface
|
||||
switch obj.GetKind() {
|
||||
case "Secret":
|
||||
dynClient = acdClients.secrets
|
||||
case "ConfigMap":
|
||||
dynClient = acdClients.configMaps
|
||||
case "AppProject":
|
||||
dynClient = acdClients.projects
|
||||
case "Application":
|
||||
dynClient = acdClients.applications
|
||||
}
|
||||
if !exists {
|
||||
if !dryRun {
|
||||
_, err = dynClient.Create(obj, metav1.CreateOptions{})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
fmt.Printf("%s/%s %s created%s\n", gvk.Group, gvk.Kind, obj.GetName(), dryRunMsg)
|
||||
} else {
|
||||
if !dryRun {
|
||||
obj.SetResourceVersion(resourceVersion)
|
||||
_, err = dynClient.Update(obj, metav1.UpdateOptions{})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
fmt.Printf("%s/%s %s replaced%s\n", gvk.Group, gvk.Kind, obj.GetName(), dryRunMsg)
|
||||
}
|
||||
}
|
||||
|
||||
for _, cluster := range newClusters {
|
||||
_, err := db.CreateCluster(context.Background(), cluster)
|
||||
if err != nil {
|
||||
log.Warn(err)
|
||||
// Delete objects not in backup
|
||||
for key := range pruneObjects {
|
||||
if prune {
|
||||
var dynClient dynamic.ResourceInterface
|
||||
switch key.Kind {
|
||||
case "Secret":
|
||||
dynClient = acdClients.secrets
|
||||
case "AppProject":
|
||||
dynClient = acdClients.projects
|
||||
case "Application":
|
||||
dynClient = acdClients.applications
|
||||
default:
|
||||
log.Fatalf("Unexpected kind '%s' in prune list", key.Kind)
|
||||
}
|
||||
if !dryRun {
|
||||
err = dynClient.Delete(key.Name, &metav1.DeleteOptions{})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
fmt.Printf("%s/%s %s pruned%s\n", key.Group, key.Kind, key.Name, dryRunMsg)
|
||||
} else {
|
||||
fmt.Printf("%s/%s %s needs pruning\n", key.Group, key.Kind, key.Name)
|
||||
}
|
||||
}
|
||||
|
||||
appClientset := appclientset.NewForConfigOrDie(config)
|
||||
for _, app := range newApps {
|
||||
out, err := appClientset.ArgoprojV1alpha1().Applications(namespace).Create(app)
|
||||
errors.CheckError(err)
|
||||
log.Println(out)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(&command)
|
||||
command.Flags().BoolVar(&dryRun, "dry-run", false, "Print what will be performed")
|
||||
command.Flags().BoolVar(&prune, "prune", false, "Prune secrets, applications and projects which do not appear in the backup")
|
||||
|
||||
return &command
|
||||
}
|
||||
|
||||
type argoCDClientsets struct {
|
||||
configMaps dynamic.ResourceInterface
|
||||
secrets dynamic.ResourceInterface
|
||||
applications dynamic.ResourceInterface
|
||||
projects dynamic.ResourceInterface
|
||||
}
|
||||
|
||||
func newArgoCDClientsets(config *rest.Config, namespace string) *argoCDClientsets {
|
||||
dynamicIf, err := dynamic.NewForConfig(config)
|
||||
errors.CheckError(err)
|
||||
return &argoCDClientsets{
|
||||
configMaps: dynamicIf.Resource(configMapResource).Namespace(namespace),
|
||||
secrets: dynamicIf.Resource(secretResource).Namespace(namespace),
|
||||
applications: dynamicIf.Resource(applicationsResource).Namespace(namespace),
|
||||
projects: dynamicIf.Resource(appprojectsResource).Namespace(namespace),
|
||||
}
|
||||
}
|
||||
|
||||
// NewExportCommand defines a new command for exporting Kubernetes and Argo CD resources.
|
||||
func NewExportCommand() *cobra.Command {
|
||||
var (
|
||||
@@ -292,75 +364,54 @@ func NewExportCommand() *cobra.Command {
|
||||
var command = cobra.Command{
|
||||
Use: "export",
|
||||
Short: "Export all Argo CD data to stdout (default) or a file",
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
kubeClientset := kubernetes.NewForConfigOrDie(config)
|
||||
|
||||
settingsMgr := settings.NewSettingsManager(context.Background(), kubeClientset, namespace)
|
||||
settings, err := settingsMgr.GetSettings()
|
||||
errors.CheckError(err)
|
||||
// certificate data is included in secrets that are exported alongside
|
||||
settings.Certificate = nil
|
||||
|
||||
db := db.NewDB(namespace, settingsMgr, kubeClientset)
|
||||
clusters, err := db.ListClusters(context.Background())
|
||||
errors.CheckError(err)
|
||||
|
||||
repoURLs, err := db.ListRepoURLs(context.Background())
|
||||
errors.CheckError(err)
|
||||
repos := make([]*v1alpha1.Repository, len(repoURLs))
|
||||
for i := range repoURLs {
|
||||
repo, err := db.GetRepository(context.Background(), repoURLs[i])
|
||||
errors.CheckError(err)
|
||||
repos = append(repos, repo)
|
||||
}
|
||||
|
||||
appClientset := appclientset.NewForConfigOrDie(config)
|
||||
apps, err := appClientset.ArgoprojV1alpha1().Applications(namespace).List(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
|
||||
rbacCM, err := kubeClientset.CoreV1().ConfigMaps(namespace).Get(common.ArgoCDRBACConfigMapName, metav1.GetOptions{})
|
||||
errors.CheckError(err)
|
||||
|
||||
// remove extraneous cruft from output
|
||||
rbacCM.ObjectMeta = metav1.ObjectMeta{
|
||||
Name: rbacCM.ObjectMeta.Name,
|
||||
}
|
||||
|
||||
// remove extraneous cruft from output
|
||||
for idx, app := range apps.Items {
|
||||
apps.Items[idx].ObjectMeta = metav1.ObjectMeta{
|
||||
Name: app.ObjectMeta.Name,
|
||||
Finalizers: app.ObjectMeta.Finalizers,
|
||||
}
|
||||
apps.Items[idx].Status = v1alpha1.ApplicationStatus{
|
||||
History: app.Status.History,
|
||||
}
|
||||
apps.Items[idx].Operation = nil
|
||||
}
|
||||
|
||||
// take a list of exportable objects, marshal them to YAML,
|
||||
// and return a string joined by a delimiter
|
||||
output := func(delimiter string, oo ...interface{}) string {
|
||||
out := make([]string, 0)
|
||||
for _, o := range oo {
|
||||
data, err := yaml.Marshal(o)
|
||||
errors.CheckError(err)
|
||||
out = append(out, string(data))
|
||||
}
|
||||
return strings.Join(out, delimiter)
|
||||
}(yamlSeparator, settings, clusters.Items, repos, apps.Items, rbacCM)
|
||||
|
||||
var writer io.Writer
|
||||
if out == "-" {
|
||||
fmt.Println(output)
|
||||
writer = os.Stdout
|
||||
} else {
|
||||
err = ioutil.WriteFile(out, []byte(output), 0644)
|
||||
f, err := os.Create(out)
|
||||
errors.CheckError(err)
|
||||
defer util.Close(f)
|
||||
writer = bufio.NewWriter(f)
|
||||
}
|
||||
|
||||
acdClients := newArgoCDClientsets(config, namespace)
|
||||
acdConfigMap, err := acdClients.configMaps.Get(common.ArgoCDConfigMapName, metav1.GetOptions{})
|
||||
errors.CheckError(err)
|
||||
export(writer, *acdConfigMap)
|
||||
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{})
|
||||
errors.CheckError(err)
|
||||
for _, secret := range secrets.Items {
|
||||
if isArgoCDSecret(referencedSecrets, secret) {
|
||||
export(writer, secret)
|
||||
}
|
||||
}
|
||||
projects, err := acdClients.projects.List(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, proj := range projects.Items {
|
||||
export(writer, proj)
|
||||
}
|
||||
applications, err := acdClients.applications.List(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, app := range applications.Items {
|
||||
export(writer, app)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -370,13 +421,93 @@ func NewExportCommand() *cobra.Command {
|
||||
return &command
|
||||
}
|
||||
|
||||
// NewClusterConfig returns a new instance of `argocd-util cluster-kubeconfig` command
|
||||
// getReferencedSecrets examines the argocd-cm config for any referenced repo secrets and returns a
|
||||
// map of all referenced secrets.
|
||||
func getReferencedSecrets(un unstructured.Unstructured) map[string]bool {
|
||||
var cm apiv1.ConfigMap
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, &cm)
|
||||
errors.CheckError(err)
|
||||
referencedSecrets := make(map[string]bool)
|
||||
if reposRAW, ok := cm.Data["repositories"]; ok {
|
||||
repoCreds := make([]settings.RepoCredentials, 0)
|
||||
err := yaml.Unmarshal([]byte(reposRAW), &repoCreds)
|
||||
errors.CheckError(err)
|
||||
for _, cred := range repoCreds {
|
||||
if cred.PasswordSecret != nil {
|
||||
referencedSecrets[cred.PasswordSecret.Name] = true
|
||||
}
|
||||
if cred.SSHPrivateKeySecret != nil {
|
||||
referencedSecrets[cred.SSHPrivateKeySecret.Name] = true
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
return referencedSecrets
|
||||
}
|
||||
|
||||
// isArgoCDSecret returns whether or not the given secret is a part of Argo CD configuration
|
||||
// (e.g. argocd-secret, repo credentials, or cluster credentials)
|
||||
func isArgoCDSecret(repoSecretRefs map[string]bool, un unstructured.Unstructured) bool {
|
||||
secretName := un.GetName()
|
||||
if secretName == common.ArgoCDSecretName {
|
||||
return true
|
||||
}
|
||||
if repoSecretRefs != nil {
|
||||
if _, ok := repoSecretRefs[secretName]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if labels := un.GetLabels(); labels != nil {
|
||||
if _, ok := labels[common.LabelKeySecretType]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if annotations := un.GetAnnotations(); annotations != nil {
|
||||
if annotations[common.AnnotationKeyManagedBy] == common.AnnotationValueManagedByArgoCD {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// export writes the unstructured object and removes extraneous cruft from output before writing
|
||||
func export(w io.Writer, un unstructured.Unstructured) {
|
||||
name := un.GetName()
|
||||
finalizers := un.GetFinalizers()
|
||||
apiVersion := un.GetAPIVersion()
|
||||
kind := un.GetKind()
|
||||
labels := un.GetLabels()
|
||||
annotations := un.GetAnnotations()
|
||||
unstructured.RemoveNestedField(un.Object, "metadata")
|
||||
un.SetName(name)
|
||||
un.SetFinalizers(finalizers)
|
||||
un.SetAPIVersion(apiVersion)
|
||||
un.SetKind(kind)
|
||||
un.SetLabels(labels)
|
||||
un.SetAnnotations(annotations)
|
||||
data, err := yaml.Marshal(un.Object)
|
||||
errors.CheckError(err)
|
||||
_, err = w.Write(data)
|
||||
errors.CheckError(err)
|
||||
_, err = w.Write([]byte(yamlSeparator))
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
// NewClusterConfig returns a new instance of `argocd-util kubeconfig` command
|
||||
func NewClusterConfig() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "cluster-kubeconfig CLUSTER_URL OUTPUT_PATH",
|
||||
Use: "kubeconfig CLUSTER_URL OUTPUT_PATH",
|
||||
Short: "Generates kubeconfig for the specified cluster",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
@@ -387,12 +518,8 @@ func NewClusterConfig() *cobra.Command {
|
||||
output := args[1]
|
||||
conf, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
namespace, wasSpecified, err := clientConfig.Namespace()
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
if !(wasSpecified) {
|
||||
namespace = "argocd"
|
||||
}
|
||||
|
||||
kubeclientset, err := kubernetes.NewForConfig(conf)
|
||||
errors.CheckError(err)
|
||||
|
||||
@@ -406,6 +533,11 @@ func NewClusterConfig() *cobra.Command {
|
||||
return command
|
||||
}
|
||||
|
||||
func redactor(dirtyString string) string {
|
||||
dirtyString = regexp.MustCompile("(clientSecret: )[^ \n]*").ReplaceAllString(dirtyString, "$1********")
|
||||
return regexp.MustCompile("(secret: )[^ \n]*").ReplaceAllString(dirtyString, "$1********")
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := NewCommand().Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
|
||||
73
cmd/argocd-util/main_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var textToRedact = `
|
||||
- config:
|
||||
clientID: aabbccddeeff00112233
|
||||
clientSecret: $dex.github.clientSecret
|
||||
orgs:
|
||||
- name: your-github-org
|
||||
redirectURI: https://argocd.example.com/api/dex/callback
|
||||
id: github
|
||||
name: GitHub
|
||||
type: github
|
||||
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: 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 = `
|
||||
- config:
|
||||
clientID: aabbccddeeff00112233
|
||||
clientSecret: ********
|
||||
orgs:
|
||||
- name: your-github-org
|
||||
redirectURI: https://argocd.example.com/api/dex/callback
|
||||
id: github
|
||||
name: GitHub
|
||||
type: github
|
||||
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
|
||||
web:
|
||||
http: 0.0.0.0:5556`
|
||||
|
||||
func TestSecretsRedactor(t *testing.T) {
|
||||
assert.Equal(t, expectedRedaction, redactor(textToRedact))
|
||||
}
|
||||
@@ -2,16 +2,21 @@ package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"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/util"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
@@ -27,6 +32,7 @@ func NewAccountCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
},
|
||||
}
|
||||
command.AddCommand(NewAccountUpdatePasswordCommand(clientOpts))
|
||||
command.AddCommand(NewAccountGetUserInfoCommand(clientOpts))
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -57,7 +63,7 @@ func NewAccountUpdatePasswordCommand(clientOpts *argocdclient.ClientOptions) *co
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
updatePasswordRequest := account.UpdatePasswordRequest{
|
||||
updatePasswordRequest := accountpkg.UpdatePasswordRequest{
|
||||
NewPassword: newPassword,
|
||||
CurrentPassword: currentPassword,
|
||||
}
|
||||
@@ -93,3 +99,48 @@ func NewAccountUpdatePasswordCommand(clientOpts *argocdclient.ClientOptions) *co
|
||||
command.Flags().StringVar(&newPassword, "new-password", "", "new password you want to update to")
|
||||
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
|
||||
}
|
||||
|
||||
175
cmd/argocd/commands/app_actions.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"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"
|
||||
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{
|
||||
Use: "actions",
|
||||
Short: "Manage Resource actions",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
},
|
||||
}
|
||||
command.AddCommand(NewApplicationResourceActionsListCommand(clientOpts))
|
||||
command.AddCommand(NewApplicationResourceActionsRunCommand(clientOpts))
|
||||
return command
|
||||
}
|
||||
|
||||
// NewApplicationResourceActionsListCommand returns a new instance of an `argocd app actions list` command
|
||||
func NewApplicationResourceActionsListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var namespace string
|
||||
var kind string
|
||||
var group string
|
||||
var resourceName string
|
||||
var output string
|
||||
var command = &cobra.Command{
|
||||
Use: "list APPNAME",
|
||||
Short: "Lists available actions on a resource",
|
||||
}
|
||||
command.Run = func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
appName := args[0]
|
||||
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
|
||||
defer util.Close(conn)
|
||||
ctx := context.Background()
|
||||
resources, err := appIf.ManagedResources(ctx, &applicationpkg.ResourcesQuery{ApplicationName: &appName})
|
||||
errors.CheckError(err)
|
||||
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, &applicationpkg.ApplicationResourceRequest{
|
||||
Name: &appName,
|
||||
Namespace: obj.GetNamespace(),
|
||||
ResourceName: obj.GetName(),
|
||||
Group: gvk.Group,
|
||||
Kind: gvk.Kind,
|
||||
})
|
||||
errors.CheckError(err)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
command.Flags().StringVar(&group, "group", "", "Group")
|
||||
command.Flags().StringVar(&namespace, "namespace", "", "Namespace")
|
||||
command.Flags().StringVarP(&output, "out", "o", "", "Output format. One of: yaml, json")
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
// 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 all bool
|
||||
var command = &cobra.Command{
|
||||
Use: "run APPNAME ACTION",
|
||||
Short: "Runs an available action on resource(s)",
|
||||
}
|
||||
|
||||
command.Flags().StringVar(&resourceName, "resource-name", "", "Name of resource")
|
||||
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) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
appName := args[0]
|
||||
actionName := args[1]
|
||||
|
||||
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
|
||||
defer util.Close(conn)
|
||||
ctx := context.Background()
|
||||
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(), &applicationpkg.ResourceActionRunRequest{
|
||||
Name: &appName,
|
||||
Namespace: obj.GetNamespace(),
|
||||
ResourceName: objResourceName,
|
||||
Group: gvk.Group,
|
||||
Kind: gvk.Kind,
|
||||
Action: actionName,
|
||||
})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
}
|
||||
return command
|
||||
}
|
||||
55
cmd/argocd/commands/app_test.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
func TestParseLabels(t *testing.T) {
|
||||
validLabels := []string{"key=value", "foo=bar", "intuit=inc"}
|
||||
|
||||
result, err := parseLabels(validLabels)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, result, 3)
|
||||
|
||||
invalidLabels := []string{"key=value", "too=many=equals"}
|
||||
_, err = parseLabels(invalidLabels)
|
||||
assert.Error(t, err)
|
||||
|
||||
emptyLabels := []string{}
|
||||
result, err = parseLabels(emptyLabels)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, result, 0)
|
||||
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
290
cmd/argocd/commands/cert.go
Normal file
@@ -0,0 +1,290 @@
|
||||
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)
|
||||
},
|
||||
}
|
||||
|
||||
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 {
|
||||
hostname, certSubType, certData, err := certutil.TokenizeSSHKnownHostsEntry(knownHostsEntry)
|
||||
errors.CheckError(err)
|
||||
_, _, err = certutil.KnownHostsLineToPublicKey(knownHostsEntry)
|
||||
errors.CheckError(err)
|
||||
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
|
||||
)
|
||||
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)
|
||||
printCertTable(certificates.Items, sortOrder)
|
||||
},
|
||||
}
|
||||
|
||||
command.Flags().StringVar(&sortOrder, "sort", "", "set display sort order, valid: '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()
|
||||
}
|
||||
@@ -19,9 +19,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
|
||||
@@ -39,16 +40,18 @@ func NewClusterCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clientc
|
||||
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
|
||||
awsRoleArn string
|
||||
awsClusterName string
|
||||
systemNamespace string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "add",
|
||||
@@ -85,7 +88,7 @@ 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)
|
||||
managerBearerToken, err = clusterauth.InstallClusterManagerRBAC(clientset, systemNamespace)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
|
||||
@@ -94,7 +97,7 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
if inCluster {
|
||||
clst.Server = common.KubernetesInternalAPIServerAddr
|
||||
}
|
||||
clstCreateReq := cluster.ClusterCreateRequest{
|
||||
clstCreateReq := clusterpkg.ClusterCreateRequest{
|
||||
Cluster: clst,
|
||||
Upsert: upsert,
|
||||
}
|
||||
@@ -108,6 +111,7 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
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(&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")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -154,20 +158,8 @@ func NewCluster(name string, conf *rest.Config, managerBearerToken string, awsAu
|
||||
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)
|
||||
@@ -188,7 +180,7 @@ 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 command = &cobra.Command{
|
||||
Use: "get",
|
||||
Use: "get CLUSTER",
|
||||
Short: "Get cluster information",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
@@ -198,7 +190,7 @@ func NewClusterGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
|
||||
defer util.Close(conn)
|
||||
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)
|
||||
errors.CheckError(err)
|
||||
@@ -212,7 +204,7 @@ func NewClusterGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
// NewClusterRemoveCommand returns a new instance of an `argocd cluster list` command
|
||||
func NewClusterRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "rm",
|
||||
Use: "rm CLUSTER",
|
||||
Short: "Remove cluster credentials",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
@@ -227,9 +219,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 +229,65 @@ 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 {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", c.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)
|
||||
if output == "server" {
|
||||
printClusterServers(clusters.Items)
|
||||
} else {
|
||||
printClusterTable(clusters.Items)
|
||||
}
|
||||
_ = w.Flush()
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: 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 CLUSTER",
|
||||
Short: fmt.Sprintf("%s cluster rotate-auth CLUSTER", 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",
|
||||
},
|
||||
})
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -18,16 +20,36 @@ 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",
|
||||
Aliases: []string{"ctx"},
|
||||
Short: "Switch between contexts",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
|
||||
localCfg, err := localconfig.ReadLocalConfig(clientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
|
||||
deletePresentContext := false
|
||||
c.Flags().Visit(func(f *pflag.Flag) {
|
||||
if f.Name == "delete" {
|
||||
deletePresentContext = true
|
||||
}
|
||||
})
|
||||
|
||||
if len(args) == 0 {
|
||||
printArgoCDContexts(clientOpts.ConfigPath)
|
||||
return
|
||||
if deletePresentContext {
|
||||
err := deleteContext(localCfg.CurrentContext, clientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
return
|
||||
} else {
|
||||
printArgoCDContexts(clientOpts.ConfigPath)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ctxName := args[0]
|
||||
|
||||
argoCDDir, err := localconfig.DefaultConfigDir()
|
||||
errors.CheckError(err)
|
||||
prevCtxFile := path.Join(argoCDDir, ".prev-ctx")
|
||||
@@ -37,8 +59,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 +68,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 +76,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 = localCfg.Contexts[0].Name
|
||||
}
|
||||
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)
|
||||
|
||||
60
cmd/argocd/commands/context_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
)
|
||||
|
||||
const testConfig = `contexts:
|
||||
- name: argocd.example.com:443
|
||||
server: argocd.example.com:443
|
||||
user: argocd.example.com:443
|
||||
- name: localhost:8080
|
||||
server: localhost:8080
|
||||
user: localhost:8080
|
||||
current-context: localhost:8080
|
||||
servers:
|
||||
- server: argocd.example.com:443
|
||||
- plain-text: true
|
||||
server: localhost:8080
|
||||
users:
|
||||
- auth-token: vErrYS3c3tReFRe$hToken
|
||||
name: argocd.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"})
|
||||
|
||||
err = deleteContext("localhost:8080", testConfigFilePath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
localConfig, err = localconfig.ReadLocalConfig(testConfigFilePath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, localConfig.CurrentContext, "argocd.example.com:443")
|
||||
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: "argocd.example.com:443", Server: "argocd.example.com:443", User: "argocd.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)
|
||||
|
||||
}
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"strconv"
|
||||
"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 +17,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"
|
||||
@@ -88,11 +88,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{
|
||||
@@ -154,7 +154,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)
|
||||
@@ -233,7 +233,7 @@ func oauth2Login(ctx context.Context, port int, oauth2conf *oauth2.Config, provi
|
||||
<p style="margin-top:20px; font-size:18; text-align:center">Authentication was successful, you can now return to CLI. This page will close automatically</p>
|
||||
<script>window.onload=function(){setTimeout(this.close, 4000)}</script>
|
||||
`
|
||||
fmt.Fprintf(w, successPage)
|
||||
fmt.Fprint(w, successPage)
|
||||
completionChan <- ""
|
||||
}
|
||||
srv := &http.Server{Addr: "localhost:" + strconv.Itoa(port)}
|
||||
@@ -243,12 +243,17 @@ 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)
|
||||
}
|
||||
@@ -278,7 +283,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
|
||||
}
|
||||
39
cmd/argocd/commands/logout_test.go
Normal file
@@ -0,0 +1,39 @@
|
||||
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: "argocd.example.com:443", Server: "argocd.example.com:443", User: "argocd.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)
|
||||
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
@@ -15,21 +17,25 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
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,7 +333,7 @@ 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)
|
||||
},
|
||||
}
|
||||
@@ -299,11 +353,11 @@ 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})
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
@@ -399,7 +453,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 +467,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 +489,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 +497,54 @@ 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)
|
||||
if output == "name" {
|
||||
printProjectNames(projects.Items)
|
||||
} else {
|
||||
printProjectTable(projects.Items)
|
||||
}
|
||||
_ = w.Flush()
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: 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,7 +577,7 @@ 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))
|
||||
}
|
||||
|
||||
// NewProjectGetCommand returns a new instance of an `argocd proj get` command
|
||||
@@ -513,7 +594,7 @@ 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)
|
||||
@@ -557,6 +638,7 @@ func NewProjectGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
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))
|
||||
},
|
||||
}
|
||||
return command
|
||||
@@ -574,7 +656,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 +673,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,16 @@ 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)
|
||||
if output == "name" {
|
||||
printProjectRoleListName(project.Spec.Roles)
|
||||
} else {
|
||||
printProjectRoleListTable(project.Spec.Roles)
|
||||
}
|
||||
_ = w.Flush()
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: wide|name")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -290,10 +311,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 +343,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 +381,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)
|
||||
},
|
||||
|
||||
311
cmd/argocd/commands/projectwindows.go
Normal file
@@ -0,0 +1,311 @@
|
||||
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 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)
|
||||
|
||||
printSyncWindows(proj)
|
||||
},
|
||||
}
|
||||
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"
|
||||
@@ -63,11 +63,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,44 +40,118 @@ 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
|
||||
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 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)
|
||||
}
|
||||
// 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)
|
||||
if err != nil {
|
||||
if git.IsSSHURL(repo.Repo) {
|
||||
// If we failed using git SSH credentials, then the repo is automatically bad
|
||||
log.Fatal(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.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)
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
|
||||
// InsecureIgnoreHostKey is deprecated and only here for backwards compat
|
||||
repo.InsecureIgnoreHostKey = insecureIgnoreHostKey
|
||||
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.
|
||||
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,
|
||||
}
|
||||
@@ -85,9 +160,16 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
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().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-validation 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
|
||||
}
|
||||
@@ -96,7 +178,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)
|
||||
@@ -105,7 +187,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)
|
||||
}
|
||||
},
|
||||
@@ -113,23 +195,49 @@ 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\tUSER\tSTATUS\tMESSAGE\n")
|
||||
for _, r := range repos {
|
||||
var username string
|
||||
if r.Username == "" {
|
||||
username = "-"
|
||||
} else {
|
||||
username = r.Username
|
||||
}
|
||||
_, _ = 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, username, r.ConnectionState.Status, r.ConnectionState.Message)
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
// Print list of repo urls
|
||||
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
|
||||
)
|
||||
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{})
|
||||
repos, err := repoIf.List(context.Background(), &repositorypkg.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)
|
||||
if output == "url" {
|
||||
printRepoUrls(repos.Items)
|
||||
} else {
|
||||
printRepoTable(repos.Items)
|
||||
}
|
||||
_ = w.Flush()
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: wide|url")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ func NewCommand() *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
command.AddCommand(NewCompletionCommand())
|
||||
command.AddCommand(NewVersionCmd(&clientOpts))
|
||||
command.AddCommand(NewClusterCommand(&clientOpts, pathOpts))
|
||||
command.AddCommand(NewApplicationCommand(&clientOpts))
|
||||
@@ -45,6 +46,8 @@ func NewCommand() *cobra.Command {
|
||||
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)
|
||||
@@ -56,5 +59,6 @@ func NewCommand() *cobra.Command {
|
||||
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(&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)")
|
||||
return command
|
||||
}
|
||||
|
||||
18
cmd/argocd/commands/testdata/config
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
contexts:
|
||||
- name: argocd.example.com:443
|
||||
server: argocd.example.com:443
|
||||
user: argocd.example.com:443
|
||||
- name: localhost:8080
|
||||
server: localhost:8080
|
||||
user: localhost:8080
|
||||
current-context: localhost:8080
|
||||
servers:
|
||||
- server: argocd.example.com:443
|
||||
- plain-text: true
|
||||
server: localhost:8080
|
||||
users:
|
||||
- auth-token: vErrYS3c3tReFRe$hToken
|
||||
name: argocd.example.com:443
|
||||
refresh-token: vErrYS3c3tReFRe$hToken
|
||||
- auth-token: vErrYS3c3tReFRe$hToken
|
||||
name: localhost:8080
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"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/util"
|
||||
@@ -22,7 +22,7 @@ func NewVersionCmd(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
Use: "version",
|
||||
Short: fmt.Sprintf("Print version information"),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
version := argocd.GetVersion()
|
||||
version := common.GetVersion()
|
||||
fmt.Printf("%s: %s\n", cliName, version)
|
||||
if !short {
|
||||
fmt.Printf(" BuildDate: %s\n", version.BuildDate)
|
||||
@@ -56,6 +56,9 @@ func NewVersionCmd(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
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)
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
@@ -15,13 +15,37 @@ 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
|
||||
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 base path where application config is located
|
||||
DefaultPathAppConfig = "/app/config"
|
||||
// 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
|
||||
@@ -52,6 +76,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
|
||||
@@ -74,6 +100,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
|
||||
@@ -85,10 +117,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"
|
||||
)
|
||||
@@ -102,14 +130,20 @@ 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"
|
||||
)
|
||||
|
||||
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 = "0.12.0"
|
||||
MinClientVersion = "1.3.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 = "0.12.0"
|
||||
CacheVersion = "1.0.0"
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package argocd
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
@@ -11,8 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"golang.org/x/sync/semaphore"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apierr "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -28,12 +28,13 @@ import (
|
||||
statecache "github.com/argoproj/argo-cd/controller/cache"
|
||||
"github.com/argoproj/argo-cd/controller/metrics"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application"
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
appinformers "github.com/argoproj/argo-cd/pkg/client/informers/externalversions"
|
||||
"github.com/argoproj/argo-cd/pkg/client/informers/externalversions/application/v1alpha1"
|
||||
applisters "github.com/argoproj/argo-cd/pkg/client/listers/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/reposerver"
|
||||
"github.com/argoproj/argo-cd/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/argo"
|
||||
argocache "github.com/argoproj/argo-cd/util/cache"
|
||||
@@ -45,8 +46,25 @@ import (
|
||||
|
||||
const (
|
||||
updateOperationStateTimeout = 1 * time.Second
|
||||
// orphanedIndex contains application which monitor orphaned resources by namespace
|
||||
orphanedIndex = "orphaned"
|
||||
)
|
||||
|
||||
type CompareWith int
|
||||
|
||||
const (
|
||||
// Compare live application state against state defined in latest git revision.
|
||||
CompareWithLatest CompareWith = 2
|
||||
// Compare live application state against state defined using revision of most recent comparison.
|
||||
CompareWithRecent CompareWith = 1
|
||||
// Skip comparison and only refresh application resources tree
|
||||
ComparisonWithNothing CompareWith = 0
|
||||
)
|
||||
|
||||
func (a CompareWith) Max(b CompareWith) CompareWith {
|
||||
return CompareWith(math.Max(float64(a), float64(b)))
|
||||
}
|
||||
|
||||
// ApplicationController is the controller for application resources.
|
||||
type ApplicationController struct {
|
||||
cache *argocache.Cache
|
||||
@@ -63,13 +81,14 @@ type ApplicationController struct {
|
||||
appStateManager AppStateManager
|
||||
stateCache statecache.LiveStateCache
|
||||
statusRefreshTimeout time.Duration
|
||||
repoClientset reposerver.Clientset
|
||||
selfHealTimeout time.Duration
|
||||
repoClientset apiclient.Clientset
|
||||
db db.ArgoDB
|
||||
settings *settings_util.ArgoCDSettings
|
||||
settingsMgr *settings_util.SettingsManager
|
||||
refreshRequestedApps map[string]bool
|
||||
refreshRequestedApps map[string]CompareWith
|
||||
refreshRequestedAppsMutex *sync.Mutex
|
||||
metricsServer *metrics.MetricsServer
|
||||
kubectlSemaphore *semaphore.Weighted
|
||||
}
|
||||
|
||||
type ApplicationControllerConfig struct {
|
||||
@@ -83,106 +102,235 @@ func NewApplicationController(
|
||||
settingsMgr *settings_util.SettingsManager,
|
||||
kubeClientset kubernetes.Interface,
|
||||
applicationClientset appclientset.Interface,
|
||||
repoClientset reposerver.Clientset,
|
||||
repoClientset apiclient.Clientset,
|
||||
argoCache *argocache.Cache,
|
||||
kubectl kube.Kubectl,
|
||||
appResyncPeriod time.Duration,
|
||||
selfHealTimeout time.Duration,
|
||||
metricsPort int,
|
||||
kubectlParallelismLimit int64,
|
||||
) (*ApplicationController, error) {
|
||||
db := db.NewDB(namespace, settingsMgr, kubeClientset)
|
||||
settings, err := settingsMgr.GetSettings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kubectlCmd := kube.KubectlCmd{}
|
||||
ctrl := ApplicationController{
|
||||
cache: argoCache,
|
||||
namespace: namespace,
|
||||
kubeClientset: kubeClientset,
|
||||
kubectl: kubectlCmd,
|
||||
kubectl: kubectl,
|
||||
applicationClientset: applicationClientset,
|
||||
repoClientset: repoClientset,
|
||||
appRefreshQueue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
|
||||
appOperationQueue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
|
||||
db: db,
|
||||
statusRefreshTimeout: appResyncPeriod,
|
||||
refreshRequestedApps: make(map[string]bool),
|
||||
refreshRequestedApps: make(map[string]CompareWith),
|
||||
refreshRequestedAppsMutex: &sync.Mutex{},
|
||||
auditLogger: argo.NewAuditLogger(namespace, kubeClientset, "argocd-application-controller"),
|
||||
settingsMgr: settingsMgr,
|
||||
settings: settings,
|
||||
selfHealTimeout: selfHealTimeout,
|
||||
}
|
||||
appInformer, appLister := ctrl.newApplicationInformerAndLister()
|
||||
projInformer := v1alpha1.NewAppProjectInformer(applicationClientset, namespace, appResyncPeriod, cache.Indexers{})
|
||||
stateCache := statecache.NewLiveStateCache(db, appInformer, ctrl.settings, kubectlCmd, func(appName string) {
|
||||
ctrl.requestAppRefresh(appName)
|
||||
ctrl.appRefreshQueue.Add(fmt.Sprintf("%s/%s", ctrl.namespace, appName))
|
||||
if kubectlParallelismLimit > 0 {
|
||||
ctrl.kubectlSemaphore = semaphore.NewWeighted(kubectlParallelismLimit)
|
||||
}
|
||||
kubectl.SetOnKubectlRun(ctrl.onKubectlRun)
|
||||
appInformer, appLister, err := ctrl.newApplicationInformerAndLister()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
indexers := cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}
|
||||
projInformer := v1alpha1.NewAppProjectInformer(applicationClientset, namespace, appResyncPeriod, indexers)
|
||||
metricsAddr := fmt.Sprintf("0.0.0.0:%d", metricsPort)
|
||||
ctrl.metricsServer = metrics.NewMetricsServer(metricsAddr, appLister, func() error {
|
||||
_, err := kubeClientset.Discovery().ServerVersion()
|
||||
return err
|
||||
})
|
||||
appStateManager := NewAppStateManager(db, applicationClientset, repoClientset, namespace, kubectlCmd, ctrl.settings, stateCache, projInformer)
|
||||
stateCache := statecache.NewLiveStateCache(db, appInformer, ctrl.settingsMgr, kubectl, ctrl.metricsServer, ctrl.handleObjectUpdated)
|
||||
appStateManager := NewAppStateManager(db, applicationClientset, repoClientset, namespace, kubectl, ctrl.settingsMgr, stateCache, projInformer, ctrl.metricsServer)
|
||||
ctrl.appInformer = appInformer
|
||||
ctrl.appLister = appLister
|
||||
ctrl.projInformer = projInformer
|
||||
ctrl.appStateManager = appStateManager
|
||||
ctrl.stateCache = stateCache
|
||||
metricsAddr := fmt.Sprintf("0.0.0.0:%d", common.PortArgoCDMetrics)
|
||||
ctrl.metricsServer = metrics.NewMetricsServer(metricsAddr, ctrl.appLister)
|
||||
|
||||
return &ctrl, nil
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) getApp(name string) (*appv1.Application, error) {
|
||||
obj, exists, err := ctrl.appInformer.GetStore().GetByKey(fmt.Sprintf("%s/%s", ctrl.namespace, name))
|
||||
func (ctrl *ApplicationController) onKubectlRun(command string) (util.Closer, error) {
|
||||
ctrl.metricsServer.IncKubectlExec(command)
|
||||
if ctrl.kubectlSemaphore != nil {
|
||||
if err := ctrl.kubectlSemaphore.Acquire(context.Background(), 1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctrl.metricsServer.IncKubectlExecPending(command)
|
||||
}
|
||||
return util.NewCloser(func() error {
|
||||
if ctrl.kubectlSemaphore != nil {
|
||||
ctrl.kubectlSemaphore.Release(1)
|
||||
ctrl.metricsServer.DecKubectlExecPending(command)
|
||||
}
|
||||
return nil
|
||||
}), nil
|
||||
}
|
||||
|
||||
func isSelfReferencedApp(app *appv1.Application, ref v1.ObjectReference) bool {
|
||||
gvk := ref.GroupVersionKind()
|
||||
return ref.UID == app.UID &&
|
||||
ref.Name == app.Name &&
|
||||
ref.Namespace == app.Namespace &&
|
||||
gvk.Group == application.Group &&
|
||||
gvk.Kind == application.ApplicationKind
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) getAppProj(app *appv1.Application) (*appv1.AppProject, error) {
|
||||
return argo.GetAppProject(&app.Spec, applisters.NewAppProjectLister(ctrl.projInformer.GetIndexer()), ctrl.namespace)
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) handleObjectUpdated(managedByApp map[string]bool, ref v1.ObjectReference) {
|
||||
// if namespaced resource is not managed by any app it might be orphaned resource of some other apps
|
||||
if len(managedByApp) == 0 && ref.Namespace != "" {
|
||||
// retrieve applications which monitor orphaned resources in the same namespace and refresh them unless resource is blacklisted in app project
|
||||
if objs, err := ctrl.appInformer.GetIndexer().ByIndex(orphanedIndex, ref.Namespace); err == nil {
|
||||
for i := range objs {
|
||||
app, ok := objs[i].(*appv1.Application)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// exclude resource unless it is permitted in the app project. If project is not permitted then it is not controlled by the user and there is no point showing the warning.
|
||||
if proj, err := ctrl.getAppProj(app); err == nil && proj.IsResourcePermitted(metav1.GroupKind{Group: ref.GroupVersionKind().Group, Kind: ref.Kind}, true) &&
|
||||
!isKnownOrphanedResourceExclusion(kube.NewResourceKey(ref.GroupVersionKind().Group, ref.GroupVersionKind().Kind, ref.Namespace, ref.Name)) {
|
||||
|
||||
managedByApp[app.Name] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for appName, isManagedResource := range managedByApp {
|
||||
skipForceRefresh := false
|
||||
|
||||
obj, exists, err := ctrl.appInformer.GetIndexer().GetByKey(ctrl.namespace + "/" + appName)
|
||||
if app, ok := obj.(*appv1.Application); exists && err == nil && ok && isSelfReferencedApp(app, ref) {
|
||||
// Don't force refresh app if related resource is application itself. This prevents infinite reconciliation loop.
|
||||
skipForceRefresh = true
|
||||
}
|
||||
|
||||
if !skipForceRefresh {
|
||||
level := ComparisonWithNothing
|
||||
if isManagedResource {
|
||||
level = CompareWithRecent
|
||||
}
|
||||
ctrl.requestAppRefresh(appName, level)
|
||||
}
|
||||
ctrl.appRefreshQueue.Add(fmt.Sprintf("%s/%s", ctrl.namespace, appName))
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) setAppManagedResources(a *appv1.Application, comparisonResult *comparisonResult) (*appv1.ApplicationTree, error) {
|
||||
managedResources, err := ctrl.managedResources(comparisonResult)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
return nil, status.Error(codes.NotFound, fmt.Sprintf("unable to find application with name %s", name))
|
||||
}
|
||||
a, ok := (obj).(*appv1.Application)
|
||||
if !ok {
|
||||
return nil, status.Errorf(codes.Internal, fmt.Sprintf("unexpected object type in app informer"))
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) setAppManagedResources(a *appv1.Application, comparisonResult *comparisonResult) error {
|
||||
tree, err := ctrl.resourceTree(a, comparisonResult.managedResources)
|
||||
tree, err := ctrl.getResourceTree(a, managedResources)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
managedResources, err := ctrl.managedResources(a, comparisonResult)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
err = ctrl.cache.SetAppResourcesTree(a.Name, tree)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
return ctrl.cache.SetAppManagedResources(a.Name, managedResources)
|
||||
return tree, ctrl.cache.SetAppManagedResources(a.Name, managedResources)
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) resourceTree(a *appv1.Application, resources []managedResource) ([]*appv1.ResourceNode, error) {
|
||||
items := make([]*appv1.ResourceNode, 0)
|
||||
for i := range resources {
|
||||
managedResource := resources[i]
|
||||
node := appv1.ResourceNode{
|
||||
Name: managedResource.Name,
|
||||
Version: managedResource.Version,
|
||||
Kind: managedResource.Kind,
|
||||
Group: managedResource.Group,
|
||||
Namespace: managedResource.Namespace,
|
||||
// returns true of given resources exist in the namespace by default and not managed by the user
|
||||
func isKnownOrphanedResourceExclusion(key kube.ResourceKey) bool {
|
||||
if key.Namespace == "default" && key.Group == "" && key.Kind == kube.ServiceKind && key.Name == "kubernetes" {
|
||||
return true
|
||||
}
|
||||
if key.Group == "" && key.Kind == kube.ServiceAccountKind && key.Name == "default" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) getResourceTree(a *appv1.Application, managedResources []*appv1.ResourceDiff) (*appv1.ApplicationTree, error) {
|
||||
nodes := make([]appv1.ResourceNode, 0)
|
||||
|
||||
proj, err := argo.GetAppProject(&a.Spec, applisters.NewAppProjectLister(ctrl.projInformer.GetIndexer()), ctrl.namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orphanedNodesMap := make(map[kube.ResourceKey]appv1.ResourceNode)
|
||||
warnOrphaned := true
|
||||
if proj.Spec.OrphanedResources != nil {
|
||||
orphanedNodesMap, err = ctrl.stateCache.GetNamespaceTopLevelResources(a.Spec.Destination.Server, a.Spec.Destination.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if managedResource.Live != nil {
|
||||
node.ResourceVersion = managedResource.Live.GetResourceVersion()
|
||||
children, err := ctrl.stateCache.GetChildren(a.Spec.Destination.Server, managedResource.Live)
|
||||
warnOrphaned = proj.Spec.OrphanedResources.IsWarn()
|
||||
}
|
||||
|
||||
for i := range managedResources {
|
||||
managedResource := managedResources[i]
|
||||
delete(orphanedNodesMap, kube.NewResourceKey(managedResource.Group, managedResource.Kind, managedResource.Namespace, managedResource.Name))
|
||||
var live = &unstructured.Unstructured{}
|
||||
err := json.Unmarshal([]byte(managedResource.LiveState), &live)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var target = &unstructured.Unstructured{}
|
||||
err = json.Unmarshal([]byte(managedResource.TargetState), &target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if live == nil {
|
||||
nodes = append(nodes, appv1.ResourceNode{
|
||||
ResourceRef: appv1.ResourceRef{
|
||||
Version: target.GroupVersionKind().Version,
|
||||
Name: managedResource.Name,
|
||||
Kind: managedResource.Kind,
|
||||
Group: managedResource.Group,
|
||||
Namespace: managedResource.Namespace,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
err := ctrl.stateCache.IterateHierarchy(a.Spec.Destination.Server, kube.GetResourceKey(live), func(child appv1.ResourceNode, appName string) {
|
||||
nodes = append(nodes, child)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
node.Children = children
|
||||
}
|
||||
items = append(items, &node)
|
||||
}
|
||||
return items, nil
|
||||
orphanedNodes := make([]appv1.ResourceNode, 0)
|
||||
for k := range orphanedNodesMap {
|
||||
if k.Namespace != "" && proj.IsResourcePermitted(metav1.GroupKind{Group: k.Group, Kind: k.Kind}, true) && !isKnownOrphanedResourceExclusion(k) {
|
||||
err := ctrl.stateCache.IterateHierarchy(a.Spec.Destination.Server, k, func(child appv1.ResourceNode, appName string) {
|
||||
belongToAnotherApp := false
|
||||
if appName != "" {
|
||||
if _, exists, err := ctrl.appInformer.GetIndexer().GetByKey(ctrl.namespace + "/" + appName); exists && err == nil {
|
||||
belongToAnotherApp = true
|
||||
}
|
||||
}
|
||||
if !belongToAnotherApp {
|
||||
orphanedNodes = append(orphanedNodes, child)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
var conditions []appv1.ApplicationCondition
|
||||
if len(orphanedNodes) > 0 && warnOrphaned {
|
||||
conditions = []appv1.ApplicationCondition{{
|
||||
Type: appv1.ApplicationConditionOrphanedResourceWarning,
|
||||
Message: fmt.Sprintf("Application has %d orphaned resources", len(orphanedNodes)),
|
||||
}}
|
||||
}
|
||||
a.Status.SetConditions(conditions, map[appv1.ApplicationConditionType]bool{appv1.ApplicationConditionOrphanedResourceWarning: true})
|
||||
return &appv1.ApplicationTree{Nodes: nodes, OrphanedNodes: orphanedNodes}, nil
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) managedResources(a *appv1.Application, comparisonResult *comparisonResult) ([]*appv1.ResourceDiff, error) {
|
||||
func (ctrl *ApplicationController) managedResources(comparisonResult *comparisonResult) ([]*appv1.ResourceDiff, error) {
|
||||
items := make([]*appv1.ResourceDiff, len(comparisonResult.managedResources))
|
||||
for i := range comparisonResult.managedResources {
|
||||
res := comparisonResult.managedResources[i]
|
||||
@@ -191,6 +339,7 @@ func (ctrl *ApplicationController) managedResources(a *appv1.Application, compar
|
||||
Name: res.Name,
|
||||
Group: res.Group,
|
||||
Kind: res.Kind,
|
||||
Hook: res.Hook,
|
||||
}
|
||||
|
||||
target := res.Target
|
||||
@@ -242,14 +391,13 @@ func (ctrl *ApplicationController) Run(ctx context.Context, statusProcessors int
|
||||
|
||||
go ctrl.appInformer.Run(ctx.Done())
|
||||
go ctrl.projInformer.Run(ctx.Done())
|
||||
go ctrl.watchSettings(ctx)
|
||||
|
||||
if !cache.WaitForCacheSync(ctx.Done(), ctrl.appInformer.HasSynced, ctrl.projInformer.HasSynced) {
|
||||
log.Error("Timed out waiting for caches to sync")
|
||||
return
|
||||
}
|
||||
|
||||
go ctrl.stateCache.Run(ctx)
|
||||
go func() { errors.CheckError(ctrl.stateCache.Run(ctx)) }()
|
||||
go func() { errors.CheckError(ctrl.metricsServer.ListenAndServe()) }()
|
||||
|
||||
for i := 0; i < statusProcessors; i++ {
|
||||
@@ -269,20 +417,20 @@ func (ctrl *ApplicationController) Run(ctx context.Context, statusProcessors int
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) requestAppRefresh(appName string) {
|
||||
func (ctrl *ApplicationController) requestAppRefresh(appName string, compareWith CompareWith) {
|
||||
ctrl.refreshRequestedAppsMutex.Lock()
|
||||
defer ctrl.refreshRequestedAppsMutex.Unlock()
|
||||
ctrl.refreshRequestedApps[appName] = true
|
||||
ctrl.refreshRequestedApps[appName] = compareWith.Max(ctrl.refreshRequestedApps[appName])
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) isRefreshRequested(appName string) bool {
|
||||
func (ctrl *ApplicationController) isRefreshRequested(appName string) (bool, CompareWith) {
|
||||
ctrl.refreshRequestedAppsMutex.Lock()
|
||||
defer ctrl.refreshRequestedAppsMutex.Unlock()
|
||||
_, ok := ctrl.refreshRequestedApps[appName]
|
||||
level, ok := ctrl.refreshRequestedApps[appName]
|
||||
if ok {
|
||||
delete(ctrl.refreshRequestedApps, appName)
|
||||
}
|
||||
return ok
|
||||
return ok, level
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) processAppOperationQueueItem() (processNext bool) {
|
||||
@@ -329,6 +477,10 @@ func (ctrl *ApplicationController) processAppOperationQueueItem() (processNext b
|
||||
return
|
||||
}
|
||||
|
||||
func shouldBeDeleted(app *appv1.Application, obj *unstructured.Unstructured) bool {
|
||||
return !kube.IsCRD(obj) && !isSelfReferencedApp(app, kube.GetObjectRef(obj))
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Application) error {
|
||||
logCtx := log.WithField("application", app.Name)
|
||||
logCtx.Infof("Deleting resources")
|
||||
@@ -347,11 +499,20 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic
|
||||
}
|
||||
objs := make([]*unstructured.Unstructured, 0)
|
||||
for k := range objsMap {
|
||||
objs = append(objs, objsMap[k])
|
||||
if objsMap[k].GetDeletionTimestamp() == nil && shouldBeDeleted(app, objsMap[k]) {
|
||||
objs = append(objs, objsMap[k])
|
||||
}
|
||||
}
|
||||
|
||||
cluster, err := ctrl.db.GetCluster(context.Background(), app.Spec.Destination.Server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config := metrics.AddMetricsTransportWrapper(ctrl.metricsServer, app, cluster.RESTConfig())
|
||||
|
||||
err = util.RunAllAsync(len(objs), func(i int) error {
|
||||
obj := objs[i]
|
||||
return ctrl.stateCache.Delete(app.Spec.Destination.Server, obj)
|
||||
return ctrl.kubectl.DeleteResource(config, obj.GroupVersionKind(), obj.GetName(), obj.GetNamespace(), false)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -361,6 +522,11 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for k, obj := range objsMap {
|
||||
if !shouldBeDeleted(app, obj) {
|
||||
delete(objsMap, k)
|
||||
}
|
||||
}
|
||||
if len(objsMap) > 0 {
|
||||
logCtx.Infof("%d objects remaining for deletion", len(objsMap))
|
||||
return nil
|
||||
@@ -454,6 +620,7 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli
|
||||
ctrl.setOperationState(app, state)
|
||||
logCtx.Infof("Initialized new operation: %v", *app.Operation)
|
||||
}
|
||||
|
||||
ctrl.appStateManager.SyncAppState(app, state)
|
||||
|
||||
if state.Phase == appv1.OperationRunning {
|
||||
@@ -475,7 +642,13 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli
|
||||
if state.Phase.Completed() {
|
||||
// if we just completed an operation, force a refresh so that UI will report up-to-date
|
||||
// sync/health information
|
||||
ctrl.requestAppRefresh(app.ObjectMeta.Name)
|
||||
if key, err := cache.MetaNamespaceKeyFunc(app); err == nil {
|
||||
// force app refresh with using CompareWithLatest comparison type and trigger app reconciliation loop
|
||||
ctrl.requestAppRefresh(app.Name, CompareWithLatest)
|
||||
ctrl.appRefreshQueue.Add(key)
|
||||
} else {
|
||||
logCtx.Warnf("Fails to requeue application: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,6 +683,10 @@ func (ctrl *ApplicationController) setOperationState(app *appv1.Application, sta
|
||||
appClient := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(ctrl.namespace)
|
||||
_, err = appClient.Patch(app.Name, types.MergePatchType, patchJSON)
|
||||
if err != nil {
|
||||
// Stop retrying updating deleted application
|
||||
if apierr.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
log.Infof("updated '%s' operation (phase: %s)", app.Name, state.Phase)
|
||||
@@ -566,141 +743,177 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
log.Warnf("Key '%s' in index is not an application", appKey)
|
||||
return
|
||||
}
|
||||
needRefresh, refreshType := ctrl.needRefreshAppStatus(origApp, ctrl.statusRefreshTimeout)
|
||||
needRefresh, refreshType, comparisonLevel := ctrl.needRefreshAppStatus(origApp, ctrl.statusRefreshTimeout)
|
||||
|
||||
if !needRefresh {
|
||||
return
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
reconcileDuration := time.Now().Sub(startTime)
|
||||
reconcileDuration := time.Since(startTime)
|
||||
ctrl.metricsServer.IncReconcile(origApp, reconcileDuration)
|
||||
logCtx := log.WithFields(log.Fields{"application": origApp.Name, "time_ms": reconcileDuration.Seconds() * 1e3})
|
||||
logCtx := log.WithFields(log.Fields{
|
||||
"application": origApp.Name,
|
||||
"time_ms": reconcileDuration.Seconds() * 1e3,
|
||||
"level": comparisonLevel,
|
||||
"dest-server": origApp.Spec.Destination.Server,
|
||||
"dest-namespace": origApp.Spec.Destination.Namespace,
|
||||
})
|
||||
logCtx.Info("Reconciliation completed")
|
||||
}()
|
||||
app := origApp.DeepCopy()
|
||||
|
||||
conditions, hasErrors := ctrl.refreshAppConditions(app)
|
||||
app := origApp.DeepCopy()
|
||||
logCtx := log.WithFields(log.Fields{"application": app.Name})
|
||||
if comparisonLevel == ComparisonWithNothing {
|
||||
managedResources := make([]*appv1.ResourceDiff, 0)
|
||||
if err := ctrl.cache.GetAppManagedResources(app.Name, &managedResources); err != nil {
|
||||
logCtx.Warnf("Failed to get cached managed resources for tree reconciliation, fallback to full reconciliation")
|
||||
} else {
|
||||
if tree, err := ctrl.getResourceTree(app, managedResources); err != nil {
|
||||
app.Status.Conditions = []appv1.ApplicationCondition{{Type: appv1.ApplicationConditionComparisonError, Message: err.Error()}}
|
||||
} else {
|
||||
app.Status.Summary = tree.GetSummary()
|
||||
if err = ctrl.cache.SetAppResourcesTree(app.Name, tree); err != nil {
|
||||
logCtx.Errorf("Failed to cache resources tree: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
now := metav1.Now()
|
||||
app.Status.ObservedAt = &now
|
||||
ctrl.persistAppStatus(origApp, &app.Status)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
hasErrors := ctrl.refreshAppConditions(app)
|
||||
if hasErrors {
|
||||
app.Status.Sync.Status = appv1.SyncStatusCodeUnknown
|
||||
app.Status.Health.Status = appv1.HealthStatusUnknown
|
||||
app.Status.Conditions = conditions
|
||||
ctrl.persistAppStatus(origApp, &app.Status)
|
||||
return
|
||||
}
|
||||
|
||||
compareResult, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, refreshType == appv1.RefreshTypeHard)
|
||||
var localManifests []string
|
||||
if opState := app.Status.OperationState; opState != nil && opState.Operation.Sync != nil {
|
||||
localManifests = opState.Operation.Sync.Manifests
|
||||
}
|
||||
|
||||
revision := app.Spec.Source.TargetRevision
|
||||
if comparisonLevel == CompareWithRecent {
|
||||
revision = app.Status.Sync.Revision
|
||||
}
|
||||
|
||||
observedAt := metav1.Now()
|
||||
compareResult := ctrl.appStateManager.CompareAppState(app, revision, app.Spec.Source, refreshType == appv1.RefreshTypeHard, localManifests)
|
||||
|
||||
ctrl.normalizeApplication(origApp, app)
|
||||
|
||||
tree, err := ctrl.setAppManagedResources(app, compareResult)
|
||||
if err != nil {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{Type: appv1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
logCtx.Errorf("Failed to cache app resources: %v", err)
|
||||
} else {
|
||||
ctrl.normalizeApplication(origApp, app, compareResult.appSourceType)
|
||||
conditions = append(conditions, compareResult.conditions...)
|
||||
app.Status.Summary = tree.GetSummary()
|
||||
}
|
||||
err = ctrl.setAppManagedResources(app, compareResult)
|
||||
|
||||
project, err := ctrl.getAppProj(app)
|
||||
if err != nil {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{Type: appv1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
logCtx.Infof("Could not lookup project for %s in order to check schedules state", app.Name)
|
||||
} else {
|
||||
if project.Spec.SyncWindows.Matches(app).CanSync(false) {
|
||||
syncErrCond := ctrl.autoSync(app, compareResult.syncStatus, compareResult.resources)
|
||||
if syncErrCond != nil {
|
||||
app.Status.SetConditions([]appv1.ApplicationCondition{*syncErrCond}, map[appv1.ApplicationConditionType]bool{appv1.ApplicationConditionSyncError: true})
|
||||
} else {
|
||||
app.Status.SetConditions([]appv1.ApplicationCondition{}, map[appv1.ApplicationConditionType]bool{appv1.ApplicationConditionSyncError: true})
|
||||
}
|
||||
} else {
|
||||
logCtx.Infof("Sync prevented by sync window")
|
||||
}
|
||||
}
|
||||
|
||||
syncErrCond := ctrl.autoSync(app, compareResult.syncStatus)
|
||||
if syncErrCond != nil {
|
||||
conditions = append(conditions, *syncErrCond)
|
||||
if app.Status.ReconciledAt == nil || comparisonLevel == CompareWithLatest {
|
||||
app.Status.ReconciledAt = &observedAt
|
||||
}
|
||||
|
||||
app.Status.ObservedAt = compareResult.observedAt
|
||||
app.Status.ObservedAt = &observedAt
|
||||
app.Status.Sync = *compareResult.syncStatus
|
||||
app.Status.Health = *compareResult.healthStatus
|
||||
app.Status.Resources = compareResult.resources
|
||||
app.Status.Conditions = conditions
|
||||
app.Status.SourceType = compareResult.appSourceType
|
||||
ctrl.persistAppStatus(origApp, &app.Status)
|
||||
return
|
||||
}
|
||||
|
||||
// needRefreshAppStatus answers if application status needs to be refreshed.
|
||||
// Returns true if application never been compared, has changed or comparison result has expired.
|
||||
func (ctrl *ApplicationController) needRefreshAppStatus(app *appv1.Application, statusRefreshTimeout time.Duration) (bool, appv1.RefreshType) {
|
||||
// Additionally returns whether full refresh was requested or not.
|
||||
// If full refresh is requested then target and live state should be reconciled, else only live state tree should be updated.
|
||||
func (ctrl *ApplicationController) needRefreshAppStatus(app *appv1.Application, statusRefreshTimeout time.Duration) (bool, appv1.RefreshType, CompareWith) {
|
||||
logCtx := log.WithFields(log.Fields{"application": app.Name})
|
||||
var reason string
|
||||
compareWith := CompareWithLatest
|
||||
refreshType := appv1.RefreshTypeNormal
|
||||
expired := app.Status.ObservedAt.Add(statusRefreshTimeout).Before(time.Now().UTC())
|
||||
expired := app.Status.ReconciledAt == nil || app.Status.ReconciledAt.Add(statusRefreshTimeout).Before(time.Now().UTC())
|
||||
|
||||
if requestedType, ok := app.IsRefreshRequested(); ok {
|
||||
// user requested app refresh.
|
||||
refreshType = requestedType
|
||||
reason = fmt.Sprintf("%s refresh requested", refreshType)
|
||||
} else if ctrl.isRefreshRequested(app.Name) {
|
||||
reason = fmt.Sprintf("controller refresh requested")
|
||||
} else if app.Status.Sync.Status == appv1.SyncStatusCodeUnknown && expired {
|
||||
reason = "comparison status unknown"
|
||||
} else if expired {
|
||||
reason = fmt.Sprintf("comparison expired. reconciledAt: %v, expiry: %v", app.Status.ReconciledAt, statusRefreshTimeout)
|
||||
} else if !app.Spec.Source.Equals(app.Status.Sync.ComparedTo.Source) {
|
||||
reason = "spec.source differs"
|
||||
} else if !app.Spec.Destination.Equals(app.Status.Sync.ComparedTo.Destination) {
|
||||
reason = "spec.destination differs"
|
||||
} else if expired {
|
||||
reason = fmt.Sprintf("comparison expired. observedAt: %v, expiry: %v", app.Status.ObservedAt, statusRefreshTimeout)
|
||||
} else if requested, level := ctrl.isRefreshRequested(app.Name); requested {
|
||||
compareWith = level
|
||||
reason = fmt.Sprintf("controller refresh requested")
|
||||
}
|
||||
|
||||
if reason != "" {
|
||||
logCtx.Infof("Refreshing app status (%s)", reason)
|
||||
return true, refreshType
|
||||
logCtx.Infof("Refreshing app status (%s), level (%d)", reason, compareWith)
|
||||
return true, refreshType, compareWith
|
||||
}
|
||||
return false, refreshType
|
||||
return false, refreshType, compareWith
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) refreshAppConditions(app *appv1.Application) ([]appv1.ApplicationCondition, bool) {
|
||||
conditions := make([]appv1.ApplicationCondition, 0)
|
||||
proj, err := argo.GetAppProject(&app.Spec, applisters.NewAppProjectLister(ctrl.projInformer.GetIndexer()), ctrl.namespace)
|
||||
func (ctrl *ApplicationController) refreshAppConditions(app *appv1.Application) bool {
|
||||
errorConditions := make([]appv1.ApplicationCondition, 0)
|
||||
proj, err := ctrl.getAppProj(app)
|
||||
if err != nil {
|
||||
if apierr.IsNotFound(err) {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{
|
||||
errorConditions = append(errorConditions, appv1.ApplicationCondition{
|
||||
Type: appv1.ApplicationConditionInvalidSpecError,
|
||||
Message: fmt.Sprintf("Application referencing project %s which does not exist", app.Spec.Project),
|
||||
})
|
||||
} else {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{
|
||||
errorConditions = append(errorConditions, appv1.ApplicationCondition{
|
||||
Type: appv1.ApplicationConditionUnknownError,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
specConditions, err := argo.GetSpecErrors(context.Background(), &app.Spec, proj, ctrl.repoClientset, ctrl.db)
|
||||
specConditions, err := argo.ValidatePermissions(context.Background(), &app.Spec, proj, ctrl.db)
|
||||
if err != nil {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{
|
||||
errorConditions = append(errorConditions, appv1.ApplicationCondition{
|
||||
Type: appv1.ApplicationConditionUnknownError,
|
||||
Message: err.Error(),
|
||||
})
|
||||
} else {
|
||||
conditions = append(conditions, specConditions...)
|
||||
errorConditions = append(errorConditions, specConditions...)
|
||||
}
|
||||
}
|
||||
|
||||
// List of condition types which have to be reevaluated by controller; all remaining conditions should stay as is.
|
||||
reevaluateTypes := map[appv1.ApplicationConditionType]bool{
|
||||
appv1.ApplicationConditionInvalidSpecError: true,
|
||||
appv1.ApplicationConditionUnknownError: true,
|
||||
appv1.ApplicationConditionComparisonError: true,
|
||||
appv1.ApplicationConditionSharedResourceWarning: true,
|
||||
appv1.ApplicationConditionSyncError: true,
|
||||
}
|
||||
appConditions := make([]appv1.ApplicationCondition, 0)
|
||||
for i := 0; i < len(app.Status.Conditions); i++ {
|
||||
condition := app.Status.Conditions[i]
|
||||
if _, ok := reevaluateTypes[condition.Type]; !ok {
|
||||
appConditions = append(appConditions, condition)
|
||||
}
|
||||
}
|
||||
hasErrors := false
|
||||
for i := range conditions {
|
||||
condition := conditions[i]
|
||||
appConditions = append(appConditions, condition)
|
||||
if condition.IsError() {
|
||||
hasErrors = true
|
||||
}
|
||||
|
||||
}
|
||||
return appConditions, hasErrors
|
||||
app.Status.SetConditions(errorConditions, map[appv1.ApplicationConditionType]bool{
|
||||
appv1.ApplicationConditionInvalidSpecError: true,
|
||||
appv1.ApplicationConditionUnknownError: true,
|
||||
})
|
||||
return len(errorConditions) > 0
|
||||
}
|
||||
|
||||
// normalizeApplication normalizes an application.spec and additionally persists updates if it changed
|
||||
func (ctrl *ApplicationController) normalizeApplication(orig, app *appv1.Application, sourceType appv1.ApplicationSourceType) {
|
||||
func (ctrl *ApplicationController) normalizeApplication(orig, app *appv1.Application) {
|
||||
logCtx := log.WithFields(log.Fields{"application": app.Name})
|
||||
app.Spec = *argo.NormalizeApplicationSpec(&app.Spec, sourceType)
|
||||
app.Spec = *argo.NormalizeApplicationSpec(&app.Spec)
|
||||
patch, modified, err := diff.CreateTwoWayMergePatch(orig, app, appv1.Application{})
|
||||
if err != nil {
|
||||
logCtx.Errorf("error constructing app spec patch: %v", err)
|
||||
@@ -756,7 +969,7 @@ func (ctrl *ApplicationController) persistAppStatus(orig *appv1.Application, new
|
||||
}
|
||||
|
||||
// autoSync will initiate a sync operation for an application configured with automated sync
|
||||
func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus *appv1.SyncStatus) *appv1.ApplicationCondition {
|
||||
func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus *appv1.SyncStatus, resources []appv1.ResourceStatus) *appv1.ApplicationCondition {
|
||||
if app.Spec.SyncPolicy == nil || app.Spec.SyncPolicy.Automated == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -769,34 +982,59 @@ func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus *
|
||||
logCtx.Infof("Skipping auto-sync: deletion in progress")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Only perform auto-sync if we detect OutOfSync status. This is to prevent us from attempting
|
||||
// a sync when application is already in a Synced or Unknown state
|
||||
if syncStatus.Status != appv1.SyncStatusCodeOutOfSync {
|
||||
logCtx.Infof("Skipping auto-sync: application status is %s", syncStatus.Status)
|
||||
return nil
|
||||
}
|
||||
|
||||
desiredCommitSHA := syncStatus.Revision
|
||||
|
||||
// It is possible for manifests to remain OutOfSync even after a sync/kubectl apply (e.g.
|
||||
// auto-sync with pruning disabled). We need to ensure that we do not keep Syncing an
|
||||
// application in an infinite loop. To detect this, we only attempt the Sync if the revision
|
||||
// and parameter overrides are different from our most recent sync operation.
|
||||
if alreadyAttemptedSync(app, desiredCommitSHA) {
|
||||
if app.Status.OperationState.Phase != appv1.OperationSucceeded {
|
||||
logCtx.Warnf("Skipping auto-sync: failed previous sync attempt to %s", desiredCommitSHA)
|
||||
message := fmt.Sprintf("Failed sync attempt to %s: %s", desiredCommitSHA, app.Status.OperationState.Message)
|
||||
return &appv1.ApplicationCondition{Type: appv1.ApplicationConditionSyncError, Message: message}
|
||||
}
|
||||
logCtx.Infof("Skipping auto-sync: most recent sync already to %s", desiredCommitSHA)
|
||||
return nil
|
||||
}
|
||||
|
||||
alreadyAttempted, attemptPhase := alreadyAttemptedSync(app, desiredCommitSHA)
|
||||
selfHeal := app.Spec.SyncPolicy.Automated.SelfHeal
|
||||
op := appv1.Operation{
|
||||
Sync: &appv1.SyncOperation{
|
||||
Revision: desiredCommitSHA,
|
||||
Prune: app.Spec.SyncPolicy.Automated.Prune,
|
||||
},
|
||||
}
|
||||
// It is possible for manifests to remain OutOfSync even after a sync/kubectl apply (e.g.
|
||||
// auto-sync with pruning disabled). We need to ensure that we do not keep Syncing an
|
||||
// application in an infinite loop. To detect this, we only attempt the Sync if the revision
|
||||
// and parameter overrides are different from our most recent sync operation.
|
||||
if alreadyAttempted && (!selfHeal || !attemptPhase.Successful()) {
|
||||
if !attemptPhase.Successful() {
|
||||
logCtx.Warnf("Skipping auto-sync: failed previous sync attempt to %s", desiredCommitSHA)
|
||||
message := fmt.Sprintf("Failed sync attempt to %s: %s", desiredCommitSHA, app.Status.OperationState.Message)
|
||||
return &appv1.ApplicationCondition{Type: appv1.ApplicationConditionSyncError, Message: message}
|
||||
}
|
||||
logCtx.Infof("Skipping auto-sync: most recent sync already to %s", desiredCommitSHA)
|
||||
return nil
|
||||
} else if alreadyAttempted && selfHeal {
|
||||
if shouldSelfHeal, retryAfter := ctrl.shouldSelfHeal(app); shouldSelfHeal {
|
||||
for _, resource := range resources {
|
||||
if resource.Status != appv1.SyncStatusCodeSynced {
|
||||
op.Sync.Resources = append(op.Sync.Resources, appv1.SyncOperationResource{
|
||||
Kind: resource.Kind,
|
||||
Group: resource.Group,
|
||||
Name: resource.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logCtx.Infof("Skipping auto-sync: already attempted sync to %s with timeout %v (retrying in %v)", desiredCommitSHA, ctrl.selfHealTimeout, retryAfter)
|
||||
if key, err := cache.MetaNamespaceKeyFunc(app); err == nil {
|
||||
ctrl.requestAppRefresh(app.Name, CompareWithLatest)
|
||||
ctrl.appRefreshQueue.AddAfter(key, retryAfter)
|
||||
} else {
|
||||
logCtx.Warnf("Fails to requeue application: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
appIf := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace)
|
||||
_, err := argo.SetAppOperation(appIf, app.Name, &op)
|
||||
if err != nil {
|
||||
@@ -811,12 +1049,12 @@ func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus *
|
||||
|
||||
// alreadyAttemptedSync returns whether or not the most recent sync was performed against the
|
||||
// commitSHA and with the same app source config which are currently set in the app
|
||||
func alreadyAttemptedSync(app *appv1.Application, commitSHA string) bool {
|
||||
func alreadyAttemptedSync(app *appv1.Application, commitSHA string) (bool, appv1.OperationPhase) {
|
||||
if app.Status.OperationState == nil || app.Status.OperationState.Operation.Sync == nil || app.Status.OperationState.SyncResult == nil {
|
||||
return false
|
||||
return false, ""
|
||||
}
|
||||
if app.Status.OperationState.SyncResult.Revision != commitSHA {
|
||||
return false
|
||||
return false, ""
|
||||
}
|
||||
// Ignore differences in target revision, since we already just verified commitSHAs are equal,
|
||||
// and we do not want to trigger auto-sync due to things like HEAD != master
|
||||
@@ -824,13 +1062,24 @@ func alreadyAttemptedSync(app *appv1.Application, commitSHA string) bool {
|
||||
specSource.TargetRevision = ""
|
||||
syncResSource := app.Status.OperationState.SyncResult.Source.DeepCopy()
|
||||
syncResSource.TargetRevision = ""
|
||||
if !reflect.DeepEqual(app.Spec.Source, app.Status.OperationState.SyncResult.Source) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return reflect.DeepEqual(app.Spec.Source, app.Status.OperationState.SyncResult.Source), app.Status.OperationState.Phase
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) newApplicationInformerAndLister() (cache.SharedIndexInformer, applisters.ApplicationLister) {
|
||||
func (ctrl *ApplicationController) shouldSelfHeal(app *appv1.Application) (bool, time.Duration) {
|
||||
if app.Status.OperationState == nil {
|
||||
return true, time.Duration(0)
|
||||
}
|
||||
|
||||
var retryAfter time.Duration
|
||||
if app.Status.OperationState.FinishedAt == nil {
|
||||
retryAfter = ctrl.selfHealTimeout
|
||||
} else {
|
||||
retryAfter = ctrl.selfHealTimeout - time.Since(app.Status.OperationState.FinishedAt.Time)
|
||||
}
|
||||
return retryAfter <= 0, retryAfter
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) newApplicationInformerAndLister() (cache.SharedIndexInformer, applisters.ApplicationLister, error) {
|
||||
appInformerFactory := appinformers.NewFilteredSharedInformerFactory(
|
||||
ctrl.applicationClientset,
|
||||
ctrl.statusRefreshTimeout,
|
||||
@@ -858,7 +1107,7 @@ func (ctrl *ApplicationController) newApplicationInformerAndLister() (cache.Shar
|
||||
if oldOK && newOK {
|
||||
if toggledAutomatedSync(oldApp, newApp) {
|
||||
log.WithField("application", newApp.Name).Info("Enabled automated sync")
|
||||
ctrl.requestAppRefresh(newApp.Name)
|
||||
ctrl.requestAppRefresh(newApp.Name, CompareWithLatest)
|
||||
}
|
||||
}
|
||||
ctrl.appRefreshQueue.Add(key)
|
||||
@@ -874,7 +1123,24 @@ func (ctrl *ApplicationController) newApplicationInformerAndLister() (cache.Shar
|
||||
},
|
||||
},
|
||||
)
|
||||
return informer, lister
|
||||
err := informer.AddIndexers(cache.Indexers{
|
||||
orphanedIndex: func(obj interface{}) (i []string, e error) {
|
||||
app, ok := obj.(*appv1.Application)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
proj, err := ctrl.getAppProj(app)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
if proj.Spec.OrphanedResources != nil {
|
||||
return []string{app.Spec.Destination.Namespace}, nil
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
})
|
||||
return informer, lister, err
|
||||
}
|
||||
|
||||
func isOperationInProgress(app *appv1.Application) bool {
|
||||
@@ -894,33 +1160,3 @@ func toggledAutomatedSync(old *appv1.Application, new *appv1.Application) bool {
|
||||
// nothing changed
|
||||
return false
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) watchSettings(ctx context.Context) {
|
||||
updateCh := make(chan *settings_util.ArgoCDSettings, 1)
|
||||
ctrl.settingsMgr.Subscribe(updateCh)
|
||||
prevAppLabelKey := ctrl.settings.GetAppInstanceLabelKey()
|
||||
prevResourceExclusions := ctrl.settings.ResourceExclusions
|
||||
done := false
|
||||
for !done {
|
||||
select {
|
||||
case newSettings := <-updateCh:
|
||||
newAppLabelKey := newSettings.GetAppInstanceLabelKey()
|
||||
*ctrl.settings = *newSettings
|
||||
if prevAppLabelKey != newAppLabelKey {
|
||||
log.Infof("label key changed: %s -> %s", prevAppLabelKey, newAppLabelKey)
|
||||
ctrl.stateCache.Invalidate()
|
||||
prevAppLabelKey = newAppLabelKey
|
||||
}
|
||||
if !reflect.DeepEqual(prevResourceExclusions, newSettings.ResourceExclusions) {
|
||||
log.Infof("resource exclusions modified")
|
||||
ctrl.stateCache.Invalidate()
|
||||
prevResourceExclusions = newSettings.ResourceExclusions
|
||||
}
|
||||
case <-ctx.Done():
|
||||
done = true
|
||||
}
|
||||
}
|
||||
log.Info("shutting down settings watch")
|
||||
ctrl.settingsMgr.Unsubscribe(updateCh)
|
||||
close(updateCh)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -9,29 +10,40 @@ 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"
|
||||
"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,11 +73,15 @@ 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,
|
||||
@@ -73,7 +89,11 @@ func newFakeController(data *fakeData) *ApplicationController {
|
||||
appclientset.NewSimpleClientset(data.apps...),
|
||||
&mockRepoClientset,
|
||||
utilcache.NewCache(utilcache.NewInMemoryCache(1*time.Hour)),
|
||||
kubectl,
|
||||
time.Minute,
|
||||
time.Minute,
|
||||
common.DefaultPortArgoCDMetrics,
|
||||
0,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -82,14 +102,25 @@ 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)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -105,12 +136,12 @@ data:
|
||||
# minikube
|
||||
name: aHR0cHM6Ly9sb2NhbGhvc3Q6NjQ0Mw==
|
||||
# https://localhost:6443
|
||||
server: aHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3Zj
|
||||
server: aHR0cHM6Ly9sb2NhbGhvc3Q6NjQ0Mw==
|
||||
kind: Secret
|
||||
metadata:
|
||||
labels:
|
||||
argocd.argoproj.io/secret-type: cluster
|
||||
name: localhost-6443
|
||||
name: some-secret
|
||||
namespace: ` + test.FakeArgoCDNamespace + `
|
||||
type: Opaque
|
||||
`
|
||||
@@ -119,6 +150,7 @@ var fakeApp = `
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
uid: "123"
|
||||
name: my-app
|
||||
namespace: ` + test.FakeArgoCDNamespace + `
|
||||
spec:
|
||||
@@ -171,7 +203,7 @@ func TestAutoSync(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)
|
||||
@@ -190,7 +222,7 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
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)
|
||||
@@ -205,7 +237,7 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
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)
|
||||
@@ -221,7 +253,7 @@ 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)
|
||||
@@ -238,7 +270,7 @@ 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)
|
||||
@@ -264,7 +296,7 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus)
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{})
|
||||
assert.NotNil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
@@ -300,7 +332,7 @@ func TestAutoSyncIndicateError(t *testing.T) {
|
||||
Source: *app.Spec.Source.DeepCopy(),
|
||||
},
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus)
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{})
|
||||
assert.NotNil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
@@ -343,7 +375,7 @@ func TestAutoSyncParameterOverrides(t *testing.T) {
|
||||
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)
|
||||
@@ -353,20 +385,26 @@ func TestAutoSyncParameterOverrides(t *testing.T) {
|
||||
// TestFinalizeAppDeletion verifies application deletion
|
||||
func TestFinalizeAppDeletion(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
|
||||
appObj := kube.MustToUnstructured(&app)
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}, managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
||||
kube.GetResourceKey(appObj): appObj,
|
||||
}})
|
||||
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
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)
|
||||
// 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
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, patched)
|
||||
}
|
||||
|
||||
// TestNormalizeApplication verifies we normalize an application during reconciliation
|
||||
@@ -391,7 +429,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 +480,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)
|
||||
ctrl.requestAppRefresh(app.Name, ComparisonWithNothing)
|
||||
|
||||
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)
|
||||
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)
|
||||
// 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)
|
||||
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)
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
318
controller/cache/cache.go
vendored
@@ -2,15 +2,17 @@ package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
v1 "k8s.io/api/core/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"
|
||||
@@ -18,20 +20,28 @@ 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, gvk schema.GroupVersionKind) (bool, error)
|
||||
// Returns child nodes for a given k8s resource
|
||||
GetChildren(server string, obj *unstructured.Unstructured) ([]appv1.ResourceNode, error)
|
||||
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, 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()
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -43,41 +53,54 @@ 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)) 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.Mutex{},
|
||||
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)
|
||||
kubectl kube.Kubectl
|
||||
settings *settings.ArgoCDSettings
|
||||
db db.ArgoDB
|
||||
clusters map[string]*clusterInfo
|
||||
lock *sync.Mutex
|
||||
appInformer cache.SharedIndexInformer
|
||||
onObjectUpdated ObjectUpdatedHandler
|
||||
kubectl kube.Kubectl
|
||||
settingsMgr *settings.SettingsManager
|
||||
metricsServer *metrics.MetricsServer
|
||||
cacheSettingsLock *sync.Mutex
|
||||
cacheSettings *cacheSettings
|
||||
}
|
||||
|
||||
func (c *liveStateCache) processEvent(event watch.EventType, obj *unstructured.Unstructured, url string) error {
|
||||
info, err := c.getSyncedCluster(url)
|
||||
func (c *liveStateCache) loadCacheSettings() (*cacheSettings, error) {
|
||||
appInstanceLabelKey, err := c.settingsMgr.GetAppInstanceLabelKey()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
return info.processEvent(event, obj)
|
||||
}
|
||||
|
||||
func (c *liveStateCache) removeCluster(server string) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
delete(c.clusters, server)
|
||||
log.Infof("Dropped cluster %s cache", server)
|
||||
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) {
|
||||
@@ -90,17 +113,17 @@ func (c *liveStateCache) getCluster(server string) (*clusterInfo, error) {
|
||||
return nil, err
|
||||
}
|
||||
info = &clusterInfo{
|
||||
apis: make(map[schema.GroupKind]*gkInfo),
|
||||
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,
|
||||
apisMeta: make(map[schema.GroupKind]*apiMeta),
|
||||
lock: &sync.Mutex{},
|
||||
nodes: make(map[kube.ResourceKey]*node),
|
||||
nsIndex: make(map[string]map[kube.ResourceKey]*node),
|
||||
onObjectUpdated: c.onObjectUpdated,
|
||||
kubectl: c.kubectl,
|
||||
cluster: cluster,
|
||||
syncTime: nil,
|
||||
syncLock: &sync.Mutex{},
|
||||
log: log.WithField("server", cluster.Server),
|
||||
cacheSettingsSrc: c.getCacheSettings,
|
||||
}
|
||||
|
||||
c.clusters[cluster.Server] = info
|
||||
@@ -132,28 +155,29 @@ func (c *liveStateCache) Invalidate() {
|
||||
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, gvk schema.GroupVersionKind) (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(gvk.GroupKind()), nil
|
||||
return clusterInfo.isNamespaced(gk), nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) GetChildren(server string, obj *unstructured.Unstructured) ([]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
|
||||
}
|
||||
clusterInfo.iterateHierarchy(key, action)
|
||||
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.getChildren(obj), nil
|
||||
return clusterInfo.getNamespaceTopLevelResources(namespace), nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) GetManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
|
||||
@@ -161,7 +185,7 @@ func (c *liveStateCache) GetManagedLiveObjs(a *appv1.Application, targetObjs []*
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return clusterInfo.getManagedLiveObjs(a, targetObjs)
|
||||
return clusterInfo.getManagedLiveObjs(a, targetObjs, c.metricsServer)
|
||||
}
|
||||
|
||||
func isClusterHasApps(apps []interface{}, cluster *appv1.Cluster) bool {
|
||||
@@ -173,137 +197,79 @@ func isClusterHasApps(apps []interface{}, cluster *appv1.Cluster) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *liveStateCache) getCacheSettings() *cacheSettings {
|
||||
c.cacheSettingsLock.Lock()
|
||||
defer c.cacheSettingsLock.Unlock()
|
||||
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) {
|
||||
watchingClustersLock := sync.Mutex{}
|
||||
watchingClusters := make(map[string]struct {
|
||||
cancel context.CancelFunc
|
||||
cluster *appv1.Cluster
|
||||
})
|
||||
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) {
|
||||
info, ok := watchingClusters[event.Cluster.Server]
|
||||
hasApps := isClusterHasApps(c.appInformer.GetStore().List(), event.Cluster)
|
||||
|
||||
// cluster resources must be watched only if cluster has at least one app
|
||||
if (event.Type == watch.Deleted || !hasApps) && ok {
|
||||
info.cancel()
|
||||
watchingClustersLock.Lock()
|
||||
delete(watchingClusters, event.Cluster.Server)
|
||||
watchingClustersLock.Unlock()
|
||||
} else if event.Type != watch.Deleted && !ok && hasApps {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
watchingClustersLock.Lock()
|
||||
watchingClusters[event.Cluster.Server] = struct {
|
||||
cancel context.CancelFunc
|
||||
cluster *appv1.Cluster
|
||||
}{
|
||||
cancel: func() {
|
||||
c.removeCluster(event.Cluster.Server)
|
||||
cancel()
|
||||
},
|
||||
cluster: event.Cluster,
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if cluster, ok := c.clusters[event.Cluster.Server]; ok {
|
||||
if event.Type == watch.Deleted {
|
||||
cluster.invalidate()
|
||||
delete(c.clusters, event.Cluster.Server)
|
||||
} else if event.Type == watch.Modified {
|
||||
cluster.cluster = event.Cluster
|
||||
cluster.invalidate()
|
||||
}
|
||||
watchingClustersLock.Unlock()
|
||||
go c.watchClusterResources(ctx, *event.Cluster)
|
||||
} 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)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
onAppModified := func(obj interface{}) {
|
||||
if app, ok := obj.(*appv1.Application); ok {
|
||||
var cluster *appv1.Cluster
|
||||
info, infoOk := watchingClusters[app.Spec.Destination.Server]
|
||||
if infoOk {
|
||||
cluster = info.cluster
|
||||
} else {
|
||||
cluster, _ = c.db.GetCluster(ctx, app.Spec.Destination.Server)
|
||||
}
|
||||
if cluster != nil {
|
||||
// trigger cluster event every time when app created/deleted to either start or stop watching resources
|
||||
clusterEventCallback(&db.ClusterEvent{Cluster: cluster, Type: watch.Modified})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.appInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: onAppModified,
|
||||
UpdateFunc: func(oldObj, newObj interface{}) {
|
||||
oldApp, oldOk := oldObj.(*appv1.Application)
|
||||
newApp, newOk := newObj.(*appv1.Application)
|
||||
if oldOk && newOk {
|
||||
if oldApp.Spec.Destination.Server != newApp.Spec.Destination.Server {
|
||||
onAppModified(oldObj)
|
||||
onAppModified(newApp)
|
||||
}
|
||||
}
|
||||
},
|
||||
DeleteFunc: onAppModified,
|
||||
})
|
||||
|
||||
return c.db.WatchClusters(ctx, clusterEventCallback)
|
||||
|
||||
}, "watch clusters", ctx, clusterRetryTimeout)
|
||||
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
// watchClusterResources watches for resource changes annotated with application label on specified cluster and schedule corresponding app refresh.
|
||||
func (c *liveStateCache) watchClusterResources(ctx context.Context, item appv1.Cluster) {
|
||||
util.RetryUntilSucceed(func() (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("Recovered from panic: %v\n", r)
|
||||
}
|
||||
}()
|
||||
config := item.RESTConfig()
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
ch, err := c.kubectl.WatchResources(ctx, config, c.settings, func(gk schema.GroupKind) (s string, e error) {
|
||||
clusterInfo, err := c.getSyncedCluster(item.Server)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return clusterInfo.getResourceVersion(gk), nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for event := range ch {
|
||||
if event.WatchEvent != nil {
|
||||
eventObj := event.WatchEvent.Object.(*unstructured.Unstructured)
|
||||
if kube.IsCRD(eventObj) {
|
||||
// restart if new CRD has been created after watch started
|
||||
if event.WatchEvent.Type == watch.Added {
|
||||
c.removeCluster(item.Server)
|
||||
return fmt.Errorf("Restarting the watch because a new CRD %s was added", eventObj.GetName())
|
||||
} else if event.WatchEvent.Type == watch.Deleted {
|
||||
c.removeCluster(item.Server)
|
||||
return fmt.Errorf("Restarting the watch because CRD %s was deleted", eventObj.GetName())
|
||||
}
|
||||
}
|
||||
err = c.processEvent(event.WatchEvent.Type, eventObj, item.Server)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to process event %s for obj %v: %v", event.WatchEvent.Type, event.WatchEvent.Object, err)
|
||||
}
|
||||
} else {
|
||||
err = c.updateCache(item.Server, event.CacheRefresh.GVK.GroupKind(), event.CacheRefresh.ResourceVersion, event.CacheRefresh.Objects)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to process event %s for obj %v: %v", event.WatchEvent.Type, event.WatchEvent.Object, err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return fmt.Errorf("resource updates channel has closed")
|
||||
}, fmt.Sprintf("watch app resources on %s", item.Server), ctx, clusterRetryTimeout)
|
||||
}
|
||||
|
||||
func (c *liveStateCache) updateCache(server string, gk schema.GroupKind, resourceVersion string, objs []unstructured.Unstructured) error {
|
||||
clusterInfo, err := c.getSyncedCluster(server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clusterInfo.updateCache(gk, resourceVersion, objs)
|
||||
return nil
|
||||
}
|
||||
|
||||
129
controller/cache/cache_test.go
vendored
@@ -1,129 +0,0 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/kube/kubetest"
|
||||
)
|
||||
|
||||
const (
|
||||
pollInterval = 500 * time.Millisecond
|
||||
)
|
||||
|
||||
func TestWatchClusterResourcesHandlesResourceEvents(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
events := make(chan kube.WatchEvent)
|
||||
defer func() {
|
||||
cancel()
|
||||
close(events)
|
||||
}()
|
||||
|
||||
pod := testPod.DeepCopy()
|
||||
|
||||
kubeMock := &kubetest.MockKubectlCmd{
|
||||
Resources: []kube.ResourcesBatch{{
|
||||
GVK: pod.GroupVersionKind(),
|
||||
Objects: make([]unstructured.Unstructured, 0),
|
||||
}},
|
||||
Events: events,
|
||||
}
|
||||
|
||||
server := "https://test"
|
||||
clusterCache := newClusterExt(kubeMock)
|
||||
|
||||
cache := &liveStateCache{
|
||||
clusters: map[string]*clusterInfo{server: clusterCache},
|
||||
lock: &sync.Mutex{},
|
||||
kubectl: kubeMock,
|
||||
}
|
||||
|
||||
go cache.watchClusterResources(ctx, v1alpha1.Cluster{Server: server})
|
||||
|
||||
assert.False(t, clusterCache.synced())
|
||||
|
||||
events <- kube.WatchEvent{WatchEvent: &watch.Event{Object: pod, Type: watch.Added}}
|
||||
|
||||
err := wait.Poll(pollInterval, wait.ForeverTestTimeout, func() (bool, error) {
|
||||
_, hasPod := clusterCache.nodes[kube.GetResourceKey(pod)]
|
||||
return hasPod, nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
|
||||
pod.SetResourceVersion("updated-resource-version")
|
||||
events <- kube.WatchEvent{WatchEvent: &watch.Event{Object: pod, Type: watch.Modified}}
|
||||
|
||||
err = wait.Poll(pollInterval, wait.ForeverTestTimeout, func() (bool, error) {
|
||||
updatedPodInfo, hasPod := clusterCache.nodes[kube.GetResourceKey(pod)]
|
||||
return hasPod && updatedPodInfo.resourceVersion == "updated-resource-version", nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
|
||||
events <- kube.WatchEvent{WatchEvent: &watch.Event{Object: pod, Type: watch.Deleted}}
|
||||
|
||||
err = wait.Poll(pollInterval, wait.ForeverTestTimeout, func() (bool, error) {
|
||||
_, hasPod := clusterCache.nodes[kube.GetResourceKey(pod)]
|
||||
return !hasPod, nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestClusterCacheDroppedOnCreatedDeletedCRD(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
events := make(chan kube.WatchEvent)
|
||||
defer func() {
|
||||
cancel()
|
||||
close(events)
|
||||
}()
|
||||
|
||||
kubeMock := &kubetest.MockKubectlCmd{
|
||||
Resources: []kube.ResourcesBatch{{
|
||||
GVK: testCRD.GroupVersionKind(),
|
||||
Objects: make([]unstructured.Unstructured, 0),
|
||||
}},
|
||||
Events: events,
|
||||
}
|
||||
|
||||
server := "https://test"
|
||||
clusterCache := newClusterExt(kubeMock)
|
||||
|
||||
cache := &liveStateCache{
|
||||
clusters: map[string]*clusterInfo{server: clusterCache},
|
||||
lock: &sync.Mutex{},
|
||||
kubectl: kubeMock,
|
||||
}
|
||||
|
||||
go cache.watchClusterResources(ctx, v1alpha1.Cluster{Server: server})
|
||||
|
||||
err := clusterCache.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
events <- kube.WatchEvent{WatchEvent: &watch.Event{Object: testCRD, Type: watch.Added}}
|
||||
err = wait.Poll(pollInterval, wait.ForeverTestTimeout, func() (bool, error) {
|
||||
cache.lock.Lock()
|
||||
defer cache.lock.Unlock()
|
||||
_, hasCache := cache.clusters[server]
|
||||
return !hasCache, nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
|
||||
cache.clusters[server] = clusterCache
|
||||
|
||||
events <- kube.WatchEvent{WatchEvent: &watch.Event{Object: testCRD, Type: watch.Deleted}}
|
||||
err = wait.Poll(pollInterval, wait.ForeverTestTimeout, func() (bool, error) {
|
||||
cache.lock.Lock()
|
||||
defer cache.lock.Unlock()
|
||||
_, hasCache := cache.clusters[server]
|
||||
return !hasCache, nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
417
controller/cache/cluster.go
vendored
@@ -1,13 +1,19 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"github.com/argoproj/argo-cd/controller/metrics"
|
||||
|
||||
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"
|
||||
@@ -16,49 +22,43 @@ import (
|
||||
|
||||
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 (
|
||||
clusterSyncTimeout = 24 * time.Hour
|
||||
clusterRetryTimeout = 10 * time.Second
|
||||
clusterSyncTimeout = 24 * time.Hour
|
||||
clusterRetryTimeout = 10 * time.Second
|
||||
watchResourcesRetryTimeout = 1 * time.Second
|
||||
)
|
||||
|
||||
type gkInfo struct {
|
||||
resource metav1.APIResource
|
||||
type apiMeta struct {
|
||||
namespaced bool
|
||||
resourceVersion string
|
||||
watchCancel context.CancelFunc
|
||||
}
|
||||
|
||||
type clusterInfo struct {
|
||||
apis map[schema.GroupKind]*gkInfo
|
||||
nodes map[kube.ResourceKey]*node
|
||||
nsIndex map[string]map[kube.ResourceKey]*node
|
||||
lock *sync.Mutex
|
||||
onAppUpdated func(appName string)
|
||||
kubectl kube.Kubectl
|
||||
cluster *appv1.Cluster
|
||||
syncLock *sync.Mutex
|
||||
syncTime *time.Time
|
||||
syncError error
|
||||
log *log.Entry
|
||||
settings *settings.ArgoCDSettings
|
||||
syncLock *sync.Mutex
|
||||
syncTime *time.Time
|
||||
syncError error
|
||||
apisMeta map[schema.GroupKind]*apiMeta
|
||||
|
||||
lock *sync.Mutex
|
||||
nodes map[kube.ResourceKey]*node
|
||||
nsIndex map[string]map[kube.ResourceKey]*node
|
||||
|
||||
onObjectUpdated ObjectUpdatedHandler
|
||||
kubectl kube.Kubectl
|
||||
cluster *appv1.Cluster
|
||||
log *log.Entry
|
||||
cacheSettingsSrc func() *cacheSettings
|
||||
}
|
||||
|
||||
func (c *clusterInfo) getResourceVersion(gk schema.GroupKind) string {
|
||||
func (c *clusterInfo) replaceResourceCache(gk schema.GroupKind, resourceVersion string, objs []unstructured.Unstructured) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
info, ok := c.apis[gk]
|
||||
if ok {
|
||||
return info.resourceVersion
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *clusterInfo) updateCache(gk schema.GroupKind, resourceVersion string, objs []unstructured.Unstructured) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
info, ok := c.apis[gk]
|
||||
info, ok := c.apisMeta[gk]
|
||||
if ok {
|
||||
objByKind := make(map[kube.ResourceKey]*unstructured.Unstructured)
|
||||
for i := range objs {
|
||||
@@ -85,33 +85,63 @@ func (c *clusterInfo) updateCache(gk schema.GroupKind, resourceVersion string, o
|
||||
}
|
||||
}
|
||||
|
||||
func createObjInfo(un *unstructured.Unstructured, appInstanceLabel string) *node {
|
||||
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()
|
||||
// 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 {
|
||||
ownerRefs = append(ownerRefs, metav1.OwnerReference{
|
||||
Name: un.GetName(),
|
||||
Kind: kube.ServiceKind,
|
||||
APIVersion: "",
|
||||
APIVersion: "v1",
|
||||
})
|
||||
}
|
||||
info := &node{
|
||||
resourceVersion: un.GetResourceVersion(),
|
||||
ref: v1.ObjectReference{
|
||||
APIVersion: un.GetAPIVersion(),
|
||||
Kind: un.GetKind(),
|
||||
Name: un.GetName(),
|
||||
Namespace: un.GetNamespace(),
|
||||
},
|
||||
ownerRefs: ownerRefs,
|
||||
info: getNodeInfo(un),
|
||||
|
||||
// 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: kube.GetObjectRef(un),
|
||||
ownerRefs: ownerRefs,
|
||||
}
|
||||
|
||||
populateNodeInfo(un, nodeInfo)
|
||||
appName := kube.GetAppInstanceLabel(un, appInstanceLabel)
|
||||
if len(ownerRefs) == 0 && appName != "" {
|
||||
info.appName = appName
|
||||
info.resource = un
|
||||
nodeInfo.appName = appName
|
||||
nodeInfo.resource = un
|
||||
}
|
||||
return info
|
||||
nodeInfo.health, _ = health.GetResourceHealth(un, c.cacheSettingsSrc().ResourceOverrides)
|
||||
return nodeInfo
|
||||
}
|
||||
|
||||
func (c *clusterInfo) setNode(n *node) {
|
||||
@@ -136,7 +166,13 @@ func (c *clusterInfo) removeNode(key kube.ResourceKey) {
|
||||
}
|
||||
|
||||
func (c *clusterInfo) invalidate() {
|
||||
c.syncLock.Lock()
|
||||
defer c.syncLock.Unlock()
|
||||
c.syncTime = nil
|
||||
for i := range c.apisMeta {
|
||||
c.apisMeta[i].watchCancel()
|
||||
}
|
||||
c.apisMeta = nil
|
||||
}
|
||||
|
||||
func (c *clusterInfo) synced() bool {
|
||||
@@ -149,38 +185,158 @@ func (c *clusterInfo) synced() bool {
|
||||
return time.Now().Before(c.syncTime.Add(clusterSyncTimeout))
|
||||
}
|
||||
|
||||
func (c *clusterInfo) sync() (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
|
||||
}
|
||||
}()
|
||||
func (c *clusterInfo) stopWatching(gk schema.GroupKind) {
|
||||
c.syncLock.Lock()
|
||||
defer c.syncLock.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.log.Info("Start syncing cluster")
|
||||
// startMissingWatches lists supported cluster resources and start watching for changes unless watch is already running
|
||||
func (c *clusterInfo) startMissingWatches() error {
|
||||
|
||||
c.apis = make(map[schema.GroupKind]*gkInfo)
|
||||
c.nodes = make(map[kube.ResourceKey]*node)
|
||||
|
||||
resources, err := c.kubectl.GetResources(c.cluster.RESTConfig(), c.settings, "")
|
||||
apis, err := c.kubectl.GetAPIResources(c.cluster.RESTConfig(), c.cacheSettingsSrc().ResourcesFilter)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to sync cluster %s: %v", c.cluster.Server, err)
|
||||
return err
|
||||
}
|
||||
|
||||
appLabelKey := c.settings.GetAppInstanceLabelKey()
|
||||
for res := range resources {
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
for i := range apis {
|
||||
api := apis[i]
|
||||
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)
|
||||
}
|
||||
if _, ok := c.apis[res.GVK.GroupKind()]; !ok {
|
||||
c.apis[res.GVK.GroupKind()] = &gkInfo{
|
||||
resourceVersion: res.ListResourceVersion,
|
||||
resource: res.ResourceInfo,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runSynced(lock *sync.Mutex, action func() error) error {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
return action()
|
||||
}
|
||||
|
||||
func (c *clusterInfo) watchEvents(ctx context.Context, api kube.APIResourceInfo, info *apiMeta) {
|
||||
util.RetryUntilSucceed(func() (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
|
||||
}
|
||||
}()
|
||||
|
||||
err = runSynced(c.syncLock, func() error {
|
||||
if info.resourceVersion == "" {
|
||||
list, err := api.Interface.List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.replaceResourceCache(api.GroupKind, list.GetResourceVersion(), list.Items)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w, err := api.Interface.Watch(metav1.ListOptions{ResourceVersion: info.resourceVersion})
|
||||
if errors.IsNotFound(err) {
|
||||
c.stopWatching(api.GroupKind)
|
||||
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 err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case event, ok := <-w.ResultChan():
|
||||
if ok {
|
||||
obj := event.Object.(*unstructured.Unstructured)
|
||||
info.resourceVersion = obj.GetResourceVersion()
|
||||
c.processEvent(event.Type, obj)
|
||||
if kube.IsCRD(obj) {
|
||||
if event.Type == watch.Deleted {
|
||||
group, groupOk, groupErr := unstructured.NestedString(obj.Object, "spec", "group")
|
||||
kind, kindOk, kindErr := unstructured.NestedString(obj.Object, "spec", "names", "kind")
|
||||
|
||||
if groupOk && groupErr == nil && kindOk && kindErr == nil {
|
||||
gk := schema.GroupKind{Group: group, Kind: kind}
|
||||
c.stopWatching(gk)
|
||||
}
|
||||
} else {
|
||||
err = runSynced(c.syncLock, func() error {
|
||||
return c.startMissingWatches()
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Warnf("Failed to start missing watch: %v", err)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("Watch %s on %s has closed", api.GroupKind, c.cluster.Server)
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := range res.Objects {
|
||||
c.setNode(createObjInfo(&res.Objects[i], appLabelKey))
|
||||
|
||||
}, fmt.Sprintf("watch %s on %s", api.GroupKind, c.cluster.Server), ctx, watchResourcesRetryTimeout)
|
||||
}
|
||||
|
||||
func (c *clusterInfo) sync() (err error) {
|
||||
|
||||
c.log.Info("Start syncing cluster")
|
||||
|
||||
for i := range c.apisMeta {
|
||||
c.apisMeta[i].watchCancel()
|
||||
}
|
||||
c.apisMeta = make(map[schema.GroupKind]*apiMeta)
|
||||
c.nodes = make(map[kube.ResourceKey]*node)
|
||||
|
||||
apis, err := c.kubectl.GetAPIResources(c.cluster.RESTConfig(), c.cacheSettingsSrc().ResourcesFilter)
|
||||
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.cacheSettingsSrc().AppInstanceLabelKey))
|
||||
}
|
||||
lock.Unlock()
|
||||
return nil
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
err = c.startMissingWatches()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Failed to sync cluster %s: %v", c.cluster.Server, err)
|
||||
return err
|
||||
}
|
||||
|
||||
c.log.Info("Cluster successfully synced")
|
||||
@@ -188,9 +344,6 @@ func (c *clusterInfo) sync() (err error) {
|
||||
}
|
||||
|
||||
func (c *clusterInfo) ensureSynced() error {
|
||||
if c.synced() {
|
||||
return c.syncError
|
||||
}
|
||||
c.syncLock.Lock()
|
||||
defer c.syncLock.Unlock()
|
||||
if c.synced() {
|
||||
@@ -204,29 +357,56 @@ func (c *clusterInfo) ensureSynced() error {
|
||||
return c.syncError
|
||||
}
|
||||
|
||||
func (c *clusterInfo) getChildren(obj *unstructured.Unstructured) []appv1.ResourceNode {
|
||||
func (c *clusterInfo) getNamespaceTopLevelResources(namespace string) map[kube.ResourceKey]appv1.ResourceNode {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
children := make([]appv1.ResourceNode, 0)
|
||||
if objInfo, ok := c.nodes[kube.GetResourceKey(obj)]; ok {
|
||||
nsNodes := c.nsIndex[obj.GetNamespace()]
|
||||
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 {
|
||||
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) {
|
||||
children = append(children, child.childResourceNodes(nsNodes, map[kube.ResourceKey]bool{objInfo.resourceKey(): true}))
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
return children
|
||||
}
|
||||
|
||||
func (c *clusterInfo) isNamespaced(gk schema.GroupKind) bool {
|
||||
if api, ok := c.apis[gk]; ok && !api.resource.Namespaced {
|
||||
if api, ok := c.apisMeta[gk]; ok && !api.namespaced {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
|
||||
func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured, metricsServer *metrics.MetricsServer) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
@@ -237,6 +417,7 @@ func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*uns
|
||||
managedObjs[key] = o.resource
|
||||
}
|
||||
}
|
||||
config := metrics.AddMetricsTransportWrapper(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{}
|
||||
@@ -253,22 +434,40 @@ 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) {
|
||||
c.checkAndInvalidateStaleCache(targetObj.GroupVersionKind(), existingObj.ref.Namespace, existingObj.ref.Name)
|
||||
return nil
|
||||
}
|
||||
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
|
||||
@@ -283,37 +482,10 @@ func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*uns
|
||||
return managedObjs, nil
|
||||
}
|
||||
|
||||
func (c *clusterInfo) delete(obj *unstructured.Unstructured) error {
|
||||
err := c.kubectl.DeleteResource(c.cluster.RESTConfig(), obj.GroupVersionKind(), obj.GetName(), obj.GetNamespace(), false)
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
// a delete request came in for an object which does not exist. it's possible that our cache
|
||||
// is stale. Check and invalidate if it is
|
||||
c.lock.Lock()
|
||||
c.checkAndInvalidateStaleCache(obj.GroupVersionKind(), obj.GetNamespace(), obj.GetName())
|
||||
c.lock.Unlock()
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// checkAndInvalidateStaleCache checks if our cache is stale and invalidate it based on error
|
||||
// should be called whenever we suspect our cache is stale
|
||||
func (c *clusterInfo) checkAndInvalidateStaleCache(gvk schema.GroupVersionKind, namespace string, name string) {
|
||||
if _, ok := c.nodes[kube.NewResourceKey(gvk.Group, gvk.Kind, namespace, name)]; ok {
|
||||
if c.syncTime != nil {
|
||||
c.log.Warnf("invalidated stale cache due to mismatch of %s, %s/%s", gvk, namespace, name)
|
||||
c.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *clusterInfo) processEvent(event watch.EventType, un *unstructured.Unstructured) error {
|
||||
func (c *clusterInfo) processEvent(event watch.EventType, un *unstructured.Unstructured) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
key := kube.GetResourceKey(un)
|
||||
if info, ok := c.apis[schema.GroupKind{Group: key.Group, Kind: key.Kind}]; ok {
|
||||
info.resourceVersion = un.GetResourceVersion()
|
||||
}
|
||||
existingNode, exists := c.nodes[key]
|
||||
if event == watch.Deleted {
|
||||
if exists {
|
||||
@@ -322,8 +494,6 @@ func (c *clusterInfo) processEvent(event watch.EventType, un *unstructured.Unstr
|
||||
} else if event != watch.Deleted {
|
||||
c.onNodeUpdated(exists, existingNode, un, key)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *clusterInfo) onNodeUpdated(exists bool, existingNode *node, un *unstructured.Unstructured, key kube.ResourceKey) {
|
||||
@@ -331,7 +501,7 @@ func (c *clusterInfo) onNodeUpdated(exists bool, existingNode *node, un *unstruc
|
||||
if exists {
|
||||
nodes = append(nodes, existingNode)
|
||||
}
|
||||
newObj := createObjInfo(un, c.settings.GetAppInstanceLabelKey())
|
||||
newObj := c.createObjInfo(un, c.cacheSettingsSrc().AppInstanceLabelKey)
|
||||
c.setNode(newObj)
|
||||
nodes = append(nodes, newObj)
|
||||
toNotify := make(map[string]bool)
|
||||
@@ -342,19 +512,24 @@ func (c *clusterInfo) onNodeUpdated(exists bool, existingNode *node, un *unstruc
|
||||
if app == "" || skipAppRequeing(key) {
|
||||
continue
|
||||
}
|
||||
toNotify[app] = true
|
||||
toNotify[app] = n.isRootAppNode() || toNotify[app]
|
||||
}
|
||||
}
|
||||
for name := range toNotify {
|
||||
c.onAppUpdated(name)
|
||||
}
|
||||
c.onObjectUpdated(toNotify, newObj.ref)
|
||||
}
|
||||
|
||||
func (c *clusterInfo) onNodeRemoved(key kube.ResourceKey, existingNode *node) {
|
||||
c.removeNode(key)
|
||||
if existingNode.appName != "" {
|
||||
c.onAppUpdated(existingNode.appName)
|
||||
func (c *clusterInfo) onNodeRemoved(key kube.ResourceKey, n *node) {
|
||||
appName := n.appName
|
||||
if ns, ok := c.nsIndex[key.Namespace]; ok {
|
||||
appName = n.getApp(ns)
|
||||
}
|
||||
|
||||
c.removeNode(key)
|
||||
managedByApp := make(map[string]bool)
|
||||
if appName != "" {
|
||||
managedByApp[appName] = n.isRootAppNode()
|
||||
}
|
||||
c.onObjectUpdated(managedByApp, n.ref)
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
332
controller/cache/cluster_test.go
vendored
@@ -1,6 +1,7 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -12,14 +13,16 @@ import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"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 {
|
||||
@@ -40,107 +43,215 @@ 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: v1
|
||||
apiVersion: extensions/v1beta1
|
||||
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(`
|
||||
apiVersion: extensions/v1beta1
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: helm-guestbook
|
||||
uid: "3"
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
resourceVersion: "123"`)
|
||||
|
||||
testCRD = strToUnstructured(`
|
||||
apiVersion: apiextensions.k8s.io/v1beta1
|
||||
kind: CustomResourceDefinition
|
||||
testService = strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: my-custom-resource-definition
|
||||
resourceVersion: "123"`)
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
resourceVersion: "123"
|
||||
uid: "4"
|
||||
spec:
|
||||
selector:
|
||||
app: guestbook
|
||||
type: LoadBalancer
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
- hostname: localhost`)
|
||||
|
||||
testIngress = strToUnstructured(`
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
uid: "4"
|
||||
spec:
|
||||
backend:
|
||||
serviceName: not-found-service
|
||||
servicePort: 443
|
||||
rules:
|
||||
- host: helm-guestbook.com
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: helm-guestbook
|
||||
servicePort: 443
|
||||
path: /
|
||||
- backend:
|
||||
serviceName: helm-guestbook
|
||||
servicePort: https
|
||||
path: /
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
- ip: 107.178.210.11`)
|
||||
)
|
||||
|
||||
func newCluster(objs ...*unstructured.Unstructured) *clusterInfo {
|
||||
resByGVK := make(map[schema.GroupVersionKind][]unstructured.Unstructured)
|
||||
runtimeObjs := make([]runtime.Object, len(objs))
|
||||
for i := range objs {
|
||||
resByGVK[objs[i].GroupVersionKind()] = append(resByGVK[objs[i].GroupVersionKind()], *objs[i])
|
||||
runtimeObjs[i] = objs[i]
|
||||
}
|
||||
resources := make([]kube.ResourcesBatch, 0)
|
||||
for gvk, objects := range resByGVK {
|
||||
resources = append(resources, kube.ResourcesBatch{
|
||||
ListResourceVersion: "1",
|
||||
GVK: gvk,
|
||||
Objects: objects,
|
||||
})
|
||||
}
|
||||
return newClusterExt(kubetest.MockKubectlCmd{
|
||||
Resources: resources,
|
||||
})
|
||||
scheme := runtime.NewScheme()
|
||||
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: "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: "Deployment"},
|
||||
Interface: client.Resource(schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}),
|
||||
Meta: metav1.APIResource{Namespaced: true},
|
||||
}}
|
||||
|
||||
return newClusterExt(&kubetest.MockKubectlCmd{APIResources: apiResources})
|
||||
}
|
||||
|
||||
func newClusterExt(kubectl kube.Kubectl) *clusterInfo {
|
||||
return &clusterInfo{
|
||||
lock: &sync.Mutex{},
|
||||
nodes: make(map[kube.ResourceKey]*node),
|
||||
onAppUpdated: func(appName string) {},
|
||||
kubectl: kubectl,
|
||||
nsIndex: make(map[string]map[kube.ResourceKey]*node),
|
||||
cluster: &appv1.Cluster{},
|
||||
syncTime: nil,
|
||||
syncLock: &sync.Mutex{},
|
||||
apis: make(map[schema.GroupKind]*gkInfo),
|
||||
log: log.WithField("cluster", "test"),
|
||||
settings: &settings.ArgoCDSettings{},
|
||||
lock: &sync.Mutex{},
|
||||
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,
|
||||
syncLock: &sync.Mutex{},
|
||||
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, app string) {
|
||||
hierarchy = append(hierarchy, child)
|
||||
})
|
||||
return hierarchy[1:]
|
||||
}
|
||||
|
||||
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()
|
||||
assert.Nil(t, err)
|
||||
|
||||
rsChildren := cluster.getChildren(testRS)
|
||||
rsChildren := getChildren(cluster, testRS)
|
||||
assert.Equal(t, []appv1.ResourceNode{{
|
||||
Kind: "Pod",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-pod",
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
ResourceRef: appv1.ResourceRef{
|
||||
Kind: "Pod",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-pod",
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
UID: "1",
|
||||
},
|
||||
ParentRefs: []appv1.ResourceRef{{
|
||||
Group: "apps",
|
||||
Version: "",
|
||||
Kind: "ReplicaSet",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-rs",
|
||||
UID: "2",
|
||||
}},
|
||||
Health: &appv1.HealthStatus{Status: appv1.HealthStatusUnknown},
|
||||
NetworkingInfo: &appv1.ResourceNetworkingInfo{Labels: testPod.GetLabels()},
|
||||
ResourceVersion: "123",
|
||||
Info: []appv1.InfoItem{{Name: "Containers", Value: "0/0"}},
|
||||
Children: make([]appv1.ResourceNode, 0),
|
||||
ResourceVersion: "123",
|
||||
}}, rsChildren)
|
||||
deployChildren := cluster.getChildren(testDeploy)
|
||||
deployChildren := getChildren(cluster, testDeploy)
|
||||
|
||||
assert.Equal(t, []appv1.ResourceNode{{
|
||||
Kind: "ReplicaSet",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-rs",
|
||||
Group: "extensions",
|
||||
Version: "v1beta1",
|
||||
assert.Equal(t, append([]appv1.ResourceNode{{
|
||||
ResourceRef: appv1.ResourceRef{
|
||||
Kind: "ReplicaSet",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-rs",
|
||||
Group: "apps",
|
||||
Version: "v1",
|
||||
UID: "2",
|
||||
},
|
||||
ResourceVersion: "123",
|
||||
Children: rsChildren,
|
||||
Info: []appv1.InfoItem{},
|
||||
}}, deployChildren)
|
||||
Health: &appv1.HealthStatus{Status: appv1.HealthStatusHealthy},
|
||||
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)
|
||||
}
|
||||
|
||||
func TestGetManagedLiveObjs(t *testing.T) {
|
||||
@@ -149,7 +260,7 @@ func TestGetManagedLiveObjs(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
|
||||
targetDeploy := strToUnstructured(`
|
||||
apiVersion: extensions/v1beta1
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
@@ -163,7 +274,7 @@ metadata:
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
}, []*unstructured.Unstructured{targetDeploy})
|
||||
}, []*unstructured.Unstructured{targetDeploy}, nil)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, managedObjs, map[kube.ResourceKey]*unstructured.Unstructured{
|
||||
kube.NewResourceKey("apps", "Deployment", "default", "helm-guestbook"): testDeploy,
|
||||
@@ -175,10 +286,9 @@ 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 := cluster.getChildren(testRS)
|
||||
rsChildren := getChildren(cluster, testRS)
|
||||
assert.Equal(t, []appv1.ResourceNode{}, rsChildren)
|
||||
}
|
||||
|
||||
@@ -191,38 +301,63 @@ 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 := cluster.getChildren(testRS)
|
||||
rsChildren := getChildren(cluster, testRS)
|
||||
sort.Slice(rsChildren, func(i, j int) bool {
|
||||
return strings.Compare(rsChildren[i].Name, rsChildren[j].Name) < 0
|
||||
})
|
||||
assert.Equal(t, []appv1.ResourceNode{{
|
||||
Kind: "Pod",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-pod",
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Info: []appv1.InfoItem{{Name: "Containers", Value: "0/0"}},
|
||||
Children: make([]appv1.ResourceNode, 0),
|
||||
ResourceRef: appv1.ResourceRef{
|
||||
Kind: "Pod",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-pod",
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
UID: "1",
|
||||
},
|
||||
Info: []appv1.InfoItem{{Name: "Containers", Value: "0/0"}},
|
||||
Health: &appv1.HealthStatus{Status: appv1.HealthStatusUnknown},
|
||||
NetworkingInfo: &appv1.ResourceNetworkingInfo{Labels: testPod.GetLabels()},
|
||||
ParentRefs: []appv1.ResourceRef{{
|
||||
Group: "apps",
|
||||
Version: "",
|
||||
Kind: "ReplicaSet",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-rs",
|
||||
UID: "2",
|
||||
}},
|
||||
ResourceVersion: "123",
|
||||
}, {
|
||||
Kind: "Pod",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-pod2",
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Info: []appv1.InfoItem{{Name: "Containers", Value: "0/0"}},
|
||||
Children: make([]appv1.ResourceNode, 0),
|
||||
ResourceRef: appv1.ResourceRef{
|
||||
Kind: "Pod",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-pod2",
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
UID: "4",
|
||||
},
|
||||
NetworkingInfo: &appv1.ResourceNetworkingInfo{Labels: testPod.GetLabels()},
|
||||
Info: []appv1.InfoItem{{Name: "Containers", Value: "0/0"}},
|
||||
Health: &appv1.HealthStatus{Status: appv1.HealthStatusUnknown},
|
||||
ParentRefs: []appv1.ResourceRef{{
|
||||
Group: "apps",
|
||||
Version: "",
|
||||
Kind: "ReplicaSet",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-rs",
|
||||
UID: "2",
|
||||
}},
|
||||
ResourceVersion: "123",
|
||||
}}, rsChildren)
|
||||
}
|
||||
@@ -257,8 +392,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))]
|
||||
|
||||
@@ -269,17 +403,18 @@ func TestUpdateResourceTags(t *testing.T) {
|
||||
func TestUpdateAppResource(t *testing.T) {
|
||||
updatesReceived := make([]string, 0)
|
||||
cluster := newCluster(testPod, testRS, testDeploy)
|
||||
cluster.onAppUpdated = func(appName string) {
|
||||
updatesReceived = append(updatesReceived, appName)
|
||||
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.Equal(t, []string{"helm-guestbook"}, updatesReceived)
|
||||
assert.Contains(t, updatesReceived, "helm-guestbook: false")
|
||||
}
|
||||
|
||||
func TestCircularReference(t *testing.T) {
|
||||
@@ -294,8 +429,13 @@ func TestCircularReference(t *testing.T) {
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
children := cluster.getChildren(dep)
|
||||
assert.Len(t, children, 1)
|
||||
children := getChildren(cluster, dep)
|
||||
assert.Len(t, children, 2)
|
||||
|
||||
node := cluster.nodes[kube.GetResourceKey(dep)]
|
||||
assert.NotNil(t, node)
|
||||
app := node.getApp(cluster.nodes)
|
||||
assert.Equal(t, "", app)
|
||||
}
|
||||
|
||||
func TestWatchCacheUpdated(t *testing.T) {
|
||||
@@ -316,7 +456,7 @@ func TestWatchCacheUpdated(t *testing.T) {
|
||||
|
||||
podGroupKind := testPod.GroupVersionKind().GroupKind()
|
||||
|
||||
cluster.updateCache(podGroupKind, "updated-list-version", []unstructured.Unstructured{*updated, *added})
|
||||
cluster.replaceResourceCache(podGroupKind, "updated-list-version", []unstructured.Unstructured{*updated, *added})
|
||||
|
||||
_, ok := cluster.nodes[kube.GetResourceKey(removed)]
|
||||
assert.False(t, ok)
|
||||
@@ -327,6 +467,22 @@ func TestWatchCacheUpdated(t *testing.T) {
|
||||
|
||||
_, ok = cluster.nodes[kube.GetResourceKey(added)]
|
||||
assert.True(t, ok)
|
||||
|
||||
assert.Equal(t, cluster.getResourceVersion(podGroupKind), "updated-list-version")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
172
controller/cache/info.go
vendored
@@ -3,29 +3,166 @@ package cache
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
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 getNodeInfo(un *unstructured.Unstructured) []v1alpha1.InfoItem {
|
||||
gvk := un.GroupVersionKind()
|
||||
func populateNodeInfo(un *unstructured.Unstructured, node *node) {
|
||||
|
||||
if gvk.Kind == kube.PodKind && gvk.Group == "" {
|
||||
return getPodInfo(un)
|
||||
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 {
|
||||
case kube.PodKind:
|
||||
populatePodInfo(un, node)
|
||||
return
|
||||
case kube.ServiceKind:
|
||||
populateServiceInfo(un, node)
|
||||
return
|
||||
}
|
||||
case "extensions", "networking.k8s.io":
|
||||
switch gvk.Kind {
|
||||
case kube.IngressKind:
|
||||
populateIngressInfo(un, node)
|
||||
return
|
||||
}
|
||||
}
|
||||
return []v1alpha1.InfoItem{}
|
||||
}
|
||||
|
||||
func getPodInfo(un *unstructured.Unstructured) []v1alpha1.InfoItem {
|
||||
func getIngress(un *unstructured.Unstructured) []v1.LoadBalancerIngress {
|
||||
ingress, ok, err := unstructured.NestedSlice(un.Object, "status", "loadBalancer", "ingress")
|
||||
if !ok || err != nil {
|
||||
return nil
|
||||
}
|
||||
res := make([]v1.LoadBalancerIngress, 0)
|
||||
for _, item := range ingress {
|
||||
if lbIngress, ok := item.(map[string]interface{}); ok {
|
||||
if hostname := lbIngress["hostname"]; hostname != nil {
|
||||
res = append(res, v1.LoadBalancerIngress{Hostname: fmt.Sprintf("%s", hostname)})
|
||||
} else if ip := lbIngress["ip"]; ip != nil {
|
||||
res = append(res, v1.LoadBalancerIngress{IP: fmt.Sprintf("%s", ip)})
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func populateServiceInfo(un *unstructured.Unstructured, node *node) {
|
||||
targetLabels, _, _ := unstructured.NestedStringMap(un.Object, "spec", "selector")
|
||||
ingress := make([]v1.LoadBalancerIngress, 0)
|
||||
if serviceType, ok, err := unstructured.NestedString(un.Object, "spec", "type"); ok && err == nil && serviceType == string(v1.ServiceTypeLoadBalancer) {
|
||||
ingress = getIngress(un)
|
||||
}
|
||||
node.networkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetLabels: targetLabels, Ingress: ingress}
|
||||
}
|
||||
|
||||
func populateIngressInfo(un *unstructured.Unstructured, node *node) {
|
||||
ingress := getIngress(un)
|
||||
targetsMap := make(map[v1alpha1.ResourceRef]bool)
|
||||
if backend, ok, err := unstructured.NestedMap(un.Object, "spec", "backend"); ok && err == nil {
|
||||
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 {
|
||||
for i := range rules {
|
||||
rule, ok := rules[i].(map[string]interface{})
|
||||
if !ok {
|
||||
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
|
||||
}
|
||||
for i := range paths {
|
||||
path, ok := paths[i].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if serviceName, ok, err := unstructured.NestedString(path, "backend", "serviceName"); ok && err == nil {
|
||||
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 != "" && 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:
|
||||
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: ingress, ExternalURLs: urls}
|
||||
}
|
||||
|
||||
func populatePodInfo(un *unstructured.Unstructured, node *node) {
|
||||
pod := v1.Pod{}
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, &pod)
|
||||
if err != nil {
|
||||
return []v1alpha1.InfoItem{}
|
||||
return
|
||||
}
|
||||
restarts := 0
|
||||
totalContainers := len(pod.Spec.Containers)
|
||||
@@ -36,6 +173,19 @@ func getPodInfo(un *unstructured.Unstructured) []v1alpha1.InfoItem {
|
||||
reason = pod.Status.Reason
|
||||
}
|
||||
|
||||
imagesSet := make(map[string]bool)
|
||||
for _, container := range pod.Spec.InitContainers {
|
||||
imagesSet[container.Image] = true
|
||||
}
|
||||
for _, container := range pod.Spec.Containers {
|
||||
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]
|
||||
@@ -99,9 +249,9 @@ func getPodInfo(un *unstructured.Unstructured) []v1alpha1.InfoItem {
|
||||
reason = "Terminating"
|
||||
}
|
||||
|
||||
info := make([]v1alpha1.InfoItem, 0)
|
||||
if reason != "" {
|
||||
info = append(info, v1alpha1.InfoItem{Name: "Status Reason", Value: reason})
|
||||
node.info = append(node.info, v1alpha1.InfoItem{Name: "Status Reason", Value: reason})
|
||||
}
|
||||
return append(info, v1alpha1.InfoItem{Name: "Containers", Value: fmt.Sprintf("%d/%d", readyContainers, totalContainers)})
|
||||
node.info = append(node.info, v1alpha1.InfoItem{Name: "Containers", Value: fmt.Sprintf("%d/%d", readyContainers, totalContainers)})
|
||||
node.networkingInfo = &v1alpha1.ResourceNetworkingInfo{Labels: un.GetLabels()}
|
||||
}
|
||||
|
||||
222
controller/cache/info_test.go
vendored
Normal file
@@ -0,0 +1,222 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetPodInfo(t *testing.T) {
|
||||
pod := strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: helm-guestbook-pod
|
||||
namespace: default
|
||||
ownerReferences:
|
||||
- apiVersion: extensions/v1beta1
|
||||
kind: ReplicaSet
|
||||
name: helm-guestbook-rs
|
||||
resourceVersion: "123"
|
||||
labels:
|
||||
app: guestbook
|
||||
spec:
|
||||
containers:
|
||||
- image: bar`)
|
||||
|
||||
node := &node{}
|
||||
populateNodeInfo(pod, node)
|
||||
assert.Equal(t, []v1alpha1.InfoItem{{Name: "Containers", Value: "0/1"}}, node.info)
|
||||
assert.Equal(t, []string{"bar"}, node.images)
|
||||
assert.Equal(t, &v1alpha1.ResourceNetworkingInfo{Labels: map[string]string{"app": "guestbook"}}, node.networkingInfo)
|
||||
}
|
||||
|
||||
func TestGetServiceInfo(t *testing.T) {
|
||||
node := &node{}
|
||||
populateNodeInfo(testService, node)
|
||||
assert.Equal(t, 0, len(node.info))
|
||||
assert.Equal(t, &v1alpha1.ResourceNetworkingInfo{
|
||||
TargetLabels: map[string]string{"app": "guestbook"},
|
||||
Ingress: []v1.LoadBalancerIngress{{Hostname: "localhost"}},
|
||||
}, node.networkingInfo)
|
||||
}
|
||||
|
||||
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{{
|
||||
Namespace: "default",
|
||||
Group: "",
|
||||
Kind: kube.ServiceKind,
|
||||
Name: "not-found-service",
|
||||
}, {
|
||||
Namespace: "default",
|
||||
Group: "",
|
||||
Kind: kube.ServiceKind,
|
||||
Name: "helm-guestbook",
|
||||
}},
|
||||
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)
|
||||
}
|
||||
134
controller/cache/mocks/LiveStateCache.go
vendored
@@ -3,56 +3,24 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
kube "github.com/argoproj/argo-cd/util/kube"
|
||||
|
||||
schema "k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
v1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
import "github.com/argoproj/argo-cd/util/kube"
|
||||
import "github.com/stretchr/testify/mock"
|
||||
import "k8s.io/apimachinery/pkg/runtime/schema"
|
||||
import "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
import "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)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, *unstructured.Unstructured) error); ok {
|
||||
r0 = rf(server, obj)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetChildren provides a mock function with given fields: server, obj
|
||||
func (_m *LiveStateCache) GetChildren(server string, obj *unstructured.Unstructured) ([]v1alpha1.ResourceNode, error) {
|
||||
ret := _m.Called(server, obj)
|
||||
|
||||
var r0 []v1alpha1.ResourceNode
|
||||
if rf, ok := ret.Get(0).(func(string, *unstructured.Unstructured) []v1alpha1.ResourceNode); ok {
|
||||
r0 = rf(server, obj)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]v1alpha1.ResourceNode)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, *unstructured.Unstructured) error); ok {
|
||||
r1 = rf(server, obj)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetManagedLiveObjs provides a mock function with given fields: a, targetObjs
|
||||
func (_m *LiveStateCache) GetManagedLiveObjs(a *v1alpha1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
|
||||
ret := _m.Called(a, targetObjs)
|
||||
@@ -76,25 +44,22 @@ func (_m *LiveStateCache) GetManagedLiveObjs(a *v1alpha1.Application, targetObjs
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Invalidate provides a mock function with given fields:
|
||||
func (_m *LiveStateCache) Invalidate() {
|
||||
_m.Called()
|
||||
}
|
||||
// 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)
|
||||
|
||||
// IsNamespaced provides a mock function with given fields: server, gvk
|
||||
func (_m *LiveStateCache) IsNamespaced(server string, gvk schema.GroupVersionKind) (bool, error) {
|
||||
ret := _m.Called(server, gvk)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(string, schema.GroupVersionKind) bool); ok {
|
||||
r0 = rf(server, gvk)
|
||||
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 {
|
||||
r0 = ret.Get(0).(bool)
|
||||
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, schema.GroupVersionKind) error); ok {
|
||||
r1 = rf(server, gvk)
|
||||
if rf, ok := ret.Get(1).(func(string, string) error); ok {
|
||||
r1 = rf(server, namespace)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
@@ -102,7 +67,56 @@ func (_m *LiveStateCache) IsNamespaced(server string, gvk schema.GroupVersionKin
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Run provides a mock function with given fields: ctx
|
||||
func (_m *LiveStateCache) Run(ctx context.Context) {
|
||||
_m.Called(ctx)
|
||||
// Invalidate provides a mock function with given fields:
|
||||
func (_m *LiveStateCache) Invalidate() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// 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, 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, schema.GroupKind) error); ok {
|
||||
r1 = rf(server, gk)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// IterateHierarchy provides a mock function with given fields: server, key, action
|
||||
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, string)) error); ok {
|
||||
r0 = rf(server, key, action)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Run provides a mock function with given fields: 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
|
||||
}
|
||||
|
||||
90
controller/cache/node.go
vendored
@@ -6,7 +6,7 @@ import (
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
@@ -18,7 +18,16 @@ type node struct {
|
||||
ownerRefs []metav1.OwnerReference
|
||||
info []appv1.InfoItem
|
||||
appName string
|
||||
resource *unstructured.Unstructured
|
||||
// available only for root application nodes
|
||||
resource *unstructured.Unstructured
|
||||
// networkingInfo are available only for known types involved into networking: Ingress, Service, Pod
|
||||
networkingInfo *appv1.ResourceNetworkingInfo
|
||||
images []string
|
||||
health *appv1.HealthStatus
|
||||
}
|
||||
|
||||
func (n *node) isRootAppNode() bool {
|
||||
return n.appName != "" && len(n.ownerRefs) == 0
|
||||
}
|
||||
|
||||
func (n *node) resourceKey() kube.ResourceKey {
|
||||
@@ -26,9 +35,16 @@ func (n *node) resourceKey() kube.ResourceKey {
|
||||
}
|
||||
|
||||
func (n *node) isParentOf(child *node) bool {
|
||||
ownerGvk := n.ref.GroupVersionKind()
|
||||
for _, ownerRef := range child.ownerRefs {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -45,13 +61,24 @@ func ownerRefGV(ownerRef metav1.OwnerReference) schema.GroupVersion {
|
||||
}
|
||||
|
||||
func (n *node) getApp(ns map[kube.ResourceKey]*node) string {
|
||||
return n.getAppRecursive(ns, map[kube.ResourceKey]bool{})
|
||||
}
|
||||
|
||||
func (n *node) getAppRecursive(ns map[kube.ResourceKey]*node, visited map[kube.ResourceKey]bool) string {
|
||||
if !visited[n.resourceKey()] {
|
||||
visited[n.resourceKey()] = true
|
||||
} else {
|
||||
log.Warnf("Circular dependency detected: %v.", visited)
|
||||
return n.appName
|
||||
}
|
||||
|
||||
if n.appName != "" {
|
||||
return n.appName
|
||||
}
|
||||
for _, ownerRef := range n.ownerRefs {
|
||||
gv := ownerRefGV(ownerRef)
|
||||
if parent, ok := ns[kube.NewResourceKey(gv.Group, ownerRef.Kind, n.ref.Namespace, ownerRef.Name)]; ok {
|
||||
app := parent.getApp(ns)
|
||||
app := parent.getAppRecursive(ns, visited)
|
||||
if app != "" {
|
||||
return app
|
||||
}
|
||||
@@ -71,30 +98,45 @@ func newResourceKeySet(set map[kube.ResourceKey]bool, keys ...kube.ResourceKey)
|
||||
return newSet
|
||||
}
|
||||
|
||||
func (n *node) childResourceNodes(ns map[kube.ResourceKey]*node, parents map[kube.ResourceKey]bool) appv1.ResourceNode {
|
||||
children := make([]appv1.ResourceNode, 0)
|
||||
for childKey := range ns {
|
||||
func (n *node) asResourceNode() appv1.ResourceNode {
|
||||
gv, err := schema.ParseGroupVersion(n.ref.APIVersion)
|
||||
if err != nil {
|
||||
gv = schema.GroupVersion{}
|
||||
}
|
||||
parentRefs := make([]appv1.ResourceRef, len(n.ownerRefs))
|
||||
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, 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,
|
||||
Kind: n.ref.Kind,
|
||||
Namespace: n.ref.Namespace,
|
||||
},
|
||||
ParentRefs: parentRefs,
|
||||
Info: n.info,
|
||||
ResourceVersion: n.resourceVersion,
|
||||
NetworkingInfo: n.networkingInfo,
|
||||
Images: n.images,
|
||||
Health: n.health,
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
children = append(children, ns[childKey].childResourceNodes(ns, newResourceKeySet(parents, n.resourceKey())))
|
||||
action(child.asResourceNode(), child.getApp(ns))
|
||||
child.iterateChildren(ns, newResourceKeySet(parents, n.resourceKey()), action)
|
||||
}
|
||||
}
|
||||
}
|
||||
gv, err := schema.ParseGroupVersion(n.ref.APIVersion)
|
||||
if err != nil {
|
||||
gv = schema.GroupVersion{}
|
||||
}
|
||||
return appv1.ResourceNode{
|
||||
Name: n.ref.Name,
|
||||
Group: gv.Group,
|
||||
Version: gv.Version,
|
||||
Kind: n.ref.Kind,
|
||||
Namespace: n.ref.Namespace,
|
||||
Info: n.info,
|
||||
Children: children,
|
||||
ResourceVersion: n.resourceVersion,
|
||||
}
|
||||
}
|
||||
|
||||
83
controller/cache/node_test.go
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var c = &clusterInfo{cacheSettingsSrc: func() *cacheSettings {
|
||||
return &cacheSettings{AppInstanceLabelKey: common.LabelKeyAppInstance}
|
||||
}}
|
||||
|
||||
func TestIsParentOf(t *testing.T) {
|
||||
child := c.createObjInfo(testPod, "")
|
||||
parent := c.createObjInfo(testRS, "")
|
||||
grandParent := c.createObjInfo(testDeploy, "")
|
||||
|
||||
assert.True(t, parent.isParentOf(child))
|
||||
assert.False(t, grandParent.isParentOf(child))
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package metrics
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
@@ -12,12 +13,16 @@ 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
|
||||
k8sRequestCounter *prometheus.CounterVec
|
||||
kubectlExecCounter *prometheus.CounterVec
|
||||
kubectlExecPendingGauge *prometheus.GaugeVec
|
||||
reconcileHistogram *prometheus.HistogramVec
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -57,10 +62,13 @@ var (
|
||||
)
|
||||
|
||||
// NewMetricsServer returns a new prometheus server which collects application metrics
|
||||
func NewMetricsServer(addr string, appLister applister.ApplicationLister) *MetricsServer {
|
||||
func NewMetricsServer(addr string, appLister applister.ApplicationLister, healthCheck func() error) *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{}))
|
||||
healthz.ServeHealthCheck(mux, healthCheck)
|
||||
|
||||
syncCounter := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
@@ -70,6 +78,24 @@ func NewMetricsServer(addr string, appLister applister.ApplicationLister) *Metri
|
||||
append(descAppDefaultLabels, "phase"),
|
||||
)
|
||||
appRegistry.MustRegister(syncCounter)
|
||||
kubectlExecCounter := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "argocd_kubectl_exec_total",
|
||||
Help: "Number of kubectl executions",
|
||||
}, []string{"command"})
|
||||
appRegistry.MustRegister(kubectlExecCounter)
|
||||
kubectlExecPendingGauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "argocd_kubectl_exec_pending",
|
||||
Help: "Number of pending kubectl executions",
|
||||
}, []string{"command"})
|
||||
appRegistry.MustRegister(kubectlExecPendingGauge)
|
||||
k8sRequestCounter := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "argocd_app_k8s_request_total",
|
||||
Help: "Number of kubernetes requests executed during application reconciliation.",
|
||||
},
|
||||
append(descAppDefaultLabels, "response_code"),
|
||||
)
|
||||
appRegistry.MustRegister(k8sRequestCounter)
|
||||
|
||||
reconcileHistogram := prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
@@ -78,7 +104,7 @@ func NewMetricsServer(addr string, appLister applister.ApplicationLister) *Metri
|
||||
// Buckets chosen after observing a ~2100ms mean reconcile time
|
||||
Buckets: []float64{0.25, .5, 1, 2, 4, 8, 16},
|
||||
},
|
||||
append(descAppDefaultLabels),
|
||||
descAppDefaultLabels,
|
||||
)
|
||||
|
||||
appRegistry.MustRegister(reconcileHistogram)
|
||||
@@ -88,8 +114,11 @@ func NewMetricsServer(addr string, appLister applister.ApplicationLister) *Metri
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
},
|
||||
syncCounter: syncCounter,
|
||||
reconcileHistogram: reconcileHistogram,
|
||||
syncCounter: syncCounter,
|
||||
k8sRequestCounter: k8sRequestCounter,
|
||||
reconcileHistogram: reconcileHistogram,
|
||||
kubectlExecCounter: kubectlExecCounter,
|
||||
kubectlExecPendingGauge: kubectlExecPendingGauge,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,11 +130,28 @@ func (m *MetricsServer) IncSync(app *argoappv1.Application, state *argoappv1.Ope
|
||||
m.syncCounter.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject(), string(state.Phase)).Inc()
|
||||
}
|
||||
|
||||
// IncKubernetesRequest increments the kubernetes requests counter for an application
|
||||
func (m *MetricsServer) IncKubernetesRequest(app *argoappv1.Application, statusCode int) {
|
||||
m.k8sRequestCounter.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject(), strconv.Itoa(statusCode)).Inc()
|
||||
}
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
type appCollector struct {
|
||||
store applister.ApplicationLister
|
||||
}
|
||||
|
||||
@@ -104,6 +104,10 @@ argocd_app_sync_status{name="my-app",namespace="argocd",project="default",sync_s
|
||||
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 {
|
||||
var app argoappv1.Application
|
||||
err := yaml.Unmarshal([]byte(fakeApp), &app)
|
||||
@@ -133,7 +137,7 @@ func newFakeLister(fakeApp ...string) (context.CancelFunc, applister.Application
|
||||
func testApp(t *testing.T, fakeApp string, expectedResponse string) {
|
||||
cancel, appLister := newFakeLister(fakeApp)
|
||||
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()
|
||||
@@ -176,7 +180,7 @@ argocd_app_sync_total{name="my-app",namespace="argocd",phase="Succeeded",project
|
||||
func TestMetricsSyncCounter(t *testing.T) {
|
||||
cancel, appLister := newFakeLister()
|
||||
defer cancel()
|
||||
metricsServ := NewMetricsServer("localhost:8082", appLister)
|
||||
metricsServ := NewMetricsServer("localhost:8082", appLister, noOpHealthCheck)
|
||||
|
||||
fakeApp := newFakeApp(fakeApp)
|
||||
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: argoappv1.OperationRunning})
|
||||
@@ -217,10 +221,10 @@ argocd_app_reconcile_count{name="my-app",namespace="argocd",project="important-p
|
||||
func TestReconcileMetrics(t *testing.T) {
|
||||
cancel, appLister := newFakeLister()
|
||||
defer cancel()
|
||||
metricsServ := NewMetricsServer("localhost:8082", appLister)
|
||||
metricsServ := NewMetricsServer("localhost:8082", appLister, noOpHealthCheck)
|
||||
|
||||
fakeApp := newFakeApp(fakeApp)
|
||||
metricsServ.IncReconcile(fakeApp, time.Duration(5*time.Second))
|
||||
metricsServ.IncReconcile(fakeApp, 5*time.Second)
|
||||
|
||||
req, err := http.NewRequest("GET", "/metrics", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
37
controller/metrics/transportwrapper.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"k8s.io/client-go/rest"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
type metricsRoundTripper struct {
|
||||
roundTripper http.RoundTripper
|
||||
app *v1alpha1.Application
|
||||
metricsServer *MetricsServer
|
||||
}
|
||||
|
||||
func (mrt *metricsRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
resp, err := mrt.roundTripper.RoundTrip(r)
|
||||
statusCode := 0
|
||||
if resp != nil {
|
||||
statusCode = resp.StatusCode
|
||||
}
|
||||
mrt.metricsServer.IncKubernetesRequest(mrt.app, statusCode)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// 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 {
|
||||
wrap := config.WrapTransport
|
||||
config.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
|
||||
if wrap != nil {
|
||||
rt = wrap(rt)
|
||||
}
|
||||
return &metricsRoundTripper{roundTripper: rt, metricsServer: server, app: app}
|
||||
}
|
||||
return config
|
||||
}
|
||||
@@ -9,16 +9,17 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
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,6 +27,8 @@ 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"
|
||||
)
|
||||
|
||||
@@ -49,42 +52,58 @@ func GetLiveObjs(res []managedResource) []*unstructured.Unstructured {
|
||||
return objs
|
||||
}
|
||||
|
||||
type ResourceInfoProvider interface {
|
||||
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, revision string, source v1alpha1.ApplicationSource, noCache bool, localObjects []string) *comparisonResult
|
||||
SyncAppState(app *v1alpha1.Application, state *v1alpha1.OperationState)
|
||||
}
|
||||
|
||||
type comparisonResult struct {
|
||||
observedAt 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
|
||||
}
|
||||
|
||||
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) {
|
||||
helmRepos, err := m.db.ListHelmRepositories(context.Background())
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
repo, err := m.db.GetRepository(context.Background(), source.RepoURL)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
repo := m.getRepo(source.RepoURL)
|
||||
conn, repoClient, err := m.repoClientset.NewRepoServerClient()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
@@ -95,14 +114,31 @@ 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
|
||||
}
|
||||
|
||||
manifestInfo, err := repoClient.GenerateManifest(context.Background(), &repository.ManifestRequest{
|
||||
tools := make([]*appv1.ConfigManagementPlugin, len(plugins))
|
||||
for i := range plugins {
|
||||
tools[i] = &plugins[i]
|
||||
}
|
||||
|
||||
buildOptions, err := m.settingsMgr.GetKustomizeBuildOptions()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
cluster, err := m.db.GetCluster(context.Background(), app.Spec.Destination.Server)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
cluster.ServerVersion, err = m.kubectl.GetServerVersion(cluster.RESTConfig())
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
manifestInfo, err := repoClient.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: repo,
|
||||
HelmRepos: helmRepos,
|
||||
Repos: helmRepos,
|
||||
Revision: revision,
|
||||
NoCache: noCache,
|
||||
AppLabelKey: appLabelKey,
|
||||
@@ -110,17 +146,31 @@ func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1
|
||||
Namespace: app.Spec.Destination.Namespace,
|
||||
ApplicationSource: &source,
|
||||
Plugins: tools,
|
||||
KustomizeOptions: &appv1.KustomizeOptions{
|
||||
BuildOptions: buildOptions,
|
||||
},
|
||||
KubeVersion: cluster.ServerVersion,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
targetObjs, hooks, err := unmarshalManifests(manifestInfo.Manifests)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
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 ignore.Ignore(obj) {
|
||||
continue
|
||||
}
|
||||
if hookutil.IsHook(obj) {
|
||||
hooks = append(hooks, obj)
|
||||
@@ -128,31 +178,169 @@ func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1
|
||||
targetObjs = append(targetObjs, obj)
|
||||
}
|
||||
}
|
||||
return targetObjs, hooks, manifestInfo, nil
|
||||
return targetObjs, hooks, nil
|
||||
}
|
||||
|
||||
func DeduplicateTargetObjects(
|
||||
server string,
|
||||
namespace string,
|
||||
objs []*unstructured.Unstructured,
|
||||
infoProvider ResourceInfoProvider,
|
||||
) ([]*unstructured.Unstructured, []v1alpha1.ApplicationCondition, error) {
|
||||
|
||||
targetByKey := make(map[kubeutil.ResourceKey][]*unstructured.Unstructured)
|
||||
for i := range objs {
|
||||
obj := objs[i]
|
||||
isNamespaced, err := infoProvider.IsNamespaced(server, obj.GroupVersionKind().GroupKind())
|
||||
if err != nil {
|
||||
return objs, nil, err
|
||||
}
|
||||
if !isNamespaced {
|
||||
obj.SetNamespace("")
|
||||
} else if obj.GetNamespace() == "" {
|
||||
obj.SetNamespace(namespace)
|
||||
}
|
||||
key := kubeutil.GetResourceKey(obj)
|
||||
targetByKey[key] = append(targetByKey[key], obj)
|
||||
}
|
||||
conditions := make([]v1alpha1.ApplicationCondition, 0)
|
||||
result := make([]*unstructured.Unstructured, 0)
|
||||
for key, targets := range targetByKey {
|
||||
if len(targets) > 1 {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{
|
||||
Type: appv1.ApplicationConditionRepeatedResourceWarning,
|
||||
Message: fmt.Sprintf("Resource %s appeared %d times among application resources.", key.String(), len(targets)),
|
||||
})
|
||||
}
|
||||
result = append(result, targets[len(targets)-1])
|
||||
}
|
||||
|
||||
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, error) {
|
||||
resourceOverrides, err := m.settingsMgr.GetResourceOverrides()
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
appLabelKey, err := m.settingsMgr.GetAppInstanceLabelKey()
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
diffNormalizer, err := argo.NewDiffNormalizer(app.Spec.IgnoreDifferences, resourceOverrides)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
return appLabelKey, resourceOverrides, diffNormalizer, 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, revision string, source v1alpha1.ApplicationSource, noCache bool, localManifests []string) *comparisonResult {
|
||||
appLabelKey, resourceOverrides, diffNormalizer, err := m.getComparisonSettings(app)
|
||||
|
||||
// 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
|
||||
|
||||
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()})
|
||||
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()})
|
||||
failedToLoadObjs = true
|
||||
}
|
||||
manifestInfo = nil
|
||||
}
|
||||
|
||||
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, dedupConditions...)
|
||||
|
||||
resFilter, err := m.settingsMgr.GetResourcesFilter()
|
||||
if err != nil {
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
} else {
|
||||
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()),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logCtx.Debugf("Generated config manifests")
|
||||
liveObjByKey, err := m.liveStateCache.GetManagedLiveObjs(app, targetObjs)
|
||||
dedupLiveResources(targetObjs, liveObjByKey)
|
||||
if err != nil {
|
||||
liveObjByKey = make(map[kubeutil.ResourceKey]*unstructured.Unstructured)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
@@ -175,7 +363,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.GroupVersionKind()); 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())
|
||||
@@ -198,16 +386,19 @@ 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()})
|
||||
}
|
||||
|
||||
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
|
||||
@@ -215,35 +406,44 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
gvk := obj.GroupVersionKind()
|
||||
|
||||
resState := v1alpha1.ResourceStatus{
|
||||
Namespace: util.FirstNonEmpty(obj.GetNamespace(), app.Spec.Destination.Namespace),
|
||||
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 {
|
||||
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
|
||||
}
|
||||
// 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,
|
||||
}
|
||||
@@ -264,32 +464,32 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
syncStatus.Revision = manifestInfo.Revision
|
||||
}
|
||||
|
||||
healthStatus, err := health.SetApplicationHealth(resourceSummaries, GetLiveObjs(managedResources), m.settings.ResourceOverrides)
|
||||
healthStatus, err := health.SetApplicationHealth(resourceSummaries, GetLiveObjs(managedResources), resourceOverrides, func(obj *unstructured.Unstructured) bool {
|
||||
return !isSelfReferencedApp(app, kubeutil.GetObjectRef(obj))
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
}
|
||||
|
||||
compRes := comparisonResult{
|
||||
observedAt: observedAt,
|
||||
syncStatus: &syncStatus,
|
||||
healthStatus: healthStatus,
|
||||
resources: resourceSummaries,
|
||||
managedResources: managedResources,
|
||||
conditions: conditions,
|
||||
hooks: hooks,
|
||||
diffNormalizer: diffNormalizer,
|
||||
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}
|
||||
if manifestInfo != nil {
|
||||
compRes.appSourceType = v1alpha1.ApplicationSourceType(manifestInfo.SourceType)
|
||||
}
|
||||
return repo
|
||||
app.Status.SetConditions(conditions, map[appv1.ApplicationConditionType]bool{
|
||||
appv1.ApplicationConditionComparisonError: true,
|
||||
appv1.ApplicationConditionSharedResourceWarning: true,
|
||||
appv1.ApplicationConditionRepeatedResourceWarning: true,
|
||||
appv1.ApplicationConditionExcludedResourceWarning: true,
|
||||
})
|
||||
return &compRes
|
||||
}
|
||||
|
||||
func (m *appStateManager) persistRevisionHistory(app *v1alpha1.Application, revision string, source v1alpha1.ApplicationSource) error {
|
||||
@@ -324,12 +524,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,
|
||||
@@ -338,7 +539,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,21 +30,21 @@ 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, "", 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{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{string(test.PodManifest)},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
@@ -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, "", 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, "", 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, "", 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, "", 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,11 +160,234 @@ 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, "", 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 {
|
||||
data, err := json.Marshal(obj)
|
||||
assert.NoError(t, err)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func TestCompareAppStateDuplicatedNamespacedResources(t *testing.T) {
|
||||
obj1 := test.NewPod()
|
||||
obj1.SetNamespace(test.FakeDestNamespace)
|
||||
obj2 := test.NewPod()
|
||||
obj3 := test.NewPod()
|
||||
obj3.SetNamespace("kube-system")
|
||||
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{toJSON(t, obj1), toJSON(t, obj2), toJSON(t, obj3)},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
||||
kube.GetResourceKey(obj1): obj1,
|
||||
kube.GetResourceKey(obj3): obj3,
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false, nil)
|
||||
|
||||
assert.NotNil(t, compRes)
|
||||
assert.Contains(t, app.Status.Conditions, argoappv1.ApplicationCondition{
|
||||
Message: "Resource /Pod/fake-dest-ns/my-pod appeared 2 times among application resources.",
|
||||
Type: argoappv1.ApplicationConditionRepeatedResourceWarning,
|
||||
})
|
||||
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, "", 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, "", 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, "", 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)
|
||||
}
|
||||
|
||||
1049
controller/sync.go
@@ -1,66 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"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,334 +2,34 @@ package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/health"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// getOperationPhase returns a hook status from an _live_ unstructured object
|
||||
func getOperationPhase(hook *unstructured.Unstructured) (operation v1alpha1.OperationPhase, message string) {
|
||||
gvk := hook.GroupVersionKind()
|
||||
apiResource, err := kube.ServerResourceForGroupVersionKind(sc.disco, gvk)
|
||||
if err != nil {
|
||||
return false, 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
return getStatusFromBatchJob(hook)
|
||||
} else if isArgoWorkflow(gvk) {
|
||||
updateStatusFromArgoWorkflow(hook, &hookStatus)
|
||||
return health.GetStatusFromArgoWorkflow(hook)
|
||||
} else if isPod(gvk) {
|
||||
updateStatusFromPod(hook, &hookStatus)
|
||||
return getStatusFromPod(hook)
|
||||
} else {
|
||||
hookStatus.HookPhase = appv1.OperationSucceeded
|
||||
hookStatus.Message = fmt.Sprintf("%s created", hook.GetName())
|
||||
return v1alpha1.OperationSucceeded, 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()
|
||||
func isRunnable(gvk schema.GroupVersionKind) bool {
|
||||
return isBatchJob(gvk) || isArgoWorkflow(gvk) || isPod(gvk)
|
||||
}
|
||||
|
||||
@@ -337,18 +37,16 @@ func isBatchJob(gvk schema.GroupVersionKind) bool {
|
||||
return gvk.Group == "batch" && gvk.Kind == "Job"
|
||||
}
|
||||
|
||||
func updateStatusFromBatchJob(hook *unstructured.Unstructured, hookStatus *appv1.ResourceResult) {
|
||||
// TODO this is a copy-and-paste of health.getJobHealth(), refactor out?
|
||||
func getStatusFromBatchJob(hook *unstructured.Unstructured) (operation v1alpha1.OperationPhase, message string) {
|
||||
var job batch.Job
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(hook.Object, &job)
|
||||
if err != nil {
|
||||
hookStatus.HookPhase = appv1.OperationError
|
||||
hookStatus.Message = err.Error()
|
||||
return
|
||||
return v1alpha1.OperationError, err.Error()
|
||||
}
|
||||
failed := false
|
||||
var failMsg string
|
||||
complete := false
|
||||
var message string
|
||||
for _, condition := range job.Status.Conditions {
|
||||
switch condition.Type {
|
||||
case batch.JobFailed:
|
||||
@@ -361,14 +59,11 @@ func updateStatusFromBatchJob(hook *unstructured.Unstructured, hookStatus *appv1
|
||||
}
|
||||
}
|
||||
if !complete {
|
||||
hookStatus.HookPhase = appv1.OperationRunning
|
||||
hookStatus.Message = message
|
||||
return v1alpha1.OperationRunning, message
|
||||
} else if failed {
|
||||
hookStatus.HookPhase = appv1.OperationFailed
|
||||
hookStatus.Message = failMsg
|
||||
return v1alpha1.OperationFailed, failMsg
|
||||
} else {
|
||||
hookStatus.HookPhase = appv1.OperationSucceeded
|
||||
hookStatus.Message = message
|
||||
return v1alpha1.OperationSucceeded, message
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,38 +71,16 @@ 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) {
|
||||
// TODO - this is very similar to health.getPodHealth() should we use that instead?
|
||||
func getStatusFromPod(hook *unstructured.Unstructured) (v1alpha1.OperationPhase, string) {
|
||||
var pod apiv1.Pod
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(hook.Object, &pod)
|
||||
if err != nil {
|
||||
hookStatus.HookPhase = appv1.OperationError
|
||||
hookStatus.Message = err.Error()
|
||||
return
|
||||
return v1alpha1.OperationError, err.Error()
|
||||
}
|
||||
getFailMessage := func(ctr *apiv1.ContainerStatus) string {
|
||||
if ctr.State.Terminated != nil {
|
||||
@@ -426,135 +99,22 @@ func updateStatusFromPod(hook *unstructured.Unstructured, hookStatus *appv1.Reso
|
||||
|
||||
switch pod.Status.Phase {
|
||||
case apiv1.PodPending, apiv1.PodRunning:
|
||||
hookStatus.HookPhase = appv1.OperationRunning
|
||||
return v1alpha1.OperationRunning, ""
|
||||
case apiv1.PodSucceeded:
|
||||
hookStatus.HookPhase = appv1.OperationSucceeded
|
||||
return v1alpha1.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
|
||||
return v1alpha1.OperationFailed, pod.Status.Message
|
||||
}
|
||||
for _, ctr := range append(pod.Status.InitContainerStatuses, pod.Status.ContainerStatuses...) {
|
||||
if msg := getFailMessage(&ctr); msg != "" {
|
||||
hookStatus.Message = msg
|
||||
return
|
||||
return v1alpha1.OperationFailed, msg
|
||||
}
|
||||
}
|
||||
return v1alpha1.OperationFailed, ""
|
||||
case apiv1.PodUnknown:
|
||||
hookStatus.HookPhase = appv1.OperationError
|
||||
return v1alpha1.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 v1alpha1.OperationRunning, ""
|
||||
}
|
||||
|
||||
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"
|
||||
@@ -45,7 +46,9 @@ func newTestSyncCtx(resources ...*v1.APIResourceList) *syncContext {
|
||||
config: &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 +73,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 +118,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 +151,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 +162,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 +182,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 +267,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 +292,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 +306,141 @@ 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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())
|
||||
}
|
||||
|
||||
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 TestPersistRevisionHistory(t *testing.T) {
|
||||
@@ -453,7 +456,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 +493,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 +529,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"))
|
||||
}
|
||||
|
||||
166
docs/CONTRIBUTING.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# 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)
|
||||
* [git](https://git-scm.com/) and [git-lfs](https://git-lfs.github.com/)
|
||||
* [golang](https://golang.org/)
|
||||
* [dep](https://github.com/golang/dep)
|
||||
* [ksonnet](https://github.com/ksonnet/ksonnet#install)
|
||||
* [helm](https://github.com/helm/helm/releases)
|
||||
* [kustomize](https://github.com/kubernetes-sigs/kustomize/releases)
|
||||
* [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 install go git-lfs kubectl kubectx dep ksonnet/tap/ks kubernetes-helm kustomize kustomize
|
||||
```
|
||||
|
||||
Check the versions:
|
||||
|
||||
```
|
||||
go version ;# must be v1.12.x
|
||||
helm version ;# must be v2.13.x
|
||||
kustomize version ;# must be v3.10.x
|
||||
```
|
||||
|
||||
Set up environment variables (e.g. is `~/.bashrc`):
|
||||
|
||||
```bash
|
||||
export GOPATH=~/go
|
||||
export PATH=$PATH:$GOPATH/bin
|
||||
```
|
||||
|
||||
Checkout the code:
|
||||
|
||||
```bash
|
||||
go get -u github.com/argoproj/argo-cd
|
||||
cd ~/go/src/github.com/argoproj/argo-cd
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
Ensure dependencies are up to date first:
|
||||
|
||||
```shell
|
||||
dep ensure
|
||||
make dev-builder-image
|
||||
make install-lint-tools
|
||||
go get github.com/mattn/goreman
|
||||
go get github.com/jstemmer/go-junit-report
|
||||
```
|
||||
|
||||
Common make targets:
|
||||
|
||||
* `make codegen` - Run code generation
|
||||
* `make lint` - Lint code
|
||||
* `make test` - Run unit tests
|
||||
* `make cli` - Make the `argocd` CLI tool
|
||||
|
||||
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/argocd-application-controller --replicas 0
|
||||
kubectl -n argocd scale deployment/argocd-dex-server --replicas 0
|
||||
kubectl -n argocd scale deployment/argocd-repo-server --replicas 0
|
||||
kubectl -n argocd scale deployment/argocd-server --replicas 0
|
||||
kubectl -n argocd scale deployment/argocd-redis --replicas 0
|
||||
```
|
||||
|
||||
Download Yarn dependencies and Compile:
|
||||
|
||||
```bash
|
||||
~/go/src/github.com/argoproj/argo-cd/ui
|
||||
yarn install
|
||||
yarn build
|
||||
```
|
||||
|
||||
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 create guestbook --path guestbook --repo https://github.com/argoproj/argocd-example-apps.git --dest-server https://kubernetes.default.svc --dest-namespace default --server localhost:8080 --plaintext --insecure
|
||||
```
|
||||
|
||||
You can open the UI: http://localhost:4000
|
||||
|
||||
As an alternative to using the above command line parameters each time you call `argocd` CLI, you can set the following environment variables:
|
||||
|
||||
```bash
|
||||
export ARGOCD_SERVER=127.0.0.1:8080
|
||||
export ARGOCD_OPTS="--plaintext --insecure"
|
||||
```
|
||||
|
||||
## 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 don't want to use `latest` as the image's tag (the default), you can set it from the environment too:
|
||||
|
||||
```bash
|
||||
export IMAGE_TAG=yourtag
|
||||
```
|
||||
|
||||
Build the image:
|
||||
|
||||
```bash
|
||||
DOCKER_PUSH=true make image
|
||||
```
|
||||
|
||||
Update the manifests (be sure to do that from a shell that has above environment variables set)
|
||||
|
||||
```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/argocd-application-controller --replicas 1
|
||||
kubectl -n argocd scale deployment/argocd-dex-server --replicas 1
|
||||
kubectl -n argocd scale deployment/argocd-repo-server --replicas 1
|
||||
kubectl -n argocd scale deployment/argocd-server --replicas 1
|
||||
kubectl -n argocd scale deployment/argocd-redis --replicas 1
|
||||
```
|
||||
|
||||
Now you can set-up the port-forwarding and open the UI or CLI.
|
||||
@@ -1,38 +0,0 @@
|
||||
# Argo CD Documentation
|
||||
|
||||
## [Getting Started](getting_started.md)
|
||||
|
||||
## Concepts
|
||||
* [Architecture](architecture.md)
|
||||
* [Tracking Strategies](tracking_strategies.md)
|
||||
|
||||
## Quick Reference
|
||||
| Name | Kind | Description |
|
||||
|------|------|-------------|
|
||||
| [`argocd-cm.yaml`](argocd-cm.yaml) | ConfigMap | General Argo CD configuration |
|
||||
| [`argocd-secret.yaml`](argocd-secret.yaml) | Secret | Password, Certificates, Signing Key |
|
||||
| [`argocd-rbac-cm.yaml`](argocd-rbac-cm.yaml) | ConfigMap | RBAC Configuration |
|
||||
| [`application.yaml`](application.yaml) | Application | Example application spec |
|
||||
| [`project.yaml`](argocd-rbac-cm.yaml) | AppProject | Example project spec |
|
||||
|
||||
## Features
|
||||
* [Application Sources](application_sources.md)
|
||||
* [Application Parameters](parameters.md)
|
||||
* [Projects](projects.md)
|
||||
* [Automated Sync](auto_sync.md)
|
||||
* [Resource Health](health.md)
|
||||
* [Resource Hooks](resource_hooks.md)
|
||||
* [Resource Diffing](diffing.md)
|
||||
* [Single Sign On](sso.md)
|
||||
* [Webhooks](webhook.md)
|
||||
* [RBAC](rbac.md)
|
||||
* [Declarative Setup](declarative-setup.md)
|
||||
* [Prometheus Metrics](metrics.md)
|
||||
* [Custom Tooling](custom_tools.md)
|
||||
|
||||
## Other
|
||||
* [Security](security.md)
|
||||
* [Best Practices](best_practices.md)
|
||||
* [Configuring Ingress](ingress.md)
|
||||
* [Integration with CI Pipelines](ci_automation.md)
|
||||
* [F.A.Q.](faq.md)
|
||||
6
docs/SUPPORT.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Support
|
||||
|
||||
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.github.io/community/join-slack).
|
||||
4. [Read issues, report a bug, or request a feature ⧉](https://github.com/argoproj/argo-cd/issues)
|
||||
@@ -1,136 +0,0 @@
|
||||
# Application Source Types
|
||||
|
||||
Argo CD supports several different ways in which kubernetes manifests can be defined:
|
||||
|
||||
* [ksonnet](https://ksonnet.io) applications
|
||||
* [kustomize](https://kustomize.io) applications
|
||||
* [helm](https://helm.sh) charts
|
||||
* Directory of YAML/json/jsonnet manifests
|
||||
* Any custom config management tool configured as a config management plugin
|
||||
|
||||
Some additional considerations should be made when deploying apps of a particular type:
|
||||
|
||||
## Ksonnet
|
||||
|
||||
### Environments
|
||||
Ksonnet has a first class concept of an "environment." To create an application from a ksonnet
|
||||
app directory, an environment must be specified. For example, the following command creates the
|
||||
"guestbook-default" app, which points to the `default` environment:
|
||||
|
||||
```
|
||||
argocd app create guestbook-default --repo https://github.com/argoproj/argocd-example-apps.git --path guestbook --env default
|
||||
```
|
||||
|
||||
### Parameters
|
||||
Ksonnet parameters all belong to a component. For example, the following are the parameters
|
||||
available in the guestbook app, all of which belong to the `guestbook-ui` component:
|
||||
|
||||
```
|
||||
$ ks param list
|
||||
COMPONENT PARAM VALUE
|
||||
========= ===== =====
|
||||
guestbook-ui containerPort 80
|
||||
guestbook-ui image "gcr.io/heptio-images/ks-guestbook-demo:0.1"
|
||||
guestbook-ui name "guestbook-ui"
|
||||
guestbook-ui replicas 1
|
||||
guestbook-ui servicePort 80
|
||||
guestbook-ui type "LoadBalancer"
|
||||
```
|
||||
|
||||
When overriding ksonnet parameters in Argo CD, the component name should also be specified in the
|
||||
`argocd app set` command, in the form of `-p COMPONENT=PARAM=VALUE`. For example:
|
||||
```
|
||||
argocd app set guestbook-default -p guestbook-ui=image=gcr.io/heptio-images/ks-guestbook-demo:0.1
|
||||
```
|
||||
|
||||
## Helm
|
||||
|
||||
### Values Files
|
||||
|
||||
Helm has the ability to use a different, or even multiple "values.yaml" files to derive its
|
||||
parameters from. Alternate or multiple values file(s), can be specified using the `--values`
|
||||
flag. The flag can be repeated to support multiple values files:
|
||||
|
||||
```
|
||||
argocd app set helm-guestbook --values values-production.yaml
|
||||
```
|
||||
|
||||
### Helm Parameters
|
||||
|
||||
Helm has the ability to set parameter values, which override any values in
|
||||
a `values.yaml`. For example, `service.type` is a common parameter which is exposed in a Helm chart:
|
||||
```
|
||||
helm template . --set service.type=LoadBalancer
|
||||
```
|
||||
Similarly Argo CD can override values in the `values.yaml` parameters using `argo app set` command,
|
||||
in the form of `-p PARAM=VALUE`. For example:
|
||||
```
|
||||
argocd app set helm-guestbook -p service.type=LoadBalancer
|
||||
```
|
||||
|
||||
### Helm Hooks
|
||||
|
||||
Helm hooks are equivalent in concept to [Argo CD resource hooks](resource_hooks.md). In helm, a hook
|
||||
is any normal kubernetes resource annotated with the `helm.sh/hook` annotation. When Argo CD deploys
|
||||
helm application which contains helm hooks, all helm hook resources are currently ignored during
|
||||
the `kubectl apply` of the manifests. There is an
|
||||
[open issue](https://github.com/argoproj/argo-cd/issues/355) to map Helm hooks to Argo CD's concept
|
||||
of Pre/Post/Sync hooks.
|
||||
|
||||
### Random Data
|
||||
|
||||
Helm templating has the ability to generate random data during chart rendering via the
|
||||
`randAlphaNum` function. Many helm charts from the [charts repository](https://github.com/helm/charts)
|
||||
make use of this feature. For example, the following is the secret for the
|
||||
[redis helm chart](https://github.com/helm/charts/blob/master/stable/redis/templates/secrets.yaml):
|
||||
|
||||
```
|
||||
data:
|
||||
{{- if .Values.password }}
|
||||
redis-password: {{ .Values.password | b64enc | quote }}
|
||||
{{- else }}
|
||||
redis-password: {{ randAlphaNum 10 | b64enc | quote }}
|
||||
{{- end }}
|
||||
```
|
||||
|
||||
The Argo CD application controller periodically compares git state against the live state, running
|
||||
the `helm template <CHART>` command to generate the helm manifests. Because the random value is
|
||||
regenerated every time the comparison is made, any application which makes use of the `randAlphaNum`
|
||||
function will always be in an `OutOfSync` state. This can be mitigated by explicitly setting a
|
||||
value, in the values.yaml such that the value is stable between each comparison. For example:
|
||||
|
||||
```
|
||||
argocd app set redis -p password=abc123
|
||||
```
|
||||
|
||||
## Config Management Plugins
|
||||
|
||||
Argo CD allows integrating more config management tools using config management plugins. Following changes are required to configure new plugin:
|
||||
|
||||
* Make sure required binaries are available in `argocd-repo-server` pod. The binaries can be added via volume mounts or using custom image (see [custom_tools](custom_tools.md)).
|
||||
* Register a new plugin in `argocd-cm` ConfigMap:
|
||||
|
||||
```yaml
|
||||
data:
|
||||
configManagementPlugins: |
|
||||
- name: pluginName
|
||||
init: # Optional command to initialize application source directory
|
||||
command: ["sample command"]
|
||||
args: ["sample args"]
|
||||
generate: # Command to generate manifests YAML
|
||||
command: ["sample command"]
|
||||
args: ["sample args"]
|
||||
```
|
||||
|
||||
The `generate` command must print a valid YAML stream to stdout. Both `init` and `generate` commands are executed inside the application source directory.
|
||||
Commands have access to system environment variables and following additional variables:
|
||||
|
||||
`ARGOCD_APP_NAME` - name of application; `ARGOCD_APP_NAMESPACE` - destination application namespace
|
||||
|
||||
* Create an application and specify required config management plugin name.
|
||||
|
||||
```
|
||||
argocd app create <appName> --config-management-plugin <pluginName>
|
||||
```
|
||||
|
||||
More config management plugin examples are available in [argocd-example-apps](https://github.com/argoproj/argocd-example-apps/tree/master/plugins).
|
||||
BIN
docs/assets/application-of-applications.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 3.5 MiB After Width: | Height: | Size: 3.5 MiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 119 KiB |
BIN
docs/assets/cert-management-add-ssh.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
docs/assets/cert-management-add-tls.png
Normal file
|
After Width: | Height: | Size: 280 KiB |
BIN
docs/assets/cert-management-overview.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
docs/assets/cert-management-remove.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
docs/assets/compare-option-ignore-needs-pruning.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
docs/assets/dashboard.jpg
Normal file
|
After Width: | Height: | Size: 321 KiB |
BIN
docs/assets/download-codegen-patch-file.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
docs/assets/logo.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
docs/assets/orphaned-resources.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
docs/assets/repo-add-https.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
docs/assets/repo-add-overview.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
docs/assets/repo-add-ssh.png
Normal file
|
After Width: | Height: | Size: 233 KiB |
BIN
docs/assets/selective-sync.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
docs/assets/status-badge-healthy-synced.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |