Compare commits
400 Commits
v1.2.2
...
release-1.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24834112d6 | ||
|
|
48cced9d92 | ||
|
|
bc77ea70c4 | ||
|
|
f8721a7360 | ||
|
|
8ada07e0f5 | ||
|
|
de8ae9c617 | ||
|
|
2d029488ab | ||
|
|
3771486c14 | ||
|
|
97922f0439 | ||
|
|
141ef96a44 | ||
|
|
92824215f8 | ||
|
|
f74640d95c | ||
|
|
5c0ebb59d2 | ||
|
|
6d65d01757 | ||
|
|
36bbc29891 | ||
|
|
5af52f6698 | ||
|
|
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 | ||
|
|
f44681693a | ||
|
|
656167fceb | ||
|
|
87cb4987c9 | ||
|
|
549503c87a | ||
|
|
ae959ec575 | ||
|
|
2c88adc2ed | ||
|
|
8e60bdfe59 | ||
|
|
803b346caf | ||
|
|
4bd81730ef | ||
|
|
df56d036ff | ||
|
|
213ba364d7 | ||
|
|
d06df7438f | ||
|
|
9ebb12cf50 | ||
|
|
cb99c9c0ee | ||
|
|
1374107bea | ||
|
|
6e60762067 | ||
|
|
15cf89535e | ||
|
|
88e9cf0e18 | ||
|
|
402ce43804 | ||
|
|
f75984fbf5 | ||
|
|
f72a5c76a0 | ||
|
|
9aa1b18610 | ||
|
|
2e05e50fcb | ||
|
|
138ec15834 | ||
|
|
e422d33fec | ||
|
|
d925b73bf8 | ||
|
|
f94495ab82 | ||
|
|
c2dce9c981 | ||
|
|
f6545cd56a | ||
|
|
5abe863ea9 | ||
|
|
963300a7c9 | ||
|
|
f45991c8a7 | ||
|
|
61d1c1c722 | ||
|
|
d59a8b5b78 | ||
|
|
cf3436dcb3 | ||
|
|
afce1abbfb | ||
|
|
5bea7c32dc | ||
|
|
cc6be7f6f4 | ||
|
|
34a94bb060 | ||
|
|
53bf214207 | ||
|
|
abf6888dd5 | ||
|
|
f116bd3588 | ||
|
|
dd21ab92ad | ||
|
|
f185137ed1 | ||
|
|
c3479b886a | ||
|
|
9df130938e | ||
|
|
16e645b268 | ||
|
|
e9b2a6212a | ||
|
|
399a022099 | ||
|
|
039d81a8fc | ||
|
|
9d1a65d6a9 | ||
|
|
b5025559ac | ||
|
|
f5faeb888b | ||
|
|
e6e4751326 | ||
|
|
9a367da4f7 | ||
|
|
01ce567b66 | ||
|
|
8099a8807b | ||
|
|
96764c4d49 | ||
|
|
0b6c02391f | ||
|
|
886e1d3a14 | ||
|
|
a82d708dd8 | ||
|
|
7aa647080f | ||
|
|
9fb8d05591 | ||
|
|
b609a8ae85 | ||
|
|
a1b7a41705 | ||
|
|
bd2c4f1161 | ||
|
|
0f7e9a1d28 | ||
|
|
32f825e779 | ||
|
|
41a440f23d | ||
|
|
706b413353 | ||
|
|
c4203f7989 | ||
|
|
0e0a6e726e | ||
|
|
a345f349bc | ||
|
|
af8e41d6f8 | ||
|
|
ad24165eef | ||
|
|
4ff56493e6 | ||
|
|
a57e37a666 | ||
|
|
06bab51ee2 | ||
|
|
affbfe9020 | ||
|
|
1e5c78e35f | ||
|
|
70a97c0db8 | ||
|
|
5ef5ebcf19 | ||
|
|
e1954e3eaf | ||
|
|
d2af2faa52 | ||
|
|
06267d74f2 | ||
|
|
047d06f16f | ||
|
|
300b9b5fae | ||
|
|
bc226dcd6f | ||
|
|
dfb44373bf | ||
|
|
dc322f8a1f | ||
|
|
41b1b0a2d5 | ||
|
|
12b45116ed | ||
|
|
fb5bc58c56 | ||
|
|
62f029af5f | ||
|
|
c0084ebfe8 | ||
|
|
e0dd4b107c | ||
|
|
5516316cd2 | ||
|
|
72ea7912eb | ||
|
|
010dd02ba8 | ||
|
|
02cc6b100c | ||
|
|
8e3e79b5fc | ||
|
|
5d606cae2f | ||
|
|
39cb8db3e1 | ||
|
|
f4f291302c | ||
|
|
a1e7618d0f | ||
|
|
b85785726c | ||
|
|
4e9772e19b | ||
|
|
b37be09d6b | ||
|
|
b42389a021 | ||
|
|
cbf9585d84 | ||
|
|
e322750265 | ||
|
|
fe90744ea3 | ||
|
|
75cc094b88 | ||
|
|
3fa91729cc | ||
|
|
ea9b8c8b27 | ||
|
|
858823d911 | ||
|
|
58c32833ed | ||
|
|
7d9b8e60cc | ||
|
|
06b2fec68c | ||
|
|
21bc70be05 | ||
|
|
9d4a32e94f | ||
|
|
476682ba8c | ||
|
|
94b0a79d94 | ||
|
|
81aa3fb786 | ||
|
|
44a69e8a73 | ||
|
|
608361ce20 | ||
|
|
8b29b9c8c2 | ||
|
|
43a333d3a6 | ||
|
|
16883df273 | ||
|
|
459402b569 | ||
|
|
318a9251bd | ||
|
|
38b0f9d21f | ||
|
|
a10dd3f184 | ||
|
|
f9286cfab9 | ||
|
|
b45b3e807c | ||
|
|
7ac9e6f23c | ||
|
|
fc934fd4d7 | ||
|
|
133dfb76fe | ||
|
|
b79b388425 | ||
|
|
5930a8a04d | ||
|
|
57db0188cc | ||
|
|
adc6afd011 | ||
|
|
be56670519 | ||
|
|
83f58f2652 | ||
|
|
49a1a77c69 | ||
|
|
c9eb111d8a | ||
|
|
575dcc1697 | ||
|
|
b96a3aa401 | ||
|
|
b7377a1080 | ||
|
|
cdeff93e3a | ||
|
|
57aa8901e2 | ||
|
|
24b04be335 | ||
|
|
9fc6185436 | ||
|
|
aa0f9a3aa7 | ||
|
|
b85d3e59fa | ||
|
|
eb7f942acb | ||
|
|
ff71377546 | ||
|
|
97a6ebfdc8 | ||
|
|
066a083c62 | ||
|
|
ec142a1031 | ||
|
|
db6146a25c | ||
|
|
5b4c132e36 | ||
|
|
e94999b07d | ||
|
|
9a2e7ca190 | ||
|
|
bad2e91039 | ||
|
|
fdbe926aa8 | ||
|
|
a69c664e38 | ||
|
|
690a3cae16 | ||
|
|
cc7862bbb8 | ||
|
|
f2a341550d | ||
|
|
65ae3c2a32 | ||
|
|
9a59e9ac28 | ||
|
|
e682056ffc | ||
|
|
b17f330b88 | ||
|
|
9d0824beb6 | ||
|
|
9c8ab50d60 | ||
|
|
d2e98df607 | ||
|
|
de4fbcdf5b | ||
|
|
3f312a9e92 | ||
|
|
28be15de56 | ||
|
|
103609794c | ||
|
|
818524694d |
@@ -1,14 +1,5 @@
|
||||
version: 2.1
|
||||
commands:
|
||||
before:
|
||||
steps:
|
||||
- restore_go_cache
|
||||
- install_golang
|
||||
- install_tools
|
||||
- clean_checkout
|
||||
- configure_git
|
||||
- install_go_deps
|
||||
- dep_ensure
|
||||
configure_git:
|
||||
steps:
|
||||
- run:
|
||||
@@ -20,22 +11,15 @@ 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
|
||||
clean_checkout:
|
||||
steps:
|
||||
- run:
|
||||
name: Remove checked out code
|
||||
command: rm -Rf /home/circleci/.go_workspace/src/github.com/argoproj/argo-cd
|
||||
- checkout
|
||||
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:
|
||||
@@ -45,138 +29,41 @@ commands:
|
||||
command: |
|
||||
go get golang.org/dl/go1.12.6
|
||||
[ -e /home/circleci/sdk/go1.12.6 ] || go1.12.6 download
|
||||
go env
|
||||
echo "export GOPATH=/home/circleci/.go_workspace" | tee -a $BASH_ENV
|
||||
echo "export PATH=/home/circleci/sdk/go1.12.6/bin:\$PATH" | tee -a $BASH_ENV
|
||||
- run:
|
||||
name: Golang diagnostics
|
||||
command: |
|
||||
env
|
||||
which go
|
||||
go version
|
||||
go env
|
||||
install_go_deps:
|
||||
steps:
|
||||
- run:
|
||||
name: Install Go deps
|
||||
command: |
|
||||
set -x
|
||||
go get github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||
go get github.com/jstemmer/go-junit-report
|
||||
go get github.com/mattn/goreman
|
||||
go get golang.org/x/tools/cmd/goimports
|
||||
install_tools:
|
||||
steps:
|
||||
- run:
|
||||
name: Create downloads dir
|
||||
command: mkdir -p /tmp/dl
|
||||
- restore_cache:
|
||||
keys:
|
||||
- dl-v6
|
||||
- run:
|
||||
name: Install Kubectx v0.6.3
|
||||
command: |
|
||||
set -x
|
||||
[ -e /tmp/dl/kubectx.zip ] || curl -sLf -C - -o /tmp/dl/kubectx.zip https://github.com/ahmetb/kubectx/archive/v0.6.3.zip
|
||||
sudo unzip /tmp/dl/kubectx.zip kubectx-0.6.3/kubectx
|
||||
sudo unzip /tmp/dl/kubectx.zip kubectx-0.6.3/kubens
|
||||
sudo mv kubectx-0.6.3/kubectx /usr/local/bin/
|
||||
sudo mv kubectx-0.6.3/kubens /usr/local/bin/
|
||||
sudo chmod +x /usr/local/bin/kubectx
|
||||
sudo chmod +x /usr/local/bin/kubens
|
||||
- run:
|
||||
name: Install Dep v0.5.3
|
||||
command: |
|
||||
set -x
|
||||
[ -e /tmp/dl/dep ] || curl -sLf -C - -o /tmp/dl/dep https://github.com/golang/dep/releases/download/v0.5.3/dep-linux-amd64
|
||||
sudo cp /tmp/dl/dep /usr/local/go/bin/dep
|
||||
sudo chmod +x /usr/local/go/bin/dep
|
||||
dep version
|
||||
- run:
|
||||
name: Install Ksonnet v0.13.1
|
||||
command: |
|
||||
set -x
|
||||
[ -e /tmp/dl/ks.tar.gz ] || curl -sLf -C - -o /tmp/dl/ks.tar.gz https://github.com/ksonnet/ksonnet/releases/download/v0.13.1/ks_0.13.1_linux_amd64.tar.gz
|
||||
tar -C /tmp -xf /tmp/dl/ks.tar.gz
|
||||
sudo cp /tmp/ks_0.13.1_linux_amd64/ks /usr/local/go/bin/ks
|
||||
sudo chmod +x /usr/local/go/bin/ks
|
||||
ks version
|
||||
- run:
|
||||
name: Install Helm v2.13.1
|
||||
command: |
|
||||
set -x
|
||||
[ -e /tmp/dl/helm.tar.gz ] || curl -sLf -C - -o /tmp/dl/helm.tar.gz https://storage.googleapis.com/kubernetes-helm/helm-v2.13.1-linux-amd64.tar.gz
|
||||
tar -C /tmp/ -xf /tmp/dl/helm.tar.gz
|
||||
sudo cp /tmp/linux-amd64/helm /usr/local/go/bin/helm
|
||||
helm version --client
|
||||
helm init --client-only
|
||||
- run:
|
||||
name: Install Kustomize v3.1.0
|
||||
command: |
|
||||
set -x
|
||||
export VER=3.1.0
|
||||
[ -e /tmp/dl/kustomize_${VER} ] || curl -sLf -C - -o /tmp/dl/kustomize_${VER} https://github.com/kubernetes-sigs/kustomize/releases/download/v${VER}/kustomize_${VER}_linux_amd64
|
||||
sudo cp /tmp/dl/kustomize_${VER} /usr/local/go/bin/kustomize
|
||||
sudo chmod +x /usr/local/go/bin/kustomize
|
||||
kustomize version
|
||||
- run:
|
||||
name: Install Git LFS plugin
|
||||
command: |
|
||||
set -x
|
||||
curl -s https://packagecloud.io/install/repositories/github/git-lfs/script.deb.sh | sudo bash
|
||||
sleep 5
|
||||
sudo killall -9 apt-get || true
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y git-lfs openssh-client
|
||||
- save_cache:
|
||||
key: dl-v6
|
||||
paths:
|
||||
- /tmp/dl
|
||||
save_go_cache:
|
||||
steps:
|
||||
- save_cache:
|
||||
key: go-v17-{{ .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
|
||||
restore_go_cache:
|
||||
steps:
|
||||
- restore_cache:
|
||||
keys:
|
||||
- go-v17-{{ .Branch }}
|
||||
- go-v17-master
|
||||
- go-v16-{{ .Branch }}
|
||||
- go-v16-master
|
||||
- go-v1-{{ .Branch }}-{{ .Environment.CIRCLE_JOB }}
|
||||
- go-v1-master-{{ .Environment.CIRCLE_JOB }}
|
||||
jobs:
|
||||
build:
|
||||
working_directory: /home/circleci/.go_workspace/src/github.com/argoproj/argo-cd
|
||||
machine:
|
||||
image: circleci/classic:201808-01
|
||||
codegen:
|
||||
docker:
|
||||
- image: circleci/golang:1.12
|
||||
working_directory: /go/src/github.com/argoproj/argo-cd
|
||||
steps:
|
||||
- before
|
||||
- run:
|
||||
name: Run unit tests
|
||||
command: |
|
||||
set -x
|
||||
mkdir -p /tmp/test-results
|
||||
trap "go-junit-report </tmp/test-results/go-test.out > /tmp/test-results/go-test-report.xml" EXIT
|
||||
make test | tee /tmp/test-results/go-test.out
|
||||
- save_go_cache
|
||||
- run:
|
||||
name: Uploading code coverage
|
||||
command: bash <(curl -s https://codecov.io/bash) -f coverage.out
|
||||
# This takes 2m, lets background it.
|
||||
background: true
|
||||
- store_test_results:
|
||||
path: /tmp/test-results
|
||||
- run:
|
||||
name: Generate code
|
||||
command: make codegen
|
||||
- run:
|
||||
name: Lint code
|
||||
# use GOGC to limit memory usage in exchange for CPU usage, https://github.com/golangci/golangci-lint#memory-usage-of-golangci-lint
|
||||
# we have 8GB RAM, 2CPUs https://circleci.com/docs/2.0/executor-types/#using-machine
|
||||
command: LINT_GOGC=20 LINT_CONCURRENCY=1 LINT_DEADLINE=3m0s make lint
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- codegen-v1-{{ checksum "Gopkg.lock" }}-{{ checksum "hack/installers/install-codegen-go-tools.sh" }}
|
||||
- run: ./hack/install.sh codegen-go-tools
|
||||
- run: sudo ./hack/install.sh codegen-tools
|
||||
- run: dep ensure -v
|
||||
- save_cache:
|
||||
key: codegen-v1-{{ checksum "Gopkg.lock" }}-{{ checksum "hack/installers/install-codegen-go-tools.sh" }}
|
||||
paths: [vendor, /tmp/dl, /go/pkg]
|
||||
- run: helm init --client-only
|
||||
- run: make codegen-local
|
||||
- run:
|
||||
name: Check nothing has changed
|
||||
command: |
|
||||
@@ -188,11 +75,44 @@ jobs:
|
||||
git diff --exit-code -- . ':!Gopkg.lock' ':!assets/swagger.json' | tee codegen.patch
|
||||
- store_artifacts:
|
||||
path: codegen.patch
|
||||
when: always
|
||||
destination: .
|
||||
test:
|
||||
working_directory: /home/circleci/.go_workspace/src/github.com/argoproj/argo-cd
|
||||
machine:
|
||||
image: circleci/classic:201808-01
|
||||
steps:
|
||||
- restore_go_cache
|
||||
- install_golang
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: test-dl-v1
|
||||
- run: sudo ./hack/install.sh kubectl-linux kubectx-linux dep-linux ksonnet-linux helm-linux kustomize-linux
|
||||
- save_cache:
|
||||
key: test-dl-v1
|
||||
paths: [/tmp/dl]
|
||||
- configure_git
|
||||
- run: go get github.com/jstemmer/go-junit-report
|
||||
- restore_vendor
|
||||
- run: dep ensure -v
|
||||
- run: make test
|
||||
- save_vendor
|
||||
- save_go_cache
|
||||
- run:
|
||||
name: Uploading code coverage
|
||||
command: bash <(curl -s https://codecov.io/bash) -f coverage.out
|
||||
- store_test_results:
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
destination: .
|
||||
e2e:
|
||||
working_directory: /home/circleci/.go_workspace/src/github.com/argoproj/argo-cd
|
||||
machine:
|
||||
image: circleci/classic:201808-01
|
||||
environment:
|
||||
ARGOCD_FAKE_IN_CLUSTER: "true"
|
||||
ARGOCD_SSH_DATA_PATH: "/tmp/argo-e2e/app/config/ssh"
|
||||
ARGOCD_TLS_DATA_PATH: "/tmp/argo-e2e/app/config/tls"
|
||||
steps:
|
||||
- run:
|
||||
name: Install and start K3S v0.5.0
|
||||
@@ -204,15 +124,20 @@ jobs:
|
||||
environment:
|
||||
INSTALL_K3S_EXEC: --docker
|
||||
INSTALL_K3S_VERSION: v0.5.0
|
||||
- before
|
||||
- run:
|
||||
# do this before we build everything else in the background, as they tend to explode
|
||||
name: Make CLI
|
||||
command: |
|
||||
set -x
|
||||
make cli
|
||||
# must be added to path for tests
|
||||
echo export PATH="`pwd`/dist:\$PATH" | tee -a $BASH_ENV
|
||||
- restore_go_cache
|
||||
- install_golang
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys: [e2e-dl-v1]
|
||||
- run: sudo ./hack/install.sh kubectx-linux dep-linux ksonnet-linux helm-linux kustomize-linux
|
||||
- run: go get github.com/jstemmer/go-junit-report
|
||||
- save_cache:
|
||||
key: e2e-dl-v10
|
||||
paths: [/tmp/dl]
|
||||
- restore_vendor
|
||||
- run: dep ensure -v
|
||||
- configure_git
|
||||
- run: make cli
|
||||
- run:
|
||||
name: Create namespace
|
||||
command: |
|
||||
@@ -236,97 +161,57 @@ jobs:
|
||||
name: Start repo server
|
||||
command: go run ./cmd/argocd-repo-server/main.go --loglevel debug --redis localhost:6379
|
||||
background: true
|
||||
environment:
|
||||
# pft. if you do not quote "true", CircleCI turns it into "1", stoopid
|
||||
ARGOCD_FAKE_IN_CLUSTER: "true"
|
||||
ARGOCD_SSH_DATA_PATH: "/tmp/argo-e2e/app/config/ssh"
|
||||
ARGOCD_TLS_DATA_PATH: "/tmp/argo-e2e/app/config/tls"
|
||||
- run:
|
||||
name: Start API server
|
||||
command: go run ./cmd/argocd-server/main.go --loglevel debug --redis localhost:6379 --insecure --dex-server http://localhost:5556 --repo-server localhost:8081 --staticassets ../argo-cd-ui/dist/app
|
||||
background: true
|
||||
environment:
|
||||
ARGOCD_FAKE_IN_CLUSTER: "true"
|
||||
ARGOCD_SSH_DATA_PATH: "/tmp/argo-e2e/app/config/ssh"
|
||||
ARGOCD_TLS_DATA_PATH: "/tmp/argo-e2e/app/config/tls"
|
||||
- run:
|
||||
name: Start Test Git
|
||||
command: |
|
||||
test/fixture/testrepos/start-git.sh
|
||||
background: true
|
||||
- run:
|
||||
name: Wait for API server
|
||||
command: |
|
||||
set -x
|
||||
until curl -v http://localhost:8080/healthz; do sleep 3; done
|
||||
- run: until curl -v http://localhost:8080/healthz; do sleep 10; done
|
||||
- run:
|
||||
name: Start controller
|
||||
command: go run ./cmd/argocd-application-controller/main.go --loglevel debug --redis localhost:6379 --repo-server localhost:8081 --kubeconfig ~/.kube/config
|
||||
background: true
|
||||
environment:
|
||||
ARGOCD_FAKE_IN_CLUSTER: "true"
|
||||
- run:
|
||||
name: Smoke test
|
||||
command: |
|
||||
set -x
|
||||
argocd login localhost:8080 --plaintext --username admin --password password
|
||||
argocd app create guestbook --dest-namespace default --dest-server https://kubernetes.default.svc --repo https://github.com/argoproj/argocd-example-apps.git --path guestbook
|
||||
argocd app sync guestbook
|
||||
argocd app delete guestbook
|
||||
- run:
|
||||
name: Run e2e tests
|
||||
command: |
|
||||
set -x
|
||||
mkdir -p /tmp/test-results
|
||||
trap "go-junit-report </tmp/test-results/go-e2e.out > /tmp/test-results/go-e2e-report.xml" EXIT
|
||||
make test-e2e | tee /tmp/test-results/go-e2e.out
|
||||
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: /tmp/test-results
|
||||
path: test-results
|
||||
- store_artifacts:
|
||||
path: test-results
|
||||
destination: .
|
||||
ui:
|
||||
# note that we checkout the code in ~/argo-cd/, but then work in ~/argo-cd/ui
|
||||
working_directory: ~/argo-cd/ui
|
||||
docker:
|
||||
- image: node:11.15.0
|
||||
working_directory: ~/argo-cd/ui
|
||||
steps:
|
||||
- checkout:
|
||||
path: ~/argo-cd/
|
||||
- restore_cache:
|
||||
name: Restore Yarn Package Cache
|
||||
keys:
|
||||
- yarn-packages-v3-{{ checksum "yarn.lock" }}
|
||||
- run:
|
||||
name: Install
|
||||
command:
|
||||
yarn install --frozen-lockfile --ignore-optional --non-interactive
|
||||
- yarn-packages-v4-{{ checksum "yarn.lock" }}
|
||||
- run: yarn install --frozen-lockfile --ignore-optional --non-interactive
|
||||
- save_cache:
|
||||
name: Save Yarn Package Cache
|
||||
key: yarn-packages-v3-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- ~/.cache/yarn
|
||||
- node_modules
|
||||
- run:
|
||||
name: Test
|
||||
command: yarn test
|
||||
# This does not appear to work, and I don't want to spend time on it.
|
||||
- store_test_results:
|
||||
path: junit.xml
|
||||
- run:
|
||||
name: Build
|
||||
command: yarn build
|
||||
- run:
|
||||
name: Lint
|
||||
command: yarn lint
|
||||
key: yarn-packages-v4-{{ checksum "yarn.lock" }}
|
||||
paths: [~/.cache/yarn, node_modules]
|
||||
- run: yarn test
|
||||
- run: ./node_modules/.bin/codecov -p ..
|
||||
- run: NODE_ENV='production' yarn build
|
||||
- run: yarn lint
|
||||
workflows:
|
||||
version: 2
|
||||
workflow:
|
||||
jobs:
|
||||
- build
|
||||
- e2e
|
||||
- test
|
||||
- codegen
|
||||
- ui:
|
||||
# this isn't strictly true, we just put in here so that we 2/4 executors rather than 3/4
|
||||
requires:
|
||||
- build
|
||||
- codegen
|
||||
- e2e
|
||||
16
.codecov.yml
@@ -1,17 +1,17 @@
|
||||
ignore:
|
||||
- "**/*.pb.go"
|
||||
- "**/*.pb.gw.go"
|
||||
- "**/*generated.go"
|
||||
- "**/*generated.deepcopy.go"
|
||||
- "**/*_test.go"
|
||||
- "pkg/apis/.*"
|
||||
- "pkg/apis/client/.*"
|
||||
- "pkg/client/.*"
|
||||
- "test/.*"
|
||||
- "vendor/.*"
|
||||
coverage:
|
||||
status:
|
||||
# allow test coverage to drop by 1%, assume that it's typically due to CI problems
|
||||
patch:
|
||||
default:
|
||||
enabled: no
|
||||
if_not_found: success
|
||||
# we've found this not to be useful
|
||||
patch: off
|
||||
project:
|
||||
default:
|
||||
threshold: 1
|
||||
# allow test coverage to drop by 2%, assume that it's typically due to CI problems
|
||||
threshold: 2
|
||||
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -4,23 +4,27 @@ about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 'bug'
|
||||
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`.
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
If we cannot reproduce, we cannot fix! Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
A list of the steps required to reproduce the issue. Best of all, give us the URL to a repository that exhibits this issue.
|
||||
|
||||
**Expected behavior**
|
||||
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Version**
|
||||
@@ -34,10 +38,3 @@ Paste the output from `argocd version` here.
|
||||
```
|
||||
Paste any relevant application logs here.
|
||||
```
|
||||
|
||||
**Have you thought about contributing a fix yourself?**
|
||||
|
||||
Open Source software thrives with your contribution. It not only gives skills you might not be able to get in your day job, it also looks amazing on your resume.
|
||||
|
||||
If you want to get involved, check out the
|
||||
[contributing guide](https://github.com/argoproj/argo-cd/blob/master/docs/CONTRIBUTING.md), then reach out to us on [Slack](https://argoproj.github.io/community/join-slack) so we can see how to get you started.
|
||||
|
||||
18
.github/ISSUE_TEMPLATE/enhancement_proposal.md
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
name: Enhancement proposal
|
||||
about: Propose an enhancement for this project
|
||||
title: ''
|
||||
labels: 'enhancement'
|
||||
assignees: ''
|
||||
---
|
||||
# Summary
|
||||
|
||||
What change you think needs making.
|
||||
|
||||
# Motivation
|
||||
|
||||
Please give examples of your use case, e.g. when would you use this.
|
||||
|
||||
# Proposal
|
||||
|
||||
How do you think this should be implemented?
|
||||
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,21 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: 'enhancement'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Have you thought about contributing yourself?**
|
||||
|
||||
Open Source software thrives with your contribution. It not only gives skills you might not be able to get in your day job, it also looks amazing on your resume.
|
||||
|
||||
If you want to get involved, check out the
|
||||
[contributing guide](https://github.com/argoproj/argo-cd/blob/master/docs/CONTRIBUTING.md), then reach out to us on [Slack](https://argoproj.github.io/community/join-slack) so we can see how to get you started.
|
||||
12
.github/pull_request_template.md
vendored
@@ -1,7 +1,7 @@
|
||||
<!--
|
||||
Thank you for submitting your PR!
|
||||
Checklist:
|
||||
|
||||
We'd love your organisation to be listed in the [README](https://github.com/argoproj/argo-cd). Don't forget to add it if you can!
|
||||
|
||||
To troubleshoot builds: https://argoproj.github.io/argo-cd/developer-guide/ci/
|
||||
-->
|
||||
* [ ] Either (a) I've created an [enhancement proposal](https://github.com/argoproj/argo-cd/issues/new/choose) and discussed it with the community, (b) this is a bug fix, or (c) this does not need to be in the release notes.
|
||||
* [ ] The title of the PR states what changed and the related issues number (used for the release note).
|
||||
* [ ] I've updated both the CLI and UI to expose my feature, or I plan to submit a second PR with them.
|
||||
* [ ] Optional. My organization is added to the README.
|
||||
* [ ] 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 mkdocs_material
|
||||
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
|
||||
1
.gitignore
vendored
@@ -9,3 +9,4 @@ site/
|
||||
cmd/**/debug
|
||||
debug.test
|
||||
coverage.out
|
||||
test-results
|
||||
@@ -1,22 +1,22 @@
|
||||
run:
|
||||
deadline: 2m
|
||||
timeout: 2m
|
||||
skip-files:
|
||||
- ".*\\.pb\\.go"
|
||||
skip-dirs:
|
||||
- pkg/client
|
||||
- vendor
|
||||
linter-settings:
|
||||
goimports:
|
||||
local-prefixes: github.com/argoproj/argo-cd
|
||||
- pkg/client/
|
||||
- vendor/
|
||||
linters:
|
||||
enable:
|
||||
- vet
|
||||
- gofmt
|
||||
- goimports
|
||||
- deadcode
|
||||
- goimports
|
||||
- varcheck
|
||||
- structcheck
|
||||
- ineffassign
|
||||
- unconvert
|
||||
- misspell
|
||||
- unparam
|
||||
linters-settings:
|
||||
goimports:
|
||||
local-prefixes: github.com/argoproj/argo-cd
|
||||
service:
|
||||
golangci-lint-version: 1.21.0
|
||||
|
||||
402
CHANGELOG.md
@@ -1,5 +1,407 @@
|
||||
# Changelog
|
||||
|
||||
## v1.4.0 (Not Released)
|
||||
|
||||
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
|
||||
|
||||
* [@abhishekjiitr](https://github.com/abhishekjiitr)
|
||||
* [@adamjohnson01](https://github.com/adamjohnson01)
|
||||
* [@alexec](https://github.com/alexec)
|
||||
* [@alexmt](https://github.com/alexmt)
|
||||
* [@binoue](https://github.com/binoue)
|
||||
* [@cabrinha](https://github.com/cabrinha)
|
||||
* [@cbanek](https://github.com/cbanek)
|
||||
* [@dgoodwin](https://github.com/dgoodwin)
|
||||
* [@jannfis](https://github.com/jannfis)
|
||||
* [@jessesuen](https://github.com/jessesuen)
|
||||
* [@masa213f](https://github.com/masa213f)
|
||||
* [@whs](https://github.com/whs)
|
||||
|
||||
## 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.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)
|
||||
- Stop loggin /repository.RepositoryService/ValidateAccess parameters (#2386)
|
||||
|
||||
## v1.2.2 (2019-09-26)
|
||||
+ Resource action equivalent to `kubectl rollout restart` (#2177)
|
||||
- Badge response does not contain cache-control header (#2317) (@greenstatic)
|
||||
- Make sure the controller uses the latest git version if app reconciliation result expired (#2339)
|
||||
|
||||
## v1.2.1 (2019-09-12)
|
||||
+ Support limiting number of concurrent kubectl fork/execs (#2022)
|
||||
+ Add --self-heal flag to argocd cli (#2296)
|
||||
- Fix degraded proxy support for http(s) git repository (#2243)
|
||||
- Fix nil pointer dereference in application controller (#2290)
|
||||
|
||||
## v1.2.0 (2019-09-05)
|
||||
|
||||
### New Features
|
||||
|
||||
#### Server Certificate And Known Hosts Management
|
||||
|
||||
The Server Certificate And Known Hosts Management feature makes it really easy to connect private Git repositories to Argo CD. Now Argo CD provides UI and CLI which
|
||||
enables managing certificates and known hosts which are used to access Git repositories. It is also possible to configure both hosts and certificates in a declarative manner using
|
||||
[argocd-ssh-known-hosts-cm](https://github.com/argoproj/argo-cd/blob/master/docs/operator-manual/argocd-ssh-known-hosts-cm.yaml) and
|
||||
[argocd-tls-certs-cm.yaml](https://github.com/argoproj/argo-cd/blob/master/docs/operator-manual/argocd-tls-certs-cm.yaml) config maps.
|
||||
|
||||
#### Self-Healing
|
||||
|
||||
The existing Automatic Sync feature allows to automatically apply any new changes in Git to the target Kubernetes cluster. However, Automatic Sync does not cover the case when the
|
||||
application is out of sync due to the unexpected change in the target cluster. The Self-Healing feature fills this gap. With Self-Healing enabled Argo CD automatically pushes the desired state from Git into the cluster every time when state deviation is detected.
|
||||
|
||||
**Anonymous access** - enable read-only access without authentication to anyone in your organization.
|
||||
|
||||
Support for Git LFS enabled repositories - now you can store Helm charts as tar files and enable Git LFS in your repository.
|
||||
|
||||
**Compact diff view** - compact diff summary of the whole application in a single view.
|
||||
|
||||
**Badge for application status** - add badge with the health and sync status of your application into README.md of your deployment repo.
|
||||
|
||||
**Allow configuring google analytics tracking** - use Google Analytics to check how many users are visiting UI or your Argo CD instance.
|
||||
|
||||
#### Backward Incompatible Changes
|
||||
- Kustomize v1 support is removed. All kustomize charts are built using the same Kustomize version
|
||||
- Kustomize v2.0.3 upgraded to v3.1.0 . We've noticed one backward incompatible change: https://github.com/kubernetes-sigs/kustomize/issues/42 . Starting v2.1.0 namespace prefix feature works with CRD ( which might cause renaming of generated resource definitions)
|
||||
- Argo CD config maps must be annotated with `app.kubernetes.io/part-of: argocd` label. Make sure to apply updated `install.yaml` manifest in addition to changing image version.
|
||||
|
||||
|
||||
#### Enhancements
|
||||
+ Adds a floating action button with help and chat links to every page.… (#2124)
|
||||
+ Enhances cookie warning with actual length to help users fix their co… (#2134)
|
||||
+ Added 'SyncFail' to possible HookTypes in UI (#2147)
|
||||
+ Support for Git LFS enabled repositories (#1853)
|
||||
+ Server certificate and known hosts management (#1514)
|
||||
+ Client HTTPS certifcates for private git repositories (#1945)
|
||||
+ Badge for application status (#1435)
|
||||
+ Make the health check for APIService a built in (#1841)
|
||||
+ Bitbucket Server and Gogs webhook providers (#1269)
|
||||
+ Jsonnet TLA arguments in ArgoCD CLI (#1626)
|
||||
+ Self Healing (#1736)
|
||||
+ Compact diff view (#1831)
|
||||
+ Allow Helm parameters to force ambiguously-typed values to be strings (#1846)
|
||||
+ Support anonymous argocd access (#1620)
|
||||
+ Allow configuring google analytics tracking (#738)
|
||||
+ Bash autocompletion for argocd (#1798)
|
||||
+ Additional commit metadata (#1219)
|
||||
+ Displays targetRevision in app dashboards. (#1239)
|
||||
+ Local path syncing (#839)
|
||||
+ System level `kustomize build` options (#1789)
|
||||
+ Adds support for `argocd app set` for Kustomize. (#1843)
|
||||
+ Allow users to create tokens for projects where they have any role. (#1977)
|
||||
+ Add Refresh button to applications table and card view (#1606)
|
||||
+ Adds CLI support for adding and removing groups from project roles. (#1851)
|
||||
+ Support dry run and hook vs. apply strategy during sync (#798)
|
||||
+ UI should remember most recent selected tab on resource info panel (#2007)
|
||||
+ Adds link to the project from the app summary page. (#1911)
|
||||
+ Different icon for resources which require pruning (#1159)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- Do not panic if the type is not api.Status (an error scenario) (#2105)
|
||||
- Make sure endpoint is shown as a child of service (#2060)
|
||||
- Word-wraps app info in the table and list views. (#2004)
|
||||
- Project source/destination removal should consider wildcards (#1780)
|
||||
- Repo whitelisting in UI does not support wildcards (#2000)
|
||||
- Wait for CRD creation during sync process (#1940)
|
||||
- Added a button to select out of sync items in the sync panel (#1902)
|
||||
- Proper handling of an excluded resource in an application (#1621)
|
||||
- Stop repeating logs on stoped container (#1614)
|
||||
- Fix git repo url parsing on application list view (#2174)
|
||||
- Fix nil pointer dereference error during app reconciliation (#2146)
|
||||
- Fix history api fallback implementation to support app names with dots (#2114)
|
||||
- Fixes some code issues related to Kustomize build options. (#2146)
|
||||
- Adds checks around valid paths for apps (#2133)
|
||||
- Enpoint incorrectly considered top level managed resource (#2060)
|
||||
- Allow adding certs for hostnames ending on a dot (#2116)
|
||||
|
||||
#### Other
|
||||
* Upgrade kustomize to v3.1.0 (#2068)
|
||||
* Remove support for Kustomize 1. (#1573)
|
||||
|
||||
#### Contributors
|
||||
|
||||
* [alexec](https://github.com/alexec)
|
||||
* [alexmt](https://github.com/alexmt)
|
||||
* [dmizelle](https://github.com/dmizelle)
|
||||
* [lcostea](https://github.com/lcostea)
|
||||
* [jutley](https://github.com/jutley)
|
||||
* [masa213f](https://github.com/masa213f)
|
||||
* [Rayyis](https://github.com/Rayyis)
|
||||
* [simster7](https://github.com/simster7)
|
||||
* [dthomson25](https://github.com/dthomson25)
|
||||
* [jannfis](https://github.com/jannfis)
|
||||
* [naynasiddharth](https://github.com/naynasiddharth)
|
||||
* [stgarf](https://github.com/stgarf)
|
||||
|
||||
|
||||
## v1.1.2 (2019-07-30)
|
||||
- 'argocd app wait' should print correct sync status (#2049)
|
||||
- Check that TLS is enabled when registering DEX Handlers (#2047)
|
||||
|
||||
62
Dockerfile
@@ -1,4 +1,4 @@
|
||||
ARG BASE_IMAGE=debian:9.5-slim
|
||||
ARG BASE_IMAGE=debian:10-slim
|
||||
####################################################################################################
|
||||
# Builder image
|
||||
# Initial stage which pulls prepares build dependencies and CLI tooling we need for our final image
|
||||
@@ -6,7 +6,7 @@ ARG BASE_IMAGE=debian:9.5-slim
|
||||
####################################################################################################
|
||||
FROM golang:1.12.6 as builder
|
||||
|
||||
RUN echo 'deb http://deb.debian.org/debian stretch-backports main' >> /etc/apt/sources.list
|
||||
RUN echo 'deb http://deb.debian.org/debian buster-backports main' >> /etc/apt/sources.list
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
openssh-server \
|
||||
@@ -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 helm-linux
|
||||
RUN ./install.sh kustomize-linux
|
||||
RUN ./install.sh aws-iam-authenticator-linux
|
||||
|
||||
####################################################################################################
|
||||
# Argo CD Base - used as the base for both the release and dev argocd images
|
||||
@@ -72,12 +41,14 @@ FROM $BASE_IMAGE as argocd-base
|
||||
|
||||
USER root
|
||||
|
||||
RUN echo 'deb http://deb.debian.org/debian stretch-backports main' >> /etc/apt/sources.list
|
||||
RUN echo 'deb http://deb.debian.org/debian buster-backports main' >> /etc/apt/sources.list
|
||||
|
||||
RUN groupadd -g 999 argocd && \
|
||||
useradd -r -u 999 -g argocd argocd && \
|
||||
mkdir -p /home/argocd && \
|
||||
chown argocd:argocd /home/argocd && \
|
||||
chown argocd:0 /home/argocd && \
|
||||
chmod g=u /home/argocd && \
|
||||
chmod g=u /etc/passwd && \
|
||||
apt-get update && \
|
||||
apt-get install -y git git-lfs && \
|
||||
apt-get clean && \
|
||||
@@ -89,6 +60,9 @@ 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
|
||||
|
||||
# support for mounting configuration from a configmap
|
||||
RUN mkdir -p /app/config/ssh && \
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
####################################################################################################
|
||||
FROM argocd-base
|
||||
COPY argocd* /usr/local/bin/
|
||||
COPY --from=argocd-ui ./src/dist/app /shared/app
|
||||
|
||||
497
Gopkg.lock
generated
@@ -1,6 +1,14 @@
|
||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
digest = "1:6d5a057da97a9dbdb10e7beedd2f43452b6bf7691001c0c8886e8dacf5610349"
|
||||
name = "bou.ke/monkey"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "bdf6dea004c6fd1cdf4b25da8ad45a606c09409a"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:9702dc153c9bb6ee7ee0587c248b7024700e89e4a7be284faaeeab9da32e1c6b"
|
||||
name = "cloud.google.com/go"
|
||||
@@ -17,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"
|
||||
@@ -50,19 +66,7 @@
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:0caf9208419fa5db5a0ca7112affaa9550c54291dda8e2abac0c0e76181c959e"
|
||||
name = "github.com/argoproj/argo"
|
||||
packages = [
|
||||
"pkg/apis/workflow",
|
||||
"pkg/apis/workflow/v1alpha1",
|
||||
"util",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "7ef1cea68c94f7f0e1e2f8bd75bedc5a7df8af90"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:4f6afcf4ebe041b3d4aa7926d09344b48d2f588e1f957526bbbe54f9cbb366a1"
|
||||
digest = "1:52905b00a73cda93a2ce8c5fa35185daed673d59e39576e81ad6ab6fb7076b3c"
|
||||
name = "github.com/argoproj/pkg"
|
||||
packages = [
|
||||
"errors",
|
||||
@@ -71,7 +75,7 @@
|
||||
"time",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "38dba6e98495680ff1f8225642b63db10a96bb06"
|
||||
revision = "02a6aac40ac4cd23de448afe7a1ec0ba4b6d2b96"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:d8a2bb36a048d1571bcc1aee208b61f39dc16c6c53823feffd37449dde162507"
|
||||
@@ -90,12 +94,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",
|
||||
@@ -104,8 +110,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"
|
||||
@@ -131,6 +150,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"
|
||||
@@ -188,12 +218,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"
|
||||
@@ -201,6 +247,7 @@
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "0ca9ea5df5451ffdf184b4428c902747c2c11cd7"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -352,7 +399,7 @@
|
||||
revision = "5a05380e4bc2440e0ec12f54f6f45648dbdd5e55"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:6e73003ecd35f4487a5e88270d3ca0a81bc80dc88053ac7e4dcfec5fba30d918"
|
||||
digest = "1:d69d2ba23955582a64e367ff2b0808cdbd048458c178cea48f11ab8c40bd7aea"
|
||||
name = "github.com/gogo/protobuf"
|
||||
packages = [
|
||||
"gogoproto",
|
||||
@@ -385,8 +432,8 @@
|
||||
"vanity/command",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "636bf0302bc95575d69441b25a2603156ffdddf1"
|
||||
version = "v1.1.1"
|
||||
revision = "5628607bb4c51c3157aacc3a50f0ab707582b805"
|
||||
version = "v1.3.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -419,6 +466,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"
|
||||
@@ -466,6 +535,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"
|
||||
@@ -556,6 +636,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"
|
||||
@@ -587,6 +675,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"
|
||||
@@ -647,6 +743,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"
|
||||
@@ -663,6 +767,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"
|
||||
@@ -734,6 +854,14 @@
|
||||
pruneopts = ""
|
||||
revision = "05ee40e3a273f7245e8777337fc7b46e533a9a92"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:6bb048133650d1fb7fbff9fb3c35bd5c7e8653fc95c3bae6df94cd17d1580278"
|
||||
name = "github.com/robfig/cron"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "45fbe1491cdd47d74d1bf1396286d67faee8b8b5"
|
||||
version = "v3.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:5f47c69f85311c4dc292be6cc995a0a3fe8337a6ce38ef4f71e5b7efd5ad42e0"
|
||||
name = "github.com/rs/cors"
|
||||
@@ -742,6 +870,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"
|
||||
@@ -778,11 +914,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"
|
||||
@@ -1229,11 +1365,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",
|
||||
@@ -1253,6 +1391,7 @@
|
||||
"coordination/v1",
|
||||
"coordination/v1beta1",
|
||||
"core/v1",
|
||||
"discovery/v1alpha1",
|
||||
"events/v1beta1",
|
||||
"extensions/v1beta1",
|
||||
"imagepolicy/v1alpha1",
|
||||
@@ -1273,34 +1412,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",
|
||||
@@ -1319,12 +1465,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",
|
||||
@@ -1343,14 +1491,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",
|
||||
@@ -1359,6 +1544,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",
|
||||
@@ -1397,6 +1584,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",
|
||||
@@ -1441,6 +1630,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",
|
||||
@@ -1451,8 +1649,10 @@
|
||||
"tools/clientcmd/api/v1",
|
||||
"tools/metrics",
|
||||
"tools/pager",
|
||||
"tools/portforward",
|
||||
"tools/reference",
|
||||
"tools/remotecommand",
|
||||
"tools/watch",
|
||||
"transport",
|
||||
"transport/spdy",
|
||||
"util/cert",
|
||||
@@ -1466,11 +1666,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",
|
||||
@@ -1479,7 +1679,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"
|
||||
@@ -1517,7 +1725,7 @@
|
||||
revision = "e80910364765199a4baebd4dec54c885fe52b680"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:42ea993b351fdd39b9aad3c9ebe71f2fdb5d1f8d12eed24e71c3dff1a31b2a43"
|
||||
digest = "1:16a343bd9d820ae320de4d1eaa8acc7a214aac4b38fb21d03255d3a457d861df"
|
||||
name = "k8s.io/kube-openapi"
|
||||
packages = [
|
||||
"cmd/openapi-gen",
|
||||
@@ -1526,41 +1734,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"
|
||||
@@ -1570,6 +1882,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"
|
||||
@@ -1582,16 +1925,14 @@
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
input-imports = [
|
||||
"bou.ke/monkey",
|
||||
"github.com/Masterminds/semver",
|
||||
"github.com/TomOnTime/utfutil",
|
||||
"github.com/argoproj/argo/pkg/apis/workflow/v1alpha1",
|
||||
"github.com/argoproj/argo/util",
|
||||
"github.com/argoproj/pkg/errors",
|
||||
"github.com/argoproj/pkg/exec",
|
||||
"github.com/argoproj/pkg/time",
|
||||
"github.com/casbin/casbin",
|
||||
"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",
|
||||
@@ -1632,6 +1973,7 @@
|
||||
"github.com/pkg/errors",
|
||||
"github.com/prometheus/client_golang/prometheus",
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp",
|
||||
"github.com/robfig/cron",
|
||||
"github.com/sirupsen/logrus",
|
||||
"github.com/sirupsen/logrus/hooks/test",
|
||||
"github.com/skratchdot/open-golang/open",
|
||||
@@ -1646,6 +1988,7 @@
|
||||
"github.com/yuin/gopher-lua",
|
||||
"golang.org/x/crypto/bcrypt",
|
||||
"golang.org/x/crypto/ssh",
|
||||
"golang.org/x/crypto/ssh/knownhosts",
|
||||
"golang.org/x/crypto/ssh/terminal",
|
||||
"golang.org/x/net/context",
|
||||
"golang.org/x/oauth2",
|
||||
@@ -1678,6 +2021,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",
|
||||
@@ -1692,10 +2036,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",
|
||||
@@ -1703,6 +2051,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",
|
||||
@@ -1711,6 +2060,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",
|
||||
@@ -1719,13 +2070,33 @@
|
||||
"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",
|
||||
]
|
||||
solver-name = "gps-cdcl"
|
||||
|
||||
53
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,25 +36,54 @@ 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"
|
||||
@@ -71,12 +100,18 @@ 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"
|
||||
|
||||
60
Makefile
@@ -9,25 +9,21 @@ GIT_COMMIT=$(shell git rev-parse HEAD)
|
||||
GIT_TAG=$(shell if [ -z "`git status --porcelain`" ]; then git describe --exact-match --tags HEAD 2>/dev/null; fi)
|
||||
GIT_TREE_STATE=$(shell if [ -z "`git status --porcelain`" ]; then echo "clean" ; else echo "dirty"; fi)
|
||||
PACKR_CMD=$(shell if [ "`which packr`" ]; then echo "packr"; else echo "go run vendor/github.com/gobuffalo/packr/packr/main.go"; fi)
|
||||
VOLUME_MOUNT=$(shell [[ $(go env GOOS)=="darwin" ]] && echo ":delegated" || echo "")
|
||||
|
||||
define run-in-dev-tool
|
||||
docker run --rm -it -u $(shell id -u) -e HOME=/home/user -v ${CURRENT_DIR}:/go/src/github.com/argoproj/argo-cd -w /go/src/github.com/argoproj/argo-cd argocd-dev-tools bash -c "GOPATH=/go $(1)"
|
||||
docker run --rm -it -u $(shell id -u) -e HOME=/home/user -v ${CURRENT_DIR}:/go/src/github.com/argoproj/argo-cd${VOLUME_MOUNT} -w /go/src/github.com/argoproj/argo-cd argocd-dev-tools bash -c "GOPATH=/go $(1)"
|
||||
endef
|
||||
|
||||
PATH:=$(PATH):$(PWD)/hack
|
||||
|
||||
# docker image publishing options
|
||||
DOCKER_PUSH?=false
|
||||
IMAGE_TAG?=latest
|
||||
IMAGE_NAMESPACE?=
|
||||
# perform static compilation
|
||||
STATIC_BUILD?=true
|
||||
# build development images
|
||||
DEV_IMAGE?=false
|
||||
# lint is memory and CPU intensive, so we can limit on CI to mitigate OOM
|
||||
LINT_GOGC?=off
|
||||
LINT_CONCURRENCY?=8
|
||||
# Set timeout for linter
|
||||
LINT_DEADLINE?=1m0s
|
||||
|
||||
override LDFLAGS += \
|
||||
-X ${PACKAGE}.version=${VERSION} \
|
||||
@@ -94,7 +90,7 @@ argocd-util: clean-debug
|
||||
|
||||
.PHONY: dev-tools-image
|
||||
dev-tools-image:
|
||||
docker build -t argocd-dev-tools ./hack -f ./hack/Dockerfile.dev-tools
|
||||
cd hack && docker build -t argocd-dev-tools . -f Dockerfile.dev-tools
|
||||
|
||||
.PHONY: manifests-local
|
||||
manifests-local:
|
||||
@@ -128,8 +124,10 @@ ifeq ($(DEV_IMAGE), true)
|
||||
# The "dev" image builds the binaries from the users desktop environment (instead of in Docker)
|
||||
# which speeds up builds. Dockerfile.dev needs to be copied into dist to perform the build, since
|
||||
# the dist directory is under .dockerignore.
|
||||
IMAGE_TAG="dev-$(shell git describe --always --dirty)"
|
||||
image: packr
|
||||
docker build -t argocd-base --target argocd-base .
|
||||
docker build -t argocd-ui --target argocd-ui .
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-server ./cmd/argocd-server
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-application-controller ./cmd/argocd-application-controller
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-repo-server ./cmd/argocd-repo-server
|
||||
@@ -149,15 +147,22 @@ builder-image:
|
||||
docker build -t $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG) --target builder .
|
||||
@if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG) ; fi
|
||||
|
||||
.PHONY: dep
|
||||
dep:
|
||||
dep ensure -v
|
||||
|
||||
.PHONY: dep-ensure
|
||||
dep-ensure:
|
||||
dep ensure -no-vendor
|
||||
|
||||
.PHONY: install-lint-tools
|
||||
install-lint-tools:
|
||||
./hack/install.sh lint-tools
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
# golangci-lint does not do a good job of formatting imports
|
||||
goimports -local github.com/argoproj/argo-cd -w `find . ! -path './vendor/*' ! -path './pkg/client/*' -type f -name '*.go'`
|
||||
GOGC=$(LINT_GOGC) golangci-lint run --fix --verbose --concurrency $(LINT_CONCURRENCY) --deadline $(LINT_DEADLINE)
|
||||
golangci-lint --version
|
||||
golangci-lint run --fix --verbose
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
@@ -165,15 +170,11 @@ build:
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test -v -covermode=count -coverprofile=coverage.out `go list ./... | grep -v "test/e2e"`
|
||||
|
||||
.PHONY: cover
|
||||
cover:
|
||||
go tool cover -html=coverage.out
|
||||
./hack/test.sh -coverprofile=coverage.out `go list ./... | grep -v 'test/e2e'`
|
||||
|
||||
.PHONY: test-e2e
|
||||
test-e2e: cli
|
||||
go test -v -timeout 10m ./test/e2e
|
||||
test-e2e:
|
||||
./hack/test.sh -timeout 15m ./test/e2e
|
||||
|
||||
.PHONY: start-e2e
|
||||
start-e2e: cli
|
||||
@@ -186,6 +187,8 @@ start-e2e: cli
|
||||
# set paths for locally managed ssh known hosts and tls certs data
|
||||
ARGOCD_SSH_DATA_PATH=/tmp/argo-e2e/app/config/ssh \
|
||||
ARGOCD_TLS_DATA_PATH=/tmp/argo-e2e/app/config/tls \
|
||||
ARGOCD_E2E_DISABLE_AUTH=false \
|
||||
ARGOCD_ZJWT_FEATURE_FLAG=always \
|
||||
goreman start
|
||||
|
||||
# Cleans VSCode debug.test files from sub-dirs to prevent them from being included in packr boxes
|
||||
@@ -202,8 +205,10 @@ start:
|
||||
killall goreman || true
|
||||
# check we can connect to Docker to start Redis
|
||||
docker version
|
||||
kubectl create ns argocd || true
|
||||
kubens argocd
|
||||
goreman start
|
||||
ARGOCD_ZJWT_FEATURE_FLAG=always \
|
||||
goreman start
|
||||
|
||||
.PHONY: pre-commit
|
||||
pre-commit: dep-ensure codegen build lint test
|
||||
@@ -216,3 +221,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
|
||||
6
OWNERS
@@ -1,8 +1,12 @@
|
||||
owners:
|
||||
- alexec
|
||||
- alexmt
|
||||
- jessesuen
|
||||
|
||||
reviewers:
|
||||
- jannfis
|
||||
|
||||
approvers:
|
||||
- alexc
|
||||
- alexec
|
||||
- alexmt
|
||||
- jessesuen
|
||||
|
||||
8
Procfile
@@ -1,7 +1,7 @@
|
||||
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 --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"
|
||||
api-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true go run ./cmd/argocd-server/main.go --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --disable-auth=${ARGOCD_E2E_DISABLE_AUTH:-'true'} --insecure --dex-server http://localhost:${ARGOCD_E2E_DEX_PORT:-5556} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --port ${ARGOCD_E2E_APISERVER_PORT:-8080} --staticassets ui/dist/app"
|
||||
dex: sh -c "go run github.com/argoproj/argo-cd/cmd/argocd-util gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p ${ARGOCD_E2E_DEX_PORT:-5556}:${ARGOCD_E2E_DEX_PORT:-5556} -v `pwd`/dist/dex.yaml:/dex.yaml quay.io/dexidp/dex:v2.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 go run ./cmd/argocd-repo-server/main.go --loglevel debug --port ${ARGOCD_E2E_REPOSERVER_PORT:-8081} --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379}"
|
||||
repo-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true go run ./cmd/argocd-repo-server/main.go --loglevel debug --port ${ARGOCD_E2E_REPOSERVER_PORT:-8081} --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379}"
|
||||
ui: sh -c 'cd ui && ${ARGOCD_E2E_YARN_CMD:-yarn} start'
|
||||
git-server: test/fixture/testrepos/start-git.sh
|
||||
git-server: test/fixture/testrepos/start-git.sh
|
||||
|
||||
34
README.md
@@ -1,5 +1,6 @@
|
||||
[](https://argoproj.github.io/community/join-slack)
|
||||
[](https://codecov.io/gh/argoproj/argo-cd)
|
||||
[](https://github.com/argoproj/argo-cd/releases/latest)
|
||||
|
||||
# Argo CD - Declarative Continuous Delivery for Kubernetes
|
||||
|
||||
@@ -12,6 +13,7 @@ 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.
|
||||
|
||||
|
||||
@@ -19,33 +21,62 @@ Application deployment and lifecycle management should be automated, auditable,
|
||||
|
||||
Organizations below are **officially** using Argo CD. Please send a PR with your organization name if you are using Argo CD.
|
||||
|
||||
1. [127Labs](https://127labs.com/)
|
||||
1. [Adevinta](https://www.adevinta.com/)
|
||||
1. [ANSTO - Australian Synchrotron](https://www.synchrotron.org.au/)
|
||||
1. [ARZ Allgemeines Rechenzentrum GmbH ](https://www.arz.at/)
|
||||
1. [Baloise](https://www.baloise.com)
|
||||
1. [BioBox Analytics](https://biobox.io)
|
||||
1. [CARFAX](https://www.carfax.com)
|
||||
1. [Celonis](https://www.celonis.com/)
|
||||
1. [Codility](https://www.codility.com/)
|
||||
1. [Commonbond](https://commonbond.co/)
|
||||
1. [CyberAgent](https://www.cyberagent.co.jp/en/)
|
||||
1. [Cybozu](https://cybozu-global.com)
|
||||
1. [EDF Renewables](https://www.edf-re.com/)
|
||||
1. [Elium](https://www.elium.com)
|
||||
1. [END.](https://www.endclothing.com/)
|
||||
1. [Fave](https://myfave.com)
|
||||
1. [Future PLC](https://www.futureplc.com/)
|
||||
1. [GMETRI](https://gmetri.com/)
|
||||
1. [hipages](https://hipages.com.au/)
|
||||
1. [Intuit](https://www.intuit.com/)
|
||||
1. [KintoHub](https://www.kintohub.com/)
|
||||
1. [KompiTech GmbH](https://www.kompitech.com/)
|
||||
1. [Mambu](https://www.mambu.com/)
|
||||
1. [Lytt](https://www.lytt.co/)
|
||||
1. [Major League Baseball](https://mlb.com)
|
||||
1. [Mambu](https://www.mambu.com/)
|
||||
1. [Max Kelsen](https://www.maxkelsen.com/)
|
||||
1. [Mirantis](https://mirantis.com/)
|
||||
1. [OpenSaaS Studio](https://opensaas.studio)
|
||||
1. [Optoro](https://www.optoro.com/)
|
||||
1. [Peloton Interactive](https://www.onepeloton.com/)
|
||||
1. [Pipefy](https://www.pipefy.com/)
|
||||
1. [Riskified](https://www.riskified.com/)
|
||||
1. [Red Hat](https://www.redhat.com/)
|
||||
1. [Saildrone](https://www.saildrone.com/)
|
||||
1. [Saloodo! GmbH](https://www.saloodo.com)
|
||||
1. [Syncier](https://syncier.com/)
|
||||
1. [Tesla](https://tesla.com/)
|
||||
1. [Tiger Analytics](https://www.tigeranalytics.com/)
|
||||
1. [tZERO](https://www.tzero.com/)
|
||||
1. [Ticketmaster](https://ticketmaster.com)
|
||||
1. [Twilio SendGrid](https://sendgrid.com)
|
||||
1. [Yieldlab](https://www.yieldlab.de/)
|
||||
1. [UBIO](https://ub.io/)
|
||||
1. [Universidad Mesoamericana](https://www.umes.edu.gt/)
|
||||
1. [Viaduct](https://www.viaduct.ai/)
|
||||
1. [Volvo Cars](https://www.volvocars.com/)
|
||||
1. [Walkbase](https://www.walkbase.com/)
|
||||
|
||||
## 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/)
|
||||
@@ -54,3 +85,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)
|
||||
|
||||
@@ -17,6 +17,7 @@ p, role:admin, applications, update, */*, allow
|
||||
p, role:admin, applications, delete, */*, allow
|
||||
p, role:admin, applications, sync, */*, allow
|
||||
p, role:admin, applications, override, */*, allow
|
||||
p, role:admin, applications, action/*, */*, allow
|
||||
p, role:admin, certificates, create, *, allow
|
||||
p, role:admin, certificates, update, *, allow
|
||||
p, role:admin, certificates, delete, *, allow
|
||||
|
||||
|
@@ -21,8 +21,9 @@ import (
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
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"
|
||||
)
|
||||
@@ -46,7 +47,8 @@ func newCommand() *cobra.Command {
|
||||
logLevel string
|
||||
glogLevel int
|
||||
metricsPort int
|
||||
cacheSrc func() (*cache.Cache, error)
|
||||
kubectlParallelismLimit int64
|
||||
cacheSrc func() (*appstatecache.Cache, error)
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: cliName,
|
||||
@@ -75,6 +77,7 @@ func newCommand() *cobra.Command {
|
||||
errors.CheckError(err)
|
||||
|
||||
settingsMgr := settings.NewSettingsManager(ctx, kubeClient, namespace)
|
||||
kubectl := &kube.KubectlCmd{}
|
||||
appController, err := controller.NewApplicationController(
|
||||
namespace,
|
||||
settingsMgr,
|
||||
@@ -82,9 +85,11 @@ func newCommand() *cobra.Command {
|
||||
appClient,
|
||||
repoClientset,
|
||||
cache,
|
||||
kubectl,
|
||||
resyncDuration,
|
||||
time.Duration(selfHealTimeoutSeconds)*time.Second,
|
||||
metricsPort)
|
||||
metricsPort,
|
||||
kubectlParallelismLimit)
|
||||
errors.CheckError(err)
|
||||
|
||||
log.Infof("Application Controller (version: %s) starting (namespace: %s)", common.GetVersion(), namespace)
|
||||
@@ -109,8 +114,9 @@ func newCommand() *cobra.Command {
|
||||
command.Flags().IntVar(&glogLevel, "gloglevel", 0, "Set the glog logging level")
|
||||
command.Flags().IntVar(&metricsPort, "metrics-port", common.DefaultPortArgoCDMetrics, "Start metrics server on given port")
|
||||
command.Flags().IntVar(&selfHealTimeoutSeconds, "self-heal-timeout-seconds", 5, "Specifies timeout between application self heal attempts")
|
||||
command.Flags().Int64Var(&kubectlParallelismLimit, "kubectl-parallelism-limit", 20, "Number of allowed concurrent kubectl fork/execs. Any value less the 1 means no limit.")
|
||||
|
||||
cacheSrc = cache.AddCacheFlagsToCmd(&command)
|
||||
cacheSrc = appstatecache.AddCacheFlagsToCmd(&command)
|
||||
return &command
|
||||
}
|
||||
|
||||
|
||||
@@ -7,17 +7,15 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/argo-cd/reposerver/metrics"
|
||||
|
||||
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"
|
||||
"github.com/argoproj/argo-cd/util/cache"
|
||||
reposervercache "github.com/argoproj/argo-cd/reposerver/cache"
|
||||
"github.com/argoproj/argo-cd/reposerver/metrics"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
"github.com/argoproj/argo-cd/util/tls"
|
||||
)
|
||||
@@ -33,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{
|
||||
@@ -48,7 +46,7 @@ func newCommand() *cobra.Command {
|
||||
cache, err := cacheSrc()
|
||||
errors.CheckError(err)
|
||||
|
||||
metricsServer := metrics.NewMetricsServer(git.NewFactory())
|
||||
metricsServer := metrics.NewMetricsServer()
|
||||
server, err := reposerver.NewServer(metricsServer, cache, tlsConfigCustomizer, parallelismLimit)
|
||||
errors.CheckError(err)
|
||||
|
||||
@@ -74,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
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
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 +35,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,
|
||||
@@ -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,9 +25,8 @@ 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"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/dex"
|
||||
@@ -73,6 +73,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 +109,7 @@ func NewRunDexCommand() *cobra.Command {
|
||||
} else {
|
||||
err = ioutil.WriteFile("/tmp/dex.yaml", dexCfgBytes, 0644)
|
||||
errors.CheckError(err)
|
||||
log.Info(string(dexCfgBytes))
|
||||
log.Info(redactor(string(dexCfgBytes)))
|
||||
cmd = exec.Command("dex", "serve", "/tmp/dex.yaml")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
@@ -219,6 +220,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 +243,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 +297,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,11 +437,13 @@ 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
|
||||
}
|
||||
@@ -449,25 +461,27 @@ func getReferencedSecrets(un unstructured.Unstructured) map[string]bool {
|
||||
}
|
||||
}
|
||||
}
|
||||
if helmReposRAW, ok := cm.Data["helm.repositories"]; ok {
|
||||
helmRepoCreds := make([]settings.HelmRepoCredentials, 0)
|
||||
err := yaml.Unmarshal([]byte(helmReposRAW), &helmRepoCreds)
|
||||
|
||||
// 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 helmRepoCreds {
|
||||
if cred.CASecret != nil {
|
||||
referencedSecrets[cred.CASecret.Name] = true
|
||||
for _, cred := range creds {
|
||||
if cred.PasswordSecret != nil {
|
||||
referencedSecrets[cred.PasswordSecret.Name] = true
|
||||
}
|
||||
if cred.CertSecret != nil {
|
||||
referencedSecrets[cred.CertSecret.Name] = true
|
||||
}
|
||||
if cred.KeySecret != nil {
|
||||
referencedSecrets[cred.KeySecret.Name] = true
|
||||
if cred.SSHPrivateKeySecret != nil {
|
||||
referencedSecrets[cred.SSHPrivateKeySecret.Name] = true
|
||||
}
|
||||
if cred.UsernameSecret != nil {
|
||||
referencedSecrets[cred.UsernameSecret.Name] = true
|
||||
}
|
||||
if cred.PasswordSecret != nil {
|
||||
referencedSecrets[cred.PasswordSecret.Name] = true
|
||||
if cred.TLSClientCertDataSecret != nil {
|
||||
referencedSecrets[cred.TLSClientCertDataSecret.Name] = true
|
||||
}
|
||||
if cred.TLSClientCertKeySecret != nil {
|
||||
referencedSecrets[cred.TLSClientCertKeySecret.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -499,6 +513,57 @@ 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", "Application":
|
||||
leftSpec, _, _ := unstructured.NestedMap(left.Object, "spec")
|
||||
rightSpec, _, _ := unstructured.NestedMap(right.Object, "spec")
|
||||
return reflect.DeepEqual(leftSpec, rightSpec)
|
||||
}
|
||||
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", "Application":
|
||||
newLive.Object["spec"] = bak.Object["spec"]
|
||||
}
|
||||
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()
|
||||
@@ -554,6 +619,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" {
|
||||
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)
|
||||
|
||||
76
cmd/argocd-util/main_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
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
|
||||
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
|
||||
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")
|
||||
}
|
||||
@@ -2,16 +2,22 @@ package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
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"
|
||||
@@ -27,6 +33,8 @@ func NewAccountCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
},
|
||||
}
|
||||
command.AddCommand(NewAccountUpdatePasswordCommand(clientOpts))
|
||||
command.AddCommand(NewAccountGetUserInfoCommand(clientOpts))
|
||||
command.AddCommand(NewAccountCanICommand(clientOpts))
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -93,3 +101,86 @@ func NewAccountUpdatePasswordCommand(clientOpts *argocdclient.ClientOptions) *co
|
||||
command.Flags().StringVar(&newPassword, "new-password", "", "new password you want to update to")
|
||||
return command
|
||||
}
|
||||
|
||||
func NewAccountGetUserInfoCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "get-user-info",
|
||||
Short: "Get user info",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
conn, client := argocdclient.NewClientOrDie(clientOpts).NewSessionClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
ctx := context.Background()
|
||||
response, err := client.GetUserInfo(ctx, &session.GetUserInfoRequest{})
|
||||
errors.CheckError(err)
|
||||
|
||||
switch output {
|
||||
case "yaml":
|
||||
yamlBytes, err := yaml.Marshal(response)
|
||||
errors.CheckError(err)
|
||||
fmt.Println(string(yamlBytes))
|
||||
case "json":
|
||||
jsonBytes, err := json.MarshalIndent(response, "", " ")
|
||||
errors.CheckError(err)
|
||||
fmt.Println(string(jsonBytes))
|
||||
case "":
|
||||
fmt.Printf("Logged In: %v\n", response.LoggedIn)
|
||||
if response.LoggedIn {
|
||||
fmt.Printf("Username: %s\n", response.Username)
|
||||
fmt.Printf("Issuer: %s\n", response.Iss)
|
||||
fmt.Printf("Groups: %v\n", strings.Join(response.Groups, ","))
|
||||
}
|
||||
default:
|
||||
log.Fatalf("Unknown output format: %s", output)
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "", "Output format. One of: yaml, json")
|
||||
return command
|
||||
}
|
||||
|
||||
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 cluster '*'
|
||||
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,30 @@ package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
applicationpkg "github.com/argoproj/argo-cd/pkg/apiclient/application"
|
||||
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
)
|
||||
|
||||
type DisplayedAction struct {
|
||||
Group string
|
||||
Kind string
|
||||
Name string
|
||||
Action string
|
||||
Disabled bool
|
||||
}
|
||||
|
||||
// NewApplicationResourceActionsCommand returns a new instance of an `argocd app actions` command
|
||||
func NewApplicationResourceActionsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
@@ -37,7 +47,7 @@ func NewApplicationResourceActionsListCommand(clientOpts *argocdclient.ClientOpt
|
||||
var kind string
|
||||
var group string
|
||||
var resourceName string
|
||||
var all bool
|
||||
var output string
|
||||
var command = &cobra.Command{
|
||||
Use: "list APPNAME",
|
||||
Short: "Lists available actions on a resource",
|
||||
@@ -53,8 +63,8 @@ func NewApplicationResourceActionsListCommand(clientOpts *argocdclient.ClientOpt
|
||||
ctx := context.Background()
|
||||
resources, err := appIf.ManagedResources(ctx, &applicationpkg.ResourcesQuery{ApplicationName: &appName})
|
||||
errors.CheckError(err)
|
||||
filteredObjects := filterResources(command, resources.Items, group, kind, namespace, resourceName, all)
|
||||
availableActions := make(map[string][]argoappv1.ResourceAction)
|
||||
filteredObjects := filterResources(command, resources.Items, group, kind, namespace, resourceName, true)
|
||||
var availableActions []DisplayedAction
|
||||
for i := range filteredObjects {
|
||||
obj := filteredObjects[i]
|
||||
gvk := obj.GroupVersionKind()
|
||||
@@ -66,34 +76,42 @@ func NewApplicationResourceActionsListCommand(clientOpts *argocdclient.ClientOpt
|
||||
Kind: gvk.Kind,
|
||||
})
|
||||
errors.CheckError(err)
|
||||
availableActions[obj.GetName()] = availActionsForResource.Actions
|
||||
}
|
||||
|
||||
var keys []string
|
||||
for key := range availableActions {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "RESOURCE\tACTION\n")
|
||||
fmt.Println()
|
||||
for key := range availableActions {
|
||||
for i := range availableActions[key] {
|
||||
action := availableActions[key][i]
|
||||
fmt.Fprintf(w, "%s\t%s\n", key, action.Name)
|
||||
|
||||
for _, action := range availActionsForResource.Actions {
|
||||
displayAction := DisplayedAction{
|
||||
Group: gvk.Group,
|
||||
Kind: gvk.Kind,
|
||||
Name: obj.GetName(),
|
||||
Action: action.Name,
|
||||
Disabled: action.Disabled,
|
||||
}
|
||||
availableActions = append(availableActions, displayAction)
|
||||
}
|
||||
}
|
||||
_ = w.Flush()
|
||||
|
||||
switch output {
|
||||
case "yaml":
|
||||
yamlBytes, err := yaml.Marshal(availableActions)
|
||||
errors.CheckError(err)
|
||||
fmt.Println(string(yamlBytes))
|
||||
case "json":
|
||||
jsonBytes, err := json.MarshalIndent(availableActions, "", " ")
|
||||
errors.CheckError(err)
|
||||
fmt.Println(string(jsonBytes))
|
||||
case "":
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "GROUP\tKIND\tNAME\tACTION\tDISABLED\n")
|
||||
fmt.Println()
|
||||
for _, action := range availableActions {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", action.Group, action.Kind, action.Name, action.Action, strconv.FormatBool(action.Disabled))
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
}
|
||||
command.Flags().StringVar(&resourceName, "resource-name", "", "Name of resource")
|
||||
command.Flags().StringVar(&kind, "kind", "", "Kind")
|
||||
err := command.MarkFlagRequired("kind")
|
||||
errors.CheckError(err)
|
||||
command.Flags().StringVar(&group, "group", "", "Group")
|
||||
command.Flags().StringVar(&namespace, "namespace", "", "Namespace")
|
||||
command.Flags().BoolVar(&all, "all", false, "Indicates whether to list actions on multiple matching resources")
|
||||
command.Flags().StringVarP(&output, "out", "o", "", "Output format. One of: yaml, json")
|
||||
|
||||
return command
|
||||
}
|
||||
@@ -101,9 +119,9 @@ func NewApplicationResourceActionsListCommand(clientOpts *argocdclient.ClientOpt
|
||||
// NewApplicationResourceActionsRunCommand returns a new instance of an `argocd app actions run` command
|
||||
func NewApplicationResourceActionsRunCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var namespace string
|
||||
var resourceName string
|
||||
var kind string
|
||||
var group string
|
||||
var resourceName string
|
||||
var all bool
|
||||
var command = &cobra.Command{
|
||||
Use: "run APPNAME ACTION",
|
||||
@@ -111,11 +129,10 @@ func NewApplicationResourceActionsRunCommand(clientOpts *argocdclient.ClientOpti
|
||||
}
|
||||
|
||||
command.Flags().StringVar(&resourceName, "resource-name", "", "Name of resource")
|
||||
command.Flags().StringVar(&kind, "kind", "", "Kind")
|
||||
err := command.MarkFlagRequired("kind")
|
||||
errors.CheckError(err)
|
||||
command.Flags().StringVar(&group, "group", "", "Group")
|
||||
command.Flags().StringVar(&namespace, "namespace", "", "Namespace")
|
||||
command.Flags().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) {
|
||||
@@ -125,12 +142,20 @@ func NewApplicationResourceActionsRunCommand(clientOpts *argocdclient.ClientOpti
|
||||
}
|
||||
appName := args[0]
|
||||
actionName := args[1]
|
||||
|
||||
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
|
||||
defer util.Close(conn)
|
||||
ctx := context.Background()
|
||||
resources, err := appIf.ManagedResources(ctx, &applicationpkg.ResourcesQuery{ApplicationName: &appName})
|
||||
errors.CheckError(err)
|
||||
filteredObjects := filterResources(command, resources.Items, group, kind, namespace, resourceName, all)
|
||||
var resGroup = filteredObjects[0].GroupVersionKind().Group
|
||||
for i := range filteredObjects[1:] {
|
||||
if filteredObjects[i].GroupVersionKind().Group != resGroup {
|
||||
log.Fatal("Ambiguous resource group. Use flag --group to specify resource group explicitly.")
|
||||
}
|
||||
}
|
||||
|
||||
for i := range filteredObjects {
|
||||
obj := filteredObjects[i]
|
||||
gvk := obj.GroupVersionKind()
|
||||
|
||||
@@ -4,22 +4,34 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
func TestParseLabels(t *testing.T) {
|
||||
validLabels := []string{"key=value", "foo=bar", "intuit=inc"}
|
||||
|
||||
result, err := parseLabels(validLabels)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, result, 3)
|
||||
|
||||
invalidLabels := []string{"key=value", "too=many=equals"}
|
||||
_, err = parseLabels(invalidLabels)
|
||||
assert.Error(t, err)
|
||||
|
||||
emptyLabels := []string{}
|
||||
result, err = parseLabels(emptyLabels)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, result, 0)
|
||||
|
||||
func Test_setHelmOpt(t *testing.T) {
|
||||
t.Run("Zero", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setHelmOpt(&src, helmOpts{})
|
||||
assert.Nil(t, src.Helm)
|
||||
})
|
||||
t.Run("ValueFiles", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setHelmOpt(&src, helmOpts{valueFiles: []string{"foo"}})
|
||||
assert.Equal(t, []string{"foo"}, src.Helm.ValueFiles)
|
||||
})
|
||||
t.Run("ReleaseName", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setHelmOpt(&src, helmOpts{releaseName: "foo"})
|
||||
assert.Equal(t, "foo", src.Helm.ReleaseName)
|
||||
})
|
||||
t.Run("HelmSets", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setHelmOpt(&src, helmOpts{helmSets: []string{"foo=bar"}})
|
||||
assert.Equal(t, []v1alpha1.HelmParameter{{Name: "foo", Value: "bar"}}, src.Helm.Parameters)
|
||||
})
|
||||
t.Run("HelmSetStrings", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setHelmOpt(&src, helmOpts{helmSetStrings: []string{"foo=bar"}})
|
||||
assert.Equal(t, []v1alpha1.HelmParameter{{Name: "foo", Value: "bar", ForceString: true}}, src.Helm.Parameters)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
@@ -239,6 +257,7 @@ func NewCertListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
certType string
|
||||
hostNamePattern string
|
||||
sortOrder string
|
||||
output string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "list",
|
||||
@@ -258,11 +277,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))
|
||||
@@ -52,9 +63,10 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
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 +77,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 +101,12 @@ 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)
|
||||
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,9 +114,9 @@ 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")
|
||||
@@ -112,6 +125,7 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
command.Flags().StringVar(&awsClusterName, "aws-cluster-name", "", "AWS Cluster name if set then aws-iam-authenticator will be used to access cluster")
|
||||
command.Flags().StringVar(&awsRoleArn, "aws-role-arn", "", "Optional AWS role arn. If set then AWS IAM Authenticator assume a role to perform cluster operations instead of the default AWS credential provider chain.")
|
||||
command.Flags().StringVar(&systemNamespace, "system-namespace", common.DefaultSystemNamespace, "Use different system namespace")
|
||||
command.Flags().StringArrayVar(&namespaces, "namespace", nil, "List of namespaces which are allowed to manage")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -154,7 +168,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 +180,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 +194,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 +208,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)
|
||||
@@ -232,9 +296,13 @@ func NewClusterRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
// Print table of cluster information
|
||||
func printClusterTable(clusters []argoappv1.Cluster) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "SERVER\tNAME\tSTATUS\tMESSAGE\n")
|
||||
_, _ = fmt.Fprintf(w, "SERVER\tNAME\tVERSION\tSTATUS\tMESSAGE\n")
|
||||
for _, c := range clusters {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", c.Server, c.Name, 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 +327,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)
|
||||
|
||||
31
cmd/argocd/commands/cluster_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
func Test_printClusterTable(t *testing.T) {
|
||||
printClusterTable([]v1alpha1.Cluster{
|
||||
{
|
||||
Server: "my-server",
|
||||
Name: "my-name",
|
||||
Config: v1alpha1.ClusterConfig{
|
||||
Username: "my-username",
|
||||
Password: "my-password",
|
||||
BearerToken: "my-bearer-token",
|
||||
TLSClientConfig: v1alpha1.TLSClientConfig{},
|
||||
AWSAuthConfig: nil,
|
||||
},
|
||||
ConnectionState: v1alpha1.ConnectionState{
|
||||
Status: "my-status",
|
||||
Message: "my-message",
|
||||
ModifiedAt: &metav1.Time{},
|
||||
},
|
||||
ServerVersion: "my-version",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -92,7 +92,7 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
errors.CheckError(err)
|
||||
oauth2conf, provider, err := acdClient.OIDCConfig(ctx, acdSet)
|
||||
errors.CheckError(err)
|
||||
tokenString, refreshToken = oauth2Login(ctx, ssoPort, oauth2conf, provider)
|
||||
tokenString, refreshToken = oauth2Login(ctx, ssoPort, acdSet.GetOIDCConfig(), oauth2conf, provider)
|
||||
}
|
||||
|
||||
parser := &jwt.Parser{
|
||||
@@ -154,7 +154,7 @@ func userDisplayName(claims jwt.MapClaims) string {
|
||||
|
||||
// oauth2Login opens a browser, runs a temporary HTTP server to delegate OAuth2 login flow and
|
||||
// returns the JWT token and a refresh token (if supported)
|
||||
func oauth2Login(ctx context.Context, port int, oauth2conf *oauth2.Config, provider *oidc.Provider) (string, string) {
|
||||
func oauth2Login(ctx context.Context, port int, oidcSettings *settingspkg.OIDCConfig, oauth2conf *oauth2.Config, provider *oidc.Provider) (string, string) {
|
||||
oauth2conf.RedirectURL = fmt.Sprintf("http://localhost:%d/auth/callback", port)
|
||||
oidcConf, err := oidcutil.ParseConfig(provider)
|
||||
errors.CheckError(err)
|
||||
@@ -243,22 +243,28 @@ func oauth2Login(ctx context.Context, port int, oauth2conf *oauth2.Config, provi
|
||||
fmt.Printf("Opening browser for authentication\n")
|
||||
|
||||
var url string
|
||||
grantType := oidcutil.InferGrantType(oauth2conf, oidcConf)
|
||||
grantType := oidcutil.InferGrantType(oidcConf)
|
||||
opts := []oauth2.AuthCodeOption{oauth2.AccessTypeOffline}
|
||||
if claimsRequested := oidcSettings.GetIDTokenClaims(); claimsRequested != nil {
|
||||
opts = oidcutil.AppendClaimsAuthenticationRequestParameter(opts, claimsRequested)
|
||||
}
|
||||
|
||||
switch grantType {
|
||||
case oidcutil.GrantTypeAuthorizationCode:
|
||||
url = oauth2conf.AuthCodeURL(stateNonce, oauth2.AccessTypeOffline)
|
||||
url = oauth2conf.AuthCodeURL(stateNonce, opts...)
|
||||
case oidcutil.GrantTypeImplicit:
|
||||
url = oidcutil.ImplicitFlowURL(oauth2conf, stateNonce, oauth2.AccessTypeOffline)
|
||||
url = oidcutil.ImplicitFlowURL(oauth2conf, stateNonce, opts...)
|
||||
default:
|
||||
log.Fatalf("Unsupported grant type: %v", grantType)
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
@@ -16,6 +18,7 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/utils/pointer"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
@@ -23,13 +26,16 @@ import (
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/config"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
)
|
||||
|
||||
type projectOpts struct {
|
||||
description string
|
||||
destinations []string
|
||||
sources []string
|
||||
description string
|
||||
destinations []string
|
||||
sources []string
|
||||
orphanedResourcesEnabled bool
|
||||
orphanedResourcesWarn bool
|
||||
}
|
||||
|
||||
type policyOpts struct {
|
||||
@@ -79,6 +85,7 @@ func NewProjectCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
command.AddCommand(NewProjectDenyClusterResourceCommand(clientOpts))
|
||||
command.AddCommand(NewProjectAllowNamespaceResourceCommand(clientOpts))
|
||||
command.AddCommand(NewProjectDenyNamespaceResourceCommand(clientOpts))
|
||||
command.AddCommand(NewProjectWindowsCommand(clientOpts))
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -86,7 +93,21 @@ func addProjFlags(command *cobra.Command, opts *projectOpts) {
|
||||
command.Flags().StringVarP(&opts.description, "description", "", "", "Project description")
|
||||
command.Flags().StringArrayVarP(&opts.destinations, "dest", "d", []string{},
|
||||
"Permitted destination server and namespace (e.g. https://192.168.99.100:8443,default)")
|
||||
command.Flags().StringArrayVarP(&opts.sources, "src", "s", []string{}, "Permitted git source repository URL")
|
||||
command.Flags().StringArrayVarP(&opts.sources, "src", "s", []string{}, "Permitted source repository URL")
|
||||
command.Flags().BoolVar(&opts.orphanedResourcesEnabled, "orphaned-resources", false, "Enables orphaned resources monitoring")
|
||||
command.Flags().BoolVar(&opts.orphanedResourcesWarn, "orphaned-resources-warn", false, "Specifies if applications should be a warning condition when orphaned resources detected")
|
||||
}
|
||||
|
||||
func getOrphanedResourcesSettings(c *cobra.Command, opts projectOpts) *v1alpha1.OrphanedResourcesMonitorSettings {
|
||||
warnChanged := c.Flag("orphaned-resources-warn").Changed
|
||||
if opts.orphanedResourcesEnabled || warnChanged {
|
||||
settings := v1alpha1.OrphanedResourcesMonitorSettings{}
|
||||
if warnChanged {
|
||||
settings.Warn = pointer.BoolPtr(opts.orphanedResourcesWarn)
|
||||
}
|
||||
return &settings
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addPolicyFlags(command *cobra.Command, opts *policyOpts) {
|
||||
@@ -103,32 +124,63 @@ func humanizeTimestamp(epoch int64) string {
|
||||
// NewProjectCreateCommand returns a new instance of an `argocd proj create` command
|
||||
func NewProjectCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
opts projectOpts
|
||||
opts projectOpts
|
||||
fileURL string
|
||||
upsert bool
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "create PROJECT",
|
||||
Short: "Create a project",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
proj := v1alpha1.AppProject{
|
||||
ObjectMeta: v1.ObjectMeta{Name: projName},
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
Description: opts.description,
|
||||
Destinations: opts.GetDestinations(),
|
||||
SourceRepos: opts.sources,
|
||||
},
|
||||
var proj v1alpha1.AppProject
|
||||
if fileURL == "-" {
|
||||
// read stdin
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
err := config.UnmarshalReader(reader, &proj)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to read manifest from stdin: %v", err)
|
||||
}
|
||||
} else if fileURL != "" {
|
||||
// read uri
|
||||
parsedURL, err := url.ParseRequestURI(fileURL)
|
||||
if err != nil || !(parsedURL.Scheme == "http" || parsedURL.Scheme == "https") {
|
||||
err = config.UnmarshalLocalFile(fileURL, &proj)
|
||||
} else {
|
||||
err = config.UnmarshalRemoteFile(fileURL, &proj)
|
||||
}
|
||||
errors.CheckError(err)
|
||||
if len(args) == 1 && args[0] != proj.Name {
|
||||
log.Fatalf("project name '%s' does not match project spec metadata.name '%s'", args[0], proj.Name)
|
||||
}
|
||||
} else {
|
||||
// read arguments
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
proj = v1alpha1.AppProject{
|
||||
ObjectMeta: v1.ObjectMeta{Name: projName},
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
Description: opts.description,
|
||||
Destinations: opts.GetDestinations(),
|
||||
SourceRepos: opts.sources,
|
||||
OrphanedResources: getOrphanedResourcesSettings(c, opts),
|
||||
},
|
||||
}
|
||||
}
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
_, err := projIf.Create(context.Background(), &projectpkg.ProjectCreateRequest{Project: &proj})
|
||||
_, err := projIf.Create(context.Background(), &projectpkg.ProjectCreateRequest{Project: &proj, Upsert: upsert})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
command.Flags().BoolVar(&upsert, "upsert", false, "Allows to override a project with the same name even if supplied project spec is different from existing spec")
|
||||
command.Flags().StringVarP(&fileURL, "file", "f", "", "Filename or URL to Kubernetes manifests for the project")
|
||||
err := command.Flags().SetAnnotation("file", cobra.BashCompFilenameExt, []string{"json", "yaml", "yml"})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
addProjFlags(command, &opts)
|
||||
return command
|
||||
}
|
||||
@@ -163,6 +215,8 @@ func NewProjectSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
proj.Spec.Destinations = opts.GetDestinations()
|
||||
case "src":
|
||||
proj.Spec.SourceRepos = opts.sources
|
||||
case "orphaned-resources", "orphaned-resources-warn":
|
||||
proj.Spec.OrphanedResources = getOrphanedResourcesSettings(c, opts)
|
||||
}
|
||||
})
|
||||
if visited == 0 {
|
||||
@@ -453,7 +507,7 @@ func printProjectNames(projects []v1alpha1.AppProject) {
|
||||
// Print table of project info
|
||||
func printProjectTable(projects []v1alpha1.AppProject) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "NAME\tDESCRIPTION\tDESTINATIONS\tSOURCES\tCLUSTER-RESOURCE-WHITELIST\tNAMESPACE-RESOURCE-BLACKLIST\n")
|
||||
fmt.Fprintf(w, "NAME\tDESCRIPTION\tDESTINATIONS\tSOURCES\tCLUSTER-RESOURCE-WHITELIST\tNAMESPACE-RESOURCE-BLACKLIST\tORPHANED-RESOURCES\n")
|
||||
for _, p := range projects {
|
||||
printProjectLine(w, &p)
|
||||
}
|
||||
@@ -473,17 +527,30 @@ 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
|
||||
}
|
||||
|
||||
func formatOrphanedResources(p *v1alpha1.AppProject) string {
|
||||
if p.Spec.OrphanedResources == nil {
|
||||
return "disabled"
|
||||
}
|
||||
return fmt.Sprintf("enabled (warn=%v)", p.Spec.OrphanedResources.IsWarn())
|
||||
}
|
||||
|
||||
func printProjectLine(w io.Writer, p *v1alpha1.AppProject) {
|
||||
var destinations, sourceRepos, clusterWhitelist, namespaceBlacklist string
|
||||
switch len(p.Spec.Destinations) {
|
||||
@@ -516,12 +583,63 @@ func printProjectLine(w io.Writer, p *v1alpha1.AppProject) {
|
||||
default:
|
||||
namespaceBlacklist = fmt.Sprintf("%d resources", len(p.Spec.NamespaceResourceBlacklist))
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%v\t%v\t%v\t%v\n", p.Name, p.Spec.Description, destinations, sourceRepos, clusterWhitelist, namespaceBlacklist)
|
||||
fmt.Fprintf(w, "%s\t%s\t%v\t%v\t%v\t%v\t%v\n", p.Name, p.Spec.Description, destinations, sourceRepos, clusterWhitelist, namespaceBlacklist, formatOrphanedResources(p))
|
||||
}
|
||||
|
||||
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",
|
||||
@@ -535,50 +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)
|
||||
}
|
||||
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))
|
||||
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))
|
||||
}
|
||||
},
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
322
cmd/argocd/commands/projectwindows.go
Normal file
@@ -0,0 +1,322 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"strconv"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
projectpkg "github.com/argoproj/argo-cd/pkg/apiclient/project"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
)
|
||||
|
||||
// NewProjectWindowsCommand returns a new instance of the `argocd proj windows` command
|
||||
func NewProjectWindowsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
roleCommand := &cobra.Command{
|
||||
Use: "windows",
|
||||
Short: "Manage a project's sync windows",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
},
|
||||
}
|
||||
roleCommand.AddCommand(NewProjectWindowsDisableManualSyncCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectWindowsEnableManualSyncCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectWindowsAddWindowCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectWindowsDeleteCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectWindowsListCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectWindowsUpdateCommand(clientOpts))
|
||||
return roleCommand
|
||||
}
|
||||
|
||||
// NewProjectSyncWindowsDisableManualSyncCommand returns a new instance of an `argocd proj windows disable-manual-sync` command
|
||||
func NewProjectWindowsDisableManualSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "disable-manual-sync PROJECT ID",
|
||||
Short: "Disable manual sync for a sync window",
|
||||
Long: "Disable manual sync for a sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
projName := args[0]
|
||||
id, err := strconv.Atoi(args[1])
|
||||
errors.CheckError(err)
|
||||
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
for i, window := range proj.Spec.SyncWindows {
|
||||
if id == i {
|
||||
window.ManualSync = false
|
||||
}
|
||||
}
|
||||
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectWindowsEnableManualSyncCommand returns a new instance of an `argocd proj windows enable-manual-sync` command
|
||||
func NewProjectWindowsEnableManualSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "enable-manual-sync PROJECT ID",
|
||||
Short: "Enable manual sync for a sync window",
|
||||
Long: "Enable manual sync for a sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
projName := args[0]
|
||||
id, err := strconv.Atoi(args[1])
|
||||
errors.CheckError(err)
|
||||
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
for i, window := range proj.Spec.SyncWindows {
|
||||
if id == i {
|
||||
window.ManualSync = true
|
||||
}
|
||||
}
|
||||
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectWindowsAddWindowCommand returns a new instance of an `argocd proj windows add` command
|
||||
func NewProjectWindowsAddWindowCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
kind string
|
||||
schedule string
|
||||
duration string
|
||||
applications []string
|
||||
namespaces []string
|
||||
clusters []string
|
||||
manualSync bool
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "add PROJECT",
|
||||
Short: "Add a sync window to a project",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
err = proj.Spec.AddWindow(kind, schedule, duration, applications, namespaces, clusters, manualSync)
|
||||
errors.CheckError(err)
|
||||
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&kind, "kind", "k", "", "Sync window kind, either allow or deny")
|
||||
command.Flags().StringVar(&schedule, "schedule", "", "Sync window schedule in cron format. (e.g. --schedule \"0 22 * * *\")")
|
||||
command.Flags().StringVar(&duration, "duration", "", "Sync window duration. (e.g. --duration 1h)")
|
||||
command.Flags().StringSliceVar(&applications, "applications", []string{}, "Applications that the schedule will be applied to. Comma separated, wildcards supported (e.g. --applications prod-\\*,website)")
|
||||
command.Flags().StringSliceVar(&namespaces, "namespaces", []string{}, "Namespaces that the schedule will be applied to. Comma separated, wildcards supported (e.g. --namespaces default,\\*-prod)")
|
||||
command.Flags().StringSliceVar(&clusters, "clusters", []string{}, "Clusters that the schedule will be applied to. Comma separated, wildcards supported (e.g. --clusters prod,staging)")
|
||||
command.Flags().BoolVar(&manualSync, "manual-sync", false, "Allow manual syncs for both deny and allow windows")
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectWindowsAddWindowCommand returns a new instance of an `argocd proj windows delete` command
|
||||
func NewProjectWindowsDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "delete PROJECT ID",
|
||||
Short: "Delete a sync window from a project. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
projName := args[0]
|
||||
id, err := strconv.Atoi(args[1])
|
||||
errors.CheckError(err)
|
||||
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
err = proj.Spec.DeleteWindow(id)
|
||||
errors.CheckError(err)
|
||||
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectWindowsUpdateCommand returns a new instance of an `argocd proj windows update` command
|
||||
func NewProjectWindowsUpdateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
schedule string
|
||||
duration string
|
||||
applications []string
|
||||
namespaces []string
|
||||
clusters []string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "update PROJECT ID",
|
||||
Short: "Update a project sync window",
|
||||
Long: "Update a project sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
projName := args[0]
|
||||
id, err := strconv.Atoi(args[1])
|
||||
errors.CheckError(err)
|
||||
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
for i, window := range proj.Spec.SyncWindows {
|
||||
if id == i {
|
||||
err := window.Update(schedule, duration, applications, namespaces, clusters)
|
||||
if err != nil {
|
||||
errors.CheckError(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&schedule, "schedule", "", "Sync window schedule in cron format. (e.g. --schedule \"0 22 * * *\")")
|
||||
command.Flags().StringVar(&duration, "duration", "", "Sync window duration. (e.g. --duration 1h)")
|
||||
command.Flags().StringSliceVar(&applications, "applications", []string{}, "Applications that the schedule will be applied to. Comma separated, wildcards supported (e.g. --applications prod-\\*,website)")
|
||||
command.Flags().StringSliceVar(&namespaces, "namespaces", []string{}, "Namespaces that the schedule will be applied to. Comma separated, wildcards supported (e.g. --namespaces default,\\*-prod)")
|
||||
command.Flags().StringSliceVar(&clusters, "clusters", []string{}, "Clusters that the schedule will be applied to. Comma separated, wildcards supported (e.g. --clusters prod,staging)")
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectWindowsListCommand returns a new instance of an `argocd proj windows list` command
|
||||
func NewProjectWindowsListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
output string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "list PROJECT",
|
||||
Short: "List project sync windows",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
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
|
||||
}
|
||||
|
||||
// Print table of sync window data
|
||||
func printSyncWindows(proj *v1alpha1.AppProject) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
var fmtStr string
|
||||
headers := []interface{}{"ID", "STATUS", "KIND", "SCHEDULE", "DURATION", "APPLICATIONS", "NAMESPACES", "CLUSTERS", "MANUALSYNC"}
|
||||
fmtStr = "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n"
|
||||
fmt.Fprintf(w, fmtStr, headers...)
|
||||
if proj.Spec.SyncWindows.HasWindows() {
|
||||
for i, window := range proj.Spec.SyncWindows {
|
||||
vals := []interface{}{
|
||||
strconv.Itoa(i),
|
||||
formatBoolOutput(window.Active()),
|
||||
window.Kind,
|
||||
window.Schedule,
|
||||
window.Duration,
|
||||
formatListOutput(window.Applications),
|
||||
formatListOutput(window.Namespaces),
|
||||
formatListOutput(window.Clusters),
|
||||
formatManualOutput(window.ManualSync),
|
||||
}
|
||||
fmt.Fprintf(w, fmtStr, vals...)
|
||||
}
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
|
||||
func formatListOutput(list []string) string {
|
||||
var o string
|
||||
if len(list) == 0 {
|
||||
o = "-"
|
||||
} else {
|
||||
o = strings.Join(list, ",")
|
||||
}
|
||||
return o
|
||||
}
|
||||
func formatBoolOutput(active bool) string {
|
||||
var o string
|
||||
if active {
|
||||
o = "Active"
|
||||
} else {
|
||||
o = "Inactive"
|
||||
}
|
||||
return o
|
||||
}
|
||||
func formatManualOutput(active bool) string {
|
||||
var o string
|
||||
if active {
|
||||
o = "Enabled"
|
||||
} else {
|
||||
o = "Disabled"
|
||||
}
|
||||
return o
|
||||
}
|
||||
@@ -67,7 +67,7 @@ func NewReloginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
errors.CheckError(err)
|
||||
oauth2conf, provider, err := acdClient.OIDCConfig(ctx, acdSet)
|
||||
errors.CheckError(err)
|
||||
tokenString, refreshToken = oauth2Login(ctx, ssoPort, oauth2conf, provider)
|
||||
tokenString, refreshToken = oauth2Login(ctx, ssoPort, acdSet.GetOIDCConfig(), oauth2conf, provider)
|
||||
}
|
||||
|
||||
localCfg.UpsertUser(localconfig.User{
|
||||
|
||||
@@ -10,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"
|
||||
@@ -23,7 +24,7 @@ import (
|
||||
func NewRepoCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "repo",
|
||||
Short: "Manage git repository connection parameters",
|
||||
Short: "Manage repository connection parameters",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
@@ -50,13 +51,20 @@ 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 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 +116,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,8 +139,14 @@ 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,
|
||||
Name: repo.Name,
|
||||
Username: repo.Username,
|
||||
Password: repo.Password,
|
||||
SshPrivateKey: repo.SSHPrivateKey,
|
||||
@@ -141,11 +161,14 @@ 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", 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)")
|
||||
@@ -162,7 +185,7 @@ Add a HTTPS repository using username/password without verifying the server's TL
|
||||
func NewRepoRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "rm REPO",
|
||||
Short: "Remove git repository credentials",
|
||||
Short: "Remove repository credentials",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
@@ -171,7 +194,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)
|
||||
}
|
||||
},
|
||||
@@ -180,23 +203,27 @@ func NewRepoRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
}
|
||||
|
||||
// Print table of repo info
|
||||
func printRepoTable(repos []appsv1.Repository) {
|
||||
func printRepoTable(repos appsv1.Repositories) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "REPO\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%v\t%v\t%s\t%s\t%s\n", 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
|
||||
func printRepoUrls(repos []appsv1.Repository) {
|
||||
// Print list of repo urls or url patterns for repository credentials
|
||||
func printRepoUrls(repos appsv1.Repositories) {
|
||||
for _, r := range repos {
|
||||
fmt.Println(r.Repo)
|
||||
}
|
||||
@@ -205,7 +232,8 @@ func printRepoUrls(repos []appsv1.Repository) {
|
||||
// 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",
|
||||
@@ -213,15 +241,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 repo` 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 repo 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 https://git.example.com/repos
|
||||
argocd repocreds add https://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 repo list` 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 rm` 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))
|
||||
@@ -59,5 +60,8 @@ func NewCommand() *cobra.Command {
|
||||
command.PersistentFlags().StringVar(&clientOpts.AuthToken, "auth-token", config.GetFlag("auth-token", ""), "Authentication token")
|
||||
command.PersistentFlags().BoolVar(&clientOpts.GRPCWeb, "grpc-web", config.GetBoolFlag("grpc-web"), "Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2.")
|
||||
command.PersistentFlags().StringVar(&logLevel, "loglevel", config.GetFlag("loglevel", "info"), "Set the logging level. One of: debug|info|warn|error")
|
||||
command.PersistentFlags().StringSliceVarP(&clientOpts.Headers, "header", "H", []string{}, "Sets additional header to all requests made by Argo CD CLI. (Can be repeated multiple times to add multiple headers, also supports comma separated headers)")
|
||||
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,57 +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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,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
|
||||
@@ -75,6 +76,8 @@ const (
|
||||
LoginEndpoint = "/auth/login"
|
||||
// CallbackEndpoint is Argo CD's final callback endpoint we reach after OAuth 2.0 login flow has been completed
|
||||
CallbackEndpoint = "/auth/callback"
|
||||
// DexCallbackEndpoint is Argo CD's final callback endpoint when Dex is configured
|
||||
DexCallbackEndpoint = "/api/dex/callback"
|
||||
// ArgoCDClientAppName is name of the Oauth client app used when registering our web app to dex
|
||||
ArgoCDClientAppName = "Argo CD"
|
||||
// ArgoCDClientAppID is the Oauth client ID we will use when registering our app to dex
|
||||
@@ -114,10 +117,6 @@ const (
|
||||
AnnotationKeyManagedBy = "managed-by"
|
||||
// AnnotationValueManagedByArgoCD is a 'managed-by' annotation value for resources managed by Argo CD
|
||||
AnnotationValueManagedByArgoCD = "argocd.argoproj.io"
|
||||
// AnnotationKeyHelmHook is the helm hook annotation
|
||||
AnnotationKeyHelmHook = "helm.sh/hook"
|
||||
// AnnotationValueHelmHookCRDInstall is a value of crd helm hook
|
||||
AnnotationValueHelmHookCRDInstall = "crd-install"
|
||||
// ResourcesFinalizerName the finalizer value which we inject to finalize deletion of an application
|
||||
ResourcesFinalizerName = "resources-finalizer.argocd.argoproj.io"
|
||||
)
|
||||
@@ -135,13 +134,17 @@ const (
|
||||
EnvVarSSHDataPath = "ARGOCD_SSH_DATA_PATH"
|
||||
// Overrides the location where TLS certificate for repo access data is stored
|
||||
EnvVarTLSDataPath = "ARGOCD_TLS_DATA_PATH"
|
||||
// Specifies number of git remote operations attempts count
|
||||
EnvGitAttemptsCount = "ARGOCD_GIT_ATTEMPTS_COUNT"
|
||||
// Overrides git submodule support, true by default
|
||||
EnvGitSubmoduleEnabled = "ARGOCD_GIT_MODULES_ENABLED"
|
||||
)
|
||||
|
||||
const (
|
||||
// MinClientVersion is the minimum client version that can interface with this API server.
|
||||
// When introducing breaking changes to the API or datastructures, this number should be bumped.
|
||||
// The value here may be lower than the current value in VERSION
|
||||
MinClientVersion = "1.0.0"
|
||||
MinClientVersion = "1.3.0"
|
||||
// CacheVersion is a objects version cached using util/cache/cache.go.
|
||||
// Number should be bumped in case of backward incompatible change to make sure cache is invalidated after upgrade.
|
||||
CacheVersion = "1.0.0"
|
||||
|
||||
@@ -7,11 +7,13 @@ import (
|
||||
"math"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/semaphore"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apierr "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -23,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"
|
||||
@@ -36,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"
|
||||
@@ -45,6 +50,8 @@ import (
|
||||
|
||||
const (
|
||||
updateOperationStateTimeout = 1 * time.Second
|
||||
// orphanedIndex contains application which monitor orphaned resources by namespace
|
||||
orphanedIndex = "orphaned"
|
||||
)
|
||||
|
||||
type CompareWith int
|
||||
@@ -62,29 +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
|
||||
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 {
|
||||
@@ -99,39 +114,50 @@ 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)
|
||||
kubectlCmd := kube.KubectlCmd{}
|
||||
ctrl := ApplicationController{
|
||||
cache: argoCache,
|
||||
namespace: namespace,
|
||||
kubeClientset: kubeClientset,
|
||||
kubectl: kubectlCmd,
|
||||
applicationClientset: applicationClientset,
|
||||
repoClientset: repoClientset,
|
||||
appRefreshQueue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
|
||||
appOperationQueue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
|
||||
db: db,
|
||||
statusRefreshTimeout: appResyncPeriod,
|
||||
refreshRequestedApps: make(map[string]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,
|
||||
}
|
||||
appInformer, appLister := ctrl.newApplicationInformerAndLister()
|
||||
projInformer := v1alpha1.NewAppProjectInformer(applicationClientset, namespace, appResyncPeriod, cache.Indexers{})
|
||||
if kubectlParallelismLimit > 0 {
|
||||
ctrl.kubectlSemaphore = semaphore.NewWeighted(kubectlParallelismLimit)
|
||||
}
|
||||
kubectl.SetOnKubectlRun(ctrl.onKubectlRun)
|
||||
appInformer, appLister, err := ctrl.newApplicationInformerAndLister()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
indexers := cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}
|
||||
projInformer := v1alpha1.NewAppProjectInformer(applicationClientset, namespace, appResyncPeriod, indexers)
|
||||
metricsAddr := fmt.Sprintf("0.0.0.0:%d", metricsPort)
|
||||
ctrl.metricsServer = metrics.NewMetricsServer(metricsAddr, appLister, func() error {
|
||||
_, err := kubeClientset.Discovery().ServerVersion()
|
||||
return err
|
||||
})
|
||||
stateCache := statecache.NewLiveStateCache(db, appInformer, ctrl.settingsMgr, kubectlCmd, ctrl.metricsServer, ctrl.handleAppUpdated)
|
||||
appStateManager := NewAppStateManager(db, applicationClientset, repoClientset, namespace, kubectlCmd, ctrl.settingsMgr, stateCache, projInformer, ctrl.metricsServer)
|
||||
stateCache := statecache.NewLiveStateCache(db, appInformer, ctrl.settingsMgr, kubectl, ctrl.metricsServer, ctrl.handleObjectUpdated)
|
||||
appStateManager := NewAppStateManager(db, applicationClientset, repoClientset, namespace, kubectl, ctrl.settingsMgr, stateCache, projInformer, ctrl.metricsServer)
|
||||
ctrl.appInformer = appInformer
|
||||
ctrl.appLister = appLister
|
||||
ctrl.projInformer = projInformer
|
||||
@@ -141,6 +167,23 @@ func NewApplicationController(
|
||||
return &ctrl, nil
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) onKubectlRun(command string) (util.Closer, error) {
|
||||
ctrl.metricsServer.IncKubectlExec(command)
|
||||
if ctrl.kubectlSemaphore != nil {
|
||||
if err := ctrl.kubectlSemaphore.Acquire(context.Background(), 1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ctrl.metricsServer.IncKubectlExecPending(command)
|
||||
}
|
||||
return util.NewCloser(func() error {
|
||||
if ctrl.kubectlSemaphore != nil {
|
||||
ctrl.kubectlSemaphore.Release(1)
|
||||
ctrl.metricsServer.DecKubectlExecPending(command)
|
||||
}
|
||||
return nil
|
||||
}), nil
|
||||
}
|
||||
|
||||
func isSelfReferencedApp(app *appv1.Application, ref v1.ObjectReference) bool {
|
||||
gvk := ref.GroupVersionKind()
|
||||
return ref.UID == app.UID &&
|
||||
@@ -150,23 +193,42 @@ func isSelfReferencedApp(app *appv1.Application, ref v1.ObjectReference) bool {
|
||||
gvk.Kind == application.ApplicationKind
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) handleAppUpdated(appName string, isManagedResource bool, ref v1.ObjectReference) {
|
||||
skipForceRefresh := false
|
||||
func (ctrl *ApplicationController) getAppProj(app *appv1.Application) (*appv1.AppProject, error) {
|
||||
return argo.GetAppProject(&app.Spec, applisters.NewAppProjectLister(ctrl.projInformer.GetIndexer()), ctrl.namespace)
|
||||
}
|
||||
|
||||
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
|
||||
func (ctrl *ApplicationController) handleObjectUpdated(managedByApp map[string]bool, ref v1.ObjectReference) {
|
||||
// if namespaced resource is not managed by any app it might be orphaned resource of some other apps
|
||||
if len(managedByApp) == 0 && ref.Namespace != "" {
|
||||
// retrieve applications which monitor orphaned resources in the same namespace and refresh them unless resource is blacklisted in app project
|
||||
if objs, err := ctrl.appInformer.GetIndexer().ByIndex(orphanedIndex, ref.Namespace); err == nil {
|
||||
for i := range objs {
|
||||
app, ok := objs[i].(*appv1.Application)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// exclude resource unless it is permitted in the app project. If project is not permitted then it is not controlled by the user and there is no point showing the warning.
|
||||
if proj, err := ctrl.getAppProj(app); err == nil && proj.IsGroupKindPermitted(ref.GroupVersionKind().GroupKind(), true) &&
|
||||
!isKnownOrphanedResourceExclusion(kube.NewResourceKey(ref.GroupVersionKind().Group, ref.GroupVersionKind().Kind, ref.Namespace, ref.Name)) {
|
||||
|
||||
managedByApp[app.Name] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for appName, isManagedResource := range managedByApp {
|
||||
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.
|
||||
continue
|
||||
}
|
||||
|
||||
if !skipForceRefresh {
|
||||
level := ComparisonWithNothing
|
||||
if isManagedResource {
|
||||
level = CompareWithRecent
|
||||
}
|
||||
ctrl.requestAppRefresh(appName, level)
|
||||
ctrl.requestAppRefresh(appName, &level, nil)
|
||||
}
|
||||
ctrl.appRefreshQueue.Add(fmt.Sprintf("%s/%s", ctrl.namespace, appName))
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) setAppManagedResources(a *appv1.Application, comparisonResult *comparisonResult) (*appv1.ApplicationTree, error) {
|
||||
@@ -185,11 +247,37 @@ func (ctrl *ApplicationController) setAppManagedResources(a *appv1.Application,
|
||||
return tree, ctrl.cache.SetAppManagedResources(a.Name, managedResources)
|
||||
}
|
||||
|
||||
// returns true of given resources exist in the namespace by default and not managed by the user
|
||||
func isKnownOrphanedResourceExclusion(key kube.ResourceKey) bool {
|
||||
if key.Namespace == "default" && key.Group == "" && key.Kind == kube.ServiceKind && key.Name == "kubernetes" {
|
||||
return true
|
||||
}
|
||||
if key.Group == "" && key.Kind == kube.ServiceAccountKind && key.Name == "default" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) getResourceTree(a *appv1.Application, managedResources []*appv1.ResourceDiff) (*appv1.ApplicationTree, error) {
|
||||
nodes := make([]appv1.ResourceNode, 0)
|
||||
|
||||
proj, err := argo.GetAppProject(&a.Spec, applisters.NewAppProjectLister(ctrl.projInformer.GetIndexer()), ctrl.namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orphanedNodesMap := make(map[kube.ResourceKey]appv1.ResourceNode)
|
||||
warnOrphaned := true
|
||||
if proj.Spec.OrphanedResources != nil {
|
||||
orphanedNodesMap, err = ctrl.stateCache.GetNamespaceTopLevelResources(a.Spec.Destination.Server, a.Spec.Destination.Namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
warnOrphaned = proj.Spec.OrphanedResources.IsWarn()
|
||||
}
|
||||
|
||||
for i := range managedResources {
|
||||
managedResource := managedResources[i]
|
||||
delete(orphanedNodesMap, kube.NewResourceKey(managedResource.Group, managedResource.Kind, managedResource.Namespace, managedResource.Name))
|
||||
var live = &unstructured.Unstructured{}
|
||||
err := json.Unmarshal([]byte(managedResource.LiveState), &live)
|
||||
if err != nil {
|
||||
@@ -212,16 +300,42 @@ func (ctrl *ApplicationController) getResourceTree(a *appv1.Application, managed
|
||||
},
|
||||
})
|
||||
} else {
|
||||
err := ctrl.stateCache.IterateHierarchy(a.Spec.Destination.Server, live, func(child appv1.ResourceNode) {
|
||||
err := ctrl.stateCache.IterateHierarchy(a.Spec.Destination.Server, kube.GetResourceKey(live), func(child appv1.ResourceNode, appName string) {
|
||||
nodes = append(nodes, child)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return &appv1.ApplicationTree{Nodes: nodes}, nil
|
||||
orphanedNodes := make([]appv1.ResourceNode, 0)
|
||||
for k := range orphanedNodesMap {
|
||||
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 != "" {
|
||||
if _, exists, err := ctrl.appInformer.GetIndexer().GetByKey(ctrl.namespace + "/" + appName); exists && err == nil {
|
||||
belongToAnotherApp = true
|
||||
}
|
||||
}
|
||||
if !belongToAnotherApp {
|
||||
orphanedNodes = append(orphanedNodes, child)
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
var conditions []appv1.ApplicationCondition
|
||||
if len(orphanedNodes) > 0 && warnOrphaned {
|
||||
conditions = []appv1.ApplicationCondition{{
|
||||
Type: appv1.ApplicationConditionOrphanedResourceWarning,
|
||||
Message: fmt.Sprintf("Application has %d orphaned resources", len(orphanedNodes)),
|
||||
}}
|
||||
}
|
||||
a.Status.SetConditions(conditions, map[appv1.ApplicationConditionType]bool{appv1.ApplicationConditionOrphanedResourceWarning: true})
|
||||
return &appv1.ApplicationTree{Nodes: nodes, OrphanedNodes: orphanedNodes}, nil
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) managedResources(comparisonResult *comparisonResult) ([]*appv1.ResourceDiff, error) {
|
||||
@@ -233,6 +347,7 @@ func (ctrl *ApplicationController) managedResources(comparisonResult *comparison
|
||||
Name: res.Name,
|
||||
Group: res.Group,
|
||||
Kind: res.Kind,
|
||||
Hook: res.Hook,
|
||||
}
|
||||
|
||||
target := res.Target
|
||||
@@ -244,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 {
|
||||
@@ -271,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
|
||||
}
|
||||
@@ -281,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())
|
||||
|
||||
@@ -307,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) {
|
||||
@@ -357,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,
|
||||
@@ -370,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
|
||||
@@ -383,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())
|
||||
|
||||
@@ -408,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
|
||||
@@ -441,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{}{
|
||||
@@ -535,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)
|
||||
}
|
||||
@@ -646,18 +825,35 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
defer func() {
|
||||
reconcileDuration := time.Since(startTime)
|
||||
ctrl.metricsServer.IncReconcile(origApp, reconcileDuration)
|
||||
logCtx := log.WithFields(log.Fields{"application": origApp.Name, "time_ms": reconcileDuration.Seconds() * 1e3, "level": comparisonLevel})
|
||||
logCtx := log.WithFields(log.Fields{
|
||||
"application": origApp.Name,
|
||||
"time_ms": reconcileDuration.Seconds() * 1e3,
|
||||
"level": comparisonLevel,
|
||||
"dest-server": origApp.Spec.Destination.Server,
|
||||
"dest-namespace": origApp.Spec.Destination.Namespace,
|
||||
})
|
||||
logCtx.Info("Reconciliation completed")
|
||||
}()
|
||||
|
||||
app := origApp.DeepCopy()
|
||||
logCtx := log.WithFields(log.Fields{"application": app.Name})
|
||||
if comparisonLevel == ComparisonWithNothing {
|
||||
if managedResources, err := ctrl.cache.GetAppManagedResources(app.Name); err != nil {
|
||||
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 {
|
||||
@@ -672,17 +868,16 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
}
|
||||
}
|
||||
|
||||
conditions, hasErrors := ctrl.refreshAppConditions(app)
|
||||
project, hasErrors := ctrl.refreshAppConditions(app)
|
||||
if hasErrors {
|
||||
app.Status.Sync.Status = appv1.SyncStatusCodeUnknown
|
||||
app.Status.Health.Status = appv1.HealthStatusUnknown
|
||||
app.Status.Conditions = conditions
|
||||
ctrl.persistAppStatus(origApp, &app.Status)
|
||||
return
|
||||
}
|
||||
|
||||
var localManifests []string
|
||||
if opState := app.Status.OperationState; opState != nil {
|
||||
if opState := app.Status.OperationState; opState != nil && opState.Operation.Sync != nil {
|
||||
localManifests = opState.Operation.Sync.Manifests
|
||||
}
|
||||
|
||||
@@ -690,13 +885,12 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
if comparisonLevel == CompareWithRecent {
|
||||
revision = app.Status.Sync.Revision
|
||||
}
|
||||
compareResult, err := ctrl.appStateManager.CompareAppState(app, revision, app.Spec.Source, refreshType == appv1.RefreshTypeHard, localManifests)
|
||||
if err != nil {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{Type: appv1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
} else {
|
||||
ctrl.normalizeApplication(origApp, app, compareResult.appSourceType)
|
||||
conditions = append(conditions, compareResult.conditions...)
|
||||
}
|
||||
|
||||
observedAt := metav1.Now()
|
||||
compareResult := ctrl.appStateManager.CompareAppState(app, project, revision, app.Spec.Source, refreshType == appv1.RefreshTypeHard, localManifests)
|
||||
|
||||
ctrl.normalizeApplication(origApp, app)
|
||||
|
||||
tree, err := ctrl.setAppManagedResources(app, compareResult)
|
||||
if err != nil {
|
||||
logCtx.Errorf("Failed to cache app resources: %v", err)
|
||||
@@ -704,17 +898,30 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
app.Status.Summary = tree.GetSummary()
|
||||
}
|
||||
|
||||
syncErrCond := ctrl.autoSync(app, compareResult.syncStatus, compareResult.resources)
|
||||
if syncErrCond != nil {
|
||||
conditions = append(conditions, *syncErrCond)
|
||||
if project.Spec.SyncWindows.Matches(app).CanSync(false) {
|
||||
syncErrCond := ctrl.autoSync(app, compareResult.syncStatus, compareResult.resources)
|
||||
if syncErrCond != nil {
|
||||
app.Status.SetConditions(
|
||||
[]appv1.ApplicationCondition{*syncErrCond},
|
||||
map[appv1.ApplicationConditionType]bool{appv1.ApplicationConditionSyncError: true},
|
||||
)
|
||||
} else {
|
||||
app.Status.SetConditions(
|
||||
[]appv1.ApplicationCondition{},
|
||||
map[appv1.ApplicationConditionType]bool{appv1.ApplicationConditionSyncError: true},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
logCtx.Infof("Sync prevented by sync window")
|
||||
}
|
||||
|
||||
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
|
||||
app.Status.Conditions = conditions
|
||||
app.Status.SourceType = compareResult.appSourceType
|
||||
ctrl.persistAppStatus(origApp, &app.Status)
|
||||
return
|
||||
@@ -730,21 +937,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 {
|
||||
// user requested app refresh.
|
||||
refreshType = requestedType
|
||||
reason = fmt.Sprintf("%s refresh requested", refreshType)
|
||||
} 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"
|
||||
} 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()
|
||||
}
|
||||
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 expired {
|
||||
reason = fmt.Sprintf("comparison expired. reconciledAt: %v, expiry: %v", app.Status.ReconciledAt, statusRefreshTimeout)
|
||||
} else if requested, level := ctrl.isRefreshRequested(app.Name); requested {
|
||||
compareWith = level
|
||||
reason = fmt.Sprintf("controller refresh requested")
|
||||
}
|
||||
|
||||
if reason != "" {
|
||||
logCtx.Infof("Refreshing app status (%s), level (%d)", reason, compareWith)
|
||||
return true, refreshType, compareWith
|
||||
@@ -752,17 +967,17 @@ func (ctrl *ApplicationController) needRefreshAppStatus(app *appv1.Application,
|
||||
return false, refreshType, compareWith
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) refreshAppConditions(app *appv1.Application) ([]appv1.ApplicationCondition, bool) {
|
||||
conditions := make([]appv1.ApplicationCondition, 0)
|
||||
proj, err := argo.GetAppProject(&app.Spec, applisters.NewAppProjectLister(ctrl.projInformer.GetIndexer()), ctrl.namespace)
|
||||
func (ctrl *ApplicationController) refreshAppConditions(app *appv1.Application) (*appv1.AppProject, bool) {
|
||||
errorConditions := make([]appv1.ApplicationCondition, 0)
|
||||
proj, err := ctrl.getAppProj(app)
|
||||
if err != nil {
|
||||
if apierr.IsNotFound(err) {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{
|
||||
errorConditions = append(errorConditions, appv1.ApplicationCondition{
|
||||
Type: appv1.ApplicationConditionInvalidSpecError,
|
||||
Message: fmt.Sprintf("Application referencing project %s which does not exist", app.Spec.Project),
|
||||
})
|
||||
} else {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{
|
||||
errorConditions = append(errorConditions, appv1.ApplicationCondition{
|
||||
Type: appv1.ApplicationConditionUnknownError,
|
||||
Message: err.Error(),
|
||||
})
|
||||
@@ -770,48 +985,25 @@ func (ctrl *ApplicationController) refreshAppConditions(app *appv1.Application)
|
||||
} else {
|
||||
specConditions, err := argo.ValidatePermissions(context.Background(), &app.Spec, proj, ctrl.db)
|
||||
if err != nil {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{
|
||||
errorConditions = append(errorConditions, appv1.ApplicationCondition{
|
||||
Type: appv1.ApplicationConditionUnknownError,
|
||||
Message: err.Error(),
|
||||
})
|
||||
} else {
|
||||
conditions = append(conditions, specConditions...)
|
||||
errorConditions = append(errorConditions, specConditions...)
|
||||
}
|
||||
}
|
||||
|
||||
// List of condition types which have to be reevaluated by controller; all remaining conditions should stay as is.
|
||||
reevaluateTypes := map[appv1.ApplicationConditionType]bool{
|
||||
appv1.ApplicationConditionInvalidSpecError: true,
|
||||
appv1.ApplicationConditionUnknownError: true,
|
||||
appv1.ApplicationConditionComparisonError: true,
|
||||
appv1.ApplicationConditionSharedResourceWarning: true,
|
||||
appv1.ApplicationConditionSyncError: true,
|
||||
appv1.ApplicationConditionRepeatedResourceWarning: true,
|
||||
appv1.ApplicationConditionExcludedResourceWarning: true,
|
||||
}
|
||||
appConditions := make([]appv1.ApplicationCondition, 0)
|
||||
for i := 0; i < len(app.Status.Conditions); i++ {
|
||||
condition := app.Status.Conditions[i]
|
||||
if _, ok := reevaluateTypes[condition.Type]; !ok {
|
||||
appConditions = append(appConditions, condition)
|
||||
}
|
||||
}
|
||||
hasErrors := false
|
||||
for i := range conditions {
|
||||
condition := conditions[i]
|
||||
appConditions = append(appConditions, condition)
|
||||
if condition.IsError() {
|
||||
hasErrors = true
|
||||
}
|
||||
|
||||
}
|
||||
return appConditions, hasErrors
|
||||
app.Status.SetConditions(errorConditions, map[appv1.ApplicationConditionType]bool{
|
||||
appv1.ApplicationConditionInvalidSpecError: true,
|
||||
appv1.ApplicationConditionUnknownError: true,
|
||||
})
|
||||
return proj, len(errorConditions) > 0
|
||||
}
|
||||
|
||||
// normalizeApplication normalizes an application.spec and additionally persists updates if it changed
|
||||
func (ctrl *ApplicationController) normalizeApplication(orig, app *appv1.Application, sourceType appv1.ApplicationSourceType) {
|
||||
func (ctrl *ApplicationController) normalizeApplication(orig, app *appv1.Application) {
|
||||
logCtx := log.WithFields(log.Fields{"application": app.Name})
|
||||
app.Spec = *argo.NormalizeApplicationSpec(&app.Spec, sourceType)
|
||||
app.Spec = *argo.NormalizeApplicationSpec(&app.Spec)
|
||||
patch, modified, err := diff.CreateTwoWayMergePatch(orig, app, appv1.Application{})
|
||||
if err != nil {
|
||||
logCtx.Errorf("error constructing app spec patch: %v", err)
|
||||
@@ -880,6 +1072,7 @@ func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus *
|
||||
logCtx.Infof("Skipping auto-sync: deletion in progress")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Only perform auto-sync if we detect OutOfSync status. This is to prevent us from attempting
|
||||
// a sync when application is already in a Synced or Unknown state
|
||||
if syncStatus.Status != appv1.SyncStatusCodeOutOfSync {
|
||||
@@ -921,12 +1114,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
|
||||
}
|
||||
|
||||
@@ -976,7 +1164,7 @@ func (ctrl *ApplicationController) shouldSelfHeal(app *appv1.Application) (bool,
|
||||
return retryAfter <= 0, retryAfter
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) newApplicationInformerAndLister() (cache.SharedIndexInformer, applisters.ApplicationLister) {
|
||||
func (ctrl *ApplicationController) newApplicationInformerAndLister() (cache.SharedIndexInformer, applisters.ApplicationLister, error) {
|
||||
appInformerFactory := appinformers.NewFilteredSharedInformerFactory(
|
||||
ctrl.applicationClientset,
|
||||
ctrl.statusRefreshTimeout,
|
||||
@@ -999,15 +1187,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{}) {
|
||||
@@ -1020,21 +1207,50 @@ func (ctrl *ApplicationController) newApplicationInformerAndLister() (cache.Shar
|
||||
},
|
||||
},
|
||||
)
|
||||
return informer, lister
|
||||
err := informer.AddIndexers(cache.Indexers{
|
||||
orphanedIndex: func(obj interface{}) (i []string, e error) {
|
||||
app, ok := obj.(*appv1.Application)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
proj, err := ctrl.getAppProj(app)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
if proj.Spec.OrphanedResources != nil {
|
||||
return []string{app.Spec.Destination.Namespace}, nil
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
})
|
||||
return informer, lister, err
|
||||
}
|
||||
|
||||
func isOperationInProgress(app *appv1.Application) bool {
|
||||
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,15 +27,24 @@ 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"
|
||||
)
|
||||
|
||||
type namespacedResource struct {
|
||||
argoappv1.ResourceNode
|
||||
AppName string
|
||||
}
|
||||
|
||||
type fakeData struct {
|
||||
apps []runtime.Object
|
||||
manifestResponse *apiclient.ManifestResponse
|
||||
managedLiveObjs map[kube.ResourceKey]*unstructured.Unstructured
|
||||
apps []runtime.Object
|
||||
manifestResponse *apiclient.ManifestResponse
|
||||
managedLiveObjs map[kube.ResourceKey]*unstructured.Unstructured
|
||||
namespacedResources map[kube.ResourceKey]namespacedResource
|
||||
configMapData map[string]string
|
||||
}
|
||||
|
||||
func newFakeController(data *fakeData) *ApplicationController {
|
||||
@@ -68,20 +78,26 @@ func newFakeController(data *fakeData) *ApplicationController {
|
||||
"app.kubernetes.io/part-of": "argocd",
|
||||
},
|
||||
},
|
||||
Data: nil,
|
||||
Data: data.configMapData,
|
||||
}
|
||||
kubeClient := fake.NewSimpleClientset(&clust, &cm, &secret)
|
||||
settingsMgr := settings.NewSettingsManager(context.Background(), kubeClient, test.FakeArgoCDNamespace)
|
||||
kubectl := &kubetest.MockKubectlCmd{}
|
||||
ctrl, err := NewApplicationController(
|
||||
test.FakeArgoCDNamespace,
|
||||
settingsMgr,
|
||||
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,
|
||||
common.DefaultPortArgoCDMetrics,
|
||||
0,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -90,14 +106,26 @@ func newFakeController(data *fakeData) *ApplicationController {
|
||||
defer cancelProj()
|
||||
cancelApp := test.StartInformer(ctrl.appInformer)
|
||||
defer cancelApp()
|
||||
// Mock out call to GetManagedLiveObjs if fake data supplied
|
||||
if data.managedLiveObjs != nil {
|
||||
mockStateCache := mockstatecache.LiveStateCache{}
|
||||
mockStateCache.On("GetManagedLiveObjs", mock.Anything, mock.Anything).Return(data.managedLiveObjs, nil)
|
||||
mockStateCache.On("IsNamespaced", mock.Anything, mock.Anything).Return(true, nil)
|
||||
ctrl.stateCache = &mockStateCache
|
||||
ctrl.appStateManager.(*appStateManager).liveStateCache = &mockStateCache
|
||||
mockStateCache := mockstatecache.LiveStateCache{}
|
||||
ctrl.appStateManager.(*appStateManager).liveStateCache = &mockStateCache
|
||||
ctrl.stateCache = &mockStateCache
|
||||
mockStateCache.On("IsNamespaced", mock.Anything, mock.Anything).Return(true, nil)
|
||||
mockStateCache.On("GetManagedLiveObjs", mock.Anything, mock.Anything).Return(data.managedLiveObjs, nil)
|
||||
mockStateCache.On("GetServerVersion", mock.Anything).Return("v1.2.3", nil)
|
||||
response := make(map[kube.ResourceKey]argoappv1.ResourceNode)
|
||||
for k, v := range data.namespacedResources {
|
||||
response[k] = v.ResourceNode
|
||||
}
|
||||
mockStateCache.On("GetNamespaceTopLevelResources", mock.Anything, mock.Anything).Return(response, nil)
|
||||
mockStateCache.On("IterateHierarchy", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
|
||||
key := args[1].(kube.ResourceKey)
|
||||
action := args[2].(func(child argoappv1.ResourceNode, appName string))
|
||||
appName := ""
|
||||
if res, ok := data.namespacedResources[key]; ok {
|
||||
appName = res.AppName
|
||||
}
|
||||
action(argoappv1.ResourceNode{ResourceRef: argoappv1.ResourceRef{Group: key.Group, Namespace: key.Namespace, Name: key.Name}}, appName)
|
||||
}).Return(nil)
|
||||
return ctrl
|
||||
}
|
||||
|
||||
@@ -164,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)
|
||||
@@ -173,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}})
|
||||
@@ -361,27 +409,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
|
||||
@@ -464,17 +603,44 @@ func TestHandleAppUpdated(t *testing.T) {
|
||||
app.Spec.Destination.Server = common.KubernetesInternalAPIServerAddr
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
|
||||
ctrl.handleAppUpdated(app.Name, true, kube.GetObjectRef(kube.MustToUnstructured(app)))
|
||||
ctrl.handleObjectUpdated(map[string]bool{app.Name: true}, kube.GetObjectRef(kube.MustToUnstructured(app)))
|
||||
isRequested, level := ctrl.isRefreshRequested(app.Name)
|
||||
assert.False(t, isRequested)
|
||||
assert.Equal(t, ComparisonWithNothing, level)
|
||||
|
||||
ctrl.handleAppUpdated(app.Name, true, corev1.ObjectReference{UID: "test", Kind: kube.DeploymentKind, Name: "test", Namespace: "default"})
|
||||
ctrl.handleObjectUpdated(map[string]bool{app.Name: true}, corev1.ObjectReference{UID: "test", Kind: kube.DeploymentKind, Name: "test", Namespace: "default"})
|
||||
isRequested, level = ctrl.isRefreshRequested(app.Name)
|
||||
assert.True(t, isRequested)
|
||||
assert.Equal(t, CompareWithRecent, level)
|
||||
}
|
||||
|
||||
func TestHandleOrphanedResourceUpdated(t *testing.T) {
|
||||
app1 := newFakeApp()
|
||||
app1.Name = "app1"
|
||||
app1.Spec.Destination.Namespace = test.FakeArgoCDNamespace
|
||||
app1.Spec.Destination.Server = common.KubernetesInternalAPIServerAddr
|
||||
|
||||
app2 := newFakeApp()
|
||||
app2.Name = "app2"
|
||||
app2.Spec.Destination.Namespace = test.FakeArgoCDNamespace
|
||||
app2.Spec.Destination.Server = common.KubernetesInternalAPIServerAddr
|
||||
|
||||
proj := defaultProj.DeepCopy()
|
||||
proj.Spec.OrphanedResources = &argoappv1.OrphanedResourcesMonitorSettings{}
|
||||
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app1, app2, proj}})
|
||||
|
||||
ctrl.handleObjectUpdated(map[string]bool{}, corev1.ObjectReference{UID: "test", Kind: kube.DeploymentKind, Name: "test", Namespace: test.FakeArgoCDNamespace})
|
||||
|
||||
isRequested, level := ctrl.isRefreshRequested(app1.Name)
|
||||
assert.True(t, isRequested)
|
||||
assert.Equal(t, ComparisonWithNothing, level)
|
||||
|
||||
isRequested, level = ctrl.isRefreshRequested(app2.Name)
|
||||
assert.True(t, isRequested)
|
||||
assert.Equal(t, ComparisonWithNothing, level)
|
||||
}
|
||||
|
||||
func TestSetOperationStateOnDeletedApp(t *testing.T) {
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{}})
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
@@ -507,8 +673,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)
|
||||
@@ -523,13 +689,160 @@ func TestNeedRefreshAppStatus(t *testing.T) {
|
||||
assert.Equal(t, argoappv1.RefreshTypeNormal, refreshType)
|
||||
assert.Equal(t, CompareWithLatest, compareWith)
|
||||
|
||||
// execute hard refresh if app has refresh annotation
|
||||
app.Annotations = map[string]string{
|
||||
common.AnnotationKeyRefresh: string(argoappv1.RefreshTypeHard),
|
||||
{
|
||||
// refresh app using the 'latest' level if comparison expired
|
||||
app := app.DeepCopy()
|
||||
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)
|
||||
assert.True(t, needRefresh)
|
||||
assert.Equal(t, argoappv1.RefreshTypeNormal, refreshType)
|
||||
assert.Equal(t, CompareWithLatest, compareWith)
|
||||
}
|
||||
needRefresh, refreshType, compareWith = ctrl.needRefreshAppStatus(app, 1*time.Hour)
|
||||
assert.True(t, needRefresh)
|
||||
assert.Equal(t, argoappv1.RefreshTypeHard, refreshType)
|
||||
assert.Equal(t, CompareWithLatest, compareWith)
|
||||
|
||||
{
|
||||
app := app.DeepCopy()
|
||||
// execute hard refresh if app has refresh annotation
|
||||
reconciledAt := metav1.NewTime(time.Now().UTC().Add(-1 * time.Hour))
|
||||
app.Status.ReconciledAt = &reconciledAt
|
||||
app.Annotations = map[string]string{
|
||||
common.AnnotationKeyRefresh: string(argoappv1.RefreshTypeHard),
|
||||
}
|
||||
needRefresh, refreshType, compareWith = ctrl.needRefreshAppStatus(app, 1*time.Hour)
|
||||
assert.True(t, needRefresh)
|
||||
assert.Equal(t, argoappv1.RefreshTypeHard, refreshType)
|
||||
assert.Equal(t, CompareWithLatest, compareWith)
|
||||
}
|
||||
|
||||
{
|
||||
app := app.DeepCopy()
|
||||
// ensure that CompareWithLatest level is used if application source has changed
|
||||
ctrl.requestAppRefresh(app.Name, ComparisonWithNothing.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) {
|
||||
defaultProj := argoappv1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
},
|
||||
Spec: argoappv1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []argoappv1.ApplicationDestination{
|
||||
{
|
||||
Server: "*",
|
||||
Namespace: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("NoErrorConditions", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}})
|
||||
|
||||
_, hasErrors := ctrl.refreshAppConditions(app)
|
||||
assert.False(t, hasErrors)
|
||||
assert.Len(t, app.Status.Conditions, 0)
|
||||
})
|
||||
|
||||
t.Run("PreserveExistingWarningCondition", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Status.SetConditions([]argoappv1.ApplicationCondition{{Type: argoappv1.ApplicationConditionExcludedResourceWarning}}, nil)
|
||||
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}})
|
||||
|
||||
_, hasErrors := ctrl.refreshAppConditions(app)
|
||||
assert.False(t, hasErrors)
|
||||
assert.Len(t, app.Status.Conditions, 1)
|
||||
assert.Equal(t, argoappv1.ApplicationConditionExcludedResourceWarning, app.Status.Conditions[0].Type)
|
||||
})
|
||||
|
||||
t.Run("ReplacesSpecErrorCondition", func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Spec.Project = "wrong project"
|
||||
app.Status.SetConditions([]argoappv1.ApplicationCondition{{Type: argoappv1.ApplicationConditionInvalidSpecError, Message: "old message"}}, nil)
|
||||
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app, &defaultProj}})
|
||||
|
||||
_, hasErrors := ctrl.refreshAppConditions(app)
|
||||
assert.True(t, hasErrors)
|
||||
assert.Len(t, app.Status.Conditions, 1)
|
||||
assert.Equal(t, argoappv1.ApplicationConditionInvalidSpecError, app.Status.Conditions[0].Type)
|
||||
assert.Equal(t, "Application referencing project wrong project which does not exist", app.Status.Conditions[0].Message)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateReconciledAt(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
reconciledAt := metav1.NewTime(time.Now().Add(-1 * time.Second))
|
||||
app.Status = argoappv1.ApplicationStatus{ReconciledAt: &reconciledAt}
|
||||
app.Status.Sync = argoappv1.SyncStatus{ComparedTo: argoappv1.ComparedTo{Source: app.Spec.Source, Destination: app.Spec.Destination}}
|
||||
ctrl := newFakeController(&fakeData{
|
||||
apps: []runtime.Object{app, &defaultProj},
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
})
|
||||
key, _ := cache.MetaNamespaceKeyFunc(app)
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
fakeAppCs.ReactionChain = nil
|
||||
receivedPatch := map[string]interface{}{}
|
||||
fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
if patchAction, ok := action.(kubetesting.PatchAction); ok {
|
||||
assert.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch))
|
||||
}
|
||||
return true, nil, nil
|
||||
})
|
||||
|
||||
t.Run("UpdatedOnFullReconciliation", func(t *testing.T) {
|
||||
receivedPatch = map[string]interface{}{}
|
||||
ctrl.requestAppRefresh(app.Name, CompareWithLatest.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)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
60
controller/cache/cache.go
vendored
@@ -27,18 +27,25 @@ type cacheSettings struct {
|
||||
}
|
||||
|
||||
type LiveStateCache interface {
|
||||
IsNamespaced(server string, obj *unstructured.Unstructured) (bool, error)
|
||||
// Returns k8s server version
|
||||
GetServerVersion(serverURL string) (string, 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, obj *unstructured.Unstructured, action func(child appv1.ResourceNode)) error
|
||||
IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string)) error
|
||||
// Returns state of live nodes which correspond for target nodes of specified application.
|
||||
GetManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error)
|
||||
// Returns all top level resources (resources without owner references) of a specified namespace
|
||||
GetNamespaceTopLevelResources(server string, namespace string) (map[kube.ResourceKey]appv1.ResourceNode, error)
|
||||
// Starts watching resources of each controlled cluster.
|
||||
Run(ctx context.Context) error
|
||||
// Invalidate invalidates the entire cluster state cache
|
||||
Invalidate()
|
||||
// Returns information about monitored clusters
|
||||
GetClustersInfo() []metrics.ClusterInfo
|
||||
}
|
||||
|
||||
type AppUpdatedHandler = func(appName string, isManagedResource bool, ref v1.ObjectReference)
|
||||
type ObjectUpdatedHandler = func(managedByApp map[string]bool, ref v1.ObjectReference)
|
||||
|
||||
func GetTargetObjKey(a *appv1.Application, un *unstructured.Unstructured, isNamespaced bool) kube.ResourceKey {
|
||||
key := kube.GetResourceKey(un)
|
||||
@@ -57,14 +64,14 @@ func NewLiveStateCache(
|
||||
settingsMgr *settings.SettingsManager,
|
||||
kubectl kube.Kubectl,
|
||||
metricsServer *metrics.MetricsServer,
|
||||
onAppUpdated AppUpdatedHandler) LiveStateCache {
|
||||
onObjectUpdated ObjectUpdatedHandler) LiveStateCache {
|
||||
|
||||
return &liveStateCache{
|
||||
appInformer: appInformer,
|
||||
db: db,
|
||||
clusters: make(map[string]*clusterInfo),
|
||||
lock: &sync.Mutex{},
|
||||
onAppUpdated: onAppUpdated,
|
||||
onObjectUpdated: onObjectUpdated,
|
||||
kubectl: kubectl,
|
||||
settingsMgr: settingsMgr,
|
||||
metricsServer: metricsServer,
|
||||
@@ -77,7 +84,7 @@ type liveStateCache struct {
|
||||
clusters map[string]*clusterInfo
|
||||
lock *sync.Mutex
|
||||
appInformer cache.SharedIndexInformer
|
||||
onAppUpdated AppUpdatedHandler
|
||||
onObjectUpdated ObjectUpdatedHandler
|
||||
kubectl kube.Kubectl
|
||||
settingsMgr *settings.SettingsManager
|
||||
metricsServer *metrics.MetricsServer
|
||||
@@ -115,13 +122,15 @@ func (c *liveStateCache) getCluster(server string) (*clusterInfo, error) {
|
||||
lock: &sync.Mutex{},
|
||||
nodes: make(map[kube.ResourceKey]*node),
|
||||
nsIndex: make(map[string]map[kube.ResourceKey]*node),
|
||||
onAppUpdated: c.onAppUpdated,
|
||||
onObjectUpdated: c.onObjectUpdated,
|
||||
kubectl: c.kubectl,
|
||||
cluster: cluster,
|
||||
syncTime: nil,
|
||||
syncLock: &sync.Mutex{},
|
||||
log: log.WithField("server", cluster.Server),
|
||||
cacheSettingsSrc: c.getCacheSettings,
|
||||
onEventReceived: func(event watch.EventType, un *unstructured.Unstructured) {
|
||||
c.metricsServer.IncClusterEventsCount(cluster.Server)
|
||||
},
|
||||
}
|
||||
|
||||
c.clusters[cluster.Server] = info
|
||||
@@ -146,30 +155,36 @@ func (c *liveStateCache) Invalidate() {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
for _, clust := range c.clusters {
|
||||
clust.lock.Lock()
|
||||
clust.invalidate()
|
||||
clust.lock.Unlock()
|
||||
}
|
||||
log.Info("live state cache invalidated")
|
||||
}
|
||||
|
||||
func (c *liveStateCache) IsNamespaced(server string, obj *unstructured.Unstructured) (bool, error) {
|
||||
func (c *liveStateCache) IsNamespaced(server string, gk schema.GroupKind) (bool, error) {
|
||||
clusterInfo, err := c.getSyncedCluster(server)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return clusterInfo.isNamespaced(obj), nil
|
||||
return clusterInfo.isNamespaced(gk), nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) IterateHierarchy(server string, obj *unstructured.Unstructured, action func(child appv1.ResourceNode)) error {
|
||||
func (c *liveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string)) error {
|
||||
clusterInfo, err := c.getSyncedCluster(server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clusterInfo.iterateHierarchy(obj, action)
|
||||
clusterInfo.iterateHierarchy(key, action)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) GetNamespaceTopLevelResources(server string, namespace string) (map[kube.ResourceKey]appv1.ResourceNode, error) {
|
||||
clusterInfo, err := c.getSyncedCluster(server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return clusterInfo.getNamespaceTopLevelResources(namespace), nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) GetManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
|
||||
clusterInfo, err := c.getSyncedCluster(a.Spec.Destination.Server)
|
||||
if err != nil {
|
||||
@@ -177,6 +192,13 @@ func (c *liveStateCache) GetManagedLiveObjs(a *appv1.Application, targetObjs []*
|
||||
}
|
||||
return clusterInfo.getManagedLiveObjs(a, targetObjs, c.metricsServer)
|
||||
}
|
||||
func (c *liveStateCache) GetServerVersion(serverURL string) (string, error) {
|
||||
clusterInfo, err := c.getSyncedCluster(serverURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return clusterInfo.serverVersion, nil
|
||||
}
|
||||
|
||||
func isClusterHasApps(apps []interface{}, cluster *appv1.Cluster) bool {
|
||||
for _, obj := range apps {
|
||||
@@ -263,3 +285,13 @@ func (c *liveStateCache) Run(ctx context.Context) error {
|
||||
<-ctx.Done()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) GetClustersInfo() []metrics.ClusterInfo {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
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.Mutex{},
|
||||
clusters: map[string]*clusterInfo{
|
||||
"http://localhost": {
|
||||
syncTime: &now,
|
||||
lock: &sync.Mutex{},
|
||||
serverVersion: "123",
|
||||
},
|
||||
}}
|
||||
|
||||
version, err := cache.GetServerVersion("http://localhost")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "123", version)
|
||||
}
|
||||
224
controller/cache/cluster.go
vendored
@@ -9,6 +9,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"k8s.io/client-go/dynamic"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"github.com/argoproj/argo-cd/controller/metrics"
|
||||
@@ -39,32 +41,32 @@ 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
|
||||
|
||||
lock *sync.Mutex
|
||||
nodes map[kube.ResourceKey]*node
|
||||
nsIndex map[string]map[kube.ResourceKey]*node
|
||||
|
||||
onAppUpdated AppUpdatedHandler
|
||||
onObjectUpdated ObjectUpdatedHandler
|
||||
onEventReceived func(event watch.EventType, un *unstructured.Unstructured)
|
||||
kubectl kube.Kubectl
|
||||
cluster *appv1.Cluster
|
||||
log *log.Entry
|
||||
cacheSettingsSrc func() *cacheSettings
|
||||
}
|
||||
|
||||
func (c *clusterInfo) replaceResourceCache(gk schema.GroupKind, resourceVersion string, objs []unstructured.Unstructured) {
|
||||
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 +74,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)
|
||||
}
|
||||
}
|
||||
@@ -85,13 +88,55 @@ func (c *clusterInfo) replaceResourceCache(gk schema.GroupKind, resourceVersion
|
||||
}
|
||||
}
|
||||
|
||||
func isServiceAccountTokenSecret(un *unstructured.Unstructured) (bool, metav1.OwnerReference) {
|
||||
ref := metav1.OwnerReference{
|
||||
APIVersion: "v1",
|
||||
Kind: kube.ServiceAccountKind,
|
||||
}
|
||||
if un.GetKind() != kube.SecretKind || un.GroupVersionKind().Group != "" {
|
||||
return false, ref
|
||||
}
|
||||
|
||||
if typeVal, ok, err := unstructured.NestedString(un.Object, "type"); !ok || err != nil || typeVal != "kubernetes.io/service-account-token" {
|
||||
return false, ref
|
||||
}
|
||||
|
||||
annotations := un.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return false, ref
|
||||
}
|
||||
|
||||
id, okId := annotations["kubernetes.io/service-account.uid"]
|
||||
name, okName := annotations["kubernetes.io/service-account.name"]
|
||||
if okId && okName {
|
||||
ref.Name = name
|
||||
ref.UID = types.UID(id)
|
||||
}
|
||||
return ref.Name != "" && ref.UID != "", ref
|
||||
}
|
||||
|
||||
func (c *clusterInfo) createObjInfo(un *unstructured.Unstructured, appInstanceLabel string) *node {
|
||||
ownerRefs := un.GetOwnerReferences()
|
||||
// Special case for endpoint. Remove after https://github.com/kubernetes/kubernetes/issues/28483 is fixed
|
||||
if un.GroupVersionKind().Group == "" && un.GetKind() == kube.EndpointsKind && len(un.GetOwnerReferences()) == 0 {
|
||||
ownerRefs = append(ownerRefs, metav1.OwnerReference{
|
||||
Name: un.GetName(),
|
||||
Kind: kube.ServiceKind,
|
||||
APIVersion: "v1",
|
||||
})
|
||||
}
|
||||
|
||||
// edge case. Consider auto-created service account tokens as a child of service account objects
|
||||
if yes, ref := isServiceAccountTokenSecret(un); yes {
|
||||
ownerRefs = append(ownerRefs, ref)
|
||||
}
|
||||
|
||||
nodeInfo := &node{
|
||||
resourceVersion: un.GetResourceVersion(),
|
||||
ref: kube.GetObjectRef(un),
|
||||
ownerRefs: ownerRefs,
|
||||
}
|
||||
|
||||
populateNodeInfo(un, nodeInfo)
|
||||
appName := kube.GetAppInstanceLabel(un, appInstanceLabel)
|
||||
if len(ownerRefs) == 0 && appName != "" {
|
||||
@@ -124,8 +169,8 @@ 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()
|
||||
@@ -143,21 +188,26 @@ func (c *clusterInfo) synced() bool {
|
||||
return time.Now().Before(c.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{})
|
||||
c.replaceResourceCache(gk, "", []unstructured.Unstructured{}, ns)
|
||||
log.Warnf("Stop watching %s not found on %s.", gk, c.cluster.Server)
|
||||
}
|
||||
}
|
||||
|
||||
// startMissingWatches lists supported cluster resources and start watching for changes unless watch is already running
|
||||
func (c *clusterInfo) startMissingWatches() error {
|
||||
config := c.cluster.RESTConfig()
|
||||
|
||||
apis, err := c.kubectl.GetAPIResources(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
|
||||
}
|
||||
@@ -168,7 +218,14 @@ func (c *clusterInfo) startMissingWatches() error {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -180,7 +237,7 @@ func runSynced(lock *sync.Mutex, action func() error) error {
|
||||
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 {
|
||||
@@ -188,13 +245,13 @@ 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{})
|
||||
list, err := resClient.List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.replaceResourceCache(api.GroupKind, list.GetResourceVersion(), list.Items)
|
||||
c.replaceResourceCache(api.GroupKind, list.GetResourceVersion(), list.Items, ns)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -203,13 +260,13 @@ 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 {
|
||||
err = runSynced(c.lock, func() error {
|
||||
if errors.IsGone(err) {
|
||||
info.resourceVersion = ""
|
||||
log.Warnf("Resource version of %s on %s is too old.", api.GroupKind, c.cluster.Server)
|
||||
@@ -237,10 +294,10 @@ 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()
|
||||
})
|
||||
|
||||
@@ -258,6 +315,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")
|
||||
@@ -267,25 +343,35 @@ func (c *clusterInfo) sync() (err error) {
|
||||
}
|
||||
c.apisMeta = make(map[schema.GroupKind]*apiMeta)
|
||||
c.nodes = make(map[kube.ResourceKey]*node)
|
||||
|
||||
apis, err := c.kubectl.GetAPIResources(c.cluster.RESTConfig(), c.cacheSettingsSrc().ResourcesFilter)
|
||||
config := c.cluster.RESTConfig()
|
||||
version, err := c.kubectl.GetServerVersion(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.serverVersion = version
|
||||
apis, err := c.kubectl.GetAPIResources(config, c.cacheSettingsSrc().ResourcesFilter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, err := c.kubectl.NewDynamicClient(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lock := sync.Mutex{}
|
||||
err = util.RunAllAsync(len(apis), func(i int) error {
|
||||
api := apis[i]
|
||||
list, err := api.Interface.List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.processApi(client, apis[i], func(resClient dynamic.ResourceInterface, _ string) error {
|
||||
list, err := resClient.List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lock.Lock()
|
||||
for i := range list.Items {
|
||||
c.setNode(c.createObjInfo(&list.Items[i], c.cacheSettingsSrc().AppInstanceLabelKey))
|
||||
}
|
||||
lock.Unlock()
|
||||
return nil
|
||||
lock.Lock()
|
||||
for i := range list.Items {
|
||||
c.setNode(c.createObjInfo(&list.Items[i], c.cacheSettingsSrc().AppInstanceLabelKey))
|
||||
}
|
||||
lock.Unlock()
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
@@ -302,8 +388,8 @@ func (c *clusterInfo) sync() (err error) {
|
||||
}
|
||||
|
||||
func (c *clusterInfo) ensureSynced() error {
|
||||
c.syncLock.Lock()
|
||||
defer c.syncLock.Unlock()
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
if c.synced() {
|
||||
return c.syncError
|
||||
}
|
||||
@@ -315,13 +401,24 @@ func (c *clusterInfo) ensureSynced() error {
|
||||
return c.syncError
|
||||
}
|
||||
|
||||
func (c *clusterInfo) iterateHierarchy(obj *unstructured.Unstructured, action func(child appv1.ResourceNode)) {
|
||||
func (c *clusterInfo) getNamespaceTopLevelResources(namespace string) map[kube.ResourceKey]appv1.ResourceNode {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
nodes := make(map[kube.ResourceKey]appv1.ResourceNode)
|
||||
for _, node := range c.nsIndex[namespace] {
|
||||
if len(node.ownerRefs) == 0 {
|
||||
nodes[node.resourceKey()] = node.asResourceNode()
|
||||
}
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
func (c *clusterInfo) iterateHierarchy(key kube.ResourceKey, action func(child appv1.ResourceNode, appName string)) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
key := kube.GetResourceKey(obj)
|
||||
if objInfo, ok := c.nodes[key]; ok {
|
||||
action(objInfo.asResourceNode())
|
||||
nsNodes := c.nsIndex[key.Namespace]
|
||||
action(objInfo.asResourceNode(), objInfo.getApp(nsNodes))
|
||||
childrenByUID := make(map[types.UID][]*node)
|
||||
for _, child := range nsNodes {
|
||||
if objInfo.isParentOf(child) {
|
||||
@@ -339,17 +436,15 @@ func (c *clusterInfo) iterateHierarchy(obj *unstructured.Unstructured, action fu
|
||||
return strings.Compare(key1.String(), key2.String()) < 0
|
||||
})
|
||||
child := children[0]
|
||||
action(child.asResourceNode())
|
||||
action(child.asResourceNode(), child.getApp(nsNodes))
|
||||
child.iterateChildren(nsNodes, map[kube.ResourceKey]bool{objInfo.resourceKey(): true}, action)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
action(c.createObjInfo(obj, c.cacheSettingsSrc().AppInstanceLabelKey).asResourceNode())
|
||||
}
|
||||
}
|
||||
|
||||
func (c *clusterInfo) isNamespaced(obj *unstructured.Unstructured) bool {
|
||||
if api, ok := c.apisMeta[kube.GetResourceKey(obj).GroupKind()]; ok && !api.namespaced {
|
||||
func (c *clusterInfo) isNamespaced(gk schema.GroupKind) bool {
|
||||
if api, ok := c.apisMeta[gk]; ok && !api.namespaced {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -372,7 +467,7 @@ func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*uns
|
||||
lock := &sync.Mutex{}
|
||||
err := util.RunAllAsync(len(targetObjs), func(i int) error {
|
||||
targetObj := targetObjs[i]
|
||||
key := GetTargetObjKey(a, targetObj, c.isNamespaced(targetObj))
|
||||
key := GetTargetObjKey(a, targetObj, c.isNamespaced(targetObj.GroupVersionKind().GroupKind()))
|
||||
lock.Lock()
|
||||
managedObj := managedObjs[key]
|
||||
lock.Unlock()
|
||||
@@ -432,6 +527,9 @@ 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)
|
||||
}
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
key := kube.GetResourceKey(un)
|
||||
@@ -464,9 +562,7 @@ func (c *clusterInfo) onNodeUpdated(exists bool, existingNode *node, un *unstruc
|
||||
toNotify[app] = n.isRootAppNode() || toNotify[app]
|
||||
}
|
||||
}
|
||||
for name, isRootAppNode := range toNotify {
|
||||
c.onAppUpdated(name, isRootAppNode, newObj.ref)
|
||||
}
|
||||
c.onObjectUpdated(toNotify, newObj.ref)
|
||||
}
|
||||
|
||||
func (c *clusterInfo) onNodeRemoved(key kube.ResourceKey, n *node) {
|
||||
@@ -476,9 +572,11 @@ func (c *clusterInfo) onNodeRemoved(key kube.ResourceKey, n *node) {
|
||||
}
|
||||
|
||||
c.removeNode(key)
|
||||
managedByApp := make(map[string]bool)
|
||||
if appName != "" {
|
||||
c.onAppUpdated(appName, n.isRootAppNode(), n.ref)
|
||||
managedByApp[appName] = n.isRootAppNode()
|
||||
}
|
||||
c.onObjectUpdated(managedByApp, n.ref)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -487,6 +585,18 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
func (c *clusterInfo) getClusterInfo() metrics.ClusterInfo {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
return metrics.ClusterInfo{
|
||||
APIsCount: len(c.apisMeta),
|
||||
K8SVersion: c.serverVersion,
|
||||
ResourcesCount: len(c.nodes),
|
||||
Server: c.cluster.Server,
|
||||
LastCacheSyncTime: c.syncTime,
|
||||
}
|
||||
}
|
||||
|
||||
// skipAppRequeing checks if the object is an API type which we want to skip requeuing against.
|
||||
// We ignore API types which have a high churn rate, and/or whose updates are irrelevant to the app
|
||||
func skipAppRequeing(key kube.ResourceKey) bool {
|
||||
|
||||
153
controller/cache/cluster_test.go
vendored
@@ -87,6 +87,7 @@ var (
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
resourceVersion: "123"
|
||||
uid: "4"
|
||||
spec:
|
||||
selector:
|
||||
app: guestbook
|
||||
@@ -102,6 +103,7 @@ var (
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
uid: "4"
|
||||
spec:
|
||||
backend:
|
||||
serviceName: not-found-service
|
||||
@@ -133,34 +135,33 @@ 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{},
|
||||
nodes: make(map[kube.ResourceKey]*node),
|
||||
onAppUpdated: func(appName string, fullRefresh bool, reference corev1.ObjectReference) {},
|
||||
kubectl: kubectl,
|
||||
nsIndex: make(map[string]map[kube.ResourceKey]*node),
|
||||
cluster: &appv1.Cluster{},
|
||||
syncTime: nil,
|
||||
syncLock: &sync.Mutex{},
|
||||
apisMeta: make(map[schema.GroupKind]*apiMeta),
|
||||
log: log.WithField("cluster", "test"),
|
||||
lock: &sync.Mutex{},
|
||||
nodes: make(map[kube.ResourceKey]*node),
|
||||
onObjectUpdated: func(managedByApp map[string]bool, reference corev1.ObjectReference) {},
|
||||
kubectl: kubectl,
|
||||
nsIndex: make(map[string]map[kube.ResourceKey]*node),
|
||||
cluster: &appv1.Cluster{},
|
||||
syncTime: nil,
|
||||
apisMeta: make(map[schema.GroupKind]*apiMeta),
|
||||
log: log.WithField("cluster", "test"),
|
||||
cacheSettingsSrc: func() *cacheSettings {
|
||||
return &cacheSettings{AppInstanceLabelKey: common.LabelKeyAppInstance}
|
||||
},
|
||||
@@ -169,12 +170,92 @@ func newClusterExt(kubectl kube.Kubectl) *clusterInfo {
|
||||
|
||||
func getChildren(cluster *clusterInfo, un *unstructured.Unstructured) []appv1.ResourceNode {
|
||||
hierarchy := make([]appv1.ResourceNode, 0)
|
||||
cluster.iterateHierarchy(un, func(child appv1.ResourceNode) {
|
||||
cluster.iterateHierarchy(kube.GetResourceKey(un), func(child appv1.ResourceNode, app string) {
|
||||
hierarchy = append(hierarchy, child)
|
||||
})
|
||||
return hierarchy[1:]
|
||||
}
|
||||
|
||||
func TestEnsureSynced(t *testing.T) {
|
||||
obj1 := strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: {"name": "helm-guestbook1", "namespace": "default1"}
|
||||
`)
|
||||
obj2 := strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: {"name": "helm-guestbook2", "namespace": "default2"}
|
||||
`)
|
||||
|
||||
cluster := newCluster(obj1, obj2)
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Len(t, cluster.nodes, 2)
|
||||
var names []string
|
||||
for k := range cluster.nodes {
|
||||
names = append(names, k.Name)
|
||||
}
|
||||
assert.ElementsMatch(t, []string{"helm-guestbook1", "helm-guestbook2"}, names)
|
||||
}
|
||||
|
||||
func TestEnsureSyncedSingleNamespace(t *testing.T) {
|
||||
obj1 := strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: {"name": "helm-guestbook1", "namespace": "default1"}
|
||||
`)
|
||||
obj2 := strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: {"name": "helm-guestbook2", "namespace": "default2"}
|
||||
`)
|
||||
|
||||
cluster := newCluster(obj1, obj2)
|
||||
cluster.cluster.Namespaces = []string{"default1"}
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Len(t, cluster.nodes, 1)
|
||||
var names []string
|
||||
for k := range cluster.nodes {
|
||||
names = append(names, k.Name)
|
||||
}
|
||||
assert.ElementsMatch(t, []string{"helm-guestbook1"}, names)
|
||||
}
|
||||
|
||||
func TestGetNamespaceResources(t *testing.T) {
|
||||
defaultNamespaceTopLevel1 := strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: {"name": "helm-guestbook1", "namespace": "default"}
|
||||
`)
|
||||
defaultNamespaceTopLevel2 := strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: {"name": "helm-guestbook2", "namespace": "default"}
|
||||
`)
|
||||
kubesystemNamespaceTopLevel2 := strToUnstructured(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata: {"name": "helm-guestbook3", "namespace": "kube-system"}
|
||||
`)
|
||||
|
||||
cluster := newCluster(defaultNamespaceTopLevel1, defaultNamespaceTopLevel2, kubesystemNamespaceTopLevel2)
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
resources := cluster.getNamespaceTopLevelResources("default")
|
||||
assert.Len(t, resources, 2)
|
||||
assert.Equal(t, resources[kube.GetResourceKey(defaultNamespaceTopLevel1)].Name, "helm-guestbook1")
|
||||
assert.Equal(t, resources[kube.GetResourceKey(defaultNamespaceTopLevel2)].Name, "helm-guestbook2")
|
||||
|
||||
resources = cluster.getNamespaceTopLevelResources("kube-system")
|
||||
assert.Len(t, resources, 1)
|
||||
assert.Equal(t, resources[kube.GetResourceKey(kubesystemNamespaceTopLevel2)].Name, "helm-guestbook3")
|
||||
}
|
||||
|
||||
func TestGetChildren(t *testing.T) {
|
||||
cluster := newCluster(testPod, testRS, testDeploy)
|
||||
err := cluster.ensureSynced()
|
||||
@@ -370,8 +451,10 @@ func TestUpdateResourceTags(t *testing.T) {
|
||||
func TestUpdateAppResource(t *testing.T) {
|
||||
updatesReceived := make([]string, 0)
|
||||
cluster := newCluster(testPod, testRS, testDeploy)
|
||||
cluster.onAppUpdated = func(appName string, fullRefresh bool, _ corev1.ObjectReference) {
|
||||
updatesReceived = append(updatesReceived, fmt.Sprintf("%s: %v", appName, fullRefresh))
|
||||
cluster.onObjectUpdated = func(managedByApp map[string]bool, _ corev1.ObjectReference) {
|
||||
for appName, fullRefresh := range managedByApp {
|
||||
updatesReceived = append(updatesReceived, fmt.Sprintf("%s: %v", appName, fullRefresh))
|
||||
}
|
||||
}
|
||||
|
||||
err := cluster.ensureSynced()
|
||||
@@ -421,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)
|
||||
@@ -434,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"})
|
||||
|
||||
17
controller/cache/info.go
vendored
@@ -31,7 +31,7 @@ func populateNodeInfo(un *unstructured.Unstructured, node *node) {
|
||||
populateServiceInfo(un, node)
|
||||
return
|
||||
}
|
||||
case "extensions":
|
||||
case "extensions", "networking.k8s.io":
|
||||
switch gvk.Kind {
|
||||
case kube.IngressKind:
|
||||
populateIngressInfo(un, node)
|
||||
@@ -126,14 +126,23 @@ func populateIngressInfo(un *unstructured.Unstructured, node *node) {
|
||||
stringPort = fmt.Sprintf("%v", port)
|
||||
}
|
||||
|
||||
var externalURL string
|
||||
switch stringPort {
|
||||
case "80", "http":
|
||||
urlsSet[fmt.Sprintf("http://%s", host)] = true
|
||||
externalURL = fmt.Sprintf("http://%s", host)
|
||||
case "443", "https":
|
||||
urlsSet[fmt.Sprintf("https://%s", host)] = true
|
||||
externalURL = fmt.Sprintf("https://%s", host)
|
||||
default:
|
||||
urlsSet[fmt.Sprintf("http://%s:%s", host, stringPort)] = true
|
||||
externalURL = fmt.Sprintf("http://%s:%s", host, stringPort)
|
||||
}
|
||||
|
||||
subPath := ""
|
||||
if nestedPath, ok, err := unstructured.NestedString(path, "path"); ok && err == nil {
|
||||
subPath = nestedPath
|
||||
}
|
||||
|
||||
externalURL += subPath
|
||||
urlsSet[externalURL] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
118
controller/cache/info_test.go
vendored
@@ -68,7 +68,7 @@ func TestGetIngressInfo(t *testing.T) {
|
||||
Kind: kube.ServiceKind,
|
||||
Name: "helm-guestbook",
|
||||
}},
|
||||
ExternalURLs: []string{"https://helm-guestbook.com"},
|
||||
ExternalURLs: []string{"https://helm-guestbook.com/"},
|
||||
}, node.networkingInfo)
|
||||
}
|
||||
|
||||
@@ -103,6 +103,120 @@ func TestGetIngressInfoNoHost(t *testing.T) {
|
||||
Kind: kube.ServiceKind,
|
||||
Name: "helm-guestbook",
|
||||
}},
|
||||
ExternalURLs: []string{"https://107.178.210.11"},
|
||||
ExternalURLs: []string{"https://107.178.210.11/"},
|
||||
}, node.networkingInfo)
|
||||
}
|
||||
func TestExternalUrlWithSubPath(t *testing.T) {
|
||||
ingress := strToUnstructured(`
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
spec:
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: helm-guestbook
|
||||
servicePort: 443
|
||||
path: /my/sub/path/
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
- ip: 107.178.210.11`)
|
||||
|
||||
node := &node{}
|
||||
populateNodeInfo(ingress, node)
|
||||
|
||||
expectedExternalUrls := []string{"https://107.178.210.11/my/sub/path/"}
|
||||
assert.Equal(t, expectedExternalUrls, node.networkingInfo.ExternalURLs)
|
||||
}
|
||||
func TestExternalUrlWithMultipleSubPaths(t *testing.T) {
|
||||
ingress := strToUnstructured(`
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
spec:
|
||||
rules:
|
||||
- host: helm-guestbook.com
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: helm-guestbook
|
||||
servicePort: 443
|
||||
path: /my/sub/path/
|
||||
- backend:
|
||||
serviceName: helm-guestbook-2
|
||||
servicePort: 443
|
||||
path: /my/sub/path/2
|
||||
- backend:
|
||||
serviceName: helm-guestbook-3
|
||||
servicePort: 443
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
- ip: 107.178.210.11`)
|
||||
|
||||
node := &node{}
|
||||
populateNodeInfo(ingress, node)
|
||||
|
||||
expectedExternalUrls := []string{"https://helm-guestbook.com/my/sub/path/", "https://helm-guestbook.com/my/sub/path/2", "https://helm-guestbook.com"}
|
||||
actualURLs := node.networkingInfo.ExternalURLs
|
||||
sort.Strings(expectedExternalUrls)
|
||||
sort.Strings(actualURLs)
|
||||
assert.Equal(t, expectedExternalUrls, actualURLs)
|
||||
}
|
||||
func TestExternalUrlWithNoSubPath(t *testing.T) {
|
||||
ingress := strToUnstructured(`
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
spec:
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: helm-guestbook
|
||||
servicePort: 443
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
- ip: 107.178.210.11`)
|
||||
|
||||
node := &node{}
|
||||
populateNodeInfo(ingress, node)
|
||||
|
||||
expectedExternalUrls := []string{"https://107.178.210.11"}
|
||||
assert.Equal(t, expectedExternalUrls, node.networkingInfo.ExternalURLs)
|
||||
}
|
||||
|
||||
func TestExternalUrlWithNetworkingApi(t *testing.T) {
|
||||
ingress := strToUnstructured(`
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
spec:
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: helm-guestbook
|
||||
servicePort: 443
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
- ip: 107.178.210.11`)
|
||||
|
||||
node := &node{}
|
||||
populateNodeInfo(ingress, node)
|
||||
|
||||
expectedExternalUrls := []string{"https://107.178.210.11"}
|
||||
assert.Equal(t, expectedExternalUrls, node.networkingInfo.ExternalURLs)
|
||||
}
|
||||
|
||||
117
controller/cache/mocks/LiveStateCache.go
vendored
@@ -2,17 +2,42 @@
|
||||
|
||||
package mocks
|
||||
|
||||
import context "context"
|
||||
import kube "github.com/argoproj/argo-cd/util/kube"
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
import unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
import v1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
import (
|
||||
context "context"
|
||||
|
||||
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"
|
||||
|
||||
v1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
// LiveStateCache is an autogenerated mock type for the LiveStateCache type
|
||||
type LiveStateCache struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -36,25 +61,22 @@ func (_m *LiveStateCache) GetManagedLiveObjs(a *v1alpha1.Application, targetObjs
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Invalidate provides a mock function with given fields:
|
||||
func (_m *LiveStateCache) Invalidate() {
|
||||
_m.Called()
|
||||
}
|
||||
// GetNamespaceTopLevelResources provides a mock function with given fields: server, namespace
|
||||
func (_m *LiveStateCache) GetNamespaceTopLevelResources(server string, namespace string) (map[kube.ResourceKey]v1alpha1.ResourceNode, error) {
|
||||
ret := _m.Called(server, namespace)
|
||||
|
||||
// IsNamespaced provides a mock function with given fields: server, obj
|
||||
func (_m *LiveStateCache) IsNamespaced(server string, obj *unstructured.Unstructured) (bool, error) {
|
||||
ret := _m.Called(server, obj)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(string, *unstructured.Unstructured) bool); ok {
|
||||
r0 = rf(server, obj)
|
||||
var r0 map[kube.ResourceKey]v1alpha1.ResourceNode
|
||||
if rf, ok := ret.Get(0).(func(string, string) map[kube.ResourceKey]v1alpha1.ResourceNode); ok {
|
||||
r0 = rf(server, namespace)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[kube.ResourceKey]v1alpha1.ResourceNode)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, *unstructured.Unstructured) error); ok {
|
||||
r1 = rf(server, obj)
|
||||
if rf, ok := ret.Get(1).(func(string, string) error); ok {
|
||||
r1 = rf(server, namespace)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
@@ -62,13 +84,60 @@ func (_m *LiveStateCache) IsNamespaced(server string, obj *unstructured.Unstruct
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// IterateHierarchy provides a mock function with given fields: server, obj, action
|
||||
func (_m *LiveStateCache) IterateHierarchy(server string, obj *unstructured.Unstructured, action func(v1alpha1.ResourceNode)) error {
|
||||
ret := _m.Called(server, obj, action)
|
||||
// GetServerVersion provides a mock function with given fields: serverURL
|
||||
func (_m *LiveStateCache) GetServerVersion(serverURL string) (string, 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 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(serverURL)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Invalidate provides a mock function with given fields:
|
||||
func (_m *LiveStateCache) Invalidate() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// IsNamespaced provides a mock function with given fields: server, gk
|
||||
func (_m *LiveStateCache) IsNamespaced(server string, gk schema.GroupKind) (bool, error) {
|
||||
ret := _m.Called(server, gk)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(string, schema.GroupKind) bool); ok {
|
||||
r0 = rf(server, gk)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, schema.GroupKind) error); ok {
|
||||
r1 = rf(server, gk)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// IterateHierarchy provides a mock function with given fields: server, key, action
|
||||
func (_m *LiveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(v1alpha1.ResourceNode, string)) error {
|
||||
ret := _m.Called(server, key, action)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, *unstructured.Unstructured, func(v1alpha1.ResourceNode)) error); ok {
|
||||
r0 = rf(server, obj, action)
|
||||
if rf, ok := ret.Get(0).(func(string, kube.ResourceKey, func(v1alpha1.ResourceNode, string)) error); ok {
|
||||
r0 = rf(server, key, action)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
17
controller/cache/node.go
vendored
@@ -35,12 +35,15 @@ func (n *node) resourceKey() kube.ResourceKey {
|
||||
}
|
||||
|
||||
func (n *node) isParentOf(child *node) bool {
|
||||
// Special case for endpoint. Remove after https://github.com/kubernetes/kubernetes/issues/28483 is fixed
|
||||
if len(child.ownerRefs) == 0 && child.ref.APIVersion == "v1" && child.ref.Kind == kube.EndpointsKind && n.ref.APIVersion == "v1" && n.ref.Kind == kube.ServiceKind && n.ref.Name == child.ref.Name {
|
||||
child.ownerRefs = []metav1.OwnerReference{{Name: n.ref.Name, Kind: n.ref.Kind, APIVersion: n.ref.APIVersion, UID: n.ref.UID}}
|
||||
}
|
||||
for i, ownerRef := range child.ownerRefs {
|
||||
|
||||
// backfill UID of inferred owner child references
|
||||
if ownerRef.UID == "" && n.ref.Kind == ownerRef.Kind && n.ref.APIVersion == ownerRef.APIVersion && n.ref.Name == ownerRef.Name {
|
||||
ownerRef.UID = n.ref.UID
|
||||
child.ownerRefs[i] = ownerRef
|
||||
return true
|
||||
}
|
||||
|
||||
for _, ownerRef := range child.ownerRefs {
|
||||
if n.ref.UID == ownerRef.UID {
|
||||
return true
|
||||
}
|
||||
@@ -124,14 +127,14 @@ func (n *node) asResourceNode() appv1.ResourceNode {
|
||||
}
|
||||
}
|
||||
|
||||
func (n *node) iterateChildren(ns map[kube.ResourceKey]*node, parents map[kube.ResourceKey]bool, action func(child appv1.ResourceNode)) {
|
||||
func (n *node) iterateChildren(ns map[kube.ResourceKey]*node, parents map[kube.ResourceKey]bool, action func(child appv1.ResourceNode, appName string)) {
|
||||
for childKey, child := range ns {
|
||||
if n.isParentOf(ns[childKey]) {
|
||||
if parents[childKey] {
|
||||
key := n.resourceKey()
|
||||
log.Warnf("Circular dependency detected. %s is child and parent of %s", childKey.String(), key.String())
|
||||
} else {
|
||||
action(child.asResourceNode())
|
||||
action(child.asResourceNode(), child.getApp(ns))
|
||||
child.iterateChildren(ns, newResourceKeySet(parents, n.resourceKey()), action)
|
||||
}
|
||||
}
|
||||
|
||||
28
controller/cache/node_test.go
vendored
@@ -51,5 +51,33 @@ metadata:
|
||||
parent := c.createObjInfo(testService, "")
|
||||
|
||||
assert.True(t, parent.isParentOf(matchingNameEndPoint))
|
||||
assert.Equal(t, parent.ref.UID, matchingNameEndPoint.ownerRefs[0].UID)
|
||||
assert.False(t, parent.isParentOf(nonMatchingNameEndPoint))
|
||||
}
|
||||
|
||||
func TestIsServiceAccoountParentOfSecret(t *testing.T) {
|
||||
serviceAccount := c.createObjInfo(strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: default
|
||||
namespace: default
|
||||
uid: '123'
|
||||
secrets:
|
||||
- name: default-token-123
|
||||
`), "")
|
||||
tokenSecret := c.createObjInfo(strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
annotations:
|
||||
kubernetes.io/service-account.name: default
|
||||
kubernetes.io/service-account.uid: '123'
|
||||
name: default-token-123
|
||||
namespace: default
|
||||
uid: '345'
|
||||
type: kubernetes.io/service-account-token
|
||||
`), "")
|
||||
|
||||
assert.True(t, serviceAccount.isParentOf(tokenSecret))
|
||||
}
|
||||
|
||||
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,6 +1,7 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -18,9 +19,13 @@ import (
|
||||
|
||||
type MetricsServer struct {
|
||||
*http.Server
|
||||
syncCounter *prometheus.CounterVec
|
||||
k8sRequestCounter *prometheus.CounterVec
|
||||
reconcileHistogram *prometheus.HistogramVec
|
||||
syncCounter *prometheus.CounterVec
|
||||
kubectlExecCounter *prometheus.CounterVec
|
||||
kubectlExecPendingGauge *prometheus.GaugeVec
|
||||
k8sRequestCounter *prometheus.CounterVec
|
||||
clusterEventsCounter *prometheus.CounterVec
|
||||
reconcileHistogram *prometheus.HistogramVec
|
||||
registry *prometheus.Registry
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -62,10 +67,13 @@ var (
|
||||
// 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{}))
|
||||
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)
|
||||
|
||||
syncCounter := prometheus.NewCounterVec(
|
||||
@@ -75,7 +83,8 @@ func NewMetricsServer(addr string, appLister applister.ApplicationLister, health
|
||||
},
|
||||
append(descAppDefaultLabels, "phase"),
|
||||
)
|
||||
appRegistry.MustRegister(syncCounter)
|
||||
registry.MustRegister(syncCounter)
|
||||
|
||||
k8sRequestCounter := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "argocd_app_k8s_request_total",
|
||||
@@ -83,7 +92,18 @@ func NewMetricsServer(addr string, appLister applister.ApplicationLister, health
|
||||
},
|
||||
append(descAppDefaultLabels, "response_code"),
|
||||
)
|
||||
appRegistry.MustRegister(k8sRequestCounter)
|
||||
registry.MustRegister(k8sRequestCounter)
|
||||
|
||||
kubectlExecCounter := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "argocd_kubectl_exec_total",
|
||||
Help: "Number of kubectl executions",
|
||||
}, []string{"command"})
|
||||
registry.MustRegister(kubectlExecCounter)
|
||||
kubectlExecPendingGauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Name: "argocd_kubectl_exec_pending",
|
||||
Help: "Number of pending kubectl executions",
|
||||
}, []string{"command"})
|
||||
registry.MustRegister(kubectlExecPendingGauge)
|
||||
|
||||
reconcileHistogram := prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
@@ -92,22 +112,37 @@ func NewMetricsServer(addr string, appLister applister.ApplicationLister, health
|
||||
// Buckets chosen after observing a ~2100ms mean reconcile time
|
||||
Buckets: []float64{0.25, .5, 1, 2, 4, 8, 16},
|
||||
},
|
||||
append(descAppDefaultLabels),
|
||||
descAppDefaultLabels,
|
||||
)
|
||||
|
||||
appRegistry.MustRegister(reconcileHistogram)
|
||||
registry.MustRegister(reconcileHistogram)
|
||||
clusterEventsCounter := prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Name: "argocd_cluster_events_total",
|
||||
Help: "Number of processes k8s resource events.",
|
||||
}, descClusterDefaultLabels)
|
||||
registry.MustRegister(clusterEventsCounter)
|
||||
|
||||
return &MetricsServer{
|
||||
registry: registry,
|
||||
Server: &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
},
|
||||
syncCounter: syncCounter,
|
||||
k8sRequestCounter: k8sRequestCounter,
|
||||
reconcileHistogram: reconcileHistogram,
|
||||
syncCounter: syncCounter,
|
||||
k8sRequestCounter: k8sRequestCounter,
|
||||
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() {
|
||||
@@ -116,6 +151,23 @@ func (m *MetricsServer) IncSync(app *argoappv1.Application, state *argoappv1.Ope
|
||||
m.syncCounter.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject(), string(state.Phase)).Inc()
|
||||
}
|
||||
|
||||
func (m *MetricsServer) IncKubectlExec(command string) {
|
||||
m.kubectlExecCounter.WithLabelValues(command).Inc()
|
||||
}
|
||||
|
||||
func (m *MetricsServer) IncKubectlExecPending(command string) {
|
||||
m.kubectlExecPendingGauge.WithLabelValues(command).Inc()
|
||||
}
|
||||
|
||||
func (m *MetricsServer) DecKubectlExecPending(command string) {
|
||||
m.kubectlExecPendingGauge.WithLabelValues(command).Dec()
|
||||
}
|
||||
|
||||
// IncClusterEventsCount increments the number of cluster events
|
||||
func (m *MetricsServer) IncClusterEventsCount(server string) {
|
||||
m.clusterEventsCounter.WithLabelValues(server).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()
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
@@ -27,6 +28,7 @@ import (
|
||||
hookutil "github.com/argoproj/argo-cd/util/hook"
|
||||
kubeutil "github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/resource"
|
||||
"github.com/argoproj/argo-cd/util/resource/ignore"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
@@ -51,27 +53,35 @@ func GetLiveObjs(res []managedResource) []*unstructured.Unstructured {
|
||||
}
|
||||
|
||||
type ResourceInfoProvider interface {
|
||||
IsNamespaced(server string, obj *unstructured.Unstructured) (bool, error)
|
||||
IsNamespaced(server string, gk schema.GroupKind) (bool, error)
|
||||
}
|
||||
|
||||
// AppStateManager defines methods which allow to compare application spec and actual application state.
|
||||
type AppStateManager interface {
|
||||
CompareAppState(app *v1alpha1.Application, revision string, source v1alpha1.ApplicationSource, noCache bool, localObjects []string) (*comparisonResult, error)
|
||||
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
|
||||
managedResources []managedResource
|
||||
conditions []v1alpha1.ApplicationCondition
|
||||
hooks []*unstructured.Unstructured
|
||||
diffNormalizer diff.Normalizer
|
||||
appSourceType v1alpha1.ApplicationSourceType
|
||||
}
|
||||
|
||||
func (cr *comparisonResult) targetObjs() []*unstructured.Unstructured {
|
||||
objs := cr.hooks
|
||||
for _, r := range cr.managedResources {
|
||||
if r.Target != nil {
|
||||
objs = append(objs, r.Target)
|
||||
}
|
||||
}
|
||||
return objs
|
||||
}
|
||||
|
||||
// appStateManager allows to compare applications to git
|
||||
type appStateManager struct {
|
||||
metricsServer *metrics.MetricsServer
|
||||
@@ -86,7 +96,7 @@ 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) {
|
||||
helmRepos, err := m.db.ListHelmRepos(context.Background())
|
||||
helmRepos, err := m.db.ListHelmRepositories(context.Background())
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
@@ -118,9 +128,13 @@ func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
serverVersion, err := m.liveStateCache.GetServerVersion(app.Spec.Destination.Server)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
manifestInfo, err := repoClient.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: repo,
|
||||
HelmRepos: helmRepos,
|
||||
Repos: helmRepos,
|
||||
Revision: revision,
|
||||
NoCache: noCache,
|
||||
AppLabelKey: appLabelKey,
|
||||
@@ -131,14 +145,16 @@ func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1
|
||||
KustomizeOptions: &appv1.KustomizeOptions{
|
||||
BuildOptions: buildOptions,
|
||||
},
|
||||
KubeVersion: serverVersion,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
targetObjs, hooks, nil := unmarshalManifests(manifestInfo.Manifests)
|
||||
targetObjs, hooks, err := unmarshalManifests(manifestInfo.Manifests)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
return targetObjs, hooks, manifestInfo, nil
|
||||
|
||||
}
|
||||
|
||||
func unmarshalManifests(manifests []string) ([]*unstructured.Unstructured, []*unstructured.Unstructured, error) {
|
||||
@@ -149,7 +165,7 @@ func unmarshalManifests(manifests []string) ([]*unstructured.Unstructured, []*un
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if resource.Ignore(obj) {
|
||||
if ignore.Ignore(obj) {
|
||||
continue
|
||||
}
|
||||
if hookutil.IsHook(obj) {
|
||||
@@ -171,7 +187,7 @@ func DeduplicateTargetObjects(
|
||||
targetByKey := make(map[kubeutil.ResourceKey][]*unstructured.Unstructured)
|
||||
for i := range objs {
|
||||
obj := objs[i]
|
||||
isNamespaced, err := infoProvider.IsNamespaced(server, obj)
|
||||
isNamespaced, err := infoProvider.IsNamespaced(server, obj.GroupVersionKind().GroupKind())
|
||||
if err != nil {
|
||||
return objs, nil, err
|
||||
}
|
||||
@@ -187,9 +203,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])
|
||||
@@ -234,44 +252,63 @@ func dedupLiveResources(targetObjs []*unstructured.Unstructured, liveObjsByKey m
|
||||
}
|
||||
}
|
||||
|
||||
// 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, error) {
|
||||
func (m *appStateManager) getComparisonSettings(app *appv1.Application) (string, map[string]v1alpha1.ResourceOverride, diff.Normalizer, error) {
|
||||
resourceOverrides, err := m.settingsMgr.GetResourceOverrides()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", nil, nil, err
|
||||
}
|
||||
appLabelKey, err := m.settingsMgr.GetAppInstanceLabelKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", nil, nil, err
|
||||
}
|
||||
diffNormalizer, err := argo.NewDiffNormalizer(app.Spec.IgnoreDifferences, resourceOverrides)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", nil, nil, err
|
||||
}
|
||||
logCtx := log.WithField("application", app.Name)
|
||||
logCtx.Infof("Comparing app state (cluster: %s, namespace: %s)", app.Spec.Destination.Server, app.Spec.Destination.Namespace)
|
||||
observedAt := metav1.Now()
|
||||
return appLabelKey, resourceOverrides, diffNormalizer, nil
|
||||
}
|
||||
|
||||
// CompareAppState compares application git state to the live app state, using the specified
|
||||
// revision and supplied source. If revision or overrides are empty, then compares against
|
||||
// revision and overrides in the app spec.
|
||||
func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *appv1.AppProject, revision string, source v1alpha1.ApplicationSource, noCache bool, localManifests []string) *comparisonResult {
|
||||
appLabelKey, resourceOverrides, diffNormalizer, err := m.getComparisonSettings(app)
|
||||
|
||||
// return unknown comparison result if basic comparison settings cannot be loaded
|
||||
if err != nil {
|
||||
return &comparisonResult{
|
||||
syncStatus: &v1alpha1.SyncStatus{
|
||||
ComparedTo: appv1.ComparedTo{Source: source, Destination: app.Spec.Destination},
|
||||
Status: appv1.SyncStatusCodeUnknown,
|
||||
},
|
||||
healthStatus: &appv1.HealthStatus{Status: appv1.HealthStatusUnknown},
|
||||
}
|
||||
}
|
||||
|
||||
// do best effort loading live and target state to present as much information about app state as possible
|
||||
failedToLoadObjs := false
|
||||
conditions := make([]v1alpha1.ApplicationCondition, 0)
|
||||
|
||||
logCtx := log.WithField("application", app.Name)
|
||||
logCtx.Infof("Comparing app state (cluster: %s, namespace: %s)", app.Spec.Destination.Server, app.Spec.Destination.Namespace)
|
||||
|
||||
var targetObjs []*unstructured.Unstructured
|
||||
var hooks []*unstructured.Unstructured
|
||||
var manifestInfo *apiclient.ManifestResponse
|
||||
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
|
||||
@@ -279,13 +316,13 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
|
||||
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...)
|
||||
|
||||
resFilter, err := m.settingsMgr.GetResourcesFilter()
|
||||
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})
|
||||
} else {
|
||||
for i := len(targetObjs) - 1; i >= 0; i-- {
|
||||
targetObj := targetObjs[i]
|
||||
@@ -293,8 +330,9 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
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()),
|
||||
Type: v1alpha1.ApplicationConditionExcludedResourceWarning,
|
||||
Message: fmt.Sprintf("Resource %s/%s %s is excluded in the settings", gvk.Group, gvk.Kind, targetObj.GetName()),
|
||||
LastTransitionTime: &now,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -302,20 +340,28 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
|
||||
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()})
|
||||
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("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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -325,7 +371,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
for i, obj := range targetObjs {
|
||||
gvk := obj.GroupVersionKind()
|
||||
ns := util.FirstNonEmpty(obj.GetNamespace(), app.Spec.Destination.Namespace)
|
||||
if namespaced, err := m.liveStateCache.IsNamespaced(app.Spec.Destination.Server, obj); err == nil && !namespaced {
|
||||
if namespaced, err := m.liveStateCache.IsNamespaced(app.Spec.Destination.Server, obj.GroupVersionKind().GroupKind()); err == nil && !namespaced {
|
||||
ns = ""
|
||||
}
|
||||
key := kubeutil.NewResourceKey(gvk.Group, gvk.Kind, ns, obj.GetName())
|
||||
@@ -348,7 +394,9 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
// Do the actual comparison
|
||||
diffResults, err := diff.DiffArray(targetObjs, managedLiveObj, diffNormalizer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
diffResults = &diff.DiffResultList{}
|
||||
failedToLoadObjs = true
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
|
||||
}
|
||||
|
||||
syncCode := v1alpha1.SyncStatusCodeSynced
|
||||
@@ -376,7 +424,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
}
|
||||
|
||||
diffResult := diffResults.Diffs[i]
|
||||
if resState.Hook || resource.Ignore(obj) {
|
||||
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 {
|
||||
// Set resource state to OutOfSync since one of the following is true:
|
||||
@@ -392,6 +440,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,
|
||||
@@ -425,23 +483,27 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
})
|
||||
|
||||
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: observedAt,
|
||||
syncStatus: &syncStatus,
|
||||
healthStatus: healthStatus,
|
||||
resources: resourceSummaries,
|
||||
managedResources: managedResources,
|
||||
conditions: conditions,
|
||||
hooks: hooks,
|
||||
diffNormalizer: diffNormalizer,
|
||||
}
|
||||
if manifestInfo != nil {
|
||||
compRes.appSourceType = v1alpha1.ApplicationSourceType(manifestInfo.SourceType)
|
||||
}
|
||||
return &compRes, nil
|
||||
app.Status.SetConditions(conditions, map[appv1.ApplicationConditionType]bool{
|
||||
appv1.ApplicationConditionComparisonError: true,
|
||||
appv1.ApplicationConditionSharedResourceWarning: true,
|
||||
appv1.ApplicationConditionRepeatedResourceWarning: true,
|
||||
appv1.ApplicationConditionExcludedResourceWarning: true,
|
||||
})
|
||||
return &compRes
|
||||
}
|
||||
|
||||
func (m *appStateManager) persistRevisionHistory(app *v1alpha1.Application, revision string, source v1alpha1.ApplicationSource) error {
|
||||
@@ -449,20 +511,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,22 +30,22 @@ func TestCompareAppStateEmpty(t *testing.T) {
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false, nil)
|
||||
assert.NoError(t, err)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.NotNil(t, compRes.syncStatus)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Equal(t, 0, len(compRes.resources))
|
||||
assert.Equal(t, 0, len(compRes.managedResources))
|
||||
assert.Equal(t, 0, len(compRes.conditions))
|
||||
assert.Len(t, compRes.resources, 0)
|
||||
assert.Len(t, compRes.managedResources, 0)
|
||||
assert.Len(t, app.Status.Conditions, 0)
|
||||
}
|
||||
|
||||
// TestCompareAppStateMissing tests when there is a manifest defined in git which doesn't exist in live
|
||||
// TestCompareAppStateMissing tests when there is a manifest defined in the repo which doesn't exist in live
|
||||
func TestCompareAppStateMissing(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
apps: []runtime.Object{app},
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{string(test.PodManifest)},
|
||||
Manifests: []string{test.PodManifest},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
@@ -53,13 +53,13 @@ func TestCompareAppStateMissing(t *testing.T) {
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false, nil)
|
||||
assert.NoError(t, err)
|
||||
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)
|
||||
assert.Equal(t, 1, len(compRes.resources))
|
||||
assert.Equal(t, 1, len(compRes.managedResources))
|
||||
assert.Equal(t, 0, len(compRes.conditions))
|
||||
assert.Len(t, compRes.resources, 1)
|
||||
assert.Len(t, compRes.managedResources, 1)
|
||||
assert.Len(t, app.Status.Conditions, 0)
|
||||
}
|
||||
|
||||
// TestCompareAppStateExtra tests when there is an extra object in live but not defined in git
|
||||
@@ -80,13 +80,12 @@ func TestCompareAppStateExtra(t *testing.T) {
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false, nil)
|
||||
assert.NoError(t, err)
|
||||
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))
|
||||
assert.Equal(t, 1, len(compRes.managedResources))
|
||||
assert.Equal(t, 0, len(compRes.conditions))
|
||||
assert.Equal(t, 0, len(app.Status.Conditions))
|
||||
}
|
||||
|
||||
// TestCompareAppStateHook checks that hooks are detected during manifest generation, and not
|
||||
@@ -107,14 +106,13 @@ func TestCompareAppStateHook(t *testing.T) {
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false, nil)
|
||||
assert.NoError(t, err)
|
||||
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))
|
||||
assert.Equal(t, 0, len(compRes.managedResources))
|
||||
assert.Equal(t, 1, len(compRes.hooks))
|
||||
assert.Equal(t, 0, len(compRes.conditions))
|
||||
assert.Equal(t, 0, len(app.Status.Conditions))
|
||||
}
|
||||
|
||||
// checks that ignore resources are detected, but excluded from status
|
||||
@@ -134,14 +132,13 @@ func TestCompareAppStateCompareOptionIgnoreExtraneous(t *testing.T) {
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false, nil)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Len(t, compRes.resources, 0)
|
||||
assert.Len(t, compRes.managedResources, 0)
|
||||
assert.Len(t, compRes.conditions, 0)
|
||||
assert.Len(t, app.Status.Conditions, 0)
|
||||
}
|
||||
|
||||
// TestCompareAppStateExtraHook tests when there is an extra _hook_ object in live but not defined in git
|
||||
@@ -163,14 +160,14 @@ func TestCompareAppStateExtraHook(t *testing.T) {
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false, nil)
|
||||
assert.NoError(t, err)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
|
||||
assert.NotNil(t, compRes)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Equal(t, 1, len(compRes.resources))
|
||||
assert.Equal(t, 1, len(compRes.managedResources))
|
||||
assert.Equal(t, 0, len(compRes.hooks))
|
||||
assert.Equal(t, 0, len(compRes.conditions))
|
||||
assert.Equal(t, 0, len(app.Status.Conditions))
|
||||
}
|
||||
|
||||
func toJSON(t *testing.T, obj *unstructured.Unstructured) string {
|
||||
@@ -200,13 +197,13 @@ func TestCompareAppStateDuplicatedNamespacedResources(t *testing.T) {
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false, nil)
|
||||
assert.NoError(t, err)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
|
||||
assert.NotNil(t, compRes)
|
||||
assert.Contains(t, compRes.conditions, argoappv1.ApplicationCondition{
|
||||
Message: "Resource /Pod/fake-dest-ns/my-pod appeared 2 times among application resources.",
|
||||
Type: argoappv1.ApplicationConditionRepeatedResourceWarning,
|
||||
})
|
||||
assert.Equal(t, 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))
|
||||
}
|
||||
|
||||
@@ -251,8 +248,7 @@ func TestSetHealth(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false, nil)
|
||||
assert.NoError(t, err)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
|
||||
assert.Equal(t, compRes.healthStatus.Status, argoappv1.HealthStatusHealthy)
|
||||
}
|
||||
@@ -284,8 +280,161 @@ func TestSetHealthSelfReferencedApp(t *testing.T) {
|
||||
},
|
||||
})
|
||||
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false, nil)
|
||||
assert.NoError(t, err)
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
|
||||
assert.Equal(t, compRes.healthStatus.Status, argoappv1.HealthStatusHealthy)
|
||||
}
|
||||
|
||||
func TestSetManagedResourcesWithOrphanedResources(t *testing.T) {
|
||||
proj := defaultProj.DeepCopy()
|
||||
proj.Spec.OrphanedResources = &argoappv1.OrphanedResourcesMonitorSettings{}
|
||||
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{
|
||||
apps: []runtime.Object{app, proj},
|
||||
namespacedResources: map[kube.ResourceKey]namespacedResource{
|
||||
kube.NewResourceKey("apps", kube.DeploymentKind, app.Namespace, "guestbook"): {
|
||||
ResourceNode: argoappv1.ResourceNode{
|
||||
ResourceRef: argoappv1.ResourceRef{Kind: kube.DeploymentKind, Name: "guestbook", Namespace: app.Namespace},
|
||||
},
|
||||
AppName: "",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
tree, err := ctrl.setAppManagedResources(app, &comparisonResult{managedResources: make([]managedResource, 0)})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(tree.OrphanedNodes), 1)
|
||||
assert.Equal(t, "guestbook", tree.OrphanedNodes[0].Name)
|
||||
assert.Equal(t, app.Namespace, tree.OrphanedNodes[0].Namespace)
|
||||
}
|
||||
|
||||
func TestSetManagedResourcesWithResourcesOfAnotherApp(t *testing.T) {
|
||||
proj := defaultProj.DeepCopy()
|
||||
proj.Spec.OrphanedResources = &argoappv1.OrphanedResourcesMonitorSettings{}
|
||||
|
||||
app1 := newFakeApp()
|
||||
app1.Name = "app1"
|
||||
app2 := newFakeApp()
|
||||
app2.Name = "app2"
|
||||
|
||||
ctrl := newFakeController(&fakeData{
|
||||
apps: []runtime.Object{app1, app2, proj},
|
||||
namespacedResources: map[kube.ResourceKey]namespacedResource{
|
||||
kube.NewResourceKey("apps", kube.DeploymentKind, app2.Namespace, "guestbook"): {
|
||||
ResourceNode: argoappv1.ResourceNode{
|
||||
ResourceRef: argoappv1.ResourceRef{Kind: kube.DeploymentKind, Name: "guestbook", Namespace: app2.Namespace},
|
||||
},
|
||||
AppName: "app2",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
tree, err := ctrl.setAppManagedResources(app1, &comparisonResult{managedResources: make([]managedResource, 0)})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, len(tree.OrphanedNodes), 0)
|
||||
}
|
||||
|
||||
func TestReturnUnknownComparisonStateOnSettingLoadError(t *testing.T) {
|
||||
proj := defaultProj.DeepCopy()
|
||||
proj.Spec.OrphanedResources = &argoappv1.OrphanedResourcesMonitorSettings{}
|
||||
|
||||
app := newFakeApp()
|
||||
|
||||
ctrl := newFakeController(&fakeData{
|
||||
apps: []runtime.Object{app, proj},
|
||||
configMapData: map[string]string{
|
||||
"resource.customizations": "invalid setting",
|
||||
},
|
||||
})
|
||||
|
||||
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
|
||||
|
||||
assert.Equal(t, argoappv1.HealthStatusUnknown, compRes.healthStatus.Status)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeUnknown, compRes.syncStatus.Status)
|
||||
}
|
||||
|
||||
func TestSetManagedResourcesKnownOrphanedResourceExceptions(t *testing.T) {
|
||||
proj := defaultProj.DeepCopy()
|
||||
proj.Spec.OrphanedResources = &argoappv1.OrphanedResourcesMonitorSettings{}
|
||||
|
||||
app := newFakeApp()
|
||||
app.Namespace = "default"
|
||||
|
||||
ctrl := newFakeController(&fakeData{
|
||||
apps: []runtime.Object{app, proj},
|
||||
namespacedResources: map[kube.ResourceKey]namespacedResource{
|
||||
kube.NewResourceKey("apps", kube.DeploymentKind, app.Namespace, "guestbook"): {
|
||||
ResourceNode: argoappv1.ResourceNode{ResourceRef: argoappv1.ResourceRef{Group: "apps", Kind: kube.DeploymentKind, Name: "guestbook", Namespace: app.Namespace}},
|
||||
},
|
||||
kube.NewResourceKey("", kube.ServiceAccountKind, app.Namespace, "default"): {
|
||||
ResourceNode: argoappv1.ResourceNode{ResourceRef: argoappv1.ResourceRef{Kind: kube.ServiceAccountKind, Name: "default", Namespace: app.Namespace}},
|
||||
},
|
||||
kube.NewResourceKey("", kube.ServiceKind, app.Namespace, "kubernetes"): {
|
||||
ResourceNode: argoappv1.ResourceNode{ResourceRef: argoappv1.ResourceRef{Kind: kube.ServiceAccountKind, Name: "kubernetes", Namespace: app.Namespace}},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
tree, err := ctrl.setAppManagedResources(app, &comparisonResult{managedResources: make([]managedResource, 0)})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, tree.OrphanedNodes, 1)
|
||||
assert.Equal(t, "guestbook", tree.OrphanedNodes[0].Name)
|
||||
}
|
||||
|
||||
func Test_comparisonResult_obs(t *testing.T) {
|
||||
assert.Len(t, (&comparisonResult{}).targetObjs(), 0)
|
||||
assert.Len(t, (&comparisonResult{managedResources: []managedResource{{}}}).targetObjs(), 0)
|
||||
assert.Len(t, (&comparisonResult{managedResources: []managedResource{{Target: test.NewPod()}}}).targetObjs(), 1)
|
||||
assert.Len(t, (&comparisonResult{hooks: []*unstructured.Unstructured{{}}}).targetObjs(), 1)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -7,14 +7,17 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
apierr "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"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"
|
||||
@@ -28,6 +31,7 @@ import (
|
||||
"github.com/argoproj/argo-cd/util/health"
|
||||
"github.com/argoproj/argo-cd/util/hook"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/rand"
|
||||
"github.com/argoproj/argo-cd/util/resource"
|
||||
)
|
||||
|
||||
@@ -35,6 +39,8 @@ const (
|
||||
crdReadinessTimeout = time.Duration(3) * time.Second
|
||||
)
|
||||
|
||||
var syncIdPrefix uint64 = 0
|
||||
|
||||
type syncContext struct {
|
||||
resourceOverrides map[string]v1alpha1.ResourceOverride
|
||||
appName string
|
||||
@@ -101,21 +107,20 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
|
||||
revision = syncOp.Revision
|
||||
}
|
||||
|
||||
compareResult, err := 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 = err.Error()
|
||||
state.Message = fmt.Sprintf("Failed to load application project: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// If there are any error conditions, do not perform the operation
|
||||
errConditions := make([]v1alpha1.ApplicationCondition, 0)
|
||||
for i := range compareResult.conditions {
|
||||
if compareResult.conditions[i].IsError() {
|
||||
errConditions = append(errConditions, compareResult.conditions[i])
|
||||
}
|
||||
}
|
||||
if len(errConditions) > 0 {
|
||||
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{
|
||||
v1alpha1.ApplicationConditionComparisonError: true,
|
||||
v1alpha1.ApplicationConditionInvalidSpecError: true,
|
||||
}); len(errConditions) > 0 {
|
||||
state.Phase = v1alpha1.OperationError
|
||||
state.Message = argo.FormatAppConditions(errConditions)
|
||||
return
|
||||
@@ -153,13 +158,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
|
||||
@@ -167,6 +165,8 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
|
||||
return
|
||||
}
|
||||
|
||||
atomic.AddUint64(&syncIdPrefix, 1)
|
||||
syncId := fmt.Sprintf("%05d-%s", syncIdPrefix, rand.RandString(5))
|
||||
syncCtx := syncContext{
|
||||
resourceOverrides: resourceOverrides,
|
||||
appName: app.Name,
|
||||
@@ -183,16 +183,18 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
|
||||
syncRes: syncRes,
|
||||
syncResources: syncResources,
|
||||
opState: state,
|
||||
log: log.WithFields(log.Fields{"application": app.Name}),
|
||||
log: log.WithFields(log.Fields{"application": app.Name, "syncId": syncId}),
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
|
||||
if state.Phase == v1alpha1.OperationTerminating {
|
||||
syncCtx.terminate()
|
||||
} else {
|
||||
syncCtx.sync()
|
||||
}
|
||||
|
||||
syncCtx.log.Info("sync/terminate complete")
|
||||
syncCtx.log.WithField("duration", time.Since(start)).Info("sync/terminate complete")
|
||||
|
||||
if !syncOp.DryRun && !syncCtx.isSelectiveSync() && syncCtx.opState.Phase.Successful() {
|
||||
err := m.persistRevisionHistory(app, compareResult.syncStatus.Revision, source)
|
||||
@@ -205,8 +207,8 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
|
||||
// sync has performs the actual apply or hook based sync
|
||||
func (sc *syncContext) sync() {
|
||||
sc.log.WithFields(log.Fields{"isSelectiveSync": sc.isSelectiveSync(), "skipHooks": sc.skipHooks(), "started": sc.started()}).Info("syncing")
|
||||
tasks, successful := sc.getSyncTasks()
|
||||
if !successful {
|
||||
tasks, ok := sc.getSyncTasks()
|
||||
if !ok {
|
||||
sc.setOperationPhase(v1alpha1.OperationFailed, "one or more synchronization tasks are not valid")
|
||||
return
|
||||
}
|
||||
@@ -221,7 +223,7 @@ func (sc *syncContext) sync() {
|
||||
// the dry-run for this operation, is if the resource or hook list is empty.
|
||||
if !sc.started() {
|
||||
sc.log.Debug("dry-run")
|
||||
if !sc.runTasks(tasks, true) {
|
||||
if sc.runTasks(tasks, true) == failed {
|
||||
sc.setOperationPhase(v1alpha1.OperationFailed, "one or more objects failed to apply (dry run)")
|
||||
return
|
||||
}
|
||||
@@ -234,16 +236,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 enforceHookDeletePolicy(task.liveObj, task.operationState) {
|
||||
err := sc.deleteResource(task)
|
||||
if err != nil {
|
||||
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)
|
||||
@@ -264,8 +271,11 @@ func (sc *syncContext) sync() {
|
||||
}
|
||||
}
|
||||
|
||||
// any running tasks, lets wait...
|
||||
if tasks.Any(func(t *syncTask) bool { return t.running() }) {
|
||||
// if (a) we are multi-step and we have any running tasks,
|
||||
// or (b) there are any running hooks,
|
||||
// then wait...
|
||||
multiStep := tasks.multiStep()
|
||||
if tasks.Any(func(t *syncTask) bool { return (multiStep || t.isHook()) && t.running() }) {
|
||||
sc.setOperationPhase(v1alpha1.OperationRunning, "one or more tasks are running")
|
||||
return
|
||||
}
|
||||
@@ -279,9 +289,9 @@ func (sc *syncContext) sync() {
|
||||
return
|
||||
}
|
||||
|
||||
sc.log.WithFields(log.Fields{"tasks": tasks}).Debug("filtering out completed tasks")
|
||||
sc.log.WithFields(log.Fields{"tasks": tasks}).Debug("filtering out non-pending tasks")
|
||||
// remove tasks that are completed, we can assume that there are no running tasks
|
||||
tasks = tasks.Filter(func(t *syncTask) bool { return !t.completed() })
|
||||
tasks = tasks.Filter(func(t *syncTask) bool { return t.pending() })
|
||||
|
||||
// If no sync tasks were generated (e.g., in case all application manifests have been removed),
|
||||
// the sync operation is successful.
|
||||
@@ -305,9 +315,11 @@ func (sc *syncContext) sync() {
|
||||
sc.setOperationPhase(v1alpha1.OperationRunning, "one or more tasks are running")
|
||||
|
||||
sc.log.WithFields(log.Fields{"tasks": tasks}).Debug("wet-run")
|
||||
if !sc.runTasks(tasks, false) {
|
||||
runState := sc.runTasks(tasks, false)
|
||||
switch runState {
|
||||
case failed:
|
||||
sc.setOperationFailed(syncFailTasks, "one or more objects failed to apply")
|
||||
} else {
|
||||
case successful:
|
||||
if complete {
|
||||
sc.setOperationPhase(v1alpha1.OperationSucceeded, "successfully synced (all tasks run)")
|
||||
}
|
||||
@@ -324,7 +336,7 @@ func (sc *syncContext) setOperationFailed(syncFailTasks syncTasks, message strin
|
||||
// otherwise, we need to start the failure hooks, and then return without setting
|
||||
// the phase, so we make sure we have at least one more sync
|
||||
sc.log.WithFields(log.Fields{"syncFailTasks": syncFailTasks}).Debug("running sync fail tasks")
|
||||
if !sc.runTasks(syncFailTasks, false) {
|
||||
if sc.runTasks(syncFailTasks, false) == failed {
|
||||
sc.setOperationPhase(v1alpha1.OperationFailed, message)
|
||||
}
|
||||
} else {
|
||||
@@ -380,7 +392,7 @@ func (sc *syncContext) getSyncTasks() (_ syncTasks, successful bool) {
|
||||
|
||||
for _, resource := range sc.compareResult.managedResources {
|
||||
if !sc.containsResource(resource) {
|
||||
log.WithFields(log.Fields{"group": resource.Group, "kind": resource.Kind, "name": resource.Name}).
|
||||
sc.log.WithFields(log.Fields{"group": resource.Group, "kind": resource.Kind, "name": resource.Name}).
|
||||
Debug("skipping")
|
||||
continue
|
||||
}
|
||||
@@ -389,7 +401,7 @@ func (sc *syncContext) getSyncTasks() (_ syncTasks, successful bool) {
|
||||
|
||||
// this creates garbage tasks
|
||||
if hook.IsHook(obj) {
|
||||
log.WithFields(log.Fields{"group": obj.GroupVersionKind().Group, "kind": obj.GetKind(), "namespace": obj.GetNamespace(), "name": obj.GetName()}).
|
||||
sc.log.WithFields(log.Fields{"group": obj.GroupVersionKind().Group, "kind": obj.GetKind(), "namespace": obj.GetNamespace(), "name": obj.GetName()}).
|
||||
Debug("skipping hook")
|
||||
continue
|
||||
}
|
||||
@@ -474,7 +486,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
|
||||
}
|
||||
@@ -572,13 +584,13 @@ func (sc *syncContext) pruneObject(liveObj *unstructured.Unstructured, prune, dr
|
||||
}
|
||||
|
||||
func (sc *syncContext) hasCRDOfGroupKind(group string, kind string) bool {
|
||||
for _, res := range sc.compareResult.managedResources {
|
||||
if res.Target != nil && kube.IsCRD(res.Target) {
|
||||
crdGroup, ok, err := unstructured.NestedString(res.Target.Object, "spec", "group")
|
||||
for _, obj := range sc.compareResult.targetObjs() {
|
||||
if kube.IsCRD(obj) {
|
||||
crdGroup, ok, err := unstructured.NestedString(obj.Object, "spec", "group")
|
||||
if err != nil || !ok {
|
||||
continue
|
||||
}
|
||||
crdKind, ok, err := unstructured.NestedString(res.Target.Object, "spec", "names", "kind")
|
||||
crdKind, ok, err := unstructured.NestedString(obj.Object, "spec", "names", "kind")
|
||||
if err != nil || !ok {
|
||||
continue
|
||||
}
|
||||
@@ -596,10 +608,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))
|
||||
@@ -607,6 +624,8 @@ func (sc *syncContext) terminate() {
|
||||
} else {
|
||||
sc.setResourceResult(task, "", v1alpha1.OperationSucceeded, fmt.Sprintf("Deleted"))
|
||||
}
|
||||
} else {
|
||||
sc.setResourceResult(task, "", phase, msg)
|
||||
}
|
||||
}
|
||||
if terminateSuccessful {
|
||||
@@ -617,17 +636,25 @@ func (sc *syncContext) terminate() {
|
||||
}
|
||||
|
||||
func (sc *syncContext) deleteResource(task *syncTask) error {
|
||||
sc.log.WithFields(log.Fields{"task": task}).Debug("deleting task")
|
||||
apiResource, err := kube.ServerResourceForGroupVersionKind(sc.disco, task.groupVersionKind())
|
||||
sc.log.WithFields(log.Fields{"task": task}).Debug("deleting resource")
|
||||
resIf, err := sc.getResourceIf(task)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resource := kube.ToGroupVersionResource(task.groupVersionKind().GroupVersion().String(), apiResource)
|
||||
resIf := kube.ToResourceInterface(sc.dynamicIf, apiResource, resource, task.namespace())
|
||||
propagationPolicy := metav1.DeletePropagationForeground
|
||||
return resIf.Delete(task.name(), &metav1.DeleteOptions{PropagationPolicy: &propagationPolicy})
|
||||
}
|
||||
|
||||
func (sc *syncContext) getResourceIf(task *syncTask) (dynamic.ResourceInterface, error) {
|
||||
apiResource, err := kube.ServerResourceForGroupVersionKind(sc.disco, task.groupVersionKind())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := kube.ToGroupVersionResource(task.groupVersionKind().GroupVersion().String(), apiResource)
|
||||
resIf := kube.ToResourceInterface(sc.dynamicIf, apiResource, res, task.namespace())
|
||||
return resIf, err
|
||||
}
|
||||
|
||||
var operationPhases = map[v1alpha1.ResultCode]v1alpha1.OperationPhase{
|
||||
v1alpha1.ResultCodeSynced: v1alpha1.OperationRunning,
|
||||
v1alpha1.ResultCodeSyncFailed: v1alpha1.OperationFailed,
|
||||
@@ -635,13 +662,22 @@ var operationPhases = map[v1alpha1.ResultCode]v1alpha1.OperationPhase{
|
||||
v1alpha1.ResultCodePruneSkipped: v1alpha1.OperationSucceeded,
|
||||
}
|
||||
|
||||
func (sc *syncContext) runTasks(tasks syncTasks, dryRun bool) bool {
|
||||
// tri-state
|
||||
type runState = int
|
||||
|
||||
const (
|
||||
successful = iota
|
||||
pending
|
||||
failed
|
||||
)
|
||||
|
||||
func (sc *syncContext) runTasks(tasks syncTasks, dryRun bool) runState {
|
||||
|
||||
dryRun = dryRun || sc.syncOp.DryRun
|
||||
|
||||
sc.log.WithFields(log.Fields{"numTasks": len(tasks), "dryRun": dryRun}).Debug("running tasks")
|
||||
|
||||
successful := true
|
||||
runState := successful
|
||||
var createTasks syncTasks
|
||||
var pruneTasks syncTasks
|
||||
|
||||
@@ -652,60 +688,96 @@ func (sc *syncContext) runTasks(tasks syncTasks, dryRun bool) bool {
|
||||
createTasks = append(createTasks, task)
|
||||
}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, task := range pruneTasks {
|
||||
wg.Add(1)
|
||||
go func(t *syncTask) {
|
||||
defer wg.Done()
|
||||
sc.log.WithFields(log.Fields{"dryRun": dryRun, "task": t}).Debug("pruning")
|
||||
result, message := sc.pruneObject(t.liveObj, sc.syncOp.Prune, dryRun)
|
||||
if result == v1alpha1.ResultCodeSyncFailed {
|
||||
successful = false
|
||||
}
|
||||
if !dryRun || result == v1alpha1.ResultCodeSyncFailed {
|
||||
sc.setResourceResult(t, result, operationPhases[result], message)
|
||||
}
|
||||
}(task)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
processCreateTasks := func(tasks syncTasks) {
|
||||
var createWg sync.WaitGroup
|
||||
for _, task := range tasks {
|
||||
if dryRun && task.skipDryRun {
|
||||
continue
|
||||
}
|
||||
createWg.Add(1)
|
||||
// prune first
|
||||
{
|
||||
var wg sync.WaitGroup
|
||||
for _, task := range pruneTasks {
|
||||
wg.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())
|
||||
defer wg.Done()
|
||||
logCtx := sc.log.WithFields(log.Fields{"dryRun": dryRun, "task": t})
|
||||
logCtx.Debug("pruning")
|
||||
result, message := sc.pruneObject(t.liveObj, sc.syncOp.Prune, dryRun)
|
||||
if result == v1alpha1.ResultCodeSyncFailed {
|
||||
successful = false
|
||||
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)
|
||||
}
|
||||
createWg.Wait()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
var tasksGroup syncTasks
|
||||
for _, task := range createTasks {
|
||||
//Only wait if the type of the next task is different than the previous type
|
||||
if len(tasksGroup) > 0 && tasksGroup[0].targetObj.GetKind() != task.kind() {
|
||||
// delete anything that need deleting
|
||||
if runState == successful && createTasks.Any(func(t *syncTask) bool { return t.needsDeleting() }) {
|
||||
var wg sync.WaitGroup
|
||||
for _, task := range createTasks.Filter(func(t *syncTask) bool { return t.needsDeleting() }) {
|
||||
wg.Add(1)
|
||||
go func(t *syncTask) {
|
||||
defer wg.Done()
|
||||
sc.log.WithFields(log.Fields{"dryRun": dryRun, "task": t}).Debug("deleting")
|
||||
if !dryRun {
|
||||
err := sc.deleteResource(t)
|
||||
if err != nil {
|
||||
// it is possible to get a race condition here, such that the resource does not exist when
|
||||
// delete is requested, we treat this as a nop
|
||||
if !apierr.IsNotFound(err) {
|
||||
runState = failed
|
||||
sc.setResourceResult(t, "", v1alpha1.OperationError, fmt.Sprintf("failed to delete resource: %v", err))
|
||||
}
|
||||
} else {
|
||||
// if there is anything that needs deleting, we are at best now in pending and
|
||||
// want to return and wait for sync to be invoked again
|
||||
runState = pending
|
||||
}
|
||||
}
|
||||
}(task)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
// finally create resources
|
||||
if runState == successful {
|
||||
processCreateTasks := func(tasks syncTasks) {
|
||||
var createWg sync.WaitGroup
|
||||
for _, task := range tasks {
|
||||
if dryRun && task.skipDryRun {
|
||||
continue
|
||||
}
|
||||
createWg.Add(1)
|
||||
go func(t *syncTask) {
|
||||
defer createWg.Done()
|
||||
logCtx := sc.log.WithFields(log.Fields{"dryRun": dryRun, "task": t})
|
||||
logCtx.Debug("applying")
|
||||
result, message := sc.applyObject(t.targetObj, dryRun, sc.syncOp.SyncStrategy.Force())
|
||||
if result == v1alpha1.ResultCodeSyncFailed {
|
||||
logCtx.WithField("message", message).Info("apply failed")
|
||||
runState = failed
|
||||
}
|
||||
if !dryRun || sc.syncOp.DryRun || result == v1alpha1.ResultCodeSyncFailed {
|
||||
sc.setResourceResult(t, result, operationPhases[result], message)
|
||||
}
|
||||
}(task)
|
||||
}
|
||||
createWg.Wait()
|
||||
}
|
||||
|
||||
var tasksGroup syncTasks
|
||||
for _, task := range createTasks {
|
||||
//Only wait if the type of the next task is different than the previous type
|
||||
if len(tasksGroup) > 0 && tasksGroup[0].targetObj.GetKind() != task.kind() {
|
||||
processCreateTasks(tasksGroup)
|
||||
tasksGroup = syncTasks{task}
|
||||
} else {
|
||||
tasksGroup = append(tasksGroup, task)
|
||||
}
|
||||
}
|
||||
if len(tasksGroup) > 0 {
|
||||
processCreateTasks(tasksGroup)
|
||||
tasksGroup = syncTasks{task}
|
||||
} else {
|
||||
tasksGroup = append(tasksGroup, task)
|
||||
}
|
||||
}
|
||||
if len(tasksGroup) > 0 {
|
||||
processCreateTasks(tasksGroup)
|
||||
}
|
||||
return successful
|
||||
return runState
|
||||
}
|
||||
|
||||
// setResourceResult sets a resource details in the SyncResult.Resources list
|
||||
|
||||
@@ -2,160 +2,34 @@ package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
|
||||
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/common"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/health"
|
||||
)
|
||||
|
||||
// enforceHookDeletePolicy examines the hook deletion policy of a object and deletes it based on the status
|
||||
func enforceHookDeletePolicy(hook *unstructured.Unstructured, operation v1alpha1.OperationPhase) bool {
|
||||
|
||||
annotations := hook.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return false
|
||||
}
|
||||
deletePolicies := strings.Split(annotations[common.AnnotationKeyHookDeletePolicy], ",")
|
||||
for _, dp := range deletePolicies {
|
||||
policy := v1alpha1.HookDeletePolicy(strings.TrimSpace(dp))
|
||||
if policy == v1alpha1.HookDeletePolicyHookSucceeded && operation == v1alpha1.OperationSucceeded {
|
||||
return true
|
||||
}
|
||||
if policy == v1alpha1.HookDeletePolicyHookFailed && operation == v1alpha1.OperationFailed {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 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 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"
|
||||
}
|
||||
|
||||
// TODO - should we move this to health.go?
|
||||
func getStatusFromArgoWorkflow(hook *unstructured.Unstructured) (operation v1alpha1.OperationPhase, message string) {
|
||||
var wf wfv1.Workflow
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(hook.Object, &wf)
|
||||
if err != nil {
|
||||
return v1alpha1.OperationError, err.Error()
|
||||
}
|
||||
switch wf.Status.Phase {
|
||||
case wfv1.NodePending, wfv1.NodeRunning:
|
||||
return v1alpha1.OperationRunning, wf.Status.Message
|
||||
case wfv1.NodeSucceeded:
|
||||
return v1alpha1.OperationSucceeded, wf.Status.Message
|
||||
case wfv1.NodeFailed:
|
||||
return v1alpha1.OperationFailed, wf.Status.Message
|
||||
case wfv1.NodeError:
|
||||
return v1alpha1.OperationError, wf.Status.Message
|
||||
}
|
||||
return v1alpha1.OperationSucceeded, wf.Status.Message
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -11,13 +11,17 @@ func syncPhases(obj *unstructured.Unstructured) []v1alpha1.SyncPhase {
|
||||
if hook.Skip(obj) {
|
||||
return nil
|
||||
} else if hook.IsHook(obj) {
|
||||
var phases []v1alpha1.SyncPhase
|
||||
phasesMap := make(map[v1alpha1.SyncPhase]bool)
|
||||
for _, hookType := range hook.Types(obj) {
|
||||
switch hookType {
|
||||
case v1alpha1.HookTypePreSync, v1alpha1.HookTypeSync, v1alpha1.HookTypePostSync, v1alpha1.HookTypeSyncFail:
|
||||
phases = append(phases, v1alpha1.SyncPhase(hookType))
|
||||
phasesMap[v1alpha1.SyncPhase(hookType)] = true
|
||||
}
|
||||
}
|
||||
var phases []v1alpha1.SyncPhase
|
||||
for phase := range phasesMap {
|
||||
phases = append(phases, phase)
|
||||
}
|
||||
return phases
|
||||
} else {
|
||||
return []v1alpha1.SyncPhase{v1alpha1.SyncPhaseSync}
|
||||
|
||||
@@ -40,11 +40,18 @@ func TestSyncPhaseFail(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSyncPhaseTwoPhases(t *testing.T) {
|
||||
assert.Equal(t, []SyncPhase{SyncPhasePreSync, SyncPhasePostSync}, syncPhases(pod("PreSync,PostSync")))
|
||||
assert.ElementsMatch(t, []SyncPhase{SyncPhasePreSync, SyncPhasePostSync}, syncPhases(pod("PreSync,PostSync")))
|
||||
}
|
||||
|
||||
func TestSyncDuplicatedPhases(t *testing.T) {
|
||||
assert.ElementsMatch(t, []SyncPhase{SyncPhasePreSync}, syncPhases(pod("PreSync,PreSync")))
|
||||
assert.ElementsMatch(t, []SyncPhase{SyncPhasePreSync}, syncPhases(podWithHelmHook("pre-install,pre-upgrade")))
|
||||
}
|
||||
|
||||
func pod(hookType string) *unstructured.Unstructured {
|
||||
pod := test.NewPod()
|
||||
pod.SetAnnotations(map[string]string{"argocd.argoproj.io/hook": hookType})
|
||||
return pod
|
||||
return test.Annotate(test.NewPod(), "argocd.argoproj.io/hook", hookType)
|
||||
}
|
||||
|
||||
func podWithHelmHook(hookType string) *unstructured.Unstructured {
|
||||
return test.Annotate(test.NewPod(), "helm.sh/hook", hookType)
|
||||
}
|
||||
|
||||
@@ -2,14 +2,13 @@ package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/hook"
|
||||
"github.com/argoproj/argo-cd/util/resource/syncwaves"
|
||||
)
|
||||
|
||||
// syncTask holds the live and target object. At least one should be non-nil. A targetObj of nil
|
||||
@@ -53,18 +52,7 @@ func (t *syncTask) obj() *unstructured.Unstructured {
|
||||
}
|
||||
|
||||
func (t *syncTask) wave() int {
|
||||
|
||||
text := t.obj().GetAnnotations()[common.AnnotationSyncWave]
|
||||
if text == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
val, err := strconv.Atoi(text)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return val
|
||||
return syncwaves.Wave(t.obj())
|
||||
}
|
||||
|
||||
func (t *syncTask) isHook() bool {
|
||||
@@ -94,8 +82,12 @@ func (t *syncTask) namespace() string {
|
||||
return t.obj().GetNamespace()
|
||||
}
|
||||
|
||||
func (t *syncTask) pending() bool {
|
||||
return t.operationState == ""
|
||||
}
|
||||
|
||||
func (t *syncTask) running() bool {
|
||||
return t.operationState == v1alpha1.OperationRunning
|
||||
return t.operationState.Running()
|
||||
}
|
||||
|
||||
func (t *syncTask) completed() bool {
|
||||
@@ -106,6 +98,10 @@ func (t *syncTask) successful() bool {
|
||||
return t.operationState.Successful()
|
||||
}
|
||||
|
||||
func (t *syncTask) failed() bool {
|
||||
return t.operationState.Failed()
|
||||
}
|
||||
|
||||
func (t *syncTask) hookType() v1alpha1.HookType {
|
||||
if t.isHook() {
|
||||
return v1alpha1.HookType(t.phase)
|
||||
@@ -113,3 +109,22 @@ func (t *syncTask) hookType() v1alpha1.HookType {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (t *syncTask) hasHookDeletePolicy(policy v1alpha1.HookDeletePolicy) bool {
|
||||
// cannot have a policy if it is not a hook, it is meaningless
|
||||
if !t.isHook() {
|
||||
return false
|
||||
}
|
||||
for _, p := range hook.DeletePolicies(t.obj()) {
|
||||
if p == policy {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *syncTask) needsDeleting() bool {
|
||||
return t.liveObj != nil && (t.pending() && t.hasHookDeletePolicy(v1alpha1.HookDeletePolicyBeforeHookCreation) ||
|
||||
t.successful() && t.hasHookDeletePolicy(v1alpha1.HookDeletePolicyHookSucceeded) ||
|
||||
t.failed() && t.hasHookDeletePolicy(v1alpha1.HookDeletePolicyHookFailed))
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
. "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/test"
|
||||
. "github.com/argoproj/argo-cd/test"
|
||||
)
|
||||
|
||||
func Test_syncTask_hookType(t *testing.T) {
|
||||
@@ -20,10 +20,10 @@ func Test_syncTask_hookType(t *testing.T) {
|
||||
fields fields
|
||||
want HookType
|
||||
}{
|
||||
{"Empty", fields{SyncPhaseSync, test.NewPod()}, ""},
|
||||
{"PreSyncHook", fields{SyncPhasePreSync, test.NewHook(HookTypePreSync)}, HookTypePreSync},
|
||||
{"SyncHook", fields{SyncPhaseSync, test.NewHook(HookTypeSync)}, HookTypeSync},
|
||||
{"PostSyncHook", fields{SyncPhasePostSync, test.NewHook(HookTypePostSync)}, HookTypePostSync},
|
||||
{"Empty", fields{SyncPhaseSync, NewPod()}, ""},
|
||||
{"PreSyncHook", fields{SyncPhasePreSync, NewHook(HookTypePreSync)}, HookTypePreSync},
|
||||
{"SyncHook", fields{SyncPhaseSync, NewHook(HookTypeSync)}, HookTypeSync},
|
||||
{"PostSyncHook", fields{SyncPhasePostSync, NewHook(HookTypePostSync)}, HookTypePostSync},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -36,3 +36,31 @@ func Test_syncTask_hookType(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_syncTask_hasHookDeletePolicy(t *testing.T) {
|
||||
assert.False(t, (&syncTask{targetObj: NewPod()}).hasHookDeletePolicy(HookDeletePolicyBeforeHookCreation))
|
||||
assert.False(t, (&syncTask{targetObj: NewPod()}).hasHookDeletePolicy(HookDeletePolicyHookSucceeded))
|
||||
assert.False(t, (&syncTask{targetObj: NewPod()}).hasHookDeletePolicy(HookDeletePolicyHookFailed))
|
||||
// must be hook
|
||||
assert.False(t, (&syncTask{targetObj: Annotate(NewPod(), "argocd.argoproj.io/hook-delete-policy", "BeforeHookCreation")}).hasHookDeletePolicy(HookDeletePolicyBeforeHookCreation))
|
||||
assert.True(t, (&syncTask{targetObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "BeforeHookCreation")}).hasHookDeletePolicy(HookDeletePolicyBeforeHookCreation))
|
||||
assert.True(t, (&syncTask{targetObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "HookSucceeded")}).hasHookDeletePolicy(HookDeletePolicyHookSucceeded))
|
||||
assert.True(t, (&syncTask{targetObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "HookFailed")}).hasHookDeletePolicy(HookDeletePolicyHookFailed))
|
||||
}
|
||||
|
||||
func Test_syncTask_needsDeleting(t *testing.T) {
|
||||
assert.False(t, (&syncTask{liveObj: NewPod()}).needsDeleting())
|
||||
// must be hook
|
||||
assert.False(t, (&syncTask{liveObj: Annotate(NewPod(), "argocd.argoproj.io/hook-delete-policy", "BeforeHookCreation")}).needsDeleting())
|
||||
// no need to delete if no live obj
|
||||
assert.False(t, (&syncTask{targetObj: Annotate(Annotate(NewPod(), "argoocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "BeforeHookCreation")}).needsDeleting())
|
||||
assert.True(t, (&syncTask{liveObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "BeforeHookCreation")}).needsDeleting())
|
||||
assert.True(t, (&syncTask{liveObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "BeforeHookCreation")}).needsDeleting())
|
||||
assert.True(t, (&syncTask{operationState: OperationSucceeded, liveObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "HookSucceeded")}).needsDeleting())
|
||||
assert.True(t, (&syncTask{operationState: OperationFailed, liveObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "HookFailed")}).needsDeleting())
|
||||
}
|
||||
|
||||
func Test_syncTask_wave(t *testing.T) {
|
||||
assert.Equal(t, 0, (&syncTask{targetObj: NewPod()}).wave())
|
||||
assert.Equal(t, 1, (&syncTask{targetObj: Annotate(NewPod(), "argocd.argoproj.io/sync-wave", "1")}).wave())
|
||||
}
|
||||
|
||||
@@ -165,3 +165,21 @@ func (s syncTasks) wave() int {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s syncTasks) lastPhase() v1alpha1.SyncPhase {
|
||||
if len(s) > 0 {
|
||||
return s[len(s)-1].phase
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s syncTasks) lastWave() int {
|
||||
if len(s) > 0 {
|
||||
return s[len(s)-1].wave()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s syncTasks) multiStep() bool {
|
||||
return s.wave() != s.lastWave() || s.phase() != s.lastPhase()
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ import (
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
. "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
. "github.com/argoproj/argo-cd/test"
|
||||
)
|
||||
|
||||
func Test_syncTasks_kindOrder(t *testing.T) {
|
||||
@@ -366,3 +368,25 @@ func TestSyncNamespaceAgainstCRD(t *testing.T) {
|
||||
|
||||
assert.Equal(t, syncTasks{namespace, crd}, unsorted)
|
||||
}
|
||||
|
||||
func Test_syncTasks_multiStep(t *testing.T) {
|
||||
t.Run("Single", func(t *testing.T) {
|
||||
tasks := syncTasks{{liveObj: Annotate(NewPod(), common.AnnotationSyncWave, "-1"), phase: SyncPhaseSync}}
|
||||
assert.Equal(t, SyncPhaseSync, tasks.phase())
|
||||
assert.Equal(t, -1, tasks.wave())
|
||||
assert.Equal(t, SyncPhaseSync, tasks.lastPhase())
|
||||
assert.Equal(t, -1, tasks.lastWave())
|
||||
assert.False(t, tasks.multiStep())
|
||||
})
|
||||
t.Run("Double", func(t *testing.T) {
|
||||
tasks := syncTasks{
|
||||
{liveObj: Annotate(NewPod(), common.AnnotationSyncWave, "-1"), phase: SyncPhasePreSync},
|
||||
{liveObj: Annotate(NewPod(), common.AnnotationSyncWave, "1"), phase: SyncPhasePostSync},
|
||||
}
|
||||
assert.Equal(t, SyncPhasePreSync, tasks.phase())
|
||||
assert.Equal(t, -1, tasks.wave())
|
||||
assert.Equal(t, SyncPhasePostSync, tasks.lastPhase())
|
||||
assert.Equal(t, 1, tasks.lastWave())
|
||||
assert.True(t, tasks.multiStep())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
fakedisco "k8s.io/client-go/discovery/fake"
|
||||
"k8s.io/client-go/dynamic/fake"
|
||||
"k8s.io/client-go/rest"
|
||||
testcore "k8s.io/client-go/testing"
|
||||
|
||||
@@ -76,6 +77,17 @@ func newTestSyncCtx(resources ...*v1.APIResourceList) *syncContext {
|
||||
return &sc
|
||||
}
|
||||
|
||||
func newManagedResource(live *unstructured.Unstructured) managedResource {
|
||||
return managedResource{
|
||||
Live: live,
|
||||
Group: live.GroupVersionKind().Group,
|
||||
Version: live.GroupVersionKind().Version,
|
||||
Kind: live.GroupVersionKind().Kind,
|
||||
Namespace: live.GetNamespace(),
|
||||
Name: live.GetName(),
|
||||
}
|
||||
}
|
||||
|
||||
func TestSyncNotPermittedNamespace(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
targetPod := test.NewPod()
|
||||
@@ -552,6 +564,22 @@ func TestSyncFailureHookWithFailedSync(t *testing.T) {
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 2)
|
||||
}
|
||||
|
||||
func TestBeforeHookCreation(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.syncOp.SyncStrategy.Apply = nil
|
||||
hook := test.Annotate(test.Annotate(test.NewPod(), common.AnnotationKeyHook, "Sync"), common.AnnotationKeyHookDeletePolicy, "BeforeHookCreation")
|
||||
hook.SetNamespace(test.FakeArgoCDNamespace)
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{newManagedResource(hook)},
|
||||
hooks: []*unstructured.Unstructured{hook},
|
||||
}
|
||||
syncCtx.dynamicIf = fake.NewSimpleDynamicClient(runtime.NewScheme())
|
||||
|
||||
syncCtx.sync()
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 1)
|
||||
assert.Empty(t, syncCtx.syncRes.Resources[0].Message)
|
||||
}
|
||||
|
||||
func TestRunSyncFailHooksFailed(t *testing.T) {
|
||||
// Tests that other SyncFail Hooks run even if one of them fail.
|
||||
|
||||
@@ -660,3 +688,12 @@ func Test_syncContext_liveObj(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_syncContext_hasCRDOfGroupKind(t *testing.T) {
|
||||
// target
|
||||
assert.False(t, (&syncContext{compareResult: &comparisonResult{managedResources: []managedResource{{Target: test.NewCRD()}}}}).hasCRDOfGroupKind("", ""))
|
||||
assert.True(t, (&syncContext{compareResult: &comparisonResult{managedResources: []managedResource{{Target: test.NewCRD()}}}}).hasCRDOfGroupKind("argoproj.io", "TestCrd"))
|
||||
// hook
|
||||
assert.False(t, (&syncContext{compareResult: &comparisonResult{hooks: []*unstructured.Unstructured{test.NewCRD()}}}).hasCRDOfGroupKind("", ""))
|
||||
assert.True(t, (&syncContext{compareResult: &comparisonResult{hooks: []*unstructured.Unstructured{test.NewCRD()}}}).hasCRDOfGroupKind("argoproj.io", "TestCrd"))
|
||||
}
|
||||
|
||||
@@ -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,9 +22,17 @@ Install:
|
||||
* [minikube](https://kubernetes.io/docs/setup/minikube/) or Docker for Desktop
|
||||
|
||||
Brew users can quickly install the lot:
|
||||
|
||||
|
||||
```bash
|
||||
brew install git-lfs go dep kubectl kubectx ksonnet/tap/ks kubernetes-helm kustomize
|
||||
brew install go git-lfs kubectl kubectx dep ksonnet/tap/ks kubernetes-helm kustomize
|
||||
```
|
||||
|
||||
Check the versions:
|
||||
|
||||
```bash
|
||||
go version ;# must be v1.12.x
|
||||
helm version ;# must be v2.13.x
|
||||
kustomize version ;# must be v3.1.x
|
||||
```
|
||||
|
||||
Set up environment variables (e.g. is `~/.bashrc`):
|
||||
@@ -45,33 +54,19 @@ cd ~/go/src/github.com/argoproj/argo-cd
|
||||
Ensure dependencies are up to date first:
|
||||
|
||||
```shell
|
||||
make dep
|
||||
dep ensure
|
||||
make dev-tools-image
|
||||
make install-lint-tools
|
||||
go get github.com/mattn/goreman
|
||||
go get github.com/jstemmer/go-junit-report
|
||||
```
|
||||
|
||||
Build `cli`, `image`, and `argocd-util` as default targets by running make:
|
||||
Common make targets:
|
||||
|
||||
```bash
|
||||
make
|
||||
```
|
||||
|
||||
The make command can take a while, and we recommend building the specific component you are working on
|
||||
|
||||
* `make codegen` - Builds protobuf and swagger files.
|
||||
|
||||
Note: `make codegen` is slow because it uses docker + volume mounts. To improve performance you might install binaries from `./hack/Dockerfile.dev-tools`
|
||||
and use `make codegen-local`. It is still recommended to run `make codegen` once before sending PR to make sure correct version of codegen tools is used.
|
||||
|
||||
* `make cli` - Make the argocd CLI tool
|
||||
* `make server` - Make the API/repo/controller server
|
||||
* `make argocd-util` - Make the administrator's utility, used for certain tasks such as import/export
|
||||
|
||||
## Running Tests
|
||||
|
||||
To run unit tests:
|
||||
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
* `make codegen` - Run code generation
|
||||
* `make lint` - Lint code
|
||||
* `make test` - Run unit tests
|
||||
* `make cli` - Make the `argocd` CLI tool
|
||||
|
||||
Check out the following [documentation](https://github.com/argoproj/argo-cd/blob/master/docs/developer-guide/test-e2e.md) for instructions on running the e2e tests.
|
||||
|
||||
@@ -82,14 +77,20 @@ It is much easier to run and debug if you run ArgoCD on your local machine than
|
||||
You should scale the deployments to zero:
|
||||
|
||||
```bash
|
||||
kubectl -n argocd scale deployment.extensions/argocd-application-controller --replicas 0
|
||||
kubectl -n argocd scale deployment.extensions/argocd-dex-server --replicas 0
|
||||
kubectl -n argocd scale deployment.extensions/argocd-repo-server --replicas 0
|
||||
kubectl -n argocd scale deployment.extensions/argocd-server --replicas 0
|
||||
kubectl -n argocd scale deployment.extensions/argocd-redis --replicas 0
|
||||
kubectl -n argocd scale deployment/argocd-application-controller --replicas 0
|
||||
kubectl -n argocd scale deployment/argocd-dex-server --replicas 0
|
||||
kubectl -n argocd scale deployment/argocd-repo-server --replicas 0
|
||||
kubectl -n argocd scale deployment/argocd-server --replicas 0
|
||||
kubectl -n argocd scale deployment/argocd-redis --replicas 0
|
||||
```
|
||||
|
||||
Note: you'll need to use the https://localhost:6443 cluster now.
|
||||
Download Yarn dependencies and Compile:
|
||||
|
||||
```bash
|
||||
~/go/src/github.com/argoproj/argo-cd/ui
|
||||
yarn install
|
||||
yarn build
|
||||
```
|
||||
|
||||
Then start the services:
|
||||
|
||||
@@ -101,12 +102,17 @@ make start
|
||||
You can now execute `argocd` command against your locally running ArgoCD by appending `--server localhost:8080 --plaintext --insecure`, e.g.:
|
||||
|
||||
```bash
|
||||
argocd app set guestbook --path guestbook --repo https://github.com/argoproj/argocd-example-apps.git --dest-server https://localhost:6443 --dest-namespace default --server localhost:8080 --plaintext --insecure
|
||||
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:8080
|
||||
You can open the UI: [http://localhost:4000](http://localhost:4000)
|
||||
|
||||
Note: you'll need to use the https://kubernetes.default.svc cluster now.
|
||||
As an alternative to using the above command line parameters each time you call `argocd` CLI, you can set the following environment variables:
|
||||
|
||||
```bash
|
||||
export ARGOCD_SERVER=127.0.0.1:8080
|
||||
export ARGOCD_OPTS="--plaintext --insecure"
|
||||
```
|
||||
|
||||
## Running Local Containers
|
||||
|
||||
@@ -124,21 +130,19 @@ Add your username as the environment variable, e.g. to your `~/.bash_profile`:
|
||||
export IMAGE_NAMESPACE=alexcollinsintuit
|
||||
```
|
||||
|
||||
If you have not built the UI image (see [the UI README](https://github.com/argoproj/argo-cd/blob/master/ui/README.md)), then do the following:
|
||||
If you don't want to use `latest` as the image's tag (the default), you can set it from the environment too:
|
||||
|
||||
```bash
|
||||
docker pull argoproj/argocd-ui:latest
|
||||
docker tag argoproj/argocd-ui:latest $IMAGE_NAMESPACE/argocd-ui:latest
|
||||
docker push $IMAGE_NAMESPACE/argocd-ui:latest
|
||||
export IMAGE_TAG=yourtag
|
||||
```
|
||||
|
||||
Build the images:
|
||||
Build the image:
|
||||
|
||||
```bash
|
||||
DOCKER_PUSH=true make image
|
||||
```
|
||||
|
||||
Update the manifests:
|
||||
Update the manifests (be sure to do that from a shell that has above environment variables set)
|
||||
|
||||
```bash
|
||||
make manifests
|
||||
@@ -153,11 +157,11 @@ kubectl -n argocd apply --force -f manifests/install.yaml
|
||||
Scale your deployments up:
|
||||
|
||||
```bash
|
||||
kubectl -n argocd scale deployment.extensions/argocd-application-controller --replicas 1
|
||||
kubectl -n argocd scale deployment.extensions/argocd-dex-server --replicas 1
|
||||
kubectl -n argocd scale deployment.extensions/argocd-repo-server --replicas 1
|
||||
kubectl -n argocd scale deployment.extensions/argocd-server --replicas 1
|
||||
kubectl -n argocd scale deployment.extensions/argocd-redis --replicas 1
|
||||
kubectl -n argocd scale deployment/argocd-application-controller --replicas 1
|
||||
kubectl -n argocd scale deployment/argocd-dex-server --replicas 1
|
||||
kubectl -n argocd scale deployment/argocd-repo-server --replicas 1
|
||||
kubectl -n argocd scale deployment/argocd-server --replicas 1
|
||||
kubectl -n argocd scale deployment/argocd-redis --replicas 1
|
||||
```
|
||||
|
||||
Now you can set-up the port-forwarding and open the UI or CLI.
|
||||
|
||||
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/orphaned-resources.png
Normal file
|
After Width: | Height: | Size: 117 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.
|
||||
|
||||