Compare commits
336 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2c19f42ad | ||
|
|
8a3b36bd28 | ||
|
|
35a7350b74 | ||
|
|
241e6d0a6e | ||
|
|
dbd6b406ac | ||
|
|
a069639135 | ||
|
|
76241f9d3b | ||
|
|
bdda410463 | ||
|
|
2faa5c89d1 | ||
|
|
a12b7bdb74 | ||
|
|
9b21c25783 | ||
|
|
e1deca2a9e | ||
|
|
62621428b1 | ||
|
|
ab1f9e4658 | ||
|
|
36d1b42d5c | ||
|
|
8b9d25f6e3 | ||
|
|
323af4d562 | ||
|
|
a946b70b5e | ||
|
|
f9f1bdaabe | ||
|
|
e66b6109f7 | ||
|
|
53897e5019 | ||
|
|
7e0d8a490c | ||
|
|
3684a10332 | ||
|
|
ab80a8126b | ||
|
|
6905196665 | ||
|
|
4e283c14fb | ||
|
|
1b5925a494 | ||
|
|
d500b27f1d | ||
|
|
868b4c4c7c | ||
|
|
4bbce1cb22 | ||
|
|
127f50d697 | ||
|
|
e51aab8d1f | ||
|
|
85a746f861 | ||
|
|
3c2be61827 | ||
|
|
42d572306d | ||
|
|
b3f8e7a02c | ||
|
|
476b09cbbf | ||
|
|
487d6647d5 | ||
|
|
0378819c54 | ||
|
|
bbb925cb63 | ||
|
|
9d1a378ce8 | ||
|
|
e2358cabc9 | ||
|
|
5cd12a3943 | ||
|
|
ebb06b8c89 | ||
|
|
d5d01eca3e | ||
|
|
e13bb79578 | ||
|
|
a8b6282b15 | ||
|
|
fc00d73cf5 | ||
|
|
303d46e67c | ||
|
|
a00798bc5e | ||
|
|
1c4a15129b | ||
|
|
0ca35ef26c | ||
|
|
b38a9aacb2 | ||
|
|
5b239fc1d1 | ||
|
|
fdf7566bb7 | ||
|
|
389858b6df | ||
|
|
20adad76ef | ||
|
|
f37ae1c1f6 | ||
|
|
6edd18bb89 | ||
|
|
4d23fe8108 | ||
|
|
7eeefb003c | ||
|
|
3ae5b2bfe4 | ||
|
|
cdebd26ab4 | ||
|
|
3a088c7c86 | ||
|
|
5a363e9d9f | ||
|
|
57ea24281c | ||
|
|
94d7c10baa | ||
|
|
28027897aa | ||
|
|
0c610f91e5 | ||
|
|
990d9ef92b | ||
|
|
6592773a35 | ||
|
|
beee4de10e | ||
|
|
7fde387dd6 | ||
|
|
64c8ac70fb | ||
|
|
f230df938e | ||
|
|
c9b0fdf1d7 | ||
|
|
ea57d15a80 | ||
|
|
ebc048167c | ||
|
|
3b8405a89b | ||
|
|
66d496d1ef | ||
|
|
9d71ae5ad6 | ||
|
|
85d660f0b9 | ||
|
|
d5286296eb | ||
|
|
916d4aed57 | ||
|
|
59d7b7d2b4 | ||
|
|
18c8716f0a | ||
|
|
a1afe44066 | ||
|
|
1695457f9c | ||
|
|
06bc4064c1 | ||
|
|
d67b4f6c36 | ||
|
|
d2ff5887ac | ||
|
|
205926fa80 | ||
|
|
c4dd9d19c2 | ||
|
|
74fe4af98e | ||
|
|
949808f0b2 | ||
|
|
9ef80ef1f7 | ||
|
|
1801212ac7 | ||
|
|
722d5b02d9 | ||
|
|
9f8505205f | ||
|
|
9e81c38c13 | ||
|
|
ff40297bdc | ||
|
|
c8d74d1a7f | ||
|
|
a3f8ec33f4 | ||
|
|
c7718242f9 | ||
|
|
57eeaa4231 | ||
|
|
7edcf47a03 | ||
|
|
f561f22caa | ||
|
|
dcea620ca6 | ||
|
|
9790a5da9c | ||
|
|
b1d281e7bb | ||
|
|
2e7fa935c4 | ||
|
|
8b69efcdb9 | ||
|
|
8b08a337c5 | ||
|
|
e22f946415 | ||
|
|
3c6715a6f9 | ||
|
|
4cf02fd813 | ||
|
|
17d217c2d6 | ||
|
|
32d5a05aef | ||
|
|
e3f3688227 | ||
|
|
ad715565a6 | ||
|
|
d7cad4ac6d | ||
|
|
cc6c67d343 | ||
|
|
6ada626dda | ||
|
|
5bc59003af | ||
|
|
fe583c2f5d | ||
|
|
539281f89e | ||
|
|
e5ea3fe1fb | ||
|
|
f36ea4646d | ||
|
|
ddcdbaa990 | ||
|
|
857ce87f00 | ||
|
|
4643a1c26d | ||
|
|
3bf8dc6fb0 | ||
|
|
f62559128d | ||
|
|
63fe5f32ba | ||
|
|
a6cb9987a9 | ||
|
|
802edf3202 | ||
|
|
6c1e6b1e72 | ||
|
|
3396a604fe | ||
|
|
574446b3c0 | ||
|
|
07e35b1839 | ||
|
|
0989faf4bd | ||
|
|
85bda0f793 | ||
|
|
ac4191ff8e | ||
|
|
08e50d4eb3 | ||
|
|
9c0db45331 | ||
|
|
76da1529d9 | ||
|
|
da6da2a229 | ||
|
|
3cbe3483ca | ||
|
|
bc33f19333 | ||
|
|
189eaf2705 | ||
|
|
cd27e55711 | ||
|
|
6d612b47f6 | ||
|
|
e07953bf74 | ||
|
|
af212ce6b9 | ||
|
|
3911cd48ca | ||
|
|
d4c0ee80ee | ||
|
|
6a0bb821cc | ||
|
|
9a23823f32 | ||
|
|
aea81373eb | ||
|
|
2354965b6f | ||
|
|
c7b48e3f7c | ||
|
|
4e579da8e9 | ||
|
|
04097a1383 | ||
|
|
dbcfccbfc0 | ||
|
|
7e8723d664 | ||
|
|
316cf5c61c | ||
|
|
b1b443543b | ||
|
|
90769d47ed | ||
|
|
f253fcfa86 | ||
|
|
aea2a51eeb | ||
|
|
d0530a6c39 | ||
|
|
21f1c6c977 | ||
|
|
e1c65a0190 | ||
|
|
9783e7e2bf | ||
|
|
e9ae87ae67 | ||
|
|
cda1be72c0 | ||
|
|
66169ba982 | ||
|
|
35adaf04bf | ||
|
|
fca87b7884 | ||
|
|
54b3786f32 | ||
|
|
899abd37b5 | ||
|
|
73f0437ef3 | ||
|
|
f815facf71 | ||
|
|
7ab781d309 | ||
|
|
566a690813 | ||
|
|
2707008c1a | ||
|
|
c4eeb8d3a6 | ||
|
|
3c24681907 | ||
|
|
20bdfeca81 | ||
|
|
0a5023d08e | ||
|
|
0bf38bb93f | ||
|
|
9670f406a3 | ||
|
|
575f7604d6 | ||
|
|
97b45c7a74 | ||
|
|
0ea0404044 | ||
|
|
0fa02b0a55 | ||
|
|
7c787a3bb0 | ||
|
|
ef2501f4b1 | ||
|
|
f56e084bb9 | ||
|
|
c3ed032c79 | ||
|
|
14d0e76f12 | ||
|
|
cc2b7b149e | ||
|
|
9ffba90a73 | ||
|
|
659b16f5f8 | ||
|
|
adc2e9ba54 | ||
|
|
3a7f6d06b1 | ||
|
|
4fa47ce6cd | ||
|
|
fb0aef3d73 | ||
|
|
8cd34503a1 | ||
|
|
e40a046504 | ||
|
|
6710ac15f4 | ||
|
|
026cfc5efd | ||
|
|
880ee7fbaf | ||
|
|
6bf943c156 | ||
|
|
78dfbd37b4 | ||
|
|
03acca8ef7 | ||
|
|
2032688f36 | ||
|
|
3ad462a112 | ||
|
|
9b679ffa75 | ||
|
|
cb4898acb5 | ||
|
|
5077be9482 | ||
|
|
89a5bc87b9 | ||
|
|
a5f22d3841 | ||
|
|
50ac3fd6c5 | ||
|
|
91b0cd0a47 | ||
|
|
9fa1886c02 | ||
|
|
148b90b5b5 | ||
|
|
0715d05733 | ||
|
|
3258f2deee | ||
|
|
72b90f6890 | ||
|
|
addf397b53 | ||
|
|
aeb48b0a69 | ||
|
|
a6ccf924b5 | ||
|
|
e416547192 | ||
|
|
60aa7fb71e | ||
|
|
cbe94440df | ||
|
|
bfe05e4755 | ||
|
|
848a576a05 | ||
|
|
a6da0ca65b | ||
|
|
af195f36f0 | ||
|
|
31e30fbf6e | ||
|
|
f994926487 | ||
|
|
c8ae89f953 | ||
|
|
257c27677a | ||
|
|
73a1a7ce76 | ||
|
|
c2ff8e856b | ||
|
|
5980b604a6 | ||
|
|
d3b670937a | ||
|
|
9593f8d3b3 | ||
|
|
0cfe1cdedf | ||
|
|
39ea6444f9 | ||
|
|
620d956038 | ||
|
|
be77f468a3 | ||
|
|
24eb0b2409 | ||
|
|
b233563e29 | ||
|
|
455837f3e8 | ||
|
|
09808b5016 | ||
|
|
f01df4e686 | ||
|
|
a6db07ff72 | ||
|
|
048e787668 | ||
|
|
3d6c77e3a8 | ||
|
|
11f00c88b5 | ||
|
|
6cbc43e2ae | ||
|
|
84f24cdb6c | ||
|
|
0d8011da8e | ||
|
|
0ff2533ba0 | ||
|
|
7982a19966 | ||
|
|
f4400b9493 | ||
|
|
4facca0ae7 | ||
|
|
8ac09c9ca9 | ||
|
|
cb6d7eaad2 | ||
|
|
4ccb02375f | ||
|
|
329b845f55 | ||
|
|
de29c9d0f5 | ||
|
|
7065229a45 | ||
|
|
68a81854c1 | ||
|
|
d06260ebbb | ||
|
|
cc7b83adf3 | ||
|
|
45270ec03f | ||
|
|
42804dbbac | ||
|
|
00eebce95a | ||
|
|
e00607b722 | ||
|
|
8575e3942c | ||
|
|
da04075120 | ||
|
|
0b684db148 | ||
|
|
1d5e6a1b90 | ||
|
|
0fab3707cc | ||
|
|
212ca9e37e | ||
|
|
89b33a1442 | ||
|
|
6930ecc947 | ||
|
|
430b933869 | ||
|
|
e328a5d144 | ||
|
|
70ec0d8b29 | ||
|
|
fdea6e2edf | ||
|
|
c096341772 | ||
|
|
424f1e9a3d | ||
|
|
4d795ac381 | ||
|
|
7610a8b2dd | ||
|
|
b8bac1e688 | ||
|
|
afda10bc8f | ||
|
|
a9a28b7e42 | ||
|
|
9d784e7e3f | ||
|
|
a53950e5a8 | ||
|
|
134469c5f0 | ||
|
|
aeb5223169 | ||
|
|
73590e1a39 | ||
|
|
a0f3903418 | ||
|
|
21cc1ec89b | ||
|
|
0675ff2fb2 | ||
|
|
c72160f681 | ||
|
|
513b0eb51b | ||
|
|
4169697302 | ||
|
|
9c3c2f3f14 | ||
|
|
99426ce659 | ||
|
|
7e4cb92fb8 | ||
|
|
a55087b6fd | ||
|
|
2d73fea0a5 | ||
|
|
5706a17155 | ||
|
|
bbfb96cb01 | ||
|
|
500730ef6c | ||
|
|
078f5ccccf | ||
|
|
cf5d9db5bb | ||
|
|
6c93047367 | ||
|
|
e7b5007361 | ||
|
|
4cb84b37ce | ||
|
|
572d376dab | ||
|
|
941ccda32e | ||
|
|
8d5939f128 | ||
|
|
e8c21ab010 | ||
|
|
0f2a88102d | ||
|
|
e3edd2ced3 | ||
|
|
37641cf2d0 | ||
|
|
2148b593ee | ||
|
|
5ec5aeb002 | ||
|
|
bbdbe364b0 | ||
|
|
8df3bad4c8 |
@@ -11,43 +11,41 @@ commands:
|
||||
git config --global user.name "Your Name"
|
||||
echo "export PATH=/home/circleci/.go_workspace/src/github.com/argoproj/argo-cd/hack:\$PATH" | tee -a $BASH_ENV
|
||||
echo "export GIT_ASKPASS=git-ask-pass.sh" | tee -a $BASH_ENV
|
||||
dep_ensure:
|
||||
restore_vendor:
|
||||
steps:
|
||||
- restore_cache:
|
||||
keys:
|
||||
- vendor-v4-{{ checksum "Gopkg.lock" }}
|
||||
- run:
|
||||
name: Run dep ensure
|
||||
command: dep ensure -v
|
||||
- vendor-v1-{{ checksum "Gopkg.lock" }}-{{ .Environment.CIRCLE_JOB }}
|
||||
save_vendor:
|
||||
steps:
|
||||
- save_cache:
|
||||
key: vendor-v4-{{ checksum "Gopkg.lock" }}
|
||||
key: vendor-v1-{{ checksum "Gopkg.lock" }}-{{ .Environment.CIRCLE_JOB }}
|
||||
paths:
|
||||
- vendor
|
||||
install_golang:
|
||||
steps:
|
||||
- run:
|
||||
name: Install Golang v1.12.6
|
||||
name: Install Golang v1.14
|
||||
command: |
|
||||
go get golang.org/dl/go1.12.6
|
||||
[ -e /home/circleci/sdk/go1.12.6 ] || go1.12.6 download
|
||||
go get golang.org/dl/go1.14
|
||||
[ -e /home/circleci/sdk/go1.14 ] || go1.14 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
|
||||
echo "export PATH=/home/circleci/sdk/go1.14/bin:\$PATH" | tee -a $BASH_ENV
|
||||
save_go_cache:
|
||||
steps:
|
||||
- save_cache:
|
||||
key: go-v18-{{ .Branch }}
|
||||
key: go-v1-{{ .Branch }}-{{ .Environment.CIRCLE_JOB }}
|
||||
# https://circleci.com/docs/2.0/language-go/
|
||||
paths:
|
||||
- /home/circleci/.go_workspace
|
||||
- /home/circleci/.cache/go-build
|
||||
- /home/circleci/sdk/go1.12.6
|
||||
- /home/circleci/sdk/go1.14
|
||||
restore_go_cache:
|
||||
steps:
|
||||
- restore_cache:
|
||||
keys:
|
||||
- go-v18-{{ .Branch }}
|
||||
- go-v18-master
|
||||
- go-v17-{{ .Branch }}
|
||||
- go-v17-master
|
||||
- go-v1-{{ .Branch }}-{{ .Environment.CIRCLE_JOB }}
|
||||
- go-v1-master-{{ .Environment.CIRCLE_JOB }}
|
||||
jobs:
|
||||
codegen:
|
||||
docker:
|
||||
@@ -56,14 +54,15 @@ jobs:
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys: [codegen-v2]
|
||||
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
|
||||
- run: dep ensure -v
|
||||
- save_cache:
|
||||
key: codegen-v2
|
||||
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: helm2 init --client-only
|
||||
- run: make codegen-local
|
||||
- run:
|
||||
name: Check nothing has changed
|
||||
@@ -86,16 +85,18 @@ jobs:
|
||||
- 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
|
||||
key: test-dl-v2
|
||||
- run: sudo ./hack/install.sh kubectl-linux kubectx-linux dep-linux ksonnet-linux helm-linux helm2-linux kustomize-linux
|
||||
- save_cache:
|
||||
key: test-dl-v1
|
||||
key: test-dl-v2
|
||||
paths: [/tmp/dl]
|
||||
- configure_git
|
||||
- run: go get github.com/jstemmer/go-junit-report
|
||||
- dep_ensure
|
||||
- save_go_cache
|
||||
- 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
|
||||
@@ -127,13 +128,14 @@ jobs:
|
||||
- install_golang
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys: [e2e-dl-v1]
|
||||
- run: sudo ./hack/install.sh kubectx-linux dep-linux ksonnet-linux helm-linux kustomize-linux
|
||||
keys: [e2e-dl-v2]
|
||||
- run: sudo ./hack/install.sh kubectx-linux dep-linux ksonnet-linux helm-linux helm2-linux kustomize-linux
|
||||
- run: go get github.com/jstemmer/go-junit-report
|
||||
- save_cache:
|
||||
key: e2e-dl-v10
|
||||
key: e2e-dl-v2
|
||||
paths: [/tmp/dl]
|
||||
- dep_ensure
|
||||
- restore_vendor
|
||||
- run: dep ensure -v
|
||||
- configure_git
|
||||
- run: make cli
|
||||
- run:
|
||||
@@ -177,8 +179,9 @@ jobs:
|
||||
command: PATH=dist:$PATH make test-e2e
|
||||
environment:
|
||||
ARGOCD_OPTS: "--server localhost:8080 --plaintext"
|
||||
ARGOCD_E2E_EXPECT_TIMEOUT: "30"
|
||||
ARGOCD_E2E_K3S: "true"
|
||||
- save_vendor
|
||||
- save_go_cache
|
||||
- store_test_results:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
@@ -199,16 +202,15 @@ jobs:
|
||||
key: yarn-packages-v4-{{ checksum "yarn.lock" }}
|
||||
paths: [~/.cache/yarn, node_modules]
|
||||
- run: yarn test
|
||||
- run: yarn build
|
||||
- run: ./node_modules/.bin/codecov -p ..
|
||||
- run: NODE_ENV='production' yarn build
|
||||
- run: yarn lint
|
||||
workflows:
|
||||
version: 2
|
||||
workflow:
|
||||
jobs:
|
||||
- test
|
||||
- codegen:
|
||||
requires:
|
||||
- test
|
||||
- codegen
|
||||
- ui:
|
||||
requires:
|
||||
- codegen
|
||||
|
||||
@@ -13,5 +13,5 @@ coverage:
|
||||
patch: off
|
||||
project:
|
||||
default:
|
||||
# allow test coverage to drop by 1%, assume that it's typically due to CI problems
|
||||
threshold: 1
|
||||
# allow test coverage to drop by 2%, assume that it's typically due to CI problems
|
||||
threshold: 2
|
||||
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -7,6 +7,7 @@ assignees: ''
|
||||
---
|
||||
Checklist:
|
||||
|
||||
* [ ] I've searched in the docs and FAQ for my answer: http://bit.ly/argocd-faq.
|
||||
* [ ] I've included steps to reproduce the bug.
|
||||
* [ ] I've pasted the output of `argocd version`.
|
||||
|
||||
|
||||
8
.github/pull_request_template.md
vendored
@@ -1,5 +1,7 @@
|
||||
Checklist:
|
||||
|
||||
* [ ] I've created an [enhancement proposal](https://github.com/argoproj/argo-cd/issues/new/choose) and I feel I've gotten a green light from the community.
|
||||
* [ ] My build is green ([troubleshooting builds](https://argoproj.github.io/argo-cd/developer-guide/ci/)).
|
||||
* [ ] Optional. My organisation is added to the README.
|
||||
* [ ] Either (a) I've created an [enhancement proposal](https://github.com/argoproj/argo-cd/issues/new/choose) and discussed it with the community, (b) this is a bug fix, or (c) this does not need to be in the release notes.
|
||||
* [ ] The title of the PR states what changed and the related issues number (used for the release note).
|
||||
* [ ] I've updated both the CLI and UI to expose my feature, or I plan to submit a second PR with them.
|
||||
* [ ] Optional. My organization is added to USERS.md.
|
||||
* [ ] I've signed the CLA and my build is green ([troubleshooting builds](https://argoproj.github.io/argo-cd/developer-guide/ci/)).
|
||||
|
||||
27
.github/workflows/gh-pages.yaml
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.x
|
||||
- name: build
|
||||
run: |
|
||||
pip install mkdocs==1.0.4 mkdocs_material==4.1.1
|
||||
mkdocs build
|
||||
mkdir ./site/.circleci && echo '{version: 2, jobs: {build: {branches: {ignore: gh-pages}}}}' > ./site/.circleci/config.yml
|
||||
- name: deploy
|
||||
uses: peaceiris/actions-gh-pages@v2.5.0
|
||||
env:
|
||||
PERSONAL_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
PUBLISH_BRANCH: gh-pages
|
||||
PUBLISH_DIR: ./site
|
||||
57
.github/workflows/image.yaml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
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'
|
||||
- uses: actions/checkout@master
|
||||
with:
|
||||
path: src/github.com/argoproj/argo-cd
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: src/github.com/argoproj/argo-cd/vendor
|
||||
key: ${{ runner.os }}-go-dep-${{ hashFiles('**/Gopkg.lock') }}
|
||||
# download dependencies
|
||||
- run: mkdir -p $GITHUB_WORKSPACE/bin && curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
|
||||
working-directory: src/github.com/argoproj/argo-cd
|
||||
- run: $GOPATH/bin/dep ensure -v
|
||||
working-directory: ./src/github.com/argoproj/argo-cd
|
||||
|
||||
# get image tag
|
||||
- run: echo ::set-output name=tag::$(cat ./VERSION)-${GITHUB_SHA::8}
|
||||
working-directory: ./src/github.com/argoproj/argo-cd
|
||||
id: image
|
||||
|
||||
# build
|
||||
- run: 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
|
||||
@@ -1,5 +1,5 @@
|
||||
run:
|
||||
deadline: 2m
|
||||
timeout: 2m
|
||||
skip-files:
|
||||
- ".*\\.pb\\.go"
|
||||
skip-dirs:
|
||||
|
||||
421
CHANGELOG.md
@@ -1,5 +1,426 @@
|
||||
# Changelog
|
||||
|
||||
## v1.5.0 (Not released)
|
||||
|
||||
#### 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)
|
||||
|
||||
#### 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.
|
||||
|
||||
#### Enhancements
|
||||
* feat: support helm3 (#2383) (#3178)
|
||||
* feat: Argo CD Service Account / Local Users #3185
|
||||
* feat: Disable Admin Login (fixes #3019) (#3179)
|
||||
* feat(ui): add docs to sync policy options present in create application panel (Close #3098) (#3203)
|
||||
* feat: add "service-account" flag to "cluster add" command (#3183) (#3184)
|
||||
* feat: Supports the validate-false option at an app level. Closes #1063 (#2542)
|
||||
* feat: add dest cluster and namespace in the Events (#3093)
|
||||
* feat: Rollback disables auto sync issue #2441 (#2591)
|
||||
* feat: allow ssh and http repository references in bitbucketserver webhook #2773 (#3036)
|
||||
* feat: Add helm --set-file support (#2751)
|
||||
* feat: Include resource group for Event's InvolvedObject.APIVersion
|
||||
* feat: Add argocd cmd for Windows #2121 (#3015)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- fix: app reconciliation fails with panic: index out of (#3233)
|
||||
- fix: upgrade argoproj/pkg version to fix leaked sensitive information in logs (#3230)
|
||||
- fix: set MaxCallSendMsgSize to MaxGRPCMessageSize for the GRPC caller (#3117)
|
||||
- fix: stop caching helm index (#3193)
|
||||
- fix: dex proxy should forward request to dex preserving the basehref (#3165)
|
||||
- fix: set default login redirect to baseHRef (#3164)
|
||||
- fix: don't double-prepend basehref to redirect URLs (fixes #3137)
|
||||
- fix: ui referring to /api/version using absolute path (#3092)
|
||||
- fix: Unhang UI on long app info items by using more sane URL match pattern (#3159)
|
||||
- fix: Allow multiple hostnames per SSH known hosts entry and also allow IPv6 (#2814) (#3074)
|
||||
- fix: argocd-util backup produced truncated backups. import app status (#3096)
|
||||
- fix: upgrade redis-ha chart and enable haproxy (#3147)
|
||||
- fix: make dex server deployment init container resilient to restarts (#3136)
|
||||
- fix: reduct secret values of manifests stored in git (#3088)
|
||||
- fix: labels not being deleted via UI (#3081)
|
||||
- fix: HTTP|HTTPS|NO_PROXY env variable reading #3055 (#3063)
|
||||
- fix: Correct usage text for repo add command regarding insecure repos (#3068)
|
||||
- fix: Ensure SSH private key is written out with a final newline character (#2890) (#3064)
|
||||
- fix: Handle SSH URLs in 'git@server:org/repo' notation correctly (#3062)
|
||||
- fix sso condition when several sso connectors has been configured (#3057)
|
||||
- fix: Fix bug where the same pointer is used. (#3059)
|
||||
- fix: Opening in new tab bad key binding on Linux (#3020)
|
||||
- fix: K8s secrets for repository credential templates are not deleted when credential template is deleted (#3028)
|
||||
- fix: SSH credential template not working #3016
|
||||
- fix: Unable to parse kubectl pre-release version strings (#3034)
|
||||
- fix: Jsonnet TLA parameters of same type are overwritten (#3022)
|
||||
- fix: Replace aws-iam-authenticator to support IRSA (#3010)
|
||||
- fix: Hide bindPW in dex config (#3025)
|
||||
- fix: SSH repo URL with a user different from `git` is not matched correctly when resolving a webhook (#2988)
|
||||
- fix: JWT invalid => Password for superuser has changed since token issued (#2108)
|
||||
|
||||
#### Contributors
|
||||
* alexandrfox
|
||||
* alexec
|
||||
* alexmt
|
||||
* bergur88
|
||||
* CBytelabs
|
||||
* dbeal-wiser
|
||||
* dnascimento
|
||||
* Elgarni
|
||||
* eSamS
|
||||
* gpaul
|
||||
* jannfis
|
||||
* jdmulloy
|
||||
* machgo
|
||||
* masa213f
|
||||
* matthyx
|
||||
* rayanebel
|
||||
* shelby-moore
|
||||
* tomcruise81
|
||||
* wecger
|
||||
* zeph
|
||||
|
||||
## v1.4.2 (2020-01-24)
|
||||
|
||||
- fix: correctly replace cache in namespace isolation mode (#3023)
|
||||
|
||||
## v1.4.1 (2020-01-23)
|
||||
|
||||
- fix: impossible to config RBAC if group name includes ',' (#3013)
|
||||
|
||||
## v1.4.0 (2020-01-17)
|
||||
|
||||
The v1.4.0 is a stability release that brings multiple bug fixes, security, performance enhancements, and multiple usability improvements.
|
||||
|
||||
#### New Features
|
||||
|
||||
#### Security
|
||||
A number of security enhancements and features have been implemented (thanks to [@jannfis](https://github.com/jannfis) for driving it! ):
|
||||
* **Repository Credential Templates Management UI/CLI**. Now you can use Argo CD CLI or UI to configure
|
||||
[credentials template](https://argoproj.github.io/argo-cd/user-guide/private-repositories/#credential-templates) for multiple repositories!
|
||||
* **X-Frame-Options header on serving static assets**. The X-Frame-Options prevents third party sites to trick users into interacting with the application.
|
||||
* **Tighten AppProject RBAC enforcement**. We've improved the enforcement of access rules specified in the
|
||||
[application project](https://argoproj.github.io/argo-cd/operator-manual/declarative-setup/#projects) configuration.
|
||||
|
||||
#### Namespace Isolation
|
||||
With the namespace isolation feature, you are no longer have to give full read-only cluster access to the Argo CD. Instead, you can give access only to selected namespaces with-in
|
||||
the cluster:
|
||||
|
||||
```bash
|
||||
argocd cluster add <mycluster> --namespace <mynamespace1> --namespace <mynamespace2>
|
||||
```
|
||||
|
||||
This feature is useful if you don't have full cluster access but still want to use Argo CD to manage some cluster namespaces. The feature also improves performance if Argo CD is
|
||||
used to manage a few namespaces of a large cluster.
|
||||
|
||||
#### Reconciliation Performance
|
||||
The Argo CD no longer fork/exec `kubectl` to apply resource changes in the target cluster or convert resource manifest to the required manifest version. This reduces
|
||||
CPU and Memory usage of large Argo CD instances.
|
||||
|
||||
#### Resources Health based Hook Status
|
||||
The existing Argo CD [resource hooks](https://argoproj.github.io/argo-cd/user-guide/resource_hooks/) feature allows running custom logic during the syncing process. You can mark
|
||||
any Kubernetes resource as a hook and Argo CD assess hook status if resource is a `Pod`, `Job` or `Argo Workflow`. In the v1.4.0 release Argo CD is going to leverage resource
|
||||
[health assessment](https://argoproj.github.io/argo-cd/operator-manual/health/) to get sync hook status. This allows using any custom CRD as a sync hook and leverage custom health
|
||||
check logic.
|
||||
|
||||
#### Manifest Generation
|
||||
* **Track Helm Charts By Semantic Version**. You've been able to track charts hosted in Git repositories using branches to tags. This is now possible for Helm charts. You no longer
|
||||
need to choose the exact version, such as v1.4.0 ,instead you can use a semantic version constraint such as v1.4.* and the latest version that matches will be installed.
|
||||
* **Build Environment Variables**. Feature allows config management tool to get access to app details during manifest generation via
|
||||
[environment variables](https://argoproj.github.io/argo-cd/user-guide/build-environment/).
|
||||
* **Git submodules**. Argo CD is going to automatically fetch sub-modules if your repository has `.gitmodules` directory.
|
||||
|
||||
#### UI and CLI
|
||||
* **Improved Resource Tree View**. The Application details page got even prettier. The resource view was tuned to fit more resources into the screen, include more information about
|
||||
each resource and don't lose usability at the same time.
|
||||
* **New Account Management CLI Command**. The CLI allows to check which actions are allowed for your account: `argocd account can-i sync applications '*'`
|
||||
|
||||
#### Maintenance Tools
|
||||
The team put more effort into building tools that help to maintain Argo CD itself:
|
||||
* **Bulk Project Editing**. The `argocd-util` allows to add and remove permissions defined in multiple project roles using one command.
|
||||
* **More Prometheus Metrics**. A set of additional metrics that contains useful information managed clusters is exposed by application controller.
|
||||
|
||||
More documentation and tools are coming in patch releases.
|
||||
|
||||
#### Breaking Changes
|
||||
|
||||
The Argo CD deletes all **in-flight** hooks if you terminate running sync operation. The hook state assessment change implemented in this release the Argo CD enables detection of
|
||||
an in-flight state for all Kubernetes resources including `Deployment`, `PVC`, `StatefulSet`, `ReplicaSet` etc. So if you terminate the sync operation that has, for example,
|
||||
`StatefulSet` hook that is `Progressing` it will be deleted. The long-running jobs are not supposed to be used as a sync hook and you should consider using
|
||||
[Sync Waves](https://argoproj.github.io/argo-cd/user-guide/sync-waves/) instead.
|
||||
|
||||
#### Enhancements
|
||||
* feat: Add custom healthchecks for cert-manager v0.11.0 (#2689)
|
||||
* feat: add git submodule support (#2495)
|
||||
* feat: Add repository credential management API and CLI (addresses #2136) (#2207)
|
||||
* feat: add support for --additional-headers cli flag (#2467)
|
||||
* feat: Add support for ssh-with-port repo url (#2866) (#2948)
|
||||
* feat: Add Time to ApplicationCondition. (#2417)
|
||||
* feat: Adds `argocd auth can-i` command. Close #2255
|
||||
* feat: Adds revision history limit. Closes #2790 (#2818)
|
||||
* feat: Adds support for ARGO_CD_[TARGET_REVISION|REVISION] and pass to Custom Tool/Helm/Jsonnet
|
||||
* feat: Adds support for Helm charts to be a semver range. Closes #2552 (#2606)
|
||||
* feat: Adds tracing to key external invocations. (#2811)
|
||||
* feat: argocd-util should allow editing project policies in bulk (#2615)
|
||||
* feat: Displays controllerrevsion's revision in the UI. Closes #2306 (#2702)
|
||||
* feat: Issue #2559 - Add gauge Prometheus metric which represents the number of pending manifest requests. (#2658)
|
||||
* feat: Make ConvertToVersion maybe 1090% faster on average (#2820)
|
||||
* feat: namespace isolation (#2839)
|
||||
* feat: removes redundant mutex usage in controller cache and adds cluster cache metrics (#2898)
|
||||
* feat: Set X-Frame-Options on serving static assets (#2706) (#2711)
|
||||
* feat: Simplify using Argo CD without users/SSO/UI (#2688)
|
||||
* feat: Template Out Data Source in Grafana Dashboard (#2859)
|
||||
* feat: Updates UI icons. Closes #2625 and #2757 (#2653)
|
||||
* feat: use editor arguments in InteractiveEditor (#2833)
|
||||
* feat: Use kubectl apply library instead of forking binary (#2861)
|
||||
* feat: use resource health for hook status evaluation (#2938)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- fix: Adds support for /api/v1/account* via HTTP. Fixes #2664 (#2701)
|
||||
- fix: Allow '@'-character in SSH usernames when connecting a repository (#2612)
|
||||
- fix: Allow dot in project policy. Closes #2724 (#2755)
|
||||
- fix: Allow you to sync local Helm apps. Fixes #2741 (#2747)
|
||||
- fix: Allows Helm parameters that contains arrays or maps. (#2525)
|
||||
- fix: application-controller doesn't deal with rm/add same cluster gracefully (x509 unknown) (#2389)
|
||||
- fix: diff local ignore kustomize build options (#2942)
|
||||
- fix: Ensures that Helm charts are correctly resolved before sync. Fixes #2758 (#2760)
|
||||
- fix: Fix 'Open application' link when using basehref (#2729)
|
||||
- fix: fix a bug with cluster add when token secret is not first in list. (#2744)
|
||||
- fix: fix bug where manifests are not cached. Fixes #2770 (#2771)
|
||||
- fix: Fixes bug whereby retry does not work for CLI. Fixes #2767 (#2768)
|
||||
- fix: git contention leads applications into Unknown state (#2877)
|
||||
- fix: Issue #1944 - Gracefully handle missing cached app state (#2464)
|
||||
- fix: Issue #2668 - Delete a specified context (#2669)
|
||||
- fix: Issue #2683 - Make sure app update don't fail due to concurrent modification (#2852)
|
||||
- fix: Issue #2721 Optimize helm repo querying (#2816)
|
||||
- fix: Issue #2853 - Improve application env variables/labels editing (#2856)
|
||||
- fix: Issue 2848 - Application Deployment history panel shows incorrect info for recent releases (#2849)
|
||||
- fix: Make BeforeHookCreation the default. Fixes #2754 (#2759)
|
||||
- fix: No error on `argocd app create` in CLI if `--revision` is omitted #2665
|
||||
- fix: Only delete resources during app delete cascade if permitted to (fixes #2693) (#2695)
|
||||
- fix: prevent user from seeing/deleting resources not permitted in project (#2908) (#2910)
|
||||
- fix: self-heal should retry syncing an application after specified delay
|
||||
- fix: stop logging dex config secrets #(2904) (#2937)
|
||||
- fix: stop using jsondiffpatch on clientside to render resource difference (#2869)
|
||||
- fix: Target Revision truncated #2736
|
||||
- fix: UI should re-trigger SSO login if SSO JWT token expires (#2891)
|
||||
- fix: update argocd-util import was not working properly (#2939)
|
||||
|
||||
#### Contributors
|
||||
|
||||
* Aalok Ahluwalia
|
||||
* Aananth K
|
||||
* Abhishek Jaisingh
|
||||
* Adam Johnson
|
||||
* Alan Tang
|
||||
* Alex Collins
|
||||
* Alexander Matyushentsev
|
||||
* Andrew Waters
|
||||
* Byungjin Park
|
||||
* Christine Banek
|
||||
* Daniel Helfand
|
||||
* David Hong
|
||||
* David J. M. Karlsen
|
||||
* David Maciel
|
||||
* Devan Goodwin
|
||||
* Devin Stein
|
||||
* dthomson25
|
||||
* Gene Liverman
|
||||
* Gregor Krmelj
|
||||
* Guido Maria Serra
|
||||
* Ilir Bekteshi
|
||||
* Imran Ismail
|
||||
* INOUE BANJI
|
||||
* Isaac Gaskin
|
||||
* jannfis
|
||||
* Jeff Hastings
|
||||
* Jesse Suen
|
||||
* John Girvan
|
||||
* Konstantin
|
||||
* Lev Aminov
|
||||
* Manatsawin Hanmongkolchai
|
||||
* Marco Schmid
|
||||
* Masayuki Ishii
|
||||
* Michael Bridgen
|
||||
* Naoki Oketani
|
||||
* niqdev
|
||||
* nitinpatil1992
|
||||
* Olivier Boukili
|
||||
* Olivier Lemasle
|
||||
* Omer Kahani
|
||||
* Paul Brit
|
||||
* Qingbo Zhou
|
||||
* Saradhi Sreegiriraju
|
||||
* Scott Cabrinha
|
||||
* shlo
|
||||
* Simon Behar
|
||||
* stgarf
|
||||
* Yujun Zhang
|
||||
* Zoltán Reegn
|
||||
|
||||
## v1.3.4 (2019-12-05)
|
||||
- #2819 Fixes logging of tracing option in CLI
|
||||
|
||||
## v1.3.3 (2019-12-05)
|
||||
- #2721 High CPU utilisation (5 cores) and spammy logs
|
||||
|
||||
## v1.3.2 (2019-12-03)
|
||||
- #2797 Fix directory traversal edge case and enhance tests
|
||||
|
||||
## v1.3.1 (2019-12-02)
|
||||
- #2664 update account password from API resulted 404
|
||||
- #2724 Can't use `DNS-1123` compliant app name when creating project role
|
||||
- #2726 App list does not show chart for Helm app
|
||||
- #2741 argocd local sync cannot parse kubernetes version
|
||||
- #2754 BeforeHookCreation should be the default hook
|
||||
- #2767 Fix bug whereby retry does not work for CLI
|
||||
- #2770 Always cache miss for manifests
|
||||
- #1345 argocd-application-controller: can not retrieve list of objects using index : Index with name namespace does not exist
|
||||
|
||||
## v1.3.0 (2019-11-13)
|
||||
|
||||
#### New Features
|
||||
|
||||
##### Helm 1st-Class Support
|
||||
|
||||
We know that for many of our users, they want to deploy existing Helm charts using Argo CD. Up until now that has required you to create an Argo CD app in a Git repo that does nothing but point to that chart. Now you can use a Helm chart repository is the same way as a Git repository.
|
||||
|
||||
On top of that, we've improved support for Helm apps. The most common types of Helm hooks such as `pre-install` and `post-install` are supported as well as a the delete policy `before-hook-creation` which makes it easier to work with hooks.
|
||||
|
||||
https://youtu.be/GP7xtrnNznw
|
||||
|
||||
##### Orphan Resources
|
||||
|
||||
Some users would like to make sure that resources in a namespace are managed only by Argo CD. So we've introduced the concept of an "orphan resource" - any resource that is in namespace associated with an app, but not managed by Argo CD. This is enabled in the project settings. Once enabled, Argo CD will show in the app view any resources in the app's namepspace that is not mananged by Argo CD.
|
||||
|
||||
https://youtu.be/9ZoTevVQf5I
|
||||
|
||||
##### Sync Windows
|
||||
|
||||
There may be instances when you want to control the times during which an Argo CD app can sync. Sync Windows now gives you the capability to create windows of time in which apps are either allowed or denied the ability to sync. This can apply to both manual and auto-sync, or just auto-sync. The windows are configured at the project level and assigned to apps using app name, namespace or cluster. Wildcards are supported for all fields.
|
||||
|
||||
#### Enhancements
|
||||
|
||||
* [UI] Add application labels to Applications list and Applications details page (#1099)
|
||||
* Helm repository as first class Argo CD Application source (#1145)
|
||||
* Ability to generate a warn/alert when a namespace deviates from the expected state (#1167)
|
||||
* Improve diff support for resource requests/limits (#1615)
|
||||
* HTTP API should allow JWT to be passed via Authorization header (#1642)
|
||||
* Ability to create & upsert projects from spec (#1852)
|
||||
* Support for in-line block from helm chart values (#1930)
|
||||
* Request OIDC groups claim if groups scope is not supported (#1956)
|
||||
* Add a maintenance window for Applications with automated syncing (#1995)
|
||||
* Support `argocd.argoproj.io/hook-delete-policy: BeforeHookCreation` (#2036)
|
||||
* Support setting Helm string parameters using CLI/UI (#2078)
|
||||
* Config management plugin environment variable UI/CLI support (#2203)
|
||||
* Helm: auto-detect URLs (#2260)
|
||||
* Helm: UI improvements (#2261)
|
||||
* Support `helm template --kube-version ` (#2275)
|
||||
* Use community icons for resources (#2277)
|
||||
* Make `group` optional for `ignoreDifferences` config (#2298)
|
||||
* Update Helm docs (#2315)
|
||||
* Add cluster information into Splunk (#2354)
|
||||
* argocd list command should have filter options like by project (#2396)
|
||||
* Add target/current revision to status badge (#2445)
|
||||
* Update tooling to use Kustomize v3 (#2487)
|
||||
* Update root `Dockerfile` to use the `hack/install.sh` (#2488)
|
||||
* Support and document using HPA for repo-server (#2559)
|
||||
* Upgrade Helm (#2587)
|
||||
* UI fixes for "Sync Apps" panel. (#2604)
|
||||
* Upgrade kustomize from v3.1.0 to v3.2.1 (#2609)
|
||||
* Map helm lifecycle hooks to ArgoCD pre/post/sync hooks (#355)
|
||||
* [UI] Enhance app creation page with Helm parameters overrides (#1059)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- failed parsing on parameters with comma (#1660)
|
||||
- Statefuleset with OnDelete Update Strategy stuck progressing (#1881)
|
||||
- Warning during secret diffing (#1923)
|
||||
- Error message "Unable to load data: key is missing" is confusing (#1944)
|
||||
- OIDC group bindings are truncated (#2006)
|
||||
- Multiple parallel app syncs causes OOM (#2022)
|
||||
- Unknown error when setting params with argocd app set on helm app (#2046)
|
||||
- Endpoint is no longer shown as a child of services (#2060)
|
||||
- SSH known hosts entry cannot be deleted if contains shell pattern in name (#2099)
|
||||
- Application 404s on names with periods (#2114)
|
||||
- Adding certs for hostnames ending with a dot (.) is not possible (#2116)
|
||||
- Fix `TestHookDeleteBeforeCreation` (#2141)
|
||||
- v1.2.0-rc1 nil pointer dereference when syncing (#2146)
|
||||
- Replacing services failure (#2150)
|
||||
- 1.2.0-rc1 - Authentication Required error in Repo Server (#2152)
|
||||
- v1.2.0-rc1 Applications List View doesn't work (#2174)
|
||||
- Manual sync does not trigger Presync hooks (#2185)
|
||||
- SyncError app condition disappears during app reconciliation (#2192)
|
||||
- argocd app wait\sync prints 'Unknown' for resources without health (#2198)
|
||||
- 1.2.0-rc2 Warning during secret diffing (#2206)
|
||||
- SSO redirect url is incorrect if configured Argo CD URL has trailing slash (#2212)
|
||||
- Application summary diff page shows hooks (#2215)
|
||||
- An app with a single resource and Sync hook remains progressing (#2216)
|
||||
- CONTRIBUTING documentation outdated (#2231)
|
||||
- v1.2.0-rc2 does not retrieve http(s) based git repository behind the proxy (#2243)
|
||||
- Intermittent "git ls-remote" request failures should not fail app reconciliation (#2245)
|
||||
- Result of ListApps operation for Git repo is cached incorrectly (#2263)
|
||||
- ListApps does not utilize cache (#2287)
|
||||
- Controller panics due to nil pointer error (#2290)
|
||||
- The Helm --kube-version support does not work on GKE: (#2303)
|
||||
- Fixes bug that prevents you creating repos via UI/CLI. (#2308)
|
||||
- The 'helm.repositories' settings is dropped without migration path (#2316)
|
||||
- Badge response does not contain cache control header (#2317)
|
||||
- Inconsistent sync result from UI and CLI (#2321)
|
||||
- Failed edit application with plugin type requiring environment (#2330)
|
||||
- AutoSync doesn't work anymore (#2339)
|
||||
- End-to-End tests not working with Kubernetes v1.16 (#2371)
|
||||
- Creating an application from Helm repository should select "Helm" as source type (#2378)
|
||||
- The parameters of ValidateAccess GRPC method should not be logged (#2386)
|
||||
- Maintenance window meaning is confusing (#2398)
|
||||
- UI bug when targetRevision is ommited (#2407)
|
||||
- Too many vulnerabilities in Docker image (#2425)
|
||||
- proj windows commands not consistent with other commands (#2443)
|
||||
- Custom resource actions cannot be executed from the UI (#2448)
|
||||
- Application controller sometimes accidentally removes duplicated/excluded resource warning condition (#2453)
|
||||
- Logic that checks sync windows state in the cli is incorrect (#2455)
|
||||
- UI don't allow to create window with `* * * * *` schedule (#2475)
|
||||
- Helm Hook is executed twice if annotated with both pre-install and pre-upgrade annotations (#2480)
|
||||
- Impossible to edit chart name using App details page (#2484)
|
||||
- ArgoCD does not provide CSRF protection (#2496)
|
||||
- ArgoCD failing to install CRDs in master from Helm Charts (#2497)
|
||||
- Timestamp in Helm package file name causes error in Application with Helm source (#2549)
|
||||
- Attempting to create a repo with password but not username panics (#2567)
|
||||
- UI incorrectly mark resources as `Required Pruning` (#2577)
|
||||
- argocd app diff prints only first difference (#2616)
|
||||
- Bump min client cache version (#2619)
|
||||
- Cluster list page fails if any cluster is not reachable (#2620)
|
||||
- Repository type should be mandatory for repo add command in CLI (#2622)
|
||||
- Repo server executes unnecessary ls-remotes (#2626)
|
||||
- Application list page incorrectly filter apps by label selector (#2633)
|
||||
- Custom actions are disabled in Argo CD UI (#2635)
|
||||
- Failure of `argocd version` in the self-building container image (#2645)
|
||||
- Application list page is not updated automatically anymore (#2655)
|
||||
- Login regression issues (#2659)
|
||||
- Regression: Cannot return Kustomize version for 3.1.0 (#2662)
|
||||
- API server does not allow creating role with action `action/*` (#2670)
|
||||
- Application controller `kubectl-parallelism-limit` flag is broken (#2673)
|
||||
- Annoying toolbar flickering (#2691)
|
||||
|
||||
## v1.2.5 (2019-10-29)
|
||||
|
||||
- Issue #2339 - Don't update `status.reconciledAt` unless compared with latest git version (#2581)
|
||||
|
||||
## v1.2.4 (2019-10-23)
|
||||
|
||||
- Issue #2185 - Manual sync don't trigger hooks (#2477)
|
||||
- Issue #2339 - Controller should compare with latest git revision if app has changed (#2543)
|
||||
- Unknown child app should not affect app health (#2544)
|
||||
- Redact secrets in dex logs (#2538)
|
||||
|
||||
## v1.2.3 (2019-10-1)
|
||||
* Make argo-cd docker images openshift friendly (#2362) (@duboisf)
|
||||
* Add dest-server and dest-namespace field to reconciliation logs (#2354)
|
||||
|
||||
62
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.0 as builder
|
||||
|
||||
RUN echo 'deb http://deb.debian.org/debian buster-backports main' >> /etc/apt/sources.list
|
||||
|
||||
@@ -23,47 +23,16 @@ RUN apt-get update && apt-get install -y \
|
||||
|
||||
WORKDIR /tmp
|
||||
|
||||
# Install dep
|
||||
ENV DEP_VERSION=0.5.0
|
||||
RUN wget https://github.com/golang/dep/releases/download/v${DEP_VERSION}/dep-linux-amd64 -O /usr/local/bin/dep && \
|
||||
chmod +x /usr/local/bin/dep
|
||||
ADD hack/install.sh .
|
||||
ADD hack/installers installers
|
||||
|
||||
# Install packr
|
||||
ENV PACKR_VERSION=1.21.9
|
||||
RUN wget https://github.com/gobuffalo/packr/releases/download/v${PACKR_VERSION}/packr_${PACKR_VERSION}_linux_amd64.tar.gz && \
|
||||
tar -vxf packr*.tar.gz -C /tmp/ && \
|
||||
mv /tmp/packr /usr/local/bin/packr
|
||||
|
||||
# Install kubectl
|
||||
# NOTE: keep the version synced with https://storage.googleapis.com/kubernetes-release/release/stable.txt
|
||||
ENV KUBECTL_VERSION=1.14.0
|
||||
RUN curl -L -o /usr/local/bin/kubectl -LO https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl && \
|
||||
chmod +x /usr/local/bin/kubectl && \
|
||||
kubectl version --client
|
||||
|
||||
# Install ksonnet
|
||||
ENV KSONNET_VERSION=0.13.1
|
||||
RUN wget https://github.com/ksonnet/ksonnet/releases/download/v${KSONNET_VERSION}/ks_${KSONNET_VERSION}_linux_amd64.tar.gz && \
|
||||
tar -C /tmp/ -xf ks_${KSONNET_VERSION}_linux_amd64.tar.gz && \
|
||||
mv /tmp/ks_${KSONNET_VERSION}_linux_amd64/ks /usr/local/bin/ks && \
|
||||
ks version
|
||||
|
||||
# Install helm
|
||||
ENV HELM_VERSION=2.12.1
|
||||
RUN wget https://storage.googleapis.com/kubernetes-helm/helm-v${HELM_VERSION}-linux-amd64.tar.gz && \
|
||||
tar -C /tmp/ -xf helm-v${HELM_VERSION}-linux-amd64.tar.gz && \
|
||||
mv /tmp/linux-amd64/helm /usr/local/bin/helm && \
|
||||
helm version --client
|
||||
|
||||
ENV KUSTOMIZE_VERSION=3.1.0
|
||||
RUN curl -L -o /usr/local/bin/kustomize https://github.com/kubernetes-sigs/kustomize/releases/download/v${KUSTOMIZE_VERSION}/kustomize_${KUSTOMIZE_VERSION}_linux_amd64 && \
|
||||
chmod +x /usr/local/bin/kustomize && \
|
||||
kustomize version
|
||||
|
||||
# Install AWS IAM Authenticator
|
||||
ENV AWS_IAM_AUTHENTICATOR_VERSION=0.4.0-alpha.1
|
||||
RUN curl -L -o /usr/local/bin/aws-iam-authenticator https://github.com/kubernetes-sigs/aws-iam-authenticator/releases/download/${AWS_IAM_AUTHENTICATOR_VERSION}/aws-iam-authenticator_${AWS_IAM_AUTHENTICATOR_VERSION}_linux_amd64 && \
|
||||
chmod +x /usr/local/bin/aws-iam-authenticator
|
||||
RUN ./install.sh dep-linux
|
||||
RUN ./install.sh packr-linux
|
||||
RUN ./install.sh kubectl-linux
|
||||
RUN ./install.sh ksonnet-linux
|
||||
RUN ./install.sh helm2-linux
|
||||
RUN ./install.sh helm-linux
|
||||
RUN ./install.sh kustomize-linux
|
||||
|
||||
####################################################################################################
|
||||
# Argo CD Base - used as the base for both the release and dev argocd images
|
||||
@@ -81,16 +50,17 @@ 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 && \
|
||||
apt-get clean && \
|
||||
pip3 install awscli==1.17.7 && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
COPY hack/git-ask-pass.sh /usr/local/bin/git-ask-pass.sh
|
||||
COPY --from=builder /usr/local/bin/ks /usr/local/bin/ks
|
||||
COPY --from=builder /usr/local/bin/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
|
||||
@@ -127,7 +97,7 @@ 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.0 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
|
||||
@@ -146,7 +116,8 @@ RUN cd ${GOPATH}/src/dummy && \
|
||||
WORKDIR /go/src/github.com/argoproj/argo-cd
|
||||
COPY . .
|
||||
RUN make cli server controller repo-server argocd-util && \
|
||||
make CLI_NAME=argocd-darwin-amd64 GOOS=darwin cli
|
||||
make CLI_NAME=argocd-darwin-amd64 GOOS=darwin cli && \
|
||||
make CLI_NAME=argocd-windows-amd64.exe GOOS=windows cli
|
||||
|
||||
|
||||
####################################################################################################
|
||||
@@ -155,4 +126,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
|
||||
|
||||
|
||||
493
Gopkg.lock
generated
@@ -25,6 +25,14 @@
|
||||
revision = "d216395917cc49052c7c7094cf57f09657ca08a8"
|
||||
version = "v3.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:63e57618d792cccb87ad7cb8a0602e6205732beb3b01b0ea858fc4a5fd3ce8f1"
|
||||
name = "github.com/MakeNowJust/heredoc"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "efb6ca8de9d5385c3963279701760e37637cf238"
|
||||
version = "v2.0.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:b856d8248663c39265a764561c1a1a149783f6cc815feb54a1f3a591b91f6eca"
|
||||
name = "github.com/Masterminds/semver"
|
||||
@@ -58,24 +66,19 @@
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:0caf9208419fa5db5a0ca7112affaa9550c54291dda8e2abac0c0e76181c959e"
|
||||
name = "github.com/argoproj/argo"
|
||||
packages = ["util"]
|
||||
pruneopts = ""
|
||||
revision = "7ef1cea68c94f7f0e1e2f8bd75bedc5a7df8af90"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:4f6afcf4ebe041b3d4aa7926d09344b48d2f588e1f957526bbbe54f9cbb366a1"
|
||||
digest = "1:fbe4779f247babd0b21e2283d419f848a8dab42c0f6bbdeb88d83f190f52c159"
|
||||
name = "github.com/argoproj/pkg"
|
||||
packages = [
|
||||
"errors",
|
||||
"exec",
|
||||
"grpc/http",
|
||||
"kubeclientmetrics",
|
||||
"rand",
|
||||
"stats",
|
||||
"time",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "38dba6e98495680ff1f8225642b63db10a96bb06"
|
||||
revision = "f46beff7cd548bafc0264b95a4efed645fb1862c"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:d8a2bb36a048d1571bcc1aee208b61f39dc16c6c53823feffd37449dde162507"
|
||||
@@ -94,12 +97,14 @@
|
||||
revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:e04162bd6a6d4950541bae744c968108e14913b1cebccf29f7650b573f44adb3"
|
||||
digest = "1:6e2b0748ea11cffebe87b4a671a44ecfb243141cdd5df54cb44b7e8e93cb7ea3"
|
||||
name = "github.com/casbin/casbin"
|
||||
packages = [
|
||||
".",
|
||||
"config",
|
||||
"effect",
|
||||
"errors",
|
||||
"log",
|
||||
"model",
|
||||
"persist",
|
||||
"persist/file-adapter",
|
||||
@@ -108,8 +113,21 @@
|
||||
"util",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "d71629e497929858300c38cd442098c178121c30"
|
||||
version = "v1.5.0"
|
||||
revision = "aaed1b7a7eac65d37ec4e15e308429fdf0bd6a9e"
|
||||
version = "v1.9.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:9c19f8c33e635e0439c8afc167d6d02e3aa6eea5b69d64880244fd354a99edc4"
|
||||
name = "github.com/chai2010/gettext-go"
|
||||
packages = [
|
||||
"gettext",
|
||||
"gettext/mo",
|
||||
"gettext/plural",
|
||||
"gettext/po",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "bf70f2a70fb1b1f36d90d671a72795984eab0fcb"
|
||||
|
||||
[[projects]]
|
||||
branch = "v2"
|
||||
@@ -135,6 +153,17 @@
|
||||
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
|
||||
version = "v3.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c05f1899f086e3b4613d94d9e6f7ba6f4b6587498a1aa6037c5c294b22f5a743"
|
||||
name = "github.com/docker/distribution"
|
||||
packages = [
|
||||
"digestset",
|
||||
"reference",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "2461543d988979529609e8cb6fca9ca190dc48da"
|
||||
version = "v2.7.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:b021ef379356343bdc13ec101e546b756fcef4b1186d08163bef7d3bc8c1e07f"
|
||||
name = "github.com/docker/docker"
|
||||
@@ -192,12 +221,28 @@
|
||||
version = "v1.9.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:4216202f4088a73e2982df875e2f0d1401137bbc248e57391e70547af167a18a"
|
||||
digest = "1:46ddeb9dd35d875ac7568c4dc1fc96ce424e034bdbb984239d8ffc151398ec01"
|
||||
name = "github.com/evanphx/json-patch"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "72bf35d0ff611848c1dc9df0f976c81192392fa5"
|
||||
version = "v4.1.0"
|
||||
revision = "026c730a0dcc5d11f93f1cf1cc65b01247ea7b6f"
|
||||
version = "v4.5.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:549f95037fea25e00a5341ac6a169a5b3e5306be107f45260440107b779b74f9"
|
||||
name = "github.com/exponent-io/jsonpath"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "d6023ce2651d8eafb5c75bb0c7167536102ec9f5"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:23a5efa4b272df86a8ebffc942f5e0c1aac4b750836037394cc450b6d91e241a"
|
||||
name = "github.com/fatih/camelcase"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "44e46d280b43ec1531bb25252440e34f1b800b65"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:b13707423743d41665fd23f0c36b2f37bb49c30e94adb813319c44188a51ba22"
|
||||
@@ -205,6 +250,7 @@
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "0ca9ea5df5451ffdf184b4428c902747c2c11cd7"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -356,7 +402,7 @@
|
||||
revision = "5a05380e4bc2440e0ec12f54f6f45648dbdd5e55"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:6e73003ecd35f4487a5e88270d3ca0a81bc80dc88053ac7e4dcfec5fba30d918"
|
||||
digest = "1:d69d2ba23955582a64e367ff2b0808cdbd048458c178cea48f11ab8c40bd7aea"
|
||||
name = "github.com/gogo/protobuf"
|
||||
packages = [
|
||||
"gogoproto",
|
||||
@@ -389,8 +435,8 @@
|
||||
"vanity/command",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "636bf0302bc95575d69441b25a2603156ffdddf1"
|
||||
version = "v1.1.1"
|
||||
revision = "5628607bb4c51c3157aacc3a50f0ab707582b805"
|
||||
version = "v1.3.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -423,6 +469,28 @@
|
||||
revision = "aa810b61a9c79d51363740d207bb46cf8e620ed5"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:1e5b1e14524ed08301977b7b8e10c719ed853cbf3f24ecb66fae783a46f207a6"
|
||||
name = "github.com/google/btree"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "4030bb1f1f0c35b30ca7009e9ebd06849dd45306"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:9fcb267c272bc5054564b392e3ff7e65e35400fd9914afb1d169f92b95e7dbc9"
|
||||
name = "github.com/google/go-cmp"
|
||||
packages = [
|
||||
"cmp",
|
||||
"cmp/internal/diff",
|
||||
"cmp/internal/flags",
|
||||
"cmp/internal/function",
|
||||
"cmp/internal/value",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "2d0692c2e9617365a95b295612ac0d4415ba4627"
|
||||
version = "v0.3.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:14d826ee25139b4674e9768ac287a135f4e7c14e1134a5b15e4e152edfd49f41"
|
||||
name = "github.com/google/go-jsonnet"
|
||||
@@ -450,6 +518,14 @@
|
||||
pruneopts = ""
|
||||
revision = "c34317bd91bf98fab745d77b03933cf8769299fe"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ad92aa49f34cbc3546063c7eb2cabb55ee2278b72842eda80e2a20a8a06a8d73"
|
||||
name = "github.com/google/uuid"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "0cd6bf5da1e1c83f8b45653022c74f71af0538a4"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2a131706ff80636629ab6373f2944569b8252ecc018cda8040931b05d32e3c16"
|
||||
name = "github.com/googleapis/gnostic"
|
||||
@@ -470,6 +546,17 @@
|
||||
revision = "66b9c49e59c6c48f0ffce28c2d8b8a5678502c6d"
|
||||
version = "v1.4.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:e1fd67b5695fb12f54f979606c5d650a5aa72ef242f8e71072bfd4f7b5a141a0"
|
||||
name = "github.com/gregjones/httpcache"
|
||||
packages = [
|
||||
".",
|
||||
"diskcache",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "901d90724c7919163f472a9812253fb26761123d"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:9dca8c981b8aed7448d94e78bc68a76784867a38b3036d5aabc0b32d92ffd1f4"
|
||||
@@ -560,6 +647,14 @@
|
||||
pruneopts = ""
|
||||
revision = "d14ea06fba99483203c19d92cfcd13ebe73135f4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:302ad9379eb146668760df4d779a95379acab43ce5f9a28f27f3273f98232020"
|
||||
name = "github.com/jonboulle/clockwork"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "2eee05ed794112d45db504eb05aa693efd2b8b09"
|
||||
version = "v0.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:31c6f3c4f1e15fcc24fcfc9f5f24603ff3963c56d6fa162116493b4025fb6acc"
|
||||
name = "github.com/json-iterator/go"
|
||||
@@ -591,6 +686,14 @@
|
||||
pruneopts = ""
|
||||
revision = "b729f2633dfe35f4d1d8a32385f6685610ce1cb5"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:93018a4331df9925058905133cb997aec8f54d5303f4536a23e49b5648632d06"
|
||||
name = "github.com/liggitt/tabwriter"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "89fcab3d43de07060e4fd4c1547430ed57e87f24"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:ccc20cacf54eb16464dad02efa1c14fa7c0b9e124639b0d2a51dcc87b0154e4c"
|
||||
@@ -651,6 +754,14 @@
|
||||
revision = "4b7aa43c6742a2c18fdef89dd197aaae7dac7ccd"
|
||||
version = "1.0.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:5d9b668b0b4581a978f07e7d2e3314af18eb27b3fb5d19b70185b7c575723d11"
|
||||
name = "github.com/opencontainers/go-digest"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "279bed98673dd5bef374d3b6e4b09e2af76183bf"
|
||||
version = "v1.0.0-rc1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:4c0404dc03d974acd5fcd8b8d3ce687b13bd169db032b89275e8b9d77b98ce8c"
|
||||
name = "github.com/patrickmn/go-cache"
|
||||
@@ -667,6 +778,22 @@
|
||||
revision = "c37440a7cf42ac63b919c752ca73a85067e05992"
|
||||
version = "v0.2.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:5f0faa008e8ff4221b55a1a5057c8b02cb2fd68da6a65c9e31c82b72cbc836d0"
|
||||
name = "github.com/petar/GoLLRB"
|
||||
packages = ["llrb"]
|
||||
pruneopts = ""
|
||||
revision = "33fb24c13b99c46c93183c291836c573ac382536"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:4709c61d984ef9ba99b037b047546d8a576ae984fb49486e48d99658aa750cd5"
|
||||
name = "github.com/peterbourgon/diskv"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "0be1b92a6df0e4f5cb0a5d15fb7f643d0ad93ce6"
|
||||
version = "v3.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:7365acd48986e205ccb8652cc746f09c8b7876030d53710ea6ef7d0bd0dcd7ca"
|
||||
name = "github.com/pkg/errors"
|
||||
@@ -754,6 +881,14 @@
|
||||
revision = "9a47f48565a795472d43519dd49aac781f3034fb"
|
||||
version = "v1.6.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2761e287c811d0948d47d0252b82281eca3801eb3c9d5f9530956643d5b9f430"
|
||||
name = "github.com/russross/blackfriday"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "05f3235734ad95d0016f6a23902f06461fcf567a"
|
||||
version = "v1.5.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:3962f553b77bf6c03fc07cd687a22dd3b00fe11aa14d31194f5505f5bb65cdc8"
|
||||
name = "github.com/sergi/go-diff"
|
||||
@@ -790,11 +925,11 @@
|
||||
version = "v0.1.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:9ba49264cef4386aded205f9cb5b1f2d30f983d7dc37a21c780d9db3edfac9a7"
|
||||
digest = "1:0c63b3c7ad6d825a898f28cb854252a3b29d37700c68a117a977263f5ec94efe"
|
||||
name = "github.com/spf13/cobra"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "fe5e611709b0c57fa4a89136deaa8e1d4004d053"
|
||||
revision = "0.0.5"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:8e243c568f36b09031ec18dff5f7d2769dcf5ca4d624ea511c8e3197dc3d352d"
|
||||
@@ -826,15 +961,15 @@
|
||||
version = "v0.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c587772fb8ad29ad4db67575dad25ba17a51f072ff18a22b4f0257a4d9c24f75"
|
||||
digest = "1:cc4eb6813da8d08694e557fcafae8fcc24f47f61a0717f952da130ca9a486dfc"
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = [
|
||||
"assert",
|
||||
"mock",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
|
||||
version = "v1.2.2"
|
||||
revision = "3ebf1ddaeb260c4b1ae502a01c7844fa8c1fa0e9"
|
||||
version = "v1.5.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:51cf0fca93f4866709ceaf01b750e51d997c299a7bd2edf7ccd79e3b428754ae"
|
||||
@@ -1241,11 +1376,13 @@
|
||||
version = "v2.2.2"
|
||||
|
||||
[[projects]]
|
||||
branch = "release-1.14"
|
||||
digest = "1:d8a6f1ec98713e685346a2e4b46c6ec4a1792a5535f8b0dffe3b1c08c9d69b12"
|
||||
branch = "release-1.16"
|
||||
digest = "1:5e5cfbab57ea5444c1eb295a39fdc403f097f5ace592c829db7b3e0e3ea66903"
|
||||
name = "k8s.io/api"
|
||||
packages = [
|
||||
"admission/v1",
|
||||
"admission/v1beta1",
|
||||
"admissionregistration/v1",
|
||||
"admissionregistration/v1beta1",
|
||||
"apps/v1",
|
||||
"apps/v1beta1",
|
||||
@@ -1265,6 +1402,7 @@
|
||||
"coordination/v1",
|
||||
"coordination/v1beta1",
|
||||
"core/v1",
|
||||
"discovery/v1alpha1",
|
||||
"events/v1beta1",
|
||||
"extensions/v1beta1",
|
||||
"imagepolicy/v1alpha1",
|
||||
@@ -1285,34 +1423,41 @@
|
||||
"storage/v1beta1",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "40a48860b5abbba9aa891b02b32da429b08d96a0"
|
||||
revision = "195af9ec35214c6d98662c5791364285bf2e2cf2"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:49e0fcdcaeaf937c6c608d1da19eb80de74fe990021278d49d46e10288659be6"
|
||||
branch = "release-1.16"
|
||||
digest = "1:7f29d62c07c68767171cf2ed8598e0cb862b99584bb8beb93189e2ed00ac520e"
|
||||
name = "k8s.io/apiextensions-apiserver"
|
||||
packages = [
|
||||
"pkg/apis/apiextensions",
|
||||
"pkg/apis/apiextensions/v1",
|
||||
"pkg/apis/apiextensions/v1beta1",
|
||||
"pkg/client/clientset/clientset",
|
||||
"pkg/client/clientset/clientset/scheme",
|
||||
"pkg/client/clientset/clientset/typed/apiextensions/v1",
|
||||
"pkg/client/clientset/clientset/typed/apiextensions/v1beta1",
|
||||
"pkg/features",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "7f7d2b94eca3a7a1c49840e119a8bc03c3afb1e3"
|
||||
revision = "07afe84a85e43cf2503133660c424a0b594b21db"
|
||||
|
||||
[[projects]]
|
||||
branch = "release-1.14"
|
||||
digest = "1:a802c91b189a31200cfb66744441fe62dac961ec7c5c58c47716570de7da195c"
|
||||
branch = "release-1.16"
|
||||
digest = "1:36db89a45a8cb3d565f7ebfd67dafd42c9c0bbb80d6bbd4991629b39b02a4c64"
|
||||
name = "k8s.io/apimachinery"
|
||||
packages = [
|
||||
"pkg/api/equality",
|
||||
"pkg/api/errors",
|
||||
"pkg/api/meta",
|
||||
"pkg/api/resource",
|
||||
"pkg/api/validation",
|
||||
"pkg/api/validation/path",
|
||||
"pkg/apis/meta/internalversion",
|
||||
"pkg/apis/meta/v1",
|
||||
"pkg/apis/meta/v1/unstructured",
|
||||
"pkg/apis/meta/v1/unstructured/unstructuredscheme",
|
||||
"pkg/apis/meta/v1/validation",
|
||||
"pkg/apis/meta/v1beta1",
|
||||
"pkg/conversion",
|
||||
"pkg/conversion/queryparams",
|
||||
@@ -1331,12 +1476,14 @@
|
||||
"pkg/util/cache",
|
||||
"pkg/util/clock",
|
||||
"pkg/util/diff",
|
||||
"pkg/util/duration",
|
||||
"pkg/util/errors",
|
||||
"pkg/util/framer",
|
||||
"pkg/util/httpstream",
|
||||
"pkg/util/httpstream/spdy",
|
||||
"pkg/util/intstr",
|
||||
"pkg/util/json",
|
||||
"pkg/util/jsonmergepatch",
|
||||
"pkg/util/mergepatch",
|
||||
"pkg/util/naming",
|
||||
"pkg/util/net",
|
||||
@@ -1355,14 +1502,51 @@
|
||||
"third_party/forked/golang/reflect",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "6a84e37a896db9780c75367af8d2ed2bb944022e"
|
||||
revision = "72ed19daf4bb788ae595ae4103c404cb0fa09c84"
|
||||
|
||||
[[projects]]
|
||||
branch = "release-11.0"
|
||||
digest = "1:794140b3ac07405646ea3d4a57e1f6155186e672aed8aa0c996779381cd92fe6"
|
||||
branch = "release-1.16"
|
||||
digest = "1:4e236f3f94cfc5f005ceb143948ad39a4b2ad10373f394b232838f797bddd6ef"
|
||||
name = "k8s.io/apiserver"
|
||||
packages = [
|
||||
"pkg/apis/audit",
|
||||
"pkg/authentication/serviceaccount",
|
||||
"pkg/authentication/user",
|
||||
"pkg/endpoints/request",
|
||||
"pkg/features",
|
||||
"pkg/util/feature",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "ebfe712c1fff40c4800d779470515e6025eda218"
|
||||
|
||||
[[projects]]
|
||||
branch = "release-1.16"
|
||||
digest = "1:b46a88b317c3187b6fa7c5351eca48b35aad182eee371168677747430ff955bb"
|
||||
name = "k8s.io/cli-runtime"
|
||||
packages = [
|
||||
"pkg/genericclioptions",
|
||||
"pkg/kustomize",
|
||||
"pkg/kustomize/k8sdeps",
|
||||
"pkg/kustomize/k8sdeps/configmapandsecret",
|
||||
"pkg/kustomize/k8sdeps/kunstruct",
|
||||
"pkg/kustomize/k8sdeps/kv",
|
||||
"pkg/kustomize/k8sdeps/transformer",
|
||||
"pkg/kustomize/k8sdeps/transformer/hash",
|
||||
"pkg/kustomize/k8sdeps/transformer/patch",
|
||||
"pkg/kustomize/k8sdeps/validator",
|
||||
"pkg/printers",
|
||||
"pkg/resource",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "6bff60de437070d7e8644b7a930837d5de512240"
|
||||
|
||||
[[projects]]
|
||||
branch = "release-13.0"
|
||||
digest = "1:84f90f6a3b5b16f2c57164c5281d302b2647da8f77aa9cb14d5ebeb17fccc25e"
|
||||
name = "k8s.io/client-go"
|
||||
packages = [
|
||||
"discovery",
|
||||
"discovery/cached/disk",
|
||||
"discovery/fake",
|
||||
"dynamic",
|
||||
"dynamic/fake",
|
||||
@@ -1371,6 +1555,8 @@
|
||||
"kubernetes",
|
||||
"kubernetes/fake",
|
||||
"kubernetes/scheme",
|
||||
"kubernetes/typed/admissionregistration/v1",
|
||||
"kubernetes/typed/admissionregistration/v1/fake",
|
||||
"kubernetes/typed/admissionregistration/v1beta1",
|
||||
"kubernetes/typed/admissionregistration/v1beta1/fake",
|
||||
"kubernetes/typed/apps/v1",
|
||||
@@ -1409,6 +1595,8 @@
|
||||
"kubernetes/typed/coordination/v1beta1/fake",
|
||||
"kubernetes/typed/core/v1",
|
||||
"kubernetes/typed/core/v1/fake",
|
||||
"kubernetes/typed/discovery/v1alpha1",
|
||||
"kubernetes/typed/discovery/v1alpha1/fake",
|
||||
"kubernetes/typed/events/v1beta1",
|
||||
"kubernetes/typed/events/v1beta1/fake",
|
||||
"kubernetes/typed/extensions/v1beta1",
|
||||
@@ -1453,6 +1641,15 @@
|
||||
"plugin/pkg/client/auth/oidc",
|
||||
"rest",
|
||||
"rest/watch",
|
||||
"restmapper",
|
||||
"scale",
|
||||
"scale/scheme",
|
||||
"scale/scheme/appsint",
|
||||
"scale/scheme/appsv1beta1",
|
||||
"scale/scheme/appsv1beta2",
|
||||
"scale/scheme/autoscalingv1",
|
||||
"scale/scheme/extensionsint",
|
||||
"scale/scheme/extensionsv1beta1",
|
||||
"testing",
|
||||
"third_party/forked/golang/template",
|
||||
"tools/auth",
|
||||
@@ -1463,8 +1660,10 @@
|
||||
"tools/clientcmd/api/v1",
|
||||
"tools/metrics",
|
||||
"tools/pager",
|
||||
"tools/portforward",
|
||||
"tools/reference",
|
||||
"tools/remotecommand",
|
||||
"tools/watch",
|
||||
"transport",
|
||||
"transport/spdy",
|
||||
"util/cert",
|
||||
@@ -1478,11 +1677,11 @@
|
||||
"util/workqueue",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "11646d1007e006f6f24995cb905c68bc62901c81"
|
||||
revision = "85029d69edeae82e97dd1a0de3b24668cee9a15d"
|
||||
|
||||
[[projects]]
|
||||
branch = "release-1.14"
|
||||
digest = "1:742ce70d2c6de0f02b5331a25d4d549f55de6b214af22044455fd6e6b451cad9"
|
||||
branch = "release-1.16"
|
||||
digest = "1:254da4cb69b3776686b730a206e081e6f8898bb64760619d1895c25c407e718f"
|
||||
name = "k8s.io/code-generator"
|
||||
packages = [
|
||||
"cmd/go-to-protobuf",
|
||||
@@ -1491,7 +1690,15 @@
|
||||
"third_party/forked/golang/reflect",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "50b561225d70b3eb79a1faafd3dfe7b1a62cbe73"
|
||||
revision = "8e001e5d18949be7e823ccb9cfe9b60026e7bda0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:06c18e328063f3612dfda3c4c5e5b8becda1eabceca689335c8d98704dffe70a"
|
||||
name = "k8s.io/component-base"
|
||||
packages = ["featuregate"]
|
||||
pruneopts = ""
|
||||
revision = "435ce712a6949916fa293dc4d3d49429962043d8"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -1529,7 +1736,7 @@
|
||||
revision = "e80910364765199a4baebd4dec54c885fe52b680"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:42ea993b351fdd39b9aad3c9ebe71f2fdb5d1f8d12eed24e71c3dff1a31b2a43"
|
||||
digest = "1:16a343bd9d820ae320de4d1eaa8acc7a214aac4b38fb21d03255d3a457d861df"
|
||||
name = "k8s.io/kube-openapi"
|
||||
packages = [
|
||||
"cmd/openapi-gen",
|
||||
@@ -1538,41 +1745,145 @@
|
||||
"pkg/generators",
|
||||
"pkg/generators/rules",
|
||||
"pkg/util/proto",
|
||||
"pkg/util/proto/validation",
|
||||
"pkg/util/sets",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "411b2483e5034420675ebcdd4a55fc76fe5e55cf"
|
||||
revision = "30be4d16710ac61bce31eb28a01054596fe6a9f1"
|
||||
|
||||
[[projects]]
|
||||
branch = "release-1.14"
|
||||
digest = "1:78aa6079e011ece0d28513c7fe1bd64284fa9eb5d671760803a839ffdf0e9e38"
|
||||
name = "k8s.io/kubernetes"
|
||||
branch = "release-1.16"
|
||||
digest = "1:687af22932f9b53ff2e6755b2eefe160f076d522794abb980f0ddb187bcefacd"
|
||||
name = "k8s.io/kubectl"
|
||||
packages = [
|
||||
"pkg/api/v1/pod",
|
||||
"pkg/apis/apps",
|
||||
"pkg/apis/autoscaling",
|
||||
"pkg/apis/batch",
|
||||
"pkg/apis/core",
|
||||
"pkg/kubectl/scheme",
|
||||
"pkg/kubectl/util/term",
|
||||
"pkg/cmd/apply",
|
||||
"pkg/cmd/delete",
|
||||
"pkg/cmd/util",
|
||||
"pkg/cmd/util/editor",
|
||||
"pkg/cmd/util/editor/crlf",
|
||||
"pkg/cmd/wait",
|
||||
"pkg/describe",
|
||||
"pkg/describe/versioned",
|
||||
"pkg/generated",
|
||||
"pkg/rawhttp",
|
||||
"pkg/scheme",
|
||||
"pkg/util",
|
||||
"pkg/util/certificate",
|
||||
"pkg/util/deployment",
|
||||
"pkg/util/event",
|
||||
"pkg/util/fieldpath",
|
||||
"pkg/util/i18n",
|
||||
"pkg/util/interrupt",
|
||||
"pkg/util/node",
|
||||
"pkg/util/openapi",
|
||||
"pkg/util/openapi/validation",
|
||||
"pkg/util/printers",
|
||||
"pkg/util/qos",
|
||||
"pkg/util/rbac",
|
||||
"pkg/util/resource",
|
||||
"pkg/util/slice",
|
||||
"pkg/util/storage",
|
||||
"pkg/util/templates",
|
||||
"pkg/util/term",
|
||||
"pkg/validation",
|
||||
"pkg/version",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "2d20b5759406ded89f8b25cf085ff4733b144ba5"
|
||||
revision = "14647fd13a8b4cffc5a8f327b0018e037f72e4e8"
|
||||
|
||||
[[projects]]
|
||||
branch = "release-1.16"
|
||||
digest = "1:02241e5570c239d31e52955b1a8e6d603a35fd6542d14e98882fb6c3c4ef3d56"
|
||||
name = "k8s.io/kubernetes"
|
||||
packages = [
|
||||
"pkg/api/legacyscheme",
|
||||
"pkg/api/v1/pod",
|
||||
"pkg/apis/apps",
|
||||
"pkg/apis/apps/install",
|
||||
"pkg/apis/apps/v1",
|
||||
"pkg/apis/apps/v1beta1",
|
||||
"pkg/apis/apps/v1beta2",
|
||||
"pkg/apis/authentication",
|
||||
"pkg/apis/authentication/install",
|
||||
"pkg/apis/authentication/v1",
|
||||
"pkg/apis/authentication/v1beta1",
|
||||
"pkg/apis/authorization",
|
||||
"pkg/apis/authorization/install",
|
||||
"pkg/apis/authorization/v1",
|
||||
"pkg/apis/authorization/v1beta1",
|
||||
"pkg/apis/autoscaling",
|
||||
"pkg/apis/autoscaling/install",
|
||||
"pkg/apis/autoscaling/v1",
|
||||
"pkg/apis/autoscaling/v2beta1",
|
||||
"pkg/apis/autoscaling/v2beta2",
|
||||
"pkg/apis/batch",
|
||||
"pkg/apis/batch/install",
|
||||
"pkg/apis/batch/v1",
|
||||
"pkg/apis/batch/v1beta1",
|
||||
"pkg/apis/batch/v2alpha1",
|
||||
"pkg/apis/certificates",
|
||||
"pkg/apis/certificates/install",
|
||||
"pkg/apis/certificates/v1beta1",
|
||||
"pkg/apis/coordination",
|
||||
"pkg/apis/coordination/install",
|
||||
"pkg/apis/coordination/v1",
|
||||
"pkg/apis/coordination/v1beta1",
|
||||
"pkg/apis/core",
|
||||
"pkg/apis/core/install",
|
||||
"pkg/apis/core/v1",
|
||||
"pkg/apis/events",
|
||||
"pkg/apis/events/install",
|
||||
"pkg/apis/events/v1beta1",
|
||||
"pkg/apis/extensions",
|
||||
"pkg/apis/extensions/install",
|
||||
"pkg/apis/extensions/v1beta1",
|
||||
"pkg/apis/networking",
|
||||
"pkg/apis/policy",
|
||||
"pkg/apis/policy/install",
|
||||
"pkg/apis/policy/v1beta1",
|
||||
"pkg/apis/rbac",
|
||||
"pkg/apis/rbac/install",
|
||||
"pkg/apis/rbac/v1",
|
||||
"pkg/apis/rbac/v1alpha1",
|
||||
"pkg/apis/rbac/v1beta1",
|
||||
"pkg/apis/scheduling",
|
||||
"pkg/apis/scheduling/install",
|
||||
"pkg/apis/scheduling/v1",
|
||||
"pkg/apis/scheduling/v1alpha1",
|
||||
"pkg/apis/scheduling/v1beta1",
|
||||
"pkg/apis/settings",
|
||||
"pkg/apis/settings/install",
|
||||
"pkg/apis/settings/v1alpha1",
|
||||
"pkg/apis/storage",
|
||||
"pkg/apis/storage/install",
|
||||
"pkg/apis/storage/v1",
|
||||
"pkg/apis/storage/v1alpha1",
|
||||
"pkg/apis/storage/v1beta1",
|
||||
"pkg/features",
|
||||
"pkg/kubectl/cmd/auth",
|
||||
"pkg/registry/rbac/reconciliation",
|
||||
"pkg/registry/rbac/validation",
|
||||
"pkg/util/node",
|
||||
"pkg/util/parsers",
|
||||
"pkg/util/slice",
|
||||
"pkg/util/workqueue/prometheus",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "bfafae8f1c2fdf3c3cfef04674db028531a7c098"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:4c5d39f7ca1c940d7e74dbc62d2221e2c59b3d35c54f1fa9c77f3fd3113bbcb1"
|
||||
digest = "1:a8a2e6bbef691323b833d0eb11bb0e570e7eb9619ac76f7b11265530e1cac922"
|
||||
name = "k8s.io/utils"
|
||||
packages = [
|
||||
"buffer",
|
||||
"exec",
|
||||
"integer",
|
||||
"net",
|
||||
"pointer",
|
||||
"trace",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "c55fbcfc754a5b2ec2fbae8fb9dcac36bdba6a12"
|
||||
revision = "6ca3b61696b65b0e81f1a39b4937fc2d2994ed6a"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -1582,6 +1893,37 @@
|
||||
pruneopts = ""
|
||||
revision = "97fed8db84274c421dbfffbb28ec859901556b97"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:0b2daace3dcced8712072529b621360cf520f3c2ead92d755f35a0ec8dca2714"
|
||||
name = "sigs.k8s.io/kustomize"
|
||||
packages = [
|
||||
"pkg/commands/build",
|
||||
"pkg/constants",
|
||||
"pkg/expansion",
|
||||
"pkg/factory",
|
||||
"pkg/fs",
|
||||
"pkg/git",
|
||||
"pkg/gvk",
|
||||
"pkg/ifc",
|
||||
"pkg/ifc/transformer",
|
||||
"pkg/image",
|
||||
"pkg/internal/error",
|
||||
"pkg/loader",
|
||||
"pkg/patch",
|
||||
"pkg/patch/transformer",
|
||||
"pkg/resid",
|
||||
"pkg/resmap",
|
||||
"pkg/resource",
|
||||
"pkg/target",
|
||||
"pkg/transformers",
|
||||
"pkg/transformers/config",
|
||||
"pkg/transformers/config/defaultconfig",
|
||||
"pkg/types",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "a6f65144121d1955266b0cd836ce954c04122dc8"
|
||||
version = "v2.0.3"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:321081b4a44256715f2b68411d8eda9a17f17ebfe6f0cc61d2cc52d11c08acfa"
|
||||
name = "sigs.k8s.io/yaml"
|
||||
@@ -1597,13 +1939,14 @@
|
||||
"bou.ke/monkey",
|
||||
"github.com/Masterminds/semver",
|
||||
"github.com/TomOnTime/utfutil",
|
||||
"github.com/argoproj/argo/util",
|
||||
"github.com/argoproj/pkg/errors",
|
||||
"github.com/argoproj/pkg/exec",
|
||||
"github.com/argoproj/pkg/grpc/http",
|
||||
"github.com/argoproj/pkg/kubeclientmetrics",
|
||||
"github.com/argoproj/pkg/stats",
|
||||
"github.com/argoproj/pkg/time",
|
||||
"github.com/casbin/casbin",
|
||||
"github.com/casbin/casbin/model",
|
||||
"github.com/casbin/casbin/persist",
|
||||
"github.com/coreos/go-oidc",
|
||||
"github.com/dgrijalva/jwt-go",
|
||||
"github.com/dustin/go-humanize",
|
||||
@@ -1627,6 +1970,7 @@
|
||||
"github.com/golang/protobuf/ptypes/empty",
|
||||
"github.com/google/go-jsonnet",
|
||||
"github.com/google/shlex",
|
||||
"github.com/google/uuid",
|
||||
"github.com/grpc-ecosystem/go-grpc-middleware",
|
||||
"github.com/grpc-ecosystem/go-grpc-middleware/auth",
|
||||
"github.com/grpc-ecosystem/go-grpc-middleware/logging",
|
||||
@@ -1692,6 +2036,7 @@
|
||||
"k8s.io/api/batch/v1",
|
||||
"k8s.io/api/core/v1",
|
||||
"k8s.io/api/extensions/v1beta1",
|
||||
"k8s.io/api/networking/v1beta1",
|
||||
"k8s.io/api/rbac/v1",
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1",
|
||||
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset",
|
||||
@@ -1706,10 +2051,14 @@
|
||||
"k8s.io/apimachinery/pkg/runtime/serializer",
|
||||
"k8s.io/apimachinery/pkg/selection",
|
||||
"k8s.io/apimachinery/pkg/types",
|
||||
"k8s.io/apimachinery/pkg/util/intstr",
|
||||
"k8s.io/apimachinery/pkg/util/jsonmergepatch",
|
||||
"k8s.io/apimachinery/pkg/util/runtime",
|
||||
"k8s.io/apimachinery/pkg/util/strategicpatch",
|
||||
"k8s.io/apimachinery/pkg/util/wait",
|
||||
"k8s.io/apimachinery/pkg/watch",
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions",
|
||||
"k8s.io/cli-runtime/pkg/printers",
|
||||
"k8s.io/client-go/discovery",
|
||||
"k8s.io/client-go/discovery/fake",
|
||||
"k8s.io/client-go/dynamic",
|
||||
@@ -1717,6 +2066,7 @@
|
||||
"k8s.io/client-go/informers/core/v1",
|
||||
"k8s.io/client-go/kubernetes",
|
||||
"k8s.io/client-go/kubernetes/fake",
|
||||
"k8s.io/client-go/kubernetes/scheme",
|
||||
"k8s.io/client-go/listers/core/v1",
|
||||
"k8s.io/client-go/plugin/pkg/client/auth/gcp",
|
||||
"k8s.io/client-go/plugin/pkg/client/auth/oidc",
|
||||
@@ -1725,6 +2075,8 @@
|
||||
"k8s.io/client-go/tools/cache",
|
||||
"k8s.io/client-go/tools/clientcmd",
|
||||
"k8s.io/client-go/tools/clientcmd/api",
|
||||
"k8s.io/client-go/tools/portforward",
|
||||
"k8s.io/client-go/transport/spdy",
|
||||
"k8s.io/client-go/util/flowcontrol",
|
||||
"k8s.io/client-go/util/workqueue",
|
||||
"k8s.io/code-generator/cmd/go-to-protobuf",
|
||||
@@ -1733,13 +2085,32 @@
|
||||
"k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1",
|
||||
"k8s.io/kube-openapi/cmd/openapi-gen",
|
||||
"k8s.io/kube-openapi/pkg/common",
|
||||
"k8s.io/kubectl/pkg/cmd/apply",
|
||||
"k8s.io/kubectl/pkg/cmd/util",
|
||||
"k8s.io/kubectl/pkg/scheme",
|
||||
"k8s.io/kubectl/pkg/util/term",
|
||||
"k8s.io/kubernetes/pkg/api/legacyscheme",
|
||||
"k8s.io/kubernetes/pkg/api/v1/pod",
|
||||
"k8s.io/kubernetes/pkg/apis/apps",
|
||||
"k8s.io/kubernetes/pkg/apis/batch",
|
||||
"k8s.io/kubernetes/pkg/apis/apps/install",
|
||||
"k8s.io/kubernetes/pkg/apis/authentication/install",
|
||||
"k8s.io/kubernetes/pkg/apis/authorization/install",
|
||||
"k8s.io/kubernetes/pkg/apis/autoscaling/install",
|
||||
"k8s.io/kubernetes/pkg/apis/batch/install",
|
||||
"k8s.io/kubernetes/pkg/apis/certificates/install",
|
||||
"k8s.io/kubernetes/pkg/apis/coordination/install",
|
||||
"k8s.io/kubernetes/pkg/apis/core",
|
||||
"k8s.io/kubernetes/pkg/kubectl/scheme",
|
||||
"k8s.io/kubernetes/pkg/kubectl/util/term",
|
||||
"k8s.io/kubernetes/pkg/apis/core/install",
|
||||
"k8s.io/kubernetes/pkg/apis/events/install",
|
||||
"k8s.io/kubernetes/pkg/apis/extensions/install",
|
||||
"k8s.io/kubernetes/pkg/apis/policy/install",
|
||||
"k8s.io/kubernetes/pkg/apis/rbac/install",
|
||||
"k8s.io/kubernetes/pkg/apis/scheduling/install",
|
||||
"k8s.io/kubernetes/pkg/apis/settings/install",
|
||||
"k8s.io/kubernetes/pkg/apis/storage/install",
|
||||
"k8s.io/kubernetes/pkg/kubectl/cmd/auth",
|
||||
"k8s.io/kubernetes/pkg/util/node",
|
||||
"k8s.io/kubernetes/pkg/util/slice",
|
||||
"k8s.io/kubernetes/pkg/util/workqueue/prometheus",
|
||||
"k8s.io/utils/pointer",
|
||||
"layeh.com/gopher-json",
|
||||
]
|
||||
|
||||
59
Gopkg.toml
@@ -19,7 +19,7 @@ required = [
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gogo/protobuf"
|
||||
version = "1.1.1"
|
||||
version = "1.3.1"
|
||||
|
||||
# override github.com/grpc-ecosystem/go-grpc-middleware's constraint on master
|
||||
[[override]]
|
||||
@@ -36,28 +36,57 @@ required = [
|
||||
revision = "7858729281ec582767b20e0d696b6041d995d5e0"
|
||||
|
||||
[[override]]
|
||||
branch = "release-1.14"
|
||||
branch = "release-1.16"
|
||||
name = "k8s.io/api"
|
||||
|
||||
[[override]]
|
||||
branch = "release-1.14"
|
||||
branch = "release-1.16"
|
||||
name = "k8s.io/kubernetes"
|
||||
|
||||
[[override]]
|
||||
branch = "release-1.14"
|
||||
branch = "release-1.16"
|
||||
name = "k8s.io/code-generator"
|
||||
|
||||
[[override]]
|
||||
branch = "release-1.14"
|
||||
branch = "release-1.16"
|
||||
name = "k8s.io/apimachinery"
|
||||
|
||||
[[override]]
|
||||
branch = "release-11.0"
|
||||
branch = "release-1.16"
|
||||
name = "k8s.io/apiextensions-apiserver"
|
||||
|
||||
[[override]]
|
||||
branch = "release-1.16"
|
||||
name = "k8s.io/apiserver"
|
||||
|
||||
[[override]]
|
||||
branch = "release-1.16"
|
||||
name = "k8s.io/kubectl"
|
||||
|
||||
[[override]]
|
||||
branch = "release-1.16"
|
||||
name = "k8s.io/cli-runtime"
|
||||
|
||||
[[override]]
|
||||
version = "2.0.3"
|
||||
name = "sigs.k8s.io/kustomize"
|
||||
|
||||
# ASCIIRenderer does not implement blackfriday.Renderer
|
||||
[[override]]
|
||||
name = "github.com/russross/blackfriday"
|
||||
version = "1.5.2"
|
||||
|
||||
[[override]]
|
||||
branch = "release-13.0"
|
||||
name = "k8s.io/client-go"
|
||||
|
||||
[[override]]
|
||||
name = "github.com/casbin/casbin"
|
||||
version = "1.9.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "1.2.2"
|
||||
version = "1.5.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gobuffalo/packr"
|
||||
@@ -71,12 +100,22 @@ required = [
|
||||
branch = "master"
|
||||
name = "github.com/yudai/gojsondiff"
|
||||
|
||||
[[constraint]]
|
||||
# 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 = "fe5e611709b0c57fa4a89136deaa8e1d4004d053"
|
||||
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 = "411b2483e5034420675ebcdd4a55fc76fe5e55cf"
|
||||
revision = "30be4d16710ac61bce31eb28a01054596fe6a9f1"
|
||||
name = "k8s.io/kube-openapi"
|
||||
|
||||
# jsonpatch replace operation does not apply: doc is missing key: /metadata/annotations
|
||||
[[override]]
|
||||
name = "github.com/evanphx/json-patch"
|
||||
version = "v4.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/google/uuid"
|
||||
version = "1.1.1"
|
||||
|
||||
38
Makefile
@@ -9,7 +9,7 @@ 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 "")
|
||||
VOLUME_MOUNT=$(shell if [[ selinuxenabled -eq 0 ]]; then echo ":Z"; elif [[ $(go env GOOS)=="darwin" ]]; 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)"
|
||||
@@ -38,6 +38,8 @@ endif
|
||||
ifneq (${GIT_TAG},)
|
||||
IMAGE_TAG=${GIT_TAG}
|
||||
LDFLAGS += -X ${PACKAGE}.gitTag=${GIT_TAG}
|
||||
else
|
||||
IMAGE_TAG?=latest
|
||||
endif
|
||||
|
||||
ifeq (${DOCKER_PUSH},true)
|
||||
@@ -81,6 +83,7 @@ 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
|
||||
@@ -97,7 +100,7 @@ manifests-local:
|
||||
./hack/update-manifests.sh
|
||||
|
||||
.PHONY: manifests
|
||||
manifests:
|
||||
manifests: dev-tools-image
|
||||
$(call run-in-dev-tool,make manifests-local IMAGE_TAG='${IMAGE_TAG}')
|
||||
|
||||
|
||||
@@ -134,6 +137,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
|
||||
@@ -162,7 +166,9 @@ install-lint-tools:
|
||||
.PHONY: lint
|
||||
lint:
|
||||
golangci-lint --version
|
||||
golangci-lint run --fix --verbose
|
||||
# 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=100 golangci-lint run --fix --verbose
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
@@ -170,11 +176,12 @@ build:
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
./hack/test.sh -coverprofile=coverage.out `go list ./... | grep -v 'test/e2e'`
|
||||
./hack/test.sh -coverprofile=coverage.out `go list ./... | grep -v 'test/e2e'`
|
||||
|
||||
.PHONY: test-e2e
|
||||
test-e2e:
|
||||
./hack/test.sh -timeout 15m ./test/e2e
|
||||
# NO_PROXY ensures all tests don't go out through a proxy if one is configured on the test system
|
||||
NO_PROXY=* ./hack/test.sh -timeout 15m -v ./test/e2e
|
||||
|
||||
.PHONY: start-e2e
|
||||
start-e2e: cli
|
||||
@@ -189,7 +196,7 @@ start-e2e: cli
|
||||
ARGOCD_TLS_DATA_PATH=/tmp/argo-e2e/app/config/tls \
|
||||
ARGOCD_E2E_DISABLE_AUTH=false \
|
||||
ARGOCD_ZJWT_FEATURE_FLAG=always \
|
||||
goreman start
|
||||
goreman start ${ARGOCD_START}
|
||||
|
||||
# Cleans VSCode debug.test files from sub-dirs to prevent them from being included in packr boxes
|
||||
.PHONY: clean-debug
|
||||
@@ -208,7 +215,7 @@ start:
|
||||
kubectl create ns argocd || true
|
||||
kubens argocd
|
||||
ARGOCD_ZJWT_FEATURE_FLAG=always \
|
||||
goreman start
|
||||
goreman start ${ARGOCD_START}
|
||||
|
||||
.PHONY: pre-commit
|
||||
pre-commit: dep-ensure codegen build lint test
|
||||
@@ -221,3 +228,20 @@ release-precheck: manifests
|
||||
|
||||
.PHONY: release
|
||||
release: pre-commit release-precheck image release-cli
|
||||
|
||||
.PHONY: build-docs
|
||||
build-docs:
|
||||
mkdocs build
|
||||
|
||||
.PHONY: serve-docs
|
||||
serve-docs:
|
||||
mkdocs serve
|
||||
|
||||
.PHONY: lint-docs
|
||||
lint-docs:
|
||||
# https://github.com/dkhamsing/awesome_bot
|
||||
find docs -name '*.md' -exec grep -l http {} + | xargs docker run --rm -v $(PWD):/mnt:ro dkhamsing/awesome_bot -t 3 --allow-dupe --allow-redirect --white-list `cat white-list | grep -v "#" | tr "\n" ','` --skip-save-results --
|
||||
|
||||
.PHONY: publish-docs
|
||||
publish-docs: lint-docs
|
||||
mkdocs gh-deploy
|
||||
|
||||
4
OWNERS
@@ -3,10 +3,8 @@ owners:
|
||||
- alexmt
|
||||
- jessesuen
|
||||
|
||||
reviewers:
|
||||
- jannfis
|
||||
|
||||
approvers:
|
||||
- alexec
|
||||
- alexmt
|
||||
- jannfis
|
||||
- jessesuen
|
||||
|
||||
2
Procfile
@@ -1,6 +1,6 @@
|
||||
controller: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true go run ./cmd/argocd-application-controller/main.go --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081}"
|
||||
api-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true go run ./cmd/argocd-server/main.go --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --disable-auth=${ARGOCD_E2E_DISABLE_AUTH:-'true'} --insecure --dex-server http://localhost:${ARGOCD_E2E_DEX_PORT:-5556} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --port ${ARGOCD_E2E_APISERVER_PORT:-8080} --staticassets ui/dist/app"
|
||||
dex: sh -c "go run ./cmd/argocd-util/main.go gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p ${ARGOCD_E2E_DEX_PORT:-5556}:${ARGOCD_E2E_DEX_PORT:-5556} -v `pwd`/dist/dex.yaml:/dex.yaml quay.io/dexidp/dex:v2.14.0 serve /dex.yaml"
|
||||
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.21.0 serve /dex.yaml"
|
||||
redis: docker run --rm --name argocd-redis -i -p ${ARGOCD_E2E_REDIS_PORT:-6379}:${ARGOCD_E2E_REDIS_PORT:-6379} redis:5.0.3-alpine --save "" --appendonly no --port ${ARGOCD_E2E_REDIS_PORT:-6379}
|
||||
repo-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true go run ./cmd/argocd-repo-server/main.go --loglevel debug --port ${ARGOCD_E2E_REPOSERVER_PORT:-8081} --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379}"
|
||||
ui: sh -c 'cd ui && ${ARGOCD_E2E_YARN_CMD:-yarn} start'
|
||||
|
||||
35
README.md
@@ -12,45 +12,21 @@ 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. [ANSTO - Australian Synchrotron](https://www.synchrotron.org.au/)
|
||||
1. [Codility](https://www.codility.com/)
|
||||
1. [Commonbond](https://commonbond.co/)
|
||||
1. [CyberAgent](https://www.cyberagent.co.jp/en/)
|
||||
1. [END.](https://www.endclothing.com/)
|
||||
1. [Future PLC](https://www.futureplc.com/)
|
||||
1. [GMETRI](https://gmetri.com/)
|
||||
1. [Intuit](https://www.intuit.com/)
|
||||
1. [KintoHub](https://www.kintohub.com/)
|
||||
1. [KompiTech GmbH](https://www.kompitech.com/)
|
||||
1. [Lytt](https://www.lytt.co/)
|
||||
1. [Mambu](https://www.mambu.com/)
|
||||
1. [Mirantis](https://mirantis.com/)
|
||||
1. [OpenSaaS Studio](https://opensaas.studio)
|
||||
1. [Optoro](https://www.optoro.com/)
|
||||
1. [Riskified](https://www.riskified.com/)
|
||||
1. [Saildrone](https://www.saildrone.com/)
|
||||
1. [Tesla](https://tesla.com/)
|
||||
1. [tZERO](https://www.tzero.com/)
|
||||
1. [Ticketmaster](https://ticketmaster.com)
|
||||
1. [Yieldlab](https://www.yieldlab.de/)
|
||||
1. [UBIO](https://ub.io/)
|
||||
1. [Volvo Cars](https://www.volvocars.com/)
|
||||
[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/)
|
||||
@@ -59,3 +35,4 @@ To learn more about Argo CD [go to the complete documentation](https://argoproj.
|
||||
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)
|
||||
|
||||
58
USERS.md
Normal file
@@ -0,0 +1,58 @@
|
||||
## Who uses Argo CD?
|
||||
|
||||
As the Argo Community grows, we'd like to keep track of our users. Please send a PR with your organization name if you are using Argo CD.
|
||||
|
||||
Currently, the following organizations are **officially** using Argo CD:
|
||||
|
||||
1. [127Labs](https://127labs.com/)
|
||||
1. [Adevinta](https://www.adevinta.com/)
|
||||
1. [ANSTO - Australian Synchrotron](https://www.synchrotron.org.au/)
|
||||
1. [ARZ Allgemeines Rechenzentrum GmbH ](https://www.arz.at/)
|
||||
1. [Baloise](https://www.baloise.com)
|
||||
1. [BioBox Analytics](https://biobox.io)
|
||||
1. [CARFAX](https://www.carfax.com)
|
||||
1. [Celonis](https://www.celonis.com/)
|
||||
1. [Codility](https://www.codility.com/)
|
||||
1. [Commonbond](https://commonbond.co/)
|
||||
1. [CyberAgent](https://www.cyberagent.co.jp/en/)
|
||||
1. [Cybozu](https://cybozu-global.com)
|
||||
1. [EDF Renewables](https://www.edf-re.com/)
|
||||
1. [Elium](https://www.elium.com)
|
||||
1. [END.](https://www.endclothing.com/)
|
||||
1. [Fave](https://myfave.com)
|
||||
1. [Future PLC](https://www.futureplc.com/)
|
||||
1. [GMETRI](https://gmetri.com/)
|
||||
1. [hipages](https://hipages.com.au/)
|
||||
1. [Intuit](https://www.intuit.com/)
|
||||
1. [KintoHub](https://www.kintohub.com/)
|
||||
1. [KompiTech GmbH](https://www.kompitech.com/)
|
||||
1. [Lytt](https://www.lytt.co/)
|
||||
1. [Major League Baseball](https://mlb.com)
|
||||
1. [Mambu](https://www.mambu.com/)
|
||||
1. [Max Kelsen](https://www.maxkelsen.com/)
|
||||
1. [Mirantis](https://mirantis.com/)
|
||||
1. [MOO Print](https://www.moo.com/)
|
||||
1. [OpenSaaS Studio](https://opensaas.studio)
|
||||
1. [Optoro](https://www.optoro.com/)
|
||||
1. [Peloton Interactive](https://www.onepeloton.com/)
|
||||
1. [Pipefy](https://www.pipefy.com/)
|
||||
1. [Prudential](https://prudential.com.sg)
|
||||
1. [Red Hat](https://www.redhat.com/)
|
||||
1. [Robotinfra](https://www.robotinfra.com)
|
||||
1. [Riskified](https://www.riskified.com/)
|
||||
1. [Saildrone](https://www.saildrone.com/)
|
||||
1. [Saloodo! GmbH](https://www.saloodo.com)
|
||||
1. [Syncier](https://syncier.com/)
|
||||
1. [Tesla](https://tesla.com/)
|
||||
1. [ThousandEyes](https://www.thousandeyes.com/)
|
||||
1. [Ticketmaster](https://ticketmaster.com)
|
||||
1. [Tiger Analytics](https://www.tigeranalytics.com/)
|
||||
1. [Twilio SendGrid](https://sendgrid.com)
|
||||
1. [tZERO](https://www.tzero.com/)
|
||||
1. [UBIO](https://ub.io/)
|
||||
1. [Universidad Mesoamericana](https://www.umes.edu.gt/)
|
||||
1. [Viaduct](https://www.viaduct.ai/)
|
||||
1. [Volvo Cars](https://www.volvocars.com/)
|
||||
1. [Walkbase](https://www.walkbase.com/)
|
||||
1. [Whitehat Berlin](https://whitehat.berlin) by Guido Maria Serra +Fenaroli
|
||||
1. [Yieldlab](https://www.yieldlab.de/)
|
||||
@@ -1,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,7 @@ p, role:readonly, certificates, get, *, allow
|
||||
p, role:readonly, clusters, get, *, allow
|
||||
p, role:readonly, repositories, get, *, allow
|
||||
p, role:readonly, projects, get, *, allow
|
||||
p, role:readonly, accounts, get, *, allow
|
||||
|
||||
p, role:admin, applications, create, */*, allow
|
||||
p, role:admin, applications, update, */*, allow
|
||||
@@ -30,6 +31,7 @@ p, role:admin, repositories, delete, *, allow
|
||||
p, role:admin, projects, create, *, allow
|
||||
p, role:admin, projects, update, *, allow
|
||||
p, role:admin, projects, delete, *, allow
|
||||
p, role:admin, accounts, update, *, allow
|
||||
|
||||
g, role:admin, role:readonly
|
||||
g, admin, role:admin
|
||||
|
||||
|
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/pkg/stats"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
@@ -19,13 +20,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"
|
||||
"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 (
|
||||
@@ -48,7 +49,7 @@ func newCommand() *cobra.Command {
|
||||
glogLevel int
|
||||
metricsPort int
|
||||
kubectlParallelismLimit int64
|
||||
cacheSrc func() (*cache.Cache, error)
|
||||
cacheSrc func() (*appstatecache.Cache, error)
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: cliName,
|
||||
@@ -59,8 +60,7 @@ func newCommand() *cobra.Command {
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
config.QPS = common.K8sClientConfigQPS
|
||||
config.Burst = common.K8sClientConfigBurst
|
||||
errors.CheckError(v1alpha1.SetK8SConfigDefaults(config))
|
||||
|
||||
kubeClient := kubernetes.NewForConfigOrDie(config)
|
||||
appClient := appclientset.NewForConfigOrDie(config)
|
||||
@@ -77,7 +77,7 @@ func newCommand() *cobra.Command {
|
||||
errors.CheckError(err)
|
||||
|
||||
settingsMgr := settings.NewSettingsManager(ctx, kubeClient, namespace)
|
||||
kubectl := kube.KubectlCmd{}
|
||||
kubectl := &kube.KubectlCmd{}
|
||||
appController, err := controller.NewApplicationController(
|
||||
namespace,
|
||||
settingsMgr,
|
||||
@@ -92,7 +92,8 @@ func newCommand() *cobra.Command {
|
||||
kubectlParallelismLimit)
|
||||
errors.CheckError(err)
|
||||
|
||||
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")
|
||||
@@ -116,7 +117,7 @@ func newCommand() *cobra.Command {
|
||||
command.Flags().IntVar(&selfHealTimeoutSeconds, "self-heal-timeout-seconds", 5, "Specifies timeout between application self heal attempts")
|
||||
command.Flags().Int64Var(&kubectlParallelismLimit, "kubectl-parallelism-limit", 20, "Number of allowed concurrent kubectl fork/execs. Any value less the 1 means no limit.")
|
||||
|
||||
cacheSrc = cache.AddCacheFlagsToCmd(&command)
|
||||
cacheSrc = appstatecache.AddCacheFlagsToCmd(&command)
|
||||
return &command
|
||||
}
|
||||
|
||||
|
||||
@@ -7,16 +7,16 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/pkg/stats"
|
||||
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"
|
||||
"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/tls"
|
||||
)
|
||||
|
||||
@@ -31,7 +31,7 @@ func newCommand() *cobra.Command {
|
||||
parallelismLimit int64
|
||||
listenPort int
|
||||
metricsPort int
|
||||
cacheSrc func() (*cache.Cache, error)
|
||||
cacheSrc func() (*reposervercache.Cache, error)
|
||||
tlsConfigCustomizerSrc func() (tls.ConfigCustomizer, error)
|
||||
)
|
||||
var command = cobra.Command{
|
||||
@@ -72,7 +72,7 @@ func newCommand() *cobra.Command {
|
||||
command.Flags().IntVar(&listenPort, "port", common.DefaultPortRepoServer, "Listen on given port for incoming connections")
|
||||
command.Flags().IntVar(&metricsPort, "metrics-port", common.DefaultPortRepoServerMetrics, "Start metrics server on given port")
|
||||
tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(&command)
|
||||
cacheSrc = cache.AddCacheFlagsToCmd(&command)
|
||||
cacheSrc = reposervercache.AddCacheFlagsToCmd(&command)
|
||||
return &command
|
||||
}
|
||||
|
||||
|
||||
@@ -4,18 +4,19 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/pkg/stats"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
"github.com/argoproj/argo-cd/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/server"
|
||||
"github.com/argoproj/argo-cd/util/cache"
|
||||
servercache "github.com/argoproj/argo-cd/server/cache"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
"github.com/argoproj/argo-cd/util/tls"
|
||||
)
|
||||
|
||||
@@ -35,7 +36,8 @@ func NewCommand() *cobra.Command {
|
||||
dexServerAddress string
|
||||
disableAuth bool
|
||||
tlsConfigCustomizerSrc func() (tls.ConfigCustomizer, error)
|
||||
cacheSrc func() (*cache.Cache, error)
|
||||
cacheSrc func() (*servercache.Cache, error)
|
||||
frameOptions string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: cliName,
|
||||
@@ -47,8 +49,7 @@ func NewCommand() *cobra.Command {
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
config.QPS = common.K8sClientConfigQPS
|
||||
config.Burst = common.K8sClientConfigBurst
|
||||
errors.CheckError(v1alpha1.SetK8SConfigDefaults(config))
|
||||
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
@@ -76,6 +77,7 @@ func NewCommand() *cobra.Command {
|
||||
DisableAuth: disableAuth,
|
||||
TLSConfigCustomizer: tlsConfigCustomizer,
|
||||
Cache: cache,
|
||||
XFrameOptions: frameOptions,
|
||||
}
|
||||
|
||||
stats.RegisterStackDumper()
|
||||
@@ -105,7 +107,8 @@ func NewCommand() *cobra.Command {
|
||||
command.Flags().IntVar(&listenPort, "port", common.DefaultPortAPIServer, "Listen on given port")
|
||||
command.Flags().IntVar(&metricsPort, "metrics-port", common.DefaultPortArgoCDAPIServerMetrics, "Start metrics on given port")
|
||||
command.Flags().IntVar(&repoServerTimeoutSeconds, "repo-server-timeout-seconds", 60, "Repo server RPC call timeout seconds.")
|
||||
command.Flags().StringVar(&frameOptions, "x-frame-options", "sameorigin", "Set X-Frame-Options header in HTTP responses to `value`. To disable, set to \"\".")
|
||||
tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(command)
|
||||
cacheSrc = cache.AddCacheFlagsToCmd(command)
|
||||
cacheSrc = servercache.AddCacheFlagsToCmd(command)
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"syscall"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
@@ -24,8 +25,6 @@ import (
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
@@ -73,6 +72,7 @@ func NewCommand() *cobra.Command {
|
||||
command.AddCommand(NewImportCommand())
|
||||
command.AddCommand(NewExportCommand())
|
||||
command.AddCommand(NewClusterConfig())
|
||||
command.AddCommand(NewProjectsCommand())
|
||||
|
||||
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
|
||||
return command
|
||||
@@ -108,7 +108,7 @@ func NewRunDexCommand() *cobra.Command {
|
||||
} else {
|
||||
err = ioutil.WriteFile("/tmp/dex.yaml", dexCfgBytes, 0644)
|
||||
errors.CheckError(err)
|
||||
log.Info(string(dexCfgBytes))
|
||||
log.Debug(redactor(string(dexCfgBytes)))
|
||||
cmd = exec.Command("dex", "serve", "/tmp/dex.yaml")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
@@ -219,6 +219,7 @@ func NewImportCommand() *cobra.Command {
|
||||
os.Exit(1)
|
||||
}
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
config.QPS = 100
|
||||
config.Burst = 50
|
||||
errors.CheckError(err)
|
||||
@@ -241,43 +242,49 @@ func NewImportCommand() *cobra.Command {
|
||||
// pruneObjects tracks live objects and it's current resource version. any remaining
|
||||
// items in this map indicates the resource should be pruned since it no longer appears
|
||||
// in the backup
|
||||
pruneObjects := make(map[kube.ResourceKey]string)
|
||||
pruneObjects := make(map[kube.ResourceKey]unstructured.Unstructured)
|
||||
configMaps, err := acdClients.configMaps.List(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
// referencedSecrets holds any secrets referenced in the argocd-cm configmap. These
|
||||
// secrets need to be imported too
|
||||
var referencedSecrets map[string]bool
|
||||
for _, cm := range configMaps.Items {
|
||||
cmName := cm.GetName()
|
||||
if cmName == common.ArgoCDConfigMapName || cmName == common.ArgoCDRBACConfigMapName {
|
||||
pruneObjects[kube.ResourceKey{Group: "", Kind: "ConfigMap", Name: cm.GetName()}] = cm.GetResourceVersion()
|
||||
if isArgoCDConfigMap(cm.GetName()) {
|
||||
pruneObjects[kube.ResourceKey{Group: "", Kind: "ConfigMap", Name: cm.GetName()}] = cm
|
||||
}
|
||||
if cm.GetName() == common.ArgoCDConfigMapName {
|
||||
referencedSecrets = getReferencedSecrets(cm)
|
||||
}
|
||||
}
|
||||
|
||||
secrets, err := acdClients.secrets.List(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, secret := range secrets.Items {
|
||||
if isArgoCDSecret(nil, secret) {
|
||||
pruneObjects[kube.ResourceKey{Group: "", Kind: "Secret", Name: secret.GetName()}] = secret.GetResourceVersion()
|
||||
if isArgoCDSecret(referencedSecrets, secret) {
|
||||
pruneObjects[kube.ResourceKey{Group: "", Kind: "Secret", Name: secret.GetName()}] = secret
|
||||
}
|
||||
}
|
||||
applications, err := acdClients.applications.List(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, app := range applications.Items {
|
||||
pruneObjects[kube.ResourceKey{Group: "argoproj.io", Kind: "Application", Name: app.GetName()}] = app.GetResourceVersion()
|
||||
pruneObjects[kube.ResourceKey{Group: "argoproj.io", Kind: "Application", Name: app.GetName()}] = app
|
||||
}
|
||||
projects, err := acdClients.projects.List(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, proj := range projects.Items {
|
||||
pruneObjects[kube.ResourceKey{Group: "argoproj.io", Kind: "AppProject", Name: proj.GetName()}] = proj.GetResourceVersion()
|
||||
pruneObjects[kube.ResourceKey{Group: "argoproj.io", Kind: "AppProject", Name: proj.GetName()}] = proj
|
||||
}
|
||||
|
||||
// Create or replace existing object
|
||||
objs, err := kube.SplitYAML(string(input))
|
||||
backupObjects, err := kube.SplitYAML(string(input))
|
||||
errors.CheckError(err)
|
||||
for _, obj := range objs {
|
||||
gvk := obj.GroupVersionKind()
|
||||
key := kube.ResourceKey{Group: gvk.Group, Kind: gvk.Kind, Name: obj.GetName()}
|
||||
resourceVersion, exists := pruneObjects[key]
|
||||
for _, bakObj := range backupObjects {
|
||||
gvk := bakObj.GroupVersionKind()
|
||||
key := kube.ResourceKey{Group: gvk.Group, Kind: gvk.Kind, Name: bakObj.GetName()}
|
||||
liveObj, exists := pruneObjects[key]
|
||||
delete(pruneObjects, key)
|
||||
var dynClient dynamic.ResourceInterface
|
||||
switch obj.GetKind() {
|
||||
switch bakObj.GetKind() {
|
||||
case "Secret":
|
||||
dynClient = acdClients.secrets
|
||||
case "ConfigMap":
|
||||
@@ -289,17 +296,19 @@ func NewImportCommand() *cobra.Command {
|
||||
}
|
||||
if !exists {
|
||||
if !dryRun {
|
||||
_, err = dynClient.Create(obj, metav1.CreateOptions{})
|
||||
_, err = dynClient.Create(bakObj, metav1.CreateOptions{})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
fmt.Printf("%s/%s %s created%s\n", gvk.Group, gvk.Kind, obj.GetName(), dryRunMsg)
|
||||
fmt.Printf("%s/%s %s created%s\n", gvk.Group, gvk.Kind, bakObj.GetName(), dryRunMsg)
|
||||
} else if specsEqual(*bakObj, liveObj) {
|
||||
fmt.Printf("%s/%s %s unchanged%s\n", gvk.Group, gvk.Kind, bakObj.GetName(), dryRunMsg)
|
||||
} else {
|
||||
if !dryRun {
|
||||
obj.SetResourceVersion(resourceVersion)
|
||||
_, err = dynClient.Update(obj, metav1.UpdateOptions{})
|
||||
newLive := updateLive(bakObj, &liveObj)
|
||||
_, err = dynClient.Update(newLive, metav1.UpdateOptions{})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
fmt.Printf("%s/%s %s replaced%s\n", gvk.Group, gvk.Kind, obj.GetName(), dryRunMsg)
|
||||
fmt.Printf("%s/%s %s updated%s\n", gvk.Group, gvk.Kind, bakObj.GetName(), dryRunMsg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,8 +384,14 @@ func NewExportCommand() *cobra.Command {
|
||||
} else {
|
||||
f, err := os.Create(out)
|
||||
errors.CheckError(err)
|
||||
defer util.Close(f)
|
||||
writer = bufio.NewWriter(f)
|
||||
bw := bufio.NewWriter(f)
|
||||
writer = bw
|
||||
defer func() {
|
||||
err = bw.Flush()
|
||||
errors.CheckError(err)
|
||||
err = f.Close()
|
||||
errors.CheckError(err)
|
||||
}()
|
||||
}
|
||||
|
||||
acdClients := newArgoCDClientsets(config, namespace)
|
||||
@@ -427,11 +442,37 @@ func getReferencedSecrets(un unstructured.Unstructured) map[string]bool {
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, &cm)
|
||||
errors.CheckError(err)
|
||||
referencedSecrets := make(map[string]bool)
|
||||
|
||||
// Referenced repository secrets
|
||||
if reposRAW, ok := cm.Data["repositories"]; ok {
|
||||
repoCreds := make([]settings.RepoCredentials, 0)
|
||||
err := yaml.Unmarshal([]byte(reposRAW), &repoCreds)
|
||||
repos := make([]settings.Repository, 0)
|
||||
err := yaml.Unmarshal([]byte(reposRAW), &repos)
|
||||
errors.CheckError(err)
|
||||
for _, cred := range repoCreds {
|
||||
for _, cred := range repos {
|
||||
if cred.PasswordSecret != nil {
|
||||
referencedSecrets[cred.PasswordSecret.Name] = true
|
||||
}
|
||||
if cred.SSHPrivateKeySecret != nil {
|
||||
referencedSecrets[cred.SSHPrivateKeySecret.Name] = true
|
||||
}
|
||||
if cred.UsernameSecret != nil {
|
||||
referencedSecrets[cred.UsernameSecret.Name] = true
|
||||
}
|
||||
if cred.TLSClientCertDataSecret != nil {
|
||||
referencedSecrets[cred.TLSClientCertDataSecret.Name] = true
|
||||
}
|
||||
if cred.TLSClientCertKeySecret != nil {
|
||||
referencedSecrets[cred.TLSClientCertKeySecret.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Referenced repository credentials secrets
|
||||
if reposRAW, ok := cm.Data["repository.credentials"]; ok {
|
||||
creds := make([]settings.RepositoryCredentials, 0)
|
||||
err := yaml.Unmarshal([]byte(reposRAW), &creds)
|
||||
errors.CheckError(err)
|
||||
for _, cred := range creds {
|
||||
if cred.PasswordSecret != nil {
|
||||
referencedSecrets[cred.PasswordSecret.Name] = true
|
||||
}
|
||||
@@ -477,6 +518,73 @@ func isArgoCDSecret(repoSecretRefs map[string]bool, un unstructured.Unstructured
|
||||
return false
|
||||
}
|
||||
|
||||
// isArgoCDConfigMap returns true if the configmap name is one of argo cd's well known configmaps
|
||||
func isArgoCDConfigMap(name string) bool {
|
||||
switch name {
|
||||
case common.ArgoCDConfigMapName, common.ArgoCDRBACConfigMapName, common.ArgoCDKnownHostsConfigMapName, common.ArgoCDTLSCertsConfigMapName:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
// specsEqual returns if the spec, data, labels, annotations, and finalizers of the two
|
||||
// supplied objects are equal, indicating that no update is necessary during importing
|
||||
func specsEqual(left, right unstructured.Unstructured) bool {
|
||||
if !reflect.DeepEqual(left.GetAnnotations(), right.GetAnnotations()) {
|
||||
return false
|
||||
}
|
||||
if !reflect.DeepEqual(left.GetLabels(), right.GetLabels()) {
|
||||
return false
|
||||
}
|
||||
if !reflect.DeepEqual(left.GetFinalizers(), right.GetFinalizers()) {
|
||||
return false
|
||||
}
|
||||
switch left.GetKind() {
|
||||
case "Secret", "ConfigMap":
|
||||
leftData, _, _ := unstructured.NestedMap(left.Object, "data")
|
||||
rightData, _, _ := unstructured.NestedMap(right.Object, "data")
|
||||
return reflect.DeepEqual(leftData, rightData)
|
||||
case "AppProject":
|
||||
leftSpec, _, _ := unstructured.NestedMap(left.Object, "spec")
|
||||
rightSpec, _, _ := unstructured.NestedMap(right.Object, "spec")
|
||||
return reflect.DeepEqual(leftSpec, rightSpec)
|
||||
case "Application":
|
||||
leftSpec, _, _ := unstructured.NestedMap(left.Object, "spec")
|
||||
rightSpec, _, _ := unstructured.NestedMap(right.Object, "spec")
|
||||
leftStatus, _, _ := unstructured.NestedMap(left.Object, "status")
|
||||
rightStatus, _, _ := unstructured.NestedMap(right.Object, "status")
|
||||
// reconciledAt and observedAt are constantly changing and we ignore any diff there
|
||||
delete(leftStatus, "reconciledAt")
|
||||
delete(rightStatus, "reconciledAt")
|
||||
delete(leftStatus, "observedAt")
|
||||
delete(rightStatus, "observedAt")
|
||||
return reflect.DeepEqual(leftSpec, rightSpec) && reflect.DeepEqual(leftStatus, rightStatus)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// updateLive replaces the live object's finalizers, spec, annotations, labels, and data from the
|
||||
// backup object but leaves all other fields intact (status, other metadata, etc...)
|
||||
func updateLive(bak, live *unstructured.Unstructured) *unstructured.Unstructured {
|
||||
newLive := live.DeepCopy()
|
||||
newLive.SetAnnotations(bak.GetAnnotations())
|
||||
newLive.SetLabels(bak.GetLabels())
|
||||
newLive.SetFinalizers(bak.GetFinalizers())
|
||||
switch live.GetKind() {
|
||||
case "Secret", "ConfigMap":
|
||||
newLive.Object["data"] = bak.Object["data"]
|
||||
case "AppProject":
|
||||
newLive.Object["spec"] = bak.Object["spec"]
|
||||
case "Application":
|
||||
newLive.Object["spec"] = bak.Object["spec"]
|
||||
if _, ok := bak.Object["status"]; ok {
|
||||
newLive.Object["status"] = bak.Object["status"]
|
||||
}
|
||||
}
|
||||
return newLive
|
||||
}
|
||||
|
||||
// export writes the unstructured object and removes extraneous cruft from output before writing
|
||||
func export(w io.Writer, un unstructured.Unstructured) {
|
||||
name := un.GetName()
|
||||
@@ -532,6 +640,38 @@ func NewClusterConfig() *cobra.Command {
|
||||
return command
|
||||
}
|
||||
|
||||
func iterateStringFields(obj interface{}, callback func(name string, val string) string) {
|
||||
if mapField, ok := obj.(map[string]interface{}); ok {
|
||||
for field, val := range mapField {
|
||||
if strVal, ok := val.(string); ok {
|
||||
mapField[field] = callback(field, strVal)
|
||||
} else {
|
||||
iterateStringFields(val, callback)
|
||||
}
|
||||
}
|
||||
} else if arrayField, ok := obj.([]interface{}); ok {
|
||||
for i := range arrayField {
|
||||
iterateStringFields(arrayField[i], callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func redactor(dirtyString string) string {
|
||||
config := make(map[string]interface{})
|
||||
err := yaml.Unmarshal([]byte(dirtyString), &config)
|
||||
errors.CheckError(err)
|
||||
iterateStringFields(config, func(name string, val string) string {
|
||||
if name == "clientSecret" || name == "secret" || name == "bindPW" {
|
||||
return "********"
|
||||
} else {
|
||||
return val
|
||||
}
|
||||
})
|
||||
data, err := yaml.Marshal(config)
|
||||
errors.CheckError(err)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := NewCommand().Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
|
||||
90
cmd/argocd-util/main_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var textToRedact = `
|
||||
connectors:
|
||||
- config:
|
||||
clientID: aabbccddeeff00112233
|
||||
clientSecret: |
|
||||
theSecret
|
||||
orgs:
|
||||
- name: your-github-org
|
||||
redirectURI: https://argocd.example.com/api/dex/callback
|
||||
id: github
|
||||
name: GitHub
|
||||
type: github
|
||||
- config:
|
||||
bindDN: uid=serviceaccount,cn=users,dc=example,dc=com
|
||||
bindPW: theSecret
|
||||
host: ldap.example.com:636
|
||||
id: ldap
|
||||
name: LDAP
|
||||
type: ldap
|
||||
grpc:
|
||||
addr: 0.0.0.0:5557
|
||||
issuer: https://argocd.example.com/api/dex
|
||||
oauth2:
|
||||
skipApprovalScreen: true
|
||||
staticClients:
|
||||
- id: argo-cd
|
||||
name: Argo CD
|
||||
redirectURIs:
|
||||
- https://argocd.example.com/auth/callback
|
||||
secret: Dis9M-GA11oTwZVQQWdDklPQw-sWXZkWJFyyEhMs
|
||||
- id: argo-cd-cli
|
||||
name: Argo CD CLI
|
||||
public: true
|
||||
redirectURIs:
|
||||
- http://localhost
|
||||
storage:
|
||||
type: memory
|
||||
web:
|
||||
http: 0.0.0.0:5556`
|
||||
|
||||
var expectedRedaction = `connectors:
|
||||
- config:
|
||||
clientID: aabbccddeeff00112233
|
||||
clientSecret: '********'
|
||||
orgs:
|
||||
- name: your-github-org
|
||||
redirectURI: https://argocd.example.com/api/dex/callback
|
||||
id: github
|
||||
name: GitHub
|
||||
type: github
|
||||
- config:
|
||||
bindDN: uid=serviceaccount,cn=users,dc=example,dc=com
|
||||
bindPW: '********'
|
||||
host: ldap.example.com:636
|
||||
id: ldap
|
||||
name: LDAP
|
||||
type: ldap
|
||||
grpc:
|
||||
addr: 0.0.0.0:5557
|
||||
issuer: https://argocd.example.com/api/dex
|
||||
oauth2:
|
||||
skipApprovalScreen: true
|
||||
staticClients:
|
||||
- id: argo-cd
|
||||
name: Argo CD
|
||||
redirectURIs:
|
||||
- https://argocd.example.com/auth/callback
|
||||
secret: '********'
|
||||
- id: argo-cd-cli
|
||||
name: Argo CD CLI
|
||||
public: true
|
||||
redirectURIs:
|
||||
- http://localhost
|
||||
storage:
|
||||
type: memory
|
||||
web:
|
||||
http: 0.0.0.0:5556
|
||||
`
|
||||
|
||||
func TestSecretsRedactor(t *testing.T) {
|
||||
assert.Equal(t, expectedRedaction, redactor(textToRedact))
|
||||
}
|
||||
192
cmd/argocd-util/projects.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
appclient "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/typed/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/diff"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
func NewProjectsCommand() *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "projects",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
},
|
||||
}
|
||||
|
||||
command.AddCommand(NewUpdatePolicyRuleCommand())
|
||||
return command
|
||||
}
|
||||
|
||||
func globMatch(pattern string, val string) bool {
|
||||
if pattern == "*" {
|
||||
return true
|
||||
}
|
||||
if ok, err := filepath.Match(pattern, val); ok && err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getModification(modification string, resource string, scope string, permission string) (func(string, string) string, error) {
|
||||
switch modification {
|
||||
case "set":
|
||||
if scope == "" {
|
||||
return nil, fmt.Errorf("Flag --group cannot be empty if permission should be set in role")
|
||||
}
|
||||
if permission == "" {
|
||||
return nil, fmt.Errorf("Flag --permission cannot be empty if permission should be set in role")
|
||||
}
|
||||
return func(proj string, action string) string {
|
||||
return fmt.Sprintf("%s, %s, %s/%s, %s", resource, action, proj, scope, permission)
|
||||
}, nil
|
||||
case "remove":
|
||||
return func(proj string, action string) string {
|
||||
return ""
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("modification %s is not supported", modification)
|
||||
}
|
||||
|
||||
func saveProject(updated v1alpha1.AppProject, orig v1alpha1.AppProject, projectsIf appclient.AppProjectInterface, dryRun bool) error {
|
||||
fmt.Printf("===== %s ======\n", updated.Name)
|
||||
target, err := kube.ToUnstructured(&updated)
|
||||
errors.CheckError(err)
|
||||
live, err := kube.ToUnstructured(&orig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = diff.PrintDiff(updated.Name, target, live)
|
||||
if !dryRun {
|
||||
_, err = projectsIf.Update(&updated)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatPolicy(proj string, role string, permission string) string {
|
||||
return fmt.Sprintf("p, proj:%s:%s, %s", proj, role, permission)
|
||||
}
|
||||
|
||||
func split(input string, delimiter string) []string {
|
||||
parts := strings.Split(input, delimiter)
|
||||
for i := range parts {
|
||||
parts[i] = strings.TrimSpace(parts[i])
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
func NewUpdatePolicyRuleCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
resource string
|
||||
scope string
|
||||
rolePattern string
|
||||
permission string
|
||||
dryRun bool
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "update-role-policy PROJECT_GLOB MODIFICATION ACTION",
|
||||
Short: "Implement bulk project role update. Useful to back-fill existing project policies or remove obsolete actions.",
|
||||
Example: ` # Add policy that allows executing any action (action/*) to roles which name matches to *deployer* in all projects
|
||||
argocd-util projects update-role-policy '*' set 'action/*' --role '*deployer*' --resource applications --scope '*' --permission allow
|
||||
|
||||
# Remove policy that which manages running (action/*) from all roles which name matches *deployer* in all projects
|
||||
argocd-util projects update-role-policy '*' remove override --role '*deployer*'
|
||||
`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projectGlob := args[0]
|
||||
modificationType := args[1]
|
||||
action := args[2]
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
config.QPS = 100
|
||||
config.Burst = 50
|
||||
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
appclients := appclientset.NewForConfigOrDie(config)
|
||||
|
||||
modification, err := getModification(modificationType, resource, scope, permission)
|
||||
errors.CheckError(err)
|
||||
projIf := appclients.ArgoprojV1alpha1().AppProjects(namespace)
|
||||
|
||||
err = updateProjects(projIf, projectGlob, rolePattern, action, modification, dryRun)
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&resource, "resource", "", "Resource e.g. 'applications'")
|
||||
command.Flags().StringVar(&scope, "scope", "", "Resource scope e.g. '*'")
|
||||
command.Flags().StringVar(&rolePattern, "role", "*", "Role name pattern e.g. '*deployer*'")
|
||||
command.Flags().StringVar(&permission, "permission", "", "Action permission")
|
||||
command.Flags().BoolVar(&dryRun, "dry-run", true, "Dry run")
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(command)
|
||||
return command
|
||||
}
|
||||
|
||||
func updateProjects(projIf appclient.AppProjectInterface, projectGlob string, rolePattern string, action string, modification func(string, string) string, dryRun bool) error {
|
||||
projects, err := projIf.List(v1.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, proj := range projects.Items {
|
||||
if !globMatch(projectGlob, proj.Name) {
|
||||
continue
|
||||
}
|
||||
origProj := proj.DeepCopy()
|
||||
updated := false
|
||||
for i, role := range proj.Spec.Roles {
|
||||
if !globMatch(rolePattern, role.Name) {
|
||||
continue
|
||||
}
|
||||
actionPolicyIndex := -1
|
||||
for i := range role.Policies {
|
||||
parts := split(role.Policies[i], ",")
|
||||
if len(parts) != 6 || parts[3] != action {
|
||||
continue
|
||||
}
|
||||
actionPolicyIndex = i
|
||||
break
|
||||
}
|
||||
policyPermission := modification(proj.Name, action)
|
||||
if actionPolicyIndex == -1 && policyPermission != "" {
|
||||
updated = true
|
||||
role.Policies = append(role.Policies, formatPolicy(proj.Name, role.Name, policyPermission))
|
||||
} else if actionPolicyIndex > -1 && policyPermission == "" {
|
||||
updated = true
|
||||
role.Policies = append(role.Policies[:actionPolicyIndex], role.Policies[actionPolicyIndex+1:]...)
|
||||
} else if actionPolicyIndex > -1 && policyPermission != "" {
|
||||
updated = true
|
||||
role.Policies[actionPolicyIndex] = formatPolicy(proj.Name, role.Name, policyPermission)
|
||||
}
|
||||
proj.Spec.Roles[i] = role
|
||||
}
|
||||
if updated {
|
||||
err = saveProject(proj, *origProj, projIf, dryRun)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
78
cmd/argocd-util/projects_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/pkg/client/clientset/versioned/fake"
|
||||
)
|
||||
|
||||
const (
|
||||
namespace = "default"
|
||||
)
|
||||
|
||||
func newProj(name string, roleNames ...string) *v1alpha1.AppProject {
|
||||
var roles []v1alpha1.ProjectRole
|
||||
for i := range roleNames {
|
||||
roles = append(roles, v1alpha1.ProjectRole{Name: roleNames[i]})
|
||||
}
|
||||
return &v1alpha1.AppProject{ObjectMeta: v1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
}, Spec: v1alpha1.AppProjectSpec{
|
||||
Roles: roles,
|
||||
}}
|
||||
}
|
||||
|
||||
func TestUpdateProjects_FindMatchingProject(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(newProj("foo", "test"), newProj("bar", "test"))
|
||||
|
||||
modification, err := getModification("set", "*", "*", "allow")
|
||||
assert.NoError(t, err)
|
||||
err = updateProjects(clientset.ArgoprojV1alpha1().AppProjects(namespace), "ba*", "*", "set", modification, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
fooProj, err := clientset.ArgoprojV1alpha1().AppProjects(namespace).Get("foo", v1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, fooProj.Spec.Roles[0].Policies, 0)
|
||||
|
||||
barProj, err := clientset.ArgoprojV1alpha1().AppProjects(namespace).Get("bar", v1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, barProj.Spec.Roles[0].Policies, []string{"p, proj:bar:test, *, set, bar/*, allow"})
|
||||
}
|
||||
|
||||
func TestUpdateProjects_FindMatchingRole(t *testing.T) {
|
||||
clientset := fake.NewSimpleClientset(newProj("proj", "foo", "bar"))
|
||||
|
||||
modification, err := getModification("set", "*", "*", "allow")
|
||||
assert.NoError(t, err)
|
||||
err = updateProjects(clientset.ArgoprojV1alpha1().AppProjects(namespace), "*", "fo*", "set", modification, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
proj, err := clientset.ArgoprojV1alpha1().AppProjects(namespace).Get("proj", v1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, proj.Spec.Roles[0].Policies, []string{"p, proj:proj:foo, *, set, proj/*, allow"})
|
||||
assert.Len(t, proj.Spec.Roles[1].Policies, 0)
|
||||
}
|
||||
|
||||
func TestGetModification_SetPolicy(t *testing.T) {
|
||||
modification, err := getModification("set", "*", "*", "allow")
|
||||
assert.NoError(t, err)
|
||||
policy := modification("proj", "myaction")
|
||||
assert.Equal(t, "*, myaction, proj/*, allow", policy)
|
||||
}
|
||||
|
||||
func TestGetModification_RemovePolicy(t *testing.T) {
|
||||
modification, err := getModification("remove", "*", "*", "allow")
|
||||
assert.NoError(t, err)
|
||||
policy := modification("proj", "myaction")
|
||||
assert.Equal(t, "", policy)
|
||||
}
|
||||
|
||||
func TestGetModification_NotSupported(t *testing.T) {
|
||||
_, err := getModification("bar", "*", "*", "allow")
|
||||
assert.Errorf(t, err, "modification bar is not supported")
|
||||
}
|
||||
@@ -5,9 +5,12 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
timeutil "github.com/argoproj/pkg/time"
|
||||
"github.com/ghodss/yaml"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -17,9 +20,11 @@ import (
|
||||
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 {
|
||||
@@ -33,11 +38,17 @@ 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
|
||||
)
|
||||
@@ -49,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 util.Close(conn)
|
||||
|
||||
if currentPassword == "" {
|
||||
userInfo := getCurrentAccount(acdClient)
|
||||
|
||||
if userInfo.Iss == sessionutil.SessionManagerClaimsIssuer && currentPassword == "" {
|
||||
fmt.Print("*** Enter current password: ")
|
||||
password, err := terminal.ReadPassword(syscall.Stdin)
|
||||
password, err := terminal.ReadPassword(int(os.Stdin.Fd()))
|
||||
errors.CheckError(err)
|
||||
currentPassword = string(password)
|
||||
fmt.Print("\n")
|
||||
}
|
||||
|
||||
if newPassword == "" {
|
||||
var err error
|
||||
newPassword, err = cli.ReadAndConfirmPassword()
|
||||
@@ -66,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
|
||||
}
|
||||
|
||||
@@ -144,3 +161,237 @@ func NewAccountGetUserInfoCommand(clientOpts *argocdclient.ClientOptions) *cobra
|
||||
command.Flags().StringVarP(&output, "output", "o", "", "Output format. One of: yaml, json")
|
||||
return command
|
||||
}
|
||||
|
||||
func NewAccountCanICommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "can-i ACTION RESOURCE SUBRESOURCE",
|
||||
Short: "Can I",
|
||||
Example: fmt.Sprintf(`
|
||||
# Can I sync any app?
|
||||
argocd account can-i sync applications '*'
|
||||
|
||||
# Can I update a project?
|
||||
argocd account can-i update projects 'default'
|
||||
|
||||
# Can I create a cluster?
|
||||
argocd account can-i create clusters '*'
|
||||
|
||||
Actions: %v
|
||||
Resources: %v
|
||||
`, rbacpolicy.Resources, rbacpolicy.Actions),
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
conn, client := argocdclient.NewClientOrDie(clientOpts).NewAccountClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
ctx := context.Background()
|
||||
response, err := client.CanI(ctx, &accountpkg.CanIRequest{
|
||||
Action: args[0],
|
||||
Resource: args[1],
|
||||
Subresource: args[2],
|
||||
})
|
||||
errors.CheckError(err)
|
||||
fmt.Println(response.Value)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func printAccountNames(accounts []*accountpkg.Account) {
|
||||
for _, p := range accounts {
|
||||
fmt.Println(p.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func printAccountsTable(items []*accountpkg.Account) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "NAME\tENABLED\tCAPABILITIES\n")
|
||||
for _, a := range items {
|
||||
fmt.Fprintf(w, "%s\t%v\t%s\n", a.Name, a.Enabled, strings.Join(a.Capabilities, ", "))
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
func NewAccountListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List accounts",
|
||||
Example: "argocd account list",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
|
||||
conn, client := argocdclient.NewClientOrDie(clientOpts).NewAccountClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
ctx := context.Background()
|
||||
response, err := client.ListAccounts(ctx, &accountpkg.ListAccountRequest{})
|
||||
|
||||
errors.CheckError(err)
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(response.Items, output, false)
|
||||
errors.CheckError(err)
|
||||
case "name":
|
||||
printAccountNames(response.Items)
|
||||
case "wide", "":
|
||||
printAccountsTable(response.Items)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|name")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func getCurrentAccount(clientset argocdclient.Client) session.GetUserInfoResponse {
|
||||
conn, client := clientset.NewSessionClientOrDie()
|
||||
defer util.Close(conn)
|
||||
userInfo, err := client.GetUserInfo(context.Background(), &session.GetUserInfoRequest{})
|
||||
errors.CheckError(err)
|
||||
return *userInfo
|
||||
}
|
||||
|
||||
func NewAccountGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
account string
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Get account details",
|
||||
Example: `# Get the currently logged in account details
|
||||
argocd account get
|
||||
|
||||
# Get details for an account by name
|
||||
argocd account get --account <account-name>`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
clientset := argocdclient.NewClientOrDie(clientOpts)
|
||||
|
||||
if account == "" {
|
||||
account = getCurrentAccount(clientset).Username
|
||||
}
|
||||
|
||||
conn, client := clientset.NewAccountClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
acc, err := client.GetAccount(context.Background(), &accountpkg.GetAccountRequest{Name: account})
|
||||
|
||||
errors.CheckError(err)
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(acc, output, true)
|
||||
errors.CheckError(err)
|
||||
case "name":
|
||||
fmt.Println(acc.Name)
|
||||
case "wide", "":
|
||||
printAccountDetails(acc)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|name")
|
||||
cmd.Flags().StringVarP(&account, "account", "a", "", "Account name. Defaults to the current account.")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func printAccountDetails(acc *accountpkg.Account) {
|
||||
fmt.Printf(printOpFmtStr, "Name:", acc.Name)
|
||||
fmt.Printf(printOpFmtStr, "Enabled:", strconv.FormatBool(acc.Enabled))
|
||||
fmt.Printf(printOpFmtStr, "Capabilities:", strings.Join(acc.Capabilities, ", "))
|
||||
fmt.Println("\nTokens:")
|
||||
if len(acc.Tokens) == 0 {
|
||||
fmt.Println("NONE")
|
||||
} else {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "ID\tISSUED AT\tEXPIRING AT\n")
|
||||
for _, t := range acc.Tokens {
|
||||
expiresAtFormatted := "never"
|
||||
if t.ExpiresAt > 0 {
|
||||
expiresAt := time.Unix(t.ExpiresAt, 0)
|
||||
expiresAtFormatted = expiresAt.Format(time.RFC3339)
|
||||
if expiresAt.Before(time.Now()) {
|
||||
expiresAtFormatted = fmt.Sprintf("%s (expired)", expiresAtFormatted)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", t.Id, time.Unix(t.IssuedAt, 0).Format(time.RFC3339), expiresAtFormatted)
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func NewAccountGenerateTokenCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
account string
|
||||
expiresIn string
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "generate-token",
|
||||
Short: "Generate account token",
|
||||
Example: `# Generate token for the currently logged in account
|
||||
argocd account generate-token
|
||||
|
||||
# Generate token for the account with the specified name
|
||||
argocd account generate-token --account <account-name>`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
|
||||
clientset := argocdclient.NewClientOrDie(clientOpts)
|
||||
conn, client := clientset.NewAccountClientOrDie()
|
||||
defer util.Close(conn)
|
||||
if account == "" {
|
||||
account = getCurrentAccount(clientset).Username
|
||||
}
|
||||
expiresIn, err := timeutil.ParseDuration(expiresIn)
|
||||
errors.CheckError(err)
|
||||
response, err := client.CreateToken(context.Background(), &accountpkg.CreateTokenRequest{
|
||||
Name: account,
|
||||
ExpiresIn: int64(expiresIn.Seconds()),
|
||||
})
|
||||
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)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func NewAccountDeleteTokenCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
account string
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete-token",
|
||||
Short: "Deletes account token",
|
||||
Example: `# Delete token of the currently logged in account
|
||||
argocd account delete-token ID
|
||||
|
||||
# Delete token of the account with the specified name
|
||||
argocd account generate-token --account <account-name>`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
id := args[0]
|
||||
|
||||
clientset := argocdclient.NewClientOrDie(clientOpts)
|
||||
conn, client := clientset.NewAccountClientOrDie()
|
||||
defer util.Close(conn)
|
||||
if account == "" {
|
||||
account = getCurrentAccount(clientset).Username
|
||||
}
|
||||
_, err := client.DeleteToken(context.Background(), &accountpkg.DeleteTokenRequest{Name: account, Id: id})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVarP(&account, "account", "a", "", "Account name. Defaults to the current account.")
|
||||
return cmd
|
||||
}
|
||||
|
||||
@@ -19,12 +19,12 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/yudai/gojsondiff"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/controller"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
@@ -46,6 +46,7 @@ import (
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/resource/ignore"
|
||||
"github.com/argoproj/argo-cd/util/templates"
|
||||
"github.com/argoproj/argo-cd/util/text/label"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -98,10 +99,30 @@ func NewApplicationCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.
|
||||
fileURL string
|
||||
appName string
|
||||
upsert bool
|
||||
labels []string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "create APPNAME",
|
||||
Short: "Create an application",
|
||||
Example: `
|
||||
# Create a directory app
|
||||
argocd app create guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path guestbook --dest-namespace default --dest-server https://kubernetes.default.svc --directory-recurse
|
||||
|
||||
# Create a Jsonnet app
|
||||
argocd app create jsonnet-guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path jsonnet-guestbook --dest-namespace default --dest-server https://kubernetes.default.svc --jsonnet-ext-str replicas=2
|
||||
|
||||
# Create a Helm app
|
||||
argocd app create helm-guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path helm-guestbook --dest-namespace default --dest-server https://kubernetes.default.svc --helm-set replicaCount=2
|
||||
|
||||
# Create a Helm app from a Helm repo
|
||||
argocd app create nginx-ingress --repo https://kubernetes-charts.storage.googleapis.com --helm-chart nginx-ingress --revision 1.24.3 --dest-namespace default --dest-server https://kubernetes.default.svc
|
||||
|
||||
# Create a Kustomize app
|
||||
argocd app create kustomize-guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path kustomize-guestbook --dest-namespace default --dest-server https://kubernetes.default.svc --kustomize-image gcr.io/heptio-images/ks-guestbook-demo=0.1
|
||||
|
||||
# Create a app using a custom tool:
|
||||
argocd app create ksane --repo https://github.com/argoproj/argocd-example-apps.git --path plugins/kasane --dest-namespace default --dest-server https://kubernetes.default.svc --config-management-plugin kasane
|
||||
`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
var app argoappv1.Application
|
||||
argocdClient := argocdclient.NewClientOrDie(clientOpts)
|
||||
@@ -140,13 +161,15 @@ func NewApplicationCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.
|
||||
Name: appName,
|
||||
},
|
||||
}
|
||||
setAppOptions(c.Flags(), &app, &appOpts)
|
||||
setAppSpecOptions(c.Flags(), &app.Spec, &appOpts)
|
||||
setParameterOverrides(&app, appOpts.parameters)
|
||||
setLabels(&app, labels)
|
||||
}
|
||||
if app.Name == "" {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
conn, appIf := argocdClient.NewApplicationClientOrDie()
|
||||
defer util.Close(conn)
|
||||
appCreateRequest := applicationpkg.ApplicationCreateRequest{
|
||||
@@ -161,6 +184,7 @@ func NewApplicationCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.
|
||||
command.Flags().StringVar(&appName, "name", "", "A name for the app, ignored if a file is set (DEPRECATED)")
|
||||
command.Flags().BoolVar(&upsert, "upsert", false, "Allows to override application with the same name even if supplied application spec is different from existing spec")
|
||||
command.Flags().StringVarP(&fileURL, "file", "f", "", "Filename or URL to Kubernetes manifests for the app")
|
||||
command.Flags().StringArrayVarP(&labels, "label", "l", []string{}, "Labels to apply to the app")
|
||||
// Only complete files with appropriate extension.
|
||||
err := command.Flags().SetAnnotation("file", cobra.BashCompFilenameExt, []string{"json", "yaml", "yml"})
|
||||
if err != nil {
|
||||
@@ -170,6 +194,12 @@ func NewApplicationCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.
|
||||
return command
|
||||
}
|
||||
|
||||
func setLabels(app *argoappv1.Application, labels []string) {
|
||||
mapLabels, err := label.Parse(labels)
|
||||
errors.CheckError(err)
|
||||
app.SetLabels(mapLabels)
|
||||
}
|
||||
|
||||
func getRefreshType(refresh bool, hardRefresh bool) *string {
|
||||
if hardRefresh {
|
||||
refreshType := string(argoappv1.RefreshTypeHard)
|
||||
@@ -216,15 +246,10 @@ func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
|
||||
windows := proj.Spec.SyncWindows.Matches(app)
|
||||
|
||||
switch output {
|
||||
case "yaml":
|
||||
yamlBytes, err := yaml.Marshal(app)
|
||||
case "yaml", "json":
|
||||
err := PrintResource(app, output)
|
||||
errors.CheckError(err)
|
||||
fmt.Println(string(yamlBytes))
|
||||
case "json":
|
||||
jsonBytes, err := json.MarshalIndent(app, "", " ")
|
||||
errors.CheckError(err)
|
||||
fmt.Println(string(jsonBytes))
|
||||
case "":
|
||||
case "wide", "":
|
||||
aURL := appURL(acdClient, app.Name)
|
||||
printAppSummaryTable(app, aURL, windows)
|
||||
|
||||
@@ -249,11 +274,11 @@ func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
|
||||
_ = w.Flush()
|
||||
}
|
||||
default:
|
||||
log.Fatalf("Unknown output format: %s", output)
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "", "Output format. One of: yaml, json")
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide")
|
||||
command.Flags().BoolVar(&showOperation, "show-operation", false, "Show application operation")
|
||||
command.Flags().BoolVar(&showParams, "show-params", false, "Show application parameters and overrides")
|
||||
command.Flags().BoolVar(&refresh, "refresh", false, "Refresh application data when retrieving")
|
||||
@@ -353,9 +378,9 @@ func printAppSourceDetails(appSrc *argoappv1.ApplicationSource) {
|
||||
}
|
||||
|
||||
func printAppConditions(w io.Writer, app *argoappv1.Application) {
|
||||
fmt.Fprintf(w, "CONDITION\tMESSAGE\n")
|
||||
_, _ = fmt.Fprintf(w, "CONDITION\tMESSAGE\tLAST TRANSITION\n")
|
||||
for _, item := range app.Status.Conditions {
|
||||
fmt.Fprintf(w, "%s\t%s\n", item.Type, item.Message)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", item.Type, item.Message, item.LastTransitionTime)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,15 +418,15 @@ func printParams(app *argoappv1.Application) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
if app.Spec.Source.Ksonnet != nil {
|
||||
fmt.Println()
|
||||
fmt.Fprintf(w, "COMPONENT\tNAME\tVALUE\n")
|
||||
_, _ = fmt.Fprintf(w, "COMPONENT\tNAME\tVALUE\n")
|
||||
for _, p := range app.Spec.Source.Ksonnet.Parameters {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\n", p.Component, p.Name, truncateString(p.Value, paramLenLimit))
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\n", p.Component, p.Name, truncateString(p.Value, paramLenLimit))
|
||||
}
|
||||
} else if app.Spec.Source.Helm != nil {
|
||||
fmt.Println()
|
||||
fmt.Fprintf(w, "NAME\tVALUE\n")
|
||||
_, _ = fmt.Fprintf(w, "NAME\tVALUE\n")
|
||||
for _, p := range app.Spec.Source.Helm.Parameters {
|
||||
fmt.Fprintf(w, "%s\t%s\n", p.Name, truncateString(p.Value, paramLenLimit))
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\n", p.Name, truncateString(p.Value, paramLenLimit))
|
||||
}
|
||||
}
|
||||
_ = w.Flush()
|
||||
@@ -427,7 +452,7 @@ func NewApplicationSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
|
||||
defer util.Close(conn)
|
||||
app, err := appIf.Get(ctx, &applicationpkg.ApplicationQuery{Name: &appName})
|
||||
errors.CheckError(err)
|
||||
visited := setAppOptions(c.Flags(), app, &appOpts)
|
||||
visited := setAppSpecOptions(c.Flags(), &app.Spec, &appOpts)
|
||||
if visited == 0 {
|
||||
log.Error("Please set at least one option to update")
|
||||
c.HelpFunc()(c, args)
|
||||
@@ -445,71 +470,104 @@ func NewApplicationSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
|
||||
return command
|
||||
}
|
||||
|
||||
func setAppOptions(flags *pflag.FlagSet, app *argoappv1.Application, appOpts *appOptions) int {
|
||||
func setAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, appOpts *appOptions) int {
|
||||
visited := 0
|
||||
flags.Visit(func(f *pflag.Flag) {
|
||||
visited++
|
||||
switch f.Name {
|
||||
case "repo":
|
||||
app.Spec.Source.RepoURL = appOpts.repoURL
|
||||
spec.Source.RepoURL = appOpts.repoURL
|
||||
case "path":
|
||||
app.Spec.Source.Path = appOpts.appPath
|
||||
spec.Source.Path = appOpts.appPath
|
||||
case "helm-chart":
|
||||
app.Spec.Source.Chart = appOpts.chart
|
||||
spec.Source.Chart = appOpts.chart
|
||||
case "env":
|
||||
setKsonnetOpt(&app.Spec.Source, &appOpts.env)
|
||||
setKsonnetOpt(&spec.Source, &appOpts.env)
|
||||
case "revision":
|
||||
app.Spec.Source.TargetRevision = appOpts.revision
|
||||
spec.Source.TargetRevision = appOpts.revision
|
||||
case "revision-history-limit":
|
||||
i := int64(appOpts.revisionHistoryLimit)
|
||||
spec.RevisionHistoryLimit = &i
|
||||
case "values":
|
||||
setHelmOpt(&app.Spec.Source, helmOpts{valueFiles: appOpts.valuesFiles})
|
||||
setHelmOpt(&spec.Source, helmOpts{valueFiles: appOpts.valuesFiles})
|
||||
case "release-name":
|
||||
setHelmOpt(&app.Spec.Source, helmOpts{releaseName: appOpts.releaseName})
|
||||
setHelmOpt(&spec.Source, helmOpts{releaseName: appOpts.releaseName})
|
||||
case "helm-set":
|
||||
setHelmOpt(&app.Spec.Source, helmOpts{helmSets: appOpts.helmSets})
|
||||
setHelmOpt(&spec.Source, helmOpts{helmSets: appOpts.helmSets})
|
||||
case "helm-set-string":
|
||||
setHelmOpt(&app.Spec.Source, helmOpts{helmSetStrings: appOpts.helmSetStrings})
|
||||
setHelmOpt(&spec.Source, helmOpts{helmSetStrings: appOpts.helmSetStrings})
|
||||
case "helm-set-file":
|
||||
setHelmOpt(&spec.Source, helmOpts{helmSetFiles: appOpts.helmSetFiles})
|
||||
case "directory-recurse":
|
||||
app.Spec.Source.Directory = &argoappv1.ApplicationSourceDirectory{Recurse: appOpts.directoryRecurse}
|
||||
spec.Source.Directory = &argoappv1.ApplicationSourceDirectory{Recurse: appOpts.directoryRecurse}
|
||||
case "config-management-plugin":
|
||||
app.Spec.Source.Plugin = &argoappv1.ApplicationSourcePlugin{Name: appOpts.configManagementPlugin}
|
||||
spec.Source.Plugin = &argoappv1.ApplicationSourcePlugin{Name: appOpts.configManagementPlugin}
|
||||
case "dest-server":
|
||||
app.Spec.Destination.Server = appOpts.destServer
|
||||
spec.Destination.Server = appOpts.destServer
|
||||
case "dest-namespace":
|
||||
app.Spec.Destination.Namespace = appOpts.destNamespace
|
||||
spec.Destination.Namespace = appOpts.destNamespace
|
||||
case "project":
|
||||
app.Spec.Project = appOpts.project
|
||||
spec.Project = appOpts.project
|
||||
case "nameprefix":
|
||||
setKustomizeOpt(&app.Spec.Source, &appOpts.namePrefix)
|
||||
setKustomizeOpt(&spec.Source, kustomizeOpts{namePrefix: appOpts.namePrefix})
|
||||
case "namesuffix":
|
||||
setKustomizeOpt(&spec.Source, kustomizeOpts{nameSuffix: appOpts.nameSuffix})
|
||||
case "kustomize-image":
|
||||
setKustomizeImages(&app.Spec.Source, appOpts.kustomizeImages)
|
||||
setKustomizeOpt(&spec.Source, kustomizeOpts{images: appOpts.kustomizeImages})
|
||||
case "jsonnet-tla-str":
|
||||
setJsonnetOpt(&app.Spec.Source, appOpts.jsonnetTlaStr, false)
|
||||
setJsonnetOpt(&spec.Source, appOpts.jsonnetTlaStr, false)
|
||||
case "jsonnet-tla-code":
|
||||
setJsonnetOpt(&app.Spec.Source, appOpts.jsonnetTlaCode, true)
|
||||
setJsonnetOpt(&spec.Source, appOpts.jsonnetTlaCode, true)
|
||||
case "jsonnet-ext-var-str":
|
||||
setJsonnetOptExtVar(&spec.Source, appOpts.jsonnetExtVarStr, false)
|
||||
case "jsonnet-ext-var-code":
|
||||
setJsonnetOptExtVar(&spec.Source, appOpts.jsonnetExtVarCode, true)
|
||||
case "sync-policy":
|
||||
switch appOpts.syncPolicy {
|
||||
case "automated":
|
||||
app.Spec.SyncPolicy = &argoappv1.SyncPolicy{
|
||||
Automated: &argoappv1.SyncPolicyAutomated{},
|
||||
if spec.SyncPolicy == nil {
|
||||
spec.SyncPolicy = &argoappv1.SyncPolicy{}
|
||||
}
|
||||
spec.SyncPolicy.Automated = &argoappv1.SyncPolicyAutomated{}
|
||||
case "none":
|
||||
app.Spec.SyncPolicy = nil
|
||||
if spec.SyncPolicy != nil {
|
||||
spec.SyncPolicy.Automated = nil
|
||||
}
|
||||
if spec.SyncPolicy.IsZero() {
|
||||
spec.SyncPolicy = nil
|
||||
}
|
||||
default:
|
||||
log.Fatalf("Invalid sync-policy: %s", appOpts.syncPolicy)
|
||||
}
|
||||
case "sync-option":
|
||||
if spec.SyncPolicy == nil {
|
||||
spec.SyncPolicy = &argoappv1.SyncPolicy{}
|
||||
}
|
||||
for _, option := range appOpts.syncOptions {
|
||||
// `!` means remove the option
|
||||
if strings.HasPrefix(option, "!") {
|
||||
option = strings.TrimPrefix(option, "!")
|
||||
spec.SyncPolicy.SyncOptions = spec.SyncPolicy.SyncOptions.RemoveOption(option)
|
||||
} else {
|
||||
spec.SyncPolicy.SyncOptions = spec.SyncPolicy.SyncOptions.AddOption(option)
|
||||
}
|
||||
}
|
||||
if spec.SyncPolicy.IsZero() {
|
||||
spec.SyncPolicy = nil
|
||||
}
|
||||
}
|
||||
})
|
||||
if flags.Changed("auto-prune") {
|
||||
if app.Spec.SyncPolicy == nil || app.Spec.SyncPolicy.Automated == nil {
|
||||
if spec.SyncPolicy == nil || spec.SyncPolicy.Automated == nil {
|
||||
log.Fatal("Cannot set --auto-prune: application not configured with automatic sync")
|
||||
}
|
||||
app.Spec.SyncPolicy.Automated.Prune = appOpts.autoPrune
|
||||
spec.SyncPolicy.Automated.Prune = appOpts.autoPrune
|
||||
}
|
||||
if flags.Changed("self-heal") {
|
||||
if app.Spec.SyncPolicy == nil || app.Spec.SyncPolicy.Automated == nil {
|
||||
if spec.SyncPolicy == nil || spec.SyncPolicy.Automated == nil {
|
||||
log.Fatal("Cannot set --self-helf: application not configured with automatic sync")
|
||||
}
|
||||
app.Spec.SyncPolicy.Automated.SelfHeal = appOpts.selfHeal
|
||||
spec.SyncPolicy.Automated.SelfHeal = appOpts.selfHeal
|
||||
}
|
||||
|
||||
return visited
|
||||
@@ -527,22 +585,19 @@ func setKsonnetOpt(src *argoappv1.ApplicationSource, env *string) {
|
||||
}
|
||||
}
|
||||
|
||||
func setKustomizeOpt(src *argoappv1.ApplicationSource, namePrefix *string) {
|
||||
if src.Kustomize == nil {
|
||||
src.Kustomize = &argoappv1.ApplicationSourceKustomize{}
|
||||
}
|
||||
if namePrefix != nil {
|
||||
src.Kustomize.NamePrefix = *namePrefix
|
||||
}
|
||||
if src.Kustomize.IsZero() {
|
||||
src.Kustomize = nil
|
||||
}
|
||||
type kustomizeOpts struct {
|
||||
namePrefix string
|
||||
nameSuffix string
|
||||
images []string
|
||||
}
|
||||
func setKustomizeImages(src *argoappv1.ApplicationSource, images []string) {
|
||||
|
||||
func setKustomizeOpt(src *argoappv1.ApplicationSource, opts kustomizeOpts) {
|
||||
if src.Kustomize == nil {
|
||||
src.Kustomize = &argoappv1.ApplicationSourceKustomize{}
|
||||
}
|
||||
for _, image := range images {
|
||||
src.Kustomize.NamePrefix = opts.namePrefix
|
||||
src.Kustomize.NameSuffix = opts.nameSuffix
|
||||
for _, image := range opts.images {
|
||||
src.Kustomize.MergeImage(argoappv1.KustomizeImage(image))
|
||||
}
|
||||
if src.Kustomize.IsZero() {
|
||||
@@ -555,6 +610,7 @@ type helmOpts struct {
|
||||
releaseName string
|
||||
helmSets []string
|
||||
helmSetStrings []string
|
||||
helmSetFiles []string
|
||||
}
|
||||
|
||||
func setHelmOpt(src *argoappv1.ApplicationSource, opts helmOpts) {
|
||||
@@ -581,6 +637,13 @@ func setHelmOpt(src *argoappv1.ApplicationSource, opts helmOpts) {
|
||||
}
|
||||
src.Helm.AddParameter(*p)
|
||||
}
|
||||
for _, text := range opts.helmSetFiles {
|
||||
p, err := argoappv1.NewHelmFileParameter(text)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
src.Helm.AddFileParameter(*p)
|
||||
}
|
||||
if src.Helm.IsZero() {
|
||||
src.Helm = nil
|
||||
}
|
||||
@@ -590,33 +653,18 @@ func setJsonnetOpt(src *argoappv1.ApplicationSource, tlaParameters []string, cod
|
||||
if src.Directory == nil {
|
||||
src.Directory = &argoappv1.ApplicationSourceDirectory{}
|
||||
}
|
||||
|
||||
if len(tlaParameters) != 0 {
|
||||
tlas := make([]argoappv1.JsonnetVar, len(tlaParameters))
|
||||
for index, paramStr := range tlaParameters {
|
||||
parts := strings.SplitN(paramStr, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
log.Fatalf("Expected parameter of the form: param=value. Received: %s", paramStr)
|
||||
break
|
||||
}
|
||||
tlas[index] = argoappv1.JsonnetVar{
|
||||
Name: parts[0],
|
||||
Value: parts[1],
|
||||
Code: code}
|
||||
}
|
||||
existingTLAs := []argoappv1.JsonnetVar{}
|
||||
for i := range src.Directory.Jsonnet.TLAs {
|
||||
if src.Directory.Jsonnet.TLAs[i].Code != code {
|
||||
existingTLAs = append(existingTLAs, src.Directory.Jsonnet.TLAs[i])
|
||||
}
|
||||
}
|
||||
src.Directory.Jsonnet.TLAs = append(existingTLAs, tlas...)
|
||||
for _, j := range tlaParameters {
|
||||
src.Directory.Jsonnet.TLAs = append(src.Directory.Jsonnet.TLAs, argoappv1.NewJsonnetVar(j, code))
|
||||
}
|
||||
}
|
||||
|
||||
if src.Directory.IsZero() {
|
||||
src.Directory = nil
|
||||
func setJsonnetOptExtVar(src *argoappv1.ApplicationSource, jsonnetExtVar []string, code bool) {
|
||||
if src.Directory == nil {
|
||||
src.Directory = &argoappv1.ApplicationSourceDirectory{}
|
||||
}
|
||||
for _, j := range jsonnetExtVar {
|
||||
src.Directory.Jsonnet.ExtVars = append(src.Directory.Jsonnet.ExtVars, argoappv1.NewJsonnetVar(j, code))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type appOptions struct {
|
||||
@@ -625,6 +673,7 @@ type appOptions struct {
|
||||
chart string
|
||||
env string
|
||||
revision string
|
||||
revisionHistoryLimit int
|
||||
destServer string
|
||||
destNamespace string
|
||||
parameters []string
|
||||
@@ -632,15 +681,20 @@ type appOptions struct {
|
||||
releaseName string
|
||||
helmSets []string
|
||||
helmSetStrings []string
|
||||
helmSetFiles []string
|
||||
project string
|
||||
syncPolicy string
|
||||
syncOptions []string
|
||||
autoPrune bool
|
||||
selfHeal bool
|
||||
namePrefix string
|
||||
nameSuffix string
|
||||
directoryRecurse bool
|
||||
configManagementPlugin string
|
||||
jsonnetTlaStr []string
|
||||
jsonnetTlaCode []string
|
||||
jsonnetExtVarStr []string
|
||||
jsonnetExtVarCode []string
|
||||
kustomizeImages []string
|
||||
}
|
||||
|
||||
@@ -649,23 +703,29 @@ func addAppFlags(command *cobra.Command, opts *appOptions) {
|
||||
command.Flags().StringVar(&opts.appPath, "path", "", "Path in repository to the app directory, ignored if a file is set")
|
||||
command.Flags().StringVar(&opts.chart, "helm-chart", "", "Helm Chart name")
|
||||
command.Flags().StringVar(&opts.env, "env", "", "Application environment to monitor")
|
||||
command.Flags().StringVar(&opts.revision, "revision", "", "The tracking source branch, tag, or commit the application will sync to")
|
||||
command.Flags().StringVar(&opts.destServer, "dest-server", "", "K8s cluster URL (overrides the server URL specified in the ksonnet app.yaml)")
|
||||
command.Flags().StringVar(&opts.revision, "revision", "", "The tracking source branch, tag, commit or Helm chart version the application will sync to")
|
||||
command.Flags().IntVar(&opts.revisionHistoryLimit, "revision-history-limit", common.RevisionHistoryLimit, "How many items to keep in revision history")
|
||||
command.Flags().StringVar(&opts.destServer, "dest-server", "", "K8s cluster URL (e.g. https://kubernetes.default.svc)")
|
||||
command.Flags().StringVar(&opts.destNamespace, "dest-namespace", "", "K8s target namespace (overrides the namespace specified in the ksonnet app.yaml)")
|
||||
command.Flags().StringArrayVarP(&opts.parameters, "parameter", "p", []string{}, "set a parameter override (e.g. -p guestbook=image=example/guestbook:latest)")
|
||||
command.Flags().StringArrayVar(&opts.valuesFiles, "values", []string{}, "Helm values file(s) to use")
|
||||
command.Flags().StringVar(&opts.releaseName, "release-name", "", "Helm release-name")
|
||||
command.Flags().StringArrayVar(&opts.helmSets, "helm-set", []string{}, "Helm set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
|
||||
command.Flags().StringArrayVar(&opts.helmSetStrings, "helm-set-string", []string{}, "Helm set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
|
||||
command.Flags().StringArrayVar(&opts.helmSets, "helm-set", []string{}, "Helm set values on the command line (can be repeated to set several values: --helm-set key1=val1 --helm-set key2=val2)")
|
||||
command.Flags().StringArrayVar(&opts.helmSetStrings, "helm-set-string", []string{}, "Helm set STRING values on the command line (can be repeated to set several values: --helm-set-string key1=val1 --helm-set-string key2=val2)")
|
||||
command.Flags().StringArrayVar(&opts.helmSetFiles, "helm-set-file", []string{}, "Helm set values from respective files specified via the command line (can be repeated to set several values: --helm-set-file key1=path1 --helm-set-file key2=path2)")
|
||||
command.Flags().StringVar(&opts.project, "project", "", "Application project name")
|
||||
command.Flags().StringVar(&opts.syncPolicy, "sync-policy", "", "Set the sync policy (one of: automated, none)")
|
||||
command.Flags().StringArrayVar(&opts.syncOptions, "sync-option", []string{}, "Add or remove a sync options, e.g add `Prune=false`. Remove using `!` prefix, e.g. `!Prune=false`")
|
||||
command.Flags().BoolVar(&opts.autoPrune, "auto-prune", false, "Set automatic pruning when sync is automated")
|
||||
command.Flags().BoolVar(&opts.selfHeal, "self-heal", false, "Set self healing when sync is automated")
|
||||
command.Flags().StringVar(&opts.namePrefix, "nameprefix", "", "Kustomize nameprefix")
|
||||
command.Flags().StringVar(&opts.nameSuffix, "namesuffix", "", "Kustomize namesuffix")
|
||||
command.Flags().BoolVar(&opts.directoryRecurse, "directory-recurse", false, "Recurse directory")
|
||||
command.Flags().StringVar(&opts.configManagementPlugin, "config-management-plugin", "", "Config management plugin name")
|
||||
command.Flags().StringArrayVar(&opts.jsonnetTlaStr, "jsonnet-tla-str", []string{}, "Jsonnet top level string arguments")
|
||||
command.Flags().StringArrayVar(&opts.jsonnetTlaCode, "jsonnet-tla-code", []string{}, "Jsonnet top level code arguments")
|
||||
command.Flags().StringArrayVar(&opts.jsonnetExtVarStr, "jsonnet-ext-var-str", []string{}, "Jsonnet string ext var")
|
||||
command.Flags().StringArrayVar(&opts.jsonnetExtVarCode, "jsonnet-ext-var-code", []string{}, "Jsonnet ext var")
|
||||
command.Flags().StringArrayVar(&opts.kustomizeImages, "kustomize-image", []string{}, "Kustomize images (e.g. --kustomize-image node:8.15.0 --kustomize-image mysql=mariadb,alpine@sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d)")
|
||||
}
|
||||
|
||||
@@ -771,8 +831,8 @@ func liveObjects(resources []*argoappv1.ResourceDiff) ([]*unstructured.Unstructu
|
||||
return objs, nil
|
||||
}
|
||||
|
||||
func getLocalObjects(app *argoappv1.Application, local, appLabelKey, kubeVersion string) []*unstructured.Unstructured {
|
||||
manifestStrings := getLocalObjectsString(app, local, appLabelKey, kubeVersion, nil)
|
||||
func getLocalObjects(app *argoappv1.Application, local, appLabelKey, kubeVersion string, kustomizeOptions *argoappv1.KustomizeOptions) []*unstructured.Unstructured {
|
||||
manifestStrings := getLocalObjectsString(app, local, appLabelKey, kubeVersion, kustomizeOptions)
|
||||
objs := make([]*unstructured.Unstructured, len(manifestStrings))
|
||||
for i := range manifestStrings {
|
||||
obj := unstructured.Unstructured{}
|
||||
@@ -784,11 +844,12 @@ func getLocalObjects(app *argoappv1.Application, local, appLabelKey, kubeVersion
|
||||
}
|
||||
|
||||
func getLocalObjectsString(app *argoappv1.Application, local, appLabelKey, kubeVersion string, kustomizeOptions *argoappv1.KustomizeOptions) []string {
|
||||
res, err := repository.GenerateManifests(local, &repoapiclient.ManifestRequest{
|
||||
ApplicationSource: &app.Spec.Source,
|
||||
res, err := repository.GenerateManifests(local, "/", app.Spec.Source.TargetRevision, &repoapiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{Repo: app.Spec.Source.RepoURL},
|
||||
AppLabelKey: appLabelKey,
|
||||
AppLabelValue: app.Name,
|
||||
Namespace: app.Spec.Destination.Namespace,
|
||||
ApplicationSource: &app.Spec.Source,
|
||||
KustomizeOptions: kustomizeOptions,
|
||||
KubeVersion: kubeVersion,
|
||||
})
|
||||
@@ -871,22 +932,13 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
defer util.Close(conn)
|
||||
cluster, err := clusterIf.Get(context.Background(), &clusterpkg.ClusterQuery{Server: app.Spec.Destination.Server})
|
||||
errors.CheckError(err)
|
||||
util.Close(conn)
|
||||
localObjs := groupLocalObjs(getLocalObjects(app, local, argoSettings.AppLabelKey, cluster.ServerVersion), liveObjs, app.Spec.Destination.Namespace)
|
||||
localObjs := groupLocalObjs(getLocalObjects(app, local, argoSettings.AppLabelKey, cluster.ServerVersion, argoSettings.KustomizeOptions), liveObjs, app.Spec.Destination.Namespace)
|
||||
for _, res := range resources.Items {
|
||||
var live = &unstructured.Unstructured{}
|
||||
err := json.Unmarshal([]byte(res.LiveState), &live)
|
||||
err := json.Unmarshal([]byte(res.NormalizedLiveState), &live)
|
||||
errors.CheckError(err)
|
||||
|
||||
var key kube.ResourceKey
|
||||
if live != nil {
|
||||
key = kube.GetResourceKey(live)
|
||||
} else {
|
||||
var target = &unstructured.Unstructured{}
|
||||
err = json.Unmarshal([]byte(res.TargetState), &target)
|
||||
errors.CheckError(err)
|
||||
key = kube.GetResourceKey(target)
|
||||
}
|
||||
key := kube.ResourceKey{Name: res.Name, Namespace: res.Namespace, Group: res.Group, Kind: res.Kind}
|
||||
if key.Kind == kube.SecretKind && key.Group == "" {
|
||||
// Don't bother comparing secrets, argo-cd doesn't have access to k8s secret data
|
||||
delete(localObjs, key)
|
||||
@@ -925,7 +977,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
for i := range resources.Items {
|
||||
res := resources.Items[i]
|
||||
var live = &unstructured.Unstructured{}
|
||||
err := json.Unmarshal([]byte(res.LiveState), &live)
|
||||
err := json.Unmarshal([]byte(res.NormalizedLiveState), &live)
|
||||
errors.CheckError(err)
|
||||
|
||||
var target = &unstructured.Unstructured{}
|
||||
@@ -956,24 +1008,26 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
}
|
||||
normalizer, err := argo.NewDiffNormalizer(app.Spec.IgnoreDifferences, overrides)
|
||||
errors.CheckError(err)
|
||||
// Diff is already available in ResourceDiff Diff field but we have to recalculate diff again due to https://github.com/yudai/gojsondiff/issues/31
|
||||
diffRes := diff.Diff(item.target, item.live, normalizer)
|
||||
|
||||
diffRes, err := diff.Diff(item.target, item.live, normalizer)
|
||||
errors.CheckError(err)
|
||||
|
||||
if diffRes.Modified || item.target == nil || item.live == nil {
|
||||
fmt.Printf("===== %s/%s %s/%s ======\n", item.key.Group, item.key.Kind, item.key.Namespace, item.key.Name)
|
||||
var live *unstructured.Unstructured
|
||||
var target *unstructured.Unstructured
|
||||
if item.target != nil && item.live != nil {
|
||||
target = item.live
|
||||
live = item.live.DeepCopy()
|
||||
gojsondiff.New().ApplyPatch(live.Object, diffRes.Diff)
|
||||
live = &unstructured.Unstructured{}
|
||||
err = json.Unmarshal(diffRes.PredictedLive, live)
|
||||
errors.CheckError(err)
|
||||
} else {
|
||||
live = item.live
|
||||
target = item.target
|
||||
}
|
||||
|
||||
foundDiffs = true
|
||||
err = diff.PrintDiff(item.key.Name, target, live)
|
||||
errors.CheckError(err)
|
||||
_ = diff.PrintDiff(item.key.Name, target, live)
|
||||
}
|
||||
}
|
||||
if foundDiffs {
|
||||
@@ -984,7 +1038,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
}
|
||||
command.Flags().BoolVar(&refresh, "refresh", false, "Refresh application data when retrieving")
|
||||
command.Flags().BoolVar(&hardRefresh, "hard-refresh", false, "Refresh application data as well as target manifests cache")
|
||||
command.Flags().StringVar(&local, "local", "", "Compare live app to a local ksonnet app")
|
||||
command.Flags().StringVar(&local, "local", "", "Compare live app to a local manifests")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -1037,7 +1091,7 @@ func printApplicationTable(apps []argoappv1.Application, output *string) {
|
||||
} else {
|
||||
fmtStr = "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n"
|
||||
}
|
||||
fmt.Fprintf(w, fmtStr, headers...)
|
||||
_, _ = fmt.Fprintf(w, fmtStr, headers...)
|
||||
for _, app := range apps {
|
||||
vals := []interface{}{
|
||||
app.Name,
|
||||
@@ -1052,7 +1106,7 @@ func printApplicationTable(apps []argoappv1.Application, output *string) {
|
||||
if *output == "wide" {
|
||||
vals = append(vals, app.Spec.Source.RepoURL, app.Spec.Source.Path, app.Spec.Source.TargetRevision)
|
||||
}
|
||||
fmt.Fprintf(w, fmtStr, vals...)
|
||||
_, _ = fmt.Fprintf(w, fmtStr, vals...)
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
@@ -1061,28 +1115,41 @@ func printApplicationTable(apps []argoappv1.Application, output *string) {
|
||||
func NewApplicationListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
selector string
|
||||
projects []string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List applications",
|
||||
Example: ` # List all apps
|
||||
argocd app list
|
||||
|
||||
# List apps by label, in this example we listing apps that are children of another app (aka app-of-apps)
|
||||
argocd app list -l app.kubernetes.io/instance=my-app`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
|
||||
defer util.Close(conn)
|
||||
apps, err := appIf.List(context.Background(), &applicationpkg.ApplicationQuery{})
|
||||
apps, err := appIf.List(context.Background(), &applicationpkg.ApplicationQuery{Selector: selector})
|
||||
errors.CheckError(err)
|
||||
appList := apps.Items
|
||||
if len(projects) != 0 {
|
||||
appList = argo.FilterByProjects(appList, projects)
|
||||
}
|
||||
if output == "name" {
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(appList, output, false)
|
||||
errors.CheckError(err)
|
||||
case "name":
|
||||
printApplicationNames(appList)
|
||||
} else {
|
||||
case "wide", "":
|
||||
printApplicationTable(appList, &output)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: wide|name")
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: wide|name|json|yaml")
|
||||
command.Flags().StringVarP(&selector, "selector", "l", "", "List apps by label")
|
||||
command.Flags().StringArrayVarP(&projects, "project", "p", []string{}, "Filter by project name")
|
||||
return command
|
||||
}
|
||||
@@ -1126,7 +1193,6 @@ func formatConditionsSummary(app argoappv1.Application) string {
|
||||
const (
|
||||
resourceFieldDelimiter = ":"
|
||||
resourceFieldCount = 3
|
||||
labelFieldDelimiter = "="
|
||||
)
|
||||
|
||||
func parseSelectedResources(resources []string) []argoappv1.SyncOperationResource {
|
||||
@@ -1149,21 +1215,6 @@ func parseSelectedResources(resources []string) []argoappv1.SyncOperationResourc
|
||||
return selectedResources
|
||||
}
|
||||
|
||||
func parseLabels(labels []string) (map[string]string, error) {
|
||||
var selectedLabels map[string]string
|
||||
if labels != nil {
|
||||
selectedLabels = map[string]string{}
|
||||
for _, r := range labels {
|
||||
fields := strings.Split(r, labelFieldDelimiter)
|
||||
if len(fields) != 2 {
|
||||
return nil, fmt.Errorf("labels should have key%svalue, but instead got: %s", labelFieldDelimiter, r)
|
||||
}
|
||||
selectedLabels[fields[0]] = fields[1]
|
||||
}
|
||||
}
|
||||
return selectedLabels, nil
|
||||
}
|
||||
|
||||
// NewApplicationWaitCommand returns a new instance of an `argocd app wait` command
|
||||
func NewApplicationWaitCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
@@ -1172,13 +1223,22 @@ func NewApplicationWaitCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
watchSuspended bool
|
||||
watchOperations bool
|
||||
timeout uint
|
||||
selector string
|
||||
resources []string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "wait APPNAME",
|
||||
Use: "wait [APPNAME.. | -l selector]",
|
||||
Short: "Wait for an application to reach a synced and healthy state",
|
||||
Example: ` # Wait for an app
|
||||
argocd app wait my-app
|
||||
|
||||
# Wait for multiple apps
|
||||
argocd app wait my-app other-app
|
||||
|
||||
# Wait for apps by label, in this example we waiting for apps that are children of another app (aka app-of-apps)
|
||||
argocd app wait -l app.kubernetes.io/instance=apps`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
if len(args) == 0 && selector == "" {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -1189,15 +1249,27 @@ func NewApplicationWaitCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
watchSuspended = false
|
||||
}
|
||||
selectedResources := parseSelectedResources(resources)
|
||||
appName := args[0]
|
||||
appNames := args
|
||||
acdClient := argocdclient.NewClientOrDie(clientOpts)
|
||||
_, err := waitOnApplicationStatus(acdClient, appName, timeout, watchSync, watchHealth, watchOperations, watchSuspended, selectedResources)
|
||||
errors.CheckError(err)
|
||||
closer, appIf := acdClient.NewApplicationClientOrDie()
|
||||
defer util.Close(closer)
|
||||
if selector != "" {
|
||||
list, err := appIf.List(context.Background(), &applicationpkg.ApplicationQuery{Selector: selector})
|
||||
errors.CheckError(err)
|
||||
for _, i := range list.Items {
|
||||
appNames = append(appNames, i.Name)
|
||||
}
|
||||
}
|
||||
for _, appName := range appNames {
|
||||
_, err := waitOnApplicationStatus(acdClient, appName, timeout, watchSync, watchHealth, watchOperations, watchSuspended, selectedResources)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().BoolVar(&watchSync, "sync", false, "Wait for sync")
|
||||
command.Flags().BoolVar(&watchHealth, "health", false, "Wait for health")
|
||||
command.Flags().BoolVar(&watchSuspended, "suspended", false, "Wait for suspended")
|
||||
command.Flags().StringVarP(&selector, "selector", "l", "", "Wait for apps by label")
|
||||
command.Flags().StringArrayVar(&resources, "resource", []string{}, fmt.Sprintf("Sync only specific resources as GROUP%sKIND%sNAME. Fields may be blank. This option may be specified repeatedly", resourceFieldDelimiter, resourceFieldDelimiter))
|
||||
command.Flags().BoolVar(&watchOperations, "operation", false, "Wait for pending operations")
|
||||
command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds")
|
||||
@@ -1218,6 +1290,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
revision string
|
||||
resources []string
|
||||
labels []string
|
||||
selector string
|
||||
prune bool
|
||||
dryRun bool
|
||||
timeout uint
|
||||
@@ -1227,10 +1300,23 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
local string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "sync APPNAME",
|
||||
Use: "sync [APPNAME... | -l selector]",
|
||||
Short: "Sync an application to its target state",
|
||||
Example: ` # Sync an app
|
||||
argocd app sync my-app
|
||||
|
||||
# Sync multiples apps
|
||||
argocd app sync my-app other-app
|
||||
|
||||
# Sync apps by label, in this example we sync apps that are children of another app (aka app-of-apps)
|
||||
argocd app sync -l app.kubernetes.io/instance=my-app
|
||||
|
||||
# Sync a specific resource
|
||||
# Resource should be formatted as GROUP:KIND:NAME. If no GROUP is specified then :KIND:NAME
|
||||
argocd app sync my-app --resource :Service:my-service
|
||||
argocd app sync my-app --resource argoproj.io:Rollout:my-rollout`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
if len(args) == 0 && selector == "" {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -1238,104 +1324,116 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
conn, appIf := acdClient.NewApplicationClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
appName := args[0]
|
||||
|
||||
selectedLabels, parseErr := parseLabels(labels)
|
||||
if parseErr != nil {
|
||||
log.Fatal(parseErr)
|
||||
}
|
||||
|
||||
if len(selectedLabels) > 0 {
|
||||
ctx := context.Background()
|
||||
|
||||
q := applicationpkg.ApplicationManifestQuery{
|
||||
Name: &appName,
|
||||
Revision: revision,
|
||||
}
|
||||
|
||||
res, err := appIf.GetManifests(ctx, &q)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, mfst := range res.Manifests {
|
||||
obj, err := argoappv1.UnmarshalToUnstructured(mfst)
|
||||
errors.CheckError(err)
|
||||
for key, selectedValue := range selectedLabels {
|
||||
if objectValue, ok := obj.GetLabels()[key]; ok && selectedValue == objectValue {
|
||||
gvk := obj.GroupVersionKind()
|
||||
resources = append(resources, fmt.Sprintf("%s:%s:%s", gvk.Group, gvk.Kind, obj.GetName()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If labels are provided and none are found return error only if specific resources were also not
|
||||
// specified.
|
||||
if len(resources) == 0 {
|
||||
log.Fatalf("No matching resources found for labels: %v", labels)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
selectedResources := parseSelectedResources(resources)
|
||||
|
||||
var localObjsStrings []string
|
||||
if local != "" {
|
||||
app, err := appIf.Get(context.Background(), &applicationpkg.ApplicationQuery{Name: &appName})
|
||||
errors.CheckError(err)
|
||||
if app.Spec.SyncPolicy != nil && app.Spec.SyncPolicy.Automated != nil {
|
||||
log.Fatal("Cannot use local sync when Automatic Sync Policy is enabled")
|
||||
}
|
||||
|
||||
errors.CheckError(err)
|
||||
conn, settingsIf := acdClient.NewSettingsClientOrDie()
|
||||
argoSettings, err := settingsIf.Get(context.Background(), &settingspkg.SettingsQuery{})
|
||||
errors.CheckError(err)
|
||||
util.Close(conn)
|
||||
|
||||
conn, clusterIf := acdClient.NewClusterClientOrDie()
|
||||
defer util.Close(conn)
|
||||
cluster, err := clusterIf.Get(context.Background(), &clusterpkg.ClusterQuery{Server: app.Spec.Destination.Server})
|
||||
errors.CheckError(err)
|
||||
util.Close(conn)
|
||||
localObjsStrings = getLocalObjectsString(app, local, cluster.ServerVersion, argoSettings.AppLabelKey, argoSettings.KustomizeOptions)
|
||||
}
|
||||
|
||||
syncReq := applicationpkg.ApplicationSyncRequest{
|
||||
Name: &appName,
|
||||
DryRun: dryRun,
|
||||
Revision: revision,
|
||||
Resources: selectedResources,
|
||||
Prune: prune,
|
||||
Manifests: localObjsStrings,
|
||||
}
|
||||
switch strategy {
|
||||
case "apply":
|
||||
syncReq.Strategy = &argoappv1.SyncStrategy{Apply: &argoappv1.SyncStrategyApply{}}
|
||||
syncReq.Strategy.Apply.Force = force
|
||||
case "", "hook":
|
||||
syncReq.Strategy = &argoappv1.SyncStrategy{Hook: &argoappv1.SyncStrategyHook{}}
|
||||
syncReq.Strategy.Hook.Force = force
|
||||
default:
|
||||
log.Fatalf("Unknown sync strategy: '%s'", strategy)
|
||||
}
|
||||
ctx := context.Background()
|
||||
_, err := appIf.Sync(ctx, &syncReq)
|
||||
selectedLabels, err := label.Parse(labels)
|
||||
errors.CheckError(err)
|
||||
|
||||
if !async {
|
||||
app, err := waitOnApplicationStatus(acdClient, appName, timeout, false, false, true, false, selectedResources)
|
||||
appNames := args
|
||||
if selector != "" {
|
||||
list, err := appIf.List(context.Background(), &applicationpkg.ApplicationQuery{Selector: selector})
|
||||
errors.CheckError(err)
|
||||
// unlike list, we'd want to fail if nothing was found
|
||||
if len(list.Items) == 0 {
|
||||
log.Fatalf("no apps match selector %v", selector)
|
||||
}
|
||||
for _, i := range list.Items {
|
||||
appNames = append(appNames, i.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Only get resources to be pruned if sync was application-wide
|
||||
if len(selectedResources) == 0 {
|
||||
pruningRequired := app.Status.OperationState.SyncResult.Resources.PruningRequired()
|
||||
if pruningRequired > 0 {
|
||||
log.Fatalf("%d resources require pruning", pruningRequired)
|
||||
for _, appName := range appNames {
|
||||
|
||||
if len(selectedLabels) > 0 {
|
||||
ctx := context.Background()
|
||||
|
||||
q := applicationpkg.ApplicationManifestQuery{
|
||||
Name: &appName,
|
||||
Revision: revision,
|
||||
}
|
||||
|
||||
if !app.Status.OperationState.Phase.Successful() && !dryRun {
|
||||
os.Exit(1)
|
||||
res, err := appIf.GetManifests(ctx, &q)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, mfst := range res.Manifests {
|
||||
obj, err := argoappv1.UnmarshalToUnstructured(mfst)
|
||||
errors.CheckError(err)
|
||||
for key, selectedValue := range selectedLabels {
|
||||
if objectValue, ok := obj.GetLabels()[key]; ok && selectedValue == objectValue {
|
||||
gvk := obj.GroupVersionKind()
|
||||
resources = append(resources, fmt.Sprintf("%s:%s:%s", gvk.Group, gvk.Kind, obj.GetName()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If labels are provided and none are found return error only if specific resources were also not
|
||||
// specified.
|
||||
if len(resources) == 0 {
|
||||
log.Fatalf("No matching resources found for labels: %v", labels)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
selectedResources := parseSelectedResources(resources)
|
||||
|
||||
var localObjsStrings []string
|
||||
if local != "" {
|
||||
app, err := appIf.Get(context.Background(), &applicationpkg.ApplicationQuery{Name: &appName})
|
||||
errors.CheckError(err)
|
||||
if app.Spec.SyncPolicy != nil && app.Spec.SyncPolicy.Automated != nil {
|
||||
log.Fatal("Cannot use local sync when Automatic Sync Policy is enabled")
|
||||
}
|
||||
|
||||
errors.CheckError(err)
|
||||
conn, settingsIf := acdClient.NewSettingsClientOrDie()
|
||||
argoSettings, err := settingsIf.Get(context.Background(), &settingspkg.SettingsQuery{})
|
||||
errors.CheckError(err)
|
||||
util.Close(conn)
|
||||
|
||||
conn, clusterIf := acdClient.NewClusterClientOrDie()
|
||||
defer util.Close(conn)
|
||||
cluster, err := clusterIf.Get(context.Background(), &clusterpkg.ClusterQuery{Server: app.Spec.Destination.Server})
|
||||
errors.CheckError(err)
|
||||
util.Close(conn)
|
||||
localObjsStrings = getLocalObjectsString(app, local, argoSettings.AppLabelKey, cluster.ServerVersion, argoSettings.KustomizeOptions)
|
||||
}
|
||||
|
||||
syncReq := applicationpkg.ApplicationSyncRequest{
|
||||
Name: &appName,
|
||||
DryRun: dryRun,
|
||||
Revision: revision,
|
||||
Resources: selectedResources,
|
||||
Prune: prune,
|
||||
Manifests: localObjsStrings,
|
||||
}
|
||||
switch strategy {
|
||||
case "apply":
|
||||
syncReq.Strategy = &argoappv1.SyncStrategy{Apply: &argoappv1.SyncStrategyApply{}}
|
||||
syncReq.Strategy.Apply.Force = force
|
||||
case "", "hook":
|
||||
syncReq.Strategy = &argoappv1.SyncStrategy{Hook: &argoappv1.SyncStrategyHook{}}
|
||||
syncReq.Strategy.Hook.Force = force
|
||||
default:
|
||||
log.Fatalf("Unknown sync strategy: '%s'", strategy)
|
||||
}
|
||||
ctx := context.Background()
|
||||
_, err := appIf.Sync(ctx, &syncReq)
|
||||
errors.CheckError(err)
|
||||
|
||||
if !async {
|
||||
app, err := waitOnApplicationStatus(acdClient, appName, timeout, false, false, true, false, selectedResources)
|
||||
errors.CheckError(err)
|
||||
|
||||
// Only get resources to be pruned if sync was application-wide
|
||||
if len(selectedResources) == 0 {
|
||||
pruningRequired := app.Status.OperationState.SyncResult.Resources.PruningRequired()
|
||||
if pruningRequired > 0 {
|
||||
log.Fatalf("%d resources require pruning", pruningRequired)
|
||||
}
|
||||
|
||||
if !app.Status.OperationState.Phase.Successful() && !dryRun {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1345,6 +1443,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
command.Flags().BoolVar(&prune, "prune", false, "Allow deleting unexpected resources")
|
||||
command.Flags().StringVar(&revision, "revision", "", "Sync to a specific revision. Preserves parameter overrides")
|
||||
command.Flags().StringArrayVar(&resources, "resource", []string{}, fmt.Sprintf("Sync only specific resources as GROUP%sKIND%sNAME. Fields may be blank. This option may be specified repeatedly", resourceFieldDelimiter, resourceFieldDelimiter))
|
||||
command.Flags().StringVarP(&selector, "selector", "l", "", "Sync apps that match this label")
|
||||
command.Flags().StringArrayVar(&labels, "label", []string{}, fmt.Sprintf("Sync only specific resources with a label. This option may be specified repeatedly."))
|
||||
command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds")
|
||||
command.Flags().StringVar(&strategy, "strategy", "", "Sync strategy (one of: apply|hook)")
|
||||
@@ -1542,7 +1641,7 @@ func waitOnApplicationStatus(acdClient apiclient.Client, appName string, timeout
|
||||
if len(selectedResources) > 0 {
|
||||
selectedResourcesAreReady = true
|
||||
for _, state := range getResourceStates(app, selectedResources) {
|
||||
resourceIsReady := checkResourceStatus(watchSync, watchHealth, false, watchSuspended, state.Health, state.Status, nil)
|
||||
resourceIsReady := checkResourceStatus(watchSync, watchHealth, watchOperation, watchSuspended, state.Health, state.Status, appEvent.Application.Operation)
|
||||
if !resourceIsReady {
|
||||
selectedResourcesAreReady = false
|
||||
break
|
||||
@@ -1565,7 +1664,7 @@ func waitOnApplicationStatus(acdClient apiclient.Client, appName string, timeout
|
||||
if prevState, found := prevStates[stateKey]; found {
|
||||
if watchHealth && prevState.Health != argoappv1.HealthStatusUnknown && prevState.Health != argoappv1.HealthStatusDegraded && newState.Health == argoappv1.HealthStatusDegraded {
|
||||
printFinalStatus(app)
|
||||
return nil, fmt.Errorf("Application '%s' health state has transitioned from %s to %s", appName, prevState.Health, newState.Health)
|
||||
return nil, fmt.Errorf("application '%s' health state has transitioned from %s to %s", appName, prevState.Health, newState.Health)
|
||||
}
|
||||
doPrint = prevState.Merge(newState)
|
||||
} else {
|
||||
@@ -1573,13 +1672,13 @@ func waitOnApplicationStatus(acdClient apiclient.Client, appName string, timeout
|
||||
doPrint = true
|
||||
}
|
||||
if doPrint {
|
||||
fmt.Fprintf(w, waitFormatString, prevStates[stateKey].FormatItems()...)
|
||||
_, _ = fmt.Fprintf(w, waitFormatString, prevStates[stateKey].FormatItems()...)
|
||||
}
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
printFinalStatus(app)
|
||||
return nil, fmt.Errorf("Timed out (%ds) waiting for app %q match desired state", timeout, appName)
|
||||
return nil, fmt.Errorf("timed out (%ds) waiting for app %q match desired state", timeout, appName)
|
||||
}
|
||||
|
||||
// setParameterOverrides updates an existing or appends a new parameter override in the application
|
||||
@@ -1662,13 +1761,13 @@ func printApplicationHistoryIds(revHistory []argoappv1.RevisionHistory) {
|
||||
// Print a history table for an application.
|
||||
func printApplicationHistoryTable(revHistory []argoappv1.RevisionHistory) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "ID\tDATE\tREVISION\n")
|
||||
_, _ = fmt.Fprintf(w, "ID\tDATE\tREVISION\n")
|
||||
for _, depInfo := range revHistory {
|
||||
rev := depInfo.Source.TargetRevision
|
||||
if len(depInfo.Revision) >= 7 {
|
||||
rev = fmt.Sprintf("%s (%s)", rev, depInfo.Revision[0:7])
|
||||
}
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\n", depInfo.ID, depInfo.DeployedAt, rev)
|
||||
_, _ = fmt.Fprintf(w, "%d\t%s\t%s\n", depInfo.ID, depInfo.DeployedAt, rev)
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
@@ -1962,11 +2061,11 @@ func filterResources(command *cobra.Command, resources []*argoappv1.ResourceDiff
|
||||
if resourceName != "" && resourceName != obj.GetName() {
|
||||
continue
|
||||
}
|
||||
if kind != "" && kind != gvk.Kind {
|
||||
if kind != gvk.Kind {
|
||||
continue
|
||||
}
|
||||
copy := obj.DeepCopy()
|
||||
filteredObjects = append(filteredObjects, copy)
|
||||
deepCopy := obj.DeepCopy()
|
||||
filteredObjects = append(filteredObjects, deepCopy)
|
||||
}
|
||||
if len(filteredObjects) == 0 {
|
||||
log.Fatal("No matching resource found")
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
@@ -121,7 +120,8 @@ func NewApplicationResourceActionsListCommand(clientOpts *argocdclient.ClientOpt
|
||||
func NewApplicationResourceActionsRunCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var namespace string
|
||||
var resourceName string
|
||||
var kindArg string
|
||||
var kind string
|
||||
var group string
|
||||
var all bool
|
||||
var command = &cobra.Command{
|
||||
Use: "run APPNAME ACTION",
|
||||
@@ -130,7 +130,9 @@ func NewApplicationResourceActionsRunCommand(clientOpts *argocdclient.ClientOpti
|
||||
|
||||
command.Flags().StringVar(&resourceName, "resource-name", "", "Name of resource")
|
||||
command.Flags().StringVar(&namespace, "namespace", "", "Namespace")
|
||||
command.Flags().StringVar(&kindArg, "kind", "", "Kind")
|
||||
command.Flags().StringVar(&kind, "kind", "", "Kind")
|
||||
command.Flags().StringVar(&group, "group", "", "Group")
|
||||
errors.CheckError(command.MarkFlagRequired("kind"))
|
||||
command.Flags().BoolVar(&all, "all", false, "Indicates whether to run the action on multiple matching resources")
|
||||
|
||||
command.Run = func(c *cobra.Command, args []string) {
|
||||
@@ -146,31 +148,14 @@ func NewApplicationResourceActionsRunCommand(clientOpts *argocdclient.ClientOpti
|
||||
ctx := context.Background()
|
||||
resources, err := appIf.ManagedResources(ctx, &applicationpkg.ResourcesQuery{ApplicationName: &appName})
|
||||
errors.CheckError(err)
|
||||
|
||||
var group string
|
||||
var kind string
|
||||
var actionNameOnly string
|
||||
// Backwards comparability for running resume actions
|
||||
if actionName == "resume" && kindArg == "Rollout" {
|
||||
group = "argoproj.io"
|
||||
kind = "Rollout"
|
||||
actionNameOnly = "resume"
|
||||
commandTail := ""
|
||||
if resourceName != "" {
|
||||
commandTail += " --resource-name " + resourceName
|
||||
filteredObjects := filterResources(command, resources.Items, group, kind, namespace, resourceName, all)
|
||||
var resGroup = filteredObjects[0].GroupVersionKind().Group
|
||||
for i := range filteredObjects[1:] {
|
||||
if filteredObjects[i].GroupVersionKind().Group != resGroup {
|
||||
log.Fatal("Ambiguous resource group. Use flag --group to specify resource group explicitly.")
|
||||
}
|
||||
if namespace != "" {
|
||||
commandTail += " --namespace " + namespace
|
||||
}
|
||||
if all {
|
||||
commandTail += " --all"
|
||||
}
|
||||
fmt.Printf("\nWarning: this syntax for running the \"resume\" action has been deprecated. Please run the action as\n\n\targocd app actions run %s argoproj.io/Rollout/resume%s\n\n", appName, commandTail)
|
||||
} else {
|
||||
group, kind, actionNameOnly = parseActionName(actionName)
|
||||
}
|
||||
|
||||
filteredObjects := filterResources(command, resources.Items, group, kind, namespace, resourceName, all)
|
||||
for i := range filteredObjects {
|
||||
obj := filteredObjects[i]
|
||||
gvk := obj.GroupVersionKind()
|
||||
@@ -181,18 +166,10 @@ func NewApplicationResourceActionsRunCommand(clientOpts *argocdclient.ClientOpti
|
||||
ResourceName: objResourceName,
|
||||
Group: gvk.Group,
|
||||
Kind: gvk.Kind,
|
||||
Action: actionNameOnly,
|
||||
Action: actionName,
|
||||
})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
func parseActionName(action string) (string, string, string) {
|
||||
actionSplit := strings.Split(action, "/")
|
||||
if len(actionSplit) != 3 {
|
||||
log.Fatal("Action name is malformed")
|
||||
}
|
||||
return actionSplit[0], actionSplit[1], actionSplit[2]
|
||||
}
|
||||
|
||||
@@ -3,29 +3,12 @@ package commands
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
func TestParseLabels(t *testing.T) {
|
||||
validLabels := []string{"key=value", "foo=bar", "intuit=inc"}
|
||||
|
||||
result, err := parseLabels(validLabels)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, result, 3)
|
||||
|
||||
invalidLabels := []string{"key=value", "too=many=equals"}
|
||||
_, err = parseLabels(invalidLabels)
|
||||
assert.Error(t, err)
|
||||
|
||||
emptyLabels := []string{}
|
||||
result, err = parseLabels(emptyLabels)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, result, 0)
|
||||
|
||||
}
|
||||
|
||||
func Test_setHelmOpt(t *testing.T) {
|
||||
t.Run("Zero", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
@@ -52,4 +35,70 @@ 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)
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -29,6 +29,24 @@ func NewCertCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
},
|
||||
Example: ` # Add a TLS certificate for cd.example.com to ArgoCD cert store from a file
|
||||
argocd cert add-tls --from ~/mycert.pem cd.example.com
|
||||
|
||||
# Add a TLS certificate for cd.example.com to ArgoCD via stdin
|
||||
cat ~/mycert.pem | argocd cert add-tls cd.example.com
|
||||
|
||||
# Add SSH known host entries for cd.example.com to ArgoCD by scanning host
|
||||
ssh-keyscan cd.example.com | argocd cert add-ssh --batch
|
||||
|
||||
# List all known TLS certificates
|
||||
argocd cert list --cert-type https
|
||||
|
||||
# Remove all TLS certificates for cd.example.com
|
||||
argocd cert rm --cert-type https cd.example.com
|
||||
|
||||
# Remove all certificates and SSH known host entries for cd.example.com
|
||||
argocd cert rm cd.example.com
|
||||
`,
|
||||
}
|
||||
|
||||
command.AddCommand(NewCertAddSSHCommand(clientOpts))
|
||||
@@ -156,18 +174,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}
|
||||
@@ -239,6 +259,7 @@ func NewCertListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
certType string
|
||||
hostNamePattern string
|
||||
sortOrder string
|
||||
output string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "list",
|
||||
@@ -258,11 +279,22 @@ func NewCertListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
defer util.Close(conn)
|
||||
certificates, err := certIf.ListCertificates(context.Background(), &certificatepkg.RepositoryCertificateQuery{HostNamePattern: hostNamePattern, CertType: certType})
|
||||
errors.CheckError(err)
|
||||
printCertTable(certificates.Items, sortOrder)
|
||||
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(certificates.Items, output, false)
|
||||
errors.CheckError(err)
|
||||
case "wide", "":
|
||||
printCertTable(certificates.Items, sortOrder)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
command.Flags().StringVar(&sortOrder, "sort", "", "set display sort order, valid: 'hostname', 'type'")
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide")
|
||||
command.Flags().StringVar(&sortOrder, "sort", "", "set display sort order for output format wide. One of: hostname|type")
|
||||
command.Flags().StringVar(&certType, "cert-type", "", "only list certificates of given type, valid: 'ssh','https'")
|
||||
command.Flags().StringVar(&hostNamePattern, "hostname-pattern", "", "only list certificates for hosts matching given glob-pattern")
|
||||
return command
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
@@ -34,6 +33,18 @@ func NewClusterCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clientc
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
},
|
||||
Example: ` # List all known clusters in JSON format:
|
||||
argocd cluster list -o json
|
||||
|
||||
# Add a target cluster configuration to ArgoCD. The context must exist in your kubectl config:
|
||||
argocd cluster add example-cluster
|
||||
|
||||
# Get specific details about a cluster in plain text (wide) format:
|
||||
argocd cluster get example-cluster -o wide
|
||||
|
||||
# Remove a target cluster context from ArgoCD
|
||||
argocd cluster rm example-cluster
|
||||
`,
|
||||
}
|
||||
|
||||
command.AddCommand(NewClusterAddCommand(clientOpts, pathOpts))
|
||||
@@ -49,12 +60,14 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
var (
|
||||
inCluster bool
|
||||
upsert bool
|
||||
serviceAccount string
|
||||
awsRoleArn string
|
||||
awsClusterName string
|
||||
systemNamespace string
|
||||
namespaces []string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "add",
|
||||
Use: "add CONTEXT",
|
||||
Short: fmt.Sprintf("%s cluster add CONTEXT", cliName),
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
var configAccess clientcmd.ConfigAccess = pathOpts
|
||||
@@ -65,9 +78,10 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
}
|
||||
config, err := configAccess.GetStartingConfig()
|
||||
errors.CheckError(err)
|
||||
clstContext := config.Contexts[args[0]]
|
||||
contextName := args[0]
|
||||
clstContext := config.Contexts[contextName]
|
||||
if clstContext == nil {
|
||||
log.Fatalf("Context %s does not exist in kubeconfig", args[0])
|
||||
log.Fatalf("Context %s does not exist in kubeconfig", contextName)
|
||||
}
|
||||
|
||||
overrides := clientcmd.ConfigOverrides{
|
||||
@@ -88,12 +102,16 @@ 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)
|
||||
if serviceAccount != "" {
|
||||
managerBearerToken, err = clusterauth.GetServiceAccountBearerToken(clientset, systemNamespace, serviceAccount)
|
||||
} else {
|
||||
managerBearerToken, err = clusterauth.InstallClusterManagerRBAC(clientset, systemNamespace, namespaces)
|
||||
}
|
||||
errors.CheckError(err)
|
||||
}
|
||||
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
|
||||
defer util.Close(conn)
|
||||
clst := NewCluster(args[0], conf, managerBearerToken, awsAuthConf)
|
||||
clst := newCluster(contextName, namespaces, conf, managerBearerToken, awsAuthConf)
|
||||
if inCluster {
|
||||
clst.Server = common.KubernetesInternalAPIServerAddr
|
||||
}
|
||||
@@ -101,17 +119,19 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
Cluster: clst,
|
||||
Upsert: upsert,
|
||||
}
|
||||
clst, err = clusterIf.Create(context.Background(), &clstCreateReq)
|
||||
_, err = clusterIf.Create(context.Background(), &clstCreateReq)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Cluster '%s' added\n", clst.Name)
|
||||
fmt.Printf("Cluster '%s' added\n", clst.Server)
|
||||
},
|
||||
}
|
||||
command.PersistentFlags().StringVar(&pathOpts.LoadingRules.ExplicitPath, pathOpts.ExplicitFileFlag, pathOpts.LoadingRules.ExplicitPath, "use a particular kubeconfig file")
|
||||
command.Flags().BoolVar(&inCluster, "in-cluster", false, "Indicates Argo CD resides inside this cluster and should connect using the internal k8s hostname (kubernetes.default.svc)")
|
||||
command.Flags().BoolVar(&upsert, "upsert", false, "Override an existing cluster with the same name even if the spec differs")
|
||||
command.Flags().StringVar(&awsClusterName, "aws-cluster-name", "", "AWS Cluster name if set then aws-iam-authenticator will be used to access cluster")
|
||||
command.Flags().StringVar(&serviceAccount, "service-account", "", fmt.Sprintf("System namespace service account to use for kubernetes resource management. If not set then default \"%s\" SA will be created", clusterauth.ArgoCDManagerServiceAccount))
|
||||
command.Flags().StringVar(&awsClusterName, "aws-cluster-name", "", "AWS Cluster name if set then aws cli eks token command will be used to access cluster")
|
||||
command.Flags().StringVar(&awsRoleArn, "aws-role-arn", "", "Optional AWS role arn. If set then AWS IAM Authenticator assume a role to perform cluster operations instead of the default AWS credential provider chain.")
|
||||
command.Flags().StringVar(&systemNamespace, "system-namespace", common.DefaultSystemNamespace, "Use different system namespace")
|
||||
command.Flags().StringArrayVar(&namespaces, "namespace", nil, "List of namespaces which are allowed to manage")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -154,7 +174,7 @@ func printKubeContexts(ca clientcmd.ConfigAccess) {
|
||||
}
|
||||
}
|
||||
|
||||
func NewCluster(name string, conf *rest.Config, managerBearerToken string, awsAuthConf *argoappv1.AWSAuthConfig) *argoappv1.Cluster {
|
||||
func newCluster(name string, namespaces []string, conf *rest.Config, managerBearerToken string, awsAuthConf *argoappv1.AWSAuthConfig) *argoappv1.Cluster {
|
||||
tlsClientConfig := argoappv1.TLSClientConfig{
|
||||
Insecure: conf.TLSClientConfig.Insecure,
|
||||
ServerName: conf.TLSClientConfig.ServerName,
|
||||
@@ -166,8 +186,9 @@ func NewCluster(name string, conf *rest.Config, managerBearerToken string, awsAu
|
||||
tlsClientConfig.CAData = data
|
||||
}
|
||||
clst := argoappv1.Cluster{
|
||||
Server: conf.Host,
|
||||
Name: name,
|
||||
Server: conf.Host,
|
||||
Name: name,
|
||||
Namespaces: namespaces,
|
||||
Config: argoappv1.ClusterConfig{
|
||||
BearerToken: managerBearerToken,
|
||||
TLSClientConfig: tlsClientConfig,
|
||||
@@ -179,9 +200,13 @@ func NewCluster(name string, conf *rest.Config, managerBearerToken string, awsAu
|
||||
|
||||
// NewClusterGetCommand returns a new instance of an `argocd cluster get` command
|
||||
func NewClusterGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "get CLUSTER",
|
||||
Short: "Get cluster information",
|
||||
Use: "get SERVER",
|
||||
Short: "Get cluster information",
|
||||
Example: `argocd cluster get https://12.34.567.89`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
@@ -189,23 +214,68 @@ func NewClusterGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
}
|
||||
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
|
||||
defer util.Close(conn)
|
||||
clusters := make([]argoappv1.Cluster, 0)
|
||||
for _, clusterName := range args {
|
||||
clst, err := clusterIf.Get(context.Background(), &clusterpkg.ClusterQuery{Server: clusterName})
|
||||
errors.CheckError(err)
|
||||
yamlBytes, err := yaml.Marshal(clst)
|
||||
clusters = append(clusters, *clst)
|
||||
}
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(clusters, output, true)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("%v", string(yamlBytes))
|
||||
case "wide", "":
|
||||
printClusterDetails(clusters)
|
||||
case "server":
|
||||
printClusterServers(clusters)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
// we have yaml as default to not break backwards-compatibility
|
||||
command.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: json|yaml|wide|server")
|
||||
return command
|
||||
}
|
||||
|
||||
func strWithDefault(value string, def string) string {
|
||||
if value == "" {
|
||||
return def
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func formatNamespaces(cluster argoappv1.Cluster) string {
|
||||
if len(cluster.Namespaces) == 0 {
|
||||
return "all namespaces"
|
||||
}
|
||||
return strings.Join(cluster.Namespaces, ", ")
|
||||
}
|
||||
|
||||
func printClusterDetails(clusters []argoappv1.Cluster) {
|
||||
for _, cluster := range clusters {
|
||||
fmt.Printf("Cluster information\n\n")
|
||||
fmt.Printf(" Server URL: %s\n", cluster.Server)
|
||||
fmt.Printf(" Server Name: %s\n", strWithDefault(cluster.Name, "-"))
|
||||
fmt.Printf(" Server Version: %s\n", cluster.ServerVersion)
|
||||
fmt.Printf(" Namespaces: %s\n", formatNamespaces(cluster))
|
||||
fmt.Printf("\nTLS configuration\n\n")
|
||||
fmt.Printf(" Client cert: %v\n", string(cluster.Config.TLSClientConfig.CertData) != "")
|
||||
fmt.Printf(" Cert validation: %v\n", !cluster.Config.TLSClientConfig.Insecure)
|
||||
fmt.Printf("\nAuthentication\n\n")
|
||||
fmt.Printf(" Basic authentication: %v\n", cluster.Config.Username != "")
|
||||
fmt.Printf(" oAuth authentication: %v\n", cluster.Config.BearerToken != "")
|
||||
fmt.Printf(" AWS authentication: %v\n", cluster.Config.AWSAuthConfig != nil)
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
// NewClusterRemoveCommand returns a new instance of an `argocd cluster list` command
|
||||
func NewClusterRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "rm CLUSTER",
|
||||
Short: "Remove cluster credentials",
|
||||
Use: "rm SERVER",
|
||||
Short: "Remove cluster credentials",
|
||||
Example: `argocd cluster rm https://12.34.567.89`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
@@ -234,7 +304,11 @@ func printClusterTable(clusters []argoappv1.Cluster) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintf(w, "SERVER\tNAME\tVERSION\tSTATUS\tMESSAGE\n")
|
||||
for _, c := range clusters {
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", c.Server, c.Name, c.ServerVersion, c.ConnectionState.Status, c.ConnectionState.Message)
|
||||
server := c.Server
|
||||
if len(c.Namespaces) > 0 {
|
||||
server = fmt.Sprintf("%s (%d namespaces)", c.Server, len(c.Namespaces))
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", server, c.Name, c.ServerVersion, c.ConnectionState.Status, c.ConnectionState.Message)
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
@@ -259,22 +333,29 @@ func NewClusterListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
defer util.Close(conn)
|
||||
clusters, err := clusterIf.List(context.Background(), &clusterpkg.ClusterQuery{})
|
||||
errors.CheckError(err)
|
||||
if output == "server" {
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(clusters.Items, output, false)
|
||||
errors.CheckError(err)
|
||||
case "server":
|
||||
printClusterServers(clusters.Items)
|
||||
} else {
|
||||
case "wide", "":
|
||||
printClusterTable(clusters.Items)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: wide|server")
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|server")
|
||||
return command
|
||||
}
|
||||
|
||||
// NewClusterRotateAuthCommand returns a new instance of an `argocd cluster rotate-auth` command
|
||||
func NewClusterRotateAuthCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "rotate-auth CLUSTER",
|
||||
Short: fmt.Sprintf("%s cluster rotate-auth CLUSTER", cliName),
|
||||
Use: "rotate-auth SERVER",
|
||||
Short: fmt.Sprintf("%s cluster rotate-auth SERVER", cliName),
|
||||
Example: fmt.Sprintf("%s cluster rotate-auth https://12.34.567.89", cliName),
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
)
|
||||
|
||||
const (
|
||||
cliName = "argocd"
|
||||
|
||||
@@ -7,3 +15,58 @@ const (
|
||||
// the OAuth2 login flow.
|
||||
DefaultSSOLocalPort = 8085
|
||||
)
|
||||
|
||||
// PrintResource prints a single resource in YAML or JSON format to stdout according to the output format
|
||||
func PrintResource(resource interface{}, output string) error {
|
||||
switch output {
|
||||
case "json":
|
||||
jsonBytes, err := json.MarshalIndent(resource, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(jsonBytes))
|
||||
case "yaml":
|
||||
yamlBytes, err := yaml.Marshal(resource)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Print(string(yamlBytes))
|
||||
default:
|
||||
return fmt.Errorf("unknown output format: %s", output)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrintResourceList marshals & prints a list of resources to stdout according to the output format
|
||||
func PrintResourceList(resources interface{}, output string, single bool) error {
|
||||
kt := reflect.ValueOf(resources)
|
||||
// Sometimes, we want to marshal the first resource of a slice or array as single item
|
||||
if kt.Kind() == reflect.Slice || kt.Kind() == reflect.Array {
|
||||
if single && kt.Len() == 1 {
|
||||
return PrintResource(kt.Index(0).Interface(), output)
|
||||
}
|
||||
|
||||
// If we have a zero len list, prevent printing "null"
|
||||
if kt.Len() == 0 {
|
||||
return PrintResource([]string{}, output)
|
||||
}
|
||||
}
|
||||
|
||||
switch output {
|
||||
case "json":
|
||||
jsonBytes, err := json.MarshalIndent(resources, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(string(jsonBytes))
|
||||
case "yaml":
|
||||
yamlBytes, err := yaml.Marshal(resources)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Print(string(yamlBytes))
|
||||
default:
|
||||
return fmt.Errorf("unknown output format: %s", output)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
142
cmd/argocd/commands/common_test.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Be careful with tabs vs. spaces in the following expected formats. Indents
|
||||
// should all be spaces, no tabs.
|
||||
const expectYamlSingle = `bar: ""
|
||||
baz: foo
|
||||
foo: bar
|
||||
`
|
||||
|
||||
const expectJsonSingle = `{
|
||||
"bar": "",
|
||||
"baz": "foo",
|
||||
"foo": "bar"
|
||||
}
|
||||
`
|
||||
const expectYamlList = `one:
|
||||
bar: ""
|
||||
baz: foo
|
||||
foo: bar
|
||||
two:
|
||||
bar: ""
|
||||
baz: foo
|
||||
foo: bar
|
||||
`
|
||||
|
||||
const expectJsonList = `{
|
||||
"one": {
|
||||
"bar": "",
|
||||
"baz": "foo",
|
||||
"foo": "bar"
|
||||
},
|
||||
"two": {
|
||||
"bar": "",
|
||||
"baz": "foo",
|
||||
"foo": "bar"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Rather dirty hack to capture stdout from PrintResource() and PrintResourceList()
|
||||
func captureOutput(f func() error) (string, error) {
|
||||
stdout := os.Stdout
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
os.Stdout = w
|
||||
err = f()
|
||||
w.Close()
|
||||
if err != nil {
|
||||
os.Stdout = stdout
|
||||
return "", err
|
||||
}
|
||||
str, err := ioutil.ReadAll(r)
|
||||
os.Stdout = stdout
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(str), err
|
||||
}
|
||||
|
||||
func Test_PrintResource(t *testing.T) {
|
||||
testResource := map[string]string{
|
||||
"foo": "bar",
|
||||
"bar": "",
|
||||
"baz": "foo",
|
||||
}
|
||||
|
||||
str, err := captureOutput(func() error {
|
||||
err := PrintResource(testResource, "yaml")
|
||||
return err
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, str, expectYamlSingle)
|
||||
|
||||
str, err = captureOutput(func() error {
|
||||
err := PrintResource(testResource, "json")
|
||||
return err
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, str, expectJsonSingle)
|
||||
|
||||
err = PrintResource(testResource, "unknown")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func Test_PrintResourceList(t *testing.T) {
|
||||
testResource := map[string]map[string]string{
|
||||
"one": {
|
||||
"foo": "bar",
|
||||
"bar": "",
|
||||
"baz": "foo",
|
||||
},
|
||||
"two": {
|
||||
"foo": "bar",
|
||||
"bar": "",
|
||||
"baz": "foo",
|
||||
},
|
||||
}
|
||||
|
||||
testResource2 := make([]map[string]string, 0)
|
||||
testResource2 = append(testResource2, testResource["one"])
|
||||
|
||||
str, err := captureOutput(func() error {
|
||||
err := PrintResourceList(testResource, "yaml", false)
|
||||
return err
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, str, expectYamlList)
|
||||
|
||||
str, err = captureOutput(func() error {
|
||||
err := PrintResourceList(testResource, "json", false)
|
||||
return err
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, str, expectJsonList)
|
||||
|
||||
str, err = captureOutput(func() error {
|
||||
err := PrintResourceList(testResource2, "yaml", true)
|
||||
return err
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, str, expectYamlSingle)
|
||||
|
||||
str, err = captureOutput(func() error {
|
||||
err := PrintResourceList(testResource2, "json", true)
|
||||
return err
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, str, expectJsonSingle)
|
||||
|
||||
err = PrintResourceList(testResource, "unknown", false)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -22,7 +20,7 @@ import (
|
||||
func NewContextCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var delete bool
|
||||
var command = &cobra.Command{
|
||||
Use: "context",
|
||||
Use: "context [CONTEXT]",
|
||||
Aliases: []string{"ctx"},
|
||||
Short: "Switch between contexts",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
@@ -30,22 +28,19 @@ func NewContextCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
localCfg, err := localconfig.ReadLocalConfig(clientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
|
||||
deletePresentContext := false
|
||||
c.Flags().Visit(func(f *pflag.Flag) {
|
||||
if f.Name == "delete" {
|
||||
deletePresentContext = true
|
||||
if delete {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
})
|
||||
err := deleteContext(args[0], clientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(args) == 0 {
|
||||
if deletePresentContext {
|
||||
err := deleteContext(localCfg.CurrentContext, clientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
return
|
||||
} else {
|
||||
printArgoCDContexts(clientOpts.ConfigPath)
|
||||
return
|
||||
}
|
||||
printArgoCDContexts(clientOpts.ConfigPath)
|
||||
return
|
||||
}
|
||||
|
||||
ctxName := args[0]
|
||||
@@ -100,7 +95,7 @@ func deleteContext(context, configPath string) error {
|
||||
errors.CheckError(err)
|
||||
} else {
|
||||
if localCfg.CurrentContext == context {
|
||||
localCfg.CurrentContext = localCfg.Contexts[0].Name
|
||||
localCfg.CurrentContext = ""
|
||||
}
|
||||
err = localconfig.ValidateLocalConfig(*localCfg)
|
||||
if err != nil {
|
||||
|
||||
@@ -11,20 +11,27 @@ import (
|
||||
)
|
||||
|
||||
const testConfig = `contexts:
|
||||
- name: argocd.example.com:443
|
||||
server: argocd.example.com:443
|
||||
user: argocd.example.com:443
|
||||
- name: argocd1.example.com:443
|
||||
server: argocd1.example.com:443
|
||||
user: argocd1.example.com:443
|
||||
- name: argocd2.example.com:443
|
||||
server: argocd2.example.com:443
|
||||
user: argocd2.example.com:443
|
||||
- name: localhost:8080
|
||||
server: localhost:8080
|
||||
user: localhost:8080
|
||||
current-context: localhost:8080
|
||||
servers:
|
||||
- server: argocd.example.com:443
|
||||
- server: argocd1.example.com:443
|
||||
- server: argocd2.example.com:443
|
||||
- plain-text: true
|
||||
server: localhost:8080
|
||||
users:
|
||||
- auth-token: vErrYS3c3tReFRe$hToken
|
||||
name: argocd.example.com:443
|
||||
name: argocd1.example.com:443
|
||||
refresh-token: vErrYS3c3tReFRe$hToken
|
||||
- auth-token: vErrYS3c3tReFRe$hToken
|
||||
name: argocd2.example.com:443
|
||||
refresh-token: vErrYS3c3tReFRe$hToken
|
||||
- auth-token: vErrYS3c3tReFRe$hToken
|
||||
name: localhost:8080`
|
||||
@@ -42,16 +49,30 @@ func TestContextDelete(t *testing.T) {
|
||||
assert.Equal(t, localConfig.CurrentContext, "localhost:8080")
|
||||
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "localhost:8080", Server: "localhost:8080", User: "localhost:8080"})
|
||||
|
||||
// Delete a non-current context
|
||||
err = deleteContext("argocd1.example.com:443", testConfigFilePath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
localConfig, err = localconfig.ReadLocalConfig(testConfigFilePath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, localConfig.CurrentContext, "localhost:8080")
|
||||
assert.NotContains(t, localConfig.Contexts, localconfig.ContextRef{Name: "argocd1.example.com:443", Server: "argocd1.example.com:443", User: "argocd1.example.com:443"})
|
||||
assert.NotContains(t, localConfig.Servers, localconfig.Server{Server: "argocd1.example.com:443"})
|
||||
assert.NotContains(t, localConfig.Users, localconfig.User{AuthToken: "vErrYS3c3tReFRe$hToken", Name: "argocd1.example.com:443"})
|
||||
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "argocd2.example.com:443", Server: "argocd2.example.com:443", User: "argocd2.example.com:443"})
|
||||
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "localhost:8080", Server: "localhost:8080", User: "localhost:8080"})
|
||||
|
||||
// Delete the current context
|
||||
err = deleteContext("localhost:8080", testConfigFilePath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
localConfig, err = localconfig.ReadLocalConfig(testConfigFilePath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, localConfig.CurrentContext, "argocd.example.com:443")
|
||||
assert.Equal(t, localConfig.CurrentContext, "")
|
||||
assert.NotContains(t, localConfig.Contexts, localconfig.ContextRef{Name: "localhost:8080", Server: "localhost:8080", User: "localhost:8080"})
|
||||
assert.NotContains(t, localConfig.Servers, localconfig.Server{PlainText: true, Server: "localhost:8080"})
|
||||
assert.NotContains(t, localConfig.Users, localconfig.User{AuthToken: "vErrYS3c3tReFRe$hToken", Name: "localhost:8080"})
|
||||
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "argocd.example.com:443", Server: "argocd.example.com:443", User: "argocd.example.com:443"})
|
||||
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "argocd2.example.com:443", Server: "argocd2.example.com:443", User: "argocd2.example.com:443"})
|
||||
|
||||
// Write the file again so that no conflicts are made in git
|
||||
err = ioutil.WriteFile(testConfigFilePath, []byte(testConfig), os.ModePerm)
|
||||
|
||||
@@ -259,11 +259,12 @@ func oauth2Login(ctx context.Context, port int, oidcSettings *settingspkg.OIDCCo
|
||||
}
|
||||
fmt.Printf("Performing %s flow login: %s\n", grantType, url)
|
||||
time.Sleep(1 * time.Second)
|
||||
err = open.Run(url)
|
||||
err = open.Start(url)
|
||||
errors.CheckError(err)
|
||||
go func() {
|
||||
log.Debugf("Listen: %s", srv.Addr)
|
||||
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
|
||||
log.Fatalf("listen: %s\n", err)
|
||||
log.Fatalf("Temporary HTTP server failed: %s", err)
|
||||
}
|
||||
}()
|
||||
errMsg := <-completionChan
|
||||
|
||||
@@ -30,7 +30,9 @@ func TestLogout(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, localConfig.CurrentContext, "localhost:8080")
|
||||
assert.NotContains(t, localConfig.Users, localconfig.User{AuthToken: "vErrYS3c3tReFRe$hToken", Name: "localhost:8080"})
|
||||
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "argocd.example.com:443", Server: "argocd.example.com:443", User: "argocd.example.com:443"})
|
||||
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "argocd1.example.com:443", Server: "argocd1.example.com:443", User: "argocd1.example.com:443"})
|
||||
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "argocd2.example.com:443", Server: "argocd2.example.com:443", User: "argocd2.example.com:443"})
|
||||
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "localhost:8080", Server: "localhost:8080", User: "localhost:8080"})
|
||||
|
||||
// Write the file again so that no conflicts are made in git
|
||||
err = ioutil.WriteFile(testConfigFilePath, []byte(testConfig), os.ModePerm)
|
||||
|
||||
@@ -527,14 +527,20 @@ func NewProjectListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
defer util.Close(conn)
|
||||
projects, err := projIf.List(context.Background(), &projectpkg.ProjectQuery{})
|
||||
errors.CheckError(err)
|
||||
if output == "name" {
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(projects.Items, output, false)
|
||||
errors.CheckError(err)
|
||||
case "name":
|
||||
printProjectNames(projects.Items)
|
||||
} else {
|
||||
case "wide", "":
|
||||
printProjectTable(projects.Items)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: wide|name")
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|name")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -580,9 +586,60 @@ func printProjectLine(w io.Writer, p *v1alpha1.AppProject) {
|
||||
fmt.Fprintf(w, "%s\t%s\t%v\t%v\t%v\t%v\t%v\n", p.Name, p.Spec.Description, destinations, sourceRepos, clusterWhitelist, namespaceBlacklist, formatOrphanedResources(p))
|
||||
}
|
||||
|
||||
func printProject(p *v1alpha1.AppProject) {
|
||||
const printProjFmtStr = "%-34s%s\n"
|
||||
|
||||
fmt.Printf(printProjFmtStr, "Name:", p.Name)
|
||||
fmt.Printf(printProjFmtStr, "Description:", p.Spec.Description)
|
||||
|
||||
// Print destinations
|
||||
dest0 := "<none>"
|
||||
if len(p.Spec.Destinations) > 0 {
|
||||
dest0 = fmt.Sprintf("%s,%s", p.Spec.Destinations[0].Server, p.Spec.Destinations[0].Namespace)
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Destinations:", dest0)
|
||||
for i := 1; i < len(p.Spec.Destinations); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s,%s", p.Spec.Destinations[i].Server, p.Spec.Destinations[i].Namespace))
|
||||
}
|
||||
|
||||
// Print sources
|
||||
src0 := "<none>"
|
||||
if len(p.Spec.SourceRepos) > 0 {
|
||||
src0 = p.Spec.SourceRepos[0]
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Repositories:", src0)
|
||||
for i := 1; i < len(p.Spec.SourceRepos); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", p.Spec.SourceRepos[i])
|
||||
}
|
||||
|
||||
// Print whitelisted cluster resources
|
||||
cwl0 := "<none>"
|
||||
if len(p.Spec.ClusterResourceWhitelist) > 0 {
|
||||
cwl0 = fmt.Sprintf("%s/%s", p.Spec.ClusterResourceWhitelist[0].Group, p.Spec.ClusterResourceWhitelist[0].Kind)
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Whitelisted Cluster Resources:", cwl0)
|
||||
for i := 1; i < len(p.Spec.ClusterResourceWhitelist); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s/%s", p.Spec.ClusterResourceWhitelist[i].Group, p.Spec.ClusterResourceWhitelist[i].Kind))
|
||||
}
|
||||
|
||||
// Print blacklisted namespaced resources
|
||||
rbl0 := "<none>"
|
||||
if len(p.Spec.NamespaceResourceBlacklist) > 0 {
|
||||
rbl0 = fmt.Sprintf("%s/%s", p.Spec.NamespaceResourceBlacklist[0].Group, p.Spec.NamespaceResourceBlacklist[0].Kind)
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Blacklisted Namespaced Resources:", rbl0)
|
||||
for i := 1; i < len(p.Spec.NamespaceResourceBlacklist); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s/%s", p.Spec.NamespaceResourceBlacklist[i].Group, p.Spec.NamespaceResourceBlacklist[i].Kind))
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Orphaned Resources:", formatOrphanedResources(p))
|
||||
|
||||
}
|
||||
|
||||
// NewProjectGetCommand returns a new instance of an `argocd proj get` command
|
||||
func NewProjectGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
const printProjFmtStr = "%-34s%s\n"
|
||||
var (
|
||||
output string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "get PROJECT",
|
||||
Short: "Get project details",
|
||||
@@ -596,51 +653,19 @@ func NewProjectGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
defer util.Close(conn)
|
||||
p, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf(printProjFmtStr, "Name:", p.Name)
|
||||
fmt.Printf(printProjFmtStr, "Description:", p.Spec.Description)
|
||||
|
||||
// Print destinations
|
||||
dest0 := "<none>"
|
||||
if len(p.Spec.Destinations) > 0 {
|
||||
dest0 = fmt.Sprintf("%s,%s", p.Spec.Destinations[0].Server, p.Spec.Destinations[0].Namespace)
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResource(p, output)
|
||||
errors.CheckError(err)
|
||||
case "wide", "":
|
||||
printProject(p)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Destinations:", dest0)
|
||||
for i := 1; i < len(p.Spec.Destinations); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s,%s", p.Spec.Destinations[i].Server, p.Spec.Destinations[i].Namespace))
|
||||
}
|
||||
|
||||
// Print sources
|
||||
src0 := "<none>"
|
||||
if len(p.Spec.SourceRepos) > 0 {
|
||||
src0 = p.Spec.SourceRepos[0]
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Repositories:", src0)
|
||||
for i := 1; i < len(p.Spec.SourceRepos); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", p.Spec.SourceRepos[i])
|
||||
}
|
||||
|
||||
// Print whitelisted cluster resources
|
||||
cwl0 := "<none>"
|
||||
if len(p.Spec.ClusterResourceWhitelist) > 0 {
|
||||
cwl0 = fmt.Sprintf("%s/%s", p.Spec.ClusterResourceWhitelist[0].Group, p.Spec.ClusterResourceWhitelist[0].Kind)
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Whitelisted Cluster Resources:", cwl0)
|
||||
for i := 1; i < len(p.Spec.ClusterResourceWhitelist); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s/%s", p.Spec.ClusterResourceWhitelist[i].Group, p.Spec.ClusterResourceWhitelist[i].Kind))
|
||||
}
|
||||
|
||||
// Print blacklisted namespaced resources
|
||||
rbl0 := "<none>"
|
||||
if len(p.Spec.NamespaceResourceBlacklist) > 0 {
|
||||
rbl0 = fmt.Sprintf("%s/%s", p.Spec.NamespaceResourceBlacklist[0].Group, p.Spec.NamespaceResourceBlacklist[0].Kind)
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Blacklisted Namespaced Resources:", rbl0)
|
||||
for i := 1; i < len(p.Spec.NamespaceResourceBlacklist); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s/%s", p.Spec.NamespaceResourceBlacklist[i].Group, p.Spec.NamespaceResourceBlacklist[i].Kind))
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Orphaned Resources:", formatOrphanedResources(p))
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide")
|
||||
return command
|
||||
}
|
||||
|
||||
|
||||
@@ -285,14 +285,20 @@ func NewProjectRoleListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
|
||||
project, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
if output == "name" {
|
||||
switch output {
|
||||
case "json", "yaml":
|
||||
err := PrintResourceList(project.Spec.Roles, output, false)
|
||||
errors.CheckError(err)
|
||||
case "name":
|
||||
printProjectRoleListName(project.Spec.Roles)
|
||||
} else {
|
||||
case "wide", "":
|
||||
printProjectRoleListTable(project.Spec.Roles)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: wide|name")
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|name")
|
||||
return command
|
||||
}
|
||||
|
||||
|
||||
@@ -235,6 +235,9 @@ func NewProjectWindowsUpdateCommand(clientOpts *argocdclient.ClientOptions) *cob
|
||||
|
||||
// NewProjectWindowsListCommand returns a new instance of an `argocd proj windows list` command
|
||||
func NewProjectWindowsListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "list PROJECT",
|
||||
Short: "List project sync windows",
|
||||
@@ -249,10 +252,18 @@ func NewProjectWindowsListCommand(clientOpts *argocdclient.ClientOptions) *cobra
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
printSyncWindows(proj)
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(proj.Spec.SyncWindows, output, false)
|
||||
errors.CheckError(err)
|
||||
case "wide", "":
|
||||
printSyncWindows(proj)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide")
|
||||
return command
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
repositorypkg "github.com/argoproj/argo-cd/pkg/apiclient/repository"
|
||||
@@ -50,13 +51,23 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
)
|
||||
|
||||
// For better readability and easier formatting
|
||||
var repoAddExamples = `
|
||||
Add a SSH repository using a private key for authentication, ignoring the server's host key:",
|
||||
$ argocd repo add git@git.example.com --insecure-ignore-host-key --ssh-private-key-path ~/id_rsa",
|
||||
Add a HTTPS repository using username/password and TLS client certificates:",
|
||||
$ argocd repo add https://git.example.com --username git --password secret --tls-client-cert-path ~/mycert.crt --tls-client-cert-key-path ~/mycert.key",
|
||||
Add a HTTPS repository using username/password without verifying the server's TLS certificate:",
|
||||
$ argocd repo add https://git.example.com --username git --password secret --insecure-skip-server-verification",
|
||||
var repoAddExamples = ` # Add a Git repository via SSH using a private key for authentication, ignoring the server's host key:
|
||||
argocd repo add git@git.example.com:repos/repo --insecure-ignore-host-key --ssh-private-key-path ~/id_rsa
|
||||
|
||||
# Add a Git repository via SSH on a non-default port - need to use ssh:// style URLs here
|
||||
argocd repo add ssh://git@git.example.com:2222/repos/repo --ssh-private-key-path ~/id_rsa
|
||||
|
||||
# Add a private Git repository via HTTPS using username/password and TLS client certificates:
|
||||
argocd repo add https://git.example.com/repos/repo --username git --password secret --tls-client-cert-path ~/mycert.crt --tls-client-cert-key-path ~/mycert.key
|
||||
|
||||
# Add a private Git repository via HTTPS using username/password without verifying the server's TLS certificate
|
||||
argocd repo add https://git.example.com/repos/repo --username git --password secret --insecure-skip-server-verification
|
||||
|
||||
# Add a public Helm repository named 'stable' via HTTPS
|
||||
argocd repo add https://kubernetes-charts.storage.googleapis.com --type helm --name stable
|
||||
|
||||
# Add a private Helm repository named 'stable' via HTTPS
|
||||
argocd repo add https://kubernetes-charts.storage.googleapis.com --type helm --name stable --username test --password test
|
||||
`
|
||||
|
||||
var command = &cobra.Command{
|
||||
@@ -108,11 +119,17 @@ Add a HTTPS repository using username/password without verifying the server's TL
|
||||
}
|
||||
}
|
||||
|
||||
// Set repository connection properties only when creating repository, not
|
||||
// when creating repository credentials.
|
||||
// InsecureIgnoreHostKey is deprecated and only here for backwards compat
|
||||
repo.InsecureIgnoreHostKey = insecureIgnoreHostKey
|
||||
repo.Insecure = insecureSkipServerVerification
|
||||
repo.EnableLFS = enableLfs
|
||||
|
||||
if repo.Type == "helm" && repo.Name == "" {
|
||||
errors.CheckError(fmt.Errorf("Must specify --name for repos of type 'helm'"))
|
||||
}
|
||||
|
||||
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
@@ -125,6 +142,10 @@ Add a HTTPS repository using username/password without verifying the server's TL
|
||||
// We let the server check access to the repository before adding it. If
|
||||
// it is a private repo, but we cannot access with with the credentials
|
||||
// that were supplied, we bail out.
|
||||
//
|
||||
// Skip validation if we are just adding credentials template, chances
|
||||
// are high that we do not have the given URL pointing to a valid Git
|
||||
// repo anyway.
|
||||
repoAccessReq := repositorypkg.RepoAccessQuery{
|
||||
Repo: repo.Repo,
|
||||
Type: repo.Type,
|
||||
@@ -143,19 +164,20 @@ Add a HTTPS repository using username/password without verifying the server's TL
|
||||
Repo: &repo,
|
||||
Upsert: upsert,
|
||||
}
|
||||
createdRepo, err := repoIf.Create(context.Background(), &repoCreateReq)
|
||||
|
||||
createdRepo, err := repoIf.CreateRepository(context.Background(), &repoCreateReq)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("repository '%s' added\n", createdRepo.Repo)
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&repo.Type, "type", "", "type of the repository, \"git\" or \"helm\"")
|
||||
command.Flags().StringVar(&repo.Name, "name", "", "name of the repository")
|
||||
command.Flags().StringVar(&repo.Type, "type", common.DefaultRepoType, "type of the repository, \"git\" or \"helm\"")
|
||||
command.Flags().StringVar(&repo.Name, "name", "", "name of the repository, mandatory for repositories of type helm")
|
||||
command.Flags().StringVar(&repo.Username, "username", "", "username to the repository")
|
||||
command.Flags().StringVar(&repo.Password, "password", "", "password to the repository")
|
||||
command.Flags().StringVar(&sshPrivateKeyPath, "ssh-private-key-path", "", "path to the private ssh key (e.g. ~/.ssh/id_rsa)")
|
||||
command.Flags().StringVar(&tlsClientCertPath, "tls-client-cert-path", "", "path to the TLS client cert (must be PEM format)")
|
||||
command.Flags().StringVar(&tlsClientCertKeyPath, "tls-client-cert-key-path", "", "path to the TLS client cert's key path (must be PEM format)")
|
||||
command.Flags().BoolVar(&insecureIgnoreHostKey, "insecure-ignore-host-key", false, "disables SSH strict host key checking (deprecated, use --insecure-skip-server-validation instead)")
|
||||
command.Flags().BoolVar(&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")
|
||||
@@ -175,7 +197,7 @@ func NewRepoRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoClientOrDie()
|
||||
defer util.Close(conn)
|
||||
for _, repoURL := range args {
|
||||
_, err := repoIf.Delete(context.Background(), &repositorypkg.RepoQuery{Repo: repoURL})
|
||||
_, err := repoIf.DeleteRepository(context.Background(), &repositorypkg.RepoQuery{Repo: repoURL})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
@@ -186,20 +208,24 @@ func NewRepoRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
// Print table of repo info
|
||||
func printRepoTable(repos appsv1.Repositories) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintf(w, "TYPE\tNAME\tREPO\tINSECURE\tLFS\tUSER\tSTATUS\tMESSAGE\n")
|
||||
_, _ = fmt.Fprintf(w, "TYPE\tNAME\tREPO\tINSECURE\tLFS\tCREDS\tSTATUS\tMESSAGE\n")
|
||||
for _, r := range repos {
|
||||
var username string
|
||||
if r.Username == "" {
|
||||
username = "-"
|
||||
var hasCreds string
|
||||
if !r.HasCredentials() {
|
||||
hasCreds = "false"
|
||||
} else {
|
||||
username = r.Username
|
||||
if r.InheritedCreds {
|
||||
hasCreds = "inherited"
|
||||
} else {
|
||||
hasCreds = "true"
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%v\t%s\t%s\t%s\n", r.Type, r.Name, r.Repo, r.IsInsecure(), r.EnableLFS, username, r.ConnectionState.Status, r.ConnectionState.Message)
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%v\t%s\t%s\t%s\n", r.Type, r.Name, r.Repo, r.IsInsecure(), r.EnableLFS, hasCreds, r.ConnectionState.Status, r.ConnectionState.Message)
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
// Print list of repo urls
|
||||
// Print list of repo urls or url patterns for repository credentials
|
||||
func printRepoUrls(repos appsv1.Repositories) {
|
||||
for _, r := range repos {
|
||||
fmt.Println(r.Repo)
|
||||
@@ -209,7 +235,8 @@ func printRepoUrls(repos appsv1.Repositories) {
|
||||
// NewRepoListCommand returns a new instance of an `argocd repo rm` command
|
||||
func NewRepoListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
output string
|
||||
refresh string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "list",
|
||||
@@ -217,15 +244,32 @@ func NewRepoListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoClientOrDie()
|
||||
defer util.Close(conn)
|
||||
repos, err := repoIf.List(context.Background(), &repositorypkg.RepoQuery{})
|
||||
forceRefresh := false
|
||||
switch refresh {
|
||||
case "":
|
||||
case "hard":
|
||||
forceRefresh = true
|
||||
default:
|
||||
err := fmt.Errorf("--refresh must be one of: 'hard'")
|
||||
errors.CheckError(err)
|
||||
}
|
||||
repos, err := repoIf.ListRepositories(context.Background(), &repositorypkg.RepoQuery{ForceRefresh: forceRefresh})
|
||||
errors.CheckError(err)
|
||||
if output == "url" {
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(repos.Items, output, false)
|
||||
errors.CheckError(err)
|
||||
case "url":
|
||||
printRepoUrls(repos.Items)
|
||||
} else {
|
||||
// wide is the default
|
||||
case "wide", "":
|
||||
printRepoTable(repos.Items)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: wide|url")
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|url")
|
||||
command.Flags().StringVar(&refresh, "refresh", "", "Force a cache refresh on connection status")
|
||||
return command
|
||||
}
|
||||
|
||||
203
cmd/argocd/commands/repocreds.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
repocredspkg "github.com/argoproj/argo-cd/pkg/apiclient/repocreds"
|
||||
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
)
|
||||
|
||||
// NewRepoCredsCommand returns a new instance of an `argocd repocreds` command
|
||||
func NewRepoCredsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "repocreds",
|
||||
Short: "Manage repository connection parameters",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
},
|
||||
}
|
||||
|
||||
command.AddCommand(NewRepoCredsAddCommand(clientOpts))
|
||||
command.AddCommand(NewRepoCredsListCommand(clientOpts))
|
||||
command.AddCommand(NewRepoCredsRemoveCommand(clientOpts))
|
||||
return command
|
||||
}
|
||||
|
||||
// NewRepoCredsAddCommand returns a new instance of an `argocd repocreds add` command
|
||||
func NewRepoCredsAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
repo appsv1.RepoCreds
|
||||
upsert bool
|
||||
sshPrivateKeyPath string
|
||||
tlsClientCertPath string
|
||||
tlsClientCertKeyPath string
|
||||
)
|
||||
|
||||
// For better readability and easier formatting
|
||||
var repocredsAddExamples = ` # Add credentials with user/pass authentication to use for all repositories under https://git.example.com/repos
|
||||
argocd repocreds add https://git.example.com/repos/ --username git --password secret
|
||||
|
||||
# Add credentials with SSH private key authentication to use for all repositories under ssh://git@git.example.com/repos
|
||||
argocd repocreds add ssh://git@git.example.com/repos/ --ssh-private-key-path ~/.ssh/id_rsa
|
||||
`
|
||||
|
||||
var command = &cobra.Command{
|
||||
Use: "add REPOURL",
|
||||
Short: "Add git repository connection parameters",
|
||||
Example: repocredsAddExamples,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Repository URL
|
||||
repo.URL = args[0]
|
||||
|
||||
// Specifying ssh-private-key-path is only valid for SSH repositories
|
||||
if sshPrivateKeyPath != "" {
|
||||
if ok, _ := git.IsSSHURL(repo.URL); ok {
|
||||
keyData, err := ioutil.ReadFile(sshPrivateKeyPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
repo.SSHPrivateKey = string(keyData)
|
||||
} else {
|
||||
err := fmt.Errorf("--ssh-private-key-path is only supported for SSH repositories.")
|
||||
errors.CheckError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// tls-client-cert-path and tls-client-cert-key-key-path must always be
|
||||
// specified together
|
||||
if (tlsClientCertPath != "" && tlsClientCertKeyPath == "") || (tlsClientCertPath == "" && tlsClientCertKeyPath != "") {
|
||||
err := fmt.Errorf("--tls-client-cert-path and --tls-client-cert-key-path must be specified together")
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
// Specifying tls-client-cert-path is only valid for HTTPS repositories
|
||||
if tlsClientCertPath != "" {
|
||||
if git.IsHTTPSURL(repo.URL) {
|
||||
tlsCertData, err := ioutil.ReadFile(tlsClientCertPath)
|
||||
errors.CheckError(err)
|
||||
tlsCertKey, err := ioutil.ReadFile(tlsClientCertKeyPath)
|
||||
errors.CheckError(err)
|
||||
repo.TLSClientCertData = string(tlsCertData)
|
||||
repo.TLSClientCertKey = string(tlsCertKey)
|
||||
} else {
|
||||
err := fmt.Errorf("--tls-client-cert-path is only supported for HTTPS repositories")
|
||||
errors.CheckError(err)
|
||||
}
|
||||
}
|
||||
|
||||
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoCredsClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
// If the user set a username, but didn't supply password via --password,
|
||||
// then we prompt for it
|
||||
if repo.Username != "" && repo.Password == "" {
|
||||
repo.Password = cli.PromptPassword(repo.Password)
|
||||
}
|
||||
|
||||
repoCreateReq := repocredspkg.RepoCredsCreateRequest{
|
||||
Creds: &repo,
|
||||
Upsert: upsert,
|
||||
}
|
||||
|
||||
createdRepo, err := repoIf.CreateRepositoryCredentials(context.Background(), &repoCreateReq)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("repository credentials for '%s' added\n", createdRepo.URL)
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&repo.Username, "username", "", "username to the repository")
|
||||
command.Flags().StringVar(&repo.Password, "password", "", "password to the repository")
|
||||
command.Flags().StringVar(&sshPrivateKeyPath, "ssh-private-key-path", "", "path to the private ssh key (e.g. ~/.ssh/id_rsa)")
|
||||
command.Flags().StringVar(&tlsClientCertPath, "tls-client-cert-path", "", "path to the TLS client cert (must be PEM format)")
|
||||
command.Flags().StringVar(&tlsClientCertKeyPath, "tls-client-cert-key-path", "", "path to the TLS client cert's key path (must be PEM format)")
|
||||
command.Flags().BoolVar(&upsert, "upsert", false, "Override an existing repository with the same name even if the spec differs")
|
||||
return command
|
||||
}
|
||||
|
||||
// NewRepoCredsRemoveCommand returns a new instance of an `argocd repocreds rm` command
|
||||
func NewRepoCredsRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "rm CREDSURL",
|
||||
Short: "Remove repository credentials",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoCredsClientOrDie()
|
||||
defer util.Close(conn)
|
||||
for _, repoURL := range args {
|
||||
_, err := repoIf.DeleteRepositoryCredentials(context.Background(), &repocredspkg.RepoCredsDeleteRequest{Url: repoURL})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// Print the repository credentials as table
|
||||
func printRepoCredsTable(repos []appsv1.RepoCreds) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "URL PATTERN\tUSERNAME\tSSH_CREDS\tTLS_CREDS\n")
|
||||
for _, r := range repos {
|
||||
if r.Username == "" {
|
||||
r.Username = "-"
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%v\t%v\n", r.URL, r.Username, r.SSHPrivateKey != "", r.TLSClientCertData != "")
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
// Print list of repo urls or url patterns for repository credentials
|
||||
func printRepoCredsUrls(repos []appsv1.RepoCreds) {
|
||||
for _, r := range repos {
|
||||
fmt.Println(r.URL)
|
||||
}
|
||||
}
|
||||
|
||||
// NewRepoCredsListCommand returns a new instance of an `argocd repo list` command
|
||||
func NewRepoCredsListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List configured repository credentials",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoCredsClientOrDie()
|
||||
defer util.Close(conn)
|
||||
repos, err := repoIf.ListRepositoryCredentials(context.Background(), &repocredspkg.RepoCredsQuery{})
|
||||
errors.CheckError(err)
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(repos.Items, output, false)
|
||||
errors.CheckError(err)
|
||||
case "url":
|
||||
printRepoCredsUrls(repos.Items)
|
||||
case "wide", "":
|
||||
printRepoCredsTable(repos.Items)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|url")
|
||||
return command
|
||||
}
|
||||
@@ -43,6 +43,7 @@ func NewCommand() *cobra.Command {
|
||||
command.AddCommand(NewLoginCommand(&clientOpts))
|
||||
command.AddCommand(NewReloginCommand(&clientOpts))
|
||||
command.AddCommand(NewRepoCommand(&clientOpts))
|
||||
command.AddCommand(NewRepoCredsCommand(&clientOpts))
|
||||
command.AddCommand(NewContextCommand(&clientOpts))
|
||||
command.AddCommand(NewProjectCommand(&clientOpts))
|
||||
command.AddCommand(NewAccountCommand(&clientOpts))
|
||||
@@ -60,5 +61,7 @@ func NewCommand() *cobra.Command {
|
||||
command.PersistentFlags().BoolVar(&clientOpts.GRPCWeb, "grpc-web", config.GetBoolFlag("grpc-web"), "Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2.")
|
||||
command.PersistentFlags().StringVar(&logLevel, "loglevel", config.GetFlag("loglevel", "info"), "Set the logging level. One of: debug|info|warn|error")
|
||||
command.PersistentFlags().StringSliceVarP(&clientOpts.Headers, "header", "H", []string{}, "Sets additional header to all requests made by Argo CD CLI. (Can be repeated multiple times to add multiple headers, also supports comma separated headers)")
|
||||
command.PersistentFlags().BoolVar(&clientOpts.PortForward, "port-forward", config.GetBoolFlag("port-forward"), "Connect to a random argocd-server port using port forwarding")
|
||||
command.PersistentFlags().StringVar(&clientOpts.PortForwardNamespace, "port-forward-namespace", config.GetFlag("port-forward-namespace", ""), "Namespace name which should be used for port forwarding")
|
||||
return command
|
||||
}
|
||||
|
||||
17
cmd/argocd/commands/testdata/config
vendored
@@ -1,18 +1,25 @@
|
||||
contexts:
|
||||
- name: argocd.example.com:443
|
||||
server: argocd.example.com:443
|
||||
user: argocd.example.com:443
|
||||
- name: argocd1.example.com:443
|
||||
server: argocd1.example.com:443
|
||||
user: argocd1.example.com:443
|
||||
- name: argocd2.example.com:443
|
||||
server: argocd2.example.com:443
|
||||
user: argocd2.example.com:443
|
||||
- name: localhost:8080
|
||||
server: localhost:8080
|
||||
user: localhost:8080
|
||||
current-context: localhost:8080
|
||||
servers:
|
||||
- server: argocd.example.com:443
|
||||
- server: argocd1.example.com:443
|
||||
- server: argocd2.example.com:443
|
||||
- plain-text: true
|
||||
server: localhost:8080
|
||||
users:
|
||||
- auth-token: vErrYS3c3tReFRe$hToken
|
||||
name: argocd.example.com:443
|
||||
name: argocd1.example.com:443
|
||||
refresh-token: vErrYS3c3tReFRe$hToken
|
||||
- auth-token: vErrYS3c3tReFRe$hToken
|
||||
name: argocd2.example.com:443
|
||||
refresh-token: vErrYS3c3tReFRe$hToken
|
||||
- auth-token: vErrYS3c3tReFRe$hToken
|
||||
name: localhost:8080
|
||||
@@ -3,6 +3,7 @@ package commands
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -10,60 +11,113 @@ import (
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/pkg/apiclient/version"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
)
|
||||
|
||||
// NewVersionCmd returns a new `version` command to be used as a sub-command to root
|
||||
func NewVersionCmd(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var short bool
|
||||
var client bool
|
||||
var (
|
||||
short bool
|
||||
client bool
|
||||
output string
|
||||
)
|
||||
|
||||
versionCmd := cobra.Command{
|
||||
Use: "version",
|
||||
Short: fmt.Sprintf("Print version information"),
|
||||
Example: ` # Print the full version of client and server to stdout
|
||||
argocd version
|
||||
|
||||
# Print only full version of the client - no connection to server will be made
|
||||
argocd version --client
|
||||
|
||||
# Print the full version of client and server in JSON format
|
||||
argocd version -o json
|
||||
|
||||
# Print only client and server core version strings in YAML format
|
||||
argocd version --short -o yaml
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
version := common.GetVersion()
|
||||
fmt.Printf("%s: %s\n", cliName, version)
|
||||
if !short {
|
||||
fmt.Printf(" BuildDate: %s\n", version.BuildDate)
|
||||
fmt.Printf(" GitCommit: %s\n", version.GitCommit)
|
||||
fmt.Printf(" GitTreeState: %s\n", version.GitTreeState)
|
||||
if version.GitTag != "" {
|
||||
fmt.Printf(" GitTag: %s\n", version.GitTag)
|
||||
var (
|
||||
versionIf version.VersionServiceClient
|
||||
serverVers *version.VersionMessage
|
||||
conn io.Closer
|
||||
err error
|
||||
)
|
||||
if !client {
|
||||
// Get Server version
|
||||
conn, versionIf = argocdclient.NewClientOrDie(clientOpts).NewVersionClientOrDie()
|
||||
defer util.Close(conn)
|
||||
serverVers, err = versionIf.Version(context.Background(), &empty.Empty{})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
clientVers := common.GetVersion()
|
||||
version := make(map[string]interface{})
|
||||
if !short {
|
||||
version["client"] = clientVers
|
||||
} else {
|
||||
version["client"] = map[string]string{cliName: clientVers.Version}
|
||||
}
|
||||
fmt.Printf(" GoVersion: %s\n", version.GoVersion)
|
||||
fmt.Printf(" Compiler: %s\n", version.Compiler)
|
||||
fmt.Printf(" Platform: %s\n", version.Platform)
|
||||
}
|
||||
if client {
|
||||
return
|
||||
}
|
||||
|
||||
// Get Server version
|
||||
conn, versionIf := argocdclient.NewClientOrDie(clientOpts).NewVersionClientOrDie()
|
||||
defer util.Close(conn)
|
||||
serverVers, err := versionIf.Version(context.Background(), &empty.Empty{})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("%s: %s\n", "argocd-server", serverVers.Version)
|
||||
if !short {
|
||||
fmt.Printf(" BuildDate: %s\n", serverVers.BuildDate)
|
||||
fmt.Printf(" GitCommit: %s\n", serverVers.GitCommit)
|
||||
fmt.Printf(" GitTreeState: %s\n", serverVers.GitTreeState)
|
||||
if version.GitTag != "" {
|
||||
fmt.Printf(" GitTag: %s\n", serverVers.GitTag)
|
||||
if !client {
|
||||
if !short {
|
||||
version["server"] = serverVers
|
||||
} else {
|
||||
version["server"] = map[string]string{"argocd-server": serverVers.Version}
|
||||
}
|
||||
}
|
||||
fmt.Printf(" GoVersion: %s\n", serverVers.GoVersion)
|
||||
fmt.Printf(" Compiler: %s\n", serverVers.Compiler)
|
||||
fmt.Printf(" Platform: %s\n", serverVers.Platform)
|
||||
fmt.Printf(" Ksonnet Version: %s\n", serverVers.KsonnetVersion)
|
||||
fmt.Printf(" Kustomize Version: %s\n", serverVers.KustomizeVersion)
|
||||
fmt.Printf(" Helm Version: %s\n", serverVers.HelmVersion)
|
||||
fmt.Printf(" Kubectl Version: %s\n", serverVers.KubectlVersion)
|
||||
err := PrintResource(version, output)
|
||||
errors.CheckError(err)
|
||||
case "short":
|
||||
printVersion(serverVers, client, true)
|
||||
case "wide", "":
|
||||
// we use value of short for backward compatibility
|
||||
printVersion(serverVers, client, short)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
versionCmd.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|short")
|
||||
versionCmd.Flags().BoolVar(&short, "short", false, "print just the version number")
|
||||
versionCmd.Flags().BoolVar(&client, "client", false, "client version only (no server required)")
|
||||
return &versionCmd
|
||||
}
|
||||
|
||||
func printVersion(serverVers *version.VersionMessage, client bool, short bool) {
|
||||
version := common.GetVersion()
|
||||
fmt.Printf("%s: %s\n", cliName, version)
|
||||
if !short {
|
||||
fmt.Printf(" BuildDate: %s\n", version.BuildDate)
|
||||
fmt.Printf(" GitCommit: %s\n", version.GitCommit)
|
||||
fmt.Printf(" GitTreeState: %s\n", version.GitTreeState)
|
||||
if version.GitTag != "" {
|
||||
fmt.Printf(" GitTag: %s\n", version.GitTag)
|
||||
}
|
||||
fmt.Printf(" GoVersion: %s\n", version.GoVersion)
|
||||
fmt.Printf(" Compiler: %s\n", version.Compiler)
|
||||
fmt.Printf(" Platform: %s\n", version.Platform)
|
||||
}
|
||||
if client {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("%s: %s\n", "argocd-server", serverVers.Version)
|
||||
if !short {
|
||||
fmt.Printf(" BuildDate: %s\n", serverVers.BuildDate)
|
||||
fmt.Printf(" GitCommit: %s\n", serverVers.GitCommit)
|
||||
fmt.Printf(" GitTreeState: %s\n", serverVers.GitTreeState)
|
||||
if version.GitTag != "" {
|
||||
fmt.Printf(" GitTag: %s\n", serverVers.GitTag)
|
||||
}
|
||||
fmt.Printf(" GoVersion: %s\n", serverVers.GoVersion)
|
||||
fmt.Printf(" Compiler: %s\n", serverVers.Compiler)
|
||||
fmt.Printf(" Platform: %s\n", serverVers.Platform)
|
||||
fmt.Printf(" Ksonnet Version: %s\n", serverVers.KsonnetVersion)
|
||||
fmt.Printf(" Kustomize Version: %s\n", serverVers.KustomizeVersion)
|
||||
fmt.Printf(" Helm Version: %s\n", serverVers.HelmVersion)
|
||||
fmt.Printf(" Kubectl Version: %s\n", serverVers.KubectlVersion)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -21,9 +27,10 @@ const (
|
||||
ArgoCDTLSCertsConfigMapName = "argocd-tls-certs-cm"
|
||||
)
|
||||
|
||||
// Default system namespace
|
||||
// Some default configurables
|
||||
const (
|
||||
DefaultSystemNamespace = "kube-system"
|
||||
DefaultRepoType = "git"
|
||||
)
|
||||
|
||||
// Default listener ports for ArgoCD components
|
||||
@@ -37,8 +44,6 @@ 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
|
||||
@@ -61,10 +66,8 @@ const (
|
||||
AuthCookieName = "argocd.token"
|
||||
// RevisionHistoryLimit is the max number of successful sync to keep in history
|
||||
RevisionHistoryLimit = 10
|
||||
// K8sClientConfigQPS controls the QPS to be used in K8s REST client configs
|
||||
K8sClientConfigQPS = 25
|
||||
// K8sClientConfigBurst controls the burst to be used in K8s REST client configs
|
||||
K8sClientConfigBurst = 50
|
||||
// ChangePasswordSSOTokenMaxAge is the max token age for password change operation
|
||||
ChangePasswordSSOTokenMaxAge = time.Minute * 5
|
||||
)
|
||||
|
||||
// Dex related constants
|
||||
@@ -135,6 +138,14 @@ const (
|
||||
EnvVarTLSDataPath = "ARGOCD_TLS_DATA_PATH"
|
||||
// Specifies number of git remote operations attempts count
|
||||
EnvGitAttemptsCount = "ARGOCD_GIT_ATTEMPTS_COUNT"
|
||||
// Overrides git submodule support, true by default
|
||||
EnvGitSubmoduleEnabled = "ARGOCD_GIT_MODULES_ENABLED"
|
||||
// EnvK8sClientQPS is the QPS value used for the kubernetes client (default: 50)
|
||||
EnvK8sClientQPS = "ARGOCD_K8S_CLIENT_QPS"
|
||||
// EnvK8sClientBurst is the burst value used for the kubernetes client (default: twice the client QPS)
|
||||
EnvK8sClientBurst = "ARGOCD_K8S_CLIENT_BURST"
|
||||
// EnvK8sClientMaxIdleConnections is the number of max idle connections in K8s REST client HTTP transport (default: 500)
|
||||
EnvK8sClientMaxIdleConnections = "ARGOCD_K8S_CLIENT_MAX_IDLE_CONNECTIONS"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -146,3 +157,33 @@ const (
|
||||
// Number should be bumped in case of backward incompatible change to make sure cache is invalidated after upgrade.
|
||||
CacheVersion = "1.0.0"
|
||||
)
|
||||
|
||||
var (
|
||||
// K8sClientConfigQPS controls the QPS to be used in K8s REST client configs
|
||||
K8sClientConfigQPS float32 = 50
|
||||
// K8sClientConfigBurst controls the burst to be used in K8s REST client configs
|
||||
K8sClientConfigBurst int = 100
|
||||
// K8sMaxIdleConnections controls the number of max idle connections in K8s REST client HTTP transport
|
||||
K8sMaxIdleConnections = 500
|
||||
)
|
||||
|
||||
func init() {
|
||||
if envQPS := os.Getenv(EnvK8sClientQPS); envQPS != "" {
|
||||
if qps, err := strconv.ParseFloat(envQPS, 32); err != nil {
|
||||
K8sClientConfigQPS = float32(qps)
|
||||
}
|
||||
}
|
||||
if envBurst := os.Getenv(EnvK8sClientBurst); envBurst != "" {
|
||||
if burst, err := strconv.Atoi(envBurst); err != nil {
|
||||
K8sClientConfigBurst = burst
|
||||
}
|
||||
} else {
|
||||
K8sClientConfigBurst = 2 * int(K8sClientConfigQPS)
|
||||
}
|
||||
|
||||
if envMaxConn := os.Getenv(EnvK8sClientMaxIdleConnections); envMaxConn != "" {
|
||||
if maxConn, err := strconv.Atoi(envMaxConn); err != nil {
|
||||
K8sMaxIdleConnections = maxConn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"math"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -24,6 +25,9 @@ import (
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
|
||||
// make sure to register workqueue prometheus metrics
|
||||
_ "k8s.io/kubernetes/pkg/util/workqueue/prometheus"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
statecache "github.com/argoproj/argo-cd/controller/cache"
|
||||
"github.com/argoproj/argo-cd/controller/metrics"
|
||||
@@ -37,7 +41,7 @@ import (
|
||||
"github.com/argoproj/argo-cd/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/argo"
|
||||
argocache "github.com/argoproj/argo-cd/util/cache"
|
||||
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"
|
||||
@@ -65,30 +69,37 @@ func (a CompareWith) Max(b CompareWith) CompareWith {
|
||||
return CompareWith(math.Max(float64(a), float64(b)))
|
||||
}
|
||||
|
||||
func (a CompareWith) Pointer() *CompareWith {
|
||||
return &a
|
||||
}
|
||||
|
||||
// ApplicationController is the controller for application resources.
|
||||
type ApplicationController struct {
|
||||
cache *argocache.Cache
|
||||
namespace string
|
||||
kubeClientset kubernetes.Interface
|
||||
kubectl kube.Kubectl
|
||||
applicationClientset appclientset.Interface
|
||||
auditLogger *argo.AuditLogger
|
||||
appRefreshQueue workqueue.RateLimitingInterface
|
||||
appOperationQueue workqueue.RateLimitingInterface
|
||||
appInformer cache.SharedIndexInformer
|
||||
appLister applisters.ApplicationLister
|
||||
projInformer cache.SharedIndexInformer
|
||||
appStateManager AppStateManager
|
||||
stateCache statecache.LiveStateCache
|
||||
statusRefreshTimeout time.Duration
|
||||
selfHealTimeout time.Duration
|
||||
repoClientset apiclient.Clientset
|
||||
db db.ArgoDB
|
||||
settingsMgr *settings_util.SettingsManager
|
||||
refreshRequestedApps map[string]CompareWith
|
||||
refreshRequestedAppsMutex *sync.Mutex
|
||||
metricsServer *metrics.MetricsServer
|
||||
kubectlSemaphore *semaphore.Weighted
|
||||
cache *appstatecache.Cache
|
||||
namespace string
|
||||
kubeClientset kubernetes.Interface
|
||||
kubectl kube.Kubectl
|
||||
applicationClientset appclientset.Interface
|
||||
auditLogger *argo.AuditLogger
|
||||
// queue contains app namespace/name
|
||||
appRefreshQueue workqueue.RateLimitingInterface
|
||||
// queue contains app namespace/name/comparisonType and used to request app refresh with the predefined comparison type
|
||||
appComparisonTypeRefreshQueue workqueue.RateLimitingInterface
|
||||
appOperationQueue workqueue.RateLimitingInterface
|
||||
appInformer cache.SharedIndexInformer
|
||||
appLister applisters.ApplicationLister
|
||||
projInformer cache.SharedIndexInformer
|
||||
appStateManager AppStateManager
|
||||
stateCache statecache.LiveStateCache
|
||||
statusRefreshTimeout time.Duration
|
||||
selfHealTimeout time.Duration
|
||||
repoClientset apiclient.Clientset
|
||||
db db.ArgoDB
|
||||
settingsMgr *settings_util.SettingsManager
|
||||
refreshRequestedApps map[string]CompareWith
|
||||
refreshRequestedAppsMutex *sync.Mutex
|
||||
metricsServer *metrics.MetricsServer
|
||||
kubectlSemaphore *semaphore.Weighted
|
||||
}
|
||||
|
||||
type ApplicationControllerConfig struct {
|
||||
@@ -103,30 +114,32 @@ func NewApplicationController(
|
||||
kubeClientset kubernetes.Interface,
|
||||
applicationClientset appclientset.Interface,
|
||||
repoClientset apiclient.Clientset,
|
||||
argoCache *argocache.Cache,
|
||||
argoCache *appstatecache.Cache,
|
||||
kubectl kube.Kubectl,
|
||||
appResyncPeriod time.Duration,
|
||||
selfHealTimeout time.Duration,
|
||||
metricsPort int,
|
||||
kubectlParallelismLimit int64,
|
||||
) (*ApplicationController, error) {
|
||||
log.Infof("appResyncPeriod=%v", appResyncPeriod)
|
||||
db := db.NewDB(namespace, settingsMgr, kubeClientset)
|
||||
ctrl := ApplicationController{
|
||||
cache: argoCache,
|
||||
namespace: namespace,
|
||||
kubeClientset: kubeClientset,
|
||||
kubectl: kubectl,
|
||||
applicationClientset: applicationClientset,
|
||||
repoClientset: repoClientset,
|
||||
appRefreshQueue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
|
||||
appOperationQueue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
|
||||
db: db,
|
||||
statusRefreshTimeout: appResyncPeriod,
|
||||
refreshRequestedApps: make(map[string]CompareWith),
|
||||
refreshRequestedAppsMutex: &sync.Mutex{},
|
||||
auditLogger: argo.NewAuditLogger(namespace, kubeClientset, "argocd-application-controller"),
|
||||
settingsMgr: settingsMgr,
|
||||
selfHealTimeout: selfHealTimeout,
|
||||
cache: argoCache,
|
||||
namespace: namespace,
|
||||
kubeClientset: kubeClientset,
|
||||
kubectl: kubectl,
|
||||
applicationClientset: applicationClientset,
|
||||
repoClientset: repoClientset,
|
||||
appRefreshQueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "app_reconciliation_queue"),
|
||||
appOperationQueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "app_operation_processing_queue"),
|
||||
appComparisonTypeRefreshQueue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
|
||||
db: db,
|
||||
statusRefreshTimeout: appResyncPeriod,
|
||||
refreshRequestedApps: make(map[string]CompareWith),
|
||||
refreshRequestedAppsMutex: &sync.Mutex{},
|
||||
auditLogger: argo.NewAuditLogger(namespace, kubeClientset, "argocd-application-controller"),
|
||||
settingsMgr: settingsMgr,
|
||||
selfHealTimeout: selfHealTimeout,
|
||||
}
|
||||
if kubectlParallelismLimit > 0 {
|
||||
ctrl.kubectlSemaphore = semaphore.NewWeighted(kubectlParallelismLimit)
|
||||
@@ -136,7 +149,8 @@ func NewApplicationController(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
projInformer := v1alpha1.NewAppProjectInformer(applicationClientset, namespace, appResyncPeriod, cache.Indexers{})
|
||||
indexers := cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}
|
||||
projInformer := v1alpha1.NewAppProjectInformer(applicationClientset, namespace, appResyncPeriod, indexers)
|
||||
metricsAddr := fmt.Sprintf("0.0.0.0:%d", metricsPort)
|
||||
ctrl.metricsServer = metrics.NewMetricsServer(metricsAddr, appLister, func() error {
|
||||
_, err := kubeClientset.Discovery().ServerVersion()
|
||||
@@ -194,7 +208,7 @@ func (ctrl *ApplicationController) handleObjectUpdated(managedByApp map[string]b
|
||||
continue
|
||||
}
|
||||
// exclude resource unless it is permitted in the app project. If project is not permitted then it is not controlled by the user and there is no point showing the warning.
|
||||
if proj, err := ctrl.getAppProj(app); err == nil && proj.IsResourcePermitted(metav1.GroupKind{Group: ref.GroupVersionKind().Group, Kind: ref.Kind}, true) &&
|
||||
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)) {
|
||||
|
||||
managedByApp[app.Name] = false
|
||||
@@ -203,22 +217,17 @@ func (ctrl *ApplicationController) handleObjectUpdated(managedByApp map[string]b
|
||||
}
|
||||
}
|
||||
for appName, isManagedResource := range managedByApp {
|
||||
skipForceRefresh := false
|
||||
|
||||
obj, exists, err := ctrl.appInformer.GetIndexer().GetByKey(ctrl.namespace + "/" + appName)
|
||||
if app, ok := obj.(*appv1.Application); exists && err == nil && ok && isSelfReferencedApp(app, ref) {
|
||||
// Don't force refresh app if related resource is application itself. This prevents infinite reconciliation loop.
|
||||
skipForceRefresh = true
|
||||
continue
|
||||
}
|
||||
|
||||
if !skipForceRefresh {
|
||||
level := ComparisonWithNothing
|
||||
if isManagedResource {
|
||||
level = CompareWithRecent
|
||||
}
|
||||
ctrl.requestAppRefresh(appName, level)
|
||||
level := ComparisonWithNothing
|
||||
if isManagedResource {
|
||||
level = CompareWithRecent
|
||||
}
|
||||
ctrl.appRefreshQueue.Add(fmt.Sprintf("%s/%s", ctrl.namespace, appName))
|
||||
ctrl.requestAppRefresh(appName, &level, nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,7 +310,7 @@ func (ctrl *ApplicationController) getResourceTree(a *appv1.Application, managed
|
||||
}
|
||||
orphanedNodes := make([]appv1.ResourceNode, 0)
|
||||
for k := range orphanedNodesMap {
|
||||
if k.Namespace != "" && proj.IsResourcePermitted(metav1.GroupKind{Group: k.Group, Kind: k.Kind}, true) && !isKnownOrphanedResourceExclusion(k) {
|
||||
if k.Namespace != "" && proj.IsGroupKindPermitted(k.GroupKind(), true) && !isKnownOrphanedResourceExclusion(k) {
|
||||
err := ctrl.stateCache.IterateHierarchy(a.Spec.Destination.Server, k, func(child appv1.ResourceNode, appName string) {
|
||||
belongToAnotherApp := false
|
||||
if appName != "" {
|
||||
@@ -350,7 +359,11 @@ func (ctrl *ApplicationController) managedResources(comparisonResult *comparison
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resDiff = *diff.Diff(target, live, comparisonResult.diffNormalizer)
|
||||
resDiffPtr, err := diff.Diff(target, live, comparisonResult.diffNormalizer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resDiff = *resDiffPtr
|
||||
}
|
||||
|
||||
if live != nil {
|
||||
@@ -377,6 +390,8 @@ func (ctrl *ApplicationController) managedResources(comparisonResult *comparison
|
||||
return nil, err
|
||||
}
|
||||
item.Diff = jsonDiff
|
||||
item.PredictedLiveState = string(resDiff.PredictedLive)
|
||||
item.NormalizedLiveState = string(resDiff.NormalizedLive)
|
||||
|
||||
items[i] = &item
|
||||
}
|
||||
@@ -387,7 +402,10 @@ func (ctrl *ApplicationController) managedResources(comparisonResult *comparison
|
||||
func (ctrl *ApplicationController) Run(ctx context.Context, statusProcessors int, operationProcessors int) {
|
||||
defer runtime.HandleCrash()
|
||||
defer ctrl.appRefreshQueue.ShutDown()
|
||||
defer ctrl.appComparisonTypeRefreshQueue.ShutDown()
|
||||
defer ctrl.appOperationQueue.ShutDown()
|
||||
|
||||
ctrl.metricsServer.RegisterClustersInfoSource(ctx, ctrl.stateCache)
|
||||
go ctrl.appInformer.Run(ctx.Done())
|
||||
go ctrl.projInformer.Run(ctx.Done())
|
||||
|
||||
@@ -413,13 +431,30 @@ func (ctrl *ApplicationController) Run(ctx context.Context, statusProcessors int
|
||||
}, time.Second, ctx.Done())
|
||||
}
|
||||
|
||||
go wait.Until(func() {
|
||||
for ctrl.processAppComparisonTypeQueueItem() {
|
||||
}
|
||||
}, time.Second, ctx.Done())
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) requestAppRefresh(appName string, compareWith CompareWith) {
|
||||
ctrl.refreshRequestedAppsMutex.Lock()
|
||||
defer ctrl.refreshRequestedAppsMutex.Unlock()
|
||||
ctrl.refreshRequestedApps[appName] = compareWith.Max(ctrl.refreshRequestedApps[appName])
|
||||
func (ctrl *ApplicationController) requestAppRefresh(appName string, compareWith *CompareWith, after *time.Duration) {
|
||||
key := fmt.Sprintf("%s/%s", ctrl.namespace, appName)
|
||||
|
||||
if compareWith != nil && after != nil {
|
||||
ctrl.appComparisonTypeRefreshQueue.AddAfter(fmt.Sprintf("%s/%d", key, compareWith), *after)
|
||||
} else {
|
||||
if compareWith != nil {
|
||||
ctrl.refreshRequestedAppsMutex.Lock()
|
||||
ctrl.refreshRequestedApps[appName] = compareWith.Max(ctrl.refreshRequestedApps[appName])
|
||||
ctrl.refreshRequestedAppsMutex.Unlock()
|
||||
}
|
||||
if after != nil {
|
||||
ctrl.appRefreshQueue.AddAfter(key, *after)
|
||||
} else {
|
||||
ctrl.appRefreshQueue.Add(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) isRefreshRequested(appName string) (bool, CompareWith) {
|
||||
@@ -463,7 +498,7 @@ func (ctrl *ApplicationController) processAppOperationQueueItem() (processNext b
|
||||
if app.Operation != nil {
|
||||
ctrl.processRequestedAppOperation(app)
|
||||
} else if app.DeletionTimestamp != nil && app.CascadedDeletion() {
|
||||
err = ctrl.finalizeApplicationDeletion(app)
|
||||
_, err = ctrl.finalizeApplicationDeletion(app)
|
||||
if err != nil {
|
||||
ctrl.setAppCondition(app, appv1.ApplicationCondition{
|
||||
Type: appv1.ApplicationConditionDeletionError,
|
||||
@@ -476,11 +511,54 @@ func (ctrl *ApplicationController) processAppOperationQueueItem() (processNext b
|
||||
return
|
||||
}
|
||||
|
||||
func shouldBeDeleted(app *appv1.Application, obj *unstructured.Unstructured) bool {
|
||||
func (ctrl *ApplicationController) processAppComparisonTypeQueueItem() (processNext bool) {
|
||||
key, shutdown := ctrl.appComparisonTypeRefreshQueue.Get()
|
||||
processNext = true
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
|
||||
}
|
||||
ctrl.appComparisonTypeRefreshQueue.Done(key)
|
||||
}()
|
||||
if shutdown {
|
||||
processNext = false
|
||||
return
|
||||
}
|
||||
|
||||
if parts := strings.Split(key.(string), "/"); len(parts) != 3 {
|
||||
log.Warnf("Unexpected key format in appComparisonTypeRefreshTypeQueue. Key should consists of namespace/name/comparisonType but got: %s", key.(string))
|
||||
} else {
|
||||
if compareWith, err := strconv.Atoi(parts[2]); err != nil {
|
||||
log.Warnf("Unable to parse comparison type: %v", err)
|
||||
return
|
||||
} else {
|
||||
ctrl.requestAppRefresh(parts[1], CompareWith(compareWith).Pointer(), nil)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Application) error {
|
||||
func (ctrl *ApplicationController) getPermittedAppLiveObjects(app *appv1.Application, proj *appv1.AppProject) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
|
||||
objsMap, err := ctrl.stateCache.GetManagedLiveObjs(app, []*unstructured.Unstructured{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Don't delete live resources which are not permitted in the app project
|
||||
for k, v := range objsMap {
|
||||
if !proj.IsLiveResourcePermitted(v, app.Spec.Destination.Server) {
|
||||
delete(objsMap, k)
|
||||
}
|
||||
}
|
||||
return objsMap, nil
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Application) ([]*unstructured.Unstructured, error) {
|
||||
logCtx := log.WithField("application", app.Name)
|
||||
logCtx.Infof("Deleting resources")
|
||||
// Get refreshed application info, since informer app copy might be stale
|
||||
@@ -489,23 +567,28 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic
|
||||
if !apierr.IsNotFound(err) {
|
||||
logCtx.Errorf("Unable to get refreshed application info prior deleting resources: %v", err)
|
||||
}
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
proj, err := ctrl.getAppProj(app)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
objsMap, err := ctrl.stateCache.GetManagedLiveObjs(app, []*unstructured.Unstructured{})
|
||||
objsMap, err := ctrl.getPermittedAppLiveObjects(app, proj)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
objs := make([]*unstructured.Unstructured, 0)
|
||||
for k := range objsMap {
|
||||
if objsMap[k].GetDeletionTimestamp() == nil && shouldBeDeleted(app, objsMap[k]) {
|
||||
if ctrl.shouldBeDeleted(app, objsMap[k]) && objsMap[k].GetDeletionTimestamp() == nil {
|
||||
objs = append(objs, objsMap[k])
|
||||
}
|
||||
}
|
||||
|
||||
cluster, err := ctrl.db.GetCluster(context.Background(), app.Spec.Destination.Server)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
config := metrics.AddMetricsTransportWrapper(ctrl.metricsServer, app, cluster.RESTConfig())
|
||||
|
||||
@@ -514,29 +597,30 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic
|
||||
return ctrl.kubectl.DeleteResource(config, obj.GroupVersionKind(), obj.GetName(), obj.GetNamespace(), false)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return objs, err
|
||||
}
|
||||
|
||||
objsMap, err = ctrl.stateCache.GetManagedLiveObjs(app, []*unstructured.Unstructured{})
|
||||
objsMap, err = ctrl.getPermittedAppLiveObjects(app, proj)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for k, obj := range objsMap {
|
||||
if !shouldBeDeleted(app, obj) {
|
||||
if !ctrl.shouldBeDeleted(app, obj) {
|
||||
delete(objsMap, k)
|
||||
}
|
||||
}
|
||||
if len(objsMap) > 0 {
|
||||
logCtx.Infof("%d objects remaining for deletion", len(objsMap))
|
||||
return nil
|
||||
return objs, nil
|
||||
}
|
||||
err = ctrl.cache.SetAppManagedResources(app.Name, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return objs, err
|
||||
}
|
||||
err = ctrl.cache.SetAppResourcesTree(app.Name, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
return objs, err
|
||||
}
|
||||
app.SetCascadedDeletion(false)
|
||||
var patch []byte
|
||||
@@ -547,26 +631,16 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic
|
||||
})
|
||||
_, err = ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Patch(app.Name, types.MergePatchType, patch)
|
||||
if err != nil {
|
||||
return err
|
||||
return objs, err
|
||||
}
|
||||
|
||||
logCtx.Info("Successfully deleted resources")
|
||||
return nil
|
||||
logCtx.Infof("Successfully deleted %d resources", len(objs))
|
||||
return objs, nil
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) setAppCondition(app *appv1.Application, condition appv1.ApplicationCondition) {
|
||||
index := -1
|
||||
for i, exiting := range app.Status.Conditions {
|
||||
if exiting.Type == condition.Type {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index > -1 {
|
||||
app.Status.Conditions[index] = condition
|
||||
} else {
|
||||
app.Status.Conditions = append(app.Status.Conditions, condition)
|
||||
}
|
||||
app.Status.SetConditions([]appv1.ApplicationCondition{condition}, map[appv1.ApplicationConditionType]bool{condition.Type: true})
|
||||
|
||||
var patch []byte
|
||||
patch, err := json.Marshal(map[string]interface{}{
|
||||
"status": map[string]interface{}{
|
||||
@@ -641,10 +715,9 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli
|
||||
if state.Phase.Completed() {
|
||||
// if we just completed an operation, force a refresh so that UI will report up-to-date
|
||||
// sync/health information
|
||||
if key, err := cache.MetaNamespaceKeyFunc(app); err == nil {
|
||||
if _, err := cache.MetaNamespaceKeyFunc(app); err == nil {
|
||||
// force app refresh with using CompareWithLatest comparison type and trigger app reconciliation loop
|
||||
ctrl.requestAppRefresh(app.Name, CompareWithLatest)
|
||||
ctrl.appRefreshQueue.Add(key)
|
||||
ctrl.requestAppRefresh(app.Name, CompareWithLatest.Pointer(), nil)
|
||||
} else {
|
||||
logCtx.Warnf("Fails to requeue application: %v", err)
|
||||
}
|
||||
@@ -748,29 +821,37 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
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-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.Conditions = []appv1.ApplicationCondition{{Type: appv1.ApplicationConditionComparisonError, Message: err.Error()}}
|
||||
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 {
|
||||
@@ -785,7 +866,7 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
}
|
||||
}
|
||||
|
||||
hasErrors := ctrl.refreshAppConditions(app)
|
||||
project, hasErrors := ctrl.refreshAppConditions(app)
|
||||
if hasErrors {
|
||||
app.Status.Sync.Status = appv1.SyncStatusCodeUnknown
|
||||
app.Status.Health.Status = appv1.HealthStatusUnknown
|
||||
@@ -803,7 +884,11 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
revision = app.Status.Sync.Revision
|
||||
}
|
||||
|
||||
compareResult := ctrl.appStateManager.CompareAppState(app, revision, app.Spec.Source, refreshType == appv1.RefreshTypeHard, localManifests)
|
||||
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)
|
||||
|
||||
@@ -814,24 +899,27 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
app.Status.Summary = tree.GetSummary()
|
||||
}
|
||||
|
||||
project, err := ctrl.getAppProj(app)
|
||||
if err != nil {
|
||||
logCtx.Infof("Could not lookup project for %s in order to check schedules state", app.Name)
|
||||
} else {
|
||||
if project.Spec.SyncWindows.Matches(app).CanSync(false) {
|
||||
syncErrCond := ctrl.autoSync(app, compareResult.syncStatus, compareResult.resources)
|
||||
if syncErrCond != nil {
|
||||
app.Status.SetConditions([]appv1.ApplicationCondition{*syncErrCond}, map[appv1.ApplicationConditionType]bool{appv1.ApplicationConditionSyncError: true})
|
||||
} else {
|
||||
app.Status.SetConditions([]appv1.ApplicationCondition{}, map[appv1.ApplicationConditionType]bool{appv1.ApplicationConditionSyncError: true})
|
||||
}
|
||||
if project.Spec.SyncWindows.Matches(app).CanSync(false) {
|
||||
syncErrCond := ctrl.autoSync(app, compareResult.syncStatus, compareResult.resources)
|
||||
if syncErrCond != nil {
|
||||
app.Status.SetConditions(
|
||||
[]appv1.ApplicationCondition{*syncErrCond},
|
||||
map[appv1.ApplicationConditionType]bool{appv1.ApplicationConditionSyncError: true},
|
||||
)
|
||||
} else {
|
||||
logCtx.Infof("Sync prevented by sync window")
|
||||
app.Status.SetConditions(
|
||||
[]appv1.ApplicationCondition{},
|
||||
map[appv1.ApplicationConditionType]bool{appv1.ApplicationConditionSyncError: true},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
logCtx.Info("Sync prevented by sync window")
|
||||
}
|
||||
|
||||
app.Status.ObservedAt = &compareResult.reconciledAt
|
||||
app.Status.ReconciledAt = &compareResult.reconciledAt
|
||||
if app.Status.ReconciledAt == nil || comparisonLevel == CompareWithLatest {
|
||||
app.Status.ReconciledAt = &observedAt
|
||||
}
|
||||
app.Status.ObservedAt = &observedAt
|
||||
app.Status.Sync = *compareResult.syncStatus
|
||||
app.Status.Health = *compareResult.healthStatus
|
||||
app.Status.Resources = compareResult.resources
|
||||
@@ -850,23 +938,29 @@ func (ctrl *ApplicationController) needRefreshAppStatus(app *appv1.Application,
|
||||
compareWith := CompareWithLatest
|
||||
refreshType := appv1.RefreshTypeNormal
|
||||
expired := app.Status.ReconciledAt == nil || app.Status.ReconciledAt.Add(statusRefreshTimeout).Before(time.Now().UTC())
|
||||
if requestedType, ok := app.IsRefreshRequested(); ok || expired {
|
||||
if ok {
|
||||
refreshType = requestedType
|
||||
reason = fmt.Sprintf("%s refresh requested", refreshType)
|
||||
} else if expired {
|
||||
reason = fmt.Sprintf("comparison expired. reconciledAt: %v, expiry: %v", app.Status.ReconciledAt, statusRefreshTimeout)
|
||||
|
||||
if requestedType, ok := app.IsRefreshRequested(); ok {
|
||||
// user requested app refresh.
|
||||
refreshType = requestedType
|
||||
reason = fmt.Sprintf("%s refresh requested", refreshType)
|
||||
} else if expired {
|
||||
// The commented line below mysteriously crashes if app.Status.ReconciledAt is nil
|
||||
// reason = fmt.Sprintf("comparison expired. reconciledAt: %v, expiry: %v", app.Status.ReconciledAt, statusRefreshTimeout)
|
||||
//TODO: find existing Golang bug or create a new one
|
||||
reconciledAtStr := "never"
|
||||
if app.Status.ReconciledAt != nil {
|
||||
reconciledAtStr = app.Status.ReconciledAt.String()
|
||||
}
|
||||
} else if requested, level := ctrl.isRefreshRequested(app.Name); requested {
|
||||
compareWith = level
|
||||
reason = fmt.Sprintf("controller refresh requested")
|
||||
} else if app.Status.Sync.Status == appv1.SyncStatusCodeUnknown && expired {
|
||||
reason = "comparison status unknown"
|
||||
reason = fmt.Sprintf("comparison expired. reconciledAt: %v, expiry: %v", reconciledAtStr, statusRefreshTimeout)
|
||||
} else if !app.Spec.Source.Equals(app.Status.Sync.ComparedTo.Source) {
|
||||
reason = "spec.source differs"
|
||||
} else if !app.Spec.Destination.Equals(app.Status.Sync.ComparedTo.Destination) {
|
||||
reason = "spec.destination differs"
|
||||
} else if requested, level := ctrl.isRefreshRequested(app.Name); requested {
|
||||
compareWith = level
|
||||
reason = fmt.Sprintf("controller refresh requested")
|
||||
}
|
||||
|
||||
if reason != "" {
|
||||
logCtx.Infof("Refreshing app status (%s), level (%d)", reason, compareWith)
|
||||
return true, refreshType, compareWith
|
||||
@@ -874,7 +968,7 @@ func (ctrl *ApplicationController) needRefreshAppStatus(app *appv1.Application,
|
||||
return false, refreshType, compareWith
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) refreshAppConditions(app *appv1.Application) bool {
|
||||
func (ctrl *ApplicationController) refreshAppConditions(app *appv1.Application) (*appv1.AppProject, bool) {
|
||||
errorConditions := make([]appv1.ApplicationCondition, 0)
|
||||
proj, err := ctrl.getAppProj(app)
|
||||
if err != nil {
|
||||
@@ -904,7 +998,7 @@ func (ctrl *ApplicationController) refreshAppConditions(app *appv1.Application)
|
||||
appv1.ApplicationConditionInvalidSpecError: true,
|
||||
appv1.ApplicationConditionUnknownError: true,
|
||||
})
|
||||
return len(errorConditions) > 0
|
||||
return proj, len(errorConditions) > 0
|
||||
}
|
||||
|
||||
// normalizeApplication normalizes an application.spec and additionally persists updates if it changed
|
||||
@@ -987,14 +1081,30 @@ 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},
|
||||
}
|
||||
// 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
|
||||
@@ -1021,12 +1131,7 @@ func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus *
|
||||
}
|
||||
} else {
|
||||
logCtx.Infof("Skipping auto-sync: already attempted sync to %s with timeout %v (retrying in %v)", desiredCommitSHA, ctrl.selfHealTimeout, retryAfter)
|
||||
if key, err := cache.MetaNamespaceKeyFunc(app); err == nil {
|
||||
ctrl.requestAppRefresh(app.Name, CompareWithLatest)
|
||||
ctrl.appRefreshQueue.AddAfter(key, retryAfter)
|
||||
} else {
|
||||
logCtx.Warnf("Fails to requeue application: %v", err)
|
||||
}
|
||||
ctrl.requestAppRefresh(app.Name, CompareWithLatest.Pointer(), &retryAfter)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1099,15 +1204,14 @@ func (ctrl *ApplicationController) newApplicationInformerAndLister() (cache.Shar
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var compareWith *CompareWith
|
||||
oldApp, oldOK := old.(*appv1.Application)
|
||||
newApp, newOK := new.(*appv1.Application)
|
||||
if oldOK && newOK {
|
||||
if toggledAutomatedSync(oldApp, newApp) {
|
||||
log.WithField("application", newApp.Name).Info("Enabled automated sync")
|
||||
ctrl.requestAppRefresh(newApp.Name, CompareWithLatest)
|
||||
}
|
||||
if oldOK && newOK && automatedSyncEnabled(oldApp, newApp) {
|
||||
log.WithField("application", newApp.Name).Info("Enabled automated sync")
|
||||
compareWith = CompareWithLatest.Pointer()
|
||||
}
|
||||
ctrl.appRefreshQueue.Add(key)
|
||||
ctrl.requestAppRefresh(newApp.Name, compareWith, nil)
|
||||
ctrl.appOperationQueue.Add(key)
|
||||
},
|
||||
DeleteFunc: func(obj interface{}) {
|
||||
@@ -1144,14 +1248,26 @@ func isOperationInProgress(app *appv1.Application) bool {
|
||||
return app.Status.OperationState != nil && !app.Status.OperationState.Phase.Completed()
|
||||
}
|
||||
|
||||
// toggledAutomatedSync tests if an app went from auto-sync disabled to enabled.
|
||||
// automatedSyncEnabled tests if an app went from auto-sync disabled to enabled.
|
||||
// if it was toggled to be enabled, the informer handler will force a refresh
|
||||
func toggledAutomatedSync(old *appv1.Application, new *appv1.Application) bool {
|
||||
if new.Spec.SyncPolicy == nil || new.Spec.SyncPolicy.Automated == nil {
|
||||
return false
|
||||
func automatedSyncEnabled(oldApp *appv1.Application, newApp *appv1.Application) bool {
|
||||
oldEnabled := false
|
||||
oldSelfHealEnabled := false
|
||||
if oldApp.Spec.SyncPolicy != nil && oldApp.Spec.SyncPolicy.Automated != nil {
|
||||
oldEnabled = true
|
||||
oldSelfHealEnabled = oldApp.Spec.SyncPolicy.Automated.SelfHeal
|
||||
}
|
||||
// auto-sync is enabled. check if it was previously disabled
|
||||
if old.Spec.SyncPolicy == nil || old.Spec.SyncPolicy.Automated == nil {
|
||||
|
||||
newEnabled := false
|
||||
newSelfHealEnabled := false
|
||||
if newApp.Spec.SyncPolicy != nil && newApp.Spec.SyncPolicy.Automated != nil {
|
||||
newEnabled = true
|
||||
newSelfHealEnabled = newApp.Spec.SyncPolicy.Automated.SelfHeal
|
||||
}
|
||||
if !oldEnabled && newEnabled {
|
||||
return true
|
||||
}
|
||||
if !oldSelfHealEnabled && newSelfHealEnabled {
|
||||
return true
|
||||
}
|
||||
// nothing changed
|
||||
|
||||
@@ -2,6 +2,7 @@ package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -26,7 +27,8 @@ import (
|
||||
mockrepoclient "github.com/argoproj/argo-cd/reposerver/apiclient/mocks"
|
||||
mockreposerver "github.com/argoproj/argo-cd/reposerver/mocks"
|
||||
"github.com/argoproj/argo-cd/test"
|
||||
utilcache "github.com/argoproj/argo-cd/util/cache"
|
||||
cacheutil "github.com/argoproj/argo-cd/util/cache"
|
||||
appstatecache "github.com/argoproj/argo-cd/util/cache/appstate"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/kube/kubetest"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
@@ -87,7 +89,10 @@ func newFakeController(data *fakeData) *ApplicationController {
|
||||
kubeClient,
|
||||
appclientset.NewSimpleClientset(data.apps...),
|
||||
&mockRepoClientset,
|
||||
utilcache.NewCache(utilcache.NewInMemoryCache(1*time.Hour)),
|
||||
appstatecache.NewCache(
|
||||
cacheutil.NewCache(cacheutil.NewInMemoryCache(1*time.Minute)),
|
||||
1*time.Minute,
|
||||
),
|
||||
kubectl,
|
||||
time.Minute,
|
||||
time.Minute,
|
||||
@@ -106,6 +111,7 @@ func newFakeController(data *fakeData) *ApplicationController {
|
||||
ctrl.stateCache = &mockStateCache
|
||||
mockStateCache.On("IsNamespaced", mock.Anything, mock.Anything).Return(true, nil)
|
||||
mockStateCache.On("GetManagedLiveObjs", mock.Anything, mock.Anything).Return(data.managedLiveObjs, nil)
|
||||
mockStateCache.On("GetVersionsInfo", mock.Anything).Return("v1.2.3", nil, nil)
|
||||
response := make(map[kube.ResourceKey]argoappv1.ResourceNode)
|
||||
for k, v := range data.namespacedResources {
|
||||
response[k] = v.ResourceNode
|
||||
@@ -186,6 +192,17 @@ status:
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps.git
|
||||
`
|
||||
|
||||
var fakeStrayResource = `
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: test-cm
|
||||
namespace: invalid
|
||||
labels:
|
||||
app.kubernetes.io/instance: my-app
|
||||
data:
|
||||
`
|
||||
|
||||
func newFakeApp() *argoappv1.Application {
|
||||
var app argoappv1.Application
|
||||
err := yaml.Unmarshal([]byte(fakeApp), &app)
|
||||
@@ -195,6 +212,15 @@ func newFakeApp() *argoappv1.Application {
|
||||
return &app
|
||||
}
|
||||
|
||||
func newFakeCM() map[string]interface{} {
|
||||
var cm map[string]interface{}
|
||||
err := yaml.Unmarshal([]byte(fakeStrayResource), &cm)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return cm
|
||||
}
|
||||
|
||||
func TestAutoSync(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
@@ -202,7 +228,7 @@ 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{})
|
||||
assert.NoError(t, err)
|
||||
@@ -214,7 +240,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{
|
||||
@@ -226,10 +252,10 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
}
|
||||
})
|
||||
|
||||
// Verify we skip when we are already Synced (even if revision is different)
|
||||
{
|
||||
t.Run("AlreadyInSyncedState", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus := argoappv1.SyncStatus{
|
||||
@@ -241,10 +267,10 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
}
|
||||
})
|
||||
|
||||
// Verify we skip when auto-sync is disabled
|
||||
{
|
||||
t.Run("AutoSyncIsDisabled", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Spec.SyncPolicy = nil
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
@@ -257,10 +283,10 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
}
|
||||
})
|
||||
|
||||
// Verify we skip when application is marked for deletion
|
||||
{
|
||||
t.Run("ApplicationIsMarkedForDeletion", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
now := metav1.Now()
|
||||
app.DeletionTimestamp = &now
|
||||
@@ -274,11 +300,11 @@ func TestSkipAutoSync(t *testing.T) {
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
}
|
||||
})
|
||||
|
||||
// Verify we skip when previous sync attempt failed and return error condition
|
||||
// Set current to 'aaaaa', desired to 'bbbbb' and add 'bbbbb' to failure history
|
||||
{
|
||||
t.Run("PreviousSyncAttemptFailed", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Status.OperationState = &argoappv1.OperationState{
|
||||
Operation: argoappv1.Operation{
|
||||
@@ -295,12 +321,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{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NeedsToPruneResourcesOnlyButAutomatedPruneDisabled", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus := argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{
|
||||
{Name: "guestbook", Kind: kube.DeploymentKind, Status: argoappv1.SyncStatusCodeOutOfSync, RequiresPruning: true},
|
||||
})
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
})
|
||||
}
|
||||
|
||||
// TestAutoSyncIndicateError verifies we skip auto-sync and return error condition if previous sync failed
|
||||
@@ -331,7 +373,7 @@ func TestAutoSyncIndicateError(t *testing.T) {
|
||||
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{})
|
||||
assert.NoError(t, err)
|
||||
@@ -374,7 +416,7 @@ func TestAutoSyncParameterOverrides(t *testing.T) {
|
||||
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{})
|
||||
assert.NoError(t, err)
|
||||
@@ -383,27 +425,118 @@ func TestAutoSyncParameterOverrides(t *testing.T) {
|
||||
|
||||
// TestFinalizeAppDeletion verifies application deletion
|
||||
func TestFinalizeAppDeletion(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
|
||||
appObj := kube.MustToUnstructured(&app)
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}, managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
||||
kube.GetResourceKey(appObj): appObj,
|
||||
}})
|
||||
// Ensure app can be deleted cascading
|
||||
{
|
||||
defaultProj := argoappv1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
},
|
||||
Spec: argoappv1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []argoappv1.ApplicationDestination{
|
||||
{
|
||||
Server: "*",
|
||||
Namespace: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
app := newFakeApp()
|
||||
app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
|
||||
appObj := kube.MustToUnstructured(&app)
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}, managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
||||
kube.GetResourceKey(appObj): appObj,
|
||||
}})
|
||||
|
||||
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)
|
||||
patched := false
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
defaultReactor := fakeAppCs.ReactionChain[0]
|
||||
fakeAppCs.ReactionChain = nil
|
||||
fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
return defaultReactor.React(action)
|
||||
})
|
||||
fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
patched = true
|
||||
return true, nil, nil
|
||||
})
|
||||
_, err := ctrl.finalizeApplicationDeletion(app)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, patched)
|
||||
}
|
||||
|
||||
// Ensure any stray resources irregulary labeled with instance label of app are not deleted upon deleting,
|
||||
// when app project restriction is in place
|
||||
{
|
||||
defaultProj := argoappv1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
},
|
||||
Spec: argoappv1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []argoappv1.ApplicationDestination{
|
||||
{
|
||||
Server: "*",
|
||||
Namespace: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
restrictedProj := argoappv1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "restricted",
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
},
|
||||
Spec: argoappv1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []argoappv1.ApplicationDestination{
|
||||
{
|
||||
Server: "*",
|
||||
Namespace: "my-app",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
app := newFakeApp()
|
||||
app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
|
||||
app.Spec.Project = "restricted"
|
||||
appObj := kube.MustToUnstructured(&app)
|
||||
cm := newFakeCM()
|
||||
strayObj := kube.MustToUnstructured(&cm)
|
||||
ctrl := newFakeController(&fakeData{
|
||||
apps: []runtime.Object{app, &defaultProj, &restrictedProj},
|
||||
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
||||
kube.GetResourceKey(appObj): appObj,
|
||||
kube.GetResourceKey(strayObj): strayObj,
|
||||
},
|
||||
})
|
||||
|
||||
patched := false
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
defaultReactor := fakeAppCs.ReactionChain[0]
|
||||
fakeAppCs.ReactionChain = nil
|
||||
fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
return defaultReactor.React(action)
|
||||
})
|
||||
fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
patched = true
|
||||
return true, nil, nil
|
||||
})
|
||||
objs, err := ctrl.finalizeApplicationDeletion(app)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, patched)
|
||||
objsMap, err := ctrl.stateCache.GetManagedLiveObjs(app, []*unstructured.Unstructured{})
|
||||
if err != nil {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
// Managed objects must be empty
|
||||
assert.Empty(t, objsMap)
|
||||
// Loop through all deleted objects, ensure that test-cm is none of them
|
||||
for _, o := range objs {
|
||||
assert.NotEqual(t, "test-cm", o.GetName())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNormalizeApplication verifies we normalize an application during reconciliation
|
||||
@@ -556,8 +689,8 @@ func TestNeedRefreshAppStatus(t *testing.T) {
|
||||
assert.False(t, needRefresh)
|
||||
|
||||
// refresh app using the 'deepest' requested comparison level
|
||||
ctrl.requestAppRefresh(app.Name, CompareWithRecent)
|
||||
ctrl.requestAppRefresh(app.Name, ComparisonWithNothing)
|
||||
ctrl.requestAppRefresh(app.Name, CompareWithRecent.Pointer(), nil)
|
||||
ctrl.requestAppRefresh(app.Name, ComparisonWithNothing.Pointer(), nil)
|
||||
|
||||
needRefresh, refreshType, compareWith := ctrl.needRefreshAppStatus(app, 1*time.Hour)
|
||||
assert.True(t, needRefresh)
|
||||
@@ -575,7 +708,7 @@ func TestNeedRefreshAppStatus(t *testing.T) {
|
||||
{
|
||||
// refresh app using the 'latest' level if comparison expired
|
||||
app := app.DeepCopy()
|
||||
ctrl.requestAppRefresh(app.Name, CompareWithRecent)
|
||||
ctrl.requestAppRefresh(app.Name, CompareWithRecent.Pointer(), nil)
|
||||
reconciledAt := metav1.NewTime(time.Now().UTC().Add(-1 * time.Hour))
|
||||
app.Status.ReconciledAt = &reconciledAt
|
||||
needRefresh, refreshType, compareWith = ctrl.needRefreshAppStatus(app, 1*time.Minute)
|
||||
@@ -597,6 +730,24 @@ func TestNeedRefreshAppStatus(t *testing.T) {
|
||||
assert.Equal(t, argoappv1.RefreshTypeHard, refreshType)
|
||||
assert.Equal(t, CompareWithLatest, compareWith)
|
||||
}
|
||||
|
||||
{
|
||||
app := app.DeepCopy()
|
||||
// ensure that CompareWithLatest level is used if application source has changed
|
||||
ctrl.requestAppRefresh(app.Name, ComparisonWithNothing.Pointer(), nil)
|
||||
// sample app source change
|
||||
app.Spec.Source.Helm = &argoappv1.ApplicationSourceHelm{
|
||||
Parameters: []argoappv1.HelmParameter{{
|
||||
Name: "foo",
|
||||
Value: "bar",
|
||||
}},
|
||||
}
|
||||
|
||||
needRefresh, refreshType, compareWith = ctrl.needRefreshAppStatus(app, 1*time.Hour)
|
||||
assert.True(t, needRefresh)
|
||||
assert.Equal(t, argoappv1.RefreshTypeNormal, refreshType)
|
||||
assert.Equal(t, CompareWithLatest, compareWith)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshAppConditions(t *testing.T) {
|
||||
@@ -620,7 +771,7 @@ func TestRefreshAppConditions(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}})
|
||||
|
||||
hasErrors := ctrl.refreshAppConditions(app)
|
||||
_, hasErrors := ctrl.refreshAppConditions(app)
|
||||
assert.False(t, hasErrors)
|
||||
assert.Len(t, app.Status.Conditions, 0)
|
||||
})
|
||||
@@ -631,7 +782,7 @@ func TestRefreshAppConditions(t *testing.T) {
|
||||
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}})
|
||||
|
||||
hasErrors := ctrl.refreshAppConditions(app)
|
||||
_, hasErrors := ctrl.refreshAppConditions(app)
|
||||
assert.False(t, hasErrors)
|
||||
assert.Len(t, app.Status.Conditions, 1)
|
||||
assert.Equal(t, argoappv1.ApplicationConditionExcludedResourceWarning, app.Status.Conditions[0].Type)
|
||||
@@ -644,10 +795,70 @@ func TestRefreshAppConditions(t *testing.T) {
|
||||
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}})
|
||||
|
||||
hasErrors := ctrl.refreshAppConditions(app)
|
||||
_, hasErrors := ctrl.refreshAppConditions(app)
|
||||
assert.True(t, hasErrors)
|
||||
assert.Len(t, app.Status.Conditions, 1)
|
||||
assert.Equal(t, argoappv1.ApplicationConditionInvalidSpecError, app.Status.Conditions[0].Type)
|
||||
assert.Equal(t, "Application referencing project wrong project which does not exist", app.Status.Conditions[0].Message)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateReconciledAt(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
reconciledAt := metav1.NewTime(time.Now().Add(-1 * time.Second))
|
||||
app.Status = argoappv1.ApplicationStatus{ReconciledAt: &reconciledAt}
|
||||
app.Status.Sync = argoappv1.SyncStatus{ComparedTo: argoappv1.ComparedTo{Source: app.Spec.Source, Destination: app.Spec.Destination}}
|
||||
ctrl := newFakeController(&fakeData{
|
||||
apps: []runtime.Object{app, &defaultProj},
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
})
|
||||
key, _ := cache.MetaNamespaceKeyFunc(app)
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
fakeAppCs.ReactionChain = nil
|
||||
receivedPatch := map[string]interface{}{}
|
||||
fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
if patchAction, ok := action.(kubetesting.PatchAction); ok {
|
||||
assert.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch))
|
||||
}
|
||||
return true, nil, nil
|
||||
})
|
||||
|
||||
t.Run("UpdatedOnFullReconciliation", func(t *testing.T) {
|
||||
receivedPatch = map[string]interface{}{}
|
||||
ctrl.requestAppRefresh(app.Name, CompareWithLatest.Pointer(), nil)
|
||||
ctrl.appRefreshQueue.Add(key)
|
||||
|
||||
ctrl.processAppRefreshQueueItem()
|
||||
|
||||
_, updated, err := unstructured.NestedString(receivedPatch, "status", "reconciledAt")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, updated)
|
||||
|
||||
_, updated, err = unstructured.NestedString(receivedPatch, "status", "observedAt")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, updated)
|
||||
})
|
||||
|
||||
t.Run("NotUpdatedOnPartialReconciliation", func(t *testing.T) {
|
||||
receivedPatch = map[string]interface{}{}
|
||||
ctrl.appRefreshQueue.Add(key)
|
||||
ctrl.requestAppRefresh(app.Name, CompareWithRecent.Pointer(), nil)
|
||||
|
||||
ctrl.processAppRefreshQueueItem()
|
||||
|
||||
_, updated, err := unstructured.NestedString(receivedPatch, "status", "reconciledAt")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, updated)
|
||||
|
||||
_, updated, err = unstructured.NestedString(receivedPatch, "status", "observedAt")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, updated)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
113
controller/cache/cache.go
vendored
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
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"
|
||||
@@ -27,6 +28,9 @@ type cacheSettings struct {
|
||||
}
|
||||
|
||||
type LiveStateCache interface {
|
||||
// Returns k8s server version
|
||||
GetVersionsInfo(serverURL string) (string, []metav1.APIGroup, error)
|
||||
// Returns true of given group kind is a namespaced resource
|
||||
IsNamespaced(server string, gk schema.GroupKind) (bool, error)
|
||||
// Executes give callback against resource specified by the key and all its children
|
||||
IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string)) error
|
||||
@@ -38,6 +42,8 @@ type LiveStateCache interface {
|
||||
Run(ctx context.Context) error
|
||||
// Invalidate invalidates the entire cluster state cache
|
||||
Invalidate()
|
||||
// Returns information about monitored clusters
|
||||
GetClustersInfo() []metrics.ClusterInfo
|
||||
}
|
||||
|
||||
type ObjectUpdatedHandler = func(managedByApp map[string]bool, ref v1.ObjectReference)
|
||||
@@ -65,7 +71,7 @@ func NewLiveStateCache(
|
||||
appInformer: appInformer,
|
||||
db: db,
|
||||
clusters: make(map[string]*clusterInfo),
|
||||
lock: &sync.Mutex{},
|
||||
lock: &sync.RWMutex{},
|
||||
onObjectUpdated: onObjectUpdated,
|
||||
kubectl: kubectl,
|
||||
settingsMgr: settingsMgr,
|
||||
@@ -77,7 +83,7 @@ func NewLiveStateCache(
|
||||
type liveStateCache struct {
|
||||
db db.ArgoDB
|
||||
clusters map[string]*clusterInfo
|
||||
lock *sync.Mutex
|
||||
lock *sync.RWMutex
|
||||
appInformer cache.SharedIndexInformer
|
||||
onObjectUpdated ObjectUpdatedHandler
|
||||
kubectl kube.Kubectl
|
||||
@@ -104,30 +110,47 @@ func (c *liveStateCache) loadCacheSettings() (*cacheSettings, error) {
|
||||
}
|
||||
|
||||
func (c *liveStateCache) getCluster(server string) (*clusterInfo, error) {
|
||||
c.lock.RLock()
|
||||
info, ok := c.clusters[server]
|
||||
c.lock.RUnlock()
|
||||
|
||||
if ok {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
info, ok := c.clusters[server]
|
||||
if !ok {
|
||||
cluster, err := c.db.GetCluster(context.Background(), server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info = &clusterInfo{
|
||||
apisMeta: make(map[schema.GroupKind]*apiMeta),
|
||||
lock: &sync.Mutex{},
|
||||
nodes: make(map[kube.ResourceKey]*node),
|
||||
nsIndex: make(map[string]map[kube.ResourceKey]*node),
|
||||
onObjectUpdated: c.onObjectUpdated,
|
||||
kubectl: c.kubectl,
|
||||
cluster: cluster,
|
||||
syncTime: nil,
|
||||
syncLock: &sync.Mutex{},
|
||||
log: log.WithField("server", cluster.Server),
|
||||
cacheSettingsSrc: c.getCacheSettings,
|
||||
}
|
||||
|
||||
c.clusters[cluster.Server] = info
|
||||
info, ok = c.clusters[server]
|
||||
if ok {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
logCtx := log.WithField("server", server)
|
||||
logCtx.Info("initializing cluster")
|
||||
cluster, err := c.db.GetCluster(context.Background(), server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info = &clusterInfo{
|
||||
apisMeta: make(map[schema.GroupKind]*apiMeta),
|
||||
lock: &sync.RWMutex{},
|
||||
nodes: make(map[kube.ResourceKey]*node),
|
||||
nsIndex: make(map[string]map[kube.ResourceKey]*node),
|
||||
onObjectUpdated: c.onObjectUpdated,
|
||||
kubectl: c.kubectl,
|
||||
cluster: cluster,
|
||||
syncTime: nil,
|
||||
log: logCtx,
|
||||
cacheSettingsSrc: c.getCacheSettings,
|
||||
onEventReceived: func(event watch.EventType, un *unstructured.Unstructured) {
|
||||
gvk := un.GroupVersionKind()
|
||||
c.metricsServer.IncClusterEventsCount(cluster.Server, gvk.Group, gvk.Kind)
|
||||
},
|
||||
metricsServer: c.metricsServer,
|
||||
}
|
||||
c.clusters[cluster.Server] = info
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
@@ -145,12 +168,10 @@ func (c *liveStateCache) getSyncedCluster(server string) (*clusterInfo, error) {
|
||||
|
||||
func (c *liveStateCache) Invalidate() {
|
||||
log.Info("invalidating live state cache")
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.lock.RLock()
|
||||
defer c.lock.RLock()
|
||||
for _, clust := range c.clusters {
|
||||
clust.lock.Lock()
|
||||
clust.invalidate()
|
||||
clust.lock.Unlock()
|
||||
}
|
||||
log.Info("live state cache invalidated")
|
||||
}
|
||||
@@ -185,7 +206,15 @@ func (c *liveStateCache) GetManagedLiveObjs(a *appv1.Application, targetObjs []*
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return clusterInfo.getManagedLiveObjs(a, targetObjs, c.metricsServer)
|
||||
return clusterInfo.getManagedLiveObjs(a, targetObjs)
|
||||
}
|
||||
|
||||
func (c *liveStateCache) GetVersionsInfo(serverURL string) (string, []metav1.APIGroup, error) {
|
||||
clusterInfo, err := c.getSyncedCluster(serverURL)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return clusterInfo.serverVersion, clusterInfo.apiGroups, nil
|
||||
}
|
||||
|
||||
func isClusterHasApps(apps []interface{}, cluster *appv1.Cluster) bool {
|
||||
@@ -198,8 +227,6 @@ func isClusterHasApps(apps []interface{}, cluster *appv1.Cluster) bool {
|
||||
}
|
||||
|
||||
func (c *liveStateCache) getCacheSettings() *cacheSettings {
|
||||
c.cacheSettingsLock.Lock()
|
||||
defer c.cacheSettingsLock.Unlock()
|
||||
return c.cacheSettings
|
||||
}
|
||||
|
||||
@@ -249,8 +276,9 @@ func (c *liveStateCache) Run(ctx context.Context) error {
|
||||
util.RetryUntilSucceed(func() error {
|
||||
clusterEventCallback := func(event *db.ClusterEvent) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if cluster, ok := c.clusters[event.Cluster.Server]; ok {
|
||||
cluster, ok := c.clusters[event.Cluster.Server]
|
||||
if ok {
|
||||
defer c.lock.Unlock()
|
||||
if event.Type == watch.Deleted {
|
||||
cluster.invalidate()
|
||||
delete(c.clusters, event.Cluster.Server)
|
||||
@@ -258,11 +286,14 @@ func (c *liveStateCache) Run(ctx context.Context) error {
|
||||
cluster.cluster = event.Cluster
|
||||
cluster.invalidate()
|
||||
}
|
||||
} else if event.Type == watch.Added && isClusterHasApps(c.appInformer.GetStore().List(), event.Cluster) {
|
||||
go func() {
|
||||
// warm up cache for cluster with apps
|
||||
_, _ = c.getSyncedCluster(event.Cluster.Server)
|
||||
}()
|
||||
} else {
|
||||
c.lock.Unlock()
|
||||
if event.Type == watch.Added && isClusterHasApps(c.appInformer.GetStore().List(), event.Cluster) {
|
||||
go func() {
|
||||
// warm up cache for cluster with apps
|
||||
_, _ = c.getSyncedCluster(event.Cluster.Server)
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,3 +304,13 @@ func (c *liveStateCache) Run(ctx context.Context) error {
|
||||
<-ctx.Done()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) GetClustersInfo() []metrics.ClusterInfo {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
res := make([]metrics.ClusterInfo, 0)
|
||||
for _, info := range c.clusters {
|
||||
res = append(res, info.getClusterInfo())
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
26
controller/cache/cache_test.go
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetServerVersion(t *testing.T) {
|
||||
now := time.Now()
|
||||
cache := &liveStateCache{
|
||||
lock: &sync.RWMutex{},
|
||||
clusters: map[string]*clusterInfo{
|
||||
"http://localhost": {
|
||||
syncTime: &now,
|
||||
lock: &sync.RWMutex{},
|
||||
serverVersion: "123",
|
||||
},
|
||||
}}
|
||||
|
||||
version, _, err := cache.GetVersionsInfo("http://localhost")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "123", version)
|
||||
}
|
||||
280
controller/cache/cluster.go
vendored
@@ -9,21 +9,22 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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/types"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/dynamic"
|
||||
|
||||
"github.com/argoproj/argo-cd/controller/metrics"
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/health"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/tools/pager"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -39,32 +40,37 @@ type apiMeta struct {
|
||||
}
|
||||
|
||||
type clusterInfo struct {
|
||||
syncLock *sync.Mutex
|
||||
syncTime *time.Time
|
||||
syncError error
|
||||
apisMeta map[schema.GroupKind]*apiMeta
|
||||
syncTime *time.Time
|
||||
syncError error
|
||||
apisMeta map[schema.GroupKind]*apiMeta
|
||||
serverVersion string
|
||||
apiGroups []metav1.APIGroup
|
||||
// namespacedResources is a simple map which indicates a groupKind is namespaced
|
||||
namespacedResources map[schema.GroupKind]bool
|
||||
|
||||
lock *sync.Mutex
|
||||
// lock is a rw lock which protects the fields of clusterInfo
|
||||
lock *sync.RWMutex
|
||||
nodes map[kube.ResourceKey]*node
|
||||
nsIndex map[string]map[kube.ResourceKey]*node
|
||||
|
||||
onObjectUpdated ObjectUpdatedHandler
|
||||
onEventReceived func(event watch.EventType, un *unstructured.Unstructured)
|
||||
kubectl kube.Kubectl
|
||||
cluster *appv1.Cluster
|
||||
log *log.Entry
|
||||
cacheSettingsSrc func() *cacheSettings
|
||||
metricsServer *metrics.MetricsServer
|
||||
}
|
||||
|
||||
func (c *clusterInfo) replaceResourceCache(gk schema.GroupKind, resourceVersion string, objs []unstructured.Unstructured) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
func (c *clusterInfo) replaceResourceCache(gk schema.GroupKind, resourceVersion string, objs []unstructured.Unstructured, ns string) {
|
||||
info, ok := c.apisMeta[gk]
|
||||
if ok {
|
||||
objByKind := make(map[kube.ResourceKey]*unstructured.Unstructured)
|
||||
objByKey := make(map[kube.ResourceKey]*unstructured.Unstructured)
|
||||
for i := range objs {
|
||||
objByKind[kube.GetResourceKey(&objs[i])] = &objs[i]
|
||||
objByKey[kube.GetResourceKey(&objs[i])] = &objs[i]
|
||||
}
|
||||
|
||||
// update existing nodes
|
||||
for i := range objs {
|
||||
obj := &objs[i]
|
||||
key := kube.GetResourceKey(&objs[i])
|
||||
@@ -72,12 +78,13 @@ func (c *clusterInfo) replaceResourceCache(gk schema.GroupKind, resourceVersion
|
||||
c.onNodeUpdated(exists, existingNode, obj, key)
|
||||
}
|
||||
|
||||
// remove existing nodes that a no longer exist
|
||||
for key, existingNode := range c.nodes {
|
||||
if key.Kind != gk.Kind || key.Group != gk.Group {
|
||||
if key.Kind != gk.Kind || key.Group != gk.Group || ns != "" && key.Namespace != ns {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := objByKind[key]; !ok {
|
||||
if _, ok := objByKey[key]; !ok {
|
||||
c.onNodeRemoved(key, existingNode)
|
||||
}
|
||||
}
|
||||
@@ -114,8 +121,9 @@ func isServiceAccountTokenSecret(un *unstructured.Unstructured) (bool, metav1.Ow
|
||||
|
||||
func (c *clusterInfo) createObjInfo(un *unstructured.Unstructured, appInstanceLabel string) *node {
|
||||
ownerRefs := un.GetOwnerReferences()
|
||||
gvk := un.GroupVersionKind()
|
||||
// Special case for endpoint. Remove after https://github.com/kubernetes/kubernetes/issues/28483 is fixed
|
||||
if un.GroupVersionKind().Group == "" && un.GetKind() == kube.EndpointsKind && len(un.GetOwnerReferences()) == 0 {
|
||||
if gvk.Group == "" && gvk.Kind == kube.EndpointsKind && len(un.GetOwnerReferences()) == 0 {
|
||||
ownerRefs = append(ownerRefs, metav1.OwnerReference{
|
||||
Name: un.GetName(),
|
||||
Kind: kube.ServiceKind,
|
||||
@@ -139,7 +147,15 @@ func (c *clusterInfo) createObjInfo(un *unstructured.Unstructured, appInstanceLa
|
||||
if len(ownerRefs) == 0 && appName != "" {
|
||||
nodeInfo.appName = appName
|
||||
nodeInfo.resource = un
|
||||
} else {
|
||||
// edge case. we do not label CRDs, so they miss the tracking label we inject. But we still
|
||||
// want the full resource to be available in our cache (to diff), so we store all CRDs
|
||||
switch gvk.Kind {
|
||||
case kube.CustomResourceDefinitionKind:
|
||||
nodeInfo.resource = un
|
||||
}
|
||||
}
|
||||
|
||||
nodeInfo.health, _ = health.GetResourceHealth(un, c.cacheSettingsSrc().ResourceOverrides)
|
||||
return nodeInfo
|
||||
}
|
||||
@@ -166,63 +182,80 @@ func (c *clusterInfo) removeNode(key kube.ResourceKey) {
|
||||
}
|
||||
|
||||
func (c *clusterInfo) invalidate() {
|
||||
c.syncLock.Lock()
|
||||
defer c.syncLock.Unlock()
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.syncTime = nil
|
||||
for i := range c.apisMeta {
|
||||
c.apisMeta[i].watchCancel()
|
||||
}
|
||||
c.apisMeta = nil
|
||||
c.namespacedResources = nil
|
||||
c.log.Warnf("invalidated cluster")
|
||||
}
|
||||
|
||||
func (c *clusterInfo) synced() bool {
|
||||
if c.syncTime == nil {
|
||||
syncTime := c.syncTime
|
||||
if syncTime == nil {
|
||||
return false
|
||||
}
|
||||
if c.syncError != nil {
|
||||
return time.Now().Before(c.syncTime.Add(clusterRetryTimeout))
|
||||
return time.Now().Before(syncTime.Add(clusterRetryTimeout))
|
||||
}
|
||||
return time.Now().Before(c.syncTime.Add(clusterSyncTimeout))
|
||||
return time.Now().Before(syncTime.Add(clusterSyncTimeout))
|
||||
}
|
||||
|
||||
func (c *clusterInfo) stopWatching(gk schema.GroupKind) {
|
||||
c.syncLock.Lock()
|
||||
defer c.syncLock.Unlock()
|
||||
func (c *clusterInfo) stopWatching(gk schema.GroupKind, ns string) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if info, ok := c.apisMeta[gk]; ok {
|
||||
info.watchCancel()
|
||||
delete(c.apisMeta, gk)
|
||||
c.replaceResourceCache(gk, "", []unstructured.Unstructured{})
|
||||
log.Warnf("Stop watching %s not found on %s.", gk, c.cluster.Server)
|
||||
c.replaceResourceCache(gk, "", []unstructured.Unstructured{}, ns)
|
||||
c.log.Warnf("Stop watching: %s not found", gk)
|
||||
}
|
||||
}
|
||||
|
||||
// startMissingWatches lists supported cluster resources and start watching for changes unless watch is already running
|
||||
func (c *clusterInfo) startMissingWatches() error {
|
||||
config := c.cluster.RESTConfig()
|
||||
|
||||
apis, err := c.kubectl.GetAPIResources(c.cluster.RESTConfig(), c.cacheSettingsSrc().ResourcesFilter)
|
||||
apis, err := c.kubectl.GetAPIResources(config, c.cacheSettingsSrc().ResourcesFilter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := c.kubectl.NewDynamicClient(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
namespacedResources := make(map[schema.GroupKind]bool)
|
||||
for i := range apis {
|
||||
api := apis[i]
|
||||
namespacedResources[api.GroupKind] = api.Meta.Namespaced
|
||||
if _, ok := c.apisMeta[api.GroupKind]; !ok {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
info := &apiMeta{namespaced: api.Meta.Namespaced, watchCancel: cancel}
|
||||
c.apisMeta[api.GroupKind] = info
|
||||
go c.watchEvents(ctx, api, info)
|
||||
|
||||
err = c.processApi(client, api, func(resClient dynamic.ResourceInterface, ns string) error {
|
||||
go c.watchEvents(ctx, api, info, resClient, ns)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
c.namespacedResources = namespacedResources
|
||||
return nil
|
||||
}
|
||||
|
||||
func runSynced(lock *sync.Mutex, action func() error) error {
|
||||
func runSynced(lock sync.Locker, action func() error) error {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
return action()
|
||||
}
|
||||
|
||||
func (c *clusterInfo) watchEvents(ctx context.Context, api kube.APIResourceInfo, info *apiMeta) {
|
||||
func (c *clusterInfo) watchEvents(ctx context.Context, api kube.APIResourceInfo, info *apiMeta, resClient dynamic.ResourceInterface, ns string) {
|
||||
util.RetryUntilSucceed(func() (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
@@ -230,13 +263,28 @@ func (c *clusterInfo) watchEvents(ctx context.Context, api kube.APIResourceInfo,
|
||||
}
|
||||
}()
|
||||
|
||||
err = runSynced(c.syncLock, func() error {
|
||||
err = runSynced(c.lock, func() error {
|
||||
if info.resourceVersion == "" {
|
||||
list, err := api.Interface.List(metav1.ListOptions{})
|
||||
listPager := pager.New(func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) {
|
||||
res, err := resClient.List(opts)
|
||||
if err == nil {
|
||||
info.resourceVersion = res.GetResourceVersion()
|
||||
}
|
||||
return res, err
|
||||
})
|
||||
var items []unstructured.Unstructured
|
||||
err = listPager.EachListItem(ctx, metav1.ListOptions{}, func(obj runtime.Object) error {
|
||||
if un, ok := obj.(*unstructured.Unstructured); !ok {
|
||||
return fmt.Errorf("object %s/%s has an unexpected type", un.GroupVersionKind().String(), un.GetName())
|
||||
} else {
|
||||
items = append(items, *un)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to load initial state of resource %s: %v", api.GroupKind.String(), err)
|
||||
}
|
||||
c.replaceResourceCache(api.GroupKind, list.GetResourceVersion(), list.Items)
|
||||
c.replaceResourceCache(api.GroupKind, info.resourceVersion, items, ns)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -245,20 +293,15 @@ func (c *clusterInfo) watchEvents(ctx context.Context, api kube.APIResourceInfo,
|
||||
return err
|
||||
}
|
||||
|
||||
w, err := api.Interface.Watch(metav1.ListOptions{ResourceVersion: info.resourceVersion})
|
||||
w, err := resClient.Watch(metav1.ListOptions{ResourceVersion: info.resourceVersion})
|
||||
if errors.IsNotFound(err) {
|
||||
c.stopWatching(api.GroupKind)
|
||||
c.stopWatching(api.GroupKind, ns)
|
||||
return nil
|
||||
}
|
||||
|
||||
err = runSynced(c.syncLock, func() error {
|
||||
if errors.IsGone(err) {
|
||||
info.resourceVersion = ""
|
||||
log.Warnf("Resource version of %s on %s is too old.", api.GroupKind, c.cluster.Server)
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
if errors.IsGone(err) {
|
||||
info.resourceVersion = ""
|
||||
c.log.Warnf("Resource version of %s is too old", api.GroupKind)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -279,17 +322,17 @@ func (c *clusterInfo) watchEvents(ctx context.Context, api kube.APIResourceInfo,
|
||||
|
||||
if groupOk && groupErr == nil && kindOk && kindErr == nil {
|
||||
gk := schema.GroupKind{Group: group, Kind: kind}
|
||||
c.stopWatching(gk)
|
||||
c.stopWatching(gk, ns)
|
||||
}
|
||||
} else {
|
||||
err = runSynced(c.syncLock, func() error {
|
||||
err = runSynced(c.lock, func() error {
|
||||
return c.startMissingWatches()
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Warnf("Failed to start missing watch: %v", err)
|
||||
c.log.Warnf("Failed to start missing watch: %v", err)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("Watch %s on %s has closed", api.GroupKind, c.cluster.Server)
|
||||
@@ -300,6 +343,25 @@ func (c *clusterInfo) watchEvents(ctx context.Context, api kube.APIResourceInfo,
|
||||
}, fmt.Sprintf("watch %s on %s", api.GroupKind, c.cluster.Server), ctx, watchResourcesRetryTimeout)
|
||||
}
|
||||
|
||||
func (c *clusterInfo) processApi(client dynamic.Interface, api kube.APIResourceInfo, callback func(resClient dynamic.ResourceInterface, ns string) error) error {
|
||||
resClient := client.Resource(api.GroupVersionResource)
|
||||
if len(c.cluster.Namespaces) == 0 {
|
||||
return callback(resClient, "")
|
||||
}
|
||||
|
||||
if !api.Meta.Namespaced {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, ns := range c.cluster.Namespaces {
|
||||
err := callback(resClient.Namespace(ns), ns)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *clusterInfo) sync() (err error) {
|
||||
|
||||
c.log.Info("Start syncing cluster")
|
||||
@@ -309,30 +371,69 @@ func (c *clusterInfo) sync() (err error) {
|
||||
}
|
||||
c.apisMeta = make(map[schema.GroupKind]*apiMeta)
|
||||
c.nodes = make(map[kube.ResourceKey]*node)
|
||||
c.namespacedResources = make(map[schema.GroupKind]bool)
|
||||
config := c.cluster.RESTConfig()
|
||||
version, err := c.kubectl.GetServerVersion(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.serverVersion = version
|
||||
groups, err := c.kubectl.GetAPIGroups(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.apiGroups = groups
|
||||
|
||||
apis, err := c.kubectl.GetAPIResources(c.cluster.RESTConfig(), c.cacheSettingsSrc().ResourcesFilter)
|
||||
apis, err := c.kubectl.GetAPIResources(config, c.cacheSettingsSrc().ResourcesFilter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := c.kubectl.NewDynamicClient(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lock := sync.Mutex{}
|
||||
err = util.RunAllAsync(len(apis), func(i int) error {
|
||||
api := apis[i]
|
||||
list, err := api.Interface.List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lock.Lock()
|
||||
for i := range list.Items {
|
||||
c.setNode(c.createObjInfo(&list.Items[i], c.cacheSettingsSrc().AppInstanceLabelKey))
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
info := &apiMeta{namespaced: api.Meta.Namespaced, watchCancel: cancel}
|
||||
c.apisMeta[api.GroupKind] = info
|
||||
c.namespacedResources[api.GroupKind] = api.Meta.Namespaced
|
||||
lock.Unlock()
|
||||
return nil
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
err = c.startMissingWatches()
|
||||
}
|
||||
return c.processApi(client, api, func(resClient dynamic.ResourceInterface, ns string) error {
|
||||
|
||||
listPager := pager.New(func(ctx context.Context, opts metav1.ListOptions) (runtime.Object, error) {
|
||||
res, err := resClient.List(opts)
|
||||
if err == nil {
|
||||
lock.Lock()
|
||||
info.resourceVersion = res.GetResourceVersion()
|
||||
lock.Unlock()
|
||||
}
|
||||
return res, err
|
||||
})
|
||||
|
||||
err = listPager.EachListItem(context.Background(), metav1.ListOptions{}, func(obj runtime.Object) error {
|
||||
if un, ok := obj.(*unstructured.Unstructured); !ok {
|
||||
return fmt.Errorf("object %s/%s has an unexpected type", un.GroupVersionKind().String(), un.GetName())
|
||||
} else {
|
||||
lock.Lock()
|
||||
c.setNode(c.createObjInfo(un, c.cacheSettingsSrc().AppInstanceLabelKey))
|
||||
lock.Unlock()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load initial state of resource %s: %v", api.GroupKind.String(), err)
|
||||
}
|
||||
|
||||
go c.watchEvents(ctx, api, info, resClient, ns)
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Failed to sync cluster %s: %v", c.cluster.Server, err)
|
||||
@@ -344,12 +445,17 @@ func (c *clusterInfo) sync() (err error) {
|
||||
}
|
||||
|
||||
func (c *clusterInfo) ensureSynced() error {
|
||||
c.syncLock.Lock()
|
||||
defer c.syncLock.Unlock()
|
||||
// first check if cluster is synced *without lock*
|
||||
if c.synced() {
|
||||
return c.syncError
|
||||
}
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
// before doing any work, check once again now that we have the lock, to see if it got
|
||||
// synced between the first check and now
|
||||
if c.synced() {
|
||||
return c.syncError
|
||||
}
|
||||
|
||||
err := c.sync()
|
||||
syncTime := time.Now()
|
||||
c.syncTime = &syncTime
|
||||
@@ -358,8 +464,8 @@ func (c *clusterInfo) ensureSynced() error {
|
||||
}
|
||||
|
||||
func (c *clusterInfo) getNamespaceTopLevelResources(namespace string) map[kube.ResourceKey]appv1.ResourceNode {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
nodes := make(map[kube.ResourceKey]appv1.ResourceNode)
|
||||
for _, node := range c.nsIndex[namespace] {
|
||||
if len(node.ownerRefs) == 0 {
|
||||
@@ -400,15 +506,17 @@ func (c *clusterInfo) iterateHierarchy(key kube.ResourceKey, action func(child a
|
||||
}
|
||||
|
||||
func (c *clusterInfo) isNamespaced(gk schema.GroupKind) bool {
|
||||
if api, ok := c.apisMeta[gk]; ok && !api.namespaced {
|
||||
return false
|
||||
// this is safe to access without a lock since we always replace the entire map instead of mutating keys
|
||||
if isNamespaced, ok := c.namespacedResources[gk]; ok {
|
||||
return isNamespaced
|
||||
}
|
||||
log.Warnf("group/kind %s scope is unknown (known objects: %d). assuming namespaced object", gk, len(c.namespacedResources))
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured, metricsServer *metrics.MetricsServer) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
|
||||
managedObjs := make(map[kube.ResourceKey]*unstructured.Unstructured)
|
||||
// iterate all objects in live state cache to find ones associated with app
|
||||
@@ -417,8 +525,8 @@ func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*uns
|
||||
managedObjs[key] = o.resource
|
||||
}
|
||||
}
|
||||
config := metrics.AddMetricsTransportWrapper(metricsServer, a, c.cluster.RESTConfig())
|
||||
// iterate target objects and identify ones that already exist in the cluster,\
|
||||
config := metrics.AddMetricsTransportWrapper(c.metricsServer, a, c.cluster.RESTConfig())
|
||||
// iterate target objects and identify ones that already exist in the cluster,
|
||||
// but are simply missing our label
|
||||
lock := &sync.Mutex{}
|
||||
err := util.RunAllAsync(len(targetObjs), func(i int) error {
|
||||
@@ -483,9 +591,15 @@ func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*uns
|
||||
}
|
||||
|
||||
func (c *clusterInfo) processEvent(event watch.EventType, un *unstructured.Unstructured) {
|
||||
if c.onEventReceived != nil {
|
||||
c.onEventReceived(event, un)
|
||||
}
|
||||
key := kube.GetResourceKey(un)
|
||||
if event == watch.Modified && skipAppRequeing(key) {
|
||||
return
|
||||
}
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
key := kube.GetResourceKey(un)
|
||||
existingNode, exists := c.nodes[key]
|
||||
if event == watch.Deleted {
|
||||
if exists {
|
||||
@@ -509,7 +623,7 @@ func (c *clusterInfo) onNodeUpdated(exists bool, existingNode *node, un *unstruc
|
||||
n := nodes[i]
|
||||
if ns, ok := c.nsIndex[n.ref.Namespace]; ok {
|
||||
app := n.getApp(ns)
|
||||
if app == "" || skipAppRequeing(key) {
|
||||
if app == "" {
|
||||
continue
|
||||
}
|
||||
toNotify[app] = n.isRootAppNode() || toNotify[app]
|
||||
@@ -538,6 +652,18 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func (c *clusterInfo) getClusterInfo() metrics.ClusterInfo {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
return metrics.ClusterInfo{
|
||||
APIsCount: len(c.apisMeta),
|
||||
K8SVersion: c.serverVersion,
|
||||
ResourcesCount: len(c.nodes),
|
||||
Server: c.cluster.Server,
|
||||
LastCacheSyncTime: c.syncTime,
|
||||
}
|
||||
}
|
||||
|
||||
// skipAppRequeing checks if the object is an API type which we want to skip requeuing against.
|
||||
// We ignore API types which have a high churn rate, and/or whose updates are irrelevant to the app
|
||||
func skipAppRequeing(key kube.ResourceKey) bool {
|
||||
|
||||
98
controller/cache/cluster_test.go
vendored
@@ -135,32 +135,31 @@ func newCluster(objs ...*unstructured.Unstructured) *clusterInfo {
|
||||
client := fake.NewSimpleDynamicClient(scheme, runtimeObjs...)
|
||||
|
||||
apiResources := []kube.APIResourceInfo{{
|
||||
GroupKind: schema.GroupKind{Group: "", Kind: "Pod"},
|
||||
Interface: client.Resource(schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}),
|
||||
Meta: metav1.APIResource{Namespaced: true},
|
||||
GroupKind: schema.GroupKind{Group: "", Kind: "Pod"},
|
||||
GroupVersionResource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
|
||||
Meta: metav1.APIResource{Namespaced: true},
|
||||
}, {
|
||||
GroupKind: schema.GroupKind{Group: "apps", Kind: "ReplicaSet"},
|
||||
Interface: client.Resource(schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "replicasets"}),
|
||||
Meta: metav1.APIResource{Namespaced: true},
|
||||
GroupKind: schema.GroupKind{Group: "apps", Kind: "ReplicaSet"},
|
||||
GroupVersionResource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "replicasets"},
|
||||
Meta: metav1.APIResource{Namespaced: true},
|
||||
}, {
|
||||
GroupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"},
|
||||
Interface: client.Resource(schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}),
|
||||
Meta: metav1.APIResource{Namespaced: true},
|
||||
GroupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"},
|
||||
GroupVersionResource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"},
|
||||
Meta: metav1.APIResource{Namespaced: true},
|
||||
}}
|
||||
|
||||
return newClusterExt(&kubetest.MockKubectlCmd{APIResources: apiResources})
|
||||
return newClusterExt(&kubetest.MockKubectlCmd{APIResources: apiResources, DynamicClient: client})
|
||||
}
|
||||
|
||||
func newClusterExt(kubectl kube.Kubectl) *clusterInfo {
|
||||
return &clusterInfo{
|
||||
lock: &sync.Mutex{},
|
||||
lock: &sync.RWMutex{},
|
||||
nodes: make(map[kube.ResourceKey]*node),
|
||||
onObjectUpdated: func(managedByApp map[string]bool, reference corev1.ObjectReference) {},
|
||||
kubectl: kubectl,
|
||||
nsIndex: make(map[string]map[kube.ResourceKey]*node),
|
||||
cluster: &appv1.Cluster{},
|
||||
syncTime: nil,
|
||||
syncLock: &sync.Mutex{},
|
||||
apisMeta: make(map[schema.GroupKind]*apiMeta),
|
||||
log: log.WithField("cluster", "test"),
|
||||
cacheSettingsSrc: func() *cacheSettings {
|
||||
@@ -177,6 +176,55 @@ func getChildren(cluster *clusterInfo, un *unstructured.Unstructured) []appv1.Re
|
||||
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
|
||||
@@ -274,7 +322,7 @@ metadata:
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
}, []*unstructured.Unstructured{targetDeploy}, nil)
|
||||
}, []*unstructured.Unstructured{targetDeploy})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, managedObjs, map[kube.ResourceKey]*unstructured.Unstructured{
|
||||
kube.NewResourceKey("apps", "Deployment", "default", "helm-guestbook"): testDeploy,
|
||||
@@ -456,7 +504,7 @@ func TestWatchCacheUpdated(t *testing.T) {
|
||||
|
||||
podGroupKind := testPod.GroupVersionKind().GroupKind()
|
||||
|
||||
cluster.replaceResourceCache(podGroupKind, "updated-list-version", []unstructured.Unstructured{*updated, *added})
|
||||
cluster.replaceResourceCache(podGroupKind, "updated-list-version", []unstructured.Unstructured{*updated, *added}, "")
|
||||
|
||||
_, ok := cluster.nodes[kube.GetResourceKey(removed)]
|
||||
assert.False(t, ok)
|
||||
@@ -469,6 +517,28 @@ func TestWatchCacheUpdated(t *testing.T) {
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestNamespaceModeReplace(t *testing.T) {
|
||||
ns1Pod := testPod.DeepCopy()
|
||||
ns1Pod.SetNamespace("ns1")
|
||||
ns1Pod.SetName("pod1")
|
||||
|
||||
ns2Pod := testPod.DeepCopy()
|
||||
ns2Pod.SetNamespace("ns2")
|
||||
podGroupKind := testPod.GroupVersionKind().GroupKind()
|
||||
|
||||
cluster := newCluster(ns1Pod, ns2Pod)
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
cluster.replaceResourceCache(podGroupKind, "", nil, "ns1")
|
||||
|
||||
_, ok := cluster.nodes[kube.GetResourceKey(ns1Pod)]
|
||||
assert.False(t, ok)
|
||||
|
||||
_, ok = cluster.nodes[kube.GetResourceKey(ns2Pod)]
|
||||
assert.True(t, ok)
|
||||
}
|
||||
|
||||
func TestGetDuplicatedChildren(t *testing.T) {
|
||||
extensionsRS := testRS.DeepCopy()
|
||||
extensionsRS.SetGroupVersionKind(schema.GroupVersionKind{Group: "extensions", Kind: kube.ReplicaSetKind, Version: "v1beta1"})
|
||||
|
||||
53
controller/cache/mocks/LiveStateCache.go
vendored
@@ -5,14 +5,17 @@ package mocks
|
||||
import (
|
||||
context "context"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
metrics "github.com/argoproj/argo-cd/controller/metrics"
|
||||
kube "github.com/argoproj/argo-cd/util/kube"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
schema "k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
v1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
@@ -21,6 +24,22 @@ type LiveStateCache struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// GetClustersInfo provides a mock function with given fields:
|
||||
func (_m *LiveStateCache) GetClustersInfo() []metrics.ClusterInfo {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []metrics.ClusterInfo
|
||||
if rf, ok := ret.Get(0).(func() []metrics.ClusterInfo); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]metrics.ClusterInfo)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetManagedLiveObjs provides a mock function with given fields: a, targetObjs
|
||||
func (_m *LiveStateCache) GetManagedLiveObjs(a *v1alpha1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
|
||||
ret := _m.Called(a, targetObjs)
|
||||
@@ -67,6 +86,36 @@ func (_m *LiveStateCache) GetNamespaceTopLevelResources(server string, namespace
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetVersionsInfo provides a mock function with given fields: serverURL
|
||||
func (_m *LiveStateCache) GetVersionsInfo(serverURL string) (string, []v1.APIGroup, error) {
|
||||
ret := _m.Called(serverURL)
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func(string) string); ok {
|
||||
r0 = rf(serverURL)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
var r1 []v1.APIGroup
|
||||
if rf, ok := ret.Get(1).(func(string) []v1.APIGroup); ok {
|
||||
r1 = rf(serverURL)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).([]v1.APIGroup)
|
||||
}
|
||||
}
|
||||
|
||||
var r2 error
|
||||
if rf, ok := ret.Get(2).(func(string) error); ok {
|
||||
r2 = rf(serverURL)
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// Invalidate provides a mock function with given fields:
|
||||
func (_m *LiveStateCache) Invalidate() {
|
||||
_m.Called()
|
||||
|
||||
99
controller/metrics/clustercollector.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
metricsCollectionInterval = 30 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
descClusterDefaultLabels = []string{"server"}
|
||||
|
||||
descClusterInfo = prometheus.NewDesc(
|
||||
"argocd_cluster_info",
|
||||
"Information about cluster.",
|
||||
append(descClusterDefaultLabels, "k8s_version"),
|
||||
nil,
|
||||
)
|
||||
descClusterCacheResources = prometheus.NewDesc(
|
||||
"argocd_cluster_api_resource_objects",
|
||||
"Number of k8s resource objects in the cache.",
|
||||
descClusterDefaultLabels,
|
||||
nil,
|
||||
)
|
||||
descClusterAPIs = prometheus.NewDesc(
|
||||
"argocd_cluster_api_resources",
|
||||
"Number of monitored kubernetes API resources.",
|
||||
descClusterDefaultLabels,
|
||||
nil,
|
||||
)
|
||||
descClusterCacheAgeSeconds = prometheus.NewDesc(
|
||||
"argocd_cluster_cache_age_seconds",
|
||||
"Cluster cache age in seconds.",
|
||||
descClusterDefaultLabels,
|
||||
nil,
|
||||
)
|
||||
)
|
||||
|
||||
type ClusterInfo struct {
|
||||
Server string
|
||||
K8SVersion string
|
||||
ResourcesCount int
|
||||
APIsCount int
|
||||
LastCacheSyncTime *time.Time
|
||||
}
|
||||
|
||||
type HasClustersInfo interface {
|
||||
GetClustersInfo() []ClusterInfo
|
||||
}
|
||||
|
||||
type clusterCollector struct {
|
||||
infoSource HasClustersInfo
|
||||
info []ClusterInfo
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func (c *clusterCollector) Run(ctx context.Context) {
|
||||
tick := time.Tick(metricsCollectionInterval)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break
|
||||
case <-tick:
|
||||
info := c.infoSource.GetClustersInfo()
|
||||
|
||||
c.lock.Lock()
|
||||
c.info = info
|
||||
c.lock.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Describe implements the prometheus.Collector interface
|
||||
func (c *clusterCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- descClusterInfo
|
||||
ch <- descClusterCacheResources
|
||||
ch <- descClusterAPIs
|
||||
ch <- descClusterCacheAgeSeconds
|
||||
}
|
||||
|
||||
func (c *clusterCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
now := time.Now()
|
||||
for _, c := range c.info {
|
||||
defaultValues := []string{c.Server}
|
||||
ch <- prometheus.MustNewConstMetric(descClusterInfo, prometheus.GaugeValue, 1, append(defaultValues, c.K8SVersion)...)
|
||||
ch <- prometheus.MustNewConstMetric(descClusterCacheResources, prometheus.GaugeValue, float64(c.ResourcesCount), defaultValues...)
|
||||
ch <- prometheus.MustNewConstMetric(descClusterAPIs, prometheus.GaugeValue, float64(c.APIsCount), defaultValues...)
|
||||
cacheAgeSeconds := -1
|
||||
if c.LastCacheSyncTime != nil {
|
||||
cacheAgeSeconds = int(now.Sub(*c.LastCacheSyncTime).Seconds())
|
||||
}
|
||||
ch <- prometheus.MustNewConstMetric(descClusterCacheAgeSeconds, prometheus.GaugeValue, float64(cacheAgeSeconds), defaultValues...)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
@@ -19,15 +20,19 @@ import (
|
||||
type MetricsServer struct {
|
||||
*http.Server
|
||||
syncCounter *prometheus.CounterVec
|
||||
k8sRequestCounter *prometheus.CounterVec
|
||||
kubectlExecCounter *prometheus.CounterVec
|
||||
kubectlExecPendingGauge *prometheus.GaugeVec
|
||||
k8sRequestCounter *prometheus.CounterVec
|
||||
clusterEventsCounter *prometheus.CounterVec
|
||||
reconcileHistogram *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
|
||||
@@ -38,106 +43,119 @@ var (
|
||||
descAppInfo = prometheus.NewDesc(
|
||||
"argocd_app_info",
|
||||
"Information about application.",
|
||||
append(descAppDefaultLabels, "repo", "dest_server", "dest_namespace"),
|
||||
append(descAppDefaultLabels, "repo", "dest_server", "dest_namespace", "sync_status", "health_status", "operation"),
|
||||
nil,
|
||||
)
|
||||
// DEPRECATED
|
||||
descAppCreated = prometheus.NewDesc(
|
||||
"argocd_app_created_time",
|
||||
"Creation time in unix timestamp for an application.",
|
||||
descAppDefaultLabels,
|
||||
nil,
|
||||
)
|
||||
// DEPRECATED: superceded by sync_status label in argocd_app_info
|
||||
descAppSyncStatusCode = prometheus.NewDesc(
|
||||
"argocd_app_sync_status",
|
||||
"The application current sync status.",
|
||||
append(descAppDefaultLabels, "sync_status"),
|
||||
nil,
|
||||
)
|
||||
// DEPRECATED: superceded by health_status label in argocd_app_info
|
||||
descAppHealthStatus = prometheus.NewDesc(
|
||||
"argocd_app_health_status",
|
||||
"The application current health status.",
|
||||
append(descAppDefaultLabels, "health_status"),
|
||||
nil,
|
||||
)
|
||||
)
|
||||
|
||||
// NewMetricsServer returns a new prometheus server which collects application metrics
|
||||
func NewMetricsServer(addr string, appLister applister.ApplicationLister, healthCheck func() error) *MetricsServer {
|
||||
mux := http.NewServeMux()
|
||||
appRegistry := NewAppRegistry(appLister)
|
||||
appRegistry.MustRegister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}))
|
||||
appRegistry.MustRegister(prometheus.NewGoCollector())
|
||||
mux.Handle(MetricsPath, promhttp.HandlerFor(appRegistry, promhttp.HandlerOpts{}))
|
||||
healthz.ServeHealthCheck(mux, healthCheck)
|
||||
|
||||
syncCounter := prometheus.NewCounterVec(
|
||||
syncCounter = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "argocd_app_sync_total",
|
||||
Help: "Number of application syncs.",
|
||||
},
|
||||
append(descAppDefaultLabels, "phase"),
|
||||
append(descAppDefaultLabels, "dest_server", "phase"),
|
||||
)
|
||||
appRegistry.MustRegister(syncCounter)
|
||||
kubectlExecCounter := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "argocd_kubectl_exec_total",
|
||||
Help: "Number of kubectl executions",
|
||||
}, []string{"command"})
|
||||
appRegistry.MustRegister(kubectlExecCounter)
|
||||
kubectlExecPendingGauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "argocd_kubectl_exec_pending",
|
||||
Help: "Number of pending kubectl executions",
|
||||
}, []string{"command"})
|
||||
appRegistry.MustRegister(kubectlExecPendingGauge)
|
||||
k8sRequestCounter := prometheus.NewCounterVec(
|
||||
|
||||
k8sRequestCounter = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "argocd_app_k8s_request_total",
|
||||
Help: "Number of kubernetes requests executed during application reconciliation.",
|
||||
},
|
||||
append(descAppDefaultLabels, "response_code"),
|
||||
append(descAppDefaultLabels, "server", "response_code", "verb", "resource_kind", "resource_namespace"),
|
||||
)
|
||||
appRegistry.MustRegister(k8sRequestCounter)
|
||||
|
||||
reconcileHistogram := prometheus.NewHistogramVec(
|
||||
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},
|
||||
},
|
||||
descAppDefaultLabels,
|
||||
[]string{"namespace", "dest_server"},
|
||||
)
|
||||
|
||||
appRegistry.MustRegister(reconcileHistogram)
|
||||
clusterEventsCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "argocd_cluster_events_total",
|
||||
Help: "Number of processes k8s resource events.",
|
||||
}, append(descClusterDefaultLabels, "group", "kind"))
|
||||
)
|
||||
|
||||
// NewMetricsServer returns a new prometheus server which collects application metrics
|
||||
func NewMetricsServer(addr string, appLister applister.ApplicationLister, healthCheck func() error) *MetricsServer {
|
||||
mux := http.NewServeMux()
|
||||
registry := NewAppRegistry(appLister)
|
||||
mux.Handle(MetricsPath, promhttp.HandlerFor(prometheus.Gatherers{
|
||||
// contains app controller specific metrics
|
||||
registry,
|
||||
// contains process, golang and controller workqueues metrics
|
||||
prometheus.DefaultGatherer,
|
||||
}, promhttp.HandlerOpts{}))
|
||||
healthz.ServeHealthCheck(mux, healthCheck)
|
||||
|
||||
registry.MustRegister(syncCounter)
|
||||
registry.MustRegister(k8sRequestCounter)
|
||||
registry.MustRegister(kubectlExecCounter)
|
||||
registry.MustRegister(kubectlExecPendingGauge)
|
||||
registry.MustRegister(reconcileHistogram)
|
||||
registry.MustRegister(clusterEventsCounter)
|
||||
|
||||
return &MetricsServer{
|
||||
registry: registry,
|
||||
Server: &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
},
|
||||
syncCounter: syncCounter,
|
||||
k8sRequestCounter: k8sRequestCounter,
|
||||
reconcileHistogram: reconcileHistogram,
|
||||
kubectlExecCounter: kubectlExecCounter,
|
||||
kubectlExecPendingGauge: kubectlExecPendingGauge,
|
||||
reconcileHistogram: reconcileHistogram,
|
||||
clusterEventsCounter: clusterEventsCounter,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MetricsServer) RegisterClustersInfoSource(ctx context.Context, source HasClustersInfo) {
|
||||
collector := &clusterCollector{infoSource: source}
|
||||
go collector.Run(ctx)
|
||||
m.registry.MustRegister(collector)
|
||||
}
|
||||
|
||||
// IncSync increments the sync counter for an application
|
||||
func (m *MetricsServer) IncSync(app *argoappv1.Application, state *argoappv1.OperationState) {
|
||||
if !state.Phase.Completed() {
|
||||
return
|
||||
}
|
||||
m.syncCounter.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject(), string(state.Phase)).Inc()
|
||||
}
|
||||
|
||||
// IncKubernetesRequest increments the kubernetes requests counter for an application
|
||||
func (m *MetricsServer) IncKubernetesRequest(app *argoappv1.Application, statusCode int) {
|
||||
m.k8sRequestCounter.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject(), strconv.Itoa(statusCode)).Inc()
|
||||
}
|
||||
|
||||
// IncReconcile increments the reconcile counter for an application
|
||||
func (m *MetricsServer) IncReconcile(app *argoappv1.Application, duration time.Duration) {
|
||||
m.reconcileHistogram.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject()).Observe(duration.Seconds())
|
||||
m.syncCounter.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject(), app.Spec.Destination.Server, string(state.Phase)).Inc()
|
||||
}
|
||||
|
||||
func (m *MetricsServer) IncKubectlExec(command string) {
|
||||
@@ -152,6 +170,30 @@ func (m *MetricsServer) DecKubectlExecPending(command string) {
|
||||
m.kubectlExecPendingGauge.WithLabelValues(command).Dec()
|
||||
}
|
||||
|
||||
// IncClusterEventsCount increments the number of cluster events
|
||||
func (m *MetricsServer) IncClusterEventsCount(server, group, kind string) {
|
||||
m.clusterEventsCounter.WithLabelValues(server, group, kind).Inc()
|
||||
}
|
||||
|
||||
// IncKubernetesRequest increments the kubernetes requests counter for an application
|
||||
func (m *MetricsServer) IncKubernetesRequest(app *argoappv1.Application, server, statusCode, verb, resourceKind, resourceNamespace string) {
|
||||
var namespace, name, project string
|
||||
if app != nil {
|
||||
namespace = app.Namespace
|
||||
name = app.Name
|
||||
project = app.Spec.GetProject()
|
||||
}
|
||||
m.k8sRequestCounter.WithLabelValues(
|
||||
namespace, name, project, server, statusCode,
|
||||
verb, resourceKind, resourceNamespace,
|
||||
).Inc()
|
||||
}
|
||||
|
||||
// IncReconcile increments the reconcile counter for an application
|
||||
func (m *MetricsServer) IncReconcile(app *argoappv1.Application, duration time.Duration) {
|
||||
m.reconcileHistogram.WithLabelValues(app.Namespace, app.Spec.Destination.Server).Observe(duration.Seconds())
|
||||
}
|
||||
|
||||
type appCollector struct {
|
||||
store applister.ApplicationLister
|
||||
}
|
||||
@@ -173,7 +215,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
|
||||
}
|
||||
@@ -207,20 +248,36 @@ func collectApps(ch chan<- prometheus.Metric, app *argoappv1.Application) {
|
||||
addConstMetric(desc, prometheus.GaugeValue, v, lv...)
|
||||
}
|
||||
|
||||
addGauge(descAppInfo, 1, git.NormalizeGitURL(app.Spec.Source.RepoURL), app.Spec.Destination.Server, app.Spec.Destination.Namespace)
|
||||
|
||||
addGauge(descAppCreated, float64(app.CreationTimestamp.Unix()))
|
||||
|
||||
var operation string
|
||||
if app.DeletionTimestamp != nil {
|
||||
operation = "delete"
|
||||
} else if app.Operation != nil && app.Operation.Sync != nil {
|
||||
operation = "sync"
|
||||
}
|
||||
syncStatus := app.Status.Sync.Status
|
||||
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeSynced), string(argoappv1.SyncStatusCodeSynced))
|
||||
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeOutOfSync), string(argoappv1.SyncStatusCodeOutOfSync))
|
||||
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeUnknown || syncStatus == ""), string(argoappv1.SyncStatusCodeUnknown))
|
||||
|
||||
if syncStatus == "" {
|
||||
syncStatus = argoappv1.SyncStatusCodeUnknown
|
||||
}
|
||||
healthStatus := app.Status.Health.Status
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusUnknown || healthStatus == ""), argoappv1.HealthStatusUnknown)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusProgressing), argoappv1.HealthStatusProgressing)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusSuspended), argoappv1.HealthStatusSuspended)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusHealthy), argoappv1.HealthStatusHealthy)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusDegraded), argoappv1.HealthStatusDegraded)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusMissing), argoappv1.HealthStatusMissing)
|
||||
if healthStatus == "" {
|
||||
healthStatus = argoappv1.HealthStatusUnknown
|
||||
}
|
||||
|
||||
addGauge(descAppInfo, 1, git.NormalizeGitURL(app.Spec.Source.RepoURL), app.Spec.Destination.Server, app.Spec.Destination.Namespace, string(syncStatus), healthStatus, operation)
|
||||
|
||||
// Deprecated controller metrics
|
||||
if os.Getenv(EnvVarLegacyControllerMetrics) == "true" {
|
||||
addGauge(descAppCreated, float64(app.CreationTimestamp.Unix()))
|
||||
|
||||
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeSynced), string(argoappv1.SyncStatusCodeSynced))
|
||||
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeOutOfSync), string(argoappv1.SyncStatusCodeOutOfSync))
|
||||
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeUnknown || syncStatus == ""), string(argoappv1.SyncStatusCodeUnknown))
|
||||
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusUnknown || healthStatus == ""), argoappv1.HealthStatusUnknown)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusProgressing), argoappv1.HealthStatusProgressing)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusSuspended), argoappv1.HealthStatusSuspended)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusHealthy), argoappv1.HealthStatusHealthy)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusDegraded), argoappv1.HealthStatusDegraded)
|
||||
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusMissing), argoappv1.HealthStatusMissing)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -42,25 +43,52 @@ status:
|
||||
status: Healthy
|
||||
`
|
||||
|
||||
const expectedResponse = `# HELP argocd_app_created_time Creation time in unix timestamp for an application.
|
||||
# TYPE argocd_app_created_time gauge
|
||||
argocd_app_created_time{name="my-app",namespace="argocd",project="important-project"} -6.21355968e+10
|
||||
# HELP argocd_app_health_status The application current health status.
|
||||
# TYPE argocd_app_health_status gauge
|
||||
argocd_app_health_status{health_status="Degraded",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
argocd_app_health_status{health_status="Healthy",name="my-app",namespace="argocd",project="important-project"} 1
|
||||
argocd_app_health_status{health_status="Missing",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
argocd_app_health_status{health_status="Progressing",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
argocd_app_health_status{health_status="Suspended",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
argocd_app_health_status{health_status="Unknown",name="my-app",namespace="argocd",project="important-project"} 0
|
||||
# HELP argocd_app_info Information about application.
|
||||
# TYPE argocd_app_info gauge
|
||||
argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",name="my-app",namespace="argocd",project="important-project",repo="https://github.com/argoproj/argocd-example-apps"} 1
|
||||
# HELP argocd_app_sync_status The application current sync status.
|
||||
# TYPE argocd_app_sync_status gauge
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="OutOfSync"} 0
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="Synced"} 1
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="Unknown"} 0
|
||||
const fakeApp2 = `
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: my-app-2
|
||||
namespace: argocd
|
||||
spec:
|
||||
destination:
|
||||
namespace: dummy-namespace
|
||||
server: https://localhost:6443
|
||||
project: important-project
|
||||
source:
|
||||
path: some/path
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps.git
|
||||
status:
|
||||
sync:
|
||||
status: Synced
|
||||
health:
|
||||
status: Healthy
|
||||
operation:
|
||||
sync:
|
||||
revision: 041eab7439ece92c99b043f0e171788185b8fc1d
|
||||
syncStrategy:
|
||||
hook: {}
|
||||
`
|
||||
|
||||
const fakeApp3 = `
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: my-app-3
|
||||
namespace: argocd
|
||||
deletionTimestamp: "2020-03-16T09:17:45Z"
|
||||
spec:
|
||||
destination:
|
||||
namespace: dummy-namespace
|
||||
server: https://localhost:6443
|
||||
project: important-project
|
||||
source:
|
||||
path: some/path
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps.git
|
||||
status:
|
||||
sync:
|
||||
status: OutOfSync
|
||||
health:
|
||||
status: Degraded
|
||||
`
|
||||
|
||||
const fakeDefaultApp = `
|
||||
@@ -83,46 +111,26 @@ status:
|
||||
status: Healthy
|
||||
`
|
||||
|
||||
const expectedDefaultResponse = `# HELP argocd_app_created_time Creation time in unix timestamp for an application.
|
||||
# TYPE argocd_app_created_time gauge
|
||||
argocd_app_created_time{name="my-app",namespace="argocd",project="default"} -6.21355968e+10
|
||||
# HELP argocd_app_health_status The application current health status.
|
||||
# TYPE argocd_app_health_status gauge
|
||||
argocd_app_health_status{health_status="Degraded",name="my-app",namespace="argocd",project="default"} 0
|
||||
argocd_app_health_status{health_status="Healthy",name="my-app",namespace="argocd",project="default"} 1
|
||||
argocd_app_health_status{health_status="Missing",name="my-app",namespace="argocd",project="default"} 0
|
||||
argocd_app_health_status{health_status="Progressing",name="my-app",namespace="argocd",project="default"} 0
|
||||
argocd_app_health_status{health_status="Suspended",name="my-app",namespace="argocd",project="default"} 0
|
||||
argocd_app_health_status{health_status="Unknown",name="my-app",namespace="argocd",project="default"} 0
|
||||
# HELP argocd_app_info Information about application.
|
||||
# TYPE argocd_app_info gauge
|
||||
argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",name="my-app",namespace="argocd",project="default",repo="https://github.com/argoproj/argocd-example-apps"} 1
|
||||
# HELP argocd_app_sync_status The application current sync status.
|
||||
# TYPE argocd_app_sync_status gauge
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="default",sync_status="OutOfSync"} 0
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="default",sync_status="Synced"} 1
|
||||
argocd_app_sync_status{name="my-app",namespace="argocd",project="default",sync_status="Unknown"} 0
|
||||
`
|
||||
|
||||
var noOpHealthCheck = func() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func newFakeApp(fakeApp string) *argoappv1.Application {
|
||||
func newFakeApp(fakeAppYAML string) *argoappv1.Application {
|
||||
var app argoappv1.Application
|
||||
err := yaml.Unmarshal([]byte(fakeApp), &app)
|
||||
err := yaml.Unmarshal([]byte(fakeAppYAML), &app)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &app
|
||||
}
|
||||
|
||||
func newFakeLister(fakeApp ...string) (context.CancelFunc, applister.ApplicationLister) {
|
||||
func newFakeLister(fakeAppYAMLs ...string) (context.CancelFunc, applister.ApplicationLister) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
var fakeApps []runtime.Object
|
||||
for _, name := range fakeApp {
|
||||
fakeApps = append(fakeApps, newFakeApp(name))
|
||||
for _, appYAML := range fakeAppYAMLs {
|
||||
a := newFakeApp(appYAML)
|
||||
fakeApps = append(fakeApps, a)
|
||||
}
|
||||
appClientset := appclientset.NewSimpleClientset(fakeApps...)
|
||||
factory := appinformer.NewFilteredSharedInformerFactory(appClientset, 0, "argocd", func(options *metav1.ListOptions) {})
|
||||
@@ -134,8 +142,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,39 +157,75 @@ 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})
|
||||
@@ -202,27 +246,31 @@ func TestMetricsSyncCounter(t *testing.T) {
|
||||
// assertMetricsPrinted asserts every line in the expected lines appears in the body
|
||||
func assertMetricsPrinted(t *testing.T, expectedLines, body string) {
|
||||
for _, line := range strings.Split(expectedLines, "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
assert.Contains(t, body, line)
|
||||
}
|
||||
}
|
||||
|
||||
const appReconcileMetrics = `argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="0.25"} 0
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="0.5"} 0
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="1"} 0
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="2"} 0
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="4"} 0
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="8"} 1
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="16"} 1
|
||||
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="+Inf"} 1
|
||||
argocd_app_reconcile_sum{name="my-app",namespace="argocd",project="important-project"} 5
|
||||
argocd_app_reconcile_count{name="my-app",namespace="argocd",project="important-project"} 1
|
||||
`
|
||||
|
||||
func TestReconcileMetrics(t *testing.T) {
|
||||
cancel, appLister := newFakeLister()
|
||||
defer cancel()
|
||||
metricsServ := NewMetricsServer("localhost:8082", appLister, 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
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/yudai/gojsondiff"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
@@ -30,6 +31,7 @@ import (
|
||||
"github.com/argoproj/argo-cd/util/resource"
|
||||
"github.com/argoproj/argo-cd/util/resource/ignore"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
)
|
||||
|
||||
type managedResource struct {
|
||||
@@ -58,12 +60,11 @@ type ResourceInfoProvider interface {
|
||||
|
||||
// AppStateManager defines methods which allow to compare application spec and actual application state.
|
||||
type AppStateManager interface {
|
||||
CompareAppState(app *v1alpha1.Application, revision string, source v1alpha1.ApplicationSource, noCache bool, localObjects []string) *comparisonResult
|
||||
CompareAppState(app *v1alpha1.Application, project *appv1.AppProject, revision string, source v1alpha1.ApplicationSource, noCache bool, localObjects []string) *comparisonResult
|
||||
SyncAppState(app *v1alpha1.Application, state *v1alpha1.OperationState)
|
||||
}
|
||||
|
||||
type comparisonResult struct {
|
||||
reconciledAt metav1.Time
|
||||
syncStatus *v1alpha1.SyncStatus
|
||||
healthStatus *v1alpha1.HealthStatus
|
||||
resources []v1alpha1.ResourceStatus
|
||||
@@ -71,6 +72,8 @@ type comparisonResult struct {
|
||||
hooks []*unstructured.Unstructured
|
||||
diffNormalizer diff.Normalizer
|
||||
appSourceType v1alpha1.ApplicationSourceType
|
||||
// timings maps phases of comparison to the duration it took to complete (for statistical purposes)
|
||||
timings map[string]time.Duration
|
||||
}
|
||||
|
||||
func (cr *comparisonResult) targetObjs() []*unstructured.Unstructured {
|
||||
@@ -97,14 +100,17 @@ type appStateManager struct {
|
||||
}
|
||||
|
||||
func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1.ApplicationSource, appLabelKey, revision string, noCache bool) ([]*unstructured.Unstructured, []*unstructured.Unstructured, *apiclient.ManifestResponse, error) {
|
||||
ts := stats.NewTimingStats()
|
||||
helmRepos, err := m.db.ListHelmRepositories(context.Background())
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
ts.AddCheckpoint("helm_ms")
|
||||
repo, err := m.db.GetRepository(context.Background(), source.RepoURL)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
ts.AddCheckpoint("repo_ms")
|
||||
conn, repoClient, err := m.repoClientset.NewRepoServerClient()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
@@ -119,7 +125,7 @@ func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
ts.AddCheckpoint("plugins_ms")
|
||||
tools := make([]*appv1.ConfigManagementPlugin, len(plugins))
|
||||
for i := range plugins {
|
||||
tools[i] = &plugins[i]
|
||||
@@ -129,14 +135,12 @@ func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
cluster, err := m.db.GetCluster(context.Background(), app.Spec.Destination.Server)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
cluster.ServerVersion, err = m.kubectl.GetServerVersion(cluster.RESTConfig())
|
||||
ts.AddCheckpoint("build_options_ms")
|
||||
serverVersion, apiGroups, err := m.liveStateCache.GetVersionsInfo(app.Spec.Destination.Server)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
ts.AddCheckpoint("version_ms")
|
||||
manifestInfo, err := repoClient.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: repo,
|
||||
Repos: helmRepos,
|
||||
@@ -150,15 +154,24 @@ func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1
|
||||
KustomizeOptions: &appv1.KustomizeOptions{
|
||||
BuildOptions: buildOptions,
|
||||
},
|
||||
KubeVersion: cluster.ServerVersion,
|
||||
KubeVersion: serverVersion,
|
||||
ApiVersions: argo.APIGroupsToVersions(apiGroups),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
ts.AddCheckpoint("manifests_ms")
|
||||
targetObjs, hooks, err := unmarshalManifests(manifestInfo.Manifests)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
ts.AddCheckpoint("unmarshal_ms")
|
||||
logCtx := log.WithField("application", app.Name)
|
||||
for k, v := range ts.Timings() {
|
||||
logCtx = logCtx.WithField(k, v.Milliseconds())
|
||||
}
|
||||
logCtx = logCtx.WithField("time_ms", time.Since(ts.StartTime).Milliseconds())
|
||||
logCtx.Info("getRepoObjs stats")
|
||||
return targetObjs, hooks, manifestInfo, nil
|
||||
}
|
||||
|
||||
@@ -170,7 +183,7 @@ func unmarshalManifests(manifests []string) ([]*unstructured.Unstructured, []*un
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if ignore.Ignore(obj) {
|
||||
if obj == nil || ignore.Ignore(obj) {
|
||||
continue
|
||||
}
|
||||
if hookutil.IsHook(obj) {
|
||||
@@ -208,9 +221,11 @@ func DeduplicateTargetObjects(
|
||||
result := make([]*unstructured.Unstructured, 0)
|
||||
for key, targets := range targetByKey {
|
||||
if len(targets) > 1 {
|
||||
now := metav1.Now()
|
||||
conditions = append(conditions, appv1.ApplicationCondition{
|
||||
Type: appv1.ApplicationConditionRepeatedResourceWarning,
|
||||
Message: fmt.Sprintf("Resource %s appeared %d times among application resources.", key.String(), len(targets)),
|
||||
Type: appv1.ApplicationConditionRepeatedResourceWarning,
|
||||
Message: fmt.Sprintf("Resource %s appeared %d times among application resources.", key.String(), len(targets)),
|
||||
LastTransitionTime: &now,
|
||||
})
|
||||
}
|
||||
result = append(result, targets[len(targets)-1])
|
||||
@@ -255,33 +270,37 @@ func dedupLiveResources(targetObjs []*unstructured.Unstructured, liveObjsByKey m
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// CompareAppState compares application git state to the live app state, using the specified
|
||||
// revision and supplied source. If revision or overrides are empty, then compares against
|
||||
// revision and overrides in the app spec.
|
||||
func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision string, source v1alpha1.ApplicationSource, noCache bool, localManifests []string) *comparisonResult {
|
||||
reconciledAt := metav1.Now()
|
||||
appLabelKey, resourceOverrides, diffNormalizer, err := m.getComparisonSettings(app)
|
||||
func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *appv1.AppProject, revision string, source v1alpha1.ApplicationSource, noCache bool, localManifests []string) *comparisonResult {
|
||||
ts := stats.NewTimingStats()
|
||||
appLabelKey, resourceOverrides, diffNormalizer, resFilter, err := m.getComparisonSettings(app)
|
||||
ts.AddCheckpoint("settings_ms")
|
||||
|
||||
// return unknown comparison result if basic comparison settings cannot be loaded
|
||||
if err != nil {
|
||||
return &comparisonResult{
|
||||
reconciledAt: reconciledAt,
|
||||
syncStatus: &v1alpha1.SyncStatus{
|
||||
ComparedTo: appv1.ComparedTo{Source: source, Destination: app.Spec.Destination},
|
||||
Status: appv1.SyncStatusCodeUnknown,
|
||||
@@ -300,63 +319,67 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
var targetObjs []*unstructured.Unstructured
|
||||
var hooks []*unstructured.Unstructured
|
||||
var manifestInfo *apiclient.ManifestResponse
|
||||
now := metav1.Now()
|
||||
|
||||
if len(localManifests) == 0 {
|
||||
targetObjs, hooks, manifestInfo, err = m.getRepoObjs(app, source, appLabelKey, revision, noCache)
|
||||
if err != nil {
|
||||
targetObjs = make([]*unstructured.Unstructured, 0)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
|
||||
failedToLoadObjs = true
|
||||
}
|
||||
} else {
|
||||
targetObjs, hooks, err = unmarshalManifests(localManifests)
|
||||
if err != nil {
|
||||
targetObjs = make([]*unstructured.Unstructured, 0)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
|
||||
failedToLoadObjs = true
|
||||
}
|
||||
manifestInfo = nil
|
||||
}
|
||||
ts.AddCheckpoint("git_ms")
|
||||
|
||||
targetObjs, dedupConditions, err := DeduplicateTargetObjects(app.Spec.Destination.Server, app.Spec.Destination.Namespace, targetObjs, m.liveStateCache)
|
||||
if err != nil {
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
|
||||
}
|
||||
conditions = append(conditions, dedupConditions...)
|
||||
for i := len(targetObjs) - 1; i >= 0; i-- {
|
||||
targetObj := targetObjs[i]
|
||||
gvk := targetObj.GroupVersionKind()
|
||||
if resFilter.IsExcludedResource(gvk.Group, gvk.Kind, app.Spec.Destination.Server) {
|
||||
targetObjs = append(targetObjs[:i], targetObjs[i+1:]...)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{
|
||||
Type: v1alpha1.ApplicationConditionExcludedResourceWarning,
|
||||
Message: fmt.Sprintf("Resource %s/%s %s is excluded in the settings", gvk.Group, gvk.Kind, targetObj.GetName()),
|
||||
LastTransitionTime: &now,
|
||||
})
|
||||
}
|
||||
}
|
||||
ts.AddCheckpoint("dedup_ms")
|
||||
|
||||
resFilter, err := m.settingsMgr.GetResourcesFilter()
|
||||
liveObjByKey, err := m.liveStateCache.GetManagedLiveObjs(app, targetObjs)
|
||||
if err != nil {
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
} else {
|
||||
for i := len(targetObjs) - 1; i >= 0; i-- {
|
||||
targetObj := targetObjs[i]
|
||||
gvk := targetObj.GroupVersionKind()
|
||||
if resFilter.IsExcludedResource(gvk.Group, gvk.Kind, app.Spec.Destination.Server) {
|
||||
targetObjs = append(targetObjs[:i], targetObjs[i+1:]...)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{
|
||||
Type: v1alpha1.ApplicationConditionExcludedResourceWarning,
|
||||
Message: fmt.Sprintf("Resource %s/%s %s is excluded in the settings", gvk.Group, gvk.Kind, targetObj.GetName()),
|
||||
})
|
||||
}
|
||||
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)
|
||||
// filter out all resources which are not permitted in the application project
|
||||
for k, v := range liveObjByKey {
|
||||
if !project.IsLiveResourcePermitted(v, app.Spec.Destination.Server) {
|
||||
delete(liveObjByKey, k)
|
||||
}
|
||||
}
|
||||
|
||||
logCtx.Debugf("Generated config manifests")
|
||||
liveObjByKey, err := m.liveStateCache.GetManagedLiveObjs(app, targetObjs)
|
||||
dedupLiveResources(targetObjs, liveObjByKey)
|
||||
if err != nil {
|
||||
liveObjByKey = make(map[kubeutil.ResourceKey]*unstructured.Unstructured)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
failedToLoadObjs = true
|
||||
}
|
||||
logCtx.Debugf("Retrieved lived manifests")
|
||||
for _, liveObj := range liveObjByKey {
|
||||
if liveObj != nil {
|
||||
appInstanceName := kubeutil.GetAppInstanceLabel(liveObj, appLabelKey)
|
||||
if appInstanceName != "" && appInstanceName != app.Name {
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{
|
||||
Type: v1alpha1.ApplicationConditionSharedResourceWarning,
|
||||
Message: fmt.Sprintf("%s/%s is part of a different application: %s", liveObj.GetKind(), liveObj.GetName(), appInstanceName),
|
||||
Type: v1alpha1.ApplicationConditionSharedResourceWarning,
|
||||
Message: fmt.Sprintf("%s/%s is part of a different application: %s", liveObj.GetKind(), liveObj.GetName(), appInstanceName),
|
||||
LastTransitionTime: &now,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -377,7 +400,8 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
managedLiveObj[i] = nil
|
||||
}
|
||||
}
|
||||
logCtx.Debugf("built managed objects list")
|
||||
ts.AddCheckpoint("live_ms")
|
||||
|
||||
// Everything remaining in liveObjByKey are "extra" resources that aren't tracked in git.
|
||||
// The following adds all the extras to the managedLiveObj list and backfills the targetObj
|
||||
// list with nils, so that the lists are of equal lengths for comparison purposes.
|
||||
@@ -391,8 +415,9 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
if err != nil {
|
||||
diffResults = &diff.DiffResultList{}
|
||||
failedToLoadObjs = true
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
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))
|
||||
@@ -418,7 +443,17 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
RequiresPruning: targetObj == nil && liveObj != nil,
|
||||
}
|
||||
|
||||
diffResult := diffResults.Diffs[i]
|
||||
var diffResult diff.DiffResult
|
||||
if i < len(diffResults.Diffs) {
|
||||
diffResult = diffResults.Diffs[i]
|
||||
} else {
|
||||
diffResult = diff.DiffResult{
|
||||
Diff: gojsondiff.New().CompareObjects(map[string]interface{}{}, map[string]interface{}{}),
|
||||
Modified: false,
|
||||
NormalizedLive: []byte("{}"),
|
||||
PredictedLive: []byte("{}"),
|
||||
}
|
||||
}
|
||||
if resState.Hook || ignore.Ignore(obj) {
|
||||
// For resource hooks, don't store sync status, and do not affect overall sync status
|
||||
} else if diffResult.Modified || targetObj == nil || liveObj == nil {
|
||||
@@ -435,6 +470,16 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
} else {
|
||||
resState.Status = v1alpha1.SyncStatusCodeSynced
|
||||
}
|
||||
// set unknown status to all resource that are not permitted in the app project
|
||||
isNamespaced, err := m.liveStateCache.IsNamespaced(app.Spec.Destination.Server, gvk.GroupKind())
|
||||
if !project.IsGroupKindPermitted(gvk.GroupKind(), isNamespaced && err == nil) {
|
||||
resState.Status = v1alpha1.SyncStatusCodeUnknown
|
||||
}
|
||||
|
||||
// we can't say anything about the status if we were unable to get the target objects
|
||||
if failedToLoadObjs {
|
||||
resState.Status = v1alpha1.SyncStatusCodeUnknown
|
||||
}
|
||||
managedResources[i] = managedResource{
|
||||
Name: resState.Name,
|
||||
Namespace: resState.Namespace,
|
||||
@@ -462,17 +507,17 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
if manifestInfo != nil {
|
||||
syncStatus.Revision = manifestInfo.Revision
|
||||
}
|
||||
ts.AddCheckpoint("sync_ms")
|
||||
|
||||
healthStatus, err := health.SetApplicationHealth(resourceSummaries, GetLiveObjs(managedResources), resourceOverrides, func(obj *unstructured.Unstructured) bool {
|
||||
return !isSelfReferencedApp(app, kubeutil.GetObjectRef(obj))
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
conditions = append(conditions, appv1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
|
||||
}
|
||||
|
||||
compRes := comparisonResult{
|
||||
reconciledAt: reconciledAt,
|
||||
syncStatus: &syncStatus,
|
||||
healthStatus: healthStatus,
|
||||
resources: resourceSummaries,
|
||||
@@ -489,6 +534,8 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
appv1.ApplicationConditionRepeatedResourceWarning: true,
|
||||
appv1.ApplicationConditionExcludedResourceWarning: true,
|
||||
})
|
||||
ts.AddCheckpoint("health_ms")
|
||||
compRes.timings = ts.Timings()
|
||||
return &compRes
|
||||
}
|
||||
|
||||
@@ -497,20 +544,18 @@ func (m *appStateManager) persistRevisionHistory(app *v1alpha1.Application, revi
|
||||
if len(app.Status.History) > 0 {
|
||||
nextID = app.Status.History[len(app.Status.History)-1].ID + 1
|
||||
}
|
||||
history := append(app.Status.History, v1alpha1.RevisionHistory{
|
||||
app.Status.History = append(app.Status.History, v1alpha1.RevisionHistory{
|
||||
Revision: revision,
|
||||
DeployedAt: metav1.NewTime(time.Now().UTC()),
|
||||
ID: nextID,
|
||||
Source: source,
|
||||
})
|
||||
|
||||
if len(history) > common.RevisionHistoryLimit {
|
||||
history = history[1 : common.RevisionHistoryLimit+1]
|
||||
}
|
||||
app.Status.History = app.Status.History.Trunc(app.Spec.GetRevisionHistoryLimit())
|
||||
|
||||
patch, err := json.Marshal(map[string]map[string][]v1alpha1.RevisionHistory{
|
||||
"status": {
|
||||
"history": history,
|
||||
"history": app.Status.History,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestCompareAppStateEmpty(t *testing.T) {
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false, nil)
|
||||
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)
|
||||
@@ -45,7 +45,7 @@ func TestCompareAppStateMissing(t *testing.T) {
|
||||
data := fakeData{
|
||||
apps: []runtime.Object{app},
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{string(test.PodManifest)},
|
||||
Manifests: []string{test.PodManifest},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
@@ -53,7 +53,7 @@ func TestCompareAppStateMissing(t *testing.T) {
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false, nil)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeOutOfSync, compRes.syncStatus.Status)
|
||||
@@ -80,7 +80,7 @@ func TestCompareAppStateExtra(t *testing.T) {
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false, nil)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeOutOfSync, compRes.syncStatus.Status)
|
||||
assert.Equal(t, 1, len(compRes.resources))
|
||||
@@ -106,7 +106,7 @@ func TestCompareAppStateHook(t *testing.T) {
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false, nil)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Equal(t, 0, len(compRes.resources))
|
||||
@@ -132,7 +132,7 @@ func TestCompareAppStateCompareOptionIgnoreExtraneous(t *testing.T) {
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false, nil)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
|
||||
assert.NotNil(t, compRes)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
@@ -160,7 +160,7 @@ func TestCompareAppStateExtraHook(t *testing.T) {
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false, nil)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
|
||||
assert.NotNil(t, compRes)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
@@ -197,13 +197,13 @@ func TestCompareAppStateDuplicatedNamespacedResources(t *testing.T) {
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false, nil)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
|
||||
assert.NotNil(t, compRes)
|
||||
assert.Contains(t, app.Status.Conditions, argoappv1.ApplicationCondition{
|
||||
Message: "Resource /Pod/fake-dest-ns/my-pod appeared 2 times among application resources.",
|
||||
Type: argoappv1.ApplicationConditionRepeatedResourceWarning,
|
||||
})
|
||||
assert.Equal(t, 1, len(app.Status.Conditions))
|
||||
assert.NotNil(t, app.Status.Conditions[0].LastTransitionTime)
|
||||
assert.Equal(t, argoappv1.ApplicationConditionRepeatedResourceWarning, app.Status.Conditions[0].Type)
|
||||
assert.Equal(t, "Resource /Pod/fake-dest-ns/my-pod appeared 2 times among application resources.", app.Status.Conditions[0].Message)
|
||||
assert.Equal(t, 2, len(compRes.resources))
|
||||
}
|
||||
|
||||
@@ -248,7 +248,7 @@ func TestSetHealth(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false, nil)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
|
||||
assert.Equal(t, compRes.healthStatus.Status, argoappv1.HealthStatusHealthy)
|
||||
}
|
||||
@@ -280,7 +280,7 @@ func TestSetHealthSelfReferencedApp(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false, nil)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
|
||||
assert.Equal(t, compRes.healthStatus.Status, argoappv1.HealthStatusHealthy)
|
||||
}
|
||||
@@ -350,11 +350,10 @@ func TestReturnUnknownComparisonStateOnSettingLoadError(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false, nil)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
|
||||
assert.Equal(t, argoappv1.HealthStatusUnknown, compRes.healthStatus.Status)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeUnknown, compRes.syncStatus.Status)
|
||||
assert.NotNil(t, compRes.reconciledAt)
|
||||
}
|
||||
|
||||
func TestSetManagedResourcesKnownOrphanedResourceExceptions(t *testing.T) {
|
||||
@@ -392,3 +391,50 @@ func Test_comparisonResult_obs(t *testing.T) {
|
||||
assert.Len(t, (&comparisonResult{managedResources: []managedResource{{Target: test.NewPod()}}}).targetObjs(), 1)
|
||||
assert.Len(t, (&comparisonResult{hooks: []*unstructured.Unstructured{{}}}).targetObjs(), 1)
|
||||
}
|
||||
|
||||
func Test_appStateManager_persistRevisionHistory(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{
|
||||
apps: []runtime.Object{app},
|
||||
})
|
||||
manager := ctrl.appStateManager.(*appStateManager)
|
||||
setRevisionHistoryLimit := func(value int) {
|
||||
i := int64(value)
|
||||
app.Spec.RevisionHistoryLimit = &i
|
||||
}
|
||||
addHistory := func() {
|
||||
err := manager.persistRevisionHistory(app, "my-revision", argoappv1.ApplicationSource{})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 1)
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 2)
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 3)
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 4)
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 5)
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 6)
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 7)
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 8)
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 9)
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 10)
|
||||
// default limit is 10
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 10)
|
||||
// increase limit
|
||||
setRevisionHistoryLimit(11)
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 11)
|
||||
// decrease limit
|
||||
setRevisionHistoryLimit(9)
|
||||
addHistory()
|
||||
assert.Len(t, app.Status.History, 9)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
apierr "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/dynamic"
|
||||
@@ -46,6 +47,7 @@ type syncContext struct {
|
||||
proj *v1alpha1.AppProject
|
||||
compareResult *comparisonResult
|
||||
config *rest.Config
|
||||
rawConfig *rest.Config
|
||||
dynamicIf dynamic.Interface
|
||||
disco discovery.DiscoveryInterface
|
||||
extensionsclientset *clientset.Clientset
|
||||
@@ -106,7 +108,14 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
|
||||
revision = syncOp.Revision
|
||||
}
|
||||
|
||||
compareResult := m.CompareAppState(app, revision, source, false, syncOp.Manifests)
|
||||
proj, err := argo.GetAppProject(&app.Spec, listersv1alpha1.NewAppProjectLister(m.projInformer.GetIndexer()), m.namespace)
|
||||
if err != nil {
|
||||
state.Phase = v1alpha1.OperationError
|
||||
state.Message = fmt.Sprintf("Failed to load application project: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
compareResult := m.CompareAppState(app, proj, revision, source, false, syncOp.Manifests)
|
||||
|
||||
// If there are any comparison or spec errors error conditions do not perform the operation
|
||||
if errConditions := app.Status.GetConditions(map[v1alpha1.ApplicationConditionType]bool{
|
||||
@@ -150,13 +159,6 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
|
||||
return
|
||||
}
|
||||
|
||||
proj, err := argo.GetAppProject(&app.Spec, listersv1alpha1.NewAppProjectLister(m.projInformer.GetIndexer()), m.namespace)
|
||||
if err != nil {
|
||||
state.Phase = v1alpha1.OperationError
|
||||
state.Message = fmt.Sprintf("Failed to load application project: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
resourceOverrides, err := m.settingsMgr.GetResourceOverrides()
|
||||
if err != nil {
|
||||
state.Phase = v1alpha1.OperationError
|
||||
@@ -172,6 +174,7 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
|
||||
proj: proj,
|
||||
compareResult: compareResult,
|
||||
config: restConfig,
|
||||
rawConfig: clst.RawRestConfig(),
|
||||
dynamicIf: dynamicIf,
|
||||
disco: disco,
|
||||
extensionsclientset: extensionsclientset,
|
||||
@@ -235,16 +238,21 @@ func (sc *syncContext) sync() {
|
||||
}) {
|
||||
if task.isHook() {
|
||||
// update the hook's result
|
||||
operationState, message := getOperationPhase(task.liveObj)
|
||||
sc.setResourceResult(task, "", operationState, message)
|
||||
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))
|
||||
// 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)
|
||||
@@ -416,7 +424,13 @@ func (sc *syncContext) getSyncTasks() (_ syncTasks, successful bool) {
|
||||
// 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()))
|
||||
var syncRevision string
|
||||
if len(sc.syncRes.Revision) >= 8 {
|
||||
syncRevision = sc.syncRes.Revision[0:7]
|
||||
} else {
|
||||
syncRevision = sc.syncRes.Revision
|
||||
}
|
||||
postfix := strings.ToLower(fmt.Sprintf("%s-%s-%d", syncRevision, phase, sc.opState.StartedAt.UTC().Unix()))
|
||||
generateName := obj.GetGenerateName()
|
||||
targetObj.SetName(fmt.Sprintf("%s%s", generateName, postfix))
|
||||
}
|
||||
@@ -480,7 +494,7 @@ func (sc *syncContext) getSyncTasks() (_ syncTasks, successful bool) {
|
||||
successful = false
|
||||
}
|
||||
} else {
|
||||
if !sc.proj.IsResourcePermitted(metav1.GroupKind{Group: task.group(), Kind: task.kind()}, serverRes.Namespaced) {
|
||||
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
|
||||
}
|
||||
@@ -542,9 +556,8 @@ func (sc *syncContext) ensureCRDReady(name string) {
|
||||
}
|
||||
|
||||
// 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)
|
||||
func (sc *syncContext) applyObject(targetObj *unstructured.Unstructured, dryRun, force, validate bool) (v1alpha1.ResultCode, string) {
|
||||
message, err := sc.kubectl.ApplyResource(sc.rawConfig, targetObj, targetObj.GetNamespace(), dryRun, force, validate)
|
||||
if err != nil {
|
||||
return v1alpha1.ResultCodeSyncFailed, err.Error()
|
||||
}
|
||||
@@ -602,10 +615,15 @@ func (sc *syncContext) terminate() {
|
||||
sc.log.Debug("terminating")
|
||||
tasks, _ := sc.getSyncTasks()
|
||||
for _, task := range tasks {
|
||||
if !task.isHook() || !task.completed() {
|
||||
if !task.isHook() || task.liveObj == nil {
|
||||
continue
|
||||
}
|
||||
if isRunnable(task.groupVersionKind()) {
|
||||
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))
|
||||
@@ -613,6 +631,8 @@ func (sc *syncContext) terminate() {
|
||||
} else {
|
||||
sc.setResourceResult(task, "", v1alpha1.OperationSucceeded, fmt.Sprintf("Deleted"))
|
||||
}
|
||||
} else {
|
||||
sc.setResourceResult(task, "", phase, msg)
|
||||
}
|
||||
}
|
||||
if terminateSuccessful {
|
||||
@@ -682,12 +702,14 @@ func (sc *syncContext) runTasks(tasks syncTasks, dryRun bool) runState {
|
||||
wg.Add(1)
|
||||
go func(t *syncTask) {
|
||||
defer wg.Done()
|
||||
sc.log.WithFields(log.Fields{"dryRun": dryRun, "task": t}).Debug("pruning")
|
||||
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 || result == v1alpha1.ResultCodeSyncFailed {
|
||||
if !dryRun || sc.syncOp.DryRun || result == v1alpha1.ResultCodeSyncFailed {
|
||||
sc.setResourceResult(t, result, operationPhases[result], message)
|
||||
}
|
||||
}(task)
|
||||
@@ -733,12 +755,15 @@ func (sc *syncContext) runTasks(tasks syncTasks, dryRun bool) runState {
|
||||
createWg.Add(1)
|
||||
go func(t *syncTask) {
|
||||
defer createWg.Done()
|
||||
sc.log.WithFields(log.Fields{"dryRun": dryRun, "task": t}).Debug("applying")
|
||||
result, message := sc.applyObject(t.targetObj, dryRun, sc.syncOp.SyncStrategy.Force())
|
||||
logCtx := sc.log.WithFields(log.Fields{"dryRun": dryRun, "task": t})
|
||||
logCtx.Debug("applying")
|
||||
validate := !(sc.syncOp.SyncOptions.HasOption("Validate=false") || resource.HasAnnotationOption(t.targetObj, common.AnnotationSyncOptions, "Validate=false"))
|
||||
result, message := sc.applyObject(t.targetObj, dryRun, sc.syncOp.SyncStrategy.Force(), validate)
|
||||
if result == v1alpha1.ResultCodeSyncFailed {
|
||||
logCtx.WithField("message", message).Info("apply failed")
|
||||
runState = failed
|
||||
}
|
||||
if !dryRun || result == v1alpha1.ResultCodeSyncFailed {
|
||||
if !dryRun || sc.syncOp.DryRun || result == v1alpha1.ResultCodeSyncFailed {
|
||||
sc.setResourceResult(t, result, operationPhases[result], message)
|
||||
}
|
||||
}(task)
|
||||
|
||||
@@ -3,118 +3,33 @@ package controller
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/argoproj/argo-cd/util/health"
|
||||
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/kubernetes/pkg/apis/batch"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/health"
|
||||
)
|
||||
|
||||
// getOperationPhase returns a hook status from an _live_ unstructured object
|
||||
func getOperationPhase(hook *unstructured.Unstructured) (operation v1alpha1.OperationPhase, message string) {
|
||||
gvk := hook.GroupVersionKind()
|
||||
if isBatchJob(gvk) {
|
||||
return getStatusFromBatchJob(hook)
|
||||
} else if isArgoWorkflow(gvk) {
|
||||
return health.GetStatusFromArgoWorkflow(hook)
|
||||
} else if isPod(gvk) {
|
||||
return getStatusFromPod(hook)
|
||||
} else {
|
||||
return v1alpha1.OperationSucceeded, fmt.Sprintf("%s created", hook.GetName())
|
||||
}
|
||||
}
|
||||
func (sc *syncContext) getOperationPhase(hook *unstructured.Unstructured) (v1alpha1.OperationPhase, string, error) {
|
||||
phase := v1alpha1.OperationSucceeded
|
||||
message := fmt.Sprintf("%s created", hook.GetName())
|
||||
|
||||
// isRunnable returns if the resource object is a runnable type which needs to be terminated
|
||||
func isRunnable(gvk schema.GroupVersionKind) bool {
|
||||
return isBatchJob(gvk) || isArgoWorkflow(gvk) || isPod(gvk)
|
||||
}
|
||||
|
||||
func isBatchJob(gvk schema.GroupVersionKind) bool {
|
||||
return gvk.Group == "batch" && gvk.Kind == "Job"
|
||||
}
|
||||
|
||||
// TODO this is a copy-and-paste of health.getJobHealth(), refactor out?
|
||||
func getStatusFromBatchJob(hook *unstructured.Unstructured) (operation v1alpha1.OperationPhase, message string) {
|
||||
var job batch.Job
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(hook.Object, &job)
|
||||
resHealth, err := health.GetResourceHealth(hook, sc.resourceOverrides)
|
||||
if err != nil {
|
||||
return v1alpha1.OperationError, err.Error()
|
||||
return "", "", err
|
||||
}
|
||||
failed := false
|
||||
var failMsg string
|
||||
complete := false
|
||||
for _, condition := range job.Status.Conditions {
|
||||
switch condition.Type {
|
||||
case batch.JobFailed:
|
||||
failed = true
|
||||
complete = true
|
||||
failMsg = condition.Message
|
||||
case batch.JobComplete:
|
||||
complete = true
|
||||
message = condition.Message
|
||||
if 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
|
||||
}
|
||||
}
|
||||
if !complete {
|
||||
return v1alpha1.OperationRunning, message
|
||||
} else if failed {
|
||||
return v1alpha1.OperationFailed, failMsg
|
||||
} else {
|
||||
return v1alpha1.OperationSucceeded, message
|
||||
}
|
||||
}
|
||||
|
||||
func isArgoWorkflow(gvk schema.GroupVersionKind) bool {
|
||||
return gvk.Group == "argoproj.io" && gvk.Kind == "Workflow"
|
||||
}
|
||||
|
||||
func isPod(gvk schema.GroupVersionKind) bool {
|
||||
return gvk.Group == "" && gvk.Kind == "Pod"
|
||||
}
|
||||
|
||||
// TODO - this is very similar to health.getPodHealth() should we use that instead?
|
||||
func getStatusFromPod(hook *unstructured.Unstructured) (v1alpha1.OperationPhase, string) {
|
||||
var pod apiv1.Pod
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(hook.Object, &pod)
|
||||
if err != nil {
|
||||
return v1alpha1.OperationError, err.Error()
|
||||
}
|
||||
getFailMessage := func(ctr *apiv1.ContainerStatus) string {
|
||||
if ctr.State.Terminated != nil {
|
||||
if ctr.State.Terminated.Message != "" {
|
||||
return ctr.State.Terminated.Message
|
||||
}
|
||||
if ctr.State.Terminated.Reason == "OOMKilled" {
|
||||
return ctr.State.Terminated.Reason
|
||||
}
|
||||
if ctr.State.Terminated.ExitCode != 0 {
|
||||
return fmt.Sprintf("container %q failed with exit code %d", ctr.Name, ctr.State.Terminated.ExitCode)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
switch pod.Status.Phase {
|
||||
case apiv1.PodPending, apiv1.PodRunning:
|
||||
return v1alpha1.OperationRunning, ""
|
||||
case apiv1.PodSucceeded:
|
||||
return v1alpha1.OperationSucceeded, ""
|
||||
case apiv1.PodFailed:
|
||||
if pod.Status.Message != "" {
|
||||
// Pod has a nice error message. Use that.
|
||||
return v1alpha1.OperationFailed, pod.Status.Message
|
||||
}
|
||||
for _, ctr := range append(pod.Status.InitContainerStatuses, pod.Status.ContainerStatuses...) {
|
||||
if msg := getFailMessage(&ctr); msg != "" {
|
||||
return v1alpha1.OperationFailed, msg
|
||||
}
|
||||
}
|
||||
return v1alpha1.OperationFailed, ""
|
||||
case apiv1.PodUnknown:
|
||||
return v1alpha1.OperationError, ""
|
||||
}
|
||||
return v1alpha1.OperationRunning, ""
|
||||
return phase, message, nil
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ func newTestSyncCtx(resources ...*v1.APIResourceList) *syncContext {
|
||||
})
|
||||
sc := syncContext{
|
||||
config: &rest.Config{},
|
||||
rawConfig: &rest.Config{},
|
||||
namespace: test.FakeArgoCDNamespace,
|
||||
server: test.FakeClusterURL,
|
||||
syncRes: &v1alpha1.SyncOperationResult{
|
||||
@@ -365,6 +366,20 @@ func TestSyncOptionValidate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// make sure Validate means we don't validate
|
||||
func TestSyncValidate(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
pod := test.NewPod()
|
||||
pod.SetNamespace(test.FakeArgoCDNamespace)
|
||||
syncCtx.compareResult = &comparisonResult{managedResources: []managedResource{{Target: pod, Live: pod}}}
|
||||
syncCtx.syncOp.SyncOptions = SyncOptions{"Validate=false"}
|
||||
|
||||
syncCtx.sync()
|
||||
|
||||
kubectl := syncCtx.kubectl.(*kubetest.MockKubectlCmd)
|
||||
assert.False(t, kubectl.LastValidate)
|
||||
}
|
||||
|
||||
func TestSelectiveSyncOnly(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
pod1 := test.NewPod()
|
||||
@@ -384,20 +399,40 @@ func TestSelectiveSyncOnly(t *testing.T) {
|
||||
}
|
||||
|
||||
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}}
|
||||
t.Run("Truncated revision", func(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.syncOp.SyncStrategy.Apply = nil
|
||||
pod := test.NewPod()
|
||||
pod.SetName("")
|
||||
pod.SetAnnotations(map[string]string{common.AnnotationKeyHook: "PreSync,PostSync"})
|
||||
syncCtx.compareResult = &comparisonResult{hooks: []*unstructured.Unstructured{pod}}
|
||||
|
||||
tasks, successful := syncCtx.getSyncTasks()
|
||||
tasks, successful := syncCtx.getSyncTasks()
|
||||
|
||||
assert.True(t, successful)
|
||||
assert.Len(t, tasks, 2)
|
||||
assert.Contains(t, tasks[0].name(), "foobarb-presync-")
|
||||
assert.Contains(t, tasks[1].name(), "foobarb-postsync-")
|
||||
assert.Equal(t, "", pod.GetName())
|
||||
})
|
||||
|
||||
t.Run("Short revision", func(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.syncOp.SyncStrategy.Apply = nil
|
||||
pod := test.NewPod()
|
||||
pod.SetName("")
|
||||
pod.SetAnnotations(map[string]string{common.AnnotationKeyHook: "PreSync,PostSync"})
|
||||
syncCtx.compareResult = &comparisonResult{hooks: []*unstructured.Unstructured{pod}}
|
||||
syncCtx.syncRes.Revision = "foobar"
|
||||
tasks, successful := syncCtx.getSyncTasks()
|
||||
|
||||
assert.True(t, successful)
|
||||
assert.Len(t, tasks, 2)
|
||||
assert.Contains(t, tasks[0].name(), "foobar-presync-")
|
||||
assert.Contains(t, tasks[1].name(), "foobar-postsync-")
|
||||
assert.Equal(t, "", pod.GetName())
|
||||
})
|
||||
|
||||
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) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# 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.
|
||||
@@ -21,17 +22,17 @@ Install:
|
||||
* [minikube](https://kubernetes.io/docs/setup/minikube/) or Docker for Desktop
|
||||
|
||||
Brew users can quickly install the lot:
|
||||
|
||||
|
||||
```bash
|
||||
brew install go git-lfs kubectl kubectx dep ksonnet/tap/ks kubernetes-helm kustomize kustomize
|
||||
brew install go git-lfs kubectl kubectx dep ksonnet/tap/ks helm@2 kustomize
|
||||
```
|
||||
|
||||
Check the versions:
|
||||
|
||||
```
|
||||
```bash
|
||||
go version ;# must be v1.12.x
|
||||
helm version ;# must be v2.13.x
|
||||
kustomize version ;# must be v3.10.x
|
||||
kustomize version ;# must be v3.1.x
|
||||
```
|
||||
|
||||
Set up environment variables (e.g. is `~/.bashrc`):
|
||||
@@ -54,8 +55,10 @@ Ensure dependencies are up to date first:
|
||||
|
||||
```shell
|
||||
dep ensure
|
||||
make dev-builder-image
|
||||
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:
|
||||
@@ -84,7 +87,7 @@ kubectl -n argocd scale deployment/argocd-redis --replicas 0
|
||||
Download Yarn dependencies and Compile:
|
||||
|
||||
```bash
|
||||
~/go/src/github.com/argoproj/argo-cd/ui
|
||||
~/go/src/github.com/argoproj/argo-cd/ui
|
||||
yarn install
|
||||
yarn build
|
||||
```
|
||||
@@ -102,7 +105,7 @@ You can now execute `argocd` command against your locally running ArgoCD by appe
|
||||
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
|
||||
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:
|
||||
|
||||
|
||||
BIN
docs/assets/api-management.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/assets/app-ui-information.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/assets/argo.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
docs/assets/connect-repo.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 76 KiB |
BIN
docs/assets/create-app.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 99 KiB |
BIN
docs/assets/destination.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
docs/assets/filter-apps.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
docs/assets/groups-claim.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
docs/assets/groups-scope.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 102 KiB |
BIN
docs/assets/new-app-of-apps.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
docs/assets/new-app.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
docs/assets/saml-1.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
docs/assets/saml-2.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
docs/assets/saml-3.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
docs/assets/saml-4.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
|
Before Width: | Height: | Size: 71 KiB |
BIN
docs/assets/sync-apps.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
56
docs/cli_installation.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Installation
|
||||
|
||||
You can download the latest Argo CD version from [the latest release page of this repository](https://github.com/argoproj/argo-cd/releases/latest), which will include the `argocd` CLI.
|
||||
|
||||
## Linux
|
||||
|
||||
You can view the latest version of Argo CD at the link above or run the following command to grab the version:
|
||||
|
||||
```bash
|
||||
VERSION=$(curl --silent "https://api.github.com/repos/argoproj/argo-cd/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
```
|
||||
|
||||
Replace `VERSION` in the command below with the version of Argo CD you would like to download:
|
||||
|
||||
```bash
|
||||
curl -sSL -o /usr/local/bin/argocd https://github.com/argoproj/argo-cd/releases/download/$VERSION/argocd-linux-amd64
|
||||
```
|
||||
|
||||
Make the `argocd` CLI executable:
|
||||
|
||||
```bash
|
||||
chmod +x /usr/local/bin/argocd
|
||||
```
|
||||
|
||||
You should now be able to run `argocd` commands.
|
||||
|
||||
## Mac
|
||||
|
||||
### Homebrew
|
||||
|
||||
```bash
|
||||
brew tap argoproj/tap
|
||||
brew install argoproj/tap/argocd
|
||||
```
|
||||
|
||||
### Download With Curl
|
||||
|
||||
You can view the latest version of Argo CD at the link above or run the following command to grab the version:
|
||||
|
||||
```bash
|
||||
VERSION=$(curl --silent "https://api.github.com/repos/argoproj/argo-cd/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
```
|
||||
|
||||
Replace `VERSION` in the command below with the version of Argo CD you would like to download:
|
||||
|
||||
```bash
|
||||
curl -sSL -o /usr/local/bin/argocd https://github.com/argoproj/argo-cd/releases/download/$VERSION/argocd-darwin-amd64
|
||||
```
|
||||
|
||||
Make the `argocd` CLI executable:
|
||||
|
||||
```bash
|
||||
chmod +x /usr/local/bin/argocd
|
||||
```
|
||||
|
||||
After finishing either of the instructions above, you should now be able to run `argocd` commands.
|
||||
@@ -4,13 +4,13 @@ Let's assume you're familiar with core Git, Docker, Kubernetes, Continuous Deliv
|
||||
|
||||
* **Application** A group of Kubernetes resources as defined by a manifest. This is a Custom Resource Definition (CRD).
|
||||
* **Application source type** Which **Tool** is used to build the application.
|
||||
* **Target state** The desired state of an application, as represented by files in a Git repository.
|
||||
* **Target state** The desired state of an application, as represented by files in a Git repository.
|
||||
* **Live state** The live state of that application. What pods etc are deployed.
|
||||
* **Sync status** Whether or not the live state matches the target state. Is the deployed application the same as Git says it should be?
|
||||
* **Sync** The process of making an application move to its target state. E.g. by applying changes to a Kubernetes cluster.
|
||||
* **Sync status** Whether or not the live state matches the target state. Is the deployed application the same as Git says it should be?
|
||||
* **Sync** The process of making an application move to its target state. E.g. by applying changes to a Kubernetes cluster.
|
||||
* **Sync operation status** Whether or not a sync succeeded.
|
||||
* **Refresh** Compare the latest code in Git with the live state. Figure out what is different.
|
||||
* **Health** The health the application, is it running correctly? Can it serve requests?
|
||||
* **Health** The health of the application, is it running correctly? Can it serve requests?
|
||||
* **Tool** A tool to create manifests from a directory of files. E.g. Kustomize or Ksonnet. See **Application Source Type**.
|
||||
* **Configuration management tool** See **Tool**.
|
||||
* **Configuration management plugin** A custom tool.
|
||||
|
||||
@@ -20,7 +20,7 @@ $ curl $ARGOCD_SERVER/api/v1/applications --cookie "argocd.token=$ARGOCD_TOKEN"
|
||||
{"metadata":{"selfLink":"/apis/argoproj.io/v1alpha1/namespaces/argocd/applications","resourceVersion":"37755"},"items":...}
|
||||
```
|
||||
|
||||
> >v1.3
|
||||
> v1.3
|
||||
|
||||
Then pass using the HTTP `Authorization` header, prefixing with `Bearer `:
|
||||
|
||||
@@ -28,5 +28,4 @@ Then pass using the HTTP `Authorization` header, prefixing with `Bearer `:
|
||||
$ curl $ARGOCD_SERVER/api/v1/applications -H "Authorization: Bearer $ARGOCD_TOKEN"
|
||||
{"metadata":{"selfLink":"/apis/argoproj.io/v1alpha1/namespaces/argocd/applications","resourceVersion":"37755"},"items":...}
|
||||
```
|
||||
|
||||
You sh
|
||||
|
||||
@@ -38,4 +38,12 @@ make builder-image IMAGE_NAMESPACE=argoproj IMAGE_TAG=v1.0.0
|
||||
|
||||
## Public CD
|
||||
|
||||
[https://cd.apps.argoproj.io/](https://cd.apps.argoproj.io/)
|
||||
Every commit to master is built and published to `docker.pkg.github.com/argoproj/argo-cd/argocd:<version>-<short-sha>`. The list of images is available at
|
||||
https://github.com/argoproj/argo-cd/packages.
|
||||
|
||||
!!! note
|
||||
Github docker registry [requires](https://github.community/t5/GitHub-Actions/docker-pull-from-public-GitHub-Package-Registry-fail-with-quot/m-p/32888#M1294) authentication to read
|
||||
even publicly available packages. Follow the steps from Kubernetes [documentation](https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry)
|
||||
to configure image pull secret if you want to use `docker.pkg.github.com/argoproj/argo-cd/argocd` image.
|
||||
|
||||
The image is automatically deployed to the dev Argo CD instance: [https://cd.apps.argoproj.io/](https://cd.apps.argoproj.io/)
|
||||
|
||||
@@ -10,16 +10,16 @@ Export the upstream repository and branch name, e.g.:
|
||||
|
||||
```bash
|
||||
REPO=upstream ;# or origin
|
||||
BRANCH=release-1.0
|
||||
BRANCH=release-1.3
|
||||
```
|
||||
|
||||
Set the `VERSION` environment variable:
|
||||
|
||||
```bash
|
||||
# release candidate
|
||||
VERSION=v1.0.0-rc1
|
||||
VERSION=v1.3.0-rc1
|
||||
# GA release
|
||||
VERSION=v1.0.2
|
||||
VERSION=v1.3.1
|
||||
```
|
||||
|
||||
Update `VERSION` and manifests with new version:
|
||||
@@ -27,41 +27,39 @@ Update `VERSION` and manifests with new version:
|
||||
```bash
|
||||
git checkout $BRANCH
|
||||
echo ${VERSION:1} > VERSION
|
||||
make dev-tools-image
|
||||
make manifests IMAGE_TAG=$VERSION
|
||||
git commit -am "Update manifests to $VERSION"
|
||||
git push $REPO $BRANCH
|
||||
git tag $VERSION
|
||||
```
|
||||
|
||||
Tag, build, and push release to Docker Hub
|
||||
Build, and push release to Docker Hub
|
||||
|
||||
```bash
|
||||
git tag $VERSION
|
||||
git clean -fd
|
||||
make release IMAGE_NAMESPACE=argoproj IMAGE_TAG=$VERSION DOCKER_PUSH=true
|
||||
git push $REPO $BRANCH
|
||||
git push $REPO $VERSION
|
||||
```
|
||||
|
||||
If GA, update `stable` tag:
|
||||
|
||||
```bash
|
||||
git tag stable --force && git push $REPO stable --force
|
||||
```
|
||||
|
||||
Update [Github releases](https://github.com/argoproj/argo-cd/releases) with:
|
||||
|
||||
* Getting started (copy from previous release)
|
||||
* Changelog
|
||||
* Binaries (e.g. `dist/argocd-darwin-amd64`).
|
||||
|
||||
|
||||
If GA, update `stable` tag:
|
||||
|
||||
```bash
|
||||
git tag stable --force && git push $REPO stable --force
|
||||
```
|
||||
|
||||
If GA, update Brew formula:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/argoproj/homebrew-tap
|
||||
git clone git@github.com:argoproj/homebrew-tap.git
|
||||
cd homebrew-tap
|
||||
git checkout master
|
||||
git pull
|
||||
./update.sh ~/go/src/github.com/argoproj/argo-cd/dist/argocd-darwin-amd64
|
||||
./update.sh argocd $VERSION
|
||||
git commit -am "Update argocd to $VERSION"
|
||||
git push
|
||||
```
|
||||
|
||||
@@ -7,19 +7,19 @@ The web site is build using `mkdocs` and `mkdocs-material`.
|
||||
To test:
|
||||
|
||||
```bash
|
||||
mkdocs serve
|
||||
make serve-docs
|
||||
```
|
||||
|
||||
Check for broken external links:
|
||||
|
||||
```bash
|
||||
find docs -name '*.md' -exec grep -l http {} + | xargs awesome_bot -t 3 --allow-dupe --allow-redirect -w argocd.example.com:443,argocd.example.com,kubernetes.default.svc:443,kubernetes.default.svc,mycluster.com,https://github.com/argoproj/my-private-repository,192.168.0.20,storage.googleapis.com,localhost:8080,localhost:6443,your-kubernetes-cluster-addr,10.97.164.88 --skip-save-results --
|
||||
make lint-docs
|
||||
```
|
||||
|
||||
## Deploying
|
||||
|
||||
```bash
|
||||
mkdocs gh-deploy
|
||||
make publish-docs
|
||||
```
|
||||
|
||||
## Analytics
|
||||
|
||||
@@ -47,6 +47,10 @@ kubectl -n argocd patch secret argocd-secret \
|
||||
|
||||
Another option is to delete both the `admin.password` and `admin.passwordMtime` keys and restart argocd-server. This will set the password back to the pod name as per [the getting started guide](getting_started.md).
|
||||
|
||||
## How to disable admin user?
|
||||
|
||||
Add `admin.enabled: "false"` to the `argocd-cm` ConfigMap (see [user management](operator-manual/user-management/index.md)).
|
||||
|
||||
## Argo CD cannot deploy Helm Chart based applications without internet access, how can I solve it?
|
||||
|
||||
Argo CD might fail to generate Helm chart manifests if the chart has dependencies located in external repositories. To solve the problem you need to make sure that `requirements.yaml`
|
||||
@@ -129,10 +133,10 @@ See [#2165](https://github.com/argoproj/argo-cd/issues/2165).
|
||||
|
||||
## Why Am I Getting `rpc error: code = Unavailable desc = transport is closing` When Using The CLI?
|
||||
|
||||
Maybe you're behind a proxy that does not support HTTP 2? Try the `--grcp-web` flag.:
|
||||
Maybe you're behind a proxy that does not support HTTP 2? Try the `--grpc-web` flag.:
|
||||
|
||||
```bash
|
||||
argocd ... --grcp-web
|
||||
argocd ... --grpc-web
|
||||
```
|
||||
|
||||
## Why Am I Getting `x509: certificate signed by unknown authority` When Using The CLI?
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Getting Started
|
||||
|
||||
!!! tip
|
||||
This guide assumes you have a grounding in the tools that Argo CD is based on. Please read the [understanding the basics](understand_the_basics.md).
|
||||
This guide assumes you have a grounding in the tools that Argo CD is based on. Please read [understanding the basics](understand_the_basics.md) to learn about these tools.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -23,9 +23,16 @@ On GKE, you will need grant your account the ability to create new cluster roles
|
||||
kubectl create clusterrolebinding YOURNAME-cluster-admin-binding --clusterrole=cluster-admin --user=YOUREMAIL@gmail.com
|
||||
```
|
||||
|
||||
!!! note
|
||||
If you are not interested in UI, SSO, multi-cluster management and just want to pull changes into the cluster then you can disable
|
||||
authentication using `--disable-auth` flag and access Argo CD via CLI using `--port-forward` or `--port-forward-namespace` flags
|
||||
and proceed to step [#6](#6-create-an-application-from-a-git-repository):
|
||||
|
||||
`kubectl patch deploy argocd-server -n argocd -p '[{"op": "add", "path": "/spec/template/spec/containers/0/command/-", "value": "--disable-auth"}]' --type json`
|
||||
|
||||
## 2. Download Argo CD CLI
|
||||
|
||||
Download the latest Argo CD version from [https://github.com/argoproj/argo-cd/releases/latest](https://github.com/argoproj/argo-cd/releases/latest).
|
||||
Download the latest Argo CD version from [https://github.com/argoproj/argo-cd/releases/latest](https://github.com/argoproj/argo-cd/releases/latest). More detailed installation instructions can be found via the [CLI installation documentation](cli_installation.md).
|
||||
|
||||
Also available in Mac Homebrew:
|
||||
|
||||
@@ -34,7 +41,6 @@ brew tap argoproj/tap
|
||||
brew install argoproj/tap/argocd
|
||||
```
|
||||
|
||||
|
||||
## 3. Access The Argo CD API Server
|
||||
|
||||
By default, the Argo CD API server is not exposed with an external IP. To access the API server,
|
||||
@@ -62,14 +68,14 @@ The API server can then be accessed using the localhost:8080
|
||||
|
||||
## 4. Login Using The CLI
|
||||
|
||||
Login as the `admin` user. The initial password is autogenerated to be the pod name of the
|
||||
The initial password is autogenerated to be the pod name of the
|
||||
Argo CD API server. This can be retrieved with the command:
|
||||
|
||||
```bash
|
||||
kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-server -o name | cut -d'/' -f 2
|
||||
```
|
||||
|
||||
Using the above password, login to Argo CD's IP or hostname:
|
||||
Using the username `admin` and the password from above, login to Argo CD's IP or hostname:
|
||||
|
||||
```bash
|
||||
argocd login <ARGOCD_SERVER>
|
||||
@@ -114,25 +120,34 @@ An example repository containing a guestbook application is available at
|
||||
|
||||
### Creating Apps Via CLI
|
||||
|
||||
~~~bash
|
||||
argocd app create guestbook \
|
||||
--repo https://github.com/argoproj/argocd-example-apps.git \
|
||||
--path guestbook \
|
||||
--dest-server https://kubernetes.default.svc \
|
||||
--dest-namespace default
|
||||
~~~
|
||||
!!! note
|
||||
You can access Argo CD using port forwarding: add `--port-forward-namespace argocd` flag to every CLI command or set `ARGOCD_OPTS` environment variable: `export ARGOCD_OPTS='--port-forward-namespace argocd'`:
|
||||
|
||||
`argocd app create guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path guestbook --dest-server https://kubernetes.default.svc --dest-namespace default`
|
||||
|
||||
### Creating Apps Via UI
|
||||
|
||||
Open a browser to the Argo CD external UI, and login using the credentials, IP/hostname set in step 4.
|
||||
Connect the [https://github.com/argoproj/argocd-example-apps.git](https://github.com/argoproj/argocd-example-apps.git) repo to Argo CD:
|
||||
Open a browser to the Argo CD external UI, and login by visiting the IP/hostname in a browser and use the credentials set in step 4.
|
||||
|
||||

|
||||
After logging in, click the **+ New App** button as shown below:
|
||||
|
||||
After connecting a repository, select the guestbook application for creation:
|
||||

|
||||
|
||||

|
||||

|
||||
Give your app the name `guestbook`, use the project `default`, and leave the sync policy as `Manual`:
|
||||
|
||||

|
||||
|
||||
Connect the [https://github.com/argoproj/argocd-example-apps.git](https://github.com/argoproj/argocd-example-apps.git) repo to Argo CD by setting repository url to the github repo url, leave revision as `HEAD`, and set the path to `guestbook`:
|
||||
|
||||

|
||||
|
||||
For **Destination**, set cluster to `in-cluster` and namespace to `default`:
|
||||
|
||||

|
||||
|
||||
After filling out the information above, click **Create** at the top of the UI to create the `guestbook` application:
|
||||
|
||||

|
||||
|
||||
|
||||
## 7. Sync (Deploy) The Application
|
||||
@@ -157,7 +172,7 @@ apps Deployment default guestbook-ui OutOfSync Missing
|
||||
Service default guestbook-ui OutOfSync Missing
|
||||
```
|
||||
|
||||
The application status is initially `OutOfSync` state, since the application has yet to be
|
||||
The application status is initially in `OutOfSync` state since the application has yet to be
|
||||
deployed, and no Kubernetes resources have been created. To sync (deploy) the application, run:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
# Overview
|
||||
|
||||
<!-- markdownlint-disable MD026 -->
|
||||
## What Is Argo CD?
|
||||
<!-- markdownlint-enable MD026 -->
|
||||
|
||||
Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes.
|
||||
|
||||

|
||||
|
||||
<!-- markdownlint-disable MD026 -->
|
||||
## Why Argo CD?
|
||||
<!-- markdownlint-enable MD026 -->
|
||||
|
||||
Application definitions, configurations, and environments should be declarative and version controlled.
|
||||
Application deployment and lifecycle management should be automated, auditable, and easy to understand.
|
||||
@@ -20,8 +24,8 @@ kubectl create namespace argocd
|
||||
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
|
||||
```
|
||||
|
||||
Follow our [getting started guide](getting_started.md). Further [documentation](docs/)
|
||||
is provided for additional features.
|
||||
Follow our [getting started guide](getting_started.md). Further user oriented [documentation](user_guide/)
|
||||
is provided for additional features. Developer oriented [documentation](developer-guide/) is available for people interested in building third-party integrations.
|
||||
|
||||
## How it works
|
||||
|
||||
@@ -31,7 +35,7 @@ the desired application state. Kubernetes manifests can be specified in several
|
||||
* [kustomize](https://kustomize.io) applications
|
||||
* [helm](https://helm.sh) charts
|
||||
* [ksonnet](https://ksonnet.io) applications
|
||||
* [jsonnet](https://jsonnet.org) files
|
||||
* [jsonnet](https://jsonnet.org) files
|
||||
* Plain directory of YAML/json manifests
|
||||
* Any custom config management tool configured as a config management plugin
|
||||
|
||||
@@ -44,7 +48,6 @@ For a quick 10 minute overview of Argo CD, check out the demo presented to the S
|
||||
meeting:
|
||||
[](https://youtu.be/aWDIQMbp1cc?t=1m4s)
|
||||
|
||||
|
||||
## Architecture
|
||||
|
||||

|
||||
@@ -82,4 +85,3 @@ For additional details, see [architecture overview](operator-manual/architecture
|
||||
## Development Status
|
||||
|
||||
Argo CD is actively developed and is being used in production to deploy SaaS services at Intuit
|
||||
|
||||
|
||||
@@ -27,9 +27,27 @@ spec:
|
||||
# Release name override (defaults to application name)
|
||||
releaseName: guestbook
|
||||
|
||||
# Helm values files for overriding values in the helm chart
|
||||
# The path is relative to the spec.source.path directory defined above
|
||||
valueFiles:
|
||||
- values-prod.yaml
|
||||
|
||||
# Values file as block file
|
||||
values: |
|
||||
ingress:
|
||||
enabled: true
|
||||
path: /
|
||||
hosts:
|
||||
- mydomain.example.com
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: nginx
|
||||
kubernetes.io/tls-acme: "true"
|
||||
labels: {}
|
||||
tls:
|
||||
- secretName: mydomain-tls
|
||||
hosts:
|
||||
- mydomain.example.com
|
||||
|
||||
# kustomize specific config
|
||||
kustomize:
|
||||
# Optional image name prefix
|
||||
@@ -74,6 +92,7 @@ spec:
|
||||
automated:
|
||||
prune: true # Specifies if resources should be pruned during auto-syncing ( false by default ).
|
||||
selfHeal: true # Specifies if partial app sync should be executed when resources are changed only in target Kubernetes cluster and no git change detected ( false by default ).
|
||||
validate: true # Validate resources before applying to k8s, defaults to true.
|
||||
|
||||
# Ignore differences at the specified json pointers
|
||||
ignoreDifferences:
|
||||
|
||||
@@ -27,7 +27,7 @@ data:
|
||||
help.chatText: 'Chat now!'
|
||||
|
||||
# A dex connector configuration (optional). See SSO configuration documentation:
|
||||
# https://github.com/argoproj/argo-cd/blob/master/docs/operator-manual/sso.md
|
||||
# https://github.com/argoproj/argo-cd/blob/master/docs/operator-manual/sso
|
||||
# https://github.com/dexidp/dex/tree/master/Documentation/connectors
|
||||
dex.config: |
|
||||
connectors:
|
||||
@@ -100,7 +100,7 @@ data:
|
||||
# List of json pointers in the object to ignore differences
|
||||
ignoreDifferences: |
|
||||
jsonPointers:
|
||||
- webhooks/0/clientConfig/caBundle
|
||||
- /webhooks/0/clientConfig/caBundle
|
||||
certmanager.k8s.io/Certificate:
|
||||
# Lua script for customizing the health status assessment
|
||||
health.lua: |
|
||||
@@ -124,6 +124,29 @@ data:
|
||||
hs.status = "Progressing"
|
||||
hs.message = "Waiting for certificate"
|
||||
return hs
|
||||
cert-manager.io/Certificate:
|
||||
# Lua script for customizing the health status assessment
|
||||
health.lua: |
|
||||
hs = {}
|
||||
if obj.status ~= nil then
|
||||
if obj.status.conditions ~= nil then
|
||||
for i, condition in ipairs(obj.status.conditions) do
|
||||
if condition.type == "Ready" and condition.status == "False" then
|
||||
hs.status = "Degraded"
|
||||
hs.message = condition.message
|
||||
return hs
|
||||
end
|
||||
if condition.type == "Ready" and condition.status == "True" then
|
||||
hs.status = "Healthy"
|
||||
hs.message = condition.message
|
||||
return hs
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
hs.status = "Progressing"
|
||||
hs.message = "Waiting for certificate"
|
||||
return hs
|
||||
apps/Deployment:
|
||||
# List of Lua Scripts to introduce custom actions
|
||||
actions: |
|
||||
@@ -160,6 +183,16 @@ data:
|
||||
clusters:
|
||||
- "*.local"
|
||||
|
||||
# By default all resource group/kinds are included. The resource.inclusions setting allows customizing
|
||||
# list of included group/kinds.
|
||||
resource.inclusions: |
|
||||
- apiGroups:
|
||||
- repositories.stash.appscode.com
|
||||
kinds:
|
||||
- Snapshot
|
||||
clusters:
|
||||
- "*.local"
|
||||
|
||||
# Configuration to add a config management plugin.
|
||||
configManagementPlugins: |
|
||||
- name: kasane
|
||||
@@ -175,3 +208,12 @@ data:
|
||||
# Tracking labels are used to determine which resources need to be deleted when pruning.
|
||||
# If omitted, Argo CD injects the app name into the label: 'app.kubernetes.io/instance'
|
||||
application.instanceLabelKey: mycompany.com/appname
|
||||
|
||||
# disables admin user. Admin is enabled by default
|
||||
admin.enabled: "false"
|
||||
# add an additional local user with apiKey and login capabilities
|
||||
# apiKey - allows generating API keys
|
||||
# login - allows to login using UI
|
||||
accounts.alice: apiKey, login
|
||||
# disables user. User is enabled by default
|
||||
accounts.alice.enabled: "false"
|
||||
@@ -24,6 +24,20 @@ data:
|
||||
|
||||
# Shared secrets for authenticating GitHub, GitLab, BitBucket webhook events (optional).
|
||||
# See https://github.com/argoproj/argo-cd/blob/master/docs/operator-manual/webhook.md for additional details.
|
||||
github.webhook.secret:
|
||||
gitlab.webhook.secret:
|
||||
bitbucket.webhook.uuid:
|
||||
# github webhook secret
|
||||
webhook.github.secret: shhhh! it's a github secret
|
||||
# gitlab webhook secret
|
||||
webhook.gitlab.secret: shhhh! it's a gitlab secret
|
||||
# bitbucket webhook secret
|
||||
webhook.bitbucket.uuid: your-bitbucket-uuid
|
||||
# bitbucket server webhook secret
|
||||
webhook.bitbucketserver.secret: shhhh! it's a bitbucket server secret
|
||||
# gogs server webhook secret
|
||||
webhook.gogs.secret: shhhh! it's a gogs server secret
|
||||
|
||||
# an additional user password and its last modified time (see user definition in argocd-cm.yaml)
|
||||
accounts.alice.password:
|
||||
accounts.alice.passwordMtime:
|
||||
# list of generated account tokens/api keys
|
||||
accounts.alice.tokens: |
|
||||
[{"id":"123","iat":1583789194,"exp":1583789194}]
|
||||
@@ -46,13 +46,10 @@ spec:
|
||||
source:
|
||||
path: guestbook
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps
|
||||
targetRevision: 08836bd970037ebcd14494831de4635bad961139
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
targetRevision: HEAD
|
||||
```
|
||||
|
||||
The sync policy to automated + prune, so that child app is are automatically created, synced, and deleted when the manifest is changed, but you may wish to disable this. I've also added the finalizer, which will ensure that you apps are deleted correctly.
|
||||
The sync policy to automated + prune, so that child apps are automatically created, synced, and deleted when the manifest is changed, but you may wish to disable this. I've also added the finalizer, which will ensure that your apps are deleted correctly.
|
||||
|
||||
Fix the revision to a specific Git commit SHA to make sure that, even if the child apps repo changes, the app will only change when the parent app change that revision. Alternatively, you can set it to HEAD or a branch name.
|
||||
|
||||
@@ -66,17 +63,33 @@ spec:
|
||||
server: https://kubernetes.default.svc
|
||||
```
|
||||
|
||||
Finally, you need to create your parent app, e.g.:
|
||||
Next, you need to create and sync your parent app, e.g. via the CLI:
|
||||
|
||||
```bash
|
||||
argocd app create applications \
|
||||
argocd app create apps \
|
||||
--dest-namespace argocd \
|
||||
--dest-server https://kubernetes.default.svc \
|
||||
--repo https://github.com/argoproj/argocd-example-apps.git \
|
||||
--path apps \
|
||||
--sync-policy automated
|
||||
--path apps
|
||||
argocd app sync apps
|
||||
```
|
||||
|
||||
In this example, I excluded auto-prune, as this would result in all apps being deleted if some accidentally deleted the *app of apps*.
|
||||
The parent app will appear as in-sync but the child apps will be out of sync:
|
||||
|
||||

|
||||
|
||||
You can either sync via the UI, firstly filter by the correct label:
|
||||
|
||||

|
||||
|
||||
Then select the "out of sync" apps and sync:
|
||||
|
||||

|
||||
|
||||
Or, via the CLI:
|
||||
|
||||
```bash
|
||||
argocd app sync -l app.kubernetes.io/instance=apps
|
||||
```
|
||||
|
||||
View [the example on Github](https://github.com/argoproj/argocd-example-apps/tree/master/apps).
|
||||
|
||||
@@ -14,7 +14,7 @@ can be customized to use alternative toolchain required by your environment.
|
||||
|
||||
## Adding Tools Via Volume Mounts
|
||||
|
||||
The first technique is to use an `init` container and a `volumeMount` to copy a different verison of
|
||||
The first technique is to use an `init` container and a `volumeMount` to copy a different version of
|
||||
a tool into the repo-server container. In the following example, an init container is overwriting
|
||||
the helm binary with a different version than what is bundled in Argo CD:
|
||||
|
||||
|
||||