Compare commits
480 Commits
release-1.
...
v1.7.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c342d3fc9c | ||
|
|
32b32290a9 | ||
|
|
0635f2faef | ||
|
|
a3eabe8d95 | ||
|
|
da5fa74ca1 | ||
|
|
f711f95162 | ||
|
|
86c6c0b329 | ||
|
|
56520dc5d8 | ||
|
|
24b93197e0 | ||
|
|
5a0bb5cefc | ||
|
|
4d59273383 | ||
|
|
4f3537d274 | ||
|
|
76e9e918d2 | ||
|
|
b2decde4fe | ||
|
|
4728412cc3 | ||
|
|
26b9331820 | ||
|
|
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 | ||
|
|
e775b8fce8 | ||
|
|
84bece53a3 | ||
|
|
4a6fe4cd31 | ||
|
|
45cb1d5967 | ||
|
|
415d5e569f | ||
|
|
03a0a192ec | ||
|
|
a40f3689b3 | ||
|
|
80515a3b57 | ||
|
|
00f99edf1a | ||
|
|
bc83719037 | ||
|
|
2926f2bd60 | ||
|
|
7b2a95e83c | ||
|
|
d89b7d8a41 | ||
|
|
36da074344 | ||
|
|
2277af2f32 | ||
|
|
ee64a4d9ca | ||
|
|
d9bae2f83f | ||
|
|
a724574ede | ||
|
|
1c5fc0076e | ||
|
|
f826b0e397 | ||
|
|
196eb16244 | ||
|
|
09f3b45e39 | ||
|
|
c914ea0218 | ||
|
|
5ad46b025a | ||
|
|
22dcc1e87d | ||
|
|
91d5d7e37b | ||
|
|
23bf07d206 | ||
|
|
6637ee5c24 | ||
|
|
867282f726 | ||
|
|
8aadc310c9 | ||
|
|
ac097f143c | ||
|
|
4a12cbb231 | ||
|
|
40eb8c79ab | ||
|
|
710d06c800 | ||
|
|
2f2f39c8a6 | ||
|
|
7c831ad781 | ||
|
|
51998e0846 | ||
|
|
313de86941 | ||
|
|
0a4ce77bce | ||
|
|
f95004c428 | ||
|
|
f59391161e | ||
|
|
27e95df536 | ||
|
|
ec23d917eb | ||
|
|
991ee9b771 | ||
|
|
c21a6eae7d | ||
|
|
173a0f011d | ||
|
|
fe8d47e0ea | ||
|
|
192ee93fc4 | ||
|
|
490d4004b1 | ||
|
|
c2ff86e8b2 | ||
|
|
a32f70f207 | ||
|
|
02b3c61fd9 | ||
|
|
f822d098c3 | ||
|
|
dc5ac89f36 | ||
|
|
046406e7d3 | ||
|
|
4f49168c1a | ||
|
|
22bb1dd40f | ||
|
|
cd1de6e680 | ||
|
|
24fa758444 | ||
|
|
9208176e86 | ||
|
|
66e1fb78f7 | ||
|
|
e42102a67e | ||
|
|
887adffcc8 | ||
|
|
c66919f9ff | ||
|
|
301d18820a | ||
|
|
d2d37583af | ||
|
|
e0e995a944 | ||
|
|
1f87950d48 | ||
|
|
9a17103830 | ||
|
|
fd49a4e74f | ||
|
|
e78f61ea37 | ||
|
|
e5d4673eac | ||
|
|
3df4850418 | ||
|
|
1b0421c3aa | ||
|
|
20d56730ff | ||
|
|
ffc99354d2 | ||
|
|
e845478a96 | ||
|
|
28dd7167d0 | ||
|
|
faee66888e | ||
|
|
fc753ac489 | ||
|
|
88a76022c1 | ||
|
|
31df9d11a9 | ||
|
|
7ed6c18762 | ||
|
|
81d5b13083 | ||
|
|
02624bf3a6 | ||
|
|
842a3d12f6 | ||
|
|
14f1725f53 | ||
|
|
9b142c799a | ||
|
|
d77072b534 | ||
|
|
8a77075cff | ||
|
|
f5b600d4af | ||
|
|
4ae70139d9 | ||
|
|
7660e40fdc | ||
|
|
310b40aa20 | ||
|
|
0b0c72a80b | ||
|
|
14188656c8 | ||
|
|
76bacfdea4 | ||
|
|
ee44e489b5 | ||
|
|
89774fef17 | ||
|
|
acc2369dc7 | ||
|
|
9de06e35eb | ||
|
|
4575adca86 | ||
|
|
ca42a375c2 | ||
|
|
0214eb8d92 | ||
|
|
949518e680 | ||
|
|
81e4bb1fef | ||
|
|
35b40cdb22 | ||
|
|
fa47fe00a2 | ||
|
|
743371ed4f | ||
|
|
75d9f23adb | ||
|
|
e05ebc4990 | ||
|
|
6ffd34dcf9 | ||
|
|
16c6eaf9ae | ||
|
|
e67a55463f | ||
|
|
476cb655b7 | ||
|
|
6c1ccf4d60 | ||
|
|
05f5a79923 | ||
|
|
3dbc330cf0 | ||
|
|
8ad928330f | ||
|
|
355e77e56f | ||
|
|
376d79a454 | ||
|
|
b74a9461ed | ||
|
|
b4236e1dc7 | ||
|
|
6ecc25edbd | ||
|
|
56ca1fb4ea | ||
|
|
3629346085 | ||
|
|
092072a281 | ||
|
|
e1142f9759 | ||
|
|
fbd3fe69ff | ||
|
|
f05f84979c | ||
|
|
3d6ff9e903 | ||
|
|
4c812576c1 | ||
|
|
6753fc9743 | ||
|
|
8d082cc46e | ||
|
|
f586385c8b | ||
|
|
466c73fa3b | ||
|
|
9e6c78d55c | ||
|
|
c6af4cca10 | ||
|
|
5448466ddc | ||
|
|
0eec2fee71 | ||
|
|
e5452ff70e | ||
|
|
9fdd782854 | ||
|
|
053ae28ed5 | ||
|
|
6eb4f41343 | ||
|
|
d9072d8200 | ||
|
|
238abbf771 | ||
|
|
aa4fb9ab4a | ||
|
|
55bc144410 | ||
|
|
c428e091ab | ||
|
|
c13bf422f8 | ||
|
|
b4bbe60b8c | ||
|
|
4fdf573fd1 | ||
|
|
d326daef62 | ||
|
|
98337065ae | ||
|
|
1b1df76ef2 | ||
|
|
7f40739b97 | ||
|
|
9db879f68f | ||
|
|
e4235dabb8 | ||
|
|
ff07b112b1 | ||
|
|
53e618a4c0 | ||
|
|
eae0527839 | ||
|
|
2d79dbb0bb | ||
|
|
a501cdbb56 | ||
|
|
d2c1821148 | ||
|
|
00d44910b8 | ||
|
|
7ae204d426 | ||
|
|
6411958be5 | ||
|
|
a7f6866344 | ||
|
|
c71bfc62ba | ||
|
|
c4d6fde1c4 | ||
|
|
306a84193a | ||
|
|
b02f7f14a7 | ||
|
|
ac8ac14545 | ||
|
|
cdb8758b34 | ||
|
|
521f87fe5f | ||
|
|
27141ff083 | ||
|
|
7599516f68 | ||
|
|
e3a18b9cd7 | ||
|
|
eef35a32ab | ||
|
|
e26dace64d | ||
|
|
1520346369 | ||
|
|
702d4358d1 | ||
|
|
0162971ea0 | ||
|
|
7fd7999e49 | ||
|
|
03f773d0ff | ||
|
|
c4bc740fb7 | ||
|
|
5934bc4699 | ||
|
|
3f0d26ec17 | ||
|
|
7665e58613 | ||
|
|
ab80a8126b | ||
|
|
6905196665 | ||
|
|
4e283c14fb | ||
|
|
1b5925a494 | ||
|
|
d500b27f1d | ||
|
|
868b4c4c7c | ||
|
|
4bbce1cb22 | ||
|
|
127f50d697 | ||
|
|
e51aab8d1f | ||
|
|
85a746f861 | ||
|
|
3c2be61827 | ||
|
|
42d572306d | ||
|
|
b3f8e7a02c | ||
|
|
476b09cbbf | ||
|
|
487d6647d5 | ||
|
|
0378819c54 | ||
|
|
bbb925cb63 | ||
|
|
9d1a378ce8 | ||
|
|
e2358cabc9 | ||
|
|
5cd12a3943 | ||
|
|
ebb06b8c89 | ||
|
|
d5d01eca3e | ||
|
|
e13bb79578 | ||
|
|
a8b6282b15 | ||
|
|
fc00d73cf5 | ||
|
|
303d46e67c | ||
|
|
a00798bc5e | ||
|
|
1c4a15129b | ||
|
|
0ca35ef26c | ||
|
|
b38a9aacb2 | ||
|
|
5b239fc1d1 | ||
|
|
fdf7566bb7 | ||
|
|
389858b6df | ||
|
|
20adad76ef | ||
|
|
f37ae1c1f6 | ||
|
|
6edd18bb89 | ||
|
|
4d23fe8108 | ||
|
|
7eeefb003c | ||
|
|
3ae5b2bfe4 | ||
|
|
cdebd26ab4 | ||
|
|
3a088c7c86 | ||
|
|
5a363e9d9f | ||
|
|
57ea24281c | ||
|
|
94d7c10baa | ||
|
|
28027897aa | ||
|
|
0c610f91e5 | ||
|
|
990d9ef92b | ||
|
|
6592773a35 | ||
|
|
beee4de10e | ||
|
|
7fde387dd6 | ||
|
|
64c8ac70fb | ||
|
|
f230df938e | ||
|
|
c9b0fdf1d7 | ||
|
|
ea57d15a80 | ||
|
|
ebc048167c | ||
|
|
3b8405a89b | ||
|
|
66d496d1ef | ||
|
|
9d71ae5ad6 | ||
|
|
85d660f0b9 | ||
|
|
d5286296eb | ||
|
|
916d4aed57 | ||
|
|
59d7b7d2b4 | ||
|
|
18c8716f0a | ||
|
|
a1afe44066 | ||
|
|
1695457f9c | ||
|
|
06bc4064c1 | ||
|
|
d67b4f6c36 | ||
|
|
d2ff5887ac | ||
|
|
205926fa80 | ||
|
|
c4dd9d19c2 | ||
|
|
74fe4af98e | ||
|
|
949808f0b2 | ||
|
|
9ef80ef1f7 | ||
|
|
1801212ac7 | ||
|
|
722d5b02d9 | ||
|
|
9f8505205f | ||
|
|
9e81c38c13 | ||
|
|
ff40297bdc | ||
|
|
c8d74d1a7f | ||
|
|
a3f8ec33f4 | ||
|
|
c7718242f9 | ||
|
|
57eeaa4231 | ||
|
|
7edcf47a03 | ||
|
|
f561f22caa | ||
|
|
dcea620ca6 | ||
|
|
9790a5da9c | ||
|
|
b1d281e7bb | ||
|
|
2e7fa935c4 | ||
|
|
8b69efcdb9 | ||
|
|
8b08a337c5 | ||
|
|
e22f946415 | ||
|
|
3c6715a6f9 | ||
|
|
4cf02fd813 | ||
|
|
17d217c2d6 | ||
|
|
32d5a05aef | ||
|
|
e3f3688227 | ||
|
|
ad715565a6 | ||
|
|
d7cad4ac6d | ||
|
|
cc6c67d343 | ||
|
|
6ada626dda | ||
|
|
5bc59003af | ||
|
|
fe583c2f5d | ||
|
|
539281f89e | ||
|
|
e5ea3fe1fb | ||
|
|
f36ea4646d | ||
|
|
ddcdbaa990 | ||
|
|
857ce87f00 | ||
|
|
4643a1c26d | ||
|
|
3bf8dc6fb0 | ||
|
|
f62559128d | ||
|
|
63fe5f32ba |
@@ -1,217 +1,16 @@
|
||||
version: 2.1
|
||||
commands:
|
||||
configure_git:
|
||||
steps:
|
||||
- run:
|
||||
name: Configure Git
|
||||
command: |
|
||||
set -x
|
||||
# must be configured for tests to run
|
||||
git config --global user.email you@example.com
|
||||
git config --global user.name "Your Name"
|
||||
echo "export PATH=/home/circleci/.go_workspace/src/github.com/argoproj/argo-cd/hack:\$PATH" | tee -a $BASH_ENV
|
||||
echo "export GIT_ASKPASS=git-ask-pass.sh" | tee -a $BASH_ENV
|
||||
restore_vendor:
|
||||
steps:
|
||||
- restore_cache:
|
||||
keys:
|
||||
- vendor-v1-{{ checksum "Gopkg.lock" }}-{{ .Environment.CIRCLE_JOB }}
|
||||
save_vendor:
|
||||
steps:
|
||||
- save_cache:
|
||||
key: vendor-v1-{{ checksum "Gopkg.lock" }}-{{ .Environment.CIRCLE_JOB }}
|
||||
paths:
|
||||
- vendor
|
||||
install_golang:
|
||||
steps:
|
||||
- run:
|
||||
name: Install Golang v1.12.6
|
||||
command: |
|
||||
go get golang.org/dl/go1.12.6
|
||||
[ -e /home/circleci/sdk/go1.12.6 ] || go1.12.6 download
|
||||
go env
|
||||
echo "export GOPATH=/home/circleci/.go_workspace" | tee -a $BASH_ENV
|
||||
echo "export PATH=/home/circleci/sdk/go1.12.6/bin:\$PATH" | tee -a $BASH_ENV
|
||||
save_go_cache:
|
||||
steps:
|
||||
- save_cache:
|
||||
key: go-v1-{{ .Branch }}-{{ .Environment.CIRCLE_JOB }}
|
||||
# https://circleci.com/docs/2.0/language-go/
|
||||
paths:
|
||||
- /home/circleci/.cache/go-build
|
||||
- /home/circleci/sdk/go1.12.6
|
||||
restore_go_cache:
|
||||
steps:
|
||||
- restore_cache:
|
||||
keys:
|
||||
- go-v1-{{ .Branch }}-{{ .Environment.CIRCLE_JOB }}
|
||||
- go-v1-master-{{ .Environment.CIRCLE_JOB }}
|
||||
jobs:
|
||||
codegen:
|
||||
dummy:
|
||||
docker:
|
||||
- image: circleci/golang:1.12
|
||||
working_directory: /go/src/github.com/argoproj/argo-cd
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- codegen-v1-{{ checksum "Gopkg.lock" }}-{{ checksum "hack/installers/install-codegen-go-tools.sh" }}
|
||||
- run: ./hack/install.sh codegen-go-tools
|
||||
- run: sudo ./hack/install.sh codegen-tools
|
||||
- run: dep ensure -v
|
||||
- save_cache:
|
||||
key: codegen-v1-{{ checksum "Gopkg.lock" }}-{{ checksum "hack/installers/install-codegen-go-tools.sh" }}
|
||||
paths: [vendor, /tmp/dl, /go/pkg]
|
||||
- run: helm init --client-only
|
||||
- run: make codegen-local
|
||||
- run:
|
||||
name: Check nothing has changed
|
||||
command: |
|
||||
set -xo pipefail
|
||||
# This makes sure you ran `make pre-commit` before you pushed.
|
||||
# We exclude the Swagger resources; CircleCI doesn't generate them correctly.
|
||||
# When this fails, it will, create a patch file you can apply locally to fix it.
|
||||
# To troubleshoot builds: https://argoproj.github.io/argo-cd/developer-guide/ci/
|
||||
git diff --exit-code -- . ':!Gopkg.lock' ':!assets/swagger.json' | tee codegen.patch
|
||||
- store_artifacts:
|
||||
path: codegen.patch
|
||||
destination: .
|
||||
test:
|
||||
working_directory: /home/circleci/.go_workspace/src/github.com/argoproj/argo-cd
|
||||
machine:
|
||||
image: circleci/classic:201808-01
|
||||
steps:
|
||||
- restore_go_cache
|
||||
- install_golang
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: test-dl-v1
|
||||
- run: sudo ./hack/install.sh kubectl-linux kubectx-linux dep-linux ksonnet-linux helm-linux kustomize-linux
|
||||
- save_cache:
|
||||
key: test-dl-v1
|
||||
paths: [/tmp/dl]
|
||||
- configure_git
|
||||
- run: go get github.com/jstemmer/go-junit-report
|
||||
- restore_vendor
|
||||
- run: dep ensure -v
|
||||
- run: make test
|
||||
- save_vendor
|
||||
- save_go_cache
|
||||
- run:
|
||||
name: Uploading code coverage
|
||||
command: bash <(curl -s https://codecov.io/bash) -f coverage.out
|
||||
- store_test_results:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
destination: .
|
||||
e2e:
|
||||
working_directory: /home/circleci/.go_workspace/src/github.com/argoproj/argo-cd
|
||||
machine:
|
||||
image: circleci/classic:201808-01
|
||||
environment:
|
||||
ARGOCD_FAKE_IN_CLUSTER: "true"
|
||||
ARGOCD_SSH_DATA_PATH: "/tmp/argo-e2e/app/config/ssh"
|
||||
ARGOCD_TLS_DATA_PATH: "/tmp/argo-e2e/app/config/tls"
|
||||
- image: cimg/base:2020.01
|
||||
steps:
|
||||
- run:
|
||||
name: Install and start K3S v0.5.0
|
||||
name: Dummy step
|
||||
command: |
|
||||
curl -sfL https://get.k3s.io | sh -
|
||||
sudo chmod -R a+rw /etc/rancher/k3s
|
||||
kubectl version
|
||||
background: true
|
||||
environment:
|
||||
INSTALL_K3S_EXEC: --docker
|
||||
INSTALL_K3S_VERSION: v0.5.0
|
||||
- restore_go_cache
|
||||
- install_golang
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys: [e2e-dl-v1]
|
||||
- run: sudo ./hack/install.sh kubectx-linux dep-linux ksonnet-linux helm-linux kustomize-linux
|
||||
- run: go get github.com/jstemmer/go-junit-report
|
||||
- save_cache:
|
||||
key: e2e-dl-v10
|
||||
paths: [/tmp/dl]
|
||||
- restore_vendor
|
||||
- run: dep ensure -v
|
||||
- configure_git
|
||||
- run: make cli
|
||||
- run:
|
||||
name: Create namespace
|
||||
command: |
|
||||
set -x
|
||||
kubectl create ns argocd-e2e
|
||||
kubens argocd-e2e
|
||||
# install the certificates (not 100% sure we need this)
|
||||
sudo cp /var/lib/rancher/k3s/server/tls/token-ca.crt /usr/local/share/ca-certificates/k3s.crt
|
||||
sudo update-ca-certificates
|
||||
# create the kubecfg, again - not sure we need this
|
||||
cat /etc/rancher/k3s/k3s.yaml | sed "s/localhost/`hostname`/" | tee ~/.kube/config
|
||||
echo "127.0.0.1 `hostname`" | sudo tee -a /etc/hosts
|
||||
- run:
|
||||
name: Apply manifests
|
||||
command: kustomize build test/manifests/base | kubectl apply -f -
|
||||
- run:
|
||||
name: Start Redis
|
||||
command: docker run --rm --name argocd-redis -i -p 6379:6379 redis:5.0.3-alpine --save "" --appendonly no
|
||||
background: true
|
||||
- run:
|
||||
name: Start repo server
|
||||
command: go run ./cmd/argocd-repo-server/main.go --loglevel debug --redis localhost:6379
|
||||
background: true
|
||||
- run:
|
||||
name: Start API server
|
||||
command: go run ./cmd/argocd-server/main.go --loglevel debug --redis localhost:6379 --insecure --dex-server http://localhost:5556 --repo-server localhost:8081 --staticassets ../argo-cd-ui/dist/app
|
||||
background: true
|
||||
- run:
|
||||
name: Start Test Git
|
||||
command: |
|
||||
test/fixture/testrepos/start-git.sh
|
||||
background: true
|
||||
- run: until curl -v http://localhost:8080/healthz; do sleep 10; done
|
||||
- run:
|
||||
name: Start controller
|
||||
command: go run ./cmd/argocd-application-controller/main.go --loglevel debug --redis localhost:6379 --repo-server localhost:8081 --kubeconfig ~/.kube/config
|
||||
background: true
|
||||
- run:
|
||||
command: PATH=dist:$PATH make test-e2e
|
||||
environment:
|
||||
ARGOCD_OPTS: "--server localhost:8080 --plaintext"
|
||||
ARGOCD_E2E_K3S: "true"
|
||||
- save_vendor
|
||||
- save_go_cache
|
||||
- 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
|
||||
echo "This is a dummy step to satisfy CircleCI"
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
workflow:
|
||||
jobs:
|
||||
- test
|
||||
- codegen
|
||||
- ui:
|
||||
requires:
|
||||
- codegen
|
||||
- e2e
|
||||
jobs:
|
||||
- dummy
|
||||
324
.circleci/config.yml.off
Normal file
@@ -0,0 +1,324 @@
|
||||
# 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
|
||||
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -5,9 +5,13 @@ title: ''
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
If you are trying to resolve an environment-specific issue or have a one-off question about the edge case that does not require a feature then please consider asking a
|
||||
question in argocd slack [channel](https://argoproj.github.io/community/join-slack).
|
||||
|
||||
Checklist:
|
||||
|
||||
* [ ] I've searched in the docs and FAQ for my answer: http://bit.ly/argocd-faq.
|
||||
* [ ] I've 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,5 @@ Checklist:
|
||||
* [ ] Either (a) I've created an [enhancement proposal](https://github.com/argoproj/argo-cd/issues/new/choose) and discussed it with the community, (b) this is a bug fix, or (c) this does not need to be in the release notes.
|
||||
* [ ] The title of the PR states what changed and the related issues number (used for the release note).
|
||||
* [ ] I've updated both the CLI and UI to expose my feature, or I plan to submit a second PR with them.
|
||||
* [ ] Optional. My organization is added to the README.
|
||||
* [ ] 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/)).
|
||||
|
||||
357
.github/workflows/ci-build.yaml
vendored
Normal file
@@ -0,0 +1,357 @@
|
||||
name: Integration tests
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'release-*'
|
||||
- '!release-1.4'
|
||||
- '!release-1.5'
|
||||
pull_request:
|
||||
branches:
|
||||
- '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
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: '1.14.2'
|
||||
- name: Download all Go modules
|
||||
run: |
|
||||
go mod download
|
||||
- name: Check for tidyness of go.mod and go.sum
|
||||
run: |
|
||||
go mod tidy
|
||||
git diff --exit-code -- .
|
||||
|
||||
build-go:
|
||||
name: Build & cache Go code
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: '1.14.2'
|
||||
- name: Restore go build cache
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.cache/go-build
|
||||
key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
|
||||
- name: Download all Go modules
|
||||
run: |
|
||||
go mod download
|
||||
- name: Compile all packages
|
||||
run: make build-local
|
||||
|
||||
lint-go:
|
||||
name: Lint Go code
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v1
|
||||
with:
|
||||
version: v1.26
|
||||
args: --timeout 5m
|
||||
|
||||
test-go:
|
||||
name: Run unit tests 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.2'
|
||||
- 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 "::add-path::/home/runner/go/bin"
|
||||
- name: Add /usr/local/bin to PATH
|
||||
run: echo "::add-path::/usr/local/bin"
|
||||
- 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-local
|
||||
- name: Generate code coverage artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: code-coverage
|
||||
path: coverage.out
|
||||
- name: Generate test results artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: test-results
|
||||
path: test-results/
|
||||
|
||||
codegen:
|
||||
name: Check changes to generated code
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: '1.14.2'
|
||||
- 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"
|
||||
- name: Download & vendor dependencies
|
||||
run: |
|
||||
# We need to vendor go modules for codegen yet
|
||||
go mod download
|
||||
go mod vendor -v
|
||||
working-directory: /home/runner/go/src/github.com/argoproj/argo-cd
|
||||
- name: Install toolchain for codegen
|
||||
run: |
|
||||
make install-codegen-tools-local
|
||||
make install-go-tools-local
|
||||
working-directory: /home/runner/go/src/github.com/argoproj/argo-cd
|
||||
- name: Initialize local Helm
|
||||
run: |
|
||||
helm2 init --client-only
|
||||
- name: Run codegen
|
||||
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
|
||||
run: |
|
||||
set -xo pipefail
|
||||
git diff --exit-code -- . ':!go.sum' ':!go.mod' ':!assets/swagger.json' | tee codegen.patch
|
||||
working-directory: /home/runner/go/src/github.com/argoproj/argo-cd
|
||||
|
||||
build-ui:
|
||||
name: Build, test & lint UI code
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup NodeJS
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '11.15.0'
|
||||
- name: Restore node dependency cache
|
||||
id: cache-dependencies
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ui/node_modules
|
||||
key: ${{ runner.os }}-node-dep-v2-${{ hashFiles('**/yarn.lock') }}
|
||||
- name: Install node dependencies
|
||||
run: |
|
||||
cd ui && yarn install --frozen-lockfile --ignore-optional --non-interactive
|
||||
- name: Build UI code
|
||||
run: |
|
||||
yarn test
|
||||
yarn build
|
||||
env:
|
||||
NODE_ENV: production
|
||||
working-directory: ui/
|
||||
- name: Run ESLint
|
||||
run: yarn lint
|
||||
working-directory: ui/
|
||||
|
||||
analyze:
|
||||
name: Process & analyze test artifacts
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- test-go
|
||||
- build-ui
|
||||
env:
|
||||
sonar_secret: ${{ secrets.SONAR_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Restore node dependency cache
|
||||
id: cache-dependencies
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ui/node_modules
|
||||
key: ${{ runner.os }}-node-dep-v2-${{ hashFiles('**/yarn.lock') }}
|
||||
- name: Remove other node_modules directory
|
||||
run: |
|
||||
rm -rf ui/node_modules/argo-ui/node_modules
|
||||
- name: Create test-results directory
|
||||
run: |
|
||||
mkdir -p test-results
|
||||
- name: Get code coverage artifiact
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: code-coverage
|
||||
- name: Get test result artifact
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: test-results
|
||||
path: test-results
|
||||
- name: Upload code coverage information to codecov.io
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
file: coverage.out
|
||||
- name: Perform static code analysis using SonarCloud
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SCANNER_VERSION: 4.2.0.1873
|
||||
SCANNER_PATH: /tmp/cache/scanner
|
||||
OS: linux
|
||||
run: |
|
||||
# We do not use the provided action, because it does contain an old
|
||||
# version of the scanner, and also takes time to build.
|
||||
set -e
|
||||
mkdir -p ${SCANNER_PATH}
|
||||
export SONAR_USER_HOME=${SCANNER_PATH}/.sonar
|
||||
if [[ ! -x "${SCANNER_PATH}/sonar-scanner-${SCANNER_VERSION}-${OS}/bin/sonar-scanner" ]]; then
|
||||
curl -Ol https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${SCANNER_VERSION}-${OS}.zip
|
||||
unzip -qq -o sonar-scanner-cli-${SCANNER_VERSION}-${OS}.zip -d ${SCANNER_PATH}
|
||||
fi
|
||||
|
||||
chmod +x ${SCANNER_PATH}/sonar-scanner-${SCANNER_VERSION}-${OS}/bin/sonar-scanner
|
||||
chmod +x ${SCANNER_PATH}/sonar-scanner-${SCANNER_VERSION}-${OS}/jre/bin/java
|
||||
|
||||
# Explicitly set NODE_MODULES
|
||||
export NODE_MODULES=${PWD}/ui/node_modules
|
||||
export NODE_PATH=${PWD}/ui/node_modules
|
||||
|
||||
${SCANNER_PATH}/sonar-scanner-${SCANNER_VERSION}-${OS}/bin/sonar-scanner
|
||||
if: env.sonar_secret != ''
|
||||
|
||||
test-e2e:
|
||||
name: Run end-to-end tests
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-go
|
||||
env:
|
||||
GOPATH: /home/runner/go
|
||||
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_SSH_KNOWN_HOSTS: "../fixture/certs/ssh_known_hosts"
|
||||
ARGOCD_E2E_K3S: "true"
|
||||
ARGOCD_IN_CI: "true"
|
||||
ARGOCD_E2E_APISERVER_PORT: "8088"
|
||||
ARGOCD_SERVER: "127.0.0.1:8088"
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: '1.14.2'
|
||||
- name: Install K3S
|
||||
env:
|
||||
INSTALL_K3S_VERSION: v0.5.0
|
||||
run: |
|
||||
set -x
|
||||
curl -sfL https://get.k3s.io | sh -
|
||||
sudo chmod -R a+rw /etc/rancher/k3s
|
||||
sudo mkdir -p $HOME/.kube && sudo chown -R runner $HOME/.kube
|
||||
sudo k3s kubectl config view --raw > $HOME/.kube/config
|
||||
sudo chown runner $HOME/.kube/config
|
||||
kubectl version
|
||||
- name: Restore go build cache
|
||||
uses: actions/cache@v1
|
||||
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"
|
||||
- name: Download Go dependencies
|
||||
run: |
|
||||
go mod download
|
||||
go get github.com/mattn/goreman
|
||||
- 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: Pull Docker image required for tests
|
||||
run: |
|
||||
docker pull quay.io/dexidp/dex:v2.22.0
|
||||
docker pull argoproj/argo-cd-ci-builder:v1.0.0
|
||||
docker pull redis:5.0.8-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: |
|
||||
set -x
|
||||
# Something is weird in GH runners -- there's a phantom listener for
|
||||
# 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 &
|
||||
count=1
|
||||
until curl -f http://127.0.0.1:8088/healthz; do
|
||||
sleep 10;
|
||||
if test $count -ge 60; then
|
||||
echo "Timeout"
|
||||
exit 1
|
||||
fi
|
||||
count=$((count+1))
|
||||
done
|
||||
- name: Run E2E testsuite
|
||||
run: |
|
||||
set -x
|
||||
make test-e2e-local
|
||||
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
|
||||
6
.github/workflows/gh-pages.yaml
vendored
@@ -4,6 +4,9 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
@@ -16,10 +19,11 @@ jobs:
|
||||
python-version: 3.x
|
||||
- name: build
|
||||
run: |
|
||||
pip install mkdocs mkdocs_material
|
||||
pip install mkdocs==1.0.4 mkdocs_material==4.1.1
|
||||
mkdocs build
|
||||
mkdir ./site/.circleci && echo '{version: 2, jobs: {build: {branches: {ignore: gh-pages}}}}' > ./site/.circleci/config.yml
|
||||
- name: deploy
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
uses: peaceiris/actions-gh-pages@v2.5.0
|
||||
env:
|
||||
PERSONAL_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
|
||||
50
.github/workflows/image.yaml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
name: Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GOPATH: /home/runner/work/argo-cd/argo-cd
|
||||
steps:
|
||||
- uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: '1.14.1'
|
||||
- uses: actions/checkout@master
|
||||
with:
|
||||
path: src/github.com/argoproj/argo-cd
|
||||
|
||||
# get image tag
|
||||
- run: echo ::set-output name=tag::$(cat ./VERSION)-${GITHUB_SHA::8}
|
||||
working-directory: ./src/github.com/argoproj/argo-cd
|
||||
id: image
|
||||
|
||||
# build
|
||||
- run: |
|
||||
docker images -a --format "{{.ID}}" | xargs -I {} docker rmi {}
|
||||
make image DEV_IMAGE=true DOCKER_PUSH=false IMAGE_NAMESPACE=docker.pkg.github.com/argoproj/argo-cd IMAGE_TAG=${{ steps.image.outputs.tag }}
|
||||
working-directory: ./src/github.com/argoproj/argo-cd
|
||||
|
||||
# publish
|
||||
- run: |
|
||||
docker login docker.pkg.github.com --username $USERNAME --password $PASSWORD
|
||||
docker push docker.pkg.github.com/argoproj/argo-cd/argocd:${{ steps.image.outputs.tag }}
|
||||
env:
|
||||
USERNAME: ${{ secrets.USERNAME }}
|
||||
PASSWORD: ${{ secrets.TOKEN }}
|
||||
|
||||
# deploy
|
||||
- run: git clone "https://$TOKEN@github.com/argoproj/argoproj-deployments"
|
||||
env:
|
||||
TOKEN: ${{ secrets.TOKEN }}
|
||||
- run: |
|
||||
docker run -v $(pwd):/src -w /src --rm -t lyft/kustomizer:v3.3.0 kustomize edit set image argoproj/argocd=docker.pkg.github.com/argoproj/argo-cd/argocd:${{ steps.image.outputs.tag }}
|
||||
git config --global user.email 'ci@argoproj.com'
|
||||
git config --global user.name 'CI'
|
||||
git diff --exit-code && echo 'Already deployed' || (git commit -am 'Upgrade argocd to ${{ steps.image.outputs.tag }}' && git push)
|
||||
working-directory: argoproj-deployments/argocd
|
||||
# TODO: clean up old images once github supports it: https://github.community/t5/How-to-use-Git-and-GitHub/Deleting-images-from-Github-Package-Registry/m-p/41202/thread-id/9811
|
||||
289
.github/workflows/release.yaml
vendored
Normal file
@@ -0,0 +1,289 @@
|
||||
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
|
||||
# The name of the repository containing tap formulae
|
||||
TAP_REPOSITORY: argoproj/homebrew-tap
|
||||
# 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 (or homebrew-tap repo specifically)
|
||||
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 "::set-env name=TARGET_VERSION::${TARGET_VERSION}"
|
||||
echo "::set-env name=TARGET_BRANCH::${TARGET_BRANCH}"
|
||||
echo "::set-env name=RELEASE_TAG::${RELEASE_TAG}"
|
||||
echo "::set-env name=PRE_RELEASE::${PRE_RELEASE}"
|
||||
|
||||
- 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 "::set-env name=RELEASE_NOTES::$RELEASE_NOTES"
|
||||
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: '1.14.2'
|
||||
|
||||
- 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: Check out homebrew tap repository
|
||||
uses: actions/checkout@v2
|
||||
env:
|
||||
HOMEBREW_TOKEN: ${{ secrets.RELEASE_HOMEBREW_TOKEN }}
|
||||
with:
|
||||
repository: ${{ env.TAP_REPOSITORY }}
|
||||
path: homebrew-tap
|
||||
fetch-depth: 0
|
||||
token: ${{ env.HOMEBREW_TOKEN }}
|
||||
if: ${{ env.HOMEBREW_TOKEN != '' && env.UPDATE_HOMEBREW == 'true' && env.PRE_RELEASE != 'true' }}
|
||||
|
||||
- name: Update homebrew tap formula
|
||||
env:
|
||||
HOMEBREW_TOKEN: ${{ secrets.RELEASE_HOMEBREW_TOKEN }}
|
||||
run: |
|
||||
set -ue
|
||||
cd homebrew-tap
|
||||
./update.sh argocd ${TARGET_VERSION}
|
||||
git commit -am "Update argocd to ${TARGET_VERSION}"
|
||||
git push
|
||||
cd ..
|
||||
rm -rf homebrew-tap
|
||||
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() }}
|
||||
4
.gitignore
vendored
@@ -9,4 +9,6 @@ site/
|
||||
cmd/**/debug
|
||||
debug.test
|
||||
coverage.out
|
||||
test-results
|
||||
test-results
|
||||
.scannerwork
|
||||
.scratch
|
||||
|
||||
793
CHANGELOG.md
@@ -1,14 +1,552 @@
|
||||
# Changelog
|
||||
|
||||
## v1.3.0-rc2 (2019-10-23)
|
||||
## v1.7.0 (Unreleased)
|
||||
|
||||
- Issue #2339 - Controller should compare with latest git revision if app has changed (#2543)
|
||||
- Unknown child app should not affect app health (#2544)
|
||||
- Redact secrets in dex logs (#2538)
|
||||
- Allows Helm parameters that contains arrays or maps. (#2525)
|
||||
- Set cookie policy to SameSite=lax and httpOnly (#2498)
|
||||
### GnuPG Signature Verification
|
||||
|
||||
## v1.3.0-rc1 (2019-10-16)
|
||||
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)
|
||||
- fix: enable redis retries; add redis request duration metric (#3547)
|
||||
- fix: when --rootpath is on, 404 is returned when URL contains encoded URI (#3564)
|
||||
|
||||
## v1.5.4 (2020-05-05)
|
||||
|
||||
- fix: CLI commands with --grpc-web
|
||||
|
||||
## v1.5.3 (2020-05-01)
|
||||
|
||||
This patch release introduces a set of enhancements and bug fixes. Here are most notable changes:
|
||||
|
||||
#### Multiple Kustomize Versions
|
||||
|
||||
The bundled Kustomize version had been upgraded to v3.5.4. Argo CD allows changing bundled version using
|
||||
[custom image or init container](https://argoproj.github.io/argo-cd/operator-manual/custom_tools/).
|
||||
This [feature](https://argoproj.github.io/argo-cd/user-guide/kustomize/#custom-kustomize-versions)
|
||||
enables bundling multiple Kustomize versions at the same time and allows end-users to specify the required version per application.
|
||||
|
||||
#### Custom Root Path
|
||||
|
||||
The feature allows accessing Argo CD UI and API using a custom root path(for example https://myhostname/argocd).
|
||||
This enables running Argo CD behind a proxy that takes care of user authentication (such as Ambassador) or hosting
|
||||
multiple Argo CD using the same hostname. A set of bug fixes and enhancements had been implemented to makes it easier.
|
||||
Use new `--rootpath` [flag](https://argoproj.github.io/argo-cd/operator-manual/ingress/#argocd-server-and-ui-root-path-v153) to enable the feature.
|
||||
|
||||
### Login Rate Limiting
|
||||
|
||||
The feature prevents a built-in user password brute force attack and addresses the known
|
||||
[vulnerability](https://argoproj.github.io/argo-cd/security_considerations/#cve-2020-8827-insufficient-anti-automationanti-brute-force).
|
||||
|
||||
### Settings Management Tools
|
||||
|
||||
A new set of [CLI commands](https://argoproj.github.io/argo-cd/operator-manual/troubleshooting/) that simplify configuring Argo CD.
|
||||
Using the CLI you can test settings changes offline without affecting running Argo CD instance and have ability to troubleshot diffing
|
||||
customizations, custom resource health checks, and more.
|
||||
|
||||
### Other
|
||||
|
||||
* New Project and Application CRD settings ([#2900](https://github.com/argoproj/argo-cd/issues/2900), [#2873](https://github.com/argoproj/argo-cd/issues/2873)) that allows customizing Argo CD behavior.
|
||||
* Upgraded Dex (v2.22.0) enables seamless [SSO integration](https://www.openshift.com/blog/openshift-authentication-integration-with-argocd) with Openshift.
|
||||
|
||||
|
||||
#### Enhancements
|
||||
|
||||
* feat: added --grpc-web-root-path for CLI. (#3483)
|
||||
* feat: limit the maximum number of concurrent login attempts (#3467)
|
||||
* feat: upgrade kustomize version to 3.5.4 (#3472)
|
||||
* feat: upgrade dex to 2.22.0 (#3468)
|
||||
* feat: support user specified account token ids (#3425)
|
||||
* feat: support separate Kustomize version per application (#3414)
|
||||
* feat: add support for dex prometheus metrics (#3249)
|
||||
* feat: add settings troubleshooting commands to the 'argocd-util' binary (#3398)
|
||||
* feat: Let user to define meaningful unique JWT token name (#3388)
|
||||
* feat: Display link between OLM ClusterServiceVersion and it's OperatorGroup (#3390)
|
||||
* feat: Introduce sync-option SkipDryRunOnMissingResource=true (#2873) (#3247)
|
||||
* feat: support normalizing CRD fields that use known built-in K8S types (#3357)
|
||||
* feat: Whitelisted namespace resources (#2900)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
* fix: added path to cookie (#3501)
|
||||
* fix: 'argocd sync' does not take into account IgnoreExtraneous annotation (#3486)
|
||||
* fix: CLI renders flipped diff results (#3480)
|
||||
* fix: GetApplicationSyncWindows API should not validate project permissions (#3456)
|
||||
* fix: argocd-util kubeconfig should use RawRestConfig to export config (#3447)
|
||||
* fix: javascript error on accounts list page (#3453)
|
||||
* fix: support both <group>/<kind> as well as <kind> as a resource override key (#3433)
|
||||
* fix: Updating to jsonnet v1.15.0 fix issue #3277 (#3431)
|
||||
* fix for helm repo add with flag --insecure-skip-server-verification (#3420)
|
||||
* fix: app diff --local support for helm repo. #3151 (#3407)
|
||||
* fix: Syncing apps incorrectly states "app synced", but this is not true (#3286)
|
||||
* fix: for jsonnet when it is localed in nested subdirectory and uses import (#3372)
|
||||
* fix: Update 4.5.3 redis-ha helm manifest (#3370)
|
||||
* fix: return 401 error code if username does not exist (#3369)
|
||||
* fix: Do not panic while running hooks with short revision (#3368)
|
||||
|
||||
## v1.5.2 (2020-04-20)
|
||||
|
||||
#### Critical security fix
|
||||
|
||||
This release contains a critical security fix. Please refer to the
|
||||
[security document](https://argoproj.github.io/argo-cd/security_considerations/#CVE-2020-5260-possible-git-credential-leak)
|
||||
for more information.
|
||||
|
||||
**Upgrading is strongly recommended**
|
||||
|
||||
## v1.4.3 (2020-04-20)
|
||||
|
||||
#### Critical security fix
|
||||
|
||||
This release contains a critical security fix. Please refer to the
|
||||
[security document](https://argoproj.github.io/argo-cd/security_considerations/#CVE-2020-5260-possible-git-credential-leak)
|
||||
for more information.
|
||||
|
||||
## v1.5.1 (2020-04-06)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
* fix: return 401 error code if username does not exist (#3369)
|
||||
* fix: Do not panic while running hooks with short revision (#3368)
|
||||
* fix: Increase HAProxy check interval to prevent intermittent failures (#3356)
|
||||
* fix: Helm v3 CRD are not deployed (#3345)
|
||||
|
||||
## v1.5.0 (2020-04-02)
|
||||
|
||||
#### Helm Integration Enhancements - Helm 3 Support And More
|
||||
|
||||
Introduced native support Helm3 charts. For backward compatibility Helm 2 charts are still rendered using Helm 2 CLI. Argo CD inspects the
|
||||
Charts.yaml file and choose the right binary based on `apiVersion` value.
|
||||
|
||||
Following enhancement were implemented in addition to Helm 3:
|
||||
* The `--api-version` flag is passed to the `helm template` command during manifest generation.
|
||||
* The `--set-file` flag can be specified in the application specification.
|
||||
* Fixed bug that prevents automatically update Helm chart when new version is published (#3193)
|
||||
|
||||
#### Better Performance and Improved Metrics
|
||||
|
||||
If you are running Argo CD instances with several hundred applications on it, you should see a
|
||||
huge performance boost and significantly less Kubernetes API server load.
|
||||
|
||||
The Argo CD controller Prometheus metrics have been reworked to enable a richer Grafana dashboard.
|
||||
The improved dashboard is available at [examples/dashboard.json](https://github.com/argoproj/argo-cd/blob/master/examples/dashboard.json).
|
||||
You can set `ARGOCD_LEGACY_CONTROLLER_METRICS=true` environment variable and use [examples/dashboard-legacy.json](https://github.com/argoproj/argo-cd/blob/master/examples/dashboard-legacy.json)
|
||||
to keep using old dashboard.
|
||||
|
||||
#### Local accounts
|
||||
|
||||
The local accounts had been introduced additional to `admin` user and SSO integration. The feature is useful for creating authentication
|
||||
tokens with limited permissions to automate Argo CD management. Local accounts also could be used small by teams when SSO integration is overkill.
|
||||
This enhancement also allows to disable admin user and enforce only SSO logins.
|
||||
|
||||
#### Redis HA Proxy mode
|
||||
|
||||
As part of this release, the bundled Redis was upgraded to version 4.3.4 with enabled HAProxy.
|
||||
The HA proxy replaced the sentinel and provides more reliable Redis connection.
|
||||
|
||||
> After publishing 1.5.0 release we've discovered that default HAProxy settings might cause intermittent failures.
|
||||
> See [argo-cd#3358](https://github.com/argoproj/argo-cd/issues/3358)
|
||||
|
||||
#### Windows CLI
|
||||
|
||||
Windows users deploy to Kubernetes too! Now you can use Argo CD CLI on Linux, Mac OS, and Windows. The Windows compatible binary is available
|
||||
in the release details page as well as on the Argo CD Help page.
|
||||
|
||||
#### Breaking Changes
|
||||
|
||||
The `argocd_app_sync_status`, `argocd_app_health_status` and `argocd_app_created_time` prometheus metrics are deprecated in favor of additional labels
|
||||
to `argocd_app_info` metric. The deprecated labels are still available can be re-enabled using `ARGOCD_LEGACY_CONTROLLER_METRICS=true` environment variable.
|
||||
The legacy example Grafana dashboard is available at [examples/dashboard-legacy.json](https://github.com/argoproj/argo-cd/blob/master/examples/dashboard-legacy.json).
|
||||
|
||||
#### Known issues
|
||||
Last-minute bugs that will be addressed in 1.5.1 shortly:
|
||||
|
||||
* https://github.com/argoproj/argo-cd/issues/3336
|
||||
* https://github.com/argoproj/argo-cd/issues/3319
|
||||
* https://github.com/argoproj/argo-cd/issues/3339
|
||||
* https://github.com/argoproj/argo-cd/issues/3358
|
||||
|
||||
#### Enhancements
|
||||
* feat: support helm3 (#2383) (#3178)
|
||||
* feat: Argo CD Service Account / Local Users #3185
|
||||
* feat: Disable Admin Login (fixes #3019) (#3179)
|
||||
* feat(ui): add docs to sync policy options present in create application panel (Close #3098) (#3203)
|
||||
* feat: add "service-account" flag to "cluster add" command (#3183) (#3184)
|
||||
* feat: Supports the validate-false option at an app level. Closes #1063 (#2542)
|
||||
* feat: add dest cluster and namespace in the Events (#3093)
|
||||
* feat: Rollback disables auto sync issue #2441 (#2591)
|
||||
* feat: allow ssh and http repository references in bitbucketserver webhook #2773 (#3036)
|
||||
* feat: Add helm --set-file support (#2751)
|
||||
* feat: Include resource group for Event's InvolvedObject.APIVersion
|
||||
* feat: Add argocd cmd for Windows #2121 (#3015)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- fix: app reconciliation fails with panic: index out of (#3233)
|
||||
- fix: upgrade argoproj/pkg version to fix leaked sensitive information in logs (#3230)
|
||||
- fix: set MaxCallSendMsgSize to MaxGRPCMessageSize for the GRPC caller (#3117)
|
||||
- fix: stop caching helm index (#3193)
|
||||
- fix: dex proxy should forward request to dex preserving the basehref (#3165)
|
||||
- fix: set default login redirect to baseHRef (#3164)
|
||||
- fix: don't double-prepend basehref to redirect URLs (fixes #3137)
|
||||
- fix: ui referring to /api/version using absolute path (#3092)
|
||||
- fix: Unhang UI on long app info items by using more sane URL match pattern (#3159)
|
||||
- fix: Allow multiple hostnames per SSH known hosts entry and also allow IPv6 (#2814) (#3074)
|
||||
- fix: argocd-util backup produced truncated backups. import app status (#3096)
|
||||
- fix: upgrade redis-ha chart and enable haproxy (#3147)
|
||||
- fix: make dex server deployment init container resilient to restarts (#3136)
|
||||
- fix: reduct secret values of manifests stored in git (#3088)
|
||||
- fix: labels not being deleted via UI (#3081)
|
||||
- fix: HTTP|HTTPS|NO_PROXY env variable reading #3055 (#3063)
|
||||
- fix: Correct usage text for repo add command regarding insecure repos (#3068)
|
||||
- fix: Ensure SSH private key is written out with a final newline character (#2890) (#3064)
|
||||
- fix: Handle SSH URLs in 'git@server:org/repo' notation correctly (#3062)
|
||||
- fix sso condition when several sso connectors has been configured (#3057)
|
||||
- fix: Fix bug where the same pointer is used. (#3059)
|
||||
- fix: Opening in new tab bad key binding on Linux (#3020)
|
||||
- fix: K8s secrets for repository credential templates are not deleted when credential template is deleted (#3028)
|
||||
- fix: SSH credential template not working #3016
|
||||
- fix: Unable to parse kubectl pre-release version strings (#3034)
|
||||
- fix: Jsonnet TLA parameters of same type are overwritten (#3022)
|
||||
- fix: Replace aws-iam-authenticator to support IRSA (#3010)
|
||||
- fix: Hide bindPW in dex config (#3025)
|
||||
- fix: SSH repo URL with a user different from `git` is not matched correctly when resolving a webhook (#2988)
|
||||
- fix: JWT invalid => Password for superuser has changed since token issued (#2108)
|
||||
|
||||
#### Contributors
|
||||
* alexandrfox
|
||||
* alexec
|
||||
* alexmt
|
||||
* bergur88
|
||||
* CBytelabs
|
||||
* dbeal-wiser
|
||||
* dnascimento
|
||||
* Elgarni
|
||||
* eSamS
|
||||
* gpaul
|
||||
* jannfis
|
||||
* jdmulloy
|
||||
* machgo
|
||||
* masa213f
|
||||
* matthyx
|
||||
* rayanebel
|
||||
* shelby-moore
|
||||
* tomcruise81
|
||||
* wecger
|
||||
* zeph
|
||||
|
||||
## v1.4.2 (2020-01-24)
|
||||
|
||||
- fix: correctly replace cache in namespace isolation mode (#3023)
|
||||
|
||||
## v1.4.1 (2020-01-23)
|
||||
|
||||
- fix: impossible to config RBAC if group name includes ',' (#3013)
|
||||
|
||||
## v1.4.0 (2020-01-17)
|
||||
|
||||
The v1.4.0 is a stability release that brings multiple bug fixes, security, performance enhancements, and multiple usability improvements.
|
||||
|
||||
#### New Features
|
||||
|
||||
#### Security
|
||||
A number of security enhancements and features have been implemented (thanks to [@jannfis](https://github.com/jannfis) for driving it! ):
|
||||
* **Repository Credential Templates Management UI/CLI**. Now you can use Argo CD CLI or UI to configure
|
||||
[credentials template](https://argoproj.github.io/argo-cd/user-guide/private-repositories/#credential-templates) for multiple repositories!
|
||||
* **X-Frame-Options header on serving static assets**. The X-Frame-Options prevents third party sites to trick users into interacting with the application.
|
||||
* **Tighten AppProject RBAC enforcement**. We've improved the enforcement of access rules specified in the
|
||||
[application project](https://argoproj.github.io/argo-cd/operator-manual/declarative-setup/#projects) configuration.
|
||||
|
||||
#### Namespace Isolation
|
||||
With the namespace isolation feature, you are no longer have to give full read-only cluster access to the Argo CD. Instead, you can give access only to selected namespaces with-in
|
||||
the cluster:
|
||||
|
||||
```bash
|
||||
argocd cluster add <mycluster> --namespace <mynamespace1> --namespace <mynamespace2>
|
||||
```
|
||||
|
||||
This feature is useful if you don't have full cluster access but still want to use Argo CD to manage some cluster namespaces. The feature also improves performance if Argo CD is
|
||||
used to manage a few namespaces of a large cluster.
|
||||
|
||||
#### Reconciliation Performance
|
||||
The Argo CD no longer fork/exec `kubectl` to apply resource changes in the target cluster or convert resource manifest to the required manifest version. This reduces
|
||||
CPU and Memory usage of large Argo CD instances.
|
||||
|
||||
#### Resources Health based Hook Status
|
||||
The existing Argo CD [resource hooks](https://argoproj.github.io/argo-cd/user-guide/resource_hooks/) feature allows running custom logic during the syncing process. You can mark
|
||||
any Kubernetes resource as a hook and Argo CD assess hook status if resource is a `Pod`, `Job` or `Argo Workflow`. In the v1.4.0 release Argo CD is going to leverage resource
|
||||
[health assessment](https://argoproj.github.io/argo-cd/operator-manual/health/) to get sync hook status. This allows using any custom CRD as a sync hook and leverage custom health
|
||||
check logic.
|
||||
|
||||
#### Manifest Generation
|
||||
* **Track Helm Charts By Semantic Version**. You've been able to track charts hosted in Git repositories using branches to tags. This is now possible for Helm charts. You no longer
|
||||
need to choose the exact version, such as v1.4.0 ,instead you can use a semantic version constraint such as v1.4.* and the latest version that matches will be installed.
|
||||
* **Build Environment Variables**. Feature allows config management tool to get access to app details during manifest generation via
|
||||
[environment variables](https://argoproj.github.io/argo-cd/user-guide/build-environment/).
|
||||
* **Git submodules**. Argo CD is going to automatically fetch sub-modules if your repository has `.gitmodules` directory.
|
||||
|
||||
#### UI and CLI
|
||||
* **Improved Resource Tree View**. The Application details page got even prettier. The resource view was tuned to fit more resources into the screen, include more information about
|
||||
each resource and don't lose usability at the same time.
|
||||
* **New Account Management CLI Command**. The CLI allows to check which actions are allowed for your account: `argocd account can-i sync applications '*'`
|
||||
|
||||
#### Maintenance Tools
|
||||
The team put more effort into building tools that help to maintain Argo CD itself:
|
||||
* **Bulk Project Editing**. The `argocd-util` allows to add and remove permissions defined in multiple project roles using one command.
|
||||
* **More Prometheus Metrics**. A set of additional metrics that contains useful information managed clusters is exposed by application controller.
|
||||
|
||||
More documentation and tools are coming in patch releases.
|
||||
|
||||
#### Breaking Changes
|
||||
|
||||
The Argo CD deletes all **in-flight** hooks if you terminate running sync operation. The hook state assessment change implemented in this release the Argo CD enables detection of
|
||||
an in-flight state for all Kubernetes resources including `Deployment`, `PVC`, `StatefulSet`, `ReplicaSet` etc. So if you terminate the sync operation that has, for example,
|
||||
`StatefulSet` hook that is `Progressing` it will be deleted. The long-running jobs are not supposed to be used as a sync hook and you should consider using
|
||||
[Sync Waves](https://argoproj.github.io/argo-cd/user-guide/sync-waves/) instead.
|
||||
|
||||
#### Enhancements
|
||||
* feat: Add custom healthchecks for cert-manager v0.11.0 (#2689)
|
||||
* feat: add git submodule support (#2495)
|
||||
* feat: Add repository credential management API and CLI (addresses #2136) (#2207)
|
||||
* feat: add support for --additional-headers cli flag (#2467)
|
||||
* feat: Add support for ssh-with-port repo url (#2866) (#2948)
|
||||
* feat: Add Time to ApplicationCondition. (#2417)
|
||||
* feat: Adds `argocd auth can-i` command. Close #2255
|
||||
* feat: Adds revision history limit. Closes #2790 (#2818)
|
||||
* feat: Adds support for ARGO_CD_[TARGET_REVISION|REVISION] and pass to Custom Tool/Helm/Jsonnet
|
||||
* feat: Adds support for Helm charts to be a semver range. Closes #2552 (#2606)
|
||||
* feat: Adds tracing to key external invocations. (#2811)
|
||||
* feat: argocd-util should allow editing project policies in bulk (#2615)
|
||||
* feat: Displays controllerrevsion's revision in the UI. Closes #2306 (#2702)
|
||||
* feat: Issue #2559 - Add gauge Prometheus metric which represents the number of pending manifest requests. (#2658)
|
||||
* feat: Make ConvertToVersion maybe 1090% faster on average (#2820)
|
||||
* feat: namespace isolation (#2839)
|
||||
* feat: removes redundant mutex usage in controller cache and adds cluster cache metrics (#2898)
|
||||
* feat: Set X-Frame-Options on serving static assets (#2706) (#2711)
|
||||
* feat: Simplify using Argo CD without users/SSO/UI (#2688)
|
||||
* feat: Template Out Data Source in Grafana Dashboard (#2859)
|
||||
* feat: Updates UI icons. Closes #2625 and #2757 (#2653)
|
||||
* feat: use editor arguments in InteractiveEditor (#2833)
|
||||
* feat: Use kubectl apply library instead of forking binary (#2861)
|
||||
* feat: use resource health for hook status evaluation (#2938)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- fix: Adds support for /api/v1/account* via HTTP. Fixes #2664 (#2701)
|
||||
- fix: Allow '@'-character in SSH usernames when connecting a repository (#2612)
|
||||
- fix: Allow dot in project policy. Closes #2724 (#2755)
|
||||
- fix: Allow you to sync local Helm apps. Fixes #2741 (#2747)
|
||||
- fix: Allows Helm parameters that contains arrays or maps. (#2525)
|
||||
- fix: application-controller doesn't deal with rm/add same cluster gracefully (x509 unknown) (#2389)
|
||||
- fix: diff local ignore kustomize build options (#2942)
|
||||
- fix: Ensures that Helm charts are correctly resolved before sync. Fixes #2758 (#2760)
|
||||
- fix: Fix 'Open application' link when using basehref (#2729)
|
||||
- fix: fix a bug with cluster add when token secret is not first in list. (#2744)
|
||||
- fix: fix bug where manifests are not cached. Fixes #2770 (#2771)
|
||||
- fix: Fixes bug whereby retry does not work for CLI. Fixes #2767 (#2768)
|
||||
- fix: git contention leads applications into Unknown state (#2877)
|
||||
- fix: Issue #1944 - Gracefully handle missing cached app state (#2464)
|
||||
- fix: Issue #2668 - Delete a specified context (#2669)
|
||||
- fix: Issue #2683 - Make sure app update don't fail due to concurrent modification (#2852)
|
||||
- fix: Issue #2721 Optimize helm repo querying (#2816)
|
||||
- fix: Issue #2853 - Improve application env variables/labels editing (#2856)
|
||||
- fix: Issue 2848 - Application Deployment history panel shows incorrect info for recent releases (#2849)
|
||||
- fix: Make BeforeHookCreation the default. Fixes #2754 (#2759)
|
||||
- fix: No error on `argocd app create` in CLI if `--revision` is omitted #2665
|
||||
- fix: Only delete resources during app delete cascade if permitted to (fixes #2693) (#2695)
|
||||
- fix: prevent user from seeing/deleting resources not permitted in project (#2908) (#2910)
|
||||
- fix: self-heal should retry syncing an application after specified delay
|
||||
- fix: stop logging dex config secrets #(2904) (#2937)
|
||||
- fix: stop using jsondiffpatch on clientside to render resource difference (#2869)
|
||||
- fix: Target Revision truncated #2736
|
||||
- fix: UI should re-trigger SSO login if SSO JWT token expires (#2891)
|
||||
- fix: update argocd-util import was not working properly (#2939)
|
||||
|
||||
#### Contributors
|
||||
|
||||
* Aalok Ahluwalia
|
||||
* Aananth K
|
||||
* Abhishek Jaisingh
|
||||
* Adam Johnson
|
||||
* Alan Tang
|
||||
* Alex Collins
|
||||
* Alexander Matyushentsev
|
||||
* Andrew Waters
|
||||
* Byungjin Park
|
||||
* Christine Banek
|
||||
* Daniel Helfand
|
||||
* David Hong
|
||||
* David J. M. Karlsen
|
||||
* David Maciel
|
||||
* Devan Goodwin
|
||||
* Devin Stein
|
||||
* dthomson25
|
||||
* Gene Liverman
|
||||
* Gregor Krmelj
|
||||
* Guido Maria Serra
|
||||
* Ilir Bekteshi
|
||||
* Imran Ismail
|
||||
* INOUE BANJI
|
||||
* Isaac Gaskin
|
||||
* jannfis
|
||||
* Jeff Hastings
|
||||
* Jesse Suen
|
||||
* John Girvan
|
||||
* Konstantin
|
||||
* Lev Aminov
|
||||
* Manatsawin Hanmongkolchai
|
||||
* Marco Schmid
|
||||
* Masayuki Ishii
|
||||
* Michael Bridgen
|
||||
* Naoki Oketani
|
||||
* niqdev
|
||||
* nitinpatil1992
|
||||
* Olivier Boukili
|
||||
* Olivier Lemasle
|
||||
* Omer Kahani
|
||||
* Paul Brit
|
||||
* Qingbo Zhou
|
||||
* Saradhi Sreegiriraju
|
||||
* Scott Cabrinha
|
||||
* shlo
|
||||
* Simon Behar
|
||||
* stgarf
|
||||
* Yujun Zhang
|
||||
* Zoltán Reegn
|
||||
|
||||
## v1.3.4 (2019-12-05)
|
||||
- #2819 Fixes logging of tracing option in CLI
|
||||
|
||||
## v1.3.3 (2019-12-05)
|
||||
- #2721 High CPU utilisation (5 cores) and spammy logs
|
||||
|
||||
## v1.3.2 (2019-12-03)
|
||||
- #2797 Fix directory traversal edge case and enhance tests
|
||||
|
||||
## v1.3.1 (2019-12-02)
|
||||
- #2664 update account password from API resulted 404
|
||||
- #2724 Can't use `DNS-1123` compliant app name when creating project role
|
||||
- #2726 App list does not show chart for Helm app
|
||||
- #2741 argocd local sync cannot parse kubernetes version
|
||||
- #2754 BeforeHookCreation should be the default hook
|
||||
- #2767 Fix bug whereby retry does not work for CLI
|
||||
- #2770 Always cache miss for manifests
|
||||
- #1345 argocd-application-controller: can not retrieve list of objects using index : Index with name namespace does not exist
|
||||
|
||||
## v1.3.0 (2019-11-13)
|
||||
|
||||
#### New Features
|
||||
|
||||
@@ -22,7 +560,9 @@ https://youtu.be/GP7xtrnNznw
|
||||
|
||||
##### Orphan Resources
|
||||
|
||||
Some users would like to make sure that resources in a namespace are managed only by Argo CD. So we've introduced the concept of an "orphan resource" - any resource that is in namespace associated with an app, but not managed by Argo CD. This is enable in the project settings. Once enabled, Argo CD will show in the app view any resources in the app's namepspace that is not mananged by Argo CD.
|
||||
Some users would like to make sure that resources in a namespace are managed only by Argo CD. So we've introduced the concept of an "orphan resource" - any resource that is in namespace associated with an app, but not managed by Argo CD. This is enabled in the project settings. Once enabled, Argo CD will show in the app view any resources in the app's namespace that is not managed by Argo CD.
|
||||
|
||||
https://youtu.be/9ZoTevVQf5I
|
||||
|
||||
##### Sync Windows
|
||||
|
||||
@@ -30,137 +570,110 @@ There may be instances when you want to control the times during which an Argo C
|
||||
|
||||
#### Enhancements
|
||||
|
||||
* Issue #2396 argocd list command should have filter options like by pr… (#2421)
|
||||
* Adds support for Helm 1st-class. Closes #1145 (#1865)
|
||||
* Issue #1167 - Implement orphan resources support (#2103)
|
||||
* Helm hooks. Closes #355 (#2069)
|
||||
* Adds support for a literal YAML block of Helm values. Closes #1930 (#2057)
|
||||
* Adds support for hook-delete-policy: BeforeHookCreation. Closes #2036 (#2048)
|
||||
* Adds support for setting Helm string parameters via CLI. Closes #2078 (#2109)
|
||||
* [UI] Add application labels to Applications list and Applications details page (#1099)
|
||||
* Helm repository as first class Argo CD Application source (#1145)
|
||||
* Ability to generate a warn/alert when a namespace deviates from the expected state (#1167)
|
||||
* Improve diff support for resource requests/limits (#1615)
|
||||
* HTTP API should allow JWT to be passed via Authorization header (#1642)
|
||||
* Ability to create & upsert projects from spec (#1852)
|
||||
* Support for in-line block from helm chart values (#1930)
|
||||
* Request OIDC groups claim if groups scope is not supported (#1956)
|
||||
* Add a maintenance window for Applications with automated syncing (#1995)
|
||||
* Support `argocd.argoproj.io/hook-delete-policy: BeforeHookCreation` (#2036)
|
||||
* Support setting Helm string parameters using CLI/UI (#2078)
|
||||
* Config management plugin environment variable UI/CLI support (#2203)
|
||||
* Helm: auto-detect URLs (#2260)
|
||||
* Helm: UI improvements (#2261)
|
||||
* Support `helm template --kube-version ` (#2275)
|
||||
* Use community icons for resources (#2277)
|
||||
* Make `group` optional for `ignoreDifferences` config (#2298)
|
||||
* Update Helm docs (#2315)
|
||||
* Add cluster information into Splunk (#2354)
|
||||
* argocd list command should have filter options like by project (#2396)
|
||||
* Add target/current revision to status badge (#2445)
|
||||
* Update tooling to use Kustomize v3 (#2487)
|
||||
* Update root `Dockerfile` to use the `hack/install.sh` (#2488)
|
||||
* Support and document using HPA for repo-server (#2559)
|
||||
* Upgrade Helm (#2587)
|
||||
* UI fixes for "Sync Apps" panel. (#2604)
|
||||
* Upgrade kustomize from v3.1.0 to v3.2.1 (#2609)
|
||||
* Map helm lifecycle hooks to ArgoCD pre/post/sync hooks (#355)
|
||||
* [UI] Enhance app creation page with Helm parameters overrides (#1059)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- Issue #2484 - Impossible to edit chart name using App details page (#2485)
|
||||
- Issue #2185 - Manual sync don't trigger hooks (#2477)
|
||||
- Issue #2453 - Application controller sometimes accidentally removes duplicated/excluded resource warning condition (#2454)
|
||||
- Issue #1944 - Gracefully handle missing cached app state (#2464)
|
||||
- Issue #2321 - Hook deletion should not fail if error message is not found (#2458)
|
||||
- Issue #2448 - Custom resource actions cannot be executed from the UI (#2449)
|
||||
- Issue #2339 - Make sure controller uses latest git version if app reconciliation result expired (#2346)
|
||||
- Issue #2290 - Fix nil pointer dereference in application controller (#2291)
|
||||
- Issue #2245 - Intermittent "git ls-remote" request failures should not fail app reconciliation (#2281)
|
||||
- Issue #2022 - Support limiting number of concurrent kubectl fork/execs (#2264)
|
||||
- Fix degraded proxy support for http(s) git repository (#2243) (#2249)
|
||||
- Issue #2198 - Print empty string instead of Unknown in 'argocd app sync' output (#2223)
|
||||
- Fix for displaying hooks in app diff view. Fixes #2215 (#2218)
|
||||
- Issue #2212 - Correctly handle trailing slash in configured URL while creating redirect URL (#2214)
|
||||
- Deals with race condition when deleting resource. Closes #2141 (#2200)
|
||||
- Issue #2192 - SyncError app condition disappears during app reconciliation (#2193)
|
||||
- Adds test for updating immutable field, adds UI button to allow force from UI. See #2150 (#2155)
|
||||
- Issue #2174 - Fix git repo url parsing on application list view (#2175)
|
||||
- Issue #2146 - Fix nil pointer dereference error during app reconciliation (#2170)
|
||||
- Issue #2114 - Fix history api fallback implementation to support app names with dots (#2168)
|
||||
- Issue #2060 - Endpoint incorrectly considered top level managed resource (#2129)
|
||||
- Fixed truncation of group in UI. Closes #2006 (#2128)
|
||||
- Allow adding certs for hostnames ending on a dot (fixes #2116) (#2120)
|
||||
- Escape square brackets in pattern matching hostnames (fixes #2099) (#2113)
|
||||
- failed parsing on parameters with comma (#1660)
|
||||
- Statefulset with OnDelete Update Strategy stuck progressing (#1881)
|
||||
- Warning during secret diffing (#1923)
|
||||
- Error message "Unable to load data: key is missing" is confusing (#1944)
|
||||
- OIDC group bindings are truncated (#2006)
|
||||
- Multiple parallel app syncs causes OOM (#2022)
|
||||
- Unknown error when setting params with argocd app set on helm app (#2046)
|
||||
- Endpoint is no longer shown as a child of services (#2060)
|
||||
- SSH known hosts entry cannot be deleted if contains shell pattern in name (#2099)
|
||||
- Application 404s on names with periods (#2114)
|
||||
- Adding certs for hostnames ending with a dot (.) is not possible (#2116)
|
||||
- Fix `TestHookDeleteBeforeCreation` (#2141)
|
||||
- v1.2.0-rc1 nil pointer dereference when syncing (#2146)
|
||||
- Replacing services failure (#2150)
|
||||
- 1.2.0-rc1 - Authentication Required error in Repo Server (#2152)
|
||||
- v1.2.0-rc1 Applications List View doesn't work (#2174)
|
||||
- Manual sync does not trigger Presync hooks (#2185)
|
||||
- SyncError app condition disappears during app reconciliation (#2192)
|
||||
- argocd app wait\sync prints 'Unknown' for resources without health (#2198)
|
||||
- 1.2.0-rc2 Warning during secret diffing (#2206)
|
||||
- SSO redirect url is incorrect if configured Argo CD URL has trailing slash (#2212)
|
||||
- Application summary diff page shows hooks (#2215)
|
||||
- An app with a single resource and Sync hook remains progressing (#2216)
|
||||
- CONTRIBUTING documentation outdated (#2231)
|
||||
- v1.2.0-rc2 does not retrieve http(s) based git repository behind the proxy (#2243)
|
||||
- Intermittent "git ls-remote" request failures should not fail app reconciliation (#2245)
|
||||
- Result of ListApps operation for Git repo is cached incorrectly (#2263)
|
||||
- ListApps does not utilize cache (#2287)
|
||||
- Controller panics due to nil pointer error (#2290)
|
||||
- The Helm --kube-version support does not work on GKE: (#2303)
|
||||
- Fixes bug that prevents you creating repos via UI/CLI. (#2308)
|
||||
- The 'helm.repositories' settings is dropped without migration path (#2316)
|
||||
- Badge response does not contain cache control header (#2317)
|
||||
- Inconsistent sync result from UI and CLI (#2321)
|
||||
- Failed edit application with plugin type requiring environment (#2330)
|
||||
- AutoSync doesn't work anymore (#2339)
|
||||
- End-to-End tests not working with Kubernetes v1.16 (#2371)
|
||||
- Creating an application from Helm repository should select "Helm" as source type (#2378)
|
||||
- The parameters of ValidateAccess GRPC method should not be logged (#2386)
|
||||
- Maintenance window meaning is confusing (#2398)
|
||||
- UI bug when targetRevision is omitted (#2407)
|
||||
- Too many vulnerabilities in Docker image (#2425)
|
||||
- proj windows commands not consistent with other commands (#2443)
|
||||
- Custom resource actions cannot be executed from the UI (#2448)
|
||||
- Application controller sometimes accidentally removes duplicated/excluded resource warning condition (#2453)
|
||||
- Logic that checks sync windows state in the cli is incorrect (#2455)
|
||||
- UI don't allow to create window with `* * * * *` schedule (#2475)
|
||||
- Helm Hook is executed twice if annotated with both pre-install and pre-upgrade annotations (#2480)
|
||||
- Impossible to edit chart name using App details page (#2484)
|
||||
- ArgoCD does not provide CSRF protection (#2496)
|
||||
- ArgoCD failing to install CRDs in master from Helm Charts (#2497)
|
||||
- Timestamp in Helm package file name causes error in Application with Helm source (#2549)
|
||||
- Attempting to create a repo with password but not username panics (#2567)
|
||||
- UI incorrectly mark resources as `Required Pruning` (#2577)
|
||||
- argocd app diff prints only first difference (#2616)
|
||||
- Bump min client cache version (#2619)
|
||||
- Cluster list page fails if any cluster is not reachable (#2620)
|
||||
- Repository type should be mandatory for repo add command in CLI (#2622)
|
||||
- Repo server executes unnecessary ls-remotes (#2626)
|
||||
- Application list page incorrectly filter apps by label selector (#2633)
|
||||
- Custom actions are disabled in Argo CD UI (#2635)
|
||||
- Failure of `argocd version` in the self-building container image (#2645)
|
||||
- Application list page is not updated automatically anymore (#2655)
|
||||
- Login regression issues (#2659)
|
||||
- Regression: Cannot return Kustomize version for 3.1.0 (#2662)
|
||||
- API server does not allow creating role with action `action/*` (#2670)
|
||||
- Application controller `kubectl-parallelism-limit` flag is broken (#2673)
|
||||
- Annoying toolbar flickering (#2691)
|
||||
|
||||
#### Other
|
||||
## v1.2.5 (2019-10-29)
|
||||
|
||||
- Fix possible path traversal attack when supporting Helm `values.yaml` (#2452)
|
||||
- Fix UI crash on application list page (#2490)
|
||||
- add support for --additional-headers cli flag (#2467)
|
||||
- Allow collapse/expand helm values text (#2469)
|
||||
- Update base image to Debian buster (#2431)
|
||||
- Error with new `actions run` suggestion (#2434)
|
||||
- Detach ArgoCD from specific workflow API (#2428)
|
||||
- Add application labels to Applications list and Applications details page (#2430)
|
||||
- Fix JS error on application creation page if no plugins configured (#2432)
|
||||
- Add missing externalURL for networking.k8s.io Ingress type (#2390)
|
||||
- App status panel shows metadata of current revision in git instead of most recent reconciled revision (#2419)
|
||||
- Adds support for plugin params. (#2406)
|
||||
- Granular RBAC Support for actions (#2110)
|
||||
- Added Kustomize, Helm, and Kubectl to `argocd version` (#2329)
|
||||
- Stop unnecessary re-loading clusters on every app list page re-render (#2411)
|
||||
- Add project level maintenance windows for applications (#2380)
|
||||
- Make argo-cd docker images openshift friendly (#2362)
|
||||
- Add dest-server and dest-namespace field to reconciliation logs (#2388)
|
||||
- Add custom action example to argocd-cm.yaml (#2375)
|
||||
- Try out community icons. (#2349)
|
||||
- Make `group` optional for `ignoreDifferences` setting (#2335)
|
||||
- Adds support for Github Enterprise URLs (#2344)
|
||||
- Add argocd project as variable to grafana dashboard (#2336)
|
||||
- Fix missing envs when updating application of content management plugin type (#2331)
|
||||
- util/localconfig: prefer HOME env var over os/user (#2326)
|
||||
- Auto-detect Helm repos + support Helm basic auth + fix bugs (#2309)
|
||||
- Add cache-control HTTP header to badge response (#2328)
|
||||
- Document flags/env variables useful for performance tuning (#2312)
|
||||
- Re-enable caching when listing apps. (#2295)
|
||||
- Fixes bug in `argocd repo list` and tidy up UI (#2307)
|
||||
- Add restart action to Deployment/StatefulSet/DaemonSet (#2300)
|
||||
- Clean-up the kube-version from Helm so that we can support GKE. (#2304)
|
||||
- Fixes issue diffing secrets (#2271)
|
||||
- Add --self-heal flag to argocd cli (#2296)
|
||||
- Support --kube-version. (#2276)
|
||||
- Fix building error when following CONTRIBUTING.md (#2278)
|
||||
- Adding information to make local execution more accessible (#2279)
|
||||
- API clients may use the HTTP Authorization header for auth. (#2262)
|
||||
- Fix TestAutoSyncSelfHealEnabled test flakiness (#2282)
|
||||
- Change Helm repo URLs to argoproj/argo-cd/master (#2266)
|
||||
- Fix/grafana datasources (#2229)
|
||||
- If there is only one wave and no pre/post hooks, we should be synced.… (#2217)
|
||||
- Create projects from manifests (#2202)
|
||||
- Fix JS crash in EditablePanel component (#2222)
|
||||
- Use same /24 network for testing immutable field update (#2213)
|
||||
- Add path to externalURLs (#2208)
|
||||
- support OIDC claims request (#1957)
|
||||
- Better detection for authorization_code OIDC response type (#2164)
|
||||
- Allow list actions to return yaml or json (#1805)
|
||||
- Adds a floating action button with help and chat links to every page.… (#2125)
|
||||
- Temporary disable Git LFS test to unblock release (#2172)
|
||||
- Determine the manifest version from the VERSION file when on release branch (#2166)
|
||||
- Enhances cookie warning with actual length to help users fix their co… (#2134)
|
||||
- Fixed routing issue for periods (#2162)
|
||||
- Added more health filters in UI (#2160)
|
||||
- Added 'SyncFail' to possible HookTypes in UI (#2153)
|
||||
- Indicate that `SyncFail` hooks are on v1.2+ (#2149)
|
||||
- Adds checks around valid paths for apps (#2133)
|
||||
- Minor CLI bug fixes (#2132)
|
||||
- Adds support for a literal YAML block of Helm values. Closes #1930 (#2057)
|
||||
- Fixed truncation of group in UI. Closes #2006 (#2128)
|
||||
- Redact secrets using "+" rather than "*" as this is base 64 compatiba… (#2119)
|
||||
|
||||
#### Contributors
|
||||
|
||||
* Aalok Ahluwalia <!-- num=1 -->
|
||||
* Adam Johnson <!-- num=4 -->
|
||||
* Alex Collins <!-- num=62 -->
|
||||
* Alexander Matyushentsev <!-- num=58 -->
|
||||
* Andrew Waters <!-- num=2 -->
|
||||
* Ben Doyle <!-- num=1 -->
|
||||
* Chris Jones <!-- num=1 -->
|
||||
* Fred Dubois <!-- num=1 -->
|
||||
* Gregor Krmelj <!-- num=2 -->
|
||||
* Gustav Paul <!-- num=2 -->
|
||||
* Isaac Gaskin <!-- num=1 -->
|
||||
* Jesse Suen <!-- num=1 -->
|
||||
* John Reese <!-- num=1 -->
|
||||
* Mitz Amano <!-- num=1 -->
|
||||
* Olivier Boukili <!-- num=1 -->
|
||||
* Olivier Lemasle <!-- num=1 -->
|
||||
* Rayyis <!-- num=1 -->
|
||||
* Rodolphe Prin <!-- num=1 -->
|
||||
* Ryota <!-- num=2 -->
|
||||
* Seiya Muramatsu <!-- num=1 -->
|
||||
* Simon Behar <!-- num=11 -->
|
||||
* Sverre Boschman <!-- num=1 -->
|
||||
* Tom Wieczorek <!-- num=3 -->
|
||||
* Yujun Zhang <!-- num=4 -->
|
||||
* Zoltán Reegn <!-- num=1 -->
|
||||
* agabet <!-- num=1 -->
|
||||
* dthomson25 <!-- num=2 -->
|
||||
* jannfis <!-- num=8 -->
|
||||
* ssbtn <!-- num=2 -->
|
||||
- Issue #2339 - Don't update `status.reconciledAt` unless compared with latest git version (#2581)
|
||||
|
||||
## v1.2.4 (2019-10-23)
|
||||
|
||||
@@ -223,7 +736,7 @@ Support for Git LFS enabled repositories - now you can store Helm charts as tar
|
||||
+ Added 'SyncFail' to possible HookTypes in UI (#2147)
|
||||
+ Support for Git LFS enabled repositories (#1853)
|
||||
+ Server certificate and known hosts management (#1514)
|
||||
+ Client HTTPS certifcates for private git repositories (#1945)
|
||||
+ Client HTTPS certificates for private git repositories (#1945)
|
||||
+ Badge for application status (#1435)
|
||||
+ Make the health check for APIService a built in (#1841)
|
||||
+ Bitbucket Server and Gogs webhook providers (#1269)
|
||||
@@ -263,7 +776,7 @@ Support for Git LFS enabled repositories - now you can store Helm charts as tar
|
||||
- Fix history api fallback implementation to support app names with dots (#2114)
|
||||
- Fixes some code issues related to Kustomize build options. (#2146)
|
||||
- Adds checks around valid paths for apps (#2133)
|
||||
- Enpoint incorrectly considered top level managed resource (#2060)
|
||||
- Endpoint incorrectly considered top level managed resource (#2060)
|
||||
- Allow adding certs for hostnames ending on a dot (#2116)
|
||||
|
||||
#### Other
|
||||
@@ -586,7 +1099,7 @@ Argo CD introduces some additional CLI commands:
|
||||
#### Label selector changes, dex-server rename
|
||||
|
||||
The label selectors for deployments were been renamed to use kubernetes common labels
|
||||
(`app.kuberentes.io/name=NAME` instead of `app=NAME`). Since K8s deployment label selectors are
|
||||
(`app.kubernetes.io/name=NAME` instead of `app=NAME`). Since K8s deployment label selectors are
|
||||
immutable, during an upgrade from v0.11 to v0.12, the old deployments should be deleted using
|
||||
`--cascade=false` which allows the new deployments to be created without introducing downtime.
|
||||
Once the new deployments are ready, the older replicasets can be deleted. Use the following
|
||||
@@ -683,7 +1196,7 @@ has a minimum client version of v0.12.0. Older CLI clients will be rejected.
|
||||
- Fix CRD creation/deletion handling (#1249)
|
||||
- Git cloning via SSH was not verifying host public key (#1276)
|
||||
- Fixed multiple goroutine leaks in controller and api-server
|
||||
- Fix isssue where `argocd app set -p` required repo privileges. (#1280)
|
||||
- Fix issue where `argocd app set -p` required repo privileges. (#1280)
|
||||
- Fix local diff of non-namespaced resources. Also handle duplicates in local diff (#1289)
|
||||
- Deprecated resource kinds from 'extensions' groups are not reconciled correctly (#1232)
|
||||
- Fix issue where CLI would panic after timeout when cli did not have get permissions (#1209)
|
||||
@@ -861,7 +1374,7 @@ which have a dependency to external helm repositories.
|
||||
|
||||
+ Allow more fine-grained sync (issue #508)
|
||||
+ Display init container logs (issue #681)
|
||||
+ Redirect to /auth/login instead of /login when SSO token is used for authenticaion (issue #348)
|
||||
+ Redirect to /auth/login instead of /login when SSO token is used for authentication (issue #348)
|
||||
+ Support ability to use a helm values files from a URL (issue #624)
|
||||
+ Support public not-connected repo in app creation UI (issue #426)
|
||||
+ Use ksonnet CLI instead of ksonnet libs (issue #626)
|
||||
@@ -1136,7 +1649,7 @@ RBAC policy rules, need to be rewritten to include one extra column with the eff
|
||||
+ Sync/Rollback/Delete is asynchronously handled by controller
|
||||
* Refactor CRUD operation on clusters and repos
|
||||
* Sync will always perform kubectl apply
|
||||
* Synced Status considers last-applied-configuration annotatoin
|
||||
* Synced Status considers last-applied-configuration annotation
|
||||
* Server & namespace are mandatory fields (still inferred from app.yaml)
|
||||
* Manifests are memoized in repo server
|
||||
- Fix connection timeouts to SSH repos
|
||||
|
||||
45
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.12.6 as builder
|
||||
FROM golang:1.14.1 as builder
|
||||
|
||||
RUN echo 'deb http://deb.debian.org/debian buster-backports main' >> /etc/apt/sources.list
|
||||
|
||||
@@ -25,14 +25,14 @@ WORKDIR /tmp
|
||||
|
||||
ADD hack/install.sh .
|
||||
ADD hack/installers installers
|
||||
ADD hack/tool-versions.sh .
|
||||
|
||||
RUN ./install.sh dep-linux
|
||||
RUN ./install.sh packr-linux
|
||||
RUN ./install.sh kubectl-linux
|
||||
RUN ./install.sh ksonnet-linux
|
||||
RUN ./install.sh helm2-linux
|
||||
RUN ./install.sh helm-linux
|
||||
RUN ./install.sh kustomize-linux
|
||||
RUN ./install.sh aws-iam-authenticator-linux
|
||||
|
||||
####################################################################################################
|
||||
# Argo CD Base - used as the base for both the release and dev argocd images
|
||||
@@ -50,16 +50,19 @@ 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 && \
|
||||
apt-get install -y git git-lfs python3-pip tini gpg && \
|
||||
apt-get clean && \
|
||||
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
|
||||
COPY --from=builder /usr/local/bin/kubectl /usr/local/bin/kubectl
|
||||
COPY --from=builder /usr/local/bin/kustomize /usr/local/bin/kustomize
|
||||
COPY --from=builder /usr/local/bin/aws-iam-authenticator /usr/local/bin/aws-iam-authenticator
|
||||
# script to add current (possibly arbitrary) user to /etc/passwd at runtime
|
||||
# (if it's not already there, to be openshift friendly)
|
||||
COPY uid_entrypoint.sh /usr/local/bin/uid_entrypoint.sh
|
||||
@@ -70,11 +73,15 @@ 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
|
||||
|
||||
####################################################################################################
|
||||
@@ -96,27 +103,26 @@ RUN NODE_ENV='production' yarn build
|
||||
####################################################################################################
|
||||
# Argo CD Build stage which performs the actual build of Argo CD binaries
|
||||
####################################################################################################
|
||||
FROM golang:1.12.6 as argocd-build
|
||||
FROM golang:1.14.1 as argocd-build
|
||||
|
||||
COPY --from=builder /usr/local/bin/dep /usr/local/bin/dep
|
||||
COPY --from=builder /usr/local/bin/packr /usr/local/bin/packr
|
||||
|
||||
# A dummy directory is created under $GOPATH/src/dummy so we are able to use dep
|
||||
# to install all the packages of our dep lock file
|
||||
COPY Gopkg.toml ${GOPATH}/src/dummy/Gopkg.toml
|
||||
COPY Gopkg.lock ${GOPATH}/src/dummy/Gopkg.lock
|
||||
WORKDIR /go/src/github.com/argoproj/argo-cd
|
||||
|
||||
RUN cd ${GOPATH}/src/dummy && \
|
||||
dep ensure -vendor-only && \
|
||||
mv vendor/* ${GOPATH}/src/ && \
|
||||
rmdir vendor
|
||||
COPY go.mod go.mod
|
||||
COPY go.sum go.sum
|
||||
|
||||
RUN go mod download
|
||||
|
||||
# Perform the build
|
||||
WORKDIR /go/src/github.com/argoproj/argo-cd
|
||||
COPY . .
|
||||
RUN make cli server controller repo-server argocd-util && \
|
||||
make CLI_NAME=argocd-darwin-amd64 GOOS=darwin cli
|
||||
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-local && \
|
||||
make CLI_NAME=argocd-windows-amd64.exe GOOS=windows cli-local \
|
||||
; fi
|
||||
|
||||
####################################################################################################
|
||||
# Final image
|
||||
@@ -124,4 +130,3 @@ RUN make cli server controller repo-server argocd-util && \
|
||||
FROM argocd-base
|
||||
COPY --from=argocd-build /go/src/github.com/argoproj/argo-cd/dist/argocd* /usr/local/bin/
|
||||
COPY --from=argocd-ui ./src/dist/app /shared/app
|
||||
|
||||
|
||||
2104
Gopkg.lock
generated
117
Gopkg.toml
@@ -1,117 +0,0 @@
|
||||
# Packages should only be added to the following list when we use them *outside* of our go code.
|
||||
# (e.g. we want to build the binary to invoke as part of the build process, such as in
|
||||
# generate-proto.sh). Normal use of golang packages should be added via `dep ensure`, and pinned
|
||||
# with a [[constraint]] or [[override]] when version is important.
|
||||
required = [
|
||||
"github.com/golang/protobuf/protoc-gen-go",
|
||||
"github.com/gogo/protobuf/protoc-gen-gofast",
|
||||
"github.com/gogo/protobuf/protoc-gen-gogofast",
|
||||
"k8s.io/code-generator/cmd/go-to-protobuf",
|
||||
"k8s.io/kube-openapi/cmd/openapi-gen",
|
||||
"github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway",
|
||||
"github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger",
|
||||
"golang.org/x/sync/errgroup",
|
||||
]
|
||||
|
||||
[[constraint]]
|
||||
name = "google.golang.org/grpc"
|
||||
version = "1.15.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gogo/protobuf"
|
||||
version = "1.3.1"
|
||||
|
||||
# override github.com/grpc-ecosystem/go-grpc-middleware's constraint on master
|
||||
[[override]]
|
||||
name = "github.com/golang/protobuf"
|
||||
version = "1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/grpc-ecosystem/grpc-gateway"
|
||||
version = "v1.3.1"
|
||||
|
||||
# prometheus does not believe in semversioning yet
|
||||
[[constraint]]
|
||||
name = "github.com/prometheus/client_golang"
|
||||
revision = "7858729281ec582767b20e0d696b6041d995d5e0"
|
||||
|
||||
[[override]]
|
||||
branch = "release-1.16"
|
||||
name = "k8s.io/api"
|
||||
|
||||
[[override]]
|
||||
branch = "release-1.16"
|
||||
name = "k8s.io/kubernetes"
|
||||
|
||||
[[override]]
|
||||
branch = "release-1.16"
|
||||
name = "k8s.io/code-generator"
|
||||
|
||||
[[override]]
|
||||
branch = "release-1.16"
|
||||
name = "k8s.io/apimachinery"
|
||||
|
||||
[[override]]
|
||||
branch = "release-1.16"
|
||||
name = "k8s.io/apiextensions-apiserver"
|
||||
|
||||
[[override]]
|
||||
branch = "release-1.16"
|
||||
name = "k8s.io/apiserver"
|
||||
|
||||
[[override]]
|
||||
branch = "release-1.16"
|
||||
name = "k8s.io/kubectl"
|
||||
|
||||
[[override]]
|
||||
branch = "release-1.16"
|
||||
name = "k8s.io/cli-runtime"
|
||||
|
||||
[[override]]
|
||||
version = "2.0.3"
|
||||
name = "sigs.k8s.io/kustomize"
|
||||
|
||||
# ASCIIRenderer does not implement blackfriday.Renderer
|
||||
[[override]]
|
||||
name = "github.com/russross/blackfriday"
|
||||
version = "1.5.2"
|
||||
|
||||
[[override]]
|
||||
branch = "release-13.0"
|
||||
name = "k8s.io/client-go"
|
||||
|
||||
[[override]]
|
||||
name = "github.com/casbin/casbin"
|
||||
version = "1.9.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "1.2.2"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gobuffalo/packr"
|
||||
version = "v1.11.0"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/argoproj/pkg"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/yudai/gojsondiff"
|
||||
|
||||
# Fixes: Could not introduce sigs.k8s.io/kustomize@v2.0.3, as it has a dependency on github.com/spf13/cobra with constraint ^0.0.2, which has no overlap with existing constraint 0.0.5 from (root)
|
||||
[[override]]
|
||||
name = "github.com/spf13/cobra"
|
||||
revision = "0.0.5"
|
||||
|
||||
# TODO: move off of k8s.io/kube-openapi and use controller-tools for CRD spec generation
|
||||
# (override argoproj/argo contraint on master)
|
||||
[[override]]
|
||||
revision = "30be4d16710ac61bce31eb28a01054596fe6a9f1"
|
||||
name = "k8s.io/kube-openapi"
|
||||
|
||||
# jsonpatch replace operation does not apply: doc is missing key: /metadata/annotations
|
||||
[[override]]
|
||||
name = "github.com/evanphx/json-patch"
|
||||
version = "v4.1.0"
|
||||
331
Makefile
@@ -3,16 +3,93 @@ 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 vendor/github.com/gobuffalo/packr/packr/main.go"; fi)
|
||||
VOLUME_MOUNT=$(shell [[ $(go env GOOS)=="darwin" ]] && echo ":delegated" || echo "")
|
||||
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 ":delegated"; else echo ""; fi)
|
||||
|
||||
define run-in-dev-tool
|
||||
docker run --rm -it -u $(shell id -u) -e HOME=/home/user -v ${CURRENT_DIR}:/go/src/github.com/argoproj/argo-cd${VOLUME_MOUNT} -w /go/src/github.com/argoproj/argo-cd argocd-dev-tools bash -c "GOPATH=/go $(1)"
|
||||
GOPATH?=$(shell if test -x `which go`; then go env GOPATH; else echo "$(HOME)/go"; fi)
|
||||
GOCACHE?=$(HOME)/.cache/go-build
|
||||
|
||||
DOCKER_SRCDIR?=$(GOPATH)/src
|
||||
DOCKER_WORKDIR?=/go/src/github.com/argoproj/argo-cd
|
||||
|
||||
ARGOCD_PROCFILE?=Procfile
|
||||
|
||||
# Configuration for building argocd-test-tools image
|
||||
TEST_TOOLS_NAMESPACE?=
|
||||
TEST_TOOLS_IMAGE=argocd-test-tools
|
||||
TEST_TOOLS_TAG?=latest
|
||||
ifdef TEST_TOOLS_NAMESPACE
|
||||
TEST_TOOLS_PREFIX=${TEST_TOOLS_NAMESPACE}/
|
||||
endif
|
||||
|
||||
# You can change the ports where ArgoCD components will be listening on by
|
||||
# setting the appropriate environment variables before running make.
|
||||
ARGOCD_E2E_APISERVER_PORT?=8080
|
||||
ARGOCD_E2E_REPOSERVER_PORT?=8081
|
||||
ARGOCD_E2E_REDIS_PORT?=6379
|
||||
ARGOCD_E2E_DEX_PORT?=5556
|
||||
ARGOCD_E2E_YARN_HOST?=localhost
|
||||
|
||||
ARGOCD_IN_CI?=false
|
||||
ARGOCD_TEST_E2E?=true
|
||||
|
||||
ARGOCD_LINT_GOGC?=20
|
||||
|
||||
# Runs any command in the argocd-test-utils container in server mode
|
||||
# Server mode container will start with uid 0 and drop privileges during runtime
|
||||
define run-in-test-server
|
||||
docker run --rm -it \
|
||||
--name argocd-test-server \
|
||||
-e USER_ID=$(shell id -u) \
|
||||
-e HOME=/home/user \
|
||||
-e GOPATH=/go \
|
||||
-e GOCACHE=/tmp/go-build-cache \
|
||||
-e ARGOCD_IN_CI=$(ARGOCD_IN_CI) \
|
||||
-e ARGOCD_E2E_TEST=$(ARGOCD_E2E_TEST) \
|
||||
-e ARGOCD_E2E_YARN_HOST=$(ARGOCD_E2E_YARN_HOST) \
|
||||
-v ${DOCKER_SRCDIR}:/go/src${VOLUME_MOUNT} \
|
||||
-v ${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} \
|
||||
-p ${ARGOCD_E2E_APISERVER_PORT}:8080 \
|
||||
-p 4000:4000 \
|
||||
$(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE):$(TEST_TOOLS_TAG) \
|
||||
bash -c "$(1)"
|
||||
endef
|
||||
|
||||
# Runs any command in the argocd-test-utils container in client mode
|
||||
define run-in-test-client
|
||||
docker run --rm -it \
|
||||
--name argocd-test-client \
|
||||
-u $(shell id -u) \
|
||||
-e HOME=/home/user \
|
||||
-e GOPATH=/go \
|
||||
-e ARGOCD_E2E_K3S=$(ARGOCD_E2E_K3S) \
|
||||
-e GOCACHE=/tmp/go-build-cache \
|
||||
-e ARGOCD_LINT_GOGC=$(ARGOCD_LINT_GOGC) \
|
||||
-v ${DOCKER_SRCDIR}:/go/src${VOLUME_MOUNT} \
|
||||
-v ${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_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)
|
||||
endef
|
||||
|
||||
PATH:=$(PATH):$(PWD)/hack
|
||||
@@ -24,6 +101,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} \
|
||||
@@ -38,6 +117,8 @@ endif
|
||||
ifneq (${GIT_TAG},)
|
||||
IMAGE_TAG=${GIT_TAG}
|
||||
LDFLAGS += -X ${PACKAGE}.gitTag=${GIT_TAG}
|
||||
else
|
||||
IMAGE_TAG?=latest
|
||||
endif
|
||||
|
||||
ifeq (${DOCKER_PUSH},true)
|
||||
@@ -53,34 +134,51 @@ endif
|
||||
.PHONY: all
|
||||
all: cli image argocd-util
|
||||
|
||||
.PHONY: gogen
|
||||
gogen:
|
||||
export GO111MODULE=off
|
||||
go generate ./util/argo/...
|
||||
|
||||
.PHONY: protogen
|
||||
protogen:
|
||||
export GO111MODULE=off
|
||||
./hack/generate-proto.sh
|
||||
|
||||
.PHONY: openapigen
|
||||
openapigen:
|
||||
export GO111MODULE=off
|
||||
./hack/update-openapi.sh
|
||||
|
||||
.PHONY: clientgen
|
||||
clientgen:
|
||||
export GO111MODULE=off
|
||||
./hack/update-codegen.sh
|
||||
|
||||
.PHONY: codegen-local
|
||||
codegen-local: protogen clientgen openapigen manifests-local
|
||||
codegen-local: mod-vendor-local gogen protogen clientgen openapigen manifests-local
|
||||
rm -rf vendor/
|
||||
|
||||
.PHONY: codegen
|
||||
codegen: dev-tools-image
|
||||
$(call run-in-dev-tool,make codegen-local)
|
||||
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
|
||||
go build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/${CLI_NAME} ./cmd/argocd
|
||||
|
||||
.PHONY: release-cli
|
||||
release-cli: clean-debug image
|
||||
docker create --name tmp-argocd-linux $(IMAGE_PREFIX)argocd:$(IMAGE_TAG)
|
||||
docker cp tmp-argocd-linux:/usr/local/bin/argocd ${DIST_DIR}/argocd-linux-amd64
|
||||
docker cp tmp-argocd-linux:/usr/local/bin/argocd-darwin-amd64 ${DIST_DIR}/argocd-darwin-amd64
|
||||
docker cp tmp-argocd-linux:/usr/local/bin/argocd-windows-amd64.exe ${DIST_DIR}/argocd-windows-amd64.exe
|
||||
docker rm tmp-argocd-linux
|
||||
|
||||
.PHONY: argocd-util
|
||||
@@ -88,17 +186,23 @@ argocd-util: clean-debug
|
||||
# Build argocd-util as a statically linked binary, so it could run within the alpine-based dex container (argoproj/argo-cd#844)
|
||||
CGO_ENABLED=0 ${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-util ./cmd/argocd-util
|
||||
|
||||
.PHONY: dev-tools-image
|
||||
dev-tools-image:
|
||||
cd hack && docker build -t argocd-dev-tools . -f Dockerfile.dev-tools
|
||||
# .PHONY: dev-tools-image
|
||||
# dev-tools-image:
|
||||
# docker build -t $(DEV_TOOLS_PREFIX)$(DEV_TOOLS_IMAGE) . -f hack/Dockerfile.dev-tools
|
||||
# docker tag $(DEV_TOOLS_PREFIX)$(DEV_TOOLS_IMAGE) $(DEV_TOOLS_PREFIX)$(DEV_TOOLS_IMAGE):$(DEV_TOOLS_VERSION)
|
||||
|
||||
.PHONY: test-tools-image
|
||||
test-tools-image:
|
||||
docker build -t $(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE) -f test/container/Dockerfile .
|
||||
docker tag $(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE) $(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE):$(TEST_TOOLS_TAG)
|
||||
|
||||
.PHONY: manifests-local
|
||||
manifests-local:
|
||||
./hack/update-manifests.sh
|
||||
|
||||
.PHONY: manifests
|
||||
manifests: dev-tools-image
|
||||
$(call run-in-dev-tool,make manifests-local IMAGE_TAG='${IMAGE_TAG}')
|
||||
manifests: test-tools-image
|
||||
$(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
|
||||
@@ -117,7 +221,7 @@ controller:
|
||||
|
||||
.PHONY: packr
|
||||
packr:
|
||||
go build -o ${DIST_DIR}/packr ./vendor/github.com/gobuffalo/packr/packr/
|
||||
go build -o ${DIST_DIR}/packr github.com/gobuffalo/packr/packr/
|
||||
|
||||
.PHONY: image
|
||||
ifeq ($(DEV_IMAGE), true)
|
||||
@@ -134,6 +238,7 @@ image: packr
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-util ./cmd/argocd-util
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd ./cmd/argocd
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-darwin-amd64 ./cmd/argocd
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-windows-amd64.exe ./cmd/argocd
|
||||
cp Dockerfile.dev dist
|
||||
docker build -t $(IMAGE_PREFIX)argocd:$(IMAGE_TAG) -f dist/Dockerfile.dev dist
|
||||
else
|
||||
@@ -142,54 +247,138 @@ image:
|
||||
endif
|
||||
@if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argocd:$(IMAGE_TAG) ; fi
|
||||
|
||||
.PHONY: armimage
|
||||
# The "BUILD_ALL_CLIS" argument is to skip building the CLIs for darwin and windows
|
||||
# which would take a really long time.
|
||||
armimage:
|
||||
docker build -t $(IMAGE_PREFIX)argocd:$(IMAGE_TAG)-arm . --build-arg BUILD_ALL_CLIS="false"
|
||||
|
||||
.PHONY: builder-image
|
||||
builder-image:
|
||||
docker build -t $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG) --target builder .
|
||||
@if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG) ; fi
|
||||
|
||||
.PHONY: dep
|
||||
dep:
|
||||
dep ensure -v
|
||||
.PHONY: mod-download
|
||||
mod-download: test-tools-image
|
||||
$(call run-in-test-client,go mod download)
|
||||
|
||||
.PHONY: dep-ensure
|
||||
dep-ensure:
|
||||
dep ensure -no-vendor
|
||||
.PHONY: mod-download-local
|
||||
mod-download-local:
|
||||
go mod download
|
||||
|
||||
.PHONY: mod-vendor
|
||||
mod-vendor: test-tools-image
|
||||
$(call run-in-test-client,go mod vendor)
|
||||
|
||||
.PHONY: mod-vendor-local
|
||||
mod-vendor-local: mod-download-local
|
||||
go mod vendor
|
||||
|
||||
# Deprecated - replace by install-local-tools
|
||||
.PHONY: install-lint-tools
|
||||
install-lint-tools:
|
||||
./hack/install.sh lint-tools
|
||||
|
||||
# Run linter on the code
|
||||
.PHONY: lint
|
||||
lint:
|
||||
golangci-lint --version
|
||||
golangci-lint run --fix --verbose
|
||||
lint: test-tools-image
|
||||
$(call run-in-test-client,make lint-local)
|
||||
|
||||
# Run linter on the code (local version)
|
||||
.PHONY: lint-local
|
||||
lint-local:
|
||||
golangci-lint --version
|
||||
# NOTE: If you get a "Killed" OOM message, try reducing the value of GOGC
|
||||
# See https://github.com/golangci/golangci-lint#memory-usage-of-golangci-lint
|
||||
GOGC=$(ARGOCD_LINT_GOGC) GOMAXPROCS=2 golangci-lint run --fix --verbose --timeout 300s
|
||||
|
||||
.PHONY: lint-ui
|
||||
lint-ui: test-tools-image
|
||||
$(call run-in-test-client,make lint-ui-local)
|
||||
|
||||
.PHONY: lint-ui-local
|
||||
lint-ui-local:
|
||||
cd ui && yarn lint
|
||||
|
||||
# Build all Go code
|
||||
.PHONY: build
|
||||
build:
|
||||
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:
|
||||
go build -v `go list ./... | grep -v 'resource_customizations\|test/e2e'`
|
||||
|
||||
# Run all unit tests
|
||||
#
|
||||
# If TEST_MODULE is set (to fully qualified module name), only this specific
|
||||
# module will be tested.
|
||||
.PHONY: test
|
||||
test:
|
||||
./hack/test.sh -coverprofile=coverage.out `go list ./... | grep -v 'test/e2e'`
|
||||
test: test-tools-image
|
||||
mkdir -p $(GOCACHE)
|
||||
$(call run-in-test-client,make TEST_MODULE=$(TEST_MODULE) test-local)
|
||||
|
||||
# Run all unit tests (local version)
|
||||
.PHONY: test-local
|
||||
test-local:
|
||||
if test "$(TEST_MODULE)" = ""; then \
|
||||
./hack/test.sh -coverprofile=coverage.out `go list ./... | grep -v 'test/e2e'`; \
|
||||
else \
|
||||
./hack/test.sh -coverprofile=coverage.out "$(TEST_MODULE)"; \
|
||||
fi
|
||||
|
||||
# Run the E2E test suite. E2E test servers (see start-e2e target) must be
|
||||
# started before.
|
||||
.PHONY: test-e2e
|
||||
test-e2e:
|
||||
./hack/test.sh -timeout 15m ./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-local
|
||||
# NO_PROXY ensures all tests don't go out through a proxy if one is configured on the test system
|
||||
export GO111MODULE=off
|
||||
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: test-tools-image
|
||||
$(call run-in-test-server,/bin/bash)
|
||||
|
||||
# Spawns a shell in the test client container for debugging purposes
|
||||
debug-test-client: test-tools-image
|
||||
$(call run-in-test-client,/bin/bash)
|
||||
|
||||
# Starts e2e server in a container
|
||||
.PHONY: start-e2e
|
||||
start-e2e: cli
|
||||
killall goreman || true
|
||||
# check we can connect to Docker to start Redis
|
||||
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:
|
||||
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
|
||||
if test "$(USER_ID)" != ""; then chown -R "$(USER_ID)" /tmp/argo-e2e; fi
|
||||
# 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 \
|
||||
goreman start
|
||||
ARGOCD_IN_CI=$(ARGOCD_IN_CI) \
|
||||
ARGOCD_E2E_TEST=true \
|
||||
goreman -f $(ARGOCD_PROCFILE) start
|
||||
|
||||
# Cleans VSCode debug.test files from sub-dirs to prevent them from being included in packr boxes
|
||||
.PHONY: clean-debug
|
||||
@@ -201,17 +390,33 @@ clean: clean-debug
|
||||
-rm -rf ${CURRENT_DIR}/dist
|
||||
|
||||
.PHONY: start
|
||||
start:
|
||||
killall goreman || true
|
||||
# check we can connect to Docker to start Redis
|
||||
start: test-tools-image
|
||||
docker version
|
||||
kubectl create ns argocd || true
|
||||
kubens argocd
|
||||
ARGOCD_ZJWT_FEATURE_FLAG=always \
|
||||
goreman start
|
||||
$(call run-in-test-server,make ARGOCD_PROCFILE=test/container/Procfile start-local ARGOCD_START=${ARGOCD_START})
|
||||
|
||||
# Starts a local instance of ArgoCD
|
||||
.PHONY: start-local
|
||||
start-local: 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: codegen-local build-local lint-local test-local
|
||||
|
||||
.PHONY: release-precheck
|
||||
release-precheck: manifests
|
||||
@@ -237,4 +442,48 @@ lint-docs:
|
||||
|
||||
.PHONY: publish-docs
|
||||
publish-docs: lint-docs
|
||||
mkdocs gh-deploy
|
||||
mkdocs gh-deploy
|
||||
|
||||
# Verify that kubectl can connect to your K8s cluster from Docker
|
||||
.PHONY: 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: test-tools-image
|
||||
@echo -n "Local Go version: "
|
||||
@go version
|
||||
@echo -n "Docker Go version: "
|
||||
$(call run-in-test-client,go version)
|
||||
|
||||
# Installs all tools required to build and test ArgoCD locally
|
||||
.PHONY: install-tools-local
|
||||
install-tools-local: install-test-tools-local install-codegen-tools-local install-go-tools-local
|
||||
|
||||
# Installs all tools required for running unit & end-to-end tests (Linux packages)
|
||||
.PHONY: install-test-tools-local
|
||||
install-test-tools-local:
|
||||
sudo ./hack/install.sh packr-linux
|
||||
sudo ./hack/install.sh kubectl-linux
|
||||
sudo ./hack/install.sh kustomize-linux
|
||||
sudo ./hack/install.sh ksonnet-linux
|
||||
sudo ./hack/install.sh helm2-linux
|
||||
sudo ./hack/install.sh helm-linux
|
||||
|
||||
# Installs all tools required for running codegen (Linux packages)
|
||||
.PHONY: install-codegen-tools-local
|
||||
install-codegen-tools-local:
|
||||
sudo ./hack/install.sh codegen-tools
|
||||
|
||||
# Installs all tools required for running codegen (Go packages)
|
||||
.PHONY: install-go-tools-local
|
||||
install-go-tools-local:
|
||||
./hack/install.sh codegen-go-tools
|
||||
|
||||
.PHONY: dep-ui
|
||||
dep-ui: test-tools-image
|
||||
$(call run-in-test-client,make dep-ui-local)
|
||||
|
||||
dep-ui-local:
|
||||
cd ui && yarn install
|
||||
|
||||
8
OWNERS
@@ -1,12 +1,12 @@
|
||||
owners:
|
||||
- alexec
|
||||
- alexmt
|
||||
- jessesuen
|
||||
|
||||
reviewers:
|
||||
- jannfis
|
||||
|
||||
approvers:
|
||||
- alexec
|
||||
- alexmt
|
||||
- dthomson25
|
||||
- jannfis
|
||||
- jessesuen
|
||||
- mayzhang2000
|
||||
- rachelwang20
|
||||
|
||||
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.14.0 serve /dex.yaml"
|
||||
redis: docker run --rm --name argocd-redis -i -p ${ARGOCD_E2E_REDIS_PORT:-6379}:${ARGOCD_E2E_REDIS_PORT:-6379} redis:5.0.3-alpine --save "" --appendonly no --port ${ARGOCD_E2E_REDIS_PORT:-6379}
|
||||
repo-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true go run ./cmd/argocd-repo-server/main.go --loglevel debug --port ${ARGOCD_E2E_REPOSERVER_PORT:-8081} --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379}"
|
||||
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 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.8-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}
|
||||
|
||||
61
README.md
@@ -1,3 +1,4 @@
|
||||
[](https://github.com/argoproj/argo-cd/actions?query=workflow%3A%22Integration+tests%22)
|
||||
[](https://argoproj.github.io/community/join-slack)
|
||||
[](https://codecov.io/gh/argoproj/argo-cd)
|
||||
[](https://github.com/argoproj/argo-cd/releases/latest)
|
||||
@@ -12,76 +13,28 @@ Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes.
|
||||
|
||||
## Why Argo CD?
|
||||
|
||||
Application definitions, configurations, and environments should be declarative and version controlled.
|
||||
|
||||
Application deployment and lifecycle management should be automated, auditable, and easy to understand.
|
||||
|
||||
1. Application definitions, configurations, and environments should be declarative and version controlled.
|
||||
1. Application deployment and lifecycle management should be automated, auditable, and easy to understand.
|
||||
|
||||
## Who uses Argo CD?
|
||||
|
||||
Organizations below are **officially** using Argo CD. Please send a PR with your organization name if you are using Argo CD.
|
||||
|
||||
1. [127Labs](https://127labs.com/)
|
||||
1. [Adevinta](https://www.adevinta.com/)
|
||||
1. [ANSTO - Australian Synchrotron](https://www.synchrotron.org.au/)
|
||||
1. [ARZ Allgemeines Rechenzentrum GmbH ](https://www.arz.at/)
|
||||
1. [Baloise](https://www.baloise.com)
|
||||
1. [BioBox Analytics](https://biobox.io)
|
||||
1. [CARFAX](https://www.carfax.com)
|
||||
1. [Celonis](https://www.celonis.com/)
|
||||
1. [Codility](https://www.codility.com/)
|
||||
1. [Commonbond](https://commonbond.co/)
|
||||
1. [CyberAgent](https://www.cyberagent.co.jp/en/)
|
||||
1. [Cybozu](https://cybozu-global.com)
|
||||
1. [EDF Renewables](https://www.edf-re.com/)
|
||||
1. [Elium](https://www.elium.com)
|
||||
1. [END.](https://www.endclothing.com/)
|
||||
1. [Fave](https://myfave.com)
|
||||
1. [Future PLC](https://www.futureplc.com/)
|
||||
1. [GMETRI](https://gmetri.com/)
|
||||
1. [hipages](https://hipages.com.au/)
|
||||
1. [Intuit](https://www.intuit.com/)
|
||||
1. [KintoHub](https://www.kintohub.com/)
|
||||
1. [KompiTech GmbH](https://www.kompitech.com/)
|
||||
1. [Lytt](https://www.lytt.co/)
|
||||
1. [Major League Baseball](https://mlb.com)
|
||||
1. [Mambu](https://www.mambu.com/)
|
||||
1. [Max Kelsen](https://www.maxkelsen.com/)
|
||||
1. [Mirantis](https://mirantis.com/)
|
||||
1. [OpenSaaS Studio](https://opensaas.studio)
|
||||
1. [Optoro](https://www.optoro.com/)
|
||||
1. [Peloton Interactive](https://www.onepeloton.com/)
|
||||
1. [Pipefy](https://www.pipefy.com/)
|
||||
1. [Riskified](https://www.riskified.com/)
|
||||
1. [Red Hat](https://www.redhat.com/)
|
||||
1. [Saildrone](https://www.saildrone.com/)
|
||||
1. [Saloodo! GmbH](https://www.saloodo.com)
|
||||
1. [Syncier](https://syncier.com/)
|
||||
1. [Tesla](https://tesla.com/)
|
||||
1. [Tiger Analytics](https://www.tigeranalytics.com/)
|
||||
1. [tZERO](https://www.tzero.com/)
|
||||
1. [Ticketmaster](https://ticketmaster.com)
|
||||
1. [Twilio SendGrid](https://sendgrid.com)
|
||||
1. [Yieldlab](https://www.yieldlab.de/)
|
||||
1. [UBIO](https://ub.io/)
|
||||
1. [Universidad Mesoamericana](https://www.umes.edu.gt/)
|
||||
1. [Viaduct](https://www.viaduct.ai/)
|
||||
1. [Volvo Cars](https://www.volvocars.com/)
|
||||
1. [Walkbase](https://www.walkbase.com/)
|
||||
[Official Argo CD user list](USERS.md)
|
||||
|
||||
## Documentation
|
||||
|
||||
To learn more about Argo CD [go to the complete documentation](https://argoproj.github.io/argo-cd/).
|
||||
Check live demo at https://cd.apps.argoproj.io/.
|
||||
|
||||
## Community Blogs and Presentations
|
||||
|
||||
1. [Tutorial: Everything You Need To Become A GitOps Ninja](https://www.youtube.com/watch?v=r50tRQjisxw) 90m tutorial on GitOps and Argo CD.
|
||||
1. [Comparison of Argo CD, Spinnaker, Jenkins X, and Tekton](https://www.inovex.de/blog/spinnaker-vs-argo-cd-vs-tekton-vs-jenkins-x/)
|
||||
1. [Simplify and Automate Deployments Using GitOps with IBM Multicloud Manager 3.1.2](https://medium.com/ibm-cloud/simplify-and-automate-deployments-using-gitops-with-ibm-multicloud-manager-3-1-2-4395af317359)
|
||||
1. [GitOps for Kubeflow using Argo CD](https://www.kubeflow.org/docs/use-cases/gitops-for-kubeflow/)
|
||||
1. [GitOps 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)
|
||||
|
||||
8
SECURITY_CONTACTS
Normal file
@@ -0,0 +1,8 @@
|
||||
# Defined below are the security contacts for this repo.
|
||||
#
|
||||
# DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE
|
||||
# INSTRUCTIONS AT https://argoproj.github.io/argo-cd/security_considerations/#reporting-vulnerabilities
|
||||
|
||||
alexmt
|
||||
edlee2121
|
||||
jessesuen
|
||||
77
USERS.md
Normal file
@@ -0,0 +1,77 @@
|
||||
## Who uses Argo CD?
|
||||
|
||||
As the Argo Community grows, we'd like to keep track of our users. Please send a PR with your organization name if you are using Argo CD.
|
||||
|
||||
Currently, the following organizations are **officially** using Argo CD:
|
||||
|
||||
1. [127Labs](https://127labs.com/)
|
||||
1. [Adevinta](https://www.adevinta.com/)
|
||||
1. [AppDirect](https://www.appdirect.com)
|
||||
1. [ANSTO - Australian Synchrotron](https://www.synchrotron.org.au/)
|
||||
1. [ARZ Allgemeines Rechenzentrum GmbH ](https://www.arz.at/)
|
||||
1. [Baloise](https://www.baloise.com)
|
||||
1. [BioBox Analytics](https://biobox.io)
|
||||
1. [CARFAX](https://www.carfax.com)
|
||||
1. [Celonis](https://www.celonis.com/)
|
||||
1. [Codility](https://www.codility.com/)
|
||||
1. [Commonbond](https://commonbond.co/)
|
||||
1. [CyberAgent](https://www.cyberagent.co.jp/en/)
|
||||
1. [Cybozu](https://cybozu-global.com)
|
||||
1. [D2iQ](https://www.d2iq.com)
|
||||
1. [EDF Renewables](https://www.edf-re.com/)
|
||||
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. [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. [OpenSaaS Studio](https://opensaas.studio)
|
||||
1. [Optoro](https://www.optoro.com/)
|
||||
1. [Peloton Interactive](https://www.onepeloton.com/)
|
||||
1. [Pipefy](https://www.pipefy.com/)
|
||||
1. [Prudential](https://prudential.com.sg)
|
||||
1. [PUBG](https://www.pubg.com)
|
||||
1. [QuintoAndar](https://quintoandar.com.br)
|
||||
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. [Tesla](https://tesla.com/)
|
||||
1. [ThousandEyes](https://www.thousandeyes.com/)
|
||||
1. [Ticketmaster](https://ticketmaster.com)
|
||||
1. [Tiger Analytics](https://www.tigeranalytics.com/)
|
||||
1. [Twilio SendGrid](https://sendgrid.com)
|
||||
1. [tZERO](https://www.tzero.com/)
|
||||
1. [UBIO](https://ub.io/)
|
||||
1. [Universidad Mesoamericana](https://www.umes.edu.gt/)
|
||||
1. [Viaduct](https://www.viaduct.ai/)
|
||||
1. [Volvo Cars](https://www.volvocars.com/)
|
||||
1. [VSHN - The DevOps Company](https://vshn.ch/)
|
||||
1. [Walkbase](https://www.walkbase.com/)
|
||||
1. [Whitehat Berlin](https://whitehat.berlin) by Guido Maria Serra +Fenaroli
|
||||
1. [Yieldlab](https://www.yieldlab.de/)
|
||||
1. [MTN Group](https://www.mtn.com/)
|
||||
1. [Moengage](https://www.moengage.com/)
|
||||
1. [LexisNexis](https://www.lexisnexis.com/)
|
||||
1. [PayPay](https://paypay.ne.jp/)
|
||||
@@ -1,22 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="131" height="20">
|
||||
<linearGradient id="b" x2="0" y2="100%">
|
||||
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||
<stop offset="1" stop-opacity=".1"/>
|
||||
</linearGradient>
|
||||
<clipPath id="a">
|
||||
<rect width="131" height="20" rx="3" fill="#fff"/>
|
||||
<svg width="131" height="20" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" >
|
||||
<defs>
|
||||
<filter id="dropShadow">
|
||||
<feDropShadow dx="0.2" dy="0.4" stdDeviation="0.2" flood-color="#333" flood-opacity="0.5"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<clipPath id="roundedCorners">
|
||||
<rect width="100%" height="100%" rx="3" opacity="1" />
|
||||
</clipPath>
|
||||
<g clip-path="url(#a)">
|
||||
<path id="leftPath" fill="#555" d="M0 0h74v20H0z"/>
|
||||
<path id="rightPath" fill="#4c1" d="M74 0h57v20H74z"/>
|
||||
<path fill="url(#b)" d="M0 0h131v20H0z"/>
|
||||
|
||||
<g clip-path="url(#roundedCorners)">
|
||||
<rect id="leftRect" fill="#555" x="0" y="0" width="74" height="20" />
|
||||
<rect id="rightRect" fill="#4c1" x="74" y="0" width="57" height="20" />
|
||||
<rect id="revisionRect" fill="#4c1" x="131" y="0" width="62" height="20" display="none"/>
|
||||
</g>
|
||||
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="90">
|
||||
|
||||
<g fill="#fff" style="filter: url(#dropShadow);" text-anchor="middle" font-family="DejaVu Sans, sans-serif" font-size="90">
|
||||
<image x="5" y="3" width="14" height="14" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB8AAAAeCAYAAADU8sWcAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAABPAAAATwFjiv3XAAACC2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOlJlc29sdXRpb25Vbml0PjI8L3RpZmY6UmVzb2x1dGlvblVuaXQ+CiAgICAgICAgIDx0aWZmOkNvbXByZXNzaW9uPjE8L3RpZmY6Q29tcHJlc3Npb24+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDx0aWZmOlBob3RvbWV0cmljSW50ZXJwcmV0YXRpb24+MjwvdGlmZjpQaG90b21ldHJpY0ludGVycHJldGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KD0UqkwAACpZJREFUSA11VnmQFcUZ//qY4527by92WWTXwHKqEJDDg0MQUioKSWopK4oaS9RAlMofUSPGbFlBSaViKomgQSNmIdECkUS8ophdozEJArKoKCsgIFlY2Pu9N29mero7X78FxCR2Vb8309PTv+/4fb9vCHz1II2Nm+jmzYul2bJsdlMyO7LyPGlZtYqQck1ZHCgwLZWiROeJgB7bDzpYb/7o0y/emzXv4Pts8+ZGBUC0uf/vQf57wdw3NTVRnOYFfXvj6pJcadkUmbDGRoxVSEItSYAQQrUmRBP81VoRpkFTJUMuZA/3g/28q3dn89b7u885D4348vgf8NPAxY033LJmSlDizlSOU94f6UhoHaZdC/3QCHWON2gDYBhAYyRAWgyD4QSi1815f29++vv/+CoD2Lm2nAFGl8mndzyxMMgk5/iW6xzPi/xVk+oyE8+vLNt14FR/0uYW14ox0IwUJ4ZAaUpAaaBEYByiyLbikWWNnnThvPIxu+L717auVeb81tbWsyk4C34u8I3LnlhcSCameMzykg7TwzOJGIYWQj+M+voLYcSo/hxY1E65FECUq4G4RFP0nSjMCMVAIIKUnOHkw90L6qq/vXvbh02trV8yoBh2E0NMY9GiG+58fIGXTF6epyw7JOnamZQbz/tCnDrVVzgqqdwTT0ZTdWDN0wPpMh3ZPZqHLSw98C7Y4RwtnAodsTwGAg0ppkcxrlkYJFID+Z0bn/zelsFImzQRXfScNJFiOG689dcT/FT6GzlqedVJTHRpPH6yz/PyfTlx2IrLCpvSdWrv5OXkX9fOJB/Mnaz3XzItap+yMDoyZgEE7F1SceIghryeRNwnTBkEqhRIxiLF6bCpDXNybW2v/ruxcTzbt2+zZmfCvWT+zxNhbek3cxaPlyYsXVWSSHT25rxC3o+OWK6aDn7sZ3r79ZNY28w45Esxuhw5RjmVvEQPlIwgR8bOiE7VdrPaA2+TeGEEhFbhtAEmG5pzk5WqqbWTPv7jC8sLpgxNSRWZrUeWTAgor7EY9ytTiURvzg8GskGU5xxKkc33yDcX1yc7x3ijLxF+zZgoEljgCrSiXOWIG/WGrjifHxl1V9iyeKwW1lHNo5SKmDmcYinivwhjVpkor55sQj9+/D4MO9bpLW8RN6zJzMvbPDUkHaecE368N1+IUUJfIm5hPf1w2kTedlm/LgntmqEWEQGT3ScJYUh2NIxiiWupSUFasor1VgzXvPATfv6BC3Roq0E9MLqAXKQcAe1rqi/dv+q3DwQUw6djY6tiAaG1jLEw7nAn64WhcStPqZqpA6chap8aage077GwbQcJDx8Awq3T3DHlbeijUHWwytHFuuizCTergdQ+YocxLRHVPNcESRAJxqo60nbSvExvvvWx23MCVvucDyRt5hitwDRHHG09pZiaT7MlSUdUhtQGZttYRdwA4WnFQjFnDA4LX7UdIlgMYpaouBgGMnuBKVN/ZhgBRCMVynNAHOfRm777q4UcrW3EZxcEnD/kclYlpVaFKFIZrIRtluvdls/G9InnLZ9fpLXoJjwZAzZkDJ5mjkIj8HTCKETH9oD0eoikMSDQ5cTrLrUg4QoaFlxUH5MdYy6yAFAaYBHed6ODkMPOkMcFZggZKa0SkaL/jMeDpq5D1Vez9693V/wNUg1jQRY8yLX8BcSba8AaOgyBOdqtQBz7DNj8lZCeNY9SN6aiD3bTOc89s2iliDVvKa3uGxf6doQAxYE2oBbm0HQPG5IxqKhOg9ZpSSQj+pDU+jv5t7913spV5c70GcqLJwgMGw6ZJUuBLbgHPW0D4rgQdewDvnAllN58B0D918AvzVBn3jWi/p4Hq2/s+et1URgBRlVRMGVvBtITfwyuyYPAoIRmWSIrEqjZW1Ml/qN7dtRWzptfBzXDoaeri6TicWhes8Zsg9iMOaAyU0D3fw4qdSEkrryquP77xx+HZCwG3T09HEZfCBUXTxh125H9VZ9SHp1OfZF4aEWI9MM1AhmiVCmaIwV6a2F5ge2q8s7OBB8ytPiOZduw6pFHYOS4cUUQGsNWXj4MVLYdaGU9UAQ0o2H8eHh49WpAITReKqdmGC0PsvF2JB4Gu+i5RqKg5JSi82mOnu9GYGEhwbFjSOXadJII7Pa6Ed3hwU8igAU8lU7D/ffdZ1pFEUT19YI+sR9oZgLIjo8h6usDu2oIzJpxOc4ZphKQtoqFxw6LzxIV/RMxkabSUPAJRz3A5oc50x9T7Lf3xkR0R0xGSS+IRIFQOUUUkqvGXdR5tGXnLmjbYXI0OLCJykIBvFe2ABVHgCQqgMpjkH9xE9IY7cSgGd0w+8mOVjiy68DOh+sauusiwSM8F11nlhCm39/SvP7ux4qNZVfba/0TJ183Is9opWtxEXe5E/cC+Y5bf+CilmczqaC/JigEOjjYTrwtzwB8tAn4kJFGMIEmy0B98gb4B7tAYimHncd19ObLrP25rW0/rb76pQqsoKTEkKJgoT64biCObVx35xvGGz67qYW3Nl0RWYH/ftxmo7pyfpSoKFFjEiLxhE73jXaueHXlGw+PZ68rS8pQ01QlodUjTqcAfcQo8qGjQB36EwQfbFAuF8wjZbmNtStefZ4nC0uibKIHez92AeZEUln5wocGuLGxyUbg2cVEFp5a9pFz11MH/ZCP6M0V8gmbxWZL35K2paFmUmjDgCXAMQkjqv8wgFMGBFVSCw8g1wWsrB5IBrTDAvB0eaAkhVHCeGzSQDUm3bE9/1hs/d7dBhy5aSqAaNNWN6Mvdn9+e0KGfl8hZCd6vBwqABmISKSkCgkqr/a6QMWrwLpsKaYWhfrwi1ikfWBNvQmUhXItAlznICMlcopFFNXcEA25wBw/EIms2L4O1onZs5u46abFUsILDWhAc/OKo7H+/OtuEFqaUcsmIE8CR47zEMsSuJvRQcceODVmOqR/uAHKftAC6R+9AKcmzgVxci9qexwLTIDZ34+MR6FVErXECXw7kc+3/G7j8gPG0dbWJmQnum1+zDj3U2rJ0rWzvFRynu/YskMTb5vetmwI7xyeg7R0g072ynEGwa0PwZTRDbDn0GGI1j0Ii2pCKPCyqJTl+eei9tOl5Kr1eU2seuG5ZMBv2fjUIMkQymAWC6jouQE3jdlYZa43PLnsLbcn++eYVwi6FCQktTCEmO3QAx2rhOllAl6bOwsahg2FDTMvhRklWVSUMkxcWEyHJFbUqyCW9AtRSV//K18AF4XmbOWeBTegCF78ujTXf3hm+XvTeo49G8/67Sg+A0a0OGc6CDzIlFbDL3+8GPYuuxLWP7AYysprIQx9YNjdzAglyVkD3qGvd3dsWvv0infM2qBj53zr49rZsJsNZwa2WeTTFxtP3L1oe1VCzh3AWqOM2FJJsFBwbMuCUAgQyAqGrVVJ7Zdwyz2RZS/X/GbrguJ5eNYgyhfnncH5v+DmYcft18Y1T5S7fl9jPMN+4aM6IpeQPmgxThNA5AnuJFhIeI2tHafiNqEUW1XQL+8NrfRznENP1drNuTOA5/5/KezmAZ4zaJBSdVaUvQk5PsNTeqfrMsKQOui5LGKibBAjmKgSRk/RIMlc8BiWCLbI95Dx05jybrMkfsh+xfgPf+hH0AC4OlsAAAAASUVORK5CYII="/>
|
||||
|
||||
<text id="leftText1" x="435" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="470"></text>
|
||||
<text id="leftText2" x="435" y="140" transform="scale(.1)" textLength="470"></text>
|
||||
|
||||
<text id="rightText1" x="995" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="470"></text>
|
||||
<text id="rightText1" x="995" y="140" transform="scale(.1)" textLength="470"></text></g>
|
||||
<text id="leftText" x="435" y="140" transform="scale(.1)" textLength="470"></text>
|
||||
<text id="rightText" x="995" y="140" transform="scale(.1)" textLength="470"></text>
|
||||
<text id="revisionText" x="1550" y="140" font-family="monospace" transform="scale(.1)" font-size="110" display="none"></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
@@ -11,6 +11,8 @@ p, role:readonly, certificates, get, *, allow
|
||||
p, role:readonly, clusters, get, *, allow
|
||||
p, role:readonly, repositories, get, *, allow
|
||||
p, role:readonly, projects, get, *, allow
|
||||
p, role:readonly, accounts, get, *, allow
|
||||
p, role:readonly, gpgkeys, get, *, allow
|
||||
|
||||
p, role:admin, applications, create, */*, allow
|
||||
p, role:admin, applications, update, */*, allow
|
||||
@@ -30,6 +32,9 @@ p, role:admin, repositories, delete, *, allow
|
||||
p, role:admin, projects, create, *, allow
|
||||
p, role:admin, projects, update, *, allow
|
||||
p, role:admin, projects, delete, *, allow
|
||||
p, role:admin, accounts, update, *, allow
|
||||
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)
|
||||
|
||||
1064
assets/swagger.json
@@ -6,6 +6,10 @@ import (
|
||||
"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"
|
||||
@@ -18,14 +22,13 @@ import (
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/controller"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
"github.com/argoproj/argo-cd/reposerver/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/kube"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -44,23 +47,25 @@ func newCommand() *cobra.Command {
|
||||
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)
|
||||
config.QPS = common.K8sClientConfigQPS
|
||||
config.Burst = common.K8sClientConfigBurst
|
||||
errors.CheckError(v1alpha1.SetK8SConfigDefaults(config))
|
||||
|
||||
kubeClient := kubernetes.NewForConfigOrDie(config)
|
||||
appClient := appclientset.NewForConfigOrDie(config)
|
||||
@@ -91,8 +96,10 @@ func newCommand() *cobra.Command {
|
||||
metricsPort,
|
||||
kubectlParallelismLimit)
|
||||
errors.CheckError(err)
|
||||
cacheutil.CollectMetrics(redisClient, appController.GetMetricsServer())
|
||||
|
||||
log.Infof("Application Controller (version: %s) starting (namespace: %s)", common.GetVersion(), namespace)
|
||||
vers := common.GetVersion()
|
||||
log.Infof("Application Controller (version: %s, built: %s) starting (namespace: %s)", vers.Version, vers.BuildDate, namespace)
|
||||
stats.RegisterStackDumper()
|
||||
stats.StartStatsTicker(10 * time.Minute)
|
||||
stats.RegisterHeapDumper("memprofile")
|
||||
@@ -110,13 +117,15 @@ func newCommand() *cobra.Command {
|
||||
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)
|
||||
cacheSrc = appstatecache.AddCacheFlagsToCmd(&command, func(client *redis.Client) {
|
||||
redisClient = client
|
||||
})
|
||||
return &command
|
||||
}
|
||||
|
||||
|
||||
@@ -7,37 +7,52 @@ import (
|
||||
"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/errors"
|
||||
"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/stats"
|
||||
"github.com/argoproj/argo-cd/util/gpg"
|
||||
"github.com/argoproj/argo-cd/util/tls"
|
||||
)
|
||||
|
||||
const (
|
||||
// CLIName is the name of the CLI
|
||||
cliName = "argocd-repo-server"
|
||||
cliName = "argocd-repo-server"
|
||||
gnuPGSourcePath = "/app/config/gpg/source"
|
||||
)
|
||||
|
||||
func getGnuPGSourcePath() string {
|
||||
if path := os.Getenv("ARGOCD_GPG_DATA_PATH"); path != "" {
|
||||
return path
|
||||
} else {
|
||||
return gnuPGSourcePath
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -47,6 +62,7 @@ func newCommand() *cobra.Command {
|
||||
errors.CheckError(err)
|
||||
|
||||
metricsServer := metrics.NewMetricsServer()
|
||||
cacheutil.CollectMetrics(redisClient, metricsServer)
|
||||
server, err := reposerver.NewServer(metricsServer, cache, tlsConfigCustomizer, parallelismLimit)
|
||||
errors.CheckError(err)
|
||||
|
||||
@@ -57,6 +73,19 @@ func newCommand() *cobra.Command {
|
||||
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)
|
||||
@@ -67,12 +96,15 @@ func newCommand() *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
cacheSrc = reposervercache.AddCacheFlagsToCmd(&command, func(client *redis.Client) {
|
||||
redisClient = client
|
||||
})
|
||||
return &command
|
||||
}
|
||||
|
||||
|
||||
@@ -4,33 +4,57 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
"github.com/argoproj/pkg/stats"
|
||||
"github.com/go-redis/redis"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
"github.com/argoproj/argo-cd/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/server"
|
||||
servercache "github.com/argoproj/argo-cd/server/cache"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
"github.com/argoproj/argo-cd/util/env"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/tls"
|
||||
)
|
||||
|
||||
const (
|
||||
failureRetryCountEnv = "ARGOCD_K8S_RETRY_COUNT"
|
||||
failureRetryPeriodMilliSecondsEnv = "ARGOCD_K8S_RETRY_DURATION_MILLISECONDS"
|
||||
)
|
||||
|
||||
var (
|
||||
failureRetryCount = 0
|
||||
failureRetryPeriodMilliSeconds = 100
|
||||
)
|
||||
|
||||
func init() {
|
||||
failureRetryCount = env.ParseNumFromEnv(failureRetryCountEnv, failureRetryCount, 0, 10)
|
||||
failureRetryPeriodMilliSeconds = env.ParseNumFromEnv(failureRetryPeriodMilliSecondsEnv, failureRetryPeriodMilliSeconds, 0, 1000)
|
||||
}
|
||||
|
||||
// NewCommand returns a new instance of an argocd command
|
||||
func NewCommand() *cobra.Command {
|
||||
var (
|
||||
redisClient *redis.Client
|
||||
insecure bool
|
||||
listenPort int
|
||||
metricsPort int
|
||||
logFormat string
|
||||
logLevel string
|
||||
glogLevel int
|
||||
clientConfig clientcmd.ClientConfig
|
||||
repoServerTimeoutSeconds int
|
||||
staticAssetsDir string
|
||||
baseHRef string
|
||||
rootPath string
|
||||
repoServerAddress string
|
||||
dexServerAddress string
|
||||
disableAuth bool
|
||||
@@ -43,13 +67,13 @@ func NewCommand() *cobra.Command {
|
||||
Short: "Run the argocd API server",
|
||||
Long: "Run the argocd API server",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
cli.SetLogFormat(logFormat)
|
||||
cli.SetLogLevel(logLevel)
|
||||
cli.SetGLogLevel(glogLevel)
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
config.QPS = common.K8sClientConfigQPS
|
||||
config.Burst = common.K8sClientConfigBurst
|
||||
errors.CheckError(v1alpha1.SetK8SConfigDefaults(config))
|
||||
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
@@ -60,9 +84,24 @@ func NewCommand() *cobra.Command {
|
||||
errors.CheckError(err)
|
||||
|
||||
kubeclientset := kubernetes.NewForConfigOrDie(config)
|
||||
appclientset := appclientset.NewForConfigOrDie(config)
|
||||
|
||||
appclientsetConfig, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
errors.CheckError(v1alpha1.SetK8SConfigDefaults(appclientsetConfig))
|
||||
|
||||
if failureRetryCount > 0 {
|
||||
appclientsetConfig = kube.AddFailureRetryWrapper(appclientsetConfig, failureRetryCount, failureRetryPeriodMilliSeconds)
|
||||
}
|
||||
appclientset := appclientset.NewForConfigOrDie(appclientsetConfig)
|
||||
repoclientset := apiclient.NewRepoServerClientset(repoServerAddress, repoServerTimeoutSeconds)
|
||||
|
||||
if rootPath != "" {
|
||||
if baseHRef != "" && baseHRef != rootPath {
|
||||
log.Warnf("--basehref and --rootpath had conflict: basehref: %s rootpath: %s", baseHRef, rootPath)
|
||||
}
|
||||
baseHRef = rootPath
|
||||
}
|
||||
|
||||
argoCDOpts := server.ArgoCDServerOpts{
|
||||
Insecure: insecure,
|
||||
ListenPort: listenPort,
|
||||
@@ -70,6 +109,7 @@ func NewCommand() *cobra.Command {
|
||||
Namespace: namespace,
|
||||
StaticAssetsDir: staticAssetsDir,
|
||||
BaseHRef: baseHRef,
|
||||
RootPath: rootPath,
|
||||
KubeClientset: kubeclientset,
|
||||
AppClientset: appclientset,
|
||||
RepoClientset: repoclientset,
|
||||
@@ -78,6 +118,7 @@ func NewCommand() *cobra.Command {
|
||||
TLSConfigCustomizer: tlsConfigCustomizer,
|
||||
Cache: cache,
|
||||
XFrameOptions: frameOptions,
|
||||
RedisClient: redisClient,
|
||||
}
|
||||
|
||||
stats.RegisterStackDumper()
|
||||
@@ -98,6 +139,8 @@ func NewCommand() *cobra.Command {
|
||||
command.Flags().BoolVar(&insecure, "insecure", false, "Run server without TLS")
|
||||
command.Flags().StringVar(&staticAssetsDir, "staticassets", "", "Static assets directory path")
|
||||
command.Flags().StringVar(&baseHRef, "basehref", "/", "Value for base href in index.html. Used if Argo CD is running behind reverse proxy under subpath different from /")
|
||||
command.Flags().StringVar(&rootPath, "rootpath", "", "Used if Argo CD is running behind reverse proxy under subpath different from /")
|
||||
command.Flags().StringVar(&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().StringVar(&repoServerAddress, "repo-server", common.DefaultRepoServerAddr, "Repo server address")
|
||||
@@ -109,6 +152,8 @@ func NewCommand() *cobra.Command {
|
||||
command.Flags().IntVar(&repoServerTimeoutSeconds, "repo-server-timeout-seconds", 60, "Repo server RPC call timeout seconds.")
|
||||
command.Flags().StringVar(&frameOptions, "x-frame-options", "sameorigin", "Set X-Frame-Options header in HTTP responses to `value`. To disable, set to \"\".")
|
||||
tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(command)
|
||||
cacheSrc = servercache.AddCacheFlagsToCmd(command)
|
||||
cacheSrc = servercache.AddCacheFlagsToCmd(command, func(client *redis.Client) {
|
||||
redisClient = client
|
||||
})
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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/errors"
|
||||
|
||||
// load the gcp plugin (required to authenticate against GKE clusters).
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
||||
|
||||
331
cmd/argocd-util/commands/apps.go
Normal file
@@ -0,0 +1,331 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
"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"
|
||||
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",
|
||||
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 := metrics.NewMetricsServer("", appLister, func() error {
|
||||
return nil
|
||||
})
|
||||
stateCache := createLiveStateCache(argoDB, appInformer, settingsMgr, server)
|
||||
if err := stateCache.Init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appStateManager := controller.NewAppStateManager(
|
||||
argoDB, appClientset, repoServerClient, namespace, &kube.KubectlCmd{}, 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, &kube.KubectlCmd{}, server, func(managedByApp map[string]bool, ref apiv1.ObjectReference) {})
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
package main
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
appclient "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/typed/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/diff"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
|
||||
"github.com/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"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
@@ -69,9 +69,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 +145,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 main
|
||||
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)
|
||||
545
cmd/argocd-util/commands/settings.go
Normal file
@@ -0,0 +1,545 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
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"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"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/lua"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
type settingsOpts struct {
|
||||
argocdCMPath string
|
||||
argocdSecretPath string
|
||||
loadClusterSettings bool
|
||||
clientConfig clientcmd.ClientConfig
|
||||
}
|
||||
|
||||
type commandContext interface {
|
||||
createSettingsManager() (*settings.SettingsManager, error)
|
||||
}
|
||||
|
||||
func collectLogs(callback func()) string {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
out := bytes.Buffer{}
|
||||
log.SetOutput(&out)
|
||||
defer log.SetLevel(log.FatalLevel)
|
||||
callback()
|
||||
return out.String()
|
||||
}
|
||||
|
||||
func setSettingsMeta(obj v1.Object) {
|
||||
obj.SetNamespace("default")
|
||||
labels := obj.GetLabels()
|
||||
if labels == nil {
|
||||
labels = make(map[string]string)
|
||||
}
|
||||
labels["app.kubernetes.io/part-of"] = "argocd"
|
||||
obj.SetLabels(labels)
|
||||
}
|
||||
|
||||
func (opts *settingsOpts) createSettingsManager() (*settings.SettingsManager, error) {
|
||||
var argocdCM *corev1.ConfigMap
|
||||
if opts.argocdCMPath == "" && !opts.loadClusterSettings {
|
||||
return nil, fmt.Errorf("either --argocd-cm-path must be provided or --load-cluster-settings must be set to true")
|
||||
} else if opts.argocdCMPath == "" {
|
||||
realClientset, ns, err := opts.getK8sClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
argocdCM, err = realClientset.CoreV1().ConfigMaps(ns).Get(context.Background(), common.ArgoCDConfigMapName, v1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
data, err := ioutil.ReadFile(opts.argocdCMPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = yaml.Unmarshal(data, &argocdCM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
setSettingsMeta(argocdCM)
|
||||
|
||||
var argocdSecret *corev1.Secret
|
||||
if opts.argocdSecretPath != "" {
|
||||
data, err := ioutil.ReadFile(opts.argocdSecretPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = yaml.Unmarshal(data, &argocdSecret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setSettingsMeta(argocdSecret)
|
||||
} else if opts.loadClusterSettings {
|
||||
realClientset, ns, err := opts.getK8sClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
argocdSecret, err = realClientset.CoreV1().Secrets(ns).Get(context.Background(), common.ArgoCDSecretName, v1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
argocdSecret = &corev1.Secret{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: common.ArgoCDSecretName,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"admin.password": []byte("test"),
|
||||
"server.secretkey": []byte("test"),
|
||||
},
|
||||
}
|
||||
}
|
||||
setSettingsMeta(argocdSecret)
|
||||
clientset := fake.NewSimpleClientset(argocdSecret, argocdCM)
|
||||
|
||||
manager := settings.NewSettingsManager(context.Background(), clientset, "default")
|
||||
errors.CheckError(manager.ResyncInformers())
|
||||
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
func (opts *settingsOpts) getK8sClient() (*kubernetes.Clientset, string, error) {
|
||||
namespace, _, err := opts.clientConfig.Namespace()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
restConfig, err := opts.clientConfig.ClientConfig()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
realClientset, err := kubernetes.NewForConfig(restConfig)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return realClientset, namespace, nil
|
||||
}
|
||||
|
||||
func NewSettingsCommand() *cobra.Command {
|
||||
var (
|
||||
opts settingsOpts
|
||||
)
|
||||
|
||||
var command = &cobra.Command{
|
||||
Use: "settings",
|
||||
Short: "Provides set of commands for settings validation and troubleshooting",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
},
|
||||
}
|
||||
log.SetLevel(log.FatalLevel)
|
||||
|
||||
command.AddCommand(NewValidateSettingsCommand(&opts))
|
||||
command.AddCommand(NewResourceOverridesCommand(&opts))
|
||||
|
||||
opts.clientConfig = cli.AddKubectlFlagsToCmd(command)
|
||||
command.PersistentFlags().StringVar(&opts.argocdCMPath, "argocd-cm-path", "", "Path to local argocd-cm.yaml file")
|
||||
command.PersistentFlags().StringVar(&opts.argocdSecretPath, "argocd-secret-path", "", "Path to local argocd-secret.yaml file")
|
||||
command.PersistentFlags().BoolVar(&opts.loadClusterSettings, "load-cluster-settings", false,
|
||||
"Indicates that config map and secret should be loaded from cluster unless local file path is provided")
|
||||
return command
|
||||
}
|
||||
|
||||
type settingValidator func(manager *settings.SettingsManager) (string, error)
|
||||
|
||||
func joinValidators(validators ...settingValidator) settingValidator {
|
||||
return func(manager *settings.SettingsManager) (string, error) {
|
||||
var errorStrs []string
|
||||
var summaries []string
|
||||
for i := range validators {
|
||||
summary, err := validators[i](manager)
|
||||
if err != nil {
|
||||
errorStrs = append(errorStrs, err.Error())
|
||||
}
|
||||
if summary != "" {
|
||||
summaries = append(summaries, summary)
|
||||
}
|
||||
}
|
||||
if len(errorStrs) > 0 {
|
||||
return "", fmt.Errorf("%s", strings.Join(errorStrs, "\n"))
|
||||
}
|
||||
return strings.Join(summaries, "\n"), nil
|
||||
}
|
||||
}
|
||||
|
||||
var validatorsByGroup = map[string]settingValidator{
|
||||
"general": joinValidators(func(manager *settings.SettingsManager) (string, error) {
|
||||
general, err := manager.GetSettings()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ssoProvider := ""
|
||||
if general.DexConfig != "" {
|
||||
if _, err := settings.UnmarshalDexConfig(general.DexConfig); err != nil {
|
||||
return "", fmt.Errorf("invalid dex.config: %v", err)
|
||||
}
|
||||
ssoProvider = "Dex"
|
||||
} else if general.OIDCConfigRAW != "" {
|
||||
if _, err := settings.UnmarshalOIDCConfig(general.OIDCConfigRAW); err != nil {
|
||||
return "", fmt.Errorf("invalid oidc.config: %v", err)
|
||||
}
|
||||
ssoProvider = "OIDC"
|
||||
}
|
||||
var summary string
|
||||
if ssoProvider != "" {
|
||||
summary = fmt.Sprintf("%s is configured", ssoProvider)
|
||||
if general.URL == "" {
|
||||
summary = summary + " ('url' field is missing)"
|
||||
}
|
||||
} else if ssoProvider != "" && general.URL != "" {
|
||||
|
||||
} else {
|
||||
summary = "SSO is not configured"
|
||||
}
|
||||
return summary, nil
|
||||
}, func(manager *settings.SettingsManager) (string, error) {
|
||||
_, err := manager.GetAppInstanceLabelKey()
|
||||
return "", err
|
||||
}, func(manager *settings.SettingsManager) (string, error) {
|
||||
_, err := manager.GetHelp()
|
||||
return "", err
|
||||
}, func(manager *settings.SettingsManager) (string, error) {
|
||||
_, err := manager.GetGoogleAnalytics()
|
||||
return "", err
|
||||
}),
|
||||
"plugins": func(manager *settings.SettingsManager) (string, error) {
|
||||
plugins, err := manager.GetConfigManagementPlugins()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%d plugins", len(plugins)), nil
|
||||
},
|
||||
"kustomize": func(manager *settings.SettingsManager) (string, error) {
|
||||
opts, err := manager.GetKustomizeSettings()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
summary := "default options"
|
||||
if opts.BuildOptions != "" {
|
||||
summary = opts.BuildOptions
|
||||
}
|
||||
if len(opts.Versions) > 0 {
|
||||
summary = fmt.Sprintf("%s (%d versions)", summary, len(opts.Versions))
|
||||
}
|
||||
return summary, err
|
||||
},
|
||||
"repositories": joinValidators(func(manager *settings.SettingsManager) (string, error) {
|
||||
repos, err := manager.GetRepositories()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%d repositories", len(repos)), nil
|
||||
}, func(manager *settings.SettingsManager) (string, error) {
|
||||
creds, err := manager.GetRepositoryCredentials()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%d repository credentials", len(creds)), nil
|
||||
}),
|
||||
"accounts": func(manager *settings.SettingsManager) (string, error) {
|
||||
accounts, err := manager.GetAccounts()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%d accounts", len(accounts)), nil
|
||||
},
|
||||
"resource-overrides": func(manager *settings.SettingsManager) (string, error) {
|
||||
overrides, err := manager.GetResourceOverrides()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%d resource overrides", len(overrides)), nil
|
||||
},
|
||||
}
|
||||
|
||||
func NewValidateSettingsCommand(cmdCtx commandContext) *cobra.Command {
|
||||
var (
|
||||
groups []string
|
||||
)
|
||||
|
||||
var allGroups []string
|
||||
for k := range validatorsByGroup {
|
||||
allGroups = append(allGroups, k)
|
||||
}
|
||||
sort.Slice(allGroups, func(i, j int) bool {
|
||||
return allGroups[i] < allGroups[j]
|
||||
})
|
||||
|
||||
var command = &cobra.Command{
|
||||
Use: "validate",
|
||||
Short: "Validate settings",
|
||||
Long: "Validates settings specified in 'argocd-cm' ConfigMap and 'argocd-secret' Secret",
|
||||
Example: `
|
||||
#Validates all settings in the specified YAML file
|
||||
argocd-util settings validate --argocd-cm-path ./argocd-cm.yaml
|
||||
|
||||
#Validates accounts and plugins settings in Kubernetes cluster of current kubeconfig context
|
||||
argocd-util settings validate --group accounts --group plugins --load-cluster-settings`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
settingsManager, err := cmdCtx.createSettingsManager()
|
||||
errors.CheckError(err)
|
||||
|
||||
if len(groups) == 0 {
|
||||
groups = allGroups
|
||||
}
|
||||
for i, group := range groups {
|
||||
validator := validatorsByGroup[group]
|
||||
|
||||
logs := collectLogs(func() {
|
||||
summary, err := validator(settingsManager)
|
||||
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(os.Stdout, "❌ %s\n", group)
|
||||
_, _ = fmt.Fprintf(os.Stdout, "%s\n", err.Error())
|
||||
} else {
|
||||
_, _ = fmt.Fprintf(os.Stdout, "✅ %s\n", group)
|
||||
if summary != "" {
|
||||
_, _ = fmt.Fprintf(os.Stdout, "%s\n", summary)
|
||||
}
|
||||
}
|
||||
})
|
||||
if logs != "" {
|
||||
_, _ = fmt.Fprintf(os.Stdout, "%s\n", logs)
|
||||
}
|
||||
if i != len(groups)-1 {
|
||||
_, _ = fmt.Fprintf(os.Stdout, "\n")
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
command.Flags().StringArrayVar(&groups, "group", nil, fmt.Sprintf(
|
||||
"Optional list of setting groups that have to be validated ( one of: %s)", strings.Join(allGroups, ", ")))
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func NewResourceOverridesCommand(cmdCtx commandContext) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "resource-overrides",
|
||||
Short: "Troubleshoot resource overrides",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
},
|
||||
}
|
||||
command.AddCommand(NewResourceIgnoreDifferencesCommand(cmdCtx))
|
||||
command.AddCommand(NewResourceActionListCommand(cmdCtx))
|
||||
command.AddCommand(NewResourceActionRunCommand(cmdCtx))
|
||||
command.AddCommand(NewResourceHealthCommand(cmdCtx))
|
||||
return command
|
||||
}
|
||||
|
||||
func executeResourceOverrideCommand(cmdCtx commandContext, args []string, callback func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride)) {
|
||||
data, err := ioutil.ReadFile(args[0])
|
||||
errors.CheckError(err)
|
||||
|
||||
res := unstructured.Unstructured{}
|
||||
errors.CheckError(yaml.Unmarshal(data, &res))
|
||||
|
||||
settingsManager, err := cmdCtx.createSettingsManager()
|
||||
errors.CheckError(err)
|
||||
|
||||
overrides, err := settingsManager.GetResourceOverrides()
|
||||
errors.CheckError(err)
|
||||
gvk := res.GroupVersionKind()
|
||||
key := gvk.Kind
|
||||
if gvk.Group != "" {
|
||||
key = fmt.Sprintf("%s/%s", gvk.Group, gvk.Kind)
|
||||
}
|
||||
override, hasOverride := overrides[key]
|
||||
if !hasOverride {
|
||||
_, _ = fmt.Printf("No overrides configured for '%s/%s'\n", gvk.Group, gvk.Kind)
|
||||
return
|
||||
}
|
||||
callback(res, override, overrides)
|
||||
}
|
||||
|
||||
func NewResourceIgnoreDifferencesCommand(cmdCtx commandContext) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "ignore-differences RESOURCE_YAML_PATH",
|
||||
Short: "Renders fields excluded from diffing",
|
||||
Long: "Renders ignored fields using the 'ignoreDifferences' setting specified in the 'resource.customizations' field of 'argocd-cm' ConfigMap",
|
||||
Example: `
|
||||
argocd-util settings resource-overrides ignore-differences ./deploy.yaml --argocd-cm-path ./argocd-cm.yaml`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) < 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
executeResourceOverrideCommand(cmdCtx, args, func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride) {
|
||||
gvk := res.GroupVersionKind()
|
||||
if len(override.IgnoreDifferences.JSONPointers) == 0 {
|
||||
_, _ = fmt.Printf("Ignore differences are not configured for '%s/%s'\n", gvk.Group, gvk.Kind)
|
||||
return
|
||||
}
|
||||
|
||||
normalizer, err := normalizers.NewIgnoreNormalizer(nil, overrides)
|
||||
errors.CheckError(err)
|
||||
|
||||
normalizedRes := res.DeepCopy()
|
||||
logs := collectLogs(func() {
|
||||
errors.CheckError(normalizer.Normalize(normalizedRes))
|
||||
})
|
||||
if logs != "" {
|
||||
_, _ = fmt.Println(logs)
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(&res, normalizedRes) {
|
||||
_, _ = fmt.Printf("No fields are ignored by ignoreDifferences settings: \n%s\n", override.IgnoreDifferences)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = fmt.Printf("Following fields are ignored:\n\n")
|
||||
_ = cli.PrintDiff(res.GetName(), &res, normalizedRes)
|
||||
})
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
func NewResourceHealthCommand(cmdCtx commandContext) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "health RESOURCE_YAML_PATH",
|
||||
Short: "Assess resource health",
|
||||
Long: "Assess resource health using the lua script configured in the 'resource.customizations' field of 'argocd-cm' ConfigMap",
|
||||
Example: `
|
||||
argocd-util settings resource-overrides health ./deploy.yaml --argocd-cm-path ./argocd-cm.yaml`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) < 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
executeResourceOverrideCommand(cmdCtx, args, func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride) {
|
||||
gvk := res.GroupVersionKind()
|
||||
if override.HealthLua == "" {
|
||||
_, _ = fmt.Printf("Health script is not configured for '%s/%s'\n", gvk.Group, gvk.Kind)
|
||||
return
|
||||
}
|
||||
|
||||
resHealth, err := healthutil.GetResourceHealth(&res, lua.ResourceHealthOverrides(overrides))
|
||||
errors.CheckError(err)
|
||||
|
||||
_, _ = fmt.Printf("STATUS: %s\n", resHealth.Status)
|
||||
_, _ = fmt.Printf("MESSAGE: %s\n", resHealth.Message)
|
||||
})
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
func NewResourceActionListCommand(cmdCtx commandContext) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "list-actions RESOURCE_YAML_PATH",
|
||||
Short: "List available resource actions",
|
||||
Long: "List actions available for given resource action using the lua scripts configured in the 'resource.customizations' field of 'argocd-cm' ConfigMap and outputs updated fields",
|
||||
Example: `
|
||||
argocd-util settings resource-overrides action list /tmp/deploy.yaml --argocd-cm-path ./argocd-cm.yaml`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) < 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
executeResourceOverrideCommand(cmdCtx, args, func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride) {
|
||||
gvk := res.GroupVersionKind()
|
||||
if override.Actions == "" {
|
||||
_, _ = fmt.Printf("Actions are not configured for '%s/%s'\n", gvk.Group, gvk.Kind)
|
||||
return
|
||||
}
|
||||
|
||||
luaVM := lua.VM{ResourceOverrides: overrides}
|
||||
discoveryScript, err := luaVM.GetResourceActionDiscovery(&res)
|
||||
errors.CheckError(err)
|
||||
|
||||
availableActions, err := luaVM.ExecuteResourceActionDiscovery(&res, discoveryScript)
|
||||
errors.CheckError(err)
|
||||
sort.Slice(availableActions, func(i, j int) bool {
|
||||
return availableActions[i].Name < availableActions[j].Name
|
||||
})
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintf(w, "NAME\tENABLED\n")
|
||||
for _, action := range availableActions {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", action.Name, strconv.FormatBool(action.Disabled))
|
||||
}
|
||||
_ = w.Flush()
|
||||
})
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
func NewResourceActionRunCommand(cmdCtx commandContext) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "run-action RESOURCE_YAML_PATH ACTION",
|
||||
Aliases: []string{"action"},
|
||||
Short: "Executes resource action",
|
||||
Long: "Executes resource action using the lua script configured in the 'resource.customizations' field of 'argocd-cm' ConfigMap and outputs updated fields",
|
||||
Example: `
|
||||
argocd-util settings resource-overrides action run /tmp/deploy.yaml restart --argocd-cm-path ./argocd-cm.yaml`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) < 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
action := args[1]
|
||||
|
||||
executeResourceOverrideCommand(cmdCtx, args, func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride) {
|
||||
gvk := res.GroupVersionKind()
|
||||
if override.Actions == "" {
|
||||
_, _ = fmt.Printf("Actions are not configured for '%s/%s'\n", gvk.Group, gvk.Kind)
|
||||
return
|
||||
}
|
||||
|
||||
luaVM := lua.VM{ResourceOverrides: overrides}
|
||||
action, err := luaVM.GetResourceAction(&res, action)
|
||||
errors.CheckError(err)
|
||||
|
||||
modifiedRes, err := luaVM.ExecuteResourceAction(&res, action.ActionLua)
|
||||
errors.CheckError(err)
|
||||
|
||||
if reflect.DeepEqual(&res, modifiedRes) {
|
||||
_, _ = fmt.Printf("No fields had been changed by action: \n%s\n", action.Name)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = fmt.Printf("Following fields have been changed:\n\n")
|
||||
_ = cli.PrintDiff(res.GetName(), &res, modifiedRes)
|
||||
})
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
383
cmd/argocd-util/commands/settings_test.go
Normal file
@@ -0,0 +1,383 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"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"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func captureStdout(callback func()) (string, error) {
|
||||
oldStdout := os.Stdout
|
||||
oldStderr := os.Stderr
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
os.Stdout = w
|
||||
defer func() {
|
||||
os.Stdout = oldStdout
|
||||
os.Stderr = oldStderr
|
||||
}()
|
||||
|
||||
callback()
|
||||
utils.Close(w)
|
||||
|
||||
data, err := ioutil.ReadAll(r)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), err
|
||||
}
|
||||
|
||||
func newSettingsManager(data map[string]string) *settings.SettingsManager {
|
||||
clientset := fake.NewSimpleClientset(&v1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: common.ArgoCDConfigMapName,
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/part-of": "argocd",
|
||||
},
|
||||
},
|
||||
Data: data,
|
||||
}, &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: common.ArgoCDSecretName,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"admin.password": []byte("test"),
|
||||
"server.secretkey": []byte("test"),
|
||||
},
|
||||
})
|
||||
return settings.NewSettingsManager(context.Background(), clientset, "default")
|
||||
}
|
||||
|
||||
type fakeCmdContext struct {
|
||||
mgr *settings.SettingsManager
|
||||
// nolint:unused,structcheck
|
||||
out bytes.Buffer
|
||||
}
|
||||
|
||||
func newCmdContext(data map[string]string) *fakeCmdContext {
|
||||
return &fakeCmdContext{mgr: newSettingsManager(data)}
|
||||
}
|
||||
|
||||
func (ctx *fakeCmdContext) createSettingsManager() (*settings.SettingsManager, error) {
|
||||
return ctx.mgr, nil
|
||||
}
|
||||
|
||||
type validatorTestCase struct {
|
||||
validator string
|
||||
data map[string]string
|
||||
containsSummary string
|
||||
containsError string
|
||||
}
|
||||
|
||||
func TestCreateSettingsManager(t *testing.T) {
|
||||
f, closer, err := tempFile(`apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-cm
|
||||
data:
|
||||
url: https://myargocd.com`)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
defer utils.Close(closer)
|
||||
|
||||
opts := settingsOpts{argocdCMPath: f}
|
||||
settingsManager, err := opts.createSettingsManager()
|
||||
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
argoCDSettings, err := settingsManager.GetSettings()
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, "https://myargocd.com", argoCDSettings.URL)
|
||||
}
|
||||
|
||||
func TestValidator(t *testing.T) {
|
||||
testCases := map[string]validatorTestCase{
|
||||
"General_SSOIsNotConfigured": {
|
||||
validator: "general", containsSummary: "SSO is not configured",
|
||||
},
|
||||
"General_DexInvalidConfig": {
|
||||
validator: "general",
|
||||
data: map[string]string{"dex.config": "abcdefg"},
|
||||
containsError: "invalid dex.config",
|
||||
},
|
||||
"General_OIDCConfigured": {
|
||||
validator: "general",
|
||||
data: map[string]string{
|
||||
"url": "https://myargocd.com",
|
||||
"oidc.config": `
|
||||
name: Okta
|
||||
issuer: https://dev-123456.oktapreview.com
|
||||
clientID: aaaabbbbccccddddeee
|
||||
clientSecret: aaaabbbbccccddddeee`,
|
||||
},
|
||||
containsSummary: "OIDC is configured",
|
||||
},
|
||||
"General_DexConfiguredMissingURL": {
|
||||
validator: "general",
|
||||
data: map[string]string{
|
||||
"dex.config": `connectors:
|
||||
- type: github
|
||||
name: GitHub
|
||||
config:
|
||||
clientID: aabbccddeeff00112233
|
||||
clientSecret: aabbccddeeff00112233`,
|
||||
},
|
||||
containsSummary: "Dex is configured ('url' field is missing)",
|
||||
},
|
||||
"Plugins_ValidConfig": {
|
||||
validator: "plugins",
|
||||
data: map[string]string{
|
||||
"configManagementPlugins": `[{"name": "test1"}, {"name": "test2"}]`,
|
||||
},
|
||||
containsSummary: "2 plugins",
|
||||
},
|
||||
"Kustomize_ModifiedOptions": {
|
||||
validator: "kustomize",
|
||||
containsSummary: "default options",
|
||||
},
|
||||
"Kustomize_DefaultOptions": {
|
||||
validator: "kustomize",
|
||||
data: map[string]string{
|
||||
"kustomize.buildOptions": "updated-options (2 versions)",
|
||||
"kustomize.versions.v123": "binary-123",
|
||||
"kustomize.versions.v321": "binary-321",
|
||||
},
|
||||
containsSummary: "updated-options",
|
||||
},
|
||||
"Repositories": {
|
||||
validator: "repositories",
|
||||
data: map[string]string{
|
||||
"repositories": `
|
||||
- url: https://github.com/argoproj/my-private-repository1
|
||||
- url: https://github.com/argoproj/my-private-repository2`,
|
||||
},
|
||||
containsSummary: "2 repositories",
|
||||
},
|
||||
"Accounts": {
|
||||
validator: "accounts",
|
||||
data: map[string]string{
|
||||
"accounts.user1": "apiKey, login",
|
||||
"accounts.user2": "login",
|
||||
"accounts.user3": "apiKey",
|
||||
},
|
||||
containsSummary: "4 accounts",
|
||||
},
|
||||
"ResourceOverrides": {
|
||||
validator: "resource-overrides",
|
||||
data: map[string]string{
|
||||
"resource.customizations": `
|
||||
admissionregistration.k8s.io/MutatingWebhookConfiguration:
|
||||
ignoreDifferences: |
|
||||
jsonPointers:
|
||||
- /webhooks/0/clientConfig/caBundle`,
|
||||
},
|
||||
containsSummary: "2 resource overrides",
|
||||
},
|
||||
}
|
||||
for name := range testCases {
|
||||
tc := testCases[name]
|
||||
t.Run(name, func(t *testing.T) {
|
||||
validator, ok := validatorsByGroup[tc.validator]
|
||||
if !assert.True(t, ok) {
|
||||
return
|
||||
}
|
||||
summary, err := validator(newSettingsManager(tc.data))
|
||||
if tc.containsSummary != "" {
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, summary, tc.containsSummary)
|
||||
} else if tc.containsError != "" {
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), tc.containsError)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
testDeploymentYAML = `apiVersion: v1
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 0`
|
||||
)
|
||||
|
||||
func tempFile(content string) (string, io.Closer, error) {
|
||||
f, err := ioutil.TempFile("", "*.yaml")
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
_, err = f.Write([]byte(content))
|
||||
if err != nil {
|
||||
_ = os.Remove(f.Name())
|
||||
return "", nil, err
|
||||
}
|
||||
return f.Name(), utils.NewCloser(func() error {
|
||||
return os.Remove(f.Name())
|
||||
}), nil
|
||||
}
|
||||
|
||||
func TestValidateSettingsCommand_NoErrors(t *testing.T) {
|
||||
cmd := NewValidateSettingsCommand(newCmdContext(map[string]string{}))
|
||||
out, err := captureStdout(func() {
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
for k := range validatorsByGroup {
|
||||
assert.Contains(t, out, fmt.Sprintf("✅ %s", k))
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceOverrideIgnoreDifferences(t *testing.T) {
|
||||
f, closer, err := tempFile(testDeploymentYAML)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
defer utils.Close(closer)
|
||||
|
||||
t.Run("NoOverridesConfigured", func(t *testing.T) {
|
||||
cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{}))
|
||||
out, err := captureStdout(func() {
|
||||
cmd.SetArgs([]string{"ignore-differences", f})
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, "No overrides configured")
|
||||
})
|
||||
|
||||
t.Run("DataIgnored", func(t *testing.T) {
|
||||
cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
|
||||
"resource.customizations": `apps/Deployment:
|
||||
ignoreDifferences: |
|
||||
jsonPointers:
|
||||
- /spec`}))
|
||||
out, err := captureStdout(func() {
|
||||
cmd.SetArgs([]string{"ignore-differences", f})
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, "< spec:")
|
||||
})
|
||||
}
|
||||
|
||||
func TestResourceOverrideHealth(t *testing.T) {
|
||||
f, closer, err := tempFile(testDeploymentYAML)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
defer utils.Close(closer)
|
||||
|
||||
t.Run("NoHealthAssessment", func(t *testing.T) {
|
||||
cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
|
||||
"resource.customizations": `apps/Deployment: {}`}))
|
||||
out, err := captureStdout(func() {
|
||||
cmd.SetArgs([]string{"health", f})
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, "Health script is not configured")
|
||||
})
|
||||
|
||||
t.Run("HealthAssessmentConfigured", func(t *testing.T) {
|
||||
cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
|
||||
"resource.customizations": `apps/Deployment:
|
||||
health.lua: |
|
||||
return { status = "Progressing" }
|
||||
`}))
|
||||
out, err := captureStdout(func() {
|
||||
cmd.SetArgs([]string{"health", f})
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, "Progressing")
|
||||
})
|
||||
}
|
||||
|
||||
func TestResourceOverrideAction(t *testing.T) {
|
||||
f, closer, err := tempFile(testDeploymentYAML)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
defer utils.Close(closer)
|
||||
|
||||
t.Run("NoActions", func(t *testing.T) {
|
||||
cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
|
||||
"resource.customizations": `apps/Deployment: {}`}))
|
||||
out, err := captureStdout(func() {
|
||||
cmd.SetArgs([]string{"run-action", f, "test"})
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, "Actions are not configured")
|
||||
})
|
||||
|
||||
t.Run("ActionConfigured", func(t *testing.T) {
|
||||
cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
|
||||
"resource.customizations": `apps/Deployment:
|
||||
actions: |
|
||||
discovery.lua: |
|
||||
actions = {}
|
||||
actions["resume"] = {["disabled"] = false}
|
||||
actions["restart"] = {["disabled"] = false}
|
||||
return actions
|
||||
definitions:
|
||||
- name: test
|
||||
action.lua: |
|
||||
obj.metadata.labels["test"] = 'updated'
|
||||
return obj
|
||||
`}))
|
||||
out, err := captureStdout(func() {
|
||||
cmd.SetArgs([]string{"run-action", f, "test"})
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, "test: updated")
|
||||
|
||||
out, err = captureStdout(func() {
|
||||
cmd.SetArgs([]string{"list-actions", f})
|
||||
err := cmd.Execute()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, out, `NAME ENABLED
|
||||
restart false
|
||||
resume false
|
||||
`)
|
||||
})
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"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"
|
||||
@@ -24,13 +26,11 @@ import (
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/argoproj/argo-cd/cmd/argocd-util/commands"
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/dex"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
|
||||
// load the gcp plugin (required to authenticate against GKE clusters).
|
||||
@@ -56,7 +56,8 @@ var (
|
||||
// NewCommand returns a new instance of an argocd command
|
||||
func NewCommand() *cobra.Command {
|
||||
var (
|
||||
logLevel string
|
||||
logFormat string
|
||||
logLevel string
|
||||
)
|
||||
|
||||
var command = &cobra.Command{
|
||||
@@ -73,8 +74,11 @@ func NewCommand() *cobra.Command {
|
||||
command.AddCommand(NewImportCommand())
|
||||
command.AddCommand(NewExportCommand())
|
||||
command.AddCommand(NewClusterConfig())
|
||||
command.AddCommand(NewProjectsCommand())
|
||||
command.AddCommand(commands.NewProjectsCommand())
|
||||
command.AddCommand(commands.NewSettingsCommand())
|
||||
command.AddCommand(commands.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
|
||||
}
|
||||
@@ -109,7 +113,7 @@ func NewRunDexCommand() *cobra.Command {
|
||||
} else {
|
||||
err = ioutil.WriteFile("/tmp/dex.yaml", dexCfgBytes, 0644)
|
||||
errors.CheckError(err)
|
||||
log.Info(redactor(string(dexCfgBytes)))
|
||||
log.Debug(redactor(string(dexCfgBytes)))
|
||||
cmd = exec.Command("dex", "serve", "/tmp/dex.yaml")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
@@ -244,7 +248,7 @@ func NewImportCommand() *cobra.Command {
|
||||
// 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{})
|
||||
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
|
||||
@@ -258,26 +262,26 @@ func NewImportCommand() *cobra.Command {
|
||||
}
|
||||
}
|
||||
|
||||
secrets, err := acdClients.secrets.List(metav1.ListOptions{})
|
||||
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(metav1.ListOptions{})
|
||||
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(metav1.ListOptions{})
|
||||
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(string(input))
|
||||
backupObjects, err := kube.SplitYAML(input)
|
||||
errors.CheckError(err)
|
||||
for _, bakObj := range backupObjects {
|
||||
gvk := bakObj.GroupVersionKind()
|
||||
@@ -297,7 +301,7 @@ func NewImportCommand() *cobra.Command {
|
||||
}
|
||||
if !exists {
|
||||
if !dryRun {
|
||||
_, err = dynClient.Create(bakObj, metav1.CreateOptions{})
|
||||
_, 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)
|
||||
@@ -306,7 +310,7 @@ func NewImportCommand() *cobra.Command {
|
||||
} else {
|
||||
if !dryRun {
|
||||
newLive := updateLive(bakObj, &liveObj)
|
||||
_, err = dynClient.Update(newLive, metav1.UpdateOptions{})
|
||||
_, 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)
|
||||
@@ -328,7 +332,7 @@ func NewImportCommand() *cobra.Command {
|
||||
log.Fatalf("Unexpected kind '%s' in prune list", key.Kind)
|
||||
}
|
||||
if !dryRun {
|
||||
err = dynClient.Delete(key.Name, &metav1.DeleteOptions{})
|
||||
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)
|
||||
@@ -385,38 +389,44 @@ func NewExportCommand() *cobra.Command {
|
||||
} else {
|
||||
f, err := os.Create(out)
|
||||
errors.CheckError(err)
|
||||
defer util.Close(f)
|
||||
writer = bufio.NewWriter(f)
|
||||
bw := bufio.NewWriter(f)
|
||||
writer = bw
|
||||
defer func() {
|
||||
err = bw.Flush()
|
||||
errors.CheckError(err)
|
||||
err = f.Close()
|
||||
errors.CheckError(err)
|
||||
}()
|
||||
}
|
||||
|
||||
acdClients := newArgoCDClientsets(config, namespace)
|
||||
acdConfigMap, err := acdClients.configMaps.Get(common.ArgoCDConfigMapName, metav1.GetOptions{})
|
||||
acdConfigMap, err := acdClients.configMaps.Get(context.Background(), common.ArgoCDConfigMapName, metav1.GetOptions{})
|
||||
errors.CheckError(err)
|
||||
export(writer, *acdConfigMap)
|
||||
acdRBACConfigMap, err := acdClients.configMaps.Get(common.ArgoCDRBACConfigMapName, metav1.GetOptions{})
|
||||
acdRBACConfigMap, err := acdClients.configMaps.Get(context.Background(), common.ArgoCDRBACConfigMapName, metav1.GetOptions{})
|
||||
errors.CheckError(err)
|
||||
export(writer, *acdRBACConfigMap)
|
||||
acdKnownHostsConfigMap, err := acdClients.configMaps.Get(common.ArgoCDKnownHostsConfigMapName, metav1.GetOptions{})
|
||||
acdKnownHostsConfigMap, err := acdClients.configMaps.Get(context.Background(), common.ArgoCDKnownHostsConfigMapName, metav1.GetOptions{})
|
||||
errors.CheckError(err)
|
||||
export(writer, *acdKnownHostsConfigMap)
|
||||
acdTLSCertsConfigMap, err := acdClients.configMaps.Get(common.ArgoCDTLSCertsConfigMapName, metav1.GetOptions{})
|
||||
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(metav1.ListOptions{})
|
||||
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(metav1.ListOptions{})
|
||||
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(metav1.ListOptions{})
|
||||
applications, err := acdClients.applications.List(context.Background(), metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, app := range applications.Items {
|
||||
export(writer, app)
|
||||
@@ -540,10 +550,21 @@ func specsEqual(left, right unstructured.Unstructured) bool {
|
||||
leftData, _, _ := unstructured.NestedMap(left.Object, "data")
|
||||
rightData, _, _ := unstructured.NestedMap(right.Object, "data")
|
||||
return reflect.DeepEqual(leftData, rightData)
|
||||
case "AppProject", "Application":
|
||||
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
|
||||
}
|
||||
@@ -558,8 +579,13 @@ func updateLive(bak, live *unstructured.Unstructured) *unstructured.Unstructured
|
||||
switch live.GetKind() {
|
||||
case "Secret", "ConfigMap":
|
||||
newLive.Object["data"] = bak.Object["data"]
|
||||
case "AppProject", "Application":
|
||||
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
|
||||
}
|
||||
@@ -611,7 +637,7 @@ func NewClusterConfig() *cobra.Command {
|
||||
|
||||
cluster, err := db.NewDB(namespace, settings.NewSettingsManager(context.Background(), kubeclientset, namespace), kubeclientset).GetCluster(context.Background(), serverUrl)
|
||||
errors.CheckError(err)
|
||||
err = kube.WriteKubeConfig(cluster.RESTConfig(), namespace, output)
|
||||
err = kube.WriteKubeConfig(cluster.RawRestConfig(), namespace, output)
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
@@ -640,7 +666,7 @@ func redactor(dirtyString string) string {
|
||||
err := yaml.Unmarshal([]byte(dirtyString), &config)
|
||||
errors.CheckError(err)
|
||||
iterateStringFields(config, func(name string, val string) string {
|
||||
if name == "clientSecret" || name == "secret" {
|
||||
if name == "clientSecret" || name == "secret" || name == "bindPW" {
|
||||
return "********"
|
||||
} else {
|
||||
return val
|
||||
|
||||
@@ -18,8 +18,17 @@ connectors:
|
||||
id: github
|
||||
name: GitHub
|
||||
type: github
|
||||
- config:
|
||||
bindDN: uid=serviceaccount,cn=users,dc=example,dc=com
|
||||
bindPW: theSecret
|
||||
host: ldap.example.com:636
|
||||
id: ldap
|
||||
name: LDAP
|
||||
type: ldap
|
||||
grpc:
|
||||
addr: 0.0.0.0:5557
|
||||
telemetry:
|
||||
http: 0.0.0.0:5558
|
||||
issuer: https://argocd.example.com/api/dex
|
||||
oauth2:
|
||||
skipApprovalScreen: true
|
||||
@@ -49,6 +58,13 @@ var expectedRedaction = `connectors:
|
||||
id: github
|
||||
name: GitHub
|
||||
type: github
|
||||
- config:
|
||||
bindDN: uid=serviceaccount,cn=users,dc=example,dc=com
|
||||
bindPW: '********'
|
||||
host: ldap.example.com:636
|
||||
id: ldap
|
||||
name: LDAP
|
||||
type: ldap
|
||||
grpc:
|
||||
addr: 0.0.0.0:5557
|
||||
issuer: https://argocd.example.com/api/dex
|
||||
@@ -67,6 +83,8 @@ staticClients:
|
||||
- http://localhost
|
||||
storage:
|
||||
type: memory
|
||||
telemetry:
|
||||
http: 0.0.0.0:5558
|
||||
web:
|
||||
http: 0.0.0.0:5556
|
||||
`
|
||||
|
||||
@@ -5,22 +5,26 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"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"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
accountpkg "github.com/argoproj/argo-cd/pkg/apiclient/account"
|
||||
"github.com/argoproj/argo-cd/pkg/apiclient/session"
|
||||
"github.com/argoproj/argo-cd/server/rbacpolicy"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
sessionutil "github.com/argoproj/argo-cd/util/session"
|
||||
)
|
||||
|
||||
func NewAccountCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
@@ -35,11 +39,16 @@ func NewAccountCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
command.AddCommand(NewAccountUpdatePasswordCommand(clientOpts))
|
||||
command.AddCommand(NewAccountGetUserInfoCommand(clientOpts))
|
||||
command.AddCommand(NewAccountCanICommand(clientOpts))
|
||||
command.AddCommand(NewAccountListCommand(clientOpts))
|
||||
command.AddCommand(NewAccountGenerateTokenCommand(clientOpts))
|
||||
command.AddCommand(NewAccountGetCommand(clientOpts))
|
||||
command.AddCommand(NewAccountDeleteTokenCommand(clientOpts))
|
||||
return command
|
||||
}
|
||||
|
||||
func NewAccountUpdatePasswordCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
account string
|
||||
currentPassword string
|
||||
newPassword string
|
||||
)
|
||||
@@ -51,14 +60,20 @@ func NewAccountUpdatePasswordCommand(clientOpts *argocdclient.ClientOptions) *co
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
acdClient := argocdclient.NewClientOrDie(clientOpts)
|
||||
conn, usrIf := acdClient.NewAccountClientOrDie()
|
||||
defer io.Close(conn)
|
||||
|
||||
if currentPassword == "" {
|
||||
userInfo := getCurrentAccount(acdClient)
|
||||
|
||||
if userInfo.Iss == sessionutil.SessionManagerClaimsIssuer && currentPassword == "" {
|
||||
fmt.Print("*** Enter current password: ")
|
||||
password, err := terminal.ReadPassword(syscall.Stdin)
|
||||
password, err := terminal.ReadPassword(int(os.Stdin.Fd()))
|
||||
errors.CheckError(err)
|
||||
currentPassword = string(password)
|
||||
fmt.Print("\n")
|
||||
}
|
||||
|
||||
if newPassword == "" {
|
||||
var err error
|
||||
newPassword, err = cli.ReadAndConfirmPassword()
|
||||
@@ -68,37 +83,37 @@ func NewAccountUpdatePasswordCommand(clientOpts *argocdclient.ClientOptions) *co
|
||||
updatePasswordRequest := accountpkg.UpdatePasswordRequest{
|
||||
NewPassword: newPassword,
|
||||
CurrentPassword: currentPassword,
|
||||
Name: account,
|
||||
}
|
||||
|
||||
acdClient := argocdclient.NewClientOrDie(clientOpts)
|
||||
conn, usrIf := acdClient.NewAccountClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
ctx := context.Background()
|
||||
_, err := usrIf.UpdatePassword(ctx, &updatePasswordRequest)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Password updated\n")
|
||||
|
||||
// Get a new JWT token after updating the password
|
||||
localCfg, err := localconfig.ReadLocalConfig(clientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
configCtx, err := localCfg.ResolveContext(clientOpts.Context)
|
||||
errors.CheckError(err)
|
||||
claims, err := configCtx.User.Claims()
|
||||
errors.CheckError(err)
|
||||
tokenString := passwordLogin(acdClient, claims.Subject, newPassword)
|
||||
localCfg.UpsertUser(localconfig.User{
|
||||
Name: localCfg.CurrentContext,
|
||||
AuthToken: tokenString,
|
||||
})
|
||||
err = localconfig.WriteLocalConfig(*localCfg, clientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Context '%s' updated\n", localCfg.CurrentContext)
|
||||
if account == "" || account == userInfo.Username {
|
||||
// Get a new JWT token after updating the password
|
||||
localCfg, err := localconfig.ReadLocalConfig(clientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
configCtx, err := localCfg.ResolveContext(clientOpts.Context)
|
||||
errors.CheckError(err)
|
||||
claims, err := configCtx.User.Claims()
|
||||
errors.CheckError(err)
|
||||
tokenString := passwordLogin(acdClient, claims.Subject, newPassword)
|
||||
localCfg.UpsertUser(localconfig.User{
|
||||
Name: localCfg.CurrentContext,
|
||||
AuthToken: tokenString,
|
||||
})
|
||||
err = localconfig.WriteLocalConfig(*localCfg, clientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Context '%s' updated\n", localCfg.CurrentContext)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
command.Flags().StringVar(¤tPassword, "current-password", "", "current password you wish to change")
|
||||
command.Flags().StringVar(&newPassword, "new-password", "", "new password you want to update to")
|
||||
command.Flags().StringVar(&account, "account", "", "an account name that should be updated. Defaults to current user account")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -116,7 +131,7 @@ func NewAccountGetUserInfoCommand(clientOpts *argocdclient.ClientOptions) *cobra
|
||||
}
|
||||
|
||||
conn, client := argocdclient.NewClientOrDie(clientOpts).NewSessionClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
|
||||
ctx := context.Background()
|
||||
response, err := client.GetUserInfo(ctx, &session.GetUserInfoRequest{})
|
||||
@@ -159,11 +174,11 @@ argocd account can-i sync applications '*'
|
||||
argocd account can-i update projects 'default'
|
||||
|
||||
# Can I create a cluster?
|
||||
argocd account can-i create cluster '*'
|
||||
argocd account can-i create clusters '*'
|
||||
|
||||
Actions: %v
|
||||
Resources: %v
|
||||
`, rbacpolicy.Resources, rbacpolicy.Actions),
|
||||
`, rbacpolicy.Actions, rbacpolicy.Resources),
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
c.HelpFunc()(c, args)
|
||||
@@ -171,7 +186,7 @@ Resources: %v
|
||||
}
|
||||
|
||||
conn, client := argocdclient.NewClientOrDie(clientOpts).NewAccountClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
|
||||
ctx := context.Background()
|
||||
response, err := client.CanI(ctx, &accountpkg.CanIRequest{
|
||||
@@ -184,3 +199,202 @@ Resources: %v
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func printAccountNames(accounts []*accountpkg.Account) {
|
||||
for _, p := range accounts {
|
||||
fmt.Println(p.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func printAccountsTable(items []*accountpkg.Account) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "NAME\tENABLED\tCAPABILITIES\n")
|
||||
for _, a := range items {
|
||||
fmt.Fprintf(w, "%s\t%v\t%s\n", a.Name, a.Enabled, strings.Join(a.Capabilities, ", "))
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
func NewAccountListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List accounts",
|
||||
Example: "argocd account list",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
|
||||
conn, client := argocdclient.NewClientOrDie(clientOpts).NewAccountClientOrDie()
|
||||
defer io.Close(conn)
|
||||
|
||||
ctx := context.Background()
|
||||
response, err := client.ListAccounts(ctx, &accountpkg.ListAccountRequest{})
|
||||
|
||||
errors.CheckError(err)
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(response.Items, output, false)
|
||||
errors.CheckError(err)
|
||||
case "name":
|
||||
printAccountNames(response.Items)
|
||||
case "wide", "":
|
||||
printAccountsTable(response.Items)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|name")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func getCurrentAccount(clientset argocdclient.Client) session.GetUserInfoResponse {
|
||||
conn, client := clientset.NewSessionClientOrDie()
|
||||
defer io.Close(conn)
|
||||
userInfo, err := client.GetUserInfo(context.Background(), &session.GetUserInfoRequest{})
|
||||
errors.CheckError(err)
|
||||
return *userInfo
|
||||
}
|
||||
|
||||
func NewAccountGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
account string
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Get account details",
|
||||
Example: `# Get the currently logged in account details
|
||||
argocd account get
|
||||
|
||||
# Get details for an account by name
|
||||
argocd account get --account <account-name>`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
clientset := argocdclient.NewClientOrDie(clientOpts)
|
||||
|
||||
if account == "" {
|
||||
account = getCurrentAccount(clientset).Username
|
||||
}
|
||||
|
||||
conn, client := clientset.NewAccountClientOrDie()
|
||||
defer io.Close(conn)
|
||||
|
||||
acc, err := client.GetAccount(context.Background(), &accountpkg.GetAccountRequest{Name: account})
|
||||
|
||||
errors.CheckError(err)
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(acc, output, true)
|
||||
errors.CheckError(err)
|
||||
case "name":
|
||||
fmt.Println(acc.Name)
|
||||
case "wide", "":
|
||||
printAccountDetails(acc)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|name")
|
||||
cmd.Flags().StringVarP(&account, "account", "a", "", "Account name. Defaults to the current account.")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func printAccountDetails(acc *accountpkg.Account) {
|
||||
fmt.Printf(printOpFmtStr, "Name:", acc.Name)
|
||||
fmt.Printf(printOpFmtStr, "Enabled:", strconv.FormatBool(acc.Enabled))
|
||||
fmt.Printf(printOpFmtStr, "Capabilities:", strings.Join(acc.Capabilities, ", "))
|
||||
fmt.Println("\nTokens:")
|
||||
if len(acc.Tokens) == 0 {
|
||||
fmt.Println("NONE")
|
||||
} else {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "ID\tISSUED AT\tEXPIRING AT\n")
|
||||
for _, t := range acc.Tokens {
|
||||
expiresAtFormatted := "never"
|
||||
if t.ExpiresAt > 0 {
|
||||
expiresAt := time.Unix(t.ExpiresAt, 0)
|
||||
expiresAtFormatted = expiresAt.Format(time.RFC3339)
|
||||
if expiresAt.Before(time.Now()) {
|
||||
expiresAtFormatted = fmt.Sprintf("%s (expired)", expiresAtFormatted)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", t.Id, time.Unix(t.IssuedAt, 0).Format(time.RFC3339), expiresAtFormatted)
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func NewAccountGenerateTokenCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
account string
|
||||
expiresIn string
|
||||
id string
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "generate-token",
|
||||
Short: "Generate account token",
|
||||
Example: `# Generate token for the currently logged in account
|
||||
argocd account generate-token
|
||||
|
||||
# Generate token for the account with the specified name
|
||||
argocd account generate-token --account <account-name>`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
|
||||
clientset := argocdclient.NewClientOrDie(clientOpts)
|
||||
conn, client := clientset.NewAccountClientOrDie()
|
||||
defer io.Close(conn)
|
||||
if account == "" {
|
||||
account = getCurrentAccount(clientset).Username
|
||||
}
|
||||
expiresIn, err := timeutil.ParseDuration(expiresIn)
|
||||
errors.CheckError(err)
|
||||
response, err := client.CreateToken(context.Background(), &accountpkg.CreateTokenRequest{
|
||||
Name: account,
|
||||
ExpiresIn: int64(expiresIn.Seconds()),
|
||||
Id: id,
|
||||
})
|
||||
errors.CheckError(err)
|
||||
fmt.Println(response.Token)
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&account, "account", "a", "", "Account name. Defaults to the current account.")
|
||||
cmd.Flags().StringVarP(&expiresIn, "expires-in", "e", "0s", "Duration before the token will expire. (Default: No expiration)")
|
||||
cmd.Flags().StringVar(&id, "id", "", "Optional token id. Fallback to uuid if not value specified.")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewAccountDeleteTokenCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
account string
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete-token",
|
||||
Short: "Deletes account token",
|
||||
Example: `# Delete token of the currently logged in account
|
||||
argocd account delete-token ID
|
||||
|
||||
# Delete token of the account with the specified name
|
||||
argocd account generate-token --account <account-name>`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
id := args[0]
|
||||
|
||||
clientset := argocdclient.NewClientOrDie(clientOpts)
|
||||
conn, client := clientset.NewAccountClientOrDie()
|
||||
defer io.Close(conn)
|
||||
if account == "" {
|
||||
account = getCurrentAccount(clientset).Username
|
||||
}
|
||||
_, err := client.DeleteToken(context.Background(), &accountpkg.DeleteTokenRequest{Name: account, Id: id})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&account, "account", "a", "", "Account name. Defaults to the current account.")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -4,18 +4,18 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"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"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
applicationpkg "github.com/argoproj/argo-cd/pkg/apiclient/application"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
)
|
||||
|
||||
type DisplayedAction struct {
|
||||
@@ -59,7 +59,7 @@ func NewApplicationResourceActionsListCommand(clientOpts *argocdclient.ClientOpt
|
||||
}
|
||||
appName := args[0]
|
||||
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
ctx := context.Background()
|
||||
resources, err := appIf.ManagedResources(ctx, &applicationpkg.ResourcesQuery{ApplicationName: &appName})
|
||||
errors.CheckError(err)
|
||||
@@ -144,7 +144,7 @@ func NewApplicationResourceActionsRunCommand(clientOpts *argocdclient.ClientOpti
|
||||
actionName := args[1]
|
||||
|
||||
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
ctx := context.Background()
|
||||
resources, err := appIf.ManagedResources(ctx, &applicationpkg.ResourcesQuery{ApplicationName: &appName})
|
||||
errors.CheckError(err)
|
||||
|
||||
@@ -3,6 +3,7 @@ package commands
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
@@ -34,4 +35,78 @@ func Test_setHelmOpt(t *testing.T) {
|
||||
setHelmOpt(&src, helmOpts{helmSetStrings: []string{"foo=bar"}})
|
||||
assert.Equal(t, []v1alpha1.HelmParameter{{Name: "foo", Value: "bar", ForceString: true}}, src.Helm.Parameters)
|
||||
})
|
||||
t.Run("HelmSetFiles", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setHelmOpt(&src, helmOpts{helmSetFiles: []string{"foo=bar"}})
|
||||
assert.Equal(t, []v1alpha1.HelmFileParameter{{Name: "foo", Path: "bar"}}, src.Helm.FileParameters)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_setJsonnetOpt(t *testing.T) {
|
||||
t.Run("TlaSets", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setJsonnetOpt(&src, []string{"foo=bar"}, false)
|
||||
assert.Equal(t, []v1alpha1.JsonnetVar{{Name: "foo", Value: "bar"}}, src.Directory.Jsonnet.TLAs)
|
||||
setJsonnetOpt(&src, []string{"bar=baz"}, false)
|
||||
assert.Equal(t, []v1alpha1.JsonnetVar{{Name: "foo", Value: "bar"}, {Name: "bar", Value: "baz"}}, src.Directory.Jsonnet.TLAs)
|
||||
})
|
||||
t.Run("ExtSets", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setJsonnetOptExtVar(&src, []string{"foo=bar"}, false)
|
||||
assert.Equal(t, []v1alpha1.JsonnetVar{{Name: "foo", Value: "bar"}}, src.Directory.Jsonnet.ExtVars)
|
||||
setJsonnetOptExtVar(&src, []string{"bar=baz"}, false)
|
||||
assert.Equal(t, []v1alpha1.JsonnetVar{{Name: "foo", Value: "bar"}, {Name: "bar", Value: "baz"}}, src.Directory.Jsonnet.ExtVars)
|
||||
})
|
||||
}
|
||||
|
||||
type appOptionsFixture struct {
|
||||
spec *v1alpha1.ApplicationSpec
|
||||
command *cobra.Command
|
||||
options *appOptions
|
||||
}
|
||||
|
||||
func (f *appOptionsFixture) SetFlag(key, value string) error {
|
||||
err := f.command.Flags().Set(key, value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = setAppSpecOptions(f.command.Flags(), f.spec, f.options)
|
||||
return err
|
||||
}
|
||||
|
||||
func newAppOptionsFixture() *appOptionsFixture {
|
||||
fixture := &appOptionsFixture{
|
||||
spec: &v1alpha1.ApplicationSpec{},
|
||||
command: &cobra.Command{},
|
||||
options: &appOptions{},
|
||||
}
|
||||
addAppFlags(fixture.command, fixture.options)
|
||||
return fixture
|
||||
}
|
||||
|
||||
func Test_setAppSpecOptions(t *testing.T) {
|
||||
f := newAppOptionsFixture()
|
||||
t.Run("SyncPolicy", func(t *testing.T) {
|
||||
assert.NoError(t, f.SetFlag("sync-policy", "automated"))
|
||||
assert.NotNil(t, f.spec.SyncPolicy.Automated)
|
||||
|
||||
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)
|
||||
})
|
||||
t.Run("SyncOptions", func(t *testing.T) {
|
||||
assert.NoError(t, f.SetFlag("sync-option", "a=1"))
|
||||
assert.True(t, f.spec.SyncPolicy.SyncOptions.HasOption("a=1"))
|
||||
|
||||
// remove the options using !
|
||||
assert.NoError(t, f.SetFlag("sync-option", "!a=1"))
|
||||
assert.Nil(t, f.spec.SyncPolicy)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,22 +2,21 @@ package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/io"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
certificatepkg "github.com/argoproj/argo-cd/pkg/apiclient/certificate"
|
||||
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
certutil "github.com/argoproj/argo-cd/util/cert"
|
||||
|
||||
"crypto/x509"
|
||||
)
|
||||
|
||||
// NewCertCommand returns a new instance of an `argocd repo` command
|
||||
@@ -66,7 +65,7 @@ func NewCertAddTLSCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
Short: "Add TLS certificate data for connecting to repository server SERVERNAME",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
conn, certIf := argocdclient.NewClientOrDie(clientOpts).NewCertClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
@@ -149,7 +148,7 @@ func NewCertAddSSHCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
|
||||
conn, certIf := argocdclient.NewClientOrDie(clientOpts).NewCertClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
|
||||
var sshKnownHostsLists []string
|
||||
var err error
|
||||
@@ -174,18 +173,20 @@ func NewCertAddSSHCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
}
|
||||
|
||||
for _, knownHostsEntry := range sshKnownHostsLists {
|
||||
hostname, certSubType, certData, err := certutil.TokenizeSSHKnownHostsEntry(knownHostsEntry)
|
||||
_, certSubType, certData, err := certutil.TokenizeSSHKnownHostsEntry(knownHostsEntry)
|
||||
errors.CheckError(err)
|
||||
_, _, err = certutil.KnownHostsLineToPublicKey(knownHostsEntry)
|
||||
hostnameList, _, err := certutil.KnownHostsLineToPublicKey(knownHostsEntry)
|
||||
errors.CheckError(err)
|
||||
certificate := appsv1.RepositoryCertificate{
|
||||
ServerName: hostname,
|
||||
CertType: "ssh",
|
||||
CertSubType: certSubType,
|
||||
CertData: certData,
|
||||
// Each key could be valid for multiple hostnames
|
||||
for _, hostname := range hostnameList {
|
||||
certificate := appsv1.RepositoryCertificate{
|
||||
ServerName: hostname,
|
||||
CertType: "ssh",
|
||||
CertSubType: certSubType,
|
||||
CertData: certData,
|
||||
}
|
||||
certificates = append(certificates, certificate)
|
||||
}
|
||||
|
||||
certificates = append(certificates, certificate)
|
||||
}
|
||||
|
||||
certList := &appsv1.RepositoryCertificateList{Items: certificates}
|
||||
@@ -219,7 +220,7 @@ func NewCertRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
os.Exit(1)
|
||||
}
|
||||
conn, certIf := argocdclient.NewClientOrDie(clientOpts).NewCertClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
hostNamePattern := args[0]
|
||||
|
||||
// Prevent the user from specifying a wildcard as hostname as precaution
|
||||
@@ -274,7 +275,7 @@ func NewCertListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
}
|
||||
|
||||
conn, certIf := argocdclient.NewClientOrDie(clientOpts).NewCertClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
certificates, err := certIf.ListCertificates(context.Background(), &certificatepkg.RepositoryCertificateQuery{HostNamePattern: hostNamePattern, CertType: certType})
|
||||
errors.CheckError(err)
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ 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"
|
||||
@@ -16,11 +18,9 @@ import (
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
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"
|
||||
"github.com/argoproj/argo-cd/util/clusterauth"
|
||||
)
|
||||
|
||||
@@ -60,10 +60,12 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
var (
|
||||
inCluster bool
|
||||
upsert bool
|
||||
serviceAccount string
|
||||
awsRoleArn string
|
||||
awsClusterName string
|
||||
systemNamespace string
|
||||
namespaces []string
|
||||
name string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "add CONTEXT",
|
||||
@@ -101,11 +103,18 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
// Install RBAC resources for managing the cluster
|
||||
clientset, err := kubernetes.NewForConfig(conf)
|
||||
errors.CheckError(err)
|
||||
managerBearerToken, err = clusterauth.InstallClusterManagerRBAC(clientset, systemNamespace, namespaces)
|
||||
if serviceAccount != "" {
|
||||
managerBearerToken, err = clusterauth.GetServiceAccountBearerToken(clientset, systemNamespace, serviceAccount)
|
||||
} else {
|
||||
managerBearerToken, err = clusterauth.InstallClusterManagerRBAC(clientset, systemNamespace, namespaces)
|
||||
}
|
||||
errors.CheckError(err)
|
||||
}
|
||||
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
if name != "" {
|
||||
contextName = name
|
||||
}
|
||||
clst := newCluster(contextName, namespaces, conf, managerBearerToken, awsAuthConf)
|
||||
if inCluster {
|
||||
clst.Server = common.KubernetesInternalAPIServerAddr
|
||||
@@ -122,10 +131,12 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
command.PersistentFlags().StringVar(&pathOpts.LoadingRules.ExplicitPath, pathOpts.ExplicitFileFlag, pathOpts.LoadingRules.ExplicitPath, "use a particular kubeconfig file")
|
||||
command.Flags().BoolVar(&inCluster, "in-cluster", false, "Indicates Argo CD resides inside this cluster and should connect using the internal k8s hostname (kubernetes.default.svc)")
|
||||
command.Flags().BoolVar(&upsert, "upsert", false, "Override an existing cluster with the same name even if the spec differs")
|
||||
command.Flags().StringVar(&awsClusterName, "aws-cluster-name", "", "AWS Cluster name if set then aws-iam-authenticator will be used to access cluster")
|
||||
command.Flags().StringVar(&serviceAccount, "service-account", "", fmt.Sprintf("System namespace service account to use for kubernetes resource management. If not set then default \"%s\" SA will be created", clusterauth.ArgoCDManagerServiceAccount))
|
||||
command.Flags().StringVar(&awsClusterName, "aws-cluster-name", "", "AWS Cluster name if set then aws cli eks token command will be used to access cluster")
|
||||
command.Flags().StringVar(&awsRoleArn, "aws-role-arn", "", "Optional AWS role arn. If set then AWS IAM Authenticator assume a role to perform cluster operations instead of the default AWS credential provider chain.")
|
||||
command.Flags().StringVar(&systemNamespace, "system-namespace", common.DefaultSystemNamespace, "Use different system namespace")
|
||||
command.Flags().StringArrayVar(&namespaces, "namespace", nil, "List of namespaces which are allowed to manage")
|
||||
command.Flags().StringVar(&name, "name", "", "Overwrite the cluster name")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -173,22 +184,42 @@ func newCluster(name string, namespaces []string, conf *rest.Config, managerBear
|
||||
Insecure: conf.TLSClientConfig.Insecure,
|
||||
ServerName: conf.TLSClientConfig.ServerName,
|
||||
CAData: conf.TLSClientConfig.CAData,
|
||||
CertData: conf.TLSClientConfig.CertData,
|
||||
KeyData: conf.TLSClientConfig.KeyData,
|
||||
}
|
||||
if len(conf.TLSClientConfig.CAData) == 0 && conf.TLSClientConfig.CAFile != "" {
|
||||
data, err := ioutil.ReadFile(conf.TLSClientConfig.CAFile)
|
||||
errors.CheckError(err)
|
||||
tlsClientConfig.CAData = data
|
||||
}
|
||||
if len(conf.TLSClientConfig.CertData) == 0 && conf.TLSClientConfig.CertFile != "" {
|
||||
data, err := ioutil.ReadFile(conf.TLSClientConfig.CertFile)
|
||||
errors.CheckError(err)
|
||||
tlsClientConfig.CertData = data
|
||||
}
|
||||
if len(conf.TLSClientConfig.KeyData) == 0 && conf.TLSClientConfig.KeyFile != "" {
|
||||
data, err := ioutil.ReadFile(conf.TLSClientConfig.KeyFile)
|
||||
errors.CheckError(err)
|
||||
tlsClientConfig.KeyData = data
|
||||
}
|
||||
|
||||
clst := argoappv1.Cluster{
|
||||
Server: conf.Host,
|
||||
Name: name,
|
||||
Namespaces: namespaces,
|
||||
Config: argoappv1.ClusterConfig{
|
||||
BearerToken: managerBearerToken,
|
||||
TLSClientConfig: tlsClientConfig,
|
||||
AWSAuthConfig: awsAuthConf,
|
||||
},
|
||||
}
|
||||
|
||||
// Bearer token will preferentially be used for auth if present,
|
||||
// Even in presence of key/cert credentials
|
||||
// So set bearer token only if the key/cert data is absent
|
||||
if len(tlsClientConfig.CertData) == 0 || len(tlsClientConfig.KeyData) == 0 {
|
||||
clst.Config.BearerToken = managerBearerToken
|
||||
}
|
||||
|
||||
return &clst
|
||||
}
|
||||
|
||||
@@ -207,7 +238,7 @@ func NewClusterGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
os.Exit(1)
|
||||
}
|
||||
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
clusters := make([]argoappv1.Cluster, 0)
|
||||
for _, clusterName := range args {
|
||||
clst, err := clusterIf.Get(context.Background(), &clusterpkg.ClusterQuery{Server: clusterName})
|
||||
@@ -276,7 +307,7 @@ func NewClusterRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
os.Exit(1)
|
||||
}
|
||||
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
|
||||
// clientset, err := kubernetes.NewForConfig(conf)
|
||||
// errors.CheckError(err)
|
||||
@@ -324,7 +355,7 @@ func NewClusterListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
Short: "List configured clusters",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
clusters, err := clusterIf.List(context.Background(), &clusterpkg.ClusterQuery{})
|
||||
errors.CheckError(err)
|
||||
switch output {
|
||||
@@ -356,7 +387,7 @@ func NewClusterRotateAuthCommand(clientOpts *argocdclient.ClientOptions) *cobra.
|
||||
os.Exit(1)
|
||||
}
|
||||
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
clusterQuery := clusterpkg.ClusterQuery{
|
||||
Server: args[0],
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/rest"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
@@ -29,3 +32,52 @@ func Test_printClusterTable(t *testing.T) {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func Test_newCluster(t *testing.T) {
|
||||
clusterWithData := newCluster("test-cluster", []string{"test-namespace"}, &rest.Config{
|
||||
TLSClientConfig: rest.TLSClientConfig{
|
||||
Insecure: false,
|
||||
ServerName: "test-endpoint.example.com",
|
||||
CAData: []byte("test-ca-data"),
|
||||
CertData: []byte("test-cert-data"),
|
||||
KeyData: []byte("test-key-data"),
|
||||
},
|
||||
Host: "test-endpoint.example.com",
|
||||
},
|
||||
"test-bearer-token",
|
||||
&v1alpha1.AWSAuthConfig{})
|
||||
|
||||
assert.Equal(t, "test-cert-data", string(clusterWithData.Config.CertData))
|
||||
assert.Equal(t, "test-key-data", string(clusterWithData.Config.KeyData))
|
||||
assert.Equal(t, "", clusterWithData.Config.BearerToken)
|
||||
|
||||
clusterWithFiles := newCluster("test-cluster", []string{"test-namespace"}, &rest.Config{
|
||||
TLSClientConfig: rest.TLSClientConfig{
|
||||
Insecure: false,
|
||||
ServerName: "test-endpoint.example.com",
|
||||
CAData: []byte("test-ca-data"),
|
||||
CertFile: "./testdata/test.cert.pem",
|
||||
KeyFile: "./testdata/test.key.pem",
|
||||
},
|
||||
Host: "test-endpoint.example.com",
|
||||
},
|
||||
"test-bearer-token",
|
||||
&v1alpha1.AWSAuthConfig{})
|
||||
|
||||
assert.True(t, strings.Contains(string(clusterWithFiles.Config.CertData), "test-cert-data"))
|
||||
assert.True(t, strings.Contains(string(clusterWithFiles.Config.KeyData), "test-key-data"))
|
||||
assert.Equal(t, "", clusterWithFiles.Config.BearerToken)
|
||||
|
||||
clusterWithBearerToken := newCluster("test-cluster", []string{"test-namespace"}, &rest.Config{
|
||||
TLSClientConfig: rest.TLSClientConfig{
|
||||
Insecure: false,
|
||||
ServerName: "test-endpoint.example.com",
|
||||
CAData: []byte("test-ca-data"),
|
||||
},
|
||||
Host: "test-endpoint.example.com",
|
||||
},
|
||||
"test-bearer-token",
|
||||
&v1alpha1.AWSAuthConfig{})
|
||||
|
||||
assert.Equal(t, "test-bearer-token", clusterWithBearerToken.Config.BearerToken)
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ package commands
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
)
|
||||
|
||||
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/argoproj/gitops-engine/pkg/utils/errors"
|
||||
argoio "github.com/argoproj/gitops-engine/pkg/utils/io"
|
||||
"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"
|
||||
)
|
||||
|
||||
// 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,12 +2,17 @@ package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"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"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -15,11 +20,9 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
sessionpkg "github.com/argoproj/argo-cd/pkg/apiclient/session"
|
||||
settingspkg "github.com/argoproj/argo-cd/pkg/apiclient/settings"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
grpc_util "github.com/argoproj/argo-cd/util/grpc"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
@@ -41,41 +44,55 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
Short: "Log in to Argo CD",
|
||||
Long: "Log in to Argo CD",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
var server string
|
||||
|
||||
if len(args) != 1 && !globalClientOpts.PortForward {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
server := args[0]
|
||||
tlsTestResult, err := grpc_util.TestTLS(server)
|
||||
errors.CheckError(err)
|
||||
if !tlsTestResult.TLS {
|
||||
if !globalClientOpts.PlainText {
|
||||
if !cli.AskToProceed("WARNING: server is not configured with TLS. Proceed (y/n)? ") {
|
||||
os.Exit(1)
|
||||
|
||||
if globalClientOpts.PortForward {
|
||||
server = "port-forward"
|
||||
} else {
|
||||
server = args[0]
|
||||
tlsTestResult, err := grpc_util.TestTLS(server)
|
||||
errors.CheckError(err)
|
||||
if !tlsTestResult.TLS {
|
||||
if !globalClientOpts.PlainText {
|
||||
if !cli.AskToProceed("WARNING: server is not configured with TLS. Proceed (y/n)? ") {
|
||||
os.Exit(1)
|
||||
}
|
||||
globalClientOpts.PlainText = true
|
||||
}
|
||||
globalClientOpts.PlainText = true
|
||||
}
|
||||
} else if tlsTestResult.InsecureErr != nil {
|
||||
if !globalClientOpts.Insecure {
|
||||
if !cli.AskToProceed(fmt.Sprintf("WARNING: server certificate had error: %s. Proceed insecurely (y/n)? ", tlsTestResult.InsecureErr)) {
|
||||
os.Exit(1)
|
||||
} else if tlsTestResult.InsecureErr != nil {
|
||||
if !globalClientOpts.Insecure {
|
||||
if !cli.AskToProceed(fmt.Sprintf("WARNING: server certificate had error: %s. Proceed insecurely (y/n)? ", tlsTestResult.InsecureErr)) {
|
||||
os.Exit(1)
|
||||
}
|
||||
globalClientOpts.Insecure = true
|
||||
}
|
||||
globalClientOpts.Insecure = true
|
||||
}
|
||||
}
|
||||
clientOpts := argocdclient.ClientOptions{
|
||||
ConfigPath: "",
|
||||
ServerAddr: server,
|
||||
Insecure: globalClientOpts.Insecure,
|
||||
PlainText: globalClientOpts.PlainText,
|
||||
GRPCWeb: globalClientOpts.GRPCWeb,
|
||||
ConfigPath: "",
|
||||
ServerAddr: server,
|
||||
Insecure: globalClientOpts.Insecure,
|
||||
PlainText: globalClientOpts.PlainText,
|
||||
GRPCWeb: globalClientOpts.GRPCWeb,
|
||||
GRPCWebRootPath: globalClientOpts.GRPCWebRootPath,
|
||||
PortForward: globalClientOpts.PortForward,
|
||||
PortForwardNamespace: globalClientOpts.PortForwardNamespace,
|
||||
}
|
||||
acdClient := argocdclient.NewClientOrDie(&clientOpts)
|
||||
setConn, setIf := acdClient.NewSettingsClientOrDie()
|
||||
defer util.Close(setConn)
|
||||
defer io.Close(setConn)
|
||||
|
||||
if ctxName == "" {
|
||||
ctxName = server
|
||||
if globalClientOpts.GRPCWebRootPath != "" {
|
||||
rootPath := strings.TrimRight(strings.TrimLeft(globalClientOpts.GRPCWebRootPath, "/"), "/")
|
||||
ctxName = fmt.Sprintf("%s/%s", server, rootPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Perform the login
|
||||
@@ -99,7 +116,7 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
SkipClaimsValidation: true,
|
||||
}
|
||||
claims := jwt.MapClaims{}
|
||||
_, _, err = parser.ParseUnverified(tokenString, &claims)
|
||||
_, _, err := parser.ParseUnverified(tokenString, &claims)
|
||||
errors.CheckError(err)
|
||||
|
||||
fmt.Printf("'%s' logged in successfully\n", userDisplayName(claims))
|
||||
@@ -110,10 +127,11 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
localCfg = &localconfig.LocalConfig{}
|
||||
}
|
||||
localCfg.UpsertServer(localconfig.Server{
|
||||
Server: server,
|
||||
PlainText: globalClientOpts.PlainText,
|
||||
Insecure: globalClientOpts.Insecure,
|
||||
GRPCWeb: globalClientOpts.GRPCWeb,
|
||||
Server: server,
|
||||
PlainText: globalClientOpts.PlainText,
|
||||
Insecure: globalClientOpts.Insecure,
|
||||
GRPCWeb: globalClientOpts.GRPCWeb,
|
||||
GRPCWebRootPath: globalClientOpts.GRPCWebRootPath,
|
||||
})
|
||||
localCfg.UpsertUser(localconfig.User{
|
||||
Name: ctxName,
|
||||
@@ -176,6 +194,11 @@ func oauth2Login(ctx context.Context, port int, oidcSettings *settingspkg.OIDCCo
|
||||
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) {
|
||||
@@ -215,7 +238,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
|
||||
@@ -251,6 +275,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...)
|
||||
@@ -283,7 +309,7 @@ func oauth2Login(ctx context.Context, port int, oidcSettings *settingspkg.OIDCCo
|
||||
func passwordLogin(acdClient argocdclient.Client, username, password string) string {
|
||||
username, password = cli.PromptCredentials(username, password)
|
||||
sessConn, sessionIf := acdClient.NewSessionClientOrDie()
|
||||
defer util.Close(sessConn)
|
||||
defer io.Close(sessConn)
|
||||
sessionRequest := sessionpkg.SessionCreateRequest{
|
||||
Username: username,
|
||||
Password: password,
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
)
|
||||
|
||||
@@ -12,28 +12,31 @@ import (
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
argoio "github.com/argoproj/gitops-engine/pkg/utils/io"
|
||||
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"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
projectpkg "github.com/argoproj/argo-cd/pkg/apiclient/project"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/config"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
"github.com/argoproj/argo-cd/util/gpg"
|
||||
)
|
||||
|
||||
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,12 +186,13 @@ func NewProjectCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
Description: opts.description,
|
||||
Destinations: opts.GetDestinations(),
|
||||
SourceRepos: opts.sources,
|
||||
SignatureKeys: opts.GetSignatureKeys(),
|
||||
OrphanedResources: getOrphanedResourcesSettings(c, opts),
|
||||
},
|
||||
}
|
||||
}
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer argoio.Close(conn)
|
||||
_, err := projIf.Create(context.Background(), &projectpkg.ProjectCreateRequest{Project: &proj, Upsert: upsert})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
@@ -200,7 +222,7 @@ func NewProjectSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
}
|
||||
projName := args[0]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer argoio.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
@@ -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{
|
||||
@@ -247,7 +346,7 @@ func NewProjectAddDestinationCommand(clientOpts *argocdclient.ClientOptions) *co
|
||||
server := args[1]
|
||||
namespace := args[2]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer argoio.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
@@ -279,7 +378,7 @@ func NewProjectRemoveDestinationCommand(clientOpts *argocdclient.ClientOptions)
|
||||
server := args[1]
|
||||
namespace := args[2]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer argoio.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
@@ -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{
|
||||
@@ -317,7 +506,7 @@ func NewProjectAddSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
|
||||
projName := args[0]
|
||||
url := args[1]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer argoio.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
@@ -340,8 +529,46 @@ func NewProjectAddSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
|
||||
return command
|
||||
}
|
||||
|
||||
func modifyProjectResourceCmd(cmdUse, cmdDesc string, clientOpts *argocdclient.ClientOptions, action func(proj *v1alpha1.AppProject, group string, kind string) bool) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
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
|
||||
}
|
||||
}
|
||||
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 modifyResourceListCmd(cmdUse, cmdDesc string, clientOpts *argocdclient.ClientOptions, allow bool, namespacedList bool) *cobra.Command {
|
||||
var (
|
||||
listType string
|
||||
defaultList string
|
||||
)
|
||||
if namespacedList {
|
||||
defaultList = "black"
|
||||
} else {
|
||||
defaultList = "white"
|
||||
}
|
||||
var command = &cobra.Command{
|
||||
Use: cmdUse,
|
||||
Short: cmdDesc,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
@@ -351,91 +578,67 @@ func modifyProjectResourceCmd(cmdUse, cmdDesc string, clientOpts *argocdclient.C
|
||||
}
|
||||
projName, group, kind := args[0], args[1], args[2]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer argoio.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
var list, white, black *[]metav1.GroupKind
|
||||
var listAction, listDesc string
|
||||
var add bool
|
||||
if namespacedList {
|
||||
white, black = &proj.Spec.NamespaceResourceWhitelist, &proj.Spec.NamespaceResourceBlacklist
|
||||
listDesc = "namespaced"
|
||||
} else {
|
||||
white, black = &proj.Spec.ClusterResourceWhitelist, &proj.Spec.ClusterResourceBlacklist
|
||||
listDesc = "cluster"
|
||||
}
|
||||
|
||||
if action(proj, group, kind) {
|
||||
if listType == "white" {
|
||||
list = white
|
||||
listAction = "whitelisted"
|
||||
add = allow
|
||||
} else {
|
||||
list = black
|
||||
listAction = "blacklisted"
|
||||
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(&listType, "list", "l", defaultList, "Use blacklist or whitelist. This can only be 'white' or 'black'")
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectAllowNamespaceResourceCommand returns a new instance of an `deny-cluster-resources` command
|
||||
func NewProjectAllowNamespaceResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
use := "allow-namespace-resource PROJECT GROUP KIND"
|
||||
desc := "Removes a namespaced API resource from the blacklist"
|
||||
return modifyProjectResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
|
||||
index := -1
|
||||
for i, item := range proj.Spec.NamespaceResourceBlacklist {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
fmt.Printf("Group '%s' and kind '%s' not in blacklisted namespaced resources\n", group, kind)
|
||||
return false
|
||||
}
|
||||
proj.Spec.NamespaceResourceBlacklist = append(proj.Spec.NamespaceResourceBlacklist[:index], proj.Spec.NamespaceResourceBlacklist[index+1:]...)
|
||||
return true
|
||||
})
|
||||
desc := "Removes a namespaced API resource from the blacklist or add a namespaced API resource to the whitelist"
|
||||
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"
|
||||
return modifyProjectResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
|
||||
for _, item := range proj.Spec.NamespaceResourceBlacklist {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
fmt.Printf("Group '%s' and kind '%s' already present in blacklisted namespaced resources\n", group, kind)
|
||||
return false
|
||||
}
|
||||
}
|
||||
proj.Spec.NamespaceResourceBlacklist = append(proj.Spec.NamespaceResourceBlacklist, v1.GroupKind{Group: group, Kind: kind})
|
||||
return true
|
||||
})
|
||||
desc := "Adds a namespaced API resource to the blacklist or removes a namespaced API resource from the whitelist"
|
||||
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 modifyProjectResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
|
||||
index := -1
|
||||
for i, item := range proj.Spec.ClusterResourceWhitelist {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
fmt.Printf("Group '%s' and kind '%s' not in whitelisted cluster resources\n", group, kind)
|
||||
return false
|
||||
}
|
||||
proj.Spec.ClusterResourceWhitelist = append(proj.Spec.ClusterResourceWhitelist[:index], proj.Spec.ClusterResourceWhitelist[index+1:]...)
|
||||
return true
|
||||
})
|
||||
desc := "Removes a cluster-scoped API resource from the whitelist and adds it to blacklist"
|
||||
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 modifyProjectResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
|
||||
for _, item := range proj.Spec.ClusterResourceWhitelist {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
fmt.Printf("Group '%s' and kind '%s' already present in whitelisted cluster resources\n", group, kind)
|
||||
return false
|
||||
}
|
||||
}
|
||||
proj.Spec.ClusterResourceWhitelist = append(proj.Spec.ClusterResourceWhitelist, v1.GroupKind{Group: group, Kind: kind})
|
||||
return true
|
||||
})
|
||||
desc := "Adds a cluster-scoped API resource to the whitelist and removes it from blacklist"
|
||||
return modifyResourceListCmd(use, desc, clientOpts, true, false)
|
||||
}
|
||||
|
||||
// NewProjectRemoveSourceCommand returns a new instance of an `argocd proj remove-src` command
|
||||
@@ -451,7 +654,7 @@ func NewProjectRemoveSourceCommand(clientOpts *argocdclient.ClientOptions) *cobr
|
||||
projName := args[0]
|
||||
url := args[1]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer argoio.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
@@ -487,7 +690,7 @@ func NewProjectDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
os.Exit(1)
|
||||
}
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer argoio.Close(conn)
|
||||
for _, name := range args {
|
||||
_, err := projIf.Delete(context.Background(), &projectpkg.ProjectQuery{Name: name})
|
||||
errors.CheckError(err)
|
||||
@@ -507,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)
|
||||
}
|
||||
@@ -524,7 +727,7 @@ func NewProjectListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
Short: "List projects",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer argoio.Close(conn)
|
||||
projects, err := projIf.List(context.Background(), &projectpkg.ProjectQuery{})
|
||||
errors.CheckError(err)
|
||||
switch output {
|
||||
@@ -548,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>"
|
||||
@@ -583,7 +790,13 @@ 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) {
|
||||
@@ -631,6 +844,18 @@ func printProject(p *v1alpha1.AppProject) {
|
||||
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))
|
||||
|
||||
}
|
||||
@@ -650,7 +875,7 @@ func NewProjectGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
}
|
||||
projName := args[0]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer argoio.Close(conn)
|
||||
p, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
@@ -680,7 +905,7 @@ func NewProjectEditCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
}
|
||||
projName := args[0]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer argoio.Close(conn)
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
projData, err := json.Marshal(proj.Spec)
|
||||
|
||||
@@ -7,14 +7,14 @@ import (
|
||||
"strconv"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/io"
|
||||
timeutil "github.com/argoproj/pkg/time"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
projectpkg "github.com/argoproj/argo-cd/pkg/apiclient/project"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -60,7 +60,7 @@ func NewProjectRoleAddPolicyCommand(clientOpts *argocdclient.ClientOptions) *cob
|
||||
projName := args[0]
|
||||
roleName := args[1]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
@@ -95,7 +95,7 @@ func NewProjectRoleRemovePolicyCommand(clientOpts *argocdclient.ClientOptions) *
|
||||
projName := args[0]
|
||||
roleName := args[1]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
@@ -140,7 +140,7 @@ func NewProjectRoleCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.
|
||||
projName := args[0]
|
||||
roleName := args[1]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
@@ -174,7 +174,7 @@ func NewProjectRoleDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.
|
||||
projName := args[0]
|
||||
roleName := args[1]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
@@ -211,7 +211,7 @@ func NewProjectRoleCreateTokenCommand(clientOpts *argocdclient.ClientOptions) *c
|
||||
projName := args[0]
|
||||
roleName := args[1]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
duration, err := timeutil.ParseDuration(expiresIn)
|
||||
errors.CheckError(err)
|
||||
token, err := projIf.CreateToken(context.Background(), &projectpkg.ProjectTokenCreateRequest{Project: projName, Role: roleName, ExpiresIn: int64(duration.Seconds())})
|
||||
@@ -240,7 +240,7 @@ func NewProjectRoleDeleteTokenCommand(clientOpts *argocdclient.ClientOptions) *c
|
||||
errors.CheckError(err)
|
||||
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
|
||||
_, err = projIf.DeleteToken(context.Background(), &projectpkg.ProjectTokenDeleteRequest{Project: projName, Role: roleName, Iat: issuedAt})
|
||||
errors.CheckError(err)
|
||||
@@ -281,7 +281,7 @@ func NewProjectRoleListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
}
|
||||
projName := args[0]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
|
||||
project, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
@@ -315,7 +315,7 @@ func NewProjectRoleGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
|
||||
projName := args[0]
|
||||
roleName := args[1]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
@@ -332,7 +332,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)
|
||||
@@ -357,7 +357,7 @@ func NewProjectRoleAddGroupCommand(clientOpts *argocdclient.ClientOptions) *cobr
|
||||
}
|
||||
projName, roleName, groupName := args[0], args[1], args[2]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
updated, err := proj.AddGroupToRole(roleName, groupName)
|
||||
@@ -386,7 +386,7 @@ func NewProjectRoleRemoveGroupCommand(clientOpts *argocdclient.ClientOptions) *c
|
||||
}
|
||||
projName, roleName, groupName := args[0], args[1], args[2]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
updated, err := proj.RemoveGroupFromRole(roleName, groupName)
|
||||
|
||||
@@ -2,21 +2,19 @@ package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"strconv"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/io"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
projectpkg "github.com/argoproj/argo-cd/pkg/apiclient/project"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
)
|
||||
|
||||
// NewProjectWindowsCommand returns a new instance of the `argocd proj windows` command
|
||||
@@ -55,7 +53,7 @@ func NewProjectWindowsDisableManualSyncCommand(clientOpts *argocdclient.ClientOp
|
||||
errors.CheckError(err)
|
||||
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
@@ -90,7 +88,7 @@ func NewProjectWindowsEnableManualSyncCommand(clientOpts *argocdclient.ClientOpt
|
||||
errors.CheckError(err)
|
||||
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
@@ -129,7 +127,7 @@ func NewProjectWindowsAddWindowCommand(clientOpts *argocdclient.ClientOptions) *
|
||||
}
|
||||
projName := args[0]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
@@ -168,7 +166,7 @@ func NewProjectWindowsDeleteCommand(clientOpts *argocdclient.ClientOptions) *cob
|
||||
errors.CheckError(err)
|
||||
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
@@ -207,7 +205,7 @@ func NewProjectWindowsUpdateCommand(clientOpts *argocdclient.ClientOptions) *cob
|
||||
errors.CheckError(err)
|
||||
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
@@ -248,7 +246,7 @@ func NewProjectWindowsListCommand(clientOpts *argocdclient.ClientOptions) *cobra
|
||||
}
|
||||
projName := args[0]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
@@ -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"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
settingspkg "github.com/argoproj/argo-cd/pkg/apiclient/settings"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
"github.com/argoproj/argo-cd/util/session"
|
||||
)
|
||||
@@ -43,11 +43,12 @@ func NewReloginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
var tokenString string
|
||||
var refreshToken string
|
||||
clientOpts := argocdclient.ClientOptions{
|
||||
ConfigPath: "",
|
||||
ServerAddr: configCtx.Server.Server,
|
||||
Insecure: configCtx.Server.Insecure,
|
||||
GRPCWeb: globalClientOpts.GRPCWeb,
|
||||
PlainText: configCtx.Server.PlainText,
|
||||
ConfigPath: "",
|
||||
ServerAddr: configCtx.Server.Server,
|
||||
Insecure: configCtx.Server.Insecure,
|
||||
GRPCWeb: globalClientOpts.GRPCWeb,
|
||||
GRPCWebRootPath: globalClientOpts.GRPCWebRootPath,
|
||||
PlainText: configCtx.Server.PlainText,
|
||||
}
|
||||
acdClient := argocdclient.NewClientOrDie(&clientOpts)
|
||||
claims, err := configCtx.User.Claims()
|
||||
@@ -58,7 +59,7 @@ func NewReloginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
} else {
|
||||
fmt.Println("Reinitiating SSO login")
|
||||
setConn, setIf := acdClient.NewSettingsClientOrDie()
|
||||
defer util.Close(setConn)
|
||||
defer argoio.Close(setConn)
|
||||
ctx := context.Background()
|
||||
httpClient, err := acdClient.HTTPClient()
|
||||
errors.CheckError(err)
|
||||
|
||||
@@ -7,15 +7,15 @@ 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"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
repositorypkg "github.com/argoproj/argo-cd/pkg/apiclient/repository"
|
||||
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
)
|
||||
@@ -32,6 +32,7 @@ func NewRepoCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
}
|
||||
|
||||
command.AddCommand(NewRepoAddCommand(clientOpts))
|
||||
command.AddCommand(NewRepoGetCommand(clientOpts))
|
||||
command.AddCommand(NewRepoListCommand(clientOpts))
|
||||
command.AddCommand(NewRepoRemoveCommand(clientOpts))
|
||||
return command
|
||||
@@ -52,7 +53,10 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
|
||||
// For better readability and easier formatting
|
||||
var repoAddExamples = ` # Add a Git repository via SSH using a private key for authentication, ignoring the server's host key:
|
||||
argocd repo add git@git.example.com:repos/repo --insecure-ignore-host-key --ssh-private-key-path ~/id_rsa
|
||||
argocd repo add git@git.example.com:repos/repo --insecure-ignore-host-key --ssh-private-key-path ~/id_rsa
|
||||
|
||||
# Add a Git repository via SSH on a non-default port - need to use ssh:// style URLs here
|
||||
argocd repo add ssh://git@git.example.com:2222/repos/repo --ssh-private-key-path ~/id_rsa
|
||||
|
||||
# Add a private Git repository via HTTPS using username/password and TLS client certificates:
|
||||
argocd repo add https://git.example.com/repos/repo --username git --password secret --tls-client-cert-path ~/mycert.crt --tls-client-cert-key-path ~/mycert.key
|
||||
@@ -128,7 +132,7 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
}
|
||||
|
||||
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
|
||||
// If the user set a username, but didn't supply password via --password,
|
||||
// then we prompt for it
|
||||
@@ -162,7 +166,7 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
Upsert: upsert,
|
||||
}
|
||||
|
||||
createdRepo, err := repoIf.CreateRepository(context.Background(), &repoCreateReq)
|
||||
createdRepo, err := repoIf.Create(context.Background(), &repoCreateReq)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("repository '%s' added\n", createdRepo.Repo)
|
||||
},
|
||||
@@ -174,7 +178,7 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
command.Flags().StringVar(&sshPrivateKeyPath, "ssh-private-key-path", "", "path to the private ssh key (e.g. ~/.ssh/id_rsa)")
|
||||
command.Flags().StringVar(&tlsClientCertPath, "tls-client-cert-path", "", "path to the TLS client cert (must be PEM format)")
|
||||
command.Flags().StringVar(&tlsClientCertKeyPath, "tls-client-cert-key-path", "", "path to the TLS client cert's key path (must be PEM format)")
|
||||
command.Flags().BoolVar(&insecureIgnoreHostKey, "insecure-ignore-host-key", false, "disables SSH strict host key checking (deprecated, use --insecure-skip-server-validation instead)")
|
||||
command.Flags().BoolVar(&insecureIgnoreHostKey, "insecure-ignore-host-key", false, "disables SSH strict host key checking (deprecated, use --insecure-skip-server-verification instead)")
|
||||
command.Flags().BoolVar(&insecureSkipServerVerification, "insecure-skip-server-verification", false, "disables server certificate and host key checks")
|
||||
command.Flags().BoolVar(&enableLfs, "enable-lfs", false, "enable git-lfs (Large File Support) on this repository")
|
||||
command.Flags().BoolVar(&upsert, "upsert", false, "Override an existing repository with the same name even if the spec differs")
|
||||
@@ -192,9 +196,9 @@ func NewRepoRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
os.Exit(1)
|
||||
}
|
||||
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
for _, repoURL := range args {
|
||||
_, err := repoIf.DeleteRepository(context.Background(), &repositorypkg.RepoQuery{Repo: repoURL})
|
||||
_, err := repoIf.Delete(context.Background(), &repositorypkg.RepoQuery{Repo: repoURL})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
@@ -240,7 +244,7 @@ func NewRepoListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
Short: "List configured repositories",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
forceRefresh := false
|
||||
switch refresh {
|
||||
case "":
|
||||
@@ -250,7 +254,7 @@ func NewRepoListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
err := fmt.Errorf("--refresh must be one of: 'hard'")
|
||||
errors.CheckError(err)
|
||||
}
|
||||
repos, err := repoIf.ListRepositories(context.Background(), &repositorypkg.RepoQuery{ForceRefresh: forceRefresh})
|
||||
repos, err := repoIf.List(context.Background(), &repositorypkg.RepoQuery{ForceRefresh: forceRefresh})
|
||||
errors.CheckError(err)
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
@@ -270,3 +274,52 @@ func NewRepoListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
command.Flags().StringVar(&refresh, "refresh", "", "Force a cache refresh on connection status")
|
||||
return command
|
||||
}
|
||||
|
||||
// NewRepoGetCommand returns a new instance of an `argocd repo rm` command
|
||||
func NewRepoGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
refresh string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Get a configured repository by URL",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Repository URL
|
||||
repoURL := args[0]
|
||||
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoClientOrDie()
|
||||
defer io.Close(conn)
|
||||
forceRefresh := false
|
||||
switch refresh {
|
||||
case "":
|
||||
case "hard":
|
||||
forceRefresh = true
|
||||
default:
|
||||
err := fmt.Errorf("--refresh must be one of: 'hard'")
|
||||
errors.CheckError(err)
|
||||
}
|
||||
repo, err := repoIf.Get(context.Background(), &repositorypkg.RepoQuery{Repo: repoURL, ForceRefresh: forceRefresh})
|
||||
errors.CheckError(err)
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResource(repo, output)
|
||||
errors.CheckError(err)
|
||||
case "url":
|
||||
fmt.Println(repo.Repo)
|
||||
// wide is the default
|
||||
case "wide", "":
|
||||
printRepoTable(appsv1.Repositories{repo})
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|url")
|
||||
command.Flags().StringVar(&refresh, "refresh", "", "Force a cache refresh on connection status")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -7,19 +7,19 @@ 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"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
repocredspkg "github.com/argoproj/argo-cd/pkg/apiclient/repocreds"
|
||||
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
)
|
||||
|
||||
// NewRepoCredsCommand returns a new instance of an `argocd repo` command
|
||||
// NewRepoCredsCommand returns a new instance of an `argocd repocreds` command
|
||||
func NewRepoCredsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "repocreds",
|
||||
@@ -36,7 +36,7 @@ func NewRepoCredsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
return command
|
||||
}
|
||||
|
||||
// NewRepoCredsAddCommand returns a new instance of an `argocd repo add` command
|
||||
// NewRepoCredsAddCommand returns a new instance of an `argocd repocreds add` command
|
||||
func NewRepoCredsAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
repo appsv1.RepoCreds
|
||||
@@ -50,8 +50,8 @@ func NewRepoCredsAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comma
|
||||
var repocredsAddExamples = ` # Add credentials with user/pass authentication to use for all repositories under https://git.example.com/repos
|
||||
argocd repocreds add https://git.example.com/repos/ --username git --password secret
|
||||
|
||||
# Add credentials with SSH private key authentication to use for all repositories under https://git.example.com/repos
|
||||
argocd repocreds add https://git.example.com/repos/ --ssh-private-key-path ~/.ssh/id_rsa
|
||||
# Add credentials with SSH private key authentication to use for all repositories under ssh://git@git.example.com/repos
|
||||
argocd repocreds add ssh://git@git.example.com/repos/ --ssh-private-key-path ~/.ssh/id_rsa
|
||||
`
|
||||
|
||||
var command = &cobra.Command{
|
||||
@@ -104,7 +104,7 @@ func NewRepoCredsAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comma
|
||||
}
|
||||
|
||||
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoCredsClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
|
||||
// If the user set a username, but didn't supply password via --password,
|
||||
// then we prompt for it
|
||||
@@ -131,7 +131,7 @@ func NewRepoCredsAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comma
|
||||
return command
|
||||
}
|
||||
|
||||
// NewRepoCredsRemoveCommand returns a new instance of an `argocd repo list` command
|
||||
// NewRepoCredsRemoveCommand returns a new instance of an `argocd repocreds rm` command
|
||||
func NewRepoCredsRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "rm CREDSURL",
|
||||
@@ -142,7 +142,7 @@ func NewRepoCredsRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
os.Exit(1)
|
||||
}
|
||||
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoCredsClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
for _, repoURL := range args {
|
||||
_, err := repoIf.DeleteRepositoryCredentials(context.Background(), &repocredspkg.RepoCredsDeleteRequest{Url: repoURL})
|
||||
errors.CheckError(err)
|
||||
@@ -172,7 +172,7 @@ func printRepoCredsUrls(repos []appsv1.RepoCreds) {
|
||||
}
|
||||
}
|
||||
|
||||
// NewRepoCredsListCommand returns a new instance of an `argocd repo rm` command
|
||||
// NewRepoCredsListCommand returns a new instance of an `argocd repo list` command
|
||||
func NewRepoCredsListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
@@ -182,7 +182,7 @@ func NewRepoCredsListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
Short: "List configured repository credentials",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoCredsClientOrDie()
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
repos, err := repoIf.ListRepositoryCredentials(context.Background(), &repocredspkg.RepoCredsQuery{})
|
||||
errors.CheckError(err)
|
||||
switch output {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/config"
|
||||
@@ -15,9 +15,13 @@ func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
}
|
||||
|
||||
var logLevel string
|
||||
var (
|
||||
logFormat string
|
||||
logLevel string
|
||||
)
|
||||
|
||||
func initConfig() {
|
||||
cli.SetLogFormat(logFormat)
|
||||
cli.SetLogLevel(logLevel)
|
||||
}
|
||||
|
||||
@@ -49,6 +53,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)
|
||||
@@ -57,8 +62,12 @@ 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.")
|
||||
command.PersistentFlags().StringVar(&logFormat, "logformat", config.GetFlag("logformat", "text"), "Set the logging format. One of: text|json")
|
||||
command.PersistentFlags().StringVar(&logLevel, "loglevel", config.GetFlag("loglevel", "info"), "Set the logging level. One of: debug|info|warn|error")
|
||||
command.PersistentFlags().StringSliceVarP(&clientOpts.Headers, "header", "H", []string{}, "Sets additional header to all requests made by Argo CD CLI. (Can be repeated multiple times to add multiple headers, also supports comma separated headers)")
|
||||
command.PersistentFlags().BoolVar(&clientOpts.PortForward, "port-forward", config.GetBoolFlag("port-forward"), "Connect to a random argocd-server port using port forwarding")
|
||||
|
||||
3
cmd/argocd/commands/testdata/test.cert.pem
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
test-cert-data
|
||||
-----END CERTIFICATE-----
|
||||
3
cmd/argocd/commands/testdata/test.key.pem
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
test-key-data
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -3,16 +3,17 @@ package commands
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
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"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/pkg/apiclient/version"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
)
|
||||
|
||||
// NewVersionCmd returns a new `version` command to be used as a sub-command to root
|
||||
@@ -25,7 +26,7 @@ func NewVersionCmd(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
|
||||
versionCmd := cobra.Command{
|
||||
Use: "version",
|
||||
Short: fmt.Sprintf("Print version information"),
|
||||
Short: "Print version information",
|
||||
Example: ` # Print the full version of client and server to stdout
|
||||
argocd version
|
||||
|
||||
@@ -39,44 +40,39 @@ func NewVersionCmd(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
argocd version --short -o yaml
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var (
|
||||
versionIf version.VersionServiceClient
|
||||
serverVers *version.VersionMessage
|
||||
conn io.Closer
|
||||
err error
|
||||
)
|
||||
if !client {
|
||||
// Get Server version
|
||||
conn, versionIf = argocdclient.NewClientOrDie(clientOpts).NewVersionClientOrDie()
|
||||
defer util.Close(conn)
|
||||
serverVers, err = versionIf.Version(context.Background(), &empty.Empty{})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
cv := common.GetVersion()
|
||||
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
clientVers := common.GetVersion()
|
||||
version := make(map[string]interface{})
|
||||
if !short {
|
||||
version["client"] = clientVers
|
||||
v := make(map[string]interface{})
|
||||
|
||||
if short {
|
||||
v["client"] = map[string]string{cliName: cv.Version}
|
||||
} else {
|
||||
version["client"] = map[string]string{cliName: clientVers.Version}
|
||||
v["client"] = cv
|
||||
}
|
||||
|
||||
if !client {
|
||||
if !short {
|
||||
version["server"] = serverVers
|
||||
sv := getServerVersion(clientOpts)
|
||||
|
||||
if short {
|
||||
v["server"] = map[string]string{"argocd-server": sv.Version}
|
||||
} else {
|
||||
version["server"] = map[string]string{"argocd-server": serverVers.Version}
|
||||
v["server"] = sv
|
||||
}
|
||||
}
|
||||
err := PrintResource(version, output)
|
||||
|
||||
err := PrintResource(v, output)
|
||||
errors.CheckError(err)
|
||||
case "short":
|
||||
printVersion(serverVers, client, true)
|
||||
case "wide", "":
|
||||
// we use value of short for backward compatibility
|
||||
printVersion(serverVers, client, short)
|
||||
case "wide", "short", "":
|
||||
printClientVersion(&cv, short || (output == "short"))
|
||||
|
||||
if !client {
|
||||
sv := getServerVersion(clientOpts)
|
||||
printServerVersion(sv, short || (output == "short"))
|
||||
}
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
log.Fatalf("unknown output format: %s", output)
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -86,38 +82,52 @@ func NewVersionCmd(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
return &versionCmd
|
||||
}
|
||||
|
||||
func printVersion(serverVers *version.VersionMessage, client bool, short bool) {
|
||||
version := common.GetVersion()
|
||||
func getServerVersion(options *argocdclient.ClientOptions) *version.VersionMessage {
|
||||
conn, versionIf := argocdclient.NewClientOrDie(options).NewVersionClientOrDie()
|
||||
defer argoio.Close(conn)
|
||||
|
||||
v, err := versionIf.Version(context.Background(), &empty.Empty{})
|
||||
errors.CheckError(err)
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func printClientVersion(version *common.Version, short bool) {
|
||||
fmt.Printf("%s: %s\n", cliName, version)
|
||||
if !short {
|
||||
fmt.Printf(" BuildDate: %s\n", version.BuildDate)
|
||||
fmt.Printf(" GitCommit: %s\n", version.GitCommit)
|
||||
fmt.Printf(" GitTreeState: %s\n", version.GitTreeState)
|
||||
if version.GitTag != "" {
|
||||
fmt.Printf(" GitTag: %s\n", version.GitTag)
|
||||
}
|
||||
fmt.Printf(" GoVersion: %s\n", version.GoVersion)
|
||||
fmt.Printf(" Compiler: %s\n", version.Compiler)
|
||||
fmt.Printf(" Platform: %s\n", version.Platform)
|
||||
}
|
||||
if client {
|
||||
|
||||
if short {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("%s: %s\n", "argocd-server", serverVers.Version)
|
||||
if !short {
|
||||
fmt.Printf(" BuildDate: %s\n", serverVers.BuildDate)
|
||||
fmt.Printf(" GitCommit: %s\n", serverVers.GitCommit)
|
||||
fmt.Printf(" GitTreeState: %s\n", serverVers.GitTreeState)
|
||||
if version.GitTag != "" {
|
||||
fmt.Printf(" GitTag: %s\n", serverVers.GitTag)
|
||||
}
|
||||
fmt.Printf(" GoVersion: %s\n", serverVers.GoVersion)
|
||||
fmt.Printf(" Compiler: %s\n", serverVers.Compiler)
|
||||
fmt.Printf(" Platform: %s\n", serverVers.Platform)
|
||||
fmt.Printf(" Ksonnet Version: %s\n", serverVers.KsonnetVersion)
|
||||
fmt.Printf(" Kustomize Version: %s\n", serverVers.KustomizeVersion)
|
||||
fmt.Printf(" Helm Version: %s\n", serverVers.HelmVersion)
|
||||
fmt.Printf(" Kubectl Version: %s\n", serverVers.KubectlVersion)
|
||||
fmt.Printf(" BuildDate: %s\n", version.BuildDate)
|
||||
fmt.Printf(" GitCommit: %s\n", version.GitCommit)
|
||||
fmt.Printf(" GitTreeState: %s\n", version.GitTreeState)
|
||||
if version.GitTag != "" {
|
||||
fmt.Printf(" GitTag: %s\n", version.GitTag)
|
||||
}
|
||||
fmt.Printf(" GoVersion: %s\n", version.GoVersion)
|
||||
fmt.Printf(" Compiler: %s\n", version.Compiler)
|
||||
fmt.Printf(" Platform: %s\n", version.Platform)
|
||||
}
|
||||
|
||||
func printServerVersion(version *version.VersionMessage, short bool) {
|
||||
fmt.Printf("%s: %s\n", "argocd-server", version.Version)
|
||||
|
||||
if short {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf(" BuildDate: %s\n", version.BuildDate)
|
||||
fmt.Printf(" GitCommit: %s\n", version.GitCommit)
|
||||
fmt.Printf(" GitTreeState: %s\n", version.GitTreeState)
|
||||
if version.GitTag != "" {
|
||||
fmt.Printf(" GitTag: %s\n", version.GitTag)
|
||||
}
|
||||
fmt.Printf(" GoVersion: %s\n", version.GoVersion)
|
||||
fmt.Printf(" Compiler: %s\n", version.Compiler)
|
||||
fmt.Printf(" Platform: %s\n", version.Platform)
|
||||
fmt.Printf(" Ksonnet Version: %s\n", version.KsonnetVersion)
|
||||
fmt.Printf(" Kustomize Version: %s\n", version.KustomizeVersion)
|
||||
fmt.Printf(" Helm Version: %s\n", version.HelmVersion)
|
||||
fmt.Printf(" Kubectl Version: %s\n", version.KubectlVersion)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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/errors"
|
||||
|
||||
// load the gcp plugin (required to authenticate against GKE clusters).
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Default service addresses and URLS of Argo CD internal services
|
||||
const (
|
||||
// DefaultRepoServerAddr is the gRPC address of the Argo CD repo server
|
||||
@@ -19,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
|
||||
@@ -38,14 +45,20 @@ const (
|
||||
|
||||
// Default paths on the pod's file system
|
||||
const (
|
||||
// The default base path where application config is located
|
||||
DefaultPathAppConfig = "/app/config"
|
||||
// The default path where TLS certificates for repositories are located
|
||||
DefaultPathTLSConfig = "/app/config/tls"
|
||||
// The default path where SSH known hosts are stored
|
||||
DefaultPathSSHConfig = "/app/config/ssh"
|
||||
// Default name for the SSH known hosts file
|
||||
DefaultSSHKnownHostsName = "ssh_known_hosts"
|
||||
// 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
|
||||
@@ -62,10 +75,8 @@ const (
|
||||
AuthCookieName = "argocd.token"
|
||||
// RevisionHistoryLimit is the max number of successful sync to keep in history
|
||||
RevisionHistoryLimit = 10
|
||||
// K8sClientConfigQPS controls the QPS to be used in K8s REST client configs
|
||||
K8sClientConfigQPS = 25
|
||||
// K8sClientConfigBurst controls the burst to be used in K8s REST client configs
|
||||
K8sClientConfigBurst = 50
|
||||
// ChangePasswordSSOTokenMaxAge is the max token age for password change operation
|
||||
ChangePasswordSSOTokenMaxAge = time.Minute * 5
|
||||
)
|
||||
|
||||
// Dex related constants
|
||||
@@ -102,14 +113,7 @@ const (
|
||||
|
||||
// AnnotationCompareOptions is a comma-separated list of options for comparison
|
||||
AnnotationCompareOptions = "argocd.argoproj.io/compare-options"
|
||||
// AnnotationSyncOptions is a comma-separated list of options for syncing
|
||||
AnnotationSyncOptions = "argocd.argoproj.io/sync-options"
|
||||
// AnnotationSyncWave indicates which wave of the sync the resource or hook should be in
|
||||
AnnotationSyncWave = "argocd.argoproj.io/sync-wave"
|
||||
// AnnotationKeyHook contains the hook type of a resource
|
||||
AnnotationKeyHook = "argocd.argoproj.io/hook"
|
||||
// AnnotationKeyHookDeletePolicy is the policy of deleting a hook
|
||||
AnnotationKeyHookDeletePolicy = "argocd.argoproj.io/hook-delete-policy"
|
||||
|
||||
// AnnotationKeyRefresh is the annotation key which indicates that app needs to be refreshed. Removed by application controller after app is refreshed.
|
||||
// Might take values 'normal'/'hard'. Value 'hard' means manifest cache and target cluster state cache should be invalidated before refresh.
|
||||
AnnotationKeyRefresh = "argocd.argoproj.io/refresh"
|
||||
@@ -138,14 +142,70 @@ const (
|
||||
EnvGitAttemptsCount = "ARGOCD_GIT_ATTEMPTS_COUNT"
|
||||
// Overrides git submodule support, true by default
|
||||
EnvGitSubmoduleEnabled = "ARGOCD_GIT_MODULES_ENABLED"
|
||||
// EnvK8sClientQPS is the QPS value used for the kubernetes client (default: 50)
|
||||
EnvK8sClientQPS = "ARGOCD_K8S_CLIENT_QPS"
|
||||
// EnvK8sClientBurst is the burst value used for the kubernetes client (default: twice the client QPS)
|
||||
EnvK8sClientBurst = "ARGOCD_K8S_CLIENT_BURST"
|
||||
// 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"
|
||||
)
|
||||
|
||||
const (
|
||||
// MinClientVersion is the minimum client version that can interface with this API server.
|
||||
// When introducing breaking changes to the API or datastructures, this number should be bumped.
|
||||
// The value here may be lower than the current value in VERSION
|
||||
MinClientVersion = "1.3.0"
|
||||
MinClientVersion = "1.4.0"
|
||||
// CacheVersion is a objects version cached using util/cache/cache.go.
|
||||
// Number should be bumped in case of backward incompatible change to make sure cache is invalidated after upgrade.
|
||||
CacheVersion = "1.0.0"
|
||||
)
|
||||
|
||||
// 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
|
||||
// K8sClientConfigBurst controls the burst to be used in K8s REST client configs
|
||||
K8sClientConfigBurst int = 100
|
||||
// K8sMaxIdleConnections controls the number of max idle connections in K8s REST client HTTP transport
|
||||
K8sMaxIdleConnections = 500
|
||||
// K8sMaxIdleConnections controls the duration of cluster cache refresh
|
||||
K8SClusterResyncDuration = 12 * time.Hour
|
||||
)
|
||||
|
||||
func init() {
|
||||
if envQPS := os.Getenv(EnvK8sClientQPS); envQPS != "" {
|
||||
if qps, err := strconv.ParseFloat(envQPS, 32); err != nil {
|
||||
K8sClientConfigQPS = float32(qps)
|
||||
}
|
||||
}
|
||||
if envBurst := os.Getenv(EnvK8sClientBurst); envBurst != "" {
|
||||
if burst, err := strconv.Atoi(envBurst); err != nil {
|
||||
K8sClientConfigBurst = burst
|
||||
}
|
||||
} else {
|
||||
K8sClientConfigBurst = 2 * int(K8sClientConfigQPS)
|
||||
}
|
||||
|
||||
if envMaxConn := os.Getenv(EnvK8sClientMaxIdleConnections); envMaxConn != "" {
|
||||
if maxConn, err := strconv.Atoi(envMaxConn); err != nil {
|
||||
K8sMaxIdleConnections = maxConn
|
||||
}
|
||||
}
|
||||
if clusterResyncDurationStr := os.Getenv(EnvClusterCacheResyncDuration); clusterResyncDurationStr != "" {
|
||||
if duration, err := time.ParseDuration(clusterResyncDurationStr); err == nil {
|
||||
K8SClusterResyncDuration = duration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,17 +7,25 @@ import (
|
||||
"math"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
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"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
@@ -26,12 +34,11 @@ import (
|
||||
"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"
|
||||
"github.com/argoproj/argo-cd/controller/metrics"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application"
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
@@ -39,12 +46,10 @@ import (
|
||||
"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"
|
||||
"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/diff"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/glob"
|
||||
settings_util "github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
@@ -86,6 +91,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
|
||||
@@ -132,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,
|
||||
@@ -151,6 +158,23 @@ func NewApplicationController(
|
||||
}
|
||||
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)
|
||||
ctrl.metricsServer = metrics.NewMetricsServer(metricsAddr, appLister, func() error {
|
||||
_, err := kubeClientset.Discovery().ServerVersion()
|
||||
@@ -167,7 +191,11 @@ func NewApplicationController(
|
||||
return &ctrl, nil
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) onKubectlRun(command string) (util.Closer, error) {
|
||||
func (ctrl *ApplicationController) GetMetricsServer() *metrics.MetricsServer {
|
||||
return ctrl.metricsServer
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) onKubectlRun(command string) (io.Closer, error) {
|
||||
ctrl.metricsServer.IncKubectlExec(command)
|
||||
if ctrl.kubectlSemaphore != nil {
|
||||
if err := ctrl.kubectlSemaphore.Acquire(context.Background(), 1); err != nil {
|
||||
@@ -175,7 +203,7 @@ func (ctrl *ApplicationController) onKubectlRun(command string) (util.Closer, er
|
||||
}
|
||||
ctrl.metricsServer.IncKubectlExecPending(command)
|
||||
}
|
||||
return util.NewCloser(func() error {
|
||||
return io.NewCloser(func() error {
|
||||
if ctrl.kubectlSemaphore != nil {
|
||||
ctrl.kubectlSemaphore.Release(1)
|
||||
ctrl.metricsServer.DecKubectlExecPending(command)
|
||||
@@ -209,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
|
||||
}
|
||||
@@ -248,13 +276,23 @@ 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
|
||||
}
|
||||
|
||||
@@ -310,7 +348,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 != "" {
|
||||
@@ -335,6 +373,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
|
||||
}
|
||||
|
||||
@@ -359,7 +400,11 @@ func (ctrl *ApplicationController) managedResources(comparisonResult *comparison
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resDiffPtr, err := diff.Diff(target, live, comparisonResult.diffNormalizer)
|
||||
compareOptions, err := ctrl.settingsMgr.GetResourceCompareOptions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resDiffPtr, err := diff.Diff(target, live, comparisonResult.diffNormalizer, compareOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -385,11 +430,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)
|
||||
|
||||
@@ -404,11 +444,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
|
||||
@@ -435,6 +480,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()
|
||||
}
|
||||
|
||||
@@ -539,7 +589,76 @@ func (ctrl *ApplicationController) processAppComparisonTypeQueueItem() (processN
|
||||
return
|
||||
}
|
||||
|
||||
// shouldbeDeleted returns whether a given resource obj should be deleted on cascade delete of application app
|
||||
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))
|
||||
}
|
||||
@@ -562,7 +681,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)
|
||||
@@ -574,6 +693,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
|
||||
@@ -581,7 +705,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])
|
||||
}
|
||||
}
|
||||
@@ -592,9 +721,10 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic
|
||||
}
|
||||
config := metrics.AddMetricsTransportWrapper(ctrl.metricsServer, app, cluster.RESTConfig())
|
||||
|
||||
err = util.RunAllAsync(len(objs), func(i int) error {
|
||||
obj := objs[i]
|
||||
return ctrl.kubectl.DeleteResource(config, obj.GroupVersionKind(), obj.GetName(), obj.GetNamespace(), false)
|
||||
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
|
||||
@@ -629,12 +759,13 @@ 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
|
||||
}
|
||||
|
||||
@@ -648,7 +779,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)
|
||||
@@ -662,7 +793,7 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
logCtx.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
|
||||
state.Phase = appv1.OperationError
|
||||
state.Phase = synccommon.OperationError
|
||||
if rerr, ok := r.(error); ok {
|
||||
state.Message = rerr.Error()
|
||||
} else {
|
||||
@@ -671,12 +802,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
|
||||
@@ -687,32 +819,73 @@ 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 {
|
||||
state.SyncResult = nil
|
||||
}
|
||||
} else {
|
||||
logCtx.Infof("Resuming in-progress operation. phase: %s, message: %s", state.Phase, state.Message)
|
||||
}
|
||||
} else {
|
||||
state = &appv1.OperationState{Phase: appv1.OperationRunning, Operation: *app.Operation, StartedAt: metav1.Now()}
|
||||
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 == appv1.OperationRunning {
|
||||
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 == appv1.OperationTerminating {
|
||||
state.Phase = appv1.OperationTerminating
|
||||
if freshApp.Status.OperationState != nil && freshApp.Status.OperationState.Phase == synccommon.OperationTerminating {
|
||||
state.Phase = synccommon.OperationTerminating
|
||||
state.Message = "operation is terminating"
|
||||
// after this, we will get requeued to the workqueue, but next time the
|
||||
// SyncAppState will operate in a Terminating phase, allowing the worker to perform
|
||||
// cleanup (e.g. delete jobs, workflows, etc...)
|
||||
}
|
||||
}
|
||||
} 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 {
|
||||
@@ -725,7 +898,7 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) setOperationState(app *appv1.Application, state *appv1.OperationState) {
|
||||
util.RetryUntilSucceed(func() error {
|
||||
kube.RetryUntilSucceed(context.Background(), updateOperationStateTimeout, "Update application operation state", func() error {
|
||||
if state.Phase == "" {
|
||||
// expose any bugs where we neglect to set phase
|
||||
panic("no phase was set")
|
||||
@@ -753,7 +926,7 @@ func (ctrl *ApplicationController) setOperationState(app *appv1.Application, sta
|
||||
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) {
|
||||
@@ -784,7 +957,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) {
|
||||
@@ -815,51 +988,48 @@ 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 {
|
||||
return
|
||||
}
|
||||
|
||||
app := origApp.DeepCopy()
|
||||
logCtx := log.WithFields(log.Fields{"application": app.Name})
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
reconcileDuration := time.Since(startTime)
|
||||
ctrl.metricsServer.IncReconcile(origApp, reconcileDuration)
|
||||
logCtx := log.WithFields(log.Fields{
|
||||
"application": origApp.Name,
|
||||
"time_ms": reconcileDuration.Seconds() * 1e3,
|
||||
logCtx.WithFields(log.Fields{
|
||||
"time_ms": reconcileDuration.Milliseconds(),
|
||||
"level": comparisonLevel,
|
||||
"dest-server": origApp.Spec.Destination.Server,
|
||||
"dest-name": origApp.Spec.Destination.Name,
|
||||
"dest-namespace": origApp.Spec.Destination.Namespace,
|
||||
})
|
||||
logCtx.Info("Reconciliation completed")
|
||||
}).Info("Reconciliation completed")
|
||||
}()
|
||||
|
||||
app := origApp.DeepCopy()
|
||||
logCtx := log.WithFields(log.Fields{"application": app.Name})
|
||||
if comparisonLevel == ComparisonWithNothing {
|
||||
managedResources := make([]*appv1.ResourceDiff, 0)
|
||||
if err := ctrl.cache.GetAppManagedResources(app.Name, &managedResources); err != nil {
|
||||
logCtx.Warnf("Failed to get cached managed resources for tree reconciliation, fallback to full reconciliation")
|
||||
} else {
|
||||
if tree, err := ctrl.getResourceTree(app, managedResources); err != nil {
|
||||
app.Status.SetConditions(
|
||||
[]appv1.ApplicationCondition{
|
||||
{
|
||||
Type: appv1.ApplicationConditionComparisonError,
|
||||
Message: err.Error(),
|
||||
},
|
||||
},
|
||||
map[appv1.ApplicationConditionType]bool{
|
||||
appv1.ApplicationConditionComparisonError: true,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
app.Status.Summary = tree.GetSummary()
|
||||
if err = ctrl.cache.SetAppResourcesTree(app.Name, tree); err != nil {
|
||||
logCtx.Errorf("Failed to cache resources tree: %v", err)
|
||||
return
|
||||
var tree *appv1.ApplicationTree
|
||||
if err = argo.ValidateDestination(context.Background(), &app.Spec.Destination, ctrl.db); err == nil {
|
||||
if tree, err = ctrl.getResourceTree(app, managedResources); err == nil {
|
||||
app.Status.Summary = tree.GetSummary()
|
||||
if err := ctrl.cache.SetAppResourcesTree(app.Name, tree); err != nil {
|
||||
logCtx.Errorf("Failed to cache resources tree: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
app.Status.SetConditions([]appv1.ApplicationCondition{{
|
||||
Type: appv1.ApplicationConditionComparisonError, Message: err.Error(),
|
||||
}}, map[appv1.ApplicationConditionType]bool{
|
||||
appv1.ApplicationConditionComparisonError: true,
|
||||
})
|
||||
}
|
||||
now := metav1.Now()
|
||||
app.Status.ObservedAt = &now
|
||||
@@ -871,7 +1041,7 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
project, hasErrors := ctrl.refreshAppConditions(app)
|
||||
if hasErrors {
|
||||
app.Status.Sync.Status = appv1.SyncStatusCodeUnknown
|
||||
app.Status.Health.Status = appv1.HealthStatusUnknown
|
||||
app.Status.Health.Status = health.HealthStatusUnknown
|
||||
ctrl.persistAppStatus(origApp, &app.Status)
|
||||
return
|
||||
}
|
||||
@@ -888,6 +1058,9 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
|
||||
observedAt := 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())
|
||||
}
|
||||
|
||||
ctrl.normalizeApplication(origApp, app)
|
||||
|
||||
@@ -912,7 +1085,7 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
)
|
||||
}
|
||||
} else {
|
||||
logCtx.Infof("Sync prevented by sync window")
|
||||
logCtx.Info("Sync prevented by sync window")
|
||||
}
|
||||
|
||||
if app.Status.ReconciledAt == nil || comparisonLevel == CompareWithLatest {
|
||||
@@ -922,11 +1095,18 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
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.
|
||||
@@ -957,7 +1137,7 @@ func (ctrl *ApplicationController) needRefreshAppStatus(app *appv1.Application,
|
||||
reason = "spec.destination differs"
|
||||
} else if requested, level := ctrl.isRefreshRequested(app.Name); requested {
|
||||
compareWith = level
|
||||
reason = fmt.Sprintf("controller refresh requested")
|
||||
reason = "controller refresh requested"
|
||||
}
|
||||
|
||||
if reason != "" {
|
||||
@@ -983,6 +1163,13 @@ func (ctrl *ApplicationController) refreshAppConditions(app *appv1.Application)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if err := argo.ValidateDestination(context.Background(), &app.Spec.Destination, ctrl.db); err != nil {
|
||||
errorConditions = append(errorConditions, appv1.ApplicationCondition{
|
||||
Message: err.Error(),
|
||||
Type: appv1.ApplicationConditionInvalidSpecError,
|
||||
})
|
||||
}
|
||||
|
||||
specConditions, err := argo.ValidatePermissions(context.Background(), &app.Spec, proj, ctrl.db)
|
||||
if err != nil {
|
||||
errorConditions = append(errorConditions, appv1.ApplicationCondition{
|
||||
@@ -1009,7 +1196,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 {
|
||||
@@ -1050,7 +1237,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 {
|
||||
@@ -1080,14 +1267,34 @@ func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus *
|
||||
return nil
|
||||
}
|
||||
|
||||
if !app.Spec.SyncPolicy.Automated.Prune {
|
||||
requirePruneOnly := true
|
||||
for _, r := range resources {
|
||||
if r.Status != appv1.SyncStatusCodeSynced && !r.RequiresPruning {
|
||||
requirePruneOnly = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if requirePruneOnly {
|
||||
logCtx.Infof("Skipping auto-sync: need to prune extra resources only but automated prune is disabled")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
desiredCommitSHA := syncStatus.Revision
|
||||
alreadyAttempted, attemptPhase := alreadyAttemptedSync(app, desiredCommitSHA)
|
||||
selfHeal := app.Spec.SyncPolicy.Automated.SelfHeal
|
||||
op := appv1.Operation{
|
||||
Sync: &appv1.SyncOperation{
|
||||
Revision: desiredCommitSHA,
|
||||
Prune: app.Spec.SyncPolicy.Automated.Prune,
|
||||
Revision: desiredCommitSHA,
|
||||
Prune: app.Spec.SyncPolicy.Automated.Prune,
|
||||
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
|
||||
@@ -1120,6 +1327,20 @@ func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus *
|
||||
|
||||
}
|
||||
|
||||
if app.Spec.SyncPolicy.Automated.Prune {
|
||||
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 resourses", 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 {
|
||||
@@ -1134,7 +1355,7 @@ func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus *
|
||||
|
||||
// alreadyAttemptedSync returns whether or not the most recent sync was performed against the
|
||||
// commitSHA and with the same app source config which are currently set in the app
|
||||
func alreadyAttemptedSync(app *appv1.Application, commitSHA string) (bool, appv1.OperationPhase) {
|
||||
func alreadyAttemptedSync(app *appv1.Application, commitSHA string) (bool, synccommon.OperationPhase) {
|
||||
if app.Status.OperationState == nil || app.Status.OperationState.Operation.Sync == nil || app.Status.OperationState.SyncResult == nil {
|
||||
return false, ""
|
||||
}
|
||||
@@ -1227,6 +1448,11 @@ func (ctrl *ApplicationController) newApplicationInformerAndLister() (cache.Shar
|
||||
return informer, lister, err
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) RegisterClusterSecretUpdater(ctx context.Context) {
|
||||
updater := NewClusterInfoUpdater(ctrl.stateCache, ctrl.db, ctrl.appLister.Applications(ctrl.namespace), ctrl.cache)
|
||||
go updater.Run(ctx)
|
||||
}
|
||||
|
||||
func isOperationInProgress(app *appv1.Application) bool {
|
||||
return app.Status.OperationState != nil && !app.Status.OperationState.Phase.Completed()
|
||||
}
|
||||
|
||||
@@ -6,6 +6,10 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/cache/mocks"
|
||||
synccommon "github.com/argoproj/gitops-engine/pkg/sync/common"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube/kubetest"
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
@@ -25,12 +29,9 @@ import (
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/fake"
|
||||
"github.com/argoproj/argo-cd/reposerver/apiclient"
|
||||
mockrepoclient "github.com/argoproj/argo-cd/reposerver/apiclient/mocks"
|
||||
mockreposerver "github.com/argoproj/argo-cd/reposerver/mocks"
|
||||
"github.com/argoproj/argo-cd/test"
|
||||
cacheutil "github.com/argoproj/argo-cd/util/cache"
|
||||
appstatecache "github.com/argoproj/argo-cd/util/cache/appstate"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/kube/kubetest"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
@@ -57,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 := mockreposerver.Clientset{}
|
||||
mockRepoClientset.On("NewRepoServerClient").Return(&fakeCloser{}, &mockRepoClient, nil)
|
||||
mockRepoClientset := mockrepoclient.Clientset{RepoServerServiceClient: &mockRepoClient}
|
||||
|
||||
secret := corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -106,17 +106,21 @@ func newFakeController(data *fakeData) *ApplicationController {
|
||||
defer cancelProj()
|
||||
cancelApp := test.StartInformer(ctrl.appInformer)
|
||||
defer cancelApp()
|
||||
clusterCacheMock := mocks.ClusterCache{}
|
||||
clusterCacheMock.On("IsNamespaced", mock.Anything).Return(true, nil)
|
||||
|
||||
mockStateCache := mockstatecache.LiveStateCache{}
|
||||
ctrl.appStateManager.(*appStateManager).liveStateCache = &mockStateCache
|
||||
ctrl.stateCache = &mockStateCache
|
||||
mockStateCache.On("IsNamespaced", mock.Anything, mock.Anything).Return(true, nil)
|
||||
mockStateCache.On("GetManagedLiveObjs", mock.Anything, mock.Anything).Return(data.managedLiveObjs, nil)
|
||||
mockStateCache.On("GetServerVersion", mock.Anything).Return("v1.2.3", nil)
|
||||
mockStateCache.On("GetVersionsInfo", mock.Anything).Return("v1.2.3", nil, nil)
|
||||
response := make(map[kube.ResourceKey]argoappv1.ResourceNode)
|
||||
for k, v := range data.namespacedResources {
|
||||
response[k] = v.ResourceNode
|
||||
}
|
||||
mockStateCache.On("GetNamespaceTopLevelResources", mock.Anything, mock.Anything).Return(response, nil)
|
||||
mockStateCache.On("GetClusterCache", mock.Anything).Return(&clusterCacheMock, nil)
|
||||
mockStateCache.On("IterateHierarchy", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
|
||||
key := args[1].(kube.ResourceKey)
|
||||
action := args[2].(func(child argoappv1.ResourceNode, appName string))
|
||||
@@ -124,22 +128,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
|
||||
@@ -192,6 +192,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
|
||||
@@ -204,8 +243,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)
|
||||
}
|
||||
@@ -228,9 +279,9 @@ func TestAutoSync(t *testing.T) {
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{})
|
||||
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)
|
||||
@@ -240,7 +291,7 @@ func TestAutoSync(t *testing.T) {
|
||||
func TestSkipAutoSync(t *testing.T) {
|
||||
// Verify we skip when we previously synced to it in our most recent history
|
||||
// Set current to 'aaaaa', desired to 'aaaa' and mark system OutOfSync
|
||||
{
|
||||
t.Run("PreviouslySyncedToRevision", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus := argoappv1.SyncStatus{
|
||||
@@ -249,13 +300,13 @@ 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)
|
||||
}
|
||||
})
|
||||
|
||||
// Verify we skip when we are already Synced (even if revision is different)
|
||||
{
|
||||
t.Run("AlreadyInSyncedState", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus := argoappv1.SyncStatus{
|
||||
@@ -264,13 +315,13 @@ 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)
|
||||
}
|
||||
})
|
||||
|
||||
// Verify we skip when auto-sync is disabled
|
||||
{
|
||||
t.Run("AutoSyncIsDisabled", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Spec.SyncPolicy = nil
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
@@ -280,13 +331,13 @@ 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)
|
||||
}
|
||||
})
|
||||
|
||||
// Verify we skip when application is marked for deletion
|
||||
{
|
||||
t.Run("ApplicationIsMarkedForDeletion", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
now := metav1.Now()
|
||||
app.DeletionTimestamp = &now
|
||||
@@ -297,20 +348,20 @@ 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)
|
||||
}
|
||||
})
|
||||
|
||||
// Verify we skip when previous sync attempt failed and return error condition
|
||||
// Set current to 'aaaaa', desired to 'bbbbb' and add 'bbbbb' to failure history
|
||||
{
|
||||
t.Run("PreviousSyncAttemptFailed", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Status.OperationState = &argoappv1.OperationState{
|
||||
Operation: argoappv1.Operation{
|
||||
Sync: &argoappv1.SyncOperation{},
|
||||
},
|
||||
Phase: argoappv1.OperationFailed,
|
||||
Phase: synccommon.OperationFailed,
|
||||
SyncResult: &argoappv1.SyncOperationResult{
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
Source: *app.Spec.Source.DeepCopy(),
|
||||
@@ -321,12 +372,28 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{})
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NeedsToPruneResourcesOnlyButAutomatedPruneDisabled", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus := argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{
|
||||
{Name: "guestbook", Kind: kube.DeploymentKind, Status: argoappv1.SyncStatusCodeOutOfSync, RequiresPruning: true},
|
||||
})
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get(context.Background(), "my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAutoSyncIndicateError verifies we skip auto-sync and return error condition if previous sync failed
|
||||
@@ -351,15 +418,15 @@ func TestAutoSyncIndicateError(t *testing.T) {
|
||||
Source: app.Spec.Source.DeepCopy(),
|
||||
},
|
||||
},
|
||||
Phase: argoappv1.OperationFailed,
|
||||
Phase: synccommon.OperationFailed,
|
||||
SyncResult: &argoappv1.SyncOperationResult{
|
||||
Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
Source: *app.Spec.Source.DeepCopy(),
|
||||
},
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{})
|
||||
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)
|
||||
}
|
||||
@@ -395,37 +462,38 @@ func TestAutoSyncParameterOverrides(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
Phase: argoappv1.OperationFailed,
|
||||
Phase: synccommon.OperationFailed,
|
||||
SyncResult: &argoappv1.SyncOperationResult{
|
||||
Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
},
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{})
|
||||
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)
|
||||
@@ -447,26 +515,11 @@ func TestFinalizeAppDeletion(t *testing.T) {
|
||||
_, err := ctrl.finalizeApplicationDeletion(app)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, patched)
|
||||
}
|
||||
})
|
||||
|
||||
// Ensure any stray resources irregulary labeled with instance label of app are not deleted upon deleting,
|
||||
// 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",
|
||||
@@ -520,7 +573,33 @@ func TestFinalizeAppDeletion(t *testing.T) {
|
||||
for _, o := range objs {
|
||||
assert.NotEqual(t, "test-cm", o.GetName())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DeleteWithDestinationClusterName", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
|
||||
app.Spec.Destination.Name = "minikube"
|
||||
app.Spec.Destination.Server = ""
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
// TestNormalizeApplication verifies we normalize an application during reconciliation
|
||||
@@ -641,6 +720,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)
|
||||
@@ -650,7 +766,7 @@ func TestSetOperationStateOnDeletedApp(t *testing.T) {
|
||||
patched = true
|
||||
return true, nil, apierr.NewNotFound(schema.GroupResource{}, "my-app")
|
||||
})
|
||||
ctrl.setOperationState(newFakeApp(), &argoappv1.OperationState{Phase: argoappv1.OperationSucceeded})
|
||||
ctrl.setOperationState(newFakeApp(), &argoappv1.OperationState{Phase: synccommon.OperationSucceeded})
|
||||
assert.True(t, patched)
|
||||
}
|
||||
|
||||
@@ -785,6 +901,26 @@ func TestRefreshAppConditions(t *testing.T) {
|
||||
assert.Equal(t, argoappv1.ApplicationConditionInvalidSpecError, app.Status.Conditions[0].Type)
|
||||
assert.Equal(t, "Application referencing project wrong project which does not exist", app.Status.Conditions[0].Message)
|
||||
})
|
||||
|
||||
t.Run("NoErrorConditionsWithDestNameOnly", func(t *testing.T) {
|
||||
app := newFakeAppWithDestName()
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}})
|
||||
|
||||
_, hasErrors := ctrl.refreshAppConditions(app)
|
||||
assert.False(t, hasErrors)
|
||||
assert.Len(t, app.Status.Conditions, 0)
|
||||
})
|
||||
|
||||
t.Run("ErrorOnBothDestNameAndServer", func(t *testing.T) {
|
||||
app := newFakeAppWithDestMismatch()
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}})
|
||||
|
||||
_, hasErrors := ctrl.refreshAppConditions(app)
|
||||
assert.True(t, hasErrors)
|
||||
assert.Len(t, app.Status.Conditions, 1)
|
||||
assert.Equal(t, argoappv1.ApplicationConditionInvalidSpecError, app.Status.Conditions[0].Type)
|
||||
assert.Equal(t, "application destination can't have both name and server defined: another-cluster https://localhost:6443", app.Status.Conditions[0].Message)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateReconciledAt(t *testing.T) {
|
||||
@@ -846,3 +982,163 @@ func TestUpdateReconciledAt(t *testing.T) {
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
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_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)
|
||||
}
|
||||
|
||||
468
controller/cache/cache.go
vendored
@@ -5,32 +5,33 @@ import (
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
clustercache "github.com/argoproj/gitops-engine/pkg/cache"
|
||||
"github.com/argoproj/gitops-engine/pkg/health"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
log "github.com/sirupsen/logrus"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/controller/metrics"
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/argo"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/lua"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
type cacheSettings struct {
|
||||
ResourceOverrides map[string]appv1.ResourceOverride
|
||||
AppInstanceLabelKey string
|
||||
ResourcesFilter *settings.ResourcesFilter
|
||||
}
|
||||
|
||||
type LiveStateCache interface {
|
||||
// Returns k8s server version
|
||||
GetServerVersion(serverURL string) (string, error)
|
||||
GetVersionsInfo(serverURL string) (string, []metav1.APIGroup, error)
|
||||
// Returns true of given group kind is a namespaced resource
|
||||
IsNamespaced(server string, gk schema.GroupKind) (bool, error)
|
||||
// Returns synced cluster cache
|
||||
GetClusterCache(server string) (clustercache.ClusterCache, error)
|
||||
// Executes give callback against resource specified by the key and all its children
|
||||
IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string)) error
|
||||
// Returns state of live nodes which correspond for target nodes of specified application.
|
||||
@@ -39,23 +40,21 @@ type LiveStateCache interface {
|
||||
GetNamespaceTopLevelResources(server string, namespace string) (map[kube.ResourceKey]appv1.ResourceNode, error)
|
||||
// Starts watching resources of each controlled cluster.
|
||||
Run(ctx context.Context) error
|
||||
// Invalidate invalidates the entire cluster state cache
|
||||
Invalidate()
|
||||
// Returns information about monitored clusters
|
||||
GetClustersInfo() []metrics.ClusterInfo
|
||||
GetClustersInfo() []clustercache.ClusterInfo
|
||||
// Init must be executed before cache can be used
|
||||
Init() error
|
||||
}
|
||||
|
||||
type ObjectUpdatedHandler = func(managedByApp map[string]bool, ref v1.ObjectReference)
|
||||
|
||||
func GetTargetObjKey(a *appv1.Application, un *unstructured.Unstructured, isNamespaced bool) kube.ResourceKey {
|
||||
key := kube.GetResourceKey(un)
|
||||
if !isNamespaced {
|
||||
key.Namespace = ""
|
||||
} else if isNamespaced && key.Namespace == "" {
|
||||
key.Namespace = a.Spec.Destination.Namespace
|
||||
}
|
||||
|
||||
return key
|
||||
type ResourceInfo struct {
|
||||
Info []appv1.InfoItem
|
||||
AppName string
|
||||
// networkingInfo are available only for known types involved into networking: Ingress, Service, Pod
|
||||
NetworkingInfo *appv1.ResourceNetworkingInfo
|
||||
Images []string
|
||||
Health *health.HealthStatus
|
||||
}
|
||||
|
||||
func NewLiveStateCache(
|
||||
@@ -67,29 +66,32 @@ func NewLiveStateCache(
|
||||
onObjectUpdated ObjectUpdatedHandler) LiveStateCache {
|
||||
|
||||
return &liveStateCache{
|
||||
appInformer: appInformer,
|
||||
db: db,
|
||||
clusters: make(map[string]*clusterInfo),
|
||||
lock: &sync.Mutex{},
|
||||
onObjectUpdated: onObjectUpdated,
|
||||
kubectl: kubectl,
|
||||
settingsMgr: settingsMgr,
|
||||
metricsServer: metricsServer,
|
||||
cacheSettingsLock: &sync.Mutex{},
|
||||
appInformer: appInformer,
|
||||
db: db,
|
||||
clusters: make(map[string]clustercache.ClusterCache),
|
||||
onObjectUpdated: onObjectUpdated,
|
||||
kubectl: kubectl,
|
||||
settingsMgr: settingsMgr,
|
||||
metricsServer: metricsServer,
|
||||
}
|
||||
}
|
||||
|
||||
type cacheSettings struct {
|
||||
clusterSettings clustercache.Settings
|
||||
appInstanceLabelKey string
|
||||
}
|
||||
|
||||
type liveStateCache struct {
|
||||
db db.ArgoDB
|
||||
clusters map[string]*clusterInfo
|
||||
lock *sync.Mutex
|
||||
appInformer cache.SharedIndexInformer
|
||||
onObjectUpdated ObjectUpdatedHandler
|
||||
kubectl kube.Kubectl
|
||||
settingsMgr *settings.SettingsManager
|
||||
metricsServer *metrics.MetricsServer
|
||||
cacheSettingsLock *sync.Mutex
|
||||
cacheSettings *cacheSettings
|
||||
db db.ArgoDB
|
||||
appInformer cache.SharedIndexInformer
|
||||
onObjectUpdated ObjectUpdatedHandler
|
||||
kubectl kube.Kubectl
|
||||
settingsMgr *settings.SettingsManager
|
||||
metricsServer *metrics.MetricsServer
|
||||
|
||||
clusters map[string]clustercache.ClusterCache
|
||||
cacheSettings cacheSettings
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func (c *liveStateCache) loadCacheSettings() (*cacheSettings, error) {
|
||||
@@ -105,57 +107,200 @@ func (c *liveStateCache) loadCacheSettings() (*cacheSettings, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cacheSettings{AppInstanceLabelKey: appInstanceLabelKey, ResourceOverrides: resourceOverrides, ResourcesFilter: resourcesFilter}, nil
|
||||
clusterSettings := clustercache.Settings{
|
||||
ResourceHealthOverride: lua.ResourceHealthOverrides(resourceOverrides),
|
||||
ResourcesFilter: resourcesFilter,
|
||||
}
|
||||
return &cacheSettings{clusterSettings, appInstanceLabelKey}, nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) getCluster(server string) (*clusterInfo, error) {
|
||||
func asResourceNode(r *clustercache.Resource) appv1.ResourceNode {
|
||||
gv, err := schema.ParseGroupVersion(r.Ref.APIVersion)
|
||||
if err != nil {
|
||||
gv = schema.GroupVersion{}
|
||||
}
|
||||
parentRefs := make([]appv1.ResourceRef, len(r.OwnerRefs))
|
||||
for _, ownerRef := range r.OwnerRefs {
|
||||
ownerGvk := schema.FromAPIVersionAndKind(ownerRef.APIVersion, ownerRef.Kind)
|
||||
ownerKey := kube.NewResourceKey(ownerGvk.Group, ownerRef.Kind, r.Ref.Namespace, ownerRef.Name)
|
||||
parentRefs[0] = appv1.ResourceRef{Name: ownerRef.Name, Kind: ownerKey.Kind, Namespace: r.Ref.Namespace, Group: ownerKey.Group, UID: string(ownerRef.UID)}
|
||||
}
|
||||
var resHealth *appv1.HealthStatus
|
||||
resourceInfo := resInfo(r)
|
||||
if resourceInfo.Health != nil {
|
||||
resHealth = &appv1.HealthStatus{Status: resourceInfo.Health.Status, Message: resourceInfo.Health.Message}
|
||||
}
|
||||
return appv1.ResourceNode{
|
||||
ResourceRef: appv1.ResourceRef{
|
||||
UID: string(r.Ref.UID),
|
||||
Name: r.Ref.Name,
|
||||
Group: gv.Group,
|
||||
Version: gv.Version,
|
||||
Kind: r.Ref.Kind,
|
||||
Namespace: r.Ref.Namespace,
|
||||
},
|
||||
ParentRefs: parentRefs,
|
||||
Info: resourceInfo.Info,
|
||||
ResourceVersion: r.ResourceVersion,
|
||||
NetworkingInfo: resourceInfo.NetworkingInfo,
|
||||
Images: resourceInfo.Images,
|
||||
Health: resHealth,
|
||||
CreatedAt: r.CreationTimestamp,
|
||||
}
|
||||
}
|
||||
|
||||
func resInfo(r *clustercache.Resource) *ResourceInfo {
|
||||
info, ok := r.Info.(*ResourceInfo)
|
||||
if !ok || info == nil {
|
||||
info = &ResourceInfo{}
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func isRootAppNode(r *clustercache.Resource) bool {
|
||||
return resInfo(r).AppName != "" && len(r.OwnerRefs) == 0
|
||||
}
|
||||
|
||||
func getApp(r *clustercache.Resource, ns map[kube.ResourceKey]*clustercache.Resource) string {
|
||||
return getAppRecursive(r, ns, map[kube.ResourceKey]bool{})
|
||||
}
|
||||
|
||||
func ownerRefGV(ownerRef metav1.OwnerReference) schema.GroupVersion {
|
||||
gv, err := schema.ParseGroupVersion(ownerRef.APIVersion)
|
||||
if err != nil {
|
||||
gv = schema.GroupVersion{}
|
||||
}
|
||||
return gv
|
||||
}
|
||||
|
||||
func getAppRecursive(r *clustercache.Resource, ns map[kube.ResourceKey]*clustercache.Resource, visited map[kube.ResourceKey]bool) string {
|
||||
if !visited[r.ResourceKey()] {
|
||||
visited[r.ResourceKey()] = true
|
||||
} else {
|
||||
log.Warnf("Circular dependency detected: %v.", visited)
|
||||
return resInfo(r).AppName
|
||||
}
|
||||
|
||||
if resInfo(r).AppName != "" {
|
||||
return resInfo(r).AppName
|
||||
}
|
||||
for _, ownerRef := range r.OwnerRefs {
|
||||
gv := ownerRefGV(ownerRef)
|
||||
if parent, ok := ns[kube.NewResourceKey(gv.Group, ownerRef.Kind, r.Ref.Namespace, ownerRef.Name)]; ok {
|
||||
app := getAppRecursive(parent, ns, visited)
|
||||
if app != "" {
|
||||
return app
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var (
|
||||
ignoredRefreshResources = map[string]bool{
|
||||
"/" + kube.EndpointsKind: true,
|
||||
}
|
||||
)
|
||||
|
||||
// skipAppRequeuing checks if the object is an API type which we want to skip requeuing against.
|
||||
// We ignore API types which have a high churn rate, and/or whose updates are irrelevant to the app
|
||||
func skipAppRequeuing(key kube.ResourceKey) bool {
|
||||
return ignoredRefreshResources[key.Group+"/"+key.Kind]
|
||||
}
|
||||
|
||||
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 {
|
||||
return clusterCache, nil
|
||||
}
|
||||
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
info, ok := c.clusters[server]
|
||||
if !ok {
|
||||
cluster, err := c.db.GetCluster(context.Background(), server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info = &clusterInfo{
|
||||
apisMeta: make(map[schema.GroupKind]*apiMeta),
|
||||
lock: &sync.Mutex{},
|
||||
nodes: make(map[kube.ResourceKey]*node),
|
||||
nsIndex: make(map[string]map[kube.ResourceKey]*node),
|
||||
onObjectUpdated: c.onObjectUpdated,
|
||||
kubectl: c.kubectl,
|
||||
cluster: cluster,
|
||||
syncTime: nil,
|
||||
log: log.WithField("server", cluster.Server),
|
||||
cacheSettingsSrc: c.getCacheSettings,
|
||||
onEventReceived: func(event watch.EventType, un *unstructured.Unstructured) {
|
||||
c.metricsServer.IncClusterEventsCount(cluster.Server)
|
||||
},
|
||||
}
|
||||
|
||||
c.clusters[cluster.Server] = info
|
||||
clusterCache, ok = c.clusters[server]
|
||||
if ok {
|
||||
return clusterCache, nil
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) getSyncedCluster(server string) (*clusterInfo, error) {
|
||||
info, err := c.getCluster(server)
|
||||
cluster, err := c.db.GetCluster(context.Background(), server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = info.ensureSynced()
|
||||
|
||||
clusterCache = clustercache.NewClusterCache(cluster.RESTConfig(),
|
||||
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, cacheSettings.clusterSettings.ResourceHealthOverride)
|
||||
appName := kube.GetAppInstanceLabel(un, cacheSettings.appInstanceLabelKey)
|
||||
if isRoot && appName != "" {
|
||||
res.AppName = appName
|
||||
}
|
||||
|
||||
// edge case. we do not label CRDs, so they miss the tracking label we inject. But we still
|
||||
// want the full resource to be available in our cache (to diff), so we store all CRDs
|
||||
return res, res.AppName != "" || un.GroupVersionKind().Kind == kube.CustomResourceDefinitionKind
|
||||
}),
|
||||
)
|
||||
|
||||
_ = clusterCache.OnResourceUpdated(func(newRes *clustercache.Resource, oldRes *clustercache.Resource, namespaceResources map[kube.ResourceKey]*clustercache.Resource) {
|
||||
toNotify := make(map[string]bool)
|
||||
var ref v1.ObjectReference
|
||||
if newRes != nil {
|
||||
ref = newRes.Ref
|
||||
} else {
|
||||
ref = oldRes.Ref
|
||||
}
|
||||
for _, r := range []*clustercache.Resource{newRes, oldRes} {
|
||||
if r == nil {
|
||||
continue
|
||||
}
|
||||
app := getApp(r, namespaceResources)
|
||||
if app == "" || skipAppRequeuing(r.ResourceKey()) {
|
||||
continue
|
||||
}
|
||||
toNotify[app] = isRootAppNode(r) || toNotify[app]
|
||||
}
|
||||
c.onObjectUpdated(toNotify, ref)
|
||||
})
|
||||
|
||||
_ = clusterCache.OnEvent(func(event watch.EventType, un *unstructured.Unstructured) {
|
||||
gvk := un.GroupVersionKind()
|
||||
c.metricsServer.IncClusterEventsCount(cluster.Server, gvk.Group, gvk.Kind)
|
||||
})
|
||||
|
||||
c.clusters[cluster.Server] = clusterCache
|
||||
|
||||
return clusterCache, nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) getSyncedCluster(server string) (clustercache.ClusterCache, error) {
|
||||
clusterCache, err := c.getCluster(server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return info, nil
|
||||
err = clusterCache.EnsureSynced()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return clusterCache, nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) Invalidate() {
|
||||
func (c *liveStateCache) invalidate(cacheSettings cacheSettings) {
|
||||
log.Info("invalidating live state cache")
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
c.cacheSettings = cacheSettings
|
||||
for _, clust := range c.clusters {
|
||||
clust.invalidate()
|
||||
clust.Invalidate(clustercache.SetSettings(cacheSettings.clusterSettings))
|
||||
}
|
||||
log.Info("live state cache invalidated")
|
||||
}
|
||||
@@ -165,7 +310,7 @@ func (c *liveStateCache) IsNamespaced(server string, gk schema.GroupKind) (bool,
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return clusterInfo.isNamespaced(gk), nil
|
||||
return clusterInfo.IsNamespaced(gk)
|
||||
}
|
||||
|
||||
func (c *liveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string)) error {
|
||||
@@ -173,7 +318,9 @@ func (c *liveStateCache) IterateHierarchy(server string, key kube.ResourceKey, a
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clusterInfo.iterateHierarchy(key, action)
|
||||
clusterInfo.IterateHierarchy(key, func(resource *clustercache.Resource, namespaceResources map[kube.ResourceKey]*clustercache.Resource) {
|
||||
action(asResourceNode(resource), getApp(resource, namespaceResources))
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -182,7 +329,12 @@ func (c *liveStateCache) GetNamespaceTopLevelResources(server string, namespace
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return clusterInfo.getNamespaceTopLevelResources(namespace), nil
|
||||
resources := clusterInfo.GetNamespaceTopLevelResources(namespace)
|
||||
res := make(map[kube.ResourceKey]appv1.ResourceNode)
|
||||
for k, r := range resources {
|
||||
res[k] = asResourceNode(r)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) GetManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
|
||||
@@ -190,31 +342,36 @@ func (c *liveStateCache) GetManagedLiveObjs(a *appv1.Application, targetObjs []*
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return clusterInfo.getManagedLiveObjs(a, targetObjs, c.metricsServer)
|
||||
}
|
||||
func (c *liveStateCache) GetServerVersion(serverURL string) (string, error) {
|
||||
clusterInfo, err := c.getSyncedCluster(serverURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return clusterInfo.serverVersion, nil
|
||||
return clusterInfo.GetManagedLiveObjs(targetObjs, func(r *clustercache.Resource) bool {
|
||||
return resInfo(r).AppName == a.Name
|
||||
})
|
||||
}
|
||||
|
||||
func isClusterHasApps(apps []interface{}, cluster *appv1.Cluster) bool {
|
||||
func (c *liveStateCache) GetVersionsInfo(serverURL string) (string, []metav1.APIGroup, error) {
|
||||
clusterInfo, err := c.getSyncedCluster(serverURL)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return clusterInfo.GetServerVersion(), clusterInfo.GetAPIGroups(), nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *liveStateCache) getCacheSettings() *cacheSettings {
|
||||
c.cacheSettingsLock.Lock()
|
||||
defer c.cacheSettingsLock.Unlock()
|
||||
return c.cacheSettings
|
||||
}
|
||||
|
||||
func (c *liveStateCache) watchSettings(ctx context.Context) {
|
||||
updateCh := make(chan *settings.ArgoCDSettings, 1)
|
||||
c.settingsMgr.Subscribe(updateCh)
|
||||
@@ -229,15 +386,15 @@ func (c *liveStateCache) watchSettings(ctx context.Context) {
|
||||
continue
|
||||
}
|
||||
|
||||
c.cacheSettingsLock.Lock()
|
||||
c.lock.Lock()
|
||||
needInvalidate := false
|
||||
if !reflect.DeepEqual(c.cacheSettings, nextCacheSettings) {
|
||||
c.cacheSettings = nextCacheSettings
|
||||
if !reflect.DeepEqual(c.cacheSettings, *nextCacheSettings) {
|
||||
c.cacheSettings = *nextCacheSettings
|
||||
needInvalidate = true
|
||||
}
|
||||
c.cacheSettingsLock.Unlock()
|
||||
c.lock.Unlock()
|
||||
if needInvalidate {
|
||||
c.Invalidate()
|
||||
c.invalidate(*nextCacheSettings)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
done = true
|
||||
@@ -248,50 +405,99 @@ 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 {
|
||||
func (c *liveStateCache) Init() error {
|
||||
cacheSettings, err := c.loadCacheSettings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.cacheSettings = cacheSettings
|
||||
|
||||
go c.watchSettings(ctx)
|
||||
|
||||
util.RetryUntilSucceed(func() error {
|
||||
clusterEventCallback := func(event *db.ClusterEvent) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if cluster, ok := c.clusters[event.Cluster.Server]; ok {
|
||||
if event.Type == watch.Deleted {
|
||||
cluster.invalidate()
|
||||
delete(c.clusters, event.Cluster.Server)
|
||||
} else if event.Type == watch.Modified {
|
||||
cluster.cluster = event.Cluster
|
||||
cluster.invalidate()
|
||||
}
|
||||
} else if event.Type == watch.Added && isClusterHasApps(c.appInformer.GetStore().List(), event.Cluster) {
|
||||
go func() {
|
||||
// warm up cache for cluster with apps
|
||||
_, _ = c.getSyncedCluster(event.Cluster.Server)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
return c.db.WatchClusters(ctx, clusterEventCallback)
|
||||
|
||||
}, "watch clusters", ctx, clusterRetryTimeout)
|
||||
|
||||
<-ctx.Done()
|
||||
c.cacheSettings = *cacheSettings
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) GetClustersInfo() []metrics.ClusterInfo {
|
||||
// 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", func() error {
|
||||
return c.db.WatchClusters(ctx, c.handleAddEvent, c.handleModEvent, c.handleDeleteEvent)
|
||||
})
|
||||
|
||||
<-ctx.Done()
|
||||
c.invalidate(c.cacheSettings)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) handleAddEvent(cluster *appv1.Cluster) {
|
||||
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 {
|
||||
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()
|
||||
res := make([]metrics.ClusterInfo, 0)
|
||||
for _, info := range c.clusters {
|
||||
res = append(res, info.getClusterInfo())
|
||||
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()
|
||||
for k := range c.clusters {
|
||||
clusters[k] = c.clusters[k]
|
||||
}
|
||||
c.lock.RUnlock()
|
||||
|
||||
res := make([]clustercache.ClusterInfo, 0)
|
||||
for server, c := range clusters {
|
||||
info := c.GetClusterInfo()
|
||||
info.Server = server
|
||||
res = append(res, info)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (c *liveStateCache) GetClusterCache(server string) (clustercache.ClusterCache, error) {
|
||||
return c.getSyncedCluster(server)
|
||||
}
|
||||
|
||||
60
controller/cache/cache_test.go
vendored
@@ -1,26 +1,52 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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 TestGetServerVersion(t *testing.T) {
|
||||
now := time.Now()
|
||||
cache := &liveStateCache{
|
||||
lock: &sync.Mutex{},
|
||||
clusters: map[string]*clusterInfo{
|
||||
"http://localhost": {
|
||||
syncTime: &now,
|
||||
lock: &sync.Mutex{},
|
||||
serverVersion: "123",
|
||||
},
|
||||
}}
|
||||
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()
|
||||
|
||||
version, err := cache.GetServerVersion("http://localhost")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "123", version)
|
||||
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_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"},
|
||||
})
|
||||
}
|
||||
|
||||
602
controller/cache/cluster.go
vendored
@@ -1,602 +0,0 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/client-go/dynamic"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"github.com/argoproj/argo-cd/controller/metrics"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/health"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
)
|
||||
|
||||
const (
|
||||
clusterSyncTimeout = 24 * time.Hour
|
||||
clusterRetryTimeout = 10 * time.Second
|
||||
watchResourcesRetryTimeout = 1 * time.Second
|
||||
)
|
||||
|
||||
type apiMeta struct {
|
||||
namespaced bool
|
||||
resourceVersion string
|
||||
watchCancel context.CancelFunc
|
||||
}
|
||||
|
||||
type clusterInfo struct {
|
||||
syncTime *time.Time
|
||||
syncError error
|
||||
apisMeta map[schema.GroupKind]*apiMeta
|
||||
serverVersion string
|
||||
|
||||
lock *sync.Mutex
|
||||
nodes map[kube.ResourceKey]*node
|
||||
nsIndex map[string]map[kube.ResourceKey]*node
|
||||
|
||||
onObjectUpdated ObjectUpdatedHandler
|
||||
onEventReceived func(event watch.EventType, un *unstructured.Unstructured)
|
||||
kubectl kube.Kubectl
|
||||
cluster *appv1.Cluster
|
||||
log *log.Entry
|
||||
cacheSettingsSrc func() *cacheSettings
|
||||
}
|
||||
|
||||
func (c *clusterInfo) replaceResourceCache(gk schema.GroupKind, resourceVersion string, objs []unstructured.Unstructured) {
|
||||
info, ok := c.apisMeta[gk]
|
||||
if ok {
|
||||
objByKind := make(map[kube.ResourceKey]*unstructured.Unstructured)
|
||||
for i := range objs {
|
||||
objByKind[kube.GetResourceKey(&objs[i])] = &objs[i]
|
||||
}
|
||||
|
||||
for i := range objs {
|
||||
obj := &objs[i]
|
||||
key := kube.GetResourceKey(&objs[i])
|
||||
existingNode, exists := c.nodes[key]
|
||||
c.onNodeUpdated(exists, existingNode, obj, key)
|
||||
}
|
||||
|
||||
for key, existingNode := range c.nodes {
|
||||
if key.Kind != gk.Kind || key.Group != gk.Group {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := objByKind[key]; !ok {
|
||||
c.onNodeRemoved(key, existingNode)
|
||||
}
|
||||
}
|
||||
info.resourceVersion = resourceVersion
|
||||
}
|
||||
}
|
||||
|
||||
func isServiceAccountTokenSecret(un *unstructured.Unstructured) (bool, metav1.OwnerReference) {
|
||||
ref := metav1.OwnerReference{
|
||||
APIVersion: "v1",
|
||||
Kind: kube.ServiceAccountKind,
|
||||
}
|
||||
if un.GetKind() != kube.SecretKind || un.GroupVersionKind().Group != "" {
|
||||
return false, ref
|
||||
}
|
||||
|
||||
if typeVal, ok, err := unstructured.NestedString(un.Object, "type"); !ok || err != nil || typeVal != "kubernetes.io/service-account-token" {
|
||||
return false, ref
|
||||
}
|
||||
|
||||
annotations := un.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return false, ref
|
||||
}
|
||||
|
||||
id, okId := annotations["kubernetes.io/service-account.uid"]
|
||||
name, okName := annotations["kubernetes.io/service-account.name"]
|
||||
if okId && okName {
|
||||
ref.Name = name
|
||||
ref.UID = types.UID(id)
|
||||
}
|
||||
return ref.Name != "" && ref.UID != "", ref
|
||||
}
|
||||
|
||||
func (c *clusterInfo) createObjInfo(un *unstructured.Unstructured, appInstanceLabel string) *node {
|
||||
ownerRefs := un.GetOwnerReferences()
|
||||
// Special case for endpoint. Remove after https://github.com/kubernetes/kubernetes/issues/28483 is fixed
|
||||
if un.GroupVersionKind().Group == "" && un.GetKind() == kube.EndpointsKind && len(un.GetOwnerReferences()) == 0 {
|
||||
ownerRefs = append(ownerRefs, metav1.OwnerReference{
|
||||
Name: un.GetName(),
|
||||
Kind: kube.ServiceKind,
|
||||
APIVersion: "v1",
|
||||
})
|
||||
}
|
||||
|
||||
// edge case. Consider auto-created service account tokens as a child of service account objects
|
||||
if yes, ref := isServiceAccountTokenSecret(un); yes {
|
||||
ownerRefs = append(ownerRefs, ref)
|
||||
}
|
||||
|
||||
nodeInfo := &node{
|
||||
resourceVersion: un.GetResourceVersion(),
|
||||
ref: kube.GetObjectRef(un),
|
||||
ownerRefs: ownerRefs,
|
||||
}
|
||||
|
||||
populateNodeInfo(un, nodeInfo)
|
||||
appName := kube.GetAppInstanceLabel(un, appInstanceLabel)
|
||||
if len(ownerRefs) == 0 && appName != "" {
|
||||
nodeInfo.appName = appName
|
||||
nodeInfo.resource = un
|
||||
}
|
||||
nodeInfo.health, _ = health.GetResourceHealth(un, c.cacheSettingsSrc().ResourceOverrides)
|
||||
return nodeInfo
|
||||
}
|
||||
|
||||
func (c *clusterInfo) setNode(n *node) {
|
||||
key := n.resourceKey()
|
||||
c.nodes[key] = n
|
||||
ns, ok := c.nsIndex[key.Namespace]
|
||||
if !ok {
|
||||
ns = make(map[kube.ResourceKey]*node)
|
||||
c.nsIndex[key.Namespace] = ns
|
||||
}
|
||||
ns[key] = n
|
||||
}
|
||||
|
||||
func (c *clusterInfo) removeNode(key kube.ResourceKey) {
|
||||
delete(c.nodes, key)
|
||||
if ns, ok := c.nsIndex[key.Namespace]; ok {
|
||||
delete(ns, key)
|
||||
if len(ns) == 0 {
|
||||
delete(c.nsIndex, key.Namespace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *clusterInfo) invalidate() {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.syncTime = nil
|
||||
for i := range c.apisMeta {
|
||||
c.apisMeta[i].watchCancel()
|
||||
}
|
||||
c.apisMeta = nil
|
||||
}
|
||||
|
||||
func (c *clusterInfo) synced() bool {
|
||||
if c.syncTime == nil {
|
||||
return false
|
||||
}
|
||||
if c.syncError != nil {
|
||||
return time.Now().Before(c.syncTime.Add(clusterRetryTimeout))
|
||||
}
|
||||
return time.Now().Before(c.syncTime.Add(clusterSyncTimeout))
|
||||
}
|
||||
|
||||
func (c *clusterInfo) stopWatching(gk schema.GroupKind) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if info, ok := c.apisMeta[gk]; ok {
|
||||
info.watchCancel()
|
||||
delete(c.apisMeta, gk)
|
||||
c.replaceResourceCache(gk, "", []unstructured.Unstructured{})
|
||||
log.Warnf("Stop watching %s not found on %s.", gk, c.cluster.Server)
|
||||
}
|
||||
}
|
||||
|
||||
// startMissingWatches lists supported cluster resources and start watching for changes unless watch is already running
|
||||
func (c *clusterInfo) startMissingWatches() error {
|
||||
config := c.cluster.RESTConfig()
|
||||
|
||||
apis, err := c.kubectl.GetAPIResources(config, c.cacheSettingsSrc().ResourcesFilter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := c.kubectl.NewDynamicClient(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range apis {
|
||||
api := apis[i]
|
||||
if _, ok := c.apisMeta[api.GroupKind]; !ok {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
info := &apiMeta{namespaced: api.Meta.Namespaced, watchCancel: cancel}
|
||||
c.apisMeta[api.GroupKind] = info
|
||||
|
||||
err = c.processApi(client, api, func(resClient dynamic.ResourceInterface) error {
|
||||
go c.watchEvents(ctx, api, info, resClient)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runSynced(lock *sync.Mutex, action func() error) error {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
return action()
|
||||
}
|
||||
|
||||
func (c *clusterInfo) watchEvents(ctx context.Context, api kube.APIResourceInfo, info *apiMeta, resClient dynamic.ResourceInterface) {
|
||||
util.RetryUntilSucceed(func() (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
|
||||
}
|
||||
}()
|
||||
|
||||
err = runSynced(c.lock, func() error {
|
||||
if info.resourceVersion == "" {
|
||||
list, err := resClient.List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.replaceResourceCache(api.GroupKind, list.GetResourceVersion(), list.Items)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w, err := resClient.Watch(metav1.ListOptions{ResourceVersion: info.resourceVersion})
|
||||
if errors.IsNotFound(err) {
|
||||
c.stopWatching(api.GroupKind)
|
||||
return nil
|
||||
}
|
||||
|
||||
err = runSynced(c.lock, func() error {
|
||||
if errors.IsGone(err) {
|
||||
info.resourceVersion = ""
|
||||
log.Warnf("Resource version of %s on %s is too old.", api.GroupKind, c.cluster.Server)
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer w.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case event, ok := <-w.ResultChan():
|
||||
if ok {
|
||||
obj := event.Object.(*unstructured.Unstructured)
|
||||
info.resourceVersion = obj.GetResourceVersion()
|
||||
c.processEvent(event.Type, obj)
|
||||
if kube.IsCRD(obj) {
|
||||
if event.Type == watch.Deleted {
|
||||
group, groupOk, groupErr := unstructured.NestedString(obj.Object, "spec", "group")
|
||||
kind, kindOk, kindErr := unstructured.NestedString(obj.Object, "spec", "names", "kind")
|
||||
|
||||
if groupOk && groupErr == nil && kindOk && kindErr == nil {
|
||||
gk := schema.GroupKind{Group: group, Kind: kind}
|
||||
c.stopWatching(gk)
|
||||
}
|
||||
} else {
|
||||
err = runSynced(c.lock, func() error {
|
||||
return c.startMissingWatches()
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Warnf("Failed to start missing watch: %v", err)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("Watch %s on %s has closed", api.GroupKind, c.cluster.Server)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}, fmt.Sprintf("watch %s on %s", api.GroupKind, c.cluster.Server), ctx, watchResourcesRetryTimeout)
|
||||
}
|
||||
|
||||
func (c *clusterInfo) processApi(client dynamic.Interface, api kube.APIResourceInfo, callback func(resClient dynamic.ResourceInterface) error) error {
|
||||
resClient := client.Resource(api.GroupVersionResource)
|
||||
if len(c.cluster.Namespaces) == 0 {
|
||||
return callback(resClient)
|
||||
}
|
||||
|
||||
if !api.Meta.Namespaced {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ns := range c.cluster.Namespaces {
|
||||
err := callback(resClient.Namespace(ns))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *clusterInfo) sync() (err error) {
|
||||
|
||||
c.log.Info("Start syncing cluster")
|
||||
|
||||
for i := range c.apisMeta {
|
||||
c.apisMeta[i].watchCancel()
|
||||
}
|
||||
c.apisMeta = make(map[schema.GroupKind]*apiMeta)
|
||||
c.nodes = make(map[kube.ResourceKey]*node)
|
||||
config := c.cluster.RESTConfig()
|
||||
version, err := c.kubectl.GetServerVersion(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.serverVersion = version
|
||||
apis, err := c.kubectl.GetAPIResources(config, c.cacheSettingsSrc().ResourcesFilter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := c.kubectl.NewDynamicClient(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lock := sync.Mutex{}
|
||||
err = util.RunAllAsync(len(apis), func(i int) error {
|
||||
return c.processApi(client, apis[i], func(resClient dynamic.ResourceInterface) error {
|
||||
list, err := resClient.List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lock.Lock()
|
||||
for i := range list.Items {
|
||||
c.setNode(c.createObjInfo(&list.Items[i], c.cacheSettingsSrc().AppInstanceLabelKey))
|
||||
}
|
||||
lock.Unlock()
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
err = c.startMissingWatches()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Failed to sync cluster %s: %v", c.cluster.Server, err)
|
||||
return err
|
||||
}
|
||||
|
||||
c.log.Info("Cluster successfully synced")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *clusterInfo) ensureSynced() error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if c.synced() {
|
||||
return c.syncError
|
||||
}
|
||||
|
||||
err := c.sync()
|
||||
syncTime := time.Now()
|
||||
c.syncTime = &syncTime
|
||||
c.syncError = err
|
||||
return c.syncError
|
||||
}
|
||||
|
||||
func (c *clusterInfo) getNamespaceTopLevelResources(namespace string) map[kube.ResourceKey]appv1.ResourceNode {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
nodes := make(map[kube.ResourceKey]appv1.ResourceNode)
|
||||
for _, node := range c.nsIndex[namespace] {
|
||||
if len(node.ownerRefs) == 0 {
|
||||
nodes[node.resourceKey()] = node.asResourceNode()
|
||||
}
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
func (c *clusterInfo) iterateHierarchy(key kube.ResourceKey, action func(child appv1.ResourceNode, appName string)) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if objInfo, ok := c.nodes[key]; ok {
|
||||
nsNodes := c.nsIndex[key.Namespace]
|
||||
action(objInfo.asResourceNode(), objInfo.getApp(nsNodes))
|
||||
childrenByUID := make(map[types.UID][]*node)
|
||||
for _, child := range nsNodes {
|
||||
if objInfo.isParentOf(child) {
|
||||
childrenByUID[child.ref.UID] = append(childrenByUID[child.ref.UID], child)
|
||||
}
|
||||
}
|
||||
// make sure children has no duplicates
|
||||
for _, children := range childrenByUID {
|
||||
if len(children) > 0 {
|
||||
// The object might have multiple children with the same UID (e.g. replicaset from apps and extensions group). It is ok to pick any object but we need to make sure
|
||||
// we pick the same child after every refresh.
|
||||
sort.Slice(children, func(i, j int) bool {
|
||||
key1 := children[i].resourceKey()
|
||||
key2 := children[j].resourceKey()
|
||||
return strings.Compare(key1.String(), key2.String()) < 0
|
||||
})
|
||||
child := children[0]
|
||||
action(child.asResourceNode(), child.getApp(nsNodes))
|
||||
child.iterateChildren(nsNodes, map[kube.ResourceKey]bool{objInfo.resourceKey(): true}, action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *clusterInfo) isNamespaced(gk schema.GroupKind) bool {
|
||||
if api, ok := c.apisMeta[gk]; ok && !api.namespaced {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured, metricsServer *metrics.MetricsServer) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
managedObjs := make(map[kube.ResourceKey]*unstructured.Unstructured)
|
||||
// iterate all objects in live state cache to find ones associated with app
|
||||
for key, o := range c.nodes {
|
||||
if o.appName == a.Name && o.resource != nil && len(o.ownerRefs) == 0 {
|
||||
managedObjs[key] = o.resource
|
||||
}
|
||||
}
|
||||
config := metrics.AddMetricsTransportWrapper(metricsServer, a, c.cluster.RESTConfig())
|
||||
// iterate target objects and identify ones that already exist in the cluster,\
|
||||
// but are simply missing our label
|
||||
lock := &sync.Mutex{}
|
||||
err := util.RunAllAsync(len(targetObjs), func(i int) error {
|
||||
targetObj := targetObjs[i]
|
||||
key := GetTargetObjKey(a, targetObj, c.isNamespaced(targetObj.GroupVersionKind().GroupKind()))
|
||||
lock.Lock()
|
||||
managedObj := managedObjs[key]
|
||||
lock.Unlock()
|
||||
|
||||
if managedObj == nil {
|
||||
if existingObj, exists := c.nodes[key]; exists {
|
||||
if existingObj.resource != nil {
|
||||
managedObj = existingObj.resource
|
||||
} else {
|
||||
var err error
|
||||
managedObj, err = c.kubectl.GetResource(config, targetObj.GroupVersionKind(), existingObj.ref.Name, existingObj.ref.Namespace)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else if _, watched := c.apisMeta[key.GroupKind()]; !watched {
|
||||
var err error
|
||||
managedObj, err = c.kubectl.GetResource(config, targetObj.GroupVersionKind(), targetObj.GetName(), targetObj.GetNamespace())
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if managedObj != nil {
|
||||
converted, err := c.kubectl.ConvertToVersion(managedObj, targetObj.GroupVersionKind().Group, targetObj.GroupVersionKind().Version)
|
||||
if err != nil {
|
||||
// fallback to loading resource from kubernetes if conversion fails
|
||||
log.Warnf("Failed to convert resource: %v", err)
|
||||
managedObj, err = c.kubectl.GetResource(config, targetObj.GroupVersionKind(), managedObj.GetName(), managedObj.GetNamespace())
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
managedObj = converted
|
||||
}
|
||||
lock.Lock()
|
||||
managedObjs[key] = managedObj
|
||||
lock.Unlock()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return managedObjs, nil
|
||||
}
|
||||
|
||||
func (c *clusterInfo) processEvent(event watch.EventType, un *unstructured.Unstructured) {
|
||||
if c.onEventReceived != nil {
|
||||
c.onEventReceived(event, un)
|
||||
}
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
key := kube.GetResourceKey(un)
|
||||
existingNode, exists := c.nodes[key]
|
||||
if event == watch.Deleted {
|
||||
if exists {
|
||||
c.onNodeRemoved(key, existingNode)
|
||||
}
|
||||
} else if event != watch.Deleted {
|
||||
c.onNodeUpdated(exists, existingNode, un, key)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *clusterInfo) onNodeUpdated(exists bool, existingNode *node, un *unstructured.Unstructured, key kube.ResourceKey) {
|
||||
nodes := make([]*node, 0)
|
||||
if exists {
|
||||
nodes = append(nodes, existingNode)
|
||||
}
|
||||
newObj := c.createObjInfo(un, c.cacheSettingsSrc().AppInstanceLabelKey)
|
||||
c.setNode(newObj)
|
||||
nodes = append(nodes, newObj)
|
||||
toNotify := make(map[string]bool)
|
||||
for i := range nodes {
|
||||
n := nodes[i]
|
||||
if ns, ok := c.nsIndex[n.ref.Namespace]; ok {
|
||||
app := n.getApp(ns)
|
||||
if app == "" || skipAppRequeing(key) {
|
||||
continue
|
||||
}
|
||||
toNotify[app] = n.isRootAppNode() || toNotify[app]
|
||||
}
|
||||
}
|
||||
c.onObjectUpdated(toNotify, newObj.ref)
|
||||
}
|
||||
|
||||
func (c *clusterInfo) onNodeRemoved(key kube.ResourceKey, n *node) {
|
||||
appName := n.appName
|
||||
if ns, ok := c.nsIndex[key.Namespace]; ok {
|
||||
appName = n.getApp(ns)
|
||||
}
|
||||
|
||||
c.removeNode(key)
|
||||
managedByApp := make(map[string]bool)
|
||||
if appName != "" {
|
||||
managedByApp[appName] = n.isRootAppNode()
|
||||
}
|
||||
c.onObjectUpdated(managedByApp, n.ref)
|
||||
}
|
||||
|
||||
var (
|
||||
ignoredRefreshResources = map[string]bool{
|
||||
"/" + kube.EndpointsKind: true,
|
||||
}
|
||||
)
|
||||
|
||||
func (c *clusterInfo) getClusterInfo() metrics.ClusterInfo {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
return metrics.ClusterInfo{
|
||||
APIsCount: len(c.apisMeta),
|
||||
K8SVersion: c.serverVersion,
|
||||
ResourcesCount: len(c.nodes),
|
||||
Server: c.cluster.Server,
|
||||
LastCacheSyncTime: c.syncTime,
|
||||
}
|
||||
}
|
||||
|
||||
// skipAppRequeing checks if the object is an API type which we want to skip requeuing against.
|
||||
// We ignore API types which have a high churn rate, and/or whose updates are irrelevant to the app
|
||||
func skipAppRequeing(key kube.ResourceKey) bool {
|
||||
return ignoredRefreshResources[key.Group+"/"+key.Kind]
|
||||
}
|
||||
536
controller/cache/cluster_test.go
vendored
@@ -1,536 +0,0 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/dynamic/fake"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/kube/kubetest"
|
||||
)
|
||||
|
||||
func strToUnstructured(jsonStr string) *unstructured.Unstructured {
|
||||
obj := make(map[string]interface{})
|
||||
err := yaml.Unmarshal([]byte(jsonStr), &obj)
|
||||
errors.CheckError(err)
|
||||
return &unstructured.Unstructured{Object: obj}
|
||||
}
|
||||
|
||||
func mustToUnstructured(obj interface{}) *unstructured.Unstructured {
|
||||
un, err := kube.ToUnstructured(obj)
|
||||
errors.CheckError(err)
|
||||
return un
|
||||
}
|
||||
|
||||
var (
|
||||
testPod = strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
uid: "1"
|
||||
name: helm-guestbook-pod
|
||||
namespace: default
|
||||
ownerReferences:
|
||||
- apiVersion: apps/v1
|
||||
kind: ReplicaSet
|
||||
name: helm-guestbook-rs
|
||||
uid: "2"
|
||||
resourceVersion: "123"`)
|
||||
|
||||
testRS = strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: ReplicaSet
|
||||
metadata:
|
||||
uid: "2"
|
||||
name: helm-guestbook-rs
|
||||
namespace: default
|
||||
annotations:
|
||||
deployment.kubernetes.io/revision: "2"
|
||||
ownerReferences:
|
||||
- apiVersion: apps/v1beta1
|
||||
kind: Deployment
|
||||
name: helm-guestbook
|
||||
uid: "3"
|
||||
resourceVersion: "123"`)
|
||||
|
||||
testDeploy = strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: helm-guestbook
|
||||
uid: "3"
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
resourceVersion: "123"`)
|
||||
|
||||
testService = strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
resourceVersion: "123"
|
||||
uid: "4"
|
||||
spec:
|
||||
selector:
|
||||
app: guestbook
|
||||
type: LoadBalancer
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
- hostname: localhost`)
|
||||
|
||||
testIngress = strToUnstructured(`
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
uid: "4"
|
||||
spec:
|
||||
backend:
|
||||
serviceName: not-found-service
|
||||
servicePort: 443
|
||||
rules:
|
||||
- host: helm-guestbook.com
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: helm-guestbook
|
||||
servicePort: 443
|
||||
path: /
|
||||
- backend:
|
||||
serviceName: helm-guestbook
|
||||
servicePort: https
|
||||
path: /
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
- ip: 107.178.210.11`)
|
||||
)
|
||||
|
||||
func newCluster(objs ...*unstructured.Unstructured) *clusterInfo {
|
||||
runtimeObjs := make([]runtime.Object, len(objs))
|
||||
for i := range objs {
|
||||
runtimeObjs[i] = objs[i]
|
||||
}
|
||||
scheme := runtime.NewScheme()
|
||||
client := fake.NewSimpleDynamicClient(scheme, runtimeObjs...)
|
||||
|
||||
apiResources := []kube.APIResourceInfo{{
|
||||
GroupKind: schema.GroupKind{Group: "", Kind: "Pod"},
|
||||
GroupVersionResource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
|
||||
Meta: metav1.APIResource{Namespaced: true},
|
||||
}, {
|
||||
GroupKind: schema.GroupKind{Group: "apps", Kind: "ReplicaSet"},
|
||||
GroupVersionResource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "replicasets"},
|
||||
Meta: metav1.APIResource{Namespaced: true},
|
||||
}, {
|
||||
GroupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"},
|
||||
GroupVersionResource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"},
|
||||
Meta: metav1.APIResource{Namespaced: true},
|
||||
}}
|
||||
|
||||
return newClusterExt(&kubetest.MockKubectlCmd{APIResources: apiResources, DynamicClient: client})
|
||||
}
|
||||
|
||||
func newClusterExt(kubectl kube.Kubectl) *clusterInfo {
|
||||
return &clusterInfo{
|
||||
lock: &sync.Mutex{},
|
||||
nodes: make(map[kube.ResourceKey]*node),
|
||||
onObjectUpdated: func(managedByApp map[string]bool, reference corev1.ObjectReference) {},
|
||||
kubectl: kubectl,
|
||||
nsIndex: make(map[string]map[kube.ResourceKey]*node),
|
||||
cluster: &appv1.Cluster{},
|
||||
syncTime: nil,
|
||||
apisMeta: make(map[schema.GroupKind]*apiMeta),
|
||||
log: log.WithField("cluster", "test"),
|
||||
cacheSettingsSrc: func() *cacheSettings {
|
||||
return &cacheSettings{AppInstanceLabelKey: common.LabelKeyAppInstance}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getChildren(cluster *clusterInfo, un *unstructured.Unstructured) []appv1.ResourceNode {
|
||||
hierarchy := make([]appv1.ResourceNode, 0)
|
||||
cluster.iterateHierarchy(kube.GetResourceKey(un), func(child appv1.ResourceNode, app string) {
|
||||
hierarchy = append(hierarchy, child)
|
||||
})
|
||||
return hierarchy[1:]
|
||||
}
|
||||
|
||||
func TestEnsureSynced(t *testing.T) {
|
||||
obj1 := strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: {"name": "helm-guestbook1", "namespace": "default1"}
|
||||
`)
|
||||
obj2 := strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: {"name": "helm-guestbook2", "namespace": "default2"}
|
||||
`)
|
||||
|
||||
cluster := newCluster(obj1, obj2)
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Len(t, cluster.nodes, 2)
|
||||
var names []string
|
||||
for k := range cluster.nodes {
|
||||
names = append(names, k.Name)
|
||||
}
|
||||
assert.ElementsMatch(t, []string{"helm-guestbook1", "helm-guestbook2"}, names)
|
||||
}
|
||||
|
||||
func TestEnsureSyncedSingleNamespace(t *testing.T) {
|
||||
obj1 := strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: {"name": "helm-guestbook1", "namespace": "default1"}
|
||||
`)
|
||||
obj2 := strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: {"name": "helm-guestbook2", "namespace": "default2"}
|
||||
`)
|
||||
|
||||
cluster := newCluster(obj1, obj2)
|
||||
cluster.cluster.Namespaces = []string{"default1"}
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Len(t, cluster.nodes, 1)
|
||||
var names []string
|
||||
for k := range cluster.nodes {
|
||||
names = append(names, k.Name)
|
||||
}
|
||||
assert.ElementsMatch(t, []string{"helm-guestbook1"}, names)
|
||||
}
|
||||
|
||||
func TestGetNamespaceResources(t *testing.T) {
|
||||
defaultNamespaceTopLevel1 := strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: {"name": "helm-guestbook1", "namespace": "default"}
|
||||
`)
|
||||
defaultNamespaceTopLevel2 := strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: {"name": "helm-guestbook2", "namespace": "default"}
|
||||
`)
|
||||
kubesystemNamespaceTopLevel2 := strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: {"name": "helm-guestbook3", "namespace": "kube-system"}
|
||||
`)
|
||||
|
||||
cluster := newCluster(defaultNamespaceTopLevel1, defaultNamespaceTopLevel2, kubesystemNamespaceTopLevel2)
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
resources := cluster.getNamespaceTopLevelResources("default")
|
||||
assert.Len(t, resources, 2)
|
||||
assert.Equal(t, resources[kube.GetResourceKey(defaultNamespaceTopLevel1)].Name, "helm-guestbook1")
|
||||
assert.Equal(t, resources[kube.GetResourceKey(defaultNamespaceTopLevel2)].Name, "helm-guestbook2")
|
||||
|
||||
resources = cluster.getNamespaceTopLevelResources("kube-system")
|
||||
assert.Len(t, resources, 1)
|
||||
assert.Equal(t, resources[kube.GetResourceKey(kubesystemNamespaceTopLevel2)].Name, "helm-guestbook3")
|
||||
}
|
||||
|
||||
func TestGetChildren(t *testing.T) {
|
||||
cluster := newCluster(testPod, testRS, testDeploy)
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
rsChildren := getChildren(cluster, testRS)
|
||||
assert.Equal(t, []appv1.ResourceNode{{
|
||||
ResourceRef: appv1.ResourceRef{
|
||||
Kind: "Pod",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-pod",
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
UID: "1",
|
||||
},
|
||||
ParentRefs: []appv1.ResourceRef{{
|
||||
Group: "apps",
|
||||
Version: "",
|
||||
Kind: "ReplicaSet",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-rs",
|
||||
UID: "2",
|
||||
}},
|
||||
Health: &appv1.HealthStatus{Status: appv1.HealthStatusUnknown},
|
||||
NetworkingInfo: &appv1.ResourceNetworkingInfo{Labels: testPod.GetLabels()},
|
||||
ResourceVersion: "123",
|
||||
Info: []appv1.InfoItem{{Name: "Containers", Value: "0/0"}},
|
||||
}}, rsChildren)
|
||||
deployChildren := getChildren(cluster, testDeploy)
|
||||
|
||||
assert.Equal(t, append([]appv1.ResourceNode{{
|
||||
ResourceRef: appv1.ResourceRef{
|
||||
Kind: "ReplicaSet",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-rs",
|
||||
Group: "apps",
|
||||
Version: "v1",
|
||||
UID: "2",
|
||||
},
|
||||
ResourceVersion: "123",
|
||||
Health: &appv1.HealthStatus{Status: appv1.HealthStatusHealthy},
|
||||
Info: []appv1.InfoItem{{Name: "Revision", Value: "Rev:2"}},
|
||||
ParentRefs: []appv1.ResourceRef{{Group: "apps", Version: "", Kind: "Deployment", Namespace: "default", Name: "helm-guestbook", UID: "3"}},
|
||||
}}, rsChildren...), deployChildren)
|
||||
}
|
||||
|
||||
func TestGetManagedLiveObjs(t *testing.T) {
|
||||
cluster := newCluster(testPod, testRS, testDeploy)
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
targetDeploy := strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
labels:
|
||||
app: helm-guestbook`)
|
||||
|
||||
managedObjs, err := cluster.getManagedLiveObjs(&appv1.Application{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "helm-guestbook"},
|
||||
Spec: appv1.ApplicationSpec{
|
||||
Destination: appv1.ApplicationDestination{
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
}, []*unstructured.Unstructured{targetDeploy}, nil)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, managedObjs, map[kube.ResourceKey]*unstructured.Unstructured{
|
||||
kube.NewResourceKey("apps", "Deployment", "default", "helm-guestbook"): testDeploy,
|
||||
})
|
||||
}
|
||||
|
||||
func TestChildDeletedEvent(t *testing.T) {
|
||||
cluster := newCluster(testPod, testRS, testDeploy)
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
cluster.processEvent(watch.Deleted, testPod)
|
||||
|
||||
rsChildren := getChildren(cluster, testRS)
|
||||
assert.Equal(t, []appv1.ResourceNode{}, rsChildren)
|
||||
}
|
||||
|
||||
func TestProcessNewChildEvent(t *testing.T) {
|
||||
cluster := newCluster(testPod, testRS, testDeploy)
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
newPod := strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
uid: "4"
|
||||
name: helm-guestbook-pod2
|
||||
namespace: default
|
||||
ownerReferences:
|
||||
- apiVersion: apps/v1
|
||||
kind: ReplicaSet
|
||||
name: helm-guestbook-rs
|
||||
uid: "2"
|
||||
resourceVersion: "123"`)
|
||||
|
||||
cluster.processEvent(watch.Added, newPod)
|
||||
|
||||
rsChildren := getChildren(cluster, testRS)
|
||||
sort.Slice(rsChildren, func(i, j int) bool {
|
||||
return strings.Compare(rsChildren[i].Name, rsChildren[j].Name) < 0
|
||||
})
|
||||
assert.Equal(t, []appv1.ResourceNode{{
|
||||
ResourceRef: appv1.ResourceRef{
|
||||
Kind: "Pod",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-pod",
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
UID: "1",
|
||||
},
|
||||
Info: []appv1.InfoItem{{Name: "Containers", Value: "0/0"}},
|
||||
Health: &appv1.HealthStatus{Status: appv1.HealthStatusUnknown},
|
||||
NetworkingInfo: &appv1.ResourceNetworkingInfo{Labels: testPod.GetLabels()},
|
||||
ParentRefs: []appv1.ResourceRef{{
|
||||
Group: "apps",
|
||||
Version: "",
|
||||
Kind: "ReplicaSet",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-rs",
|
||||
UID: "2",
|
||||
}},
|
||||
ResourceVersion: "123",
|
||||
}, {
|
||||
ResourceRef: appv1.ResourceRef{
|
||||
Kind: "Pod",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-pod2",
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
UID: "4",
|
||||
},
|
||||
NetworkingInfo: &appv1.ResourceNetworkingInfo{Labels: testPod.GetLabels()},
|
||||
Info: []appv1.InfoItem{{Name: "Containers", Value: "0/0"}},
|
||||
Health: &appv1.HealthStatus{Status: appv1.HealthStatusUnknown},
|
||||
ParentRefs: []appv1.ResourceRef{{
|
||||
Group: "apps",
|
||||
Version: "",
|
||||
Kind: "ReplicaSet",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-rs",
|
||||
UID: "2",
|
||||
}},
|
||||
ResourceVersion: "123",
|
||||
}}, rsChildren)
|
||||
}
|
||||
|
||||
func TestUpdateResourceTags(t *testing.T) {
|
||||
pod := &corev1.Pod{
|
||||
TypeMeta: metav1.TypeMeta{Kind: "Pod", APIVersion: "v1"},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "testPod", Namespace: "default"},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "test",
|
||||
Image: "test",
|
||||
}},
|
||||
},
|
||||
}
|
||||
cluster := newCluster(mustToUnstructured(pod))
|
||||
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
podNode := cluster.nodes[kube.GetResourceKey(mustToUnstructured(pod))]
|
||||
|
||||
assert.NotNil(t, podNode)
|
||||
assert.Equal(t, []appv1.InfoItem{{Name: "Containers", Value: "0/1"}}, podNode.info)
|
||||
|
||||
pod.Status = corev1.PodStatus{
|
||||
ContainerStatuses: []corev1.ContainerStatus{{
|
||||
State: corev1.ContainerState{
|
||||
Terminated: &corev1.ContainerStateTerminated{
|
||||
ExitCode: -1,
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
cluster.processEvent(watch.Modified, mustToUnstructured(pod))
|
||||
|
||||
podNode = cluster.nodes[kube.GetResourceKey(mustToUnstructured(pod))]
|
||||
|
||||
assert.NotNil(t, podNode)
|
||||
assert.Equal(t, []appv1.InfoItem{{Name: "Status Reason", Value: "ExitCode:-1"}, {Name: "Containers", Value: "0/1"}}, podNode.info)
|
||||
}
|
||||
|
||||
func TestUpdateAppResource(t *testing.T) {
|
||||
updatesReceived := make([]string, 0)
|
||||
cluster := newCluster(testPod, testRS, testDeploy)
|
||||
cluster.onObjectUpdated = func(managedByApp map[string]bool, _ corev1.ObjectReference) {
|
||||
for appName, fullRefresh := range managedByApp {
|
||||
updatesReceived = append(updatesReceived, fmt.Sprintf("%s: %v", appName, fullRefresh))
|
||||
}
|
||||
}
|
||||
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
cluster.processEvent(watch.Modified, mustToUnstructured(testPod))
|
||||
|
||||
assert.Contains(t, updatesReceived, "helm-guestbook: false")
|
||||
}
|
||||
|
||||
func TestCircularReference(t *testing.T) {
|
||||
dep := testDeploy.DeepCopy()
|
||||
dep.SetOwnerReferences([]metav1.OwnerReference{{
|
||||
Name: testPod.GetName(),
|
||||
Kind: testPod.GetKind(),
|
||||
APIVersion: testPod.GetAPIVersion(),
|
||||
}})
|
||||
cluster := newCluster(testPod, testRS, dep)
|
||||
err := cluster.ensureSynced()
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
children := getChildren(cluster, dep)
|
||||
assert.Len(t, children, 2)
|
||||
|
||||
node := cluster.nodes[kube.GetResourceKey(dep)]
|
||||
assert.NotNil(t, node)
|
||||
app := node.getApp(cluster.nodes)
|
||||
assert.Equal(t, "", app)
|
||||
}
|
||||
|
||||
func TestWatchCacheUpdated(t *testing.T) {
|
||||
removed := testPod.DeepCopy()
|
||||
removed.SetName(testPod.GetName() + "-removed-pod")
|
||||
|
||||
updated := testPod.DeepCopy()
|
||||
updated.SetName(testPod.GetName() + "-updated-pod")
|
||||
updated.SetResourceVersion("updated-pod-version")
|
||||
|
||||
cluster := newCluster(removed, updated)
|
||||
err := cluster.ensureSynced()
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
added := testPod.DeepCopy()
|
||||
added.SetName(testPod.GetName() + "-new-pod")
|
||||
|
||||
podGroupKind := testPod.GroupVersionKind().GroupKind()
|
||||
|
||||
cluster.replaceResourceCache(podGroupKind, "updated-list-version", []unstructured.Unstructured{*updated, *added})
|
||||
|
||||
_, ok := cluster.nodes[kube.GetResourceKey(removed)]
|
||||
assert.False(t, ok)
|
||||
|
||||
updatedNode, ok := cluster.nodes[kube.GetResourceKey(updated)]
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, updatedNode.resourceVersion, "updated-pod-version")
|
||||
|
||||
_, ok = cluster.nodes[kube.GetResourceKey(added)]
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestGetDuplicatedChildren(t *testing.T) {
|
||||
extensionsRS := testRS.DeepCopy()
|
||||
extensionsRS.SetGroupVersionKind(schema.GroupVersionKind{Group: "extensions", Kind: kube.ReplicaSetKind, Version: "v1beta1"})
|
||||
cluster := newCluster(testDeploy, testRS, extensionsRS)
|
||||
err := cluster.ensureSynced()
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
// Get children multiple times to make sure the right child is picked up every time.
|
||||
for i := 0; i < 5; i++ {
|
||||
children := getChildren(cluster, testDeploy)
|
||||
assert.Len(t, children, 1)
|
||||
assert.Equal(t, "apps", children[0].Group)
|
||||
assert.Equal(t, kube.ReplicaSetKind, children[0].Kind)
|
||||
assert.Equal(t, testRS.GetName(), children[0].Name)
|
||||
}
|
||||
}
|
||||
37
controller/cache/info.go
vendored
@@ -3,38 +3,37 @@ package cache
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/text"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
k8snode "k8s.io/kubernetes/pkg/util/node"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/resource"
|
||||
)
|
||||
|
||||
func populateNodeInfo(un *unstructured.Unstructured, node *node) {
|
||||
|
||||
func populateNodeInfo(un *unstructured.Unstructured, res *ResourceInfo) {
|
||||
gvk := un.GroupVersionKind()
|
||||
revision := resource.GetRevision(un)
|
||||
if revision > 0 {
|
||||
node.info = append(node.info, v1alpha1.InfoItem{Name: "Revision", Value: fmt.Sprintf("Rev:%v", revision)})
|
||||
res.Info = append(res.Info, v1alpha1.InfoItem{Name: "Revision", Value: fmt.Sprintf("Rev:%v", revision)})
|
||||
}
|
||||
switch gvk.Group {
|
||||
case "":
|
||||
switch gvk.Kind {
|
||||
case kube.PodKind:
|
||||
populatePodInfo(un, node)
|
||||
populatePodInfo(un, res)
|
||||
return
|
||||
case kube.ServiceKind:
|
||||
populateServiceInfo(un, node)
|
||||
populateServiceInfo(un, res)
|
||||
return
|
||||
}
|
||||
case "extensions", "networking.k8s.io":
|
||||
switch gvk.Kind {
|
||||
case kube.IngressKind:
|
||||
populateIngressInfo(un, node)
|
||||
populateIngressInfo(un, res)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -58,16 +57,16 @@ func getIngress(un *unstructured.Unstructured) []v1.LoadBalancerIngress {
|
||||
return res
|
||||
}
|
||||
|
||||
func populateServiceInfo(un *unstructured.Unstructured, node *node) {
|
||||
func populateServiceInfo(un *unstructured.Unstructured, res *ResourceInfo) {
|
||||
targetLabels, _, _ := unstructured.NestedStringMap(un.Object, "spec", "selector")
|
||||
ingress := make([]v1.LoadBalancerIngress, 0)
|
||||
if serviceType, ok, err := unstructured.NestedString(un.Object, "spec", "type"); ok && err == nil && serviceType == string(v1.ServiceTypeLoadBalancer) {
|
||||
ingress = getIngress(un)
|
||||
}
|
||||
node.networkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetLabels: targetLabels, Ingress: ingress}
|
||||
res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetLabels: targetLabels, Ingress: ingress}
|
||||
}
|
||||
|
||||
func populateIngressInfo(un *unstructured.Unstructured, node *node) {
|
||||
func populateIngressInfo(un *unstructured.Unstructured, res *ResourceInfo) {
|
||||
ingress := getIngress(un)
|
||||
targetsMap := make(map[v1alpha1.ResourceRef]bool)
|
||||
if backend, ok, err := unstructured.NestedMap(un.Object, "spec", "backend"); ok && err == nil {
|
||||
@@ -88,7 +87,7 @@ func populateIngressInfo(un *unstructured.Unstructured, node *node) {
|
||||
host := rule["host"]
|
||||
if host == nil || host == "" {
|
||||
for i := range ingress {
|
||||
host = util.FirstNonEmpty(ingress[i].Hostname, ingress[i].IP)
|
||||
host = text.FirstNonEmpty(ingress[i].Hostname, ingress[i].IP)
|
||||
if host != "" {
|
||||
break
|
||||
}
|
||||
@@ -155,10 +154,10 @@ func populateIngressInfo(un *unstructured.Unstructured, node *node) {
|
||||
for url := range urlsSet {
|
||||
urls = append(urls, url)
|
||||
}
|
||||
node.networkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetRefs: targets, Ingress: ingress, ExternalURLs: urls}
|
||||
res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetRefs: targets, Ingress: ingress, ExternalURLs: urls}
|
||||
}
|
||||
|
||||
func populatePodInfo(un *unstructured.Unstructured, node *node) {
|
||||
func populatePodInfo(un *unstructured.Unstructured, res *ResourceInfo) {
|
||||
pod := v1.Pod{}
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, &pod)
|
||||
if err != nil {
|
||||
@@ -181,9 +180,9 @@ func populatePodInfo(un *unstructured.Unstructured, node *node) {
|
||||
imagesSet[container.Image] = true
|
||||
}
|
||||
|
||||
node.images = nil
|
||||
res.Images = nil
|
||||
for image := range imagesSet {
|
||||
node.images = append(node.images, image)
|
||||
res.Images = append(res.Images, image)
|
||||
}
|
||||
|
||||
initializing := false
|
||||
@@ -250,8 +249,8 @@ func populatePodInfo(un *unstructured.Unstructured, node *node) {
|
||||
}
|
||||
|
||||
if reason != "" {
|
||||
node.info = append(node.info, v1alpha1.InfoItem{Name: "Status Reason", Value: reason})
|
||||
res.Info = append(res.Info, v1alpha1.InfoItem{Name: "Status Reason", Value: reason})
|
||||
}
|
||||
node.info = append(node.info, v1alpha1.InfoItem{Name: "Containers", Value: fmt.Sprintf("%d/%d", readyContainers, totalContainers)})
|
||||
node.networkingInfo = &v1alpha1.ResourceNetworkingInfo{Labels: un.GetLabels()}
|
||||
res.Info = append(res.Info, v1alpha1.InfoItem{Name: "Containers", Value: fmt.Sprintf("%d/%d", readyContainers, totalContainers)})
|
||||
res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{Labels: un.GetLabels()}
|
||||
}
|
||||
|
||||
120
controller/cache/info_test.go
vendored
@@ -5,12 +5,68 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
"github.com/argoproj/pkg/errors"
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/stretchr/testify/assert"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
)
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
func strToUnstructured(jsonStr string) *unstructured.Unstructured {
|
||||
obj := make(map[string]interface{})
|
||||
err := yaml.Unmarshal([]byte(jsonStr), &obj)
|
||||
errors.CheckError(err)
|
||||
return &unstructured.Unstructured{Object: obj}
|
||||
}
|
||||
|
||||
var (
|
||||
testService = strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
resourceVersion: "123"
|
||||
uid: "4"
|
||||
spec:
|
||||
selector:
|
||||
app: guestbook
|
||||
type: LoadBalancer
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
- hostname: localhost`)
|
||||
|
||||
testIngress = strToUnstructured(`
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
uid: "4"
|
||||
spec:
|
||||
backend:
|
||||
serviceName: not-found-service
|
||||
servicePort: 443
|
||||
rules:
|
||||
- host: helm-guestbook.com
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: helm-guestbook
|
||||
servicePort: 443
|
||||
path: /
|
||||
- backend:
|
||||
serviceName: helm-guestbook
|
||||
servicePort: https
|
||||
path: /
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
- ip: 107.178.210.11`)
|
||||
)
|
||||
|
||||
func TestGetPodInfo(t *testing.T) {
|
||||
@@ -31,29 +87,29 @@ func TestGetPodInfo(t *testing.T) {
|
||||
containers:
|
||||
- image: bar`)
|
||||
|
||||
node := &node{}
|
||||
populateNodeInfo(pod, node)
|
||||
assert.Equal(t, []v1alpha1.InfoItem{{Name: "Containers", Value: "0/1"}}, node.info)
|
||||
assert.Equal(t, []string{"bar"}, node.images)
|
||||
assert.Equal(t, &v1alpha1.ResourceNetworkingInfo{Labels: map[string]string{"app": "guestbook"}}, node.networkingInfo)
|
||||
info := &ResourceInfo{}
|
||||
populateNodeInfo(pod, info)
|
||||
assert.Equal(t, []v1alpha1.InfoItem{{Name: "Containers", Value: "0/1"}}, info.Info)
|
||||
assert.Equal(t, []string{"bar"}, info.Images)
|
||||
assert.Equal(t, &v1alpha1.ResourceNetworkingInfo{Labels: map[string]string{"app": "guestbook"}}, info.NetworkingInfo)
|
||||
}
|
||||
|
||||
func TestGetServiceInfo(t *testing.T) {
|
||||
node := &node{}
|
||||
populateNodeInfo(testService, node)
|
||||
assert.Equal(t, 0, len(node.info))
|
||||
info := &ResourceInfo{}
|
||||
populateNodeInfo(testService, info)
|
||||
assert.Equal(t, 0, len(info.Info))
|
||||
assert.Equal(t, &v1alpha1.ResourceNetworkingInfo{
|
||||
TargetLabels: map[string]string{"app": "guestbook"},
|
||||
Ingress: []v1.LoadBalancerIngress{{Hostname: "localhost"}},
|
||||
}, node.networkingInfo)
|
||||
}, info.NetworkingInfo)
|
||||
}
|
||||
|
||||
func TestGetIngressInfo(t *testing.T) {
|
||||
node := &node{}
|
||||
populateNodeInfo(testIngress, node)
|
||||
assert.Equal(t, 0, len(node.info))
|
||||
sort.Slice(node.networkingInfo.TargetRefs, func(i, j int) bool {
|
||||
return strings.Compare(node.networkingInfo.TargetRefs[j].Name, node.networkingInfo.TargetRefs[i].Name) < 0
|
||||
info := &ResourceInfo{}
|
||||
populateNodeInfo(testIngress, 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"}},
|
||||
@@ -69,7 +125,7 @@ func TestGetIngressInfo(t *testing.T) {
|
||||
Name: "helm-guestbook",
|
||||
}},
|
||||
ExternalURLs: []string{"https://helm-guestbook.com/"},
|
||||
}, node.networkingInfo)
|
||||
}, info.NetworkingInfo)
|
||||
}
|
||||
|
||||
func TestGetIngressInfoNoHost(t *testing.T) {
|
||||
@@ -92,8 +148,8 @@ func TestGetIngressInfoNoHost(t *testing.T) {
|
||||
ingress:
|
||||
- ip: 107.178.210.11`)
|
||||
|
||||
node := &node{}
|
||||
populateNodeInfo(ingress, node)
|
||||
info := &ResourceInfo{}
|
||||
populateNodeInfo(ingress, info)
|
||||
|
||||
assert.Equal(t, &v1alpha1.ResourceNetworkingInfo{
|
||||
Ingress: []v1.LoadBalancerIngress{{IP: "107.178.210.11"}},
|
||||
@@ -104,7 +160,7 @@ func TestGetIngressInfoNoHost(t *testing.T) {
|
||||
Name: "helm-guestbook",
|
||||
}},
|
||||
ExternalURLs: []string{"https://107.178.210.11/"},
|
||||
}, node.networkingInfo)
|
||||
}, info.NetworkingInfo)
|
||||
}
|
||||
func TestExternalUrlWithSubPath(t *testing.T) {
|
||||
ingress := strToUnstructured(`
|
||||
@@ -126,11 +182,11 @@ func TestExternalUrlWithSubPath(t *testing.T) {
|
||||
ingress:
|
||||
- ip: 107.178.210.11`)
|
||||
|
||||
node := &node{}
|
||||
populateNodeInfo(ingress, node)
|
||||
info := &ResourceInfo{}
|
||||
populateNodeInfo(ingress, info)
|
||||
|
||||
expectedExternalUrls := []string{"https://107.178.210.11/my/sub/path/"}
|
||||
assert.Equal(t, expectedExternalUrls, node.networkingInfo.ExternalURLs)
|
||||
assert.Equal(t, expectedExternalUrls, info.NetworkingInfo.ExternalURLs)
|
||||
}
|
||||
func TestExternalUrlWithMultipleSubPaths(t *testing.T) {
|
||||
ingress := strToUnstructured(`
|
||||
@@ -160,11 +216,11 @@ func TestExternalUrlWithMultipleSubPaths(t *testing.T) {
|
||||
ingress:
|
||||
- ip: 107.178.210.11`)
|
||||
|
||||
node := &node{}
|
||||
populateNodeInfo(ingress, node)
|
||||
info := &ResourceInfo{}
|
||||
populateNodeInfo(ingress, info)
|
||||
|
||||
expectedExternalUrls := []string{"https://helm-guestbook.com/my/sub/path/", "https://helm-guestbook.com/my/sub/path/2", "https://helm-guestbook.com"}
|
||||
actualURLs := node.networkingInfo.ExternalURLs
|
||||
actualURLs := info.NetworkingInfo.ExternalURLs
|
||||
sort.Strings(expectedExternalUrls)
|
||||
sort.Strings(actualURLs)
|
||||
assert.Equal(t, expectedExternalUrls, actualURLs)
|
||||
@@ -188,11 +244,11 @@ func TestExternalUrlWithNoSubPath(t *testing.T) {
|
||||
ingress:
|
||||
- ip: 107.178.210.11`)
|
||||
|
||||
node := &node{}
|
||||
populateNodeInfo(ingress, node)
|
||||
info := &ResourceInfo{}
|
||||
populateNodeInfo(ingress, info)
|
||||
|
||||
expectedExternalUrls := []string{"https://107.178.210.11"}
|
||||
assert.Equal(t, expectedExternalUrls, node.networkingInfo.ExternalURLs)
|
||||
assert.Equal(t, expectedExternalUrls, info.NetworkingInfo.ExternalURLs)
|
||||
}
|
||||
|
||||
func TestExternalUrlWithNetworkingApi(t *testing.T) {
|
||||
@@ -214,9 +270,9 @@ func TestExternalUrlWithNetworkingApi(t *testing.T) {
|
||||
ingress:
|
||||
- ip: 107.178.210.11`)
|
||||
|
||||
node := &node{}
|
||||
populateNodeInfo(ingress, node)
|
||||
info := &ResourceInfo{}
|
||||
populateNodeInfo(ingress, info)
|
||||
|
||||
expectedExternalUrls := []string{"https://107.178.210.11"}
|
||||
assert.Equal(t, expectedExternalUrls, node.networkingInfo.ExternalURLs)
|
||||
assert.Equal(t, expectedExternalUrls, info.NetworkingInfo.ExternalURLs)
|
||||
}
|
||||
|
||||
74
controller/cache/mocks/LiveStateCache.go
vendored
@@ -5,8 +5,9 @@ package mocks
|
||||
import (
|
||||
context "context"
|
||||
|
||||
metrics "github.com/argoproj/argo-cd/controller/metrics"
|
||||
kube "github.com/argoproj/argo-cd/util/kube"
|
||||
cache "github.com/argoproj/gitops-engine/pkg/cache"
|
||||
|
||||
kube "github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
@@ -14,6 +15,8 @@ import (
|
||||
|
||||
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
v1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
@@ -22,16 +25,39 @@ type LiveStateCache struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// GetClusterCache provides a mock function with given fields: server
|
||||
func (_m *LiveStateCache) GetClusterCache(server string) (cache.ClusterCache, error) {
|
||||
ret := _m.Called(server)
|
||||
|
||||
var r0 cache.ClusterCache
|
||||
if rf, ok := ret.Get(0).(func(string) cache.ClusterCache); ok {
|
||||
r0 = rf(server)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(cache.ClusterCache)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(server)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetClustersInfo provides a mock function with given fields:
|
||||
func (_m *LiveStateCache) GetClustersInfo() []metrics.ClusterInfo {
|
||||
func (_m *LiveStateCache) GetClustersInfo() []cache.ClusterInfo {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []metrics.ClusterInfo
|
||||
if rf, ok := ret.Get(0).(func() []metrics.ClusterInfo); ok {
|
||||
var r0 []cache.ClusterInfo
|
||||
if rf, ok := ret.Get(0).(func() []cache.ClusterInfo); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]metrics.ClusterInfo)
|
||||
r0 = ret.Get(0).([]cache.ClusterInfo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,8 +110,8 @@ func (_m *LiveStateCache) GetNamespaceTopLevelResources(server string, namespace
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetServerVersion provides a mock function with given fields: serverURL
|
||||
func (_m *LiveStateCache) GetServerVersion(serverURL string) (string, error) {
|
||||
// GetVersionsInfo provides a mock function with given fields: serverURL
|
||||
func (_m *LiveStateCache) GetVersionsInfo(serverURL string) (string, []v1.APIGroup, error) {
|
||||
ret := _m.Called(serverURL)
|
||||
|
||||
var r0 string
|
||||
@@ -95,19 +121,37 @@ func (_m *LiveStateCache) GetServerVersion(serverURL string) (string, error) {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
var r1 []v1.APIGroup
|
||||
if rf, ok := ret.Get(1).(func(string) []v1.APIGroup); ok {
|
||||
r1 = rf(serverURL)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).([]v1.APIGroup)
|
||||
}
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
var r2 error
|
||||
if rf, ok := ret.Get(2).(func(string) error); ok {
|
||||
r2 = rf(serverURL)
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// Invalidate provides a mock function with given fields:
|
||||
func (_m *LiveStateCache) Invalidate() {
|
||||
_m.Called()
|
||||
// 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
|
||||
|
||||
142
controller/cache/node.go
vendored
@@ -1,142 +0,0 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type node struct {
|
||||
resourceVersion string
|
||||
ref v1.ObjectReference
|
||||
ownerRefs []metav1.OwnerReference
|
||||
info []appv1.InfoItem
|
||||
appName string
|
||||
// available only for root application nodes
|
||||
resource *unstructured.Unstructured
|
||||
// networkingInfo are available only for known types involved into networking: Ingress, Service, Pod
|
||||
networkingInfo *appv1.ResourceNetworkingInfo
|
||||
images []string
|
||||
health *appv1.HealthStatus
|
||||
}
|
||||
|
||||
func (n *node) isRootAppNode() bool {
|
||||
return n.appName != "" && len(n.ownerRefs) == 0
|
||||
}
|
||||
|
||||
func (n *node) resourceKey() kube.ResourceKey {
|
||||
return kube.NewResourceKey(n.ref.GroupVersionKind().Group, n.ref.Kind, n.ref.Namespace, n.ref.Name)
|
||||
}
|
||||
|
||||
func (n *node) isParentOf(child *node) bool {
|
||||
for i, ownerRef := range child.ownerRefs {
|
||||
|
||||
// backfill UID of inferred owner child references
|
||||
if ownerRef.UID == "" && n.ref.Kind == ownerRef.Kind && n.ref.APIVersion == ownerRef.APIVersion && n.ref.Name == ownerRef.Name {
|
||||
ownerRef.UID = n.ref.UID
|
||||
child.ownerRefs[i] = ownerRef
|
||||
return true
|
||||
}
|
||||
|
||||
if n.ref.UID == ownerRef.UID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func ownerRefGV(ownerRef metav1.OwnerReference) schema.GroupVersion {
|
||||
gv, err := schema.ParseGroupVersion(ownerRef.APIVersion)
|
||||
if err != nil {
|
||||
gv = schema.GroupVersion{}
|
||||
}
|
||||
return gv
|
||||
}
|
||||
|
||||
func (n *node) getApp(ns map[kube.ResourceKey]*node) string {
|
||||
return n.getAppRecursive(ns, map[kube.ResourceKey]bool{})
|
||||
}
|
||||
|
||||
func (n *node) getAppRecursive(ns map[kube.ResourceKey]*node, visited map[kube.ResourceKey]bool) string {
|
||||
if !visited[n.resourceKey()] {
|
||||
visited[n.resourceKey()] = true
|
||||
} else {
|
||||
log.Warnf("Circular dependency detected: %v.", visited)
|
||||
return n.appName
|
||||
}
|
||||
|
||||
if n.appName != "" {
|
||||
return n.appName
|
||||
}
|
||||
for _, ownerRef := range n.ownerRefs {
|
||||
gv := ownerRefGV(ownerRef)
|
||||
if parent, ok := ns[kube.NewResourceKey(gv.Group, ownerRef.Kind, n.ref.Namespace, ownerRef.Name)]; ok {
|
||||
app := parent.getAppRecursive(ns, visited)
|
||||
if app != "" {
|
||||
return app
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func newResourceKeySet(set map[kube.ResourceKey]bool, keys ...kube.ResourceKey) map[kube.ResourceKey]bool {
|
||||
newSet := make(map[kube.ResourceKey]bool)
|
||||
for k, v := range set {
|
||||
newSet[k] = v
|
||||
}
|
||||
for i := range keys {
|
||||
newSet[keys[i]] = true
|
||||
}
|
||||
return newSet
|
||||
}
|
||||
|
||||
func (n *node) asResourceNode() appv1.ResourceNode {
|
||||
gv, err := schema.ParseGroupVersion(n.ref.APIVersion)
|
||||
if err != nil {
|
||||
gv = schema.GroupVersion{}
|
||||
}
|
||||
parentRefs := make([]appv1.ResourceRef, len(n.ownerRefs))
|
||||
for _, ownerRef := range n.ownerRefs {
|
||||
ownerGvk := schema.FromAPIVersionAndKind(ownerRef.APIVersion, ownerRef.Kind)
|
||||
ownerKey := kube.NewResourceKey(ownerGvk.Group, ownerRef.Kind, n.ref.Namespace, ownerRef.Name)
|
||||
parentRefs[0] = appv1.ResourceRef{Name: ownerRef.Name, Kind: ownerKey.Kind, Namespace: n.ref.Namespace, Group: ownerKey.Group, UID: string(ownerRef.UID)}
|
||||
}
|
||||
return appv1.ResourceNode{
|
||||
ResourceRef: appv1.ResourceRef{
|
||||
UID: string(n.ref.UID),
|
||||
Name: n.ref.Name,
|
||||
Group: gv.Group,
|
||||
Version: gv.Version,
|
||||
Kind: n.ref.Kind,
|
||||
Namespace: n.ref.Namespace,
|
||||
},
|
||||
ParentRefs: parentRefs,
|
||||
Info: n.info,
|
||||
ResourceVersion: n.resourceVersion,
|
||||
NetworkingInfo: n.networkingInfo,
|
||||
Images: n.images,
|
||||
Health: n.health,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *node) iterateChildren(ns map[kube.ResourceKey]*node, parents map[kube.ResourceKey]bool, action func(child appv1.ResourceNode, appName string)) {
|
||||
for childKey, child := range ns {
|
||||
if n.isParentOf(ns[childKey]) {
|
||||
if parents[childKey] {
|
||||
key := n.resourceKey()
|
||||
log.Warnf("Circular dependency detected. %s is child and parent of %s", childKey.String(), key.String())
|
||||
} else {
|
||||
action(child.asResourceNode(), child.getApp(ns))
|
||||
child.iterateChildren(ns, newResourceKeySet(parents, n.resourceKey()), action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
83
controller/cache/node_test.go
vendored
@@ -1,83 +0,0 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var c = &clusterInfo{cacheSettingsSrc: func() *cacheSettings {
|
||||
return &cacheSettings{AppInstanceLabelKey: common.LabelKeyAppInstance}
|
||||
}}
|
||||
|
||||
func TestIsParentOf(t *testing.T) {
|
||||
child := c.createObjInfo(testPod, "")
|
||||
parent := c.createObjInfo(testRS, "")
|
||||
grandParent := c.createObjInfo(testDeploy, "")
|
||||
|
||||
assert.True(t, parent.isParentOf(child))
|
||||
assert.False(t, grandParent.isParentOf(child))
|
||||
}
|
||||
|
||||
func TestIsParentOfSameKindDifferentGroupAndUID(t *testing.T) {
|
||||
rs := testRS.DeepCopy()
|
||||
rs.SetAPIVersion("somecrd.io/v1")
|
||||
rs.SetUID("123")
|
||||
child := c.createObjInfo(testPod, "")
|
||||
invalidParent := c.createObjInfo(rs, "")
|
||||
|
||||
assert.False(t, invalidParent.isParentOf(child))
|
||||
}
|
||||
|
||||
func TestIsServiceParentOfEndPointWithTheSameName(t *testing.T) {
|
||||
nonMatchingNameEndPoint := c.createObjInfo(strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: Endpoints
|
||||
metadata:
|
||||
name: not-matching-name
|
||||
namespace: default
|
||||
`), "")
|
||||
|
||||
matchingNameEndPoint := c.createObjInfo(strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: Endpoints
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
`), "")
|
||||
|
||||
parent := c.createObjInfo(testService, "")
|
||||
|
||||
assert.True(t, parent.isParentOf(matchingNameEndPoint))
|
||||
assert.Equal(t, parent.ref.UID, matchingNameEndPoint.ownerRefs[0].UID)
|
||||
assert.False(t, parent.isParentOf(nonMatchingNameEndPoint))
|
||||
}
|
||||
|
||||
func TestIsServiceAccoountParentOfSecret(t *testing.T) {
|
||||
serviceAccount := c.createObjInfo(strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: default
|
||||
namespace: default
|
||||
uid: '123'
|
||||
secrets:
|
||||
- name: default-token-123
|
||||
`), "")
|
||||
tokenSecret := c.createObjInfo(strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
annotations:
|
||||
kubernetes.io/service-account.name: default
|
||||
kubernetes.io/service-account.uid: '123'
|
||||
name: default-token-123
|
||||
namespace: default
|
||||
uid: '345'
|
||||
type: kubernetes.io/service-account-token
|
||||
`), "")
|
||||
|
||||
assert.True(t, serviceAccount.isParentOf(tokenSecret))
|
||||
}
|
||||
116
controller/clusterinfoupdater.go
Normal file
@@ -0,0 +1,116 @@
|
||||
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
|
||||
}
|
||||
|
||||
func NewClusterInfoUpdater(
|
||||
infoSource metrics.HasClustersInfo,
|
||||
db db.ArgoDB,
|
||||
appLister v1alpha1.ApplicationNamespaceLister,
|
||||
cache *appstatecache.Cache) *clusterInfoUpdater {
|
||||
|
||||
return &clusterInfoUpdater{infoSource, db, appLister, cache}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
_ = kube.RunAllAsync(len(clusters.Items), func(i int) error {
|
||||
cluster := clusters.Items[i]
|
||||
if err := c.updateClusterInfo(cluster, infoByServer[cluster.Server]); err != nil {
|
||||
log.Warnf("Failed to save clusters info: %v", err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
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()
|
||||
appInfomer := 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(appInfomer.GetIndexer()).Applications(fakeNamespace)
|
||||
updater := NewClusterInfoUpdater(nil, argoDB, lister, appCache)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/cache"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
@@ -41,25 +43,19 @@ var (
|
||||
)
|
||||
)
|
||||
|
||||
type ClusterInfo struct {
|
||||
Server string
|
||||
K8SVersion string
|
||||
ResourcesCount int
|
||||
APIsCount int
|
||||
LastCacheSyncTime *time.Time
|
||||
}
|
||||
|
||||
type HasClustersInfo interface {
|
||||
GetClustersInfo() []ClusterInfo
|
||||
GetClustersInfo() []cache.ClusterInfo
|
||||
}
|
||||
|
||||
type clusterCollector struct {
|
||||
infoSource HasClustersInfo
|
||||
info []ClusterInfo
|
||||
info []cache.ClusterInfo
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func (c *clusterCollector) Run(ctx context.Context) {
|
||||
// FIXME: complains about SA1015
|
||||
// nolint:staticcheck
|
||||
tick := time.Tick(metricsCollectionInterval)
|
||||
for {
|
||||
select {
|
||||
|
||||
@@ -3,9 +3,11 @@ package metrics
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/health"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -24,13 +26,17 @@ type MetricsServer struct {
|
||||
kubectlExecPendingGauge *prometheus.GaugeVec
|
||||
k8sRequestCounter *prometheus.CounterVec
|
||||
clusterEventsCounter *prometheus.CounterVec
|
||||
redisRequestCounter *prometheus.CounterVec
|
||||
reconcileHistogram *prometheus.HistogramVec
|
||||
redisRequestHistogram *prometheus.HistogramVec
|
||||
registry *prometheus.Registry
|
||||
}
|
||||
|
||||
const (
|
||||
// MetricsPath is the endpoint to collect application metrics
|
||||
MetricsPath = "/metrics"
|
||||
// EnvVarLegacyControllerMetrics is a env var to re-enable deprecated prometheus metrics
|
||||
EnvVarLegacyControllerMetrics = "ARGOCD_LEGACY_CONTROLLER_METRICS"
|
||||
)
|
||||
|
||||
// Follow Prometheus naming practices
|
||||
@@ -41,27 +47,88 @@ var (
|
||||
descAppInfo = prometheus.NewDesc(
|
||||
"argocd_app_info",
|
||||
"Information about application.",
|
||||
append(descAppDefaultLabels, "repo", "dest_server", "dest_namespace"),
|
||||
append(descAppDefaultLabels, "repo", "dest_server", "dest_namespace", "sync_status", "health_status", "operation"),
|
||||
nil,
|
||||
)
|
||||
// DEPRECATED
|
||||
descAppCreated = prometheus.NewDesc(
|
||||
"argocd_app_created_time",
|
||||
"Creation time in unix timestamp for an application.",
|
||||
descAppDefaultLabels,
|
||||
nil,
|
||||
)
|
||||
// DEPRECATED: superceded by sync_status label in argocd_app_info
|
||||
descAppSyncStatusCode = prometheus.NewDesc(
|
||||
"argocd_app_sync_status",
|
||||
"The application current sync status.",
|
||||
append(descAppDefaultLabels, "sync_status"),
|
||||
nil,
|
||||
)
|
||||
// DEPRECATED: superceded by health_status label in argocd_app_info
|
||||
descAppHealthStatus = prometheus.NewDesc(
|
||||
"argocd_app_health_status",
|
||||
"The application current health status.",
|
||||
append(descAppDefaultLabels, "health_status"),
|
||||
nil,
|
||||
)
|
||||
|
||||
syncCounter = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "argocd_app_sync_total",
|
||||
Help: "Number of application syncs.",
|
||||
},
|
||||
append(descAppDefaultLabels, "dest_server", "phase"),
|
||||
)
|
||||
|
||||
k8sRequestCounter = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "argocd_app_k8s_request_total",
|
||||
Help: "Number of kubernetes requests executed during application reconciliation.",
|
||||
},
|
||||
append(descAppDefaultLabels, "server", "response_code", "verb", "resource_kind", "resource_namespace"),
|
||||
)
|
||||
|
||||
kubectlExecCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "argocd_kubectl_exec_total",
|
||||
Help: "Number of kubectl executions",
|
||||
}, []string{"command"})
|
||||
|
||||
kubectlExecPendingGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "argocd_kubectl_exec_pending",
|
||||
Help: "Number of pending kubectl executions",
|
||||
}, []string{"command"})
|
||||
|
||||
reconcileHistogram = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "argocd_app_reconcile",
|
||||
Help: "Application reconciliation performance.",
|
||||
// Buckets chosen after observing a ~2100ms mean reconcile time
|
||||
Buckets: []float64{0.25, .5, 1, 2, 4, 8, 16},
|
||||
},
|
||||
[]string{"namespace", "dest_server"},
|
||||
)
|
||||
|
||||
clusterEventsCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "argocd_cluster_events_total",
|
||||
Help: "Number of processes k8s resource events.",
|
||||
}, append(descClusterDefaultLabels, "group", "kind"))
|
||||
|
||||
redisRequestCounter = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "argocd_redis_request_total",
|
||||
Help: "Number of kubernetes requests executed during application reconciliation.",
|
||||
},
|
||||
[]string{"initiator", "failed"},
|
||||
)
|
||||
|
||||
redisRequestHistogram = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "argocd_redis_request_duration",
|
||||
Help: "Redis requests duration.",
|
||||
Buckets: []float64{0.01, 0.05, 0.10, 0.25, .5, 1},
|
||||
},
|
||||
[]string{"initiator"},
|
||||
)
|
||||
)
|
||||
|
||||
// NewMetricsServer returns a new prometheus server which collects application metrics
|
||||
@@ -76,51 +143,14 @@ func NewMetricsServer(addr string, appLister applister.ApplicationLister, health
|
||||
}, promhttp.HandlerOpts{}))
|
||||
healthz.ServeHealthCheck(mux, healthCheck)
|
||||
|
||||
syncCounter := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "argocd_app_sync_total",
|
||||
Help: "Number of application syncs.",
|
||||
},
|
||||
append(descAppDefaultLabels, "phase"),
|
||||
)
|
||||
registry.MustRegister(syncCounter)
|
||||
|
||||
k8sRequestCounter := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "argocd_app_k8s_request_total",
|
||||
Help: "Number of kubernetes requests executed during application reconciliation.",
|
||||
},
|
||||
append(descAppDefaultLabels, "response_code"),
|
||||
)
|
||||
registry.MustRegister(k8sRequestCounter)
|
||||
|
||||
kubectlExecCounter := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "argocd_kubectl_exec_total",
|
||||
Help: "Number of kubectl executions",
|
||||
}, []string{"command"})
|
||||
registry.MustRegister(kubectlExecCounter)
|
||||
kubectlExecPendingGauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "argocd_kubectl_exec_pending_total",
|
||||
Help: "Number of pending kubectl executions",
|
||||
}, []string{"command"})
|
||||
registry.MustRegister(kubectlExecPendingGauge)
|
||||
|
||||
reconcileHistogram := prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "argocd_app_reconcile",
|
||||
Help: "Application reconciliation performance.",
|
||||
// Buckets chosen after observing a ~2100ms mean reconcile time
|
||||
Buckets: []float64{0.25, .5, 1, 2, 4, 8, 16},
|
||||
},
|
||||
descAppDefaultLabels,
|
||||
)
|
||||
|
||||
registry.MustRegister(reconcileHistogram)
|
||||
clusterEventsCounter := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "argocd_cluster_events_total",
|
||||
Help: "Number of processes k8s resource events.",
|
||||
}, descClusterDefaultLabels)
|
||||
registry.MustRegister(clusterEventsCounter)
|
||||
registry.MustRegister(redisRequestCounter)
|
||||
registry.MustRegister(redisRequestHistogram)
|
||||
|
||||
return &MetricsServer{
|
||||
registry: registry,
|
||||
@@ -134,6 +164,8 @@ func NewMetricsServer(addr string, appLister applister.ApplicationLister, health
|
||||
kubectlExecPendingGauge: kubectlExecPendingGauge,
|
||||
reconcileHistogram: reconcileHistogram,
|
||||
clusterEventsCounter: clusterEventsCounter,
|
||||
redisRequestCounter: redisRequestCounter,
|
||||
redisRequestHistogram: redisRequestHistogram,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +180,7 @@ func (m *MetricsServer) IncSync(app *argoappv1.Application, state *argoappv1.Ope
|
||||
if !state.Phase.Completed() {
|
||||
return
|
||||
}
|
||||
m.syncCounter.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject(), string(state.Phase)).Inc()
|
||||
m.syncCounter.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject(), app.Spec.Destination.Server, string(state.Phase)).Inc()
|
||||
}
|
||||
|
||||
func (m *MetricsServer) IncKubectlExec(command string) {
|
||||
@@ -164,18 +196,36 @@ func (m *MetricsServer) DecKubectlExecPending(command string) {
|
||||
}
|
||||
|
||||
// IncClusterEventsCount increments the number of cluster events
|
||||
func (m *MetricsServer) IncClusterEventsCount(server string) {
|
||||
m.clusterEventsCounter.WithLabelValues(server).Inc()
|
||||
func (m *MetricsServer) IncClusterEventsCount(server, group, kind string) {
|
||||
m.clusterEventsCounter.WithLabelValues(server, group, kind).Inc()
|
||||
}
|
||||
|
||||
// IncKubernetesRequest increments the kubernetes requests counter for an application
|
||||
func (m *MetricsServer) IncKubernetesRequest(app *argoappv1.Application, statusCode int) {
|
||||
m.k8sRequestCounter.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject(), strconv.Itoa(statusCode)).Inc()
|
||||
func (m *MetricsServer) IncKubernetesRequest(app *argoappv1.Application, server, statusCode, verb, resourceKind, resourceNamespace string) {
|
||||
var namespace, name, project string
|
||||
if app != nil {
|
||||
namespace = app.Namespace
|
||||
name = app.Name
|
||||
project = app.Spec.GetProject()
|
||||
}
|
||||
m.k8sRequestCounter.WithLabelValues(
|
||||
namespace, name, project, server, statusCode,
|
||||
verb, resourceKind, resourceNamespace,
|
||||
).Inc()
|
||||
}
|
||||
|
||||
func (m *MetricsServer) IncRedisRequest(failed bool) {
|
||||
m.redisRequestCounter.WithLabelValues("argocd-application-controller", strconv.FormatBool(failed)).Inc()
|
||||
}
|
||||
|
||||
// ObserveRedisRequestDuration observes redis request duration
|
||||
func (m *MetricsServer) ObserveRedisRequestDuration(duration time.Duration) {
|
||||
m.redisRequestHistogram.WithLabelValues("argocd-application-controller").Observe(duration.Seconds())
|
||||
}
|
||||
|
||||
// IncReconcile increments the reconcile counter for an application
|
||||
func (m *MetricsServer) IncReconcile(app *argoappv1.Application, duration time.Duration) {
|
||||
m.reconcileHistogram.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject()).Observe(duration.Seconds())
|
||||
m.reconcileHistogram.WithLabelValues(app.Namespace, app.Spec.Destination.Server).Observe(duration.Seconds())
|
||||
}
|
||||
|
||||
type appCollector struct {
|
||||
@@ -199,7 +249,6 @@ func NewAppRegistry(appLister applister.ApplicationLister) *prometheus.Registry
|
||||
// Describe implements the prometheus.Collector interface
|
||||
func (c *appCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- descAppInfo
|
||||
ch <- descAppCreated
|
||||
ch <- descAppSyncStatusCode
|
||||
ch <- descAppHealthStatus
|
||||
}
|
||||
@@ -233,20 +282,37 @@ func collectApps(ch chan<- prometheus.Metric, app *argoappv1.Application) {
|
||||
addConstMetric(desc, prometheus.GaugeValue, v, lv...)
|
||||
}
|
||||
|
||||
addGauge(descAppInfo, 1, git.NormalizeGitURL(app.Spec.Source.RepoURL), app.Spec.Destination.Server, app.Spec.Destination.Namespace)
|
||||
|
||||
addGauge(descAppCreated, float64(app.CreationTimestamp.Unix()))
|
||||
|
||||
var operation string
|
||||
if app.DeletionTimestamp != nil {
|
||||
operation = "delete"
|
||||
} else if app.Operation != nil && app.Operation.Sync != nil {
|
||||
operation = "sync"
|
||||
}
|
||||
syncStatus := app.Status.Sync.Status
|
||||
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeSynced), string(argoappv1.SyncStatusCodeSynced))
|
||||
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeOutOfSync), string(argoappv1.SyncStatusCodeOutOfSync))
|
||||
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeUnknown || syncStatus == ""), string(argoappv1.SyncStatusCodeUnknown))
|
||||
|
||||
if syncStatus == "" {
|
||||
syncStatus = argoappv1.SyncStatusCodeUnknown
|
||||
}
|
||||
healthStatus := app.Status.Health.Status
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusUnknown || healthStatus == ""), argoappv1.HealthStatusUnknown)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusProgressing), argoappv1.HealthStatusProgressing)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusSuspended), argoappv1.HealthStatusSuspended)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusHealthy), argoappv1.HealthStatusHealthy)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusDegraded), argoappv1.HealthStatusDegraded)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusMissing), argoappv1.HealthStatusMissing)
|
||||
if healthStatus == "" {
|
||||
healthStatus = health.HealthStatusUnknown
|
||||
}
|
||||
|
||||
addGauge(descAppInfo, 1, git.NormalizeGitURL(app.Spec.Source.RepoURL), app.Spec.Destination.Server, app.Spec.Destination.Namespace, string(syncStatus), string(healthStatus), operation)
|
||||
|
||||
// Deprecated controller metrics
|
||||
if os.Getenv(EnvVarLegacyControllerMetrics) == "true" {
|
||||
addGauge(descAppCreated, float64(app.CreationTimestamp.Unix()))
|
||||
|
||||
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeSynced), string(argoappv1.SyncStatusCodeSynced))
|
||||
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeOutOfSync), string(argoappv1.SyncStatusCodeOutOfSync))
|
||||
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeUnknown || syncStatus == ""), string(argoappv1.SyncStatusCodeUnknown))
|
||||
|
||||
healthStatus := app.Status.Health.Status
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == health.HealthStatusUnknown || healthStatus == ""), string(health.HealthStatusUnknown))
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == health.HealthStatusProgressing), string(health.HealthStatusProgressing))
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == health.HealthStatusSuspended), string(health.HealthStatusSuspended))
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == health.HealthStatusHealthy), string(health.HealthStatusHealthy))
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == health.HealthStatusDegraded), string(health.HealthStatusDegraded))
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == health.HealthStatusMissing), string(health.HealthStatusMissing))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/sync/common"
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/stretchr/testify/assert"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -42,25 +44,52 @@ status:
|
||||
status: Healthy
|
||||
`
|
||||
|
||||
const expectedResponse = `# HELP argocd_app_created_time Creation time in unix timestamp for an application.
|
||||
# TYPE argocd_app_created_time gauge
|
||||
argocd_app_created_time{name="my-app",namespace="argocd",project="important-project"} -6.21355968e+10
|
||||
# HELP argocd_app_health_status The application current health status.
|
||||
# TYPE argocd_app_health_status gauge
|
||||
argocd_app_health_status{health_status="Degraded",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
argocd_app_health_status{health_status="Healthy",name="my-app",namespace="argocd",project="important-project"} 1
|
||||
argocd_app_health_status{health_status="Missing",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
argocd_app_health_status{health_status="Progressing",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
argocd_app_health_status{health_status="Suspended",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
argocd_app_health_status{health_status="Unknown",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
# HELP argocd_app_info Information about application.
|
||||
# TYPE argocd_app_info gauge
|
||||
argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",name="my-app",namespace="argocd",project="important-project",repo="https://github.com/argoproj/argocd-example-apps"} 1
|
||||
# HELP argocd_app_sync_status The application current sync status.
|
||||
# TYPE argocd_app_sync_status gauge
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="OutOfSync"} 0
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="Synced"} 1
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="Unknown"} 0
|
||||
const fakeApp2 = `
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: my-app-2
|
||||
namespace: argocd
|
||||
spec:
|
||||
destination:
|
||||
namespace: dummy-namespace
|
||||
server: https://localhost:6443
|
||||
project: important-project
|
||||
source:
|
||||
path: some/path
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps.git
|
||||
status:
|
||||
sync:
|
||||
status: Synced
|
||||
health:
|
||||
status: Healthy
|
||||
operation:
|
||||
sync:
|
||||
revision: 041eab7439ece92c99b043f0e171788185b8fc1d
|
||||
syncStrategy:
|
||||
hook: {}
|
||||
`
|
||||
|
||||
const fakeApp3 = `
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: my-app-3
|
||||
namespace: argocd
|
||||
deletionTimestamp: "2020-03-16T09:17:45Z"
|
||||
spec:
|
||||
destination:
|
||||
namespace: dummy-namespace
|
||||
server: https://localhost:6443
|
||||
project: important-project
|
||||
source:
|
||||
path: some/path
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps.git
|
||||
status:
|
||||
sync:
|
||||
status: OutOfSync
|
||||
health:
|
||||
status: Degraded
|
||||
`
|
||||
|
||||
const fakeDefaultApp = `
|
||||
@@ -83,46 +112,26 @@ status:
|
||||
status: Healthy
|
||||
`
|
||||
|
||||
const expectedDefaultResponse = `# HELP argocd_app_created_time Creation time in unix timestamp for an application.
|
||||
# TYPE argocd_app_created_time gauge
|
||||
argocd_app_created_time{name="my-app",namespace="argocd",project="default"} -6.21355968e+10
|
||||
# HELP argocd_app_health_status The application current health status.
|
||||
# TYPE argocd_app_health_status gauge
|
||||
argocd_app_health_status{health_status="Degraded",name="my-app",namespace="argocd",project="default"} 0
|
||||
argocd_app_health_status{health_status="Healthy",name="my-app",namespace="argocd",project="default"} 1
|
||||
argocd_app_health_status{health_status="Missing",name="my-app",namespace="argocd",project="default"} 0
|
||||
argocd_app_health_status{health_status="Progressing",name="my-app",namespace="argocd",project="default"} 0
|
||||
argocd_app_health_status{health_status="Suspended",name="my-app",namespace="argocd",project="default"} 0
|
||||
argocd_app_health_status{health_status="Unknown",name="my-app",namespace="argocd",project="default"} 0
|
||||
# HELP argocd_app_info Information about application.
|
||||
# TYPE argocd_app_info gauge
|
||||
argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",name="my-app",namespace="argocd",project="default",repo="https://github.com/argoproj/argocd-example-apps"} 1
|
||||
# HELP argocd_app_sync_status The application current sync status.
|
||||
# TYPE argocd_app_sync_status gauge
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="default",sync_status="OutOfSync"} 0
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="default",sync_status="Synced"} 1
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="default",sync_status="Unknown"} 0
|
||||
`
|
||||
|
||||
var noOpHealthCheck = func() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newFakeApp(fakeApp string) *argoappv1.Application {
|
||||
func newFakeApp(fakeAppYAML string) *argoappv1.Application {
|
||||
var app argoappv1.Application
|
||||
err := yaml.Unmarshal([]byte(fakeApp), &app)
|
||||
err := yaml.Unmarshal([]byte(fakeAppYAML), &app)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &app
|
||||
}
|
||||
|
||||
func newFakeLister(fakeApp ...string) (context.CancelFunc, applister.ApplicationLister) {
|
||||
func newFakeLister(fakeAppYAMLs ...string) (context.CancelFunc, applister.ApplicationLister) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
var fakeApps []runtime.Object
|
||||
for _, name := range fakeApp {
|
||||
fakeApps = append(fakeApps, newFakeApp(name))
|
||||
for _, appYAML := range fakeAppYAMLs {
|
||||
a := newFakeApp(appYAML)
|
||||
fakeApps = append(fakeApps, a)
|
||||
}
|
||||
appClientset := appclientset.NewSimpleClientset(fakeApps...)
|
||||
factory := appinformer.NewFilteredSharedInformerFactory(appClientset, 0, "argocd", func(options *metav1.ListOptions) {})
|
||||
@@ -134,8 +143,8 @@ func newFakeLister(fakeApp ...string) (context.CancelFunc, applister.Application
|
||||
return cancel, factory.Argoproj().V1alpha1().Applications().Lister()
|
||||
}
|
||||
|
||||
func testApp(t *testing.T, fakeApp string, expectedResponse string) {
|
||||
cancel, appLister := newFakeLister(fakeApp)
|
||||
func testApp(t *testing.T, fakeAppYAMLs []string, expectedResponse string) {
|
||||
cancel, appLister := newFakeLister(fakeAppYAMLs...)
|
||||
defer cancel()
|
||||
metricsServ := NewMetricsServer("localhost:8082", appLister, noOpHealthCheck)
|
||||
req, err := http.NewRequest("GET", "/metrics", nil)
|
||||
@@ -149,45 +158,81 @@ func testApp(t *testing.T, fakeApp string, expectedResponse string) {
|
||||
}
|
||||
|
||||
type testCombination struct {
|
||||
application string
|
||||
applications []string
|
||||
expectedResponse string
|
||||
}
|
||||
|
||||
func TestMetrics(t *testing.T) {
|
||||
combinations := []testCombination{
|
||||
{
|
||||
application: fakeApp,
|
||||
expectedResponse: expectedResponse,
|
||||
applications: []string{fakeApp, fakeApp2, fakeApp3},
|
||||
expectedResponse: `
|
||||
# HELP argocd_app_info Information about application.
|
||||
# TYPE argocd_app_info gauge
|
||||
argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",health_status="Degraded",name="my-app-3",namespace="argocd",operation="delete",project="important-project",repo="https://github.com/argoproj/argocd-example-apps",sync_status="OutOfSync"} 1
|
||||
argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",health_status="Healthy",name="my-app",namespace="argocd",operation="",project="important-project",repo="https://github.com/argoproj/argocd-example-apps",sync_status="Synced"} 1
|
||||
argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",health_status="Healthy",name="my-app-2",namespace="argocd",operation="sync",project="important-project",repo="https://github.com/argoproj/argocd-example-apps",sync_status="Synced"} 1
|
||||
`,
|
||||
},
|
||||
{
|
||||
application: fakeDefaultApp,
|
||||
expectedResponse: expectedDefaultResponse,
|
||||
applications: []string{fakeDefaultApp},
|
||||
expectedResponse: `
|
||||
# HELP argocd_app_info Information about application.
|
||||
# TYPE argocd_app_info gauge
|
||||
argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",health_status="Healthy",name="my-app",namespace="argocd",operation="",project="default",repo="https://github.com/argoproj/argocd-example-apps",sync_status="Synced"} 1
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, combination := range combinations {
|
||||
testApp(t, combination.application, combination.expectedResponse)
|
||||
testApp(t, combination.applications, combination.expectedResponse)
|
||||
}
|
||||
}
|
||||
|
||||
const appSyncTotal = `# HELP argocd_app_sync_total Number of application syncs.
|
||||
# TYPE argocd_app_sync_total counter
|
||||
argocd_app_sync_total{name="my-app",namespace="argocd",phase="Error",project="important-project"} 1
|
||||
argocd_app_sync_total{name="my-app",namespace="argocd",phase="Failed",project="important-project"} 1
|
||||
argocd_app_sync_total{name="my-app",namespace="argocd",phase="Succeeded",project="important-project"} 2
|
||||
func TestLegacyMetrics(t *testing.T) {
|
||||
os.Setenv(EnvVarLegacyControllerMetrics, "true")
|
||||
defer os.Unsetenv(EnvVarLegacyControllerMetrics)
|
||||
|
||||
expectedResponse := `
|
||||
# HELP argocd_app_created_time Creation time in unix timestamp for an application.
|
||||
# TYPE argocd_app_created_time gauge
|
||||
argocd_app_created_time{name="my-app",namespace="argocd",project="important-project"} -6.21355968e+10
|
||||
# HELP argocd_app_health_status The application current health status.
|
||||
# TYPE argocd_app_health_status gauge
|
||||
argocd_app_health_status{health_status="Degraded",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
argocd_app_health_status{health_status="Healthy",name="my-app",namespace="argocd",project="important-project"} 1
|
||||
argocd_app_health_status{health_status="Missing",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
argocd_app_health_status{health_status="Progressing",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
argocd_app_health_status{health_status="Suspended",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
argocd_app_health_status{health_status="Unknown",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
# HELP argocd_app_sync_status The application current sync status.
|
||||
# TYPE argocd_app_sync_status gauge
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="OutOfSync"} 0
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="Synced"} 1
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="Unknown"} 0
|
||||
`
|
||||
testApp(t, []string{fakeApp}, expectedResponse)
|
||||
}
|
||||
|
||||
func TestMetricsSyncCounter(t *testing.T) {
|
||||
cancel, appLister := newFakeLister()
|
||||
defer cancel()
|
||||
metricsServ := NewMetricsServer("localhost:8082", appLister, noOpHealthCheck)
|
||||
|
||||
appSyncTotal := `
|
||||
# HELP argocd_app_sync_total Number of application syncs.
|
||||
# TYPE argocd_app_sync_total counter
|
||||
argocd_app_sync_total{dest_server="https://localhost:6443",name="my-app",namespace="argocd",phase="Error",project="important-project"} 1
|
||||
argocd_app_sync_total{dest_server="https://localhost:6443",name="my-app",namespace="argocd",phase="Failed",project="important-project"} 1
|
||||
argocd_app_sync_total{dest_server="https://localhost:6443",name="my-app",namespace="argocd",phase="Succeeded",project="important-project"} 2
|
||||
`
|
||||
|
||||
fakeApp := newFakeApp(fakeApp)
|
||||
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: argoappv1.OperationRunning})
|
||||
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: argoappv1.OperationFailed})
|
||||
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: argoappv1.OperationError})
|
||||
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: argoappv1.OperationSucceeded})
|
||||
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: argoappv1.OperationSucceeded})
|
||||
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: common.OperationRunning})
|
||||
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: common.OperationFailed})
|
||||
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: common.OperationError})
|
||||
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: common.OperationSucceeded})
|
||||
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: common.OperationSucceeded})
|
||||
|
||||
req, err := http.NewRequest("GET", "/metrics", nil)
|
||||
assert.NoError(t, err)
|
||||
@@ -202,27 +247,31 @@ func TestMetricsSyncCounter(t *testing.T) {
|
||||
// assertMetricsPrinted asserts every line in the expected lines appears in the body
|
||||
func assertMetricsPrinted(t *testing.T, expectedLines, body string) {
|
||||
for _, line := range strings.Split(expectedLines, "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
assert.Contains(t, body, line)
|
||||
}
|
||||
}
|
||||
|
||||
const appReconcileMetrics = `argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="0.25"} 0
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="0.5"} 0
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="1"} 0
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="2"} 0
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="4"} 0
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="8"} 1
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="16"} 1
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="+Inf"} 1
|
||||
argocd_app_reconcile_sum{name="my-app",namespace="argocd",project="important-project"} 5
|
||||
argocd_app_reconcile_count{name="my-app",namespace="argocd",project="important-project"} 1
|
||||
`
|
||||
|
||||
func TestReconcileMetrics(t *testing.T) {
|
||||
cancel, appLister := newFakeLister()
|
||||
defer cancel()
|
||||
metricsServ := NewMetricsServer("localhost:8082", appLister, noOpHealthCheck)
|
||||
|
||||
appReconcileMetrics := `
|
||||
# HELP argocd_app_reconcile Application reconciliation performance.
|
||||
# TYPE argocd_app_reconcile histogram
|
||||
argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="0.25"} 0
|
||||
argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="0.5"} 0
|
||||
argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="1"} 0
|
||||
argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="2"} 0
|
||||
argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="4"} 0
|
||||
argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="8"} 1
|
||||
argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="16"} 1
|
||||
argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="+Inf"} 1
|
||||
argocd_app_reconcile_sum{dest_server="https://localhost:6443",namespace="argocd"} 5
|
||||
argocd_app_reconcile_count{dest_server="https://localhost:6443",namespace="argocd"} 1
|
||||
`
|
||||
fakeApp := newFakeApp(fakeApp)
|
||||
metricsServ.IncReconcile(fakeApp, 5*time.Second)
|
||||
|
||||
|
||||
@@ -1,37 +1,24 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/argoproj/pkg/kubeclientmetrics"
|
||||
"k8s.io/client-go/rest"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
type metricsRoundTripper struct {
|
||||
roundTripper http.RoundTripper
|
||||
app *v1alpha1.Application
|
||||
metricsServer *MetricsServer
|
||||
}
|
||||
|
||||
func (mrt *metricsRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
resp, err := mrt.roundTripper.RoundTrip(r)
|
||||
statusCode := 0
|
||||
if resp != nil {
|
||||
statusCode = resp.StatusCode
|
||||
}
|
||||
mrt.metricsServer.IncKubernetesRequest(mrt.app, statusCode)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// AddMetricsTransportWrapper adds a transport wrapper which increments 'argocd_app_k8s_request_total' counter on each kubernetes request
|
||||
func AddMetricsTransportWrapper(server *MetricsServer, app *v1alpha1.Application, config *rest.Config) *rest.Config {
|
||||
wrap := config.WrapTransport
|
||||
config.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
|
||||
if wrap != nil {
|
||||
rt = wrap(rt)
|
||||
}
|
||||
return &metricsRoundTripper{roundTripper: rt, metricsServer: server, app: app}
|
||||
inc := func(resourceInfo kubeclientmetrics.ResourceInfo) error {
|
||||
namespace := resourceInfo.Namespace
|
||||
kind := resourceInfo.Kind
|
||||
statusCode := strconv.Itoa(resourceInfo.StatusCode)
|
||||
server.IncKubernetesRequest(app, resourceInfo.Server, statusCode, string(resourceInfo.Verb), kind, namespace)
|
||||
return nil
|
||||
}
|
||||
return config
|
||||
|
||||
newConfig := kubeclientmetrics.AddMetricsTransportWrapper(config, inc)
|
||||
return newConfig
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -6,6 +6,14 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/diff"
|
||||
"github.com/argoproj/gitops-engine/pkg/health"
|
||||
"github.com/argoproj/gitops-engine/pkg/sync"
|
||||
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"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
@@ -20,18 +28,21 @@ import (
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
"github.com/argoproj/argo-cd/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/argo"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/diff"
|
||||
"github.com/argoproj/argo-cd/util/health"
|
||||
hookutil "github.com/argoproj/argo-cd/util/hook"
|
||||
kubeutil "github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/resource"
|
||||
"github.com/argoproj/argo-cd/util/resource/ignore"
|
||||
"github.com/argoproj/argo-cd/util/gpg"
|
||||
argohealth "github.com/argoproj/argo-cd/util/health"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
)
|
||||
|
||||
type resourceInfoProviderStub struct {
|
||||
}
|
||||
|
||||
func (r *resourceInfoProviderStub) IsNamespaced(_ schema.GroupKind) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
type managedResource struct {
|
||||
Target *unstructured.Unstructured
|
||||
Live *unstructured.Unstructured
|
||||
@@ -44,16 +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
|
||||
}
|
||||
return objs
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
type ResourceInfoProvider interface {
|
||||
IsNamespaced(server string, gk schema.GroupKind) (bool, error)
|
||||
liveObjs = append(liveObjs, resource.Live)
|
||||
resStatuses = append(resStatuses, &statuses[i])
|
||||
}
|
||||
return resStatuses, liveObjs
|
||||
}
|
||||
|
||||
// AppStateManager defines methods which allow to compare application spec and actual application state.
|
||||
@@ -63,23 +76,23 @@ type AppStateManager interface {
|
||||
}
|
||||
|
||||
type comparisonResult struct {
|
||||
syncStatus *v1alpha1.SyncStatus
|
||||
healthStatus *v1alpha1.HealthStatus
|
||||
resources []v1alpha1.ResourceStatus
|
||||
managedResources []managedResource
|
||||
hooks []*unstructured.Unstructured
|
||||
diffNormalizer diff.Normalizer
|
||||
appSourceType v1alpha1.ApplicationSourceType
|
||||
syncStatus *v1alpha1.SyncStatus
|
||||
healthStatus *v1alpha1.HealthStatus
|
||||
resources []v1alpha1.ResourceStatus
|
||||
managedResources []managedResource
|
||||
reconciliationResult sync.ReconciliationResult
|
||||
diffNormalizer diff.Normalizer
|
||||
appSourceType v1alpha1.ApplicationSourceType
|
||||
// timings maps phases of comparison to the duration it took to complete (for statistical purposes)
|
||||
timings map[string]time.Duration
|
||||
}
|
||||
|
||||
func (cr *comparisonResult) targetObjs() []*unstructured.Unstructured {
|
||||
objs := cr.hooks
|
||||
for _, r := range cr.managedResources {
|
||||
if r.Target != nil {
|
||||
objs = append(objs, r.Target)
|
||||
}
|
||||
}
|
||||
return objs
|
||||
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
|
||||
@@ -95,20 +108,23 @@ type appStateManager struct {
|
||||
namespace string
|
||||
}
|
||||
|
||||
func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1.ApplicationSource, appLabelKey, revision string, noCache bool) ([]*unstructured.Unstructured, []*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 {
|
||||
return nil, nil, nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
ts.AddCheckpoint("helm_ms")
|
||||
repo, err := m.db.GetRepository(context.Background(), source.RepoURL)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
ts.AddCheckpoint("repo_ms")
|
||||
conn, repoClient, err := m.repoClientset.NewRepoServerClient()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
defer util.Close(conn)
|
||||
defer io.Close(conn)
|
||||
|
||||
if revision == "" {
|
||||
revision = source.TargetRevision
|
||||
@@ -116,22 +132,28 @@ func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1
|
||||
|
||||
plugins, err := m.settingsMgr.GetConfigManagementPlugins()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
ts.AddCheckpoint("plugins_ms")
|
||||
tools := make([]*appv1.ConfigManagementPlugin, len(plugins))
|
||||
for i := range plugins {
|
||||
tools[i] = &plugins[i]
|
||||
}
|
||||
|
||||
buildOptions, err := m.settingsMgr.GetKustomizeBuildOptions()
|
||||
kustomizeSettings, err := m.settingsMgr.GetKustomizeSettings()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
serverVersion, err := m.liveStateCache.GetServerVersion(app.Spec.Destination.Server)
|
||||
kustomizeOptions, err := kustomizeSettings.GetOptions(app.Spec.Source)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
ts.AddCheckpoint("build_options_ms")
|
||||
serverVersion, apiGroups, err := m.liveStateCache.GetVersionsInfo(app.Spec.Destination.Server)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ts.AddCheckpoint("version_ms")
|
||||
manifestInfo, err := repoClient.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: repo,
|
||||
Repos: helmRepos,
|
||||
@@ -142,61 +164,64 @@ func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1
|
||||
Namespace: app.Spec.Destination.Namespace,
|
||||
ApplicationSource: &source,
|
||||
Plugins: tools,
|
||||
KustomizeOptions: &appv1.KustomizeOptions{
|
||||
BuildOptions: buildOptions,
|
||||
},
|
||||
KubeVersion: serverVersion,
|
||||
KustomizeOptions: kustomizeOptions,
|
||||
KubeVersion: serverVersion,
|
||||
ApiVersions: argo.APIGroupsToVersions(apiGroups),
|
||||
VerifySignature: verifySignature,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
targetObjs, hooks, err := unmarshalManifests(manifestInfo.Manifests)
|
||||
targetObjs, err := unmarshalManifests(manifestInfo.Manifests)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
return targetObjs, hooks, manifestInfo, nil
|
||||
|
||||
ts.AddCheckpoint("unmarshal_ms")
|
||||
logCtx := log.WithField("application", app.Name)
|
||||
for k, v := range ts.Timings() {
|
||||
logCtx = logCtx.WithField(k, v.Milliseconds())
|
||||
}
|
||||
logCtx = logCtx.WithField("time_ms", time.Since(ts.StartTime).Milliseconds())
|
||||
logCtx.Info("getRepoObjs stats")
|
||||
return targetObjs, manifestInfo, nil
|
||||
}
|
||||
|
||||
func unmarshalManifests(manifests []string) ([]*unstructured.Unstructured, []*unstructured.Unstructured, error) {
|
||||
func unmarshalManifests(manifests []string) ([]*unstructured.Unstructured, error) {
|
||||
targetObjs := make([]*unstructured.Unstructured, 0)
|
||||
hooks := make([]*unstructured.Unstructured, 0)
|
||||
for _, manifest := range manifests {
|
||||
obj, err := v1alpha1.UnmarshalToUnstructured(manifest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if ignore.Ignore(obj) {
|
||||
continue
|
||||
}
|
||||
if hookutil.IsHook(obj) {
|
||||
hooks = append(hooks, obj)
|
||||
} else {
|
||||
targetObjs = append(targetObjs, obj)
|
||||
return nil, err
|
||||
}
|
||||
targetObjs = append(targetObjs, obj)
|
||||
}
|
||||
return targetObjs, hooks, nil
|
||||
return targetObjs, nil
|
||||
}
|
||||
|
||||
func DeduplicateTargetObjects(
|
||||
server string,
|
||||
namespace string,
|
||||
objs []*unstructured.Unstructured,
|
||||
infoProvider ResourceInfoProvider,
|
||||
infoProvider kubeutil.ResourceInfoProvider,
|
||||
) ([]*unstructured.Unstructured, []v1alpha1.ApplicationCondition, error) {
|
||||
|
||||
targetByKey := make(map[kubeutil.ResourceKey][]*unstructured.Unstructured)
|
||||
for i := range objs {
|
||||
obj := objs[i]
|
||||
isNamespaced, err := infoProvider.IsNamespaced(server, obj.GroupVersionKind().GroupKind())
|
||||
if err != nil {
|
||||
return objs, nil, err
|
||||
if obj == nil {
|
||||
continue
|
||||
}
|
||||
isNamespaced := kubeutil.IsNamespacedOrUnknown(infoProvider, obj.GroupVersionKind().GroupKind())
|
||||
if !isNamespaced {
|
||||
obj.SetNamespace("")
|
||||
} else if obj.GetNamespace() == "" {
|
||||
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)
|
||||
@@ -216,63 +241,77 @@ func DeduplicateTargetObjects(
|
||||
return result, conditions, nil
|
||||
}
|
||||
|
||||
// dedupLiveResources handles removes live resource duplicates with the same UID. Duplicates are created in a separate resource groups.
|
||||
// E.g. apps/Deployment produces duplicate in extensions/Deployment, authorization.openshift.io/ClusterRole produces duplicate in rbac.authorization.k8s.io/ClusterRole etc.
|
||||
// The method removes such duplicates unless it was defined in git ( exists in target resources list ). At least one duplicate stays.
|
||||
// If non of duplicates are in git at random one stays
|
||||
func dedupLiveResources(targetObjs []*unstructured.Unstructured, liveObjsByKey map[kubeutil.ResourceKey]*unstructured.Unstructured) {
|
||||
targetObjByKey := make(map[kubeutil.ResourceKey]*unstructured.Unstructured)
|
||||
for i := range targetObjs {
|
||||
targetObjByKey[kubeutil.GetResourceKey(targetObjs[i])] = targetObjs[i]
|
||||
}
|
||||
liveObjsById := make(map[types.UID][]*unstructured.Unstructured)
|
||||
for k := range liveObjsByKey {
|
||||
obj := liveObjsByKey[k]
|
||||
if obj != nil {
|
||||
liveObjsById[obj.GetUID()] = append(liveObjsById[obj.GetUID()], obj)
|
||||
}
|
||||
}
|
||||
for id := range liveObjsById {
|
||||
objs := liveObjsById[id]
|
||||
|
||||
if len(objs) > 1 {
|
||||
duplicatesLeft := len(objs)
|
||||
for i := range objs {
|
||||
obj := objs[i]
|
||||
resourceKey := kubeutil.GetResourceKey(obj)
|
||||
if _, ok := targetObjByKey[resourceKey]; !ok {
|
||||
delete(liveObjsByKey, resourceKey)
|
||||
duplicatesLeft--
|
||||
if duplicatesLeft == 1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *appStateManager) getComparisonSettings(app *appv1.Application) (string, map[string]v1alpha1.ResourceOverride, diff.Normalizer, error) {
|
||||
func (m *appStateManager) getComparisonSettings(app *appv1.Application) (string, map[string]v1alpha1.ResourceOverride, diff.Normalizer, *settings.ResourcesFilter, error) {
|
||||
resourceOverrides, err := m.settingsMgr.GetResourceOverrides()
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
return "", nil, nil, nil, err
|
||||
}
|
||||
appLabelKey, err := m.settingsMgr.GetAppInstanceLabelKey()
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
return "", nil, nil, nil, err
|
||||
}
|
||||
diffNormalizer, err := argo.NewDiffNormalizer(app.Spec.IgnoreDifferences, resourceOverrides)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
return "", nil, nil, nil, err
|
||||
}
|
||||
return appLabelKey, resourceOverrides, diffNormalizer, nil
|
||||
resFilter, err := m.settingsMgr.GetResourcesFilter()
|
||||
if err != nil {
|
||||
return "", nil, nil, nil, err
|
||||
}
|
||||
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 verificatin 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.
|
||||
func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *appv1.AppProject, revision string, source v1alpha1.ApplicationSource, noCache bool, localManifests []string) *comparisonResult {
|
||||
appLabelKey, resourceOverrides, diffNormalizer, err := m.getComparisonSettings(app)
|
||||
ts := stats.NewTimingStats()
|
||||
appLabelKey, resourceOverrides, diffNormalizer, resFilter, err := m.getComparisonSettings(app)
|
||||
ts.AddCheckpoint("settings_ms")
|
||||
|
||||
// return unknown comparison result if basic comparison settings cannot be loaded
|
||||
if err != nil {
|
||||
@@ -281,10 +320,16 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
|
||||
ComparedTo: appv1.ComparedTo{Source: source, Destination: app.Spec.Destination},
|
||||
Status: appv1.SyncStatusCodeUnknown,
|
||||
},
|
||||
healthStatus: &appv1.HealthStatus{Status: appv1.HealthStatusUnknown},
|
||||
healthStatus: &appv1.HealthStatus{Status: health.HealthStatusUnknown},
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -293,59 +338,68 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
|
||||
logCtx.Infof("Comparing app state (cluster: %s, namespace: %s)", app.Spec.Destination.Server, app.Spec.Destination.Namespace)
|
||||
|
||||
var targetObjs []*unstructured.Unstructured
|
||||
var hooks []*unstructured.Unstructured
|
||||
var manifestInfo *apiclient.ManifestResponse
|
||||
now := metav1.Now()
|
||||
|
||||
if len(localManifests) == 0 {
|
||||
targetObjs, hooks, manifestInfo, err = m.getRepoObjs(app, source, appLabelKey, revision, noCache)
|
||||
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, hooks, 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
|
||||
}
|
||||
ts.AddCheckpoint("git_ms")
|
||||
|
||||
targetObjs, dedupConditions, err := DeduplicateTargetObjects(app.Spec.Destination.Server, app.Spec.Destination.Namespace, targetObjs, m.liveStateCache)
|
||||
var infoProvider kubeutil.ResourceInfoProvider
|
||||
infoProvider, err = m.liveStateCache.GetClusterCache(app.Spec.Destination.Server)
|
||||
if err != nil {
|
||||
infoProvider = &resourceInfoProviderStub{}
|
||||
}
|
||||
targetObjs, dedupConditions, err := DeduplicateTargetObjects(app.Spec.Destination.Namespace, targetObjs, infoProvider)
|
||||
if err != nil {
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
|
||||
}
|
||||
conditions = append(conditions, dedupConditions...)
|
||||
|
||||
resFilter, err := m.settingsMgr.GetResourcesFilter()
|
||||
if err != nil {
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
|
||||
} else {
|
||||
for i := len(targetObjs) - 1; i >= 0; i-- {
|
||||
targetObj := targetObjs[i]
|
||||
gvk := targetObj.GroupVersionKind()
|
||||
if resFilter.IsExcludedResource(gvk.Group, gvk.Kind, app.Spec.Destination.Server) {
|
||||
targetObjs = append(targetObjs[:i], targetObjs[i+1:]...)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{
|
||||
Type: v1alpha1.ApplicationConditionExcludedResourceWarning,
|
||||
Message: fmt.Sprintf("Resource %s/%s %s is excluded in the settings", gvk.Group, gvk.Kind, targetObj.GetName()),
|
||||
LastTransitionTime: &now,
|
||||
})
|
||||
}
|
||||
for i := len(targetObjs) - 1; i >= 0; i-- {
|
||||
targetObj := targetObjs[i]
|
||||
gvk := targetObj.GroupVersionKind()
|
||||
if resFilter.IsExcludedResource(gvk.Group, gvk.Kind, app.Spec.Destination.Server) {
|
||||
targetObjs = append(targetObjs[:i], targetObjs[i+1:]...)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{
|
||||
Type: v1alpha1.ApplicationConditionExcludedResourceWarning,
|
||||
Message: fmt.Sprintf("Resource %s/%s %s is excluded in the settings", gvk.Group, gvk.Kind, targetObj.GetName()),
|
||||
LastTransitionTime: &now,
|
||||
})
|
||||
}
|
||||
}
|
||||
ts.AddCheckpoint("dedup_ms")
|
||||
|
||||
logCtx.Debugf("Generated config manifests")
|
||||
liveObjByKey, err := m.liveStateCache.GetManagedLiveObjs(app, targetObjs)
|
||||
if err != nil {
|
||||
liveObjByKey = make(map[kubeutil.ResourceKey]*unstructured.Unstructured)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
|
||||
failedToLoadObjs = true
|
||||
}
|
||||
dedupLiveResources(targetObjs, liveObjByKey)
|
||||
logCtx.Debugf("Retrieved lived manifests")
|
||||
|
||||
// filter out all resources which are not permitted in the application project
|
||||
for k, v := range liveObjByKey {
|
||||
if !project.IsLiveResourcePermitted(v, app.Spec.Destination.Server) {
|
||||
@@ -353,7 +407,6 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
|
||||
}
|
||||
}
|
||||
|
||||
logCtx.Debugf("Retrieved lived manifests")
|
||||
for _, liveObj := range liveObjByKey {
|
||||
if liveObj != nil {
|
||||
appInstanceName := kubeutil.GetAppInstanceLabel(liveObj, appLabelKey)
|
||||
@@ -367,43 +420,30 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
|
||||
}
|
||||
}
|
||||
|
||||
managedLiveObj := make([]*unstructured.Unstructured, len(targetObjs))
|
||||
for i, obj := range targetObjs {
|
||||
gvk := obj.GroupVersionKind()
|
||||
ns := util.FirstNonEmpty(obj.GetNamespace(), app.Spec.Destination.Namespace)
|
||||
if namespaced, err := m.liveStateCache.IsNamespaced(app.Spec.Destination.Server, obj.GroupVersionKind().GroupKind()); err == nil && !namespaced {
|
||||
ns = ""
|
||||
}
|
||||
key := kubeutil.NewResourceKey(gvk.Group, gvk.Kind, ns, obj.GetName())
|
||||
if liveObj, ok := liveObjByKey[key]; ok {
|
||||
managedLiveObj[i] = liveObj
|
||||
delete(liveObjByKey, key)
|
||||
} else {
|
||||
managedLiveObj[i] = nil
|
||||
}
|
||||
}
|
||||
logCtx.Debugf("built managed objects list")
|
||||
// Everything remaining in liveObjByKey are "extra" resources that aren't tracked in git.
|
||||
// The following adds all the extras to the managedLiveObj list and backfills the targetObj
|
||||
// list with nils, so that the lists are of equal lengths for comparison purposes.
|
||||
for _, obj := range liveObjByKey {
|
||||
targetObjs = append(targetObjs, nil)
|
||||
managedLiveObj = append(managedLiveObj, obj)
|
||||
reconciliation := sync.Reconcile(targetObjs, liveObjByKey, app.Spec.Destination.Namespace, infoProvider)
|
||||
ts.AddCheckpoint("live_ms")
|
||||
|
||||
compareOptions, err := m.settingsMgr.GetResourceCompareOptions()
|
||||
if err != nil {
|
||||
log.Warnf("Could not get compare options from ConfigMap (assuming defaults): %v", err)
|
||||
compareOptions = diff.GetDefaultDiffOptions()
|
||||
}
|
||||
|
||||
logCtx.Debugf("built managed objects list")
|
||||
// Do the actual comparison
|
||||
diffResults, err := diff.DiffArray(targetObjs, managedLiveObj, diffNormalizer)
|
||||
diffResults, err := diff.DiffArray(reconciliation.Target, reconciliation.Live, diffNormalizer, compareOptions)
|
||||
if err != nil {
|
||||
diffResults = &diff.DiffResultList{}
|
||||
failedToLoadObjs = true
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
|
||||
}
|
||||
ts.AddCheckpoint("diff_ms")
|
||||
|
||||
syncCode := v1alpha1.SyncStatusCodeSynced
|
||||
managedResources := make([]managedResource, len(targetObjs))
|
||||
resourceSummaries := make([]v1alpha1.ResourceStatus, len(targetObjs))
|
||||
for i, targetObj := range targetObjs {
|
||||
liveObj := managedLiveObj[i]
|
||||
managedResources := make([]managedResource, len(reconciliation.Target))
|
||||
resourceSummaries := make([]v1alpha1.ResourceStatus, len(reconciliation.Target))
|
||||
for i, targetObj := range reconciliation.Target {
|
||||
liveObj := reconciliation.Live[i]
|
||||
obj := liveObj
|
||||
if obj == nil {
|
||||
obj = targetObj
|
||||
@@ -423,9 +463,14 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
|
||||
RequiresPruning: targetObj == nil && liveObj != nil,
|
||||
}
|
||||
|
||||
diffResult := diffResults.Diffs[i]
|
||||
if resState.Hook || ignore.Ignore(obj) {
|
||||
// For resource hooks, don't store sync status, and do not affect overall sync status
|
||||
var diffResult diff.DiffResult
|
||||
if i < len(diffResults.Diffs) {
|
||||
diffResult = diffResults.Diffs[i]
|
||||
} else {
|
||||
diffResult = diff.DiffResult{Modified: false, NormalizedLive: []byte("{}"), PredictedLive: []byte("{}")}
|
||||
}
|
||||
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
|
||||
@@ -434,7 +479,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
|
||||
resState.Status = v1alpha1.SyncStatusCodeOutOfSync
|
||||
// we ignore the status if the obj needs pruning AND we have the annotation
|
||||
needsPruning := targetObj == nil && liveObj != nil
|
||||
if !(needsPruning && resource.HasAnnotationOption(obj, common.AnnotationCompareOptions, "IgnoreExtraneous")) {
|
||||
if !(needsPruning && resourceutil.HasAnnotationOption(obj, common.AnnotationCompareOptions, "IgnoreExtraneous")) {
|
||||
syncCode = v1alpha1.SyncStatusCodeOutOfSync
|
||||
}
|
||||
} else {
|
||||
@@ -446,6 +491,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
|
||||
@@ -477,8 +526,10 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
|
||||
if manifestInfo != nil {
|
||||
syncStatus.Revision = manifestInfo.Revision
|
||||
}
|
||||
ts.AddCheckpoint("sync_ms")
|
||||
|
||||
healthStatus, err := health.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))
|
||||
})
|
||||
|
||||
@@ -486,13 +537,20 @@ 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 oppinion 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,
|
||||
resources: resourceSummaries,
|
||||
managedResources: managedResources,
|
||||
hooks: hooks,
|
||||
diffNormalizer: diffNormalizer,
|
||||
syncStatus: &syncStatus,
|
||||
healthStatus: healthStatus,
|
||||
resources: resourceSummaries,
|
||||
managedResources: managedResources,
|
||||
reconciliationResult: reconciliation,
|
||||
diffNormalizer: diffNormalizer,
|
||||
}
|
||||
if manifestInfo != nil {
|
||||
compRes.appSourceType = v1alpha1.ApplicationSourceType(manifestInfo.SourceType)
|
||||
@@ -503,19 +561,22 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
|
||||
appv1.ApplicationConditionRepeatedResourceWarning: true,
|
||||
appv1.ApplicationConditionExcludedResourceWarning: true,
|
||||
})
|
||||
ts.AddCheckpoint("health_ms")
|
||||
compRes.timings = ts.Timings()
|
||||
return &compRes
|
||||
}
|
||||
|
||||
func (m *appStateManager) persistRevisionHistory(app *v1alpha1.Application, revision string, source v1alpha1.ApplicationSource) error {
|
||||
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())
|
||||
@@ -528,11 +589,11 @@ 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
|
||||
}
|
||||
|
||||
// NewAppStateManager creates new instance of Ksonnet app comparator
|
||||
// NewAppStateManager creates new instance of AppStateManager
|
||||
func NewAppStateManager(
|
||||
db db.ArgoDB,
|
||||
appclientset appclientset.Interface,
|
||||
|
||||
@@ -2,8 +2,15 @@ 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"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
. "github.com/argoproj/gitops-engine/pkg/utils/testing"
|
||||
"github.com/stretchr/testify/assert"
|
||||
v1 "k8s.io/api/apps/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -14,7 +21,6 @@ import (
|
||||
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/test"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
)
|
||||
|
||||
// TestCompareAppStateEmpty tests comparison when both git and live have no objects
|
||||
@@ -45,7 +51,7 @@ func TestCompareAppStateMissing(t *testing.T) {
|
||||
data := fakeData{
|
||||
apps: []runtime.Object{app},
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{test.PodManifest},
|
||||
Manifests: []string{PodManifest},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
@@ -64,7 +70,7 @@ func TestCompareAppStateMissing(t *testing.T) {
|
||||
|
||||
// TestCompareAppStateExtra tests when there is an extra object in live but not defined in git
|
||||
func TestCompareAppStateExtra(t *testing.T) {
|
||||
pod := test.NewPod()
|
||||
pod := NewPod()
|
||||
pod.SetNamespace(test.FakeDestNamespace)
|
||||
app := newFakeApp()
|
||||
key := kube.ResourceKey{Group: "", Kind: "Pod", Namespace: test.FakeDestNamespace, Name: app.Name}
|
||||
@@ -91,8 +97,8 @@ func TestCompareAppStateExtra(t *testing.T) {
|
||||
// TestCompareAppStateHook checks that hooks are detected during manifest generation, and not
|
||||
// considered as part of resources when assessing Synced status
|
||||
func TestCompareAppStateHook(t *testing.T) {
|
||||
pod := test.NewPod()
|
||||
pod.SetAnnotations(map[string]string{common.AnnotationKeyHook: "PreSync"})
|
||||
pod := NewPod()
|
||||
pod.SetAnnotations(map[string]string{synccommon.AnnotationKeyHook: "PreSync"})
|
||||
podBytes, _ := json.Marshal(pod)
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
@@ -111,13 +117,40 @@ func TestCompareAppStateHook(t *testing.T) {
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Equal(t, 0, len(compRes.resources))
|
||||
assert.Equal(t, 0, len(compRes.managedResources))
|
||||
assert.Equal(t, 1, len(compRes.hooks))
|
||||
assert.Equal(t, 1, len(compRes.reconciliationResult.Hooks))
|
||||
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 := test.NewPod()
|
||||
pod := NewPod()
|
||||
pod.SetAnnotations(map[string]string{common.AnnotationCompareOptions: "IgnoreExtraneous"})
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
@@ -143,8 +176,8 @@ func TestCompareAppStateCompareOptionIgnoreExtraneous(t *testing.T) {
|
||||
|
||||
// TestCompareAppStateExtraHook tests when there is an extra _hook_ object in live but not defined in git
|
||||
func TestCompareAppStateExtraHook(t *testing.T) {
|
||||
pod := test.NewPod()
|
||||
pod.SetAnnotations(map[string]string{common.AnnotationKeyHook: "PreSync"})
|
||||
pod := NewPod()
|
||||
pod.SetAnnotations(map[string]string{synccommon.AnnotationKeyHook: "PreSync"})
|
||||
pod.SetNamespace(test.FakeDestNamespace)
|
||||
app := newFakeApp()
|
||||
key := kube.ResourceKey{Group: "", Kind: "Pod", Namespace: test.FakeDestNamespace, Name: app.Name}
|
||||
@@ -166,7 +199,7 @@ func TestCompareAppStateExtraHook(t *testing.T) {
|
||||
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.hooks))
|
||||
assert.Equal(t, 0, len(compRes.reconciliationResult.Hooks))
|
||||
assert.Equal(t, 0, len(app.Status.Conditions))
|
||||
}
|
||||
|
||||
@@ -177,16 +210,22 @@ func toJSON(t *testing.T, obj *unstructured.Unstructured) string {
|
||||
}
|
||||
|
||||
func TestCompareAppStateDuplicatedNamespacedResources(t *testing.T) {
|
||||
obj1 := test.NewPod()
|
||||
obj1 := NewPod()
|
||||
obj1.SetNamespace(test.FakeDestNamespace)
|
||||
obj2 := test.NewPod()
|
||||
obj3 := test.NewPod()
|
||||
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",
|
||||
@@ -204,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{
|
||||
@@ -250,7 +289,7 @@ func TestSetHealth(t *testing.T) {
|
||||
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
|
||||
assert.Equal(t, compRes.healthStatus.Status, argoappv1.HealthStatusHealthy)
|
||||
assert.Equal(t, compRes.healthStatus.Status, health.HealthStatusHealthy)
|
||||
}
|
||||
|
||||
func TestSetHealthSelfReferencedApp(t *testing.T) {
|
||||
@@ -282,7 +321,7 @@ func TestSetHealthSelfReferencedApp(t *testing.T) {
|
||||
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
|
||||
assert.Equal(t, compRes.healthStatus.Status, argoappv1.HealthStatusHealthy)
|
||||
assert.Equal(t, compRes.healthStatus.Status, health.HealthStatusHealthy)
|
||||
}
|
||||
|
||||
func TestSetManagedResourcesWithOrphanedResources(t *testing.T) {
|
||||
@@ -352,7 +391,7 @@ func TestReturnUnknownComparisonStateOnSettingLoadError(t *testing.T) {
|
||||
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
|
||||
assert.Equal(t, argoappv1.HealthStatusUnknown, compRes.healthStatus.Status)
|
||||
assert.Equal(t, health.HealthStatusUnknown, compRes.healthStatus.Status)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeUnknown, compRes.syncStatus.Status)
|
||||
}
|
||||
|
||||
@@ -385,13 +424,6 @@ func TestSetManagedResourcesKnownOrphanedResourceExceptions(t *testing.T) {
|
||||
assert.Equal(t, "guestbook", tree.OrphanedNodes[0].Name)
|
||||
}
|
||||
|
||||
func Test_comparisonResult_obs(t *testing.T) {
|
||||
assert.Len(t, (&comparisonResult{}).targetObjs(), 0)
|
||||
assert.Len(t, (&comparisonResult{managedResources: []managedResource{{}}}).targetObjs(), 0)
|
||||
assert.Len(t, (&comparisonResult{managedResources: []managedResource{{Target: test.NewPod()}}}).targetObjs(), 1)
|
||||
assert.Len(t, (&comparisonResult{hooks: []*unstructured.Unstructured{{}}}).targetObjs(), 1)
|
||||
}
|
||||
|
||||
func Test_appStateManager_persistRevisionHistory(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{
|
||||
@@ -403,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()
|
||||
@@ -437,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,65 +3,27 @@ package controller
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/sync"
|
||||
"github.com/argoproj/gitops-engine/pkg/sync/common"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
apierr "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/rest"
|
||||
|
||||
"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"
|
||||
"github.com/argoproj/argo-cd/util/health"
|
||||
"github.com/argoproj/argo-cd/util/hook"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/lua"
|
||||
"github.com/argoproj/argo-cd/util/rand"
|
||||
"github.com/argoproj/argo-cd/util/resource"
|
||||
)
|
||||
|
||||
const (
|
||||
crdReadinessTimeout = time.Duration(3) * time.Second
|
||||
)
|
||||
|
||||
var syncIdPrefix uint64 = 0
|
||||
|
||||
type syncContext struct {
|
||||
resourceOverrides map[string]v1alpha1.ResourceOverride
|
||||
appName string
|
||||
proj *v1alpha1.AppProject
|
||||
compareResult *comparisonResult
|
||||
config *rest.Config
|
||||
dynamicIf dynamic.Interface
|
||||
disco discovery.DiscoveryInterface
|
||||
extensionsclientset *clientset.Clientset
|
||||
kubectl kube.Kubectl
|
||||
namespace string
|
||||
server string
|
||||
syncOp *v1alpha1.SyncOperation
|
||||
syncRes *v1alpha1.SyncOperationResult
|
||||
syncResources []v1alpha1.SyncOperationResource
|
||||
opState *v1alpha1.OperationState
|
||||
log *log.Entry
|
||||
// lock to protect concurrent updates of the result list
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
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
|
||||
@@ -71,11 +33,10 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
|
||||
var revision string
|
||||
var syncOp v1alpha1.SyncOperation
|
||||
var syncRes *v1alpha1.SyncOperationResult
|
||||
var syncResources []v1alpha1.SyncOperationResource
|
||||
var source v1alpha1.ApplicationSource
|
||||
|
||||
if state.Operation.Sync == nil {
|
||||
state.Phase = v1alpha1.OperationFailed
|
||||
state.Phase = common.OperationFailed
|
||||
state.Message = "Invalid operation request: no operation specified"
|
||||
return
|
||||
}
|
||||
@@ -87,7 +48,7 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
|
||||
// rollback case
|
||||
source = *state.Operation.Sync.Source
|
||||
}
|
||||
syncResources = syncOp.Resources
|
||||
|
||||
if state.SyncResult != nil {
|
||||
syncRes = state.SyncResult
|
||||
revision = state.SyncResult.Revision
|
||||
@@ -109,717 +70,118 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
|
||||
|
||||
proj, err := argo.GetAppProject(&app.Spec, listersv1alpha1.NewAppProjectLister(m.projInformer.GetIndexer()), m.namespace)
|
||||
if err != nil {
|
||||
state.Phase = v1alpha1.OperationError
|
||||
state.Phase = common.OperationError
|
||||
state.Message = fmt.Sprintf("Failed to load application project: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
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{
|
||||
v1alpha1.ApplicationConditionComparisonError: true,
|
||||
v1alpha1.ApplicationConditionInvalidSpecError: true,
|
||||
}); len(errConditions) > 0 {
|
||||
state.Phase = v1alpha1.OperationError
|
||||
state.Phase = common.OperationError
|
||||
state.Message = argo.FormatAppConditions(errConditions)
|
||||
return
|
||||
}
|
||||
|
||||
// We now have a concrete commit SHA. Save this in the sync result revision so that we remember
|
||||
// what we should be syncing to when resuming operations.
|
||||
syncRes.Revision = compareResult.syncStatus.Revision
|
||||
|
||||
clst, err := m.db.GetCluster(context.Background(), app.Spec.Destination.Server)
|
||||
if err != nil {
|
||||
state.Phase = v1alpha1.OperationError
|
||||
state.Phase = common.OperationError
|
||||
state.Message = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
rawConfig := clst.RawRestConfig()
|
||||
restConfig := metrics.AddMetricsTransportWrapper(m.metricsServer, app, clst.RESTConfig())
|
||||
dynamicIf, err := dynamic.NewForConfig(restConfig)
|
||||
if err != nil {
|
||||
state.Phase = v1alpha1.OperationError
|
||||
state.Message = fmt.Sprintf("Failed to initialize dynamic client: %v", err)
|
||||
return
|
||||
}
|
||||
disco, err := discovery.NewDiscoveryClientForConfig(restConfig)
|
||||
if err != nil {
|
||||
state.Phase = v1alpha1.OperationError
|
||||
state.Message = fmt.Sprintf("Failed to initialize discovery client: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
extensionsclientset, err := clientset.NewForConfig(restConfig)
|
||||
if err != nil {
|
||||
state.Phase = v1alpha1.OperationError
|
||||
state.Message = fmt.Sprintf("Failed to initialize extensions client: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
resourceOverrides, err := m.settingsMgr.GetResourceOverrides()
|
||||
if err != nil {
|
||||
state.Phase = v1alpha1.OperationError
|
||||
state.Phase = common.OperationError
|
||||
state.Message = fmt.Sprintf("Failed to load resource overrides: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
atomic.AddUint64(&syncIdPrefix, 1)
|
||||
syncId := fmt.Sprintf("%05d-%s", syncIdPrefix, rand.RandString(5))
|
||||
syncCtx := syncContext{
|
||||
resourceOverrides: resourceOverrides,
|
||||
appName: app.Name,
|
||||
proj: proj,
|
||||
compareResult: compareResult,
|
||||
config: restConfig,
|
||||
dynamicIf: dynamicIf,
|
||||
disco: disco,
|
||||
extensionsclientset: extensionsclientset,
|
||||
kubectl: m.kubectl,
|
||||
namespace: app.Spec.Destination.Namespace,
|
||||
server: app.Spec.Destination.Server,
|
||||
syncOp: &syncOp,
|
||||
syncRes: syncRes,
|
||||
syncResources: syncResources,
|
||||
opState: state,
|
||||
log: log.WithFields(log.Fields{"application": app.Name, "syncId": syncId}),
|
||||
|
||||
logEntry := log.WithFields(log.Fields{"application": app.Name, "syncId": syncId})
|
||||
initialResourcesRes := make([]common.ResourceSyncResult, 0)
|
||||
for i, res := range syncRes.Resources {
|
||||
key := kube.ResourceKey{Group: res.Group, Kind: res.Kind, Namespace: res.Namespace, Name: res.Name}
|
||||
initialResourcesRes = append(initialResourcesRes, common.ResourceSyncResult{
|
||||
ResourceKey: key,
|
||||
Message: res.Message,
|
||||
Status: res.Status,
|
||||
HookPhase: res.HookPhase,
|
||||
HookType: res.HookType,
|
||||
SyncPhase: res.SyncPhase,
|
||||
Version: res.Version,
|
||||
Order: i + 1,
|
||||
})
|
||||
}
|
||||
syncCtx, err := sync.NewSyncContext(compareResult.syncStatus.Revision, compareResult.reconciliationResult, restConfig, rawConfig, m.kubectl, app.Spec.Destination.Namespace, logEntry,
|
||||
sync.WithHealthOverride(lua.ResourceHealthOverrides(resourceOverrides)),
|
||||
sync.WithPermissionValidator(func(un *unstructured.Unstructured, res *v1.APIResource) error {
|
||||
if !proj.IsGroupKindPermitted(un.GroupVersionKind().GroupKind(), res.Namespaced) {
|
||||
return fmt.Errorf("Resource %s:%s is not permitted in project %s.", un.GroupVersionKind().Group, un.GroupVersionKind().Kind, proj.Name)
|
||||
}
|
||||
if res.Namespaced && !proj.IsDestinationPermitted(v1alpha1.ApplicationDestination{Namespace: un.GetNamespace(), Server: app.Spec.Destination.Server}) {
|
||||
return fmt.Errorf("namespace %v is not permitted in project '%s'", un.GetNamespace(), proj.Name)
|
||||
}
|
||||
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.WithResourcesFilter(func(key kube.ResourceKey, target *unstructured.Unstructured, live *unstructured.Unstructured) bool {
|
||||
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")),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
state.Phase = common.OperationError
|
||||
state.Message = fmt.Sprintf("failed to record sync to history: %v", err)
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
if state.Phase == v1alpha1.OperationTerminating {
|
||||
syncCtx.terminate()
|
||||
if state.Phase == common.OperationTerminating {
|
||||
syncCtx.Terminate()
|
||||
} else {
|
||||
syncCtx.sync()
|
||||
syncCtx.Sync()
|
||||
}
|
||||
var resState []common.ResourceSyncResult
|
||||
state.Phase, state.Message, resState = syncCtx.GetState()
|
||||
state.SyncResult.Resources = nil
|
||||
for _, res := range resState {
|
||||
state.SyncResult.Resources = append(state.SyncResult.Resources, &v1alpha1.ResourceResult{
|
||||
HookType: res.HookType,
|
||||
Group: res.ResourceKey.Group,
|
||||
Kind: res.ResourceKey.Kind,
|
||||
Namespace: res.ResourceKey.Namespace,
|
||||
Name: res.ResourceKey.Name,
|
||||
Version: res.Version,
|
||||
SyncPhase: res.SyncPhase,
|
||||
HookPhase: res.HookPhase,
|
||||
Status: res.Status,
|
||||
Message: res.Message,
|
||||
})
|
||||
}
|
||||
|
||||
syncCtx.log.WithField("duration", time.Since(start)).Info("sync/terminate complete")
|
||||
logEntry.WithField("duration", time.Since(start)).Info("sync/terminate complete")
|
||||
|
||||
if !syncOp.DryRun && !syncCtx.isSelectiveSync() && syncCtx.opState.Phase.Successful() {
|
||||
err := m.persistRevisionHistory(app, compareResult.syncStatus.Revision, source)
|
||||
if !syncOp.DryRun && len(syncOp.Resources) == 0 && state.Phase.Successful() {
|
||||
err := m.persistRevisionHistory(app, compareResult.syncStatus.Revision, source, state.StartedAt)
|
||||
if err != nil {
|
||||
syncCtx.setOperationPhase(v1alpha1.OperationError, fmt.Sprintf("failed to record sync to history: %v", err))
|
||||
state.Phase = common.OperationError
|
||||
state.Message = fmt.Sprintf("failed to record sync to history: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sync has performs the actual apply or hook based sync
|
||||
func (sc *syncContext) sync() {
|
||||
sc.log.WithFields(log.Fields{"isSelectiveSync": sc.isSelectiveSync(), "skipHooks": sc.skipHooks(), "started": sc.started()}).Info("syncing")
|
||||
tasks, ok := sc.getSyncTasks()
|
||||
if !ok {
|
||||
sc.setOperationPhase(v1alpha1.OperationFailed, "one or more synchronization tasks are not valid")
|
||||
return
|
||||
}
|
||||
|
||||
sc.log.WithFields(log.Fields{"tasks": tasks, "isSelectiveSync": sc.isSelectiveSync()}).Info("tasks")
|
||||
|
||||
// Perform a `kubectl apply --dry-run` against all the manifests. This will detect most (but
|
||||
// not all) validation issues with the user's manifests (e.g. will detect syntax issues, but
|
||||
// will not not detect if they are mutating immutable fields). If anything fails, we will refuse
|
||||
// to perform the sync. we only wish to do this once per operation, performing additional dry-runs
|
||||
// is harmless, but redundant. The indicator we use to detect if we have already performed
|
||||
// the dry-run for this operation, is if the resource or hook list is empty.
|
||||
if !sc.started() {
|
||||
sc.log.Debug("dry-run")
|
||||
if sc.runTasks(tasks, true) == failed {
|
||||
sc.setOperationPhase(v1alpha1.OperationFailed, "one or more objects failed to apply (dry run)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// update status of any tasks that are running, note that this must exclude pruning tasks
|
||||
for _, task := range tasks.Filter(func(t *syncTask) bool {
|
||||
// just occasionally, you can be running yet not have a live resource
|
||||
return t.running() && t.liveObj != nil
|
||||
}) {
|
||||
if task.isHook() {
|
||||
// update the hook's result
|
||||
operationState, message, err := sc.getOperationPhase(task.liveObj)
|
||||
if err != nil {
|
||||
sc.setResourceResult(task, "", v1alpha1.OperationError, fmt.Sprintf("failed to get resource health: %v", err))
|
||||
} else {
|
||||
sc.setResourceResult(task, "", operationState, message)
|
||||
|
||||
// maybe delete the hook
|
||||
if task.needsDeleting() {
|
||||
err := sc.deleteResource(task)
|
||||
if err != nil && !errors.IsNotFound(err) {
|
||||
sc.setResourceResult(task, "", v1alpha1.OperationError, fmt.Sprintf("failed to delete resource: %v", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// this must be calculated on the live object
|
||||
healthStatus, err := health.GetResourceHealth(task.liveObj, sc.resourceOverrides)
|
||||
if err == nil {
|
||||
log.WithFields(log.Fields{"task": task, "healthStatus": healthStatus}).Debug("attempting to update health of running task")
|
||||
if healthStatus == nil {
|
||||
// some objects (e.g. secret) do not have health, and they automatically success
|
||||
sc.setResourceResult(task, task.syncStatus, v1alpha1.OperationSucceeded, task.message)
|
||||
} else {
|
||||
switch healthStatus.Status {
|
||||
case v1alpha1.HealthStatusHealthy:
|
||||
sc.setResourceResult(task, task.syncStatus, v1alpha1.OperationSucceeded, healthStatus.Message)
|
||||
case v1alpha1.HealthStatusDegraded:
|
||||
sc.setResourceResult(task, task.syncStatus, v1alpha1.OperationFailed, healthStatus.Message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if (a) we are multi-step and we have any running tasks,
|
||||
// or (b) there are any running hooks,
|
||||
// then wait...
|
||||
multiStep := tasks.multiStep()
|
||||
if tasks.Any(func(t *syncTask) bool { return (multiStep || t.isHook()) && t.running() }) {
|
||||
sc.setOperationPhase(v1alpha1.OperationRunning, "one or more tasks are running")
|
||||
return
|
||||
}
|
||||
|
||||
// syncFailTasks only run during failure, so separate them from regular tasks
|
||||
syncFailTasks, tasks := tasks.Split(func(t *syncTask) bool { return t.phase == v1alpha1.SyncPhaseSyncFail })
|
||||
|
||||
// if there are any completed but unsuccessful tasks, sync is a failure.
|
||||
if tasks.Any(func(t *syncTask) bool { return t.completed() && !t.successful() }) {
|
||||
sc.setOperationFailed(syncFailTasks, "one or more synchronization tasks completed unsuccessfully")
|
||||
return
|
||||
}
|
||||
|
||||
sc.log.WithFields(log.Fields{"tasks": tasks}).Debug("filtering out non-pending tasks")
|
||||
// remove tasks that are completed, we can assume that there are no running tasks
|
||||
tasks = tasks.Filter(func(t *syncTask) bool { return t.pending() })
|
||||
|
||||
// If no sync tasks were generated (e.g., in case all application manifests have been removed),
|
||||
// the sync operation is successful.
|
||||
if len(tasks) == 0 {
|
||||
sc.setOperationPhase(v1alpha1.OperationSucceeded, "successfully synced (no more tasks)")
|
||||
return
|
||||
}
|
||||
|
||||
// remove any tasks not in this wave
|
||||
phase := tasks.phase()
|
||||
wave := tasks.wave()
|
||||
|
||||
// if it is the last phase/wave and the only remaining tasks are non-hooks, the we are successful
|
||||
// EVEN if those objects subsequently degraded
|
||||
// This handles the common case where neither hooks or waves are used and a sync equates to simply an (asynchronous) kubectl apply of manifests, which succeeds immediately.
|
||||
complete := !tasks.Any(func(t *syncTask) bool { return t.phase != phase || wave != t.wave() || t.isHook() })
|
||||
|
||||
sc.log.WithFields(log.Fields{"phase": phase, "wave": wave, "tasks": tasks, "syncFailTasks": syncFailTasks}).Debug("filtering tasks in correct phase and wave")
|
||||
tasks = tasks.Filter(func(t *syncTask) bool { return t.phase == phase && t.wave() == wave })
|
||||
|
||||
sc.setOperationPhase(v1alpha1.OperationRunning, "one or more tasks are running")
|
||||
|
||||
sc.log.WithFields(log.Fields{"tasks": tasks}).Debug("wet-run")
|
||||
runState := sc.runTasks(tasks, false)
|
||||
switch runState {
|
||||
case failed:
|
||||
sc.setOperationFailed(syncFailTasks, "one or more objects failed to apply")
|
||||
case successful:
|
||||
if complete {
|
||||
sc.setOperationPhase(v1alpha1.OperationSucceeded, "successfully synced (all tasks run)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *syncContext) setOperationFailed(syncFailTasks syncTasks, message string) {
|
||||
if len(syncFailTasks) > 0 {
|
||||
// if all the failure hooks are completed, don't run them again, and mark the sync as failed
|
||||
if syncFailTasks.All(func(task *syncTask) bool { return task.completed() }) {
|
||||
sc.setOperationPhase(v1alpha1.OperationFailed, message)
|
||||
return
|
||||
}
|
||||
// otherwise, we need to start the failure hooks, and then return without setting
|
||||
// the phase, so we make sure we have at least one more sync
|
||||
sc.log.WithFields(log.Fields{"syncFailTasks": syncFailTasks}).Debug("running sync fail tasks")
|
||||
if sc.runTasks(syncFailTasks, false) == failed {
|
||||
sc.setOperationPhase(v1alpha1.OperationFailed, message)
|
||||
}
|
||||
} else {
|
||||
sc.setOperationPhase(v1alpha1.OperationFailed, message)
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *syncContext) started() bool {
|
||||
return len(sc.syncRes.Resources) > 0
|
||||
}
|
||||
|
||||
func (sc *syncContext) isSelectiveSync() bool {
|
||||
// we've selected no resources
|
||||
if sc.syncResources == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// map both lists into string
|
||||
var a []string
|
||||
for _, r := range sc.compareResult.resources {
|
||||
if !r.Hook {
|
||||
a = append(a, fmt.Sprintf("%s:%s:%s", r.Group, r.Kind, r.Name))
|
||||
}
|
||||
}
|
||||
sort.Strings(a)
|
||||
|
||||
var b []string
|
||||
for _, r := range sc.syncResources {
|
||||
b = append(b, fmt.Sprintf("%s:%s:%s", r.Group, r.Kind, r.Name))
|
||||
}
|
||||
sort.Strings(b)
|
||||
|
||||
return !reflect.DeepEqual(a, b)
|
||||
}
|
||||
|
||||
// this essentially enforces the old "apply" behaviour
|
||||
func (sc *syncContext) skipHooks() bool {
|
||||
// All objects passed a `kubectl apply --dry-run`, so we are now ready to actually perform the sync.
|
||||
// default sync strategy to hook if no strategy
|
||||
return sc.syncOp.IsApplyStrategy() || sc.isSelectiveSync()
|
||||
}
|
||||
|
||||
func (sc *syncContext) containsResource(resourceState managedResource) bool {
|
||||
return !sc.isSelectiveSync() ||
|
||||
(resourceState.Live != nil && argo.ContainsSyncResource(resourceState.Live.GetName(), resourceState.Live.GroupVersionKind(), sc.syncResources)) ||
|
||||
(resourceState.Target != nil && argo.ContainsSyncResource(resourceState.Target.GetName(), resourceState.Target.GroupVersionKind(), sc.syncResources))
|
||||
}
|
||||
|
||||
// generates the list of sync tasks we will be performing during this sync.
|
||||
func (sc *syncContext) getSyncTasks() (_ syncTasks, successful bool) {
|
||||
resourceTasks := syncTasks{}
|
||||
successful = true
|
||||
|
||||
for _, resource := range sc.compareResult.managedResources {
|
||||
if !sc.containsResource(resource) {
|
||||
sc.log.WithFields(log.Fields{"group": resource.Group, "kind": resource.Kind, "name": resource.Name}).
|
||||
Debug("skipping")
|
||||
continue
|
||||
}
|
||||
|
||||
obj := obj(resource.Target, resource.Live)
|
||||
|
||||
// this creates garbage tasks
|
||||
if hook.IsHook(obj) {
|
||||
sc.log.WithFields(log.Fields{"group": obj.GroupVersionKind().Group, "kind": obj.GetKind(), "namespace": obj.GetNamespace(), "name": obj.GetName()}).
|
||||
Debug("skipping hook")
|
||||
continue
|
||||
}
|
||||
|
||||
for _, phase := range syncPhases(obj) {
|
||||
resourceTasks = append(resourceTasks, &syncTask{phase: phase, targetObj: resource.Target, liveObj: resource.Live})
|
||||
}
|
||||
}
|
||||
|
||||
sc.log.WithFields(log.Fields{"resourceTasks": resourceTasks}).Debug("tasks from managed resources")
|
||||
|
||||
hookTasks := syncTasks{}
|
||||
if !sc.skipHooks() {
|
||||
for _, obj := range sc.compareResult.hooks {
|
||||
for _, phase := range syncPhases(obj) {
|
||||
// Hook resources names are deterministic, whether they are defined by the user (metadata.name),
|
||||
// or formulated at the time of the operation (metadata.generateName). If user specifies
|
||||
// metadata.generateName, then we will generate a formulated metadata.name before submission.
|
||||
targetObj := obj.DeepCopy()
|
||||
if targetObj.GetName() == "" {
|
||||
postfix := strings.ToLower(fmt.Sprintf("%s-%s-%d", sc.syncRes.Revision[0:7], phase, sc.opState.StartedAt.UTC().Unix()))
|
||||
generateName := obj.GetGenerateName()
|
||||
targetObj.SetName(fmt.Sprintf("%s%s", generateName, postfix))
|
||||
}
|
||||
|
||||
hookTasks = append(hookTasks, &syncTask{phase: phase, targetObj: targetObj})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sc.log.WithFields(log.Fields{"hookTasks": hookTasks}).Debug("tasks from hooks")
|
||||
|
||||
tasks := resourceTasks
|
||||
tasks = append(tasks, hookTasks...)
|
||||
|
||||
// enrich target objects with the namespace
|
||||
for _, task := range tasks {
|
||||
if task.targetObj == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if task.targetObj.GetNamespace() == "" {
|
||||
// If target object's namespace is empty, we set namespace in the object. We do
|
||||
// this even though it might be a cluster-scoped resource. This prevents any
|
||||
// possibility of the resource from unintentionally becoming created in the
|
||||
// namespace during the `kubectl apply`
|
||||
task.targetObj = task.targetObj.DeepCopy()
|
||||
task.targetObj.SetNamespace(sc.namespace)
|
||||
}
|
||||
}
|
||||
|
||||
// enrich task with live obj
|
||||
for _, task := range tasks {
|
||||
if task.targetObj == nil || task.liveObj != nil {
|
||||
continue
|
||||
}
|
||||
task.liveObj = sc.liveObj(task.targetObj)
|
||||
}
|
||||
|
||||
// enrich tasks with the result
|
||||
for _, task := range tasks {
|
||||
_, result := sc.syncRes.Resources.Find(task.group(), task.kind(), task.namespace(), task.name(), task.phase)
|
||||
if result != nil {
|
||||
task.syncStatus = result.Status
|
||||
task.operationState = result.HookPhase
|
||||
task.message = result.Message
|
||||
}
|
||||
}
|
||||
|
||||
// check permissions
|
||||
for _, task := range tasks {
|
||||
serverRes, err := kube.ServerResourceForGroupVersionKind(sc.disco, task.groupVersionKind())
|
||||
if err != nil {
|
||||
// Special case for custom resources: if CRD is not yet known by the K8s API server,
|
||||
// skip verification during `kubectl apply --dry-run` since we expect the CRD
|
||||
// to be created during app synchronization.
|
||||
if apierr.IsNotFound(err) && sc.hasCRDOfGroupKind(task.group(), task.kind()) {
|
||||
sc.log.WithFields(log.Fields{"task": task}).Debug("skip dry-run for custom resource")
|
||||
task.skipDryRun = true
|
||||
} else {
|
||||
sc.setResourceResult(task, v1alpha1.ResultCodeSyncFailed, "", err.Error())
|
||||
successful = false
|
||||
}
|
||||
} else {
|
||||
if !sc.proj.IsGroupKindPermitted(schema.GroupKind{Group: task.group(), Kind: task.kind()}, serverRes.Namespaced) {
|
||||
sc.setResourceResult(task, v1alpha1.ResultCodeSyncFailed, "", fmt.Sprintf("Resource %s:%s is not permitted in project %s.", task.group(), task.kind(), sc.proj.Name))
|
||||
successful = false
|
||||
}
|
||||
if serverRes.Namespaced && !sc.proj.IsDestinationPermitted(v1alpha1.ApplicationDestination{Namespace: task.namespace(), Server: sc.server}) {
|
||||
sc.setResourceResult(task, v1alpha1.ResultCodeSyncFailed, "", fmt.Sprintf("namespace %v is not permitted in project '%s'", task.namespace(), sc.proj.Name))
|
||||
successful = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(tasks)
|
||||
|
||||
return tasks, successful
|
||||
}
|
||||
|
||||
func obj(a, b *unstructured.Unstructured) *unstructured.Unstructured {
|
||||
if a != nil {
|
||||
return a
|
||||
} else {
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *syncContext) liveObj(obj *unstructured.Unstructured) *unstructured.Unstructured {
|
||||
for _, resource := range sc.compareResult.managedResources {
|
||||
if resource.Group == obj.GroupVersionKind().Group &&
|
||||
resource.Kind == obj.GetKind() &&
|
||||
// cluster scoped objects will not have a namespace, even if the user has defined it
|
||||
(resource.Namespace == "" || resource.Namespace == obj.GetNamespace()) &&
|
||||
resource.Name == obj.GetName() {
|
||||
return resource.Live
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sc *syncContext) setOperationPhase(phase v1alpha1.OperationPhase, message string) {
|
||||
if sc.opState.Phase != phase || sc.opState.Message != message {
|
||||
sc.log.Infof("Updating operation state. phase: %s -> %s, message: '%s' -> '%s'", sc.opState.Phase, phase, sc.opState.Message, message)
|
||||
}
|
||||
sc.opState.Phase = phase
|
||||
sc.opState.Message = message
|
||||
}
|
||||
|
||||
// ensureCRDReady waits until specified CRD is ready (established condition is true). Method is best effort - it does not fail even if CRD is not ready without timeout.
|
||||
func (sc *syncContext) ensureCRDReady(name string) {
|
||||
_ = wait.PollImmediate(time.Duration(100)*time.Millisecond, crdReadinessTimeout, func() (bool, error) {
|
||||
crd, err := sc.extensionsclientset.ApiextensionsV1beta1().CustomResourceDefinitions().Get(name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, condition := range crd.Status.Conditions {
|
||||
if condition.Type == v1beta1.Established {
|
||||
return condition.Status == v1beta1.ConditionTrue, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
}
|
||||
|
||||
// applyObject performs a `kubectl apply` of a single resource
|
||||
func (sc *syncContext) applyObject(targetObj *unstructured.Unstructured, dryRun bool, force bool) (v1alpha1.ResultCode, string) {
|
||||
validate := !resource.HasAnnotationOption(targetObj, common.AnnotationSyncOptions, "Validate=false")
|
||||
message, err := sc.kubectl.ApplyResource(sc.config, targetObj, targetObj.GetNamespace(), dryRun, force, validate)
|
||||
if err != nil {
|
||||
return v1alpha1.ResultCodeSyncFailed, err.Error()
|
||||
}
|
||||
if kube.IsCRD(targetObj) && !dryRun {
|
||||
sc.ensureCRDReady(targetObj.GetName())
|
||||
}
|
||||
return v1alpha1.ResultCodeSynced, message
|
||||
}
|
||||
|
||||
// pruneObject deletes the object if both prune is true and dryRun is false. Otherwise appropriate message
|
||||
func (sc *syncContext) pruneObject(liveObj *unstructured.Unstructured, prune, dryRun bool) (v1alpha1.ResultCode, string) {
|
||||
if !prune {
|
||||
return v1alpha1.ResultCodePruneSkipped, "ignored (requires pruning)"
|
||||
} else if resource.HasAnnotationOption(liveObj, common.AnnotationSyncOptions, "Prune=false") {
|
||||
return v1alpha1.ResultCodePruneSkipped, "ignored (no prune)"
|
||||
} else {
|
||||
if dryRun {
|
||||
return v1alpha1.ResultCodePruned, "pruned (dry run)"
|
||||
} else {
|
||||
// Skip deletion if object is already marked for deletion, so we don't cause a resource update hotloop
|
||||
deletionTimestamp := liveObj.GetDeletionTimestamp()
|
||||
if deletionTimestamp == nil || deletionTimestamp.IsZero() {
|
||||
err := sc.kubectl.DeleteResource(sc.config, liveObj.GroupVersionKind(), liveObj.GetName(), liveObj.GetNamespace(), false)
|
||||
if err != nil {
|
||||
return v1alpha1.ResultCodeSyncFailed, err.Error()
|
||||
}
|
||||
}
|
||||
return v1alpha1.ResultCodePruned, "pruned"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *syncContext) hasCRDOfGroupKind(group string, kind string) bool {
|
||||
for _, obj := range sc.compareResult.targetObjs() {
|
||||
if kube.IsCRD(obj) {
|
||||
crdGroup, ok, err := unstructured.NestedString(obj.Object, "spec", "group")
|
||||
if err != nil || !ok {
|
||||
continue
|
||||
}
|
||||
crdKind, ok, err := unstructured.NestedString(obj.Object, "spec", "names", "kind")
|
||||
if err != nil || !ok {
|
||||
continue
|
||||
}
|
||||
if group == crdGroup && crdKind == kind {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// terminate looks for any running jobs/workflow hooks and deletes the resource
|
||||
func (sc *syncContext) terminate() {
|
||||
terminateSuccessful := true
|
||||
sc.log.Debug("terminating")
|
||||
tasks, _ := sc.getSyncTasks()
|
||||
for _, task := range tasks {
|
||||
if !task.isHook() || task.liveObj == nil {
|
||||
continue
|
||||
}
|
||||
phase, msg, err := sc.getOperationPhase(task.liveObj)
|
||||
if err != nil {
|
||||
sc.setOperationPhase(v1alpha1.OperationError, fmt.Sprintf("Failed to get hook health: %v", err))
|
||||
return
|
||||
}
|
||||
if phase == v1alpha1.OperationRunning {
|
||||
err := sc.deleteResource(task)
|
||||
if err != nil {
|
||||
sc.setResourceResult(task, "", v1alpha1.OperationFailed, fmt.Sprintf("Failed to delete: %v", err))
|
||||
terminateSuccessful = false
|
||||
} else {
|
||||
sc.setResourceResult(task, "", v1alpha1.OperationSucceeded, fmt.Sprintf("Deleted"))
|
||||
}
|
||||
} else {
|
||||
sc.setResourceResult(task, "", phase, msg)
|
||||
}
|
||||
}
|
||||
if terminateSuccessful {
|
||||
sc.setOperationPhase(v1alpha1.OperationFailed, "Operation terminated")
|
||||
} else {
|
||||
sc.setOperationPhase(v1alpha1.OperationError, "Operation termination had errors")
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *syncContext) deleteResource(task *syncTask) error {
|
||||
sc.log.WithFields(log.Fields{"task": task}).Debug("deleting resource")
|
||||
resIf, err := sc.getResourceIf(task)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
propagationPolicy := metav1.DeletePropagationForeground
|
||||
return resIf.Delete(task.name(), &metav1.DeleteOptions{PropagationPolicy: &propagationPolicy})
|
||||
}
|
||||
|
||||
func (sc *syncContext) getResourceIf(task *syncTask) (dynamic.ResourceInterface, error) {
|
||||
apiResource, err := kube.ServerResourceForGroupVersionKind(sc.disco, task.groupVersionKind())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := kube.ToGroupVersionResource(task.groupVersionKind().GroupVersion().String(), apiResource)
|
||||
resIf := kube.ToResourceInterface(sc.dynamicIf, apiResource, res, task.namespace())
|
||||
return resIf, err
|
||||
}
|
||||
|
||||
var operationPhases = map[v1alpha1.ResultCode]v1alpha1.OperationPhase{
|
||||
v1alpha1.ResultCodeSynced: v1alpha1.OperationRunning,
|
||||
v1alpha1.ResultCodeSyncFailed: v1alpha1.OperationFailed,
|
||||
v1alpha1.ResultCodePruned: v1alpha1.OperationSucceeded,
|
||||
v1alpha1.ResultCodePruneSkipped: v1alpha1.OperationSucceeded,
|
||||
}
|
||||
|
||||
// tri-state
|
||||
type runState = int
|
||||
|
||||
const (
|
||||
successful = iota
|
||||
pending
|
||||
failed
|
||||
)
|
||||
|
||||
func (sc *syncContext) runTasks(tasks syncTasks, dryRun bool) runState {
|
||||
|
||||
dryRun = dryRun || sc.syncOp.DryRun
|
||||
|
||||
sc.log.WithFields(log.Fields{"numTasks": len(tasks), "dryRun": dryRun}).Debug("running tasks")
|
||||
|
||||
runState := successful
|
||||
var createTasks syncTasks
|
||||
var pruneTasks syncTasks
|
||||
|
||||
for _, task := range tasks {
|
||||
if task.isPrune() {
|
||||
pruneTasks = append(pruneTasks, task)
|
||||
} else {
|
||||
createTasks = append(createTasks, task)
|
||||
}
|
||||
}
|
||||
// prune first
|
||||
{
|
||||
var wg sync.WaitGroup
|
||||
for _, task := range pruneTasks {
|
||||
wg.Add(1)
|
||||
go func(t *syncTask) {
|
||||
defer wg.Done()
|
||||
logCtx := sc.log.WithFields(log.Fields{"dryRun": dryRun, "task": t})
|
||||
logCtx.Debug("pruning")
|
||||
result, message := sc.pruneObject(t.liveObj, sc.syncOp.Prune, dryRun)
|
||||
if result == v1alpha1.ResultCodeSyncFailed {
|
||||
runState = failed
|
||||
logCtx.WithField("message", message).Info("pruning failed")
|
||||
}
|
||||
if !dryRun || sc.syncOp.DryRun || result == v1alpha1.ResultCodeSyncFailed {
|
||||
sc.setResourceResult(t, result, operationPhases[result], message)
|
||||
}
|
||||
}(task)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// delete anything that need deleting
|
||||
if runState == successful && createTasks.Any(func(t *syncTask) bool { return t.needsDeleting() }) {
|
||||
var wg sync.WaitGroup
|
||||
for _, task := range createTasks.Filter(func(t *syncTask) bool { return t.needsDeleting() }) {
|
||||
wg.Add(1)
|
||||
go func(t *syncTask) {
|
||||
defer wg.Done()
|
||||
sc.log.WithFields(log.Fields{"dryRun": dryRun, "task": t}).Debug("deleting")
|
||||
if !dryRun {
|
||||
err := sc.deleteResource(t)
|
||||
if err != nil {
|
||||
// it is possible to get a race condition here, such that the resource does not exist when
|
||||
// delete is requested, we treat this as a nop
|
||||
if !apierr.IsNotFound(err) {
|
||||
runState = failed
|
||||
sc.setResourceResult(t, "", v1alpha1.OperationError, fmt.Sprintf("failed to delete resource: %v", err))
|
||||
}
|
||||
} else {
|
||||
// if there is anything that needs deleting, we are at best now in pending and
|
||||
// want to return and wait for sync to be invoked again
|
||||
runState = pending
|
||||
}
|
||||
}
|
||||
}(task)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
// finally create resources
|
||||
if runState == successful {
|
||||
processCreateTasks := func(tasks syncTasks) {
|
||||
var createWg sync.WaitGroup
|
||||
for _, task := range tasks {
|
||||
if dryRun && task.skipDryRun {
|
||||
continue
|
||||
}
|
||||
createWg.Add(1)
|
||||
go func(t *syncTask) {
|
||||
defer createWg.Done()
|
||||
logCtx := sc.log.WithFields(log.Fields{"dryRun": dryRun, "task": t})
|
||||
logCtx.Debug("applying")
|
||||
result, message := sc.applyObject(t.targetObj, dryRun, sc.syncOp.SyncStrategy.Force())
|
||||
if result == v1alpha1.ResultCodeSyncFailed {
|
||||
logCtx.WithField("message", message).Info("apply failed")
|
||||
runState = failed
|
||||
}
|
||||
if !dryRun || sc.syncOp.DryRun || result == v1alpha1.ResultCodeSyncFailed {
|
||||
sc.setResourceResult(t, result, operationPhases[result], message)
|
||||
}
|
||||
}(task)
|
||||
}
|
||||
createWg.Wait()
|
||||
}
|
||||
|
||||
var tasksGroup syncTasks
|
||||
for _, task := range createTasks {
|
||||
//Only wait if the type of the next task is different than the previous type
|
||||
if len(tasksGroup) > 0 && tasksGroup[0].targetObj.GetKind() != task.kind() {
|
||||
processCreateTasks(tasksGroup)
|
||||
tasksGroup = syncTasks{task}
|
||||
} else {
|
||||
tasksGroup = append(tasksGroup, task)
|
||||
}
|
||||
}
|
||||
if len(tasksGroup) > 0 {
|
||||
processCreateTasks(tasksGroup)
|
||||
}
|
||||
}
|
||||
return runState
|
||||
}
|
||||
|
||||
// setResourceResult sets a resource details in the SyncResult.Resources list
|
||||
func (sc *syncContext) setResourceResult(task *syncTask, syncStatus v1alpha1.ResultCode, operationState v1alpha1.OperationPhase, message string) {
|
||||
|
||||
task.syncStatus = syncStatus
|
||||
task.operationState = operationState
|
||||
// we always want to keep the latest message
|
||||
if message != "" {
|
||||
task.message = message
|
||||
}
|
||||
|
||||
sc.lock.Lock()
|
||||
defer sc.lock.Unlock()
|
||||
i, existing := sc.syncRes.Resources.Find(task.group(), task.kind(), task.namespace(), task.name(), task.phase)
|
||||
|
||||
res := v1alpha1.ResourceResult{
|
||||
Group: task.group(),
|
||||
Version: task.version(),
|
||||
Kind: task.kind(),
|
||||
Namespace: task.namespace(),
|
||||
Name: task.name(),
|
||||
Status: task.syncStatus,
|
||||
Message: task.message,
|
||||
HookType: task.hookType(),
|
||||
HookPhase: task.operationState,
|
||||
SyncPhase: task.phase,
|
||||
}
|
||||
|
||||
logCtx := sc.log.WithFields(log.Fields{"namespace": task.namespace(), "kind": task.kind(), "name": task.name(), "phase": task.phase})
|
||||
|
||||
if existing != nil {
|
||||
// update existing value
|
||||
if res.Status != existing.Status || res.HookPhase != existing.HookPhase || res.Message != existing.Message {
|
||||
logCtx.Infof("updating resource result, status: '%s' -> '%s', phase '%s' -> '%s', message '%s' -> '%s'",
|
||||
existing.Status, res.Status,
|
||||
existing.HookPhase, res.HookPhase,
|
||||
existing.Message, res.Message)
|
||||
}
|
||||
sc.syncRes.Resources[i] = &res
|
||||
} else {
|
||||
logCtx.Infof("adding resource result, status: '%s', phase: '%s', message: '%s'", res.Status, res.HookPhase, res.Message)
|
||||
sc.syncRes.Resources = append(sc.syncRes.Resources, &res)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/health"
|
||||
)
|
||||
|
||||
// getOperationPhase returns a hook status from an _live_ unstructured object
|
||||
func (sc *syncContext) getOperationPhase(hook *unstructured.Unstructured) (v1alpha1.OperationPhase, string, error) {
|
||||
phase := v1alpha1.OperationSucceeded
|
||||
message := fmt.Sprintf("%s created", hook.GetName())
|
||||
|
||||
resHealth, err := health.GetResourceHealth(hook, sc.resourceOverrides)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if resHealth != nil {
|
||||
switch resHealth.Status {
|
||||
case v1alpha1.HealthStatusUnknown, v1alpha1.HealthStatusDegraded:
|
||||
phase = v1alpha1.OperationFailed
|
||||
message = resHealth.Message
|
||||
case v1alpha1.HealthStatusProgressing, v1alpha1.HealthStatusSuspended:
|
||||
phase = v1alpha1.OperationRunning
|
||||
message = resHealth.Message
|
||||
case v1alpha1.HealthStatusHealthy:
|
||||
phase = v1alpha1.OperationSucceeded
|
||||
message = resHealth.Message
|
||||
}
|
||||
}
|
||||
return phase, message, nil
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/hook"
|
||||
)
|
||||
|
||||
func syncPhases(obj *unstructured.Unstructured) []v1alpha1.SyncPhase {
|
||||
if hook.Skip(obj) {
|
||||
return nil
|
||||
} else if hook.IsHook(obj) {
|
||||
phasesMap := make(map[v1alpha1.SyncPhase]bool)
|
||||
for _, hookType := range hook.Types(obj) {
|
||||
switch hookType {
|
||||
case v1alpha1.HookTypePreSync, v1alpha1.HookTypeSync, v1alpha1.HookTypePostSync, v1alpha1.HookTypeSyncFail:
|
||||
phasesMap[v1alpha1.SyncPhase(hookType)] = true
|
||||
}
|
||||
}
|
||||
var phases []v1alpha1.SyncPhase
|
||||
for phase := range phasesMap {
|
||||
phases = append(phases, phase)
|
||||
}
|
||||
return phases
|
||||
} else {
|
||||
return []v1alpha1.SyncPhase{v1alpha1.SyncPhaseSync}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
. "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/test"
|
||||
)
|
||||
|
||||
func TestSyncPhaseNone(t *testing.T) {
|
||||
assert.Equal(t, []SyncPhase{SyncPhaseSync}, syncPhases(&unstructured.Unstructured{}))
|
||||
}
|
||||
|
||||
func TestSyncPhasePreSync(t *testing.T) {
|
||||
assert.Equal(t, []SyncPhase{SyncPhasePreSync}, syncPhases(pod("PreSync")))
|
||||
}
|
||||
|
||||
func TestSyncPhaseSync(t *testing.T) {
|
||||
assert.Equal(t, []SyncPhase{SyncPhaseSync}, syncPhases(pod("Sync")))
|
||||
}
|
||||
|
||||
func TestSyncPhaseSkip(t *testing.T) {
|
||||
assert.Nil(t, syncPhases(pod("Skip")))
|
||||
}
|
||||
|
||||
// garbage hooks are still hooks, but have no phases, because some user spelled something wrong
|
||||
func TestSyncPhaseGarbage(t *testing.T) {
|
||||
assert.Nil(t, syncPhases(pod("Garbage")))
|
||||
}
|
||||
|
||||
func TestSyncPhasePost(t *testing.T) {
|
||||
assert.Equal(t, []SyncPhase{SyncPhasePostSync}, syncPhases(pod("PostSync")))
|
||||
}
|
||||
|
||||
func TestSyncPhaseFail(t *testing.T) {
|
||||
assert.Equal(t, []SyncPhase{SyncPhaseSyncFail}, syncPhases(pod("SyncFail")))
|
||||
}
|
||||
|
||||
func TestSyncPhaseTwoPhases(t *testing.T) {
|
||||
assert.ElementsMatch(t, []SyncPhase{SyncPhasePreSync, SyncPhasePostSync}, syncPhases(pod("PreSync,PostSync")))
|
||||
}
|
||||
|
||||
func TestSyncDuplicatedPhases(t *testing.T) {
|
||||
assert.ElementsMatch(t, []SyncPhase{SyncPhasePreSync}, syncPhases(pod("PreSync,PreSync")))
|
||||
assert.ElementsMatch(t, []SyncPhase{SyncPhasePreSync}, syncPhases(podWithHelmHook("pre-install,pre-upgrade")))
|
||||
}
|
||||
|
||||
func pod(hookType string) *unstructured.Unstructured {
|
||||
return test.Annotate(test.NewPod(), "argocd.argoproj.io/hook", hookType)
|
||||
}
|
||||
|
||||
func podWithHelmHook(hookType string) *unstructured.Unstructured {
|
||||
return test.Annotate(test.NewPod(), "helm.sh/hook", hookType)
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/hook"
|
||||
"github.com/argoproj/argo-cd/util/resource/syncwaves"
|
||||
)
|
||||
|
||||
// syncTask holds the live and target object. At least one should be non-nil. A targetObj of nil
|
||||
// indicates the live object needs to be pruned. A liveObj of nil indicates the object has yet to
|
||||
// be deployed
|
||||
type syncTask struct {
|
||||
phase v1alpha1.SyncPhase
|
||||
liveObj *unstructured.Unstructured
|
||||
targetObj *unstructured.Unstructured
|
||||
skipDryRun bool
|
||||
syncStatus v1alpha1.ResultCode
|
||||
operationState v1alpha1.OperationPhase
|
||||
message string
|
||||
}
|
||||
|
||||
func ternary(val bool, a, b string) string {
|
||||
if val {
|
||||
return a
|
||||
} else {
|
||||
return b
|
||||
}
|
||||
}
|
||||
|
||||
func (t *syncTask) String() string {
|
||||
return fmt.Sprintf("%s/%d %s %s/%s:%s/%s %s->%s (%s,%s,%s)",
|
||||
t.phase, t.wave(),
|
||||
ternary(t.isHook(), "hook", "resource"), t.group(), t.kind(), t.namespace(), t.name(),
|
||||
ternary(t.liveObj != nil, "obj", "nil"), ternary(t.targetObj != nil, "obj", "nil"),
|
||||
t.syncStatus, t.operationState, t.message,
|
||||
)
|
||||
}
|
||||
|
||||
func (t *syncTask) isPrune() bool {
|
||||
return t.targetObj == nil
|
||||
}
|
||||
|
||||
// return the target object (if this exists) otherwise the live object
|
||||
// some caution - often you explicitly want the live object not the target object
|
||||
func (t *syncTask) obj() *unstructured.Unstructured {
|
||||
return obj(t.targetObj, t.liveObj)
|
||||
}
|
||||
|
||||
func (t *syncTask) wave() int {
|
||||
return syncwaves.Wave(t.obj())
|
||||
}
|
||||
|
||||
func (t *syncTask) isHook() bool {
|
||||
return hook.IsHook(t.obj())
|
||||
}
|
||||
|
||||
func (t *syncTask) group() string {
|
||||
return t.groupVersionKind().Group
|
||||
}
|
||||
func (t *syncTask) kind() string {
|
||||
return t.groupVersionKind().Kind
|
||||
}
|
||||
|
||||
func (t *syncTask) version() string {
|
||||
return t.groupVersionKind().Version
|
||||
}
|
||||
|
||||
func (t *syncTask) groupVersionKind() schema.GroupVersionKind {
|
||||
return t.obj().GroupVersionKind()
|
||||
}
|
||||
|
||||
func (t *syncTask) name() string {
|
||||
return t.obj().GetName()
|
||||
}
|
||||
|
||||
func (t *syncTask) namespace() string {
|
||||
return t.obj().GetNamespace()
|
||||
}
|
||||
|
||||
func (t *syncTask) pending() bool {
|
||||
return t.operationState == ""
|
||||
}
|
||||
|
||||
func (t *syncTask) running() bool {
|
||||
return t.operationState.Running()
|
||||
}
|
||||
|
||||
func (t *syncTask) completed() bool {
|
||||
return t.operationState.Completed()
|
||||
}
|
||||
|
||||
func (t *syncTask) successful() bool {
|
||||
return t.operationState.Successful()
|
||||
}
|
||||
|
||||
func (t *syncTask) failed() bool {
|
||||
return t.operationState.Failed()
|
||||
}
|
||||
|
||||
func (t *syncTask) hookType() v1alpha1.HookType {
|
||||
if t.isHook() {
|
||||
return v1alpha1.HookType(t.phase)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (t *syncTask) hasHookDeletePolicy(policy v1alpha1.HookDeletePolicy) bool {
|
||||
// cannot have a policy if it is not a hook, it is meaningless
|
||||
if !t.isHook() {
|
||||
return false
|
||||
}
|
||||
for _, p := range hook.DeletePolicies(t.obj()) {
|
||||
if p == policy {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *syncTask) needsDeleting() bool {
|
||||
return t.liveObj != nil && (t.pending() && t.hasHookDeletePolicy(v1alpha1.HookDeletePolicyBeforeHookCreation) ||
|
||||
t.successful() && t.hasHookDeletePolicy(v1alpha1.HookDeletePolicyHookSucceeded) ||
|
||||
t.failed() && t.hasHookDeletePolicy(v1alpha1.HookDeletePolicyHookFailed))
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
. "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
. "github.com/argoproj/argo-cd/test"
|
||||
)
|
||||
|
||||
func Test_syncTask_hookType(t *testing.T) {
|
||||
type fields struct {
|
||||
phase SyncPhase
|
||||
liveObj *unstructured.Unstructured
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want HookType
|
||||
}{
|
||||
{"Empty", fields{SyncPhaseSync, NewPod()}, ""},
|
||||
{"PreSyncHook", fields{SyncPhasePreSync, NewHook(HookTypePreSync)}, HookTypePreSync},
|
||||
{"SyncHook", fields{SyncPhaseSync, NewHook(HookTypeSync)}, HookTypeSync},
|
||||
{"PostSyncHook", fields{SyncPhasePostSync, NewHook(HookTypePostSync)}, HookTypePostSync},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
task := &syncTask{
|
||||
phase: tt.fields.phase,
|
||||
liveObj: tt.fields.liveObj,
|
||||
}
|
||||
hookType := task.hookType()
|
||||
assert.EqualValues(t, tt.want, hookType)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_syncTask_hasHookDeletePolicy(t *testing.T) {
|
||||
assert.False(t, (&syncTask{targetObj: NewPod()}).hasHookDeletePolicy(HookDeletePolicyBeforeHookCreation))
|
||||
assert.False(t, (&syncTask{targetObj: NewPod()}).hasHookDeletePolicy(HookDeletePolicyHookSucceeded))
|
||||
assert.False(t, (&syncTask{targetObj: NewPod()}).hasHookDeletePolicy(HookDeletePolicyHookFailed))
|
||||
// must be hook
|
||||
assert.False(t, (&syncTask{targetObj: Annotate(NewPod(), "argocd.argoproj.io/hook-delete-policy", "BeforeHookCreation")}).hasHookDeletePolicy(HookDeletePolicyBeforeHookCreation))
|
||||
assert.True(t, (&syncTask{targetObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "BeforeHookCreation")}).hasHookDeletePolicy(HookDeletePolicyBeforeHookCreation))
|
||||
assert.True(t, (&syncTask{targetObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "HookSucceeded")}).hasHookDeletePolicy(HookDeletePolicyHookSucceeded))
|
||||
assert.True(t, (&syncTask{targetObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "HookFailed")}).hasHookDeletePolicy(HookDeletePolicyHookFailed))
|
||||
}
|
||||
|
||||
func Test_syncTask_needsDeleting(t *testing.T) {
|
||||
assert.False(t, (&syncTask{liveObj: NewPod()}).needsDeleting())
|
||||
// must be hook
|
||||
assert.False(t, (&syncTask{liveObj: Annotate(NewPod(), "argocd.argoproj.io/hook-delete-policy", "BeforeHookCreation")}).needsDeleting())
|
||||
// no need to delete if no live obj
|
||||
assert.False(t, (&syncTask{targetObj: Annotate(Annotate(NewPod(), "argoocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "BeforeHookCreation")}).needsDeleting())
|
||||
assert.True(t, (&syncTask{liveObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "BeforeHookCreation")}).needsDeleting())
|
||||
assert.True(t, (&syncTask{liveObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "BeforeHookCreation")}).needsDeleting())
|
||||
assert.True(t, (&syncTask{operationState: OperationSucceeded, liveObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "HookSucceeded")}).needsDeleting())
|
||||
assert.True(t, (&syncTask{operationState: OperationFailed, liveObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "HookFailed")}).needsDeleting())
|
||||
}
|
||||
|
||||
func Test_syncTask_wave(t *testing.T) {
|
||||
assert.Equal(t, 0, (&syncTask{targetObj: NewPod()}).wave())
|
||||
assert.Equal(t, 1, (&syncTask{targetObj: Annotate(NewPod(), "argocd.argoproj.io/sync-wave", "1")}).wave())
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
// kindOrder represents the correct order of Kubernetes resources within a manifest
|
||||
var syncPhaseOrder = map[v1alpha1.SyncPhase]int{
|
||||
v1alpha1.SyncPhasePreSync: -1,
|
||||
v1alpha1.SyncPhaseSync: 0,
|
||||
v1alpha1.SyncPhasePostSync: 1,
|
||||
v1alpha1.SyncPhaseSyncFail: 2,
|
||||
}
|
||||
|
||||
// kindOrder represents the correct order of Kubernetes resources within a manifest
|
||||
// https://github.com/helm/helm/blob/master/pkg/tiller/kind_sorter.go
|
||||
var kindOrder = map[string]int{}
|
||||
|
||||
func init() {
|
||||
kinds := []string{
|
||||
"Namespace",
|
||||
"ResourceQuota",
|
||||
"LimitRange",
|
||||
"PodSecurityPolicy",
|
||||
"PodDisruptionBudget",
|
||||
"Secret",
|
||||
"ConfigMap",
|
||||
"StorageClass",
|
||||
"PersistentVolume",
|
||||
"PersistentVolumeClaim",
|
||||
"ServiceAccount",
|
||||
"CustomResourceDefinition",
|
||||
"ClusterRole",
|
||||
"ClusterRoleBinding",
|
||||
"Role",
|
||||
"RoleBinding",
|
||||
"Service",
|
||||
"DaemonSet",
|
||||
"Pod",
|
||||
"ReplicationController",
|
||||
"ReplicaSet",
|
||||
"Deployment",
|
||||
"StatefulSet",
|
||||
"Job",
|
||||
"CronJob",
|
||||
"Ingress",
|
||||
"APIService",
|
||||
}
|
||||
for i, kind := range kinds {
|
||||
// make sure none of the above entries are zero, we need that for custom resources
|
||||
kindOrder[kind] = i - len(kinds)
|
||||
}
|
||||
}
|
||||
|
||||
type syncTasks []*syncTask
|
||||
|
||||
func (s syncTasks) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func (s syncTasks) Swap(i, j int) {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
// order is
|
||||
// 1. phase
|
||||
// 2. wave
|
||||
// 3. kind
|
||||
// 4. name
|
||||
func (s syncTasks) Less(i, j int) bool {
|
||||
|
||||
tA := s[i]
|
||||
tB := s[j]
|
||||
|
||||
d := syncPhaseOrder[tA.phase] - syncPhaseOrder[tB.phase]
|
||||
if d != 0 {
|
||||
return d < 0
|
||||
}
|
||||
|
||||
d = tA.wave() - tB.wave()
|
||||
if d != 0 {
|
||||
return d < 0
|
||||
}
|
||||
|
||||
a := tA.obj()
|
||||
b := tB.obj()
|
||||
|
||||
// we take advantage of the fact that if the kind is not in the kindOrder map,
|
||||
// then it will return the default int value of zero, which is the highest value
|
||||
d = kindOrder[a.GetKind()] - kindOrder[b.GetKind()]
|
||||
if d != 0 {
|
||||
return d < 0
|
||||
}
|
||||
|
||||
return a.GetName() < b.GetName()
|
||||
}
|
||||
|
||||
func (s syncTasks) Filter(predicate func(task *syncTask) bool) (tasks syncTasks) {
|
||||
for _, task := range s {
|
||||
if predicate(task) {
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
func (s syncTasks) Split(predicate func(task *syncTask) bool) (trueTasks, falseTasks syncTasks) {
|
||||
for _, task := range s {
|
||||
if predicate(task) {
|
||||
trueTasks = append(trueTasks, task)
|
||||
} else {
|
||||
falseTasks = append(falseTasks, task)
|
||||
}
|
||||
}
|
||||
return trueTasks, falseTasks
|
||||
}
|
||||
|
||||
func (s syncTasks) All(predicate func(task *syncTask) bool) bool {
|
||||
for _, task := range s {
|
||||
if !predicate(task) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s syncTasks) Any(predicate func(task *syncTask) bool) bool {
|
||||
for _, task := range s {
|
||||
if predicate(task) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s syncTasks) Find(predicate func(task *syncTask) bool) *syncTask {
|
||||
for _, task := range s {
|
||||
if predicate(task) {
|
||||
return task
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s syncTasks) String() string {
|
||||
var values []string
|
||||
for _, task := range s {
|
||||
values = append(values, task.String())
|
||||
}
|
||||
return "[" + strings.Join(values, ", ") + "]"
|
||||
}
|
||||
|
||||
func (s syncTasks) phase() v1alpha1.SyncPhase {
|
||||
if len(s) > 0 {
|
||||
return s[0].phase
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s syncTasks) wave() int {
|
||||
if len(s) > 0 {
|
||||
return s[0].wave()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s syncTasks) lastPhase() v1alpha1.SyncPhase {
|
||||
if len(s) > 0 {
|
||||
return s[len(s)-1].phase
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s syncTasks) lastWave() int {
|
||||
if len(s) > 0 {
|
||||
return s[len(s)-1].wave()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s syncTasks) multiStep() bool {
|
||||
return s.wave() != s.lastWave() || s.phase() != s.lastPhase()
|
||||
}
|
||||
@@ -1,392 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
. "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
. "github.com/argoproj/argo-cd/test"
|
||||
)
|
||||
|
||||
func Test_syncTasks_kindOrder(t *testing.T) {
|
||||
assert.Equal(t, -27, kindOrder["Namespace"])
|
||||
assert.Equal(t, -1, kindOrder["APIService"])
|
||||
assert.Equal(t, 0, kindOrder["MyCRD"])
|
||||
}
|
||||
|
||||
func TestSortSyncTask(t *testing.T) {
|
||||
sort.Sort(unsortedTasks)
|
||||
assert.Equal(t, sortedTasks, unsortedTasks)
|
||||
}
|
||||
|
||||
func TestAnySyncTasks(t *testing.T) {
|
||||
res := unsortedTasks.Any(func(task *syncTask) bool {
|
||||
return task.name() == "a"
|
||||
})
|
||||
assert.True(t, res)
|
||||
|
||||
res = unsortedTasks.Any(func(task *syncTask) bool {
|
||||
return task.name() == "does-not-exist"
|
||||
})
|
||||
assert.False(t, res)
|
||||
|
||||
}
|
||||
|
||||
func TestAllSyncTasks(t *testing.T) {
|
||||
res := unsortedTasks.All(func(task *syncTask) bool {
|
||||
return task.name() != ""
|
||||
})
|
||||
assert.False(t, res)
|
||||
|
||||
res = unsortedTasks.All(func(task *syncTask) bool {
|
||||
return task.name() == "a"
|
||||
})
|
||||
assert.False(t, res)
|
||||
}
|
||||
|
||||
func TestSplitSyncTasks(t *testing.T) {
|
||||
named, unnamed := sortedTasks.Split(func(task *syncTask) bool {
|
||||
return task.name() != ""
|
||||
})
|
||||
assert.Equal(t, named, namedObjTasks)
|
||||
assert.Equal(t, unnamed, unnamedTasks)
|
||||
}
|
||||
|
||||
var unsortedTasks = syncTasks{
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "Pod",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "Service",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "PersistentVolume",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
phase: SyncPhaseSyncFail, targetObj: &unstructured.Unstructured{},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"annotations": map[string]interface{}{
|
||||
"argocd.argoproj.io/sync-wave": "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "a",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"annotations": map[string]interface{}{
|
||||
"argocd.argoproj.io/sync-wave": "-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
phase: SyncPhasePreSync,
|
||||
targetObj: &unstructured.Unstructured{},
|
||||
},
|
||||
{
|
||||
phase: SyncPhasePostSync, targetObj: &unstructured.Unstructured{},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "ConfigMap",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var sortedTasks = syncTasks{
|
||||
{
|
||||
phase: SyncPhasePreSync,
|
||||
targetObj: &unstructured.Unstructured{},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"annotations": map[string]interface{}{
|
||||
"argocd.argoproj.io/sync-wave": "-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "ConfigMap",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "PersistentVolume",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "Service",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "Pod",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "a",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"annotations": map[string]interface{}{
|
||||
"argocd.argoproj.io/sync-wave": "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
phase: SyncPhasePostSync,
|
||||
targetObj: &unstructured.Unstructured{},
|
||||
},
|
||||
{
|
||||
phase: SyncPhaseSyncFail,
|
||||
targetObj: &unstructured.Unstructured{},
|
||||
},
|
||||
}
|
||||
|
||||
var namedObjTasks = syncTasks{
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "a",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "b",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var unnamedTasks = syncTasks{
|
||||
{
|
||||
phase: SyncPhasePreSync,
|
||||
targetObj: &unstructured.Unstructured{},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"annotations": map[string]interface{}{
|
||||
"argocd.argoproj.io/sync-wave": "-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "ConfigMap",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "PersistentVolume",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "Service",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "Pod",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"annotations": map[string]interface{}{
|
||||
"argocd.argoproj.io/sync-wave": "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
phase: SyncPhasePostSync,
|
||||
targetObj: &unstructured.Unstructured{},
|
||||
},
|
||||
{
|
||||
phase: SyncPhaseSyncFail,
|
||||
targetObj: &unstructured.Unstructured{},
|
||||
},
|
||||
}
|
||||
|
||||
func Test_syncTasks_Filter(t *testing.T) {
|
||||
tasks := syncTasks{{phase: SyncPhaseSync}, {phase: SyncPhasePostSync}}
|
||||
|
||||
assert.Equal(t, syncTasks{{phase: SyncPhaseSync}}, tasks.Filter(func(t *syncTask) bool {
|
||||
return t.phase == SyncPhaseSync
|
||||
}))
|
||||
}
|
||||
|
||||
func TestSyncNamespaceAgainstCRD(t *testing.T) {
|
||||
crd := &syncTask{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "Workflow",
|
||||
},
|
||||
}}
|
||||
namespace := &syncTask{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"kind": "Namespace",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
unsorted := syncTasks{crd, namespace}
|
||||
sort.Sort(unsorted)
|
||||
|
||||
assert.Equal(t, syncTasks{namespace, crd}, unsorted)
|
||||
}
|
||||
|
||||
func Test_syncTasks_multiStep(t *testing.T) {
|
||||
t.Run("Single", func(t *testing.T) {
|
||||
tasks := syncTasks{{liveObj: Annotate(NewPod(), common.AnnotationSyncWave, "-1"), phase: SyncPhaseSync}}
|
||||
assert.Equal(t, SyncPhaseSync, tasks.phase())
|
||||
assert.Equal(t, -1, tasks.wave())
|
||||
assert.Equal(t, SyncPhaseSync, tasks.lastPhase())
|
||||
assert.Equal(t, -1, tasks.lastWave())
|
||||
assert.False(t, tasks.multiStep())
|
||||
})
|
||||
t.Run("Double", func(t *testing.T) {
|
||||
tasks := syncTasks{
|
||||
{liveObj: Annotate(NewPod(), common.AnnotationSyncWave, "-1"), phase: SyncPhasePreSync},
|
||||
{liveObj: Annotate(NewPod(), common.AnnotationSyncWave, "1"), phase: SyncPhasePostSync},
|
||||
}
|
||||
assert.Equal(t, SyncPhasePreSync, tasks.phase())
|
||||
assert.Equal(t, -1, tasks.wave())
|
||||
assert.Equal(t, SyncPhasePostSync, tasks.lastPhase())
|
||||
assert.Equal(t, 1, tasks.lastWave())
|
||||
assert.True(t, tasks.multiStep())
|
||||
})
|
||||
}
|
||||
@@ -1,448 +1,21 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
"github.com/stretchr/testify/assert"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
fakedisco "k8s.io/client-go/discovery/fake"
|
||||
"k8s.io/client-go/dynamic/fake"
|
||||
"k8s.io/client-go/rest"
|
||||
testcore "k8s.io/client-go/testing"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
. "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/test"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/kube/kubetest"
|
||||
)
|
||||
|
||||
func newTestSyncCtx(resources ...*v1.APIResourceList) *syncContext {
|
||||
fakeDisco := &fakedisco.FakeDiscovery{Fake: &testcore.Fake{}}
|
||||
fakeDisco.Resources = append(resources,
|
||||
&v1.APIResourceList{
|
||||
GroupVersion: "v1",
|
||||
APIResources: []v1.APIResource{
|
||||
{Kind: "Pod", Group: "", Version: "v1", Namespaced: true},
|
||||
{Kind: "Service", Group: "", Version: "v1", Namespaced: true},
|
||||
},
|
||||
},
|
||||
&v1.APIResourceList{
|
||||
GroupVersion: "apps/v1",
|
||||
APIResources: []v1.APIResource{
|
||||
{Kind: "Deployment", Group: "apps", Version: "v1", Namespaced: true},
|
||||
},
|
||||
})
|
||||
sc := syncContext{
|
||||
config: &rest.Config{},
|
||||
namespace: test.FakeArgoCDNamespace,
|
||||
server: test.FakeClusterURL,
|
||||
syncRes: &v1alpha1.SyncOperationResult{
|
||||
Revision: "FooBarBaz",
|
||||
},
|
||||
syncOp: &v1alpha1.SyncOperation{
|
||||
Prune: true,
|
||||
SyncStrategy: &v1alpha1.SyncStrategy{
|
||||
Apply: &v1alpha1.SyncStrategyApply{},
|
||||
},
|
||||
},
|
||||
proj: &v1alpha1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
},
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
Destinations: []v1alpha1.ApplicationDestination{{
|
||||
Server: test.FakeClusterURL,
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
}},
|
||||
ClusterResourceWhitelist: []v1.GroupKind{
|
||||
{Group: "*", Kind: "*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
opState: &v1alpha1.OperationState{},
|
||||
disco: fakeDisco,
|
||||
log: log.WithFields(log.Fields{"application": "fake-app"}),
|
||||
}
|
||||
sc.kubectl = &kubetest.MockKubectlCmd{}
|
||||
return &sc
|
||||
}
|
||||
|
||||
func newManagedResource(live *unstructured.Unstructured) managedResource {
|
||||
return managedResource{
|
||||
Live: live,
|
||||
Group: live.GroupVersionKind().Group,
|
||||
Version: live.GroupVersionKind().Version,
|
||||
Kind: live.GroupVersionKind().Kind,
|
||||
Namespace: live.GetNamespace(),
|
||||
Name: live.GetName(),
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncNotPermittedNamespace(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
targetPod := test.NewPod()
|
||||
targetPod.SetNamespace("kube-system")
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: nil,
|
||||
Target: targetPod,
|
||||
}, {
|
||||
Live: nil,
|
||||
Target: test.NewService(),
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Equal(t, v1alpha1.OperationFailed, syncCtx.opState.Phase)
|
||||
assert.Contains(t, syncCtx.syncRes.Resources[0].Message, "not permitted in project")
|
||||
}
|
||||
|
||||
func TestSyncCreateInSortedOrder(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: nil,
|
||||
Target: test.NewPod(),
|
||||
}, {
|
||||
Live: nil,
|
||||
Target: test.NewService(),
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Equal(t, v1alpha1.OperationSucceeded, syncCtx.opState.Phase)
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 2)
|
||||
for i := range syncCtx.syncRes.Resources {
|
||||
result := syncCtx.syncRes.Resources[i]
|
||||
if result.Kind == "Pod" {
|
||||
assert.Equal(t, v1alpha1.ResultCodeSynced, result.Status)
|
||||
assert.Equal(t, "", result.Message)
|
||||
} else if result.Kind == "Service" {
|
||||
assert.Equal(t, "", result.Message)
|
||||
} else {
|
||||
t.Error("Resource isn't a pod or a service")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncCreateNotWhitelistedClusterResources(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx(&v1.APIResourceList{
|
||||
GroupVersion: v1alpha1.SchemeGroupVersion.String(),
|
||||
APIResources: []v1.APIResource{
|
||||
{Name: "workflows", Namespaced: false, Kind: "Workflow", Group: "argoproj.io"},
|
||||
{Name: "application", Namespaced: false, Kind: "Application", Group: "argoproj.io"},
|
||||
},
|
||||
}, &v1.APIResourceList{
|
||||
GroupVersion: "rbac.authorization.k8s.io/v1",
|
||||
APIResources: []v1.APIResource{
|
||||
{Name: "clusterroles", Namespaced: false, Kind: "ClusterRole", Group: "rbac.authorization.k8s.io"},
|
||||
},
|
||||
})
|
||||
|
||||
syncCtx.proj.Spec.ClusterResourceWhitelist = []v1.GroupKind{
|
||||
{Group: "argoproj.io", Kind: "*"},
|
||||
}
|
||||
|
||||
syncCtx.kubectl = &kubetest.MockKubectlCmd{}
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: nil,
|
||||
Target: kube.MustToUnstructured(&rbacv1.ClusterRole{
|
||||
TypeMeta: metav1.TypeMeta{Kind: "ClusterRole", APIVersion: "rbac.authorization.k8s.io/v1"},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "argo-ui-cluster-role"}}),
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 1)
|
||||
result := syncCtx.syncRes.Resources[0]
|
||||
assert.Equal(t, v1alpha1.ResultCodeSyncFailed, result.Status)
|
||||
assert.Contains(t, result.Message, "not permitted in project")
|
||||
}
|
||||
|
||||
func TestSyncBlacklistedNamespacedResources(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
|
||||
syncCtx.proj.Spec.NamespaceResourceBlacklist = []v1.GroupKind{
|
||||
{Group: "*", Kind: "Deployment"},
|
||||
}
|
||||
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: nil,
|
||||
Target: test.NewDeployment(),
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 1)
|
||||
result := syncCtx.syncRes.Resources[0]
|
||||
assert.Equal(t, v1alpha1.ResultCodeSyncFailed, result.Status)
|
||||
assert.Contains(t, result.Message, "not permitted in project")
|
||||
}
|
||||
|
||||
func TestSyncSuccessfully(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
pod := test.NewPod()
|
||||
pod.SetNamespace(test.FakeArgoCDNamespace)
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: nil,
|
||||
Target: test.NewService(),
|
||||
}, {
|
||||
Live: pod,
|
||||
Target: nil,
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Equal(t, v1alpha1.OperationSucceeded, syncCtx.opState.Phase)
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 2)
|
||||
for i := range syncCtx.syncRes.Resources {
|
||||
result := syncCtx.syncRes.Resources[i]
|
||||
if result.Kind == "Pod" {
|
||||
assert.Equal(t, v1alpha1.ResultCodePruned, result.Status)
|
||||
assert.Equal(t, "pruned", result.Message)
|
||||
} else if result.Kind == "Service" {
|
||||
assert.Equal(t, v1alpha1.ResultCodeSynced, result.Status)
|
||||
assert.Equal(t, "", result.Message)
|
||||
} else {
|
||||
t.Error("Resource isn't a pod or a service")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncDeleteSuccessfully(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
svc := test.NewService()
|
||||
svc.SetNamespace(test.FakeArgoCDNamespace)
|
||||
pod := test.NewPod()
|
||||
pod.SetNamespace(test.FakeArgoCDNamespace)
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: svc,
|
||||
Target: nil,
|
||||
}, {
|
||||
Live: pod,
|
||||
Target: nil,
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Equal(t, v1alpha1.OperationSucceeded, syncCtx.opState.Phase)
|
||||
for i := range syncCtx.syncRes.Resources {
|
||||
result := syncCtx.syncRes.Resources[i]
|
||||
if result.Kind == "Pod" {
|
||||
assert.Equal(t, v1alpha1.ResultCodePruned, result.Status)
|
||||
assert.Equal(t, "pruned", result.Message)
|
||||
} else if result.Kind == "Service" {
|
||||
assert.Equal(t, v1alpha1.ResultCodePruned, result.Status)
|
||||
assert.Equal(t, "pruned", result.Message)
|
||||
} else {
|
||||
t.Error("Resource isn't a pod or a service")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncCreateFailure(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
testSvc := test.NewService()
|
||||
syncCtx.kubectl = &kubetest.MockKubectlCmd{
|
||||
Commands: map[string]kubetest.KubectlOutput{
|
||||
testSvc.GetName(): {
|
||||
Output: "",
|
||||
Err: fmt.Errorf("foo"),
|
||||
},
|
||||
},
|
||||
}
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: nil,
|
||||
Target: testSvc,
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 1)
|
||||
result := syncCtx.syncRes.Resources[0]
|
||||
assert.Equal(t, v1alpha1.ResultCodeSyncFailed, result.Status)
|
||||
assert.Equal(t, "foo", result.Message)
|
||||
}
|
||||
|
||||
func TestSyncPruneFailure(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.kubectl = &kubetest.MockKubectlCmd{
|
||||
Commands: map[string]kubetest.KubectlOutput{
|
||||
"test-service": {
|
||||
Output: "",
|
||||
Err: fmt.Errorf("foo"),
|
||||
},
|
||||
},
|
||||
}
|
||||
testSvc := test.NewService()
|
||||
testSvc.SetName("test-service")
|
||||
testSvc.SetNamespace(test.FakeArgoCDNamespace)
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: testSvc,
|
||||
Target: nil,
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Equal(t, v1alpha1.OperationFailed, syncCtx.opState.Phase)
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 1)
|
||||
result := syncCtx.syncRes.Resources[0]
|
||||
assert.Equal(t, v1alpha1.ResultCodeSyncFailed, result.Status)
|
||||
assert.Equal(t, "foo", result.Message)
|
||||
}
|
||||
|
||||
func TestDontSyncOrPruneHooks(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
targetPod := test.NewPod()
|
||||
targetPod.SetName("dont-create-me")
|
||||
targetPod.SetAnnotations(map[string]string{common.AnnotationKeyHook: "PreSync"})
|
||||
liveSvc := test.NewService()
|
||||
liveSvc.SetName("dont-prune-me")
|
||||
liveSvc.SetNamespace(test.FakeArgoCDNamespace)
|
||||
liveSvc.SetAnnotations(map[string]string{common.AnnotationKeyHook: "PreSync"})
|
||||
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
hooks: []*unstructured.Unstructured{targetPod, liveSvc},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 0)
|
||||
syncCtx.sync()
|
||||
assert.Equal(t, v1alpha1.OperationSucceeded, syncCtx.opState.Phase)
|
||||
}
|
||||
|
||||
// make sure that we do not prune resources with Prune=false
|
||||
func TestDontPrunePruneFalse(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
pod := test.NewPod()
|
||||
pod.SetAnnotations(map[string]string{common.AnnotationSyncOptions: "Prune=false"})
|
||||
pod.SetNamespace(test.FakeArgoCDNamespace)
|
||||
syncCtx.compareResult = &comparisonResult{managedResources: []managedResource{{Live: pod}}}
|
||||
|
||||
syncCtx.sync()
|
||||
|
||||
assert.Equal(t, v1alpha1.OperationSucceeded, syncCtx.opState.Phase)
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 1)
|
||||
assert.Equal(t, v1alpha1.ResultCodePruneSkipped, syncCtx.syncRes.Resources[0].Status)
|
||||
assert.Equal(t, "ignored (no prune)", syncCtx.syncRes.Resources[0].Message)
|
||||
|
||||
syncCtx.sync()
|
||||
|
||||
assert.Equal(t, v1alpha1.OperationSucceeded, syncCtx.opState.Phase)
|
||||
}
|
||||
|
||||
// make sure Validate=false means we don't validate
|
||||
func TestSyncOptionValidate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
annotationVal string
|
||||
want bool
|
||||
}{
|
||||
{"Empty", "", true},
|
||||
{"True", "Validate=true", true},
|
||||
{"False", "Validate=false", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
pod := test.NewPod()
|
||||
pod.SetAnnotations(map[string]string{common.AnnotationSyncOptions: tt.annotationVal})
|
||||
pod.SetNamespace(test.FakeArgoCDNamespace)
|
||||
syncCtx.compareResult = &comparisonResult{managedResources: []managedResource{{Target: pod, Live: pod}}}
|
||||
|
||||
syncCtx.sync()
|
||||
|
||||
kubectl, _ := syncCtx.kubectl.(*kubetest.MockKubectlCmd)
|
||||
assert.Equal(t, tt.want, kubectl.LastValidate)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectiveSyncOnly(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
pod1 := test.NewPod()
|
||||
pod1.SetName("pod-1")
|
||||
pod2 := test.NewPod()
|
||||
pod2.SetName("pod-2")
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{Target: pod1}},
|
||||
}
|
||||
syncCtx.syncResources = []v1alpha1.SyncOperationResource{{Kind: "Pod", Name: "pod-1"}}
|
||||
|
||||
tasks, successful := syncCtx.getSyncTasks()
|
||||
|
||||
assert.True(t, successful)
|
||||
assert.Len(t, tasks, 1)
|
||||
assert.Equal(t, "pod-1", tasks[0].name())
|
||||
}
|
||||
|
||||
func TestUnnamedHooksGetUniqueNames(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.syncOp.SyncStrategy.Apply = nil
|
||||
pod := test.NewPod()
|
||||
pod.SetName("")
|
||||
pod.SetAnnotations(map[string]string{common.AnnotationKeyHook: "PreSync,PostSync"})
|
||||
syncCtx.compareResult = &comparisonResult{hooks: []*unstructured.Unstructured{pod}}
|
||||
|
||||
tasks, successful := syncCtx.getSyncTasks()
|
||||
|
||||
assert.True(t, successful)
|
||||
assert.Len(t, tasks, 2)
|
||||
assert.Contains(t, tasks[0].name(), "foobarb-presync-")
|
||||
assert.Contains(t, tasks[1].name(), "foobarb-postsync-")
|
||||
assert.Equal(t, "", pod.GetName())
|
||||
}
|
||||
|
||||
func TestManagedResourceAreNotNamed(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
pod := test.NewPod()
|
||||
pod.SetName("")
|
||||
syncCtx.compareResult = &comparisonResult{managedResources: []managedResource{{Target: pod}}}
|
||||
|
||||
tasks, successful := syncCtx.getSyncTasks()
|
||||
|
||||
assert.True(t, successful)
|
||||
assert.Len(t, tasks, 1)
|
||||
assert.Equal(t, "", tasks[0].name())
|
||||
assert.Equal(t, "", pod.GetName())
|
||||
}
|
||||
|
||||
func TestDeDupingTasks(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.syncOp.SyncStrategy.Apply = nil
|
||||
pod := test.NewPod()
|
||||
pod.SetAnnotations(map[string]string{common.AnnotationKeyHook: "Sync"})
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{Target: pod}},
|
||||
hooks: []*unstructured.Unstructured{pod},
|
||||
}
|
||||
|
||||
tasks, successful := syncCtx.getSyncTasks()
|
||||
|
||||
assert.True(t, successful)
|
||||
assert.Len(t, tasks, 1)
|
||||
}
|
||||
|
||||
func TestObjectsGetANamespace(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
pod := test.NewPod()
|
||||
syncCtx.compareResult = &comparisonResult{managedResources: []managedResource{{Target: pod}}}
|
||||
|
||||
tasks, successful := syncCtx.getSyncTasks()
|
||||
|
||||
assert.True(t, successful)
|
||||
assert.Len(t, tasks, 1)
|
||||
assert.Equal(t, test.FakeArgoCDNamespace, tasks[0].namespace())
|
||||
assert.Equal(t, "", pod.GetNamespace())
|
||||
}
|
||||
|
||||
func TestPersistRevisionHistory(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Status.OperationState = nil
|
||||
@@ -474,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)
|
||||
@@ -523,177 +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 TestSyncFailureHookWithSuccessfulSync(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.syncOp.SyncStrategy.Apply = nil
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{Target: test.NewPod()}},
|
||||
hooks: []*unstructured.Unstructured{test.NewHook(HookTypeSyncFail)},
|
||||
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)
|
||||
|
||||
syncCtx.sync()
|
||||
// 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)
|
||||
|
||||
assert.Equal(t, OperationSucceeded, syncCtx.opState.Phase)
|
||||
// only one result, we did not run the failure failureHook
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 1)
|
||||
}
|
||||
|
||||
func TestSyncFailureHookWithFailedSync(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.syncOp.SyncStrategy.Apply = nil
|
||||
pod := test.NewPod()
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{Target: pod}},
|
||||
hooks: []*unstructured.Unstructured{test.NewHook(HookTypeSyncFail)},
|
||||
}
|
||||
syncCtx.kubectl = &kubetest.MockKubectlCmd{
|
||||
Commands: map[string]kubetest.KubectlOutput{pod.GetName(): {Err: fmt.Errorf("")}},
|
||||
}
|
||||
|
||||
syncCtx.sync()
|
||||
syncCtx.sync()
|
||||
|
||||
assert.Equal(t, OperationFailed, syncCtx.opState.Phase)
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 2)
|
||||
}
|
||||
|
||||
func TestBeforeHookCreation(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.syncOp.SyncStrategy.Apply = nil
|
||||
hook := test.Annotate(test.Annotate(test.NewPod(), common.AnnotationKeyHook, "Sync"), common.AnnotationKeyHookDeletePolicy, "BeforeHookCreation")
|
||||
hook.SetNamespace(test.FakeArgoCDNamespace)
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{newManagedResource(hook)},
|
||||
hooks: []*unstructured.Unstructured{hook},
|
||||
}
|
||||
syncCtx.dynamicIf = fake.NewSimpleDynamicClient(runtime.NewScheme())
|
||||
|
||||
syncCtx.sync()
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 1)
|
||||
assert.Empty(t, syncCtx.syncRes.Resources[0].Message)
|
||||
}
|
||||
|
||||
func TestRunSyncFailHooksFailed(t *testing.T) {
|
||||
// Tests that other SyncFail Hooks run even if one of them fail.
|
||||
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.syncOp.SyncStrategy.Apply = nil
|
||||
pod := test.NewPod()
|
||||
successfulSyncFailHook := test.NewHook(HookTypeSyncFail)
|
||||
successfulSyncFailHook.SetName("successful-sync-fail-hook")
|
||||
failedSyncFailHook := test.NewHook(HookTypeSyncFail)
|
||||
failedSyncFailHook.SetName("failed-sync-fail-hook")
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{Target: pod}},
|
||||
hooks: []*unstructured.Unstructured{successfulSyncFailHook, failedSyncFailHook},
|
||||
}
|
||||
|
||||
syncCtx.kubectl = &kubetest.MockKubectlCmd{
|
||||
Commands: map[string]kubetest.KubectlOutput{
|
||||
// Fail operation
|
||||
pod.GetName(): {Err: fmt.Errorf("")},
|
||||
// Fail a single SyncFail hook
|
||||
failedSyncFailHook.GetName(): {Err: fmt.Errorf("")}},
|
||||
}
|
||||
|
||||
syncCtx.sync()
|
||||
syncCtx.sync()
|
||||
|
||||
fmt.Println(syncCtx.syncRes.Resources)
|
||||
fmt.Println(syncCtx.opState.Phase)
|
||||
// Operation as a whole should fail
|
||||
assert.Equal(t, OperationFailed, syncCtx.opState.Phase)
|
||||
// failedSyncFailHook should fail
|
||||
assert.Equal(t, OperationFailed, syncCtx.syncRes.Resources[1].HookPhase)
|
||||
assert.Equal(t, ResultCodeSyncFailed, syncCtx.syncRes.Resources[1].Status)
|
||||
// successfulSyncFailHook should be synced running (it is an nginx pod)
|
||||
assert.Equal(t, OperationRunning, syncCtx.syncRes.Resources[2].HookPhase)
|
||||
assert.Equal(t, ResultCodeSynced, syncCtx.syncRes.Resources[2].Status)
|
||||
}
|
||||
|
||||
func Test_syncContext_isSelectiveSync(t *testing.T) {
|
||||
type fields struct {
|
||||
compareResult *comparisonResult
|
||||
syncResources []SyncOperationResource
|
||||
}
|
||||
oneSyncResource := []SyncOperationResource{{}}
|
||||
oneResource := func(group, kind, name string, hook bool) *comparisonResult {
|
||||
return &comparisonResult{resources: []v1alpha1.ResourceStatus{{Group: group, Kind: kind, Name: name, Hook: hook}}}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want bool
|
||||
}{
|
||||
{"Empty", fields{}, false},
|
||||
{"OneCompareResult", fields{oneResource("", "", "", false), []SyncOperationResource{}}, true},
|
||||
{"OneSyncResource", fields{&comparisonResult{}, oneSyncResource}, true},
|
||||
{"Equal", fields{oneResource("", "", "", false), oneSyncResource}, false},
|
||||
{"EqualOutOfOrder", fields{&comparisonResult{resources: []v1alpha1.ResourceStatus{{Group: "a"}, {Group: "b"}}}, []SyncOperationResource{{Group: "b"}, {Group: "a"}}}, false},
|
||||
{"KindDifferent", fields{oneResource("foo", "", "", false), oneSyncResource}, true},
|
||||
{"GroupDifferent", fields{oneResource("", "foo", "", false), oneSyncResource}, true},
|
||||
{"NameDifferent", fields{oneResource("", "", "foo", false), oneSyncResource}, true},
|
||||
{"HookIgnored", fields{oneResource("", "", "", true), []SyncOperationResource{}}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sc := &syncContext{
|
||||
compareResult: tt.fields.compareResult,
|
||||
syncResources: tt.fields.syncResources,
|
||||
}
|
||||
if got := sc.isSelectiveSync(); got != tt.want {
|
||||
t.Errorf("syncContext.isSelectiveSync() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_syncContext_liveObj(t *testing.T) {
|
||||
type fields struct {
|
||||
compareResult *comparisonResult
|
||||
}
|
||||
type args struct {
|
||||
obj *unstructured.Unstructured
|
||||
}
|
||||
obj := test.NewPod()
|
||||
obj.SetNamespace("my-ns")
|
||||
|
||||
found := test.NewPod()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *unstructured.Unstructured
|
||||
}{
|
||||
{"None", fields{compareResult: &comparisonResult{managedResources: []managedResource{}}}, args{obj: &unstructured.Unstructured{}}, nil},
|
||||
{"Found", fields{compareResult: &comparisonResult{managedResources: []managedResource{{Group: obj.GroupVersionKind().Group, Kind: obj.GetKind(), Namespace: obj.GetNamespace(), Name: obj.GetName(), Live: found}}}}, args{obj: obj}, found},
|
||||
{"EmptyNamespace", fields{compareResult: &comparisonResult{managedResources: []managedResource{{Group: obj.GroupVersionKind().Group, Kind: obj.GetKind(), Name: obj.GetName(), Live: found}}}}, args{obj: obj}, found},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sc := &syncContext{
|
||||
compareResult: tt.fields.compareResult,
|
||||
}
|
||||
if got := sc.liveObj(tt.args.obj); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("syncContext.liveObj() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_syncContext_hasCRDOfGroupKind(t *testing.T) {
|
||||
// target
|
||||
assert.False(t, (&syncContext{compareResult: &comparisonResult{managedResources: []managedResource{{Target: test.NewCRD()}}}}).hasCRDOfGroupKind("", ""))
|
||||
assert.True(t, (&syncContext{compareResult: &comparisonResult{managedResources: []managedResource{{Target: test.NewCRD()}}}}).hasCRDOfGroupKind("argoproj.io", "TestCrd"))
|
||||
// hook
|
||||
assert.False(t, (&syncContext{compareResult: &comparisonResult{hooks: []*unstructured.Unstructured{test.NewCRD()}}}).hasCRDOfGroupKind("", ""))
|
||||
assert.True(t, (&syncContext{compareResult: &comparisonResult{hooks: []*unstructured.Unstructured{test.NewCRD()}}}).hasCRDOfGroupKind("argoproj.io", "TestCrd"))
|
||||
conditions := app.Status.GetConditions(map[v1alpha1.ApplicationConditionType]bool{v1alpha1.ApplicationConditionComparisonError: true})
|
||||
assert.NotEmpty(t, conditions)
|
||||
assert.Equal(t, "abc123", opState.SyncResult.Revision)
|
||||
}
|
||||
|
||||
@@ -1,167 +1 @@
|
||||
# Contributing
|
||||
|
||||
## Before You Start
|
||||
|
||||
You must install and run the ArgoCD using a local Kubernetes (e.g. Docker for Desktop or Minikube) first. This will help you understand the application, but also get your local environment set-up.
|
||||
|
||||
Then, to get a good grounding in Go, try out [the tutorial](https://tour.golang.org/).
|
||||
|
||||
## Pre-requisites
|
||||
|
||||
Install:
|
||||
|
||||
* [docker](https://docs.docker.com/install/#supported-platforms)
|
||||
* [git](https://git-scm.com/) and [git-lfs](https://git-lfs.github.com/)
|
||||
* [golang](https://golang.org/)
|
||||
* [dep](https://github.com/golang/dep)
|
||||
* [ksonnet](https://github.com/ksonnet/ksonnet#install)
|
||||
* [helm](https://github.com/helm/helm/releases)
|
||||
* [kustomize](https://github.com/kubernetes-sigs/kustomize/releases)
|
||||
* [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
|
||||
* [kubectx](https://kubectx.dev)
|
||||
* [minikube](https://kubernetes.io/docs/setup/minikube/) or Docker for Desktop
|
||||
|
||||
Brew users can quickly install the lot:
|
||||
|
||||
```bash
|
||||
brew install go git-lfs kubectl kubectx dep ksonnet/tap/ks kubernetes-helm kustomize
|
||||
```
|
||||
|
||||
Check the versions:
|
||||
|
||||
```bash
|
||||
go version ;# must be v1.12.x
|
||||
helm version ;# must be v2.13.x
|
||||
kustomize version ;# must be v3.1.x
|
||||
```
|
||||
|
||||
Set up environment variables (e.g. is `~/.bashrc`):
|
||||
|
||||
```bash
|
||||
export GOPATH=~/go
|
||||
export PATH=$PATH:$GOPATH/bin
|
||||
```
|
||||
|
||||
Checkout the code:
|
||||
|
||||
```bash
|
||||
go get -u github.com/argoproj/argo-cd
|
||||
cd ~/go/src/github.com/argoproj/argo-cd
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
Ensure dependencies are up to date first:
|
||||
|
||||
```shell
|
||||
dep ensure
|
||||
make dev-tools-image
|
||||
make install-lint-tools
|
||||
go get github.com/mattn/goreman
|
||||
go get github.com/jstemmer/go-junit-report
|
||||
```
|
||||
|
||||
Common make targets:
|
||||
|
||||
* `make codegen` - Run code generation
|
||||
* `make lint` - Lint code
|
||||
* `make test` - Run unit tests
|
||||
* `make cli` - Make the `argocd` CLI tool
|
||||
|
||||
Check out the following [documentation](https://github.com/argoproj/argo-cd/blob/master/docs/developer-guide/test-e2e.md) for instructions on running the e2e tests.
|
||||
|
||||
## Running Locally
|
||||
|
||||
It is much easier to run and debug if you run ArgoCD on your local machine than in the Kubernetes cluster.
|
||||
|
||||
You should scale the deployments to zero:
|
||||
|
||||
```bash
|
||||
kubectl -n argocd scale deployment/argocd-application-controller --replicas 0
|
||||
kubectl -n argocd scale deployment/argocd-dex-server --replicas 0
|
||||
kubectl -n argocd scale deployment/argocd-repo-server --replicas 0
|
||||
kubectl -n argocd scale deployment/argocd-server --replicas 0
|
||||
kubectl -n argocd scale deployment/argocd-redis --replicas 0
|
||||
```
|
||||
|
||||
Download Yarn dependencies and Compile:
|
||||
|
||||
```bash
|
||||
~/go/src/github.com/argoproj/argo-cd/ui
|
||||
yarn install
|
||||
yarn build
|
||||
```
|
||||
|
||||
Then start the services:
|
||||
|
||||
```bash
|
||||
cd ~/go/src/github.com/argoproj/argo-cd
|
||||
make start
|
||||
```
|
||||
|
||||
You can now execute `argocd` command against your locally running ArgoCD by appending `--server localhost:8080 --plaintext --insecure`, e.g.:
|
||||
|
||||
```bash
|
||||
argocd app create guestbook --path guestbook --repo https://github.com/argoproj/argocd-example-apps.git --dest-server https://kubernetes.default.svc --dest-namespace default --server localhost:8080 --plaintext --insecure
|
||||
```
|
||||
|
||||
You can open the UI: [http://localhost:4000](http://localhost:4000)
|
||||
|
||||
As an alternative to using the above command line parameters each time you call `argocd` CLI, you can set the following environment variables:
|
||||
|
||||
```bash
|
||||
export ARGOCD_SERVER=127.0.0.1:8080
|
||||
export ARGOCD_OPTS="--plaintext --insecure"
|
||||
```
|
||||
|
||||
## Running Local Containers
|
||||
|
||||
You may need to run containers locally, so here's how:
|
||||
|
||||
Create login to Docker Hub, then login.
|
||||
|
||||
```bash
|
||||
docker login
|
||||
```
|
||||
|
||||
Add your username as the environment variable, e.g. to your `~/.bash_profile`:
|
||||
|
||||
```bash
|
||||
export IMAGE_NAMESPACE=alexcollinsintuit
|
||||
```
|
||||
|
||||
If you don't want to use `latest` as the image's tag (the default), you can set it from the environment too:
|
||||
|
||||
```bash
|
||||
export IMAGE_TAG=yourtag
|
||||
```
|
||||
|
||||
Build the image:
|
||||
|
||||
```bash
|
||||
DOCKER_PUSH=true make image
|
||||
```
|
||||
|
||||
Update the manifests (be sure to do that from a shell that has above environment variables set)
|
||||
|
||||
```bash
|
||||
make manifests
|
||||
```
|
||||
|
||||
Install the manifests:
|
||||
|
||||
```bash
|
||||
kubectl -n argocd apply --force -f manifests/install.yaml
|
||||
```
|
||||
|
||||
Scale your deployments up:
|
||||
|
||||
```bash
|
||||
kubectl -n argocd scale deployment/argocd-application-controller --replicas 1
|
||||
kubectl -n argocd scale deployment/argocd-dex-server --replicas 1
|
||||
kubectl -n argocd scale deployment/argocd-repo-server --replicas 1
|
||||
kubectl -n argocd scale deployment/argocd-server --replicas 1
|
||||
kubectl -n argocd scale deployment/argocd-redis --replicas 1
|
||||
```
|
||||
|
||||
Now you can set-up the port-forwarding and open the UI or CLI.
|
||||
Please refer to [the Contribution Guide](https://argoproj.github.io/argo-cd/developer-guide/contributing/)
|
||||
|
||||
BIN
docs/assets/azure-api-permissions.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
docs/assets/azure-token-configuration.png
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
docs/assets/favicon.png
Normal file
|
After Width: | Height: | Size: 2.6 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 |