mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-02-20 09:38:49 +01:00
Compare commits
290 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07ac038a8f | ||
|
|
3828da7f8c | ||
|
|
3bd9121545 | ||
|
|
df6e7c169e | ||
|
|
25b01666f0 | ||
|
|
9d6e6d84de | ||
|
|
7f9ff6e8c3 | ||
|
|
ecc2af9dca | ||
|
|
c5b0279050 | ||
|
|
6df17e7c56 | ||
|
|
e55ecf9107 | ||
|
|
21f208f17e | ||
|
|
b65c1699fa | ||
|
|
4c1428a6be | ||
|
|
bca190bd0c | ||
|
|
88ca5aabf2 | ||
|
|
8297f827a8 | ||
|
|
b85ef39e0d | ||
|
|
fe42780229 | ||
|
|
057d95374d | ||
|
|
1546c1d314 | ||
|
|
31564c6067 | ||
|
|
6e54e59e82 | ||
|
|
196dab98dc | ||
|
|
9d4ed2847e | ||
|
|
ffc6080060 | ||
|
|
ca2e3041f1 | ||
|
|
31676e2aea | ||
|
|
81f9bc20ec | ||
|
|
0b868bd221 | ||
|
|
37c1585c2f | ||
|
|
e095ef6e4c | ||
|
|
8396e06121 | ||
|
|
d4a6498d80 | ||
|
|
4fc814bc16 | ||
|
|
bf67d55f3f | ||
|
|
87402fdc4c | ||
|
|
10b4c47aad | ||
|
|
e6a9400c38 | ||
|
|
ebb21736df | ||
|
|
ea7ad844cb | ||
|
|
76c0b32887 | ||
|
|
827096c195 | ||
|
|
bca450f7aa | ||
|
|
3c7d99f82c | ||
|
|
c5c25cd75b | ||
|
|
f54d0a037f | ||
|
|
6da138956a | ||
|
|
b3a181ea88 | ||
|
|
2f5da24654 | ||
|
|
cc572a5eb2 | ||
|
|
ae03c3b872 | ||
|
|
79ac472382 | ||
|
|
061ba811f1 | ||
|
|
b5909d59ec | ||
|
|
68f17dbe8e | ||
|
|
bc6b4950ba | ||
|
|
c2cf633bbc | ||
|
|
bdd05d5c7a | ||
|
|
4b04a39180 | ||
|
|
aa54aa7eda | ||
|
|
4bbd0e57dd | ||
|
|
2d6b619a86 | ||
|
|
39487ef7c7 | ||
|
|
fcc7c0b080 | ||
|
|
ed734fedbb | ||
|
|
7414f2d42c | ||
|
|
8139df8983 | ||
|
|
f364330de2 | ||
|
|
4ec67d8b08 | ||
|
|
3d3f81df4a | ||
|
|
37f01f6f32 | ||
|
|
36eab6b82c | ||
|
|
793acc147f | ||
|
|
b50609c0e6 | ||
|
|
5f48ce96c6 | ||
|
|
c32460a2bc | ||
|
|
7eb1aba99b | ||
|
|
1a8139f4d6 | ||
|
|
02c03c3b26 | ||
|
|
cdb20d5060 | ||
|
|
7d7eed4932 | ||
|
|
af8c5eb07a | ||
|
|
1a476f7564 | ||
|
|
7f15389c72 | ||
|
|
1a3556e1cc | ||
|
|
4aa614dafb | ||
|
|
6abccea3f0 | ||
|
|
5ffc60f88d | ||
|
|
04e05c5106 | ||
|
|
f387ab846b | ||
|
|
6dc544db99 | ||
|
|
061e575d82 | ||
|
|
d9c1af9f25 | ||
|
|
6abc85a3a5 | ||
|
|
a16471b8a5 | ||
|
|
ecc3ab3cab | ||
|
|
0aeda4366d | ||
|
|
cf601c96d3 | ||
|
|
96f95ca1c1 | ||
|
|
0b2e585373 | ||
|
|
83458e0bd9 | ||
|
|
4aa7cbfb91 | ||
|
|
26db08931e | ||
|
|
3ae9bffa30 | ||
|
|
4c7651b5f8 | ||
|
|
feaf949968 | ||
|
|
b7bdb8ffe7 | ||
|
|
c3b3521f59 | ||
|
|
285454f011 | ||
|
|
134f880aae | ||
|
|
9ac935d59f | ||
|
|
8c2cef6f28 | ||
|
|
26b9ba3dc6 | ||
|
|
de496d6b4b | ||
|
|
ae71475869 | ||
|
|
ef8ae570bf | ||
|
|
6962fc22ee | ||
|
|
25698fd9f5 | ||
|
|
44d596f0ee | ||
|
|
1547b44ffc | ||
|
|
9155d14f9a | ||
|
|
2fc0a25c0a | ||
|
|
988d760474 | ||
|
|
a17c77e238 | ||
|
|
dc24d05a31 | ||
|
|
113b3da691 | ||
|
|
8608a3192a | ||
|
|
2fb3986386 | ||
|
|
8b95052720 | ||
|
|
010411012f | ||
|
|
22f4a936cc | ||
|
|
41fc70d17e | ||
|
|
303cc4613b | ||
|
|
2072d1a4b0 | ||
|
|
d33caacb9b | ||
|
|
3c5033c6bf | ||
|
|
978162fdee | ||
|
|
4f62aa4398 | ||
|
|
6f59fe660f | ||
|
|
0dfc125d95 | ||
|
|
f65289748a | ||
|
|
47187bd30e | ||
|
|
4f155b539b | ||
|
|
cb47740d62 | ||
|
|
0b9b7c31f8 | ||
|
|
35f5b32f53 | ||
|
|
2f83312800 | ||
|
|
074db0cf28 | ||
|
|
502762acc0 | ||
|
|
55ea34292a | ||
|
|
7ff5f0755b | ||
|
|
4a04bb9096 | ||
|
|
fb30f1bd03 | ||
|
|
d6f3f87c69 | ||
|
|
6240ef00e7 | ||
|
|
5c85df90d5 | ||
|
|
07cc533896 | ||
|
|
d511fb8deb | ||
|
|
99583c74c3 | ||
|
|
c92922e3d6 | ||
|
|
5e6aee5961 | ||
|
|
57e0ffceb7 | ||
|
|
c8bcabeba5 | ||
|
|
4b5df692d7 | ||
|
|
5d770d6c54 | ||
|
|
dfd40efb93 | ||
|
|
27af0b4924 | ||
|
|
5d1fb9f775 | ||
|
|
03c5d20d58 | ||
|
|
3fe924ebd6 | ||
|
|
5da1522b17 | ||
|
|
8b57bc990c | ||
|
|
37851b165e | ||
|
|
722fe9273a | ||
|
|
f1d4166a7c | ||
|
|
d00e112791 | ||
|
|
6cd0d97e23 | ||
|
|
2547176b29 | ||
|
|
9530b38f7a | ||
|
|
4a55b13d66 | ||
|
|
c0349f3795 | ||
|
|
48d46e395f | ||
|
|
9cb9a380da | ||
|
|
399d2a830b | ||
|
|
3247090838 | ||
|
|
99d1dcad03 | ||
|
|
8e6fcde4d6 | ||
|
|
160a4fda5e | ||
|
|
cb1f06c72a | ||
|
|
62f29865e7 | ||
|
|
ae9abe5175 | ||
|
|
b558579f28 | ||
|
|
b3ea0a38bb | ||
|
|
32838b1f71 | ||
|
|
e822437eb3 | ||
|
|
de582e0bd5 | ||
|
|
26f83501ad | ||
|
|
000f951b9c | ||
|
|
129e9ea89e | ||
|
|
d1d82c6d6f | ||
|
|
d8c3113c06 | ||
|
|
7bac2c151a | ||
|
|
800b1093e7 | ||
|
|
18f7b416a8 | ||
|
|
63dfc78917 | ||
|
|
487db973d3 | ||
|
|
fcaa8abd01 | ||
|
|
e32c07007c | ||
|
|
e50d03eb61 | ||
|
|
e15c8a58a6 | ||
|
|
3bd8688004 | ||
|
|
ad1ed1d51b | ||
|
|
914fb8337e | ||
|
|
0676b65f7e | ||
|
|
2e494689cd | ||
|
|
561a44d0a8 | ||
|
|
63e70a9f71 | ||
|
|
656f790aed | ||
|
|
7f09331e3f | ||
|
|
f9ba9e9811 | ||
|
|
17c6daa9ea | ||
|
|
f89d40c9e0 | ||
|
|
6a1472371f | ||
|
|
3ea2ba06f5 | ||
|
|
29a6be4afe | ||
|
|
bed36e2027 | ||
|
|
9ff7c0b9c7 | ||
|
|
cf78fe429a | ||
|
|
5b17fc36d4 | ||
|
|
5978dfa167 | ||
|
|
ddb90ab496 | ||
|
|
0a792fefac | ||
|
|
e48350756b | ||
|
|
6d8ff61b16 | ||
|
|
238ff275c4 | ||
|
|
7eccd21b47 | ||
|
|
c9ddaabf08 | ||
|
|
a51169e8c0 | ||
|
|
bea379b036 | ||
|
|
b813301ed6 | ||
|
|
458a10310c | ||
|
|
6221ef20af | ||
|
|
2087eaa4c7 | ||
|
|
7d59cbb5be | ||
|
|
dae43a23f9 | ||
|
|
78e256eef3 | ||
|
|
ccd15d1151 | ||
|
|
05935a9d7e | ||
|
|
891d9b2f38 | ||
|
|
0f845a0416 | ||
|
|
a456a92493 | ||
|
|
e967f8836a | ||
|
|
a8ed010921 | ||
|
|
0c352f8bd6 | ||
|
|
e81f25026d | ||
|
|
cd497c4eec | ||
|
|
7490ee483e | ||
|
|
ced5c1deed | ||
|
|
b977813e9a | ||
|
|
a2f97af2e4 | ||
|
|
6b4a13c4bd | ||
|
|
d64014364c | ||
|
|
0f75eecbd2 | ||
|
|
335becebb1 | ||
|
|
d89c708399 | ||
|
|
d8cfafbd92 | ||
|
|
394a2c8318 | ||
|
|
faee047081 | ||
|
|
a05a2633ba | ||
|
|
0f2f9a97e3 | ||
|
|
d346fceea9 | ||
|
|
b40508323b | ||
|
|
8851842d8e | ||
|
|
70a08a0aa6 | ||
|
|
9cb9ef6752 | ||
|
|
23967f8717 | ||
|
|
475bea599d | ||
|
|
8e8a1ded27 | ||
|
|
0c0bc1f769 | ||
|
|
8bbf8887d2 | ||
|
|
012268d619 | ||
|
|
af3fb9a4d0 | ||
|
|
2370164c98 | ||
|
|
18a1b07d1a | ||
|
|
fafa79fac1 | ||
|
|
8a5b34581a | ||
|
|
27928d0dc6 | ||
|
|
0c91f65e67 | ||
|
|
09c5d5f42a |
10
.github/ISSUE_TEMPLATE/bug_report.md
vendored
10
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -6,7 +6,7 @@ labels: 'bug'
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
If you are trying to resolve an environment-specific issue or have a one-off question about the edge case that does not require a feature then please consider asking a question in argocd slack [channel](https://argoproj.github.io/community/join-slack).
|
||||
<!-- If you are trying to resolve an environment-specific issue or have a one-off question about the edge case that does not require a feature then please consider asking a question in argocd slack [channel](https://argoproj.github.io/community/join-slack). -->
|
||||
|
||||
Checklist:
|
||||
|
||||
@@ -16,19 +16,19 @@ Checklist:
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
A clear and concise description of what the bug is.
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**To Reproduce**
|
||||
|
||||
A list of the steps required to reproduce the issue. Best of all, give us the URL to a repository that exhibits this issue.
|
||||
<!-- 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.
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
**Screenshots**
|
||||
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
**Version**
|
||||
|
||||
|
||||
22
.github/workflows/ci-build.yaml
vendored
22
.github/workflows/ci-build.yaml
vendored
@@ -12,19 +12,9 @@ on:
|
||||
|
||||
env:
|
||||
# Golang version to use across CI steps
|
||||
GOLANG_VERSION: '1.16.5'
|
||||
GOLANG_VERSION: '1.17.6'
|
||||
|
||||
jobs:
|
||||
build-docker:
|
||||
name: Build Docker image
|
||||
runs-on: ubuntu-latest
|
||||
if: github.head_ref != ''
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Build Docker image
|
||||
run: |
|
||||
make image
|
||||
check-go:
|
||||
name: Ensure Go modules synchronicity
|
||||
runs-on: ubuntu-latest
|
||||
@@ -264,6 +254,7 @@ jobs:
|
||||
env:
|
||||
NODE_ENV: production
|
||||
NODE_ONLINE_ENV: online
|
||||
HOST_ARCH: amd64
|
||||
working-directory: ui/
|
||||
- name: Run ESLint
|
||||
run: yarn lint
|
||||
@@ -340,7 +331,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
k3s-version: [v1.21.2, v1.20.2, v1.19.2, v1.18.9, v1.17.11]
|
||||
k3s-version: [v1.23.3, v1.22.6, v1.21.2]
|
||||
needs:
|
||||
- build-go
|
||||
env:
|
||||
@@ -385,10 +376,13 @@ jobs:
|
||||
- name: Add /usr/local/bin to PATH
|
||||
run: |
|
||||
echo "/usr/local/bin" >> $GITHUB_PATH
|
||||
- name: Add ./dist to PATH
|
||||
run: |
|
||||
echo "$(pwd)/dist" >> $GITHUB_PATH
|
||||
- name: Download Go dependencies
|
||||
run: |
|
||||
go mod download
|
||||
go get github.com/mattn/goreman
|
||||
go install github.com/mattn/goreman@latest
|
||||
- name: Install all tools required for building & testing
|
||||
run: |
|
||||
make install-test-tools-local
|
||||
@@ -400,7 +394,7 @@ jobs:
|
||||
run: |
|
||||
docker pull quay.io/dexidp/dex:v2.25.0
|
||||
docker pull argoproj/argo-cd-ci-builder:v1.0.0
|
||||
docker pull redis:6.2.4-alpine
|
||||
docker pull redis:6.2.6-alpine
|
||||
- name: Create target directory for binaries in the build-process
|
||||
run: |
|
||||
mkdir -p dist
|
||||
|
||||
1
.github/workflows/codeql.yml
vendored
1
.github/workflows/codeql.yml
vendored
@@ -8,6 +8,7 @@ on:
|
||||
|
||||
jobs:
|
||||
CodeQL-Build:
|
||||
if: github.repository == 'argoproj/argo-cd'
|
||||
|
||||
# CodeQL runs on ubuntu-latest and windows-latest
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
23
.github/workflows/gh-pages.yaml
vendored
23
.github/workflows/gh-pages.yaml
vendored
@@ -1,23 +0,0 @@
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
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.9.8
|
||||
- name: build
|
||||
run: |
|
||||
pip install -r docs/requirements.txt
|
||||
mkdocs build
|
||||
44
.github/workflows/image.yaml
vendored
44
.github/workflows/image.yaml
vendored
@@ -4,12 +4,17 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
types: [ labeled, unlabeled, opened, synchronize, reopened ]
|
||||
|
||||
env:
|
||||
GOLANG_VERSION: '1.16.5'
|
||||
GOLANG_VERSION: '1.17.6'
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
if: github.repository == 'argoproj/argo-cd'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GOPATH: /home/runner/work/argo-cd/argo-cd
|
||||
@@ -26,28 +31,36 @@ jobs:
|
||||
working-directory: ./src/github.com/argoproj/argo-cd
|
||||
id: image
|
||||
|
||||
# build
|
||||
- run: |
|
||||
docker images -a --format "{{.ID}}" | xargs -I {} docker rmi {}
|
||||
make image DEV_IMAGE=true DOCKER_PUSH=false IMAGE_NAMESPACE=ghcr.io/argoproj IMAGE_TAG=${{ steps.image.outputs.tag }}
|
||||
working-directory: ./src/github.com/argoproj/argo-cd
|
||||
|
||||
# publish
|
||||
# login
|
||||
- run: |
|
||||
docker login ghcr.io --username $USERNAME --password $PASSWORD
|
||||
docker push ghcr.io/argoproj/argocd:${{ steps.image.outputs.tag }}
|
||||
|
||||
docker login --username "${DOCKER_USERNAME}" --password "${DOCKER_TOKEN}"
|
||||
docker tag ghcr.io/argoproj/argocd:${{ steps.image.outputs.tag }} argoproj/argocd:latest
|
||||
docker push argoproj/argocd:latest
|
||||
docker login quay.io --username "${DOCKER_USERNAME}" --password "${DOCKER_TOKEN}"
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
USERNAME: ${{ secrets.USERNAME }}
|
||||
PASSWORD: ${{ secrets.TOKEN }}
|
||||
DOCKER_USERNAME: ${{ secrets.RELEASE_DOCKERHUB_USERNAME }}
|
||||
DOCKER_TOKEN: ${{ secrets.RELEASE_DOCKERHUB_TOKEN }}
|
||||
DOCKER_USERNAME: ${{ secrets.RELEASE_QUAY_USERNAME }}
|
||||
DOCKER_TOKEN: ${{ secrets.RELEASE_QUAY_TOKEN }}
|
||||
|
||||
# build
|
||||
- uses: docker/setup-qemu-action@v1
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- run: |
|
||||
IMAGE_PLATFORMS=linux/amd64
|
||||
if [[ "${{ github.event_name }}" == "push" || "${{ contains(github.event.pull_request.labels.*.name, 'test-arm-image') }}" == "true" ]]
|
||||
then
|
||||
IMAGE_PLATFORMS=linux/amd64,linux/arm64
|
||||
fi
|
||||
echo "Building image for platforms: $IMAGE_PLATFORMS"
|
||||
docker buildx build --platform $IMAGE_PLATFORMS --push="${{ github.event_name == 'push' }}" \
|
||||
-t ghcr.io/argoproj/argocd:${{ steps.image.outputs.tag }} \
|
||||
-t quay.io/argoproj/argocd:latest .
|
||||
working-directory: ./src/github.com/argoproj/argo-cd
|
||||
|
||||
|
||||
# deploy
|
||||
- run: git clone "https://$TOKEN@github.com/argoproj/argoproj-deployments"
|
||||
if: github.event_name == 'push'
|
||||
env:
|
||||
TOKEN: ${{ secrets.TOKEN }}
|
||||
- run: |
|
||||
@@ -55,5 +68,6 @@ jobs:
|
||||
git config --global user.email 'ci@argoproj.com'
|
||||
git config --global user.name 'CI'
|
||||
git diff --exit-code && echo 'Already deployed' || (git commit -am 'Upgrade argocd to ${{ steps.image.outputs.tag }}' && git push)
|
||||
if: github.event_name == 'push'
|
||||
working-directory: argoproj-deployments/argocd
|
||||
# TODO: clean up old images once github supports it: https://github.community/t5/How-to-use-Git-and-GitHub/Deleting-images-from-GitHub-Package-Registry/m-p/41202/thread-id/9811
|
||||
|
||||
102
.github/workflows/release.yaml
vendored
102
.github/workflows/release.yaml
vendored
@@ -12,11 +12,12 @@ on:
|
||||
- '!release-v0*'
|
||||
|
||||
env:
|
||||
GOLANG_VERSION: '1.16.5'
|
||||
GOLANG_VERSION: '1.17.6'
|
||||
|
||||
jobs:
|
||||
prepare-release:
|
||||
name: Perform automatic release on trigger ${{ github.ref }}
|
||||
if: github.repository == 'argoproj/argo-cd'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
# The name of the tag as supplied by the GitHub event
|
||||
@@ -94,7 +95,7 @@ jobs:
|
||||
echo "=========== BEGIN COMMIT MESSAGE ============="
|
||||
git show ${SOURCE_TAG}
|
||||
echo "============ END COMMIT MESSAGE =============="
|
||||
|
||||
|
||||
# Quite dirty hack to get the release notes from the annotated tag
|
||||
# into a temporary file.
|
||||
RELEASE_NOTES=$(mktemp -p /tmp release-notes.XXXXXX)
|
||||
@@ -141,7 +142,7 @@ jobs:
|
||||
echo "RELEASE_NOTES=${RELEASE_NOTES}" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@v1
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ env.GOLANG_VERSION }}
|
||||
|
||||
@@ -182,18 +183,7 @@ jobs:
|
||||
echo "Creating release ${RELEASE_TAG}"
|
||||
git tag ${RELEASE_TAG}
|
||||
|
||||
- name: Build Docker image for release
|
||||
run: |
|
||||
set -ue
|
||||
git clean -fd
|
||||
mkdir -p dist/
|
||||
make image IMAGE_TAG="${TARGET_VERSION}" DOCKER_PUSH=false
|
||||
make release-cli
|
||||
chmod +x ./dist/argocd-linux-amd64
|
||||
./dist/argocd-linux-amd64 version --client
|
||||
if: ${{ env.DRY_RUN != 'true' }}
|
||||
|
||||
- name: Push docker image to repository
|
||||
- name: Login to docker repositories
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.RELEASE_DOCKERHUB_USERNAME }}
|
||||
DOCKER_TOKEN: ${{ secrets.RELEASE_DOCKERHUB_TOKEN }}
|
||||
@@ -202,11 +192,22 @@ jobs:
|
||||
run: |
|
||||
set -ue
|
||||
docker login quay.io --username "${QUAY_USERNAME}" --password "${QUAY_TOKEN}"
|
||||
docker push ${IMAGE_NAMESPACE}/argocd:v${TARGET_VERSION}
|
||||
# Remove the following when Docker Hub is gone
|
||||
docker login --username "${DOCKER_USERNAME}" --password "${DOCKER_TOKEN}"
|
||||
docker tag ${IMAGE_NAMESPACE}/argocd:v${TARGET_VERSION} argoproj/argocd:v${TARGET_VERSION}
|
||||
docker push argoproj/argocd:v${TARGET_VERSION}
|
||||
if: ${{ env.DRY_RUN != 'true' }}
|
||||
|
||||
|
||||
- uses: docker/setup-qemu-action@v1
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- name: Build and push Docker image for release
|
||||
run: |
|
||||
set -ue
|
||||
git clean -fd
|
||||
mkdir -p dist/
|
||||
docker buildx build --platform linux/amd64,linux/arm64 --push -t ${IMAGE_NAMESPACE}/argocd:v${TARGET_VERSION} -t argoproj/argocd:v${TARGET_VERSION} .
|
||||
make release-cli
|
||||
chmod +x ./dist/argocd-linux-amd64
|
||||
./dist/argocd-linux-amd64 version --client
|
||||
if: ${{ env.DRY_RUN != 'true' }}
|
||||
|
||||
- name: Read release notes file
|
||||
@@ -244,6 +245,17 @@ jobs:
|
||||
asset_content_type: application/octet-stream
|
||||
if: ${{ env.DRY_RUN != 'true' }}
|
||||
|
||||
- name: Upload argocd-linux-arm64 binary to release assets
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./dist/argocd-linux-arm64
|
||||
asset_name: argocd-linux-arm64
|
||||
asset_content_type: application/octet-stream
|
||||
if: ${{ env.DRY_RUN != 'true' }}
|
||||
|
||||
- name: Upload argocd-darwin-amd64 binary to release assets
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
@@ -255,6 +267,17 @@ jobs:
|
||||
asset_content_type: application/octet-stream
|
||||
if: ${{ env.DRY_RUN != 'true' }}
|
||||
|
||||
- name: Upload argocd-darwin-arm64 binary to release assets
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./dist/argocd-darwin-arm64
|
||||
asset_name: argocd-darwin-arm64
|
||||
asset_content_type: application/octet-stream
|
||||
if: ${{ env.DRY_RUN != 'true' }}
|
||||
|
||||
- name: Upload argocd-windows-amd64 binary to release assets
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
@@ -266,6 +289,48 @@ jobs:
|
||||
asset_content_type: application/octet-stream
|
||||
if: ${{ env.DRY_RUN != 'true' }}
|
||||
|
||||
- name: Generate SBOM (spdx)
|
||||
id: spdx-builder
|
||||
env:
|
||||
# defines the spdx/spdx-sbom-generator version to use.
|
||||
SPDX_GEN_VERSION: v0.0.13
|
||||
# defines the sigs.k8s.io/bom version to use.
|
||||
SIGS_BOM_VERSION: v0.2.1
|
||||
# comma delimited list of project relative folders to inspect for package
|
||||
# managers (gomod, yarn, npm).
|
||||
PROJECT_FOLDERS: ".,./ui"
|
||||
# full qualified name of the docker image to be inspected
|
||||
DOCKER_IMAGE: ${{env.IMAGE_NAMESPACE}}/argocd:v${{env.TARGET_VERSION}}
|
||||
run: |
|
||||
yarn install --cwd ./ui
|
||||
go install github.com/spdx/spdx-sbom-generator/cmd/generator@$SPDX_GEN_VERSION
|
||||
go install sigs.k8s.io/bom/cmd/bom@$SIGS_BOM_VERSION
|
||||
|
||||
# Generate SPDX for project dependencies analyzing package managers
|
||||
for folder in $(echo $PROJECT_FOLDERS | sed "s/,/ /g")
|
||||
do
|
||||
generator -p $folder -o /tmp
|
||||
done
|
||||
|
||||
# Generate SPDX for binaries analyzing the docker image
|
||||
if [[ ! -z $DOCKER_IMAGE ]]; then
|
||||
bom generate -o /tmp/bom-docker-image.spdx -i $DOCKER_IMAGE
|
||||
fi
|
||||
|
||||
cd /tmp && tar -zcf sbom.tar.gz *.spdx
|
||||
if: ${{ env.DRY_RUN != 'true' }}
|
||||
|
||||
- name: Upload SBOM to release assets
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: /tmp/sbom.tar.gz
|
||||
asset_name: sbom.tar.gz
|
||||
asset_content_type: application/octet-stream
|
||||
if: ${{ env.DRY_RUN != 'true' }}
|
||||
|
||||
- name: Update homebrew formula
|
||||
env:
|
||||
HOMEBREW_TOKEN: ${{ secrets.RELEASE_HOMEBREW_TOKEN }}
|
||||
@@ -280,3 +345,4 @@ jobs:
|
||||
set -ue
|
||||
git push --delete origin ${SOURCE_TAG}
|
||||
if: ${{ always() }}
|
||||
|
||||
|
||||
2
.gitpod.Dockerfile
vendored
2
.gitpod.Dockerfile
vendored
@@ -9,7 +9,7 @@ RUN curl -L https://go.kubebuilder.io/dl/2.3.1/$(go env GOOS)/$(go env GOARCH) |
|
||||
tar -xz -C /tmp/ && mv /tmp/kubebuilder_2.3.1_$(go env GOOS)_$(go env GOARCH) /usr/local/kubebuilder
|
||||
|
||||
RUN apt-get install redis-server -y
|
||||
RUN go get github.com/mattn/goreman
|
||||
RUN go install github.com/mattn/goreman@latest
|
||||
|
||||
USER gitpod
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@ image:
|
||||
file: .gitpod.Dockerfile
|
||||
|
||||
tasks:
|
||||
- init: make mod-download-local dep-ui-local && GO111MODULE=off go get github.com/mattn/goreman
|
||||
- init: make mod-download-local dep-ui-local && GO111MODULE=off go install github.com/mattn/goreman@latest
|
||||
command: make start-test-k8s
|
||||
221
CHANGELOG.md
221
CHANGELOG.md
@@ -1,5 +1,224 @@
|
||||
# Changelog
|
||||
|
||||
## v2.3.0 (Unreleased)
|
||||
|
||||
### Argo CD ApplicationSet and Notifications are now part of Argo CD
|
||||
|
||||
Two popular [Argoproj Labs](https://github.com/argoproj-labs) projects [Argo CD ApplicationSet](https://github.com/argoproj/applicationset) and
|
||||
[Argo CD Notifications](https://github.com/argoproj-labs/argocd-notifications) are now part of Argo CD! The default Argo CD installation manifests now
|
||||
bundle both projects out of the box. Going forward you can expect more tightened integration of these projects into Argo CD.
|
||||
|
||||
### New sync and diff strategies
|
||||
|
||||
Users can now configure the Application resource to instruct Argo CD to consider the ignore difference setup during the sync process.
|
||||
In order to do so, add the new sync option RespectIgnoreDifferences=true in the Application resource. Once the sync option is added,
|
||||
Argo CD won't change ignored fields during the syncing process.
|
||||
|
||||
Configuring ignored fields is also easier now. Instead of listing fields one by one users can now leverage the
|
||||
managedFields metadata to instruct Argo CD about trusted managers and automatically ignore any fields owned by them. A new diff customization
|
||||
(managedFieldsManagers) is now available allowing users to specify managers the application should trust and to ignore all fields owned by those managers.
|
||||
Read more about these changes at [New sync and diff strategies in ArgoCD](https://blog.argoproj.io/new-sync-and-diff-strategies-in-argocd-44195d3f8b8c) blog post.
|
||||
|
||||
### ARM Images
|
||||
|
||||
An officially supported ARM 64 image is now available. Enjoy running Argo CD on your Raspberry Pi! Additionally, the image size was reduced by nearly ~50%
|
||||
and is only 200MB now. The ARM version of `argocd` CLI is also available and published as a Github release artifact.
|
||||
|
||||
### Compact Tree View And Click Application Navigation
|
||||
|
||||
The application details page now supports compact application resources tree visualization. Using the "Group Nodes" button, you can collapse the similar resources
|
||||
into a single group node to remove the clutter and make it easier to understand the state of application resources. You still can get detailed information about the collapsed resources by clicking on the group node. The list of collapsed resources will be available in a sliding panel. Compact resource tree is still too big?
|
||||
You can use the zoom in and zoom out feature to make it smaller - or even larger!
|
||||
|
||||
You no longer need to move back and forth between the application details page and the application list page. Instead you can navigate directly to the required application by clicking the search icon in the application details page title.
|
||||
|
||||
### Upgraded Config Management Tools
|
||||
|
||||
Both bundled Helm and Kustomize binaries have been upgraded to the latest versions. Kustomize has been upgraded from 4.2.0 to 4.4.1 and Helm has been upgraded from 3.7.1 to 3.8.0.
|
||||
|
||||
### Bug Fixes and Performance Enhancements
|
||||
|
||||
* Config management tools enhancements:
|
||||
* The skipCrds flag and ability to ignore missing values files for Helm (#8012, #8003)
|
||||
* Additional environment variables for Kustomize (#8096)
|
||||
* Argo CD CLI follows the XDG Base directory standard (#7638)
|
||||
* Redis is no longer used during SSO login (#8241)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
- feat: Add app list and details page views to navigation history (#7776) (#7937)
|
||||
- feat: Add skipCrds flag for helm charts (#8012)
|
||||
- feat: Add visual indicator for newly created pods (#8006)
|
||||
- feat: Added a new Helm option ignoreMissingValueFiles (#7767) (#8003)
|
||||
- feat: Allow configuring system wide ignore differences for all resources (#8224)
|
||||
- feat: Allow escaping dollar in Envsubst (#7961)
|
||||
- feat: Allow external links on Application (#3487) (#8231)
|
||||
- feat: Allow selecting application on detail page (#8176)
|
||||
- feat: Bundle applicationset-controller with argocd (#8148)
|
||||
- feat: Enable specifying root ca for oidc (#6712)
|
||||
- feat: Expose ARGOCD_APP_NAME to the `kustomize build` command (#8096)
|
||||
- feat: Ignore differences owned by trusted managers from managedFields (#7869)
|
||||
- feat: New sync option to use ignore diff configs during sync (#8078)
|
||||
- feat: Provide address flag for admin dashboard command (#8095)
|
||||
- feat: Store "Group Nodes" button state in application details preferences (#8036)
|
||||
- feat: Support specifying cluster by name in addition to API server URL in Cluster API (#8077)
|
||||
- feat: Support XDG Base directory standard (#7638) (#7791)
|
||||
- feat: Use encrypted cookie to store OAuth2 state nonce (instead of redis) (#8241)
|
||||
- feat: Build images on PR and conditionally build arm64 image on push (#8108)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix: Add "Restarting MinIO" status to MiniO Tenant health check (#8191)
|
||||
- fix: Add all resources in list view (#7295)
|
||||
- fix: Adding pagination to grouped nodes sliding panel#7837 (#7915)
|
||||
- fix: Allow all resources to add external links (#7923)
|
||||
- fix: Always call ValidateDestination (#7976)
|
||||
- fix: Application exist panic when execute api call (#8188)
|
||||
- fix: Application-icons-alignment (#8054)
|
||||
- fix: Controller panics if resource manifest has incorrect annotation (#8022)
|
||||
- fix: Correctly handle project field during partial cluster update (#7994)
|
||||
- fix: Default value for retry validation #8055 (#8064)
|
||||
- fix: Fix a possible crash when parsing RBAC (#8165)
|
||||
- fix: Grouped node list missing resources on Compact resources view #8014 (#8018)
|
||||
- fix: Issue with headless installation (#7958)
|
||||
- fix: Issue with project scoped resources (#8048)
|
||||
- fix: Kubernetes labels normalization for Prometheus (#7925)
|
||||
- fix: Nested Refresh dropdown does not work on Application Details page #1524 (#7950)
|
||||
- fix: Network line colors and menu icon alignment (#8059)
|
||||
- fix: Opening app details shows UI error on some apps (#8016) (#8019)
|
||||
- fix: Parse to correct uint32 type (#8177)
|
||||
- fix: Prevent possible nil-pointer deref in normalizer (#8185)
|
||||
- fix: Prevent possible out-of-bounds access when loading policies (#8186)
|
||||
- fix: Provide a semantic version parsed version for KUBE_VERSION (#8250)
|
||||
- fix: Refreshing label toast (#7979)
|
||||
- fix: Resource details page crashes when resource is not deployed and hide managed fields is selected (#7971)
|
||||
- fix: Retry disabled text (#8004)
|
||||
- fix: Route health check stuck in 'Progressing' (#8170)
|
||||
- fix: Sync window panel is crashed if resource name not contain letters (#8053)
|
||||
- fix: Targetervision compatible without prefix refs/heads or refs/tags (#7939)
|
||||
- fix: Trailing line in Filter Dropdown Menus #7821 (#8001)
|
||||
- fix: Webhook URL matching edge cases (#7981)
|
||||
- fix(ui): Use consistent case for diff modes (#7945)
|
||||
- fix: Use gRPC timeout for sidecar CMPs (#8131) (#8236)
|
||||
|
||||
### Other
|
||||
|
||||
- chore: Bump go-jsonnet to v0.18.0 (#8011)
|
||||
- chore: Escape proj in regex (#7985)
|
||||
- chore: Exclude argocd-server rbac for core-install (#8234)
|
||||
- chore: Log out the resource triggering reconciliation (#8192)
|
||||
- chore: Migrate to use golang-jwt/jwt v4.2.0 (#8136)
|
||||
- chore: Move resolveRevision from api-server to repo-server (#7966)
|
||||
- chore: Update notifications version (#8267)
|
||||
- chore: Update slack version (#8299)
|
||||
- chore: Update to Redis 6.2.4 (#8157)
|
||||
- chore: Upgrade awscli to 2.4.6 and remove python deps (#7947)
|
||||
- chore: Upgrade base image to ubuntu:21.10 (#8230)
|
||||
- chore: Upgrade dex to v2.30.2 (https://github.com/dexidp/dex/issues/2326) (#8237)
|
||||
- chore: Upgrade gitops engine (#8288)
|
||||
- chore: Upgrade golang to 1.17.6 (#8229)
|
||||
- chore: Upgrade helm to most recent version (v3.7.2) (#8226)
|
||||
- chore: Upgrade k8s client to v1.23 (#8213)
|
||||
- chore: Upgrade kustomize to most recent version (v4.4.1) (#8227)
|
||||
- refactor: Introduce 'byClusterName' secret index to speedup cluster server URL lookup (#8133)
|
||||
- refactor: Move project filtering to server side (#8102)
|
||||
|
||||
## v2.2.3 (2022-01-18)
|
||||
|
||||
- fix: Application exist panic when execute api call (#8188)
|
||||
- fix: Route health check stuck in 'Progressing' (#8170)
|
||||
- refactor: Introduce 'byClusterName' secret index to speedup cluster server URL lookup (#8133)
|
||||
- chore: Update to Redis 6.2.4 (#8157) (#8158)
|
||||
|
||||
## v2.2.2 (2021-12-31)
|
||||
|
||||
- fix: Issue with project scoped resources (#8048)
|
||||
- fix: Escape proj in regex (#7985)
|
||||
- fix: Default value for retry validation #8055 (#8064)
|
||||
- fix: Sync window panel is crashed if resource name not contain letters (#8053)
|
||||
- fix: Upgrade github.com/argoproj/gitops-engine to v0.5.2
|
||||
- fix: Retry disabled text (#8004)
|
||||
- fix: Opening app details shows UI error on some apps (#8016) (#8019)
|
||||
- fix: Correctly handle project field during partial cluster update (#7994)
|
||||
- fix: Cluster API does not support updating labels and annotations (#7901)
|
||||
|
||||
## v2.2.1 (2021-12-16)
|
||||
|
||||
- fix: Resource details page crashes when resource is not deployed and hide managed fields is selected (#7971)
|
||||
- fix: Issue with headless installation (#7958)
|
||||
- fix: Nil pointer (#7905)
|
||||
|
||||
## v2.2.0 (2021-12-14)
|
||||
|
||||
> [Upgrade instructions](./docs/operator-manual/upgrading/2.1-2.2.md)
|
||||
|
||||
### Project Scoped repositories and clusters
|
||||
|
||||
The project scoped repositories and clusters is a feature that simplifies registering the repositories and cluster credentials.
|
||||
Instead of requiring operators to set up in advance all clusters and git repositories that can be used, developers can now do
|
||||
this on their own in a self-service manner.
|
||||
|
||||
### Config Management Plugins V2
|
||||
|
||||
The Config Management Plugins V2 is set of enhancement of the existing config management plugins feature.
|
||||
The list includes improved installation experience, ability to package plugin into a separate image and
|
||||
improved plugin manifests discovery.
|
||||
|
||||
### Resource tracking
|
||||
|
||||
Argo CD has traditionally tracked the resources it manages by the well-known "app.kubernetes.io/instance" property.
|
||||
While using this property works ok in simple scenarios, it also has several limitations. ArgoCD now allows you to use
|
||||
a new annotation (argocd.argoproj.io/tracking-id) for tracking your resources. Using this annotation is a much more flexible approach
|
||||
as there are no conflicts with other Kubernetes tools, and you can easily install multiple Argo CD instances on the same clusters.
|
||||
|
||||
### Bug Fixes and Performance Enhancements
|
||||
|
||||
* Argo CD API server caches RBAC checks that significantly improves the GET /api/v1/applications API performance (#7587)
|
||||
* Argo CD RBAC supports regex matches (#7165)
|
||||
* Health check support for KubeVirt (#7176), Cassandra (#7017), Openshift Route (#7112), DeploymentConfig (#7114), Confluent (#6957) and SparkApplication (#7434) CRDs.
|
||||
* Persistent banner (#7312) with custom positioning (#7462)
|
||||
* Cluster name support in project destinations (#7198)
|
||||
* around 30 more features and a total of 84 bug fixes
|
||||
|
||||
## v2.1.7 (2021-12-14)
|
||||
|
||||
- fix: issue with keepalive (#7861)
|
||||
- fix nil pointer dereference error (#7905)
|
||||
- fix: env vars to tune cluster cache were broken (#7779)
|
||||
- fix: upgraded gitops engine to v0.4.2 (fixes #7561)
|
||||
|
||||
|
||||
## v2.1.6 (2021-11-16)
|
||||
|
||||
- fix: don't use revision caching during app creation (#7508)
|
||||
- fix: supporting OCI dependencies. Fixes #6062 (#6994)
|
||||
|
||||
## v2.1.5 (2021-11-16)
|
||||
|
||||
- fix: Invalid memory address or nil pointer dereference in processRequestedAppOperation (#7501)
|
||||
|
||||
## v2.1.4 (2021-11-15)
|
||||
|
||||
- fix: Operation has completed with phase: Running (#7482)
|
||||
- fix: Application status panel shows Syncing instead of Deleting (#7486)
|
||||
- fix(ui): Add Error Boundary around Extensions and comply with new Extensions API (#7215)
|
||||
|
||||
|
||||
## v2.1.3 (2021-10-29)
|
||||
|
||||
- fix: core-install.yaml always refers to latest argocd image (#7321)
|
||||
- fix: handle applicationset backup forbidden error (#7306)
|
||||
- fix: Argo CD should not use cached git/helm revision during app creation/update validation (#7244)
|
||||
|
||||
## v2.1.2 (2021-10-02)
|
||||
|
||||
- fix: cluster filter popping out of box (#7135)
|
||||
- fix: gracefully shutdown metrics server when dex config changes (#7138)
|
||||
- fix: upgrade gitops engine version to v0.4.1 (#7088)
|
||||
- fix: repository name already exists when multiple helm dependencies (#7096)
|
||||
|
||||
|
||||
## v2.1.1 (2021-08-25)
|
||||
|
||||
### Bug Fixes
|
||||
@@ -753,7 +972,7 @@ More documentation and tools are coming in patch releases.
|
||||
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.
|
||||
[Sync Waves](https://argo-cd.readthedocs.io/en/stable/user-guide/sync-waves/) instead.
|
||||
|
||||
#### Enhancements
|
||||
* feat: Add custom health checks for cert-manager v0.11.0 (#2689)
|
||||
|
||||
24
Dockerfile
24
Dockerfile
@@ -1,10 +1,10 @@
|
||||
ARG BASE_IMAGE=docker.io/library/ubuntu:21.04
|
||||
ARG BASE_IMAGE=docker.io/library/ubuntu:21.10
|
||||
####################################################################################################
|
||||
# Builder image
|
||||
# Initial stage which pulls prepares build dependencies and CLI tooling we need for our final image
|
||||
# Also used as the image in CI jobs so needs all dependencies
|
||||
####################################################################################################
|
||||
FROM docker.io/library/golang:1.16.5 as builder
|
||||
FROM docker.io/library/golang:1.17.6 as builder
|
||||
|
||||
RUN echo 'deb http://deb.debian.org/debian buster-backports main' >> /etc/apt/sources.list
|
||||
|
||||
@@ -32,6 +32,7 @@ RUN ./install.sh ksonnet-linux
|
||||
RUN ./install.sh helm2-linux
|
||||
RUN ./install.sh helm-linux
|
||||
RUN ./install.sh kustomize-linux
|
||||
RUN ./install.sh awscli-linux
|
||||
|
||||
####################################################################################################
|
||||
# Argo CD Base - used as the base for both the release and dev argocd images
|
||||
@@ -49,21 +50,21 @@ RUN groupadd -g 999 argocd && \
|
||||
chmod g=u /home/argocd && \
|
||||
apt-get update && \
|
||||
apt-get dist-upgrade -y && \
|
||||
apt-get install -y git git-lfs python3-pip tini gpg tzdata && \
|
||||
apt-get install -y git git-lfs tini gpg tzdata && \
|
||||
apt-get clean && \
|
||||
pip3 install awscli==1.18.80 && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
COPY hack/git-ask-pass.sh /usr/local/bin/git-ask-pass.sh
|
||||
COPY hack/gpg-wrapper.sh /usr/local/bin/gpg-wrapper.sh
|
||||
COPY hack/git-verify-wrapper.sh /usr/local/bin/git-verify-wrapper.sh
|
||||
COPY --from=builder /usr/local/bin/ks /usr/local/bin/ks
|
||||
COPY --from=builder /usr/local/bin/helm2 /usr/local/bin/helm2
|
||||
COPY --from=builder /usr/local/bin/helm /usr/local/bin/helm
|
||||
COPY --from=builder /usr/local/bin/kustomize /usr/local/bin/kustomize
|
||||
COPY --from=builder /usr/local/aws-cli/v2/current/dist /usr/local/aws-cli/v2/current/dist
|
||||
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
# keep uid_entrypoint.sh for backward compatibility
|
||||
RUN ln -s /usr/local/bin/entrypoint.sh /usr/local/bin/uid_entrypoint.sh
|
||||
RUN ln -s /usr/local/aws-cli/v2/current/dist/aws /usr/local/bin/aws
|
||||
|
||||
# support for mounting configuration from a configmap
|
||||
RUN mkdir -p /app/config/ssh && \
|
||||
@@ -90,18 +91,18 @@ FROM docker.io/library/node:12.18.4 as argocd-ui
|
||||
WORKDIR /src
|
||||
ADD ["ui/package.json", "ui/yarn.lock", "./"]
|
||||
|
||||
RUN yarn install
|
||||
RUN yarn install --network-timeout 200000
|
||||
|
||||
ADD ["ui/", "."]
|
||||
|
||||
ARG ARGO_VERSION=latest
|
||||
ENV ARGO_VERSION=$ARGO_VERSION
|
||||
RUN NODE_ENV='production' NODE_ONLINE_ENV='online' yarn build
|
||||
RUN HOST_ARCH='amd64' NODE_ENV='production' NODE_ONLINE_ENV='online' NODE_OPTIONS=--max_old_space_size=8192 yarn build
|
||||
|
||||
####################################################################################################
|
||||
# Argo CD Build stage which performs the actual build of Argo CD binaries
|
||||
####################################################################################################
|
||||
FROM golang:1.16.5 as argocd-build
|
||||
FROM docker.io/library/golang:1.17.6 as argocd-build
|
||||
|
||||
WORKDIR /go/src/github.com/argoproj/argo-cd
|
||||
|
||||
@@ -115,12 +116,6 @@ COPY . .
|
||||
COPY --from=argocd-ui /src/dist/app /go/src/github.com/argoproj/argo-cd/ui/dist/app
|
||||
RUN make argocd-all
|
||||
|
||||
ARG BUILD_ALL_CLIS=true
|
||||
RUN if [ "$BUILD_ALL_CLIS" = "true" ] ; then \
|
||||
make BIN_NAME=argocd-darwin-amd64 GOOS=darwin argocd-all && \
|
||||
make BIN_NAME=argocd-windows-amd64.exe GOOS=windows argocd-all \
|
||||
; fi
|
||||
|
||||
####################################################################################################
|
||||
# Final image
|
||||
####################################################################################################
|
||||
@@ -133,5 +128,6 @@ RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-repo-server
|
||||
RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-cmp-server
|
||||
RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-application-controller
|
||||
RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-dex
|
||||
RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-notifications
|
||||
|
||||
USER 999
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
####################################################################################################
|
||||
FROM argocd-base
|
||||
COPY argocd /usr/local/bin/
|
||||
COPY argocd-darwin-amd64 /usr/local/bin/
|
||||
COPY argocd-windows-amd64.exe /usr/local/bin/
|
||||
|
||||
USER root
|
||||
RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-server
|
||||
RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-repo-server
|
||||
RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-application-controller
|
||||
RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-dex
|
||||
RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-notifications
|
||||
USER 999
|
||||
|
||||
64
Makefile
64
Makefile
@@ -4,6 +4,8 @@ DIST_DIR=${CURRENT_DIR}/dist
|
||||
CLI_NAME=argocd
|
||||
BIN_NAME=argocd
|
||||
|
||||
GEN_RESOURCES_CLI_NAME=argocd-resources-gen
|
||||
|
||||
HOST_OS:=$(shell go env GOOS)
|
||||
HOST_ARCH:=$(shell go env GOARCH)
|
||||
|
||||
@@ -13,7 +15,7 @@ GIT_COMMIT=$(shell git rev-parse HEAD)
|
||||
GIT_TAG=$(shell if [ -z "`git status --porcelain`" ]; then git describe --exact-match --tags HEAD 2>/dev/null; fi)
|
||||
GIT_TREE_STATE=$(shell if [ -z "`git status --porcelain`" ]; then echo "clean" ; else echo "dirty"; fi)
|
||||
VOLUME_MOUNT=$(shell if test "$(go env GOOS)" = "darwin"; then echo ":delegated"; elif test selinuxenabled; then echo ":delegated"; else echo ""; fi)
|
||||
KUBECTL_VERSION=$(shell go list -m all | grep k8s.io/client-go | cut -d ' ' -f5)
|
||||
KUBECTL_VERSION=$(shell go list -m k8s.io/client-go | head -n 1 | rev | cut -d' ' -f1 | rev)
|
||||
|
||||
GOPATH?=$(shell if test -x `which go`; then go env GOPATH; else echo "$(HOME)/go"; fi)
|
||||
GOCACHE?=$(HOME)/.cache/go-build
|
||||
@@ -45,7 +47,7 @@ ARGOCD_E2E_DEX_PORT?=5556
|
||||
ARGOCD_E2E_YARN_HOST?=localhost
|
||||
ARGOCD_E2E_DISABLE_AUTH?=
|
||||
|
||||
ARGOCD_E2E_TEST_TIMEOUT?=20m
|
||||
ARGOCD_E2E_TEST_TIMEOUT?=30m
|
||||
|
||||
ARGOCD_IN_CI?=false
|
||||
ARGOCD_TEST_E2E?=true
|
||||
@@ -177,7 +179,7 @@ gogen: ensure-gopath
|
||||
go generate ./util/argo/...
|
||||
|
||||
.PHONY: protogen
|
||||
protogen: ensure-gopath
|
||||
protogen: ensure-gopath mod-vendor-local
|
||||
export GO111MODULE=off
|
||||
./hack/generate-proto.sh
|
||||
|
||||
@@ -186,6 +188,16 @@ openapigen: ensure-gopath
|
||||
export GO111MODULE=off
|
||||
./hack/update-openapi.sh
|
||||
|
||||
.PHONY: notification-catalog
|
||||
notification-catalog:
|
||||
go run ./hack/gen-catalog catalog
|
||||
|
||||
.PHONY: notification-docs
|
||||
notification-docs:
|
||||
go run ./hack/gen-docs
|
||||
go run ./hack/gen-catalog docs
|
||||
|
||||
|
||||
.PHONY: clientgen
|
||||
clientgen: ensure-gopath
|
||||
export GO111MODULE=off
|
||||
@@ -195,8 +207,9 @@ clientgen: ensure-gopath
|
||||
clidocsgen: ensure-gopath
|
||||
go run tools/cmd-docs/main.go
|
||||
|
||||
|
||||
.PHONY: codegen-local
|
||||
codegen-local: ensure-gopath mod-vendor-local gogen protogen clientgen openapigen clidocsgen manifests-local
|
||||
codegen-local: ensure-gopath mod-vendor-local notification-docs notification-catalog gogen protogen clientgen openapigen clidocsgen manifests-local
|
||||
rm -rf vendor/
|
||||
|
||||
.PHONY: codegen
|
||||
@@ -211,13 +224,17 @@ cli: test-tools-image
|
||||
cli-local: clean-debug
|
||||
CGO_ENABLED=0 go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/${CLI_NAME} ./cmd
|
||||
|
||||
.PHONY: gen-resources-cli-local
|
||||
gen-resources-cli-local: clean-debug
|
||||
CGO_ENABLED=0 go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/${GEN_RESOURCES_CLI_NAME} ./hack/gen-resources/cmd
|
||||
|
||||
.PHONY: release-cli
|
||||
release-cli: clean-debug image
|
||||
docker create --name tmp-argocd-linux $(IMAGE_PREFIX)argocd:$(IMAGE_TAG)
|
||||
docker cp tmp-argocd-linux:/usr/local/bin/argocd ${DIST_DIR}/argocd-linux-amd64
|
||||
docker cp tmp-argocd-linux:/usr/local/bin/argocd-darwin-amd64 ${DIST_DIR}/argocd-darwin-amd64
|
||||
docker cp tmp-argocd-linux:/usr/local/bin/argocd-windows-amd64.exe ${DIST_DIR}/argocd-windows-amd64.exe
|
||||
docker rm tmp-argocd-linux
|
||||
release-cli: clean-debug build-ui
|
||||
make BIN_NAME=argocd-darwin-amd64 GOOS=darwin argocd-all
|
||||
make BIN_NAME=argocd-darwin-arm64 GOOS=darwin GOARCH=arm64 argocd-all
|
||||
make BIN_NAME=argocd-linux-amd64 GOOS=linux argocd-all
|
||||
make BIN_NAME=argocd-linux-arm64 GOOS=linux GOARCH=arm64 argocd-all
|
||||
make BIN_NAME=argocd-windows-amd64.exe GOOS=windows argocd-all
|
||||
|
||||
.PHONY: test-tools-image
|
||||
test-tools-image:
|
||||
@@ -235,7 +252,7 @@ manifests: test-tools-image
|
||||
# consolidated binary for cli, util, server, repo-server, controller
|
||||
.PHONY: argocd-all
|
||||
argocd-all: clean-debug
|
||||
CGO_ENABLED=0 go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/${BIN_NAME} ./cmd
|
||||
CGO_ENABLED=0 GOOS=${GOOS} GOARCH=${GOARCH} go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/${BIN_NAME} ./cmd
|
||||
|
||||
.PHONY: server
|
||||
server: clean-debug
|
||||
@@ -249,20 +266,21 @@ repo-server:
|
||||
controller:
|
||||
CGO_ENABLED=0 go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-application-controller ./cmd
|
||||
|
||||
.PHONY: build-ui
|
||||
build-ui:
|
||||
docker build -t argocd-ui --target argocd-ui .
|
||||
find ./ui/dist -type f -not -name gitkeep -delete
|
||||
docker run -v ${CURRENT_DIR}/ui/dist/app:/tmp/app --rm -t argocd-ui sh -c 'cp -r ./dist/app/* /tmp/app/'
|
||||
|
||||
.PHONY: image
|
||||
ifeq ($(DEV_IMAGE), true)
|
||||
# The "dev" image builds the binaries from the users desktop environment (instead of in Docker)
|
||||
# which speeds up builds. Dockerfile.dev needs to be copied into dist to perform the build, since
|
||||
# the dist directory is under .dockerignore.
|
||||
IMAGE_TAG="dev-$(shell git describe --always --dirty)"
|
||||
image:
|
||||
image: build-ui
|
||||
docker build -t argocd-base --target argocd-base .
|
||||
docker build -t argocd-ui --target argocd-ui .
|
||||
find ./ui/dist -type f -not -name gitkeep -delete
|
||||
docker run -v ${CURRENT_DIR}/ui/dist/app:/tmp/app --rm -t argocd-ui sh -c 'cp -r ./dist/app/* /tmp/app/'
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd ./cmd
|
||||
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-darwin-amd64 ./cmd
|
||||
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-windows-amd64.exe ./cmd
|
||||
ln -sfn ${DIST_DIR}/argocd ${DIST_DIR}/argocd-server
|
||||
ln -sfn ${DIST_DIR}/argocd ${DIST_DIR}/argocd-application-controller
|
||||
ln -sfn ${DIST_DIR}/argocd ${DIST_DIR}/argocd-repo-server
|
||||
@@ -277,10 +295,8 @@ endif
|
||||
@if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argocd:$(IMAGE_TAG) ; fi
|
||||
|
||||
.PHONY: armimage
|
||||
# The "BUILD_ALL_CLIS" argument is to skip building the CLIs for darwin and windows
|
||||
# which would take a really long time.
|
||||
armimage:
|
||||
docker build -t $(IMAGE_PREFIX)argocd:$(IMAGE_TAG)-arm . --build-arg BUILD_ALL_CLIS="false"
|
||||
docker build -t $(IMAGE_PREFIX)argocd:$(IMAGE_TAG)-arm .
|
||||
|
||||
.PHONY: builder-image
|
||||
builder-image:
|
||||
@@ -493,10 +509,6 @@ serve-docs-local:
|
||||
serve-docs:
|
||||
docker run ${MKDOCS_RUN_ARGS} --rm -it -p 8000:8000 -v ${CURRENT_DIR}:/docs ${MKDOCS_DOCKER_IMAGE} serve -a 0.0.0.0:8000
|
||||
|
||||
.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 --
|
||||
|
||||
# Verify that kubectl can connect to your K8s cluster from Docker
|
||||
.PHONY: verify-kube-connect
|
||||
@@ -542,3 +554,7 @@ dep-ui-local:
|
||||
|
||||
start-test-k8s:
|
||||
go run ./hack/k8s
|
||||
|
||||
.PHONY: list
|
||||
list:
|
||||
@LC_ALL=C $(MAKE) -pRrq -f $(lastword $(MAKEFILE_LIST)) : 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | sort | egrep -v -e '^[^[:alnum:]]' -e '^$@$$'
|
||||
|
||||
6
OWNERS
6
OWNERS
@@ -20,4 +20,8 @@ reviewers:
|
||||
- hblixt
|
||||
- chetan-rns
|
||||
- wanghong230
|
||||
- pasha-codefresh
|
||||
- pasha-codefresh
|
||||
- ciiay
|
||||
- leoluz
|
||||
- crenshaw-dev
|
||||
- saumeya
|
||||
|
||||
10
Procfile
10
Procfile
@@ -1,8 +1,8 @@
|
||||
controller: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-application-controller go run ./cmd/main.go --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081}"
|
||||
api-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-server go run ./cmd/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} "
|
||||
dex: sh -c "ARGOCD_BINARY_NAME=argocd-dex go run github.com/argoproj/argo-cd/v2/cmd gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p ${ARGOCD_E2E_DEX_PORT:-5556}:${ARGOCD_E2E_DEX_PORT:-5556} -v `pwd`/dist/dex.yaml:/dex.yaml ghcr.io/dexidp/dex:v2.30.0 serve /dex.yaml"
|
||||
redis: bash -c "if [ \"$ARGOCD_REDIS_LOCAL\" == 'true' ]; then redis-server --save '' --appendonly no --port ${ARGOCD_E2E_REDIS_PORT:-6379}; else docker run --rm --name argocd-redis -i -p ${ARGOCD_E2E_REDIS_PORT:-6379}:${ARGOCD_E2E_REDIS_PORT:-6379} redis:6.2.4-alpine --save '' --appendonly no --port ${ARGOCD_E2E_REDIS_PORT:-6379}; fi"
|
||||
repo-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_GNUPGHOME=${ARGOCD_GNUPGHOME:-/tmp/argocd-local/gpg/keys} ARGOCD_PLUGINSOCKFILEPATH=${ARGOCD_PLUGINSOCKFILEPATH:-/tmp/argo-e2e/app/config/plugin} ARGOCD_GPG_DATA_PATH=${ARGOCD_GPG_DATA_PATH:-/tmp/argocd-local/gpg/source} ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-repo-server ARGOCD_GPG_ENABLED=${ARGOCD_GPG_ENABLED:-false} go run ./cmd/main.go --loglevel debug --port ${ARGOCD_E2E_REPOSERVER_PORT:-8081} --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379}"
|
||||
controller: [ "$BIN_MODE" == 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-application-controller $COMMAND --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081}"
|
||||
api-server: [ "$BIN_MODE" == 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-server $COMMAND --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} "
|
||||
dex: sh -c "ARGOCD_BINARY_NAME=argocd-dex go run github.com/argoproj/argo-cd/v2/cmd gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p ${ARGOCD_E2E_DEX_PORT:-5556}:${ARGOCD_E2E_DEX_PORT:-5556} -v `pwd`/dist/dex.yaml:/dex.yaml ghcr.io/dexidp/dex:v2.30.2 dex serve /dex.yaml"
|
||||
redis: bash -c "if [ \"$ARGOCD_REDIS_LOCAL\" == 'true' ]; then redis-server --save '' --appendonly no --port ${ARGOCD_E2E_REDIS_PORT:-6379}; else docker run --rm --name argocd-redis -i -p ${ARGOCD_E2E_REDIS_PORT:-6379}:${ARGOCD_E2E_REDIS_PORT:-6379} redis:6.2.6-alpine --save '' --appendonly no --port ${ARGOCD_E2E_REDIS_PORT:-6379}; fi"
|
||||
repo-server: [ "$BIN_MODE" == 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_GNUPGHOME=${ARGOCD_GNUPGHOME:-/tmp/argocd-local/gpg/keys} ARGOCD_PLUGINSOCKFILEPATH=${ARGOCD_PLUGINSOCKFILEPATH:-/tmp/argo-e2e/app/config/plugin} ARGOCD_GPG_DATA_PATH=${ARGOCD_GPG_DATA_PATH:-/tmp/argocd-local/gpg/source} ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-repo-server ARGOCD_GPG_ENABLED=${ARGOCD_GPG_ENABLED:-false} $COMMAND --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
|
||||
helm-registry: test/fixture/testrepos/start-helm-registry.sh
|
||||
|
||||
@@ -46,6 +46,7 @@ Participation in the Argo CD project is governed by the [CNCF Code of Conduct](h
|
||||
### Blogs and Presentations
|
||||
|
||||
1. [Awesome-Argo: A Curated List of Awesome Projects and Resources Related to Argo](https://github.com/terrytangyuan/awesome-argo)
|
||||
1. [Unveil the Secret Ingredients of Continuous Delivery at Enterprise Scale with Argo CD](https://blog.akuity.io/unveil-the-secret-ingredients-of-continuous-delivery-at-enterprise-scale-with-argo-cd-7c5b4057ee49)
|
||||
1. [GitOps Without Pipelines With ArgoCD Image Updater](https://youtu.be/avPUQin9kzU)
|
||||
1. [Combining Argo CD (GitOps), Crossplane (Control Plane), And KubeVela (OAM)](https://youtu.be/eEcgn_gU3SM)
|
||||
1. [How to Apply GitOps to Everything - Combining Argo CD and Crossplane](https://youtu.be/yrj4lmScKHQ)
|
||||
|
||||
16
SECURITY.md
16
SECURITY.md
@@ -1,6 +1,6 @@
|
||||
# Security Policy for Argo CD
|
||||
|
||||
Version: **v1.2 (2020-08-07)**
|
||||
Version: **v1.4 (2022-01-23)**
|
||||
|
||||
## Preface
|
||||
|
||||
@@ -26,8 +26,12 @@ are well aware of the issues that may affect Argo CD and are constantly
|
||||
working on the remediation of those that affect Argo CD and our users.
|
||||
|
||||
If you believe that we might have missed an issue that we should take a look
|
||||
at (that can happen), then please discuss it with us. But please, do validate
|
||||
that assumption before at least roughly.
|
||||
at (that can happen), then please discuss it with us. If there is a CVE
|
||||
assigned to the issue, please do open an issue on our GitHub tracker instead
|
||||
of writing to the security contact e-mail, since things reported by scanners
|
||||
are public already and the discussion that might emerge is of benefit to the
|
||||
general community. However, please validate your scanner results and its
|
||||
impact on Argo CD before opening an issue at least roughly.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
@@ -64,3 +68,9 @@ findings (unless you prefer to stay anonymous, of course).
|
||||
Please report vulnerabilities by e-mail to the following address:
|
||||
|
||||
* cncf-argo-security@lists.cncf.io
|
||||
|
||||
## Securing your Argo CD Instance
|
||||
|
||||
See the [operator manual security page](docs/operator-manual/security.md) for
|
||||
additional information about Argo CD's security features and how to make your
|
||||
Argo CD production ready.
|
||||
|
||||
17
USERS.md
17
USERS.md
@@ -11,6 +11,7 @@ Currently, the following organizations are **officially** using Argo CD:
|
||||
1. [Adventure](https://jp.adventurekk.com/)
|
||||
1. [Akuity](https://akuity.io/)
|
||||
1. [Alibaba Group](https://www.alibabagroup.com/)
|
||||
1. [Allianz Direct](https://www.allianzdirect.de/)
|
||||
1. [Ambassador Labs](https://www.getambassador.io/)
|
||||
1. [Ant Group](https://www.antgroup.com/)
|
||||
1. [ANSTO - Australian Synchrotron](https://www.synchrotron.org.au/)
|
||||
@@ -30,6 +31,7 @@ Currently, the following organizations are **officially** using Argo CD:
|
||||
1. [CARFAX](https://www.carfax.com)
|
||||
1. [Celonis](https://www.celonis.com/)
|
||||
1. [Chime](https://www.chime.com)
|
||||
1. [Cisco ET&I](https://eti.cisco.com/)
|
||||
1. [Codefresh](https://www.codefresh.io/)
|
||||
1. [Codility](https://www.codility.com/)
|
||||
1. [Commonbond](https://commonbond.co/)
|
||||
@@ -39,6 +41,7 @@ Currently, the following organizations are **officially** using Argo CD:
|
||||
1. [Cybozu](https://cybozu-global.com)
|
||||
1. [Chargetrip](https://chargetrip.com)
|
||||
1. [D2iQ](https://www.d2iq.com)
|
||||
1. [Deloitte](https://www.deloitte.com/)
|
||||
1. [Devtron Labs](https://github.com/devtron-labs/devtron)
|
||||
1. [EDF Renewables](https://www.edf-re.com/)
|
||||
1. [edX](https://edx.org)
|
||||
@@ -47,21 +50,26 @@ Currently, the following organizations are **officially** using Argo CD:
|
||||
1. [END.](https://www.endclothing.com/)
|
||||
1. [Energisme](https://energisme.com/)
|
||||
1. [Fave](https://myfave.com)
|
||||
1. [Flip](https://flip.id)
|
||||
1. [Fonoa](https://www.fonoa.com/)
|
||||
1. [Future PLC](https://www.futureplc.com/)
|
||||
1. [Garner](https://www.garnercorp.com)
|
||||
1. [G DATA CyberDefense AG](https://www.gdata-software.com/)
|
||||
1. [Generali Deutschland AG](https://www.generali.de/)
|
||||
1. [Gitpod](https://www.gitpod.io)
|
||||
1. [Glovo](https://www.glovoapp.com)
|
||||
1. [Gllue](https://gllue.com)
|
||||
1. [GMETRI](https://gmetri.com/)
|
||||
1. [Gojek](https://www.gojek.io/)
|
||||
1. [Greenpass](https://www.greenpass.com.br/)
|
||||
1. [Handelsbanken](https://www.handelsbanken.se)
|
||||
1. [Healy](https://www.healyworld.net)
|
||||
1. [Helio](https://helio.exchange)
|
||||
1. [hipages](https://hipages.com.au/)
|
||||
1. [Hiya](https://hiya.com)
|
||||
1. [Honestbank](https://honestbank.com)
|
||||
1. [IBM](https://www.ibm.com/)
|
||||
1. [Ibotta](https://home.ibotta.com)
|
||||
1. [IITS-Consulting](https://iits-consulting.de)
|
||||
1. [Index Exchange](https://www.indexexchange.com/)
|
||||
1. [InsideBoard](https://www.insideboard.com)
|
||||
@@ -93,7 +101,9 @@ Currently, the following organizations are **officially** using Argo CD:
|
||||
1. [New Relic](https://newrelic.com/)
|
||||
1. [Nextdoor](https://nextdoor.com/)
|
||||
1. [Nikkei](https://www.nikkei.co.jp/nikkeiinfo/en/)
|
||||
1. [Nitro](https://gonitro.com)
|
||||
1. [Octadesk](https://octadesk.com)
|
||||
1. [omegaUp](https://omegaUp.com)
|
||||
1. [openEuler](https://openeuler.org)
|
||||
1. [openGauss](https://opengauss.org/)
|
||||
1. [openLooKeng](https://openlookeng.io)
|
||||
@@ -119,11 +129,13 @@ Currently, the following organizations are **officially** using Argo CD:
|
||||
1. [Saildrone](https://www.saildrone.com/)
|
||||
1. [Saloodo! GmbH](https://www.saloodo.com)
|
||||
1. [Schwarz IT](https://jobs.schwarz/it-mission)
|
||||
1. [Skit](https://skit.ai/)
|
||||
1. [Snyk](https://snyk.io/)
|
||||
1. [Speee](https://speee.jp/)
|
||||
1. [Spendesk](https://spendesk.com/)
|
||||
1. [Sumo Logic](https://sumologic.com/)
|
||||
1. [Sutpc](http://www.sutpc.com/)
|
||||
1. [Swiss Post](https://github.com/swisspost)
|
||||
1. [Swisscom](https://www.swisscom.ch)
|
||||
1. [Swissquote](https://github.com/swissquote)
|
||||
1. [Syncier](https://syncier.com/)
|
||||
@@ -178,3 +190,8 @@ Currently, the following organizations are **officially** using Argo CD:
|
||||
1. [p3r](https://www.p3r.one/)
|
||||
1. [Faro](https://www.faro.com/)
|
||||
1. [Rise](https://www.risecard.eu/)
|
||||
1. [Devopsi - Poland Software/DevOps Consulting](https://devopsi.pl/)
|
||||
1. [Skyscanner](https://www.skyscanner.net/)
|
||||
1. [Casavo](https://casavo.com)
|
||||
1. [Majid Al Futtaim](https://www.majidalfuttaim.com/)
|
||||
1. [ZOZO](https://corp.zozo.com/)
|
||||
|
||||
@@ -256,7 +256,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "the selector to to restrict returned list to applications only with matched labels.",
|
||||
"description": "the selector to restrict returned list to applications only with matched labels.",
|
||||
"name": "selector",
|
||||
"in": "query"
|
||||
},
|
||||
@@ -520,7 +520,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "the selector to to restrict returned list to applications only with matched labels.",
|
||||
"description": "the selector to restrict returned list to applications only with matched labels.",
|
||||
"name": "selector",
|
||||
"in": "query"
|
||||
},
|
||||
@@ -1605,6 +1605,18 @@
|
||||
"type": "string",
|
||||
"name": "name",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "type is the type of the specified cluster identifier ( \"server\" - default, \"name\" ).",
|
||||
"name": "id.type",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "value holds the cluster server URL or cluster name.",
|
||||
"name": "id.value",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -1659,7 +1671,53 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/clusters/{cluster.server}": {
|
||||
"/api/v1/clusters/{id.value}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"ClusterService"
|
||||
],
|
||||
"summary": "Get returns a cluster by server address",
|
||||
"operationId": "ClusterService_Get",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "value holds the cluster server URL or cluster name",
|
||||
"name": "id.value",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "server",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "name",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "type is the type of the specified cluster identifier ( \"server\" - default, \"name\" ).",
|
||||
"name": "id.type",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1alpha1Cluster"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "An unexpected error response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/runtimeError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"tags": [
|
||||
"ClusterService"
|
||||
@@ -1669,8 +1727,8 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Server is the API server URL of the Kubernetes cluster",
|
||||
"name": "cluster.server",
|
||||
"description": "value holds the cluster server URL or cluster name",
|
||||
"name": "id.value",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
@@ -1690,41 +1748,11 @@
|
||||
"collectionFormat": "multi",
|
||||
"name": "updatedFields",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1alpha1Cluster"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "An unexpected error response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/runtimeError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/clusters/{server}": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"ClusterService"
|
||||
],
|
||||
"summary": "Get returns a cluster by server address",
|
||||
"operationId": "ClusterService_Get",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "server",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "name",
|
||||
"description": "type is the type of the specified cluster identifier ( \"server\" - default, \"name\" ).",
|
||||
"name": "id.type",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
@@ -1752,14 +1780,26 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "server",
|
||||
"description": "value holds the cluster server URL or cluster name",
|
||||
"name": "id.value",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "server",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "name",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "type is the type of the specified cluster identifier ( \"server\" - default, \"name\" ).",
|
||||
"name": "id.type",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -1778,7 +1818,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/clusters/{server}/invalidate-cache": {
|
||||
"/api/v1/clusters/{id.value}/invalidate-cache": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"ClusterService"
|
||||
@@ -1788,7 +1828,8 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "server",
|
||||
"description": "value holds the cluster server URL or cluster name",
|
||||
"name": "id.value",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
@@ -1809,7 +1850,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/clusters/{server}/rotate-auth": {
|
||||
"/api/v1/clusters/{id.value}/rotate-auth": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"ClusterService"
|
||||
@@ -1819,7 +1860,8 @@
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "server",
|
||||
"description": "value holds the cluster server URL or cluster name",
|
||||
"name": "id.value",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
@@ -2702,6 +2744,16 @@
|
||||
"type": "string",
|
||||
"name": "revision",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "appName",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "appProject",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -3106,7 +3158,7 @@
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "the selector to to restrict returned list to applications only with matched labels.",
|
||||
"description": "the selector to restrict returned list to applications only with matched labels.",
|
||||
"name": "selector",
|
||||
"in": "query"
|
||||
},
|
||||
@@ -3530,6 +3582,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"clusterClusterID": {
|
||||
"type": "object",
|
||||
"title": "ClusterID holds a cluster server URL or cluster name",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"title": "type is the type of the specified cluster identifier ( \"server\" - default, \"name\" )"
|
||||
},
|
||||
"value": {
|
||||
"type": "string",
|
||||
"title": "value holds the cluster server URL or cluster name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"clusterClusterResponse": {
|
||||
"type": "object"
|
||||
},
|
||||
@@ -3570,6 +3636,13 @@
|
||||
"type": "object",
|
||||
"title": "Help settings",
|
||||
"properties": {
|
||||
"binaryUrls": {
|
||||
"type": "object",
|
||||
"title": "the URLs for downloading argocd binaries",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"chatText": {
|
||||
"type": "string",
|
||||
"title": "the text for getting chat help, defaults to \"Chat now!\""
|
||||
@@ -4042,6 +4115,9 @@
|
||||
"appName": {
|
||||
"type": "string"
|
||||
},
|
||||
"appProject": {
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/v1alpha1ApplicationSource"
|
||||
}
|
||||
@@ -4892,6 +4968,10 @@
|
||||
"$ref": "#/definitions/v1alpha1HelmFileParameter"
|
||||
}
|
||||
},
|
||||
"ignoreMissingValueFiles": {
|
||||
"type": "boolean",
|
||||
"title": "IgnoreMissingValueFiles prevents helm template from failing when valueFiles do not exist locally by not appending them to helm template --values"
|
||||
},
|
||||
"parameters": {
|
||||
"type": "array",
|
||||
"title": "Parameters is a list of Helm parameters which are passed to the helm template command upon manifest generation",
|
||||
@@ -4907,6 +4987,10 @@
|
||||
"type": "string",
|
||||
"title": "ReleaseName is the Helm release name to use. If omitted it will use the application name"
|
||||
},
|
||||
"skipCrds": {
|
||||
"type": "boolean",
|
||||
"title": "SkipCrds skips custom resource definition installation step (Helm's --skip-crds)"
|
||||
},
|
||||
"valueFiles": {
|
||||
"type": "array",
|
||||
"title": "ValuesFiles is a list of Helm value files to use when generating a template",
|
||||
@@ -5785,16 +5869,25 @@
|
||||
},
|
||||
"v1alpha1OverrideIgnoreDiff": {
|
||||
"type": "object",
|
||||
"title": "TODO: describe this type",
|
||||
"title": "OverrideIgnoreDiff contains configurations about how fields should be ignored during diffs between\nthe desired state and live state",
|
||||
"properties": {
|
||||
"jSONPointers": {
|
||||
"type": "array",
|
||||
"title": "JSONPointers is a JSON path list following the format defined in RFC4627 (https://datatracker.ietf.org/doc/html/rfc6902#section-3)",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"jqPathExpressions": {
|
||||
"type": "array",
|
||||
"title": "JQPathExpressions is a JQ path list that will be evaludated during the diff process",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"managedFieldsManagers": {
|
||||
"type": "array",
|
||||
"title": "ManagedFieldsManagers is a list of trusted managers. Fields mutated by those managers will take precedence over the\ndesired state defined in the SCM and won't be displayed in diffs",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -6157,6 +6250,13 @@
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"managedFieldsManagers": {
|
||||
"type": "array",
|
||||
"title": "ManagedFieldsManagers is a list of trusted managers. Fields mutated by those managers will take precedence over the\ndesired state defined in the SCM and won't be displayed in diffs",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
57
cmd/argocd-git-ask-pass/commands/argocd_git_ask_pass.go
Normal file
57
cmd/argocd-git-ask-pass/commands/argocd_git_ask_pass.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/util/git"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/reposerver/askpass"
|
||||
"github.com/argoproj/argo-cd/v2/util/errors"
|
||||
grpc_util "github.com/argoproj/argo-cd/v2/util/grpc"
|
||||
"github.com/argoproj/argo-cd/v2/util/io"
|
||||
)
|
||||
|
||||
const (
|
||||
// cliName is the name of the CLI
|
||||
cliName = "argocd-git-ask-pass"
|
||||
)
|
||||
|
||||
func NewCommand() *cobra.Command {
|
||||
var command = cobra.Command{
|
||||
Use: cliName,
|
||||
Short: "Argo CD git credential helper",
|
||||
DisableAutoGenTag: true,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(os.Args) != 2 {
|
||||
errors.CheckError(fmt.Errorf("expected 1 argument, got %d", len(os.Args)-1))
|
||||
}
|
||||
nonce := os.Getenv(git.ASKPASS_NONCE_ENV)
|
||||
if nonce == "" {
|
||||
errors.CheckError(fmt.Errorf("%s is not set", git.ASKPASS_NONCE_ENV))
|
||||
}
|
||||
conn, err := grpc_util.BlockingDial(context.Background(), "unix", askpass.SocketPath, nil, grpc.WithInsecure())
|
||||
errors.CheckError(err)
|
||||
defer io.Close(conn)
|
||||
client := askpass.NewAskPassServiceClient(conn)
|
||||
|
||||
creds, err := client.GetCredentials(context.Background(), &askpass.CredentialsRequest{Nonce: nonce})
|
||||
errors.CheckError(err)
|
||||
switch {
|
||||
case strings.HasPrefix(os.Args[1], "Username"):
|
||||
fmt.Println(creds.Username)
|
||||
case strings.HasPrefix(os.Args[1], "Password"):
|
||||
fmt.Println(creds.Password)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown credential type '%s'", os.Args[1]))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return &command
|
||||
}
|
||||
132
cmd/argocd-notification/commands/controller.go
Normal file
132
cmd/argocd-notification/commands/controller.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
service "github.com/argoproj/argo-cd/v2/util/notification/argocd"
|
||||
|
||||
notificationscontroller "github.com/argoproj/argo-cd/v2/notification_controller/controller"
|
||||
|
||||
controller "github.com/argoproj/notifications-engine/pkg/controller"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultMetricsPort = 9001
|
||||
)
|
||||
|
||||
func addK8SFlagsToCmd(cmd *cobra.Command) clientcmd.ClientConfig {
|
||||
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||
loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig
|
||||
overrides := clientcmd.ConfigOverrides{}
|
||||
kflags := clientcmd.RecommendedConfigOverrideFlags("")
|
||||
cmd.PersistentFlags().StringVar(&loadingRules.ExplicitPath, "kubeconfig", "", "Path to a kube config. Only required if out-of-cluster")
|
||||
clientcmd.BindOverrideFlags(&overrides, cmd.PersistentFlags(), kflags)
|
||||
return clientcmd.NewInteractiveDeferredLoadingClientConfig(loadingRules, &overrides, os.Stdin)
|
||||
}
|
||||
|
||||
func NewCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
processorsCount int
|
||||
namespace string
|
||||
appLabelSelector string
|
||||
logLevel string
|
||||
logFormat string
|
||||
metricsPort int
|
||||
argocdRepoServer string
|
||||
argocdRepoServerPlaintext bool
|
||||
argocdRepoServerStrictTLS bool
|
||||
configMapName string
|
||||
secretName string
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: "controller",
|
||||
Short: "Starts Argo CD Notifications controller",
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
restConfig, err := clientConfig.ClientConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dynamicClient, err := dynamic.NewForConfig(restConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
k8sClient, err := kubernetes.NewForConfig(restConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if namespace == "" {
|
||||
namespace, _, err = clientConfig.Namespace()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
level, err := log.ParseLevel(logLevel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.SetLevel(level)
|
||||
|
||||
switch strings.ToLower(logFormat) {
|
||||
case "json":
|
||||
log.SetFormatter(&log.JSONFormatter{})
|
||||
case "text":
|
||||
if os.Getenv("FORCE_LOG_COLORS") == "1" {
|
||||
log.SetFormatter(&log.TextFormatter{ForceColors: true})
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("Unknown log format '%s'", logFormat)
|
||||
}
|
||||
|
||||
argocdService, err := service.NewArgoCDService(k8sClient, namespace, argocdRepoServer, argocdRepoServerPlaintext, argocdRepoServerStrictTLS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer argocdService.Close()
|
||||
|
||||
registry := controller.NewMetricsRegistry("argocd")
|
||||
http.Handle("/metrics", promhttp.HandlerFor(prometheus.Gatherers{registry, prometheus.DefaultGatherer}, promhttp.HandlerOpts{}))
|
||||
|
||||
go func() {
|
||||
log.Fatal(http.ListenAndServe(fmt.Sprintf("0.0.0.0:%d", metricsPort), http.DefaultServeMux))
|
||||
}()
|
||||
log.Infof("serving metrics on port %d", metricsPort)
|
||||
log.Infof("loading configuration %d", metricsPort)
|
||||
|
||||
ctrl := notificationscontroller.NewController(k8sClient, dynamicClient, argocdService, namespace, appLabelSelector, registry, secretName, configMapName)
|
||||
err = ctrl.Init(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go ctrl.Run(context.Background(), processorsCount)
|
||||
<-context.Background().Done()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
clientConfig = addK8SFlagsToCmd(&command)
|
||||
command.Flags().IntVar(&processorsCount, "processors-count", 1, "Processors count.")
|
||||
command.Flags().StringVar(&appLabelSelector, "app-label-selector", "", "App label selector.")
|
||||
command.Flags().StringVar(&namespace, "namespace", "", "Namespace which controller handles. Current namespace if empty.")
|
||||
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
|
||||
command.Flags().StringVar(&logFormat, "logformat", "text", "Set the logging format. One of: text|json")
|
||||
command.Flags().IntVar(&metricsPort, "metrics-port", defaultMetricsPort, "Metrics port")
|
||||
command.Flags().StringVar(&argocdRepoServer, "argocd-repo-server", "argocd-repo-server:8081", "Argo CD repo server address")
|
||||
command.Flags().BoolVar(&argocdRepoServerPlaintext, "argocd-repo-server-plaintext", false, "Use a plaintext client (non-TLS) to connect to repository server")
|
||||
command.Flags().BoolVar(&argocdRepoServerStrictTLS, "argocd-repo-server-strict-tls", false, "Perform strict validation of TLS certificates when connecting to repo server")
|
||||
command.Flags().StringVar(&configMapName, "config-map-name", "argocd-notifications-cm", "Set notifications ConfigMap name")
|
||||
command.Flags().StringVar(&secretName, "secret-name", "argocd-notifications-secret", "Set notifications Secret name")
|
||||
return &command
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/argoproj/argo-cd/v2/common"
|
||||
"github.com/argoproj/argo-cd/v2/reposerver"
|
||||
"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/v2/reposerver/askpass"
|
||||
reposervercache "github.com/argoproj/argo-cd/v2/reposerver/cache"
|
||||
"github.com/argoproj/argo-cd/v2/reposerver/metrics"
|
||||
"github.com/argoproj/argo-cd/v2/reposerver/repository"
|
||||
@@ -61,6 +62,10 @@ func getPauseGenerationOnFailureForRequests() int {
|
||||
return env.ParseNumFromEnv(common.EnvPauseGenerationRequests, defaultPauseGenerationOnFailureForRequests, 0, math.MaxInt32)
|
||||
}
|
||||
|
||||
func getSubmoduleEnabled() bool {
|
||||
return env.ParseBoolFromEnv(common.EnvGitSubmoduleEnabled, true)
|
||||
}
|
||||
|
||||
func NewCommand() *cobra.Command {
|
||||
var (
|
||||
parallelismLimit int64
|
||||
@@ -90,6 +95,7 @@ func NewCommand() *cobra.Command {
|
||||
cache, err := cacheSrc()
|
||||
errors.CheckError(err)
|
||||
|
||||
askPassServer := askpass.NewServer()
|
||||
metricsServer := metrics.NewMetricsServer()
|
||||
cacheutil.CollectMetrics(redisClient, metricsServer)
|
||||
server, err := reposerver.NewServer(metricsServer, cache, tlsConfigCustomizer, repository.RepoServerInitConstants{
|
||||
@@ -97,7 +103,8 @@ func NewCommand() *cobra.Command {
|
||||
PauseGenerationAfterFailedGenerationAttempts: getPauseGenerationAfterFailedGenerationAttempts(),
|
||||
PauseGenerationOnFailureForMinutes: getPauseGenerationOnFailureForMinutes(),
|
||||
PauseGenerationOnFailureForRequests: getPauseGenerationOnFailureForRequests(),
|
||||
})
|
||||
SubmoduleEnabled: getSubmoduleEnabled(),
|
||||
}, askPassServer)
|
||||
errors.CheckError(err)
|
||||
|
||||
grpc := server.CreateGRPC()
|
||||
@@ -128,6 +135,7 @@ func NewCommand() *cobra.Command {
|
||||
})
|
||||
http.Handle("/metrics", metricsServer.GetHandler())
|
||||
go func() { errors.CheckError(http.ListenAndServe(fmt.Sprintf(":%d", metricsPort), nil)) }()
|
||||
go func() { errors.CheckError(askPassServer.Run(askpass.SocketPath)) }()
|
||||
|
||||
if gpg.IsGPGEnabled() {
|
||||
log.Infof("Initializing GnuPG keyring at %s", common.GetGnuPGHomePath())
|
||||
|
||||
@@ -55,6 +55,7 @@ func NewAdminCommand() *cobra.Command {
|
||||
command.AddCommand(NewImportCommand())
|
||||
command.AddCommand(NewExportCommand())
|
||||
command.AddCommand(NewDashboardCommand())
|
||||
command.AddCommand(NewNotificationsCommand())
|
||||
|
||||
command.Flags().StringVar(&cmdutil.LogFormat, "logformat", "text", "Set the logging format. One of: text|json")
|
||||
command.Flags().StringVar(&cmdutil.LogLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
|
||||
|
||||
@@ -111,10 +111,11 @@ func NewExportCommand() *cobra.Command {
|
||||
// NewImportCommand defines a new command for exporting Kubernetes and Argo CD resources.
|
||||
func NewImportCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
prune bool
|
||||
dryRun bool
|
||||
verbose bool
|
||||
clientConfig clientcmd.ClientConfig
|
||||
prune bool
|
||||
dryRun bool
|
||||
verbose bool
|
||||
stopOperation bool
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: "import SOURCE",
|
||||
@@ -228,14 +229,14 @@ func NewImportCommand() *cobra.Command {
|
||||
fmt.Printf("%s/%s %s created%s\n", gvk.Group, gvk.Kind, bakObj.GetName(), dryRunMsg)
|
||||
}
|
||||
|
||||
} else if specsEqual(*bakObj, liveObj) {
|
||||
} else if specsEqual(*bakObj, liveObj) && checkAppHasNoNeedToStopOperation(liveObj, stopOperation) {
|
||||
if verbose {
|
||||
fmt.Printf("%s/%s %s unchanged%s\n", gvk.Group, gvk.Kind, bakObj.GetName(), dryRunMsg)
|
||||
}
|
||||
} else {
|
||||
isForbidden := false
|
||||
if !dryRun {
|
||||
newLive := updateLive(bakObj, &liveObj)
|
||||
newLive := updateLive(bakObj, &liveObj, stopOperation)
|
||||
_, err = dynClient.Update(context.Background(), newLive, v1.UpdateOptions{})
|
||||
if apierr.IsForbidden(err) || apierr.IsNotFound(err) {
|
||||
isForbidden = true
|
||||
@@ -300,10 +301,23 @@ func NewImportCommand() *cobra.Command {
|
||||
command.Flags().BoolVar(&dryRun, "dry-run", false, "Print what will be performed")
|
||||
command.Flags().BoolVar(&prune, "prune", false, "Prune secrets, applications and projects which do not appear in the backup")
|
||||
command.Flags().BoolVar(&verbose, "verbose", false, "Verbose output (versus only changed output)")
|
||||
command.Flags().BoolVar(&stopOperation, "stop-operation", false, "Stop any existing operations")
|
||||
|
||||
return &command
|
||||
}
|
||||
|
||||
// check app has no need to stop operation.
|
||||
func checkAppHasNoNeedToStopOperation(liveObj unstructured.Unstructured, stopOperation bool) bool {
|
||||
if !stopOperation {
|
||||
return true
|
||||
}
|
||||
switch liveObj.GetKind() {
|
||||
case "Application":
|
||||
return liveObj.Object["operation"] == nil
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// export writes the unstructured object and removes extraneous cruft from output before writing
|
||||
func export(w io.Writer, un unstructured.Unstructured) {
|
||||
name := un.GetName()
|
||||
@@ -329,7 +343,7 @@ func export(w io.Writer, un unstructured.Unstructured) {
|
||||
|
||||
// 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 {
|
||||
func updateLive(bak, live *unstructured.Unstructured, stopOperation bool) *unstructured.Unstructured {
|
||||
newLive := live.DeepCopy()
|
||||
newLive.SetAnnotations(bak.GetAnnotations())
|
||||
newLive.SetLabels(bak.GetLabels())
|
||||
@@ -344,6 +358,10 @@ func updateLive(bak, live *unstructured.Unstructured) *unstructured.Unstructured
|
||||
if _, ok := bak.Object["status"]; ok {
|
||||
newLive.Object["status"] = bak.Object["status"]
|
||||
}
|
||||
if stopOperation {
|
||||
newLive.Object["operation"] = nil
|
||||
}
|
||||
|
||||
case "ApplicationSet":
|
||||
newLive.Object["spec"] = bak.Object["spec"]
|
||||
}
|
||||
|
||||
@@ -13,18 +13,20 @@ import (
|
||||
|
||||
func NewDashboardCommand() *cobra.Command {
|
||||
var (
|
||||
port int
|
||||
port int
|
||||
address string
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "dashboard",
|
||||
Short: "Starts Argo CD Web UI locally",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
println(fmt.Sprintf("Argo CD UI is available at http://localhost:%d", port))
|
||||
println(fmt.Sprintf("Argo CD UI is available at http://%s:%d", address, port))
|
||||
<-context.Background().Done()
|
||||
},
|
||||
}
|
||||
clientOpts := &apiclient.ClientOptions{Core: true}
|
||||
headless.InitCommand(cmd, clientOpts, &port)
|
||||
headless.InitCommand(cmd, clientOpts, &port, &address)
|
||||
cmd.Flags().IntVar(&port, "port", common.DefaultPortAPIServer, "Listen on given port")
|
||||
cmd.Flags().StringVar(&address, "address", common.DefaultAddressAPIServer, "Listen on given address")
|
||||
return cmd
|
||||
}
|
||||
|
||||
51
cmd/argocd/commands/admin/notifications.go
Normal file
51
cmd/argocd/commands/admin/notifications.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
service "github.com/argoproj/argo-cd/v2/util/notification/argocd"
|
||||
settings "github.com/argoproj/argo-cd/v2/util/notification/settings"
|
||||
|
||||
"github.com/argoproj/notifications-engine/pkg/cmd"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
applications = schema.GroupVersionResource{Group: "argoproj.io", Version: "v1alpha1", Resource: "applications"}
|
||||
)
|
||||
|
||||
func NewNotificationsCommand() *cobra.Command {
|
||||
var (
|
||||
argocdRepoServer string
|
||||
argocdRepoServerPlaintext bool
|
||||
argocdRepoServerStrictTLS bool
|
||||
)
|
||||
|
||||
var argocdService service.Service
|
||||
toolsCommand := cmd.NewToolsCommand(
|
||||
"notifications",
|
||||
"notifications",
|
||||
applications,
|
||||
settings.GetFactorySettings(argocdService, "argocd-notifications-secret", "argocd-notifications-cm"), func(clientConfig clientcmd.ClientConfig) {
|
||||
k8sCfg, err := clientConfig.ClientConfig()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse k8s config: %v", err)
|
||||
}
|
||||
ns, _, err := clientConfig.Namespace()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse k8s config: %v", err)
|
||||
}
|
||||
argocdService, err = service.NewArgoCDService(kubernetes.NewForConfigOrDie(k8sCfg), ns, argocdRepoServer, argocdRepoServerPlaintext, argocdRepoServerStrictTLS)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initalize Argo CD service: %v", err)
|
||||
}
|
||||
})
|
||||
toolsCommand.PersistentFlags().StringVar(&argocdRepoServer, "argocd-repo-server", "argocd-repo-server:8081", "Argo CD repo server address")
|
||||
toolsCommand.PersistentFlags().BoolVar(&argocdRepoServerPlaintext, "argocd-repo-server-plaintext", false, "Use a plaintext client (non-TLS) to connect to repository server")
|
||||
toolsCommand.PersistentFlags().BoolVar(&argocdRepoServerStrictTLS, "argocd-repo-server-strict-tls", false, "Perform strict validation of TLS certificates when connecting to repo server")
|
||||
return toolsCommand
|
||||
}
|
||||
@@ -406,6 +406,10 @@ argocd admin settings resource-overrides ignore-differences ./deploy.yaml --argo
|
||||
return
|
||||
}
|
||||
|
||||
// This normalizer won't verify 'managedFieldsManagers' ignore difference
|
||||
// configurations. This requires access to live resources which is not the
|
||||
// purpose of this command. This will just apply jsonPointers and
|
||||
// jqPathExpressions configurations.
|
||||
normalizer, err := normalizers.NewIgnoreNormalizer(nil, overrides)
|
||||
errors.CheckError(err)
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/sync/common"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/diff"
|
||||
"github.com/argoproj/gitops-engine/pkg/health"
|
||||
"github.com/argoproj/gitops-engine/pkg/sync/hook"
|
||||
"github.com/argoproj/gitops-engine/pkg/sync/ignore"
|
||||
@@ -46,6 +45,7 @@ import (
|
||||
repoapiclient "github.com/argoproj/argo-cd/v2/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/v2/reposerver/repository"
|
||||
"github.com/argoproj/argo-cd/v2/util/argo"
|
||||
argodiff "github.com/argoproj/argo-cd/v2/util/argo/diff"
|
||||
"github.com/argoproj/argo-cd/v2/util/cli"
|
||||
"github.com/argoproj/argo-cd/v2/util/errors"
|
||||
"github.com/argoproj/argo-cd/v2/util/git"
|
||||
@@ -283,6 +283,7 @@ func NewApplicationLogsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
untilTime string
|
||||
filter string
|
||||
container string
|
||||
previous bool
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "logs APPNAME",
|
||||
@@ -312,6 +313,7 @@ func NewApplicationLogsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
UntilTime: &untilTime,
|
||||
Filter: &filter,
|
||||
Container: container,
|
||||
Previous: previous,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get pod logs: %v", err)
|
||||
@@ -353,6 +355,7 @@ func NewApplicationLogsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
command.Flags().StringVar(&untilTime, "until-time", "", "Show logs until this time")
|
||||
command.Flags().StringVar(&filter, "filter", "", "Show logs contain this string")
|
||||
command.Flags().StringVar(&container, "container", "", "Optional container name")
|
||||
command.Flags().BoolVarP(&previous, "previous", "p", false, "Specify if the previously terminated container logs should be returned")
|
||||
|
||||
return command
|
||||
}
|
||||
@@ -558,15 +561,16 @@ func NewApplicationSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
|
||||
// NewApplicationUnsetCommand returns a new instance of an `argocd app unset` command
|
||||
func NewApplicationUnsetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
parameters []string
|
||||
valuesLiteral bool
|
||||
valuesFiles []string
|
||||
nameSuffix bool
|
||||
namePrefix bool
|
||||
kustomizeVersion bool
|
||||
kustomizeImages []string
|
||||
pluginEnvs []string
|
||||
appOpts cmdutil.AppOptions
|
||||
parameters []string
|
||||
valuesLiteral bool
|
||||
valuesFiles []string
|
||||
ignoreMissingValueFiles bool
|
||||
nameSuffix bool
|
||||
namePrefix bool
|
||||
kustomizeVersion bool
|
||||
kustomizeImages []string
|
||||
pluginEnvs []string
|
||||
appOpts cmdutil.AppOptions
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "unset APPNAME parameters",
|
||||
@@ -643,7 +647,7 @@ func NewApplicationUnsetCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
|
||||
}
|
||||
}
|
||||
if app.Spec.Source.Helm != nil {
|
||||
if len(parameters) == 0 && len(valuesFiles) == 0 && !valuesLiteral {
|
||||
if len(parameters) == 0 && len(valuesFiles) == 0 && !valuesLiteral && !ignoreMissingValueFiles {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -671,6 +675,10 @@ func NewApplicationUnsetCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
|
||||
}
|
||||
}
|
||||
}
|
||||
if ignoreMissingValueFiles {
|
||||
app.Spec.Source.Helm.IgnoreMissingValueFiles = false
|
||||
updated = true
|
||||
}
|
||||
if app.Spec.Source.Helm.PassCredentials {
|
||||
app.Spec.Source.Helm.PassCredentials = false
|
||||
updated = true
|
||||
@@ -706,6 +714,7 @@ func NewApplicationUnsetCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
|
||||
command.Flags().StringArrayVarP(¶meters, "parameter", "p", []string{}, "Unset a parameter override (e.g. -p guestbook=image)")
|
||||
command.Flags().StringArrayVar(&valuesFiles, "values", []string{}, "Unset one or more Helm values files")
|
||||
command.Flags().BoolVar(&valuesLiteral, "values-literal", false, "Unset literal Helm values block")
|
||||
command.Flags().BoolVar(&ignoreMissingValueFiles, "ignore-missing-value-files", false, "Unset the helm ignore-missing-value-files option (revert to false)")
|
||||
command.Flags().BoolVar(&nameSuffix, "namesuffix", false, "Kustomize namesuffix")
|
||||
command.Flags().BoolVar(&namePrefix, "nameprefix", false, "Kustomize nameprefix")
|
||||
command.Flags().BoolVar(&kustomizeVersion, "kustomize-version", false, "Kustomize version")
|
||||
@@ -740,9 +749,9 @@ func liveObjects(resources []*argoappv1.ResourceDiff) ([]*unstructured.Unstructu
|
||||
return objs, nil
|
||||
}
|
||||
|
||||
func getLocalObjects(app *argoappv1.Application, local, localRepoRoot, appLabelKey, kubeVersion string, kustomizeOptions *argoappv1.KustomizeOptions,
|
||||
func getLocalObjects(app *argoappv1.Application, local, localRepoRoot, appLabelKey, kubeVersion string, apiVersions []string, kustomizeOptions *argoappv1.KustomizeOptions,
|
||||
configManagementPlugins []*argoappv1.ConfigManagementPlugin, trackingMethod string) []*unstructured.Unstructured {
|
||||
manifestStrings := getLocalObjectsString(app, local, localRepoRoot, appLabelKey, kubeVersion, kustomizeOptions, configManagementPlugins, trackingMethod)
|
||||
manifestStrings := getLocalObjectsString(app, local, localRepoRoot, appLabelKey, kubeVersion, apiVersions, kustomizeOptions, configManagementPlugins, trackingMethod)
|
||||
objs := make([]*unstructured.Unstructured, len(manifestStrings))
|
||||
for i := range manifestStrings {
|
||||
obj := unstructured.Unstructured{}
|
||||
@@ -753,10 +762,10 @@ func getLocalObjects(app *argoappv1.Application, local, localRepoRoot, appLabelK
|
||||
return objs
|
||||
}
|
||||
|
||||
func getLocalObjectsString(app *argoappv1.Application, local, localRepoRoot, appLabelKey, kubeVersion string, kustomizeOptions *argoappv1.KustomizeOptions,
|
||||
func getLocalObjectsString(app *argoappv1.Application, local, localRepoRoot, appLabelKey, kubeVersion string, apiVersions []string, kustomizeOptions *argoappv1.KustomizeOptions,
|
||||
configManagementPlugins []*argoappv1.ConfigManagementPlugin, trackingMethod string) []string {
|
||||
|
||||
res, err := repository.GenerateManifests(local, localRepoRoot, app.Spec.Source.TargetRevision, &repoapiclient.ManifestRequest{
|
||||
res, err := repository.GenerateManifests(context.Background(), local, localRepoRoot, app.Spec.Source.TargetRevision, &repoapiclient.ManifestRequest{
|
||||
Repo: &argoappv1.Repository{Repo: app.Spec.Source.RepoURL},
|
||||
AppLabelKey: appLabelKey,
|
||||
AppName: app.Name,
|
||||
@@ -764,9 +773,10 @@ func getLocalObjectsString(app *argoappv1.Application, local, localRepoRoot, app
|
||||
ApplicationSource: &app.Spec.Source,
|
||||
KustomizeOptions: kustomizeOptions,
|
||||
KubeVersion: kubeVersion,
|
||||
ApiVersions: apiVersions,
|
||||
Plugins: configManagementPlugins,
|
||||
TrackingMethod: trackingMethod,
|
||||
}, true)
|
||||
}, true, &git.NoopCredsStore{})
|
||||
errors.CheckError(err)
|
||||
|
||||
return res.Manifests
|
||||
@@ -851,7 +861,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
defer argoio.Close(conn)
|
||||
cluster, err := clusterIf.Get(context.Background(), &clusterpkg.ClusterQuery{Name: app.Spec.Destination.Name, Server: app.Spec.Destination.Server})
|
||||
errors.CheckError(err)
|
||||
localObjs := groupObjsByKey(getLocalObjects(app, local, localRepoRoot, argoSettings.AppLabelKey, cluster.ServerVersion, argoSettings.KustomizeOptions, argoSettings.ConfigManagementPlugins, argoSettings.TrackingMethod), liveObjs, app.Spec.Destination.Namespace)
|
||||
localObjs := groupObjsByKey(getLocalObjects(app, local, localRepoRoot, argoSettings.AppLabelKey, cluster.Info.ServerVersion, cluster.Info.APIVersions, argoSettings.KustomizeOptions, argoSettings.ConfigManagementPlugins, argoSettings.TrackingMethod), liveObjs, app.Spec.Destination.Namespace)
|
||||
items = groupObjsForDiff(resources, localObjs, items, argoSettings, appName)
|
||||
} else if revision != "" {
|
||||
var unstructureds []*unstructured.Unstructured
|
||||
@@ -893,10 +903,18 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
val := argoSettings.ResourceOverrides[k]
|
||||
overrides[k] = *val
|
||||
}
|
||||
normalizer, err := argo.NewDiffNormalizer(app.Spec.IgnoreDifferences, overrides)
|
||||
|
||||
// TODO remove hardcoded IgnoreAggregatedRoles and retrieve the
|
||||
// compareOptions in the protobuf
|
||||
ignoreAggregatedRoles := false
|
||||
diffConfig, err := argodiff.NewDiffConfigBuilder().
|
||||
WithDiffSettings(app.Spec.IgnoreDifferences, overrides, ignoreAggregatedRoles).
|
||||
WithTracking(argoSettings.AppLabelKey, argoSettings.TrackingMethod).
|
||||
WithNoCache().
|
||||
Build()
|
||||
errors.CheckError(err)
|
||||
|
||||
diffRes, err := diff.Diff(item.target, item.live, diff.WithNormalizer(normalizer))
|
||||
diffRes, err := argodiff.StateDiff(item.live, item.target, diffConfig)
|
||||
errors.CheckError(err)
|
||||
|
||||
if diffRes.Modified || item.target == nil || item.live == nil {
|
||||
@@ -1388,7 +1406,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
cluster, err := clusterIf.Get(context.Background(), &clusterpkg.ClusterQuery{Name: app.Spec.Destination.Name, Server: app.Spec.Destination.Server})
|
||||
errors.CheckError(err)
|
||||
argoio.Close(conn)
|
||||
localObjsStrings = getLocalObjectsString(app, local, localRepoRoot, argoSettings.AppLabelKey, cluster.ServerVersion, argoSettings.KustomizeOptions, argoSettings.ConfigManagementPlugins, argoSettings.TrackingMethod)
|
||||
localObjsStrings = getLocalObjectsString(app, local, localRepoRoot, argoSettings.AppLabelKey, cluster.Info.ServerVersion, cluster.Info.APIVersions, argoSettings.KustomizeOptions, argoSettings.ConfigManagementPlugins, argoSettings.TrackingMethod)
|
||||
}
|
||||
|
||||
syncOptionsFactory := func() *applicationpkg.SyncOptions {
|
||||
|
||||
@@ -85,15 +85,6 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
log.Fatalf("Context %s does not exist in kubeconfig", contextName)
|
||||
}
|
||||
|
||||
isTerminal := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
|
||||
|
||||
if isTerminal && !skipConfirmation {
|
||||
message := fmt.Sprintf("WARNING: This will create a service account `argocd-manager` on the cluster referenced by context `%s` with full cluster level admin privileges. Do you want to continue [y/N]? ", contextName)
|
||||
if !cli.AskToProceed(message) {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
overrides := clientcmd.ConfigOverrides{
|
||||
Context: *clstContext,
|
||||
}
|
||||
@@ -124,6 +115,14 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
if clusterOpts.ServiceAccount != "" {
|
||||
managerBearerToken, err = clusterauth.GetServiceAccountBearerToken(clientset, clusterOpts.SystemNamespace, clusterOpts.ServiceAccount)
|
||||
} else {
|
||||
isTerminal := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
|
||||
|
||||
if isTerminal && !skipConfirmation {
|
||||
message := fmt.Sprintf("WARNING: This will create a service account `argocd-manager` on the cluster referenced by context `%s` with full cluster level admin privileges. Do you want to continue [y/N]? ", contextName)
|
||||
if !cli.AskToProceed(message) {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
managerBearerToken, err = clusterauth.InstallClusterManagerRBAC(clientset, clusterOpts.SystemNamespace, clusterOpts.Namespaces)
|
||||
}
|
||||
errors.CheckError(err)
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
type forwardCacheClient struct {
|
||||
namespace string
|
||||
context string
|
||||
init sync.Once
|
||||
client cache.CacheClient
|
||||
err error
|
||||
@@ -25,7 +26,9 @@ type forwardCacheClient struct {
|
||||
|
||||
func (c *forwardCacheClient) doLazy(action func(client cache.CacheClient) error) error {
|
||||
c.init.Do(func() {
|
||||
overrides := clientcmd.ConfigOverrides{}
|
||||
overrides := clientcmd.ConfigOverrides{
|
||||
CurrentContext: c.context,
|
||||
}
|
||||
redisPort, err := kubeutil.PortForward(6379, c.namespace, &overrides,
|
||||
"app.kubernetes.io/name=argocd-redis-ha-haproxy", "app.kubernetes.io/name=argocd-redis")
|
||||
if err != nil {
|
||||
@@ -74,6 +77,7 @@ func (c *forwardCacheClient) NotifyUpdated(key string) error {
|
||||
|
||||
type forwardRepoClientset struct {
|
||||
namespace string
|
||||
context string
|
||||
init sync.Once
|
||||
repoClientset repoapiclient.Clientset
|
||||
err error
|
||||
@@ -81,7 +85,9 @@ type forwardRepoClientset struct {
|
||||
|
||||
func (c *forwardRepoClientset) NewRepoServerClient() (io.Closer, repoapiclient.RepoServerServiceClient, error) {
|
||||
c.init.Do(func() {
|
||||
overrides := clientcmd.ConfigOverrides{}
|
||||
overrides := clientcmd.ConfigOverrides{
|
||||
CurrentContext: c.context,
|
||||
}
|
||||
repoServerPort, err := kubeutil.PortForward(8081, c.namespace, &overrides, "app.kubernetes.io/name=argocd-repo-server")
|
||||
if err != nil {
|
||||
c.err = err
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/utils/pointer"
|
||||
|
||||
argoapi "github.com/argoproj/argo-cd/v2/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
|
||||
@@ -27,6 +28,8 @@ import (
|
||||
"github.com/argoproj/argo-cd/v2/util/cli"
|
||||
"github.com/argoproj/argo-cd/v2/util/io"
|
||||
"github.com/argoproj/argo-cd/v2/util/localconfig"
|
||||
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
func testAPI(clientOpts *argoapi.ClientOptions) error {
|
||||
@@ -43,9 +46,16 @@ func testAPI(clientOpts *argoapi.ClientOptions) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func retrieveContextIfChanged(contextFlag *flag.Flag) string {
|
||||
if contextFlag != nil && contextFlag.Changed {
|
||||
return contextFlag.Value.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// InitCommand allows executing command in a headless mode: on the fly starts Argo CD API server and
|
||||
// changes provided client options to use started API server port
|
||||
func InitCommand(cmd *cobra.Command, clientOpts *argoapi.ClientOptions, port *int) *cobra.Command {
|
||||
func InitCommand(cmd *cobra.Command, clientOpts *argoapi.ClientOptions, port *int, address *string) *cobra.Command {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
flags := pflag.NewFlagSet("tmp", pflag.ContinueOnError)
|
||||
clientConfig := cli.AddKubectlFlagsToSet(flags)
|
||||
@@ -81,8 +91,12 @@ func InitCommand(cmd *cobra.Command, clientOpts *argoapi.ClientOptions, port *in
|
||||
cli.SetLogLevel(log.ErrorLevel.String())
|
||||
log.SetLevel(log.ErrorLevel)
|
||||
os.Setenv(v1alpha1.EnvVarFakeInClusterConfig, "true")
|
||||
if address == nil {
|
||||
address = pointer.String("localhost")
|
||||
}
|
||||
if port == nil || *port == 0 {
|
||||
ln, err := net.Listen("tcp", "localhost:0")
|
||||
addr := fmt.Sprintf("%s:0", *address)
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -108,12 +122,14 @@ func InitCommand(cmd *cobra.Command, clientOpts *argoapi.ClientOptions, port *in
|
||||
return err
|
||||
}
|
||||
|
||||
context := retrieveContextIfChanged(cmd.Flag("context"))
|
||||
|
||||
mr, err := miniredis.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
appstateCache := appstatecache.NewCache(cacheutil.NewCache(&forwardCacheClient{namespace: namespace}), time.Hour)
|
||||
appstateCache := appstatecache.NewCache(cacheutil.NewCache(&forwardCacheClient{namespace: namespace, context: context}), time.Hour)
|
||||
srv := server.NewServer(ctx, server.ArgoCDServerOpts{
|
||||
EnableGZip: false,
|
||||
Namespace: namespace,
|
||||
@@ -124,12 +140,12 @@ func InitCommand(cmd *cobra.Command, clientOpts *argoapi.ClientOptions, port *in
|
||||
Cache: servercache.NewCache(appstateCache, 0, 0, 0),
|
||||
KubeClientset: kubeClientset,
|
||||
Insecure: true,
|
||||
ListenHost: "localhost",
|
||||
RepoClientset: &forwardRepoClientset{namespace: namespace},
|
||||
ListenHost: *address,
|
||||
RepoClientset: &forwardRepoClientset{namespace: namespace, context: context},
|
||||
})
|
||||
|
||||
go srv.Run(ctx, *port, 0)
|
||||
clientOpts.ServerAddr = fmt.Sprintf("localhost:%d", *port)
|
||||
clientOpts.ServerAddr = fmt.Sprintf("%s:%d", *address, *port)
|
||||
clientOpts.PlainText = true
|
||||
if !cache.WaitForCacheSync(ctx.Done(), srv.Initialized) {
|
||||
log.Fatal("Timed out waiting for project cache to sync")
|
||||
|
||||
80
cmd/argocd/commands/headless/headless_test.go
Normal file
80
cmd/argocd/commands/headless/headless_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package headless
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
flag "github.com/spf13/pflag"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type StringFlag struct {
|
||||
// The exact value provided on the flag
|
||||
value string
|
||||
}
|
||||
|
||||
func (f StringFlag) String() string {
|
||||
return f.value
|
||||
}
|
||||
|
||||
func (f *StringFlag) Set(value string) error {
|
||||
f.value = value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *StringFlag) Type() string {
|
||||
return "string"
|
||||
}
|
||||
|
||||
func Test_FlagContextNotChanged(t *testing.T) {
|
||||
res := retrieveContextIfChanged(&flag.Flag{
|
||||
Name: "",
|
||||
Shorthand: "",
|
||||
Usage: "",
|
||||
Value: &StringFlag{value: "test"},
|
||||
DefValue: "",
|
||||
Changed: false,
|
||||
NoOptDefVal: "",
|
||||
Deprecated: "",
|
||||
Hidden: false,
|
||||
ShorthandDeprecated: "",
|
||||
Annotations: nil,
|
||||
})
|
||||
|
||||
assert.Equal(t, "", res)
|
||||
}
|
||||
|
||||
func Test_FlagContextChanged(t *testing.T) {
|
||||
res := retrieveContextIfChanged(&flag.Flag{
|
||||
Name: "",
|
||||
Shorthand: "",
|
||||
Usage: "",
|
||||
Value: &StringFlag{value: "test"},
|
||||
DefValue: "",
|
||||
Changed: true,
|
||||
NoOptDefVal: "",
|
||||
Deprecated: "",
|
||||
Hidden: false,
|
||||
ShorthandDeprecated: "",
|
||||
Annotations: nil,
|
||||
})
|
||||
|
||||
assert.Equal(t, "test", res)
|
||||
}
|
||||
|
||||
func Test_FlagContextNil(t *testing.T) {
|
||||
res := retrieveContextIfChanged(&flag.Flag{
|
||||
Name: "",
|
||||
Shorthand: "",
|
||||
Usage: "",
|
||||
Value: nil,
|
||||
DefValue: "",
|
||||
Changed: false,
|
||||
NoOptDefVal: "",
|
||||
Deprecated: "",
|
||||
Hidden: false,
|
||||
ShorthandDeprecated: "",
|
||||
Annotations: nil,
|
||||
})
|
||||
|
||||
assert.Equal(t, "", res)
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/dgrijalva/jwt-go/v4"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
"github.com/spf13/cobra"
|
||||
@@ -127,9 +127,7 @@ argocd login cd.argoproj.io --core`,
|
||||
errors.CheckError(err)
|
||||
tokenString, refreshToken = oauth2Login(ctx, ssoPort, acdSet.GetOIDCConfig(), oauth2conf, provider)
|
||||
}
|
||||
parser := &jwt.Parser{
|
||||
ValidationHelper: jwt.NewValidationHelper(jwt.WithoutClaimsValidation(), jwt.WithoutAudienceValidation()),
|
||||
}
|
||||
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
|
||||
claims := jwt.MapClaims{}
|
||||
_, _, err := parser.ParseUnverified(tokenString, &claims)
|
||||
errors.CheckError(err)
|
||||
|
||||
@@ -3,7 +3,7 @@ package commands
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/dgrijalva/jwt-go/v4"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
timeutil "github.com/argoproj/pkg/time"
|
||||
jwtgo "github.com/dgrijalva/jwt-go/v4"
|
||||
jwtgo "github.com/golang-jwt/jwt/v4"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
argocdclient "github.com/argoproj/argo-cd/v2/pkg/apiclient"
|
||||
|
||||
@@ -40,19 +40,19 @@ func NewCommand() *cobra.Command {
|
||||
}
|
||||
|
||||
command.AddCommand(NewCompletionCommand())
|
||||
command.AddCommand(headless.InitCommand(NewVersionCmd(&clientOpts), &clientOpts, nil))
|
||||
command.AddCommand(headless.InitCommand(NewClusterCommand(&clientOpts, pathOpts), &clientOpts, nil))
|
||||
command.AddCommand(headless.InitCommand(NewApplicationCommand(&clientOpts), &clientOpts, nil))
|
||||
command.AddCommand(headless.InitCommand(NewVersionCmd(&clientOpts), &clientOpts, nil, nil))
|
||||
command.AddCommand(headless.InitCommand(NewClusterCommand(&clientOpts, pathOpts), &clientOpts, nil, nil))
|
||||
command.AddCommand(headless.InitCommand(NewApplicationCommand(&clientOpts), &clientOpts, nil, nil))
|
||||
command.AddCommand(NewLoginCommand(&clientOpts))
|
||||
command.AddCommand(NewReloginCommand(&clientOpts))
|
||||
command.AddCommand(headless.InitCommand(NewRepoCommand(&clientOpts), &clientOpts, nil))
|
||||
command.AddCommand(headless.InitCommand(NewRepoCredsCommand(&clientOpts), &clientOpts, nil))
|
||||
command.AddCommand(headless.InitCommand(NewRepoCommand(&clientOpts), &clientOpts, nil, nil))
|
||||
command.AddCommand(headless.InitCommand(NewRepoCredsCommand(&clientOpts), &clientOpts, nil, nil))
|
||||
command.AddCommand(NewContextCommand(&clientOpts))
|
||||
command.AddCommand(headless.InitCommand(NewProjectCommand(&clientOpts), &clientOpts, nil))
|
||||
command.AddCommand(headless.InitCommand(NewAccountCommand(&clientOpts), &clientOpts, nil))
|
||||
command.AddCommand(headless.InitCommand(NewProjectCommand(&clientOpts), &clientOpts, nil, nil))
|
||||
command.AddCommand(headless.InitCommand(NewAccountCommand(&clientOpts), &clientOpts, nil, nil))
|
||||
command.AddCommand(NewLogoutCommand(&clientOpts))
|
||||
command.AddCommand(headless.InitCommand(NewCertCommand(&clientOpts), &clientOpts, nil))
|
||||
command.AddCommand(headless.InitCommand(NewGPGCommand(&clientOpts), &clientOpts, nil))
|
||||
command.AddCommand(headless.InitCommand(NewCertCommand(&clientOpts), &clientOpts, nil, nil))
|
||||
command.AddCommand(headless.InitCommand(NewGPGCommand(&clientOpts), &clientOpts, nil, nil))
|
||||
command.AddCommand(admin.NewAdminCommand())
|
||||
|
||||
defaultLocalConfigPath, err := localconfig.DefaultLocalConfigPath()
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
appcontroller "github.com/argoproj/argo-cd/v2/cmd/argocd-application-controller/commands"
|
||||
cmpserver "github.com/argoproj/argo-cd/v2/cmd/argocd-cmp-server/commands"
|
||||
dex "github.com/argoproj/argo-cd/v2/cmd/argocd-dex/commands"
|
||||
gitaskpass "github.com/argoproj/argo-cd/v2/cmd/argocd-git-ask-pass/commands"
|
||||
notification "github.com/argoproj/argo-cd/v2/cmd/argocd-notification/commands"
|
||||
reposerver "github.com/argoproj/argo-cd/v2/cmd/argocd-repo-server/commands"
|
||||
apiserver "github.com/argoproj/argo-cd/v2/cmd/argocd-server/commands"
|
||||
cli "github.com/argoproj/argo-cd/v2/cmd/argocd/commands"
|
||||
@@ -39,6 +41,10 @@ func main() {
|
||||
command = cmpserver.NewCommand()
|
||||
case "argocd-dex":
|
||||
command = dex.NewCommand()
|
||||
case "argocd-notifications":
|
||||
command = notification.NewCommand()
|
||||
case "argocd-git-ask-pass":
|
||||
command = gitaskpass.NewCommand()
|
||||
default:
|
||||
command = cli.NewCommand()
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ type AppOptions struct {
|
||||
destNamespace string
|
||||
Parameters []string
|
||||
valuesFiles []string
|
||||
ignoreMissingValueFiles bool
|
||||
values string
|
||||
releaseName string
|
||||
helmSets []string
|
||||
@@ -43,6 +44,7 @@ type AppOptions struct {
|
||||
helmSetFiles []string
|
||||
helmVersion string
|
||||
helmPassCredentials bool
|
||||
helmSkipCrds bool
|
||||
project string
|
||||
syncPolicy string
|
||||
syncOptions []string
|
||||
@@ -86,6 +88,7 @@ func AddAppFlags(command *cobra.Command, opts *AppOptions) {
|
||||
command.Flags().StringVar(&opts.destNamespace, "dest-namespace", "", "K8s target namespace (overrides the namespace specified in the ksonnet app.yaml)")
|
||||
command.Flags().StringArrayVarP(&opts.Parameters, "parameter", "p", []string{}, "set a parameter override (e.g. -p guestbook=image=example/guestbook:latest)")
|
||||
command.Flags().StringArrayVar(&opts.valuesFiles, "values", []string{}, "Helm values file(s) to use")
|
||||
command.Flags().BoolVar(&opts.ignoreMissingValueFiles, "ignore-missing-value-files", false, "Ignore locally missing valueFiles when setting helm template --values")
|
||||
command.Flags().StringVar(&opts.values, "values-literal-file", "", "Filename or URL to import as a literal Helm values block")
|
||||
command.Flags().StringVar(&opts.releaseName, "release-name", "", "Helm release-name")
|
||||
command.Flags().StringVar(&opts.helmVersion, "helm-version", "", "Helm version")
|
||||
@@ -93,6 +96,7 @@ func AddAppFlags(command *cobra.Command, opts *AppOptions) {
|
||||
command.Flags().StringArrayVar(&opts.helmSets, "helm-set", []string{}, "Helm set values on the command line (can be repeated to set several values: --helm-set key1=val1 --helm-set key2=val2)")
|
||||
command.Flags().StringArrayVar(&opts.helmSetStrings, "helm-set-string", []string{}, "Helm set STRING values on the command line (can be repeated to set several values: --helm-set-string key1=val1 --helm-set-string key2=val2)")
|
||||
command.Flags().StringArrayVar(&opts.helmSetFiles, "helm-set-file", []string{}, "Helm set values from respective files specified via the command line (can be repeated to set several values: --helm-set-file key1=path1 --helm-set-file key2=path2)")
|
||||
command.Flags().BoolVar(&opts.helmSkipCrds, "helm-skip-crds", false, "Skip helm crd installation step")
|
||||
command.Flags().StringVar(&opts.project, "project", "", "Application project name")
|
||||
command.Flags().StringVar(&opts.syncPolicy, "sync-policy", "", "Set the sync policy (one of: none, automated (aliases of automated: auto, automatic))")
|
||||
command.Flags().StringArrayVar(&opts.syncOptions, "sync-option", []string{}, "Add or remove a sync option, e.g add `Prune=false`. Remove using `!` prefix, e.g. `!Prune=false`")
|
||||
@@ -147,6 +151,8 @@ func SetAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, ap
|
||||
spec.RevisionHistoryLimit = &i
|
||||
case "values":
|
||||
setHelmOpt(&spec.Source, helmOpts{valueFiles: appOpts.valuesFiles})
|
||||
case "ignore-missing-value-files":
|
||||
setHelmOpt(&spec.Source, helmOpts{ignoreMissingValueFiles: appOpts.ignoreMissingValueFiles})
|
||||
case "values-literal-file":
|
||||
var data []byte
|
||||
|
||||
@@ -171,6 +177,8 @@ func SetAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, ap
|
||||
setHelmOpt(&spec.Source, helmOpts{helmSetStrings: appOpts.helmSetStrings})
|
||||
case "helm-set-file":
|
||||
setHelmOpt(&spec.Source, helmOpts{helmSetFiles: appOpts.helmSetFiles})
|
||||
case "helm-skip-crds":
|
||||
setHelmOpt(&spec.Source, helmOpts{skipCrds: appOpts.helmSkipCrds})
|
||||
case "directory-recurse":
|
||||
if spec.Source.Directory != nil {
|
||||
spec.Source.Directory.Recurse = appOpts.directoryRecurse
|
||||
@@ -381,14 +389,16 @@ func setPluginOptEnvs(src *argoappv1.ApplicationSource, envs []string) {
|
||||
}
|
||||
|
||||
type helmOpts struct {
|
||||
valueFiles []string
|
||||
values string
|
||||
releaseName string
|
||||
version string
|
||||
helmSets []string
|
||||
helmSetStrings []string
|
||||
helmSetFiles []string
|
||||
passCredentials bool
|
||||
valueFiles []string
|
||||
ignoreMissingValueFiles bool
|
||||
values string
|
||||
releaseName string
|
||||
version string
|
||||
helmSets []string
|
||||
helmSetStrings []string
|
||||
helmSetFiles []string
|
||||
passCredentials bool
|
||||
skipCrds bool
|
||||
}
|
||||
|
||||
func setHelmOpt(src *argoappv1.ApplicationSource, opts helmOpts) {
|
||||
@@ -398,6 +408,9 @@ func setHelmOpt(src *argoappv1.ApplicationSource, opts helmOpts) {
|
||||
if len(opts.valueFiles) > 0 {
|
||||
src.Helm.ValueFiles = opts.valueFiles
|
||||
}
|
||||
if opts.ignoreMissingValueFiles {
|
||||
src.Helm.IgnoreMissingValueFiles = opts.ignoreMissingValueFiles
|
||||
}
|
||||
if len(opts.values) > 0 {
|
||||
src.Helm.Values = opts.values
|
||||
}
|
||||
@@ -410,6 +423,9 @@ func setHelmOpt(src *argoappv1.ApplicationSource, opts helmOpts) {
|
||||
if opts.passCredentials {
|
||||
src.Helm.PassCredentials = opts.passCredentials
|
||||
}
|
||||
if opts.skipCrds {
|
||||
src.Helm.SkipCrds = opts.skipCrds
|
||||
}
|
||||
for _, text := range opts.helmSets {
|
||||
p, err := argoappv1.NewHelmParameter(text, false)
|
||||
if err != nil {
|
||||
|
||||
@@ -24,6 +24,11 @@ func Test_setHelmOpt(t *testing.T) {
|
||||
setHelmOpt(&src, helmOpts{valueFiles: []string{"foo"}})
|
||||
assert.Equal(t, []string{"foo"}, src.Helm.ValueFiles)
|
||||
})
|
||||
t.Run("IgnoreMissingValueFiles", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setHelmOpt(&src, helmOpts{ignoreMissingValueFiles: true})
|
||||
assert.Equal(t, true, src.Helm.IgnoreMissingValueFiles)
|
||||
})
|
||||
t.Run("ReleaseName", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setHelmOpt(&src, helmOpts{releaseName: "foo"})
|
||||
@@ -54,6 +59,11 @@ func Test_setHelmOpt(t *testing.T) {
|
||||
setHelmOpt(&src, helmOpts{passCredentials: true})
|
||||
assert.Equal(t, true, src.Helm.PassCredentials)
|
||||
})
|
||||
t.Run("HelmSkipCrds", func(t *testing.T) {
|
||||
src := v1alpha1.ApplicationSource{}
|
||||
setHelmOpt(&src, helmOpts{skipCrds: true})
|
||||
assert.Equal(t, true, src.Helm.SkipCrds)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_setKustomizeOpt(t *testing.T) {
|
||||
|
||||
@@ -24,27 +24,23 @@ type Clientset interface {
|
||||
}
|
||||
|
||||
type clientSet struct {
|
||||
address string
|
||||
timeoutSeconds int
|
||||
address string
|
||||
}
|
||||
|
||||
func (c *clientSet) NewConfigManagementPluginClient() (io.Closer, ConfigManagementPluginServiceClient, error) {
|
||||
conn, err := NewConnection(c.address, c.timeoutSeconds)
|
||||
conn, err := NewConnection(c.address)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return conn, NewConfigManagementPluginServiceClient(conn), nil
|
||||
}
|
||||
|
||||
func NewConnection(address string, timeoutSeconds int) (*grpc.ClientConn, error) {
|
||||
func NewConnection(address string) (*grpc.ClientConn, error) {
|
||||
retryOpts := []grpc_retry.CallOption{
|
||||
grpc_retry.WithMax(3),
|
||||
grpc_retry.WithBackoff(grpc_retry.BackoffLinear(1000 * time.Millisecond)),
|
||||
}
|
||||
unaryInterceptors := []grpc.UnaryClientInterceptor{grpc_retry.UnaryClientInterceptor(retryOpts...)}
|
||||
if timeoutSeconds > 0 {
|
||||
unaryInterceptors = append(unaryInterceptors, grpc_util.WithTimeout(time.Duration(timeoutSeconds)*time.Second))
|
||||
}
|
||||
dialOpts := []grpc.DialOption{
|
||||
grpc.WithStreamInterceptor(grpc_retry.StreamClientInterceptor(retryOpts...)),
|
||||
grpc.WithUnaryInterceptor(grpc_middleware.ChainUnaryClient(unaryInterceptors...)),
|
||||
@@ -60,7 +56,7 @@ func NewConnection(address string, timeoutSeconds int) (*grpc.ClientConn, error)
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// NewCMPServerClientset creates new instance of config management plugin server Clientset
|
||||
func NewConfigManagementPluginClientSet(address string, timeoutSeconds int) Clientset {
|
||||
return &clientSet{address: address, timeoutSeconds: timeoutSeconds}
|
||||
// NewConfigManagementPluginClientSet creates new instance of config management plugin server Clientset
|
||||
func NewConfigManagementPluginClientSet(address string) Clientset {
|
||||
return &clientSet{address: address}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/pkg/rand"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/util/buffered_context"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
"github.com/mattn/go-zglob"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/cmpserver/apiclient"
|
||||
executil "github.com/argoproj/argo-cd/v2/util/exec"
|
||||
)
|
||||
|
||||
// cmpTimeoutBuffer is the amount of time before the request deadline to timeout server-side work. It makes sure there's
|
||||
// enough time before the client times out to send a meaningful error message.
|
||||
const cmpTimeoutBuffer = 100 * time.Millisecond
|
||||
|
||||
// Service implements ConfigManagementPluginService interface
|
||||
type Service struct {
|
||||
initConstants CMPServerInitConstants
|
||||
@@ -32,14 +42,78 @@ func NewService(initConstants CMPServerInitConstants) *Service {
|
||||
}
|
||||
}
|
||||
|
||||
func runCommand(command Command, path string, env []string) (string, error) {
|
||||
func runCommand(ctx context.Context, command Command, path string, env []string) (string, error) {
|
||||
if len(command.Command) == 0 {
|
||||
return "", fmt.Errorf("Command is empty")
|
||||
}
|
||||
cmd := exec.Command(command.Command[0], append(command.Command[1:], command.Args...)...)
|
||||
cmd := exec.CommandContext(ctx, command.Command[0], append(command.Command[1:], command.Args...)...)
|
||||
|
||||
cmd.Env = env
|
||||
cmd.Dir = path
|
||||
return executil.Run(cmd)
|
||||
|
||||
execId, err := rand.RandString(5)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
logCtx := log.WithFields(log.Fields{"execID": execId})
|
||||
|
||||
// log in a way we can copy-and-paste into a terminal
|
||||
args := strings.Join(cmd.Args, " ")
|
||||
logCtx.WithFields(log.Fields{"dir": cmd.Dir}).Info(args)
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
// Make sure the command is killed immediately on timeout. https://stackoverflow.com/a/38133948/684776
|
||||
cmd.SysProcAttr = newSysProcAttr(true)
|
||||
|
||||
start := time.Now()
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
// Kill by group ID to make sure child processes are killed. The - tells `kill` that it's a group ID.
|
||||
// Since we didn't set Pgid in SysProcAttr, the group ID is the same as the process ID. https://pkg.go.dev/syscall#SysProcAttr
|
||||
_ = sysCallKill(-cmd.Process.Pid)
|
||||
}()
|
||||
|
||||
err = cmd.Wait()
|
||||
|
||||
duration := time.Since(start)
|
||||
output := stdout.String()
|
||||
|
||||
logCtx.WithFields(log.Fields{"duration": duration}).Debug(output)
|
||||
|
||||
if err != nil {
|
||||
err := newCmdError(args, errors.New(err.Error()), strings.TrimSpace(stderr.String()))
|
||||
logCtx.Error(err.Error())
|
||||
return strings.TrimSuffix(output, "\n"), err
|
||||
}
|
||||
|
||||
return strings.TrimSuffix(output, "\n"), nil
|
||||
}
|
||||
|
||||
type CmdError struct {
|
||||
Args string
|
||||
Stderr string
|
||||
Cause error
|
||||
}
|
||||
|
||||
func (ce *CmdError) Error() string {
|
||||
res := fmt.Sprintf("`%v` failed %v", ce.Args, ce.Cause)
|
||||
if ce.Stderr != "" {
|
||||
res = fmt.Sprintf("%s: %s", res, ce.Stderr)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func newCmdError(args string, cause error, stderr string) *CmdError {
|
||||
return &CmdError{Args: args, Stderr: stderr, Cause: cause}
|
||||
}
|
||||
|
||||
// Environ returns a list of environment variables in name=value format from a list of variables
|
||||
@@ -55,17 +129,26 @@ func environ(envVars []*apiclient.EnvEntry) []string {
|
||||
|
||||
// GenerateManifest runs generate command from plugin config file and returns generated manifest files
|
||||
func (s *Service) GenerateManifest(ctx context.Context, q *apiclient.ManifestRequest) (*apiclient.ManifestResponse, error) {
|
||||
bufferedCtx, cancel := buffered_context.WithEarlierDeadline(ctx, cmpTimeoutBuffer)
|
||||
defer cancel()
|
||||
|
||||
if deadline, ok := bufferedCtx.Deadline(); ok {
|
||||
log.Infof("Generating manifests with deadline %v from now", time.Until(deadline))
|
||||
} else {
|
||||
log.Info("Generating manifests with no request-level timeout")
|
||||
}
|
||||
|
||||
config := s.initConstants.PluginConfig
|
||||
|
||||
env := append(os.Environ(), environ(q.Env)...)
|
||||
if len(config.Spec.Init.Command) > 0 {
|
||||
_, err := runCommand(config.Spec.Init, q.AppPath, env)
|
||||
_, err := runCommand(bufferedCtx, config.Spec.Init, q.AppPath, env)
|
||||
if err != nil {
|
||||
return &apiclient.ManifestResponse{}, err
|
||||
}
|
||||
}
|
||||
|
||||
out, err := runCommand(config.Spec.Generate, q.AppPath, env)
|
||||
out, err := runCommand(bufferedCtx, config.Spec.Generate, q.AppPath, env)
|
||||
if err != nil {
|
||||
return &apiclient.ManifestResponse{}, err
|
||||
}
|
||||
@@ -82,6 +165,9 @@ func (s *Service) GenerateManifest(ctx context.Context, q *apiclient.ManifestReq
|
||||
|
||||
// MatchRepository checks whether the application repository type is supported by config management plugin server
|
||||
func (s *Service) MatchRepository(ctx context.Context, q *apiclient.RepositoryRequest) (*apiclient.RepositoryResponse, error) {
|
||||
bufferedCtx, cancel := buffered_context.WithEarlierDeadline(ctx, cmpTimeoutBuffer)
|
||||
defer cancel()
|
||||
|
||||
var repoResponse apiclient.RepositoryResponse
|
||||
config := s.initConstants.PluginConfig
|
||||
if config.Spec.Discover.FileName != "" {
|
||||
@@ -113,7 +199,7 @@ func (s *Service) MatchRepository(ctx context.Context, q *apiclient.RepositoryRe
|
||||
}
|
||||
|
||||
log.Debugf("Going to try runCommand.")
|
||||
find, err := runCommand(config.Spec.Discover.Find.Command, q.Path, os.Environ())
|
||||
find, err := runCommand(bufferedCtx, config.Spec.Discover.Find.Command, q.Path, os.Environ())
|
||||
if err != nil {
|
||||
return &repoResponse, err
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/cmpserver/apiclient"
|
||||
@@ -63,3 +65,19 @@ func TestGenerateManifest(t *testing.T) {
|
||||
require.Equal(t, expectedOutput, res1.Manifests[0])
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunCommandContextTimeout makes sure the command dies at timeout rather than sleeping past the timeout.
|
||||
func TestRunCommandContextTimeout(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 990*time.Millisecond)
|
||||
defer cancel()
|
||||
// Use a subshell so there's a child command.
|
||||
command := Command{
|
||||
Command: []string{"sh", "-c"},
|
||||
Args: []string{"sleep 5"},
|
||||
}
|
||||
before := time.Now()
|
||||
_, err := runCommand(ctx, command, "", []string{})
|
||||
after := time.Now()
|
||||
assert.Error(t, err) // The command should time out, causing an error.
|
||||
assert.Less(t, after.Sub(before), 1*time.Second)
|
||||
}
|
||||
|
||||
16
cmpserver/plugin/plugin_unix.go
Normal file
16
cmpserver/plugin/plugin_unix.go
Normal file
@@ -0,0 +1,16 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func newSysProcAttr(setpgid bool) *syscall.SysProcAttr {
|
||||
return &syscall.SysProcAttr{Setpgid: setpgid}
|
||||
}
|
||||
|
||||
func sysCallKill(pid int) error {
|
||||
return syscall.Kill(pid, syscall.SIGKILL)
|
||||
}
|
||||
16
cmpserver/plugin/plugin_windows.go
Normal file
16
cmpserver/plugin/plugin_windows.go
Normal file
@@ -0,0 +1,16 @@
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func newSysProcAttr(setpgid bool) *syscall.SysProcAttr {
|
||||
return &syscall.SysProcAttr{}
|
||||
}
|
||||
|
||||
func sysCallKill(pid int) error {
|
||||
return nil
|
||||
}
|
||||
@@ -42,6 +42,11 @@ const (
|
||||
DefaultPortRepoServerMetrics = 8084
|
||||
)
|
||||
|
||||
// Default listener address for ArgoCD components
|
||||
const (
|
||||
DefaultAddressAPIServer = "localhost"
|
||||
)
|
||||
|
||||
// Default paths on the pod's file system
|
||||
const (
|
||||
// The default path where TLS certificates for repositories are located
|
||||
@@ -71,6 +76,10 @@ const (
|
||||
ArgoCDUserAgentName = "argocd-client"
|
||||
// AuthCookieName is the HTTP cookie name where we store our auth token
|
||||
AuthCookieName = "argocd.token"
|
||||
// StateCookieName is the HTTP cookie name that holds temporary nonce tokens for CSRF protection
|
||||
StateCookieName = "argocd.oauthstate"
|
||||
// StateCookieMaxAge is the maximum age of the oauth state cookie
|
||||
StateCookieMaxAge = time.Minute * 5
|
||||
|
||||
// ChangePasswordSSOTokenMaxAge is the max token age for password change operation
|
||||
ChangePasswordSSOTokenMaxAge = time.Minute * 5
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
apiruntime "k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
@@ -36,9 +37,6 @@ import (
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
|
||||
// make sure to register workqueue prometheus metrics
|
||||
_ "k8s.io/component-base/metrics/prometheus/workqueue"
|
||||
|
||||
statecache "github.com/argoproj/argo-cd/v2/controller/cache"
|
||||
"github.com/argoproj/argo-cd/v2/controller/metrics"
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application"
|
||||
@@ -48,6 +46,7 @@ import (
|
||||
applisters "github.com/argoproj/argo-cd/v2/pkg/client/listers/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/v2/util/argo"
|
||||
argodiff "github.com/argoproj/argo-cd/v2/util/argo/diff"
|
||||
appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate"
|
||||
"github.com/argoproj/argo-cd/v2/util/db"
|
||||
"github.com/argoproj/argo-cd/v2/util/errors"
|
||||
@@ -166,19 +165,26 @@ func NewApplicationController(
|
||||
AddFunc: func(obj interface{}) {
|
||||
if key, err := cache.MetaNamespaceKeyFunc(obj); err == nil {
|
||||
ctrl.projectRefreshQueue.Add(key)
|
||||
ctrl.InvalidateProjectsCache()
|
||||
if projMeta, ok := obj.(metav1.Object); ok {
|
||||
ctrl.InvalidateProjectsCache(projMeta.GetName())
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
UpdateFunc: func(old, new interface{}) {
|
||||
if key, err := cache.MetaNamespaceKeyFunc(new); err == nil {
|
||||
ctrl.projectRefreshQueue.Add(key)
|
||||
ctrl.InvalidateProjectsCache()
|
||||
if projMeta, ok := new.(metav1.Object); ok {
|
||||
ctrl.InvalidateProjectsCache(projMeta.GetName())
|
||||
}
|
||||
}
|
||||
},
|
||||
DeleteFunc: func(obj interface{}) {
|
||||
if key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj); err == nil {
|
||||
ctrl.projectRefreshQueue.Add(key)
|
||||
ctrl.InvalidateProjectsCache()
|
||||
if projMeta, ok := obj.(metav1.Object); ok {
|
||||
ctrl.InvalidateProjectsCache(projMeta.GetName())
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -207,11 +213,17 @@ func NewApplicationController(
|
||||
return &ctrl, nil
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) InvalidateProjectsCache() {
|
||||
ctrl.projByNameCache.Range(func(key, _ interface{}) bool {
|
||||
ctrl.projByNameCache.Delete(key)
|
||||
return true
|
||||
})
|
||||
func (ctrl *ApplicationController) InvalidateProjectsCache(names ...string) {
|
||||
if len(names) > 0 {
|
||||
for _, name := range names {
|
||||
ctrl.projByNameCache.Delete(name)
|
||||
}
|
||||
} else {
|
||||
ctrl.projByNameCache.Range(func(key, _ interface{}) bool {
|
||||
ctrl.projByNameCache.Delete(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) GetMetricsServer() *metrics.MetricsServer {
|
||||
@@ -284,12 +296,8 @@ func (ctrl *ApplicationController) handleObjectUpdated(managedByApp map[string]b
|
||||
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), proj) {
|
||||
|
||||
managedByApp[app.Name] = false
|
||||
}
|
||||
managedByApp[app.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,12 +317,27 @@ func (ctrl *ApplicationController) handleObjectUpdated(managedByApp map[string]b
|
||||
if isManagedResource {
|
||||
level = CompareWithRecent
|
||||
}
|
||||
|
||||
// Additional check for debug level so we don't need to evaluate the
|
||||
// format string in case of non-debug scenarios
|
||||
if log.GetLevel() >= log.DebugLevel {
|
||||
var resKey string
|
||||
if ref.Namespace != "" {
|
||||
resKey = ref.Namespace + "/" + ref.Name
|
||||
} else {
|
||||
resKey = "(cluster-scoped)/" + ref.Name
|
||||
}
|
||||
log.Debugf("Refreshing app %s for change in cluster of object %s of type %s/%s", appName, resKey, ref.APIVersion, ref.Kind)
|
||||
}
|
||||
|
||||
ctrl.requestAppRefresh(appName, &level, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// setAppManagedResources will build a list of ResourceDiff based on the provided comparisonResult
|
||||
// and persist app resources related data in the cache. Will return the persisted ApplicationTree.
|
||||
func (ctrl *ApplicationController) setAppManagedResources(a *appv1.Application, comparisonResult *comparisonResult) (*appv1.ApplicationTree, error) {
|
||||
managedResources, err := ctrl.managedResources(comparisonResult)
|
||||
managedResources, err := ctrl.hideSecretData(a, comparisonResult)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting managed resources: %s", err)
|
||||
}
|
||||
@@ -360,7 +383,7 @@ func isKnownOrphanedResourceExclusion(key kube.ResourceKey, proj *appv1.AppProje
|
||||
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, ctrl.settingsMgr, ctrl.db, context.TODO())
|
||||
proj, err := ctrl.getAppProj(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -399,8 +422,12 @@ func (ctrl *ApplicationController) getResourceTree(a *appv1.Application, managed
|
||||
},
|
||||
})
|
||||
} else {
|
||||
err := ctrl.stateCache.IterateHierarchy(a.Spec.Destination.Server, kube.GetResourceKey(live), func(child appv1.ResourceNode, appName string) {
|
||||
err := ctrl.stateCache.IterateHierarchy(a.Spec.Destination.Server, kube.GetResourceKey(live), func(child appv1.ResourceNode, appName string) bool {
|
||||
if !proj.IsResourcePermitted(schema.GroupKind{Group: child.ResourceRef.Group, Kind: child.ResourceRef.Kind}, child.Namespace, a.Spec.Destination) {
|
||||
return false
|
||||
}
|
||||
nodes = append(nodes, child)
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -410,16 +437,18 @@ func (ctrl *ApplicationController) getResourceTree(a *appv1.Application, managed
|
||||
orphanedNodes := make([]appv1.ResourceNode, 0)
|
||||
for k := range orphanedNodesMap {
|
||||
if k.Namespace != "" && proj.IsGroupKindPermitted(k.GroupKind(), true) && !isKnownOrphanedResourceExclusion(k, proj) {
|
||||
err := ctrl.stateCache.IterateHierarchy(a.Spec.Destination.Server, k, func(child appv1.ResourceNode, appName string) {
|
||||
err := ctrl.stateCache.IterateHierarchy(a.Spec.Destination.Server, k, func(child appv1.ResourceNode, appName string) bool {
|
||||
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 belongToAnotherApp || !proj.IsResourcePermitted(schema.GroupKind{Group: child.ResourceRef.Group, Kind: child.ResourceRef.Kind}, child.Namespace, a.Spec.Destination) {
|
||||
return false
|
||||
}
|
||||
orphanedNodes = append(orphanedNodes, child)
|
||||
return true
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -534,7 +563,7 @@ func (ctrl *ApplicationController) getAppHosts(a *appv1.Application, appNodes []
|
||||
return hosts, nil
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) managedResources(comparisonResult *comparisonResult) ([]*appv1.ResourceDiff, error) {
|
||||
func (ctrl *ApplicationController) hideSecretData(app *appv1.Application, comparisonResult *comparisonResult) ([]*appv1.ResourceDiff, error) {
|
||||
items := make([]*appv1.ResourceDiff, len(comparisonResult.managedResources))
|
||||
for i := range comparisonResult.managedResources {
|
||||
res := comparisonResult.managedResources[i]
|
||||
@@ -560,14 +589,34 @@ func (ctrl *ApplicationController) managedResources(comparisonResult *comparison
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting resource compare options: %s", err)
|
||||
}
|
||||
resDiffPtr, err := diff.Diff(target, live,
|
||||
diff.WithNormalizer(comparisonResult.diffNormalizer),
|
||||
diff.WithLogr(logutils.NewLogrusLogger(logutils.NewWithCurrentConfig())),
|
||||
diff.IgnoreAggregatedRoles(compareOptions.IgnoreAggregatedRoles))
|
||||
resourceOverrides, err := ctrl.settingsMgr.GetResourceOverrides()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting resource overrides: %s", err)
|
||||
}
|
||||
appLabelKey, err := ctrl.settingsMgr.GetAppInstanceLabelKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting app instance label key: %s", err)
|
||||
}
|
||||
trackingMethod, err := ctrl.settingsMgr.GetTrackingMethod()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting tracking method: %s", err)
|
||||
}
|
||||
|
||||
diffConfig, err := argodiff.NewDiffConfigBuilder().
|
||||
WithDiffSettings(app.Spec.IgnoreDifferences, resourceOverrides, compareOptions.IgnoreAggregatedRoles).
|
||||
WithTracking(appLabelKey, trackingMethod).
|
||||
WithNoCache().
|
||||
WithLogger(logutils.NewLogrusLogger(logutils.NewWithCurrentConfig())).
|
||||
Build()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("appcontroller error building diff config: %s", err)
|
||||
}
|
||||
|
||||
diffResult, err := argodiff.StateDiff(live, target, diffConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error applying diff: %s", err)
|
||||
}
|
||||
resDiff = *resDiffPtr
|
||||
resDiff = diffResult
|
||||
}
|
||||
|
||||
if live != nil {
|
||||
@@ -1093,7 +1142,7 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli
|
||||
}
|
||||
|
||||
ctrl.setOperationState(app, state)
|
||||
if state.Phase.Completed() && !app.Operation.Sync.DryRun {
|
||||
if state.Phase.Completed() && (app.Operation.Sync != nil && !app.Operation.Sync.DryRun) {
|
||||
// if we just completed an operation, force a refresh so that UI will report up-to-date
|
||||
// sync/health information
|
||||
if _, err := cache.MetaNamespaceKeyFunc(app); err == nil {
|
||||
@@ -1249,6 +1298,13 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
app.Status.Sync.Status = appv1.SyncStatusCodeUnknown
|
||||
app.Status.Health.Status = health.HealthStatusUnknown
|
||||
ctrl.persistAppStatus(origApp, &app.Status)
|
||||
|
||||
if err := ctrl.cache.SetAppResourcesTree(app.Name, &appv1.ApplicationTree{}); err != nil {
|
||||
log.Warnf("failed to set app resource tree: %v", err)
|
||||
}
|
||||
if err := ctrl.cache.SetAppManagedResources(app.Name, nil); err != nil {
|
||||
log.Warnf("failed to set app managed resources tree: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1638,7 +1694,7 @@ func (ctrl *ApplicationController) newApplicationInformerAndLister() (cache.Shar
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
proj, err := ctrl.getAppProj(app)
|
||||
proj, err := applisters.NewAppProjectLister(ctrl.projInformer.GetIndexer()).AppProjects(ctrl.namespace).Get(app.Spec.GetProject())
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -136,12 +136,12 @@ func newFakeController(data *fakeData) *ApplicationController {
|
||||
mockStateCache.On("GetClusterCache", mock.Anything).Return(&clusterCacheMock, nil)
|
||||
mockStateCache.On("IterateHierarchy", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
|
||||
key := args[1].(kube.ResourceKey)
|
||||
action := args[2].(func(child argoappv1.ResourceNode, appName string))
|
||||
action := args[2].(func(child argoappv1.ResourceNode, appName string) bool)
|
||||
appName := ""
|
||||
if res, ok := data.namespacedResources[key]; ok {
|
||||
appName = res.AppName
|
||||
}
|
||||
action(argoappv1.ResourceNode{ResourceRef: argoappv1.ResourceRef{Kind: key.Kind, Group: key.Group, Namespace: key.Namespace, Name: key.Name}}, appName)
|
||||
_ = action(argoappv1.ResourceNode{ResourceRef: argoappv1.ResourceRef{Kind: key.Kind, Group: key.Group, Namespace: key.Namespace, Name: key.Name}}, appName)
|
||||
}).Return(nil)
|
||||
return ctrl
|
||||
}
|
||||
@@ -785,11 +785,11 @@ func TestHandleOrphanedResourceUpdated(t *testing.T) {
|
||||
|
||||
isRequested, level := ctrl.isRefreshRequested(app1.Name)
|
||||
assert.True(t, isRequested)
|
||||
assert.Equal(t, ComparisonWithNothing, level)
|
||||
assert.Equal(t, CompareWithRecent, level)
|
||||
|
||||
isRequested, level = ctrl.isRefreshRequested(app2.Name)
|
||||
assert.True(t, isRequested)
|
||||
assert.Equal(t, ComparisonWithNothing, level)
|
||||
assert.Equal(t, CompareWithRecent, level)
|
||||
}
|
||||
|
||||
func TestGetResourceTree_HasOrphanedResources(t *testing.T) {
|
||||
|
||||
121
controller/cache/cache.go
vendored
121
controller/cache/cache.go
vendored
@@ -2,10 +2,12 @@ package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"math"
|
||||
"reflect"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
clustercache "github.com/argoproj/gitops-engine/pkg/cache"
|
||||
@@ -14,6 +16,7 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/sync/semaphore"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
kerrors "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"
|
||||
@@ -24,6 +27,7 @@ import (
|
||||
appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v2/util/argo"
|
||||
"github.com/argoproj/argo-cd/v2/util/db"
|
||||
"github.com/argoproj/argo-cd/v2/util/env"
|
||||
logutils "github.com/argoproj/argo-cd/v2/util/log"
|
||||
"github.com/argoproj/argo-cd/v2/util/lua"
|
||||
"github.com/argoproj/argo-cd/v2/util/settings"
|
||||
@@ -32,20 +36,64 @@ import (
|
||||
const (
|
||||
// EnvClusterCacheResyncDuration is the env variable that holds cluster cache re-sync duration
|
||||
EnvClusterCacheResyncDuration = "ARGOCD_CLUSTER_CACHE_RESYNC_DURATION"
|
||||
|
||||
// EnvClusterCacheWatchResyncDuration is the env variable that holds cluster cache watch re-sync duration
|
||||
EnvClusterCacheWatchResyncDuration = "ARGOCD_CLUSTER_CACHE_WATCH_RESYNC_DURATION"
|
||||
|
||||
// EnvClusterRetryTimeoutDuration is the env variable that holds cluster retry duration when sync error happens
|
||||
EnvClusterSyncRetryTimeoutDuration = "ARGOCD_CLUSTER_SYNC_RETRY_TIMEOUT_DURATION"
|
||||
|
||||
// EnvClusterCacheListPageSize is the env variable to control size of the list page size when making K8s queries
|
||||
EnvClusterCacheListPageSize = "ARGOCD_CLUSTER_CACHE_LIST_PAGE_SIZE"
|
||||
|
||||
// EnvClusterCacheListSemaphore is the env variable to control size of the list semaphore
|
||||
// This is used to limit the number of concurrent memory consuming operations on the
|
||||
// k8s list queries results across all clusters to avoid memory spikes during cache initialization.
|
||||
EnvClusterCacheListSemaphore = "ARGOCD_CLUSTER_CACHE_LIST_SEMAPHORE"
|
||||
|
||||
// EnvClusterCacheRetryLimit is the env variable to control the retry limit for listing resources during cluster cache sync
|
||||
EnvClusterCacheAttemptLimit = "ARGOCD_CLUSTER_CACHE_ATTEMPT_LIMIT"
|
||||
|
||||
// EnvClusterCacheRetryUseBackoff is the env variable to control whether to use a backoff strategy with the retry during cluster cache sync
|
||||
EnvClusterCacheRetryUseBackoff = "ARGOCD_CLUSTER_CACHE_RETRY_USE_BACKOFF"
|
||||
)
|
||||
|
||||
// GitOps engine cluster cache tuning options
|
||||
var (
|
||||
// K8SClusterResyncDuration controls the duration of cluster cache refresh
|
||||
K8SClusterResyncDuration = 12 * time.Hour
|
||||
// clusterCacheResyncDuration controls the duration of cluster cache refresh.
|
||||
// NOTE: this differs from gitops-engine default of 24h
|
||||
clusterCacheResyncDuration = 12 * time.Hour
|
||||
|
||||
// clusterCacheWatchResyncDuration controls the maximum duration that group/kind watches are allowed to run
|
||||
// for before relisting & restarting the watch
|
||||
clusterCacheWatchResyncDuration = 10 * time.Minute
|
||||
|
||||
// clusterSyncRetryTimeoutDuration controls the sync retry duration when cluster sync error happens
|
||||
clusterSyncRetryTimeoutDuration = 10 * time.Second
|
||||
|
||||
// The default limit of 50 is chosen based on experiments.
|
||||
clusterCacheListSemaphoreSize int64 = 50
|
||||
|
||||
// clusterCacheListPageSize is the page size when performing K8s list requests.
|
||||
// 500 is equal to kubectl's size
|
||||
clusterCacheListPageSize int64 = 500
|
||||
|
||||
// clusterCacheRetryLimit sets a retry limit for failed requests during cluster cache sync
|
||||
// If set to 1, retries are disabled.
|
||||
clusterCacheAttemptLimit int32 = 1
|
||||
|
||||
// clusterCacheRetryUseBackoff specifies whether to use a backoff strategy on cluster cache sync, if retry is enabled
|
||||
clusterCacheRetryUseBackoff bool = false
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
if clusterResyncDurationStr := os.Getenv(EnvClusterCacheResyncDuration); clusterResyncDurationStr != "" {
|
||||
if duration, err := time.ParseDuration(clusterResyncDurationStr); err == nil {
|
||||
K8SClusterResyncDuration = duration
|
||||
}
|
||||
}
|
||||
clusterCacheResyncDuration = env.ParseDurationFromEnv(EnvClusterCacheResyncDuration, clusterCacheResyncDuration, 0, math.MaxInt64)
|
||||
clusterCacheWatchResyncDuration = env.ParseDurationFromEnv(EnvClusterCacheWatchResyncDuration, clusterCacheWatchResyncDuration, 0, math.MaxInt64)
|
||||
clusterSyncRetryTimeoutDuration = env.ParseDurationFromEnv(EnvClusterSyncRetryTimeoutDuration, clusterSyncRetryTimeoutDuration, 0, math.MaxInt64)
|
||||
clusterCacheListPageSize = env.ParseInt64FromEnv(EnvClusterCacheListPageSize, clusterCacheListPageSize, 0, math.MaxInt64)
|
||||
clusterCacheListSemaphoreSize = env.ParseInt64FromEnv(EnvClusterCacheListSemaphore, clusterCacheListSemaphoreSize, 0, math.MaxInt64)
|
||||
clusterCacheAttemptLimit = int32(env.ParseInt64FromEnv(EnvClusterCacheAttemptLimit, 1, 1, math.MaxInt32))
|
||||
clusterCacheRetryUseBackoff = env.ParseBoolFromEnv(EnvClusterCacheRetryUseBackoff, false)
|
||||
}
|
||||
|
||||
type LiveStateCache interface {
|
||||
@@ -56,7 +104,7 @@ type LiveStateCache interface {
|
||||
// Returns synced cluster cache
|
||||
GetClusterCache(server string) (clustercache.ClusterCache, error)
|
||||
// Executes give callback against resource specified by the key and all its children
|
||||
IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string)) error
|
||||
IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string) bool) 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)
|
||||
// IterateResources iterates all resource stored in cache
|
||||
@@ -109,15 +157,13 @@ func NewLiveStateCache(
|
||||
resourceTracking argo.ResourceTracking) LiveStateCache {
|
||||
|
||||
return &liveStateCache{
|
||||
appInformer: appInformer,
|
||||
db: db,
|
||||
clusters: make(map[string]clustercache.ClusterCache),
|
||||
onObjectUpdated: onObjectUpdated,
|
||||
kubectl: kubectl,
|
||||
settingsMgr: settingsMgr,
|
||||
metricsServer: metricsServer,
|
||||
// The default limit of 50 is chosen based on experiments.
|
||||
listSemaphore: semaphore.NewWeighted(50),
|
||||
appInformer: appInformer,
|
||||
db: db,
|
||||
clusters: make(map[string]clustercache.ClusterCache),
|
||||
onObjectUpdated: onObjectUpdated,
|
||||
kubectl: kubectl,
|
||||
settingsMgr: settingsMgr,
|
||||
metricsServer: metricsServer,
|
||||
clusterFilter: clusterFilter,
|
||||
resourceTracking: resourceTracking,
|
||||
}
|
||||
@@ -138,10 +184,6 @@ type liveStateCache struct {
|
||||
clusterFilter func(cluster *appv1.Cluster) bool
|
||||
resourceTracking argo.ResourceTracking
|
||||
|
||||
// listSemaphore is used to limit the number of concurrent memory consuming operations on the
|
||||
// k8s list queries results across all clusters to avoid memory spikes during cache initialization.
|
||||
listSemaphore *semaphore.Weighted
|
||||
|
||||
clusters map[string]clustercache.ClusterCache
|
||||
cacheSettings cacheSettings
|
||||
lock sync.RWMutex
|
||||
@@ -261,6 +303,19 @@ func skipAppRequeuing(key kube.ResourceKey) bool {
|
||||
return ignoredRefreshResources[key.Group+"/"+key.Kind]
|
||||
}
|
||||
|
||||
// isRetryableError is a helper method to see whether an error
|
||||
// returned from the dynamic client is potentially retryable.
|
||||
func isRetryableError(err error) bool {
|
||||
return kerrors.IsInternalError(err) ||
|
||||
kerrors.IsInvalid(err) ||
|
||||
kerrors.IsServerTimeout(err) ||
|
||||
kerrors.IsServiceUnavailable(err) ||
|
||||
kerrors.IsTimeout(err) ||
|
||||
kerrors.IsUnexpectedObjectError(err) ||
|
||||
kerrors.IsUnexpectedServerError(err) ||
|
||||
errors.Is(err, syscall.ECONNRESET)
|
||||
}
|
||||
|
||||
func (c *liveStateCache) getCluster(server string) (clustercache.ClusterCache, error) {
|
||||
c.lock.RLock()
|
||||
clusterCache, ok := c.clusters[server]
|
||||
@@ -289,9 +344,12 @@ func (c *liveStateCache) getCluster(server string) (clustercache.ClusterCache, e
|
||||
}
|
||||
|
||||
trackingMethod := argo.GetTrackingMethod(c.settingsMgr)
|
||||
clusterCache = clustercache.NewClusterCache(cluster.RESTConfig(),
|
||||
clustercache.SetListSemaphore(c.listSemaphore),
|
||||
clustercache.SetResyncTimeout(K8SClusterResyncDuration),
|
||||
clusterCacheOpts := []clustercache.UpdateSettingsFunc{
|
||||
clustercache.SetListSemaphore(semaphore.NewWeighted(clusterCacheListSemaphoreSize)),
|
||||
clustercache.SetListPageSize(clusterCacheListPageSize),
|
||||
clustercache.SetWatchResyncTimeout(clusterCacheWatchResyncDuration),
|
||||
clustercache.SetClusterSyncRetryTimeout(clusterSyncRetryTimeoutDuration),
|
||||
clustercache.SetResyncTimeout(clusterCacheResyncDuration),
|
||||
clustercache.SetSettings(cacheSettings.clusterSettings),
|
||||
clustercache.SetNamespaces(cluster.Namespaces),
|
||||
clustercache.SetClusterResources(cluster.ClusterResources),
|
||||
@@ -311,7 +369,10 @@ func (c *liveStateCache) getCluster(server string) (clustercache.ClusterCache, e
|
||||
return res, res.AppName != "" || gvk.Kind == kube.CustomResourceDefinitionKind
|
||||
}),
|
||||
clustercache.SetLogr(logutils.NewLogrusLogger(log.WithField("server", cluster.Server))),
|
||||
)
|
||||
clustercache.SetRetryOptions(clusterCacheAttemptLimit, clusterCacheRetryUseBackoff, isRetryableError),
|
||||
}
|
||||
|
||||
clusterCache = clustercache.NewClusterCache(cluster.RESTConfig(), clusterCacheOpts...)
|
||||
|
||||
_ = clusterCache.OnResourceUpdated(func(newRes *clustercache.Resource, oldRes *clustercache.Resource, namespaceResources map[kube.ResourceKey]*clustercache.Resource) {
|
||||
toNotify := make(map[string]bool)
|
||||
@@ -376,13 +437,13 @@ func (c *liveStateCache) IsNamespaced(server string, gk schema.GroupKind) (bool,
|
||||
return clusterInfo.IsNamespaced(gk)
|
||||
}
|
||||
|
||||
func (c *liveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string)) error {
|
||||
func (c *liveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string) bool) error {
|
||||
clusterInfo, err := c.getSyncedCluster(server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clusterInfo.IterateHierarchy(key, func(resource *clustercache.Resource, namespaceResources map[kube.ResourceKey]*clustercache.Resource) {
|
||||
action(asResourceNode(resource), getApp(resource, namespaceResources))
|
||||
clusterInfo.IterateHierarchy(key, func(resource *clustercache.Resource, namespaceResources map[kube.ResourceKey]*clustercache.Resource) bool {
|
||||
return action(asResourceNode(resource), getApp(resource, namespaceResources))
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
5
controller/cache/info.go
vendored
5
controller/cache/info.go
vendored
@@ -30,25 +30,20 @@ func populateNodeInfo(un *unstructured.Unstructured, res *ResourceInfo) {
|
||||
switch gvk.Kind {
|
||||
case kube.PodKind:
|
||||
populatePodInfo(un, res)
|
||||
return
|
||||
case kube.ServiceKind:
|
||||
populateServiceInfo(un, res)
|
||||
return
|
||||
case "Node":
|
||||
populateHostNodeInfo(un, res)
|
||||
return
|
||||
}
|
||||
case "extensions", "networking.k8s.io":
|
||||
switch gvk.Kind {
|
||||
case kube.IngressKind:
|
||||
populateIngressInfo(un, res)
|
||||
return
|
||||
}
|
||||
case "networking.istio.io":
|
||||
switch gvk.Kind {
|
||||
case "VirtualService":
|
||||
populateIstioVirtualServiceInfo(un, res)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
87
controller/cache/info_test.go
vendored
87
controller/cache/info_test.go
vendored
@@ -43,6 +43,25 @@ var (
|
||||
ingress:
|
||||
- hostname: localhost`)
|
||||
|
||||
testLinkAnnotatedService = strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
resourceVersion: "123"
|
||||
uid: "4"
|
||||
annotations:
|
||||
link.argocd.argoproj.io/external-link: http://my-grafana.com/pre-generated-link
|
||||
spec:
|
||||
selector:
|
||||
app: guestbook
|
||||
type: LoadBalancer
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
- hostname: localhost`)
|
||||
|
||||
testIngress = strToUnstructured(`
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
@@ -74,6 +93,39 @@ var (
|
||||
ingress:
|
||||
- ip: 107.178.210.11`)
|
||||
|
||||
testLinkAnnotatedIngress = strToUnstructured(`
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
uid: "4"
|
||||
annotations:
|
||||
link.argocd.argoproj.io/external-link: http://my-grafana.com/ingress-link
|
||||
spec:
|
||||
backend:
|
||||
serviceName: not-found-service
|
||||
servicePort: 443
|
||||
rules:
|
||||
- host: helm-guestbook.com
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: helm-guestbook
|
||||
servicePort: 443
|
||||
path: /
|
||||
- backend:
|
||||
serviceName: helm-guestbook
|
||||
servicePort: https
|
||||
path: /
|
||||
tls:
|
||||
- host: helm-guestbook.com
|
||||
secretName: my-tls-secret
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
- ip: 107.178.210.11`)
|
||||
|
||||
testIngressWildCardPath = strToUnstructured(`
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
@@ -268,6 +320,17 @@ func TestGetServiceInfo(t *testing.T) {
|
||||
}, info.NetworkingInfo)
|
||||
}
|
||||
|
||||
func TestGetLinkAnnotatedServiceInfo(t *testing.T) {
|
||||
info := &ResourceInfo{}
|
||||
populateNodeInfo(testLinkAnnotatedService, info)
|
||||
assert.Equal(t, 0, len(info.Info))
|
||||
assert.Equal(t, &v1alpha1.ResourceNetworkingInfo{
|
||||
TargetLabels: map[string]string{"app": "guestbook"},
|
||||
Ingress: []v1.LoadBalancerIngress{{Hostname: "localhost"}},
|
||||
ExternalURLs: []string{"http://my-grafana.com/pre-generated-link"},
|
||||
}, info.NetworkingInfo)
|
||||
}
|
||||
|
||||
func TestGetIstioVirtualServiceInfo(t *testing.T) {
|
||||
info := &ResourceInfo{}
|
||||
populateNodeInfo(testIstioVirtualService, info)
|
||||
@@ -323,6 +386,30 @@ func TestGetIngressInfo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLinkAnnotatedIngressInfo(t *testing.T) {
|
||||
info := &ResourceInfo{}
|
||||
populateNodeInfo(testLinkAnnotatedIngress, info)
|
||||
assert.Equal(t, 0, len(info.Info))
|
||||
sort.Slice(info.NetworkingInfo.TargetRefs, func(i, j int) bool {
|
||||
return strings.Compare(info.NetworkingInfo.TargetRefs[j].Name, info.NetworkingInfo.TargetRefs[i].Name) < 0
|
||||
})
|
||||
assert.Equal(t, &v1alpha1.ResourceNetworkingInfo{
|
||||
Ingress: []v1.LoadBalancerIngress{{IP: "107.178.210.11"}},
|
||||
TargetRefs: []v1alpha1.ResourceRef{{
|
||||
Namespace: "default",
|
||||
Group: "",
|
||||
Kind: kube.ServiceKind,
|
||||
Name: "not-found-service",
|
||||
}, {
|
||||
Namespace: "default",
|
||||
Group: "",
|
||||
Kind: kube.ServiceKind,
|
||||
Name: "helm-guestbook",
|
||||
}},
|
||||
ExternalURLs: []string{"https://helm-guestbook.com/", "http://my-grafana.com/ingress-link"},
|
||||
}, info.NetworkingInfo)
|
||||
}
|
||||
|
||||
func TestGetIngressInfoWildCardPath(t *testing.T) {
|
||||
info := &ResourceInfo{}
|
||||
populateNodeInfo(testIngressWildCardPath, info)
|
||||
|
||||
4
controller/cache/mocks/LiveStateCache.go
vendored
4
controller/cache/mocks/LiveStateCache.go
vendored
@@ -176,11 +176,11 @@ func (_m *LiveStateCache) IsNamespaced(server string, gk schema.GroupKind) (bool
|
||||
}
|
||||
|
||||
// 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 {
|
||||
func (_m *LiveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(v1alpha1.ResourceNode, string) bool) error {
|
||||
ret := _m.Called(server, key, action)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, kube.ResourceKey, func(v1alpha1.ResourceNode, string)) error); ok {
|
||||
if rf, ok := ret.Get(0).(func(string, kube.ResourceKey, func(v1alpha1.ResourceNode, string) bool) error); ok {
|
||||
r0 = rf(server, key, action)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
|
||||
@@ -64,6 +64,7 @@ func (c *clusterInfoUpdater) updateClusters() {
|
||||
clusters, err := c.db.ListClusters(context.Background())
|
||||
if err != nil {
|
||||
log.Warnf("Failed to save clusters info: %v", err)
|
||||
return
|
||||
}
|
||||
var clustersFiltered []appv1.Cluster
|
||||
if c.clusterFilter == nil {
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/health"
|
||||
@@ -159,6 +159,7 @@ func NewMetricsServer(addr string, appLister applister.ApplicationLister, appFil
|
||||
|
||||
mux := http.NewServeMux()
|
||||
registry := NewAppRegistry(appLister, appFilter, appLabels)
|
||||
registry.MustRegister(depth, adds, latency, workDuration, unfinished, longestRunningProcessor, retries)
|
||||
mux.Handle(MetricsPath, promhttp.HandlerFor(prometheus.Gatherers{
|
||||
// contains app controller specific metrics
|
||||
registry,
|
||||
@@ -196,11 +197,14 @@ func NewMetricsServer(addr string, appLister applister.ApplicationLister, appFil
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Prometheus invalid labels, more info: https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels.
|
||||
var invalidPromLabelChars = regexp.MustCompile(`[^a-zA-Z0-9_]`)
|
||||
|
||||
func normalizeLabels(prefix string, appLabels []string) []string {
|
||||
results := []string{}
|
||||
for _, label := range appLabels {
|
||||
//prometheus labels don't accept dash in their name
|
||||
curr := strings.ReplaceAll(label, "-", "_")
|
||||
curr := invalidPromLabelChars.ReplaceAllString(label, "_")
|
||||
result := fmt.Sprintf("%s_%s", prefix, curr)
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ metadata:
|
||||
labels:
|
||||
team-name: my-team
|
||||
team-bu: bu-id
|
||||
argoproj.io/cluster: test-cluster
|
||||
spec:
|
||||
destination:
|
||||
namespace: dummy-namespace
|
||||
@@ -57,6 +58,7 @@ metadata:
|
||||
labels:
|
||||
team-name: my-team
|
||||
team-bu: bu-id
|
||||
argoproj.io/cluster: test-cluster
|
||||
spec:
|
||||
destination:
|
||||
namespace: dummy-namespace
|
||||
@@ -87,6 +89,7 @@ metadata:
|
||||
labels:
|
||||
team-name: my-team
|
||||
team-bu: bu-id
|
||||
argoproj.io/cluster: test-cluster
|
||||
spec:
|
||||
destination:
|
||||
namespace: dummy-namespace
|
||||
@@ -254,14 +257,14 @@ func TestMetricLabels(t *testing.T) {
|
||||
cases := []testCases{
|
||||
{
|
||||
description: "will return the labels metrics successfully",
|
||||
metricLabels: []string{"team-name", "team-bu"},
|
||||
metricLabels: []string{"team-name", "team-bu", "argoproj.io/cluster"},
|
||||
testCombination: testCombination{
|
||||
applications: []string{fakeApp, fakeApp2, fakeApp3},
|
||||
responseContains: `
|
||||
# TYPE argocd_app_labels gauge
|
||||
argocd_app_labels{label_team_bu="bu-id",label_team_name="my-team",name="my-app",namespace="argocd",project="important-project"} 1
|
||||
argocd_app_labels{label_team_bu="bu-id",label_team_name="my-team",name="my-app-2",namespace="argocd",project="important-project"} 1
|
||||
argocd_app_labels{label_team_bu="bu-id",label_team_name="my-team",name="my-app-3",namespace="argocd",project="important-project"} 1
|
||||
argocd_app_labels{label_argoproj_io_cluster="test-cluster",label_team_bu="bu-id",label_team_name="my-team",name="my-app",namespace="argocd",project="important-project"} 1
|
||||
argocd_app_labels{label_argoproj_io_cluster="test-cluster",label_team_bu="bu-id",label_team_name="my-team",name="my-app-2",namespace="argocd",project="important-project"} 1
|
||||
argocd_app_labels{label_argoproj_io_cluster="test-cluster",label_team_bu="bu-id",label_team_name="my-team",name="my-app-3",namespace="argocd",project="important-project"} 1
|
||||
`,
|
||||
},
|
||||
},
|
||||
|
||||
101
controller/metrics/workqueue.go
Normal file
101
controller/metrics/workqueue.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
)
|
||||
|
||||
const (
|
||||
WorkQueueSubsystem = "workqueue"
|
||||
DepthKey = "depth"
|
||||
AddsKey = "adds_total"
|
||||
QueueLatencyKey = "queue_duration_seconds"
|
||||
WorkDurationKey = "work_duration_seconds"
|
||||
UnfinishedWorkKey = "unfinished_work_seconds"
|
||||
LongestRunningProcessorKey = "longest_running_processor_seconds"
|
||||
RetriesKey = "retries_total"
|
||||
)
|
||||
|
||||
var (
|
||||
depth = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Subsystem: WorkQueueSubsystem,
|
||||
Name: DepthKey,
|
||||
Help: "Current depth of workqueue",
|
||||
}, []string{"name"})
|
||||
|
||||
adds = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Subsystem: WorkQueueSubsystem,
|
||||
Name: AddsKey,
|
||||
Help: "Total number of adds handled by workqueue",
|
||||
}, []string{"name"})
|
||||
|
||||
latency = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Subsystem: WorkQueueSubsystem,
|
||||
Name: QueueLatencyKey,
|
||||
Help: "How long in seconds an item stays in workqueue before being requested",
|
||||
Buckets: []float64{1e-6, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 1, 5, 10, 15, 30, 60, 120, 180},
|
||||
}, []string{"name"})
|
||||
|
||||
workDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{
|
||||
Subsystem: WorkQueueSubsystem,
|
||||
Name: WorkDurationKey,
|
||||
Help: "How long in seconds processing an item from workqueue takes.",
|
||||
Buckets: []float64{1e-6, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 1, 5, 10, 15, 30, 60, 120, 180},
|
||||
}, []string{"name"})
|
||||
|
||||
unfinished = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Subsystem: WorkQueueSubsystem,
|
||||
Name: UnfinishedWorkKey,
|
||||
Help: "How many seconds of work has been done that " +
|
||||
"is in progress and hasn't been observed by work_duration. Large " +
|
||||
"values indicate stuck threads. One can deduce the number of stuck " +
|
||||
"threads by observing the rate at which this increases.",
|
||||
}, []string{"name"})
|
||||
|
||||
longestRunningProcessor = prometheus.NewGaugeVec(prometheus.GaugeOpts{
|
||||
Subsystem: WorkQueueSubsystem,
|
||||
Name: LongestRunningProcessorKey,
|
||||
Help: "How many seconds has the longest running " +
|
||||
"processor for workqueue been running.",
|
||||
}, []string{"name"})
|
||||
|
||||
retries = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||
Subsystem: WorkQueueSubsystem,
|
||||
Name: RetriesKey,
|
||||
Help: "Total number of retries handled by workqueue",
|
||||
}, []string{"name"})
|
||||
)
|
||||
|
||||
func init() {
|
||||
workqueue.SetProvider(workqueueMetricsProvider{})
|
||||
}
|
||||
|
||||
type workqueueMetricsProvider struct{}
|
||||
|
||||
func (workqueueMetricsProvider) NewDepthMetric(name string) workqueue.GaugeMetric {
|
||||
return depth.WithLabelValues(name)
|
||||
}
|
||||
|
||||
func (workqueueMetricsProvider) NewAddsMetric(name string) workqueue.CounterMetric {
|
||||
return adds.WithLabelValues(name)
|
||||
}
|
||||
|
||||
func (workqueueMetricsProvider) NewLatencyMetric(name string) workqueue.HistogramMetric {
|
||||
return latency.WithLabelValues(name)
|
||||
}
|
||||
|
||||
func (workqueueMetricsProvider) NewWorkDurationMetric(name string) workqueue.HistogramMetric {
|
||||
return workDuration.WithLabelValues(name)
|
||||
}
|
||||
|
||||
func (workqueueMetricsProvider) NewUnfinishedWorkSecondsMetric(name string) workqueue.SettableGaugeMetric {
|
||||
return unfinished.WithLabelValues(name)
|
||||
}
|
||||
|
||||
func (workqueueMetricsProvider) NewLongestRunningProcessorSecondsMetric(name string) workqueue.SettableGaugeMetric {
|
||||
return longestRunningProcessor.WithLabelValues(name)
|
||||
}
|
||||
|
||||
func (workqueueMetricsProvider) NewRetriesMetric(name string) workqueue.CounterMetric {
|
||||
return retries.WithLabelValues(name)
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
hookutil "github.com/argoproj/gitops-engine/pkg/sync/hook"
|
||||
"github.com/argoproj/gitops-engine/pkg/sync/ignore"
|
||||
resourceutil "github.com/argoproj/gitops-engine/pkg/sync/resource"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
kubeutil "github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
log "github.com/sirupsen/logrus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -30,6 +29,7 @@ import (
|
||||
appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned"
|
||||
"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/v2/util/argo"
|
||||
argodiff "github.com/argoproj/argo-cd/v2/util/argo/diff"
|
||||
appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate"
|
||||
"github.com/argoproj/argo-cd/v2/util/db"
|
||||
"github.com/argoproj/argo-cd/v2/util/gpg"
|
||||
@@ -71,7 +71,7 @@ type comparisonResult struct {
|
||||
resources []v1alpha1.ResourceStatus
|
||||
managedResources []managedResource
|
||||
reconciliationResult sync.ReconciliationResult
|
||||
diffNormalizer diff.Normalizer
|
||||
diffConfig argodiff.DiffConfig
|
||||
appSourceType v1alpha1.ApplicationSourceType
|
||||
// timings maps phases of comparison to the duration it took to complete (for statistical purposes)
|
||||
timings map[string]time.Duration
|
||||
@@ -140,6 +140,10 @@ func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
enabledSourceTypes, err := m.settingsMgr.GetEnabledSourceTypes()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ts.AddCheckpoint("plugins_ms")
|
||||
tools := make([]*appv1.ConfigManagementPlugin, len(plugins))
|
||||
for i := range plugins {
|
||||
@@ -155,6 +159,11 @@ func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
helmOptions, err := m.settingsMgr.GetHelmSettings()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ts.AddCheckpoint("build_options_ms")
|
||||
serverVersion, apiResources, err := m.liveStateCache.GetVersionsInfo(app.Spec.Destination.Server)
|
||||
if err != nil {
|
||||
@@ -162,22 +171,24 @@ func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1
|
||||
}
|
||||
ts.AddCheckpoint("version_ms")
|
||||
manifestInfo, err := repoClient.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
Repo: repo,
|
||||
Repos: permittedHelmRepos,
|
||||
Revision: revision,
|
||||
NoCache: noCache,
|
||||
NoRevisionCache: noRevisionCache,
|
||||
AppLabelKey: appLabelKey,
|
||||
AppName: app.Name,
|
||||
Namespace: app.Spec.Destination.Namespace,
|
||||
ApplicationSource: &source,
|
||||
Plugins: tools,
|
||||
KustomizeOptions: kustomizeOptions,
|
||||
KubeVersion: serverVersion,
|
||||
ApiVersions: argo.APIResourcesToStrings(apiResources, true),
|
||||
VerifySignature: verifySignature,
|
||||
HelmRepoCreds: permittedHelmCredentials,
|
||||
TrackingMethod: string(argo.GetTrackingMethod(m.settingsMgr)),
|
||||
Repo: repo,
|
||||
Repos: permittedHelmRepos,
|
||||
Revision: revision,
|
||||
NoCache: noCache,
|
||||
NoRevisionCache: noRevisionCache,
|
||||
AppLabelKey: appLabelKey,
|
||||
AppName: app.Name,
|
||||
Namespace: app.Spec.Destination.Namespace,
|
||||
ApplicationSource: &source,
|
||||
Plugins: tools,
|
||||
KustomizeOptions: kustomizeOptions,
|
||||
KubeVersion: serverVersion,
|
||||
ApiVersions: argo.APIResourcesToStrings(apiResources, true),
|
||||
VerifySignature: verifySignature,
|
||||
HelmRepoCreds: permittedHelmCredentials,
|
||||
TrackingMethod: string(argo.GetTrackingMethod(m.settingsMgr)),
|
||||
EnabledSourceTypes: enabledSourceTypes,
|
||||
HelmOptions: helmOptions,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -251,24 +262,22 @@ func DeduplicateTargetObjects(
|
||||
return result, conditions, nil
|
||||
}
|
||||
|
||||
func (m *appStateManager) getComparisonSettings(app *appv1.Application) (string, map[string]v1alpha1.ResourceOverride, diff.Normalizer, *settings.ResourcesFilter, error) {
|
||||
// getComparisonSettings will return the system level settings related to the
|
||||
// diff/normalization process.
|
||||
func (m *appStateManager) getComparisonSettings() (string, map[string]v1alpha1.ResourceOverride, *settings.ResourcesFilter, error) {
|
||||
resourceOverrides, err := m.settingsMgr.GetResourceOverrides()
|
||||
if err != nil {
|
||||
return "", nil, nil, nil, err
|
||||
return "", nil, nil, err
|
||||
}
|
||||
appLabelKey, err := m.settingsMgr.GetAppInstanceLabelKey()
|
||||
if err != nil {
|
||||
return "", nil, nil, nil, err
|
||||
}
|
||||
diffNormalizer, err := argo.NewDiffNormalizer(app.Spec.IgnoreDifferences, resourceOverrides)
|
||||
if err != nil {
|
||||
return "", nil, nil, nil, err
|
||||
return "", nil, nil, err
|
||||
}
|
||||
resFilter, err := m.settingsMgr.GetResourcesFilter()
|
||||
if err != nil {
|
||||
return "", nil, nil, nil, err
|
||||
return "", nil, nil, err
|
||||
}
|
||||
return appLabelKey, resourceOverrides, diffNormalizer, resFilter, nil
|
||||
return appLabelKey, resourceOverrides, resFilter, nil
|
||||
}
|
||||
|
||||
// verifyGnuPGSignature verifies the result of a GnuPG operation for a given git
|
||||
@@ -310,64 +319,13 @@ func verifyGnuPGSignature(revision string, project *appv1.AppProject, manifestIn
|
||||
return conditions
|
||||
}
|
||||
|
||||
func (m *appStateManager) diffArrayCached(configArray []*unstructured.Unstructured, liveArray []*unstructured.Unstructured, cachedDiff []*appv1.ResourceDiff, opts ...diff.Option) (*diff.DiffResultList, error) {
|
||||
numItems := len(configArray)
|
||||
if len(liveArray) != numItems {
|
||||
return nil, fmt.Errorf("left and right arrays have mismatched lengths")
|
||||
}
|
||||
|
||||
diffByKey := map[kube.ResourceKey]*appv1.ResourceDiff{}
|
||||
for i := range cachedDiff {
|
||||
res := cachedDiff[i]
|
||||
diffByKey[kube.NewResourceKey(res.Group, res.Kind, res.Namespace, res.Name)] = cachedDiff[i]
|
||||
}
|
||||
|
||||
diffResultList := diff.DiffResultList{
|
||||
Diffs: make([]diff.DiffResult, numItems),
|
||||
}
|
||||
|
||||
for i := 0; i < numItems; i++ {
|
||||
config := configArray[i]
|
||||
live := liveArray[i]
|
||||
resourceVersion := ""
|
||||
var key kube.ResourceKey
|
||||
if live != nil {
|
||||
key = kube.GetResourceKey(live)
|
||||
resourceVersion = live.GetResourceVersion()
|
||||
} else {
|
||||
key = kube.GetResourceKey(config)
|
||||
}
|
||||
var dr *diff.DiffResult
|
||||
if cachedDiff, ok := diffByKey[key]; ok && cachedDiff.ResourceVersion == resourceVersion {
|
||||
dr = &diff.DiffResult{
|
||||
NormalizedLive: []byte(cachedDiff.NormalizedLiveState),
|
||||
PredictedLive: []byte(cachedDiff.PredictedLiveState),
|
||||
Modified: cachedDiff.Modified,
|
||||
}
|
||||
} else {
|
||||
res, err := diff.Diff(configArray[i], liveArray[i], opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dr = res
|
||||
}
|
||||
if dr != nil {
|
||||
diffResultList.Diffs[i] = *dr
|
||||
if dr.Modified {
|
||||
diffResultList.Modified = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &diffResultList, 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, noRevisionCache bool, localManifests []string) *comparisonResult {
|
||||
ts := stats.NewTimingStats()
|
||||
appLabelKey, resourceOverrides, diffNormalizer, resFilter, err := m.getComparisonSettings(app)
|
||||
appLabelKey, resourceOverrides, resFilter, err := m.getComparisonSettings()
|
||||
|
||||
ts.AddCheckpoint("settings_ms")
|
||||
|
||||
// return unknown comparison result if basic comparison settings cannot be loaded
|
||||
@@ -488,32 +446,26 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
|
||||
compareOptions = settings.GetDefaultDiffOptions()
|
||||
}
|
||||
|
||||
logCtx.Debugf("built managed objects list")
|
||||
var diffResults *diff.DiffResultList
|
||||
|
||||
diffOpts := []diff.Option{
|
||||
diff.WithNormalizer(diffNormalizer),
|
||||
diff.IgnoreAggregatedRoles(compareOptions.IgnoreAggregatedRoles),
|
||||
}
|
||||
cachedDiff := make([]*appv1.ResourceDiff, 0)
|
||||
// restore comparison using cached diff result if previous comparison was performed for the same revision
|
||||
revisionChanged := manifestInfo == nil || app.Status.Sync.Revision != manifestInfo.Revision
|
||||
specChanged := !reflect.DeepEqual(app.Status.Sync.ComparedTo, appv1.ComparedTo{Source: app.Spec.Source, Destination: app.Spec.Destination})
|
||||
|
||||
_, refreshRequested := app.IsRefreshRequested()
|
||||
noCache = noCache || refreshRequested || app.Status.Expired(m.statusRefreshTimeout)
|
||||
noCache = noCache || refreshRequested || app.Status.Expired(m.statusRefreshTimeout) || specChanged || revisionChanged
|
||||
|
||||
for i := range reconciliation.Target {
|
||||
_ = m.resourceTracking.Normalize(reconciliation.Target[i], reconciliation.Live[i], appLabelKey, string(trackingMethod))
|
||||
}
|
||||
diffConfigBuilder := argodiff.NewDiffConfigBuilder().
|
||||
WithDiffSettings(app.Spec.IgnoreDifferences, resourceOverrides, compareOptions.IgnoreAggregatedRoles).
|
||||
WithTracking(appLabelKey, string(trackingMethod))
|
||||
|
||||
if noCache || specChanged || revisionChanged || m.cache.GetAppManagedResources(app.Name, &cachedDiff) != nil {
|
||||
// (rare) cache miss
|
||||
diffResults, err = diff.DiffArray(reconciliation.Target, reconciliation.Live, diffOpts...)
|
||||
if noCache {
|
||||
diffConfigBuilder.WithNoCache()
|
||||
} else {
|
||||
diffResults, err = m.diffArrayCached(reconciliation.Target, reconciliation.Live, cachedDiff, diffOpts...)
|
||||
diffConfigBuilder.WithCache(m.cache, app.GetName())
|
||||
}
|
||||
// it necessary to ignore the error at this point to avoid creating duplicated
|
||||
// application conditions as argo.StateDiffs will validate this diffConfig again.
|
||||
diffConfig, _ := diffConfigBuilder.Build()
|
||||
|
||||
diffResults, err := argodiff.StateDiffs(reconciliation.Live, reconciliation.Target, diffConfig)
|
||||
if err != nil {
|
||||
diffResults = &diff.DiffResultList{}
|
||||
failedToLoadObjs = true
|
||||
@@ -634,7 +586,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
|
||||
resources: resourceSummaries,
|
||||
managedResources: managedResources,
|
||||
reconciliationResult: reconciliation,
|
||||
diffNormalizer: diffNormalizer,
|
||||
diffConfig: diffConfig,
|
||||
diffResultList: diffResults,
|
||||
}
|
||||
if manifestInfo != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"github.com/argoproj/gitops-engine/pkg/sync"
|
||||
"github.com/argoproj/gitops-engine/pkg/sync/common"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
jsonpatch "github.com/evanphx/json-patch"
|
||||
log "github.com/sirupsen/logrus"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
@@ -22,6 +24,7 @@ import (
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
|
||||
listersv1alpha1 "github.com/argoproj/argo-cd/v2/pkg/client/listers/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v2/util/argo"
|
||||
"github.com/argoproj/argo-cd/v2/util/argo/diff"
|
||||
logutils "github.com/argoproj/argo-cd/v2/util/log"
|
||||
"github.com/argoproj/argo-cd/v2/util/lua"
|
||||
"github.com/argoproj/argo-cd/v2/util/rand"
|
||||
@@ -172,9 +175,24 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
|
||||
return
|
||||
}
|
||||
|
||||
reconciliationResult := compareResult.reconciliationResult
|
||||
|
||||
// if RespectIgnoreDifferences is enabled, it should normalize the target
|
||||
// resources which in this case applies the live values in the configured
|
||||
// ignore differences fields.
|
||||
if syncOp.SyncOptions.HasOption("RespectIgnoreDifferences=true") {
|
||||
patchedTargets, err := normalizeTargetResources(compareResult)
|
||||
if err != nil {
|
||||
state.Phase = common.OperationError
|
||||
state.Message = fmt.Sprintf("Failed to normalize target resources: %s", err)
|
||||
return
|
||||
}
|
||||
reconciliationResult.Target = patchedTargets
|
||||
}
|
||||
|
||||
syncCtx, cleanup, err := sync.NewSyncContext(
|
||||
compareResult.syncStatus.Revision,
|
||||
compareResult.reconciliationResult,
|
||||
reconciliationResult,
|
||||
restConfig,
|
||||
rawConfig,
|
||||
m.kubectl,
|
||||
@@ -255,6 +273,147 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
|
||||
}
|
||||
}
|
||||
|
||||
// normalizeTargetResources will apply the diff normalization in all live and target resources.
|
||||
// Then it calculates the merge patch between the normalized live and the current live resources.
|
||||
// Finally it applies the merge patch in the normalized target resources. This is done to ensure
|
||||
// that target resources have the same ignored diff fields values from live ones to avoid them to
|
||||
// be applied in the cluster. Returns the list of normalized target resources.
|
||||
func normalizeTargetResources(cr *comparisonResult) ([]*unstructured.Unstructured, error) {
|
||||
// normalize live and target resources
|
||||
normalized, err := diff.Normalize(cr.reconciliationResult.Live, cr.reconciliationResult.Target, cr.diffConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patchedTargets := []*unstructured.Unstructured{}
|
||||
for idx, live := range cr.reconciliationResult.Live {
|
||||
normalizedTarget := normalized.Targets[idx]
|
||||
if normalizedTarget == nil {
|
||||
patchedTargets = append(patchedTargets, nil)
|
||||
continue
|
||||
}
|
||||
originalTarget := cr.reconciliationResult.Target[idx]
|
||||
if live == nil {
|
||||
patchedTargets = append(patchedTargets, originalTarget)
|
||||
continue
|
||||
}
|
||||
// calculate targetPatch between normalized and target resource
|
||||
targetPatch, err := getMergePatch(normalizedTarget, originalTarget)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// check if there is a patch to apply. An empty patch is identified by a '{}' string.
|
||||
if len(targetPatch) > 2 {
|
||||
livePatch, err := getMergePatch(normalized.Lives[idx], live)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// generate a minimal patch that uses the fields from targetPatch (template)
|
||||
// with livePatch values
|
||||
patch, err := compilePatch(targetPatch, livePatch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
normalizedTarget, err = applyMergePatch(normalizedTarget, patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// if there is no patch just use the original target
|
||||
normalizedTarget = originalTarget
|
||||
}
|
||||
patchedTargets = append(patchedTargets, normalizedTarget)
|
||||
}
|
||||
return patchedTargets, nil
|
||||
}
|
||||
|
||||
// compilePatch will generate a patch using the fields from templatePatch with
|
||||
// the values from valuePatch.
|
||||
func compilePatch(templatePatch, valuePatch []byte) ([]byte, error) {
|
||||
templateMap := make(map[string]interface{})
|
||||
err := json.Unmarshal(templatePatch, &templateMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
valueMap := make(map[string]interface{})
|
||||
err = json.Unmarshal(valuePatch, &valueMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resultMap := intersectMap(templateMap, valueMap)
|
||||
return json.Marshal(resultMap)
|
||||
}
|
||||
|
||||
// intersectMap will return map with the fields intersection from the 2 provided
|
||||
// maps populated with the valueMap values.
|
||||
func intersectMap(templateMap, valueMap map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
for k, v := range templateMap {
|
||||
if innerTMap, ok := v.(map[string]interface{}); ok {
|
||||
if innerVMap, ok := valueMap[k].(map[string]interface{}); ok {
|
||||
result[k] = intersectMap(innerTMap, innerVMap)
|
||||
}
|
||||
} else if innerTSlice, ok := v.([]interface{}); ok {
|
||||
if innerVSlice, ok := valueMap[k].([]interface{}); ok {
|
||||
items := []interface{}{}
|
||||
for idx, innerTSliceValue := range innerTSlice {
|
||||
if idx < len(innerVSlice) {
|
||||
if tSliceValueMap, ok := innerTSliceValue.(map[string]interface{}); ok {
|
||||
if vSliceValueMap, ok := innerVSlice[idx].(map[string]interface{}); ok {
|
||||
item := intersectMap(tSliceValueMap, vSliceValueMap)
|
||||
items = append(items, item)
|
||||
}
|
||||
} else {
|
||||
items = append(items, innerVSlice[idx])
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(items) > 0 {
|
||||
result[k] = items
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if _, ok := valueMap[k]; ok {
|
||||
result[k] = valueMap[k]
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// getMergePatch calculates and returns the patch between the original and the
|
||||
// modified unstructures.
|
||||
func getMergePatch(original, modified *unstructured.Unstructured) ([]byte, error) {
|
||||
originalJSON, err := original.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
modifiedJSON, err := modified.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return jsonpatch.CreateMergePatch(originalJSON, modifiedJSON)
|
||||
}
|
||||
|
||||
// applyMergePatch will apply the given patch in the obj and return the patched
|
||||
// unstructure.
|
||||
func applyMergePatch(obj *unstructured.Unstructured, patch []byte) (*unstructured.Unstructured, error) {
|
||||
originalJSON, err := obj.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patchedJSON, err := jsonpatch.MergePatch(originalJSON, patch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patchedObj := &unstructured.Unstructured{}
|
||||
_, _, err = unstructured.UnstructuredJSONScheme.Decode(patchedJSON, nil, patchedObj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return patchedObj, nil
|
||||
}
|
||||
|
||||
// hasSharedResourceCondition will check if the Application has any resource that has already
|
||||
// been synced by another Application. If the resource is found in another Application it returns
|
||||
// true along with a human readable message of which specific resource has this condition.
|
||||
|
||||
@@ -5,16 +5,20 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/sync"
|
||||
"github.com/argoproj/gitops-engine/pkg/sync/common"
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
"github.com/argoproj/argo-cd/v2/controller/testdata"
|
||||
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/v2/test"
|
||||
"github.com/argoproj/argo-cd/v2/util/argo/diff"
|
||||
)
|
||||
|
||||
func TestPersistRevisionHistory(t *testing.T) {
|
||||
@@ -211,3 +215,136 @@ func TestAppStateManager_SyncAppState(t *testing.T) {
|
||||
assert.Contains(t, opState.Message, syncErrorMsg)
|
||||
})
|
||||
}
|
||||
|
||||
func TestNormalizeTargetResources(t *testing.T) {
|
||||
type fixture struct {
|
||||
comparisonResult *comparisonResult
|
||||
}
|
||||
setup := func(t *testing.T, ignores []v1alpha1.ResourceIgnoreDifferences) *fixture {
|
||||
t.Helper()
|
||||
dc, err := diff.NewDiffConfigBuilder().
|
||||
WithDiffSettings(ignores, nil, true).
|
||||
WithNoCache().
|
||||
Build()
|
||||
require.NoError(t, err)
|
||||
live := test.YamlToUnstructured(testdata.LiveDeploymentYaml)
|
||||
target := test.YamlToUnstructured(testdata.TargetDeploymentYaml)
|
||||
return &fixture{
|
||||
&comparisonResult{
|
||||
reconciliationResult: sync.ReconciliationResult{
|
||||
Live: []*unstructured.Unstructured{live},
|
||||
Target: []*unstructured.Unstructured{target},
|
||||
},
|
||||
diffConfig: dc,
|
||||
},
|
||||
}
|
||||
}
|
||||
t.Run("will modify target resource adding live state in fields it should ignore", func(t *testing.T) {
|
||||
// given
|
||||
ignore := v1alpha1.ResourceIgnoreDifferences{
|
||||
Group: "*",
|
||||
Kind: "*",
|
||||
ManagedFieldsManagers: []string{"janitor"},
|
||||
}
|
||||
ignores := []v1alpha1.ResourceIgnoreDifferences{ignore}
|
||||
f := setup(t, ignores)
|
||||
|
||||
// when
|
||||
targets, err := normalizeTargetResources(f.comparisonResult)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(targets))
|
||||
iksmVersion := targets[0].GetAnnotations()["iksm-version"]
|
||||
assert.Equal(t, "2.0", iksmVersion)
|
||||
})
|
||||
t.Run("will not modify target resource if ignore difference is not configured", func(t *testing.T) {
|
||||
// given
|
||||
f := setup(t, []v1alpha1.ResourceIgnoreDifferences{})
|
||||
|
||||
// when
|
||||
targets, err := normalizeTargetResources(f.comparisonResult)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(targets))
|
||||
iksmVersion := targets[0].GetAnnotations()["iksm-version"]
|
||||
assert.Equal(t, "1.0", iksmVersion)
|
||||
})
|
||||
t.Run("will remove fields from target if not present in live", func(t *testing.T) {
|
||||
ignore := v1alpha1.ResourceIgnoreDifferences{
|
||||
Group: "apps",
|
||||
Kind: "Deployment",
|
||||
JSONPointers: []string{"/metadata/annotations/iksm-version"},
|
||||
}
|
||||
ignores := []v1alpha1.ResourceIgnoreDifferences{ignore}
|
||||
f := setup(t, ignores)
|
||||
live := f.comparisonResult.reconciliationResult.Live[0]
|
||||
unstructured.RemoveNestedField(live.Object, "metadata", "annotations", "iksm-version")
|
||||
|
||||
// when
|
||||
targets, err := normalizeTargetResources(f.comparisonResult)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(targets))
|
||||
_, ok := targets[0].GetAnnotations()["iksm-version"]
|
||||
assert.False(t, ok)
|
||||
})
|
||||
t.Run("will correctly normalize with multiple ignore configurations", func(t *testing.T) {
|
||||
// given
|
||||
ignores := []v1alpha1.ResourceIgnoreDifferences{
|
||||
{
|
||||
Group: "apps",
|
||||
Kind: "Deployment",
|
||||
JSONPointers: []string{"/spec/replicas"},
|
||||
},
|
||||
{
|
||||
Group: "*",
|
||||
Kind: "*",
|
||||
ManagedFieldsManagers: []string{"janitor"},
|
||||
},
|
||||
}
|
||||
f := setup(t, ignores)
|
||||
|
||||
// when
|
||||
targets, err := normalizeTargetResources(f.comparisonResult)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(targets))
|
||||
normalized := targets[0]
|
||||
iksmVersion, ok := normalized.GetAnnotations()["iksm-version"]
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "2.0", iksmVersion)
|
||||
replicas, ok, err := unstructured.NestedInt64(normalized.Object, "spec", "replicas")
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, int64(4), replicas)
|
||||
})
|
||||
t.Run("will keep new array entries not found in live state if not ignored", func(t *testing.T) {
|
||||
t.Skip("limitation in the current implementation")
|
||||
// given
|
||||
ignores := []v1alpha1.ResourceIgnoreDifferences{
|
||||
{
|
||||
Group: "apps",
|
||||
Kind: "Deployment",
|
||||
JQPathExpressions: []string{".spec.template.spec.containers[] | select(.name == \"guestbook-ui\")"},
|
||||
},
|
||||
}
|
||||
f := setup(t, ignores)
|
||||
target := test.YamlToUnstructured(testdata.TargetDeploymentNewEntries)
|
||||
f.comparisonResult.reconciliationResult.Target = []*unstructured.Unstructured{target}
|
||||
|
||||
// when
|
||||
targets, err := normalizeTargetResources(f.comparisonResult)
|
||||
|
||||
// then
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, len(targets))
|
||||
containers, ok, err := unstructured.NestedSlice(targets[0].Object, "spec", "template", "spec", "containers")
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, 2, len(containers))
|
||||
})
|
||||
}
|
||||
|
||||
14
controller/testdata/data.go
vendored
Normal file
14
controller/testdata/data.go
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
package testdata
|
||||
|
||||
import _ "embed"
|
||||
|
||||
var (
|
||||
//go:embed live-deployment.yaml
|
||||
LiveDeploymentYaml string
|
||||
|
||||
//go:embed target-deployment.yaml
|
||||
TargetDeploymentYaml string
|
||||
|
||||
//go:embed target-deployment-new-entries.yaml
|
||||
TargetDeploymentNewEntries string
|
||||
)
|
||||
177
controller/testdata/live-deployment.yaml
vendored
Normal file
177
controller/testdata/live-deployment.yaml
vendored
Normal file
@@ -0,0 +1,177 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
argocd.argoproj.io/tracking-id: 'guestbook:apps/Deployment:default/kustomize-guestbook-ui'
|
||||
deployment.kubernetes.io/revision: '9'
|
||||
iksm-version: '2.0'
|
||||
kubectl.kubernetes.io/last-applied-configuration: >
|
||||
{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{"argocd.argoproj.io/tracking-id":"guestbook:apps/Deployment:default/kustomize-guestbook-ui","iksm-version":"2.0"},"name":"kustomize-guestbook-ui","namespace":"default"},"spec":{"replicas":4,"revisionHistoryLimit":3,"selector":{"matchLabels":{"app":"guestbook-ui"}},"template":{"metadata":{"labels":{"app":"guestbook-ui"}},"spec":{"containers":[{"env":[{"name":"SOME_ENV_VAR","value":"some_value"}],"image":"gcr.io/heptio-images/ks-guestbook-demo:0.1","name":"guestbook-ui","ports":[{"containerPort":80}],"resources":{"requests":{"cpu":"50m","memory":"100Mi"}}}]}}}}
|
||||
creationTimestamp: '2022-01-05T15:45:21Z'
|
||||
generation: 119
|
||||
managedFields:
|
||||
- apiVersion: apps/v1
|
||||
fieldsType: FieldsV1
|
||||
fieldsV1:
|
||||
'f:metadata':
|
||||
'f:annotations':
|
||||
'f:iksm-version': {}
|
||||
manager: janitor
|
||||
operation: Apply
|
||||
time: '2022-01-06T18:21:04Z'
|
||||
- apiVersion: apps/v1
|
||||
fieldsType: FieldsV1
|
||||
fieldsV1:
|
||||
'f:metadata':
|
||||
'f:annotations':
|
||||
.: {}
|
||||
'f:argocd.argoproj.io/tracking-id': {}
|
||||
'f:kubectl.kubernetes.io/last-applied-configuration': {}
|
||||
'f:spec':
|
||||
'f:progressDeadlineSeconds': {}
|
||||
'f:replicas': {}
|
||||
'f:revisionHistoryLimit': {}
|
||||
'f:selector': {}
|
||||
'f:strategy':
|
||||
'f:rollingUpdate':
|
||||
.: {}
|
||||
'f:maxSurge': {}
|
||||
'f:maxUnavailable': {}
|
||||
'f:type': {}
|
||||
'f:template':
|
||||
'f:metadata':
|
||||
'f:labels':
|
||||
.: {}
|
||||
'f:app': {}
|
||||
'f:spec':
|
||||
'f:containers':
|
||||
'k:{"name":"guestbook-ui"}':
|
||||
.: {}
|
||||
'f:env':
|
||||
.: {}
|
||||
'k:{"name":"SOME_ENV_VAR"}':
|
||||
.: {}
|
||||
'f:name': {}
|
||||
'f:value': {}
|
||||
'f:image': {}
|
||||
'f:imagePullPolicy': {}
|
||||
'f:name': {}
|
||||
'f:ports':
|
||||
.: {}
|
||||
'k:{"containerPort":80,"protocol":"TCP"}':
|
||||
.: {}
|
||||
'f:containerPort': {}
|
||||
'f:protocol': {}
|
||||
'f:resources':
|
||||
.: {}
|
||||
'f:requests':
|
||||
.: {}
|
||||
'f:cpu': {}
|
||||
'f:memory': {}
|
||||
'f:terminationMessagePath': {}
|
||||
'f:terminationMessagePolicy': {}
|
||||
'f:dnsPolicy': {}
|
||||
'f:restartPolicy': {}
|
||||
'f:schedulerName': {}
|
||||
'f:securityContext': {}
|
||||
'f:terminationGracePeriodSeconds': {}
|
||||
manager: argocd
|
||||
operation: Update
|
||||
time: '2022-01-06T15:04:15Z'
|
||||
- apiVersion: apps/v1
|
||||
fieldsType: FieldsV1
|
||||
fieldsV1:
|
||||
'f:metadata':
|
||||
'f:annotations':
|
||||
'f:deployment.kubernetes.io/revision': {}
|
||||
'f:status':
|
||||
'f:availableReplicas': {}
|
||||
'f:conditions':
|
||||
.: {}
|
||||
'k:{"type":"Available"}':
|
||||
.: {}
|
||||
'f:lastTransitionTime': {}
|
||||
'f:lastUpdateTime': {}
|
||||
'f:message': {}
|
||||
'f:reason': {}
|
||||
'f:status': {}
|
||||
'f:type': {}
|
||||
'k:{"type":"Progressing"}':
|
||||
.: {}
|
||||
'f:lastTransitionTime': {}
|
||||
'f:lastUpdateTime': {}
|
||||
'f:message': {}
|
||||
'f:reason': {}
|
||||
'f:status': {}
|
||||
'f:type': {}
|
||||
'f:observedGeneration': {}
|
||||
'f:readyReplicas': {}
|
||||
'f:replicas': {}
|
||||
'f:updatedReplicas': {}
|
||||
manager: kube-controller-manager
|
||||
operation: Update
|
||||
time: '2022-01-06T18:15:14Z'
|
||||
name: kustomize-guestbook-ui
|
||||
namespace: default
|
||||
resourceVersion: '8289211'
|
||||
uid: ef253575-ce44-4c5e-84ad-16e81d0df6eb
|
||||
spec:
|
||||
progressDeadlineSeconds: 600
|
||||
replicas: 4
|
||||
revisionHistoryLimit: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: guestbook-ui
|
||||
strategy:
|
||||
rollingUpdate:
|
||||
maxSurge: 25%
|
||||
maxUnavailable: 25%
|
||||
type: RollingUpdate
|
||||
template:
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
labels:
|
||||
app: guestbook-ui
|
||||
spec:
|
||||
containers:
|
||||
- env:
|
||||
- name: SOME_ENV_VAR
|
||||
value: some_value
|
||||
image: 'gcr.io/heptio-images/ks-guestbook-demo:0.1'
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: guestbook-ui
|
||||
ports:
|
||||
- containerPort: 80
|
||||
protocol: TCP
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 100Mi
|
||||
terminationMessagePath: /dev/termination-log
|
||||
terminationMessagePolicy: File
|
||||
dnsPolicy: ClusterFirst
|
||||
restartPolicy: Always
|
||||
schedulerName: default-scheduler
|
||||
securityContext: {}
|
||||
terminationGracePeriodSeconds: 30
|
||||
status:
|
||||
availableReplicas: 4
|
||||
conditions:
|
||||
- lastTransitionTime: '2022-01-05T22:20:37Z'
|
||||
lastUpdateTime: '2022-01-05T22:43:47Z'
|
||||
message: >-
|
||||
ReplicaSet "kustomize-guestbook-ui-6549d54677" has successfully
|
||||
progressed.
|
||||
reason: NewReplicaSetAvailable
|
||||
status: 'True'
|
||||
type: Progressing
|
||||
- lastTransitionTime: '2022-01-06T18:15:14Z'
|
||||
lastUpdateTime: '2022-01-06T18:15:14Z'
|
||||
message: Deployment has minimum availability.
|
||||
reason: MinimumReplicasAvailable
|
||||
status: 'True'
|
||||
type: Available
|
||||
observedGeneration: 119
|
||||
readyReplicas: 4
|
||||
replicas: 4
|
||||
updatedReplicas: 4
|
||||
36
controller/testdata/target-deployment-new-entries.yaml
vendored
Normal file
36
controller/testdata/target-deployment-new-entries.yaml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
argocd.argoproj.io/tracking-id: 'guestbook:apps/Deployment:default/kustomize-guestbook-ui'
|
||||
iksm-version: '1.0'
|
||||
name: kustomize-guestbook-ui
|
||||
namespace: default
|
||||
spec:
|
||||
replicas: 1
|
||||
revisionHistoryLimit: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: guestbook-ui
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: guestbook-ui
|
||||
spec:
|
||||
containers:
|
||||
- name: guestbook-ui
|
||||
image: 'gcr.io/heptio-images/ks-guestbook-demo:0.1'
|
||||
env:
|
||||
- name: SOME_ENV_VAR
|
||||
value: some_value
|
||||
- name: NEW_ENV_VAR
|
||||
value: new_value
|
||||
ports:
|
||||
- containerPort: 80
|
||||
- grpcPort: 8081
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 100Mi
|
||||
- name: new-container
|
||||
image: 'new-image:1.0'
|
||||
31
controller/testdata/target-deployment.yaml
vendored
Normal file
31
controller/testdata/target-deployment.yaml
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
annotations:
|
||||
argocd.argoproj.io/tracking-id: 'guestbook:apps/Deployment:default/kustomize-guestbook-ui'
|
||||
iksm-version: '1.0'
|
||||
name: kustomize-guestbook-ui
|
||||
namespace: default
|
||||
spec:
|
||||
replicas: 1
|
||||
revisionHistoryLimit: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: guestbook-ui
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: guestbook-ui
|
||||
spec:
|
||||
containers:
|
||||
- env:
|
||||
- name: SOME_ENV_VAR
|
||||
value: some_value
|
||||
image: 'gcr.io/heptio-images/ks-guestbook-demo:0.1'
|
||||
name: guestbook-ui
|
||||
ports:
|
||||
- containerPort: 80
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 100Mi
|
||||
@@ -4,6 +4,12 @@ You can download the latest Argo CD version from [the latest release page of thi
|
||||
|
||||
## Linux and WSL
|
||||
|
||||
### ArchLinux User Repository ([AUR](https://aur.archlinux.org/packages/))
|
||||
|
||||
```bash
|
||||
yay -Sy argocd-bin
|
||||
```
|
||||
|
||||
### Homebrew
|
||||
|
||||
```bash
|
||||
|
||||
@@ -29,7 +29,7 @@ The Docker version must be fairly recent, and support multi-stage builds. You sh
|
||||
|
||||
* Obviously, you will need a `git` client for pulling source code and pushing back your changes.
|
||||
|
||||
* Last but not least, you will need a Go SDK and related tools (such as GNU `make`) installed and working on your development environment. The minimum required Go version for building and testing Argo CD is **v1.16**.
|
||||
* Last but not least, you will need a Go SDK and related tools (such as GNU `make`) installed and working on your development environment. The minimum required Go version for building and testing Argo CD is **v1.17**.
|
||||
|
||||
* We will assume that your Go workspace is at `~/go`.
|
||||
|
||||
|
||||
12
docs/faq.md
12
docs/faq.md
@@ -86,6 +86,18 @@ data:
|
||||
name: stable
|
||||
```
|
||||
|
||||
## After deploying my Helm application with Argo CD I cannot see it with `helm ls` and other Helm commands
|
||||
|
||||
When deploying a Helm application Argo CD is using Helm
|
||||
only as a template mechanism. It runs `helm template` and
|
||||
then deploys the resulting manifests on the cluster instead of doing `helm install`. This means that you cannot use any Helm command
|
||||
to view/verify the application. It is fully managed by Argo CD.
|
||||
Note that Argo CD supports natively some capabilities that you might miss in Helm (such as the history and rollback commands).
|
||||
|
||||
This decision was made so that Argo CD is neutral
|
||||
to all manifest generators.
|
||||
|
||||
|
||||
## I've configured [cluster secret](./operator-manual/declarative-setup.md#clusters) but it does not show up in CLI/UI, how do I fix it?
|
||||
|
||||
Check if cluster secret has `argocd.argoproj.io/secret-type: cluster` label. If secret has the label but the cluster is
|
||||
|
||||
@@ -18,7 +18,7 @@ kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/st
|
||||
This will create a new namespace, `argocd`, where Argo CD services and application resources will live.
|
||||
|
||||
!!! warning
|
||||
The installation manifests include `ClusterRoleBinding` resources that reference `argocd` namespace. If you installing Argo CD into a different
|
||||
The installation manifests include `ClusterRoleBinding` resources that reference `argocd` namespace. If you are installing Argo CD into a different
|
||||
namespace then make sure to update the namespace reference.
|
||||
|
||||
If you are not interested in UI, SSO, multi-cluster features then you can install [core](operator-manual/installation.md#core) Argo CD components only:
|
||||
@@ -133,7 +133,7 @@ An example repository containing a guestbook application is available at
|
||||
Create the example guestbook application with the following command:
|
||||
|
||||
```bash
|
||||
argocd app create guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path guestbook --dest-server https://kubernetes.default.svc --dest-namespace default`
|
||||
argocd app create guestbook --repo https://github.com/argoproj/argocd-example-apps.git --path guestbook --dest-server https://kubernetes.default.svc --dest-namespace default
|
||||
```
|
||||
|
||||
### Creating Apps Via UI
|
||||
|
||||
@@ -4,7 +4,7 @@ metadata:
|
||||
name: guestbook
|
||||
# You'll usually want to add your resources to the argocd namespace.
|
||||
namespace: argocd
|
||||
# Add a this finalizer ONLY if you want these to cascade delete.
|
||||
# Add this finalizer ONLY if you want these to cascade delete.
|
||||
finalizers:
|
||||
- resources-finalizer.argocd.argoproj.io
|
||||
spec:
|
||||
@@ -13,11 +13,12 @@ spec:
|
||||
|
||||
# Source of the application manifests
|
||||
source:
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps.git
|
||||
targetRevision: HEAD
|
||||
path: guestbook
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps.git # Can point to either a Helm chart repo or a git repo.
|
||||
targetRevision: HEAD # For Helm, this refers to the chart version.
|
||||
path: guestbook # This has no meaning for Helm charts pulled directly from a Helm repo instead of git.
|
||||
|
||||
# helm specific config
|
||||
chart: chart-name # Set this when pulling directly from a Helm repo. DO NOT set for git-hosted Helm charts.
|
||||
helm:
|
||||
# Extra parameters to set (same as setting through values.yaml, but these take precedence)
|
||||
parameters:
|
||||
@@ -27,6 +28,11 @@ spec:
|
||||
value: "true"
|
||||
forceString: true # ensures that value is treated as a string
|
||||
|
||||
# Use the contents of files as parameters (uses Helm's --set-file)
|
||||
fileParameters:
|
||||
- name: config
|
||||
path: files/config.json
|
||||
|
||||
# Release name override (defaults to application name)
|
||||
releaseName: guestbook
|
||||
|
||||
@@ -85,6 +91,9 @@ spec:
|
||||
|
||||
# plugin specific config
|
||||
plugin:
|
||||
# Only set the plugin name if the plugin is defined in argocd-cm.
|
||||
# If the plugin is defined as a sidecar, omit the name. The plugin will be automatically matched with the
|
||||
# Application according to the plugin's discovery rules.
|
||||
name: mypluginname
|
||||
# environment variables passed to the plugin
|
||||
env:
|
||||
@@ -115,9 +124,16 @@ spec:
|
||||
factor: 2 # a factor to multiply the base duration after each failed retry
|
||||
maxDuration: 3m # the maximum amount of time allowed for the backoff strategy
|
||||
|
||||
# Ignore differences at the specified json pointers
|
||||
# Will ignore differences between live and desired states during the diff. Note that these configurations are not
|
||||
# used during the sync process.
|
||||
ignoreDifferences:
|
||||
# for the specified json pointers
|
||||
- group: apps
|
||||
kind: Deployment
|
||||
jsonPointers:
|
||||
- /spec/replicas
|
||||
# for the specified managedFields managers
|
||||
- group: "*"
|
||||
kind: "*"
|
||||
managedFieldsManagers:
|
||||
- kube-controller-manager
|
||||
|
||||
@@ -31,4 +31,3 @@ The application controller is a Kubernetes controller which continuously monitor
|
||||
applications and compares the current, live state against the desired target state (as specified in
|
||||
the repo). It detects `OutOfSync` application state and optionally takes corrective action. It
|
||||
is responsible for invoking any user-defined hooks for lifecycle events (PreSync, Sync, PostSync)
|
||||
|
||||
|
||||
@@ -30,6 +30,12 @@ data:
|
||||
help.chatUrl: "https://mycorp.slack.com/argo-cd"
|
||||
# the text for getting chat help, defaults to "Chat now!"
|
||||
help.chatText: "Chat now!"
|
||||
# The URLs to download additional ArgoCD binaries (besides the Linux amd64 binary included by default)
|
||||
# for different OS architectures. If provided, additional download buttons will be displayed on the help page.
|
||||
help.download.linux-arm64: "path-or-url-to-download"
|
||||
help.download.darwin-amd64: "path-or-url-to-download"
|
||||
help.download.darwin-arm64: "path-or-url-to-download"
|
||||
help.download.windows-amd64: "path-or-url-to-download"
|
||||
|
||||
# A dex connector configuration (optional). See SSO configuration documentation:
|
||||
# https://github.com/argoproj/argo-cd/blob/master/docs/operator-manual/sso
|
||||
@@ -75,6 +81,16 @@ data:
|
||||
- /webhooks/0/clientConfig/caBundle
|
||||
jqPathExpressions:
|
||||
- .webhooks[0].clientConfig.caBundle
|
||||
managedFieldsManagers:
|
||||
- kube-controller-manager
|
||||
|
||||
# Configuration to define customizations ignoring differences between live and desired states for
|
||||
# all resources (GK).
|
||||
resource.customizations.ignoreDifferences.all: |
|
||||
managedFieldsManagers:
|
||||
- kube-controller-manager
|
||||
jsonPointers:
|
||||
- /spec/replicas
|
||||
|
||||
resource.customizations.health.certmanager.k8s.io-Certificate: |
|
||||
hs = {}
|
||||
@@ -183,6 +199,13 @@ data:
|
||||
generate:
|
||||
command: [kasane, show]
|
||||
|
||||
# A set of settings that allow enabling or disabling the config management tool.
|
||||
# If unset, each defaults to "true".
|
||||
kustomize.enabled: true
|
||||
jsonnet.enabled: true
|
||||
helm.enabled: true
|
||||
ksonnet.enabled: true
|
||||
|
||||
# Build options/parameters to use with `kustomize build` (optional)
|
||||
kustomize.buildOptions: --load_restrictor none
|
||||
|
||||
@@ -194,6 +217,10 @@ data:
|
||||
kustomize.version.v3.5.1: /custom-tools/kustomize_3_5_1
|
||||
kustomize.version.v3.5.4: /custom-tools/kustomize_3_5_4
|
||||
|
||||
# Comma delimited list of additional custom remote values file schemes (http are https are allowed by default).
|
||||
# Change to empty value if you want to disable remote values files altogether.
|
||||
helm.valuesFileSchemes: http, https
|
||||
|
||||
# The metadata.label key name where Argo CD injects the app name as a tracking label (optional).
|
||||
# Tracking labels are used to determine which resources need to be deleted when pruning.
|
||||
# If omitted, Argo CD injects the app name into the label: 'app.kubernetes.io/instance'
|
||||
|
||||
@@ -14,3 +14,5 @@ data:
|
||||
gitlab.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9
|
||||
ssh.dev.azure.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7Hr1oTWqNqOlzGJOfGJ4NakVyIzf1rXYd4d7wo6jBlkLvCA4odBlL0mDUyZ0/QUfTTqeu+tm22gOsv+VrVTMk6vwRU75gY/y9ut5Mb3bR5BV58dKXyq9A9UeB5Cakehn5Zgm6x1mKoVyf+FFn26iYqXJRgzIZZcZ5V6hrE0Qg39kZm4az48o0AUbf6Sp4SLdvnuMa2sVNwHBboS7EJkm57XQPVU3/QpyNLHbWDdzwtrlS+ez30S3AdYhLKEOxAG8weOnyrtLJAUen9mTkol8oII1edf7mWWbWVf0nBmly21+nZcmCTISQBtdcyPaEno7fFQMDD26/s0lfKob4Kw8H
|
||||
vs-ssh.visualstudio.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7Hr1oTWqNqOlzGJOfGJ4NakVyIzf1rXYd4d7wo6jBlkLvCA4odBlL0mDUyZ0/QUfTTqeu+tm22gOsv+VrVTMk6vwRU75gY/y9ut5Mb3bR5BV58dKXyq9A9UeB5Cakehn5Zgm6x1mKoVyf+FFn26iYqXJRgzIZZcZ5V6hrE0Qg39kZm4az48o0AUbf6Sp4SLdvnuMa2sVNwHBboS7EJkm57XQPVU3/QpyNLHbWDdzwtrlS+ez30S3AdYhLKEOxAG8weOnyrtLJAUen9mTkol8oII1edf7mWWbWVf0nBmly21+nZcmCTISQBtdcyPaEno7fFQMDD26/s0lfKob4Kw8H
|
||||
github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=
|
||||
github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
|
||||
|
||||
@@ -93,3 +93,21 @@ argocd app sync -l app.kubernetes.io/instance=apps
|
||||
```
|
||||
|
||||
View [the example on GitHub](https://github.com/argoproj/argocd-example-apps/tree/master/apps).
|
||||
|
||||
|
||||
|
||||
### Cascading deletion
|
||||
|
||||
If you want to ensure that child-apps and all of their resources are deleted when the parent-app is deleted make sure to add the appropriate [finalizer](https://argo-cd-docs.readthedocs.io/en/latest/user-guide/app_deletion/#about-the-deletion-finalizer) to your `Application` definition
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: guestbook
|
||||
namespace: argocd
|
||||
finalizers:
|
||||
- resources-finalizer.argocd.argoproj.io
|
||||
spec:
|
||||
...
|
||||
```
|
||||
|
||||
@@ -5,9 +5,9 @@ as part of its container images. Sometimes, it may be desired to use a specific
|
||||
other than what Argo CD bundles. Some reasons to do this might be:
|
||||
|
||||
* To upgrade/downgrade to a specific version of a tool due to bugs or bug fixes.
|
||||
* To install additional dependencies which to be used by kustomize's configmap/secret generators
|
||||
* To install additional dependencies to be used by kustomize's configmap/secret generators.
|
||||
(e.g. curl, vault, gpg, AWS CLI)
|
||||
* To install a [config management plugin](../user-guide/application_sources.md#config-management-plugins)
|
||||
* To install a [config management plugin](../user-guide/config-management-plugins.md).
|
||||
|
||||
As the Argo CD repo-server is the single service responsible for generating Kubernetes manifests, it
|
||||
can be customized to use alternative toolchain required by your environment.
|
||||
@@ -46,7 +46,7 @@ the helm binary with a different version than what is bundled in Argo CD:
|
||||
|
||||
## BYOI (Build Your Own Image)
|
||||
|
||||
Sometimes replacing a binary isn't sufficient and you need to install other dependencies. The
|
||||
Sometimes replacing a binary isn't sufficient, and you need to install other dependencies. The
|
||||
following example builds an entirely customized repo-server from a Dockerfile, installing extra
|
||||
dependencies that may be needed for generating manifests.
|
||||
|
||||
|
||||
@@ -213,6 +213,7 @@ stringData:
|
||||
Example for GitHub App:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: github-repo
|
||||
@@ -224,7 +225,7 @@ stringData:
|
||||
repo: https://github.com/argoproj/my-private-repository
|
||||
githubAppID: 1
|
||||
githubAppInstallationID: 2
|
||||
githubAppPrivateKeySecret: |
|
||||
githubAppPrivateKey: |
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
...
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
@@ -406,6 +407,8 @@ data:
|
||||
gitlab.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9
|
||||
ssh.dev.azure.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7Hr1oTWqNqOlzGJOfGJ4NakVyIzf1rXYd4d7wo6jBlkLvCA4odBlL0mDUyZ0/QUfTTqeu+tm22gOsv+VrVTMk6vwRU75gY/y9ut5Mb3bR5BV58dKXyq9A9UeB5Cakehn5Zgm6x1mKoVyf+FFn26iYqXJRgzIZZcZ5V6hrE0Qg39kZm4az48o0AUbf6Sp4SLdvnuMa2sVNwHBboS7EJkm57XQPVU3/QpyNLHbWDdzwtrlS+ez30S3AdYhLKEOxAG8weOnyrtLJAUen9mTkol8oII1edf7mWWbWVf0nBmly21+nZcmCTISQBtdcyPaEno7fFQMDD26/s0lfKob4Kw8H
|
||||
vs-ssh.visualstudio.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7Hr1oTWqNqOlzGJOfGJ4NakVyIzf1rXYd4d7wo6jBlkLvCA4odBlL0mDUyZ0/QUfTTqeu+tm22gOsv+VrVTMk6vwRU75gY/y9ut5Mb3bR5BV58dKXyq9A9UeB5Cakehn5Zgm6x1mKoVyf+FFn26iYqXJRgzIZZcZ5V6hrE0Qg39kZm4az48o0AUbf6Sp4SLdvnuMa2sVNwHBboS7EJkm57XQPVU3/QpyNLHbWDdzwtrlS+ez30S3AdYhLKEOxAG8weOnyrtLJAUen9mTkol8oII1edf7mWWbWVf0nBmly21+nZcmCTISQBtdcyPaEno7fFQMDD26/s0lfKob4Kw8H
|
||||
github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=
|
||||
github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
@@ -32,7 +32,7 @@ and might fail. To avoid failed syncs use `ARGOCD_GIT_ATTEMPTS_COUNT` environmen
|
||||
|
||||
* `argocd-repo-server` Every 3m (by default) Argo CD checks for changes to the app manifests. Argo CD assumes by default that manifests only change when the repo changes, so it caches generated manifests (for 24h by default). With Kustomize remote bases, or Helm patch releases, the manifests can change even though the repo has not changed. By reducing the cache time, you can get the changes without waiting for 24h. Use `--repo-cache-expiration duration`, and we'd suggest in low volume environments you try '1h'. Bear in mind this will negate the benefit of caching if set too low.
|
||||
|
||||
* `argocd-repo-server` fork exec config management tools such as `helm` or `kustomize` and enforces 90 seconds timeout. The timeout can be increased using `ARGOCD_EXEC_TIMEOUT` env variable.
|
||||
* `argocd-repo-server` fork exec config management tools such as `helm` or `kustomize` and enforces 90 seconds timeout. The timeout can be increased using `ARGOCD_EXEC_TIMEOUT` env variable. The value should be in Go time duration string format, for example, `2m30s`.
|
||||
|
||||
**metrics:**
|
||||
|
||||
|
||||
@@ -32,15 +32,19 @@ metadata:
|
||||
name: argocd-server-cli
|
||||
namespace: argocd
|
||||
spec:
|
||||
# NOTE: the port must be ignored if you have strip_matching_host_port enabled on envoy
|
||||
host: argocd.example.com:443
|
||||
prefix: /
|
||||
service: argocd-server:443
|
||||
service: argocd-server:80
|
||||
regex_headers:
|
||||
Content-Type: "^application/grpc.*$"
|
||||
grpc: true
|
||||
```
|
||||
|
||||
Login with the `argocd` CLI using the extra `--grpc-web-root-path` flag for gRPC-web.
|
||||
Login with the `argocd` CLI:
|
||||
|
||||
```shell
|
||||
argocd login <host>:<port> --grpc-web-root-path /
|
||||
argocd login <host>
|
||||
```
|
||||
|
||||
### Option 2: Mapping CRD for Path-based Routing
|
||||
@@ -446,7 +450,7 @@ To:
|
||||
|
||||
### Creating a service
|
||||
|
||||
Now you need an externally accesible service. This is practically the same as the internal service Argo CD has, but as a NodePort and with Google Cloud annotations. Note that this service is annotated to use a [Network Endpoint Group](https://cloud.google.com/load-balancing/docs/negs) (NEG) to allow your load balancer to send traffic directly to your pods without using kube-proxy, so remove the `neg` annotation it that's not what you want.
|
||||
Now you need an externally accesible service. This is practically the same as the internal service Argo CD has, but with Google Cloud annotations. Note that this service is annotated to use a [Network Endpoint Group](https://cloud.google.com/load-balancing/docs/negs) (NEG) to allow your load balancer to send traffic directly to your pods without using kube-proxy, so remove the `neg` annotation it that's not what you want.
|
||||
|
||||
The service:
|
||||
|
||||
@@ -454,13 +458,13 @@ The service:
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: argocd-server-external
|
||||
name: argocd-server
|
||||
namespace: argocd
|
||||
annotations:
|
||||
cloud.google.com/neg: '{"ingress": true}'
|
||||
cloud.google.com/backend-config: '{"ports": {"http":"argocd-backend-config"}}'
|
||||
spec:
|
||||
type: NodePort
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
@@ -528,7 +532,7 @@ kubectl -n argocd create secret tls secret-yourdomain-com \
|
||||
|
||||
And finally, to top it all, our Ingress. Note the reference to our frontend config, the service, and to the certificate secret:
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: argocd
|
||||
@@ -540,19 +544,16 @@ spec:
|
||||
- secretName: secret-yourdomain-com
|
||||
rules:
|
||||
- host: argocd.yourdomain.com
|
||||
http:
|
||||
paths:
|
||||
- path: /*
|
||||
backend:
|
||||
serviceName: argocd-server-external
|
||||
servicePort: http
|
||||
http:
|
||||
paths:
|
||||
- pathType: Prefix
|
||||
path: "/"
|
||||
backend:
|
||||
service:
|
||||
name: argocd-server
|
||||
port:
|
||||
number: 80
|
||||
```
|
||||
---
|
||||
!!! warning "Deprecation Warning"
|
||||
|
||||
Note that, according to this [deprecation guide](https://kubernetes.io/docs/reference/using-api/deprecation-guide/#ingress-v122), if you're using Kubernetes 1.22+, instead of `networking.k8s.io/v1beta1`, you should use `networking.k8s.io/v1`.
|
||||
|
||||
---
|
||||
|
||||
As you may know already, it can take some minutes to deploy the load balancer and become ready to accept connections. Once it's ready, get the public IP address for your Load Balancer, go to your DNS server (Google or third party) and point your domain or subdomain (i.e. argocd.yourdomain.com) to that IP address.
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
# Notifications
|
||||
|
||||
The notifications support is not bundled into the Argo CD itself. Instead of reinventing the wheel and implementing opinionated notifications system Argo CD leverages integrations
|
||||
with the third-party notification system. Following integrations are recommended:
|
||||
|
||||
* To monitor Argo CD performance or health state of managed applications use [Prometheus Metrics](./metrics.md) in combination with [Grafana](https://grafana.com/),
|
||||
[Alertmanager](https://prometheus.io/docs/alerting/alertmanager/).
|
||||
* To notify the end-users of Argo CD about events like application upgrades, user errors in application definition, etc use one of the following projects:
|
||||
* [ArgoCD Notifications](https://github.com/argoproj-labs/argocd-notifications) - Argo CD specific notification system that continuously monitors Argo CD applications
|
||||
and aims to integrate with various notification services such as Slack, SMTP, Telegram, Discord, etc.
|
||||
* [Argo Kube Notifier](https://github.com/argoproj-labs/argo-kube-notifier) - generic Kubernetes resource controller that allows monitoring any Kubernetes resource and sends a
|
||||
notification when the configured rule is met.
|
||||
* [Kube Watch](https://github.com/bitnami-labs/kubewatch) - a Kubernetes watcher that could publishes notification to Slack/hipchat/mattermost/flock channels. It watches the
|
||||
cluster for resource changes and notifies them through webhooks.
|
||||
@@ -0,0 +1,95 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-notifications-cm
|
||||
data:
|
||||
# Triggers define the condition when the notification should be sent and list of templates required to generate the message
|
||||
# Recipients can subscribe to the trigger and specify the required message template and destination notification service.
|
||||
trigger.on-sync-status-unknown: |
|
||||
- when: app.status.sync.status == 'Unknown'
|
||||
send: [my-custom-template]
|
||||
|
||||
# Optional 'oncePer' property ensure that notification is sent only once per specified field value
|
||||
# E.g. following is triggered once per sync revision
|
||||
trigger.on-deployed: |
|
||||
- when: app.status.operationState.phase in ['Succeeded'] and app.status.health.status == 'Healthy'
|
||||
oncePer: app.status.sync.revision
|
||||
send: [app-sync-succeeded]
|
||||
|
||||
# Templates are used to generate the notification template message
|
||||
template.my-custom-template: |
|
||||
message: |
|
||||
Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}.
|
||||
|
||||
# Templates might have notification service specific fields. E.g. slack message might include annotations
|
||||
template.my-custom-template-slack-template: |
|
||||
message: |
|
||||
Application {{.app.metadata.name}} sync is {{.app.status.sync.status}}.
|
||||
Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}.
|
||||
email:
|
||||
subject: Application {{.app.metadata.name}} sync status is {{.app.status.sync.status}}
|
||||
slack:
|
||||
attachments: |
|
||||
[{
|
||||
"title": "{{.app.metadata.name}}",
|
||||
"title_link": "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
|
||||
"color": "#18be52"
|
||||
}]
|
||||
|
||||
# Holds list of triggers that are used by default if trigger is not specified explicitly in the subscription
|
||||
defaultTriggers: |
|
||||
- on-sync-status-unknown
|
||||
|
||||
# Notification services are used to deliver message.
|
||||
# Service definition might reference values from argocd-notifications-secret Secret using $my-key format
|
||||
# Service format key is: service.<type>.<optional-custom-name>
|
||||
# Slack
|
||||
service.slack: |
|
||||
token: $slack-token
|
||||
username: <override-username> # optional username
|
||||
icon: <override-icon> # optional icon for the message (supports both emoij and url notation)
|
||||
|
||||
# Slack based notifier with name mattermost
|
||||
service.slack.mattermost: |
|
||||
apiURL: https://my-mattermost-url.com/api
|
||||
token: $slack-token
|
||||
username: <override-username> # optional username
|
||||
icon: <override-icon> # optional icon for the message (supports both emoij and url notation)
|
||||
|
||||
# Email
|
||||
service.email: |
|
||||
host: smtp.gmail.com
|
||||
port: 587
|
||||
from: <myemail>@gmail.com
|
||||
username: $email-username
|
||||
password: $email-password
|
||||
|
||||
# Opsgenie
|
||||
service.opsgenie: |
|
||||
apiUrl: api.opsgenie.com
|
||||
apiKeys:
|
||||
$opsgenie-team-id: $opsgenie-team-api-key
|
||||
...
|
||||
|
||||
# Telegram
|
||||
service.telegram: |
|
||||
token: $telegram-token
|
||||
|
||||
# Context holds list of variables that can be referenced in templates
|
||||
context: |
|
||||
argocdUrl: https://cd.apps.argoproj.io/
|
||||
|
||||
# Contains centrally managed global application subscriptions
|
||||
subscriptions: |
|
||||
# subscription for on-sync-status-unknown trigger notifications
|
||||
- recipients:
|
||||
- slack:test2
|
||||
- email:test@gmail.com
|
||||
triggers:
|
||||
- on-sync-status-unknown
|
||||
# subscription restricted to applications with matching labels only
|
||||
- recipients:
|
||||
- slack:test3
|
||||
selector: test=true
|
||||
triggers:
|
||||
- on-sync-status-unknown
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: argocd-notifications-secret
|
||||
stringData:
|
||||
slack-token: <my-slack-token>
|
||||
email-username: <myemail>@gmail.com
|
||||
email-password: <mypassword>
|
||||
|
||||
type: Opaque
|
||||
531
docs/operator-manual/notifications/catalog.md
Normal file
531
docs/operator-manual/notifications/catalog.md
Normal file
@@ -0,0 +1,531 @@
|
||||
# Triggers and Templates Catalog
|
||||
## Triggers
|
||||
| NAME | DESCRIPTION | TEMPLATE |
|
||||
|------------------------|---------------------------------------------------------------|-----------------------------------------------------|
|
||||
| on-created | Application is created. | [app-created](#app-created) |
|
||||
| on-deleted | Application is deleted. | [app-deleted](#app-deleted) |
|
||||
| on-deployed | Application is synced and healthy. Triggered once per commit. | [app-deployed](#app-deployed) |
|
||||
| on-health-degraded | Application has degraded | [app-health-degraded](#app-health-degraded) |
|
||||
| on-sync-failed | Application syncing has failed | [app-sync-failed](#app-sync-failed) |
|
||||
| on-sync-running | Application is being synced | [app-sync-running](#app-sync-running) |
|
||||
| on-sync-status-unknown | Application status is 'Unknown' | [app-sync-status-unknown](#app-sync-status-unknown) |
|
||||
| on-sync-succeeded | Application syncing has succeeded | [app-sync-succeeded](#app-sync-succeeded) |
|
||||
|
||||
## Templates
|
||||
### app-created
|
||||
**definition**:
|
||||
```yaml
|
||||
email:
|
||||
subject: Application {{.app.metadata.name}} has been created.
|
||||
message: Application {{.app.metadata.name}} has been created.
|
||||
teams:
|
||||
title: Application {{.app.metadata.name}} has been created.
|
||||
|
||||
```
|
||||
### app-deleted
|
||||
**definition**:
|
||||
```yaml
|
||||
email:
|
||||
subject: Application {{.app.metadata.name}} has been deleted.
|
||||
message: Application {{.app.metadata.name}} has been deleted.
|
||||
teams:
|
||||
title: Application {{.app.metadata.name}} has been deleted.
|
||||
|
||||
```
|
||||
### app-deployed
|
||||
**definition**:
|
||||
```yaml
|
||||
email:
|
||||
subject: New version of an application {{.app.metadata.name}} is up and running.
|
||||
message: |
|
||||
{{if eq .serviceType "slack"}}:white_check_mark:{{end}} Application {{.app.metadata.name}} is now running new version of deployments manifests.
|
||||
slack:
|
||||
attachments: |
|
||||
[{
|
||||
"title": "{{ .app.metadata.name}}",
|
||||
"title_link":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
|
||||
"color": "#18be52",
|
||||
"fields": [
|
||||
{
|
||||
"title": "Sync Status",
|
||||
"value": "{{.app.status.sync.status}}",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"title": "Repository",
|
||||
"value": "{{.app.spec.source.repoURL}}",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"title": "Revision",
|
||||
"value": "{{.app.status.sync.revision}}",
|
||||
"short": true
|
||||
}
|
||||
{{range $index, $c := .app.status.conditions}}
|
||||
{{if not $index}},{{end}}
|
||||
{{if $index}},{{end}}
|
||||
{
|
||||
"title": "{{$c.type}}",
|
||||
"value": "{{$c.message}}",
|
||||
"short": true
|
||||
}
|
||||
{{end}}
|
||||
]
|
||||
}]
|
||||
deliveryPolicy: Post
|
||||
groupingKey: ""
|
||||
notifyBroadcast: false
|
||||
teams:
|
||||
facts: |
|
||||
[{
|
||||
"name": "Sync Status",
|
||||
"value": "{{.app.status.sync.status}}"
|
||||
},
|
||||
{
|
||||
"name": "Repository",
|
||||
"value": "{{.app.spec.source.repoURL}}"
|
||||
},
|
||||
{
|
||||
"name": "Revision",
|
||||
"value": "{{.app.status.sync.revision}}"
|
||||
}
|
||||
{{range $index, $c := .app.status.conditions}}
|
||||
{{if not $index}},{{end}}
|
||||
{{if $index}},{{end}}
|
||||
{
|
||||
"name": "{{$c.type}}",
|
||||
"value": "{{$c.message}}"
|
||||
}
|
||||
{{end}}
|
||||
]
|
||||
potentialAction: |-
|
||||
[{
|
||||
"@type":"OpenUri",
|
||||
"name":"Operation Application",
|
||||
"targets":[{
|
||||
"os":"default",
|
||||
"uri":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"@type":"OpenUri",
|
||||
"name":"Open Repository",
|
||||
"targets":[{
|
||||
"os":"default",
|
||||
"uri":"{{.app.spec.source.repoURL | call .repo.RepoURLToHTTPS}}"
|
||||
}]
|
||||
}]
|
||||
themeColor: '#000080'
|
||||
title: New version of an application {{.app.metadata.name}} is up and running.
|
||||
|
||||
```
|
||||
### app-health-degraded
|
||||
**definition**:
|
||||
```yaml
|
||||
email:
|
||||
subject: Application {{.app.metadata.name}} has degraded.
|
||||
message: |
|
||||
{{if eq .serviceType "slack"}}:exclamation:{{end}} Application {{.app.metadata.name}} has degraded.
|
||||
Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}.
|
||||
slack:
|
||||
attachments: |
|
||||
[{
|
||||
"title": "{{ .app.metadata.name}}",
|
||||
"title_link": "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
|
||||
"color": "#f4c030",
|
||||
"fields": [
|
||||
{
|
||||
"title": "Health Status",
|
||||
"value": "{{.app.status.health.status}}",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"title": "Repository",
|
||||
"value": "{{.app.spec.source.repoURL}}",
|
||||
"short": true
|
||||
}
|
||||
{{range $index, $c := .app.status.conditions}}
|
||||
{{if not $index}},{{end}}
|
||||
{{if $index}},{{end}}
|
||||
{
|
||||
"title": "{{$c.type}}",
|
||||
"value": "{{$c.message}}",
|
||||
"short": true
|
||||
}
|
||||
{{end}}
|
||||
]
|
||||
}]
|
||||
deliveryPolicy: Post
|
||||
groupingKey: ""
|
||||
notifyBroadcast: false
|
||||
teams:
|
||||
facts: |
|
||||
[{
|
||||
"name": "Health Status",
|
||||
"value": "{{.app.status.health.status}}"
|
||||
},
|
||||
{
|
||||
"name": "Repository",
|
||||
"value": "{{.app.spec.source.repoURL}}"
|
||||
}
|
||||
{{range $index, $c := .app.status.conditions}}
|
||||
{{if not $index}},{{end}}
|
||||
{{if $index}},{{end}}
|
||||
{
|
||||
"name": "{{$c.type}}",
|
||||
"value": "{{$c.message}}"
|
||||
}
|
||||
{{end}}
|
||||
]
|
||||
potentialAction: |
|
||||
[{
|
||||
"@type":"OpenUri",
|
||||
"name":"Open Application",
|
||||
"targets":[{
|
||||
"os":"default",
|
||||
"uri":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"@type":"OpenUri",
|
||||
"name":"Open Repository",
|
||||
"targets":[{
|
||||
"os":"default",
|
||||
"uri":"{{.app.spec.source.repoURL | call .repo.RepoURLToHTTPS}}"
|
||||
}]
|
||||
}]
|
||||
themeColor: '#FF0000'
|
||||
title: Application {{.app.metadata.name}} has degraded.
|
||||
|
||||
```
|
||||
### app-sync-failed
|
||||
**definition**:
|
||||
```yaml
|
||||
email:
|
||||
subject: Failed to sync application {{.app.metadata.name}}.
|
||||
message: |
|
||||
{{if eq .serviceType "slack"}}:exclamation:{{end}} The sync operation of application {{.app.metadata.name}} has failed at {{.app.status.operationState.finishedAt}} with the following error: {{.app.status.operationState.message}}
|
||||
Sync operation details are available at: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true .
|
||||
slack:
|
||||
attachments: |
|
||||
[{
|
||||
"title": "{{ .app.metadata.name}}",
|
||||
"title_link":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
|
||||
"color": "#E96D76",
|
||||
"fields": [
|
||||
{
|
||||
"title": "Sync Status",
|
||||
"value": "{{.app.status.sync.status}}",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"title": "Repository",
|
||||
"value": "{{.app.spec.source.repoURL}}",
|
||||
"short": true
|
||||
}
|
||||
{{range $index, $c := .app.status.conditions}}
|
||||
{{if not $index}},{{end}}
|
||||
{{if $index}},{{end}}
|
||||
{
|
||||
"title": "{{$c.type}}",
|
||||
"value": "{{$c.message}}",
|
||||
"short": true
|
||||
}
|
||||
{{end}}
|
||||
]
|
||||
}]
|
||||
deliveryPolicy: Post
|
||||
groupingKey: ""
|
||||
notifyBroadcast: false
|
||||
teams:
|
||||
facts: |
|
||||
[{
|
||||
"name": "Sync Status",
|
||||
"value": "{{.app.status.sync.status}}"
|
||||
},
|
||||
{
|
||||
"name": "Failed at",
|
||||
"value": "{{.app.status.operationState.finishedAt}}"
|
||||
},
|
||||
{
|
||||
"name": "Repository",
|
||||
"value": "{{.app.spec.source.repoURL}}"
|
||||
}
|
||||
{{range $index, $c := .app.status.conditions}}
|
||||
{{if not $index}},{{end}}
|
||||
{{if $index}},{{end}}
|
||||
{
|
||||
"name": "{{$c.type}}",
|
||||
"value": "{{$c.message}}"
|
||||
}
|
||||
{{end}}
|
||||
]
|
||||
potentialAction: |-
|
||||
[{
|
||||
"@type":"OpenUri",
|
||||
"name":"Open Operation",
|
||||
"targets":[{
|
||||
"os":"default",
|
||||
"uri":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"@type":"OpenUri",
|
||||
"name":"Open Repository",
|
||||
"targets":[{
|
||||
"os":"default",
|
||||
"uri":"{{.app.spec.source.repoURL | call .repo.RepoURLToHTTPS}}"
|
||||
}]
|
||||
}]
|
||||
themeColor: '#FF0000'
|
||||
title: Failed to sync application {{.app.metadata.name}}.
|
||||
|
||||
```
|
||||
### app-sync-running
|
||||
**definition**:
|
||||
```yaml
|
||||
email:
|
||||
subject: Start syncing application {{.app.metadata.name}}.
|
||||
message: |
|
||||
The sync operation of application {{.app.metadata.name}} has started at {{.app.status.operationState.startedAt}}.
|
||||
Sync operation details are available at: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true .
|
||||
slack:
|
||||
attachments: |
|
||||
[{
|
||||
"title": "{{ .app.metadata.name}}",
|
||||
"title_link":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
|
||||
"color": "#0DADEA",
|
||||
"fields": [
|
||||
{
|
||||
"title": "Sync Status",
|
||||
"value": "{{.app.status.sync.status}}",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"title": "Repository",
|
||||
"value": "{{.app.spec.source.repoURL}}",
|
||||
"short": true
|
||||
}
|
||||
{{range $index, $c := .app.status.conditions}}
|
||||
{{if not $index}},{{end}}
|
||||
{{if $index}},{{end}}
|
||||
{
|
||||
"title": "{{$c.type}}",
|
||||
"value": "{{$c.message}}",
|
||||
"short": true
|
||||
}
|
||||
{{end}}
|
||||
]
|
||||
}]
|
||||
deliveryPolicy: Post
|
||||
groupingKey: ""
|
||||
notifyBroadcast: false
|
||||
teams:
|
||||
facts: |
|
||||
[{
|
||||
"name": "Sync Status",
|
||||
"value": "{{.app.status.sync.status}}"
|
||||
},
|
||||
{
|
||||
"name": "Started at",
|
||||
"value": "{{.app.status.operationState.startedAt}}"
|
||||
},
|
||||
{
|
||||
"name": "Repository",
|
||||
"value": "{{.app.spec.source.repoURL}}"
|
||||
}
|
||||
{{range $index, $c := .app.status.conditions}}
|
||||
{{if not $index}},{{end}}
|
||||
{{if $index}},{{end}}
|
||||
{
|
||||
"name": "{{$c.type}}",
|
||||
"value": "{{$c.message}}"
|
||||
}
|
||||
{{end}}
|
||||
]
|
||||
potentialAction: |-
|
||||
[{
|
||||
"@type":"OpenUri",
|
||||
"name":"Open Operation",
|
||||
"targets":[{
|
||||
"os":"default",
|
||||
"uri":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"@type":"OpenUri",
|
||||
"name":"Open Repository",
|
||||
"targets":[{
|
||||
"os":"default",
|
||||
"uri":"{{.app.spec.source.repoURL | call .repo.RepoURLToHTTPS}}"
|
||||
}]
|
||||
}]
|
||||
title: Start syncing application {{.app.metadata.name}}.
|
||||
|
||||
```
|
||||
### app-sync-status-unknown
|
||||
**definition**:
|
||||
```yaml
|
||||
email:
|
||||
subject: Application {{.app.metadata.name}} sync status is 'Unknown'
|
||||
message: |
|
||||
{{if eq .serviceType "slack"}}:exclamation:{{end}} Application {{.app.metadata.name}} sync is 'Unknown'.
|
||||
Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}.
|
||||
{{if ne .serviceType "slack"}}
|
||||
{{range $c := .app.status.conditions}}
|
||||
* {{$c.message}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
slack:
|
||||
attachments: |
|
||||
[{
|
||||
"title": "{{ .app.metadata.name}}",
|
||||
"title_link":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
|
||||
"color": "#E96D76",
|
||||
"fields": [
|
||||
{
|
||||
"title": "Sync Status",
|
||||
"value": "{{.app.status.sync.status}}",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"title": "Repository",
|
||||
"value": "{{.app.spec.source.repoURL}}",
|
||||
"short": true
|
||||
}
|
||||
{{range $index, $c := .app.status.conditions}}
|
||||
{{if not $index}},{{end}}
|
||||
{{if $index}},{{end}}
|
||||
{
|
||||
"title": "{{$c.type}}",
|
||||
"value": "{{$c.message}}",
|
||||
"short": true
|
||||
}
|
||||
{{end}}
|
||||
]
|
||||
}]
|
||||
deliveryPolicy: Post
|
||||
groupingKey: ""
|
||||
notifyBroadcast: false
|
||||
teams:
|
||||
facts: |
|
||||
[{
|
||||
"name": "Sync Status",
|
||||
"value": "{{.app.status.sync.status}}"
|
||||
},
|
||||
{
|
||||
"name": "Repository",
|
||||
"value": "{{.app.spec.source.repoURL}}"
|
||||
}
|
||||
{{range $index, $c := .app.status.conditions}}
|
||||
{{if not $index}},{{end}}
|
||||
{{if $index}},{{end}}
|
||||
{
|
||||
"name": "{{$c.type}}",
|
||||
"value": "{{$c.message}}"
|
||||
}
|
||||
{{end}}
|
||||
]
|
||||
potentialAction: |-
|
||||
[{
|
||||
"@type":"OpenUri",
|
||||
"name":"Open Application",
|
||||
"targets":[{
|
||||
"os":"default",
|
||||
"uri":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"@type":"OpenUri",
|
||||
"name":"Open Repository",
|
||||
"targets":[{
|
||||
"os":"default",
|
||||
"uri":"{{.app.spec.source.repoURL | call .repo.RepoURLToHTTPS}}"
|
||||
}]
|
||||
}]
|
||||
title: Application {{.app.metadata.name}} sync status is 'Unknown'
|
||||
|
||||
```
|
||||
### app-sync-succeeded
|
||||
**definition**:
|
||||
```yaml
|
||||
email:
|
||||
subject: Application {{.app.metadata.name}} has been successfully synced.
|
||||
message: |
|
||||
{{if eq .serviceType "slack"}}:white_check_mark:{{end}} Application {{.app.metadata.name}} has been successfully synced at {{.app.status.operationState.finishedAt}}.
|
||||
Sync operation details are available at: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true .
|
||||
slack:
|
||||
attachments: |
|
||||
[{
|
||||
"title": "{{ .app.metadata.name}}",
|
||||
"title_link":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
|
||||
"color": "#18be52",
|
||||
"fields": [
|
||||
{
|
||||
"title": "Sync Status",
|
||||
"value": "{{.app.status.sync.status}}",
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"title": "Repository",
|
||||
"value": "{{.app.spec.source.repoURL}}",
|
||||
"short": true
|
||||
}
|
||||
{{range $index, $c := .app.status.conditions}}
|
||||
{{if not $index}},{{end}}
|
||||
{{if $index}},{{end}}
|
||||
{
|
||||
"title": "{{$c.type}}",
|
||||
"value": "{{$c.message}}",
|
||||
"short": true
|
||||
}
|
||||
{{end}}
|
||||
]
|
||||
}]
|
||||
deliveryPolicy: Post
|
||||
groupingKey: ""
|
||||
notifyBroadcast: false
|
||||
teams:
|
||||
facts: |
|
||||
[{
|
||||
"name": "Sync Status",
|
||||
"value": "{{.app.status.sync.status}}"
|
||||
},
|
||||
{
|
||||
"name": "Synced at",
|
||||
"value": "{{.app.status.operationState.finishedAt}}"
|
||||
},
|
||||
{
|
||||
"name": "Repository",
|
||||
"value": "{{.app.spec.source.repoURL}}"
|
||||
}
|
||||
{{range $index, $c := .app.status.conditions}}
|
||||
{{if not $index}},{{end}}
|
||||
{{if $index}},{{end}}
|
||||
{
|
||||
"name": "{{$c.type}}",
|
||||
"value": "{{$c.message}}"
|
||||
}
|
||||
{{end}}
|
||||
]
|
||||
potentialAction: |-
|
||||
[{
|
||||
"@type":"OpenUri",
|
||||
"name":"Operation Details",
|
||||
"targets":[{
|
||||
"os":"default",
|
||||
"uri":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"@type":"OpenUri",
|
||||
"name":"Open Repository",
|
||||
"targets":[{
|
||||
"os":"default",
|
||||
"uri":"{{.app.spec.source.repoURL | call .repo.RepoURLToHTTPS}}"
|
||||
}]
|
||||
}]
|
||||
themeColor: '#000080'
|
||||
title: Application {{.app.metadata.name}} has been successfully synced
|
||||
|
||||
```
|
||||
79
docs/operator-manual/notifications/functions.md
Normal file
79
docs/operator-manual/notifications/functions.md
Normal file
@@ -0,0 +1,79 @@
|
||||
### **time**
|
||||
Time related functions.
|
||||
|
||||
<hr>
|
||||
**`time.Now() Time`**
|
||||
|
||||
Executes function built-in Golang [time.Now](https://golang.org/pkg/time/#Now) function. Returns an instance of
|
||||
Golang [Time](https://golang.org/pkg/time/#Time).
|
||||
|
||||
<hr>
|
||||
**`time.Parse(val string) Time`**
|
||||
|
||||
Parses specified string using RFC3339 layout. Returns an instance of Golang [Time](https://golang.org/pkg/time/#Time).
|
||||
|
||||
### **strings**
|
||||
String related functions.
|
||||
|
||||
<hr>
|
||||
**`strings.ReplaceAll() string`**
|
||||
|
||||
Executes function built-in Golang [strings.ReplaceAll](https://pkg.go.dev/strings#ReplaceAll) function.
|
||||
|
||||
<hr>
|
||||
**`strings.ToUpper() string`**
|
||||
|
||||
Executes function built-in Golang [strings.ToUpper](https://pkg.go.dev/strings#ToUpper) function.
|
||||
|
||||
<hr>
|
||||
**`strings.ToLower() string`**
|
||||
|
||||
Executes function built-in Golang [strings.ToLower](https://pkg.go.dev/strings#ToLower) function.
|
||||
|
||||
### **sync**
|
||||
|
||||
<hr>
|
||||
**`sync.GetInfoItem(app map, name string) string`**
|
||||
Returns the `info` item value by given name stored in the Argo CD App sync operation.
|
||||
|
||||
### **repo**
|
||||
Functions that provide additional information about Application source repository.
|
||||
<hr>
|
||||
**`repo.RepoURLToHTTPS(url string) string`**
|
||||
|
||||
Transforms given GIT URL into HTTPs format.
|
||||
|
||||
<hr>
|
||||
**`repo.FullNameByRepoURL(url string) string`**
|
||||
|
||||
Returns repository URL full name `(<owner>/<repoName>)`. Currently supports only Github, Gitlab and Bitbucket.
|
||||
|
||||
<hr>
|
||||
**`repo.GetCommitMetadata(sha string) CommitMetadata`**
|
||||
|
||||
Returns commit metadata. The commit must belong to the application source repository. `CommitMetadata` fields:
|
||||
|
||||
* `Message string` commit message
|
||||
* `Author string` - commit author
|
||||
* `Date time.Time` - commit creation date
|
||||
* `Tags []string` - Associated tags
|
||||
|
||||
<hr>
|
||||
**`repo.GetAppDetails() AppDetail`**
|
||||
|
||||
Returns application details. `AppDetail` fields:
|
||||
|
||||
* `Type string` - AppDetail type
|
||||
* `Helm HelmAppSpec` - Helm details
|
||||
* Fields :
|
||||
* `Name string`
|
||||
* `ValueFiles []string`
|
||||
* `Parameters []*v1alpha1.HelmParameter`
|
||||
* `Values string`
|
||||
* `FileParameters []*v1alpha1.HelmFileParameter`
|
||||
* Methods :
|
||||
* `GetParameterValueByName(Name string)` Retrieve value by name in Parameters field
|
||||
* `GetFileParameterPathByName(Name string)` Retrieve path by name in FileParameters field
|
||||
* `Ksonnet *apiclient.KsonnetAppSpec` - Ksonnet details
|
||||
* `Kustomize *apiclient.KustomizeAppSpec` - Kustomize details
|
||||
* `Directory *apiclient.DirectoryAppSpec` - Directory details
|
||||
305
docs/operator-manual/notifications/grafana-dashboard.json
Normal file
305
docs/operator-manual/notifications/grafana-dashboard.json
Normal file
@@ -0,0 +1,305 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": "-- Grafana --",
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"id": 4,
|
||||
"iteration": 1589141097815,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "$datasource",
|
||||
"fill": 1,
|
||||
"fillGradient": 0,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"hiddenSeries": false,
|
||||
"id": 4,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"nullPointMode": "null",
|
||||
"options": {
|
||||
"dataLinks": []
|
||||
},
|
||||
"percentage": false,
|
||||
"pointradius": 2,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(increase(argocd_notifications_trigger_eval_total[$interval])) by (notifier)",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "Trigger Evaluations",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "$datasource",
|
||||
"fill": 1,
|
||||
"fillGradient": 0,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"hiddenSeries": false,
|
||||
"id": 2,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"nullPointMode": "null",
|
||||
"options": {
|
||||
"dataLinks": []
|
||||
},
|
||||
"percentage": false,
|
||||
"pointradius": 2,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(increase(argocd_notifications_deliveries_total[$interval])) by (notifier)",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeRegions": [],
|
||||
"timeShift": null,
|
||||
"title": "Notification deliveries",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"schemaVersion": 21,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"current": {
|
||||
"text": "Prometheus",
|
||||
"value": "Prometheus"
|
||||
},
|
||||
"hide": 0,
|
||||
"includeAll": false,
|
||||
"label": null,
|
||||
"multi": false,
|
||||
"name": "datasource",
|
||||
"options": [],
|
||||
"query": "prometheus",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"type": "datasource"
|
||||
},
|
||||
{
|
||||
"auto": true,
|
||||
"auto_count": 30,
|
||||
"auto_min": "10s",
|
||||
"current": {
|
||||
"selected": false,
|
||||
"text": "1m",
|
||||
"value": "1m"
|
||||
},
|
||||
"hide": 0,
|
||||
"label": null,
|
||||
"name": "interval",
|
||||
"options": [
|
||||
{
|
||||
"selected": false,
|
||||
"text": "auto",
|
||||
"value": "$__auto_interval_interval"
|
||||
},
|
||||
{
|
||||
"selected": true,
|
||||
"text": "1m",
|
||||
"value": "1m"
|
||||
},
|
||||
{
|
||||
"selected": false,
|
||||
"text": "5m",
|
||||
"value": "5m"
|
||||
},
|
||||
{
|
||||
"selected": false,
|
||||
"text": "10m",
|
||||
"value": "10m"
|
||||
},
|
||||
{
|
||||
"selected": false,
|
||||
"text": "30m",
|
||||
"value": "30m"
|
||||
},
|
||||
{
|
||||
"selected": false,
|
||||
"text": "1h",
|
||||
"value": "1h"
|
||||
},
|
||||
{
|
||||
"selected": false,
|
||||
"text": "2h",
|
||||
"value": "2h"
|
||||
},
|
||||
{
|
||||
"selected": false,
|
||||
"text": "4h",
|
||||
"value": "4h"
|
||||
},
|
||||
{
|
||||
"selected": false,
|
||||
"text": "8h",
|
||||
"value": "8h"
|
||||
}
|
||||
],
|
||||
"query": "1m,5m,10m,30m,1h,2h,4h,8h",
|
||||
"refresh": 2,
|
||||
"skipUrlSync": false,
|
||||
"type": "interval"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-15m",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
]
|
||||
},
|
||||
"timezone": "",
|
||||
"title": "Argo CD Notifications",
|
||||
"uid": "3qXvXigMz",
|
||||
"version": 1
|
||||
}
|
||||
46
docs/operator-manual/notifications/index.md
Normal file
46
docs/operator-manual/notifications/index.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Overview
|
||||
|
||||
Argo CD Notifications continuously monitors Argo CD applications and provides a flexible way to notify
|
||||
users about important changes in the application state. Using a flexible mechanism of
|
||||
[triggers](triggers.md) and [templates](templates.md) you can configure when the notification should be sent as
|
||||
well as notification content. Argo CD Notifications includes the [catalog](catalog.md) of useful triggers and templates.
|
||||
So you can just use them instead of reinventing new ones.
|
||||
|
||||
## Getting Started
|
||||
|
||||
* Install Triggers and Templates from the catalog
|
||||
|
||||
```
|
||||
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/notifications_catalog/install.yaml
|
||||
```
|
||||
|
||||
* Add Email username and password token to `argocd-notifications-secret` secret
|
||||
|
||||
```bash
|
||||
export EMAIL_USER=<your-username>
|
||||
export PASSWORD=<your-password>
|
||||
kubectl apply -n argocd -f - << EOF
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: argocd-notifications-secret
|
||||
stringData:
|
||||
email-username: $EMAIL_USER
|
||||
email-password: $PASSWORD
|
||||
type: Opaque
|
||||
EOF
|
||||
```
|
||||
|
||||
* Register Email notification service
|
||||
|
||||
```bash
|
||||
kubectl patch cm argocd-notifications-cm -n argocd --type merge -p '{"data": {"service.email.gmail": "{ username: $email-username, password: $email-password, host: smtp.gmail.com, port: 465, from: $email-username }" }}'
|
||||
```
|
||||
|
||||
* Subscribe to notifications by adding the `notifications.argoproj.io/subscribe.on-sync-succeeded.slack` annotation to the Argo CD application or project:
|
||||
|
||||
```bash
|
||||
kubectl patch app <my-app> -n argocd -p '{"metadata": {"annotations": {"notifications.argoproj.io/subscribe.on-sync-succeeded.slack":"<my-channel>"}}}' --type merge
|
||||
```
|
||||
|
||||
Try syncing and application and get the notification once sync is completed.
|
||||
30
docs/operator-manual/notifications/monitoring.md
Normal file
30
docs/operator-manual/notifications/monitoring.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Monitoring
|
||||
|
||||
The Argo CD Notification controller serves Prometheus metrics on port 9001.
|
||||
|
||||
!!! note
|
||||
Metrics port might be changed using the `--metrics-port` flag in `argocd-notifications-controller` deployment.
|
||||
|
||||
## Metrics
|
||||
The following metrics are available:
|
||||
|
||||
### `argocd_notifications_deliveries_total`
|
||||
|
||||
Number of delivered notifications.
|
||||
Labels:
|
||||
|
||||
* `template` - notification template name
|
||||
* `notifier` - notification service name
|
||||
* `succeeded` - flag that indicates if notification was successfully sent or failed.
|
||||
|
||||
### `argocd_notifications_trigger_eval_total`
|
||||
|
||||
Number of trigger evaluations.
|
||||
Labels:
|
||||
|
||||
* `name` - trigger name
|
||||
* `triggered` - flag that indicates if trigger condition returned true of false.
|
||||
|
||||
# Examples:
|
||||
|
||||
* Grafana Dashboard: [grafana-dashboard.json](grafana-dashboard.json)
|
||||
164
docs/operator-manual/notifications/services/alertmanager.md
Executable file
164
docs/operator-manual/notifications/services/alertmanager.md
Executable file
@@ -0,0 +1,164 @@
|
||||
# Alertmanager
|
||||
|
||||
## Parameters
|
||||
|
||||
The notification service is used to push events to [Alertmanager](https://github.com/prometheus/alertmanager), and the following settings need to be specified:
|
||||
|
||||
* `targets` - the alertmanager service address, array type
|
||||
* `scheme` - optional, default is "http", e.g. http or https
|
||||
* `apiPath` - optional, default is "/api/v2/alerts"
|
||||
* `insecureSkipVerify` - optional, default is "false", when scheme is https whether to skip the verification of ca
|
||||
* `basicAuth` - optional, server auth
|
||||
* `bearerToken` - optional, server auth
|
||||
* `timeout` - optional, the timeout in seconds used when sending alerts, default is "3 seconds"
|
||||
|
||||
`basicAuth` or `bearerToken` is used for authentication, you can choose one. If the two are set at the same time, `basicAuth` takes precedence over `bearerToken`.
|
||||
|
||||
## Example
|
||||
|
||||
### Prometheus Alertmanager config
|
||||
|
||||
```yaml
|
||||
global:
|
||||
resolve_timeout: 5m
|
||||
|
||||
route:
|
||||
group_by: ['alertname']
|
||||
group_wait: 10s
|
||||
group_interval: 10s
|
||||
repeat_interval: 1h
|
||||
receiver: 'default'
|
||||
receivers:
|
||||
- name: 'default'
|
||||
webhook_configs:
|
||||
- send_resolved: false
|
||||
url: 'http://10.5.39.39:10080/api/alerts/webhook'
|
||||
```
|
||||
|
||||
You should turn off "send_resolved" or you will receive unnecessary recovery notifications after "resolve_timeout".
|
||||
|
||||
### Send one alertmanager without auth
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: <config-map-name>
|
||||
data:
|
||||
service.alertmanager: |
|
||||
targets:
|
||||
- 10.5.39.39:9093
|
||||
```
|
||||
|
||||
### Send alertmanager cluster with custom api path
|
||||
|
||||
If your alertmanager has changed the default api, you can customize "apiPath".
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: <config-map-name>
|
||||
data:
|
||||
service.alertmanager: |
|
||||
targets:
|
||||
- 10.5.39.39:443
|
||||
scheme: https
|
||||
apiPath: /api/events
|
||||
insecureSkipVerify: true
|
||||
```
|
||||
|
||||
### Send high availability alertmanager with auth
|
||||
|
||||
Store auth token in `argocd-notifications-secret` Secret and use configure in `argocd-notifications-cm` ConfigMap.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: <secret-name>
|
||||
stringData:
|
||||
alertmanager-username: <username>
|
||||
alertmanager-password: <password>
|
||||
alertmanager-bearer-token: <token>
|
||||
```
|
||||
|
||||
- with basicAuth
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: <config-map-name>
|
||||
data:
|
||||
service.alertmanager: |
|
||||
targets:
|
||||
- 10.5.39.39:19093
|
||||
- 10.5.39.39:29093
|
||||
- 10.5.39.39:39093
|
||||
scheme: https
|
||||
apiPath: /api/v2/alerts
|
||||
insecureSkipVerify: true
|
||||
basicAuth:
|
||||
username: $alertmanager-username
|
||||
password: $alertmanager-password
|
||||
```
|
||||
|
||||
- with bearerToken
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: <config-map-name>
|
||||
data:
|
||||
service.alertmanager: |
|
||||
targets:
|
||||
- 10.5.39.39:19093
|
||||
- 10.5.39.39:29093
|
||||
- 10.5.39.39:39093
|
||||
scheme: https
|
||||
apiPath: /api/v2/alerts
|
||||
insecureSkipVerify: true
|
||||
bearerToken: $alertmanager-bearer-token
|
||||
```
|
||||
|
||||
## Templates
|
||||
|
||||
* `labels` - at least one label pair required, implement different notification strategies according to alertmanager routing
|
||||
* `annotations` - optional, specifies a set of information labels, which can be used to store longer additional information, but only for display
|
||||
* `generatorURL` - optional, default is '{{.app.spec.source.repoURL}}', backlink used to identify the entity that caused this alert in the client
|
||||
|
||||
the `label` or `annotations` or `generatorURL` values can be templated.
|
||||
|
||||
```yaml
|
||||
context: |
|
||||
argocdUrl: https://example.com/argocd
|
||||
|
||||
template.app-deployed: |
|
||||
message: Application {{.app.metadata.name}} has been healthy.
|
||||
alertmanager:
|
||||
labels:
|
||||
fault_priority: "P5"
|
||||
event_bucket: "deploy"
|
||||
event_status: "succeed"
|
||||
recipient: "{{.recipient}}"
|
||||
annotations:
|
||||
application: '<a href="{{.context.argocdUrl}}/applications/{{.app.metadata.name}}">{{.app.metadata.name}}</a>'
|
||||
author: "{{(call .repo.GetCommitMetadata .app.status.sync.revision).Author}}"
|
||||
message: "{{(call .repo.GetCommitMetadata .app.status.sync.revision).Message}}"
|
||||
```
|
||||
|
||||
You can do targeted push on [Alertmanager](https://github.com/prometheus/alertmanager) according to labels.
|
||||
|
||||
```yaml
|
||||
template.app-deployed: |
|
||||
message: Application {{.app.metadata.name}} has been healthy.
|
||||
alertmanager:
|
||||
labels:
|
||||
alertname: app-deployed
|
||||
fault_priority: "P5"
|
||||
event_bucket: "deploy"
|
||||
```
|
||||
|
||||
There is a special label `alertname`. If you don’t set its value, it will be equal to the template name by default.
|
||||
63
docs/operator-manual/notifications/services/email.md
Executable file
63
docs/operator-manual/notifications/services/email.md
Executable file
@@ -0,0 +1,63 @@
|
||||
# Email
|
||||
|
||||
## Parameters
|
||||
|
||||
The Email notification service sends email notifications using SMTP protocol and requires specifying the following settings:
|
||||
|
||||
* `host` - the SMTP server host name
|
||||
* `port` - the SMTP server port
|
||||
* `username` - username
|
||||
* `password` - password
|
||||
* `from` - from email address
|
||||
* `html` - optional bool, true or false
|
||||
* `insecure_skip_verify` - optional bool, true or false
|
||||
|
||||
## Example
|
||||
|
||||
The following snippet contains sample Gmail service configuration:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: <config-map-name>
|
||||
data:
|
||||
service.email.gmail: |
|
||||
username: $email-username
|
||||
password: $email-password
|
||||
host: smtp.gmail.com
|
||||
port: 465
|
||||
from: $email-username
|
||||
```
|
||||
|
||||
Without authentication:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: <config-map-name>
|
||||
data:
|
||||
service.email.example: |
|
||||
host: smtp.example.com
|
||||
port: 587
|
||||
from: $email-username
|
||||
```
|
||||
|
||||
## Template
|
||||
|
||||
Notification templates support specifying subject for email notifications:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: <config-map-name>
|
||||
data:
|
||||
template.app-sync-succeeded: |
|
||||
email:
|
||||
subject: Application {{.app.metadata.name}} has been successfully synced.
|
||||
message: |
|
||||
{{if eq .serviceType "slack"}}:white_check_mark:{{end}} Application {{.app.metadata.name}} has been successfully synced at {{.app.status.operationState.finishedAt}}.
|
||||
Sync operation details are available at: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true .
|
||||
```
|
||||
72
docs/operator-manual/notifications/services/github.md
Executable file
72
docs/operator-manual/notifications/services/github.md
Executable file
@@ -0,0 +1,72 @@
|
||||
# GitHub
|
||||
|
||||
## Parameters
|
||||
|
||||
The GitHub notification service changes commit status using [GitHub Apps](https://docs.github.com/en/developers/apps) and requires specifying the following settings:
|
||||
|
||||
* `appID` - the app id
|
||||
* `installationID` - the app installation id
|
||||
* `privateKey` - the app private key
|
||||
* `enterpriseBaseURL` - optional URL, e.g. https://git.example.com/
|
||||
|
||||
## Configuration
|
||||
|
||||
1. Create a GitHub Apps using https://github.com/settings/apps/new
|
||||
2. Change repository permissions to enable write commit statuses
|
||||

|
||||
3. Generate a private key, and download it automatically
|
||||

|
||||
4. Install app to account
|
||||
5. Store privateKey in `argocd-notifications-secret` Secret and configure GitHub integration
|
||||
in `argocd-notifications-cm` ConfigMap
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: <config-map-name>
|
||||
data:
|
||||
service.github: |
|
||||
appID: <app-id>
|
||||
installationID: <installation-id>
|
||||
privateKey: $github-privateKey
|
||||
```
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: <secret-name>
|
||||
stringData:
|
||||
github-privateKey: |
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
(snip)
|
||||
-----END RSA PRIVATE KEY-----
|
||||
```
|
||||
|
||||
6. Create subscription for your GitHub integration
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
annotations:
|
||||
notifications.argoproj.io/subscribe.<trigger-name>.github: ""
|
||||
```
|
||||
|
||||
## Templates
|
||||
|
||||

|
||||
|
||||
If the message is set to 140 characters or more, it will be truncate.
|
||||
|
||||
```yaml
|
||||
template.app-deployed: |
|
||||
message: |
|
||||
Application {{.app.metadata.name}} is now running new version of deployments manifests.
|
||||
github:
|
||||
status:
|
||||
state: success
|
||||
label: "continuous-delivery/{{.app.metadata.name}}"
|
||||
targetURL: "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true"
|
||||
```
|
||||
81
docs/operator-manual/notifications/services/googlechat.md
Executable file
81
docs/operator-manual/notifications/services/googlechat.md
Executable file
@@ -0,0 +1,81 @@
|
||||
# Google Chat
|
||||
|
||||
## Parameters
|
||||
|
||||
The Google Chat notification service send message notifications to a google chat webhook. This service uses the following settings:
|
||||
|
||||
* `webhooks` - a map of the form `webhookName: webhookUrl`
|
||||
|
||||
## Configuration
|
||||
|
||||
1. Open `Google chat` and go to the space to which you want to send messages
|
||||
2. From the menu at the top of the page, select **Configure Webhooks**
|
||||
3. Under **Incoming Webhooks**, click **Add Webhook**
|
||||
4. Give a name to the webhook, optionally add an image and click **Save**
|
||||
5. Copy the URL next to your webhook
|
||||
6. Store the URL in `argocd-notification-secret` and declare it in `argocd-notifications-cm`
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: <config-map-name>
|
||||
data:
|
||||
service.googlechat: |
|
||||
webhooks:
|
||||
spaceName: $space-webhook-url
|
||||
```
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: <secret-name>
|
||||
stringData:
|
||||
space-webhook-url: https://chat.googleapis.com/v1/spaces/<space_id>/messages?key=<key>&token=<token>
|
||||
```
|
||||
|
||||
6. Create a subscription for your space
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
annotations:
|
||||
notifications.argoproj.io/subscribe.on-sync-succeeded.googlechat: spaceName
|
||||
```
|
||||
|
||||
## Templates
|
||||
|
||||
You can send [simple text](https://developers.google.com/chat/reference/message-formats/basic) or [card messages](https://developers.google.com/chat/reference/message-formats/cards) to a Google Chat space. A simple text message template can be defined as follows:
|
||||
|
||||
```yaml
|
||||
template.app-sync-succeeded: |
|
||||
message: The app {{ .app.metadata.name }} has succesfully synced!
|
||||
```
|
||||
|
||||
A card message can be defined as follows:
|
||||
|
||||
```yaml
|
||||
template.app-sync-succeeded: |
|
||||
googlechat:
|
||||
cards: |
|
||||
- header:
|
||||
title: ArgoCD Bot Notification
|
||||
sections:
|
||||
- widgets:
|
||||
- textParagraph:
|
||||
text: The app {{ .app.metadata.name }} has succesfully synced!
|
||||
- widgets:
|
||||
- keyValue:
|
||||
topLabel: Repository
|
||||
content: {{ call .repo.RepoURLToHTTPS .app.spec.source.repoURL }}
|
||||
- keyValue:
|
||||
topLabel: Revision
|
||||
content: {{ .app.spec.source.targetRevision }}
|
||||
- keyValue:
|
||||
topLabel: Author
|
||||
content: {{ (call .repo.GetCommitMetadata .app.status.sync.revision).Author }}
|
||||
```
|
||||
|
||||
The card message can be written in JSON too.
|
||||
45
docs/operator-manual/notifications/services/grafana.md
Executable file
45
docs/operator-manual/notifications/services/grafana.md
Executable file
@@ -0,0 +1,45 @@
|
||||
# Grafana
|
||||
|
||||
To be able to create Grafana annotation with argocd-notifications you have to create an [API Key](https://grafana.com/docs/grafana/latest/http_api/auth/#create-api-key) inside your [Grafana](https://grafana.com).
|
||||
|
||||

|
||||
|
||||
1. Login to your Grafana instance as `admin`
|
||||
2. On the left menu, go to Configuration / API Keys
|
||||
3. Click "Add API Key"
|
||||
4. Fill the Key with name `ArgoCD Notification`, role `Editor` and Time to Live `10y` (for example)
|
||||
5. Click on Add button
|
||||
6. Store apiKey in `argocd-notifications-secret` Secret and Copy your API Key and define it in `argocd-notifications-cm` ConfigMap
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: <config-map-name>
|
||||
data:
|
||||
service.grafana: |
|
||||
apiUrl: https://grafana.example.com/api
|
||||
apiKey: $grafana-api-key
|
||||
```
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: <secret-name>
|
||||
stringData:
|
||||
grafana-api-key: api-key
|
||||
```
|
||||
|
||||
7. Create subscription for your Grafana integration
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
annotations:
|
||||
notifications.argoproj.io/subscribe.<trigger-name>.grafana: tag1|tag2 # list of tags separated with |
|
||||
```
|
||||
|
||||
8. Change the annotations settings
|
||||

|
||||
78
docs/operator-manual/notifications/services/mattermost.md
Executable file
78
docs/operator-manual/notifications/services/mattermost.md
Executable file
@@ -0,0 +1,78 @@
|
||||
# Mattermost
|
||||
|
||||
## Parameters
|
||||
|
||||
* `apiURL` - the server url, e.g. https://mattermost.example.com
|
||||
* `token` - the bot token
|
||||
* `insecureSkipVerify` - optional bool, true or false
|
||||
|
||||
## Configuration
|
||||
|
||||
1. Create a bot account and copy token after creating it
|
||||

|
||||
2. Invite team
|
||||

|
||||
3. Store token in `argocd-notifications-secret` Secret and configure Mattermost integration
|
||||
in `argocd-notifications-cm` ConfigMap
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: <config-map-name>
|
||||
data:
|
||||
service.mattermost: |
|
||||
apiURL: <api-url>
|
||||
token: $mattermost-token
|
||||
```
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: <secret-name>
|
||||
stringData:
|
||||
mattermost-token: token
|
||||
```
|
||||
|
||||
4. Copy channel id
|
||||

|
||||
|
||||
5. Create subscription for your Mattermost integration
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
annotations:
|
||||
notifications.argoproj.io/subscribe.<trigger-name>.mattermost: <channel-id>
|
||||
```
|
||||
|
||||
## Templates
|
||||
|
||||

|
||||
|
||||
You can reuse the template of slack.
|
||||
Mattermost is compatible with attachments of Slack. See [Mattermost Integration Guide](https://docs.mattermost.com/developer/message-attachments.html).
|
||||
|
||||
```yaml
|
||||
template.app-deployed: |
|
||||
message: |
|
||||
Application {{.app.metadata.name}} is now running new version of deployments manifests.
|
||||
mattermost:
|
||||
attachments: |
|
||||
[{
|
||||
"title": "{{.app.metadata.name}}",
|
||||
"title_link": "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
|
||||
"color": "#18be52",
|
||||
"fields": [{
|
||||
"title": "Sync Status",
|
||||
"value": "{{.app.status.sync.status}}",
|
||||
"short": true
|
||||
}, {
|
||||
"title": "Repository",
|
||||
"value": "{{.app.spec.source.repoURL}}",
|
||||
"short": true
|
||||
}]
|
||||
}]
|
||||
```
|
||||
28
docs/operator-manual/notifications/services/opsgenie.md
Executable file
28
docs/operator-manual/notifications/services/opsgenie.md
Executable file
@@ -0,0 +1,28 @@
|
||||
# Opsgenie
|
||||
|
||||
To be able to send notifications with argocd-notifications you have to create an [API Integration](https://docs.opsgenie.com/docs/integrations-overview) inside your [Opsgenie Team](https://docs.opsgenie.com/docs/teams).
|
||||
|
||||
1. Login to Opsgenie at https://app.opsgenie.com or https://app.eu.opsgenie.com (if you have an account in the european union)
|
||||
2. Make sure you already have a team, if not follow this guide https://docs.opsgenie.com/docs/teams
|
||||
3. Click "Teams" in the Menu on the left
|
||||
4. Select the team that you want to notify
|
||||
5. In the teams configuration menu select "Integrations"
|
||||
6. click "Add Integration" in the top right corner
|
||||
7. Select "API" integration
|
||||
8. Give your integration a name, copy the "API key" and safe it somewhere for later
|
||||
9. Make sure the checkboxes for "Create and Update Access" and "enable" are selected, disable the other checkboxes to remove unnecessary permissions
|
||||
10. Click "Safe Integration" at the bottom
|
||||
11. Check your browser for the correct server apiURL. If it is "app.opsgenie.com" then use the us/international api url `api.opsgenie.com` in the next step, otherwise use `api.eu.opsgenie.com` (european api).
|
||||
12. You are finished with configuring opsgenie. Now you need to configure argocd-notifications. Use the apiUrl, the team name and the apiKey to configure the opsgenie integration in the `argocd-notifications-secret` secret.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: <config-map-name>
|
||||
data:
|
||||
service.opsgenie: |
|
||||
apiUrl: <api-url>
|
||||
apiKeys:
|
||||
<your-team>: <integration-api-key>
|
||||
```
|
||||
53
docs/operator-manual/notifications/services/overview.md
Executable file
53
docs/operator-manual/notifications/services/overview.md
Executable file
@@ -0,0 +1,53 @@
|
||||
The notification services represent integration with services such as slack, email or custom webhook. Services are configured in `argocd-notifications-cm` ConfigMap
|
||||
using `service.<type>.(<custom-name>)` keys and might reference sensitive data from `argocd-notifications-secret` Secret. Following example demonstrates slack
|
||||
service configuration:
|
||||
|
||||
```yaml
|
||||
service.slack: |
|
||||
token: $slack-token
|
||||
```
|
||||
|
||||
|
||||
The `slack` indicates that service sends slack notification; name is missing and defaults to `slack`.
|
||||
|
||||
## Sensitive Data
|
||||
|
||||
Sensitive data like authentication tokens should be stored in `<secret-name>` Secret and can be referenced in
|
||||
service configuration using `$<secret-key>` format. For example `$slack-token` referencing value of key `slack-token` in
|
||||
`<secret-name>` Secret.
|
||||
|
||||
## Custom Names
|
||||
|
||||
Service custom names allow configuring two instances of the same service type.
|
||||
|
||||
```yaml
|
||||
service.slack.workspace1: |
|
||||
token: $slack-token-workspace1
|
||||
service.slack.workspace2: |
|
||||
token: $slack-token-workspace2
|
||||
```
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
annotations:
|
||||
notifications.argoproj.io/subscribe.on-sync-succeeded.workspace1: my-channel
|
||||
notifications.argoproj.io/subscribe.on-sync-succeeded.workspace2: my-channel
|
||||
```
|
||||
|
||||
## Service Types
|
||||
|
||||
* [Email](./email.md)
|
||||
* [GitHub](./github.md)
|
||||
* [Slack](./slack.md)
|
||||
* [Mattermost](./mattermost.md)
|
||||
* [Opsgenie](./opsgenie.md)
|
||||
* [Grafana](./grafana.md)
|
||||
* [Webhook](./webhook.md)
|
||||
* [Telegram](./telegram.md)
|
||||
* [Teams](./teams.md)
|
||||
* [Google Chat](./googlechat.md)
|
||||
* [Rocket.Chat](./rocketchat.md)
|
||||
* [Pushover](./pushover.md)
|
||||
* [Alertmanager](./alertmanager.md)
|
||||
33
docs/operator-manual/notifications/services/pushover.md
Executable file
33
docs/operator-manual/notifications/services/pushover.md
Executable file
@@ -0,0 +1,33 @@
|
||||
# Pushover
|
||||
|
||||
1. Create an app at [pushover.net](https://pushover.net/apps/build).
|
||||
2. Store the API key in `<secret-name>` Secret and define the secret name in `<config-map-name>` ConfigMap:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: <config-map-name>
|
||||
data:
|
||||
service.pushover: |
|
||||
token: $pushover-token
|
||||
```
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: <secret-name>
|
||||
stringData:
|
||||
pushover-token: avtc41pn13asmra6zaiyf7dh6cgx97
|
||||
```
|
||||
|
||||
3. Add your user key to your Application resource:
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
annotations:
|
||||
notifications.argoproj.io/subscribe.on-sync-succeeded.pushover: uumy8u4owy7bgkapp6mc5mvhfsvpcd
|
||||
```
|
||||
96
docs/operator-manual/notifications/services/rocketchat.md
Executable file
96
docs/operator-manual/notifications/services/rocketchat.md
Executable file
@@ -0,0 +1,96 @@
|
||||
# Rocket.Chat
|
||||
|
||||
## Parameters
|
||||
|
||||
The Rocket.Chat notification service configuration includes following settings:
|
||||
|
||||
* `email` - the Rocker.Chat user's email
|
||||
* `password` - the Rocker.Chat user's password
|
||||
* `alias` - optional alias that should be used to post message
|
||||
* `icon` - optional message icon
|
||||
* `avatar` - optional message avatar
|
||||
* `serverUrl` - optional Rocket.Chat server url
|
||||
|
||||
## Configuration
|
||||
|
||||
1. Login to your RocketChat instance
|
||||
2. Go to user management
|
||||
|
||||

|
||||
|
||||
3. Add new user with `bot` role. Also note that `Require password change` checkbox mus be not checked
|
||||
|
||||

|
||||
|
||||
4. Copy username and password that you was created for bot user
|
||||
5. Create a public or private channel, or a team, for this example `my_channel`
|
||||
6. Add your bot to this channel **otherwise it won't work**
|
||||
7. Store email and password in argocd_notifications-secret Secret
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: <secret-name>
|
||||
stringData:
|
||||
rocketchat-email: <email>
|
||||
rocketchat-password: <password>
|
||||
```
|
||||
|
||||
8. Finally, use these credentials to configure the RocketChat integration in the `argocd-configmap` config map:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: <config-map-name>
|
||||
data:
|
||||
service.rocketchat: |
|
||||
email: $rocketchat-email
|
||||
password: $rocketchat-password
|
||||
```
|
||||
|
||||
9. Create a subscription for your Rocket.Chat integration:
|
||||
|
||||
*Note: channel, team or user must be prefixed with # or @ elsewhere we will be interpretative destination as a room ID*
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
annotations:
|
||||
notifications.argoproj.io/subscribe.on-sync-succeeded.rocketchat: #my_channel
|
||||
```
|
||||
|
||||
## Templates
|
||||
|
||||
Notification templates can be customized with RocketChat [attachments](https://developer.rocket.chat/api/rest-api/methods/chat/postmessage#attachments-detail).
|
||||
|
||||
*Note: Attachments structure in Rocketchat is same with Slack attachments [feature](https://api.slack.com/messaging/composing/layouts).*
|
||||
|
||||
<!-- TODO: @sergeyshevch Need to add screenshot with RocketChat attachments -->
|
||||
|
||||
The message attachments can be specified in `attachments` string fields under `rocketchat` field:
|
||||
|
||||
```yaml
|
||||
template.app-sync-status: |
|
||||
message: |
|
||||
Application {{.app.metadata.name}} sync is {{.app.status.sync.status}}.
|
||||
Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}.
|
||||
rocketchat:
|
||||
attachments: |
|
||||
[{
|
||||
"title": "{{.app.metadata.name}}",
|
||||
"title_link": "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
|
||||
"color": "#18be52",
|
||||
"fields": [{
|
||||
"title": "Sync Status",
|
||||
"value": "{{.app.status.sync.status}}",
|
||||
"short": true
|
||||
}, {
|
||||
"title": "Repository",
|
||||
"value": "{{.app.spec.source.repoURL}}",
|
||||
"short": true
|
||||
}]
|
||||
}]
|
||||
```
|
||||
149
docs/operator-manual/notifications/services/slack.md
Executable file
149
docs/operator-manual/notifications/services/slack.md
Executable file
@@ -0,0 +1,149 @@
|
||||
# Slack
|
||||
|
||||
If you want to send message using incoming webhook, you can use [webhook](./webhook.md#send-slack).
|
||||
|
||||
## Parameters
|
||||
|
||||
The Slack notification service configuration includes following settings:
|
||||
|
||||
* `token` - the app token
|
||||
* `apiURL` - optional, the server url, e.g. https://example.com/api
|
||||
* `username` - optional, the app username
|
||||
* `icon` - optional, the app icon, e.g. :robot_face: or https://example.com/image.png
|
||||
* `insecureSkipVerify` - optional bool, true or false
|
||||
|
||||
## Configuration
|
||||
|
||||
1. Create Slack Application using https://api.slack.com/apps?new_app=1
|
||||

|
||||
1. Once application is created navigate to `Enter OAuth & Permissions`
|
||||

|
||||
1. Click `Permissions` under `Add features and functionality` section and add `chat:write` scope. To use the optional username and icon overrides in the Slack notification service also add the `chat:write.customize` scope.
|
||||

|
||||
1. Scroll back to the top, click 'Install App to Workspace' button and confirm the installation.
|
||||

|
||||
1. Once installation is completed copy the OAuth token.
|
||||

|
||||
|
||||
1. Create a public or private channel, for this example `my_channel`
|
||||
1. Invite your slack bot to this channel **otherwise slack bot won't be able to deliver notifications to this channel**
|
||||
1. Store Oauth access token in `argocd-notifications-secret` secret
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: <secret-name>
|
||||
stringData:
|
||||
slack-token: <Oauth-access-token>
|
||||
```
|
||||
|
||||
1. Define service type slack in data section of `argocd-notifications-cm` configmap:
|
||||
service
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: <config-map-name>
|
||||
data:
|
||||
service.slack: |
|
||||
token: $slack-token
|
||||
```
|
||||
|
||||
1. Add annotation in application yaml file to enable notifications for specific argocd app
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
annotations:
|
||||
notifications.argoproj.io/subscribe.on-sync-succeeded.slack: my_channel
|
||||
```
|
||||
|
||||
## Templates
|
||||
|
||||
Notification templates can be customized to leverage slack message blocks and attachments
|
||||
[feature](https://api.slack.com/messaging/composing/layouts).
|
||||
|
||||

|
||||
|
||||
The message blocks and attachments can be specified in `blocks` and `attachments` string fields under `slack` field:
|
||||
|
||||
```yaml
|
||||
template.app-sync-status: |
|
||||
message: |
|
||||
Application {{.app.metadata.name}} sync is {{.app.status.sync.status}}.
|
||||
Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}.
|
||||
slack:
|
||||
attachments: |
|
||||
[{
|
||||
"title": "{{.app.metadata.name}}",
|
||||
"title_link": "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
|
||||
"color": "#18be52",
|
||||
"fields": [{
|
||||
"title": "Sync Status",
|
||||
"value": "{{.app.status.sync.status}}",
|
||||
"short": true
|
||||
}, {
|
||||
"title": "Repository",
|
||||
"value": "{{.app.spec.source.repoURL}}",
|
||||
"short": true
|
||||
}]
|
||||
}]
|
||||
```
|
||||
|
||||
The messages can be aggregated to the slack threads by grouping key which can be specified in a `groupingKey` string field under `slack` field.
|
||||
`groupingKey` is used across each template and works independently on each slack channel.
|
||||
When multiple applications will be updated at the same time or frequently, the messages in slack channel can be easily read by aggregating with git commit hash, application name, etc.
|
||||
Furthermore, the messages can be broadcast to the channel at the specific template by `notifyBroadcast` field.
|
||||
|
||||
```yaml
|
||||
template.app-sync-status: |
|
||||
message: |
|
||||
Application {{.app.metadata.name}} sync is {{.app.status.sync.status}}.
|
||||
Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}.
|
||||
slack:
|
||||
attachments: |
|
||||
[{
|
||||
"title": "{{.app.metadata.name}}",
|
||||
"title_link": "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
|
||||
"color": "#18be52",
|
||||
"fields": [{
|
||||
"title": "Sync Status",
|
||||
"value": "{{.app.status.sync.status}}",
|
||||
"short": true
|
||||
}, {
|
||||
"title": "Repository",
|
||||
"value": "{{.app.spec.source.repoURL}}",
|
||||
"short": true
|
||||
}]
|
||||
}]
|
||||
# Aggregate the messages to the thread by git commit hash
|
||||
groupingKey: "{{.app.status.sync.revision}}"
|
||||
notifyBroadcast: false
|
||||
template.app-sync-failed: |
|
||||
message: |
|
||||
Application {{.app.metadata.name}} sync is {{.app.status.sync.status}}.
|
||||
Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}.
|
||||
slack:
|
||||
attachments: |
|
||||
[{
|
||||
"title": "{{.app.metadata.name}}",
|
||||
"title_link": "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
|
||||
"color": "#ff0000",
|
||||
"fields": [{
|
||||
"title": "Sync Status",
|
||||
"value": "{{.app.status.sync.status}}",
|
||||
"short": true
|
||||
}, {
|
||||
"title": "Repository",
|
||||
"value": "{{.app.spec.source.repoURL}}",
|
||||
"short": true
|
||||
}]
|
||||
}]
|
||||
# Aggregate the messages to the thread by git commit hash
|
||||
groupingKey: "{{.app.status.sync.revision}}"
|
||||
notifyBroadcast: true
|
||||
```
|
||||
|
||||
The message is sent according to the `deliveryPolicy` string field under the `slack` field. The available modes are `Post` (default), `PostAndUpdate`, and `Update`. The `PostAndUpdate` and `Update` settings require `groupingKey` to be set.
|
||||
126
docs/operator-manual/notifications/services/teams.md
Executable file
126
docs/operator-manual/notifications/services/teams.md
Executable file
@@ -0,0 +1,126 @@
|
||||
# Teams
|
||||
|
||||
## Parameters
|
||||
|
||||
The Teams notification service send message notifications using Teams bot and requires specifying the following settings:
|
||||
|
||||
* `recipientUrls` - the webhook url map, e.g. `channelName: https://example.com`
|
||||
|
||||
## Configuration
|
||||
|
||||
1. Open `Teams` and goto `Apps`
|
||||
2. Find `Incoming Webhook` microsoft app and click on it
|
||||
3. Press `Add to a team` -> select team and channel -> press `Set up a connector`
|
||||
4. Enter webhook name and upload image (optional)
|
||||
5. Press `Create` then copy webhook url and store it in `argocd-notifications-secret` and define it in `argocd-notifications-cm`
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: <config-map-name>
|
||||
data:
|
||||
service.teams: |
|
||||
recipientUrls:
|
||||
channelName: $channel-teams-url
|
||||
```
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: <secret-name>
|
||||
stringData:
|
||||
channel-teams-url: https://example.com
|
||||
```
|
||||
|
||||
6. Create subscription for your Teams integration:
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
annotations:
|
||||
notifications.argoproj.io/subscribe.on-sync-succeeded.teams: channelName
|
||||
```
|
||||
|
||||
## Templates
|
||||
|
||||

|
||||
|
||||
Notification templates can be customized to leverage teams message sections, facts, themeColor, summary and potentialAction [feature](https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using).
|
||||
|
||||
```yaml
|
||||
template.app-sync-succeeded: |
|
||||
teams:
|
||||
themeColor: "#000080"
|
||||
sections: |
|
||||
[{
|
||||
"facts": [
|
||||
{
|
||||
"name": "Sync Status",
|
||||
"value": "{{.app.status.sync.status}}"
|
||||
},
|
||||
{
|
||||
"name": "Repository",
|
||||
"value": "{{.app.spec.source.repoURL}}"
|
||||
}
|
||||
]
|
||||
}]
|
||||
potentialAction: |-
|
||||
[{
|
||||
"@type":"OpenUri",
|
||||
"name":"Operation Details",
|
||||
"targets":[{
|
||||
"os":"default",
|
||||
"uri":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true"
|
||||
}]
|
||||
}]
|
||||
title: Application {{.app.metadata.name}} has been successfully synced
|
||||
text: Application {{.app.metadata.name}} has been successfully synced at {{.app.status.operationState.finishedAt}}.
|
||||
summary: "{{.app.metadata.name}} sync succeeded"
|
||||
```
|
||||
|
||||
### facts field
|
||||
|
||||
You can use `facts` field instead of `sections` field.
|
||||
|
||||
```yaml
|
||||
template.app-sync-succeeded: |
|
||||
teams:
|
||||
facts: |
|
||||
[{
|
||||
"name": "Sync Status",
|
||||
"value": "{{.app.status.sync.status}}"
|
||||
},
|
||||
{
|
||||
"name": "Repository",
|
||||
"value": "{{.app.spec.source.repoURL}}"
|
||||
}]
|
||||
```
|
||||
|
||||
### theme color field
|
||||
|
||||
You can set theme color as hex string for the message.
|
||||
|
||||

|
||||
|
||||
```yaml
|
||||
template.app-sync-succeeded: |
|
||||
teams:
|
||||
themeColor: "#000080"
|
||||
```
|
||||
|
||||
### summary field
|
||||
|
||||
You can set a summary of the message that will be shown on Notifcation & Activity Feed
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
```yaml
|
||||
template.app-sync-succeeded: |
|
||||
teams:
|
||||
summary: "Sync Succeeded"
|
||||
```
|
||||
35
docs/operator-manual/notifications/services/telegram.md
Executable file
35
docs/operator-manual/notifications/services/telegram.md
Executable file
@@ -0,0 +1,35 @@
|
||||
# Telegram
|
||||
|
||||
1. Get an API token using [@Botfather](https://t.me/Botfather).
|
||||
2. Store token in `<secret-name>` Secret and configure telegram integration
|
||||
in `<config-map-name>` ConfigMap:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: <config-map-name>
|
||||
data:
|
||||
service.telegram: |
|
||||
token: $telegram-token
|
||||
```
|
||||
|
||||
3. Create new Telegram [channel](https://telegram.org/blog/channels).
|
||||
4. Add your bot as an administrator.
|
||||
5. Use this channel `username` (public channel) or `chatID` (private channel) in the subscription for your Telegram integration:
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
annotations:
|
||||
notifications.argoproj.io/subscribe.on-sync-succeeded.telegram: username
|
||||
```
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
annotations:
|
||||
notifications.argoproj.io/subscribe.on-sync-succeeded.telegram: -1000000000000
|
||||
```
|
||||
177
docs/operator-manual/notifications/services/webhook.md
Executable file
177
docs/operator-manual/notifications/services/webhook.md
Executable file
@@ -0,0 +1,177 @@
|
||||
## Configuration
|
||||
|
||||
The webhook notification service allows sending a generic HTTP request using the templatized request body and URL.
|
||||
Using Webhook you might trigger a Jenkins job, update Github commit status.
|
||||
|
||||
Use the following steps to configure webhook:
|
||||
|
||||
1 Register webhook in `argocd-notifications-cm` ConfigMap:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: <config-map-name>
|
||||
data:
|
||||
service.webhook.<webhook-name>: |
|
||||
url: https://<hostname>/<optional-path>
|
||||
headers: #optional headers
|
||||
- name: <header-name>
|
||||
value: <header-value>
|
||||
basicAuth: #optional username password
|
||||
username: <username>
|
||||
password: <api-key>
|
||||
```
|
||||
|
||||
2 Define template that customizes webhook request method, path and body:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: <config-map-name>
|
||||
data:
|
||||
template.github-commit-status: |
|
||||
webhook:
|
||||
<webhook-name>:
|
||||
method: POST # one of: GET, POST, PUT, PATCH. Default value: GET
|
||||
path: <optional-path-template>
|
||||
body: |
|
||||
<optional-body-template>
|
||||
trigger.<trigger-name>: |
|
||||
- when: app.status.operationState.phase in ['Succeeded']
|
||||
send: [github-commit-status]
|
||||
```
|
||||
|
||||
3 Create subscription for webhook integration:
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
annotations:
|
||||
notifications.argoproj.io/subscribe.<trigger-name>.<webhook-name>: ""
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Set Github commit status
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: <config-map-name>
|
||||
data:
|
||||
service.webhook.github: |
|
||||
url: https://api.github.com
|
||||
headers: #optional headers
|
||||
- name: Authorization
|
||||
value: token $github-token
|
||||
```
|
||||
|
||||
2 Define template that customizes webhook request method, path and body:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: <config-map-name>
|
||||
data:
|
||||
service.webhook.github: |
|
||||
url: https://api.github.com
|
||||
headers: #optional headers
|
||||
- name: Authorization
|
||||
value: token $github-token
|
||||
|
||||
template.github-commit-status: |
|
||||
webhook:
|
||||
github:
|
||||
method: POST
|
||||
path: /repos/{{call .repo.FullNameByRepoURL .app.spec.source.repoURL}}/statuses/{{.app.status.operationState.operation.sync.revision}}
|
||||
body: |
|
||||
{
|
||||
{{if eq .app.status.operationState.phase "Running"}} "state": "pending"{{end}}
|
||||
{{if eq .app.status.operationState.phase "Succeeded"}} "state": "success"{{end}}
|
||||
{{if eq .app.status.operationState.phase "Error"}} "state": "error"{{end}}
|
||||
{{if eq .app.status.operationState.phase "Failed"}} "state": "error"{{end}},
|
||||
"description": "ArgoCD",
|
||||
"target_url": "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
|
||||
"context": "continuous-delivery/{{.app.metadata.name}}"
|
||||
}
|
||||
```
|
||||
|
||||
### Start Jenkins Job
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: <config-map-name>
|
||||
data:
|
||||
service.webhook.jenkins: |
|
||||
url: http://<jenkins-host>/job/<job-name>/build?token=<job-secret>
|
||||
basicAuth:
|
||||
username: <username>
|
||||
password: <api-key>
|
||||
|
||||
type: Opaque
|
||||
```
|
||||
|
||||
### Send form-data
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: <config-map-name>
|
||||
data:
|
||||
service.webhook.form: |
|
||||
url: https://form.example.com
|
||||
headers:
|
||||
- name: Content-Type
|
||||
value: application/x-www-form-urlencoded
|
||||
|
||||
template.form-data: |
|
||||
webhook:
|
||||
form:
|
||||
method: POST
|
||||
body: key1=value1&key2=value2
|
||||
```
|
||||
|
||||
### Send Slack
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: <config-map-name>
|
||||
data:
|
||||
service.webhook.slack_webhook: |
|
||||
url: https://hooks.slack.com/services/xxxxx
|
||||
headers:
|
||||
- name: Content-Type
|
||||
value: application/json
|
||||
|
||||
template.send-slack: |
|
||||
webhook:
|
||||
slack_webhook:
|
||||
method: POST
|
||||
body: |
|
||||
{
|
||||
"attachments": [{
|
||||
"title": "{{.app.metadata.name}}",
|
||||
"title_link": "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
|
||||
"color": "#18be52",
|
||||
"fields": [{
|
||||
"title": "Sync Status",
|
||||
"value": "{{.app.status.sync.status}}",
|
||||
"short": true
|
||||
}, {
|
||||
"title": "Repository",
|
||||
"value": "{{.app.spec.source.repoURL}}",
|
||||
"short": true
|
||||
}]
|
||||
}]
|
||||
}
|
||||
```
|
||||
71
docs/operator-manual/notifications/subscriptions.md
Normal file
71
docs/operator-manual/notifications/subscriptions.md
Normal file
@@ -0,0 +1,71 @@
|
||||
The subscription to Argo CD application events can be defined using `notifications.argoproj.io/subscribe.<trigger>.<service>: <recipient>` annotation.
|
||||
For example, the following annotation subscribes two Slack channels to notifications about every successful synchronization of the Argo CD application:
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
annotations:
|
||||
notifications.argoproj.io/subscribe.on-sync-succeeded.slack: my-channel1;my-channel2
|
||||
```
|
||||
|
||||
Annotation key consists of following parts:
|
||||
|
||||
* `on-sync-succeeded` - trigger name
|
||||
* `slack` - notification service name
|
||||
* `my-channel1;my-channel2` - a semicolon separated list of recipients
|
||||
|
||||
You can create subscriptions for all applications of the Argo CD project by adding the same annotation to AppProject CRD:
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: AppProject
|
||||
metadata:
|
||||
annotations:
|
||||
notifications.argoproj.io/subscribe.on-sync-succeeded.slack: my-channel1;my-channel2
|
||||
```
|
||||
|
||||
## Default Subscriptions
|
||||
|
||||
The subscriptions might be configured globally in the `argocd-notifications-cm` ConfigMap using `subscriptions` field. The default subscriptions
|
||||
are applied to all applications. The trigger and applications might be configured using the
|
||||
`triggers` and `selector` fields:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-notifications-cm
|
||||
data:
|
||||
# Contains centrally managed global application subscriptions
|
||||
subscriptions: |
|
||||
# subscription for on-sync-status-unknown trigger notifications
|
||||
- recipients:
|
||||
- slack:test2
|
||||
- email:test@gmail.com
|
||||
triggers:
|
||||
- on-sync-status-unknown
|
||||
# subscription restricted to applications with matching labels only
|
||||
- recipients:
|
||||
- slack:test3
|
||||
selector: test=true
|
||||
triggers:
|
||||
- on-sync-status-unknown
|
||||
```
|
||||
|
||||
If you want to use webhook in subscriptions, you need to store the custom name to recipients.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-notifications-cm
|
||||
data:
|
||||
service.webhook.<webhook-name>: |
|
||||
(snip)
|
||||
subscriptions: |
|
||||
- recipients:
|
||||
- <webhook-name>
|
||||
triggers:
|
||||
- on-sync-status-unknown
|
||||
```
|
||||
93
docs/operator-manual/notifications/templates.md
Normal file
93
docs/operator-manual/notifications/templates.md
Normal file
@@ -0,0 +1,93 @@
|
||||
The notification template is used to generate the notification content and configured in `argocd-notifications-cm` ConfigMap. The template is leveraging
|
||||
[html/template](https://golang.org/pkg/html/template/) golang package and allow to customize notification message.
|
||||
Templates are meant to be reusable and can be referenced by multiple triggers.
|
||||
|
||||
The following template is used to notify the user about application sync status.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-notifications-cm
|
||||
data:
|
||||
template.my-custom-template-slack-template: |
|
||||
message: |
|
||||
Application {{.app.metadata.name}} sync is {{.app.status.sync.status}}.
|
||||
Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}.
|
||||
```
|
||||
|
||||
Each template has access to the following fields:
|
||||
|
||||
- `app` holds the application object.
|
||||
- `context` is user defined string map and might include any string keys and values.
|
||||
- `serviceType` holds the notification service type name. The field can be used to conditionally
|
||||
render service specific fields.
|
||||
- `recipient` holds the recipient name.
|
||||
|
||||
## Defining user-defined `context`
|
||||
|
||||
It is possible to define some shared context between all notification templates by setting a top-level
|
||||
YAML document of key-value pairs, which can then be used within templates, like so:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-notifications-cm
|
||||
data:
|
||||
context: |
|
||||
region: east
|
||||
environmentName: staging
|
||||
|
||||
template.a-slack-template-with-context: |
|
||||
message: "Something happened in {{ .context.environmentName }} in the {{ .context.region }} data center!"
|
||||
```
|
||||
|
||||
## Notification Service Specific Fields
|
||||
|
||||
The `message` field of the template definition allows creating a basic notification for any notification service. You can leverage notification service-specific
|
||||
fields to create complex notifications. For example using service-specific you can add blocks and attachments for Slack, subject for Email or URL path, and body for Webhook.
|
||||
See corresponding service [documentation](services/overview.md) for more information.
|
||||
|
||||
## Change the timezone
|
||||
|
||||
You can change the timezone to show it as follows.
|
||||
|
||||
1. Call time functions.
|
||||
|
||||
```
|
||||
{{ (call .time.Parse .app.status.operationState.startedAt).Local.Format "2006-01-02T15:04:05Z07:00" }}
|
||||
```
|
||||
|
||||
2. Set environment to container.
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: argocd-notifications-controller
|
||||
spec:
|
||||
(snip)
|
||||
spec:
|
||||
containers:
|
||||
- name: argocd-notifications-controller
|
||||
env:
|
||||
- name: TZ
|
||||
value: Asia/Tokyo
|
||||
```
|
||||
|
||||
## Functions
|
||||
|
||||
Templates have access to the set of built-in functions:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-notifications-cm
|
||||
data:
|
||||
template.my-custom-template-slack-template: |
|
||||
message: "Author: {{(call .repo.GetCommitMetadata .app.status.sync.revision).Author}}"
|
||||
```
|
||||
|
||||
{!functions.md!}
|
||||
125
docs/operator-manual/notifications/triggers.md
Normal file
125
docs/operator-manual/notifications/triggers.md
Normal file
@@ -0,0 +1,125 @@
|
||||
The trigger defines the condition when the notification should be sent. The definition includes name, condition
|
||||
and notification templates reference. The condition is a predicate expression that returns true if the notification
|
||||
should be sent. The trigger condition evaluation is powered by [antonmedv/expr](https://github.com/antonmedv/expr).
|
||||
The condition language syntax is described at [Language-Definition.md](https://github.com/antonmedv/expr/blob/master/docs/Language-Definition.md).
|
||||
|
||||
The trigger is configured in `argocd-notifications-cm` ConfigMap. For example the following trigger sends a notification
|
||||
when application sync status changes to `Unknown` using the `app-sync-status` template:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-notifications-cm
|
||||
data:
|
||||
trigger.on-sync-status-unknown: |
|
||||
- when: app.status.sync.status == 'Unknown' # trigger condition
|
||||
send: [app-sync-status, github-commit-status] # template names
|
||||
```
|
||||
|
||||
Each condition might use several templates. Typically each template is responsible for generating a service-specific notification part.
|
||||
In the example above `app-sync-status` template "knows" how to create email and slack notification and `github-commit-status` knows how to
|
||||
generate payload for Github webhook.
|
||||
|
||||
## Conditions Bundles
|
||||
|
||||
Triggers are typically managed by administrators and encapsulate information about when and which notification should be sent.
|
||||
The end users just need to subscribe to the trigger and specify the notification destination. In order to improve user experience
|
||||
triggers might include multiple conditions with a different set of templates for each condition. For example, the following trigger
|
||||
covers all stages of sync status operation and use a different template for different cases:
|
||||
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-notifications-cm
|
||||
data:
|
||||
trigger.sync-operation-change: |
|
||||
- when: app.status.operationState.phase in ['Succeeded']
|
||||
send: [github-commit-status]
|
||||
- when: app.status.operationState.phase in ['Running']
|
||||
send: [github-commit-status]
|
||||
- when: app.status.operationState.phase in ['Error', 'Failed']
|
||||
send: [app-sync-failed, github-commit-status]
|
||||
```
|
||||
|
||||
## Avoid Sending Same Notification Too Often
|
||||
|
||||
In some cases, the trigger condition might be "flapping". The example below illustrates the problem.
|
||||
The trigger is supposed to generate a notification once when Argo CD application is successfully synchronized and healthy.
|
||||
However, the application health status might intermittently switch to `Progressing` and then back to `Healthy` so the trigger might unnecessarily generate
|
||||
multiple notifications. The `oncePer` field configures triggers to generate the notification only when the corresponding application field changes.
|
||||
The `on-deployed` trigger from the example below sends the notification only once per observed Git revision of the deployment repository.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-notifications-cm
|
||||
data:
|
||||
# Optional 'oncePer' property ensure that notification is sent only once per specified field value
|
||||
# E.g. following is triggered once per sync revision
|
||||
trigger.on-deployed: |
|
||||
when: app.status.operationState.phase in ['Succeeded'] and app.status.health.status == 'Healthy'
|
||||
oncePer: app.status.sync.revision
|
||||
send: [app-sync-succeeded]
|
||||
```
|
||||
|
||||
### oncePer
|
||||
|
||||
The `oncePer` filed is supported like as follows.
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
annotations:
|
||||
example.com/version: v0.1
|
||||
```
|
||||
|
||||
```yaml
|
||||
oncePer: app.metadata.annotations["example.com/version"]
|
||||
```
|
||||
|
||||
## Default Triggers
|
||||
|
||||
You can use `defaultTriggers` field instead of specifying individual triggers to the annotations.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-notifications-cm
|
||||
data:
|
||||
# Holds list of triggers that are used by default if trigger is not specified explicitly in the subscription
|
||||
defaultTriggers: |
|
||||
- on-sync-status-unknown
|
||||
|
||||
defaultTriggers.mattermost: |
|
||||
- on-sync-running
|
||||
- on-sync-succeeded
|
||||
```
|
||||
|
||||
Specify the annotations as follows to use `defaultTriggers`. In this example, `slack` sends when `on-sync-status-unknown`, and `mattermost` sends when `on-sync-running` and `on-sync-succeeded`.
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
annotations:
|
||||
notifications.argoproj.io/subscribe.slack: my-channel
|
||||
notifications.argoproj.io/subscribe.mattermost: my-mattermost-channel
|
||||
```
|
||||
|
||||
## Functions
|
||||
|
||||
Triggers have access to the set of built-in functions.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
when: time.Now().Sub(time.Parse(app.status.operationState.startedAt)).Minutes() >= 5
|
||||
```
|
||||
|
||||
{!functions.md!}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user