Compare commits

..

16 Commits

Author SHA1 Message Date
Alexander Matyushentsev
24834112d6 Update manifests to v1.4.3 2020-04-15 09:35:12 -07:00
Alexander Matyushentsev
48cced9d92 Update manifests to v1.4.2 2020-01-23 16:48:25 -08:00
Alexander Matyushentsev
bc77ea70c4 fix: correctly replace cache in namespace isolation mode (#3023) 2020-01-23 16:20:40 -08:00
Alexander Matyushentsev
f8721a7360 Update manifests to v1.4.1 2020-01-22 14:44:13 -08:00
Alexander Matyushentsev
8ada07e0f5 fix: run dep ensure (#3018) 2020-01-22 14:43:38 -08:00
Alexander Matyushentsev
de8ae9c617 fix: impossible to config RBAC if group name includes ',' (#3013)
* fix:  impossible to config RBAC if group name includes ','

* apply reviewer notes
2020-01-22 11:47:31 -08:00
Alexander Matyushentsev
2d029488ab Update manifests to v1.4.0 2020-01-17 21:32:51 -08:00
Alexander Matyushentsev
3771486c14 feat: upgrade dex to v2.21.0 (#2985) 2020-01-15 14:07:44 -08:00
Alexander Matyushentsev
97922f0439 fix: sync apps panel fails with 'No App Selected' message if name contains '.' (#2983) 2020-01-15 14:07:39 -08:00
Alexander Matyushentsev
141ef96a44 docs: add notifications.md with recommandation about notifications (#2979)
* docs: add demo argocd/grafana links

* docs: add notifications.md with recommandation about notifications
2020-01-15 14:07:34 -08:00
Paul Brit
92824215f8 docs: Fix a broken link to Helm Hooks (#2970) 2020-01-15 14:07:30 -08:00
Alexander Matyushentsev
f74640d95c fix: fix nil pointer dereference in CreateRepositoryCredentials method (#2975) 2020-01-15 14:07:27 -08:00
Alexander Matyushentsev
5c0ebb59d2 fix: remove 'total' suffix from gauge prom metric (#2976) 2020-01-15 14:07:23 -08:00
Alexander Matyushentsev
6d65d01757 fix: fix rendering CRD acronym (#2978) 2020-01-15 14:07:20 -08:00
Alexander Matyushentsev
36bbc29891 docs: v1.3 and v1.4 changelog (#2952) 2020-01-15 14:07:17 -08:00
Alexander Matyushentsev
5af52f6698 Update manifests to v1.4.0-rc1 2020-01-13 09:03:55 -08:00
589 changed files with 19210 additions and 31704 deletions

View File

@@ -1,16 +1,217 @@
version: 2.1
jobs:
dummy:
docker:
- image: cimg/base:2020.01
commands:
configure_git:
steps:
- run:
name: Dummy step
name: Configure Git
command: |
echo "This is a dummy step to satisfy CircleCI"
set -x
# must be configured for tests to run
git config --global user.email you@example.com
git config --global user.name "Your Name"
echo "export PATH=/home/circleci/.go_workspace/src/github.com/argoproj/argo-cd/hack:\$PATH" | tee -a $BASH_ENV
echo "export GIT_ASKPASS=git-ask-pass.sh" | tee -a $BASH_ENV
restore_vendor:
steps:
- restore_cache:
keys:
- vendor-v1-{{ checksum "Gopkg.lock" }}-{{ .Environment.CIRCLE_JOB }}
save_vendor:
steps:
- save_cache:
key: vendor-v1-{{ checksum "Gopkg.lock" }}-{{ .Environment.CIRCLE_JOB }}
paths:
- vendor
install_golang:
steps:
- run:
name: Install Golang v1.12.6
command: |
go get golang.org/dl/go1.12.6
[ -e /home/circleci/sdk/go1.12.6 ] || go1.12.6 download
go env
echo "export GOPATH=/home/circleci/.go_workspace" | tee -a $BASH_ENV
echo "export PATH=/home/circleci/sdk/go1.12.6/bin:\$PATH" | tee -a $BASH_ENV
save_go_cache:
steps:
- save_cache:
key: go-v1-{{ .Branch }}-{{ .Environment.CIRCLE_JOB }}
# https://circleci.com/docs/2.0/language-go/
paths:
- /home/circleci/.cache/go-build
- /home/circleci/sdk/go1.12.6
restore_go_cache:
steps:
- restore_cache:
keys:
- go-v1-{{ .Branch }}-{{ .Environment.CIRCLE_JOB }}
- go-v1-master-{{ .Environment.CIRCLE_JOB }}
jobs:
codegen:
docker:
- image: circleci/golang:1.12
working_directory: /go/src/github.com/argoproj/argo-cd
steps:
- checkout
- restore_cache:
keys:
- codegen-v1-{{ checksum "Gopkg.lock" }}-{{ checksum "hack/installers/install-codegen-go-tools.sh" }}
- run: ./hack/install.sh codegen-go-tools
- run: sudo ./hack/install.sh codegen-tools
- run: dep ensure -v
- save_cache:
key: codegen-v1-{{ checksum "Gopkg.lock" }}-{{ checksum "hack/installers/install-codegen-go-tools.sh" }}
paths: [vendor, /tmp/dl, /go/pkg]
- run: helm init --client-only
- run: make codegen-local
- run:
name: Check nothing has changed
command: |
set -xo pipefail
# This makes sure you ran `make pre-commit` before you pushed.
# We exclude the Swagger resources; CircleCI doesn't generate them correctly.
# When this fails, it will, create a patch file you can apply locally to fix it.
# To troubleshoot builds: https://argoproj.github.io/argo-cd/developer-guide/ci/
git diff --exit-code -- . ':!Gopkg.lock' ':!assets/swagger.json' | tee codegen.patch
- store_artifacts:
path: codegen.patch
destination: .
test:
working_directory: /home/circleci/.go_workspace/src/github.com/argoproj/argo-cd
machine:
image: circleci/classic:201808-01
steps:
- restore_go_cache
- install_golang
- checkout
- restore_cache:
key: test-dl-v1
- run: sudo ./hack/install.sh kubectl-linux kubectx-linux dep-linux ksonnet-linux helm-linux kustomize-linux
- save_cache:
key: test-dl-v1
paths: [/tmp/dl]
- configure_git
- run: go get github.com/jstemmer/go-junit-report
- restore_vendor
- run: dep ensure -v
- run: make test
- save_vendor
- save_go_cache
- run:
name: Uploading code coverage
command: bash <(curl -s https://codecov.io/bash) -f coverage.out
- store_test_results:
path: test-results
- store_artifacts:
path: test-results
destination: .
e2e:
working_directory: /home/circleci/.go_workspace/src/github.com/argoproj/argo-cd
machine:
image: circleci/classic:201808-01
environment:
ARGOCD_FAKE_IN_CLUSTER: "true"
ARGOCD_SSH_DATA_PATH: "/tmp/argo-e2e/app/config/ssh"
ARGOCD_TLS_DATA_PATH: "/tmp/argo-e2e/app/config/tls"
steps:
- run:
name: Install and start K3S v0.5.0
command: |
curl -sfL https://get.k3s.io | sh -
sudo chmod -R a+rw /etc/rancher/k3s
kubectl version
background: true
environment:
INSTALL_K3S_EXEC: --docker
INSTALL_K3S_VERSION: v0.5.0
- restore_go_cache
- install_golang
- checkout
- restore_cache:
keys: [e2e-dl-v1]
- run: sudo ./hack/install.sh kubectx-linux dep-linux ksonnet-linux helm-linux kustomize-linux
- run: go get github.com/jstemmer/go-junit-report
- save_cache:
key: e2e-dl-v10
paths: [/tmp/dl]
- restore_vendor
- run: dep ensure -v
- configure_git
- run: make cli
- run:
name: Create namespace
command: |
set -x
kubectl create ns argocd-e2e
kubens argocd-e2e
# install the certificates (not 100% sure we need this)
sudo cp /var/lib/rancher/k3s/server/tls/token-ca.crt /usr/local/share/ca-certificates/k3s.crt
sudo update-ca-certificates
# create the kubecfg, again - not sure we need this
cat /etc/rancher/k3s/k3s.yaml | sed "s/localhost/`hostname`/" | tee ~/.kube/config
echo "127.0.0.1 `hostname`" | sudo tee -a /etc/hosts
- run:
name: Apply manifests
command: kustomize build test/manifests/base | kubectl apply -f -
- run:
name: Start Redis
command: docker run --rm --name argocd-redis -i -p 6379:6379 redis:5.0.3-alpine --save "" --appendonly no
background: true
- run:
name: Start repo server
command: go run ./cmd/argocd-repo-server/main.go --loglevel debug --redis localhost:6379
background: true
- run:
name: Start API server
command: go run ./cmd/argocd-server/main.go --loglevel debug --redis localhost:6379 --insecure --dex-server http://localhost:5556 --repo-server localhost:8081 --staticassets ../argo-cd-ui/dist/app
background: true
- run:
name: Start Test Git
command: |
test/fixture/testrepos/start-git.sh
background: true
- run: until curl -v http://localhost:8080/healthz; do sleep 10; done
- run:
name: Start controller
command: go run ./cmd/argocd-application-controller/main.go --loglevel debug --redis localhost:6379 --repo-server localhost:8081 --kubeconfig ~/.kube/config
background: true
- run:
command: PATH=dist:$PATH make test-e2e
environment:
ARGOCD_OPTS: "--server localhost:8080 --plaintext"
ARGOCD_E2E_K3S: "true"
- save_vendor
- save_go_cache
- store_test_results:
path: test-results
- store_artifacts:
path: test-results
destination: .
ui:
docker:
- image: node:11.15.0
working_directory: ~/argo-cd/ui
steps:
- checkout:
path: ~/argo-cd/
- restore_cache:
keys:
- yarn-packages-v4-{{ checksum "yarn.lock" }}
- run: yarn install --frozen-lockfile --ignore-optional --non-interactive
- save_cache:
key: yarn-packages-v4-{{ checksum "yarn.lock" }}
paths: [~/.cache/yarn, node_modules]
- run: yarn test
- run: ./node_modules/.bin/codecov -p ..
- run: NODE_ENV='production' yarn build
- run: yarn lint
workflows:
version: 2
workflow:
jobs:
- dummy
jobs:
- test
- codegen
- ui:
requires:
- codegen
- e2e

View File

@@ -1,324 +0,0 @@
# CircleCI currently disabled in favor of GH actions
version: 2.1
commands:
prepare_environment:
steps:
- run:
name: Configure environment
command: |
set -x
echo "export GOCACHE=/tmp/go-build-cache" | tee -a $BASH_ENV
echo "export ARGOCD_TEST_VERBOSE=true" | tee -a $BASH_ENV
echo "export ARGOCD_TEST_PARALLELISM=4" | tee -a $BASH_ENV
echo "export ARGOCD_SONAR_VERSION=4.2.0.1873" | tee -a $BASH_ENV
configure_git:
steps:
- run:
name: Configure Git
command: |
set -x
# must be configured for tests to run
git config --global user.email you@example.com
git config --global user.name "Your Name"
echo "export PATH=/home/circleci/.go_workspace/src/github.com/argoproj/argo-cd/hack:\$PATH" | tee -a $BASH_ENV
echo "export GIT_ASKPASS=git-ask-pass.sh" | tee -a $BASH_ENV
setup_go_modules:
steps:
- run:
name: Run go mod download and populate vendor
command: |
go mod download
go mod vendor
save_coverage_info:
steps:
- persist_to_workspace:
root: .
paths:
- coverage.out
save_node_modules:
steps:
- persist_to_workspace:
root: ~/argo-cd
paths:
- ui/node_modules
save_go_cache:
steps:
- persist_to_workspace:
root: /tmp
paths:
- go-build-cache
attach_go_cache:
steps:
- attach_workspace:
at: /tmp
install_golang:
steps:
- run:
name: Install Golang v1.14.1
command: |
go get golang.org/dl/go1.14.1
[ -e /home/circleci/sdk/go1.14.1 ] || go1.14.1 download
go env
echo "export GOPATH=/home/circleci/.go_workspace" | tee -a $BASH_ENV
echo "export PATH=/home/circleci/sdk/go1.14.1/bin:\$PATH" | tee -a $BASH_ENV
jobs:
build:
docker:
- image: argoproj/argocd-test-tools:v0.5.0
working_directory: /go/src/github.com/argoproj/argo-cd
steps:
- prepare_environment
- checkout
- run: make build-local
- run: chmod -R 777 vendor
- run: chmod -R 777 ${GOCACHE}
- save_go_cache
codegen:
docker:
- image: argoproj/argocd-test-tools:v0.5.0
working_directory: /go/src/github.com/argoproj/argo-cd
steps:
- prepare_environment
- checkout
- attach_go_cache
- run: helm2 init --client-only
- run: make codegen-local
- run:
name: Check nothing has changed
command: |
set -xo pipefail
# This makes sure you ran `make pre-commit` before you pushed.
# We exclude the Swagger resources; CircleCI doesn't generate them correctly.
# When this fails, it will, create a patch file you can apply locally to fix it.
# To troubleshoot builds: https://argoproj.github.io/argo-cd/developer-guide/ci/
git diff --exit-code -- . ':!Gopkg.lock' ':!assets/swagger.json' | tee codegen.patch
- store_artifacts:
path: codegen.patch
destination: .
test:
working_directory: /go/src/github.com/argoproj/argo-cd
docker:
- image: argoproj/argocd-test-tools:v0.5.0
steps:
- prepare_environment
- checkout
- configure_git
- attach_go_cache
- run: make test-local
- run:
name: Uploading code coverage
command: bash <(curl -s https://codecov.io/bash) -f coverage.out
- run:
name: Output of test-results
command: |
ls -l test-results || true
cat test-results/junit.xml || true
- save_coverage_info
- store_test_results:
path: test-results
- store_artifacts:
path: test-results
destination: .
lint:
working_directory: /go/src/github.com/argoproj/argo-cd
docker:
- image: argoproj/argocd-test-tools:v0.5.0
steps:
- prepare_environment
- checkout
- configure_git
- attach_vendor
- store_go_cache_docker
- run:
name: Run golangci-lint
command: ARGOCD_LINT_GOGC=10 make lint-local
- run:
name: Check that nothing has changed
command: |
gDiff=$(git diff)
if test "$gDiff" != ""; then
echo
echo "###############################################################################"
echo "golangci-lint has made automatic corrections to your code. Please check below"
echo "diff output and commit this to your local branch, or run make lint locally."
echo "###############################################################################"
echo
git diff
exit 1
fi
sonarcloud:
working_directory: /go/src/github.com/argoproj/argo-cd
docker:
- image: argoproj/argocd-test-tools:v0.5.0
environment:
NODE_MODULES: /go/src/github.com/argoproj/argo-cd/ui/node_modules
steps:
- prepare_environment
- checkout
- attach_workspace:
at: .
- run:
command: mkdir -p /tmp/cache/scanner
name: Create cache directory if it doesn't exist
- restore_cache:
keys:
- v1-sonarcloud-scanner-4.2.0.1873
- run:
command: |
set -e
VERSION=4.2.0.1873
SONAR_TOKEN=$SONAR_TOKEN
SCANNER_DIRECTORY=/tmp/cache/scanner
export SONAR_USER_HOME=$SCANNER_DIRECTORY/.sonar
OS="linux"
echo $SONAR_USER_HOME
if [[ ! -x "$SCANNER_DIRECTORY/sonar-scanner-$VERSION-$OS/bin/sonar-scanner" ]]; then
curl -Ol https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$VERSION-$OS.zip
unzip -qq -o sonar-scanner-cli-$VERSION-$OS.zip -d $SCANNER_DIRECTORY
fi
chmod +x $SCANNER_DIRECTORY/sonar-scanner-$VERSION-$OS/bin/sonar-scanner
chmod +x $SCANNER_DIRECTORY/sonar-scanner-$VERSION-$OS/jre/bin/java
# Workaround for a possible bug in CircleCI
if ! echo $CIRCLE_PULL_REQUEST | grep https://github.com/argoproj; then
unset CIRCLE_PULL_REQUEST
unset CIRCLE_PULL_REQUESTS
fi
# Explicitly set NODE_MODULES
export NODE_MODULES=/go/src/github.com/argoproj/argo-cd/ui/node_modules
export NODE_PATH=/go/src/github.com/argoproj/argo-cd/ui/node_modules
$SCANNER_DIRECTORY/sonar-scanner-$VERSION-$OS/bin/sonar-scanner
name: SonarCloud
- save_cache:
key: v1-sonarcloud-scanner-4.2.0.1873
paths:
- /tmp/cache/scanner
e2e:
working_directory: /home/circleci/.go_workspace/src/github.com/argoproj/argo-cd
machine:
image: ubuntu-1604:201903-01
environment:
ARGOCD_FAKE_IN_CLUSTER: "true"
ARGOCD_SSH_DATA_PATH: "/tmp/argo-e2e/app/config/ssh"
ARGOCD_TLS_DATA_PATH: "/tmp/argo-e2e/app/config/tls"
ARGOCD_E2E_K3S: "true"
steps:
- run:
name: Install and start K3S v0.5.0
command: |
curl -sfL https://get.k3s.io | sh -
sudo chmod -R a+rw /etc/rancher/k3s
kubectl version
environment:
INSTALL_K3S_EXEC: --docker
INSTALL_K3S_VERSION: v0.5.0
- prepare_environment
- checkout
- run:
name: Fix permissions on filesystem
command: |
mkdir -p /home/circleci/.go_workspace/pkg/mod
chmod -R 777 /home/circleci/.go_workspace/pkg/mod
mkdir -p /tmp/go-build-cache
chmod -R 777 /tmp/go-build-cache
- attach_go_cache
- run:
name: Update kubectl configuration for container
command: |
ipaddr=$(ifconfig $IFACE |grep "inet " | awk '{print $2}')
if echo $ipaddr | grep -q 'addr:'; then
ipaddr=$(echo $ipaddr | awk -F ':' '{print $2}')
fi
test -d $HOME/.kube || mkdir -p $HOME/.kube
kubectl config view --raw | sed -e "s/127.0.0.1:6443/${ipaddr}:6443/g" -e "s/localhost:6443/${ipaddr}:6443/g" > $HOME/.kube/config
environment:
IFACE: ens4
- run:
name: Start E2E test server
command: make start-e2e
background: true
environment:
DOCKER_SRCDIR: /home/circleci/.go_workspace/src
ARGOCD_E2E_TEST: "true"
ARGOCD_IN_CI: "true"
GOPATH: /home/circleci/.go_workspace
- run:
name: Wait for API server to become available
command: |
count=1
until curl -v http://localhost:8080/healthz; do
sleep 10;
if test $count -ge 60; then
echo "Timeout"
exit 1
fi
count=$((count+1))
done
- run:
name: Run E2E tests
command: |
make test-e2e
environment:
ARGOCD_OPTS: "--plaintext"
ARGOCD_E2E_K3S: "true"
IFACE: ens4
DOCKER_SRCDIR: /home/circleci/.go_workspace/src
GOPATH: /home/circleci/.go_workspace
- store_test_results:
path: test-results
- store_artifacts:
path: test-results
destination: .
ui:
docker:
- image: node:11.15.0
working_directory: ~/argo-cd/ui
steps:
- checkout:
path: ~/argo-cd/
- restore_cache:
keys:
- yarn-packages-v4-{{ checksum "yarn.lock" }}
- run: yarn install --frozen-lockfile --ignore-optional --non-interactive
- save_cache:
key: yarn-packages-v4-{{ checksum "yarn.lock" }}
paths: [~/.cache/yarn, node_modules]
- run: yarn test
- run: ./node_modules/.bin/codecov -p ..
- run: NODE_ENV='production' yarn build
- run: yarn lint
- save_node_modules
orbs:
sonarcloud: sonarsource/sonarcloud@1.0.1
workflows:
version: 2
workflow:
jobs:
- build
- test:
requires:
- build
- codegen:
requires:
- build
- ui:
requires:
- build
- sonarcloud:
context: SonarCloud
requires:
- test
- ui
- e2e:
requires:
- build

View File

@@ -5,10 +5,6 @@ title: ''
labels: 'bug'
assignees: ''
---
If you are trying to resolve an environment-specific issue or have a one-off question about the edge case that does not require a feature then please consider asking a
question in argocd slack [channel](https://argoproj.github.io/community/join-slack).
Checklist:
* [ ] I've searched in the docs and FAQ for my answer: http://bit.ly/argocd-faq.

View File

@@ -3,5 +3,5 @@ Checklist:
* [ ] Either (a) I've created an [enhancement proposal](https://github.com/argoproj/argo-cd/issues/new/choose) and discussed it with the community, (b) this is a bug fix, or (c) this does not need to be in the release notes.
* [ ] The title of the PR states what changed and the related issues number (used for the release note).
* [ ] I've updated both the CLI and UI to expose my feature, or I plan to submit a second PR with them.
* [ ] Optional. My organization is added to USERS.md.
* [ ] Optional. My organization is added to the README.
* [ ] I've signed the CLA and my build is green ([troubleshooting builds](https://argoproj.github.io/argo-cd/developer-guide/ci/)).

View File

@@ -1,342 +0,0 @@
name: Integration tests
on:
push:
branches:
- 'master'
- 'release-*'
- '!release-1.4'
- '!release-1.5'
pull_request:
branches:
- 'master'
jobs:
check-go:
name: Ensure Go modules synchronicity
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Golang
uses: actions/setup-go@v1
with:
go-version: '1.14.2'
- name: Download all Go modules
run: |
go mod download
- name: Check for tidyness of go.mod and go.sum
run: |
go mod tidy
git diff --exit-code -- .
build-go:
name: Build & cache Go code
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Golang
uses: actions/setup-go@v1
with:
go-version: '1.14.2'
- name: Restore go build cache
uses: actions/cache@v1
with:
path: ~/.cache/go-build
key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
- name: Download all Go modules
run: |
go mod download
- name: Compile all packages
run: make build-local
lint-go:
name: Lint Go code
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v1
with:
version: v1.26
args: --timeout 5m
test-go:
name: Run unit tests for Go packages
runs-on: ubuntu-latest
needs:
- build-go
steps:
- name: Create checkout directory
run: mkdir -p ~/go/src/github.com/argoproj
- name: Checkout code
uses: actions/checkout@v2
- name: Create symlink in GOPATH
run: ln -s $(pwd) ~/go/src/github.com/argoproj/argo-cd
- name: Setup Golang
uses: actions/setup-go@v1
with:
go-version: '1.14.2'
- name: Install required packages
run: |
sudo apt-get install git -y
- name: Switch to temporal branch so we re-attach head
run: |
git switch -c temporal-pr-branch
git status
- name: Fetch complete history for blame information
run: |
git fetch --prune --no-tags --depth=1 origin +refs/heads/*:refs/remotes/origin/*
- name: Add ~/go/bin to PATH
run: echo "::add-path::/home/runner/go/bin"
- name: Add /usr/local/bin to PATH
run: echo "::add-path::/usr/local/bin"
- name: Restore go build cache
uses: actions/cache@v1
with:
path: ~/.cache/go-build
key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
- name: Install all tools required for building & testing
run: |
make install-test-tools-local
- name: Setup git username and email
run: |
git config --global user.name "John Doe"
git config --global user.email "john.doe@example.com"
- name: Download and vendor all required packages
run: |
go mod download
- name: Run all unit tests
run: make test-local
- name: Generate code coverage artifacts
uses: actions/upload-artifact@v2
with:
name: code-coverage
path: coverage.out
- name: Generate test results artifacts
uses: actions/upload-artifact@v2
with:
name: test-results
path: test-results/
codegen:
name: Check changes to generated code
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Golang
uses: actions/setup-go@v1
with:
go-version: '1.14.2'
- name: Create symlink in GOPATH
run: |
mkdir -p ~/go/src/github.com/argoproj
cp -a ../argo-cd ~/go/src/github.com/argoproj
- name: Add /usr/local/bin to PATH
run: echo "::add-path::/usr/local/bin"
- name: Add ~/go/bin to PATH
run: echo "::add-path::/home/runner/go/bin"
- name: Download & vendor dependencies
run: |
# We need to vendor go modules for codegen yet
go mod download
go mod vendor -v
working-directory: /home/runner/go/src/github.com/argoproj/argo-cd
- name: Install toolchain for codegen
run: |
make install-codegen-tools-local
make install-go-tools-local
working-directory: /home/runner/go/src/github.com/argoproj/argo-cd
- name: Initialize local Helm
run: |
helm2 init --client-only
- name: Run codegen
run: |
set -x
export GOPATH=$(go env GOPATH)
make codegen-local
working-directory: /home/runner/go/src/github.com/argoproj/argo-cd
- name: Check nothing has changed
run: |
set -xo pipefail
git diff --exit-code -- . ':!go.sum' ':!go.mod' ':!assets/swagger.json' | tee codegen.patch
working-directory: /home/runner/go/src/github.com/argoproj/argo-cd
build-ui:
name: Build, test & lint UI code
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup NodeJS
uses: actions/setup-node@v1
with:
node-version: '11.15.0'
- name: Restore node dependency cache
id: cache-dependencies
uses: actions/cache@v1
with:
path: ui/node_modules
key: ${{ runner.os }}-node-dep-v2-${{ hashFiles('**/yarn.lock') }}
- name: Install node dependencies
run: |
cd ui && yarn install --frozen-lockfile --ignore-optional --non-interactive
- name: Build UI code
run: |
yarn test
yarn build
env:
NODE_ENV: production
working-directory: ui/
- name: Run ESLint
run: yarn lint
working-directory: ui/
analyze:
name: Process & analyze test artifacts
runs-on: ubuntu-latest
needs:
- test-go
- build-ui
env:
sonar_secret: ${{ secrets.SONAR_TOKEN }}
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Restore node dependency cache
id: cache-dependencies
uses: actions/cache@v1
with:
path: ui/node_modules
key: ${{ runner.os }}-node-dep-v2-${{ hashFiles('**/yarn.lock') }}
- name: Remove other node_modules directory
run: |
rm -rf ui/node_modules/argo-ui/node_modules
- name: Create test-results directory
run: |
mkdir -p test-results
- name: Get code coverage artifiact
uses: actions/download-artifact@v2
with:
name: code-coverage
- name: Get test result artifact
uses: actions/download-artifact@v2
with:
name: test-results
path: test-results
- name: Upload code coverage information to codecov.io
uses: codecov/codecov-action@v1
with:
file: coverage.out
- name: Perform static code analysis using SonarCloud
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SCANNER_VERSION: 4.2.0.1873
SCANNER_PATH: /tmp/cache/scanner
OS: linux
run: |
# We do not use the provided action, because it does contain an old
# version of the scanner, and also takes time to build.
set -e
mkdir -p ${SCANNER_PATH}
export SONAR_USER_HOME=${SCANNER_PATH}/.sonar
if [[ ! -x "${SCANNER_PATH}/sonar-scanner-${SCANNER_VERSION}-${OS}/bin/sonar-scanner" ]]; then
curl -Ol https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${SCANNER_VERSION}-${OS}.zip
unzip -qq -o sonar-scanner-cli-${SCANNER_VERSION}-${OS}.zip -d ${SCANNER_PATH}
fi
chmod +x ${SCANNER_PATH}/sonar-scanner-${SCANNER_VERSION}-${OS}/bin/sonar-scanner
chmod +x ${SCANNER_PATH}/sonar-scanner-${SCANNER_VERSION}-${OS}/jre/bin/java
# Explicitly set NODE_MODULES
export NODE_MODULES=${PWD}/ui/node_modules
export NODE_PATH=${PWD}/ui/node_modules
${SCANNER_PATH}/sonar-scanner-${SCANNER_VERSION}-${OS}/bin/sonar-scanner
if: env.sonar_secret != ''
test-e2e:
name: Run end-to-end tests
runs-on: ubuntu-latest
needs:
- build-go
env:
GOPATH: /home/runner/go
ARGOCD_FAKE_IN_CLUSTER: "true"
ARGOCD_SSH_DATA_PATH: "/tmp/argo-e2e/app/config/ssh"
ARGOCD_TLS_DATA_PATH: "/tmp/argo-e2e/app/config/tls"
ARGOCD_E2E_SSH_KNOWN_HOSTS: "../fixture/certs/ssh_known_hosts"
ARGOCD_E2E_K3S: "true"
ARGOCD_IN_CI: "true"
ARGOCD_E2E_APISERVER_PORT: "8088"
ARGOCD_SERVER: "127.0.0.1:8088"
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Golang
uses: actions/setup-go@v1
with:
go-version: '1.14.2'
- name: Install K3S
env:
INSTALL_K3S_VERSION: v0.5.0
run: |
set -x
curl -sfL https://get.k3s.io | sh -
sudo chmod -R a+rw /etc/rancher/k3s
sudo mkdir -p $HOME/.kube && sudo chown -R runner $HOME/.kube
sudo k3s kubectl config view --raw > $HOME/.kube/config
sudo chown runner $HOME/.kube/config
kubectl version
- name: Restore go build cache
uses: actions/cache@v1
with:
path: ~/.cache/go-build
key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
- name: Add /usr/local/bin to PATH
run: echo "::add-path::/usr/local/bin"
- name: Add ~/go/bin to PATH
run: echo "::add-path::/home/runner/go/bin"
- name: Download Go dependencies
run: |
go mod download
go get github.com/mattn/goreman
- name: Install all tools required for building & testing
run: |
make install-test-tools-local
- name: Setup git username and email
run: |
git config --global user.name "John Doe"
git config --global user.email "john.doe@example.com"
- name: Pull Docker image required for tests
run: |
docker pull quay.io/dexidp/dex:v2.22.0
docker pull argoproj/argo-cd-ci-builder:v1.0.0
docker pull redis:5.0.3-alpine
- name: Run E2E server and wait for it being available
timeout-minutes: 30
run: |
set -x
# Something is weird in GH runners -- there's a phantom listener for
# port 8080 which is not visible in netstat -tulpen, but still there
# with a HTTP listener. We have API server listening on port 8088
# instead.
make start-e2e-local &
count=1
until curl -f http://127.0.0.1:8088/healthz; do
sleep 10;
if test $count -ge 60; then
echo "Timeout"
exit 1
fi
count=$((count+1))
done
- name: Run E2E testsuite
run: |
set -x
make test-e2e-local

View File

@@ -16,7 +16,7 @@ jobs:
python-version: 3.x
- name: build
run: |
pip install mkdocs==1.0.4 mkdocs_material==4.1.1
pip install mkdocs mkdocs_material
mkdocs build
mkdir ./site/.circleci && echo '{version: 2, jobs: {build: {branches: {ignore: gh-pages}}}}' > ./site/.circleci/config.yml
- name: deploy

View File

@@ -1,50 +0,0 @@
name: Image
on:
push:
branches:
- master
jobs:
publish:
runs-on: ubuntu-latest
env:
GOPATH: /home/runner/work/argo-cd/argo-cd
steps:
- uses: actions/setup-go@v1
with:
go-version: '1.14.1'
- uses: actions/checkout@master
with:
path: src/github.com/argoproj/argo-cd
# get image tag
- run: echo ::set-output name=tag::$(cat ./VERSION)-${GITHUB_SHA::8}
working-directory: ./src/github.com/argoproj/argo-cd
id: image
# build
- run: |
docker images -a --format "{{.ID}}" | xargs -I {} docker rmi {}
make image DEV_IMAGE=true DOCKER_PUSH=false IMAGE_NAMESPACE=docker.pkg.github.com/argoproj/argo-cd IMAGE_TAG=${{ steps.image.outputs.tag }}
working-directory: ./src/github.com/argoproj/argo-cd
# publish
- run: |
docker login docker.pkg.github.com --username $USERNAME --password $PASSWORD
docker push docker.pkg.github.com/argoproj/argo-cd/argocd:${{ steps.image.outputs.tag }}
env:
USERNAME: ${{ secrets.USERNAME }}
PASSWORD: ${{ secrets.TOKEN }}
# deploy
- run: git clone "https://$TOKEN@github.com/argoproj/argoproj-deployments"
env:
TOKEN: ${{ secrets.TOKEN }}
- run: |
docker run -v $(pwd):/src -w /src --rm -t lyft/kustomizer:v3.3.0 kustomize edit set image argoproj/argocd=docker.pkg.github.com/argoproj/argo-cd/argocd:${{ steps.image.outputs.tag }}
git config --global user.email 'ci@argoproj.com'
git config --global user.name 'CI'
git diff --exit-code && echo 'Already deployed' || (git commit -am 'Upgrade argocd to ${{ steps.image.outputs.tag }}' && git push)
working-directory: argoproj-deployments/argocd
# TODO: clean up old images once github supports it: https://github.community/t5/How-to-use-Git-and-GitHub/Deleting-images-from-Github-Package-Registry/m-p/41202/thread-id/9811

View File

@@ -1,289 +0,0 @@
name: Create ArgoCD release
on:
push:
tags:
- 'release-v*'
- '!release-v1.5*'
- '!release-v1.4*'
- '!release-v1.3*'
- '!release-v1.2*'
- '!release-v1.1*'
- '!release-v1.0*'
- '!release-v0*'
jobs:
prepare-release:
name: Perform automatic release on trigger ${{ github.ref }}
runs-on: ubuntu-latest
env:
# The name of the tag as supplied by the GitHub event
SOURCE_TAG: ${{ github.ref }}
# The image namespace where Docker image will be published to
IMAGE_NAMESPACE: argoproj
# Whether to create & push image and release assets
DRY_RUN: false
# Whether a draft release should be created, instead of public one
DRAFT_RELEASE: false
# The name of the repository containing tap formulae
TAP_REPOSITORY: argoproj/homebrew-tap
# Whether to update homebrew with this release as well
# Set RELEASE_HOMEBREW_TOKEN secret in repository for this to work - needs
# access to public repositories (or homebrew-tap repo specifically)
UPDATE_HOMEBREW: false
# Name of the GitHub user for Git config
GIT_USERNAME: argo-bot
# E-Mail of the GitHub user for Git config
GIT_EMAIL: argoproj@gmail.com
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Check if the published tag is well formed and setup vars
run: |
set -xue
# Target version must match major.minor.patch and optional -rcX suffix
# where X must be a number.
TARGET_VERSION=${SOURCE_TAG#*release-v}
if ! echo ${TARGET_VERSION} | egrep '^[0-9]+\.[0-9]+\.[0-9]+(-rc[0-9]+)*$'; then
echo "::error::Target version '${TARGET_VERSION}' is malformed, refusing to continue." >&2
exit 1
fi
# Target branch is the release branch we're going to operate on
# Its name is 'release-<major>.<minor>'
TARGET_BRANCH="release-${TARGET_VERSION%\.[0-9]*}"
# The release tag is the source tag, minus the release- prefix
RELEASE_TAG="${SOURCE_TAG#*release-}"
# Whether this is a pre-release (indicated by -rc suffix)
PRE_RELEASE=false
if echo "${RELEASE_TAG}" | egrep -- '-rc[0-9]+$'; then
PRE_RELEASE=true
fi
# We must not have a release trigger within the same release branch,
# because that means a release for this branch is already running.
if git tag -l | grep "release-v${TARGET_VERSION%\.[0-9]*}" | grep -v "release-v${TARGET_VERSION}"; then
echo "::error::Another release for branch ${TARGET_BRANCH} is currently in progress."
exit 1
fi
# Ensure that release do not yet exist
if git rev-parse ${RELEASE_TAG}; then
echo "::error::Release tag ${RELEASE_TAG} already exists in repository. Refusing to continue."
exit 1
fi
# Make the variables available in follow-up steps
echo "::set-env name=TARGET_VERSION::${TARGET_VERSION}"
echo "::set-env name=TARGET_BRANCH::${TARGET_BRANCH}"
echo "::set-env name=RELEASE_TAG::${RELEASE_TAG}"
echo "::set-env name=PRE_RELEASE::${PRE_RELEASE}"
- name: Check if our release tag has a correct annotation
run: |
set -ue
# Fetch all tag information as well
git fetch --prune --tags --force
echo "=========== BEGIN COMMIT MESSAGE ============="
git show ${SOURCE_TAG}
echo "============ END COMMIT MESSAGE =============="
# Quite dirty hack to get the release notes from the annotated tag
# into a temporary file.
RELEASE_NOTES=$(mktemp -p /tmp release-notes.XXXXXX)
prefix=true
begin=false
git show ${SOURCE_TAG} | while read line; do
# Whatever is in commit history for the tag, we only want that
# annotation from our tag. We discard everything else.
if test "$begin" = "false"; then
if echo $line | grep -q "tag ${SOURCE_TAG#refs/tags/}"; then begin="true"; fi
continue
fi
if test "$prefix" = "true"; then
if test -z "$line"; then prefix=false; fi
else
if echo $line | egrep -q '^commit [0-9a-f]+'; then
break
fi
echo $line >> ${RELEASE_NOTES}
fi
done
# For debug purposes
echo "============BEGIN RELEASE NOTES================="
cat ${RELEASE_NOTES}
echo "=============END RELEASE NOTES=================="
# Too short release notes are suspicious. We need at least 100 bytes.
relNoteLen=$(stat -c '%s' $RELEASE_NOTES)
if test $relNoteLen -lt 100; then
echo "::error::No release notes provided in tag annotation (or tag is not annotated)"
exit 1
fi
# Check for magic string '## Quick Start' in head of release notes
if ! head -2 ${RELEASE_NOTES} | grep -iq '## Quick Start'; then
echo "::error::Release notes seem invalid, quick start section not found."
exit 1
fi
# We store path to temporary release notes file for later reading, we
# need it when creating release.
echo "::set-env name=RELEASE_NOTES::$RELEASE_NOTES"
- name: Setup Golang
uses: actions/setup-go@v1
with:
go-version: '1.14.2'
- name: Setup Git author information
run: |
set -ue
git config --global user.email "${GIT_EMAIL}"
git config --global user.name "${GIT_USERNAME}"
- name: Checkout corresponding release branch
run: |
set -ue
echo "Switching to release branch '${TARGET_BRANCH}'"
if ! git checkout ${TARGET_BRANCH}; then
echo "::error::Checking out release branch '${TARGET_BRANCH}' for target version '${TARGET_VERSION}' (tagged '${RELEASE_TAG}') failed. Does it exist in repo?"
exit 1
fi
- name: Create VERSION information
run: |
set -ue
echo "Bumping version from $(cat VERSION) to ${TARGET_VERSION}"
echo "${TARGET_VERSION}" > VERSION
git commit -m "Bump version to ${TARGET_VERSION}" VERSION
- name: Generate new set of manifests
run: |
set -ue
make install-codegen-tools-local
helm2 init --client-only
make manifests-local VERSION=${TARGET_VERSION}
git diff
git commit manifests/ -m "Bump version to ${TARGET_VERSION}"
- name: Create the release tag
run: |
set -ue
echo "Creating release ${RELEASE_TAG}"
git tag ${RELEASE_TAG}
- name: Build Docker image for release
run: |
set -ue
git clean -fd
mkdir -p dist/
make image IMAGE_TAG="${TARGET_VERSION}" DOCKER_PUSH=false
make release-cli
chmod +x ./dist/argocd-linux-amd64
./dist/argocd-linux-amd64 version --client
if: ${{ env.DRY_RUN != 'true' }}
- name: Push docker image to repository
env:
DOCKER_USERNAME: ${{ secrets.RELEASE_DOCKERHUB_USERNAME }}
DOCKER_TOKEN: ${{ secrets.RELEASE_DOCKERHUB_TOKEN }}
run: |
set -ue
docker login --username "${DOCKER_USERNAME}" --password "${DOCKER_TOKEN}"
docker push ${IMAGE_NAMESPACE}/argocd:v${TARGET_VERSION}
if: ${{ env.DRY_RUN != 'true' }}
- name: Read release notes file
id: release-notes
uses: juliangruber/read-file-action@v1
with:
path: ${{ env.RELEASE_NOTES }}
- name: Push changes to release branch
run: |
set -ue
git push origin ${TARGET_BRANCH}
git push origin ${RELEASE_TAG}
- name: Create GitHub release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
id: create_release
with:
tag_name: ${{ env.RELEASE_TAG }}
release_name: ${{ env.RELEASE_TAG }}
draft: ${{ env.DRAFT_RELEASE }}
prerelease: ${{ env.PRE_RELEASE }}
body: ${{ steps.release-notes.outputs.content }}
- name: Upload argocd-linux-amd64 binary to release assets
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dist/argocd-linux-amd64
asset_name: argocd-linux-amd64
asset_content_type: application/octet-stream
if: ${{ env.DRY_RUN != 'true' }}
- name: Upload argocd-darwin-amd64 binary to release assets
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dist/argocd-darwin-amd64
asset_name: argocd-darwin-amd64
asset_content_type: application/octet-stream
if: ${{ env.DRY_RUN != 'true' }}
- name: Upload argocd-windows-amd64 binary to release assets
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dist/argocd-windows-amd64.exe
asset_name: argocd-windows-amd64.exe
asset_content_type: application/octet-stream
if: ${{ env.DRY_RUN != 'true' }}
- name: Check out homebrew tap repository
uses: actions/checkout@v2
env:
HOMEBREW_TOKEN: ${{ secrets.RELEASE_HOMEBREW_TOKEN }}
with:
repository: ${{ env.TAP_REPOSITORY }}
path: homebrew-tap
fetch-depth: 0
token: ${{ env.HOMEBREW_TOKEN }}
if: ${{ env.HOMEBREW_TOKEN != '' && env.UPDATE_HOMEBREW == 'true' && env.PRE_RELEASE != 'true' }}
- name: Update homebrew tap formula
env:
HOMEBREW_TOKEN: ${{ secrets.RELEASE_HOMEBREW_TOKEN }}
run: |
set -ue
cd homebrew-tap
./update.sh argocd ${TARGET_VERSION}
git commit -am "Update argocd to ${TARGET_VERSION}"
git push
cd ..
rm -rf homebrew-tap
if: ${{ env.HOMEBREW_TOKEN != '' && env.UPDATE_HOMEBREW == 'true' && env.PRE_RELEASE != 'true' }}
- name: Delete original request tag from repository
run: |
set -ue
git push --delete origin ${SOURCE_TAG}
if: ${{ always() }}

4
.gitignore vendored
View File

@@ -9,6 +9,4 @@ site/
cmd/**/debug
debug.test
coverage.out
test-results
.scannerwork
.scratch
test-results

View File

@@ -1,244 +1,6 @@
# Changelog
## v1.5.5 (2020-05-16)
- feat: add Rollout restart action (#3557)
- fix: enable redis retries; add redis request duration metric (#3547)
- fix: when --rootpath is on, 404 is returned when URL contains encoded URI (#3564)
## v1.5.4 (2020-05-05)
- fix: CLI commands with --grpc-web
## v1.5.3 (2020-05-01)
This patch release introduces a set of enhancements and bug fixes. Here are most notable changes:
#### Multiple Kustomize Versions
The bundled Kustomize version had been upgraded to v3.5.4. Argo CD allows changing bundled version using
[custom image or init container](https://argoproj.github.io/argo-cd/operator-manual/custom_tools/).
This [feature](https://argoproj.github.io/argo-cd/user-guide/kustomize/#custom-kustomize-versions)
enables bundling multiple Kustomize versions at the same time and allows end-users to specify the required version per application.
#### Custom Root Path
The feature allows accessing Argo CD UI and API using a custom root path(for example https://myhostname/argocd).
This enables running Argo CD behind a proxy that takes care of user authentication (such as Ambassador) or hosting
multiple Argo CD using the same hostname. A set of bug fixes and enhancements had been implemented to makes it easier.
Use new `--rootpath` [flag](https://argoproj.github.io/argo-cd/operator-manual/ingress/#argocd-server-and-ui-root-path-v153) to enable the feature.
### Login Rate Limiting
The feature prevents a built-in user password brute force attack and addresses the known
[vulnerability](https://argoproj.github.io/argo-cd/security_considerations/#cve-2020-8827-insufficient-anti-automationanti-brute-force).
### Settings Management Tools
A new set of [CLI commands](https://argoproj.github.io/argo-cd/operator-manual/troubleshooting/) that simplify configuring Argo CD.
Using the CLI you can test settings changes offline without affecting running Argo CD instance and have ability to troubleshot diffing
customizations, custom resource health checks, and more.
### Other
* New Project and Application CRD settings ([#2900](https://github.com/argoproj/argo-cd/issues/2900), [#2873](https://github.com/argoproj/argo-cd/issues/2873)) that allows customizing Argo CD behavior.
* Upgraded Dex (v2.22.0) enables seamless [SSO integration](https://www.openshift.com/blog/openshift-authentication-integration-with-argocd) with Openshift.
#### Enhancements
* feat: added --grpc-web-root-path for CLI. (#3483)
* feat: limit the maximum number of concurrent login attempts (#3467)
* feat: upgrade kustomize version to 3.5.4 (#3472)
* feat: upgrade dex to 2.22.0 (#3468)
* feat: support user specified account token ids (#3425)
* feat: support separate Kustomize version per application (#3414)
* feat: add support for dex prometheus metrics (#3249)
* feat: add settings troubleshooting commands to the 'argocd-util' binary (#3398)
* feat: Let user to define meaningful unique JWT token name (#3388)
* feat: Display link between OLM ClusterServiceVersion and it's OperatorGroup (#3390)
* feat: Introduce sync-option SkipDryRunOnMissingResource=true (#2873) (#3247)
* feat: support normalizing CRD fields that use known built-in K8S types (#3357)
* feat: Whitelisted namespace resources (#2900)
#### Bug Fixes
* fix: added path to cookie (#3501)
* fix: 'argocd sync' does not take into account IgnoreExtraneous annotation (#3486)
* fix: CLI renders flipped diff results (#3480)
* fix: GetApplicationSyncWindows API should not validate project permissions (#3456)
* fix: argocd-util kubeconfig should use RawRestConfig to export config (#3447)
* fix: javascript error on accounts list page (#3453)
* fix: support both <group>/<kind> as well as <kind> as a resource override key (#3433)
* fix: Updating to jsonnet v1.15.0 fix issue #3277 (#3431)
* fix for helm repo add with flag --insecure-skip-server-verification (#3420)
* fix: app diff --local support for helm repo. #3151 (#3407)
* fix: Syncing apps incorrectly states "app synced", but this is not true (#3286)
* fix: for jsonnet when it is localed in nested subdirectory and uses import (#3372)
* fix: Update 4.5.3 redis-ha helm manifest (#3370)
* fix: return 401 error code if username does not exist (#3369)
* fix: Do not panic while running hooks with short revision (#3368)
## v1.5.2 (2020-04-20)
#### Critical security fix
This release contains a critical security fix. Please refer to the
[security document](https://argoproj.github.io/argo-cd/security_considerations/#CVE-2020-5260-possible-git-credential-leak)
for more information.
**Upgrading is strongly recommended**
## v1.4.3 (2020-04-20)
#### Critical security fix
This release contains a critical security fix. Please refer to the
[security document](https://argoproj.github.io/argo-cd/security_considerations/#CVE-2020-5260-possible-git-credential-leak)
for more information.
## v1.5.1 (2020-04-06)
#### Bug Fixes
* fix: return 401 error code if username does not exist (#3369)
* fix: Do not panic while running hooks with short revision (#3368)
* fix: Increase HAProxy check interval to prevent intermittent failures (#3356)
* fix: Helm v3 CRD are not deployed (#3345)
## v1.5.0 (2020-04-02)
#### Helm Integration Enhancements - Helm 3 Support And More
Introduced native support Helm3 charts. For backward compatibility Helm 2 charts are still rendered using Helm 2 CLI. Argo CD inspects the
Charts.yaml file and choose the right binary based on `apiVersion` value.
Following enhancement were implemented in addition to Helm 3:
* The `--api-version` flag is passed to the `helm template` command during manifest generation.
* The `--set-file` flag can be specified in the application specification.
* Fixed bug that prevents automatically update Helm chart when new version is published (#3193)
#### Better Performance and Improved Metrics
If you are running Argo CD instances with several hundred applications on it, you should see a
huge performance boost and significantly less Kubernetes API server load.
The Argo CD controller Prometheus metrics have been reworked to enable a richer Grafana dashboard.
The improved dashboard is available at [examples/dashboard.json](https://github.com/argoproj/argo-cd/blob/master/examples/dashboard.json).
You can set `ARGOCD_LEGACY_CONTROLLER_METRICS=true` environment variable and use [examples/dashboard-legacy.json](https://github.com/argoproj/argo-cd/blob/master/examples/dashboard-legacy.json)
to keep using old dashboard.
#### Local accounts
The local accounts had been introduced additional to `admin` user and SSO integration. The feature is useful for creating authentication
tokens with limited permissions to automate Argo CD management. Local accounts also could be used small by teams when SSO integration is overkill.
This enhancement also allows to disable admin user and enforce only SSO logins.
#### Redis HA Proxy mode
As part of this release, the bundled Redis was upgraded to version 4.3.4 with enabled HAProxy.
The HA proxy replaced the sentinel and provides more reliable Redis connection.
> After publishing 1.5.0 release we've discovered that default HAProxy settings might cause intermittent failures.
> See [argo-cd#3358](https://github.com/argoproj/argo-cd/issues/3358)
#### Windows CLI
Windows users deploy to Kubernetes too! Now you can use Argo CD CLI on Linux, Mac OS, and Windows. The Windows compatible binary is available
in the release details page as well as on the Argo CD Help page.
#### Breaking Changes
The `argocd_app_sync_status`, `argocd_app_health_status` and `argocd_app_created_time` prometheus metrics are deprecated in favor of additional labels
to `argocd_app_info` metric. The deprecated labels are still available can be re-enabled using `ARGOCD_LEGACY_CONTROLLER_METRICS=true` environment variable.
The legacy example Grafana dashboard is available at [examples/dashboard-legacy.json](https://github.com/argoproj/argo-cd/blob/master/examples/dashboard-legacy.json).
#### Known issues
Last-minute bugs that will be addressed in 1.5.1 shortly:
* https://github.com/argoproj/argo-cd/issues/3336
* https://github.com/argoproj/argo-cd/issues/3319
* https://github.com/argoproj/argo-cd/issues/3339
* https://github.com/argoproj/argo-cd/issues/3358
#### Enhancements
* feat: support helm3 (#2383) (#3178)
* feat: Argo CD Service Account / Local Users #3185
* feat: Disable Admin Login (fixes #3019) (#3179)
* feat(ui): add docs to sync policy options present in create application panel (Close #3098) (#3203)
* feat: add "service-account" flag to "cluster add" command (#3183) (#3184)
* feat: Supports the validate-false option at an app level. Closes #1063 (#2542)
* feat: add dest cluster and namespace in the Events (#3093)
* feat: Rollback disables auto sync issue #2441 (#2591)
* feat: allow ssh and http repository references in bitbucketserver webhook #2773 (#3036)
* feat: Add helm --set-file support (#2751)
* feat: Include resource group for Event's InvolvedObject.APIVersion
* feat: Add argocd cmd for Windows #2121 (#3015)
#### Bug Fixes
- fix: app reconciliation fails with panic: index out of (#3233)
- fix: upgrade argoproj/pkg version to fix leaked sensitive information in logs (#3230)
- fix: set MaxCallSendMsgSize to MaxGRPCMessageSize for the GRPC caller (#3117)
- fix: stop caching helm index (#3193)
- fix: dex proxy should forward request to dex preserving the basehref (#3165)
- fix: set default login redirect to baseHRef (#3164)
- fix: don't double-prepend basehref to redirect URLs (fixes #3137)
- fix: ui referring to /api/version using absolute path (#3092)
- fix: Unhang UI on long app info items by using more sane URL match pattern (#3159)
- fix: Allow multiple hostnames per SSH known hosts entry and also allow IPv6 (#2814) (#3074)
- fix: argocd-util backup produced truncated backups. import app status (#3096)
- fix: upgrade redis-ha chart and enable haproxy (#3147)
- fix: make dex server deployment init container resilient to restarts (#3136)
- fix: reduct secret values of manifests stored in git (#3088)
- fix: labels not being deleted via UI (#3081)
- fix: HTTP|HTTPS|NO_PROXY env variable reading #3055 (#3063)
- fix: Correct usage text for repo add command regarding insecure repos (#3068)
- fix: Ensure SSH private key is written out with a final newline character (#2890) (#3064)
- fix: Handle SSH URLs in 'git@server:org/repo' notation correctly (#3062)
- fix sso condition when several sso connectors has been configured (#3057)
- fix: Fix bug where the same pointer is used. (#3059)
- fix: Opening in new tab bad key binding on Linux (#3020)
- fix: K8s secrets for repository credential templates are not deleted when credential template is deleted (#3028)
- fix: SSH credential template not working #3016
- fix: Unable to parse kubectl pre-release version strings (#3034)
- fix: Jsonnet TLA parameters of same type are overwritten (#3022)
- fix: Replace aws-iam-authenticator to support IRSA (#3010)
- fix: Hide bindPW in dex config (#3025)
- fix: SSH repo URL with a user different from `git` is not matched correctly when resolving a webhook (#2988)
- fix: JWT invalid => Password for superuser has changed since token issued (#2108)
#### Contributors
* alexandrfox
* alexec
* alexmt
* bergur88
* CBytelabs
* dbeal-wiser
* dnascimento
* Elgarni
* eSamS
* gpaul
* jannfis
* jdmulloy
* machgo
* masa213f
* matthyx
* rayanebel
* shelby-moore
* tomcruise81
* wecger
* zeph
## v1.4.2 (2020-01-24)
- fix: correctly replace cache in namespace isolation mode (#3023)
## v1.4.1 (2020-01-23)
- fix: impossible to config RBAC if group name includes ',' (#3013)
## v1.4.0 (2020-01-17)
## v1.4.0 (Not Released)
The v1.4.0 is a stability release that brings multiple bug fixes, security, performance enhancements, and multiple usability improvements.
@@ -359,55 +121,18 @@ an in-flight state for all Kubernetes resources including `Deployment`, `PVC`, `
#### Contributors
* Aalok Ahluwalia
* Aananth K
* Abhishek Jaisingh
* Adam Johnson
* Alan Tang
* Alex Collins
* Alexander Matyushentsev
* Andrew Waters
* Byungjin Park
* Christine Banek
* Daniel Helfand
* David Hong
* David J. M. Karlsen
* David Maciel
* Devan Goodwin
* Devin Stein
* dthomson25
* Gene Liverman
* Gregor Krmelj
* Guido Maria Serra
* Ilir Bekteshi
* Imran Ismail
* INOUE BANJI
* Isaac Gaskin
* jannfis
* Jeff Hastings
* Jesse Suen
* John Girvan
* Konstantin
* Lev Aminov
* Manatsawin Hanmongkolchai
* Marco Schmid
* Masayuki Ishii
* Michael Bridgen
* Naoki Oketani
* niqdev
* nitinpatil1992
* Olivier Boukili
* Olivier Lemasle
* Omer Kahani
* Paul Brit
* Qingbo Zhou
* Saradhi Sreegiriraju
* Scott Cabrinha
* shlo
* Simon Behar
* stgarf
* Yujun Zhang
* Zoltán Reegn
* [@abhishekjiitr](https://github.com/abhishekjiitr)
* [@adamjohnson01](https://github.com/adamjohnson01)
* [@alexec](https://github.com/alexec)
* [@alexmt](https://github.com/alexmt)
* [@binoue](https://github.com/binoue)
* [@cabrinha](https://github.com/cabrinha)
* [@cbanek](https://github.com/cbanek)
* [@dgoodwin](https://github.com/dgoodwin)
* [@jannfis](https://github.com/jannfis)
* [@jessesuen](https://github.com/jessesuen)
* [@masa213f](https://github.com/masa213f)
* [@whs](https://github.com/whs)
## v1.3.4 (2019-12-05)
- #2819 Fixes logging of tracing option in CLI
@@ -442,7 +167,7 @@ https://youtu.be/GP7xtrnNznw
##### Orphan Resources
Some users would like to make sure that resources in a namespace are managed only by Argo CD. So we've introduced the concept of an "orphan resource" - any resource that is in namespace associated with an app, but not managed by Argo CD. This is enabled in the project settings. Once enabled, Argo CD will show in the app view any resources in the app's namespace that is not managed by Argo CD.
Some users would like to make sure that resources in a namespace are managed only by Argo CD. So we've introduced the concept of an "orphan resource" - any resource that is in namespace associated with an app, but not managed by Argo CD. This is enabled in the project settings. Once enabled, Argo CD will show in the app view any resources in the app's namepspace that is not mananged by Argo CD.
https://youtu.be/9ZoTevVQf5I
@@ -485,7 +210,7 @@ There may be instances when you want to control the times during which an Argo C
#### Bug Fixes
- failed parsing on parameters with comma (#1660)
- Statefulset with OnDelete Update Strategy stuck progressing (#1881)
- Statefuleset with OnDelete Update Strategy stuck progressing (#1881)
- Warning during secret diffing (#1923)
- Error message "Unable to load data: key is missing" is confusing (#1944)
- OIDC group bindings are truncated (#2006)
@@ -524,7 +249,7 @@ There may be instances when you want to control the times during which an Argo C
- Creating an application from Helm repository should select "Helm" as source type (#2378)
- The parameters of ValidateAccess GRPC method should not be logged (#2386)
- Maintenance window meaning is confusing (#2398)
- UI bug when targetRevision is omitted (#2407)
- UI bug when targetRevision is ommited (#2407)
- Too many vulnerabilities in Docker image (#2425)
- proj windows commands not consistent with other commands (#2443)
- Custom resource actions cannot be executed from the UI (#2448)
@@ -553,10 +278,6 @@ There may be instances when you want to control the times during which an Argo C
- Application controller `kubectl-parallelism-limit` flag is broken (#2673)
- Annoying toolbar flickering (#2691)
## v1.2.5 (2019-10-29)
- Issue #2339 - Don't update `status.reconciledAt` unless compared with latest git version (#2581)
## v1.2.4 (2019-10-23)
- Issue #2185 - Manual sync don't trigger hooks (#2477)
@@ -618,7 +339,7 @@ Support for Git LFS enabled repositories - now you can store Helm charts as tar
+ Added 'SyncFail' to possible HookTypes in UI (#2147)
+ Support for Git LFS enabled repositories (#1853)
+ Server certificate and known hosts management (#1514)
+ Client HTTPS certificates for private git repositories (#1945)
+ Client HTTPS certifcates for private git repositories (#1945)
+ Badge for application status (#1435)
+ Make the health check for APIService a built in (#1841)
+ Bitbucket Server and Gogs webhook providers (#1269)
@@ -658,7 +379,7 @@ Support for Git LFS enabled repositories - now you can store Helm charts as tar
- Fix history api fallback implementation to support app names with dots (#2114)
- Fixes some code issues related to Kustomize build options. (#2146)
- Adds checks around valid paths for apps (#2133)
- Endpoint incorrectly considered top level managed resource (#2060)
- Enpoint incorrectly considered top level managed resource (#2060)
- Allow adding certs for hostnames ending on a dot (#2116)
#### Other
@@ -981,7 +702,7 @@ Argo CD introduces some additional CLI commands:
#### Label selector changes, dex-server rename
The label selectors for deployments were been renamed to use kubernetes common labels
(`app.kubernetes.io/name=NAME` instead of `app=NAME`). Since K8s deployment label selectors are
(`app.kuberentes.io/name=NAME` instead of `app=NAME`). Since K8s deployment label selectors are
immutable, during an upgrade from v0.11 to v0.12, the old deployments should be deleted using
`--cascade=false` which allows the new deployments to be created without introducing downtime.
Once the new deployments are ready, the older replicasets can be deleted. Use the following
@@ -1078,7 +799,7 @@ has a minimum client version of v0.12.0. Older CLI clients will be rejected.
- Fix CRD creation/deletion handling (#1249)
- Git cloning via SSH was not verifying host public key (#1276)
- Fixed multiple goroutine leaks in controller and api-server
- Fix issue where `argocd app set -p` required repo privileges. (#1280)
- Fix isssue where `argocd app set -p` required repo privileges. (#1280)
- Fix local diff of non-namespaced resources. Also handle duplicates in local diff (#1289)
- Deprecated resource kinds from 'extensions' groups are not reconciled correctly (#1232)
- Fix issue where CLI would panic after timeout when cli did not have get permissions (#1209)
@@ -1256,7 +977,7 @@ which have a dependency to external helm repositories.
+ Allow more fine-grained sync (issue #508)
+ Display init container logs (issue #681)
+ Redirect to /auth/login instead of /login when SSO token is used for authentication (issue #348)
+ Redirect to /auth/login instead of /login when SSO token is used for authenticaion (issue #348)
+ Support ability to use a helm values files from a URL (issue #624)
+ Support public not-connected repo in app creation UI (issue #426)
+ Use ksonnet CLI instead of ksonnet libs (issue #626)
@@ -1531,7 +1252,7 @@ RBAC policy rules, need to be rewritten to include one extra column with the eff
+ Sync/Rollback/Delete is asynchronously handled by controller
* Refactor CRUD operation on clusters and repos
* Sync will always perform kubectl apply
* Synced Status considers last-applied-configuration annotation
* Synced Status considers last-applied-configuration annotatoin
* Server & namespace are mandatory fields (still inferred from app.yaml)
* Manifests are memoized in repo server
- Fix connection timeouts to SSH repos

View File

@@ -4,7 +4,7 @@ ARG BASE_IMAGE=debian:10-slim
# Initial stage which pulls prepares build dependencies and CLI tooling we need for our final image
# Also used as the image in CI jobs so needs all dependencies
####################################################################################################
FROM golang:1.14.1 as builder
FROM golang:1.12.6 as builder
RUN echo 'deb http://deb.debian.org/debian buster-backports main' >> /etc/apt/sources.list
@@ -25,14 +25,14 @@ WORKDIR /tmp
ADD hack/install.sh .
ADD hack/installers installers
ADD hack/tool-versions.sh .
RUN ./install.sh dep-linux
RUN ./install.sh packr-linux
RUN ./install.sh kubectl-linux
RUN ./install.sh ksonnet-linux
RUN ./install.sh helm2-linux
RUN ./install.sh helm-linux
RUN ./install.sh kustomize-linux
RUN ./install.sh aws-iam-authenticator-linux
####################################################################################################
# Argo CD Base - used as the base for both the release and dev argocd images
@@ -50,17 +50,16 @@ RUN groupadd -g 999 argocd && \
chmod g=u /home/argocd && \
chmod g=u /etc/passwd && \
apt-get update && \
apt-get install -y git git-lfs python3-pip tini && \
apt-get install -y git git-lfs && \
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 --from=builder /usr/local/bin/ks /usr/local/bin/ks
COPY --from=builder /usr/local/bin/helm2 /usr/local/bin/helm2
COPY --from=builder /usr/local/bin/helm /usr/local/bin/helm
COPY --from=builder /usr/local/bin/kubectl /usr/local/bin/kubectl
COPY --from=builder /usr/local/bin/kustomize /usr/local/bin/kustomize
COPY --from=builder /usr/local/bin/aws-iam-authenticator /usr/local/bin/aws-iam-authenticator
# script to add current (possibly arbitrary) user to /etc/passwd at runtime
# (if it's not already there, to be openshift friendly)
COPY uid_entrypoint.sh /usr/local/bin/uid_entrypoint.sh
@@ -75,7 +74,7 @@ RUN mkdir -p /app/config/tls
# workaround ksonnet issue https://github.com/ksonnet/ksonnet/issues/298
ENV USER=argocd
USER 999
USER argocd
WORKDIR /home/argocd
####################################################################################################
@@ -97,26 +96,27 @@ RUN NODE_ENV='production' yarn build
####################################################################################################
# Argo CD Build stage which performs the actual build of Argo CD binaries
####################################################################################################
FROM golang:1.14.1 as argocd-build
FROM golang:1.12.6 as argocd-build
COPY --from=builder /usr/local/bin/dep /usr/local/bin/dep
COPY --from=builder /usr/local/bin/packr /usr/local/bin/packr
WORKDIR /go/src/github.com/argoproj/argo-cd
# A dummy directory is created under $GOPATH/src/dummy so we are able to use dep
# to install all the packages of our dep lock file
COPY Gopkg.toml ${GOPATH}/src/dummy/Gopkg.toml
COPY Gopkg.lock ${GOPATH}/src/dummy/Gopkg.lock
COPY go.mod go.mod
COPY go.sum go.sum
RUN go mod download
RUN cd ${GOPATH}/src/dummy && \
dep ensure -vendor-only && \
mv vendor/* ${GOPATH}/src/ && \
rmdir vendor
# Perform the build
WORKDIR /go/src/github.com/argoproj/argo-cd
COPY . .
RUN make cli server controller repo-server argocd-util
RUN make cli server controller repo-server argocd-util && \
make CLI_NAME=argocd-darwin-amd64 GOOS=darwin cli
ARG BUILD_ALL_CLIS=true
RUN if [ "$BUILD_ALL_CLIS" = "true" ] ; then \
make CLI_NAME=argocd-darwin-amd64 GOOS=darwin cli && \
make CLI_NAME=argocd-windows-amd64.exe GOOS=windows cli \
; fi
####################################################################################################
# Final image
@@ -124,3 +124,4 @@ RUN if [ "$BUILD_ALL_CLIS" = "true" ] ; then \
FROM argocd-base
COPY --from=argocd-build /go/src/github.com/argoproj/argo-cd/dist/argocd* /usr/local/bin/
COPY --from=argocd-ui ./src/dist/app /shared/app

2103
Gopkg.lock generated Normal file

File diff suppressed because it is too large Load Diff

117
Gopkg.toml Normal file
View File

@@ -0,0 +1,117 @@
# Packages should only be added to the following list when we use them *outside* of our go code.
# (e.g. we want to build the binary to invoke as part of the build process, such as in
# generate-proto.sh). Normal use of golang packages should be added via `dep ensure`, and pinned
# with a [[constraint]] or [[override]] when version is important.
required = [
"github.com/golang/protobuf/protoc-gen-go",
"github.com/gogo/protobuf/protoc-gen-gofast",
"github.com/gogo/protobuf/protoc-gen-gogofast",
"k8s.io/code-generator/cmd/go-to-protobuf",
"k8s.io/kube-openapi/cmd/openapi-gen",
"github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway",
"github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger",
"golang.org/x/sync/errgroup",
]
[[constraint]]
name = "google.golang.org/grpc"
version = "1.15.0"
[[constraint]]
name = "github.com/gogo/protobuf"
version = "1.3.1"
# override github.com/grpc-ecosystem/go-grpc-middleware's constraint on master
[[override]]
name = "github.com/golang/protobuf"
version = "1.2.0"
[[constraint]]
name = "github.com/grpc-ecosystem/grpc-gateway"
version = "v1.3.1"
# prometheus does not believe in semversioning yet
[[constraint]]
name = "github.com/prometheus/client_golang"
revision = "7858729281ec582767b20e0d696b6041d995d5e0"
[[override]]
branch = "release-1.16"
name = "k8s.io/api"
[[override]]
branch = "release-1.16"
name = "k8s.io/kubernetes"
[[override]]
branch = "release-1.16"
name = "k8s.io/code-generator"
[[override]]
branch = "release-1.16"
name = "k8s.io/apimachinery"
[[override]]
branch = "release-1.16"
name = "k8s.io/apiextensions-apiserver"
[[override]]
branch = "release-1.16"
name = "k8s.io/apiserver"
[[override]]
branch = "release-1.16"
name = "k8s.io/kubectl"
[[override]]
branch = "release-1.16"
name = "k8s.io/cli-runtime"
[[override]]
version = "2.0.3"
name = "sigs.k8s.io/kustomize"
# ASCIIRenderer does not implement blackfriday.Renderer
[[override]]
name = "github.com/russross/blackfriday"
version = "1.5.2"
[[override]]
branch = "release-13.0"
name = "k8s.io/client-go"
[[override]]
name = "github.com/casbin/casbin"
version = "1.9.1"
[[constraint]]
name = "github.com/stretchr/testify"
version = "1.2.2"
[[constraint]]
name = "github.com/gobuffalo/packr"
version = "v1.11.0"
[[constraint]]
branch = "master"
name = "github.com/argoproj/pkg"
[[constraint]]
branch = "master"
name = "github.com/yudai/gojsondiff"
# Fixes: Could not introduce sigs.k8s.io/kustomize@v2.0.3, as it has a dependency on github.com/spf13/cobra with constraint ^0.0.2, which has no overlap with existing constraint 0.0.5 from (root)
[[override]]
name = "github.com/spf13/cobra"
revision = "0.0.5"
# TODO: move off of k8s.io/kube-openapi and use controller-tools for CRD spec generation
# (override argoproj/argo contraint on master)
[[override]]
revision = "30be4d16710ac61bce31eb28a01054596fe6a9f1"
name = "k8s.io/kube-openapi"
# jsonpatch replace operation does not apply: doc is missing key: /metadata/annotations
[[override]]
name = "github.com/evanphx/json-patch"
version = "v4.1.0"

291
Makefile
View File

@@ -8,85 +8,11 @@ BUILD_DATE=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
GIT_COMMIT=$(shell git rev-parse HEAD)
GIT_TAG=$(shell if [ -z "`git status --porcelain`" ]; then git describe --exact-match --tags HEAD 2>/dev/null; fi)
GIT_TREE_STATE=$(shell if [ -z "`git status --porcelain`" ]; then echo "clean" ; else echo "dirty"; fi)
PACKR_CMD=$(shell if [ "`which packr`" ]; then echo "packr"; else echo "go run github.com/gobuffalo/packr/packr"; fi)
VOLUME_MOUNT=$(shell if test "$(go env GOOS)" = "darwin"; then echo ":delegated"; elif test selinuxenabled; then echo ":Z"; else echo ""; fi)
PACKR_CMD=$(shell if [ "`which packr`" ]; then echo "packr"; else echo "go run vendor/github.com/gobuffalo/packr/packr/main.go"; fi)
VOLUME_MOUNT=$(shell [[ $(go env GOOS)=="darwin" ]] && echo ":delegated" || echo "")
GOPATH?=$(shell if test -x `which go`; then go env GOPATH; else echo "$(HOME)/go"; fi)
GOCACHE?=$(HOME)/.cache/go-build
DOCKER_SRCDIR?=$(GOPATH)/src
DOCKER_WORKDIR?=/go/src/github.com/argoproj/argo-cd
ARGOCD_PROCFILE?=Procfile
# Configuration for building argocd-test-tools image
TEST_TOOLS_NAMESPACE?=argoproj
TEST_TOOLS_IMAGE=argocd-test-tools
TEST_TOOLS_TAG?=v0.5.0
ifdef TEST_TOOLS_NAMESPACE
TEST_TOOLS_PREFIX=${TEST_TOOLS_NAMESPACE}/
endif
# You can change the ports where ArgoCD components will be listening on by
# setting the appropriate environment variables before running make.
ARGOCD_E2E_APISERVER_PORT?=8080
ARGOCD_E2E_REPOSERVER_PORT?=8081
ARGOCD_E2E_REDIS_PORT?=6379
ARGOCD_E2E_DEX_PORT?=5556
ARGOCD_E2E_YARN_HOST?=localhost
ARGOCD_IN_CI?=false
ARGOCD_TEST_E2E?=true
ARGOCD_LINT_GOGC?=20
# Runs any command in the argocd-test-utils container in server mode
# Server mode container will start with uid 0 and drop privileges during runtime
define run-in-test-server
docker run --rm -it \
--name argocd-test-server \
-e USER_ID=$(shell id -u) \
-e HOME=/home/user \
-e GOPATH=/go \
-e GOCACHE=/tmp/go-build-cache \
-e ARGOCD_IN_CI=$(ARGOCD_IN_CI) \
-e ARGOCD_E2E_TEST=$(ARGOCD_E2E_TEST) \
-e ARGOCD_E2E_YARN_HOST=$(ARGOCD_E2E_YARN_HOST) \
-v ${DOCKER_SRCDIR}:/go/src${VOLUME_MOUNT} \
-v ${GOPATH}/pkg/mod:/go/pkg/mod${VOLUME_MOUNT} \
-v ${GOCACHE}:/tmp/go-build-cache${VOLUME_MOUNT} \
-v ${HOME}/.kube:/home/user/.kube${VOLUME_MOUNT} \
-v /tmp:/tmp${VOLUME_MOUNT} \
-w ${DOCKER_WORKDIR} \
-p ${ARGOCD_E2E_APISERVER_PORT}:8080 \
-p 4000:4000 \
$(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE):$(TEST_TOOLS_TAG) \
bash -c "$(1)"
endef
# Runs any command in the argocd-test-utils container in client mode
define run-in-test-client
docker run --rm -it \
--name argocd-test-client \
-u $(shell id -u) \
-e HOME=/home/user \
-e GOPATH=/go \
-e ARGOCD_E2E_K3S=$(ARGOCD_E2E_K3S) \
-e GOCACHE=/tmp/go-build-cache \
-e ARGOCD_LINT_GOGC=$(ARGOCD_LINT_GOGC) \
-v ${DOCKER_SRCDIR}:/go/src${VOLUME_MOUNT} \
-v ${GOPATH}/pkg/mod:/go/pkg/mod${VOLUME_MOUNT} \
-v ${GOCACHE}:/tmp/go-build-cache${VOLUME_MOUNT} \
-v ${HOME}/.kube:/home/user/.kube${VOLUME_MOUNT} \
-v /tmp:/tmp${VOLUME_MOUNT} \
-w ${DOCKER_WORKDIR} \
$(TEST_TOOLS_NAMESPACE)/$(TEST_TOOLS_IMAGE):$(TEST_TOOLS_TAG) \
bash -c "$(1)"
endef
#
define exec-in-test-server
docker exec -it -u $(shell id -u) -e ARGOCD_E2E_K3S=$(ARGOCD_E2E_K3S) argocd-test-server $(1)
define run-in-dev-tool
docker run --rm -it -u $(shell id -u) -e HOME=/home/user -v ${CURRENT_DIR}:/go/src/github.com/argoproj/argo-cd${VOLUME_MOUNT} -w /go/src/github.com/argoproj/argo-cd argocd-dev-tools bash -c "GOPATH=/go $(1)"
endef
PATH:=$(PATH):$(PWD)/hack
@@ -112,8 +38,6 @@ endif
ifneq (${GIT_TAG},)
IMAGE_TAG=${GIT_TAG}
LDFLAGS += -X ${PACKAGE}.gitTag=${GIT_TAG}
else
IMAGE_TAG?=latest
endif
ifeq (${DOCKER_PUSH},true)
@@ -129,47 +53,34 @@ endif
.PHONY: all
all: cli image argocd-util
.PHONY: gogen
gogen:
export GO111MODULE=off
go generate ./util/argo/...
.PHONY: protogen
protogen:
export GO111MODULE=off
./hack/generate-proto.sh
.PHONY: openapigen
openapigen:
export GO111MODULE=off
./hack/update-openapi.sh
.PHONY: clientgen
clientgen:
export GO111MODULE=off
./hack/update-codegen.sh
.PHONY: codegen-local
codegen-local: mod-vendor-local gogen protogen clientgen openapigen manifests-local
rm -rf vendor/
codegen-local: protogen clientgen openapigen manifests-local
.PHONY: codegen
codegen:
$(call run-in-test-client,make codegen-local)
codegen: dev-tools-image
$(call run-in-dev-tool,make codegen-local)
.PHONY: cli
cli: clean-debug
CGO_ENABLED=0 ${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/${CLI_NAME} ./cmd/argocd
.PHONY: cli-docker
go build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/${CLI_NAME} ./cmd/argocd
.PHONY: release-cli
release-cli: clean-debug image
docker create --name tmp-argocd-linux $(IMAGE_PREFIX)argocd:$(IMAGE_TAG)
docker cp tmp-argocd-linux:/usr/local/bin/argocd ${DIST_DIR}/argocd-linux-amd64
docker cp tmp-argocd-linux:/usr/local/bin/argocd-darwin-amd64 ${DIST_DIR}/argocd-darwin-amd64
docker cp tmp-argocd-linux:/usr/local/bin/argocd-windows-amd64.exe ${DIST_DIR}/argocd-windows-amd64.exe
docker rm tmp-argocd-linux
.PHONY: argocd-util
@@ -177,23 +88,17 @@ argocd-util: clean-debug
# Build argocd-util as a statically linked binary, so it could run within the alpine-based dex container (argoproj/argo-cd#844)
CGO_ENABLED=0 ${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-util ./cmd/argocd-util
# .PHONY: dev-tools-image
# dev-tools-image:
# docker build -t $(DEV_TOOLS_PREFIX)$(DEV_TOOLS_IMAGE) . -f hack/Dockerfile.dev-tools
# docker tag $(DEV_TOOLS_PREFIX)$(DEV_TOOLS_IMAGE) $(DEV_TOOLS_PREFIX)$(DEV_TOOLS_IMAGE):$(DEV_TOOLS_VERSION)
.PHONY: test-tools-image
test-tools-image:
docker build -t $(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE) -f test/container/Dockerfile .
docker tag $(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE) $(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE):$(TEST_TOOLS_TAG)
.PHONY: dev-tools-image
dev-tools-image:
cd hack && docker build -t argocd-dev-tools . -f Dockerfile.dev-tools
.PHONY: manifests-local
manifests-local:
./hack/update-manifests.sh
.PHONY: manifests
manifests: test-tools-image
$(call run-in-test-client,make manifests-local IMAGE_TAG='${IMAGE_TAG}')
manifests: dev-tools-image
$(call run-in-dev-tool,make manifests-local IMAGE_TAG='${IMAGE_TAG}')
# NOTE: we use packr to do the build instead of go, since we embed swagger files and policy.csv
@@ -212,7 +117,7 @@ controller:
.PHONY: packr
packr:
go build -o ${DIST_DIR}/packr github.com/gobuffalo/packr/packr/
go build -o ${DIST_DIR}/packr ./vendor/github.com/gobuffalo/packr/packr/
.PHONY: image
ifeq ($(DEV_IMAGE), true)
@@ -229,7 +134,6 @@ image: packr
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-util ./cmd/argocd-util
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd ./cmd/argocd
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-darwin-amd64 ./cmd/argocd
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-windows-amd64.exe ./cmd/argocd
cp Dockerfile.dev dist
docker build -t $(IMAGE_PREFIX)argocd:$(IMAGE_TAG) -f dist/Dockerfile.dev dist
else
@@ -238,119 +142,45 @@ image:
endif
@if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argocd:$(IMAGE_TAG) ; fi
.PHONY: armimage
# The "BUILD_ALL_CLIS" argument is to skip building the CLIs for darwin and windows
# which would take a really long time.
armimage:
docker build -t $(IMAGE_PREFIX)argocd:$(IMAGE_TAG)-arm . --build-arg BUILD_ALL_CLIS="false"
.PHONY: builder-image
builder-image:
docker build -t $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG) --target builder .
@if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG) ; fi
.PHONY: mod-download
mod-download:
$(call run-in-test-client,go mod download)
.PHONY: dep
dep:
dep ensure -v
.PHONY: mod-download-local
mod-download-local:
go mod download
.PHONY: dep-ensure
dep-ensure:
dep ensure -no-vendor
.PHONY: mod-vendor
mod-vendor:
$(call run-in-test-client,go mod vendor)
.PHONY: mod-vendor-local
mod-vendor-local: mod-download-local
go mod vendor
# Deprecated - replace by install-local-tools
.PHONY: install-lint-tools
install-lint-tools:
./hack/install.sh lint-tools
# Run linter on the code
.PHONY: lint
lint:
$(call run-in-test-client,make lint-local)
# Run linter on the code (local version)
.PHONY: lint-local
lint-local:
golangci-lint --version
# NOTE: If you get a "Killed" OOM message, try reducing the value of GOGC
# See https://github.com/golangci/golangci-lint#memory-usage-of-golangci-lint
GOGC=$(ARGOCD_LINT_GOGC) GOMAXPROCS=2 golangci-lint run --fix --verbose --timeout 300s
golangci-lint run --fix --verbose
.PHONY: lint-ui
lint-ui:
$(call run-in-test-client,make lint-ui-local)
.PHONY: lint-ui-local
lint-ui-local:
cd ui && yarn lint
# Build all Go code
.PHONY: build
build:
mkdir -p $(GOCACHE)
$(call run-in-test-client, make build-local)
# Build all Go code (local version)
.PHONY: build-local
build-local:
go build -v `go list ./... | grep -v 'resource_customizations\|test/e2e'`
# Run all unit tests
#
# If TEST_MODULE is set (to fully qualified module name), only this specific
# module will be tested.
.PHONY: test
test:
mkdir -p $(GOCACHE)
$(call run-in-test-client,make TEST_MODULE=$(TEST_MODULE) test-local)
./hack/test.sh -coverprofile=coverage.out `go list ./... | grep -v 'test/e2e'`
# Run all unit tests (local version)
.PHONY: test-local
test-local:
if test "$(TEST_MODULE)" = ""; then \
./hack/test.sh -coverprofile=coverage.out `go list ./... | grep -v 'test/e2e'`; \
else \
./hack/test.sh -coverprofile=coverage.out "$(TEST_MODULE)"; \
fi
# Run the E2E test suite. E2E test servers (see start-e2e target) must be
# started before.
.PHONY: test-e2e
test-e2e:
$(call exec-in-test-server,make test-e2e-local)
test-e2e:
./hack/test.sh -timeout 15m ./test/e2e
# Run the E2E test suite (local version)
.PHONY: test-e2e-local
test-e2e-local: cli
# NO_PROXY ensures all tests don't go out through a proxy if one is configured on the test system
export GO111MODULE=off
NO_PROXY=* ./hack/test.sh -timeout 15m -v ./test/e2e
# Spawns a shell in the test server container for debugging purposes
debug-test-server:
$(call run-in-test-server,/bin/bash)
# Spawns a shell in the test client container for debugging purposes
debug-test-client:
$(call run-in-test-client,/bin/bash)
# Starts e2e server in a container
.PHONY: start-e2e
start-e2e:
start-e2e: cli
killall goreman || true
# check we can connect to Docker to start Redis
docker version
mkdir -p ${GOCACHE}
$(call run-in-test-server,make ARGOCD_PROCFILE=test/container/Procfile start-e2e-local)
# Starts e2e server locally (or within a container)
.PHONY: start-e2e-local
start-e2e-local:
kubectl create ns argocd-e2e || true
kubectl config set-context --current --namespace=argocd-e2e
kustomize build test/manifests/base | kubectl apply -f -
@@ -359,9 +189,7 @@ start-e2e-local:
ARGOCD_TLS_DATA_PATH=/tmp/argo-e2e/app/config/tls \
ARGOCD_E2E_DISABLE_AUTH=false \
ARGOCD_ZJWT_FEATURE_FLAG=always \
ARGOCD_IN_CI=$(ARGOCD_IN_CI) \
ARGOCD_E2E_TEST=true \
goreman -f $(ARGOCD_PROCFILE) start
goreman start
# Cleans VSCode debug.test files from sub-dirs to prevent them from being included in packr boxes
.PHONY: clean-debug
@@ -374,28 +202,17 @@ clean: clean-debug
.PHONY: start
start:
docker version
$(call run-in-test-server,make ARGOCD_PROCFILE=test/container/Procfile start-local ARGOCD_START=${ARGOCD_START})
# Starts a local instance of ArgoCD
.PHONY: start-local
start-local: mod-vendor-local
# check we can connect to Docker to start Redis
killall goreman || true
# check we can connect to Docker to start Redis
docker version
kubectl create ns argocd || true
kubens argocd
ARGOCD_ZJWT_FEATURE_FLAG=always \
ARGOCD_IN_CI=false \
ARGOCD_E2E_TEST=false \
goreman -f $(ARGOCD_PROCFILE) start ${ARGOCD_START}
goreman start
# Runs pre-commit validation with the virtualized toolchain
.PHONY: pre-commit
pre-commit: dep-ensure codegen build lint test
# Runs pre-commit validation with the local toolchain
.PHONY: pre-commit-local
pre-commit-local: dep-ensure-local codegen-local build-local lint-local test-local
.PHONY: release-precheck
release-precheck: manifests
@if [ "$(GIT_TREE_STATE)" != "clean" ]; then echo 'git tree state is $(GIT_TREE_STATE)' ; exit 1; fi
@@ -420,48 +237,4 @@ lint-docs:
.PHONY: publish-docs
publish-docs: lint-docs
mkdocs gh-deploy
# Verify that kubectl can connect to your K8s cluster from Docker
.PHONY: verify-kube-connect
verify-kube-connect:
$(call run-in-test-client,kubectl version)
# Show the Go version of local and virtualized environments
.PHONY: show-go-version
show-go-version:
@echo -n "Local Go version: "
@go version
@echo -n "Docker Go version: "
$(call run-in-test-client,go version)
# Installs all tools required to build and test ArgoCD locally
.PHONY: install-tools-local
install-tools-local: install-test-tools-local install-codegen-tools-local install-go-tools-local
# Installs all tools required for running unit & end-to-end tests (Linux packages)
.PHONY: install-test-tools-local
install-test-tools-local:
sudo ./hack/install.sh packr-linux
sudo ./hack/install.sh kubectl-linux
sudo ./hack/install.sh kustomize-linux
sudo ./hack/install.sh ksonnet-linux
sudo ./hack/install.sh helm2-linux
sudo ./hack/install.sh helm-linux
# Installs all tools required for running codegen (Linux packages)
.PHONY: install-codegen-tools-local
install-codegen-tools-local:
sudo ./hack/install.sh codegen-tools
# Installs all tools required for running codegen (Go packages)
.PHONY: install-go-tools-local
install-go-tools-local:
./hack/install.sh codegen-go-tools
.PHONY: dep-ui
dep-ui:
$(call run-in-test-client,make dep-ui-local)
dep-ui-local:
cd ui && yarn install
mkdocs gh-deploy

8
OWNERS
View File

@@ -1,12 +1,12 @@
owners:
- alexec
- alexmt
- jessesuen
reviewers:
- jannfis
approvers:
- alexec
- alexmt
- dthomson25
- jannfis
- jessesuen
- mayzhang2000
- rachelwang20

View File

@@ -1,6 +1,6 @@
controller: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true go run ./cmd/argocd-application-controller/main.go --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081}"
api-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true go run ./cmd/argocd-server/main.go --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --disable-auth=${ARGOCD_E2E_DISABLE_AUTH:-'true'} --insecure --dex-server http://localhost:${ARGOCD_E2E_DEX_PORT:-5556} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --port ${ARGOCD_E2E_APISERVER_PORT:-8080} --staticassets ui/dist/app"
dex: sh -c "go run github.com/argoproj/argo-cd/cmd/argocd-util gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p ${ARGOCD_E2E_DEX_PORT:-5556}:${ARGOCD_E2E_DEX_PORT:-5556} -v `pwd`/dist/dex.yaml:/dex.yaml quay.io/dexidp/dex:v2.22.0 serve /dex.yaml"
dex: sh -c "go run github.com/argoproj/argo-cd/cmd/argocd-util gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p ${ARGOCD_E2E_DEX_PORT:-5556}:${ARGOCD_E2E_DEX_PORT:-5556} -v `pwd`/dist/dex.yaml:/dex.yaml quay.io/dexidp/dex:v2.21.0 serve /dex.yaml"
redis: docker run --rm --name argocd-redis -i -p ${ARGOCD_E2E_REDIS_PORT:-6379}:${ARGOCD_E2E_REDIS_PORT:-6379} redis:5.0.3-alpine --save "" --appendonly no --port ${ARGOCD_E2E_REDIS_PORT:-6379}
repo-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true go run ./cmd/argocd-repo-server/main.go --loglevel debug --port ${ARGOCD_E2E_REPOSERVER_PORT:-8081} --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379}"
ui: sh -c 'cd ui && ${ARGOCD_E2E_YARN_CMD:-yarn} start'

View File

@@ -1,4 +1,3 @@
[![Integration tests](https://github.com/argoproj/argo-cd/workflows/Integration%20tests/badge.svg?branch=master)](https://github.com/argoproj/argo-cd/actions?query=workflow%3A%22Integration+tests%22)
[![slack](https://img.shields.io/badge/slack-argoproj-brightgreen.svg?logo=slack)](https://argoproj.github.io/community/join-slack)
[![codecov](https://codecov.io/gh/argoproj/argo-cd/branch/master/graph/badge.svg)](https://codecov.io/gh/argoproj/argo-cd)
[![Release Version](https://img.shields.io/github/v/release/argoproj/argo-cd?label=argo-cd)](https://github.com/argoproj/argo-cd/releases/latest)
@@ -13,12 +12,62 @@ Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes.
## Why Argo CD?
1. Application definitions, configurations, and environments should be declarative and version controlled.
1. Application deployment and lifecycle management should be automated, auditable, and easy to understand.
Application definitions, configurations, and environments should be declarative and version controlled.
Application deployment and lifecycle management should be automated, auditable, and easy to understand.
## Who uses Argo CD?
[Official Argo CD user list](USERS.md)
Organizations below are **officially** using Argo CD. Please send a PR with your organization name if you are using Argo CD.
1. [127Labs](https://127labs.com/)
1. [Adevinta](https://www.adevinta.com/)
1. [ANSTO - Australian Synchrotron](https://www.synchrotron.org.au/)
1. [ARZ Allgemeines Rechenzentrum GmbH ](https://www.arz.at/)
1. [Baloise](https://www.baloise.com)
1. [BioBox Analytics](https://biobox.io)
1. [CARFAX](https://www.carfax.com)
1. [Celonis](https://www.celonis.com/)
1. [Codility](https://www.codility.com/)
1. [Commonbond](https://commonbond.co/)
1. [CyberAgent](https://www.cyberagent.co.jp/en/)
1. [Cybozu](https://cybozu-global.com)
1. [EDF Renewables](https://www.edf-re.com/)
1. [Elium](https://www.elium.com)
1. [END.](https://www.endclothing.com/)
1. [Fave](https://myfave.com)
1. [Future PLC](https://www.futureplc.com/)
1. [GMETRI](https://gmetri.com/)
1. [hipages](https://hipages.com.au/)
1. [Intuit](https://www.intuit.com/)
1. [KintoHub](https://www.kintohub.com/)
1. [KompiTech GmbH](https://www.kompitech.com/)
1. [Lytt](https://www.lytt.co/)
1. [Major League Baseball](https://mlb.com)
1. [Mambu](https://www.mambu.com/)
1. [Max Kelsen](https://www.maxkelsen.com/)
1. [Mirantis](https://mirantis.com/)
1. [OpenSaaS Studio](https://opensaas.studio)
1. [Optoro](https://www.optoro.com/)
1. [Peloton Interactive](https://www.onepeloton.com/)
1. [Pipefy](https://www.pipefy.com/)
1. [Riskified](https://www.riskified.com/)
1. [Red Hat](https://www.redhat.com/)
1. [Saildrone](https://www.saildrone.com/)
1. [Saloodo! GmbH](https://www.saloodo.com)
1. [Syncier](https://syncier.com/)
1. [Tesla](https://tesla.com/)
1. [Tiger Analytics](https://www.tigeranalytics.com/)
1. [tZERO](https://www.tzero.com/)
1. [Ticketmaster](https://ticketmaster.com)
1. [Twilio SendGrid](https://sendgrid.com)
1. [Yieldlab](https://www.yieldlab.de/)
1. [UBIO](https://ub.io/)
1. [Universidad Mesoamericana](https://www.umes.edu.gt/)
1. [Viaduct](https://www.viaduct.ai/)
1. [Volvo Cars](https://www.volvocars.com/)
1. [Walkbase](https://www.walkbase.com/)
## Documentation

View File

@@ -1,8 +0,0 @@
# Defined below are the security contacts for this repo.
#
# DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE
# INSTRUCTIONS AT https://argoproj.github.io/argo-cd/security_considerations/#reporting-vulnerabilities
alexmt
edlee2121
jessesuen

View File

@@ -1,66 +0,0 @@
## Who uses Argo CD?
As the Argo Community grows, we'd like to keep track of our users. Please send a PR with your organization name if you are using Argo CD.
Currently, the following organizations are **officially** using Argo CD:
1. [127Labs](https://127labs.com/)
1. [Adevinta](https://www.adevinta.com/)
1. [AppDirect](https://www.appdirect.com)
1. [ANSTO - Australian Synchrotron](https://www.synchrotron.org.au/)
1. [ARZ Allgemeines Rechenzentrum GmbH ](https://www.arz.at/)
1. [Baloise](https://www.baloise.com)
1. [BioBox Analytics](https://biobox.io)
1. [CARFAX](https://www.carfax.com)
1. [Celonis](https://www.celonis.com/)
1. [Codility](https://www.codility.com/)
1. [Commonbond](https://commonbond.co/)
1. [CyberAgent](https://www.cyberagent.co.jp/en/)
1. [Cybozu](https://cybozu-global.com)
1. [EDF Renewables](https://www.edf-re.com/)
1. [Elium](https://www.elium.com)
1. [END.](https://www.endclothing.com/)
1. [Fave](https://myfave.com)
1. [Future PLC](https://www.futureplc.com/)
1. [GMETRI](https://gmetri.com/)
1. [Healy](https://www.healyworld.net)
1. [hipages](https://hipages.com.au/)
1. [Honestbank](https://honestbank.com)
1. [Intuit](https://www.intuit.com/)
1. [KintoHub](https://www.kintohub.com/)
1. [KompiTech GmbH](https://www.kompitech.com/)
1. [Lytt](https://www.lytt.co/)
1. [Major League Baseball](https://mlb.com)
1. [Mambu](https://www.mambu.com/)
1. [Max Kelsen](https://www.maxkelsen.com/)
1. [Mirantis](https://mirantis.com/)
1. [MOO Print](https://www.moo.com/)
1. [OpenSaaS Studio](https://opensaas.studio)
1. [Optoro](https://www.optoro.com/)
1. [Peloton Interactive](https://www.onepeloton.com/)
1. [Pipefy](https://www.pipefy.com/)
1. [Prudential](https://prudential.com.sg)
1. [PUBG](https://www.pubg.com)
1. [Red Hat](https://www.redhat.com/)
1. [Robotinfra](https://www.robotinfra.com)
1. [Riskified](https://www.riskified.com/)
1. [Saildrone](https://www.saildrone.com/)
1. [Saloodo! GmbH](https://www.saloodo.com)
1. [Swissquote](https://github.com/swissquote)
1. [Syncier](https://syncier.com/)
1. [Tesla](https://tesla.com/)
1. [ThousandEyes](https://www.thousandeyes.com/)
1. [Ticketmaster](https://ticketmaster.com)
1. [Tiger Analytics](https://www.tigeranalytics.com/)
1. [Twilio SendGrid](https://sendgrid.com)
1. [tZERO](https://www.tzero.com/)
1. [UBIO](https://ub.io/)
1. [Universidad Mesoamericana](https://www.umes.edu.gt/)
1. [Viaduct](https://www.viaduct.ai/)
1. [Volvo Cars](https://www.volvocars.com/)
1. [VSHN - The DevOps Company](https://vshn.ch/)
1. [Walkbase](https://www.walkbase.com/)
1. [Whitehat Berlin](https://whitehat.berlin) by Guido Maria Serra +Fenaroli
1. [Yieldlab](https://www.yieldlab.de/)
1. [MTN Group](https://www.mtn.com/)
1. [Moengage](https://www.moengage.com/)

View File

@@ -1 +1 @@
1.6.2
1.4.3

View File

@@ -1,24 +1,22 @@
<svg width="131" height="20" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" >
<defs>
<filter id="dropShadow">
<feDropShadow dx="0.2" dy="0.4" stdDeviation="0.2" flood-color="#333" flood-opacity="0.5"/>
</filter>
</defs>
<clipPath id="roundedCorners">
<rect width="100%" height="100%" rx="3" opacity="1" />
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="131" height="20">
<linearGradient id="b" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<clipPath id="a">
<rect width="131" height="20" rx="3" fill="#fff"/>
</clipPath>
<g clip-path="url(#roundedCorners)">
<rect id="leftRect" fill="#555" x="0" y="0" width="74" height="20" />
<rect id="rightRect" fill="#4c1" x="74" y="0" width="57" height="20" />
<rect id="revisionRect" fill="#4c1" x="131" y="0" width="62" height="20" display="none"/>
<g clip-path="url(#a)">
<path id="leftPath" fill="#555" d="M0 0h74v20H0z"/>
<path id="rightPath" fill="#4c1" d="M74 0h57v20H74z"/>
<path fill="url(#b)" d="M0 0h131v20H0z"/>
</g>
<g fill="#fff" style="filter: url(#dropShadow);" text-anchor="middle" font-family="DejaVu Sans, sans-serif" font-size="90">
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="90">
<image x="5" y="3" width="14" height="14" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB8AAAAeCAYAAADU8sWcAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAAABPAAAATwFjiv3XAAACC2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOlJlc29sdXRpb25Vbml0PjI8L3RpZmY6UmVzb2x1dGlvblVuaXQ+CiAgICAgICAgIDx0aWZmOkNvbXByZXNzaW9uPjE8L3RpZmY6Q29tcHJlc3Npb24+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDx0aWZmOlBob3RvbWV0cmljSW50ZXJwcmV0YXRpb24+MjwvdGlmZjpQaG90b21ldHJpY0ludGVycHJldGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KD0UqkwAACpZJREFUSA11VnmQFcUZ//qY4527by92WWTXwHKqEJDDg0MQUioKSWopK4oaS9RAlMofUSPGbFlBSaViKomgQSNmIdECkUS8ophdozEJArKoKCsgIFlY2Pu9N29mero7X78FxCR2Vb8309PTv+/4fb9vCHz1II2Nm+jmzYul2bJsdlMyO7LyPGlZtYqQck1ZHCgwLZWiROeJgB7bDzpYb/7o0y/emzXv4Pts8+ZGBUC0uf/vQf57wdw3NTVRnOYFfXvj6pJcadkUmbDGRoxVSEItSYAQQrUmRBP81VoRpkFTJUMuZA/3g/28q3dn89b7u885D4348vgf8NPAxY033LJmSlDizlSOU94f6UhoHaZdC/3QCHWON2gDYBhAYyRAWgyD4QSi1815f29++vv/+CoD2Lm2nAFGl8mndzyxMMgk5/iW6xzPi/xVk+oyE8+vLNt14FR/0uYW14ox0IwUJ4ZAaUpAaaBEYByiyLbikWWNnnThvPIxu+L717auVeb81tbWsyk4C34u8I3LnlhcSCameMzykg7TwzOJGIYWQj+M+voLYcSo/hxY1E65FECUq4G4RFP0nSjMCMVAIIKUnOHkw90L6qq/vXvbh02trV8yoBh2E0NMY9GiG+58fIGXTF6epyw7JOnamZQbz/tCnDrVVzgqqdwTT0ZTdWDN0wPpMh3ZPZqHLSw98C7Y4RwtnAodsTwGAg0ppkcxrlkYJFID+Z0bn/zelsFImzQRXfScNJFiOG689dcT/FT6GzlqedVJTHRpPH6yz/PyfTlx2IrLCpvSdWrv5OXkX9fOJB/Mnaz3XzItap+yMDoyZgEE7F1SceIghryeRNwnTBkEqhRIxiLF6bCpDXNybW2v/ruxcTzbt2+zZmfCvWT+zxNhbek3cxaPlyYsXVWSSHT25rxC3o+OWK6aDn7sZ3r79ZNY28w45Esxuhw5RjmVvEQPlIwgR8bOiE7VdrPaA2+TeGEEhFbhtAEmG5pzk5WqqbWTPv7jC8sLpgxNSRWZrUeWTAgor7EY9ytTiURvzg8GskGU5xxKkc33yDcX1yc7x3ijLxF+zZgoEljgCrSiXOWIG/WGrjifHxl1V9iyeKwW1lHNo5SKmDmcYinivwhjVpkor55sQj9+/D4MO9bpLW8RN6zJzMvbPDUkHaecE368N1+IUUJfIm5hPf1w2kTedlm/LgntmqEWEQGT3ScJYUh2NIxiiWupSUFasor1VgzXvPATfv6BC3Roq0E9MLqAXKQcAe1rqi/dv+q3DwQUw6djY6tiAaG1jLEw7nAn64WhcStPqZqpA6chap8aage077GwbQcJDx8Awq3T3DHlbeijUHWwytHFuuizCTergdQ+YocxLRHVPNcESRAJxqo60nbSvExvvvWx23MCVvucDyRt5hitwDRHHG09pZiaT7MlSUdUhtQGZttYRdwA4WnFQjFnDA4LX7UdIlgMYpaouBgGMnuBKVN/ZhgBRCMVynNAHOfRm777q4UcrW3EZxcEnD/kclYlpVaFKFIZrIRtluvdls/G9InnLZ9fpLXoJjwZAzZkDJ5mjkIj8HTCKETH9oD0eoikMSDQ5cTrLrUg4QoaFlxUH5MdYy6yAFAaYBHed6ODkMPOkMcFZggZKa0SkaL/jMeDpq5D1Vez9693V/wNUg1jQRY8yLX8BcSba8AaOgyBOdqtQBz7DNj8lZCeNY9SN6aiD3bTOc89s2iliDVvKa3uGxf6doQAxYE2oBbm0HQPG5IxqKhOg9ZpSSQj+pDU+jv5t7913spV5c70GcqLJwgMGw6ZJUuBLbgHPW0D4rgQdewDvnAllN58B0D918AvzVBn3jWi/p4Hq2/s+et1URgBRlVRMGVvBtITfwyuyYPAoIRmWSIrEqjZW1Ml/qN7dtRWzptfBzXDoaeri6TicWhes8Zsg9iMOaAyU0D3fw4qdSEkrryquP77xx+HZCwG3T09HEZfCBUXTxh125H9VZ9SHp1OfZF4aEWI9MM1AhmiVCmaIwV6a2F5ge2q8s7OBB8ytPiOZduw6pFHYOS4cUUQGsNWXj4MVLYdaGU9UAQ0o2H8eHh49WpAITReKqdmGC0PsvF2JB4Gu+i5RqKg5JSi82mOnu9GYGEhwbFjSOXadJII7Pa6Ed3hwU8igAU8lU7D/ffdZ1pFEUT19YI+sR9oZgLIjo8h6usDu2oIzJpxOc4ZphKQtoqFxw6LzxIV/RMxkabSUPAJRz3A5oc50x9T7Lf3xkR0R0xGSS+IRIFQOUUUkqvGXdR5tGXnLmjbYXI0OLCJykIBvFe2ABVHgCQqgMpjkH9xE9IY7cSgGd0w+8mOVjiy68DOh+sauusiwSM8F11nlhCm39/SvP7ux4qNZVfba/0TJ183Is9opWtxEXe5E/cC+Y5bf+CilmczqaC/JigEOjjYTrwtzwB8tAn4kJFGMIEmy0B98gb4B7tAYimHncd19ObLrP25rW0/rb76pQqsoKTEkKJgoT64biCObVx35xvGGz67qYW3Nl0RWYH/ftxmo7pyfpSoKFFjEiLxhE73jXaueHXlGw+PZ68rS8pQ01QlodUjTqcAfcQo8qGjQB36EwQfbFAuF8wjZbmNtStefZ4nC0uibKIHez92AeZEUln5wocGuLGxyUbg2cVEFp5a9pFz11MH/ZCP6M0V8gmbxWZL35K2paFmUmjDgCXAMQkjqv8wgFMGBFVSCw8g1wWsrB5IBrTDAvB0eaAkhVHCeGzSQDUm3bE9/1hs/d7dBhy5aSqAaNNWN6Mvdn9+e0KGfl8hZCd6vBwqABmISKSkCgkqr/a6QMWrwLpsKaYWhfrwi1ikfWBNvQmUhXItAlznICMlcopFFNXcEA25wBw/EIms2L4O1onZs5u46abFUsILDWhAc/OKo7H+/OtuEFqaUcsmIE8CR47zEMsSuJvRQcceODVmOqR/uAHKftAC6R+9AKcmzgVxci9qexwLTIDZ34+MR6FVErXECXw7kc+3/G7j8gPG0dbWJmQnum1+zDj3U2rJ0rWzvFRynu/YskMTb5vetmwI7xyeg7R0g072ynEGwa0PwZTRDbDn0GGI1j0Ii2pCKPCyqJTl+eei9tOl5Kr1eU2seuG5ZMBv2fjUIMkQymAWC6jouQE3jdlYZa43PLnsLbcn++eYVwi6FCQktTCEmO3QAx2rhOllAl6bOwsahg2FDTMvhRklWVSUMkxcWEyHJFbUqyCW9AtRSV//K18AF4XmbOWeBTegCF78ujTXf3hm+XvTeo49G8/67Sg+A0a0OGc6CDzIlFbDL3+8GPYuuxLWP7AYysprIQx9YNjdzAglyVkD3qGvd3dsWvv0infM2qBj53zr49rZsJsNZwa2WeTTFxtP3L1oe1VCzh3AWqOM2FJJsFBwbMuCUAgQyAqGrVVJ7Zdwyz2RZS/X/GbrguJ5eNYgyhfnncH5v+DmYcft18Y1T5S7fl9jPMN+4aM6IpeQPmgxThNA5AnuJFhIeI2tHafiNqEUW1XQL+8NrfRznENP1drNuTOA5/5/KezmAZ4zaJBSdVaUvQk5PsNTeqfrMsKQOui5LGKibBAjmKgSRk/RIMlc8BiWCLbI95Dx05jybrMkfsh+xfgPf+hH0AC4OlsAAAAASUVORK5CYII="/>
<text id="leftText" x="435" y="140" transform="scale(.1)" textLength="470"></text>
<text id="rightText" x="995" y="140" transform="scale(.1)" textLength="470"></text>
<text id="revisionText" x="1550" y="140" font-family="monospace" transform="scale(.1)" font-size="110" display="none"></text>
</g>
<text id="leftText1" x="435" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="470"></text>
<text id="leftText2" x="435" y="140" transform="scale(.1)" textLength="470"></text>
<text id="rightText1" x="995" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="470"></text>
<text id="rightText1" x="995" y="140" transform="scale(.1)" textLength="470"></text></g>
</svg>

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -11,7 +11,6 @@ p, role:readonly, certificates, get, *, allow
p, role:readonly, clusters, get, *, allow
p, role:readonly, repositories, get, *, allow
p, role:readonly, projects, get, *, allow
p, role:readonly, accounts, get, *, allow
p, role:admin, applications, create, */*, allow
p, role:admin, applications, update, */*, allow
@@ -31,7 +30,6 @@ p, role:admin, repositories, delete, *, allow
p, role:admin, projects, create, *, allow
p, role:admin, projects, update, *, allow
p, role:admin, projects, delete, *, allow
p, role:admin, accounts, update, *, allow
g, role:admin, role:readonly
g, admin, role:admin
1 # Built-in policy which defines two roles: role:readonly and role:admin,
11 p, role:readonly, repositories, get, *, allow
12 p, role:readonly, projects, get, *, allow
13 p, role:readonly, accounts, get, *, allow p, role:admin, applications, create, */*, allow
p, role:admin, applications, create, */*, allow
14 p, role:admin, applications, update, */*, allow
15 p, role:admin, applications, delete, */*, allow
16 p, role:admin, applications, sync, */*, allow
30 p, role:admin, projects, delete, *, allow
31 p, role:admin, accounts, update, *, allow g, role:admin, role:readonly
32 g, role:admin, role:readonly g, admin, role:admin
g, admin, role:admin
33
34
35

View File

@@ -11,4 +11,4 @@ g = _, _
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
[matchers]
m = g(r.sub, p.sub) && globMatch(r.res, p.res) && globMatch(r.act, p.act) && globMatch(r.obj, p.obj)
m = g(r.sub, p.sub) && keyMatch(r.res, p.res) && keyMatch(r.act, p.act) && keyMatch(r.obj, p.obj)

View File

@@ -16,22 +16,6 @@
"version": "version not set"
},
"paths": {
"/api/v1/account": {
"get": {
"tags": [
"AccountService"
],
"operationId": "ListAccounts",
"responses": {
"200": {
"description": "(empty)",
"schema": {
"$ref": "#/definitions/accountAccountsList"
}
}
}
}
},
"/api/v1/account/can-i/{resource}/{action}/{subresource}": {
"get": {
"tags": [
@@ -95,92 +79,6 @@
}
}
},
"/api/v1/account/{name}": {
"get": {
"tags": [
"AccountService"
],
"operationId": "GetAccount",
"parameters": [
{
"type": "string",
"name": "name",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "(empty)",
"schema": {
"$ref": "#/definitions/accountAccount"
}
}
}
}
},
"/api/v1/account/{name}/token": {
"post": {
"tags": [
"AccountService"
],
"operationId": "CreateTokenMixin9",
"parameters": [
{
"type": "string",
"name": "name",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/accountCreateTokenRequest"
}
}
],
"responses": {
"200": {
"description": "(empty)",
"schema": {
"$ref": "#/definitions/accountCreateTokenResponse"
}
}
}
}
},
"/api/v1/account/{name}/token/{id}": {
"delete": {
"tags": [
"AccountService"
],
"operationId": "DeleteTokenMixin9",
"parameters": [
{
"type": "string",
"name": "name",
"in": "path",
"required": true
},
{
"type": "string",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "(empty)",
"schema": {
"$ref": "#/definitions/accountEmptyResponse"
}
}
}
}
},
"/api/v1/applications": {
"get": {
"tags": [
@@ -1689,36 +1587,6 @@
}
},
"/api/v1/repositories/{repo}": {
"get": {
"tags": [
"RepositoryService"
],
"summary": "Get returns a repository or its credentials",
"operationId": "GetMixin3",
"parameters": [
{
"type": "string",
"name": "repo",
"in": "path",
"required": true
},
{
"type": "boolean",
"format": "boolean",
"description": "Whether to force a cache refresh on repo's connection state.",
"name": "forceRefresh",
"in": "query"
}
],
"responses": {
"200": {
"description": "(empty)",
"schema": {
"$ref": "#/definitions/v1alpha1Repository"
}
}
}
},
"delete": {
"tags": [
"RepositoryService"
@@ -2017,41 +1885,6 @@
}
},
"definitions": {
"accountAccount": {
"type": "object",
"properties": {
"capabilities": {
"type": "array",
"items": {
"type": "string"
}
},
"enabled": {
"type": "boolean",
"format": "boolean"
},
"name": {
"type": "string"
},
"tokens": {
"type": "array",
"items": {
"$ref": "#/definitions/accountToken"
}
}
}
},
"accountAccountsList": {
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"$ref": "#/definitions/accountAccount"
}
}
}
},
"accountCanIResponse": {
"type": "object",
"properties": {
@@ -2060,58 +1893,12 @@
}
}
},
"accountCreateTokenRequest": {
"type": "object",
"properties": {
"expiresIn": {
"type": "string",
"format": "int64",
"title": "expiresIn represents a duration in seconds"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"accountCreateTokenResponse": {
"type": "object",
"properties": {
"token": {
"type": "string"
}
}
},
"accountEmptyResponse": {
"type": "object"
},
"accountToken": {
"type": "object",
"properties": {
"expiresAt": {
"type": "string",
"format": "int64"
},
"id": {
"type": "string"
},
"issuedAt": {
"type": "string",
"format": "int64"
}
}
},
"accountUpdatePasswordRequest": {
"type": "object",
"properties": {
"currentPassword": {
"type": "string"
},
"name": {
"type": "string"
},
"newPassword": {
"type": "string"
}
@@ -2174,12 +1961,6 @@
"type": "boolean",
"format": "boolean"
},
"infos": {
"type": "array",
"items": {
"$ref": "#/definitions/v1alpha1Info"
}
},
"manifests": {
"type": "array",
"items": {
@@ -2378,12 +2159,6 @@
"appLabelKey": {
"type": "string"
},
"configManagementPlugins": {
"type": "array",
"items": {
"$ref": "#/definitions/v1alpha1ConfigManagementPlugin"
}
},
"dexConfig": {
"$ref": "#/definitions/clusterDexConfig"
},
@@ -2396,12 +2171,6 @@
"kustomizeOptions": {
"$ref": "#/definitions/v1alpha1KustomizeOptions"
},
"kustomizeVersions": {
"type": "array",
"items": {
"type": "string"
}
},
"oidcConfig": {
"$ref": "#/definitions/clusterOIDCConfig"
},
@@ -2423,10 +2192,6 @@
},
"url": {
"type": "string"
},
"userLoginsDisabled": {
"type": "boolean",
"format": "boolean"
}
}
},
@@ -2476,9 +2241,6 @@
"format": "int64",
"title": "expiresIn represents a duration in seconds"
},
"id": {
"type": "string"
},
"project": {
"type": "string"
},
@@ -2517,7 +2279,7 @@
},
"repocredsRepoCredsResponse": {
"type": "object",
"title": "RepoCredsResponse is a response to most repository credentials requests"
"title": "RepoCredsResponse is a resonse to most repository credentials requests"
},
"repositoryAppInfo": {
"type": "object",
@@ -2539,13 +2301,6 @@
"type": "object",
"title": "HelmAppSpec contains helm app name in source repo",
"properties": {
"fileParameters": {
"type": "array",
"title": "helm file parameters",
"items": {
"$ref": "#/definitions/v1alpha1HelmFileParameter"
}
},
"name": {
"type": "string"
},
@@ -2621,7 +2376,7 @@
"$ref": "#/definitions/repositoryKsonnetEnvironmentDestination"
},
"k8sVersion": {
"description": "KubernetesVersion is the kubernetes version the targeted cluster is running on.",
"description": "KubernetesVersion is the kubernetes version the targetted cluster is running on.",
"type": "string"
},
"name": {
@@ -3220,13 +2975,6 @@
"$ref": "#/definitions/v1GroupKind"
}
},
"namespaceResourceWhitelist": {
"type": "array",
"title": "NamespaceResourceWhitelist contains list of whitelisted namespace level resources",
"items": {
"$ref": "#/definitions/v1GroupKind"
}
},
"orphanedResources": {
"$ref": "#/definitions/v1alpha1OrphanedResourcesMonitorSettings"
},
@@ -3370,13 +3118,6 @@
"type": "object",
"title": "ApplicationSourceHelm holds helm specific options",
"properties": {
"fileParameters": {
"type": "array",
"title": "FileParameters are file parameters to the helm template",
"items": {
"$ref": "#/definitions/v1alpha1HelmFileParameter"
}
},
"parameters": {
"type": "array",
"title": "Parameters are parameters to the helm template",
@@ -3463,10 +3204,6 @@
"nameSuffix": {
"type": "string",
"title": "NameSuffix is a suffix appended to resources for kustomize apps"
},
"version": {
"type": "string",
"title": "Version contains optional Kustomize version"
}
}
},
@@ -3688,24 +3425,6 @@
}
}
},
"v1alpha1Command": {
"type": "object",
"title": "Command holds binary path and arguments list",
"properties": {
"args": {
"type": "array",
"items": {
"type": "string"
}
},
"command": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"v1alpha1ComparedTo": {
"type": "object",
"title": "ComparedTo contains application source and target which was used for resources comparison",
@@ -3718,21 +3437,6 @@
}
}
},
"v1alpha1ConfigManagementPlugin": {
"type": "object",
"title": "ConfigManagementPlugin contains config management plugin configuration",
"properties": {
"generate": {
"$ref": "#/definitions/v1alpha1Command"
},
"init": {
"$ref": "#/definitions/v1alpha1Command"
},
"name": {
"type": "string"
}
}
},
"v1alpha1ConnectionState": {
"type": "object",
"title": "ConnectionState contains information about remote resource connection state",
@@ -3772,20 +3476,6 @@
}
}
},
"v1alpha1HelmFileParameter": {
"type": "object",
"title": "HelmFileParameter is a file parameter to a helm template",
"properties": {
"name": {
"type": "string",
"title": "Name is the name of the helm parameter"
},
"path": {
"type": "string",
"title": "Path is the path value for the helm parameter"
}
}
},
"v1alpha1HelmParameter": {
"type": "object",
"title": "HelmParameter is a parameter to a helm template",
@@ -3841,9 +3531,6 @@
"iat": {
"type": "string",
"format": "int64"
},
"id": {
"type": "string"
}
}
},
@@ -3863,18 +3550,6 @@
}
}
},
"v1alpha1KnownTypeField": {
"type": "object",
"title": "KnownTypeField contains mapping between CRD field and known Kubernetes type",
"properties": {
"field": {
"type": "string"
},
"type": {
"type": "string"
}
}
},
"v1alpha1KsonnetParameter": {
"type": "object",
"title": "KsonnetParameter is a ksonnet component parameter",
@@ -3894,10 +3569,6 @@
"type": "object",
"title": "KustomizeOptions are options for kustomize to use when building manifests",
"properties": {
"binaryPath": {
"type": "string",
"title": "BinaryPath holds optional path to kustomize binary"
},
"buildOptions": {
"type": "string",
"title": "BuildOptions is a string of build parameters to use when calling `kustomize build`"
@@ -3908,35 +3579,11 @@
"description": "Operation contains requested operation parameters.",
"type": "object",
"properties": {
"info": {
"type": "array",
"items": {
"$ref": "#/definitions/v1alpha1Info"
}
},
"initiatedBy": {
"$ref": "#/definitions/v1alpha1OperationInitiator"
},
"sync": {
"$ref": "#/definitions/v1alpha1SyncOperation"
}
}
},
"v1alpha1OperationInitiator": {
"type": "object",
"title": "OperationInitiator holds information about the operation initiator",
"properties": {
"automated": {
"description": "Automated is set to true if operation was initiated automatically by the application controller.",
"type": "boolean",
"format": "boolean"
},
"username": {
"description": "Name of a user who started operation.",
"type": "string"
}
}
},
"v1alpha1OperationState": {
"description": "OperationState contains information about state of currently performing operation on application.",
"type": "object",
@@ -4359,12 +4006,6 @@
},
"ignoreDifferences": {
"type": "string"
},
"knownTypeFields": {
"type": "array",
"items": {
"$ref": "#/definitions/v1alpha1KnownTypeField"
}
}
}
},
@@ -4546,13 +4187,6 @@
"source": {
"$ref": "#/definitions/v1alpha1ApplicationSource"
},
"syncOptions": {
"type": "array",
"title": "SyncOptions provide per-sync sync-options, e.g. Validate=false",
"items": {
"type": "string"
}
},
"syncStrategy": {
"$ref": "#/definitions/v1alpha1SyncStrategy"
}
@@ -4599,13 +4233,6 @@
"properties": {
"automated": {
"$ref": "#/definitions/v1alpha1SyncPolicyAutomated"
},
"syncOptions": {
"type": "array",
"title": "Options allow you to specify whole app sync-options",
"items": {
"type": "string"
}
}
}
},

View File

@@ -6,10 +6,6 @@ import (
"os"
"time"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
"github.com/argoproj/pkg/stats"
"github.com/go-redis/redis"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"k8s.io/client-go/kubernetes"
@@ -22,13 +18,14 @@ import (
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/controller"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/errors"
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
"github.com/argoproj/argo-cd/reposerver/apiclient"
cacheutil "github.com/argoproj/argo-cd/util/cache"
appstatecache "github.com/argoproj/argo-cd/util/cache/appstate"
"github.com/argoproj/argo-cd/util/cli"
"github.com/argoproj/argo-cd/util/kube"
"github.com/argoproj/argo-cd/util/settings"
"github.com/argoproj/argo-cd/util/stats"
)
const (
@@ -47,25 +44,23 @@ func newCommand() *cobra.Command {
selfHealTimeoutSeconds int
statusProcessors int
operationProcessors int
logFormat string
logLevel string
glogLevel int
metricsPort int
kubectlParallelismLimit int64
cacheSrc func() (*appstatecache.Cache, error)
redisClient *redis.Client
)
var command = cobra.Command{
Use: cliName,
Short: "application-controller is a controller to operate on applications CRD",
RunE: func(c *cobra.Command, args []string) error {
cli.SetLogFormat(logFormat)
cli.SetLogLevel(logLevel)
cli.SetGLogLevel(glogLevel)
config, err := clientConfig.ClientConfig()
errors.CheckError(err)
errors.CheckError(v1alpha1.SetK8SConfigDefaults(config))
config.QPS = common.K8sClientConfigQPS
config.Burst = common.K8sClientConfigBurst
kubeClient := kubernetes.NewForConfigOrDie(config)
appClient := appclientset.NewForConfigOrDie(config)
@@ -96,10 +91,8 @@ func newCommand() *cobra.Command {
metricsPort,
kubectlParallelismLimit)
errors.CheckError(err)
cacheutil.CollectMetrics(redisClient, appController.GetMetricsServer())
vers := common.GetVersion()
log.Infof("Application Controller (version: %s, built: %s) starting (namespace: %s)", vers.Version, vers.BuildDate, namespace)
log.Infof("Application Controller (version: %s) starting (namespace: %s)", common.GetVersion(), namespace)
stats.RegisterStackDumper()
stats.StartStatsTicker(10 * time.Minute)
stats.RegisterHeapDumper("memprofile")
@@ -117,15 +110,13 @@ func newCommand() *cobra.Command {
command.Flags().IntVar(&repoServerTimeoutSeconds, "repo-server-timeout-seconds", 60, "Repo server RPC call timeout seconds.")
command.Flags().IntVar(&statusProcessors, "status-processors", 1, "Number of application status processors")
command.Flags().IntVar(&operationProcessors, "operation-processors", 1, "Number of application operation processors")
command.Flags().StringVar(&logFormat, "logformat", "text", "Set the logging format. One of: text|json")
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
command.Flags().IntVar(&glogLevel, "gloglevel", 0, "Set the glog logging level")
command.Flags().IntVar(&metricsPort, "metrics-port", common.DefaultPortArgoCDMetrics, "Start metrics server on given port")
command.Flags().IntVar(&selfHealTimeoutSeconds, "self-heal-timeout-seconds", 5, "Specifies timeout between application self heal attempts")
command.Flags().Int64Var(&kubectlParallelismLimit, "kubectl-parallelism-limit", 20, "Number of allowed concurrent kubectl fork/execs. Any value less the 1 means no limit.")
cacheSrc = appstatecache.AddCacheFlagsToCmd(&command, func(client *redis.Client) {
redisClient = client
})
cacheSrc = appstatecache.AddCacheFlagsToCmd(&command)
return &command
}

View File

@@ -7,18 +7,16 @@ import (
"os"
"time"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
"github.com/argoproj/pkg/stats"
"github.com/go-redis/redis"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/errors"
"github.com/argoproj/argo-cd/reposerver"
reposervercache "github.com/argoproj/argo-cd/reposerver/cache"
"github.com/argoproj/argo-cd/reposerver/metrics"
cacheutil "github.com/argoproj/argo-cd/util/cache"
"github.com/argoproj/argo-cd/util/cli"
"github.com/argoproj/argo-cd/util/stats"
"github.com/argoproj/argo-cd/util/tls"
)
@@ -29,20 +27,17 @@ const (
func newCommand() *cobra.Command {
var (
logFormat string
logLevel string
parallelismLimit int64
listenPort int
metricsPort int
cacheSrc func() (*reposervercache.Cache, error)
tlsConfigCustomizerSrc func() (tls.ConfigCustomizer, error)
redisClient *redis.Client
)
var command = cobra.Command{
Use: cliName,
Short: "Run argocd-repo-server",
RunE: func(c *cobra.Command, args []string) error {
cli.SetLogFormat(logFormat)
cli.SetLogLevel(logLevel)
tlsConfigCustomizer, err := tlsConfigCustomizerSrc()
@@ -52,7 +47,6 @@ func newCommand() *cobra.Command {
errors.CheckError(err)
metricsServer := metrics.NewMetricsServer()
cacheutil.CollectMetrics(redisClient, metricsServer)
server, err := reposerver.NewServer(metricsServer, cache, tlsConfigCustomizer, parallelismLimit)
errors.CheckError(err)
@@ -73,15 +67,12 @@ func newCommand() *cobra.Command {
},
}
command.Flags().StringVar(&logFormat, "logformat", "text", "Set the logging format. One of: text|json")
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
command.Flags().Int64Var(&parallelismLimit, "parallelismlimit", 0, "Limit on number of concurrent manifests generate requests. Any value less the 1 means no limit.")
command.Flags().IntVar(&listenPort, "port", common.DefaultPortRepoServer, "Listen on given port for incoming connections")
command.Flags().IntVar(&metricsPort, "metrics-port", common.DefaultPortRepoServerMetrics, "Start metrics server on given port")
tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(&command)
cacheSrc = reposervercache.AddCacheFlagsToCmd(&command, func(client *redis.Client) {
redisClient = client
})
cacheSrc = reposervercache.AddCacheFlagsToCmd(&command)
return &command
}

View File

@@ -4,57 +4,33 @@ import (
"context"
"time"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
"github.com/argoproj/pkg/stats"
"github.com/go-redis/redis"
"github.com/spf13/cobra"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
log "github.com/sirupsen/logrus"
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/errors"
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
"github.com/argoproj/argo-cd/reposerver/apiclient"
"github.com/argoproj/argo-cd/server"
servercache "github.com/argoproj/argo-cd/server/cache"
"github.com/argoproj/argo-cd/util/cli"
"github.com/argoproj/argo-cd/util/env"
"github.com/argoproj/argo-cd/util/kube"
"github.com/argoproj/argo-cd/util/stats"
"github.com/argoproj/argo-cd/util/tls"
)
const (
failureRetryCountEnv = "ARGOCD_K8S_RETRY_COUNT"
failureRetryPeriodMilliSecondsEnv = "ARGOCD_K8S_RETRY_DURATION_MILLISECONDS"
)
var (
failureRetryCount = 0
failureRetryPeriodMilliSeconds = 100
)
func init() {
failureRetryCount = env.ParseNumFromEnv(failureRetryCountEnv, failureRetryCount, 0, 10)
failureRetryPeriodMilliSeconds = env.ParseNumFromEnv(failureRetryPeriodMilliSecondsEnv, failureRetryPeriodMilliSeconds, 0, 1000)
}
// NewCommand returns a new instance of an argocd command
func NewCommand() *cobra.Command {
var (
redisClient *redis.Client
insecure bool
listenPort int
metricsPort int
logFormat string
logLevel string
glogLevel int
clientConfig clientcmd.ClientConfig
repoServerTimeoutSeconds int
staticAssetsDir string
baseHRef string
rootPath string
repoServerAddress string
dexServerAddress string
disableAuth bool
@@ -67,13 +43,13 @@ func NewCommand() *cobra.Command {
Short: "Run the argocd API server",
Long: "Run the argocd API server",
Run: func(c *cobra.Command, args []string) {
cli.SetLogFormat(logFormat)
cli.SetLogLevel(logLevel)
cli.SetGLogLevel(glogLevel)
config, err := clientConfig.ClientConfig()
errors.CheckError(err)
errors.CheckError(v1alpha1.SetK8SConfigDefaults(config))
config.QPS = common.K8sClientConfigQPS
config.Burst = common.K8sClientConfigBurst
namespace, _, err := clientConfig.Namespace()
errors.CheckError(err)
@@ -84,24 +60,9 @@ func NewCommand() *cobra.Command {
errors.CheckError(err)
kubeclientset := kubernetes.NewForConfigOrDie(config)
appclientsetConfig, err := clientConfig.ClientConfig()
errors.CheckError(err)
errors.CheckError(v1alpha1.SetK8SConfigDefaults(appclientsetConfig))
if failureRetryCount > 0 {
appclientsetConfig = kube.AddFailureRetryWrapper(appclientsetConfig, failureRetryCount, failureRetryPeriodMilliSeconds)
}
appclientset := appclientset.NewForConfigOrDie(appclientsetConfig)
appclientset := appclientset.NewForConfigOrDie(config)
repoclientset := apiclient.NewRepoServerClientset(repoServerAddress, repoServerTimeoutSeconds)
if rootPath != "" {
if baseHRef != "" && baseHRef != rootPath {
log.Warnf("--basehref and --rootpath had conflict: basehref: %s rootpath: %s", baseHRef, rootPath)
}
baseHRef = rootPath
}
argoCDOpts := server.ArgoCDServerOpts{
Insecure: insecure,
ListenPort: listenPort,
@@ -109,7 +70,6 @@ func NewCommand() *cobra.Command {
Namespace: namespace,
StaticAssetsDir: staticAssetsDir,
BaseHRef: baseHRef,
RootPath: rootPath,
KubeClientset: kubeclientset,
AppClientset: appclientset,
RepoClientset: repoclientset,
@@ -118,7 +78,6 @@ func NewCommand() *cobra.Command {
TLSConfigCustomizer: tlsConfigCustomizer,
Cache: cache,
XFrameOptions: frameOptions,
RedisClient: redisClient,
}
stats.RegisterStackDumper()
@@ -139,8 +98,6 @@ func NewCommand() *cobra.Command {
command.Flags().BoolVar(&insecure, "insecure", false, "Run server without TLS")
command.Flags().StringVar(&staticAssetsDir, "staticassets", "", "Static assets directory path")
command.Flags().StringVar(&baseHRef, "basehref", "/", "Value for base href in index.html. Used if Argo CD is running behind reverse proxy under subpath different from /")
command.Flags().StringVar(&rootPath, "rootpath", "", "Used if Argo CD is running behind reverse proxy under subpath different from /")
command.Flags().StringVar(&logFormat, "logformat", "text", "Set the logging format. One of: text|json")
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
command.Flags().IntVar(&glogLevel, "gloglevel", 0, "Set the glog logging level")
command.Flags().StringVar(&repoServerAddress, "repo-server", common.DefaultRepoServerAddr, "Repo server address")
@@ -152,8 +109,6 @@ func NewCommand() *cobra.Command {
command.Flags().IntVar(&repoServerTimeoutSeconds, "repo-server-timeout-seconds", 60, "Repo server RPC call timeout seconds.")
command.Flags().StringVar(&frameOptions, "x-frame-options", "sameorigin", "Set X-Frame-Options header in HTTP responses to `value`. To disable, set to \"\".")
tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(command)
cacheSrc = servercache.AddCacheFlagsToCmd(command, func(client *redis.Client) {
redisClient = client
})
cacheSrc = servercache.AddCacheFlagsToCmd(command)
return command
}

View File

@@ -1,9 +1,8 @@
package main
import (
"github.com/argoproj/gitops-engine/pkg/utils/errors"
commands "github.com/argoproj/argo-cd/cmd/argocd-server/commands"
"github.com/argoproj/argo-cd/errors"
// load the gcp plugin (required to authenticate against GKE clusters).
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"

View File

@@ -1,546 +0,0 @@
package commands
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"os"
"reflect"
"sort"
"strconv"
"strings"
"text/tabwriter"
"github.com/argoproj/gitops-engine/pkg/diff"
healthutil "github.com/argoproj/gitops-engine/pkg/health"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
"github.com/ghodss/yaml"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/tools/clientcmd"
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/util/argo/normalizers"
"github.com/argoproj/argo-cd/util/cli"
"github.com/argoproj/argo-cd/util/lua"
"github.com/argoproj/argo-cd/util/settings"
)
type settingsOpts struct {
argocdCMPath string
argocdSecretPath string
loadClusterSettings bool
clientConfig clientcmd.ClientConfig
}
type commandContext interface {
createSettingsManager() (*settings.SettingsManager, error)
}
func collectLogs(callback func()) string {
log.SetLevel(log.DebugLevel)
out := bytes.Buffer{}
log.SetOutput(&out)
defer log.SetLevel(log.FatalLevel)
callback()
return out.String()
}
func setSettingsMeta(obj v1.Object) {
obj.SetNamespace("default")
labels := obj.GetLabels()
if labels == nil {
labels = make(map[string]string)
}
labels["app.kubernetes.io/part-of"] = "argocd"
obj.SetLabels(labels)
}
func (opts *settingsOpts) createSettingsManager() (*settings.SettingsManager, error) {
var argocdCM *corev1.ConfigMap
if opts.argocdCMPath == "" && !opts.loadClusterSettings {
return nil, fmt.Errorf("either --argocd-cm-path must be provided or --load-cluster-settings must be set to true")
} else if opts.argocdCMPath == "" {
realClientset, ns, err := opts.getK8sClient()
if err != nil {
return nil, err
}
argocdCM, err = realClientset.CoreV1().ConfigMaps(ns).Get(common.ArgoCDConfigMapName, v1.GetOptions{})
if err != nil {
return nil, err
}
} else {
data, err := ioutil.ReadFile(opts.argocdCMPath)
if err != nil {
return nil, err
}
err = yaml.Unmarshal(data, &argocdCM)
if err != nil {
return nil, err
}
}
setSettingsMeta(argocdCM)
var argocdSecret *corev1.Secret
if opts.argocdSecretPath != "" {
data, err := ioutil.ReadFile(opts.argocdSecretPath)
if err != nil {
return nil, err
}
err = yaml.Unmarshal(data, &argocdSecret)
if err != nil {
return nil, err
}
setSettingsMeta(argocdSecret)
} else if opts.loadClusterSettings {
realClientset, ns, err := opts.getK8sClient()
if err != nil {
return nil, err
}
argocdSecret, err = realClientset.CoreV1().Secrets(ns).Get(common.ArgoCDSecretName, v1.GetOptions{})
if err != nil {
return nil, err
}
} else {
argocdSecret = &corev1.Secret{
ObjectMeta: v1.ObjectMeta{
Name: common.ArgoCDSecretName,
},
Data: map[string][]byte{
"admin.password": []byte("test"),
"server.secretkey": []byte("test"),
},
}
}
setSettingsMeta(argocdSecret)
clientset := fake.NewSimpleClientset(argocdSecret, argocdCM)
manager := settings.NewSettingsManager(context.Background(), clientset, "default")
errors.CheckError(manager.ResyncInformers())
return manager, nil
}
func (opts *settingsOpts) getK8sClient() (*kubernetes.Clientset, string, error) {
namespace, _, err := opts.clientConfig.Namespace()
if err != nil {
return nil, "", err
}
restConfig, err := opts.clientConfig.ClientConfig()
if err != nil {
return nil, "", err
}
realClientset, err := kubernetes.NewForConfig(restConfig)
if err != nil {
return nil, "", err
}
return realClientset, namespace, nil
}
func NewSettingsCommand() *cobra.Command {
var (
opts settingsOpts
)
var command = &cobra.Command{
Use: "settings",
Short: "Provides set of commands for settings validation and troubleshooting",
Run: func(c *cobra.Command, args []string) {
c.HelpFunc()(c, args)
},
}
log.SetLevel(log.FatalLevel)
command.AddCommand(NewValidateSettingsCommand(&opts))
command.AddCommand(NewResourceOverridesCommand(&opts))
opts.clientConfig = cli.AddKubectlFlagsToCmd(command)
command.PersistentFlags().StringVar(&opts.argocdCMPath, "argocd-cm-path", "", "Path to local argocd-cm.yaml file")
command.PersistentFlags().StringVar(&opts.argocdSecretPath, "argocd-secret-path", "", "Path to local argocd-secret.yaml file")
command.PersistentFlags().BoolVar(&opts.loadClusterSettings, "load-cluster-settings", false,
"Indicates that config map and secret should be loaded from cluster unless local file path is provided")
return command
}
type settingValidator func(manager *settings.SettingsManager) (string, error)
func joinValidators(validators ...settingValidator) settingValidator {
return func(manager *settings.SettingsManager) (string, error) {
var errorStrs []string
var summaries []string
for i := range validators {
summary, err := validators[i](manager)
if err != nil {
errorStrs = append(errorStrs, err.Error())
}
if summary != "" {
summaries = append(summaries, summary)
}
}
if len(errorStrs) > 0 {
return "", fmt.Errorf("%s", strings.Join(errorStrs, "\n"))
}
return strings.Join(summaries, "\n"), nil
}
}
var validatorsByGroup = map[string]settingValidator{
"general": joinValidators(func(manager *settings.SettingsManager) (string, error) {
general, err := manager.GetSettings()
if err != nil {
return "", err
}
ssoProvider := ""
if general.DexConfig != "" {
if _, err := settings.UnmarshalDexConfig(general.DexConfig); err != nil {
return "", fmt.Errorf("invalid dex.config: %v", err)
}
ssoProvider = "Dex"
} else if general.OIDCConfigRAW != "" {
if _, err := settings.UnmarshalOIDCConfig(general.OIDCConfigRAW); err != nil {
return "", fmt.Errorf("invalid oidc.config: %v", err)
}
ssoProvider = "OIDC"
}
var summary string
if ssoProvider != "" {
summary = fmt.Sprintf("%s is configured", ssoProvider)
if general.URL == "" {
summary = summary + " ('url' field is missing)"
}
} else if ssoProvider != "" && general.URL != "" {
} else {
summary = "SSO is not configured"
}
return summary, nil
}, func(manager *settings.SettingsManager) (string, error) {
_, err := manager.GetAppInstanceLabelKey()
return "", err
}, func(manager *settings.SettingsManager) (string, error) {
_, err := manager.GetHelp()
return "", err
}, func(manager *settings.SettingsManager) (string, error) {
_, err := manager.GetGoogleAnalytics()
return "", err
}),
"plugins": func(manager *settings.SettingsManager) (string, error) {
plugins, err := manager.GetConfigManagementPlugins()
if err != nil {
return "", err
}
return fmt.Sprintf("%d plugins", len(plugins)), nil
},
"kustomize": func(manager *settings.SettingsManager) (string, error) {
opts, err := manager.GetKustomizeSettings()
if err != nil {
return "", err
}
summary := "default options"
if opts.BuildOptions != "" {
summary = opts.BuildOptions
}
if len(opts.Versions) > 0 {
summary = fmt.Sprintf("%s (%d versions)", summary, len(opts.Versions))
}
return summary, err
},
"repositories": joinValidators(func(manager *settings.SettingsManager) (string, error) {
repos, err := manager.GetRepositories()
if err != nil {
return "", err
}
return fmt.Sprintf("%d repositories", len(repos)), nil
}, func(manager *settings.SettingsManager) (string, error) {
creds, err := manager.GetRepositoryCredentials()
if err != nil {
return "", err
}
return fmt.Sprintf("%d repository credentials", len(creds)), nil
}),
"accounts": func(manager *settings.SettingsManager) (string, error) {
accounts, err := manager.GetAccounts()
if err != nil {
return "", err
}
return fmt.Sprintf("%d accounts", len(accounts)), nil
},
"resource-overrides": func(manager *settings.SettingsManager) (string, error) {
overrides, err := manager.GetResourceOverrides()
if err != nil {
return "", err
}
return fmt.Sprintf("%d resource overrides", len(overrides)), nil
},
}
func NewValidateSettingsCommand(cmdCtx commandContext) *cobra.Command {
var (
groups []string
)
var allGroups []string
for k := range validatorsByGroup {
allGroups = append(allGroups, k)
}
sort.Slice(allGroups, func(i, j int) bool {
return allGroups[i] < allGroups[j]
})
var command = &cobra.Command{
Use: "validate",
Short: "Validate settings",
Long: "Validates settings specified in 'argocd-cm' ConfigMap and 'argocd-secret' Secret",
Example: `
#Validates all settings in the specified YAML file
argocd-util settings validate --argocd-cm-path ./argocd-cm.yaml
#Validates accounts and plugins settings in Kubernetes cluster of current kubeconfig context
argocd-util settings validate --group accounts --group plugins --load-cluster-settings`,
Run: func(c *cobra.Command, args []string) {
settingsManager, err := cmdCtx.createSettingsManager()
errors.CheckError(err)
if len(groups) == 0 {
groups = allGroups
}
for i, group := range groups {
validator := validatorsByGroup[group]
logs := collectLogs(func() {
summary, err := validator(settingsManager)
if err != nil {
_, _ = fmt.Fprintf(os.Stdout, "❌ %s\n", group)
_, _ = fmt.Fprintf(os.Stdout, "%s\n", err.Error())
} else {
_, _ = fmt.Fprintf(os.Stdout, "✅ %s\n", group)
if summary != "" {
_, _ = fmt.Fprintf(os.Stdout, "%s\n", summary)
}
}
})
if logs != "" {
_, _ = fmt.Fprintf(os.Stdout, "%s\n", logs)
}
if i != len(groups)-1 {
_, _ = fmt.Fprintf(os.Stdout, "\n")
}
}
},
}
command.Flags().StringArrayVar(&groups, "group", nil, fmt.Sprintf(
"Optional list of setting groups that have to be validated ( one of: %s)", strings.Join(allGroups, ", ")))
return command
}
func NewResourceOverridesCommand(cmdCtx commandContext) *cobra.Command {
var command = &cobra.Command{
Use: "resource-overrides",
Short: "Troubleshoot resource overrides",
Run: func(c *cobra.Command, args []string) {
c.HelpFunc()(c, args)
},
}
command.AddCommand(NewResourceIgnoreDifferencesCommand(cmdCtx))
command.AddCommand(NewResourceActionListCommand(cmdCtx))
command.AddCommand(NewResourceActionRunCommand(cmdCtx))
command.AddCommand(NewResourceHealthCommand(cmdCtx))
return command
}
func executeResourceOverrideCommand(cmdCtx commandContext, args []string, callback func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride)) {
data, err := ioutil.ReadFile(args[0])
errors.CheckError(err)
res := unstructured.Unstructured{}
errors.CheckError(yaml.Unmarshal(data, &res))
settingsManager, err := cmdCtx.createSettingsManager()
errors.CheckError(err)
overrides, err := settingsManager.GetResourceOverrides()
errors.CheckError(err)
gvk := res.GroupVersionKind()
key := gvk.Kind
if gvk.Group != "" {
key = fmt.Sprintf("%s/%s", gvk.Group, gvk.Kind)
}
override, hasOverride := overrides[key]
if !hasOverride {
_, _ = fmt.Printf("No overrides configured for '%s/%s'\n", gvk.Group, gvk.Kind)
return
}
callback(res, override, overrides)
}
func NewResourceIgnoreDifferencesCommand(cmdCtx commandContext) *cobra.Command {
var command = &cobra.Command{
Use: "ignore-differences RESOURCE_YAML_PATH",
Short: "Renders fields excluded from diffing",
Long: "Renders ignored fields using the 'ignoreDifferences' setting specified in the 'resource.customizations' field of 'argocd-cm' ConfigMap",
Example: `
argocd-util settings resource-overrides ignore-differences ./deploy.yaml --argocd-cm-path ./argocd-cm.yaml`,
Run: func(c *cobra.Command, args []string) {
if len(args) < 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
executeResourceOverrideCommand(cmdCtx, args, func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride) {
gvk := res.GroupVersionKind()
if override.IgnoreDifferences == "" {
_, _ = fmt.Printf("Ignore differences are not configured for '%s/%s'\n", gvk.Group, gvk.Kind)
return
}
normalizer, err := normalizers.NewIgnoreNormalizer(nil, overrides)
errors.CheckError(err)
normalizedRes := res.DeepCopy()
logs := collectLogs(func() {
errors.CheckError(normalizer.Normalize(normalizedRes))
})
if logs != "" {
_, _ = fmt.Println(logs)
}
if reflect.DeepEqual(&res, normalizedRes) {
_, _ = fmt.Printf("No fields are ignored by ignoreDifferences settings: \n%s\n", override.IgnoreDifferences)
return
}
_, _ = fmt.Printf("Following fields are ignored:\n\n")
_ = diff.PrintDiff(res.GetName(), &res, normalizedRes)
})
},
}
return command
}
func NewResourceHealthCommand(cmdCtx commandContext) *cobra.Command {
var command = &cobra.Command{
Use: "health RESOURCE_YAML_PATH",
Short: "Assess resource health",
Long: "Assess resource health using the lua script configured in the 'resource.customizations' field of 'argocd-cm' ConfigMap",
Example: `
argocd-util settings resource-overrides health ./deploy.yaml --argocd-cm-path ./argocd-cm.yaml`,
Run: func(c *cobra.Command, args []string) {
if len(args) < 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
executeResourceOverrideCommand(cmdCtx, args, func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride) {
gvk := res.GroupVersionKind()
if override.HealthLua == "" {
_, _ = fmt.Printf("Health script is not configured for '%s/%s'\n", gvk.Group, gvk.Kind)
return
}
resHealth, err := healthutil.GetResourceHealth(&res, lua.ResourceHealthOverrides(overrides))
errors.CheckError(err)
_, _ = fmt.Printf("STATUS: %s\n", resHealth.Status)
_, _ = fmt.Printf("MESSAGE: %s\n", resHealth.Message)
})
},
}
return command
}
func NewResourceActionListCommand(cmdCtx commandContext) *cobra.Command {
var command = &cobra.Command{
Use: "list-actions RESOURCE_YAML_PATH",
Short: "List available resource actions",
Long: "List actions available for given resource action using the lua scripts configured in the 'resource.customizations' field of 'argocd-cm' ConfigMap and outputs updated fields",
Example: `
argocd-util settings resource-overrides action list /tmp/deploy.yaml --argocd-cm-path ./argocd-cm.yaml`,
Run: func(c *cobra.Command, args []string) {
if len(args) < 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
executeResourceOverrideCommand(cmdCtx, args, func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride) {
gvk := res.GroupVersionKind()
if override.Actions == "" {
_, _ = fmt.Printf("Actions are not configured for '%s/%s'\n", gvk.Group, gvk.Kind)
return
}
luaVM := lua.VM{ResourceOverrides: overrides}
discoveryScript, err := luaVM.GetResourceActionDiscovery(&res)
errors.CheckError(err)
availableActions, err := luaVM.ExecuteResourceActionDiscovery(&res, discoveryScript)
errors.CheckError(err)
sort.Slice(availableActions, func(i, j int) bool {
return availableActions[i].Name < availableActions[j].Name
})
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintf(w, "NAME\tENABLED\n")
for _, action := range availableActions {
_, _ = fmt.Fprintf(w, "%s\t%s\n", action.Name, strconv.FormatBool(action.Disabled))
}
_ = w.Flush()
})
},
}
return command
}
func NewResourceActionRunCommand(cmdCtx commandContext) *cobra.Command {
var command = &cobra.Command{
Use: "run-action RESOURCE_YAML_PATH ACTION",
Aliases: []string{"action"},
Short: "Executes resource action",
Long: "Executes resource action using the lua script configured in the 'resource.customizations' field of 'argocd-cm' ConfigMap and outputs updated fields",
Example: `
argocd-util settings resource-overrides action run /tmp/deploy.yaml restart --argocd-cm-path ./argocd-cm.yaml`,
Run: func(c *cobra.Command, args []string) {
if len(args) < 2 {
c.HelpFunc()(c, args)
os.Exit(1)
}
action := args[1]
executeResourceOverrideCommand(cmdCtx, args, func(res unstructured.Unstructured, override v1alpha1.ResourceOverride, overrides map[string]v1alpha1.ResourceOverride) {
gvk := res.GroupVersionKind()
if override.Actions == "" {
_, _ = fmt.Printf("Actions are not configured for '%s/%s'\n", gvk.Group, gvk.Kind)
return
}
luaVM := lua.VM{ResourceOverrides: overrides}
action, err := luaVM.GetResourceAction(&res, action)
errors.CheckError(err)
modifiedRes, err := luaVM.ExecuteResourceAction(&res, action.ActionLua)
errors.CheckError(err)
if reflect.DeepEqual(&res, modifiedRes) {
_, _ = fmt.Printf("No fields had been changed by action: \n%s\n", action.Name)
return
}
_, _ = fmt.Printf("Following fields have been changed:\n\n")
_ = diff.PrintDiff(res.GetName(), &res, modifiedRes)
})
},
}
return command
}

View File

@@ -1,383 +0,0 @@
package commands
import (
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"os"
"testing"
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/util/settings"
utils "github.com/argoproj/gitops-engine/pkg/utils/io"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)
func captureStdout(callback func()) (string, error) {
oldStdout := os.Stdout
oldStderr := os.Stderr
r, w, err := os.Pipe()
if err != nil {
return "", err
}
os.Stdout = w
defer func() {
os.Stdout = oldStdout
os.Stderr = oldStderr
}()
callback()
utils.Close(w)
data, err := ioutil.ReadAll(r)
if err != nil {
return "", err
}
return string(data), err
}
func newSettingsManager(data map[string]string) *settings.SettingsManager {
clientset := fake.NewSimpleClientset(&v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: common.ArgoCDConfigMapName,
Labels: map[string]string{
"app.kubernetes.io/part-of": "argocd",
},
},
Data: data,
}, &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: common.ArgoCDSecretName,
},
Data: map[string][]byte{
"admin.password": []byte("test"),
"server.secretkey": []byte("test"),
},
})
return settings.NewSettingsManager(context.Background(), clientset, "default")
}
type fakeCmdContext struct {
mgr *settings.SettingsManager
// nolint:unused,structcheck
out bytes.Buffer
}
func newCmdContext(data map[string]string) *fakeCmdContext {
return &fakeCmdContext{mgr: newSettingsManager(data)}
}
func (ctx *fakeCmdContext) createSettingsManager() (*settings.SettingsManager, error) {
return ctx.mgr, nil
}
type validatorTestCase struct {
validator string
data map[string]string
containsSummary string
containsError string
}
func TestCreateSettingsManager(t *testing.T) {
f, closer, err := tempFile(`apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
data:
url: https://myargocd.com`)
if !assert.NoError(t, err) {
return
}
defer utils.Close(closer)
opts := settingsOpts{argocdCMPath: f}
settingsManager, err := opts.createSettingsManager()
if !assert.NoError(t, err) {
return
}
argoCDSettings, err := settingsManager.GetSettings()
if !assert.NoError(t, err) {
return
}
assert.Equal(t, "https://myargocd.com", argoCDSettings.URL)
}
func TestValidator(t *testing.T) {
testCases := map[string]validatorTestCase{
"General_SSOIsNotConfigured": {
validator: "general", containsSummary: "SSO is not configured",
},
"General_DexInvalidConfig": {
validator: "general",
data: map[string]string{"dex.config": "abcdefg"},
containsError: "invalid dex.config",
},
"General_OIDCConfigured": {
validator: "general",
data: map[string]string{
"url": "https://myargocd.com",
"oidc.config": `
name: Okta
issuer: https://dev-123456.oktapreview.com
clientID: aaaabbbbccccddddeee
clientSecret: aaaabbbbccccddddeee`,
},
containsSummary: "OIDC is configured",
},
"General_DexConfiguredMissingURL": {
validator: "general",
data: map[string]string{
"dex.config": `connectors:
- type: github
name: GitHub
config:
clientID: aabbccddeeff00112233
clientSecret: aabbccddeeff00112233`,
},
containsSummary: "Dex is configured ('url' field is missing)",
},
"Plugins_ValidConfig": {
validator: "plugins",
data: map[string]string{
"configManagementPlugins": `[{"name": "test1"}, {"name": "test2"}]`,
},
containsSummary: "2 plugins",
},
"Kustomize_ModifiedOptions": {
validator: "kustomize",
containsSummary: "default options",
},
"Kustomize_DefaultOptions": {
validator: "kustomize",
data: map[string]string{
"kustomize.buildOptions": "updated-options (2 versions)",
"kustomize.versions.v123": "binary-123",
"kustomize.versions.v321": "binary-321",
},
containsSummary: "updated-options",
},
"Repositories": {
validator: "repositories",
data: map[string]string{
"repositories": `
- url: https://github.com/argoproj/my-private-repository1
- url: https://github.com/argoproj/my-private-repository2`,
},
containsSummary: "2 repositories",
},
"Accounts": {
validator: "accounts",
data: map[string]string{
"accounts.user1": "apiKey, login",
"accounts.user2": "login",
"accounts.user3": "apiKey",
},
containsSummary: "4 accounts",
},
"ResourceOverrides": {
validator: "resource-overrides",
data: map[string]string{
"resource.customizations": `
admissionregistration.k8s.io/MutatingWebhookConfiguration:
ignoreDifferences: |
jsonPointers:
- /webhooks/0/clientConfig/caBundle`,
},
containsSummary: "1 resource overrides",
},
}
for name := range testCases {
tc := testCases[name]
t.Run(name, func(t *testing.T) {
validator, ok := validatorsByGroup[tc.validator]
if !assert.True(t, ok) {
return
}
summary, err := validator(newSettingsManager(tc.data))
if tc.containsSummary != "" {
assert.NoError(t, err)
assert.Contains(t, summary, tc.containsSummary)
} else if tc.containsError != "" {
if assert.Error(t, err) {
assert.Contains(t, err.Error(), tc.containsError)
}
}
})
}
}
const (
testDeploymentYAML = `apiVersion: v1
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 0`
)
func tempFile(content string) (string, io.Closer, error) {
f, err := ioutil.TempFile("", "*.yaml")
if err != nil {
return "", nil, err
}
_, err = f.Write([]byte(content))
if err != nil {
_ = os.Remove(f.Name())
return "", nil, err
}
return f.Name(), utils.NewCloser(func() error {
return os.Remove(f.Name())
}), nil
}
func TestValidateSettingsCommand_NoErrors(t *testing.T) {
cmd := NewValidateSettingsCommand(newCmdContext(map[string]string{}))
out, err := captureStdout(func() {
err := cmd.Execute()
assert.NoError(t, err)
})
assert.NoError(t, err)
for k := range validatorsByGroup {
assert.Contains(t, out, fmt.Sprintf("✅ %s", k))
}
}
func TestResourceOverrideIgnoreDifferences(t *testing.T) {
f, closer, err := tempFile(testDeploymentYAML)
if !assert.NoError(t, err) {
return
}
defer utils.Close(closer)
t.Run("NoOverridesConfigured", func(t *testing.T) {
cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{}))
out, err := captureStdout(func() {
cmd.SetArgs([]string{"ignore-differences", f})
err := cmd.Execute()
assert.NoError(t, err)
})
assert.NoError(t, err)
assert.Contains(t, out, "No overrides configured")
})
t.Run("DataIgnored", func(t *testing.T) {
cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
"resource.customizations": `apps/Deployment:
ignoreDifferences: |
jsonPointers:
- /spec`}))
out, err := captureStdout(func() {
cmd.SetArgs([]string{"ignore-differences", f})
err := cmd.Execute()
assert.NoError(t, err)
})
assert.NoError(t, err)
assert.Contains(t, out, "< spec:")
})
}
func TestResourceOverrideHealth(t *testing.T) {
f, closer, err := tempFile(testDeploymentYAML)
if !assert.NoError(t, err) {
return
}
defer utils.Close(closer)
t.Run("NoHealthAssessment", func(t *testing.T) {
cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
"resource.customizations": `apps/Deployment: {}`}))
out, err := captureStdout(func() {
cmd.SetArgs([]string{"health", f})
err := cmd.Execute()
assert.NoError(t, err)
})
assert.NoError(t, err)
assert.Contains(t, out, "Health script is not configured")
})
t.Run("HealthAssessmentConfigured", func(t *testing.T) {
cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
"resource.customizations": `apps/Deployment:
health.lua: |
return { status = "Progressing" }
`}))
out, err := captureStdout(func() {
cmd.SetArgs([]string{"health", f})
err := cmd.Execute()
assert.NoError(t, err)
})
assert.NoError(t, err)
assert.Contains(t, out, "Progressing")
})
}
func TestResourceOverrideAction(t *testing.T) {
f, closer, err := tempFile(testDeploymentYAML)
if !assert.NoError(t, err) {
return
}
defer utils.Close(closer)
t.Run("NoActions", func(t *testing.T) {
cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
"resource.customizations": `apps/Deployment: {}`}))
out, err := captureStdout(func() {
cmd.SetArgs([]string{"run-action", f, "test"})
err := cmd.Execute()
assert.NoError(t, err)
})
assert.NoError(t, err)
assert.Contains(t, out, "Actions are not configured")
})
t.Run("ActionConfigured", func(t *testing.T) {
cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
"resource.customizations": `apps/Deployment:
actions: |
discovery.lua: |
actions = {}
actions["resume"] = {["disabled"] = false}
actions["restart"] = {["disabled"] = false}
return actions
definitions:
- name: test
action.lua: |
obj.metadata.labels["test"] = 'updated'
return obj
`}))
out, err := captureStdout(func() {
cmd.SetArgs([]string{"run-action", f, "test"})
err := cmd.Execute()
assert.NoError(t, err)
})
assert.NoError(t, err)
assert.Contains(t, out, "test: updated")
out, err = captureStdout(func() {
cmd.SetArgs([]string{"list-actions", f})
err := cmd.Execute()
assert.NoError(t, err)
})
assert.NoError(t, err)
assert.Contains(t, out, `NAME ENABLED
restart false
resume false
`)
})
}

View File

@@ -11,8 +11,6 @@ import (
"reflect"
"syscall"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
"github.com/ghodss/yaml"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@@ -26,11 +24,13 @@ import (
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"github.com/argoproj/argo-cd/cmd/argocd-util/commands"
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/errors"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/cli"
"github.com/argoproj/argo-cd/util/db"
"github.com/argoproj/argo-cd/util/dex"
"github.com/argoproj/argo-cd/util/kube"
"github.com/argoproj/argo-cd/util/settings"
// load the gcp plugin (required to authenticate against GKE clusters).
@@ -56,8 +56,7 @@ var (
// NewCommand returns a new instance of an argocd command
func NewCommand() *cobra.Command {
var (
logFormat string
logLevel string
logLevel string
)
var command = &cobra.Command{
@@ -74,10 +73,8 @@ func NewCommand() *cobra.Command {
command.AddCommand(NewImportCommand())
command.AddCommand(NewExportCommand())
command.AddCommand(NewClusterConfig())
command.AddCommand(commands.NewProjectsCommand())
command.AddCommand(commands.NewSettingsCommand())
command.AddCommand(NewProjectsCommand())
command.Flags().StringVar(&logFormat, "logformat", "text", "Set the logging format. One of: text|json")
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
return command
}
@@ -112,7 +109,7 @@ func NewRunDexCommand() *cobra.Command {
} else {
err = ioutil.WriteFile("/tmp/dex.yaml", dexCfgBytes, 0644)
errors.CheckError(err)
log.Debug(redactor(string(dexCfgBytes)))
log.Info(redactor(string(dexCfgBytes)))
cmd = exec.Command("dex", "serve", "/tmp/dex.yaml")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
@@ -388,14 +385,8 @@ func NewExportCommand() *cobra.Command {
} else {
f, err := os.Create(out)
errors.CheckError(err)
bw := bufio.NewWriter(f)
writer = bw
defer func() {
err = bw.Flush()
errors.CheckError(err)
err = f.Close()
errors.CheckError(err)
}()
defer util.Close(f)
writer = bufio.NewWriter(f)
}
acdClients := newArgoCDClientsets(config, namespace)
@@ -549,21 +540,10 @@ func specsEqual(left, right unstructured.Unstructured) bool {
leftData, _, _ := unstructured.NestedMap(left.Object, "data")
rightData, _, _ := unstructured.NestedMap(right.Object, "data")
return reflect.DeepEqual(leftData, rightData)
case "AppProject":
case "AppProject", "Application":
leftSpec, _, _ := unstructured.NestedMap(left.Object, "spec")
rightSpec, _, _ := unstructured.NestedMap(right.Object, "spec")
return reflect.DeepEqual(leftSpec, rightSpec)
case "Application":
leftSpec, _, _ := unstructured.NestedMap(left.Object, "spec")
rightSpec, _, _ := unstructured.NestedMap(right.Object, "spec")
leftStatus, _, _ := unstructured.NestedMap(left.Object, "status")
rightStatus, _, _ := unstructured.NestedMap(right.Object, "status")
// reconciledAt and observedAt are constantly changing and we ignore any diff there
delete(leftStatus, "reconciledAt")
delete(rightStatus, "reconciledAt")
delete(leftStatus, "observedAt")
delete(rightStatus, "observedAt")
return reflect.DeepEqual(leftSpec, rightSpec) && reflect.DeepEqual(leftStatus, rightStatus)
}
return false
}
@@ -578,13 +558,8 @@ func updateLive(bak, live *unstructured.Unstructured) *unstructured.Unstructured
switch live.GetKind() {
case "Secret", "ConfigMap":
newLive.Object["data"] = bak.Object["data"]
case "AppProject":
case "AppProject", "Application":
newLive.Object["spec"] = bak.Object["spec"]
case "Application":
newLive.Object["spec"] = bak.Object["spec"]
if _, ok := bak.Object["status"]; ok {
newLive.Object["status"] = bak.Object["status"]
}
}
return newLive
}
@@ -636,7 +611,7 @@ func NewClusterConfig() *cobra.Command {
cluster, err := db.NewDB(namespace, settings.NewSettingsManager(context.Background(), kubeclientset, namespace), kubeclientset).GetCluster(context.Background(), serverUrl)
errors.CheckError(err)
err = kube.WriteKubeConfig(cluster.RawRestConfig(), namespace, output)
err = kube.WriteKubeConfig(cluster.RESTConfig(), namespace, output)
errors.CheckError(err)
},
}
@@ -665,7 +640,7 @@ func redactor(dirtyString string) string {
err := yaml.Unmarshal([]byte(dirtyString), &config)
errors.CheckError(err)
iterateStringFields(config, func(name string, val string) string {
if name == "clientSecret" || name == "secret" || name == "bindPW" {
if name == "clientSecret" || name == "secret" {
return "********"
} else {
return val

View File

@@ -18,17 +18,8 @@ connectors:
id: github
name: GitHub
type: github
- config:
bindDN: uid=serviceaccount,cn=users,dc=example,dc=com
bindPW: theSecret
host: ldap.example.com:636
id: ldap
name: LDAP
type: ldap
grpc:
addr: 0.0.0.0:5557
telemetry:
http: 0.0.0.0:5558
issuer: https://argocd.example.com/api/dex
oauth2:
skipApprovalScreen: true
@@ -58,13 +49,6 @@ var expectedRedaction = `connectors:
id: github
name: GitHub
type: github
- config:
bindDN: uid=serviceaccount,cn=users,dc=example,dc=com
bindPW: '********'
host: ldap.example.com:636
id: ldap
name: LDAP
type: ldap
grpc:
addr: 0.0.0.0:5557
issuer: https://argocd.example.com/api/dex
@@ -83,8 +67,6 @@ staticClients:
- http://localhost
storage:
type: memory
telemetry:
http: 0.0.0.0:5558
web:
http: 0.0.0.0:5556
`

View File

@@ -1,4 +1,4 @@
package commands
package main
import (
"fmt"
@@ -6,14 +6,14 @@ import (
"path/filepath"
"strings"
"github.com/argoproj/argo-cd/errors"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
appclient "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/typed/application/v1alpha1"
"github.com/argoproj/argo-cd/util/cli"
"github.com/argoproj/argo-cd/util/diff"
"github.com/argoproj/argo-cd/util/kube"
"github.com/argoproj/gitops-engine/pkg/diff"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
"github.com/spf13/cobra"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/clientcmd"

View File

@@ -1,4 +1,4 @@
package commands
package main
import (
"testing"

View File

@@ -5,26 +5,22 @@ import (
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"text/tabwriter"
"time"
"syscall"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
"github.com/argoproj/gitops-engine/pkg/utils/io"
timeutil "github.com/argoproj/pkg/time"
"github.com/ghodss/yaml"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal"
"github.com/argoproj/argo-cd/errors"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
accountpkg "github.com/argoproj/argo-cd/pkg/apiclient/account"
"github.com/argoproj/argo-cd/pkg/apiclient/session"
"github.com/argoproj/argo-cd/server/rbacpolicy"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/cli"
"github.com/argoproj/argo-cd/util/localconfig"
sessionutil "github.com/argoproj/argo-cd/util/session"
)
func NewAccountCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
@@ -39,16 +35,11 @@ func NewAccountCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
command.AddCommand(NewAccountUpdatePasswordCommand(clientOpts))
command.AddCommand(NewAccountGetUserInfoCommand(clientOpts))
command.AddCommand(NewAccountCanICommand(clientOpts))
command.AddCommand(NewAccountListCommand(clientOpts))
command.AddCommand(NewAccountGenerateTokenCommand(clientOpts))
command.AddCommand(NewAccountGetCommand(clientOpts))
command.AddCommand(NewAccountDeleteTokenCommand(clientOpts))
return command
}
func NewAccountUpdatePasswordCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
account string
currentPassword string
newPassword string
)
@@ -60,20 +51,14 @@ func NewAccountUpdatePasswordCommand(clientOpts *argocdclient.ClientOptions) *co
c.HelpFunc()(c, args)
os.Exit(1)
}
acdClient := argocdclient.NewClientOrDie(clientOpts)
conn, usrIf := acdClient.NewAccountClientOrDie()
defer io.Close(conn)
userInfo := getCurrentAccount(acdClient)
if userInfo.Iss == sessionutil.SessionManagerClaimsIssuer && currentPassword == "" {
if currentPassword == "" {
fmt.Print("*** Enter current password: ")
password, err := terminal.ReadPassword(int(os.Stdin.Fd()))
password, err := terminal.ReadPassword(syscall.Stdin)
errors.CheckError(err)
currentPassword = string(password)
fmt.Print("\n")
}
if newPassword == "" {
var err error
newPassword, err = cli.ReadAndConfirmPassword()
@@ -83,37 +68,37 @@ func NewAccountUpdatePasswordCommand(clientOpts *argocdclient.ClientOptions) *co
updatePasswordRequest := accountpkg.UpdatePasswordRequest{
NewPassword: newPassword,
CurrentPassword: currentPassword,
Name: account,
}
acdClient := argocdclient.NewClientOrDie(clientOpts)
conn, usrIf := acdClient.NewAccountClientOrDie()
defer util.Close(conn)
ctx := context.Background()
_, err := usrIf.UpdatePassword(ctx, &updatePasswordRequest)
errors.CheckError(err)
fmt.Printf("Password updated\n")
if account == "" || account == userInfo.Username {
// Get a new JWT token after updating the password
localCfg, err := localconfig.ReadLocalConfig(clientOpts.ConfigPath)
errors.CheckError(err)
configCtx, err := localCfg.ResolveContext(clientOpts.Context)
errors.CheckError(err)
claims, err := configCtx.User.Claims()
errors.CheckError(err)
tokenString := passwordLogin(acdClient, claims.Subject, newPassword)
localCfg.UpsertUser(localconfig.User{
Name: localCfg.CurrentContext,
AuthToken: tokenString,
})
err = localconfig.WriteLocalConfig(*localCfg, clientOpts.ConfigPath)
errors.CheckError(err)
fmt.Printf("Context '%s' updated\n", localCfg.CurrentContext)
}
// Get a new JWT token after updating the password
localCfg, err := localconfig.ReadLocalConfig(clientOpts.ConfigPath)
errors.CheckError(err)
configCtx, err := localCfg.ResolveContext(clientOpts.Context)
errors.CheckError(err)
claims, err := configCtx.User.Claims()
errors.CheckError(err)
tokenString := passwordLogin(acdClient, claims.Subject, newPassword)
localCfg.UpsertUser(localconfig.User{
Name: localCfg.CurrentContext,
AuthToken: tokenString,
})
err = localconfig.WriteLocalConfig(*localCfg, clientOpts.ConfigPath)
errors.CheckError(err)
fmt.Printf("Context '%s' updated\n", localCfg.CurrentContext)
},
}
command.Flags().StringVar(&currentPassword, "current-password", "", "current password you wish to change")
command.Flags().StringVar(&newPassword, "new-password", "", "new password you want to update to")
command.Flags().StringVar(&account, "account", "", "an account name that should be updated. Defaults to current user account")
return command
}
@@ -131,7 +116,7 @@ func NewAccountGetUserInfoCommand(clientOpts *argocdclient.ClientOptions) *cobra
}
conn, client := argocdclient.NewClientOrDie(clientOpts).NewSessionClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
ctx := context.Background()
response, err := client.GetUserInfo(ctx, &session.GetUserInfoRequest{})
@@ -174,11 +159,11 @@ argocd account can-i sync applications '*'
argocd account can-i update projects 'default'
# Can I create a cluster?
argocd account can-i create clusters '*'
argocd account can-i create cluster '*'
Actions: %v
Resources: %v
`, rbacpolicy.Actions, rbacpolicy.Resources),
`, rbacpolicy.Resources, rbacpolicy.Actions),
Run: func(c *cobra.Command, args []string) {
if len(args) != 3 {
c.HelpFunc()(c, args)
@@ -186,7 +171,7 @@ Resources: %v
}
conn, client := argocdclient.NewClientOrDie(clientOpts).NewAccountClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
ctx := context.Background()
response, err := client.CanI(ctx, &accountpkg.CanIRequest{
@@ -199,202 +184,3 @@ Resources: %v
},
}
}
func printAccountNames(accounts []*accountpkg.Account) {
for _, p := range accounts {
fmt.Println(p.Name)
}
}
func printAccountsTable(items []*accountpkg.Account) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "NAME\tENABLED\tCAPABILITIES\n")
for _, a := range items {
fmt.Fprintf(w, "%s\t%v\t%s\n", a.Name, a.Enabled, strings.Join(a.Capabilities, ", "))
}
_ = w.Flush()
}
func NewAccountListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
output string
)
cmd := &cobra.Command{
Use: "list",
Short: "List accounts",
Example: "argocd account list",
Run: func(c *cobra.Command, args []string) {
conn, client := argocdclient.NewClientOrDie(clientOpts).NewAccountClientOrDie()
defer io.Close(conn)
ctx := context.Background()
response, err := client.ListAccounts(ctx, &accountpkg.ListAccountRequest{})
errors.CheckError(err)
switch output {
case "yaml", "json":
err := PrintResourceList(response.Items, output, false)
errors.CheckError(err)
case "name":
printAccountNames(response.Items)
case "wide", "":
printAccountsTable(response.Items)
default:
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
}
},
}
cmd.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|name")
return cmd
}
func getCurrentAccount(clientset argocdclient.Client) session.GetUserInfoResponse {
conn, client := clientset.NewSessionClientOrDie()
defer io.Close(conn)
userInfo, err := client.GetUserInfo(context.Background(), &session.GetUserInfoRequest{})
errors.CheckError(err)
return *userInfo
}
func NewAccountGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
output string
account string
)
cmd := &cobra.Command{
Use: "get",
Short: "Get account details",
Example: `# Get the currently logged in account details
argocd account get
# Get details for an account by name
argocd account get --account <account-name>`,
Run: func(c *cobra.Command, args []string) {
clientset := argocdclient.NewClientOrDie(clientOpts)
if account == "" {
account = getCurrentAccount(clientset).Username
}
conn, client := clientset.NewAccountClientOrDie()
defer io.Close(conn)
acc, err := client.GetAccount(context.Background(), &accountpkg.GetAccountRequest{Name: account})
errors.CheckError(err)
switch output {
case "yaml", "json":
err := PrintResourceList(acc, output, true)
errors.CheckError(err)
case "name":
fmt.Println(acc.Name)
case "wide", "":
printAccountDetails(acc)
default:
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
}
},
}
cmd.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|name")
cmd.Flags().StringVarP(&account, "account", "a", "", "Account name. Defaults to the current account.")
return cmd
}
func printAccountDetails(acc *accountpkg.Account) {
fmt.Printf(printOpFmtStr, "Name:", acc.Name)
fmt.Printf(printOpFmtStr, "Enabled:", strconv.FormatBool(acc.Enabled))
fmt.Printf(printOpFmtStr, "Capabilities:", strings.Join(acc.Capabilities, ", "))
fmt.Println("\nTokens:")
if len(acc.Tokens) == 0 {
fmt.Println("NONE")
} else {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "ID\tISSUED AT\tEXPIRING AT\n")
for _, t := range acc.Tokens {
expiresAtFormatted := "never"
if t.ExpiresAt > 0 {
expiresAt := time.Unix(t.ExpiresAt, 0)
expiresAtFormatted = expiresAt.Format(time.RFC3339)
if expiresAt.Before(time.Now()) {
expiresAtFormatted = fmt.Sprintf("%s (expired)", expiresAtFormatted)
}
}
fmt.Fprintf(w, "%s\t%s\t%s\n", t.Id, time.Unix(t.IssuedAt, 0).Format(time.RFC3339), expiresAtFormatted)
}
_ = w.Flush()
}
}
func NewAccountGenerateTokenCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
account string
expiresIn string
id string
)
cmd := &cobra.Command{
Use: "generate-token",
Short: "Generate account token",
Example: `# Generate token for the currently logged in account
argocd account generate-token
# Generate token for the account with the specified name
argocd account generate-token --account <account-name>`,
Run: func(c *cobra.Command, args []string) {
clientset := argocdclient.NewClientOrDie(clientOpts)
conn, client := clientset.NewAccountClientOrDie()
defer io.Close(conn)
if account == "" {
account = getCurrentAccount(clientset).Username
}
expiresIn, err := timeutil.ParseDuration(expiresIn)
errors.CheckError(err)
response, err := client.CreateToken(context.Background(), &accountpkg.CreateTokenRequest{
Name: account,
ExpiresIn: int64(expiresIn.Seconds()),
Id: id,
})
errors.CheckError(err)
fmt.Println(response.Token)
},
}
cmd.Flags().StringVarP(&account, "account", "a", "", "Account name. Defaults to the current account.")
cmd.Flags().StringVarP(&expiresIn, "expires-in", "e", "0s", "Duration before the token will expire. (Default: No expiration)")
cmd.Flags().StringVar(&id, "id", "", "Optional token id. Fallback to uuid if not value specified.")
return cmd
}
func NewAccountDeleteTokenCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
account string
)
cmd := &cobra.Command{
Use: "delete-token",
Short: "Deletes account token",
Example: `# Delete token of the currently logged in account
argocd account delete-token ID
# Delete token of the account with the specified name
argocd account generate-token --account <account-name>`,
Run: func(c *cobra.Command, args []string) {
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
id := args[0]
clientset := argocdclient.NewClientOrDie(clientOpts)
conn, client := clientset.NewAccountClientOrDie()
defer io.Close(conn)
if account == "" {
account = getCurrentAccount(clientset).Username
}
_, err := client.DeleteToken(context.Background(), &accountpkg.DeleteTokenRequest{Name: account, Id: id})
errors.CheckError(err)
},
}
cmd.Flags().StringVarP(&account, "account", "a", "", "Account name. Defaults to the current account.")
return cmd
}

View File

@@ -6,7 +6,6 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/url"
"os"
"reflect"
@@ -16,13 +15,6 @@ import (
"text/tabwriter"
"time"
"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"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
argoio "github.com/argoproj/gitops-engine/pkg/utils/io"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
"github.com/ghodss/yaml"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
@@ -34,6 +26,7 @@ import (
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/controller"
"github.com/argoproj/argo-cd/errors"
"github.com/argoproj/argo-cd/pkg/apiclient"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
applicationpkg "github.com/argoproj/argo-cd/pkg/apiclient/application"
@@ -43,11 +36,15 @@ import (
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
repoapiclient "github.com/argoproj/argo-cd/reposerver/apiclient"
"github.com/argoproj/argo-cd/reposerver/repository"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/argo"
"github.com/argoproj/argo-cd/util/cli"
"github.com/argoproj/argo-cd/util/config"
"github.com/argoproj/argo-cd/util/diff"
"github.com/argoproj/argo-cd/util/git"
argokube "github.com/argoproj/argo-cd/util/kube"
"github.com/argoproj/argo-cd/util/hook"
"github.com/argoproj/argo-cd/util/kube"
"github.com/argoproj/argo-cd/util/resource/ignore"
"github.com/argoproj/argo-cd/util/templates"
"github.com/argoproj/argo-cd/util/text/label"
)
@@ -174,11 +171,10 @@ func NewApplicationCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.
}
conn, appIf := argocdClient.NewApplicationClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
appCreateRequest := applicationpkg.ApplicationCreateRequest{
Application: app,
Upsert: &upsert,
Validate: &appOpts.validate,
}
created, err := appIf.Create(context.Background(), &appCreateRequest)
errors.CheckError(err)
@@ -204,18 +200,6 @@ func setLabels(app *argoappv1.Application, labels []string) {
app.SetLabels(mapLabels)
}
func getInfos(infos []string) []*argoappv1.Info {
mapInfos, err := label.Parse(infos)
errors.CheckError(err)
sliceInfos := make([]*argoappv1.Info, len(mapInfos))
i := 0
for key, element := range mapInfos {
sliceInfos[i] = &argoappv1.Info{Name: key, Value: element}
i++
}
return sliceInfos
}
func getRefreshType(refresh bool, hardRefresh bool) *string {
if hardRefresh {
refreshType := string(argoappv1.RefreshTypeHard)
@@ -249,13 +233,13 @@ func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
}
acdClient := argocdclient.NewClientOrDie(clientOpts)
conn, appIf := acdClient.NewApplicationClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
appName := args[0]
app, err := appIf.Get(context.Background(), &applicationpkg.ApplicationQuery{Name: &appName, Refresh: getRefreshType(refresh, hardRefresh)})
errors.CheckError(err)
pConn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer argoio.Close(pConn)
defer util.Close(pConn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: app.Spec.Project})
errors.CheckError(err)
@@ -374,7 +358,7 @@ func printAppSummaryTable(app *argoappv1.Application, appURL string, windows *ar
syncStatusStr += fmt.Sprintf(" (%s)", app.Status.Sync.Revision[0:7])
}
fmt.Printf(printOpFmtStr, "Sync Status:", syncStatusStr)
healthStr := string(app.Status.Health.Status)
healthStr := app.Status.Health.Status
if app.Status.Health.Message != "" {
healthStr = fmt.Sprintf("%s (%s)", app.Status.Health.Status, app.Status.Health.Message)
}
@@ -465,7 +449,7 @@ func NewApplicationSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
appName := args[0]
argocdClient := argocdclient.NewClientOrDie(clientOpts)
conn, appIf := argocdClient.NewApplicationClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
app, err := appIf.Get(ctx, &applicationpkg.ApplicationQuery{Name: &appName})
errors.CheckError(err)
visited := setAppSpecOptions(c.Flags(), &app.Spec, &appOpts)
@@ -476,9 +460,8 @@ func NewApplicationSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
}
setParameterOverrides(app, appOpts.parameters)
_, err = appIf.UpdateSpec(ctx, &applicationpkg.ApplicationUpdateSpecRequest{
Name: &app.Name,
Spec: app.Spec,
Validate: &appOpts.validate,
Name: &app.Name,
Spec: app.Spec,
})
errors.CheckError(err)
},
@@ -507,26 +490,12 @@ func setAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, ap
spec.RevisionHistoryLimit = &i
case "values":
setHelmOpt(&spec.Source, helmOpts{valueFiles: appOpts.valuesFiles})
case "values-literal-file":
var data []byte
// read uri
parsedURL, err := url.ParseRequestURI(appOpts.values)
if err != nil || !(parsedURL.Scheme == "http" || parsedURL.Scheme == "https") {
data, err = ioutil.ReadFile(appOpts.values)
} else {
data, err = config.ReadRemoteFile(appOpts.values)
}
errors.CheckError(err)
setHelmOpt(&spec.Source, helmOpts{values: string(data)})
case "release-name":
setHelmOpt(&spec.Source, helmOpts{releaseName: appOpts.releaseName})
case "helm-set":
setHelmOpt(&spec.Source, helmOpts{helmSets: appOpts.helmSets})
case "helm-set-string":
setHelmOpt(&spec.Source, helmOpts{helmSetStrings: appOpts.helmSetStrings})
case "helm-set-file":
setHelmOpt(&spec.Source, helmOpts{helmSetFiles: appOpts.helmSetFiles})
case "directory-recurse":
spec.Source.Directory = &argoappv1.ApplicationSourceDirectory{Recurse: appOpts.directoryRecurse}
case "config-management-plugin":
@@ -543,8 +512,6 @@ func setAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, ap
setKustomizeOpt(&spec.Source, kustomizeOpts{nameSuffix: appOpts.nameSuffix})
case "kustomize-image":
setKustomizeOpt(&spec.Source, kustomizeOpts{images: appOpts.kustomizeImages})
case "kustomize-version":
setKustomizeOpt(&spec.Source, kustomizeOpts{version: appOpts.kustomizeVersion})
case "jsonnet-tla-str":
setJsonnetOpt(&spec.Source, appOpts.jsonnetTlaStr, false)
case "jsonnet-tla-code":
@@ -556,36 +523,14 @@ func setAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, ap
case "sync-policy":
switch appOpts.syncPolicy {
case "automated":
if spec.SyncPolicy == nil {
spec.SyncPolicy = &argoappv1.SyncPolicy{}
spec.SyncPolicy = &argoappv1.SyncPolicy{
Automated: &argoappv1.SyncPolicyAutomated{},
}
spec.SyncPolicy.Automated = &argoappv1.SyncPolicyAutomated{}
case "none":
if spec.SyncPolicy != nil {
spec.SyncPolicy.Automated = nil
}
if spec.SyncPolicy.IsZero() {
spec.SyncPolicy = nil
}
spec.SyncPolicy = nil
default:
log.Fatalf("Invalid sync-policy: %s", appOpts.syncPolicy)
}
case "sync-option":
if spec.SyncPolicy == nil {
spec.SyncPolicy = &argoappv1.SyncPolicy{}
}
for _, option := range appOpts.syncOptions {
// `!` means remove the option
if strings.HasPrefix(option, "!") {
option = strings.TrimPrefix(option, "!")
spec.SyncPolicy.SyncOptions = spec.SyncPolicy.SyncOptions.RemoveOption(option)
} else {
spec.SyncPolicy.SyncOptions = spec.SyncPolicy.SyncOptions.AddOption(option)
}
}
if spec.SyncPolicy.IsZero() {
spec.SyncPolicy = nil
}
}
})
if flags.Changed("auto-prune") {
@@ -596,7 +541,7 @@ func setAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, ap
}
if flags.Changed("self-heal") {
if spec.SyncPolicy == nil || spec.SyncPolicy.Automated == nil {
log.Fatal("Cannot set --self-heal: application not configured with automatic sync")
log.Fatal("Cannot set --self-helf: application not configured with automatic sync")
}
spec.SyncPolicy.Automated.SelfHeal = appOpts.selfHeal
}
@@ -620,14 +565,12 @@ type kustomizeOpts struct {
namePrefix string
nameSuffix string
images []string
version string
}
func setKustomizeOpt(src *argoappv1.ApplicationSource, opts kustomizeOpts) {
if src.Kustomize == nil {
src.Kustomize = &argoappv1.ApplicationSourceKustomize{}
}
src.Kustomize.Version = opts.version
src.Kustomize.NamePrefix = opts.namePrefix
src.Kustomize.NameSuffix = opts.nameSuffix
for _, image := range opts.images {
@@ -640,11 +583,9 @@ func setKustomizeOpt(src *argoappv1.ApplicationSource, opts kustomizeOpts) {
type helmOpts struct {
valueFiles []string
values string
releaseName string
helmSets []string
helmSetStrings []string
helmSetFiles []string
}
func setHelmOpt(src *argoappv1.ApplicationSource, opts helmOpts) {
@@ -654,9 +595,6 @@ func setHelmOpt(src *argoappv1.ApplicationSource, opts helmOpts) {
if len(opts.valueFiles) > 0 {
src.Helm.ValueFiles = opts.valueFiles
}
if len(opts.values) > 0 {
src.Helm.Values = opts.values
}
if opts.releaseName != "" {
src.Helm.ReleaseName = opts.releaseName
}
@@ -674,13 +612,6 @@ func setHelmOpt(src *argoappv1.ApplicationSource, opts helmOpts) {
}
src.Helm.AddParameter(*p)
}
for _, text := range opts.helmSetFiles {
p, err := argoappv1.NewHelmFileParameter(text)
if err != nil {
log.Fatal(err)
}
src.Helm.AddFileParameter(*p)
}
if src.Helm.IsZero() {
src.Helm = nil
}
@@ -690,8 +621,31 @@ func setJsonnetOpt(src *argoappv1.ApplicationSource, tlaParameters []string, cod
if src.Directory == nil {
src.Directory = &argoappv1.ApplicationSourceDirectory{}
}
for _, j := range tlaParameters {
src.Directory.Jsonnet.TLAs = append(src.Directory.Jsonnet.TLAs, argoappv1.NewJsonnetVar(j, code))
if len(tlaParameters) != 0 {
tlas := make([]argoappv1.JsonnetVar, len(tlaParameters))
for index, paramStr := range tlaParameters {
parts := strings.SplitN(paramStr, "=", 2)
if len(parts) != 2 {
log.Fatalf("Expected parameter of the form: param=value. Received: %s", paramStr)
break
}
tlas[index] = argoappv1.JsonnetVar{
Name: parts[0],
Value: parts[1],
Code: code}
}
var existingTLAs []argoappv1.JsonnetVar
for i := range src.Directory.Jsonnet.TLAs {
if src.Directory.Jsonnet.TLAs[i].Code != code {
existingTLAs = append(existingTLAs, src.Directory.Jsonnet.TLAs[i])
}
}
src.Directory.Jsonnet.TLAs = append(existingTLAs, tlas...)
}
if src.Directory.IsZero() {
src.Directory = nil
}
}
@@ -715,14 +669,11 @@ type appOptions struct {
destNamespace string
parameters []string
valuesFiles []string
values string
releaseName string
helmSets []string
helmSetStrings []string
helmSetFiles []string
project string
syncPolicy string
syncOptions []string
autoPrune bool
selfHeal bool
namePrefix string
@@ -734,8 +685,6 @@ type appOptions struct {
jsonnetExtVarStr []string
jsonnetExtVarCode []string
kustomizeImages []string
kustomizeVersion string
validate bool
}
func addAppFlags(command *cobra.Command, opts *appOptions) {
@@ -749,19 +698,15 @@ 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().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().StringArrayVar(&opts.helmSets, "helm-set", []string{}, "Helm set values on the command line (can be repeated to set several values: --helm-set key1=val1 --helm-set key2=val2)")
command.Flags().StringArrayVar(&opts.helmSetStrings, "helm-set-string", []string{}, "Helm set STRING values on the command line (can be repeated to set several values: --helm-set-string key1=val1 --helm-set-string key2=val2)")
command.Flags().StringArrayVar(&opts.helmSetFiles, "helm-set-file", []string{}, "Helm set values from respective files specified via the command line (can be repeated to set several values: --helm-set-file key1=path1 --helm-set-file key2=path2)")
command.Flags().StringVar(&opts.project, "project", "", "Application project name")
command.Flags().StringVar(&opts.syncPolicy, "sync-policy", "", "Set the sync policy (one of: automated, none)")
command.Flags().StringArrayVar(&opts.syncOptions, "sync-option", []string{}, "Add or remove a sync options, e.g add `Prune=false`. Remove using `!` prefix, e.g. `!Prune=false`")
command.Flags().BoolVar(&opts.autoPrune, "auto-prune", false, "Set automatic pruning when sync is automated")
command.Flags().BoolVar(&opts.selfHeal, "self-heal", false, "Set self healing when sync is automated")
command.Flags().StringVar(&opts.namePrefix, "nameprefix", "", "Kustomize nameprefix")
command.Flags().StringVar(&opts.nameSuffix, "namesuffix", "", "Kustomize namesuffix")
command.Flags().StringVar(&opts.nameSuffix, "kustomize-version", "", "Kustomize version")
command.Flags().BoolVar(&opts.directoryRecurse, "directory-recurse", false, "Recurse directory")
command.Flags().StringVar(&opts.configManagementPlugin, "config-management-plugin", "", "Config management plugin name")
command.Flags().StringArrayVar(&opts.jsonnetTlaStr, "jsonnet-tla-str", []string{}, "Jsonnet top level string arguments")
@@ -769,80 +714,30 @@ func addAppFlags(command *cobra.Command, opts *appOptions) {
command.Flags().StringArrayVar(&opts.jsonnetExtVarStr, "jsonnet-ext-var-str", []string{}, "Jsonnet string ext var")
command.Flags().StringArrayVar(&opts.jsonnetExtVarCode, "jsonnet-ext-var-code", []string{}, "Jsonnet ext var")
command.Flags().StringArrayVar(&opts.kustomizeImages, "kustomize-image", []string{}, "Kustomize images (e.g. --kustomize-image node:8.15.0 --kustomize-image mysql=mariadb,alpine@sha256:24a0c4b4a4c0eb97a1aabb8e29f18e917d05abfe1b7a7c07857230879ce7d3d)")
command.Flags().BoolVar(&opts.validate, "validate", true, "Validation of repo and cluster")
}
// 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
appOpts appOptions
parameters []string
valuesFiles []string
)
var command = &cobra.Command{
Use: "unset APPNAME parameters",
Use: "unset APPNAME -p COMPONENT=PARAM",
Short: "Unset application parameters",
Example: ` # Unset kustomize override kustomize image
argocd app unset my-app --kustomize-image=alpine
# Unset kustomize override prefix
argocd app unset my-app --namesuffix
# Unset parameter override
argocd app unset my-app -p COMPONENT=PARAM`,
Run: func(c *cobra.Command, args []string) {
if len(args) != 1 {
if len(args) != 1 || (len(parameters) == 0 && len(valuesFiles) == 0) {
c.HelpFunc()(c, args)
os.Exit(1)
}
appName := args[0]
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
app, err := appIf.Get(context.Background(), &applicationpkg.ApplicationQuery{Name: &appName})
errors.CheckError(err)
updated := false
if app.Spec.Source.Kustomize != nil {
if namePrefix {
updated = true
app.Spec.Source.Kustomize.NamePrefix = ""
}
if nameSuffix {
updated = true
app.Spec.Source.Kustomize.NameSuffix = ""
}
if kustomizeVersion {
updated = true
app.Spec.Source.Kustomize.Version = ""
}
for _, kustomizeImage := range kustomizeImages {
for i, item := range app.Spec.Source.Kustomize.Images {
if argoappv1.KustomizeImage(kustomizeImage).Match(item) {
updated = true
//remove i
a := app.Spec.Source.Kustomize.Images
copy(a[i:], a[i+1:]) // Shift a[i+1:] left one index.
a[len(a)-1] = "" // Erase last element (write zero value).
a = a[:len(a)-1] // Truncate slice.
app.Spec.Source.Kustomize.Images = a
}
}
}
}
if app.Spec.Source.Ksonnet != nil {
if len(parameters) == 0 && len(valuesFiles) == 0 {
c.HelpFunc()(c, args)
os.Exit(1)
}
for _, paramStr := range parameters {
parts := strings.SplitN(paramStr, "=", 2)
if len(parts) != 2 {
@@ -859,10 +754,6 @@ func NewApplicationUnsetCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
}
}
if app.Spec.Source.Helm != nil {
if len(parameters) == 0 && len(valuesFiles) == 0 && !valuesLiteral {
c.HelpFunc()(c, args)
os.Exit(1)
}
for _, paramStr := range parameters {
helmParams := app.Spec.Source.Helm.Parameters
for i, p := range helmParams {
@@ -873,41 +764,31 @@ func NewApplicationUnsetCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
}
}
}
if valuesLiteral {
app.Spec.Source.Helm.Values = ""
updated = true
}
specValueFiles := app.Spec.Source.Helm.ValueFiles
for _, valuesFile := range valuesFiles {
specValueFiles := app.Spec.Source.Helm.ValueFiles
for i, vf := range specValueFiles {
if vf == valuesFile {
app.Spec.Source.Helm.ValueFiles = append(specValueFiles[0:i], specValueFiles[i+1:]...)
specValueFiles = append(specValueFiles[0:i], specValueFiles[i+1:]...)
updated = true
break
}
}
}
setHelmOpt(&app.Spec.Source, helmOpts{valueFiles: specValueFiles})
if !updated {
return
}
}
setAppSpecOptions(c.Flags(), &app.Spec, &appOpts)
_, err = appIf.UpdateSpec(context.Background(), &applicationpkg.ApplicationUpdateSpecRequest{
Name: &app.Name,
Spec: app.Spec,
Validate: &appOpts.validate,
Name: &app.Name,
Spec: app.Spec,
})
errors.CheckError(err)
},
}
command.Flags().StringArrayVarP(&parameters, "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(&nameSuffix, "namesuffix", false, "Kustomize namesuffix")
command.Flags().BoolVar(&namePrefix, "nameprefix", false, "Kustomize nameprefix")
command.Flags().BoolVar(&kustomizeVersion, "kustomize-version", false, "Kustomize version")
command.Flags().StringArrayVar(&kustomizeImages, "kustomize-image", []string{}, "Kustomize images name (e.g. --kustomize-image node --kustomize-image mysql)")
command.Flags().StringArrayVarP(&parameters, "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")
return command
}
@@ -937,9 +818,8 @@ func liveObjects(resources []*argoappv1.ResourceDiff) ([]*unstructured.Unstructu
return objs, nil
}
func getLocalObjects(app *argoappv1.Application, local, appLabelKey, kubeVersion string, kustomizeOptions *argoappv1.KustomizeOptions,
configManagementPlugins []*argoappv1.ConfigManagementPlugin) []*unstructured.Unstructured {
manifestStrings := getLocalObjectsString(app, local, appLabelKey, kubeVersion, kustomizeOptions, configManagementPlugins)
func getLocalObjects(app *argoappv1.Application, local, appLabelKey, kubeVersion string, kustomizeOptions *argoappv1.KustomizeOptions) []*unstructured.Unstructured {
manifestStrings := getLocalObjectsString(app, local, appLabelKey, kubeVersion, kustomizeOptions)
objs := make([]*unstructured.Unstructured, len(manifestStrings))
for i := range manifestStrings {
obj := unstructured.Unstructured{}
@@ -950,8 +830,7 @@ func getLocalObjects(app *argoappv1.Application, local, appLabelKey, kubeVersion
return objs
}
func getLocalObjectsString(app *argoappv1.Application, local, appLabelKey, kubeVersion string, kustomizeOptions *argoappv1.KustomizeOptions,
configManagementPlugins []*argoappv1.ConfigManagementPlugin) []string {
func getLocalObjectsString(app *argoappv1.Application, local, appLabelKey, kubeVersion string, kustomizeOptions *argoappv1.KustomizeOptions) []string {
res, err := repository.GenerateManifests(local, "/", app.Spec.Source.TargetRevision, &repoapiclient.ManifestRequest{
Repo: &argoappv1.Repository{Repo: app.Spec.Source.RepoURL},
AppLabelKey: appLabelKey,
@@ -960,8 +839,7 @@ func getLocalObjectsString(app *argoappv1.Application, local, appLabelKey, kubeV
ApplicationSource: &app.Spec.Source,
KustomizeOptions: kustomizeOptions,
KubeVersion: kubeVersion,
Plugins: configManagementPlugins,
}, true)
})
errors.CheckError(err)
return res.Manifests
@@ -973,7 +851,7 @@ type resourceInfoProvider struct {
// Infer if obj is namespaced or not from corresponding live objects list. If corresponding live object has namespace then target object is also namespaced.
// If live object is missing then it does not matter if target is namespaced or not.
func (p *resourceInfoProvider) IsNamespaced(gk schema.GroupKind) (bool, error) {
func (p *resourceInfoProvider) IsNamespaced(server string, gk schema.GroupKind) (bool, error) {
return p.namespacedByGk[gk], nil
}
@@ -985,7 +863,7 @@ func groupLocalObjs(localObs []*unstructured.Unstructured, liveObjs []*unstructu
namespacedByGk[schema.GroupKind{Group: key.Group, Kind: key.Kind}] = key.Namespace != ""
}
}
localObs, _, err := controller.DeduplicateTargetObjects(appNamespace, localObs, &resourceInfoProvider{namespacedByGk: namespacedByGk})
localObs, _, err := controller.DeduplicateTargetObjects("", appNamespace, localObs, &resourceInfoProvider{namespacedByGk: namespacedByGk})
errors.CheckError(err)
objByKey := make(map[kube.ResourceKey]*unstructured.Unstructured)
for i := range localObs {
@@ -1017,7 +895,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
clientset := argocdclient.NewClientOrDie(clientOpts)
conn, appIf := clientset.NewApplicationClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
appName := args[0]
app, err := appIf.Get(context.Background(), &applicationpkg.ApplicationQuery{Name: &appName, Refresh: getRefreshType(refresh, hardRefresh)})
errors.CheckError(err)
@@ -1032,16 +910,16 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
}, 0)
conn, settingsIf := clientset.NewSettingsClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
argoSettings, err := settingsIf.Get(context.Background(), &settingspkg.SettingsQuery{})
errors.CheckError(err)
if local != "" {
conn, clusterIf := clientset.NewClusterClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
cluster, err := clusterIf.Get(context.Background(), &clusterpkg.ClusterQuery{Server: app.Spec.Destination.Server})
errors.CheckError(err)
localObjs := groupLocalObjs(getLocalObjects(app, local, argoSettings.AppLabelKey, cluster.ServerVersion, argoSettings.KustomizeOptions, argoSettings.ConfigManagementPlugins), liveObjs, app.Spec.Destination.Namespace)
localObjs := groupLocalObjs(getLocalObjects(app, local, argoSettings.AppLabelKey, cluster.ServerVersion, argoSettings.KustomizeOptions), liveObjs, app.Spec.Destination.Namespace)
for _, res := range resources.Items {
var live = &unstructured.Unstructured{}
err := json.Unmarshal([]byte(res.NormalizedLiveState), &live)
@@ -1055,7 +933,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
}
if local, ok := localObjs[key]; ok || live != nil {
if local != nil && !kube.IsCRD(local) {
err = argokube.SetAppInstanceLabel(local, argoSettings.AppLabelKey, appName)
err = kube.SetAppInstanceLabel(local, argoSettings.AppLabelKey, appName)
errors.CheckError(err)
}
@@ -1118,7 +996,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
normalizer, err := argo.NewDiffNormalizer(app.Spec.IgnoreDifferences, overrides)
errors.CheckError(err)
diffRes, err := diff.Diff(item.target, item.live, normalizer, diff.GetDefaultDiffOptions())
diffRes, err := diff.Diff(item.target, item.live, normalizer)
errors.CheckError(err)
if diffRes.Modified || item.target == nil || item.live == nil {
@@ -1126,9 +1004,9 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
var live *unstructured.Unstructured
var target *unstructured.Unstructured
if item.target != nil && item.live != nil {
target = &unstructured.Unstructured{}
live = item.live
err = json.Unmarshal(diffRes.PredictedLive, target)
target = item.live
live = &unstructured.Unstructured{}
err = json.Unmarshal(diffRes.PredictedLive, live)
errors.CheckError(err)
} else {
live = item.live
@@ -1136,7 +1014,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
}
foundDiffs = true
_ = diff.PrintDiff(item.key.Name, live, target)
_ = diff.PrintDiff(item.key.Name, target, live)
}
}
if foundDiffs {
@@ -1165,7 +1043,7 @@ func NewApplicationDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.
os.Exit(1)
}
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
for _, appName := range args {
appDeleteReq := applicationpkg.ApplicationDeleteRequest{
Name: &appName,
@@ -1237,7 +1115,7 @@ func NewApplicationListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
argocd app list -l app.kubernetes.io/instance=my-app`,
Run: func(c *cobra.Command, args []string) {
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
apps, err := appIf.List(context.Background(), &applicationpkg.ApplicationQuery{Selector: selector})
errors.CheckError(err)
appList := apps.Items
@@ -1361,7 +1239,7 @@ func NewApplicationWaitCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
appNames := args
acdClient := argocdclient.NewClientOrDie(clientOpts)
closer, appIf := acdClient.NewApplicationClientOrDie()
defer argoio.Close(closer)
defer util.Close(closer)
if selector != "" {
list, err := appIf.List(context.Background(), &applicationpkg.ApplicationQuery{Selector: selector})
errors.CheckError(err)
@@ -1407,7 +1285,6 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
force bool
async bool
local string
infos []string
)
var command = &cobra.Command{
Use: "sync [APPNAME... | -l selector]",
@@ -1432,7 +1309,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
}
acdClient := argocdclient.NewClientOrDie(clientOpts)
conn, appIf := acdClient.NewApplicationClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
selectedLabels, err := label.Parse(labels)
errors.CheckError(err)
@@ -1498,14 +1375,14 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
conn, settingsIf := acdClient.NewSettingsClientOrDie()
argoSettings, err := settingsIf.Get(context.Background(), &settingspkg.SettingsQuery{})
errors.CheckError(err)
argoio.Close(conn)
util.Close(conn)
conn, clusterIf := acdClient.NewClusterClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
cluster, err := clusterIf.Get(context.Background(), &clusterpkg.ClusterQuery{Server: app.Spec.Destination.Server})
errors.CheckError(err)
argoio.Close(conn)
localObjsStrings = getLocalObjectsString(app, local, argoSettings.AppLabelKey, cluster.ServerVersion, argoSettings.KustomizeOptions, argoSettings.ConfigManagementPlugins)
util.Close(conn)
localObjsStrings = getLocalObjectsString(app, local, argoSettings.AppLabelKey, cluster.ServerVersion, argoSettings.KustomizeOptions)
}
syncReq := applicationpkg.ApplicationSyncRequest{
@@ -1515,7 +1392,6 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
Resources: selectedResources,
Prune: prune,
Manifests: localObjsStrings,
Infos: getInfos(infos),
}
switch strategy {
case "apply":
@@ -1535,15 +1411,15 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
app, err := waitOnApplicationStatus(acdClient, appName, timeout, false, false, true, false, selectedResources)
errors.CheckError(err)
if !dryRun {
if !app.Status.OperationState.Phase.Successful() {
log.Fatalf("Operation has completed with phase: %s", app.Status.OperationState.Phase)
} else if len(selectedResources) == 0 && app.Status.Sync.Status != argoappv1.SyncStatusCodeSynced {
// Only get resources to be pruned if sync was application-wide and final status is not synced
pruningRequired := app.Status.OperationState.SyncResult.Resources.PruningRequired()
if pruningRequired > 0 {
log.Fatalf("%d resources require pruning", pruningRequired)
}
// Only get resources to be pruned if sync was application-wide
if len(selectedResources) == 0 {
pruningRequired := app.Status.OperationState.SyncResult.Resources.PruningRequired()
if pruningRequired > 0 {
log.Fatalf("%d resources require pruning", pruningRequired)
}
if !app.Status.OperationState.Phase.Successful() && !dryRun {
os.Exit(1)
}
}
}
@@ -1555,13 +1431,12 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
command.Flags().StringVar(&revision, "revision", "", "Sync to a specific revision. Preserves parameter overrides")
command.Flags().StringArrayVar(&resources, "resource", []string{}, fmt.Sprintf("Sync only specific resources as GROUP%sKIND%sNAME. Fields may be blank. This option may be specified repeatedly", resourceFieldDelimiter, resourceFieldDelimiter))
command.Flags().StringVarP(&selector, "selector", "l", "", "Sync apps that match this label")
command.Flags().StringArrayVar(&labels, "label", []string{}, "Sync only specific resources with a label. This option may be specified repeatedly.")
command.Flags().StringArrayVar(&labels, "label", []string{}, fmt.Sprintf("Sync only specific resources with a label. This option may be specified repeatedly."))
command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds")
command.Flags().StringVar(&strategy, "strategy", "", "Sync strategy (one of: apply|hook)")
command.Flags().BoolVar(&force, "force", false, "Use a force apply")
command.Flags().BoolVar(&async, "async", false, "Do not wait for application to sync before continuing")
command.Flags().StringVar(&local, "local", "", "Path to a local directory. When this flag is present no git queries will be made")
command.Flags().StringArrayVar(&infos, "info", []string{}, "A list of key-value pairs during sync process. These infos will be persisted in app.")
return command
}
@@ -1622,7 +1497,7 @@ func getResourceStates(app *argoappv1.Application, selectedResources []argoappv1
if resource, ok := resourceByKey[key]; ok && res.HookType == "" {
health = ""
if resource.Health != nil {
health = string(resource.Health.Status)
health = resource.Health.Status
}
sync = string(resource.Status)
}
@@ -1643,7 +1518,7 @@ func getResourceStates(app *argoappv1.Application, selectedResources []argoappv1
res := resourceByKey[resKey]
health := ""
if res.Health != nil {
health = string(res.Health.Status)
health = res.Health.Status
}
states = append(states, &resourceState{
Group: res.Group, Kind: res.Kind, Namespace: res.Namespace, Name: res.Name, Status: string(res.Status), Health: health, Hook: "", Message: ""})
@@ -1676,12 +1551,12 @@ func groupResourceStates(app *argoappv1.Application, selectedResources []argoapp
func checkResourceStatus(watchSync bool, watchHealth bool, watchOperation bool, watchSuspended bool, healthStatus string, syncStatus string, operationStatus *argoappv1.Operation) bool {
healthCheckPassed := true
if watchSuspended && watchHealth {
healthCheckPassed = healthStatus == string(health.HealthStatusHealthy) ||
healthStatus == string(health.HealthStatusSuspended)
healthCheckPassed = healthStatus == argoappv1.HealthStatusHealthy ||
healthStatus == argoappv1.HealthStatusSuspended
} else if watchSuspended {
healthCheckPassed = healthStatus == string(health.HealthStatusSuspended)
healthCheckPassed = healthStatus == argoappv1.HealthStatusSuspended
} else if watchHealth {
healthCheckPassed = healthStatus == string(health.HealthStatusHealthy)
healthCheckPassed = healthStatus == argoappv1.HealthStatusHealthy
}
synced := !watchSync || syncStatus == string(argoappv1.SyncStatusCodeSynced)
@@ -1700,7 +1575,7 @@ func waitOnApplicationStatus(acdClient apiclient.Client, appName string, timeout
// time when the sync status lags behind when an operation completes
refresh := false
printFinalStatus := func(app *argoappv1.Application) *argoappv1.Application {
printFinalStatus := func(app *argoappv1.Application) {
var err error
if refresh {
conn, appClient := acdClient.NewApplicationClientOrDie()
@@ -1723,7 +1598,6 @@ func waitOnApplicationStatus(acdClient apiclient.Client, appName string, timeout
printAppResources(w, app)
_ = w.Flush()
}
return app
}
if timeout != 0 {
@@ -1738,27 +1612,14 @@ func waitOnApplicationStatus(acdClient apiclient.Client, appName string, timeout
prevStates := make(map[string]*resourceState)
appEventCh := acdClient.WatchApplicationWithRetry(ctx, appName)
conn, appClient := acdClient.NewApplicationClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
app, err := appClient.Get(ctx, &applicationpkg.ApplicationQuery{Name: &appName})
errors.CheckError(err)
for appEvent := range appEventCh {
app = &appEvent.Application
operationInProgress := false
// consider the operation is in progress
if app.Operation != nil {
// if it just got requested
operationInProgress = true
refresh = true
} else if app.Status.OperationState != nil {
if app.Status.OperationState.FinishedAt == nil {
// if it is not finished yet
operationInProgress = true
} else if app.Status.ReconciledAt == nil || app.Status.ReconciledAt.Before(app.Status.OperationState.FinishedAt) {
// if it is just finished and we need to wait for controller to reconcile app once after syncing
operationInProgress = true
}
}
var selectedResourcesAreReady bool
@@ -1775,11 +1636,11 @@ func waitOnApplicationStatus(acdClient apiclient.Client, appName string, timeout
}
} else {
// Wait on the application as a whole
selectedResourcesAreReady = checkResourceStatus(watchSync, watchHealth, watchOperation, watchSuspended, string(app.Status.Health.Status), string(app.Status.Sync.Status), appEvent.Application.Operation)
selectedResourcesAreReady = checkResourceStatus(watchSync, watchHealth, watchOperation, watchSuspended, app.Status.Health.Status, string(app.Status.Sync.Status), appEvent.Application.Operation)
}
if selectedResourcesAreReady && !operationInProgress {
app = printFinalStatus(app)
if selectedResourcesAreReady {
printFinalStatus(app)
return app, nil
}
@@ -1788,8 +1649,8 @@ func waitOnApplicationStatus(acdClient apiclient.Client, appName string, timeout
var doPrint bool
stateKey := newState.Key()
if prevState, found := prevStates[stateKey]; found {
if watchHealth && prevState.Health != string(health.HealthStatusUnknown) && prevState.Health != string(health.HealthStatusDegraded) && newState.Health == string(health.HealthStatusDegraded) {
_ = printFinalStatus(app)
if watchHealth && prevState.Health != argoappv1.HealthStatusUnknown && prevState.Health != argoappv1.HealthStatusDegraded && newState.Health == argoappv1.HealthStatusDegraded {
printFinalStatus(app)
return nil, fmt.Errorf("application '%s' health state has transitioned from %s to %s", appName, prevState.Health, newState.Health)
}
doPrint = prevState.Merge(newState)
@@ -1803,7 +1664,7 @@ func waitOnApplicationStatus(acdClient apiclient.Client, appName string, timeout
}
_ = w.Flush()
}
_ = printFinalStatus(app)
printFinalStatus(app)
return nil, fmt.Errorf("timed out (%ds) waiting for app %q match desired state", timeout, appName)
}
@@ -1912,7 +1773,7 @@ func NewApplicationHistoryCommand(clientOpts *argocdclient.ClientOptions) *cobra
os.Exit(1)
}
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
appName := args[0]
app, err := appIf.Get(context.Background(), &applicationpkg.ApplicationQuery{Name: &appName})
errors.CheckError(err)
@@ -1946,7 +1807,7 @@ func NewApplicationRollbackCommand(clientOpts *argocdclient.ClientOptions) *cobr
errors.CheckError(err)
acdClient := argocdclient.NewClientOrDie(clientOpts)
conn, appIf := acdClient.NewApplicationClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
ctx := context.Background()
app, err := appIf.Get(ctx, &applicationpkg.ApplicationQuery{Name: &appName})
errors.CheckError(err)
@@ -2019,7 +1880,7 @@ func NewApplicationManifestsCommand(clientOpts *argocdclient.ClientOptions) *cob
}
appName := args[0]
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
ctx := context.Background()
resources, err := appIf.ManagedResources(context.Background(), &applicationpkg.ResourcesQuery{ApplicationName: &appName})
errors.CheckError(err)
@@ -2077,7 +1938,7 @@ func NewApplicationTerminateOpCommand(clientOpts *argocdclient.ClientOptions) *c
}
appName := args[0]
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
ctx := context.Background()
_, err := appIf.TerminateOperation(ctx, &applicationpkg.OperationTerminateRequest{Name: &appName})
errors.CheckError(err)
@@ -2098,7 +1959,7 @@ func NewApplicationEditCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
}
appName := args[0]
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
app, err := appIf.Get(context.Background(), &applicationpkg.ApplicationQuery{Name: &appName})
errors.CheckError(err)
appData, err := json.Marshal(app.Spec)
@@ -2116,10 +1977,7 @@ func NewApplicationEditCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
if err != nil {
return err
}
var appOpts appOptions
setAppSpecOptions(c.Flags(), &app.Spec, &appOpts)
_, err = appIf.UpdateSpec(context.Background(), &applicationpkg.ApplicationUpdateSpecRequest{Name: &app.Name, Spec: updatedSpec, Validate: &appOpts.validate})
_, err = appIf.UpdateSpec(context.Background(), &applicationpkg.ApplicationUpdateSpecRequest{Name: &app.Name, Spec: updatedSpec})
if err != nil {
return fmt.Errorf("Failed to update application spec:\n%v", err)
}
@@ -2150,7 +2008,7 @@ func NewApplicationPatchCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
}
appName := args[0]
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
patchedApp, err := appIf.Patch(context.Background(), &applicationpkg.ApplicationPatchRequest{
Name: &appName,
@@ -2237,7 +2095,7 @@ func NewApplicationPatchResourceCommand(clientOpts *argocdclient.ClientOptions)
appName := args[0]
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
ctx := context.Background()
resources, err := appIf.ManagedResources(ctx, &applicationpkg.ResourcesQuery{ApplicationName: &appName})
errors.CheckError(err)

View File

@@ -4,18 +4,18 @@ import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"strconv"
"text/tabwriter"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
"github.com/argoproj/gitops-engine/pkg/utils/io"
"github.com/ghodss/yaml"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/argoproj/argo-cd/errors"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
applicationpkg "github.com/argoproj/argo-cd/pkg/apiclient/application"
"github.com/argoproj/argo-cd/util"
)
type DisplayedAction struct {
@@ -59,7 +59,7 @@ func NewApplicationResourceActionsListCommand(clientOpts *argocdclient.ClientOpt
}
appName := args[0]
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
ctx := context.Background()
resources, err := appIf.ManagedResources(ctx, &applicationpkg.ResourcesQuery{ApplicationName: &appName})
errors.CheckError(err)
@@ -144,7 +144,7 @@ func NewApplicationResourceActionsRunCommand(clientOpts *argocdclient.ClientOpti
actionName := args[1]
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
ctx := context.Background()
resources, err := appIf.ManagedResources(ctx, &applicationpkg.ResourcesQuery{ApplicationName: &appName})
errors.CheckError(err)

View File

@@ -3,7 +3,6 @@ package commands
import (
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
@@ -35,70 +34,4 @@ func Test_setHelmOpt(t *testing.T) {
setHelmOpt(&src, helmOpts{helmSetStrings: []string{"foo=bar"}})
assert.Equal(t, []v1alpha1.HelmParameter{{Name: "foo", Value: "bar", ForceString: true}}, src.Helm.Parameters)
})
t.Run("HelmSetFiles", func(t *testing.T) {
src := v1alpha1.ApplicationSource{}
setHelmOpt(&src, helmOpts{helmSetFiles: []string{"foo=bar"}})
assert.Equal(t, []v1alpha1.HelmFileParameter{{Name: "foo", Path: "bar"}}, src.Helm.FileParameters)
})
}
func Test_setJsonnetOpt(t *testing.T) {
t.Run("TlaSets", func(t *testing.T) {
src := v1alpha1.ApplicationSource{}
setJsonnetOpt(&src, []string{"foo=bar"}, false)
assert.Equal(t, []v1alpha1.JsonnetVar{{Name: "foo", Value: "bar"}}, src.Directory.Jsonnet.TLAs)
setJsonnetOpt(&src, []string{"bar=baz"}, false)
assert.Equal(t, []v1alpha1.JsonnetVar{{Name: "foo", Value: "bar"}, {Name: "bar", Value: "baz"}}, src.Directory.Jsonnet.TLAs)
})
t.Run("ExtSets", func(t *testing.T) {
src := v1alpha1.ApplicationSource{}
setJsonnetOptExtVar(&src, []string{"foo=bar"}, false)
assert.Equal(t, []v1alpha1.JsonnetVar{{Name: "foo", Value: "bar"}}, src.Directory.Jsonnet.ExtVars)
setJsonnetOptExtVar(&src, []string{"bar=baz"}, false)
assert.Equal(t, []v1alpha1.JsonnetVar{{Name: "foo", Value: "bar"}, {Name: "bar", Value: "baz"}}, src.Directory.Jsonnet.ExtVars)
})
}
type appOptionsFixture struct {
spec *v1alpha1.ApplicationSpec
command *cobra.Command
options *appOptions
}
func (f *appOptionsFixture) SetFlag(key, value string) error {
err := f.command.Flags().Set(key, value)
if err != nil {
return err
}
_ = setAppSpecOptions(f.command.Flags(), f.spec, f.options)
return err
}
func newAppOptionsFixture() *appOptionsFixture {
fixture := &appOptionsFixture{
spec: &v1alpha1.ApplicationSpec{},
command: &cobra.Command{},
options: &appOptions{},
}
addAppFlags(fixture.command, fixture.options)
return fixture
}
func Test_setAppSpecOptions(t *testing.T) {
f := newAppOptionsFixture()
t.Run("SyncPolicy", func(t *testing.T) {
assert.NoError(t, f.SetFlag("sync-policy", "automated"))
assert.NotNil(t, f.spec.SyncPolicy.Automated)
assert.NoError(t, f.SetFlag("sync-policy", "none"))
assert.Nil(t, f.spec.SyncPolicy)
})
t.Run("SyncOptions", func(t *testing.T) {
assert.NoError(t, f.SetFlag("sync-option", "a=1"))
assert.True(t, f.spec.SyncPolicy.SyncOptions.HasOption("a=1"))
// remove the options using !
assert.NoError(t, f.SetFlag("sync-option", "!a=1"))
assert.Nil(t, f.spec.SyncPolicy)
})
}

View File

@@ -2,21 +2,22 @@ package commands
import (
"context"
"crypto/x509"
"fmt"
"os"
"sort"
"strings"
"text/tabwriter"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
"github.com/argoproj/gitops-engine/pkg/utils/io"
"github.com/spf13/cobra"
"github.com/argoproj/argo-cd/errors"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
certificatepkg "github.com/argoproj/argo-cd/pkg/apiclient/certificate"
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/util"
certutil "github.com/argoproj/argo-cd/util/cert"
"crypto/x509"
)
// NewCertCommand returns a new instance of an `argocd repo` command
@@ -65,7 +66,7 @@ func NewCertAddTLSCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
Short: "Add TLS certificate data for connecting to repository server SERVERNAME",
Run: func(c *cobra.Command, args []string) {
conn, certIf := argocdclient.NewClientOrDie(clientOpts).NewCertClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
if len(args) != 1 {
c.HelpFunc()(c, args)
@@ -148,7 +149,7 @@ func NewCertAddSSHCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
Run: func(c *cobra.Command, args []string) {
conn, certIf := argocdclient.NewClientOrDie(clientOpts).NewCertClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
var sshKnownHostsLists []string
var err error
@@ -173,20 +174,18 @@ func NewCertAddSSHCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
}
for _, knownHostsEntry := range sshKnownHostsLists {
_, certSubType, certData, err := certutil.TokenizeSSHKnownHostsEntry(knownHostsEntry)
hostname, certSubType, certData, err := certutil.TokenizeSSHKnownHostsEntry(knownHostsEntry)
errors.CheckError(err)
hostnameList, _, err := certutil.KnownHostsLineToPublicKey(knownHostsEntry)
_, _, err = certutil.KnownHostsLineToPublicKey(knownHostsEntry)
errors.CheckError(err)
// Each key could be valid for multiple hostnames
for _, hostname := range hostnameList {
certificate := appsv1.RepositoryCertificate{
ServerName: hostname,
CertType: "ssh",
CertSubType: certSubType,
CertData: certData,
}
certificates = append(certificates, certificate)
certificate := appsv1.RepositoryCertificate{
ServerName: hostname,
CertType: "ssh",
CertSubType: certSubType,
CertData: certData,
}
certificates = append(certificates, certificate)
}
certList := &appsv1.RepositoryCertificateList{Items: certificates}
@@ -220,7 +219,7 @@ func NewCertRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
os.Exit(1)
}
conn, certIf := argocdclient.NewClientOrDie(clientOpts).NewCertClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
hostNamePattern := args[0]
// Prevent the user from specifying a wildcard as hostname as precaution
@@ -275,7 +274,7 @@ func NewCertListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
}
conn, certIf := argocdclient.NewClientOrDie(clientOpts).NewCertClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
certificates, err := certIf.ListCertificates(context.Background(), &certificatepkg.RepositoryCertificateQuery{HostNamePattern: hostNamePattern, CertType: certType})
errors.CheckError(err)

View File

@@ -9,8 +9,6 @@ import (
"strings"
"text/tabwriter"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
"github.com/argoproj/gitops-engine/pkg/utils/io"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"k8s.io/client-go/kubernetes"
@@ -18,9 +16,11 @@ import (
"k8s.io/client-go/tools/clientcmd"
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/errors"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
clusterpkg "github.com/argoproj/argo-cd/pkg/apiclient/cluster"
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/clusterauth"
)
@@ -60,7 +60,6 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
var (
inCluster bool
upsert bool
serviceAccount string
awsRoleArn string
awsClusterName string
systemNamespace string
@@ -102,15 +101,11 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
// Install RBAC resources for managing the cluster
clientset, err := kubernetes.NewForConfig(conf)
errors.CheckError(err)
if serviceAccount != "" {
managerBearerToken, err = clusterauth.GetServiceAccountBearerToken(clientset, systemNamespace, serviceAccount)
} else {
managerBearerToken, err = clusterauth.InstallClusterManagerRBAC(clientset, systemNamespace, namespaces)
}
managerBearerToken, err = clusterauth.InstallClusterManagerRBAC(clientset, systemNamespace, namespaces)
errors.CheckError(err)
}
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
clst := newCluster(contextName, namespaces, conf, managerBearerToken, awsAuthConf)
if inCluster {
clst.Server = common.KubernetesInternalAPIServerAddr
@@ -127,8 +122,7 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
command.PersistentFlags().StringVar(&pathOpts.LoadingRules.ExplicitPath, pathOpts.ExplicitFileFlag, pathOpts.LoadingRules.ExplicitPath, "use a particular kubeconfig file")
command.Flags().BoolVar(&inCluster, "in-cluster", false, "Indicates Argo CD resides inside this cluster and should connect using the internal k8s hostname (kubernetes.default.svc)")
command.Flags().BoolVar(&upsert, "upsert", false, "Override an existing cluster with the same name even if the spec differs")
command.Flags().StringVar(&serviceAccount, "service-account", "", fmt.Sprintf("System namespace service account to use for kubernetes resource management. If not set then default \"%s\" SA will be created", clusterauth.ArgoCDManagerServiceAccount))
command.Flags().StringVar(&awsClusterName, "aws-cluster-name", "", "AWS Cluster name if set then aws cli eks token command will be used to access cluster")
command.Flags().StringVar(&awsClusterName, "aws-cluster-name", "", "AWS Cluster name if set then aws-iam-authenticator will be used to access cluster")
command.Flags().StringVar(&awsRoleArn, "aws-role-arn", "", "Optional AWS role arn. If set then AWS IAM Authenticator assume a role to perform cluster operations instead of the default AWS credential provider chain.")
command.Flags().StringVar(&systemNamespace, "system-namespace", common.DefaultSystemNamespace, "Use different system namespace")
command.Flags().StringArrayVar(&namespaces, "namespace", nil, "List of namespaces which are allowed to manage")
@@ -179,42 +173,22 @@ func newCluster(name string, namespaces []string, conf *rest.Config, managerBear
Insecure: conf.TLSClientConfig.Insecure,
ServerName: conf.TLSClientConfig.ServerName,
CAData: conf.TLSClientConfig.CAData,
CertData: conf.TLSClientConfig.CertData,
KeyData: conf.TLSClientConfig.KeyData,
}
if len(conf.TLSClientConfig.CAData) == 0 && conf.TLSClientConfig.CAFile != "" {
data, err := ioutil.ReadFile(conf.TLSClientConfig.CAFile)
errors.CheckError(err)
tlsClientConfig.CAData = data
}
if len(conf.TLSClientConfig.CertData) == 0 && conf.TLSClientConfig.CertFile != "" {
data, err := ioutil.ReadFile(conf.TLSClientConfig.CertFile)
errors.CheckError(err)
tlsClientConfig.CertData = data
}
if len(conf.TLSClientConfig.KeyData) == 0 && conf.TLSClientConfig.KeyFile != "" {
data, err := ioutil.ReadFile(conf.TLSClientConfig.KeyFile)
errors.CheckError(err)
tlsClientConfig.KeyData = data
}
clst := argoappv1.Cluster{
Server: conf.Host,
Name: name,
Namespaces: namespaces,
Config: argoappv1.ClusterConfig{
BearerToken: managerBearerToken,
TLSClientConfig: tlsClientConfig,
AWSAuthConfig: awsAuthConf,
},
}
// Bearer token will preferentially be used for auth if present,
// Even in presence of key/cert credentials
// So set bearer token only if the key/cert data is absent
if len(tlsClientConfig.CertData) == 0 || len(tlsClientConfig.KeyData) == 0 {
clst.Config.BearerToken = managerBearerToken
}
return &clst
}
@@ -233,7 +207,7 @@ func NewClusterGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
os.Exit(1)
}
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
clusters := make([]argoappv1.Cluster, 0)
for _, clusterName := range args {
clst, err := clusterIf.Get(context.Background(), &clusterpkg.ClusterQuery{Server: clusterName})
@@ -302,7 +276,7 @@ func NewClusterRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
os.Exit(1)
}
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
// clientset, err := kubernetes.NewForConfig(conf)
// errors.CheckError(err)
@@ -350,7 +324,7 @@ func NewClusterListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman
Short: "List configured clusters",
Run: func(c *cobra.Command, args []string) {
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
clusters, err := clusterIf.List(context.Background(), &clusterpkg.ClusterQuery{})
errors.CheckError(err)
switch output {
@@ -382,7 +356,7 @@ func NewClusterRotateAuthCommand(clientOpts *argocdclient.ClientOptions) *cobra.
os.Exit(1)
}
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
clusterQuery := clusterpkg.ClusterQuery{
Server: args[0],
}

View File

@@ -1,12 +1,9 @@
package commands
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/rest"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
)
@@ -32,52 +29,3 @@ func Test_printClusterTable(t *testing.T) {
},
})
}
func Test_newCluster(t *testing.T) {
clusterWithData := newCluster("test-cluster", []string{"test-namespace"}, &rest.Config{
TLSClientConfig: rest.TLSClientConfig{
Insecure: false,
ServerName: "test-endpoint.example.com",
CAData: []byte("test-ca-data"),
CertData: []byte("test-cert-data"),
KeyData: []byte("test-key-data"),
},
Host: "test-endpoint.example.com",
},
"test-bearer-token",
&v1alpha1.AWSAuthConfig{})
assert.Equal(t, "test-cert-data", string(clusterWithData.Config.CertData))
assert.Equal(t, "test-key-data", string(clusterWithData.Config.KeyData))
assert.Equal(t, "", clusterWithData.Config.BearerToken)
clusterWithFiles := newCluster("test-cluster", []string{"test-namespace"}, &rest.Config{
TLSClientConfig: rest.TLSClientConfig{
Insecure: false,
ServerName: "test-endpoint.example.com",
CAData: []byte("test-ca-data"),
CertFile: "./testdata/test.cert.pem",
KeyFile: "./testdata/test.key.pem",
},
Host: "test-endpoint.example.com",
},
"test-bearer-token",
&v1alpha1.AWSAuthConfig{})
assert.True(t, strings.Contains(string(clusterWithFiles.Config.CertData), "test-cert-data"))
assert.True(t, strings.Contains(string(clusterWithFiles.Config.KeyData), "test-key-data"))
assert.Equal(t, "", clusterWithFiles.Config.BearerToken)
clusterWithBearerToken := newCluster("test-cluster", []string{"test-namespace"}, &rest.Config{
TLSClientConfig: rest.TLSClientConfig{
Insecure: false,
ServerName: "test-endpoint.example.com",
CAData: []byte("test-ca-data"),
},
Host: "test-endpoint.example.com",
},
"test-bearer-token",
&v1alpha1.AWSAuthConfig{})
assert.Equal(t, "test-bearer-token", clusterWithBearerToken.Config.BearerToken)
}

View File

@@ -3,9 +3,9 @@ package commands
import (
"fmt"
"io"
"log"
"os"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

View File

@@ -8,10 +8,10 @@ import (
"strings"
"text/tabwriter"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/argoproj/argo-cd/errors"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
"github.com/argoproj/argo-cd/util/localconfig"
)

View File

@@ -6,11 +6,8 @@ import (
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
"github.com/argoproj/gitops-engine/pkg/utils/io"
"github.com/coreos/go-oidc"
"github.com/dgrijalva/jwt-go"
log "github.com/sirupsen/logrus"
@@ -18,9 +15,11 @@ import (
"github.com/spf13/cobra"
"golang.org/x/oauth2"
"github.com/argoproj/argo-cd/errors"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
sessionpkg "github.com/argoproj/argo-cd/pkg/apiclient/session"
settingspkg "github.com/argoproj/argo-cd/pkg/apiclient/settings"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/cli"
grpc_util "github.com/argoproj/argo-cd/util/grpc"
"github.com/argoproj/argo-cd/util/localconfig"
@@ -42,55 +41,41 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
Short: "Log in to Argo CD",
Long: "Log in to Argo CD",
Run: func(c *cobra.Command, args []string) {
var server string
if len(args) != 1 && !globalClientOpts.PortForward {
if len(args) == 0 {
c.HelpFunc()(c, args)
os.Exit(1)
}
if globalClientOpts.PortForward {
server = "port-forward"
} else {
server = args[0]
tlsTestResult, err := grpc_util.TestTLS(server)
errors.CheckError(err)
if !tlsTestResult.TLS {
if !globalClientOpts.PlainText {
if !cli.AskToProceed("WARNING: server is not configured with TLS. Proceed (y/n)? ") {
os.Exit(1)
}
globalClientOpts.PlainText = true
server := args[0]
tlsTestResult, err := grpc_util.TestTLS(server)
errors.CheckError(err)
if !tlsTestResult.TLS {
if !globalClientOpts.PlainText {
if !cli.AskToProceed("WARNING: server is not configured with TLS. Proceed (y/n)? ") {
os.Exit(1)
}
} else if tlsTestResult.InsecureErr != nil {
if !globalClientOpts.Insecure {
if !cli.AskToProceed(fmt.Sprintf("WARNING: server certificate had error: %s. Proceed insecurely (y/n)? ", tlsTestResult.InsecureErr)) {
os.Exit(1)
}
globalClientOpts.Insecure = true
globalClientOpts.PlainText = true
}
} else if tlsTestResult.InsecureErr != nil {
if !globalClientOpts.Insecure {
if !cli.AskToProceed(fmt.Sprintf("WARNING: server certificate had error: %s. Proceed insecurely (y/n)? ", tlsTestResult.InsecureErr)) {
os.Exit(1)
}
globalClientOpts.Insecure = true
}
}
clientOpts := argocdclient.ClientOptions{
ConfigPath: "",
ServerAddr: server,
Insecure: globalClientOpts.Insecure,
PlainText: globalClientOpts.PlainText,
GRPCWeb: globalClientOpts.GRPCWeb,
GRPCWebRootPath: globalClientOpts.GRPCWebRootPath,
PortForward: globalClientOpts.PortForward,
PortForwardNamespace: globalClientOpts.PortForwardNamespace,
ConfigPath: "",
ServerAddr: server,
Insecure: globalClientOpts.Insecure,
PlainText: globalClientOpts.PlainText,
GRPCWeb: globalClientOpts.GRPCWeb,
}
acdClient := argocdclient.NewClientOrDie(&clientOpts)
setConn, setIf := acdClient.NewSettingsClientOrDie()
defer io.Close(setConn)
defer util.Close(setConn)
if ctxName == "" {
ctxName = server
if globalClientOpts.GRPCWebRootPath != "" {
rootPath := strings.TrimRight(strings.TrimLeft(globalClientOpts.GRPCWebRootPath, "/"), "/")
ctxName = fmt.Sprintf("%s/%s", server, rootPath)
}
}
// Perform the login
@@ -114,7 +99,7 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
SkipClaimsValidation: true,
}
claims := jwt.MapClaims{}
_, _, err := parser.ParseUnverified(tokenString, &claims)
_, _, err = parser.ParseUnverified(tokenString, &claims)
errors.CheckError(err)
fmt.Printf("'%s' logged in successfully\n", userDisplayName(claims))
@@ -125,11 +110,10 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
localCfg = &localconfig.LocalConfig{}
}
localCfg.UpsertServer(localconfig.Server{
Server: server,
PlainText: globalClientOpts.PlainText,
Insecure: globalClientOpts.Insecure,
GRPCWeb: globalClientOpts.GRPCWeb,
GRPCWebRootPath: globalClientOpts.GRPCWebRootPath,
Server: server,
PlainText: globalClientOpts.PlainText,
Insecure: globalClientOpts.Insecure,
GRPCWeb: globalClientOpts.GRPCWeb,
})
localCfg.UpsertUser(localconfig.User{
Name: ctxName,
@@ -299,7 +283,7 @@ func oauth2Login(ctx context.Context, port int, oidcSettings *settingspkg.OIDCCo
func passwordLogin(acdClient argocdclient.Client, username, password string) string {
username, password = cli.PromptCredentials(username, password)
sessConn, sessionIf := acdClient.NewSessionClientOrDie()
defer io.Close(sessConn)
defer util.Close(sessConn)
sessionRequest := sessionpkg.SessionCreateRequest{
Username: username,
Password: password,

View File

@@ -4,10 +4,10 @@ import (
"fmt"
"os"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/argoproj/argo-cd/errors"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
"github.com/argoproj/argo-cd/util/localconfig"
)

View File

@@ -12,8 +12,6 @@ import (
"text/tabwriter"
"time"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
argoio "github.com/argoproj/gitops-engine/pkg/utils/io"
"github.com/dustin/go-humanize"
"github.com/ghodss/yaml"
log "github.com/sirupsen/logrus"
@@ -22,9 +20,11 @@ import (
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
"github.com/argoproj/argo-cd/errors"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
projectpkg "github.com/argoproj/argo-cd/pkg/apiclient/project"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/cli"
"github.com/argoproj/argo-cd/util/config"
"github.com/argoproj/argo-cd/util/git"
@@ -170,7 +170,7 @@ func NewProjectCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
}
}
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
_, err := projIf.Create(context.Background(), &projectpkg.ProjectCreateRequest{Project: &proj, Upsert: upsert})
errors.CheckError(err)
},
@@ -200,7 +200,7 @@ func NewProjectSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
}
projName := args[0]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
@@ -247,7 +247,7 @@ func NewProjectAddDestinationCommand(clientOpts *argocdclient.ClientOptions) *co
server := args[1]
namespace := args[2]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
@@ -279,7 +279,7 @@ func NewProjectRemoveDestinationCommand(clientOpts *argocdclient.ClientOptions)
server := args[1]
namespace := args[2]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
@@ -317,7 +317,7 @@ func NewProjectAddSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
projName := args[0]
url := args[1]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
@@ -340,19 +340,18 @@ func NewProjectAddSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
return command
}
func modifyClusterResourceCmd(cmdUse, cmdDesc string, clientOpts *argocdclient.ClientOptions, action func(proj *v1alpha1.AppProject, group string, kind string) bool) *cobra.Command {
func modifyProjectResourceCmd(cmdUse, cmdDesc string, clientOpts *argocdclient.ClientOptions, action func(proj *v1alpha1.AppProject, group string, kind string) bool) *cobra.Command {
return &cobra.Command{
Use: cmdUse,
Short: cmdDesc,
Run: func(c *cobra.Command, args []string) {
if len(args) != 3 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName, group, kind := args[0], args[1], args[2]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
@@ -365,55 +364,11 @@ func modifyClusterResourceCmd(cmdUse, cmdDesc string, clientOpts *argocdclient.C
}
}
func modifyNamespaceResourceCmd(cmdUse, cmdDesc string, clientOpts *argocdclient.ClientOptions, action func(proj *v1alpha1.AppProject, group string, kind string, useWhitelist bool) bool) *cobra.Command {
var (
list string
)
var command = &cobra.Command{
Use: cmdUse,
Short: cmdDesc,
Run: func(c *cobra.Command, args []string) {
if len(args) != 3 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName, group, kind := args[0], args[1], args[2]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer argoio.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
var useWhitelist = false
if list == "white" {
useWhitelist = true
}
if action(proj, group, kind, useWhitelist) {
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
}
},
}
command.Flags().StringVarP(&list, "list", "l", "black", "Use blacklist or whitelist. This can only be 'white' or 'black'")
return command
}
// NewProjectAllowNamespaceResourceCommand returns a new instance of an `deny-cluster-resources` command
func NewProjectAllowNamespaceResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
use := "allow-namespace-resource PROJECT GROUP KIND"
desc := "Removes a namespaced API resource from the blacklist or add a namespaced API resource to the whitelist"
return modifyNamespaceResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string, useWhitelist bool) bool {
if useWhitelist {
for _, item := range proj.Spec.NamespaceResourceWhitelist {
if item.Group == group && item.Kind == kind {
fmt.Printf("Group '%s' and kind '%s' already present in whitelisted namespaced resources\n", group, kind)
return false
}
}
proj.Spec.NamespaceResourceWhitelist = append(proj.Spec.NamespaceResourceWhitelist, v1.GroupKind{Group: group, Kind: kind})
fmt.Printf("Group '%s' and kind '%s' is added to whitelisted namespaced resources\n", group, kind)
return true
}
desc := "Removes a namespaced API resource from the blacklist"
return modifyProjectResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
index := -1
for i, item := range proj.Spec.NamespaceResourceBlacklist {
if item.Group == group && item.Kind == kind {
@@ -426,7 +381,6 @@ func NewProjectAllowNamespaceResourceCommand(clientOpts *argocdclient.ClientOpti
return false
}
proj.Spec.NamespaceResourceBlacklist = append(proj.Spec.NamespaceResourceBlacklist[:index], proj.Spec.NamespaceResourceBlacklist[index+1:]...)
fmt.Printf("Group '%s' and kind '%s' is removed from blacklisted namespaced resources\n", group, kind)
return true
})
}
@@ -434,25 +388,8 @@ func NewProjectAllowNamespaceResourceCommand(clientOpts *argocdclient.ClientOpti
// NewProjectDenyNamespaceResourceCommand returns a new instance of an `argocd proj deny-namespace-resource` command
func NewProjectDenyNamespaceResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
use := "deny-namespace-resource PROJECT GROUP KIND"
desc := "Adds a namespaced API resource to the blacklist or removes a namespaced API resource from the whitelist"
return modifyNamespaceResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string, useWhitelist bool) bool {
if useWhitelist {
index := -1
for i, item := range proj.Spec.NamespaceResourceWhitelist {
if item.Group == group && item.Kind == kind {
index = i
break
}
}
if index == -1 {
fmt.Printf("Group '%s' and kind '%s' not in whitelisted namespaced resources\n", group, kind)
return false
}
proj.Spec.NamespaceResourceWhitelist = append(proj.Spec.NamespaceResourceWhitelist[:index], proj.Spec.NamespaceResourceWhitelist[index+1:]...)
fmt.Printf("Group '%s' and kind '%s' is removed from whitelisted namespaced resources\n", group, kind)
return true
}
desc := "Adds a namespaced API resource to the blacklist"
return modifyProjectResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
for _, item := range proj.Spec.NamespaceResourceBlacklist {
if item.Group == group && item.Kind == kind {
fmt.Printf("Group '%s' and kind '%s' already present in blacklisted namespaced resources\n", group, kind)
@@ -460,7 +397,6 @@ func NewProjectDenyNamespaceResourceCommand(clientOpts *argocdclient.ClientOptio
}
}
proj.Spec.NamespaceResourceBlacklist = append(proj.Spec.NamespaceResourceBlacklist, v1.GroupKind{Group: group, Kind: kind})
fmt.Printf("Group '%s' and kind '%s' is added to blacklisted namespaced resources\n", group, kind)
return true
})
}
@@ -469,7 +405,7 @@ func NewProjectDenyNamespaceResourceCommand(clientOpts *argocdclient.ClientOptio
func NewProjectDenyClusterResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
use := "deny-cluster-resource PROJECT GROUP KIND"
desc := "Removes a cluster-scoped API resource from the whitelist"
return modifyClusterResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
return modifyProjectResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
index := -1
for i, item := range proj.Spec.ClusterResourceWhitelist {
if item.Group == group && item.Kind == kind {
@@ -490,7 +426,7 @@ func NewProjectDenyClusterResourceCommand(clientOpts *argocdclient.ClientOptions
func NewProjectAllowClusterResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
use := "allow-cluster-resource PROJECT GROUP KIND"
desc := "Adds a cluster-scoped API resource to the whitelist"
return modifyClusterResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
return modifyProjectResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
for _, item := range proj.Spec.ClusterResourceWhitelist {
if item.Group == group && item.Kind == kind {
fmt.Printf("Group '%s' and kind '%s' already present in whitelisted cluster resources\n", group, kind)
@@ -515,7 +451,7 @@ func NewProjectRemoveSourceCommand(clientOpts *argocdclient.ClientOptions) *cobr
projName := args[0]
url := args[1]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
@@ -551,7 +487,7 @@ func NewProjectDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
os.Exit(1)
}
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
for _, name := range args {
_, err := projIf.Delete(context.Background(), &projectpkg.ProjectQuery{Name: name})
errors.CheckError(err)
@@ -588,7 +524,7 @@ func NewProjectListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman
Short: "List projects",
Run: func(c *cobra.Command, args []string) {
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
projects, err := projIf.List(context.Background(), &projectpkg.ProjectQuery{})
errors.CheckError(err)
switch output {
@@ -714,7 +650,7 @@ func NewProjectGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
}
projName := args[0]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
p, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
@@ -744,7 +680,7 @@ func NewProjectEditCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman
}
projName := args[0]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer argoio.Close(conn)
defer util.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
projData, err := json.Marshal(proj.Spec)

View File

@@ -7,14 +7,14 @@ import (
"strconv"
"text/tabwriter"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
"github.com/argoproj/gitops-engine/pkg/utils/io"
timeutil "github.com/argoproj/pkg/time"
"github.com/spf13/cobra"
"github.com/argoproj/argo-cd/errors"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
projectpkg "github.com/argoproj/argo-cd/pkg/apiclient/project"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/util"
)
const (
@@ -60,7 +60,7 @@ func NewProjectRoleAddPolicyCommand(clientOpts *argocdclient.ClientOptions) *cob
projName := args[0]
roleName := args[1]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
@@ -95,7 +95,7 @@ func NewProjectRoleRemovePolicyCommand(clientOpts *argocdclient.ClientOptions) *
projName := args[0]
roleName := args[1]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
@@ -140,7 +140,7 @@ func NewProjectRoleCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.
projName := args[0]
roleName := args[1]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
@@ -174,7 +174,7 @@ func NewProjectRoleDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.
projName := args[0]
roleName := args[1]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
@@ -211,7 +211,7 @@ func NewProjectRoleCreateTokenCommand(clientOpts *argocdclient.ClientOptions) *c
projName := args[0]
roleName := args[1]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
duration, err := timeutil.ParseDuration(expiresIn)
errors.CheckError(err)
token, err := projIf.CreateToken(context.Background(), &projectpkg.ProjectTokenCreateRequest{Project: projName, Role: roleName, ExpiresIn: int64(duration.Seconds())})
@@ -240,7 +240,7 @@ func NewProjectRoleDeleteTokenCommand(clientOpts *argocdclient.ClientOptions) *c
errors.CheckError(err)
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
_, err = projIf.DeleteToken(context.Background(), &projectpkg.ProjectTokenDeleteRequest{Project: projName, Role: roleName, Iat: issuedAt})
errors.CheckError(err)
@@ -281,7 +281,7 @@ func NewProjectRoleListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
}
projName := args[0]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
project, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
@@ -315,7 +315,7 @@ func NewProjectRoleGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
projName := args[0]
roleName := args[1]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
@@ -357,7 +357,7 @@ func NewProjectRoleAddGroupCommand(clientOpts *argocdclient.ClientOptions) *cobr
}
projName, roleName, groupName := args[0], args[1], args[2]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
updated, err := proj.AddGroupToRole(roleName, groupName)
@@ -386,7 +386,7 @@ func NewProjectRoleRemoveGroupCommand(clientOpts *argocdclient.ClientOptions) *c
}
projName, roleName, groupName := args[0], args[1], args[2]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
updated, err := proj.RemoveGroupFromRole(roleName, groupName)

View File

@@ -2,19 +2,21 @@ package commands
import (
"context"
"fmt"
"os"
"strconv"
"github.com/spf13/cobra"
"fmt"
"strings"
"text/tabwriter"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
"github.com/argoproj/gitops-engine/pkg/utils/io"
"github.com/spf13/cobra"
"strconv"
"github.com/argoproj/argo-cd/errors"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
projectpkg "github.com/argoproj/argo-cd/pkg/apiclient/project"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/util"
)
// NewProjectWindowsCommand returns a new instance of the `argocd proj windows` command
@@ -53,7 +55,7 @@ func NewProjectWindowsDisableManualSyncCommand(clientOpts *argocdclient.ClientOp
errors.CheckError(err)
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
@@ -88,7 +90,7 @@ func NewProjectWindowsEnableManualSyncCommand(clientOpts *argocdclient.ClientOpt
errors.CheckError(err)
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
@@ -127,7 +129,7 @@ func NewProjectWindowsAddWindowCommand(clientOpts *argocdclient.ClientOptions) *
}
projName := args[0]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
@@ -166,7 +168,7 @@ func NewProjectWindowsDeleteCommand(clientOpts *argocdclient.ClientOptions) *cob
errors.CheckError(err)
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
@@ -205,7 +207,7 @@ func NewProjectWindowsUpdateCommand(clientOpts *argocdclient.ClientOptions) *cob
errors.CheckError(err)
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
@@ -246,7 +248,7 @@ func NewProjectWindowsListCommand(clientOpts *argocdclient.ClientOptions) *cobra
}
projName := args[0]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)

View File

@@ -5,14 +5,14 @@ import (
"fmt"
"os"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
argoio "github.com/argoproj/gitops-engine/pkg/utils/io"
"github.com/coreos/go-oidc"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/argoproj/argo-cd/errors"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
settingspkg "github.com/argoproj/argo-cd/pkg/apiclient/settings"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/localconfig"
"github.com/argoproj/argo-cd/util/session"
)
@@ -43,12 +43,11 @@ func NewReloginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comm
var tokenString string
var refreshToken string
clientOpts := argocdclient.ClientOptions{
ConfigPath: "",
ServerAddr: configCtx.Server.Server,
Insecure: configCtx.Server.Insecure,
GRPCWeb: globalClientOpts.GRPCWeb,
GRPCWebRootPath: globalClientOpts.GRPCWebRootPath,
PlainText: configCtx.Server.PlainText,
ConfigPath: "",
ServerAddr: configCtx.Server.Server,
Insecure: configCtx.Server.Insecure,
GRPCWeb: globalClientOpts.GRPCWeb,
PlainText: configCtx.Server.PlainText,
}
acdClient := argocdclient.NewClientOrDie(&clientOpts)
claims, err := configCtx.User.Claims()
@@ -59,7 +58,7 @@ func NewReloginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comm
} else {
fmt.Println("Reinitiating SSO login")
setConn, setIf := acdClient.NewSettingsClientOrDie()
defer argoio.Close(setConn)
defer util.Close(setConn)
ctx := context.Background()
httpClient, err := acdClient.HTTPClient()
errors.CheckError(err)

View File

@@ -7,15 +7,15 @@ import (
"os"
"text/tabwriter"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
"github.com/argoproj/gitops-engine/pkg/utils/io"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/errors"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
repositorypkg "github.com/argoproj/argo-cd/pkg/apiclient/repository"
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/cli"
"github.com/argoproj/argo-cd/util/git"
)
@@ -32,7 +32,6 @@ func NewRepoCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
}
command.AddCommand(NewRepoAddCommand(clientOpts))
command.AddCommand(NewRepoGetCommand(clientOpts))
command.AddCommand(NewRepoListCommand(clientOpts))
command.AddCommand(NewRepoRemoveCommand(clientOpts))
return command
@@ -53,10 +52,7 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
// For better readability and easier formatting
var repoAddExamples = ` # Add a Git repository via SSH using a private key for authentication, ignoring the server's host key:
argocd repo add git@git.example.com:repos/repo --insecure-ignore-host-key --ssh-private-key-path ~/id_rsa
# Add a Git repository via SSH on a non-default port - need to use ssh:// style URLs here
argocd repo add ssh://git@git.example.com:2222/repos/repo --ssh-private-key-path ~/id_rsa
argocd repo add git@git.example.com:repos/repo --insecure-ignore-host-key --ssh-private-key-path ~/id_rsa
# Add a private Git repository via HTTPS using username/password and TLS client certificates:
argocd repo add https://git.example.com/repos/repo --username git --password secret --tls-client-cert-path ~/mycert.crt --tls-client-cert-key-path ~/mycert.key
@@ -132,7 +128,7 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
}
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
// If the user set a username, but didn't supply password via --password,
// then we prompt for it
@@ -166,7 +162,7 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
Upsert: upsert,
}
createdRepo, err := repoIf.Create(context.Background(), &repoCreateReq)
createdRepo, err := repoIf.CreateRepository(context.Background(), &repoCreateReq)
errors.CheckError(err)
fmt.Printf("repository '%s' added\n", createdRepo.Repo)
},
@@ -178,7 +174,7 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
command.Flags().StringVar(&sshPrivateKeyPath, "ssh-private-key-path", "", "path to the private ssh key (e.g. ~/.ssh/id_rsa)")
command.Flags().StringVar(&tlsClientCertPath, "tls-client-cert-path", "", "path to the TLS client cert (must be PEM format)")
command.Flags().StringVar(&tlsClientCertKeyPath, "tls-client-cert-key-path", "", "path to the TLS client cert's key path (must be PEM format)")
command.Flags().BoolVar(&insecureIgnoreHostKey, "insecure-ignore-host-key", false, "disables SSH strict host key checking (deprecated, use --insecure-skip-server-verification instead)")
command.Flags().BoolVar(&insecureIgnoreHostKey, "insecure-ignore-host-key", false, "disables SSH strict host key checking (deprecated, use --insecure-skip-server-validation instead)")
command.Flags().BoolVar(&insecureSkipServerVerification, "insecure-skip-server-verification", false, "disables server certificate and host key checks")
command.Flags().BoolVar(&enableLfs, "enable-lfs", false, "enable git-lfs (Large File Support) on this repository")
command.Flags().BoolVar(&upsert, "upsert", false, "Override an existing repository with the same name even if the spec differs")
@@ -196,9 +192,9 @@ func NewRepoRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
os.Exit(1)
}
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
for _, repoURL := range args {
_, err := repoIf.Delete(context.Background(), &repositorypkg.RepoQuery{Repo: repoURL})
_, err := repoIf.DeleteRepository(context.Background(), &repositorypkg.RepoQuery{Repo: repoURL})
errors.CheckError(err)
}
},
@@ -244,7 +240,7 @@ func NewRepoListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
Short: "List configured repositories",
Run: func(c *cobra.Command, args []string) {
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
forceRefresh := false
switch refresh {
case "":
@@ -254,7 +250,7 @@ func NewRepoListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
err := fmt.Errorf("--refresh must be one of: 'hard'")
errors.CheckError(err)
}
repos, err := repoIf.List(context.Background(), &repositorypkg.RepoQuery{ForceRefresh: forceRefresh})
repos, err := repoIf.ListRepositories(context.Background(), &repositorypkg.RepoQuery{ForceRefresh: forceRefresh})
errors.CheckError(err)
switch output {
case "yaml", "json":
@@ -274,52 +270,3 @@ func NewRepoListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
command.Flags().StringVar(&refresh, "refresh", "", "Force a cache refresh on connection status")
return command
}
// NewRepoGetCommand returns a new instance of an `argocd repo rm` command
func NewRepoGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
output string
refresh string
)
var command = &cobra.Command{
Use: "get",
Short: "Get a configured repository by URL",
Run: func(c *cobra.Command, args []string) {
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
// Repository URL
repoURL := args[0]
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoClientOrDie()
defer io.Close(conn)
forceRefresh := false
switch refresh {
case "":
case "hard":
forceRefresh = true
default:
err := fmt.Errorf("--refresh must be one of: 'hard'")
errors.CheckError(err)
}
repo, err := repoIf.Get(context.Background(), &repositorypkg.RepoQuery{Repo: repoURL, ForceRefresh: forceRefresh})
errors.CheckError(err)
switch output {
case "yaml", "json":
err := PrintResource(repo, output)
errors.CheckError(err)
case "url":
fmt.Println(repo.Repo)
// wide is the default
case "wide", "":
printRepoTable(appsv1.Repositories{repo})
default:
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
}
},
}
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|url")
command.Flags().StringVar(&refresh, "refresh", "", "Force a cache refresh on connection status")
return command
}

View File

@@ -7,19 +7,19 @@ import (
"os"
"text/tabwriter"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
"github.com/argoproj/gitops-engine/pkg/utils/io"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/argoproj/argo-cd/errors"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
repocredspkg "github.com/argoproj/argo-cd/pkg/apiclient/repocreds"
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/cli"
"github.com/argoproj/argo-cd/util/git"
)
// NewRepoCredsCommand returns a new instance of an `argocd repocreds` command
// NewRepoCredsCommand returns a new instance of an `argocd repo` command
func NewRepoCredsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "repocreds",
@@ -36,7 +36,7 @@ func NewRepoCredsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
return command
}
// NewRepoCredsAddCommand returns a new instance of an `argocd repocreds add` command
// NewRepoCredsAddCommand returns a new instance of an `argocd repo add` command
func NewRepoCredsAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
repo appsv1.RepoCreds
@@ -50,8 +50,8 @@ func NewRepoCredsAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comma
var repocredsAddExamples = ` # Add credentials with user/pass authentication to use for all repositories under https://git.example.com/repos
argocd repocreds add https://git.example.com/repos/ --username git --password secret
# Add credentials with SSH private key authentication to use for all repositories under ssh://git@git.example.com/repos
argocd repocreds add ssh://git@git.example.com/repos/ --ssh-private-key-path ~/.ssh/id_rsa
# Add credentials with SSH private key authentication to use for all repositories under https://git.example.com/repos
argocd repocreds add https://git.example.com/repos/ --ssh-private-key-path ~/.ssh/id_rsa
`
var command = &cobra.Command{
@@ -104,7 +104,7 @@ func NewRepoCredsAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comma
}
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoCredsClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
// If the user set a username, but didn't supply password via --password,
// then we prompt for it
@@ -131,7 +131,7 @@ func NewRepoCredsAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comma
return command
}
// NewRepoCredsRemoveCommand returns a new instance of an `argocd repocreds rm` command
// NewRepoCredsRemoveCommand returns a new instance of an `argocd repo list` command
func NewRepoCredsRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "rm CREDSURL",
@@ -142,7 +142,7 @@ func NewRepoCredsRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
os.Exit(1)
}
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoCredsClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
for _, repoURL := range args {
_, err := repoIf.DeleteRepositoryCredentials(context.Background(), &repocredspkg.RepoCredsDeleteRequest{Url: repoURL})
errors.CheckError(err)
@@ -172,7 +172,7 @@ func printRepoCredsUrls(repos []appsv1.RepoCreds) {
}
}
// NewRepoCredsListCommand returns a new instance of an `argocd repo list` command
// NewRepoCredsListCommand returns a new instance of an `argocd repo rm` command
func NewRepoCredsListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
output string
@@ -182,7 +182,7 @@ func NewRepoCredsListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
Short: "List configured repository credentials",
Run: func(c *cobra.Command, args []string) {
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoCredsClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
repos, err := repoIf.ListRepositoryCredentials(context.Background(), &repocredspkg.RepoCredsQuery{})
errors.CheckError(err)
switch output {

View File

@@ -1,10 +1,10 @@
package commands
import (
"github.com/argoproj/gitops-engine/pkg/utils/errors"
"github.com/spf13/cobra"
"k8s.io/client-go/tools/clientcmd"
"github.com/argoproj/argo-cd/errors"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
"github.com/argoproj/argo-cd/util/cli"
"github.com/argoproj/argo-cd/util/config"
@@ -15,13 +15,9 @@ func init() {
cobra.OnInitialize(initConfig)
}
var (
logFormat string
logLevel string
)
var logLevel string
func initConfig() {
cli.SetLogFormat(logFormat)
cli.SetLogLevel(logLevel)
}
@@ -63,8 +59,6 @@ func NewCommand() *cobra.Command {
command.PersistentFlags().StringVar(&clientOpts.CertFile, "server-crt", config.GetFlag("server-crt", ""), "Server certificate file")
command.PersistentFlags().StringVar(&clientOpts.AuthToken, "auth-token", config.GetFlag("auth-token", ""), "Authentication token")
command.PersistentFlags().BoolVar(&clientOpts.GRPCWeb, "grpc-web", config.GetBoolFlag("grpc-web"), "Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2.")
command.PersistentFlags().StringVar(&clientOpts.GRPCWebRootPath, "grpc-web-root-path", config.GetFlag("grpc-web-root-path", ""), "Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2. Set web root.")
command.PersistentFlags().StringVar(&logFormat, "logformat", config.GetFlag("logformat", "text"), "Set the logging format. One of: text|json")
command.PersistentFlags().StringVar(&logLevel, "loglevel", config.GetFlag("loglevel", "info"), "Set the logging level. One of: debug|info|warn|error")
command.PersistentFlags().StringSliceVarP(&clientOpts.Headers, "header", "H", []string{}, "Sets additional header to all requests made by Argo CD CLI. (Can be repeated multiple times to add multiple headers, also supports comma separated headers)")
command.PersistentFlags().BoolVar(&clientOpts.PortForward, "port-forward", config.GetBoolFlag("port-forward"), "Connect to a random argocd-server port using port forwarding")

View File

@@ -1,3 +0,0 @@
-----BEGIN CERTIFICATE-----
test-cert-data
-----END CERTIFICATE-----

View File

@@ -1,3 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
test-key-data
-----END RSA PRIVATE KEY-----

View File

@@ -3,17 +3,16 @@ package commands
import (
"context"
"fmt"
"io"
"github.com/golang/protobuf/ptypes/empty"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
argoio "github.com/argoproj/gitops-engine/pkg/utils/io"
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/errors"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
"github.com/argoproj/argo-cd/pkg/apiclient/version"
"github.com/argoproj/argo-cd/util"
)
// NewVersionCmd returns a new `version` command to be used as a sub-command to root
@@ -26,7 +25,7 @@ func NewVersionCmd(clientOpts *argocdclient.ClientOptions) *cobra.Command {
versionCmd := cobra.Command{
Use: "version",
Short: "Print version information",
Short: fmt.Sprintf("Print version information"),
Example: ` # Print the full version of client and server to stdout
argocd version
@@ -40,39 +39,44 @@ func NewVersionCmd(clientOpts *argocdclient.ClientOptions) *cobra.Command {
argocd version --short -o yaml
`,
Run: func(cmd *cobra.Command, args []string) {
cv := common.GetVersion()
var (
versionIf version.VersionServiceClient
serverVers *version.VersionMessage
conn io.Closer
err error
)
if !client {
// Get Server version
conn, versionIf = argocdclient.NewClientOrDie(clientOpts).NewVersionClientOrDie()
defer util.Close(conn)
serverVers, err = versionIf.Version(context.Background(), &empty.Empty{})
errors.CheckError(err)
}
switch output {
case "yaml", "json":
v := make(map[string]interface{})
if short {
v["client"] = map[string]string{cliName: cv.Version}
clientVers := common.GetVersion()
version := make(map[string]interface{})
if !short {
version["client"] = clientVers
} else {
v["client"] = cv
version["client"] = map[string]string{cliName: clientVers.Version}
}
if !client {
sv := getServerVersion(clientOpts)
if short {
v["server"] = map[string]string{"argocd-server": sv.Version}
if !short {
version["server"] = serverVers
} else {
v["server"] = sv
version["server"] = map[string]string{"argocd-server": serverVers.Version}
}
}
err := PrintResource(v, output)
err := PrintResource(version, output)
errors.CheckError(err)
case "wide", "short", "":
printClientVersion(&cv, short || (output == "short"))
if !client {
sv := getServerVersion(clientOpts)
printServerVersion(sv, short || (output == "short"))
}
case "short":
printVersion(serverVers, client, true)
case "wide", "":
// we use value of short for backward compatibility
printVersion(serverVers, client, short)
default:
log.Fatalf("unknown output format: %s", output)
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
}
},
}
@@ -82,52 +86,38 @@ func NewVersionCmd(clientOpts *argocdclient.ClientOptions) *cobra.Command {
return &versionCmd
}
func getServerVersion(options *argocdclient.ClientOptions) *version.VersionMessage {
conn, versionIf := argocdclient.NewClientOrDie(options).NewVersionClientOrDie()
defer argoio.Close(conn)
v, err := versionIf.Version(context.Background(), &empty.Empty{})
errors.CheckError(err)
return v
}
func printClientVersion(version *common.Version, short bool) {
func printVersion(serverVers *version.VersionMessage, client bool, short bool) {
version := common.GetVersion()
fmt.Printf("%s: %s\n", cliName, version)
if short {
if !short {
fmt.Printf(" BuildDate: %s\n", version.BuildDate)
fmt.Printf(" GitCommit: %s\n", version.GitCommit)
fmt.Printf(" GitTreeState: %s\n", version.GitTreeState)
if version.GitTag != "" {
fmt.Printf(" GitTag: %s\n", version.GitTag)
}
fmt.Printf(" GoVersion: %s\n", version.GoVersion)
fmt.Printf(" Compiler: %s\n", version.Compiler)
fmt.Printf(" Platform: %s\n", version.Platform)
}
if client {
return
}
fmt.Printf(" BuildDate: %s\n", version.BuildDate)
fmt.Printf(" GitCommit: %s\n", version.GitCommit)
fmt.Printf(" GitTreeState: %s\n", version.GitTreeState)
if version.GitTag != "" {
fmt.Printf(" GitTag: %s\n", version.GitTag)
fmt.Printf("%s: %s\n", "argocd-server", serverVers.Version)
if !short {
fmt.Printf(" BuildDate: %s\n", serverVers.BuildDate)
fmt.Printf(" GitCommit: %s\n", serverVers.GitCommit)
fmt.Printf(" GitTreeState: %s\n", serverVers.GitTreeState)
if version.GitTag != "" {
fmt.Printf(" GitTag: %s\n", serverVers.GitTag)
}
fmt.Printf(" GoVersion: %s\n", serverVers.GoVersion)
fmt.Printf(" Compiler: %s\n", serverVers.Compiler)
fmt.Printf(" Platform: %s\n", serverVers.Platform)
fmt.Printf(" Ksonnet Version: %s\n", serverVers.KsonnetVersion)
fmt.Printf(" Kustomize Version: %s\n", serverVers.KustomizeVersion)
fmt.Printf(" Helm Version: %s\n", serverVers.HelmVersion)
fmt.Printf(" Kubectl Version: %s\n", serverVers.KubectlVersion)
}
fmt.Printf(" GoVersion: %s\n", version.GoVersion)
fmt.Printf(" Compiler: %s\n", version.Compiler)
fmt.Printf(" Platform: %s\n", version.Platform)
}
func printServerVersion(version *version.VersionMessage, short bool) {
fmt.Printf("%s: %s\n", "argocd-server", version.Version)
if short {
return
}
fmt.Printf(" BuildDate: %s\n", version.BuildDate)
fmt.Printf(" GitCommit: %s\n", version.GitCommit)
fmt.Printf(" GitTreeState: %s\n", version.GitTreeState)
if version.GitTag != "" {
fmt.Printf(" GitTag: %s\n", version.GitTag)
}
fmt.Printf(" GoVersion: %s\n", version.GoVersion)
fmt.Printf(" Compiler: %s\n", version.Compiler)
fmt.Printf(" Platform: %s\n", version.Platform)
fmt.Printf(" Ksonnet Version: %s\n", version.KsonnetVersion)
fmt.Printf(" Kustomize Version: %s\n", version.KustomizeVersion)
fmt.Printf(" Helm Version: %s\n", version.HelmVersion)
fmt.Printf(" Kubectl Version: %s\n", version.KubectlVersion)
}

View File

@@ -1,9 +1,8 @@
package main
import (
"github.com/argoproj/gitops-engine/pkg/utils/errors"
commands "github.com/argoproj/argo-cd/cmd/argocd/commands"
"github.com/argoproj/argo-cd/errors"
// load the gcp plugin (required to authenticate against GKE clusters).
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"

View File

@@ -1,11 +1,5 @@
package common
import (
"os"
"strconv"
"time"
)
// Default service addresses and URLS of Argo CD internal services
const (
// DefaultRepoServerAddr is the gRPC address of the Argo CD repo server
@@ -44,6 +38,8 @@ const (
// Default paths on the pod's file system
const (
// The default base path where application config is located
DefaultPathAppConfig = "/app/config"
// The default path where TLS certificates for repositories are located
DefaultPathTLSConfig = "/app/config/tls"
// The default path where SSH known hosts are stored
@@ -66,8 +62,10 @@ const (
AuthCookieName = "argocd.token"
// RevisionHistoryLimit is the max number of successful sync to keep in history
RevisionHistoryLimit = 10
// ChangePasswordSSOTokenMaxAge is the max token age for password change operation
ChangePasswordSSOTokenMaxAge = time.Minute * 5
// K8sClientConfigQPS controls the QPS to be used in K8s REST client configs
K8sClientConfigQPS = 25
// K8sClientConfigBurst controls the burst to be used in K8s REST client configs
K8sClientConfigBurst = 50
)
// Dex related constants
@@ -104,7 +102,14 @@ const (
// AnnotationCompareOptions is a comma-separated list of options for comparison
AnnotationCompareOptions = "argocd.argoproj.io/compare-options"
// AnnotationSyncOptions is a comma-separated list of options for syncing
AnnotationSyncOptions = "argocd.argoproj.io/sync-options"
// AnnotationSyncWave indicates which wave of the sync the resource or hook should be in
AnnotationSyncWave = "argocd.argoproj.io/sync-wave"
// AnnotationKeyHook contains the hook type of a resource
AnnotationKeyHook = "argocd.argoproj.io/hook"
// AnnotationKeyHookDeletePolicy is the policy of deleting a hook
AnnotationKeyHookDeletePolicy = "argocd.argoproj.io/hook-delete-policy"
// AnnotationKeyRefresh is the annotation key which indicates that app needs to be refreshed. Removed by application controller after app is refreshed.
// Might take values 'normal'/'hard'. Value 'hard' means manifest cache and target cluster state cache should be invalidated before refresh.
AnnotationKeyRefresh = "argocd.argoproj.io/refresh"
@@ -133,50 +138,14 @@ const (
EnvGitAttemptsCount = "ARGOCD_GIT_ATTEMPTS_COUNT"
// Overrides git submodule support, true by default
EnvGitSubmoduleEnabled = "ARGOCD_GIT_MODULES_ENABLED"
// EnvK8sClientQPS is the QPS value used for the kubernetes client (default: 50)
EnvK8sClientQPS = "ARGOCD_K8S_CLIENT_QPS"
// EnvK8sClientBurst is the burst value used for the kubernetes client (default: twice the client QPS)
EnvK8sClientBurst = "ARGOCD_K8S_CLIENT_BURST"
// EnvK8sClientMaxIdleConnections is the number of max idle connections in K8s REST client HTTP transport (default: 500)
EnvK8sClientMaxIdleConnections = "ARGOCD_K8S_CLIENT_MAX_IDLE_CONNECTIONS"
)
const (
// MinClientVersion is the minimum client version that can interface with this API server.
// When introducing breaking changes to the API or datastructures, this number should be bumped.
// The value here may be lower than the current value in VERSION
MinClientVersion = "1.4.0"
MinClientVersion = "1.3.0"
// CacheVersion is a objects version cached using util/cache/cache.go.
// Number should be bumped in case of backward incompatible change to make sure cache is invalidated after upgrade.
CacheVersion = "1.0.0"
)
var (
// K8sClientConfigQPS controls the QPS to be used in K8s REST client configs
K8sClientConfigQPS float32 = 50
// K8sClientConfigBurst controls the burst to be used in K8s REST client configs
K8sClientConfigBurst int = 100
// K8sMaxIdleConnections controls the number of max idle connections in K8s REST client HTTP transport
K8sMaxIdleConnections = 500
)
func init() {
if envQPS := os.Getenv(EnvK8sClientQPS); envQPS != "" {
if qps, err := strconv.ParseFloat(envQPS, 32); err != nil {
K8sClientConfigQPS = float32(qps)
}
}
if envBurst := os.Getenv(EnvK8sClientBurst); envBurst != "" {
if burst, err := strconv.Atoi(envBurst); err != nil {
K8sClientConfigBurst = burst
}
} else {
K8sClientConfigBurst = 2 * int(K8sClientConfigQPS)
}
if envMaxConn := os.Getenv(EnvK8sClientMaxIdleConnections); envMaxConn != "" {
if maxConn, err := strconv.Atoi(envMaxConn); err != nil {
K8sMaxIdleConnections = maxConn
}
}
}

View File

@@ -12,12 +12,6 @@ import (
"sync"
"time"
"github.com/argoproj/gitops-engine/pkg/diff"
"github.com/argoproj/gitops-engine/pkg/health"
synccommon "github.com/argoproj/gitops-engine/pkg/sync/common"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
"github.com/argoproj/gitops-engine/pkg/utils/io"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
log "github.com/sirupsen/logrus"
"golang.org/x/sync/semaphore"
v1 "k8s.io/api/core/v1"
@@ -37,6 +31,7 @@ import (
"github.com/argoproj/argo-cd/common"
statecache "github.com/argoproj/argo-cd/controller/cache"
"github.com/argoproj/argo-cd/controller/metrics"
"github.com/argoproj/argo-cd/errors"
"github.com/argoproj/argo-cd/pkg/apis/application"
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
@@ -44,9 +39,12 @@ import (
"github.com/argoproj/argo-cd/pkg/client/informers/externalversions/application/v1alpha1"
applisters "github.com/argoproj/argo-cd/pkg/client/listers/application/v1alpha1"
"github.com/argoproj/argo-cd/reposerver/apiclient"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/argo"
appstatecache "github.com/argoproj/argo-cd/util/cache/appstate"
"github.com/argoproj/argo-cd/util/db"
"github.com/argoproj/argo-cd/util/diff"
"github.com/argoproj/argo-cd/util/kube"
settings_util "github.com/argoproj/argo-cd/util/settings"
)
@@ -169,11 +167,7 @@ func NewApplicationController(
return &ctrl, nil
}
func (ctrl *ApplicationController) GetMetricsServer() *metrics.MetricsServer {
return ctrl.metricsServer
}
func (ctrl *ApplicationController) onKubectlRun(command string) (io.Closer, error) {
func (ctrl *ApplicationController) onKubectlRun(command string) (util.Closer, error) {
ctrl.metricsServer.IncKubectlExec(command)
if ctrl.kubectlSemaphore != nil {
if err := ctrl.kubectlSemaphore.Acquire(context.Background(), 1); err != nil {
@@ -181,7 +175,7 @@ func (ctrl *ApplicationController) onKubectlRun(command string) (io.Closer, erro
}
ctrl.metricsServer.IncKubectlExecPending(command)
}
return io.NewCloser(func() error {
return util.NewCloser(func() error {
if ctrl.kubectlSemaphore != nil {
ctrl.kubectlSemaphore.Release(1)
ctrl.metricsServer.DecKubectlExecPending(command)
@@ -365,11 +359,7 @@ func (ctrl *ApplicationController) managedResources(comparisonResult *comparison
if err != nil {
return nil, err
}
compareOptions, err := ctrl.settingsMgr.GetResourceCompareOptions()
if err != nil {
return nil, err
}
resDiffPtr, err := diff.Diff(target, live, comparisonResult.diffNormalizer, compareOptions)
resDiffPtr, err := diff.Diff(target, live, comparisonResult.diffNormalizer)
if err != nil {
return nil, err
}
@@ -395,6 +385,11 @@ func (ctrl *ApplicationController) managedResources(comparisonResult *comparison
} else {
item.TargetState = "null"
}
jsonDiff, err := resDiff.JSONFormat()
if err != nil {
return nil, err
}
item.Diff = jsonDiff
item.PredictedLiveState = string(resDiff.PredictedLive)
item.NormalizedLiveState = string(resDiff.NormalizedLive)
@@ -414,8 +409,6 @@ func (ctrl *ApplicationController) Run(ctx context.Context, statusProcessors int
go ctrl.appInformer.Run(ctx.Done())
go ctrl.projInformer.Run(ctx.Done())
errors.CheckError(ctrl.stateCache.Init())
if !cache.WaitForCacheSync(ctx.Done(), ctrl.appInformer.HasSynced, ctrl.projInformer.HasSynced) {
log.Error("Timed out waiting for caches to sync")
return
@@ -546,7 +539,7 @@ func (ctrl *ApplicationController) processAppComparisonTypeQueueItem() (processN
return
}
// shouldBeDeleted returns whether a given resource obj should be deleted on cascade delete of application app
// shouldbeDeleted returns whether a given resource obj should be deleted on cascade delete of application app
func (ctrl *ApplicationController) shouldBeDeleted(app *appv1.Application, obj *unstructured.Unstructured) bool {
return !kube.IsCRD(obj) && !isSelfReferencedApp(app, kube.GetObjectRef(obj))
}
@@ -599,7 +592,7 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic
}
config := metrics.AddMetricsTransportWrapper(ctrl.metricsServer, app, cluster.RESTConfig())
err = kube.RunAllAsync(len(objs), func(i int) error {
err = util.RunAllAsync(len(objs), func(i int) error {
obj := objs[i]
return ctrl.kubectl.DeleteResource(config, obj.GroupVersionKind(), obj.GetName(), obj.GetNamespace(), false)
})
@@ -669,7 +662,7 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli
defer func() {
if r := recover(); r != nil {
logCtx.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
state.Phase = synccommon.OperationError
state.Phase = appv1.OperationError
if rerr, ok := r.(error); ok {
state.Message = rerr.Error()
} else {
@@ -696,20 +689,20 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli
state = app.Status.OperationState.DeepCopy()
logCtx.Infof("Resuming in-progress operation. phase: %s, message: %s", state.Phase, state.Message)
} else {
state = &appv1.OperationState{Phase: synccommon.OperationRunning, Operation: *app.Operation, StartedAt: metav1.Now()}
state = &appv1.OperationState{Phase: appv1.OperationRunning, Operation: *app.Operation, StartedAt: metav1.Now()}
ctrl.setOperationState(app, state)
logCtx.Infof("Initialized new operation: %v", *app.Operation)
}
ctrl.appStateManager.SyncAppState(app, state)
if state.Phase == synccommon.OperationRunning {
if state.Phase == appv1.OperationRunning {
// It's possible for an app to be terminated while we were operating on it. We do not want
// to clobber the Terminated state with Running. Get the latest app state to check for this.
freshApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(ctrl.namespace).Get(app.ObjectMeta.Name, metav1.GetOptions{})
if err == nil {
if freshApp.Status.OperationState != nil && freshApp.Status.OperationState.Phase == synccommon.OperationTerminating {
state.Phase = synccommon.OperationTerminating
if freshApp.Status.OperationState != nil && freshApp.Status.OperationState.Phase == appv1.OperationTerminating {
state.Phase = appv1.OperationTerminating
state.Message = "operation is terminating"
// after this, we will get requeued to the workqueue, but next time the
// SyncAppState will operate in a Terminating phase, allowing the worker to perform
@@ -732,7 +725,7 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli
}
func (ctrl *ApplicationController) setOperationState(app *appv1.Application, state *appv1.OperationState) {
kube.RetryUntilSucceed(func() error {
util.RetryUntilSucceed(func() error {
if state.Phase == "" {
// expose any bugs where we neglect to set phase
panic("no phase was set")
@@ -828,20 +821,22 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
return
}
app := origApp.DeepCopy()
logCtx := log.WithFields(log.Fields{"application": app.Name})
startTime := time.Now()
defer func() {
reconcileDuration := time.Since(startTime)
ctrl.metricsServer.IncReconcile(origApp, reconcileDuration)
logCtx.WithFields(log.Fields{
"time_ms": reconcileDuration.Milliseconds(),
logCtx := log.WithFields(log.Fields{
"application": origApp.Name,
"time_ms": reconcileDuration.Seconds() * 1e3,
"level": comparisonLevel,
"dest-server": origApp.Spec.Destination.Server,
"dest-namespace": origApp.Spec.Destination.Namespace,
}).Info("Reconciliation completed")
})
logCtx.Info("Reconciliation completed")
}()
app := origApp.DeepCopy()
logCtx := log.WithFields(log.Fields{"application": app.Name})
if comparisonLevel == ComparisonWithNothing {
managedResources := make([]*appv1.ResourceDiff, 0)
if err := ctrl.cache.GetAppManagedResources(app.Name, &managedResources); err != nil {
@@ -876,7 +871,7 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
project, hasErrors := ctrl.refreshAppConditions(app)
if hasErrors {
app.Status.Sync.Status = appv1.SyncStatusCodeUnknown
app.Status.Health.Status = health.HealthStatusUnknown
app.Status.Health.Status = appv1.HealthStatusUnknown
ctrl.persistAppStatus(origApp, &app.Status)
return
}
@@ -893,9 +888,6 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
observedAt := metav1.Now()
compareResult := ctrl.appStateManager.CompareAppState(app, project, revision, app.Spec.Source, refreshType == appv1.RefreshTypeHard, localManifests)
for k, v := range compareResult.timings {
logCtx = logCtx.WithField(k, v.Milliseconds())
}
ctrl.normalizeApplication(origApp, app)
@@ -920,7 +912,7 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
)
}
} else {
logCtx.Info("Sync prevented by sync window")
logCtx.Infof("Sync prevented by sync window")
}
if app.Status.ReconciledAt == nil || comparisonLevel == CompareWithLatest {
@@ -965,7 +957,7 @@ func (ctrl *ApplicationController) needRefreshAppStatus(app *appv1.Application,
reason = "spec.destination differs"
} else if requested, level := ctrl.isRefreshRequested(app.Name); requested {
compareWith = level
reason = "controller refresh requested"
reason = fmt.Sprintf("controller refresh requested")
}
if reason != "" {
@@ -1088,30 +1080,14 @@ func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus *
return nil
}
if !app.Spec.SyncPolicy.Automated.Prune {
requirePruneOnly := true
for _, r := range resources {
if r.Status != appv1.SyncStatusCodeSynced && !r.RequiresPruning {
requirePruneOnly = false
break
}
}
if requirePruneOnly {
logCtx.Infof("Skipping auto-sync: need to prune extra resources only but automated prune is disabled")
return nil
}
}
desiredCommitSHA := syncStatus.Revision
alreadyAttempted, attemptPhase := alreadyAttemptedSync(app, desiredCommitSHA)
selfHeal := app.Spec.SyncPolicy.Automated.SelfHeal
op := appv1.Operation{
Sync: &appv1.SyncOperation{
Revision: desiredCommitSHA,
Prune: app.Spec.SyncPolicy.Automated.Prune,
SyncOptions: app.Spec.SyncPolicy.SyncOptions,
Revision: desiredCommitSHA,
Prune: app.Spec.SyncPolicy.Automated.Prune,
},
InitiatedBy: appv1.OperationInitiator{Automated: true},
}
// It is possible for manifests to remain OutOfSync even after a sync/kubectl apply (e.g.
// auto-sync with pruning disabled). We need to ensure that we do not keep Syncing an
@@ -1158,7 +1134,7 @@ func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus *
// alreadyAttemptedSync returns whether or not the most recent sync was performed against the
// commitSHA and with the same app source config which are currently set in the app
func alreadyAttemptedSync(app *appv1.Application, commitSHA string) (bool, synccommon.OperationPhase) {
func alreadyAttemptedSync(app *appv1.Application, commitSHA string) (bool, appv1.OperationPhase) {
if app.Status.OperationState == nil || app.Status.OperationState.Operation.Sync == nil || app.Status.OperationState.SyncResult == nil {
return false, ""
}

View File

@@ -6,10 +6,6 @@ import (
"testing"
"time"
"github.com/argoproj/gitops-engine/pkg/cache/mocks"
synccommon "github.com/argoproj/gitops-engine/pkg/sync/common"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
"github.com/argoproj/gitops-engine/pkg/utils/kube/kubetest"
"github.com/ghodss/yaml"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@@ -29,9 +25,12 @@ import (
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/fake"
"github.com/argoproj/argo-cd/reposerver/apiclient"
mockrepoclient "github.com/argoproj/argo-cd/reposerver/apiclient/mocks"
mockreposerver "github.com/argoproj/argo-cd/reposerver/mocks"
"github.com/argoproj/argo-cd/test"
cacheutil "github.com/argoproj/argo-cd/util/cache"
appstatecache "github.com/argoproj/argo-cd/util/cache/appstate"
"github.com/argoproj/argo-cd/util/kube"
"github.com/argoproj/argo-cd/util/kube/kubetest"
"github.com/argoproj/argo-cd/util/settings"
)
@@ -58,7 +57,7 @@ func newFakeController(data *fakeData) *ApplicationController {
// Mock out call to GenerateManifest
mockRepoClient := mockrepoclient.RepoServerServiceClient{}
mockRepoClient.On("GenerateManifest", mock.Anything, mock.Anything).Return(data.manifestResponse, nil)
mockRepoClientset := mockrepoclient.Clientset{}
mockRepoClientset := mockreposerver.Clientset{}
mockRepoClientset.On("NewRepoServerClient").Return(&fakeCloser{}, &mockRepoClient, nil)
secret := corev1.Secret{
@@ -107,21 +106,17 @@ func newFakeController(data *fakeData) *ApplicationController {
defer cancelProj()
cancelApp := test.StartInformer(ctrl.appInformer)
defer cancelApp()
clusterCacheMock := mocks.ClusterCache{}
clusterCacheMock.On("IsNamespaced", mock.Anything).Return(true, nil)
mockStateCache := mockstatecache.LiveStateCache{}
ctrl.appStateManager.(*appStateManager).liveStateCache = &mockStateCache
ctrl.stateCache = &mockStateCache
mockStateCache.On("IsNamespaced", mock.Anything, mock.Anything).Return(true, nil)
mockStateCache.On("GetManagedLiveObjs", mock.Anything, mock.Anything).Return(data.managedLiveObjs, nil)
mockStateCache.On("GetVersionsInfo", mock.Anything).Return("v1.2.3", nil, nil)
mockStateCache.On("GetServerVersion", mock.Anything).Return("v1.2.3", nil)
response := make(map[kube.ResourceKey]argoappv1.ResourceNode)
for k, v := range data.namespacedResources {
response[k] = v.ResourceNode
}
mockStateCache.On("GetNamespaceTopLevelResources", mock.Anything, mock.Anything).Return(response, nil)
mockStateCache.On("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))
@@ -233,7 +228,7 @@ func TestAutoSync(t *testing.T) {
Status: argoappv1.SyncStatusCodeOutOfSync,
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
}
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: argoappv1.SyncStatusCodeOutOfSync}})
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{})
assert.Nil(t, cond)
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
assert.NoError(t, err)
@@ -245,7 +240,7 @@ func TestAutoSync(t *testing.T) {
func TestSkipAutoSync(t *testing.T) {
// Verify we skip when we previously synced to it in our most recent history
// Set current to 'aaaaa', desired to 'aaaa' and mark system OutOfSync
t.Run("PreviouslySyncedToRevision", func(t *testing.T) {
{
app := newFakeApp()
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
syncStatus := argoappv1.SyncStatus{
@@ -257,10 +252,10 @@ func TestSkipAutoSync(t *testing.T) {
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
assert.NoError(t, err)
assert.Nil(t, app.Operation)
})
}
// Verify we skip when we are already Synced (even if revision is different)
t.Run("AlreadyInSyncedState", func(t *testing.T) {
{
app := newFakeApp()
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
syncStatus := argoappv1.SyncStatus{
@@ -272,10 +267,10 @@ func TestSkipAutoSync(t *testing.T) {
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
assert.NoError(t, err)
assert.Nil(t, app.Operation)
})
}
// Verify we skip when auto-sync is disabled
t.Run("AutoSyncIsDisabled", func(t *testing.T) {
{
app := newFakeApp()
app.Spec.SyncPolicy = nil
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
@@ -288,10 +283,10 @@ func TestSkipAutoSync(t *testing.T) {
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
assert.NoError(t, err)
assert.Nil(t, app.Operation)
})
}
// Verify we skip when application is marked for deletion
t.Run("ApplicationIsMarkedForDeletion", func(t *testing.T) {
{
app := newFakeApp()
now := metav1.Now()
app.DeletionTimestamp = &now
@@ -305,17 +300,17 @@ func TestSkipAutoSync(t *testing.T) {
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
assert.NoError(t, err)
assert.Nil(t, app.Operation)
})
}
// Verify we skip when previous sync attempt failed and return error condition
// Set current to 'aaaaa', desired to 'bbbbb' and add 'bbbbb' to failure history
t.Run("PreviousSyncAttemptFailed", func(t *testing.T) {
{
app := newFakeApp()
app.Status.OperationState = &argoappv1.OperationState{
Operation: argoappv1.Operation{
Sync: &argoappv1.SyncOperation{},
},
Phase: synccommon.OperationFailed,
Phase: argoappv1.OperationFailed,
SyncResult: &argoappv1.SyncOperationResult{
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
Source: *app.Spec.Source.DeepCopy(),
@@ -326,28 +321,12 @@ func TestSkipAutoSync(t *testing.T) {
Status: argoappv1.SyncStatusCodeOutOfSync,
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
}
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: argoappv1.SyncStatusCodeOutOfSync}})
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{})
assert.NotNil(t, cond)
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
assert.NoError(t, err)
assert.Nil(t, app.Operation)
})
t.Run("NeedsToPruneResourcesOnlyButAutomatedPruneDisabled", func(t *testing.T) {
app := newFakeApp()
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
syncStatus := argoappv1.SyncStatus{
Status: argoappv1.SyncStatusCodeOutOfSync,
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
}
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{
{Name: "guestbook", Kind: kube.DeploymentKind, Status: argoappv1.SyncStatusCodeOutOfSync, RequiresPruning: true},
})
assert.Nil(t, cond)
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
assert.NoError(t, err)
assert.Nil(t, app.Operation)
})
}
}
// TestAutoSyncIndicateError verifies we skip auto-sync and return error condition if previous sync failed
@@ -372,13 +351,13 @@ func TestAutoSyncIndicateError(t *testing.T) {
Source: app.Spec.Source.DeepCopy(),
},
},
Phase: synccommon.OperationFailed,
Phase: argoappv1.OperationFailed,
SyncResult: &argoappv1.SyncOperationResult{
Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Source: *app.Spec.Source.DeepCopy(),
},
}
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: argoappv1.SyncStatusCodeOutOfSync}})
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{})
assert.NotNil(t, cond)
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
assert.NoError(t, err)
@@ -416,12 +395,12 @@ func TestAutoSyncParameterOverrides(t *testing.T) {
},
},
},
Phase: synccommon.OperationFailed,
Phase: argoappv1.OperationFailed,
SyncResult: &argoappv1.SyncOperationResult{
Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
},
}
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{{Name: "guestbook", Kind: kube.DeploymentKind, Status: argoappv1.SyncStatusCodeOutOfSync}})
cond := ctrl.autoSync(app, &syncStatus, []argoappv1.ResourceStatus{})
assert.Nil(t, cond)
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
assert.NoError(t, err)
@@ -470,7 +449,7 @@ func TestFinalizeAppDeletion(t *testing.T) {
assert.True(t, patched)
}
// Ensure any stray resources irregularly labeled with instance label of app are not deleted upon deleting,
// Ensure any stray resources irregulary labeled with instance label of app are not deleted upon deleting,
// when app project restriction is in place
{
defaultProj := argoappv1.AppProject{
@@ -671,7 +650,7 @@ func TestSetOperationStateOnDeletedApp(t *testing.T) {
patched = true
return true, nil, apierr.NewNotFound(schema.GroupResource{}, "my-app")
})
ctrl.setOperationState(newFakeApp(), &argoappv1.OperationState{Phase: synccommon.OperationSucceeded})
ctrl.setOperationState(newFakeApp(), &argoappv1.OperationState{Phase: argoappv1.OperationSucceeded})
assert.True(t, patched)
}

View File

@@ -5,12 +5,8 @@ import (
"reflect"
"sync"
clustercache "github.com/argoproj/gitops-engine/pkg/cache"
"github.com/argoproj/gitops-engine/pkg/health"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
log "github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/watch"
@@ -18,18 +14,23 @@ import (
"github.com/argoproj/argo-cd/controller/metrics"
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/db"
"github.com/argoproj/argo-cd/util/lua"
"github.com/argoproj/argo-cd/util/kube"
"github.com/argoproj/argo-cd/util/settings"
)
type cacheSettings struct {
ResourceOverrides map[string]appv1.ResourceOverride
AppInstanceLabelKey string
ResourcesFilter *settings.ResourcesFilter
}
type LiveStateCache interface {
// Returns k8s server version
GetVersionsInfo(serverURL string) (string, []metav1.APIGroup, error)
GetServerVersion(serverURL string) (string, error)
// Returns true of given group kind is a namespaced resource
IsNamespaced(server string, gk schema.GroupKind) (bool, error)
// Returns synced cluster cache
GetClusterCache(server string) (clustercache.ClusterCache, error)
// Executes give callback against resource specified by the key and all its children
IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string)) error
// Returns state of live nodes which correspond for target nodes of specified application.
@@ -38,21 +39,23 @@ type LiveStateCache interface {
GetNamespaceTopLevelResources(server string, namespace string) (map[kube.ResourceKey]appv1.ResourceNode, error)
// Starts watching resources of each controlled cluster.
Run(ctx context.Context) error
// Invalidate invalidates the entire cluster state cache
Invalidate()
// Returns information about monitored clusters
GetClustersInfo() []clustercache.ClusterInfo
// Init must be executed before cache can be used
Init() error
GetClustersInfo() []metrics.ClusterInfo
}
type ObjectUpdatedHandler = func(managedByApp map[string]bool, ref v1.ObjectReference)
type ResourceInfo struct {
Info []appv1.InfoItem
AppName string
// networkingInfo are available only for known types involved into networking: Ingress, Service, Pod
NetworkingInfo *appv1.ResourceNetworkingInfo
Images []string
Health *health.HealthStatus
func GetTargetObjKey(a *appv1.Application, un *unstructured.Unstructured, isNamespaced bool) kube.ResourceKey {
key := kube.GetResourceKey(un)
if !isNamespaced {
key.Namespace = ""
} else if isNamespaced && key.Namespace == "" {
key.Namespace = a.Spec.Destination.Namespace
}
return key
}
func NewLiveStateCache(
@@ -64,32 +67,29 @@ func NewLiveStateCache(
onObjectUpdated ObjectUpdatedHandler) LiveStateCache {
return &liveStateCache{
appInformer: appInformer,
db: db,
clusters: make(map[string]clustercache.ClusterCache),
onObjectUpdated: onObjectUpdated,
kubectl: kubectl,
settingsMgr: settingsMgr,
metricsServer: metricsServer,
appInformer: appInformer,
db: db,
clusters: make(map[string]*clusterInfo),
lock: &sync.Mutex{},
onObjectUpdated: onObjectUpdated,
kubectl: kubectl,
settingsMgr: settingsMgr,
metricsServer: metricsServer,
cacheSettingsLock: &sync.Mutex{},
}
}
type cacheSettings struct {
clusterSettings clustercache.Settings
appInstanceLabelKey string
}
type liveStateCache struct {
db db.ArgoDB
appInformer cache.SharedIndexInformer
onObjectUpdated ObjectUpdatedHandler
kubectl kube.Kubectl
settingsMgr *settings.SettingsManager
metricsServer *metrics.MetricsServer
clusters map[string]clustercache.ClusterCache
cacheSettings cacheSettings
lock sync.RWMutex
db db.ArgoDB
clusters map[string]*clusterInfo
lock *sync.Mutex
appInformer cache.SharedIndexInformer
onObjectUpdated ObjectUpdatedHandler
kubectl kube.Kubectl
settingsMgr *settings.SettingsManager
metricsServer *metrics.MetricsServer
cacheSettingsLock *sync.Mutex
cacheSettings *cacheSettings
}
func (c *liveStateCache) loadCacheSettings() (*cacheSettings, error) {
@@ -105,198 +105,57 @@ func (c *liveStateCache) loadCacheSettings() (*cacheSettings, error) {
if err != nil {
return nil, err
}
clusterSettings := clustercache.Settings{
ResourceHealthOverride: lua.ResourceHealthOverrides(resourceOverrides),
ResourcesFilter: resourcesFilter,
}
return &cacheSettings{clusterSettings, appInstanceLabelKey}, nil
return &cacheSettings{AppInstanceLabelKey: appInstanceLabelKey, ResourceOverrides: resourceOverrides, ResourcesFilter: resourcesFilter}, nil
}
func asResourceNode(r *clustercache.Resource) appv1.ResourceNode {
gv, err := schema.ParseGroupVersion(r.Ref.APIVersion)
if err != nil {
gv = schema.GroupVersion{}
}
parentRefs := make([]appv1.ResourceRef, len(r.OwnerRefs))
for _, ownerRef := range r.OwnerRefs {
ownerGvk := schema.FromAPIVersionAndKind(ownerRef.APIVersion, ownerRef.Kind)
ownerKey := kube.NewResourceKey(ownerGvk.Group, ownerRef.Kind, r.Ref.Namespace, ownerRef.Name)
parentRefs[0] = appv1.ResourceRef{Name: ownerRef.Name, Kind: ownerKey.Kind, Namespace: r.Ref.Namespace, Group: ownerKey.Group, UID: string(ownerRef.UID)}
}
var resHealth *appv1.HealthStatus
resourceInfo := resInfo(r)
if resourceInfo.Health != nil {
resHealth = &appv1.HealthStatus{Status: resourceInfo.Health.Status, Message: resourceInfo.Health.Message}
}
return appv1.ResourceNode{
ResourceRef: appv1.ResourceRef{
UID: string(r.Ref.UID),
Name: r.Ref.Name,
Group: gv.Group,
Version: gv.Version,
Kind: r.Ref.Kind,
Namespace: r.Ref.Namespace,
},
ParentRefs: parentRefs,
Info: resourceInfo.Info,
ResourceVersion: r.ResourceVersion,
NetworkingInfo: resourceInfo.NetworkingInfo,
Images: resourceInfo.Images,
Health: resHealth,
}
}
func resInfo(r *clustercache.Resource) *ResourceInfo {
info, ok := r.Info.(*ResourceInfo)
if !ok || info == nil {
info = &ResourceInfo{}
}
return info
}
func isRootAppNode(r *clustercache.Resource) bool {
return resInfo(r).AppName != "" && len(r.OwnerRefs) == 0
}
func getApp(r *clustercache.Resource, ns map[kube.ResourceKey]*clustercache.Resource) string {
return getAppRecursive(r, ns, map[kube.ResourceKey]bool{})
}
func ownerRefGV(ownerRef metav1.OwnerReference) schema.GroupVersion {
gv, err := schema.ParseGroupVersion(ownerRef.APIVersion)
if err != nil {
gv = schema.GroupVersion{}
}
return gv
}
func getAppRecursive(r *clustercache.Resource, ns map[kube.ResourceKey]*clustercache.Resource, visited map[kube.ResourceKey]bool) string {
if !visited[r.ResourceKey()] {
visited[r.ResourceKey()] = true
} else {
log.Warnf("Circular dependency detected: %v.", visited)
return resInfo(r).AppName
}
if resInfo(r).AppName != "" {
return resInfo(r).AppName
}
for _, ownerRef := range r.OwnerRefs {
gv := ownerRefGV(ownerRef)
if parent, ok := ns[kube.NewResourceKey(gv.Group, ownerRef.Kind, r.Ref.Namespace, ownerRef.Name)]; ok {
app := getAppRecursive(parent, ns, visited)
if app != "" {
return app
}
}
}
return ""
}
var (
ignoredRefreshResources = map[string]bool{
"/" + kube.EndpointsKind: true,
}
)
// skipAppRequeuing checks if the object is an API type which we want to skip requeuing against.
// We ignore API types which have a high churn rate, and/or whose updates are irrelevant to the app
func skipAppRequeuing(key kube.ResourceKey) bool {
return ignoredRefreshResources[key.Group+"/"+key.Kind]
}
func (c *liveStateCache) getCluster(server string) (clustercache.ClusterCache, error) {
c.lock.RLock()
clusterCache, ok := c.clusters[server]
cacheSettings := c.cacheSettings
c.lock.RUnlock()
if ok {
return clusterCache, nil
}
func (c *liveStateCache) getCluster(server string) (*clusterInfo, error) {
c.lock.Lock()
defer c.lock.Unlock()
clusterCache, ok = c.clusters[server]
if ok {
return clusterCache, nil
}
cluster, err := c.db.GetCluster(context.Background(), server)
if err != nil {
return nil, err
}
clusterCache = clustercache.NewClusterCache(cluster.RESTConfig(),
clustercache.SetSettings(cacheSettings.clusterSettings),
clustercache.SetNamespaces(cluster.Namespaces),
clustercache.SetPopulateResourceInfoHandler(func(un *unstructured.Unstructured, isRoot bool) (interface{}, bool) {
res := &ResourceInfo{}
populateNodeInfo(un, res)
res.Health, _ = health.GetResourceHealth(un, cacheSettings.clusterSettings.ResourceHealthOverride)
appName := kube.GetAppInstanceLabel(un, cacheSettings.appInstanceLabelKey)
if isRoot && appName != "" {
res.AppName = appName
}
// edge case. we do not label CRDs, so they miss the tracking label we inject. But we still
// want the full resource to be available in our cache (to diff), so we store all CRDs
return res, res.AppName != "" || un.GroupVersionKind().Kind == kube.CustomResourceDefinitionKind
}),
)
_ = clusterCache.OnResourceUpdated(func(newRes *clustercache.Resource, oldRes *clustercache.Resource, namespaceResources map[kube.ResourceKey]*clustercache.Resource) {
toNotify := make(map[string]bool)
var ref v1.ObjectReference
if newRes != nil {
ref = newRes.Ref
} else {
ref = oldRes.Ref
info, ok := c.clusters[server]
if !ok {
cluster, err := c.db.GetCluster(context.Background(), server)
if err != nil {
return nil, err
}
for _, r := range []*clustercache.Resource{newRes, oldRes} {
if r == nil {
continue
}
app := getApp(r, namespaceResources)
if app == "" || skipAppRequeuing(r.ResourceKey()) {
continue
}
toNotify[app] = isRootAppNode(r) || toNotify[app]
info = &clusterInfo{
apisMeta: make(map[schema.GroupKind]*apiMeta),
lock: &sync.Mutex{},
nodes: make(map[kube.ResourceKey]*node),
nsIndex: make(map[string]map[kube.ResourceKey]*node),
onObjectUpdated: c.onObjectUpdated,
kubectl: c.kubectl,
cluster: cluster,
syncTime: nil,
log: log.WithField("server", cluster.Server),
cacheSettingsSrc: c.getCacheSettings,
onEventReceived: func(event watch.EventType, un *unstructured.Unstructured) {
c.metricsServer.IncClusterEventsCount(cluster.Server)
},
}
c.onObjectUpdated(toNotify, ref)
})
_ = clusterCache.OnEvent(func(event watch.EventType, un *unstructured.Unstructured) {
gvk := un.GroupVersionKind()
c.metricsServer.IncClusterEventsCount(cluster.Server, gvk.Group, gvk.Kind)
})
c.clusters[cluster.Server] = clusterCache
return clusterCache, nil
c.clusters[cluster.Server] = info
}
return info, nil
}
func (c *liveStateCache) getSyncedCluster(server string) (clustercache.ClusterCache, error) {
clusterCache, err := c.getCluster(server)
func (c *liveStateCache) getSyncedCluster(server string) (*clusterInfo, error) {
info, err := c.getCluster(server)
if err != nil {
return nil, err
}
err = clusterCache.EnsureSynced()
err = info.ensureSynced()
if err != nil {
return nil, err
}
return clusterCache, nil
return info, nil
}
func (c *liveStateCache) invalidate(cacheSettings cacheSettings) {
func (c *liveStateCache) Invalidate() {
log.Info("invalidating live state cache")
c.lock.Lock()
defer c.lock.Unlock()
c.cacheSettings = cacheSettings
for _, clust := range c.clusters {
clust.Invalidate(clustercache.SetSettings(cacheSettings.clusterSettings))
clust.invalidate()
}
log.Info("live state cache invalidated")
}
@@ -306,7 +165,7 @@ func (c *liveStateCache) IsNamespaced(server string, gk schema.GroupKind) (bool,
if err != nil {
return false, err
}
return clusterInfo.IsNamespaced(gk)
return clusterInfo.isNamespaced(gk), nil
}
func (c *liveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string)) error {
@@ -314,9 +173,7 @@ func (c *liveStateCache) IterateHierarchy(server string, key kube.ResourceKey, a
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, action)
return nil
}
@@ -325,12 +182,7 @@ func (c *liveStateCache) GetNamespaceTopLevelResources(server string, namespace
if err != nil {
return nil, err
}
resources := clusterInfo.GetNamespaceTopLevelResources(namespace)
res := make(map[kube.ResourceKey]appv1.ResourceNode)
for k, r := range resources {
res[k] = asResourceNode(r)
}
return res, nil
return clusterInfo.getNamespaceTopLevelResources(namespace), nil
}
func (c *liveStateCache) GetManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
@@ -338,17 +190,14 @@ func (c *liveStateCache) GetManagedLiveObjs(a *appv1.Application, targetObjs []*
if err != nil {
return nil, err
}
return clusterInfo.GetManagedLiveObjs(targetObjs, func(r *clustercache.Resource) bool {
return resInfo(r).AppName == a.Name
})
return clusterInfo.getManagedLiveObjs(a, targetObjs, c.metricsServer)
}
func (c *liveStateCache) GetVersionsInfo(serverURL string) (string, []metav1.APIGroup, error) {
func (c *liveStateCache) GetServerVersion(serverURL string) (string, error) {
clusterInfo, err := c.getSyncedCluster(serverURL)
if err != nil {
return "", nil, err
return "", err
}
return clusterInfo.GetServerVersion(), clusterInfo.GetAPIGroups(), nil
return clusterInfo.serverVersion, nil
}
func isClusterHasApps(apps []interface{}, cluster *appv1.Cluster) bool {
@@ -360,6 +209,12 @@ func isClusterHasApps(apps []interface{}, cluster *appv1.Cluster) bool {
return false
}
func (c *liveStateCache) getCacheSettings() *cacheSettings {
c.cacheSettingsLock.Lock()
defer c.cacheSettingsLock.Unlock()
return c.cacheSettings
}
func (c *liveStateCache) watchSettings(ctx context.Context) {
updateCh := make(chan *settings.ArgoCDSettings, 1)
c.settingsMgr.Subscribe(updateCh)
@@ -374,15 +229,15 @@ func (c *liveStateCache) watchSettings(ctx context.Context) {
continue
}
c.lock.Lock()
c.cacheSettingsLock.Lock()
needInvalidate := false
if !reflect.DeepEqual(c.cacheSettings, *nextCacheSettings) {
c.cacheSettings = *nextCacheSettings
if !reflect.DeepEqual(c.cacheSettings, nextCacheSettings) {
c.cacheSettings = nextCacheSettings
needInvalidate = true
}
c.lock.Unlock()
c.cacheSettingsLock.Unlock()
if needInvalidate {
c.invalidate(*nextCacheSettings)
c.Invalidate()
}
case <-ctx.Done():
done = true
@@ -393,61 +248,50 @@ func (c *liveStateCache) watchSettings(ctx context.Context) {
close(updateCh)
}
func (c *liveStateCache) Init() error {
// Run watches for resource changes annotated with application label on all registered clusters and schedule corresponding app refresh.
func (c *liveStateCache) Run(ctx context.Context) error {
cacheSettings, err := c.loadCacheSettings()
if err != nil {
return err
}
c.cacheSettings = *cacheSettings
return nil
}
c.cacheSettings = cacheSettings
// Run watches for resource changes annotated with application label on all registered clusters and schedule corresponding app refresh.
func (c *liveStateCache) Run(ctx context.Context) error {
go c.watchSettings(ctx)
kube.RetryUntilSucceed(func() error {
util.RetryUntilSucceed(func() error {
clusterEventCallback := func(event *db.ClusterEvent) {
c.lock.Lock()
cluster, ok := c.clusters[event.Cluster.Server]
if ok {
defer c.lock.Unlock()
defer c.lock.Unlock()
if cluster, ok := c.clusters[event.Cluster.Server]; ok {
if event.Type == watch.Deleted {
cluster.Invalidate()
cluster.invalidate()
delete(c.clusters, event.Cluster.Server)
} else if event.Type == watch.Modified {
cluster.Invalidate(clustercache.SetConfig(event.Cluster.RESTConfig()))
}
} else {
c.lock.Unlock()
if event.Type == watch.Added && isClusterHasApps(c.appInformer.GetStore().List(), event.Cluster) {
go func() {
// warm up cache for cluster with apps
_, _ = c.getSyncedCluster(event.Cluster.Server)
}()
cluster.cluster = event.Cluster
cluster.invalidate()
}
} else if event.Type == watch.Added && isClusterHasApps(c.appInformer.GetStore().List(), event.Cluster) {
go func() {
// warm up cache for cluster with apps
_, _ = c.getSyncedCluster(event.Cluster.Server)
}()
}
}
return c.db.WatchClusters(ctx, clusterEventCallback)
}, "watch clusters", ctx, clustercache.ClusterRetryTimeout)
}, "watch clusters", ctx, clusterRetryTimeout)
<-ctx.Done()
c.invalidate(c.cacheSettings)
return nil
}
func (c *liveStateCache) GetClustersInfo() []clustercache.ClusterInfo {
c.lock.RLock()
defer c.lock.RUnlock()
res := make([]clustercache.ClusterInfo, 0)
func (c *liveStateCache) GetClustersInfo() []metrics.ClusterInfo {
c.lock.Lock()
defer c.lock.Unlock()
res := make([]metrics.ClusterInfo, 0)
for _, info := range c.clusters {
res = append(res, info.GetClusterInfo())
res = append(res, info.getClusterInfo())
}
return res
}
func (c *liveStateCache) GetClusterCache(server string) (clustercache.ClusterCache, error) {
return c.getSyncedCluster(server)
}

26
controller/cache/cache_test.go vendored Normal file
View File

@@ -0,0 +1,26 @@
package cache
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestGetServerVersion(t *testing.T) {
now := time.Now()
cache := &liveStateCache{
lock: &sync.Mutex{},
clusters: map[string]*clusterInfo{
"http://localhost": {
syncTime: &now,
lock: &sync.Mutex{},
serverVersion: "123",
},
}}
version, err := cache.GetServerVersion("http://localhost")
assert.NoError(t, err)
assert.Equal(t, "123", version)
}

604
controller/cache/cluster.go vendored Normal file
View File

@@ -0,0 +1,604 @@
package cache
import (
"context"
"fmt"
"runtime/debug"
"sort"
"strings"
"sync"
"time"
"k8s.io/client-go/dynamic"
"k8s.io/apimachinery/pkg/types"
"github.com/argoproj/argo-cd/controller/metrics"
log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/watch"
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/health"
"github.com/argoproj/argo-cd/util/kube"
)
const (
clusterSyncTimeout = 24 * time.Hour
clusterRetryTimeout = 10 * time.Second
watchResourcesRetryTimeout = 1 * time.Second
)
type apiMeta struct {
namespaced bool
resourceVersion string
watchCancel context.CancelFunc
}
type clusterInfo struct {
syncTime *time.Time
syncError error
apisMeta map[schema.GroupKind]*apiMeta
serverVersion string
lock *sync.Mutex
nodes map[kube.ResourceKey]*node
nsIndex map[string]map[kube.ResourceKey]*node
onObjectUpdated ObjectUpdatedHandler
onEventReceived func(event watch.EventType, un *unstructured.Unstructured)
kubectl kube.Kubectl
cluster *appv1.Cluster
log *log.Entry
cacheSettingsSrc func() *cacheSettings
}
func (c *clusterInfo) replaceResourceCache(gk schema.GroupKind, resourceVersion string, objs []unstructured.Unstructured, ns string) {
info, ok := c.apisMeta[gk]
if ok {
objByKey := make(map[kube.ResourceKey]*unstructured.Unstructured)
for i := range objs {
objByKey[kube.GetResourceKey(&objs[i])] = &objs[i]
}
// update existing nodes
for i := range objs {
obj := &objs[i]
key := kube.GetResourceKey(&objs[i])
existingNode, exists := c.nodes[key]
c.onNodeUpdated(exists, existingNode, obj, key)
}
// remove existing nodes that a no longer exist
for key, existingNode := range c.nodes {
if key.Kind != gk.Kind || key.Group != gk.Group || ns != "" && key.Namespace != ns {
continue
}
if _, ok := objByKey[key]; !ok {
c.onNodeRemoved(key, existingNode)
}
}
info.resourceVersion = resourceVersion
}
}
func isServiceAccountTokenSecret(un *unstructured.Unstructured) (bool, metav1.OwnerReference) {
ref := metav1.OwnerReference{
APIVersion: "v1",
Kind: kube.ServiceAccountKind,
}
if un.GetKind() != kube.SecretKind || un.GroupVersionKind().Group != "" {
return false, ref
}
if typeVal, ok, err := unstructured.NestedString(un.Object, "type"); !ok || err != nil || typeVal != "kubernetes.io/service-account-token" {
return false, ref
}
annotations := un.GetAnnotations()
if annotations == nil {
return false, ref
}
id, okId := annotations["kubernetes.io/service-account.uid"]
name, okName := annotations["kubernetes.io/service-account.name"]
if okId && okName {
ref.Name = name
ref.UID = types.UID(id)
}
return ref.Name != "" && ref.UID != "", ref
}
func (c *clusterInfo) createObjInfo(un *unstructured.Unstructured, appInstanceLabel string) *node {
ownerRefs := un.GetOwnerReferences()
// Special case for endpoint. Remove after https://github.com/kubernetes/kubernetes/issues/28483 is fixed
if un.GroupVersionKind().Group == "" && un.GetKind() == kube.EndpointsKind && len(un.GetOwnerReferences()) == 0 {
ownerRefs = append(ownerRefs, metav1.OwnerReference{
Name: un.GetName(),
Kind: kube.ServiceKind,
APIVersion: "v1",
})
}
// edge case. Consider auto-created service account tokens as a child of service account objects
if yes, ref := isServiceAccountTokenSecret(un); yes {
ownerRefs = append(ownerRefs, ref)
}
nodeInfo := &node{
resourceVersion: un.GetResourceVersion(),
ref: kube.GetObjectRef(un),
ownerRefs: ownerRefs,
}
populateNodeInfo(un, nodeInfo)
appName := kube.GetAppInstanceLabel(un, appInstanceLabel)
if len(ownerRefs) == 0 && appName != "" {
nodeInfo.appName = appName
nodeInfo.resource = un
}
nodeInfo.health, _ = health.GetResourceHealth(un, c.cacheSettingsSrc().ResourceOverrides)
return nodeInfo
}
func (c *clusterInfo) setNode(n *node) {
key := n.resourceKey()
c.nodes[key] = n
ns, ok := c.nsIndex[key.Namespace]
if !ok {
ns = make(map[kube.ResourceKey]*node)
c.nsIndex[key.Namespace] = ns
}
ns[key] = n
}
func (c *clusterInfo) removeNode(key kube.ResourceKey) {
delete(c.nodes, key)
if ns, ok := c.nsIndex[key.Namespace]; ok {
delete(ns, key)
if len(ns) == 0 {
delete(c.nsIndex, key.Namespace)
}
}
}
func (c *clusterInfo) invalidate() {
c.lock.Lock()
defer c.lock.Unlock()
c.syncTime = nil
for i := range c.apisMeta {
c.apisMeta[i].watchCancel()
}
c.apisMeta = nil
}
func (c *clusterInfo) synced() bool {
if c.syncTime == nil {
return false
}
if c.syncError != nil {
return time.Now().Before(c.syncTime.Add(clusterRetryTimeout))
}
return time.Now().Before(c.syncTime.Add(clusterSyncTimeout))
}
func (c *clusterInfo) stopWatching(gk schema.GroupKind, ns string) {
c.lock.Lock()
defer c.lock.Unlock()
if info, ok := c.apisMeta[gk]; ok {
info.watchCancel()
delete(c.apisMeta, gk)
c.replaceResourceCache(gk, "", []unstructured.Unstructured{}, ns)
log.Warnf("Stop watching %s not found on %s.", gk, c.cluster.Server)
}
}
// startMissingWatches lists supported cluster resources and start watching for changes unless watch is already running
func (c *clusterInfo) startMissingWatches() error {
config := c.cluster.RESTConfig()
apis, err := c.kubectl.GetAPIResources(config, c.cacheSettingsSrc().ResourcesFilter)
if err != nil {
return err
}
client, err := c.kubectl.NewDynamicClient(config)
if err != nil {
return err
}
for i := range apis {
api := apis[i]
if _, ok := c.apisMeta[api.GroupKind]; !ok {
ctx, cancel := context.WithCancel(context.Background())
info := &apiMeta{namespaced: api.Meta.Namespaced, watchCancel: cancel}
c.apisMeta[api.GroupKind] = info
err = c.processApi(client, api, func(resClient dynamic.ResourceInterface, ns string) error {
go c.watchEvents(ctx, api, info, resClient, ns)
return nil
})
if err != nil {
return err
}
}
}
return nil
}
func runSynced(lock *sync.Mutex, action func() error) error {
lock.Lock()
defer lock.Unlock()
return action()
}
func (c *clusterInfo) watchEvents(ctx context.Context, api kube.APIResourceInfo, info *apiMeta, resClient dynamic.ResourceInterface, ns string) {
util.RetryUntilSucceed(func() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
}
}()
err = runSynced(c.lock, func() error {
if info.resourceVersion == "" {
list, err := resClient.List(metav1.ListOptions{})
if err != nil {
return err
}
c.replaceResourceCache(api.GroupKind, list.GetResourceVersion(), list.Items, ns)
}
return nil
})
if err != nil {
return err
}
w, err := resClient.Watch(metav1.ListOptions{ResourceVersion: info.resourceVersion})
if errors.IsNotFound(err) {
c.stopWatching(api.GroupKind, ns)
return nil
}
err = runSynced(c.lock, func() error {
if errors.IsGone(err) {
info.resourceVersion = ""
log.Warnf("Resource version of %s on %s is too old.", api.GroupKind, c.cluster.Server)
}
return err
})
if err != nil {
return err
}
defer w.Stop()
for {
select {
case <-ctx.Done():
return nil
case event, ok := <-w.ResultChan():
if ok {
obj := event.Object.(*unstructured.Unstructured)
info.resourceVersion = obj.GetResourceVersion()
c.processEvent(event.Type, obj)
if kube.IsCRD(obj) {
if event.Type == watch.Deleted {
group, groupOk, groupErr := unstructured.NestedString(obj.Object, "spec", "group")
kind, kindOk, kindErr := unstructured.NestedString(obj.Object, "spec", "names", "kind")
if groupOk && groupErr == nil && kindOk && kindErr == nil {
gk := schema.GroupKind{Group: group, Kind: kind}
c.stopWatching(gk, ns)
}
} else {
err = runSynced(c.lock, func() error {
return c.startMissingWatches()
})
}
}
if err != nil {
log.Warnf("Failed to start missing watch: %v", err)
}
} else {
return fmt.Errorf("Watch %s on %s has closed", api.GroupKind, c.cluster.Server)
}
}
}
}, fmt.Sprintf("watch %s on %s", api.GroupKind, c.cluster.Server), ctx, watchResourcesRetryTimeout)
}
func (c *clusterInfo) processApi(client dynamic.Interface, api kube.APIResourceInfo, callback func(resClient dynamic.ResourceInterface, ns string) error) error {
resClient := client.Resource(api.GroupVersionResource)
if len(c.cluster.Namespaces) == 0 {
return callback(resClient, "")
}
if !api.Meta.Namespaced {
return nil
}
for _, ns := range c.cluster.Namespaces {
err := callback(resClient.Namespace(ns), ns)
if err != nil {
return err
}
}
return nil
}
func (c *clusterInfo) sync() (err error) {
c.log.Info("Start syncing cluster")
for i := range c.apisMeta {
c.apisMeta[i].watchCancel()
}
c.apisMeta = make(map[schema.GroupKind]*apiMeta)
c.nodes = make(map[kube.ResourceKey]*node)
config := c.cluster.RESTConfig()
version, err := c.kubectl.GetServerVersion(config)
if err != nil {
return err
}
c.serverVersion = version
apis, err := c.kubectl.GetAPIResources(config, c.cacheSettingsSrc().ResourcesFilter)
if err != nil {
return err
}
client, err := c.kubectl.NewDynamicClient(config)
if err != nil {
return err
}
lock := sync.Mutex{}
err = util.RunAllAsync(len(apis), func(i int) error {
return c.processApi(client, apis[i], func(resClient dynamic.ResourceInterface, _ string) error {
list, err := resClient.List(metav1.ListOptions{})
if err != nil {
return err
}
lock.Lock()
for i := range list.Items {
c.setNode(c.createObjInfo(&list.Items[i], c.cacheSettingsSrc().AppInstanceLabelKey))
}
lock.Unlock()
return nil
})
})
if err == nil {
err = c.startMissingWatches()
}
if err != nil {
log.Errorf("Failed to sync cluster %s: %v", c.cluster.Server, err)
return err
}
c.log.Info("Cluster successfully synced")
return nil
}
func (c *clusterInfo) ensureSynced() error {
c.lock.Lock()
defer c.lock.Unlock()
if c.synced() {
return c.syncError
}
err := c.sync()
syncTime := time.Now()
c.syncTime = &syncTime
c.syncError = err
return c.syncError
}
func (c *clusterInfo) getNamespaceTopLevelResources(namespace string) map[kube.ResourceKey]appv1.ResourceNode {
c.lock.Lock()
defer c.lock.Unlock()
nodes := make(map[kube.ResourceKey]appv1.ResourceNode)
for _, node := range c.nsIndex[namespace] {
if len(node.ownerRefs) == 0 {
nodes[node.resourceKey()] = node.asResourceNode()
}
}
return nodes
}
func (c *clusterInfo) iterateHierarchy(key kube.ResourceKey, action func(child appv1.ResourceNode, appName string)) {
c.lock.Lock()
defer c.lock.Unlock()
if objInfo, ok := c.nodes[key]; ok {
nsNodes := c.nsIndex[key.Namespace]
action(objInfo.asResourceNode(), objInfo.getApp(nsNodes))
childrenByUID := make(map[types.UID][]*node)
for _, child := range nsNodes {
if objInfo.isParentOf(child) {
childrenByUID[child.ref.UID] = append(childrenByUID[child.ref.UID], child)
}
}
// make sure children has no duplicates
for _, children := range childrenByUID {
if len(children) > 0 {
// The object might have multiple children with the same UID (e.g. replicaset from apps and extensions group). It is ok to pick any object but we need to make sure
// we pick the same child after every refresh.
sort.Slice(children, func(i, j int) bool {
key1 := children[i].resourceKey()
key2 := children[j].resourceKey()
return strings.Compare(key1.String(), key2.String()) < 0
})
child := children[0]
action(child.asResourceNode(), child.getApp(nsNodes))
child.iterateChildren(nsNodes, map[kube.ResourceKey]bool{objInfo.resourceKey(): true}, action)
}
}
}
}
func (c *clusterInfo) isNamespaced(gk schema.GroupKind) bool {
if api, ok := c.apisMeta[gk]; ok && !api.namespaced {
return false
}
return true
}
func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured, metricsServer *metrics.MetricsServer) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
c.lock.Lock()
defer c.lock.Unlock()
managedObjs := make(map[kube.ResourceKey]*unstructured.Unstructured)
// iterate all objects in live state cache to find ones associated with app
for key, o := range c.nodes {
if o.appName == a.Name && o.resource != nil && len(o.ownerRefs) == 0 {
managedObjs[key] = o.resource
}
}
config := metrics.AddMetricsTransportWrapper(metricsServer, a, c.cluster.RESTConfig())
// iterate target objects and identify ones that already exist in the cluster,\
// but are simply missing our label
lock := &sync.Mutex{}
err := util.RunAllAsync(len(targetObjs), func(i int) error {
targetObj := targetObjs[i]
key := GetTargetObjKey(a, targetObj, c.isNamespaced(targetObj.GroupVersionKind().GroupKind()))
lock.Lock()
managedObj := managedObjs[key]
lock.Unlock()
if managedObj == nil {
if existingObj, exists := c.nodes[key]; exists {
if existingObj.resource != nil {
managedObj = existingObj.resource
} else {
var err error
managedObj, err = c.kubectl.GetResource(config, targetObj.GroupVersionKind(), existingObj.ref.Name, existingObj.ref.Namespace)
if err != nil {
if errors.IsNotFound(err) {
return nil
}
return err
}
}
} else if _, watched := c.apisMeta[key.GroupKind()]; !watched {
var err error
managedObj, err = c.kubectl.GetResource(config, targetObj.GroupVersionKind(), targetObj.GetName(), targetObj.GetNamespace())
if err != nil {
if errors.IsNotFound(err) {
return nil
}
return err
}
}
}
if managedObj != nil {
converted, err := c.kubectl.ConvertToVersion(managedObj, targetObj.GroupVersionKind().Group, targetObj.GroupVersionKind().Version)
if err != nil {
// fallback to loading resource from kubernetes if conversion fails
log.Warnf("Failed to convert resource: %v", err)
managedObj, err = c.kubectl.GetResource(config, targetObj.GroupVersionKind(), managedObj.GetName(), managedObj.GetNamespace())
if err != nil {
if errors.IsNotFound(err) {
return nil
}
return err
}
} else {
managedObj = converted
}
lock.Lock()
managedObjs[key] = managedObj
lock.Unlock()
}
return nil
})
if err != nil {
return nil, err
}
return managedObjs, nil
}
func (c *clusterInfo) processEvent(event watch.EventType, un *unstructured.Unstructured) {
if c.onEventReceived != nil {
c.onEventReceived(event, un)
}
c.lock.Lock()
defer c.lock.Unlock()
key := kube.GetResourceKey(un)
existingNode, exists := c.nodes[key]
if event == watch.Deleted {
if exists {
c.onNodeRemoved(key, existingNode)
}
} else if event != watch.Deleted {
c.onNodeUpdated(exists, existingNode, un, key)
}
}
func (c *clusterInfo) onNodeUpdated(exists bool, existingNode *node, un *unstructured.Unstructured, key kube.ResourceKey) {
nodes := make([]*node, 0)
if exists {
nodes = append(nodes, existingNode)
}
newObj := c.createObjInfo(un, c.cacheSettingsSrc().AppInstanceLabelKey)
c.setNode(newObj)
nodes = append(nodes, newObj)
toNotify := make(map[string]bool)
for i := range nodes {
n := nodes[i]
if ns, ok := c.nsIndex[n.ref.Namespace]; ok {
app := n.getApp(ns)
if app == "" || skipAppRequeing(key) {
continue
}
toNotify[app] = n.isRootAppNode() || toNotify[app]
}
}
c.onObjectUpdated(toNotify, newObj.ref)
}
func (c *clusterInfo) onNodeRemoved(key kube.ResourceKey, n *node) {
appName := n.appName
if ns, ok := c.nsIndex[key.Namespace]; ok {
appName = n.getApp(ns)
}
c.removeNode(key)
managedByApp := make(map[string]bool)
if appName != "" {
managedByApp[appName] = n.isRootAppNode()
}
c.onObjectUpdated(managedByApp, n.ref)
}
var (
ignoredRefreshResources = map[string]bool{
"/" + kube.EndpointsKind: true,
}
)
func (c *clusterInfo) getClusterInfo() metrics.ClusterInfo {
c.lock.Lock()
defer c.lock.Unlock()
return metrics.ClusterInfo{
APIsCount: len(c.apisMeta),
K8SVersion: c.serverVersion,
ResourcesCount: len(c.nodes),
Server: c.cluster.Server,
LastCacheSyncTime: c.syncTime,
}
}
// skipAppRequeing checks if the object is an API type which we want to skip requeuing against.
// We ignore API types which have a high churn rate, and/or whose updates are irrelevant to the app
func skipAppRequeing(key kube.ResourceKey) bool {
return ignoredRefreshResources[key.Group+"/"+key.Kind]
}

558
controller/cache/cluster_test.go vendored Normal file
View File

@@ -0,0 +1,558 @@
package cache
import (
"fmt"
"sort"
"strings"
"sync"
"testing"
"github.com/ghodss/yaml"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/dynamic/fake"
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/errors"
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/util/kube"
"github.com/argoproj/argo-cd/util/kube/kubetest"
)
func strToUnstructured(jsonStr string) *unstructured.Unstructured {
obj := make(map[string]interface{})
err := yaml.Unmarshal([]byte(jsonStr), &obj)
errors.CheckError(err)
return &unstructured.Unstructured{Object: obj}
}
func mustToUnstructured(obj interface{}) *unstructured.Unstructured {
un, err := kube.ToUnstructured(obj)
errors.CheckError(err)
return un
}
var (
testPod = strToUnstructured(`
apiVersion: v1
kind: Pod
metadata:
uid: "1"
name: helm-guestbook-pod
namespace: default
ownerReferences:
- apiVersion: apps/v1
kind: ReplicaSet
name: helm-guestbook-rs
uid: "2"
resourceVersion: "123"`)
testRS = strToUnstructured(`
apiVersion: apps/v1
kind: ReplicaSet
metadata:
uid: "2"
name: helm-guestbook-rs
namespace: default
annotations:
deployment.kubernetes.io/revision: "2"
ownerReferences:
- apiVersion: apps/v1beta1
kind: Deployment
name: helm-guestbook
uid: "3"
resourceVersion: "123"`)
testDeploy = strToUnstructured(`
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/instance: helm-guestbook
uid: "3"
name: helm-guestbook
namespace: default
resourceVersion: "123"`)
testService = strToUnstructured(`
apiVersion: v1
kind: Service
metadata:
name: helm-guestbook
namespace: default
resourceVersion: "123"
uid: "4"
spec:
selector:
app: guestbook
type: LoadBalancer
status:
loadBalancer:
ingress:
- hostname: localhost`)
testIngress = strToUnstructured(`
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: helm-guestbook
namespace: default
uid: "4"
spec:
backend:
serviceName: not-found-service
servicePort: 443
rules:
- host: helm-guestbook.com
http:
paths:
- backend:
serviceName: helm-guestbook
servicePort: 443
path: /
- backend:
serviceName: helm-guestbook
servicePort: https
path: /
status:
loadBalancer:
ingress:
- ip: 107.178.210.11`)
)
func newCluster(objs ...*unstructured.Unstructured) *clusterInfo {
runtimeObjs := make([]runtime.Object, len(objs))
for i := range objs {
runtimeObjs[i] = objs[i]
}
scheme := runtime.NewScheme()
client := fake.NewSimpleDynamicClient(scheme, runtimeObjs...)
apiResources := []kube.APIResourceInfo{{
GroupKind: schema.GroupKind{Group: "", Kind: "Pod"},
GroupVersionResource: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
Meta: metav1.APIResource{Namespaced: true},
}, {
GroupKind: schema.GroupKind{Group: "apps", Kind: "ReplicaSet"},
GroupVersionResource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "replicasets"},
Meta: metav1.APIResource{Namespaced: true},
}, {
GroupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"},
GroupVersionResource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"},
Meta: metav1.APIResource{Namespaced: true},
}}
return newClusterExt(&kubetest.MockKubectlCmd{APIResources: apiResources, DynamicClient: client})
}
func newClusterExt(kubectl kube.Kubectl) *clusterInfo {
return &clusterInfo{
lock: &sync.Mutex{},
nodes: make(map[kube.ResourceKey]*node),
onObjectUpdated: func(managedByApp map[string]bool, reference corev1.ObjectReference) {},
kubectl: kubectl,
nsIndex: make(map[string]map[kube.ResourceKey]*node),
cluster: &appv1.Cluster{},
syncTime: nil,
apisMeta: make(map[schema.GroupKind]*apiMeta),
log: log.WithField("cluster", "test"),
cacheSettingsSrc: func() *cacheSettings {
return &cacheSettings{AppInstanceLabelKey: common.LabelKeyAppInstance}
},
}
}
func getChildren(cluster *clusterInfo, un *unstructured.Unstructured) []appv1.ResourceNode {
hierarchy := make([]appv1.ResourceNode, 0)
cluster.iterateHierarchy(kube.GetResourceKey(un), func(child appv1.ResourceNode, app string) {
hierarchy = append(hierarchy, child)
})
return hierarchy[1:]
}
func TestEnsureSynced(t *testing.T) {
obj1 := strToUnstructured(`
apiVersion: apps/v1
kind: Deployment
metadata: {"name": "helm-guestbook1", "namespace": "default1"}
`)
obj2 := strToUnstructured(`
apiVersion: apps/v1
kind: Deployment
metadata: {"name": "helm-guestbook2", "namespace": "default2"}
`)
cluster := newCluster(obj1, obj2)
err := cluster.ensureSynced()
assert.Nil(t, err)
assert.Len(t, cluster.nodes, 2)
var names []string
for k := range cluster.nodes {
names = append(names, k.Name)
}
assert.ElementsMatch(t, []string{"helm-guestbook1", "helm-guestbook2"}, names)
}
func TestEnsureSyncedSingleNamespace(t *testing.T) {
obj1 := strToUnstructured(`
apiVersion: apps/v1
kind: Deployment
metadata: {"name": "helm-guestbook1", "namespace": "default1"}
`)
obj2 := strToUnstructured(`
apiVersion: apps/v1
kind: Deployment
metadata: {"name": "helm-guestbook2", "namespace": "default2"}
`)
cluster := newCluster(obj1, obj2)
cluster.cluster.Namespaces = []string{"default1"}
err := cluster.ensureSynced()
assert.Nil(t, err)
assert.Len(t, cluster.nodes, 1)
var names []string
for k := range cluster.nodes {
names = append(names, k.Name)
}
assert.ElementsMatch(t, []string{"helm-guestbook1"}, names)
}
func TestGetNamespaceResources(t *testing.T) {
defaultNamespaceTopLevel1 := strToUnstructured(`
apiVersion: apps/v1
kind: Deployment
metadata: {"name": "helm-guestbook1", "namespace": "default"}
`)
defaultNamespaceTopLevel2 := strToUnstructured(`
apiVersion: apps/v1
kind: Deployment
metadata: {"name": "helm-guestbook2", "namespace": "default"}
`)
kubesystemNamespaceTopLevel2 := strToUnstructured(`
apiVersion: apps/v1
kind: Deployment
metadata: {"name": "helm-guestbook3", "namespace": "kube-system"}
`)
cluster := newCluster(defaultNamespaceTopLevel1, defaultNamespaceTopLevel2, kubesystemNamespaceTopLevel2)
err := cluster.ensureSynced()
assert.Nil(t, err)
resources := cluster.getNamespaceTopLevelResources("default")
assert.Len(t, resources, 2)
assert.Equal(t, resources[kube.GetResourceKey(defaultNamespaceTopLevel1)].Name, "helm-guestbook1")
assert.Equal(t, resources[kube.GetResourceKey(defaultNamespaceTopLevel2)].Name, "helm-guestbook2")
resources = cluster.getNamespaceTopLevelResources("kube-system")
assert.Len(t, resources, 1)
assert.Equal(t, resources[kube.GetResourceKey(kubesystemNamespaceTopLevel2)].Name, "helm-guestbook3")
}
func TestGetChildren(t *testing.T) {
cluster := newCluster(testPod, testRS, testDeploy)
err := cluster.ensureSynced()
assert.Nil(t, err)
rsChildren := getChildren(cluster, testRS)
assert.Equal(t, []appv1.ResourceNode{{
ResourceRef: appv1.ResourceRef{
Kind: "Pod",
Namespace: "default",
Name: "helm-guestbook-pod",
Group: "",
Version: "v1",
UID: "1",
},
ParentRefs: []appv1.ResourceRef{{
Group: "apps",
Version: "",
Kind: "ReplicaSet",
Namespace: "default",
Name: "helm-guestbook-rs",
UID: "2",
}},
Health: &appv1.HealthStatus{Status: appv1.HealthStatusUnknown},
NetworkingInfo: &appv1.ResourceNetworkingInfo{Labels: testPod.GetLabels()},
ResourceVersion: "123",
Info: []appv1.InfoItem{{Name: "Containers", Value: "0/0"}},
}}, rsChildren)
deployChildren := getChildren(cluster, testDeploy)
assert.Equal(t, append([]appv1.ResourceNode{{
ResourceRef: appv1.ResourceRef{
Kind: "ReplicaSet",
Namespace: "default",
Name: "helm-guestbook-rs",
Group: "apps",
Version: "v1",
UID: "2",
},
ResourceVersion: "123",
Health: &appv1.HealthStatus{Status: appv1.HealthStatusHealthy},
Info: []appv1.InfoItem{{Name: "Revision", Value: "Rev:2"}},
ParentRefs: []appv1.ResourceRef{{Group: "apps", Version: "", Kind: "Deployment", Namespace: "default", Name: "helm-guestbook", UID: "3"}},
}}, rsChildren...), deployChildren)
}
func TestGetManagedLiveObjs(t *testing.T) {
cluster := newCluster(testPod, testRS, testDeploy)
err := cluster.ensureSynced()
assert.Nil(t, err)
targetDeploy := strToUnstructured(`
apiVersion: apps/v1
kind: Deployment
metadata:
name: helm-guestbook
labels:
app: helm-guestbook`)
managedObjs, err := cluster.getManagedLiveObjs(&appv1.Application{
ObjectMeta: metav1.ObjectMeta{Name: "helm-guestbook"},
Spec: appv1.ApplicationSpec{
Destination: appv1.ApplicationDestination{
Namespace: "default",
},
},
}, []*unstructured.Unstructured{targetDeploy}, nil)
assert.Nil(t, err)
assert.Equal(t, managedObjs, map[kube.ResourceKey]*unstructured.Unstructured{
kube.NewResourceKey("apps", "Deployment", "default", "helm-guestbook"): testDeploy,
})
}
func TestChildDeletedEvent(t *testing.T) {
cluster := newCluster(testPod, testRS, testDeploy)
err := cluster.ensureSynced()
assert.Nil(t, err)
cluster.processEvent(watch.Deleted, testPod)
rsChildren := getChildren(cluster, testRS)
assert.Equal(t, []appv1.ResourceNode{}, rsChildren)
}
func TestProcessNewChildEvent(t *testing.T) {
cluster := newCluster(testPod, testRS, testDeploy)
err := cluster.ensureSynced()
assert.Nil(t, err)
newPod := strToUnstructured(`
apiVersion: v1
kind: Pod
metadata:
uid: "4"
name: helm-guestbook-pod2
namespace: default
ownerReferences:
- apiVersion: apps/v1
kind: ReplicaSet
name: helm-guestbook-rs
uid: "2"
resourceVersion: "123"`)
cluster.processEvent(watch.Added, newPod)
rsChildren := getChildren(cluster, testRS)
sort.Slice(rsChildren, func(i, j int) bool {
return strings.Compare(rsChildren[i].Name, rsChildren[j].Name) < 0
})
assert.Equal(t, []appv1.ResourceNode{{
ResourceRef: appv1.ResourceRef{
Kind: "Pod",
Namespace: "default",
Name: "helm-guestbook-pod",
Group: "",
Version: "v1",
UID: "1",
},
Info: []appv1.InfoItem{{Name: "Containers", Value: "0/0"}},
Health: &appv1.HealthStatus{Status: appv1.HealthStatusUnknown},
NetworkingInfo: &appv1.ResourceNetworkingInfo{Labels: testPod.GetLabels()},
ParentRefs: []appv1.ResourceRef{{
Group: "apps",
Version: "",
Kind: "ReplicaSet",
Namespace: "default",
Name: "helm-guestbook-rs",
UID: "2",
}},
ResourceVersion: "123",
}, {
ResourceRef: appv1.ResourceRef{
Kind: "Pod",
Namespace: "default",
Name: "helm-guestbook-pod2",
Group: "",
Version: "v1",
UID: "4",
},
NetworkingInfo: &appv1.ResourceNetworkingInfo{Labels: testPod.GetLabels()},
Info: []appv1.InfoItem{{Name: "Containers", Value: "0/0"}},
Health: &appv1.HealthStatus{Status: appv1.HealthStatusUnknown},
ParentRefs: []appv1.ResourceRef{{
Group: "apps",
Version: "",
Kind: "ReplicaSet",
Namespace: "default",
Name: "helm-guestbook-rs",
UID: "2",
}},
ResourceVersion: "123",
}}, rsChildren)
}
func TestUpdateResourceTags(t *testing.T) {
pod := &corev1.Pod{
TypeMeta: metav1.TypeMeta{Kind: "Pod", APIVersion: "v1"},
ObjectMeta: metav1.ObjectMeta{Name: "testPod", Namespace: "default"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Name: "test",
Image: "test",
}},
},
}
cluster := newCluster(mustToUnstructured(pod))
err := cluster.ensureSynced()
assert.Nil(t, err)
podNode := cluster.nodes[kube.GetResourceKey(mustToUnstructured(pod))]
assert.NotNil(t, podNode)
assert.Equal(t, []appv1.InfoItem{{Name: "Containers", Value: "0/1"}}, podNode.info)
pod.Status = corev1.PodStatus{
ContainerStatuses: []corev1.ContainerStatus{{
State: corev1.ContainerState{
Terminated: &corev1.ContainerStateTerminated{
ExitCode: -1,
},
},
}},
}
cluster.processEvent(watch.Modified, mustToUnstructured(pod))
podNode = cluster.nodes[kube.GetResourceKey(mustToUnstructured(pod))]
assert.NotNil(t, podNode)
assert.Equal(t, []appv1.InfoItem{{Name: "Status Reason", Value: "ExitCode:-1"}, {Name: "Containers", Value: "0/1"}}, podNode.info)
}
func TestUpdateAppResource(t *testing.T) {
updatesReceived := make([]string, 0)
cluster := newCluster(testPod, testRS, testDeploy)
cluster.onObjectUpdated = func(managedByApp map[string]bool, _ corev1.ObjectReference) {
for appName, fullRefresh := range managedByApp {
updatesReceived = append(updatesReceived, fmt.Sprintf("%s: %v", appName, fullRefresh))
}
}
err := cluster.ensureSynced()
assert.Nil(t, err)
cluster.processEvent(watch.Modified, mustToUnstructured(testPod))
assert.Contains(t, updatesReceived, "helm-guestbook: false")
}
func TestCircularReference(t *testing.T) {
dep := testDeploy.DeepCopy()
dep.SetOwnerReferences([]metav1.OwnerReference{{
Name: testPod.GetName(),
Kind: testPod.GetKind(),
APIVersion: testPod.GetAPIVersion(),
}})
cluster := newCluster(testPod, testRS, dep)
err := cluster.ensureSynced()
assert.Nil(t, err)
children := getChildren(cluster, dep)
assert.Len(t, children, 2)
node := cluster.nodes[kube.GetResourceKey(dep)]
assert.NotNil(t, node)
app := node.getApp(cluster.nodes)
assert.Equal(t, "", app)
}
func TestWatchCacheUpdated(t *testing.T) {
removed := testPod.DeepCopy()
removed.SetName(testPod.GetName() + "-removed-pod")
updated := testPod.DeepCopy()
updated.SetName(testPod.GetName() + "-updated-pod")
updated.SetResourceVersion("updated-pod-version")
cluster := newCluster(removed, updated)
err := cluster.ensureSynced()
assert.Nil(t, err)
added := testPod.DeepCopy()
added.SetName(testPod.GetName() + "-new-pod")
podGroupKind := testPod.GroupVersionKind().GroupKind()
cluster.replaceResourceCache(podGroupKind, "updated-list-version", []unstructured.Unstructured{*updated, *added}, "")
_, ok := cluster.nodes[kube.GetResourceKey(removed)]
assert.False(t, ok)
updatedNode, ok := cluster.nodes[kube.GetResourceKey(updated)]
assert.True(t, ok)
assert.Equal(t, updatedNode.resourceVersion, "updated-pod-version")
_, ok = cluster.nodes[kube.GetResourceKey(added)]
assert.True(t, ok)
}
func TestNamespaceModeReplace(t *testing.T) {
ns1Pod := testPod.DeepCopy()
ns1Pod.SetNamespace("ns1")
ns1Pod.SetName("pod1")
ns2Pod := testPod.DeepCopy()
ns2Pod.SetNamespace("ns2")
podGroupKind := testPod.GroupVersionKind().GroupKind()
cluster := newCluster(ns1Pod, ns2Pod)
err := cluster.ensureSynced()
assert.Nil(t, err)
cluster.replaceResourceCache(podGroupKind, "", nil, "ns1")
_, ok := cluster.nodes[kube.GetResourceKey(ns1Pod)]
assert.False(t, ok)
_, ok = cluster.nodes[kube.GetResourceKey(ns2Pod)]
assert.True(t, ok)
}
func TestGetDuplicatedChildren(t *testing.T) {
extensionsRS := testRS.DeepCopy()
extensionsRS.SetGroupVersionKind(schema.GroupVersionKind{Group: "extensions", Kind: kube.ReplicaSetKind, Version: "v1beta1"})
cluster := newCluster(testDeploy, testRS, extensionsRS)
err := cluster.ensureSynced()
assert.Nil(t, err)
// Get children multiple times to make sure the right child is picked up every time.
for i := 0; i < 5; i++ {
children := getChildren(cluster, testDeploy)
assert.Len(t, children, 1)
assert.Equal(t, "apps", children[0].Group)
assert.Equal(t, kube.ReplicaSetKind, children[0].Kind)
assert.Equal(t, testRS.GetName(), children[0].Name)
}
}

View File

@@ -3,37 +3,38 @@ package cache
import (
"fmt"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
"github.com/argoproj/gitops-engine/pkg/utils/text"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
k8snode "k8s.io/kubernetes/pkg/util/node"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/kube"
"github.com/argoproj/argo-cd/util/resource"
)
func populateNodeInfo(un *unstructured.Unstructured, res *ResourceInfo) {
func populateNodeInfo(un *unstructured.Unstructured, node *node) {
gvk := un.GroupVersionKind()
revision := resource.GetRevision(un)
if revision > 0 {
res.Info = append(res.Info, v1alpha1.InfoItem{Name: "Revision", Value: fmt.Sprintf("Rev:%v", revision)})
node.info = append(node.info, v1alpha1.InfoItem{Name: "Revision", Value: fmt.Sprintf("Rev:%v", revision)})
}
switch gvk.Group {
case "":
switch gvk.Kind {
case kube.PodKind:
populatePodInfo(un, res)
populatePodInfo(un, node)
return
case kube.ServiceKind:
populateServiceInfo(un, res)
populateServiceInfo(un, node)
return
}
case "extensions", "networking.k8s.io":
switch gvk.Kind {
case kube.IngressKind:
populateIngressInfo(un, res)
populateIngressInfo(un, node)
return
}
}
@@ -57,16 +58,16 @@ func getIngress(un *unstructured.Unstructured) []v1.LoadBalancerIngress {
return res
}
func populateServiceInfo(un *unstructured.Unstructured, res *ResourceInfo) {
func populateServiceInfo(un *unstructured.Unstructured, node *node) {
targetLabels, _, _ := unstructured.NestedStringMap(un.Object, "spec", "selector")
ingress := make([]v1.LoadBalancerIngress, 0)
if serviceType, ok, err := unstructured.NestedString(un.Object, "spec", "type"); ok && err == nil && serviceType == string(v1.ServiceTypeLoadBalancer) {
ingress = getIngress(un)
}
res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetLabels: targetLabels, Ingress: ingress}
node.networkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetLabels: targetLabels, Ingress: ingress}
}
func populateIngressInfo(un *unstructured.Unstructured, res *ResourceInfo) {
func populateIngressInfo(un *unstructured.Unstructured, node *node) {
ingress := getIngress(un)
targetsMap := make(map[v1alpha1.ResourceRef]bool)
if backend, ok, err := unstructured.NestedMap(un.Object, "spec", "backend"); ok && err == nil {
@@ -87,7 +88,7 @@ func populateIngressInfo(un *unstructured.Unstructured, res *ResourceInfo) {
host := rule["host"]
if host == nil || host == "" {
for i := range ingress {
host = text.FirstNonEmpty(ingress[i].Hostname, ingress[i].IP)
host = util.FirstNonEmpty(ingress[i].Hostname, ingress[i].IP)
if host != "" {
break
}
@@ -154,10 +155,10 @@ func populateIngressInfo(un *unstructured.Unstructured, res *ResourceInfo) {
for url := range urlsSet {
urls = append(urls, url)
}
res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetRefs: targets, Ingress: ingress, ExternalURLs: urls}
node.networkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetRefs: targets, Ingress: ingress, ExternalURLs: urls}
}
func populatePodInfo(un *unstructured.Unstructured, res *ResourceInfo) {
func populatePodInfo(un *unstructured.Unstructured, node *node) {
pod := v1.Pod{}
err := runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, &pod)
if err != nil {
@@ -180,9 +181,9 @@ func populatePodInfo(un *unstructured.Unstructured, res *ResourceInfo) {
imagesSet[container.Image] = true
}
res.Images = nil
node.images = nil
for image := range imagesSet {
res.Images = append(res.Images, image)
node.images = append(node.images, image)
}
initializing := false
@@ -249,8 +250,8 @@ func populatePodInfo(un *unstructured.Unstructured, res *ResourceInfo) {
}
if reason != "" {
res.Info = append(res.Info, v1alpha1.InfoItem{Name: "Status Reason", Value: reason})
node.info = append(node.info, v1alpha1.InfoItem{Name: "Status Reason", Value: reason})
}
res.Info = append(res.Info, v1alpha1.InfoItem{Name: "Containers", Value: fmt.Sprintf("%d/%d", readyContainers, totalContainers)})
res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{Labels: un.GetLabels()}
node.info = append(node.info, v1alpha1.InfoItem{Name: "Containers", Value: fmt.Sprintf("%d/%d", readyContainers, totalContainers)})
node.networkingInfo = &v1alpha1.ResourceNetworkingInfo{Labels: un.GetLabels()}
}

View File

@@ -5,68 +5,12 @@ import (
"strings"
"testing"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
"github.com/argoproj/pkg/errors"
"github.com/ghodss/yaml"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
)
"github.com/argoproj/argo-cd/util/kube"
func strToUnstructured(jsonStr string) *unstructured.Unstructured {
obj := make(map[string]interface{})
err := yaml.Unmarshal([]byte(jsonStr), &obj)
errors.CheckError(err)
return &unstructured.Unstructured{Object: obj}
}
var (
testService = strToUnstructured(`
apiVersion: v1
kind: Service
metadata:
name: helm-guestbook
namespace: default
resourceVersion: "123"
uid: "4"
spec:
selector:
app: guestbook
type: LoadBalancer
status:
loadBalancer:
ingress:
- hostname: localhost`)
testIngress = strToUnstructured(`
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: helm-guestbook
namespace: default
uid: "4"
spec:
backend:
serviceName: not-found-service
servicePort: 443
rules:
- host: helm-guestbook.com
http:
paths:
- backend:
serviceName: helm-guestbook
servicePort: 443
path: /
- backend:
serviceName: helm-guestbook
servicePort: https
path: /
status:
loadBalancer:
ingress:
- ip: 107.178.210.11`)
"github.com/stretchr/testify/assert"
)
func TestGetPodInfo(t *testing.T) {
@@ -87,29 +31,29 @@ func TestGetPodInfo(t *testing.T) {
containers:
- image: bar`)
info := &ResourceInfo{}
populateNodeInfo(pod, info)
assert.Equal(t, []v1alpha1.InfoItem{{Name: "Containers", Value: "0/1"}}, info.Info)
assert.Equal(t, []string{"bar"}, info.Images)
assert.Equal(t, &v1alpha1.ResourceNetworkingInfo{Labels: map[string]string{"app": "guestbook"}}, info.NetworkingInfo)
node := &node{}
populateNodeInfo(pod, node)
assert.Equal(t, []v1alpha1.InfoItem{{Name: "Containers", Value: "0/1"}}, node.info)
assert.Equal(t, []string{"bar"}, node.images)
assert.Equal(t, &v1alpha1.ResourceNetworkingInfo{Labels: map[string]string{"app": "guestbook"}}, node.networkingInfo)
}
func TestGetServiceInfo(t *testing.T) {
info := &ResourceInfo{}
populateNodeInfo(testService, info)
assert.Equal(t, 0, len(info.Info))
node := &node{}
populateNodeInfo(testService, node)
assert.Equal(t, 0, len(node.info))
assert.Equal(t, &v1alpha1.ResourceNetworkingInfo{
TargetLabels: map[string]string{"app": "guestbook"},
Ingress: []v1.LoadBalancerIngress{{Hostname: "localhost"}},
}, info.NetworkingInfo)
}, node.networkingInfo)
}
func TestGetIngressInfo(t *testing.T) {
info := &ResourceInfo{}
populateNodeInfo(testIngress, info)
assert.Equal(t, 0, len(info.Info))
sort.Slice(info.NetworkingInfo.TargetRefs, func(i, j int) bool {
return strings.Compare(info.NetworkingInfo.TargetRefs[j].Name, info.NetworkingInfo.TargetRefs[i].Name) < 0
node := &node{}
populateNodeInfo(testIngress, node)
assert.Equal(t, 0, len(node.info))
sort.Slice(node.networkingInfo.TargetRefs, func(i, j int) bool {
return strings.Compare(node.networkingInfo.TargetRefs[j].Name, node.networkingInfo.TargetRefs[i].Name) < 0
})
assert.Equal(t, &v1alpha1.ResourceNetworkingInfo{
Ingress: []v1.LoadBalancerIngress{{IP: "107.178.210.11"}},
@@ -125,7 +69,7 @@ func TestGetIngressInfo(t *testing.T) {
Name: "helm-guestbook",
}},
ExternalURLs: []string{"https://helm-guestbook.com/"},
}, info.NetworkingInfo)
}, node.networkingInfo)
}
func TestGetIngressInfoNoHost(t *testing.T) {
@@ -148,8 +92,8 @@ func TestGetIngressInfoNoHost(t *testing.T) {
ingress:
- ip: 107.178.210.11`)
info := &ResourceInfo{}
populateNodeInfo(ingress, info)
node := &node{}
populateNodeInfo(ingress, node)
assert.Equal(t, &v1alpha1.ResourceNetworkingInfo{
Ingress: []v1.LoadBalancerIngress{{IP: "107.178.210.11"}},
@@ -160,7 +104,7 @@ func TestGetIngressInfoNoHost(t *testing.T) {
Name: "helm-guestbook",
}},
ExternalURLs: []string{"https://107.178.210.11/"},
}, info.NetworkingInfo)
}, node.networkingInfo)
}
func TestExternalUrlWithSubPath(t *testing.T) {
ingress := strToUnstructured(`
@@ -182,11 +126,11 @@ func TestExternalUrlWithSubPath(t *testing.T) {
ingress:
- ip: 107.178.210.11`)
info := &ResourceInfo{}
populateNodeInfo(ingress, info)
node := &node{}
populateNodeInfo(ingress, node)
expectedExternalUrls := []string{"https://107.178.210.11/my/sub/path/"}
assert.Equal(t, expectedExternalUrls, info.NetworkingInfo.ExternalURLs)
assert.Equal(t, expectedExternalUrls, node.networkingInfo.ExternalURLs)
}
func TestExternalUrlWithMultipleSubPaths(t *testing.T) {
ingress := strToUnstructured(`
@@ -216,11 +160,11 @@ func TestExternalUrlWithMultipleSubPaths(t *testing.T) {
ingress:
- ip: 107.178.210.11`)
info := &ResourceInfo{}
populateNodeInfo(ingress, info)
node := &node{}
populateNodeInfo(ingress, node)
expectedExternalUrls := []string{"https://helm-guestbook.com/my/sub/path/", "https://helm-guestbook.com/my/sub/path/2", "https://helm-guestbook.com"}
actualURLs := info.NetworkingInfo.ExternalURLs
actualURLs := node.networkingInfo.ExternalURLs
sort.Strings(expectedExternalUrls)
sort.Strings(actualURLs)
assert.Equal(t, expectedExternalUrls, actualURLs)
@@ -244,11 +188,11 @@ func TestExternalUrlWithNoSubPath(t *testing.T) {
ingress:
- ip: 107.178.210.11`)
info := &ResourceInfo{}
populateNodeInfo(ingress, info)
node := &node{}
populateNodeInfo(ingress, node)
expectedExternalUrls := []string{"https://107.178.210.11"}
assert.Equal(t, expectedExternalUrls, info.NetworkingInfo.ExternalURLs)
assert.Equal(t, expectedExternalUrls, node.networkingInfo.ExternalURLs)
}
func TestExternalUrlWithNetworkingApi(t *testing.T) {
@@ -270,9 +214,9 @@ func TestExternalUrlWithNetworkingApi(t *testing.T) {
ingress:
- ip: 107.178.210.11`)
info := &ResourceInfo{}
populateNodeInfo(ingress, info)
node := &node{}
populateNodeInfo(ingress, node)
expectedExternalUrls := []string{"https://107.178.210.11"}
assert.Equal(t, expectedExternalUrls, info.NetworkingInfo.ExternalURLs)
assert.Equal(t, expectedExternalUrls, node.networkingInfo.ExternalURLs)
}

View File

@@ -5,9 +5,8 @@ package mocks
import (
context "context"
cache "github.com/argoproj/gitops-engine/pkg/cache"
kube "github.com/argoproj/gitops-engine/pkg/utils/kube"
metrics "github.com/argoproj/argo-cd/controller/metrics"
kube "github.com/argoproj/argo-cd/util/kube"
mock "github.com/stretchr/testify/mock"
@@ -15,8 +14,6 @@ import (
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
)
@@ -25,39 +22,16 @@ type LiveStateCache struct {
mock.Mock
}
// GetClusterCache provides a mock function with given fields: server
func (_m *LiveStateCache) GetClusterCache(server string) (cache.ClusterCache, error) {
ret := _m.Called(server)
var r0 cache.ClusterCache
if rf, ok := ret.Get(0).(func(string) cache.ClusterCache); ok {
r0 = rf(server)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(cache.ClusterCache)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(server)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetClustersInfo provides a mock function with given fields:
func (_m *LiveStateCache) GetClustersInfo() []cache.ClusterInfo {
func (_m *LiveStateCache) GetClustersInfo() []metrics.ClusterInfo {
ret := _m.Called()
var r0 []cache.ClusterInfo
if rf, ok := ret.Get(0).(func() []cache.ClusterInfo); ok {
var r0 []metrics.ClusterInfo
if rf, ok := ret.Get(0).(func() []metrics.ClusterInfo); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]cache.ClusterInfo)
r0 = ret.Get(0).([]metrics.ClusterInfo)
}
}
@@ -110,8 +84,8 @@ func (_m *LiveStateCache) GetNamespaceTopLevelResources(server string, namespace
return r0, r1
}
// GetVersionsInfo provides a mock function with given fields: serverURL
func (_m *LiveStateCache) GetVersionsInfo(serverURL string) (string, []v1.APIGroup, error) {
// GetServerVersion provides a mock function with given fields: serverURL
func (_m *LiveStateCache) GetServerVersion(serverURL string) (string, error) {
ret := _m.Called(serverURL)
var r0 string
@@ -121,37 +95,19 @@ func (_m *LiveStateCache) GetVersionsInfo(serverURL string) (string, []v1.APIGro
r0 = ret.Get(0).(string)
}
var r1 []v1.APIGroup
if rf, ok := ret.Get(1).(func(string) []v1.APIGroup); ok {
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(serverURL)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).([]v1.APIGroup)
}
r1 = ret.Error(1)
}
var r2 error
if rf, ok := ret.Get(2).(func(string) error); ok {
r2 = rf(serverURL)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
return r0, r1
}
// Init provides a mock function with given fields:
func (_m *LiveStateCache) Init() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
// Invalidate provides a mock function with given fields:
func (_m *LiveStateCache) Invalidate() {
_m.Called()
}
// IsNamespaced provides a mock function with given fields: server, gk

142
controller/cache/node.go vendored Normal file
View File

@@ -0,0 +1,142 @@
package cache
import (
log "github.com/sirupsen/logrus"
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/util/kube"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
)
type node struct {
resourceVersion string
ref v1.ObjectReference
ownerRefs []metav1.OwnerReference
info []appv1.InfoItem
appName string
// available only for root application nodes
resource *unstructured.Unstructured
// networkingInfo are available only for known types involved into networking: Ingress, Service, Pod
networkingInfo *appv1.ResourceNetworkingInfo
images []string
health *appv1.HealthStatus
}
func (n *node) isRootAppNode() bool {
return n.appName != "" && len(n.ownerRefs) == 0
}
func (n *node) resourceKey() kube.ResourceKey {
return kube.NewResourceKey(n.ref.GroupVersionKind().Group, n.ref.Kind, n.ref.Namespace, n.ref.Name)
}
func (n *node) isParentOf(child *node) bool {
for i, ownerRef := range child.ownerRefs {
// backfill UID of inferred owner child references
if ownerRef.UID == "" && n.ref.Kind == ownerRef.Kind && n.ref.APIVersion == ownerRef.APIVersion && n.ref.Name == ownerRef.Name {
ownerRef.UID = n.ref.UID
child.ownerRefs[i] = ownerRef
return true
}
if n.ref.UID == ownerRef.UID {
return true
}
}
return false
}
func ownerRefGV(ownerRef metav1.OwnerReference) schema.GroupVersion {
gv, err := schema.ParseGroupVersion(ownerRef.APIVersion)
if err != nil {
gv = schema.GroupVersion{}
}
return gv
}
func (n *node) getApp(ns map[kube.ResourceKey]*node) string {
return n.getAppRecursive(ns, map[kube.ResourceKey]bool{})
}
func (n *node) getAppRecursive(ns map[kube.ResourceKey]*node, visited map[kube.ResourceKey]bool) string {
if !visited[n.resourceKey()] {
visited[n.resourceKey()] = true
} else {
log.Warnf("Circular dependency detected: %v.", visited)
return n.appName
}
if n.appName != "" {
return n.appName
}
for _, ownerRef := range n.ownerRefs {
gv := ownerRefGV(ownerRef)
if parent, ok := ns[kube.NewResourceKey(gv.Group, ownerRef.Kind, n.ref.Namespace, ownerRef.Name)]; ok {
app := parent.getAppRecursive(ns, visited)
if app != "" {
return app
}
}
}
return ""
}
func newResourceKeySet(set map[kube.ResourceKey]bool, keys ...kube.ResourceKey) map[kube.ResourceKey]bool {
newSet := make(map[kube.ResourceKey]bool)
for k, v := range set {
newSet[k] = v
}
for i := range keys {
newSet[keys[i]] = true
}
return newSet
}
func (n *node) asResourceNode() appv1.ResourceNode {
gv, err := schema.ParseGroupVersion(n.ref.APIVersion)
if err != nil {
gv = schema.GroupVersion{}
}
parentRefs := make([]appv1.ResourceRef, len(n.ownerRefs))
for _, ownerRef := range n.ownerRefs {
ownerGvk := schema.FromAPIVersionAndKind(ownerRef.APIVersion, ownerRef.Kind)
ownerKey := kube.NewResourceKey(ownerGvk.Group, ownerRef.Kind, n.ref.Namespace, ownerRef.Name)
parentRefs[0] = appv1.ResourceRef{Name: ownerRef.Name, Kind: ownerKey.Kind, Namespace: n.ref.Namespace, Group: ownerKey.Group, UID: string(ownerRef.UID)}
}
return appv1.ResourceNode{
ResourceRef: appv1.ResourceRef{
UID: string(n.ref.UID),
Name: n.ref.Name,
Group: gv.Group,
Version: gv.Version,
Kind: n.ref.Kind,
Namespace: n.ref.Namespace,
},
ParentRefs: parentRefs,
Info: n.info,
ResourceVersion: n.resourceVersion,
NetworkingInfo: n.networkingInfo,
Images: n.images,
Health: n.health,
}
}
func (n *node) iterateChildren(ns map[kube.ResourceKey]*node, parents map[kube.ResourceKey]bool, action func(child appv1.ResourceNode, appName string)) {
for childKey, child := range ns {
if n.isParentOf(ns[childKey]) {
if parents[childKey] {
key := n.resourceKey()
log.Warnf("Circular dependency detected. %s is child and parent of %s", childKey.String(), key.String())
} else {
action(child.asResourceNode(), child.getApp(ns))
child.iterateChildren(ns, newResourceKeySet(parents, n.resourceKey()), action)
}
}
}
}

83
controller/cache/node_test.go vendored Normal file
View File

@@ -0,0 +1,83 @@
package cache
import (
"testing"
"github.com/argoproj/argo-cd/common"
"github.com/stretchr/testify/assert"
)
var c = &clusterInfo{cacheSettingsSrc: func() *cacheSettings {
return &cacheSettings{AppInstanceLabelKey: common.LabelKeyAppInstance}
}}
func TestIsParentOf(t *testing.T) {
child := c.createObjInfo(testPod, "")
parent := c.createObjInfo(testRS, "")
grandParent := c.createObjInfo(testDeploy, "")
assert.True(t, parent.isParentOf(child))
assert.False(t, grandParent.isParentOf(child))
}
func TestIsParentOfSameKindDifferentGroupAndUID(t *testing.T) {
rs := testRS.DeepCopy()
rs.SetAPIVersion("somecrd.io/v1")
rs.SetUID("123")
child := c.createObjInfo(testPod, "")
invalidParent := c.createObjInfo(rs, "")
assert.False(t, invalidParent.isParentOf(child))
}
func TestIsServiceParentOfEndPointWithTheSameName(t *testing.T) {
nonMatchingNameEndPoint := c.createObjInfo(strToUnstructured(`
apiVersion: v1
kind: Endpoints
metadata:
name: not-matching-name
namespace: default
`), "")
matchingNameEndPoint := c.createObjInfo(strToUnstructured(`
apiVersion: v1
kind: Endpoints
metadata:
name: helm-guestbook
namespace: default
`), "")
parent := c.createObjInfo(testService, "")
assert.True(t, parent.isParentOf(matchingNameEndPoint))
assert.Equal(t, parent.ref.UID, matchingNameEndPoint.ownerRefs[0].UID)
assert.False(t, parent.isParentOf(nonMatchingNameEndPoint))
}
func TestIsServiceAccoountParentOfSecret(t *testing.T) {
serviceAccount := c.createObjInfo(strToUnstructured(`
apiVersion: v1
kind: ServiceAccount
metadata:
name: default
namespace: default
uid: '123'
secrets:
- name: default-token-123
`), "")
tokenSecret := c.createObjInfo(strToUnstructured(`
apiVersion: v1
kind: Secret
metadata:
annotations:
kubernetes.io/service-account.name: default
kubernetes.io/service-account.uid: '123'
name: default-token-123
namespace: default
uid: '345'
type: kubernetes.io/service-account-token
`), "")
assert.True(t, serviceAccount.isParentOf(tokenSecret))
}

View File

@@ -5,8 +5,6 @@ import (
"sync"
"time"
"github.com/argoproj/gitops-engine/pkg/cache"
"github.com/prometheus/client_golang/prometheus"
)
@@ -43,19 +41,25 @@ var (
)
)
type ClusterInfo struct {
Server string
K8SVersion string
ResourcesCount int
APIsCount int
LastCacheSyncTime *time.Time
}
type HasClustersInfo interface {
GetClustersInfo() []cache.ClusterInfo
GetClustersInfo() []ClusterInfo
}
type clusterCollector struct {
infoSource HasClustersInfo
info []cache.ClusterInfo
info []ClusterInfo
lock sync.Mutex
}
func (c *clusterCollector) Run(ctx context.Context) {
// FIXME: complains about SA1015
// nolint:staticcheck
tick := time.Tick(metricsCollectionInterval)
for {
select {

View File

@@ -3,11 +3,9 @@ package metrics
import (
"context"
"net/http"
"os"
"strconv"
"time"
"github.com/argoproj/gitops-engine/pkg/health"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
log "github.com/sirupsen/logrus"
@@ -26,17 +24,13 @@ type MetricsServer struct {
kubectlExecPendingGauge *prometheus.GaugeVec
k8sRequestCounter *prometheus.CounterVec
clusterEventsCounter *prometheus.CounterVec
redisRequestCounter *prometheus.CounterVec
reconcileHistogram *prometheus.HistogramVec
redisRequestHistogram *prometheus.HistogramVec
registry *prometheus.Registry
}
const (
// MetricsPath is the endpoint to collect application metrics
MetricsPath = "/metrics"
// EnvVarLegacyControllerMetrics is a env var to re-enable deprecated prometheus metrics
EnvVarLegacyControllerMetrics = "ARGOCD_LEGACY_CONTROLLER_METRICS"
)
// Follow Prometheus naming practices
@@ -47,88 +41,27 @@ var (
descAppInfo = prometheus.NewDesc(
"argocd_app_info",
"Information about application.",
append(descAppDefaultLabels, "repo", "dest_server", "dest_namespace", "sync_status", "health_status", "operation"),
append(descAppDefaultLabels, "repo", "dest_server", "dest_namespace"),
nil,
)
// DEPRECATED
descAppCreated = prometheus.NewDesc(
"argocd_app_created_time",
"Creation time in unix timestamp for an application.",
descAppDefaultLabels,
nil,
)
// DEPRECATED: superceded by sync_status label in argocd_app_info
descAppSyncStatusCode = prometheus.NewDesc(
"argocd_app_sync_status",
"The application current sync status.",
append(descAppDefaultLabels, "sync_status"),
nil,
)
// DEPRECATED: superceded by health_status label in argocd_app_info
descAppHealthStatus = prometheus.NewDesc(
"argocd_app_health_status",
"The application current health status.",
append(descAppDefaultLabels, "health_status"),
nil,
)
syncCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "argocd_app_sync_total",
Help: "Number of application syncs.",
},
append(descAppDefaultLabels, "dest_server", "phase"),
)
k8sRequestCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "argocd_app_k8s_request_total",
Help: "Number of kubernetes requests executed during application reconciliation.",
},
append(descAppDefaultLabels, "server", "response_code", "verb", "resource_kind", "resource_namespace"),
)
kubectlExecCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "argocd_kubectl_exec_total",
Help: "Number of kubectl executions",
}, []string{"command"})
kubectlExecPendingGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "argocd_kubectl_exec_pending",
Help: "Number of pending kubectl executions",
}, []string{"command"})
reconcileHistogram = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "argocd_app_reconcile",
Help: "Application reconciliation performance.",
// Buckets chosen after observing a ~2100ms mean reconcile time
Buckets: []float64{0.25, .5, 1, 2, 4, 8, 16},
},
[]string{"namespace", "dest_server"},
)
clusterEventsCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "argocd_cluster_events_total",
Help: "Number of processes k8s resource events.",
}, append(descClusterDefaultLabels, "group", "kind"))
redisRequestCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "argocd_redis_request_total",
Help: "Number of kubernetes requests executed during application reconciliation.",
},
[]string{"initiator", "failed"},
)
redisRequestHistogram = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "argocd_redis_request_duration",
Help: "Redis requests duration.",
Buckets: []float64{0.01, 0.05, 0.10, 0.25, .5, 1},
},
[]string{"initiator"},
)
)
// NewMetricsServer returns a new prometheus server which collects application metrics
@@ -143,14 +76,51 @@ func NewMetricsServer(addr string, appLister applister.ApplicationLister, health
}, promhttp.HandlerOpts{}))
healthz.ServeHealthCheck(mux, healthCheck)
syncCounter := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "argocd_app_sync_total",
Help: "Number of application syncs.",
},
append(descAppDefaultLabels, "phase"),
)
registry.MustRegister(syncCounter)
k8sRequestCounter := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "argocd_app_k8s_request_total",
Help: "Number of kubernetes requests executed during application reconciliation.",
},
append(descAppDefaultLabels, "response_code"),
)
registry.MustRegister(k8sRequestCounter)
kubectlExecCounter := prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "argocd_kubectl_exec_total",
Help: "Number of kubectl executions",
}, []string{"command"})
registry.MustRegister(kubectlExecCounter)
kubectlExecPendingGauge := prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "argocd_kubectl_exec_pending",
Help: "Number of pending kubectl executions",
}, []string{"command"})
registry.MustRegister(kubectlExecPendingGauge)
reconcileHistogram := prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "argocd_app_reconcile",
Help: "Application reconciliation performance.",
// Buckets chosen after observing a ~2100ms mean reconcile time
Buckets: []float64{0.25, .5, 1, 2, 4, 8, 16},
},
descAppDefaultLabels,
)
registry.MustRegister(reconcileHistogram)
clusterEventsCounter := prometheus.NewCounterVec(prometheus.CounterOpts{
Name: "argocd_cluster_events_total",
Help: "Number of processes k8s resource events.",
}, descClusterDefaultLabels)
registry.MustRegister(clusterEventsCounter)
registry.MustRegister(redisRequestCounter)
registry.MustRegister(redisRequestHistogram)
return &MetricsServer{
registry: registry,
@@ -164,8 +134,6 @@ func NewMetricsServer(addr string, appLister applister.ApplicationLister, health
kubectlExecPendingGauge: kubectlExecPendingGauge,
reconcileHistogram: reconcileHistogram,
clusterEventsCounter: clusterEventsCounter,
redisRequestCounter: redisRequestCounter,
redisRequestHistogram: redisRequestHistogram,
}
}
@@ -180,7 +148,7 @@ func (m *MetricsServer) IncSync(app *argoappv1.Application, state *argoappv1.Ope
if !state.Phase.Completed() {
return
}
m.syncCounter.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject(), app.Spec.Destination.Server, string(state.Phase)).Inc()
m.syncCounter.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject(), string(state.Phase)).Inc()
}
func (m *MetricsServer) IncKubectlExec(command string) {
@@ -196,36 +164,18 @@ func (m *MetricsServer) DecKubectlExecPending(command string) {
}
// IncClusterEventsCount increments the number of cluster events
func (m *MetricsServer) IncClusterEventsCount(server, group, kind string) {
m.clusterEventsCounter.WithLabelValues(server, group, kind).Inc()
func (m *MetricsServer) IncClusterEventsCount(server string) {
m.clusterEventsCounter.WithLabelValues(server).Inc()
}
// IncKubernetesRequest increments the kubernetes requests counter for an application
func (m *MetricsServer) IncKubernetesRequest(app *argoappv1.Application, server, statusCode, verb, resourceKind, resourceNamespace string) {
var namespace, name, project string
if app != nil {
namespace = app.Namespace
name = app.Name
project = app.Spec.GetProject()
}
m.k8sRequestCounter.WithLabelValues(
namespace, name, project, server, statusCode,
verb, resourceKind, resourceNamespace,
).Inc()
}
func (m *MetricsServer) IncRedisRequest(failed bool) {
m.redisRequestCounter.WithLabelValues("argocd-application-controller", strconv.FormatBool(failed)).Inc()
}
// ObserveRedisRequestDuration observes redis request duration
func (m *MetricsServer) ObserveRedisRequestDuration(duration time.Duration) {
m.redisRequestHistogram.WithLabelValues("argocd-application-controller").Observe(duration.Seconds())
func (m *MetricsServer) IncKubernetesRequest(app *argoappv1.Application, statusCode int) {
m.k8sRequestCounter.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject(), strconv.Itoa(statusCode)).Inc()
}
// IncReconcile increments the reconcile counter for an application
func (m *MetricsServer) IncReconcile(app *argoappv1.Application, duration time.Duration) {
m.reconcileHistogram.WithLabelValues(app.Namespace, app.Spec.Destination.Server).Observe(duration.Seconds())
m.reconcileHistogram.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject()).Observe(duration.Seconds())
}
type appCollector struct {
@@ -249,6 +199,7 @@ func NewAppRegistry(appLister applister.ApplicationLister) *prometheus.Registry
// Describe implements the prometheus.Collector interface
func (c *appCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- descAppInfo
ch <- descAppCreated
ch <- descAppSyncStatusCode
ch <- descAppHealthStatus
}
@@ -282,37 +233,20 @@ func collectApps(ch chan<- prometheus.Metric, app *argoappv1.Application) {
addConstMetric(desc, prometheus.GaugeValue, v, lv...)
}
var operation string
if app.DeletionTimestamp != nil {
operation = "delete"
} else if app.Operation != nil && app.Operation.Sync != nil {
operation = "sync"
}
addGauge(descAppInfo, 1, git.NormalizeGitURL(app.Spec.Source.RepoURL), app.Spec.Destination.Server, app.Spec.Destination.Namespace)
addGauge(descAppCreated, float64(app.CreationTimestamp.Unix()))
syncStatus := app.Status.Sync.Status
if syncStatus == "" {
syncStatus = argoappv1.SyncStatusCodeUnknown
}
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeSynced), string(argoappv1.SyncStatusCodeSynced))
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeOutOfSync), string(argoappv1.SyncStatusCodeOutOfSync))
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeUnknown || syncStatus == ""), string(argoappv1.SyncStatusCodeUnknown))
healthStatus := app.Status.Health.Status
if healthStatus == "" {
healthStatus = health.HealthStatusUnknown
}
addGauge(descAppInfo, 1, git.NormalizeGitURL(app.Spec.Source.RepoURL), app.Spec.Destination.Server, app.Spec.Destination.Namespace, string(syncStatus), string(healthStatus), operation)
// Deprecated controller metrics
if os.Getenv(EnvVarLegacyControllerMetrics) == "true" {
addGauge(descAppCreated, float64(app.CreationTimestamp.Unix()))
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeSynced), string(argoappv1.SyncStatusCodeSynced))
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeOutOfSync), string(argoappv1.SyncStatusCodeOutOfSync))
addGauge(descAppSyncStatusCode, boolFloat64(syncStatus == argoappv1.SyncStatusCodeUnknown || syncStatus == ""), string(argoappv1.SyncStatusCodeUnknown))
healthStatus := app.Status.Health.Status
addGauge(descAppHealthStatus, boolFloat64(healthStatus == health.HealthStatusUnknown || healthStatus == ""), string(health.HealthStatusUnknown))
addGauge(descAppHealthStatus, boolFloat64(healthStatus == health.HealthStatusProgressing), string(health.HealthStatusProgressing))
addGauge(descAppHealthStatus, boolFloat64(healthStatus == health.HealthStatusSuspended), string(health.HealthStatusSuspended))
addGauge(descAppHealthStatus, boolFloat64(healthStatus == health.HealthStatusHealthy), string(health.HealthStatusHealthy))
addGauge(descAppHealthStatus, boolFloat64(healthStatus == health.HealthStatusDegraded), string(health.HealthStatusDegraded))
addGauge(descAppHealthStatus, boolFloat64(healthStatus == health.HealthStatusMissing), string(health.HealthStatusMissing))
}
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusUnknown || healthStatus == ""), argoappv1.HealthStatusUnknown)
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusProgressing), argoappv1.HealthStatusProgressing)
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusSuspended), argoappv1.HealthStatusSuspended)
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusHealthy), argoappv1.HealthStatusHealthy)
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusDegraded), argoappv1.HealthStatusDegraded)
addGauge(descAppHealthStatus, boolFloat64(healthStatus == argoappv1.HealthStatusMissing), argoappv1.HealthStatusMissing)
}

View File

@@ -5,12 +5,10 @@ import (
"log"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"github.com/argoproj/gitops-engine/pkg/sync/common"
"github.com/ghodss/yaml"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -44,52 +42,25 @@ status:
status: Healthy
`
const fakeApp2 = `
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app-2
namespace: argocd
spec:
destination:
namespace: dummy-namespace
server: https://localhost:6443
project: important-project
source:
path: some/path
repoURL: https://github.com/argoproj/argocd-example-apps.git
status:
sync:
status: Synced
health:
status: Healthy
operation:
sync:
revision: 041eab7439ece92c99b043f0e171788185b8fc1d
syncStrategy:
hook: {}
`
const fakeApp3 = `
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app-3
namespace: argocd
deletionTimestamp: "2020-03-16T09:17:45Z"
spec:
destination:
namespace: dummy-namespace
server: https://localhost:6443
project: important-project
source:
path: some/path
repoURL: https://github.com/argoproj/argocd-example-apps.git
status:
sync:
status: OutOfSync
health:
status: Degraded
const expectedResponse = `# HELP argocd_app_created_time Creation time in unix timestamp for an application.
# TYPE argocd_app_created_time gauge
argocd_app_created_time{name="my-app",namespace="argocd",project="important-project"} -6.21355968e+10
# HELP argocd_app_health_status The application current health status.
# TYPE argocd_app_health_status gauge
argocd_app_health_status{health_status="Degraded",name="my-app",namespace="argocd",project="important-project"} 0
argocd_app_health_status{health_status="Healthy",name="my-app",namespace="argocd",project="important-project"} 1
argocd_app_health_status{health_status="Missing",name="my-app",namespace="argocd",project="important-project"} 0
argocd_app_health_status{health_status="Progressing",name="my-app",namespace="argocd",project="important-project"} 0
argocd_app_health_status{health_status="Suspended",name="my-app",namespace="argocd",project="important-project"} 0
argocd_app_health_status{health_status="Unknown",name="my-app",namespace="argocd",project="important-project"} 0
# HELP argocd_app_info Information about application.
# TYPE argocd_app_info gauge
argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",name="my-app",namespace="argocd",project="important-project",repo="https://github.com/argoproj/argocd-example-apps"} 1
# HELP argocd_app_sync_status The application current sync status.
# TYPE argocd_app_sync_status gauge
argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="OutOfSync"} 0
argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="Synced"} 1
argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="Unknown"} 0
`
const fakeDefaultApp = `
@@ -112,26 +83,46 @@ status:
status: Healthy
`
const expectedDefaultResponse = `# HELP argocd_app_created_time Creation time in unix timestamp for an application.
# TYPE argocd_app_created_time gauge
argocd_app_created_time{name="my-app",namespace="argocd",project="default"} -6.21355968e+10
# HELP argocd_app_health_status The application current health status.
# TYPE argocd_app_health_status gauge
argocd_app_health_status{health_status="Degraded",name="my-app",namespace="argocd",project="default"} 0
argocd_app_health_status{health_status="Healthy",name="my-app",namespace="argocd",project="default"} 1
argocd_app_health_status{health_status="Missing",name="my-app",namespace="argocd",project="default"} 0
argocd_app_health_status{health_status="Progressing",name="my-app",namespace="argocd",project="default"} 0
argocd_app_health_status{health_status="Suspended",name="my-app",namespace="argocd",project="default"} 0
argocd_app_health_status{health_status="Unknown",name="my-app",namespace="argocd",project="default"} 0
# HELP argocd_app_info Information about application.
# TYPE argocd_app_info gauge
argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",name="my-app",namespace="argocd",project="default",repo="https://github.com/argoproj/argocd-example-apps"} 1
# HELP argocd_app_sync_status The application current sync status.
# TYPE argocd_app_sync_status gauge
argocd_app_sync_status{name="my-app",namespace="argocd",project="default",sync_status="OutOfSync"} 0
argocd_app_sync_status{name="my-app",namespace="argocd",project="default",sync_status="Synced"} 1
argocd_app_sync_status{name="my-app",namespace="argocd",project="default",sync_status="Unknown"} 0
`
var noOpHealthCheck = func() error {
return nil
}
func newFakeApp(fakeAppYAML string) *argoappv1.Application {
func newFakeApp(fakeApp string) *argoappv1.Application {
var app argoappv1.Application
err := yaml.Unmarshal([]byte(fakeAppYAML), &app)
err := yaml.Unmarshal([]byte(fakeApp), &app)
if err != nil {
panic(err)
}
return &app
}
func newFakeLister(fakeAppYAMLs ...string) (context.CancelFunc, applister.ApplicationLister) {
func newFakeLister(fakeApp ...string) (context.CancelFunc, applister.ApplicationLister) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var fakeApps []runtime.Object
for _, appYAML := range fakeAppYAMLs {
a := newFakeApp(appYAML)
fakeApps = append(fakeApps, a)
for _, name := range fakeApp {
fakeApps = append(fakeApps, newFakeApp(name))
}
appClientset := appclientset.NewSimpleClientset(fakeApps...)
factory := appinformer.NewFilteredSharedInformerFactory(appClientset, 0, "argocd", func(options *metav1.ListOptions) {})
@@ -143,8 +134,8 @@ func newFakeLister(fakeAppYAMLs ...string) (context.CancelFunc, applister.Applic
return cancel, factory.Argoproj().V1alpha1().Applications().Lister()
}
func testApp(t *testing.T, fakeAppYAMLs []string, expectedResponse string) {
cancel, appLister := newFakeLister(fakeAppYAMLs...)
func testApp(t *testing.T, fakeApp string, expectedResponse string) {
cancel, appLister := newFakeLister(fakeApp)
defer cancel()
metricsServ := NewMetricsServer("localhost:8082", appLister, noOpHealthCheck)
req, err := http.NewRequest("GET", "/metrics", nil)
@@ -158,81 +149,45 @@ func testApp(t *testing.T, fakeAppYAMLs []string, expectedResponse string) {
}
type testCombination struct {
applications []string
application string
expectedResponse string
}
func TestMetrics(t *testing.T) {
combinations := []testCombination{
{
applications: []string{fakeApp, fakeApp2, fakeApp3},
expectedResponse: `
# HELP argocd_app_info Information about application.
# TYPE argocd_app_info gauge
argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",health_status="Degraded",name="my-app-3",namespace="argocd",operation="delete",project="important-project",repo="https://github.com/argoproj/argocd-example-apps",sync_status="OutOfSync"} 1
argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",health_status="Healthy",name="my-app",namespace="argocd",operation="",project="important-project",repo="https://github.com/argoproj/argocd-example-apps",sync_status="Synced"} 1
argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",health_status="Healthy",name="my-app-2",namespace="argocd",operation="sync",project="important-project",repo="https://github.com/argoproj/argocd-example-apps",sync_status="Synced"} 1
`,
application: fakeApp,
expectedResponse: expectedResponse,
},
{
applications: []string{fakeDefaultApp},
expectedResponse: `
# HELP argocd_app_info Information about application.
# TYPE argocd_app_info gauge
argocd_app_info{dest_namespace="dummy-namespace",dest_server="https://localhost:6443",health_status="Healthy",name="my-app",namespace="argocd",operation="",project="default",repo="https://github.com/argoproj/argocd-example-apps",sync_status="Synced"} 1
`,
application: fakeDefaultApp,
expectedResponse: expectedDefaultResponse,
},
}
for _, combination := range combinations {
testApp(t, combination.applications, combination.expectedResponse)
testApp(t, combination.application, combination.expectedResponse)
}
}
func TestLegacyMetrics(t *testing.T) {
os.Setenv(EnvVarLegacyControllerMetrics, "true")
defer os.Unsetenv(EnvVarLegacyControllerMetrics)
expectedResponse := `
# HELP argocd_app_created_time Creation time in unix timestamp for an application.
# TYPE argocd_app_created_time gauge
argocd_app_created_time{name="my-app",namespace="argocd",project="important-project"} -6.21355968e+10
# HELP argocd_app_health_status The application current health status.
# TYPE argocd_app_health_status gauge
argocd_app_health_status{health_status="Degraded",name="my-app",namespace="argocd",project="important-project"} 0
argocd_app_health_status{health_status="Healthy",name="my-app",namespace="argocd",project="important-project"} 1
argocd_app_health_status{health_status="Missing",name="my-app",namespace="argocd",project="important-project"} 0
argocd_app_health_status{health_status="Progressing",name="my-app",namespace="argocd",project="important-project"} 0
argocd_app_health_status{health_status="Suspended",name="my-app",namespace="argocd",project="important-project"} 0
argocd_app_health_status{health_status="Unknown",name="my-app",namespace="argocd",project="important-project"} 0
# HELP argocd_app_sync_status The application current sync status.
# TYPE argocd_app_sync_status gauge
argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="OutOfSync"} 0
argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="Synced"} 1
argocd_app_sync_status{name="my-app",namespace="argocd",project="important-project",sync_status="Unknown"} 0
const appSyncTotal = `# HELP argocd_app_sync_total Number of application syncs.
# TYPE argocd_app_sync_total counter
argocd_app_sync_total{name="my-app",namespace="argocd",phase="Error",project="important-project"} 1
argocd_app_sync_total{name="my-app",namespace="argocd",phase="Failed",project="important-project"} 1
argocd_app_sync_total{name="my-app",namespace="argocd",phase="Succeeded",project="important-project"} 2
`
testApp(t, []string{fakeApp}, expectedResponse)
}
func TestMetricsSyncCounter(t *testing.T) {
cancel, appLister := newFakeLister()
defer cancel()
metricsServ := NewMetricsServer("localhost:8082", appLister, noOpHealthCheck)
appSyncTotal := `
# HELP argocd_app_sync_total Number of application syncs.
# TYPE argocd_app_sync_total counter
argocd_app_sync_total{dest_server="https://localhost:6443",name="my-app",namespace="argocd",phase="Error",project="important-project"} 1
argocd_app_sync_total{dest_server="https://localhost:6443",name="my-app",namespace="argocd",phase="Failed",project="important-project"} 1
argocd_app_sync_total{dest_server="https://localhost:6443",name="my-app",namespace="argocd",phase="Succeeded",project="important-project"} 2
`
fakeApp := newFakeApp(fakeApp)
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: common.OperationRunning})
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: common.OperationFailed})
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: common.OperationError})
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: common.OperationSucceeded})
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: common.OperationSucceeded})
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: argoappv1.OperationRunning})
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: argoappv1.OperationFailed})
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: argoappv1.OperationError})
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: argoappv1.OperationSucceeded})
metricsServ.IncSync(fakeApp, &argoappv1.OperationState{Phase: argoappv1.OperationSucceeded})
req, err := http.NewRequest("GET", "/metrics", nil)
assert.NoError(t, err)
@@ -247,31 +202,27 @@ argocd_app_sync_total{dest_server="https://localhost:6443",name="my-app",namespa
// assertMetricsPrinted asserts every line in the expected lines appears in the body
func assertMetricsPrinted(t *testing.T, expectedLines, body string) {
for _, line := range strings.Split(expectedLines, "\n") {
if line == "" {
continue
}
assert.Contains(t, body, line)
}
}
const appReconcileMetrics = `argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="0.25"} 0
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="0.5"} 0
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="1"} 0
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="2"} 0
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="4"} 0
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="8"} 1
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="16"} 1
argocd_app_reconcile_bucket{name="my-app",namespace="argocd",project="important-project",le="+Inf"} 1
argocd_app_reconcile_sum{name="my-app",namespace="argocd",project="important-project"} 5
argocd_app_reconcile_count{name="my-app",namespace="argocd",project="important-project"} 1
`
func TestReconcileMetrics(t *testing.T) {
cancel, appLister := newFakeLister()
defer cancel()
metricsServ := NewMetricsServer("localhost:8082", appLister, noOpHealthCheck)
appReconcileMetrics := `
# HELP argocd_app_reconcile Application reconciliation performance.
# TYPE argocd_app_reconcile histogram
argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="0.25"} 0
argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="0.5"} 0
argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="1"} 0
argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="2"} 0
argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="4"} 0
argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="8"} 1
argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="16"} 1
argocd_app_reconcile_bucket{dest_server="https://localhost:6443",namespace="argocd",le="+Inf"} 1
argocd_app_reconcile_sum{dest_server="https://localhost:6443",namespace="argocd"} 5
argocd_app_reconcile_count{dest_server="https://localhost:6443",namespace="argocd"} 1
`
fakeApp := newFakeApp(fakeApp)
metricsServ.IncReconcile(fakeApp, 5*time.Second)

View File

@@ -1,24 +1,37 @@
package metrics
import (
"strconv"
"net/http"
"github.com/argoproj/pkg/kubeclientmetrics"
"k8s.io/client-go/rest"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
)
type metricsRoundTripper struct {
roundTripper http.RoundTripper
app *v1alpha1.Application
metricsServer *MetricsServer
}
func (mrt *metricsRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
resp, err := mrt.roundTripper.RoundTrip(r)
statusCode := 0
if resp != nil {
statusCode = resp.StatusCode
}
mrt.metricsServer.IncKubernetesRequest(mrt.app, statusCode)
return resp, err
}
// AddMetricsTransportWrapper adds a transport wrapper which increments 'argocd_app_k8s_request_total' counter on each kubernetes request
func AddMetricsTransportWrapper(server *MetricsServer, app *v1alpha1.Application, config *rest.Config) *rest.Config {
inc := func(resourceInfo kubeclientmetrics.ResourceInfo) error {
namespace := resourceInfo.Namespace
kind := resourceInfo.Kind
statusCode := strconv.Itoa(resourceInfo.StatusCode)
server.IncKubernetesRequest(app, resourceInfo.Server, statusCode, string(resourceInfo.Verb), kind, namespace)
return nil
wrap := config.WrapTransport
config.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
if wrap != nil {
rt = wrap(rt)
}
return &metricsRoundTripper{roundTripper: rt, metricsServer: server, app: app}
}
newConfig := kubeclientmetrics.AddMetricsTransportWrapper(config, inc)
return newConfig
return config
}

View File

@@ -6,14 +6,6 @@ import (
"fmt"
"time"
"github.com/argoproj/gitops-engine/pkg/diff"
"github.com/argoproj/gitops-engine/pkg/health"
"github.com/argoproj/gitops-engine/pkg/sync"
hookutil "github.com/argoproj/gitops-engine/pkg/sync/hook"
"github.com/argoproj/gitops-engine/pkg/sync/ignore"
resourceutil "github.com/argoproj/gitops-engine/pkg/sync/resource"
"github.com/argoproj/gitops-engine/pkg/utils/io"
kubeutil "github.com/argoproj/gitops-engine/pkg/utils/kube"
log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -28,20 +20,18 @@ import (
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
"github.com/argoproj/argo-cd/reposerver/apiclient"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/argo"
"github.com/argoproj/argo-cd/util/db"
argohealth "github.com/argoproj/argo-cd/util/health"
"github.com/argoproj/argo-cd/util/diff"
"github.com/argoproj/argo-cd/util/health"
hookutil "github.com/argoproj/argo-cd/util/hook"
kubeutil "github.com/argoproj/argo-cd/util/kube"
"github.com/argoproj/argo-cd/util/resource"
"github.com/argoproj/argo-cd/util/resource/ignore"
"github.com/argoproj/argo-cd/util/settings"
"github.com/argoproj/argo-cd/util/stats"
)
type resourceInfoProviderStub struct {
}
func (r *resourceInfoProviderStub) IsNamespaced(_ schema.GroupKind) (bool, error) {
return false, nil
}
type managedResource struct {
Target *unstructured.Unstructured
Live *unstructured.Unstructured
@@ -62,6 +52,10 @@ func GetLiveObjs(res []managedResource) []*unstructured.Unstructured {
return objs
}
type ResourceInfoProvider interface {
IsNamespaced(server string, gk schema.GroupKind) (bool, error)
}
// AppStateManager defines methods which allow to compare application spec and actual application state.
type AppStateManager interface {
CompareAppState(app *v1alpha1.Application, project *appv1.AppProject, revision string, source v1alpha1.ApplicationSource, noCache bool, localObjects []string) *comparisonResult
@@ -69,15 +63,23 @@ type AppStateManager interface {
}
type comparisonResult struct {
syncStatus *v1alpha1.SyncStatus
healthStatus *v1alpha1.HealthStatus
resources []v1alpha1.ResourceStatus
managedResources []managedResource
reconciliationResult sync.ReconciliationResult
diffNormalizer diff.Normalizer
appSourceType v1alpha1.ApplicationSourceType
// timings maps phases of comparison to the duration it took to complete (for statistical purposes)
timings map[string]time.Duration
syncStatus *v1alpha1.SyncStatus
healthStatus *v1alpha1.HealthStatus
resources []v1alpha1.ResourceStatus
managedResources []managedResource
hooks []*unstructured.Unstructured
diffNormalizer diff.Normalizer
appSourceType v1alpha1.ApplicationSourceType
}
func (cr *comparisonResult) targetObjs() []*unstructured.Unstructured {
objs := cr.hooks
for _, r := range cr.managedResources {
if r.Target != nil {
objs = append(objs, r.Target)
}
}
return objs
}
// appStateManager allows to compare applications to git
@@ -93,23 +95,20 @@ type appStateManager struct {
namespace string
}
func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1.ApplicationSource, appLabelKey, revision string, noCache bool) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error) {
ts := stats.NewTimingStats()
func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1.ApplicationSource, appLabelKey, revision string, noCache bool) ([]*unstructured.Unstructured, []*unstructured.Unstructured, *apiclient.ManifestResponse, error) {
helmRepos, err := m.db.ListHelmRepositories(context.Background())
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
ts.AddCheckpoint("helm_ms")
repo, err := m.db.GetRepository(context.Background(), source.RepoURL)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
ts.AddCheckpoint("repo_ms")
conn, repoClient, err := m.repoClientset.NewRepoServerClient()
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
defer io.Close(conn)
defer util.Close(conn)
if revision == "" {
revision = source.TargetRevision
@@ -117,28 +116,22 @@ func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1
plugins, err := m.settingsMgr.GetConfigManagementPlugins()
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
ts.AddCheckpoint("plugins_ms")
tools := make([]*appv1.ConfigManagementPlugin, len(plugins))
for i := range plugins {
tools[i] = &plugins[i]
}
kustomizeSettings, err := m.settingsMgr.GetKustomizeSettings()
buildOptions, err := m.settingsMgr.GetKustomizeBuildOptions()
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
kustomizeOptions, err := kustomizeSettings.GetOptions(app.Spec.Source)
serverVersion, err := m.liveStateCache.GetServerVersion(app.Spec.Destination.Server)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
ts.AddCheckpoint("build_options_ms")
serverVersion, apiGroups, err := m.liveStateCache.GetVersionsInfo(app.Spec.Destination.Server)
if err != nil {
return nil, nil, err
}
ts.AddCheckpoint("version_ms")
manifestInfo, err := repoClient.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
Repo: repo,
Repos: helmRepos,
@@ -149,63 +142,61 @@ func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1
Namespace: app.Spec.Destination.Namespace,
ApplicationSource: &source,
Plugins: tools,
KustomizeOptions: kustomizeOptions,
KubeVersion: serverVersion,
ApiVersions: argo.APIGroupsToVersions(apiGroups),
KustomizeOptions: &appv1.KustomizeOptions{
BuildOptions: buildOptions,
},
KubeVersion: serverVersion,
})
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
targetObjs, err := unmarshalManifests(manifestInfo.Manifests)
targetObjs, hooks, err := unmarshalManifests(manifestInfo.Manifests)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
ts.AddCheckpoint("unmarshal_ms")
logCtx := log.WithField("application", app.Name)
for k, v := range ts.Timings() {
logCtx = logCtx.WithField(k, v.Milliseconds())
}
logCtx = logCtx.WithField("time_ms", time.Since(ts.StartTime).Milliseconds())
logCtx.Info("getRepoObjs stats")
return targetObjs, manifestInfo, nil
return targetObjs, hooks, manifestInfo, nil
}
func unmarshalManifests(manifests []string) ([]*unstructured.Unstructured, error) {
func unmarshalManifests(manifests []string) ([]*unstructured.Unstructured, []*unstructured.Unstructured, error) {
targetObjs := make([]*unstructured.Unstructured, 0)
hooks := make([]*unstructured.Unstructured, 0)
for _, manifest := range manifests {
obj, err := v1alpha1.UnmarshalToUnstructured(manifest)
if err != nil {
return nil, err
return nil, nil, err
}
if ignore.Ignore(obj) {
continue
}
if hookutil.IsHook(obj) {
hooks = append(hooks, obj)
} else {
targetObjs = append(targetObjs, obj)
}
targetObjs = append(targetObjs, obj)
}
return targetObjs, nil
return targetObjs, hooks, nil
}
func DeduplicateTargetObjects(
server string,
namespace string,
objs []*unstructured.Unstructured,
infoProvider kubeutil.ResourceInfoProvider,
infoProvider ResourceInfoProvider,
) ([]*unstructured.Unstructured, []v1alpha1.ApplicationCondition, error) {
targetByKey := make(map[kubeutil.ResourceKey][]*unstructured.Unstructured)
for i := range objs {
obj := objs[i]
if obj == nil {
continue
isNamespaced, err := infoProvider.IsNamespaced(server, obj.GroupVersionKind().GroupKind())
if err != nil {
return objs, nil, err
}
isNamespaced := kubeutil.IsNamespacedOrUnknown(infoProvider, obj.GroupVersionKind().GroupKind())
if !isNamespaced {
obj.SetNamespace("")
} else if obj.GetNamespace() == "" {
obj.SetNamespace(namespace)
}
key := kubeutil.GetResourceKey(obj)
if key.Name == "" && obj.GetGenerateName() != "" {
key.Name = fmt.Sprintf("%s%d", obj.GetGenerateName(), i)
}
targetByKey[key] = append(targetByKey[key], obj)
}
conditions := make([]v1alpha1.ApplicationCondition, 0)
@@ -225,33 +216,63 @@ func DeduplicateTargetObjects(
return result, conditions, nil
}
func (m *appStateManager) getComparisonSettings(app *appv1.Application) (string, map[string]v1alpha1.ResourceOverride, diff.Normalizer, *settings.ResourcesFilter, error) {
// dedupLiveResources handles removes live resource duplicates with the same UID. Duplicates are created in a separate resource groups.
// E.g. apps/Deployment produces duplicate in extensions/Deployment, authorization.openshift.io/ClusterRole produces duplicate in rbac.authorization.k8s.io/ClusterRole etc.
// The method removes such duplicates unless it was defined in git ( exists in target resources list ). At least one duplicate stays.
// If non of duplicates are in git at random one stays
func dedupLiveResources(targetObjs []*unstructured.Unstructured, liveObjsByKey map[kubeutil.ResourceKey]*unstructured.Unstructured) {
targetObjByKey := make(map[kubeutil.ResourceKey]*unstructured.Unstructured)
for i := range targetObjs {
targetObjByKey[kubeutil.GetResourceKey(targetObjs[i])] = targetObjs[i]
}
liveObjsById := make(map[types.UID][]*unstructured.Unstructured)
for k := range liveObjsByKey {
obj := liveObjsByKey[k]
if obj != nil {
liveObjsById[obj.GetUID()] = append(liveObjsById[obj.GetUID()], obj)
}
}
for id := range liveObjsById {
objs := liveObjsById[id]
if len(objs) > 1 {
duplicatesLeft := len(objs)
for i := range objs {
obj := objs[i]
resourceKey := kubeutil.GetResourceKey(obj)
if _, ok := targetObjByKey[resourceKey]; !ok {
delete(liveObjsByKey, resourceKey)
duplicatesLeft--
if duplicatesLeft == 1 {
break
}
}
}
}
}
}
func (m *appStateManager) getComparisonSettings(app *appv1.Application) (string, map[string]v1alpha1.ResourceOverride, diff.Normalizer, error) {
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
return "", 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 appLabelKey, resourceOverrides, diffNormalizer, resFilter, nil
return appLabelKey, resourceOverrides, diffNormalizer, nil
}
// CompareAppState compares application git state to the live app state, using the specified
// revision and supplied source. If revision or overrides are empty, then compares against
// revision and overrides in the app spec.
func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *appv1.AppProject, revision string, source v1alpha1.ApplicationSource, noCache bool, localManifests []string) *comparisonResult {
ts := stats.NewTimingStats()
appLabelKey, resourceOverrides, diffNormalizer, resFilter, err := m.getComparisonSettings(app)
ts.AddCheckpoint("settings_ms")
appLabelKey, resourceOverrides, diffNormalizer, err := m.getComparisonSettings(app)
// return unknown comparison result if basic comparison settings cannot be loaded
if err != nil {
@@ -260,7 +281,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
ComparedTo: appv1.ComparedTo{Source: source, Destination: app.Spec.Destination},
Status: appv1.SyncStatusCodeUnknown,
},
healthStatus: &appv1.HealthStatus{Status: health.HealthStatusUnknown},
healthStatus: &appv1.HealthStatus{Status: appv1.HealthStatusUnknown},
}
}
@@ -272,18 +293,19 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
logCtx.Infof("Comparing app state (cluster: %s, namespace: %s)", app.Spec.Destination.Server, app.Spec.Destination.Namespace)
var targetObjs []*unstructured.Unstructured
var hooks []*unstructured.Unstructured
var manifestInfo *apiclient.ManifestResponse
now := metav1.Now()
if len(localManifests) == 0 {
targetObjs, manifestInfo, err = m.getRepoObjs(app, source, appLabelKey, revision, noCache)
targetObjs, hooks, manifestInfo, err = m.getRepoObjs(app, source, appLabelKey, revision, noCache)
if err != nil {
targetObjs = make([]*unstructured.Unstructured, 0)
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
failedToLoadObjs = true
}
} else {
targetObjs, err = unmarshalManifests(localManifests)
targetObjs, hooks, err = unmarshalManifests(localManifests)
if err != nil {
targetObjs = make([]*unstructured.Unstructured, 0)
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
@@ -291,40 +313,39 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
}
manifestInfo = nil
}
ts.AddCheckpoint("git_ms")
var infoProvider kubeutil.ResourceInfoProvider
infoProvider, err = m.liveStateCache.GetClusterCache(app.Spec.Destination.Server)
if err != nil {
infoProvider = &resourceInfoProviderStub{}
}
targetObjs, dedupConditions, err := DeduplicateTargetObjects(app.Spec.Destination.Namespace, targetObjs, infoProvider)
targetObjs, dedupConditions, err := DeduplicateTargetObjects(app.Spec.Destination.Server, app.Spec.Destination.Namespace, targetObjs, m.liveStateCache)
if err != nil {
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
}
conditions = append(conditions, dedupConditions...)
for i := len(targetObjs) - 1; i >= 0; i-- {
targetObj := targetObjs[i]
gvk := targetObj.GroupVersionKind()
if resFilter.IsExcludedResource(gvk.Group, gvk.Kind, app.Spec.Destination.Server) {
targetObjs = append(targetObjs[:i], targetObjs[i+1:]...)
conditions = append(conditions, v1alpha1.ApplicationCondition{
Type: v1alpha1.ApplicationConditionExcludedResourceWarning,
Message: fmt.Sprintf("Resource %s/%s %s is excluded in the settings", gvk.Group, gvk.Kind, targetObj.GetName()),
LastTransitionTime: &now,
})
resFilter, err := m.settingsMgr.GetResourcesFilter()
if err != nil {
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
} else {
for i := len(targetObjs) - 1; i >= 0; i-- {
targetObj := targetObjs[i]
gvk := targetObj.GroupVersionKind()
if resFilter.IsExcludedResource(gvk.Group, gvk.Kind, app.Spec.Destination.Server) {
targetObjs = append(targetObjs[:i], targetObjs[i+1:]...)
conditions = append(conditions, v1alpha1.ApplicationCondition{
Type: v1alpha1.ApplicationConditionExcludedResourceWarning,
Message: fmt.Sprintf("Resource %s/%s %s is excluded in the settings", gvk.Group, gvk.Kind, targetObj.GetName()),
LastTransitionTime: &now,
})
}
}
}
ts.AddCheckpoint("dedup_ms")
logCtx.Debugf("Generated config manifests")
liveObjByKey, err := m.liveStateCache.GetManagedLiveObjs(app, targetObjs)
if err != nil {
liveObjByKey = make(map[kubeutil.ResourceKey]*unstructured.Unstructured)
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
failedToLoadObjs = true
}
logCtx.Debugf("Retrieved lived manifests")
dedupLiveResources(targetObjs, liveObjByKey)
// filter out all resources which are not permitted in the application project
for k, v := range liveObjByKey {
if !project.IsLiveResourcePermitted(v, app.Spec.Destination.Server) {
@@ -332,6 +353,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
}
}
logCtx.Debugf("Retrieved lived manifests")
for _, liveObj := range liveObjByKey {
if liveObj != nil {
appInstanceName := kubeutil.GetAppInstanceLabel(liveObj, appLabelKey)
@@ -345,30 +367,43 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
}
}
reconciliation := sync.Reconcile(targetObjs, liveObjByKey, app.Spec.Destination.Namespace, infoProvider)
ts.AddCheckpoint("live_ms")
compareOptions, err := m.settingsMgr.GetResourceCompareOptions()
if err != nil {
log.Warnf("Could not get compare options from ConfigMap (assuming defaults): %v", err)
compareOptions = diff.GetDefaultDiffOptions()
managedLiveObj := make([]*unstructured.Unstructured, len(targetObjs))
for i, obj := range targetObjs {
gvk := obj.GroupVersionKind()
ns := util.FirstNonEmpty(obj.GetNamespace(), app.Spec.Destination.Namespace)
if namespaced, err := m.liveStateCache.IsNamespaced(app.Spec.Destination.Server, obj.GroupVersionKind().GroupKind()); err == nil && !namespaced {
ns = ""
}
key := kubeutil.NewResourceKey(gvk.Group, gvk.Kind, ns, obj.GetName())
if liveObj, ok := liveObjByKey[key]; ok {
managedLiveObj[i] = liveObj
delete(liveObjByKey, key)
} else {
managedLiveObj[i] = nil
}
}
logCtx.Debugf("built managed objects list")
// Everything remaining in liveObjByKey are "extra" resources that aren't tracked in git.
// The following adds all the extras to the managedLiveObj list and backfills the targetObj
// list with nils, so that the lists are of equal lengths for comparison purposes.
for _, obj := range liveObjByKey {
targetObjs = append(targetObjs, nil)
managedLiveObj = append(managedLiveObj, obj)
}
logCtx.Debugf("built managed objects list")
// Do the actual comparison
diffResults, err := diff.DiffArray(reconciliation.Target, reconciliation.Live, diffNormalizer, compareOptions)
diffResults, err := diff.DiffArray(targetObjs, managedLiveObj, diffNormalizer)
if err != nil {
diffResults = &diff.DiffResultList{}
failedToLoadObjs = true
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
}
ts.AddCheckpoint("diff_ms")
syncCode := v1alpha1.SyncStatusCodeSynced
managedResources := make([]managedResource, len(reconciliation.Target))
resourceSummaries := make([]v1alpha1.ResourceStatus, len(reconciliation.Target))
for i, targetObj := range reconciliation.Target {
liveObj := reconciliation.Live[i]
managedResources := make([]managedResource, len(targetObjs))
resourceSummaries := make([]v1alpha1.ResourceStatus, len(targetObjs))
for i, targetObj := range targetObjs {
liveObj := managedLiveObj[i]
obj := liveObj
if obj == nil {
obj = targetObj
@@ -388,12 +423,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
RequiresPruning: targetObj == nil && liveObj != nil,
}
var diffResult diff.DiffResult
if i < len(diffResults.Diffs) {
diffResult = diffResults.Diffs[i]
} else {
diffResult = diff.DiffResult{Modified: false, NormalizedLive: []byte("{}"), PredictedLive: []byte("{}")}
}
diffResult := diffResults.Diffs[i]
if resState.Hook || ignore.Ignore(obj) {
// For resource hooks, don't store sync status, and do not affect overall sync status
} else if diffResult.Modified || targetObj == nil || liveObj == nil {
@@ -404,7 +434,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
resState.Status = v1alpha1.SyncStatusCodeOutOfSync
// we ignore the status if the obj needs pruning AND we have the annotation
needsPruning := targetObj == nil && liveObj != nil
if !(needsPruning && resourceutil.HasAnnotationOption(obj, common.AnnotationCompareOptions, "IgnoreExtraneous")) {
if !(needsPruning && resource.HasAnnotationOption(obj, common.AnnotationCompareOptions, "IgnoreExtraneous")) {
syncCode = v1alpha1.SyncStatusCodeOutOfSync
}
} else {
@@ -447,9 +477,8 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
if manifestInfo != nil {
syncStatus.Revision = manifestInfo.Revision
}
ts.AddCheckpoint("sync_ms")
healthStatus, err := argohealth.SetApplicationHealth(resourceSummaries, GetLiveObjs(managedResources), resourceOverrides, func(obj *unstructured.Unstructured) bool {
healthStatus, err := health.SetApplicationHealth(resourceSummaries, GetLiveObjs(managedResources), resourceOverrides, func(obj *unstructured.Unstructured) bool {
return !isSelfReferencedApp(app, kubeutil.GetObjectRef(obj))
})
@@ -458,12 +487,12 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
}
compRes := comparisonResult{
syncStatus: &syncStatus,
healthStatus: healthStatus,
resources: resourceSummaries,
managedResources: managedResources,
reconciliationResult: reconciliation,
diffNormalizer: diffNormalizer,
syncStatus: &syncStatus,
healthStatus: healthStatus,
resources: resourceSummaries,
managedResources: managedResources,
hooks: hooks,
diffNormalizer: diffNormalizer,
}
if manifestInfo != nil {
compRes.appSourceType = v1alpha1.ApplicationSourceType(manifestInfo.SourceType)
@@ -474,8 +503,6 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
appv1.ApplicationConditionRepeatedResourceWarning: true,
appv1.ApplicationConditionExcludedResourceWarning: true,
})
ts.AddCheckpoint("health_ms")
compRes.timings = ts.Timings()
return &compRes
}
@@ -505,7 +532,7 @@ func (m *appStateManager) persistRevisionHistory(app *v1alpha1.Application, revi
return err
}
// NewAppStateManager creates new instance of AppStateManager
// NewAppStateManager creates new instance of Ksonnet app comparator
func NewAppStateManager(
db db.ArgoDB,
appclientset appclientset.Interface,

View File

@@ -4,10 +4,6 @@ import (
"encoding/json"
"testing"
"github.com/argoproj/gitops-engine/pkg/health"
synccommon "github.com/argoproj/gitops-engine/pkg/sync/common"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
. "github.com/argoproj/gitops-engine/pkg/utils/testing"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -18,6 +14,7 @@ import (
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/reposerver/apiclient"
"github.com/argoproj/argo-cd/test"
"github.com/argoproj/argo-cd/util/kube"
)
// TestCompareAppStateEmpty tests comparison when both git and live have no objects
@@ -48,7 +45,7 @@ func TestCompareAppStateMissing(t *testing.T) {
data := fakeData{
apps: []runtime.Object{app},
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{PodManifest},
Manifests: []string{test.PodManifest},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
@@ -67,7 +64,7 @@ func TestCompareAppStateMissing(t *testing.T) {
// TestCompareAppStateExtra tests when there is an extra object in live but not defined in git
func TestCompareAppStateExtra(t *testing.T) {
pod := NewPod()
pod := test.NewPod()
pod.SetNamespace(test.FakeDestNamespace)
app := newFakeApp()
key := kube.ResourceKey{Group: "", Kind: "Pod", Namespace: test.FakeDestNamespace, Name: app.Name}
@@ -94,8 +91,8 @@ func TestCompareAppStateExtra(t *testing.T) {
// TestCompareAppStateHook checks that hooks are detected during manifest generation, and not
// considered as part of resources when assessing Synced status
func TestCompareAppStateHook(t *testing.T) {
pod := NewPod()
pod.SetAnnotations(map[string]string{synccommon.AnnotationKeyHook: "PreSync"})
pod := test.NewPod()
pod.SetAnnotations(map[string]string{common.AnnotationKeyHook: "PreSync"})
podBytes, _ := json.Marshal(pod)
app := newFakeApp()
data := fakeData{
@@ -114,13 +111,13 @@ func TestCompareAppStateHook(t *testing.T) {
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Equal(t, 0, len(compRes.resources))
assert.Equal(t, 0, len(compRes.managedResources))
assert.Equal(t, 1, len(compRes.reconciliationResult.Hooks))
assert.Equal(t, 1, len(compRes.hooks))
assert.Equal(t, 0, len(app.Status.Conditions))
}
// checks that ignore resources are detected, but excluded from status
func TestCompareAppStateCompareOptionIgnoreExtraneous(t *testing.T) {
pod := NewPod()
pod := test.NewPod()
pod.SetAnnotations(map[string]string{common.AnnotationCompareOptions: "IgnoreExtraneous"})
app := newFakeApp()
data := fakeData{
@@ -146,8 +143,8 @@ func TestCompareAppStateCompareOptionIgnoreExtraneous(t *testing.T) {
// TestCompareAppStateExtraHook tests when there is an extra _hook_ object in live but not defined in git
func TestCompareAppStateExtraHook(t *testing.T) {
pod := NewPod()
pod.SetAnnotations(map[string]string{synccommon.AnnotationKeyHook: "PreSync"})
pod := test.NewPod()
pod.SetAnnotations(map[string]string{common.AnnotationKeyHook: "PreSync"})
pod.SetNamespace(test.FakeDestNamespace)
app := newFakeApp()
key := kube.ResourceKey{Group: "", Kind: "Pod", Namespace: test.FakeDestNamespace, Name: app.Name}
@@ -169,7 +166,7 @@ func TestCompareAppStateExtraHook(t *testing.T) {
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Equal(t, 1, len(compRes.resources))
assert.Equal(t, 1, len(compRes.managedResources))
assert.Equal(t, 0, len(compRes.reconciliationResult.Hooks))
assert.Equal(t, 0, len(compRes.hooks))
assert.Equal(t, 0, len(app.Status.Conditions))
}
@@ -180,22 +177,16 @@ func toJSON(t *testing.T, obj *unstructured.Unstructured) string {
}
func TestCompareAppStateDuplicatedNamespacedResources(t *testing.T) {
obj1 := NewPod()
obj1 := test.NewPod()
obj1.SetNamespace(test.FakeDestNamespace)
obj2 := NewPod()
obj3 := NewPod()
obj2 := test.NewPod()
obj3 := test.NewPod()
obj3.SetNamespace("kube-system")
obj4 := NewPod()
obj4.SetGenerateName("my-pod")
obj4.SetName("")
obj5 := NewPod()
obj5.SetName("")
obj5.SetGenerateName("my-pod")
app := newFakeApp()
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{toJSON(t, obj1), toJSON(t, obj2), toJSON(t, obj3), toJSON(t, obj4), toJSON(t, obj5)},
Manifests: []string{toJSON(t, obj1), toJSON(t, obj2), toJSON(t, obj3)},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
@@ -213,7 +204,7 @@ func TestCompareAppStateDuplicatedNamespacedResources(t *testing.T) {
assert.NotNil(t, app.Status.Conditions[0].LastTransitionTime)
assert.Equal(t, argoappv1.ApplicationConditionRepeatedResourceWarning, app.Status.Conditions[0].Type)
assert.Equal(t, "Resource /Pod/fake-dest-ns/my-pod appeared 2 times among application resources.", app.Status.Conditions[0].Message)
assert.Equal(t, 4, len(compRes.resources))
assert.Equal(t, 2, len(compRes.resources))
}
var defaultProj = argoappv1.AppProject{
@@ -259,7 +250,7 @@ func TestSetHealth(t *testing.T) {
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
assert.Equal(t, compRes.healthStatus.Status, health.HealthStatusHealthy)
assert.Equal(t, compRes.healthStatus.Status, argoappv1.HealthStatusHealthy)
}
func TestSetHealthSelfReferencedApp(t *testing.T) {
@@ -291,7 +282,7 @@ func TestSetHealthSelfReferencedApp(t *testing.T) {
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
assert.Equal(t, compRes.healthStatus.Status, health.HealthStatusHealthy)
assert.Equal(t, compRes.healthStatus.Status, argoappv1.HealthStatusHealthy)
}
func TestSetManagedResourcesWithOrphanedResources(t *testing.T) {
@@ -361,7 +352,7 @@ func TestReturnUnknownComparisonStateOnSettingLoadError(t *testing.T) {
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
assert.Equal(t, health.HealthStatusUnknown, compRes.healthStatus.Status)
assert.Equal(t, argoappv1.HealthStatusUnknown, compRes.healthStatus.Status)
assert.Equal(t, argoappv1.SyncStatusCodeUnknown, compRes.syncStatus.Status)
}
@@ -394,6 +385,13 @@ func TestSetManagedResourcesKnownOrphanedResourceExceptions(t *testing.T) {
assert.Equal(t, "guestbook", tree.OrphanedNodes[0].Name)
}
func Test_comparisonResult_obs(t *testing.T) {
assert.Len(t, (&comparisonResult{}).targetObjs(), 0)
assert.Len(t, (&comparisonResult{managedResources: []managedResource{{}}}).targetObjs(), 0)
assert.Len(t, (&comparisonResult{managedResources: []managedResource{{Target: test.NewPod()}}}).targetObjs(), 1)
assert.Len(t, (&comparisonResult{hooks: []*unstructured.Unstructured{{}}}).targetObjs(), 1)
}
func Test_appStateManager_persistRevisionHistory(t *testing.T) {
app := newFakeApp()
ctrl := newFakeController(&fakeData{

View File

@@ -3,27 +3,65 @@ package controller
import (
"context"
"fmt"
"reflect"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/argoproj/gitops-engine/pkg/sync"
"github.com/argoproj/gitops-engine/pkg/sync/common"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
log "github.com/sirupsen/logrus"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apimachinery/pkg/api/errors"
apierr "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/controller/metrics"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
listersv1alpha1 "github.com/argoproj/argo-cd/pkg/client/listers/application/v1alpha1"
"github.com/argoproj/argo-cd/util/argo"
"github.com/argoproj/argo-cd/util/lua"
"github.com/argoproj/argo-cd/util/health"
"github.com/argoproj/argo-cd/util/hook"
"github.com/argoproj/argo-cd/util/kube"
"github.com/argoproj/argo-cd/util/rand"
"github.com/argoproj/argo-cd/util/resource"
)
const (
crdReadinessTimeout = time.Duration(3) * time.Second
)
var syncIdPrefix uint64 = 0
type syncContext struct {
resourceOverrides map[string]v1alpha1.ResourceOverride
appName string
proj *v1alpha1.AppProject
compareResult *comparisonResult
config *rest.Config
dynamicIf dynamic.Interface
disco discovery.DiscoveryInterface
extensionsclientset *clientset.Clientset
kubectl kube.Kubectl
namespace string
server string
syncOp *v1alpha1.SyncOperation
syncRes *v1alpha1.SyncOperationResult
syncResources []v1alpha1.SyncOperationResource
opState *v1alpha1.OperationState
log *log.Entry
// lock to protect concurrent updates of the result list
lock sync.Mutex
}
func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha1.OperationState) {
// Sync requests might be requested with ambiguous revisions (e.g. master, HEAD, v1.2.3).
// This can change meaning when resuming operations (e.g a hook sync). After calculating a
@@ -33,10 +71,11 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
var revision string
var syncOp v1alpha1.SyncOperation
var syncRes *v1alpha1.SyncOperationResult
var syncResources []v1alpha1.SyncOperationResource
var source v1alpha1.ApplicationSource
if state.Operation.Sync == nil {
state.Phase = common.OperationFailed
state.Phase = v1alpha1.OperationFailed
state.Message = "Invalid operation request: no operation specified"
return
}
@@ -48,7 +87,7 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
// rollback case
source = *state.Operation.Sync.Source
}
syncResources = syncOp.Resources
if state.SyncResult != nil {
syncRes = state.SyncResult
revision = state.SyncResult.Revision
@@ -70,7 +109,7 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
proj, err := argo.GetAppProject(&app.Spec, listersv1alpha1.NewAppProjectLister(m.projInformer.GetIndexer()), m.namespace)
if err != nil {
state.Phase = common.OperationError
state.Phase = v1alpha1.OperationError
state.Message = fmt.Sprintf("Failed to load application project: %v", err)
return
}
@@ -82,7 +121,7 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
v1alpha1.ApplicationConditionComparisonError: true,
v1alpha1.ApplicationConditionInvalidSpecError: true,
}); len(errConditions) > 0 {
state.Phase = common.OperationError
state.Phase = v1alpha1.OperationError
state.Message = argo.FormatAppConditions(errConditions)
return
}
@@ -93,95 +132,694 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
clst, err := m.db.GetCluster(context.Background(), app.Spec.Destination.Server)
if err != nil {
state.Phase = common.OperationError
state.Phase = v1alpha1.OperationError
state.Message = err.Error()
return
}
rawConfig := clst.RawRestConfig()
restConfig := metrics.AddMetricsTransportWrapper(m.metricsServer, app, clst.RESTConfig())
dynamicIf, err := dynamic.NewForConfig(restConfig)
if err != nil {
state.Phase = v1alpha1.OperationError
state.Message = fmt.Sprintf("Failed to initialize dynamic client: %v", err)
return
}
disco, err := discovery.NewDiscoveryClientForConfig(restConfig)
if err != nil {
state.Phase = v1alpha1.OperationError
state.Message = fmt.Sprintf("Failed to initialize discovery client: %v", err)
return
}
extensionsclientset, err := clientset.NewForConfig(restConfig)
if err != nil {
state.Phase = v1alpha1.OperationError
state.Message = fmt.Sprintf("Failed to initialize extensions client: %v", err)
return
}
resourceOverrides, err := m.settingsMgr.GetResourceOverrides()
if err != nil {
state.Phase = common.OperationError
state.Phase = v1alpha1.OperationError
state.Message = fmt.Sprintf("Failed to load resource overrides: %v", err)
return
}
atomic.AddUint64(&syncIdPrefix, 1)
syncId := fmt.Sprintf("%05d-%s", syncIdPrefix, rand.RandString(5))
logEntry := log.WithFields(log.Fields{"application": app.Name, "syncId": syncId})
initialResourcesRes := make([]common.ResourceSyncResult, 0)
for i, res := range syncRes.Resources {
key := kube.ResourceKey{Group: res.Group, Kind: res.Kind, Namespace: res.Namespace, Name: res.Name}
initialResourcesRes = append(initialResourcesRes, common.ResourceSyncResult{
ResourceKey: key,
Message: res.Message,
Status: res.Status,
HookPhase: res.HookPhase,
HookType: res.HookType,
SyncPhase: res.SyncPhase,
Version: res.Version,
Order: i + 1,
})
}
syncCtx, err := sync.NewSyncContext(compareResult.syncStatus.Revision, compareResult.reconciliationResult, restConfig, rawConfig, m.kubectl, app.Spec.Destination.Namespace, logEntry,
sync.WithHealthOverride(lua.ResourceHealthOverrides(resourceOverrides)),
sync.WithPermissionValidator(func(un *unstructured.Unstructured, res *v1.APIResource) error {
if !proj.IsGroupKindPermitted(un.GroupVersionKind().GroupKind(), res.Namespaced) {
return fmt.Errorf("Resource %s:%s is not permitted in project %s.", un.GroupVersionKind().Group, un.GroupVersionKind().Kind, proj.Name)
}
if res.Namespaced && !proj.IsDestinationPermitted(v1alpha1.ApplicationDestination{Namespace: un.GetNamespace(), Server: app.Spec.Destination.Server}) {
return fmt.Errorf("namespace %v is not permitted in project '%s'", un.GetNamespace(), proj.Name)
}
return nil
}),
sync.WithOperationSettings(syncOp.DryRun, syncOp.Prune, syncOp.SyncStrategy.Force(), syncOp.IsApplyStrategy() || len(syncOp.Resources) > 0),
sync.WithInitialState(state.Phase, state.Message, initialResourcesRes),
sync.WithResourcesFilter(func(key kube.ResourceKey, target *unstructured.Unstructured, live *unstructured.Unstructured) bool {
return len(syncOp.Resources) == 0 || argo.ContainsSyncResource(key.Name, schema.GroupVersionKind{Kind: key.Kind, Group: key.Group}, syncOp.Resources)
}),
sync.WithManifestValidation(!syncOp.SyncOptions.HasOption("Validate=false")),
)
if err != nil {
state.Phase = common.OperationError
state.Message = fmt.Sprintf("failed to record sync to history: %v", err)
syncCtx := syncContext{
resourceOverrides: resourceOverrides,
appName: app.Name,
proj: proj,
compareResult: compareResult,
config: restConfig,
dynamicIf: dynamicIf,
disco: disco,
extensionsclientset: extensionsclientset,
kubectl: m.kubectl,
namespace: app.Spec.Destination.Namespace,
server: app.Spec.Destination.Server,
syncOp: &syncOp,
syncRes: syncRes,
syncResources: syncResources,
opState: state,
log: log.WithFields(log.Fields{"application": app.Name, "syncId": syncId}),
}
start := time.Now()
if state.Phase == common.OperationTerminating {
syncCtx.Terminate()
if state.Phase == v1alpha1.OperationTerminating {
syncCtx.terminate()
} else {
syncCtx.Sync()
}
var resState []common.ResourceSyncResult
state.Phase, state.Message, resState = syncCtx.GetState()
state.SyncResult.Resources = nil
for _, res := range resState {
state.SyncResult.Resources = append(state.SyncResult.Resources, &v1alpha1.ResourceResult{
HookType: res.HookType,
Group: res.ResourceKey.Group,
Kind: res.ResourceKey.Kind,
Namespace: res.ResourceKey.Namespace,
Name: res.ResourceKey.Name,
Version: res.Version,
SyncPhase: res.SyncPhase,
HookPhase: res.HookPhase,
Status: res.Status,
Message: res.Message,
})
syncCtx.sync()
}
logEntry.WithField("duration", time.Since(start)).Info("sync/terminate complete")
syncCtx.log.WithField("duration", time.Since(start)).Info("sync/terminate complete")
if !syncOp.DryRun && len(syncOp.Resources) == 0 && state.Phase.Successful() {
if !syncOp.DryRun && !syncCtx.isSelectiveSync() && syncCtx.opState.Phase.Successful() {
err := m.persistRevisionHistory(app, compareResult.syncStatus.Revision, source)
if err != nil {
state.Phase = common.OperationError
state.Message = fmt.Sprintf("failed to record sync to history: %v", err)
syncCtx.setOperationPhase(v1alpha1.OperationError, fmt.Sprintf("failed to record sync to history: %v", err))
}
}
}
// sync has performs the actual apply or hook based sync
func (sc *syncContext) sync() {
sc.log.WithFields(log.Fields{"isSelectiveSync": sc.isSelectiveSync(), "skipHooks": sc.skipHooks(), "started": sc.started()}).Info("syncing")
tasks, ok := sc.getSyncTasks()
if !ok {
sc.setOperationPhase(v1alpha1.OperationFailed, "one or more synchronization tasks are not valid")
return
}
sc.log.WithFields(log.Fields{"tasks": tasks, "isSelectiveSync": sc.isSelectiveSync()}).Info("tasks")
// Perform a `kubectl apply --dry-run` against all the manifests. This will detect most (but
// not all) validation issues with the user's manifests (e.g. will detect syntax issues, but
// will not not detect if they are mutating immutable fields). If anything fails, we will refuse
// to perform the sync. we only wish to do this once per operation, performing additional dry-runs
// is harmless, but redundant. The indicator we use to detect if we have already performed
// the dry-run for this operation, is if the resource or hook list is empty.
if !sc.started() {
sc.log.Debug("dry-run")
if sc.runTasks(tasks, true) == failed {
sc.setOperationPhase(v1alpha1.OperationFailed, "one or more objects failed to apply (dry run)")
return
}
}
// update status of any tasks that are running, note that this must exclude pruning tasks
for _, task := range tasks.Filter(func(t *syncTask) bool {
// just occasionally, you can be running yet not have a live resource
return t.running() && t.liveObj != nil
}) {
if task.isHook() {
// update the hook's result
operationState, message, err := sc.getOperationPhase(task.liveObj)
if err != nil {
sc.setResourceResult(task, "", v1alpha1.OperationError, fmt.Sprintf("failed to get resource health: %v", err))
} else {
sc.setResourceResult(task, "", operationState, message)
// maybe delete the hook
if task.needsDeleting() {
err := sc.deleteResource(task)
if err != nil && !errors.IsNotFound(err) {
sc.setResourceResult(task, "", v1alpha1.OperationError, fmt.Sprintf("failed to delete resource: %v", err))
}
}
}
} else {
// this must be calculated on the live object
healthStatus, err := health.GetResourceHealth(task.liveObj, sc.resourceOverrides)
if err == nil {
log.WithFields(log.Fields{"task": task, "healthStatus": healthStatus}).Debug("attempting to update health of running task")
if healthStatus == nil {
// some objects (e.g. secret) do not have health, and they automatically success
sc.setResourceResult(task, task.syncStatus, v1alpha1.OperationSucceeded, task.message)
} else {
switch healthStatus.Status {
case v1alpha1.HealthStatusHealthy:
sc.setResourceResult(task, task.syncStatus, v1alpha1.OperationSucceeded, healthStatus.Message)
case v1alpha1.HealthStatusDegraded:
sc.setResourceResult(task, task.syncStatus, v1alpha1.OperationFailed, healthStatus.Message)
}
}
}
}
}
// if (a) we are multi-step and we have any running tasks,
// or (b) there are any running hooks,
// then wait...
multiStep := tasks.multiStep()
if tasks.Any(func(t *syncTask) bool { return (multiStep || t.isHook()) && t.running() }) {
sc.setOperationPhase(v1alpha1.OperationRunning, "one or more tasks are running")
return
}
// syncFailTasks only run during failure, so separate them from regular tasks
syncFailTasks, tasks := tasks.Split(func(t *syncTask) bool { return t.phase == v1alpha1.SyncPhaseSyncFail })
// if there are any completed but unsuccessful tasks, sync is a failure.
if tasks.Any(func(t *syncTask) bool { return t.completed() && !t.successful() }) {
sc.setOperationFailed(syncFailTasks, "one or more synchronization tasks completed unsuccessfully")
return
}
sc.log.WithFields(log.Fields{"tasks": tasks}).Debug("filtering out non-pending tasks")
// remove tasks that are completed, we can assume that there are no running tasks
tasks = tasks.Filter(func(t *syncTask) bool { return t.pending() })
// If no sync tasks were generated (e.g., in case all application manifests have been removed),
// the sync operation is successful.
if len(tasks) == 0 {
sc.setOperationPhase(v1alpha1.OperationSucceeded, "successfully synced (no more tasks)")
return
}
// remove any tasks not in this wave
phase := tasks.phase()
wave := tasks.wave()
// if it is the last phase/wave and the only remaining tasks are non-hooks, the we are successful
// EVEN if those objects subsequently degraded
// This handles the common case where neither hooks or waves are used and a sync equates to simply an (asynchronous) kubectl apply of manifests, which succeeds immediately.
complete := !tasks.Any(func(t *syncTask) bool { return t.phase != phase || wave != t.wave() || t.isHook() })
sc.log.WithFields(log.Fields{"phase": phase, "wave": wave, "tasks": tasks, "syncFailTasks": syncFailTasks}).Debug("filtering tasks in correct phase and wave")
tasks = tasks.Filter(func(t *syncTask) bool { return t.phase == phase && t.wave() == wave })
sc.setOperationPhase(v1alpha1.OperationRunning, "one or more tasks are running")
sc.log.WithFields(log.Fields{"tasks": tasks}).Debug("wet-run")
runState := sc.runTasks(tasks, false)
switch runState {
case failed:
sc.setOperationFailed(syncFailTasks, "one or more objects failed to apply")
case successful:
if complete {
sc.setOperationPhase(v1alpha1.OperationSucceeded, "successfully synced (all tasks run)")
}
}
}
func (sc *syncContext) setOperationFailed(syncFailTasks syncTasks, message string) {
if len(syncFailTasks) > 0 {
// if all the failure hooks are completed, don't run them again, and mark the sync as failed
if syncFailTasks.All(func(task *syncTask) bool { return task.completed() }) {
sc.setOperationPhase(v1alpha1.OperationFailed, message)
return
}
// otherwise, we need to start the failure hooks, and then return without setting
// the phase, so we make sure we have at least one more sync
sc.log.WithFields(log.Fields{"syncFailTasks": syncFailTasks}).Debug("running sync fail tasks")
if sc.runTasks(syncFailTasks, false) == failed {
sc.setOperationPhase(v1alpha1.OperationFailed, message)
}
} else {
sc.setOperationPhase(v1alpha1.OperationFailed, message)
}
}
func (sc *syncContext) started() bool {
return len(sc.syncRes.Resources) > 0
}
func (sc *syncContext) isSelectiveSync() bool {
// we've selected no resources
if sc.syncResources == nil {
return false
}
// map both lists into string
var a []string
for _, r := range sc.compareResult.resources {
if !r.Hook {
a = append(a, fmt.Sprintf("%s:%s:%s", r.Group, r.Kind, r.Name))
}
}
sort.Strings(a)
var b []string
for _, r := range sc.syncResources {
b = append(b, fmt.Sprintf("%s:%s:%s", r.Group, r.Kind, r.Name))
}
sort.Strings(b)
return !reflect.DeepEqual(a, b)
}
// this essentially enforces the old "apply" behaviour
func (sc *syncContext) skipHooks() bool {
// All objects passed a `kubectl apply --dry-run`, so we are now ready to actually perform the sync.
// default sync strategy to hook if no strategy
return sc.syncOp.IsApplyStrategy() || sc.isSelectiveSync()
}
func (sc *syncContext) containsResource(resourceState managedResource) bool {
return !sc.isSelectiveSync() ||
(resourceState.Live != nil && argo.ContainsSyncResource(resourceState.Live.GetName(), resourceState.Live.GroupVersionKind(), sc.syncResources)) ||
(resourceState.Target != nil && argo.ContainsSyncResource(resourceState.Target.GetName(), resourceState.Target.GroupVersionKind(), sc.syncResources))
}
// generates the list of sync tasks we will be performing during this sync.
func (sc *syncContext) getSyncTasks() (_ syncTasks, successful bool) {
resourceTasks := syncTasks{}
successful = true
for _, resource := range sc.compareResult.managedResources {
if !sc.containsResource(resource) {
sc.log.WithFields(log.Fields{"group": resource.Group, "kind": resource.Kind, "name": resource.Name}).
Debug("skipping")
continue
}
obj := obj(resource.Target, resource.Live)
// this creates garbage tasks
if hook.IsHook(obj) {
sc.log.WithFields(log.Fields{"group": obj.GroupVersionKind().Group, "kind": obj.GetKind(), "namespace": obj.GetNamespace(), "name": obj.GetName()}).
Debug("skipping hook")
continue
}
for _, phase := range syncPhases(obj) {
resourceTasks = append(resourceTasks, &syncTask{phase: phase, targetObj: resource.Target, liveObj: resource.Live})
}
}
sc.log.WithFields(log.Fields{"resourceTasks": resourceTasks}).Debug("tasks from managed resources")
hookTasks := syncTasks{}
if !sc.skipHooks() {
for _, obj := range sc.compareResult.hooks {
for _, phase := range syncPhases(obj) {
// Hook resources names are deterministic, whether they are defined by the user (metadata.name),
// or formulated at the time of the operation (metadata.generateName). If user specifies
// metadata.generateName, then we will generate a formulated metadata.name before submission.
targetObj := obj.DeepCopy()
if targetObj.GetName() == "" {
postfix := strings.ToLower(fmt.Sprintf("%s-%s-%d", sc.syncRes.Revision[0:7], phase, sc.opState.StartedAt.UTC().Unix()))
generateName := obj.GetGenerateName()
targetObj.SetName(fmt.Sprintf("%s%s", generateName, postfix))
}
hookTasks = append(hookTasks, &syncTask{phase: phase, targetObj: targetObj})
}
}
}
sc.log.WithFields(log.Fields{"hookTasks": hookTasks}).Debug("tasks from hooks")
tasks := resourceTasks
tasks = append(tasks, hookTasks...)
// enrich target objects with the namespace
for _, task := range tasks {
if task.targetObj == nil {
continue
}
if task.targetObj.GetNamespace() == "" {
// If target object's namespace is empty, we set namespace in the object. We do
// this even though it might be a cluster-scoped resource. This prevents any
// possibility of the resource from unintentionally becoming created in the
// namespace during the `kubectl apply`
task.targetObj = task.targetObj.DeepCopy()
task.targetObj.SetNamespace(sc.namespace)
}
}
// enrich task with live obj
for _, task := range tasks {
if task.targetObj == nil || task.liveObj != nil {
continue
}
task.liveObj = sc.liveObj(task.targetObj)
}
// enrich tasks with the result
for _, task := range tasks {
_, result := sc.syncRes.Resources.Find(task.group(), task.kind(), task.namespace(), task.name(), task.phase)
if result != nil {
task.syncStatus = result.Status
task.operationState = result.HookPhase
task.message = result.Message
}
}
// check permissions
for _, task := range tasks {
serverRes, err := kube.ServerResourceForGroupVersionKind(sc.disco, task.groupVersionKind())
if err != nil {
// Special case for custom resources: if CRD is not yet known by the K8s API server,
// skip verification during `kubectl apply --dry-run` since we expect the CRD
// to be created during app synchronization.
if apierr.IsNotFound(err) && sc.hasCRDOfGroupKind(task.group(), task.kind()) {
sc.log.WithFields(log.Fields{"task": task}).Debug("skip dry-run for custom resource")
task.skipDryRun = true
} else {
sc.setResourceResult(task, v1alpha1.ResultCodeSyncFailed, "", err.Error())
successful = false
}
} else {
if !sc.proj.IsGroupKindPermitted(schema.GroupKind{Group: task.group(), Kind: task.kind()}, serverRes.Namespaced) {
sc.setResourceResult(task, v1alpha1.ResultCodeSyncFailed, "", fmt.Sprintf("Resource %s:%s is not permitted in project %s.", task.group(), task.kind(), sc.proj.Name))
successful = false
}
if serverRes.Namespaced && !sc.proj.IsDestinationPermitted(v1alpha1.ApplicationDestination{Namespace: task.namespace(), Server: sc.server}) {
sc.setResourceResult(task, v1alpha1.ResultCodeSyncFailed, "", fmt.Sprintf("namespace %v is not permitted in project '%s'", task.namespace(), sc.proj.Name))
successful = false
}
}
}
sort.Sort(tasks)
return tasks, successful
}
func obj(a, b *unstructured.Unstructured) *unstructured.Unstructured {
if a != nil {
return a
} else {
return b
}
}
func (sc *syncContext) liveObj(obj *unstructured.Unstructured) *unstructured.Unstructured {
for _, resource := range sc.compareResult.managedResources {
if resource.Group == obj.GroupVersionKind().Group &&
resource.Kind == obj.GetKind() &&
// cluster scoped objects will not have a namespace, even if the user has defined it
(resource.Namespace == "" || resource.Namespace == obj.GetNamespace()) &&
resource.Name == obj.GetName() {
return resource.Live
}
}
return nil
}
func (sc *syncContext) setOperationPhase(phase v1alpha1.OperationPhase, message string) {
if sc.opState.Phase != phase || sc.opState.Message != message {
sc.log.Infof("Updating operation state. phase: %s -> %s, message: '%s' -> '%s'", sc.opState.Phase, phase, sc.opState.Message, message)
}
sc.opState.Phase = phase
sc.opState.Message = message
}
// ensureCRDReady waits until specified CRD is ready (established condition is true). Method is best effort - it does not fail even if CRD is not ready without timeout.
func (sc *syncContext) ensureCRDReady(name string) {
_ = wait.PollImmediate(time.Duration(100)*time.Millisecond, crdReadinessTimeout, func() (bool, error) {
crd, err := sc.extensionsclientset.ApiextensionsV1beta1().CustomResourceDefinitions().Get(name, metav1.GetOptions{})
if err != nil {
return false, err
}
for _, condition := range crd.Status.Conditions {
if condition.Type == v1beta1.Established {
return condition.Status == v1beta1.ConditionTrue, nil
}
}
return false, nil
})
}
// applyObject performs a `kubectl apply` of a single resource
func (sc *syncContext) applyObject(targetObj *unstructured.Unstructured, dryRun bool, force bool) (v1alpha1.ResultCode, string) {
validate := !resource.HasAnnotationOption(targetObj, common.AnnotationSyncOptions, "Validate=false")
message, err := sc.kubectl.ApplyResource(sc.config, targetObj, targetObj.GetNamespace(), dryRun, force, validate)
if err != nil {
return v1alpha1.ResultCodeSyncFailed, err.Error()
}
if kube.IsCRD(targetObj) && !dryRun {
sc.ensureCRDReady(targetObj.GetName())
}
return v1alpha1.ResultCodeSynced, message
}
// pruneObject deletes the object if both prune is true and dryRun is false. Otherwise appropriate message
func (sc *syncContext) pruneObject(liveObj *unstructured.Unstructured, prune, dryRun bool) (v1alpha1.ResultCode, string) {
if !prune {
return v1alpha1.ResultCodePruneSkipped, "ignored (requires pruning)"
} else if resource.HasAnnotationOption(liveObj, common.AnnotationSyncOptions, "Prune=false") {
return v1alpha1.ResultCodePruneSkipped, "ignored (no prune)"
} else {
if dryRun {
return v1alpha1.ResultCodePruned, "pruned (dry run)"
} else {
// Skip deletion if object is already marked for deletion, so we don't cause a resource update hotloop
deletionTimestamp := liveObj.GetDeletionTimestamp()
if deletionTimestamp == nil || deletionTimestamp.IsZero() {
err := sc.kubectl.DeleteResource(sc.config, liveObj.GroupVersionKind(), liveObj.GetName(), liveObj.GetNamespace(), false)
if err != nil {
return v1alpha1.ResultCodeSyncFailed, err.Error()
}
}
return v1alpha1.ResultCodePruned, "pruned"
}
}
}
func (sc *syncContext) hasCRDOfGroupKind(group string, kind string) bool {
for _, obj := range sc.compareResult.targetObjs() {
if kube.IsCRD(obj) {
crdGroup, ok, err := unstructured.NestedString(obj.Object, "spec", "group")
if err != nil || !ok {
continue
}
crdKind, ok, err := unstructured.NestedString(obj.Object, "spec", "names", "kind")
if err != nil || !ok {
continue
}
if group == crdGroup && crdKind == kind {
return true
}
}
}
return false
}
// terminate looks for any running jobs/workflow hooks and deletes the resource
func (sc *syncContext) terminate() {
terminateSuccessful := true
sc.log.Debug("terminating")
tasks, _ := sc.getSyncTasks()
for _, task := range tasks {
if !task.isHook() || task.liveObj == nil {
continue
}
phase, msg, err := sc.getOperationPhase(task.liveObj)
if err != nil {
sc.setOperationPhase(v1alpha1.OperationError, fmt.Sprintf("Failed to get hook health: %v", err))
return
}
if phase == v1alpha1.OperationRunning {
err := sc.deleteResource(task)
if err != nil {
sc.setResourceResult(task, "", v1alpha1.OperationFailed, fmt.Sprintf("Failed to delete: %v", err))
terminateSuccessful = false
} else {
sc.setResourceResult(task, "", v1alpha1.OperationSucceeded, fmt.Sprintf("Deleted"))
}
} else {
sc.setResourceResult(task, "", phase, msg)
}
}
if terminateSuccessful {
sc.setOperationPhase(v1alpha1.OperationFailed, "Operation terminated")
} else {
sc.setOperationPhase(v1alpha1.OperationError, "Operation termination had errors")
}
}
func (sc *syncContext) deleteResource(task *syncTask) error {
sc.log.WithFields(log.Fields{"task": task}).Debug("deleting resource")
resIf, err := sc.getResourceIf(task)
if err != nil {
return err
}
propagationPolicy := metav1.DeletePropagationForeground
return resIf.Delete(task.name(), &metav1.DeleteOptions{PropagationPolicy: &propagationPolicy})
}
func (sc *syncContext) getResourceIf(task *syncTask) (dynamic.ResourceInterface, error) {
apiResource, err := kube.ServerResourceForGroupVersionKind(sc.disco, task.groupVersionKind())
if err != nil {
return nil, err
}
res := kube.ToGroupVersionResource(task.groupVersionKind().GroupVersion().String(), apiResource)
resIf := kube.ToResourceInterface(sc.dynamicIf, apiResource, res, task.namespace())
return resIf, err
}
var operationPhases = map[v1alpha1.ResultCode]v1alpha1.OperationPhase{
v1alpha1.ResultCodeSynced: v1alpha1.OperationRunning,
v1alpha1.ResultCodeSyncFailed: v1alpha1.OperationFailed,
v1alpha1.ResultCodePruned: v1alpha1.OperationSucceeded,
v1alpha1.ResultCodePruneSkipped: v1alpha1.OperationSucceeded,
}
// tri-state
type runState = int
const (
successful = iota
pending
failed
)
func (sc *syncContext) runTasks(tasks syncTasks, dryRun bool) runState {
dryRun = dryRun || sc.syncOp.DryRun
sc.log.WithFields(log.Fields{"numTasks": len(tasks), "dryRun": dryRun}).Debug("running tasks")
runState := successful
var createTasks syncTasks
var pruneTasks syncTasks
for _, task := range tasks {
if task.isPrune() {
pruneTasks = append(pruneTasks, task)
} else {
createTasks = append(createTasks, task)
}
}
// prune first
{
var wg sync.WaitGroup
for _, task := range pruneTasks {
wg.Add(1)
go func(t *syncTask) {
defer wg.Done()
logCtx := sc.log.WithFields(log.Fields{"dryRun": dryRun, "task": t})
logCtx.Debug("pruning")
result, message := sc.pruneObject(t.liveObj, sc.syncOp.Prune, dryRun)
if result == v1alpha1.ResultCodeSyncFailed {
runState = failed
logCtx.WithField("message", message).Info("pruning failed")
}
if !dryRun || sc.syncOp.DryRun || result == v1alpha1.ResultCodeSyncFailed {
sc.setResourceResult(t, result, operationPhases[result], message)
}
}(task)
}
wg.Wait()
}
// delete anything that need deleting
if runState == successful && createTasks.Any(func(t *syncTask) bool { return t.needsDeleting() }) {
var wg sync.WaitGroup
for _, task := range createTasks.Filter(func(t *syncTask) bool { return t.needsDeleting() }) {
wg.Add(1)
go func(t *syncTask) {
defer wg.Done()
sc.log.WithFields(log.Fields{"dryRun": dryRun, "task": t}).Debug("deleting")
if !dryRun {
err := sc.deleteResource(t)
if err != nil {
// it is possible to get a race condition here, such that the resource does not exist when
// delete is requested, we treat this as a nop
if !apierr.IsNotFound(err) {
runState = failed
sc.setResourceResult(t, "", v1alpha1.OperationError, fmt.Sprintf("failed to delete resource: %v", err))
}
} else {
// if there is anything that needs deleting, we are at best now in pending and
// want to return and wait for sync to be invoked again
runState = pending
}
}
}(task)
}
wg.Wait()
}
// finally create resources
if runState == successful {
processCreateTasks := func(tasks syncTasks) {
var createWg sync.WaitGroup
for _, task := range tasks {
if dryRun && task.skipDryRun {
continue
}
createWg.Add(1)
go func(t *syncTask) {
defer createWg.Done()
logCtx := sc.log.WithFields(log.Fields{"dryRun": dryRun, "task": t})
logCtx.Debug("applying")
result, message := sc.applyObject(t.targetObj, dryRun, sc.syncOp.SyncStrategy.Force())
if result == v1alpha1.ResultCodeSyncFailed {
logCtx.WithField("message", message).Info("apply failed")
runState = failed
}
if !dryRun || sc.syncOp.DryRun || result == v1alpha1.ResultCodeSyncFailed {
sc.setResourceResult(t, result, operationPhases[result], message)
}
}(task)
}
createWg.Wait()
}
var tasksGroup syncTasks
for _, task := range createTasks {
//Only wait if the type of the next task is different than the previous type
if len(tasksGroup) > 0 && tasksGroup[0].targetObj.GetKind() != task.kind() {
processCreateTasks(tasksGroup)
tasksGroup = syncTasks{task}
} else {
tasksGroup = append(tasksGroup, task)
}
}
if len(tasksGroup) > 0 {
processCreateTasks(tasksGroup)
}
}
return runState
}
// setResourceResult sets a resource details in the SyncResult.Resources list
func (sc *syncContext) setResourceResult(task *syncTask, syncStatus v1alpha1.ResultCode, operationState v1alpha1.OperationPhase, message string) {
task.syncStatus = syncStatus
task.operationState = operationState
// we always want to keep the latest message
if message != "" {
task.message = message
}
sc.lock.Lock()
defer sc.lock.Unlock()
i, existing := sc.syncRes.Resources.Find(task.group(), task.kind(), task.namespace(), task.name(), task.phase)
res := v1alpha1.ResourceResult{
Group: task.group(),
Version: task.version(),
Kind: task.kind(),
Namespace: task.namespace(),
Name: task.name(),
Status: task.syncStatus,
Message: task.message,
HookType: task.hookType(),
HookPhase: task.operationState,
SyncPhase: task.phase,
}
logCtx := sc.log.WithFields(log.Fields{"namespace": task.namespace(), "kind": task.kind(), "name": task.name(), "phase": task.phase})
if existing != nil {
// update existing value
if res.Status != existing.Status || res.HookPhase != existing.HookPhase || res.Message != existing.Message {
logCtx.Infof("updating resource result, status: '%s' -> '%s', phase '%s' -> '%s', message '%s' -> '%s'",
existing.Status, res.Status,
existing.HookPhase, res.HookPhase,
existing.Message, res.Message)
}
sc.syncRes.Resources[i] = &res
} else {
logCtx.Infof("adding resource result, status: '%s', phase: '%s', message: '%s'", res.Status, res.HookPhase, res.Message)
sc.syncRes.Resources = append(sc.syncRes.Resources, &res)
}
}

35
controller/sync_hooks.go Normal file
View File

@@ -0,0 +1,35 @@
package controller
import (
"fmt"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/util/health"
)
// getOperationPhase returns a hook status from an _live_ unstructured object
func (sc *syncContext) getOperationPhase(hook *unstructured.Unstructured) (v1alpha1.OperationPhase, string, error) {
phase := v1alpha1.OperationSucceeded
message := fmt.Sprintf("%s created", hook.GetName())
resHealth, err := health.GetResourceHealth(hook, sc.resourceOverrides)
if err != nil {
return "", "", err
}
if resHealth != nil {
switch resHealth.Status {
case v1alpha1.HealthStatusUnknown, v1alpha1.HealthStatusDegraded:
phase = v1alpha1.OperationFailed
message = resHealth.Message
case v1alpha1.HealthStatusProgressing, v1alpha1.HealthStatusSuspended:
phase = v1alpha1.OperationRunning
message = resHealth.Message
case v1alpha1.HealthStatusHealthy:
phase = v1alpha1.OperationSucceeded
message = resHealth.Message
}
}
return phase, message, nil
}

29
controller/sync_phase.go Normal file
View File

@@ -0,0 +1,29 @@
package controller
import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/util/hook"
)
func syncPhases(obj *unstructured.Unstructured) []v1alpha1.SyncPhase {
if hook.Skip(obj) {
return nil
} else if hook.IsHook(obj) {
phasesMap := make(map[v1alpha1.SyncPhase]bool)
for _, hookType := range hook.Types(obj) {
switch hookType {
case v1alpha1.HookTypePreSync, v1alpha1.HookTypeSync, v1alpha1.HookTypePostSync, v1alpha1.HookTypeSyncFail:
phasesMap[v1alpha1.SyncPhase(hookType)] = true
}
}
var phases []v1alpha1.SyncPhase
for phase := range phasesMap {
phases = append(phases, phase)
}
return phases
} else {
return []v1alpha1.SyncPhase{v1alpha1.SyncPhaseSync}
}
}

View File

@@ -0,0 +1,57 @@
package controller
import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
. "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/test"
)
func TestSyncPhaseNone(t *testing.T) {
assert.Equal(t, []SyncPhase{SyncPhaseSync}, syncPhases(&unstructured.Unstructured{}))
}
func TestSyncPhasePreSync(t *testing.T) {
assert.Equal(t, []SyncPhase{SyncPhasePreSync}, syncPhases(pod("PreSync")))
}
func TestSyncPhaseSync(t *testing.T) {
assert.Equal(t, []SyncPhase{SyncPhaseSync}, syncPhases(pod("Sync")))
}
func TestSyncPhaseSkip(t *testing.T) {
assert.Nil(t, syncPhases(pod("Skip")))
}
// garbage hooks are still hooks, but have no phases, because some user spelled something wrong
func TestSyncPhaseGarbage(t *testing.T) {
assert.Nil(t, syncPhases(pod("Garbage")))
}
func TestSyncPhasePost(t *testing.T) {
assert.Equal(t, []SyncPhase{SyncPhasePostSync}, syncPhases(pod("PostSync")))
}
func TestSyncPhaseFail(t *testing.T) {
assert.Equal(t, []SyncPhase{SyncPhaseSyncFail}, syncPhases(pod("SyncFail")))
}
func TestSyncPhaseTwoPhases(t *testing.T) {
assert.ElementsMatch(t, []SyncPhase{SyncPhasePreSync, SyncPhasePostSync}, syncPhases(pod("PreSync,PostSync")))
}
func TestSyncDuplicatedPhases(t *testing.T) {
assert.ElementsMatch(t, []SyncPhase{SyncPhasePreSync}, syncPhases(pod("PreSync,PreSync")))
assert.ElementsMatch(t, []SyncPhase{SyncPhasePreSync}, syncPhases(podWithHelmHook("pre-install,pre-upgrade")))
}
func pod(hookType string) *unstructured.Unstructured {
return test.Annotate(test.NewPod(), "argocd.argoproj.io/hook", hookType)
}
func podWithHelmHook(hookType string) *unstructured.Unstructured {
return test.Annotate(test.NewPod(), "helm.sh/hook", hookType)
}

130
controller/sync_task.go Normal file
View File

@@ -0,0 +1,130 @@
package controller
import (
"fmt"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/util/hook"
"github.com/argoproj/argo-cd/util/resource/syncwaves"
)
// syncTask holds the live and target object. At least one should be non-nil. A targetObj of nil
// indicates the live object needs to be pruned. A liveObj of nil indicates the object has yet to
// be deployed
type syncTask struct {
phase v1alpha1.SyncPhase
liveObj *unstructured.Unstructured
targetObj *unstructured.Unstructured
skipDryRun bool
syncStatus v1alpha1.ResultCode
operationState v1alpha1.OperationPhase
message string
}
func ternary(val bool, a, b string) string {
if val {
return a
} else {
return b
}
}
func (t *syncTask) String() string {
return fmt.Sprintf("%s/%d %s %s/%s:%s/%s %s->%s (%s,%s,%s)",
t.phase, t.wave(),
ternary(t.isHook(), "hook", "resource"), t.group(), t.kind(), t.namespace(), t.name(),
ternary(t.liveObj != nil, "obj", "nil"), ternary(t.targetObj != nil, "obj", "nil"),
t.syncStatus, t.operationState, t.message,
)
}
func (t *syncTask) isPrune() bool {
return t.targetObj == nil
}
// return the target object (if this exists) otherwise the live object
// some caution - often you explicitly want the live object not the target object
func (t *syncTask) obj() *unstructured.Unstructured {
return obj(t.targetObj, t.liveObj)
}
func (t *syncTask) wave() int {
return syncwaves.Wave(t.obj())
}
func (t *syncTask) isHook() bool {
return hook.IsHook(t.obj())
}
func (t *syncTask) group() string {
return t.groupVersionKind().Group
}
func (t *syncTask) kind() string {
return t.groupVersionKind().Kind
}
func (t *syncTask) version() string {
return t.groupVersionKind().Version
}
func (t *syncTask) groupVersionKind() schema.GroupVersionKind {
return t.obj().GroupVersionKind()
}
func (t *syncTask) name() string {
return t.obj().GetName()
}
func (t *syncTask) namespace() string {
return t.obj().GetNamespace()
}
func (t *syncTask) pending() bool {
return t.operationState == ""
}
func (t *syncTask) running() bool {
return t.operationState.Running()
}
func (t *syncTask) completed() bool {
return t.operationState.Completed()
}
func (t *syncTask) successful() bool {
return t.operationState.Successful()
}
func (t *syncTask) failed() bool {
return t.operationState.Failed()
}
func (t *syncTask) hookType() v1alpha1.HookType {
if t.isHook() {
return v1alpha1.HookType(t.phase)
} else {
return ""
}
}
func (t *syncTask) hasHookDeletePolicy(policy v1alpha1.HookDeletePolicy) bool {
// cannot have a policy if it is not a hook, it is meaningless
if !t.isHook() {
return false
}
for _, p := range hook.DeletePolicies(t.obj()) {
if p == policy {
return true
}
}
return false
}
func (t *syncTask) needsDeleting() bool {
return t.liveObj != nil && (t.pending() && t.hasHookDeletePolicy(v1alpha1.HookDeletePolicyBeforeHookCreation) ||
t.successful() && t.hasHookDeletePolicy(v1alpha1.HookDeletePolicyHookSucceeded) ||
t.failed() && t.hasHookDeletePolicy(v1alpha1.HookDeletePolicyHookFailed))
}

View File

@@ -0,0 +1,66 @@
package controller
import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
. "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
. "github.com/argoproj/argo-cd/test"
)
func Test_syncTask_hookType(t *testing.T) {
type fields struct {
phase SyncPhase
liveObj *unstructured.Unstructured
}
tests := []struct {
name string
fields fields
want HookType
}{
{"Empty", fields{SyncPhaseSync, NewPod()}, ""},
{"PreSyncHook", fields{SyncPhasePreSync, NewHook(HookTypePreSync)}, HookTypePreSync},
{"SyncHook", fields{SyncPhaseSync, NewHook(HookTypeSync)}, HookTypeSync},
{"PostSyncHook", fields{SyncPhasePostSync, NewHook(HookTypePostSync)}, HookTypePostSync},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
task := &syncTask{
phase: tt.fields.phase,
liveObj: tt.fields.liveObj,
}
hookType := task.hookType()
assert.EqualValues(t, tt.want, hookType)
})
}
}
func Test_syncTask_hasHookDeletePolicy(t *testing.T) {
assert.False(t, (&syncTask{targetObj: NewPod()}).hasHookDeletePolicy(HookDeletePolicyBeforeHookCreation))
assert.False(t, (&syncTask{targetObj: NewPod()}).hasHookDeletePolicy(HookDeletePolicyHookSucceeded))
assert.False(t, (&syncTask{targetObj: NewPod()}).hasHookDeletePolicy(HookDeletePolicyHookFailed))
// must be hook
assert.False(t, (&syncTask{targetObj: Annotate(NewPod(), "argocd.argoproj.io/hook-delete-policy", "BeforeHookCreation")}).hasHookDeletePolicy(HookDeletePolicyBeforeHookCreation))
assert.True(t, (&syncTask{targetObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "BeforeHookCreation")}).hasHookDeletePolicy(HookDeletePolicyBeforeHookCreation))
assert.True(t, (&syncTask{targetObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "HookSucceeded")}).hasHookDeletePolicy(HookDeletePolicyHookSucceeded))
assert.True(t, (&syncTask{targetObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "HookFailed")}).hasHookDeletePolicy(HookDeletePolicyHookFailed))
}
func Test_syncTask_needsDeleting(t *testing.T) {
assert.False(t, (&syncTask{liveObj: NewPod()}).needsDeleting())
// must be hook
assert.False(t, (&syncTask{liveObj: Annotate(NewPod(), "argocd.argoproj.io/hook-delete-policy", "BeforeHookCreation")}).needsDeleting())
// no need to delete if no live obj
assert.False(t, (&syncTask{targetObj: Annotate(Annotate(NewPod(), "argoocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "BeforeHookCreation")}).needsDeleting())
assert.True(t, (&syncTask{liveObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "BeforeHookCreation")}).needsDeleting())
assert.True(t, (&syncTask{liveObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "BeforeHookCreation")}).needsDeleting())
assert.True(t, (&syncTask{operationState: OperationSucceeded, liveObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "HookSucceeded")}).needsDeleting())
assert.True(t, (&syncTask{operationState: OperationFailed, liveObj: Annotate(Annotate(NewPod(), "argocd.argoproj.io/hook", "Sync"), "argocd.argoproj.io/hook-delete-policy", "HookFailed")}).needsDeleting())
}
func Test_syncTask_wave(t *testing.T) {
assert.Equal(t, 0, (&syncTask{targetObj: NewPod()}).wave())
assert.Equal(t, 1, (&syncTask{targetObj: Annotate(NewPod(), "argocd.argoproj.io/sync-wave", "1")}).wave())
}

185
controller/sync_tasks.go Normal file
View File

@@ -0,0 +1,185 @@
package controller
import (
"strings"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
)
// kindOrder represents the correct order of Kubernetes resources within a manifest
var syncPhaseOrder = map[v1alpha1.SyncPhase]int{
v1alpha1.SyncPhasePreSync: -1,
v1alpha1.SyncPhaseSync: 0,
v1alpha1.SyncPhasePostSync: 1,
v1alpha1.SyncPhaseSyncFail: 2,
}
// kindOrder represents the correct order of Kubernetes resources within a manifest
// https://github.com/helm/helm/blob/master/pkg/tiller/kind_sorter.go
var kindOrder = map[string]int{}
func init() {
kinds := []string{
"Namespace",
"ResourceQuota",
"LimitRange",
"PodSecurityPolicy",
"PodDisruptionBudget",
"Secret",
"ConfigMap",
"StorageClass",
"PersistentVolume",
"PersistentVolumeClaim",
"ServiceAccount",
"CustomResourceDefinition",
"ClusterRole",
"ClusterRoleBinding",
"Role",
"RoleBinding",
"Service",
"DaemonSet",
"Pod",
"ReplicationController",
"ReplicaSet",
"Deployment",
"StatefulSet",
"Job",
"CronJob",
"Ingress",
"APIService",
}
for i, kind := range kinds {
// make sure none of the above entries are zero, we need that for custom resources
kindOrder[kind] = i - len(kinds)
}
}
type syncTasks []*syncTask
func (s syncTasks) Len() int {
return len(s)
}
func (s syncTasks) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// order is
// 1. phase
// 2. wave
// 3. kind
// 4. name
func (s syncTasks) Less(i, j int) bool {
tA := s[i]
tB := s[j]
d := syncPhaseOrder[tA.phase] - syncPhaseOrder[tB.phase]
if d != 0 {
return d < 0
}
d = tA.wave() - tB.wave()
if d != 0 {
return d < 0
}
a := tA.obj()
b := tB.obj()
// we take advantage of the fact that if the kind is not in the kindOrder map,
// then it will return the default int value of zero, which is the highest value
d = kindOrder[a.GetKind()] - kindOrder[b.GetKind()]
if d != 0 {
return d < 0
}
return a.GetName() < b.GetName()
}
func (s syncTasks) Filter(predicate func(task *syncTask) bool) (tasks syncTasks) {
for _, task := range s {
if predicate(task) {
tasks = append(tasks, task)
}
}
return tasks
}
func (s syncTasks) Split(predicate func(task *syncTask) bool) (trueTasks, falseTasks syncTasks) {
for _, task := range s {
if predicate(task) {
trueTasks = append(trueTasks, task)
} else {
falseTasks = append(falseTasks, task)
}
}
return trueTasks, falseTasks
}
func (s syncTasks) All(predicate func(task *syncTask) bool) bool {
for _, task := range s {
if !predicate(task) {
return false
}
}
return true
}
func (s syncTasks) Any(predicate func(task *syncTask) bool) bool {
for _, task := range s {
if predicate(task) {
return true
}
}
return false
}
func (s syncTasks) Find(predicate func(task *syncTask) bool) *syncTask {
for _, task := range s {
if predicate(task) {
return task
}
}
return nil
}
func (s syncTasks) String() string {
var values []string
for _, task := range s {
values = append(values, task.String())
}
return "[" + strings.Join(values, ", ") + "]"
}
func (s syncTasks) phase() v1alpha1.SyncPhase {
if len(s) > 0 {
return s[0].phase
}
return ""
}
func (s syncTasks) wave() int {
if len(s) > 0 {
return s[0].wave()
}
return 0
}
func (s syncTasks) lastPhase() v1alpha1.SyncPhase {
if len(s) > 0 {
return s[len(s)-1].phase
}
return ""
}
func (s syncTasks) lastWave() int {
if len(s) > 0 {
return s[len(s)-1].wave()
}
return 0
}
func (s syncTasks) multiStep() bool {
return s.wave() != s.lastWave() || s.phase() != s.lastPhase()
}

View File

@@ -0,0 +1,392 @@
package controller
import (
"sort"
"testing"
"github.com/stretchr/testify/assert"
apiv1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/argoproj/argo-cd/common"
. "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
. "github.com/argoproj/argo-cd/test"
)
func Test_syncTasks_kindOrder(t *testing.T) {
assert.Equal(t, -27, kindOrder["Namespace"])
assert.Equal(t, -1, kindOrder["APIService"])
assert.Equal(t, 0, kindOrder["MyCRD"])
}
func TestSortSyncTask(t *testing.T) {
sort.Sort(unsortedTasks)
assert.Equal(t, sortedTasks, unsortedTasks)
}
func TestAnySyncTasks(t *testing.T) {
res := unsortedTasks.Any(func(task *syncTask) bool {
return task.name() == "a"
})
assert.True(t, res)
res = unsortedTasks.Any(func(task *syncTask) bool {
return task.name() == "does-not-exist"
})
assert.False(t, res)
}
func TestAllSyncTasks(t *testing.T) {
res := unsortedTasks.All(func(task *syncTask) bool {
return task.name() != ""
})
assert.False(t, res)
res = unsortedTasks.All(func(task *syncTask) bool {
return task.name() == "a"
})
assert.False(t, res)
}
func TestSplitSyncTasks(t *testing.T) {
named, unnamed := sortedTasks.Split(func(task *syncTask) bool {
return task.name() != ""
})
assert.Equal(t, named, namedObjTasks)
assert.Equal(t, unnamed, unnamedTasks)
}
var unsortedTasks = syncTasks{
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"GroupVersion": apiv1.SchemeGroupVersion.String(),
"kind": "Pod",
},
},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"GroupVersion": apiv1.SchemeGroupVersion.String(),
"kind": "Service",
},
},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"GroupVersion": apiv1.SchemeGroupVersion.String(),
"kind": "PersistentVolume",
},
},
},
{
phase: SyncPhaseSyncFail, targetObj: &unstructured.Unstructured{},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
"argocd.argoproj.io/sync-wave": "1",
},
},
},
},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "b",
},
},
},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "a",
},
},
},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
"argocd.argoproj.io/sync-wave": "-1",
},
},
},
},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"GroupVersion": apiv1.SchemeGroupVersion.String(),
},
},
},
{
phase: SyncPhasePreSync,
targetObj: &unstructured.Unstructured{},
},
{
phase: SyncPhasePostSync, targetObj: &unstructured.Unstructured{},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"GroupVersion": apiv1.SchemeGroupVersion.String(),
"kind": "ConfigMap",
},
},
},
}
var sortedTasks = syncTasks{
{
phase: SyncPhasePreSync,
targetObj: &unstructured.Unstructured{},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
"argocd.argoproj.io/sync-wave": "-1",
},
},
},
},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"GroupVersion": apiv1.SchemeGroupVersion.String(),
"kind": "ConfigMap",
},
},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"GroupVersion": apiv1.SchemeGroupVersion.String(),
"kind": "PersistentVolume",
},
},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"GroupVersion": apiv1.SchemeGroupVersion.String(),
"kind": "Service",
},
},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"GroupVersion": apiv1.SchemeGroupVersion.String(),
"kind": "Pod",
},
},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"GroupVersion": apiv1.SchemeGroupVersion.String(),
},
},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "a",
},
},
},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "b",
},
},
},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
"argocd.argoproj.io/sync-wave": "1",
},
},
},
},
},
{
phase: SyncPhasePostSync,
targetObj: &unstructured.Unstructured{},
},
{
phase: SyncPhaseSyncFail,
targetObj: &unstructured.Unstructured{},
},
}
var namedObjTasks = syncTasks{
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "a",
},
},
},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"name": "b",
},
},
},
},
}
var unnamedTasks = syncTasks{
{
phase: SyncPhasePreSync,
targetObj: &unstructured.Unstructured{},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
"argocd.argoproj.io/sync-wave": "-1",
},
},
},
},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"GroupVersion": apiv1.SchemeGroupVersion.String(),
"kind": "ConfigMap",
},
},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"GroupVersion": apiv1.SchemeGroupVersion.String(),
"kind": "PersistentVolume",
},
},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"GroupVersion": apiv1.SchemeGroupVersion.String(),
"kind": "Service",
},
},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"GroupVersion": apiv1.SchemeGroupVersion.String(),
"kind": "Pod",
},
},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"GroupVersion": apiv1.SchemeGroupVersion.String(),
},
},
},
{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"metadata": map[string]interface{}{
"annotations": map[string]interface{}{
"argocd.argoproj.io/sync-wave": "1",
},
},
},
},
},
{
phase: SyncPhasePostSync,
targetObj: &unstructured.Unstructured{},
},
{
phase: SyncPhaseSyncFail,
targetObj: &unstructured.Unstructured{},
},
}
func Test_syncTasks_Filter(t *testing.T) {
tasks := syncTasks{{phase: SyncPhaseSync}, {phase: SyncPhasePostSync}}
assert.Equal(t, syncTasks{{phase: SyncPhaseSync}}, tasks.Filter(func(t *syncTask) bool {
return t.phase == SyncPhaseSync
}))
}
func TestSyncNamespaceAgainstCRD(t *testing.T) {
crd := &syncTask{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"kind": "Workflow",
},
}}
namespace := &syncTask{
targetObj: &unstructured.Unstructured{
Object: map[string]interface{}{
"kind": "Namespace",
},
},
}
unsorted := syncTasks{crd, namespace}
sort.Sort(unsorted)
assert.Equal(t, syncTasks{namespace, crd}, unsorted)
}
func Test_syncTasks_multiStep(t *testing.T) {
t.Run("Single", func(t *testing.T) {
tasks := syncTasks{{liveObj: Annotate(NewPod(), common.AnnotationSyncWave, "-1"), phase: SyncPhaseSync}}
assert.Equal(t, SyncPhaseSync, tasks.phase())
assert.Equal(t, -1, tasks.wave())
assert.Equal(t, SyncPhaseSync, tasks.lastPhase())
assert.Equal(t, -1, tasks.lastWave())
assert.False(t, tasks.multiStep())
})
t.Run("Double", func(t *testing.T) {
tasks := syncTasks{
{liveObj: Annotate(NewPod(), common.AnnotationSyncWave, "-1"), phase: SyncPhasePreSync},
{liveObj: Annotate(NewPod(), common.AnnotationSyncWave, "1"), phase: SyncPhasePostSync},
}
assert.Equal(t, SyncPhasePreSync, tasks.phase())
assert.Equal(t, -1, tasks.wave())
assert.Equal(t, SyncPhasePostSync, tasks.lastPhase())
assert.Equal(t, 1, tasks.lastWave())
assert.True(t, tasks.multiStep())
})
}

View File

@@ -1,19 +1,448 @@
package controller
import (
"fmt"
"reflect"
"testing"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
fakedisco "k8s.io/client-go/discovery/fake"
"k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/rest"
testcore "k8s.io/client-go/testing"
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
. "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/reposerver/apiclient"
"github.com/argoproj/argo-cd/test"
"github.com/argoproj/argo-cd/util/kube"
"github.com/argoproj/argo-cd/util/kube/kubetest"
)
func newTestSyncCtx(resources ...*v1.APIResourceList) *syncContext {
fakeDisco := &fakedisco.FakeDiscovery{Fake: &testcore.Fake{}}
fakeDisco.Resources = append(resources,
&v1.APIResourceList{
GroupVersion: "v1",
APIResources: []v1.APIResource{
{Kind: "Pod", Group: "", Version: "v1", Namespaced: true},
{Kind: "Service", Group: "", Version: "v1", Namespaced: true},
},
},
&v1.APIResourceList{
GroupVersion: "apps/v1",
APIResources: []v1.APIResource{
{Kind: "Deployment", Group: "apps", Version: "v1", Namespaced: true},
},
})
sc := syncContext{
config: &rest.Config{},
namespace: test.FakeArgoCDNamespace,
server: test.FakeClusterURL,
syncRes: &v1alpha1.SyncOperationResult{
Revision: "FooBarBaz",
},
syncOp: &v1alpha1.SyncOperation{
Prune: true,
SyncStrategy: &v1alpha1.SyncStrategy{
Apply: &v1alpha1.SyncStrategyApply{},
},
},
proj: &v1alpha1.AppProject{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
},
Spec: v1alpha1.AppProjectSpec{
Destinations: []v1alpha1.ApplicationDestination{{
Server: test.FakeClusterURL,
Namespace: test.FakeArgoCDNamespace,
}},
ClusterResourceWhitelist: []v1.GroupKind{
{Group: "*", Kind: "*"},
},
},
},
opState: &v1alpha1.OperationState{},
disco: fakeDisco,
log: log.WithFields(log.Fields{"application": "fake-app"}),
}
sc.kubectl = &kubetest.MockKubectlCmd{}
return &sc
}
func newManagedResource(live *unstructured.Unstructured) managedResource {
return managedResource{
Live: live,
Group: live.GroupVersionKind().Group,
Version: live.GroupVersionKind().Version,
Kind: live.GroupVersionKind().Kind,
Namespace: live.GetNamespace(),
Name: live.GetName(),
}
}
func TestSyncNotPermittedNamespace(t *testing.T) {
syncCtx := newTestSyncCtx()
targetPod := test.NewPod()
targetPod.SetNamespace("kube-system")
syncCtx.compareResult = &comparisonResult{
managedResources: []managedResource{{
Live: nil,
Target: targetPod,
}, {
Live: nil,
Target: test.NewService(),
}},
}
syncCtx.sync()
assert.Equal(t, v1alpha1.OperationFailed, syncCtx.opState.Phase)
assert.Contains(t, syncCtx.syncRes.Resources[0].Message, "not permitted in project")
}
func TestSyncCreateInSortedOrder(t *testing.T) {
syncCtx := newTestSyncCtx()
syncCtx.compareResult = &comparisonResult{
managedResources: []managedResource{{
Live: nil,
Target: test.NewPod(),
}, {
Live: nil,
Target: test.NewService(),
}},
}
syncCtx.sync()
assert.Equal(t, v1alpha1.OperationSucceeded, syncCtx.opState.Phase)
assert.Len(t, syncCtx.syncRes.Resources, 2)
for i := range syncCtx.syncRes.Resources {
result := syncCtx.syncRes.Resources[i]
if result.Kind == "Pod" {
assert.Equal(t, v1alpha1.ResultCodeSynced, result.Status)
assert.Equal(t, "", result.Message)
} else if result.Kind == "Service" {
assert.Equal(t, "", result.Message)
} else {
t.Error("Resource isn't a pod or a service")
}
}
}
func TestSyncCreateNotWhitelistedClusterResources(t *testing.T) {
syncCtx := newTestSyncCtx(&v1.APIResourceList{
GroupVersion: v1alpha1.SchemeGroupVersion.String(),
APIResources: []v1.APIResource{
{Name: "workflows", Namespaced: false, Kind: "Workflow", Group: "argoproj.io"},
{Name: "application", Namespaced: false, Kind: "Application", Group: "argoproj.io"},
},
}, &v1.APIResourceList{
GroupVersion: "rbac.authorization.k8s.io/v1",
APIResources: []v1.APIResource{
{Name: "clusterroles", Namespaced: false, Kind: "ClusterRole", Group: "rbac.authorization.k8s.io"},
},
})
syncCtx.proj.Spec.ClusterResourceWhitelist = []v1.GroupKind{
{Group: "argoproj.io", Kind: "*"},
}
syncCtx.kubectl = &kubetest.MockKubectlCmd{}
syncCtx.compareResult = &comparisonResult{
managedResources: []managedResource{{
Live: nil,
Target: kube.MustToUnstructured(&rbacv1.ClusterRole{
TypeMeta: metav1.TypeMeta{Kind: "ClusterRole", APIVersion: "rbac.authorization.k8s.io/v1"},
ObjectMeta: metav1.ObjectMeta{Name: "argo-ui-cluster-role"}}),
}},
}
syncCtx.sync()
assert.Len(t, syncCtx.syncRes.Resources, 1)
result := syncCtx.syncRes.Resources[0]
assert.Equal(t, v1alpha1.ResultCodeSyncFailed, result.Status)
assert.Contains(t, result.Message, "not permitted in project")
}
func TestSyncBlacklistedNamespacedResources(t *testing.T) {
syncCtx := newTestSyncCtx()
syncCtx.proj.Spec.NamespaceResourceBlacklist = []v1.GroupKind{
{Group: "*", Kind: "Deployment"},
}
syncCtx.compareResult = &comparisonResult{
managedResources: []managedResource{{
Live: nil,
Target: test.NewDeployment(),
}},
}
syncCtx.sync()
assert.Len(t, syncCtx.syncRes.Resources, 1)
result := syncCtx.syncRes.Resources[0]
assert.Equal(t, v1alpha1.ResultCodeSyncFailed, result.Status)
assert.Contains(t, result.Message, "not permitted in project")
}
func TestSyncSuccessfully(t *testing.T) {
syncCtx := newTestSyncCtx()
pod := test.NewPod()
pod.SetNamespace(test.FakeArgoCDNamespace)
syncCtx.compareResult = &comparisonResult{
managedResources: []managedResource{{
Live: nil,
Target: test.NewService(),
}, {
Live: pod,
Target: nil,
}},
}
syncCtx.sync()
assert.Equal(t, v1alpha1.OperationSucceeded, syncCtx.opState.Phase)
assert.Len(t, syncCtx.syncRes.Resources, 2)
for i := range syncCtx.syncRes.Resources {
result := syncCtx.syncRes.Resources[i]
if result.Kind == "Pod" {
assert.Equal(t, v1alpha1.ResultCodePruned, result.Status)
assert.Equal(t, "pruned", result.Message)
} else if result.Kind == "Service" {
assert.Equal(t, v1alpha1.ResultCodeSynced, result.Status)
assert.Equal(t, "", result.Message)
} else {
t.Error("Resource isn't a pod or a service")
}
}
}
func TestSyncDeleteSuccessfully(t *testing.T) {
syncCtx := newTestSyncCtx()
svc := test.NewService()
svc.SetNamespace(test.FakeArgoCDNamespace)
pod := test.NewPod()
pod.SetNamespace(test.FakeArgoCDNamespace)
syncCtx.compareResult = &comparisonResult{
managedResources: []managedResource{{
Live: svc,
Target: nil,
}, {
Live: pod,
Target: nil,
}},
}
syncCtx.sync()
assert.Equal(t, v1alpha1.OperationSucceeded, syncCtx.opState.Phase)
for i := range syncCtx.syncRes.Resources {
result := syncCtx.syncRes.Resources[i]
if result.Kind == "Pod" {
assert.Equal(t, v1alpha1.ResultCodePruned, result.Status)
assert.Equal(t, "pruned", result.Message)
} else if result.Kind == "Service" {
assert.Equal(t, v1alpha1.ResultCodePruned, result.Status)
assert.Equal(t, "pruned", result.Message)
} else {
t.Error("Resource isn't a pod or a service")
}
}
}
func TestSyncCreateFailure(t *testing.T) {
syncCtx := newTestSyncCtx()
testSvc := test.NewService()
syncCtx.kubectl = &kubetest.MockKubectlCmd{
Commands: map[string]kubetest.KubectlOutput{
testSvc.GetName(): {
Output: "",
Err: fmt.Errorf("foo"),
},
},
}
syncCtx.compareResult = &comparisonResult{
managedResources: []managedResource{{
Live: nil,
Target: testSvc,
}},
}
syncCtx.sync()
assert.Len(t, syncCtx.syncRes.Resources, 1)
result := syncCtx.syncRes.Resources[0]
assert.Equal(t, v1alpha1.ResultCodeSyncFailed, result.Status)
assert.Equal(t, "foo", result.Message)
}
func TestSyncPruneFailure(t *testing.T) {
syncCtx := newTestSyncCtx()
syncCtx.kubectl = &kubetest.MockKubectlCmd{
Commands: map[string]kubetest.KubectlOutput{
"test-service": {
Output: "",
Err: fmt.Errorf("foo"),
},
},
}
testSvc := test.NewService()
testSvc.SetName("test-service")
testSvc.SetNamespace(test.FakeArgoCDNamespace)
syncCtx.compareResult = &comparisonResult{
managedResources: []managedResource{{
Live: testSvc,
Target: nil,
}},
}
syncCtx.sync()
assert.Equal(t, v1alpha1.OperationFailed, syncCtx.opState.Phase)
assert.Len(t, syncCtx.syncRes.Resources, 1)
result := syncCtx.syncRes.Resources[0]
assert.Equal(t, v1alpha1.ResultCodeSyncFailed, result.Status)
assert.Equal(t, "foo", result.Message)
}
func TestDontSyncOrPruneHooks(t *testing.T) {
syncCtx := newTestSyncCtx()
targetPod := test.NewPod()
targetPod.SetName("dont-create-me")
targetPod.SetAnnotations(map[string]string{common.AnnotationKeyHook: "PreSync"})
liveSvc := test.NewService()
liveSvc.SetName("dont-prune-me")
liveSvc.SetNamespace(test.FakeArgoCDNamespace)
liveSvc.SetAnnotations(map[string]string{common.AnnotationKeyHook: "PreSync"})
syncCtx.compareResult = &comparisonResult{
hooks: []*unstructured.Unstructured{targetPod, liveSvc},
}
syncCtx.sync()
assert.Len(t, syncCtx.syncRes.Resources, 0)
syncCtx.sync()
assert.Equal(t, v1alpha1.OperationSucceeded, syncCtx.opState.Phase)
}
// make sure that we do not prune resources with Prune=false
func TestDontPrunePruneFalse(t *testing.T) {
syncCtx := newTestSyncCtx()
pod := test.NewPod()
pod.SetAnnotations(map[string]string{common.AnnotationSyncOptions: "Prune=false"})
pod.SetNamespace(test.FakeArgoCDNamespace)
syncCtx.compareResult = &comparisonResult{managedResources: []managedResource{{Live: pod}}}
syncCtx.sync()
assert.Equal(t, v1alpha1.OperationSucceeded, syncCtx.opState.Phase)
assert.Len(t, syncCtx.syncRes.Resources, 1)
assert.Equal(t, v1alpha1.ResultCodePruneSkipped, syncCtx.syncRes.Resources[0].Status)
assert.Equal(t, "ignored (no prune)", syncCtx.syncRes.Resources[0].Message)
syncCtx.sync()
assert.Equal(t, v1alpha1.OperationSucceeded, syncCtx.opState.Phase)
}
// make sure Validate=false means we don't validate
func TestSyncOptionValidate(t *testing.T) {
tests := []struct {
name string
annotationVal string
want bool
}{
{"Empty", "", true},
{"True", "Validate=true", true},
{"False", "Validate=false", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
syncCtx := newTestSyncCtx()
pod := test.NewPod()
pod.SetAnnotations(map[string]string{common.AnnotationSyncOptions: tt.annotationVal})
pod.SetNamespace(test.FakeArgoCDNamespace)
syncCtx.compareResult = &comparisonResult{managedResources: []managedResource{{Target: pod, Live: pod}}}
syncCtx.sync()
kubectl, _ := syncCtx.kubectl.(*kubetest.MockKubectlCmd)
assert.Equal(t, tt.want, kubectl.LastValidate)
})
}
}
func TestSelectiveSyncOnly(t *testing.T) {
syncCtx := newTestSyncCtx()
pod1 := test.NewPod()
pod1.SetName("pod-1")
pod2 := test.NewPod()
pod2.SetName("pod-2")
syncCtx.compareResult = &comparisonResult{
managedResources: []managedResource{{Target: pod1}},
}
syncCtx.syncResources = []v1alpha1.SyncOperationResource{{Kind: "Pod", Name: "pod-1"}}
tasks, successful := syncCtx.getSyncTasks()
assert.True(t, successful)
assert.Len(t, tasks, 1)
assert.Equal(t, "pod-1", tasks[0].name())
}
func TestUnnamedHooksGetUniqueNames(t *testing.T) {
syncCtx := newTestSyncCtx()
syncCtx.syncOp.SyncStrategy.Apply = nil
pod := test.NewPod()
pod.SetName("")
pod.SetAnnotations(map[string]string{common.AnnotationKeyHook: "PreSync,PostSync"})
syncCtx.compareResult = &comparisonResult{hooks: []*unstructured.Unstructured{pod}}
tasks, successful := syncCtx.getSyncTasks()
assert.True(t, successful)
assert.Len(t, tasks, 2)
assert.Contains(t, tasks[0].name(), "foobarb-presync-")
assert.Contains(t, tasks[1].name(), "foobarb-postsync-")
assert.Equal(t, "", pod.GetName())
}
func TestManagedResourceAreNotNamed(t *testing.T) {
syncCtx := newTestSyncCtx()
pod := test.NewPod()
pod.SetName("")
syncCtx.compareResult = &comparisonResult{managedResources: []managedResource{{Target: pod}}}
tasks, successful := syncCtx.getSyncTasks()
assert.True(t, successful)
assert.Len(t, tasks, 1)
assert.Equal(t, "", tasks[0].name())
assert.Equal(t, "", pod.GetName())
}
func TestDeDupingTasks(t *testing.T) {
syncCtx := newTestSyncCtx()
syncCtx.syncOp.SyncStrategy.Apply = nil
pod := test.NewPod()
pod.SetAnnotations(map[string]string{common.AnnotationKeyHook: "Sync"})
syncCtx.compareResult = &comparisonResult{
managedResources: []managedResource{{Target: pod}},
hooks: []*unstructured.Unstructured{pod},
}
tasks, successful := syncCtx.getSyncTasks()
assert.True(t, successful)
assert.Len(t, tasks, 1)
}
func TestObjectsGetANamespace(t *testing.T) {
syncCtx := newTestSyncCtx()
pod := test.NewPod()
syncCtx.compareResult = &comparisonResult{managedResources: []managedResource{{Target: pod}}}
tasks, successful := syncCtx.getSyncTasks()
assert.True(t, successful)
assert.Len(t, tasks, 1)
assert.Equal(t, test.FakeArgoCDNamespace, tasks[0].namespace())
assert.Equal(t, "", pod.GetNamespace())
}
func TestPersistRevisionHistory(t *testing.T) {
app := newFakeApp()
app.Status.OperationState = nil
@@ -100,3 +529,171 @@ func TestPersistRevisionHistoryRollback(t *testing.T) {
assert.Equal(t, source, updatedApp.Status.History[0].Source)
assert.Equal(t, "abc123", updatedApp.Status.History[0].Revision)
}
func TestSyncFailureHookWithSuccessfulSync(t *testing.T) {
syncCtx := newTestSyncCtx()
syncCtx.syncOp.SyncStrategy.Apply = nil
syncCtx.compareResult = &comparisonResult{
managedResources: []managedResource{{Target: test.NewPod()}},
hooks: []*unstructured.Unstructured{test.NewHook(HookTypeSyncFail)},
}
syncCtx.sync()
assert.Equal(t, OperationSucceeded, syncCtx.opState.Phase)
// only one result, we did not run the failure failureHook
assert.Len(t, syncCtx.syncRes.Resources, 1)
}
func TestSyncFailureHookWithFailedSync(t *testing.T) {
syncCtx := newTestSyncCtx()
syncCtx.syncOp.SyncStrategy.Apply = nil
pod := test.NewPod()
syncCtx.compareResult = &comparisonResult{
managedResources: []managedResource{{Target: pod}},
hooks: []*unstructured.Unstructured{test.NewHook(HookTypeSyncFail)},
}
syncCtx.kubectl = &kubetest.MockKubectlCmd{
Commands: map[string]kubetest.KubectlOutput{pod.GetName(): {Err: fmt.Errorf("")}},
}
syncCtx.sync()
syncCtx.sync()
assert.Equal(t, OperationFailed, syncCtx.opState.Phase)
assert.Len(t, syncCtx.syncRes.Resources, 2)
}
func TestBeforeHookCreation(t *testing.T) {
syncCtx := newTestSyncCtx()
syncCtx.syncOp.SyncStrategy.Apply = nil
hook := test.Annotate(test.Annotate(test.NewPod(), common.AnnotationKeyHook, "Sync"), common.AnnotationKeyHookDeletePolicy, "BeforeHookCreation")
hook.SetNamespace(test.FakeArgoCDNamespace)
syncCtx.compareResult = &comparisonResult{
managedResources: []managedResource{newManagedResource(hook)},
hooks: []*unstructured.Unstructured{hook},
}
syncCtx.dynamicIf = fake.NewSimpleDynamicClient(runtime.NewScheme())
syncCtx.sync()
assert.Len(t, syncCtx.syncRes.Resources, 1)
assert.Empty(t, syncCtx.syncRes.Resources[0].Message)
}
func TestRunSyncFailHooksFailed(t *testing.T) {
// Tests that other SyncFail Hooks run even if one of them fail.
syncCtx := newTestSyncCtx()
syncCtx.syncOp.SyncStrategy.Apply = nil
pod := test.NewPod()
successfulSyncFailHook := test.NewHook(HookTypeSyncFail)
successfulSyncFailHook.SetName("successful-sync-fail-hook")
failedSyncFailHook := test.NewHook(HookTypeSyncFail)
failedSyncFailHook.SetName("failed-sync-fail-hook")
syncCtx.compareResult = &comparisonResult{
managedResources: []managedResource{{Target: pod}},
hooks: []*unstructured.Unstructured{successfulSyncFailHook, failedSyncFailHook},
}
syncCtx.kubectl = &kubetest.MockKubectlCmd{
Commands: map[string]kubetest.KubectlOutput{
// Fail operation
pod.GetName(): {Err: fmt.Errorf("")},
// Fail a single SyncFail hook
failedSyncFailHook.GetName(): {Err: fmt.Errorf("")}},
}
syncCtx.sync()
syncCtx.sync()
fmt.Println(syncCtx.syncRes.Resources)
fmt.Println(syncCtx.opState.Phase)
// Operation as a whole should fail
assert.Equal(t, OperationFailed, syncCtx.opState.Phase)
// failedSyncFailHook should fail
assert.Equal(t, OperationFailed, syncCtx.syncRes.Resources[1].HookPhase)
assert.Equal(t, ResultCodeSyncFailed, syncCtx.syncRes.Resources[1].Status)
// successfulSyncFailHook should be synced running (it is an nginx pod)
assert.Equal(t, OperationRunning, syncCtx.syncRes.Resources[2].HookPhase)
assert.Equal(t, ResultCodeSynced, syncCtx.syncRes.Resources[2].Status)
}
func Test_syncContext_isSelectiveSync(t *testing.T) {
type fields struct {
compareResult *comparisonResult
syncResources []SyncOperationResource
}
oneSyncResource := []SyncOperationResource{{}}
oneResource := func(group, kind, name string, hook bool) *comparisonResult {
return &comparisonResult{resources: []v1alpha1.ResourceStatus{{Group: group, Kind: kind, Name: name, Hook: hook}}}
}
tests := []struct {
name string
fields fields
want bool
}{
{"Empty", fields{}, false},
{"OneCompareResult", fields{oneResource("", "", "", false), []SyncOperationResource{}}, true},
{"OneSyncResource", fields{&comparisonResult{}, oneSyncResource}, true},
{"Equal", fields{oneResource("", "", "", false), oneSyncResource}, false},
{"EqualOutOfOrder", fields{&comparisonResult{resources: []v1alpha1.ResourceStatus{{Group: "a"}, {Group: "b"}}}, []SyncOperationResource{{Group: "b"}, {Group: "a"}}}, false},
{"KindDifferent", fields{oneResource("foo", "", "", false), oneSyncResource}, true},
{"GroupDifferent", fields{oneResource("", "foo", "", false), oneSyncResource}, true},
{"NameDifferent", fields{oneResource("", "", "foo", false), oneSyncResource}, true},
{"HookIgnored", fields{oneResource("", "", "", true), []SyncOperationResource{}}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sc := &syncContext{
compareResult: tt.fields.compareResult,
syncResources: tt.fields.syncResources,
}
if got := sc.isSelectiveSync(); got != tt.want {
t.Errorf("syncContext.isSelectiveSync() = %v, want %v", got, tt.want)
}
})
}
}
func Test_syncContext_liveObj(t *testing.T) {
type fields struct {
compareResult *comparisonResult
}
type args struct {
obj *unstructured.Unstructured
}
obj := test.NewPod()
obj.SetNamespace("my-ns")
found := test.NewPod()
tests := []struct {
name string
fields fields
args args
want *unstructured.Unstructured
}{
{"None", fields{compareResult: &comparisonResult{managedResources: []managedResource{}}}, args{obj: &unstructured.Unstructured{}}, nil},
{"Found", fields{compareResult: &comparisonResult{managedResources: []managedResource{{Group: obj.GroupVersionKind().Group, Kind: obj.GetKind(), Namespace: obj.GetNamespace(), Name: obj.GetName(), Live: found}}}}, args{obj: obj}, found},
{"EmptyNamespace", fields{compareResult: &comparisonResult{managedResources: []managedResource{{Group: obj.GroupVersionKind().Group, Kind: obj.GetKind(), Name: obj.GetName(), Live: found}}}}, args{obj: obj}, found},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sc := &syncContext{
compareResult: tt.fields.compareResult,
}
if got := sc.liveObj(tt.args.obj); !reflect.DeepEqual(got, tt.want) {
t.Errorf("syncContext.liveObj() = %v, want %v", got, tt.want)
}
})
}
}
func Test_syncContext_hasCRDOfGroupKind(t *testing.T) {
// target
assert.False(t, (&syncContext{compareResult: &comparisonResult{managedResources: []managedResource{{Target: test.NewCRD()}}}}).hasCRDOfGroupKind("", ""))
assert.True(t, (&syncContext{compareResult: &comparisonResult{managedResources: []managedResource{{Target: test.NewCRD()}}}}).hasCRDOfGroupKind("argoproj.io", "TestCrd"))
// hook
assert.False(t, (&syncContext{compareResult: &comparisonResult{hooks: []*unstructured.Unstructured{test.NewCRD()}}}).hasCRDOfGroupKind("", ""))
assert.True(t, (&syncContext{compareResult: &comparisonResult{hooks: []*unstructured.Unstructured{test.NewCRD()}}}).hasCRDOfGroupKind("argoproj.io", "TestCrd"))
}

View File

@@ -1 +1,167 @@
Please refer to [the Contribution Guide](https://argoproj.github.io/argo-cd/developer-guide/contributing/)
# Contributing
## Before You Start
You must install and run the ArgoCD using a local Kubernetes (e.g. Docker for Desktop or Minikube) first. This will help you understand the application, but also get your local environment set-up.
Then, to get a good grounding in Go, try out [the tutorial](https://tour.golang.org/).
## Pre-requisites
Install:
* [docker](https://docs.docker.com/install/#supported-platforms)
* [git](https://git-scm.com/) and [git-lfs](https://git-lfs.github.com/)
* [golang](https://golang.org/)
* [dep](https://github.com/golang/dep)
* [ksonnet](https://github.com/ksonnet/ksonnet#install)
* [helm](https://github.com/helm/helm/releases)
* [kustomize](https://github.com/kubernetes-sigs/kustomize/releases)
* [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
* [kubectx](https://kubectx.dev)
* [minikube](https://kubernetes.io/docs/setup/minikube/) or Docker for Desktop
Brew users can quickly install the lot:
```bash
brew install go git-lfs kubectl kubectx dep ksonnet/tap/ks kubernetes-helm kustomize
```
Check the versions:
```bash
go version ;# must be v1.12.x
helm version ;# must be v2.13.x
kustomize version ;# must be v3.1.x
```
Set up environment variables (e.g. is `~/.bashrc`):
```bash
export GOPATH=~/go
export PATH=$PATH:$GOPATH/bin
```
Checkout the code:
```bash
go get -u github.com/argoproj/argo-cd
cd ~/go/src/github.com/argoproj/argo-cd
```
## Building
Ensure dependencies are up to date first:
```shell
dep ensure
make dev-tools-image
make install-lint-tools
go get github.com/mattn/goreman
go get github.com/jstemmer/go-junit-report
```
Common make targets:
* `make codegen` - Run code generation
* `make lint` - Lint code
* `make test` - Run unit tests
* `make cli` - Make the `argocd` CLI tool
Check out the following [documentation](https://github.com/argoproj/argo-cd/blob/master/docs/developer-guide/test-e2e.md) for instructions on running the e2e tests.
## Running Locally
It is much easier to run and debug if you run ArgoCD on your local machine than in the Kubernetes cluster.
You should scale the deployments to zero:
```bash
kubectl -n argocd scale deployment/argocd-application-controller --replicas 0
kubectl -n argocd scale deployment/argocd-dex-server --replicas 0
kubectl -n argocd scale deployment/argocd-repo-server --replicas 0
kubectl -n argocd scale deployment/argocd-server --replicas 0
kubectl -n argocd scale deployment/argocd-redis --replicas 0
```
Download Yarn dependencies and Compile:
```bash
~/go/src/github.com/argoproj/argo-cd/ui
yarn install
yarn build
```
Then start the services:
```bash
cd ~/go/src/github.com/argoproj/argo-cd
make start
```
You can now execute `argocd` command against your locally running ArgoCD by appending `--server localhost:8080 --plaintext --insecure`, e.g.:
```bash
argocd app create guestbook --path guestbook --repo https://github.com/argoproj/argocd-example-apps.git --dest-server https://kubernetes.default.svc --dest-namespace default --server localhost:8080 --plaintext --insecure
```
You can open the UI: [http://localhost:4000](http://localhost:4000)
As an alternative to using the above command line parameters each time you call `argocd` CLI, you can set the following environment variables:
```bash
export ARGOCD_SERVER=127.0.0.1:8080
export ARGOCD_OPTS="--plaintext --insecure"
```
## Running Local Containers
You may need to run containers locally, so here's how:
Create login to Docker Hub, then login.
```bash
docker login
```
Add your username as the environment variable, e.g. to your `~/.bash_profile`:
```bash
export IMAGE_NAMESPACE=alexcollinsintuit
```
If you don't want to use `latest` as the image's tag (the default), you can set it from the environment too:
```bash
export IMAGE_TAG=yourtag
```
Build the image:
```bash
DOCKER_PUSH=true make image
```
Update the manifests (be sure to do that from a shell that has above environment variables set)
```bash
make manifests
```
Install the manifests:
```bash
kubectl -n argocd apply --force -f manifests/install.yaml
```
Scale your deployments up:
```bash
kubectl -n argocd scale deployment/argocd-application-controller --replicas 1
kubectl -n argocd scale deployment/argocd-dex-server --replicas 1
kubectl -n argocd scale deployment/argocd-repo-server --replicas 1
kubectl -n argocd scale deployment/argocd-server --replicas 1
kubectl -n argocd scale deployment/argocd-redis --replicas 1
```
Now you can set-up the port-forwarding and open the UI or CLI.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -1,175 +0,0 @@
# Bug triage proposal for ArgoCD
## Situation
Lots of issues on our issue tracker. Many of them not bugs, but questions,
or very environment related. It's easy to lose oversight.
Also, it's not obvious which bugs are important. Which bugs should be fixed
first? Can we make a new release with the current inventory of open bugs?
Is there still a bug that should make it to the new release?
## Proposal
We should agree upon a common issue triage process. The process must be lean
and efficient, and should support us and the community looking into the GH
issue tracker at making the following decisions:
* Is it even a real bug?
* If it is a real bug, what is the current status of the bug (next to "open" or "closed")?
* How important is it to fix the bug?
* How urgent is it to fix the bug?
* Who will be working to fix the bug?
We need new methods to classify our bugs, at least into these categories:
* validity: Does the issue indeed represent a true bug
* severity: Denominates what impact the bug has
* priority: Denominates the urgency of the fix
## Triage process
GH issue tracker provides us with the possibility to label issues. Using these
labels is not perfect, but should give a good start. Each new issue created in
our issue tracker should be correctly labeled during its lifecycle, so keeping
an overview would be simplified by the ability to filter for labels.
The triage process could be as follows:
1. A new bug issue is created by someone on the tracker
1. The first person of the core team to see it will start the triage by classifying
the issue (see below). This will indicate the creator that we have noticed the
issue, and that it's not "fire & forget" tracker.
1. Initial classification should be possible even when much of the information is
missing yet. In this case, the issue would be classified as such (see below).
Again, this indicates that someone has noticed the issue, and there is activity
in progress to get the required information.
1. Classification of the issue can change over its life-cycle. However, once the
issue has been initially classified correctly (that is, with something else than
the "placeholder" classification discussed above), changes to the classification
should be discussed first with the person who initially classified the issue.
## Classification
We have introduced some new labels in the GH issue tracker for classifying the
bug issues. These labels are prefixed with the string `bug/`, and should be
applied to all new issues in our tracker.
### Classification requires more information
If it is not yet possible to classify the bug, i.e. because more information is
required to correctly classify the bug, you should always set the label
`bug/in-triage` to make it clear that triage process has started but could not
yet be completed.
### Issue type
If it's clear that a bug issue is not a bug, but a question or reach for support,
it should be marked as such:
* Remove any of the labels prefixed `bug/` that might be attached to the issue
* Remove the label `bug` from the issue
* Add the label `inquiry` to the issue
If the inquiry turns out to be something that should be covered by the docs, but
is not, the following actions should be taken:
* The title of the issue should be adapted that it will be clear that the bug
affects the docs, not the code
* The label `documentation` should be attached to the issue
If the issue is too confusing (can happen), another possibility is to close the
issue and create a new one as described in above (with a meaningful title and
the label `documentation` attached to it).
### Validity
Some reported bugs may be invalid. It could be a user error, a misconfiguration
or something along these lines. If it is clear that the bug falls into one of
these categories:
* Remove any of the labels prefixed `bug/` that might be attached to the issue
* Add the label `invalid` to the issue
* Retain the `bug` label to the issue
* Close the issue
When closing the issue, it is important to let requester know why the issue
has been closed. The optimum would be to provide a solution to his request
in the comments of the issue, or at least pointers to possible solutions.
### Regressions
Sometimes it happens that something that worked in a previous release does
not work now when it should still work. If this is the case, the following
actions should be done
* Add the label `regression` to the issue
* Continue with triage
### Severity
It is important to find out how severe the impact of a bug is, and to label
the bug with this information. For this purpose, the following labels exist
in our tracker:
* `bug/severity:minor`: Bug has limited impact and maybe affects only an
edge-case. Core functionality is not affected, and there is no data loss
involved. Something might not work as expected. Example of these kind of
bugs could be a CLI command that is not working as expected, a glitch in
the UI, wrong documentation, etc.
* `bug/severity:major`: Malfunction in one of the core components, impacting
a majority of users or one of the core functionalities in ArgoCD. There is
no data loss involved, but for example a sync is not working due to a bug
in ArgoCD (and not due to user error), manifests fail to render, etc.
* `bug/severity:critical`: A critical bug in ArgoCD, possibly resulting in
data loss, integrity breach or severe degraded overall functionality.
### Priority
The priority of an issue indicates how quickly the issue should be fixed and
released. This information should help us in deciding the target release for
the fix, and whether a bug would even justify a dedicated patch release. The
following labels can be used to classify bugs into their priority:
* `bug/priority:low`: Will be fixed without any specific target release.
* `bug/priority:medium`: Should be fixed in the minor or major release, which
ever comes first.
* `bug/priority:high`: Should be fixed with the next patch release.
* `bug/priority:urgent`: Should be fixed immediately and might even justify a
dedicated patch release.
The priority should be set according to the value of the fix and the attached
severity. This means. a bug with a severity of `minor` could still be classified
with priority `high`, when it is a *low hanging fruit* (i.e. the bug is easy to
fix with low effort) and contributes to overall user experience of ArgoCD.
Likewise, a bug classified with a severity of `major` could still have a
priority of `medium`, if there is a workaround available for the bug which
mitigates the effects of the bug to a bearable extend.
Bugs classified with a severity of `critical` most likely belong to either
the `urgent` priority, or to the `high` category when there is a workaround
available.
Bugs that have a `regression`label attached (see Regression above) should
usually be handled with higher priority, so those kind of issues will most
likely have a priority of `high` or `urgent` attached to it.
## Summary
Applying a little discipline when working with our issue tracker could greatly
help us in making informed decision about which bugs to fix when. Also, it
would help us to get a clear view whether we can do for example a new minor
release without having forgot any outstanding issues that should make it into
that release.
If we are able to work with classification of bug issues, we might want to
extend the triage for enhancement proposals and PRs as well.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

View File

@@ -1,9 +1,5 @@
# CI
!!!warning
This documentation is out-of-date. Please bear with us while we work to
update the documentation to reflect reality!
## Troubleshooting Builds
### "Check nothing has changed" step fails
@@ -42,12 +38,4 @@ make builder-image IMAGE_NAMESPACE=argoproj IMAGE_TAG=v1.0.0
## Public CD
Every commit to master is built and published to `docker.pkg.github.com/argoproj/argo-cd/argocd:<version>-<short-sha>`. The list of images is available at
https://github.com/argoproj/argo-cd/packages.
!!! note
Github docker registry [requires](https://github.community/t5/GitHub-Actions/docker-pull-from-public-GitHub-Package-Registry-fail-with-quot/m-p/32888#M1294) authentication to read
even publicly available packages. Follow the steps from Kubernetes [documentation](https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry)
to configure image pull secret if you want to use `docker.pkg.github.com/argoproj/argo-cd/argocd` image.
The image is automatically deployed to the dev Argo CD instance: [https://cd.apps.argoproj.io/](https://cd.apps.argoproj.io/)
[https://cd.apps.argoproj.io/](https://cd.apps.argoproj.io/)

View File

@@ -1,261 +0,0 @@
# Contribution guide
## Preface
We want to make contributing to ArgoCD as simple and smooth as possible.
This guide shall help you in setting up your build & test environment, so that you can start developing and testing bug fixes and feature enhancements without having to make too much effort in setting up a local toolchain.
If you want to to submit a PR, please read this document carefully, as it contains important information guiding you through our PR quality gates.
As is the case with the development process, this document is under constant change. If you notice any error, or if you think this document is out-of-date, or if you think it is missing something: Feel free to submit a PR or submit a bug to our GitHub issue tracker.
If you need guidance with submitting a PR, or have any other questions regarding development of ArgoCD, do not hesitate to [join our Slack](https://argoproj.github.io/community/join-slack) and get in touch with us in the `#argo-dev` channel!
## Before you start
You will need at least the following things in your toolchain in order to develop and test ArgoCD locally:
* A Kubernetes cluster. You won't need a fully blown multi-master, multi-node cluster, but you will need something like K3S, Minikube or microk8s. You will also need a working Kubernetes client (`kubectl`) configuration in your development environment. The configuration must reside in `~/.kube/config` and the API server URL must point to the IP address of your local machine (or VM), and **not** to `localhost` or `127.0.0.1` if you are using the virtualized development toolchain (see below)
* You will also need a working Docker runtime environment, to be able to build and run images.
The Docker version must be fairly recent, and support multi-stage builds. You should not work as root. Make your local user a member of the `docker` group to be able to control the Docker service on your machine.
* 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 ArgoCD is **v1.14.0**.
* We will assume that your Go workspace is at `~/go`
!!! note
**Attention minikube users**: By default, minikube will create Kubernetes client configuration that uses authentication data from files. This is incompatible with the virtualized toolchain. So if you intend to use the virtualized toolchain, you have to embed this authentication data into the client configuration. To do so, issue `minikube config set embed-certs true` and restart your minikube. Please also note that minikube using the Docker driver is currently not supported with the virtualized toolchain, because the Docker driver exposes the API server on 127.0.0.1 hard-coded. If in doubt, run `make verify-kube-connect` to find out.
## Submitting PRs
When you submit a PR against ArgoCD's GitHub repository, a couple of CI checks will be run automatically to ensure your changes will build fine and meet certain quality standards. Your contribution needs to pass those checks in order to be merged into the repository.
In general, it might be beneficial to only submit a PR for an existing issue. Especially for larger changes, an Enhancement Proposal should exist before.
!!!note
Please make sure that you always create PRs from a branch that is up-to-date with the latest changes from ArgoCD's master branch. Depending on how long it takes for the maintainers to review and merge your PR, it might be necessary to pull in latest changes into your branch again.
Please understand that we, as an Open Source project, have limited capacities for reviewing and merging PRs to ArgoCD. We will do our best to review your PR and give you feedback as soon as possible, but please bear with us if it takes a little longer as expected.
The following read will help you to submit a PR that meets the standards of our CI tests:
### Title of the PR
Please use a meaningful and concise title for your PR. This will help us to pick PRs for review quickly, and the PR title will also end up in the Changelog.
We use the [Semantic PR title checker](https://github.com/zeke/semantic-pull-requests) to categorize your PR into one of the following categories:
* `fix` - Your PR contains one or more code bug fixes
* `feat` - Your PR contains a new feature
* `docs` - Your PR improves the documentation
* `chore` - Your PR improves any internals of ArgoCD, such as the build process, unit tests, etc
Please prefix the title of your PR with one of the valid categories. For example, if you chose the title your PR `Add documentation for GitHub SSO integration`, please use `docs: Add documentation for GitHub SSO integration` instead.
### Contributor License Agreement
Every contributor to ArgoCD must have signed the current Contributor License Agreement (CLA). You only have to sign the CLA when you are a first time contributor, or when the agreement has changed since your last time signing it. The main purpose of the CLA is to ensure that you hold the required rights for your contribution. The CLA signing is an automated process.
You can read the current version of the CLA [here](https://cla-assistant.io/argoproj/argo-cd).
### PR template checklist
Upon opening a PR, the details will contain a checklist from a template. Please read the checklist, and tick those marks that apply to you.
### Automated builds & tests
After you have submitted your PR, and whenever you push new commits to that branch, GitHub will run a number of Continuous Integration checks against your code. It will execute the following actions, and each of them has to pass:
* Build the Go code (`make build`)
* Generate API glue code and manifests (`make codegen`)
* Run a Go linter on the code (`make lint`)
* Run the unit tests (`make test`)
* Run the End-to-End tests (`make test-e2e`)
* Build and lint the UI code (`make ui`)
* Build the `argocd` CLI (`make cli`)
If any of these tests in the CI pipeline fail, it means that some of your contribution is considered faulty (or a test might be flaky, see below).
### Code test coverage
We use [CodeCov](https://codecov.io) in our CI pipeline to check for test coverage, and once you submit your PR, it will run and report on the coverage difference as a comment within your PR. If the difference is too high in the negative, i.e. your submission introduced a significant drop in code coverage, the CI check will fail.
Whenever you develop a new feature or submit a bug fix, please also write appropriate unit tests for it. If you write a completely new module, please aim for at least 80% of coverage.
If you want to see how much coverage just a specific module (i.e. your new one) has, you can set the `TEST_MODULE` to the (fully qualified) name of that module with `make test`, i.e.
```bash
make test TEST_MODULE=github.com/argoproj/argo-cd/server/cache
...
ok github.com/argoproj/argo-cd/server/cache 0.029s coverage: 89.3% of statements
```
## Local vs Virtualized toolchain
ArgoCD provides a fully virtualized development and testing toolchain using Docker images. It is recommended to use those images, as they provide the same runtime environment as the final product and it is much easier to keep up-to-date with changes to the toolchain and dependencies. But as using Docker comes with a slight performance penalty, you might want to setup a local toolchain.
Most relevant targets for the build & test cycles in the `Makefile` provide two variants, one of them suffixed with `-local`. For example, `make test` will run unit tests in the Docker container, `make test-local` will run it natively on your local system.
If you are going to use the virtualized toolchain, please bear in mind the following things:
* Your Kubernetes API server must listen on the interface of your local machine or VM, and not on `127.0.0.1` only.
* Your Kubernetes client configuration (`~/.kube/config`) must not use an API URL that points to `localhost` or `127.0.0.1`.
You can test whether the virtualized toolchain has access to your Kubernetes cluster by running `make verify-kube-connect` (*after* you have setup your development environment, as described below), which will run `kubectl version` inside the Docker container used for running all tests.
The Docker container for the virtualized toolchain will use the following local mounts from your workstation, and possibly modify its contents:
* `~/go/src` - Your Go workspace's source directory (modifications expected)
* `~/.cache/go-build` - Your Go build cache (modifications expected)
* `~/.kube` - Your Kubernetes client configuration (no modifications)
* `/tmp` - Your system's temp directory (modifications expected)
## Setting up your development environment
The following steps are required no matter whether you chose to use a virtualized or a local toolchain.
### Clone the ArgoCD repository from your personal fork on GitHub
* `mkdir -p ~/go/src/github.com/argoproj`
* `cd ~/go/src/github.com/argoproj`
* `git clone https://github.com/yourghuser/argo-cd`
* `cd argo-cd`
### Optional: Setup an additional Git remote
While everyone has their own Git workflow, the author of this document recommends to create a remote called `upstream` in your local copy pointing to the original ArgoCD repository. This way, you can easily keep your local branches up-to-date by merging in latest changes from the ArgoCD repository, i.e. by doing a `git pull upstream master` in your locally checked out branch. To create the remote, run `git remote add upstream https://github.com/argoproj/argo-cd`
### Install the must-have requirements
Make sure you fulfill the pre-requisites above and run some preliminary tests. Neither of them should report an error.
* Run `kubectl version`
* Run `docker version`
* Run `go version`
### Build (or pull) the required Docker image
Build the required Docker image by running `make test-tools-image` or pull the latest version by issuing `docker pull argoproj/argocd-test-tools`.
The `Dockerfile` used to build these images can be found at `test/container/Dockerfile`.
### Test connection from build container to your K8s cluster
Run `make verify-kube-connect`, it should execute without error.
If you receive an error similar to the following:
```
The connection to the server 127.0.0.1:6443 was refused - did you specify the right host or port?
make: *** [Makefile:386: verify-kube-connect] Error 1
```
you should edit your `~/.kube/config` and modify the `server` option to point to your correct K8s API (as described above).
## The development cycle
When you have developed and possibly manually tested the code you want to contribute, you should ensure that everything will build correctly. Commit your changes to the local copy of your Git branch and perform the following steps:
### Pull in all build dependencies
As build dependencies change over time, you have to synchronize your development environment with the current specification. In order to pull in all required dependencies, issue:
* `make dep-ui`
ArgoCD recently migrated to Go modules. Usually, dependencies will be downloaded on build time, but the Makefile provides two targets to download and vendor all dependencies:
* `make mod-download` will download all required Go modules and
* `make mod-vendor` will vendor those dependencies into the ArgoCD source tree
### Generate API glue code and other assets
ArgoCD relies on Google's [Protocol Buffers](https://developers.google.com/protocol-buffers) for its API, and this makes heavy use of auto-generated glue code and stubs. Whenever you touched parts of the API code, you must re-generate the auto generated code.
* Run `make codegen`, this might take a while
* Check if something has changed by running `git status` or `git diff`
* Commit any possible changes to your local Git branch, an appropriate commit message would be `Changes from codegen`, for example.
!!!note
There are a few non-obvious assets that are auto-generated. You should not change the autogenerated assets, as they will be overwritten by a subsequent run of `make codegen`. Instead, change their source files. Prominent examples of non-obvious auto-generated code are `swagger.json` or the installation manifest YAMLs.
### Build your code and run unit tests
After the code glue has been generated, your code should build and the unit tests should run without any errors. Execute the following statements:
* `make build`
* `make test`
These steps are non-modifying, so there's no need to check for changes afterwards.
### Lint your code base
In order to keep a consistent code style in our source tree, your code must be well-formed in accordance to some widely accepted rules, which are applied by a Linter.
The Linter might make some automatic changes to your code, such as indentation fixes. Some other errors reported by the Linter have to be fixed manually.
* Run `make lint` and observe any errors reported by the Linter
* Fix any of the errors reported and commit to your local branch
* Finally, after the Linter reports no errors anymore, run `git status` or `git diff` to check for any changes made automatically by Lint
* If there were automatic changes, commit them to your local branch
If you touched UI code, you should also run the Yarn linter on it:
* Run `make lint-ui`
* Fix any of the errors reported by it
## Setting up a local toolchain
For development, you can either use the fully virtualized toolchain provided as Docker images, or you can set up the toolchain on your local development machine. Due to the dynamic nature of requirements, you might want to stay with the virtualized environment.
### Install required dependencies and build-tools
!!!note
The installations instructions are valid for Linux hosts only. Mac instructions will follow shortly.
For installing the tools required to build and test ArgoCD on your local system, we provide convenient installer scripts. By default, they will install binaries to `/usr/local/bin` on your system, which might require `root` privileges.
You can change the target location by setting the `BIN` environment before running the installer scripts. For example, you can install the binaries into `~/go/bin` (which should then be the first component in your `PATH` environment, i.e. `export PATH=~/go/bin:$PATH`):
```shell
make BIN=~/go/bin install-tools-local
```
Additionally, you have to install at least the following tools via your OS's package manager (this list might not be always up-to-date):
* Git LFS plugin
* GnuPG version 2
### Install Go dependencies
You need to pull in all required Go dependencies. To do so, run
* `make mod-download-local`
* `make mod-vendor-local`
### Test your build toolchain
The first thing you can do whether your build toolchain is setup correctly is by generating the glue code for the API and after that, run a normal build:
* `make codegen-local`
* `make build-local`
This should return without any error.
### Run unit-tests
The next thing is to make sure that unit tests are running correctly on your system. These will require that all dependencies, such as Helm, Kustomize, Git, GnuPG, etc are correctly installed and fully functioning:
* `make test-local`
### Run end-to-end tests
The final step is running the End-to-End testsuite, which makes sure that your Kubernetes dependencies are working properly. This will involve starting all of the ArgoCD components locally on your computer. The end-to-end tests consists of two parts: a server component, and a client component.
* First, start the End-to-End server: `make start-e2e-local`. This will spawn a number of processes and services on your system.
* When all components have started, run `make test-e2e-local` to run the end-to-end tests against your local services.
For more information about End-to-End tests, refer to the [End-to-End test documentation](test-e2e.md).

View File

@@ -1,65 +0,0 @@
# Contribution FAQ
## General
### Can I discuss my contribution ideas somewhere?
Sure thing! You can either open an Enhancement Proposal in our GitHub issue tracker or you can [join us on Slack](https://argoproj.github.io/community/join-slack) in channel #argo-dev to discuss your ideas and get guidance for submitting a PR.
### Noone has looked at my PR yet. Why?
As we have limited man power, it can sometimes take a while for someone to respond to your PR. Especially, when your PR contains complex or non-obvious changes. Please bear with us, we try to look at every PR that we receive.
### Why has my PR been declined? I put much work in it!
We appreciate that you have put your valuable time and know how into a contribution. Alas, some changes do not fit into the overall ArgoCD philosophy, and therefore can't be merged into the official ArgoCD source tree.
To be on the safe side, make sure that you have created an Enhancement Proposal for your change before starting to work on your PR and have gathered enough feedback from the community and the maintainers.
## Failing CI checks
### One of the CI checks failed. Why?
You can click on the "Details" link next to the failed step to get more details about the failure. This will take you to CircleCI website.
![CircleCI pipeline](ci-pipeline-failed.png)
### Can I retrigger the checks without pushing a new commit?
Since the CI pipeline is triggered on Git commits, there is currently no (known) way on how to retrigger the CI checks without pushing a new commit to your branch.
If you are absolutely sure that the failure was due to a failure in the pipeline, and not an error within the changes you commited, you can push an empty commit to your branch, thus retriggering the pipeline without any code changes. To do so, issue
```bash
git commit --allow-empty -m "Retrigger CI pipeline"
git push origin <yourbranch>
```
### Why does the build step fail?
Chances are that it fails for two of the following reasons in the CI while running fine on your machine:
* Sometimes, CircleCI kills the build step due to excessive memory usage. This happens rarely, but it has happened in the past. If you see a message like "killed" in the log output of CircleCI, you should retrigger the pipeline as described above. If the issue persists, please let us know.
* If the build is failing at the `Ensuring Gopkg.lock is up-to-date` step, you need to update the dependencies before you push your commits. Run `make dep-ensure` and `make dep` and commit the changes to `Gopkg.lock` to your branch.
### Why does the codegen step fail?
If the codegen step fails with "Check nothing has changed...", chances are high that you did not run `make codegen`, or did not commit the changes it made. You should double check by running `make codegen` followed by `git status` in the local working copy of your branch. Commit any changes and push them to your GH branch to have the CI check it again.
A second common case for this is, when you modified any of the auto generated assets, as these will be overwritten upon `make codegen`.
Generally, this step runs `codegen` and compares the outcome against the Git branch it has checked out. If there are differences, the step will fail.
### Why does the lint step fail?
The lint step is most likely to fail for two reasons:
* The `golangci-lint` process was OOM killed by CircleCI. This happens sometimes, and is annoying. This is indicated by a `Killed.` message in the CircleCI output.
If this is the case, please re-trigger the CI process as described above and see if it runs through.
* Your code failed to lint correctly, or modifications were performed by the `golangci-lint` process. You should run `make lint` on your local branch and fix all the issues.
### Why does the test or e2e steps fail?
You should check for the cause of the failure on the CircleCI web site, as described above. This will give you the name of the test that has failed, and details about why. If your test are passing locally (using the virtualized toolchain), chances are that the test might be flaky and will pass the next time it is run. Please retrigger the CI pipeline as described above and see if the test step now passes.

View File

@@ -1,155 +1,5 @@
# Releasing
## Automated release procedure
Starting from `release-1.6` branch, ArgoCD can be released in automatic fashion
using GitHub actions. The release process takes about 20 minutes, sometimes a
little less, depending on the performance of GitHub actions runners.
The target release branch must already exist in GitHub repository. If you for
example want to create a release `v1.7.0`, the corresponding release branch
`release-1.7` needs to exist, otherwise the release cannot be build. Also,
the trigger tag should always be created in the release branch, checked out
in your local repository clone.
Before triggering the release automation, the `CHANGELOG.md` should be updated
with the latest information, and this change should be commited and pushed to
the GitHub repository to the release branch. Afterwards, the automation can be
triggered.
**Manual steps before release creation:**
* Update `CHANGELOG.md` with changes for this release
* Commit & push changes to `CHANGELOG.md`
* Prepare release notes (save to some file, or copy from Changelog)
**The automation will perform the following steps:**
* Update `VERSION` file in release branch
* Update manifests with image tags of new version in release branch
* Build the Docker image and push to Docker Hub
* Create release tag in the GitHub repository
* Create GitHub release and attach the required assets to it (CLI binaries, ...)
Finally, it will the remove trigger tag from repository again.
Automation supports both, GA and pre-releases. The automation is triggered by
pushing a tag to the repository. The tag must be in one of the following formats
to trigger the GH workflow:
* GA: `release-v<MAJOR>.<MINOR>.<PATCH>`
* Pre-release: `release-v<MAJOR>.<MINOR>.<PATCH>-rc<RC#>`
The tag must be an annotated tag, and it must contain the release notes in the
commit message. Please note that Markdown uses `#` character for formatting, but
Git uses it as comment char. To solve this, temporarily switch Git comment char
to something else, the `;` character is recommended.
For example, considering you have configured the Git remote for repository to
`github.com/argoproj/argo-cd` to be named `upstream` and are in your locally
checked out repo:
```shell
git config core.commentChar ';'
git tag -a -F /path/to/release-notes.txt release-v1.6.0-rc2
git push upstream release-v1.6.0-rc2
git tag -d release-v1.6.0-rc2
git config core.commentChar '#'
```
For convenience, there is a shell script in the tree that ensures all the
pre-requisites are met and that the trigger is well-formed before pushing
it to the GitHub repo.
In summary, the modifications it does are:
* Create annotated trigger tag in your local repository
* Push tag to GitHub repository to trigger workflow
* Remove trigger tag from your local repository
The script can be found at `hacks/trigger-release.sh` and is used as follows:
```shell
./hacks/trigger-release.sh <version> <remote name> [<release notes path>]
```
The `<version>` identifier needs to be specified **without** the `release-`
prefix, so just specify it as `v1.6.0-rc2` for example. The `<remote name>`
specifies the name of the remote used to push to the GitHub repository.
If you omit the `<release notes path>`, an editor will pop-up asking you to
enter the tag's annotation so you can paste the release notes, save and exit.
It will also take care of temporarily configuring the `core.commentChar` and
setting it back to its original state.
!!!note
It is strongly recommended to use this script to trigger the workflow
instead of manually pushing a tag to the repository.
Once the trigger tag is pushed to the repo, the GitHub workflow will start
execution. You can follow its progress under `Actions` tab, the name of the
action is `Create release`. Don't get confused by the name of the running
workflow, it will be the commit message of the latest commit to `master`
branch, this is a limitation of GH actions.
The workflow performs necessary checks so that the release can be sucessfully
build before the build actually starts. It will error when one of the
prerequisites is not met, or if the release cannot be build (i.e. already
exists, release notes invalid, etc etc). You can see a summary of what has
failed in the job's overview page, and more detailed errors in the output
of the step that has failed.
!!!note
You cannot perform more than one release on the same release branch at the
same time. For example, both `v1.6.0` and `v1.6.1` would operate on the
`release-1.6` branch. If you submit `v1.6.1` while `v1.6.0` is still
executing, the release automation will not execute. You have to either
cancel `v1.6.0` before submitting `v1.6.1` or wait until it has finished.
You can execute releases on different release branches simultaneously, for
example `v1.6.0` and `v1.7.0-rc1`, without problems.
### Verifying automated release
After the automatic release creation has finished, you should perform manual
checks to see if the release came out correctly:
* Check status & output of the GitHub action
* Check [https://github.com/argoproj/argo-cd/releases](https://github.com/argoproj/argo-cd/releases)
to see if release has been correctly created, and if all required assets
are attached.
* Check whether the image has been published on DockerHub correctly
### If something went wrong
If something went wrong, damage should be limited. Depending on the steps that
have been performed, you will need to manually clean up.
* Delete release tag (i.e. `v1.6.0-rc2`) created on GitHub repository. This
will immediately set release (if created) to `draft` status, invisible for
general public.
* Delete the draft release (if created) from `Releases` page on GitHub
* If Docker image has been pushed to DockerHub, delete it
* If commits have been performed to the release branch, revert them. Paths that could have been commited to are:
* `VERSION`
* `manifests/*`
### Post-process manual steps
For now, the only manual steps left are to
* update brew formulae for ArgoCD CLI on Mac if release is GA
* update stable tag in GitHub repository to point to new release (if appropriate)
These will be automated as well in the future.
## Manual releasing
Automatic release process does not interfere with manual release process, since
the trigger tag does not match a normal release tag. If you prefer to perform,
manual release or if automatic release is for some reason broken, these are the
steps:
Make sure you are logged into Docker Hub:
```bash
@@ -192,14 +42,18 @@ git push $REPO $BRANCH
git push $REPO $VERSION
```
If GA, update `stable` tag:
```bash
git tag stable --force && git push $REPO stable --force
```
Update [Github releases](https://github.com/argoproj/argo-cd/releases) with:
* Getting started (copy from previous release)
* Changelog
* Binaries (e.g. `dist/argocd-darwin-amd64`).
## Update brew formulae (manual)
If GA, update Brew formula:
```bash
@@ -210,15 +64,7 @@ git commit -am "Update argocd to $VERSION"
git push
```
## Update stable tag (manual)
If GA, update `stable` tag:
```bash
git tag stable --force && git push $REPO stable --force
```
## Verify release
### Verify
Locally:

View File

@@ -1,108 +0,0 @@
# Running ArgoCD locally
## Run ArgoCD outside of Kubernetes
During development, it might be viable to run ArgoCD outside of a Kubernetes cluster. This will greatly speed up development, as you don't have to constantly build, push and install new ArgoCD Docker images with your latest changes.
You will still need a working Kubernetes cluster, as described in the [Contribution Guide](contributing.md), where ArgoCD will store all of its resources.
If you followed the [Contribution Guide](contributing.md) in setting up your toolchain, you can run ArgoCD locally with these simple steps:
### Scale down any ArgoCD instance in your cluster
First make sure that ArgoCD is not running in your development cluster by scaling down the deployments:
```shell
kubectl -n argocd scale deployment/argocd-application-controller --replicas 0
kubectl -n argocd scale deployment/argocd-dex-server --replicas 0
kubectl -n argocd scale deployment/argocd-repo-server --replicas 0
kubectl -n argocd scale deployment/argocd-server --replicas 0
kubectl -n argocd scale deployment/argocd-redis --replicas 0
```
### Start local services
When you use the virtualized toolchain, starting local services is as simple as running
```bash
make start
```
This will start all ArgoCD services and the UI in a Docker container and expose the following ports to your host:
* The ArgoCD API server on port 8080
* The ArgoCD UI server on port 4000
You can now use either the web UI by pointing your browser to `http://localhost:4000` or use the CLI against the API at `http://localhost:8080`. Be sure to use the `--insecure` and `--plaintext` options to the CLI.
As an alternative to using the above command line parameters each time you call `argocd` CLI, you can set the following environment variables:
```bash
export ARGOCD_SERVER=127.0.0.1:8080
export ARGOCD_OPTS="--plaintext --insecure"
```
### Scale up ArgoCD in your cluster
Once you have finished testing your changes locally and want to bring back ArgoCD in your development cluster, simply scale the deployments up again:
```bash
kubectl -n argocd scale deployment/argocd-application-controller --replicas 1
kubectl -n argocd scale deployment/argocd-dex-server --replicas 1
kubectl -n argocd scale deployment/argocd-repo-server --replicas 1
kubectl -n argocd scale deployment/argocd-server --replicas 1
kubectl -n argocd scale deployment/argocd-redis --replicas 1
```
## Run your own ArgoCD images on your cluster
For your final tests, it might be necessary to build your own images and run them in your development cluster.
### Create Docker account and login
You might need to create a account on [Docker Hub](https://hub.docker.com) if you don't have one already. Once you created your account, login from your development environment:
```bash
docker login
```
### Create and push Docker images
You will need to push the built images to your own Docker namespace:
```bash
export IMAGE_NAMESPACE=youraccount
```
If you don't set `IMAGE_TAG` in your environment, the default of `:latest` will be used. To change the tag, export the variable in the environment:
```bash
export IMAGE_TAG=1.5.0-myrc
```
Then you can build & push the image in one step:
```bash
DOCKER_PUSH=true make image
```
### Configure manifests for your image
With `IMAGE_NAMESPACE` and `IMAGE_TAG` still set, run
```bash
make manifests
```
to build a new set of installation manifests which include your specific image reference.
!!!note
Do not commit these manifests to your repository. If you want to revert the changes, the easiest way is to unset `IMAGE_NAMESPACE` and `IMAGE_TAG` from your environment and run `make manifests` again. This will re-create the default manifests.
### Configure your cluster with custom manifests
The final step is to push the manifests to your cluster, so it will pull and run your image:
```bash
kubectl -n argocd --force -f manifests/install.yaml
```

View File

@@ -1,9 +1,5 @@
# E2E Tests
!!!warning
This documentation is out-of-date. Please bear with us while we work to
update the documentation to reflect reality!
The directory contains E2E tests and test applications. The test assume that Argo CD services are installed into `argocd-e2e` namespace or cluster in current context. One throw-away
namespace `argocd-e2e***` is created prior to tests execute. The throw-away namespace is used as a target namespace for test applications.

View File

@@ -47,10 +47,6 @@ kubectl -n argocd patch secret argocd-secret \
Another option is to delete both the `admin.password` and `admin.passwordMtime` keys and restart argocd-server. This will set the password back to the pod name as per [the getting started guide](getting_started.md).
## How to disable admin user?
Add `admin.enabled: "false"` to the `argocd-cm` ConfigMap (see [user management](operator-manual/user-management/index.md)).
## Argo CD cannot deploy Helm Chart based applications without internet access, how can I solve it?
Argo CD might fail to generate Helm chart manifests if the chart has dependencies located in external repositories. To solve the problem you need to make sure that `requirements.yaml`
@@ -115,9 +111,11 @@ E.g.
* `'3072Mi'` normalized to `'3Gi'`
* `3072` normalized to `'3072'` (quotes added)
To fix this use diffing customizations [settings](./user-guide/diffing.md#known-kubernetes-types-in-crds-resource-limits-volume-mounts-etc).
To fix this - replace your values with the normalized values.
## How Do I Fix "invalid cookie, longer than max length 4093"?
See [#1615](https://github.com/argoproj/argo-cd/issues/1615)
# How Do I Fix "invalid cookie, longer than max length 4093"?
Argo CD uses a JWT as the auth token. You likely are part of many groups and have gone over the 4KB limit which is set for cookies.
You can get the list of groups by opening "developer tools -> network"
@@ -148,8 +146,3 @@ argocd ... --insecure
```
!!! warning "Do not use `--insecure` in production"
## I have configured Dex via `dex.config` in `argocd-cm`, it still says Dex is unconfigured. Why?
Most likely you forgot to set the `url` in `argocd-cm` to point to your ArgoCD as well. See also
[the docs](/operator-manual/user-management/#2-configure-argo-cd-for-sso)

View File

@@ -20,7 +20,7 @@ This will create a new namespace, `argocd`, where Argo CD services and applicati
On GKE, you will need grant your account the ability to create new cluster roles:
```bash
kubectl create clusterrolebinding cluster-admin-binding --clusterrole=cluster-admin --user="$(gcloud config get-value account)"
kubectl create clusterrolebinding YOURNAME-cluster-admin-binding --clusterrole=cluster-admin --user=YOUREMAIL@gmail.com
```
!!! note

View File

@@ -24,9 +24,8 @@ kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
```
Follow our [getting started guide](getting_started.md). Further user oriented [documentation](user_guide/)
is provided for additional features. If you are looking to upgrade ArgoCD, see the [upgrade guide](./operator-manual/upgrading/overview.md).
Developer oriented [documentation](developer-guide/) is available for people interested in building third-party integrations.
Follow our [getting started guide](getting_started.md). Further [documentation](docs/)
is provided for additional features.
## How it works

View File

@@ -27,11 +27,9 @@ spec:
# Release name override (defaults to application name)
releaseName: guestbook
# Helm values files for overriding values in the helm chart
# The path is relative to the spec.source.path directory defined above
valueFiles:
- values-prod.yaml
# Values file as block file
values: |
ingress:
@@ -50,8 +48,6 @@ spec:
# kustomize specific config
kustomize:
# Optional kustomize version. Note: version must be configured in argocd-cm ConfigMap
version: v3.5.4
# Optional image name prefix
namePrefix: prod-
# Optional images passed to "kustomize edit set image".
@@ -94,8 +90,6 @@ spec:
automated:
prune: true # Specifies if resources should be pruned during auto-syncing ( false by default ).
selfHeal: true # Specifies if partial app sync should be executed when resources are changed only in target Kubernetes cluster and no git change detected ( false by default ).
syncOptions: # Sync options which modifies sync behavior
- Validate=false # disables resource validation (equivalent to 'kubectl apply --validate=true')
# Ignore differences at the specified json pointers
ignoreDifferences:

View File

@@ -100,7 +100,7 @@ data:
# List of json pointers in the object to ignore differences
ignoreDifferences: |
jsonPointers:
- /webhooks/0/clientConfig/caBundle
- webhooks/0/clientConfig/caBundle
certmanager.k8s.io/Certificate:
# Lua script for customizing the health status assessment
health.lua: |
@@ -183,16 +183,6 @@ data:
clusters:
- "*.local"
# By default all resource group/kinds are included. The resource.inclusions setting allows customizing
# list of included group/kinds.
resource.inclusions: |
- apiGroups:
- repositories.stash.appscode.com
kinds:
- Snapshot
clusters:
- "*.local"
# Configuration to add a config management plugin.
configManagementPlugins: |
- name: kasane
@@ -204,20 +194,7 @@ data:
# Build options/parameters to use with `kustomize build` (optional)
kustomize.buildOptions: --load_restrictor none
# Additional Kustomize versions and corresponding binary paths
kustomize.version.v3.5.1: /custom-tools/kustomize_3_5_1
kustomize.version.v3.5.4: /custom-tools/kustomize_3_5_4
# 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'
application.instanceLabelKey: mycompany.com/appname
# disables admin user. Admin is enabled by default
admin.enabled: "false"
# add an additional local user with apiKey and login capabilities
# apiKey - allows generating API keys
# login - allows to login using UI
accounts.alice: apiKey, login
# disables user. User is enabled by default
accounts.alice.enabled: "false"

View File

@@ -9,7 +9,7 @@ metadata:
type: Opaque
data:
# TLS certificate and private key for API server (required).
# Autogenerated with a self-signed certificate when keys are missing or invalid.
# Autogenerated with a self-signed ceritificate when keys are missing or invalid.
tls.crt:
tls.key:
@@ -34,10 +34,3 @@ data:
webhook.bitbucketserver.secret: shhhh! it's a bitbucket server secret
# gogs server webhook secret
webhook.gogs.secret: shhhh! it's a gogs server secret
# an additional user password and its last modified time (see user definition in argocd-cm.yaml)
accounts.alice.password:
accounts.alice.passwordMtime:
# list of generated account tokens/api keys
accounts.alice.tokens: |
[{"id":"123","iat":1583789194,"exp":1583789194}]

Some files were not shown because too many files have changed in this diff Show More