Compare commits
794 Commits
v0.2.0
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0bd546a07 | ||
|
|
984829fcc8 | ||
|
|
c48e27f265 | ||
|
|
5fe1447b72 | ||
|
|
539516bd43 | ||
|
|
8a57d544ff | ||
|
|
cd77e2a048 | ||
|
|
a52f766815 | ||
|
|
646fd37e16 | ||
|
|
c74ca22023 | ||
|
|
2d170be242 | ||
|
|
079101522d | ||
|
|
1bea98e01b | ||
|
|
7c09221f7c | ||
|
|
6355e910d4 | ||
|
|
891e0320d7 | ||
|
|
486323ae58 | ||
|
|
4ef875aa0b | ||
|
|
e756b8db7a | ||
|
|
8023f8ac8d | ||
|
|
803408904a | ||
|
|
702f9095da | ||
|
|
0b9ee1ae6d | ||
|
|
2f003e08ff | ||
|
|
e090857d6b | ||
|
|
4e29fff5a3 | ||
|
|
e279377696 | ||
|
|
5e52839ce3 | ||
|
|
3ca632a552 | ||
|
|
cfe55357ac | ||
|
|
0f6d768eca | ||
|
|
75330da328 | ||
|
|
a1bcbab0e5 | ||
|
|
db9272032a | ||
|
|
d6d6c655ff | ||
|
|
58acc92790 | ||
|
|
c3074c0977 | ||
|
|
af254f3047 | ||
|
|
c140976eeb | ||
|
|
05c22d4ddc | ||
|
|
3e08938a20 | ||
|
|
5937bb574d | ||
|
|
ded55b26d1 | ||
|
|
d79ed65de0 | ||
|
|
3b71bd05a4 | ||
|
|
e75a7a5dea | ||
|
|
60273ba84f | ||
|
|
c33604f2ef | ||
|
|
eea804b3f6 | ||
|
|
25edf8ac3f | ||
|
|
3ed6dc91dd | ||
|
|
13e5348177 | ||
|
|
90e44c092a | ||
|
|
8027882c1c | ||
|
|
ad9ed33f8d | ||
|
|
ddf5f0cf46 | ||
|
|
76811a992e | ||
|
|
2eac7bf457 | ||
|
|
53cbcd362d | ||
|
|
11c878b847 | ||
|
|
25d5333894 | ||
|
|
4541ca664a | ||
|
|
97422b4148 | ||
|
|
4df07a278d | ||
|
|
efa418c58b | ||
|
|
be40dbc8cc | ||
|
|
0bd7023b66 | ||
|
|
db82456dde | ||
|
|
a51441546c | ||
|
|
0bd323140d | ||
|
|
ad22949925 | ||
|
|
0726ee8995 | ||
|
|
c120004084 | ||
|
|
e15b97ee08 | ||
|
|
bbc7d39928 | ||
|
|
b53c34c3f7 | ||
|
|
d2928d5b31 | ||
|
|
7e76d6de33 | ||
|
|
3eac376a41 | ||
|
|
7084e3af5c | ||
|
|
ac3d12c746 | ||
|
|
41a3352516 | ||
|
|
311ff8caed | ||
|
|
197bbda02e | ||
|
|
56cd8fcc95 | ||
|
|
3c4b42de75 | ||
|
|
e4b8a9d895 | ||
|
|
76d25d3795 | ||
|
|
97a59ca753 | ||
|
|
544bd47e94 | ||
|
|
56916a0321 | ||
|
|
eff83a45cd | ||
|
|
9df1e27191 | ||
|
|
abe25f62d0 | ||
|
|
ad5d26f08a | ||
|
|
dea731a6b2 | ||
|
|
1d19447e8e | ||
|
|
ac938c8738 | ||
|
|
88a1c2a593 | ||
|
|
1e8db87320 | ||
|
|
7382ebce27 | ||
|
|
9988b3d8e6 | ||
|
|
6b69449175 | ||
|
|
85a5fb5a41 | ||
|
|
f5bc901dd7 | ||
|
|
4ac062d09e | ||
|
|
a15ca7259c | ||
|
|
d4ee7972ca | ||
|
|
86f6b657e2 | ||
|
|
ac7906fdea | ||
|
|
4d494f3a1b | ||
|
|
57ff5b25e4 | ||
|
|
28fa4a7571 | ||
|
|
723228598e | ||
|
|
790cdd1d45 | ||
|
|
81e21a551d | ||
|
|
7cf3f6cd19 | ||
|
|
506d95da10 | ||
|
|
36b4683e84 | ||
|
|
2becacd48d | ||
|
|
ae41425c77 | ||
|
|
59837cb513 | ||
|
|
66e5d51329 | ||
|
|
15dfa79708 | ||
|
|
896d46525e | ||
|
|
a8b70b411c | ||
|
|
cd25c4b3c9 | ||
|
|
b28d8361f5 | ||
|
|
b40ba175a3 | ||
|
|
dfa91d87cf | ||
|
|
102c24cc29 | ||
|
|
7d3b6cc8e0 | ||
|
|
9ef7064cc4 | ||
|
|
56f0ff204e | ||
|
|
af896533df | ||
|
|
aa099f3fc0 | ||
|
|
e07a877e73 | ||
|
|
1f675f4bb9 | ||
|
|
e482d74d19 | ||
|
|
50bff3e540 | ||
|
|
0d7c42ba54 | ||
|
|
ec7cbf8e15 | ||
|
|
d60fb2b449 | ||
|
|
dc989dbebc | ||
|
|
09164cae6c | ||
|
|
80f0f779db | ||
|
|
c605e892b6 | ||
|
|
80fe3e1877 | ||
|
|
8f7a7ef6a4 | ||
|
|
2aad4d0ab5 | ||
|
|
df7b0c6682 | ||
|
|
ea1519de82 | ||
|
|
b60067af97 | ||
|
|
22ddd53ea5 | ||
|
|
cafe24da86 | ||
|
|
c33acf749c | ||
|
|
dab3b688f0 | ||
|
|
a34d2c750b | ||
|
|
5210c678b9 | ||
|
|
baf157901c | ||
|
|
e457dd6f6c | ||
|
|
2724aeef32 | ||
|
|
1d3ec93ec7 | ||
|
|
fea3899f26 | ||
|
|
f016acdade | ||
|
|
0c4d5009a2 | ||
|
|
815ba879e6 | ||
|
|
3df86a7918 | ||
|
|
5e7b48c9a2 | ||
|
|
0f248e9149 | ||
|
|
461d8c980f | ||
|
|
9a7fecef06 | ||
|
|
39c63371bf | ||
|
|
80b0e1138c | ||
|
|
3acc0b3af2 | ||
|
|
39174ab969 | ||
|
|
0578b6cf36 | ||
|
|
cc7b283f23 | ||
|
|
4adca869c8 | ||
|
|
80af8ce93c | ||
|
|
c90b020a83 | ||
|
|
052bd6808b | ||
|
|
87507d50c6 | ||
|
|
058d32ec0b | ||
|
|
e830f5d4e7 | ||
|
|
a848090014 | ||
|
|
ecb2601164 | ||
|
|
31e801425f | ||
|
|
d386e24df4 | ||
|
|
a9d352efb2 | ||
|
|
80fb0c90ea | ||
|
|
f3172d6727 | ||
|
|
af0f6e578b | ||
|
|
863e66ecde | ||
|
|
0b07ce6d79 | ||
|
|
02319bcfd7 | ||
|
|
6654601bb1 | ||
|
|
caacba907c | ||
|
|
2f62d1b763 | ||
|
|
ee617d13d5 | ||
|
|
03e0076eec | ||
|
|
33953954a2 | ||
|
|
e0594aa9b5 | ||
|
|
99bb3cff43 | ||
|
|
8295a5cc6b | ||
|
|
f704cd07e6 | ||
|
|
d2df9dbdfb | ||
|
|
eb431308de | ||
|
|
418676ffab | ||
|
|
87b327f52d | ||
|
|
21be93548c | ||
|
|
c2a000c605 | ||
|
|
943f3364e1 | ||
|
|
06c55b348a | ||
|
|
c630e19d0d | ||
|
|
5222d47b5d | ||
|
|
1c1d9c95ef | ||
|
|
7d21318c64 | ||
|
|
1770fb250b | ||
|
|
d555245fd5 | ||
|
|
665b80c048 | ||
|
|
ce6ee88721 | ||
|
|
8d5c15b8f4 | ||
|
|
ecfb009f66 | ||
|
|
cbe862765f | ||
|
|
173bf63617 | ||
|
|
15ef5b425f | ||
|
|
d2ca6715c6 | ||
|
|
19906ded5b | ||
|
|
4283b8ad0d | ||
|
|
cb9eb0a9bb | ||
|
|
3e4e4f30ec | ||
|
|
bc32e7472f | ||
|
|
70b0194218 | ||
|
|
b1edc18faa | ||
|
|
b5e733d29b | ||
|
|
c6dd3727fb | ||
|
|
802da56e14 | ||
|
|
eeed50e6c0 | ||
|
|
bc38c06f37 | ||
|
|
c49e8da3d6 | ||
|
|
b2b5eea343 | ||
|
|
e68fe35a55 | ||
|
|
297a91fde4 | ||
|
|
d6c88cd77a | ||
|
|
00421bb46e | ||
|
|
cefa9d9ba4 | ||
|
|
1295d71988 | ||
|
|
204428cf0a | ||
|
|
f205c1380f | ||
|
|
8875ebc9f8 | ||
|
|
ccbf80312e | ||
|
|
838d77050b | ||
|
|
711e271583 | ||
|
|
d40bbb23cb | ||
|
|
130a5ee0d9 | ||
|
|
074c812592 | ||
|
|
ea428eb722 | ||
|
|
8da5fd9bb7 | ||
|
|
cf486a480e | ||
|
|
a36af99dcd | ||
|
|
421e4ff058 | ||
|
|
4140c9867d | ||
|
|
2988cebaa9 | ||
|
|
6a4d84d42c | ||
|
|
07effbd950 | ||
|
|
4e36713c8b | ||
|
|
2939907c48 | ||
|
|
0b64a813da | ||
|
|
5f06be724c | ||
|
|
2200a27a5f | ||
|
|
4e2700b5d4 | ||
|
|
e56d3a0412 | ||
|
|
1996d191c0 | ||
|
|
bf5cf3256f | ||
|
|
f732e23488 | ||
|
|
a10b0af718 | ||
|
|
09067585fa | ||
|
|
bc4c5d83ce | ||
|
|
7750c359b9 | ||
|
|
f87d16f90f | ||
|
|
fa4b761612 | ||
|
|
3884b07295 | ||
|
|
3379585847 | ||
|
|
5490766972 | ||
|
|
0d3fe64c08 | ||
|
|
2745bab613 | ||
|
|
df0591a831 | ||
|
|
88dbcee873 | ||
|
|
053875f47f | ||
|
|
24063e7a7b | ||
|
|
4d1e65c6b0 | ||
|
|
8c4a7a9b39 | ||
|
|
229d4c167a | ||
|
|
3f362be146 | ||
|
|
57e979c24c | ||
|
|
1582ddf3c4 | ||
|
|
0d8121710f | ||
|
|
8d020fa694 | ||
|
|
04564add01 | ||
|
|
881d052f0d | ||
|
|
14ec5f6e33 | ||
|
|
6389fc5c6d | ||
|
|
eee33e336d | ||
|
|
1570736631 | ||
|
|
d42fabaa7a | ||
|
|
c904fa9092 | ||
|
|
761033ccef | ||
|
|
5166344d33 | ||
|
|
a1cc18c285 | ||
|
|
34a080faa9 | ||
|
|
1deeada249 | ||
|
|
84863acac9 | ||
|
|
ec23932203 | ||
|
|
483872a190 | ||
|
|
457e137dda | ||
|
|
4a06693994 | ||
|
|
0f39f1032a | ||
|
|
4f12660c1b | ||
|
|
832564864f | ||
|
|
51638bc6f5 | ||
|
|
8ea474a3c9 | ||
|
|
689c498549 | ||
|
|
eb2a716661 | ||
|
|
0af3836f74 | ||
|
|
11b0c2848c | ||
|
|
636968d381 | ||
|
|
7eb211eb94 | ||
|
|
5cedfb8ead | ||
|
|
c20a49c531 | ||
|
|
874fe69683 | ||
|
|
cfefec06a9 | ||
|
|
0a7d14040d | ||
|
|
974ab11b76 | ||
|
|
7d40d228da | ||
|
|
c73ea2de1d | ||
|
|
c308165816 | ||
|
|
38720b2e46 | ||
|
|
e7b2e9f639 | ||
|
|
cbaf8a0bc8 | ||
|
|
f5861aa708 | ||
|
|
2fba6abc8d | ||
|
|
f762188b89 | ||
|
|
d987416c9b | ||
|
|
246392f0f6 | ||
|
|
0693a6dd70 | ||
|
|
4fa33f300b | ||
|
|
2c8e9fa9ac | ||
|
|
6ac3e8ec45 | ||
|
|
c66b444213 | ||
|
|
64be0913ad | ||
|
|
ec70110ab2 | ||
|
|
93ad11095a | ||
|
|
26af75061e | ||
|
|
83fb5fb388 | ||
|
|
6dede28f72 | ||
|
|
800c4b1d48 | ||
|
|
3a9196ce18 | ||
|
|
76c5df087a | ||
|
|
6368a3e548 | ||
|
|
cde040e10f | ||
|
|
477ca61da5 | ||
|
|
2b23812e6e | ||
|
|
b53ad60b48 | ||
|
|
db92f0b569 | ||
|
|
456ae3ab84 | ||
|
|
9347f4157e | ||
|
|
3b447a19ea | ||
|
|
a70cab7ac1 | ||
|
|
4b2aecc9bc | ||
|
|
9cb445a71a | ||
|
|
bcd9cd6cd7 | ||
|
|
16ff90c070 | ||
|
|
925f9486e3 | ||
|
|
b439424cef | ||
|
|
12423e9358 | ||
|
|
bb82919131 | ||
|
|
be4f9d85ae | ||
|
|
275b9e194d | ||
|
|
44087fa1b5 | ||
|
|
d3043461a6 | ||
|
|
3f7a0d3c97 | ||
|
|
361931f104 | ||
|
|
641b7fd060 | ||
|
|
17bcb3bdbf | ||
|
|
7520a8a4f8 | ||
|
|
8734f287eb | ||
|
|
7552a3610f | ||
|
|
f4387d5394 | ||
|
|
ffa0c340b8 | ||
|
|
34aad3f0df | ||
|
|
49e022a2c0 | ||
|
|
317d2a8aa8 | ||
|
|
f52589c128 | ||
|
|
75d2c57688 | ||
|
|
92d0df1412 | ||
|
|
34bb60f064 | ||
|
|
1206926ac2 | ||
|
|
958096aaa8 | ||
|
|
2c8c2fcd64 | ||
|
|
d7b3a5c6eb | ||
|
|
5c7a3329f3 | ||
|
|
a0b5af0dae | ||
|
|
37c25383b7 | ||
|
|
2498f60c57 | ||
|
|
7e390e76d0 | ||
|
|
ce7d02c94a | ||
|
|
b578de77f7 | ||
|
|
d055efba3c | ||
|
|
93bc108a24 | ||
|
|
3fd528de2b | ||
|
|
1844be638b | ||
|
|
dca1996640 | ||
|
|
cb3656b45c | ||
|
|
5c2eaf202e | ||
|
|
5abba4f85b | ||
|
|
22b77f5b34 | ||
|
|
c76db90437 | ||
|
|
080f7ff4e0 | ||
|
|
c5730c8f5f | ||
|
|
d46f284d9f | ||
|
|
221f19ae15 | ||
|
|
550cb277df | ||
|
|
9c79af9340 | ||
|
|
1ba52c8880 | ||
|
|
92629067f7 | ||
|
|
93a808e65a | ||
|
|
bf99b251f8 | ||
|
|
f491540636 | ||
|
|
7f84f7d541 | ||
|
|
42b01f7126 | ||
|
|
7e5c17939b | ||
|
|
d6937ec629 | ||
|
|
f5a32f47d3 | ||
|
|
316fcc6126 | ||
|
|
e163177a12 | ||
|
|
1fe257c71e | ||
|
|
1eaa813f28 | ||
|
|
924dad8980 | ||
|
|
1ba10a1a20 | ||
|
|
ab02e10791 | ||
|
|
dd94e5e5c3 | ||
|
|
1fcb90c4d9 | ||
|
|
523c7ddf82 | ||
|
|
3577a68d2d | ||
|
|
d963f5fcc5 | ||
|
|
bed82d68df | ||
|
|
359271dfa8 | ||
|
|
0af77a0706 | ||
|
|
e6efd79ad8 | ||
|
|
c953934d2e | ||
|
|
5b4742d42b | ||
|
|
269f70df51 | ||
|
|
67177f933b | ||
|
|
606fdcded7 | ||
|
|
70b9db68b4 | ||
|
|
dc8a2f5d62 | ||
|
|
8830cf9556 | ||
|
|
bfb558eb92 | ||
|
|
505866a4c6 | ||
|
|
acd2de80fb | ||
|
|
0b08bf4537 | ||
|
|
223091482c | ||
|
|
4699946e1b | ||
|
|
097f87fd52 | ||
|
|
66b4f3a685 | ||
|
|
02116d4bfc | ||
|
|
fb17589af6 | ||
|
|
15ce7ea880 | ||
|
|
57a3123a55 | ||
|
|
32e96e4bb2 | ||
|
|
47ee26a77a | ||
|
|
9cd5d52fbc | ||
|
|
aa2afcd47b | ||
|
|
fd510e7933 | ||
|
|
e29d5b9634 | ||
|
|
2f9891b15b | ||
|
|
c3ecd615ff | ||
|
|
4e22a3cb21 | ||
|
|
bc98b65190 | ||
|
|
02b756ef40 | ||
|
|
954706570c | ||
|
|
e2faf6970f | ||
|
|
a528ae9c12 | ||
|
|
0a5871eba4 | ||
|
|
27471d5249 | ||
|
|
ed484c00db | ||
|
|
b868f26ca4 | ||
|
|
d7c04ae24c | ||
|
|
5bcf8c40e0 | ||
|
|
b8e30ed953 | ||
|
|
e3adb30ca7 | ||
|
|
1e8c570c8a | ||
|
|
40f2220f1d | ||
|
|
4da779c44c | ||
|
|
b54a5a3e25 | ||
|
|
f572bcff58 | ||
|
|
d47b7e6128 | ||
|
|
8d9e4faae9 | ||
|
|
cf630055b0 | ||
|
|
a5870c894f | ||
|
|
c236ee99d4 | ||
|
|
130e242aa9 | ||
|
|
39f0a17d0d | ||
|
|
da0682afa7 | ||
|
|
3c755a2002 | ||
|
|
66f64fbf15 | ||
|
|
4c0a0e09e2 | ||
|
|
f8de6084ed | ||
|
|
36624f9d89 | ||
|
|
cbf1e3419b | ||
|
|
7c8cc41d4c | ||
|
|
5dbbd0a76f | ||
|
|
da7be2e3ca | ||
|
|
d138c10eb6 | ||
|
|
4af13eba60 | ||
|
|
e998e499db | ||
|
|
53cdced69b | ||
|
|
e726da46a5 | ||
|
|
b0d6a7092e | ||
|
|
1fe870c0d7 | ||
|
|
469cf1d164 | ||
|
|
00299707e5 | ||
|
|
9f5a718323 | ||
|
|
231d86e249 | ||
|
|
5fb8b3f73c | ||
|
|
6fc345f555 | ||
|
|
c9d5f2ec9e | ||
|
|
b0a71612b7 | ||
|
|
88ff4b28b9 | ||
|
|
6905029c17 | ||
|
|
5d75dc02b1 | ||
|
|
0e78172665 | ||
|
|
3ad036aacb | ||
|
|
a364f8ab49 | ||
|
|
7b6b945cbf | ||
|
|
6a7df88cf4 | ||
|
|
a7c7523a8c | ||
|
|
36589f75f4 | ||
|
|
b3af671803 | ||
|
|
5dde0f6bd8 | ||
|
|
2343818ab5 | ||
|
|
11bb1e3e56 | ||
|
|
3dbb6f3002 | ||
|
|
0591f2bcc5 | ||
|
|
a48151f07a | ||
|
|
82fda1c7af | ||
|
|
d108129972 | ||
|
|
6124ab1b3e | ||
|
|
e294a315fc | ||
|
|
3baed5295e | ||
|
|
a5334ecde7 | ||
|
|
9f35bad93f | ||
|
|
972f639051 | ||
|
|
99cc4f8d39 | ||
|
|
273f99b293 | ||
|
|
48ef2e919e | ||
|
|
65b1b083ee | ||
|
|
c0367ed595 | ||
|
|
062b13e92a | ||
|
|
39b9f4d31a | ||
|
|
39701d0455 | ||
|
|
6fbf78ef52 | ||
|
|
7e0cd01758 | ||
|
|
76bf77eded | ||
|
|
97189f300e | ||
|
|
078b2339bd | ||
|
|
4915490cbb | ||
|
|
f65859bc25 | ||
|
|
543ae3cf13 | ||
|
|
610a4510cf | ||
|
|
7b92977889 | ||
|
|
a17806c37c | ||
|
|
d6b87b2047 | ||
|
|
9d921f65f3 | ||
|
|
32ba7f468c | ||
|
|
9cddd4c368 | ||
|
|
8db465c699 | ||
|
|
eb1caf2231 | ||
|
|
adb84f3d03 | ||
|
|
67acd29541 | ||
|
|
41976122d5 | ||
|
|
d5b973c15a | ||
|
|
3a6892a011 | ||
|
|
b092e1bc7d | ||
|
|
9d7d1989e9 | ||
|
|
7d4dd0fdd5 | ||
|
|
364415f83a | ||
|
|
d633f6b299 | ||
|
|
bf2fe9d33f | ||
|
|
c2fde1ddc0 | ||
|
|
ebb47580a0 | ||
|
|
4e533c90c2 | ||
|
|
9a14134f65 | ||
|
|
e58bdab492 | ||
|
|
279b01a180 | ||
|
|
f046884ae0 | ||
|
|
f4ce59650d | ||
|
|
bc49af56c0 | ||
|
|
3e7116c427 | ||
|
|
12b3ec6989 | ||
|
|
e536cc183a | ||
|
|
63348fa903 | ||
|
|
ab00aef75e | ||
|
|
653f9d3913 | ||
|
|
21c3fb905b | ||
|
|
4353f736d4 | ||
|
|
f3712313ba | ||
|
|
b2d8fcb36e | ||
|
|
81021839d5 | ||
|
|
7c36dd6c56 | ||
|
|
687e0b0dea | ||
|
|
834e22d7b1 | ||
|
|
1e29f98924 | ||
|
|
933f3da538 | ||
|
|
a7fa2fd256 | ||
|
|
0de1a3b20a | ||
|
|
bcc114ec60 | ||
|
|
1148fae419 | ||
|
|
d7188c29f8 | ||
|
|
4b97732659 | ||
|
|
8ff98cc6e1 | ||
|
|
cf0c324a74 | ||
|
|
69119a21cd | ||
|
|
16fa41d25b | ||
|
|
4e170c2033 | ||
|
|
3fbbe940a1 | ||
|
|
271b57e5c5 | ||
|
|
df0e2e4015 | ||
|
|
9fa622d63b | ||
|
|
fed2149174 | ||
|
|
aa4291183b | ||
|
|
0d3fc9648f | ||
|
|
339138b576 | ||
|
|
666769f9d9 | ||
|
|
8fc594bd2b | ||
|
|
8cf8ad7e24 | ||
|
|
0818f698e6 | ||
|
|
44a33b0a5f | ||
|
|
85078bdb66 | ||
|
|
30a3dba7ad | ||
|
|
0afc671723 | ||
|
|
12e7447e9f | ||
|
|
b675e79b89 | ||
|
|
febdccfb58 | ||
|
|
54835a0d93 | ||
|
|
423fe3487c | ||
|
|
98cb3f7950 | ||
|
|
371492bf5c | ||
|
|
7df831e96d | ||
|
|
f0be1bd251 | ||
|
|
948341a885 | ||
|
|
1b2bf8ce0e | ||
|
|
4f68a0f634 | ||
|
|
e785abeb8f | ||
|
|
3acca5095e | ||
|
|
5a62286127 | ||
|
|
5452aff0be | ||
|
|
0f4f1262af | ||
|
|
4e7f68ccba | ||
|
|
96c05babe0 | ||
|
|
6b78cddb19 | ||
|
|
12596ff936 | ||
|
|
a240f1b2b9 | ||
|
|
f6da19672e | ||
|
|
e81d30be9b | ||
|
|
13b090e3bd | ||
|
|
d0479e6ddc | ||
|
|
1432827006 | ||
|
|
89bf4eac71 | ||
|
|
07aac0bdae | ||
|
|
61220b8d0d | ||
|
|
4e470aaf09 | ||
|
|
76922b620b | ||
|
|
ac0f623eda | ||
|
|
afd5450882 | ||
|
|
c17266fc21 | ||
|
|
f62c825495 | ||
|
|
45f44dd4be | ||
|
|
9c0daebfe0 | ||
|
|
f2a0ca5609 | ||
|
|
e04d315853 | ||
|
|
daec697658 | ||
|
|
7ad5670710 | ||
|
|
8036423373 | ||
|
|
8039228a9d | ||
|
|
b1103af429 | ||
|
|
d67ad5acfd | ||
|
|
c9df9c17b7 | ||
|
|
8fa46b02b0 | ||
|
|
92c481330d | ||
|
|
2664db3e40 | ||
|
|
6b554e5f4e | ||
|
|
2bc0dff135 | ||
|
|
c61795f71a | ||
|
|
a8a7491bf0 | ||
|
|
d1c7c4fcaf | ||
|
|
3dbbcf8918 | ||
|
|
0cfd1ad05f | ||
|
|
bfa4e233b7 | ||
|
|
dc662da3d6 | ||
|
|
4107d2422b | ||
|
|
b83eac5dc2 | ||
|
|
bb51837c56 | ||
|
|
5bbb4fe1a1 | ||
|
|
b5c20e9b46 | ||
|
|
1e1ab636e0 | ||
|
|
7348553897 | ||
|
|
5f65a5128a | ||
|
|
d9c12e7271 | ||
|
|
fb2d6b4aff | ||
|
|
3f4ec0ab22 | ||
|
|
ac938fe8a3 | ||
|
|
dc1e8796fb | ||
|
|
8c5d59c60c | ||
|
|
13558b7ce8 | ||
|
|
3b2b3dacf5 | ||
|
|
1b2f89995c | ||
|
|
0479fcdf82 | ||
|
|
a04465466d | ||
|
|
670921df90 | ||
|
|
18f7e17d7a | ||
|
|
a2aede0441 | ||
|
|
1d876c7729 | ||
|
|
70465a0520 | ||
|
|
3c9845719f | ||
|
|
a36cc8946c | ||
|
|
9567b539d1 | ||
|
|
fdf9515de2 | ||
|
|
b320238487 | ||
|
|
7872a60499 | ||
|
|
5fea3846d1 | ||
|
|
86a4e0baaa | ||
|
|
917f1df250 | ||
|
|
11260f2476 | ||
|
|
38d20d0f04 | ||
|
|
938f40e817 | ||
|
|
5f9c8b862e | ||
|
|
d96d67bb9a | ||
|
|
19c3b87676 | ||
|
|
7d08ab4e2b | ||
|
|
efea09d216 | ||
|
|
2adaef547b | ||
|
|
d71bbf0d9a | ||
|
|
36b3b2b853 | ||
|
|
63dafa08cc | ||
|
|
afddbbe875 | ||
|
|
34811cafca | ||
|
|
8a2851169c | ||
|
|
1a85a2d805 | ||
|
|
cf2d00e1e0 | ||
|
|
266c948add | ||
|
|
dd564ee9dd | ||
|
|
b9d48cabb9 | ||
|
|
276e0674c3 | ||
|
|
f3c4a69327 | ||
|
|
1c60a69866 | ||
|
|
050f937a24 | ||
|
|
b24e478224 | ||
|
|
e34380ed76 | ||
|
|
338a1b826f | ||
|
|
b87c63c897 | ||
|
|
f6ed150bb7 | ||
|
|
d5c683bc76 | ||
|
|
3ac95f3f84 | ||
|
|
3be872ad32 | ||
|
|
80964a79b2 | ||
|
|
e719035ea5 | ||
|
|
f2bcf63b26 | ||
|
|
2c9843f1a0 | ||
|
|
0560406d81 | ||
|
|
db8083c657 | ||
|
|
fcc9f50b3f | ||
|
|
c1ffbad8d8 | ||
|
|
d7cdb1a5af | ||
|
|
6c41ce5e08 | ||
|
|
685a814f38 | ||
|
|
06b64047a4 | ||
|
|
8a90b32446 | ||
|
|
7e47b1ebae | ||
|
|
150b51a3ac | ||
|
|
0002f8db9e | ||
|
|
59ed50d230 | ||
|
|
820b4bac1a | ||
|
|
19c5ecdbfa | ||
|
|
66b0702c24 | ||
|
|
5b5dc0efc4 | ||
|
|
0febf05160 | ||
|
|
ee924bda6e | ||
|
|
ecfe571e75 | ||
|
|
173ecd9397 | ||
|
|
ba3db35ba0 | ||
|
|
074053dac7 | ||
|
|
6bc98f91b1 |
124
.argo-ci/ci.yaml
@@ -10,29 +10,94 @@ spec:
|
||||
value: master
|
||||
- name: repo
|
||||
value: https://github.com/argoproj/argo-cd.git
|
||||
volumes:
|
||||
- name: k3setc
|
||||
emptyDir: {}
|
||||
- name: k3svar
|
||||
emptyDir: {}
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
|
||||
templates:
|
||||
- name: argo-cd-ci
|
||||
steps:
|
||||
- - name: build
|
||||
template: ci-dind
|
||||
arguments:
|
||||
parameters:
|
||||
- name: cmd
|
||||
value: "{{item}}"
|
||||
withItems:
|
||||
- make controller-image
|
||||
- make server-image
|
||||
- make repo-server-image
|
||||
- - name: build-e2e
|
||||
template: build-e2e
|
||||
|
||||
- name: test
|
||||
template: ci-builder
|
||||
arguments:
|
||||
parameters:
|
||||
- name: cmd
|
||||
value: "{{item}}"
|
||||
withItems:
|
||||
- dep ensure && make lint
|
||||
- dep ensure && make test
|
||||
value: "dep ensure && make lint test && bash <(curl -s https://codecov.io/bash) -f coverage.out"
|
||||
|
||||
# The step builds argo cd image, deploy argo cd components into throw-away kubernetes cluster provisioned using k3s and run e2e tests against it.
|
||||
- name: build-e2e
|
||||
inputs:
|
||||
artifacts:
|
||||
- name: code
|
||||
path: /go/src/github.com/argoproj/argo-cd
|
||||
git:
|
||||
repo: "{{workflow.parameters.repo}}"
|
||||
revision: "{{workflow.parameters.revision}}"
|
||||
container:
|
||||
image: argoproj/argo-cd-ci-builder:v0.13.1
|
||||
imagePullPolicy: Always
|
||||
command: [sh, -c]
|
||||
# Main contains build argocd image. The image is saved it into k3s agent images directory so it could be preloaded by the k3s cluster.
|
||||
args: ["
|
||||
dep ensure && until docker ps; do sleep 3; done && \
|
||||
make image DEV_IMAGE=true && mkdir -p /var/lib/rancher/k3s/agent/images && \
|
||||
docker save argocd:latest > /var/lib/rancher/k3s/agent/images/argocd.tar && \
|
||||
touch /var/lib/rancher/k3s/ready && until ls /etc/rancher/k3s/k3s.yaml; do sleep 3; done && \
|
||||
kubectl create ns argocd-e2e && kustomize build ./test/manifests/ci | kubectl apply -n argocd-e2e -f - && \
|
||||
kubectl rollout status deployment -n argocd-e2e argocd-application-controller && kubectl rollout status deployment -n argocd-e2e argocd-server && \
|
||||
git config --global user.email \"test@example.com\" && \
|
||||
export ARGOCD_SERVER=$(kubectl get service argocd-server -o=jsonpath={.spec.clusterIP} -n argocd-e2e):443 && make test-e2e"
|
||||
]
|
||||
workingDir: /go/src/github.com/argoproj/argo-cd
|
||||
env:
|
||||
- name: USER
|
||||
value: argocd
|
||||
- name: DOCKER_HOST
|
||||
value: 127.0.0.1
|
||||
- name: DOCKER_BUILDKIT
|
||||
value: "1"
|
||||
- name: KUBECONFIG
|
||||
value: /etc/rancher/k3s/k3s.yaml
|
||||
volumeMounts:
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: k3setc
|
||||
mountPath: /etc/rancher/k3s
|
||||
- name: k3svar
|
||||
mountPath: /var/lib/rancher/k3s
|
||||
sidecars:
|
||||
- name: dind
|
||||
image: docker:18.09-dind
|
||||
securityContext:
|
||||
privileged: true
|
||||
resources:
|
||||
requests:
|
||||
memory: 2048Mi
|
||||
cpu: 500m
|
||||
mirrorVolumeMounts: true
|
||||
|
||||
# Steps waits for file /var/lib/rancher/k3s/ready which indicates that all required images are ready, then starts the cluster.
|
||||
- name: k3s
|
||||
image: rancher/k3s:v0.3.0-rc1
|
||||
imagePullPolicy: Always
|
||||
command: [sh, -c]
|
||||
args: ["until ls /var/lib/rancher/k3s/ready; do sleep 3; done && k3s server || true"]
|
||||
securityContext:
|
||||
privileged: true
|
||||
volumeMounts:
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
- name: k3setc
|
||||
mountPath: /etc/rancher/k3s
|
||||
- name: k3svar
|
||||
mountPath: /var/lib/rancher/k3s
|
||||
|
||||
- name: ci-builder
|
||||
inputs:
|
||||
@@ -45,10 +110,23 @@ spec:
|
||||
repo: "{{workflow.parameters.repo}}"
|
||||
revision: "{{workflow.parameters.revision}}"
|
||||
container:
|
||||
image: argoproj/argo-cd-ci-builder:latest
|
||||
command: [sh, -c]
|
||||
image: argoproj/argo-cd-ci-builder:v0.13.1
|
||||
imagePullPolicy: Always
|
||||
command: [bash, -c]
|
||||
args: ["{{inputs.parameters.cmd}}"]
|
||||
workingDir: /go/src/github.com/argoproj/argo-cd
|
||||
env:
|
||||
- name: CODECOV_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: codecov-token
|
||||
key: codecov-token
|
||||
resources:
|
||||
requests:
|
||||
memory: 1024Mi
|
||||
cpu: 200m
|
||||
archiveLocation:
|
||||
archiveLogs: true
|
||||
|
||||
- name: ci-dind
|
||||
inputs:
|
||||
@@ -61,17 +139,25 @@ spec:
|
||||
repo: "{{workflow.parameters.repo}}"
|
||||
revision: "{{workflow.parameters.revision}}"
|
||||
container:
|
||||
image: argoproj/argo-cd-ci-builder:latest
|
||||
image: argoproj/argo-cd-ci-builder:v0.13.1
|
||||
imagePullPolicy: Always
|
||||
command: [sh, -c]
|
||||
args: ["until docker ps; do sleep 3; done && {{inputs.parameters.cmd}}"]
|
||||
workingDir: /go/src/github.com/argoproj/argo-cd
|
||||
env:
|
||||
- name: DOCKER_HOST
|
||||
value: 127.0.0.1
|
||||
- name: DOCKER_BUILDKIT
|
||||
value: "1"
|
||||
resources:
|
||||
requests:
|
||||
memory: 1024Mi
|
||||
cpu: 200m
|
||||
sidecars:
|
||||
- name: dind
|
||||
image: docker:17.10-dind
|
||||
image: docker:18.09-dind
|
||||
securityContext:
|
||||
privileged: true
|
||||
mirrorVolumeMounts: true
|
||||
|
||||
archiveLocation:
|
||||
archiveLogs: true
|
||||
|
||||
7
.codecov.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
ignore:
|
||||
- "**/*.pb.go"
|
||||
- "**/*.pb.gw.go"
|
||||
- "**/*_test.go"
|
||||
- "pkg/apis/.*"
|
||||
- "pkg/client/.*"
|
||||
- "test/.*"
|
||||
@@ -1,5 +1,12 @@
|
||||
# Prevent vendor directory from being copied to ensure we are not not pulling unexpected cruft from
|
||||
# a user's workspace, and are only building off of what is locked by dep.
|
||||
vendor
|
||||
Dockerfile-argocd
|
||||
dist
|
||||
.vscode/
|
||||
.idea/
|
||||
.DS_Store
|
||||
vendor/
|
||||
dist/
|
||||
*.iml
|
||||
# delve debug binaries
|
||||
cmd/**/debug
|
||||
debug.test
|
||||
coverage.out
|
||||
|
||||
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: 'enhancement'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
1
.github/no-response.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
# See https://github.com/probot/no-response
|
||||
1
.github/stale.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
# See https://github.com/probot/stale
|
||||
2
.gitignore
vendored
@@ -3,7 +3,9 @@
|
||||
.DS_Store
|
||||
vendor/
|
||||
dist/
|
||||
site/
|
||||
*.iml
|
||||
# delve debug binaries
|
||||
cmd/**/debug
|
||||
debug.test
|
||||
coverage.out
|
||||
|
||||
21
.golangci.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
run:
|
||||
deadline: 8m
|
||||
skip-files:
|
||||
- ".*\\.pb\\.go"
|
||||
skip-dirs:
|
||||
- pkg/client
|
||||
- vendor
|
||||
linter-settings:
|
||||
goimports:
|
||||
local-prefixes: github.com/argoproj/argo-cd
|
||||
linters:
|
||||
enable:
|
||||
- vet
|
||||
- gofmt
|
||||
- goimports
|
||||
- deadcode
|
||||
- varcheck
|
||||
- structcheck
|
||||
- ineffassign
|
||||
- unconvert
|
||||
- misspell
|
||||
773
CHANGELOG.md
Normal file
@@ -0,0 +1,773 @@
|
||||
# Changelog
|
||||
|
||||
## v1.0.0
|
||||
|
||||
### New Features
|
||||
|
||||
#### Network View
|
||||
|
||||
TODO
|
||||
|
||||
#### 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
|
||||
|
||||
* 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.
|
||||
|
||||
### 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
|
||||
|
||||
#### 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
|
||||
|
||||
## 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)
|
||||
- Fix EncodeX509KeyPair function so it takes in account chained certificates (#1137) (@amarruedo)
|
||||
- Exclude metrics.k8s.io from watch (#1128)
|
||||
- Fix issue where dex restart could cause login failures (#1114)
|
||||
- Relax ingress/service health check to accept non-empty ingress list (#1053)
|
||||
- [UI] Correctly handle empty response from repository/<repo>/apps API
|
||||
|
||||
## v0.11.1 (2019-01-18)
|
||||
+ Allow using redis as a cache in repo-server (#1020)
|
||||
- Fix controller deadlock when checking for stale cache (#1044)
|
||||
- Namespaces are not being sorted during apply (#1038)
|
||||
- Controller cache was susceptible to clock skew in managed cluster
|
||||
- Fix ability to unset ApplicationSource specific parameters
|
||||
- Fix force resource delete API (#1033)
|
||||
- Incorrect PermissionDenied error during app creation when using project roles + user-defined RBAC (#1019)
|
||||
- Fix `kubctl convert` issue preventing deployment of extensions/NetworkPolicy (#1012)
|
||||
- Do not allow metadata.creationTimestamp to affect sync status (#1021)
|
||||
- Graceful handling of clusters where API resource discovery is partially successful (#1018)
|
||||
- Handle k8s resources circular dependency (#1016)
|
||||
- Fix `app diff --local` command (#1008)
|
||||
|
||||
## v0.11.0 (2019-01-10)
|
||||
This is Argo CD's biggest release ever and introduces a completely redesigned controller architecture.
|
||||
|
||||
### New Features
|
||||
|
||||
#### Performance & Scalability
|
||||
The application controller has a completely redesigned architecture which improved performance and
|
||||
scalability during application reconciliation.
|
||||
|
||||
This was achieved by introducing an in-memory, live state cache of lightweight Kubernetes object
|
||||
metadata. During reconciliation, the controller no longer performs expensive, in-line queries of app
|
||||
related resources in K8s API server, instead relying on the metadata available in the live state
|
||||
cache. This dramatically improves performance and responsiveness, and is less burdensome to the K8s
|
||||
API server.
|
||||
|
||||
#### Object relationship visualization for CRDs
|
||||
With the new controller design, Argo CD is now able to understand ownership relationship between
|
||||
*all* Kubernetes objects, not just the built-in types. This enables Argo CD to visualize
|
||||
parent/child relationships between all kubernetes objects, including CRDs.
|
||||
|
||||
#### Multi-namespaced applications
|
||||
During sync, Argo CD will now honor any explicitly set namespace in a manifest. Manifests without a
|
||||
namespace will continue deploy to the "preferred" namespace, as specified in app's
|
||||
`spec.destination.namespace`. This enables support for a class of applications which install to
|
||||
multiple namespaces. For example, Argo CD can now install the
|
||||
[prometheus-operator](https://github.com/helm/charts/tree/master/stable/prometheus-operator)
|
||||
helm chart, which deploys some resources into `kube-system`, and others into the
|
||||
`prometheus-operator` namespace.
|
||||
|
||||
#### Large application support
|
||||
Full resource objects are no longer stored in the Application CRD object status. Instead, only
|
||||
lightweight metadata is stored in the status, such as a resource's sync and health status.
|
||||
This change enabled Argo CD to support applications with a very large number of resources
|
||||
(e.g. istio), and reduces the bandwidth requirements when listing applications in the UI.
|
||||
|
||||
#### Resource lifecycle hook improvements
|
||||
Resource lifecycle hooks (e.g. PreSync, PostSync) are now visible/manageable from the UI.
|
||||
Additionally, bare Pods with a restart policy of Never can now be used as a resource hook, as an
|
||||
alternative to Jobs, Workflows.
|
||||
|
||||
#### K8s recommended application labels
|
||||
The tracking label for resources has been changed to use `app.kubernetes.io/instance`, as
|
||||
recommended in [Kubernetes recommended labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/),
|
||||
(changed from `applications.argoproj.io/app-name`). This will enable applications managed by Argo CD
|
||||
to interoperate with other tooling which are also converging on this labeling, such as the
|
||||
Kubernetes dashboard. Additionally, Argo CD no longer injects any tracking labels at the
|
||||
`spec.template.metadata` level.
|
||||
|
||||
#### External OIDC provider support
|
||||
Argo CD now supports auth delegation to an existing, external OIDC providers without the need for
|
||||
running Dex (e.g. Okta, OneLogin, Auth0, Microsoft, etc...)
|
||||
|
||||
The optional, [Dex IDP OIDC provider](https://github.com/dexidp/dex) is still bundled as part of the
|
||||
default installation, in order to provide a seamless out-of-box experience, enabling Argo CD to
|
||||
integrate with non-OIDC providers, and to benefit from Dex's full range of
|
||||
[connectors](https://github.com/dexidp/dex/tree/master/Documentation/connectors).
|
||||
|
||||
#### OIDC group bindings to Project Roles
|
||||
OIDC group claims from an OAuth2 provider can now be bound to a Argo CD project roles. Previously,
|
||||
group claims could only be managed in the centralized ConfigMap, `argocd-rbac-cm`. They can now be
|
||||
managed at a project level. This enables project admins to self service access to applications
|
||||
within a project.
|
||||
|
||||
#### Declarative Argo CD configuration
|
||||
Argo CD settings can be now be configured either declaratively, or imperatively. The `argocd-cm`
|
||||
ConfigMap now has a `repositories` field, which can reference credentials in a normal Kubernetes
|
||||
secret which you can create declaratively, outside of Argo CD.
|
||||
|
||||
#### Helm repository support
|
||||
Helm repositories can be configured at the system level, enabling the deployment of helm charts
|
||||
which have a dependency to external helm repositories.
|
||||
|
||||
### Breaking changes:
|
||||
|
||||
* Argo CD's resource names were renamed for consistency. For example, the application-controller
|
||||
deployment was renamed to argocd-application-controller. When upgrading from v0.10 to v0.11,
|
||||
the older resources should be pruned to avoid inconsistent state and controller in-fighting.
|
||||
|
||||
* As a consequence to moving to recommended kubernetes labels, when upgrading from v0.10 to v0.11,
|
||||
all applications will immediately be OutOfSync due to the change in tracking labels. This will
|
||||
correct itself with another sync of the application. However, since Pods will be recreated, please
|
||||
take this into consideration, especially if your applications are configured with auto-sync.
|
||||
|
||||
* There was significant reworking of the `app.status` fields to reduce the payload size, simplify
|
||||
the datastructure and remove fields which were no longer used by the controller. No breaking
|
||||
changes were made in `app.spec`.
|
||||
|
||||
* An older Argo CD CLI (v0.10 and below) will not be compatible with Argo CD v0.11. To keep
|
||||
CI pipelines in sync with the API server, it is recommended to have pipelines download the CLI
|
||||
directly from the API server https://${ARGOCD_SERVER}/download/argocd-linux-amd64 during the CI
|
||||
pipeline.
|
||||
|
||||
### Changes since v0.10:
|
||||
* Improve Application state reconciliation performance (#806)
|
||||
* Refactor, consolidate and rename resource type data structures
|
||||
+ Declarative setup and configuration of ArgoCD (#536)
|
||||
+ Declaratively add helm repositories (#747)
|
||||
+ Switch to k8s recommended app.kubernetes.io/instance label (#857)
|
||||
+ Ability for a single application to deploy into multiple namespaces (#696)
|
||||
+ Self service group access to project applications (#742)
|
||||
+ Support for Pods as a sync hook (#801)
|
||||
+ Support 'crd-install' helm hook (#355)
|
||||
+ Use external 'diff' utility to render actual vs target state difference
|
||||
+ Show sync policy in app list view
|
||||
* Remove resources state from application CRD (#758)
|
||||
* API server & UI should serve argocd binaries instead of linking to GitHub (#716)
|
||||
* Update versions for kubectl (v1.13.1), helm (v2.12.1), ksonnet (v0.13.1)
|
||||
* Update version of aws-iam-authenticator (0.4.0-alpha.1)
|
||||
* Ability to force refresh of application manifests from git
|
||||
* Improve diff assessment for Secrets, ClusterRoles, Roles
|
||||
- Failed to deploy helm chart with local dependencies and no internet access (#786)
|
||||
- Out of sync reported if Secrets with stringData are used (#763)
|
||||
- Unable to delete application in K8s v1.12 (#718)
|
||||
|
||||
## v0.10.6 (2018-11-14)
|
||||
- Fix issue preventing in-cluster app sync due to go-client changes (issue #774)
|
||||
|
||||
## v0.10.5 (2018-11-13)
|
||||
+ Increase concurrency of application controller
|
||||
* Update dependencies to k8s v1.12 and client-go v9.0 (#729)
|
||||
- add argo cluster permission to view logs (#766) (@conorfennell)
|
||||
- Fix issue where applications could not be deleted on k8s v1.12
|
||||
- Allow 'syncApplication' action to reference target revision rather then hard-coding to 'HEAD' (#69) (@chrisgarland)
|
||||
- Issue #768 - Fix application wizard crash
|
||||
|
||||
## v0.10.4 (2018-11-07)
|
||||
* Upgrade to Helm v0.11.0 (@amarrella)
|
||||
- Health check is not discerning apiVersion when assessing CRDs (issue #753)
|
||||
- Fix nil pointer dereference in util/health (@mduarte)
|
||||
|
||||
## v0.10.3 (2018-10-28)
|
||||
* Fix applying TLS version settings
|
||||
* Update to kustomize 1.0.10 (@twz123)
|
||||
|
||||
## v0.10.2 (2018-10-25)
|
||||
* Update to kustomize 1.0.9 (@twz123)
|
||||
- Fix app refresh err when k8s patch is too slow
|
||||
|
||||
## v0.10.1 (2018-10-24)
|
||||
|
||||
- Handle case where OIDC settings become invalid after dex server restart (issue #710)
|
||||
- git clean also needs to clean files under gitignore (issue #711)
|
||||
|
||||
## v0.10.0 (2018-10-19)
|
||||
|
||||
### Changes since v0.9:
|
||||
|
||||
+ Allow more fine-grained sync (issue #508)
|
||||
+ Display init container logs (issue #681)
|
||||
+ Redirect to /auth/login instead of /login when SSO token is used for authenticaion (issue #348)
|
||||
+ Support ability to use a helm values files from a URL (issue #624)
|
||||
+ Support public not-connected repo in app creation UI (issue #426)
|
||||
+ Use ksonnet CLI instead of ksonnet libs (issue #626)
|
||||
+ We should be able to select the order of the `yaml` files while creating a Helm App (#664)
|
||||
* Remove default params from app history (issue #556)
|
||||
* Update to ksonnet v0.13.0
|
||||
* Update to kustomize 1.0.8
|
||||
- API Server fails to return apps due to grpc max message size limit (issue #690)
|
||||
- App Creation UI for Helm Apps shows only files prefixed with `values-` (issue #663)
|
||||
- App creation UI should allow specifying values files outside of helm app directory bug (issue #658)
|
||||
- argocd-server logs credentials in plain text when adding git repositories (issue #653)
|
||||
- Azure Repos do not work as a repository (issue #643)
|
||||
- Better update conflict error handing during app editing (issue #685)
|
||||
- Cluster watch needs to be restarted when CRDs get created (issue #627)
|
||||
- Credentials not being accepted for Google Source Repositories (issue #651)
|
||||
- Default project is created without permission to deploy cluster level resources (issue #679)
|
||||
- Generate role token click resets policy changes (issue #655)
|
||||
- Input type text instead of password on Connect repo panel (issue #693)
|
||||
- Metrics endpoint not reachable through the metrics kubernetes service (issue #672)
|
||||
- Operation stuck in 'in progress' state if application has no resources (issue #682)
|
||||
- Project should influence options for cluster and namespace during app creation (issue #592)
|
||||
- Repo server unable to execute ls-remote for private repos (issue #639)
|
||||
- Resource is always out of sync if it has only 'ksonnet.io/component' label (issue #686)
|
||||
- Resource nodes are 'jumping' on app details page (issue #683)
|
||||
- Sync always suggest using latest revision instead of target UI bug (issue #669)
|
||||
- Temporary ignore service catalog resources (issue #650)
|
||||
|
||||
## v0.9.2 (2018-09-28)
|
||||
|
||||
* Update to kustomize 1.0.8
|
||||
- Fix issue where argocd-server logged credentials in plain text during repo add (issue #653)
|
||||
- Credentials not being accepted for Google Source Repositories (issue #651)
|
||||
- Azure Repos do not work as a repository (issue #643)
|
||||
- Temporary ignore service catalog resources (issue #650)
|
||||
- Normalize policies by always adding space after comma
|
||||
|
||||
## v0.9.1 (2018-09-24)
|
||||
|
||||
- Repo server unable to execute ls-remote for private repos (issue #639)
|
||||
|
||||
## v0.9.0 (2018-09-24)
|
||||
|
||||
### Notes about upgrading from v0.8
|
||||
* Cluster wide resources should be allowed in default project (due to issue #330):
|
||||
|
||||
```
|
||||
argocd project allow-cluster-resource default '*' '*'
|
||||
```
|
||||
|
||||
* Projects now provide the ability to allow or deny deployments of cluster-scoped resources
|
||||
(e.g. Namespaces, ClusterRoles, CustomResourceDefinitions). When upgrading from v0.8 to v0.9, to
|
||||
match the behavior of v0.8 (which did not have restrictions on deploying resources) and continue to
|
||||
allow deployment of cluster-scoped resources, an additional command should be run:
|
||||
|
||||
```bash
|
||||
argocd proj allow-cluster-resource default '*' '*'
|
||||
```
|
||||
|
||||
The above command allows the `default` project to deploy any cluster-scoped resources which matches
|
||||
the behavior of v0.8.
|
||||
|
||||
* The secret keys in the argocd-secret containing the TLS certificate and key, has been renamed from
|
||||
`server.crt` and `server.key` to the standard `tls.crt` and `tls.key` keys. This enables Argo CD
|
||||
to integrate better with Ingress and cert-manager. When upgrading to v0.9, the `server.crt` and
|
||||
`server.key` keys in argocd-secret should be renamed to the new keys.
|
||||
|
||||
### Changes since v0.8:
|
||||
+ Auto-sync option in application CRD instance (issue #79)
|
||||
+ Support raw jsonnet as an application source (issue #540)
|
||||
+ Reorder K8s resources to correct creation order (issue #102)
|
||||
+ Redact K8s secrets from API server payloads (issue #470)
|
||||
+ Support --in-cluster authentication without providing a kubeconfig (issue #527)
|
||||
+ Special handling of CustomResourceDefinitions (issue #613)
|
||||
+ Argo CD should download helm chart dependencies (issue #582)
|
||||
+ Export Argo CD stats as prometheus style metrics (issue #513)
|
||||
+ Support restricting TLS version (issue #609)
|
||||
+ Use 'kubectl auth reconcile' before 'kubectl apply' (issue #523)
|
||||
+ Projects need controls on cluster-scoped resources (issue #330)
|
||||
+ Support IAM Authentication for managing external K8s clusters (issue #482)
|
||||
+ Compatibility with cert manager (issue #617)
|
||||
* Enable TLS for repo server (issue #553)
|
||||
* Split out dex into it's own deployment (instead of sidecar) (issue #555)
|
||||
+ [UI] Support selection of helm values files in App creation wizard (issue #499)
|
||||
+ [UI] Support specifying source revision in App creation wizard allow (issue #503)
|
||||
+ [UI] Improve resource diff rendering (issue #457)
|
||||
+ [UI] Indicate number of ready containers in pod (issue #539)
|
||||
+ [UI] Indicate when app is overriding parameters (issue #503)
|
||||
+ [UI] Provide a YAML view of resources (issue #396)
|
||||
+ [UI] Project Role/Token management from UI (issue #548)
|
||||
+ [UI] App creation wizard should allow specifying source revision (issue #562)
|
||||
+ [UI] Ability to modify application from UI (issue #615)
|
||||
+ [UI] indicate when operation is in progress or has failed (issue #566)
|
||||
- Fix issue where changes were not pulled when tracking a branch (issue #567)
|
||||
- Lazy enforcement of unknown cluster/namespace restricted resources (issue #599)
|
||||
- Fix controller hot loop when app source contains bad manifests (issue #568)
|
||||
- Fix issue where Argo CD fails to deploy when resources are in a K8s list format (issue #584)
|
||||
- Fix comparison failure when app contains unregistered custom resource (issue #583)
|
||||
- Fix issue where helm hooks were being deployed as part of sync (issue #605)
|
||||
- Fix race conditions in kube.GetResourcesWithLabel and DeleteResourceWithLabel (issue #587)
|
||||
- [UI] Fix issue where projects filter does not work when application got changed
|
||||
- [UI] Creating apps from directories is not obvious (issue #565)
|
||||
- Helm hooks are being deployed as resources (issue #605)
|
||||
- Disagreement in three way diff calculation (issue #597)
|
||||
- SIGSEGV in kube.GetResourcesWithLabel (issue #587)
|
||||
- Argo CD fails to deploy resources list (issue #584)
|
||||
- Branch tracking not working properly (issue #567)
|
||||
- Controller hot loop when application source has bad manifests (issue #568)
|
||||
|
||||
## v0.8.2 (2018-09-12)
|
||||
- Downgrade ksonnet from v0.12.0 to v0.11.0 due to quote unescape regression
|
||||
- Fix CLI panic when performing an initial `argocd sync/wait`
|
||||
|
||||
## v0.8.1 (2018-09-10)
|
||||
+ [UI] Support selection of helm values files in App creation wizard (issue #499)
|
||||
+ [UI] Support specifying source revision in App creation wizard allow (issue #503)
|
||||
+ [UI] Improve resource diff rendering (issue #457)
|
||||
+ [UI] Indicate number of ready containers in pod (issue #539)
|
||||
+ [UI] Indicate when app is overriding parameters (issue #503)
|
||||
+ [UI] Provide a YAML view of resources (issue #396)
|
||||
- Fix issue where changes were not pulled when tracking a branch (issue #567)
|
||||
- Fix controller hot loop when app source contains bad manifests (issue #568)
|
||||
- [UI] Fix issue where projects filter does not work when application got changed
|
||||
|
||||
## v0.8.0 (2018-09-04)
|
||||
|
||||
### Notes about upgrading from v0.7
|
||||
* The RBAC model has been improved to support explicit denies. What this means is that any previous
|
||||
RBAC policy rules, need to be rewritten to include one extra column with the effect:
|
||||
`allow` or `deny`. For example, if a rule was written like this:
|
||||
```
|
||||
p, my-org:my-team, applications, get, */*
|
||||
```
|
||||
It should be rewritten to look like this:
|
||||
```
|
||||
p, my-org:my-team, applications, get, */*, allow
|
||||
```
|
||||
|
||||
### Changes since v0.7:
|
||||
+ Support kustomize as an application source (issue #510)
|
||||
+ Introduce project tokens for automation access (issue #498)
|
||||
+ Add ability to delete a single application resource to support immutable updates (issue #262)
|
||||
+ Update RBAC model to support explicit denies (issue #497)
|
||||
+ Ability to view Kubernetes events related to application projects for auditing
|
||||
+ Add PVC healthcheck to controller (issue #501)
|
||||
+ Run all containers as an unprivileged user (issue #528)
|
||||
* Upgrade ksonnet to v0.12.0
|
||||
* Add readiness probes to API server (issue #522)
|
||||
* Use gRPC error codes instead of fmt.Errorf (#532)
|
||||
- API discovery becomes best effort when partial resource list is returned (issue #524)
|
||||
- Fix `argocd app wait` printing incorrect Sync output (issue #542)
|
||||
- Fix issue where argocd could not sync to a tag (#541)
|
||||
- Fix issue where static assets were browser cached between upgrades (issue #489)
|
||||
|
||||
## v0.7.2 (2018-08-21)
|
||||
- API discovery becomes best effort when partial resource list is returned (issue #524)
|
||||
|
||||
## v0.7.1 (2018-08-03)
|
||||
+ Surface helm parameters to the application level (#485)
|
||||
+ [UI] Improve application creation wizard (#459)
|
||||
+ [UI] Show indicator when refresh is still in progress (#493)
|
||||
* [UI] Improve data loading error notification (#446)
|
||||
* Infer username from claims during an `argocd relogin` (#475)
|
||||
* Expand RBAC role to be able to create application events. Fix username claims extraction
|
||||
- Fix scalability issues with the ListApps API (#494)
|
||||
- Fix issue where application server was retrieving events from incorrect cluster (#478)
|
||||
- Fix failure in identifying app source type when path was '.'
|
||||
- AppProjectSpec SourceRepos mislabeled (#490)
|
||||
- Failed e2e test was not failing CI workflow
|
||||
* Fix linux download link in getting_started.md (#487) (@chocopowwwa)
|
||||
|
||||
## v0.7.0 (2018-07-27)
|
||||
+ Support helm charts and yaml directories as an application source
|
||||
+ Audit trails in the form of API call logs
|
||||
+ Generate kubernetes events for application state changes
|
||||
+ Add ksonnet version to version endpoint (#433)
|
||||
+ Show CLI progress for sync and rollback
|
||||
+ Make use of dex refresh tokens and store them into local config
|
||||
+ Expire local superuser tokens when their password changes
|
||||
+ Add `argocd relogin` command as a convenience around login to current context
|
||||
- Fix saving default connection status for repos and clusters
|
||||
- Fix undesired fail-fast behavior of health check
|
||||
- Fix memory leak in the cluster resource watch
|
||||
- Health check for StatefulSets, DaemonSet, and ReplicaSets were failing due to use of wrong converters
|
||||
|
||||
## v0.6.2 (2018-07-23)
|
||||
- Health check for StatefulSets, DaemonSet, and ReplicaSets were failing due to use of wrong converters
|
||||
|
||||
## v0.6.1 (2018-07-18)
|
||||
- Fix regression where deployment health check incorrectly reported Healthy
|
||||
+ Intercept dex SSO errors and present them in Argo login page
|
||||
|
||||
## v0.6.0 (2018-07-16)
|
||||
+ Support PreSync, Sync, PostSync resource hooks
|
||||
+ Introduce Application Projects for finer grain RBAC controls
|
||||
+ Swagger Docs & UI
|
||||
+ Support in-cluster deployments internal kubernetes service name
|
||||
+ Refactoring & Improvements
|
||||
* Improved error handling, status and condition reporting
|
||||
* Remove installer in favor of kubectl apply instructions
|
||||
* Add validation when setting application parameters
|
||||
* Cascade deletion is decided during app deletion, instead of app creation
|
||||
- Fix git authentication implementation when using using SSH key
|
||||
- app-name label was inadvertently injected into spec.selector if selector was omitted from v1beta1 specs
|
||||
|
||||
## v0.5.4 (2018-06-27)
|
||||
- Refresh flag to sync should be optional, not required
|
||||
|
||||
## v0.5.3 (2018-06-20)
|
||||
+ Support cluster management using the internal k8s API address https://kubernetes.default.svc (#307)
|
||||
+ Support diffing a local ksonnet app to the live application state (resolves #239) (#298)
|
||||
+ Add ability to show last operation result in app get. Show path in app list -o wide (#297)
|
||||
+ Update dependencies: ksonnet v0.11, golang v1.10, debian v9.4 (#296)
|
||||
+ Add ability to force a refresh of an app during get (resolves #269) (#293)
|
||||
+ Automatically restart API server upon certificate changes (#292)
|
||||
|
||||
## v0.5.2 (2018-06-14)
|
||||
+ Resource events tab on application details page (#286)
|
||||
+ Display pod status on application details page (#231)
|
||||
|
||||
## v0.5.1 (2018-06-13)
|
||||
- API server incorrectly compose application fully qualified name for RBAC check (#283)
|
||||
- UI crash while rendering application operation info if operation failed
|
||||
|
||||
## v0.5.0 (2018-06-12)
|
||||
+ RBAC access control
|
||||
+ Repository/Cluster state monitoring
|
||||
+ Argo CD settings import/export
|
||||
+ Application creation UI wizard
|
||||
+ argocd app manifests for printing the application manifests
|
||||
+ argocd app unset command to unset parameter overrides
|
||||
+ Fail app sync if prune flag is required (#276)
|
||||
+ Take into account number of unavailable replicas to decided if deployment is healthy or not #270
|
||||
+ Add ability to show parameters and overrides in CLI (resolves #240)
|
||||
- Repo names containing underscores were not being accepted (#258)
|
||||
- Cookie token was not parsed properly when mixed with other site cookies
|
||||
|
||||
## v0.4.7 (2018-06-07)
|
||||
- Fix argocd app wait health checking logic
|
||||
|
||||
## v0.4.6 (2018-06-06)
|
||||
- Retry argocd app wait connection errors from EOF watch. Show detailed state changes
|
||||
|
||||
## v0.4.5 (2018-05-31)
|
||||
+ Add argocd app unset command to unset parameter overrides
|
||||
- Cookie token was not parsed properly when mixed with other site cookies
|
||||
|
||||
## v0.4.4 (2018-05-30)
|
||||
+ Add ability to show parameters and overrides in CLI (resolves #240)
|
||||
+ Add Events API endpoint
|
||||
+ Issue #238 - add upsert flag to 'argocd app create' command
|
||||
+ Add repo browsing endpoint (#229)
|
||||
+ Support subscribing to settings updates and auto-restart of dex and API server
|
||||
- Issue #233 - Controller does not persist rollback operation result
|
||||
- App sync frequently fails due to concurrent app modification
|
||||
|
||||
## v0.4.3 (2018-05-21)
|
||||
- Move local branch deletion as part of git Reset() (resolves #185) (#222)
|
||||
- Fix exit code for app wait (#219)
|
||||
|
||||
## v0.4.2 (2018-05-21)
|
||||
+ Show URL in argocd app get
|
||||
- Remove interactive context name prompt during login which broke login automation
|
||||
* Rename force flag to cascade in argocd app delete
|
||||
|
||||
## v0.4.1 (2018-05-18)
|
||||
+ Implemented argocd app wait command
|
||||
|
||||
## v0.4.0 (2018-05-17)
|
||||
+ SSO Integration
|
||||
+ GitHub Webhook
|
||||
+ Add application health status
|
||||
+ Sync/Rollback/Delete is asynchronously handled by controller
|
||||
* Refactor CRUD operation on clusters and repos
|
||||
* Sync will always perform kubectl apply
|
||||
* Synced Status considers last-applied-configuration annotatoin
|
||||
* Server & namespace are mandatory fields (still inferred from app.yaml)
|
||||
* Manifests are memoized in repo server
|
||||
- Fix connection timeouts to SSH repos
|
||||
|
||||
## v0.3.2 (2018-05-03)
|
||||
+ Application sync should delete 'unexpected' resources #139
|
||||
+ Update ksonnet to v0.10.1
|
||||
+ Detect unexpected resources
|
||||
- Fix: App sync frequently fails due to concurrent app modification #147
|
||||
- Fix: improve app state comparator: #136, #132
|
||||
|
||||
## v0.3.1 (2018-04-24)
|
||||
+ Add new rollback RPC with numeric identifiers
|
||||
+ New argo app history and argo app rollback command
|
||||
+ Switch to gogo/protobuf for golang code generation
|
||||
- Fix: create .argocd directory during argo login (issue #123)
|
||||
- Fix: Allow overriding server or namespace separately (issue #110)
|
||||
|
||||
## v0.3.0 (2018-04-23)
|
||||
+ Auth support
|
||||
+ TLS support
|
||||
+ DAG-based application view
|
||||
+ Bulk watch
|
||||
+ ksonnet v0.10.0-alpha.3
|
||||
+ kubectl apply deployment strategy
|
||||
+ CLI improvements for app management
|
||||
|
||||
## v0.2.0 (2018-04-03)
|
||||
+ Rollback UI
|
||||
+ Override parameters
|
||||
|
||||
## v0.1.0 (2018-03-12)
|
||||
+ Define app in Github with dev and preprod environment using KSonnet
|
||||
+ Add cluster Diff App with a cluster Deploy app in a cluster
|
||||
+ Deploy a new version of the app in the cluster
|
||||
+ App sync based on Github app config change - polling only
|
||||
+ Basic UI: App diff between Git and k8s cluster for all environments Basic GUI
|
||||
@@ -1,40 +0,0 @@
|
||||
## Requirements
|
||||
Make sure you have following tools installed [golang](https://golang.org/), [dep](https://github.com/golang/dep), [protobuf](https://developers.google.com/protocol-buffers/),
|
||||
[kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/).
|
||||
|
||||
```
|
||||
$ brew install go dep protobuf kubectl
|
||||
$ go get -u github.com/golang/protobuf/protoc-gen-go
|
||||
```
|
||||
|
||||
Nice to have [gometalinter](https://github.com/alecthomas/gometalinter) and [goreman](https://github.com/mattn/goreman):
|
||||
|
||||
```
|
||||
$ go get -u gopkg.in/alecthomas/gometalinter.v2 github.com/mattn/goreman && gometalinter.v2 --install
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
$ go get -u github.com/argoproj/argo-cd
|
||||
$ dep ensure
|
||||
$ make
|
||||
```
|
||||
|
||||
## Running locally
|
||||
|
||||
You need to have access to kubernetes cluster (including [minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/) or [docker edge](https://docs.docker.com/docker-for-mac/install/) ) in order to run Argo CD on your laptop:
|
||||
|
||||
* install kubectl: `brew install kubectl`
|
||||
* make sure `kubectl` is connected to your cluster (e.g. `kubectl get pods` should work).
|
||||
* install application CRD using following command:
|
||||
|
||||
```
|
||||
$ kubectl create -f install/manifests/01_application-crd.yaml
|
||||
```
|
||||
|
||||
* start Argo CD services using [goreman](https://github.com/mattn/goreman):
|
||||
|
||||
```
|
||||
$ goreman start
|
||||
```
|
||||
13
COPYRIGHT
@@ -1,13 +0,0 @@
|
||||
Copyright 2017-2018 The Argo Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License..
|
||||
152
Dockerfile
Normal file
@@ -0,0 +1,152 @@
|
||||
####################################################################################################
|
||||
# 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
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
make \
|
||||
wget \
|
||||
gcc \
|
||||
zip && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
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
|
||||
ENV KUBECTL_VERSION=1.14.0
|
||||
RUN curl -L -o /usr/local/bin/kubectl -LO https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl && \
|
||||
chmod +x /usr/local/bin/kubectl && \
|
||||
kubectl version --client
|
||||
|
||||
# Install ksonnet
|
||||
ENV KSONNET_VERSION=0.13.1
|
||||
RUN wget https://github.com/ksonnet/ksonnet/releases/download/v${KSONNET_VERSION}/ks_${KSONNET_VERSION}_linux_amd64.tar.gz && \
|
||||
tar -C /tmp/ -xf ks_${KSONNET_VERSION}_linux_amd64.tar.gz && \
|
||||
mv /tmp/ks_${KSONNET_VERSION}_linux_amd64/ks /usr/local/bin/ks && \
|
||||
ks version
|
||||
|
||||
# Install helm
|
||||
ENV HELM_VERSION=2.12.1
|
||||
RUN wget https://storage.googleapis.com/kubernetes-helm/helm-v${HELM_VERSION}-linux-amd64.tar.gz && \
|
||||
tar -C /tmp/ -xf helm-v${HELM_VERSION}-linux-amd64.tar.gz && \
|
||||
mv /tmp/linux-amd64/helm /usr/local/bin/helm && \
|
||||
helm version --client
|
||||
|
||||
# Install kustomize
|
||||
ENV KUSTOMIZE1_VERSION=1.0.11
|
||||
RUN curl -L -o /usr/local/bin/kustomize1 https://github.com/kubernetes-sigs/kustomize/releases/download/v${KUSTOMIZE1_VERSION}/kustomize_${KUSTOMIZE1_VERSION}_linux_amd64 && \
|
||||
chmod +x /usr/local/bin/kustomize1 && \
|
||||
kustomize1 version
|
||||
|
||||
|
||||
ENV KUSTOMIZE_VERSION=2.0.3
|
||||
RUN curl -L -o /usr/local/bin/kustomize https://github.com/kubernetes-sigs/kustomize/releases/download/v${KUSTOMIZE_VERSION}/kustomize_${KUSTOMIZE_VERSION}_linux_amd64 && \
|
||||
chmod +x /usr/local/bin/kustomize && \
|
||||
kustomize version
|
||||
|
||||
# Install AWS IAM Authenticator
|
||||
ENV AWS_IAM_AUTHENTICATOR_VERSION=0.4.0-alpha.1
|
||||
RUN curl -L -o /usr/local/bin/aws-iam-authenticator https://github.com/kubernetes-sigs/aws-iam-authenticator/releases/download/${AWS_IAM_AUTHENTICATOR_VERSION}/aws-iam-authenticator_${AWS_IAM_AUTHENTICATOR_VERSION}_linux_amd64 && \
|
||||
chmod +x /usr/local/bin/aws-iam-authenticator
|
||||
|
||||
# Install golangci-lint
|
||||
RUN wget https://install.goreleaser.com/github.com/golangci/golangci-lint.sh && \
|
||||
chmod +x ./golangci-lint.sh && \
|
||||
./golangci-lint.sh -b $GOPATH/bin && \
|
||||
golangci-lint linters
|
||||
|
||||
COPY .golangci.yml ${GOPATH}/src/dummy/.golangci.yml
|
||||
|
||||
RUN cd ${GOPATH}/src/dummy && \
|
||||
touch dummy.go \
|
||||
golangci-lint run
|
||||
|
||||
####################################################################################################
|
||||
# Argo CD Base - used as the base for both the release and dev argocd images
|
||||
####################################################################################################
|
||||
FROM debian:9.5-slim as argocd-base
|
||||
|
||||
RUN groupadd -g 999 argocd && \
|
||||
useradd -r -u 999 -g argocd argocd && \
|
||||
mkdir -p /home/argocd && \
|
||||
chown argocd:argocd /home/argocd && \
|
||||
apt-get update && \
|
||||
apt-get install -y git && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
COPY hack/ssh_known_hosts /etc/ssh/ssh_known_hosts
|
||||
COPY hack/git-ask-pass.sh /usr/local/bin/git-ask-pass.sh
|
||||
COPY --from=builder /usr/local/bin/ks /usr/local/bin/ks
|
||||
COPY --from=builder /usr/local/bin/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
|
||||
|
||||
# workaround ksonnet issue https://github.com/ksonnet/ksonnet/issues/298
|
||||
ENV USER=argocd
|
||||
|
||||
USER argocd
|
||||
WORKDIR /home/argocd
|
||||
|
||||
|
||||
####################################################################################################
|
||||
# Argo CD Build stage which performs the actual build of Argo CD binaries
|
||||
####################################################################################################
|
||||
FROM golang:1.11.4 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
|
||||
|
||||
# A dummy directory is created under $GOPATH/src/dummy so we are able to use dep
|
||||
# to install all the packages of our dep lock file
|
||||
COPY Gopkg.toml ${GOPATH}/src/dummy/Gopkg.toml
|
||||
COPY Gopkg.lock ${GOPATH}/src/dummy/Gopkg.lock
|
||||
|
||||
RUN cd ${GOPATH}/src/dummy && \
|
||||
dep ensure -vendor-only && \
|
||||
mv vendor/* ${GOPATH}/src/ && \
|
||||
rmdir vendor
|
||||
|
||||
# Perform the build
|
||||
WORKDIR /go/src/github.com/argoproj/argo-cd
|
||||
COPY . .
|
||||
RUN make cli server controller repo-server argocd-util && \
|
||||
make CLI_NAME=argocd-darwin-amd64 GOOS=darwin cli
|
||||
|
||||
|
||||
####################################################################################################
|
||||
# Final image
|
||||
####################################################################################################
|
||||
FROM argocd-base
|
||||
COPY --from=argocd-build /go/src/github.com/argoproj/argo-cd/dist/argocd* /usr/local/bin/
|
||||
@@ -1,60 +0,0 @@
|
||||
FROM debian:9.3 as builder
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
make \
|
||||
wget \
|
||||
gcc \
|
||||
zip && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# Install go
|
||||
ENV GO_VERSION 1.9.3
|
||||
ENV GO_ARCH amd64
|
||||
ENV GOPATH /root/go
|
||||
ENV PATH ${GOPATH}/bin:/usr/local/go/bin:${PATH}
|
||||
RUN wget https://storage.googleapis.com/golang/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz && \
|
||||
tar -C /usr/local/ -xf /go${GO_VERSION}.linux-${GO_ARCH}.tar.gz && \
|
||||
rm /go${GO_VERSION}.linux-${GO_ARCH}.tar.gz
|
||||
|
||||
# Install protoc, dep, packr
|
||||
ENV PROTOBUF_VERSION 3.5.1
|
||||
RUN cd /usr/local && \
|
||||
wget https://github.com/google/protobuf/releases/download/v${PROTOBUF_VERSION}/protoc-${PROTOBUF_VERSION}-linux-x86_64.zip && \
|
||||
unzip protoc-*.zip && \
|
||||
wget https://github.com/golang/dep/releases/download/v0.4.1/dep-linux-amd64 -O /usr/local/bin/dep && \
|
||||
chmod +x /usr/local/bin/dep && \
|
||||
wget https://github.com/gobuffalo/packr/releases/download/v1.10.4/packr_1.10.4_linux_amd64.tar.gz && \
|
||||
tar -vxf packr*.tar.gz -C /tmp/ && \
|
||||
mv /tmp/packr /usr/local/bin/packr
|
||||
|
||||
# A dummy directory is created under $GOPATH/src/dummy so we are able to use dep
|
||||
# to install all the packages of our dep lock file
|
||||
COPY Gopkg.toml ${GOPATH}/src/dummy/Gopkg.toml
|
||||
COPY Gopkg.lock ${GOPATH}/src/dummy/Gopkg.lock
|
||||
|
||||
RUN cd ${GOPATH}/src/dummy && \
|
||||
dep ensure -vendor-only && \
|
||||
mv vendor/* ${GOPATH}/src/ && \
|
||||
rmdir vendor
|
||||
|
||||
ARG MAKE_TARGET
|
||||
|
||||
# Perform the build
|
||||
WORKDIR /root/go/src/github.com/argoproj/argo-cd
|
||||
COPY . .
|
||||
RUN make ${MAKE_TARGET}
|
||||
RUN wget https://github.com/ksonnet/ksonnet/releases/download/v0.9.1/ks_0.9.1_linux_amd64.tar.gz && tar -C /tmp/ -xf ks_0.9.1_linux_amd64.tar.gz
|
||||
|
||||
FROM debian:9.3
|
||||
RUN apt-get update && apt-get install -y git && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
ARG BINARY
|
||||
COPY --from=builder /root/go/src/github.com/argoproj/argo-cd/dist/${BINARY} /${BINARY}
|
||||
COPY --from=builder /tmp/ks_0.9.1_linux_amd64/ks /usr/local/bin/ks
|
||||
# workaround ksonnet issue https://github.com/ksonnet/ksonnet/issues/298
|
||||
ENV USER=root
|
||||
ENV BINARY=$BINARY
|
||||
CMD /$BINARY
|
||||
@@ -1,16 +0,0 @@
|
||||
FROM golang:1.9.2
|
||||
|
||||
WORKDIR /tmp
|
||||
|
||||
RUN curl -O https://get.docker.com/builds/Linux/x86_64/docker-1.13.1.tgz && \
|
||||
tar -xzf docker-1.13.1.tgz && \
|
||||
mv docker/docker /usr/local/bin/docker && \
|
||||
rm -rf ./docker && \
|
||||
go get -u github.com/golang/dep/cmd/dep && \
|
||||
go get -u gopkg.in/alecthomas/gometalinter.v2 && \
|
||||
gometalinter.v2 --install
|
||||
|
||||
# Install ksonnet
|
||||
# TODO(jessesuen): pin this once we decide on a stable version
|
||||
RUN go get -v -u github.com/ksonnet/ksonnet && \
|
||||
mv ${GOPATH}/bin/ksonnet /usr/local/bin/ks
|
||||
5
Dockerfile.dev
Normal file
@@ -0,0 +1,5 @@
|
||||
####################################################################################################
|
||||
# argocd-dev
|
||||
####################################################################################################
|
||||
FROM argocd-base
|
||||
COPY argocd* /usr/local/bin/
|
||||
1241
Gopkg.lock
generated
53
Gopkg.toml
@@ -1,45 +1,68 @@
|
||||
# Packages should only be added to the following list when we use them *outside* of our go code.
|
||||
# (e.g. we want to build the binary to invoke as part of the build process, such as in
|
||||
# generate-proto.sh). Normal use of golang packages should be added via `dep ensure`, and pinned
|
||||
# with a [[constraint]] or [[override]] when version is important.
|
||||
required = [
|
||||
"github.com/golang/protobuf/protoc-gen-go",
|
||||
"github.com/gogo/protobuf/protoc-gen-gofast",
|
||||
"golang.org/x/sync/errgroup",
|
||||
"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",
|
||||
]
|
||||
|
||||
[[constraint]]
|
||||
name = "google.golang.org/grpc"
|
||||
version = "1.9.2"
|
||||
version = "1.15.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gogo/protobuf"
|
||||
version = "1.1.1"
|
||||
|
||||
# override github.com/grpc-ecosystem/go-grpc-middleware's constraint on master
|
||||
[[override]]
|
||||
name = "github.com/golang/protobuf"
|
||||
version = "1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/grpc-ecosystem/grpc-gateway"
|
||||
version = "v1.3.1"
|
||||
|
||||
# prometheus does not believe in semversioning yet
|
||||
[[constraint]]
|
||||
branch = "release-1.9"
|
||||
name = "k8s.io/apimachinery"
|
||||
name = "github.com/prometheus/client_golang"
|
||||
revision = "7858729281ec582767b20e0d696b6041d995d5e0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "release-1.9"
|
||||
branch = "release-1.12"
|
||||
name = "k8s.io/api"
|
||||
|
||||
[[constraint]]
|
||||
name = "k8s.io/apiextensions-apiserver"
|
||||
branch = "release-1.9"
|
||||
|
||||
[[constraint]]
|
||||
branch = "release-1.9"
|
||||
branch = "release-1.12"
|
||||
name = "k8s.io/code-generator"
|
||||
|
||||
[[constraint]]
|
||||
branch = "release-6.0"
|
||||
branch = "release-9.0"
|
||||
name = "k8s.io/client-go"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "1.2.1"
|
||||
version = "1.2.2"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gobuffalo/packr"
|
||||
version = "v1.11.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/ksonnet/ksonnet"
|
||||
branch = "master"
|
||||
name = "github.com/argoproj/pkg"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/pborman/uuid"
|
||||
version = "1.1.0"
|
||||
branch = "master"
|
||||
name = "github.com/yudai/gojsondiff"
|
||||
|
||||
[[override]]
|
||||
revision = "master"
|
||||
name = "k8s.io/kube-openapi"
|
||||
|
||||
2
LICENSE
@@ -187,7 +187,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
Copyright 2017-2018 The Argo Authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
155
Makefile
@@ -1,6 +1,7 @@
|
||||
PACKAGE=github.com/argoproj/argo-cd
|
||||
CURRENT_DIR=$(shell pwd)
|
||||
DIST_DIR=${CURRENT_DIR}/dist
|
||||
CLI_NAME=argocd
|
||||
|
||||
VERSION=$(shell cat ${CURRENT_DIR}/VERSION)
|
||||
BUILD_DATE=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
@@ -8,6 +9,17 @@ GIT_COMMIT=$(shell git rev-parse HEAD)
|
||||
GIT_TAG=$(shell if [ -z "`git status --porcelain`" ]; then git describe --exact-match --tags HEAD 2>/dev/null; fi)
|
||||
GIT_TREE_STATE=$(shell if [ -z "`git status --porcelain`" ]; then echo "clean" ; else echo "dirty"; fi)
|
||||
PACKR_CMD=$(shell if [ "`which packr`" ]; then echo "packr"; else echo "go run vendor/github.com/gobuffalo/packr/packr/main.go"; fi)
|
||||
TEST_CMD=$(shell [ "`which gotestsum`" != "" ] && echo gotestsum -- || echo go test)
|
||||
|
||||
PATH:=$(PATH):$(PWD)/hack
|
||||
|
||||
# docker image publishing options
|
||||
DOCKER_PUSH=false
|
||||
IMAGE_TAG=latest
|
||||
# perform static compilation
|
||||
STATIC_BUILD=true
|
||||
# build development images
|
||||
DEV_IMAGE=false
|
||||
|
||||
override LDFLAGS += \
|
||||
-X ${PACKAGE}.version=${VERSION} \
|
||||
@@ -15,19 +27,14 @@ override LDFLAGS += \
|
||||
-X ${PACKAGE}.gitCommit=${GIT_COMMIT} \
|
||||
-X ${PACKAGE}.gitTreeState=${GIT_TREE_STATE}
|
||||
|
||||
# docker image publishing options
|
||||
DOCKER_PUSH=false
|
||||
IMAGE_TAG=latest
|
||||
ifeq (${STATIC_BUILD}, true)
|
||||
override LDFLAGS += -extldflags "-static"
|
||||
endif
|
||||
|
||||
ifneq (${GIT_TAG},)
|
||||
IMAGE_TAG=${GIT_TAG}
|
||||
LDFLAGS += -X ${PACKAGE}.gitTag=${GIT_TAG}
|
||||
endif
|
||||
ifneq (${IMAGE_NAMESPACE},)
|
||||
override LDFLAGS += -X ${PACKAGE}/install.imageNamespace=${IMAGE_NAMESPACE}
|
||||
endif
|
||||
ifneq (${IMAGE_TAG},)
|
||||
override LDFLAGS += -X ${PACKAGE}/install.imageTag=${IMAGE_TAG}
|
||||
endif
|
||||
|
||||
ifeq (${DOCKER_PUSH},true)
|
||||
ifndef IMAGE_NAMESPACE
|
||||
@@ -40,68 +47,138 @@ IMAGE_PREFIX=${IMAGE_NAMESPACE}/
|
||||
endif
|
||||
|
||||
.PHONY: all
|
||||
all: cli server-image controller-image repo-server-image
|
||||
all: cli image argocd-util
|
||||
|
||||
.PHONY: protogen
|
||||
protogen:
|
||||
./hack/generate-proto.sh
|
||||
|
||||
.PHONY: openapigen
|
||||
openapigen:
|
||||
./hack/update-openapi.sh
|
||||
|
||||
.PHONY: clientgen
|
||||
clientgen:
|
||||
./hack/update-codegen.sh
|
||||
|
||||
.PHONY: codegen
|
||||
codegen: protogen clientgen
|
||||
codegen: protogen clientgen openapigen
|
||||
|
||||
# NOTE: we use packr to do the build instead of go, since we embed .yaml files into the go binary.
|
||||
# This enables ease of maintenance of the yaml files.
|
||||
.PHONY: cli
|
||||
cli:
|
||||
CGO_ENABLED=0 ${PACKR_CMD} build -v -i -ldflags '${LDFLAGS} -extldflags "-static"' -o ${DIST_DIR}/argocd ./cmd/argocd
|
||||
cli: clean-debug
|
||||
CGO_ENABLED=0 ${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/${CLI_NAME} ./cmd/argocd
|
||||
|
||||
.PHONY: release-cli
|
||||
release-cli: clean-debug image
|
||||
docker create --name tmp-argocd-linux $(IMAGE_PREFIX)argocd:$(IMAGE_TAG)
|
||||
docker cp tmp-argocd-linux:/usr/local/bin/argocd ${DIST_DIR}/argocd-linux-amd64
|
||||
docker cp tmp-argocd-linux:/usr/local/bin/argocd-darwin-amd64 ${DIST_DIR}/argocd-darwin-amd64
|
||||
docker rm tmp-argocd-linux
|
||||
|
||||
.PHONY: argocd-util
|
||||
argocd-util: clean-debug
|
||||
# Build argocd-util as a statically linked binary, so it could run within the alpine-based dex container (argoproj/argo-cd#844)
|
||||
CGO_ENABLED=0 go build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-util ./cmd/argocd-util
|
||||
|
||||
.PHONY: manifests
|
||||
manifests:
|
||||
./hack/update-manifests.sh
|
||||
|
||||
# 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:
|
||||
CGO_ENABLED=0 go build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-server ./cmd/argocd-server
|
||||
|
||||
.PHONY: server-image
|
||||
server-image:
|
||||
docker build --build-arg BINARY=argocd-server --build-arg MAKE_TARGET=server -t $(IMAGE_PREFIX)argocd-server:$(IMAGE_TAG) -f Dockerfile-argocd .
|
||||
@if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argocd-server:$(IMAGE_TAG) ; fi
|
||||
|
||||
server: clean-debug
|
||||
CGO_ENABLED=0 ${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-server ./cmd/argocd-server
|
||||
|
||||
.PHONY: repo-server
|
||||
repo-server:
|
||||
CGO_ENABLED=0 go build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-repo-server ./cmd/argocd-repo-server
|
||||
|
||||
.PHONY: repo-server-image
|
||||
repo-server-image:
|
||||
docker build --build-arg BINARY=argocd-repo-server --build-arg MAKE_TARGET=repo-server -t $(IMAGE_PREFIX)argocd-repo-server:$(IMAGE_TAG) -f Dockerfile-argocd .
|
||||
@if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argocd-repo-server:$(IMAGE_TAG) ; fi
|
||||
|
||||
.PHONY: controller
|
||||
controller:
|
||||
CGO_ENABLED=0 go 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: controller-image
|
||||
controller-image:
|
||||
docker build --build-arg BINARY=argocd-application-controller --build-arg MAKE_TARGET=controller -t $(IMAGE_PREFIX)argocd-application-controller:$(IMAGE_TAG) -f Dockerfile-argocd .
|
||||
@if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argocd-application-controller:$(IMAGE_TAG) ; fi
|
||||
.PHONY: packr
|
||||
packr:
|
||||
go build -o ${DIST_DIR}/packr ./vendor/github.com/gobuffalo/packr/packr/
|
||||
|
||||
.PHONY: image
|
||||
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: packr
|
||||
docker build -t argocd-base --target argocd-base .
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-server ./cmd/argocd-server
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-application-controller ./cmd/argocd-application-controller
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-repo-server ./cmd/argocd-repo-server
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-util ./cmd/argocd-util
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd ./cmd/argocd
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-darwin-amd64 ./cmd/argocd
|
||||
cp Dockerfile.dev dist
|
||||
docker build -t $(IMAGE_PREFIX)argocd:$(IMAGE_TAG) -f dist/Dockerfile.dev dist
|
||||
else
|
||||
image:
|
||||
docker build -t $(IMAGE_PREFIX)argocd:$(IMAGE_TAG) .
|
||||
endif
|
||||
@if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argocd:$(IMAGE_TAG) ; fi
|
||||
|
||||
.PHONY: builder-image
|
||||
builder-image:
|
||||
docker build -t $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG) -f Dockerfile-ci-builder .
|
||||
docker build -t $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG) --target builder .
|
||||
docker push $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG)
|
||||
|
||||
.PHONY: dep-ensure
|
||||
dep-ensure:
|
||||
dep ensure -no-vendor
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
gometalinter.v2 --config gometalinter.json ./...
|
||||
golangci-lint run --fix
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
go build `go list ./... | grep -v resource_customizations`
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test ./...
|
||||
$(TEST_CMD) -covermode=count -coverprofile=coverage.out `go list ./... | grep -v "github.com/argoproj/argo-cd/test/e2e"`
|
||||
|
||||
.PHONY: test-e2e
|
||||
test-e2e: cli
|
||||
$(TEST_CMD) -v -failfast -timeout 20m ./test/e2e
|
||||
|
||||
.PHONY: start-e2e
|
||||
start-e2e: cli
|
||||
killall goreman || true
|
||||
kubectl create ns argocd-e2e || true
|
||||
kubens argocd-e2e
|
||||
kustomize build test/manifests/base | kubectl apply -f -
|
||||
make start
|
||||
|
||||
# Cleans VSCode debug.test files from sub-dirs to prevent them from being included in packr boxes
|
||||
.PHONY: clean-debug
|
||||
clean-debug:
|
||||
-find ${CURRENT_DIR} -name debug.test | xargs rm -f
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
clean: clean-debug
|
||||
-rm -rf ${CURRENT_DIR}/dist
|
||||
|
||||
.PHONY: precheckin
|
||||
precheckin: test lint
|
||||
.PHONY: start
|
||||
start:
|
||||
killall goreman || true
|
||||
kubens argocd
|
||||
goreman start
|
||||
|
||||
.PHONY: pre-commit
|
||||
pre-commit: dep-ensure codegen build lint test
|
||||
|
||||
.PHONY: release-precheck
|
||||
release-precheck: manifests
|
||||
@if [ "$(GIT_TREE_STATE)" != "clean" ]; then echo 'git tree state is $(GIT_TREE_STATE)' ; exit 1; fi
|
||||
@if [ -z "$(GIT_TAG)" ]; then echo 'commit must be tagged to perform release' ; exit 1; fi
|
||||
@if [ "$(GIT_TAG)" != "v`cat VERSION`" ]; then echo 'VERSION does not match git tag'; exit 1; fi
|
||||
|
||||
.PHONY: release
|
||||
release: release-precheck pre-commit image release-cli
|
||||
|
||||
8
OWNERS
Normal file
@@ -0,0 +1,8 @@
|
||||
owners:
|
||||
- alexmt
|
||||
- jessesuen
|
||||
|
||||
approvers:
|
||||
- alexmt
|
||||
- jessesuen
|
||||
- merenbach
|
||||
8
Procfile
@@ -1,3 +1,5 @@
|
||||
controller: go run ./cmd/argocd-application-controller/main.go --app-resync 10
|
||||
api-server: go run ./cmd/argocd-server/main.go
|
||||
repo-server: go run ./cmd/argocd-repo-server/main.go
|
||||
controller: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true go run ./cmd/argocd-application-controller/main.go --loglevel debug --redis localhost:6379 --repo-server localhost:8081"
|
||||
api-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true go run ./cmd/argocd-server/main.go --loglevel debug --redis localhost:6379 --disable-auth --insecure --dex-server http://localhost:5556 --repo-server localhost:8081 --staticassets ../argo-cd-ui/dist/app"
|
||||
repo-server: sh -c "FORCE_LOG_COLORS=1 go run ./cmd/argocd-repo-server/main.go --loglevel debug --redis localhost:6379"
|
||||
dex: sh -c "go run ./cmd/argocd-util/main.go gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p 5556:5556 -v `pwd`/dist/dex.yaml:/dex.yaml quay.io/dexidp/dex:v2.14.0 serve /dex.yaml"
|
||||
redis: docker run --rm --name argocd-redis -i -p 6379:6379 redis:5.0.3-alpine --save ""--appendonly no
|
||||
|
||||
89
README.md
@@ -1,87 +1,32 @@
|
||||
[](https://argoproj.github.io/community/join-slack)
|
||||
[](https://codecov.io/gh/argoproj/argo-cd)
|
||||
|
||||
# Argo CD - GitOps Continuous Delivery for Kubernetes
|
||||
# Argo CD - Declarative Continuous Delivery for Kubernetes
|
||||
|
||||
## What is Argo CD?
|
||||
|
||||
Argo CD is a declarative, continuous delivery service based on ksonnet for Kubernetes.
|
||||
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
|
||||
|
||||
Follow our [getting started guide](docs/GETTING_STARTED.md).
|
||||
## Who uses Argo CD?
|
||||
|
||||
## How it works
|
||||
Organizations below are **officially** using Argo CD. Please send a PR with your organization name if you are using Argo CD.
|
||||
|
||||
Argo CD uses git repositories as the source of truth for defining the desired application state as
|
||||
well as the target deployment environments. Kubernetes manifests are specified as
|
||||
[ksonnet](https://ksonnet.io) applications. Argo CD automates the deployment of the desired
|
||||
application states in the specified target environments.
|
||||
1. [Intuit](https://www.intuit.com/)
|
||||
2. [KompiTech GmbH](https://www.kompitech.com/)
|
||||
3. [Yieldlab](https://www.yieldlab.de/)
|
||||
4. [Ticketmaster](https://ticketmaster.com)
|
||||
5. [CyberAgent](https://www.cyberagent.co.jp/en/)
|
||||
6. [OpenSaaS Studio](https://opensaas.studio)
|
||||
7. [Riskified](https://www.riskified.com/)
|
||||
|
||||

|
||||
## Documentation
|
||||
|
||||
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 out-of-sync.
|
||||
Argo CD reports & visualizes the differences as well as 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
|
||||
* Continuous monitoring of deployed applications
|
||||
* Automated or manual syncing of applications to its target state
|
||||
* Web and CLI based visualization of applications and differences between live vs. target state
|
||||
* Rollback/Roll-anywhere to any application state committed in the git repository
|
||||
|
||||
## What is ksonnet?
|
||||
|
||||
* [Jsonnet](http://jsonnet.org), the basis for ksonnet, is a domain specific configuration language,
|
||||
which provides extreme flexibility for composing and manipulating JSON/YAML specifications.
|
||||
* [Ksonnet](http://ksonnet.io) goes one step further by applying Jsonnet principles to Kubernetes
|
||||
manifests. It provides an opinionated file & directory structure to organize applications into
|
||||
reusable components, parameters, and environments. Environments can be hierarchical, which promotes
|
||||
both re-use and granular customization of application and environment specifications.
|
||||
|
||||
## Why ksonnet?
|
||||
|
||||
Application configuration management is a hard problem and grows rapidly in complexity as you deploy
|
||||
more applications, against more and more environments. Current templating systems, such as Jinja,
|
||||
and Golang templating, are unnatural ways to maintain kubernetes manifests, and are not well suited to
|
||||
capture subtle configuration differences between environments. Its ability to compose and re-use
|
||||
application and environment configurations is also very limited.
|
||||
|
||||
Imagine we have a single guestbook application deployed in following environments:
|
||||
|
||||
| Environment | K8s Version | Application Image | DB Connection String | Environment Vars | Sidecars |
|
||||
|---------------|-------------|------------------------|-----------------------|------------------|---------------|
|
||||
| minikube | 1.10.0 | jesse/guestbook:latest | sql://locahost/db | DEBUG=true | |
|
||||
| dev | 1.9.0 | app/guestbook:latest | sql://dev-test/db | DEBUG=true | |
|
||||
| staging | 1.8.0 | app/guestbook:e3c0263 | sql://staging/db | | istio,dnsmasq |
|
||||
| us-west-1 | 1.8.0 | app/guestbook:abc1234 | sql://prod/db | FOO_FEATURE=true | istio,dnsmasq |
|
||||
| us-west-2 | 1.8.0 | app/guestbook:abc1234 | sql://prod/db | | istio,dnsmasq |
|
||||
| us-east-1 | 1.9.0 | app/guestbook:abc1234 | sql://prod/db | BAR_FEATURE=true | istio,dnsmasq |
|
||||
|
||||
Ksonnet:
|
||||
* Enables composition and re-use of common YAML specifications
|
||||
* Allows overrides, additions, and subtractions of YAML sub-components specific to each environment
|
||||
* Guarantees proper generation of K8s manifests suitable for the corresponding Kubernetes API version
|
||||
* Provides [kubernetes-specific jsonnet libraries](https://github.com/ksonnet/ksonnet-lib) to enable
|
||||
concise definition of kubernetes manifests
|
||||
|
||||
## Development Status
|
||||
* Argo CD is in early development
|
||||
|
||||
## Roadmap
|
||||
* PreSync, PostSync, OutOfSync hooks
|
||||
* Customized application actions as Argo workflows
|
||||
* Blue/Green & canary upgrades
|
||||
* SSO Integration
|
||||
* GitHub & Docker webhooks
|
||||
To learn more about Argo CD [go to the complete documentation](https://argoproj.github.io/argo-cd/).
|
||||
|
||||
29
assets/builtin-policy.csv
Normal file
@@ -0,0 +1,29 @@
|
||||
# Built-in policy which defines two roles: role:readonly and role:admin,
|
||||
# and additionally assigns the admin user to the role:admin role.
|
||||
# There are two policy formats:
|
||||
# 1. Applications (which belong to a project):
|
||||
# p, <user/group>, <resource>, <action>, <project>/<object>
|
||||
# 2. All other resources:
|
||||
# p, <user/group>, <resource>, <action>, <object>
|
||||
|
||||
p, role:readonly, applications, get, */*, allow
|
||||
p, role:readonly, clusters, get, *, allow
|
||||
p, role:readonly, repositories, get, *, allow
|
||||
p, role:readonly, projects, get, *, allow
|
||||
|
||||
p, role:admin, applications, create, */*, allow
|
||||
p, role:admin, applications, update, */*, allow
|
||||
p, role:admin, applications, delete, */*, allow
|
||||
p, role:admin, applications, sync, */*, allow
|
||||
p, role:admin, clusters, create, *, allow
|
||||
p, role:admin, clusters, update, *, allow
|
||||
p, role:admin, clusters, delete, *, allow
|
||||
p, role:admin, repositories, create, *, allow
|
||||
p, role:admin, repositories, update, *, allow
|
||||
p, role:admin, repositories, delete, *, allow
|
||||
p, role:admin, projects, create, *, allow
|
||||
p, role:admin, projects, update, *, allow
|
||||
p, role:admin, projects, delete, *, allow
|
||||
|
||||
g, role:admin, role:readonly
|
||||
g, admin, role:admin
|
||||
|
14
assets/model.conf
Normal file
@@ -0,0 +1,14 @@
|
||||
[request_definition]
|
||||
r = sub, res, act, obj
|
||||
|
||||
[policy_definition]
|
||||
p = sub, res, act, obj, eft
|
||||
|
||||
[role_definition]
|
||||
g = _, _
|
||||
|
||||
[policy_effect]
|
||||
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
|
||||
|
||||
[matchers]
|
||||
m = g(r.sub, p.sub) && keyMatch(r.res, p.res) && keyMatch(r.act, p.act) && keyMatch(r.obj, p.obj)
|
||||
3473
assets/swagger.json
Normal file
@@ -6,13 +6,6 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
argocd "github.com/argoproj/argo-cd"
|
||||
"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/server/cluster"
|
||||
apirepository "github.com/argoproj/argo-cd/server/repository"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
@@ -21,29 +14,50 @@ import (
|
||||
// load the gcp plugin (required to authenticate against GKE clusters).
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
||||
// load the oidc plugin (required to authenticate with OpenID Connect).
|
||||
"github.com/argoproj/argo-cd/reposerver"
|
||||
_ "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/util/cache"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
)
|
||||
|
||||
const (
|
||||
// CLIName is the name of the CLI
|
||||
cliName = "argocd-application-controller"
|
||||
// Default time in seconds for application resync period
|
||||
defaultAppResyncPeriod = 600
|
||||
defaultAppResyncPeriod = 180
|
||||
)
|
||||
|
||||
func newCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
appResyncPeriod int64
|
||||
repoServerAddress string
|
||||
clientConfig clientcmd.ClientConfig
|
||||
appResyncPeriod int64
|
||||
repoServerAddress string
|
||||
repoServerTimeoutSeconds int
|
||||
statusProcessors int
|
||||
operationProcessors int
|
||||
logLevel string
|
||||
glogLevel int
|
||||
cacheSrc func() (*cache.Cache, error)
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: cliName,
|
||||
Short: "application-controller is a controller to operate on applications CRD",
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
cli.SetLogLevel(logLevel)
|
||||
cli.SetGLogLevel(glogLevel)
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
config.QPS = common.K8sClientConfigQPS
|
||||
config.Burst = common.K8sClientConfigBurst
|
||||
|
||||
kubeClient := kubernetes.NewForConfigOrDie(config)
|
||||
appClient := appclientset.NewForConfigOrDie(config)
|
||||
@@ -51,30 +65,32 @@ func newCommand() *cobra.Command {
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
|
||||
// TODO (amatyushentsev): Use config map to store controller configuration
|
||||
controllerConfig := controller.ApplicationControllerConfig{
|
||||
Namespace: namespace,
|
||||
InstanceID: "",
|
||||
}
|
||||
resyncDuration := time.Duration(appResyncPeriod) * time.Second
|
||||
apiRepoServer := apirepository.NewServer(namespace, kubeClient, appClient)
|
||||
clusterService := cluster.NewServer(namespace, kubeClient, appClient)
|
||||
appComparator := controller.NewKsonnetAppComparator(clusterService)
|
||||
|
||||
appController := controller.NewApplicationController(
|
||||
kubeClient,
|
||||
appClient,
|
||||
reposerver.NewRepositoryServerClientset(repoServerAddress),
|
||||
apiRepoServer,
|
||||
appComparator,
|
||||
resyncDuration,
|
||||
&controllerConfig)
|
||||
|
||||
repoClientset := reposerver.NewRepoServerClientset(repoServerAddress, repoServerTimeoutSeconds)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
cache, err := cacheSrc()
|
||||
errors.CheckError(err)
|
||||
|
||||
settingsMgr := settings.NewSettingsManager(ctx, kubeClient, namespace)
|
||||
appController, err := controller.NewApplicationController(
|
||||
namespace,
|
||||
settingsMgr,
|
||||
kubeClient,
|
||||
appClient,
|
||||
repoClientset,
|
||||
cache,
|
||||
resyncDuration)
|
||||
errors.CheckError(err)
|
||||
|
||||
log.Infof("Application Controller (version: %s) starting (namespace: %s)", argocd.GetVersion(), namespace)
|
||||
go appController.Run(ctx, 1)
|
||||
stats.RegisterStackDumper()
|
||||
stats.StartStatsTicker(10 * time.Minute)
|
||||
stats.RegisterHeapDumper("memprofile")
|
||||
|
||||
go appController.Run(ctx, statusProcessors, operationProcessors)
|
||||
|
||||
// Wait forever
|
||||
select {}
|
||||
},
|
||||
@@ -82,7 +98,13 @@ 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", "localhost:8081", "Repo server address.")
|
||||
command.Flags().StringVar(&repoServerAddress, "repo-server", common.DefaultRepoServerAddr, "Repo server address.")
|
||||
command.Flags().IntVar(&repoServerTimeoutSeconds, "repo-server-timeout-seconds", 60, "Repo server RPC call timeout seconds.")
|
||||
command.Flags().IntVar(&statusProcessors, "status-processors", 1, "Number of application status processors")
|
||||
command.Flags().IntVar(&operationProcessors, "operation-processors", 1, "Number of application operation processors")
|
||||
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
|
||||
command.Flags().IntVar(&glogLevel, "gloglevel", 0, "Set the glog logging level")
|
||||
cacheSrc = cache.AddCacheFlagsToCmd(&command)
|
||||
return &command
|
||||
}
|
||||
|
||||
|
||||
@@ -3,67 +3,72 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
// load the gcp plugin (required to authenticate against GKE clusters).
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
||||
// load the oidc plugin (required to authenticate with OpenID Connect).
|
||||
"github.com/argoproj/argo-cd"
|
||||
argocd "github.com/argoproj/argo-cd"
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/reposerver"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
|
||||
"github.com/argoproj/argo-cd/util/cache"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
"github.com/argoproj/argo-cd/util/tls"
|
||||
)
|
||||
|
||||
const (
|
||||
// CLIName is the name of the CLI
|
||||
cliName = "argocd-repo-server"
|
||||
port = 8081
|
||||
)
|
||||
|
||||
func newCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
logLevel string
|
||||
logLevel string
|
||||
parallelismLimit int64
|
||||
cacheSrc func() (*cache.Cache, error)
|
||||
tlsConfigCustomizerSrc func() (tls.ConfigCustomizer, error)
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: cliName,
|
||||
Short: "Run argocd-repo-server",
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
level, err := log.ParseLevel(logLevel)
|
||||
errors.CheckError(err)
|
||||
log.SetLevel(level)
|
||||
cli.SetLogLevel(logLevel)
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
tlsConfigCustomizer, err := tlsConfigCustomizerSrc()
|
||||
errors.CheckError(err)
|
||||
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
cache, err := cacheSrc()
|
||||
errors.CheckError(err)
|
||||
|
||||
kubeClientset := kubernetes.NewForConfigOrDie(config)
|
||||
|
||||
server := reposerver.NewServer(kubeClientset, namespace)
|
||||
nativeGitClient, err := git.NewNativeGitClient()
|
||||
server, err := reposerver.NewServer(git.NewFactory(), cache, tlsConfigCustomizer, parallelismLimit)
|
||||
errors.CheckError(err)
|
||||
grpc := server.CreateGRPC(nativeGitClient)
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
grpc := server.CreateGRPC()
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", common.PortRepoServer))
|
||||
errors.CheckError(err)
|
||||
|
||||
log.Infof("argocd-repo-server %s serving on %s (namespace: %s)", argocd.GetVersion(), listener.Addr(), namespace)
|
||||
http.Handle("/metrics", promhttp.Handler())
|
||||
go func() { errors.CheckError(http.ListenAndServe(fmt.Sprintf(":%d", common.PortRepoServerMetrics), nil)) }()
|
||||
|
||||
log.Infof("argocd-repo-server %s serving on %s", argocd.GetVersion(), listener.Addr())
|
||||
stats.RegisterStackDumper()
|
||||
stats.StartStatsTicker(10 * time.Minute)
|
||||
stats.RegisterHeapDumper("memprofile")
|
||||
err = grpc.Serve(listener)
|
||||
errors.CheckError(err)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(&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.")
|
||||
tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(&command)
|
||||
cacheSrc = cache.AddCacheFlagsToCmd(&command)
|
||||
return &command
|
||||
}
|
||||
|
||||
|
||||
@@ -1,55 +1,103 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
"github.com/argoproj/argo-cd/reposerver"
|
||||
"github.com/argoproj/argo-cd/server"
|
||||
"github.com/argoproj/argo-cd/util/cache"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
"github.com/argoproj/argo-cd/util/tls"
|
||||
)
|
||||
|
||||
// NewCommand returns a new instance of an argocd command
|
||||
func NewCommand() *cobra.Command {
|
||||
var (
|
||||
logLevel string
|
||||
clientConfig clientcmd.ClientConfig
|
||||
staticAssetsDir string
|
||||
repoServerAddress string
|
||||
configMapName string
|
||||
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)
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: cliName,
|
||||
Short: "Run the argocd API server",
|
||||
Long: "Run the argocd API server",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
level, err := log.ParseLevel(logLevel)
|
||||
errors.CheckError(err)
|
||||
log.SetLevel(level)
|
||||
cli.SetLogLevel(logLevel)
|
||||
cli.SetGLogLevel(glogLevel)
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
config.QPS = common.K8sClientConfigQPS
|
||||
config.Burst = common.K8sClientConfigBurst
|
||||
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
|
||||
tlsConfigCustomizer, err := tlsConfigCustomizerSrc()
|
||||
errors.CheckError(err)
|
||||
cache, err := cacheSrc()
|
||||
errors.CheckError(err)
|
||||
|
||||
kubeclientset := kubernetes.NewForConfigOrDie(config)
|
||||
appclientset := appclientset.NewForConfigOrDie(config)
|
||||
repoclientset := reposerver.NewRepositoryServerClientset(repoServerAddress)
|
||||
repoclientset := reposerver.NewRepoServerClientset(repoServerAddress, 0)
|
||||
|
||||
argocd := server.NewServer(kubeclientset, appclientset, repoclientset, namespace, staticAssetsDir, configMapName)
|
||||
argocd.Run()
|
||||
argoCDOpts := server.ArgoCDServerOpts{
|
||||
Insecure: insecure,
|
||||
Namespace: namespace,
|
||||
StaticAssetsDir: staticAssetsDir,
|
||||
BaseHRef: baseHRef,
|
||||
KubeClientset: kubeclientset,
|
||||
AppClientset: appclientset,
|
||||
RepoClientset: repoclientset,
|
||||
DexServerAddr: dexServerAddress,
|
||||
DisableAuth: disableAuth,
|
||||
TLSConfigCustomizer: tlsConfigCustomizer,
|
||||
Cache: cache,
|
||||
}
|
||||
|
||||
stats.RegisterStackDumper()
|
||||
stats.StartStatsTicker(10 * time.Minute)
|
||||
stats.RegisterHeapDumper("memprofile")
|
||||
|
||||
for {
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
argocd := server.NewServer(ctx, argoCDOpts)
|
||||
argocd.Run(ctx, common.PortAPIServer)
|
||||
cancel()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(command)
|
||||
command.Flags().BoolVar(&insecure, "insecure", false, "Run server without TLS")
|
||||
command.Flags().StringVar(&staticAssetsDir, "staticassets", "", "Static assets directory path")
|
||||
command.Flags().StringVar(&baseHRef, "basehref", "/", "Value for base href in index.html. Used if Argo CD is running behind reverse proxy under subpath different from /")
|
||||
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
|
||||
command.Flags().StringVar(&repoServerAddress, "repo-server", "localhost:8081", "Repo server address.")
|
||||
command.Flags().StringVar(&configMapName, "config-map", "", "Name of a Kubernetes config map to use.")
|
||||
command.Flags().IntVar(&glogLevel, "gloglevel", 0, "Set the glog logging level")
|
||||
command.Flags().StringVar(&repoServerAddress, "repo-server", common.DefaultRepoServerAddr, "Repo server address")
|
||||
command.Flags().StringVar(&dexServerAddress, "dex-server", common.DefaultDexServerAddr, "Dex server address")
|
||||
command.Flags().BoolVar(&disableAuth, "disable-auth", false, "Disable client authentication")
|
||||
command.AddCommand(cli.NewVersionCmd(cliName))
|
||||
tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(command)
|
||||
cacheSrc = cache.AddCacheFlagsToCmd(command)
|
||||
return command
|
||||
}
|
||||
|
||||
550
cmd/argocd-util/main.go
Normal file
@@ -0,0 +1,550 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"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/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/dex"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
|
||||
// load the gcp plugin (required to authenticate against GKE clusters).
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
||||
// load the oidc plugin (required to authenticate with OpenID Connect).
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
|
||||
)
|
||||
|
||||
const (
|
||||
// CLIName is the name of the CLI
|
||||
cliName = "argocd-util"
|
||||
// YamlSeparator separates sections of a YAML file
|
||||
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
|
||||
func NewCommand() *cobra.Command {
|
||||
var (
|
||||
logLevel string
|
||||
)
|
||||
|
||||
var command = &cobra.Command{
|
||||
Use: cliName,
|
||||
Short: "argocd-util has internal tools used by Argo CD",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
},
|
||||
}
|
||||
|
||||
command.AddCommand(cli.NewVersionCmd(cliName))
|
||||
command.AddCommand(NewRunDexCommand())
|
||||
command.AddCommand(NewGenDexConfigCommand())
|
||||
command.AddCommand(NewImportCommand())
|
||||
command.AddCommand(NewExportCommand())
|
||||
command.AddCommand(NewClusterConfig())
|
||||
|
||||
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
|
||||
return command
|
||||
}
|
||||
|
||||
func NewRunDexCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: "rundex",
|
||||
Short: "Runs dex generating a config using settings from the Argo CD configmap and secret",
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
_, err := exec.LookPath("dex")
|
||||
errors.CheckError(err)
|
||||
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)
|
||||
prevSettings, err := settingsMgr.GetSettings()
|
||||
errors.CheckError(err)
|
||||
updateCh := make(chan *settings.ArgoCDSettings, 1)
|
||||
settingsMgr.Subscribe(updateCh)
|
||||
|
||||
for {
|
||||
var cmd *exec.Cmd
|
||||
dexCfgBytes, err := dex.GenerateDexConfigYAML(prevSettings)
|
||||
errors.CheckError(err)
|
||||
if len(dexCfgBytes) == 0 {
|
||||
log.Infof("dex is not configured")
|
||||
} else {
|
||||
err = ioutil.WriteFile("/tmp/dex.yaml", dexCfgBytes, 0644)
|
||||
errors.CheckError(err)
|
||||
log.Info(string(dexCfgBytes))
|
||||
cmd = exec.Command("dex", "serve", "/tmp/dex.yaml")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
err = cmd.Start()
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
// loop until the dex config changes
|
||||
for {
|
||||
newSettings := <-updateCh
|
||||
newDexCfgBytes, err := dex.GenerateDexConfigYAML(newSettings)
|
||||
errors.CheckError(err)
|
||||
if string(newDexCfgBytes) != string(dexCfgBytes) {
|
||||
prevSettings = newSettings
|
||||
log.Infof("dex config modified. restarting dex")
|
||||
if cmd != nil && cmd.Process != nil {
|
||||
err = cmd.Process.Signal(syscall.SIGTERM)
|
||||
errors.CheckError(err)
|
||||
_, err = cmd.Process.Wait()
|
||||
errors.CheckError(err)
|
||||
}
|
||||
break
|
||||
} else {
|
||||
log.Infof("dex config unmodified")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(&command)
|
||||
return &command
|
||||
}
|
||||
|
||||
func NewGenDexConfigCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
out string
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: "gendexcfg",
|
||||
Short: "Generates a dex config from Argo CD settings",
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
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)
|
||||
dexCfgBytes, err := dex.GenerateDexConfigYAML(settings)
|
||||
errors.CheckError(err)
|
||||
if len(dexCfgBytes) == 0 {
|
||||
log.Infof("dex is not configured")
|
||||
return nil
|
||||
}
|
||||
if out == "" {
|
||||
dexCfg := make(map[string]interface{})
|
||||
err := yaml.Unmarshal(dexCfgBytes, &dexCfg)
|
||||
errors.CheckError(err)
|
||||
if staticClientsInterface, ok := dexCfg["staticClients"]; ok {
|
||||
if staticClients, ok := staticClientsInterface.([]interface{}); ok {
|
||||
for i := range staticClients {
|
||||
staticClient := staticClients[i]
|
||||
if mappings, ok := staticClient.(map[string]interface{}); ok {
|
||||
for key := range mappings {
|
||||
if key == "secret" {
|
||||
mappings[key] = "******"
|
||||
}
|
||||
}
|
||||
staticClients[i] = mappings
|
||||
}
|
||||
}
|
||||
dexCfg["staticClients"] = staticClients
|
||||
}
|
||||
}
|
||||
errors.CheckError(err)
|
||||
maskedDexCfgBytes, err := yaml.Marshal(dexCfg)
|
||||
errors.CheckError(err)
|
||||
fmt.Print(string(maskedDexCfgBytes))
|
||||
} else {
|
||||
err = ioutil.WriteFile(out, dexCfgBytes, 0644)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(&command)
|
||||
command.Flags().StringVarP(&out, "out", "o", "", "Output to the specified file instead of stdout")
|
||||
return &command
|
||||
}
|
||||
|
||||
// NewImportCommand defines a new command for exporting Kubernetes and Argo CD resources.
|
||||
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",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
config, err := clientConfig.ClientConfig()
|
||||
config.QPS = 100
|
||||
config.Burst = 50
|
||||
errors.CheckError(err)
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
acdClients := newArgoCDClientsets(config, namespace)
|
||||
|
||||
var input []byte
|
||||
if in := args[0]; in == "-" {
|
||||
input, err = ioutil.ReadAll(os.Stdin)
|
||||
} else {
|
||||
input, err = ioutil.ReadFile(in)
|
||||
}
|
||||
errors.CheckError(err)
|
||||
var dryRunMsg string
|
||||
if dryRun {
|
||||
dryRunMsg = " (dry run)"
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
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 (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
out string
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: "export",
|
||||
Short: "Export all Argo CD data to stdout (default) or a file",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
|
||||
var writer io.Writer
|
||||
if out == "-" {
|
||||
writer = os.Stdout
|
||||
} else {
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(&command)
|
||||
command.Flags().StringVarP(&out, "out", "o", "-", "Output to the specified file instead of stdout")
|
||||
|
||||
return &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 helmReposRAW, ok := cm.Data["helm.repositories"]; ok {
|
||||
helmRepoCreds := make([]settings.HelmRepoCredentials, 0)
|
||||
err := yaml.Unmarshal([]byte(helmReposRAW), &helmRepoCreds)
|
||||
errors.CheckError(err)
|
||||
for _, cred := range helmRepoCreds {
|
||||
if cred.CASecret != nil {
|
||||
referencedSecrets[cred.CASecret.Name] = true
|
||||
}
|
||||
if cred.CertSecret != nil {
|
||||
referencedSecrets[cred.CertSecret.Name] = true
|
||||
}
|
||||
if cred.KeySecret != nil {
|
||||
referencedSecrets[cred.KeySecret.Name] = true
|
||||
}
|
||||
if cred.UsernameSecret != nil {
|
||||
referencedSecrets[cred.UsernameSecret.Name] = true
|
||||
}
|
||||
if cred.PasswordSecret != nil {
|
||||
referencedSecrets[cred.PasswordSecret.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: "kubeconfig CLUSTER_URL OUTPUT_PATH",
|
||||
Short: "Generates kubeconfig for the specified cluster",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
serverUrl := args[0]
|
||||
output := args[1]
|
||||
conf, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
kubeclientset, err := kubernetes.NewForConfig(conf)
|
||||
errors.CheckError(err)
|
||||
|
||||
cluster, err := db.NewDB(namespace, settings.NewSettingsManager(context.Background(), kubeclientset, namespace), kubeclientset).GetCluster(context.Background(), serverUrl)
|
||||
errors.CheckError(err)
|
||||
err = kube.WriteKubeConfig(cluster.RESTConfig(), namespace, output)
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(command)
|
||||
return command
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := NewCommand().Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
95
cmd/argocd/commands/account.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"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"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
)
|
||||
|
||||
func NewAccountCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "account",
|
||||
Short: "Manage account settings",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
},
|
||||
}
|
||||
command.AddCommand(NewAccountUpdatePasswordCommand(clientOpts))
|
||||
return command
|
||||
}
|
||||
|
||||
func NewAccountUpdatePasswordCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
currentPassword string
|
||||
newPassword string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "update-password",
|
||||
Short: "Update password",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if currentPassword == "" {
|
||||
fmt.Print("*** Enter current password: ")
|
||||
password, err := terminal.ReadPassword(syscall.Stdin)
|
||||
errors.CheckError(err)
|
||||
currentPassword = string(password)
|
||||
fmt.Print("\n")
|
||||
}
|
||||
if newPassword == "" {
|
||||
var err error
|
||||
newPassword, err = cli.ReadAndConfirmPassword()
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
updatePasswordRequest := account.UpdatePasswordRequest{
|
||||
NewPassword: newPassword,
|
||||
CurrentPassword: currentPassword,
|
||||
}
|
||||
|
||||
acdClient := argocdclient.NewClientOrDie(clientOpts)
|
||||
conn, usrIf := acdClient.NewAccountClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
ctx := context.Background()
|
||||
_, err := usrIf.UpdatePassword(ctx, &updatePasswordRequest)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Password updated\n")
|
||||
|
||||
// Get a new JWT token after updating the password
|
||||
localCfg, err := localconfig.ReadLocalConfig(clientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
configCtx, err := localCfg.ResolveContext(clientOpts.Context)
|
||||
errors.CheckError(err)
|
||||
claims, err := configCtx.User.Claims()
|
||||
errors.CheckError(err)
|
||||
tokenString := passwordLogin(acdClient, claims.Subject, newPassword)
|
||||
localCfg.UpsertUser(localconfig.User{
|
||||
Name: localCfg.CurrentContext,
|
||||
AuthToken: tokenString,
|
||||
})
|
||||
err = localconfig.WriteLocalConfig(*localCfg, clientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Context '%s' updated\n", localCfg.CurrentContext)
|
||||
},
|
||||
}
|
||||
|
||||
command.Flags().StringVar(¤tPassword, "current-password", "", "current password you wish to change")
|
||||
command.Flags().StringVar(&newPassword, "new-password", "", "new password you want to update to")
|
||||
return command
|
||||
}
|
||||
150
cmd/argocd/commands/app_actions.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/server/application"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
)
|
||||
|
||||
// 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 all bool
|
||||
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, &application.ResourcesQuery{ApplicationName: &appName})
|
||||
errors.CheckError(err)
|
||||
filteredObjects := filterResources(command, resources.Items, group, kind, namespace, resourceName, all)
|
||||
availableActions := make(map[string][]argoappv1.ResourceAction)
|
||||
for i := range filteredObjects {
|
||||
obj := filteredObjects[i]
|
||||
gvk := obj.GroupVersionKind()
|
||||
availActionsForResource, err := appIf.ListResourceActions(ctx, &application.ApplicationResourceRequest{
|
||||
Name: &appName,
|
||||
Namespace: obj.GetNamespace(),
|
||||
ResourceName: obj.GetName(),
|
||||
Group: gvk.Group,
|
||||
Kind: gvk.Kind,
|
||||
})
|
||||
errors.CheckError(err)
|
||||
availableActions[obj.GetName()] = availActionsForResource.Actions
|
||||
}
|
||||
|
||||
var keys []string
|
||||
for key := range availableActions {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "RESOURCE\tACTION\n")
|
||||
fmt.Println()
|
||||
for key := range availableActions {
|
||||
for i := range availableActions[key] {
|
||||
action := availableActions[key][i]
|
||||
fmt.Fprintf(w, "%s\t%s\n", key, action.Name)
|
||||
|
||||
}
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
command.Flags().StringVar(&resourceName, "resource-name", "", "Name of resource")
|
||||
command.Flags().StringVar(&kind, "kind", "", "Kind")
|
||||
err := command.MarkFlagRequired("kind")
|
||||
errors.CheckError(err)
|
||||
command.Flags().StringVar(&group, "group", "", "Group")
|
||||
command.Flags().StringVar(&namespace, "namespace", "", "Namespace")
|
||||
command.Flags().BoolVar(&all, "all", false, "Indicates whether to list actions on multiple matching resources")
|
||||
|
||||
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 kind string
|
||||
var group string
|
||||
var resourceName 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(&kind, "kind", "", "Kind")
|
||||
err := command.MarkFlagRequired("kind")
|
||||
errors.CheckError(err)
|
||||
command.Flags().StringVar(&group, "group", "", "Group")
|
||||
command.Flags().StringVar(&namespace, "namespace", "", "Namespace")
|
||||
command.Flags().BoolVar(&all, "all", false, "Indicates whether to 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, &application.ResourcesQuery{ApplicationName: &appName})
|
||||
errors.CheckError(err)
|
||||
filteredObjects := filterResources(command, resources.Items, group, kind, namespace, resourceName, all)
|
||||
for i := range filteredObjects {
|
||||
obj := filteredObjects[i]
|
||||
gvk := obj.GroupVersionKind()
|
||||
objResourceName := obj.GetName()
|
||||
_, err := appIf.RunResourceAction(context.Background(), &application.ResourceActionRunRequest{
|
||||
Name: &appName,
|
||||
Namespace: obj.GetNamespace(),
|
||||
ResourceName: objResourceName,
|
||||
Group: gvk.Group,
|
||||
Kind: gvk.Kind,
|
||||
Action: actionName,
|
||||
})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
}
|
||||
return command
|
||||
}
|
||||
25
cmd/argocd/commands/app_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
}
|
||||
@@ -9,24 +9,26 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"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/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/server/cluster"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/ghodss/yaml"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
// NewClusterCommand returns a new instance of an `argocd cluster` command
|
||||
func NewClusterCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clientcmd.PathOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "cluster",
|
||||
Short: fmt.Sprintf("%s cluster COMMAND", cliName),
|
||||
Short: "Manage cluster credentials",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
@@ -42,6 +44,12 @@ func NewClusterCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clientc
|
||||
|
||||
// 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
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "add",
|
||||
Short: fmt.Sprintf("%s cluster add CONTEXT", cliName),
|
||||
@@ -49,7 +57,7 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
var configAccess clientcmd.ConfigAccess = pathOpts
|
||||
if len(args) == 0 {
|
||||
log.Error("Choose a context name from:")
|
||||
printContexts(configAccess)
|
||||
printKubeContexts(configAccess)
|
||||
os.Exit(1)
|
||||
}
|
||||
config, err := configAccess.GetStartingConfig()
|
||||
@@ -58,6 +66,7 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
if clstContext == nil {
|
||||
log.Fatalf("Context %s does not exist in kubeconfig", args[0])
|
||||
}
|
||||
|
||||
overrides := clientcmd.ConfigOverrides{
|
||||
Context: *clstContext,
|
||||
}
|
||||
@@ -65,29 +74,49 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
conf, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
|
||||
if conf.Username == "" || conf.Password == "" {
|
||||
// Install RBAC resources for managing the cluster if username and password are not specified
|
||||
conf.BearerToken = common.InstallClusterManagerRBAC(conf)
|
||||
managerBearerToken := ""
|
||||
var awsAuthConf *argoappv1.AWSAuthConfig
|
||||
if awsClusterName != "" {
|
||||
awsAuthConf = &argoappv1.AWSAuthConfig{
|
||||
ClusterName: awsClusterName,
|
||||
RoleARN: awsRoleArn,
|
||||
}
|
||||
} else {
|
||||
// Install RBAC resources for managing the cluster
|
||||
clientset, err := kubernetes.NewForConfig(conf)
|
||||
errors.CheckError(err)
|
||||
managerBearerToken, err = common.InstallClusterManagerRBAC(clientset)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
|
||||
defer util.Close(conn)
|
||||
clst := NewCluster(args[0], conf)
|
||||
clst, err = clusterIf.Create(context.Background(), clst)
|
||||
clst := NewCluster(args[0], conf, managerBearerToken, awsAuthConf)
|
||||
if inCluster {
|
||||
clst.Server = common.KubernetesInternalAPIServerAddr
|
||||
}
|
||||
clstCreateReq := cluster.ClusterCreateRequest{
|
||||
Cluster: clst,
|
||||
Upsert: upsert,
|
||||
}
|
||||
clst, err = clusterIf.Create(context.Background(), &clstCreateReq)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Cluster '%s' added\n", clst.Name)
|
||||
},
|
||||
}
|
||||
command.PersistentFlags().StringVar(&pathOpts.LoadingRules.ExplicitPath, pathOpts.ExplicitFileFlag, pathOpts.LoadingRules.ExplicitPath, "use a particular kubeconfig file")
|
||||
command.Flags().BoolVar(&inCluster, "in-cluster", false, "Indicates Argo CD resides inside this cluster and should connect using the internal k8s hostname (kubernetes.default.svc)")
|
||||
command.Flags().BoolVar(&upsert, "upsert", false, "Override an existing cluster with the same name even if the spec differs")
|
||||
command.Flags().StringVar(&awsClusterName, "aws-cluster-name", "", "AWS Cluster name if set then aws-iam-authenticator will be used to access cluster")
|
||||
command.Flags().StringVar(&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.")
|
||||
return command
|
||||
}
|
||||
|
||||
func printContexts(ca clientcmd.ConfigAccess) {
|
||||
func printKubeContexts(ca clientcmd.ConfigAccess) {
|
||||
config, err := ca.GetStartingConfig()
|
||||
errors.CheckError(err)
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
defer func() { _ = w.Flush() }()
|
||||
columnNames := []string{"CURRENT", "NAME", "CLUSTER", "AUTHINFO", "NAMESPACE"}
|
||||
columnNames := []string{"CURRENT", "NAME", "CLUSTER", "SERVER"}
|
||||
_, err = fmt.Fprintf(w, "%s\n", strings.Join(columnNames, "\t"))
|
||||
errors.CheckError(err)
|
||||
|
||||
@@ -98,35 +127,35 @@ func printContexts(ca clientcmd.ConfigAccess) {
|
||||
}
|
||||
sort.Strings(contextNames)
|
||||
|
||||
if config.Clusters == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, name := range contextNames {
|
||||
// ignore malformed kube config entries
|
||||
context := config.Contexts[name]
|
||||
if context == nil {
|
||||
continue
|
||||
}
|
||||
cluster := config.Clusters[context.Cluster]
|
||||
if cluster == nil {
|
||||
continue
|
||||
}
|
||||
prefix := " "
|
||||
if config.CurrentContext == name {
|
||||
prefix = "*"
|
||||
}
|
||||
_, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", prefix, name, context.Cluster, context.AuthInfo, context.Namespace)
|
||||
_, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", prefix, name, context.Cluster, cluster.Server)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
}
|
||||
|
||||
func NewCluster(name string, conf *rest.Config) *argoappv1.Cluster {
|
||||
func NewCluster(name string, conf *rest.Config, managerBearerToken string, awsAuthConf *argoappv1.AWSAuthConfig) *argoappv1.Cluster {
|
||||
tlsClientConfig := argoappv1.TLSClientConfig{
|
||||
Insecure: conf.TLSClientConfig.Insecure,
|
||||
ServerName: conf.TLSClientConfig.ServerName,
|
||||
CertData: conf.TLSClientConfig.CertData,
|
||||
KeyData: conf.TLSClientConfig.KeyData,
|
||||
CAData: conf.TLSClientConfig.CAData,
|
||||
}
|
||||
if len(conf.TLSClientConfig.CertData) == 0 && conf.TLSClientConfig.CertFile != "" {
|
||||
data, err := ioutil.ReadFile(conf.TLSClientConfig.CertFile)
|
||||
errors.CheckError(err)
|
||||
tlsClientConfig.CertData = data
|
||||
}
|
||||
if len(conf.TLSClientConfig.KeyData) == 0 && conf.TLSClientConfig.KeyFile != "" {
|
||||
data, err := ioutil.ReadFile(conf.TLSClientConfig.KeyFile)
|
||||
errors.CheckError(err)
|
||||
tlsClientConfig.KeyData = data
|
||||
}
|
||||
if len(conf.TLSClientConfig.CAData) == 0 && conf.TLSClientConfig.CAFile != "" {
|
||||
data, err := ioutil.ReadFile(conf.TLSClientConfig.CAFile)
|
||||
errors.CheckError(err)
|
||||
@@ -136,10 +165,9 @@ func NewCluster(name string, conf *rest.Config) *argoappv1.Cluster {
|
||||
Server: conf.Host,
|
||||
Name: name,
|
||||
Config: argoappv1.ClusterConfig{
|
||||
Username: conf.Username,
|
||||
Password: conf.Password,
|
||||
BearerToken: conf.BearerToken,
|
||||
BearerToken: managerBearerToken,
|
||||
TLSClientConfig: tlsClientConfig,
|
||||
AWSAuthConfig: awsAuthConf,
|
||||
},
|
||||
}
|
||||
return &clst
|
||||
@@ -149,7 +177,7 @@ func NewCluster(name string, conf *rest.Config) *argoappv1.Cluster {
|
||||
func NewClusterGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "get",
|
||||
Short: fmt.Sprintf("%s cluster get SERVER", cliName),
|
||||
Short: "Get cluster information",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
@@ -173,7 +201,7 @@ func NewClusterGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
func NewClusterRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "rm",
|
||||
Short: fmt.Sprintf("%s cluster rm SERVER", cliName),
|
||||
Short: "Remove cluster credentials",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
@@ -181,9 +209,14 @@ func NewClusterRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
}
|
||||
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
// clientset, err := kubernetes.NewForConfig(conf)
|
||||
// errors.CheckError(err)
|
||||
|
||||
for _, clusterName := range args {
|
||||
// TODO(jessesuen): find the right context and remove manager RBAC artifacts
|
||||
// common.UninstallClusterManagerRBAC(conf)
|
||||
// err := common.UninstallClusterManagerRBAC(clientset)
|
||||
// errors.CheckError(err)
|
||||
_, err := clusterIf.Delete(context.Background(), &cluster.ClusterQuery{Server: clusterName})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
@@ -196,16 +229,16 @@ func NewClusterRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
func NewClusterListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: fmt.Sprintf("%s cluster list", cliName),
|
||||
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{})
|
||||
errors.CheckError(err)
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "SERVER\tNAME\n")
|
||||
fmt.Fprintf(w, "SERVER\tNAME\tSTATUS\tMESSAGE\n")
|
||||
for _, c := range clusters.Items {
|
||||
fmt.Fprintf(w, "%s\t%s\n", c.Server, c.Name)
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", c.Server, c.Name, c.ConnectionState.Status, c.ConnectionState.Message)
|
||||
}
|
||||
_ = w.Flush()
|
||||
},
|
||||
|
||||
@@ -2,4 +2,8 @@ package commands
|
||||
|
||||
const (
|
||||
cliName = "argocd"
|
||||
|
||||
// DefaultSSOLocalPort is the localhost port to listen on for the temporary web server performing
|
||||
// the OAuth2 login flow.
|
||||
DefaultSSOLocalPort = 8085
|
||||
)
|
||||
|
||||
85
cmd/argocd/commands/context.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
)
|
||||
|
||||
// NewContextCommand returns a new instance of an `argocd ctx` command
|
||||
func NewContextCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "context",
|
||||
Aliases: []string{"ctx"},
|
||||
Short: "Switch between contexts",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
printArgoCDContexts(clientOpts.ConfigPath)
|
||||
return
|
||||
}
|
||||
ctxName := args[0]
|
||||
argoCDDir, err := localconfig.DefaultConfigDir()
|
||||
errors.CheckError(err)
|
||||
prevCtxFile := path.Join(argoCDDir, ".prev-ctx")
|
||||
|
||||
if ctxName == "-" {
|
||||
prevCtxBytes, err := ioutil.ReadFile(prevCtxFile)
|
||||
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
|
||||
}
|
||||
if _, err = localCfg.ResolveContext(ctxName); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
prevCtx := localCfg.CurrentContext
|
||||
localCfg.CurrentContext = ctxName
|
||||
err = localconfig.WriteLocalConfig(*localCfg, clientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
err = ioutil.WriteFile(prevCtxFile, []byte(prevCtx), 0644)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Switched to context '%s'\n", localCfg.CurrentContext)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
func printArgoCDContexts(configPath string) {
|
||||
localCfg, err := localconfig.ReadLocalConfig(configPath)
|
||||
errors.CheckError(err)
|
||||
if localCfg == nil {
|
||||
log.Fatalf("No contexts defined in %s", configPath)
|
||||
}
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
defer func() { _ = w.Flush() }()
|
||||
columnNames := []string{"CURRENT", "NAME", "SERVER"}
|
||||
_, err = fmt.Fprintf(w, "%s\n", strings.Join(columnNames, "\t"))
|
||||
errors.CheckError(err)
|
||||
|
||||
for _, contextRef := range localCfg.Contexts {
|
||||
context, err := localCfg.ResolveContext(contextRef.Name)
|
||||
if err != nil {
|
||||
log.Warnf("Context '%s' had error: %v", contextRef.Name, err)
|
||||
}
|
||||
prefix := " "
|
||||
if localCfg.CurrentContext == context.Name {
|
||||
prefix = "*"
|
||||
}
|
||||
_, err = fmt.Fprintf(w, "%s\t%s\t%s\n", prefix, context.Name, context.Server.Server)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/install"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
// NewInstallCommand returns a new instance of `argocd install` command
|
||||
func NewInstallCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
installOpts install.InstallOptions
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "install",
|
||||
Short: "Install Argo CD",
|
||||
Long: "Install Argo CD",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
conf, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
installer, err := install.NewInstaller(conf, installOpts)
|
||||
errors.CheckError(err)
|
||||
installer.Install()
|
||||
},
|
||||
}
|
||||
command.Flags().BoolVar(&installOpts.Upgrade, "upgrade", false, "upgrade controller/ui deployments and configmap if already installed")
|
||||
command.Flags().BoolVar(&installOpts.DryRun, "dry-run", false, "print the kubernetes manifests to stdout instead of installing")
|
||||
command.Flags().BoolVar(&installOpts.ConfigSuperuser, "config-superuser", false, "create or update a superuser username and password")
|
||||
command.Flags().StringVar(&installOpts.ConfigMap, "config-map", "", "apply settings from a Kubernetes config map")
|
||||
command.Flags().StringVar(&installOpts.Namespace, "install-namespace", install.DefaultInstallNamespace, "install into a specific namespace")
|
||||
command.Flags().StringVar(&installOpts.ControllerImage, "controller-image", install.DefaultControllerImage, "use a specified controller image")
|
||||
command.Flags().StringVar(&installOpts.ServerImage, "server-image", install.DefaultServerImage, "use a specified api server image")
|
||||
command.Flags().StringVar(&installOpts.UIImage, "ui-image", install.DefaultUIImage, "use a specified ui image")
|
||||
command.Flags().StringVar(&installOpts.RepoServerImage, "repo-server-image", install.DefaultRepoServerImage, "use a specified repo server image")
|
||||
command.Flags().StringVar(&installOpts.ImagePullPolicy, "image-pull-policy", "", "set the image pull policy of the pod specs")
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(command)
|
||||
return command
|
||||
}
|
||||
288
cmd/argocd/commands/login.go
Normal file
@@ -0,0 +1,288 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"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"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
grpc_util "github.com/argoproj/argo-cd/util/grpc"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
oidcutil "github.com/argoproj/argo-cd/util/oidc"
|
||||
"github.com/argoproj/argo-cd/util/rand"
|
||||
)
|
||||
|
||||
// NewLoginCommand returns a new instance of `argocd login` command
|
||||
func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
ctxName string
|
||||
username string
|
||||
password string
|
||||
sso bool
|
||||
ssoPort int
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "login SERVER",
|
||||
Short: "Log in to Argo CD",
|
||||
Long: "Log in to Argo CD",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
server := args[0]
|
||||
tlsTestResult, err := grpc_util.TestTLS(server)
|
||||
errors.CheckError(err)
|
||||
if !tlsTestResult.TLS {
|
||||
if !globalClientOpts.PlainText {
|
||||
if !cli.AskToProceed("WARNING: server is not configured with TLS. Proceed (y/n)? ") {
|
||||
os.Exit(1)
|
||||
}
|
||||
globalClientOpts.PlainText = true
|
||||
}
|
||||
} else if tlsTestResult.InsecureErr != nil {
|
||||
if !globalClientOpts.Insecure {
|
||||
if !cli.AskToProceed(fmt.Sprintf("WARNING: server certificate had error: %s. Proceed insecurely (y/n)? ", tlsTestResult.InsecureErr)) {
|
||||
os.Exit(1)
|
||||
}
|
||||
globalClientOpts.Insecure = true
|
||||
}
|
||||
}
|
||||
clientOpts := argocdclient.ClientOptions{
|
||||
ConfigPath: "",
|
||||
ServerAddr: server,
|
||||
Insecure: globalClientOpts.Insecure,
|
||||
PlainText: globalClientOpts.PlainText,
|
||||
GRPCWeb: globalClientOpts.GRPCWeb,
|
||||
}
|
||||
acdClient := argocdclient.NewClientOrDie(&clientOpts)
|
||||
setConn, setIf := acdClient.NewSettingsClientOrDie()
|
||||
defer util.Close(setConn)
|
||||
|
||||
if ctxName == "" {
|
||||
ctxName = server
|
||||
}
|
||||
|
||||
// Perform the login
|
||||
var tokenString string
|
||||
var refreshToken string
|
||||
if !sso {
|
||||
tokenString = passwordLogin(acdClient, username, password)
|
||||
} else {
|
||||
ctx := context.Background()
|
||||
httpClient, err := acdClient.HTTPClient()
|
||||
errors.CheckError(err)
|
||||
ctx = oidc.ClientContext(ctx, httpClient)
|
||||
acdSet, err := setIf.Get(ctx, &settings.SettingsQuery{})
|
||||
errors.CheckError(err)
|
||||
oauth2conf, provider, err := acdClient.OIDCConfig(ctx, acdSet)
|
||||
errors.CheckError(err)
|
||||
tokenString, refreshToken = oauth2Login(ctx, ssoPort, oauth2conf, provider)
|
||||
}
|
||||
|
||||
parser := &jwt.Parser{
|
||||
SkipClaimsValidation: true,
|
||||
}
|
||||
claims := jwt.MapClaims{}
|
||||
_, _, err = parser.ParseUnverified(tokenString, &claims)
|
||||
errors.CheckError(err)
|
||||
|
||||
fmt.Printf("'%s' logged in successfully\n", userDisplayName(claims))
|
||||
// login successful. Persist the config
|
||||
localCfg, err := localconfig.ReadLocalConfig(globalClientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
if localCfg == nil {
|
||||
localCfg = &localconfig.LocalConfig{}
|
||||
}
|
||||
localCfg.UpsertServer(localconfig.Server{
|
||||
Server: server,
|
||||
PlainText: globalClientOpts.PlainText,
|
||||
Insecure: globalClientOpts.Insecure,
|
||||
GRPCWeb: globalClientOpts.GRPCWeb,
|
||||
})
|
||||
localCfg.UpsertUser(localconfig.User{
|
||||
Name: ctxName,
|
||||
AuthToken: tokenString,
|
||||
RefreshToken: refreshToken,
|
||||
})
|
||||
if ctxName == "" {
|
||||
ctxName = server
|
||||
}
|
||||
localCfg.CurrentContext = ctxName
|
||||
localCfg.UpsertContext(localconfig.ContextRef{
|
||||
Name: ctxName,
|
||||
User: ctxName,
|
||||
Server: server,
|
||||
})
|
||||
err = localconfig.WriteLocalConfig(*localCfg, globalClientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Context '%s' updated\n", ctxName)
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&ctxName, "name", "", "name to use for the context")
|
||||
command.Flags().StringVar(&username, "username", "", "the username of an account to authenticate")
|
||||
command.Flags().StringVar(&password, "password", "", "the password of an account to authenticate")
|
||||
command.Flags().BoolVar(&sso, "sso", false, "perform SSO login")
|
||||
command.Flags().IntVar(&ssoPort, "sso-port", DefaultSSOLocalPort, "port to run local OAuth2 login application")
|
||||
return command
|
||||
}
|
||||
|
||||
func userDisplayName(claims jwt.MapClaims) string {
|
||||
if email, ok := claims["email"]; ok && email != nil {
|
||||
return email.(string)
|
||||
}
|
||||
if name, ok := claims["name"]; ok && name != nil {
|
||||
return name.(string)
|
||||
}
|
||||
return claims["sub"].(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) {
|
||||
oauth2conf.RedirectURL = fmt.Sprintf("http://localhost:%d/auth/callback", port)
|
||||
oidcConf, err := oidcutil.ParseConfig(provider)
|
||||
errors.CheckError(err)
|
||||
log.Debug("OIDC Configuration:")
|
||||
log.Debugf(" supported_scopes: %v", oidcConf.ScopesSupported)
|
||||
log.Debugf(" response_types_supported: %v", oidcConf.ResponseTypesSupported)
|
||||
|
||||
// handledRequests ensures we do not handle more requests than necessary
|
||||
handledRequests := 0
|
||||
// completionChan is to signal flow completed. Non-empty string indicates error
|
||||
completionChan := make(chan string)
|
||||
// stateNonce is an OAuth2 state nonce
|
||||
stateNonce := rand.RandString(10)
|
||||
var tokenString string
|
||||
var refreshToken string
|
||||
|
||||
handleErr := func(w http.ResponseWriter, errMsg string) {
|
||||
http.Error(w, errMsg, http.StatusBadRequest)
|
||||
completionChan <- errMsg
|
||||
}
|
||||
|
||||
// Authorization redirect callback from OAuth2 auth flow.
|
||||
// Handles both implicit and authorization code flow
|
||||
callbackHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Debugf("Callback: %s", r.URL)
|
||||
|
||||
if formErr := r.FormValue("error"); formErr != "" {
|
||||
handleErr(w, formErr+": "+r.FormValue("error_description"))
|
||||
return
|
||||
}
|
||||
|
||||
handledRequests++
|
||||
if handledRequests > 2 {
|
||||
// Since implicit flow will redirect back to ourselves, this counter ensures we do not
|
||||
// fallinto a redirect loop (e.g. user visits the page by hand)
|
||||
handleErr(w, "Unable to complete login flow: too many redirects")
|
||||
return
|
||||
}
|
||||
|
||||
if len(r.Form) == 0 {
|
||||
// If we get here, no form data was set. We presume to be performing an implicit login
|
||||
// flow where the id_token is contained in a URL fragment, making it inaccessible to be
|
||||
// read from the request. This javascript will redirect the browser to send the
|
||||
// fragments as query parameters so our callback handler can read and return token.
|
||||
fmt.Fprintf(w, `<script>window.location.search = window.location.hash.substring(1)</script>`)
|
||||
return
|
||||
}
|
||||
|
||||
if state := r.FormValue("state"); state != stateNonce {
|
||||
handleErr(w, "Unknown state nonce")
|
||||
return
|
||||
}
|
||||
|
||||
tokenString = r.FormValue("id_token")
|
||||
if tokenString == "" {
|
||||
code := r.FormValue("code")
|
||||
if code == "" {
|
||||
handleErr(w, fmt.Sprintf("no code in request: %q", r.Form))
|
||||
return
|
||||
}
|
||||
tok, err := oauth2conf.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
handleErr(w, err.Error())
|
||||
return
|
||||
}
|
||||
var ok bool
|
||||
tokenString, ok = tok.Extra("id_token").(string)
|
||||
if !ok {
|
||||
handleErr(w, "no id_token in token response")
|
||||
return
|
||||
}
|
||||
refreshToken, _ = tok.Extra("refresh_token").(string)
|
||||
}
|
||||
successPage := `
|
||||
<div style="height:100px; width:100%!; display:flex; flex-direction: column; justify-content: center; align-items:center; background-color:#2ecc71; color:white; font-size:22"><div>Authentication successful!</div></div>
|
||||
<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.Fprint(w, successPage)
|
||||
completionChan <- ""
|
||||
}
|
||||
srv := &http.Server{Addr: "localhost:" + strconv.Itoa(port)}
|
||||
http.HandleFunc("/auth/callback", callbackHandler)
|
||||
|
||||
// Redirect user to login & consent page to ask for permission for the scopes specified above.
|
||||
fmt.Printf("Opening browser for authentication\n")
|
||||
|
||||
var url string
|
||||
grantType := oidcutil.InferGrantType(oauth2conf, oidcConf)
|
||||
switch grantType {
|
||||
case oidcutil.GrantTypeAuthorizationCode:
|
||||
url = oauth2conf.AuthCodeURL(stateNonce, oauth2.AccessTypeOffline)
|
||||
case oidcutil.GrantTypeImplicit:
|
||||
url = oidcutil.ImplicitFlowURL(oauth2conf, stateNonce, oauth2.AccessTypeOffline)
|
||||
default:
|
||||
log.Fatalf("Unsupported grant type: %v", grantType)
|
||||
}
|
||||
fmt.Printf("Performing %s flow login: %s\n", grantType, url)
|
||||
time.Sleep(1 * time.Second)
|
||||
err = open.Run(url)
|
||||
errors.CheckError(err)
|
||||
go func() {
|
||||
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
|
||||
log.Fatalf("listen: %s\n", err)
|
||||
}
|
||||
}()
|
||||
errMsg := <-completionChan
|
||||
if errMsg != "" {
|
||||
log.Fatal(errMsg)
|
||||
}
|
||||
fmt.Printf("Authentication successful\n")
|
||||
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
|
||||
defer cancel()
|
||||
_ = srv.Shutdown(ctx)
|
||||
log.Debugf("Token: %s", tokenString)
|
||||
log.Debugf("Refresh Token: %s", refreshToken)
|
||||
return tokenString, refreshToken
|
||||
}
|
||||
|
||||
func passwordLogin(acdClient argocdclient.Client, username, password string) string {
|
||||
username, password = cli.PromptCredentials(username, password)
|
||||
sessConn, sessionIf := acdClient.NewSessionClientOrDie()
|
||||
defer util.Close(sessConn)
|
||||
sessionRequest := session.SessionCreateRequest{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
createdSession, err := sessionIf.Create(context.Background(), &sessionRequest)
|
||||
errors.CheckError(err)
|
||||
return createdSession.Token
|
||||
}
|
||||
608
cmd/argocd/commands/project.go
Normal file
@@ -0,0 +1,608 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/ghodss/yaml"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"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/git"
|
||||
)
|
||||
|
||||
type projectOpts struct {
|
||||
description string
|
||||
destinations []string
|
||||
sources []string
|
||||
}
|
||||
|
||||
type policyOpts struct {
|
||||
action string
|
||||
permission string
|
||||
object string
|
||||
}
|
||||
|
||||
func (opts *projectOpts) GetDestinations() []v1alpha1.ApplicationDestination {
|
||||
destinations := make([]v1alpha1.ApplicationDestination, 0)
|
||||
for _, destStr := range opts.destinations {
|
||||
parts := strings.Split(destStr, ",")
|
||||
if len(parts) != 2 {
|
||||
log.Fatalf("Expected destination of the form: server,namespace. Received: %s", destStr)
|
||||
} else {
|
||||
destinations = append(destinations, v1alpha1.ApplicationDestination{
|
||||
Server: parts[0],
|
||||
Namespace: parts[1],
|
||||
})
|
||||
}
|
||||
}
|
||||
return destinations
|
||||
}
|
||||
|
||||
// NewProjectCommand returns a new instance of an `argocd proj` command
|
||||
func NewProjectCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "proj",
|
||||
Short: "Manage projects",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
},
|
||||
}
|
||||
command.AddCommand(NewProjectRoleCommand(clientOpts))
|
||||
command.AddCommand(NewProjectCreateCommand(clientOpts))
|
||||
command.AddCommand(NewProjectGetCommand(clientOpts))
|
||||
command.AddCommand(NewProjectDeleteCommand(clientOpts))
|
||||
command.AddCommand(NewProjectListCommand(clientOpts))
|
||||
command.AddCommand(NewProjectSetCommand(clientOpts))
|
||||
command.AddCommand(NewProjectEditCommand(clientOpts))
|
||||
command.AddCommand(NewProjectAddDestinationCommand(clientOpts))
|
||||
command.AddCommand(NewProjectRemoveDestinationCommand(clientOpts))
|
||||
command.AddCommand(NewProjectAddSourceCommand(clientOpts))
|
||||
command.AddCommand(NewProjectRemoveSourceCommand(clientOpts))
|
||||
command.AddCommand(NewProjectAllowClusterResourceCommand(clientOpts))
|
||||
command.AddCommand(NewProjectDenyClusterResourceCommand(clientOpts))
|
||||
command.AddCommand(NewProjectAllowNamespaceResourceCommand(clientOpts))
|
||||
command.AddCommand(NewProjectDenyNamespaceResourceCommand(clientOpts))
|
||||
return command
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
func addPolicyFlags(command *cobra.Command, opts *policyOpts) {
|
||||
command.Flags().StringVarP(&opts.action, "action", "a", "", "Action to grant/deny permission on (e.g. get, create, list, update, delete)")
|
||||
command.Flags().StringVarP(&opts.permission, "permission", "p", "allow", "Whether to allow or deny access to object with the action. This can only be 'allow' or 'deny'")
|
||||
command.Flags().StringVarP(&opts.object, "object", "o", "", "Object within the project to grant/deny access. Use '*' for a wildcard. Will want access to '<project>/<object>'")
|
||||
}
|
||||
|
||||
func humanizeTimestamp(epoch int64) string {
|
||||
ts := time.Unix(epoch, 0)
|
||||
return fmt.Sprintf("%s (%s)", ts.Format(time.RFC3339), humanize.Time(ts))
|
||||
}
|
||||
|
||||
// NewProjectCreateCommand returns a new instance of an `argocd proj create` command
|
||||
func NewProjectCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
opts projectOpts
|
||||
)
|
||||
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,
|
||||
},
|
||||
}
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
_, err := projIf.Create(context.Background(), &project.ProjectCreateRequest{Project: &proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
addProjFlags(command, &opts)
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectSetCommand returns a new instance of an `argocd proj set` command
|
||||
func NewProjectSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
opts projectOpts
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "set PROJECT",
|
||||
Short: "Set project parameters",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
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(), &project.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
visited := 0
|
||||
c.Flags().Visit(func(f *pflag.Flag) {
|
||||
visited++
|
||||
switch f.Name {
|
||||
case "description":
|
||||
proj.Spec.Description = opts.description
|
||||
case "dest":
|
||||
proj.Spec.Destinations = opts.GetDestinations()
|
||||
case "src":
|
||||
proj.Spec.SourceRepos = opts.sources
|
||||
}
|
||||
})
|
||||
if visited == 0 {
|
||||
log.Error("Please set at least one option to update")
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
addProjFlags(command, &opts)
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectAddDestinationCommand returns a new instance of an `argocd proj add-destination` command
|
||||
func NewProjectAddDestinationCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "add-destination PROJECT SERVER NAMESPACE",
|
||||
Short: "Add project destination",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
server := args[1]
|
||||
namespace := args[2]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
for _, dest := range proj.Spec.Destinations {
|
||||
if dest.Namespace == namespace && dest.Server == server {
|
||||
log.Fatal("Specified destination is already defined in project")
|
||||
}
|
||||
}
|
||||
proj.Spec.Destinations = append(proj.Spec.Destinations, v1alpha1.ApplicationDestination{Server: server, Namespace: namespace})
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectRemoveDestinationCommand returns a new instance of an `argocd proj remove-destination` command
|
||||
func NewProjectRemoveDestinationCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "remove-destination PROJECT SERVER NAMESPACE",
|
||||
Short: "Remove project destination",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
server := args[1]
|
||||
namespace := args[2]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
index := -1
|
||||
for i, dest := range proj.Spec.Destinations {
|
||||
if dest.Namespace == namespace && dest.Server == server {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
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})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectAddSourceCommand returns a new instance of an `argocd proj add-src` command
|
||||
func NewProjectAddSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "add-source PROJECT URL",
|
||||
Short: "Add project source repository",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
url := args[1]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
for _, item := range proj.Spec.SourceRepos {
|
||||
if item == "*" && item == url {
|
||||
fmt.Printf("Source repository '*' already allowed in project\n")
|
||||
return
|
||||
}
|
||||
if git.SameURL(item, url) {
|
||||
fmt.Printf("Source repository '%s' already allowed in project\n", item)
|
||||
return
|
||||
}
|
||||
}
|
||||
proj.Spec.SourceRepos = append(proj.Spec.SourceRepos, url)
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
func modifyProjectResourceCmd(cmdUse, cmdDesc string, clientOpts *argocdclient.ClientOptions, action func(proj *v1alpha1.AppProject, group string, kind string) bool) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: cmdUse,
|
||||
Short: cmdDesc,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName, group, kind := args[0], args[1], args[2]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
if action(proj, group, kind) {
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewProjectAllowNamespaceResourceCommand returns a new instance of an `deny-cluster-resources` command
|
||||
func NewProjectAllowNamespaceResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
use := "allow-namespace-resource PROJECT GROUP KIND"
|
||||
desc := "Removes a namespaced API resource from the blacklist"
|
||||
return modifyProjectResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
|
||||
index := -1
|
||||
for i, item := range proj.Spec.NamespaceResourceBlacklist {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
fmt.Printf("Group '%s' and kind '%s' not in blacklisted namespaced resources\n", group, kind)
|
||||
return false
|
||||
}
|
||||
proj.Spec.NamespaceResourceBlacklist = append(proj.Spec.NamespaceResourceBlacklist[:index], proj.Spec.NamespaceResourceBlacklist[index+1:]...)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// NewProjectDenyNamespaceResourceCommand returns a new instance of an `argocd proj deny-namespace-resource` command
|
||||
func NewProjectDenyNamespaceResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
use := "deny-namespace-resource PROJECT GROUP KIND"
|
||||
desc := "Adds a namespaced API resource to the blacklist"
|
||||
return modifyProjectResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
|
||||
for _, item := range proj.Spec.NamespaceResourceBlacklist {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
fmt.Printf("Group '%s' and kind '%s' already present in blacklisted namespaced resources\n", group, kind)
|
||||
return false
|
||||
}
|
||||
}
|
||||
proj.Spec.NamespaceResourceBlacklist = append(proj.Spec.NamespaceResourceBlacklist, v1.GroupKind{Group: group, Kind: kind})
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// NewProjectDenyClusterResourceCommand returns a new instance of an `deny-cluster-resource` command
|
||||
func NewProjectDenyClusterResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
use := "deny-cluster-resource PROJECT GROUP KIND"
|
||||
desc := "Removes a cluster-scoped API resource from the whitelist"
|
||||
return modifyProjectResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
|
||||
index := -1
|
||||
for i, item := range proj.Spec.ClusterResourceWhitelist {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
fmt.Printf("Group '%s' and kind '%s' not in whitelisted cluster resources\n", group, kind)
|
||||
return false
|
||||
}
|
||||
proj.Spec.ClusterResourceWhitelist = append(proj.Spec.ClusterResourceWhitelist[:index], proj.Spec.ClusterResourceWhitelist[index+1:]...)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// NewProjectAllowClusterResourceCommand returns a new instance of an `argocd proj allow-cluster-resource` command
|
||||
func NewProjectAllowClusterResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
use := "allow-cluster-resource PROJECT GROUP KIND"
|
||||
desc := "Adds a cluster-scoped API resource to the whitelist"
|
||||
return modifyProjectResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
|
||||
for _, item := range proj.Spec.ClusterResourceWhitelist {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
fmt.Printf("Group '%s' and kind '%s' already present in whitelisted cluster resources\n", group, kind)
|
||||
return false
|
||||
}
|
||||
}
|
||||
proj.Spec.ClusterResourceWhitelist = append(proj.Spec.ClusterResourceWhitelist, v1.GroupKind{Group: group, Kind: kind})
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// NewProjectRemoveSourceCommand returns a new instance of an `argocd proj remove-src` command
|
||||
func NewProjectRemoveSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "remove-source PROJECT URL",
|
||||
Short: "Remove project source repository",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
url := args[1]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
index := -1
|
||||
for i, item := range proj.Spec.SourceRepos {
|
||||
if item == url {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
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})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectDeleteCommand returns a new instance of an `argocd proj delete` command
|
||||
func NewProjectDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "delete PROJECT",
|
||||
Short: "Delete project",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
for _, name := range args {
|
||||
_, err := projIf.Delete(context.Background(), &project.ProjectQuery{Name: name})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectListCommand returns a new instance of an `argocd proj list` command
|
||||
func NewProjectListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
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{})
|
||||
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)
|
||||
}
|
||||
_ = w.Flush()
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
func printProjectLine(w io.Writer, p *v1alpha1.AppProject) {
|
||||
var destinations, sourceRepos, clusterWhitelist, namespaceBlacklist string
|
||||
switch len(p.Spec.Destinations) {
|
||||
case 0:
|
||||
destinations = "<none>"
|
||||
case 1:
|
||||
destinations = fmt.Sprintf("%s,%s", p.Spec.Destinations[0].Server, p.Spec.Destinations[0].Namespace)
|
||||
default:
|
||||
destinations = fmt.Sprintf("%d destinations", len(p.Spec.Destinations))
|
||||
}
|
||||
switch len(p.Spec.SourceRepos) {
|
||||
case 0:
|
||||
sourceRepos = "<none>"
|
||||
case 1:
|
||||
sourceRepos = p.Spec.SourceRepos[0]
|
||||
default:
|
||||
sourceRepos = fmt.Sprintf("%d repos", len(p.Spec.SourceRepos))
|
||||
}
|
||||
switch len(p.Spec.ClusterResourceWhitelist) {
|
||||
case 0:
|
||||
clusterWhitelist = "<none>"
|
||||
case 1:
|
||||
clusterWhitelist = fmt.Sprintf("%s/%s", p.Spec.ClusterResourceWhitelist[0].Group, p.Spec.ClusterResourceWhitelist[0].Kind)
|
||||
default:
|
||||
clusterWhitelist = fmt.Sprintf("%d resources", len(p.Spec.ClusterResourceWhitelist))
|
||||
}
|
||||
switch len(p.Spec.NamespaceResourceBlacklist) {
|
||||
case 0:
|
||||
namespaceBlacklist = "<none>"
|
||||
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)
|
||||
}
|
||||
|
||||
// NewProjectGetCommand returns a new instance of an `argocd proj get` command
|
||||
func NewProjectGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
const printProjFmtStr = "%-34s%s\n"
|
||||
var command = &cobra.Command{
|
||||
Use: "get PROJECT",
|
||||
Short: "Get project details",
|
||||
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)
|
||||
p, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf(printProjFmtStr, "Name:", p.Name)
|
||||
fmt.Printf(printProjFmtStr, "Description:", p.Spec.Description)
|
||||
|
||||
// Print destinations
|
||||
dest0 := "<none>"
|
||||
if len(p.Spec.Destinations) > 0 {
|
||||
dest0 = fmt.Sprintf("%s,%s", p.Spec.Destinations[0].Server, p.Spec.Destinations[0].Namespace)
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Destinations:", dest0)
|
||||
for i := 1; i < len(p.Spec.Destinations); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s,%s", p.Spec.Destinations[i].Server, p.Spec.Destinations[i].Namespace))
|
||||
}
|
||||
|
||||
// Print sources
|
||||
src0 := "<none>"
|
||||
if len(p.Spec.SourceRepos) > 0 {
|
||||
src0 = p.Spec.SourceRepos[0]
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Repositories:", src0)
|
||||
for i := 1; i < len(p.Spec.SourceRepos); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", p.Spec.SourceRepos[i])
|
||||
}
|
||||
|
||||
// Print whitelisted cluster resources
|
||||
cwl0 := "<none>"
|
||||
if len(p.Spec.ClusterResourceWhitelist) > 0 {
|
||||
cwl0 = fmt.Sprintf("%s/%s", p.Spec.ClusterResourceWhitelist[0].Group, p.Spec.ClusterResourceWhitelist[0].Kind)
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Whitelisted Cluster Resources:", cwl0)
|
||||
for i := 1; i < len(p.Spec.ClusterResourceWhitelist); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s/%s", p.Spec.ClusterResourceWhitelist[i].Group, p.Spec.ClusterResourceWhitelist[i].Kind))
|
||||
}
|
||||
|
||||
// Print blacklisted namespaced resources
|
||||
rbl0 := "<none>"
|
||||
if len(p.Spec.NamespaceResourceBlacklist) > 0 {
|
||||
rbl0 = fmt.Sprintf("%s/%s", p.Spec.NamespaceResourceBlacklist[0].Group, p.Spec.NamespaceResourceBlacklist[0].Kind)
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Blacklisted Namespaced Resources:", rbl0)
|
||||
for i := 1; i < len(p.Spec.NamespaceResourceBlacklist); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s/%s", p.Spec.NamespaceResourceBlacklist[i].Group, p.Spec.NamespaceResourceBlacklist[i].Kind))
|
||||
}
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
func NewProjectEditCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "edit PROJECT",
|
||||
Short: "Edit 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(), &project.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
projData, err := json.Marshal(proj.Spec)
|
||||
errors.CheckError(err)
|
||||
projData, err = yaml.JSONToYAML(projData)
|
||||
errors.CheckError(err)
|
||||
|
||||
cli.InteractiveEdit(fmt.Sprintf("%s-*-edit.yaml", projName), projData, func(input []byte) error {
|
||||
input, err = yaml.YAMLToJSON(input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updatedSpec := v1alpha1.AppProjectSpec{}
|
||||
err = json.Unmarshal(input, &updatedSpec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
proj.Spec = updatedSpec
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to update project:\n%v", err)
|
||||
}
|
||||
return err
|
||||
})
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
377
cmd/argocd/commands/project_role.go
Normal file
@@ -0,0 +1,377 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"text/tabwriter"
|
||||
|
||||
timeutil "github.com/argoproj/pkg/time"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"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 (
|
||||
policyTemplate = "p, proj:%s:%s, applications, %s, %s/%s, %s"
|
||||
)
|
||||
|
||||
// NewProjectRoleCommand returns a new instance of the `argocd proj role` command
|
||||
func NewProjectRoleCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
roleCommand := &cobra.Command{
|
||||
Use: "role",
|
||||
Short: "Manage a project's roles",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
},
|
||||
}
|
||||
roleCommand.AddCommand(NewProjectRoleListCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectRoleGetCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectRoleCreateCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectRoleDeleteCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectRoleCreateTokenCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectRoleDeleteTokenCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectRoleAddPolicyCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectRoleRemovePolicyCommand(clientOpts))
|
||||
return roleCommand
|
||||
}
|
||||
|
||||
// NewProjectRoleAddPolicyCommand returns a new instance of an `argocd proj role add-policy` command
|
||||
func NewProjectRoleAddPolicyCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
opts policyOpts
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "add-policy PROJECT ROLE-NAME",
|
||||
Short: "Add a policy to a project role",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
roleName := args[1]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
role, roleIndex, err := projectutil.GetRoleByName(proj, 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})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
addPolicyFlags(command, &opts)
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectRoleRemovePolicyCommand returns a new instance of an `argocd proj role remove-policy` command
|
||||
func NewProjectRoleRemovePolicyCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
opts policyOpts
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "remove-policy PROJECT ROLE-NAME",
|
||||
Short: "Remove a policy from a role within a project",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
roleName := args[1]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
role, roleIndex, err := projectutil.GetRoleByName(proj, roleName)
|
||||
errors.CheckError(err)
|
||||
|
||||
policyToRemove := fmt.Sprintf(policyTemplate, proj.Name, role.Name, opts.action, proj.Name, opts.object, opts.permission)
|
||||
duplicateIndex := -1
|
||||
for i, policy := range role.Policies {
|
||||
if policy == policyToRemove {
|
||||
duplicateIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if duplicateIndex < 0 {
|
||||
return
|
||||
}
|
||||
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})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
addPolicyFlags(command, &opts)
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectRoleCreateCommand returns a new instance of an `argocd proj role create` command
|
||||
func NewProjectRoleCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
description string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "create PROJECT ROLE-NAME",
|
||||
Short: "Create a project role",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
roleName := args[1]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
_, _, err = projectutil.GetRoleByName(proj, 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})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Role '%s' created\n", roleName)
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&description, "description", "", "", "Project description")
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectRoleDeleteCommand returns a new instance of an `argocd proj role delete` command
|
||||
func NewProjectRoleDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "delete PROJECT ROLE-NAME",
|
||||
Short: "Delete a project role",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
roleName := args[1]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
_, index, err := projectutil.GetRoleByName(proj, roleName)
|
||||
if err != nil {
|
||||
fmt.Printf("Role '%s' does not exist in project\n", roleName)
|
||||
return
|
||||
}
|
||||
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})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Role '%s' deleted\n", roleName)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectRoleCreateTokenCommand returns a new instance of an `argocd proj role create-token` command
|
||||
func NewProjectRoleCreateTokenCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
expiresIn string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "create-token PROJECT ROLE-NAME",
|
||||
Short: "Create a project token",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
roleName := args[1]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
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())})
|
||||
errors.CheckError(err)
|
||||
fmt.Println(token.Token)
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&expiresIn, "expires-in", "e", "0s", "Duration before the token will expire. (Default: No expiration)")
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectRoleDeleteTokenCommand returns a new instance of an `argocd proj role delete-token` command
|
||||
func NewProjectRoleDeleteTokenCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "delete-token PROJECT ROLE-NAME ISSUED-AT",
|
||||
Short: "Delete a project token",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
roleName := args[1]
|
||||
issuedAt, err := strconv.ParseInt(args[2], 10, 64)
|
||||
errors.CheckError(err)
|
||||
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
_, err = projIf.DeleteToken(context.Background(), &project.ProjectTokenDeleteRequest{Project: projName, Role: roleName, Iat: issuedAt})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectRoleListCommand returns a new instance of an `argocd proj roles list` command
|
||||
func NewProjectRoleListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "list PROJECT",
|
||||
Short: "List all the roles in 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)
|
||||
|
||||
project, err := projIf.Get(context.Background(), &project.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)
|
||||
}
|
||||
_ = w.Flush()
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectRoleGetCommand returns a new instance of an `argocd proj roles get` command
|
||||
func NewProjectRoleGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "get PROJECT ROLE-NAME",
|
||||
Short: "Get the details of a specific role",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
roleName := args[1]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
role, _, err := projectutil.GetRoleByName(proj, roleName)
|
||||
errors.CheckError(err)
|
||||
|
||||
printRoleFmtStr := "%-15s%s\n"
|
||||
fmt.Printf(printRoleFmtStr, "Role Name:", roleName)
|
||||
fmt.Printf(printRoleFmtStr, "Description:", role.Description)
|
||||
fmt.Printf("Policies:\n")
|
||||
fmt.Printf("%s\n", proj.ProjectPoliciesString())
|
||||
fmt.Printf("JWT Tokens:\n")
|
||||
// TODO(jessesuen): print groups
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "ID\tISSUED-AT\tEXPIRES-AT\n")
|
||||
for _, token := range role.JWTTokens {
|
||||
expiresAt := "<none>"
|
||||
if token.ExpiresAt > 0 {
|
||||
expiresAt = humanizeTimestamp(token.ExpiresAt)
|
||||
}
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\n", token.IssuedAt, humanizeTimestamp(token.IssuedAt), expiresAt)
|
||||
}
|
||||
_ = w.Flush()
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectRoleAddGroupCommand returns a new instance of an `argocd proj role add-group` command
|
||||
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",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
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})
|
||||
errors.CheckError(err)
|
||||
updated, err := projectutil.AddGroupToRole(proj, roleName, groupName)
|
||||
errors.CheckError(err)
|
||||
if updated {
|
||||
fmt.Printf("Group '%s' already present in role '%s'\n", groupName, roleName)
|
||||
return
|
||||
}
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Group '%s' added to role '%s'\n", groupName, roleName)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectRoleRemoveGroupCommand returns a new instance of an `argocd proj role remove-group` command
|
||||
func NewProjectRoleRemoveGroupCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "remove-group PROJECT ROLE-NAME GROUP-CLAIM",
|
||||
Short: "Remove a group claim from a role within a project",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
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})
|
||||
errors.CheckError(err)
|
||||
updated, err := projectutil.RemoveGroupFromRole(proj, 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})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Group '%s' removed from role '%s'\n", groupName, roleName)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
86
cmd/argocd/commands/relogin.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
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"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
"github.com/argoproj/argo-cd/util/session"
|
||||
)
|
||||
|
||||
// NewReloginCommand returns a new instance of `argocd relogin` command
|
||||
func NewReloginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
password string
|
||||
ssoPort int
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "relogin",
|
||||
Short: "Refresh an expired authenticate token",
|
||||
Long: "Refresh an expired authenticate token",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
localCfg, err := localconfig.ReadLocalConfig(globalClientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
if localCfg == nil {
|
||||
log.Fatalf("No context found. Login using `argocd login`")
|
||||
}
|
||||
configCtx, err := localCfg.ResolveContext(localCfg.CurrentContext)
|
||||
errors.CheckError(err)
|
||||
|
||||
var tokenString string
|
||||
var refreshToken string
|
||||
clientOpts := argocdclient.ClientOptions{
|
||||
ConfigPath: "",
|
||||
ServerAddr: configCtx.Server.Server,
|
||||
Insecure: configCtx.Server.Insecure,
|
||||
GRPCWeb: globalClientOpts.GRPCWeb,
|
||||
PlainText: configCtx.Server.PlainText,
|
||||
}
|
||||
acdClient := argocdclient.NewClientOrDie(&clientOpts)
|
||||
claims, err := configCtx.User.Claims()
|
||||
errors.CheckError(err)
|
||||
if claims.Issuer == session.SessionManagerClaimsIssuer {
|
||||
fmt.Printf("Relogging in as '%s'\n", claims.Subject)
|
||||
tokenString = passwordLogin(acdClient, claims.Subject, password)
|
||||
} else {
|
||||
fmt.Println("Reinitiating SSO login")
|
||||
setConn, setIf := acdClient.NewSettingsClientOrDie()
|
||||
defer util.Close(setConn)
|
||||
ctx := context.Background()
|
||||
httpClient, err := acdClient.HTTPClient()
|
||||
errors.CheckError(err)
|
||||
ctx = oidc.ClientContext(ctx, httpClient)
|
||||
acdSet, err := setIf.Get(ctx, &settings.SettingsQuery{})
|
||||
errors.CheckError(err)
|
||||
oauth2conf, provider, err := acdClient.OIDCConfig(ctx, acdSet)
|
||||
errors.CheckError(err)
|
||||
tokenString, refreshToken = oauth2Login(ctx, ssoPort, oauth2conf, provider)
|
||||
}
|
||||
|
||||
localCfg.UpsertUser(localconfig.User{
|
||||
Name: localCfg.CurrentContext,
|
||||
AuthToken: tokenString,
|
||||
RefreshToken: refreshToken,
|
||||
})
|
||||
err = localconfig.WriteLocalConfig(*localCfg, globalClientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Context '%s' updated\n", localCfg.CurrentContext)
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&password, "password", "", "the password of an account to authenticate")
|
||||
command.Flags().IntVar(&ssoPort, "sso-port", DefaultSSOLocalPort, "port to run local OAuth2 login application")
|
||||
return command
|
||||
}
|
||||
@@ -1,30 +1,29 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"syscall"
|
||||
"text/tabwriter"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
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"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
// NewRepoCommand returns a new instance of an `argocd repo` command
|
||||
func NewRepoCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "repo",
|
||||
Short: fmt.Sprintf("%s repo COMMAND", cliName),
|
||||
Short: "Manage git repository credentials",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
@@ -40,12 +39,14 @@ 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
|
||||
sshPrivateKeyPath string
|
||||
repo appsv1.Repository
|
||||
upsert bool
|
||||
sshPrivateKeyPath string
|
||||
insecureIgnoreHostKey bool
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "add",
|
||||
Short: fmt.Sprintf("%s repo add REPO", cliName),
|
||||
Use: "add REPO",
|
||||
Short: "Add git repository credentials",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
@@ -59,49 +60,46 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
}
|
||||
repo.SSHPrivateKey = string(keyData)
|
||||
}
|
||||
err := git.TestRepo(repo.Repo, repo.Username, repo.Password, repo.SSHPrivateKey)
|
||||
repo.InsecureIgnoreHostKey = insecureIgnoreHostKey
|
||||
// First test the repo *without* username/password. This gives us a hint on whether this
|
||||
// is a private repo.
|
||||
// NOTE: it is important not to run git commands to test git credentials on the user's
|
||||
// system since it may mess with their git credential store (e.g. osx keychain).
|
||||
// See issue #315
|
||||
err := git.TestRepo(repo.Repo, "", "", repo.SSHPrivateKey, repo.InsecureIgnoreHostKey)
|
||||
if err != nil {
|
||||
if repo.Username != "" && repo.Password != "" || git.IsSshURL(repo.Repo) {
|
||||
// if everything was supplied or repo URL is SSH url, one of the inputs was definitely bad
|
||||
if yes, _ := git.IsSSHURL(repo.Repo); yes {
|
||||
// If we failed using git SSH credentials, then the repo is automatically bad
|
||||
log.Fatal(err)
|
||||
}
|
||||
// If we can't test the repo, it's probably private. Prompt for credentials and try again.
|
||||
promptCredentials(&repo)
|
||||
err = git.TestRepo(repo.Repo, repo.Username, repo.Password, repo.SSHPrivateKey)
|
||||
// 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)
|
||||
}
|
||||
errors.CheckError(err)
|
||||
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoClientOrDie()
|
||||
defer util.Close(conn)
|
||||
createdRepo, err := repoIf.Create(context.Background(), &repo)
|
||||
repoCreateReq := repository.RepoCreateRequest{
|
||||
Repo: &repo,
|
||||
Upsert: upsert,
|
||||
}
|
||||
createdRepo, err := repoIf.Create(context.Background(), &repoCreateReq)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("repository '%s' added\n", createdRepo.Repo)
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&repo.Username, "username", "", "username to the repository")
|
||||
command.Flags().StringVar(&repo.Password, "password", "", "password to the repository")
|
||||
command.Flags().StringVar(&sshPrivateKeyPath, "sshPrivateKeyPath", "", "path to the private ssh key (e.g. ~/.ssh/id_rsa)")
|
||||
command.Flags().StringVar(&sshPrivateKeyPath, "ssh-private-key-path", "", "path to the private ssh key (e.g. ~/.ssh/id_rsa)")
|
||||
command.Flags().BoolVar(&insecureIgnoreHostKey, "insecure-ignore-host-key", false, "disables SSH strict host key checking")
|
||||
command.Flags().BoolVar(&upsert, "upsert", false, "Override an existing repository with the same name even if the spec differs")
|
||||
return command
|
||||
}
|
||||
|
||||
func promptCredentials(repo *appsv1.Repository) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
if repo.Username == "" {
|
||||
fmt.Print("Username: ")
|
||||
username, _ := reader.ReadString('\n')
|
||||
repo.Username = username
|
||||
}
|
||||
if repo.Password == "" {
|
||||
fmt.Print("Password: ")
|
||||
bytePassword, _ := terminal.ReadPassword(syscall.Stdin)
|
||||
repo.Password = string(bytePassword)
|
||||
}
|
||||
}
|
||||
|
||||
// NewRepoRemoveCommand returns a new instance of an `argocd repo list` command
|
||||
func NewRepoRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "rm",
|
||||
Short: fmt.Sprintf("%s repo rm REPO", cliName),
|
||||
Use: "rm REPO",
|
||||
Short: "Remove git repository credentials",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
@@ -122,16 +120,16 @@ func NewRepoRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
func NewRepoListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: fmt.Sprintf("%s repo list", cliName),
|
||||
Short: "List configured repositories",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoClientOrDie()
|
||||
defer util.Close(conn)
|
||||
repos, err := repoIf.List(context.Background(), &repository.RepoQuery{})
|
||||
errors.CheckError(err)
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "REPO\tUSER\n")
|
||||
fmt.Fprintf(w, "REPO\tUSER\tSTATUS\tMESSAGE\n")
|
||||
for _, r := range repos.Items {
|
||||
fmt.Fprintf(w, "%s\t%s\n", r.Repo, r.Username)
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", r.Repo, r.Username, r.ConnectionState.Status, r.ConnectionState.Message)
|
||||
}
|
||||
_ = w.Flush()
|
||||
},
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/config"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
}
|
||||
|
||||
var logLevel string
|
||||
|
||||
func initConfig() {
|
||||
cli.SetLogLevel(logLevel)
|
||||
}
|
||||
|
||||
// NewCommand returns a new instance of an argocd command
|
||||
func NewCommand() *cobra.Command {
|
||||
var (
|
||||
@@ -16,21 +30,31 @@ func NewCommand() *cobra.Command {
|
||||
|
||||
var command = &cobra.Command{
|
||||
Use: cliName,
|
||||
Short: "argocd controls a ArgoCD server",
|
||||
Short: "argocd controls a Argo CD server",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
},
|
||||
}
|
||||
|
||||
command.AddCommand(cli.NewVersionCmd(cliName))
|
||||
command.AddCommand(NewVersionCmd(&clientOpts))
|
||||
command.AddCommand(NewClusterCommand(&clientOpts, pathOpts))
|
||||
command.AddCommand(NewApplicationCommand(&clientOpts))
|
||||
command.AddCommand(NewLoginCommand(&clientOpts))
|
||||
command.AddCommand(NewReloginCommand(&clientOpts))
|
||||
command.AddCommand(NewRepoCommand(&clientOpts))
|
||||
command.AddCommand(NewInstallCommand())
|
||||
command.AddCommand(NewUninstallCommand())
|
||||
|
||||
command.PersistentFlags().StringVar(&clientOpts.ServerAddr, "server", "", "ArgoCD server address")
|
||||
command.PersistentFlags().BoolVar(&clientOpts.Insecure, "insecure", true, "Disable transport security for the client connection")
|
||||
command.AddCommand(NewContextCommand(&clientOpts))
|
||||
command.AddCommand(NewProjectCommand(&clientOpts))
|
||||
command.AddCommand(NewAccountCommand(&clientOpts))
|
||||
|
||||
defaultLocalConfigPath, err := localconfig.DefaultLocalConfigPath()
|
||||
errors.CheckError(err)
|
||||
command.PersistentFlags().StringVar(&clientOpts.ConfigPath, "config", config.GetFlag("config", defaultLocalConfigPath), "Path to Argo CD config")
|
||||
command.PersistentFlags().StringVar(&clientOpts.ServerAddr, "server", config.GetFlag("server", ""), "Argo CD server address")
|
||||
command.PersistentFlags().BoolVar(&clientOpts.PlainText, "plaintext", config.GetBoolFlag("plaintext"), "Disable TLS")
|
||||
command.PersistentFlags().BoolVar(&clientOpts.Insecure, "insecure", config.GetBoolFlag("insecure"), "Skip server certificate and domain verification")
|
||||
command.PersistentFlags().StringVar(&clientOpts.CertFile, "server-crt", config.GetFlag("server-crt", ""), "Server certificate file")
|
||||
command.PersistentFlags().StringVar(&clientOpts.AuthToken, "auth-token", config.GetFlag("auth-token", ""), "Authentication token")
|
||||
command.PersistentFlags().BoolVar(&clientOpts.GRPCWeb, "grpc-web", config.GetBoolFlag("grpc-web"), "Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2.")
|
||||
command.PersistentFlags().StringVar(&logLevel, "loglevel", config.GetFlag("loglevel", "info"), "Set the logging level. One of: debug|info|warn|error")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/install"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
// NewUninstallCommand returns a new instance of `argocd install` command
|
||||
func NewUninstallCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
installOpts install.InstallOptions
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "uninstall",
|
||||
Short: "Uninstall Argo CD",
|
||||
Long: "Uninstall Argo CD",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
conf, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
installer, err := install.NewInstaller(conf, installOpts)
|
||||
errors.CheckError(err)
|
||||
installer.Uninstall()
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&installOpts.Namespace, "install-namespace", install.DefaultInstallNamespace, "uninstall from a specific namespace")
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(command)
|
||||
return command
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/ghodss/yaml"
|
||||
)
|
||||
|
||||
// unmarshalApplication tries to convert a YAML or JSON byte array into an Application struct.
|
||||
func unmarshalApplication(data []byte, app *argoappv1.Application) {
|
||||
// first, try unmarshaling as JSON
|
||||
// Based on technique from Kubectl, which supports both YAML and JSON:
|
||||
// https://mlafeldt.github.io/blog/teaching-go-programs-to-love-json-and-yaml/
|
||||
// http://ghodss.com/2014/the-right-way-to-handle-yaml-in-golang/
|
||||
// Short version: JSON unmarshaling won't zero out null fields; YAML unmarshaling will.
|
||||
// This may have unintended effects or hard-to-catch issues when populating our application object.
|
||||
data, err := yaml.YAMLToJSON(data)
|
||||
if err != nil {
|
||||
log.Fatal("Could not decode valid JSON or YAML Kubernetes manifest")
|
||||
}
|
||||
err = json.Unmarshal(data, &app)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not unmarshal Kubernetes manifest: %s", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
// readLocalFile reads a file from disk and returns its contents as a byte array.
|
||||
// The caller is responsible for checking error return values.
|
||||
func readLocalFile(path string) (data []byte, err error) {
|
||||
data, err = ioutil.ReadFile(path)
|
||||
return
|
||||
}
|
||||
|
||||
// readRemoteFile issues a GET request to retrieve the contents of the specified URL as a byte array.
|
||||
// The caller is responsible for checking error return values.
|
||||
func readRemoteFile(url string) (data []byte, err error) {
|
||||
resp, err := http.Get(url)
|
||||
if err == nil {
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
data, err = ioutil.ReadAll(resp.Body)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReadLocalFile(t *testing.T) {
|
||||
sentinel := "Hello, world!"
|
||||
|
||||
file, err := ioutil.TempFile(os.TempDir(), "")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer func() {
|
||||
_ = os.Remove(file.Name())
|
||||
}()
|
||||
|
||||
_, _ = file.WriteString(sentinel)
|
||||
_ = file.Sync()
|
||||
|
||||
data, err := readLocalFile(file.Name())
|
||||
if string(data) != sentinel {
|
||||
t.Errorf("Test data did not match (err = %v)! Expected \"%s\" and received \"%s\"", err, sentinel, string(data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadRemoteFile(t *testing.T) {
|
||||
sentinel := "Hello, world!"
|
||||
|
||||
serve := func(c chan<- string) {
|
||||
// listen on first available dynamic (unprivileged) port
|
||||
listener, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// send back the address so that it can be used
|
||||
c <- listener.Addr().String()
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// return the sentinel text at root URL
|
||||
fmt.Fprint(w, sentinel)
|
||||
})
|
||||
|
||||
panic(http.Serve(listener, nil))
|
||||
}
|
||||
|
||||
c := make(chan string, 1)
|
||||
|
||||
// run a local webserver to test data retrieval
|
||||
go serve(c)
|
||||
|
||||
address := <-c
|
||||
data, err := readRemoteFile("http://" + address)
|
||||
t.Logf("Listening at address: %s", address)
|
||||
if string(data) != sentinel {
|
||||
t.Errorf("Test data did not match (err = %v)! Expected \"%s\" and received \"%s\"", err, sentinel, string(data))
|
||||
}
|
||||
}
|
||||
66
cmd/argocd/commands/version.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
argocd "github.com/argoproj/argo-cd"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
)
|
||||
|
||||
// NewVersionCmd returns a new `version` command to be used as a sub-command to root
|
||||
func NewVersionCmd(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var short bool
|
||||
var client bool
|
||||
|
||||
versionCmd := cobra.Command{
|
||||
Use: "version",
|
||||
Short: fmt.Sprintf("Print version information"),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
version := argocd.GetVersion()
|
||||
fmt.Printf("%s: %s\n", cliName, version)
|
||||
if !short {
|
||||
fmt.Printf(" BuildDate: %s\n", version.BuildDate)
|
||||
fmt.Printf(" GitCommit: %s\n", version.GitCommit)
|
||||
fmt.Printf(" GitTreeState: %s\n", version.GitTreeState)
|
||||
if version.GitTag != "" {
|
||||
fmt.Printf(" GitTag: %s\n", version.GitTag)
|
||||
}
|
||||
fmt.Printf(" GoVersion: %s\n", version.GoVersion)
|
||||
fmt.Printf(" Compiler: %s\n", version.Compiler)
|
||||
fmt.Printf(" Platform: %s\n", version.Platform)
|
||||
}
|
||||
if client {
|
||||
return
|
||||
}
|
||||
|
||||
// Get Server version
|
||||
conn, versionIf := argocdclient.NewClientOrDie(clientOpts).NewVersionClientOrDie()
|
||||
defer util.Close(conn)
|
||||
serverVers, err := versionIf.Version(context.Background(), &empty.Empty{})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("%s: %s\n", "argocd-server", serverVers.Version)
|
||||
if !short {
|
||||
fmt.Printf(" BuildDate: %s\n", serverVers.BuildDate)
|
||||
fmt.Printf(" GitCommit: %s\n", serverVers.GitCommit)
|
||||
fmt.Printf(" GitTreeState: %s\n", serverVers.GitTreeState)
|
||||
if version.GitTag != "" {
|
||||
fmt.Printf(" GitTag: %s\n", serverVers.GitTag)
|
||||
}
|
||||
fmt.Printf(" GoVersion: %s\n", serverVers.GoVersion)
|
||||
fmt.Printf(" Compiler: %s\n", serverVers.Compiler)
|
||||
fmt.Printf(" Platform: %s\n", serverVers.Platform)
|
||||
fmt.Printf(" Ksonnet Version: %s\n", serverVers.KsonnetVersion)
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
versionCmd.Flags().BoolVar(&short, "short", false, "print just the version number")
|
||||
versionCmd.Flags().BoolVar(&client, "client", false, "client version only (no server required)")
|
||||
return &versionCmd
|
||||
}
|
||||
179
common/common.go
@@ -1,89 +1,116 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
// Default service addresses and URLS of Argo CD internal services
|
||||
const (
|
||||
// DefaultRepoServerAddr is the gRPC address of the Argo CD repo server
|
||||
DefaultRepoServerAddr = "argocd-repo-server:8081"
|
||||
// DefaultDexServerAddr is the HTTP address of the Dex OIDC server, which we run a reverse proxy against
|
||||
DefaultDexServerAddr = "http://argocd-dex-server:5556"
|
||||
// DefaultRedisAddr is the default redis address
|
||||
DefaultRedisAddr = "argocd-redis:6379"
|
||||
)
|
||||
|
||||
// Kubernetes ConfigMap and Secret resource names which hold Argo CD settings
|
||||
const (
|
||||
ArgoCDConfigMapName = "argocd-cm"
|
||||
ArgoCDSecretName = "argocd-secret"
|
||||
ArgoCDRBACConfigMapName = "argocd-rbac-cm"
|
||||
)
|
||||
|
||||
const (
|
||||
// MetadataPrefix is the prefix used for our labels and annotations
|
||||
MetadataPrefix = "argocd.argoproj.io"
|
||||
|
||||
// SecretTypeRepository indicates a secret type of repository
|
||||
SecretTypeRepository = "repository"
|
||||
|
||||
// SecretTypeCluster indicates a secret type of cluster
|
||||
SecretTypeCluster = "cluster"
|
||||
PortAPIServer = 8080
|
||||
PortRepoServer = 8081
|
||||
PortArgoCDMetrics = 8082
|
||||
PortArgoCDAPIServerMetrics = 8083
|
||||
PortRepoServerMetrics = 8084
|
||||
)
|
||||
|
||||
var (
|
||||
// LabelKeyAppInstance refers to the application instance resource name
|
||||
LabelKeyAppInstance = MetadataPrefix + "/app-instance"
|
||||
|
||||
// LabelKeySecretType contains the type of argocd secret (either 'cluster' or 'repo')
|
||||
LabelKeySecretType = MetadataPrefix + "/secret-type"
|
||||
|
||||
// LabelKeyApplicationControllerInstanceID is the label which allows to separate application among multiple running application controllers.
|
||||
LabelKeyApplicationControllerInstanceID = application.ApplicationFullName + "/controller-instanceid"
|
||||
// LabelApplicationName is the label which indicates that resource belongs to application with the specified name
|
||||
LabelApplicationName = application.ApplicationFullName + "/app-name"
|
||||
)
|
||||
|
||||
// ArgoCDManagerServiceAccount is the name of the service account for managing a cluster
|
||||
// Argo CD application related constants
|
||||
const (
|
||||
ArgoCDManagerServiceAccount = "argocd-manager"
|
||||
ArgoCDManagerClusterRole = "argocd-manager-role"
|
||||
ArgoCDManagerClusterRoleBinding = "argocd-manager-role-binding"
|
||||
// KubernetesInternalAPIServerAddr is address of the k8s API server when accessing internal to the cluster
|
||||
KubernetesInternalAPIServerAddr = "https://kubernetes.default.svc"
|
||||
// DefaultAppProjectName contains name of 'default' app project, which is available in every Argo CD installation
|
||||
DefaultAppProjectName = "default"
|
||||
// ArgoCDAdminUsername is the username of the 'admin' user
|
||||
ArgoCDAdminUsername = "admin"
|
||||
// ArgoCDUserAgentName is the default user-agent name used by the gRPC API client library and grpc-gateway
|
||||
ArgoCDUserAgentName = "argocd-client"
|
||||
// AuthCookieName is the HTTP cookie name where we store our auth token
|
||||
AuthCookieName = "argocd.token"
|
||||
// RevisionHistoryLimit is the max number of successful sync to keep in history
|
||||
RevisionHistoryLimit = 10
|
||||
// K8sClientConfigQPS controls the QPS to be used in K8s REST client configs
|
||||
K8sClientConfigQPS = 25
|
||||
// K8sClientConfigBurst controls the burst to be used in K8s REST client configs
|
||||
K8sClientConfigBurst = 50
|
||||
)
|
||||
|
||||
// ArgoCDManagerPolicyRules are the policies to give argocd-manager
|
||||
var ArgoCDManagerPolicyRules = []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{"*"},
|
||||
Resources: []string{"*"},
|
||||
Verbs: []string{"*"},
|
||||
},
|
||||
}
|
||||
// Dex related constants
|
||||
const (
|
||||
// DexAPIEndpoint is the endpoint where we serve the Dex API server
|
||||
DexAPIEndpoint = "/api/dex"
|
||||
// LoginEndpoint is Argo CD's shorthand login endpoint which redirects to dex's OAuth 2.0 provider's consent page
|
||||
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"
|
||||
// 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
|
||||
ArgoCDClientAppID = "argo-cd"
|
||||
// ArgoCDCLIClientAppName is name of the Oauth client app used when registering our CLI to dex
|
||||
ArgoCDCLIClientAppName = "Argo CD CLI"
|
||||
// ArgoCDCLIClientAppID is the Oauth client ID we will use when registering our CLI to dex
|
||||
ArgoCDCLIClientAppID = "argo-cd-cli"
|
||||
)
|
||||
|
||||
// Resource metadata labels and annotations (keys and values) used by Argo CD components
|
||||
const (
|
||||
// LabelKeyAppInstance is the label key to use to uniquely identify the instance of an application
|
||||
// The Argo CD application name is used as the instance name
|
||||
LabelKeyAppInstance = "app.kubernetes.io/instance"
|
||||
// LegacyLabelApplicationName is the legacy label (v0.10 and below) and is superceded by 'app.kubernetes.io/instance'
|
||||
LabelKeyLegacyApplicationName = "applications.argoproj.io/app-name"
|
||||
// LabelKeySecretType contains the type of argocd secret (currently: 'cluster')
|
||||
LabelKeySecretType = "argocd.argoproj.io/secret-type"
|
||||
// LabelValueSecretTypeCluster indicates a secret type of cluster
|
||||
LabelValueSecretTypeCluster = "cluster"
|
||||
|
||||
// AnnotationKeyHook contains the hook type of a resource
|
||||
AnnotationKeyHook = "argocd.argoproj.io/hook"
|
||||
// AnnotationKeyHookDeletePolicy is the policy of deleting a hook
|
||||
AnnotationKeyHookDeletePolicy = "argocd.argoproj.io/hook-delete-policy"
|
||||
// AnnotationKeyRefresh is the annotation key which indicates that app needs to be refreshed. Removed by application controller after app is refreshed.
|
||||
// Might take values 'normal'/'hard'. Value 'hard' means manifest cache and target cluster state cache should be invalidated before refresh.
|
||||
AnnotationKeyRefresh = "argocd.argoproj.io/refresh"
|
||||
// AnnotationKeyManagedBy is annotation name which indicates that k8s resource is managed by an application.
|
||||
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"
|
||||
)
|
||||
|
||||
// Environment variables for tuning and debugging Argo CD
|
||||
const (
|
||||
// EnvVarSSODebug is an environment variable to enable additional OAuth debugging in the API server
|
||||
EnvVarSSODebug = "ARGOCD_SSO_DEBUG"
|
||||
// EnvVarRBACDebug is an environment variable to enable additional RBAC debugging in the API server
|
||||
EnvVarRBACDebug = "ARGOCD_RBAC_DEBUG"
|
||||
// EnvVarFakeInClusterConfig is an environment variable to fake an in-cluster RESTConfig using
|
||||
// the current kubectl context (for development purposes)
|
||||
EnvVarFakeInClusterConfig = "ARGOCD_FAKE_IN_CLUSTER"
|
||||
)
|
||||
|
||||
const (
|
||||
ArgoCDServerServiceAccount = "argocd-server"
|
||||
ArgoCDServerRole = "argocd-server-role"
|
||||
ArgoCDServerRoleBinding = "argocd-server-role-binding"
|
||||
// MinClientVersion is the minimum client version that can interface with this API server.
|
||||
// When introducing breaking changes to the API or datastructures, this number should be bumped.
|
||||
// The value here may be lower than the current value in VERSION
|
||||
MinClientVersion = "1.0.0"
|
||||
// CacheVersion is a objects version cached using util/cache/cache.go.
|
||||
// Number should be bumped in case of backward incompatible change to make sure cache is invalidated after upgrade.
|
||||
CacheVersion = "1.0.0"
|
||||
)
|
||||
|
||||
var ArgoCDServerPolicyRules = []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"pods", "pods/exec", "pods/log"},
|
||||
Verbs: []string{"get", "list", "watch"},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"secrets"},
|
||||
Verbs: []string{"create", "get", "list", "watch", "update", "patch", "delete"},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{"argoproj.io"},
|
||||
Resources: []string{"applications"},
|
||||
Verbs: []string{"create", "get", "list", "watch", "update", "patch", "delete"},
|
||||
},
|
||||
}
|
||||
|
||||
const (
|
||||
ApplicationControllerServiceAccount = "application-controller"
|
||||
ApplicationControllerRole = "application-controller-role"
|
||||
ApplicationControllerRoleBinding = "application-controller-role-binding"
|
||||
)
|
||||
|
||||
var ApplicationControllerPolicyRules = []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"secrets"},
|
||||
Verbs: []string{"get"},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{"argoproj.io"},
|
||||
Resources: []string{"applications"},
|
||||
Verbs: []string{"create", "get", "list", "watch", "update", "patch", "delete"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
@@ -12,15 +11,34 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// ArgoCDManagerServiceAccount is the name of the service account for managing a cluster
|
||||
const (
|
||||
ArgoCDManagerServiceAccount = "argocd-manager"
|
||||
ArgoCDManagerClusterRole = "argocd-manager-role"
|
||||
ArgoCDManagerClusterRoleBinding = "argocd-manager-role-binding"
|
||||
)
|
||||
|
||||
// ArgoCDManagerPolicyRules are the policies to give argocd-manager
|
||||
var ArgoCDManagerPolicyRules = []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{"*"},
|
||||
Resources: []string{"*"},
|
||||
Verbs: []string{"*"},
|
||||
},
|
||||
{
|
||||
NonResourceURLs: []string{"*"},
|
||||
Verbs: []string{"*"},
|
||||
},
|
||||
}
|
||||
|
||||
// CreateServiceAccount creates a service account
|
||||
func CreateServiceAccount(
|
||||
clientset kubernetes.Interface,
|
||||
serviceAccountName string,
|
||||
namespace string,
|
||||
) {
|
||||
) error {
|
||||
serviceAccount := apiv1.ServiceAccount{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
@@ -34,12 +52,13 @@ func CreateServiceAccount(
|
||||
_, err := clientset.CoreV1().ServiceAccounts(namespace).Create(&serviceAccount)
|
||||
if err != nil {
|
||||
if !apierr.IsAlreadyExists(err) {
|
||||
log.Fatalf("Failed to create service account '%s': %v\n", serviceAccountName, err)
|
||||
return fmt.Errorf("Failed to create service account %q: %v", serviceAccountName, err)
|
||||
}
|
||||
fmt.Printf("ServiceAccount '%s' already exists\n", serviceAccountName)
|
||||
return
|
||||
log.Infof("ServiceAccount %q already exists", serviceAccountName)
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("ServiceAccount '%s' created\n", serviceAccountName)
|
||||
log.Infof("ServiceAccount %q created", serviceAccountName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateClusterRole creates a cluster role
|
||||
@@ -47,7 +66,7 @@ func CreateClusterRole(
|
||||
clientset kubernetes.Interface,
|
||||
clusterRoleName string,
|
||||
rules []rbacv1.PolicyRule,
|
||||
) {
|
||||
) error {
|
||||
clusterRole := rbacv1.ClusterRole{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
@@ -62,16 +81,17 @@ func CreateClusterRole(
|
||||
_, err := crclient.Create(&clusterRole)
|
||||
if err != nil {
|
||||
if !apierr.IsAlreadyExists(err) {
|
||||
log.Fatalf("Failed to create ClusterRole '%s': %v\n", clusterRoleName, err)
|
||||
return fmt.Errorf("Failed to create ClusterRole %q: %v", clusterRoleName, err)
|
||||
}
|
||||
_, err = crclient.Update(&clusterRole)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to update ClusterRole '%s': %v\n", clusterRoleName, err)
|
||||
return fmt.Errorf("Failed to update ClusterRole %q: %v", clusterRoleName, err)
|
||||
}
|
||||
fmt.Printf("ClusterRole '%s' updated\n", clusterRoleName)
|
||||
log.Infof("ClusterRole %q updated", clusterRoleName)
|
||||
} else {
|
||||
fmt.Printf("ClusterRole '%s' created\n", clusterRoleName)
|
||||
log.Infof("ClusterRole %q created", clusterRoleName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateClusterRoleBinding create a ClusterRoleBinding
|
||||
@@ -81,7 +101,7 @@ func CreateClusterRoleBinding(
|
||||
serviceAccountName,
|
||||
clusterRoleName string,
|
||||
namespace string,
|
||||
) {
|
||||
) error {
|
||||
roleBinding := rbacv1.ClusterRoleBinding{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
@@ -106,22 +126,33 @@ func CreateClusterRoleBinding(
|
||||
_, err := clientset.RbacV1().ClusterRoleBindings().Create(&roleBinding)
|
||||
if err != nil {
|
||||
if !apierr.IsAlreadyExists(err) {
|
||||
log.Fatalf("Failed to create ClusterRoleBinding %s: %v\n", clusterBindingRoleName, err)
|
||||
return fmt.Errorf("Failed to create ClusterRoleBinding %s: %v", clusterBindingRoleName, err)
|
||||
}
|
||||
fmt.Printf("ClusterRoleBinding '%s' already exists\n", clusterBindingRoleName)
|
||||
return
|
||||
log.Infof("ClusterRoleBinding %q already exists", clusterBindingRoleName)
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("ClusterRoleBinding '%s' created, bound '%s' to '%s'\n", clusterBindingRoleName, serviceAccountName, clusterRoleName)
|
||||
log.Infof("ClusterRoleBinding %q created, bound %q to %q", clusterBindingRoleName, serviceAccountName, clusterRoleName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// InstallClusterManagerRBAC installs RBAC resources for a cluster manager to operate a cluster. Returns a token
|
||||
func InstallClusterManagerRBAC(conf *rest.Config) string {
|
||||
func InstallClusterManagerRBAC(clientset kubernetes.Interface) (string, error) {
|
||||
const ns = "kube-system"
|
||||
clientset, err := kubernetes.NewForConfig(conf)
|
||||
errors.CheckError(err)
|
||||
CreateServiceAccount(clientset, ArgoCDManagerServiceAccount, ns)
|
||||
CreateClusterRole(clientset, ArgoCDManagerClusterRole, ArgoCDManagerPolicyRules)
|
||||
CreateClusterRoleBinding(clientset, ArgoCDManagerClusterRoleBinding, ArgoCDManagerServiceAccount, ArgoCDManagerClusterRole, ns)
|
||||
|
||||
err := CreateServiceAccount(clientset, ArgoCDManagerServiceAccount, ns)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = CreateClusterRole(clientset, ArgoCDManagerClusterRole, ArgoCDManagerPolicyRules)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = CreateClusterRoleBinding(clientset, ArgoCDManagerClusterRoleBinding, ArgoCDManagerServiceAccount, ArgoCDManagerClusterRole, ns)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var serviceAccount *apiv1.ServiceAccount
|
||||
var secretName string
|
||||
@@ -137,52 +168,51 @@ func InstallClusterManagerRBAC(conf *rest.Config) string {
|
||||
return true, nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to wait for service account secret: %v", err)
|
||||
return "", fmt.Errorf("Failed to wait for service account secret: %v", err)
|
||||
}
|
||||
secret, err := clientset.CoreV1().Secrets(ns).Get(secretName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to retrieve secret '%s': %v", secretName, err)
|
||||
return "", fmt.Errorf("Failed to retrieve secret %q: %v", secretName, err)
|
||||
}
|
||||
token, ok := secret.Data["token"]
|
||||
if !ok {
|
||||
log.Fatalf("Secret '%s' for service account '%s' did not have a token", secretName, serviceAccount)
|
||||
return "", fmt.Errorf("Secret %q for service account %q did not have a token", secretName, serviceAccount)
|
||||
}
|
||||
return string(token)
|
||||
return string(token), nil
|
||||
}
|
||||
|
||||
// UninstallClusterManagerRBAC removes RBAC resources for a cluster manager to operate a cluster
|
||||
func UninstallClusterManagerRBAC(conf *rest.Config) {
|
||||
clientset, err := kubernetes.NewForConfig(conf)
|
||||
errors.CheckError(err)
|
||||
UninstallRBAC(clientset, "kube-system", ArgoCDManagerClusterRoleBinding, ArgoCDManagerClusterRole, ArgoCDManagerServiceAccount)
|
||||
func UninstallClusterManagerRBAC(clientset kubernetes.Interface) error {
|
||||
return UninstallRBAC(clientset, "kube-system", ArgoCDManagerClusterRoleBinding, ArgoCDManagerClusterRole, ArgoCDManagerServiceAccount)
|
||||
}
|
||||
|
||||
// UninstallRBAC uninstalls RBAC related resources for a binding, role, and service account
|
||||
func UninstallRBAC(clientset kubernetes.Interface, namespace, bindingName, roleName, serviceAccount string) {
|
||||
func UninstallRBAC(clientset kubernetes.Interface, namespace, bindingName, roleName, serviceAccount string) error {
|
||||
if err := clientset.RbacV1().ClusterRoleBindings().Delete(bindingName, &metav1.DeleteOptions{}); err != nil {
|
||||
if !apierr.IsNotFound(err) {
|
||||
log.Fatalf("Failed to delete ClusterRoleBinding: %v\n", err)
|
||||
return fmt.Errorf("Failed to delete ClusterRoleBinding: %v", err)
|
||||
}
|
||||
fmt.Printf("ClusterRoleBinding '%s' not found\n", bindingName)
|
||||
log.Infof("ClusterRoleBinding %q not found", bindingName)
|
||||
} else {
|
||||
fmt.Printf("ClusterRoleBinding '%s' deleted\n", bindingName)
|
||||
log.Infof("ClusterRoleBinding %q deleted", bindingName)
|
||||
}
|
||||
|
||||
if err := clientset.RbacV1().ClusterRoles().Delete(roleName, &metav1.DeleteOptions{}); err != nil {
|
||||
if !apierr.IsNotFound(err) {
|
||||
log.Fatalf("Failed to delete ClusterRole: %v\n", err)
|
||||
return fmt.Errorf("Failed to delete ClusterRole: %v", err)
|
||||
}
|
||||
fmt.Printf("ClusterRole '%s' not found\n", roleName)
|
||||
log.Infof("ClusterRole %q not found", roleName)
|
||||
} else {
|
||||
fmt.Printf("ClusterRole '%s' deleted\n", roleName)
|
||||
log.Infof("ClusterRole %q deleted", roleName)
|
||||
}
|
||||
|
||||
if err := clientset.CoreV1().ServiceAccounts(namespace).Delete(serviceAccount, &metav1.DeleteOptions{}); err != nil {
|
||||
if !apierr.IsNotFound(err) {
|
||||
log.Fatalf("Failed to delete ServiceAccount: %v\n", err)
|
||||
return fmt.Errorf("Failed to delete ServiceAccount: %v", err)
|
||||
}
|
||||
fmt.Printf("ServiceAccount '%s' in namespace '%s' not found\n", serviceAccount, namespace)
|
||||
log.Infof("ServiceAccount %q in namespace %q not found", serviceAccount, namespace)
|
||||
} else {
|
||||
fmt.Printf("ServiceAccount '%s' deleted\n", serviceAccount)
|
||||
log.Infof("ServiceAccount %q deleted", serviceAccount)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
999
controller/appcontroller.go
Normal file
@@ -0,0 +1,999 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
v1 "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/types"
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
|
||||
"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/errors"
|
||||
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/util"
|
||||
"github.com/argoproj/argo-cd/util/argo"
|
||||
argocache "github.com/argoproj/argo-cd/util/cache"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/diff"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
settings_util "github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
const (
|
||||
updateOperationStateTimeout = 1 * time.Second
|
||||
)
|
||||
|
||||
// ApplicationController is the controller for application resources.
|
||||
type ApplicationController struct {
|
||||
cache *argocache.Cache
|
||||
namespace string
|
||||
kubeClientset kubernetes.Interface
|
||||
kubectl kube.Kubectl
|
||||
applicationClientset appclientset.Interface
|
||||
auditLogger *argo.AuditLogger
|
||||
appRefreshQueue workqueue.RateLimitingInterface
|
||||
appOperationQueue workqueue.RateLimitingInterface
|
||||
appInformer cache.SharedIndexInformer
|
||||
appLister applisters.ApplicationLister
|
||||
projInformer cache.SharedIndexInformer
|
||||
appStateManager AppStateManager
|
||||
stateCache statecache.LiveStateCache
|
||||
statusRefreshTimeout time.Duration
|
||||
repoClientset reposerver.Clientset
|
||||
db db.ArgoDB
|
||||
settings *settings_util.ArgoCDSettings
|
||||
settingsMgr *settings_util.SettingsManager
|
||||
refreshRequestedApps map[string]bool
|
||||
refreshRequestedAppsMutex *sync.Mutex
|
||||
metricsServer *metrics.MetricsServer
|
||||
}
|
||||
|
||||
type ApplicationControllerConfig struct {
|
||||
InstanceID string
|
||||
Namespace string
|
||||
}
|
||||
|
||||
// NewApplicationController creates new instance of ApplicationController.
|
||||
func NewApplicationController(
|
||||
namespace string,
|
||||
settingsMgr *settings_util.SettingsManager,
|
||||
kubeClientset kubernetes.Interface,
|
||||
applicationClientset appclientset.Interface,
|
||||
repoClientset reposerver.Clientset,
|
||||
argoCache *argocache.Cache,
|
||||
appResyncPeriod time.Duration,
|
||||
) (*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,
|
||||
applicationClientset: applicationClientset,
|
||||
repoClientset: repoClientset,
|
||||
appRefreshQueue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
|
||||
appOperationQueue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
|
||||
db: db,
|
||||
statusRefreshTimeout: appResyncPeriod,
|
||||
refreshRequestedApps: make(map[string]bool),
|
||||
refreshRequestedAppsMutex: &sync.Mutex{},
|
||||
auditLogger: argo.NewAuditLogger(namespace, kubeClientset, "argocd-application-controller"),
|
||||
settingsMgr: settingsMgr,
|
||||
settings: settings,
|
||||
}
|
||||
appInformer, appLister := ctrl.newApplicationInformerAndLister()
|
||||
projInformer := v1alpha1.NewAppProjectInformer(applicationClientset, namespace, appResyncPeriod, cache.Indexers{})
|
||||
metricsAddr := fmt.Sprintf("0.0.0.0:%d", common.PortArgoCDMetrics)
|
||||
ctrl.metricsServer = metrics.NewMetricsServer(metricsAddr, appLister)
|
||||
stateCache := statecache.NewLiveStateCache(db, appInformer, ctrl.settings, kubectlCmd, ctrl.metricsServer, ctrl.handleAppUpdated)
|
||||
appStateManager := NewAppStateManager(db, applicationClientset, repoClientset, namespace, kubectlCmd, ctrl.settings, stateCache, projInformer, ctrl.metricsServer)
|
||||
ctrl.appInformer = appInformer
|
||||
ctrl.appLister = appLister
|
||||
ctrl.projInformer = projInformer
|
||||
ctrl.appStateManager = appStateManager
|
||||
ctrl.stateCache = stateCache
|
||||
|
||||
return &ctrl, 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) handleAppUpdated(appName string, fullRefresh bool, ref v1.ObjectReference) {
|
||||
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 {
|
||||
ctrl.requestAppRefresh(appName, fullRefresh)
|
||||
}
|
||||
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(a, comparisonResult)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tree, err := ctrl.getResourceTree(a, managedResources)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = ctrl.cache.SetAppResourcesTree(a.Name, tree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tree, ctrl.cache.SetAppManagedResources(a.Name, managedResources)
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) getResourceTree(a *appv1.Application, managedResources []*appv1.ResourceDiff) (*appv1.ApplicationTree, error) {
|
||||
nodes := make([]appv1.ResourceNode, 0)
|
||||
|
||||
for i := range managedResources {
|
||||
managedResource := managedResources[i]
|
||||
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) {
|
||||
nodes = append(nodes, child)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return &appv1.ApplicationTree{Nodes: nodes}, nil
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) managedResources(a *appv1.Application, comparisonResult *comparisonResult) ([]*appv1.ResourceDiff, error) {
|
||||
items := make([]*appv1.ResourceDiff, len(comparisonResult.managedResources))
|
||||
for i := range comparisonResult.managedResources {
|
||||
res := comparisonResult.managedResources[i]
|
||||
item := appv1.ResourceDiff{
|
||||
Namespace: res.Namespace,
|
||||
Name: res.Name,
|
||||
Group: res.Group,
|
||||
Kind: res.Kind,
|
||||
}
|
||||
|
||||
target := res.Target
|
||||
live := res.Live
|
||||
resDiff := res.Diff
|
||||
if res.Kind == kube.SecretKind && res.Group == "" {
|
||||
var err error
|
||||
target, live, err = diff.HideSecretData(res.Target, res.Live)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resDiff = *diff.Diff(target, live, comparisonResult.diffNormalizer)
|
||||
}
|
||||
|
||||
if live != nil {
|
||||
data, err := json.Marshal(live)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.LiveState = string(data)
|
||||
} else {
|
||||
item.LiveState = "null"
|
||||
}
|
||||
|
||||
if target != nil {
|
||||
data, err := json.Marshal(target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.TargetState = string(data)
|
||||
} else {
|
||||
item.TargetState = "null"
|
||||
}
|
||||
jsonDiff, err := resDiff.JSONFormat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.Diff = jsonDiff
|
||||
|
||||
items[i] = &item
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// Run starts the Application CRD controller.
|
||||
func (ctrl *ApplicationController) Run(ctx context.Context, statusProcessors int, operationProcessors int) {
|
||||
defer runtime.HandleCrash()
|
||||
defer ctrl.appRefreshQueue.ShutDown()
|
||||
|
||||
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.metricsServer.ListenAndServe()) }()
|
||||
|
||||
for i := 0; i < statusProcessors; i++ {
|
||||
go wait.Until(func() {
|
||||
for ctrl.processAppRefreshQueueItem() {
|
||||
}
|
||||
}, time.Second, ctx.Done())
|
||||
}
|
||||
|
||||
for i := 0; i < operationProcessors; i++ {
|
||||
go wait.Until(func() {
|
||||
for ctrl.processAppOperationQueueItem() {
|
||||
}
|
||||
}, time.Second, ctx.Done())
|
||||
}
|
||||
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) requestAppRefresh(appName string, fullRefresh bool) {
|
||||
ctrl.refreshRequestedAppsMutex.Lock()
|
||||
defer ctrl.refreshRequestedAppsMutex.Unlock()
|
||||
ctrl.refreshRequestedApps[appName] = fullRefresh || ctrl.refreshRequestedApps[appName]
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) isRefreshRequested(appName string) (bool, bool) {
|
||||
ctrl.refreshRequestedAppsMutex.Lock()
|
||||
defer ctrl.refreshRequestedAppsMutex.Unlock()
|
||||
fullRefresh, ok := ctrl.refreshRequestedApps[appName]
|
||||
if ok {
|
||||
delete(ctrl.refreshRequestedApps, appName)
|
||||
}
|
||||
return ok, fullRefresh
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) processAppOperationQueueItem() (processNext bool) {
|
||||
appKey, shutdown := ctrl.appOperationQueue.Get()
|
||||
if shutdown {
|
||||
processNext = false
|
||||
return
|
||||
}
|
||||
processNext = true
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
|
||||
}
|
||||
ctrl.appOperationQueue.Done(appKey)
|
||||
}()
|
||||
|
||||
obj, exists, err := ctrl.appInformer.GetIndexer().GetByKey(appKey.(string))
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get application '%s' from informer index: %+v", appKey, err)
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
// This happens after app was deleted, but the work queue still had an entry for it.
|
||||
return
|
||||
}
|
||||
app, ok := obj.(*appv1.Application)
|
||||
if !ok {
|
||||
log.Warnf("Key '%s' in index is not an application", appKey)
|
||||
return
|
||||
}
|
||||
if app.Operation != nil {
|
||||
ctrl.processRequestedAppOperation(app)
|
||||
} else if app.DeletionTimestamp != nil && app.CascadedDeletion() {
|
||||
err = ctrl.finalizeApplicationDeletion(app)
|
||||
if err != nil {
|
||||
ctrl.setAppCondition(app, appv1.ApplicationCondition{
|
||||
Type: appv1.ApplicationConditionDeletionError,
|
||||
Message: err.Error(),
|
||||
})
|
||||
message := fmt.Sprintf("Unable to delete application resources: %v", err.Error())
|
||||
ctrl.auditLogger.LogAppEvent(app, argo.EventInfo{Reason: argo.EventReasonStatusRefreshed, Type: v1.EventTypeWarning}, message)
|
||||
}
|
||||
}
|
||||
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")
|
||||
// Get refreshed application info, since informer app copy might be stale
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Get(app.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if !apierr.IsNotFound(err) {
|
||||
logCtx.Errorf("Unable to get refreshed application info prior deleting resources: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
objsMap, err := ctrl.stateCache.GetManagedLiveObjs(app, []*unstructured.Unstructured{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
objs := make([]*unstructured.Unstructured, 0)
|
||||
for k := range objsMap {
|
||||
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.kubectl.DeleteResource(config, obj.GroupVersionKind(), obj.GetName(), obj.GetNamespace(), false)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
objsMap, err = ctrl.stateCache.GetManagedLiveObjs(app, []*unstructured.Unstructured{})
|
||||
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
|
||||
}
|
||||
err = ctrl.cache.SetAppManagedResources(app.Name, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ctrl.cache.SetAppResourcesTree(app.Name, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
app.SetCascadedDeletion(false)
|
||||
var patch []byte
|
||||
patch, _ = json.Marshal(map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"finalizers": app.Finalizers,
|
||||
},
|
||||
})
|
||||
_, err = ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Patch(app.Name, types.MergePatchType, patch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logCtx.Info("Successfully deleted resources")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) setAppCondition(app *appv1.Application, condition appv1.ApplicationCondition) {
|
||||
index := -1
|
||||
for i, exiting := range app.Status.Conditions {
|
||||
if exiting.Type == condition.Type {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index > -1 {
|
||||
app.Status.Conditions[index] = condition
|
||||
} else {
|
||||
app.Status.Conditions = append(app.Status.Conditions, condition)
|
||||
}
|
||||
var patch []byte
|
||||
patch, err := json.Marshal(map[string]interface{}{
|
||||
"status": map[string]interface{}{
|
||||
"conditions": app.Status.Conditions,
|
||||
},
|
||||
})
|
||||
if err == nil {
|
||||
_, err = ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Patch(app.Name, types.MergePatchType, patch)
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("Unable to set application condition: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Application) {
|
||||
logCtx := log.WithField("application", app.Name)
|
||||
var state *appv1.OperationState
|
||||
// Recover from any unexpected panics and automatically set the status to be failed
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logCtx.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
|
||||
state.Phase = appv1.OperationError
|
||||
if rerr, ok := r.(error); ok {
|
||||
state.Message = rerr.Error()
|
||||
} else {
|
||||
state.Message = fmt.Sprintf("%v", r)
|
||||
}
|
||||
ctrl.setOperationState(app, state)
|
||||
}
|
||||
}()
|
||||
if isOperationInProgress(app) {
|
||||
// If we get here, we are about process an operation but we notice it is already in progress.
|
||||
// We need to detect if the app object we pulled off the informer is stale and doesn't
|
||||
// reflect the fact that the operation is completed. We don't want to perform the operation
|
||||
// again. To detect this, always retrieve the latest version to ensure it is not stale.
|
||||
freshApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(ctrl.namespace).Get(app.ObjectMeta.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
logCtx.Errorf("Failed to retrieve latest application state: %v", err)
|
||||
return
|
||||
}
|
||||
if !isOperationInProgress(freshApp) {
|
||||
logCtx.Infof("Skipping operation on stale application state")
|
||||
return
|
||||
}
|
||||
app = freshApp
|
||||
state = app.Status.OperationState.DeepCopy()
|
||||
logCtx.Infof("Resuming in-progress operation. phase: %s, message: %s", state.Phase, state.Message)
|
||||
} else {
|
||||
state = &appv1.OperationState{Phase: appv1.OperationRunning, Operation: *app.Operation, StartedAt: metav1.Now()}
|
||||
ctrl.setOperationState(app, state)
|
||||
logCtx.Infof("Initialized new operation: %v", *app.Operation)
|
||||
}
|
||||
ctrl.appStateManager.SyncAppState(app, state)
|
||||
|
||||
if state.Phase == appv1.OperationRunning {
|
||||
// It's possible for an app to be terminated while we were operating on it. We do not want
|
||||
// to clobber the Terminated state with Running. Get the latest app state to check for this.
|
||||
freshApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(ctrl.namespace).Get(app.ObjectMeta.Name, metav1.GetOptions{})
|
||||
if err == nil {
|
||||
if freshApp.Status.OperationState != nil && freshApp.Status.OperationState.Phase == appv1.OperationTerminating {
|
||||
state.Phase = appv1.OperationTerminating
|
||||
state.Message = "operation is terminating"
|
||||
// after this, we will get requeued to the workqueue, but next time the
|
||||
// SyncAppState will operate in a Terminating phase, allowing the worker to perform
|
||||
// cleanup (e.g. delete jobs, workflows, etc...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctrl.setOperationState(app, state)
|
||||
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, true)
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) setOperationState(app *appv1.Application, state *appv1.OperationState) {
|
||||
util.RetryUntilSucceed(func() error {
|
||||
if state.Phase == "" {
|
||||
// expose any bugs where we neglect to set phase
|
||||
panic("no phase was set")
|
||||
}
|
||||
if state.Phase.Completed() {
|
||||
now := metav1.Now()
|
||||
state.FinishedAt = &now
|
||||
}
|
||||
patch := map[string]interface{}{
|
||||
"status": map[string]interface{}{
|
||||
"operationState": state,
|
||||
},
|
||||
}
|
||||
if state.Phase.Completed() {
|
||||
// If operation is completed, clear the operation field to indicate no operation is
|
||||
// in progress.
|
||||
patch["operation"] = nil
|
||||
}
|
||||
if reflect.DeepEqual(app.Status.OperationState, state) {
|
||||
log.Infof("No operation updates necessary to '%s'. Skipping patch", app.Name)
|
||||
return nil
|
||||
}
|
||||
patchJSON, err := json.Marshal(patch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
appClient := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(ctrl.namespace)
|
||||
_, err = appClient.Patch(app.Name, types.MergePatchType, patchJSON)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("updated '%s' operation (phase: %s)", app.Name, state.Phase)
|
||||
if state.Phase.Completed() {
|
||||
eventInfo := argo.EventInfo{Reason: argo.EventReasonOperationCompleted}
|
||||
var messages []string
|
||||
if state.Operation.Sync != nil && len(state.Operation.Sync.Resources) > 0 {
|
||||
messages = []string{"Partial sync operation"}
|
||||
} else {
|
||||
messages = []string{"Sync operation"}
|
||||
}
|
||||
if state.SyncResult != nil {
|
||||
messages = append(messages, "to", state.SyncResult.Revision)
|
||||
}
|
||||
if state.Phase.Successful() {
|
||||
eventInfo.Type = v1.EventTypeNormal
|
||||
messages = append(messages, "succeeded")
|
||||
} else {
|
||||
eventInfo.Type = v1.EventTypeWarning
|
||||
messages = append(messages, "failed:", state.Message)
|
||||
}
|
||||
ctrl.auditLogger.LogAppEvent(app, eventInfo, strings.Join(messages, " "))
|
||||
ctrl.metricsServer.IncSync(app, state)
|
||||
}
|
||||
return nil
|
||||
}, "Update application operation state", context.Background(), updateOperationStateTimeout)
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext bool) {
|
||||
appKey, shutdown := ctrl.appRefreshQueue.Get()
|
||||
if shutdown {
|
||||
processNext = false
|
||||
return
|
||||
}
|
||||
processNext = true
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
|
||||
}
|
||||
ctrl.appRefreshQueue.Done(appKey)
|
||||
}()
|
||||
|
||||
obj, exists, err := ctrl.appInformer.GetIndexer().GetByKey(appKey.(string))
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get application '%s' from informer index: %+v", appKey, err)
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
// This happens after app was deleted, but the work queue still had an entry for it.
|
||||
return
|
||||
}
|
||||
origApp, ok := obj.(*appv1.Application)
|
||||
if !ok {
|
||||
log.Warnf("Key '%s' in index is not an application", appKey)
|
||||
return
|
||||
}
|
||||
needRefresh, refreshType, fullRefresh := ctrl.needRefreshAppStatus(origApp, ctrl.statusRefreshTimeout)
|
||||
|
||||
if !needRefresh {
|
||||
return
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
reconcileDuration := time.Since(startTime)
|
||||
ctrl.metricsServer.IncReconcile(origApp, reconcileDuration)
|
||||
logCtx := log.WithFields(log.Fields{"application": origApp.Name, "time_ms": reconcileDuration.Seconds() * 1e3, "full": fullRefresh})
|
||||
logCtx.Info("Reconciliation completed")
|
||||
}()
|
||||
|
||||
app := origApp.DeepCopy()
|
||||
logCtx := log.WithFields(log.Fields{"application": app.Name})
|
||||
if !fullRefresh {
|
||||
if managedResources, err := ctrl.cache.GetAppManagedResources(app.Name); 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
|
||||
}
|
||||
}
|
||||
app.Status.ObservedAt = metav1.Now()
|
||||
ctrl.persistAppStatus(origApp, &app.Status)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
conditions, 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)
|
||||
if err != nil {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{Type: appv1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
} else {
|
||||
ctrl.normalizeApplication(origApp, app, compareResult.appSourceType)
|
||||
conditions = append(conditions, compareResult.conditions...)
|
||||
}
|
||||
tree, err := ctrl.setAppManagedResources(app, compareResult)
|
||||
if err != nil {
|
||||
logCtx.Errorf("Failed to cache app resources: %v", err)
|
||||
} else {
|
||||
app.Status.Summary = tree.GetSummary()
|
||||
}
|
||||
|
||||
syncErrCond := ctrl.autoSync(app, compareResult.syncStatus)
|
||||
if syncErrCond != nil {
|
||||
conditions = append(conditions, *syncErrCond)
|
||||
}
|
||||
|
||||
app.Status.ObservedAt = compareResult.reconciledAt
|
||||
app.Status.ReconciledAt = compareResult.reconciledAt
|
||||
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.
|
||||
// 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, bool) {
|
||||
logCtx := log.WithFields(log.Fields{"application": app.Name})
|
||||
var reason string
|
||||
fullRefresh := true
|
||||
refreshType := appv1.RefreshTypeNormal
|
||||
expired := app.Status.ReconciledAt.Add(statusRefreshTimeout).Before(time.Now().UTC())
|
||||
if requestedType, ok := app.IsRefreshRequested(); ok {
|
||||
refreshType = requestedType
|
||||
reason = fmt.Sprintf("%s refresh requested", refreshType)
|
||||
} else if requested, full := ctrl.isRefreshRequested(app.Name); requested {
|
||||
fullRefresh = full
|
||||
reason = fmt.Sprintf("controller refresh requested")
|
||||
} else if app.Status.Sync.Status == appv1.SyncStatusCodeUnknown && expired {
|
||||
reason = "comparison status unknown"
|
||||
} 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. reconciledAt: %v, expiry: %v", app.Status.ReconciledAt, statusRefreshTimeout)
|
||||
}
|
||||
if reason != "" {
|
||||
logCtx.Infof("Refreshing app status (%s)", reason)
|
||||
return true, refreshType, fullRefresh
|
||||
}
|
||||
return false, refreshType, fullRefresh
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
if apierr.IsNotFound(err) {
|
||||
conditions = append(conditions, 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{
|
||||
Type: appv1.ApplicationConditionUnknownError,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
specConditions, err := argo.ValidatePermissions(context.Background(), &app.Spec, proj, ctrl.db)
|
||||
if err != nil {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{
|
||||
Type: appv1.ApplicationConditionUnknownError,
|
||||
Message: err.Error(),
|
||||
})
|
||||
} else {
|
||||
conditions = append(conditions, 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,
|
||||
appv1.ApplicationConditionRepeatedResourceWarning: 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
|
||||
}
|
||||
|
||||
// normalizeApplication normalizes an application.spec and additionally persists updates if it changed
|
||||
func (ctrl *ApplicationController) normalizeApplication(orig, app *appv1.Application, sourceType appv1.ApplicationSourceType) {
|
||||
logCtx := log.WithFields(log.Fields{"application": app.Name})
|
||||
app.Spec = *argo.NormalizeApplicationSpec(&app.Spec, sourceType)
|
||||
patch, modified, err := diff.CreateTwoWayMergePatch(orig, app, appv1.Application{})
|
||||
if err != nil {
|
||||
logCtx.Errorf("error constructing app spec patch: %v", err)
|
||||
} else if modified {
|
||||
appClient := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace)
|
||||
_, err = appClient.Patch(app.Name, types.MergePatchType, patch)
|
||||
if err != nil {
|
||||
logCtx.Errorf("Error persisting normalized application spec: %v", err)
|
||||
} else {
|
||||
logCtx.Infof("Normalized app spec: %s", string(patch))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// persistAppStatus persists updates to application status. If no changes were made, it is a no-op
|
||||
func (ctrl *ApplicationController) persistAppStatus(orig *appv1.Application, newStatus *appv1.ApplicationStatus) {
|
||||
logCtx := log.WithFields(log.Fields{"application": orig.Name})
|
||||
if orig.Status.Sync.Status != newStatus.Sync.Status {
|
||||
message := fmt.Sprintf("Updated sync status: %s -> %s", orig.Status.Sync.Status, newStatus.Sync.Status)
|
||||
ctrl.auditLogger.LogAppEvent(orig, argo.EventInfo{Reason: argo.EventReasonResourceUpdated, Type: v1.EventTypeNormal}, message)
|
||||
}
|
||||
if orig.Status.Health.Status != newStatus.Health.Status {
|
||||
message := fmt.Sprintf("Updated health status: %s -> %s", orig.Status.Health.Status, newStatus.Health.Status)
|
||||
ctrl.auditLogger.LogAppEvent(orig, argo.EventInfo{Reason: argo.EventReasonResourceUpdated, Type: v1.EventTypeNormal}, message)
|
||||
}
|
||||
var newAnnotations map[string]string
|
||||
if orig.GetAnnotations() != nil {
|
||||
newAnnotations = make(map[string]string)
|
||||
for k, v := range orig.GetAnnotations() {
|
||||
newAnnotations[k] = v
|
||||
}
|
||||
delete(newAnnotations, common.AnnotationKeyRefresh)
|
||||
}
|
||||
patch, modified, err := diff.CreateTwoWayMergePatch(
|
||||
&appv1.Application{ObjectMeta: metav1.ObjectMeta{Annotations: orig.GetAnnotations()}, Status: orig.Status},
|
||||
&appv1.Application{ObjectMeta: metav1.ObjectMeta{Annotations: newAnnotations}, Status: *newStatus}, appv1.Application{})
|
||||
if err != nil {
|
||||
logCtx.Errorf("Error constructing app status patch: %v", err)
|
||||
return
|
||||
}
|
||||
if !modified {
|
||||
logCtx.Infof("No status changes. Skipping patch")
|
||||
return
|
||||
}
|
||||
logCtx.Debugf("patch: %s", string(patch))
|
||||
appClient := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(orig.Namespace)
|
||||
_, err = appClient.Patch(orig.Name, types.MergePatchType, patch)
|
||||
if err != nil {
|
||||
logCtx.Warnf("Error updating application: %v", err)
|
||||
} else {
|
||||
logCtx.Infof("Update successful")
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if app.Spec.SyncPolicy == nil || app.Spec.SyncPolicy.Automated == nil {
|
||||
return nil
|
||||
}
|
||||
logCtx := log.WithFields(log.Fields{"application": app.Name})
|
||||
if app.Operation != nil {
|
||||
logCtx.Infof("Skipping auto-sync: another operation is in progress")
|
||||
return nil
|
||||
}
|
||||
if app.DeletionTimestamp != nil && !app.DeletionTimestamp.IsZero() {
|
||||
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
|
||||
}
|
||||
|
||||
op := appv1.Operation{
|
||||
Sync: &appv1.SyncOperation{
|
||||
Revision: desiredCommitSHA,
|
||||
Prune: app.Spec.SyncPolicy.Automated.Prune,
|
||||
},
|
||||
}
|
||||
appIf := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace)
|
||||
_, err := argo.SetAppOperation(appIf, app.Name, &op)
|
||||
if err != nil {
|
||||
logCtx.Errorf("Failed to initiate auto-sync to %s: %v", desiredCommitSHA, err)
|
||||
return &appv1.ApplicationCondition{Type: appv1.ApplicationConditionSyncError, Message: err.Error()}
|
||||
}
|
||||
message := fmt.Sprintf("Initiated automated sync to '%s'", desiredCommitSHA)
|
||||
ctrl.auditLogger.LogAppEvent(app, argo.EventInfo{Reason: argo.EventReasonOperationStarted, Type: v1.EventTypeNormal}, message)
|
||||
logCtx.Info(message)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if app.Status.OperationState == nil || app.Status.OperationState.Operation.Sync == nil || app.Status.OperationState.SyncResult == nil {
|
||||
return false
|
||||
}
|
||||
if app.Status.OperationState.SyncResult.Revision != commitSHA {
|
||||
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
|
||||
specSource := app.Spec.Source.DeepCopy()
|
||||
specSource.TargetRevision = ""
|
||||
syncResSource := app.Status.OperationState.SyncResult.Source.DeepCopy()
|
||||
syncResSource.TargetRevision = ""
|
||||
return reflect.DeepEqual(app.Spec.Source, app.Status.OperationState.SyncResult.Source)
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) newApplicationInformerAndLister() (cache.SharedIndexInformer, applisters.ApplicationLister) {
|
||||
appInformerFactory := appinformers.NewFilteredSharedInformerFactory(
|
||||
ctrl.applicationClientset,
|
||||
ctrl.statusRefreshTimeout,
|
||||
ctrl.namespace,
|
||||
func(options *metav1.ListOptions) {},
|
||||
)
|
||||
informer := appInformerFactory.Argoproj().V1alpha1().Applications().Informer()
|
||||
lister := appInformerFactory.Argoproj().V1alpha1().Applications().Lister()
|
||||
informer.AddEventHandler(
|
||||
cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(obj interface{}) {
|
||||
key, err := cache.MetaNamespaceKeyFunc(obj)
|
||||
if err == nil {
|
||||
ctrl.appRefreshQueue.Add(key)
|
||||
ctrl.appOperationQueue.Add(key)
|
||||
}
|
||||
},
|
||||
UpdateFunc: func(old, new interface{}) {
|
||||
key, err := cache.MetaNamespaceKeyFunc(new)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
oldApp, oldOK := old.(*appv1.Application)
|
||||
newApp, newOK := new.(*appv1.Application)
|
||||
if oldOK && newOK {
|
||||
if toggledAutomatedSync(oldApp, newApp) {
|
||||
log.WithField("application", newApp.Name).Info("Enabled automated sync")
|
||||
ctrl.requestAppRefresh(newApp.Name, true)
|
||||
}
|
||||
}
|
||||
ctrl.appRefreshQueue.Add(key)
|
||||
ctrl.appOperationQueue.Add(key)
|
||||
},
|
||||
DeleteFunc: func(obj interface{}) {
|
||||
// IndexerInformer uses a delta queue, therefore for deletes we have to use this
|
||||
// key function.
|
||||
key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
|
||||
if err == nil {
|
||||
ctrl.appRefreshQueue.Add(key)
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
return informer, lister
|
||||
}
|
||||
|
||||
func isOperationInProgress(app *appv1.Application) bool {
|
||||
return app.Status.OperationState != nil && !app.Status.OperationState.Phase.Completed()
|
||||
}
|
||||
|
||||
// toggledAutomatedSync tests if an app went from auto-sync disabled to enabled.
|
||||
// if it was toggled to be enabled, the informer handler will force a refresh
|
||||
func toggledAutomatedSync(old *appv1.Application, new *appv1.Application) bool {
|
||||
if new.Spec.SyncPolicy == nil || new.Spec.SyncPolicy.Automated == nil {
|
||||
return false
|
||||
}
|
||||
// auto-sync is enabled. check if it was previously disabled
|
||||
if old.Spec.SyncPolicy == nil || old.Spec.SyncPolicy.Automated == nil {
|
||||
return true
|
||||
}
|
||||
// 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
|
||||
prevResourceInclusions := ctrl.settings.ResourceInclusions
|
||||
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.WithFields(log.Fields{"prevResourceExclusions": prevResourceExclusions, "newResourceExclusions": newSettings.ResourceExclusions}).Info("resource exclusions modified")
|
||||
ctrl.stateCache.Invalidate()
|
||||
prevResourceExclusions = newSettings.ResourceExclusions
|
||||
}
|
||||
if !reflect.DeepEqual(prevResourceInclusions, newSettings.ResourceInclusions) {
|
||||
log.WithFields(log.Fields{"prevResourceInclusions": prevResourceInclusions, "newResourceInclusions": newSettings.ResourceInclusions}).Info("resource inclusions modified")
|
||||
ctrl.stateCache.Invalidate()
|
||||
prevResourceInclusions = newSettings.ResourceInclusions
|
||||
}
|
||||
case <-ctx.Done():
|
||||
done = true
|
||||
}
|
||||
}
|
||||
log.Info("shutting down settings watch")
|
||||
ctrl.settingsMgr.Unsubscribe(updateCh)
|
||||
close(updateCh)
|
||||
}
|
||||
468
controller/appcontroller_test.go
Normal file
@@ -0,0 +1,468 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
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/client-go/kubernetes/fake"
|
||||
kubetesting "k8s.io/client-go/testing"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
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"
|
||||
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/settings"
|
||||
)
|
||||
|
||||
type fakeData struct {
|
||||
apps []runtime.Object
|
||||
manifestResponse *repository.ManifestResponse
|
||||
managedLiveObjs map[kube.ResourceKey]*unstructured.Unstructured
|
||||
}
|
||||
|
||||
func newFakeController(data *fakeData) *ApplicationController {
|
||||
var clust corev1.Secret
|
||||
err := yaml.Unmarshal([]byte(fakeCluster), &clust)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Mock out call to GenerateManifest
|
||||
mockRepoClient := mockrepoclient.RepoServerServiceClient{}
|
||||
mockRepoClient.On("GenerateManifest", mock.Anything, mock.Anything).Return(data.manifestResponse, nil)
|
||||
mockRepoClientset := mockreposerver.Clientset{}
|
||||
mockRepoClientset.On("NewRepoServerClient").Return(&fakeCloser{}, &mockRepoClient, nil)
|
||||
|
||||
secret := corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "argocd-secret",
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"admin.password": []byte("test"),
|
||||
"server.secretkey": []byte("test"),
|
||||
},
|
||||
}
|
||||
cm := corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "argocd-cm",
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
},
|
||||
Data: nil,
|
||||
}
|
||||
kubeClient := fake.NewSimpleClientset(&clust, &cm, &secret)
|
||||
settingsMgr := settings.NewSettingsManager(context.Background(), kubeClient, test.FakeArgoCDNamespace)
|
||||
ctrl, err := NewApplicationController(
|
||||
test.FakeArgoCDNamespace,
|
||||
settingsMgr,
|
||||
kubeClient,
|
||||
appclientset.NewSimpleClientset(data.apps...),
|
||||
&mockRepoClientset,
|
||||
utilcache.NewCache(utilcache.NewInMemoryCache(1*time.Hour)),
|
||||
time.Minute,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cancelProj := test.StartInformer(ctrl.projInformer)
|
||||
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
|
||||
}
|
||||
return ctrl
|
||||
}
|
||||
|
||||
type fakeCloser struct{}
|
||||
|
||||
func (f *fakeCloser) Close() error { return nil }
|
||||
|
||||
var fakeCluster = `
|
||||
apiVersion: v1
|
||||
data:
|
||||
# {"bearerToken":"fake","tlsClientConfig":{"insecure":true},"awsAuthConfig":null}
|
||||
config: eyJiZWFyZXJUb2tlbiI6ImZha2UiLCJ0bHNDbGllbnRDb25maWciOnsiaW5zZWN1cmUiOnRydWV9LCJhd3NBdXRoQ29uZmlnIjpudWxsfQ==
|
||||
# minikube
|
||||
name: aHR0cHM6Ly9sb2NhbGhvc3Q6NjQ0Mw==
|
||||
# https://localhost:6443
|
||||
server: aHR0cHM6Ly9sb2NhbGhvc3Q6NjQ0Mw==
|
||||
kind: Secret
|
||||
metadata:
|
||||
labels:
|
||||
argocd.argoproj.io/secret-type: cluster
|
||||
name: some-secret
|
||||
namespace: ` + test.FakeArgoCDNamespace + `
|
||||
type: Opaque
|
||||
`
|
||||
|
||||
var fakeApp = `
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
uid: "123"
|
||||
name: my-app
|
||||
namespace: ` + test.FakeArgoCDNamespace + `
|
||||
spec:
|
||||
destination:
|
||||
namespace: ` + test.FakeDestNamespace + `
|
||||
server: https://localhost:6443
|
||||
project: default
|
||||
source:
|
||||
path: some/path
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps.git
|
||||
syncPolicy:
|
||||
automated: {}
|
||||
status:
|
||||
operationState:
|
||||
finishedAt: 2018-09-21T23:50:29Z
|
||||
message: successfully synced
|
||||
operation:
|
||||
sync:
|
||||
revision: HEAD
|
||||
phase: Succeeded
|
||||
startedAt: 2018-09-21T23:50:25Z
|
||||
syncResult:
|
||||
resources:
|
||||
- kind: RoleBinding
|
||||
message: |-
|
||||
rolebinding.rbac.authorization.k8s.io/always-outofsync reconciled
|
||||
rolebinding.rbac.authorization.k8s.io/always-outofsync configured
|
||||
name: always-outofsync
|
||||
namespace: default
|
||||
status: Synced
|
||||
revision: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
source:
|
||||
path: some/path
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps.git
|
||||
`
|
||||
|
||||
func newFakeApp() *argoappv1.Application {
|
||||
var app argoappv1.Application
|
||||
err := yaml.Unmarshal([]byte(fakeApp), &app)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &app
|
||||
}
|
||||
|
||||
func TestAutoSync(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus := argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus)
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, app.Operation)
|
||||
assert.NotNil(t, app.Operation.Sync)
|
||||
assert.False(t, app.Operation.Sync.Prune)
|
||||
}
|
||||
|
||||
func TestSkipAutoSync(t *testing.T) {
|
||||
// Verify we skip when we previously synced to it in our most recent history
|
||||
// Set current to 'aaaaa', desired to 'aaaa' and mark system OutOfSync
|
||||
{
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus := argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus)
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
}
|
||||
|
||||
// Verify we skip when we are already Synced (even if revision is different)
|
||||
{
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus := argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeSynced,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus)
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
}
|
||||
|
||||
// Verify we skip when auto-sync is disabled
|
||||
{
|
||||
app := newFakeApp()
|
||||
app.Spec.SyncPolicy = nil
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus := argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus)
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
}
|
||||
|
||||
// Verify we skip when application is marked for deletion
|
||||
{
|
||||
app := newFakeApp()
|
||||
now := metav1.Now()
|
||||
app.DeletionTimestamp = &now
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus := argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus)
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
}
|
||||
|
||||
// Verify we skip when previous sync attempt failed and return error condition
|
||||
// Set current to 'aaaaa', desired to 'bbbbb' and add 'bbbbb' to failure history
|
||||
{
|
||||
app := newFakeApp()
|
||||
app.Status.OperationState = &argoappv1.OperationState{
|
||||
Operation: argoappv1.Operation{
|
||||
Sync: &argoappv1.SyncOperation{},
|
||||
},
|
||||
Phase: argoappv1.OperationFailed,
|
||||
SyncResult: &argoappv1.SyncOperationResult{
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
Source: *app.Spec.Source.DeepCopy(),
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus := argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus)
|
||||
assert.NotNil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAutoSyncIndicateError verifies we skip auto-sync and return error condition if previous sync failed
|
||||
func TestAutoSyncIndicateError(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Spec.Source.Helm = &argoappv1.ApplicationSourceHelm{
|
||||
Parameters: []argoappv1.HelmParameter{
|
||||
{
|
||||
Name: "a",
|
||||
Value: "1",
|
||||
},
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus := argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
}
|
||||
app.Status.OperationState = &argoappv1.OperationState{
|
||||
Operation: argoappv1.Operation{
|
||||
Sync: &argoappv1.SyncOperation{
|
||||
Source: app.Spec.Source.DeepCopy(),
|
||||
},
|
||||
},
|
||||
Phase: argoappv1.OperationFailed,
|
||||
SyncResult: &argoappv1.SyncOperationResult{
|
||||
Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
Source: *app.Spec.Source.DeepCopy(),
|
||||
},
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus)
|
||||
assert.NotNil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
}
|
||||
|
||||
// TestAutoSyncParameterOverrides verifies we auto-sync if revision is same but parameter overrides are different
|
||||
func TestAutoSyncParameterOverrides(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Spec.Source.Helm = &argoappv1.ApplicationSourceHelm{
|
||||
Parameters: []argoappv1.HelmParameter{
|
||||
{
|
||||
Name: "a",
|
||||
Value: "1",
|
||||
},
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus := argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
}
|
||||
app.Status.OperationState = &argoappv1.OperationState{
|
||||
Operation: argoappv1.Operation{
|
||||
Sync: &argoappv1.SyncOperation{
|
||||
Source: &argoappv1.ApplicationSource{
|
||||
Helm: &argoappv1.ApplicationSourceHelm{
|
||||
Parameters: []argoappv1.HelmParameter{
|
||||
{
|
||||
Name: "a",
|
||||
Value: "2", // this value changed
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Phase: argoappv1.OperationFailed,
|
||||
SyncResult: &argoappv1.SyncOperationResult{
|
||||
Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
},
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus)
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, app.Operation)
|
||||
}
|
||||
|
||||
// TestFinalizeAppDeletion verifies application deletion
|
||||
func TestFinalizeAppDeletion(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
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,
|
||||
}})
|
||||
|
||||
patched := false
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
defaultReactor := fakeAppCs.ReactionChain[0]
|
||||
fakeAppCs.ReactionChain = nil
|
||||
fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
return defaultReactor.React(action)
|
||||
})
|
||||
fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
patched = true
|
||||
return true, nil, nil
|
||||
})
|
||||
err := ctrl.finalizeApplicationDeletion(app)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, patched)
|
||||
}
|
||||
|
||||
// TestNormalizeApplication verifies we normalize an application during reconciliation
|
||||
func TestNormalizeApplication(t *testing.T) {
|
||||
defaultProj := argoappv1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
},
|
||||
Spec: argoappv1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []argoappv1.ApplicationDestination{
|
||||
{
|
||||
Server: "*",
|
||||
Namespace: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
app := newFakeApp()
|
||||
app.Spec.Project = ""
|
||||
app.Spec.Source.Kustomize = &argoappv1.ApplicationSourceKustomize{NamePrefix: "foo-"}
|
||||
data := fakeData{
|
||||
apps: []runtime.Object{app, &defaultProj},
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
|
||||
{
|
||||
// Verify we normalize the app because project is missing
|
||||
ctrl := newFakeController(&data)
|
||||
key, _ := cache.MetaNamespaceKeyFunc(app)
|
||||
ctrl.appRefreshQueue.Add(key)
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
fakeAppCs.ReactionChain = nil
|
||||
normalized := false
|
||||
fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
if patchAction, ok := action.(kubetesting.PatchAction); ok {
|
||||
if string(patchAction.GetPatch()) == `{"spec":{"project":"default"}}` {
|
||||
normalized = true
|
||||
}
|
||||
}
|
||||
return true, nil, nil
|
||||
})
|
||||
ctrl.processAppRefreshQueueItem()
|
||||
assert.True(t, normalized)
|
||||
}
|
||||
|
||||
{
|
||||
// Verify we don't unnecessarily normalize app when project is set
|
||||
app.Spec.Project = "default"
|
||||
data.apps[0] = app
|
||||
ctrl := newFakeController(&data)
|
||||
key, _ := cache.MetaNamespaceKeyFunc(app)
|
||||
ctrl.appRefreshQueue.Add(key)
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
fakeAppCs.ReactionChain = nil
|
||||
normalized := false
|
||||
fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
if patchAction, ok := action.(kubetesting.PatchAction); ok {
|
||||
if string(patchAction.GetPatch()) == `{"spec":{"project":"default"}}` {
|
||||
normalized = true
|
||||
}
|
||||
}
|
||||
return true, nil, nil
|
||||
})
|
||||
ctrl.processAppRefreshQueueItem()
|
||||
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.handleAppUpdated(app.Name, true, kube.GetObjectRef(kube.MustToUnstructured(app)))
|
||||
isRequested, _ := ctrl.isRefreshRequested(app.Name)
|
||||
assert.False(t, isRequested)
|
||||
|
||||
ctrl.handleAppUpdated(app.Name, true, corev1.ObjectReference{UID: "test", Kind: kube.DeploymentKind, Name: "test", Namespace: "default"})
|
||||
isRequested, _ = ctrl.isRefreshRequested(app.Name)
|
||||
assert.True(t, isRequested)
|
||||
}
|
||||
191
controller/cache/cache.go
vendored
Normal file
@@ -0,0 +1,191 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
type LiveStateCache interface {
|
||||
IsNamespaced(server string, obj *unstructured.Unstructured) (bool, error)
|
||||
// Executes give callback against resource specified by the key and all its children
|
||||
IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode)) error
|
||||
// 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)
|
||||
// Starts watching resources of each controlled cluster.
|
||||
Run(ctx context.Context)
|
||||
// Invalidate invalidates the entire cluster state cache
|
||||
Invalidate()
|
||||
}
|
||||
|
||||
type AppUpdatedHandler = func(appName string, fullRefresh bool, ref v1.ObjectReference)
|
||||
|
||||
func GetTargetObjKey(a *appv1.Application, un *unstructured.Unstructured, isNamespaced bool) kube.ResourceKey {
|
||||
key := kube.GetResourceKey(un)
|
||||
if !isNamespaced {
|
||||
key.Namespace = ""
|
||||
} else if isNamespaced && key.Namespace == "" {
|
||||
key.Namespace = a.Spec.Destination.Namespace
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
func NewLiveStateCache(
|
||||
db db.ArgoDB,
|
||||
appInformer cache.SharedIndexInformer,
|
||||
settings *settings.ArgoCDSettings,
|
||||
kubectl kube.Kubectl,
|
||||
metricsServer *metrics.MetricsServer,
|
||||
onAppUpdated AppUpdatedHandler) LiveStateCache {
|
||||
|
||||
return &liveStateCache{
|
||||
appInformer: appInformer,
|
||||
db: db,
|
||||
clusters: make(map[string]*clusterInfo),
|
||||
lock: &sync.Mutex{},
|
||||
onAppUpdated: onAppUpdated,
|
||||
kubectl: kubectl,
|
||||
settings: settings,
|
||||
metricsServer: metricsServer,
|
||||
}
|
||||
}
|
||||
|
||||
type liveStateCache struct {
|
||||
db db.ArgoDB
|
||||
clusters map[string]*clusterInfo
|
||||
lock *sync.Mutex
|
||||
appInformer cache.SharedIndexInformer
|
||||
onAppUpdated AppUpdatedHandler
|
||||
kubectl kube.Kubectl
|
||||
settings *settings.ArgoCDSettings
|
||||
metricsServer *metrics.MetricsServer
|
||||
}
|
||||
|
||||
func (c *liveStateCache) getCluster(server string) (*clusterInfo, error) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
info, ok := c.clusters[server]
|
||||
if !ok {
|
||||
cluster, err := c.db.GetCluster(context.Background(), server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info = &clusterInfo{
|
||||
apisMeta: make(map[schema.GroupKind]*apiMeta),
|
||||
lock: &sync.Mutex{},
|
||||
nodes: make(map[kube.ResourceKey]*node),
|
||||
nsIndex: make(map[string]map[kube.ResourceKey]*node),
|
||||
onAppUpdated: c.onAppUpdated,
|
||||
kubectl: c.kubectl,
|
||||
cluster: cluster,
|
||||
syncTime: nil,
|
||||
syncLock: &sync.Mutex{},
|
||||
log: log.WithField("server", cluster.Server),
|
||||
settings: c.settings,
|
||||
}
|
||||
|
||||
c.clusters[cluster.Server] = info
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) getSyncedCluster(server string) (*clusterInfo, error) {
|
||||
info, err := c.getCluster(server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = info.ensureSynced()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) Invalidate() {
|
||||
log.Info("invalidating live state cache")
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
for _, clust := range c.clusters {
|
||||
clust.lock.Lock()
|
||||
clust.invalidate()
|
||||
clust.lock.Unlock()
|
||||
}
|
||||
log.Info("live state cache invalidated")
|
||||
}
|
||||
|
||||
func (c *liveStateCache) IsNamespaced(server string, obj *unstructured.Unstructured) (bool, error) {
|
||||
clusterInfo, err := c.getSyncedCluster(server)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return clusterInfo.isNamespaced(obj), nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode)) error {
|
||||
clusterInfo, err := c.getSyncedCluster(server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clusterInfo.iterateHierarchy(key, action)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) GetManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
|
||||
clusterInfo, err := c.getSyncedCluster(a.Spec.Destination.Server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return clusterInfo.getManagedLiveObjs(a, targetObjs, c.metricsServer)
|
||||
}
|
||||
|
||||
func isClusterHasApps(apps []interface{}, cluster *appv1.Cluster) bool {
|
||||
for _, obj := range apps {
|
||||
if app, ok := obj.(*appv1.Application); ok && app.Spec.Destination.Server == cluster.Server {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 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) {
|
||||
util.RetryUntilSucceed(func() error {
|
||||
clusterEventCallback := func(event *db.ClusterEvent) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if cluster, ok := c.clusters[event.Cluster.Server]; ok {
|
||||
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()
|
||||
}
|
||||
} 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)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
return c.db.WatchClusters(ctx, clusterEventCallback)
|
||||
|
||||
}, "watch clusters", ctx, clusterRetryTimeout)
|
||||
|
||||
<-ctx.Done()
|
||||
}
|
||||
479
controller/cache/cluster.go
vendored
Normal file
@@ -0,0 +1,479 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/argo-cd/controller/metrics"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
|
||||
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
|
||||
watchResourcesRetryTimeout = 1 * time.Second
|
||||
)
|
||||
|
||||
type apiMeta struct {
|
||||
namespaced bool
|
||||
resourceVersion string
|
||||
watchCancel context.CancelFunc
|
||||
}
|
||||
|
||||
type clusterInfo struct {
|
||||
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
|
||||
|
||||
onAppUpdated AppUpdatedHandler
|
||||
kubectl kube.Kubectl
|
||||
cluster *appv1.Cluster
|
||||
log *log.Entry
|
||||
settings *settings.ArgoCDSettings
|
||||
}
|
||||
|
||||
func (c *clusterInfo) replaceResourceCache(gk schema.GroupKind, resourceVersion string, objs []unstructured.Unstructured) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
info, ok := c.apisMeta[gk]
|
||||
if ok {
|
||||
objByKind := make(map[kube.ResourceKey]*unstructured.Unstructured)
|
||||
for i := range objs {
|
||||
objByKind[kube.GetResourceKey(&objs[i])] = &objs[i]
|
||||
}
|
||||
|
||||
for i := range objs {
|
||||
obj := &objs[i]
|
||||
key := kube.GetResourceKey(&objs[i])
|
||||
existingNode, exists := c.nodes[key]
|
||||
c.onNodeUpdated(exists, existingNode, obj, key)
|
||||
}
|
||||
|
||||
for key, existingNode := range c.nodes {
|
||||
if key.Kind != gk.Kind || key.Group != gk.Group {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := objByKind[key]; !ok {
|
||||
c.onNodeRemoved(key, existingNode)
|
||||
}
|
||||
}
|
||||
info.resourceVersion = resourceVersion
|
||||
}
|
||||
}
|
||||
|
||||
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: "",
|
||||
})
|
||||
}
|
||||
nodeInfo := &node{
|
||||
resourceVersion: un.GetResourceVersion(),
|
||||
ref: kube.GetObjectRef(un),
|
||||
ownerRefs: ownerRefs,
|
||||
}
|
||||
populateNodeInfo(un, nodeInfo)
|
||||
appName := kube.GetAppInstanceLabel(un, appInstanceLabel)
|
||||
if len(ownerRefs) == 0 && appName != "" {
|
||||
nodeInfo.appName = appName
|
||||
nodeInfo.resource = un
|
||||
}
|
||||
nodeInfo.health, _ = health.GetResourceHealth(un, c.settings.ResourceOverrides)
|
||||
return nodeInfo
|
||||
}
|
||||
|
||||
func (c *clusterInfo) setNode(n *node) {
|
||||
key := n.resourceKey()
|
||||
c.nodes[key] = n
|
||||
ns, ok := c.nsIndex[key.Namespace]
|
||||
if !ok {
|
||||
ns = make(map[kube.ResourceKey]*node)
|
||||
c.nsIndex[key.Namespace] = ns
|
||||
}
|
||||
ns[key] = n
|
||||
}
|
||||
|
||||
func (c *clusterInfo) removeNode(key kube.ResourceKey) {
|
||||
delete(c.nodes, key)
|
||||
if ns, ok := c.nsIndex[key.Namespace]; ok {
|
||||
delete(ns, key)
|
||||
if len(ns) == 0 {
|
||||
delete(c.nsIndex, key.Namespace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
if c.syncTime == nil {
|
||||
return false
|
||||
}
|
||||
if c.syncError != nil {
|
||||
return time.Now().Before(c.syncTime.Add(clusterRetryTimeout))
|
||||
}
|
||||
return time.Now().Before(c.syncTime.Add(clusterSyncTimeout))
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// startMissingWatches lists supported cluster resources and start watching for changes unless watch is already running
|
||||
func (c *clusterInfo) startMissingWatches() error {
|
||||
|
||||
apis, err := c.kubectl.GetAPIResources(c.cluster.RESTConfig(), c.settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
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()
|
||||
err = c.processEvent(event.Type, obj)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to process event %s %s/%s/%s: %v", event.Type, obj.GroupVersionKind(), obj.GetNamespace(), obj.GetName(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}, 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.settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lock := sync.Mutex{}
|
||||
err = util.RunAllAsync(len(apis), func(i int) error {
|
||||
api := apis[i]
|
||||
list, err := api.Interface.List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lock.Lock()
|
||||
for i := range list.Items {
|
||||
c.setNode(c.createObjInfo(&list.Items[i], c.settings.GetAppInstanceLabelKey()))
|
||||
}
|
||||
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")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *clusterInfo) ensureSynced() error {
|
||||
c.syncLock.Lock()
|
||||
defer c.syncLock.Unlock()
|
||||
if c.synced() {
|
||||
return c.syncError
|
||||
}
|
||||
|
||||
err := c.sync()
|
||||
syncTime := time.Now()
|
||||
c.syncTime = &syncTime
|
||||
c.syncError = err
|
||||
return c.syncError
|
||||
}
|
||||
|
||||
func (c *clusterInfo) iterateHierarchy(key kube.ResourceKey, action func(child appv1.ResourceNode)) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if objInfo, ok := c.nodes[key]; ok {
|
||||
action(objInfo.asResourceNode())
|
||||
nsNodes := c.nsIndex[key.Namespace]
|
||||
for _, child := range nsNodes {
|
||||
if objInfo.isParentOf(child) {
|
||||
action(child.asResourceNode())
|
||||
child.iterateChildren(nsNodes, map[kube.ResourceKey]bool{objInfo.resourceKey(): true}, action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *clusterInfo) isNamespaced(obj *unstructured.Unstructured) bool {
|
||||
if api, ok := c.apisMeta[kube.GetResourceKey(obj).GroupKind()]; ok && !api.namespaced {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
managedObjs := make(map[kube.ResourceKey]*unstructured.Unstructured)
|
||||
// iterate all objects in live state cache to find ones associated with app
|
||||
for key, o := range c.nodes {
|
||||
if o.appName == a.Name && o.resource != nil && len(o.ownerRefs) == 0 {
|
||||
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{}
|
||||
err := util.RunAllAsync(len(targetObjs), func(i int) error {
|
||||
targetObj := targetObjs[i]
|
||||
key := GetTargetObjKey(a, targetObj, c.isNamespaced(targetObj))
|
||||
lock.Lock()
|
||||
managedObj := managedObjs[key]
|
||||
lock.Unlock()
|
||||
|
||||
if managedObj == nil {
|
||||
if existingObj, exists := c.nodes[key]; exists {
|
||||
if existingObj.resource != nil {
|
||||
managedObj = existingObj.resource
|
||||
} else {
|
||||
var err error
|
||||
managedObj, err = c.kubectl.GetResource(config, targetObj.GroupVersionKind(), existingObj.ref.Name, existingObj.ref.Namespace)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if managedObj != nil {
|
||||
converted, err := c.kubectl.ConvertToVersion(managedObj, targetObj.GroupVersionKind().Group, targetObj.GroupVersionKind().Version)
|
||||
if err != nil {
|
||||
// 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
|
||||
lock.Unlock()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return managedObjs, nil
|
||||
}
|
||||
|
||||
func (c *clusterInfo) processEvent(event watch.EventType, un *unstructured.Unstructured) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
key := kube.GetResourceKey(un)
|
||||
existingNode, exists := c.nodes[key]
|
||||
if event == watch.Deleted {
|
||||
if exists {
|
||||
c.onNodeRemoved(key, existingNode)
|
||||
}
|
||||
} else if event != watch.Deleted {
|
||||
c.onNodeUpdated(exists, existingNode, un, key)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *clusterInfo) onNodeUpdated(exists bool, existingNode *node, un *unstructured.Unstructured, key kube.ResourceKey) {
|
||||
nodes := make([]*node, 0)
|
||||
if exists {
|
||||
nodes = append(nodes, existingNode)
|
||||
}
|
||||
newObj := c.createObjInfo(un, c.settings.GetAppInstanceLabelKey())
|
||||
c.setNode(newObj)
|
||||
nodes = append(nodes, newObj)
|
||||
toNotify := make(map[string]bool)
|
||||
for i := range nodes {
|
||||
n := nodes[i]
|
||||
if ns, ok := c.nsIndex[n.ref.Namespace]; ok {
|
||||
app := n.getApp(ns)
|
||||
if app == "" || skipAppRequeing(key) {
|
||||
continue
|
||||
}
|
||||
toNotify[app] = n.isRootAppNode() || toNotify[app]
|
||||
}
|
||||
}
|
||||
for name, full := range toNotify {
|
||||
c.onAppUpdated(name, full, newObj.ref)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if appName != "" {
|
||||
c.onAppUpdated(appName, n.isRootAppNode(), n.ref)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ignoredRefreshResources = map[string]bool{
|
||||
"/" + kube.EndpointsKind: true,
|
||||
}
|
||||
)
|
||||
|
||||
// skipAppRequeing checks if the object is an API type which we want to skip requeuing against.
|
||||
// We ignore API types which have a high churn rate, and/or whose updates are irrelevant to the app
|
||||
func skipAppRequeing(key kube.ResourceKey) bool {
|
||||
return ignoredRefreshResources[key.Group+"/"+key.Kind]
|
||||
}
|
||||
421
controller/cache/cluster_test.go
vendored
Normal file
@@ -0,0 +1,421 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
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/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 {
|
||||
obj := make(map[string]interface{})
|
||||
err := yaml.Unmarshal([]byte(jsonStr), &obj)
|
||||
errors.CheckError(err)
|
||||
return &unstructured.Unstructured{Object: obj}
|
||||
}
|
||||
|
||||
func mustToUnstructured(obj interface{}) *unstructured.Unstructured {
|
||||
un, err := kube.ToUnstructured(obj)
|
||||
errors.CheckError(err)
|
||||
return un
|
||||
}
|
||||
|
||||
var (
|
||||
testPod = strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: helm-guestbook-pod
|
||||
namespace: default
|
||||
ownerReferences:
|
||||
- apiVersion: extensions/v1beta1
|
||||
kind: ReplicaSet
|
||||
name: helm-guestbook-rs
|
||||
resourceVersion: "123"`)
|
||||
|
||||
testRS = strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: ReplicaSet
|
||||
metadata:
|
||||
name: helm-guestbook-rs
|
||||
namespace: default
|
||||
ownerReferences:
|
||||
- apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
name: helm-guestbook
|
||||
resourceVersion: "123"`)
|
||||
|
||||
testDeploy = strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: helm-guestbook
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
resourceVersion: "123"`)
|
||||
|
||||
testService = strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
resourceVersion: "123"
|
||||
spec:
|
||||
selector:
|
||||
app: guestbook
|
||||
type: LoadBalancer
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
- hostname: localhost`)
|
||||
|
||||
testIngress = strToUnstructured(`
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
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 {
|
||||
runtimeObjs := make([]runtime.Object, len(objs))
|
||||
for i := range objs {
|
||||
runtimeObjs[i] = objs[i]
|
||||
}
|
||||
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, fullRefresh 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"),
|
||||
settings: &settings.ArgoCDSettings{},
|
||||
}
|
||||
}
|
||||
|
||||
func getChildren(cluster *clusterInfo, un *unstructured.Unstructured) []appv1.ResourceNode {
|
||||
hierarchy := make([]appv1.ResourceNode, 0)
|
||||
cluster.iterateHierarchy(kube.GetResourceKey(un), func(child appv1.ResourceNode) {
|
||||
hierarchy = append(hierarchy, child)
|
||||
})
|
||||
return hierarchy[1:]
|
||||
}
|
||||
|
||||
func TestGetChildren(t *testing.T) {
|
||||
cluster := newCluster(testPod, testRS, testDeploy)
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
rsChildren := getChildren(cluster, testRS)
|
||||
assert.Equal(t, []appv1.ResourceNode{{
|
||||
ResourceRef: appv1.ResourceRef{
|
||||
Kind: "Pod",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-pod",
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
},
|
||||
ParentRefs: []appv1.ResourceRef{{
|
||||
Group: "apps",
|
||||
Version: "",
|
||||
Kind: "ReplicaSet",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-rs",
|
||||
}},
|
||||
Health: &appv1.HealthStatus{Status: appv1.HealthStatusUnknown},
|
||||
NetworkingInfo: &appv1.ResourceNetworkingInfo{Labels: testPod.GetLabels()},
|
||||
ResourceVersion: "123",
|
||||
Info: []appv1.InfoItem{{Name: "Containers", Value: "0/0"}},
|
||||
}}, rsChildren)
|
||||
deployChildren := getChildren(cluster, testDeploy)
|
||||
|
||||
assert.Equal(t, append([]appv1.ResourceNode{{
|
||||
ResourceRef: appv1.ResourceRef{
|
||||
Kind: "ReplicaSet",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-rs",
|
||||
Group: "apps",
|
||||
Version: "v1",
|
||||
},
|
||||
ResourceVersion: "123",
|
||||
Health: &appv1.HealthStatus{Status: appv1.HealthStatusHealthy},
|
||||
Info: []appv1.InfoItem{},
|
||||
ParentRefs: []appv1.ResourceRef{{Group: "apps", Version: "", Kind: "Deployment", Namespace: "default", Name: "helm-guestbook"}},
|
||||
}}, rsChildren...), deployChildren)
|
||||
}
|
||||
|
||||
func TestGetManagedLiveObjs(t *testing.T) {
|
||||
cluster := newCluster(testPod, testRS, testDeploy)
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
targetDeploy := strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
labels:
|
||||
app: helm-guestbook`)
|
||||
|
||||
managedObjs, err := cluster.getManagedLiveObjs(&appv1.Application{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "helm-guestbook"},
|
||||
Spec: appv1.ApplicationSpec{
|
||||
Destination: appv1.ApplicationDestination{
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
}, []*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,
|
||||
})
|
||||
}
|
||||
|
||||
func TestChildDeletedEvent(t *testing.T) {
|
||||
cluster := newCluster(testPod, testRS, testDeploy)
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = cluster.processEvent(watch.Deleted, testPod)
|
||||
assert.Nil(t, err)
|
||||
|
||||
rsChildren := getChildren(cluster, testRS)
|
||||
assert.Equal(t, []appv1.ResourceNode{}, rsChildren)
|
||||
}
|
||||
|
||||
func TestProcessNewChildEvent(t *testing.T) {
|
||||
cluster := newCluster(testPod, testRS, testDeploy)
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
newPod := strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: helm-guestbook-pod2
|
||||
namespace: default
|
||||
ownerReferences:
|
||||
- apiVersion: extensions/v1beta1
|
||||
kind: ReplicaSet
|
||||
name: helm-guestbook-rs
|
||||
resourceVersion: "123"`)
|
||||
|
||||
err = cluster.processEvent(watch.Added, newPod)
|
||||
assert.Nil(t, err)
|
||||
|
||||
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{{
|
||||
ResourceRef: appv1.ResourceRef{
|
||||
Kind: "Pod",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-pod",
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
},
|
||||
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",
|
||||
}},
|
||||
ResourceVersion: "123",
|
||||
}, {
|
||||
ResourceRef: appv1.ResourceRef{
|
||||
Kind: "Pod",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-pod2",
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
},
|
||||
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",
|
||||
}},
|
||||
ResourceVersion: "123",
|
||||
}}, rsChildren)
|
||||
}
|
||||
|
||||
func TestUpdateResourceTags(t *testing.T) {
|
||||
pod := &corev1.Pod{
|
||||
TypeMeta: metav1.TypeMeta{Kind: "Pod", APIVersion: "v1"},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "testPod", Namespace: "default"},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "test",
|
||||
Image: "test",
|
||||
}},
|
||||
},
|
||||
}
|
||||
cluster := newCluster(mustToUnstructured(pod))
|
||||
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
podNode := cluster.nodes[kube.GetResourceKey(mustToUnstructured(pod))]
|
||||
|
||||
assert.NotNil(t, podNode)
|
||||
assert.Equal(t, []appv1.InfoItem{{Name: "Containers", Value: "0/1"}}, podNode.info)
|
||||
|
||||
pod.Status = corev1.PodStatus{
|
||||
ContainerStatuses: []corev1.ContainerStatus{{
|
||||
State: corev1.ContainerState{
|
||||
Terminated: &corev1.ContainerStateTerminated{
|
||||
ExitCode: -1,
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
err = cluster.processEvent(watch.Modified, mustToUnstructured(pod))
|
||||
assert.Nil(t, err)
|
||||
|
||||
podNode = cluster.nodes[kube.GetResourceKey(mustToUnstructured(pod))]
|
||||
|
||||
assert.NotNil(t, podNode)
|
||||
assert.Equal(t, []appv1.InfoItem{{Name: "Status Reason", Value: "ExitCode:-1"}, {Name: "Containers", Value: "0/1"}}, podNode.info)
|
||||
}
|
||||
|
||||
func TestUpdateAppResource(t *testing.T) {
|
||||
updatesReceived := make([]string, 0)
|
||||
cluster := newCluster(testPod, testRS, testDeploy)
|
||||
cluster.onAppUpdated = func(appName string, fullRefresh bool, _ corev1.ObjectReference) {
|
||||
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)
|
||||
|
||||
assert.Contains(t, updatesReceived, "helm-guestbook: false")
|
||||
}
|
||||
|
||||
func TestCircularReference(t *testing.T) {
|
||||
dep := testDeploy.DeepCopy()
|
||||
dep.SetOwnerReferences([]metav1.OwnerReference{{
|
||||
Name: testPod.GetName(),
|
||||
Kind: testPod.GetKind(),
|
||||
APIVersion: testPod.GetAPIVersion(),
|
||||
}})
|
||||
cluster := newCluster(testPod, testRS, dep)
|
||||
err := cluster.ensureSynced()
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
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) {
|
||||
removed := testPod.DeepCopy()
|
||||
removed.SetName(testPod.GetName() + "-removed-pod")
|
||||
|
||||
updated := testPod.DeepCopy()
|
||||
updated.SetName(testPod.GetName() + "-updated-pod")
|
||||
updated.SetResourceVersion("updated-pod-version")
|
||||
|
||||
cluster := newCluster(removed, updated)
|
||||
err := cluster.ensureSynced()
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
added := testPod.DeepCopy()
|
||||
added.SetName(testPod.GetName() + "-new-pod")
|
||||
|
||||
podGroupKind := testPod.GroupVersionKind().GroupKind()
|
||||
|
||||
cluster.replaceResourceCache(podGroupKind, "updated-list-version", []unstructured.Unstructured{*updated, *added})
|
||||
|
||||
_, ok := cluster.nodes[kube.GetResourceKey(removed)]
|
||||
assert.False(t, ok)
|
||||
|
||||
updatedNode, ok := cluster.nodes[kube.GetResourceKey(updated)]
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, updatedNode.resourceVersion, "updated-pod-version")
|
||||
|
||||
_, ok = cluster.nodes[kube.GetResourceKey(added)]
|
||||
assert.True(t, ok)
|
||||
}
|
||||
246
controller/cache/info.go
vendored
Normal file
@@ -0,0 +1,246 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
func populateNodeInfo(un *unstructured.Unstructured, node *node) {
|
||||
|
||||
gvk := un.GroupVersionKind()
|
||||
switch gvk.Group {
|
||||
case "":
|
||||
switch gvk.Kind {
|
||||
case kube.PodKind:
|
||||
populatePodInfo(un, node)
|
||||
return
|
||||
case kube.ServiceKind:
|
||||
populateServiceInfo(un, node)
|
||||
return
|
||||
}
|
||||
case "extensions":
|
||||
switch gvk.Kind {
|
||||
case kube.IngressKind:
|
||||
populateIngressInfo(un, node)
|
||||
return
|
||||
}
|
||||
}
|
||||
node.info = []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)
|
||||
}
|
||||
|
||||
switch stringPort {
|
||||
case "80", "http":
|
||||
urlsSet[fmt.Sprintf("http://%s", host)] = true
|
||||
case "443", "https":
|
||||
urlsSet[fmt.Sprintf("https://%s", host)] = true
|
||||
default:
|
||||
urlsSet[fmt.Sprintf("http://%s:%s", host, stringPort)] = 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 {
|
||||
node.info = []v1alpha1.InfoItem{}
|
||||
return
|
||||
}
|
||||
restarts := 0
|
||||
totalContainers := len(pod.Spec.Containers)
|
||||
readyContainers := 0
|
||||
|
||||
reason := string(pod.Status.Phase)
|
||||
if pod.Status.Reason != "" {
|
||||
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]
|
||||
restarts += int(container.RestartCount)
|
||||
switch {
|
||||
case container.State.Terminated != nil && container.State.Terminated.ExitCode == 0:
|
||||
continue
|
||||
case container.State.Terminated != nil:
|
||||
// initialization is failed
|
||||
if len(container.State.Terminated.Reason) == 0 {
|
||||
if container.State.Terminated.Signal != 0 {
|
||||
reason = fmt.Sprintf("Init:Signal:%d", container.State.Terminated.Signal)
|
||||
} else {
|
||||
reason = fmt.Sprintf("Init:ExitCode:%d", container.State.Terminated.ExitCode)
|
||||
}
|
||||
} else {
|
||||
reason = "Init:" + container.State.Terminated.Reason
|
||||
}
|
||||
initializing = true
|
||||
case container.State.Waiting != nil && len(container.State.Waiting.Reason) > 0 && container.State.Waiting.Reason != "PodInitializing":
|
||||
reason = "Init:" + container.State.Waiting.Reason
|
||||
initializing = true
|
||||
default:
|
||||
reason = fmt.Sprintf("Init:%d/%d", i, len(pod.Spec.InitContainers))
|
||||
initializing = true
|
||||
}
|
||||
break
|
||||
}
|
||||
if !initializing {
|
||||
restarts = 0
|
||||
hasRunning := false
|
||||
for i := len(pod.Status.ContainerStatuses) - 1; i >= 0; i-- {
|
||||
container := pod.Status.ContainerStatuses[i]
|
||||
|
||||
restarts += int(container.RestartCount)
|
||||
if container.State.Waiting != nil && container.State.Waiting.Reason != "" {
|
||||
reason = container.State.Waiting.Reason
|
||||
} else if container.State.Terminated != nil && container.State.Terminated.Reason != "" {
|
||||
reason = container.State.Terminated.Reason
|
||||
} else if container.State.Terminated != nil && container.State.Terminated.Reason == "" {
|
||||
if container.State.Terminated.Signal != 0 {
|
||||
reason = fmt.Sprintf("Signal:%d", container.State.Terminated.Signal)
|
||||
} else {
|
||||
reason = fmt.Sprintf("ExitCode:%d", container.State.Terminated.ExitCode)
|
||||
}
|
||||
} else if container.Ready && container.State.Running != nil {
|
||||
hasRunning = true
|
||||
readyContainers++
|
||||
}
|
||||
}
|
||||
|
||||
// change pod status back to "Running" if there is at least one container still reporting as "Running" status
|
||||
if reason == "Completed" && hasRunning {
|
||||
reason = "Running"
|
||||
}
|
||||
}
|
||||
|
||||
if pod.DeletionTimestamp != nil && pod.Status.Reason == k8snode.NodeUnreachablePodReason {
|
||||
reason = "Unknown"
|
||||
} else if pod.DeletionTimestamp != nil {
|
||||
reason = "Terminating"
|
||||
}
|
||||
|
||||
node.info = make([]v1alpha1.InfoItem, 0)
|
||||
if reason != "" {
|
||||
node.info = append(node.info, v1alpha1.InfoItem{Name: "Status Reason", Value: reason})
|
||||
}
|
||||
node.info = append(node.info, v1alpha1.InfoItem{Name: "Containers", Value: fmt.Sprintf("%d/%d", readyContainers, totalContainers)})
|
||||
node.networkingInfo = &v1alpha1.ResourceNetworkingInfo{Labels: un.GetLabels()}
|
||||
}
|
||||
108
controller/cache/info_test.go
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
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)
|
||||
}
|
||||
96
controller/cache/mocks/LiveStateCache.go
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
// Code generated by mockery v1.0.0. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import context "context"
|
||||
import kube "github.com/argoproj/argo-cd/util/kube"
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
import unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
import v1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
var r0 map[kube.ResourceKey]*unstructured.Unstructured
|
||||
if rf, ok := ret.Get(0).(func(*v1alpha1.Application, []*unstructured.Unstructured) map[kube.ResourceKey]*unstructured.Unstructured); ok {
|
||||
r0 = rf(a, targetObjs)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[kube.ResourceKey]*unstructured.Unstructured)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(*v1alpha1.Application, []*unstructured.Unstructured) error); ok {
|
||||
r1 = rf(a, targetObjs)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Invalidate provides a mock function with given fields:
|
||||
func (_m *LiveStateCache) Invalidate() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// IsNamespaced provides a mock function with given fields: server, obj
|
||||
func (_m *LiveStateCache) IsNamespaced(server string, obj *unstructured.Unstructured) (bool, error) {
|
||||
ret := _m.Called(server, obj)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(string, *unstructured.Unstructured) bool); ok {
|
||||
r0 = rf(server, obj)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, *unstructured.Unstructured) error); ok {
|
||||
r1 = rf(server, obj)
|
||||
} 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)) error {
|
||||
ret := _m.Called(server, key, action)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, kube.ResourceKey, func(v1alpha1.ResourceNode)) error); ok {
|
||||
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) {
|
||||
_m.Called(ctx)
|
||||
}
|
||||
134
controller/cache/node.go
vendored
Normal file
@@ -0,0 +1,134 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
type node struct {
|
||||
resourceVersion string
|
||||
ref v1.ObjectReference
|
||||
ownerRefs []metav1.OwnerReference
|
||||
info []appv1.InfoItem
|
||||
appName string
|
||||
// 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 {
|
||||
return kube.NewResourceKey(n.ref.GroupVersionKind().Group, n.ref.Kind, n.ref.Namespace, n.ref.Name)
|
||||
}
|
||||
|
||||
func (n *node) isParentOf(child *node) bool {
|
||||
for _, ownerRef := range child.ownerRefs {
|
||||
ownerGvk := schema.FromAPIVersionAndKind(ownerRef.APIVersion, ownerRef.Kind)
|
||||
if kube.NewResourceKey(ownerGvk.Group, ownerRef.Kind, n.ref.Namespace, ownerRef.Name) == n.resourceKey() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func ownerRefGV(ownerRef metav1.OwnerReference) schema.GroupVersion {
|
||||
gv, err := schema.ParseGroupVersion(ownerRef.APIVersion)
|
||||
if err != nil {
|
||||
gv = schema.GroupVersion{}
|
||||
}
|
||||
return gv
|
||||
}
|
||||
|
||||
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.getAppRecursive(ns, visited)
|
||||
if app != "" {
|
||||
return app
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func newResourceKeySet(set map[kube.ResourceKey]bool, keys ...kube.ResourceKey) map[kube.ResourceKey]bool {
|
||||
newSet := make(map[kube.ResourceKey]bool)
|
||||
for k, v := range set {
|
||||
newSet[k] = v
|
||||
}
|
||||
for i := range keys {
|
||||
newSet[keys[i]] = true
|
||||
}
|
||||
return newSet
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
return appv1.ResourceNode{
|
||||
ResourceRef: appv1.ResourceRef{
|
||||
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)) {
|
||||
for childKey, child := range ns {
|
||||
if n.isParentOf(ns[childKey]) {
|
||||
if parents[childKey] {
|
||||
key := n.resourceKey()
|
||||
log.Warnf("Circular dependency detected. %s is child and parent of %s", childKey.String(), key.String())
|
||||
} else {
|
||||
action(child.asResourceNode())
|
||||
child.iterateChildren(ns, newResourceKeySet(parents, n.resourceKey()), action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
29
controller/cache/node_test.go
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
var c = &clusterInfo{settings: &settings.ArgoCDSettings{}}
|
||||
|
||||
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 TestIsParentOfSameKindDifferentGroup(t *testing.T) {
|
||||
rs := testRS.DeepCopy()
|
||||
rs.SetAPIVersion("somecrd.io/v1")
|
||||
child := c.createObjInfo(testPod, "")
|
||||
invalidParent := c.createObjInfo(rs, "")
|
||||
|
||||
assert.False(t, invalidParent.isParentOf(child))
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/server/cluster"
|
||||
"github.com/argoproj/argo-cd/util/diff"
|
||||
kubeutil "github.com/argoproj/argo-cd/util/kube"
|
||||
log "github.com/sirupsen/logrus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
// AppComparator defines methods which allow to compare application spec and actual application state.
|
||||
type AppComparator interface {
|
||||
CompareAppState(server string, namespace string, targetObjs []*unstructured.Unstructured, app *v1alpha1.Application) (*v1alpha1.ComparisonResult, error)
|
||||
}
|
||||
|
||||
// KsonnetAppComparator allows to compare application using KSonnet CLI
|
||||
type KsonnetAppComparator struct {
|
||||
clusterService cluster.ClusterServiceServer
|
||||
}
|
||||
|
||||
// CompareAppState compares application spec and real app state using KSonnet
|
||||
func (ks *KsonnetAppComparator) CompareAppState(
|
||||
server string,
|
||||
namespace string,
|
||||
targetObjs []*unstructured.Unstructured,
|
||||
app *v1alpha1.Application) (*v1alpha1.ComparisonResult, error) {
|
||||
|
||||
log.Infof("Comparing app %s state in cluster %s (namespace: %s)", app.ObjectMeta.Name, server, namespace)
|
||||
// Get the REST config for the cluster corresponding to the environment
|
||||
clst, err := ks.clusterService.Get(context.Background(), &cluster.ClusterQuery{Server: server})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Retrieve the live versions of the objects
|
||||
liveObjs, err := kubeutil.GetLiveResources(clst.RESTConfig(), targetObjs, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Do the actual comparison
|
||||
diffResults, err := diff.DiffArray(targetObjs, liveObjs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resources := make([]v1alpha1.ResourceState, len(targetObjs))
|
||||
for i := 0; i < len(targetObjs); i++ {
|
||||
resState := v1alpha1.ResourceState{}
|
||||
targetObjBytes, err := json.Marshal(targetObjs[i].Object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resState.TargetState = string(targetObjBytes)
|
||||
if liveObjs[i] == nil {
|
||||
resState.LiveState = "null"
|
||||
} else {
|
||||
liveObjBytes, err := json.Marshal(liveObjs[i].Object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resState.LiveState = string(liveObjBytes)
|
||||
}
|
||||
diffResult := diffResults.Diffs[i]
|
||||
if diffResult.Modified {
|
||||
resState.Status = v1alpha1.ComparisonStatusOutOfSync
|
||||
} else {
|
||||
resState.Status = v1alpha1.ComparisonStatusSynced
|
||||
}
|
||||
resources[i] = resState
|
||||
}
|
||||
compResult := v1alpha1.ComparisonResult{
|
||||
ComparedTo: app.Spec.Source,
|
||||
ComparedAt: metav1.Time{Time: time.Now().UTC()},
|
||||
Server: clst.Server,
|
||||
Namespace: namespace,
|
||||
Resources: resources,
|
||||
}
|
||||
if diffResults.Modified {
|
||||
compResult.Status = v1alpha1.ComparisonStatusOutOfSync
|
||||
} else {
|
||||
compResult.Status = v1alpha1.ComparisonStatusSynced
|
||||
}
|
||||
return &compResult, nil
|
||||
}
|
||||
|
||||
// NewKsonnetAppComparator creates new instance of Ksonnet app comparator
|
||||
func NewKsonnetAppComparator(clusterService cluster.ClusterServiceServer) AppComparator {
|
||||
return &KsonnetAppComparator{
|
||||
clusterService: clusterService,
|
||||
}
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
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/reposerver"
|
||||
"github.com/argoproj/argo-cd/reposerver/repository"
|
||||
apireposerver "github.com/argoproj/argo-cd/server/repository"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
argoutil "github.com/argoproj/argo-cd/util/argo"
|
||||
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/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
)
|
||||
|
||||
// ApplicationController is the controller for application resources.
|
||||
type ApplicationController struct {
|
||||
repoClientset reposerver.Clientset
|
||||
kubeClientset kubernetes.Interface
|
||||
applicationClientset appclientset.Interface
|
||||
appQueue workqueue.RateLimitingInterface
|
||||
appInformer cache.SharedIndexInformer
|
||||
appComparator AppComparator
|
||||
statusRefreshTimeout time.Duration
|
||||
apiRepoService apireposerver.RepositoryServiceServer
|
||||
}
|
||||
|
||||
type ApplicationControllerConfig struct {
|
||||
InstanceID string
|
||||
Namespace string
|
||||
}
|
||||
|
||||
// NewApplicationController creates new instance of ApplicationController.
|
||||
func NewApplicationController(
|
||||
kubeClientset kubernetes.Interface,
|
||||
applicationClientset appclientset.Interface,
|
||||
repoClientset reposerver.Clientset,
|
||||
apiRepoService apireposerver.RepositoryServiceServer,
|
||||
appComparator AppComparator,
|
||||
appResyncPeriod time.Duration,
|
||||
config *ApplicationControllerConfig,
|
||||
) *ApplicationController {
|
||||
appQueue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())
|
||||
return &ApplicationController{
|
||||
kubeClientset: kubeClientset,
|
||||
applicationClientset: applicationClientset,
|
||||
repoClientset: repoClientset,
|
||||
appQueue: appQueue,
|
||||
apiRepoService: apiRepoService,
|
||||
appComparator: appComparator,
|
||||
appInformer: newApplicationInformer(applicationClientset, appQueue, appResyncPeriod, config),
|
||||
statusRefreshTimeout: appResyncPeriod,
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts the Application CRD controller.
|
||||
func (ctrl *ApplicationController) Run(ctx context.Context, appWorkers int) {
|
||||
defer runtime.HandleCrash()
|
||||
defer ctrl.appQueue.ShutDown()
|
||||
|
||||
go ctrl.appInformer.Run(ctx.Done())
|
||||
|
||||
if !cache.WaitForCacheSync(ctx.Done(), ctrl.appInformer.HasSynced) {
|
||||
log.Error("Timed out waiting for caches to sync")
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < appWorkers; i++ {
|
||||
go wait.Until(ctrl.runWorker, time.Second, ctx.Done())
|
||||
}
|
||||
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) processNextItem() bool {
|
||||
appKey, shutdown := ctrl.appQueue.Get()
|
||||
if shutdown {
|
||||
return false
|
||||
}
|
||||
|
||||
defer ctrl.appQueue.Done(appKey)
|
||||
|
||||
obj, exists, err := ctrl.appInformer.GetIndexer().GetByKey(appKey.(string))
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get application '%s' from informer index: %+v", appKey, err)
|
||||
return true
|
||||
}
|
||||
if !exists {
|
||||
// This happens after app was deleted, but the work queue still had an entry for it.
|
||||
return true
|
||||
}
|
||||
app, ok := obj.(*appv1.Application)
|
||||
if !ok {
|
||||
log.Warnf("Key '%s' in index is not an application", appKey)
|
||||
return true
|
||||
}
|
||||
|
||||
if app.NeedRefreshAppStatus(ctrl.statusRefreshTimeout) {
|
||||
updatedApp := app.DeepCopy()
|
||||
status, err := ctrl.tryRefreshAppStatus(updatedApp)
|
||||
if err != nil {
|
||||
updatedApp.Status.ComparisonResult = appv1.ComparisonResult{
|
||||
Status: appv1.ComparisonStatusError,
|
||||
Error: fmt.Sprintf("Failed to get application status for application '%s': %v", app.Name, err),
|
||||
ComparedTo: app.Spec.Source,
|
||||
ComparedAt: metav1.Time{Time: time.Now().UTC()},
|
||||
}
|
||||
} else {
|
||||
updatedApp.Status = *status
|
||||
}
|
||||
ctrl.persistApp(updatedApp)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) tryRefreshAppStatus(app *appv1.Application) (*appv1.ApplicationStatus, error) {
|
||||
conn, client, err := ctrl.repoClientset.NewRepositoryClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer util.Close(conn)
|
||||
repo, err := ctrl.apiRepoService.Get(context.Background(), &apireposerver.RepoQuery{Repo: app.Spec.Source.RepoURL})
|
||||
if err != nil {
|
||||
// If we couldn't retrieve from the repo service, assume public repositories
|
||||
repo = &appv1.Repository{
|
||||
Repo: app.Spec.Source.RepoURL,
|
||||
Username: "",
|
||||
Password: "",
|
||||
}
|
||||
}
|
||||
overrides := make([]*appv1.ComponentParameter, len(app.Spec.Source.ComponentParameterOverrides))
|
||||
if app.Spec.Source.ComponentParameterOverrides != nil {
|
||||
for i := range app.Spec.Source.ComponentParameterOverrides {
|
||||
item := app.Spec.Source.ComponentParameterOverrides[i]
|
||||
overrides[i] = &item
|
||||
}
|
||||
}
|
||||
revision := app.Spec.Source.TargetRevision
|
||||
manifestInfo, err := client.GenerateManifest(context.Background(), &repository.ManifestRequest{
|
||||
Repo: repo,
|
||||
Revision: revision,
|
||||
Path: app.Spec.Source.Path,
|
||||
Environment: app.Spec.Source.Environment,
|
||||
AppLabel: app.Name,
|
||||
ComponentParameterOverrides: overrides,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("Failed to load application manifest %v", err)
|
||||
return nil, err
|
||||
}
|
||||
targetObjs := make([]*unstructured.Unstructured, len(manifestInfo.Manifests))
|
||||
for i, manifestStr := range manifestInfo.Manifests {
|
||||
var obj unstructured.Unstructured
|
||||
if err := json.Unmarshal([]byte(manifestStr), &obj); err != nil {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
targetObjs[i] = &obj
|
||||
}
|
||||
|
||||
server, namespace := argoutil.ResolveServerNamespace(app.Spec.Destination, manifestInfo)
|
||||
comparisonResult, err := ctrl.appComparator.CompareAppState(server, namespace, targetObjs, app)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Infof("App %s comparison result: prev: %s. current: %s", app.Name, app.Status.ComparisonResult.Status, comparisonResult.Status)
|
||||
newStatus := app.Status
|
||||
newStatus.ComparisonResult = *comparisonResult
|
||||
return &newStatus, nil
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) runWorker() {
|
||||
for ctrl.processNextItem() {
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) persistApp(app *appv1.Application) {
|
||||
appClient := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.ObjectMeta.Namespace)
|
||||
_, err := appClient.Update(app)
|
||||
if err != nil {
|
||||
log.Warnf("Error updating application: %v", err)
|
||||
}
|
||||
log.Info("Application update successful")
|
||||
}
|
||||
|
||||
func newApplicationInformer(
|
||||
appClientset appclientset.Interface, appQueue workqueue.RateLimitingInterface, appResyncPeriod time.Duration, config *ApplicationControllerConfig) cache.SharedIndexInformer {
|
||||
|
||||
appInformerFactory := appinformers.NewFilteredSharedInformerFactory(
|
||||
appClientset,
|
||||
appResyncPeriod,
|
||||
config.Namespace,
|
||||
func(options *metav1.ListOptions) {
|
||||
var instanceIDReq *labels.Requirement
|
||||
var err error
|
||||
if config.InstanceID != "" {
|
||||
instanceIDReq, err = labels.NewRequirement(common.LabelKeyApplicationControllerInstanceID, selection.Equals, []string{config.InstanceID})
|
||||
} else {
|
||||
instanceIDReq, err = labels.NewRequirement(common.LabelKeyApplicationControllerInstanceID, selection.DoesNotExist, nil)
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
options.FieldSelector = fields.Everything().String()
|
||||
labelSelector := labels.NewSelector().Add(*instanceIDReq)
|
||||
options.LabelSelector = labelSelector.String()
|
||||
},
|
||||
)
|
||||
informer := appInformerFactory.Argoproj().V1alpha1().Applications().Informer()
|
||||
informer.AddEventHandler(
|
||||
cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(obj interface{}) {
|
||||
key, err := cache.MetaNamespaceKeyFunc(obj)
|
||||
if err == nil {
|
||||
appQueue.Add(key)
|
||||
}
|
||||
},
|
||||
UpdateFunc: func(old, new interface{}) {
|
||||
key, err := cache.MetaNamespaceKeyFunc(new)
|
||||
if err == nil {
|
||||
appQueue.Add(key)
|
||||
}
|
||||
},
|
||||
DeleteFunc: func(obj interface{}) {
|
||||
// IndexerInformer uses a delta queue, therefore for deletes we have to use this
|
||||
// key function.
|
||||
key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
|
||||
if err == nil {
|
||||
appQueue.Add(key)
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
return informer
|
||||
}
|
||||
198
controller/metrics/metrics.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
type MetricsServer struct {
|
||||
*http.Server
|
||||
syncCounter *prometheus.CounterVec
|
||||
k8sRequestCounter *prometheus.CounterVec
|
||||
reconcileHistogram *prometheus.HistogramVec
|
||||
}
|
||||
|
||||
const (
|
||||
// MetricsPath is the endpoint to collect application metrics
|
||||
MetricsPath = "/metrics"
|
||||
)
|
||||
|
||||
// Follow Prometheus naming practices
|
||||
// https://prometheus.io/docs/practices/naming/
|
||||
var (
|
||||
descAppDefaultLabels = []string{"namespace", "name", "project"}
|
||||
|
||||
descAppInfo = prometheus.NewDesc(
|
||||
"argocd_app_info",
|
||||
"Information about application.",
|
||||
append(descAppDefaultLabels, "repo", "dest_server", "dest_namespace"),
|
||||
nil,
|
||||
)
|
||||
descAppCreated = prometheus.NewDesc(
|
||||
"argocd_app_created_time",
|
||||
"Creation time in unix timestamp for an application.",
|
||||
descAppDefaultLabels,
|
||||
nil,
|
||||
)
|
||||
descAppSyncStatusCode = prometheus.NewDesc(
|
||||
"argocd_app_sync_status",
|
||||
"The application current sync status.",
|
||||
append(descAppDefaultLabels, "sync_status"),
|
||||
nil,
|
||||
)
|
||||
descAppHealthStatus = prometheus.NewDesc(
|
||||
"argocd_app_health_status",
|
||||
"The application current health status.",
|
||||
append(descAppDefaultLabels, "health_status"),
|
||||
nil,
|
||||
)
|
||||
)
|
||||
|
||||
// NewMetricsServer returns a new prometheus server which collects application metrics
|
||||
func NewMetricsServer(addr string, appLister applister.ApplicationLister) *MetricsServer {
|
||||
mux := http.NewServeMux()
|
||||
appRegistry := NewAppRegistry(appLister)
|
||||
appRegistry.MustRegister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}))
|
||||
appRegistry.MustRegister(prometheus.NewGoCollector())
|
||||
mux.Handle(MetricsPath, promhttp.HandlerFor(appRegistry, promhttp.HandlerOpts{}))
|
||||
|
||||
syncCounter := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "argocd_app_sync_total",
|
||||
Help: "Number of application syncs.",
|
||||
},
|
||||
append(descAppDefaultLabels, "phase"),
|
||||
)
|
||||
appRegistry.MustRegister(syncCounter)
|
||||
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{
|
||||
Name: "argocd_app_reconcile",
|
||||
Help: "Application reconciliation performance.",
|
||||
// Buckets chosen after observing a ~2100ms mean reconcile time
|
||||
Buckets: []float64{0.25, .5, 1, 2, 4, 8, 16},
|
||||
},
|
||||
append(descAppDefaultLabels),
|
||||
)
|
||||
|
||||
appRegistry.MustRegister(reconcileHistogram)
|
||||
|
||||
return &MetricsServer{
|
||||
Server: &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
},
|
||||
syncCounter: syncCounter,
|
||||
k8sRequestCounter: k8sRequestCounter,
|
||||
reconcileHistogram: reconcileHistogram,
|
||||
}
|
||||
}
|
||||
|
||||
// IncSync increments the sync counter for an application
|
||||
func (m *MetricsServer) IncSync(app *argoappv1.Application, state *argoappv1.OperationState) {
|
||||
if !state.Phase.Completed() {
|
||||
return
|
||||
}
|
||||
m.syncCounter.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject(), string(state.Phase)).Inc()
|
||||
}
|
||||
|
||||
// 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())
|
||||
}
|
||||
|
||||
type appCollector struct {
|
||||
store applister.ApplicationLister
|
||||
}
|
||||
|
||||
// NewAppCollector returns a prometheus collector for application metrics
|
||||
func NewAppCollector(appLister applister.ApplicationLister) prometheus.Collector {
|
||||
return &appCollector{
|
||||
store: appLister,
|
||||
}
|
||||
}
|
||||
|
||||
// NewAppRegistry creates a new prometheus registry that collects applications
|
||||
func NewAppRegistry(appLister applister.ApplicationLister) *prometheus.Registry {
|
||||
registry := prometheus.NewRegistry()
|
||||
registry.MustRegister(NewAppCollector(appLister))
|
||||
return registry
|
||||
}
|
||||
|
||||
// Describe implements the prometheus.Collector interface
|
||||
func (c *appCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- descAppInfo
|
||||
ch <- descAppCreated
|
||||
ch <- descAppSyncStatusCode
|
||||
ch <- descAppHealthStatus
|
||||
}
|
||||
|
||||
// Collect implements the prometheus.Collector interface
|
||||
func (c *appCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
apps, err := c.store.List(labels.NewSelector())
|
||||
if err != nil {
|
||||
log.Warnf("Failed to collect applications: %v", err)
|
||||
return
|
||||
}
|
||||
for _, app := range apps {
|
||||
collectApps(ch, app)
|
||||
}
|
||||
}
|
||||
|
||||
func boolFloat64(b bool) float64 {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func collectApps(ch chan<- prometheus.Metric, app *argoappv1.Application) {
|
||||
addConstMetric := func(desc *prometheus.Desc, t prometheus.ValueType, v float64, lv ...string) {
|
||||
project := app.Spec.GetProject()
|
||||
lv = append([]string{app.Namespace, app.Name, project}, lv...)
|
||||
ch <- prometheus.MustNewConstMetric(desc, t, v, lv...)
|
||||
}
|
||||
addGauge := func(desc *prometheus.Desc, v float64, lv ...string) {
|
||||
addConstMetric(desc, prometheus.GaugeValue, v, lv...)
|
||||
}
|
||||
|
||||
addGauge(descAppInfo, 1, git.NormalizeGitURL(app.Spec.Source.RepoURL), app.Spec.Destination.Server, app.Spec.Destination.Namespace)
|
||||
|
||||
addGauge(descAppCreated, float64(app.CreationTimestamp.Unix()))
|
||||
|
||||
syncStatus := app.Status.Sync.Status
|
||||
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeSynced), string(argoappv1.SyncStatusCodeSynced))
|
||||
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeOutOfSync), string(argoappv1.SyncStatusCodeOutOfSync))
|
||||
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeUnknown || syncStatus == ""), string(argoappv1.SyncStatusCodeUnknown))
|
||||
|
||||
healthStatus := app.Status.Health.Status
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusUnknown || healthStatus == ""), argoappv1.HealthStatusUnknown)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusProgressing), argoappv1.HealthStatusProgressing)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusSuspended), argoappv1.HealthStatusSuspended)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusHealthy), argoappv1.HealthStatusHealthy)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusDegraded), argoappv1.HealthStatusDegraded)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusMissing), argoappv1.HealthStatusMissing)
|
||||
}
|
||||
233
controller/metrics/metrics_test.go
Normal file
@@ -0,0 +1,233 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/stretchr/testify/assert"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/fake"
|
||||
appinformer "github.com/argoproj/argo-cd/pkg/client/informers/externalversions"
|
||||
applister "github.com/argoproj/argo-cd/pkg/client/listers/application/v1alpha1"
|
||||
)
|
||||
|
||||
const fakeApp = `
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: my-app
|
||||
namespace: argocd
|
||||
spec:
|
||||
destination:
|
||||
namespace: dummy-namespace
|
||||
server: https://localhost:6443
|
||||
project: important-project
|
||||
source:
|
||||
path: some/path
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps.git
|
||||
status:
|
||||
sync:
|
||||
status: Synced
|
||||
health:
|
||||
status: Healthy
|
||||
`
|
||||
|
||||
const expectedResponse = `# HELP argocd_app_created_time Creation time in unix timestamp for an application.
|
||||
# TYPE argocd_app_created_time gauge
|
||||
argocd_app_created_time{name="my-app",namespace="argocd",project="important-project"} -6.21355968e+10
|
||||
# HELP argocd_app_health_status The application current health status.
|
||||
# TYPE argocd_app_health_status gauge
|
||||
argocd_app_health_status{health_status="Degraded",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
argocd_app_health_status{health_status="Healthy",name="my-app",namespace="argocd",project="important-project"} 1
|
||||
argocd_app_health_status{health_status="Missing",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
argocd_app_health_status{health_status="Progressing",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
argocd_app_health_status{health_status="Suspended",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
argocd_app_health_status{health_status="Unknown",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
# HELP argocd_app_info Information about application.
|
||||
# TYPE argocd_app_info gauge
|
||||
argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",name="my-app",namespace="argocd",project="important-project",repo="https://github.com/argoproj/argocd-example-apps"} 1
|
||||
# HELP argocd_app_sync_status The application current sync status.
|
||||
# TYPE argocd_app_sync_status gauge
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="OutOfSync"} 0
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="Synced"} 1
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="Unknown"} 0
|
||||
`
|
||||
|
||||
const fakeDefaultApp = `
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: my-app
|
||||
namespace: argocd
|
||||
spec:
|
||||
destination:
|
||||
namespace: dummy-namespace
|
||||
server: https://localhost:6443
|
||||
source:
|
||||
path: some/path
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps.git
|
||||
status:
|
||||
sync:
|
||||
status: Synced
|
||||
health:
|
||||
status: Healthy
|
||||
`
|
||||
|
||||
const expectedDefaultResponse = `# HELP argocd_app_created_time Creation time in unix timestamp for an application.
|
||||
# TYPE argocd_app_created_time gauge
|
||||
argocd_app_created_time{name="my-app",namespace="argocd",project="default"} -6.21355968e+10
|
||||
# HELP argocd_app_health_status The application current health status.
|
||||
# TYPE argocd_app_health_status gauge
|
||||
argocd_app_health_status{health_status="Degraded",name="my-app",namespace="argocd",project="default"} 0
|
||||
argocd_app_health_status{health_status="Healthy",name="my-app",namespace="argocd",project="default"} 1
|
||||
argocd_app_health_status{health_status="Missing",name="my-app",namespace="argocd",project="default"} 0
|
||||
argocd_app_health_status{health_status="Progressing",name="my-app",namespace="argocd",project="default"} 0
|
||||
argocd_app_health_status{health_status="Suspended",name="my-app",namespace="argocd",project="default"} 0
|
||||
argocd_app_health_status{health_status="Unknown",name="my-app",namespace="argocd",project="default"} 0
|
||||
# HELP argocd_app_info Information about application.
|
||||
# TYPE argocd_app_info gauge
|
||||
argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",name="my-app",namespace="argocd",project="default",repo="https://github.com/argoproj/argocd-example-apps"} 1
|
||||
# HELP argocd_app_sync_status The application current sync status.
|
||||
# TYPE argocd_app_sync_status gauge
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="default",sync_status="OutOfSync"} 0
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="default",sync_status="Synced"} 1
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="default",sync_status="Unknown"} 0
|
||||
`
|
||||
|
||||
func newFakeApp(fakeApp string) *argoappv1.Application {
|
||||
var app argoappv1.Application
|
||||
err := yaml.Unmarshal([]byte(fakeApp), &app)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &app
|
||||
}
|
||||
|
||||
func newFakeLister(fakeApp ...string) (context.CancelFunc, applister.ApplicationLister) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
var fakeApps []runtime.Object
|
||||
for _, name := range fakeApp {
|
||||
fakeApps = append(fakeApps, newFakeApp(name))
|
||||
}
|
||||
appClientset := appclientset.NewSimpleClientset(fakeApps...)
|
||||
factory := appinformer.NewFilteredSharedInformerFactory(appClientset, 0, "argocd", func(options *metav1.ListOptions) {})
|
||||
appInformer := factory.Argoproj().V1alpha1().Applications().Informer()
|
||||
go appInformer.Run(ctx.Done())
|
||||
if !cache.WaitForCacheSync(ctx.Done(), appInformer.HasSynced) {
|
||||
log.Fatal("Timed out waiting for caches to sync")
|
||||
}
|
||||
return cancel, factory.Argoproj().V1alpha1().Applications().Lister()
|
||||
}
|
||||
|
||||
func testApp(t *testing.T, fakeApp string, expectedResponse string) {
|
||||
cancel, appLister := newFakeLister(fakeApp)
|
||||
defer cancel()
|
||||
metricsServ := NewMetricsServer("localhost:8082", appLister)
|
||||
req, err := http.NewRequest("GET", "/metrics", nil)
|
||||
assert.NoError(t, err)
|
||||
rr := httptest.NewRecorder()
|
||||
metricsServ.Handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, rr.Code, http.StatusOK)
|
||||
body := rr.Body.String()
|
||||
log.Println(body)
|
||||
assertMetricsPrinted(t, expectedResponse, body)
|
||||
}
|
||||
|
||||
type testCombination struct {
|
||||
application string
|
||||
expectedResponse string
|
||||
}
|
||||
|
||||
func TestMetrics(t *testing.T) {
|
||||
combinations := []testCombination{
|
||||
{
|
||||
application: fakeApp,
|
||||
expectedResponse: expectedResponse,
|
||||
},
|
||||
{
|
||||
application: fakeDefaultApp,
|
||||
expectedResponse: expectedDefaultResponse,
|
||||
},
|
||||
}
|
||||
|
||||
for _, combination := range combinations {
|
||||
testApp(t, combination.application, combination.expectedResponse)
|
||||
}
|
||||
}
|
||||
|
||||
const appSyncTotal = `# HELP argocd_app_sync_total Number of application syncs.
|
||||
# TYPE argocd_app_sync_total counter
|
||||
argocd_app_sync_total{name="my-app",namespace="argocd",phase="Error",project="important-project"} 1
|
||||
argocd_app_sync_total{name="my-app",namespace="argocd",phase="Failed",project="important-project"} 1
|
||||
argocd_app_sync_total{name="my-app",namespace="argocd",phase="Succeeded",project="important-project"} 2
|
||||
`
|
||||
|
||||
func TestMetricsSyncCounter(t *testing.T) {
|
||||
cancel, appLister := newFakeLister()
|
||||
defer cancel()
|
||||
metricsServ := NewMetricsServer("localhost:8082", appLister)
|
||||
|
||||
fakeApp := newFakeApp(fakeApp)
|
||||
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: argoappv1.OperationRunning})
|
||||
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: argoappv1.OperationFailed})
|
||||
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: argoappv1.OperationError})
|
||||
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: argoappv1.OperationSucceeded})
|
||||
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: argoappv1.OperationSucceeded})
|
||||
|
||||
req, err := http.NewRequest("GET", "/metrics", nil)
|
||||
assert.NoError(t, err)
|
||||
rr := httptest.NewRecorder()
|
||||
metricsServ.Handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, rr.Code, http.StatusOK)
|
||||
body := rr.Body.String()
|
||||
log.Println(body)
|
||||
assertMetricsPrinted(t, appSyncTotal, body)
|
||||
}
|
||||
|
||||
// assertMetricsPrinted asserts every line in the expected lines appears in the body
|
||||
func assertMetricsPrinted(t *testing.T, expectedLines, body string) {
|
||||
for _, line := range strings.Split(expectedLines, "\n") {
|
||||
assert.Contains(t, body, line)
|
||||
}
|
||||
}
|
||||
|
||||
const appReconcileMetrics = `argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="0.25"} 0
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="0.5"} 0
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="1"} 0
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="2"} 0
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="4"} 0
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="8"} 1
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="16"} 1
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="+Inf"} 1
|
||||
argocd_app_reconcile_sum{name="my-app",namespace="argocd",project="important-project"} 5
|
||||
argocd_app_reconcile_count{name="my-app",namespace="argocd",project="important-project"} 1
|
||||
`
|
||||
|
||||
func TestReconcileMetrics(t *testing.T) {
|
||||
cancel, appLister := newFakeLister()
|
||||
defer cancel()
|
||||
metricsServ := NewMetricsServer("localhost:8082", appLister)
|
||||
|
||||
fakeApp := newFakeApp(fakeApp)
|
||||
metricsServ.IncReconcile(fakeApp, 5*time.Second)
|
||||
|
||||
req, err := http.NewRequest("GET", "/metrics", nil)
|
||||
assert.NoError(t, err)
|
||||
rr := httptest.NewRecorder()
|
||||
metricsServ.Handler.ServeHTTP(rr, req)
|
||||
assert.Equal(t, rr.Code, http.StatusOK)
|
||||
body := rr.Body.String()
|
||||
log.Println(body)
|
||||
assertMetricsPrinted(t, appReconcileMetrics, body)
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// Code generated by mockery v1.0.0
|
||||
package mocks
|
||||
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
import v1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
|
||||
// AppComparator is an autogenerated mock type for the AppComparator type
|
||||
type AppComparator struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// CompareAppState provides a mock function with given fields: appRepoPath, app
|
||||
func (_m *AppComparator) CompareAppState(appRepoPath string, app *v1alpha1.Application) (*v1alpha1.ComparisonResult, error) {
|
||||
ret := _m.Called(appRepoPath, app)
|
||||
|
||||
var r0 *v1alpha1.ComparisonResult
|
||||
if rf, ok := ret.Get(0).(func(string, *v1alpha1.Application) *v1alpha1.ComparisonResult); ok {
|
||||
r0 = rf(appRepoPath, app)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*v1alpha1.ComparisonResult)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, *v1alpha1.Application) error); ok {
|
||||
r1 = rf(appRepoPath, app)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
394
controller/state.go
Normal file
@@ -0,0 +1,394 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
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/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/util"
|
||||
"github.com/argoproj/argo-cd/util/argo"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/diff"
|
||||
"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/settings"
|
||||
)
|
||||
|
||||
type managedResource struct {
|
||||
Target *unstructured.Unstructured
|
||||
Live *unstructured.Unstructured
|
||||
Diff diff.DiffResult
|
||||
Group string
|
||||
Version string
|
||||
Kind string
|
||||
Namespace string
|
||||
Name string
|
||||
Hook bool
|
||||
}
|
||||
|
||||
func GetLiveObjs(res []managedResource) []*unstructured.Unstructured {
|
||||
objs := make([]*unstructured.Unstructured, len(res))
|
||||
for i := range res {
|
||||
objs[i] = res[i].Live
|
||||
}
|
||||
return objs
|
||||
}
|
||||
|
||||
type ResourceInfoProvider interface {
|
||||
IsNamespaced(server string, obj *unstructured.Unstructured) (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)
|
||||
SyncAppState(app *v1alpha1.Application, state *v1alpha1.OperationState)
|
||||
}
|
||||
|
||||
type comparisonResult struct {
|
||||
reconciledAt metav1.Time
|
||||
syncStatus *v1alpha1.SyncStatus
|
||||
healthStatus *v1alpha1.HealthStatus
|
||||
resources []v1alpha1.ResourceStatus
|
||||
managedResources []managedResource
|
||||
conditions []v1alpha1.ApplicationCondition
|
||||
hooks []*unstructured.Unstructured
|
||||
diffNormalizer diff.Normalizer
|
||||
appSourceType v1alpha1.ApplicationSourceType
|
||||
}
|
||||
|
||||
// appStateManager allows to compare applications to git
|
||||
type appStateManager struct {
|
||||
metricsServer *metrics.MetricsServer
|
||||
db db.ArgoDB
|
||||
settings *settings.ArgoCDSettings
|
||||
appclientset appclientset.Interface
|
||||
projInformer cache.SharedIndexInformer
|
||||
kubectl kubeutil.Kubectl
|
||||
repoClientset reposerver.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())
|
||||
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
|
||||
}
|
||||
conn, repoClient, err := m.repoClientset.NewRepoServerClient()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
defer util.Close(conn)
|
||||
|
||||
if revision == "" {
|
||||
revision = source.TargetRevision
|
||||
}
|
||||
|
||||
tools := make([]*appv1.ConfigManagementPlugin, len(m.settings.ConfigManagementPlugins))
|
||||
for i := range m.settings.ConfigManagementPlugins {
|
||||
tools[i] = &m.settings.ConfigManagementPlugins[i]
|
||||
}
|
||||
|
||||
manifestInfo, err := repoClient.GenerateManifest(context.Background(), &repository.ManifestRequest{
|
||||
Repo: repo,
|
||||
HelmRepos: helmRepos,
|
||||
Revision: revision,
|
||||
NoCache: noCache,
|
||||
AppLabelKey: appLabelKey,
|
||||
AppLabelValue: app.Name,
|
||||
Namespace: app.Spec.Destination.Namespace,
|
||||
ApplicationSource: &source,
|
||||
Plugins: tools,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
targetObjs := make([]*unstructured.Unstructured, 0)
|
||||
hooks := make([]*unstructured.Unstructured, 0)
|
||||
for _, manifest := range manifestInfo.Manifests {
|
||||
obj, err := v1alpha1.UnmarshalToUnstructured(manifest)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
if hookutil.IsHook(obj) {
|
||||
hooks = append(hooks, obj)
|
||||
} else {
|
||||
targetObjs = append(targetObjs, obj)
|
||||
}
|
||||
}
|
||||
return targetObjs, hooks, manifestInfo, 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)
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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()
|
||||
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
|
||||
}
|
||||
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...)
|
||||
|
||||
logCtx.Debugf("Generated config manifests")
|
||||
liveObjByKey, err := m.liveStateCache.GetManagedLiveObjs(app, targetObjs)
|
||||
if err != nil {
|
||||
liveObjByKey = make(map[kubeutil.ResourceKey]*unstructured.Unstructured)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
failedToLoadObjs = true
|
||||
}
|
||||
logCtx.Debugf("Retrieved lived manifests")
|
||||
for _, liveObj := range liveObjByKey {
|
||||
if liveObj != nil {
|
||||
appInstanceName := kubeutil.GetAppInstanceLabel(liveObj, appLabelKey)
|
||||
if appInstanceName != "" && appInstanceName != app.Name {
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{
|
||||
Type: v1alpha1.ApplicationConditionSharedResourceWarning,
|
||||
Message: fmt.Sprintf("%s/%s is part of a different application: %s", liveObj.GetKind(), liveObj.GetName(), appInstanceName),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
managedLiveObj := make([]*unstructured.Unstructured, len(targetObjs))
|
||||
for i, obj := range targetObjs {
|
||||
gvk := obj.GroupVersionKind()
|
||||
ns := util.FirstNonEmpty(obj.GetNamespace(), app.Spec.Destination.Namespace)
|
||||
if namespaced, err := m.liveStateCache.IsNamespaced(app.Spec.Destination.Server, obj); err == nil && !namespaced {
|
||||
ns = ""
|
||||
}
|
||||
key := kubeutil.NewResourceKey(gvk.Group, gvk.Kind, ns, obj.GetName())
|
||||
if liveObj, ok := liveObjByKey[key]; ok {
|
||||
managedLiveObj[i] = liveObj
|
||||
delete(liveObjByKey, key)
|
||||
} else {
|
||||
managedLiveObj[i] = nil
|
||||
}
|
||||
}
|
||||
logCtx.Debugf("built managed objects list")
|
||||
// Everything remaining in liveObjByKey are "extra" resources that aren't tracked in git.
|
||||
// The following adds all the extras to the managedLiveObj list and backfills the targetObj
|
||||
// list with nils, so that the lists are of equal lengths for comparison purposes.
|
||||
for _, obj := range liveObjByKey {
|
||||
targetObjs = append(targetObjs, nil)
|
||||
managedLiveObj = append(managedLiveObj, obj)
|
||||
}
|
||||
|
||||
// Do the actual comparison
|
||||
diffResults, err := diff.DiffArray(targetObjs, managedLiveObj, diffNormalizer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
syncCode := v1alpha1.SyncStatusCodeSynced
|
||||
managedResources := make([]managedResource, len(targetObjs))
|
||||
resourceSummaries := make([]v1alpha1.ResourceStatus, len(targetObjs))
|
||||
for i := 0; i < len(targetObjs); i++ {
|
||||
obj := managedLiveObj[i]
|
||||
if obj == nil {
|
||||
obj = targetObjs[i]
|
||||
}
|
||||
if obj == nil {
|
||||
continue
|
||||
}
|
||||
gvk := obj.GroupVersionKind()
|
||||
|
||||
resState := v1alpha1.ResourceStatus{
|
||||
Namespace: obj.GetNamespace(),
|
||||
Name: obj.GetName(),
|
||||
Kind: gvk.Kind,
|
||||
Version: gvk.Version,
|
||||
Group: gvk.Group,
|
||||
Hook: hookutil.IsHook(obj),
|
||||
}
|
||||
|
||||
diffResult := diffResults.Diffs[i]
|
||||
if resState.Hook {
|
||||
// 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 {
|
||||
// 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
|
||||
} else {
|
||||
resState.Status = v1alpha1.SyncStatusCodeSynced
|
||||
}
|
||||
managedResources[i] = managedResource{
|
||||
Name: resState.Name,
|
||||
Namespace: resState.Namespace,
|
||||
Group: resState.Group,
|
||||
Kind: resState.Kind,
|
||||
Version: resState.Version,
|
||||
Live: managedLiveObj[i],
|
||||
Target: targetObjs[i],
|
||||
Diff: diffResult,
|
||||
Hook: resState.Hook,
|
||||
}
|
||||
resourceSummaries[i] = resState
|
||||
}
|
||||
|
||||
if failedToLoadObjs {
|
||||
syncCode = v1alpha1.SyncStatusCodeUnknown
|
||||
}
|
||||
syncStatus := v1alpha1.SyncStatus{
|
||||
ComparedTo: appv1.ComparedTo{
|
||||
Source: source,
|
||||
Destination: app.Spec.Destination,
|
||||
},
|
||||
Status: syncCode,
|
||||
}
|
||||
if manifestInfo != nil {
|
||||
syncStatus.Revision = manifestInfo.Revision
|
||||
}
|
||||
|
||||
healthStatus, err := health.SetApplicationHealth(resourceSummaries, GetLiveObjs(managedResources), m.settings.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{
|
||||
reconciledAt: observedAt,
|
||||
syncStatus: &syncStatus,
|
||||
healthStatus: healthStatus,
|
||||
resources: resourceSummaries,
|
||||
managedResources: managedResources,
|
||||
conditions: conditions,
|
||||
hooks: hooks,
|
||||
diffNormalizer: diffNormalizer,
|
||||
}
|
||||
if manifestInfo != nil {
|
||||
compRes.appSourceType = v1alpha1.ApplicationSourceType(manifestInfo.SourceType)
|
||||
}
|
||||
return &compRes, nil
|
||||
}
|
||||
|
||||
func (m *appStateManager) persistRevisionHistory(app *v1alpha1.Application, revision string, source v1alpha1.ApplicationSource) error {
|
||||
var nextID int64
|
||||
if len(app.Status.History) > 0 {
|
||||
nextID = app.Status.History[len(app.Status.History)-1].ID + 1
|
||||
}
|
||||
history := append(app.Status.History, v1alpha1.RevisionHistory{
|
||||
Revision: revision,
|
||||
DeployedAt: metav1.NewTime(time.Now().UTC()),
|
||||
ID: nextID,
|
||||
Source: source,
|
||||
})
|
||||
|
||||
if len(history) > common.RevisionHistoryLimit {
|
||||
history = history[1 : common.RevisionHistoryLimit+1]
|
||||
}
|
||||
|
||||
patch, err := json.Marshal(map[string]map[string][]v1alpha1.RevisionHistory{
|
||||
"status": {
|
||||
"history": history,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = m.appclientset.ArgoprojV1alpha1().Applications(m.namespace).Patch(app.Name, types.MergePatchType, patch)
|
||||
return err
|
||||
}
|
||||
|
||||
// NewAppStateManager creates new instance of Ksonnet app comparator
|
||||
func NewAppStateManager(
|
||||
db db.ArgoDB,
|
||||
appclientset appclientset.Interface,
|
||||
repoClientset reposerver.Clientset,
|
||||
namespace string,
|
||||
kubectl kubeutil.Kubectl,
|
||||
settings *settings.ArgoCDSettings,
|
||||
liveStateCache statecache.LiveStateCache,
|
||||
projInformer cache.SharedIndexInformer,
|
||||
metricsServer *metrics.MetricsServer,
|
||||
) AppStateManager {
|
||||
return &appStateManager{
|
||||
liveStateCache: liveStateCache,
|
||||
db: db,
|
||||
appclientset: appclientset,
|
||||
kubectl: kubectl,
|
||||
repoClientset: repoClientset,
|
||||
namespace: namespace,
|
||||
settings: settings,
|
||||
projInformer: projInformer,
|
||||
metricsServer: metricsServer,
|
||||
}
|
||||
}
|
||||
263
controller/state_test.go
Normal file
@@ -0,0 +1,263 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
v1 "k8s.io/api/apps/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"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/test"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
)
|
||||
|
||||
// TestCompareAppStateEmpty tests comparison when both git and live have no objects
|
||||
func TestCompareAppStateEmpty(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
|
||||
assert.NoError(t, err)
|
||||
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))
|
||||
}
|
||||
|
||||
// TestCompareAppStateMissing tests when there is a manifest defined in git which doesn't exist in live
|
||||
func TestCompareAppStateMissing(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
apps: []runtime.Object{app},
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
Manifests: []string{string(test.PodManifest)},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
|
||||
assert.NoError(t, err)
|
||||
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))
|
||||
}
|
||||
|
||||
// TestCompareAppStateExtra tests when there is an extra object in live but not defined in git
|
||||
func TestCompareAppStateExtra(t *testing.T) {
|
||||
pod := test.NewPod()
|
||||
pod.SetNamespace(test.FakeDestNamespace)
|
||||
app := newFakeApp()
|
||||
key := kube.ResourceKey{Group: "", Kind: "Pod", Namespace: test.FakeDestNamespace, Name: app.Name}
|
||||
data := fakeData{
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
||||
key: pod,
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
|
||||
assert.NoError(t, err)
|
||||
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))
|
||||
}
|
||||
|
||||
// TestCompareAppStateHook checks that hooks are detected during manifest generation, and not
|
||||
// considered as part of resources when assessing Synced status
|
||||
func TestCompareAppStateHook(t *testing.T) {
|
||||
pod := test.NewPod()
|
||||
pod.SetAnnotations(map[string]string{common.AnnotationKeyHook: "PreSync"})
|
||||
podBytes, _ := json.Marshal(pod)
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
apps: []runtime.Object{app},
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
Manifests: []string{string(podBytes)},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
|
||||
assert.NoError(t, err)
|
||||
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))
|
||||
}
|
||||
|
||||
// TestCompareAppStateExtraHook tests when there is an extra _hook_ object in live but not defined in git
|
||||
func TestCompareAppStateExtraHook(t *testing.T) {
|
||||
pod := test.NewPod()
|
||||
pod.SetAnnotations(map[string]string{common.AnnotationKeyHook: "PreSync"})
|
||||
pod.SetNamespace(test.FakeDestNamespace)
|
||||
app := newFakeApp()
|
||||
key := kube.ResourceKey{Group: "", Kind: "Pod", Namespace: test.FakeDestNamespace, Name: app.Name}
|
||||
data := fakeData{
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
||||
key: pod,
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
|
||||
assert.NoError(t, err)
|
||||
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))
|
||||
}
|
||||
|
||||
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: &repository.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, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.Contains(t, compRes.conditions, argoappv1.ApplicationCondition{
|
||||
Message: "Resource /Pod/fake-dest-ns/my-pod appeared 2 times among application resources.",
|
||||
Type: argoappv1.ApplicationConditionRepeatedResourceWarning,
|
||||
})
|
||||
assert.Equal(t, 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: &repository.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
||||
kube.GetResourceKey(deployment): deployment,
|
||||
},
|
||||
})
|
||||
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
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: &repository.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, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, compRes.healthStatus.Status, argoappv1.HealthStatusHealthy)
|
||||
}
|
||||
624
controller/sync.go
Normal file
@@ -0,0 +1,624 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
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/client-go/discovery"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/rest"
|
||||
|
||||
"github.com/argoproj/argo-cd/controller/metrics"
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/pkg/client/listers/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/argo"
|
||||
hookutil "github.com/argoproj/argo-cd/util/hook"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
)
|
||||
|
||||
type syncContext struct {
|
||||
appName string
|
||||
proj *appv1.AppProject
|
||||
compareResult *comparisonResult
|
||||
config *rest.Config
|
||||
dynamicIf dynamic.Interface
|
||||
disco discovery.DiscoveryInterface
|
||||
kubectl kube.Kubectl
|
||||
namespace string
|
||||
server string
|
||||
syncOp *appv1.SyncOperation
|
||||
syncRes *appv1.SyncOperationResult
|
||||
syncResources []appv1.SyncOperationResource
|
||||
opState *appv1.OperationState
|
||||
log *log.Entry
|
||||
// lock to protect concurrent updates of the result list
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func (m *appStateManager) SyncAppState(app *appv1.Application, state *appv1.OperationState) {
|
||||
// Sync requests might be requested with ambiguous revisions (e.g. master, HEAD, v1.2.3).
|
||||
// This can change meaning when resuming operations (e.g a hook sync). After calculating a
|
||||
// concrete git commit SHA, the SHA is remembered in the status.operationState.syncResult field.
|
||||
// This ensures that when resuming an operation, we sync to the same revision that we initially
|
||||
// started with.
|
||||
var revision string
|
||||
var syncOp appv1.SyncOperation
|
||||
var syncRes *appv1.SyncOperationResult
|
||||
var syncResources []appv1.SyncOperationResource
|
||||
var source appv1.ApplicationSource
|
||||
|
||||
if state.Operation.Sync == nil {
|
||||
state.Phase = appv1.OperationFailed
|
||||
state.Message = "Invalid operation request: no operation specified"
|
||||
return
|
||||
}
|
||||
syncOp = *state.Operation.Sync
|
||||
if syncOp.Source == nil {
|
||||
// normal sync case (where source is taken from app.spec.source)
|
||||
source = app.Spec.Source
|
||||
} else {
|
||||
// rollback case
|
||||
source = *state.Operation.Sync.Source
|
||||
}
|
||||
syncResources = syncOp.Resources
|
||||
if state.SyncResult != nil {
|
||||
syncRes = state.SyncResult
|
||||
revision = state.SyncResult.Revision
|
||||
} else {
|
||||
syncRes = &appv1.SyncOperationResult{}
|
||||
// status.operationState.syncResult.source. must be set properly since auto-sync relies
|
||||
// on this information to decide if it should sync (if source is different than the last
|
||||
// sync attempt)
|
||||
syncRes.Source = source
|
||||
state.SyncResult = syncRes
|
||||
}
|
||||
|
||||
if revision == "" {
|
||||
// if we get here, it means we did not remember a commit SHA which we should be syncing to.
|
||||
// This typically indicates we are just about to begin a brand new sync/rollback operation.
|
||||
// Take the value in the requested operation. We will resolve this to a SHA later.
|
||||
revision = syncOp.Revision
|
||||
}
|
||||
|
||||
compareResult, err := m.CompareAppState(app, revision, source, false)
|
||||
if err != nil {
|
||||
state.Phase = appv1.OperationError
|
||||
state.Message = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
// If there are any error conditions, do not perform the operation
|
||||
errConditions := make([]appv1.ApplicationCondition, 0)
|
||||
for i := range compareResult.conditions {
|
||||
if compareResult.conditions[i].IsError() {
|
||||
errConditions = append(errConditions, compareResult.conditions[i])
|
||||
}
|
||||
}
|
||||
if len(errConditions) > 0 {
|
||||
state.Phase = appv1.OperationError
|
||||
state.Message = argo.FormatAppConditions(errConditions)
|
||||
return
|
||||
}
|
||||
|
||||
// We now have a concrete commit SHA. Save this in the sync result revision so that we remember
|
||||
// what we should be syncing to when resuming operations.
|
||||
syncRes.Revision = compareResult.syncStatus.Revision
|
||||
|
||||
clst, err := m.db.GetCluster(context.Background(), app.Spec.Destination.Server)
|
||||
if err != nil {
|
||||
state.Phase = appv1.OperationError
|
||||
state.Message = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
restConfig := metrics.AddMetricsTransportWrapper(m.metricsServer, app, clst.RESTConfig())
|
||||
dynamicIf, err := dynamic.NewForConfig(restConfig)
|
||||
if err != nil {
|
||||
state.Phase = appv1.OperationError
|
||||
state.Message = fmt.Sprintf("Failed to initialize dynamic client: %v", err)
|
||||
return
|
||||
}
|
||||
disco, err := discovery.NewDiscoveryClientForConfig(restConfig)
|
||||
if err != nil {
|
||||
state.Phase = appv1.OperationError
|
||||
state.Message = fmt.Sprintf("Failed to initialize discovery client: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
proj, err := argo.GetAppProject(&app.Spec, v1alpha1.NewAppProjectLister(m.projInformer.GetIndexer()), m.namespace)
|
||||
if err != nil {
|
||||
state.Phase = appv1.OperationError
|
||||
state.Message = fmt.Sprintf("Failed to load application project: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
syncCtx := syncContext{
|
||||
appName: app.Name,
|
||||
proj: proj,
|
||||
compareResult: compareResult,
|
||||
config: restConfig,
|
||||
dynamicIf: dynamicIf,
|
||||
disco: disco,
|
||||
kubectl: m.kubectl,
|
||||
namespace: app.Spec.Destination.Namespace,
|
||||
server: app.Spec.Destination.Server,
|
||||
syncOp: &syncOp,
|
||||
syncRes: syncRes,
|
||||
syncResources: syncResources,
|
||||
opState: state,
|
||||
log: log.WithFields(log.Fields{"application": app.Name}),
|
||||
}
|
||||
|
||||
if state.Phase == appv1.OperationTerminating {
|
||||
syncCtx.terminate()
|
||||
} else {
|
||||
syncCtx.sync()
|
||||
}
|
||||
|
||||
if !syncOp.DryRun && len(syncOp.Resources) == 0 && syncCtx.opState.Phase.Successful() {
|
||||
err := m.persistRevisionHistory(app, compareResult.syncStatus.Revision, source)
|
||||
if err != nil {
|
||||
state.Phase = appv1.OperationError
|
||||
state.Message = fmt.Sprintf("failed to record sync to history: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
liveObj *unstructured.Unstructured
|
||||
targetObj *unstructured.Unstructured
|
||||
skipDryRun bool
|
||||
}
|
||||
|
||||
// sync has performs the actual apply or hook based sync
|
||||
func (sc *syncContext) sync() {
|
||||
syncTasks, successful := sc.generateSyncTasks()
|
||||
if !successful {
|
||||
sc.setOperationPhase(appv1.OperationFailed, "one or more synchronization tasks are not valid")
|
||||
return
|
||||
}
|
||||
|
||||
// If no sync tasks were generated (e.g., in case all application manifests have been removed),
|
||||
// set the sync operation as successful.
|
||||
if len(syncTasks) == 0 {
|
||||
sc.setOperationPhase(appv1.OperationSucceeded, "successfully synced (no manifests)")
|
||||
return
|
||||
}
|
||||
|
||||
// Perform a `kubectl apply --dry-run` against all the manifests. This will detect most (but
|
||||
// not all) validation issues with the user's manifests (e.g. will detect syntax issues, but
|
||||
// will not not detect if they are mutating immutable fields). If anything fails, we will refuse
|
||||
// to perform the sync.
|
||||
if !sc.startedPreSyncPhase() {
|
||||
// Optimization: we only wish to do this once per operation, performing additional dry-runs
|
||||
// is harmless, but redundant. The indicator we use to detect if we have already performed
|
||||
// the dry-run for this operation, is if the resource or hook list is empty.
|
||||
if !sc.doApplySync(syncTasks, true, false, sc.syncOp.DryRun) {
|
||||
sc.setOperationPhase(appv1.OperationFailed, "one or more objects failed to apply (dry run)")
|
||||
return
|
||||
}
|
||||
if sc.syncOp.DryRun {
|
||||
sc.setOperationPhase(appv1.OperationSucceeded, "successfully synced (dry run)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// All objects passed a `kubectl apply --dry-run`, so we are now ready to actually perform the sync.
|
||||
if sc.syncOp.SyncStrategy == nil {
|
||||
// default sync strategy to hook if no strategy
|
||||
sc.syncOp.SyncStrategy = &appv1.SyncStrategy{Hook: &appv1.SyncStrategyHook{}}
|
||||
}
|
||||
if sc.syncOp.SyncStrategy.Apply != nil {
|
||||
if !sc.startedSyncPhase() {
|
||||
if !sc.doApplySync(syncTasks, false, sc.syncOp.SyncStrategy.Apply.Force, true) {
|
||||
sc.setOperationPhase(appv1.OperationFailed, "one or more objects failed to apply")
|
||||
return
|
||||
}
|
||||
// If apply was successful, return here and force an app refresh. This is so the app
|
||||
// will become requeued into the workqueue, to force a new sync/health assessment before
|
||||
// marking the operation as completed
|
||||
return
|
||||
}
|
||||
sc.setOperationPhase(appv1.OperationSucceeded, "successfully synced")
|
||||
} else if sc.syncOp.SyncStrategy.Hook != nil {
|
||||
hooks, err := sc.getHooks()
|
||||
if err != nil {
|
||||
sc.setOperationPhase(appv1.OperationError, fmt.Sprintf("failed to generate hooks resources: %v", err))
|
||||
return
|
||||
}
|
||||
sc.doHookSync(syncTasks, hooks)
|
||||
} else {
|
||||
sc.setOperationPhase(appv1.OperationFailed, "Unknown sync strategy")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// generateSyncTasks() generates the list of sync tasks we will be performing during this sync.
|
||||
func (sc *syncContext) generateSyncTasks() ([]syncTask, bool) {
|
||||
syncTasks := make([]syncTask, 0)
|
||||
successful := true
|
||||
for _, resourceState := range sc.compareResult.managedResources {
|
||||
if resourceState.Hook {
|
||||
continue
|
||||
}
|
||||
if sc.syncResources == nil ||
|
||||
(resourceState.Live != nil && argo.ContainsSyncResource(resourceState.Live.GetName(), resourceState.Live.GroupVersionKind(), sc.syncResources)) ||
|
||||
(resourceState.Target != nil && argo.ContainsSyncResource(resourceState.Target.GetName(), resourceState.Target.GroupVersionKind(), sc.syncResources)) {
|
||||
|
||||
skipDryRun := false
|
||||
var targetObj *unstructured.Unstructured
|
||||
if resourceState.Target != nil {
|
||||
targetObj = resourceState.Target.DeepCopy()
|
||||
if targetObj.GetNamespace() == "" {
|
||||
// If target object's namespace is empty, we set namespace in the object. We do
|
||||
// this even though it might be a cluster-scoped resource. This prevents any
|
||||
// possibility of the resource from unintentionally becoming created in the
|
||||
// namespace during the `kubectl apply`
|
||||
targetObj.SetNamespace(sc.namespace)
|
||||
}
|
||||
gvk := targetObj.GroupVersionKind()
|
||||
serverRes, err := kube.ServerResourceForGroupVersionKind(sc.disco, gvk)
|
||||
|
||||
if err != nil {
|
||||
// Special case for custom resources: if CRD is not yet known by the K8s API server,
|
||||
// skip verification during `kubectl apply --dry-run` since we expect the CRD
|
||||
// to be created during app synchronization.
|
||||
if apierr.IsNotFound(err) && hasCRDOfGroupKind(sc.compareResult.managedResources, gvk.Group, gvk.Kind) {
|
||||
skipDryRun = true
|
||||
} else {
|
||||
sc.setResourceDetails(&appv1.ResourceResult{
|
||||
Name: targetObj.GetName(),
|
||||
Group: gvk.Group,
|
||||
Version: gvk.Version,
|
||||
Kind: targetObj.GetKind(),
|
||||
Namespace: targetObj.GetNamespace(),
|
||||
Message: err.Error(),
|
||||
Status: appv1.ResultCodeSyncFailed,
|
||||
})
|
||||
successful = false
|
||||
}
|
||||
} else {
|
||||
if !sc.proj.IsResourcePermitted(metav1.GroupKind{Group: gvk.Group, Kind: gvk.Kind}, serverRes.Namespaced) {
|
||||
sc.setResourceDetails(&appv1.ResourceResult{
|
||||
Name: targetObj.GetName(),
|
||||
Group: gvk.Group,
|
||||
Version: gvk.Version,
|
||||
Kind: targetObj.GetKind(),
|
||||
Namespace: targetObj.GetNamespace(),
|
||||
Message: fmt.Sprintf("Resource %s:%s is not permitted in project %s.", gvk.Group, gvk.Kind, sc.proj.Name),
|
||||
Status: appv1.ResultCodeSyncFailed,
|
||||
})
|
||||
successful = false
|
||||
}
|
||||
|
||||
if serverRes.Namespaced && !sc.proj.IsDestinationPermitted(appv1.ApplicationDestination{Namespace: targetObj.GetNamespace(), Server: sc.server}) {
|
||||
sc.setResourceDetails(&appv1.ResourceResult{
|
||||
Name: targetObj.GetName(),
|
||||
Group: gvk.Group,
|
||||
Version: gvk.Version,
|
||||
Kind: targetObj.GetKind(),
|
||||
Namespace: targetObj.GetNamespace(),
|
||||
Message: fmt.Sprintf("namespace %v is not permitted in project '%s'", targetObj.GetNamespace(), sc.proj.Name),
|
||||
Status: appv1.ResultCodeSyncFailed,
|
||||
})
|
||||
successful = false
|
||||
}
|
||||
}
|
||||
}
|
||||
syncTask := syncTask{
|
||||
liveObj: resourceState.Live,
|
||||
targetObj: targetObj,
|
||||
skipDryRun: skipDryRun,
|
||||
}
|
||||
syncTasks = append(syncTasks, syncTask)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(newKindSorter(syncTasks, resourceOrder))
|
||||
return syncTasks, successful
|
||||
}
|
||||
|
||||
// startedPreSyncPhase detects if we already started the PreSync stage of a sync operation.
|
||||
// This is equal to if we have anything in our resource or hook list
|
||||
func (sc *syncContext) startedPreSyncPhase() bool {
|
||||
return len(sc.syncRes.Resources) > 0
|
||||
}
|
||||
|
||||
// startedSyncPhase detects if we have already started the Sync stage of a sync operation.
|
||||
// This is equal to if the resource list is non-empty, or we we see Sync/PostSync hooks
|
||||
func (sc *syncContext) startedSyncPhase() bool {
|
||||
for _, res := range sc.syncRes.Resources {
|
||||
if !res.IsHook() {
|
||||
return true
|
||||
}
|
||||
if res.HookType == appv1.HookTypeSync || res.HookType == appv1.HookTypePostSync {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// startedPostSyncPhase detects if we have already started the PostSync stage. This is equal to if
|
||||
// we see any PostSync hooks
|
||||
func (sc *syncContext) startedPostSyncPhase() bool {
|
||||
for _, res := range sc.syncRes.Resources {
|
||||
if res.IsHook() && res.HookType == appv1.HookTypePostSync {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (sc *syncContext) setOperationPhase(phase appv1.OperationPhase, message string) {
|
||||
if sc.opState.Phase != phase || sc.opState.Message != message {
|
||||
sc.log.Infof("Updating operation state. phase: %s -> %s, message: '%s' -> '%s'", sc.opState.Phase, phase, sc.opState.Message, message)
|
||||
}
|
||||
sc.opState.Phase = phase
|
||||
sc.opState.Message = message
|
||||
}
|
||||
|
||||
// applyObject performs a `kubectl apply` of a single resource
|
||||
func (sc *syncContext) applyObject(targetObj *unstructured.Unstructured, dryRun bool, force bool) appv1.ResourceResult {
|
||||
gvk := targetObj.GroupVersionKind()
|
||||
resDetails := appv1.ResourceResult{
|
||||
Name: targetObj.GetName(),
|
||||
Group: gvk.Group,
|
||||
Version: gvk.Version,
|
||||
Kind: targetObj.GetKind(),
|
||||
Namespace: targetObj.GetNamespace(),
|
||||
}
|
||||
message, err := sc.kubectl.ApplyResource(sc.config, targetObj, targetObj.GetNamespace(), dryRun, force)
|
||||
if err != nil {
|
||||
resDetails.Message = err.Error()
|
||||
resDetails.Status = appv1.ResultCodeSyncFailed
|
||||
return resDetails
|
||||
}
|
||||
|
||||
resDetails.Message = message
|
||||
resDetails.Status = appv1.ResultCodeSynced
|
||||
return resDetails
|
||||
}
|
||||
|
||||
// pruneObject deletes the object if both prune is true and dryRun is false. Otherwise appropriate message
|
||||
func (sc *syncContext) pruneObject(liveObj *unstructured.Unstructured, prune, dryRun bool) appv1.ResourceResult {
|
||||
gvk := liveObj.GroupVersionKind()
|
||||
resDetails := appv1.ResourceResult{
|
||||
Name: liveObj.GetName(),
|
||||
Group: gvk.Group,
|
||||
Version: gvk.Version,
|
||||
Kind: liveObj.GetKind(),
|
||||
Namespace: liveObj.GetNamespace(),
|
||||
}
|
||||
if prune {
|
||||
if dryRun {
|
||||
resDetails.Message = "pruned (dry run)"
|
||||
resDetails.Status = appv1.ResultCodePruned
|
||||
} else {
|
||||
resDetails.Message = "pruned"
|
||||
resDetails.Status = appv1.ResultCodePruned
|
||||
// Skip deletion if object is already marked for deletion, so we don't cause a resource update hotloop
|
||||
deletionTimestamp := liveObj.GetDeletionTimestamp()
|
||||
if deletionTimestamp == nil || deletionTimestamp.IsZero() {
|
||||
err := sc.kubectl.DeleteResource(sc.config, liveObj.GroupVersionKind(), liveObj.GetName(), liveObj.GetNamespace(), false)
|
||||
if err != nil {
|
||||
resDetails.Message = err.Error()
|
||||
resDetails.Status = appv1.ResultCodeSyncFailed
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resDetails.Message = "ignored (requires pruning)"
|
||||
resDetails.Status = appv1.ResultCodePruneSkipped
|
||||
}
|
||||
return resDetails
|
||||
}
|
||||
|
||||
func hasCRDOfGroupKind(resources []managedResource, group string, kind string) bool {
|
||||
for _, res := range resources {
|
||||
if res.Target != nil && kube.IsCRD(res.Target) {
|
||||
crdGroup, ok, err := unstructured.NestedString(res.Target.Object, "spec", "group")
|
||||
if err != nil || !ok {
|
||||
continue
|
||||
}
|
||||
crdKind, ok, err := unstructured.NestedString(res.Target.Object, "spec", "names", "kind")
|
||||
if err != nil || !ok {
|
||||
continue
|
||||
}
|
||||
if group == crdGroup && crdKind == kind {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// performs a apply based sync of the given sync tasks (possibly pruning the objects).
|
||||
// If update is true, will updates the resource details with the result.
|
||||
// Or if the prune/apply failed, will also update the result.
|
||||
func (sc *syncContext) doApplySync(syncTasks []syncTask, dryRun, force, update bool) bool {
|
||||
syncSuccessful := true
|
||||
|
||||
var createTasks []syncTask
|
||||
var pruneTasks []syncTask
|
||||
for _, syncTask := range syncTasks {
|
||||
if syncTask.targetObj == nil {
|
||||
pruneTasks = append(pruneTasks, syncTask)
|
||||
} else {
|
||||
createTasks = append(createTasks, syncTask)
|
||||
}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, task := range pruneTasks {
|
||||
wg.Add(1)
|
||||
go func(t syncTask) {
|
||||
defer wg.Done()
|
||||
resDetails := sc.pruneObject(t.liveObj, sc.syncOp.Prune, dryRun)
|
||||
if !resDetails.Status.Successful() {
|
||||
syncSuccessful = false
|
||||
}
|
||||
if update || !resDetails.Status.Successful() {
|
||||
sc.setResourceDetails(&resDetails)
|
||||
}
|
||||
}(task)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
processCreateTasks := func(tasks []syncTask) {
|
||||
var createWg sync.WaitGroup
|
||||
for i := range tasks {
|
||||
if dryRun && tasks[i].skipDryRun {
|
||||
continue
|
||||
}
|
||||
createWg.Add(1)
|
||||
go func(t syncTask) {
|
||||
defer createWg.Done()
|
||||
if hookutil.IsHook(t.targetObj) {
|
||||
return
|
||||
}
|
||||
resDetails := sc.applyObject(t.targetObj, dryRun, force)
|
||||
if !resDetails.Status.Successful() {
|
||||
syncSuccessful = false
|
||||
}
|
||||
if update || !resDetails.Status.Successful() {
|
||||
sc.setResourceDetails(&resDetails)
|
||||
}
|
||||
}(tasks[i])
|
||||
}
|
||||
createWg.Wait()
|
||||
}
|
||||
|
||||
var tasksGroup []syncTask
|
||||
for _, task := range createTasks {
|
||||
//Only wait if the type of the next task is different than the previous type
|
||||
if len(tasksGroup) > 0 && tasksGroup[0].targetObj.GetKind() != task.targetObj.GetKind() {
|
||||
processCreateTasks(tasksGroup)
|
||||
tasksGroup = []syncTask{task}
|
||||
} else {
|
||||
tasksGroup = append(tasksGroup, task)
|
||||
}
|
||||
}
|
||||
if len(tasksGroup) > 0 {
|
||||
processCreateTasks(tasksGroup)
|
||||
}
|
||||
return syncSuccessful
|
||||
}
|
||||
|
||||
// setResourceDetails sets a resource details in the SyncResult.Resources list
|
||||
func (sc *syncContext) setResourceDetails(details *appv1.ResourceResult) {
|
||||
sc.lock.Lock()
|
||||
defer sc.lock.Unlock()
|
||||
for i, res := range sc.syncRes.Resources {
|
||||
if res.Group == details.Group && res.Kind == details.Kind && res.Namespace == details.Namespace && res.Name == details.Name {
|
||||
// update existing value
|
||||
if res.Status != details.Status {
|
||||
sc.log.Infof("updated resource %s/%s/%s status: %s -> %s", res.Kind, res.Namespace, res.Name, res.Status, details.Status)
|
||||
}
|
||||
if res.Message != details.Message {
|
||||
sc.log.Infof("updated resource %s/%s/%s message: %s -> %s", res.Kind, res.Namespace, res.Name, res.Message, details.Message)
|
||||
}
|
||||
sc.syncRes.Resources[i] = details
|
||||
return
|
||||
}
|
||||
}
|
||||
sc.log.Infof("added resource %s/%s status: %s, message: %s", details.Kind, details.Name, details.Status, details.Message)
|
||||
sc.syncRes.Resources = append(sc.syncRes.Resources, details)
|
||||
}
|
||||
|
||||
// This code is mostly taken from https://github.com/helm/helm/blob/release-2.10/pkg/tiller/kind_sorter.go
|
||||
|
||||
// sortOrder is an ordering of Kinds.
|
||||
type sortOrder []string
|
||||
|
||||
// resourceOrder represents the correct order of Kubernetes resources within a manifest
|
||||
var resourceOrder sortOrder = []string{
|
||||
"Namespace",
|
||||
"ResourceQuota",
|
||||
"LimitRange",
|
||||
"PodSecurityPolicy",
|
||||
"Secret",
|
||||
"ConfigMap",
|
||||
"StorageClass",
|
||||
"PersistentVolume",
|
||||
"PersistentVolumeClaim",
|
||||
"ServiceAccount",
|
||||
"CustomResourceDefinition",
|
||||
"ClusterRole",
|
||||
"ClusterRoleBinding",
|
||||
"Role",
|
||||
"RoleBinding",
|
||||
"Service",
|
||||
"DaemonSet",
|
||||
"Pod",
|
||||
"ReplicationController",
|
||||
"ReplicaSet",
|
||||
"Deployment",
|
||||
"StatefulSet",
|
||||
"Job",
|
||||
"CronJob",
|
||||
"Ingress",
|
||||
"APIService",
|
||||
}
|
||||
|
||||
type kindSorter struct {
|
||||
ordering map[string]int
|
||||
manifests []syncTask
|
||||
}
|
||||
|
||||
func newKindSorter(m []syncTask, s sortOrder) *kindSorter {
|
||||
o := make(map[string]int, len(s))
|
||||
for v, k := range s {
|
||||
o[k] = v
|
||||
}
|
||||
|
||||
return &kindSorter{
|
||||
manifests: m,
|
||||
ordering: o,
|
||||
}
|
||||
}
|
||||
|
||||
func (k *kindSorter) Len() int { return len(k.manifests) }
|
||||
|
||||
func (k *kindSorter) Swap(i, j int) { k.manifests[i], k.manifests[j] = k.manifests[j], k.manifests[i] }
|
||||
|
||||
func (k *kindSorter) Less(i, j int) bool {
|
||||
a := k.manifests[i].targetObj
|
||||
if a == nil {
|
||||
return false
|
||||
}
|
||||
b := k.manifests[j].targetObj
|
||||
if b == nil {
|
||||
return true
|
||||
}
|
||||
first, aok := k.ordering[a.GetKind()]
|
||||
second, bok := k.ordering[b.GetKind()]
|
||||
|
||||
// if both are unknown and of different kind sort by kind alphabetically
|
||||
if !aok && !bok && a.GetKind() != b.GetKind() {
|
||||
return a.GetKind() < b.GetKind()
|
||||
}
|
||||
|
||||
// unknown kind is last
|
||||
if !aok {
|
||||
return false
|
||||
}
|
||||
if !bok {
|
||||
return true
|
||||
}
|
||||
|
||||
// if same kind (including unknown) sub sort alphanumeric
|
||||
if first == second {
|
||||
return a.GetName() < b.GetName()
|
||||
}
|
||||
// sort different kinds
|
||||
return first < second
|
||||
}
|
||||
66
controller/sync_hook_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/test"
|
||||
"github.com/argoproj/argo-cd/util/kube/kubetest"
|
||||
)
|
||||
|
||||
var clusterRoleHook = `
|
||||
{
|
||||
"apiVersion": "rbac.authorization.k8s.io/v1",
|
||||
"kind": "ClusterRole",
|
||||
"metadata": {
|
||||
"name": "cluster-role-hook",
|
||||
"annotations": {
|
||||
"argocd.argoproj.io/hook": "PostSync"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
func TestSyncHookProjectPermissions(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx(&v1.APIResourceList{
|
||||
GroupVersion: "v1",
|
||||
APIResources: []v1.APIResource{
|
||||
{Name: "pod", Namespaced: true, Kind: "Pod", Group: "v1"},
|
||||
},
|
||||
}, &v1.APIResourceList{
|
||||
GroupVersion: "rbac.authorization.k8s.io/v1",
|
||||
APIResources: []v1.APIResource{
|
||||
{Name: "clusterroles", Namespaced: false, Kind: "ClusterRole", Group: "rbac.authorization.k8s.io"},
|
||||
},
|
||||
})
|
||||
|
||||
syncCtx.kubectl = kubetest.MockKubectlCmd{}
|
||||
crHook, _ := v1alpha1.UnmarshalToUnstructured(clusterRoleHook)
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
hooks: []*unstructured.Unstructured{
|
||||
crHook,
|
||||
},
|
||||
managedResources: []managedResource{{
|
||||
Target: test.NewPod(),
|
||||
}},
|
||||
}
|
||||
syncCtx.proj.Spec.ClusterResourceWhitelist = []v1.GroupKind{}
|
||||
|
||||
syncCtx.syncOp.SyncStrategy = nil
|
||||
syncCtx.sync()
|
||||
assert.Equal(t, v1alpha1.OperationFailed, syncCtx.opState.Phase)
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 0)
|
||||
assert.Contains(t, syncCtx.opState.Message, "not permitted in project")
|
||||
|
||||
// Now add the resource to the whitelist and try again. Resource should be created
|
||||
syncCtx.proj.Spec.ClusterResourceWhitelist = []v1.GroupKind{
|
||||
{Group: "rbac.authorization.k8s.io", Kind: "ClusterRole"},
|
||||
}
|
||||
syncCtx.syncOp.SyncStrategy = nil
|
||||
syncCtx.sync()
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 1)
|
||||
assert.Equal(t, v1alpha1.ResultCodeSynced, syncCtx.syncRes.Resources[0].Status)
|
||||
}
|
||||
560
controller/sync_hooks.go
Normal file
@@ -0,0 +1,560 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
apierr "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/kubernetes/pkg/apis/batch"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
hookutil "github.com/argoproj/argo-cd/util/hook"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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)
|
||||
} else if isArgoWorkflow(gvk) {
|
||||
updateStatusFromArgoWorkflow(hook, &hookStatus)
|
||||
} else if isPod(gvk) {
|
||||
updateStatusFromPod(hook, &hookStatus)
|
||||
} else {
|
||||
hookStatus.HookPhase = appv1.OperationSucceeded
|
||||
hookStatus.Message = fmt.Sprintf("%s created", hook.GetName())
|
||||
}
|
||||
return hookStatus
|
||||
}
|
||||
|
||||
// isRunnable returns if the resource object is a runnable type which needs to be terminated
|
||||
func isRunnable(res *appv1.ResourceResult) bool {
|
||||
gvk := res.GroupVersionKind()
|
||||
return isBatchJob(gvk) || isArgoWorkflow(gvk) || isPod(gvk)
|
||||
}
|
||||
|
||||
func isBatchJob(gvk schema.GroupVersionKind) bool {
|
||||
return gvk.Group == "batch" && gvk.Kind == "Job"
|
||||
}
|
||||
|
||||
func updateStatusFromBatchJob(hook *unstructured.Unstructured, hookStatus *appv1.ResourceResult) {
|
||||
var job batch.Job
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(hook.Object, &job)
|
||||
if err != nil {
|
||||
hookStatus.HookPhase = appv1.OperationError
|
||||
hookStatus.Message = err.Error()
|
||||
return
|
||||
}
|
||||
failed := false
|
||||
var failMsg string
|
||||
complete := false
|
||||
var message string
|
||||
for _, condition := range job.Status.Conditions {
|
||||
switch condition.Type {
|
||||
case batch.JobFailed:
|
||||
failed = true
|
||||
complete = true
|
||||
failMsg = condition.Message
|
||||
case batch.JobComplete:
|
||||
complete = true
|
||||
message = condition.Message
|
||||
}
|
||||
}
|
||||
if !complete {
|
||||
hookStatus.HookPhase = appv1.OperationRunning
|
||||
hookStatus.Message = message
|
||||
} else if failed {
|
||||
hookStatus.HookPhase = appv1.OperationFailed
|
||||
hookStatus.Message = failMsg
|
||||
} else {
|
||||
hookStatus.HookPhase = appv1.OperationSucceeded
|
||||
hookStatus.Message = message
|
||||
}
|
||||
}
|
||||
|
||||
func isArgoWorkflow(gvk schema.GroupVersionKind) bool {
|
||||
return gvk.Group == "argoproj.io" && gvk.Kind == "Workflow"
|
||||
}
|
||||
|
||||
func updateStatusFromArgoWorkflow(hook *unstructured.Unstructured, hookStatus *appv1.ResourceResult) {
|
||||
var wf wfv1.Workflow
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(hook.Object, &wf)
|
||||
if err != nil {
|
||||
hookStatus.HookPhase = appv1.OperationError
|
||||
hookStatus.Message = err.Error()
|
||||
return
|
||||
}
|
||||
switch wf.Status.Phase {
|
||||
case wfv1.NodePending, wfv1.NodeRunning:
|
||||
hookStatus.HookPhase = appv1.OperationRunning
|
||||
case wfv1.NodeSucceeded:
|
||||
hookStatus.HookPhase = appv1.OperationSucceeded
|
||||
case wfv1.NodeFailed:
|
||||
hookStatus.HookPhase = appv1.OperationFailed
|
||||
case wfv1.NodeError:
|
||||
hookStatus.HookPhase = appv1.OperationError
|
||||
}
|
||||
hookStatus.Message = wf.Status.Message
|
||||
}
|
||||
|
||||
func isPod(gvk schema.GroupVersionKind) bool {
|
||||
return gvk.Group == "" && gvk.Kind == "Pod"
|
||||
}
|
||||
|
||||
func updateStatusFromPod(hook *unstructured.Unstructured, hookStatus *appv1.ResourceResult) {
|
||||
var pod apiv1.Pod
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(hook.Object, &pod)
|
||||
if err != nil {
|
||||
hookStatus.HookPhase = appv1.OperationError
|
||||
hookStatus.Message = err.Error()
|
||||
return
|
||||
}
|
||||
getFailMessage := func(ctr *apiv1.ContainerStatus) string {
|
||||
if ctr.State.Terminated != nil {
|
||||
if ctr.State.Terminated.Message != "" {
|
||||
return ctr.State.Terminated.Message
|
||||
}
|
||||
if ctr.State.Terminated.Reason == "OOMKilled" {
|
||||
return ctr.State.Terminated.Reason
|
||||
}
|
||||
if ctr.State.Terminated.ExitCode != 0 {
|
||||
return fmt.Sprintf("container %q failed with exit code %d", ctr.Name, ctr.State.Terminated.ExitCode)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
switch pod.Status.Phase {
|
||||
case apiv1.PodPending, apiv1.PodRunning:
|
||||
hookStatus.HookPhase = appv1.OperationRunning
|
||||
case apiv1.PodSucceeded:
|
||||
hookStatus.HookPhase = appv1.OperationSucceeded
|
||||
case apiv1.PodFailed:
|
||||
hookStatus.HookPhase = appv1.OperationFailed
|
||||
if pod.Status.Message != "" {
|
||||
// Pod has a nice error message. Use that.
|
||||
hookStatus.Message = pod.Status.Message
|
||||
return
|
||||
}
|
||||
for _, ctr := range append(pod.Status.InitContainerStatuses, pod.Status.ContainerStatuses...) {
|
||||
if msg := getFailMessage(&ctr); msg != "" {
|
||||
hookStatus.Message = msg
|
||||
return
|
||||
}
|
||||
}
|
||||
case apiv1.PodUnknown:
|
||||
hookStatus.HookPhase = appv1.OperationError
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *syncContext) getHookStatus(hookObj *unstructured.Unstructured, hookType appv1.HookType) *appv1.ResourceResult {
|
||||
for _, hr := range sc.syncRes.Resources {
|
||||
if !hr.IsHook() {
|
||||
continue
|
||||
}
|
||||
ns := util.FirstNonEmpty(hookObj.GetNamespace(), sc.namespace)
|
||||
if hookEqual(hr, hookObj.GroupVersionKind().Group, hookObj.GetKind(), ns, hookObj.GetName(), hookType) {
|
||||
return hr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hookEqual(hr *appv1.ResourceResult, group, kind, namespace, name string, hookType appv1.HookType) bool {
|
||||
return bool(
|
||||
hr.Group == group &&
|
||||
hr.Kind == kind &&
|
||||
hr.Namespace == namespace &&
|
||||
hr.Name == name &&
|
||||
hr.HookType == hookType)
|
||||
}
|
||||
|
||||
// updateHookStatus updates the status of a hook. Returns true if the hook was modified
|
||||
func (sc *syncContext) updateHookStatus(hookStatus appv1.ResourceResult) bool {
|
||||
sc.lock.Lock()
|
||||
defer sc.lock.Unlock()
|
||||
for i, prev := range sc.syncRes.Resources {
|
||||
if !prev.IsHook() {
|
||||
continue
|
||||
}
|
||||
if hookEqual(prev, hookStatus.Group, hookStatus.Kind, hookStatus.Namespace, hookStatus.Name, hookStatus.HookType) {
|
||||
if reflect.DeepEqual(prev, hookStatus) {
|
||||
return false
|
||||
}
|
||||
if prev.HookPhase != hookStatus.HookPhase {
|
||||
sc.log.Infof("Hook %s %s/%s hookPhase: %s -> %s", hookStatus.HookType, prev.Kind, prev.Name, prev.HookPhase, hookStatus.HookPhase)
|
||||
}
|
||||
if prev.Status != hookStatus.Status {
|
||||
sc.log.Infof("Hook %s %s/%s status: %s -> %s", hookStatus.HookType, prev.Kind, prev.Name, prev.Status, hookStatus.Status)
|
||||
}
|
||||
if prev.Message != hookStatus.Message {
|
||||
sc.log.Infof("Hook %s %s/%s message: '%s' -> '%s'", hookStatus.HookType, prev.Kind, prev.Name, prev.Message, hookStatus.Message)
|
||||
}
|
||||
sc.syncRes.Resources[i] = &hookStatus
|
||||
return true
|
||||
}
|
||||
}
|
||||
sc.syncRes.Resources = append(sc.syncRes.Resources, &hookStatus)
|
||||
sc.log.Infof("Set new hook %s %s/%s. phase: %s, message: %s", hookStatus.HookType, hookStatus.Kind, hookStatus.Name, hookStatus.HookPhase, hookStatus.Message)
|
||||
return true
|
||||
}
|
||||
|
||||
// areHooksCompletedSuccessful checks if all the hooks of the specified type are completed and successful
|
||||
func areHooksCompletedSuccessful(hookType appv1.HookType, hookStatuses []*appv1.ResourceResult) (bool, bool) {
|
||||
isSuccessful := true
|
||||
for _, hookStatus := range hookStatuses {
|
||||
if !hookStatus.IsHook() {
|
||||
continue
|
||||
}
|
||||
if hookStatus.HookType != hookType {
|
||||
continue
|
||||
}
|
||||
if !hookStatus.HookPhase.Completed() {
|
||||
return false, false
|
||||
}
|
||||
if !hookStatus.HookPhase.Successful() {
|
||||
isSuccessful = false
|
||||
}
|
||||
}
|
||||
return true, isSuccessful
|
||||
}
|
||||
|
||||
// terminate looks for any running jobs/workflow hooks and deletes the resource
|
||||
func (sc *syncContext) terminate() {
|
||||
terminateSuccessful := true
|
||||
for _, hookStatus := range sc.syncRes.Resources {
|
||||
if !hookStatus.IsHook() {
|
||||
continue
|
||||
}
|
||||
if hookStatus.HookPhase.Completed() {
|
||||
continue
|
||||
}
|
||||
if isRunnable(hookStatus) {
|
||||
hookStatus.HookPhase = appv1.OperationFailed
|
||||
err := sc.deleteHook(hookStatus.Name, hookStatus.Namespace, hookStatus.GroupVersionKind())
|
||||
if err != nil {
|
||||
hookStatus.Message = fmt.Sprintf("Failed to delete %s hook %s/%s: %v", hookStatus.HookType, hookStatus.Kind, hookStatus.Name, err)
|
||||
terminateSuccessful = false
|
||||
} else {
|
||||
hookStatus.Message = fmt.Sprintf("Deleted %s hook %s/%s", hookStatus.HookType, hookStatus.Kind, hookStatus.Name)
|
||||
}
|
||||
sc.updateHookStatus(*hookStatus)
|
||||
}
|
||||
}
|
||||
if terminateSuccessful {
|
||||
sc.setOperationPhase(appv1.OperationFailed, "Operation terminated")
|
||||
} else {
|
||||
sc.setOperationPhase(appv1.OperationError, "Operation termination had errors")
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *syncContext) deleteHook(name, namespace string, gvk schema.GroupVersionKind) error {
|
||||
apiResource, err := kube.ServerResourceForGroupVersionKind(sc.disco, gvk)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resource := kube.ToGroupVersionResource(gvk.GroupVersion().String(), apiResource)
|
||||
resIf := kube.ToResourceInterface(sc.dynamicIf, apiResource, resource, namespace)
|
||||
propagationPolicy := metav1.DeletePropagationForeground
|
||||
return resIf.Delete(name, &metav1.DeleteOptions{PropagationPolicy: &propagationPolicy})
|
||||
}
|
||||
528
controller/sync_test.go
Normal file
@@ -0,0 +1,528 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"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/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/test"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/kube/kubetest"
|
||||
)
|
||||
|
||||
func newTestSyncCtx(resources ...*v1.APIResourceList) *syncContext {
|
||||
fakeDisco := &fakedisco.FakeDiscovery{Fake: &testcore.Fake{}}
|
||||
fakeDisco.Resources = append(resources,
|
||||
&v1.APIResourceList{
|
||||
GroupVersion: "v1",
|
||||
APIResources: []v1.APIResource{
|
||||
{Kind: "Pod", Group: "", Version: "v1", Namespaced: true},
|
||||
{Kind: "Service", Group: "", Version: "v1", Namespaced: true},
|
||||
},
|
||||
},
|
||||
&v1.APIResourceList{
|
||||
GroupVersion: "apps/v1",
|
||||
APIResources: []v1.APIResource{
|
||||
{Kind: "Deployment", Group: "apps", Version: "v1", Namespaced: true},
|
||||
},
|
||||
})
|
||||
sc := syncContext{
|
||||
config: &rest.Config{},
|
||||
namespace: test.FakeArgoCDNamespace,
|
||||
server: test.FakeClusterURL,
|
||||
syncRes: &v1alpha1.SyncOperationResult{},
|
||||
syncOp: &v1alpha1.SyncOperation{
|
||||
Prune: true,
|
||||
SyncStrategy: &v1alpha1.SyncStrategy{
|
||||
Apply: &v1alpha1.SyncStrategyApply{},
|
||||
},
|
||||
},
|
||||
proj: &v1alpha1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
},
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
Destinations: []v1alpha1.ApplicationDestination{{
|
||||
Server: test.FakeClusterURL,
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
}},
|
||||
ClusterResourceWhitelist: []v1.GroupKind{
|
||||
{Group: "*", Kind: "*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
opState: &v1alpha1.OperationState{},
|
||||
disco: fakeDisco,
|
||||
log: log.WithFields(log.Fields{"application": "fake-app"}),
|
||||
}
|
||||
sc.kubectl = kubetest.MockKubectlCmd{}
|
||||
return &sc
|
||||
}
|
||||
|
||||
func TestSyncNotPermittedNamespace(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
targetPod := test.NewPod()
|
||||
targetPod.SetNamespace("kube-system")
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: nil,
|
||||
Target: targetPod,
|
||||
}, {
|
||||
Live: nil,
|
||||
Target: test.NewService(),
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Equal(t, v1alpha1.OperationFailed, syncCtx.opState.Phase)
|
||||
assert.Contains(t, syncCtx.syncRes.Resources[0].Message, "not permitted in project")
|
||||
}
|
||||
|
||||
func TestSyncCreateInSortedOrder(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: nil,
|
||||
Target: test.NewPod(),
|
||||
}, {
|
||||
Live: nil,
|
||||
Target: test.NewService(),
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
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)
|
||||
} 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) {
|
||||
syncCtx := newTestSyncCtx(&v1.APIResourceList{
|
||||
GroupVersion: v1alpha1.SchemeGroupVersion.String(),
|
||||
APIResources: []v1.APIResource{
|
||||
{Name: "workflows", Namespaced: false, Kind: "Workflow", Group: "argoproj.io"},
|
||||
{Name: "application", Namespaced: false, Kind: "Application", Group: "argoproj.io"},
|
||||
},
|
||||
}, &v1.APIResourceList{
|
||||
GroupVersion: "rbac.authorization.k8s.io/v1",
|
||||
APIResources: []v1.APIResource{
|
||||
{Name: "clusterroles", Namespaced: false, Kind: "ClusterRole", Group: "rbac.authorization.k8s.io"},
|
||||
},
|
||||
})
|
||||
|
||||
syncCtx.proj.Spec.ClusterResourceWhitelist = []v1.GroupKind{
|
||||
{Group: "argoproj.io", Kind: "*"},
|
||||
}
|
||||
|
||||
syncCtx.kubectl = kubetest.MockKubectlCmd{}
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: nil,
|
||||
Target: kube.MustToUnstructured(&rbacv1.ClusterRole{
|
||||
TypeMeta: metav1.TypeMeta{Kind: "ClusterRole", APIVersion: "rbac.authorization.k8s.io/v1"},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "argo-ui-cluster-role"}}),
|
||||
}},
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
func TestSyncBlacklistedNamespacedResources(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
|
||||
syncCtx.proj.Spec.NamespaceResourceBlacklist = []v1.GroupKind{
|
||||
{Group: "*", Kind: "Deployment"},
|
||||
}
|
||||
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: nil,
|
||||
Target: test.NewDeployment(),
|
||||
}},
|
||||
}
|
||||
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")
|
||||
}
|
||||
|
||||
func TestSyncSuccessfully(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: nil,
|
||||
Target: test.NewService(),
|
||||
}, {
|
||||
Live: test.NewPod(),
|
||||
Target: nil,
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
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)
|
||||
} 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()
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: test.NewService(),
|
||||
Target: nil,
|
||||
}, {
|
||||
Live: test.NewPod(),
|
||||
Target: nil,
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
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)
|
||||
} 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{
|
||||
Commands: map[string]kubetest.KubectlOutput{
|
||||
"test-service": {
|
||||
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"),
|
||||
},
|
||||
},
|
||||
}
|
||||
testSvc := test.NewService()
|
||||
testSvc.SetAPIVersion("")
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: nil,
|
||||
Target: testSvc,
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 1)
|
||||
assert.Equal(t, v1alpha1.ResultCodeSyncFailed, syncCtx.syncRes.Resources[0].Status)
|
||||
}
|
||||
|
||||
func TestSyncPruneFailure(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
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"),
|
||||
},
|
||||
},
|
||||
}
|
||||
testSvc := test.NewService()
|
||||
testSvc.SetName("test-service")
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: testSvc,
|
||||
Target: nil,
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDontSyncOrPruneHooks(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
targetPod := test.NewPod()
|
||||
targetPod.SetName("dont-create-me")
|
||||
targetPod.SetAnnotations(map[string]string{common.AnnotationKeyHook: "PreSync"})
|
||||
liveSvc := test.NewService()
|
||||
liveSvc.SetName("dont-prune-me")
|
||||
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,
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 0)
|
||||
syncCtx.sync()
|
||||
assert.Equal(t, syncCtx.opState.Phase, v1alpha1.OperationSucceeded)
|
||||
}
|
||||
|
||||
func TestPersistRevisionHistory(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Status.OperationState = nil
|
||||
app.Status.History = nil
|
||||
|
||||
defaultProject := &v1alpha1.AppProject{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
Name: "default",
|
||||
},
|
||||
}
|
||||
data := fakeData{
|
||||
apps: []runtime.Object{app, defaultProject},
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
|
||||
// Sync with source unspecified
|
||||
opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
|
||||
Sync: &v1alpha1.SyncOperation{},
|
||||
}}
|
||||
ctrl.appStateManager.SyncAppState(app, opState)
|
||||
// Ensure we record spec.source into sync result
|
||||
assert.Equal(t, app.Spec.Source, opState.SyncResult.Source)
|
||||
|
||||
updatedApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Get(app.Name, v1.GetOptions{})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(updatedApp.Status.History))
|
||||
assert.Equal(t, app.Spec.Source, updatedApp.Status.History[0].Source)
|
||||
assert.Equal(t, "abc123", updatedApp.Status.History[0].Revision)
|
||||
}
|
||||
|
||||
func TestPersistRevisionHistoryRollback(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Status.OperationState = nil
|
||||
app.Status.History = nil
|
||||
defaultProject := &v1alpha1.AppProject{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
Name: "default",
|
||||
},
|
||||
}
|
||||
data := fakeData{
|
||||
apps: []runtime.Object{app, defaultProject},
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
|
||||
// Sync with source specified
|
||||
source := v1alpha1.ApplicationSource{
|
||||
Helm: &v1alpha1.ApplicationSourceHelm{
|
||||
Parameters: []v1alpha1.HelmParameter{
|
||||
{
|
||||
Name: "test",
|
||||
Value: "123",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
|
||||
Sync: &v1alpha1.SyncOperation{
|
||||
Source: &source,
|
||||
},
|
||||
}}
|
||||
ctrl.appStateManager.SyncAppState(app, opState)
|
||||
// Ensure we record opState's source into sync result
|
||||
assert.Equal(t, source, opState.SyncResult.Source)
|
||||
|
||||
updatedApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Get(app.Name, v1.GetOptions{})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(updatedApp.Status.History))
|
||||
assert.Equal(t, source, updatedApp.Status.History[0].Source)
|
||||
assert.Equal(t, "abc123", updatedApp.Status.History[0].Revision)
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
|
||||
# Argo CD - Architectural Overview
|
||||
|
||||

|
||||
|
||||
## Components
|
||||
|
||||
### API Server
|
||||
The API server is a gRPC/REST server which exposes the API consumed by the Web UI, CLI, and CI/CD
|
||||
systems. It has the following responsibilities:
|
||||
* application management and status reporting
|
||||
* invoking of application actions (e.g. manual sync, user-defined actions)
|
||||
* repository and cluster credential management (stored as K8s secrets)
|
||||
* authentication and RBAC enforcement, with eventual integration with external identity providers
|
||||
* listener/forwarder for git webhook events
|
||||
|
||||
### Repository Server
|
||||
The repository server is an internal service which maintains a local cache of the git repository
|
||||
holding the application manifests. It is responsible for generating and returning the Kubernetes
|
||||
manifests when provided the following inputs:
|
||||
* repository URL
|
||||
* git revision (commit, tag, branch)
|
||||
* application path
|
||||
* application environment
|
||||
|
||||
### Application Controller
|
||||
The application controller is 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). It detects out-of-sync application state and optionally takes corrective action. It
|
||||
is responsible for invoking any user-defined handlers (argo workflows) for Sync, OutOfSync events
|
||||
|
||||
### Application CRD (Custom Resource Definition)
|
||||
The Application CRD is the Kubernetes resource object representing a deployed application instance
|
||||
in an environment. It holds a reference to the desired target state (repo, revision, app, environment)
|
||||
of which the application controller will enforce state against.
|
||||
186
docs/CONTRIBUTING.md
Normal file
@@ -0,0 +1,186 @@
|
||||
# Contributing
|
||||
## Before You Start
|
||||
|
||||
You must install and run the ArgoCD using a local Kubernetes (e.g. Docker for Desktop or Minikube) first. This will help you understand the application, but also get your local environment set-up.
|
||||
|
||||
Then, to get a good grounding in Go, try out [the tutorial](https://tour.golang.org/).
|
||||
|
||||
## Pre-requisites
|
||||
|
||||
Install:
|
||||
|
||||
* [docker](https://docs.docker.com/install/#supported-platforms)
|
||||
* [golang](https://golang.org/)
|
||||
* [dep](https://github.com/golang/dep)
|
||||
* [protobuf](https://developers.google.com/protocol-buffers/)
|
||||
* [ksonnet](https://github.com/ksonnet/ksonnet#install)
|
||||
* [helm](https://github.com/helm/helm/releases)
|
||||
* [kustomize](https://github.com/kubernetes-sigs/kustomize/releases)
|
||||
* [go-swagger](https://github.com/go-swagger/go-swagger/blob/master/docs/install.md)
|
||||
* [jq](https://stedolan.github.io/jq/)
|
||||
* [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
|
||||
* [kubectx](https://kubectx.dev)
|
||||
* [minikube](https://kubernetes.io/docs/setup/minikube/) or Docker for Desktop
|
||||
|
||||
Brew users can quickly install the lot:
|
||||
|
||||
```bash
|
||||
brew tap go-swagger/go-swagger
|
||||
brew install go dep protobuf kubectl kubectx ksonnet/tap/ks kubernetes-helm jq go-swagger
|
||||
```
|
||||
|
||||
!!! note "Kustomize"
|
||||
Since Argo CD supports Kustomize v1.0 and v2.0, you will need to install both versions in order for the unit tests to run. The Kustomize 1 unit test expects to find a `kustomize1` binary in the path. You can use this [link](https://github.com/argoproj/argo-cd/blob/master/Dockerfile#L66-L69) to find the Kustomize 1 currently used by Argo CD and modify the curl command to download the correct OS.
|
||||
|
||||
Set up environment variables (e.g. is `~/.bashrc`):
|
||||
|
||||
```bash
|
||||
export GOPATH=~/go
|
||||
export PATH=$PATH:$GOPATH/bin
|
||||
```
|
||||
|
||||
Install go dependencies:
|
||||
|
||||
```bash
|
||||
go get -u github.com/golang/protobuf/protoc-gen-go
|
||||
go get -u github.com/go-swagger/go-swagger/cmd/swagger
|
||||
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
|
||||
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
|
||||
go get -u github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||
go get -u github.com/mattn/goreman
|
||||
go get -u gotest.tools/gotestsum
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
go get -u github.com/argoproj/argo-cd
|
||||
dep ensure
|
||||
make
|
||||
```
|
||||
|
||||
The make command can take a while, and we recommend building the specific component you are working on
|
||||
|
||||
* `make codegen` - Builds protobuf and swagger files
|
||||
* `make cli` - Make the argocd CLI tool
|
||||
* `make server` - Make the API/repo/controller server
|
||||
* `make argocd-util` - Make the administrator's utility, used for certain tasks such as import/export
|
||||
|
||||
## Running Tests
|
||||
|
||||
To run unit tests:
|
||||
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
|
||||
Check out the following [documentation](https://github.com/argoproj/argo-cd/blob/master/docs/developer-guide/test-e2e.md) for instructions on running the e2e tests.
|
||||
|
||||
## Running Locally
|
||||
|
||||
It is much easier to run and debug if you run ArgoCD on your local machine than in the Kubernetes cluster.
|
||||
|
||||
You should scale the deployments to zero:
|
||||
|
||||
```bash
|
||||
kubectl -n argocd scale deployment.extensions/argocd-application-controller --replicas 0
|
||||
kubectl -n argocd scale deployment.extensions/argocd-dex-server --replicas 0
|
||||
kubectl -n argocd scale deployment.extensions/argocd-repo-server --replicas 0
|
||||
kubectl -n argocd scale deployment.extensions/argocd-server --replicas 0
|
||||
kubectl -n argocd scale deployment.extensions/argocd-redis --replicas 0
|
||||
```
|
||||
|
||||
Then checkout and build the UI next to your code
|
||||
|
||||
```
|
||||
cd ~/go/src/github.com/argoproj
|
||||
git clone git@github.com:argoproj/argo-cd-ui.git
|
||||
```
|
||||
|
||||
Follow the UI's [README](https://github.com/argoproj/argo-cd-ui/blob/master/README.md) to build it.
|
||||
|
||||
Note: you'll need to use the https://localhost:6443 cluster now.
|
||||
|
||||
Then start the services:
|
||||
|
||||
```bash
|
||||
cd ~/go/src/github.com/argoproj/argo-cd
|
||||
make start
|
||||
```
|
||||
|
||||
You can now execute `argocd` command against your locally running ArgoCD by appending `--server localhost:8080 --plaintext --insecure`, e.g.:
|
||||
|
||||
```bash
|
||||
argocd app set guestbook --path guestbook --repo https://github.com/argoproj/argocd-example-apps.git --dest-server https://localhost:6443 --dest-namespace default --server localhost:8080 --plaintext --insecure
|
||||
```
|
||||
|
||||
You can open the UI: http://localhost:8080
|
||||
|
||||
Note: you'll need to use the https://kubernetes.default.svc cluster now.
|
||||
|
||||
## Running Local Containers
|
||||
|
||||
You may need to run containers locally, so here's how:
|
||||
|
||||
Create login to Docker Hub, then login.
|
||||
|
||||
```bash
|
||||
docker login
|
||||
```
|
||||
|
||||
Add your username as the environment variable, e.g. to your `~/.bash_profile`:
|
||||
|
||||
```bash
|
||||
export IMAGE_NAMESPACE=alexcollinsintuit
|
||||
```
|
||||
|
||||
If you have not built the UI image (see [the UI README](https://github.com/argoproj/argo-cd-ui/blob/master/README.md)), then do the following:
|
||||
|
||||
```bash
|
||||
docker pull argoproj/argocd-ui:latest
|
||||
docker tag argoproj/argocd-ui:latest $IMAGE_NAMESPACE/argocd-ui:latest
|
||||
docker push $IMAGE_NAMESPACE/argocd-ui:latest
|
||||
```
|
||||
|
||||
Build the images:
|
||||
|
||||
```bash
|
||||
DOCKER_PUSH=true make image
|
||||
```
|
||||
|
||||
Update the manifests:
|
||||
|
||||
```bash
|
||||
make manifests
|
||||
```
|
||||
|
||||
Install the manifests:
|
||||
|
||||
```bash
|
||||
kubectl -n argocd apply --force -f manifests/install.yaml
|
||||
```
|
||||
|
||||
Scale your deployments up:
|
||||
|
||||
```bash
|
||||
kubectl -n argocd scale deployment.extensions/argocd-application-controller --replicas 1
|
||||
kubectl -n argocd scale deployment.extensions/argocd-dex-server --replicas 1
|
||||
kubectl -n argocd scale deployment.extensions/argocd-repo-server --replicas 1
|
||||
kubectl -n argocd scale deployment.extensions/argocd-server --replicas 1
|
||||
kubectl -n argocd scale deployment.extensions/argocd-redis --replicas 1
|
||||
```
|
||||
|
||||
Now you can set-up the port-forwarding and open the UI or CLI.
|
||||
|
||||
## Pre-commit Checks
|
||||
|
||||
Before you commit, make sure you've formatted and linted your code, or your PR will fail CI:
|
||||
|
||||
```bash
|
||||
STAGED_GO_FILES=$(git diff --cached --name-only | grep ".go$")
|
||||
|
||||
gofmt -w $STAGED_GO_FILES
|
||||
|
||||
make codgen
|
||||
make precommit ;# lint and test
|
||||
```
|
||||
@@ -1,77 +0,0 @@
|
||||
# Argo CD Getting Started
|
||||
|
||||
An example Ksonnet guestbook application is provided to demonstrates how Argo CD works.
|
||||
|
||||
## Requirements
|
||||
* Installed [minikube](https://github.com/kubernetes/minikube#installation)
|
||||
* Installed the [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) command-line tool
|
||||
* Have a [kubeconfig](https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/) file (default location is `~/.kube/config`).
|
||||
|
||||
## 1. Download Argo CD
|
||||
|
||||
Download the latest Argo CD version from the [releases](https://github.com/argoproj/argo-cd/releases) page.
|
||||
|
||||
## 2. Install the Argo CD components
|
||||
```
|
||||
$ argocd install
|
||||
```
|
||||
This will create a new namespace, `argocd`, where Argo CD services and application resources will live.
|
||||
|
||||
## 3. Open access to Argo CD API server
|
||||
|
||||
By default, the Argo CD API server is not exposed with an external IP. To expose the API server,
|
||||
change service type to `LoadBalancer`:
|
||||
|
||||
```
|
||||
$ kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "LoadBalancer"}}'
|
||||
```
|
||||
|
||||
Export API server URL into `ARGOCD_SERVER` environment variable, which the CLI looks to for
|
||||
connection information:
|
||||
|
||||
```
|
||||
$ export ARGOCD_SERVER=$(minikube service argocd-server -n argocd --url | cut -d'/' -f 3)
|
||||
```
|
||||
|
||||
Now, Argo CD is able to talk to API server and you can deploy your first application.
|
||||
|
||||
## 4. Connect and deploy the Guestbook application
|
||||
|
||||
1. Register the minikube cluster to Argo CD:
|
||||
|
||||
```
|
||||
$ argocd cluster add minikube
|
||||
```
|
||||
The `argocd cluster add CONTEXT` command installs an `argocd-manager` ServiceAccount and ClusterRole into
|
||||
the cluster associated with the supplied kubectl context. Argo CD then uses the associated service account
|
||||
token to perform its required management tasks (i.e. deploy/monitoring).
|
||||
|
||||
2. Add the guestbook application and github repository containing the Guestbook application
|
||||
|
||||
```
|
||||
$ argocd app add --name guestbook --repo https://github.com/argoproj/argo-cd.git --path examples/guestbook --env minikube --dest-server https://$(minikube ip):8443
|
||||
```
|
||||
|
||||
Once the application is added, you can now see its status:
|
||||
|
||||
```
|
||||
$ argocd app list
|
||||
$ argocd app sync guestbook
|
||||
```
|
||||
|
||||
The application status is initially in an `OutOfSync` state, since the application has yet to be
|
||||
deployed, and no Kubernetes resouces have been created. To sync (deploy) the application, run:
|
||||
|
||||
```
|
||||
$ argocd app sync guestbook
|
||||
```
|
||||
|
||||
[](https://asciinema.org/a/uYnbFMy5WI2rc9S49oEAyGLb0)
|
||||
|
||||
Argo CD also allows to view and manager applications using web UI. Get the web UI URL by running:
|
||||
|
||||
```
|
||||
$ minikube service argocd-server -n argocd --url
|
||||
```
|
||||
|
||||

|
||||
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.slack.com/messages/CASHNF6MS).
|
||||
4. [Read issues, report a bug, or request a feature ⧉](https://github.com/argoproj/argo-cd/issues)
|
||||
|
Before Width: | Height: | Size: 109 KiB |
BIN
docs/assets/argocd-ui.gif
Normal file
|
After Width: | Height: | Size: 3.5 MiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 119 KiB |
BIN
docs/assets/connect_repo.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
docs/assets/create_app.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
docs/assets/dashboard.jpg
Normal file
|
After Width: | Height: | Size: 321 KiB |
BIN
docs/assets/guestbook-app.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
docs/assets/guestbook-tree.png
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
docs/assets/logo.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
docs/assets/oauth2-config.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
docs/assets/register-app.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
docs/assets/select_app.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
docs/assets/select_env.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
docs/assets/select_repo.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
docs/assets/webhook-config.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
16
docs/core_concepts.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Core Concepts
|
||||
|
||||
Let's assume you're familiar with core Git, Docker, Kubernetes, Continuous Delivery, and GitOps concepts.
|
||||
|
||||
* **Application** A group of Kubernetes resources as defined by a manifest. This is a Custom Resource Definition (CRD).
|
||||
* **Application source type** Which **Tool** is used to build the application.
|
||||
* **Target state** The desired state of an application, as represented by files in a Git repository.
|
||||
* **Live state** The live state of that application. What pods etc are deployed.
|
||||
* **Sync status** Whether or not the live state matches the target state. Is the deployed application the same as Git says it should be?
|
||||
* **Sync** The process of making an application move to its target state. E.g. by applying changes to a Kubernetes cluster.
|
||||
* **Sync operation status** Whether or not a sync succeeded.
|
||||
* **Refresh** Compare the latest code in Git with the live state. Figure out what is different.
|
||||
* **Health** The health the application, is it running correctly? Can it serve requests?
|
||||
* **Tool** A tool to create manifests from a directory of files. E.g. Kustomize or Ksonnet. See **Application Source Type**.
|
||||
* **Configuration management tool** See **Tool**.
|
||||
* **Configuration management plugin** A custom tool.
|
||||
3
docs/developer-guide/api-docs.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# API Docs
|
||||
|
||||
You can find Swagger docs but setting the path `/swagger-ui` to your Argo CD UI's. E.g. [http://localhost:8080/swagger-ui](http://localhost:8080/swagger-ui).
|
||||
10
docs/developer-guide/index.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Overview
|
||||
|
||||
!!! warning "You probably don't want to be reading this section of the docs."
|
||||
This part of the manual is aimed at people wanting to develop third-party applications that interact with Argo CD, e.g.
|
||||
|
||||
* An chat bot
|
||||
* An Slack integration
|
||||
|
||||
!!! note
|
||||
Please make sure you've completed the [getting started guide](../getting_started.md).
|
||||
48
docs/developer-guide/releasing.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Releasing
|
||||
|
||||
1. Tag, build, and push argo-cd-ui
|
||||
```bash
|
||||
cd argo-cd-ui
|
||||
git checkout -b release-X.Y
|
||||
git tag vX.Y.Z
|
||||
git push upstream release-X.Y --tags
|
||||
IMAGE_NAMESPACE=argoproj IMAGE_TAG=vX.Y.Z DOCKER_PUSH=true yarn docker
|
||||
```
|
||||
|
||||
2. Create release-X.Y branch (if creating initial X.Y release)
|
||||
```bash
|
||||
git checkout -b release-X.Y
|
||||
git push upstream release-X.Y
|
||||
```
|
||||
|
||||
3. Update VERSION and manifests with new version
|
||||
```bash
|
||||
vi VERSION # ensure value is desired X.Y.Z semantic version
|
||||
make manifests IMAGE_TAG=vX.Y.Z
|
||||
git commit -a -m "Update manifests to vX.Y.Z"
|
||||
git push upstream release-X.Y
|
||||
```
|
||||
|
||||
4. Tag, build, and push release to docker hub
|
||||
```bash
|
||||
git tag vX.Y.Z
|
||||
make release IMAGE_NAMESPACE=argoproj IMAGE_TAG=vX.Y.Z DOCKER_PUSH=true
|
||||
git push upstream vX.Y.Z
|
||||
```
|
||||
|
||||
5. Update argocd brew formula
|
||||
```bash
|
||||
git clone https://github.com/argoproj/homebrew-tap
|
||||
cd homebrew-tap
|
||||
./update.sh ~/go/src/github.com/argoproj/argo-cd/dist/argocd-darwin-amd64
|
||||
git commit -a -m "Update argocd to vX.Y.Z"
|
||||
git push
|
||||
```
|
||||
|
||||
6. Update documentation:
|
||||
* Edit CHANGELOG.md with release notes
|
||||
* Update `stable` tag
|
||||
```
|
||||
git tag stable --force && git push upstream stable --force
|
||||
```
|
||||
* Create GitHub release from new tag and upload binaries (e.g. dist/argocd-darwin-amd64)
|
||||
30
docs/developer-guide/site.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Site
|
||||
|
||||
## Developing And Testing
|
||||
|
||||
The web site is build using `mkdocs` and `mkdocs-material`.
|
||||
|
||||
To test:
|
||||
|
||||
```bash
|
||||
mkdocs serve
|
||||
```
|
||||
|
||||
Check for broken external links:
|
||||
|
||||
```bash
|
||||
find docs -name '*.md' -exec grep -l http {} + | xargs awesome_bot -t 3 --allow-dupe --allow-redirect -w argocd.example.com:443,argocd.example.com,kubernetes.default.svc:443,kubernetes.default.svc,mycluster.com,https://github.com/argoproj/my-private-repository,192.168.0.20,storage.googleapis.com,localhost:8080,localhost:6443,your-kubernetes-cluster-addr,10.97.164.88 --skip-save-results --
|
||||
```
|
||||
|
||||
## Deploying
|
||||
|
||||
```bash
|
||||
mkdocs gh-deploy
|
||||
```
|
||||
|
||||
## Analytics
|
||||
|
||||
!!! tip
|
||||
Don't forget to disable your ad-blocker when testing.
|
||||
|
||||
We collect [Google Analytics](https://analytics.google.com/analytics/web/#/report-home/a105170809w198079555p192782995).
|
||||
19
docs/developer-guide/test-e2e.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# E2E Tests
|
||||
|
||||
The directory contains E2E tests and test applications. The test assume that Argo CD services are installed into `argocd-e2e` namespace or cluster in current context. One throw-away
|
||||
namespace `argocd-e2e***` is created prior to tests execute. The throw-away namespace is used as a target namespace for test applications.
|
||||
|
||||
The `test/e2e/testdata` directory contains various Argo CD applications. Before test execution directory is copies into `/tmp/argocd-e2e***` temp directory and used in tests as a
|
||||
Git repository via file url: `file:///tmp/argocd-e2e***`.
|
||||
|
||||
## Running Tests Locally
|
||||
|
||||
1. Start the e2e version `make start-e2e`
|
||||
1. Run the tests: `make test-e2e`
|
||||
|
||||
You can observe the tests by using the UI [http://localhost:8080/applications](http://localhost:8080/applications).
|
||||
|
||||
## CI Set-up
|
||||
|
||||
The tests are executed by Argo Workflow defined at `.argo-ci/ci.yaml`. CI job The builds an Argo CD image, deploy argo cd components into throw-away kubernetes cluster provisioned
|
||||
using k3s and run e2e tests against it.
|
||||
58
docs/faq.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# FAQ
|
||||
|
||||
## Why is my application still `OutOfSync` immediately after a successful Sync?
|
||||
|
||||
See [Diffing](user-guide/diffing.md) documentation for reasons resources can be OutOfSync, and ways to configure
|
||||
Argo CD to ignore fields when differences are expected.
|
||||
|
||||
|
||||
## Why is my application stuck in `Progressing` state?
|
||||
|
||||
Argo CD provides health for several standard Kubernetes types. The `Ingress` and `StatefulSet` types have known issues which might cause health check
|
||||
to return `Progressing` state instead of `Healthy`.
|
||||
|
||||
* `Ingress` is considered healthy if `status.loadBalancer.ingress` list is non-empty, with at least one value for `hostname` or `IP`. Some ingress controllers
|
||||
([contour](https://github.com/heptio/contour/issues/403), [traefik](https://github.com/argoproj/argo-cd/issues/968#issuecomment-451082913)) don't update
|
||||
`status.loadBalancer.ingress` field which causes `Ingress` to stuck in `Progressing` state forever.
|
||||
|
||||
* `StatufulSet` is considered healthy if value of `status.updatedReplicas` field matches to `spec.replicas` field. Due to Kubernetes bug
|
||||
[kubernetes/kubernetes#68573](https://github.com/kubernetes/kubernetes/issues/68573) the `status.updatedReplicas` is not populated. So unless you run Kubernetes version which
|
||||
include the fix [kubernetes/kubernetes#67570](https://github.com/kubernetes/kubernetes/pull/67570) `StatefulSet` might stay in `Progressing` state.
|
||||
|
||||
As workaround Argo CD allows providing [health check](operator-manual/health.md) customization which overrides default behavior.
|
||||
|
||||
## I forgot the admin password, how do I reset it?
|
||||
|
||||
Edit the `argocd-secret` secret and update the `admin.password` field with a new bcrypt hash. You
|
||||
can use a site like https://www.browserling.com/tools/bcrypt to generate a new hash. Another option
|
||||
is to delete both the `admin.password` and `admin.passwordMtime` keys and restart argocd-server.
|
||||
|
||||
## Argo CD cannot deploy Helm Chart based applications without internet access, how can I solve it?
|
||||
|
||||
Argo CD might fail to generate Helm chart manifests if the chart has dependencies located in external repositories. To solve the problem you need to make sure that `requirements.yaml`
|
||||
uses only internally available Helm repositories. Even if the chart uses only dependencies from internal repos Helm might decide to refresh `stable` repo. As workaround override
|
||||
`stable` repo URL in `argocd-cm` config map:
|
||||
|
||||
```yaml
|
||||
data:
|
||||
helm.repositories: |
|
||||
- url: http://<internal-helm-repo-host>:8080
|
||||
name: stable
|
||||
```
|
||||
|
||||
## I've configured [cluster secret](./operator-manual/declarative-setup.md#clusters) but it does not show up in CLI/UI, how do I fix it?
|
||||
|
||||
Check if cluster secret has `argocd.argoproj.io/secret-type: cluster` label. If secret has the label but the cluster is still not visible then make sure it might be a
|
||||
permission issue. Try to list clusters using `admin` user (e.g. `argocd login --username admin && argocd cluster list`).
|
||||
|
||||
## Argo CD is unable to connect to my cluster, how do I troubleshoot it?
|
||||
|
||||
Use the following steps to reconstruct configured cluster config and connect to your cluster manually using kubectl:
|
||||
|
||||
```bash
|
||||
kubectl exec -it <argocd-pod-name> bash # ssh into any argocd server pod
|
||||
argocd-util kubeconfig https://<cluster-url> /tmp/config --namespace argocd # generate your cluster config
|
||||
KUBECONFIG=/tmp/config kubectl get pods # test connection manually
|
||||
```
|
||||
|
||||
Now you can manually verify that cluster is accessible from the Argo CD pod.
|
||||
175
docs/getting_started.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Getting Started
|
||||
|
||||
!!! tip
|
||||
This guide assumes you have a grounding in the tools that Argo CD is based on. Please read the [understanding the basics](understand_the_basics.md).
|
||||
|
||||
## Requirements
|
||||
|
||||
* Installed [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) command-line tool
|
||||
* Have a [kubeconfig](https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/) file (default location is `~/.kube/config`).
|
||||
|
||||
## 1. Install Argo CD
|
||||
|
||||
```bash
|
||||
kubectl create namespace argocd
|
||||
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
|
||||
```
|
||||
|
||||
This will create a new namespace, `argocd`, where Argo CD services and application resources will live.
|
||||
|
||||
On GKE, you will need grant your account the ability to create new cluster roles:
|
||||
|
||||
```bash
|
||||
kubectl create clusterrolebinding YOURNAME-cluster-admin-binding --clusterrole=cluster-admin --user=YOUREMAIL@gmail.com
|
||||
```
|
||||
|
||||
## 2. Download Argo CD CLI
|
||||
|
||||
Download the latest Argo CD version from [https://github.com/argoproj/argo-cd/releases/latest].
|
||||
|
||||
Also available in Mac Homebrew:
|
||||
|
||||
```bash
|
||||
brew tap argoproj/tap
|
||||
brew install argoproj/tap/argocd
|
||||
```
|
||||
|
||||
|
||||
## 3. Access The Argo CD API Server
|
||||
|
||||
By default, the Argo CD API server is not exposed with an external IP. To access the API server,
|
||||
choose one of the following techniques to expose the Argo CD API server:
|
||||
|
||||
### Service Type Load Balancer
|
||||
Change the argocd-server service type to `LoadBalancer`:
|
||||
|
||||
```bash
|
||||
kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "LoadBalancer"}}'
|
||||
```
|
||||
|
||||
### Ingress
|
||||
Follow the [ingress documentation](operator-manual/ingress.md) on how to configure Argo CD with ingress.
|
||||
|
||||
### Port Forwarding
|
||||
Kubectl port-forwarding can also be used to connect to the API server without exposing the service.
|
||||
|
||||
```bash
|
||||
kubectl port-forward svc/argocd-server -n argocd 8080:443
|
||||
```
|
||||
|
||||
The API server can then be accessed using the localhost:8080
|
||||
|
||||
|
||||
## 4. Login Using The CLI
|
||||
|
||||
Login as the `admin` user. The initial password is autogenerated to be the pod name of the
|
||||
Argo CD API server. This can be retrieved with the command:
|
||||
|
||||
```bash
|
||||
kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-server -o name | cut -d'/' -f 2
|
||||
```
|
||||
|
||||
Using the above password, login to Argo CD's IP or hostname:
|
||||
|
||||
```bash
|
||||
argocd login <ARGOCD_SERVER>
|
||||
```
|
||||
|
||||
Change the password using the command:
|
||||
|
||||
```bash
|
||||
argocd account update-password
|
||||
```
|
||||
|
||||
|
||||
## 5. Register A Cluster To Deploy Apps To (Optional)
|
||||
|
||||
This step registers a cluster's credentials to Argo CD, and is only necessary when deploying to
|
||||
an external cluster. When deploying internally (to the same cluster that Argo CD is running in),
|
||||
https://kubernetes.default.svc should be used as the application's K8s API server address.
|
||||
|
||||
First list all clusters contexts in your current kubconfig:
|
||||
```bash
|
||||
argocd cluster add
|
||||
```
|
||||
|
||||
Choose a context name from the list and supply it to `argocd cluster add CONTEXTNAME`. For example,
|
||||
for docker-for-desktop context, run:
|
||||
```bash
|
||||
argocd cluster add docker-for-desktop
|
||||
```
|
||||
|
||||
The above command installs a ServiceAccount (`argocd-manager`), into the kube-system namespace of
|
||||
that kubectl context, and binds the service account to an admin-level ClusterRole. Argo CD uses this
|
||||
service account token to perform its management tasks (i.e. deploy/monitoring).
|
||||
|
||||
!!! note
|
||||
The rules of the `argocd-manager-role` role can be modified such that it only has `create`, `update`, `patch`, `delete` privileges to a limited set of namespaces, groups, kinds.
|
||||
However `get`, `list`, `watch` privileges are required at the cluster-scope for Argo CD to function.
|
||||
|
||||
## 6. Create An Application From A Git Repository
|
||||
|
||||
An example repository containing a guestbook application is available at
|
||||
https://github.com/argoproj/argocd-example-apps.git to demonstrate how Argo CD works.
|
||||
|
||||
### Creating Apps Via CLI
|
||||
|
||||
~~~bash
|
||||
argocd app create guestbook \
|
||||
--repo https://github.com/argoproj/argocd-example-apps.git \
|
||||
--path guestbook \
|
||||
--dest-server https://kubernetes.default.svc \
|
||||
--dest-namespace default
|
||||
~~~
|
||||
|
||||
### Creating Apps Via UI
|
||||
|
||||
Open a browser to the Argo CD external UI, and login using the credentials, IP/hostname set in step 4.
|
||||
Connect the https://github.com/argoproj/argocd-example-apps.git repo to Argo CD:
|
||||
|
||||

|
||||
|
||||
After connecting a repository, select the guestbook application for creation:
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
## 7. Sync (Deploy) The Application
|
||||
|
||||
Once the guestbook application is created, you can now view its status:
|
||||
|
||||
```bash
|
||||
$ argocd app get guestbook
|
||||
Name: guestbook
|
||||
Server: https://kubernetes.default.svc
|
||||
Namespace: default
|
||||
URL: https://10.97.164.88/applications/guestbook
|
||||
Repo: https://github.com/argoproj/argocd-example-apps.git
|
||||
Target:
|
||||
Path: guestbook
|
||||
Sync Policy: <none>
|
||||
Sync Status: OutOfSync from (1ff8a67)
|
||||
Health Status: Missing
|
||||
|
||||
GROUP KIND NAMESPACE NAME STATUS HEALTH
|
||||
apps Deployment default guestbook-ui OutOfSync Missing
|
||||
Service default guestbook-ui OutOfSync Missing
|
||||
```
|
||||
|
||||
The application status is initially `OutOfSync` state, since the application has yet to be
|
||||
deployed, and no Kubernetes resources have been created. To sync (deploy) the application, run:
|
||||
|
||||
```bash
|
||||
argocd app sync guestbook
|
||||
```
|
||||
|
||||
This command retrieves the manifests from the repository and performs a `kubectl apply` of the
|
||||
manifests. The guestbook app is now running and you can now view its resource components, logs,
|
||||
events, and assessed health status:
|
||||
|
||||
### From UI:
|
||||
|
||||

|
||||

|
||||
|
||||