Compare commits
546 Commits
release-1.
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebbc1d02f5 | ||
|
|
9b0631b6c6 | ||
|
|
eb3d1fb84b | ||
|
|
e97b643526 | ||
|
|
b51ba85aae | ||
|
|
be72e7cc42 | ||
|
|
f6e9e41d7b | ||
|
|
087b8b2fd5 | ||
|
|
6dbbb18aa9 | ||
|
|
62b9b3aeb5 | ||
|
|
31110cde4d | ||
|
|
d6c5c72eb4 | ||
|
|
0b3333ef4b | ||
|
|
d0f8edfec8 | ||
|
|
b1ff29fdf9 | ||
|
|
785bb9ecce | ||
|
|
b57579c4ae | ||
|
|
6b53ac785e | ||
|
|
e38920f570 | ||
|
|
28aea3dfde | ||
|
|
fe59190a96 | ||
|
|
0a04a491d9 | ||
|
|
701ce05393 | ||
|
|
965825f752 | ||
|
|
bd73326b8a | ||
|
|
502b8944c4 | ||
|
|
f5b0db240b | ||
|
|
ce43b7a438 | ||
|
|
ebcfea64ff | ||
|
|
14c3dd2c59 | ||
|
|
4359d345a0 | ||
|
|
7081068a2d | ||
|
|
0f9c684278 | ||
|
|
3ea3c13665 | ||
|
|
13fed83ec6 | ||
|
|
94017f2c8d | ||
|
|
3c53ea6cff | ||
|
|
7b2946962d | ||
|
|
df3422798d | ||
|
|
f44855fa4d | ||
|
|
1f4a052da3 | ||
|
|
74f5eae750 | ||
|
|
36a9465d85 | ||
|
|
9e6c04700e | ||
|
|
8abe96ad9a | ||
|
|
10de6e7cfc | ||
|
|
e57071a150 | ||
|
|
41db5fc010 | ||
|
|
31f257e957 | ||
|
|
5fd93a7db5 | ||
|
|
c2547dca95 | ||
|
|
522ed90f38 | ||
|
|
7a0266f0fb | ||
|
|
30348417a9 | ||
|
|
910eddbbf3 | ||
|
|
f150ba18fb | ||
|
|
97d030b4b2 | ||
|
|
5df269b3fa | ||
|
|
775f9711e7 | ||
|
|
3f974825a6 | ||
|
|
1d55439f7f | ||
|
|
41daf71851 | ||
|
|
f4796398a8 | ||
|
|
0f0b6ce278 | ||
|
|
aee4dfaa1e | ||
|
|
74a92c6031 | ||
|
|
4237c6f00f | ||
|
|
0f3d74fa58 | ||
|
|
868516f0bd | ||
|
|
e50c43fb3f | ||
|
|
9b6a0dc3cd | ||
|
|
1f63e99b78 | ||
|
|
589ad5d2ac | ||
|
|
4464f99df7 | ||
|
|
65a2d9f1ff | ||
|
|
bcaabc51a1 | ||
|
|
9140fea0fb | ||
|
|
d83e0ddb56 | ||
|
|
25ca589bff | ||
|
|
6e6f4f50a0 | ||
|
|
ed547aa545 | ||
|
|
ca7fa55a2b | ||
|
|
7ee951b5b8 | ||
|
|
be8308199c | ||
|
|
d8c08bfe7d | ||
|
|
231509bb3c | ||
|
|
a2d6582e54 | ||
|
|
4b23918802 | ||
|
|
51f2949883 | ||
|
|
ffa824bbba | ||
|
|
e4e503aad7 | ||
|
|
e8e810934d | ||
|
|
762b33c819 | ||
|
|
69d8831b38 | ||
|
|
ad8715cbad | ||
|
|
6cadaa2a5d | ||
|
|
8b1a118cdb | ||
|
|
ccb7371047 | ||
|
|
2f72f3adad | ||
|
|
ae17c70b00 | ||
|
|
5921feda5f | ||
|
|
71bd3fdd24 | ||
|
|
30816bc549 | ||
|
|
0681c2754a | ||
|
|
5040d6f080 | ||
|
|
e3b733627f | ||
|
|
be513e431a | ||
|
|
5e24d21ae8 | ||
|
|
eec8f79923 | ||
|
|
d1ba640bff | ||
|
|
7947b59eeb | ||
|
|
4c3f97f78a | ||
|
|
c4dcae3442 | ||
|
|
d1a36e5b6d | ||
|
|
9c51838ccc | ||
|
|
700a4104c6 | ||
|
|
dcb5f07c23 | ||
|
|
9625e50ccd | ||
|
|
3e19b2fdf1 | ||
|
|
96e0f0d3be | ||
|
|
620e31e52d | ||
|
|
ab7e1773f0 | ||
|
|
e67d934827 | ||
|
|
9ee0d2c6c0 | ||
|
|
9c684ddc08 | ||
|
|
c9f3c64a58 | ||
|
|
8c3a8e3655 | ||
|
|
449b50cf6c | ||
|
|
da3ab59be0 | ||
|
|
e8f63d4583 | ||
|
|
cdb3df1077 | ||
|
|
858676c4f8 | ||
|
|
44c31e278c | ||
|
|
a759601264 | ||
|
|
9bbbda55a4 | ||
|
|
b97f4f7f8e | ||
|
|
422a26e8d8 | ||
|
|
245e1ee636 | ||
|
|
2166fea351 | ||
|
|
dea75eb481 | ||
|
|
6ef89e3c09 | ||
|
|
b9954e55ac | ||
|
|
86031504af | ||
|
|
97003caebc | ||
|
|
42ebb227e1 | ||
|
|
c7f7631f2e | ||
|
|
9cdfe40faf | ||
|
|
21304ee2c5 | ||
|
|
7f0ffb4cd2 | ||
|
|
22e0b4ff55 | ||
|
|
0767dff025 | ||
|
|
5ba8710ff1 | ||
|
|
dde5f143fc | ||
|
|
f02115af15 | ||
|
|
cd302fd055 | ||
|
|
cfd59aded2 | ||
|
|
dd856e1c2b | ||
|
|
387f775f4a | ||
|
|
837ed45361 | ||
|
|
46ee2f21a2 | ||
|
|
3bf9deb15e | ||
|
|
a96b476f16 | ||
|
|
764ea07fc0 | ||
|
|
6ddd98c4f8 | ||
|
|
be60425a47 | ||
|
|
0850bcc184 | ||
|
|
aaae4003a0 | ||
|
|
6535d1ac34 | ||
|
|
81b84e66c1 | ||
|
|
5fdbe2057a | ||
|
|
ac8d18d39d | ||
|
|
3d39accdb2 | ||
|
|
4d643a151d | ||
|
|
41ab92fbdd | ||
|
|
2e06118792 | ||
|
|
bbfbf6834f | ||
|
|
c7dbe4883b | ||
|
|
bdee71d4c7 | ||
|
|
9af729c738 | ||
|
|
c0f9c9ae93 | ||
|
|
dfc75df7a0 | ||
|
|
cf03c1dcc5 | ||
|
|
7824a1fc2b | ||
|
|
5db8d97bf0 | ||
|
|
05c493b3a6 | ||
|
|
894f95dce5 | ||
|
|
90227f226d | ||
|
|
fd482316d0 | ||
|
|
c620fa7aaa | ||
|
|
a0f6e033c0 | ||
|
|
15b0a6e793 | ||
|
|
3408e2d72d | ||
|
|
e09cacba65 | ||
|
|
66d86fe56f | ||
|
|
67c91564c4 | ||
|
|
698712f396 | ||
|
|
001d990d0c | ||
|
|
1cbada9d86 | ||
|
|
dff7da7271 | ||
|
|
12957a494c | ||
|
|
4f1e371830 | ||
|
|
8e11facb94 | ||
|
|
61c8f73e21 | ||
|
|
23ac24bdea | ||
|
|
e6f116319b | ||
|
|
5fa808a788 | ||
|
|
6d64280fba | ||
|
|
3ae1d13dfd | ||
|
|
f512d213cf | ||
|
|
35914ff7ab | ||
|
|
48891e2536 | ||
|
|
a88c729148 | ||
|
|
c8ca3e7c45 | ||
|
|
2a0012d5f1 | ||
|
|
fd483babb7 | ||
|
|
52f4ed203f | ||
|
|
497cd603ca | ||
|
|
97f094756d | ||
|
|
5cdcca4544 | ||
|
|
d479d22de7 | ||
|
|
303925f4a0 | ||
|
|
8eb3306064 | ||
|
|
9f2eab665b | ||
|
|
3ac0bc36d4 | ||
|
|
2b84672641 | ||
|
|
701dda9a28 | ||
|
|
8995d0405a | ||
|
|
286f98ba82 | ||
|
|
10f68dde2d | ||
|
|
5592150f18 | ||
|
|
7af7f30715 | ||
|
|
4534bd2725 | ||
|
|
95d19cdcca | ||
|
|
05b70f1e97 | ||
|
|
da57c9f1c8 | ||
|
|
287e8cffdb | ||
|
|
f93da5346c | ||
|
|
aefa739169 | ||
|
|
3123c00a85 | ||
|
|
efb7028d84 | ||
|
|
3848f64807 | ||
|
|
5af0c5ad3a | ||
|
|
24927c4d4f | ||
|
|
91e62bfc3b | ||
|
|
b08f895d9a | ||
|
|
3409e0728f | ||
|
|
dd63715017 | ||
|
|
0d2fc86330 | ||
|
|
8299e99049 | ||
|
|
7b60548e8a | ||
|
|
0226190ef4 | ||
|
|
25823a4625 | ||
|
|
712df19fac | ||
|
|
bccaefdac9 | ||
|
|
53d50df001 | ||
|
|
8b2e05c20d | ||
|
|
9bb9c19e67 | ||
|
|
6b106768a5 | ||
|
|
4810874348 | ||
|
|
d96083c293 | ||
|
|
f815c96605 | ||
|
|
4347a3c0ad | ||
|
|
3f7d60018f | ||
|
|
b7c2002a11 | ||
|
|
c1ee89b502 | ||
|
|
74f5043e87 | ||
|
|
fbfa89d358 | ||
|
|
1c95c90a2d | ||
|
|
9f47a11621 | ||
|
|
e28a3e5ed0 | ||
|
|
52cae98705 | ||
|
|
3850e80040 | ||
|
|
989f5c80c6 | ||
|
|
82340a0740 | ||
|
|
28e60406a8 | ||
|
|
eb0d018c31 | ||
|
|
850de2021a | ||
|
|
b8d1b9bbc0 | ||
|
|
7caa2106ef | ||
|
|
24b5c1e34d | ||
|
|
3aa0748c70 | ||
|
|
1462ab3c06 | ||
|
|
5de3a302fb | ||
|
|
14fa7f954c | ||
|
|
1b3d7a02e1 | ||
|
|
eb13305984 | ||
|
|
3a30a4fc74 | ||
|
|
d4ddd51602 | ||
|
|
95eda65759 | ||
|
|
c26573369d | ||
|
|
c91acc0673 | ||
|
|
51a5795b44 | ||
|
|
9aae99cf7f | ||
|
|
5f680d6cec | ||
|
|
28a76352c5 | ||
|
|
cfb925c0d4 | ||
|
|
4e6d8cc1d2 | ||
|
|
f215233af4 | ||
|
|
b1e3036bc2 | ||
|
|
df4002a987 | ||
|
|
96035d3b51 | ||
|
|
89ee234634 | ||
|
|
ff2aa41539 | ||
|
|
85b27e6deb | ||
|
|
d60486fb47 | ||
|
|
41ca6b2ada | ||
|
|
d9f4e224a0 | ||
|
|
29c1095a2d | ||
|
|
0473ed8104 | ||
|
|
706205958c | ||
|
|
e5dde6eefc | ||
|
|
aa2762a9b7 | ||
|
|
6959f032f0 | ||
|
|
1dfe670d1c | ||
|
|
76782a0270 | ||
|
|
b3be910465 | ||
|
|
34c4aa42d6 | ||
|
|
89ebf5a906 | ||
|
|
bae5f93590 | ||
|
|
8f7eabefd4 | ||
|
|
8fa2c7f43f | ||
|
|
d3eb8e9590 | ||
|
|
ad38421b76 | ||
|
|
92bbcf15e8 | ||
|
|
10dc3ac12a | ||
|
|
2f5e45490c | ||
|
|
e10c20f683 | ||
|
|
bc537c1e87 | ||
|
|
a482546112 | ||
|
|
7ead93458e | ||
|
|
dc6d88950c | ||
|
|
c44074d4d6 | ||
|
|
016c8b333a | ||
|
|
cc1592eb0c | ||
|
|
ea9b0b35d0 | ||
|
|
44623d6be2 | ||
|
|
f625ddc6b9 | ||
|
|
324a336a52 | ||
|
|
d09bd23cf8 | ||
|
|
90eb262f64 | ||
|
|
c14f87d565 | ||
|
|
fca0f69b5e | ||
|
|
edf2904004 | ||
|
|
d7a70bfc6f | ||
|
|
a4ea2624a8 | ||
|
|
dec73c77e6 | ||
|
|
761ad0bdcf | ||
|
|
d1e272e192 | ||
|
|
9fb7aa4f20 | ||
|
|
8fa0f04e43 | ||
|
|
c00e84700c | ||
|
|
2d2335f95a | ||
|
|
10d05cdb60 | ||
|
|
9f79340505 | ||
|
|
290712d4b3 | ||
|
|
c49dd8d383 | ||
|
|
3a50f8df81 | ||
|
|
569a2a6bc6 | ||
|
|
2e8a8f09b1 | ||
|
|
d04b6e2d35 | ||
|
|
f208700f78 | ||
|
|
7a3d05cb7c | ||
|
|
8df8bfff18 | ||
|
|
9ac6bb3248 | ||
|
|
ee57ded16f | ||
|
|
f508dec107 | ||
|
|
0b387a454b | ||
|
|
bc565d384d | ||
|
|
372eae0f21 | ||
|
|
474301c5ab | ||
|
|
beb2817d6f | ||
|
|
0ee983fc31 | ||
|
|
c32d5fd5ee | ||
|
|
0d193cfd57 | ||
|
|
263e7a8497 | ||
|
|
7f86e6b38c | ||
|
|
194d471db4 | ||
|
|
84e8af7976 | ||
|
|
f2dca0315c | ||
|
|
01d9b94f62 | ||
|
|
ebb216ff11 | ||
|
|
e56997f504 | ||
|
|
f66dd977e7 | ||
|
|
0a19fbc6e3 | ||
|
|
88159ed84c | ||
|
|
7eee090507 | ||
|
|
c387a27f73 | ||
|
|
959dd4ee99 | ||
|
|
e2cdfddc8d | ||
|
|
b4f80133d0 | ||
|
|
523bc50eb7 | ||
|
|
3d4d64e1b7 | ||
|
|
f6dbe5ad8a | ||
|
|
cea3c19d62 | ||
|
|
c99ddc46bb | ||
|
|
176faa57b0 | ||
|
|
2854497644 | ||
|
|
f1212736c0 | ||
|
|
997f38d640 | ||
|
|
c3a05e8cf5 | ||
|
|
978d10f2f3 | ||
|
|
6cbcbe7003 | ||
|
|
aaab777d27 | ||
|
|
011415f5bd | ||
|
|
cc8dd94d27 | ||
|
|
68597718bf | ||
|
|
c82451ca9a | ||
|
|
ba71ad934c | ||
|
|
2f92cdd2eb | ||
|
|
cb7fa39144 | ||
|
|
5908963cca | ||
|
|
38584bff25 | ||
|
|
aecf149159 | ||
|
|
18de22744e | ||
|
|
c4834492b9 | ||
|
|
e80e5fcbe4 | ||
|
|
613af547c3 | ||
|
|
9b99276d59 | ||
|
|
e0d0968b89 | ||
|
|
b96910cddc | ||
|
|
382bbdf031 | ||
|
|
50d9914e8d | ||
|
|
5a4ded4f3f | ||
|
|
a6399e59e1 | ||
|
|
09c1656a22 | ||
|
|
067dcce88d | ||
|
|
3b8ee7840b | ||
|
|
34b7ad7000 | ||
|
|
506fceae32 | ||
|
|
275daa7976 | ||
|
|
7a348f786b | ||
|
|
1a36fd178a | ||
|
|
6ebd156198 | ||
|
|
1b956f133b | ||
|
|
4ceb403632 | ||
|
|
141018acc0 | ||
|
|
ececbed999 | ||
|
|
53a9222ad7 | ||
|
|
dfd7457c21 | ||
|
|
52926b7cb0 | ||
|
|
c8def406b0 | ||
|
|
e92e0fa409 | ||
|
|
9805996975 | ||
|
|
664609af91 | ||
|
|
9ef40f457a | ||
|
|
fec4dc78c3 | ||
|
|
f9889e3c0b | ||
|
|
561f30815c | ||
|
|
1a94538568 | ||
|
|
0d571fce14 | ||
|
|
4336c46c8a | ||
|
|
6f77d9b7bb | ||
|
|
e930de1228 | ||
|
|
2a7aabe5a5 | ||
|
|
60637e6df2 | ||
|
|
817f68aeec | ||
|
|
95820cf64f | ||
|
|
921606169a | ||
|
|
a4815e0f8a | ||
|
|
e18438cf7b | ||
|
|
48d942087d | ||
|
|
63acc26211 | ||
|
|
508e2c5f78 | ||
|
|
b3c118d4c0 | ||
|
|
021b13c660 | ||
|
|
34a51b4772 | ||
|
|
d09d25cc2a | ||
|
|
21c93d95f4 | ||
|
|
0dd00580c2 | ||
|
|
12cec86e43 | ||
|
|
66dbc7ec73 | ||
|
|
c6d8beed3e | ||
|
|
d676209daa | ||
|
|
20eb8bbc4d | ||
|
|
9466071561 | ||
|
|
099811c200 | ||
|
|
b697f56b4c | ||
|
|
ba31d2001c | ||
|
|
7fbf51c346 | ||
|
|
aee6003d6e | ||
|
|
ce4ac1f88e | ||
|
|
83f9bbf8c4 | ||
|
|
c76d7b9c7c | ||
|
|
28eb286f85 | ||
|
|
24acaefce3 | ||
|
|
5751404c58 | ||
|
|
5d5d6a4ad6 | ||
|
|
7d4f8558fe | ||
|
|
42e24e6e2a | ||
|
|
fc2e3f82a2 | ||
|
|
be718e2b61 | ||
|
|
a886241ef2 | ||
|
|
7ccb16bf7a | ||
|
|
fab28f7f64 | ||
|
|
49b6157308 | ||
|
|
c7c554674e | ||
|
|
37f5a8bfc0 | ||
|
|
62bb719f88 | ||
|
|
3126f4f4ae | ||
|
|
0486c72b95 | ||
|
|
6143f6d9be | ||
|
|
332617099d | ||
|
|
04ea9e77f5 | ||
|
|
b37134c3c1 | ||
|
|
963341727e | ||
|
|
2e84b643be | ||
|
|
cddeabe976 | ||
|
|
6036ff8afd | ||
|
|
a969f5e681 | ||
|
|
c6d1179307 | ||
|
|
6d44c4de41 | ||
|
|
56b3a89157 | ||
|
|
f2c7c3f230 | ||
|
|
9019ae101e | ||
|
|
7e877b0698 | ||
|
|
e54d039998 | ||
|
|
1380af6af5 | ||
|
|
1aeba18d81 | ||
|
|
a9866a7013 | ||
|
|
a0b67fb607 | ||
|
|
b79db51340 | ||
|
|
9192cd94c9 | ||
|
|
ef0a63d45d | ||
|
|
d040d9bf04 | ||
|
|
108a580a3f | ||
|
|
d63ced413e | ||
|
|
e102ec11ac | ||
|
|
460f6653dc | ||
|
|
e143fb4cb2 | ||
|
|
3117a2c3b5 | ||
|
|
10dc082404 | ||
|
|
4032e8efd7 | ||
|
|
b6e2d5a430 | ||
|
|
94e6efc0fc | ||
|
|
0a815be07a | ||
|
|
1add08bb20 | ||
|
|
d60bb6804c | ||
|
|
86bfb6b380 | ||
|
|
7ca04b5897 | ||
|
|
9bee00f942 | ||
|
|
11b4614d60 | ||
|
|
132e667a7b | ||
|
|
d60e1b2876 | ||
|
|
60dbf545b6 | ||
|
|
4bf6e88189 | ||
|
|
53e5c65e11 |
@@ -1,16 +0,0 @@
|
||||
version: 2.1
|
||||
jobs:
|
||||
dummy:
|
||||
docker:
|
||||
- image: cimg/base:2020.01
|
||||
steps:
|
||||
- run:
|
||||
name: Dummy step
|
||||
command: |
|
||||
echo "This is a dummy step to satisfy CircleCI"
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
workflow:
|
||||
jobs:
|
||||
- dummy
|
||||
@@ -1,324 +0,0 @@
|
||||
# CircleCI currently disabled in favor of GH actions
|
||||
version: 2.1
|
||||
commands:
|
||||
prepare_environment:
|
||||
steps:
|
||||
- run:
|
||||
name: Configure environment
|
||||
command: |
|
||||
set -x
|
||||
echo "export GOCACHE=/tmp/go-build-cache" | tee -a $BASH_ENV
|
||||
echo "export ARGOCD_TEST_VERBOSE=true" | tee -a $BASH_ENV
|
||||
echo "export ARGOCD_TEST_PARALLELISM=4" | tee -a $BASH_ENV
|
||||
echo "export ARGOCD_SONAR_VERSION=4.2.0.1873" | tee -a $BASH_ENV
|
||||
configure_git:
|
||||
steps:
|
||||
- run:
|
||||
name: Configure Git
|
||||
command: |
|
||||
set -x
|
||||
# must be configured for tests to run
|
||||
git config --global user.email you@example.com
|
||||
git config --global user.name "Your Name"
|
||||
echo "export PATH=/home/circleci/.go_workspace/src/github.com/argoproj/argo-cd/hack:\$PATH" | tee -a $BASH_ENV
|
||||
echo "export GIT_ASKPASS=git-ask-pass.sh" | tee -a $BASH_ENV
|
||||
setup_go_modules:
|
||||
steps:
|
||||
- run:
|
||||
name: Run go mod download and populate vendor
|
||||
command: |
|
||||
go mod download
|
||||
go mod vendor
|
||||
save_coverage_info:
|
||||
steps:
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- coverage.out
|
||||
save_node_modules:
|
||||
steps:
|
||||
- persist_to_workspace:
|
||||
root: ~/argo-cd
|
||||
paths:
|
||||
- ui/node_modules
|
||||
save_go_cache:
|
||||
steps:
|
||||
- persist_to_workspace:
|
||||
root: /tmp
|
||||
paths:
|
||||
- go-build-cache
|
||||
attach_go_cache:
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: /tmp
|
||||
install_golang:
|
||||
steps:
|
||||
- run:
|
||||
name: Install Golang v1.14.1
|
||||
command: |
|
||||
go get golang.org/dl/go1.14.1
|
||||
[ -e /home/circleci/sdk/go1.14.1 ] || go1.14.1 download
|
||||
go env
|
||||
echo "export GOPATH=/home/circleci/.go_workspace" | tee -a $BASH_ENV
|
||||
echo "export PATH=/home/circleci/sdk/go1.14.1/bin:\$PATH" | tee -a $BASH_ENV
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: argoproj/argocd-test-tools:v0.5.0
|
||||
working_directory: /go/src/github.com/argoproj/argo-cd
|
||||
steps:
|
||||
- prepare_environment
|
||||
- checkout
|
||||
- run: make build-local
|
||||
- run: chmod -R 777 vendor
|
||||
- run: chmod -R 777 ${GOCACHE}
|
||||
- save_go_cache
|
||||
|
||||
codegen:
|
||||
docker:
|
||||
- image: argoproj/argocd-test-tools:v0.5.0
|
||||
working_directory: /go/src/github.com/argoproj/argo-cd
|
||||
steps:
|
||||
- prepare_environment
|
||||
- checkout
|
||||
- attach_go_cache
|
||||
- run: helm2 init --client-only
|
||||
- run: make codegen-local
|
||||
- run:
|
||||
name: Check nothing has changed
|
||||
command: |
|
||||
set -xo pipefail
|
||||
# This makes sure you ran `make pre-commit` before you pushed.
|
||||
# We exclude the Swagger resources; CircleCI doesn't generate them correctly.
|
||||
# When this fails, it will, create a patch file you can apply locally to fix it.
|
||||
# To troubleshoot builds: https://argoproj.github.io/argo-cd/developer-guide/ci/
|
||||
git diff --exit-code -- . ':!Gopkg.lock' ':!assets/swagger.json' | tee codegen.patch
|
||||
- store_artifacts:
|
||||
path: codegen.patch
|
||||
destination: .
|
||||
test:
|
||||
working_directory: /go/src/github.com/argoproj/argo-cd
|
||||
docker:
|
||||
- image: argoproj/argocd-test-tools:v0.5.0
|
||||
steps:
|
||||
- prepare_environment
|
||||
- checkout
|
||||
- configure_git
|
||||
- attach_go_cache
|
||||
- run: make test-local
|
||||
- run:
|
||||
name: Uploading code coverage
|
||||
command: bash <(curl -s https://codecov.io/bash) -f coverage.out
|
||||
- run:
|
||||
name: Output of test-results
|
||||
command: |
|
||||
ls -l test-results || true
|
||||
cat test-results/junit.xml || true
|
||||
- save_coverage_info
|
||||
- store_test_results:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
destination: .
|
||||
|
||||
lint:
|
||||
working_directory: /go/src/github.com/argoproj/argo-cd
|
||||
docker:
|
||||
- image: argoproj/argocd-test-tools:v0.5.0
|
||||
steps:
|
||||
- prepare_environment
|
||||
- checkout
|
||||
- configure_git
|
||||
- attach_vendor
|
||||
- store_go_cache_docker
|
||||
- run:
|
||||
name: Run golangci-lint
|
||||
command: ARGOCD_LINT_GOGC=10 make lint-local
|
||||
- run:
|
||||
name: Check that nothing has changed
|
||||
command: |
|
||||
gDiff=$(git diff)
|
||||
if test "$gDiff" != ""; then
|
||||
echo
|
||||
echo "###############################################################################"
|
||||
echo "golangci-lint has made automatic corrections to your code. Please check below"
|
||||
echo "diff output and commit this to your local branch, or run make lint locally."
|
||||
echo "###############################################################################"
|
||||
echo
|
||||
git diff
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sonarcloud:
|
||||
working_directory: /go/src/github.com/argoproj/argo-cd
|
||||
docker:
|
||||
- image: argoproj/argocd-test-tools:v0.5.0
|
||||
environment:
|
||||
NODE_MODULES: /go/src/github.com/argoproj/argo-cd/ui/node_modules
|
||||
steps:
|
||||
- prepare_environment
|
||||
- checkout
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
command: mkdir -p /tmp/cache/scanner
|
||||
name: Create cache directory if it doesn't exist
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-sonarcloud-scanner-4.2.0.1873
|
||||
- run:
|
||||
command: |
|
||||
set -e
|
||||
VERSION=4.2.0.1873
|
||||
SONAR_TOKEN=$SONAR_TOKEN
|
||||
SCANNER_DIRECTORY=/tmp/cache/scanner
|
||||
export SONAR_USER_HOME=$SCANNER_DIRECTORY/.sonar
|
||||
OS="linux"
|
||||
echo $SONAR_USER_HOME
|
||||
|
||||
if [[ ! -x "$SCANNER_DIRECTORY/sonar-scanner-$VERSION-$OS/bin/sonar-scanner" ]]; then
|
||||
curl -Ol https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$VERSION-$OS.zip
|
||||
unzip -qq -o sonar-scanner-cli-$VERSION-$OS.zip -d $SCANNER_DIRECTORY
|
||||
fi
|
||||
|
||||
chmod +x $SCANNER_DIRECTORY/sonar-scanner-$VERSION-$OS/bin/sonar-scanner
|
||||
chmod +x $SCANNER_DIRECTORY/sonar-scanner-$VERSION-$OS/jre/bin/java
|
||||
|
||||
# Workaround for a possible bug in CircleCI
|
||||
if ! echo $CIRCLE_PULL_REQUEST | grep https://github.com/argoproj; then
|
||||
unset CIRCLE_PULL_REQUEST
|
||||
unset CIRCLE_PULL_REQUESTS
|
||||
fi
|
||||
|
||||
# Explicitly set NODE_MODULES
|
||||
export NODE_MODULES=/go/src/github.com/argoproj/argo-cd/ui/node_modules
|
||||
export NODE_PATH=/go/src/github.com/argoproj/argo-cd/ui/node_modules
|
||||
|
||||
$SCANNER_DIRECTORY/sonar-scanner-$VERSION-$OS/bin/sonar-scanner
|
||||
name: SonarCloud
|
||||
- save_cache:
|
||||
key: v1-sonarcloud-scanner-4.2.0.1873
|
||||
paths:
|
||||
- /tmp/cache/scanner
|
||||
|
||||
e2e:
|
||||
working_directory: /home/circleci/.go_workspace/src/github.com/argoproj/argo-cd
|
||||
machine:
|
||||
image: ubuntu-1604:201903-01
|
||||
environment:
|
||||
ARGOCD_FAKE_IN_CLUSTER: "true"
|
||||
ARGOCD_SSH_DATA_PATH: "/tmp/argo-e2e/app/config/ssh"
|
||||
ARGOCD_TLS_DATA_PATH: "/tmp/argo-e2e/app/config/tls"
|
||||
ARGOCD_E2E_K3S: "true"
|
||||
steps:
|
||||
- run:
|
||||
name: Install and start K3S v0.5.0
|
||||
command: |
|
||||
curl -sfL https://get.k3s.io | sh -
|
||||
sudo chmod -R a+rw /etc/rancher/k3s
|
||||
kubectl version
|
||||
environment:
|
||||
INSTALL_K3S_EXEC: --docker
|
||||
INSTALL_K3S_VERSION: v0.5.0
|
||||
- prepare_environment
|
||||
- checkout
|
||||
- run:
|
||||
name: Fix permissions on filesystem
|
||||
command: |
|
||||
mkdir -p /home/circleci/.go_workspace/pkg/mod
|
||||
chmod -R 777 /home/circleci/.go_workspace/pkg/mod
|
||||
mkdir -p /tmp/go-build-cache
|
||||
chmod -R 777 /tmp/go-build-cache
|
||||
- attach_go_cache
|
||||
- run:
|
||||
name: Update kubectl configuration for container
|
||||
command: |
|
||||
ipaddr=$(ifconfig $IFACE |grep "inet " | awk '{print $2}')
|
||||
if echo $ipaddr | grep -q 'addr:'; then
|
||||
ipaddr=$(echo $ipaddr | awk -F ':' '{print $2}')
|
||||
fi
|
||||
test -d $HOME/.kube || mkdir -p $HOME/.kube
|
||||
kubectl config view --raw | sed -e "s/127.0.0.1:6443/${ipaddr}:6443/g" -e "s/localhost:6443/${ipaddr}:6443/g" > $HOME/.kube/config
|
||||
environment:
|
||||
IFACE: ens4
|
||||
- run:
|
||||
name: Start E2E test server
|
||||
command: make start-e2e
|
||||
background: true
|
||||
environment:
|
||||
DOCKER_SRCDIR: /home/circleci/.go_workspace/src
|
||||
ARGOCD_E2E_TEST: "true"
|
||||
ARGOCD_IN_CI: "true"
|
||||
GOPATH: /home/circleci/.go_workspace
|
||||
- run:
|
||||
name: Wait for API server to become available
|
||||
command: |
|
||||
count=1
|
||||
until curl -v http://localhost:8080/healthz; do
|
||||
sleep 10;
|
||||
if test $count -ge 60; then
|
||||
echo "Timeout"
|
||||
exit 1
|
||||
fi
|
||||
count=$((count+1))
|
||||
done
|
||||
- run:
|
||||
name: Run E2E tests
|
||||
command: |
|
||||
make test-e2e
|
||||
environment:
|
||||
ARGOCD_OPTS: "--plaintext"
|
||||
ARGOCD_E2E_K3S: "true"
|
||||
IFACE: ens4
|
||||
DOCKER_SRCDIR: /home/circleci/.go_workspace/src
|
||||
GOPATH: /home/circleci/.go_workspace
|
||||
- store_test_results:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
destination: .
|
||||
ui:
|
||||
docker:
|
||||
- image: node:11.15.0
|
||||
working_directory: ~/argo-cd/ui
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/argo-cd/
|
||||
- restore_cache:
|
||||
keys:
|
||||
- yarn-packages-v4-{{ checksum "yarn.lock" }}
|
||||
- run: yarn install --frozen-lockfile --ignore-optional --non-interactive
|
||||
- save_cache:
|
||||
key: yarn-packages-v4-{{ checksum "yarn.lock" }}
|
||||
paths: [~/.cache/yarn, node_modules]
|
||||
- run: yarn test
|
||||
- run: ./node_modules/.bin/codecov -p ..
|
||||
- run: NODE_ENV='production' yarn build
|
||||
- run: yarn lint
|
||||
- save_node_modules
|
||||
|
||||
orbs:
|
||||
sonarcloud: sonarsource/sonarcloud@1.0.1
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
workflow:
|
||||
jobs:
|
||||
- build
|
||||
- test:
|
||||
requires:
|
||||
- build
|
||||
- codegen:
|
||||
requires:
|
||||
- build
|
||||
- ui:
|
||||
requires:
|
||||
- build
|
||||
- sonarcloud:
|
||||
context: SonarCloud
|
||||
requires:
|
||||
- test
|
||||
- ui
|
||||
- e2e:
|
||||
requires:
|
||||
- build
|
||||
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -6,12 +6,11 @@ labels: 'bug'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
If you are trying to resolve an environment-specific issue or have a one-off question about the edge case that does not require a feature then please consider asking a
|
||||
question in argocd slack [channel](https://argoproj.github.io/community/join-slack).
|
||||
If you are trying to resolve an environment-specific issue or have a one-off question about the edge case that does not require a feature then please consider asking a question in argocd slack [channel](https://argoproj.github.io/community/join-slack).
|
||||
|
||||
Checklist:
|
||||
|
||||
* [ ] I've searched in the docs and FAQ for my answer: http://bit.ly/argocd-faq.
|
||||
* [ ] I've searched in the docs and FAQ for my answer: https://bit.ly/argocd-faq.
|
||||
* [ ] I've included steps to reproduce the bug.
|
||||
* [ ] I've pasted the output of `argocd version`.
|
||||
|
||||
|
||||
2
.github/pull_request_template.md
vendored
@@ -3,5 +3,7 @@ Checklist:
|
||||
* [ ] Either (a) I've created an [enhancement proposal](https://github.com/argoproj/argo-cd/issues/new/choose) and discussed it with the community, (b) this is a bug fix, or (c) this does not need to be in the release notes.
|
||||
* [ ] The title of the PR states what changed and the related issues number (used for the release note).
|
||||
* [ ] I've updated both the CLI and UI to expose my feature, or I plan to submit a second PR with them.
|
||||
* [ ] Does this PR require documentation updates?
|
||||
* [ ] I've updated documentation as required by this PR.
|
||||
* [ ] Optional. My organization is added to USERS.md.
|
||||
* [ ] I've signed the CLA and my build is green ([troubleshooting builds](https://argoproj.github.io/argo-cd/developer-guide/ci/)).
|
||||
|
||||
129
.github/workflows/ci-build.yaml
vendored
@@ -11,6 +11,16 @@ on:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
build-docker:
|
||||
name: Build Docker image
|
||||
runs-on: ubuntu-latest
|
||||
if: github.head_ref != ''
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Build Docker image
|
||||
run: |
|
||||
make image
|
||||
check-go:
|
||||
name: Ensure Go modules synchronicity
|
||||
runs-on: ubuntu-latest
|
||||
@@ -20,7 +30,7 @@ jobs:
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: '1.14.2'
|
||||
go-version: '1.14.12'
|
||||
- name: Download all Go modules
|
||||
run: |
|
||||
go mod download
|
||||
@@ -38,7 +48,7 @@ jobs:
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: '1.14.2'
|
||||
go-version: '1.14.12'
|
||||
- name: Restore go build cache
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
@@ -57,10 +67,10 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v1
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
with:
|
||||
version: v1.26
|
||||
args: --timeout 5m
|
||||
version: v1.29
|
||||
args: --timeout 5m --exclude SA5011
|
||||
|
||||
test-go:
|
||||
name: Run unit tests for Go packages
|
||||
@@ -77,7 +87,7 @@ jobs:
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: '1.14.2'
|
||||
go-version: '1.14.12'
|
||||
- name: Install required packages
|
||||
run: |
|
||||
sudo apt-get install git -y
|
||||
@@ -89,9 +99,11 @@ jobs:
|
||||
run: |
|
||||
git fetch --prune --no-tags --depth=1 origin +refs/heads/*:refs/remotes/origin/*
|
||||
- name: Add ~/go/bin to PATH
|
||||
run: echo "::add-path::/home/runner/go/bin"
|
||||
run: |
|
||||
echo "/home/runner/go/bin" >> $GITHUB_PATH
|
||||
- name: Add /usr/local/bin to PATH
|
||||
run: echo "::add-path::/usr/local/bin"
|
||||
run: |
|
||||
echo "/usr/local/bin" >> $GITHUB_PATH
|
||||
- name: Restore go build cache
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
@@ -120,6 +132,61 @@ jobs:
|
||||
name: test-results
|
||||
path: test-results/
|
||||
|
||||
test-go-race:
|
||||
name: Run unit tests with -race, for Go packages
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-go
|
||||
steps:
|
||||
- name: Create checkout directory
|
||||
run: mkdir -p ~/go/src/github.com/argoproj
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Create symlink in GOPATH
|
||||
run: ln -s $(pwd) ~/go/src/github.com/argoproj/argo-cd
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: '1.14.12'
|
||||
- name: Install required packages
|
||||
run: |
|
||||
sudo apt-get install git -y
|
||||
- name: Switch to temporal branch so we re-attach head
|
||||
run: |
|
||||
git switch -c temporal-pr-branch
|
||||
git status
|
||||
- name: Fetch complete history for blame information
|
||||
run: |
|
||||
git fetch --prune --no-tags --depth=1 origin +refs/heads/*:refs/remotes/origin/*
|
||||
- name: Add ~/go/bin to PATH
|
||||
run: |
|
||||
echo "/home/runner/go/bin" >> $GITHUB_PATH
|
||||
- name: Add /usr/local/bin to PATH
|
||||
run: |
|
||||
echo "/usr/local/bin" >> $GITHUB_PATH
|
||||
- name: Restore go build cache
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/go-build
|
||||
key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
|
||||
- name: Install all tools required for building & testing
|
||||
run: |
|
||||
make install-test-tools-local
|
||||
- name: Setup git username and email
|
||||
run: |
|
||||
git config --global user.name "John Doe"
|
||||
git config --global user.email "john.doe@example.com"
|
||||
- name: Download and vendor all required packages
|
||||
run: |
|
||||
go mod download
|
||||
- name: Run all unit tests
|
||||
run: make test-race-local
|
||||
- name: Generate test results artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: race-results
|
||||
path: test-results/
|
||||
|
||||
codegen:
|
||||
name: Check changes to generated code
|
||||
runs-on: ubuntu-latest
|
||||
@@ -129,15 +196,17 @@ jobs:
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: '1.14.2'
|
||||
go-version: '1.14.12'
|
||||
- name: Create symlink in GOPATH
|
||||
run: |
|
||||
mkdir -p ~/go/src/github.com/argoproj
|
||||
cp -a ../argo-cd ~/go/src/github.com/argoproj
|
||||
- name: Add /usr/local/bin to PATH
|
||||
run: echo "::add-path::/usr/local/bin"
|
||||
- name: Add ~/go/bin to PATH
|
||||
run: echo "::add-path::/home/runner/go/bin"
|
||||
run: |
|
||||
echo "/home/runner/go/bin" >> $GITHUB_PATH
|
||||
- name: Add /usr/local/bin to PATH
|
||||
run: |
|
||||
echo "/usr/local/bin" >> $GITHUB_PATH
|
||||
- name: Download & vendor dependencies
|
||||
run: |
|
||||
# We need to vendor go modules for codegen yet
|
||||
@@ -156,6 +225,7 @@ jobs:
|
||||
run: |
|
||||
set -x
|
||||
export GOPATH=$(go env GOPATH)
|
||||
git checkout -- go.mod go.sum
|
||||
make codegen-local
|
||||
working-directory: /home/runner/go/src/github.com/argoproj/argo-cd
|
||||
- name: Check nothing has changed
|
||||
@@ -173,7 +243,7 @@ jobs:
|
||||
- name: Setup NodeJS
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '11.15.0'
|
||||
node-version: '12.18.4'
|
||||
- name: Restore node dependency cache
|
||||
id: cache-dependencies
|
||||
uses: actions/cache@v1
|
||||
@@ -263,6 +333,9 @@ jobs:
|
||||
test-e2e:
|
||||
name: Run end-to-end tests
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
k3s-version: [v1.19.2, v1.18.9, v1.17.11, v1.16.15]
|
||||
needs:
|
||||
- build-go
|
||||
env:
|
||||
@@ -281,10 +354,10 @@ jobs:
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: '1.14.2'
|
||||
go-version: '1.14.12'
|
||||
- name: Install K3S
|
||||
env:
|
||||
INSTALL_K3S_VERSION: v0.5.0
|
||||
INSTALL_K3S_VERSION: ${{ matrix.k3s-version }}+k3s1
|
||||
run: |
|
||||
set -x
|
||||
curl -sfL https://get.k3s.io | sh -
|
||||
@@ -298,10 +371,12 @@ jobs:
|
||||
with:
|
||||
path: ~/.cache/go-build
|
||||
key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
|
||||
- name: Add /usr/local/bin to PATH
|
||||
run: echo "::add-path::/usr/local/bin"
|
||||
- name: Add ~/go/bin to PATH
|
||||
run: echo "::add-path::/home/runner/go/bin"
|
||||
run: |
|
||||
echo "/home/runner/go/bin" >> $GITHUB_PATH
|
||||
- name: Add /usr/local/bin to PATH
|
||||
run: |
|
||||
echo "/usr/local/bin" >> $GITHUB_PATH
|
||||
- name: Download Go dependencies
|
||||
run: |
|
||||
go mod download
|
||||
@@ -315,9 +390,13 @@ jobs:
|
||||
git config --global user.email "john.doe@example.com"
|
||||
- name: Pull Docker image required for tests
|
||||
run: |
|
||||
docker pull quay.io/dexidp/dex:v2.22.0
|
||||
docker pull quay.io/dexidp/dex:v2.25.0
|
||||
docker pull argoproj/argo-cd-ci-builder:v1.0.0
|
||||
docker pull redis:5.0.3-alpine
|
||||
docker pull redis:5.0.10-alpine
|
||||
- name: Create target directory for binaries in the build-process
|
||||
run: |
|
||||
mkdir -p dist
|
||||
chown runner dist
|
||||
- name: Run E2E server and wait for it being available
|
||||
timeout-minutes: 30
|
||||
run: |
|
||||
@@ -326,7 +405,7 @@ jobs:
|
||||
# port 8080 which is not visible in netstat -tulpen, but still there
|
||||
# with a HTTP listener. We have API server listening on port 8088
|
||||
# instead.
|
||||
make start-e2e-local &
|
||||
make start-e2e-local 2>&1 | sed -r "s/[[:cntrl:]]\[[0-9]{1,3}m//g" > /tmp/e2e-server.log &
|
||||
count=1
|
||||
until curl -f http://127.0.0.1:8088/healthz; do
|
||||
sleep 10;
|
||||
@@ -339,4 +418,10 @@ jobs:
|
||||
- name: Run E2E testsuite
|
||||
run: |
|
||||
set -x
|
||||
make test-e2e-local
|
||||
make test-e2e-local
|
||||
- name: Upload e2e-server logs
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: e2e-server-k8s${{ matrix.k3s-version }}.log
|
||||
path: /tmp/e2e-server.log
|
||||
if: ${{ failure() }}
|
||||
|
||||
52
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: "Code scanning - action"
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '0 19 * * 0'
|
||||
|
||||
jobs:
|
||||
CodeQL-Build:
|
||||
|
||||
# CodeQL runs on ubuntu-latest and windows-latest
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
- run: git checkout HEAD^2
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
# Override language selection by uncommenting this and choosing your languages
|
||||
# with:
|
||||
# languages: go, javascript, csharp, python, cpp, java
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
4
.github/workflows/gh-pages.yaml
vendored
@@ -4,6 +4,9 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
@@ -20,6 +23,7 @@ jobs:
|
||||
mkdocs build
|
||||
mkdir ./site/.circleci && echo '{version: 2, jobs: {build: {branches: {ignore: gh-pages}}}}' > ./site/.circleci/config.yml
|
||||
- name: deploy
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
uses: peaceiris/actions-gh-pages@v2.5.0
|
||||
env:
|
||||
PERSONAL_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
|
||||
4
.github/workflows/image.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: '1.14.1'
|
||||
go-version: '1.14.12'
|
||||
- uses: actions/checkout@master
|
||||
with:
|
||||
path: src/github.com/argoproj/argo-cd
|
||||
@@ -47,4 +47,4 @@ jobs:
|
||||
git config --global user.name 'CI'
|
||||
git diff --exit-code && echo 'Already deployed' || (git commit -am 'Upgrade argocd to ${{ steps.image.outputs.tag }}' && git push)
|
||||
working-directory: argoproj-deployments/argocd
|
||||
# TODO: clean up old images once github supports it: https://github.community/t5/How-to-use-Git-and-GitHub/Deleting-images-from-Github-Package-Registry/m-p/41202/thread-id/9811
|
||||
# TODO: clean up old images once github supports it: https://github.community/t5/How-to-use-Git-and-GitHub/Deleting-images-from-Github-Package-Registry/m-p/41202/thread-id/9811
|
||||
|
||||
272
.github/workflows/release.yaml
vendored
Normal file
@@ -0,0 +1,272 @@
|
||||
name: Create ArgoCD release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'release-v*'
|
||||
- '!release-v1.5*'
|
||||
- '!release-v1.4*'
|
||||
- '!release-v1.3*'
|
||||
- '!release-v1.2*'
|
||||
- '!release-v1.1*'
|
||||
- '!release-v1.0*'
|
||||
- '!release-v0*'
|
||||
jobs:
|
||||
prepare-release:
|
||||
name: Perform automatic release on trigger ${{ github.ref }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# The name of the tag as supplied by the GitHub event
|
||||
SOURCE_TAG: ${{ github.ref }}
|
||||
# The image namespace where Docker image will be published to
|
||||
IMAGE_NAMESPACE: argoproj
|
||||
# Whether to create & push image and release assets
|
||||
DRY_RUN: false
|
||||
# Whether a draft release should be created, instead of public one
|
||||
DRAFT_RELEASE: false
|
||||
# Whether to update homebrew with this release as well
|
||||
# Set RELEASE_HOMEBREW_TOKEN secret in repository for this to work - needs
|
||||
# access to public repositories
|
||||
UPDATE_HOMEBREW: false
|
||||
# Name of the GitHub user for Git config
|
||||
GIT_USERNAME: argo-bot
|
||||
# E-Mail of the GitHub user for Git config
|
||||
GIT_EMAIL: argoproj@gmail.com
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Check if the published tag is well formed and setup vars
|
||||
run: |
|
||||
set -xue
|
||||
# Target version must match major.minor.patch and optional -rcX suffix
|
||||
# where X must be a number.
|
||||
TARGET_VERSION=${SOURCE_TAG#*release-v}
|
||||
if ! echo "${TARGET_VERSION}" | egrep '^[0-9]+\.[0-9]+\.[0-9]+(-rc[0-9]+)*$'; then
|
||||
echo "::error::Target version '${TARGET_VERSION}' is malformed, refusing to continue." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Target branch is the release branch we're going to operate on
|
||||
# Its name is 'release-<major>.<minor>'
|
||||
TARGET_BRANCH="release-${TARGET_VERSION%\.[0-9]*}"
|
||||
|
||||
# The release tag is the source tag, minus the release- prefix
|
||||
RELEASE_TAG="${SOURCE_TAG#*release-}"
|
||||
|
||||
# Whether this is a pre-release (indicated by -rc suffix)
|
||||
PRE_RELEASE=false
|
||||
if echo "${RELEASE_TAG}" | egrep -- '-rc[0-9]+$'; then
|
||||
PRE_RELEASE=true
|
||||
fi
|
||||
|
||||
# We must not have a release trigger within the same release branch,
|
||||
# because that means a release for this branch is already running.
|
||||
if git tag -l | grep "release-v${TARGET_VERSION%\.[0-9]*}" | grep -v "release-v${TARGET_VERSION}"; then
|
||||
echo "::error::Another release for branch ${TARGET_BRANCH} is currently in progress."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure that release do not yet exist
|
||||
if git rev-parse ${RELEASE_TAG}; then
|
||||
echo "::error::Release tag ${RELEASE_TAG} already exists in repository. Refusing to continue."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Make the variables available in follow-up steps
|
||||
echo "TARGET_VERSION=${TARGET_VERSION}" >> $GITHUB_ENV
|
||||
echo "TARGET_BRANCH=${TARGET_BRANCH}" >> $GITHUB_ENV
|
||||
echo "RELEASE_TAG=${RELEASE_TAG}" >> $GITHUB_ENV
|
||||
echo "PRE_RELEASE=${PRE_RELEASE}" >> $GITHUB_ENV
|
||||
|
||||
- name: Check if our release tag has a correct annotation
|
||||
run: |
|
||||
set -ue
|
||||
# Fetch all tag information as well
|
||||
git fetch --prune --tags --force
|
||||
|
||||
echo "=========== BEGIN COMMIT MESSAGE ============="
|
||||
git show ${SOURCE_TAG}
|
||||
echo "============ END COMMIT MESSAGE =============="
|
||||
|
||||
# Quite dirty hack to get the release notes from the annotated tag
|
||||
# into a temporary file.
|
||||
RELEASE_NOTES=$(mktemp -p /tmp release-notes.XXXXXX)
|
||||
|
||||
prefix=true
|
||||
begin=false
|
||||
git show ${SOURCE_TAG} | while read line; do
|
||||
# Whatever is in commit history for the tag, we only want that
|
||||
# annotation from our tag. We discard everything else.
|
||||
if test "$begin" = "false"; then
|
||||
if echo "$line" | grep -q "tag ${SOURCE_TAG#refs/tags/}"; then begin="true"; fi
|
||||
continue
|
||||
fi
|
||||
if test "$prefix" = "true"; then
|
||||
if test -z "$line"; then prefix=false; fi
|
||||
else
|
||||
if echo "$line" | egrep -q '^commit [0-9a-f]+'; then
|
||||
break
|
||||
fi
|
||||
echo "$line" >> ${RELEASE_NOTES}
|
||||
fi
|
||||
done
|
||||
|
||||
# For debug purposes
|
||||
echo "============BEGIN RELEASE NOTES================="
|
||||
cat ${RELEASE_NOTES}
|
||||
echo "=============END RELEASE NOTES=================="
|
||||
|
||||
# Too short release notes are suspicious. We need at least 100 bytes.
|
||||
relNoteLen=$(stat -c '%s' $RELEASE_NOTES)
|
||||
if test $relNoteLen -lt 100; then
|
||||
echo "::error::No release notes provided in tag annotation (or tag is not annotated)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for magic string '## Quick Start' in head of release notes
|
||||
if ! head -2 ${RELEASE_NOTES} | grep -iq '## Quick Start'; then
|
||||
echo "::error::Release notes seem invalid, quick start section not found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# We store path to temporary release notes file for later reading, we
|
||||
# need it when creating release.
|
||||
echo "RELEASE_NOTES=${RELEASE_NOTES}" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: '1.14.12'
|
||||
|
||||
- name: Setup Git author information
|
||||
run: |
|
||||
set -ue
|
||||
git config --global user.email "${GIT_EMAIL}"
|
||||
git config --global user.name "${GIT_USERNAME}"
|
||||
|
||||
- name: Checkout corresponding release branch
|
||||
run: |
|
||||
set -ue
|
||||
echo "Switching to release branch '${TARGET_BRANCH}'"
|
||||
if ! git checkout ${TARGET_BRANCH}; then
|
||||
echo "::error::Checking out release branch '${TARGET_BRANCH}' for target version '${TARGET_VERSION}' (tagged '${RELEASE_TAG}') failed. Does it exist in repo?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create VERSION information
|
||||
run: |
|
||||
set -ue
|
||||
echo "Bumping version from $(cat VERSION) to ${TARGET_VERSION}"
|
||||
echo "${TARGET_VERSION}" > VERSION
|
||||
git commit -m "Bump version to ${TARGET_VERSION}" VERSION
|
||||
|
||||
- name: Generate new set of manifests
|
||||
run: |
|
||||
set -ue
|
||||
make install-codegen-tools-local
|
||||
helm2 init --client-only
|
||||
make manifests-local VERSION=${TARGET_VERSION}
|
||||
git diff
|
||||
git commit manifests/ -m "Bump version to ${TARGET_VERSION}"
|
||||
|
||||
- name: Create the release tag
|
||||
run: |
|
||||
set -ue
|
||||
echo "Creating release ${RELEASE_TAG}"
|
||||
git tag ${RELEASE_TAG}
|
||||
|
||||
- name: Build Docker image for release
|
||||
run: |
|
||||
set -ue
|
||||
git clean -fd
|
||||
mkdir -p dist/
|
||||
make image IMAGE_TAG="${TARGET_VERSION}" DOCKER_PUSH=false
|
||||
make release-cli
|
||||
chmod +x ./dist/argocd-linux-amd64
|
||||
./dist/argocd-linux-amd64 version --client
|
||||
if: ${{ env.DRY_RUN != 'true' }}
|
||||
|
||||
- name: Push docker image to repository
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.RELEASE_DOCKERHUB_USERNAME }}
|
||||
DOCKER_TOKEN: ${{ secrets.RELEASE_DOCKERHUB_TOKEN }}
|
||||
run: |
|
||||
set -ue
|
||||
docker login --username "${DOCKER_USERNAME}" --password "${DOCKER_TOKEN}"
|
||||
docker push ${IMAGE_NAMESPACE}/argocd:v${TARGET_VERSION}
|
||||
if: ${{ env.DRY_RUN != 'true' }}
|
||||
|
||||
- name: Read release notes file
|
||||
id: release-notes
|
||||
uses: juliangruber/read-file-action@v1
|
||||
with:
|
||||
path: ${{ env.RELEASE_NOTES }}
|
||||
|
||||
- name: Push changes to release branch
|
||||
run: |
|
||||
set -ue
|
||||
git push origin ${TARGET_BRANCH}
|
||||
git push origin ${RELEASE_TAG}
|
||||
|
||||
- name: Create GitHub release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
id: create_release
|
||||
with:
|
||||
tag_name: ${{ env.RELEASE_TAG }}
|
||||
release_name: ${{ env.RELEASE_TAG }}
|
||||
draft: ${{ env.DRAFT_RELEASE }}
|
||||
prerelease: ${{ env.PRE_RELEASE }}
|
||||
body: ${{ steps.release-notes.outputs.content }}
|
||||
|
||||
- name: Upload argocd-linux-amd64 binary to release assets
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./dist/argocd-linux-amd64
|
||||
asset_name: argocd-linux-amd64
|
||||
asset_content_type: application/octet-stream
|
||||
if: ${{ env.DRY_RUN != 'true' }}
|
||||
|
||||
- name: Upload argocd-darwin-amd64 binary to release assets
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./dist/argocd-darwin-amd64
|
||||
asset_name: argocd-darwin-amd64
|
||||
asset_content_type: application/octet-stream
|
||||
if: ${{ env.DRY_RUN != 'true' }}
|
||||
|
||||
- name: Upload argocd-windows-amd64 binary to release assets
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./dist/argocd-windows-amd64.exe
|
||||
asset_name: argocd-windows-amd64.exe
|
||||
asset_content_type: application/octet-stream
|
||||
if: ${{ env.DRY_RUN != 'true' }}
|
||||
|
||||
- name: Update homebrew formula
|
||||
env:
|
||||
HOMEBREW_TOKEN: ${{ secrets.RELEASE_HOMEBREW_TOKEN }}
|
||||
uses: dawidd6/action-homebrew-bump-formula@v3
|
||||
with:
|
||||
token: ${{env.HOMEBREW_TOKEN}}
|
||||
formula: argocd
|
||||
if: ${{ env.HOMEBREW_TOKEN != '' && env.UPDATE_HOMEBREW == 'true' && env.PRE_RELEASE != 'true' }}
|
||||
|
||||
- name: Delete original request tag from repository
|
||||
run: |
|
||||
set -ue
|
||||
git push --delete origin ${SOURCE_TAG}
|
||||
if: ${{ always() }}
|
||||
8
.gitignore
vendored
@@ -12,3 +12,11 @@ coverage.out
|
||||
test-results
|
||||
.scannerwork
|
||||
.scratch
|
||||
node_modules/
|
||||
|
||||
# ignore built binaries
|
||||
cmd/argocd/argocd
|
||||
cmd/argocd-application-controller/argocd-application-controller
|
||||
cmd/argocd-repo-server/argocd-repo-server
|
||||
cmd/argocd-server/argocd-server
|
||||
cmd/argocd-util/argocd-util
|
||||
7
.readthedocs.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
version: 2
|
||||
formats: all
|
||||
mkdocs:
|
||||
fail_on_warning: false
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
248
CHANGELOG.md
@@ -1,5 +1,253 @@
|
||||
# Changelog
|
||||
|
||||
## v1.8.0 (Unreleased)
|
||||
|
||||
### Mono-Repository Improvements
|
||||
|
||||
Enhanced performance during manifest generation from mono-repository - the repository that represents the
|
||||
desired state of the whole cluster and contains hundreds of applications. The improved argocd-repo-server
|
||||
now able to concurrently generate manifests from the same repository and for the same commit SHA. This
|
||||
might provide 10x performance improvement of manifests generation.
|
||||
|
||||
### Annotation Based Path Detection
|
||||
|
||||
The feature that allows specifying which source repository directories influence the application manifest generation
|
||||
using the `argocd.argoproj.io/manifest-generate-paths` annotation. The annotation improves the Git webhook handler
|
||||
behavior. The webhook avoids related applications reconciliation if no related files have been changed by the Git commit
|
||||
and even allows to skip manifests generation for new commit by re-using generation manifests for the previous commit.
|
||||
|
||||
### Horizontal Controller Scaling
|
||||
|
||||
This release allows scaling the `argocd-application-controller` horizontally. This allows you to manage as many Kubernetes clusters
|
||||
as needed using a single Argo CD instance.
|
||||
|
||||
## New Core Functionality Features
|
||||
|
||||
Besides performance improvements, Argo CD got a lot of usability enhancements and new features:
|
||||
|
||||
* Namespace and CRD creation [#4354](https://github.com/argoproj/argo-cd/issues/4354)
|
||||
* Unknown fields of built-in K8S types [#1787](https://github.com/argoproj/argo-cd/issues/1787)
|
||||
* Endpoints Diffing [#1816](https://github.com/argoproj/argo-cd/issues/1816)
|
||||
* Better compatibility with Helm Hooks [#1816](https://github.com/argoproj/argo-cd/issues/1816)
|
||||
* App-of-Apps Health Assessment [#3781](https://github.com/argoproj/argo-cd/issues/3781)
|
||||
|
||||
## Global Projects
|
||||
|
||||
This release makes it easy to manage an Argo CD that has hundreds of Projects. Instead of duplicating the same organization-wide rules in all projects
|
||||
you can put such rules into one project and make this project “global” for all other projects. Rules defined in the global project are inherited by all
|
||||
other projects and therefore don’t have to be duplicated. The sample below demonstrates how you can create a global project and specify which project should
|
||||
inherit global project rules using Kubernetes labels.
|
||||
|
||||
## User Interface Improvements
|
||||
|
||||
The Argo CD user interface is an important part of a project and we keep working hard on improving the user experience. Here is an incomplete list of implemented improvements:
|
||||
|
||||
* Improved Applications Filters [#4622](https://github.com/argoproj/argo-cd/issues/4622)
|
||||
* Git tags and branches autocompletion [#4713](https://github.com/argoproj/argo-cd/issues/4713)
|
||||
* Project Details Page [#4400](https://github.com/argoproj/argo-cd/issues/4400)
|
||||
* New version information panel [#4376](https://github.com/argoproj/argo-cd/issues/4376)
|
||||
* Progress Indicators [#4411](https://github.com/argoproj/argo-cd/issues/4411)
|
||||
* External links annotations [#4380](https://github.com/argoproj/argo-cd/issues/4380) and more!
|
||||
|
||||
## Config Management Tools Enhancements
|
||||
|
||||
* OCI Based Repositories [#4018](https://github.com/argoproj/argo-cd/issues/4018)
|
||||
* Configurable Helm Versions [#4111](https://github.com/argoproj/argo-cd/issues/4111)
|
||||
|
||||
## Bug fixes and under the hood changes
|
||||
|
||||
In addition to new features and enhancements, we’ve fixed more than 50 bugs and upgraded third-party components and libraries that Argo CD relies on.
|
||||
|
||||
## v1.7.9 (2020-11-17)
|
||||
|
||||
- fix: improve commit verification tolerance (#4825)
|
||||
- fix: argocd diff --local should not print data of local secrets (#4850)
|
||||
- fix(ui): stack overflow crash of resource tree view for large applications (#4685)
|
||||
- chore: Update golang to v1.14.12 [backport to release-1.7] (#4834)
|
||||
- chore: Update redis to 5.0.10 (#4767)
|
||||
- chore: Replace deprecated GH actions directives for integration tests (#4589)
|
||||
|
||||
## v1.7.8 (2020-10-15)
|
||||
|
||||
- fix(logging.go): changing marshaler for JSON logging to use gogo (#4319)
|
||||
- fix: login with apiKey capability (#4557)
|
||||
- fix: api-server should not try creating default project it is exists already (#4517)
|
||||
- fix: JS error on application list page if app has no namespace (#4499)
|
||||
|
||||
|
||||
## v1.7.7 (2020-09-28)
|
||||
|
||||
- fix: Support transition from a git managed namespace to auto create (#4401)
|
||||
- fix: reduce memory spikes during cluster cache refresh (#4298)
|
||||
- fix: No error/warning condition if application destination namespace not monitored by Argo CD (#4329)
|
||||
- fix: Fix local diff/sync of apps using cluster name (#4201)
|
||||
|
||||
## v1.7.6 (2020-09-18)
|
||||
|
||||
- fix: Added cluster authentication to AKS clusters (#4265)
|
||||
- fix: swagger UI stuck loading (#4377)
|
||||
- fix: prevent 'argocd app sync' hangs if sync is completed too quickly (#4373)
|
||||
- fix: argocd app wait/sync might stuck (#4350)
|
||||
- fix: failed syncs are not retried soon enough (#4353)
|
||||
|
||||
## v1.7.5 (2020-09-15)
|
||||
|
||||
- fix: app create with -f should not ignore other options (#4322)
|
||||
- fix: limit concurrent list requests accross all clusters (#4328)
|
||||
- fix: fix possible deadlock in /v1/api/stream/applications and /v1/api/application APIs (#4315)
|
||||
- fix: WatchResourceTree does not enforce RBAC (#4311)
|
||||
- fix: app refresh API should use app resource version (#4303)
|
||||
- fix: use informer instead of k8s watch to ensure app is refreshed (#4290)
|
||||
|
||||
|
||||
## v1.7.4 (2020-09-04)
|
||||
|
||||
- fix: automatically stop watch API requests when page is hidden (#4269)
|
||||
- fix: upgrade gitops-engine dependency (issues #4242, #1881) (#4268)
|
||||
- fix: application stream API should not return 'ADDED' events if resource version is provided (#4260)
|
||||
- fix: return parsing error (#3942)
|
||||
- fix: JS error when using cluster filter in the /application view (#4247)
|
||||
- fix: improve applications list page client side performance (#4244)
|
||||
|
||||
## v1.7.3 (2020-09-01)
|
||||
|
||||
- fix: application details page crash when app is deleted (#4229)
|
||||
- fix: api-server unnecessary normalize projects on every start (#4219)
|
||||
- fix: load only project names in UI (#4217)
|
||||
- fix: Re-create already initialized ARGOCD_GNUPGHOME on startup (#4214) (#4223)
|
||||
- fix: Add openshift as a dex connector type which requires a redirectURI (#4222)
|
||||
- fix: Replace status.observedAt with redis pub/sub channels for resource tree updates (#1340) (#4208)
|
||||
- fix: cache inconsistency of child resources (#4053) (#4202)
|
||||
- fix: do not include kube-api check in application liveness flow (#4163)
|
||||
|
||||
## v1.7.2 (2020-08-27)
|
||||
|
||||
- fix: Sync hangs with cert-manager on latest RC (#4105)
|
||||
- fix: support for PKCE for cli login (#2932)
|
||||
|
||||
## v1.7.2 (2020-08-25)
|
||||
|
||||
- fix: Unable to create project JWT token on K8S v1.15 (#4165)
|
||||
- fix: Argo CD does not exclude creationTimestamp from diffing (#4157)
|
||||
|
||||
## v1.7.0 (2020-08-24)
|
||||
|
||||
### GnuPG Signature Verification
|
||||
|
||||
The feature allows to only sync against commits that are signed in Git using GnuPG. The list of public
|
||||
GPG keys required for verification is configured at the system level and can be managed using Argo CD CLI or Web user interface.
|
||||
The keys management is integrated with Argo CD SSO and access control system (e.g. `argocd gpg add --from <path-to-key>`)
|
||||
|
||||
The signature verification is enabled on the project level. The ApplicationProject CRD has a new signatureKeys field that includes
|
||||
a list of imported public GPG keys. Argo CD will verify the commit signature by these keys for every project application.
|
||||
|
||||
### Cluster Management Enhancements
|
||||
|
||||
The feature allows using the cluster name instead of the URL to specify the application destination cluster.
|
||||
Additionally, the cluster CLI and Web user interface have been improved. Argo CD operators now can view and edit cluster
|
||||
details using the Cluster Details page. The page includes cluster settings details as well as runtime information such
|
||||
as the number of monitored Kubernetes resources.
|
||||
|
||||
### Diffing And Synchronization Usability
|
||||
|
||||
* **Diffing logic improvement** Argo CD performs client-side resource diffing to detect deviations and present detected
|
||||
differences in the UI and CLI. The 1.7 release aligns a comparison algorithm with server-side Kubernetes implementation
|
||||
and removes inaccuracies in some edge cases.
|
||||
|
||||
* **Helm Hooks Compatibility** The improvement removes the discrepancy between the way how Argo CD and Helm deletes
|
||||
hooks resources. This significantly improves the compatibility and enables additional use cases.
|
||||
|
||||
* **Namespace Auto-Creation** With a new option for applications Argo CD will ensure that namespace specified as the
|
||||
application destination exists in the destination cluster.
|
||||
|
||||
* **Failed Sync Retry** This feature enables retrying of failed synchronization attempts during both manually-triggered
|
||||
and automated synchronization.
|
||||
|
||||
### Orphaned Resources Monitoring Enhancement
|
||||
|
||||
The enhancement allows configuring an exception list in Orphaned Resources settings to avoid false alarms.
|
||||
|
||||
## v1.6.2 (2020-08-01)
|
||||
|
||||
- feat: adding validate for app create and app set (#4016)
|
||||
- fix: use glob matcher in casbin built-in model (#3966)
|
||||
- fix: Normalize Helm chart path when chart name contains a slash (#3987)
|
||||
- fix: allow duplicates when using generateName (#3878)
|
||||
- fix: nil pointer dereference while syncing an app (#3915)
|
||||
|
||||
## v1.6.1 (2020-06-18)
|
||||
|
||||
- fix: User unable to generate project token even if account has appropriate permissions (#3804)
|
||||
|
||||
## v1.6.0 (2020-06-16)
|
||||
|
||||
[1.6 Release blog post](https://blog.argoproj.io/argo-cd-v1-6-democratizing-gitops-with-gitops-engine-5a17cfc87d62)
|
||||
|
||||
### GitOps Engine
|
||||
|
||||
As part of 1.6 release, the core Argo CD functionality has been moved into [GitOps Engine](https://github.com/argoproj/gitops-engine).
|
||||
GitOps Engine is a reusable library that empowers you to quickly build specialized tools that implement specific GitOps
|
||||
use cases, such as bootstrapping a Kubernetes cluster, or decentralized management of namespaces.
|
||||
|
||||
#### Enhancements
|
||||
|
||||
- feat: upgrade kustomize to v3.6.1 version (#3696)
|
||||
- feat: Add build support for ARM images (#3554)
|
||||
- feat: CLI: Allow setting Helm values literal (#3601) (#3646)
|
||||
- feat: argocd-util settings resource-overrides list-actions (#3616)
|
||||
- feat: adding failure retry (#3548)
|
||||
- feat: Implement GKE ManagedCertificate CRD health checks (#3600)
|
||||
- feat: Introduce diff normalizer knobs and allow for ignoring aggregated cluster roles (#2382) (#3076)
|
||||
- feat: Implement Crossplane CRD health checks (#3581)
|
||||
- feat: Adding deploy time and duration label (#3563)
|
||||
- feat: support delete cluster from UI (#3555)
|
||||
- feat: add button loading status for time-consuming operations (#3559)
|
||||
- feat: Add --logformat switch to API server, repository server and controller (#3408)
|
||||
- feat: Add a Get Repo command to see if Argo CD has a repo (#3523)
|
||||
- feat: Allow selecting TLS ciphers on server (#3524)
|
||||
- feat: Support additional metadata in Application sync operation (#3747)
|
||||
- feat: upgrade redis to 5.0.8-alpine (#3783)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- fix: settings manager should invalidate cache after updating repositories/repository credentials (#3672)
|
||||
- fix: Allow unsetting the last remaining values file (#3644) (#3645)
|
||||
- fix: Read cert data from kubeconfig during cluster addition and use if present (#3655) (#3667)
|
||||
- fix: oidc should set samesite cookie (#3632)
|
||||
- fix: Allow underscores in hostnames in certificate module (#3596)
|
||||
- fix: apply scopes from argocd-rbac-cm to project jwt group searches (#3508)
|
||||
- fix: fix nil pointer dereference error after cluster deletion (#3634)
|
||||
- fix: Prevent possible nil pointer dereference when getting Helm client (#3613)
|
||||
- fix: Allow CLI version command to succeed without server connection (#3049) (#3550)
|
||||
- fix: Fix login with port forwarding (#3574)
|
||||
- fix: use 'git show-ref' to both retrieve and store generated manifests (#3578)
|
||||
- fix: enable redis retries; add redis request duration metric (#3575)
|
||||
- fix: Disable keep-alive for HTTPS connection to Git (#3531)
|
||||
- fix: use uid instead of named user in Dockerfile (#3108)
|
||||
|
||||
#### Other
|
||||
|
||||
- refactoring: Gitops engine (#3066)
|
||||
|
||||
## v1.5.8 (2020-06-16)
|
||||
|
||||
- fix: upgrade awscli version (#3774)
|
||||
- fix: html encode login error/description before rendering it (#3773)
|
||||
- fix: oidc should set samesite cookie (#3632)
|
||||
- fix: avoid panic in badge handler (#3741)
|
||||
|
||||
## v1.5.7 (2020-06-09)
|
||||
|
||||
The 1.5.7 patch release resolves issue #3719 . The ARGOCD_ENABLE_LEGACY_DIFF=true should be added to argocd-application-controller deployment.
|
||||
|
||||
- fix: application with EnvoyFilter causes high memory/CPU usage (#3719)
|
||||
|
||||
## v1.5.6 (2020-06-02)
|
||||
|
||||
- feat: Upgrade kustomize to 3.6.1
|
||||
- fix: Prevent possible nil pointer dereference when getting Helm client (#3613)
|
||||
- fix: avoid deadlock in settings manager (#3637)
|
||||
|
||||
## v1.5.5 (2020-05-16)
|
||||
|
||||
- feat: add Rollout restart action (#3557)
|
||||
|
||||
24
Dockerfile
@@ -4,7 +4,7 @@ ARG BASE_IMAGE=debian:10-slim
|
||||
# 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.14.1 as builder
|
||||
FROM golang:1.14.12 as builder
|
||||
|
||||
RUN echo 'deb http://deb.debian.org/debian buster-backports main' >> /etc/apt/sources.list
|
||||
|
||||
@@ -50,12 +50,14 @@ RUN groupadd -g 999 argocd && \
|
||||
chmod g=u /home/argocd && \
|
||||
chmod g=u /etc/passwd && \
|
||||
apt-get update && \
|
||||
apt-get install -y git git-lfs python3-pip && \
|
||||
apt-get install -y git git-lfs python3-pip tini gpg && \
|
||||
apt-get clean && \
|
||||
pip3 install awscli==1.17.7 && \
|
||||
pip3 install awscli==1.18.80 && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
COPY hack/git-ask-pass.sh /usr/local/bin/git-ask-pass.sh
|
||||
COPY hack/gpg-wrapper.sh /usr/local/bin/gpg-wrapper.sh
|
||||
COPY hack/git-verify-wrapper.sh /usr/local/bin/git-verify-wrapper.sh
|
||||
COPY --from=builder /usr/local/bin/ks /usr/local/bin/ks
|
||||
COPY --from=builder /usr/local/bin/helm2 /usr/local/bin/helm2
|
||||
COPY --from=builder /usr/local/bin/helm /usr/local/bin/helm
|
||||
@@ -71,17 +73,21 @@ RUN mkdir -p /app/config/ssh && \
|
||||
ln -s /app/config/ssh/ssh_known_hosts /etc/ssh/ssh_known_hosts
|
||||
|
||||
RUN mkdir -p /app/config/tls
|
||||
RUN mkdir -p /app/config/gpg/source && \
|
||||
mkdir -p /app/config/gpg/keys && \
|
||||
chown argocd /app/config/gpg/keys && \
|
||||
chmod 0700 /app/config/gpg/keys
|
||||
|
||||
# workaround ksonnet issue https://github.com/ksonnet/ksonnet/issues/298
|
||||
ENV USER=argocd
|
||||
|
||||
USER argocd
|
||||
USER 999
|
||||
WORKDIR /home/argocd
|
||||
|
||||
####################################################################################################
|
||||
# Argo CD UI stage
|
||||
####################################################################################################
|
||||
FROM node:11.15.0 as argocd-ui
|
||||
FROM node:12.18.4 as argocd-ui
|
||||
|
||||
WORKDIR /src
|
||||
ADD ["ui/package.json", "ui/yarn.lock", "./"]
|
||||
@@ -97,7 +103,7 @@ RUN NODE_ENV='production' yarn build
|
||||
####################################################################################################
|
||||
# Argo CD Build stage which performs the actual build of Argo CD binaries
|
||||
####################################################################################################
|
||||
FROM golang:1.14.1 as argocd-build
|
||||
FROM golang:1.14.12 as argocd-build
|
||||
|
||||
COPY --from=builder /usr/local/bin/packr /usr/local/bin/packr
|
||||
|
||||
@@ -110,12 +116,12 @@ RUN go mod download
|
||||
|
||||
# Perform the build
|
||||
COPY . .
|
||||
RUN make cli server controller repo-server argocd-util
|
||||
RUN make cli-local server controller repo-server argocd-util
|
||||
|
||||
ARG BUILD_ALL_CLIS=true
|
||||
RUN if [ "$BUILD_ALL_CLIS" = "true" ] ; then \
|
||||
make CLI_NAME=argocd-darwin-amd64 GOOS=darwin cli && \
|
||||
make CLI_NAME=argocd-windows-amd64.exe GOOS=windows cli \
|
||||
make CLI_NAME=argocd-darwin-amd64 GOOS=darwin cli-local && \
|
||||
make CLI_NAME=argocd-windows-amd64.exe GOOS=windows cli-local \
|
||||
; fi
|
||||
|
||||
####################################################################################################
|
||||
|
||||
139
Makefile
@@ -3,13 +3,16 @@ CURRENT_DIR=$(shell pwd)
|
||||
DIST_DIR=${CURRENT_DIR}/dist
|
||||
CLI_NAME=argocd
|
||||
|
||||
HOST_OS:=$(shell go env GOOS)
|
||||
HOST_ARCH:=$(shell go env GOARCH)
|
||||
|
||||
VERSION=$(shell cat ${CURRENT_DIR}/VERSION)
|
||||
BUILD_DATE=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
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 github.com/gobuffalo/packr/packr"; fi)
|
||||
VOLUME_MOUNT=$(shell if test "$(go env GOOS)" = "darwin"; then echo ":delegated"; elif test selinuxenabled; then echo ":Z"; else echo ""; fi)
|
||||
VOLUME_MOUNT=$(shell if test "$(go env GOOS)" = "darwin"; then echo ":delegated"; elif test selinuxenabled; then echo ":delegated"; else echo ""; fi)
|
||||
|
||||
GOPATH?=$(shell if test -x `which go`; then go env GOPATH; else echo "$(HOME)/go"; fi)
|
||||
GOCACHE?=$(HOME)/.cache/go-build
|
||||
@@ -20,9 +23,9 @@ DOCKER_WORKDIR?=/go/src/github.com/argoproj/argo-cd
|
||||
ARGOCD_PROCFILE?=Procfile
|
||||
|
||||
# Configuration for building argocd-test-tools image
|
||||
TEST_TOOLS_NAMESPACE?=argoproj
|
||||
TEST_TOOLS_NAMESPACE?=
|
||||
TEST_TOOLS_IMAGE=argocd-test-tools
|
||||
TEST_TOOLS_TAG?=v0.5.0
|
||||
TEST_TOOLS_TAG?=latest
|
||||
ifdef TEST_TOOLS_NAMESPACE
|
||||
TEST_TOOLS_PREFIX=${TEST_TOOLS_NAMESPACE}/
|
||||
endif
|
||||
@@ -40,11 +43,21 @@ ARGOCD_TEST_E2E?=true
|
||||
|
||||
ARGOCD_LINT_GOGC?=20
|
||||
|
||||
# Depending on where we are (legacy or non-legacy pwd), we need to use
|
||||
# different Docker volume mounts for our source tree
|
||||
LEGACY_PATH=$(GOPATH)/src/github.com/argoproj/argo-cd
|
||||
ifeq ("$(PWD)","$(LEGACY_PATH)")
|
||||
DOCKER_SRC_MOUNT="$(DOCKER_SRCDIR):/go/src$(VOLUME_MOUNT)"
|
||||
else
|
||||
DOCKER_SRC_MOUNT="$(PWD):/go/src/github.com/argoproj/argo-cd$(VOLUME_MOUNT)"
|
||||
endif
|
||||
|
||||
# Runs any command in the argocd-test-utils container in server mode
|
||||
# Server mode container will start with uid 0 and drop privileges during runtime
|
||||
define run-in-test-server
|
||||
docker run --rm -it \
|
||||
--name argocd-test-server \
|
||||
-u $(shell id -u):$(shell id -g) \
|
||||
-e USER_ID=$(shell id -u) \
|
||||
-e HOME=/home/user \
|
||||
-e GOPATH=/go \
|
||||
@@ -52,7 +65,7 @@ define run-in-test-server
|
||||
-e ARGOCD_IN_CI=$(ARGOCD_IN_CI) \
|
||||
-e ARGOCD_E2E_TEST=$(ARGOCD_E2E_TEST) \
|
||||
-e ARGOCD_E2E_YARN_HOST=$(ARGOCD_E2E_YARN_HOST) \
|
||||
-v ${DOCKER_SRCDIR}:/go/src${VOLUME_MOUNT} \
|
||||
-v ${DOCKER_SRC_MOUNT} \
|
||||
-v ${GOPATH}/pkg/mod:/go/pkg/mod${VOLUME_MOUNT} \
|
||||
-v ${GOCACHE}:/tmp/go-build-cache${VOLUME_MOUNT} \
|
||||
-v ${HOME}/.kube:/home/user/.kube${VOLUME_MOUNT} \
|
||||
@@ -68,25 +81,25 @@ endef
|
||||
define run-in-test-client
|
||||
docker run --rm -it \
|
||||
--name argocd-test-client \
|
||||
-u $(shell id -u) \
|
||||
-u $(shell id -u):$(shell id -g) \
|
||||
-e HOME=/home/user \
|
||||
-e GOPATH=/go \
|
||||
-e ARGOCD_E2E_K3S=$(ARGOCD_E2E_K3S) \
|
||||
-e GOCACHE=/tmp/go-build-cache \
|
||||
-e ARGOCD_LINT_GOGC=$(ARGOCD_LINT_GOGC) \
|
||||
-v ${DOCKER_SRCDIR}:/go/src${VOLUME_MOUNT} \
|
||||
-v ${DOCKER_SRC_MOUNT} \
|
||||
-v ${GOPATH}/pkg/mod:/go/pkg/mod${VOLUME_MOUNT} \
|
||||
-v ${GOCACHE}:/tmp/go-build-cache${VOLUME_MOUNT} \
|
||||
-v ${HOME}/.kube:/home/user/.kube${VOLUME_MOUNT} \
|
||||
-v /tmp:/tmp${VOLUME_MOUNT} \
|
||||
-w ${DOCKER_WORKDIR} \
|
||||
$(TEST_TOOLS_NAMESPACE)/$(TEST_TOOLS_IMAGE):$(TEST_TOOLS_TAG) \
|
||||
$(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE):$(TEST_TOOLS_TAG) \
|
||||
bash -c "$(1)"
|
||||
endef
|
||||
|
||||
#
|
||||
define exec-in-test-server
|
||||
docker exec -it -u $(shell id -u) -e ARGOCD_E2E_K3S=$(ARGOCD_E2E_K3S) argocd-test-server $(1)
|
||||
docker exec -it -u $(shell id -u):$(shell id -g) -e ARGOCD_E2E_K3S=$(ARGOCD_E2E_K3S) argocd-test-server $(1)
|
||||
endef
|
||||
|
||||
PATH:=$(PATH):$(PWD)/hack
|
||||
@@ -98,6 +111,8 @@ IMAGE_NAMESPACE?=
|
||||
STATIC_BUILD?=true
|
||||
# build development images
|
||||
DEV_IMAGE?=false
|
||||
ARGOCD_GPG_ENABLED?=true
|
||||
ARGOCD_E2E_APISERVER_PORT?=8080
|
||||
|
||||
override LDFLAGS += \
|
||||
-X ${PACKAGE}.version=${VERSION} \
|
||||
@@ -129,39 +144,59 @@ endif
|
||||
.PHONY: all
|
||||
all: cli image argocd-util
|
||||
|
||||
# We have some legacy requirements for being checked out within $GOPATH.
|
||||
# The ensure-gopath target can be used as dependency to ensure we are running
|
||||
# within these boundaries.
|
||||
.PHONY: ensure-gopath
|
||||
ensure-gopath:
|
||||
ifneq ("$(PWD)","$(LEGACY_PATH)")
|
||||
@echo "Due to legacy requirements for codegen, repository needs to be checked out within \$$GOPATH"
|
||||
@echo "Location of this repo should be '$(LEGACY_PATH)' but is '$(PWD)'"
|
||||
@exit 1
|
||||
endif
|
||||
|
||||
.PHONY: gogen
|
||||
gogen:
|
||||
gogen: ensure-gopath
|
||||
export GO111MODULE=off
|
||||
go generate ./util/argo/...
|
||||
|
||||
.PHONY: protogen
|
||||
protogen:
|
||||
protogen: ensure-gopath
|
||||
export GO111MODULE=off
|
||||
./hack/generate-proto.sh
|
||||
|
||||
.PHONY: openapigen
|
||||
openapigen:
|
||||
openapigen: ensure-gopath
|
||||
export GO111MODULE=off
|
||||
./hack/update-openapi.sh
|
||||
|
||||
.PHONY: clientgen
|
||||
clientgen:
|
||||
clientgen: ensure-gopath
|
||||
export GO111MODULE=off
|
||||
./hack/update-codegen.sh
|
||||
|
||||
.PHONY: clidocsgen
|
||||
clidocsgen: ensure-gopath
|
||||
go run tools/cmd-docs/main.go
|
||||
|
||||
.PHONY: codegen-local
|
||||
codegen-local: mod-vendor-local gogen protogen clientgen openapigen manifests-local
|
||||
codegen-local: ensure-gopath mod-vendor-local gogen protogen clientgen openapigen clidocsgen manifests-local
|
||||
rm -rf vendor/
|
||||
|
||||
.PHONY: codegen
|
||||
codegen:
|
||||
codegen: test-tools-image
|
||||
$(call run-in-test-client,make codegen-local)
|
||||
|
||||
.PHONY: cli
|
||||
cli: clean-debug
|
||||
cli: test-tools-image
|
||||
$(call run-in-test-client, GOOS=${HOST_OS} GOARCH=${HOST_ARCH} make cli-local)
|
||||
|
||||
.PHONY: cli-local
|
||||
cli-local: clean-debug
|
||||
CGO_ENABLED=0 ${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/${CLI_NAME} ./cmd/argocd
|
||||
|
||||
.PHONY: cli-docker
|
||||
.PHONY: cli-argocd
|
||||
cli-argocd:
|
||||
go build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/${CLI_NAME} ./cmd/argocd
|
||||
|
||||
.PHONY: release-cli
|
||||
@@ -184,7 +219,7 @@ argocd-util: clean-debug
|
||||
|
||||
.PHONY: test-tools-image
|
||||
test-tools-image:
|
||||
docker build -t $(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE) -f test/container/Dockerfile .
|
||||
docker build --build-arg UID=$(shell id -u) -t $(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE) -f test/container/Dockerfile .
|
||||
docker tag $(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE) $(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE):$(TEST_TOOLS_TAG)
|
||||
|
||||
.PHONY: manifests-local
|
||||
@@ -193,7 +228,7 @@ manifests-local:
|
||||
|
||||
.PHONY: manifests
|
||||
manifests: test-tools-image
|
||||
$(call run-in-test-client,make manifests-local IMAGE_TAG='${IMAGE_TAG}')
|
||||
$(call run-in-test-client,make manifests-local IMAGE_NAMESPACE='${IMAGE_NAMESPACE}' IMAGE_TAG='${IMAGE_TAG}')
|
||||
|
||||
|
||||
# NOTE: we use packr to do the build instead of go, since we embed swagger files and policy.csv
|
||||
@@ -250,7 +285,7 @@ builder-image:
|
||||
@if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG) ; fi
|
||||
|
||||
.PHONY: mod-download
|
||||
mod-download:
|
||||
mod-download: test-tools-image
|
||||
$(call run-in-test-client,go mod download)
|
||||
|
||||
.PHONY: mod-download-local
|
||||
@@ -258,7 +293,7 @@ mod-download-local:
|
||||
go mod download
|
||||
|
||||
.PHONY: mod-vendor
|
||||
mod-vendor:
|
||||
mod-vendor: test-tools-image
|
||||
$(call run-in-test-client,go mod vendor)
|
||||
|
||||
.PHONY: mod-vendor-local
|
||||
@@ -272,7 +307,7 @@ install-lint-tools:
|
||||
|
||||
# Run linter on the code
|
||||
.PHONY: lint
|
||||
lint:
|
||||
lint: test-tools-image
|
||||
$(call run-in-test-client,make lint-local)
|
||||
|
||||
# Run linter on the code (local version)
|
||||
@@ -284,7 +319,7 @@ lint-local:
|
||||
GOGC=$(ARGOCD_LINT_GOGC) GOMAXPROCS=2 golangci-lint run --fix --verbose --timeout 300s
|
||||
|
||||
.PHONY: lint-ui
|
||||
lint-ui:
|
||||
lint-ui: test-tools-image
|
||||
$(call run-in-test-client,make lint-ui-local)
|
||||
|
||||
.PHONY: lint-ui-local
|
||||
@@ -293,13 +328,13 @@ lint-ui-local:
|
||||
|
||||
# Build all Go code
|
||||
.PHONY: build
|
||||
build:
|
||||
build: test-tools-image
|
||||
mkdir -p $(GOCACHE)
|
||||
$(call run-in-test-client, make build-local)
|
||||
|
||||
# Build all Go code (local version)
|
||||
.PHONY: build-local
|
||||
build-local:
|
||||
build-local:
|
||||
go build -v `go list ./... | grep -v 'resource_customizations\|test/e2e'`
|
||||
|
||||
# Run all unit tests
|
||||
@@ -307,7 +342,7 @@ build-local:
|
||||
# If TEST_MODULE is set (to fully qualified module name), only this specific
|
||||
# module will be tested.
|
||||
.PHONY: test
|
||||
test:
|
||||
test: test-tools-image
|
||||
mkdir -p $(GOCACHE)
|
||||
$(call run-in-test-client,make TEST_MODULE=$(TEST_MODULE) test-local)
|
||||
|
||||
@@ -320,48 +355,69 @@ test-local:
|
||||
./hack/test.sh -coverprofile=coverage.out "$(TEST_MODULE)"; \
|
||||
fi
|
||||
|
||||
.PHONY: test-race
|
||||
test-race: test-tools-image
|
||||
mkdir -p $(GOCACHE)
|
||||
$(call run-in-test-client,make TEST_MODULE=$(TEST_MODULE) test-race-local)
|
||||
|
||||
# Run all unit tests, with data race detection, skipping known failures (local version)
|
||||
.PHONY: test-race-local
|
||||
test-race-local:
|
||||
if test "$(TEST_MODULE)" = ""; then \
|
||||
./hack/test.sh -race -coverprofile=coverage.out `go list ./... | grep -v 'test/e2e'`; \
|
||||
else \
|
||||
./hack/test.sh -race -coverprofile=coverage.out "$(TEST_MODULE)"; \
|
||||
fi
|
||||
|
||||
# Run the E2E test suite. E2E test servers (see start-e2e target) must be
|
||||
# started before.
|
||||
.PHONY: test-e2e
|
||||
test-e2e:
|
||||
test-e2e:
|
||||
$(call exec-in-test-server,make test-e2e-local)
|
||||
|
||||
# Run the E2E test suite (local version)
|
||||
.PHONY: test-e2e-local
|
||||
test-e2e-local: cli
|
||||
test-e2e-local: cli-local
|
||||
# NO_PROXY ensures all tests don't go out through a proxy if one is configured on the test system
|
||||
export GO111MODULE=off
|
||||
NO_PROXY=* ./hack/test.sh -timeout 15m -v ./test/e2e
|
||||
ARGOCD_GPG_ENABLED=true NO_PROXY=* ./hack/test.sh -timeout 20m -v ./test/e2e
|
||||
|
||||
# Spawns a shell in the test server container for debugging purposes
|
||||
debug-test-server:
|
||||
debug-test-server: test-tools-image
|
||||
$(call run-in-test-server,/bin/bash)
|
||||
|
||||
# Spawns a shell in the test client container for debugging purposes
|
||||
debug-test-client:
|
||||
debug-test-client: test-tools-image
|
||||
$(call run-in-test-client,/bin/bash)
|
||||
|
||||
# Starts e2e server in a container
|
||||
.PHONY: start-e2e
|
||||
start-e2e:
|
||||
start-e2e: test-tools-image
|
||||
docker version
|
||||
mkdir -p ${GOCACHE}
|
||||
$(call run-in-test-server,make ARGOCD_PROCFILE=test/container/Procfile start-e2e-local)
|
||||
|
||||
# Starts e2e server locally (or within a container)
|
||||
.PHONY: start-e2e-local
|
||||
start-e2e-local:
|
||||
start-e2e-local:
|
||||
kubectl create ns argocd-e2e || true
|
||||
kubectl config set-context --current --namespace=argocd-e2e
|
||||
kustomize build test/manifests/base | kubectl apply -f -
|
||||
# Create GPG keys and source directories
|
||||
if test -d /tmp/argo-e2e/app/config/gpg; then rm -rf /tmp/argo-e2e/app/config/gpg/*; fi
|
||||
mkdir -p /tmp/argo-e2e/app/config/gpg/keys && chmod 0700 /tmp/argo-e2e/app/config/gpg/keys
|
||||
mkdir -p /tmp/argo-e2e/app/config/gpg/source && chmod 0700 /tmp/argo-e2e/app/config/gpg/source
|
||||
# set paths for locally managed ssh known hosts and tls certs data
|
||||
ARGOCD_SSH_DATA_PATH=/tmp/argo-e2e/app/config/ssh \
|
||||
ARGOCD_TLS_DATA_PATH=/tmp/argo-e2e/app/config/tls \
|
||||
ARGOCD_GPG_DATA_PATH=/tmp/argo-e2e/app/config/gpg/source \
|
||||
ARGOCD_GNUPGHOME=/tmp/argo-e2e/app/config/gpg/keys \
|
||||
ARGOCD_GPG_ENABLED=true \
|
||||
ARGOCD_E2E_DISABLE_AUTH=false \
|
||||
ARGOCD_ZJWT_FEATURE_FLAG=always \
|
||||
ARGOCD_IN_CI=$(ARGOCD_IN_CI) \
|
||||
ARGOCD_E2E_TEST=true \
|
||||
goreman -f $(ARGOCD_PROCFILE) start
|
||||
goreman -f $(ARGOCD_PROCFILE) start ${ARGOCD_START}
|
||||
|
||||
# Cleans VSCode debug.test files from sub-dirs to prevent them from being included in packr boxes
|
||||
.PHONY: clean-debug
|
||||
@@ -373,7 +429,7 @@ clean: clean-debug
|
||||
-rm -rf ${CURRENT_DIR}/dist
|
||||
|
||||
.PHONY: start
|
||||
start:
|
||||
start: test-tools-image
|
||||
docker version
|
||||
$(call run-in-test-server,make ARGOCD_PROCFILE=test/container/Procfile start-local ARGOCD_START=${ARGOCD_START})
|
||||
|
||||
@@ -383,18 +439,23 @@ start-local: mod-vendor-local
|
||||
# check we can connect to Docker to start Redis
|
||||
killall goreman || true
|
||||
kubectl create ns argocd || true
|
||||
rm -rf /tmp/argocd-local
|
||||
mkdir -p /tmp/argocd-local
|
||||
mkdir -p /tmp/argocd-local/gpg/keys && chmod 0700 /tmp/argocd-local/gpg/keys
|
||||
mkdir -p /tmp/argocd-local/gpg/source
|
||||
ARGOCD_ZJWT_FEATURE_FLAG=always \
|
||||
ARGOCD_IN_CI=false \
|
||||
ARGOCD_GPG_ENABLED=true \
|
||||
ARGOCD_E2E_TEST=false \
|
||||
goreman -f $(ARGOCD_PROCFILE) start ${ARGOCD_START}
|
||||
|
||||
# Runs pre-commit validation with the virtualized toolchain
|
||||
.PHONY: pre-commit
|
||||
pre-commit: dep-ensure codegen build lint test
|
||||
pre-commit: codegen build lint test
|
||||
|
||||
# Runs pre-commit validation with the local toolchain
|
||||
.PHONY: pre-commit-local
|
||||
pre-commit-local: dep-ensure-local codegen-local build-local lint-local test-local
|
||||
pre-commit-local: codegen-local build-local lint-local test-local
|
||||
|
||||
.PHONY: release-precheck
|
||||
release-precheck: manifests
|
||||
@@ -424,12 +485,12 @@ publish-docs: lint-docs
|
||||
|
||||
# Verify that kubectl can connect to your K8s cluster from Docker
|
||||
.PHONY: verify-kube-connect
|
||||
verify-kube-connect:
|
||||
verify-kube-connect: test-tools-image
|
||||
$(call run-in-test-client,kubectl version)
|
||||
|
||||
# Show the Go version of local and virtualized environments
|
||||
.PHONY: show-go-version
|
||||
show-go-version:
|
||||
show-go-version: test-tools-image
|
||||
@echo -n "Local Go version: "
|
||||
@go version
|
||||
@echo -n "Docker Go version: "
|
||||
@@ -460,7 +521,7 @@ install-go-tools-local:
|
||||
./hack/install.sh codegen-go-tools
|
||||
|
||||
.PHONY: dep-ui
|
||||
dep-ui:
|
||||
dep-ui: test-tools-image
|
||||
$(call run-in-test-client,make dep-ui-local)
|
||||
|
||||
dep-ui-local:
|
||||
|
||||
5
OWNERS
@@ -10,3 +10,8 @@ approvers:
|
||||
- jessesuen
|
||||
- mayzhang2000
|
||||
- rachelwang20
|
||||
|
||||
reviewers:
|
||||
- jgwest
|
||||
- wtam2018
|
||||
- tetchel
|
||||
|
||||
11
Procfile
@@ -1,7 +1,8 @@
|
||||
controller: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true go run ./cmd/argocd-application-controller/main.go --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081}"
|
||||
api-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true go run ./cmd/argocd-server/main.go --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --disable-auth=${ARGOCD_E2E_DISABLE_AUTH:-'true'} --insecure --dex-server http://localhost:${ARGOCD_E2E_DEX_PORT:-5556} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --port ${ARGOCD_E2E_APISERVER_PORT:-8080} --staticassets ui/dist/app"
|
||||
dex: sh -c "go run github.com/argoproj/argo-cd/cmd/argocd-util gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p ${ARGOCD_E2E_DEX_PORT:-5556}:${ARGOCD_E2E_DEX_PORT:-5556} -v `pwd`/dist/dex.yaml:/dex.yaml quay.io/dexidp/dex:v2.22.0 serve /dex.yaml"
|
||||
redis: docker run --rm --name argocd-redis -i -p ${ARGOCD_E2E_REDIS_PORT:-6379}:${ARGOCD_E2E_REDIS_PORT:-6379} redis:5.0.3-alpine --save "" --appendonly no --port ${ARGOCD_E2E_REDIS_PORT:-6379}
|
||||
repo-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true go run ./cmd/argocd-repo-server/main.go --loglevel debug --port ${ARGOCD_E2E_REPOSERVER_PORT:-8081} --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379}"
|
||||
controller: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} go run ./cmd/argocd-application-controller/main.go --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081}"
|
||||
api-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} go run ./cmd/argocd-server/main.go --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --disable-auth=${ARGOCD_E2E_DISABLE_AUTH:-'true'} --insecure --dex-server http://localhost:${ARGOCD_E2E_DEX_PORT:-5556} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --port ${ARGOCD_E2E_APISERVER_PORT:-8080} --staticassets ui/dist/app"
|
||||
dex: sh -c "go run github.com/argoproj/argo-cd/cmd/argocd-util gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p ${ARGOCD_E2E_DEX_PORT:-5556}:${ARGOCD_E2E_DEX_PORT:-5556} -v `pwd`/dist/dex.yaml:/dex.yaml ghcr.io/dexidp/dex:v2.27.0 serve /dex.yaml"
|
||||
redis: docker run --rm --name argocd-redis -i -p ${ARGOCD_E2E_REDIS_PORT:-6379}:${ARGOCD_E2E_REDIS_PORT:-6379} redis:5.0.10-alpine --save "" --appendonly no --port ${ARGOCD_E2E_REDIS_PORT:-6379}
|
||||
repo-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_GNUPGHOME=${ARGOCD_GNUPGHOME:-/tmp/argocd-local/gpg/keys} ARGOCD_GPG_DATA_PATH=${ARGOCD_GPG_DATA_PATH:-/tmp/argocd-local/gpg/source} ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} go run ./cmd/argocd-repo-server/main.go --loglevel debug --port ${ARGOCD_E2E_REPOSERVER_PORT:-8081} --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379}"
|
||||
ui: sh -c 'cd ui && ${ARGOCD_E2E_YARN_CMD:-yarn} start'
|
||||
git-server: test/fixture/testrepos/start-git.sh
|
||||
dev-mounter: [[ "$ARGOCD_E2E_TEST" != "true" ]] && go run hack/dev-mounter/main.go --configmap argocd-ssh-known-hosts-cm=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} --configmap argocd-tls-certs-cm=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} --configmap argocd-gpg-keys-cm=${ARGOCD_GPG_DATA_PATH:-/tmp/argocd-local/gpg/source}
|
||||
|
||||
@@ -27,13 +27,18 @@ Check live demo at https://cd.apps.argoproj.io/.
|
||||
|
||||
## Community Blogs and Presentations
|
||||
|
||||
1. [Environments Based On Pull Requests (PRs): Using Argo CD To Apply GitOps Principles On Previews](https://youtu.be/cpAaI8p4R60)
|
||||
1. [Argo CD: Applying GitOps Principles To Manage Production Environment In Kubernetes](https://youtu.be/vpWQeoaiRM4)
|
||||
1. [Tutorial: Everything You Need To Become A GitOps Ninja](https://www.youtube.com/watch?v=r50tRQjisxw) 90m tutorial on GitOps and Argo CD.
|
||||
1. [Comparison of Argo CD, Spinnaker, Jenkins X, and Tekton](https://www.inovex.de/blog/spinnaker-vs-argo-cd-vs-tekton-vs-jenkins-x/)
|
||||
1. [Simplify and Automate Deployments Using GitOps with IBM Multicloud Manager 3.1.2](https://medium.com/ibm-cloud/simplify-and-automate-deployments-using-gitops-with-ibm-multicloud-manager-3-1-2-4395af317359)
|
||||
1. [GitOps for Kubeflow using Argo CD](https://www.kubeflow.org/docs/use-cases/gitops-for-kubeflow/)
|
||||
1. [GitOps for Kubeflow using Argo CD](https://v0-6.kubeflow.org/docs/use-cases/gitops-for-kubeflow/)
|
||||
1. [GitOps Toolsets on Kubernetes with CircleCI and Argo CD](https://www.digitalocean.com/community/tutorials/webinar-series-gitops-tool-sets-on-kubernetes-with-circleci-and-argo-cd)
|
||||
1. [Simplify and Automate Deployments Using GitOps with IBM Multicloud Manager](https://www.ibm.com/blogs/bluemix/2019/02/simplify-and-automate-deployments-using-gitops-with-ibm-multicloud-manager-3-1-2/)
|
||||
1. [CI/CD in Light Speed with K8s and Argo CD](https://www.youtube.com/watch?v=OdzH82VpMwI&feature=youtu.be)
|
||||
1. [Machine Learning as Code](https://www.youtube.com/watch?v=VXrGp5er1ZE&t=0s&index=135&list=PLj6h78yzYM2PZf9eA7bhWnIh_mK1vyOfU). Among other things, describes how Kubeflow uses Argo CD to implement GitOPs for ML
|
||||
1. [Argo CD - GitOps Continuous Delivery for Kubernetes](https://www.youtube.com/watch?v=aWDIQMbp1cc&feature=youtu.be&t=1m4s)
|
||||
1. [Introduction to Argo CD : Kubernetes DevOps CI/CD](https://www.youtube.com/watch?v=2WSJF7d8dUg&feature=youtu.be)
|
||||
1. [GitOps Deployment and Kubernetes - using ArgoCD](https://medium.com/riskified-technology/gitops-deployment-and-kubernetes-f1ab289efa4b)
|
||||
1. [Deploy Argo CD with Ingress and TLS in Three Steps: No YAML Yak Shaving Required](https://itnext.io/deploy-argo-cd-with-ingress-and-tls-in-three-steps-no-yaml-yak-shaving-required-bc536d401491)
|
||||
1. [GitOps Continuous Delivery with Argo and Codefresh](https://codefresh.io/events/cncf-member-webinar-gitops-continuous-delivery-argo-codefresh/)
|
||||
|
||||
47
SECURITY.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Security Policy for Argo CD
|
||||
|
||||
Version: **v1.0 (2020-02-26)**
|
||||
|
||||
## Preface
|
||||
|
||||
As a deployment tool, Argo CD needs to have production access which makes
|
||||
security a very important topic. The Argoproj team takes security very
|
||||
seriously and is continuously working on improving it.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We currently support the most recent release (`N`, e.g. `1.8`) and the release
|
||||
previous to the most recent one (`N-1`, e.g. `1.7`). With the release of
|
||||
`N+1`, `N-1` drops out of support and `N` becomes `N-1`.
|
||||
|
||||
We regularly perform patch releases (e.g. `1.8.5` and `1.7.12`) for the
|
||||
supported versions, which will contain fixes for security vulnerabilities and
|
||||
important bugs. Prior releases might receive critical security fixes on a best
|
||||
effort basis, however, it cannot be guaranteed that security fixes get
|
||||
back-ported to these unsupported versions.
|
||||
|
||||
In rare cases, where a security fix needs complex re-design of a feature or is
|
||||
otherwise very intrusive, and there's a workaround available, we may decide to
|
||||
provide a forward-fix only, e.g. to be released the next minor release, instead
|
||||
of releasing it within a patch branch for the currently supported releases.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you find a security related bug in ArgoCD, we kindly ask you for responsible
|
||||
disclosure and for giving us appropriate time to react, analyze and develop a
|
||||
fix to mitigate the found security vulnerability.
|
||||
|
||||
We will do our best to react quickly on your inquiry, and to coordinate a fix
|
||||
and disclosure with you. Sometimes, it might take a little longer for us to
|
||||
react (e.g. out of office conditions), so please bear with us in these cases.
|
||||
|
||||
We will publish security advisiories using the Git Hub SA feature to keep our
|
||||
community well informed, and will credit you for your findings (unless you
|
||||
prefer to stay anonymous, of course).
|
||||
|
||||
Please report vulnerabilities by e-mail to all of the following people:
|
||||
|
||||
* jfischer@redhat.com
|
||||
* Jesse_Suen@intuit.com
|
||||
* Alexander_Matyushentsev@intuit.com
|
||||
* Edward_Lee@intuit.com
|
||||
40
USERS.md
@@ -5,56 +5,81 @@ As the Argo Community grows, we'd like to keep track of our users. Please send a
|
||||
Currently, the following organizations are **officially** using Argo CD:
|
||||
|
||||
1. [127Labs](https://127labs.com/)
|
||||
1. [3Rein](https://www.3rein.com/)
|
||||
1. [Adevinta](https://www.adevinta.com/)
|
||||
1. [AppDirect](https://www.appdirect.com)
|
||||
1. [ANSTO - Australian Synchrotron](https://www.synchrotron.org.au/)
|
||||
1. [ARZ Allgemeines Rechenzentrum GmbH ](https://www.arz.at/)
|
||||
1. [Arctiq Inc.](https://www.arctiq.ca)
|
||||
1. [Baloise](https://www.baloise.com)
|
||||
1. [BCDevExchange DevOps Platform](https://bcdevexchange.org/DevOpsPlatform)
|
||||
1. [Beat](https://thebeat.co/en/)
|
||||
1. [Beez Innovation Labs](https://www.beezlabs.com/)
|
||||
1. [BioBox Analytics](https://biobox.io)
|
||||
1. [Camptocamp](https://camptocamp.com)
|
||||
1. [CARFAX](https://www.carfax.com)
|
||||
1. [Celonis](https://www.celonis.com/)
|
||||
1. [Codility](https://www.codility.com/)
|
||||
1. [Commonbond](https://commonbond.co/)
|
||||
1. [CyberAgent](https://www.cyberagent.co.jp/en/)
|
||||
1. [Cybozu](https://cybozu-global.com)
|
||||
1. [D2iQ](https://www.d2iq.com)
|
||||
1. [EDF Renewables](https://www.edf-re.com/)
|
||||
1. [edX](https://edx.org)
|
||||
1. [Electronic Arts Inc. ](https://www.ea.com)
|
||||
1. [Elium](https://www.elium.com)
|
||||
1. [END.](https://www.endclothing.com/)
|
||||
1. [Fave](https://myfave.com)
|
||||
1. [Future PLC](https://www.futureplc.com/)
|
||||
1. [Garner](https://www.garnercorp.com)
|
||||
1. [GMETRI](https://gmetri.com/)
|
||||
1. [Greenpass](https://www.greenpass.com.br/)
|
||||
1. [Healy](https://www.healyworld.net)
|
||||
1. [hipages](https://hipages.com.au/)
|
||||
1. [Honestbank](https://honestbank.com)
|
||||
1. [InsideBoard](https://www.insideboard.com)
|
||||
1. [Intuit](https://www.intuit.com/)
|
||||
1. [Kasa](https://kasa.co.kr/)
|
||||
1. [KintoHub](https://www.kintohub.com/)
|
||||
1. [KompiTech GmbH](https://www.kompitech.com/)
|
||||
1. [LINE](https://linecorp.com/en/)
|
||||
1. [Lytt](https://www.lytt.co/)
|
||||
1. [Major League Baseball](https://mlb.com)
|
||||
1. [Mambu](https://www.mambu.com/)
|
||||
1. [Max Kelsen](https://www.maxkelsen.com/)
|
||||
1. [Mirantis](https://mirantis.com/)
|
||||
1. [Money Forward](https://corp.moneyforward.com/en/)
|
||||
1. [MOO Print](https://www.moo.com/)
|
||||
1. [Nikkei](https://www.nikkei.co.jp/nikkeiinfo/en/)
|
||||
1. [OpenSaaS Studio](https://opensaas.studio)
|
||||
1. [Opensurvey](https://www.opensurvey.co.kr/)
|
||||
1. [Optoro](https://www.optoro.com/)
|
||||
1. [Peloton Interactive](https://www.onepeloton.com/)
|
||||
1. [Pipefy](https://www.pipefy.com/)
|
||||
1. [Preferred Networks](https://preferred.jp/en/)
|
||||
1. [Prudential](https://prudential.com.sg)
|
||||
1. [PUBG](https://www.pubg.com)
|
||||
1. [QuintoAndar](https://quintoandar.com.br)
|
||||
1. [Quipper](https://www.quipper.com/)
|
||||
1. [Red Hat](https://www.redhat.com/)
|
||||
1. [Robotinfra](https://www.robotinfra.com)
|
||||
1. [Riskified](https://www.riskified.com/)
|
||||
1. [Saildrone](https://www.saildrone.com/)
|
||||
1. [Saloodo! GmbH](https://www.saloodo.com)
|
||||
1. [Swisscom](https://www.swisscom.ch)
|
||||
1. [Swissquote](https://github.com/swissquote)
|
||||
1. [Syncier](https://syncier.com/)
|
||||
1. [TableCheck](https://tablecheck.com/)
|
||||
1. [Tesla](https://tesla.com/)
|
||||
1. [ThousandEyes](https://www.thousandeyes.com/)
|
||||
1. [Ticketmaster](https://ticketmaster.com)
|
||||
1. [Tiger Analytics](https://www.tigeranalytics.com/)
|
||||
1. [Toss](https://toss.im/en)
|
||||
1. [tru.ID](https://tru.id)
|
||||
1. [Twilio SendGrid](https://sendgrid.com)
|
||||
1. [tZERO](https://www.tzero.com/)
|
||||
1. [UBIO](https://ub.io/)
|
||||
1. [UFirstGroup](https://www.ufirstgroup.com/en/)
|
||||
1. [Universidad Mesoamericana](https://www.umes.edu.gt/)
|
||||
1. [Viaduct](https://www.viaduct.ai/)
|
||||
1. [Volvo Cars](https://www.volvocars.com/)
|
||||
@@ -64,3 +89,18 @@ Currently, the following organizations are **officially** using Argo CD:
|
||||
1. [Yieldlab](https://www.yieldlab.de/)
|
||||
1. [MTN Group](https://www.mtn.com/)
|
||||
1. [Moengage](https://www.moengage.com/)
|
||||
1. [LexisNexis](https://www.lexisnexis.com/)
|
||||
1. [PayPay](https://paypay.ne.jp/)
|
||||
1. [New Relic](https://newrelic.com/)
|
||||
1. [Sumo Logic](https://sumologic.com/)
|
||||
1. [Kinguin](https://www.kinguin.net/)
|
||||
1. [Speee](https://speee.jp/)
|
||||
1. [VISITS Technologies](https://visits.world/en)
|
||||
1. [Qonto](https://qonto.com)
|
||||
1. [openEuler](https://openeuler.org)
|
||||
1. [MindSpore](https://mindspore.cn)
|
||||
1. [openLooKeng](https://openlookeng.io)
|
||||
1. [openGauss](https://opengauss.org/)
|
||||
1. [Virtuo](https://www.govirtuo.com/)
|
||||
1. [WeMo Scooter](https://www.wemoscooter.com/)
|
||||
1. [Codefresh](https://www.codefresh.io/)
|
||||
@@ -12,6 +12,7 @@ p, role:readonly, clusters, get, *, allow
|
||||
p, role:readonly, repositories, get, *, allow
|
||||
p, role:readonly, projects, get, *, allow
|
||||
p, role:readonly, accounts, get, *, allow
|
||||
p, role:readonly, gpgkeys, get, *, allow
|
||||
|
||||
p, role:admin, applications, create, */*, allow
|
||||
p, role:admin, applications, update, */*, allow
|
||||
@@ -32,6 +33,8 @@ p, role:admin, projects, create, *, allow
|
||||
p, role:admin, projects, update, *, allow
|
||||
p, role:admin, projects, delete, *, allow
|
||||
p, role:admin, accounts, update, *, allow
|
||||
p, role:admin, gpgkeys, create, *, allow
|
||||
p, role:admin, gpgkeys, delete, *, allow
|
||||
|
||||
g, role:admin, role:readonly
|
||||
g, admin, role:admin
|
||||
|
||||
|
@@ -11,4 +11,4 @@ g = _, _
|
||||
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)
|
||||
m = g(r.sub, p.sub) && globMatch(r.res, p.res) && globMatch(r.act, p.act) && globMatch(r.obj, p.obj)
|
||||
|
||||
1029
assets/swagger.json
@@ -0,0 +1,149 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/pkg/stats"
|
||||
"github.com/go-redis/redis/v8"
|
||||
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/common"
|
||||
"github.com/argoproj/argo-cd/controller"
|
||||
"github.com/argoproj/argo-cd/controller/sharding"
|
||||
"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/apiclient"
|
||||
cacheutil "github.com/argoproj/argo-cd/util/cache"
|
||||
appstatecache "github.com/argoproj/argo-cd/util/cache/appstate"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/env"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
kubeutil "github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
const (
|
||||
// CLIName is the name of the CLI
|
||||
cliName = "argocd-application-controller"
|
||||
// Default time in seconds for application resync period
|
||||
defaultAppResyncPeriod = 180
|
||||
)
|
||||
|
||||
func NewCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
appResyncPeriod int64
|
||||
repoServerAddress string
|
||||
repoServerTimeoutSeconds int
|
||||
selfHealTimeoutSeconds int
|
||||
statusProcessors int
|
||||
operationProcessors int
|
||||
logFormat string
|
||||
logLevel string
|
||||
glogLevel int
|
||||
metricsPort int
|
||||
kubectlParallelismLimit int64
|
||||
cacheSrc func() (*appstatecache.Cache, error)
|
||||
redisClient *redis.Client
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: cliName,
|
||||
Short: "Run ArgoCD Application Controller",
|
||||
Long: "ArgoCD application controller is a Kubernetes controller that continuously monitors running applications and compares the current, live state against the desired target state (as specified in the repo). This command runs Application Controller in the foreground. It can be configured by following options.",
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
cli.SetLogFormat(logFormat)
|
||||
cli.SetLogLevel(logLevel)
|
||||
cli.SetGLogLevel(glogLevel)
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
errors.CheckError(v1alpha1.SetK8SConfigDefaults(config))
|
||||
|
||||
kubeClient := kubernetes.NewForConfigOrDie(config)
|
||||
appClient := appclientset.NewForConfigOrDie(config)
|
||||
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
|
||||
resyncDuration := time.Duration(appResyncPeriod) * time.Second
|
||||
repoClientset := apiclient.NewRepoServerClientset(repoServerAddress, repoServerTimeoutSeconds)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
cache, err := cacheSrc()
|
||||
errors.CheckError(err)
|
||||
cache.Cache.SetClient(cacheutil.NewTwoLevelClient(cache.Cache.GetClient(), 10*time.Minute))
|
||||
|
||||
settingsMgr := settings.NewSettingsManager(ctx, kubeClient, namespace)
|
||||
kubectl := kubeutil.NewKubectl()
|
||||
clusterFilter := getClusterFilter()
|
||||
appController, err := controller.NewApplicationController(
|
||||
namespace,
|
||||
settingsMgr,
|
||||
kubeClient,
|
||||
appClient,
|
||||
repoClientset,
|
||||
cache,
|
||||
kubectl,
|
||||
resyncDuration,
|
||||
time.Duration(selfHealTimeoutSeconds)*time.Second,
|
||||
metricsPort,
|
||||
kubectlParallelismLimit,
|
||||
clusterFilter)
|
||||
errors.CheckError(err)
|
||||
cacheutil.CollectMetrics(redisClient, appController.GetMetricsServer())
|
||||
|
||||
vers := common.GetVersion()
|
||||
log.Infof("Application Controller (version: %s, built: %s) starting (namespace: %s)", vers.Version, vers.BuildDate, namespace)
|
||||
stats.RegisterStackDumper()
|
||||
stats.StartStatsTicker(10 * time.Minute)
|
||||
stats.RegisterHeapDumper("memprofile")
|
||||
|
||||
go appController.Run(ctx, statusProcessors, operationProcessors)
|
||||
|
||||
// Wait forever
|
||||
select {}
|
||||
},
|
||||
}
|
||||
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(&command)
|
||||
command.Flags().Int64Var(&appResyncPeriod, "app-resync", defaultAppResyncPeriod, "Time period in seconds for application resync.")
|
||||
command.Flags().StringVar(&repoServerAddress, "repo-server", common.DefaultRepoServerAddr, "Repo server address.")
|
||||
command.Flags().IntVar(&repoServerTimeoutSeconds, "repo-server-timeout-seconds", 60, "Repo server RPC call timeout seconds.")
|
||||
command.Flags().IntVar(&statusProcessors, "status-processors", 1, "Number of application status processors")
|
||||
command.Flags().IntVar(&operationProcessors, "operation-processors", 1, "Number of application operation processors")
|
||||
command.Flags().StringVar(&logFormat, "logformat", "text", "Set the logging format. One of: text|json")
|
||||
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
|
||||
command.Flags().IntVar(&glogLevel, "gloglevel", 0, "Set the glog logging level")
|
||||
command.Flags().IntVar(&metricsPort, "metrics-port", common.DefaultPortArgoCDMetrics, "Start metrics server on given port")
|
||||
command.Flags().IntVar(&selfHealTimeoutSeconds, "self-heal-timeout-seconds", 5, "Specifies timeout between application self heal attempts")
|
||||
command.Flags().Int64Var(&kubectlParallelismLimit, "kubectl-parallelism-limit", 20, "Number of allowed concurrent kubectl fork/execs. Any value less the 1 means no limit.")
|
||||
cacheSrc = appstatecache.AddCacheFlagsToCmd(&command, func(client *redis.Client) {
|
||||
redisClient = client
|
||||
})
|
||||
return &command
|
||||
}
|
||||
|
||||
func getClusterFilter() func(cluster *v1alpha1.Cluster) bool {
|
||||
replicas := env.ParseNumFromEnv(common.EnvControllerReplicas, 0, 0, math.MaxInt32)
|
||||
shard := env.ParseNumFromEnv(common.EnvControllerShard, -1, -math.MaxInt32, math.MaxInt32)
|
||||
var clusterFilter func(cluster *v1alpha1.Cluster) bool
|
||||
if replicas > 1 {
|
||||
if shard < 0 {
|
||||
var err error
|
||||
shard, err = sharding.InferShard()
|
||||
errors.CheckError(err)
|
||||
}
|
||||
log.Infof("Processing clusters from shard %d", shard)
|
||||
clusterFilter = sharding.GetClusterFilter(replicas, shard)
|
||||
} else {
|
||||
log.Info("Processing all cluster shards")
|
||||
}
|
||||
return clusterFilter
|
||||
}
|
||||
@@ -1,136 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
"github.com/argoproj/pkg/stats"
|
||||
"github.com/go-redis/redis"
|
||||
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).
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
|
||||
// load the azure plugin (required to authenticate with AKS clusters).
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/azure"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/controller"
|
||||
"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/apiclient"
|
||||
cacheutil "github.com/argoproj/argo-cd/util/cache"
|
||||
appstatecache "github.com/argoproj/argo-cd/util/cache/appstate"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
"github.com/argoproj/argo-cd/cmd/argocd-application-controller/commands"
|
||||
)
|
||||
|
||||
const (
|
||||
// CLIName is the name of the CLI
|
||||
cliName = "argocd-application-controller"
|
||||
// Default time in seconds for application resync period
|
||||
defaultAppResyncPeriod = 180
|
||||
)
|
||||
|
||||
func newCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
appResyncPeriod int64
|
||||
repoServerAddress string
|
||||
repoServerTimeoutSeconds int
|
||||
selfHealTimeoutSeconds int
|
||||
statusProcessors int
|
||||
operationProcessors int
|
||||
logFormat string
|
||||
logLevel string
|
||||
glogLevel int
|
||||
metricsPort int
|
||||
kubectlParallelismLimit int64
|
||||
cacheSrc func() (*appstatecache.Cache, error)
|
||||
redisClient *redis.Client
|
||||
)
|
||||
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.SetLogFormat(logFormat)
|
||||
cli.SetLogLevel(logLevel)
|
||||
cli.SetGLogLevel(glogLevel)
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
errors.CheckError(v1alpha1.SetK8SConfigDefaults(config))
|
||||
|
||||
kubeClient := kubernetes.NewForConfigOrDie(config)
|
||||
appClient := appclientset.NewForConfigOrDie(config)
|
||||
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
|
||||
resyncDuration := time.Duration(appResyncPeriod) * time.Second
|
||||
repoClientset := apiclient.NewRepoServerClientset(repoServerAddress, repoServerTimeoutSeconds)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
cache, err := cacheSrc()
|
||||
errors.CheckError(err)
|
||||
|
||||
settingsMgr := settings.NewSettingsManager(ctx, kubeClient, namespace)
|
||||
kubectl := &kube.KubectlCmd{}
|
||||
appController, err := controller.NewApplicationController(
|
||||
namespace,
|
||||
settingsMgr,
|
||||
kubeClient,
|
||||
appClient,
|
||||
repoClientset,
|
||||
cache,
|
||||
kubectl,
|
||||
resyncDuration,
|
||||
time.Duration(selfHealTimeoutSeconds)*time.Second,
|
||||
metricsPort,
|
||||
kubectlParallelismLimit)
|
||||
errors.CheckError(err)
|
||||
cacheutil.CollectMetrics(redisClient, appController.GetMetricsServer())
|
||||
|
||||
vers := common.GetVersion()
|
||||
log.Infof("Application Controller (version: %s, built: %s) starting (namespace: %s)", vers.Version, vers.BuildDate, namespace)
|
||||
stats.RegisterStackDumper()
|
||||
stats.StartStatsTicker(10 * time.Minute)
|
||||
stats.RegisterHeapDumper("memprofile")
|
||||
|
||||
go appController.Run(ctx, statusProcessors, operationProcessors)
|
||||
|
||||
// Wait forever
|
||||
select {}
|
||||
},
|
||||
}
|
||||
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(&command)
|
||||
command.Flags().Int64Var(&appResyncPeriod, "app-resync", defaultAppResyncPeriod, "Time period in seconds for application resync.")
|
||||
command.Flags().StringVar(&repoServerAddress, "repo-server", common.DefaultRepoServerAddr, "Repo server address.")
|
||||
command.Flags().IntVar(&repoServerTimeoutSeconds, "repo-server-timeout-seconds", 60, "Repo server RPC call timeout seconds.")
|
||||
command.Flags().IntVar(&statusProcessors, "status-processors", 1, "Number of application status processors")
|
||||
command.Flags().IntVar(&operationProcessors, "operation-processors", 1, "Number of application operation processors")
|
||||
command.Flags().StringVar(&logFormat, "logformat", "text", "Set the logging format. One of: text|json")
|
||||
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
|
||||
command.Flags().IntVar(&glogLevel, "gloglevel", 0, "Set the glog logging level")
|
||||
command.Flags().IntVar(&metricsPort, "metrics-port", common.DefaultPortArgoCDMetrics, "Start metrics server on given port")
|
||||
command.Flags().IntVar(&selfHealTimeoutSeconds, "self-heal-timeout-seconds", 5, "Specifies timeout between application self heal attempts")
|
||||
command.Flags().Int64Var(&kubectlParallelismLimit, "kubectl-parallelism-limit", 20, "Number of allowed concurrent kubectl fork/execs. Any value less the 1 means no limit.")
|
||||
cacheSrc = appstatecache.AddCacheFlagsToCmd(&command, func(client *redis.Client) {
|
||||
redisClient = client
|
||||
})
|
||||
return &command
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := newCommand().Execute(); err != nil {
|
||||
if err := commands.NewCommand().Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
162
cmd/argocd-repo-server/commands/argocd_repo_server.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/pkg/stats"
|
||||
"github.com/go-redis/redis/v8"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc/health/grpc_health_v1"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/reposerver"
|
||||
"github.com/argoproj/argo-cd/reposerver/apiclient"
|
||||
reposervercache "github.com/argoproj/argo-cd/reposerver/cache"
|
||||
"github.com/argoproj/argo-cd/reposerver/metrics"
|
||||
"github.com/argoproj/argo-cd/reposerver/repository"
|
||||
cacheutil "github.com/argoproj/argo-cd/util/cache"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/env"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
"github.com/argoproj/argo-cd/util/gpg"
|
||||
"github.com/argoproj/argo-cd/util/healthz"
|
||||
ioutil "github.com/argoproj/argo-cd/util/io"
|
||||
"github.com/argoproj/argo-cd/util/tls"
|
||||
)
|
||||
|
||||
const (
|
||||
// CLIName is the name of the CLI
|
||||
cliName = "argocd-repo-server"
|
||||
gnuPGSourcePath = "/app/config/gpg/source"
|
||||
|
||||
defaultPauseGenerationAfterFailedGenerationAttempts = 3
|
||||
defaultPauseGenerationOnFailureForMinutes = 60
|
||||
defaultPauseGenerationOnFailureForRequests = 0
|
||||
)
|
||||
|
||||
func getGnuPGSourcePath() string {
|
||||
if path := os.Getenv("ARGOCD_GPG_DATA_PATH"); path != "" {
|
||||
return path
|
||||
} else {
|
||||
return gnuPGSourcePath
|
||||
}
|
||||
}
|
||||
|
||||
func getPauseGenerationAfterFailedGenerationAttempts() int {
|
||||
return env.ParseNumFromEnv(common.EnvPauseGenerationAfterFailedAttempts, defaultPauseGenerationAfterFailedGenerationAttempts, 0, math.MaxInt32)
|
||||
}
|
||||
|
||||
func getPauseGenerationOnFailureForMinutes() int {
|
||||
return env.ParseNumFromEnv(common.EnvPauseGenerationMinutes, defaultPauseGenerationOnFailureForMinutes, 0, math.MaxInt32)
|
||||
}
|
||||
|
||||
func getPauseGenerationOnFailureForRequests() int {
|
||||
return env.ParseNumFromEnv(common.EnvPauseGenerationRequests, defaultPauseGenerationOnFailureForRequests, 0, math.MaxInt32)
|
||||
}
|
||||
|
||||
func NewCommand() *cobra.Command {
|
||||
var (
|
||||
logFormat string
|
||||
logLevel string
|
||||
parallelismLimit int64
|
||||
listenPort int
|
||||
metricsPort int
|
||||
cacheSrc func() (*reposervercache.Cache, error)
|
||||
tlsConfigCustomizerSrc func() (tls.ConfigCustomizer, error)
|
||||
redisClient *redis.Client
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: cliName,
|
||||
Short: "Run ArgoCD Repository Server",
|
||||
Long: "ArgoCD Repository Server is an internal service which maintains a local cache of the Git repository holding the application manifests, and is responsible for generating and returning the Kubernetes manifests. This command runs Repository Server in the foreground. It can be configured by following options.",
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
cli.SetLogFormat(logFormat)
|
||||
cli.SetLogLevel(logLevel)
|
||||
|
||||
tlsConfigCustomizer, err := tlsConfigCustomizerSrc()
|
||||
errors.CheckError(err)
|
||||
|
||||
cache, err := cacheSrc()
|
||||
errors.CheckError(err)
|
||||
|
||||
metricsServer := metrics.NewMetricsServer()
|
||||
cacheutil.CollectMetrics(redisClient, metricsServer)
|
||||
server, err := reposerver.NewServer(metricsServer, cache, tlsConfigCustomizer, repository.RepoServerInitConstants{
|
||||
ParallelismLimit: parallelismLimit,
|
||||
PauseGenerationAfterFailedGenerationAttempts: getPauseGenerationAfterFailedGenerationAttempts(),
|
||||
PauseGenerationOnFailureForMinutes: getPauseGenerationOnFailureForMinutes(),
|
||||
PauseGenerationOnFailureForRequests: getPauseGenerationOnFailureForRequests(),
|
||||
})
|
||||
errors.CheckError(err)
|
||||
|
||||
grpc := server.CreateGRPC()
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", listenPort))
|
||||
errors.CheckError(err)
|
||||
|
||||
healthz.ServeHealthCheck(http.DefaultServeMux, func(r *http.Request) error {
|
||||
if val, ok := r.URL.Query()["full"]; ok && len(val) > 0 && val[0] == "true" {
|
||||
// connect to itself to make sure repo server is able to serve connection
|
||||
// used by liveness probe to auto restart repo server
|
||||
// see https://github.com/argoproj/argo-cd/issues/5110 for more information
|
||||
conn, err := apiclient.NewConnection(fmt.Sprintf("localhost:%d", listenPort), 60)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ioutil.Close(conn)
|
||||
client := grpc_health_v1.NewHealthClient(conn)
|
||||
res, err := client.Check(r.Context(), &grpc_health_v1.HealthCheckRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.Status != grpc_health_v1.HealthCheckResponse_SERVING {
|
||||
return fmt.Errorf("grpc health check status is '%v'", res.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
http.Handle("/metrics", metricsServer.GetHandler())
|
||||
go func() { errors.CheckError(http.ListenAndServe(fmt.Sprintf(":%d", metricsPort), nil)) }()
|
||||
|
||||
if gpg.IsGPGEnabled() {
|
||||
log.Infof("Initializing GnuPG keyring at %s", common.GetGnuPGHomePath())
|
||||
err = gpg.InitializeGnuPG()
|
||||
errors.CheckError(err)
|
||||
|
||||
log.Infof("Populating GnuPG keyring with keys from %s", getGnuPGSourcePath())
|
||||
added, removed, err := gpg.SyncKeyRingFromDirectory(getGnuPGSourcePath())
|
||||
errors.CheckError(err)
|
||||
log.Infof("Loaded %d (and removed %d) keys from keyring", len(added), len(removed))
|
||||
|
||||
go func() { errors.CheckError(reposerver.StartGPGWatcher(getGnuPGSourcePath())) }()
|
||||
}
|
||||
|
||||
log.Infof("argocd-repo-server %s serving on %s", common.GetVersion(), listener.Addr())
|
||||
stats.RegisterStackDumper()
|
||||
stats.StartStatsTicker(10 * time.Minute)
|
||||
stats.RegisterHeapDumper("memprofile")
|
||||
err = grpc.Serve(listener)
|
||||
errors.CheckError(err)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
command.Flags().StringVar(&logFormat, "logformat", "text", "Set the logging format. One of: text|json")
|
||||
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
|
||||
command.Flags().Int64Var(¶llelismLimit, "parallelismlimit", 0, "Limit on number of concurrent manifests generate requests. Any value less the 1 means no limit.")
|
||||
command.Flags().IntVar(&listenPort, "port", common.DefaultPortRepoServer, "Listen on given port for incoming connections")
|
||||
command.Flags().IntVar(&metricsPort, "metrics-port", common.DefaultPortRepoServerMetrics, "Start metrics server on given port")
|
||||
|
||||
tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(&command)
|
||||
cacheSrc = reposervercache.AddCacheFlagsToCmd(&command, func(client *redis.Client) {
|
||||
redisClient = client
|
||||
})
|
||||
return &command
|
||||
}
|
||||
@@ -2,91 +2,13 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
"github.com/argoproj/pkg/stats"
|
||||
"github.com/go-redis/redis"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/reposerver"
|
||||
reposervercache "github.com/argoproj/argo-cd/reposerver/cache"
|
||||
"github.com/argoproj/argo-cd/reposerver/metrics"
|
||||
cacheutil "github.com/argoproj/argo-cd/util/cache"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/tls"
|
||||
"github.com/argoproj/argo-cd/cmd/argocd-repo-server/commands"
|
||||
)
|
||||
|
||||
const (
|
||||
// CLIName is the name of the CLI
|
||||
cliName = "argocd-repo-server"
|
||||
)
|
||||
|
||||
func newCommand() *cobra.Command {
|
||||
var (
|
||||
logFormat string
|
||||
logLevel string
|
||||
parallelismLimit int64
|
||||
listenPort int
|
||||
metricsPort int
|
||||
cacheSrc func() (*reposervercache.Cache, error)
|
||||
tlsConfigCustomizerSrc func() (tls.ConfigCustomizer, error)
|
||||
redisClient *redis.Client
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: cliName,
|
||||
Short: "Run argocd-repo-server",
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
cli.SetLogFormat(logFormat)
|
||||
cli.SetLogLevel(logLevel)
|
||||
|
||||
tlsConfigCustomizer, err := tlsConfigCustomizerSrc()
|
||||
errors.CheckError(err)
|
||||
|
||||
cache, err := cacheSrc()
|
||||
errors.CheckError(err)
|
||||
|
||||
metricsServer := metrics.NewMetricsServer()
|
||||
cacheutil.CollectMetrics(redisClient, metricsServer)
|
||||
server, err := reposerver.NewServer(metricsServer, cache, tlsConfigCustomizer, parallelismLimit)
|
||||
errors.CheckError(err)
|
||||
|
||||
grpc := server.CreateGRPC()
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", listenPort))
|
||||
errors.CheckError(err)
|
||||
|
||||
http.Handle("/metrics", metricsServer.GetHandler())
|
||||
go func() { errors.CheckError(http.ListenAndServe(fmt.Sprintf(":%d", metricsPort), nil)) }()
|
||||
|
||||
log.Infof("argocd-repo-server %s serving on %s", common.GetVersion(), listener.Addr())
|
||||
stats.RegisterStackDumper()
|
||||
stats.StartStatsTicker(10 * time.Minute)
|
||||
stats.RegisterHeapDumper("memprofile")
|
||||
err = grpc.Serve(listener)
|
||||
errors.CheckError(err)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
command.Flags().StringVar(&logFormat, "logformat", "text", "Set the logging format. One of: text|json")
|
||||
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
|
||||
command.Flags().Int64Var(¶llelismLimit, "parallelismlimit", 0, "Limit on number of concurrent manifests generate requests. Any value less the 1 means no limit.")
|
||||
command.Flags().IntVar(&listenPort, "port", common.DefaultPortRepoServer, "Listen on given port for incoming connections")
|
||||
command.Flags().IntVar(&metricsPort, "metrics-port", common.DefaultPortRepoServerMetrics, "Start metrics server on given port")
|
||||
tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(&command)
|
||||
cacheSrc = reposervercache.AddCacheFlagsToCmd(&command, func(client *redis.Client) {
|
||||
redisClient = client
|
||||
})
|
||||
return &command
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := newCommand().Execute(); err != nil {
|
||||
if err := commands.NewCommand().Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -4,15 +4,13 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
"github.com/argoproj/pkg/stats"
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/go-redis/redis/v8"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
@@ -21,6 +19,7 @@ import (
|
||||
servercache "github.com/argoproj/argo-cd/server/cache"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/env"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/tls"
|
||||
)
|
||||
@@ -58,14 +57,16 @@ func NewCommand() *cobra.Command {
|
||||
repoServerAddress string
|
||||
dexServerAddress string
|
||||
disableAuth bool
|
||||
enableGZip bool
|
||||
tlsConfigCustomizerSrc func() (tls.ConfigCustomizer, error)
|
||||
cacheSrc func() (*servercache.Cache, error)
|
||||
frameOptions string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: cliName,
|
||||
Short: "Run the argocd API server",
|
||||
Long: "Run the argocd API server",
|
||||
Use: cliName,
|
||||
Short: "Run the ArgoCD API server",
|
||||
Long: "The API server is a gRPC/REST server which exposes the API consumed by the Web UI, CLI, and CI/CD systems. This command runs API server in the foreground. It can be configured by following options.",
|
||||
DisableAutoGenTag: true,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
cli.SetLogFormat(logFormat)
|
||||
cli.SetLogLevel(logLevel)
|
||||
@@ -115,6 +116,7 @@ func NewCommand() *cobra.Command {
|
||||
RepoClientset: repoclientset,
|
||||
DexServerAddr: dexServerAddress,
|
||||
DisableAuth: disableAuth,
|
||||
EnableGZip: enableGZip,
|
||||
TLSConfigCustomizer: tlsConfigCustomizer,
|
||||
Cache: cache,
|
||||
XFrameOptions: frameOptions,
|
||||
@@ -146,6 +148,7 @@ func NewCommand() *cobra.Command {
|
||||
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.Flags().BoolVar(&enableGZip, "enable-gzip", false, "Enable GZIP compression")
|
||||
command.AddCommand(cli.NewVersionCmd(cliName))
|
||||
command.Flags().IntVar(&listenPort, "port", common.DefaultPortAPIServer, "Listen on given port")
|
||||
command.Flags().IntVar(&metricsPort, "metrics-port", common.DefaultPortArgoCDAPIServerMetrics, "Start metrics on given port")
|
||||
@@ -1,14 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
|
||||
commands "github.com/argoproj/argo-cd/cmd/argocd-server/commands"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
|
||||
// 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"
|
||||
// load the azure plugin (required to authenticate with AKS clusters).
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/azure"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
338
cmd/argocd-util/commands/apps.go
Normal file
@@ -0,0 +1,338 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/spf13/cobra"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
kubecache "k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/controller"
|
||||
"github.com/argoproj/argo-cd/controller/cache"
|
||||
"github.com/argoproj/argo-cd/controller/metrics"
|
||||
"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/apiclient"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/config"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
kubeutil "github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
func NewAppsCommand() *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "apps",
|
||||
Short: "Utility commands operate on ArgoCD applications",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
},
|
||||
}
|
||||
|
||||
command.AddCommand(NewReconcileCommand())
|
||||
command.AddCommand(NewDiffReconcileResults())
|
||||
return command
|
||||
}
|
||||
|
||||
type appReconcileResult struct {
|
||||
Name string `json:"name"`
|
||||
Health *v1alpha1.HealthStatus `json:"health"`
|
||||
Sync *v1alpha1.SyncStatus `json:"sync"`
|
||||
Conditions []v1alpha1.ApplicationCondition `json:"conditions"`
|
||||
}
|
||||
|
||||
type reconcileResults struct {
|
||||
Applications []appReconcileResult `json:"applications"`
|
||||
}
|
||||
|
||||
func (r *reconcileResults) getAppsMap() map[string]appReconcileResult {
|
||||
res := map[string]appReconcileResult{}
|
||||
for i := range r.Applications {
|
||||
res[r.Applications[i].Name] = r.Applications[i]
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func printLine(format string, a ...interface{}) {
|
||||
_, _ = fmt.Printf(format+"\n", a...)
|
||||
}
|
||||
|
||||
func NewDiffReconcileResults() *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "diff-reconcile-results PATH1 PATH2",
|
||||
Short: "Compare results of two reconciliations and print diff.",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
path1 := args[0]
|
||||
path2 := args[1]
|
||||
var res1 reconcileResults
|
||||
var res2 reconcileResults
|
||||
errors.CheckError(config.UnmarshalLocalFile(path1, &res1))
|
||||
errors.CheckError(config.UnmarshalLocalFile(path2, &res2))
|
||||
errors.CheckError(diffReconcileResults(res1, res2))
|
||||
},
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func toUnstructured(val interface{}) (*unstructured.Unstructured, error) {
|
||||
data, err := json.Marshal(val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := make(map[string]interface{})
|
||||
err = json.Unmarshal(data, &res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &unstructured.Unstructured{Object: res}, nil
|
||||
}
|
||||
|
||||
type diffPair struct {
|
||||
name string
|
||||
first *unstructured.Unstructured
|
||||
second *unstructured.Unstructured
|
||||
}
|
||||
|
||||
func diffReconcileResults(res1 reconcileResults, res2 reconcileResults) error {
|
||||
var pairs []diffPair
|
||||
resMap1 := res1.getAppsMap()
|
||||
resMap2 := res2.getAppsMap()
|
||||
for k, v := range resMap1 {
|
||||
firstUn, err := toUnstructured(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var secondUn *unstructured.Unstructured
|
||||
second, ok := resMap2[k]
|
||||
if ok {
|
||||
secondUn, err = toUnstructured(second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
delete(resMap2, k)
|
||||
}
|
||||
pairs = append(pairs, diffPair{name: k, first: firstUn, second: secondUn})
|
||||
}
|
||||
for k, v := range resMap2 {
|
||||
secondUn, err := toUnstructured(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pairs = append(pairs, diffPair{name: k, first: nil, second: secondUn})
|
||||
}
|
||||
sort.Slice(pairs, func(i, j int) bool {
|
||||
return pairs[i].name < pairs[j].name
|
||||
})
|
||||
for _, item := range pairs {
|
||||
printLine(item.name)
|
||||
_ = cli.PrintDiff(item.name, item.first, item.second)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewReconcileCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
selector string
|
||||
repoServerAddress string
|
||||
outputFormat string
|
||||
refresh bool
|
||||
)
|
||||
|
||||
var command = &cobra.Command{
|
||||
Use: "get-reconcile-results PATH",
|
||||
Short: "Reconcile all applications and stores reconciliation summary in the specified file.",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
// get rid of logging error handler
|
||||
runtime.ErrorHandlers = runtime.ErrorHandlers[1:]
|
||||
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
outputPath := args[0]
|
||||
|
||||
errors.CheckError(os.Setenv(common.EnvVarFakeInClusterConfig, "true"))
|
||||
cfg, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
|
||||
var result []appReconcileResult
|
||||
if refresh {
|
||||
if repoServerAddress == "" {
|
||||
printLine("Repo server is not provided, trying to port-forward to argocd-repo-server pod.")
|
||||
repoServerPort, err := kubeutil.PortForward("app.kubernetes.io/name=argocd-repo-server", 8081, namespace)
|
||||
errors.CheckError(err)
|
||||
repoServerAddress = fmt.Sprintf("localhost:%d", repoServerPort)
|
||||
}
|
||||
repoServerClient := apiclient.NewRepoServerClientset(repoServerAddress, 60)
|
||||
|
||||
appClientset := appclientset.NewForConfigOrDie(cfg)
|
||||
kubeClientset := kubernetes.NewForConfigOrDie(cfg)
|
||||
result, err = reconcileApplications(kubeClientset, appClientset, namespace, repoServerClient, selector, newLiveStateCache)
|
||||
errors.CheckError(err)
|
||||
} else {
|
||||
appClientset := appclientset.NewForConfigOrDie(cfg)
|
||||
result, err = getReconcileResults(appClientset, namespace, selector)
|
||||
}
|
||||
|
||||
errors.CheckError(saveToFile(err, outputFormat, reconcileResults{Applications: result}, outputPath))
|
||||
},
|
||||
}
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(command)
|
||||
command.Flags().StringVar(&repoServerAddress, "repo-server", "", "Repo server address.")
|
||||
command.Flags().StringVar(&selector, "l", "", "Label selector")
|
||||
command.Flags().StringVar(&outputFormat, "o", "yaml", "Output format (yaml|json)")
|
||||
command.Flags().BoolVar(&refresh, "refresh", false, "If set to true then recalculates apps reconciliation")
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func saveToFile(err error, outputFormat string, result reconcileResults, outputPath string) error {
|
||||
errors.CheckError(err)
|
||||
var data []byte
|
||||
switch outputFormat {
|
||||
case "yaml":
|
||||
if data, err = yaml.Marshal(result); err != nil {
|
||||
return err
|
||||
}
|
||||
case "json":
|
||||
if data, err = json.Marshal(result); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("format %s is not supported", outputFormat)
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(outputPath, data, 0644)
|
||||
}
|
||||
|
||||
func getReconcileResults(appClientset appclientset.Interface, namespace string, selector string) ([]appReconcileResult, error) {
|
||||
appsList, err := appClientset.ArgoprojV1alpha1().Applications(namespace).List(context.Background(), v1.ListOptions{LabelSelector: selector})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var items []appReconcileResult
|
||||
for _, app := range appsList.Items {
|
||||
items = append(items, appReconcileResult{
|
||||
Name: app.Name,
|
||||
Conditions: app.Status.Conditions,
|
||||
Health: &app.Status.Health,
|
||||
Sync: &app.Status.Sync,
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func reconcileApplications(
|
||||
kubeClientset kubernetes.Interface,
|
||||
appClientset appclientset.Interface,
|
||||
namespace string,
|
||||
repoServerClient apiclient.Clientset,
|
||||
selector string,
|
||||
createLiveStateCache func(argoDB db.ArgoDB, appInformer kubecache.SharedIndexInformer, settingsMgr *settings.SettingsManager, server *metrics.MetricsServer) cache.LiveStateCache,
|
||||
) ([]appReconcileResult, error) {
|
||||
|
||||
settingsMgr := settings.NewSettingsManager(context.Background(), kubeClientset, namespace)
|
||||
argoDB := db.NewDB(namespace, settingsMgr, kubeClientset)
|
||||
appInformerFactory := appinformers.NewFilteredSharedInformerFactory(
|
||||
appClientset,
|
||||
1*time.Hour,
|
||||
namespace,
|
||||
func(options *v1.ListOptions) {},
|
||||
)
|
||||
|
||||
appInformer := appInformerFactory.Argoproj().V1alpha1().Applications().Informer()
|
||||
projInformer := appInformerFactory.Argoproj().V1alpha1().AppProjects().Informer()
|
||||
go appInformer.Run(context.Background().Done())
|
||||
go projInformer.Run(context.Background().Done())
|
||||
if !kubecache.WaitForCacheSync(context.Background().Done(), appInformer.HasSynced, projInformer.HasSynced) {
|
||||
return nil, fmt.Errorf("failed to sync cache")
|
||||
}
|
||||
|
||||
appLister := appInformerFactory.Argoproj().V1alpha1().Applications().Lister()
|
||||
projLister := appInformerFactory.Argoproj().V1alpha1().AppProjects().Lister()
|
||||
server, err := metrics.NewMetricsServer("", appLister, func(obj interface{}) bool {
|
||||
return true
|
||||
}, func(r *http.Request) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stateCache := createLiveStateCache(argoDB, appInformer, settingsMgr, server)
|
||||
if err := stateCache.Init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appStateManager := controller.NewAppStateManager(
|
||||
argoDB, appClientset, repoServerClient, namespace, kubeutil.NewKubectl(), settingsMgr, stateCache, projInformer, server)
|
||||
|
||||
appsList, err := appClientset.ArgoprojV1alpha1().Applications(namespace).List(context.Background(), v1.ListOptions{LabelSelector: selector})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sort.Slice(appsList.Items, func(i, j int) bool {
|
||||
return appsList.Items[i].Spec.Destination.Server < appsList.Items[j].Spec.Destination.Server
|
||||
})
|
||||
|
||||
var items []appReconcileResult
|
||||
prevServer := ""
|
||||
for _, app := range appsList.Items {
|
||||
if prevServer != app.Spec.Destination.Server {
|
||||
if prevServer != "" {
|
||||
if clusterCache, err := stateCache.GetClusterCache(prevServer); err == nil {
|
||||
clusterCache.Invalidate()
|
||||
}
|
||||
}
|
||||
printLine("Reconciling apps of %s", app.Spec.Destination.Server)
|
||||
prevServer = app.Spec.Destination.Server
|
||||
}
|
||||
printLine(app.Name)
|
||||
|
||||
proj, err := projLister.AppProjects(namespace).Get(app.Spec.Project)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := appStateManager.CompareAppState(&app, proj, app.Spec.Source.TargetRevision, app.Spec.Source, false, nil)
|
||||
items = append(items, appReconcileResult{
|
||||
Name: app.Name,
|
||||
Conditions: app.Status.Conditions,
|
||||
Health: res.GetHealthStatus(),
|
||||
Sync: res.GetSyncStatus(),
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func newLiveStateCache(argoDB db.ArgoDB, appInformer kubecache.SharedIndexInformer, settingsMgr *settings.SettingsManager, server *metrics.MetricsServer) cache.LiveStateCache {
|
||||
return cache.NewLiveStateCache(argoDB, appInformer, settingsMgr, kubeutil.NewKubectl(), server, func(managedByApp map[string]bool, ref apiv1.ObjectReference) {}, nil)
|
||||
}
|
||||
182
cmd/argocd-util/commands/apps_test.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/argoproj/argo-cd/test"
|
||||
|
||||
clustermocks "github.com/argoproj/gitops-engine/pkg/cache/mocks"
|
||||
"github.com/argoproj/gitops-engine/pkg/health"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
"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"
|
||||
kubefake "k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
statecache "github.com/argoproj/argo-cd/controller/cache"
|
||||
cachemocks "github.com/argoproj/argo-cd/controller/cache/mocks"
|
||||
"github.com/argoproj/argo-cd/controller/metrics"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appfake "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/fake"
|
||||
"github.com/argoproj/argo-cd/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/reposerver/apiclient/mocks"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
func TestGetReconcileResults(t *testing.T) {
|
||||
appClientset := appfake.NewSimpleClientset(&v1alpha1.Application{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
Namespace: "default",
|
||||
},
|
||||
Status: v1alpha1.ApplicationStatus{
|
||||
Health: v1alpha1.HealthStatus{Status: health.HealthStatusHealthy},
|
||||
Sync: v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeOutOfSync},
|
||||
},
|
||||
})
|
||||
|
||||
result, err := getReconcileResults(appClientset, "default", "")
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
expectedResults := []appReconcileResult{{
|
||||
Name: "test",
|
||||
Health: &v1alpha1.HealthStatus{Status: health.HealthStatusHealthy},
|
||||
Sync: &v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeOutOfSync},
|
||||
}}
|
||||
assert.ElementsMatch(t, expectedResults, result)
|
||||
}
|
||||
|
||||
func TestGetReconcileResults_Refresh(t *testing.T) {
|
||||
cm := corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "argocd-cm",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/part-of": "argocd",
|
||||
},
|
||||
},
|
||||
}
|
||||
proj := &v1alpha1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1alpha1.AppProjectSpec{Destinations: []v1alpha1.ApplicationDestination{{Namespace: "*", Server: "*"}}},
|
||||
}
|
||||
|
||||
app := &v1alpha1.Application{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Project: "default",
|
||||
Destination: v1alpha1.ApplicationDestination{
|
||||
Server: common.KubernetesInternalAPIServerAddr,
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
appClientset := appfake.NewSimpleClientset(app, proj)
|
||||
deployment := test.NewDeployment()
|
||||
kubeClientset := kubefake.NewSimpleClientset(deployment, &cm)
|
||||
clusterCache := clustermocks.ClusterCache{}
|
||||
clusterCache.On("IsNamespaced", mock.Anything).Return(true, nil)
|
||||
repoServerClient := mocks.RepoServerServiceClient{}
|
||||
repoServerClient.On("GenerateManifest", mock.Anything, mock.Anything).Return(&apiclient.ManifestResponse{
|
||||
Manifests: []string{test.DeploymentManifest},
|
||||
}, nil)
|
||||
repoServerClientset := mocks.Clientset{RepoServerServiceClient: &repoServerClient}
|
||||
liveStateCache := cachemocks.LiveStateCache{}
|
||||
liveStateCache.On("GetManagedLiveObjs", mock.Anything, mock.Anything).Return(map[kube.ResourceKey]*unstructured.Unstructured{
|
||||
kube.GetResourceKey(deployment): deployment,
|
||||
}, nil)
|
||||
liveStateCache.On("GetVersionsInfo", mock.Anything).Return("v1.2.3", nil, nil)
|
||||
liveStateCache.On("Init").Return(nil, nil)
|
||||
liveStateCache.On("GetClusterCache", mock.Anything).Return(&clusterCache, nil)
|
||||
liveStateCache.On("IsNamespaced", mock.Anything, mock.Anything).Return(true, nil)
|
||||
|
||||
result, err := reconcileApplications(kubeClientset, appClientset, "default", &repoServerClientset, "",
|
||||
func(argoDB db.ArgoDB, appInformer cache.SharedIndexInformer, settingsMgr *settings.SettingsManager, server *metrics.MetricsServer) statecache.LiveStateCache {
|
||||
return &liveStateCache
|
||||
},
|
||||
)
|
||||
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, result[0].Health.Status, health.HealthStatusMissing)
|
||||
assert.Equal(t, result[0].Sync.Status, v1alpha1.SyncStatusCodeOutOfSync)
|
||||
}
|
||||
|
||||
func TestDiffReconcileResults_NoDifferences(t *testing.T) {
|
||||
logs, err := captureStdout(func() {
|
||||
assert.NoError(t, diffReconcileResults(
|
||||
reconcileResults{Applications: []appReconcileResult{{
|
||||
Name: "app1",
|
||||
Sync: &v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeOutOfSync},
|
||||
}}},
|
||||
reconcileResults{Applications: []appReconcileResult{{
|
||||
Name: "app1",
|
||||
Sync: &v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeOutOfSync},
|
||||
}}},
|
||||
))
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "app1\n", logs)
|
||||
}
|
||||
|
||||
func TestDiffReconcileResults_DifferentApps(t *testing.T) {
|
||||
logs, err := captureStdout(func() {
|
||||
assert.NoError(t, diffReconcileResults(
|
||||
reconcileResults{Applications: []appReconcileResult{{
|
||||
Name: "app1",
|
||||
Sync: &v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeOutOfSync},
|
||||
}, {
|
||||
Name: "app2",
|
||||
Sync: &v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeOutOfSync},
|
||||
}}},
|
||||
reconcileResults{Applications: []appReconcileResult{{
|
||||
Name: "app1",
|
||||
Sync: &v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeOutOfSync},
|
||||
}, {
|
||||
Name: "app3",
|
||||
Sync: &v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeOutOfSync},
|
||||
}}},
|
||||
))
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `app1
|
||||
app2
|
||||
1,9d0
|
||||
< conditions: null
|
||||
< health: null
|
||||
< name: app2
|
||||
< sync:
|
||||
< comparedTo:
|
||||
< destination: {}
|
||||
< source:
|
||||
< repoURL: ""
|
||||
< status: OutOfSync
|
||||
app3
|
||||
0a1,9
|
||||
> conditions: null
|
||||
> health: null
|
||||
> name: app3
|
||||
> sync:
|
||||
> comparedTo:
|
||||
> destination: {}
|
||||
> source:
|
||||
> repoURL: ""
|
||||
> status: OutOfSync
|
||||
`, logs)
|
||||
}
|
||||
675
cmd/argocd-util/commands/argocd_util.go
Normal file
@@ -0,0 +1,675 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"syscall"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
"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/cli"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/dex"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
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 (
|
||||
logFormat string
|
||||
logLevel string
|
||||
)
|
||||
|
||||
var command = &cobra.Command{
|
||||
Use: cliName,
|
||||
Short: "argocd-util tools used by Argo CD",
|
||||
Long: "argocd-util has internal utility tools used by Argo CD",
|
||||
DisableAutoGenTag: true,
|
||||
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.AddCommand(NewProjectsCommand())
|
||||
command.AddCommand(NewSettingsCommand())
|
||||
command.AddCommand(NewAppsCommand())
|
||||
|
||||
command.Flags().StringVar(&logFormat, "logformat", "text", "Set the logging format. One of: text|json")
|
||||
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.Debug(redactor(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()
|
||||
errors.CheckError(err)
|
||||
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]unstructured.Unstructured)
|
||||
configMaps, err := acdClients.configMaps.List(context.Background(), metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
// referencedSecrets holds any secrets referenced in the argocd-cm configmap. These
|
||||
// secrets need to be imported too
|
||||
var referencedSecrets map[string]bool
|
||||
for _, cm := range configMaps.Items {
|
||||
if isArgoCDConfigMap(cm.GetName()) {
|
||||
pruneObjects[kube.ResourceKey{Group: "", Kind: "ConfigMap", Name: cm.GetName()}] = cm
|
||||
}
|
||||
if cm.GetName() == common.ArgoCDConfigMapName {
|
||||
referencedSecrets = getReferencedSecrets(cm)
|
||||
}
|
||||
}
|
||||
|
||||
secrets, err := acdClients.secrets.List(context.Background(), metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, secret := range secrets.Items {
|
||||
if isArgoCDSecret(referencedSecrets, secret) {
|
||||
pruneObjects[kube.ResourceKey{Group: "", Kind: "Secret", Name: secret.GetName()}] = secret
|
||||
}
|
||||
}
|
||||
applications, err := acdClients.applications.List(context.Background(), metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, app := range applications.Items {
|
||||
pruneObjects[kube.ResourceKey{Group: "argoproj.io", Kind: "Application", Name: app.GetName()}] = app
|
||||
}
|
||||
projects, err := acdClients.projects.List(context.Background(), metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, proj := range projects.Items {
|
||||
pruneObjects[kube.ResourceKey{Group: "argoproj.io", Kind: "AppProject", Name: proj.GetName()}] = proj
|
||||
}
|
||||
|
||||
// Create or replace existing object
|
||||
backupObjects, err := kube.SplitYAML(input)
|
||||
errors.CheckError(err)
|
||||
for _, bakObj := range backupObjects {
|
||||
gvk := bakObj.GroupVersionKind()
|
||||
key := kube.ResourceKey{Group: gvk.Group, Kind: gvk.Kind, Name: bakObj.GetName()}
|
||||
liveObj, exists := pruneObjects[key]
|
||||
delete(pruneObjects, key)
|
||||
var dynClient dynamic.ResourceInterface
|
||||
switch bakObj.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(context.Background(), bakObj, metav1.CreateOptions{})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
fmt.Printf("%s/%s %s created%s\n", gvk.Group, gvk.Kind, bakObj.GetName(), dryRunMsg)
|
||||
} else if specsEqual(*bakObj, liveObj) {
|
||||
fmt.Printf("%s/%s %s unchanged%s\n", gvk.Group, gvk.Kind, bakObj.GetName(), dryRunMsg)
|
||||
} else {
|
||||
if !dryRun {
|
||||
newLive := updateLive(bakObj, &liveObj)
|
||||
_, err = dynClient.Update(context.Background(), newLive, metav1.UpdateOptions{})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
fmt.Printf("%s/%s %s updated%s\n", gvk.Group, gvk.Kind, bakObj.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(context.Background(), 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)
|
||||
bw := bufio.NewWriter(f)
|
||||
writer = bw
|
||||
defer func() {
|
||||
err = bw.Flush()
|
||||
errors.CheckError(err)
|
||||
err = f.Close()
|
||||
errors.CheckError(err)
|
||||
}()
|
||||
}
|
||||
|
||||
acdClients := newArgoCDClientsets(config, namespace)
|
||||
acdConfigMap, err := acdClients.configMaps.Get(context.Background(), common.ArgoCDConfigMapName, metav1.GetOptions{})
|
||||
errors.CheckError(err)
|
||||
export(writer, *acdConfigMap)
|
||||
acdRBACConfigMap, err := acdClients.configMaps.Get(context.Background(), common.ArgoCDRBACConfigMapName, metav1.GetOptions{})
|
||||
errors.CheckError(err)
|
||||
export(writer, *acdRBACConfigMap)
|
||||
acdKnownHostsConfigMap, err := acdClients.configMaps.Get(context.Background(), common.ArgoCDKnownHostsConfigMapName, metav1.GetOptions{})
|
||||
errors.CheckError(err)
|
||||
export(writer, *acdKnownHostsConfigMap)
|
||||
acdTLSCertsConfigMap, err := acdClients.configMaps.Get(context.Background(), common.ArgoCDTLSCertsConfigMapName, metav1.GetOptions{})
|
||||
errors.CheckError(err)
|
||||
export(writer, *acdTLSCertsConfigMap)
|
||||
|
||||
referencedSecrets := getReferencedSecrets(*acdConfigMap)
|
||||
secrets, err := acdClients.secrets.List(context.Background(), metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, secret := range secrets.Items {
|
||||
if isArgoCDSecret(referencedSecrets, secret) {
|
||||
export(writer, secret)
|
||||
}
|
||||
}
|
||||
projects, err := acdClients.projects.List(context.Background(), metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, proj := range projects.Items {
|
||||
export(writer, proj)
|
||||
}
|
||||
applications, err := acdClients.applications.List(context.Background(), 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)
|
||||
|
||||
// Referenced repository secrets
|
||||
if reposRAW, ok := cm.Data["repositories"]; ok {
|
||||
repos := make([]settings.Repository, 0)
|
||||
err := yaml.Unmarshal([]byte(reposRAW), &repos)
|
||||
errors.CheckError(err)
|
||||
for _, cred := range repos {
|
||||
if cred.PasswordSecret != nil {
|
||||
referencedSecrets[cred.PasswordSecret.Name] = true
|
||||
}
|
||||
if cred.SSHPrivateKeySecret != nil {
|
||||
referencedSecrets[cred.SSHPrivateKeySecret.Name] = true
|
||||
}
|
||||
if cred.UsernameSecret != nil {
|
||||
referencedSecrets[cred.UsernameSecret.Name] = true
|
||||
}
|
||||
if cred.TLSClientCertDataSecret != nil {
|
||||
referencedSecrets[cred.TLSClientCertDataSecret.Name] = true
|
||||
}
|
||||
if cred.TLSClientCertKeySecret != nil {
|
||||
referencedSecrets[cred.TLSClientCertKeySecret.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Referenced repository credentials secrets
|
||||
if reposRAW, ok := cm.Data["repository.credentials"]; ok {
|
||||
creds := make([]settings.RepositoryCredentials, 0)
|
||||
err := yaml.Unmarshal([]byte(reposRAW), &creds)
|
||||
errors.CheckError(err)
|
||||
for _, cred := range creds {
|
||||
if cred.PasswordSecret != nil {
|
||||
referencedSecrets[cred.PasswordSecret.Name] = true
|
||||
}
|
||||
if cred.SSHPrivateKeySecret != nil {
|
||||
referencedSecrets[cred.SSHPrivateKeySecret.Name] = true
|
||||
}
|
||||
if cred.UsernameSecret != nil {
|
||||
referencedSecrets[cred.UsernameSecret.Name] = true
|
||||
}
|
||||
if cred.TLSClientCertDataSecret != nil {
|
||||
referencedSecrets[cred.TLSClientCertDataSecret.Name] = true
|
||||
}
|
||||
if cred.TLSClientCertKeySecret != nil {
|
||||
referencedSecrets[cred.TLSClientCertKeySecret.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return referencedSecrets
|
||||
}
|
||||
|
||||
// isArgoCDSecret returns whether or not the given secret is a part of Argo CD configuration
|
||||
// (e.g. argocd-secret, repo credentials, or cluster credentials)
|
||||
func isArgoCDSecret(repoSecretRefs map[string]bool, un unstructured.Unstructured) bool {
|
||||
secretName := un.GetName()
|
||||
if secretName == common.ArgoCDSecretName {
|
||||
return true
|
||||
}
|
||||
if repoSecretRefs != nil {
|
||||
if _, ok := repoSecretRefs[secretName]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if labels := un.GetLabels(); labels != nil {
|
||||
if _, ok := labels[common.LabelKeySecretType]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if annotations := un.GetAnnotations(); annotations != nil {
|
||||
if annotations[common.AnnotationKeyManagedBy] == common.AnnotationValueManagedByArgoCD {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isArgoCDConfigMap returns true if the configmap name is one of argo cd's well known configmaps
|
||||
func isArgoCDConfigMap(name string) bool {
|
||||
switch name {
|
||||
case common.ArgoCDConfigMapName, common.ArgoCDRBACConfigMapName, common.ArgoCDKnownHostsConfigMapName, common.ArgoCDTLSCertsConfigMapName:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
// specsEqual returns if the spec, data, labels, annotations, and finalizers of the two
|
||||
// supplied objects are equal, indicating that no update is necessary during importing
|
||||
func specsEqual(left, right unstructured.Unstructured) bool {
|
||||
if !reflect.DeepEqual(left.GetAnnotations(), right.GetAnnotations()) {
|
||||
return false
|
||||
}
|
||||
if !reflect.DeepEqual(left.GetLabels(), right.GetLabels()) {
|
||||
return false
|
||||
}
|
||||
if !reflect.DeepEqual(left.GetFinalizers(), right.GetFinalizers()) {
|
||||
return false
|
||||
}
|
||||
switch left.GetKind() {
|
||||
case "Secret", "ConfigMap":
|
||||
leftData, _, _ := unstructured.NestedMap(left.Object, "data")
|
||||
rightData, _, _ := unstructured.NestedMap(right.Object, "data")
|
||||
return reflect.DeepEqual(leftData, rightData)
|
||||
case "AppProject":
|
||||
leftSpec, _, _ := unstructured.NestedMap(left.Object, "spec")
|
||||
rightSpec, _, _ := unstructured.NestedMap(right.Object, "spec")
|
||||
return reflect.DeepEqual(leftSpec, rightSpec)
|
||||
case "Application":
|
||||
leftSpec, _, _ := unstructured.NestedMap(left.Object, "spec")
|
||||
rightSpec, _, _ := unstructured.NestedMap(right.Object, "spec")
|
||||
leftStatus, _, _ := unstructured.NestedMap(left.Object, "status")
|
||||
rightStatus, _, _ := unstructured.NestedMap(right.Object, "status")
|
||||
// reconciledAt and observedAt are constantly changing and we ignore any diff there
|
||||
delete(leftStatus, "reconciledAt")
|
||||
delete(rightStatus, "reconciledAt")
|
||||
delete(leftStatus, "observedAt")
|
||||
delete(rightStatus, "observedAt")
|
||||
return reflect.DeepEqual(leftSpec, rightSpec) && reflect.DeepEqual(leftStatus, rightStatus)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// updateLive replaces the live object's finalizers, spec, annotations, labels, and data from the
|
||||
// backup object but leaves all other fields intact (status, other metadata, etc...)
|
||||
func updateLive(bak, live *unstructured.Unstructured) *unstructured.Unstructured {
|
||||
newLive := live.DeepCopy()
|
||||
newLive.SetAnnotations(bak.GetAnnotations())
|
||||
newLive.SetLabels(bak.GetLabels())
|
||||
newLive.SetFinalizers(bak.GetFinalizers())
|
||||
switch live.GetKind() {
|
||||
case "Secret", "ConfigMap":
|
||||
newLive.Object["data"] = bak.Object["data"]
|
||||
case "AppProject":
|
||||
newLive.Object["spec"] = bak.Object["spec"]
|
||||
case "Application":
|
||||
newLive.Object["spec"] = bak.Object["spec"]
|
||||
if _, ok := bak.Object["status"]; ok {
|
||||
newLive.Object["status"] = bak.Object["status"]
|
||||
}
|
||||
}
|
||||
return newLive
|
||||
}
|
||||
|
||||
// export writes the unstructured object and removes extraneous cruft from output before writing
|
||||
func export(w io.Writer, un unstructured.Unstructured) {
|
||||
name := un.GetName()
|
||||
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",
|
||||
DisableAutoGenTag: true,
|
||||
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.RawRestConfig(), namespace, output)
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(command)
|
||||
return command
|
||||
}
|
||||
|
||||
func iterateStringFields(obj interface{}, callback func(name string, val string) string) {
|
||||
if mapField, ok := obj.(map[string]interface{}); ok {
|
||||
for field, val := range mapField {
|
||||
if strVal, ok := val.(string); ok {
|
||||
mapField[field] = callback(field, strVal)
|
||||
} else {
|
||||
iterateStringFields(val, callback)
|
||||
}
|
||||
}
|
||||
} else if arrayField, ok := obj.([]interface{}); ok {
|
||||
for i := range arrayField {
|
||||
iterateStringFields(arrayField[i], callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func redactor(dirtyString string) string {
|
||||
config := make(map[string]interface{})
|
||||
err := yaml.Unmarshal([]byte(dirtyString), &config)
|
||||
errors.CheckError(err)
|
||||
iterateStringFields(config, func(name string, val string) string {
|
||||
if name == "clientSecret" || name == "secret" || name == "bindPW" {
|
||||
return "********"
|
||||
} else {
|
||||
return val
|
||||
}
|
||||
})
|
||||
data, err := yaml.Marshal(config)
|
||||
errors.CheckError(err)
|
||||
return string(data)
|
||||
}
|
||||
144
cmd/argocd-util/commands/project_allowlist.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/spf13/cobra"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
|
||||
// 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"
|
||||
// load the azure plugin (required to authenticate with AKS clusters).
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/azure"
|
||||
)
|
||||
|
||||
// NewProjectAllowListGenCommand generates a project from clusterRole
|
||||
func NewProjectAllowListGenCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
out string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "generate-allow-list CLUSTERROLE_PATH PROJ_NAME",
|
||||
Short: "Generates project allow list from the specified clusterRole file",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
clusterRoleFileName := args[0]
|
||||
projName := args[1]
|
||||
|
||||
var writer io.Writer
|
||||
if out == "-" {
|
||||
writer = os.Stdout
|
||||
} else {
|
||||
f, err := os.Create(out)
|
||||
errors.CheckError(err)
|
||||
bw := bufio.NewWriter(f)
|
||||
writer = bw
|
||||
defer func() {
|
||||
err = bw.Flush()
|
||||
errors.CheckError(err)
|
||||
err = f.Close()
|
||||
errors.CheckError(err)
|
||||
}()
|
||||
}
|
||||
|
||||
globalProj := generateProjectAllowList(clientConfig, clusterRoleFileName, projName)
|
||||
|
||||
yamlBytes, err := yaml.Marshal(globalProj)
|
||||
errors.CheckError(err)
|
||||
|
||||
_, err = writer.Write(yamlBytes)
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(command)
|
||||
command.Flags().StringVarP(&out, "out", "o", "-", "Output to the specified file instead of stdout")
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func generateProjectAllowList(clientConfig clientcmd.ClientConfig, clusterRoleFileName string, projName string) v1alpha1.AppProject {
|
||||
yamlBytes, err := ioutil.ReadFile(clusterRoleFileName)
|
||||
errors.CheckError(err)
|
||||
var obj unstructured.Unstructured
|
||||
err = yaml.Unmarshal(yamlBytes, &obj)
|
||||
errors.CheckError(err)
|
||||
|
||||
clusterRole := &rbacv1.ClusterRole{}
|
||||
err = scheme.Scheme.Convert(&obj, clusterRole, nil)
|
||||
errors.CheckError(err)
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
disco, err := discovery.NewDiscoveryClientForConfig(config)
|
||||
errors.CheckError(err)
|
||||
serverResources, err := disco.ServerPreferredResources()
|
||||
errors.CheckError(err)
|
||||
|
||||
resourceList := make([]metav1.GroupKind, 0)
|
||||
for _, rule := range clusterRole.Rules {
|
||||
if len(rule.APIGroups) <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
canCreate := false
|
||||
for _, verb := range rule.Verbs {
|
||||
if strings.EqualFold(verb, "Create") {
|
||||
canCreate = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !canCreate {
|
||||
continue
|
||||
}
|
||||
|
||||
ruleApiGroup := rule.APIGroups[0]
|
||||
for _, ruleResource := range rule.Resources {
|
||||
for _, apiResourcesList := range serverResources {
|
||||
gv, err := schema.ParseGroupVersion(apiResourcesList.GroupVersion)
|
||||
if err != nil {
|
||||
gv = schema.GroupVersion{}
|
||||
}
|
||||
if ruleApiGroup == gv.Group {
|
||||
for _, apiResource := range apiResourcesList.APIResources {
|
||||
if apiResource.Name == ruleResource {
|
||||
resourceList = append(resourceList, metav1.GroupKind{Group: ruleApiGroup, Kind: apiResource.Kind})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
globalProj := v1alpha1.AppProject{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "AppProject",
|
||||
APIVersion: "argoproj.io/v1alpha1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: projName},
|
||||
Spec: v1alpha1.AppProjectSpec{},
|
||||
}
|
||||
globalProj.Spec.NamespaceResourceWhitelist = resourceList
|
||||
return globalProj
|
||||
}
|
||||
57
cmd/argocd-util/commands/project_allowlist_test.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/undefinedlabs/go-mpatch"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/discovery"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
func TestProjectAllowListGen(t *testing.T) {
|
||||
useMock := true
|
||||
rules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||
overrides := &clientcmd.ConfigOverrides{}
|
||||
clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(rules, overrides)
|
||||
|
||||
if useMock {
|
||||
var patchClientConfig *mpatch.Patch
|
||||
patchClientConfig, err := mpatch.PatchInstanceMethodByName(reflect.TypeOf(clientConfig), "ClientConfig", func(*clientcmd.DeferredLoadingClientConfig) (*restclient.Config, error) {
|
||||
return nil, nil
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
patch, err := mpatch.PatchMethod(discovery.NewDiscoveryClientForConfig, func(c *restclient.Config) (*discovery.DiscoveryClient, error) {
|
||||
return &discovery.DiscoveryClient{LegacyPrefix: "/api"}, nil
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
var patchSeverPreferedResources *mpatch.Patch
|
||||
discoClient := &discovery.DiscoveryClient{}
|
||||
patchSeverPreferedResources, err = mpatch.PatchInstanceMethodByName(reflect.TypeOf(discoClient), "ServerPreferredResources", func(*discovery.DiscoveryClient) ([]*metav1.APIResourceList, error) {
|
||||
res := metav1.APIResource{
|
||||
Name: "services",
|
||||
Kind: "Service",
|
||||
}
|
||||
resourceList := []*metav1.APIResourceList{{APIResources: []metav1.APIResource{res}}}
|
||||
return resourceList, nil
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
defer func() {
|
||||
err = patchClientConfig.Unpatch()
|
||||
assert.NoError(t, err)
|
||||
err = patch.Unpatch()
|
||||
assert.NoError(t, err)
|
||||
err = patchSeverPreferedResources.Unpatch()
|
||||
err = patch.Unpatch()
|
||||
}()
|
||||
}
|
||||
|
||||
globalProj := generateProjectAllowList(clientConfig, "testdata/test_clusterrole.yaml", "testproj")
|
||||
assert.True(t, len(globalProj.Spec.NamespaceResourceWhitelist) > 0)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -10,9 +11,8 @@ import (
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
appclient "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/typed/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/diff"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
"github.com/spf13/cobra"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -21,13 +21,15 @@ import (
|
||||
|
||||
func NewProjectsCommand() *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "projects",
|
||||
Use: "projects",
|
||||
Short: "Utility commands operate on ArgoCD Projects",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
},
|
||||
}
|
||||
|
||||
command.AddCommand(NewUpdatePolicyRuleCommand())
|
||||
command.AddCommand(NewProjectAllowListGenCommand())
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -69,9 +71,9 @@ func saveProject(updated v1alpha1.AppProject, orig v1alpha1.AppProject, projects
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = diff.PrintDiff(updated.Name, target, live)
|
||||
_ = cli.PrintDiff(updated.Name, target, live)
|
||||
if !dryRun {
|
||||
_, err = projectsIf.Update(&updated)
|
||||
_, err = projectsIf.Update(context.Background(), &updated, v1.UpdateOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -145,7 +147,7 @@ func NewUpdatePolicyRuleCommand() *cobra.Command {
|
||||
}
|
||||
|
||||
func updateProjects(projIf appclient.AppProjectInterface, projectGlob string, rolePattern string, action string, modification func(string, string) string, dryRun bool) error {
|
||||
projects, err := projIf.List(v1.ListOptions{})
|
||||
projects, err := projIf.List(context.Background(), v1.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -35,11 +36,11 @@ func TestUpdateProjects_FindMatchingProject(t *testing.T) {
|
||||
err = updateProjects(clientset.ArgoprojV1alpha1().AppProjects(namespace), "ba*", "*", "set", modification, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
fooProj, err := clientset.ArgoprojV1alpha1().AppProjects(namespace).Get("foo", v1.GetOptions{})
|
||||
fooProj, err := clientset.ArgoprojV1alpha1().AppProjects(namespace).Get(context.Background(), "foo", v1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, fooProj.Spec.Roles[0].Policies, 0)
|
||||
|
||||
barProj, err := clientset.ArgoprojV1alpha1().AppProjects(namespace).Get("bar", v1.GetOptions{})
|
||||
barProj, err := clientset.ArgoprojV1alpha1().AppProjects(namespace).Get(context.Background(), "bar", v1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, barProj.Spec.Roles[0].Policies, []string{"p, proj:bar:test, *, set, bar/*, allow"})
|
||||
}
|
||||
@@ -52,7 +53,7 @@ func TestUpdateProjects_FindMatchingRole(t *testing.T) {
|
||||
err = updateProjects(clientset.ArgoprojV1alpha1().AppProjects(namespace), "*", "fo*", "set", modification, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
proj, err := clientset.ArgoprojV1alpha1().AppProjects(namespace).Get("proj", v1.GetOptions{})
|
||||
proj, err := clientset.ArgoprojV1alpha1().AppProjects(namespace).Get(context.Background(), "proj", v1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, proj.Spec.Roles[0].Policies, []string{"p, proj:proj:foo, *, set, proj/*, allow"})
|
||||
assert.Len(t, proj.Spec.Roles[1].Policies, 0)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -12,9 +12,7 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/diff"
|
||||
healthutil "github.com/argoproj/gitops-engine/pkg/health"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
"github.com/ghodss/yaml"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -29,6 +27,7 @@ import (
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/argo/normalizers"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
"github.com/argoproj/argo-cd/util/lua"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
@@ -73,7 +72,7 @@ func (opts *settingsOpts) createSettingsManager() (*settings.SettingsManager, er
|
||||
return nil, err
|
||||
}
|
||||
|
||||
argocdCM, err = realClientset.CoreV1().ConfigMaps(ns).Get(common.ArgoCDConfigMapName, v1.GetOptions{})
|
||||
argocdCM, err = realClientset.CoreV1().ConfigMaps(ns).Get(context.Background(), common.ArgoCDConfigMapName, v1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -105,7 +104,7 @@ func (opts *settingsOpts) createSettingsManager() (*settings.SettingsManager, er
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
argocdSecret, err = realClientset.CoreV1().Secrets(ns).Get(common.ArgoCDSecretName, v1.GetOptions{})
|
||||
argocdSecret, err = realClientset.CoreV1().Secrets(ns).Get(context.Background(), common.ArgoCDSecretName, v1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -401,7 +400,7 @@ argocd-util settings resource-overrides ignore-differences ./deploy.yaml --argoc
|
||||
|
||||
executeResourceOverrideCommand(cmdCtx, args, func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride) {
|
||||
gvk := res.GroupVersionKind()
|
||||
if override.IgnoreDifferences == "" {
|
||||
if len(override.IgnoreDifferences.JSONPointers) == 0 {
|
||||
_, _ = fmt.Printf("Ignore differences are not configured for '%s/%s'\n", gvk.Group, gvk.Kind)
|
||||
return
|
||||
}
|
||||
@@ -423,7 +422,7 @@ argocd-util settings resource-overrides ignore-differences ./deploy.yaml --argoc
|
||||
}
|
||||
|
||||
_, _ = fmt.Printf("Following fields are ignored:\n\n")
|
||||
_ = diff.PrintDiff(res.GetName(), &res, normalizedRes)
|
||||
_ = cli.PrintDiff(res.GetName(), &res, normalizedRes)
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -538,7 +537,7 @@ argocd-util settings resource-overrides action run /tmp/deploy.yaml restart --ar
|
||||
}
|
||||
|
||||
_, _ = fmt.Printf("Following fields have been changed:\n\n")
|
||||
_ = diff.PrintDiff(res.GetName(), &res, modifiedRes)
|
||||
_ = cli.PrintDiff(res.GetName(), &res, modifiedRes)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
utils "github.com/argoproj/argo-cd/util/io"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
|
||||
utils "github.com/argoproj/gitops-engine/pkg/utils/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -195,7 +195,7 @@ admissionregistration.k8s.io/MutatingWebhookConfiguration:
|
||||
jsonPointers:
|
||||
- /webhooks/0/clientConfig/caBundle`,
|
||||
},
|
||||
containsSummary: "1 resource overrides",
|
||||
containsSummary: "2 resource overrides",
|
||||
},
|
||||
}
|
||||
for name := range testCases {
|
||||
|
||||
787
cmd/argocd-util/commands/testdata/test_clusterrole.yaml
vendored
Normal file
@@ -0,0 +1,787 @@
|
||||
aggregationRule:
|
||||
clusterRoleSelectors:
|
||||
- matchLabels:
|
||||
rbac.authorization.k8s.io/aggregate-to-admin: "true"
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
labels:
|
||||
kubernetes.io/bootstrapping: rbac-defaults
|
||||
name: admin
|
||||
rules:
|
||||
- apiGroups:
|
||||
- argoproj.io
|
||||
resources:
|
||||
- workflows
|
||||
- workflows/finalizers
|
||||
- workflowtemplates
|
||||
- workflowtemplates/finalizers
|
||||
- cronworkflows
|
||||
- cronworkflows/finalizers
|
||||
- clusterworkflowtemplates
|
||||
- clusterworkflowtemplates/finalizers
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- deletecollection
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- argoproj.io
|
||||
resources:
|
||||
- gateways
|
||||
- gateways/finalizers
|
||||
- sensors
|
||||
- sensors/finalizers
|
||||
- eventsources
|
||||
- eventsources/finalizers
|
||||
- eventbuses
|
||||
- eventbuses/finalizers
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- deletecollection
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- argoproj.io
|
||||
resources:
|
||||
- rollouts
|
||||
- rollouts/scale
|
||||
- experiments
|
||||
- analysistemplates
|
||||
- clusteranalysistemplates
|
||||
- analysisruns
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- deletecollection
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- metrics.k8s.io
|
||||
resources:
|
||||
- pods
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- iammanager.keikoproj.io
|
||||
resources:
|
||||
- iamroles
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- pods/attach
|
||||
- pods/exec
|
||||
- pods/portforward
|
||||
- pods/proxy
|
||||
- secrets
|
||||
- services/proxy
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- serviceaccounts
|
||||
verbs:
|
||||
- impersonate
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- pods
|
||||
- pods/attach
|
||||
- pods/exec
|
||||
- pods/portforward
|
||||
- pods/proxy
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- deletecollection
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- configmaps
|
||||
- endpoints
|
||||
- persistentvolumeclaims
|
||||
- replicationcontrollers
|
||||
- replicationcontrollers/scale
|
||||
- secrets
|
||||
- serviceaccounts
|
||||
- services
|
||||
- services/proxy
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- deletecollection
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- daemonsets
|
||||
- deployments
|
||||
- deployments/rollback
|
||||
- deployments/scale
|
||||
- replicasets
|
||||
- replicasets/scale
|
||||
- statefulsets
|
||||
- statefulsets/scale
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- deletecollection
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- autoscaling
|
||||
resources:
|
||||
- horizontalpodautoscalers
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- deletecollection
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- batch
|
||||
resources:
|
||||
- cronjobs
|
||||
- jobs
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- deletecollection
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- extensions
|
||||
resources:
|
||||
- daemonsets
|
||||
- deployments
|
||||
- deployments/rollback
|
||||
- deployments/scale
|
||||
- ingresses
|
||||
- networkpolicies
|
||||
- replicasets
|
||||
- replicasets/scale
|
||||
- replicationcontrollers/scale
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- deletecollection
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- policy
|
||||
resources:
|
||||
- poddisruptionbudgets
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- deletecollection
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- networkpolicies
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- deletecollection
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- ingresses
|
||||
verbs:
|
||||
- create
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- ingresses
|
||||
verbs:
|
||||
- delete
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- ingresses
|
||||
verbs:
|
||||
- deletecollection
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- ingresses
|
||||
verbs:
|
||||
- patch
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- ingresses
|
||||
verbs:
|
||||
- update
|
||||
- apiGroups:
|
||||
- argoproj.io
|
||||
resources:
|
||||
- workflows
|
||||
- workflows/finalizers
|
||||
- workflowtemplates
|
||||
- workflowtemplates/finalizers
|
||||
- cronworkflows
|
||||
- cronworkflows/finalizers
|
||||
- clusterworkflowtemplates
|
||||
- clusterworkflowtemplates/finalizers
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- argoproj.io
|
||||
resources:
|
||||
- gateways
|
||||
- gateways/finalizers
|
||||
- sensors
|
||||
- sensors/finalizers
|
||||
- eventsources
|
||||
- eventsources/finalizers
|
||||
- eventbuses
|
||||
- eventbuses/finalizers
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- argoproj.io
|
||||
resources:
|
||||
- rollouts
|
||||
- rollouts/scale
|
||||
- experiments
|
||||
- analysistemplates
|
||||
- clusteranalysistemplates
|
||||
- analysisruns
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resourceNames:
|
||||
- prometheus-k8s-prometheus-1
|
||||
- prometheus-k8s-prometheus-0
|
||||
resources:
|
||||
- pods/portforward
|
||||
verbs:
|
||||
- create
|
||||
- apiGroups:
|
||||
- networking.istio.io
|
||||
resources:
|
||||
- virtualservices
|
||||
- destinationrules
|
||||
- serviceentries
|
||||
- envoyfilters
|
||||
- gateways
|
||||
- sidecars
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- configmaps
|
||||
- endpoints
|
||||
- persistentvolumeclaims
|
||||
- pods
|
||||
- replicationcontrollers
|
||||
- replicationcontrollers/scale
|
||||
- serviceaccounts
|
||||
- services
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- bindings
|
||||
- events
|
||||
- limitranges
|
||||
- namespaces/status
|
||||
- pods/log
|
||||
- pods/status
|
||||
- replicationcontrollers/status
|
||||
- resourcequotas
|
||||
- resourcequotas/status
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- namespaces
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- daemonsets
|
||||
- deployments
|
||||
- deployments/scale
|
||||
- replicasets
|
||||
- replicasets/scale
|
||||
- statefulsets
|
||||
- statefulsets/scale
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- autoscaling
|
||||
resources:
|
||||
- horizontalpodautoscalers
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- batch
|
||||
resources:
|
||||
- cronjobs
|
||||
- jobs
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- extensions
|
||||
resources:
|
||||
- daemonsets
|
||||
- deployments
|
||||
- deployments/scale
|
||||
- ingresses
|
||||
- networkpolicies
|
||||
- replicasets
|
||||
- replicasets/scale
|
||||
- replicationcontrollers/scale
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- policy
|
||||
resources:
|
||||
- poddisruptionbudgets
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- networkpolicies
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- controllerrevisions
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- controllerrevisions
|
||||
verbs:
|
||||
- list
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- controllerrevisions
|
||||
verbs:
|
||||
- watch
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- ingresses
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- ingresses
|
||||
verbs:
|
||||
- list
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- ingresses
|
||||
verbs:
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- persistentvolumeclaims/status
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- persistentvolumeclaims/status
|
||||
verbs:
|
||||
- list
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- persistentvolumeclaims/status
|
||||
verbs:
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- services/status
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- services/status
|
||||
verbs:
|
||||
- list
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- services/status
|
||||
verbs:
|
||||
- watch
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- daemonsets/status
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- daemonsets/status
|
||||
verbs:
|
||||
- list
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- daemonsets/status
|
||||
verbs:
|
||||
- watch
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- deployments/status
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- deployments/status
|
||||
verbs:
|
||||
- list
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- deployments/status
|
||||
verbs:
|
||||
- watch
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- replicasets/status
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- replicasets/status
|
||||
verbs:
|
||||
- list
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- replicasets/status
|
||||
verbs:
|
||||
- watch
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- statefulsets/status
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- statefulsets/status
|
||||
verbs:
|
||||
- list
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- statefulsets/status
|
||||
verbs:
|
||||
- watch
|
||||
- apiGroups:
|
||||
- autoscaling
|
||||
resources:
|
||||
- horizontalpodautoscalers/status
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- autoscaling
|
||||
resources:
|
||||
- horizontalpodautoscalers/status
|
||||
verbs:
|
||||
- list
|
||||
- apiGroups:
|
||||
- autoscaling
|
||||
resources:
|
||||
- horizontalpodautoscalers/status
|
||||
verbs:
|
||||
- watch
|
||||
- apiGroups:
|
||||
- batch
|
||||
resources:
|
||||
- cronjobs/status
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- batch
|
||||
resources:
|
||||
- cronjobs/status
|
||||
verbs:
|
||||
- list
|
||||
- apiGroups:
|
||||
- batch
|
||||
resources:
|
||||
- cronjobs/status
|
||||
verbs:
|
||||
- watch
|
||||
- apiGroups:
|
||||
- batch
|
||||
resources:
|
||||
- jobs/status
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- batch
|
||||
resources:
|
||||
- jobs/status
|
||||
verbs:
|
||||
- list
|
||||
- apiGroups:
|
||||
- batch
|
||||
resources:
|
||||
- jobs/status
|
||||
verbs:
|
||||
- watch
|
||||
- apiGroups:
|
||||
- extensions
|
||||
resources:
|
||||
- daemonsets/status
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- extensions
|
||||
resources:
|
||||
- daemonsets/status
|
||||
verbs:
|
||||
- list
|
||||
- apiGroups:
|
||||
- extensions
|
||||
resources:
|
||||
- daemonsets/status
|
||||
verbs:
|
||||
- watch
|
||||
- apiGroups:
|
||||
- extensions
|
||||
resources:
|
||||
- deployments/status
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- extensions
|
||||
resources:
|
||||
- deployments/status
|
||||
verbs:
|
||||
- list
|
||||
- apiGroups:
|
||||
- extensions
|
||||
resources:
|
||||
- deployments/status
|
||||
verbs:
|
||||
- watch
|
||||
- apiGroups:
|
||||
- extensions
|
||||
resources:
|
||||
- ingresses/status
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- extensions
|
||||
resources:
|
||||
- ingresses/status
|
||||
verbs:
|
||||
- list
|
||||
- apiGroups:
|
||||
- extensions
|
||||
resources:
|
||||
- ingresses/status
|
||||
verbs:
|
||||
- watch
|
||||
- apiGroups:
|
||||
- extensions
|
||||
resources:
|
||||
- replicasets/status
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- extensions
|
||||
resources:
|
||||
- replicasets/status
|
||||
verbs:
|
||||
- list
|
||||
- apiGroups:
|
||||
- extensions
|
||||
resources:
|
||||
- replicasets/status
|
||||
verbs:
|
||||
- watch
|
||||
- apiGroups:
|
||||
- policy
|
||||
resources:
|
||||
- poddisruptionbudgets/status
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- policy
|
||||
resources:
|
||||
- poddisruptionbudgets/status
|
||||
verbs:
|
||||
- list
|
||||
- apiGroups:
|
||||
- policy
|
||||
resources:
|
||||
- poddisruptionbudgets/status
|
||||
verbs:
|
||||
- watch
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- ingresses/status
|
||||
verbs:
|
||||
- get
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- ingresses/status
|
||||
verbs:
|
||||
- list
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- ingresses/status
|
||||
verbs:
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- events
|
||||
verbs:
|
||||
- create
|
||||
- patch
|
||||
- apiGroups:
|
||||
- extensions
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- ingresses/status
|
||||
verbs:
|
||||
- update
|
||||
- apiGroups:
|
||||
- monitoring.coreos.com
|
||||
resources:
|
||||
- prometheusrules
|
||||
verbs:
|
||||
- get
|
||||
- watch
|
||||
- list
|
||||
- update
|
||||
- delete
|
||||
- create
|
||||
- apiGroups:
|
||||
- hpa.orkaproj.io
|
||||
resources:
|
||||
- hpaalgoes
|
||||
verbs:
|
||||
- get
|
||||
- watch
|
||||
- list
|
||||
- update
|
||||
- delete
|
||||
- create
|
||||
- apiGroups:
|
||||
- networking.istio.io
|
||||
resources:
|
||||
- virtualservices
|
||||
- destinationrules
|
||||
- serviceentries
|
||||
- envoyfilters
|
||||
- gateways
|
||||
- sidecars
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- create
|
||||
- update
|
||||
- delete
|
||||
- patch
|
||||
- watch
|
||||
- apiGroups:
|
||||
- authorization.k8s.io
|
||||
resources:
|
||||
- localsubjectaccessreviews
|
||||
verbs:
|
||||
- create
|
||||
- apiGroups:
|
||||
- rbac.authorization.k8s.io
|
||||
resources:
|
||||
- rolebindings
|
||||
- roles
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- deletecollection
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
@@ -1,683 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"syscall"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
"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/cmd/argocd-util/commands"
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"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/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"
|
||||
// load the azure plugin (required to authenticate with AKS clusters).
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/azure"
|
||||
)
|
||||
|
||||
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 (
|
||||
logFormat string
|
||||
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.AddCommand(commands.NewProjectsCommand())
|
||||
command.AddCommand(commands.NewSettingsCommand())
|
||||
|
||||
command.Flags().StringVar(&logFormat, "logformat", "text", "Set the logging format. One of: text|json")
|
||||
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.Debug(redactor(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()
|
||||
errors.CheckError(err)
|
||||
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]unstructured.Unstructured)
|
||||
configMaps, err := acdClients.configMaps.List(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
// referencedSecrets holds any secrets referenced in the argocd-cm configmap. These
|
||||
// secrets need to be imported too
|
||||
var referencedSecrets map[string]bool
|
||||
for _, cm := range configMaps.Items {
|
||||
if isArgoCDConfigMap(cm.GetName()) {
|
||||
pruneObjects[kube.ResourceKey{Group: "", Kind: "ConfigMap", Name: cm.GetName()}] = cm
|
||||
}
|
||||
if cm.GetName() == common.ArgoCDConfigMapName {
|
||||
referencedSecrets = getReferencedSecrets(cm)
|
||||
}
|
||||
}
|
||||
|
||||
secrets, err := acdClients.secrets.List(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, secret := range secrets.Items {
|
||||
if isArgoCDSecret(referencedSecrets, secret) {
|
||||
pruneObjects[kube.ResourceKey{Group: "", Kind: "Secret", Name: secret.GetName()}] = secret
|
||||
}
|
||||
}
|
||||
applications, err := acdClients.applications.List(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, app := range applications.Items {
|
||||
pruneObjects[kube.ResourceKey{Group: "argoproj.io", Kind: "Application", Name: app.GetName()}] = app
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// Create or replace existing object
|
||||
backupObjects, err := kube.SplitYAML(string(input))
|
||||
errors.CheckError(err)
|
||||
for _, bakObj := range backupObjects {
|
||||
gvk := bakObj.GroupVersionKind()
|
||||
key := kube.ResourceKey{Group: gvk.Group, Kind: gvk.Kind, Name: bakObj.GetName()}
|
||||
liveObj, exists := pruneObjects[key]
|
||||
delete(pruneObjects, key)
|
||||
var dynClient dynamic.ResourceInterface
|
||||
switch bakObj.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(bakObj, metav1.CreateOptions{})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
fmt.Printf("%s/%s %s created%s\n", gvk.Group, gvk.Kind, bakObj.GetName(), dryRunMsg)
|
||||
} else if specsEqual(*bakObj, liveObj) {
|
||||
fmt.Printf("%s/%s %s unchanged%s\n", gvk.Group, gvk.Kind, bakObj.GetName(), dryRunMsg)
|
||||
} else {
|
||||
if !dryRun {
|
||||
newLive := updateLive(bakObj, &liveObj)
|
||||
_, err = dynClient.Update(newLive, metav1.UpdateOptions{})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
fmt.Printf("%s/%s %s updated%s\n", gvk.Group, gvk.Kind, bakObj.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)
|
||||
bw := bufio.NewWriter(f)
|
||||
writer = bw
|
||||
defer func() {
|
||||
err = bw.Flush()
|
||||
errors.CheckError(err)
|
||||
err = f.Close()
|
||||
errors.CheckError(err)
|
||||
}()
|
||||
}
|
||||
|
||||
acdClients := newArgoCDClientsets(config, namespace)
|
||||
acdConfigMap, err := acdClients.configMaps.Get(common.ArgoCDConfigMapName, metav1.GetOptions{})
|
||||
errors.CheckError(err)
|
||||
export(writer, *acdConfigMap)
|
||||
acdRBACConfigMap, err := acdClients.configMaps.Get(common.ArgoCDRBACConfigMapName, metav1.GetOptions{})
|
||||
errors.CheckError(err)
|
||||
export(writer, *acdRBACConfigMap)
|
||||
acdKnownHostsConfigMap, err := acdClients.configMaps.Get(common.ArgoCDKnownHostsConfigMapName, metav1.GetOptions{})
|
||||
errors.CheckError(err)
|
||||
export(writer, *acdKnownHostsConfigMap)
|
||||
acdTLSCertsConfigMap, err := acdClients.configMaps.Get(common.ArgoCDTLSCertsConfigMapName, metav1.GetOptions{})
|
||||
errors.CheckError(err)
|
||||
export(writer, *acdTLSCertsConfigMap)
|
||||
|
||||
referencedSecrets := getReferencedSecrets(*acdConfigMap)
|
||||
secrets, err := acdClients.secrets.List(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, secret := range secrets.Items {
|
||||
if isArgoCDSecret(referencedSecrets, secret) {
|
||||
export(writer, secret)
|
||||
}
|
||||
}
|
||||
projects, err := acdClients.projects.List(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, proj := range projects.Items {
|
||||
export(writer, proj)
|
||||
}
|
||||
applications, err := acdClients.applications.List(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, app := range applications.Items {
|
||||
export(writer, app)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// Referenced repository secrets
|
||||
if reposRAW, ok := cm.Data["repositories"]; ok {
|
||||
repos := make([]settings.Repository, 0)
|
||||
err := yaml.Unmarshal([]byte(reposRAW), &repos)
|
||||
errors.CheckError(err)
|
||||
for _, cred := range repos {
|
||||
if cred.PasswordSecret != nil {
|
||||
referencedSecrets[cred.PasswordSecret.Name] = true
|
||||
}
|
||||
if cred.SSHPrivateKeySecret != nil {
|
||||
referencedSecrets[cred.SSHPrivateKeySecret.Name] = true
|
||||
}
|
||||
if cred.UsernameSecret != nil {
|
||||
referencedSecrets[cred.UsernameSecret.Name] = true
|
||||
}
|
||||
if cred.TLSClientCertDataSecret != nil {
|
||||
referencedSecrets[cred.TLSClientCertDataSecret.Name] = true
|
||||
}
|
||||
if cred.TLSClientCertKeySecret != nil {
|
||||
referencedSecrets[cred.TLSClientCertKeySecret.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Referenced repository credentials secrets
|
||||
if reposRAW, ok := cm.Data["repository.credentials"]; ok {
|
||||
creds := make([]settings.RepositoryCredentials, 0)
|
||||
err := yaml.Unmarshal([]byte(reposRAW), &creds)
|
||||
errors.CheckError(err)
|
||||
for _, cred := range creds {
|
||||
if cred.PasswordSecret != nil {
|
||||
referencedSecrets[cred.PasswordSecret.Name] = true
|
||||
}
|
||||
if cred.SSHPrivateKeySecret != nil {
|
||||
referencedSecrets[cred.SSHPrivateKeySecret.Name] = true
|
||||
}
|
||||
if cred.UsernameSecret != nil {
|
||||
referencedSecrets[cred.UsernameSecret.Name] = true
|
||||
}
|
||||
if cred.TLSClientCertDataSecret != nil {
|
||||
referencedSecrets[cred.TLSClientCertDataSecret.Name] = true
|
||||
}
|
||||
if cred.TLSClientCertKeySecret != nil {
|
||||
referencedSecrets[cred.TLSClientCertKeySecret.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return referencedSecrets
|
||||
}
|
||||
|
||||
// isArgoCDSecret returns whether or not the given secret is a part of Argo CD configuration
|
||||
// (e.g. argocd-secret, repo credentials, or cluster credentials)
|
||||
func isArgoCDSecret(repoSecretRefs map[string]bool, un unstructured.Unstructured) bool {
|
||||
secretName := un.GetName()
|
||||
if secretName == common.ArgoCDSecretName {
|
||||
return true
|
||||
}
|
||||
if repoSecretRefs != nil {
|
||||
if _, ok := repoSecretRefs[secretName]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if labels := un.GetLabels(); labels != nil {
|
||||
if _, ok := labels[common.LabelKeySecretType]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if annotations := un.GetAnnotations(); annotations != nil {
|
||||
if annotations[common.AnnotationKeyManagedBy] == common.AnnotationValueManagedByArgoCD {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isArgoCDConfigMap returns true if the configmap name is one of argo cd's well known configmaps
|
||||
func isArgoCDConfigMap(name string) bool {
|
||||
switch name {
|
||||
case common.ArgoCDConfigMapName, common.ArgoCDRBACConfigMapName, common.ArgoCDKnownHostsConfigMapName, common.ArgoCDTLSCertsConfigMapName:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
// specsEqual returns if the spec, data, labels, annotations, and finalizers of the two
|
||||
// supplied objects are equal, indicating that no update is necessary during importing
|
||||
func specsEqual(left, right unstructured.Unstructured) bool {
|
||||
if !reflect.DeepEqual(left.GetAnnotations(), right.GetAnnotations()) {
|
||||
return false
|
||||
}
|
||||
if !reflect.DeepEqual(left.GetLabels(), right.GetLabels()) {
|
||||
return false
|
||||
}
|
||||
if !reflect.DeepEqual(left.GetFinalizers(), right.GetFinalizers()) {
|
||||
return false
|
||||
}
|
||||
switch left.GetKind() {
|
||||
case "Secret", "ConfigMap":
|
||||
leftData, _, _ := unstructured.NestedMap(left.Object, "data")
|
||||
rightData, _, _ := unstructured.NestedMap(right.Object, "data")
|
||||
return reflect.DeepEqual(leftData, rightData)
|
||||
case "AppProject":
|
||||
leftSpec, _, _ := unstructured.NestedMap(left.Object, "spec")
|
||||
rightSpec, _, _ := unstructured.NestedMap(right.Object, "spec")
|
||||
return reflect.DeepEqual(leftSpec, rightSpec)
|
||||
case "Application":
|
||||
leftSpec, _, _ := unstructured.NestedMap(left.Object, "spec")
|
||||
rightSpec, _, _ := unstructured.NestedMap(right.Object, "spec")
|
||||
leftStatus, _, _ := unstructured.NestedMap(left.Object, "status")
|
||||
rightStatus, _, _ := unstructured.NestedMap(right.Object, "status")
|
||||
// reconciledAt and observedAt are constantly changing and we ignore any diff there
|
||||
delete(leftStatus, "reconciledAt")
|
||||
delete(rightStatus, "reconciledAt")
|
||||
delete(leftStatus, "observedAt")
|
||||
delete(rightStatus, "observedAt")
|
||||
return reflect.DeepEqual(leftSpec, rightSpec) && reflect.DeepEqual(leftStatus, rightStatus)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// updateLive replaces the live object's finalizers, spec, annotations, labels, and data from the
|
||||
// backup object but leaves all other fields intact (status, other metadata, etc...)
|
||||
func updateLive(bak, live *unstructured.Unstructured) *unstructured.Unstructured {
|
||||
newLive := live.DeepCopy()
|
||||
newLive.SetAnnotations(bak.GetAnnotations())
|
||||
newLive.SetLabels(bak.GetLabels())
|
||||
newLive.SetFinalizers(bak.GetFinalizers())
|
||||
switch live.GetKind() {
|
||||
case "Secret", "ConfigMap":
|
||||
newLive.Object["data"] = bak.Object["data"]
|
||||
case "AppProject":
|
||||
newLive.Object["spec"] = bak.Object["spec"]
|
||||
case "Application":
|
||||
newLive.Object["spec"] = bak.Object["spec"]
|
||||
if _, ok := bak.Object["status"]; ok {
|
||||
newLive.Object["status"] = bak.Object["status"]
|
||||
}
|
||||
}
|
||||
return newLive
|
||||
}
|
||||
|
||||
// export writes the unstructured object and removes extraneous cruft from output before writing
|
||||
func export(w io.Writer, un unstructured.Unstructured) {
|
||||
name := un.GetName()
|
||||
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.RawRestConfig(), namespace, output)
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(command)
|
||||
return command
|
||||
}
|
||||
|
||||
func iterateStringFields(obj interface{}, callback func(name string, val string) string) {
|
||||
if mapField, ok := obj.(map[string]interface{}); ok {
|
||||
for field, val := range mapField {
|
||||
if strVal, ok := val.(string); ok {
|
||||
mapField[field] = callback(field, strVal)
|
||||
} else {
|
||||
iterateStringFields(val, callback)
|
||||
}
|
||||
}
|
||||
} else if arrayField, ok := obj.([]interface{}); ok {
|
||||
for i := range arrayField {
|
||||
iterateStringFields(arrayField[i], callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func redactor(dirtyString string) string {
|
||||
config := make(map[string]interface{})
|
||||
err := yaml.Unmarshal([]byte(dirtyString), &config)
|
||||
errors.CheckError(err)
|
||||
iterateStringFields(config, func(name string, val string) string {
|
||||
if name == "clientSecret" || name == "secret" || name == "bindPW" {
|
||||
return "********"
|
||||
} else {
|
||||
return val
|
||||
}
|
||||
})
|
||||
data, err := yaml.Marshal(config)
|
||||
errors.CheckError(err)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := NewCommand().Execute(); err != nil {
|
||||
if err := commands.NewCommand().Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/io"
|
||||
timeutil "github.com/argoproj/pkg/time"
|
||||
"github.com/ghodss/yaml"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -23,6 +21,8 @@ import (
|
||||
"github.com/argoproj/argo-cd/pkg/apiclient/session"
|
||||
"github.com/argoproj/argo-cd/server/rbacpolicy"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
"github.com/argoproj/argo-cd/util/io"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
sessionutil "github.com/argoproj/argo-cd/util/session"
|
||||
)
|
||||
|
||||
@@ -20,8 +20,6 @@ import (
|
||||
"github.com/argoproj/gitops-engine/pkg/health"
|
||||
"github.com/argoproj/gitops-engine/pkg/sync/hook"
|
||||
"github.com/argoproj/gitops-engine/pkg/sync/ignore"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
argoio "github.com/argoproj/gitops-engine/pkg/utils/io"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
"github.com/ghodss/yaml"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -31,14 +29,17 @@ import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/utils/pointer"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/controller"
|
||||
"github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/pkg/apiclient/application"
|
||||
applicationpkg "github.com/argoproj/argo-cd/pkg/apiclient/application"
|
||||
clusterpkg "github.com/argoproj/argo-cd/pkg/apiclient/cluster"
|
||||
projectpkg "github.com/argoproj/argo-cd/pkg/apiclient/project"
|
||||
"github.com/argoproj/argo-cd/pkg/apiclient/settings"
|
||||
settingspkg "github.com/argoproj/argo-cd/pkg/apiclient/settings"
|
||||
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
repoapiclient "github.com/argoproj/argo-cd/reposerver/apiclient"
|
||||
@@ -46,7 +47,9 @@ import (
|
||||
"github.com/argoproj/argo-cd/util/argo"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/config"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
argoio "github.com/argoproj/argo-cd/util/io"
|
||||
argokube "github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/templates"
|
||||
"github.com/argoproj/argo-cd/util/text/label"
|
||||
@@ -92,6 +95,7 @@ func NewApplicationCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
command.AddCommand(NewApplicationPatchCommand(clientOpts))
|
||||
command.AddCommand(NewApplicationPatchResourceCommand(clientOpts))
|
||||
command.AddCommand(NewApplicationResourceActionsCommand(clientOpts))
|
||||
command.AddCommand(NewApplicationListResourcesCommand(clientOpts))
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -121,7 +125,7 @@ func NewApplicationCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.
|
||||
argocd app create nginx-ingress --repo https://kubernetes-charts.storage.googleapis.com --helm-chart nginx-ingress --revision 1.24.3 --dest-namespace default --dest-server https://kubernetes.default.svc
|
||||
|
||||
# Create a Kustomize app
|
||||
argocd app create kustomize-guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path kustomize-guestbook --dest-namespace default --dest-server https://kubernetes.default.svc --kustomize-image gcr.io/heptio-images/ks-guestbook-demo=0.1
|
||||
argocd app create kustomize-guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path kustomize-guestbook --dest-namespace default --dest-server https://kubernetes.default.svc --kustomize-image gcr.io/heptio-images/ks-guestbook-demo:0.1
|
||||
|
||||
# Create a app using a custom tool:
|
||||
argocd app create ksane --repo https://github.com/argoproj/argocd-example-apps.git --path plugins/kasane --dest-namespace default --dest-server https://kubernetes.default.svc --config-management-plugin kasane
|
||||
@@ -149,8 +153,15 @@ func NewApplicationCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.
|
||||
log.Fatalf("app name '%s' does not match app spec metadata.name '%s'", args[0], app.Name)
|
||||
}
|
||||
if appName != "" && appName != app.Name {
|
||||
log.Fatalf("--name argument '%s' does not match app spec metadata.name '%s'", appName, app.Name)
|
||||
app.Name = appName
|
||||
}
|
||||
if app.Name == "" {
|
||||
log.Fatalf("app.Name is empty. --name argument can be used to provide app.Name")
|
||||
}
|
||||
setAppSpecOptions(c.Flags(), &app.Spec, &appOpts)
|
||||
setParameterOverrides(&app, appOpts.parameters)
|
||||
setLabels(&app, labels)
|
||||
|
||||
} else {
|
||||
// read arguments
|
||||
if len(args) == 1 {
|
||||
@@ -178,6 +189,7 @@ func NewApplicationCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.
|
||||
appCreateRequest := applicationpkg.ApplicationCreateRequest{
|
||||
Application: app,
|
||||
Upsert: &upsert,
|
||||
Validate: &appOpts.validate,
|
||||
}
|
||||
created, err := appIf.Create(context.Background(), &appCreateRequest)
|
||||
errors.CheckError(err)
|
||||
@@ -203,6 +215,18 @@ func setLabels(app *argoappv1.Application, labels []string) {
|
||||
app.SetLabels(mapLabels)
|
||||
}
|
||||
|
||||
func getInfos(infos []string) []*argoappv1.Info {
|
||||
mapInfos, err := label.Parse(infos)
|
||||
errors.CheckError(err)
|
||||
sliceInfos := make([]*argoappv1.Info, len(mapInfos))
|
||||
i := 0
|
||||
for key, element := range mapInfos {
|
||||
sliceInfos[i] = &argoappv1.Info{Name: key, Value: element}
|
||||
i++
|
||||
}
|
||||
return sliceInfos
|
||||
}
|
||||
|
||||
func getRefreshType(refresh bool, hardRefresh bool) *string {
|
||||
if hardRefresh {
|
||||
refreshType := string(argoappv1.RefreshTypeHard)
|
||||
@@ -463,8 +487,9 @@ func NewApplicationSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
|
||||
}
|
||||
setParameterOverrides(app, appOpts.parameters)
|
||||
_, err = appIf.UpdateSpec(ctx, &applicationpkg.ApplicationUpdateSpecRequest{
|
||||
Name: &app.Name,
|
||||
Spec: app.Spec,
|
||||
Name: &app.Name,
|
||||
Spec: app.Spec,
|
||||
Validate: &appOpts.validate,
|
||||
})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
@@ -507,6 +532,8 @@ func setAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, ap
|
||||
setHelmOpt(&spec.Source, helmOpts{values: string(data)})
|
||||
case "release-name":
|
||||
setHelmOpt(&spec.Source, helmOpts{releaseName: appOpts.releaseName})
|
||||
case "helm-version":
|
||||
setHelmOpt(&spec.Source, helmOpts{version: appOpts.helmVersion})
|
||||
case "helm-set":
|
||||
setHelmOpt(&spec.Source, helmOpts{helmSets: appOpts.helmSets})
|
||||
case "helm-set-string":
|
||||
@@ -514,9 +541,21 @@ func setAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, ap
|
||||
case "helm-set-file":
|
||||
setHelmOpt(&spec.Source, helmOpts{helmSetFiles: appOpts.helmSetFiles})
|
||||
case "directory-recurse":
|
||||
spec.Source.Directory = &argoappv1.ApplicationSourceDirectory{Recurse: appOpts.directoryRecurse}
|
||||
if spec.Source.Directory != nil {
|
||||
spec.Source.Directory.Recurse = appOpts.directoryRecurse
|
||||
} else {
|
||||
spec.Source.Directory = &argoappv1.ApplicationSourceDirectory{Recurse: appOpts.directoryRecurse}
|
||||
}
|
||||
case "directory-exclude":
|
||||
if spec.Source.Directory != nil {
|
||||
spec.Source.Directory.Exclude = appOpts.directoryExclude
|
||||
} else {
|
||||
spec.Source.Directory = &argoappv1.ApplicationSourceDirectory{Exclude: appOpts.directoryExclude}
|
||||
}
|
||||
case "config-management-plugin":
|
||||
spec.Source.Plugin = &argoappv1.ApplicationSourcePlugin{Name: appOpts.configManagementPlugin}
|
||||
case "dest-name":
|
||||
spec.Destination.Name = appOpts.destName
|
||||
case "dest-server":
|
||||
spec.Destination.Server = appOpts.destServer
|
||||
case "dest-namespace":
|
||||
@@ -531,6 +570,14 @@ func setAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, ap
|
||||
setKustomizeOpt(&spec.Source, kustomizeOpts{images: appOpts.kustomizeImages})
|
||||
case "kustomize-version":
|
||||
setKustomizeOpt(&spec.Source, kustomizeOpts{version: appOpts.kustomizeVersion})
|
||||
case "kustomize-common-label":
|
||||
parsedLabels, err := label.Parse(appOpts.kustomizeCommonLabels)
|
||||
errors.CheckError(err)
|
||||
setKustomizeOpt(&spec.Source, kustomizeOpts{commonLabels: parsedLabels})
|
||||
case "kustomize-common-annotation":
|
||||
parsedAnnotations, err := label.Parse(appOpts.kustomizeCommonAnnotations)
|
||||
errors.CheckError(err)
|
||||
setKustomizeOpt(&spec.Source, kustomizeOpts{commonAnnotations: parsedAnnotations})
|
||||
case "jsonnet-tla-str":
|
||||
setJsonnetOpt(&spec.Source, appOpts.jsonnetTlaStr, false)
|
||||
case "jsonnet-tla-code":
|
||||
@@ -539,13 +586,10 @@ func setAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, ap
|
||||
setJsonnetOptExtVar(&spec.Source, appOpts.jsonnetExtVarStr, false)
|
||||
case "jsonnet-ext-var-code":
|
||||
setJsonnetOptExtVar(&spec.Source, appOpts.jsonnetExtVarCode, true)
|
||||
case "jsonnet-libs":
|
||||
setJsonnetOptLibs(&spec.Source, appOpts.jsonnetLibs)
|
||||
case "sync-policy":
|
||||
switch appOpts.syncPolicy {
|
||||
case "automated":
|
||||
if spec.SyncPolicy == nil {
|
||||
spec.SyncPolicy = &argoappv1.SyncPolicy{}
|
||||
}
|
||||
spec.SyncPolicy.Automated = &argoappv1.SyncPolicyAutomated{}
|
||||
case "none":
|
||||
if spec.SyncPolicy != nil {
|
||||
spec.SyncPolicy.Automated = nil
|
||||
@@ -553,6 +597,11 @@ func setAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, ap
|
||||
if spec.SyncPolicy.IsZero() {
|
||||
spec.SyncPolicy = nil
|
||||
}
|
||||
case "automated", "automatic", "auto":
|
||||
if spec.SyncPolicy == nil {
|
||||
spec.SyncPolicy = &argoappv1.SyncPolicy{}
|
||||
}
|
||||
spec.SyncPolicy.Automated = &argoappv1.SyncPolicyAutomated{}
|
||||
default:
|
||||
log.Fatalf("Invalid sync-policy: %s", appOpts.syncPolicy)
|
||||
}
|
||||
@@ -586,6 +635,12 @@ func setAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, ap
|
||||
}
|
||||
spec.SyncPolicy.Automated.SelfHeal = appOpts.selfHeal
|
||||
}
|
||||
if flags.Changed("allow-empty") {
|
||||
if spec.SyncPolicy == nil || spec.SyncPolicy.Automated == nil {
|
||||
log.Fatal("Cannot set --allow-empty: application not configured with automatic sync")
|
||||
}
|
||||
spec.SyncPolicy.Automated.AllowEmpty = appOpts.allowEmpty
|
||||
}
|
||||
|
||||
return visited
|
||||
}
|
||||
@@ -603,19 +658,33 @@ func setKsonnetOpt(src *argoappv1.ApplicationSource, env *string) {
|
||||
}
|
||||
|
||||
type kustomizeOpts struct {
|
||||
namePrefix string
|
||||
nameSuffix string
|
||||
images []string
|
||||
version string
|
||||
namePrefix string
|
||||
nameSuffix string
|
||||
images []string
|
||||
version string
|
||||
commonLabels map[string]string
|
||||
commonAnnotations map[string]string
|
||||
}
|
||||
|
||||
func setKustomizeOpt(src *argoappv1.ApplicationSource, opts kustomizeOpts) {
|
||||
if src.Kustomize == nil {
|
||||
src.Kustomize = &argoappv1.ApplicationSourceKustomize{}
|
||||
}
|
||||
src.Kustomize.Version = opts.version
|
||||
src.Kustomize.NamePrefix = opts.namePrefix
|
||||
src.Kustomize.NameSuffix = opts.nameSuffix
|
||||
if opts.version != "" {
|
||||
src.Kustomize.Version = opts.version
|
||||
}
|
||||
if opts.namePrefix != "" {
|
||||
src.Kustomize.NamePrefix = opts.namePrefix
|
||||
}
|
||||
if opts.nameSuffix != "" {
|
||||
src.Kustomize.NameSuffix = opts.nameSuffix
|
||||
}
|
||||
if opts.commonLabels != nil {
|
||||
src.Kustomize.CommonLabels = opts.commonLabels
|
||||
}
|
||||
if opts.commonAnnotations != nil {
|
||||
src.Kustomize.CommonAnnotations = opts.commonAnnotations
|
||||
}
|
||||
for _, image := range opts.images {
|
||||
src.Kustomize.MergeImage(argoappv1.KustomizeImage(image))
|
||||
}
|
||||
@@ -628,6 +697,7 @@ type helmOpts struct {
|
||||
valueFiles []string
|
||||
values string
|
||||
releaseName string
|
||||
version string
|
||||
helmSets []string
|
||||
helmSetStrings []string
|
||||
helmSetFiles []string
|
||||
@@ -646,6 +716,9 @@ func setHelmOpt(src *argoappv1.ApplicationSource, opts helmOpts) {
|
||||
if opts.releaseName != "" {
|
||||
src.Helm.ReleaseName = opts.releaseName
|
||||
}
|
||||
if opts.version != "" {
|
||||
src.Helm.Version = opts.version
|
||||
}
|
||||
for _, text := range opts.helmSets {
|
||||
p, err := argoappv1.NewHelmParameter(text, false)
|
||||
if err != nil {
|
||||
@@ -689,38 +762,52 @@ func setJsonnetOptExtVar(src *argoappv1.ApplicationSource, jsonnetExtVar []strin
|
||||
src.Directory.Jsonnet.ExtVars = append(src.Directory.Jsonnet.ExtVars, argoappv1.NewJsonnetVar(j, code))
|
||||
}
|
||||
}
|
||||
func setJsonnetOptLibs(src *argoappv1.ApplicationSource, libs []string) {
|
||||
if src.Directory == nil {
|
||||
src.Directory = &argoappv1.ApplicationSourceDirectory{}
|
||||
}
|
||||
src.Directory.Jsonnet.Libs = append(src.Directory.Jsonnet.Libs, libs...)
|
||||
}
|
||||
|
||||
type appOptions struct {
|
||||
repoURL string
|
||||
appPath string
|
||||
chart string
|
||||
env string
|
||||
revision string
|
||||
revisionHistoryLimit int
|
||||
destServer string
|
||||
destNamespace string
|
||||
parameters []string
|
||||
valuesFiles []string
|
||||
values string
|
||||
releaseName string
|
||||
helmSets []string
|
||||
helmSetStrings []string
|
||||
helmSetFiles []string
|
||||
project string
|
||||
syncPolicy string
|
||||
syncOptions []string
|
||||
autoPrune bool
|
||||
selfHeal bool
|
||||
namePrefix string
|
||||
nameSuffix string
|
||||
directoryRecurse bool
|
||||
configManagementPlugin string
|
||||
jsonnetTlaStr []string
|
||||
jsonnetTlaCode []string
|
||||
jsonnetExtVarStr []string
|
||||
jsonnetExtVarCode []string
|
||||
kustomizeImages []string
|
||||
kustomizeVersion string
|
||||
repoURL string
|
||||
appPath string
|
||||
chart string
|
||||
env string
|
||||
revision string
|
||||
revisionHistoryLimit int
|
||||
destName string
|
||||
destServer string
|
||||
destNamespace string
|
||||
parameters []string
|
||||
valuesFiles []string
|
||||
values string
|
||||
releaseName string
|
||||
helmSets []string
|
||||
helmSetStrings []string
|
||||
helmSetFiles []string
|
||||
helmVersion string
|
||||
project string
|
||||
syncPolicy string
|
||||
syncOptions []string
|
||||
autoPrune bool
|
||||
selfHeal bool
|
||||
allowEmpty bool
|
||||
namePrefix string
|
||||
nameSuffix string
|
||||
directoryRecurse bool
|
||||
configManagementPlugin string
|
||||
jsonnetTlaStr []string
|
||||
jsonnetTlaCode []string
|
||||
jsonnetExtVarStr []string
|
||||
jsonnetExtVarCode []string
|
||||
jsonnetLibs []string
|
||||
kustomizeImages []string
|
||||
kustomizeVersion string
|
||||
kustomizeCommonLabels []string
|
||||
kustomizeCommonAnnotations []string
|
||||
validate bool
|
||||
directoryExclude string
|
||||
}
|
||||
|
||||
func addAppFlags(command *cobra.Command, opts *appOptions) {
|
||||
@@ -731,29 +818,37 @@ func addAppFlags(command *cobra.Command, opts *appOptions) {
|
||||
command.Flags().StringVar(&opts.revision, "revision", "", "The tracking source branch, tag, commit or Helm chart version the application will sync to")
|
||||
command.Flags().IntVar(&opts.revisionHistoryLimit, "revision-history-limit", common.RevisionHistoryLimit, "How many items to keep in revision history")
|
||||
command.Flags().StringVar(&opts.destServer, "dest-server", "", "K8s cluster URL (e.g. https://kubernetes.default.svc)")
|
||||
command.Flags().StringVar(&opts.destName, "dest-name", "", "K8s cluster Name (e.g. minikube)")
|
||||
command.Flags().StringVar(&opts.destNamespace, "dest-namespace", "", "K8s target namespace (overrides the namespace specified in the ksonnet app.yaml)")
|
||||
command.Flags().StringArrayVarP(&opts.parameters, "parameter", "p", []string{}, "set a parameter override (e.g. -p guestbook=image=example/guestbook:latest)")
|
||||
command.Flags().StringArrayVar(&opts.valuesFiles, "values", []string{}, "Helm values file(s) to use")
|
||||
command.Flags().StringVar(&opts.values, "values-literal-file", "", "Filename or URL to import as a literal Helm values block")
|
||||
command.Flags().StringVar(&opts.releaseName, "release-name", "", "Helm release-name")
|
||||
command.Flags().StringVar(&opts.helmVersion, "helm-version", "", "Helm version")
|
||||
command.Flags().StringArrayVar(&opts.helmSets, "helm-set", []string{}, "Helm set values on the command line (can be repeated to set several values: --helm-set key1=val1 --helm-set key2=val2)")
|
||||
command.Flags().StringArrayVar(&opts.helmSetStrings, "helm-set-string", []string{}, "Helm set STRING values on the command line (can be repeated to set several values: --helm-set-string key1=val1 --helm-set-string key2=val2)")
|
||||
command.Flags().StringArrayVar(&opts.helmSetFiles, "helm-set-file", []string{}, "Helm set values from respective files specified via the command line (can be repeated to set several values: --helm-set-file key1=path1 --helm-set-file key2=path2)")
|
||||
command.Flags().StringVar(&opts.project, "project", "", "Application project name")
|
||||
command.Flags().StringVar(&opts.syncPolicy, "sync-policy", "", "Set the sync policy (one of: automated, none)")
|
||||
command.Flags().StringVar(&opts.syncPolicy, "sync-policy", "", "Set the sync policy (one of: none, automated (aliases of automated: auto, automatic))")
|
||||
command.Flags().StringArrayVar(&opts.syncOptions, "sync-option", []string{}, "Add or remove a sync options, e.g add `Prune=false`. Remove using `!` prefix, e.g. `!Prune=false`")
|
||||
command.Flags().BoolVar(&opts.autoPrune, "auto-prune", false, "Set automatic pruning when sync is automated")
|
||||
command.Flags().BoolVar(&opts.selfHeal, "self-heal", false, "Set self healing when sync is automated")
|
||||
command.Flags().BoolVar(&opts.allowEmpty, "allow-empty", false, "Set allow zero live resources when sync is automated")
|
||||
command.Flags().StringVar(&opts.namePrefix, "nameprefix", "", "Kustomize nameprefix")
|
||||
command.Flags().StringVar(&opts.nameSuffix, "namesuffix", "", "Kustomize namesuffix")
|
||||
command.Flags().StringVar(&opts.nameSuffix, "kustomize-version", "", "Kustomize version")
|
||||
command.Flags().StringVar(&opts.kustomizeVersion, "kustomize-version", "", "Kustomize version")
|
||||
command.Flags().BoolVar(&opts.directoryRecurse, "directory-recurse", false, "Recurse directory")
|
||||
command.Flags().StringVar(&opts.configManagementPlugin, "config-management-plugin", "", "Config management plugin name")
|
||||
command.Flags().StringArrayVar(&opts.jsonnetTlaStr, "jsonnet-tla-str", []string{}, "Jsonnet top level string arguments")
|
||||
command.Flags().StringArrayVar(&opts.jsonnetTlaCode, "jsonnet-tla-code", []string{}, "Jsonnet top level code arguments")
|
||||
command.Flags().StringArrayVar(&opts.jsonnetExtVarStr, "jsonnet-ext-var-str", []string{}, "Jsonnet string ext var")
|
||||
command.Flags().StringArrayVar(&opts.jsonnetExtVarCode, "jsonnet-ext-var-code", []string{}, "Jsonnet ext var")
|
||||
command.Flags().StringArrayVar(&opts.jsonnetLibs, "jsonnet-libs", []string{}, "Additional jsonnet libs (prefixed by repoRoot)")
|
||||
command.Flags().StringArrayVar(&opts.kustomizeImages, "kustomize-image", []string{}, "Kustomize images (e.g. --kustomize-image node:8.15.0 --kustomize-image mysql=mariadb,alpine@sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d)")
|
||||
command.Flags().BoolVar(&opts.validate, "validate", true, "Validation of repo and cluster")
|
||||
command.Flags().StringArrayVar(&opts.kustomizeCommonLabels, "kustomize-common-label", []string{}, "Set common labels in Kustomize")
|
||||
command.Flags().StringArrayVar(&opts.kustomizeCommonAnnotations, "kustomize-common-annotation", []string{}, "Set common labels in Kustomize")
|
||||
command.Flags().StringVar(&opts.directoryExclude, "directory-exclude", "", "Set glob expression used to exclude files from application source path")
|
||||
}
|
||||
|
||||
// NewApplicationUnsetCommand returns a new instance of an `argocd app unset` command
|
||||
@@ -766,6 +861,7 @@ func NewApplicationUnsetCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
|
||||
namePrefix bool
|
||||
kustomizeVersion bool
|
||||
kustomizeImages []string
|
||||
appOpts appOptions
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "unset APPNAME parameters",
|
||||
@@ -875,9 +971,11 @@ func NewApplicationUnsetCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
|
||||
}
|
||||
}
|
||||
|
||||
setAppSpecOptions(c.Flags(), &app.Spec, &appOpts)
|
||||
_, err = appIf.UpdateSpec(context.Background(), &applicationpkg.ApplicationUpdateSpecRequest{
|
||||
Name: &app.Name,
|
||||
Spec: app.Spec,
|
||||
Name: &app.Name,
|
||||
Spec: app.Spec,
|
||||
Validate: &appOpts.validate,
|
||||
})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
@@ -918,9 +1016,9 @@ func liveObjects(resources []*argoappv1.ResourceDiff) ([]*unstructured.Unstructu
|
||||
return objs, nil
|
||||
}
|
||||
|
||||
func getLocalObjects(app *argoappv1.Application, local, appLabelKey, kubeVersion string, kustomizeOptions *argoappv1.KustomizeOptions,
|
||||
func getLocalObjects(app *argoappv1.Application, local, localRepoRoot, appLabelKey, kubeVersion string, kustomizeOptions *argoappv1.KustomizeOptions,
|
||||
configManagementPlugins []*argoappv1.ConfigManagementPlugin) []*unstructured.Unstructured {
|
||||
manifestStrings := getLocalObjectsString(app, local, appLabelKey, kubeVersion, kustomizeOptions, configManagementPlugins)
|
||||
manifestStrings := getLocalObjectsString(app, local, localRepoRoot, appLabelKey, kubeVersion, kustomizeOptions, configManagementPlugins)
|
||||
objs := make([]*unstructured.Unstructured, len(manifestStrings))
|
||||
for i := range manifestStrings {
|
||||
obj := unstructured.Unstructured{}
|
||||
@@ -931,9 +1029,10 @@ func getLocalObjects(app *argoappv1.Application, local, appLabelKey, kubeVersion
|
||||
return objs
|
||||
}
|
||||
|
||||
func getLocalObjectsString(app *argoappv1.Application, local, appLabelKey, kubeVersion string, kustomizeOptions *argoappv1.KustomizeOptions,
|
||||
func getLocalObjectsString(app *argoappv1.Application, local, localRepoRoot, appLabelKey, kubeVersion string, kustomizeOptions *argoappv1.KustomizeOptions,
|
||||
configManagementPlugins []*argoappv1.ConfigManagementPlugin) []string {
|
||||
res, err := repository.GenerateManifests(local, "/", app.Spec.Source.TargetRevision, &repoapiclient.ManifestRequest{
|
||||
|
||||
res, err := repository.GenerateManifests(local, localRepoRoot, app.Spec.Source.TargetRevision, &repoapiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{Repo: app.Spec.Source.RepoURL},
|
||||
AppLabelKey: appLabelKey,
|
||||
AppLabelValue: app.Name,
|
||||
@@ -958,7 +1057,7 @@ func (p *resourceInfoProvider) IsNamespaced(gk schema.GroupKind) (bool, error) {
|
||||
return p.namespacedByGk[gk], nil
|
||||
}
|
||||
|
||||
func groupLocalObjs(localObs []*unstructured.Unstructured, liveObjs []*unstructured.Unstructured, appNamespace string) map[kube.ResourceKey]*unstructured.Unstructured {
|
||||
func groupObjsByKey(localObs []*unstructured.Unstructured, liveObjs []*unstructured.Unstructured, appNamespace string) map[kube.ResourceKey]*unstructured.Unstructured {
|
||||
namespacedByGk := make(map[schema.GroupKind]bool)
|
||||
for i := range liveObjs {
|
||||
if liveObjs[i] != nil {
|
||||
@@ -978,12 +1077,20 @@ func groupLocalObjs(localObs []*unstructured.Unstructured, liveObjs []*unstructu
|
||||
return objByKey
|
||||
}
|
||||
|
||||
type objKeyLiveTarget struct {
|
||||
key kube.ResourceKey
|
||||
live *unstructured.Unstructured
|
||||
target *unstructured.Unstructured
|
||||
}
|
||||
|
||||
// NewApplicationDiffCommand returns a new instance of an `argocd app diff` command
|
||||
func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
refresh bool
|
||||
hardRefresh bool
|
||||
local string
|
||||
refresh bool
|
||||
hardRefresh bool
|
||||
local string
|
||||
revision string
|
||||
localRepoRoot string
|
||||
)
|
||||
shortDesc := "Perform a diff against the target and live state."
|
||||
var command = &cobra.Command{
|
||||
@@ -1006,11 +1113,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
errors.CheckError(err)
|
||||
liveObjs, err := liveObjects(resources.Items)
|
||||
errors.CheckError(err)
|
||||
items := make([]struct {
|
||||
key kube.ResourceKey
|
||||
live *unstructured.Unstructured
|
||||
target *unstructured.Unstructured
|
||||
}, 0)
|
||||
items := make([]objKeyLiveTarget, 0)
|
||||
|
||||
conn, settingsIf := clientset.NewSettingsClientOrDie()
|
||||
defer argoio.Close(conn)
|
||||
@@ -1020,49 +1123,25 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
if local != "" {
|
||||
conn, clusterIf := clientset.NewClusterClientOrDie()
|
||||
defer argoio.Close(conn)
|
||||
cluster, err := clusterIf.Get(context.Background(), &clusterpkg.ClusterQuery{Server: app.Spec.Destination.Server})
|
||||
cluster, err := clusterIf.Get(context.Background(), &clusterpkg.ClusterQuery{Name: app.Spec.Destination.Name, Server: app.Spec.Destination.Server})
|
||||
errors.CheckError(err)
|
||||
localObjs := groupLocalObjs(getLocalObjects(app, local, argoSettings.AppLabelKey, cluster.ServerVersion, argoSettings.KustomizeOptions, argoSettings.ConfigManagementPlugins), liveObjs, app.Spec.Destination.Namespace)
|
||||
for _, res := range resources.Items {
|
||||
var live = &unstructured.Unstructured{}
|
||||
err := json.Unmarshal([]byte(res.NormalizedLiveState), &live)
|
||||
localObjs := groupObjsByKey(getLocalObjects(app, local, localRepoRoot, argoSettings.AppLabelKey, cluster.ServerVersion, argoSettings.KustomizeOptions, argoSettings.ConfigManagementPlugins), liveObjs, app.Spec.Destination.Namespace)
|
||||
items = groupObjsForDiff(resources, localObjs, items, argoSettings, appName)
|
||||
} else if revision != "" {
|
||||
var unstructureds []*unstructured.Unstructured
|
||||
q := applicationpkg.ApplicationManifestQuery{
|
||||
Name: &appName,
|
||||
Revision: revision,
|
||||
}
|
||||
res, err := appIf.GetManifests(context.Background(), &q)
|
||||
errors.CheckError(err)
|
||||
for _, mfst := range res.Manifests {
|
||||
obj, err := argoappv1.UnmarshalToUnstructured(mfst)
|
||||
errors.CheckError(err)
|
||||
|
||||
key := kube.ResourceKey{Name: res.Name, Namespace: res.Namespace, Group: res.Group, Kind: res.Kind}
|
||||
if key.Kind == kube.SecretKind && key.Group == "" {
|
||||
// Don't bother comparing secrets, argo-cd doesn't have access to k8s secret data
|
||||
delete(localObjs, key)
|
||||
continue
|
||||
}
|
||||
if local, ok := localObjs[key]; ok || live != nil {
|
||||
if local != nil && !kube.IsCRD(local) {
|
||||
err = argokube.SetAppInstanceLabel(local, argoSettings.AppLabelKey, appName)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
items = append(items, struct {
|
||||
key kube.ResourceKey
|
||||
live *unstructured.Unstructured
|
||||
target *unstructured.Unstructured
|
||||
}{
|
||||
live: live,
|
||||
target: local,
|
||||
key: key,
|
||||
})
|
||||
delete(localObjs, key)
|
||||
}
|
||||
}
|
||||
for key, local := range localObjs {
|
||||
items = append(items, struct {
|
||||
key kube.ResourceKey
|
||||
live *unstructured.Unstructured
|
||||
target *unstructured.Unstructured
|
||||
}{
|
||||
live: nil,
|
||||
target: local,
|
||||
key: key,
|
||||
})
|
||||
unstructureds = append(unstructureds, obj)
|
||||
}
|
||||
groupedObjs := groupObjsByKey(unstructureds, liveObjs, app.Spec.Destination.Namespace)
|
||||
items = groupObjsForDiff(resources, groupedObjs, items, argoSettings, appName)
|
||||
} else {
|
||||
for i := range resources.Items {
|
||||
res := resources.Items[i]
|
||||
@@ -1074,15 +1153,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
err = json.Unmarshal([]byte(res.TargetState), &target)
|
||||
errors.CheckError(err)
|
||||
|
||||
items = append(items, struct {
|
||||
key kube.ResourceKey
|
||||
live *unstructured.Unstructured
|
||||
target *unstructured.Unstructured
|
||||
}{
|
||||
live: live,
|
||||
target: target,
|
||||
key: kube.NewResourceKey(res.Group, res.Kind, res.Namespace, res.Name),
|
||||
})
|
||||
items = append(items, objKeyLiveTarget{kube.NewResourceKey(res.Group, res.Kind, res.Namespace, res.Name), live, target})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1099,7 +1170,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
normalizer, err := argo.NewDiffNormalizer(app.Spec.IgnoreDifferences, overrides)
|
||||
errors.CheckError(err)
|
||||
|
||||
diffRes, err := diff.Diff(item.target, item.live, normalizer, diff.GetDefaultDiffOptions())
|
||||
diffRes, err := diff.Diff(item.target, item.live, diff.WithNormalizer(normalizer))
|
||||
errors.CheckError(err)
|
||||
|
||||
if diffRes.Modified || item.target == nil || item.live == nil {
|
||||
@@ -1117,7 +1188,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
}
|
||||
|
||||
foundDiffs = true
|
||||
_ = diff.PrintDiff(item.key.Name, live, target)
|
||||
_ = cli.PrintDiff(item.key.Name, live, target)
|
||||
}
|
||||
}
|
||||
if foundDiffs {
|
||||
@@ -1129,9 +1200,44 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
command.Flags().BoolVar(&refresh, "refresh", false, "Refresh application data when retrieving")
|
||||
command.Flags().BoolVar(&hardRefresh, "hard-refresh", false, "Refresh application data as well as target manifests cache")
|
||||
command.Flags().StringVar(&local, "local", "", "Compare live app to a local manifests")
|
||||
command.Flags().StringVar(&revision, "revision", "", "Compare live app to a particular revision")
|
||||
command.Flags().StringVar(&localRepoRoot, "local-repo-root", "/", "Path to the repository root. Used together with --local allows setting the repository root")
|
||||
return command
|
||||
}
|
||||
|
||||
func groupObjsForDiff(resources *application.ManagedResourcesResponse, objs map[kube.ResourceKey]*unstructured.Unstructured, items []objKeyLiveTarget, argoSettings *settings.Settings, appName string) []objKeyLiveTarget {
|
||||
for _, res := range resources.Items {
|
||||
var live = &unstructured.Unstructured{}
|
||||
err := json.Unmarshal([]byte(res.NormalizedLiveState), &live)
|
||||
errors.CheckError(err)
|
||||
|
||||
key := kube.ResourceKey{Name: res.Name, Namespace: res.Namespace, Group: res.Group, Kind: res.Kind}
|
||||
if key.Kind == kube.SecretKind && key.Group == "" {
|
||||
// Don't bother comparing secrets, argo-cd doesn't have access to k8s secret data
|
||||
delete(objs, key)
|
||||
continue
|
||||
}
|
||||
if local, ok := objs[key]; ok || live != nil {
|
||||
if local != nil && !kube.IsCRD(local) {
|
||||
err = argokube.SetAppInstanceLabel(local, argoSettings.AppLabelKey, appName)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
items = append(items, objKeyLiveTarget{key, live, local})
|
||||
delete(objs, key)
|
||||
}
|
||||
}
|
||||
for key, local := range objs {
|
||||
if key.Kind == kube.SecretKind && key.Group == "" {
|
||||
// Don't bother comparing secrets, argo-cd doesn't have access to k8s secret data
|
||||
delete(objs, key)
|
||||
continue
|
||||
}
|
||||
items = append(items, objKeyLiveTarget{key, nil, local})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
// NewApplicationDeleteCommand returns a new instance of an `argocd app delete` command
|
||||
func NewApplicationDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
@@ -1281,8 +1387,10 @@ func formatConditionsSummary(app argoappv1.Application) string {
|
||||
}
|
||||
|
||||
const (
|
||||
resourceFieldDelimiter = ":"
|
||||
resourceFieldCount = 3
|
||||
resourceFieldDelimiter = ":"
|
||||
resourceFieldCount = 3
|
||||
resourceFieldNamespaceDelimiter = "/"
|
||||
resourceFieldNameWithNamespaceCount = 2
|
||||
)
|
||||
|
||||
func parseSelectedResources(resources []string) []argoappv1.SyncOperationResource {
|
||||
@@ -1294,10 +1402,21 @@ func parseSelectedResources(resources []string) []argoappv1.SyncOperationResourc
|
||||
if len(fields) != resourceFieldCount {
|
||||
log.Fatalf("Resource should have GROUP%sKIND%sNAME, but instead got: %s", resourceFieldDelimiter, resourceFieldDelimiter, r)
|
||||
}
|
||||
name := fields[2]
|
||||
namespace := ""
|
||||
if strings.Contains(fields[2], resourceFieldNamespaceDelimiter) {
|
||||
nameFields := strings.Split(fields[2], resourceFieldNamespaceDelimiter)
|
||||
if len(nameFields) != resourceFieldNameWithNamespaceCount {
|
||||
log.Fatalf("Resource with namespace should have GROUP%sKIND%sNAMESPACE%sNAME, but instead got: %s", resourceFieldDelimiter, resourceFieldDelimiter, resourceFieldNamespaceDelimiter, r)
|
||||
}
|
||||
namespace = nameFields[0]
|
||||
name = nameFields[1]
|
||||
}
|
||||
rsrc := argoappv1.SyncOperationResource{
|
||||
Group: fields[0],
|
||||
Kind: fields[1],
|
||||
Name: fields[2],
|
||||
Group: fields[0],
|
||||
Kind: fields[1],
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
}
|
||||
selectedResources = append(selectedResources, rsrc)
|
||||
}
|
||||
@@ -1377,17 +1496,23 @@ func printAppResources(w io.Writer, app *argoappv1.Application) {
|
||||
// NewApplicationSyncCommand returns a new instance of an `argocd app sync` command
|
||||
func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
revision string
|
||||
resources []string
|
||||
labels []string
|
||||
selector string
|
||||
prune bool
|
||||
dryRun bool
|
||||
timeout uint
|
||||
strategy string
|
||||
force bool
|
||||
async bool
|
||||
local string
|
||||
revision string
|
||||
resources []string
|
||||
labels []string
|
||||
selector string
|
||||
prune bool
|
||||
dryRun bool
|
||||
timeout uint
|
||||
strategy string
|
||||
force bool
|
||||
async bool
|
||||
retryLimit int64
|
||||
retryBackoffDuration string
|
||||
retryBackoffMaxDuration string
|
||||
retryBackoffFactor int64
|
||||
local string
|
||||
localRepoRoot string
|
||||
infos []string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "sync [APPNAME... | -l selector]",
|
||||
@@ -1404,7 +1529,9 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
# Sync a specific resource
|
||||
# Resource should be formatted as GROUP:KIND:NAME. If no GROUP is specified then :KIND:NAME
|
||||
argocd app sync my-app --resource :Service:my-service
|
||||
argocd app sync my-app --resource argoproj.io:Rollout:my-rollout`,
|
||||
argocd app sync my-app --resource argoproj.io:Rollout:my-rollout
|
||||
# Specify namespace if the application has resources with the same name in different namespaces
|
||||
argocd app sync my-app --resource argoproj.io:Rollout:my-namespace/my-rollout`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 && selector == "" {
|
||||
c.HelpFunc()(c, args)
|
||||
@@ -1470,8 +1597,8 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
if local != "" {
|
||||
app, err := appIf.Get(context.Background(), &applicationpkg.ApplicationQuery{Name: &appName})
|
||||
errors.CheckError(err)
|
||||
if app.Spec.SyncPolicy != nil && app.Spec.SyncPolicy.Automated != nil {
|
||||
log.Fatal("Cannot use local sync when Automatic Sync Policy is enabled")
|
||||
if app.Spec.SyncPolicy != nil && app.Spec.SyncPolicy.Automated != nil && !dryRun {
|
||||
log.Fatal("Cannot use local sync when Automatic Sync Policy is enabled except with --dry-run")
|
||||
}
|
||||
|
||||
errors.CheckError(err)
|
||||
@@ -1482,10 +1609,10 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
|
||||
conn, clusterIf := acdClient.NewClusterClientOrDie()
|
||||
defer argoio.Close(conn)
|
||||
cluster, err := clusterIf.Get(context.Background(), &clusterpkg.ClusterQuery{Server: app.Spec.Destination.Server})
|
||||
cluster, err := clusterIf.Get(context.Background(), &clusterpkg.ClusterQuery{Name: app.Spec.Destination.Name, Server: app.Spec.Destination.Server})
|
||||
errors.CheckError(err)
|
||||
argoio.Close(conn)
|
||||
localObjsStrings = getLocalObjectsString(app, local, argoSettings.AppLabelKey, cluster.ServerVersion, argoSettings.KustomizeOptions, argoSettings.ConfigManagementPlugins)
|
||||
localObjsStrings = getLocalObjectsString(app, local, localRepoRoot, argoSettings.AppLabelKey, cluster.ServerVersion, argoSettings.KustomizeOptions, argoSettings.ConfigManagementPlugins)
|
||||
}
|
||||
|
||||
syncReq := applicationpkg.ApplicationSyncRequest{
|
||||
@@ -1495,6 +1622,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
Resources: selectedResources,
|
||||
Prune: prune,
|
||||
Manifests: localObjsStrings,
|
||||
Infos: getInfos(infos),
|
||||
}
|
||||
switch strategy {
|
||||
case "apply":
|
||||
@@ -1506,6 +1634,16 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
default:
|
||||
log.Fatalf("Unknown sync strategy: '%s'", strategy)
|
||||
}
|
||||
if retryLimit > 0 {
|
||||
syncReq.RetryStrategy = &argoappv1.RetryStrategy{
|
||||
Limit: retryLimit,
|
||||
Backoff: &argoappv1.Backoff{
|
||||
Duration: retryBackoffDuration,
|
||||
MaxDuration: retryBackoffMaxDuration,
|
||||
Factor: pointer.Int64Ptr(retryBackoffFactor),
|
||||
},
|
||||
}
|
||||
}
|
||||
ctx := context.Background()
|
||||
_, err := appIf.Sync(ctx, &syncReq)
|
||||
errors.CheckError(err)
|
||||
@@ -1536,10 +1674,16 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
command.Flags().StringVarP(&selector, "selector", "l", "", "Sync apps that match this label")
|
||||
command.Flags().StringArrayVar(&labels, "label", []string{}, "Sync only specific resources with a label. This option may be specified repeatedly.")
|
||||
command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds")
|
||||
command.Flags().Int64Var(&retryLimit, "retry-limit", 0, "Max number of allowed sync retries")
|
||||
command.Flags().StringVar(&retryBackoffDuration, "retry-backoff-duration", fmt.Sprintf("%ds", common.DefaultSyncRetryDuration/time.Second), "Retry backoff base duration. Default unit is seconds, but could also be a duration (e.g. 2m, 1h)")
|
||||
command.Flags().StringVar(&retryBackoffMaxDuration, "retry-backoff-max-duration", fmt.Sprintf("%ds", common.DefaultSyncRetryMaxDuration/time.Second), "Max retry backoff duration. Default unit is seconds, but could also be a duration (e.g. 2m, 1h)")
|
||||
command.Flags().Int64Var(&retryBackoffFactor, "retry-backoff-factor", common.DefaultSyncRetryFactor, "Factor multiplies the base duration after each failed retry")
|
||||
command.Flags().StringVar(&strategy, "strategy", "", "Sync strategy (one of: apply|hook)")
|
||||
command.Flags().BoolVar(&force, "force", false, "Use a force apply")
|
||||
command.Flags().BoolVar(&async, "async", false, "Do not wait for application to sync before continuing")
|
||||
command.Flags().StringVar(&local, "local", "", "Path to a local directory. When this flag is present no git queries will be made")
|
||||
command.Flags().StringVar(&localRepoRoot, "local-repo-root", "/", "Path to the repository root. Used together with --local allows setting the repository root")
|
||||
command.Flags().StringArrayVar(&infos, "info", []string{}, "A list of key-value pairs during sync process. These infos will be persisted in app.")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -1630,7 +1774,7 @@ func getResourceStates(app *argoappv1.Application, selectedResources []argoappv1
|
||||
if len(selectedResources) > 0 {
|
||||
for i := len(states) - 1; i >= 0; i-- {
|
||||
res := states[i]
|
||||
if !argo.ContainsSyncResource(res.Name, schema.GroupVersionKind{Group: res.Group, Kind: res.Kind}, selectedResources) {
|
||||
if !argo.ContainsSyncResource(res.Name, res.Namespace, schema.GroupVersionKind{Group: res.Group, Kind: res.Kind}, selectedResources) {
|
||||
states = append(states[:i], states[i+1:]...)
|
||||
}
|
||||
}
|
||||
@@ -1714,12 +1858,11 @@ func waitOnApplicationStatus(acdClient apiclient.Client, appName string, timeout
|
||||
_, _ = fmt.Fprintf(w, waitFormatString, "TIMESTAMP", "GROUP", "KIND", "NAMESPACE", "NAME", "STATUS", "HEALTH", "HOOK", "MESSAGE")
|
||||
|
||||
prevStates := make(map[string]*resourceState)
|
||||
appEventCh := acdClient.WatchApplicationWithRetry(ctx, appName)
|
||||
conn, appClient := acdClient.NewApplicationClientOrDie()
|
||||
defer argoio.Close(conn)
|
||||
app, err := appClient.Get(ctx, &applicationpkg.ApplicationQuery{Name: &appName})
|
||||
errors.CheckError(err)
|
||||
|
||||
appEventCh := acdClient.WatchApplicationWithRetry(ctx, appName, app.ResourceVersion)
|
||||
for appEvent := range appEventCh {
|
||||
app = &appEvent.Application
|
||||
|
||||
@@ -1728,12 +1871,14 @@ func waitOnApplicationStatus(acdClient apiclient.Client, appName string, timeout
|
||||
if app.Operation != nil {
|
||||
// if it just got requested
|
||||
operationInProgress = true
|
||||
refresh = true
|
||||
if !app.Operation.DryRun() {
|
||||
refresh = true
|
||||
}
|
||||
} else if app.Status.OperationState != nil {
|
||||
if app.Status.OperationState.FinishedAt == nil {
|
||||
// if it is not finished yet
|
||||
operationInProgress = true
|
||||
} else if app.Status.ReconciledAt == nil || app.Status.ReconciledAt.Before(app.Status.OperationState.FinishedAt) {
|
||||
} else if !app.Status.OperationState.Operation.DryRun() && (app.Status.ReconciledAt == nil || app.Status.ReconciledAt.Before(app.Status.OperationState.FinishedAt)) {
|
||||
// if it is just finished and we need to wait for controller to reconcile app once after syncing
|
||||
operationInProgress = true
|
||||
}
|
||||
@@ -1756,7 +1901,7 @@ func waitOnApplicationStatus(acdClient apiclient.Client, appName string, timeout
|
||||
selectedResourcesAreReady = checkResourceStatus(watchSync, watchHealth, watchOperation, watchSuspended, string(app.Status.Health.Status), string(app.Status.Sync.Status), appEvent.Application.Operation)
|
||||
}
|
||||
|
||||
if selectedResourcesAreReady && !operationInProgress {
|
||||
if selectedResourcesAreReady && (!operationInProgress || !watchOperation) {
|
||||
app = printFinalStatus(app)
|
||||
return app, nil
|
||||
}
|
||||
@@ -2094,7 +2239,10 @@ func NewApplicationEditCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = appIf.UpdateSpec(context.Background(), &applicationpkg.ApplicationUpdateSpecRequest{Name: &app.Name, Spec: updatedSpec})
|
||||
|
||||
var appOpts appOptions
|
||||
setAppSpecOptions(c.Flags(), &app.Spec, &appOpts)
|
||||
_, err = appIf.UpdateSpec(context.Background(), &applicationpkg.ApplicationUpdateSpecRequest{Name: &app.Name, Spec: updatedSpec, Validate: &appOpts.validate})
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to update application spec:\n%v", err)
|
||||
}
|
||||
@@ -2105,6 +2253,45 @@ func NewApplicationEditCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
return command
|
||||
}
|
||||
|
||||
func NewApplicationListResourcesCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var orphaned bool
|
||||
var command = &cobra.Command{
|
||||
Use: "resources APPNAME",
|
||||
Short: "List resource of application",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
listAll := !c.Flag("orphaned").Changed
|
||||
appName := args[0]
|
||||
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
|
||||
defer argoio.Close(conn)
|
||||
appResourceTree, err := appIf.ResourceTree(context.Background(), &applicationpkg.ResourcesQuery{ApplicationName: &appName})
|
||||
errors.CheckError(err)
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
headers := []interface{}{"GROUP", "KIND", "NAMESPACE", "NAME", "ORPHANED"}
|
||||
fmtStr := "%s\t%s\t%s\t%s\t%s\n"
|
||||
_, _ = fmt.Fprintf(w, fmtStr, headers...)
|
||||
if !orphaned || listAll {
|
||||
for _, res := range appResourceTree.Nodes {
|
||||
if len(res.ParentRefs) == 0 {
|
||||
_, _ = fmt.Fprintf(w, fmtStr, res.Group, res.Kind, res.Namespace, res.Name, "No")
|
||||
}
|
||||
}
|
||||
}
|
||||
if orphaned || listAll {
|
||||
for _, res := range appResourceTree.OrphanedNodes {
|
||||
_, _ = fmt.Fprintf(w, fmtStr, res.Group, res.Kind, res.Namespace, res.Name, "Yes")
|
||||
}
|
||||
}
|
||||
_ = w.Flush()
|
||||
},
|
||||
}
|
||||
command.Flags().BoolVar(&orphaned, "orphaned", false, "Lists only orphaned resources")
|
||||
return command
|
||||
}
|
||||
|
||||
func NewApplicationPatchCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var patch string
|
||||
var patchType string
|
||||
@@ -2165,7 +2352,7 @@ func filterResources(command *cobra.Command, resources []*argoappv1.ResourceDiff
|
||||
if resourceName != "" && resourceName != obj.GetName() {
|
||||
continue
|
||||
}
|
||||
if kind != gvk.Kind {
|
||||
if kind != "" && kind != gvk.Kind {
|
||||
continue
|
||||
}
|
||||
deepCopy := obj.DeepCopy()
|
||||
|
||||
@@ -8,14 +8,14 @@ import (
|
||||
"strconv"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/io"
|
||||
"github.com/ghodss/yaml"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
applicationpkg "github.com/argoproj/argo-cd/pkg/apiclient/application"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
"github.com/argoproj/argo-cd/util/io"
|
||||
)
|
||||
|
||||
type DisplayedAction struct {
|
||||
@@ -100,7 +100,6 @@ func NewApplicationResourceActionsListCommand(clientOpts *argocdclient.ClientOpt
|
||||
case "":
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "GROUP\tKIND\tNAME\tACTION\tDISABLED\n")
|
||||
fmt.Println()
|
||||
for _, action := range availableActions {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", action.Group, action.Kind, action.Name, action.Action, strconv.FormatBool(action.Disabled))
|
||||
}
|
||||
|
||||
@@ -40,6 +40,49 @@ func Test_setHelmOpt(t *testing.T) {
|
||||
setHelmOpt(&src, helmOpts{helmSetFiles: []string{"foo=bar"}})
|
||||
assert.Equal(t, []v1alpha1.HelmFileParameter{{Name: "foo", Path: "bar"}}, src.Helm.FileParameters)
|
||||
})
|
||||
t.Run("Version", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setHelmOpt(&src, helmOpts{version: "v3"})
|
||||
assert.Equal(t, "v3", src.Helm.Version)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_setKustomizeOpt(t *testing.T) {
|
||||
t.Run("No kustomize", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setKustomizeOpt(&src, kustomizeOpts{})
|
||||
assert.Nil(t, src.Kustomize)
|
||||
})
|
||||
t.Run("Name prefix", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setKustomizeOpt(&src, kustomizeOpts{namePrefix: "test-"})
|
||||
assert.Equal(t, &v1alpha1.ApplicationSourceKustomize{NamePrefix: "test-"}, src.Kustomize)
|
||||
})
|
||||
t.Run("Name suffix", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setKustomizeOpt(&src, kustomizeOpts{nameSuffix: "-test"})
|
||||
assert.Equal(t, &v1alpha1.ApplicationSourceKustomize{NameSuffix: "-test"}, src.Kustomize)
|
||||
})
|
||||
t.Run("Images", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setKustomizeOpt(&src, kustomizeOpts{images: []string{"org/image:v1", "org/image:v2"}})
|
||||
assert.Equal(t, &v1alpha1.ApplicationSourceKustomize{Images: v1alpha1.KustomizeImages{v1alpha1.KustomizeImage("org/image:v2")}}, src.Kustomize)
|
||||
})
|
||||
t.Run("Version", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setKustomizeOpt(&src, kustomizeOpts{version: "v0.1"})
|
||||
assert.Equal(t, &v1alpha1.ApplicationSourceKustomize{Version: "v0.1"}, src.Kustomize)
|
||||
})
|
||||
t.Run("Common labels", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setKustomizeOpt(&src, kustomizeOpts{commonLabels: map[string]string{"foo1": "bar1", "foo2": "bar2"}})
|
||||
assert.Equal(t, &v1alpha1.ApplicationSourceKustomize{CommonLabels: map[string]string{"foo1": "bar1", "foo2": "bar2"}}, src.Kustomize)
|
||||
})
|
||||
t.Run("Common annotations", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setKustomizeOpt(&src, kustomizeOpts{commonAnnotations: map[string]string{"foo1": "bar1", "foo2": "bar2"}})
|
||||
assert.Equal(t, &v1alpha1.ApplicationSourceKustomize{CommonAnnotations: map[string]string{"foo1": "bar1", "foo2": "bar2"}}, src.Kustomize)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_setJsonnetOpt(t *testing.T) {
|
||||
@@ -90,6 +133,14 @@ func Test_setAppSpecOptions(t *testing.T) {
|
||||
assert.NoError(t, f.SetFlag("sync-policy", "automated"))
|
||||
assert.NotNil(t, f.spec.SyncPolicy.Automated)
|
||||
|
||||
f.spec.SyncPolicy = nil
|
||||
assert.NoError(t, f.SetFlag("sync-policy", "automatic"))
|
||||
assert.NotNil(t, f.spec.SyncPolicy.Automated)
|
||||
|
||||
f.spec.SyncPolicy = nil
|
||||
assert.NoError(t, f.SetFlag("sync-policy", "auto"))
|
||||
assert.NotNil(t, f.spec.SyncPolicy.Automated)
|
||||
|
||||
assert.NoError(t, f.SetFlag("sync-policy", "none"))
|
||||
assert.Nil(t, f.spec.SyncPolicy)
|
||||
})
|
||||
|
||||
@@ -9,14 +9,14 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/io"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
certificatepkg "github.com/argoproj/argo-cd/pkg/apiclient/certificate"
|
||||
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
certutil "github.com/argoproj/argo-cd/util/cert"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
"github.com/argoproj/argo-cd/util/io"
|
||||
)
|
||||
|
||||
// NewCertCommand returns a new instance of an `argocd repo` command
|
||||
|
||||
@@ -9,8 +9,6 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/io"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
@@ -22,6 +20,8 @@ import (
|
||||
clusterpkg "github.com/argoproj/argo-cd/pkg/apiclient/cluster"
|
||||
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/clusterauth"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
"github.com/argoproj/argo-cd/util/io"
|
||||
)
|
||||
|
||||
// NewClusterCommand returns a new instance of an `argocd cluster` command
|
||||
@@ -58,13 +58,20 @@ 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
|
||||
serviceAccount string
|
||||
awsRoleArn string
|
||||
awsClusterName string
|
||||
systemNamespace string
|
||||
namespaces []string
|
||||
inCluster bool
|
||||
upsert bool
|
||||
serviceAccount string
|
||||
awsRoleArn string
|
||||
awsClusterName string
|
||||
systemNamespace string
|
||||
namespaces []string
|
||||
name string
|
||||
shard int64
|
||||
execProviderCommand string
|
||||
execProviderArgs []string
|
||||
execProviderEnv map[string]string
|
||||
execProviderAPIVersion string
|
||||
execProviderInstallHint string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "add CONTEXT",
|
||||
@@ -93,11 +100,20 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
|
||||
managerBearerToken := ""
|
||||
var awsAuthConf *argoappv1.AWSAuthConfig
|
||||
var execProviderConf *argoappv1.ExecProviderConfig
|
||||
if awsClusterName != "" {
|
||||
awsAuthConf = &argoappv1.AWSAuthConfig{
|
||||
ClusterName: awsClusterName,
|
||||
RoleARN: awsRoleArn,
|
||||
}
|
||||
} else if execProviderCommand != "" {
|
||||
execProviderConf = &argoappv1.ExecProviderConfig{
|
||||
Command: execProviderCommand,
|
||||
Args: execProviderArgs,
|
||||
Env: execProviderEnv,
|
||||
APIVersion: execProviderAPIVersion,
|
||||
InstallHint: execProviderInstallHint,
|
||||
}
|
||||
} else {
|
||||
// Install RBAC resources for managing the cluster
|
||||
clientset, err := kubernetes.NewForConfig(conf)
|
||||
@@ -111,10 +127,16 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
}
|
||||
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
|
||||
defer io.Close(conn)
|
||||
clst := newCluster(contextName, namespaces, conf, managerBearerToken, awsAuthConf)
|
||||
if name != "" {
|
||||
contextName = name
|
||||
}
|
||||
clst := newCluster(contextName, namespaces, conf, managerBearerToken, awsAuthConf, execProviderConf)
|
||||
if inCluster {
|
||||
clst.Server = common.KubernetesInternalAPIServerAddr
|
||||
}
|
||||
if shard >= 0 {
|
||||
clst.Shard = &shard
|
||||
}
|
||||
clstCreateReq := clusterpkg.ClusterCreateRequest{
|
||||
Cluster: clst,
|
||||
Upsert: upsert,
|
||||
@@ -132,6 +154,13 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
command.Flags().StringVar(&awsRoleArn, "aws-role-arn", "", "Optional AWS role arn. If set then AWS IAM Authenticator assume a role to perform cluster operations instead of the default AWS credential provider chain.")
|
||||
command.Flags().StringVar(&systemNamespace, "system-namespace", common.DefaultSystemNamespace, "Use different system namespace")
|
||||
command.Flags().StringArrayVar(&namespaces, "namespace", nil, "List of namespaces which are allowed to manage")
|
||||
command.Flags().StringVar(&name, "name", "", "Overwrite the cluster name")
|
||||
command.Flags().Int64Var(&shard, "shard", -1, "Cluster shard number; inferred from hostname if not set")
|
||||
command.Flags().StringVar(&execProviderCommand, "exec-command", "", "Command to run to provide client credentials to the cluster. You may need to build a custom ArgoCD image to ensure the command is available at runtime.")
|
||||
command.Flags().StringArrayVar(&execProviderArgs, "exec-command-args", nil, "Arguments to supply to the --exec-command command")
|
||||
command.Flags().StringToStringVar(&execProviderEnv, "exec-command-env", nil, "Environment vars to set when running the --exec-command command")
|
||||
command.Flags().StringVar(&execProviderAPIVersion, "exec-command-api-version", "", "Preferred input version of the ExecInfo for the --exec-command")
|
||||
command.Flags().StringVar(&execProviderInstallHint, "exec-command-install-hint", "", "Text shown to the user when the --exec-command executable doesn't seem to be present")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -174,7 +203,7 @@ func printKubeContexts(ca clientcmd.ConfigAccess) {
|
||||
}
|
||||
}
|
||||
|
||||
func newCluster(name string, namespaces []string, conf *rest.Config, managerBearerToken string, awsAuthConf *argoappv1.AWSAuthConfig) *argoappv1.Cluster {
|
||||
func newCluster(name string, namespaces []string, conf *rest.Config, managerBearerToken string, awsAuthConf *argoappv1.AWSAuthConfig, execProviderConf *argoappv1.ExecProviderConfig) *argoappv1.Cluster {
|
||||
tlsClientConfig := argoappv1.TLSClientConfig{
|
||||
Insecure: conf.TLSClientConfig.Insecure,
|
||||
ServerName: conf.TLSClientConfig.ServerName,
|
||||
@@ -203,8 +232,9 @@ func newCluster(name string, namespaces []string, conf *rest.Config, managerBear
|
||||
Name: name,
|
||||
Namespaces: namespaces,
|
||||
Config: argoappv1.ClusterConfig{
|
||||
TLSClientConfig: tlsClientConfig,
|
||||
AWSAuthConfig: awsAuthConf,
|
||||
TLSClientConfig: tlsClientConfig,
|
||||
AWSAuthConfig: awsAuthConf,
|
||||
ExecProviderConfig: execProviderConf,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,8 @@ func Test_newCluster(t *testing.T) {
|
||||
Host: "test-endpoint.example.com",
|
||||
},
|
||||
"test-bearer-token",
|
||||
&v1alpha1.AWSAuthConfig{})
|
||||
&v1alpha1.AWSAuthConfig{},
|
||||
&v1alpha1.ExecProviderConfig{})
|
||||
|
||||
assert.Equal(t, "test-cert-data", string(clusterWithData.Config.CertData))
|
||||
assert.Equal(t, "test-key-data", string(clusterWithData.Config.KeyData))
|
||||
@@ -62,7 +63,8 @@ func Test_newCluster(t *testing.T) {
|
||||
Host: "test-endpoint.example.com",
|
||||
},
|
||||
"test-bearer-token",
|
||||
&v1alpha1.AWSAuthConfig{})
|
||||
&v1alpha1.AWSAuthConfig{},
|
||||
&v1alpha1.ExecProviderConfig{})
|
||||
|
||||
assert.True(t, strings.Contains(string(clusterWithFiles.Config.CertData), "test-cert-data"))
|
||||
assert.True(t, strings.Contains(string(clusterWithFiles.Config.KeyData), "test-key-data"))
|
||||
@@ -77,7 +79,8 @@ func Test_newCluster(t *testing.T) {
|
||||
Host: "test-endpoint.example.com",
|
||||
},
|
||||
"test-bearer-token",
|
||||
&v1alpha1.AWSAuthConfig{})
|
||||
&v1alpha1.AWSAuthConfig{},
|
||||
&v1alpha1.ExecProviderConfig{})
|
||||
|
||||
assert.Equal(t, "test-bearer-token", clusterWithBearerToken.Config.BearerToken)
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
)
|
||||
|
||||
|
||||
162
cmd/argocd/commands/gpg.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
gpgkeypkg "github.com/argoproj/argo-cd/pkg/apiclient/gpgkey"
|
||||
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
argoio "github.com/argoproj/argo-cd/util/io"
|
||||
)
|
||||
|
||||
// NewGPGCommand returns a new instance of an `argocd repo` command
|
||||
func NewGPGCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "gpg",
|
||||
Short: "Manage GPG keys used for signature verification",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
},
|
||||
Example: ``,
|
||||
}
|
||||
command.AddCommand(NewGPGListCommand(clientOpts))
|
||||
command.AddCommand(NewGPGGetCommand(clientOpts))
|
||||
command.AddCommand(NewGPGAddCommand(clientOpts))
|
||||
command.AddCommand(NewGPGDeleteCommand(clientOpts))
|
||||
return command
|
||||
}
|
||||
|
||||
// NewGPGListCommand lists all configured public keys from the server
|
||||
func NewGPGListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List configured GPG public keys",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
conn, gpgIf := argocdclient.NewClientOrDie(clientOpts).NewGPGKeyClientOrDie()
|
||||
defer argoio.Close(conn)
|
||||
keys, err := gpgIf.List(context.Background(), &gpgkeypkg.GnuPGPublicKeyQuery{})
|
||||
errors.CheckError(err)
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(keys.Items, output, false)
|
||||
errors.CheckError(err)
|
||||
case "wide", "":
|
||||
printKeyTable(keys.Items)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide")
|
||||
return command
|
||||
}
|
||||
|
||||
// NewGPGGetCommand retrieves a single public key from the server
|
||||
func NewGPGGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "get KEYID",
|
||||
Short: "Get the GPG public key with ID <KEYID> from the server",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
errors.CheckError(fmt.Errorf("Missing KEYID argument"))
|
||||
}
|
||||
conn, gpgIf := argocdclient.NewClientOrDie(clientOpts).NewGPGKeyClientOrDie()
|
||||
defer argoio.Close(conn)
|
||||
key, err := gpgIf.Get(context.Background(), &gpgkeypkg.GnuPGPublicKeyQuery{KeyID: args[0]})
|
||||
errors.CheckError(err)
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(key, output, false)
|
||||
errors.CheckError(err)
|
||||
case "wide", "":
|
||||
fmt.Printf("Key ID: %s\n", key.KeyID)
|
||||
fmt.Printf("Key fingerprint: %s\n", key.Fingerprint)
|
||||
fmt.Printf("Key subtype: %s\n", strings.ToUpper(key.SubType))
|
||||
fmt.Printf("Key owner: %s\n", key.Owner)
|
||||
fmt.Printf("Key data follows until EOF:\n%s\n", key.KeyData)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide")
|
||||
return command
|
||||
}
|
||||
|
||||
// NewGPGAddCommand adds a public key to the server's configuration
|
||||
func NewGPGAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
fromFile string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "add",
|
||||
Short: "Adds a GPG public key to the server's keyring",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if fromFile == "" {
|
||||
errors.CheckError(fmt.Errorf("--from is mandatory"))
|
||||
}
|
||||
keyData, err := ioutil.ReadFile(fromFile)
|
||||
if err != nil {
|
||||
errors.CheckError(err)
|
||||
}
|
||||
conn, gpgIf := argocdclient.NewClientOrDie(clientOpts).NewGPGKeyClientOrDie()
|
||||
defer argoio.Close(conn)
|
||||
resp, err := gpgIf.Create(context.Background(), &gpgkeypkg.GnuPGPublicKeyCreateRequest{Publickey: &appsv1.GnuPGPublicKey{KeyData: string(keyData)}})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Created %d key(s) from input file", len(resp.Created.Items))
|
||||
if len(resp.Skipped) > 0 {
|
||||
fmt.Printf(", and %d key(s) were skipped because they exist already", len(resp.Skipped))
|
||||
}
|
||||
fmt.Printf(".\n")
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&fromFile, "from", "f", "", "Path to the file that contains the GPG public key to import")
|
||||
return command
|
||||
|
||||
}
|
||||
|
||||
// NewGPGDeleteCommand removes a key from the server's keyring
|
||||
func NewGPGDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "rm KEYID",
|
||||
Short: "Removes a GPG public key from the server's keyring",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
errors.CheckError(fmt.Errorf("Missing KEYID argument"))
|
||||
}
|
||||
conn, gpgIf := argocdclient.NewClientOrDie(clientOpts).NewGPGKeyClientOrDie()
|
||||
defer argoio.Close(conn)
|
||||
_, err := gpgIf.Delete(context.Background(), &gpgkeypkg.GnuPGPublicKeyQuery{KeyID: args[0]})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Deleted key with key ID %s\n", args[0])
|
||||
},
|
||||
}
|
||||
return command
|
||||
|
||||
}
|
||||
|
||||
// Print table of certificate info
|
||||
func printKeyTable(keys []appsv1.GnuPGPublicKey) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "KEYID\tTYPE\tIDENTITY\n")
|
||||
|
||||
for _, k := range keys {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", k.KeyID, strings.ToUpper(k.SubType), k.Owner)
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
@@ -2,17 +2,18 @@ package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"html"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/io"
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/dgrijalva/jwt-go/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -22,7 +23,10 @@ import (
|
||||
sessionpkg "github.com/argoproj/argo-cd/pkg/apiclient/session"
|
||||
settingspkg "github.com/argoproj/argo-cd/pkg/apiclient/settings"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
grpc_util "github.com/argoproj/argo-cd/util/grpc"
|
||||
"github.com/argoproj/argo-cd/util/io"
|
||||
jwtutil "github.com/argoproj/argo-cd/util/jwt"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
oidcutil "github.com/argoproj/argo-cd/util/oidc"
|
||||
"github.com/argoproj/argo-cd/util/rand"
|
||||
@@ -111,7 +115,7 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
}
|
||||
|
||||
parser := &jwt.Parser{
|
||||
SkipClaimsValidation: true,
|
||||
ValidationHelper: jwt.NewValidationHelper(jwt.WithoutClaimsValidation(), jwt.WithoutAudienceValidation()),
|
||||
}
|
||||
claims := jwt.MapClaims{}
|
||||
_, _, err := parser.ParseUnverified(tokenString, &claims)
|
||||
@@ -159,13 +163,13 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
}
|
||||
|
||||
func userDisplayName(claims jwt.MapClaims) string {
|
||||
if email, ok := claims["email"]; ok && email != nil {
|
||||
return email.(string)
|
||||
if email := jwtutil.StringField(claims, "email"); email != "" {
|
||||
return email
|
||||
}
|
||||
if name, ok := claims["name"]; ok && name != nil {
|
||||
return name.(string)
|
||||
if name := jwtutil.StringField(claims, "name"); name != "" {
|
||||
return name
|
||||
}
|
||||
return claims["sub"].(string)
|
||||
return jwtutil.StringField(claims, "sub")
|
||||
}
|
||||
|
||||
// oauth2Login opens a browser, runs a temporary HTTP server to delegate OAuth2 login flow and
|
||||
@@ -188,17 +192,22 @@ func oauth2Login(ctx context.Context, port int, oidcSettings *settingspkg.OIDCCo
|
||||
var refreshToken string
|
||||
|
||||
handleErr := func(w http.ResponseWriter, errMsg string) {
|
||||
http.Error(w, errMsg, http.StatusBadRequest)
|
||||
http.Error(w, html.EscapeString(errMsg), http.StatusBadRequest)
|
||||
completionChan <- errMsg
|
||||
}
|
||||
|
||||
// PKCE implementation of https://tools.ietf.org/html/rfc7636
|
||||
codeVerifier := rand.RandStringCharset(43, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~")
|
||||
codeChallengeHash := sha256.Sum256([]byte(codeVerifier))
|
||||
codeChallenge := base64.RawURLEncoding.EncodeToString(codeChallengeHash[:])
|
||||
|
||||
// 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"))
|
||||
handleErr(w, fmt.Sprintf("%s: %s", formErr, r.FormValue("error_description")))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -231,7 +240,8 @@ func oauth2Login(ctx context.Context, port int, oidcSettings *settingspkg.OIDCCo
|
||||
handleErr(w, fmt.Sprintf("no code in request: %q", r.Form))
|
||||
return
|
||||
}
|
||||
tok, err := oauth2conf.Exchange(ctx, code)
|
||||
opts := []oauth2.AuthCodeOption{oauth2.SetAuthURLParam("code_verifier", codeVerifier)}
|
||||
tok, err := oauth2conf.Exchange(ctx, code, opts...)
|
||||
if err != nil {
|
||||
handleErr(w, err.Error())
|
||||
return
|
||||
@@ -267,6 +277,8 @@ func oauth2Login(ctx context.Context, port int, oidcSettings *settingspkg.OIDCCo
|
||||
|
||||
switch grantType {
|
||||
case oidcutil.GrantTypeAuthorizationCode:
|
||||
opts = append(opts, oauth2.SetAuthURLParam("code_challenge", codeChallenge))
|
||||
opts = append(opts, oauth2.SetAuthURLParam("code_challenge_method", "S256"))
|
||||
url = oauth2conf.AuthCodeURL(stateNonce, opts...)
|
||||
case oidcutil.GrantTypeImplicit:
|
||||
url = oidcutil.ImplicitFlowURL(oauth2conf, stateNonce, opts...)
|
||||
|
||||
31
cmd/argocd/commands/login_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/dgrijalva/jwt-go/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
//
|
||||
|
||||
func Test_userDisplayName_email(t *testing.T) {
|
||||
claims := jwt.MapClaims{"iss": "qux", "sub": "foo", "email": "firstname.lastname@example.com", "groups": []string{"baz"}}
|
||||
actualName := userDisplayName(claims)
|
||||
expectedName := "firstname.lastname@example.com"
|
||||
assert.Equal(t, expectedName, actualName)
|
||||
}
|
||||
|
||||
func Test_userDisplayName_name(t *testing.T) {
|
||||
claims := jwt.MapClaims{"iss": "qux", "sub": "foo", "name": "Firstname Lastname", "groups": []string{"baz"}}
|
||||
actualName := userDisplayName(claims)
|
||||
expectedName := "Firstname Lastname"
|
||||
assert.Equal(t, expectedName, actualName)
|
||||
}
|
||||
|
||||
func Test_userDisplayName_sub(t *testing.T) {
|
||||
claims := jwt.MapClaims{"iss": "qux", "sub": "foo", "groups": []string{"baz"}}
|
||||
actualName := userDisplayName(claims)
|
||||
expectedName := "foo"
|
||||
assert.Equal(t, expectedName, actualName)
|
||||
}
|
||||
@@ -4,11 +4,11 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
)
|
||||
|
||||
|
||||
@@ -12,13 +12,12 @@ import (
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
argoio "github.com/argoproj/gitops-engine/pkg/utils/io"
|
||||
"github.com/dustin/go-humanize"
|
||||
humanize "github.com/dustin/go-humanize"
|
||||
"github.com/ghodss/yaml"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/utils/pointer"
|
||||
|
||||
@@ -27,13 +26,17 @@ import (
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/config"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
"github.com/argoproj/argo-cd/util/gpg"
|
||||
argoio "github.com/argoproj/argo-cd/util/io"
|
||||
)
|
||||
|
||||
type projectOpts struct {
|
||||
description string
|
||||
destinations []string
|
||||
sources []string
|
||||
signatureKeys []string
|
||||
orphanedResourcesEnabled bool
|
||||
orphanedResourcesWarn bool
|
||||
}
|
||||
@@ -60,6 +63,18 @@ func (opts *projectOpts) GetDestinations() []v1alpha1.ApplicationDestination {
|
||||
return destinations
|
||||
}
|
||||
|
||||
// TODO: Get configured keys and emit warning when a key is specified that is not configured
|
||||
func (opts *projectOpts) GetSignatureKeys() []v1alpha1.SignatureKey {
|
||||
signatureKeys := make([]v1alpha1.SignatureKey, 0)
|
||||
for _, keyStr := range opts.signatureKeys {
|
||||
if !gpg.IsShortKeyID(keyStr) && !gpg.IsLongKeyID(keyStr) {
|
||||
log.Fatalf("'%s' is not a valid GnuPG key ID", keyStr)
|
||||
}
|
||||
signatureKeys = append(signatureKeys, v1alpha1.SignatureKey{KeyID: gpg.KeyID(keyStr)})
|
||||
}
|
||||
return signatureKeys
|
||||
}
|
||||
|
||||
// NewProjectCommand returns a new instance of an `argocd proj` command
|
||||
func NewProjectCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
@@ -77,6 +92,8 @@ func NewProjectCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
command.AddCommand(NewProjectListCommand(clientOpts))
|
||||
command.AddCommand(NewProjectSetCommand(clientOpts))
|
||||
command.AddCommand(NewProjectEditCommand(clientOpts))
|
||||
command.AddCommand(NewProjectAddSignatureKeyCommand(clientOpts))
|
||||
command.AddCommand(NewProjectRemoveSignatureKeyCommand(clientOpts))
|
||||
command.AddCommand(NewProjectAddDestinationCommand(clientOpts))
|
||||
command.AddCommand(NewProjectRemoveDestinationCommand(clientOpts))
|
||||
command.AddCommand(NewProjectAddSourceCommand(clientOpts))
|
||||
@@ -86,6 +103,8 @@ func NewProjectCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
command.AddCommand(NewProjectAllowNamespaceResourceCommand(clientOpts))
|
||||
command.AddCommand(NewProjectDenyNamespaceResourceCommand(clientOpts))
|
||||
command.AddCommand(NewProjectWindowsCommand(clientOpts))
|
||||
command.AddCommand(NewProjectAddOrphanedIgnoreCommand(clientOpts))
|
||||
command.AddCommand(NewProjectRemoveOrphanedIgnoreCommand(clientOpts))
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -94,6 +113,7 @@ func addProjFlags(command *cobra.Command, opts *projectOpts) {
|
||||
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 source repository URL")
|
||||
command.Flags().StringSliceVar(&opts.signatureKeys, "signature-keys", []string{}, "GnuPG public key IDs for commit signature verification")
|
||||
command.Flags().BoolVar(&opts.orphanedResourcesEnabled, "orphaned-resources", false, "Enables orphaned resources monitoring")
|
||||
command.Flags().BoolVar(&opts.orphanedResourcesWarn, "orphaned-resources-warn", false, "Specifies if applications should be a warning condition when orphaned resources detected")
|
||||
}
|
||||
@@ -133,6 +153,7 @@ func NewProjectCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
Short: "Create a project",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
var proj v1alpha1.AppProject
|
||||
fmt.Printf("EE: %d/%v\n", len(opts.signatureKeys), opts.signatureKeys)
|
||||
if fileURL == "-" {
|
||||
// read stdin
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
@@ -165,6 +186,7 @@ func NewProjectCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
Description: opts.description,
|
||||
Destinations: opts.GetDestinations(),
|
||||
SourceRepos: opts.sources,
|
||||
SignatureKeys: opts.GetSignatureKeys(),
|
||||
OrphanedResources: getOrphanedResourcesSettings(c, opts),
|
||||
},
|
||||
}
|
||||
@@ -215,6 +237,8 @@ func NewProjectSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
proj.Spec.Destinations = opts.GetDestinations()
|
||||
case "src":
|
||||
proj.Spec.SourceRepos = opts.sources
|
||||
case "signature-keys":
|
||||
proj.Spec.SignatureKeys = opts.GetSignatureKeys()
|
||||
case "orphaned-resources", "orphaned-resources-warn":
|
||||
proj.Spec.OrphanedResources = getOrphanedResourcesSettings(c, opts)
|
||||
}
|
||||
@@ -233,6 +257,81 @@ func NewProjectSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectAddSignatureKeyCommand returns a new instance of an `argocd proj add-signature-key` command
|
||||
func NewProjectAddSignatureKeyCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "add-signature-key PROJECT KEY-ID",
|
||||
Short: "Add GnuPG signature key to project",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
signatureKey := args[1]
|
||||
|
||||
if !gpg.IsShortKeyID(signatureKey) && !gpg.IsLongKeyID(signatureKey) {
|
||||
log.Fatalf("%s is not a valid GnuPG key ID", signatureKey)
|
||||
}
|
||||
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer argoio.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
for _, key := range proj.Spec.SignatureKeys {
|
||||
if key.KeyID == signatureKey {
|
||||
log.Fatal("Specified signature key is already defined in project")
|
||||
}
|
||||
}
|
||||
proj.Spec.SignatureKeys = append(proj.Spec.SignatureKeys, v1alpha1.SignatureKey{KeyID: signatureKey})
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectRemoveSignatureKeyCommand returns a new instance of an `argocd proj remove-signature-key` command
|
||||
func NewProjectRemoveSignatureKeyCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "remove-signature-key PROJECT KEY-ID",
|
||||
Short: "Remove GnuPG signature key from project",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
signatureKey := args[1]
|
||||
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer argoio.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
index := -1
|
||||
for i, key := range proj.Spec.SignatureKeys {
|
||||
if key.KeyID == signatureKey {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
log.Fatal("Specified signature key is not configured for project")
|
||||
} else {
|
||||
proj.Spec.SignatureKeys = append(proj.Spec.SignatureKeys[:index], proj.Spec.SignatureKeys[index+1:]...)
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
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{
|
||||
@@ -304,6 +403,96 @@ func NewProjectRemoveDestinationCommand(clientOpts *argocdclient.ClientOptions)
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectAddOrphanedIgnoreCommand returns a new instance of an `argocd proj add-orphaned-ignore` command
|
||||
func NewProjectAddOrphanedIgnoreCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
name string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "add-orphaned-ignore PROJECT GROUP KIND",
|
||||
Short: "Add a resource to orphaned ignore list",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
group := args[1]
|
||||
kind := args[2]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer argoio.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
if proj.Spec.OrphanedResources == nil {
|
||||
settings := v1alpha1.OrphanedResourcesMonitorSettings{}
|
||||
settings.Ignore = []v1alpha1.OrphanedResourceKey{{Group: group, Kind: kind, Name: name}}
|
||||
proj.Spec.OrphanedResources = &settings
|
||||
} else {
|
||||
for _, ignore := range proj.Spec.OrphanedResources.Ignore {
|
||||
if ignore.Group == group && ignore.Kind == kind && ignore.Name == name {
|
||||
log.Fatal("Specified resource is already defined in the orphaned ignore list of project")
|
||||
return
|
||||
}
|
||||
}
|
||||
proj.Spec.OrphanedResources.Ignore = append(proj.Spec.OrphanedResources.Ignore, v1alpha1.OrphanedResourceKey{Group: group, Kind: kind, Name: name})
|
||||
}
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&name, "name", "", "Resource name pattern")
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectRemoveOrphanedIgnoreCommand returns a new instance of an `argocd proj remove-orphaned-ignore` command
|
||||
func NewProjectRemoveOrphanedIgnoreCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
name string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "remove-orphaned-ignore PROJECT GROUP KIND NAME",
|
||||
Short: "Remove a resource from orphaned ignore list",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
group := args[1]
|
||||
kind := args[2]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer argoio.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
if proj.Spec.OrphanedResources == nil {
|
||||
log.Fatal("Specified resource does not exist in the orphaned ignore list of project")
|
||||
return
|
||||
}
|
||||
|
||||
index := -1
|
||||
for i, ignore := range proj.Spec.OrphanedResources.Ignore {
|
||||
if ignore.Group == group && ignore.Kind == kind && ignore.Name == name {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
log.Fatal("Specified resource does not exist in the orphaned ignore of project")
|
||||
} else {
|
||||
proj.Spec.OrphanedResources.Ignore = append(proj.Spec.OrphanedResources.Ignore[:index], proj.Spec.OrphanedResources.Ignore[index+1:]...)
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&name, "name", "", "Resource name pattern")
|
||||
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{
|
||||
@@ -340,35 +529,45 @@ func NewProjectAddSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
|
||||
return command
|
||||
}
|
||||
|
||||
func modifyClusterResourceCmd(cmdUse, cmdDesc string, clientOpts *argocdclient.ClientOptions, action func(proj *v1alpha1.AppProject, group string, kind string) bool) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: cmdUse,
|
||||
Short: cmdDesc,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
|
||||
if len(args) != 3 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
func modifyResourcesList(list *[]metav1.GroupKind, add bool, listDesc string, group string, kind string) bool {
|
||||
if add {
|
||||
for _, item := range *list {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
fmt.Printf("Group '%s' and kind '%s' already present in %s resources\n", group, kind, listDesc)
|
||||
return false
|
||||
}
|
||||
projName, group, kind := args[0], args[1], args[2]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer argoio.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
if action(proj, group, kind) {
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
fmt.Printf("Group '%s' and kind '%s' is added to %s resources\n", group, kind, listDesc)
|
||||
*list = append(*list, v1.GroupKind{Group: group, Kind: kind})
|
||||
return true
|
||||
} else {
|
||||
index := -1
|
||||
for i, item := range *list {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
},
|
||||
}
|
||||
if index == -1 {
|
||||
fmt.Printf("Group '%s' and kind '%s' not in %s resources\n", group, kind, listDesc)
|
||||
return false
|
||||
}
|
||||
*list = append((*list)[:index], (*list)[index+1:]...)
|
||||
fmt.Printf("Group '%s' and kind '%s' is removed from %s resources\n", group, kind, listDesc)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func modifyNamespaceResourceCmd(cmdUse, cmdDesc string, clientOpts *argocdclient.ClientOptions, action func(proj *v1alpha1.AppProject, group string, kind string, useWhitelist bool) bool) *cobra.Command {
|
||||
func modifyResourceListCmd(cmdUse, cmdDesc string, clientOpts *argocdclient.ClientOptions, allow bool, namespacedList bool) *cobra.Command {
|
||||
var (
|
||||
list string
|
||||
listType string
|
||||
defaultList string
|
||||
)
|
||||
if namespacedList {
|
||||
defaultList = "deny"
|
||||
} else {
|
||||
defaultList = "allow"
|
||||
}
|
||||
var command = &cobra.Command{
|
||||
Use: cmdUse,
|
||||
Short: cmdDesc,
|
||||
@@ -383,123 +582,63 @@ func modifyNamespaceResourceCmd(cmdUse, cmdDesc string, clientOpts *argocdclient
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
var useWhitelist = false
|
||||
if list == "white" {
|
||||
useWhitelist = true
|
||||
var list, allowList, denyList *[]metav1.GroupKind
|
||||
var listAction, listDesc string
|
||||
var add bool
|
||||
if namespacedList {
|
||||
allowList, denyList = &proj.Spec.NamespaceResourceWhitelist, &proj.Spec.NamespaceResourceBlacklist
|
||||
listDesc = "namespaced"
|
||||
} else {
|
||||
allowList, denyList = &proj.Spec.ClusterResourceWhitelist, &proj.Spec.ClusterResourceBlacklist
|
||||
listDesc = "cluster"
|
||||
}
|
||||
if action(proj, group, kind, useWhitelist) {
|
||||
|
||||
if (listType == "allow") || (listType == "white") {
|
||||
list = allowList
|
||||
listAction = "allowed"
|
||||
add = allow
|
||||
} else {
|
||||
list = denyList
|
||||
listAction = "denied"
|
||||
add = !allow
|
||||
}
|
||||
|
||||
if modifyResourcesList(list, add, listAction+" "+listDesc, group, kind) {
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&list, "list", "l", "black", "Use blacklist or whitelist. This can only be 'white' or 'black'")
|
||||
command.Flags().StringVarP(&listType, "list", "l", defaultList, "Use deny list or allow list. This can only be 'allow' or 'deny'")
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectAllowNamespaceResourceCommand returns a new instance of an `deny-cluster-resources` command
|
||||
func NewProjectAllowNamespaceResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
use := "allow-namespace-resource PROJECT GROUP KIND"
|
||||
desc := "Removes a namespaced API resource from the blacklist or add a namespaced API resource to the whitelist"
|
||||
|
||||
return modifyNamespaceResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string, useWhitelist bool) bool {
|
||||
if useWhitelist {
|
||||
for _, item := range proj.Spec.NamespaceResourceWhitelist {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
fmt.Printf("Group '%s' and kind '%s' already present in whitelisted namespaced resources\n", group, kind)
|
||||
return false
|
||||
}
|
||||
}
|
||||
proj.Spec.NamespaceResourceWhitelist = append(proj.Spec.NamespaceResourceWhitelist, v1.GroupKind{Group: group, Kind: kind})
|
||||
fmt.Printf("Group '%s' and kind '%s' is added to whitelisted namespaced resources\n", group, kind)
|
||||
return true
|
||||
}
|
||||
index := -1
|
||||
for i, item := range proj.Spec.NamespaceResourceBlacklist {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
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:]...)
|
||||
fmt.Printf("Group '%s' and kind '%s' is removed from blacklisted namespaced resources\n", group, kind)
|
||||
return true
|
||||
})
|
||||
desc := "Removes a namespaced API resource from the deny list or add a namespaced API resource to the allow list"
|
||||
return modifyResourceListCmd(use, desc, clientOpts, true, 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 or removes a namespaced API resource from the whitelist"
|
||||
return modifyNamespaceResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string, useWhitelist bool) bool {
|
||||
if useWhitelist {
|
||||
index := -1
|
||||
for i, item := range proj.Spec.NamespaceResourceWhitelist {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
fmt.Printf("Group '%s' and kind '%s' not in whitelisted namespaced resources\n", group, kind)
|
||||
return false
|
||||
}
|
||||
proj.Spec.NamespaceResourceWhitelist = append(proj.Spec.NamespaceResourceWhitelist[:index], proj.Spec.NamespaceResourceWhitelist[index+1:]...)
|
||||
fmt.Printf("Group '%s' and kind '%s' is removed from whitelisted namespaced resources\n", group, kind)
|
||||
return true
|
||||
}
|
||||
|
||||
for _, item := range proj.Spec.NamespaceResourceBlacklist {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
fmt.Printf("Group '%s' and kind '%s' already present in blacklisted namespaced resources\n", group, kind)
|
||||
return false
|
||||
}
|
||||
}
|
||||
proj.Spec.NamespaceResourceBlacklist = append(proj.Spec.NamespaceResourceBlacklist, v1.GroupKind{Group: group, Kind: kind})
|
||||
fmt.Printf("Group '%s' and kind '%s' is added to blacklisted namespaced resources\n", group, kind)
|
||||
return true
|
||||
})
|
||||
desc := "Adds a namespaced API resource to the deny list or removes a namespaced API resource from the allow list"
|
||||
return modifyResourceListCmd(use, desc, clientOpts, false, 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 modifyClusterResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
|
||||
index := -1
|
||||
for i, item := range proj.Spec.ClusterResourceWhitelist {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
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
|
||||
})
|
||||
desc := "Removes a cluster-scoped API resource from the allow list and adds it to deny list"
|
||||
return modifyResourceListCmd(use, desc, clientOpts, false, false)
|
||||
}
|
||||
|
||||
// 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 modifyClusterResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
|
||||
for _, item := range proj.Spec.ClusterResourceWhitelist {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
fmt.Printf("Group '%s' and kind '%s' already present in whitelisted cluster resources\n", group, kind)
|
||||
return false
|
||||
}
|
||||
}
|
||||
proj.Spec.ClusterResourceWhitelist = append(proj.Spec.ClusterResourceWhitelist, v1.GroupKind{Group: group, Kind: kind})
|
||||
return true
|
||||
})
|
||||
desc := "Adds a cluster-scoped API resource to the allow list and removes it from deny list"
|
||||
return modifyResourceListCmd(use, desc, clientOpts, true, false)
|
||||
}
|
||||
|
||||
// NewProjectRemoveSourceCommand returns a new instance of an `argocd proj remove-src` command
|
||||
@@ -571,7 +710,7 @@ func printProjectNames(projects []v1alpha1.AppProject) {
|
||||
// Print table of project info
|
||||
func printProjectTable(projects []v1alpha1.AppProject) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "NAME\tDESCRIPTION\tDESTINATIONS\tSOURCES\tCLUSTER-RESOURCE-WHITELIST\tNAMESPACE-RESOURCE-BLACKLIST\tORPHANED-RESOURCES\n")
|
||||
fmt.Fprintf(w, "NAME\tDESCRIPTION\tDESTINATIONS\tSOURCES\tCLUSTER-RESOURCE-WHITELIST\tNAMESPACE-RESOURCE-BLACKLIST\tSIGNATURE-KEYS\tORPHANED-RESOURCES\n")
|
||||
for _, p := range projects {
|
||||
printProjectLine(w, &p)
|
||||
}
|
||||
@@ -612,11 +751,15 @@ func formatOrphanedResources(p *v1alpha1.AppProject) string {
|
||||
if p.Spec.OrphanedResources == nil {
|
||||
return "disabled"
|
||||
}
|
||||
return fmt.Sprintf("enabled (warn=%v)", p.Spec.OrphanedResources.IsWarn())
|
||||
details := fmt.Sprintf("warn=%v", p.Spec.OrphanedResources.IsWarn())
|
||||
if len(p.Spec.OrphanedResources.Ignore) > 0 {
|
||||
details = fmt.Sprintf("%s, ignored %d", details, len(p.Spec.OrphanedResources.Ignore))
|
||||
}
|
||||
return fmt.Sprintf("enabled (%s)", details)
|
||||
}
|
||||
|
||||
func printProjectLine(w io.Writer, p *v1alpha1.AppProject) {
|
||||
var destinations, sourceRepos, clusterWhitelist, namespaceBlacklist string
|
||||
var destinations, sourceRepos, clusterWhitelist, namespaceBlacklist, signatureKeys string
|
||||
switch len(p.Spec.Destinations) {
|
||||
case 0:
|
||||
destinations = "<none>"
|
||||
@@ -647,11 +790,17 @@ func printProjectLine(w io.Writer, p *v1alpha1.AppProject) {
|
||||
default:
|
||||
namespaceBlacklist = fmt.Sprintf("%d resources", len(p.Spec.NamespaceResourceBlacklist))
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%v\t%v\t%v\t%v\t%v\n", p.Name, p.Spec.Description, destinations, sourceRepos, clusterWhitelist, namespaceBlacklist, formatOrphanedResources(p))
|
||||
switch len(p.Spec.SignatureKeys) {
|
||||
case 0:
|
||||
signatureKeys = "<none>"
|
||||
default:
|
||||
signatureKeys = fmt.Sprintf("%d key(s)", len(p.Spec.SignatureKeys))
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%v\t%v\t%v\t%v\t%v\t%v\n", p.Name, p.Spec.Description, destinations, sourceRepos, clusterWhitelist, namespaceBlacklist, signatureKeys, formatOrphanedResources(p))
|
||||
}
|
||||
|
||||
func printProject(p *v1alpha1.AppProject) {
|
||||
const printProjFmtStr = "%-34s%s\n"
|
||||
const printProjFmtStr = "%-29s%s\n"
|
||||
|
||||
fmt.Printf(printProjFmtStr, "Name:", p.Name)
|
||||
fmt.Printf(printProjFmtStr, "Description:", p.Spec.Description)
|
||||
@@ -676,25 +825,37 @@ func printProject(p *v1alpha1.AppProject) {
|
||||
fmt.Printf(printProjFmtStr, "", p.Spec.SourceRepos[i])
|
||||
}
|
||||
|
||||
// Print whitelisted cluster resources
|
||||
// Print allowed 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)
|
||||
fmt.Printf(printProjFmtStr, "Allowed 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
|
||||
// Print denied 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)
|
||||
fmt.Printf(printProjFmtStr, "Denied 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))
|
||||
}
|
||||
|
||||
// Print required signature keys
|
||||
signatureKeysStr := "<none>"
|
||||
if len(p.Spec.SignatureKeys) > 0 {
|
||||
kids := make([]string, 0)
|
||||
for _, key := range p.Spec.SignatureKeys {
|
||||
kids = append(kids, key.KeyID)
|
||||
}
|
||||
signatureKeysStr = strings.Join(kids, ", ")
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Signature keys:", signatureKeysStr)
|
||||
|
||||
fmt.Printf(printProjFmtStr, "Orphaned Resources:", formatOrphanedResources(p))
|
||||
|
||||
}
|
||||
|
||||
@@ -6,15 +6,18 @@ import (
|
||||
"os"
|
||||
"strconv"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/io"
|
||||
timeutil "github.com/argoproj/pkg/time"
|
||||
jwtgo "github.com/dgrijalva/jwt-go/v4"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
projectpkg "github.com/argoproj/argo-cd/pkg/apiclient/project"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
"github.com/argoproj/argo-cd/util/io"
|
||||
"github.com/argoproj/argo-cd/util/jwt"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -36,6 +39,7 @@ func NewProjectRoleCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
roleCommand.AddCommand(NewProjectRoleCreateCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectRoleDeleteCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectRoleCreateTokenCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectRoleListTokensCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectRoleDeleteTokenCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectRoleAddPolicyCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectRoleRemovePolicyCommand(clientOpts))
|
||||
@@ -195,14 +199,25 @@ func NewProjectRoleDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.
|
||||
return command
|
||||
}
|
||||
|
||||
func tokenTimeToString(t int64) string {
|
||||
tokenTimeToString := "Never"
|
||||
if t > 0 {
|
||||
tokenTimeToString = time.Unix(t, 0).Format(time.RFC3339)
|
||||
}
|
||||
return tokenTimeToString
|
||||
}
|
||||
|
||||
// NewProjectRoleCreateTokenCommand returns a new instance of an `argocd proj role create-token` command
|
||||
func NewProjectRoleCreateTokenCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
expiresIn string
|
||||
expiresIn string
|
||||
outputTokenOnly bool
|
||||
tokenID string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "create-token PROJECT ROLE-NAME",
|
||||
Short: "Create a project token",
|
||||
Use: "create-token PROJECT ROLE-NAME",
|
||||
Short: "Create a project token",
|
||||
Aliases: []string{"token-create"},
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
@@ -212,23 +227,109 @@ func NewProjectRoleCreateTokenCommand(clientOpts *argocdclient.ClientOptions) *c
|
||||
roleName := args[1]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer io.Close(conn)
|
||||
if expiresIn == "" {
|
||||
expiresIn = "0s"
|
||||
}
|
||||
duration, err := timeutil.ParseDuration(expiresIn)
|
||||
errors.CheckError(err)
|
||||
token, err := projIf.CreateToken(context.Background(), &projectpkg.ProjectTokenCreateRequest{Project: projName, Role: roleName, ExpiresIn: int64(duration.Seconds())})
|
||||
tokenResponse, err := projIf.CreateToken(context.Background(), &projectpkg.ProjectTokenCreateRequest{
|
||||
Project: projName,
|
||||
Role: roleName,
|
||||
ExpiresIn: int64(duration.Seconds()),
|
||||
Id: tokenID,
|
||||
})
|
||||
errors.CheckError(err)
|
||||
fmt.Println(token.Token)
|
||||
|
||||
token, err := jwtgo.Parse(tokenResponse.Token, nil)
|
||||
if token == nil {
|
||||
err = fmt.Errorf("received malformed token %v", err)
|
||||
errors.CheckError(err)
|
||||
return
|
||||
}
|
||||
|
||||
claims := token.Claims.(jwtgo.MapClaims)
|
||||
issuedAt, _ := jwt.IssuedAt(claims)
|
||||
expiresAt := int64(jwt.Float64Field(claims, "exp"))
|
||||
id := jwt.StringField(claims, "jti")
|
||||
subject := jwt.StringField(claims, "sub")
|
||||
|
||||
if !outputTokenOnly {
|
||||
fmt.Printf("Create token succeeded for %s.\n", subject)
|
||||
fmt.Printf(" ID: %s\n Issued At: %s\n Expires At: %s\n",
|
||||
id, tokenTimeToString(issuedAt), tokenTimeToString(expiresAt),
|
||||
)
|
||||
fmt.Println(" Token: " + tokenResponse.Token)
|
||||
} else {
|
||||
fmt.Println(tokenResponse.Token)
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&expiresIn, "expires-in", "e", "0s", "Duration before the token will expire. (Default: No expiration)")
|
||||
command.Flags().StringVarP(&expiresIn, "expires-in", "e", "",
|
||||
"Duration before the token will expire, eg \"12h\", \"7d\". (Default: No expiration)",
|
||||
)
|
||||
command.Flags().StringVarP(&tokenID, "id", "i", "", "Token unique identifier. (Default: Random UUID)")
|
||||
command.Flags().BoolVarP(&outputTokenOnly, "token-only", "t", false, "Output token only - for use in scripts.")
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func NewProjectRoleListTokensCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
useUnixTime bool
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "list-tokens PROJECT ROLE-NAME",
|
||||
Short: "List tokens for a given role.",
|
||||
Aliases: []string{"list-token", "token-list"},
|
||||
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 io.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
role, _, err := proj.GetRoleByName(roleName)
|
||||
errors.CheckError(err)
|
||||
|
||||
if len(role.JWTTokens) == 0 {
|
||||
fmt.Printf("No tokens for %s.%s\n", projName, roleName)
|
||||
return
|
||||
}
|
||||
|
||||
writer := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0)
|
||||
_, err = fmt.Fprintf(writer, "ID\tISSUED AT\tEXPIRES AT\n")
|
||||
errors.CheckError(err)
|
||||
|
||||
tokenRowFormat := "%s\t%v\t%v\n"
|
||||
for _, token := range role.JWTTokens {
|
||||
if useUnixTime {
|
||||
_, _ = fmt.Fprintf(writer, tokenRowFormat, token.ID, token.IssuedAt, token.ExpiresAt)
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(writer, tokenRowFormat, token.ID, tokenTimeToString(token.IssuedAt), tokenTimeToString(token.ExpiresAt))
|
||||
}
|
||||
}
|
||||
err = writer.Flush()
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
command.Flags().BoolVarP(&useUnixTime, "unixtime", "u", false,
|
||||
"Print timestamps as Unix time instead of converting. Useful for piping into delete-token.",
|
||||
)
|
||||
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",
|
||||
Use: "delete-token PROJECT ROLE-NAME ISSUED-AT",
|
||||
Short: "Delete a project token",
|
||||
Aliases: []string{"token-delete", "remove-token"},
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
c.HelpFunc()(c, args)
|
||||
@@ -332,7 +433,7 @@ func NewProjectRoleGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
|
||||
// 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 {
|
||||
for _, token := range proj.Status.JWTTokensByRole[roleName].Items {
|
||||
expiresAt := "<none>"
|
||||
if token.ExpiresAt > 0 {
|
||||
expiresAt = humanizeTimestamp(token.ExpiresAt)
|
||||
|
||||
@@ -8,13 +8,13 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/io"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
projectpkg "github.com/argoproj/argo-cd/pkg/apiclient/project"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
"github.com/argoproj/argo-cd/util/io"
|
||||
)
|
||||
|
||||
// NewProjectWindowsCommand returns a new instance of the `argocd proj windows` command
|
||||
|
||||
@@ -5,14 +5,14 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
argoio "github.com/argoproj/gitops-engine/pkg/utils/io"
|
||||
"github.com/coreos/go-oidc"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
settingspkg "github.com/argoproj/argo-cd/pkg/apiclient/settings"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
argoio "github.com/argoproj/argo-cd/util/io"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
"github.com/argoproj/argo-cd/util/session"
|
||||
)
|
||||
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/io"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -17,7 +15,9 @@ import (
|
||||
repositorypkg "github.com/argoproj/argo-cd/pkg/apiclient/repository"
|
||||
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
"github.com/argoproj/argo-cd/util/io"
|
||||
)
|
||||
|
||||
// NewRepoCommand returns a new instance of an `argocd repo` command
|
||||
@@ -49,6 +49,7 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
tlsClientCertPath string
|
||||
tlsClientCertKeyPath string
|
||||
enableLfs bool
|
||||
enableOci bool
|
||||
)
|
||||
|
||||
// For better readability and easier formatting
|
||||
@@ -69,6 +70,9 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
|
||||
# Add a private Helm repository named 'stable' via HTTPS
|
||||
argocd repo add https://kubernetes-charts.storage.googleapis.com --type helm --name stable --username test --password test
|
||||
|
||||
# Add a private Helm OCI-based repository named 'stable' via HTTPS
|
||||
argocd repo add helm-oci-registry.cn-zhangjiakou.cr.aliyuncs.com --type helm --name stable --enable-oci --username test --password test
|
||||
`
|
||||
|
||||
var command = &cobra.Command{
|
||||
@@ -126,6 +130,7 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
repo.InsecureIgnoreHostKey = insecureIgnoreHostKey
|
||||
repo.Insecure = insecureSkipServerVerification
|
||||
repo.EnableLFS = enableLfs
|
||||
repo.EnableOCI = enableOci
|
||||
|
||||
if repo.Type == "helm" && repo.Name == "" {
|
||||
errors.CheckError(fmt.Errorf("Must specify --name for repos of type 'helm'"))
|
||||
@@ -157,6 +162,7 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
TlsClientCertData: repo.TLSClientCertData,
|
||||
TlsClientCertKey: repo.TLSClientCertKey,
|
||||
Insecure: repo.IsInsecure(),
|
||||
EnableOci: repo.EnableOCI,
|
||||
}
|
||||
_, err := repoIf.ValidateAccess(context.Background(), &repoAccessReq)
|
||||
errors.CheckError(err)
|
||||
@@ -181,6 +187,7 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
command.Flags().BoolVar(&insecureIgnoreHostKey, "insecure-ignore-host-key", false, "disables SSH strict host key checking (deprecated, use --insecure-skip-server-verification instead)")
|
||||
command.Flags().BoolVar(&insecureSkipServerVerification, "insecure-skip-server-verification", false, "disables server certificate and host key checks")
|
||||
command.Flags().BoolVar(&enableLfs, "enable-lfs", false, "enable git-lfs (Large File Support) on this repository")
|
||||
command.Flags().BoolVar(&enableOci, "enable-oci", false, "enable helm-oci (Helm OCI-Based Repository)")
|
||||
command.Flags().BoolVar(&upsert, "upsert", false, "Override an existing repository with the same name even if the spec differs")
|
||||
return command
|
||||
}
|
||||
@@ -209,7 +216,7 @@ func NewRepoRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
// Print table of repo info
|
||||
func printRepoTable(repos appsv1.Repositories) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintf(w, "TYPE\tNAME\tREPO\tINSECURE\tLFS\tCREDS\tSTATUS\tMESSAGE\n")
|
||||
_, _ = fmt.Fprintf(w, "TYPE\tNAME\tREPO\tINSECURE\tOCI\tLFS\tCREDS\tSTATUS\tMESSAGE\n")
|
||||
for _, r := range repos {
|
||||
var hasCreds string
|
||||
if !r.HasCredentials() {
|
||||
@@ -221,7 +228,7 @@ func printRepoTable(repos appsv1.Repositories) {
|
||||
hasCreds = "true"
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%v\t%s\t%s\t%s\n", r.Type, r.Name, r.Repo, r.IsInsecure(), r.EnableLFS, hasCreds, r.ConnectionState.Status, r.ConnectionState.Message)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%v\t%v\t%s\t%s\t%s\n", r.Type, r.Name, r.Repo, r.IsInsecure(), r.EnableOCI, r.EnableLFS, hasCreds, r.ConnectionState.Status, r.ConnectionState.Message)
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ import (
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/io"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -16,7 +14,9 @@ import (
|
||||
repocredspkg "github.com/argoproj/argo-cd/pkg/apiclient/repocreds"
|
||||
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
"github.com/argoproj/argo-cd/util/io"
|
||||
)
|
||||
|
||||
// NewRepoCredsCommand returns a new instance of an `argocd repocreds` command
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
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/errors"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
)
|
||||
|
||||
@@ -38,6 +38,7 @@ func NewCommand() *cobra.Command {
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
},
|
||||
DisableAutoGenTag: true,
|
||||
}
|
||||
|
||||
command.AddCommand(NewCompletionCommand())
|
||||
@@ -53,6 +54,7 @@ func NewCommand() *cobra.Command {
|
||||
command.AddCommand(NewAccountCommand(&clientOpts))
|
||||
command.AddCommand(NewLogoutCommand(&clientOpts))
|
||||
command.AddCommand(NewCertCommand(&clientOpts))
|
||||
command.AddCommand(NewGPGCommand(&clientOpts))
|
||||
|
||||
defaultLocalConfigPath, err := localconfig.DefaultLocalConfigPath()
|
||||
errors.CheckError(err)
|
||||
@@ -61,6 +63,8 @@ func NewCommand() *cobra.Command {
|
||||
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.ClientCertFile, "client-crt", config.GetFlag("client-crt", ""), "Client certificate file")
|
||||
command.PersistentFlags().StringVar(&clientOpts.ClientCertKeyFile, "client-crt-key", config.GetFlag("client-crt-key", ""), "Client certificate key file")
|
||||
command.PersistentFlags().StringVar(&clientOpts.AuthToken, "auth-token", config.GetFlag("auth-token", ""), "Authentication token")
|
||||
command.PersistentFlags().BoolVar(&clientOpts.GRPCWeb, "grpc-web", config.GetBoolFlag("grpc-web"), "Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2.")
|
||||
command.PersistentFlags().StringVar(&clientOpts.GRPCWebRootPath, "grpc-web-root-path", config.GetFlag("grpc-web-root-path", ""), "Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2. Set web root.")
|
||||
|
||||
@@ -8,12 +8,11 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
argoio "github.com/argoproj/gitops-engine/pkg/utils/io"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/pkg/apiclient/version"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
argoio "github.com/argoproj/argo-cd/util/io"
|
||||
)
|
||||
|
||||
// NewVersionCmd returns a new `version` command to be used as a sub-command to root
|
||||
@@ -130,4 +129,5 @@ func printServerVersion(version *version.VersionMessage, short bool) {
|
||||
fmt.Printf(" Kustomize Version: %s\n", version.KustomizeVersion)
|
||||
fmt.Printf(" Helm Version: %s\n", version.HelmVersion)
|
||||
fmt.Printf(" Kubectl Version: %s\n", version.KubectlVersion)
|
||||
fmt.Printf(" Jsonnet Version: %s\n", version.JsonnetVersion)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
|
||||
commands "github.com/argoproj/argo-cd/cmd/argocd/commands"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
|
||||
// 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"
|
||||
// load the azure plugin (required to authenticate with AKS clusters).
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/azure"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -25,6 +25,7 @@ const (
|
||||
ArgoCDKnownHostsConfigMapName = "argocd-ssh-known-hosts-cm"
|
||||
// Contains TLS certificate data for connecting repositories. Will get mounted as volume to pods
|
||||
ArgoCDTLSCertsConfigMapName = "argocd-tls-certs-cm"
|
||||
ArgoCDGPGKeysConfigMapName = "argocd-gpg-keys-cm"
|
||||
)
|
||||
|
||||
// Some default configurables
|
||||
@@ -50,6 +51,14 @@ const (
|
||||
DefaultPathSSHConfig = "/app/config/ssh"
|
||||
// Default name for the SSH known hosts file
|
||||
DefaultSSHKnownHostsName = "ssh_known_hosts"
|
||||
// Default path to GnuPG home directory
|
||||
DefaultGnuPgHomePath = "/app/config/gpg/keys"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultSyncRetryDuration = 5 * time.Second
|
||||
DefaultSyncRetryMaxDuration = 3 * time.Minute
|
||||
DefaultSyncRetryFactor = int64(2)
|
||||
)
|
||||
|
||||
// Argo CD application related constants
|
||||
@@ -76,6 +85,8 @@ const (
|
||||
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"
|
||||
// LogoutEndpoint is Argo CD's shorthand logout endpoint which invalidates OIDC session after logout
|
||||
LogoutEndpoint = "/auth/logout"
|
||||
// CallbackEndpoint is Argo CD's final callback endpoint we reach after OAuth 2.0 login flow has been completed
|
||||
CallbackEndpoint = "/auth/callback"
|
||||
// DexCallbackEndpoint is Argo CD's final callback endpoint when Dex is configured
|
||||
@@ -114,6 +125,22 @@ const (
|
||||
AnnotationValueManagedByArgoCD = "argocd.argoproj.io"
|
||||
// ResourcesFinalizerName the finalizer value which we inject to finalize deletion of an application
|
||||
ResourcesFinalizerName = "resources-finalizer.argocd.argoproj.io"
|
||||
|
||||
// AnnotationKeyManifestGeneratePaths is an annotation that contains a list of semicolon-separated paths in the
|
||||
// manifests repository that affects the manifest generation. Paths might be either relative or absolute. The
|
||||
// absolute path means an absolute path within the repository and the relative path is relative to the application
|
||||
// source path within the repository.
|
||||
AnnotationKeyManifestGeneratePaths = "argocd.argoproj.io/manifest-generate-paths"
|
||||
|
||||
// AnnotationKeyLinkPrefix tells the UI to add an external link icon to the application node
|
||||
// that links to the value given in the annotation.
|
||||
// The annotation key must be followed by a unique identifier. Ex: link.argocd.argoproj.io/dashboard
|
||||
// It's valid to have multiple annotations that match the prefix.
|
||||
// Values can simply be a url or they can have
|
||||
// an optional link title separated by a "|"
|
||||
// Ex: "http://grafana.example.com/d/yu5UH4MMz/deployments"
|
||||
// Ex: "Go to Dashboard|http://grafana.example.com/d/yu5UH4MMz/deployments"
|
||||
AnnotationKeyLinkPrefix = "link.argocd.argoproj.io/"
|
||||
)
|
||||
|
||||
// Environment variables for tuning and debugging Argo CD
|
||||
@@ -137,8 +164,26 @@ const (
|
||||
EnvK8sClientQPS = "ARGOCD_K8S_CLIENT_QPS"
|
||||
// EnvK8sClientBurst is the burst value used for the kubernetes client (default: twice the client QPS)
|
||||
EnvK8sClientBurst = "ARGOCD_K8S_CLIENT_BURST"
|
||||
// EnvClusterCacheResyncDuration is the env variable that holds cluster cache re-sync duration
|
||||
EnvClusterCacheResyncDuration = "ARGOCD_CLUSTER_CACHE_RESYNC_DURATION"
|
||||
// EnvK8sClientMaxIdleConnections is the number of max idle connections in K8s REST client HTTP transport (default: 500)
|
||||
EnvK8sClientMaxIdleConnections = "ARGOCD_K8S_CLIENT_MAX_IDLE_CONNECTIONS"
|
||||
// EnvGnuPGHome is the path to ArgoCD's GnuPG keyring for signature verification
|
||||
EnvGnuPGHome = "ARGOCD_GNUPGHOME"
|
||||
// EnvWatchAPIBufferSize is the buffer size used to transfer K8S watch events to watch API consumer
|
||||
EnvWatchAPIBufferSize = "ARGOCD_WATCH_API_BUFFER_SIZE"
|
||||
// EnvPauseGenerationAfterFailedAttempts will pause manifest generation after the specified number of failed generation attempts
|
||||
EnvPauseGenerationAfterFailedAttempts = "ARGOCD_PAUSE_GEN_AFTER_FAILED_ATTEMPTS"
|
||||
// EnvPauseGenerationMinutes pauses manifest generation for the specified number of minutes, after sufficient manifest generation failures
|
||||
EnvPauseGenerationMinutes = "ARGOCD_PAUSE_GEN_MINUTES"
|
||||
// EnvPauseGenerationRequests pauses manifest generation for the specified number of requests, after sufficient manifest generation failures
|
||||
EnvPauseGenerationRequests = "ARGOCD_PAUSE_GEN_REQUESTS"
|
||||
// EnvControllerReplicas is the number of controller replicas
|
||||
EnvControllerReplicas = "ARGOCD_CONTROLLER_REPLICAS"
|
||||
// EnvControllerShard is the shard number that should be handled by controller
|
||||
EnvControllerShard = "ARGOCD_CONTROLLER_SHARD"
|
||||
// EnvEnableGRPCTimeHistogramEnv enables gRPC metrics collection
|
||||
EnvEnableGRPCTimeHistogramEnv = "ARGOCD_ENABLE_GRPC_TIME_HISTOGRAM"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -148,9 +193,18 @@ const (
|
||||
MinClientVersion = "1.4.0"
|
||||
// CacheVersion is a objects version cached using util/cache/cache.go.
|
||||
// Number should be bumped in case of backward incompatible change to make sure cache is invalidated after upgrade.
|
||||
CacheVersion = "1.0.0"
|
||||
CacheVersion = "1.8.3"
|
||||
)
|
||||
|
||||
// GetGnuPGHomePath retrieves the path to use for GnuPG home directory, which is either taken from GNUPGHOME environment or a default value
|
||||
func GetGnuPGHomePath() string {
|
||||
if gnuPgHome := os.Getenv(EnvGnuPGHome); gnuPgHome == "" {
|
||||
return DefaultGnuPgHomePath
|
||||
} else {
|
||||
return gnuPgHome
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
// K8sClientConfigQPS controls the QPS to be used in K8s REST client configs
|
||||
K8sClientConfigQPS float32 = 50
|
||||
@@ -158,6 +212,8 @@ var (
|
||||
K8sClientConfigBurst int = 100
|
||||
// K8sMaxIdleConnections controls the number of max idle connections in K8s REST client HTTP transport
|
||||
K8sMaxIdleConnections = 500
|
||||
// K8sMaxIdleConnections controls the duration of cluster cache refresh
|
||||
K8SClusterResyncDuration = 12 * time.Hour
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -179,4 +235,9 @@ func init() {
|
||||
K8sMaxIdleConnections = maxConn
|
||||
}
|
||||
}
|
||||
if clusterResyncDurationStr := os.Getenv(EnvClusterCacheResyncDuration); clusterResyncDurationStr != "" {
|
||||
if duration, err := time.ParseDuration(clusterResyncDurationStr); err == nil {
|
||||
K8SClusterResyncDuration = duration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -15,24 +17,26 @@ import (
|
||||
"github.com/argoproj/gitops-engine/pkg/diff"
|
||||
"github.com/argoproj/gitops-engine/pkg/health"
|
||||
synccommon "github.com/argoproj/gitops-engine/pkg/sync/common"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/io"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/semaphore"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apierr "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
apiruntime "k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
|
||||
// make sure to register workqueue prometheus metrics
|
||||
_ "k8s.io/kubernetes/pkg/util/workqueue/prometheus"
|
||||
_ "k8s.io/component-base/metrics/prometheus/workqueue"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
statecache "github.com/argoproj/argo-cd/controller/cache"
|
||||
@@ -40,13 +44,15 @@ import (
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application"
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
appinformers "github.com/argoproj/argo-cd/pkg/client/informers/externalversions"
|
||||
"github.com/argoproj/argo-cd/pkg/client/informers/externalversions/application/v1alpha1"
|
||||
applisters "github.com/argoproj/argo-cd/pkg/client/listers/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/util/argo"
|
||||
appstatecache "github.com/argoproj/argo-cd/util/cache/appstate"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/errors"
|
||||
"github.com/argoproj/argo-cd/util/glob"
|
||||
logutils "github.com/argoproj/argo-cd/util/log"
|
||||
settings_util "github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
@@ -88,6 +94,7 @@ type ApplicationController struct {
|
||||
// queue contains app namespace/name/comparisonType and used to request app refresh with the predefined comparison type
|
||||
appComparisonTypeRefreshQueue workqueue.RateLimitingInterface
|
||||
appOperationQueue workqueue.RateLimitingInterface
|
||||
projectRefreshQueue workqueue.RateLimitingInterface
|
||||
appInformer cache.SharedIndexInformer
|
||||
appLister applisters.ApplicationLister
|
||||
projInformer cache.SharedIndexInformer
|
||||
@@ -102,11 +109,7 @@ type ApplicationController struct {
|
||||
refreshRequestedAppsMutex *sync.Mutex
|
||||
metricsServer *metrics.MetricsServer
|
||||
kubectlSemaphore *semaphore.Weighted
|
||||
}
|
||||
|
||||
type ApplicationControllerConfig struct {
|
||||
InstanceID string
|
||||
Namespace string
|
||||
clusterFilter func(cluster *appv1.Cluster) bool
|
||||
}
|
||||
|
||||
// NewApplicationController creates new instance of ApplicationController.
|
||||
@@ -122,6 +125,7 @@ func NewApplicationController(
|
||||
selfHealTimeout time.Duration,
|
||||
metricsPort int,
|
||||
kubectlParallelismLimit int64,
|
||||
clusterFilter func(cluster *appv1.Cluster) bool,
|
||||
) (*ApplicationController, error) {
|
||||
log.Infof("appResyncPeriod=%v", appResyncPeriod)
|
||||
db := db.NewDB(namespace, settingsMgr, kubeClientset)
|
||||
@@ -134,6 +138,7 @@ func NewApplicationController(
|
||||
repoClientset: repoClientset,
|
||||
appRefreshQueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "app_reconciliation_queue"),
|
||||
appOperationQueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "app_operation_processing_queue"),
|
||||
projectRefreshQueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "project_reconciliation_queue"),
|
||||
appComparisonTypeRefreshQueue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
|
||||
db: db,
|
||||
statusRefreshTimeout: appResyncPeriod,
|
||||
@@ -142,23 +147,41 @@ func NewApplicationController(
|
||||
auditLogger: argo.NewAuditLogger(namespace, kubeClientset, "argocd-application-controller"),
|
||||
settingsMgr: settingsMgr,
|
||||
selfHealTimeout: selfHealTimeout,
|
||||
clusterFilter: clusterFilter,
|
||||
}
|
||||
if kubectlParallelismLimit > 0 {
|
||||
ctrl.kubectlSemaphore = semaphore.NewWeighted(kubectlParallelismLimit)
|
||||
}
|
||||
kubectl.SetOnKubectlRun(ctrl.onKubectlRun)
|
||||
appInformer, appLister, err := ctrl.newApplicationInformerAndLister()
|
||||
appInformer, appLister := ctrl.newApplicationInformerAndLister()
|
||||
indexers := cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}
|
||||
projInformer := v1alpha1.NewAppProjectInformer(applicationClientset, namespace, appResyncPeriod, indexers)
|
||||
projInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(obj interface{}) {
|
||||
if key, err := cache.MetaNamespaceKeyFunc(obj); err == nil {
|
||||
ctrl.projectRefreshQueue.Add(key)
|
||||
}
|
||||
},
|
||||
UpdateFunc: func(old, new interface{}) {
|
||||
if key, err := cache.MetaNamespaceKeyFunc(new); err == nil {
|
||||
ctrl.projectRefreshQueue.Add(key)
|
||||
}
|
||||
},
|
||||
DeleteFunc: func(obj interface{}) {
|
||||
if key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj); err == nil {
|
||||
ctrl.projectRefreshQueue.Add(key)
|
||||
}
|
||||
},
|
||||
})
|
||||
metricsAddr := fmt.Sprintf("0.0.0.0:%d", metricsPort)
|
||||
var err error
|
||||
ctrl.metricsServer, err = metrics.NewMetricsServer(metricsAddr, appLister, ctrl.canProcessApp, func(r *http.Request) error {
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
indexers := cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}
|
||||
projInformer := v1alpha1.NewAppProjectInformer(applicationClientset, namespace, appResyncPeriod, indexers)
|
||||
metricsAddr := fmt.Sprintf("0.0.0.0:%d", metricsPort)
|
||||
ctrl.metricsServer = metrics.NewMetricsServer(metricsAddr, appLister, func() error {
|
||||
_, err := kubeClientset.Discovery().ServerVersion()
|
||||
return err
|
||||
})
|
||||
stateCache := statecache.NewLiveStateCache(db, appInformer, ctrl.settingsMgr, kubectl, ctrl.metricsServer, ctrl.handleObjectUpdated)
|
||||
stateCache := statecache.NewLiveStateCache(db, appInformer, ctrl.settingsMgr, kubectl, ctrl.metricsServer, ctrl.handleObjectUpdated, clusterFilter)
|
||||
appStateManager := NewAppStateManager(db, applicationClientset, repoClientset, namespace, kubectl, ctrl.settingsMgr, stateCache, projInformer, ctrl.metricsServer)
|
||||
ctrl.appInformer = appInformer
|
||||
ctrl.appLister = appLister
|
||||
@@ -173,7 +196,7 @@ func (ctrl *ApplicationController) GetMetricsServer() *metrics.MetricsServer {
|
||||
return ctrl.metricsServer
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) onKubectlRun(command string) (io.Closer, error) {
|
||||
func (ctrl *ApplicationController) onKubectlRun(command string) (kube.CleanupFunc, error) {
|
||||
ctrl.metricsServer.IncKubectlExec(command)
|
||||
if ctrl.kubectlSemaphore != nil {
|
||||
if err := ctrl.kubectlSemaphore.Acquire(context.Background(), 1); err != nil {
|
||||
@@ -181,13 +204,12 @@ func (ctrl *ApplicationController) onKubectlRun(command string) (io.Closer, erro
|
||||
}
|
||||
ctrl.metricsServer.IncKubectlExecPending(command)
|
||||
}
|
||||
return io.NewCloser(func() error {
|
||||
return func() {
|
||||
if ctrl.kubectlSemaphore != nil {
|
||||
ctrl.kubectlSemaphore.Release(1)
|
||||
ctrl.metricsServer.DecKubectlExecPending(command)
|
||||
}
|
||||
return nil
|
||||
}), nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func isSelfReferencedApp(app *appv1.Application, ref v1.ObjectReference) bool {
|
||||
@@ -200,13 +222,13 @@ func isSelfReferencedApp(app *appv1.Application, ref v1.ObjectReference) bool {
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) getAppProj(app *appv1.Application) (*appv1.AppProject, error) {
|
||||
return argo.GetAppProject(&app.Spec, applisters.NewAppProjectLister(ctrl.projInformer.GetIndexer()), ctrl.namespace)
|
||||
return argo.GetAppProject(&app.Spec, applisters.NewAppProjectLister(ctrl.projInformer.GetIndexer()), ctrl.namespace, ctrl.settingsMgr)
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) handleObjectUpdated(managedByApp map[string]bool, ref v1.ObjectReference) {
|
||||
// if namespaced resource is not managed by any app it might be orphaned resource of some other apps
|
||||
if len(managedByApp) == 0 && ref.Namespace != "" {
|
||||
// retrieve applications which monitor orphaned resources in the same namespace and refresh them unless resource is blacklisted in app project
|
||||
// retrieve applications which monitor orphaned resources in the same namespace and refresh them unless resource is denied in app project
|
||||
if objs, err := ctrl.appInformer.GetIndexer().ByIndex(orphanedIndex, ref.Namespace); err == nil {
|
||||
for i := range objs {
|
||||
app, ok := objs[i].(*appv1.Application)
|
||||
@@ -215,7 +237,7 @@ func (ctrl *ApplicationController) handleObjectUpdated(managedByApp map[string]b
|
||||
}
|
||||
// exclude resource unless it is permitted in the app project. If project is not permitted then it is not controlled by the user and there is no point showing the warning.
|
||||
if proj, err := ctrl.getAppProj(app); err == nil && proj.IsGroupKindPermitted(ref.GroupVersionKind().GroupKind(), true) &&
|
||||
!isKnownOrphanedResourceExclusion(kube.NewResourceKey(ref.GroupVersionKind().Group, ref.GroupVersionKind().Kind, ref.Namespace, ref.Name)) {
|
||||
!isKnownOrphanedResourceExclusion(kube.NewResourceKey(ref.GroupVersionKind().Group, ref.GroupVersionKind().Kind, ref.Namespace, ref.Name), proj) {
|
||||
|
||||
managedByApp[app.Name] = false
|
||||
}
|
||||
@@ -229,6 +251,11 @@ func (ctrl *ApplicationController) handleObjectUpdated(managedByApp map[string]b
|
||||
continue
|
||||
}
|
||||
|
||||
if !ctrl.canProcessApp(obj) {
|
||||
// Don't force refresh app if app belongs to a different controller shard
|
||||
continue
|
||||
}
|
||||
|
||||
level := ComparisonWithNothing
|
||||
if isManagedResource {
|
||||
level = CompareWithRecent
|
||||
@@ -254,20 +281,30 @@ func (ctrl *ApplicationController) setAppManagedResources(a *appv1.Application,
|
||||
}
|
||||
|
||||
// returns true of given resources exist in the namespace by default and not managed by the user
|
||||
func isKnownOrphanedResourceExclusion(key kube.ResourceKey) bool {
|
||||
func isKnownOrphanedResourceExclusion(key kube.ResourceKey, proj *appv1.AppProject) bool {
|
||||
if key.Namespace == "default" && key.Group == "" && key.Kind == kube.ServiceKind && key.Name == "kubernetes" {
|
||||
return true
|
||||
}
|
||||
if key.Group == "" && key.Kind == kube.ServiceAccountKind && key.Name == "default" {
|
||||
return true
|
||||
}
|
||||
list := proj.Spec.OrphanedResources.Ignore
|
||||
for _, item := range list {
|
||||
if item.Kind == "" || glob.Match(item.Kind, key.Kind) {
|
||||
if glob.Match(item.Group, key.Group) {
|
||||
if item.Name == "" || glob.Match(item.Name, key.Name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) getResourceTree(a *appv1.Application, managedResources []*appv1.ResourceDiff) (*appv1.ApplicationTree, error) {
|
||||
nodes := make([]appv1.ResourceNode, 0)
|
||||
|
||||
proj, err := argo.GetAppProject(&a.Spec, applisters.NewAppProjectLister(ctrl.projInformer.GetIndexer()), ctrl.namespace)
|
||||
proj, err := argo.GetAppProject(&a.Spec, applisters.NewAppProjectLister(ctrl.projInformer.GetIndexer()), ctrl.namespace, ctrl.settingsMgr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -316,7 +353,7 @@ func (ctrl *ApplicationController) getResourceTree(a *appv1.Application, managed
|
||||
}
|
||||
orphanedNodes := make([]appv1.ResourceNode, 0)
|
||||
for k := range orphanedNodesMap {
|
||||
if k.Namespace != "" && proj.IsGroupKindPermitted(k.GroupKind(), true) && !isKnownOrphanedResourceExclusion(k) {
|
||||
if k.Namespace != "" && proj.IsGroupKindPermitted(k.GroupKind(), true) && !isKnownOrphanedResourceExclusion(k, proj) {
|
||||
err := ctrl.stateCache.IterateHierarchy(a.Spec.Destination.Server, k, func(child appv1.ResourceNode, appName string) {
|
||||
belongToAnotherApp := false
|
||||
if appName != "" {
|
||||
@@ -341,6 +378,9 @@ func (ctrl *ApplicationController) getResourceTree(a *appv1.Application, managed
|
||||
}}
|
||||
}
|
||||
a.Status.SetConditions(conditions, map[appv1.ApplicationConditionType]bool{appv1.ApplicationConditionOrphanedResourceWarning: true})
|
||||
sort.Slice(orphanedNodes, func(i, j int) bool {
|
||||
return orphanedNodes[i].ResourceRef.String() < orphanedNodes[j].ResourceRef.String()
|
||||
})
|
||||
return &appv1.ApplicationTree{Nodes: nodes, OrphanedNodes: orphanedNodes}, nil
|
||||
}
|
||||
|
||||
@@ -369,7 +409,10 @@ func (ctrl *ApplicationController) managedResources(comparisonResult *comparison
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resDiffPtr, err := diff.Diff(target, live, comparisonResult.diffNormalizer, compareOptions)
|
||||
resDiffPtr, err := diff.Diff(target, live,
|
||||
diff.WithNormalizer(comparisonResult.diffNormalizer),
|
||||
diff.WithLogr(logutils.NewLogrusLogger(log.New())),
|
||||
diff.IgnoreAggregatedRoles(compareOptions.IgnoreAggregatedRoles))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -395,11 +438,6 @@ func (ctrl *ApplicationController) managedResources(comparisonResult *comparison
|
||||
} else {
|
||||
item.TargetState = "null"
|
||||
}
|
||||
jsonDiff, err := resDiff.JSONFormat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.Diff = jsonDiff
|
||||
item.PredictedLiveState = string(resDiff.PredictedLive)
|
||||
item.NormalizedLiveState = string(resDiff.NormalizedLive)
|
||||
|
||||
@@ -414,11 +452,16 @@ func (ctrl *ApplicationController) Run(ctx context.Context, statusProcessors int
|
||||
defer ctrl.appRefreshQueue.ShutDown()
|
||||
defer ctrl.appComparisonTypeRefreshQueue.ShutDown()
|
||||
defer ctrl.appOperationQueue.ShutDown()
|
||||
defer ctrl.projectRefreshQueue.ShutDown()
|
||||
|
||||
ctrl.metricsServer.RegisterClustersInfoSource(ctx, ctrl.stateCache)
|
||||
ctrl.RegisterClusterSecretUpdater(ctx)
|
||||
|
||||
go ctrl.appInformer.Run(ctx.Done())
|
||||
go ctrl.projInformer.Run(ctx.Done())
|
||||
|
||||
errors.CheckError(ctrl.stateCache.Init())
|
||||
|
||||
if !cache.WaitForCacheSync(ctx.Done(), ctrl.appInformer.HasSynced, ctrl.projInformer.HasSynced) {
|
||||
log.Error("Timed out waiting for caches to sync")
|
||||
return
|
||||
@@ -445,6 +488,11 @@ func (ctrl *ApplicationController) Run(ctx context.Context, statusProcessors int
|
||||
for ctrl.processAppComparisonTypeQueueItem() {
|
||||
}
|
||||
}, time.Second, ctx.Done())
|
||||
|
||||
go wait.Until(func() {
|
||||
for ctrl.processProjectQueueItem() {
|
||||
}
|
||||
}, time.Second, ctx.Done())
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
@@ -461,8 +509,10 @@ func (ctrl *ApplicationController) requestAppRefresh(appName string, compareWith
|
||||
}
|
||||
if after != nil {
|
||||
ctrl.appRefreshQueue.AddAfter(key, *after)
|
||||
ctrl.appOperationQueue.AddAfter(key, *after)
|
||||
} else {
|
||||
ctrl.appRefreshQueue.Add(key)
|
||||
ctrl.appOperationQueue.Add(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -500,11 +550,13 @@ func (ctrl *ApplicationController) processAppOperationQueueItem() (processNext b
|
||||
// This happens after app was deleted, but the work queue still had an entry for it.
|
||||
return
|
||||
}
|
||||
app, ok := obj.(*appv1.Application)
|
||||
origApp, ok := obj.(*appv1.Application)
|
||||
if !ok {
|
||||
log.Warnf("Key '%s' in index is not an application", appKey)
|
||||
return
|
||||
}
|
||||
app := origApp.DeepCopy()
|
||||
|
||||
if app.Operation != nil {
|
||||
ctrl.processRequestedAppOperation(app)
|
||||
} else if app.DeletionTimestamp != nil && app.CascadedDeletion() {
|
||||
@@ -549,6 +601,75 @@ func (ctrl *ApplicationController) processAppComparisonTypeQueueItem() (processN
|
||||
return
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) processProjectQueueItem() (processNext bool) {
|
||||
key, shutdown := ctrl.projectRefreshQueue.Get()
|
||||
processNext = true
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
|
||||
}
|
||||
ctrl.projectRefreshQueue.Done(key)
|
||||
}()
|
||||
if shutdown {
|
||||
processNext = false
|
||||
return
|
||||
}
|
||||
obj, exists, err := ctrl.projInformer.GetIndexer().GetByKey(key.(string))
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get project '%s' from informer index: %+v", key, err)
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
// This happens after appproj was deleted, but the work queue still had an entry for it.
|
||||
return
|
||||
}
|
||||
origProj, ok := obj.(*appv1.AppProject)
|
||||
if !ok {
|
||||
log.Warnf("Key '%s' in index is not an appproject", key)
|
||||
return
|
||||
}
|
||||
|
||||
if origProj.DeletionTimestamp != nil && origProj.HasFinalizer() {
|
||||
if err := ctrl.finalizeProjectDeletion(origProj.DeepCopy()); err != nil {
|
||||
log.Warnf("Failed to finalize project deletion: %v", err)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) finalizeProjectDeletion(proj *appv1.AppProject) error {
|
||||
apps, err := ctrl.appLister.Applications(ctrl.namespace).List(labels.Everything())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
appsCount := 0
|
||||
for i := range apps {
|
||||
if apps[i].Spec.GetProject() == proj.Name {
|
||||
appsCount++
|
||||
break
|
||||
}
|
||||
}
|
||||
if appsCount == 0 {
|
||||
return ctrl.removeProjectFinalizer(proj)
|
||||
} else {
|
||||
log.Infof("Cannot remove project '%s' finalizer as is referenced by %d applications", proj.Name, appsCount)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) removeProjectFinalizer(proj *appv1.AppProject) error {
|
||||
proj.RemoveFinalizer()
|
||||
var patch []byte
|
||||
patch, _ = json.Marshal(map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"finalizers": proj.Finalizers,
|
||||
},
|
||||
})
|
||||
_, err := ctrl.applicationClientset.ArgoprojV1alpha1().AppProjects(ctrl.namespace).Patch(context.Background(), proj.Name, types.MergePatchType, patch, metav1.PatchOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
// shouldBeDeleted returns whether a given resource obj should be deleted on cascade delete of application app
|
||||
func (ctrl *ApplicationController) shouldBeDeleted(app *appv1.Application, obj *unstructured.Unstructured) bool {
|
||||
return !kube.IsCRD(obj) && !isSelfReferencedApp(app, kube.GetObjectRef(obj))
|
||||
@@ -572,7 +693,7 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic
|
||||
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{})
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Get(context.Background(), app.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if !apierr.IsNotFound(err) {
|
||||
logCtx.Errorf("Unable to get refreshed application info prior deleting resources: %v", err)
|
||||
@@ -584,6 +705,11 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = argo.ValidateDestination(context.Background(), &app.Spec.Destination, ctrl.db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
objsMap, err := ctrl.getPermittedAppLiveObjects(app, proj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -591,7 +717,12 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic
|
||||
|
||||
objs := make([]*unstructured.Unstructured, 0)
|
||||
for k := range objsMap {
|
||||
if ctrl.shouldBeDeleted(app, objsMap[k]) && objsMap[k].GetDeletionTimestamp() == nil {
|
||||
// Wait for objects pending deletion to complete before proceeding with next sync wave
|
||||
if objsMap[k].GetDeletionTimestamp() != nil {
|
||||
logCtx.Infof("%d objects remaining for deletion", len(objsMap))
|
||||
return objs, nil
|
||||
}
|
||||
if ctrl.shouldBeDeleted(app, objsMap[k]) {
|
||||
objs = append(objs, objsMap[k])
|
||||
}
|
||||
}
|
||||
@@ -602,9 +733,10 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic
|
||||
}
|
||||
config := metrics.AddMetricsTransportWrapper(ctrl.metricsServer, app, cluster.RESTConfig())
|
||||
|
||||
err = kube.RunAllAsync(len(objs), func(i int) error {
|
||||
obj := objs[i]
|
||||
return ctrl.kubectl.DeleteResource(config, obj.GroupVersionKind(), obj.GetName(), obj.GetNamespace(), false)
|
||||
filteredObjs := FilterObjectsForDeletion(objs)
|
||||
err = kube.RunAllAsync(len(filteredObjs), func(i int) error {
|
||||
obj := filteredObjs[i]
|
||||
return ctrl.kubectl.DeleteResource(context.Background(), config, obj.GroupVersionKind(), obj.GetName(), obj.GetNamespace(), false)
|
||||
})
|
||||
if err != nil {
|
||||
return objs, err
|
||||
@@ -639,16 +771,24 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic
|
||||
"finalizers": app.Finalizers,
|
||||
},
|
||||
})
|
||||
_, err = ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Patch(app.Name, types.MergePatchType, patch)
|
||||
_, err = ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Patch(context.Background(), app.Name, types.MergePatchType, patch, metav1.PatchOptions{})
|
||||
if err != nil {
|
||||
return objs, err
|
||||
}
|
||||
|
||||
logCtx.Infof("Successfully deleted %d resources", len(objs))
|
||||
ctrl.projectRefreshQueue.Add(fmt.Sprintf("%s/%s", app.Namespace, app.Spec.GetProject()))
|
||||
return objs, nil
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) setAppCondition(app *appv1.Application, condition appv1.ApplicationCondition) {
|
||||
// do nothing if app already has same condition
|
||||
for _, c := range app.Status.Conditions {
|
||||
if c.Message == condition.Message && c.Type == condition.Type {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
app.Status.SetConditions([]appv1.ApplicationCondition{condition}, map[appv1.ApplicationConditionType]bool{condition.Type: true})
|
||||
|
||||
var patch []byte
|
||||
@@ -658,7 +798,7 @@ func (ctrl *ApplicationController) setAppCondition(app *appv1.Application, condi
|
||||
},
|
||||
})
|
||||
if err == nil {
|
||||
_, err = ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Patch(app.Name, types.MergePatchType, patch)
|
||||
_, err = ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Patch(context.Background(), app.Name, types.MergePatchType, patch, metav1.PatchOptions{})
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("Unable to set application condition: %v", err)
|
||||
@@ -681,12 +821,13 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli
|
||||
ctrl.setOperationState(app, state)
|
||||
}
|
||||
}()
|
||||
terminating := false
|
||||
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{})
|
||||
freshApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(ctrl.namespace).Get(context.Background(), app.ObjectMeta.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
logCtx.Errorf("Failed to retrieve latest application state: %v", err)
|
||||
return
|
||||
@@ -697,19 +838,49 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli
|
||||
}
|
||||
app = freshApp
|
||||
state = app.Status.OperationState.DeepCopy()
|
||||
logCtx.Infof("Resuming in-progress operation. phase: %s, message: %s", state.Phase, state.Message)
|
||||
terminating = state.Phase == synccommon.OperationTerminating
|
||||
// Failed operation with retry strategy might have be in-progress and has completion time
|
||||
if state.FinishedAt != nil && !terminating {
|
||||
retryAt, err := app.Status.OperationState.Operation.Retry.NextRetryAt(state.FinishedAt.Time, state.RetryCount)
|
||||
if err != nil {
|
||||
state.Phase = synccommon.OperationFailed
|
||||
state.Message = err.Error()
|
||||
ctrl.setOperationState(app, state)
|
||||
return
|
||||
}
|
||||
retryAfter := time.Until(retryAt)
|
||||
if retryAfter > 0 {
|
||||
logCtx.Infof("Skipping retrying in-progress operation. Attempting again at: %s", retryAt.Format(time.RFC3339))
|
||||
ctrl.requestAppRefresh(app.Name, CompareWithLatest.Pointer(), &retryAfter)
|
||||
return
|
||||
} else {
|
||||
// retrying operation. remove previous failure time in app since it is used as a trigger
|
||||
// that previous failed and operation should be retried
|
||||
state.FinishedAt = nil
|
||||
ctrl.setOperationState(app, state)
|
||||
// Get rid of sync results and null out previous operation completion time
|
||||
state.SyncResult = nil
|
||||
}
|
||||
} else {
|
||||
logCtx.Infof("Resuming in-progress operation. phase: %s, message: %s", state.Phase, state.Message)
|
||||
}
|
||||
} else {
|
||||
state = &appv1.OperationState{Phase: synccommon.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 err := argo.ValidateDestination(context.Background(), &app.Spec.Destination, ctrl.db); err != nil {
|
||||
state.Phase = synccommon.OperationFailed
|
||||
state.Message = err.Error()
|
||||
} else {
|
||||
ctrl.appStateManager.SyncAppState(app, state)
|
||||
}
|
||||
|
||||
if state.Phase == synccommon.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{})
|
||||
freshApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(ctrl.namespace).Get(context.Background(), app.ObjectMeta.Name, metav1.GetOptions{})
|
||||
if err == nil {
|
||||
if freshApp.Status.OperationState != nil && freshApp.Status.OperationState.Phase == synccommon.OperationTerminating {
|
||||
state.Phase = synccommon.OperationTerminating
|
||||
@@ -719,10 +890,26 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli
|
||||
// cleanup (e.g. delete jobs, workflows, etc...)
|
||||
}
|
||||
}
|
||||
} else if state.Phase == synccommon.OperationFailed || state.Phase == synccommon.OperationError {
|
||||
if !terminating && (state.RetryCount < state.Operation.Retry.Limit || state.Operation.Retry.Limit < 0) {
|
||||
now := metav1.Now()
|
||||
state.FinishedAt = &now
|
||||
if retryAt, err := state.Operation.Retry.NextRetryAt(now.Time, state.RetryCount); err != nil {
|
||||
state.Phase = synccommon.OperationFailed
|
||||
state.Message = fmt.Sprintf("%s (failed to retry: %v)", state.Message, err)
|
||||
} else {
|
||||
state.Phase = synccommon.OperationRunning
|
||||
state.RetryCount++
|
||||
state.Message = fmt.Sprintf("%s. Retrying attempt #%d at %s.", state.Message, state.RetryCount, retryAt.Format(time.Kitchen))
|
||||
}
|
||||
} else if state.RetryCount > 0 {
|
||||
state.Message = fmt.Sprintf("%s (retried %d times).", state.Message, state.RetryCount)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ctrl.setOperationState(app, state)
|
||||
if state.Phase.Completed() {
|
||||
if state.Phase.Completed() && !app.Operation.Sync.DryRun {
|
||||
// if we just completed an operation, force a refresh so that UI will report up-to-date
|
||||
// sync/health information
|
||||
if _, err := cache.MetaNamespaceKeyFunc(app); err == nil {
|
||||
@@ -735,7 +922,7 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) setOperationState(app *appv1.Application, state *appv1.OperationState) {
|
||||
kube.RetryUntilSucceed(func() error {
|
||||
kube.RetryUntilSucceed(context.Background(), updateOperationStateTimeout, "Update application operation state", logutils.NewLogrusLogger(log.New()), func() error {
|
||||
if state.Phase == "" {
|
||||
// expose any bugs where we neglect to set phase
|
||||
panic("no phase was set")
|
||||
@@ -762,8 +949,15 @@ func (ctrl *ApplicationController) setOperationState(app *appv1.Application, sta
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if app.Status.OperationState != nil && app.Status.OperationState.FinishedAt != nil && state.FinishedAt == nil {
|
||||
patchJSON, err = jsonpatch.MergeMergePatches(patchJSON, []byte(`{"status": {"operationState": {"finishedAt": null}}}`))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
appClient := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(ctrl.namespace)
|
||||
_, err = appClient.Patch(app.Name, types.MergePatchType, patchJSON)
|
||||
_, err = appClient.Patch(context.Background(), app.Name, types.MergePatchType, patchJSON, metav1.PatchOptions{})
|
||||
if err != nil {
|
||||
// Stop retrying updating deleted application
|
||||
if apierr.IsNotFound(err) {
|
||||
@@ -794,7 +988,7 @@ func (ctrl *ApplicationController) setOperationState(app *appv1.Application, sta
|
||||
ctrl.metricsServer.IncSync(app, state)
|
||||
}
|
||||
return nil
|
||||
}, "Update application operation state", context.Background(), updateOperationStateTimeout)
|
||||
})
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext bool) {
|
||||
@@ -825,6 +1019,7 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
log.Warnf("Key '%s' in index is not an application", appKey)
|
||||
return
|
||||
}
|
||||
origApp = origApp.DeepCopy()
|
||||
needRefresh, refreshType, comparisonLevel := ctrl.needRefreshAppStatus(origApp, ctrl.statusRefreshTimeout)
|
||||
|
||||
if !needRefresh {
|
||||
@@ -841,6 +1036,7 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
"time_ms": reconcileDuration.Milliseconds(),
|
||||
"level": comparisonLevel,
|
||||
"dest-server": origApp.Spec.Destination.Server,
|
||||
"dest-name": origApp.Spec.Destination.Name,
|
||||
"dest-namespace": origApp.Spec.Destination.Namespace,
|
||||
}).Info("Reconciliation completed")
|
||||
}()
|
||||
@@ -850,27 +1046,15 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
if err := ctrl.cache.GetAppManagedResources(app.Name, &managedResources); err != nil {
|
||||
logCtx.Warnf("Failed to get cached managed resources for tree reconciliation, fallback to full reconciliation")
|
||||
} else {
|
||||
if tree, err := ctrl.getResourceTree(app, managedResources); err != nil {
|
||||
app.Status.SetConditions(
|
||||
[]appv1.ApplicationCondition{
|
||||
{
|
||||
Type: appv1.ApplicationConditionComparisonError,
|
||||
Message: err.Error(),
|
||||
},
|
||||
},
|
||||
map[appv1.ApplicationConditionType]bool{
|
||||
appv1.ApplicationConditionComparisonError: true,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
var tree *appv1.ApplicationTree
|
||||
if tree, err = ctrl.getResourceTree(app, managedResources); err == nil {
|
||||
app.Status.Summary = tree.GetSummary()
|
||||
if err = ctrl.cache.SetAppResourcesTree(app.Name, tree); err != nil {
|
||||
if err := ctrl.cache.SetAppResourcesTree(app.Name, tree); err != nil {
|
||||
logCtx.Errorf("Failed to cache resources tree: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
now := metav1.Now()
|
||||
app.Status.ObservedAt = &now
|
||||
|
||||
ctrl.persistAppStatus(origApp, &app.Status)
|
||||
return
|
||||
}
|
||||
@@ -894,7 +1078,7 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
revision = app.Status.Sync.Revision
|
||||
}
|
||||
|
||||
observedAt := metav1.Now()
|
||||
now := metav1.Now()
|
||||
compareResult := ctrl.appStateManager.CompareAppState(app, project, revision, app.Spec.Source, refreshType == appv1.RefreshTypeHard, localManifests)
|
||||
for k, v := range compareResult.timings {
|
||||
logCtx = logCtx.WithField(k, v.Milliseconds())
|
||||
@@ -927,17 +1111,23 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
}
|
||||
|
||||
if app.Status.ReconciledAt == nil || comparisonLevel == CompareWithLatest {
|
||||
app.Status.ReconciledAt = &observedAt
|
||||
app.Status.ReconciledAt = &now
|
||||
}
|
||||
app.Status.ObservedAt = &observedAt
|
||||
app.Status.Sync = *compareResult.syncStatus
|
||||
app.Status.Health = *compareResult.healthStatus
|
||||
app.Status.Resources = compareResult.resources
|
||||
sort.Slice(app.Status.Resources, func(i, j int) bool {
|
||||
return resourceStatusKey(app.Status.Resources[i]) < resourceStatusKey(app.Status.Resources[j])
|
||||
})
|
||||
app.Status.SourceType = compareResult.appSourceType
|
||||
ctrl.persistAppStatus(origApp, &app.Status)
|
||||
return
|
||||
}
|
||||
|
||||
func resourceStatusKey(res appv1.ResourceStatus) string {
|
||||
return strings.Join([]string{res.Group, res.Kind, res.Namespace, res.Name}, "/")
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -1020,7 +1210,7 @@ func (ctrl *ApplicationController) normalizeApplication(orig, app *appv1.Applica
|
||||
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)
|
||||
_, err = appClient.Patch(context.Background(), app.Name, types.MergePatchType, patch, metav1.PatchOptions{})
|
||||
if err != nil {
|
||||
logCtx.Errorf("Error persisting normalized application spec: %v", err)
|
||||
} else {
|
||||
@@ -1061,7 +1251,7 @@ func (ctrl *ApplicationController) persistAppStatus(orig *appv1.Application, new
|
||||
}
|
||||
logCtx.Debugf("patch: %s", string(patch))
|
||||
appClient := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(orig.Namespace)
|
||||
_, err = appClient.Patch(orig.Name, types.MergePatchType, patch)
|
||||
_, err = appClient.Patch(context.Background(), orig.Name, types.MergePatchType, patch, metav1.PatchOptions{})
|
||||
if err != nil {
|
||||
logCtx.Warnf("Error updating application: %v", err)
|
||||
} else {
|
||||
@@ -1115,6 +1305,10 @@ func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus *
|
||||
SyncOptions: app.Spec.SyncPolicy.SyncOptions,
|
||||
},
|
||||
InitiatedBy: appv1.OperationInitiator{Automated: true},
|
||||
Retry: appv1.RetryStrategy{Limit: 5},
|
||||
}
|
||||
if app.Spec.SyncPolicy.Retry != nil {
|
||||
op.Retry = *app.Spec.SyncPolicy.Retry
|
||||
}
|
||||
// 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
|
||||
@@ -1147,6 +1341,20 @@ func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus *
|
||||
|
||||
}
|
||||
|
||||
if app.Spec.SyncPolicy.Automated.Prune && !app.Spec.SyncPolicy.Automated.AllowEmpty {
|
||||
bAllNeedPrune := true
|
||||
for _, r := range resources {
|
||||
if !r.RequiresPruning {
|
||||
bAllNeedPrune = false
|
||||
}
|
||||
}
|
||||
if bAllNeedPrune {
|
||||
message := fmt.Sprintf("Skipping sync attempt to %s: auto-sync will wipe out all resources", desiredCommitSHA)
|
||||
logCtx.Warnf(message)
|
||||
return &appv1.ApplicationCondition{Type: appv1.ApplicationConditionSyncError, Message: message}
|
||||
}
|
||||
}
|
||||
|
||||
appIf := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace)
|
||||
_, err := argo.SetAppOperation(appIf, app.Name, &op)
|
||||
if err != nil {
|
||||
@@ -1191,18 +1399,69 @@ func (ctrl *ApplicationController) shouldSelfHeal(app *appv1.Application) (bool,
|
||||
return retryAfter <= 0, retryAfter
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) newApplicationInformerAndLister() (cache.SharedIndexInformer, applisters.ApplicationLister, error) {
|
||||
appInformerFactory := appinformers.NewFilteredSharedInformerFactory(
|
||||
ctrl.applicationClientset,
|
||||
func (ctrl *ApplicationController) canProcessApp(obj interface{}) bool {
|
||||
app, ok := obj.(*appv1.Application)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if ctrl.clusterFilter != nil {
|
||||
cluster, err := ctrl.db.GetCluster(context.Background(), app.Spec.Destination.Server)
|
||||
if err != nil {
|
||||
return ctrl.clusterFilter(nil)
|
||||
}
|
||||
return ctrl.clusterFilter(cluster)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) newApplicationInformerAndLister() (cache.SharedIndexInformer, applisters.ApplicationLister) {
|
||||
informer := cache.NewSharedIndexInformer(
|
||||
&cache.ListWatch{
|
||||
ListFunc: func(options metav1.ListOptions) (apiruntime.Object, error) {
|
||||
return ctrl.applicationClientset.ArgoprojV1alpha1().Applications(ctrl.namespace).List(context.TODO(), options)
|
||||
},
|
||||
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
|
||||
return ctrl.applicationClientset.ArgoprojV1alpha1().Applications(ctrl.namespace).Watch(context.TODO(), options)
|
||||
},
|
||||
},
|
||||
&appv1.Application{},
|
||||
ctrl.statusRefreshTimeout,
|
||||
ctrl.namespace,
|
||||
func(options *metav1.ListOptions) {},
|
||||
cache.Indexers{
|
||||
cache.NamespaceIndex: func(obj interface{}) ([]string, error) {
|
||||
app, ok := obj.(*appv1.Application)
|
||||
if ok {
|
||||
if err := argo.ValidateDestination(context.Background(), &app.Spec.Destination, ctrl.db); err != nil {
|
||||
ctrl.setAppCondition(app, appv1.ApplicationCondition{Type: appv1.ApplicationConditionInvalidSpecError, Message: err.Error()})
|
||||
}
|
||||
}
|
||||
|
||||
return cache.MetaNamespaceIndexFunc(obj)
|
||||
},
|
||||
orphanedIndex: func(obj interface{}) (i []string, e error) {
|
||||
app, ok := obj.(*appv1.Application)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
proj, err := ctrl.getAppProj(app)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
if proj.Spec.OrphanedResources != nil {
|
||||
return []string{app.Spec.Destination.Namespace}, nil
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
)
|
||||
informer := appInformerFactory.Argoproj().V1alpha1().Applications().Informer()
|
||||
lister := appInformerFactory.Argoproj().V1alpha1().Applications().Lister()
|
||||
lister := applisters.NewApplicationLister(informer.GetIndexer())
|
||||
informer.AddEventHandler(
|
||||
cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(obj interface{}) {
|
||||
if !ctrl.canProcessApp(obj) {
|
||||
return
|
||||
}
|
||||
key, err := cache.MetaNamespaceKeyFunc(obj)
|
||||
if err == nil {
|
||||
ctrl.appRefreshQueue.Add(key)
|
||||
@@ -1210,6 +1469,10 @@ func (ctrl *ApplicationController) newApplicationInformerAndLister() (cache.Shar
|
||||
}
|
||||
},
|
||||
UpdateFunc: func(old, new interface{}) {
|
||||
if !ctrl.canProcessApp(new) {
|
||||
return
|
||||
}
|
||||
|
||||
key, err := cache.MetaNamespaceKeyFunc(new)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -1225,6 +1488,9 @@ func (ctrl *ApplicationController) newApplicationInformerAndLister() (cache.Shar
|
||||
ctrl.appOperationQueue.Add(key)
|
||||
},
|
||||
DeleteFunc: func(obj interface{}) {
|
||||
if !ctrl.canProcessApp(obj) {
|
||||
return
|
||||
}
|
||||
// IndexerInformer uses a delta queue, therefore for deletes we have to use this
|
||||
// key function.
|
||||
key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
|
||||
@@ -1234,24 +1500,12 @@ func (ctrl *ApplicationController) newApplicationInformerAndLister() (cache.Shar
|
||||
},
|
||||
},
|
||||
)
|
||||
err := informer.AddIndexers(cache.Indexers{
|
||||
orphanedIndex: func(obj interface{}) (i []string, e error) {
|
||||
app, ok := obj.(*appv1.Application)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
return informer, lister
|
||||
}
|
||||
|
||||
proj, err := ctrl.getAppProj(app)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
if proj.Spec.OrphanedResources != nil {
|
||||
return []string{app.Spec.Destination.Namespace}, nil
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
})
|
||||
return informer, lister, err
|
||||
func (ctrl *ApplicationController) RegisterClusterSecretUpdater(ctx context.Context) {
|
||||
updater := NewClusterInfoUpdater(ctrl.stateCache, ctrl.db, ctrl.appLister.Applications(ctrl.namespace), ctrl.cache, ctrl.clusterFilter)
|
||||
go updater.Run(ctx)
|
||||
}
|
||||
|
||||
func isOperationInProgress(app *appv1.Application) bool {
|
||||
|
||||
@@ -58,8 +58,7 @@ func newFakeController(data *fakeData) *ApplicationController {
|
||||
// Mock out call to GenerateManifest
|
||||
mockRepoClient := mockrepoclient.RepoServerServiceClient{}
|
||||
mockRepoClient.On("GenerateManifest", mock.Anything, mock.Anything).Return(data.manifestResponse, nil)
|
||||
mockRepoClientset := mockrepoclient.Clientset{}
|
||||
mockRepoClientset.On("NewRepoServerClient").Return(&fakeCloser{}, &mockRepoClient, nil)
|
||||
mockRepoClientset := mockrepoclient.Clientset{RepoServerServiceClient: &mockRepoClient}
|
||||
|
||||
secret := corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -99,6 +98,7 @@ func newFakeController(data *fakeData) *ApplicationController {
|
||||
time.Minute,
|
||||
common.DefaultPortArgoCDMetrics,
|
||||
0,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -129,22 +129,18 @@ func newFakeController(data *fakeData) *ApplicationController {
|
||||
if res, ok := data.namespacedResources[key]; ok {
|
||||
appName = res.AppName
|
||||
}
|
||||
action(argoappv1.ResourceNode{ResourceRef: argoappv1.ResourceRef{Group: key.Group, Namespace: key.Namespace, Name: key.Name}}, appName)
|
||||
action(argoappv1.ResourceNode{ResourceRef: argoappv1.ResourceRef{Kind: key.Kind, Group: key.Group, Namespace: key.Namespace, Name: key.Name}}, appName)
|
||||
}).Return(nil)
|
||||
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==
|
||||
name: bWluaWt1YmU=
|
||||
# https://localhost:6443
|
||||
server: aHR0cHM6Ly9sb2NhbGhvc3Q6NjQ0Mw==
|
||||
kind: Secret
|
||||
@@ -197,6 +193,45 @@ status:
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps.git
|
||||
`
|
||||
|
||||
var fakeAppWithDestName = `
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
uid: "123"
|
||||
name: my-app
|
||||
namespace: ` + test.FakeArgoCDNamespace + `
|
||||
spec:
|
||||
destination:
|
||||
namespace: ` + test.FakeDestNamespace + `
|
||||
name: minikube
|
||||
project: default
|
||||
source:
|
||||
path: some/path
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps.git
|
||||
syncPolicy:
|
||||
automated: {}
|
||||
`
|
||||
|
||||
var fakeAppWithDestMismatch = `
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
uid: "123"
|
||||
name: my-app
|
||||
namespace: ` + test.FakeArgoCDNamespace + `
|
||||
spec:
|
||||
destination:
|
||||
namespace: ` + test.FakeDestNamespace + `
|
||||
name: another-cluster
|
||||
server: https://localhost:6443
|
||||
project: default
|
||||
source:
|
||||
path: some/path
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps.git
|
||||
syncPolicy:
|
||||
automated: {}
|
||||
`
|
||||
|
||||
var fakeStrayResource = `
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
@@ -209,8 +244,20 @@ data:
|
||||
`
|
||||
|
||||
func newFakeApp() *argoappv1.Application {
|
||||
return createFakeApp(fakeApp)
|
||||
}
|
||||
|
||||
func newFakeAppWithDestMismatch() *argoappv1.Application {
|
||||
return createFakeApp(fakeAppWithDestMismatch)
|
||||
}
|
||||
|
||||
func newFakeAppWithDestName() *argoappv1.Application {
|
||||
return createFakeApp(fakeAppWithDestName)
|
||||
}
|
||||
|
||||
func createFakeApp(testApp string) *argoappv1.Application {
|
||||
var app argoappv1.Application
|
||||
err := yaml.Unmarshal([]byte(fakeApp), &app)
|
||||
err := yaml.Unmarshal([]byte(testApp), &app)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -235,13 +282,38 @@ func TestAutoSync(t *testing.T) {
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: argoappv1.SyncStatusCodeOutOfSync}})
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(context.Background(), "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 TestAutoSyncNotAllowEmpty(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Spec.SyncPolicy.Automated.Prune = true
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus := argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{})
|
||||
assert.NotNil(t, cond)
|
||||
}
|
||||
|
||||
func TestAutoSyncAllowEmpty(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Spec.SyncPolicy.Automated.Prune = true
|
||||
app.Spec.SyncPolicy.Automated.AllowEmpty = true
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus := argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{})
|
||||
assert.Nil(t, cond)
|
||||
}
|
||||
|
||||
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
|
||||
@@ -254,7 +326,7 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{})
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(context.Background(), "my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
})
|
||||
@@ -269,7 +341,7 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{})
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(context.Background(), "my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
})
|
||||
@@ -285,7 +357,7 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{})
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(context.Background(), "my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
})
|
||||
@@ -302,7 +374,7 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{})
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(context.Background(), "my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
})
|
||||
@@ -328,7 +400,7 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: argoappv1.SyncStatusCodeOutOfSync}})
|
||||
assert.NotNil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(context.Background(), "my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
})
|
||||
@@ -344,7 +416,7 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
{Name: "guestbook", Kind: kube.DeploymentKind, Status: argoappv1.SyncStatusCodeOutOfSync, RequiresPruning: true},
|
||||
})
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(context.Background(), "my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
})
|
||||
@@ -380,7 +452,7 @@ func TestAutoSyncIndicateError(t *testing.T) {
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: argoappv1.SyncStatusCodeOutOfSync}})
|
||||
assert.NotNil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(context.Background(), "my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
}
|
||||
@@ -423,30 +495,31 @@ func TestAutoSyncParameterOverrides(t *testing.T) {
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: argoappv1.SyncStatusCodeOutOfSync}})
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(context.Background(), "my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, app.Operation)
|
||||
}
|
||||
|
||||
// TestFinalizeAppDeletion verifies application deletion
|
||||
func TestFinalizeAppDeletion(t *testing.T) {
|
||||
// Ensure app can be deleted cascading
|
||||
{
|
||||
defaultProj := argoappv1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
},
|
||||
Spec: argoappv1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []argoappv1.ApplicationDestination{
|
||||
{
|
||||
Server: "*",
|
||||
Namespace: "*",
|
||||
},
|
||||
defaultProj := argoappv1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
},
|
||||
Spec: argoappv1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []argoappv1.ApplicationDestination{
|
||||
{
|
||||
Server: "*",
|
||||
Namespace: "*",
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Ensure app can be deleted cascading
|
||||
t.Run("CascadingDelete", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
|
||||
appObj := kube.MustToUnstructured(&app)
|
||||
@@ -468,26 +541,11 @@ func TestFinalizeAppDeletion(t *testing.T) {
|
||||
_, err := ctrl.finalizeApplicationDeletion(app)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, patched)
|
||||
}
|
||||
})
|
||||
|
||||
// Ensure any stray resources irregularly labeled with instance label of app are not deleted upon deleting,
|
||||
// when app project restriction is in place
|
||||
{
|
||||
defaultProj := argoappv1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
},
|
||||
Spec: argoappv1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []argoappv1.ApplicationDestination{
|
||||
{
|
||||
Server: "*",
|
||||
Namespace: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
t.Run("ProjectRestrictionEnforced", func(*testing.T) {
|
||||
restrictedProj := argoappv1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "restricted",
|
||||
@@ -541,7 +599,50 @@ func TestFinalizeAppDeletion(t *testing.T) {
|
||||
for _, o := range objs {
|
||||
assert.NotEqual(t, "test-cm", o.GetName())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DeleteWithDestinationClusterName", func(t *testing.T) {
|
||||
app := newFakeAppWithDestName()
|
||||
appObj := kube.MustToUnstructured(&app)
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}, 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)
|
||||
})
|
||||
|
||||
t.Run("ErrorOnBothDestNameAndServer", func(t *testing.T) {
|
||||
app := newFakeAppWithDestMismatch()
|
||||
appObj := kube.MustToUnstructured(&app)
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}, managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
||||
kube.GetResourceKey(appObj): appObj,
|
||||
}})
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
func() {
|
||||
fakeAppCs.Lock()
|
||||
defer fakeAppCs.Unlock()
|
||||
|
||||
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)
|
||||
})
|
||||
}()
|
||||
_, err := ctrl.finalizeApplicationDeletion(app)
|
||||
assert.EqualError(t, err, "application destination can't have both name and server defined: another-cluster https://localhost:6443")
|
||||
})
|
||||
}
|
||||
|
||||
// TestNormalizeApplication verifies we normalize an application during reconciliation
|
||||
@@ -662,6 +763,43 @@ func TestHandleOrphanedResourceUpdated(t *testing.T) {
|
||||
assert.Equal(t, ComparisonWithNothing, level)
|
||||
}
|
||||
|
||||
func TestGetResourceTree_HasOrphanedResources(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
proj := defaultProj.DeepCopy()
|
||||
proj.Spec.OrphanedResources = &argoappv1.OrphanedResourcesMonitorSettings{}
|
||||
|
||||
managedDeploy := argoappv1.ResourceNode{
|
||||
ResourceRef: argoappv1.ResourceRef{Group: "apps", Kind: "Deployment", Namespace: "default", Name: "nginx-deployment", Version: "v1"},
|
||||
}
|
||||
orphanedDeploy1 := argoappv1.ResourceNode{
|
||||
ResourceRef: argoappv1.ResourceRef{Group: "apps", Kind: "Deployment", Namespace: "default", Name: "deploy1"},
|
||||
}
|
||||
orphanedDeploy2 := argoappv1.ResourceNode{
|
||||
ResourceRef: argoappv1.ResourceRef{Group: "apps", Kind: "Deployment", Namespace: "default", Name: "deploy2"},
|
||||
}
|
||||
|
||||
ctrl := newFakeController(&fakeData{
|
||||
apps: []runtime.Object{app, proj},
|
||||
namespacedResources: map[kube.ResourceKey]namespacedResource{
|
||||
kube.NewResourceKey("apps", "Deployment", "default", "nginx-deployment"): {ResourceNode: managedDeploy},
|
||||
kube.NewResourceKey("apps", "Deployment", "default", "deploy1"): {ResourceNode: orphanedDeploy1},
|
||||
kube.NewResourceKey("apps", "Deployment", "default", "deploy2"): {ResourceNode: orphanedDeploy2},
|
||||
},
|
||||
})
|
||||
tree, err := ctrl.getResourceTree(app, []*argoappv1.ResourceDiff{{
|
||||
Namespace: "default",
|
||||
Name: "nginx-deployment",
|
||||
Kind: "Deployment",
|
||||
Group: "apps",
|
||||
LiveState: "null",
|
||||
TargetState: test.DeploymentManifest,
|
||||
}})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tree.Nodes, []argoappv1.ResourceNode{managedDeploy})
|
||||
assert.Equal(t, tree.OrphanedNodes, []argoappv1.ResourceNode{orphanedDeploy1, orphanedDeploy2})
|
||||
}
|
||||
|
||||
func TestSetOperationStateOnDeletedApp(t *testing.T) {
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{}})
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
@@ -847,7 +985,7 @@ func TestUpdateReconciledAt(t *testing.T) {
|
||||
|
||||
_, updated, err = unstructured.NestedString(receivedPatch, "status", "observedAt")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, updated)
|
||||
assert.False(t, updated)
|
||||
})
|
||||
|
||||
t.Run("NotUpdatedOnPartialReconciliation", func(t *testing.T) {
|
||||
@@ -863,7 +1001,195 @@ func TestUpdateReconciledAt(t *testing.T) {
|
||||
|
||||
_, updated, err = unstructured.NestedString(receivedPatch, "status", "observedAt")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, updated)
|
||||
assert.False(t, updated)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestFinalizeProjectDeletion_HasApplications(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
proj := &argoappv1.AppProject{ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: test.FakeArgoCDNamespace}}
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, proj}})
|
||||
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
patched := false
|
||||
fakeAppCs.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
patched = true
|
||||
return true, nil, nil
|
||||
})
|
||||
|
||||
err := ctrl.finalizeProjectDeletion(proj)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, patched)
|
||||
}
|
||||
|
||||
func TestFinalizeProjectDeletion_DoesNotHaveApplications(t *testing.T) {
|
||||
proj := &argoappv1.AppProject{ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: test.FakeArgoCDNamespace}}
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{&defaultProj}})
|
||||
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
receivedPatch := map[string]interface{}{}
|
||||
fakeAppCs.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
if patchAction, ok := action.(kubetesting.PatchAction); ok {
|
||||
assert.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch))
|
||||
}
|
||||
return true, nil, nil
|
||||
})
|
||||
|
||||
err := ctrl.finalizeProjectDeletion(proj)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"finalizers": nil,
|
||||
},
|
||||
}, receivedPatch)
|
||||
}
|
||||
|
||||
func TestProcessRequestedAppOperation_FailedNoRetries(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Spec.Project = "invalid-project"
|
||||
app.Operation = &argoappv1.Operation{
|
||||
Sync: &argoappv1.SyncOperation{},
|
||||
}
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
receivedPatch := map[string]interface{}{}
|
||||
fakeAppCs.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
if patchAction, ok := action.(kubetesting.PatchAction); ok {
|
||||
assert.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch))
|
||||
}
|
||||
return true, nil, nil
|
||||
})
|
||||
|
||||
ctrl.processRequestedAppOperation(app)
|
||||
|
||||
phase, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "phase")
|
||||
assert.Equal(t, string(synccommon.OperationError), phase)
|
||||
}
|
||||
|
||||
func TestProcessRequestedAppOperation_InvalidDestination(t *testing.T) {
|
||||
app := newFakeAppWithDestMismatch()
|
||||
app.Spec.Project = "test-project"
|
||||
app.Operation = &argoappv1.Operation{
|
||||
Sync: &argoappv1.SyncOperation{},
|
||||
}
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
receivedPatch := map[string]interface{}{}
|
||||
func() {
|
||||
fakeAppCs.Lock()
|
||||
defer fakeAppCs.Unlock()
|
||||
fakeAppCs.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
if patchAction, ok := action.(kubetesting.PatchAction); ok {
|
||||
assert.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch))
|
||||
}
|
||||
return true, nil, nil
|
||||
})
|
||||
}()
|
||||
|
||||
ctrl.processRequestedAppOperation(app)
|
||||
|
||||
phase, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "phase")
|
||||
assert.Equal(t, string(synccommon.OperationFailed), phase)
|
||||
message, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "message")
|
||||
assert.Contains(t, message, "application destination can't have both name and server defined: another-cluster https://localhost:6443")
|
||||
}
|
||||
|
||||
func TestProcessRequestedAppOperation_FailedHasRetries(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Spec.Project = "invalid-project"
|
||||
app.Operation = &argoappv1.Operation{
|
||||
Sync: &argoappv1.SyncOperation{},
|
||||
Retry: argoappv1.RetryStrategy{Limit: 1},
|
||||
}
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
receivedPatch := map[string]interface{}{}
|
||||
fakeAppCs.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
if patchAction, ok := action.(kubetesting.PatchAction); ok {
|
||||
assert.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch))
|
||||
}
|
||||
return true, nil, nil
|
||||
})
|
||||
|
||||
ctrl.processRequestedAppOperation(app)
|
||||
|
||||
phase, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "phase")
|
||||
assert.Equal(t, string(synccommon.OperationRunning), phase)
|
||||
message, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "message")
|
||||
assert.Contains(t, message, "Retrying attempt #1")
|
||||
retryCount, _, _ := unstructured.NestedFloat64(receivedPatch, "status", "operationState", "retryCount")
|
||||
assert.Equal(t, float64(1), retryCount)
|
||||
}
|
||||
|
||||
func TestProcessRequestedAppOperation_RunningPreviouslyFailed(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Operation = &argoappv1.Operation{
|
||||
Sync: &argoappv1.SyncOperation{},
|
||||
Retry: argoappv1.RetryStrategy{Limit: 1},
|
||||
}
|
||||
app.Status.OperationState.Phase = synccommon.OperationRunning
|
||||
app.Status.OperationState.SyncResult.Resources = []*argoappv1.ResourceResult{{
|
||||
Name: "guestbook",
|
||||
Kind: "Deployment",
|
||||
Group: "apps",
|
||||
Status: synccommon.ResultCodeSyncFailed,
|
||||
}}
|
||||
|
||||
data := &fakeData{
|
||||
apps: []runtime.Object{app, &defaultProj},
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(data)
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
receivedPatch := map[string]interface{}{}
|
||||
fakeAppCs.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
if patchAction, ok := action.(kubetesting.PatchAction); ok {
|
||||
assert.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch))
|
||||
}
|
||||
return true, nil, nil
|
||||
})
|
||||
|
||||
ctrl.processRequestedAppOperation(app)
|
||||
|
||||
phase, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "phase")
|
||||
assert.Equal(t, string(synccommon.OperationSucceeded), phase)
|
||||
}
|
||||
|
||||
func TestProcessRequestedAppOperation_HasRetriesTerminated(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Operation = &argoappv1.Operation{
|
||||
Sync: &argoappv1.SyncOperation{},
|
||||
Retry: argoappv1.RetryStrategy{Limit: 10},
|
||||
}
|
||||
app.Status.OperationState.Phase = synccommon.OperationTerminating
|
||||
|
||||
data := &fakeData{
|
||||
apps: []runtime.Object{app, &defaultProj},
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(data)
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
receivedPatch := map[string]interface{}{}
|
||||
fakeAppCs.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
if patchAction, ok := action.(kubetesting.PatchAction); ok {
|
||||
assert.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch))
|
||||
}
|
||||
return true, nil, nil
|
||||
})
|
||||
|
||||
ctrl.processRequestedAppOperation(app)
|
||||
|
||||
phase, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "phase")
|
||||
assert.Equal(t, string(synccommon.OperationFailed), phase)
|
||||
}
|
||||
|
||||
238
controller/cache/cache.go
vendored
@@ -2,6 +2,7 @@ package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"github.com/argoproj/gitops-engine/pkg/health"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/semaphore"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
@@ -16,9 +18,12 @@ import (
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/controller/metrics"
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/argo"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
logutils "github.com/argoproj/argo-cd/util/log"
|
||||
"github.com/argoproj/argo-cd/util/lua"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
@@ -40,6 +45,8 @@ type LiveStateCache interface {
|
||||
Run(ctx context.Context) error
|
||||
// Returns information about monitored clusters
|
||||
GetClustersInfo() []clustercache.ClusterInfo
|
||||
// Init must be executed before cache can be used
|
||||
Init() error
|
||||
}
|
||||
|
||||
type ObjectUpdatedHandler = func(managedByApp map[string]bool, ref v1.ObjectReference)
|
||||
@@ -59,7 +66,8 @@ func NewLiveStateCache(
|
||||
settingsMgr *settings.SettingsManager,
|
||||
kubectl kube.Kubectl,
|
||||
metricsServer *metrics.MetricsServer,
|
||||
onObjectUpdated ObjectUpdatedHandler) LiveStateCache {
|
||||
onObjectUpdated ObjectUpdatedHandler,
|
||||
clusterFilter func(cluster *appv1.Cluster) bool) LiveStateCache {
|
||||
|
||||
return &liveStateCache{
|
||||
appInformer: appInformer,
|
||||
@@ -69,36 +77,53 @@ func NewLiveStateCache(
|
||||
kubectl: kubectl,
|
||||
settingsMgr: settingsMgr,
|
||||
metricsServer: metricsServer,
|
||||
// The default limit of 50 is chosen based on experiments.
|
||||
listSemaphore: semaphore.NewWeighted(50),
|
||||
clusterFilter: clusterFilter,
|
||||
}
|
||||
}
|
||||
|
||||
type liveStateCache struct {
|
||||
db db.ArgoDB
|
||||
clusters map[string]clustercache.ClusterCache
|
||||
lock sync.RWMutex
|
||||
appInformer cache.SharedIndexInformer
|
||||
onObjectUpdated ObjectUpdatedHandler
|
||||
kubectl kube.Kubectl
|
||||
settingsMgr *settings.SettingsManager
|
||||
metricsServer *metrics.MetricsServer
|
||||
cacheSettings clustercache.Settings
|
||||
type cacheSettings struct {
|
||||
clusterSettings clustercache.Settings
|
||||
appInstanceLabelKey string
|
||||
}
|
||||
|
||||
func (c *liveStateCache) loadCacheSettings() (*clustercache.Settings, string, error) {
|
||||
type liveStateCache struct {
|
||||
db db.ArgoDB
|
||||
appInformer cache.SharedIndexInformer
|
||||
onObjectUpdated ObjectUpdatedHandler
|
||||
kubectl kube.Kubectl
|
||||
settingsMgr *settings.SettingsManager
|
||||
metricsServer *metrics.MetricsServer
|
||||
clusterFilter func(cluster *appv1.Cluster) bool
|
||||
|
||||
// listSemaphore is used to limit the number of concurrent memory consuming operations on the
|
||||
// k8s list queries results across all clusters to avoid memory spikes during cache initialization.
|
||||
listSemaphore *semaphore.Weighted
|
||||
|
||||
clusters map[string]clustercache.ClusterCache
|
||||
cacheSettings cacheSettings
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func (c *liveStateCache) loadCacheSettings() (*cacheSettings, error) {
|
||||
appInstanceLabelKey, err := c.settingsMgr.GetAppInstanceLabelKey()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, err
|
||||
}
|
||||
resourcesFilter, err := c.settingsMgr.GetResourcesFilter()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, err
|
||||
}
|
||||
resourceOverrides, err := c.settingsMgr.GetResourceOverrides()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, err
|
||||
}
|
||||
return &clustercache.Settings{ResourceHealthOverride: lua.ResourceHealthOverrides(resourceOverrides), ResourcesFilter: resourcesFilter}, appInstanceLabelKey, nil
|
||||
clusterSettings := clustercache.Settings{
|
||||
ResourceHealthOverride: lua.ResourceHealthOverrides(resourceOverrides),
|
||||
ResourcesFilter: resourcesFilter,
|
||||
}
|
||||
return &cacheSettings{clusterSettings, appInstanceLabelKey}, nil
|
||||
}
|
||||
|
||||
func asResourceNode(r *clustercache.Resource) appv1.ResourceNode {
|
||||
@@ -132,6 +157,7 @@ func asResourceNode(r *clustercache.Resource) appv1.ResourceNode {
|
||||
NetworkingInfo: resourceInfo.NetworkingInfo,
|
||||
Images: resourceInfo.Images,
|
||||
Health: resHealth,
|
||||
CreatedAt: r.CreationTimestamp,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,6 +223,7 @@ func skipAppRequeuing(key kube.ResourceKey) bool {
|
||||
func (c *liveStateCache) getCluster(server string) (clustercache.ClusterCache, error) {
|
||||
c.lock.RLock()
|
||||
clusterCache, ok := c.clusters[server]
|
||||
cacheSettings := c.cacheSettings
|
||||
c.lock.RUnlock()
|
||||
|
||||
if ok {
|
||||
@@ -216,14 +243,20 @@ func (c *liveStateCache) getCluster(server string) (clustercache.ClusterCache, e
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !c.canHandleCluster(cluster) {
|
||||
return nil, fmt.Errorf("controller is configured to ignore cluster %s", cluster.Server)
|
||||
}
|
||||
|
||||
clusterCache = clustercache.NewClusterCache(cluster.RESTConfig(),
|
||||
clustercache.SetSettings(c.cacheSettings),
|
||||
clustercache.SetListSemaphore(c.listSemaphore),
|
||||
clustercache.SetResyncTimeout(common.K8SClusterResyncDuration),
|
||||
clustercache.SetSettings(cacheSettings.clusterSettings),
|
||||
clustercache.SetNamespaces(cluster.Namespaces),
|
||||
clustercache.SetPopulateResourceInfoHandler(func(un *unstructured.Unstructured, isRoot bool) (interface{}, bool) {
|
||||
res := &ResourceInfo{}
|
||||
populateNodeInfo(un, res)
|
||||
res.Health, _ = health.GetResourceHealth(un, c.cacheSettings.ResourceHealthOverride)
|
||||
appName := kube.GetAppInstanceLabel(un, c.appInstanceLabelKey)
|
||||
res.Health, _ = health.GetResourceHealth(un, cacheSettings.clusterSettings.ResourceHealthOverride)
|
||||
appName := kube.GetAppInstanceLabel(un, cacheSettings.appInstanceLabelKey)
|
||||
if isRoot && appName != "" {
|
||||
res.AppName = appName
|
||||
}
|
||||
@@ -232,6 +265,7 @@ func (c *liveStateCache) getCluster(server string) (clustercache.ClusterCache, e
|
||||
// want the full resource to be available in our cache (to diff), so we store all CRDs
|
||||
return res, res.AppName != "" || un.GroupVersionKind().Kind == kube.CustomResourceDefinitionKind
|
||||
}),
|
||||
clustercache.SetLogr(logutils.NewLogrusLogger(log.WithField("server", cluster.Server))),
|
||||
)
|
||||
|
||||
_ = clusterCache.OnResourceUpdated(func(newRes *clustercache.Resource, oldRes *clustercache.Resource, namespaceResources map[kube.ResourceKey]*clustercache.Resource) {
|
||||
@@ -260,7 +294,7 @@ func (c *liveStateCache) getCluster(server string) (clustercache.ClusterCache, e
|
||||
c.metricsServer.IncClusterEventsCount(cluster.Server, gvk.Group, gvk.Kind)
|
||||
})
|
||||
|
||||
c.clusters[cluster.Server] = clusterCache
|
||||
c.clusters[server] = clusterCache
|
||||
|
||||
return clusterCache, nil
|
||||
}
|
||||
@@ -277,14 +311,14 @@ func (c *liveStateCache) getSyncedCluster(server string) (clustercache.ClusterCa
|
||||
return clusterCache, nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) invalidate(settings clustercache.Settings, appInstanceLabelKey string) {
|
||||
func (c *liveStateCache) invalidate(cacheSettings cacheSettings) {
|
||||
log.Info("invalidating live state cache")
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
c.appInstanceLabelKey = appInstanceLabelKey
|
||||
c.cacheSettings = cacheSettings
|
||||
for _, clust := range c.clusters {
|
||||
clust.Invalidate(clustercache.SetSettings(settings))
|
||||
clust.Invalidate(clustercache.SetSettings(cacheSettings.clusterSettings))
|
||||
}
|
||||
log.Info("live state cache invalidated")
|
||||
}
|
||||
@@ -339,9 +373,17 @@ func (c *liveStateCache) GetVersionsInfo(serverURL string) (string, []metav1.API
|
||||
return clusterInfo.GetServerVersion(), clusterInfo.GetAPIGroups(), nil
|
||||
}
|
||||
|
||||
func isClusterHasApps(apps []interface{}, cluster *appv1.Cluster) bool {
|
||||
func (c *liveStateCache) isClusterHasApps(apps []interface{}, cluster *appv1.Cluster) bool {
|
||||
for _, obj := range apps {
|
||||
if app, ok := obj.(*appv1.Application); ok && app.Spec.Destination.Server == cluster.Server {
|
||||
app, ok := obj.(*appv1.Application)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
err := argo.ValidateDestination(context.Background(), &app.Spec.Destination, c.db)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if app.Spec.Destination.Server == cluster.Server {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -356,7 +398,7 @@ func (c *liveStateCache) watchSettings(ctx context.Context) {
|
||||
for !done {
|
||||
select {
|
||||
case <-updateCh:
|
||||
nextCacheSettings, appInstanceLabelKey, err := c.loadCacheSettings()
|
||||
nextCacheSettings, err := c.loadCacheSettings()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to read updated settings: %v", err)
|
||||
continue
|
||||
@@ -370,7 +412,7 @@ func (c *liveStateCache) watchSettings(ctx context.Context) {
|
||||
}
|
||||
c.lock.Unlock()
|
||||
if needInvalidate {
|
||||
c.invalidate(*nextCacheSettings, appInstanceLabelKey)
|
||||
c.invalidate(*nextCacheSettings)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
done = true
|
||||
@@ -381,55 +423,115 @@ func (c *liveStateCache) watchSettings(ctx context.Context) {
|
||||
close(updateCh)
|
||||
}
|
||||
|
||||
// Run watches for resource changes annotated with application label on all registered clusters and schedule corresponding app refresh.
|
||||
func (c *liveStateCache) Run(ctx context.Context) error {
|
||||
cacheSettings, appInstanceLabelKey, err := c.loadCacheSettings()
|
||||
func (c *liveStateCache) Init() error {
|
||||
cacheSettings, err := c.loadCacheSettings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.cacheSettings = *cacheSettings
|
||||
c.appInstanceLabelKey = appInstanceLabelKey
|
||||
|
||||
go c.watchSettings(ctx)
|
||||
|
||||
kube.RetryUntilSucceed(func() error {
|
||||
clusterEventCallback := func(event *db.ClusterEvent) {
|
||||
c.lock.Lock()
|
||||
cluster, ok := c.clusters[event.Cluster.Server]
|
||||
if ok {
|
||||
defer c.lock.Unlock()
|
||||
if event.Type == watch.Deleted {
|
||||
cluster.Invalidate()
|
||||
delete(c.clusters, event.Cluster.Server)
|
||||
} else if event.Type == watch.Modified {
|
||||
cluster.Invalidate(clustercache.SetConfig(event.Cluster.RESTConfig()))
|
||||
}
|
||||
} else {
|
||||
c.lock.Unlock()
|
||||
if event.Type == watch.Added && isClusterHasApps(c.appInformer.GetStore().List(), event.Cluster) {
|
||||
go func() {
|
||||
// warm up cache for cluster with apps
|
||||
_, _ = c.getSyncedCluster(event.Cluster.Server)
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return c.db.WatchClusters(ctx, clusterEventCallback)
|
||||
|
||||
}, "watch clusters", ctx, clustercache.ClusterRetryTimeout)
|
||||
|
||||
<-ctx.Done()
|
||||
c.invalidate(c.cacheSettings, c.appInstanceLabelKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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) error {
|
||||
go c.watchSettings(ctx)
|
||||
|
||||
kube.RetryUntilSucceed(ctx, clustercache.ClusterRetryTimeout, "watch clusters", logutils.NewLogrusLogger(log.New()), func() error {
|
||||
return c.db.WatchClusters(ctx, c.handleAddEvent, c.handleModEvent, c.handleDeleteEvent)
|
||||
})
|
||||
|
||||
<-ctx.Done()
|
||||
c.invalidate(c.cacheSettings)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) canHandleCluster(cluster *appv1.Cluster) bool {
|
||||
if c.clusterFilter == nil {
|
||||
return true
|
||||
}
|
||||
return c.clusterFilter(cluster)
|
||||
}
|
||||
|
||||
func (c *liveStateCache) handleAddEvent(cluster *appv1.Cluster) {
|
||||
if !c.canHandleCluster(cluster) {
|
||||
log.Infof("Ignoring cluster %s", cluster.Server)
|
||||
return
|
||||
}
|
||||
|
||||
c.lock.Lock()
|
||||
_, ok := c.clusters[cluster.Server]
|
||||
c.lock.Unlock()
|
||||
if !ok {
|
||||
if c.isClusterHasApps(c.appInformer.GetStore().List(), cluster) {
|
||||
go func() {
|
||||
// warm up cache for cluster with apps
|
||||
_, _ = c.getSyncedCluster(cluster.Server)
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *liveStateCache) handleModEvent(oldCluster *appv1.Cluster, newCluster *appv1.Cluster) {
|
||||
c.lock.Lock()
|
||||
cluster, ok := c.clusters[newCluster.Server]
|
||||
c.lock.Unlock()
|
||||
if ok {
|
||||
if !c.canHandleCluster(newCluster) {
|
||||
cluster.Invalidate()
|
||||
c.lock.Lock()
|
||||
delete(c.clusters, newCluster.Server)
|
||||
c.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
var updateSettings []clustercache.UpdateSettingsFunc
|
||||
if !reflect.DeepEqual(oldCluster.Config, newCluster.Config) {
|
||||
updateSettings = append(updateSettings, clustercache.SetConfig(newCluster.RESTConfig()))
|
||||
}
|
||||
if !reflect.DeepEqual(oldCluster.Namespaces, newCluster.Namespaces) {
|
||||
updateSettings = append(updateSettings, clustercache.SetNamespaces(newCluster.Namespaces))
|
||||
}
|
||||
forceInvalidate := false
|
||||
if newCluster.RefreshRequestedAt != nil &&
|
||||
cluster.GetClusterInfo().LastCacheSyncTime != nil &&
|
||||
cluster.GetClusterInfo().LastCacheSyncTime.Before(newCluster.RefreshRequestedAt.Time) {
|
||||
forceInvalidate = true
|
||||
}
|
||||
|
||||
if len(updateSettings) > 0 || forceInvalidate {
|
||||
cluster.Invalidate(updateSettings...)
|
||||
go func() {
|
||||
// warm up cluster cache
|
||||
_ = cluster.EnsureSynced()
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (c *liveStateCache) handleDeleteEvent(clusterServer string) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
cluster, ok := c.clusters[clusterServer]
|
||||
if ok {
|
||||
cluster.Invalidate()
|
||||
delete(c.clusters, clusterServer)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *liveStateCache) GetClustersInfo() []clustercache.ClusterInfo {
|
||||
clusters := make(map[string]clustercache.ClusterCache)
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
for k := range c.clusters {
|
||||
clusters[k] = c.clusters[k]
|
||||
}
|
||||
c.lock.RUnlock()
|
||||
|
||||
res := make([]clustercache.ClusterInfo, 0)
|
||||
for _, info := range c.clusters {
|
||||
res = append(res, info.GetClusterInfo())
|
||||
for server, c := range clusters {
|
||||
info := c.GetClusterInfo()
|
||||
info.Server = server
|
||||
res = append(res, info)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
95
controller/cache/cache_test.go
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/cache"
|
||||
"github.com/argoproj/gitops-engine/pkg/cache/mocks"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
func TestHandleModEvent_HasChanges(t *testing.T) {
|
||||
clusterCache := &mocks.ClusterCache{}
|
||||
clusterCache.On("Invalidate", mock.Anything, mock.Anything).Return(nil).Once()
|
||||
clusterCache.On("EnsureSynced").Return(nil).Once()
|
||||
|
||||
clustersCache := liveStateCache{
|
||||
clusters: map[string]cache.ClusterCache{
|
||||
"https://mycluster": clusterCache,
|
||||
},
|
||||
}
|
||||
|
||||
clustersCache.handleModEvent(&appv1.Cluster{
|
||||
Server: "https://mycluster",
|
||||
Config: appv1.ClusterConfig{Username: "foo"},
|
||||
}, &appv1.Cluster{
|
||||
Server: "https://mycluster",
|
||||
Config: appv1.ClusterConfig{Username: "bar"},
|
||||
Namespaces: []string{"default"},
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleModEvent_ClusterExcluded(t *testing.T) {
|
||||
clusterCache := &mocks.ClusterCache{}
|
||||
clusterCache.On("Invalidate", mock.Anything, mock.Anything).Return(nil).Once()
|
||||
clusterCache.On("EnsureSynced").Return(nil).Once()
|
||||
|
||||
clustersCache := liveStateCache{
|
||||
clusters: map[string]cache.ClusterCache{
|
||||
"https://mycluster": clusterCache,
|
||||
},
|
||||
clusterFilter: func(cluster *appv1.Cluster) bool {
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
clustersCache.handleModEvent(&appv1.Cluster{
|
||||
Server: "https://mycluster",
|
||||
Config: appv1.ClusterConfig{Username: "foo"},
|
||||
}, &appv1.Cluster{
|
||||
Server: "https://mycluster",
|
||||
Config: appv1.ClusterConfig{Username: "bar"},
|
||||
Namespaces: []string{"default"},
|
||||
})
|
||||
|
||||
assert.Len(t, clustersCache.clusters, 0)
|
||||
}
|
||||
|
||||
func TestHandleModEvent_NoChanges(t *testing.T) {
|
||||
clusterCache := &mocks.ClusterCache{}
|
||||
clusterCache.On("Invalidate", mock.Anything).Panic("should not invalidate")
|
||||
clusterCache.On("EnsureSynced").Return(nil).Panic("should not re-sync")
|
||||
|
||||
clustersCache := liveStateCache{
|
||||
clusters: map[string]cache.ClusterCache{
|
||||
"https://mycluster": clusterCache,
|
||||
},
|
||||
}
|
||||
|
||||
clustersCache.handleModEvent(&appv1.Cluster{
|
||||
Server: "https://mycluster",
|
||||
Config: appv1.ClusterConfig{Username: "bar"},
|
||||
}, &appv1.Cluster{
|
||||
Server: "https://mycluster",
|
||||
Config: appv1.ClusterConfig{Username: "bar"},
|
||||
})
|
||||
}
|
||||
|
||||
func TestHandleAddEvent_ClusterExcluded(t *testing.T) {
|
||||
clustersCache := liveStateCache{
|
||||
clusters: map[string]cache.ClusterCache{},
|
||||
clusterFilter: func(cluster *appv1.Cluster) bool {
|
||||
return false
|
||||
},
|
||||
}
|
||||
clustersCache.handleAddEvent(&appv1.Cluster{
|
||||
Server: "https://mycluster",
|
||||
Config: appv1.ClusterConfig{Username: "bar"},
|
||||
})
|
||||
|
||||
assert.Len(t, clustersCache.clusters, 0)
|
||||
}
|
||||
126
controller/cache/info.go
vendored
@@ -2,6 +2,7 @@ package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/text"
|
||||
@@ -10,6 +11,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
k8snode "k8s.io/kubernetes/pkg/util/node"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/resource"
|
||||
)
|
||||
@@ -36,6 +38,21 @@ func populateNodeInfo(un *unstructured.Unstructured, res *ResourceInfo) {
|
||||
populateIngressInfo(un, res)
|
||||
return
|
||||
}
|
||||
case "networking.istio.io":
|
||||
switch gvk.Kind {
|
||||
case "VirtualService":
|
||||
populateIstioVirtualServiceInfo(un, res)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range un.GetAnnotations() {
|
||||
if strings.HasPrefix(k, common.AnnotationKeyLinkPrefix) {
|
||||
if res.NetworkingInfo == nil {
|
||||
res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{}
|
||||
}
|
||||
res.NetworkingInfo.ExternalURLs = append(res.NetworkingInfo.ExternalURLs, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,36 +129,82 @@ func populateIngressInfo(un *unstructured.Unstructured, res *ResourceInfo) {
|
||||
}] = 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)
|
||||
stringPort := "http"
|
||||
if tls, ok, err := unstructured.NestedSlice(un.Object, "spec", "tls"); ok && err == nil {
|
||||
for i := range tls {
|
||||
tlsline, ok := tls[i].(map[string]interface{})
|
||||
secretName := tlsline["secretName"]
|
||||
if ok && secretName != nil {
|
||||
stringPort = "https"
|
||||
}
|
||||
tlshost := tlsline["host"]
|
||||
if tlshost == host {
|
||||
stringPort = "https"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
externalURL := fmt.Sprintf("%s://%s", stringPort, host)
|
||||
|
||||
subPath := ""
|
||||
if nestedPath, ok, err := unstructured.NestedString(path, "path"); ok && err == nil {
|
||||
subPath = strings.TrimSuffix(nestedPath, "*")
|
||||
}
|
||||
externalURL += subPath
|
||||
urlsSet[externalURL] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
targets := make([]v1alpha1.ResourceRef, 0)
|
||||
for target := range targetsMap {
|
||||
targets = append(targets, target)
|
||||
}
|
||||
|
||||
var urls []string
|
||||
if res.NetworkingInfo != nil {
|
||||
urls = res.NetworkingInfo.ExternalURLs
|
||||
}
|
||||
for url := range urlsSet {
|
||||
urls = append(urls, url)
|
||||
}
|
||||
res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetRefs: targets, Ingress: ingress, ExternalURLs: urls}
|
||||
}
|
||||
|
||||
func populateIstioVirtualServiceInfo(un *unstructured.Unstructured, res *ResourceInfo) {
|
||||
targetsMap := make(map[v1alpha1.ResourceRef]bool)
|
||||
|
||||
if rules, ok, err := unstructured.NestedSlice(un.Object, "spec", "http"); ok && err == nil {
|
||||
for i := range rules {
|
||||
rule, ok := rules[i].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
routes, ok, err := unstructured.NestedSlice(rule, "route")
|
||||
if !ok || err != nil {
|
||||
continue
|
||||
}
|
||||
for i := range routes {
|
||||
route, ok := routes[i].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if hostName, ok, err := unstructured.NestedString(route, "destination", "host"); ok && err == nil {
|
||||
hostSplits := strings.Split(hostName, ".")
|
||||
serviceName := hostSplits[0]
|
||||
|
||||
var namespace string
|
||||
if len(hostSplits) >= 2 {
|
||||
namespace = hostSplits[1]
|
||||
} else {
|
||||
namespace = un.GetNamespace()
|
||||
}
|
||||
|
||||
var externalURL string
|
||||
switch stringPort {
|
||||
case "80", "http":
|
||||
externalURL = fmt.Sprintf("http://%s", host)
|
||||
case "443", "https":
|
||||
externalURL = fmt.Sprintf("https://%s", host)
|
||||
default:
|
||||
externalURL = fmt.Sprintf("http://%s:%s", host, stringPort)
|
||||
}
|
||||
|
||||
subPath := ""
|
||||
if nestedPath, ok, err := unstructured.NestedString(path, "path"); ok && err == nil {
|
||||
subPath = nestedPath
|
||||
}
|
||||
|
||||
externalURL += subPath
|
||||
urlsSet[externalURL] = true
|
||||
targetsMap[v1alpha1.ResourceRef{
|
||||
Kind: kube.ServiceKind,
|
||||
Name: serviceName,
|
||||
Namespace: namespace,
|
||||
}] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,11 +213,8 @@ func populateIngressInfo(un *unstructured.Unstructured, res *ResourceInfo) {
|
||||
for target := range targetsMap {
|
||||
targets = append(targets, target)
|
||||
}
|
||||
urls := make([]string, 0)
|
||||
for url := range urlsSet {
|
||||
urls = append(urls, url)
|
||||
}
|
||||
res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetRefs: targets, Ingress: ingress, ExternalURLs: urls}
|
||||
|
||||
res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetRefs: targets}
|
||||
}
|
||||
|
||||
func populatePodInfo(un *unstructured.Unstructured, res *ResourceInfo) {
|
||||
|
||||
168
controller/cache/info_test.go
vendored
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/argoproj/pkg/errors"
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
@@ -63,10 +64,96 @@ var (
|
||||
serviceName: helm-guestbook
|
||||
servicePort: https
|
||||
path: /
|
||||
tls:
|
||||
- host: helm-guestbook.com
|
||||
secretName: my-tls-secret
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
- ip: 107.178.210.11`)
|
||||
|
||||
testIngressWildCardPath = strToUnstructured(`
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
uid: "4"
|
||||
spec:
|
||||
backend:
|
||||
serviceName: not-found-service
|
||||
servicePort: 443
|
||||
rules:
|
||||
- host: helm-guestbook.com
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: helm-guestbook
|
||||
servicePort: 443
|
||||
path: /*
|
||||
- backend:
|
||||
serviceName: helm-guestbook
|
||||
servicePort: https
|
||||
path: /*
|
||||
tls:
|
||||
- host: helm-guestbook.com
|
||||
secretName: my-tls-secret
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
- ip: 107.178.210.11`)
|
||||
|
||||
testIngressWithoutTls = strToUnstructured(`
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
uid: "4"
|
||||
spec:
|
||||
backend:
|
||||
serviceName: not-found-service
|
||||
servicePort: 443
|
||||
rules:
|
||||
- host: helm-guestbook.com
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: helm-guestbook
|
||||
servicePort: 443
|
||||
path: /
|
||||
- backend:
|
||||
serviceName: helm-guestbook
|
||||
servicePort: https
|
||||
path: /
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
- ip: 107.178.210.11`)
|
||||
|
||||
testIstioVirtualService = strToUnstructured(`
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: VirtualService
|
||||
metadata:
|
||||
name: hello-world
|
||||
namespace: demo
|
||||
spec:
|
||||
http:
|
||||
- match:
|
||||
- uri:
|
||||
prefix: "/1"
|
||||
route:
|
||||
- destination:
|
||||
host: service_full.demo.svc.cluster.local
|
||||
- destination:
|
||||
host: service_namespace.namespace
|
||||
- match:
|
||||
- uri:
|
||||
prefix: "/2"
|
||||
route:
|
||||
- destination:
|
||||
host: service
|
||||
`)
|
||||
)
|
||||
|
||||
func TestGetPodInfo(t *testing.T) {
|
||||
@@ -104,6 +191,29 @@ func TestGetServiceInfo(t *testing.T) {
|
||||
}, info.NetworkingInfo)
|
||||
}
|
||||
|
||||
func TestGetIstioVirtualServiceInfo(t *testing.T) {
|
||||
info := &ResourceInfo{}
|
||||
populateNodeInfo(testIstioVirtualService, info)
|
||||
assert.Equal(t, 0, len(info.Info))
|
||||
require.NotNil(t, info.NetworkingInfo)
|
||||
require.NotNil(t, info.NetworkingInfo.TargetRefs)
|
||||
assert.Contains(t, info.NetworkingInfo.TargetRefs, v1alpha1.ResourceRef{
|
||||
Kind: kube.ServiceKind,
|
||||
Name: "service_full",
|
||||
Namespace: "demo",
|
||||
})
|
||||
assert.Contains(t, info.NetworkingInfo.TargetRefs, v1alpha1.ResourceRef{
|
||||
Kind: kube.ServiceKind,
|
||||
Name: "service_namespace",
|
||||
Namespace: "namespace",
|
||||
})
|
||||
assert.Contains(t, info.NetworkingInfo.TargetRefs, v1alpha1.ResourceRef{
|
||||
Kind: kube.ServiceKind,
|
||||
Name: "service",
|
||||
Namespace: "demo",
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetIngressInfo(t *testing.T) {
|
||||
info := &ResourceInfo{}
|
||||
populateNodeInfo(testIngress, info)
|
||||
@@ -128,6 +238,54 @@ func TestGetIngressInfo(t *testing.T) {
|
||||
}, info.NetworkingInfo)
|
||||
}
|
||||
|
||||
func TestGetIngressInfoWildCardPath(t *testing.T) {
|
||||
info := &ResourceInfo{}
|
||||
populateNodeInfo(testIngressWildCardPath, info)
|
||||
assert.Equal(t, 0, len(info.Info))
|
||||
sort.Slice(info.NetworkingInfo.TargetRefs, func(i, j int) bool {
|
||||
return strings.Compare(info.NetworkingInfo.TargetRefs[j].Name, info.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/"},
|
||||
}, info.NetworkingInfo)
|
||||
}
|
||||
|
||||
func TestGetIngressInfoWithoutTls(t *testing.T) {
|
||||
info := &ResourceInfo{}
|
||||
populateNodeInfo(testIngressWithoutTls, info)
|
||||
assert.Equal(t, 0, len(info.Info))
|
||||
sort.Slice(info.NetworkingInfo.TargetRefs, func(i, j int) bool {
|
||||
return strings.Compare(info.NetworkingInfo.TargetRefs[j].Name, info.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{"http://helm-guestbook.com/"},
|
||||
}, info.NetworkingInfo)
|
||||
}
|
||||
|
||||
func TestGetIngressInfoNoHost(t *testing.T) {
|
||||
ingress := strToUnstructured(`
|
||||
apiVersion: extensions/v1beta1
|
||||
@@ -143,6 +301,8 @@ func TestGetIngressInfoNoHost(t *testing.T) {
|
||||
serviceName: helm-guestbook
|
||||
servicePort: 443
|
||||
path: /
|
||||
tls:
|
||||
- secretName: my-tls
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
@@ -177,6 +337,8 @@ func TestExternalUrlWithSubPath(t *testing.T) {
|
||||
serviceName: helm-guestbook
|
||||
servicePort: 443
|
||||
path: /my/sub/path/
|
||||
tls:
|
||||
- secretName: my-tls
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
@@ -211,6 +373,8 @@ func TestExternalUrlWithMultipleSubPaths(t *testing.T) {
|
||||
- backend:
|
||||
serviceName: helm-guestbook-3
|
||||
servicePort: 443
|
||||
tls:
|
||||
- secretName: my-tls
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
@@ -239,6 +403,8 @@ func TestExternalUrlWithNoSubPath(t *testing.T) {
|
||||
- backend:
|
||||
serviceName: helm-guestbook
|
||||
servicePort: 443
|
||||
tls:
|
||||
- secretName: my-tls
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
@@ -265,6 +431,8 @@ func TestExternalUrlWithNetworkingApi(t *testing.T) {
|
||||
- backend:
|
||||
serviceName: helm-guestbook
|
||||
servicePort: 443
|
||||
tls:
|
||||
- secretName: my-tls
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
|
||||
14
controller/cache/mocks/LiveStateCache.go
vendored
@@ -140,6 +140,20 @@ func (_m *LiveStateCache) GetVersionsInfo(serverURL string) (string, []v1.APIGro
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// Init provides a mock function with given fields:
|
||||
func (_m *LiveStateCache) Init() error {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// IsNamespaced provides a mock function with given fields: server, gk
|
||||
func (_m *LiveStateCache) IsNamespaced(server string, gk schema.GroupKind) (bool, error) {
|
||||
ret := _m.Called(server, gk)
|
||||
|
||||
129
controller/clusterinfoupdater.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/cache"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
log "github.com/sirupsen/logrus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
|
||||
"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"
|
||||
appstatecache "github.com/argoproj/argo-cd/util/cache/appstate"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
)
|
||||
|
||||
const (
|
||||
secretUpdateInterval = 10 * time.Second
|
||||
)
|
||||
|
||||
type clusterInfoUpdater struct {
|
||||
infoSource metrics.HasClustersInfo
|
||||
db db.ArgoDB
|
||||
appLister v1alpha1.ApplicationNamespaceLister
|
||||
cache *appstatecache.Cache
|
||||
clusterFilter func(cluster *appv1.Cluster) bool
|
||||
}
|
||||
|
||||
func NewClusterInfoUpdater(
|
||||
infoSource metrics.HasClustersInfo,
|
||||
db db.ArgoDB,
|
||||
appLister v1alpha1.ApplicationNamespaceLister,
|
||||
cache *appstatecache.Cache,
|
||||
clusterFilter func(cluster *appv1.Cluster) bool) *clusterInfoUpdater {
|
||||
|
||||
return &clusterInfoUpdater{infoSource, db, appLister, cache, clusterFilter}
|
||||
}
|
||||
|
||||
func (c *clusterInfoUpdater) Run(ctx context.Context) {
|
||||
c.updateClusters()
|
||||
ticker := time.NewTicker(secretUpdateInterval)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
ticker.Stop()
|
||||
break
|
||||
case <-ticker.C:
|
||||
c.updateClusters()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *clusterInfoUpdater) updateClusters() {
|
||||
infoByServer := make(map[string]*cache.ClusterInfo)
|
||||
clustersInfo := c.infoSource.GetClustersInfo()
|
||||
for i := range clustersInfo {
|
||||
info := clustersInfo[i]
|
||||
infoByServer[info.Server] = &info
|
||||
}
|
||||
clusters, err := c.db.ListClusters(context.Background())
|
||||
if err != nil {
|
||||
log.Warnf("Failed to save clusters info: %v", err)
|
||||
}
|
||||
var clustersFiltered []appv1.Cluster
|
||||
if c.clusterFilter == nil {
|
||||
clustersFiltered = clusters.Items
|
||||
} else {
|
||||
for i := range clusters.Items {
|
||||
if c.clusterFilter(&clusters.Items[i]) {
|
||||
clustersFiltered = append(clustersFiltered, clusters.Items[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = kube.RunAllAsync(len(clustersFiltered), func(i int) error {
|
||||
cluster := clustersFiltered[i]
|
||||
if err := c.updateClusterInfo(cluster, infoByServer[cluster.Server]); err != nil {
|
||||
log.Warnf("Failed to save clusters info: %v", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
log.Debugf("Successfully saved info of %d clusters", len(clustersFiltered))
|
||||
}
|
||||
|
||||
func (c *clusterInfoUpdater) updateClusterInfo(cluster appv1.Cluster, info *cache.ClusterInfo) error {
|
||||
apps, err := c.appLister.List(labels.Everything())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var appCount int64
|
||||
for _, a := range apps {
|
||||
if err := argo.ValidateDestination(context.Background(), &a.Spec.Destination, c.db); err != nil {
|
||||
continue
|
||||
}
|
||||
if a.Spec.Destination.Server == cluster.Server {
|
||||
appCount += 1
|
||||
}
|
||||
}
|
||||
now := metav1.Now()
|
||||
clusterInfo := appv1.ClusterInfo{
|
||||
ConnectionState: appv1.ConnectionState{ModifiedAt: &now},
|
||||
ApplicationsCount: appCount,
|
||||
}
|
||||
if info != nil {
|
||||
clusterInfo.ServerVersion = info.K8SVersion
|
||||
if info.LastCacheSyncTime == nil {
|
||||
clusterInfo.ConnectionState.Status = appv1.ConnectionStatusUnknown
|
||||
} else if info.SyncError == nil {
|
||||
clusterInfo.ConnectionState.Status = appv1.ConnectionStatusSuccessful
|
||||
syncTime := metav1.NewTime(*info.LastCacheSyncTime)
|
||||
clusterInfo.CacheInfo.LastCacheSyncTime = &syncTime
|
||||
clusterInfo.CacheInfo.APIsCount = int64(info.APIsCount)
|
||||
clusterInfo.CacheInfo.ResourcesCount = int64(info.ResourcesCount)
|
||||
} else {
|
||||
clusterInfo.ConnectionState.Status = appv1.ConnectionStatusFailed
|
||||
clusterInfo.ConnectionState.Message = info.SyncError.Error()
|
||||
}
|
||||
} else {
|
||||
clusterInfo.ConnectionState.Status = appv1.ConnectionStatusUnknown
|
||||
if appCount == 0 {
|
||||
clusterInfo.ConnectionState.Message = "Cluster has no application and not being monitored."
|
||||
}
|
||||
}
|
||||
|
||||
return c.cache.SetClusterInfo(cluster.Server, &clusterInfo)
|
||||
}
|
||||
72
controller/clusterinfoupdater_test.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appsfake "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/fake"
|
||||
appinformers "github.com/argoproj/argo-cd/pkg/client/informers/externalversions/application/v1alpha1"
|
||||
applisters "github.com/argoproj/argo-cd/pkg/client/listers/application/v1alpha1"
|
||||
cacheutil "github.com/argoproj/argo-cd/util/cache"
|
||||
"github.com/argoproj/argo-cd/util/cache/appstate"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
|
||||
clustercache "github.com/argoproj/gitops-engine/pkg/cache"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
// Expect cluster cache update is persisted in cluster secret
|
||||
func TestClusterSecretUpdater(t *testing.T) {
|
||||
const fakeNamespace = "fake-ns"
|
||||
const updatedK8sVersion = "1.0"
|
||||
now := time.Now()
|
||||
|
||||
var tests = []struct {
|
||||
LastCacheSyncTime *time.Time
|
||||
SyncError error
|
||||
ExpectedStatus v1alpha1.ConnectionStatus
|
||||
}{
|
||||
{nil, nil, v1alpha1.ConnectionStatusUnknown},
|
||||
{&now, nil, v1alpha1.ConnectionStatusSuccessful},
|
||||
{&now, fmt.Errorf("sync failed"), v1alpha1.ConnectionStatusFailed},
|
||||
}
|
||||
|
||||
kubeclientset := fake.NewSimpleClientset()
|
||||
appclientset := appsfake.NewSimpleClientset()
|
||||
appInformer := appinformers.NewApplicationInformer(appclientset, "", time.Minute, cache.Indexers{})
|
||||
settingsManager := settings.NewSettingsManager(context.Background(), kubeclientset, fakeNamespace)
|
||||
argoDB := db.NewDB(fakeNamespace, settingsManager, kubeclientset)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
appCache := appstate.NewCache(cacheutil.NewCache(cacheutil.NewInMemoryCache(time.Minute)), time.Minute)
|
||||
cluster, err := argoDB.CreateCluster(ctx, &v1alpha1.Cluster{Server: "http://minikube"})
|
||||
assert.NoError(t, err, "Test prepare test data create cluster failed")
|
||||
|
||||
for _, test := range tests {
|
||||
info := &clustercache.ClusterInfo{
|
||||
Server: cluster.Server,
|
||||
K8SVersion: updatedK8sVersion,
|
||||
LastCacheSyncTime: test.LastCacheSyncTime,
|
||||
SyncError: test.SyncError,
|
||||
}
|
||||
|
||||
lister := applisters.NewApplicationLister(appInformer.GetIndexer()).Applications(fakeNamespace)
|
||||
updater := NewClusterInfoUpdater(nil, argoDB, lister, appCache, nil)
|
||||
|
||||
err = updater.updateClusterInfo(*cluster, info)
|
||||
assert.NoError(t, err, "Invoking updateClusterInfo failed.")
|
||||
|
||||
var clusterInfo v1alpha1.ClusterInfo
|
||||
err = appCache.GetClusterInfo(cluster.Server, &clusterInfo)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, updatedK8sVersion, clusterInfo.ServerVersion)
|
||||
assert.Equal(t, test.ExpectedStatus, clusterInfo.ConnectionState.Status)
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ type MetricsServer struct {
|
||||
reconcileHistogram *prometheus.HistogramVec
|
||||
redisRequestHistogram *prometheus.HistogramVec
|
||||
registry *prometheus.Registry
|
||||
hostname string
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -91,12 +92,12 @@ var (
|
||||
kubectlExecCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "argocd_kubectl_exec_total",
|
||||
Help: "Number of kubectl executions",
|
||||
}, []string{"command"})
|
||||
}, []string{"hostname", "command"})
|
||||
|
||||
kubectlExecPendingGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "argocd_kubectl_exec_pending",
|
||||
Help: "Number of pending kubectl executions",
|
||||
}, []string{"command"})
|
||||
}, []string{"hostname", "command"})
|
||||
|
||||
reconcileHistogram = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
@@ -118,7 +119,7 @@ var (
|
||||
Name: "argocd_redis_request_total",
|
||||
Help: "Number of kubernetes requests executed during application reconciliation.",
|
||||
},
|
||||
[]string{"initiator", "failed"},
|
||||
[]string{"hostname", "initiator", "failed"},
|
||||
)
|
||||
|
||||
redisRequestHistogram = prometheus.NewHistogramVec(
|
||||
@@ -127,14 +128,18 @@ var (
|
||||
Help: "Redis requests duration.",
|
||||
Buckets: []float64{0.01, 0.05, 0.10, 0.25, .5, 1},
|
||||
},
|
||||
[]string{"initiator"},
|
||||
[]string{"hostname", "initiator"},
|
||||
)
|
||||
)
|
||||
|
||||
// NewMetricsServer returns a new prometheus server which collects application metrics
|
||||
func NewMetricsServer(addr string, appLister applister.ApplicationLister, healthCheck func() error) *MetricsServer {
|
||||
func NewMetricsServer(addr string, appLister applister.ApplicationLister, appFilter func(obj interface{}) bool, healthCheck func(r *http.Request) error) (*MetricsServer, error) {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
registry := NewAppRegistry(appLister)
|
||||
registry := NewAppRegistry(appLister, appFilter)
|
||||
mux.Handle(MetricsPath, promhttp.HandlerFor(prometheus.Gatherers{
|
||||
// contains app controller specific metrics
|
||||
registry,
|
||||
@@ -166,7 +171,8 @@ func NewMetricsServer(addr string, appLister applister.ApplicationLister, health
|
||||
clusterEventsCounter: clusterEventsCounter,
|
||||
redisRequestCounter: redisRequestCounter,
|
||||
redisRequestHistogram: redisRequestHistogram,
|
||||
}
|
||||
hostname: hostname,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MetricsServer) RegisterClustersInfoSource(ctx context.Context, source HasClustersInfo) {
|
||||
@@ -184,15 +190,15 @@ func (m *MetricsServer) IncSync(app *argoappv1.Application, state *argoappv1.Ope
|
||||
}
|
||||
|
||||
func (m *MetricsServer) IncKubectlExec(command string) {
|
||||
m.kubectlExecCounter.WithLabelValues(command).Inc()
|
||||
m.kubectlExecCounter.WithLabelValues(m.hostname, command).Inc()
|
||||
}
|
||||
|
||||
func (m *MetricsServer) IncKubectlExecPending(command string) {
|
||||
m.kubectlExecPendingGauge.WithLabelValues(command).Inc()
|
||||
m.kubectlExecPendingGauge.WithLabelValues(m.hostname, command).Inc()
|
||||
}
|
||||
|
||||
func (m *MetricsServer) DecKubectlExecPending(command string) {
|
||||
m.kubectlExecPendingGauge.WithLabelValues(command).Dec()
|
||||
m.kubectlExecPendingGauge.WithLabelValues(m.hostname, command).Dec()
|
||||
}
|
||||
|
||||
// IncClusterEventsCount increments the number of cluster events
|
||||
@@ -215,12 +221,12 @@ func (m *MetricsServer) IncKubernetesRequest(app *argoappv1.Application, server,
|
||||
}
|
||||
|
||||
func (m *MetricsServer) IncRedisRequest(failed bool) {
|
||||
m.redisRequestCounter.WithLabelValues("argocd-application-controller", strconv.FormatBool(failed)).Inc()
|
||||
m.redisRequestCounter.WithLabelValues(m.hostname, "argocd-application-controller", strconv.FormatBool(failed)).Inc()
|
||||
}
|
||||
|
||||
// ObserveRedisRequestDuration observes redis request duration
|
||||
func (m *MetricsServer) ObserveRedisRequestDuration(duration time.Duration) {
|
||||
m.redisRequestHistogram.WithLabelValues("argocd-application-controller").Observe(duration.Seconds())
|
||||
m.redisRequestHistogram.WithLabelValues(m.hostname, "argocd-application-controller").Observe(duration.Seconds())
|
||||
}
|
||||
|
||||
// IncReconcile increments the reconcile counter for an application
|
||||
@@ -229,20 +235,22 @@ func (m *MetricsServer) IncReconcile(app *argoappv1.Application, duration time.D
|
||||
}
|
||||
|
||||
type appCollector struct {
|
||||
store applister.ApplicationLister
|
||||
store applister.ApplicationLister
|
||||
appFilter func(obj interface{}) bool
|
||||
}
|
||||
|
||||
// NewAppCollector returns a prometheus collector for application metrics
|
||||
func NewAppCollector(appLister applister.ApplicationLister) prometheus.Collector {
|
||||
func NewAppCollector(appLister applister.ApplicationLister, appFilter func(obj interface{}) bool) prometheus.Collector {
|
||||
return &appCollector{
|
||||
store: appLister,
|
||||
store: appLister,
|
||||
appFilter: appFilter,
|
||||
}
|
||||
}
|
||||
|
||||
// NewAppRegistry creates a new prometheus registry that collects applications
|
||||
func NewAppRegistry(appLister applister.ApplicationLister) *prometheus.Registry {
|
||||
func NewAppRegistry(appLister applister.ApplicationLister, appFilter func(obj interface{}) bool) *prometheus.Registry {
|
||||
registry := prometheus.NewRegistry()
|
||||
registry.MustRegister(NewAppCollector(appLister))
|
||||
registry.MustRegister(NewAppCollector(appLister, appFilter))
|
||||
return registry
|
||||
}
|
||||
|
||||
@@ -261,7 +269,9 @@ func (c *appCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
return
|
||||
}
|
||||
for _, app := range apps {
|
||||
collectApps(ch, app)
|
||||
if c.appFilter(app) {
|
||||
collectApps(ch, app)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -112,10 +112,14 @@ status:
|
||||
status: Healthy
|
||||
`
|
||||
|
||||
var noOpHealthCheck = func() error {
|
||||
var noOpHealthCheck = func(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var appFilter = func(obj interface{}) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func newFakeApp(fakeAppYAML string) *argoappv1.Application {
|
||||
var app argoappv1.Application
|
||||
err := yaml.Unmarshal([]byte(fakeAppYAML), &app)
|
||||
@@ -146,7 +150,8 @@ func newFakeLister(fakeAppYAMLs ...string) (context.CancelFunc, applister.Applic
|
||||
func testApp(t *testing.T, fakeAppYAMLs []string, expectedResponse string) {
|
||||
cancel, appLister := newFakeLister(fakeAppYAMLs...)
|
||||
defer cancel()
|
||||
metricsServ := NewMetricsServer("localhost:8082", appLister, noOpHealthCheck)
|
||||
metricsServ, err := NewMetricsServer("localhost:8082", appLister, appFilter, noOpHealthCheck)
|
||||
assert.NoError(t, err)
|
||||
req, err := http.NewRequest("GET", "/metrics", nil)
|
||||
assert.NoError(t, err)
|
||||
rr := httptest.NewRecorder()
|
||||
@@ -217,7 +222,8 @@ argocd_app_sync_status{name="my-app",namespace="argocd",project="important-proje
|
||||
func TestMetricsSyncCounter(t *testing.T) {
|
||||
cancel, appLister := newFakeLister()
|
||||
defer cancel()
|
||||
metricsServ := NewMetricsServer("localhost:8082", appLister, noOpHealthCheck)
|
||||
metricsServ, err := NewMetricsServer("localhost:8082", appLister, appFilter, noOpHealthCheck)
|
||||
assert.NoError(t, err)
|
||||
|
||||
appSyncTotal := `
|
||||
# HELP argocd_app_sync_total Number of application syncs.
|
||||
@@ -257,7 +263,9 @@ func assertMetricsPrinted(t *testing.T, expectedLines, body string) {
|
||||
func TestReconcileMetrics(t *testing.T) {
|
||||
cancel, appLister := newFakeLister()
|
||||
defer cancel()
|
||||
metricsServ := NewMetricsServer("localhost:8082", appLister, noOpHealthCheck)
|
||||
metricsServ, err := NewMetricsServer("localhost:8082", appLister, appFilter, noOpHealthCheck)
|
||||
assert.NoError(t, err)
|
||||
|
||||
appReconcileMetrics := `
|
||||
# HELP argocd_app_reconcile Application reconciliation performance.
|
||||
# TYPE argocd_app_reconcile histogram
|
||||
|
||||
53
controller/sharding/sharding.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package sharding
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
func InferShard() (int, error) {
|
||||
hostname, err := os.Hostname()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
parts := strings.Split(hostname, "-")
|
||||
if len(parts) == 0 {
|
||||
return 0, fmt.Errorf("hostname should ends with shard number separated by '-' but got: %s", hostname)
|
||||
}
|
||||
shard, err := strconv.Atoi(parts[len(parts)-1])
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("hostname should ends with shard number separated by '-' but got: %s", hostname)
|
||||
}
|
||||
return shard, nil
|
||||
}
|
||||
|
||||
// getShardByID calculates cluster shard as `clusterSecret.UID % replicas count`
|
||||
func getShardByID(id string, replicas int) int {
|
||||
if id == "" {
|
||||
return 0
|
||||
} else {
|
||||
h := fnv.New32a()
|
||||
_, _ = h.Write([]byte(id))
|
||||
return int(h.Sum32() % uint32(replicas))
|
||||
}
|
||||
}
|
||||
|
||||
func GetClusterFilter(replicas int, shard int) func(c *v1alpha1.Cluster) bool {
|
||||
return func(c *v1alpha1.Cluster) bool {
|
||||
clusterShard := 0
|
||||
// cluster might be nil if app is using invalid cluster URL, assume shard 0 in this case.
|
||||
if c != nil {
|
||||
if c.Shard != nil {
|
||||
clusterShard = int(*c.Shard)
|
||||
} else {
|
||||
clusterShard = getShardByID(c.ID, replicas)
|
||||
}
|
||||
}
|
||||
return clusterShard == shard
|
||||
}
|
||||
}
|
||||
29
controller/sharding/sharding_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package sharding
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetShardByID_NotEmptyID(t *testing.T) {
|
||||
assert.Equal(t, 0, getShardByID("1", 2))
|
||||
assert.Equal(t, 1, getShardByID("2", 2))
|
||||
assert.Equal(t, 0, getShardByID("3", 2))
|
||||
assert.Equal(t, 1, getShardByID("4", 2))
|
||||
}
|
||||
|
||||
func TestGetShardByID_EmptyID(t *testing.T) {
|
||||
shard := getShardByID("", 10)
|
||||
assert.Equal(t, 0, shard)
|
||||
}
|
||||
|
||||
func TestGetClusterFilter(t *testing.T) {
|
||||
filter := GetClusterFilter(2, 1)
|
||||
assert.False(t, filter(&v1alpha1.Cluster{ID: "1"}))
|
||||
assert.True(t, filter(&v1alpha1.Cluster{ID: "2"}))
|
||||
assert.False(t, filter(&v1alpha1.Cluster{ID: "3"}))
|
||||
assert.True(t, filter(&v1alpha1.Cluster{ID: "4"}))
|
||||
}
|
||||
40
controller/sort_delete.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/sync/syncwaves"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
type syncWaveSorter []*unstructured.Unstructured
|
||||
|
||||
func (s syncWaveSorter) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func (s syncWaveSorter) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
func (s syncWaveSorter) Less(i, j int) bool {
|
||||
return syncwaves.Wave(s[i]) < syncwaves.Wave(s[j])
|
||||
}
|
||||
|
||||
func FilterObjectsForDeletion(objs []*unstructured.Unstructured) []*unstructured.Unstructured {
|
||||
if len(objs) <= 1 {
|
||||
return objs
|
||||
}
|
||||
|
||||
sort.Sort(sort.Reverse(syncWaveSorter(objs)))
|
||||
|
||||
currentSyncWave := syncwaves.Wave(objs[0])
|
||||
filteredObjs := make([]*unstructured.Unstructured, 0)
|
||||
for _, obj := range objs {
|
||||
if syncwaves.Wave(obj) != currentSyncWave {
|
||||
break
|
||||
}
|
||||
filteredObjs = append(filteredObjs, obj)
|
||||
}
|
||||
return filteredObjs
|
||||
}
|
||||
41
controller/sort_delete_test.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/sync/common"
|
||||
. "github.com/argoproj/gitops-engine/pkg/utils/testing"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
func TestFilterObjectsForDeletion(t *testing.T) {
|
||||
tests := []struct {
|
||||
input []string
|
||||
want []string
|
||||
}{
|
||||
{[]string{"1", "5", "7", "7", "4"}, []string{"7", "7"}},
|
||||
{[]string{"1", "5", "2", "2", "4"}, []string{"5"}},
|
||||
{[]string{"1"}, []string{"1"}},
|
||||
{[]string{}, []string{}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
in := sliceOfObjectsWithSyncWaves(tt.input)
|
||||
need := sliceOfObjectsWithSyncWaves(tt.want)
|
||||
if got := FilterObjectsForDeletion(in); !reflect.DeepEqual(got, need) {
|
||||
t.Errorf("Received unexpected objects for deletion = %v, want %v", got, need)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func podWithSyncWave(wave string) *unstructured.Unstructured {
|
||||
return Annotate(NewPod(), common.AnnotationSyncWave, wave)
|
||||
}
|
||||
|
||||
func sliceOfObjectsWithSyncWaves(waves []string) []*unstructured.Unstructured {
|
||||
objects := make([]*unstructured.Unstructured, 0)
|
||||
for _, wave := range waves {
|
||||
objects = append(objects, podWithSyncWave(wave))
|
||||
}
|
||||
return objects
|
||||
}
|
||||
@@ -12,10 +12,8 @@ import (
|
||||
hookutil "github.com/argoproj/gitops-engine/pkg/sync/hook"
|
||||
"github.com/argoproj/gitops-engine/pkg/sync/ignore"
|
||||
resourceutil "github.com/argoproj/gitops-engine/pkg/sync/resource"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/io"
|
||||
kubeutil "github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/yudai/gojsondiff"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
@@ -31,7 +29,9 @@ import (
|
||||
"github.com/argoproj/argo-cd/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/util/argo"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/gpg"
|
||||
argohealth "github.com/argoproj/argo-cd/util/health"
|
||||
"github.com/argoproj/argo-cd/util/io"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
)
|
||||
@@ -55,12 +55,18 @@ type managedResource struct {
|
||||
Hook bool
|
||||
}
|
||||
|
||||
func GetLiveObjs(res []managedResource) []*unstructured.Unstructured {
|
||||
objs := make([]*unstructured.Unstructured, len(res))
|
||||
for i := range res {
|
||||
objs[i] = res[i].Live
|
||||
func GetLiveObjsForApplicationHealth(resources []managedResource, statuses []appv1.ResourceStatus) ([]*appv1.ResourceStatus, []*unstructured.Unstructured) {
|
||||
liveObjs := make([]*unstructured.Unstructured, 0)
|
||||
resStatuses := make([]*appv1.ResourceStatus, 0)
|
||||
for i, resource := range resources {
|
||||
if resource.Target != nil && hookutil.Skip(resource.Target) {
|
||||
continue
|
||||
}
|
||||
|
||||
liveObjs = append(liveObjs, resource.Live)
|
||||
resStatuses = append(resStatuses, &statuses[i])
|
||||
}
|
||||
return objs
|
||||
return resStatuses, liveObjs
|
||||
}
|
||||
|
||||
// AppStateManager defines methods which allow to compare application spec and actual application state.
|
||||
@@ -81,6 +87,14 @@ type comparisonResult struct {
|
||||
timings map[string]time.Duration
|
||||
}
|
||||
|
||||
func (res *comparisonResult) GetSyncStatus() *v1alpha1.SyncStatus {
|
||||
return res.syncStatus
|
||||
}
|
||||
|
||||
func (res *comparisonResult) GetHealthStatus() *v1alpha1.HealthStatus {
|
||||
return res.healthStatus
|
||||
}
|
||||
|
||||
// appStateManager allows to compare applications to git
|
||||
type appStateManager struct {
|
||||
metricsServer *metrics.MetricsServer
|
||||
@@ -94,7 +108,7 @@ type appStateManager struct {
|
||||
namespace string
|
||||
}
|
||||
|
||||
func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1.ApplicationSource, appLabelKey, revision string, noCache bool) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error) {
|
||||
func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1.ApplicationSource, appLabelKey, revision string, noCache, verifySignature bool) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error) {
|
||||
ts := stats.NewTimingStats()
|
||||
helmRepos, err := m.db.ListHelmRepositories(context.Background())
|
||||
if err != nil {
|
||||
@@ -153,6 +167,7 @@ func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1
|
||||
KustomizeOptions: kustomizeOptions,
|
||||
KubeVersion: serverVersion,
|
||||
ApiVersions: argo.APIGroupsToVersions(apiGroups),
|
||||
VerifySignature: verifySignature,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -194,6 +209,9 @@ func DeduplicateTargetObjects(
|
||||
targetByKey := make(map[kubeutil.ResourceKey][]*unstructured.Unstructured)
|
||||
for i := range objs {
|
||||
obj := objs[i]
|
||||
if obj == nil {
|
||||
continue
|
||||
}
|
||||
isNamespaced := kubeutil.IsNamespacedOrUnknown(infoProvider, obj.GroupVersionKind().GroupKind())
|
||||
if !isNamespaced {
|
||||
obj.SetNamespace("")
|
||||
@@ -201,6 +219,9 @@ func DeduplicateTargetObjects(
|
||||
obj.SetNamespace(namespace)
|
||||
}
|
||||
key := kubeutil.GetResourceKey(obj)
|
||||
if key.Name == "" && obj.GetGenerateName() != "" {
|
||||
key.Name = fmt.Sprintf("%s%d", obj.GetGenerateName(), i)
|
||||
}
|
||||
targetByKey[key] = append(targetByKey[key], obj)
|
||||
}
|
||||
conditions := make([]v1alpha1.ApplicationCondition, 0)
|
||||
@@ -240,6 +261,50 @@ func (m *appStateManager) getComparisonSettings(app *appv1.Application) (string,
|
||||
return appLabelKey, resourceOverrides, diffNormalizer, resFilter, nil
|
||||
}
|
||||
|
||||
// verifyGnuPGSignature verifies the result of a GnuPG operation for a given git
|
||||
// revision.
|
||||
func verifyGnuPGSignature(revision string, project *appv1.AppProject, manifestInfo *apiclient.ManifestResponse) []appv1.ApplicationCondition {
|
||||
now := metav1.Now()
|
||||
conditions := make([]appv1.ApplicationCondition, 0)
|
||||
// We need to have some data in the verification result to parse, otherwise there was no signature
|
||||
if manifestInfo.VerifyResult != "" {
|
||||
verifyResult, err := gpg.ParseGitCommitVerification(manifestInfo.VerifyResult)
|
||||
if err != nil {
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
|
||||
log.Errorf("Error while verifying git commit for revision %s: %s", revision, err.Error())
|
||||
} else {
|
||||
switch verifyResult.Result {
|
||||
case gpg.VerifyResultGood:
|
||||
// This is the only case we allow to sync to, but we need to make sure signing key is allowed
|
||||
validKey := false
|
||||
for _, k := range project.Spec.SignatureKeys {
|
||||
if gpg.KeyID(k.KeyID) == gpg.KeyID(verifyResult.KeyID) && gpg.KeyID(k.KeyID) != "" {
|
||||
validKey = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !validKey {
|
||||
msg := fmt.Sprintf("Found good signature made with %s key %s, but this key is not allowed in AppProject",
|
||||
verifyResult.Cipher, verifyResult.KeyID)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: msg, LastTransitionTime: &now})
|
||||
}
|
||||
case gpg.VerifyResultInvalid:
|
||||
msg := fmt.Sprintf("Found signature made with %s key %s, but verification result was invalid: '%s'",
|
||||
verifyResult.Cipher, verifyResult.KeyID, verifyResult.Message)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: msg, LastTransitionTime: &now})
|
||||
default:
|
||||
msg := fmt.Sprintf("Could not verify commit signature on revision '%s', check logs for more information.", revision)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: msg, LastTransitionTime: &now})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
msg := fmt.Sprintf("Target revision %s in Git is not signed, but a signature is required", revision)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: msg, LastTransitionTime: &now})
|
||||
}
|
||||
|
||||
return conditions
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -259,6 +324,12 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
|
||||
}
|
||||
}
|
||||
|
||||
// When signature keys are defined in the project spec, we need to verify the signature on the Git revision
|
||||
verifySignature := false
|
||||
if project.Spec.SignatureKeys != nil && len(project.Spec.SignatureKeys) > 0 && gpg.IsGPGEnabled() {
|
||||
verifySignature = true
|
||||
}
|
||||
|
||||
// do best effort loading live and target state to present as much information about app state as possible
|
||||
failedToLoadObjs := false
|
||||
conditions := make([]v1alpha1.ApplicationCondition, 0)
|
||||
@@ -271,18 +342,27 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
|
||||
now := metav1.Now()
|
||||
|
||||
if len(localManifests) == 0 {
|
||||
targetObjs, manifestInfo, err = m.getRepoObjs(app, source, appLabelKey, revision, noCache)
|
||||
targetObjs, manifestInfo, err = m.getRepoObjs(app, source, appLabelKey, revision, noCache, verifySignature)
|
||||
if err != nil {
|
||||
targetObjs = make([]*unstructured.Unstructured, 0)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
|
||||
failedToLoadObjs = true
|
||||
}
|
||||
} else {
|
||||
targetObjs, err = unmarshalManifests(localManifests)
|
||||
if err != nil {
|
||||
// Prevent applying local manifests for now when signature verification is enabled
|
||||
// This is also enforced on API level, but as a last resort, we also enforce it here
|
||||
if gpg.IsGPGEnabled() && verifySignature {
|
||||
msg := "Cannot use local manifests when signature verification is required"
|
||||
targetObjs = make([]*unstructured.Unstructured, 0)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: msg, LastTransitionTime: &now})
|
||||
failedToLoadObjs = true
|
||||
} else {
|
||||
targetObjs, err = unmarshalManifests(localManifests)
|
||||
if err != nil {
|
||||
targetObjs = make([]*unstructured.Unstructured, 0)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
|
||||
failedToLoadObjs = true
|
||||
}
|
||||
}
|
||||
manifestInfo = nil
|
||||
}
|
||||
@@ -333,7 +413,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
|
||||
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),
|
||||
Message: fmt.Sprintf("%s/%s is part of applications %s and %s", liveObj.GetKind(), liveObj.GetName(), app.Name, appInstanceName),
|
||||
LastTransitionTime: &now,
|
||||
})
|
||||
}
|
||||
@@ -346,12 +426,15 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
|
||||
compareOptions, err := m.settingsMgr.GetResourceCompareOptions()
|
||||
if err != nil {
|
||||
log.Warnf("Could not get compare options from ConfigMap (assuming defaults): %v", err)
|
||||
compareOptions = diff.GetDefaultDiffOptions()
|
||||
compareOptions = settings.GetDefaultDiffOptions()
|
||||
}
|
||||
|
||||
logCtx.Debugf("built managed objects list")
|
||||
// Do the actual comparison
|
||||
diffResults, err := diff.DiffArray(reconciliation.Target, reconciliation.Live, diffNormalizer, compareOptions)
|
||||
diffResults, err := diff.DiffArray(
|
||||
reconciliation.Target, reconciliation.Live,
|
||||
diff.WithNormalizer(diffNormalizer),
|
||||
diff.IgnoreAggregatedRoles(compareOptions.IgnoreAggregatedRoles))
|
||||
if err != nil {
|
||||
diffResults = &diff.DiffResultList{}
|
||||
failedToLoadObjs = true
|
||||
@@ -387,15 +470,10 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
|
||||
if i < len(diffResults.Diffs) {
|
||||
diffResult = diffResults.Diffs[i]
|
||||
} else {
|
||||
diffResult = diff.DiffResult{
|
||||
Diff: gojsondiff.New().CompareObjects(map[string]interface{}{}, map[string]interface{}{}),
|
||||
Modified: false,
|
||||
NormalizedLive: []byte("{}"),
|
||||
PredictedLive: []byte("{}"),
|
||||
}
|
||||
diffResult = diff.DiffResult{Modified: false, NormalizedLive: []byte("{}"), PredictedLive: []byte("{}")}
|
||||
}
|
||||
if resState.Hook || ignore.Ignore(obj) {
|
||||
// For resource hooks, don't store sync status, and do not affect overall sync status
|
||||
if resState.Hook || ignore.Ignore(obj) || (targetObj != nil && hookutil.Skip(targetObj)) {
|
||||
// For resource hooks or skipped resources, don't store sync status, and do not affect overall sync status
|
||||
} else if diffResult.Modified || targetObj == nil || liveObj == nil {
|
||||
// Set resource state to OutOfSync since one of the following is true:
|
||||
// * target and live resource are different
|
||||
@@ -416,6 +494,10 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
|
||||
resState.Status = v1alpha1.SyncStatusCodeUnknown
|
||||
}
|
||||
|
||||
if isNamespaced && obj.GetNamespace() == "" {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{Type: v1alpha1.ApplicationConditionInvalidSpecError, Message: fmt.Sprintf("Namespace for %s %s is missing.", obj.GetName(), gvk.String()), LastTransitionTime: &now})
|
||||
}
|
||||
|
||||
// we can't say anything about the status if we were unable to get the target objects
|
||||
if failedToLoadObjs {
|
||||
resState.Status = v1alpha1.SyncStatusCodeUnknown
|
||||
@@ -449,7 +531,8 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
|
||||
}
|
||||
ts.AddCheckpoint("sync_ms")
|
||||
|
||||
healthStatus, err := argohealth.SetApplicationHealth(resourceSummaries, GetLiveObjs(managedResources), resourceOverrides, func(obj *unstructured.Unstructured) bool {
|
||||
resSumForAppHealth, liveObjsForAppHealth := GetLiveObjsForApplicationHealth(managedResources, resourceSummaries)
|
||||
healthStatus, err := argohealth.SetApplicationHealth(resSumForAppHealth, liveObjsForAppHealth, resourceOverrides, func(obj *unstructured.Unstructured) bool {
|
||||
return !isSelfReferencedApp(app, kubeutil.GetObjectRef(obj))
|
||||
})
|
||||
|
||||
@@ -457,6 +540,13 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
|
||||
conditions = append(conditions, appv1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
|
||||
}
|
||||
|
||||
// Git has already performed the signature verification via its GPG interface, and the result is available
|
||||
// in the manifest info received from the repository server. We now need to form our opinion about the result
|
||||
// and stop processing if we do not agree about the outcome.
|
||||
if gpg.IsGPGEnabled() && verifySignature && manifestInfo != nil {
|
||||
conditions = append(conditions, verifyGnuPGSignature(revision, project, manifestInfo)...)
|
||||
}
|
||||
|
||||
compRes := comparisonResult{
|
||||
syncStatus: &syncStatus,
|
||||
healthStatus: healthStatus,
|
||||
@@ -479,16 +569,17 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
|
||||
return &compRes
|
||||
}
|
||||
|
||||
func (m *appStateManager) persistRevisionHistory(app *v1alpha1.Application, revision string, source v1alpha1.ApplicationSource) error {
|
||||
func (m *appStateManager) persistRevisionHistory(app *v1alpha1.Application, revision string, source v1alpha1.ApplicationSource, startedAt metav1.Time) error {
|
||||
var nextID int64
|
||||
if len(app.Status.History) > 0 {
|
||||
nextID = app.Status.History[len(app.Status.History)-1].ID + 1
|
||||
nextID = app.Status.History.LastRevisionHistory().ID + 1
|
||||
}
|
||||
app.Status.History = append(app.Status.History, v1alpha1.RevisionHistory{
|
||||
Revision: revision,
|
||||
DeployedAt: metav1.NewTime(time.Now().UTC()),
|
||||
ID: nextID,
|
||||
Source: source,
|
||||
Revision: revision,
|
||||
DeployedAt: metav1.NewTime(time.Now().UTC()),
|
||||
DeployStartedAt: &startedAt,
|
||||
ID: nextID,
|
||||
Source: source,
|
||||
})
|
||||
|
||||
app.Status.History = app.Status.History.Trunc(app.Spec.GetRevisionHistoryLimit())
|
||||
@@ -501,7 +592,7 @@ func (m *appStateManager) persistRevisionHistory(app *v1alpha1.Application, revi
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = m.appclientset.ArgoprojV1alpha1().Applications(m.namespace).Patch(app.Name, types.MergePatchType, patch)
|
||||
_, err = m.appclientset.ArgoprojV1alpha1().Applications(m.namespace).Patch(context.Background(), app.Name, types.MergePatchType, patch, metav1.PatchOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/health"
|
||||
synccommon "github.com/argoproj/gitops-engine/pkg/sync/common"
|
||||
@@ -118,6 +121,33 @@ func TestCompareAppStateHook(t *testing.T) {
|
||||
assert.Equal(t, 0, len(app.Status.Conditions))
|
||||
}
|
||||
|
||||
// TestCompareAppStateSkipHook checks that skipped resources are detected during manifest generation, and not
|
||||
// considered as part of resources when assessing Synced status
|
||||
func TestCompareAppStateSkipHook(t *testing.T) {
|
||||
pod := NewPod()
|
||||
pod.SetAnnotations(map[string]string{synccommon.AnnotationKeyHook: "Skip"})
|
||||
podBytes, _ := json.Marshal(pod)
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
apps: []runtime.Object{app},
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{string(podBytes)},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Equal(t, 1, len(compRes.resources))
|
||||
assert.Equal(t, 1, len(compRes.managedResources))
|
||||
assert.Equal(t, 0, len(compRes.reconciliationResult.Hooks))
|
||||
assert.Equal(t, 0, len(app.Status.Conditions))
|
||||
}
|
||||
|
||||
// checks that ignore resources are detected, but excluded from status
|
||||
func TestCompareAppStateCompareOptionIgnoreExtraneous(t *testing.T) {
|
||||
pod := NewPod()
|
||||
@@ -185,11 +215,17 @@ func TestCompareAppStateDuplicatedNamespacedResources(t *testing.T) {
|
||||
obj2 := NewPod()
|
||||
obj3 := NewPod()
|
||||
obj3.SetNamespace("kube-system")
|
||||
obj4 := NewPod()
|
||||
obj4.SetGenerateName("my-pod")
|
||||
obj4.SetName("")
|
||||
obj5 := NewPod()
|
||||
obj5.SetName("")
|
||||
obj5.SetGenerateName("my-pod")
|
||||
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{toJSON(t, obj1), toJSON(t, obj2), toJSON(t, obj3)},
|
||||
Manifests: []string{toJSON(t, obj1), toJSON(t, obj2), toJSON(t, obj3), toJSON(t, obj4), toJSON(t, obj5)},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
@@ -207,7 +243,7 @@ func TestCompareAppStateDuplicatedNamespacedResources(t *testing.T) {
|
||||
assert.NotNil(t, app.Status.Conditions[0].LastTransitionTime)
|
||||
assert.Equal(t, argoappv1.ApplicationConditionRepeatedResourceWarning, app.Status.Conditions[0].Type)
|
||||
assert.Equal(t, "Resource /Pod/fake-dest-ns/my-pod appeared 2 times among application resources.", app.Status.Conditions[0].Message)
|
||||
assert.Equal(t, 2, len(compRes.resources))
|
||||
assert.Equal(t, 4, len(compRes.resources))
|
||||
}
|
||||
|
||||
var defaultProj = argoappv1.AppProject{
|
||||
@@ -399,7 +435,7 @@ func Test_appStateManager_persistRevisionHistory(t *testing.T) {
|
||||
app.Spec.RevisionHistoryLimit = &i
|
||||
}
|
||||
addHistory := func() {
|
||||
err := manager.persistRevisionHistory(app, "my-revision", argoappv1.ApplicationSource{})
|
||||
err := manager.persistRevisionHistory(app, "my-revision", argoappv1.ApplicationSource{}, metav1.Time{})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
addHistory()
|
||||
@@ -433,4 +469,304 @@ func Test_appStateManager_persistRevisionHistory(t *testing.T) {
|
||||
setRevisionHistoryLimit(9)
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 9)
|
||||
|
||||
metav1NowTime := metav1.NewTime(time.Now())
|
||||
err := manager.persistRevisionHistory(app, "my-revision", argoappv1.ApplicationSource{}, metav1NowTime)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, app.Status.History.LastRevisionHistory().DeployStartedAt, &metav1NowTime)
|
||||
}
|
||||
|
||||
// helper function to read contents of a file to string
|
||||
// panics on error
|
||||
func mustReadFile(path string) string {
|
||||
b, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
var signedProj = argoappv1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
},
|
||||
Spec: argoappv1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []argoappv1.ApplicationDestination{
|
||||
{
|
||||
Server: "*",
|
||||
Namespace: "*",
|
||||
},
|
||||
},
|
||||
SignatureKeys: []argoappv1.SignatureKey{
|
||||
{
|
||||
KeyID: "4AEE18F83AFDEB23",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestSignedResponseNoSignatureRequired(t *testing.T) {
|
||||
oldval := os.Getenv("ARGOCD_GPG_ENABLED")
|
||||
os.Setenv("ARGOCD_GPG_ENABLED", "true")
|
||||
defer os.Setenv("ARGOCD_GPG_ENABLED", oldval)
|
||||
// We have a good signature response, but project does not require signed commits
|
||||
{
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
VerifyResult: mustReadFile("../util/gpg/testdata/good_signature.txt"),
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Len(t, compRes.resources, 0)
|
||||
assert.Len(t, compRes.managedResources, 0)
|
||||
assert.Len(t, app.Status.Conditions, 0)
|
||||
}
|
||||
// We have a bad signature response, but project does not require signed commits
|
||||
{
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
VerifyResult: mustReadFile("../util/gpg/testdata/bad_signature_bad.txt"),
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Len(t, compRes.resources, 0)
|
||||
assert.Len(t, compRes.managedResources, 0)
|
||||
assert.Len(t, app.Status.Conditions, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignedResponseSignatureRequired(t *testing.T) {
|
||||
oldval := os.Getenv("ARGOCD_GPG_ENABLED")
|
||||
os.Setenv("ARGOCD_GPG_ENABLED", "true")
|
||||
defer os.Setenv("ARGOCD_GPG_ENABLED", oldval)
|
||||
|
||||
// We have a good signature response, valid key, and signing is required - sync!
|
||||
{
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
VerifyResult: mustReadFile("../util/gpg/testdata/good_signature.txt"),
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Len(t, compRes.resources, 0)
|
||||
assert.Len(t, compRes.managedResources, 0)
|
||||
assert.Len(t, app.Status.Conditions, 0)
|
||||
}
|
||||
// We have a bad signature response and signing is required - do not sync
|
||||
{
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
VerifyResult: mustReadFile("../util/gpg/testdata/bad_signature_bad.txt"),
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Len(t, compRes.resources, 0)
|
||||
assert.Len(t, compRes.managedResources, 0)
|
||||
assert.Len(t, app.Status.Conditions, 1)
|
||||
}
|
||||
// We have a malformed signature response and signing is required - do not sync
|
||||
{
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
VerifyResult: mustReadFile("../util/gpg/testdata/bad_signature_malformed1.txt"),
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Len(t, compRes.resources, 0)
|
||||
assert.Len(t, compRes.managedResources, 0)
|
||||
assert.Len(t, app.Status.Conditions, 1)
|
||||
}
|
||||
// We have no signature response (no signature made) and signing is required - do not sync
|
||||
{
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
VerifyResult: "",
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Len(t, compRes.resources, 0)
|
||||
assert.Len(t, compRes.managedResources, 0)
|
||||
assert.Len(t, app.Status.Conditions, 1)
|
||||
}
|
||||
|
||||
// We have a good signature and signing is required, but key is not allowed - do not sync
|
||||
{
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
VerifyResult: mustReadFile("../util/gpg/testdata/good_signature.txt"),
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
testProj := signedProj
|
||||
testProj.Spec.SignatureKeys[0].KeyID = "4AEE18F83AFDEB24"
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &testProj, "abc123", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Len(t, compRes.resources, 0)
|
||||
assert.Len(t, compRes.managedResources, 0)
|
||||
assert.Len(t, app.Status.Conditions, 1)
|
||||
assert.Contains(t, app.Status.Conditions[0].Message, "key is not allowed")
|
||||
}
|
||||
// Signature required and local manifests supplied - do not sync
|
||||
{
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
VerifyResult: "",
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
// it doesn't matter for our test whether local manifests are valid
|
||||
localManifests := []string{"foobar"}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, localManifests)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeUnknown, compRes.syncStatus.Status)
|
||||
assert.Len(t, compRes.resources, 0)
|
||||
assert.Len(t, compRes.managedResources, 0)
|
||||
assert.Len(t, app.Status.Conditions, 1)
|
||||
assert.Contains(t, app.Status.Conditions[0].Message, "Cannot use local manifests")
|
||||
}
|
||||
|
||||
os.Setenv("ARGOCD_GPG_ENABLED", "false")
|
||||
// We have a bad signature response and signing would be required, but GPG subsystem is disabled - sync
|
||||
{
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
VerifyResult: mustReadFile("../util/gpg/testdata/bad_signature_bad.txt"),
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Len(t, compRes.resources, 0)
|
||||
assert.Len(t, compRes.managedResources, 0)
|
||||
assert.Len(t, app.Status.Conditions, 0)
|
||||
}
|
||||
|
||||
// Signature required and local manifests supplied and GPG subystem is disabled - sync
|
||||
{
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
VerifyResult: "",
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
// it doesn't matter for our test whether local manifests are valid
|
||||
localManifests := []string{""}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, localManifests)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Len(t, compRes.resources, 0)
|
||||
assert.Len(t, compRes.managedResources, 0)
|
||||
assert.Len(t, app.Status.Conditions, 0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestComparisonResult_GetHealthStatus(t *testing.T) {
|
||||
status := &argoappv1.HealthStatus{Status: health.HealthStatusMissing}
|
||||
res := comparisonResult{
|
||||
healthStatus: status,
|
||||
}
|
||||
|
||||
assert.Equal(t, status, res.GetHealthStatus())
|
||||
}
|
||||
|
||||
func TestComparisonResult_GetSyncStatus(t *testing.T) {
|
||||
status := &argoappv1.SyncStatus{Status: argoappv1.SyncStatusCodeOutOfSync}
|
||||
res := comparisonResult{
|
||||
syncStatus: status,
|
||||
}
|
||||
|
||||
assert.Equal(t, status, res.GetSyncStatus())
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package controller
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
@@ -14,16 +16,24 @@ import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
cdcommon "github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/controller/metrics"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
listersv1alpha1 "github.com/argoproj/argo-cd/pkg/client/listers/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/argo"
|
||||
logutils "github.com/argoproj/argo-cd/util/log"
|
||||
"github.com/argoproj/argo-cd/util/lua"
|
||||
"github.com/argoproj/argo-cd/util/rand"
|
||||
)
|
||||
|
||||
var syncIdPrefix uint64 = 0
|
||||
|
||||
const (
|
||||
// EnvVarSyncWaveDelay is an environment variable which controls the delay in seconds between
|
||||
// each sync-wave
|
||||
EnvVarSyncWaveDelay = "ARGOCD_SYNC_WAVE_DELAY"
|
||||
)
|
||||
|
||||
func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha1.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
|
||||
@@ -68,7 +78,7 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
|
||||
revision = syncOp.Revision
|
||||
}
|
||||
|
||||
proj, err := argo.GetAppProject(&app.Spec, listersv1alpha1.NewAppProjectLister(m.projInformer.GetIndexer()), m.namespace)
|
||||
proj, err := argo.GetAppProject(&app.Spec, listersv1alpha1.NewAppProjectLister(m.projInformer.GetIndexer()), m.namespace, m.settingsMgr)
|
||||
if err != nil {
|
||||
state.Phase = common.OperationError
|
||||
state.Message = fmt.Sprintf("Failed to load application project: %v", err)
|
||||
@@ -76,6 +86,9 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
|
||||
}
|
||||
|
||||
compareResult := m.CompareAppState(app, proj, revision, source, false, syncOp.Manifests)
|
||||
// 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
|
||||
|
||||
// If there are any comparison or spec errors error conditions do not perform the operation
|
||||
if errConditions := app.Status.GetConditions(map[v1alpha1.ApplicationConditionType]bool{
|
||||
@@ -87,10 +100,6 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
|
||||
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 = common.OperationError
|
||||
@@ -126,7 +135,14 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
|
||||
Order: i + 1,
|
||||
})
|
||||
}
|
||||
syncCtx, err := sync.NewSyncContext(compareResult.syncStatus.Revision, compareResult.reconciliationResult, restConfig, rawConfig, m.kubectl, app.Spec.Destination.Namespace, logEntry,
|
||||
syncCtx, err := sync.NewSyncContext(
|
||||
compareResult.syncStatus.Revision,
|
||||
compareResult.reconciliationResult,
|
||||
restConfig,
|
||||
rawConfig,
|
||||
m.kubectl,
|
||||
app.Spec.Destination.Namespace,
|
||||
sync.WithLogr(logutils.NewLogrusLogger(logEntry)),
|
||||
sync.WithHealthOverride(lua.ResourceHealthOverrides(resourceOverrides)),
|
||||
sync.WithPermissionValidator(func(un *unstructured.Unstructured, res *v1.APIResource) error {
|
||||
if !proj.IsGroupKindPermitted(un.GroupVersionKind().GroupKind(), res.Namespaced) {
|
||||
@@ -138,11 +154,19 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
|
||||
return nil
|
||||
}),
|
||||
sync.WithOperationSettings(syncOp.DryRun, syncOp.Prune, syncOp.SyncStrategy.Force(), syncOp.IsApplyStrategy() || len(syncOp.Resources) > 0),
|
||||
sync.WithInitialState(state.Phase, state.Message, initialResourcesRes),
|
||||
sync.WithInitialState(state.Phase, state.Message, initialResourcesRes, state.StartedAt),
|
||||
sync.WithResourcesFilter(func(key kube.ResourceKey, target *unstructured.Unstructured, live *unstructured.Unstructured) bool {
|
||||
return len(syncOp.Resources) == 0 || argo.ContainsSyncResource(key.Name, schema.GroupVersionKind{Kind: key.Kind, Group: key.Group}, syncOp.Resources)
|
||||
return len(syncOp.Resources) == 0 || argo.ContainsSyncResource(key.Name, key.Namespace, schema.GroupVersionKind{Kind: key.Kind, Group: key.Group}, syncOp.Resources)
|
||||
}),
|
||||
sync.WithManifestValidation(!syncOp.SyncOptions.HasOption("Validate=false")),
|
||||
sync.WithNamespaceCreation(syncOp.SyncOptions.HasOption("CreateNamespace=true"), func(un *unstructured.Unstructured) bool {
|
||||
if un != nil && kube.GetAppInstanceLabel(un, cdcommon.LabelKeyAppInstance) != "" {
|
||||
kube.UnsetLabel(un, cdcommon.LabelKeyAppInstance)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}),
|
||||
sync.WithSyncWaveHook(delayBetweenSyncWaves),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
@@ -178,10 +202,32 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
|
||||
logEntry.WithField("duration", time.Since(start)).Info("sync/terminate complete")
|
||||
|
||||
if !syncOp.DryRun && len(syncOp.Resources) == 0 && state.Phase.Successful() {
|
||||
err := m.persistRevisionHistory(app, compareResult.syncStatus.Revision, source)
|
||||
err := m.persistRevisionHistory(app, compareResult.syncStatus.Revision, source, state.StartedAt)
|
||||
if err != nil {
|
||||
state.Phase = common.OperationError
|
||||
state.Message = fmt.Sprintf("failed to record sync to history: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// delayBetweenSyncWaves is a gitops-engine SyncWaveHook which introduces an artificial delay
|
||||
// between each sync wave. We introduce an artificial delay in order give other controllers a
|
||||
// _chance_ to react to the spec change that we just applied. This is important because without
|
||||
// this, Argo CD will likely assess resource health too quickly (against the stale object), causing
|
||||
// hooks to fire prematurely. See: https://github.com/argoproj/argo-cd/issues/4669.
|
||||
// Note, this is not foolproof, since a proper fix would require the CRD record
|
||||
// status.observedGeneration coupled with a health.lua that verifies
|
||||
// status.observedGeneration == metadata.generation
|
||||
func delayBetweenSyncWaves(phase common.SyncPhase, wave int, finalWave bool) error {
|
||||
if !finalWave {
|
||||
delaySec := 2
|
||||
if delaySecStr := os.Getenv(EnvVarSyncWaveDelay); delaySecStr != "" {
|
||||
if val, err := strconv.Atoi(delaySecStr); err == nil {
|
||||
delaySec = val
|
||||
}
|
||||
}
|
||||
duration := time.Duration(delaySec) * time.Second
|
||||
time.Sleep(duration)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
@@ -45,7 +47,7 @@ func TestPersistRevisionHistory(t *testing.T) {
|
||||
// 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{})
|
||||
updatedApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Get(context.Background(), 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)
|
||||
@@ -94,9 +96,49 @@ func TestPersistRevisionHistoryRollback(t *testing.T) {
|
||||
// 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{})
|
||||
updatedApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Get(context.Background(), 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)
|
||||
}
|
||||
|
||||
func TestSyncComparisonError(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Status.OperationState = nil
|
||||
app.Status.History = nil
|
||||
|
||||
defaultProject := &v1alpha1.AppProject{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
Name: "default",
|
||||
},
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
SignatureKeys: []v1alpha1.SignatureKey{{KeyID: "test"}},
|
||||
},
|
||||
}
|
||||
data := fakeData{
|
||||
apps: []runtime.Object{app, defaultProject},
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
VerifyResult: "something went wrong",
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
|
||||
// Sync with source unspecified
|
||||
opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
|
||||
Sync: &v1alpha1.SyncOperation{},
|
||||
}}
|
||||
os.Setenv("ARGOCD_GPG_ENABLED", "true")
|
||||
defer os.Setenv("ARGOCD_GPG_ENABLED", "false")
|
||||
ctrl.appStateManager.SyncAppState(app, opState)
|
||||
|
||||
conditions := app.Status.GetConditions(map[v1alpha1.ApplicationConditionType]bool{v1alpha1.ApplicationConditionComparisonError: true})
|
||||
assert.NotEmpty(t, conditions)
|
||||
assert.Equal(t, "abc123", opState.SyncResult.Revision)
|
||||
}
|
||||
|
||||
BIN
docs/assets/azure-enterprise-claims.png
Executable file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
docs/assets/azure-enterprise-saml-urls.png
Executable file
|
After Width: | Height: | Size: 52 KiB |
BIN
docs/assets/azure-enterprise-users.png
Executable file
|
After Width: | Height: | Size: 31 KiB |
BIN
docs/assets/favicon.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
docs/assets/google-admin-idp-metadata.png
Normal file
|
After Width: | Height: | Size: 272 KiB |
BIN
docs/assets/google-admin-saml-add-app-menu.png
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
docs/assets/google-admin-saml-app-details.png
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
docs/assets/google-admin-saml-apps-menu.png
Normal file
|
After Width: | Height: | Size: 131 KiB |
BIN
docs/assets/google-admin-service-provider-details.png
Normal file
|
After Width: | Height: | Size: 162 KiB |
BIN
docs/assets/keycloak-add-client.png
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
docs/assets/keycloak-add-scope.png
Normal file
|
After Width: | Height: | Size: 169 KiB |
BIN
docs/assets/keycloak-client-scope-selected.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
BIN
docs/assets/keycloak-client-scope.png
Normal file
|
After Width: | Height: | Size: 242 KiB |
BIN
docs/assets/keycloak-client-secret.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
docs/assets/keycloak-configure-client.png
Normal file
|
After Width: | Height: | Size: 194 KiB |
BIN
docs/assets/keycloak-groups-mapper.png
Normal file
|
After Width: | Height: | Size: 196 KiB |
BIN
docs/assets/keycloak-login.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
docs/assets/keycloak-user-group.png
Normal file
|
After Width: | Height: | Size: 99 KiB |