Compare commits

..

44 Commits

Author SHA1 Message Date
Alex Collins
e0bd546a07 Update manifests to v1.0.2 2019-06-14 09:46:05 -07:00
Alex Collins
984829fcc8 Merge branch 'release-1.0' of github.com:argoproj/argo-cd into release-1.0 2019-06-14 09:38:08 -07:00
Jesse Suen
c48e27f265 Cluster registration was unintentionally persisting client-cert auth credentials (#1742)
Remove unused CreateClusterFromKubeConfig server method
2019-06-14 04:03:01 -07:00
Alex Collins
5fe1447b72 Update manifests to v1.0.1 2019-05-28 10:08:34 -07:00
Alex Collins
539516bd43 Update manifests to v1.0.1 2019-05-28 10:07:32 -07:00
Alex Collins
8a57d544ff Update manifests to v1.0.1 2019-05-28 09:01:21 -07:00
Alex Collins
cd77e2a048 Update manifests to v1.0.1 2019-05-28 08:58:01 -07:00
Alex Collins
a52f766815 removes file which cannot be compiled 2019-05-24 15:03:49 -07:00
Alex Collins
646fd37e16 Public git creds (#1633) 2019-05-24 15:01:03 -07:00
Alexander Matyushentsev
c74ca22023 Update manifests to v1.0.0 2019-05-16 13:00:40 -07:00
Alexander Matyushentsev
2d170be242 Issue #1471 - Support configuring requested OIDC provider scopes and enforced RBAC scopes (#1585)
* Issue #1471 - Support configuring requested OIDC provider scopes and enforced RBAC scopes

* Apply reviewer notes
2019-05-16 07:35:44 -07:00
Alexander Matyushentsev
079101522d Issue #1533 - Prevent reconciliation loop for self-managed apps (#1608) 2019-05-14 08:05:28 -07:00
Jesse Suen
1bea98e01b Supply resourceVersion to watch request to prevent reading of stale cache (#1612) 2019-05-13 15:01:15 -07:00
Alexander Matyushentsev
7c09221f7c Update manifests to v1.0.0-rc3 2019-05-09 09:52:56 -07:00
Alexander Matyushentsev
6355e910d4 Fix flaky TestGetIngressInfo unit test (#1529) 2019-05-09 09:52:16 -07:00
Alexander Matyushentsev
891e0320d7 Issue #1586 - Ignore patch errors during diffing normalization (#1599) 2019-05-09 09:30:48 -07:00
Alexander Matyushentsev
486323ae58 Issue #1596 - SSH URLs support is partially broken (#1597) 2019-05-09 09:30:42 -07:00
Alexander Matyushentsev
4ef875aa0b Issue #1552 - Improve rendering app image information (#1584) 2019-05-09 09:30:31 -07:00
Alexander Matyushentsev
e756b8db7a Fix ingress browsable url formatting if port is not string (#1576) 2019-05-09 09:28:49 -07:00
Alexander Matyushentsev
8023f8ac8d Issue #1579 - Impossible to sync to HEAD from UI if auto-sync is enabled (#1580) 2019-05-09 09:28:42 -07:00
Alexander Matyushentsev
803408904a Issue #1570 - Application controller is unable to delete self-referenced app (#1574) 2019-05-09 09:28:35 -07:00
Alexander Matyushentsev
702f9095da Issue #1546 - Add liveness probe to repo server/api servers (#1560) 2019-05-09 09:28:30 -07:00
Alexander Matyushentsev
0b9ee1ae6d ISsue #1557 - Controller incorrectly report health state of self managed application (#1558) 2019-05-09 09:28:19 -07:00
Alexander Matyushentsev
2f003e08ff Issue #1540 - Fix kustomize manifest generation crash is manifest has image without version (#1559) 2019-05-09 09:28:14 -07:00
Paul Brit
e090857d6b Fix hardcoded 'git' user in util/git.NewClient (#1556)
Closes #1555
2019-05-09 09:28:09 -07:00
dthomson25
4e29fff5a3 Improve Rollout health.lua (#1554) 2019-05-09 09:28:03 -07:00
Alexander Matyushentsev
e279377696 Fix invalid URL for ingress without hostname (#1553) 2019-05-01 15:38:40 -07:00
Alexander Matyushentsev
5e52839ce3 Issue #1533 - Prevent reconciliation loop for self-managed apps (#1547) 2019-05-01 10:22:09 -07:00
Alexander Matyushentsev
3ca632a552 Update manifests to v1.0.0-rc2 2019-04-30 13:19:50 -07:00
Alexander Matyushentsev
cfe55357ac Rollout health checks/actions should support v0.2 and v0.2+ versions (#1543) 2019-04-30 13:18:15 -07:00
Alex Collins
0f6d768eca Fixes bug in normalizer (#1542) 2019-04-30 11:32:54 -07:00
Alexander Matyushentsev
75330da328 Ingress resource might get invalid ExternalURL (#1522) (#1523) 2019-04-30 11:14:18 -07:00
Alexander Matyushentsev
a1bcbab0e5 Issue 1476 - Avoid validating repository in application controller (#1535) 2019-04-30 11:10:06 -07:00
Alexander Matyushentsev
db9272032a Issue #1414 - Load target resource using K8S if conversion fails (#1527) 2019-04-30 11:10:02 -07:00
Alexander Matyushentsev
d6d6c655ff Issue #1476 - Add repo server grpc call timeout (#1528) 2019-04-30 11:09:58 -07:00
Alex Collins
58acc92790 Adds support for configuring repo creds at a domain/org level. Closes… (#1496) 2019-04-30 11:09:53 -07:00
Simon Behar
c3074c0977 Whitelisting of resources (#1509)
* Added whitelisting of resources
2019-04-30 11:09:26 -07:00
Simon Behar
af254f3047 Added ability to sync specific labels from the command line (#1501)
* Finished initial implementation

* Added tests and fix a few bugs
2019-04-30 11:09:16 -07:00
Alex Collins
c140976eeb Updates Makefile 2019-04-24 10:52:53 -07:00
Alex Collins
05c22d4ddc Updates VERSION 2019-04-24 10:51:50 -07:00
Alex Collins
3e08938a20 Updates VERSION 2019-04-24 10:49:30 -07:00
Alex Collins
5937bb574d Update manifests to v1.0.0-rc1 2019-04-24 10:48:24 -07:00
Alex Collins
ded55b26d1 Update manifests to v1.0.0-rc1 2019-04-24 10:46:23 -07:00
Alex Collins
d79ed65de0 Updated CHANGELOG.md 2019-04-24 10:35:04 -07:00
1188 changed files with 37316 additions and 144450 deletions

163
.argo-ci/ci.yaml Normal file
View File

@@ -0,0 +1,163 @@
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
generateName: argo-cd-ci-
spec:
entrypoint: argo-cd-ci
arguments:
parameters:
- name: revision
value: master
- name: repo
value: https://github.com/argoproj/argo-cd.git
volumes:
- name: k3setc
emptyDir: {}
- name: k3svar
emptyDir: {}
- name: tmp
emptyDir: {}
templates:
- name: argo-cd-ci
steps:
- - name: build-e2e
template: build-e2e
- name: test
template: ci-builder
arguments:
parameters:
- name: cmd
value: "dep ensure && make lint test && bash <(curl -s https://codecov.io/bash) -f coverage.out"
# The step builds argo cd image, deploy argo cd components into throw-away kubernetes cluster provisioned using k3s and run e2e tests against it.
- name: build-e2e
inputs:
artifacts:
- name: code
path: /go/src/github.com/argoproj/argo-cd
git:
repo: "{{workflow.parameters.repo}}"
revision: "{{workflow.parameters.revision}}"
container:
image: argoproj/argo-cd-ci-builder:v0.13.1
imagePullPolicy: Always
command: [sh, -c]
# Main contains build argocd image. The image is saved it into k3s agent images directory so it could be preloaded by the k3s cluster.
args: ["
dep ensure && until docker ps; do sleep 3; done && \
make image DEV_IMAGE=true && mkdir -p /var/lib/rancher/k3s/agent/images && \
docker save argocd:latest > /var/lib/rancher/k3s/agent/images/argocd.tar && \
touch /var/lib/rancher/k3s/ready && until ls /etc/rancher/k3s/k3s.yaml; do sleep 3; done && \
kubectl create ns argocd-e2e && kustomize build ./test/manifests/ci | kubectl apply -n argocd-e2e -f - && \
kubectl rollout status deployment -n argocd-e2e argocd-application-controller && kubectl rollout status deployment -n argocd-e2e argocd-server && \
git config --global user.email \"test@example.com\" && \
export ARGOCD_SERVER=$(kubectl get service argocd-server -o=jsonpath={.spec.clusterIP} -n argocd-e2e):443 && make test-e2e"
]
workingDir: /go/src/github.com/argoproj/argo-cd
env:
- name: USER
value: argocd
- name: DOCKER_HOST
value: 127.0.0.1
- name: DOCKER_BUILDKIT
value: "1"
- name: KUBECONFIG
value: /etc/rancher/k3s/k3s.yaml
volumeMounts:
- name: tmp
mountPath: /tmp
- name: k3setc
mountPath: /etc/rancher/k3s
- name: k3svar
mountPath: /var/lib/rancher/k3s
sidecars:
- name: dind
image: docker:18.09-dind
securityContext:
privileged: true
resources:
requests:
memory: 2048Mi
cpu: 500m
mirrorVolumeMounts: true
# Steps waits for file /var/lib/rancher/k3s/ready which indicates that all required images are ready, then starts the cluster.
- name: k3s
image: rancher/k3s:v0.3.0-rc1
imagePullPolicy: Always
command: [sh, -c]
args: ["until ls /var/lib/rancher/k3s/ready; do sleep 3; done && k3s server || true"]
securityContext:
privileged: true
volumeMounts:
- name: tmp
mountPath: /tmp
- name: k3setc
mountPath: /etc/rancher/k3s
- name: k3svar
mountPath: /var/lib/rancher/k3s
- name: ci-builder
inputs:
parameters:
- name: cmd
artifacts:
- name: code
path: /go/src/github.com/argoproj/argo-cd
git:
repo: "{{workflow.parameters.repo}}"
revision: "{{workflow.parameters.revision}}"
container:
image: argoproj/argo-cd-ci-builder:v0.13.1
imagePullPolicy: Always
command: [bash, -c]
args: ["{{inputs.parameters.cmd}}"]
workingDir: /go/src/github.com/argoproj/argo-cd
env:
- name: CODECOV_TOKEN
valueFrom:
secretKeyRef:
name: codecov-token
key: codecov-token
resources:
requests:
memory: 1024Mi
cpu: 200m
archiveLocation:
archiveLogs: true
- name: ci-dind
inputs:
parameters:
- name: cmd
artifacts:
- name: code
path: /go/src/github.com/argoproj/argo-cd
git:
repo: "{{workflow.parameters.repo}}"
revision: "{{workflow.parameters.revision}}"
container:
image: argoproj/argo-cd-ci-builder:v0.13.1
imagePullPolicy: Always
command: [sh, -c]
args: ["until docker ps; do sleep 3; done && {{inputs.parameters.cmd}}"]
workingDir: /go/src/github.com/argoproj/argo-cd
env:
- name: DOCKER_HOST
value: 127.0.0.1
- name: DOCKER_BUILDKIT
value: "1"
resources:
requests:
memory: 1024Mi
cpu: 200m
sidecars:
- name: dind
image: docker:18.09-dind
securityContext:
privileged: true
mirrorVolumeMounts: true
archiveLocation:
archiveLogs: true

View File

@@ -1,16 +0,0 @@
version: 2.1
jobs:
dummy:
docker:
- image: cimg/base:2020.01
steps:
- run:
name: Dummy step
command: |
echo "This is a dummy step to satisfy CircleCI"
workflows:
version: 2
workflow:
jobs:
- dummy

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

@@ -1,17 +1,7 @@
ignore:
- "**/*.pb.go"
- "**/*.pb.gw.go"
- "**/*generated.go"
- "**/*generated.deepcopy.go"
- "**/*_test.go"
- "pkg/apis/client/.*"
- "pkg/apis/.*"
- "pkg/client/.*"
- "vendor/.*"
coverage:
status:
# we've found this not to be useful
patch: off
project:
default:
# allow test coverage to drop by 2%, assume that it's typically due to CI problems
threshold: 2
- "test/.*"

View File

@@ -10,4 +10,3 @@ dist/
cmd/**/debug
debug.test
coverage.out
ui/node_modules/

View File

@@ -4,41 +4,24 @@ about: Create a report to help us improve
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: https://bit.ly/argocd-faq.
* [ ] I've included steps to reproduce the bug.
* [ ] I've pasted the output of `argocd version`.
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
A list of the steps required to reproduce the issue. Best of all, give us the URL to a repository that exhibits this issue.
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Version**
```shell
Paste the output from `argocd version` here.
```
**Logs**
```
Paste any relevant application logs here.
```
**Additional context**
Add any other context about the problem here.

View File

@@ -1,18 +0,0 @@
---
name: Enhancement proposal
about: Propose an enhancement for this project
title: ''
labels: 'enhancement'
assignees: ''
---
# Summary
What change you think needs making.
# Motivation
Please give examples of your use case, e.g. when would you use this.
# Proposal
How do you think this should be implemented?

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: 'enhancement'
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,7 +0,0 @@
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.
* [ ] I've signed the CLA and my build is green ([troubleshooting builds](https://argoproj.github.io/argo-cd/developer-guide/ci/)).

3
.github/stale.yml vendored
View File

@@ -1,4 +1 @@
# See https://github.com/probot/stale
# See https://github.com/probot/stale
exemptLabels:
- backlog

View File

@@ -1,364 +0,0 @@
name: Integration tests
on:
push:
branches:
- 'master'
- 'release-*'
- '!release-1.4'
- '!release-1.5'
pull_request:
branches:
- 'master'
- 'release-1.7'
jobs:
build-docker:
name: Build Docker image
runs-on: ubuntu-latest
if: github.head_ref != ''
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Build Docker image
run: |
make image
check-go:
name: Ensure Go modules synchronicity
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Golang
uses: actions/setup-go@v1
with:
go-version: '1.14.12'
- 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.12'
- 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@v2
with:
version: v1.29
args: --timeout 5m --exclude SA5011
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.12'
- 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 "/home/runner/go/bin" >> $GITHUB_PATH
- name: Add /usr/local/bin to PATH
run: |
echo "/usr/local/bin" >> $GITHUB_PATH
- 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.12'
- name: Create symlink in GOPATH
run: |
mkdir -p ~/go/src/github.com/argoproj
cp -a ../argo-cd ~/go/src/github.com/argoproj
- name: Add ~/go/bin to PATH
run: |
echo "/home/runner/go/bin" >> $GITHUB_PATH
- name: Add /usr/local/bin to PATH
run: |
echo "/usr/local/bin" >> $GITHUB_PATH
- name: Download & vendor dependencies
run: |
# We need to vendor go modules for codegen yet
go mod download
go mod vendor -v
working-directory: /home/runner/go/src/github.com/argoproj/argo-cd
- name: Install toolchain for codegen
run: |
make install-codegen-tools-local
make install-go-tools-local
working-directory: /home/runner/go/src/github.com/argoproj/argo-cd
- name: Initialize local Helm
run: |
helm2 init --client-only
- name: Run codegen
run: |
set -x
export GOPATH=$(go env GOPATH)
git checkout -- go.mod go.sum
make codegen-local
working-directory: /home/runner/go/src/github.com/argoproj/argo-cd
- name: Check nothing has changed
run: |
set -xo pipefail
git diff --exit-code -- . ':!go.sum' ':!go.mod' ':!assets/swagger.json' | tee codegen.patch
working-directory: /home/runner/go/src/github.com/argoproj/argo-cd
build-ui:
name: Build, test & lint UI code
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup NodeJS
uses: actions/setup-node@v1
with:
node-version: '11.15.0'
- name: Restore node dependency cache
id: cache-dependencies
uses: actions/cache@v1
with:
path: ui/node_modules
key: ${{ runner.os }}-node-dep-v2-${{ hashFiles('**/yarn.lock') }}
- name: Install node dependencies
run: |
cd ui && yarn install --frozen-lockfile --ignore-optional --non-interactive
- name: Build UI code
run: |
yarn test
yarn build
env:
NODE_ENV: production
working-directory: ui/
- name: Run ESLint
run: yarn lint
working-directory: ui/
analyze:
name: Process & analyze test artifacts
runs-on: ubuntu-latest
needs:
- test-go
- build-ui
env:
sonar_secret: ${{ secrets.SONAR_TOKEN }}
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Restore node dependency cache
id: cache-dependencies
uses: actions/cache@v1
with:
path: ui/node_modules
key: ${{ runner.os }}-node-dep-v2-${{ hashFiles('**/yarn.lock') }}
- name: Remove other node_modules directory
run: |
rm -rf ui/node_modules/argo-ui/node_modules
- name: Create test-results directory
run: |
mkdir -p test-results
- name: Get code coverage artifiact
uses: actions/download-artifact@v2
with:
name: code-coverage
- name: Get test result artifact
uses: actions/download-artifact@v2
with:
name: test-results
path: test-results
- name: Upload code coverage information to codecov.io
uses: codecov/codecov-action@v1
with:
file: coverage.out
- name: Perform static code analysis using SonarCloud
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SCANNER_VERSION: 4.2.0.1873
SCANNER_PATH: /tmp/cache/scanner
OS: linux
run: |
# We do not use the provided action, because it does contain an old
# version of the scanner, and also takes time to build.
set -e
mkdir -p ${SCANNER_PATH}
export SONAR_USER_HOME=${SCANNER_PATH}/.sonar
if [[ ! -x "${SCANNER_PATH}/sonar-scanner-${SCANNER_VERSION}-${OS}/bin/sonar-scanner" ]]; then
curl -Ol https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${SCANNER_VERSION}-${OS}.zip
unzip -qq -o sonar-scanner-cli-${SCANNER_VERSION}-${OS}.zip -d ${SCANNER_PATH}
fi
chmod +x ${SCANNER_PATH}/sonar-scanner-${SCANNER_VERSION}-${OS}/bin/sonar-scanner
chmod +x ${SCANNER_PATH}/sonar-scanner-${SCANNER_VERSION}-${OS}/jre/bin/java
# Explicitly set NODE_MODULES
export NODE_MODULES=${PWD}/ui/node_modules
export NODE_PATH=${PWD}/ui/node_modules
${SCANNER_PATH}/sonar-scanner-${SCANNER_VERSION}-${OS}/bin/sonar-scanner
if: env.sonar_secret != ''
test-e2e:
name: Run end-to-end tests
runs-on: ubuntu-latest
needs:
- build-go
env:
GOPATH: /home/runner/go
ARGOCD_FAKE_IN_CLUSTER: "true"
ARGOCD_SSH_DATA_PATH: "/tmp/argo-e2e/app/config/ssh"
ARGOCD_TLS_DATA_PATH: "/tmp/argo-e2e/app/config/tls"
ARGOCD_E2E_SSH_KNOWN_HOSTS: "../fixture/certs/ssh_known_hosts"
ARGOCD_E2E_K3S: "true"
ARGOCD_IN_CI: "true"
ARGOCD_E2E_APISERVER_PORT: "8088"
ARGOCD_SERVER: "127.0.0.1:8088"
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Golang
uses: actions/setup-go@v1
with:
go-version: '1.14.12'
- 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 ~/go/bin to PATH
run: |
echo "/home/runner/go/bin" >> $GITHUB_PATH
- name: Add /usr/local/bin to PATH
run: |
echo "/usr/local/bin" >> $GITHUB_PATH
- 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.10-alpine
- name: Create target directory for binaries in the build-process
run: |
mkdir -p dist
chown runner dist
- name: Run E2E server and wait for it being available
timeout-minutes: 30
run: |
set -x
# Something is weird in GH runners -- there's a phantom listener for
# port 8080 which is not visible in netstat -tulpen, but still there
# with a HTTP listener. We have API server listening on port 8088
# instead.
make start-e2e-local &
count=1
until curl -f http://127.0.0.1:8088/healthz; do
sleep 10;
if test $count -ge 60; then
echo "Timeout"
exit 1
fi
count=$((count+1))
done
- name: Run E2E testsuite
run: |
set -x
make test-e2e-local

View File

@@ -1,52 +0,0 @@
name: "Code scanning - action"
on:
push:
pull_request:
schedule:
- cron: '0 19 * * 0'
jobs:
CodeQL-Build:
# CodeQL runs on ubuntu-latest and windows-latest
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
# Override language selection by uncommenting this and choosing your languages
# with:
# languages: go, javascript, csharp, python, cpp, java
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -1,31 +0,0 @@
name: Deploy
on:
push:
branches:
- master
pull_request:
branches:
- 'master'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Setup Python
uses: actions/setup-python@v1
with:
python-version: 3.x
- name: build
run: |
pip install mkdocs==1.0.4 mkdocs_material==4.1.1
mkdocs build
mkdir ./site/.circleci && echo '{version: 2, jobs: {build: {branches: {ignore: gh-pages}}}}' > ./site/.circleci/config.yml
- name: deploy
if: ${{ github.event_name == 'push' }}
uses: peaceiris/actions-gh-pages@v2.5.0
env:
PERSONAL_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
PUBLISH_BRANCH: gh-pages
PUBLISH_DIR: ./site

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.12'
- 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 "TARGET_VERSION=${TARGET_VERSION}" >> $GITHUB_ENV
echo "TARGET_BRANCH=${TARGET_BRANCH}" >> $GITHUB_ENV
echo "RELEASE_TAG=${RELEASE_TAG}" >> $GITHUB_ENV
echo "PRE_RELEASE=${PRE_RELEASE}" >> $GITHUB_ENV
- 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 "RELEASE_NOTES=${RELEASE_NOTES}" >> $GITHUB_ENV
- name: Setup Golang
uses: actions/setup-go@v1
with:
go-version: '1.14.12'
- 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() }}

3
.gitignore vendored
View File

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

View File

@@ -1,22 +1,21 @@
run:
timeout: 2m
deadline: 8m
skip-files:
- ".*\\.pb\\.go"
skip-dirs:
- pkg/client/
- vendor/
- pkg/client
- vendor
linter-settings:
goimports:
local-prefixes: github.com/argoproj/argo-cd
linters:
enable:
- vet
- deadcode
- gofmt
- goimports
- deadcode
- varcheck
- structcheck
- ineffassign
- unconvert
- unparam
linters-settings:
goimports:
local-prefixes: github.com/argoproj/argo-cd
service:
golangci-lint-version: 1.21.0
- misspell

View File

@@ -1,922 +1,22 @@
# Changelog
## v1.7.0 (Unreleased)
### GnuPG Signature Verification
The feature allows to only sync against commits that are signed in Git using GnuPG. The list of public
GPG keys required for verification is configured at the system level and can be managed using Argo CD CLI or Web user interface.
The keys management is integrated with Argo CD SSO and access control system (e.g. `argocd gpg add --from <path-to-key>`)
The signature verification is enabled on the project level. The ApplicationProject CRD has a new signatureKeys field that includes
a list of imported public GPG keys. Argo CD will verify the commit signature by these keys for every project application.
### Cluster Management Enhancements
The feature allows using the cluster name instead of the URL to specify the application destination cluster.
Additionally, the cluster CLI and Web user interface have been improved. Argo CD operators now can view and edit cluster
details using the Cluster Details page. The page includes cluster settings details as well as runtime information such
as the number of monitored Kubernetes resources.
### Diffing And Synchronization Usability
* **Diffing logic improvement** Argo CD performs client-side resource diffing to detect deviations and present detected
differences in the UI and CLI. The 1.7 release aligns a comparison algorithm with server-side Kubernetes implementation
and removes inaccuracies in some edge cases.
* **Helm Hooks Compatibility** The improvement removes the discrepancy between the way how Argo CD and Helm deletes
hooks resources. This significantly improves the compatibility and enables additional use cases.
* **Namespace Auto-Creation** With a new option for applications Argo CD will ensure that namespace specified as the
application destination exists in the destination cluster.
* **Failed Sync Retry** This feature enables retrying of failed synchronization attempts during both manually-triggered
and automated synchronization.
### Orphaned Resources Monitoring Enhancement
The enhancement allows configuring an exception list in Orphaned Resources settings to avoid false alarms.
## v1.6.2 (2020-08-01)
- feat: adding validate for app create and app set (#4016)
- fix: use glob matcher in casbin built-in model (#3966)
- fix: Normalize Helm chart path when chart name contains a slash (#3987)
- fix: allow duplicates when using generateName (#3878)
- fix: nil pointer dereference while syncing an app (#3915)
## v1.6.1 (2020-06-18)
- fix: User unable to generate project token even if account has appropriate permissions (#3804)
## v1.6.0 (2020-06-16)
[1.6 Release blog post](https://blog.argoproj.io/argo-cd-v1-6-democratizing-gitops-with-gitops-engine-5a17cfc87d62)
### GitOps Engine
As part of 1.6 release, the core Argo CD functionality has been moved into [GitOps Engine](https://github.com/argoproj/gitops-engine).
GitOps Engine is a reusable library that empowers you to quickly build specialized tools that implement specific GitOps
use cases, such as bootstrapping a Kubernetes cluster, or decentralized management of namespaces.
#### Enhancements
- feat: upgrade kustomize to v3.6.1 version (#3696)
- feat: Add build support for ARM images (#3554)
- feat: CLI: Allow setting Helm values literal (#3601) (#3646)
- feat: argocd-util settings resource-overrides list-actions (#3616)
- feat: adding failure retry (#3548)
- feat: Implement GKE ManagedCertificate CRD health checks (#3600)
- feat: Introduce diff normalizer knobs and allow for ignoring aggregated cluster roles (#2382) (#3076)
- feat: Implement Crossplane CRD health checks (#3581)
- feat: Adding deploy time and duration label (#3563)
- feat: support delete cluster from UI (#3555)
- feat: add button loading status for time-consuming operations (#3559)
- feat: Add --logformat switch to API server, repository server and controller (#3408)
- feat: Add a Get Repo command to see if Argo CD has a repo (#3523)
- feat: Allow selecting TLS ciphers on server (#3524)
- feat: Support additional metadata in Application sync operation (#3747)
- feat: upgrade redis to 5.0.8-alpine (#3783)
#### Bug Fixes
- fix: settings manager should invalidate cache after updating repositories/repository credentials (#3672)
- fix: Allow unsetting the last remaining values file (#3644) (#3645)
- fix: Read cert data from kubeconfig during cluster addition and use if present (#3655) (#3667)
- fix: oidc should set samesite cookie (#3632)
- fix: Allow underscores in hostnames in certificate module (#3596)
- fix: apply scopes from argocd-rbac-cm to project jwt group searches (#3508)
- fix: fix nil pointer dereference error after cluster deletion (#3634)
- fix: Prevent possible nil pointer dereference when getting Helm client (#3613)
- fix: Allow CLI version command to succeed without server connection (#3049) (#3550)
- fix: Fix login with port forwarding (#3574)
- fix: use 'git show-ref' to both retrieve and store generated manifests (#3578)
- fix: enable redis retries; add redis request duration metric (#3575)
- fix: Disable keep-alive for HTTPS connection to Git (#3531)
- fix: use uid instead of named user in Dockerfile (#3108)
#### Other
- refactoring: Gitops engine (#3066)
## v1.5.8 (2020-06-16)
- fix: upgrade awscli version (#3774)
- fix: html encode login error/description before rendering it (#3773)
- fix: oidc should set samesite cookie (#3632)
- fix: avoid panic in badge handler (#3741)
## v1.5.7 (2020-06-09)
The 1.5.7 patch release resolves issue #3719 . The ARGOCD_ENABLE_LEGACY_DIFF=true should be added to argocd-application-controller deployment.
- fix: application with EnvoyFilter causes high memory/CPU usage (#3719)
## v1.5.6 (2020-06-02)
- feat: Upgrade kustomize to 3.6.1
- fix: Prevent possible nil pointer dereference when getting Helm client (#3613)
- fix: avoid deadlock in settings manager (#3637)
## v1.5.5 (2020-05-16)
- feat: add Rollout restart action (#3557)
- fix: enable redis retries; add redis request duration metric (#3547)
- fix: when --rootpath is on, 404 is returned when URL contains encoded URI (#3564)
## v1.5.4 (2020-05-05)
- fix: CLI commands with --grpc-web
## v1.5.3 (2020-05-01)
This patch release introduces a set of enhancements and bug fixes. Here are most notable changes:
#### Multiple Kustomize Versions
The bundled Kustomize version had been upgraded to v3.5.4. Argo CD allows changing bundled version using
[custom image or init container](https://argoproj.github.io/argo-cd/operator-manual/custom_tools/).
This [feature](https://argoproj.github.io/argo-cd/user-guide/kustomize/#custom-kustomize-versions)
enables bundling multiple Kustomize versions at the same time and allows end-users to specify the required version per application.
#### Custom Root Path
The feature allows accessing Argo CD UI and API using a custom root path(for example https://myhostname/argocd).
This enables running Argo CD behind a proxy that takes care of user authentication (such as Ambassador) or hosting
multiple Argo CD using the same hostname. A set of bug fixes and enhancements had been implemented to makes it easier.
Use new `--rootpath` [flag](https://argoproj.github.io/argo-cd/operator-manual/ingress/#argocd-server-and-ui-root-path-v153) to enable the feature.
### Login Rate Limiting
The feature prevents a built-in user password brute force attack and addresses the known
[vulnerability](https://argoproj.github.io/argo-cd/security_considerations/#cve-2020-8827-insufficient-anti-automationanti-brute-force).
### Settings Management Tools
A new set of [CLI commands](https://argoproj.github.io/argo-cd/operator-manual/troubleshooting/) that simplify configuring Argo CD.
Using the CLI you can test settings changes offline without affecting running Argo CD instance and have ability to troubleshot diffing
customizations, custom resource health checks, and more.
### Other
* New Project and Application CRD settings ([#2900](https://github.com/argoproj/argo-cd/issues/2900), [#2873](https://github.com/argoproj/argo-cd/issues/2873)) that allows customizing Argo CD behavior.
* Upgraded Dex (v2.22.0) enables seamless [SSO integration](https://www.openshift.com/blog/openshift-authentication-integration-with-argocd) with Openshift.
#### Enhancements
* feat: added --grpc-web-root-path for CLI. (#3483)
* feat: limit the maximum number of concurrent login attempts (#3467)
* feat: upgrade kustomize version to 3.5.4 (#3472)
* feat: upgrade dex to 2.22.0 (#3468)
* feat: support user specified account token ids (#3425)
* feat: support separate Kustomize version per application (#3414)
* feat: add support for dex prometheus metrics (#3249)
* feat: add settings troubleshooting commands to the 'argocd-util' binary (#3398)
* feat: Let user to define meaningful unique JWT token name (#3388)
* feat: Display link between OLM ClusterServiceVersion and it's OperatorGroup (#3390)
* feat: Introduce sync-option SkipDryRunOnMissingResource=true (#2873) (#3247)
* feat: support normalizing CRD fields that use known built-in K8S types (#3357)
* feat: Whitelisted namespace resources (#2900)
#### Bug Fixes
* fix: added path to cookie (#3501)
* fix: 'argocd sync' does not take into account IgnoreExtraneous annotation (#3486)
* fix: CLI renders flipped diff results (#3480)
* fix: GetApplicationSyncWindows API should not validate project permissions (#3456)
* fix: argocd-util kubeconfig should use RawRestConfig to export config (#3447)
* fix: javascript error on accounts list page (#3453)
* fix: support both <group>/<kind> as well as <kind> as a resource override key (#3433)
* fix: Updating to jsonnet v1.15.0 fix issue #3277 (#3431)
* fix for helm repo add with flag --insecure-skip-server-verification (#3420)
* fix: app diff --local support for helm repo. #3151 (#3407)
* fix: Syncing apps incorrectly states "app synced", but this is not true (#3286)
* fix: for jsonnet when it is localed in nested subdirectory and uses import (#3372)
* fix: Update 4.5.3 redis-ha helm manifest (#3370)
* fix: return 401 error code if username does not exist (#3369)
* fix: Do not panic while running hooks with short revision (#3368)
## v1.5.2 (2020-04-20)
#### Critical security fix
This release contains a critical security fix. Please refer to the
[security document](https://argoproj.github.io/argo-cd/security_considerations/#CVE-2020-5260-possible-git-credential-leak)
for more information.
**Upgrading is strongly recommended**
## v1.4.3 (2020-04-20)
#### Critical security fix
This release contains a critical security fix. Please refer to the
[security document](https://argoproj.github.io/argo-cd/security_considerations/#CVE-2020-5260-possible-git-credential-leak)
for more information.
## v1.5.1 (2020-04-06)
#### Bug Fixes
* fix: return 401 error code if username does not exist (#3369)
* fix: Do not panic while running hooks with short revision (#3368)
* fix: Increase HAProxy check interval to prevent intermittent failures (#3356)
* fix: Helm v3 CRD are not deployed (#3345)
## v1.5.0 (2020-04-02)
#### Helm Integration Enhancements - Helm 3 Support And More
Introduced native support Helm3 charts. For backward compatibility Helm 2 charts are still rendered using Helm 2 CLI. Argo CD inspects the
Charts.yaml file and choose the right binary based on `apiVersion` value.
Following enhancement were implemented in addition to Helm 3:
* The `--api-version` flag is passed to the `helm template` command during manifest generation.
* The `--set-file` flag can be specified in the application specification.
* Fixed bug that prevents automatically update Helm chart when new version is published (#3193)
#### Better Performance and Improved Metrics
If you are running Argo CD instances with several hundred applications on it, you should see a
huge performance boost and significantly less Kubernetes API server load.
The Argo CD controller Prometheus metrics have been reworked to enable a richer Grafana dashboard.
The improved dashboard is available at [examples/dashboard.json](https://github.com/argoproj/argo-cd/blob/master/examples/dashboard.json).
You can set `ARGOCD_LEGACY_CONTROLLER_METRICS=true` environment variable and use [examples/dashboard-legacy.json](https://github.com/argoproj/argo-cd/blob/master/examples/dashboard-legacy.json)
to keep using old dashboard.
#### Local accounts
The local accounts had been introduced additional to `admin` user and SSO integration. The feature is useful for creating authentication
tokens with limited permissions to automate Argo CD management. Local accounts also could be used small by teams when SSO integration is overkill.
This enhancement also allows to disable admin user and enforce only SSO logins.
#### Redis HA Proxy mode
As part of this release, the bundled Redis was upgraded to version 4.3.4 with enabled HAProxy.
The HA proxy replaced the sentinel and provides more reliable Redis connection.
> After publishing 1.5.0 release we've discovered that default HAProxy settings might cause intermittent failures.
> See [argo-cd#3358](https://github.com/argoproj/argo-cd/issues/3358)
#### Windows CLI
Windows users deploy to Kubernetes too! Now you can use Argo CD CLI on Linux, Mac OS, and Windows. The Windows compatible binary is available
in the release details page as well as on the Argo CD Help page.
#### Breaking Changes
The `argocd_app_sync_status`, `argocd_app_health_status` and `argocd_app_created_time` prometheus metrics are deprecated in favor of additional labels
to `argocd_app_info` metric. The deprecated labels are still available can be re-enabled using `ARGOCD_LEGACY_CONTROLLER_METRICS=true` environment variable.
The legacy example Grafana dashboard is available at [examples/dashboard-legacy.json](https://github.com/argoproj/argo-cd/blob/master/examples/dashboard-legacy.json).
#### Known issues
Last-minute bugs that will be addressed in 1.5.1 shortly:
* https://github.com/argoproj/argo-cd/issues/3336
* https://github.com/argoproj/argo-cd/issues/3319
* https://github.com/argoproj/argo-cd/issues/3339
* https://github.com/argoproj/argo-cd/issues/3358
#### Enhancements
* feat: support helm3 (#2383) (#3178)
* feat: Argo CD Service Account / Local Users #3185
* feat: Disable Admin Login (fixes #3019) (#3179)
* feat(ui): add docs to sync policy options present in create application panel (Close #3098) (#3203)
* feat: add "service-account" flag to "cluster add" command (#3183) (#3184)
* feat: Supports the validate-false option at an app level. Closes #1063 (#2542)
* feat: add dest cluster and namespace in the Events (#3093)
* feat: Rollback disables auto sync issue #2441 (#2591)
* feat: allow ssh and http repository references in bitbucketserver webhook #2773 (#3036)
* feat: Add helm --set-file support (#2751)
* feat: Include resource group for Event's InvolvedObject.APIVersion
* feat: Add argocd cmd for Windows #2121 (#3015)
#### Bug Fixes
- fix: app reconciliation fails with panic: index out of (#3233)
- fix: upgrade argoproj/pkg version to fix leaked sensitive information in logs (#3230)
- fix: set MaxCallSendMsgSize to MaxGRPCMessageSize for the GRPC caller (#3117)
- fix: stop caching helm index (#3193)
- fix: dex proxy should forward request to dex preserving the basehref (#3165)
- fix: set default login redirect to baseHRef (#3164)
- fix: don't double-prepend basehref to redirect URLs (fixes #3137)
- fix: ui referring to /api/version using absolute path (#3092)
- fix: Unhang UI on long app info items by using more sane URL match pattern (#3159)
- fix: Allow multiple hostnames per SSH known hosts entry and also allow IPv6 (#2814) (#3074)
- fix: argocd-util backup produced truncated backups. import app status (#3096)
- fix: upgrade redis-ha chart and enable haproxy (#3147)
- fix: make dex server deployment init container resilient to restarts (#3136)
- fix: reduct secret values of manifests stored in git (#3088)
- fix: labels not being deleted via UI (#3081)
- fix: HTTP|HTTPS|NO_PROXY env variable reading #3055 (#3063)
- fix: Correct usage text for repo add command regarding insecure repos (#3068)
- fix: Ensure SSH private key is written out with a final newline character (#2890) (#3064)
- fix: Handle SSH URLs in 'git@server:org/repo' notation correctly (#3062)
- fix sso condition when several sso connectors has been configured (#3057)
- fix: Fix bug where the same pointer is used. (#3059)
- fix: Opening in new tab bad key binding on Linux (#3020)
- fix: K8s secrets for repository credential templates are not deleted when credential template is deleted (#3028)
- fix: SSH credential template not working #3016
- fix: Unable to parse kubectl pre-release version strings (#3034)
- fix: Jsonnet TLA parameters of same type are overwritten (#3022)
- fix: Replace aws-iam-authenticator to support IRSA (#3010)
- fix: Hide bindPW in dex config (#3025)
- fix: SSH repo URL with a user different from `git` is not matched correctly when resolving a webhook (#2988)
- fix: JWT invalid => Password for superuser has changed since token issued (#2108)
#### Contributors
* alexandrfox
* alexec
* alexmt
* bergur88
* CBytelabs
* dbeal-wiser
* dnascimento
* Elgarni
* eSamS
* gpaul
* jannfis
* jdmulloy
* machgo
* masa213f
* matthyx
* rayanebel
* shelby-moore
* tomcruise81
* wecger
* zeph
## v1.4.2 (2020-01-24)
- fix: correctly replace cache in namespace isolation mode (#3023)
## v1.4.1 (2020-01-23)
- fix: impossible to config RBAC if group name includes ',' (#3013)
## v1.4.0 (2020-01-17)
The v1.4.0 is a stability release that brings multiple bug fixes, security, performance enhancements, and multiple usability improvements.
#### New Features
#### Security
A number of security enhancements and features have been implemented (thanks to [@jannfis](https://github.com/jannfis) for driving it! ):
* **Repository Credential Templates Management UI/CLI**. Now you can use Argo CD CLI or UI to configure
[credentials template](https://argoproj.github.io/argo-cd/user-guide/private-repositories/#credential-templates) for multiple repositories!
* **X-Frame-Options header on serving static assets**. The X-Frame-Options prevents third party sites to trick users into interacting with the application.
* **Tighten AppProject RBAC enforcement**. We've improved the enforcement of access rules specified in the
[application project](https://argoproj.github.io/argo-cd/operator-manual/declarative-setup/#projects) configuration.
#### Namespace Isolation
With the namespace isolation feature, you are no longer have to give full read-only cluster access to the Argo CD. Instead, you can give access only to selected namespaces with-in
the cluster:
```bash
argocd cluster add <mycluster> --namespace <mynamespace1> --namespace <mynamespace2>
```
This feature is useful if you don't have full cluster access but still want to use Argo CD to manage some cluster namespaces. The feature also improves performance if Argo CD is
used to manage a few namespaces of a large cluster.
#### Reconciliation Performance
The Argo CD no longer fork/exec `kubectl` to apply resource changes in the target cluster or convert resource manifest to the required manifest version. This reduces
CPU and Memory usage of large Argo CD instances.
#### Resources Health based Hook Status
The existing Argo CD [resource hooks](https://argoproj.github.io/argo-cd/user-guide/resource_hooks/) feature allows running custom logic during the syncing process. You can mark
any Kubernetes resource as a hook and Argo CD assess hook status if resource is a `Pod`, `Job` or `Argo Workflow`. In the v1.4.0 release Argo CD is going to leverage resource
[health assessment](https://argoproj.github.io/argo-cd/operator-manual/health/) to get sync hook status. This allows using any custom CRD as a sync hook and leverage custom health
check logic.
#### Manifest Generation
* **Track Helm Charts By Semantic Version**. You've been able to track charts hosted in Git repositories using branches to tags. This is now possible for Helm charts. You no longer
need to choose the exact version, such as v1.4.0 ,instead you can use a semantic version constraint such as v1.4.* and the latest version that matches will be installed.
* **Build Environment Variables**. Feature allows config management tool to get access to app details during manifest generation via
[environment variables](https://argoproj.github.io/argo-cd/user-guide/build-environment/).
* **Git submodules**. Argo CD is going to automatically fetch sub-modules if your repository has `.gitmodules` directory.
#### UI and CLI
* **Improved Resource Tree View**. The Application details page got even prettier. The resource view was tuned to fit more resources into the screen, include more information about
each resource and don't lose usability at the same time.
* **New Account Management CLI Command**. The CLI allows to check which actions are allowed for your account: `argocd account can-i sync applications '*'`
#### Maintenance Tools
The team put more effort into building tools that help to maintain Argo CD itself:
* **Bulk Project Editing**. The `argocd-util` allows to add and remove permissions defined in multiple project roles using one command.
* **More Prometheus Metrics**. A set of additional metrics that contains useful information managed clusters is exposed by application controller.
More documentation and tools are coming in patch releases.
#### Breaking Changes
The Argo CD deletes all **in-flight** hooks if you terminate running sync operation. The hook state assessment change implemented in this release the Argo CD enables detection of
an in-flight state for all Kubernetes resources including `Deployment`, `PVC`, `StatefulSet`, `ReplicaSet` etc. So if you terminate the sync operation that has, for example,
`StatefulSet` hook that is `Progressing` it will be deleted. The long-running jobs are not supposed to be used as a sync hook and you should consider using
[Sync Waves](https://argoproj.github.io/argo-cd/user-guide/sync-waves/) instead.
#### Enhancements
* feat: Add custom healthchecks for cert-manager v0.11.0 (#2689)
* feat: add git submodule support (#2495)
* feat: Add repository credential management API and CLI (addresses #2136) (#2207)
* feat: add support for --additional-headers cli flag (#2467)
* feat: Add support for ssh-with-port repo url (#2866) (#2948)
* feat: Add Time to ApplicationCondition. (#2417)
* feat: Adds `argocd auth can-i` command. Close #2255
* feat: Adds revision history limit. Closes #2790 (#2818)
* feat: Adds support for ARGO_CD_[TARGET_REVISION|REVISION] and pass to Custom Tool/Helm/Jsonnet
* feat: Adds support for Helm charts to be a semver range. Closes #2552 (#2606)
* feat: Adds tracing to key external invocations. (#2811)
* feat: argocd-util should allow editing project policies in bulk (#2615)
* feat: Displays controllerrevsion's revision in the UI. Closes #2306 (#2702)
* feat: Issue #2559 - Add gauge Prometheus metric which represents the number of pending manifest requests. (#2658)
* feat: Make ConvertToVersion maybe 1090% faster on average (#2820)
* feat: namespace isolation (#2839)
* feat: removes redundant mutex usage in controller cache and adds cluster cache metrics (#2898)
* feat: Set X-Frame-Options on serving static assets (#2706) (#2711)
* feat: Simplify using Argo CD without users/SSO/UI (#2688)
* feat: Template Out Data Source in Grafana Dashboard (#2859)
* feat: Updates UI icons. Closes #2625 and #2757 (#2653)
* feat: use editor arguments in InteractiveEditor (#2833)
* feat: Use kubectl apply library instead of forking binary (#2861)
* feat: use resource health for hook status evaluation (#2938)
#### Bug Fixes
- fix: Adds support for /api/v1/account* via HTTP. Fixes #2664 (#2701)
- fix: Allow '@'-character in SSH usernames when connecting a repository (#2612)
- fix: Allow dot in project policy. Closes #2724 (#2755)
- fix: Allow you to sync local Helm apps. Fixes #2741 (#2747)
- fix: Allows Helm parameters that contains arrays or maps. (#2525)
- fix: application-controller doesn't deal with rm/add same cluster gracefully (x509 unknown) (#2389)
- fix: diff local ignore kustomize build options (#2942)
- fix: Ensures that Helm charts are correctly resolved before sync. Fixes #2758 (#2760)
- fix: Fix 'Open application' link when using basehref (#2729)
- fix: fix a bug with cluster add when token secret is not first in list. (#2744)
- fix: fix bug where manifests are not cached. Fixes #2770 (#2771)
- fix: Fixes bug whereby retry does not work for CLI. Fixes #2767 (#2768)
- fix: git contention leads applications into Unknown state (#2877)
- fix: Issue #1944 - Gracefully handle missing cached app state (#2464)
- fix: Issue #2668 - Delete a specified context (#2669)
- fix: Issue #2683 - Make sure app update don't fail due to concurrent modification (#2852)
- fix: Issue #2721 Optimize helm repo querying (#2816)
- fix: Issue #2853 - Improve application env variables/labels editing (#2856)
- fix: Issue 2848 - Application Deployment history panel shows incorrect info for recent releases (#2849)
- fix: Make BeforeHookCreation the default. Fixes #2754 (#2759)
- fix: No error on `argocd app create` in CLI if `--revision` is omitted #2665
- fix: Only delete resources during app delete cascade if permitted to (fixes #2693) (#2695)
- fix: prevent user from seeing/deleting resources not permitted in project (#2908) (#2910)
- fix: self-heal should retry syncing an application after specified delay
- fix: stop logging dex config secrets #(2904) (#2937)
- fix: stop using jsondiffpatch on clientside to render resource difference (#2869)
- fix: Target Revision truncated #2736
- fix: UI should re-trigger SSO login if SSO JWT token expires (#2891)
- fix: update argocd-util import was not working properly (#2939)
#### Contributors
* Aalok Ahluwalia
* Aananth K
* Abhishek Jaisingh
* Adam Johnson
* Alan Tang
* Alex Collins
* Alexander Matyushentsev
* Andrew Waters
* Byungjin Park
* Christine Banek
* Daniel Helfand
* David Hong
* David J. M. Karlsen
* David Maciel
* Devan Goodwin
* Devin Stein
* dthomson25
* Gene Liverman
* Gregor Krmelj
* Guido Maria Serra
* Ilir Bekteshi
* Imran Ismail
* INOUE BANJI
* Isaac Gaskin
* jannfis
* Jeff Hastings
* Jesse Suen
* John Girvan
* Konstantin
* Lev Aminov
* Manatsawin Hanmongkolchai
* Marco Schmid
* Masayuki Ishii
* Michael Bridgen
* Naoki Oketani
* niqdev
* nitinpatil1992
* Olivier Boukili
* Olivier Lemasle
* Omer Kahani
* Paul Brit
* Qingbo Zhou
* Saradhi Sreegiriraju
* Scott Cabrinha
* shlo
* Simon Behar
* stgarf
* Yujun Zhang
* Zoltán Reegn
## v1.3.4 (2019-12-05)
- #2819 Fixes logging of tracing option in CLI
## v1.3.3 (2019-12-05)
- #2721 High CPU utilisation (5 cores) and spammy logs
## v1.3.2 (2019-12-03)
- #2797 Fix directory traversal edge case and enhance tests
## v1.3.1 (2019-12-02)
- #2664 update account password from API resulted 404
- #2724 Can't use `DNS-1123` compliant app name when creating project role
- #2726 App list does not show chart for Helm app
- #2741 argocd local sync cannot parse kubernetes version
- #2754 BeforeHookCreation should be the default hook
- #2767 Fix bug whereby retry does not work for CLI
- #2770 Always cache miss for manifests
- #1345 argocd-application-controller: can not retrieve list of objects using index : Index with name namespace does not exist
## v1.3.0 (2019-11-13)
#### New Features
##### Helm 1st-Class Support
We know that for many of our users, they want to deploy existing Helm charts using Argo CD. Up until now that has required you to create an Argo CD app in a Git repo that does nothing but point to that chart. Now you can use a Helm chart repository is the same way as a Git repository.
On top of that, we've improved support for Helm apps. The most common types of Helm hooks such as `pre-install` and `post-install` are supported as well as a the delete policy `before-hook-creation` which makes it easier to work with hooks.
https://youtu.be/GP7xtrnNznw
##### Orphan Resources
Some users would like to make sure that resources in a namespace are managed only by Argo CD. So we've introduced the concept of an "orphan resource" - any resource that is in namespace associated with an app, but not managed by Argo CD. This is enabled in the project settings. Once enabled, Argo CD will show in the app view any resources in the app's namespace that is not managed by Argo CD.
https://youtu.be/9ZoTevVQf5I
##### Sync Windows
There may be instances when you want to control the times during which an Argo CD app can sync. Sync Windows now gives you the capability to create windows of time in which apps are either allowed or denied the ability to sync. This can apply to both manual and auto-sync, or just auto-sync. The windows are configured at the project level and assigned to apps using app name, namespace or cluster. Wildcards are supported for all fields.
#### Enhancements
* [UI] Add application labels to Applications list and Applications details page (#1099)
* Helm repository as first class Argo CD Application source (#1145)
* Ability to generate a warn/alert when a namespace deviates from the expected state (#1167)
* Improve diff support for resource requests/limits (#1615)
* HTTP API should allow JWT to be passed via Authorization header (#1642)
* Ability to create & upsert projects from spec (#1852)
* Support for in-line block from helm chart values (#1930)
* Request OIDC groups claim if groups scope is not supported (#1956)
* Add a maintenance window for Applications with automated syncing (#1995)
* Support `argocd.argoproj.io/hook-delete-policy: BeforeHookCreation` (#2036)
* Support setting Helm string parameters using CLI/UI (#2078)
* Config management plugin environment variable UI/CLI support (#2203)
* Helm: auto-detect URLs (#2260)
* Helm: UI improvements (#2261)
* Support `helm template --kube-version ` (#2275)
* Use community icons for resources (#2277)
* Make `group` optional for `ignoreDifferences` config (#2298)
* Update Helm docs (#2315)
* Add cluster information into Splunk (#2354)
* argocd list command should have filter options like by project (#2396)
* Add target/current revision to status badge (#2445)
* Update tooling to use Kustomize v3 (#2487)
* Update root `Dockerfile` to use the `hack/install.sh` (#2488)
* Support and document using HPA for repo-server (#2559)
* Upgrade Helm (#2587)
* UI fixes for "Sync Apps" panel. (#2604)
* Upgrade kustomize from v3.1.0 to v3.2.1 (#2609)
* Map helm lifecycle hooks to ArgoCD pre/post/sync hooks (#355)
* [UI] Enhance app creation page with Helm parameters overrides (#1059)
#### Bug Fixes
- failed parsing on parameters with comma (#1660)
- Statefulset with OnDelete Update Strategy stuck progressing (#1881)
- Warning during secret diffing (#1923)
- Error message "Unable to load data: key is missing" is confusing (#1944)
- OIDC group bindings are truncated (#2006)
- Multiple parallel app syncs causes OOM (#2022)
- Unknown error when setting params with argocd app set on helm app (#2046)
- Endpoint is no longer shown as a child of services (#2060)
- SSH known hosts entry cannot be deleted if contains shell pattern in name (#2099)
- Application 404s on names with periods (#2114)
- Adding certs for hostnames ending with a dot (.) is not possible (#2116)
- Fix `TestHookDeleteBeforeCreation` (#2141)
- v1.2.0-rc1 nil pointer dereference when syncing (#2146)
- Replacing services failure (#2150)
- 1.2.0-rc1 - Authentication Required error in Repo Server (#2152)
- v1.2.0-rc1 Applications List View doesn't work (#2174)
- Manual sync does not trigger Presync hooks (#2185)
- SyncError app condition disappears during app reconciliation (#2192)
- argocd app wait\sync prints 'Unknown' for resources without health (#2198)
- 1.2.0-rc2 Warning during secret diffing (#2206)
- SSO redirect url is incorrect if configured Argo CD URL has trailing slash (#2212)
- Application summary diff page shows hooks (#2215)
- An app with a single resource and Sync hook remains progressing (#2216)
- CONTRIBUTING documentation outdated (#2231)
- v1.2.0-rc2 does not retrieve http(s) based git repository behind the proxy (#2243)
- Intermittent "git ls-remote" request failures should not fail app reconciliation (#2245)
- Result of ListApps operation for Git repo is cached incorrectly (#2263)
- ListApps does not utilize cache (#2287)
- Controller panics due to nil pointer error (#2290)
- The Helm --kube-version support does not work on GKE: (#2303)
- Fixes bug that prevents you creating repos via UI/CLI. (#2308)
- The 'helm.repositories' settings is dropped without migration path (#2316)
- Badge response does not contain cache control header (#2317)
- Inconsistent sync result from UI and CLI (#2321)
- Failed edit application with plugin type requiring environment (#2330)
- AutoSync doesn't work anymore (#2339)
- End-to-End tests not working with Kubernetes v1.16 (#2371)
- Creating an application from Helm repository should select "Helm" as source type (#2378)
- The parameters of ValidateAccess GRPC method should not be logged (#2386)
- Maintenance window meaning is confusing (#2398)
- UI bug when targetRevision is omitted (#2407)
- Too many vulnerabilities in Docker image (#2425)
- proj windows commands not consistent with other commands (#2443)
- Custom resource actions cannot be executed from the UI (#2448)
- Application controller sometimes accidentally removes duplicated/excluded resource warning condition (#2453)
- Logic that checks sync windows state in the cli is incorrect (#2455)
- UI don't allow to create window with `* * * * *` schedule (#2475)
- Helm Hook is executed twice if annotated with both pre-install and pre-upgrade annotations (#2480)
- Impossible to edit chart name using App details page (#2484)
- ArgoCD does not provide CSRF protection (#2496)
- ArgoCD failing to install CRDs in master from Helm Charts (#2497)
- Timestamp in Helm package file name causes error in Application with Helm source (#2549)
- Attempting to create a repo with password but not username panics (#2567)
- UI incorrectly mark resources as `Required Pruning` (#2577)
- argocd app diff prints only first difference (#2616)
- Bump min client cache version (#2619)
- Cluster list page fails if any cluster is not reachable (#2620)
- Repository type should be mandatory for repo add command in CLI (#2622)
- Repo server executes unnecessary ls-remotes (#2626)
- Application list page incorrectly filter apps by label selector (#2633)
- Custom actions are disabled in Argo CD UI (#2635)
- Failure of `argocd version` in the self-building container image (#2645)
- Application list page is not updated automatically anymore (#2655)
- Login regression issues (#2659)
- Regression: Cannot return Kustomize version for 3.1.0 (#2662)
- API server does not allow creating role with action `action/*` (#2670)
- Application controller `kubectl-parallelism-limit` flag is broken (#2673)
- Annoying toolbar flickering (#2691)
## v1.2.5 (2019-10-29)
- Issue #2339 - Don't update `status.reconciledAt` unless compared with latest git version (#2581)
## v1.2.4 (2019-10-23)
- Issue #2185 - Manual sync don't trigger hooks (#2477)
- Issue #2339 - Controller should compare with latest git revision if app has changed (#2543)
- Unknown child app should not affect app health (#2544)
- Redact secrets in dex logs (#2538)
## v1.2.3 (2019-10-1)
* Make argo-cd docker images openshift friendly (#2362) (@duboisf)
* Add dest-server and dest-namespace field to reconciliation logs (#2354)
- Stop loggin /repository.RepositoryService/ValidateAccess parameters (#2386)
## v1.2.2 (2019-09-26)
+ Resource action equivalent to `kubectl rollout restart` (#2177)
- Badge response does not contain cache-control header (#2317) (@greenstatic)
- Make sure the controller uses the latest git version if app reconciliation result expired (#2339)
## v1.2.1 (2019-09-12)
+ Support limiting number of concurrent kubectl fork/execs (#2022)
+ Add --self-heal flag to argocd cli (#2296)
- Fix degraded proxy support for http(s) git repository (#2243)
- Fix nil pointer dereference in application controller (#2290)
## v1.2.0 (2019-09-05)
### New Features
#### Server Certificate And Known Hosts Management
The Server Certificate And Known Hosts Management feature makes it really easy to connect private Git repositories to Argo CD. Now Argo CD provides UI and CLI which
enables managing certificates and known hosts which are used to access Git repositories. It is also possible to configure both hosts and certificates in a declarative manner using
[argocd-ssh-known-hosts-cm](https://github.com/argoproj/argo-cd/blob/master/docs/operator-manual/argocd-ssh-known-hosts-cm.yaml) and
[argocd-tls-certs-cm.yaml](https://github.com/argoproj/argo-cd/blob/master/docs/operator-manual/argocd-tls-certs-cm.yaml) config maps.
#### Self-Healing
The existing Automatic Sync feature allows to automatically apply any new changes in Git to the target Kubernetes cluster. However, Automatic Sync does not cover the case when the
application is out of sync due to the unexpected change in the target cluster. The Self-Healing feature fills this gap. With Self-Healing enabled Argo CD automatically pushes the desired state from Git into the cluster every time when state deviation is detected.
**Anonymous access** - enable read-only access without authentication to anyone in your organization.
Support for Git LFS enabled repositories - now you can store Helm charts as tar files and enable Git LFS in your repository.
**Compact diff view** - compact diff summary of the whole application in a single view.
**Badge for application status** - add badge with the health and sync status of your application into README.md of your deployment repo.
**Allow configuring google analytics tracking** - use Google Analytics to check how many users are visiting UI or your Argo CD instance.
#### Backward Incompatible Changes
- Kustomize v1 support is removed. All kustomize charts are built using the same Kustomize version
- Kustomize v2.0.3 upgraded to v3.1.0 . We've noticed one backward incompatible change: https://github.com/kubernetes-sigs/kustomize/issues/42 . Starting v2.1.0 namespace prefix feature works with CRD ( which might cause renaming of generated resource definitions)
- Argo CD config maps must be annotated with `app.kubernetes.io/part-of: argocd` label. Make sure to apply updated `install.yaml` manifest in addition to changing image version.
#### Enhancements
+ Adds a floating action button with help and chat links to every page.… (#2124)
+ Enhances cookie warning with actual length to help users fix their co… (#2134)
+ Added 'SyncFail' to possible HookTypes in UI (#2147)
+ Support for Git LFS enabled repositories (#1853)
+ Server certificate and known hosts management (#1514)
+ Client HTTPS certificates for private git repositories (#1945)
+ Badge for application status (#1435)
+ Make the health check for APIService a built in (#1841)
+ Bitbucket Server and Gogs webhook providers (#1269)
+ Jsonnet TLA arguments in ArgoCD CLI (#1626)
+ Self Healing (#1736)
+ Compact diff view (#1831)
+ Allow Helm parameters to force ambiguously-typed values to be strings (#1846)
+ Support anonymous argocd access (#1620)
+ Allow configuring google analytics tracking (#738)
+ Bash autocompletion for argocd (#1798)
+ Additional commit metadata (#1219)
+ Displays targetRevision in app dashboards. (#1239)
+ Local path syncing (#839)
+ System level `kustomize build` options (#1789)
+ Adds support for `argocd app set` for Kustomize. (#1843)
+ Allow users to create tokens for projects where they have any role. (#1977)
+ Add Refresh button to applications table and card view (#1606)
+ Adds CLI support for adding and removing groups from project roles. (#1851)
+ Support dry run and hook vs. apply strategy during sync (#798)
+ UI should remember most recent selected tab on resource info panel (#2007)
+ Adds link to the project from the app summary page. (#1911)
+ Different icon for resources which require pruning (#1159)
#### Bug Fixes
- Do not panic if the type is not api.Status (an error scenario) (#2105)
- Make sure endpoint is shown as a child of service (#2060)
- Word-wraps app info in the table and list views. (#2004)
- Project source/destination removal should consider wildcards (#1780)
- Repo whitelisting in UI does not support wildcards (#2000)
- Wait for CRD creation during sync process (#1940)
- Added a button to select out of sync items in the sync panel (#1902)
- Proper handling of an excluded resource in an application (#1621)
- Stop repeating logs on stoped container (#1614)
- Fix git repo url parsing on application list view (#2174)
- Fix nil pointer dereference error during app reconciliation (#2146)
- Fix history api fallback implementation to support app names with dots (#2114)
- Fixes some code issues related to Kustomize build options. (#2146)
- Adds checks around valid paths for apps (#2133)
- Endpoint incorrectly considered top level managed resource (#2060)
- Allow adding certs for hostnames ending on a dot (#2116)
#### Other
* Upgrade kustomize to v3.1.0 (#2068)
* Remove support for Kustomize 1. (#1573)
#### Contributors
* [alexec](https://github.com/alexec)
* [alexmt](https://github.com/alexmt)
* [dmizelle](https://github.com/dmizelle)
* [lcostea](https://github.com/lcostea)
* [jutley](https://github.com/jutley)
* [masa213f](https://github.com/masa213f)
* [Rayyis](https://github.com/Rayyis)
* [simster7](https://github.com/simster7)
* [dthomson25](https://github.com/dthomson25)
* [jannfis](https://github.com/jannfis)
* [naynasiddharth](https://github.com/naynasiddharth)
* [stgarf](https://github.com/stgarf)
## v1.1.2 (2019-07-30)
- 'argocd app wait' should print correct sync status (#2049)
- Check that TLS is enabled when registering DEX Handlers (#2047)
- Do not ignore Argo hooks when there is a Helm hook. (#1952)
## v1.1.1 (2019-07-25)
+ Support 'override' action in UI/API (#1984)
- Fix argocd app wait message (#1982)
## v1.1.0 (2019-07-24)
### New Features
#### Sync Waves
Sync waves feature allows executing a sync operation in a number of steps or waves. Within each synchronization phase (pre-sync, sync, post-sync) you can have one or more waves,
than allows you to ensure certain resources are healthy before subsequent resources are synced.
#### Optimized Interaction With Git
Argo CD needs to execute `git fetch` operation to access application manifests and `git ls-remote` to resolve ambiguous git revision. The `git ls-remote` is executed very frequently
and although the operation is very lightweight it adds unnecessary load on Git server and might cause performance issues. In v1.1 release, the application reconciliation process was
optimized which significantly reduced the number of Git requests. With v1.1 release, Argo CD should send 3x ~ 5x fewer Git requests.
#### User Defined Application Metadata
User-defined Application metadata enables the user to define a list of useful URLs for their specific application and expose those links on the UI
(e.g. reference tp a CI pipeline or an application-specific management tool). These links should provide helpful shortcuts that make easier to integrate Argo CD into existing
systems by making it easier to find other components inside and outside Argo CD.
### Deprecation Notice
* Kustomize v1.0 is deprecated and support will be removed in the Argo CD v1.2 release.
#### Enhancements
- Sync waves [#1544](https://github.com/argoproj/argo-cd/issues/1544)
- Adds Prune=false and IgnoreExtraneous options [#1629](https://github.com/argoproj/argo-cd/issues/1629)
- Forward Git credentials to config management plugins [#1628](https://github.com/argoproj/argo-cd/issues/1628)
- Improve Kustomize 2 parameters UI [#1609](https://github.com/argoproj/argo-cd/issues/1609)
- Adds `argocd logout` [#1210](https://github.com/argoproj/argo-cd/issues/1210)
- Make it possible to set Helm release name different from Argo CD app name. [#1066](https://github.com/argoproj/argo-cd/issues/1066)
- Add ability to specify system namespace during cluster add operation [#1661](https://github.com/argoproj/argo-cd/pull/1661)
- Make listener and metrics ports configurable [#1647](https://github.com/argoproj/argo-cd/pull/1647)
- Using SSH keys to authenticate kustomize bases from git [#827](https://github.com/argoproj/argo-cd/issues/827)
- Adds `argocd app sync APPNAME --async` [#1728](https://github.com/argoproj/argo-cd/issues/1728)
- Allow users to define app specific urls to expose in the UI [#1677](https://github.com/argoproj/argo-cd/issues/1677)
- Error view instead of blank page in UI [#1375](https://github.com/argoproj/argo-cd/issues/1375)
- Project Editor: Whitelisted Cluster Resources doesn't strip whitespace [#1693](https://github.com/argoproj/argo-cd/issues/1693)
- Eliminate unnecessary git interactions for top-level resource changes (#1919)
- Ability to rotate the bearer token used to manage external clusters (#1084)
#### Bug Fixes
- Project Editor: Whitelisted Cluster Resources doesn't strip whitespace [#1693](https://github.com/argoproj/argo-cd/issues/1693)
- \[ui small bug\] menu position outside block [#1711](https://github.com/argoproj/argo-cd/issues/1711)
- UI will crash when create application without destination namespace [#1701](https://github.com/argoproj/argo-cd/issues/1701)
- ArgoCD synchronization failed due to internal error [#1697](https://github.com/argoproj/argo-cd/issues/1697)
- Replicasets ordering is not stable on app tree view [#1668](https://github.com/argoproj/argo-cd/issues/1668)
- Stuck processor on App Controller after deleting application with incomplete operation [#1665](https://github.com/argoproj/argo-cd/issues/1665)
- Role edit page fails with JS error [#1662](https://github.com/argoproj/argo-cd/issues/1662)
- failed parsing on parameters with comma [#1660](https://github.com/argoproj/argo-cd/issues/1660)
- Handle nil obj when processing custom actions [#1700](https://github.com/argoproj/argo-cd/pull/1700)
- Account for missing fields in Rollout HealthStatus [#1699](https://github.com/argoproj/argo-cd/pull/1699)
- Sync operation unnecessary waits for a healthy state of all resources [#1715](https://github.com/argoproj/argo-cd/issues/1715)
- failed parsing on parameters with comma [#1660](https://github.com/argoproj/argo-cd/issues/1660)
- argocd app sync hangs when cluster is not configured (#1935)
- Do not allow app-of-app child app's Missing status to affect parent (#1954)
- Argo CD don't handle well k8s objects which size exceeds 1mb (#1685)
- Secret data not redacted in last-applied-configuration (#897)
- Running app actions requires only read privileges (#1827)
- UI should allow editing repo URL (#1763)
- Make status fields as optional fields (#1779)
- Use correct healthcheck for Rollout with empty steps list (#1776)
#### Other
- Add Prometheus metrics for git repo interactions (#1912)
- App controller should log additional information during app syncing (#1909)
- Make sure api server to repo server grpc calls have timeout (#1820)
- Forked tool processes should timeout (#1821)
- Add health check to the controller deployment (#1785)
#### Contributors
* [Aditya Gupta](https://github.com/AdityaGupta1)
* [Alex Collins](https://github.com/alexec)
* [Alex Matyushentsev](https://github.com/alexmt)
* [Danny Thomson](https://github.com/dthomson25)
* [jannfis](https://github.com/jannfis)
* [Jesse Suen](https://github.com/jessesuen)
* [Liviu Costea](https://github.com/lcostea)
* [narg95](https://github.com/narg95)
* [Simon Behar](https://github.com/simster7)
See also [milestone v1.1](https://github.com/argoproj/argo-cd/milestone/13)
## v1.0.0 (2019-05-16)
## v1.0.0
### New Features
#### Network View
A new way to visual application resources had been introduced to the Application Details page. The Network View visualizes connections between Ingresses, Services and Pods
based on ingress reference service, service's label selectors and labels. The new view is useful to understand the application traffic flow and troubleshot connectivity issues.
TODO
#### Custom Actions
Argo CD introduces Custom Resource Actions to allow users to provide their own Lua scripts to modify existing Kubernetes resources in their applications. These actions are exposed in the UI to allow easy, safe, and reliable changes to their resources. This functionality can be used to introduce functionality such as suspending and enabling a Kubernetes cronjob, continue a BlueGreen deployment with Argo Rollouts, or scaling a deployment.
#### UI Enhancements & Usability Enhancements
#### UI Enhancements
* New color palette intended to highlight unhealthily and out-of-sync resources more clearly.
* The health of more resources is displayed, so it easier to quickly zoom to unhealthy pods, replica-sets, etc.
* Resources that do not have health no longer appear to be healthy.
* Support for configuring Git repo credentials at a domain/org level
* Support for configuring requested OIDC provider scopes and enforced RBAC scopes
* Support for configuring monitored resources whitelist in addition to excluded resources
### Breaking Changes
@@ -948,12 +48,6 @@ Argo CD introduces Custom Resource Actions to allow users to provide their own L
* UI Enhancement Proposals Quick Wins #1274
* Update argocd-util import/export to support proper backup and restore (#1328)
* Whitelisting repos/clusters in projects should consider repo/cluster permissions #1432
* Adds support for configuring repo creds at a domain/org level. (#1332)
* Implement whitelist option analogous to `resource.exclusions` (#1490)
* Added ability to sync specific labels from the command line (#1241)
* Improve rendering app image information (#1552)
* Add liveness probe to repo server/api servers (#1546)
* Support configuring requested OIDC provider scopes and enforced RBAC scopes (#1471)
#### Bug Fixes
@@ -967,17 +61,7 @@ Argo CD introduces Custom Resource Actions to allow users to provide their own L
- Rollback UI is not showing correct ksonnet parameters in preview #1326
- See details of applications fails with "r.nodes is undefined" #1371
- UI fails to load custom actions is resource is not deployed #1502
- Unable to create app from private repo: x509: certificate signed by unknown authority (#1171)
- Fix hardcoded 'git' user in `util/git.NewClient` (#1555)
- Application controller becomes unresponsive (#1476)
- Load target resource using K8S if conversion fails (#1414)
- Can't ignore a non-existent pointer anymore (#1586)
- Impossible to sync to HEAD from UI if auto-sync is enabled (#1579)
- Application controller is unable to delete self-referenced app (#1570)
- Prevent reconciliation loop for self-managed apps (#1533)
- Controller incorrectly report health state of self managed application (#1557)
- Fix kustomize manifest generation crash is manifest has image without version (#1540)
- Supply resourceVersion to watch request to prevent reading of stale cache (#1605)
- Unable to create app from private repo: x509: certificate signed by unknown authority #1171
## v0.12.2 (2019-04-22)
@@ -1099,7 +183,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
@@ -1196,7 +280,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)
@@ -1374,7 +458,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)
@@ -1649,7 +733,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

@@ -1,19 +1,12 @@
ARG BASE_IMAGE=debian:10-slim
####################################################################################################
# Builder image
# Initial stage which pulls prepares build dependencies and CLI tooling we need for our final image
# Also used as the image in CI jobs so needs all dependencies
####################################################################################################
FROM golang:1.14.12 as builder
RUN echo 'deb http://deb.debian.org/debian buster-backports main' >> /etc/apt/sources.list
FROM golang:1.11.4 as builder
RUN apt-get update && apt-get install -y \
openssh-server \
nginx \
fcgiwrap \
git \
git-lfs \
make \
wget \
gcc \
@@ -23,110 +16,137 @@ RUN apt-get update && apt-get install -y \
WORKDIR /tmp
ADD hack/install.sh .
ADD hack/installers installers
ADD hack/tool-versions.sh .
# Install docker
ENV DOCKER_CHANNEL stable
ENV DOCKER_VERSION 18.09.1
RUN wget -O docker.tgz "https://download.docker.com/linux/static/${DOCKER_CHANNEL}/x86_64/docker-${DOCKER_VERSION}.tgz" && \
tar --extract --file docker.tgz --strip-components 1 --directory /usr/local/bin/ && \
rm docker.tgz
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
# Install dep
ENV DEP_VERSION=0.5.0
RUN wget https://github.com/golang/dep/releases/download/v${DEP_VERSION}/dep-linux-amd64 -O /usr/local/bin/dep && \
chmod +x /usr/local/bin/dep
# Install gometalinter
ENV GOMETALINTER_VERSION=2.0.12
RUN curl -sLo- https://github.com/alecthomas/gometalinter/releases/download/v${GOMETALINTER_VERSION}/gometalinter-${GOMETALINTER_VERSION}-linux-amd64.tar.gz | \
tar -xzC "$GOPATH/bin" --exclude COPYING --exclude README.md --strip-components 1 -f- && \
ln -s $GOPATH/bin/gometalinter $GOPATH/bin/gometalinter.v2
# Install packr
ENV PACKR_VERSION=1.21.9
RUN wget https://github.com/gobuffalo/packr/releases/download/v${PACKR_VERSION}/packr_${PACKR_VERSION}_linux_amd64.tar.gz && \
tar -vxf packr*.tar.gz -C /tmp/ && \
mv /tmp/packr /usr/local/bin/packr
# Install kubectl
# NOTE: keep the version synced with https://storage.googleapis.com/kubernetes-release/release/stable.txt
ENV KUBECTL_VERSION=1.14.0
RUN curl -L -o /usr/local/bin/kubectl -LO https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl && \
chmod +x /usr/local/bin/kubectl && \
kubectl version --client
# Install ksonnet
ENV KSONNET_VERSION=0.13.1
RUN wget https://github.com/ksonnet/ksonnet/releases/download/v${KSONNET_VERSION}/ks_${KSONNET_VERSION}_linux_amd64.tar.gz && \
tar -C /tmp/ -xf ks_${KSONNET_VERSION}_linux_amd64.tar.gz && \
mv /tmp/ks_${KSONNET_VERSION}_linux_amd64/ks /usr/local/bin/ks && \
ks version
# Install helm
ENV HELM_VERSION=2.12.1
RUN wget https://storage.googleapis.com/kubernetes-helm/helm-v${HELM_VERSION}-linux-amd64.tar.gz && \
tar -C /tmp/ -xf helm-v${HELM_VERSION}-linux-amd64.tar.gz && \
mv /tmp/linux-amd64/helm /usr/local/bin/helm && \
helm version --client
# Install kustomize
ENV KUSTOMIZE1_VERSION=1.0.11
RUN curl -L -o /usr/local/bin/kustomize1 https://github.com/kubernetes-sigs/kustomize/releases/download/v${KUSTOMIZE1_VERSION}/kustomize_${KUSTOMIZE1_VERSION}_linux_amd64 && \
chmod +x /usr/local/bin/kustomize1 && \
kustomize1 version
ENV KUSTOMIZE_VERSION=2.0.3
RUN curl -L -o /usr/local/bin/kustomize https://github.com/kubernetes-sigs/kustomize/releases/download/v${KUSTOMIZE_VERSION}/kustomize_${KUSTOMIZE_VERSION}_linux_amd64 && \
chmod +x /usr/local/bin/kustomize && \
kustomize version
# Install AWS IAM Authenticator
ENV AWS_IAM_AUTHENTICATOR_VERSION=0.4.0-alpha.1
RUN curl -L -o /usr/local/bin/aws-iam-authenticator https://github.com/kubernetes-sigs/aws-iam-authenticator/releases/download/${AWS_IAM_AUTHENTICATOR_VERSION}/aws-iam-authenticator_${AWS_IAM_AUTHENTICATOR_VERSION}_linux_amd64 && \
chmod +x /usr/local/bin/aws-iam-authenticator
# Install golangci-lint
RUN wget https://install.goreleaser.com/github.com/golangci/golangci-lint.sh && \
chmod +x ./golangci-lint.sh && \
./golangci-lint.sh -b $GOPATH/bin && \
golangci-lint linters
COPY .golangci.yml ${GOPATH}/src/dummy/.golangci.yml
RUN cd ${GOPATH}/src/dummy && \
touch dummy.go \
golangci-lint run
####################################################################################################
# Argo CD Base - used as the base for both the release and dev argocd images
####################################################################################################
FROM $BASE_IMAGE as argocd-base
USER root
RUN echo 'deb http://deb.debian.org/debian buster-backports main' >> /etc/apt/sources.list
FROM debian:9.5-slim as argocd-base
RUN groupadd -g 999 argocd && \
useradd -r -u 999 -g argocd argocd && \
mkdir -p /home/argocd && \
chown argocd:0 /home/argocd && \
chmod g=u /home/argocd && \
chmod g=u /etc/passwd && \
chown argocd:argocd /home/argocd && \
apt-get update && \
apt-get install -y git git-lfs python3-pip tini gpg && \
apt-get install -y git && \
apt-get clean && \
pip3 install awscli==1.18.80 && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
COPY hack/ssh_known_hosts /etc/ssh/ssh_known_hosts
COPY hack/git-ask-pass.sh /usr/local/bin/git-ask-pass.sh
COPY hack/gpg-wrapper.sh /usr/local/bin/gpg-wrapper.sh
COPY hack/git-verify-wrapper.sh /usr/local/bin/git-verify-wrapper.sh
COPY --from=builder /usr/local/bin/ks /usr/local/bin/ks
COPY --from=builder /usr/local/bin/helm2 /usr/local/bin/helm2
COPY --from=builder /usr/local/bin/helm /usr/local/bin/helm
COPY --from=builder /usr/local/bin/kubectl /usr/local/bin/kubectl
COPY --from=builder /usr/local/bin/kustomize1 /usr/local/bin/kustomize1
COPY --from=builder /usr/local/bin/kustomize /usr/local/bin/kustomize
# script to add current (possibly arbitrary) user to /etc/passwd at runtime
# (if it's not already there, to be openshift friendly)
COPY uid_entrypoint.sh /usr/local/bin/uid_entrypoint.sh
# support for mounting configuration from a configmap
RUN mkdir -p /app/config/ssh && \
touch /app/config/ssh/ssh_known_hosts && \
ln -s /app/config/ssh/ssh_known_hosts /etc/ssh/ssh_known_hosts
RUN mkdir -p /app/config/tls
RUN mkdir -p /app/config/gpg/source && \
mkdir -p /app/config/gpg/keys && \
chown argocd /app/config/gpg/keys && \
chmod 0700 /app/config/gpg/keys
COPY --from=builder /usr/local/bin/aws-iam-authenticator /usr/local/bin/aws-iam-authenticator
# workaround ksonnet issue https://github.com/ksonnet/ksonnet/issues/298
ENV USER=argocd
USER 999
USER argocd
WORKDIR /home/argocd
####################################################################################################
# Argo CD UI stage
####################################################################################################
FROM node:11.15.0 as argocd-ui
WORKDIR /src
ADD ["ui/package.json", "ui/yarn.lock", "./"]
RUN yarn install
ADD ["ui/", "."]
ARG ARGO_VERSION=latest
ENV ARGO_VERSION=$ARGO_VERSION
RUN NODE_ENV='production' yarn build
####################################################################################################
# Argo CD Build stage which performs the actual build of Argo CD binaries
####################################################################################################
FROM golang:1.14.12 as argocd-build
FROM golang:1.11.4 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-local 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-local && \
make CLI_NAME=argocd-windows-amd64.exe GOOS=windows cli-local \
; fi
####################################################################################################
# Final image
####################################################################################################
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

View File

@@ -3,4 +3,3 @@
####################################################################################################
FROM argocd-base
COPY argocd* /usr/local/bin/
COPY --from=argocd-ui ./src/dist/app /shared/app

1686
Gopkg.lock generated Normal file

File diff suppressed because it is too large Load Diff

68
Gopkg.toml Normal file
View File

@@ -0,0 +1,68 @@
# 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.1.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"
[[constraint]]
branch = "release-1.12"
name = "k8s.io/api"
[[constraint]]
branch = "release-1.12"
name = "k8s.io/code-generator"
[[constraint]]
branch = "release-9.0"
name = "k8s.io/client-go"
[[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"
[[override]]
revision = "master"
name = "k8s.io/kube-openapi"

377
Makefile
View File

@@ -1,108 +1,25 @@
PACKAGE=github.com/argoproj/argo-cd/common
PACKAGE=github.com/argoproj/argo-cd
CURRENT_DIR=$(shell pwd)
DIST_DIR=${CURRENT_DIR}/dist
CLI_NAME=argocd
HOST_OS:=$(shell go env GOOS)
HOST_ARCH:=$(shell go env GOARCH)
VERSION=$(shell cat ${CURRENT_DIR}/VERSION)
BUILD_DATE=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
GIT_COMMIT=$(shell git rev-parse HEAD)
GIT_TAG=$(shell if [ -z "`git status --porcelain`" ]; then git describe --exact-match --tags HEAD 2>/dev/null; fi)
GIT_TREE_STATE=$(shell if [ -z "`git status --porcelain`" ]; then echo "clean" ; else echo "dirty"; fi)
PACKR_CMD=$(shell if [ "`which packr`" ]; then echo "packr"; else echo "go run github.com/gobuffalo/packr/packr"; fi)
VOLUME_MOUNT=$(shell if test "$(go env GOOS)" = "darwin"; then echo ":delegated"; elif test selinuxenabled; then echo ":delegated"; else echo ""; fi)
GOPATH?=$(shell if test -x `which go`; then go env GOPATH; else echo "$(HOME)/go"; fi)
GOCACHE?=$(HOME)/.cache/go-build
DOCKER_SRCDIR?=$(GOPATH)/src
DOCKER_WORKDIR?=/go/src/github.com/argoproj/argo-cd
ARGOCD_PROCFILE?=Procfile
# Configuration for building argocd-test-tools image
TEST_TOOLS_NAMESPACE?=
TEST_TOOLS_IMAGE=argocd-test-tools
TEST_TOOLS_TAG?=latest
ifdef TEST_TOOLS_NAMESPACE
TEST_TOOLS_PREFIX=${TEST_TOOLS_NAMESPACE}/
endif
# You can change the ports where ArgoCD components will be listening on by
# setting the appropriate environment variables before running make.
ARGOCD_E2E_APISERVER_PORT?=8080
ARGOCD_E2E_REPOSERVER_PORT?=8081
ARGOCD_E2E_REDIS_PORT?=6379
ARGOCD_E2E_DEX_PORT?=5556
ARGOCD_E2E_YARN_HOST?=localhost
ARGOCD_IN_CI?=false
ARGOCD_TEST_E2E?=true
ARGOCD_LINT_GOGC?=20
# Runs any command in the argocd-test-utils container in server mode
# Server mode container will start with uid 0 and drop privileges during runtime
define run-in-test-server
docker run --rm -it \
--name argocd-test-server \
-e USER_ID=$(shell id -u) \
-e HOME=/home/user \
-e GOPATH=/go \
-e GOCACHE=/tmp/go-build-cache \
-e ARGOCD_IN_CI=$(ARGOCD_IN_CI) \
-e ARGOCD_E2E_TEST=$(ARGOCD_E2E_TEST) \
-e ARGOCD_E2E_YARN_HOST=$(ARGOCD_E2E_YARN_HOST) \
-v ${DOCKER_SRCDIR}:/go/src${VOLUME_MOUNT} \
-v ${GOPATH}/pkg/mod:/go/pkg/mod${VOLUME_MOUNT} \
-v ${GOCACHE}:/tmp/go-build-cache${VOLUME_MOUNT} \
-v ${HOME}/.kube:/home/user/.kube${VOLUME_MOUNT} \
-v /tmp:/tmp${VOLUME_MOUNT} \
-w ${DOCKER_WORKDIR} \
-p ${ARGOCD_E2E_APISERVER_PORT}:8080 \
-p 4000:4000 \
$(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE):$(TEST_TOOLS_TAG) \
bash -c "$(1)"
endef
# Runs any command in the argocd-test-utils container in client mode
define run-in-test-client
docker run --rm -it \
--name argocd-test-client \
-u $(shell id -u) \
-e HOME=/home/user \
-e GOPATH=/go \
-e ARGOCD_E2E_K3S=$(ARGOCD_E2E_K3S) \
-e GOCACHE=/tmp/go-build-cache \
-e ARGOCD_LINT_GOGC=$(ARGOCD_LINT_GOGC) \
-v ${DOCKER_SRCDIR}:/go/src${VOLUME_MOUNT} \
-v ${GOPATH}/pkg/mod:/go/pkg/mod${VOLUME_MOUNT} \
-v ${GOCACHE}:/tmp/go-build-cache${VOLUME_MOUNT} \
-v ${HOME}/.kube:/home/user/.kube${VOLUME_MOUNT} \
-v /tmp:/tmp${VOLUME_MOUNT} \
-w ${DOCKER_WORKDIR} \
$(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE):$(TEST_TOOLS_TAG) \
bash -c "$(1)"
endef
#
define exec-in-test-server
docker exec -it -u $(shell id -u) -e ARGOCD_E2E_K3S=$(ARGOCD_E2E_K3S) argocd-test-server $(1)
endef
PACKR_CMD=$(shell if [ "`which packr`" ]; then echo "packr"; else echo "go run vendor/github.com/gobuffalo/packr/packr/main.go"; fi)
TEST_CMD=$(shell [ "`which gotestsum`" != "" ] && echo gotestsum -- || echo go test)
PATH:=$(PATH):$(PWD)/hack
# docker image publishing options
DOCKER_PUSH?=false
IMAGE_NAMESPACE?=
DOCKER_PUSH=false
IMAGE_TAG=latest
# perform static compilation
STATIC_BUILD?=true
STATIC_BUILD=true
# build development images
DEV_IMAGE?=false
ARGOCD_GPG_ENABLED?=true
ARGOCD_E2E_APISERVER_PORT?=8080
DEV_IMAGE=false
override LDFLAGS += \
-X ${PACKAGE}.version=${VERSION} \
@@ -117,8 +34,6 @@ endif
ifneq (${GIT_TAG},)
IMAGE_TAG=${GIT_TAG}
LDFLAGS += -X ${PACKAGE}.gitTag=${GIT_TAG}
else
IMAGE_TAG?=latest
endif
ifeq (${DOCKER_PUSH},true)
@@ -134,86 +49,50 @@ 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/
.PHONY: codegen
codegen: test-tools-image
$(call run-in-test-client,make codegen-local)
codegen: protogen clientgen openapigen
.PHONY: cli
cli: test-tools-image
$(call run-in-test-client, GOOS=${HOST_OS} GOARCH=${HOST_ARCH} make cli-local)
.PHONY: cli-local
cli-local: clean-debug
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
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: manifests-local
manifests-local:
./hack/update-manifests.sh
CGO_ENABLED=0 go build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-util ./cmd/argocd-util
.PHONY: manifests
manifests: test-tools-image
$(call run-in-test-client,make manifests-local IMAGE_NAMESPACE='${IMAGE_NAMESPACE}' IMAGE_TAG='${IMAGE_TAG}')
manifests:
./hack/update-manifests.sh
# NOTE: we use packr to do the build instead of go, since we embed swagger files and policy.csv
# files into the go binary
.PHONY: server
server: clean-debug
CGO_ENABLED=0 ${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-server ./cmd/argocd-server
.PHONY: repo-server
repo-server:
CGO_ENABLED=0 ${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-repo-server ./cmd/argocd-repo-server
CGO_ENABLED=0 go build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-repo-server ./cmd/argocd-repo-server
.PHONY: controller
controller:
@@ -221,24 +100,21 @@ 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)
# The "dev" image builds the binaries from the users desktop environment (instead of in Docker)
# which speeds up builds. Dockerfile.dev needs to be copied into dist to perform the build, since
# the dist directory is under .dockerignore.
IMAGE_TAG="dev-$(shell git describe --always --dirty)"
image: packr
docker build -t argocd-base --target argocd-base .
docker build -t argocd-ui --target argocd-ui .
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-server ./cmd/argocd-server
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-application-controller ./cmd/argocd-application-controller
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 dist/packr build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-repo-server ./cmd/argocd-repo-server
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
@@ -247,138 +123,38 @@ 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
docker push $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG)
.PHONY: mod-download
mod-download: test-tools-image
$(call run-in-test-client,go mod download)
.PHONY: dep-ensure
dep-ensure:
dep ensure -no-vendor
.PHONY: mod-download-local
mod-download-local:
go mod download
.PHONY: mod-vendor
mod-vendor: test-tools-image
$(call run-in-test-client,go mod vendor)
.PHONY: mod-vendor-local
mod-vendor-local: mod-download-local
go mod vendor
# Deprecated - replace by install-local-tools
.PHONY: install-lint-tools
install-lint-tools:
./hack/install.sh lint-tools
# Run linter on the code
.PHONY: lint
lint: test-tools-image
$(call run-in-test-client,make lint-local)
lint:
golangci-lint run --fix
# Run linter on the code (local version)
.PHONY: lint-local
lint-local:
golangci-lint --version
# NOTE: If you get a "Killed" OOM message, try reducing the value of GOGC
# See https://github.com/golangci/golangci-lint#memory-usage-of-golangci-lint
GOGC=$(ARGOCD_LINT_GOGC) GOMAXPROCS=2 golangci-lint run --fix --verbose --timeout 300s
.PHONY: lint-ui
lint-ui: test-tools-image
$(call run-in-test-client,make lint-ui-local)
.PHONY: lint-ui-local
lint-ui-local:
cd ui && yarn lint
# Build all Go code
.PHONY: build
build: test-tools-image
mkdir -p $(GOCACHE)
$(call run-in-test-client, make build-local)
build:
go build `go list ./... | grep -v resource_customizations`
# 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: test-tools-image
mkdir -p $(GOCACHE)
$(call run-in-test-client,make TEST_MODULE=$(TEST_MODULE) test-local)
test:
$(TEST_CMD) -covermode=count -coverprofile=coverage.out `go list ./... | grep -v "github.com/argoproj/argo-cd/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: cli
$(TEST_CMD) -v -failfast -timeout 20m ./test/e2e
# Run the E2E test suite (local version)
.PHONY: test-e2e-local
test-e2e-local: cli-local
# NO_PROXY ensures all tests don't go out through a proxy if one is configured on the test system
export GO111MODULE=off
ARGOCD_GPG_ENABLED=true NO_PROXY=* ./hack/test.sh -timeout 20m -v ./test/e2e
# Spawns a shell in the test server container for debugging purposes
debug-test-server: test-tools-image
$(call run-in-test-server,/bin/bash)
# Spawns a shell in the test client container for debugging purposes
debug-test-client: test-tools-image
$(call run-in-test-client,/bin/bash)
# Starts e2e server in a container
.PHONY: start-e2e
start-e2e: test-tools-image
docker version
mkdir -p ${GOCACHE}
$(call run-in-test-server,make ARGOCD_PROCFILE=test/container/Procfile start-e2e-local)
# Starts e2e server locally (or within a container)
.PHONY: start-e2e-local
start-e2e-local:
start-e2e: cli
killall goreman || true
kubectl create ns argocd-e2e || true
kubectl config set-context --current --namespace=argocd-e2e
kubens argocd-e2e
kustomize build test/manifests/base | kubectl apply -f -
# Create GPG keys and source directories
if test -d /tmp/argo-e2e/app/config/gpg; then rm -rf /tmp/argo-e2e/app/config/gpg/*; fi
mkdir -p /tmp/argo-e2e/app/config/gpg/keys && chmod 0700 /tmp/argo-e2e/app/config/gpg/keys
mkdir -p /tmp/argo-e2e/app/config/gpg/source && chmod 0700 /tmp/argo-e2e/app/config/gpg/source
if test "$(USER_ID)" != ""; then chown -R "$(USER_ID)" /tmp/argo-e2e; fi
# set paths for locally managed ssh known hosts and tls certs data
ARGOCD_SSH_DATA_PATH=/tmp/argo-e2e/app/config/ssh \
ARGOCD_TLS_DATA_PATH=/tmp/argo-e2e/app/config/tls \
ARGOCD_GPG_DATA_PATH=/tmp/argo-e2e/app/config/gpg/source \
ARGOCD_GNUPGHOME=/tmp/argo-e2e/app/config/gpg/keys \
ARGOCD_GPG_ENABLED=true \
ARGOCD_E2E_DISABLE_AUTH=false \
ARGOCD_ZJWT_FEATURE_FLAG=always \
ARGOCD_IN_CI=$(ARGOCD_IN_CI) \
ARGOCD_E2E_TEST=true \
goreman -f $(ARGOCD_PROCFILE) start
make start
# Cleans VSCode debug.test files from sub-dirs to prevent them from being included in packr boxes
.PHONY: clean-debug
@@ -390,33 +166,13 @@ clean: clean-debug
-rm -rf ${CURRENT_DIR}/dist
.PHONY: start
start: test-tools-image
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
start:
killall goreman || true
kubectl create ns argocd || true
rm -rf /tmp/argocd-local
mkdir -p /tmp/argocd-local
mkdir -p /tmp/argocd-local/gpg/keys && chmod 0700 /tmp/argocd-local/gpg/keys
mkdir -p /tmp/argocd-local/gpg/source
ARGOCD_ZJWT_FEATURE_FLAG=always \
ARGOCD_IN_CI=false \
ARGOCD_GPG_ENABLED=true \
ARGOCD_E2E_TEST=false \
goreman -f $(ARGOCD_PROCFILE) start ${ARGOCD_START}
kubens argocd
goreman start
# Runs pre-commit validation with the virtualized toolchain
.PHONY: pre-commit
pre-commit: codegen build lint test
# Runs pre-commit validation with the local toolchain
.PHONY: pre-commit-local
pre-commit-local: codegen-local build-local lint-local test-local
pre-commit: dep-ensure codegen build lint test
.PHONY: release-precheck
release-precheck: manifests
@@ -425,65 +181,4 @@ release-precheck: manifests
@if [ "$(GIT_TAG)" != "v`cat VERSION`" ]; then echo 'VERSION does not match git tag'; exit 1; fi
.PHONY: release
release: pre-commit release-precheck image release-cli
.PHONY: build-docs
build-docs:
mkdocs build
.PHONY: serve-docs
serve-docs:
mkdocs serve
.PHONY: lint-docs
lint-docs:
# https://github.com/dkhamsing/awesome_bot
find docs -name '*.md' -exec grep -l http {} + | xargs docker run --rm -v $(PWD):/mnt:ro dkhamsing/awesome_bot -t 3 --allow-dupe --allow-redirect --white-list `cat white-list | grep -v "#" | tr "\n" ','` --skip-save-results --
.PHONY: publish-docs
publish-docs: lint-docs
mkdocs gh-deploy
# Verify that kubectl can connect to your K8s cluster from Docker
.PHONY: verify-kube-connect
verify-kube-connect: test-tools-image
$(call run-in-test-client,kubectl version)
# Show the Go version of local and virtualized environments
.PHONY: show-go-version
show-go-version: test-tools-image
@echo -n "Local Go version: "
@go version
@echo -n "Docker Go version: "
$(call run-in-test-client,go version)
# Installs all tools required to build and test ArgoCD locally
.PHONY: install-tools-local
install-tools-local: install-test-tools-local install-codegen-tools-local install-go-tools-local
# Installs all tools required for running unit & end-to-end tests (Linux packages)
.PHONY: install-test-tools-local
install-test-tools-local:
sudo ./hack/install.sh packr-linux
sudo ./hack/install.sh kubectl-linux
sudo ./hack/install.sh kustomize-linux
sudo ./hack/install.sh ksonnet-linux
sudo ./hack/install.sh helm2-linux
sudo ./hack/install.sh helm-linux
# Installs all tools required for running codegen (Linux packages)
.PHONY: install-codegen-tools-local
install-codegen-tools-local:
sudo ./hack/install.sh codegen-tools
# Installs all tools required for running codegen (Go packages)
.PHONY: install-go-tools-local
install-go-tools-local:
./hack/install.sh codegen-go-tools
.PHONY: dep-ui
dep-ui: test-tools-image
$(call run-in-test-client,make dep-ui-local)
dep-ui-local:
cd ui && yarn install
release: release-precheck pre-commit image release-cli

6
OWNERS
View File

@@ -3,10 +3,6 @@ owners:
- jessesuen
approvers:
- alexec
- alexmt
- dthomson25
- jannfis
- jessesuen
- mayzhang2000
- rachelwang20
- merenbach

View File

@@ -1,8 +1,5 @@
controller: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} go run ./cmd/argocd-application-controller/main.go --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081}"
api-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} go run ./cmd/argocd-server/main.go --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --disable-auth=${ARGOCD_E2E_DISABLE_AUTH:-'true'} --insecure --dex-server http://localhost:${ARGOCD_E2E_DEX_PORT:-5556} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --port ${ARGOCD_E2E_APISERVER_PORT:-8080} --staticassets ui/dist/app"
dex: sh -c "go run github.com/argoproj/argo-cd/cmd/argocd-util gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p ${ARGOCD_E2E_DEX_PORT:-5556}:${ARGOCD_E2E_DEX_PORT:-5556} -v `pwd`/dist/dex.yaml:/dex.yaml ghcr.io/dexidp/dex:v2.27.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.10-alpine --save "" --appendonly no --port ${ARGOCD_E2E_REDIS_PORT:-6379}
repo-server: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_GNUPGHOME=${ARGOCD_GNUPGHOME:-/tmp/argocd-local/gpg/keys} ARGOCD_GPG_DATA_PATH=${ARGOCD_GPG_DATA_PATH:-/tmp/argocd-local/gpg/source} ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} go run ./cmd/argocd-repo-server/main.go --loglevel debug --port ${ARGOCD_E2E_REPOSERVER_PORT:-8081} --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379}"
ui: sh -c 'cd ui && ${ARGOCD_E2E_YARN_CMD:-yarn} start'
git-server: test/fixture/testrepos/start-git.sh
dev-mounter: [[ "$ARGOCD_E2E_TEST" != "true" ]] && go run hack/dev-mounter/main.go --configmap argocd-ssh-known-hosts-cm=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} --configmap argocd-tls-certs-cm=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} --configmap argocd-gpg-keys-cm=${ARGOCD_GPG_DATA_PATH:-/tmp/argocd-local/gpg/source}
controller: sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true go run ./cmd/argocd-application-controller/main.go --loglevel debug --redis localhost:6379 --repo-server localhost: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:6379 --disable-auth --insecure --dex-server http://localhost:5556 --repo-server localhost:8081 --staticassets ../argo-cd-ui/dist/app"
repo-server: sh -c "FORCE_LOG_COLORS=1 go run ./cmd/argocd-repo-server/main.go --loglevel debug --redis localhost:6379"
dex: sh -c "go run ./cmd/argocd-util/main.go gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p 5556:5556 -v `pwd`/dist/dex.yaml:/dex.yaml quay.io/dexidp/dex:v2.14.0 serve /dex.yaml"
redis: docker run --rm --name argocd-redis -i -p 6379:6379 redis:5.0.3-alpine --save ""--appendonly no

View File

@@ -1,7 +1,5 @@
[![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)
# Argo CD - Declarative Continuous Delivery for Kubernetes
@@ -13,28 +11,22 @@ 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. [Intuit](https://www.intuit.com/)
2. [KompiTech GmbH](https://www.kompitech.com/)
3. [Yieldlab](https://www.yieldlab.de/)
4. [Ticketmaster](https://ticketmaster.com)
5. [CyberAgent](https://www.cyberagent.co.jp/en/)
6. [OpenSaaS Studio](https://opensaas.studio)
7. [Riskified](https://www.riskified.com/)
## Documentation
To learn more about Argo CD [go to the complete documentation](https://argoproj.github.io/argo-cd/).
Check live demo at https://cd.apps.argoproj.io/.
## Community Blogs and Presentations
1. [Tutorial: Everything You Need To Become A GitOps Ninja](https://www.youtube.com/watch?v=r50tRQjisxw) 90m tutorial on GitOps and Argo CD.
1. [Comparison of Argo CD, Spinnaker, Jenkins X, and Tekton](https://www.inovex.de/blog/spinnaker-vs-argo-cd-vs-tekton-vs-jenkins-x/)
1. [Simplify and Automate Deployments Using GitOps with IBM Multicloud Manager 3.1.2](https://medium.com/ibm-cloud/simplify-and-automate-deployments-using-gitops-with-ibm-multicloud-manager-3-1-2-4395af317359)
1. [GitOps for Kubeflow using Argo CD](https://v0-6.kubeflow.org/docs/use-cases/gitops-for-kubeflow/)
1. [GitOps Toolsets on Kubernetes with CircleCI and Argo CD](https://www.digitalocean.com/community/tutorials/webinar-series-gitops-tool-sets-on-kubernetes-with-circleci-and-argo-cd)
1. [Simplify and Automate Deployments Using GitOps with IBM Multicloud Manager](https://www.ibm.com/blogs/bluemix/2019/02/simplify-and-automate-deployments-using-gitops-with-ibm-multicloud-manager-3-1-2/)
1. [CI/CD in Light Speed with K8s and Argo CD](https://www.youtube.com/watch?v=OdzH82VpMwI&feature=youtu.be)
1. [Machine Learning as Code](https://www.youtube.com/watch?v=VXrGp5er1ZE&t=0s&index=135&list=PLj6h78yzYM2PZf9eA7bhWnIh_mK1vyOfU). Among other things, describes how Kubeflow uses Argo CD to implement GitOPs for ML
1. [Argo CD - GitOps Continuous Delivery for Kubernetes](https://www.youtube.com/watch?v=aWDIQMbp1cc&feature=youtu.be&t=1m4s)
1. [Introduction to Argo CD : Kubernetes DevOps CI/CD](https://www.youtube.com/watch?v=2WSJF7d8dUg&feature=youtu.be)
1. [GitOps Deployment and Kubernetes - using ArgoCD](https://medium.com/riskified-technology/gitops-deployment-and-kubernetes-f1ab289efa4b)

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,80 +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. [BCDevExchange DevOps Platform](https://bcdevexchange.org/DevOpsPlatform)
1. [Beat](https://thebeat.co/en/)
1. [Beez Innovation Labs](https://www.beezlabs.com/)
1. [BioBox Analytics](https://biobox.io)
1. [CARFAX](https://www.carfax.com)
1. [Celonis](https://www.celonis.com/)
1. [Codility](https://www.codility.com/)
1. [Commonbond](https://commonbond.co/)
1. [CyberAgent](https://www.cyberagent.co.jp/en/)
1. [Cybozu](https://cybozu-global.com)
1. [D2iQ](https://www.d2iq.com)
1. [EDF Renewables](https://www.edf-re.com/)
1. [Electronic Arts Inc. ](https://www.ea.com)
1. [Elium](https://www.elium.com)
1. [END.](https://www.endclothing.com/)
1. [Fave](https://myfave.com)
1. [Future PLC](https://www.futureplc.com/)
1. [Garner](https://www.garnercorp.com)
1. [GMETRI](https://gmetri.com/)
1. [Greenpass](https://www.greenpass.com.br/)
1. [Healy](https://www.healyworld.net)
1. [hipages](https://hipages.com.au/)
1. [Honestbank](https://honestbank.com)
1. [InsideBoard](https://www.insideboard.com)
1. [Intuit](https://www.intuit.com/)
1. [KintoHub](https://www.kintohub.com/)
1. [KompiTech GmbH](https://www.kompitech.com/)
1. [LINE](https://linecorp.com/en/)
1. [Lytt](https://www.lytt.co/)
1. [Major League Baseball](https://mlb.com)
1. [Mambu](https://www.mambu.com/)
1. [Max Kelsen](https://www.maxkelsen.com/)
1. [Mirantis](https://mirantis.com/)
1. [Money Forward](https://corp.moneyforward.com/en/)
1. [MOO Print](https://www.moo.com/)
1. [OpenSaaS Studio](https://opensaas.studio)
1. [Optoro](https://www.optoro.com/)
1. [Peloton Interactive](https://www.onepeloton.com/)
1. [Pipefy](https://www.pipefy.com/)
1. [Prudential](https://prudential.com.sg)
1. [PUBG](https://www.pubg.com)
1. [QuintoAndar](https://quintoandar.com.br)
1. [Red Hat](https://www.redhat.com/)
1. [Robotinfra](https://www.robotinfra.com)
1. [Riskified](https://www.riskified.com/)
1. [Saildrone](https://www.saildrone.com/)
1. [Saloodo! GmbH](https://www.saloodo.com)
1. [Swisscom](https://www.swisscom.ch)
1. [Swissquote](https://github.com/swissquote)
1. [Syncier](https://syncier.com/)
1. [Tesla](https://tesla.com/)
1. [ThousandEyes](https://www.thousandeyes.com/)
1. [Ticketmaster](https://ticketmaster.com)
1. [Tiger Analytics](https://www.tigeranalytics.com/)
1. [Twilio SendGrid](https://sendgrid.com)
1. [tZERO](https://www.tzero.com/)
1. [UBIO](https://ub.io/)
1. [Universidad Mesoamericana](https://www.umes.edu.gt/)
1. [Viaduct](https://www.viaduct.ai/)
1. [Volvo Cars](https://www.volvocars.com/)
1. [VSHN - The DevOps Company](https://vshn.ch/)
1. [Walkbase](https://www.walkbase.com/)
1. [Whitehat Berlin](https://whitehat.berlin) by Guido Maria Serra +Fenaroli
1. [Yieldlab](https://www.yieldlab.de/)
1. [MTN Group](https://www.mtn.com/)
1. [Moengage](https://www.moengage.com/)
1. [LexisNexis](https://www.lexisnexis.com/)
1. [PayPay](https://paypay.ne.jp/)

View File

@@ -1 +1 @@
1.7.14
1.0.2

View File

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

Before

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -7,22 +7,14 @@
# p, <user/group>, <resource>, <action>, <object>
p, role:readonly, applications, get, */*, allow
p, role:readonly, certificates, get, *, allow
p, role:readonly, clusters, get, *, allow
p, role:readonly, repositories, get, *, allow
p, role:readonly, projects, get, *, allow
p, role:readonly, accounts, get, *, allow
p, role:readonly, gpgkeys, get, *, allow
p, role:admin, applications, create, */*, allow
p, role:admin, applications, update, */*, allow
p, role:admin, applications, delete, */*, allow
p, role:admin, applications, sync, */*, allow
p, role:admin, applications, override, */*, allow
p, role:admin, applications, action/*, */*, allow
p, role:admin, certificates, create, *, allow
p, role:admin, certificates, update, *, allow
p, role:admin, certificates, delete, *, allow
p, role:admin, clusters, create, *, allow
p, role:admin, clusters, update, *, allow
p, role:admin, clusters, delete, *, allow
@@ -32,9 +24,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
p, role:admin, gpgkeys, create, *, allow
p, role:admin, gpgkeys, delete, *, allow
g, role:admin, role:readonly
g, admin, role:admin
1 # Built-in policy which defines two roles: role:readonly and role:admin,
7 # p, <user/group>, <resource>, <action>, <object>
8 p, role:readonly, applications, get, */*, allow
9 p, role:readonly, certificates, get, *, allow p, role:readonly, clusters, get, *, allow
p, role:readonly, clusters, get, *, allow
10 p, role:readonly, repositories, get, *, allow
11 p, role:readonly, projects, get, *, allow
12 p, role:readonly, accounts, get, *, allow p, role:admin, applications, create, */*, allow
p, role:readonly, gpgkeys, get, *, allow
p, role:admin, applications, create, */*, allow
13 p, role:admin, applications, update, */*, allow
14 p, role:admin, applications, delete, */*, allow
15 p, role:admin, applications, sync, */*, allow
16 p, role:admin, applications, override, */*, allow p, role:admin, clusters, create, *, allow
17 p, role:admin, applications, action/*, */*, allow p, role:admin, clusters, update, *, allow
p, role:admin, certificates, create, *, allow
p, role:admin, certificates, update, *, allow
p, role:admin, certificates, delete, *, allow
p, role:admin, clusters, create, *, allow
p, role:admin, clusters, update, *, allow
18 p, role:admin, clusters, delete, *, allow
19 p, role:admin, repositories, create, *, allow
20 p, role:admin, repositories, update, *, allow
24 p, role:admin, projects, delete, *, allow
25 p, role:admin, accounts, update, *, allow g, role:admin, role:readonly
26 p, role:admin, gpgkeys, create, *, allow g, admin, role:admin
p, role:admin, gpgkeys, delete, *, allow
g, role:admin, role:readonly
g, admin, role:admin
27
28
29

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)

File diff suppressed because it is too large Load Diff

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"
@@ -19,18 +15,17 @@ import (
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
// load the oidc plugin (required to authenticate with OpenID Connect).
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
// load the azure plugin (required to authenticate with AKS clusters).
_ "k8s.io/client-go/plugin/pkg/client/auth/azure"
argocd "github.com/argoproj/argo-cd"
"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/reposerver"
"github.com/argoproj/argo-cd/util/cache"
"github.com/argoproj/argo-cd/util/cli"
"github.com/argoproj/argo-cd/util/settings"
"github.com/argoproj/argo-cd/util/stats"
)
const (
@@ -46,28 +41,23 @@ func newCommand() *cobra.Command {
appResyncPeriod int64
repoServerAddress string
repoServerTimeoutSeconds int
selfHealTimeoutSeconds int
statusProcessors int
operationProcessors int
logFormat string
logLevel string
glogLevel int
metricsPort int
kubectlParallelismLimit int64
cacheSrc func() (*appstatecache.Cache, error)
redisClient *redis.Client
cacheSrc func() (*cache.Cache, error)
)
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)
@@ -76,7 +66,7 @@ func newCommand() *cobra.Command {
errors.CheckError(err)
resyncDuration := time.Duration(appResyncPeriod) * time.Second
repoClientset := apiclient.NewRepoServerClientset(repoServerAddress, repoServerTimeoutSeconds)
repoClientset := reposerver.NewRepoServerClientset(repoServerAddress, repoServerTimeoutSeconds)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -84,7 +74,6 @@ func newCommand() *cobra.Command {
errors.CheckError(err)
settingsMgr := settings.NewSettingsManager(ctx, kubeClient, namespace)
kubectl := &kube.KubectlCmd{}
appController, err := controller.NewApplicationController(
namespace,
settingsMgr,
@@ -92,16 +81,10 @@ func newCommand() *cobra.Command {
appClient,
repoClientset,
cache,
kubectl,
resyncDuration,
time.Duration(selfHealTimeoutSeconds)*time.Second,
metricsPort,
kubectlParallelismLimit)
resyncDuration)
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)", argocd.GetVersion(), namespace)
stats.RegisterStackDumper()
stats.StartStatsTicker(10 * time.Minute)
stats.RegisterHeapDumper("memprofile")
@@ -119,15 +102,9 @@ 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 = cache.AddCacheFlagsToCmd(&command)
return &command
}

View File

@@ -7,52 +7,37 @@ import (
"os"
"time"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
"github.com/argoproj/pkg/stats"
"github.com/go-redis/redis"
"github.com/prometheus/client_golang/prometheus/promhttp"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
argocd "github.com/argoproj/argo-cd"
"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/cache"
"github.com/argoproj/argo-cd/util/cli"
"github.com/argoproj/argo-cd/util/gpg"
"github.com/argoproj/argo-cd/util/git"
"github.com/argoproj/argo-cd/util/stats"
"github.com/argoproj/argo-cd/util/tls"
)
const (
// CLIName is the name of the CLI
cliName = "argocd-repo-server"
gnuPGSourcePath = "/app/config/gpg/source"
cliName = "argocd-repo-server"
)
func getGnuPGSourcePath() string {
if path := os.Getenv("ARGOCD_GPG_DATA_PATH"); path != "" {
return path
} else {
return gnuPGSourcePath
}
}
func newCommand() *cobra.Command {
var (
logFormat string
logLevel string
parallelismLimit int64
listenPort int
metricsPort int
cacheSrc func() (*reposervercache.Cache, error)
cacheSrc func() (*cache.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()
@@ -61,32 +46,16 @@ func newCommand() *cobra.Command {
cache, err := cacheSrc()
errors.CheckError(err)
metricsServer := metrics.NewMetricsServer()
cacheutil.CollectMetrics(redisClient, metricsServer)
server, err := reposerver.NewServer(metricsServer, cache, tlsConfigCustomizer, parallelismLimit)
server, err := reposerver.NewServer(git.NewFactory(), cache, tlsConfigCustomizer, parallelismLimit)
errors.CheckError(err)
grpc := server.CreateGRPC()
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", listenPort))
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", common.PortRepoServer))
errors.CheckError(err)
http.Handle("/metrics", metricsServer.GetHandler())
go func() { errors.CheckError(http.ListenAndServe(fmt.Sprintf(":%d", metricsPort), nil)) }()
http.Handle("/metrics", promhttp.Handler())
go func() { errors.CheckError(http.ListenAndServe(fmt.Sprintf(":%d", common.PortRepoServerMetrics), nil)) }()
if gpg.IsGPGEnabled() {
log.Infof("Initializing GnuPG keyring at %s", common.GetGnuPGHomePath())
err = gpg.InitializeGnuPG()
errors.CheckError(err)
log.Infof("Populating GnuPG keyring with keys from %s", getGnuPGSourcePath())
added, removed, err := gpg.SyncKeyRingFromDirectory(getGnuPGSourcePath())
errors.CheckError(err)
log.Infof("Loaded %d (and removed %d) keys from keyring", len(added), len(removed))
go func() { errors.CheckError(reposerver.StartGPGWatcher(getGnuPGSourcePath())) }()
}
log.Infof("argocd-repo-server %s serving on %s", common.GetVersion(), listener.Addr())
log.Infof("argocd-repo-server %s serving on %s", argocd.GetVersion(), listener.Addr())
stats.RegisterStackDumper()
stats.StartStatsTicker(10 * time.Minute)
stats.RegisterHeapDumper("memprofile")
@@ -96,15 +65,10 @@ 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 = cache.AddCacheFlagsToCmd(&command)
return &command
}

View File

@@ -4,77 +4,48 @@ 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/reposerver"
"github.com/argoproj/argo-cd/server"
servercache "github.com/argoproj/argo-cd/server/cache"
"github.com/argoproj/argo-cd/util/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
enableGZip bool
tlsConfigCustomizerSrc func() (tls.ConfigCustomizer, error)
cacheSrc func() (*servercache.Cache, error)
frameOptions string
insecure bool
logLevel string
glogLevel int
clientConfig clientcmd.ClientConfig
staticAssetsDir string
baseHRef string
repoServerAddress string
dexServerAddress string
disableAuth bool
tlsConfigCustomizerSrc func() (tls.ConfigCustomizer, error)
cacheSrc func() (*cache.Cache, error)
)
var command = &cobra.Command{
Use: cliName,
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)
@@ -85,42 +56,21 @@ 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)
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
}
appclientset := appclientset.NewForConfigOrDie(config)
repoclientset := reposerver.NewRepoServerClientset(repoServerAddress, 0)
argoCDOpts := server.ArgoCDServerOpts{
Insecure: insecure,
ListenPort: listenPort,
MetricsPort: metricsPort,
Namespace: namespace,
StaticAssetsDir: staticAssetsDir,
BaseHRef: baseHRef,
RootPath: rootPath,
KubeClientset: kubeclientset,
AppClientset: appclientset,
RepoClientset: repoclientset,
DexServerAddr: dexServerAddress,
DisableAuth: disableAuth,
EnableGZip: enableGZip,
TLSConfigCustomizer: tlsConfigCustomizer,
Cache: cache,
XFrameOptions: frameOptions,
RedisClient: redisClient,
}
stats.RegisterStackDumper()
@@ -131,7 +81,7 @@ func NewCommand() *cobra.Command {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
argocd := server.NewServer(ctx, argoCDOpts)
argocd.Run(ctx, listenPort, metricsPort)
argocd.Run(ctx, common.PortAPIServer)
cancel()
}
},
@@ -141,22 +91,13 @@ 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")
command.Flags().StringVar(&dexServerAddress, "dex-server", common.DefaultDexServerAddr, "Dex server address")
command.Flags().BoolVar(&disableAuth, "disable-auth", false, "Disable client authentication")
command.Flags().BoolVar(&enableGZip, "enable-gzip", false, "Enable GZIP compression")
command.AddCommand(cli.NewVersionCmd(cliName))
command.Flags().IntVar(&listenPort, "port", common.DefaultPortAPIServer, "Listen on given port")
command.Flags().IntVar(&metricsPort, "metrics-port", common.DefaultPortArgoCDAPIServerMetrics, "Start metrics on given port")
command.Flags().IntVar(&repoServerTimeoutSeconds, "repo-server-timeout-seconds", 60, "Repo server RPC call timeout seconds.")
command.Flags().StringVar(&frameOptions, "x-frame-options", "sameorigin", "Set X-Frame-Options header in HTTP responses to `value`. To disable, set to \"\".")
tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(command)
cacheSrc = servercache.AddCacheFlagsToCmd(command, func(client *redis.Client) {
redisClient = client
})
cacheSrc = cache.AddCacheFlagsToCmd(command)
return command
}

View File

@@ -1,16 +1,13 @@
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"
// load the oidc plugin (required to authenticate with OpenID Connect).
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
// load the azure plugin (required to authenticate with AKS clusters).
_ "k8s.io/client-go/plugin/pkg/client/auth/azure"
)
func main() {

View File

@@ -1,331 +0,0 @@
package commands
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"sort"
"time"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
"github.com/ghodss/yaml"
"github.com/spf13/cobra"
apiv1 "k8s.io/api/core/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/kubernetes"
kubecache "k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/clientcmd"
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/controller"
"github.com/argoproj/argo-cd/controller/cache"
"github.com/argoproj/argo-cd/controller/metrics"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
appinformers "github.com/argoproj/argo-cd/pkg/client/informers/externalversions"
"github.com/argoproj/argo-cd/reposerver/apiclient"
"github.com/argoproj/argo-cd/util/cli"
"github.com/argoproj/argo-cd/util/config"
"github.com/argoproj/argo-cd/util/db"
kubeutil "github.com/argoproj/argo-cd/util/kube"
"github.com/argoproj/argo-cd/util/settings"
)
func NewAppsCommand() *cobra.Command {
var command = &cobra.Command{
Use: "apps",
Run: func(c *cobra.Command, args []string) {
c.HelpFunc()(c, args)
},
}
command.AddCommand(NewReconcileCommand())
command.AddCommand(NewDiffReconcileResults())
return command
}
type appReconcileResult struct {
Name string `json:"name"`
Health *v1alpha1.HealthStatus `json:"health"`
Sync *v1alpha1.SyncStatus `json:"sync"`
Conditions []v1alpha1.ApplicationCondition `json:"conditions"`
}
type reconcileResults struct {
Applications []appReconcileResult `json:"applications"`
}
func (r *reconcileResults) getAppsMap() map[string]appReconcileResult {
res := map[string]appReconcileResult{}
for i := range r.Applications {
res[r.Applications[i].Name] = r.Applications[i]
}
return res
}
func printLine(format string, a ...interface{}) {
_, _ = fmt.Printf(format+"\n", a...)
}
func NewDiffReconcileResults() *cobra.Command {
var command = &cobra.Command{
Use: "diff-reconcile-results PATH1 PATH2",
Short: "Compare results of two reconciliations and print diff.",
Run: func(c *cobra.Command, args []string) {
if len(args) != 2 {
c.HelpFunc()(c, args)
os.Exit(1)
}
path1 := args[0]
path2 := args[1]
var res1 reconcileResults
var res2 reconcileResults
errors.CheckError(config.UnmarshalLocalFile(path1, &res1))
errors.CheckError(config.UnmarshalLocalFile(path2, &res2))
errors.CheckError(diffReconcileResults(res1, res2))
},
}
return command
}
func toUnstructured(val interface{}) (*unstructured.Unstructured, error) {
data, err := json.Marshal(val)
if err != nil {
return nil, err
}
res := make(map[string]interface{})
err = json.Unmarshal(data, &res)
if err != nil {
return nil, err
}
return &unstructured.Unstructured{Object: res}, nil
}
type diffPair struct {
name string
first *unstructured.Unstructured
second *unstructured.Unstructured
}
func diffReconcileResults(res1 reconcileResults, res2 reconcileResults) error {
var pairs []diffPair
resMap1 := res1.getAppsMap()
resMap2 := res2.getAppsMap()
for k, v := range resMap1 {
firstUn, err := toUnstructured(v)
if err != nil {
return err
}
var secondUn *unstructured.Unstructured
second, ok := resMap2[k]
if ok {
secondUn, err = toUnstructured(second)
if err != nil {
return err
}
delete(resMap2, k)
}
pairs = append(pairs, diffPair{name: k, first: firstUn, second: secondUn})
}
for k, v := range resMap2 {
secondUn, err := toUnstructured(v)
if err != nil {
return err
}
pairs = append(pairs, diffPair{name: k, first: nil, second: secondUn})
}
sort.Slice(pairs, func(i, j int) bool {
return pairs[i].name < pairs[j].name
})
for _, item := range pairs {
printLine(item.name)
_ = cli.PrintDiff(item.name, item.first, item.second)
}
return nil
}
func NewReconcileCommand() *cobra.Command {
var (
clientConfig clientcmd.ClientConfig
selector string
repoServerAddress string
outputFormat string
refresh bool
)
var command = &cobra.Command{
Use: "get-reconcile-results PATH",
Short: "Reconcile all applications and stores reconciliation summary in the specified file.",
Run: func(c *cobra.Command, args []string) {
// get rid of logging error handler
runtime.ErrorHandlers = runtime.ErrorHandlers[1:]
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
outputPath := args[0]
errors.CheckError(os.Setenv(common.EnvVarFakeInClusterConfig, "true"))
cfg, err := clientConfig.ClientConfig()
errors.CheckError(err)
namespace, _, err := clientConfig.Namespace()
errors.CheckError(err)
var result []appReconcileResult
if refresh {
if repoServerAddress == "" {
printLine("Repo server is not provided, trying to port-forward to argocd-repo-server pod.")
repoServerPort, err := kubeutil.PortForward("app.kubernetes.io/name=argocd-repo-server", 8081, namespace)
errors.CheckError(err)
repoServerAddress = fmt.Sprintf("localhost:%d", repoServerPort)
}
repoServerClient := apiclient.NewRepoServerClientset(repoServerAddress, 60)
appClientset := appclientset.NewForConfigOrDie(cfg)
kubeClientset := kubernetes.NewForConfigOrDie(cfg)
result, err = reconcileApplications(kubeClientset, appClientset, namespace, repoServerClient, selector, newLiveStateCache)
errors.CheckError(err)
} else {
appClientset := appclientset.NewForConfigOrDie(cfg)
result, err = getReconcileResults(appClientset, namespace, selector)
}
errors.CheckError(saveToFile(err, outputFormat, reconcileResults{Applications: result}, outputPath))
},
}
clientConfig = cli.AddKubectlFlagsToCmd(command)
command.Flags().StringVar(&repoServerAddress, "repo-server", "", "Repo server address.")
command.Flags().StringVar(&selector, "l", "", "Label selector")
command.Flags().StringVar(&outputFormat, "o", "yaml", "Output format (yaml|json)")
command.Flags().BoolVar(&refresh, "refresh", false, "If set to true then recalculates apps reconciliation")
return command
}
func saveToFile(err error, outputFormat string, result reconcileResults, outputPath string) error {
errors.CheckError(err)
var data []byte
switch outputFormat {
case "yaml":
if data, err = yaml.Marshal(result); err != nil {
return err
}
case "json":
if data, err = json.Marshal(result); err != nil {
return err
}
default:
return fmt.Errorf("format %s is not supported", outputFormat)
}
return ioutil.WriteFile(outputPath, data, 0644)
}
func getReconcileResults(appClientset appclientset.Interface, namespace string, selector string) ([]appReconcileResult, error) {
appsList, err := appClientset.ArgoprojV1alpha1().Applications(namespace).List(context.Background(), v1.ListOptions{LabelSelector: selector})
if err != nil {
return nil, err
}
var items []appReconcileResult
for _, app := range appsList.Items {
items = append(items, appReconcileResult{
Name: app.Name,
Conditions: app.Status.Conditions,
Health: &app.Status.Health,
Sync: &app.Status.Sync,
})
}
return items, nil
}
func reconcileApplications(
kubeClientset kubernetes.Interface,
appClientset appclientset.Interface,
namespace string,
repoServerClient apiclient.Clientset,
selector string,
createLiveStateCache func(argoDB db.ArgoDB, appInformer kubecache.SharedIndexInformer, settingsMgr *settings.SettingsManager, server *metrics.MetricsServer) cache.LiveStateCache,
) ([]appReconcileResult, error) {
settingsMgr := settings.NewSettingsManager(context.Background(), kubeClientset, namespace)
argoDB := db.NewDB(namespace, settingsMgr, kubeClientset)
appInformerFactory := appinformers.NewFilteredSharedInformerFactory(
appClientset,
1*time.Hour,
namespace,
func(options *v1.ListOptions) {},
)
appInformer := appInformerFactory.Argoproj().V1alpha1().Applications().Informer()
projInformer := appInformerFactory.Argoproj().V1alpha1().AppProjects().Informer()
go appInformer.Run(context.Background().Done())
go projInformer.Run(context.Background().Done())
if !kubecache.WaitForCacheSync(context.Background().Done(), appInformer.HasSynced, projInformer.HasSynced) {
return nil, fmt.Errorf("failed to sync cache")
}
appLister := appInformerFactory.Argoproj().V1alpha1().Applications().Lister()
projLister := appInformerFactory.Argoproj().V1alpha1().AppProjects().Lister()
server := metrics.NewMetricsServer("", appLister, func() error {
return nil
})
stateCache := createLiveStateCache(argoDB, appInformer, settingsMgr, server)
if err := stateCache.Init(); err != nil {
return nil, err
}
appStateManager := controller.NewAppStateManager(
argoDB, appClientset, repoServerClient, namespace, &kube.KubectlCmd{}, settingsMgr, stateCache, projInformer, server)
appsList, err := appClientset.ArgoprojV1alpha1().Applications(namespace).List(context.Background(), v1.ListOptions{LabelSelector: selector})
if err != nil {
return nil, err
}
sort.Slice(appsList.Items, func(i, j int) bool {
return appsList.Items[i].Spec.Destination.Server < appsList.Items[j].Spec.Destination.Server
})
var items []appReconcileResult
prevServer := ""
for _, app := range appsList.Items {
if prevServer != app.Spec.Destination.Server {
if prevServer != "" {
if clusterCache, err := stateCache.GetClusterCache(prevServer); err == nil {
clusterCache.Invalidate()
}
}
printLine("Reconciling apps of %s", app.Spec.Destination.Server)
prevServer = app.Spec.Destination.Server
}
printLine(app.Name)
proj, err := projLister.AppProjects(namespace).Get(app.Spec.Project)
if err != nil {
return nil, err
}
res := appStateManager.CompareAppState(&app, proj, app.Spec.Source.TargetRevision, app.Spec.Source, false, nil)
items = append(items, appReconcileResult{
Name: app.Name,
Conditions: app.Status.Conditions,
Health: res.GetHealthStatus(),
Sync: res.GetSyncStatus(),
})
}
return items, nil
}
func newLiveStateCache(argoDB db.ArgoDB, appInformer kubecache.SharedIndexInformer, settingsMgr *settings.SettingsManager, server *metrics.MetricsServer) cache.LiveStateCache {
return cache.NewLiveStateCache(argoDB, appInformer, settingsMgr, &kube.KubectlCmd{}, server, func(managedByApp map[string]bool, ref apiv1.ObjectReference) {})
}

View File

@@ -1,182 +0,0 @@
package commands
import (
"testing"
"github.com/argoproj/argo-cd/test"
clustermocks "github.com/argoproj/gitops-engine/pkg/cache/mocks"
"github.com/argoproj/gitops-engine/pkg/health"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
kubefake "k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/tools/cache"
"github.com/argoproj/argo-cd/common"
statecache "github.com/argoproj/argo-cd/controller/cache"
cachemocks "github.com/argoproj/argo-cd/controller/cache/mocks"
"github.com/argoproj/argo-cd/controller/metrics"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
appfake "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/fake"
"github.com/argoproj/argo-cd/reposerver/apiclient"
"github.com/argoproj/argo-cd/reposerver/apiclient/mocks"
"github.com/argoproj/argo-cd/util/db"
"github.com/argoproj/argo-cd/util/settings"
)
func TestGetReconcileResults(t *testing.T) {
appClientset := appfake.NewSimpleClientset(&v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Status: v1alpha1.ApplicationStatus{
Health: v1alpha1.HealthStatus{Status: health.HealthStatusHealthy},
Sync: v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeOutOfSync},
},
})
result, err := getReconcileResults(appClientset, "default", "")
if !assert.NoError(t, err) {
return
}
expectedResults := []appReconcileResult{{
Name: "test",
Health: &v1alpha1.HealthStatus{Status: health.HealthStatusHealthy},
Sync: &v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeOutOfSync},
}}
assert.ElementsMatch(t, expectedResults, result)
}
func TestGetReconcileResults_Refresh(t *testing.T) {
cm := corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "argocd-cm",
Namespace: "default",
Labels: map[string]string{
"app.kubernetes.io/part-of": "argocd",
},
},
}
proj := &v1alpha1.AppProject{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Namespace: "default",
},
Spec: v1alpha1.AppProjectSpec{Destinations: []v1alpha1.ApplicationDestination{{Namespace: "*", Server: "*"}}},
}
app := &v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
Spec: v1alpha1.ApplicationSpec{
Project: "default",
Destination: v1alpha1.ApplicationDestination{
Server: common.KubernetesInternalAPIServerAddr,
Namespace: "default",
},
},
}
appClientset := appfake.NewSimpleClientset(app, proj)
deployment := test.NewDeployment()
kubeClientset := kubefake.NewSimpleClientset(deployment, &cm)
clusterCache := clustermocks.ClusterCache{}
clusterCache.On("IsNamespaced", mock.Anything).Return(true, nil)
repoServerClient := mocks.RepoServerServiceClient{}
repoServerClient.On("GenerateManifest", mock.Anything, mock.Anything).Return(&apiclient.ManifestResponse{
Manifests: []string{test.DeploymentManifest},
}, nil)
repoServerClientset := mocks.Clientset{RepoServerServiceClient: &repoServerClient}
liveStateCache := cachemocks.LiveStateCache{}
liveStateCache.On("GetManagedLiveObjs", mock.Anything, mock.Anything).Return(map[kube.ResourceKey]*unstructured.Unstructured{
kube.GetResourceKey(deployment): deployment,
}, nil)
liveStateCache.On("GetVersionsInfo", mock.Anything).Return("v1.2.3", nil, nil)
liveStateCache.On("Init").Return(nil, nil)
liveStateCache.On("GetClusterCache", mock.Anything).Return(&clusterCache, nil)
liveStateCache.On("IsNamespaced", mock.Anything, mock.Anything).Return(true, nil)
result, err := reconcileApplications(kubeClientset, appClientset, "default", &repoServerClientset, "",
func(argoDB db.ArgoDB, appInformer cache.SharedIndexInformer, settingsMgr *settings.SettingsManager, server *metrics.MetricsServer) statecache.LiveStateCache {
return &liveStateCache
},
)
if !assert.NoError(t, err) {
return
}
assert.Equal(t, result[0].Health.Status, health.HealthStatusMissing)
assert.Equal(t, result[0].Sync.Status, v1alpha1.SyncStatusCodeOutOfSync)
}
func TestDiffReconcileResults_NoDifferences(t *testing.T) {
logs, err := captureStdout(func() {
assert.NoError(t, diffReconcileResults(
reconcileResults{Applications: []appReconcileResult{{
Name: "app1",
Sync: &v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeOutOfSync},
}}},
reconcileResults{Applications: []appReconcileResult{{
Name: "app1",
Sync: &v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeOutOfSync},
}}},
))
})
assert.NoError(t, err)
assert.Equal(t, "app1\n", logs)
}
func TestDiffReconcileResults_DifferentApps(t *testing.T) {
logs, err := captureStdout(func() {
assert.NoError(t, diffReconcileResults(
reconcileResults{Applications: []appReconcileResult{{
Name: "app1",
Sync: &v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeOutOfSync},
}, {
Name: "app2",
Sync: &v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeOutOfSync},
}}},
reconcileResults{Applications: []appReconcileResult{{
Name: "app1",
Sync: &v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeOutOfSync},
}, {
Name: "app3",
Sync: &v1alpha1.SyncStatus{Status: v1alpha1.SyncStatusCodeOutOfSync},
}}},
))
})
assert.NoError(t, err)
assert.Equal(t, `app1
app2
1,9d0
< conditions: null
< health: null
< name: app2
< sync:
< comparedTo:
< destination: {}
< source:
< repoURL: ""
< status: OutOfSync
app3
0a1,9
> conditions: null
> health: null
> name: app3
> sync:
> comparedTo:
> destination: {}
> source:
> repoURL: ""
> status: OutOfSync
`, logs)
}

View File

@@ -1,192 +0,0 @@
package commands
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"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/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"
)
func NewProjectsCommand() *cobra.Command {
var command = &cobra.Command{
Use: "projects",
Run: func(c *cobra.Command, args []string) {
c.HelpFunc()(c, args)
},
}
command.AddCommand(NewUpdatePolicyRuleCommand())
return command
}
func globMatch(pattern string, val string) bool {
if pattern == "*" {
return true
}
if ok, err := filepath.Match(pattern, val); ok && err == nil {
return true
}
return false
}
func getModification(modification string, resource string, scope string, permission string) (func(string, string) string, error) {
switch modification {
case "set":
if scope == "" {
return nil, fmt.Errorf("Flag --group cannot be empty if permission should be set in role")
}
if permission == "" {
return nil, fmt.Errorf("Flag --permission cannot be empty if permission should be set in role")
}
return func(proj string, action string) string {
return fmt.Sprintf("%s, %s, %s/%s, %s", resource, action, proj, scope, permission)
}, nil
case "remove":
return func(proj string, action string) string {
return ""
}, nil
}
return nil, fmt.Errorf("modification %s is not supported", modification)
}
func saveProject(updated v1alpha1.AppProject, orig v1alpha1.AppProject, projectsIf appclient.AppProjectInterface, dryRun bool) error {
fmt.Printf("===== %s ======\n", updated.Name)
target, err := kube.ToUnstructured(&updated)
errors.CheckError(err)
live, err := kube.ToUnstructured(&orig)
if err != nil {
return err
}
_ = cli.PrintDiff(updated.Name, target, live)
if !dryRun {
_, err = projectsIf.Update(context.Background(), &updated, v1.UpdateOptions{})
if err != nil {
return err
}
}
return nil
}
func formatPolicy(proj string, role string, permission string) string {
return fmt.Sprintf("p, proj:%s:%s, %s", proj, role, permission)
}
func split(input string, delimiter string) []string {
parts := strings.Split(input, delimiter)
for i := range parts {
parts[i] = strings.TrimSpace(parts[i])
}
return parts
}
func NewUpdatePolicyRuleCommand() *cobra.Command {
var (
clientConfig clientcmd.ClientConfig
resource string
scope string
rolePattern string
permission string
dryRun bool
)
var command = &cobra.Command{
Use: "update-role-policy PROJECT_GLOB MODIFICATION ACTION",
Short: "Implement bulk project role update. Useful to back-fill existing project policies or remove obsolete actions.",
Example: ` # Add policy that allows executing any action (action/*) to roles which name matches to *deployer* in all projects
argocd-util projects update-role-policy '*' set 'action/*' --role '*deployer*' --resource applications --scope '*' --permission allow
# Remove policy that which manages running (action/*) from all roles which name matches *deployer* in all projects
argocd-util projects update-role-policy '*' remove override --role '*deployer*'
`,
Run: func(c *cobra.Command, args []string) {
if len(args) != 3 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projectGlob := args[0]
modificationType := args[1]
action := args[2]
config, err := clientConfig.ClientConfig()
errors.CheckError(err)
config.QPS = 100
config.Burst = 50
namespace, _, err := clientConfig.Namespace()
errors.CheckError(err)
appclients := appclientset.NewForConfigOrDie(config)
modification, err := getModification(modificationType, resource, scope, permission)
errors.CheckError(err)
projIf := appclients.ArgoprojV1alpha1().AppProjects(namespace)
err = updateProjects(projIf, projectGlob, rolePattern, action, modification, dryRun)
errors.CheckError(err)
},
}
command.Flags().StringVar(&resource, "resource", "", "Resource e.g. 'applications'")
command.Flags().StringVar(&scope, "scope", "", "Resource scope e.g. '*'")
command.Flags().StringVar(&rolePattern, "role", "*", "Role name pattern e.g. '*deployer*'")
command.Flags().StringVar(&permission, "permission", "", "Action permission")
command.Flags().BoolVar(&dryRun, "dry-run", true, "Dry run")
clientConfig = cli.AddKubectlFlagsToCmd(command)
return command
}
func updateProjects(projIf appclient.AppProjectInterface, projectGlob string, rolePattern string, action string, modification func(string, string) string, dryRun bool) error {
projects, err := projIf.List(context.Background(), v1.ListOptions{})
if err != nil {
return err
}
for _, proj := range projects.Items {
if !globMatch(projectGlob, proj.Name) {
continue
}
origProj := proj.DeepCopy()
updated := false
for i, role := range proj.Spec.Roles {
if !globMatch(rolePattern, role.Name) {
continue
}
actionPolicyIndex := -1
for i := range role.Policies {
parts := split(role.Policies[i], ",")
if len(parts) != 6 || parts[3] != action {
continue
}
actionPolicyIndex = i
break
}
policyPermission := modification(proj.Name, action)
if actionPolicyIndex == -1 && policyPermission != "" {
updated = true
role.Policies = append(role.Policies, formatPolicy(proj.Name, role.Name, policyPermission))
} else if actionPolicyIndex > -1 && policyPermission == "" {
updated = true
role.Policies = append(role.Policies[:actionPolicyIndex], role.Policies[actionPolicyIndex+1:]...)
} else if actionPolicyIndex > -1 && policyPermission != "" {
updated = true
role.Policies[actionPolicyIndex] = formatPolicy(proj.Name, role.Name, policyPermission)
}
proj.Spec.Roles[i] = role
}
if updated {
err = saveProject(proj, *origProj, projIf, dryRun)
if err != nil {
return err
}
}
}
return nil
}

View File

@@ -1,79 +0,0 @@
package commands
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/pkg/client/clientset/versioned/fake"
)
const (
namespace = "default"
)
func newProj(name string, roleNames ...string) *v1alpha1.AppProject {
var roles []v1alpha1.ProjectRole
for i := range roleNames {
roles = append(roles, v1alpha1.ProjectRole{Name: roleNames[i]})
}
return &v1alpha1.AppProject{ObjectMeta: v1.ObjectMeta{
Name: name,
Namespace: namespace,
}, Spec: v1alpha1.AppProjectSpec{
Roles: roles,
}}
}
func TestUpdateProjects_FindMatchingProject(t *testing.T) {
clientset := fake.NewSimpleClientset(newProj("foo", "test"), newProj("bar", "test"))
modification, err := getModification("set", "*", "*", "allow")
assert.NoError(t, err)
err = updateProjects(clientset.ArgoprojV1alpha1().AppProjects(namespace), "ba*", "*", "set", modification, false)
assert.NoError(t, err)
fooProj, err := clientset.ArgoprojV1alpha1().AppProjects(namespace).Get(context.Background(), "foo", v1.GetOptions{})
assert.NoError(t, err)
assert.Len(t, fooProj.Spec.Roles[0].Policies, 0)
barProj, err := clientset.ArgoprojV1alpha1().AppProjects(namespace).Get(context.Background(), "bar", v1.GetOptions{})
assert.NoError(t, err)
assert.EqualValues(t, barProj.Spec.Roles[0].Policies, []string{"p, proj:bar:test, *, set, bar/*, allow"})
}
func TestUpdateProjects_FindMatchingRole(t *testing.T) {
clientset := fake.NewSimpleClientset(newProj("proj", "foo", "bar"))
modification, err := getModification("set", "*", "*", "allow")
assert.NoError(t, err)
err = updateProjects(clientset.ArgoprojV1alpha1().AppProjects(namespace), "*", "fo*", "set", modification, false)
assert.NoError(t, err)
proj, err := clientset.ArgoprojV1alpha1().AppProjects(namespace).Get(context.Background(), "proj", v1.GetOptions{})
assert.NoError(t, err)
assert.EqualValues(t, proj.Spec.Roles[0].Policies, []string{"p, proj:proj:foo, *, set, proj/*, allow"})
assert.Len(t, proj.Spec.Roles[1].Policies, 0)
}
func TestGetModification_SetPolicy(t *testing.T) {
modification, err := getModification("set", "*", "*", "allow")
assert.NoError(t, err)
policy := modification("proj", "myaction")
assert.Equal(t, "*, myaction, proj/*, allow", policy)
}
func TestGetModification_RemovePolicy(t *testing.T) {
modification, err := getModification("remove", "*", "*", "allow")
assert.NoError(t, err)
policy := modification("proj", "myaction")
assert.Equal(t, "", policy)
}
func TestGetModification_NotSupported(t *testing.T) {
_, err := getModification("bar", "*", "*", "allow")
assert.Errorf(t, err, "modification bar is not supported")
}

View File

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

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: "2 resource overrides",
},
}
for name := range testCases {
tc := testCases[name]
t.Run(name, func(t *testing.T) {
validator, ok := validatorsByGroup[tc.validator]
if !assert.True(t, ok) {
return
}
summary, err := validator(newSettingsManager(tc.data))
if tc.containsSummary != "" {
assert.NoError(t, err)
assert.Contains(t, summary, tc.containsSummary)
} else if tc.containsError != "" {
if assert.Error(t, err) {
assert.Contains(t, err.Error(), tc.containsError)
}
}
})
}
}
const (
testDeploymentYAML = `apiVersion: v1
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 0`
)
func tempFile(content string) (string, io.Closer, error) {
f, err := ioutil.TempFile("", "*.yaml")
if err != nil {
return "", nil, err
}
_, err = f.Write([]byte(content))
if err != nil {
_ = os.Remove(f.Name())
return "", nil, err
}
return f.Name(), utils.NewCloser(func() error {
return os.Remove(f.Name())
}), nil
}
func TestValidateSettingsCommand_NoErrors(t *testing.T) {
cmd := NewValidateSettingsCommand(newCmdContext(map[string]string{}))
out, err := captureStdout(func() {
err := cmd.Execute()
assert.NoError(t, err)
})
assert.NoError(t, err)
for k := range validatorsByGroup {
assert.Contains(t, out, fmt.Sprintf("✅ %s", k))
}
}
func TestResourceOverrideIgnoreDifferences(t *testing.T) {
f, closer, err := tempFile(testDeploymentYAML)
if !assert.NoError(t, err) {
return
}
defer utils.Close(closer)
t.Run("NoOverridesConfigured", func(t *testing.T) {
cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{}))
out, err := captureStdout(func() {
cmd.SetArgs([]string{"ignore-differences", f})
err := cmd.Execute()
assert.NoError(t, err)
})
assert.NoError(t, err)
assert.Contains(t, out, "No overrides configured")
})
t.Run("DataIgnored", func(t *testing.T) {
cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
"resource.customizations": `apps/Deployment:
ignoreDifferences: |
jsonPointers:
- /spec`}))
out, err := captureStdout(func() {
cmd.SetArgs([]string{"ignore-differences", f})
err := cmd.Execute()
assert.NoError(t, err)
})
assert.NoError(t, err)
assert.Contains(t, out, "< spec:")
})
}
func TestResourceOverrideHealth(t *testing.T) {
f, closer, err := tempFile(testDeploymentYAML)
if !assert.NoError(t, err) {
return
}
defer utils.Close(closer)
t.Run("NoHealthAssessment", func(t *testing.T) {
cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
"resource.customizations": `apps/Deployment: {}`}))
out, err := captureStdout(func() {
cmd.SetArgs([]string{"health", f})
err := cmd.Execute()
assert.NoError(t, err)
})
assert.NoError(t, err)
assert.Contains(t, out, "Health script is not configured")
})
t.Run("HealthAssessmentConfigured", func(t *testing.T) {
cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
"resource.customizations": `apps/Deployment:
health.lua: |
return { status = "Progressing" }
`}))
out, err := captureStdout(func() {
cmd.SetArgs([]string{"health", f})
err := cmd.Execute()
assert.NoError(t, err)
})
assert.NoError(t, err)
assert.Contains(t, out, "Progressing")
})
}
func TestResourceOverrideAction(t *testing.T) {
f, closer, err := tempFile(testDeploymentYAML)
if !assert.NoError(t, err) {
return
}
defer utils.Close(closer)
t.Run("NoActions", func(t *testing.T) {
cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
"resource.customizations": `apps/Deployment: {}`}))
out, err := captureStdout(func() {
cmd.SetArgs([]string{"run-action", f, "test"})
err := cmd.Execute()
assert.NoError(t, err)
})
assert.NoError(t, err)
assert.Contains(t, out, "Actions are not configured")
})
t.Run("ActionConfigured", func(t *testing.T) {
cmd := NewResourceOverridesCommand(newCmdContext(map[string]string{
"resource.customizations": `apps/Deployment:
actions: |
discovery.lua: |
actions = {}
actions["resume"] = {["disabled"] = false}
actions["restart"] = {["disabled"] = false}
return actions
definitions:
- name: test
action.lua: |
obj.metadata.labels["test"] = 'updated'
return obj
`}))
out, err := captureStdout(func() {
cmd.SetArgs([]string{"run-action", f, "test"})
err := cmd.Execute()
assert.NoError(t, err)
})
assert.NoError(t, err)
assert.Contains(t, out, "test: updated")
out, err = captureStdout(func() {
cmd.SetArgs([]string{"list-actions", f})
err := cmd.Execute()
assert.NoError(t, err)
})
assert.NoError(t, err)
assert.Contains(t, out, `NAME ENABLED
restart false
resume false
`)
})
}

View File

@@ -8,11 +8,8 @@ import (
"io/ioutil"
"os"
"os/exec"
"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,19 +23,20 @@ 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/util"
"github.com/argoproj/argo-cd/errors"
"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).
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
// load the oidc plugin (required to authenticate with OpenID Connect).
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
// load the azure plugin (required to authenticate with AKS clusters).
_ "k8s.io/client-go/plugin/pkg/client/auth/azure"
)
const (
@@ -58,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{
@@ -76,11 +73,7 @@ func NewCommand() *cobra.Command {
command.AddCommand(NewImportCommand())
command.AddCommand(NewExportCommand())
command.AddCommand(NewClusterConfig())
command.AddCommand(commands.NewProjectsCommand())
command.AddCommand(commands.NewSettingsCommand())
command.AddCommand(commands.NewAppsCommand())
command.Flags().StringVar(&logFormat, "logformat", "text", "Set the logging format. One of: text|json")
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
return command
}
@@ -115,7 +108,7 @@ func NewRunDexCommand() *cobra.Command {
} else {
err = ioutil.WriteFile("/tmp/dex.yaml", dexCfgBytes, 0644)
errors.CheckError(err)
log.Debug(redactor(string(dexCfgBytes)))
log.Info(string(dexCfgBytes))
cmd = exec.Command("dex", "serve", "/tmp/dex.yaml")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
@@ -226,7 +219,6 @@ func NewImportCommand() *cobra.Command {
os.Exit(1)
}
config, err := clientConfig.ClientConfig()
errors.CheckError(err)
config.QPS = 100
config.Burst = 50
errors.CheckError(err)
@@ -249,49 +241,43 @@ func NewImportCommand() *cobra.Command {
// pruneObjects tracks live objects and it's current resource version. any remaining
// items in this map indicates the resource should be pruned since it no longer appears
// in the backup
pruneObjects := make(map[kube.ResourceKey]unstructured.Unstructured)
configMaps, err := acdClients.configMaps.List(context.Background(), metav1.ListOptions{})
pruneObjects := make(map[kube.ResourceKey]string)
configMaps, err := acdClients.configMaps.List(metav1.ListOptions{})
errors.CheckError(err)
// referencedSecrets holds any secrets referenced in the argocd-cm configmap. These
// secrets need to be imported too
var referencedSecrets map[string]bool
for _, cm := range configMaps.Items {
if isArgoCDConfigMap(cm.GetName()) {
pruneObjects[kube.ResourceKey{Group: "", Kind: "ConfigMap", Name: cm.GetName()}] = cm
}
if cm.GetName() == common.ArgoCDConfigMapName {
referencedSecrets = getReferencedSecrets(cm)
cmName := cm.GetName()
if cmName == common.ArgoCDConfigMapName || cmName == common.ArgoCDRBACConfigMapName {
pruneObjects[kube.ResourceKey{Group: "", Kind: "ConfigMap", Name: cm.GetName()}] = cm.GetResourceVersion()
}
}
secrets, err := acdClients.secrets.List(context.Background(), metav1.ListOptions{})
secrets, err := acdClients.secrets.List(metav1.ListOptions{})
errors.CheckError(err)
for _, secret := range secrets.Items {
if isArgoCDSecret(referencedSecrets, secret) {
pruneObjects[kube.ResourceKey{Group: "", Kind: "Secret", Name: secret.GetName()}] = secret
if isArgoCDSecret(nil, secret) {
pruneObjects[kube.ResourceKey{Group: "", Kind: "Secret", Name: secret.GetName()}] = secret.GetResourceVersion()
}
}
applications, err := acdClients.applications.List(context.Background(), metav1.ListOptions{})
applications, err := acdClients.applications.List(metav1.ListOptions{})
errors.CheckError(err)
for _, app := range applications.Items {
pruneObjects[kube.ResourceKey{Group: "argoproj.io", Kind: "Application", Name: app.GetName()}] = app
pruneObjects[kube.ResourceKey{Group: "argoproj.io", Kind: "Application", Name: app.GetName()}] = app.GetResourceVersion()
}
projects, err := acdClients.projects.List(context.Background(), metav1.ListOptions{})
projects, err := acdClients.projects.List(metav1.ListOptions{})
errors.CheckError(err)
for _, proj := range projects.Items {
pruneObjects[kube.ResourceKey{Group: "argoproj.io", Kind: "AppProject", Name: proj.GetName()}] = proj
pruneObjects[kube.ResourceKey{Group: "argoproj.io", Kind: "AppProject", Name: proj.GetName()}] = proj.GetResourceVersion()
}
// Create or replace existing object
backupObjects, err := kube.SplitYAML(input)
objs, err := kube.SplitYAML(string(input))
errors.CheckError(err)
for _, bakObj := range backupObjects {
gvk := bakObj.GroupVersionKind()
key := kube.ResourceKey{Group: gvk.Group, Kind: gvk.Kind, Name: bakObj.GetName()}
liveObj, exists := pruneObjects[key]
for _, obj := range objs {
gvk := obj.GroupVersionKind()
key := kube.ResourceKey{Group: gvk.Group, Kind: gvk.Kind, Name: obj.GetName()}
resourceVersion, exists := pruneObjects[key]
delete(pruneObjects, key)
var dynClient dynamic.ResourceInterface
switch bakObj.GetKind() {
switch obj.GetKind() {
case "Secret":
dynClient = acdClients.secrets
case "ConfigMap":
@@ -303,19 +289,17 @@ func NewImportCommand() *cobra.Command {
}
if !exists {
if !dryRun {
_, err = dynClient.Create(context.Background(), bakObj, metav1.CreateOptions{})
_, err = dynClient.Create(obj, metav1.CreateOptions{})
errors.CheckError(err)
}
fmt.Printf("%s/%s %s created%s\n", gvk.Group, gvk.Kind, bakObj.GetName(), dryRunMsg)
} else if specsEqual(*bakObj, liveObj) {
fmt.Printf("%s/%s %s unchanged%s\n", gvk.Group, gvk.Kind, bakObj.GetName(), dryRunMsg)
fmt.Printf("%s/%s %s created%s\n", gvk.Group, gvk.Kind, obj.GetName(), dryRunMsg)
} else {
if !dryRun {
newLive := updateLive(bakObj, &liveObj)
_, err = dynClient.Update(context.Background(), newLive, metav1.UpdateOptions{})
obj.SetResourceVersion(resourceVersion)
_, err = dynClient.Update(obj, metav1.UpdateOptions{})
errors.CheckError(err)
}
fmt.Printf("%s/%s %s updated%s\n", gvk.Group, gvk.Kind, bakObj.GetName(), dryRunMsg)
fmt.Printf("%s/%s %s replaced%s\n", gvk.Group, gvk.Kind, obj.GetName(), dryRunMsg)
}
}
@@ -334,7 +318,7 @@ func NewImportCommand() *cobra.Command {
log.Fatalf("Unexpected kind '%s' in prune list", key.Kind)
}
if !dryRun {
err = dynClient.Delete(context.Background(), key.Name, metav1.DeleteOptions{})
err = dynClient.Delete(key.Name, &metav1.DeleteOptions{})
errors.CheckError(err)
}
fmt.Printf("%s/%s %s pruned%s\n", key.Group, key.Kind, key.Name, dryRunMsg)
@@ -391,44 +375,32 @@ 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)
acdConfigMap, err := acdClients.configMaps.Get(context.Background(), common.ArgoCDConfigMapName, metav1.GetOptions{})
acdConfigMap, err := acdClients.configMaps.Get(common.ArgoCDConfigMapName, metav1.GetOptions{})
errors.CheckError(err)
export(writer, *acdConfigMap)
acdRBACConfigMap, err := acdClients.configMaps.Get(context.Background(), common.ArgoCDRBACConfigMapName, metav1.GetOptions{})
acdRBACConfigMap, err := acdClients.configMaps.Get(common.ArgoCDRBACConfigMapName, metav1.GetOptions{})
errors.CheckError(err)
export(writer, *acdRBACConfigMap)
acdKnownHostsConfigMap, err := acdClients.configMaps.Get(context.Background(), common.ArgoCDKnownHostsConfigMapName, metav1.GetOptions{})
errors.CheckError(err)
export(writer, *acdKnownHostsConfigMap)
acdTLSCertsConfigMap, err := acdClients.configMaps.Get(context.Background(), common.ArgoCDTLSCertsConfigMapName, metav1.GetOptions{})
errors.CheckError(err)
export(writer, *acdTLSCertsConfigMap)
referencedSecrets := getReferencedSecrets(*acdConfigMap)
secrets, err := acdClients.secrets.List(context.Background(), metav1.ListOptions{})
secrets, err := acdClients.secrets.List(metav1.ListOptions{})
errors.CheckError(err)
for _, secret := range secrets.Items {
if isArgoCDSecret(referencedSecrets, secret) {
export(writer, secret)
}
}
projects, err := acdClients.projects.List(context.Background(), metav1.ListOptions{})
projects, err := acdClients.projects.List(metav1.ListOptions{})
errors.CheckError(err)
for _, proj := range projects.Items {
export(writer, proj)
}
applications, err := acdClients.applications.List(context.Background(), metav1.ListOptions{})
applications, err := acdClients.applications.List(metav1.ListOptions{})
errors.CheckError(err)
for _, app := range applications.Items {
export(writer, app)
@@ -449,13 +421,11 @@ func getReferencedSecrets(un unstructured.Unstructured) map[string]bool {
err := runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, &cm)
errors.CheckError(err)
referencedSecrets := make(map[string]bool)
// Referenced repository secrets
if reposRAW, ok := cm.Data["repositories"]; ok {
repos := make([]settings.Repository, 0)
err := yaml.Unmarshal([]byte(reposRAW), &repos)
repoCreds := make([]settings.RepoCredentials, 0)
err := yaml.Unmarshal([]byte(reposRAW), &repoCreds)
errors.CheckError(err)
for _, cred := range repos {
for _, cred := range repoCreds {
if cred.PasswordSecret != nil {
referencedSecrets[cred.PasswordSecret.Name] = true
}
@@ -465,35 +435,27 @@ func getReferencedSecrets(un unstructured.Unstructured) map[string]bool {
if cred.UsernameSecret != nil {
referencedSecrets[cred.UsernameSecret.Name] = true
}
if cred.TLSClientCertDataSecret != nil {
referencedSecrets[cred.TLSClientCertDataSecret.Name] = true
}
if cred.TLSClientCertKeySecret != nil {
referencedSecrets[cred.TLSClientCertKeySecret.Name] = true
}
}
}
// Referenced repository credentials secrets
if reposRAW, ok := cm.Data["repository.credentials"]; ok {
creds := make([]settings.RepositoryCredentials, 0)
err := yaml.Unmarshal([]byte(reposRAW), &creds)
if helmReposRAW, ok := cm.Data["helm.repositories"]; ok {
helmRepoCreds := make([]settings.HelmRepoCredentials, 0)
err := yaml.Unmarshal([]byte(helmReposRAW), &helmRepoCreds)
errors.CheckError(err)
for _, cred := range creds {
if cred.PasswordSecret != nil {
referencedSecrets[cred.PasswordSecret.Name] = true
for _, cred := range helmRepoCreds {
if cred.CASecret != nil {
referencedSecrets[cred.CASecret.Name] = true
}
if cred.SSHPrivateKeySecret != nil {
referencedSecrets[cred.SSHPrivateKeySecret.Name] = true
if cred.CertSecret != nil {
referencedSecrets[cred.CertSecret.Name] = true
}
if cred.KeySecret != nil {
referencedSecrets[cred.KeySecret.Name] = true
}
if cred.UsernameSecret != nil {
referencedSecrets[cred.UsernameSecret.Name] = true
}
if cred.TLSClientCertDataSecret != nil {
referencedSecrets[cred.TLSClientCertDataSecret.Name] = true
}
if cred.TLSClientCertKeySecret != nil {
referencedSecrets[cred.TLSClientCertKeySecret.Name] = true
if cred.PasswordSecret != nil {
referencedSecrets[cred.PasswordSecret.Name] = true
}
}
}
@@ -525,73 +487,6 @@ func isArgoCDSecret(repoSecretRefs map[string]bool, un unstructured.Unstructured
return false
}
// isArgoCDConfigMap returns true if the configmap name is one of argo cd's well known configmaps
func isArgoCDConfigMap(name string) bool {
switch name {
case common.ArgoCDConfigMapName, common.ArgoCDRBACConfigMapName, common.ArgoCDKnownHostsConfigMapName, common.ArgoCDTLSCertsConfigMapName:
return true
}
return false
}
// specsEqual returns if the spec, data, labels, annotations, and finalizers of the two
// supplied objects are equal, indicating that no update is necessary during importing
func specsEqual(left, right unstructured.Unstructured) bool {
if !reflect.DeepEqual(left.GetAnnotations(), right.GetAnnotations()) {
return false
}
if !reflect.DeepEqual(left.GetLabels(), right.GetLabels()) {
return false
}
if !reflect.DeepEqual(left.GetFinalizers(), right.GetFinalizers()) {
return false
}
switch left.GetKind() {
case "Secret", "ConfigMap":
leftData, _, _ := unstructured.NestedMap(left.Object, "data")
rightData, _, _ := unstructured.NestedMap(right.Object, "data")
return reflect.DeepEqual(leftData, rightData)
case "AppProject":
leftSpec, _, _ := unstructured.NestedMap(left.Object, "spec")
rightSpec, _, _ := unstructured.NestedMap(right.Object, "spec")
return reflect.DeepEqual(leftSpec, rightSpec)
case "Application":
leftSpec, _, _ := unstructured.NestedMap(left.Object, "spec")
rightSpec, _, _ := unstructured.NestedMap(right.Object, "spec")
leftStatus, _, _ := unstructured.NestedMap(left.Object, "status")
rightStatus, _, _ := unstructured.NestedMap(right.Object, "status")
// reconciledAt and observedAt are constantly changing and we ignore any diff there
delete(leftStatus, "reconciledAt")
delete(rightStatus, "reconciledAt")
delete(leftStatus, "observedAt")
delete(rightStatus, "observedAt")
return reflect.DeepEqual(leftSpec, rightSpec) && reflect.DeepEqual(leftStatus, rightStatus)
}
return false
}
// updateLive replaces the live object's finalizers, spec, annotations, labels, and data from the
// backup object but leaves all other fields intact (status, other metadata, etc...)
func updateLive(bak, live *unstructured.Unstructured) *unstructured.Unstructured {
newLive := live.DeepCopy()
newLive.SetAnnotations(bak.GetAnnotations())
newLive.SetLabels(bak.GetLabels())
newLive.SetFinalizers(bak.GetFinalizers())
switch live.GetKind() {
case "Secret", "ConfigMap":
newLive.Object["data"] = bak.Object["data"]
case "AppProject":
newLive.Object["spec"] = bak.Object["spec"]
case "Application":
newLive.Object["spec"] = bak.Object["spec"]
if _, ok := bak.Object["status"]; ok {
newLive.Object["status"] = bak.Object["status"]
}
}
return newLive
}
// export writes the unstructured object and removes extraneous cruft from output before writing
func export(w io.Writer, un unstructured.Unstructured) {
name := un.GetName()
@@ -639,7 +534,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)
},
}
@@ -647,38 +542,6 @@ func NewClusterConfig() *cobra.Command {
return command
}
func iterateStringFields(obj interface{}, callback func(name string, val string) string) {
if mapField, ok := obj.(map[string]interface{}); ok {
for field, val := range mapField {
if strVal, ok := val.(string); ok {
mapField[field] = callback(field, strVal)
} else {
iterateStringFields(val, callback)
}
}
} else if arrayField, ok := obj.([]interface{}); ok {
for i := range arrayField {
iterateStringFields(arrayField[i], callback)
}
}
}
func redactor(dirtyString string) string {
config := make(map[string]interface{})
err := yaml.Unmarshal([]byte(dirtyString), &config)
errors.CheckError(err)
iterateStringFields(config, func(name string, val string) string {
if name == "clientSecret" || name == "secret" || name == "bindPW" {
return "********"
} else {
return val
}
})
data, err := yaml.Marshal(config)
errors.CheckError(err)
return string(data)
}
func main() {
if err := NewCommand().Execute(); err != nil {
fmt.Println(err)

View File

@@ -1,94 +0,0 @@
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
var textToRedact = `
connectors:
- config:
clientID: aabbccddeeff00112233
clientSecret: |
theSecret
orgs:
- name: your-github-org
redirectURI: https://argocd.example.com/api/dex/callback
id: github
name: GitHub
type: github
- config:
bindDN: uid=serviceaccount,cn=users,dc=example,dc=com
bindPW: theSecret
host: ldap.example.com:636
id: ldap
name: LDAP
type: ldap
grpc:
addr: 0.0.0.0:5557
telemetry:
http: 0.0.0.0:5558
issuer: https://argocd.example.com/api/dex
oauth2:
skipApprovalScreen: true
staticClients:
- id: argo-cd
name: Argo CD
redirectURIs:
- https://argocd.example.com/auth/callback
secret: Dis9M-GA11oTwZVQQWdDklPQw-sWXZkWJFyyEhMs
- id: argo-cd-cli
name: Argo CD CLI
public: true
redirectURIs:
- http://localhost
storage:
type: memory
web:
http: 0.0.0.0:5556`
var expectedRedaction = `connectors:
- config:
clientID: aabbccddeeff00112233
clientSecret: '********'
orgs:
- name: your-github-org
redirectURI: https://argocd.example.com/api/dex/callback
id: github
name: GitHub
type: github
- config:
bindDN: uid=serviceaccount,cn=users,dc=example,dc=com
bindPW: '********'
host: ldap.example.com:636
id: ldap
name: LDAP
type: ldap
grpc:
addr: 0.0.0.0:5557
issuer: https://argocd.example.com/api/dex
oauth2:
skipApprovalScreen: true
staticClients:
- id: argo-cd
name: Argo CD
redirectURIs:
- https://argocd.example.com/auth/callback
secret: '********'
- id: argo-cd-cli
name: Argo CD CLI
public: true
redirectURIs:
- http://localhost
storage:
type: memory
telemetry:
http: 0.0.0.0:5558
web:
http: 0.0.0.0:5556
`
func TestSecretsRedactor(t *testing.T) {
assert.Equal(t, expectedRedaction, redactor(textToRedact))
}

View File

@@ -2,29 +2,19 @@ package commands
import (
"context"
"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/server/account"
"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 {
@@ -37,18 +27,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,341 +43,53 @@ 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()
errors.CheckError(err)
}
updatePasswordRequest := accountpkg.UpdatePasswordRequest{
updatePasswordRequest := account.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
}
func NewAccountGetUserInfoCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
output string
)
var command = &cobra.Command{
Use: "get-user-info",
Short: "Get user info",
Run: func(c *cobra.Command, args []string) {
if len(args) != 0 {
c.HelpFunc()(c, args)
os.Exit(1)
}
conn, client := argocdclient.NewClientOrDie(clientOpts).NewSessionClientOrDie()
defer io.Close(conn)
ctx := context.Background()
response, err := client.GetUserInfo(ctx, &session.GetUserInfoRequest{})
errors.CheckError(err)
switch output {
case "yaml":
yamlBytes, err := yaml.Marshal(response)
errors.CheckError(err)
fmt.Println(string(yamlBytes))
case "json":
jsonBytes, err := json.MarshalIndent(response, "", " ")
errors.CheckError(err)
fmt.Println(string(jsonBytes))
case "":
fmt.Printf("Logged In: %v\n", response.LoggedIn)
if response.LoggedIn {
fmt.Printf("Username: %s\n", response.Username)
fmt.Printf("Issuer: %s\n", response.Iss)
fmt.Printf("Groups: %v\n", strings.Join(response.Groups, ","))
}
default:
log.Fatalf("Unknown output format: %s", output)
}
},
}
command.Flags().StringVarP(&output, "output", "o", "", "Output format. One of: yaml, json")
return command
}
func NewAccountCanICommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
return &cobra.Command{
Use: "can-i ACTION RESOURCE SUBRESOURCE",
Short: "Can I",
Example: fmt.Sprintf(`
# Can I sync any app?
argocd account can-i sync applications '*'
# Can I update a project?
argocd account can-i update projects 'default'
# Can I create a cluster?
argocd account can-i create clusters '*'
Actions: %v
Resources: %v
`, rbacpolicy.Actions, rbacpolicy.Resources),
Run: func(c *cobra.Command, args []string) {
if len(args) != 3 {
c.HelpFunc()(c, args)
os.Exit(1)
}
conn, client := argocdclient.NewClientOrDie(clientOpts).NewAccountClientOrDie()
defer io.Close(conn)
ctx := context.Background()
response, err := client.CanI(ctx, &accountpkg.CanIRequest{
Action: args[0],
Resource: args[1],
Subresource: args[2],
})
errors.CheckError(err)
fmt.Println(response.Value)
},
}
}
func printAccountNames(accounts []*accountpkg.Account) {
for _, p := range accounts {
fmt.Println(p.Name)
}
}
func printAccountsTable(items []*accountpkg.Account) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "NAME\tENABLED\tCAPABILITIES\n")
for _, a := range items {
fmt.Fprintf(w, "%s\t%v\t%s\n", a.Name, a.Enabled, strings.Join(a.Capabilities, ", "))
}
_ = w.Flush()
}
func NewAccountListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
output string
)
cmd := &cobra.Command{
Use: "list",
Short: "List accounts",
Example: "argocd account list",
Run: func(c *cobra.Command, args []string) {
conn, client := argocdclient.NewClientOrDie(clientOpts).NewAccountClientOrDie()
defer 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
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,30 +2,20 @@ package commands
import (
"context"
"encoding/json"
"fmt"
"os"
"strconv"
"sort"
"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"
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/server/application"
"github.com/argoproj/argo-cd/util"
)
type DisplayedAction struct {
Group string
Kind string
Name string
Action string
Disabled bool
}
// NewApplicationResourceActionsCommand returns a new instance of an `argocd app actions` command
func NewApplicationResourceActionsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
@@ -47,7 +37,7 @@ func NewApplicationResourceActionsListCommand(clientOpts *argocdclient.ClientOpt
var kind string
var group string
var resourceName string
var output string
var all bool
var command = &cobra.Command{
Use: "list APPNAME",
Short: "Lists available actions on a resource",
@@ -59,16 +49,16 @@ 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})
resources, err := appIf.ManagedResources(ctx, &application.ResourcesQuery{ApplicationName: &appName})
errors.CheckError(err)
filteredObjects := filterResources(command, resources.Items, group, kind, namespace, resourceName, true)
var availableActions []DisplayedAction
filteredObjects := filterResources(command, resources.Items, group, kind, namespace, resourceName, all)
availableActions := make(map[string][]argoappv1.ResourceAction)
for i := range filteredObjects {
obj := filteredObjects[i]
gvk := obj.GroupVersionKind()
availActionsForResource, err := appIf.ListResourceActions(ctx, &applicationpkg.ApplicationResourceRequest{
availActionsForResource, err := appIf.ListResourceActions(ctx, &application.ApplicationResourceRequest{
Name: &appName,
Namespace: obj.GetNamespace(),
ResourceName: obj.GetName(),
@@ -76,42 +66,34 @@ func NewApplicationResourceActionsListCommand(clientOpts *argocdclient.ClientOpt
Kind: gvk.Kind,
})
errors.CheckError(err)
for _, action := range availActionsForResource.Actions {
displayAction := DisplayedAction{
Group: gvk.Group,
Kind: gvk.Kind,
Name: obj.GetName(),
Action: action.Name,
Disabled: action.Disabled,
}
availableActions = append(availableActions, displayAction)
}
availableActions[obj.GetName()] = availActionsForResource.Actions
}
switch output {
case "yaml":
yamlBytes, err := yaml.Marshal(availableActions)
errors.CheckError(err)
fmt.Println(string(yamlBytes))
case "json":
jsonBytes, err := json.MarshalIndent(availableActions, "", " ")
errors.CheckError(err)
fmt.Println(string(jsonBytes))
case "":
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "GROUP\tKIND\tNAME\tACTION\tDISABLED\n")
fmt.Println()
for _, action := range availableActions {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", action.Group, action.Kind, action.Name, action.Action, strconv.FormatBool(action.Disabled))
}
_ = w.Flush()
var keys []string
for key := range availableActions {
keys = append(keys, key)
}
sort.Strings(keys)
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "RESOURCE\tACTION\n")
fmt.Println()
for key := range availableActions {
for i := range availableActions[key] {
action := availableActions[key][i]
fmt.Fprintf(w, "%s\t%s\n", key, action.Name)
}
}
_ = w.Flush()
}
command.Flags().StringVar(&resourceName, "resource-name", "", "Name of resource")
command.Flags().StringVar(&kind, "kind", "", "Kind")
err := command.MarkFlagRequired("kind")
errors.CheckError(err)
command.Flags().StringVar(&group, "group", "", "Group")
command.Flags().StringVar(&namespace, "namespace", "", "Namespace")
command.Flags().StringVarP(&output, "out", "o", "", "Output format. One of: yaml, json")
command.Flags().BoolVar(&all, "all", false, "Indicates whether to list actions on multiple matching resources")
return command
}
@@ -119,9 +101,9 @@ func NewApplicationResourceActionsListCommand(clientOpts *argocdclient.ClientOpt
// NewApplicationResourceActionsRunCommand returns a new instance of an `argocd app actions run` command
func NewApplicationResourceActionsRunCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var namespace string
var resourceName string
var kind string
var group string
var resourceName string
var all bool
var command = &cobra.Command{
Use: "run APPNAME ACTION",
@@ -129,10 +111,11 @@ func NewApplicationResourceActionsRunCommand(clientOpts *argocdclient.ClientOpti
}
command.Flags().StringVar(&resourceName, "resource-name", "", "Name of resource")
command.Flags().StringVar(&namespace, "namespace", "", "Namespace")
command.Flags().StringVar(&kind, "kind", "", "Kind")
err := command.MarkFlagRequired("kind")
errors.CheckError(err)
command.Flags().StringVar(&group, "group", "", "Group")
errors.CheckError(command.MarkFlagRequired("kind"))
command.Flags().StringVar(&namespace, "namespace", "", "Namespace")
command.Flags().BoolVar(&all, "all", false, "Indicates whether to run the action on multiple matching resources")
command.Run = func(c *cobra.Command, args []string) {
@@ -142,25 +125,17 @@ func NewApplicationResourceActionsRunCommand(clientOpts *argocdclient.ClientOpti
}
appName := args[0]
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})
resources, err := appIf.ManagedResources(ctx, &application.ResourcesQuery{ApplicationName: &appName})
errors.CheckError(err)
filteredObjects := filterResources(command, resources.Items, group, kind, namespace, resourceName, all)
var resGroup = filteredObjects[0].GroupVersionKind().Group
for i := range filteredObjects[1:] {
if filteredObjects[i].GroupVersionKind().Group != resGroup {
log.Fatal("Ambiguous resource group. Use flag --group to specify resource group explicitly.")
}
}
for i := range filteredObjects {
obj := filteredObjects[i]
gvk := obj.GroupVersionKind()
objResourceName := obj.GetName()
_, err := appIf.RunResourceAction(context.Background(), &applicationpkg.ResourceActionRunRequest{
_, err := appIf.RunResourceAction(context.Background(), &application.ResourceActionRunRequest{
Name: &appName,
Namespace: obj.GetNamespace(),
ResourceName: objResourceName,

View File

@@ -3,110 +3,23 @@ package commands
import (
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
)
func Test_setHelmOpt(t *testing.T) {
t.Run("Zero", func(t *testing.T) {
src := v1alpha1.ApplicationSource{}
setHelmOpt(&src, helmOpts{})
assert.Nil(t, src.Helm)
})
t.Run("ValueFiles", func(t *testing.T) {
src := v1alpha1.ApplicationSource{}
setHelmOpt(&src, helmOpts{valueFiles: []string{"foo"}})
assert.Equal(t, []string{"foo"}, src.Helm.ValueFiles)
})
t.Run("ReleaseName", func(t *testing.T) {
src := v1alpha1.ApplicationSource{}
setHelmOpt(&src, helmOpts{releaseName: "foo"})
assert.Equal(t, "foo", src.Helm.ReleaseName)
})
t.Run("HelmSets", func(t *testing.T) {
src := v1alpha1.ApplicationSource{}
setHelmOpt(&src, helmOpts{helmSets: []string{"foo=bar"}})
assert.Equal(t, []v1alpha1.HelmParameter{{Name: "foo", Value: "bar"}}, src.Helm.Parameters)
})
t.Run("HelmSetStrings", func(t *testing.T) {
src := v1alpha1.ApplicationSource{}
setHelmOpt(&src, helmOpts{helmSetStrings: []string{"foo=bar"}})
assert.Equal(t, []v1alpha1.HelmParameter{{Name: "foo", Value: "bar", ForceString: true}}, src.Helm.Parameters)
})
t.Run("HelmSetFiles", func(t *testing.T) {
src := v1alpha1.ApplicationSource{}
setHelmOpt(&src, helmOpts{helmSetFiles: []string{"foo=bar"}})
assert.Equal(t, []v1alpha1.HelmFileParameter{{Name: "foo", Path: "bar"}}, src.Helm.FileParameters)
})
}
func Test_setJsonnetOpt(t *testing.T) {
t.Run("TlaSets", func(t *testing.T) {
src := v1alpha1.ApplicationSource{}
setJsonnetOpt(&src, []string{"foo=bar"}, false)
assert.Equal(t, []v1alpha1.JsonnetVar{{Name: "foo", Value: "bar"}}, src.Directory.Jsonnet.TLAs)
setJsonnetOpt(&src, []string{"bar=baz"}, false)
assert.Equal(t, []v1alpha1.JsonnetVar{{Name: "foo", Value: "bar"}, {Name: "bar", Value: "baz"}}, src.Directory.Jsonnet.TLAs)
})
t.Run("ExtSets", func(t *testing.T) {
src := v1alpha1.ApplicationSource{}
setJsonnetOptExtVar(&src, []string{"foo=bar"}, false)
assert.Equal(t, []v1alpha1.JsonnetVar{{Name: "foo", Value: "bar"}}, src.Directory.Jsonnet.ExtVars)
setJsonnetOptExtVar(&src, []string{"bar=baz"}, false)
assert.Equal(t, []v1alpha1.JsonnetVar{{Name: "foo", Value: "bar"}, {Name: "bar", Value: "baz"}}, src.Directory.Jsonnet.ExtVars)
})
}
type appOptionsFixture struct {
spec *v1alpha1.ApplicationSpec
command *cobra.Command
options *appOptions
}
func (f *appOptionsFixture) SetFlag(key, value string) error {
err := f.command.Flags().Set(key, value)
if err != nil {
return err
}
_ = setAppSpecOptions(f.command.Flags(), f.spec, f.options)
return err
}
func newAppOptionsFixture() *appOptionsFixture {
fixture := &appOptionsFixture{
spec: &v1alpha1.ApplicationSpec{},
command: &cobra.Command{},
options: &appOptions{},
}
addAppFlags(fixture.command, fixture.options)
return fixture
}
func Test_setAppSpecOptions(t *testing.T) {
f := newAppOptionsFixture()
t.Run("SyncPolicy", func(t *testing.T) {
assert.NoError(t, f.SetFlag("sync-policy", "automated"))
assert.NotNil(t, f.spec.SyncPolicy.Automated)
f.spec.SyncPolicy = nil
assert.NoError(t, f.SetFlag("sync-policy", "automatic"))
assert.NotNil(t, f.spec.SyncPolicy.Automated)
f.spec.SyncPolicy = nil
assert.NoError(t, f.SetFlag("sync-policy", "auto"))
assert.NotNil(t, f.spec.SyncPolicy.Automated)
assert.NoError(t, f.SetFlag("sync-policy", "none"))
assert.Nil(t, f.spec.SyncPolicy)
})
t.Run("SyncOptions", func(t *testing.T) {
assert.NoError(t, f.SetFlag("sync-option", "a=1"))
assert.True(t, f.spec.SyncPolicy.SyncOptions.HasOption("a=1"))
// remove the options using !
assert.NoError(t, f.SetFlag("sync-option", "!a=1"))
assert.Nil(t, f.spec.SyncPolicy)
})
func TestParseLabels(t *testing.T) {
validLabels := []string{"key=value", "foo=bar", "intuit=inc"}
result, err := parseLabels(validLabels)
assert.NoError(t, err)
assert.Len(t, result, 3)
invalidLabels := []string{"key=value", "too=many=equals"}
_, err = parseLabels(invalidLabels)
assert.Error(t, err)
emptyLabels := []string{}
result, err = parseLabels(emptyLabels)
assert.NoError(t, err)
assert.Len(t, result, 0)
}

View File

@@ -1,321 +0,0 @@
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"
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"
certutil "github.com/argoproj/argo-cd/util/cert"
)
// NewCertCommand returns a new instance of an `argocd repo` command
func NewCertCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "cert",
Short: "Manage repository certificates and SSH known hosts entries",
Run: func(c *cobra.Command, args []string) {
c.HelpFunc()(c, args)
os.Exit(1)
},
Example: ` # Add a TLS certificate for cd.example.com to ArgoCD cert store from a file
argocd cert add-tls --from ~/mycert.pem cd.example.com
# Add a TLS certificate for cd.example.com to ArgoCD via stdin
cat ~/mycert.pem | argocd cert add-tls cd.example.com
# Add SSH known host entries for cd.example.com to ArgoCD by scanning host
ssh-keyscan cd.example.com | argocd cert add-ssh --batch
# List all known TLS certificates
argocd cert list --cert-type https
# Remove all TLS certificates for cd.example.com
argocd cert rm --cert-type https cd.example.com
# Remove all certificates and SSH known host entries for cd.example.com
argocd cert rm cd.example.com
`,
}
command.AddCommand(NewCertAddSSHCommand(clientOpts))
command.AddCommand(NewCertAddTLSCommand(clientOpts))
command.AddCommand(NewCertListCommand(clientOpts))
command.AddCommand(NewCertRemoveCommand(clientOpts))
return command
}
func NewCertAddTLSCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
fromFile string
upsert bool
)
var command = &cobra.Command{
Use: "add-tls SERVERNAME",
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)
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
var certificateArray []string
var err error
if fromFile != "" {
fmt.Printf("Reading TLS certificate data in PEM format from '%s'\n", fromFile)
certificateArray, err = certutil.ParseTLSCertificatesFromPath(fromFile)
} else {
fmt.Println("Enter TLS certificate data in PEM format. Press CTRL-D when finished.")
certificateArray, err = certutil.ParseTLSCertificatesFromStream(os.Stdin)
}
errors.CheckError(err)
certificateList := make([]appsv1.RepositoryCertificate, 0)
subjectMap := make(map[string]*x509.Certificate)
for _, entry := range certificateArray {
// We want to make sure to only send valid certificate data to the
// server, so we decode the certificate into X509 structure before
// further processing it.
x509cert, err := certutil.DecodePEMCertificateToX509(entry)
errors.CheckError(err)
// TODO: We need a better way to detect duplicates sent in the stream,
// maybe by using fingerprints? For now, no two certs with the same
// subject may be sent.
if subjectMap[x509cert.Subject.String()] != nil {
fmt.Printf("ERROR: Cert with subject '%s' already seen in the input stream.\n", x509cert.Subject.String())
continue
} else {
subjectMap[x509cert.Subject.String()] = x509cert
}
}
serverName := args[0]
if len(certificateArray) > 0 {
certificateList = append(certificateList, appsv1.RepositoryCertificate{
ServerName: serverName,
CertType: "https",
CertData: []byte(strings.Join(certificateArray, "\n")),
})
certificates, err := certIf.CreateCertificate(context.Background(), &certificatepkg.RepositoryCertificateCreateRequest{
Certificates: &appsv1.RepositoryCertificateList{
Items: certificateList,
},
Upsert: upsert,
})
errors.CheckError(err)
fmt.Printf("Created entry with %d PEM certificates for repository server %s\n", len(certificates.Items), serverName)
} else {
fmt.Printf("No valid certificates have been detected in the stream.\n")
}
},
}
command.Flags().StringVar(&fromFile, "from", "", "read TLS certificate data from file (default is to read from stdin)")
command.Flags().BoolVar(&upsert, "upsert", false, "Replace existing TLS certificate if certificate is different in input")
return command
}
// NewCertAddCommand returns a new instance of an `argocd cert add` command
func NewCertAddSSHCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
fromFile string
batchProcess bool
upsert bool
certificates []appsv1.RepositoryCertificate
)
var command = &cobra.Command{
Use: "add-ssh --batch",
Short: "Add SSH known host entries for repository servers",
Run: func(c *cobra.Command, args []string) {
conn, certIf := argocdclient.NewClientOrDie(clientOpts).NewCertClientOrDie()
defer io.Close(conn)
var sshKnownHostsLists []string
var err error
// --batch is a flag, but it is mandatory for now.
if batchProcess {
if fromFile != "" {
fmt.Printf("Reading SSH known hosts entries from file '%s'\n", fromFile)
sshKnownHostsLists, err = certutil.ParseSSHKnownHostsFromPath(fromFile)
} else {
fmt.Println("Enter SSH known hosts entries, one per line. Press CTRL-D when finished.")
sshKnownHostsLists, err = certutil.ParseSSHKnownHostsFromStream(os.Stdin)
}
} else {
err = fmt.Errorf("You need to specify --batch or specify --help for usage instructions")
}
errors.CheckError(err)
if len(sshKnownHostsLists) == 0 {
errors.CheckError(fmt.Errorf("No valid SSH known hosts data found."))
}
for _, knownHostsEntry := range sshKnownHostsLists {
_, certSubType, certData, err := certutil.TokenizeSSHKnownHostsEntry(knownHostsEntry)
errors.CheckError(err)
hostnameList, _, 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)
}
}
certList := &appsv1.RepositoryCertificateList{Items: certificates}
response, err := certIf.CreateCertificate(context.Background(), &certificatepkg.RepositoryCertificateCreateRequest{
Certificates: certList,
Upsert: upsert,
})
errors.CheckError(err)
fmt.Printf("Successfully created %d SSH known host entries\n", len(response.Items))
},
}
command.Flags().StringVar(&fromFile, "from", "", "Read SSH known hosts data from file (default is to read from stdin)")
command.Flags().BoolVar(&batchProcess, "batch", false, "Perform batch processing by reading in SSH known hosts data (mandatory flag)")
command.Flags().BoolVar(&upsert, "upsert", false, "Replace existing SSH server public host keys if key is different in input")
return command
}
// NewCertRemoveCommand returns a new instance of an `argocd cert rm` command
func NewCertRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
certType string
certSubType string
certQuery certificatepkg.RepositoryCertificateQuery
)
var command = &cobra.Command{
Use: "rm REPOSERVER",
Short: "Remove certificate of TYPE for REPOSERVER",
Run: func(c *cobra.Command, args []string) {
if len(args) < 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
conn, certIf := argocdclient.NewClientOrDie(clientOpts).NewCertClientOrDie()
defer io.Close(conn)
hostNamePattern := args[0]
// Prevent the user from specifying a wildcard as hostname as precaution
// measure -- the user could still use "?*" or any other pattern to
// remove all certificates, but it's less likely that it happens by
// accident.
if hostNamePattern == "*" {
err := fmt.Errorf("A single wildcard is not allowed as REPOSERVER name.")
errors.CheckError(err)
}
certQuery = certificatepkg.RepositoryCertificateQuery{
HostNamePattern: hostNamePattern,
CertType: certType,
CertSubType: certSubType,
}
removed, err := certIf.DeleteCertificate(context.Background(), &certQuery)
errors.CheckError(err)
if len(removed.Items) > 0 {
for _, cert := range removed.Items {
fmt.Printf("Removed cert for '%s' of type '%s' (subtype '%s')\n", cert.ServerName, cert.CertType, cert.CertSubType)
}
} else {
fmt.Println("No certificates were removed (none matched the given patterns)")
}
},
}
command.Flags().StringVar(&certType, "cert-type", "", "Only remove certs of given type (ssh, https)")
command.Flags().StringVar(&certSubType, "cert-sub-type", "", "Only remove certs of given sub-type (only for ssh)")
return command
}
// NewCertListCommand returns a new instance of an `argocd cert rm` command
func NewCertListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
certType string
hostNamePattern string
sortOrder string
output string
)
var command = &cobra.Command{
Use: "list",
Short: "List configured certificates",
Run: func(c *cobra.Command, args []string) {
if certType != "" {
switch certType {
case "ssh":
case "https":
default:
fmt.Println("cert-type must be either ssh or https")
os.Exit(1)
}
}
conn, certIf := argocdclient.NewClientOrDie(clientOpts).NewCertClientOrDie()
defer io.Close(conn)
certificates, err := certIf.ListCertificates(context.Background(), &certificatepkg.RepositoryCertificateQuery{HostNamePattern: hostNamePattern, CertType: certType})
errors.CheckError(err)
switch output {
case "yaml", "json":
err := PrintResourceList(certificates.Items, output, false)
errors.CheckError(err)
case "wide", "":
printCertTable(certificates.Items, sortOrder)
default:
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
}
},
}
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide")
command.Flags().StringVar(&sortOrder, "sort", "", "set display sort order for output format wide. One of: hostname|type")
command.Flags().StringVar(&certType, "cert-type", "", "only list certificates of given type, valid: 'ssh','https'")
command.Flags().StringVar(&hostNamePattern, "hostname-pattern", "", "only list certificates for hosts matching given glob-pattern")
return command
}
// Print table of certificate info
func printCertTable(certs []appsv1.RepositoryCertificate, sortOrder string) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "HOSTNAME\tTYPE\tSUBTYPE\tINFO\n")
if sortOrder == "hostname" || sortOrder == "" {
sort.Slice(certs, func(i, j int) bool {
return certs[i].ServerName < certs[j].ServerName
})
} else if sortOrder == "type" {
sort.Slice(certs, func(i, j int) bool {
return certs[i].CertType < certs[j].CertType
})
}
for _, c := range certs {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", c.ServerName, c.CertType, c.CertSubType, c.CertInfo)
}
_ = w.Flush()
}

View File

@@ -9,8 +9,7 @@ import (
"strings"
"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"
"k8s.io/client-go/kubernetes"
@@ -18,10 +17,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/clusterauth"
"github.com/argoproj/argo-cd/server/cluster"
"github.com/argoproj/argo-cd/util"
)
// NewClusterCommand returns a new instance of an `argocd cluster` command
@@ -33,42 +33,25 @@ func NewClusterCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clientc
c.HelpFunc()(c, args)
os.Exit(1)
},
Example: ` # List all known clusters in JSON format:
argocd cluster list -o json
# Add a target cluster configuration to ArgoCD. The context must exist in your kubectl config:
argocd cluster add example-cluster
# Get specific details about a cluster in plain text (wide) format:
argocd cluster get example-cluster -o wide
# Remove a target cluster context from ArgoCD
argocd cluster rm example-cluster
`,
}
command.AddCommand(NewClusterAddCommand(clientOpts, pathOpts))
command.AddCommand(NewClusterGetCommand(clientOpts))
command.AddCommand(NewClusterListCommand(clientOpts))
command.AddCommand(NewClusterRemoveCommand(clientOpts))
command.AddCommand(NewClusterRotateAuthCommand(clientOpts))
return command
}
// NewClusterAddCommand returns a new instance of an `argocd cluster add` command
func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clientcmd.PathOptions) *cobra.Command {
var (
inCluster bool
upsert bool
serviceAccount string
awsRoleArn string
awsClusterName string
systemNamespace string
namespaces []string
name string
inCluster bool
upsert bool
awsRoleArn string
awsClusterName string
)
var command = &cobra.Command{
Use: "add CONTEXT",
Use: "add",
Short: fmt.Sprintf("%s cluster add CONTEXT", cliName),
Run: func(c *cobra.Command, args []string) {
var configAccess clientcmd.ConfigAccess = pathOpts
@@ -79,10 +62,9 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
}
config, err := configAccess.GetStartingConfig()
errors.CheckError(err)
contextName := args[0]
clstContext := config.Contexts[contextName]
clstContext := config.Contexts[args[0]]
if clstContext == nil {
log.Fatalf("Context %s does not exist in kubeconfig", contextName)
log.Fatalf("Context %s does not exist in kubeconfig", args[0])
}
overrides := clientcmd.ConfigOverrides{
@@ -103,40 +85,29 @@ 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 = common.InstallClusterManagerRBAC(clientset)
errors.CheckError(err)
}
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
defer io.Close(conn)
if name != "" {
contextName = name
}
clst := newCluster(contextName, namespaces, conf, managerBearerToken, awsAuthConf)
defer util.Close(conn)
clst := NewCluster(args[0], conf, managerBearerToken, awsAuthConf)
if inCluster {
clst.Server = common.KubernetesInternalAPIServerAddr
}
clstCreateReq := clusterpkg.ClusterCreateRequest{
clstCreateReq := cluster.ClusterCreateRequest{
Cluster: clst,
Upsert: upsert,
}
_, err = clusterIf.Create(context.Background(), &clstCreateReq)
clst, err = clusterIf.Create(context.Background(), &clstCreateReq)
errors.CheckError(err)
fmt.Printf("Cluster '%s' added\n", clst.Server)
fmt.Printf("Cluster '%s' added\n", clst.Name)
},
}
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")
command.Flags().StringVar(&name, "name", "", "Overwrite the cluster name")
return command
}
@@ -179,144 +150,74 @@ func printKubeContexts(ca clientcmd.ConfigAccess) {
}
}
func newCluster(name string, namespaces []string, conf *rest.Config, managerBearerToken string, awsAuthConf *argoappv1.AWSAuthConfig) *argoappv1.Cluster {
func NewCluster(name string, conf *rest.Config, managerBearerToken string, awsAuthConf *argoappv1.AWSAuthConfig) *argoappv1.Cluster {
tlsClientConfig := argoappv1.TLSClientConfig{
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,
Server: conf.Host,
Name: name,
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
}
// NewClusterGetCommand returns a new instance of an `argocd cluster get` command
func NewClusterGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
output string
)
var command = &cobra.Command{
Use: "get SERVER",
Short: "Get cluster information",
Example: `argocd cluster get https://12.34.567.89`,
Use: "get",
Short: "Get cluster information",
Run: func(c *cobra.Command, args []string) {
if len(args) == 0 {
c.HelpFunc()(c, args)
os.Exit(1)
}
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
defer io.Close(conn)
clusters := make([]argoappv1.Cluster, 0)
defer util.Close(conn)
for _, clusterName := range args {
clst, err := clusterIf.Get(context.Background(), &clusterpkg.ClusterQuery{Server: clusterName})
clst, err := clusterIf.Get(context.Background(), &cluster.ClusterQuery{Server: clusterName})
errors.CheckError(err)
clusters = append(clusters, *clst)
}
switch output {
case "yaml", "json":
err := PrintResourceList(clusters, output, true)
yamlBytes, err := yaml.Marshal(clst)
errors.CheckError(err)
case "wide", "":
printClusterDetails(clusters)
case "server":
printClusterServers(clusters)
default:
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
fmt.Printf("%v", string(yamlBytes))
}
},
}
// we have yaml as default to not break backwards-compatibility
command.Flags().StringVarP(&output, "output", "o", "yaml", "Output format. One of: json|yaml|wide|server")
return command
}
func strWithDefault(value string, def string) string {
if value == "" {
return def
}
return value
}
func formatNamespaces(cluster argoappv1.Cluster) string {
if len(cluster.Namespaces) == 0 {
return "all namespaces"
}
return strings.Join(cluster.Namespaces, ", ")
}
func printClusterDetails(clusters []argoappv1.Cluster) {
for _, cluster := range clusters {
fmt.Printf("Cluster information\n\n")
fmt.Printf(" Server URL: %s\n", cluster.Server)
fmt.Printf(" Server Name: %s\n", strWithDefault(cluster.Name, "-"))
fmt.Printf(" Server Version: %s\n", cluster.ServerVersion)
fmt.Printf(" Namespaces: %s\n", formatNamespaces(cluster))
fmt.Printf("\nTLS configuration\n\n")
fmt.Printf(" Client cert: %v\n", string(cluster.Config.TLSClientConfig.CertData) != "")
fmt.Printf(" Cert validation: %v\n", !cluster.Config.TLSClientConfig.Insecure)
fmt.Printf("\nAuthentication\n\n")
fmt.Printf(" Basic authentication: %v\n", cluster.Config.Username != "")
fmt.Printf(" oAuth authentication: %v\n", cluster.Config.BearerToken != "")
fmt.Printf(" AWS authentication: %v\n", cluster.Config.AWSAuthConfig != nil)
fmt.Println()
}
}
// NewClusterRemoveCommand returns a new instance of an `argocd cluster list` command
func NewClusterRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "rm SERVER",
Short: "Remove cluster credentials",
Example: `argocd cluster rm https://12.34.567.89`,
Use: "rm",
Short: "Remove cluster credentials",
Run: func(c *cobra.Command, args []string) {
if len(args) == 0 {
c.HelpFunc()(c, args)
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)
for _, clusterName := range args {
// TODO(jessesuen): find the right context and remove manager RBAC artifacts
// err := clusterauth.UninstallClusterManagerRBAC(clientset)
// err := common.UninstallClusterManagerRBAC(clientset)
// errors.CheckError(err)
_, err := clusterIf.Delete(context.Background(), &clusterpkg.ClusterQuery{Server: clusterName})
_, err := clusterIf.Delete(context.Background(), &cluster.ClusterQuery{Server: clusterName})
errors.CheckError(err)
}
},
@@ -324,76 +225,22 @@ func NewClusterRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
return command
}
// Print table of cluster information
func printClusterTable(clusters []argoappv1.Cluster) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintf(w, "SERVER\tNAME\tVERSION\tSTATUS\tMESSAGE\n")
for _, c := range clusters {
server := c.Server
if len(c.Namespaces) > 0 {
server = fmt.Sprintf("%s (%d namespaces)", c.Server, len(c.Namespaces))
}
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", server, c.Name, c.ServerVersion, c.ConnectionState.Status, c.ConnectionState.Message)
}
_ = w.Flush()
}
// Print list of cluster servers
func printClusterServers(clusters []argoappv1.Cluster) {
for _, c := range clusters {
fmt.Println(c.Server)
}
}
// NewClusterListCommand returns a new instance of an `argocd cluster rm` command
func NewClusterListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
output string
)
var command = &cobra.Command{
Use: "list",
Short: "List configured clusters",
Run: func(c *cobra.Command, args []string) {
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
defer io.Close(conn)
clusters, err := clusterIf.List(context.Background(), &clusterpkg.ClusterQuery{})
defer util.Close(conn)
clusters, err := clusterIf.List(context.Background(), &cluster.ClusterQuery{})
errors.CheckError(err)
switch output {
case "yaml", "json":
err := PrintResourceList(clusters.Items, output, false)
errors.CheckError(err)
case "server":
printClusterServers(clusters.Items)
case "wide", "":
printClusterTable(clusters.Items)
default:
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "SERVER\tNAME\tSTATUS\tMESSAGE\n")
for _, c := range clusters.Items {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", c.Server, c.Name, c.ConnectionState.Status, c.ConnectionState.Message)
}
},
}
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|server")
return command
}
// NewClusterRotateAuthCommand returns a new instance of an `argocd cluster rotate-auth` command
func NewClusterRotateAuthCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "rotate-auth SERVER",
Short: fmt.Sprintf("%s cluster rotate-auth SERVER", cliName),
Example: fmt.Sprintf("%s cluster rotate-auth https://12.34.567.89", cliName),
Run: func(c *cobra.Command, args []string) {
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
defer io.Close(conn)
clusterQuery := clusterpkg.ClusterQuery{
Server: args[0],
}
_, err := clusterIf.RotateAuth(context.Background(), &clusterQuery)
errors.CheckError(err)
fmt.Printf("Cluster '%s' rotated auth\n", clusterQuery.Server)
_ = w.Flush()
},
}
return command

View File

@@ -1,83 +0,0 @@
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"
)
func Test_printClusterTable(t *testing.T) {
printClusterTable([]v1alpha1.Cluster{
{
Server: "my-server",
Name: "my-name",
Config: v1alpha1.ClusterConfig{
Username: "my-username",
Password: "my-password",
BearerToken: "my-bearer-token",
TLSClientConfig: v1alpha1.TLSClientConfig{},
AWSAuthConfig: nil,
},
ConnectionState: v1alpha1.ConnectionState{
Status: "my-status",
Message: "my-message",
ModifiedAt: &metav1.Time{},
},
ServerVersion: "my-version",
},
})
}
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

@@ -1,13 +1,5 @@
package commands
import (
"encoding/json"
"fmt"
"reflect"
"github.com/ghodss/yaml"
)
const (
cliName = "argocd"
@@ -15,58 +7,3 @@ const (
// the OAuth2 login flow.
DefaultSSOLocalPort = 8085
)
// PrintResource prints a single resource in YAML or JSON format to stdout according to the output format
func PrintResource(resource interface{}, output string) error {
switch output {
case "json":
jsonBytes, err := json.MarshalIndent(resource, "", " ")
if err != nil {
return err
}
fmt.Println(string(jsonBytes))
case "yaml":
yamlBytes, err := yaml.Marshal(resource)
if err != nil {
return err
}
fmt.Print(string(yamlBytes))
default:
return fmt.Errorf("unknown output format: %s", output)
}
return nil
}
// PrintResourceList marshals & prints a list of resources to stdout according to the output format
func PrintResourceList(resources interface{}, output string, single bool) error {
kt := reflect.ValueOf(resources)
// Sometimes, we want to marshal the first resource of a slice or array as single item
if kt.Kind() == reflect.Slice || kt.Kind() == reflect.Array {
if single && kt.Len() == 1 {
return PrintResource(kt.Index(0).Interface(), output)
}
// If we have a zero len list, prevent printing "null"
if kt.Len() == 0 {
return PrintResource([]string{}, output)
}
}
switch output {
case "json":
jsonBytes, err := json.MarshalIndent(resources, "", " ")
if err != nil {
return err
}
fmt.Println(string(jsonBytes))
case "yaml":
yamlBytes, err := yaml.Marshal(resources)
if err != nil {
return err
}
fmt.Print(string(yamlBytes))
default:
return fmt.Errorf("unknown output format: %s", output)
}
return nil
}

View File

@@ -1,142 +0,0 @@
package commands
import (
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
// Be careful with tabs vs. spaces in the following expected formats. Indents
// should all be spaces, no tabs.
const expectYamlSingle = `bar: ""
baz: foo
foo: bar
`
const expectJsonSingle = `{
"bar": "",
"baz": "foo",
"foo": "bar"
}
`
const expectYamlList = `one:
bar: ""
baz: foo
foo: bar
two:
bar: ""
baz: foo
foo: bar
`
const expectJsonList = `{
"one": {
"bar": "",
"baz": "foo",
"foo": "bar"
},
"two": {
"bar": "",
"baz": "foo",
"foo": "bar"
}
}
`
// Rather dirty hack to capture stdout from PrintResource() and PrintResourceList()
func captureOutput(f func() error) (string, error) {
stdout := os.Stdout
r, w, err := os.Pipe()
if err != nil {
return "", err
}
os.Stdout = w
err = f()
w.Close()
if err != nil {
os.Stdout = stdout
return "", err
}
str, err := ioutil.ReadAll(r)
os.Stdout = stdout
if err != nil {
return "", err
}
return string(str), err
}
func Test_PrintResource(t *testing.T) {
testResource := map[string]string{
"foo": "bar",
"bar": "",
"baz": "foo",
}
str, err := captureOutput(func() error {
err := PrintResource(testResource, "yaml")
return err
})
assert.NoError(t, err)
assert.Equal(t, str, expectYamlSingle)
str, err = captureOutput(func() error {
err := PrintResource(testResource, "json")
return err
})
assert.NoError(t, err)
assert.Equal(t, str, expectJsonSingle)
err = PrintResource(testResource, "unknown")
assert.Error(t, err)
}
func Test_PrintResourceList(t *testing.T) {
testResource := map[string]map[string]string{
"one": {
"foo": "bar",
"bar": "",
"baz": "foo",
},
"two": {
"foo": "bar",
"bar": "",
"baz": "foo",
},
}
testResource2 := make([]map[string]string, 0)
testResource2 = append(testResource2, testResource["one"])
str, err := captureOutput(func() error {
err := PrintResourceList(testResource, "yaml", false)
return err
})
assert.NoError(t, err)
assert.Equal(t, str, expectYamlList)
str, err = captureOutput(func() error {
err := PrintResourceList(testResource, "json", false)
return err
})
assert.NoError(t, err)
assert.Equal(t, str, expectJsonList)
str, err = captureOutput(func() error {
err := PrintResourceList(testResource2, "yaml", true)
return err
})
assert.NoError(t, err)
assert.Equal(t, str, expectYamlSingle)
str, err = captureOutput(func() error {
err := PrintResourceList(testResource2, "json", true)
return err
})
assert.NoError(t, err)
assert.Equal(t, str, expectJsonSingle)
err = PrintResourceList(testResource, "unknown", false)
assert.Error(t, err)
}

View File

@@ -1,233 +0,0 @@
package commands
import (
"fmt"
"io"
"os"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
const (
bashCompletionFunc = `
__argocd_list_apps() {
local -a argocd_out
if argocd_out=($(argocd app list --output name 2>/dev/null)); then
COMPREPLY+=( $( compgen -W "${argocd_out[*]}" -- "$cur" ) )
fi
}
__argocd_list_app_history() {
local app=$1
local -a argocd_out
if argocd_out=($(argocd app history $app --output id 2>/dev/null)); then
COMPREPLY+=( $( compgen -W "${argocd_out[*]}" -- "$cur" ) )
fi
}
__argocd_app_rollback() {
local -a command
for comp_word in "${COMP_WORDS[@]}"; do
if [[ $comp_word =~ ^-.*$ ]]; then
continue
fi
command+=($comp_word)
done
# fourth arg is app (if present): e.g.- argocd app rollback guestbook
local app=${command[3]}
local id=${command[4]}
if [[ -z $app || $app == $cur ]]; then
__argocd_list_apps
elif [[ -z $id || $id == $cur ]]; then
__argocd_list_app_history $app
fi
}
__argocd_list_servers() {
local -a argocd_out
if argocd_out=($(argocd cluster list --output server 2>/dev/null)); then
COMPREPLY+=( $( compgen -W "${argocd_out[*]}" -- "$cur" ) )
fi
}
__argocd_list_repos() {
local -a argocd_out
if argocd_out=($(argocd repo list --output url 2>/dev/null)); then
COMPREPLY+=( $( compgen -W "${argocd_out[*]}" -- "$cur" ) )
fi
}
__argocd_list_projects() {
local -a argocd_out
if argocd_out=($(argocd proj list --output name 2>/dev/null)); then
COMPREPLY+=( $( compgen -W "${argocd_out[*]}" -- "$cur" ) )
fi
}
__argocd_list_namespaces() {
local -a argocd_out
if argocd_out=($(kubectl get namespaces --no-headers 2>/dev/null | cut -f1 -d' ' 2>/dev/null)); then
COMPREPLY+=( $( compgen -W "${argocd_out[*]}" -- "$cur" ) )
fi
}
__argocd_proj_server_namespace() {
local -a command
for comp_word in "${COMP_WORDS[@]}"; do
if [[ $comp_word =~ ^-.*$ ]]; then
continue
fi
command+=($comp_word)
done
# expect something like this: argocd proj add-destination PROJECT SERVER NAMESPACE
local project=${command[3]}
local server=${command[4]}
local namespace=${command[5]}
if [[ -z $project || $project == $cur ]]; then
__argocd_list_projects
elif [[ -z $server || $server == $cur ]]; then
__argocd_list_servers
elif [[ -z $namespace || $namespace == $cur ]]; then
__argocd_list_namespaces
fi
}
__argocd_list_project_role() {
local project="$1"
local -a argocd_out
if argocd_out=($(argocd proj role list "$project" --output=name 2>/dev/null)); then
COMPREPLY+=( $( compgen -W "${argocd_out[*]}" -- "$cur" ) )
fi
}
__argocd_proj_role(){
local -a command
for comp_word in "${COMP_WORDS[@]}"; do
if [[ $comp_word =~ ^-.*$ ]]; then
continue
fi
command+=($comp_word)
done
# expect something like this: argocd proj role add-policy PROJECT ROLE-NAME
local project=${command[4]}
local role=${command[5]}
if [[ -z $project || $project == $cur ]]; then
__argocd_list_projects
elif [[ -z $role || $role == $cur ]]; then
__argocd_list_project_role $project
fi
}
__argocd_custom_func() {
case ${last_command} in
argocd_app_delete | \
argocd_app_diff | \
argocd_app_edit | \
argocd_app_get | \
argocd_app_history | \
argocd_app_manifests | \
argocd_app_patch-resource | \
argocd_app_set | \
argocd_app_sync | \
argocd_app_terminate-op | \
argocd_app_unset | \
argocd_app_wait | \
argocd_app_create)
__argocd_list_apps
return
;;
argocd_app_rollback)
__argocd_app_rollback
return
;;
argocd_cluster_get | \
argocd_cluster_rm | \
argocd_login | \
argocd_cluster_add)
__argocd_list_servers
return
;;
argocd_repo_rm | \
argocd_repo_add)
__argocd_list_repos
return
;;
argocd_proj_add-destination | \
argocd_proj_remove-destination)
__argocd_proj_server_namespace
return
;;
argocd_proj_add-source | \
argocd_proj_remove-source | \
argocd_proj_allow-cluster-resource | \
argocd_proj_allow-namespace-resource | \
argocd_proj_deny-cluster-resource | \
argocd_proj_deny-namespace-resource | \
argocd_proj_delete | \
argocd_proj_edit | \
argocd_proj_get | \
argocd_proj_set | \
argocd_proj_role_list)
__argocd_list_projects
return
;;
argocd_proj_role_remove-policy | \
argocd_proj_role_add-policy | \
argocd_proj_role_create | \
argocd_proj_role_delete | \
argocd_proj_role_get | \
argocd_proj_role_create-token | \
argocd_proj_role_delete-token)
__argocd_proj_role
return
;;
*)
;;
esac
}
`
)
func NewCompletionCommand() *cobra.Command {
var command = &cobra.Command{
Use: "completion SHELL",
Short: "output shell completion code for the specified shell (bash or zsh)",
Long: `Write bash or zsh shell completion code to standard output.
For bash, ensure you have bash completions installed and enabled.
To access completions in your current shell, run
$ source <(argocd completion bash)
Alternatively, write it to a file and source in .bash_profile
For zsh, output to a file in a directory referenced by the $fpath shell
variable.
`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 1 {
cmd.HelpFunc()(cmd, args)
os.Exit(1)
}
shell := args[0]
rootCommand := NewCommand()
rootCommand.BashCompletionFunction = bashCompletionFunc
availableCompletions := map[string]func(io.Writer) error{
"bash": rootCommand.GenBashCompletion,
"zsh": rootCommand.GenZshCompletion,
}
completion, ok := availableCompletions[shell]
if !ok {
fmt.Printf("Invalid shell '%s'. The supported shells are bash and zsh.\n", shell)
os.Exit(1)
}
if err := completion(os.Stdout); err != nil {
log.Fatal(err)
}
},
}
return command
}

View File

@@ -8,43 +8,26 @@ 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"
)
// NewContextCommand returns a new instance of an `argocd ctx` command
func NewContextCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var delete bool
var command = &cobra.Command{
Use: "context [CONTEXT]",
Use: "context",
Aliases: []string{"ctx"},
Short: "Switch between contexts",
Run: func(c *cobra.Command, args []string) {
localCfg, err := localconfig.ReadLocalConfig(clientOpts.ConfigPath)
errors.CheckError(err)
if delete {
if len(args) == 0 {
c.HelpFunc()(c, args)
os.Exit(1)
}
err := deleteContext(args[0], clientOpts.ConfigPath)
errors.CheckError(err)
return
}
if len(args) == 0 {
printArgoCDContexts(clientOpts.ConfigPath)
return
}
ctxName := args[0]
argoCDDir, err := localconfig.DefaultConfigDir()
errors.CheckError(err)
prevCtxFile := path.Join(argoCDDir, ".prev-ctx")
@@ -54,6 +37,8 @@ func NewContextCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
errors.CheckError(err)
ctxName = string(prevCtxBytes)
}
localCfg, err := localconfig.ReadLocalConfig(clientOpts.ConfigPath)
errors.CheckError(err)
if localCfg.CurrentContext == ctxName {
fmt.Printf("Already at context '%s'\n", localCfg.CurrentContext)
return
@@ -63,7 +48,6 @@ func NewContextCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
}
prevCtx := localCfg.CurrentContext
localCfg.CurrentContext = ctxName
err = localconfig.WriteLocalConfig(*localCfg, clientOpts.ConfigPath)
errors.CheckError(err)
err = ioutil.WriteFile(prevCtxFile, []byte(prevCtx), 0644)
@@ -71,43 +55,9 @@ func NewContextCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
fmt.Printf("Switched to context '%s'\n", localCfg.CurrentContext)
},
}
command.Flags().BoolVar(&delete, "delete", false, "Delete the context instead of switching to it")
return command
}
func deleteContext(context, configPath string) error {
localCfg, err := localconfig.ReadLocalConfig(configPath)
errors.CheckError(err)
if localCfg == nil {
return fmt.Errorf("Nothing to logout from")
}
serverName, ok := localCfg.RemoveContext(context)
if !ok {
return fmt.Errorf("Context %s does not exist", context)
}
_ = localCfg.RemoveUser(context)
_ = localCfg.RemoveServer(serverName)
if localCfg.IsEmpty() {
err = localconfig.DeleteLocalConfig(configPath)
errors.CheckError(err)
} else {
if localCfg.CurrentContext == context {
localCfg.CurrentContext = ""
}
err = localconfig.ValidateLocalConfig(*localCfg)
if err != nil {
return fmt.Errorf("Error in logging out")
}
err = localconfig.WriteLocalConfig(*localCfg, configPath)
errors.CheckError(err)
}
fmt.Printf("Context '%s' deleted\n", context)
return nil
}
func printArgoCDContexts(configPath string) {
localCfg, err := localconfig.ReadLocalConfig(configPath)
errors.CheckError(err)

View File

@@ -1,81 +0,0 @@
package commands
import (
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/argoproj/argo-cd/util/localconfig"
)
const testConfig = `contexts:
- name: argocd1.example.com:443
server: argocd1.example.com:443
user: argocd1.example.com:443
- name: argocd2.example.com:443
server: argocd2.example.com:443
user: argocd2.example.com:443
- name: localhost:8080
server: localhost:8080
user: localhost:8080
current-context: localhost:8080
servers:
- server: argocd1.example.com:443
- server: argocd2.example.com:443
- plain-text: true
server: localhost:8080
users:
- auth-token: vErrYS3c3tReFRe$hToken
name: argocd1.example.com:443
refresh-token: vErrYS3c3tReFRe$hToken
- auth-token: vErrYS3c3tReFRe$hToken
name: argocd2.example.com:443
refresh-token: vErrYS3c3tReFRe$hToken
- auth-token: vErrYS3c3tReFRe$hToken
name: localhost:8080`
const testConfigFilePath = "./testdata/config"
func TestContextDelete(t *testing.T) {
// Write the test config file
err := ioutil.WriteFile(testConfigFilePath, []byte(testConfig), os.ModePerm)
assert.NoError(t, err)
localConfig, err := localconfig.ReadLocalConfig(testConfigFilePath)
assert.NoError(t, err)
assert.Equal(t, localConfig.CurrentContext, "localhost:8080")
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "localhost:8080", Server: "localhost:8080", User: "localhost:8080"})
// Delete a non-current context
err = deleteContext("argocd1.example.com:443", testConfigFilePath)
assert.NoError(t, err)
localConfig, err = localconfig.ReadLocalConfig(testConfigFilePath)
assert.NoError(t, err)
assert.Equal(t, localConfig.CurrentContext, "localhost:8080")
assert.NotContains(t, localConfig.Contexts, localconfig.ContextRef{Name: "argocd1.example.com:443", Server: "argocd1.example.com:443", User: "argocd1.example.com:443"})
assert.NotContains(t, localConfig.Servers, localconfig.Server{Server: "argocd1.example.com:443"})
assert.NotContains(t, localConfig.Users, localconfig.User{AuthToken: "vErrYS3c3tReFRe$hToken", Name: "argocd1.example.com:443"})
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "argocd2.example.com:443", Server: "argocd2.example.com:443", User: "argocd2.example.com:443"})
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "localhost:8080", Server: "localhost:8080", User: "localhost:8080"})
// Delete the current context
err = deleteContext("localhost:8080", testConfigFilePath)
assert.NoError(t, err)
localConfig, err = localconfig.ReadLocalConfig(testConfigFilePath)
assert.NoError(t, err)
assert.Equal(t, localConfig.CurrentContext, "")
assert.NotContains(t, localConfig.Contexts, localconfig.ContextRef{Name: "localhost:8080", Server: "localhost:8080", User: "localhost:8080"})
assert.NotContains(t, localConfig.Servers, localconfig.Server{PlainText: true, Server: "localhost:8080"})
assert.NotContains(t, localConfig.Users, localconfig.User{AuthToken: "vErrYS3c3tReFRe$hToken", Name: "localhost:8080"})
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "argocd2.example.com:443", Server: "argocd2.example.com:443", User: "argocd2.example.com:443"})
// Write the file again so that no conflicts are made in git
err = ioutil.WriteFile(testConfigFilePath, []byte(testConfig), os.ModePerm)
assert.NoError(t, err)
}

View File

@@ -1,162 +0,0 @@
package commands
import (
"context"
"fmt"
"io/ioutil"
"os"
"strings"
"text/tabwriter"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
argoio "github.com/argoproj/gitops-engine/pkg/utils/io"
"github.com/spf13/cobra"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
gpgkeypkg "github.com/argoproj/argo-cd/pkg/apiclient/gpgkey"
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
)
// NewGPGCommand returns a new instance of an `argocd repo` command
func NewGPGCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "gpg",
Short: "Manage GPG keys used for signature verification",
Run: func(c *cobra.Command, args []string) {
c.HelpFunc()(c, args)
os.Exit(1)
},
Example: ``,
}
command.AddCommand(NewGPGListCommand(clientOpts))
command.AddCommand(NewGPGGetCommand(clientOpts))
command.AddCommand(NewGPGAddCommand(clientOpts))
command.AddCommand(NewGPGDeleteCommand(clientOpts))
return command
}
// NewGPGListCommand lists all configured public keys from the server
func NewGPGListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
output string
)
var command = &cobra.Command{
Use: "list",
Short: "List configured GPG public keys",
Run: func(c *cobra.Command, args []string) {
conn, gpgIf := argocdclient.NewClientOrDie(clientOpts).NewGPGKeyClientOrDie()
defer argoio.Close(conn)
keys, err := gpgIf.List(context.Background(), &gpgkeypkg.GnuPGPublicKeyQuery{})
errors.CheckError(err)
switch output {
case "yaml", "json":
err := PrintResourceList(keys.Items, output, false)
errors.CheckError(err)
case "wide", "":
printKeyTable(keys.Items)
default:
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
}
},
}
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide")
return command
}
// NewGPGGetCommand retrieves a single public key from the server
func NewGPGGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
output string
)
var command = &cobra.Command{
Use: "get KEYID",
Short: "Get the GPG public key with ID <KEYID> from the server",
Run: func(c *cobra.Command, args []string) {
if len(args) != 1 {
errors.CheckError(fmt.Errorf("Missing KEYID argument"))
}
conn, gpgIf := argocdclient.NewClientOrDie(clientOpts).NewGPGKeyClientOrDie()
defer argoio.Close(conn)
key, err := gpgIf.Get(context.Background(), &gpgkeypkg.GnuPGPublicKeyQuery{KeyID: args[0]})
errors.CheckError(err)
switch output {
case "yaml", "json":
err := PrintResourceList(key, output, false)
errors.CheckError(err)
case "wide", "":
fmt.Printf("Key ID: %s\n", key.KeyID)
fmt.Printf("Key fingerprint: %s\n", key.Fingerprint)
fmt.Printf("Key subtype: %s\n", strings.ToUpper(key.SubType))
fmt.Printf("Key owner: %s\n", key.Owner)
fmt.Printf("Key data follows until EOF:\n%s\n", key.KeyData)
default:
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
}
},
}
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide")
return command
}
// NewGPGAddCommand adds a public key to the server's configuration
func NewGPGAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
fromFile string
)
var command = &cobra.Command{
Use: "add",
Short: "Adds a GPG public key to the server's keyring",
Run: func(c *cobra.Command, args []string) {
if fromFile == "" {
errors.CheckError(fmt.Errorf("--from is mandatory"))
}
keyData, err := ioutil.ReadFile(fromFile)
if err != nil {
errors.CheckError(err)
}
conn, gpgIf := argocdclient.NewClientOrDie(clientOpts).NewGPGKeyClientOrDie()
defer argoio.Close(conn)
resp, err := gpgIf.Create(context.Background(), &gpgkeypkg.GnuPGPublicKeyCreateRequest{Publickey: &appsv1.GnuPGPublicKey{KeyData: string(keyData)}})
errors.CheckError(err)
fmt.Printf("Created %d key(s) from input file", len(resp.Created.Items))
if len(resp.Skipped) > 0 {
fmt.Printf(", and %d key(s) were skipped because they exist already", len(resp.Skipped))
}
fmt.Printf(".\n")
},
}
command.Flags().StringVarP(&fromFile, "from", "f", "", "Path to the file that contains the GPG public key to import")
return command
}
// NewGPGDeleteCommand removes a key from the server's keyring
func NewGPGDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "rm KEYID",
Short: "Removes a GPG public key from the server's keyring",
Run: func(c *cobra.Command, args []string) {
if len(args) != 1 {
errors.CheckError(fmt.Errorf("Missing KEYID argument"))
}
conn, gpgIf := argocdclient.NewClientOrDie(clientOpts).NewGPGKeyClientOrDie()
defer argoio.Close(conn)
_, err := gpgIf.Delete(context.Background(), &gpgkeypkg.GnuPGPublicKeyQuery{KeyID: args[0]})
errors.CheckError(err)
fmt.Printf("Deleted key with key ID %s\n", args[0])
},
}
return command
}
// Print table of certificate info
func printKeyTable(keys []appsv1.GnuPGPublicKey) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "KEYID\tTYPE\tIDENTITY\n")
for _, k := range keys {
fmt.Fprintf(w, "%s\t%s\t%s\n", k.KeyID, strings.ToUpper(k.SubType), k.Owner)
}
_ = w.Flush()
}

View File

@@ -2,31 +2,26 @@ package commands
import (
"context"
"crypto/sha256"
"encoding/base64"
"fmt"
"html"
"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"
oidc "github.com/coreos/go-oidc"
jwt "github.com/dgrijalva/jwt-go"
log "github.com/sirupsen/logrus"
"github.com/skratchdot/open-golang/open"
"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/server/session"
"github.com/argoproj/argo-cd/server/settings"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/cli"
grpc_util "github.com/argoproj/argo-cd/util/grpc"
jwtutil "github.com/argoproj/argo-cd/util/jwt"
"github.com/argoproj/argo-cd/util/localconfig"
oidcutil "github.com/argoproj/argo-cd/util/oidc"
"github.com/argoproj/argo-cd/util/rand"
@@ -46,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
@@ -107,18 +88,18 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
httpClient, err := acdClient.HTTPClient()
errors.CheckError(err)
ctx = oidc.ClientContext(ctx, httpClient)
acdSet, err := setIf.Get(ctx, &settingspkg.SettingsQuery{})
acdSet, err := setIf.Get(ctx, &settings.SettingsQuery{})
errors.CheckError(err)
oauth2conf, provider, err := acdClient.OIDCConfig(ctx, acdSet)
errors.CheckError(err)
tokenString, refreshToken = oauth2Login(ctx, ssoPort, acdSet.GetOIDCConfig(), oauth2conf, provider)
tokenString, refreshToken = oauth2Login(ctx, ssoPort, oauth2conf, provider)
}
parser := &jwt.Parser{
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))
@@ -129,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,
@@ -163,18 +143,18 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
}
func userDisplayName(claims jwt.MapClaims) string {
if email := jwtutil.StringField(claims, "email"); email != "" {
return email
if email, ok := claims["email"]; ok && email != nil {
return email.(string)
}
if name := jwtutil.StringField(claims, "name"); name != "" {
return name
if name, ok := claims["name"]; ok && name != nil {
return name.(string)
}
return jwtutil.StringField(claims, "sub")
return claims["sub"].(string)
}
// oauth2Login opens a browser, runs a temporary HTTP server to delegate OAuth2 login flow and
// returns the JWT token and a refresh token (if supported)
func oauth2Login(ctx context.Context, port int, oidcSettings *settingspkg.OIDCConfig, oauth2conf *oauth2.Config, provider *oidc.Provider) (string, string) {
func oauth2Login(ctx context.Context, port int, oauth2conf *oauth2.Config, provider *oidc.Provider) (string, string) {
oauth2conf.RedirectURL = fmt.Sprintf("http://localhost:%d/auth/callback", port)
oidcConf, err := oidcutil.ParseConfig(provider)
errors.CheckError(err)
@@ -192,22 +172,17 @@ func oauth2Login(ctx context.Context, port int, oidcSettings *settingspkg.OIDCCo
var refreshToken string
handleErr := func(w http.ResponseWriter, errMsg string) {
http.Error(w, html.EscapeString(errMsg), http.StatusBadRequest)
http.Error(w, errMsg, http.StatusBadRequest)
completionChan <- errMsg
}
// PKCE implementation of https://tools.ietf.org/html/rfc7636
codeVerifier := rand.RandStringCharset(43, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~")
codeChallengeHash := sha256.Sum256([]byte(codeVerifier))
codeChallenge := base64.RawURLEncoding.EncodeToString(codeChallengeHash[:])
// Authorization redirect callback from OAuth2 auth flow.
// Handles both implicit and authorization code flow
callbackHandler := func(w http.ResponseWriter, r *http.Request) {
log.Debugf("Callback: %s", r.URL)
if formErr := r.FormValue("error"); formErr != "" {
handleErr(w, fmt.Sprintf("%s: %s", formErr, r.FormValue("error_description")))
handleErr(w, formErr+": "+r.FormValue("error_description"))
return
}
@@ -240,8 +215,7 @@ func oauth2Login(ctx context.Context, port int, oidcSettings *settingspkg.OIDCCo
handleErr(w, fmt.Sprintf("no code in request: %q", r.Form))
return
}
opts := []oauth2.AuthCodeOption{oauth2.SetAuthURLParam("code_verifier", codeVerifier)}
tok, err := oauth2conf.Exchange(ctx, code, opts...)
tok, err := oauth2conf.Exchange(ctx, code)
if err != nil {
handleErr(w, err.Error())
return
@@ -269,30 +243,22 @@ func oauth2Login(ctx context.Context, port int, oidcSettings *settingspkg.OIDCCo
fmt.Printf("Opening browser for authentication\n")
var url string
grantType := oidcutil.InferGrantType(oidcConf)
opts := []oauth2.AuthCodeOption{oauth2.AccessTypeOffline}
if claimsRequested := oidcSettings.GetIDTokenClaims(); claimsRequested != nil {
opts = oidcutil.AppendClaimsAuthenticationRequestParameter(opts, claimsRequested)
}
grantType := oidcutil.InferGrantType(oauth2conf, oidcConf)
switch grantType {
case oidcutil.GrantTypeAuthorizationCode:
opts = append(opts, oauth2.SetAuthURLParam("code_challenge", codeChallenge))
opts = append(opts, oauth2.SetAuthURLParam("code_challenge_method", "S256"))
url = oauth2conf.AuthCodeURL(stateNonce, opts...)
url = oauth2conf.AuthCodeURL(stateNonce, oauth2.AccessTypeOffline)
case oidcutil.GrantTypeImplicit:
url = oidcutil.ImplicitFlowURL(oauth2conf, stateNonce, opts...)
url = oidcutil.ImplicitFlowURL(oauth2conf, stateNonce, oauth2.AccessTypeOffline)
default:
log.Fatalf("Unsupported grant type: %v", grantType)
}
fmt.Printf("Performing %s flow login: %s\n", grantType, url)
time.Sleep(1 * time.Second)
err = open.Start(url)
err = open.Run(url)
errors.CheckError(err)
go func() {
log.Debugf("Listen: %s", srv.Addr)
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("Temporary HTTP server failed: %s", err)
log.Fatalf("listen: %s\n", err)
}
}()
errMsg := <-completionChan
@@ -311,8 +277,8 @@ 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)
sessionRequest := sessionpkg.SessionCreateRequest{
defer util.Close(sessConn)
sessionRequest := session.SessionCreateRequest{
Username: username,
Password: password,
}

View File

@@ -1,31 +0,0 @@
package commands
import (
"testing"
"github.com/dgrijalva/jwt-go"
"github.com/stretchr/testify/assert"
)
//
func Test_userDisplayName_email(t *testing.T) {
claims := jwt.MapClaims{"iss": "qux", "sub": "foo", "email": "firstname.lastname@example.com", "groups": []string{"baz"}}
actualName := userDisplayName(claims)
expectedName := "firstname.lastname@example.com"
assert.Equal(t, expectedName, actualName)
}
func Test_userDisplayName_name(t *testing.T) {
claims := jwt.MapClaims{"iss": "qux", "sub": "foo", "name": "Firstname Lastname", "groups": []string{"baz"}}
actualName := userDisplayName(claims)
expectedName := "Firstname Lastname"
assert.Equal(t, expectedName, actualName)
}
func Test_userDisplayName_sub(t *testing.T) {
claims := jwt.MapClaims{"iss": "qux", "sub": "foo", "groups": []string{"baz"}}
actualName := userDisplayName(claims)
expectedName := "foo"
assert.Equal(t, expectedName, actualName)
}

View File

@@ -1,50 +0,0 @@
package commands
import (
"fmt"
"os"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
"github.com/argoproj/argo-cd/util/localconfig"
)
// NewLogoutCommand returns a new instance of `argocd logout` command
func NewLogoutCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "logout CONTEXT",
Short: "Log out from Argo CD",
Long: "Log out from Argo CD",
Run: func(c *cobra.Command, args []string) {
if len(args) == 0 {
c.HelpFunc()(c, args)
os.Exit(1)
}
context := args[0]
localCfg, err := localconfig.ReadLocalConfig(globalClientOpts.ConfigPath)
errors.CheckError(err)
if localCfg == nil {
log.Fatalf("Nothing to logout from")
}
ok := localCfg.RemoveToken(context)
if !ok {
log.Fatalf("Context %s does not exist", context)
}
err = localconfig.ValidateLocalConfig(*localCfg)
if err != nil {
log.Fatalf("Error in logging out: %s", err)
}
err = localconfig.WriteLocalConfig(*localCfg, globalClientOpts.ConfigPath)
errors.CheckError(err)
fmt.Printf("Logged out from '%s'\n", context)
},
}
return command
}

View File

@@ -1,41 +0,0 @@
package commands
import (
"io/ioutil"
"os"
"testing"
"github.com/argoproj/argo-cd/pkg/apiclient"
"github.com/stretchr/testify/assert"
"github.com/argoproj/argo-cd/util/localconfig"
)
func TestLogout(t *testing.T) {
// Write the test config file
err := ioutil.WriteFile(testConfigFilePath, []byte(testConfig), os.ModePerm)
assert.NoError(t, err)
localConfig, err := localconfig.ReadLocalConfig(testConfigFilePath)
assert.NoError(t, err)
assert.Equal(t, localConfig.CurrentContext, "localhost:8080")
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "localhost:8080", Server: "localhost:8080", User: "localhost:8080"})
command := NewLogoutCommand(&apiclient.ClientOptions{ConfigPath: testConfigFilePath})
command.Run(nil, []string{"localhost:8080"})
localConfig, err = localconfig.ReadLocalConfig(testConfigFilePath)
assert.NoError(t, err)
assert.Equal(t, localConfig.CurrentContext, "localhost:8080")
assert.NotContains(t, localConfig.Users, localconfig.User{AuthToken: "vErrYS3c3tReFRe$hToken", Name: "localhost:8080"})
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "argocd1.example.com:443", Server: "argocd1.example.com:443", User: "argocd1.example.com:443"})
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "argocd2.example.com:443", Server: "argocd2.example.com:443", User: "argocd2.example.com:443"})
assert.Contains(t, localConfig.Contexts, localconfig.ContextRef{Name: "localhost:8080", Server: "localhost:8080", User: "localhost:8080"})
// Write the file again so that no conflicts are made in git
err = ioutil.WriteFile(testConfigFilePath, []byte(testConfig), os.ModePerm)
assert.NoError(t, err)
}

View File

@@ -1,44 +1,35 @@
package commands
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"net/url"
"os"
"strings"
"text/tabwriter"
"time"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
argoio "github.com/argoproj/gitops-engine/pkg/utils/io"
humanize "github.com/dustin/go-humanize"
"github.com/dustin/go-humanize"
"github.com/ghodss/yaml"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
"github.com/argoproj/argo-cd/errors"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
projectpkg "github.com/argoproj/argo-cd/pkg/apiclient/project"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/server/project"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/cli"
"github.com/argoproj/argo-cd/util/config"
"github.com/argoproj/argo-cd/util/git"
"github.com/argoproj/argo-cd/util/gpg"
)
type projectOpts struct {
description string
destinations []string
sources []string
signatureKeys []string
orphanedResourcesEnabled bool
orphanedResourcesWarn bool
description string
destinations []string
sources []string
}
type policyOpts struct {
@@ -63,18 +54,6 @@ func (opts *projectOpts) GetDestinations() []v1alpha1.ApplicationDestination {
return destinations
}
// TODO: Get configured keys and emit warning when a key is specified that is not configured
func (opts *projectOpts) GetSignatureKeys() []v1alpha1.SignatureKey {
signatureKeys := make([]v1alpha1.SignatureKey, 0)
for _, keyStr := range opts.signatureKeys {
if !gpg.IsShortKeyID(keyStr) && !gpg.IsLongKeyID(keyStr) {
log.Fatalf("'%s' is not a valid GnuPG key ID", keyStr)
}
signatureKeys = append(signatureKeys, v1alpha1.SignatureKey{KeyID: gpg.KeyID(keyStr)})
}
return signatureKeys
}
// NewProjectCommand returns a new instance of an `argocd proj` command
func NewProjectCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
@@ -92,8 +71,6 @@ func NewProjectCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
command.AddCommand(NewProjectListCommand(clientOpts))
command.AddCommand(NewProjectSetCommand(clientOpts))
command.AddCommand(NewProjectEditCommand(clientOpts))
command.AddCommand(NewProjectAddSignatureKeyCommand(clientOpts))
command.AddCommand(NewProjectRemoveSignatureKeyCommand(clientOpts))
command.AddCommand(NewProjectAddDestinationCommand(clientOpts))
command.AddCommand(NewProjectRemoveDestinationCommand(clientOpts))
command.AddCommand(NewProjectAddSourceCommand(clientOpts))
@@ -102,9 +79,6 @@ func NewProjectCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
command.AddCommand(NewProjectDenyClusterResourceCommand(clientOpts))
command.AddCommand(NewProjectAllowNamespaceResourceCommand(clientOpts))
command.AddCommand(NewProjectDenyNamespaceResourceCommand(clientOpts))
command.AddCommand(NewProjectWindowsCommand(clientOpts))
command.AddCommand(NewProjectAddOrphanedIgnoreCommand(clientOpts))
command.AddCommand(NewProjectRemoveOrphanedIgnoreCommand(clientOpts))
return command
}
@@ -112,22 +86,7 @@ func addProjFlags(command *cobra.Command, opts *projectOpts) {
command.Flags().StringVarP(&opts.description, "description", "", "", "Project description")
command.Flags().StringArrayVarP(&opts.destinations, "dest", "d", []string{},
"Permitted destination server and namespace (e.g. https://192.168.99.100:8443,default)")
command.Flags().StringArrayVarP(&opts.sources, "src", "s", []string{}, "Permitted source repository URL")
command.Flags().StringSliceVar(&opts.signatureKeys, "signature-keys", []string{}, "GnuPG public key IDs for commit signature verification")
command.Flags().BoolVar(&opts.orphanedResourcesEnabled, "orphaned-resources", false, "Enables orphaned resources monitoring")
command.Flags().BoolVar(&opts.orphanedResourcesWarn, "orphaned-resources-warn", false, "Specifies if applications should be a warning condition when orphaned resources detected")
}
func getOrphanedResourcesSettings(c *cobra.Command, opts projectOpts) *v1alpha1.OrphanedResourcesMonitorSettings {
warnChanged := c.Flag("orphaned-resources-warn").Changed
if opts.orphanedResourcesEnabled || warnChanged {
settings := v1alpha1.OrphanedResourcesMonitorSettings{}
if warnChanged {
settings.Warn = pointer.BoolPtr(opts.orphanedResourcesWarn)
}
return &settings
}
return nil
command.Flags().StringArrayVarP(&opts.sources, "src", "s", []string{}, "Permitted git source repository URL")
}
func addPolicyFlags(command *cobra.Command, opts *policyOpts) {
@@ -144,65 +103,32 @@ func humanizeTimestamp(epoch int64) string {
// NewProjectCreateCommand returns a new instance of an `argocd proj create` command
func NewProjectCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
opts projectOpts
fileURL string
upsert bool
opts projectOpts
)
var command = &cobra.Command{
Use: "create PROJECT",
Short: "Create a project",
Run: func(c *cobra.Command, args []string) {
var proj v1alpha1.AppProject
fmt.Printf("EE: %d/%v\n", len(opts.signatureKeys), opts.signatureKeys)
if fileURL == "-" {
// read stdin
reader := bufio.NewReader(os.Stdin)
err := config.UnmarshalReader(reader, &proj)
if err != nil {
log.Fatalf("unable to read manifest from stdin: %v", err)
}
} else if fileURL != "" {
// read uri
parsedURL, err := url.ParseRequestURI(fileURL)
if err != nil || !(parsedURL.Scheme == "http" || parsedURL.Scheme == "https") {
err = config.UnmarshalLocalFile(fileURL, &proj)
} else {
err = config.UnmarshalRemoteFile(fileURL, &proj)
}
errors.CheckError(err)
if len(args) == 1 && args[0] != proj.Name {
log.Fatalf("project name '%s' does not match project spec metadata.name '%s'", args[0], proj.Name)
}
} else {
// read arguments
if len(args) == 0 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName := args[0]
proj = v1alpha1.AppProject{
ObjectMeta: v1.ObjectMeta{Name: projName},
Spec: v1alpha1.AppProjectSpec{
Description: opts.description,
Destinations: opts.GetDestinations(),
SourceRepos: opts.sources,
SignatureKeys: opts.GetSignatureKeys(),
OrphanedResources: getOrphanedResourcesSettings(c, opts),
},
}
if len(args) == 0 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName := args[0]
proj := v1alpha1.AppProject{
ObjectMeta: v1.ObjectMeta{Name: projName},
Spec: v1alpha1.AppProjectSpec{
Description: opts.description,
Destinations: opts.GetDestinations(),
SourceRepos: opts.sources,
},
}
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer argoio.Close(conn)
_, err := projIf.Create(context.Background(), &projectpkg.ProjectCreateRequest{Project: &proj, Upsert: upsert})
defer util.Close(conn)
_, err := projIf.Create(context.Background(), &project.ProjectCreateRequest{Project: &proj})
errors.CheckError(err)
},
}
command.Flags().BoolVar(&upsert, "upsert", false, "Allows to override a project with the same name even if supplied project spec is different from existing spec")
command.Flags().StringVarP(&fileURL, "file", "f", "", "Filename or URL to Kubernetes manifests for the project")
err := command.Flags().SetAnnotation("file", cobra.BashCompFilenameExt, []string{"json", "yaml", "yml"})
if err != nil {
log.Fatal(err)
}
addProjFlags(command, &opts)
return command
}
@@ -222,9 +148,9 @@ 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})
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
errors.CheckError(err)
visited := 0
@@ -237,10 +163,6 @@ func NewProjectSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
proj.Spec.Destinations = opts.GetDestinations()
case "src":
proj.Spec.SourceRepos = opts.sources
case "signature-keys":
proj.Spec.SignatureKeys = opts.GetSignatureKeys()
case "orphaned-resources", "orphaned-resources-warn":
proj.Spec.OrphanedResources = getOrphanedResourcesSettings(c, opts)
}
})
if visited == 0 {
@@ -249,7 +171,7 @@ func NewProjectSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
os.Exit(1)
}
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
},
}
@@ -257,81 +179,6 @@ func NewProjectSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
return command
}
// NewProjectAddSignatureKeyCommand returns a new instance of an `argocd proj add-signature-key` command
func NewProjectAddSignatureKeyCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "add-signature-key PROJECT KEY-ID",
Short: "Add GnuPG signature key to project",
Run: func(c *cobra.Command, args []string) {
if len(args) != 2 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName := args[0]
signatureKey := args[1]
if !gpg.IsShortKeyID(signatureKey) && !gpg.IsLongKeyID(signatureKey) {
log.Fatalf("%s is not a valid GnuPG key ID", signatureKey)
}
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer argoio.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
for _, key := range proj.Spec.SignatureKeys {
if key.KeyID == signatureKey {
log.Fatal("Specified signature key is already defined in project")
}
}
proj.Spec.SignatureKeys = append(proj.Spec.SignatureKeys, v1alpha1.SignatureKey{KeyID: signatureKey})
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
},
}
return command
}
// NewProjectRemoveSignatureKeyCommand returns a new instance of an `argocd proj remove-signature-key` command
func NewProjectRemoveSignatureKeyCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "remove-signature-key PROJECT KEY-ID",
Short: "Remove GnuPG signature key from project",
Run: func(c *cobra.Command, args []string) {
if len(args) != 2 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName := args[0]
signatureKey := args[1]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer argoio.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
index := -1
for i, key := range proj.Spec.SignatureKeys {
if key.KeyID == signatureKey {
index = i
break
}
}
if index == -1 {
log.Fatal("Specified signature key is not configured for project")
} else {
proj.Spec.SignatureKeys = append(proj.Spec.SignatureKeys[:index], proj.Spec.SignatureKeys[index+1:]...)
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
}
},
}
return command
}
// NewProjectAddDestinationCommand returns a new instance of an `argocd proj add-destination` command
func NewProjectAddDestinationCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
@@ -346,9 +193,9 @@ 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})
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
errors.CheckError(err)
for _, dest := range proj.Spec.Destinations {
@@ -357,7 +204,7 @@ func NewProjectAddDestinationCommand(clientOpts *argocdclient.ClientOptions) *co
}
}
proj.Spec.Destinations = append(proj.Spec.Destinations, v1alpha1.ApplicationDestination{Server: server, Namespace: namespace})
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
},
}
@@ -378,9 +225,9 @@ 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})
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
errors.CheckError(err)
index := -1
@@ -394,7 +241,7 @@ func NewProjectRemoveDestinationCommand(clientOpts *argocdclient.ClientOptions)
log.Fatal("Specified destination does not exist in project")
} else {
proj.Spec.Destinations = append(proj.Spec.Destinations[:index], proj.Spec.Destinations[index+1:]...)
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
}
},
@@ -403,96 +250,6 @@ func NewProjectRemoveDestinationCommand(clientOpts *argocdclient.ClientOptions)
return command
}
// NewProjectAddOrphanedIgnoreCommand returns a new instance of an `argocd proj add-orphaned-ignore` command
func NewProjectAddOrphanedIgnoreCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
name string
)
var command = &cobra.Command{
Use: "add-orphaned-ignore PROJECT GROUP KIND",
Short: "Add a resource to orphaned ignore list",
Run: func(c *cobra.Command, args []string) {
if len(args) != 3 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName := args[0]
group := args[1]
kind := args[2]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer argoio.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
if proj.Spec.OrphanedResources == nil {
settings := v1alpha1.OrphanedResourcesMonitorSettings{}
settings.Ignore = []v1alpha1.OrphanedResourceKey{{Group: group, Kind: kind, Name: name}}
proj.Spec.OrphanedResources = &settings
} else {
for _, ignore := range proj.Spec.OrphanedResources.Ignore {
if ignore.Group == group && ignore.Kind == kind && ignore.Name == name {
log.Fatal("Specified resource is already defined in the orphaned ignore list of project")
return
}
}
proj.Spec.OrphanedResources.Ignore = append(proj.Spec.OrphanedResources.Ignore, v1alpha1.OrphanedResourceKey{Group: group, Kind: kind, Name: name})
}
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
},
}
command.Flags().StringVar(&name, "name", "", "Resource name pattern")
return command
}
// NewProjectRemoveOrphanedIgnoreCommand returns a new instance of an `argocd proj remove-orphaned-ignore` command
func NewProjectRemoveOrphanedIgnoreCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
name string
)
var command = &cobra.Command{
Use: "remove-orphaned-ignore PROJECT GROUP KIND NAME",
Short: "Remove a resource from orphaned ignore list",
Run: func(c *cobra.Command, args []string) {
if len(args) != 3 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName := args[0]
group := args[1]
kind := args[2]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer argoio.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
if proj.Spec.OrphanedResources == nil {
log.Fatal("Specified resource does not exist in the orphaned ignore list of project")
return
}
index := -1
for i, ignore := range proj.Spec.OrphanedResources.Ignore {
if ignore.Group == group && ignore.Kind == kind && ignore.Name == name {
index = i
break
}
}
if index == -1 {
log.Fatal("Specified resource does not exist in the orphaned ignore of project")
} else {
proj.Spec.OrphanedResources.Ignore = append(proj.Spec.OrphanedResources.Ignore[:index], proj.Spec.OrphanedResources.Ignore[index+1:]...)
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
}
},
}
command.Flags().StringVar(&name, "name", "", "Resource name pattern")
return command
}
// NewProjectAddSourceCommand returns a new instance of an `argocd proj add-src` command
func NewProjectAddSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
@@ -506,9 +263,9 @@ 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})
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
errors.CheckError(err)
for _, item := range proj.Spec.SourceRepos {
@@ -522,53 +279,15 @@ func NewProjectAddSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
}
}
proj.Spec.SourceRepos = append(proj.Spec.SourceRepos, url)
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
},
}
return command
}
func modifyResourcesList(list *[]metav1.GroupKind, add bool, listDesc string, group string, kind string) bool {
if add {
for _, item := range *list {
if item.Group == group && item.Kind == kind {
fmt.Printf("Group '%s' and kind '%s' already present in %s resources\n", group, kind, listDesc)
return false
}
}
fmt.Printf("Group '%s' and kind '%s' is added to %s resources\n", group, kind, listDesc)
*list = append(*list, v1.GroupKind{Group: group, Kind: kind})
return true
} else {
index := -1
for i, item := range *list {
if item.Group == group && item.Kind == kind {
index = i
break
}
}
if index == -1 {
fmt.Printf("Group '%s' and kind '%s' not in %s resources\n", group, kind, listDesc)
return false
}
*list = append((*list)[:index], (*list)[index+1:]...)
fmt.Printf("Group '%s' and kind '%s' is removed from %s resources\n", group, kind, listDesc)
return true
}
}
func modifyResourceListCmd(cmdUse, cmdDesc string, clientOpts *argocdclient.ClientOptions, allow bool, namespacedList bool) *cobra.Command {
var (
listType string
defaultList string
)
if namespacedList {
defaultList = "black"
} else {
defaultList = "white"
}
var command = &cobra.Command{
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) {
@@ -578,67 +297,91 @@ func modifyResourceListCmd(cmdUse, cmdDesc string, clientOpts *argocdclient.Clie
}
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})
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
errors.CheckError(err)
var list, white, black *[]metav1.GroupKind
var listAction, listDesc string
var add bool
if namespacedList {
white, black = &proj.Spec.NamespaceResourceWhitelist, &proj.Spec.NamespaceResourceBlacklist
listDesc = "namespaced"
} else {
white, black = &proj.Spec.ClusterResourceWhitelist, &proj.Spec.ClusterResourceBlacklist
listDesc = "cluster"
}
if listType == "white" {
list = white
listAction = "whitelisted"
add = allow
} else {
list = black
listAction = "blacklisted"
add = !allow
}
if modifyResourcesList(list, add, listAction+" "+listDesc, group, kind) {
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
if action(proj, group, kind) {
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
}
},
}
command.Flags().StringVarP(&listType, "list", "l", defaultList, "Use blacklist or whitelist. This can only be 'white' or 'black'")
return command
}
// NewProjectAllowNamespaceResourceCommand returns a new instance of an `deny-cluster-resources` command
func NewProjectAllowNamespaceResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
use := "allow-namespace-resource PROJECT GROUP KIND"
desc := "Removes a namespaced API resource from the blacklist or add a namespaced API resource to the whitelist"
return modifyResourceListCmd(use, desc, clientOpts, true, 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 {
index = i
break
}
}
if index == -1 {
fmt.Printf("Group '%s' and kind '%s' not in blacklisted namespaced resources\n", group, kind)
return false
}
proj.Spec.NamespaceResourceBlacklist = append(proj.Spec.NamespaceResourceBlacklist[:index], proj.Spec.NamespaceResourceBlacklist[index+1:]...)
return true
})
}
// 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 modifyResourceListCmd(use, desc, clientOpts, false, 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)
return false
}
}
proj.Spec.NamespaceResourceBlacklist = append(proj.Spec.NamespaceResourceBlacklist, v1.GroupKind{Group: group, Kind: kind})
return true
})
}
// NewProjectDenyClusterResourceCommand returns a new instance of an `deny-cluster-resource` command
func NewProjectDenyClusterResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
use := "deny-cluster-resource PROJECT GROUP KIND"
desc := "Removes a cluster-scoped API resource from the whitelist and adds it to blacklist"
return modifyResourceListCmd(use, desc, clientOpts, false, false)
desc := "Removes a cluster-scoped API resource from the whitelist"
return modifyProjectResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
index := -1
for i, item := range proj.Spec.ClusterResourceWhitelist {
if item.Group == group && item.Kind == kind {
index = i
break
}
}
if index == -1 {
fmt.Printf("Group '%s' and kind '%s' not in whitelisted cluster resources\n", group, kind)
return false
}
proj.Spec.ClusterResourceWhitelist = append(proj.Spec.ClusterResourceWhitelist[:index], proj.Spec.ClusterResourceWhitelist[index+1:]...)
return true
})
}
// NewProjectAllowClusterResourceCommand returns a new instance of an `argocd proj allow-cluster-resource` command
func NewProjectAllowClusterResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
use := "allow-cluster-resource PROJECT GROUP KIND"
desc := "Adds a cluster-scoped API resource to the whitelist and removes it from blacklist"
return modifyResourceListCmd(use, desc, clientOpts, true, false)
desc := "Adds a cluster-scoped API resource to the whitelist"
return modifyProjectResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
for _, item := range proj.Spec.ClusterResourceWhitelist {
if item.Group == group && item.Kind == kind {
fmt.Printf("Group '%s' and kind '%s' already present in whitelisted cluster resources\n", group, kind)
return false
}
}
proj.Spec.ClusterResourceWhitelist = append(proj.Spec.ClusterResourceWhitelist, v1.GroupKind{Group: group, Kind: kind})
return true
})
}
// NewProjectRemoveSourceCommand returns a new instance of an `argocd proj remove-src` command
@@ -654,9 +397,9 @@ 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})
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
errors.CheckError(err)
index := -1
@@ -670,7 +413,7 @@ func NewProjectRemoveSourceCommand(clientOpts *argocdclient.ClientOptions) *cobr
fmt.Printf("Source repository '%s' does not exist in project\n", url)
} else {
proj.Spec.SourceRepos = append(proj.Spec.SourceRepos[:index], proj.Spec.SourceRepos[index+1:]...)
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
}
},
@@ -690,9 +433,9 @@ 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})
_, err := projIf.Delete(context.Background(), &project.ProjectQuery{Name: name})
errors.CheckError(err)
}
},
@@ -700,66 +443,29 @@ func NewProjectDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
return command
}
// Print list of project names
func printProjectNames(projects []v1alpha1.AppProject) {
for _, p := range projects {
fmt.Println(p.Name)
}
}
// Print table of project info
func printProjectTable(projects []v1alpha1.AppProject) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "NAME\tDESCRIPTION\tDESTINATIONS\tSOURCES\tCLUSTER-RESOURCE-WHITELIST\tNAMESPACE-RESOURCE-BLACKLIST\tSIGNATURE-KEYS\tORPHANED-RESOURCES\n")
for _, p := range projects {
printProjectLine(w, &p)
}
_ = w.Flush()
}
// NewProjectListCommand returns a new instance of an `argocd proj list` command
func NewProjectListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
output string
)
var command = &cobra.Command{
Use: "list",
Short: "List projects",
Run: func(c *cobra.Command, args []string) {
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer argoio.Close(conn)
projects, err := projIf.List(context.Background(), &projectpkg.ProjectQuery{})
defer util.Close(conn)
projects, err := projIf.List(context.Background(), &project.ProjectQuery{})
errors.CheckError(err)
switch output {
case "yaml", "json":
err := PrintResourceList(projects.Items, output, false)
errors.CheckError(err)
case "name":
printProjectNames(projects.Items)
case "wide", "":
printProjectTable(projects.Items)
default:
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "NAME\tDESCRIPTION\tDESTINATIONS\tSOURCES\tCLUSTER-RESOURCE-WHITELIST\tNAMESPACE-RESOURCE-BLACKLIST\n")
for _, p := range projects.Items {
printProjectLine(w, &p)
}
_ = w.Flush()
},
}
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|name")
return command
}
func formatOrphanedResources(p *v1alpha1.AppProject) string {
if p.Spec.OrphanedResources == nil {
return "disabled"
}
details := fmt.Sprintf("warn=%v", p.Spec.OrphanedResources.IsWarn())
if len(p.Spec.OrphanedResources.Ignore) > 0 {
details = fmt.Sprintf("%s, ignored %d", details, len(p.Spec.OrphanedResources.Ignore))
}
return fmt.Sprintf("enabled (%s)", details)
}
func printProjectLine(w io.Writer, p *v1alpha1.AppProject) {
var destinations, sourceRepos, clusterWhitelist, namespaceBlacklist, signatureKeys string
var destinations, sourceRepos, clusterWhitelist, namespaceBlacklist string
switch len(p.Spec.Destinations) {
case 0:
destinations = "<none>"
@@ -790,81 +496,12 @@ func printProjectLine(w io.Writer, p *v1alpha1.AppProject) {
default:
namespaceBlacklist = fmt.Sprintf("%d resources", len(p.Spec.NamespaceResourceBlacklist))
}
switch len(p.Spec.SignatureKeys) {
case 0:
signatureKeys = "<none>"
default:
signatureKeys = fmt.Sprintf("%d key(s)", len(p.Spec.SignatureKeys))
}
fmt.Fprintf(w, "%s\t%s\t%v\t%v\t%v\t%v\t%v\t%v\n", p.Name, p.Spec.Description, destinations, sourceRepos, clusterWhitelist, namespaceBlacklist, signatureKeys, formatOrphanedResources(p))
}
func printProject(p *v1alpha1.AppProject) {
const printProjFmtStr = "%-34s%s\n"
fmt.Printf(printProjFmtStr, "Name:", p.Name)
fmt.Printf(printProjFmtStr, "Description:", p.Spec.Description)
// Print destinations
dest0 := "<none>"
if len(p.Spec.Destinations) > 0 {
dest0 = fmt.Sprintf("%s,%s", p.Spec.Destinations[0].Server, p.Spec.Destinations[0].Namespace)
}
fmt.Printf(printProjFmtStr, "Destinations:", dest0)
for i := 1; i < len(p.Spec.Destinations); i++ {
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s,%s", p.Spec.Destinations[i].Server, p.Spec.Destinations[i].Namespace))
}
// Print sources
src0 := "<none>"
if len(p.Spec.SourceRepos) > 0 {
src0 = p.Spec.SourceRepos[0]
}
fmt.Printf(printProjFmtStr, "Repositories:", src0)
for i := 1; i < len(p.Spec.SourceRepos); i++ {
fmt.Printf(printProjFmtStr, "", p.Spec.SourceRepos[i])
}
// Print whitelisted cluster resources
cwl0 := "<none>"
if len(p.Spec.ClusterResourceWhitelist) > 0 {
cwl0 = fmt.Sprintf("%s/%s", p.Spec.ClusterResourceWhitelist[0].Group, p.Spec.ClusterResourceWhitelist[0].Kind)
}
fmt.Printf(printProjFmtStr, "Whitelisted Cluster Resources:", cwl0)
for i := 1; i < len(p.Spec.ClusterResourceWhitelist); i++ {
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s/%s", p.Spec.ClusterResourceWhitelist[i].Group, p.Spec.ClusterResourceWhitelist[i].Kind))
}
// Print blacklisted namespaced resources
rbl0 := "<none>"
if len(p.Spec.NamespaceResourceBlacklist) > 0 {
rbl0 = fmt.Sprintf("%s/%s", p.Spec.NamespaceResourceBlacklist[0].Group, p.Spec.NamespaceResourceBlacklist[0].Kind)
}
fmt.Printf(printProjFmtStr, "Blacklisted Namespaced Resources:", rbl0)
for i := 1; i < len(p.Spec.NamespaceResourceBlacklist); i++ {
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s/%s", p.Spec.NamespaceResourceBlacklist[i].Group, p.Spec.NamespaceResourceBlacklist[i].Kind))
}
// Print required signature keys
signatureKeysStr := "<none>"
if len(p.Spec.SignatureKeys) > 0 {
kids := make([]string, 0)
for _, key := range p.Spec.SignatureKeys {
kids = append(kids, key.KeyID)
}
signatureKeysStr = strings.Join(kids, ", ")
}
fmt.Printf(printProjFmtStr, "Signature keys:", signatureKeysStr)
fmt.Printf(printProjFmtStr, "Orphaned Resources:", formatOrphanedResources(p))
fmt.Fprintf(w, "%s\t%s\t%v\t%v\t%v\t%v\n", p.Name, p.Spec.Description, destinations, sourceRepos, clusterWhitelist, namespaceBlacklist)
}
// NewProjectGetCommand returns a new instance of an `argocd proj get` command
func NewProjectGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
output string
)
const printProjFmtStr = "%-34s%s\n"
var command = &cobra.Command{
Use: "get PROJECT",
Short: "Get project details",
@@ -875,22 +512,53 @@ func NewProjectGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
}
projName := args[0]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer argoio.Close(conn)
p, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
defer util.Close(conn)
p, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
errors.CheckError(err)
fmt.Printf(printProjFmtStr, "Name:", p.Name)
fmt.Printf(printProjFmtStr, "Description:", p.Spec.Description)
switch output {
case "yaml", "json":
err := PrintResource(p, output)
errors.CheckError(err)
case "wide", "":
printProject(p)
default:
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
// Print destinations
dest0 := "<none>"
if len(p.Spec.Destinations) > 0 {
dest0 = fmt.Sprintf("%s,%s", p.Spec.Destinations[0].Server, p.Spec.Destinations[0].Namespace)
}
fmt.Printf(printProjFmtStr, "Destinations:", dest0)
for i := 1; i < len(p.Spec.Destinations); i++ {
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s,%s", p.Spec.Destinations[i].Server, p.Spec.Destinations[i].Namespace))
}
// Print sources
src0 := "<none>"
if len(p.Spec.SourceRepos) > 0 {
src0 = p.Spec.SourceRepos[0]
}
fmt.Printf(printProjFmtStr, "Repositories:", src0)
for i := 1; i < len(p.Spec.SourceRepos); i++ {
fmt.Printf(printProjFmtStr, "", p.Spec.SourceRepos[i])
}
// Print whitelisted cluster resources
cwl0 := "<none>"
if len(p.Spec.ClusterResourceWhitelist) > 0 {
cwl0 = fmt.Sprintf("%s/%s", p.Spec.ClusterResourceWhitelist[0].Group, p.Spec.ClusterResourceWhitelist[0].Kind)
}
fmt.Printf(printProjFmtStr, "Whitelisted Cluster Resources:", cwl0)
for i := 1; i < len(p.Spec.ClusterResourceWhitelist); i++ {
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s/%s", p.Spec.ClusterResourceWhitelist[i].Group, p.Spec.ClusterResourceWhitelist[i].Kind))
}
// Print blacklisted namespaced resources
rbl0 := "<none>"
if len(p.Spec.NamespaceResourceBlacklist) > 0 {
rbl0 = fmt.Sprintf("%s/%s", p.Spec.NamespaceResourceBlacklist[0].Group, p.Spec.NamespaceResourceBlacklist[0].Kind)
}
fmt.Printf(printProjFmtStr, "Blacklisted Namespaced Resources:", rbl0)
for i := 1; i < len(p.Spec.NamespaceResourceBlacklist); i++ {
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s/%s", p.Spec.NamespaceResourceBlacklist[i].Group, p.Spec.NamespaceResourceBlacklist[i].Kind))
}
},
}
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide")
return command
}
@@ -905,8 +573,8 @@ func NewProjectEditCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman
}
projName := args[0]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer argoio.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
defer util.Close(conn)
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
errors.CheckError(err)
projData, err := json.Marshal(proj.Spec)
errors.CheckError(err)
@@ -923,12 +591,12 @@ func NewProjectEditCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman
if err != nil {
return err
}
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
if err != nil {
return err
}
proj.Spec = updatedSpec
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
if err != nil {
return fmt.Errorf("Failed to update project:\n%v", err)
}

View File

@@ -7,14 +7,15 @@ 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/server/project"
"github.com/argoproj/argo-cd/util"
projectutil "github.com/argoproj/argo-cd/util/project"
)
const (
@@ -39,8 +40,6 @@ func NewProjectRoleCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman
roleCommand.AddCommand(NewProjectRoleDeleteTokenCommand(clientOpts))
roleCommand.AddCommand(NewProjectRoleAddPolicyCommand(clientOpts))
roleCommand.AddCommand(NewProjectRoleRemovePolicyCommand(clientOpts))
roleCommand.AddCommand(NewProjectRoleAddGroupCommand(clientOpts))
roleCommand.AddCommand(NewProjectRoleRemoveGroupCommand(clientOpts))
return roleCommand
}
@@ -60,18 +59,18 @@ 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})
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
errors.CheckError(err)
role, roleIndex, err := proj.GetRoleByName(roleName)
role, roleIndex, err := projectutil.GetRoleByName(proj, roleName)
errors.CheckError(err)
policy := fmt.Sprintf(policyTemplate, proj.Name, role.Name, opts.action, proj.Name, opts.object, opts.permission)
proj.Spec.Roles[roleIndex].Policies = append(role.Policies, policy)
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
},
}
@@ -95,12 +94,12 @@ 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})
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
errors.CheckError(err)
role, roleIndex, err := proj.GetRoleByName(roleName)
role, roleIndex, err := projectutil.GetRoleByName(proj, roleName)
errors.CheckError(err)
policyToRemove := fmt.Sprintf(policyTemplate, proj.Name, role.Name, opts.action, proj.Name, opts.object, opts.permission)
@@ -116,7 +115,7 @@ func NewProjectRoleRemovePolicyCommand(clientOpts *argocdclient.ClientOptions) *
}
role.Policies[duplicateIndex] = role.Policies[len(role.Policies)-1]
proj.Spec.Roles[roleIndex].Policies = role.Policies[:len(role.Policies)-1]
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
},
}
@@ -140,19 +139,19 @@ 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})
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
errors.CheckError(err)
_, _, err = proj.GetRoleByName(roleName)
_, _, err = projectutil.GetRoleByName(proj, roleName)
if err == nil {
fmt.Printf("Role '%s' already exists\n", roleName)
return
}
proj.Spec.Roles = append(proj.Spec.Roles, v1alpha1.ProjectRole{Name: roleName, Description: description})
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
fmt.Printf("Role '%s' created\n", roleName)
},
@@ -174,12 +173,12 @@ 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})
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
errors.CheckError(err)
_, index, err := proj.GetRoleByName(roleName)
_, index, err := projectutil.GetRoleByName(proj, roleName)
if err != nil {
fmt.Printf("Role '%s' does not exist in project\n", roleName)
return
@@ -187,7 +186,7 @@ func NewProjectRoleDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.
proj.Spec.Roles[index] = proj.Spec.Roles[len(proj.Spec.Roles)-1]
proj.Spec.Roles = proj.Spec.Roles[:len(proj.Spec.Roles)-1]
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
fmt.Printf("Role '%s' deleted\n", roleName)
},
@@ -211,10 +210,10 @@ 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())})
token, err := projIf.CreateToken(context.Background(), &project.ProjectTokenCreateRequest{Project: projName, Role: roleName, ExpiresIn: int64(duration.Seconds())})
errors.CheckError(err)
fmt.Println(token.Token)
},
@@ -240,37 +239,17 @@ 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})
_, err = projIf.DeleteToken(context.Background(), &project.ProjectTokenDeleteRequest{Project: projName, Role: roleName, Iat: issuedAt})
errors.CheckError(err)
},
}
return command
}
// Print list of project role names
func printProjectRoleListName(roles []v1alpha1.ProjectRole) {
for _, role := range roles {
fmt.Println(role.Name)
}
}
// Print table of project roles
func printProjectRoleListTable(roles []v1alpha1.ProjectRole) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "ROLE-NAME\tDESCRIPTION\n")
for _, role := range roles {
fmt.Fprintf(w, "%s\t%s\n", role.Name, role.Description)
}
_ = w.Flush()
}
// NewProjectRoleListCommand returns a new instance of an `argocd proj roles list` command
func NewProjectRoleListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
output string
)
var command = &cobra.Command{
Use: "list PROJECT",
Short: "List all the roles in a project",
@@ -281,24 +260,18 @@ 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})
project, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
errors.CheckError(err)
switch output {
case "json", "yaml":
err := PrintResourceList(project.Spec.Roles, output, false)
errors.CheckError(err)
case "name":
printProjectRoleListName(project.Spec.Roles)
case "wide", "":
printProjectRoleListTable(project.Spec.Roles)
default:
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "ROLE-NAME\tDESCRIPTION\n")
for _, role := range project.Spec.Roles {
fmt.Fprintf(w, "%s\t%s\n", role.Name, role.Description)
}
_ = w.Flush()
},
}
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|name")
return command
}
@@ -315,12 +288,12 @@ 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})
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
errors.CheckError(err)
role, _, err := proj.GetRoleByName(roleName)
role, _, err := projectutil.GetRoleByName(proj, roleName)
errors.CheckError(err)
printRoleFmtStr := "%-15s%s\n"
@@ -332,7 +305,7 @@ func NewProjectRoleGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
// TODO(jessesuen): print groups
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "ID\tISSUED-AT\tEXPIRES-AT\n")
for _, token := range proj.Status.JWTTokensByRole[roleName].Items {
for _, token := range role.JWTTokens {
expiresAt := "<none>"
if token.ExpiresAt > 0 {
expiresAt = humanizeTimestamp(token.ExpiresAt)
@@ -349,24 +322,24 @@ func NewProjectRoleGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
func NewProjectRoleAddGroupCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "add-group PROJECT ROLE-NAME GROUP-CLAIM",
Short: "Add a group claim to a project role",
Short: "Add a policy to a project role",
Run: func(c *cobra.Command, args []string) {
if len(args) != 3 {
if len(args) != 2 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName, roleName, groupName := args[0], args[1], args[2]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer io.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
defer util.Close(conn)
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
errors.CheckError(err)
updated, err := proj.AddGroupToRole(roleName, groupName)
updated, err := projectutil.AddGroupToRole(proj, roleName, groupName)
errors.CheckError(err)
if !updated {
if updated {
fmt.Printf("Group '%s' already present in role '%s'\n", groupName, roleName)
return
}
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
fmt.Printf("Group '%s' added to role '%s'\n", groupName, roleName)
},
@@ -386,16 +359,16 @@ 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)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
defer util.Close(conn)
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
errors.CheckError(err)
updated, err := proj.RemoveGroupFromRole(roleName, groupName)
updated, err := projectutil.RemoveGroupFromRole(proj, roleName, groupName)
errors.CheckError(err)
if !updated {
fmt.Printf("Group '%s' not present in role '%s'\n", groupName, roleName)
return
}
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
fmt.Printf("Group '%s' removed from role '%s'\n", groupName, roleName)
},

View File

@@ -1,320 +0,0 @@
package commands
import (
"context"
"fmt"
"os"
"strconv"
"strings"
"text/tabwriter"
"github.com/argoproj/gitops-engine/pkg/utils/errors"
"github.com/argoproj/gitops-engine/pkg/utils/io"
"github.com/spf13/cobra"
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"
)
// NewProjectWindowsCommand returns a new instance of the `argocd proj windows` command
func NewProjectWindowsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
roleCommand := &cobra.Command{
Use: "windows",
Short: "Manage a project's sync windows",
Run: func(c *cobra.Command, args []string) {
c.HelpFunc()(c, args)
os.Exit(1)
},
}
roleCommand.AddCommand(NewProjectWindowsDisableManualSyncCommand(clientOpts))
roleCommand.AddCommand(NewProjectWindowsEnableManualSyncCommand(clientOpts))
roleCommand.AddCommand(NewProjectWindowsAddWindowCommand(clientOpts))
roleCommand.AddCommand(NewProjectWindowsDeleteCommand(clientOpts))
roleCommand.AddCommand(NewProjectWindowsListCommand(clientOpts))
roleCommand.AddCommand(NewProjectWindowsUpdateCommand(clientOpts))
return roleCommand
}
// NewProjectSyncWindowsDisableManualSyncCommand returns a new instance of an `argocd proj windows disable-manual-sync` command
func NewProjectWindowsDisableManualSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "disable-manual-sync PROJECT ID",
Short: "Disable manual sync for a sync window",
Long: "Disable manual sync for a sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
Run: func(c *cobra.Command, args []string) {
if len(args) != 2 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName := args[0]
id, err := strconv.Atoi(args[1])
errors.CheckError(err)
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer io.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
for i, window := range proj.Spec.SyncWindows {
if id == i {
window.ManualSync = false
}
}
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
},
}
return command
}
// NewProjectWindowsEnableManualSyncCommand returns a new instance of an `argocd proj windows enable-manual-sync` command
func NewProjectWindowsEnableManualSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "enable-manual-sync PROJECT ID",
Short: "Enable manual sync for a sync window",
Long: "Enable manual sync for a sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
Run: func(c *cobra.Command, args []string) {
if len(args) != 2 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName := args[0]
id, err := strconv.Atoi(args[1])
errors.CheckError(err)
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer io.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
for i, window := range proj.Spec.SyncWindows {
if id == i {
window.ManualSync = true
}
}
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
},
}
return command
}
// NewProjectWindowsAddWindowCommand returns a new instance of an `argocd proj windows add` command
func NewProjectWindowsAddWindowCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
kind string
schedule string
duration string
applications []string
namespaces []string
clusters []string
manualSync bool
)
var command = &cobra.Command{
Use: "add PROJECT",
Short: "Add a sync window to a project",
Run: func(c *cobra.Command, args []string) {
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName := args[0]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer io.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
err = proj.Spec.AddWindow(kind, schedule, duration, applications, namespaces, clusters, manualSync)
errors.CheckError(err)
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
},
}
command.Flags().StringVarP(&kind, "kind", "k", "", "Sync window kind, either allow or deny")
command.Flags().StringVar(&schedule, "schedule", "", "Sync window schedule in cron format. (e.g. --schedule \"0 22 * * *\")")
command.Flags().StringVar(&duration, "duration", "", "Sync window duration. (e.g. --duration 1h)")
command.Flags().StringSliceVar(&applications, "applications", []string{}, "Applications that the schedule will be applied to. Comma separated, wildcards supported (e.g. --applications prod-\\*,website)")
command.Flags().StringSliceVar(&namespaces, "namespaces", []string{}, "Namespaces that the schedule will be applied to. Comma separated, wildcards supported (e.g. --namespaces default,\\*-prod)")
command.Flags().StringSliceVar(&clusters, "clusters", []string{}, "Clusters that the schedule will be applied to. Comma separated, wildcards supported (e.g. --clusters prod,staging)")
command.Flags().BoolVar(&manualSync, "manual-sync", false, "Allow manual syncs for both deny and allow windows")
return command
}
// NewProjectWindowsAddWindowCommand returns a new instance of an `argocd proj windows delete` command
func NewProjectWindowsDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "delete PROJECT ID",
Short: "Delete a sync window from a project. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
Run: func(c *cobra.Command, args []string) {
if len(args) != 2 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName := args[0]
id, err := strconv.Atoi(args[1])
errors.CheckError(err)
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer io.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
err = proj.Spec.DeleteWindow(id)
errors.CheckError(err)
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
},
}
return command
}
// NewProjectWindowsUpdateCommand returns a new instance of an `argocd proj windows update` command
func NewProjectWindowsUpdateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
schedule string
duration string
applications []string
namespaces []string
clusters []string
)
var command = &cobra.Command{
Use: "update PROJECT ID",
Short: "Update a project sync window",
Long: "Update a project sync window. Requires ID which can be found by running \"argocd proj windows list PROJECT\"",
Run: func(c *cobra.Command, args []string) {
if len(args) != 2 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName := args[0]
id, err := strconv.Atoi(args[1])
errors.CheckError(err)
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer io.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
for i, window := range proj.Spec.SyncWindows {
if id == i {
err := window.Update(schedule, duration, applications, namespaces, clusters)
if err != nil {
errors.CheckError(err)
}
}
}
_, err = projIf.Update(context.Background(), &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
},
}
command.Flags().StringVar(&schedule, "schedule", "", "Sync window schedule in cron format. (e.g. --schedule \"0 22 * * *\")")
command.Flags().StringVar(&duration, "duration", "", "Sync window duration. (e.g. --duration 1h)")
command.Flags().StringSliceVar(&applications, "applications", []string{}, "Applications that the schedule will be applied to. Comma separated, wildcards supported (e.g. --applications prod-\\*,website)")
command.Flags().StringSliceVar(&namespaces, "namespaces", []string{}, "Namespaces that the schedule will be applied to. Comma separated, wildcards supported (e.g. --namespaces default,\\*-prod)")
command.Flags().StringSliceVar(&clusters, "clusters", []string{}, "Clusters that the schedule will be applied to. Comma separated, wildcards supported (e.g. --clusters prod,staging)")
return command
}
// NewProjectWindowsListCommand returns a new instance of an `argocd proj windows list` command
func NewProjectWindowsListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
output string
)
var command = &cobra.Command{
Use: "list PROJECT",
Short: "List project sync windows",
Run: func(c *cobra.Command, args []string) {
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
projName := args[0]
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
defer io.Close(conn)
proj, err := projIf.Get(context.Background(), &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
switch output {
case "yaml", "json":
err := PrintResourceList(proj.Spec.SyncWindows, output, false)
errors.CheckError(err)
case "wide", "":
printSyncWindows(proj)
default:
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
}
},
}
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide")
return command
}
// Print table of sync window data
func printSyncWindows(proj *v1alpha1.AppProject) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
var fmtStr string
headers := []interface{}{"ID", "STATUS", "KIND", "SCHEDULE", "DURATION", "APPLICATIONS", "NAMESPACES", "CLUSTERS", "MANUALSYNC"}
fmtStr = "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n"
fmt.Fprintf(w, fmtStr, headers...)
if proj.Spec.SyncWindows.HasWindows() {
for i, window := range proj.Spec.SyncWindows {
vals := []interface{}{
strconv.Itoa(i),
formatBoolOutput(window.Active()),
window.Kind,
window.Schedule,
window.Duration,
formatListOutput(window.Applications),
formatListOutput(window.Namespaces),
formatListOutput(window.Clusters),
formatManualOutput(window.ManualSync),
}
fmt.Fprintf(w, fmtStr, vals...)
}
}
_ = w.Flush()
}
func formatListOutput(list []string) string {
var o string
if len(list) == 0 {
o = "-"
} else {
o = strings.Join(list, ",")
}
return o
}
func formatBoolOutput(active bool) string {
var o string
if active {
o = "Active"
} else {
o = "Inactive"
}
return o
}
func formatManualOutput(active bool) string {
var o string
if active {
o = "Enabled"
} else {
o = "Disabled"
}
return o
}

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"
oidc "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/server/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,16 +58,16 @@ 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)
ctx = oidc.ClientContext(ctx, httpClient)
acdSet, err := setIf.Get(ctx, &settingspkg.SettingsQuery{})
acdSet, err := setIf.Get(ctx, &settings.SettingsQuery{})
errors.CheckError(err)
oauth2conf, provider, err := acdClient.OIDCConfig(ctx, acdSet)
errors.CheckError(err)
tokenString, refreshToken = oauth2Login(ctx, ssoPort, acdSet.GetOIDCConfig(), oauth2conf, provider)
tokenString, refreshToken = oauth2Login(ctx, ssoPort, oauth2conf, provider)
}
localCfg.UpsertUser(localconfig.User{

View File

@@ -7,15 +7,14 @@ 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/server/repository"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/cli"
"github.com/argoproj/argo-cd/util/git"
)
@@ -24,7 +23,7 @@ import (
func NewRepoCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "repo",
Short: "Manage repository connection parameters",
Short: "Manage git repository credentials",
Run: func(c *cobra.Command, args []string) {
c.HelpFunc()(c, args)
os.Exit(1)
@@ -32,7 +31,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
@@ -41,146 +39,58 @@ func NewRepoCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
// NewRepoAddCommand returns a new instance of an `argocd repo add` command
func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
repo appsv1.Repository
upsert bool
sshPrivateKeyPath string
insecureIgnoreHostKey bool
insecureSkipServerVerification bool
tlsClientCertPath string
tlsClientCertKeyPath string
enableLfs bool
repo appsv1.Repository
upsert bool
sshPrivateKeyPath string
insecureIgnoreHostKey bool
)
// 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
# Add a private Git repository via HTTPS using username/password and TLS client certificates:
argocd repo add https://git.example.com/repos/repo --username git --password secret --tls-client-cert-path ~/mycert.crt --tls-client-cert-key-path ~/mycert.key
# Add a private Git repository via HTTPS using username/password without verifying the server's TLS certificate
argocd repo add https://git.example.com/repos/repo --username git --password secret --insecure-skip-server-verification
# Add a public Helm repository named 'stable' via HTTPS
argocd repo add https://kubernetes-charts.storage.googleapis.com --type helm --name stable
# Add a private Helm repository named 'stable' via HTTPS
argocd repo add https://kubernetes-charts.storage.googleapis.com --type helm --name stable --username test --password test
`
var command = &cobra.Command{
Use: "add REPOURL",
Short: "Add git repository connection parameters",
Example: repoAddExamples,
Use: "add REPO",
Short: "Add git repository credentials",
Run: func(c *cobra.Command, args []string) {
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
// Repository URL
repo.Repo = args[0]
// Specifying ssh-private-key-path is only valid for SSH repositories
if sshPrivateKeyPath != "" {
if ok, _ := git.IsSSHURL(repo.Repo); ok {
keyData, err := ioutil.ReadFile(sshPrivateKeyPath)
if err != nil {
log.Fatal(err)
}
repo.SSHPrivateKey = string(keyData)
} else {
err := fmt.Errorf("--ssh-private-key-path is only supported for SSH repositories.")
errors.CheckError(err)
keyData, err := ioutil.ReadFile(sshPrivateKeyPath)
if err != nil {
log.Fatal(err)
}
repo.SSHPrivateKey = string(keyData)
}
// tls-client-cert-path and tls-client-cert-key-key-path must always be
// specified together
if (tlsClientCertPath != "" && tlsClientCertKeyPath == "") || (tlsClientCertPath == "" && tlsClientCertKeyPath != "") {
err := fmt.Errorf("--tls-client-cert-path and --tls-client-cert-key-path must be specified together")
errors.CheckError(err)
}
// Specifying tls-client-cert-path is only valid for HTTPS repositories
if tlsClientCertPath != "" {
if git.IsHTTPSURL(repo.Repo) {
tlsCertData, err := ioutil.ReadFile(tlsClientCertPath)
errors.CheckError(err)
tlsCertKey, err := ioutil.ReadFile(tlsClientCertKeyPath)
errors.CheckError(err)
repo.TLSClientCertData = string(tlsCertData)
repo.TLSClientCertKey = string(tlsCertKey)
} else {
err := fmt.Errorf("--tls-client-cert-path is only supported for HTTPS repositories")
errors.CheckError(err)
}
}
// Set repository connection properties only when creating repository, not
// when creating repository credentials.
// InsecureIgnoreHostKey is deprecated and only here for backwards compat
repo.InsecureIgnoreHostKey = insecureIgnoreHostKey
repo.Insecure = insecureSkipServerVerification
repo.EnableLFS = enableLfs
if repo.Type == "helm" && repo.Name == "" {
errors.CheckError(fmt.Errorf("Must specify --name for repos of type 'helm'"))
// First test the repo *without* username/password. This gives us a hint on whether this
// is a private repo.
// NOTE: it is important not to run git commands to test git credentials on the user's
// system since it may mess with their git credential store (e.g. osx keychain).
// See issue #315
err := git.TestRepo(repo.Repo, "", "", repo.SSHPrivateKey, repo.InsecureIgnoreHostKey)
if err != nil {
if yes, _ := git.IsSSHURL(repo.Repo); yes {
// If we failed using git SSH credentials, then the repo is automatically bad
log.Fatal(err)
}
// If we can't test the repo, it's probably private. Prompt for credentials and
// let the server test it.
repo.Username, repo.Password = cli.PromptCredentials(repo.Username, repo.Password)
}
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoClientOrDie()
defer io.Close(conn)
// If the user set a username, but didn't supply password via --password,
// then we prompt for it
if repo.Username != "" && repo.Password == "" {
repo.Password = cli.PromptPassword(repo.Password)
}
// We let the server check access to the repository before adding it. If
// it is a private repo, but we cannot access with with the credentials
// that were supplied, we bail out.
//
// Skip validation if we are just adding credentials template, chances
// are high that we do not have the given URL pointing to a valid Git
// repo anyway.
repoAccessReq := repositorypkg.RepoAccessQuery{
Repo: repo.Repo,
Type: repo.Type,
Name: repo.Name,
Username: repo.Username,
Password: repo.Password,
SshPrivateKey: repo.SSHPrivateKey,
TlsClientCertData: repo.TLSClientCertData,
TlsClientCertKey: repo.TLSClientCertKey,
Insecure: repo.IsInsecure(),
}
_, err := repoIf.ValidateAccess(context.Background(), &repoAccessReq)
errors.CheckError(err)
repoCreateReq := repositorypkg.RepoCreateRequest{
defer util.Close(conn)
repoCreateReq := repository.RepoCreateRequest{
Repo: &repo,
Upsert: upsert,
}
createdRepo, err := repoIf.Create(context.Background(), &repoCreateReq)
errors.CheckError(err)
fmt.Printf("repository '%s' added\n", createdRepo.Repo)
},
}
command.Flags().StringVar(&repo.Type, "type", common.DefaultRepoType, "type of the repository, \"git\" or \"helm\"")
command.Flags().StringVar(&repo.Name, "name", "", "name of the repository, mandatory for repositories of type helm")
command.Flags().StringVar(&repo.Username, "username", "", "username to the repository")
command.Flags().StringVar(&repo.Password, "password", "", "password to the repository")
command.Flags().StringVar(&sshPrivateKeyPath, "ssh-private-key-path", "", "path to the private ssh key (e.g. ~/.ssh/id_rsa)")
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(&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(&insecureIgnoreHostKey, "insecure-ignore-host-key", false, "disables SSH strict host key checking")
command.Flags().BoolVar(&upsert, "upsert", false, "Override an existing repository with the same name even if the spec differs")
return command
}
@@ -189,16 +99,16 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
func NewRepoRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "rm REPO",
Short: "Remove repository credentials",
Short: "Remove git repository credentials",
Run: func(c *cobra.Command, args []string) {
if len(args) == 0 {
c.HelpFunc()(c, args)
os.Exit(1)
}
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoClientOrDie()
defer io.Close(conn)
defer util.Close(conn)
for _, repoURL := range args {
_, err := repoIf.Delete(context.Background(), &repositorypkg.RepoQuery{Repo: repoURL})
_, err := repoIf.Delete(context.Background(), &repository.RepoQuery{Repo: repoURL})
errors.CheckError(err)
}
},
@@ -206,120 +116,23 @@ func NewRepoRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
return command
}
// Print table of repo info
func printRepoTable(repos appsv1.Repositories) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintf(w, "TYPE\tNAME\tREPO\tINSECURE\tLFS\tCREDS\tSTATUS\tMESSAGE\n")
for _, r := range repos {
var hasCreds string
if !r.HasCredentials() {
hasCreds = "false"
} else {
if r.InheritedCreds {
hasCreds = "inherited"
} else {
hasCreds = "true"
}
}
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%v\t%s\t%s\t%s\n", r.Type, r.Name, r.Repo, r.IsInsecure(), r.EnableLFS, hasCreds, r.ConnectionState.Status, r.ConnectionState.Message)
}
_ = w.Flush()
}
// Print list of repo urls or url patterns for repository credentials
func printRepoUrls(repos appsv1.Repositories) {
for _, r := range repos {
fmt.Println(r.Repo)
}
}
// NewRepoListCommand returns a new instance of an `argocd repo rm` command
func NewRepoListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
output string
refresh string
)
var command = &cobra.Command{
Use: "list",
Short: "List configured repositories",
Run: func(c *cobra.Command, args []string) {
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)
}
repos, err := repoIf.List(context.Background(), &repositorypkg.RepoQuery{ForceRefresh: forceRefresh})
defer util.Close(conn)
repos, err := repoIf.List(context.Background(), &repository.RepoQuery{})
errors.CheckError(err)
switch output {
case "yaml", "json":
err := PrintResourceList(repos.Items, output, false)
errors.CheckError(err)
case "url":
printRepoUrls(repos.Items)
// wide is the default
case "wide", "":
printRepoTable(repos.Items)
default:
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "REPO\tUSER\tSTATUS\tMESSAGE\n")
for _, r := range repos.Items {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", r.Repo, r.Username, r.ConnectionState.Status, r.ConnectionState.Message)
}
_ = w.Flush()
},
}
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
}
// 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

@@ -1,203 +0,0 @@
package commands
import (
"context"
"fmt"
"io/ioutil"
"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"
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/cli"
"github.com/argoproj/argo-cd/util/git"
)
// NewRepoCredsCommand returns a new instance of an `argocd repocreds` command
func NewRepoCredsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "repocreds",
Short: "Manage repository connection parameters",
Run: func(c *cobra.Command, args []string) {
c.HelpFunc()(c, args)
os.Exit(1)
},
}
command.AddCommand(NewRepoCredsAddCommand(clientOpts))
command.AddCommand(NewRepoCredsListCommand(clientOpts))
command.AddCommand(NewRepoCredsRemoveCommand(clientOpts))
return command
}
// NewRepoCredsAddCommand returns a new instance of an `argocd repocreds add` command
func NewRepoCredsAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
repo appsv1.RepoCreds
upsert bool
sshPrivateKeyPath string
tlsClientCertPath string
tlsClientCertKeyPath string
)
// For better readability and easier formatting
var repocredsAddExamples = ` # Add credentials with user/pass authentication to use for all repositories under https://git.example.com/repos
argocd repocreds add https://git.example.com/repos/ --username git --password secret
# Add credentials with SSH private key authentication to use for all repositories under ssh://git@git.example.com/repos
argocd repocreds add ssh://git@git.example.com/repos/ --ssh-private-key-path ~/.ssh/id_rsa
`
var command = &cobra.Command{
Use: "add REPOURL",
Short: "Add git repository connection parameters",
Example: repocredsAddExamples,
Run: func(c *cobra.Command, args []string) {
if len(args) != 1 {
c.HelpFunc()(c, args)
os.Exit(1)
}
// Repository URL
repo.URL = args[0]
// Specifying ssh-private-key-path is only valid for SSH repositories
if sshPrivateKeyPath != "" {
if ok, _ := git.IsSSHURL(repo.URL); ok {
keyData, err := ioutil.ReadFile(sshPrivateKeyPath)
if err != nil {
log.Fatal(err)
}
repo.SSHPrivateKey = string(keyData)
} else {
err := fmt.Errorf("--ssh-private-key-path is only supported for SSH repositories.")
errors.CheckError(err)
}
}
// tls-client-cert-path and tls-client-cert-key-key-path must always be
// specified together
if (tlsClientCertPath != "" && tlsClientCertKeyPath == "") || (tlsClientCertPath == "" && tlsClientCertKeyPath != "") {
err := fmt.Errorf("--tls-client-cert-path and --tls-client-cert-key-path must be specified together")
errors.CheckError(err)
}
// Specifying tls-client-cert-path is only valid for HTTPS repositories
if tlsClientCertPath != "" {
if git.IsHTTPSURL(repo.URL) {
tlsCertData, err := ioutil.ReadFile(tlsClientCertPath)
errors.CheckError(err)
tlsCertKey, err := ioutil.ReadFile(tlsClientCertKeyPath)
errors.CheckError(err)
repo.TLSClientCertData = string(tlsCertData)
repo.TLSClientCertKey = string(tlsCertKey)
} else {
err := fmt.Errorf("--tls-client-cert-path is only supported for HTTPS repositories")
errors.CheckError(err)
}
}
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoCredsClientOrDie()
defer io.Close(conn)
// If the user set a username, but didn't supply password via --password,
// then we prompt for it
if repo.Username != "" && repo.Password == "" {
repo.Password = cli.PromptPassword(repo.Password)
}
repoCreateReq := repocredspkg.RepoCredsCreateRequest{
Creds: &repo,
Upsert: upsert,
}
createdRepo, err := repoIf.CreateRepositoryCredentials(context.Background(), &repoCreateReq)
errors.CheckError(err)
fmt.Printf("repository credentials for '%s' added\n", createdRepo.URL)
},
}
command.Flags().StringVar(&repo.Username, "username", "", "username to the repository")
command.Flags().StringVar(&repo.Password, "password", "", "password to the repository")
command.Flags().StringVar(&sshPrivateKeyPath, "ssh-private-key-path", "", "path to the private ssh key (e.g. ~/.ssh/id_rsa)")
command.Flags().StringVar(&tlsClientCertPath, "tls-client-cert-path", "", "path to the TLS client cert (must be PEM format)")
command.Flags().StringVar(&tlsClientCertKeyPath, "tls-client-cert-key-path", "", "path to the TLS client cert's key path (must be PEM format)")
command.Flags().BoolVar(&upsert, "upsert", false, "Override an existing repository with the same name even if the spec differs")
return command
}
// NewRepoCredsRemoveCommand returns a new instance of an `argocd repocreds rm` command
func NewRepoCredsRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var command = &cobra.Command{
Use: "rm CREDSURL",
Short: "Remove repository credentials",
Run: func(c *cobra.Command, args []string) {
if len(args) == 0 {
c.HelpFunc()(c, args)
os.Exit(1)
}
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoCredsClientOrDie()
defer io.Close(conn)
for _, repoURL := range args {
_, err := repoIf.DeleteRepositoryCredentials(context.Background(), &repocredspkg.RepoCredsDeleteRequest{Url: repoURL})
errors.CheckError(err)
}
},
}
return command
}
// Print the repository credentials as table
func printRepoCredsTable(repos []appsv1.RepoCreds) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintf(w, "URL PATTERN\tUSERNAME\tSSH_CREDS\tTLS_CREDS\n")
for _, r := range repos {
if r.Username == "" {
r.Username = "-"
}
fmt.Fprintf(w, "%s\t%s\t%v\t%v\n", r.URL, r.Username, r.SSHPrivateKey != "", r.TLSClientCertData != "")
}
_ = w.Flush()
}
// Print list of repo urls or url patterns for repository credentials
func printRepoCredsUrls(repos []appsv1.RepoCreds) {
for _, r := range repos {
fmt.Println(r.URL)
}
}
// NewRepoCredsListCommand returns a new instance of an `argocd repo list` command
func NewRepoCredsListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
output string
)
var command = &cobra.Command{
Use: "list",
Short: "List configured repository credentials",
Run: func(c *cobra.Command, args []string) {
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoCredsClientOrDie()
defer io.Close(conn)
repos, err := repoIf.ListRepositoryCredentials(context.Background(), &repocredspkg.RepoCredsQuery{})
errors.CheckError(err)
switch output {
case "yaml", "json":
err := PrintResourceList(repos.Items, output, false)
errors.CheckError(err)
case "url":
printRepoCredsUrls(repos.Items)
case "wide", "":
printRepoCredsTable(repos.Items)
default:
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
}
},
}
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|url")
return command
}

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)
}
@@ -40,20 +36,15 @@ func NewCommand() *cobra.Command {
},
}
command.AddCommand(NewCompletionCommand())
command.AddCommand(NewVersionCmd(&clientOpts))
command.AddCommand(NewClusterCommand(&clientOpts, pathOpts))
command.AddCommand(NewApplicationCommand(&clientOpts))
command.AddCommand(NewLoginCommand(&clientOpts))
command.AddCommand(NewReloginCommand(&clientOpts))
command.AddCommand(NewRepoCommand(&clientOpts))
command.AddCommand(NewRepoCredsCommand(&clientOpts))
command.AddCommand(NewContextCommand(&clientOpts))
command.AddCommand(NewProjectCommand(&clientOpts))
command.AddCommand(NewAccountCommand(&clientOpts))
command.AddCommand(NewLogoutCommand(&clientOpts))
command.AddCommand(NewCertCommand(&clientOpts))
command.AddCommand(NewGPGCommand(&clientOpts))
defaultLocalConfigPath, err := localconfig.DefaultLocalConfigPath()
errors.CheckError(err)
@@ -62,15 +53,8 @@ func NewCommand() *cobra.Command {
command.PersistentFlags().BoolVar(&clientOpts.PlainText, "plaintext", config.GetBoolFlag("plaintext"), "Disable TLS")
command.PersistentFlags().BoolVar(&clientOpts.Insecure, "insecure", config.GetBoolFlag("insecure"), "Skip server certificate and domain verification")
command.PersistentFlags().StringVar(&clientOpts.CertFile, "server-crt", config.GetFlag("server-crt", ""), "Server certificate file")
command.PersistentFlags().StringVar(&clientOpts.ClientCertFile, "client-crt", config.GetFlag("client-crt", ""), "Client certificate file")
command.PersistentFlags().StringVar(&clientOpts.ClientCertKeyFile, "client-crt-key", config.GetFlag("client-crt-key", ""), "Client certificate key file")
command.PersistentFlags().StringVar(&clientOpts.AuthToken, "auth-token", config.GetFlag("auth-token", ""), "Authentication token")
command.PersistentFlags().BoolVar(&clientOpts.GRPCWeb, "grpc-web", config.GetBoolFlag("grpc-web"), "Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2.")
command.PersistentFlags().StringVar(&clientOpts.GRPCWebRootPath, "grpc-web-root-path", config.GetFlag("grpc-web-root-path", ""), "Enables gRPC-web protocol. Useful if Argo CD server is behind proxy which does not support HTTP2. Set web root.")
command.PersistentFlags().StringVar(&logFormat, "logformat", config.GetFlag("logformat", "text"), "Set the logging format. One of: text|json")
command.PersistentFlags().StringVar(&logLevel, "loglevel", config.GetFlag("loglevel", "info"), "Set the logging level. One of: debug|info|warn|error")
command.PersistentFlags().StringSliceVarP(&clientOpts.Headers, "header", "H", []string{}, "Sets additional header to all requests made by Argo CD CLI. (Can be repeated multiple times to add multiple headers, also supports comma separated headers)")
command.PersistentFlags().BoolVar(&clientOpts.PortForward, "port-forward", config.GetBoolFlag("port-forward"), "Connect to a random argocd-server port using port forwarding")
command.PersistentFlags().StringVar(&clientOpts.PortForwardNamespace, "port-forward-namespace", config.GetFlag("port-forward-namespace", ""), "Namespace name which should be used for port forwarding")
return command
}

View File

@@ -1,25 +0,0 @@
contexts:
- name: argocd1.example.com:443
server: argocd1.example.com:443
user: argocd1.example.com:443
- name: argocd2.example.com:443
server: argocd2.example.com:443
user: argocd2.example.com:443
- name: localhost:8080
server: localhost:8080
user: localhost:8080
current-context: localhost:8080
servers:
- server: argocd1.example.com:443
- server: argocd2.example.com:443
- plain-text: true
server: localhost:8080
users:
- auth-token: vErrYS3c3tReFRe$hToken
name: argocd1.example.com:443
refresh-token: vErrYS3c3tReFRe$hToken
- auth-token: vErrYS3c3tReFRe$hToken
name: argocd2.example.com:443
refresh-token: vErrYS3c3tReFRe$hToken
- auth-token: vErrYS3c3tReFRe$hToken
name: localhost:8080

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

@@ -5,129 +5,62 @@ import (
"fmt"
"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"
argocd "github.com/argoproj/argo-cd"
"github.com/argoproj/argo-cd/errors"
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
"github.com/argoproj/argo-cd/pkg/apiclient/version"
"github.com/argoproj/argo-cd/util"
)
// NewVersionCmd returns a new `version` command to be used as a sub-command to root
func NewVersionCmd(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
short bool
client bool
output string
)
var short bool
var client bool
versionCmd := cobra.Command{
Use: "version",
Short: "Print version information",
Example: ` # Print the full version of client and server to stdout
argocd version
# Print only full version of the client - no connection to server will be made
argocd version --client
# Print the full version of client and server in JSON format
argocd version -o json
# Print only client and server core version strings in YAML format
argocd version --short -o yaml
`,
Short: fmt.Sprintf("Print version information"),
Run: func(cmd *cobra.Command, args []string) {
cv := common.GetVersion()
switch output {
case "yaml", "json":
v := make(map[string]interface{})
if short {
v["client"] = map[string]string{cliName: cv.Version}
} else {
v["client"] = cv
version := argocd.GetVersion()
fmt.Printf("%s: %s\n", cliName, version)
if !short {
fmt.Printf(" BuildDate: %s\n", version.BuildDate)
fmt.Printf(" GitCommit: %s\n", version.GitCommit)
fmt.Printf(" GitTreeState: %s\n", version.GitTreeState)
if version.GitTag != "" {
fmt.Printf(" GitTag: %s\n", version.GitTag)
}
if !client {
sv := getServerVersion(clientOpts)
if short {
v["server"] = map[string]string{"argocd-server": sv.Version}
} else {
v["server"] = sv
}
}
err := PrintResource(v, output)
errors.CheckError(err)
case "wide", "short", "":
printClientVersion(&cv, short || (output == "short"))
if !client {
sv := getServerVersion(clientOpts)
printServerVersion(sv, short || (output == "short"))
}
default:
log.Fatalf("unknown output format: %s", output)
fmt.Printf(" GoVersion: %s\n", version.GoVersion)
fmt.Printf(" Compiler: %s\n", version.Compiler)
fmt.Printf(" Platform: %s\n", version.Platform)
}
if client {
return
}
// Get Server version
conn, versionIf := argocdclient.NewClientOrDie(clientOpts).NewVersionClientOrDie()
defer util.Close(conn)
serverVers, err := versionIf.Version(context.Background(), &empty.Empty{})
errors.CheckError(err)
fmt.Printf("%s: %s\n", "argocd-server", serverVers.Version)
if !short {
fmt.Printf(" BuildDate: %s\n", serverVers.BuildDate)
fmt.Printf(" GitCommit: %s\n", serverVers.GitCommit)
fmt.Printf(" GitTreeState: %s\n", serverVers.GitTreeState)
if version.GitTag != "" {
fmt.Printf(" GitTag: %s\n", serverVers.GitTag)
}
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)
}
},
}
versionCmd.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|short")
versionCmd.Flags().BoolVar(&short, "short", false, "print just the version number")
versionCmd.Flags().BoolVar(&client, "client", false, "client version only (no server required)")
return &versionCmd
}
func getServerVersion(options *argocdclient.ClientOptions) *version.VersionMessage {
conn, versionIf := argocdclient.NewClientOrDie(options).NewVersionClientOrDie()
defer argoio.Close(conn)
v, err := versionIf.Version(context.Background(), &empty.Empty{})
errors.CheckError(err)
return v
}
func printClientVersion(version *common.Version, short bool) {
fmt.Printf("%s: %s\n", cliName, version)
if short {
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)
}
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,16 +1,13 @@
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"
// load the oidc plugin (required to authenticate with OpenID Connect).
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
// load the azure plugin (required to authenticate with AKS clusters).
_ "k8s.io/client-go/plugin/pkg/client/auth/azure"
)
func main() {

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
@@ -21,44 +15,14 @@ const (
ArgoCDConfigMapName = "argocd-cm"
ArgoCDSecretName = "argocd-secret"
ArgoCDRBACConfigMapName = "argocd-rbac-cm"
// Contains SSH known hosts data for connecting repositories. Will get mounted as volume to pods
ArgoCDKnownHostsConfigMapName = "argocd-ssh-known-hosts-cm"
// Contains TLS certificate data for connecting repositories. Will get mounted as volume to pods
ArgoCDTLSCertsConfigMapName = "argocd-tls-certs-cm"
ArgoCDGPGKeysConfigMapName = "argocd-gpg-keys-cm"
)
// Some default configurables
const (
DefaultSystemNamespace = "kube-system"
DefaultRepoType = "git"
)
// Default listener ports for ArgoCD components
const (
DefaultPortAPIServer = 8080
DefaultPortRepoServer = 8081
DefaultPortArgoCDMetrics = 8082
DefaultPortArgoCDAPIServerMetrics = 8083
DefaultPortRepoServerMetrics = 8084
)
// Default paths on the pod's file system
const (
// The default path where TLS certificates for repositories are located
DefaultPathTLSConfig = "/app/config/tls"
// The default path where SSH known hosts are stored
DefaultPathSSHConfig = "/app/config/ssh"
// Default name for the SSH known hosts file
DefaultSSHKnownHostsName = "ssh_known_hosts"
// Default path to GnuPG home directory
DefaultGnuPgHomePath = "/app/config/gpg/keys"
)
const (
DefaultSyncRetryDuration = 5 * time.Second
DefaultSyncRetryMaxDuration = 3 * time.Minute
DefaultSyncRetryFactor = int64(2)
PortAPIServer = 8080
PortRepoServer = 8081
PortArgoCDMetrics = 8082
PortArgoCDAPIServerMetrics = 8083
PortRepoServerMetrics = 8084
)
// Argo CD application related constants
@@ -75,8 +39,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
@@ -87,8 +53,6 @@ const (
LoginEndpoint = "/auth/login"
// CallbackEndpoint is Argo CD's final callback endpoint we reach after OAuth 2.0 login flow has been completed
CallbackEndpoint = "/auth/callback"
// DexCallbackEndpoint is Argo CD's final callback endpoint when Dex is configured
DexCallbackEndpoint = "/api/dex/callback"
// ArgoCDClientAppName is name of the Oauth client app used when registering our web app to dex
ArgoCDClientAppName = "Argo CD"
// ArgoCDClientAppID is the Oauth client ID we will use when registering our app to dex
@@ -111,9 +75,10 @@ const (
// LabelValueSecretTypeCluster indicates a secret type of cluster
LabelValueSecretTypeCluster = "cluster"
// AnnotationCompareOptions is a comma-separated list of options for comparison
AnnotationCompareOptions = "argocd.argoproj.io/compare-options"
// 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"
@@ -121,6 +86,10 @@ const (
AnnotationKeyManagedBy = "managed-by"
// AnnotationValueManagedByArgoCD is a 'managed-by' annotation value for resources managed by Argo CD
AnnotationValueManagedByArgoCD = "argocd.argoproj.io"
// AnnotationKeyHelmHook is the helm hook annotation
AnnotationKeyHelmHook = "helm.sh/hook"
// AnnotationValueHelmHookCRDInstall is a value of crd helm hook
AnnotationValueHelmHookCRDInstall = "crd-install"
// ResourcesFinalizerName the finalizer value which we inject to finalize deletion of an application
ResourcesFinalizerName = "resources-finalizer.argocd.argoproj.io"
)
@@ -134,80 +103,14 @@ const (
// EnvVarFakeInClusterConfig is an environment variable to fake an in-cluster RESTConfig using
// the current kubectl context (for development purposes)
EnvVarFakeInClusterConfig = "ARGOCD_FAKE_IN_CLUSTER"
// Overrides the location where SSH known hosts for repo access data is stored
EnvVarSSHDataPath = "ARGOCD_SSH_DATA_PATH"
// Overrides the location where TLS certificate for repo access data is stored
EnvVarTLSDataPath = "ARGOCD_TLS_DATA_PATH"
// Specifies number of git remote operations attempts count
EnvGitAttemptsCount = "ARGOCD_GIT_ATTEMPTS_COUNT"
// Overrides git submodule support, true by default
EnvGitSubmoduleEnabled = "ARGOCD_GIT_MODULES_ENABLED"
// EnvK8sClientQPS is the QPS value used for the kubernetes client (default: 50)
EnvK8sClientQPS = "ARGOCD_K8S_CLIENT_QPS"
// EnvK8sClientBurst is the burst value used for the kubernetes client (default: twice the client QPS)
EnvK8sClientBurst = "ARGOCD_K8S_CLIENT_BURST"
// EnvClusterCacheResyncDuration is the env variable that holds cluster cache re-sync duration
EnvClusterCacheResyncDuration = "ARGOCD_CLUSTER_CACHE_RESYNC_DURATION"
// EnvK8sClientMaxIdleConnections is the number of max idle connections in K8s REST client HTTP transport (default: 500)
EnvK8sClientMaxIdleConnections = "ARGOCD_K8S_CLIENT_MAX_IDLE_CONNECTIONS"
// EnvGnuPGHome is the path to ArgoCD's GnuPG keyring for signature verification
EnvGnuPGHome = "ARGOCD_GNUPGHOME"
// EnvWatchAPIBufferSize is the buffer size used to transfer K8S watch events to watch API consumer
EnvWatchAPIBufferSize = "ARGOCD_WATCH_API_BUFFER_SIZE"
)
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.0.0"
// CacheVersion is a objects version cached using util/cache/cache.go.
// Number should be bumped in case of backward incompatible change to make sure cache is invalidated after upgrade.
CacheVersion = "1.0.0"
)
// GetGnuPGHomePath retrieves the path to use for GnuPG home directory, which is either taken from GNUPGHOME environment or a default value
func GetGnuPGHomePath() string {
if gnuPgHome := os.Getenv(EnvGnuPGHome); gnuPgHome == "" {
return DefaultGnuPgHomePath
} else {
return gnuPgHome
}
}
var (
// K8sClientConfigQPS controls the QPS to be used in K8s REST client configs
K8sClientConfigQPS float32 = 50
// K8sClientConfigBurst controls the burst to be used in K8s REST client configs
K8sClientConfigBurst int = 100
// K8sMaxIdleConnections controls the number of max idle connections in K8s REST client HTTP transport
K8sMaxIdleConnections = 500
// K8sMaxIdleConnections controls the duration of cluster cache refresh
K8SClusterResyncDuration = 12 * time.Hour
)
func init() {
if envQPS := os.Getenv(EnvK8sClientQPS); envQPS != "" {
if qps, err := strconv.ParseFloat(envQPS, 32); err != nil {
K8sClientConfigQPS = float32(qps)
}
}
if envBurst := os.Getenv(EnvK8sClientBurst); envBurst != "" {
if burst, err := strconv.Atoi(envBurst); err != nil {
K8sClientConfigBurst = burst
}
} else {
K8sClientConfigBurst = 2 * int(K8sClientConfigQPS)
}
if envMaxConn := os.Getenv(EnvK8sClientMaxIdleConnections); envMaxConn != "" {
if maxConn, err := strconv.Atoi(envMaxConn); err != nil {
K8sMaxIdleConnections = maxConn
}
}
if clusterResyncDurationStr := os.Getenv(EnvClusterCacheResyncDuration); clusterResyncDurationStr != "" {
if duration, err := time.ParseDuration(clusterResyncDurationStr); err == nil {
K8SClusterResyncDuration = duration
}
}
}

218
common/installer.go Normal file
View File

@@ -0,0 +1,218 @@
package common
import (
"fmt"
"time"
log "github.com/sirupsen/logrus"
apiv1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
apierr "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes"
)
// ArgoCDManagerServiceAccount is the name of the service account for managing a cluster
const (
ArgoCDManagerServiceAccount = "argocd-manager"
ArgoCDManagerClusterRole = "argocd-manager-role"
ArgoCDManagerClusterRoleBinding = "argocd-manager-role-binding"
)
// ArgoCDManagerPolicyRules are the policies to give argocd-manager
var ArgoCDManagerPolicyRules = []rbacv1.PolicyRule{
{
APIGroups: []string{"*"},
Resources: []string{"*"},
Verbs: []string{"*"},
},
{
NonResourceURLs: []string{"*"},
Verbs: []string{"*"},
},
}
// CreateServiceAccount creates a service account
func CreateServiceAccount(
clientset kubernetes.Interface,
serviceAccountName string,
namespace string,
) error {
serviceAccount := apiv1.ServiceAccount{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "ServiceAccount",
},
ObjectMeta: metav1.ObjectMeta{
Name: serviceAccountName,
Namespace: namespace,
},
}
_, err := clientset.CoreV1().ServiceAccounts(namespace).Create(&serviceAccount)
if err != nil {
if !apierr.IsAlreadyExists(err) {
return fmt.Errorf("Failed to create service account %q: %v", serviceAccountName, err)
}
log.Infof("ServiceAccount %q already exists", serviceAccountName)
return nil
}
log.Infof("ServiceAccount %q created", serviceAccountName)
return nil
}
// CreateClusterRole creates a cluster role
func CreateClusterRole(
clientset kubernetes.Interface,
clusterRoleName string,
rules []rbacv1.PolicyRule,
) error {
clusterRole := rbacv1.ClusterRole{
TypeMeta: metav1.TypeMeta{
APIVersion: "rbac.authorization.k8s.io/v1",
Kind: "ClusterRole",
},
ObjectMeta: metav1.ObjectMeta{
Name: clusterRoleName,
},
Rules: rules,
}
crclient := clientset.RbacV1().ClusterRoles()
_, err := crclient.Create(&clusterRole)
if err != nil {
if !apierr.IsAlreadyExists(err) {
return fmt.Errorf("Failed to create ClusterRole %q: %v", clusterRoleName, err)
}
_, err = crclient.Update(&clusterRole)
if err != nil {
return fmt.Errorf("Failed to update ClusterRole %q: %v", clusterRoleName, err)
}
log.Infof("ClusterRole %q updated", clusterRoleName)
} else {
log.Infof("ClusterRole %q created", clusterRoleName)
}
return nil
}
// CreateClusterRoleBinding create a ClusterRoleBinding
func CreateClusterRoleBinding(
clientset kubernetes.Interface,
clusterBindingRoleName,
serviceAccountName,
clusterRoleName string,
namespace string,
) error {
roleBinding := rbacv1.ClusterRoleBinding{
TypeMeta: metav1.TypeMeta{
APIVersion: "rbac.authorization.k8s.io/v1",
Kind: "ClusterRoleBinding",
},
ObjectMeta: metav1.ObjectMeta{
Name: clusterBindingRoleName,
},
RoleRef: rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "ClusterRole",
Name: clusterRoleName,
},
Subjects: []rbacv1.Subject{
{
Kind: rbacv1.ServiceAccountKind,
Name: serviceAccountName,
Namespace: namespace,
},
},
}
_, err := clientset.RbacV1().ClusterRoleBindings().Create(&roleBinding)
if err != nil {
if !apierr.IsAlreadyExists(err) {
return fmt.Errorf("Failed to create ClusterRoleBinding %s: %v", clusterBindingRoleName, err)
}
log.Infof("ClusterRoleBinding %q already exists", clusterBindingRoleName)
return nil
}
log.Infof("ClusterRoleBinding %q created, bound %q to %q", clusterBindingRoleName, serviceAccountName, clusterRoleName)
return nil
}
// InstallClusterManagerRBAC installs RBAC resources for a cluster manager to operate a cluster. Returns a token
func InstallClusterManagerRBAC(clientset kubernetes.Interface) (string, error) {
const ns = "kube-system"
err := CreateServiceAccount(clientset, ArgoCDManagerServiceAccount, ns)
if err != nil {
return "", err
}
err = CreateClusterRole(clientset, ArgoCDManagerClusterRole, ArgoCDManagerPolicyRules)
if err != nil {
return "", err
}
err = CreateClusterRoleBinding(clientset, ArgoCDManagerClusterRoleBinding, ArgoCDManagerServiceAccount, ArgoCDManagerClusterRole, ns)
if err != nil {
return "", err
}
var serviceAccount *apiv1.ServiceAccount
var secretName string
err = wait.Poll(500*time.Millisecond, 30*time.Second, func() (bool, error) {
serviceAccount, err = clientset.CoreV1().ServiceAccounts(ns).Get(ArgoCDManagerServiceAccount, metav1.GetOptions{})
if err != nil {
return false, err
}
if len(serviceAccount.Secrets) == 0 {
return false, nil
}
secretName = serviceAccount.Secrets[0].Name
return true, nil
})
if err != nil {
return "", fmt.Errorf("Failed to wait for service account secret: %v", err)
}
secret, err := clientset.CoreV1().Secrets(ns).Get(secretName, metav1.GetOptions{})
if err != nil {
return "", fmt.Errorf("Failed to retrieve secret %q: %v", secretName, err)
}
token, ok := secret.Data["token"]
if !ok {
return "", fmt.Errorf("Secret %q for service account %q did not have a token", secretName, serviceAccount)
}
return string(token), nil
}
// UninstallClusterManagerRBAC removes RBAC resources for a cluster manager to operate a cluster
func UninstallClusterManagerRBAC(clientset kubernetes.Interface) error {
return UninstallRBAC(clientset, "kube-system", ArgoCDManagerClusterRoleBinding, ArgoCDManagerClusterRole, ArgoCDManagerServiceAccount)
}
// UninstallRBAC uninstalls RBAC related resources for a binding, role, and service account
func UninstallRBAC(clientset kubernetes.Interface, namespace, bindingName, roleName, serviceAccount string) error {
if err := clientset.RbacV1().ClusterRoleBindings().Delete(bindingName, &metav1.DeleteOptions{}); err != nil {
if !apierr.IsNotFound(err) {
return fmt.Errorf("Failed to delete ClusterRoleBinding: %v", err)
}
log.Infof("ClusterRoleBinding %q not found", bindingName)
} else {
log.Infof("ClusterRoleBinding %q deleted", bindingName)
}
if err := clientset.RbacV1().ClusterRoles().Delete(roleName, &metav1.DeleteOptions{}); err != nil {
if !apierr.IsNotFound(err) {
return fmt.Errorf("Failed to delete ClusterRole: %v", err)
}
log.Infof("ClusterRole %q not found", roleName)
} else {
log.Infof("ClusterRole %q deleted", roleName)
}
if err := clientset.CoreV1().ServiceAccounts(namespace).Delete(serviceAccount, &metav1.DeleteOptions{}); err != nil {
if !apierr.IsNotFound(err) {
return fmt.Errorf("Failed to delete ServiceAccount: %v", err)
}
log.Infof("ServiceAccount %q in namespace %q not found", serviceAccount, namespace)
} else {
log.Infof("ServiceAccount %q deleted", serviceAccount)
}
return nil
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,510 +2,190 @@ package cache
import (
"context"
"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"
"golang.org/x/sync/semaphore"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/tools/cache"
"github.com/argoproj/argo-cd/common"
"github.com/argoproj/argo-cd/controller/metrics"
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/util/argo"
"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 LiveStateCache interface {
// Returns k8s server version
GetVersionsInfo(serverURL string) (string, []metav1.APIGroup, error)
// Returns true of given group kind is a namespaced resource
IsNamespaced(server string, gk schema.GroupKind) (bool, error)
// Returns synced cluster cache
GetClusterCache(server string) (clustercache.ClusterCache, error)
IsNamespaced(server string, obj *unstructured.Unstructured) (bool, error)
// Executes give callback against resource specified by the key and all its children
IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string)) error
IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode)) error
// Returns state of live nodes which correspond for target nodes of specified application.
GetManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error)
// Returns all top level resources (resources without owner references) of a specified namespace
GetNamespaceTopLevelResources(server string, namespace string) (map[kube.ResourceKey]appv1.ResourceNode, error)
// Starts watching resources of each controlled cluster.
Run(ctx context.Context) error
// Returns information about monitored clusters
GetClustersInfo() []clustercache.ClusterInfo
// Init must be executed before cache can be used
Init() error
Run(ctx context.Context)
// Invalidate invalidates the entire cluster state cache
Invalidate()
}
type ObjectUpdatedHandler = func(managedByApp map[string]bool, ref v1.ObjectReference)
type AppUpdatedHandler = func(appName string, fullRefresh 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(
db db.ArgoDB,
appInformer cache.SharedIndexInformer,
settingsMgr *settings.SettingsManager,
settings *settings.ArgoCDSettings,
kubectl kube.Kubectl,
metricsServer *metrics.MetricsServer,
onObjectUpdated ObjectUpdatedHandler) LiveStateCache {
onAppUpdated AppUpdatedHandler) LiveStateCache {
return &liveStateCache{
appInformer: appInformer,
db: db,
clusters: make(map[string]clustercache.ClusterCache),
onObjectUpdated: onObjectUpdated,
kubectl: kubectl,
settingsMgr: settingsMgr,
metricsServer: metricsServer,
// The default limit of 50 is chosen based on experiments.
listSemaphore: semaphore.NewWeighted(50),
appInformer: appInformer,
db: db,
clusters: make(map[string]*clusterInfo),
lock: &sync.Mutex{},
onAppUpdated: onAppUpdated,
kubectl: kubectl,
settings: settings,
metricsServer: metricsServer,
}
}
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
// listSemaphore is used to limit the number of concurrent memory consuming operations on the
// k8s list queries results across all clusters to avoid memory spikes during cache initialization.
listSemaphore *semaphore.Weighted
clusters map[string]clustercache.ClusterCache
cacheSettings cacheSettings
lock sync.RWMutex
db db.ArgoDB
clusters map[string]*clusterInfo
lock *sync.Mutex
appInformer cache.SharedIndexInformer
onAppUpdated AppUpdatedHandler
kubectl kube.Kubectl
settings *settings.ArgoCDSettings
metricsServer *metrics.MetricsServer
}
func (c *liveStateCache) loadCacheSettings() (*cacheSettings, error) {
appInstanceLabelKey, err := c.settingsMgr.GetAppInstanceLabelKey()
if err != nil {
return nil, err
}
resourcesFilter, err := c.settingsMgr.GetResourcesFilter()
if err != nil {
return nil, err
}
resourceOverrides, err := c.settingsMgr.GetResourceOverrides()
if err != nil {
return nil, err
}
clusterSettings := clustercache.Settings{
ResourceHealthOverride: lua.ResourceHealthOverrides(resourceOverrides),
ResourcesFilter: resourcesFilter,
}
return &cacheSettings{clusterSettings, appInstanceLabelKey}, 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,
CreatedAt: r.CreationTimestamp,
}
}
func resInfo(r *clustercache.Resource) *ResourceInfo {
info, ok := r.Info.(*ResourceInfo)
if !ok || info == nil {
info = &ResourceInfo{}
}
return info
}
func isRootAppNode(r *clustercache.Resource) bool {
return resInfo(r).AppName != "" && len(r.OwnerRefs) == 0
}
func getApp(r *clustercache.Resource, ns map[kube.ResourceKey]*clustercache.Resource) string {
return getAppRecursive(r, ns, map[kube.ResourceKey]bool{})
}
func ownerRefGV(ownerRef metav1.OwnerReference) schema.GroupVersion {
gv, err := schema.ParseGroupVersion(ownerRef.APIVersion)
if err != nil {
gv = schema.GroupVersion{}
}
return gv
}
func getAppRecursive(r *clustercache.Resource, ns map[kube.ResourceKey]*clustercache.Resource, visited map[kube.ResourceKey]bool) string {
if !visited[r.ResourceKey()] {
visited[r.ResourceKey()] = true
} else {
log.Warnf("Circular dependency detected: %v.", visited)
return resInfo(r).AppName
}
if resInfo(r).AppName != "" {
return resInfo(r).AppName
}
for _, ownerRef := range r.OwnerRefs {
gv := ownerRefGV(ownerRef)
if parent, ok := ns[kube.NewResourceKey(gv.Group, ownerRef.Kind, r.Ref.Namespace, ownerRef.Name)]; ok {
app := getAppRecursive(parent, ns, visited)
if app != "" {
return app
}
}
}
return ""
}
var (
ignoredRefreshResources = map[string]bool{
"/" + kube.EndpointsKind: true,
}
)
// skipAppRequeuing checks if the object is an API type which we want to skip requeuing against.
// We ignore API types which have a high churn rate, and/or whose updates are irrelevant to the app
func skipAppRequeuing(key kube.ResourceKey) bool {
return ignoredRefreshResources[key.Group+"/"+key.Kind]
}
func (c *liveStateCache) getCluster(server string) (clustercache.ClusterCache, error) {
c.lock.RLock()
clusterCache, ok := c.clusters[server]
cacheSettings := c.cacheSettings
c.lock.RUnlock()
if ok {
return clusterCache, nil
}
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.SetListSemaphore(c.listSemaphore),
clustercache.SetResyncTimeout(common.K8SClusterResyncDuration),
clustercache.SetSettings(cacheSettings.clusterSettings),
clustercache.SetNamespaces(cluster.Namespaces),
clustercache.SetPopulateResourceInfoHandler(func(un *unstructured.Unstructured, isRoot bool) (interface{}, bool) {
res := &ResourceInfo{}
populateNodeInfo(un, res)
res.Health, _ = health.GetResourceHealth(un, cacheSettings.clusterSettings.ResourceHealthOverride)
appName := kube.GetAppInstanceLabel(un, cacheSettings.appInstanceLabelKey)
if isRoot && appName != "" {
res.AppName = appName
}
// edge case. we do not label CRDs, so they miss the tracking label we inject. But we still
// want the full resource to be available in our cache (to diff), so we store all CRDs
return res, res.AppName != "" || un.GroupVersionKind().Kind == kube.CustomResourceDefinitionKind
}),
)
_ = clusterCache.OnResourceUpdated(func(newRes *clustercache.Resource, oldRes *clustercache.Resource, namespaceResources map[kube.ResourceKey]*clustercache.Resource) {
toNotify := make(map[string]bool)
var ref v1.ObjectReference
if newRes != nil {
ref = newRes.Ref
} else {
ref = oldRes.Ref
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),
onAppUpdated: c.onAppUpdated,
kubectl: c.kubectl,
cluster: cluster,
syncTime: nil,
syncLock: &sync.Mutex{},
log: log.WithField("server", cluster.Server),
settings: c.settings,
}
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[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.lock.Lock()
clust.invalidate()
clust.lock.Unlock()
}
log.Info("live state cache invalidated")
}
func (c *liveStateCache) IsNamespaced(server string, gk schema.GroupKind) (bool, error) {
func (c *liveStateCache) IsNamespaced(server string, obj *unstructured.Unstructured) (bool, error) {
clusterInfo, err := c.getSyncedCluster(server)
if err != nil {
return false, err
}
return clusterInfo.IsNamespaced(gk)
return clusterInfo.isNamespaced(obj), nil
}
func (c *liveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string)) error {
func (c *liveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(child appv1.ResourceNode)) error {
clusterInfo, err := c.getSyncedCluster(server)
if err != nil {
return err
}
clusterInfo.IterateHierarchy(key, func(resource *clustercache.Resource, namespaceResources map[kube.ResourceKey]*clustercache.Resource) {
action(asResourceNode(resource), getApp(resource, namespaceResources))
})
clusterInfo.iterateHierarchy(key, action)
return nil
}
func (c *liveStateCache) GetNamespaceTopLevelResources(server string, namespace string) (map[kube.ResourceKey]appv1.ResourceNode, error) {
clusterInfo, err := c.getSyncedCluster(server)
if err != nil {
return nil, err
}
resources := clusterInfo.GetNamespaceTopLevelResources(namespace)
res := make(map[kube.ResourceKey]appv1.ResourceNode)
for k, r := range resources {
res[k] = asResourceNode(r)
}
return res, nil
}
func (c *liveStateCache) GetManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
clusterInfo, err := c.getSyncedCluster(a.Spec.Destination.Server)
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) {
clusterInfo, err := c.getSyncedCluster(serverURL)
if err != nil {
return "", nil, err
}
return clusterInfo.GetServerVersion(), clusterInfo.GetAPIGroups(), nil
}
func (c *liveStateCache) isClusterHasApps(apps []interface{}, cluster *appv1.Cluster) bool {
func isClusterHasApps(apps []interface{}, cluster *appv1.Cluster) bool {
for _, obj := range apps {
app, ok := obj.(*appv1.Application)
if !ok {
continue
}
err := argo.ValidateDestination(context.Background(), &app.Spec.Destination, c.db)
if err != nil {
continue
}
if app.Spec.Destination.Server == cluster.Server {
if app, ok := obj.(*appv1.Application); ok && app.Spec.Destination.Server == cluster.Server {
return true
}
}
return false
}
func (c *liveStateCache) watchSettings(ctx context.Context) {
updateCh := make(chan *settings.ArgoCDSettings, 1)
c.settingsMgr.Subscribe(updateCh)
done := false
for !done {
select {
case <-updateCh:
nextCacheSettings, err := c.loadCacheSettings()
if err != nil {
log.Warnf("Failed to read updated settings: %v", err)
continue
}
c.lock.Lock()
needInvalidate := false
if !reflect.DeepEqual(c.cacheSettings, *nextCacheSettings) {
c.cacheSettings = *nextCacheSettings
needInvalidate = true
}
c.lock.Unlock()
if needInvalidate {
c.invalidate(*nextCacheSettings)
}
case <-ctx.Done():
done = true
}
}
log.Info("shutting down settings watch")
c.settingsMgr.Unsubscribe(updateCh)
close(updateCh)
}
func (c *liveStateCache) Init() error {
cacheSettings, err := c.loadCacheSettings()
if err != nil {
return err
}
c.cacheSettings = *cacheSettings
return nil
}
// 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)
func (c *liveStateCache) Run(ctx context.Context) {
util.RetryUntilSucceed(func() error {
clusterEventCallback := func(event *db.ClusterEvent) {
c.lock.Lock()
defer c.lock.Unlock()
if cluster, ok := c.clusters[event.Cluster.Server]; ok {
if event.Type == watch.Deleted {
cluster.invalidate()
delete(c.clusters, event.Cluster.Server)
} else if event.Type == watch.Modified {
cluster.cluster = event.Cluster
cluster.invalidate()
}
} else if event.Type == watch.Added && isClusterHasApps(c.appInformer.GetStore().List(), event.Cluster) {
go func() {
// warm up cache for cluster with apps
_, _ = c.getSyncedCluster(event.Cluster.Server)
}()
}
}
kube.RetryUntilSucceed(ctx, clustercache.ClusterRetryTimeout, "watch clusters", func() error {
return c.db.WatchClusters(ctx, c.handleAddEvent, c.handleModEvent, c.handleDeleteEvent)
})
return c.db.WatchClusters(ctx, clusterEventCallback)
}, "watch clusters", ctx, clusterRetryTimeout)
<-ctx.Done()
c.invalidate(c.cacheSettings)
return nil
}
func (c *liveStateCache) handleAddEvent(cluster *appv1.Cluster) {
c.lock.Lock()
_, ok := c.clusters[cluster.Server]
c.lock.Unlock()
if !ok {
if c.isClusterHasApps(c.appInformer.GetStore().List(), cluster) {
go func() {
// warm up cache for cluster with apps
_, _ = c.getSyncedCluster(cluster.Server)
}()
}
}
}
func (c *liveStateCache) handleModEvent(oldCluster *appv1.Cluster, newCluster *appv1.Cluster) {
c.lock.Lock()
cluster, ok := c.clusters[newCluster.Server]
c.lock.Unlock()
if ok {
var updateSettings []clustercache.UpdateSettingsFunc
if !reflect.DeepEqual(oldCluster.Config, newCluster.Config) {
updateSettings = append(updateSettings, clustercache.SetConfig(newCluster.RESTConfig()))
}
if !reflect.DeepEqual(oldCluster.Namespaces, newCluster.Namespaces) {
updateSettings = append(updateSettings, clustercache.SetNamespaces(newCluster.Namespaces))
}
forceInvalidate := false
if newCluster.RefreshRequestedAt != nil &&
cluster.GetClusterInfo().LastCacheSyncTime != nil &&
cluster.GetClusterInfo().LastCacheSyncTime.Before(newCluster.RefreshRequestedAt.Time) {
forceInvalidate = true
}
if len(updateSettings) > 0 || forceInvalidate {
cluster.Invalidate(updateSettings...)
go func() {
// warm up cluster cache
_ = cluster.EnsureSynced()
}()
}
}
}
func (c *liveStateCache) handleDeleteEvent(clusterServer string) {
c.lock.Lock()
defer c.lock.Unlock()
cluster, ok := c.clusters[clusterServer]
if ok {
cluster.Invalidate()
delete(c.clusters, clusterServer)
}
}
func (c *liveStateCache) GetClustersInfo() []clustercache.ClusterInfo {
clusters := make(map[string]clustercache.ClusterCache)
c.lock.RLock()
for k := range c.clusters {
clusters[k] = c.clusters[k]
}
c.lock.RUnlock()
res := make([]clustercache.ClusterInfo, 0)
for server, c := range clusters {
info := c.GetClusterInfo()
info.Server = server
res = append(res, info)
}
return res
}
func (c *liveStateCache) GetClusterCache(server string) (clustercache.ClusterCache, error) {
return c.getSyncedCluster(server)
}

View File

@@ -1,52 +0,0 @@
package cache
import (
"testing"
"github.com/argoproj/gitops-engine/pkg/cache"
"github.com/argoproj/gitops-engine/pkg/cache/mocks"
"github.com/stretchr/testify/mock"
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
)
func TestHandleModEvent_HasChanges(t *testing.T) {
clusterCache := &mocks.ClusterCache{}
clusterCache.On("Invalidate", mock.Anything, mock.Anything).Return(nil).Once()
clusterCache.On("EnsureSynced").Return(nil).Once()
clustersCache := liveStateCache{
clusters: map[string]cache.ClusterCache{
"https://mycluster": clusterCache,
},
}
clustersCache.handleModEvent(&appv1.Cluster{
Server: "https://mycluster",
Config: appv1.ClusterConfig{Username: "foo"},
}, &appv1.Cluster{
Server: "https://mycluster",
Config: appv1.ClusterConfig{Username: "bar"},
Namespaces: []string{"default"},
})
}
func TestHandleModEvent_NoChanges(t *testing.T) {
clusterCache := &mocks.ClusterCache{}
clusterCache.On("Invalidate", mock.Anything).Panic("should not invalidate")
clusterCache.On("EnsureSynced").Return(nil).Panic("should not re-sync")
clustersCache := liveStateCache{
clusters: map[string]cache.ClusterCache{
"https://mycluster": clusterCache,
},
}
clustersCache.handleModEvent(&appv1.Cluster{
Server: "https://mycluster",
Config: appv1.ClusterConfig{Username: "bar"},
}, &appv1.Cluster{
Server: "https://mycluster",
Config: appv1.ClusterConfig{Username: "bar"},
})
}

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

@@ -0,0 +1,479 @@
package cache
import (
"context"
"fmt"
"runtime/debug"
"sync"
"time"
"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"
"github.com/argoproj/argo-cd/util/settings"
)
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 {
syncLock *sync.Mutex
syncTime *time.Time
syncError error
apisMeta map[schema.GroupKind]*apiMeta
lock *sync.Mutex
nodes map[kube.ResourceKey]*node
nsIndex map[string]map[kube.ResourceKey]*node
onAppUpdated AppUpdatedHandler
kubectl kube.Kubectl
cluster *appv1.Cluster
log *log.Entry
settings *settings.ArgoCDSettings
}
func (c *clusterInfo) replaceResourceCache(gk schema.GroupKind, resourceVersion string, objs []unstructured.Unstructured) {
c.lock.Lock()
defer c.lock.Unlock()
info, ok := c.apisMeta[gk]
if ok {
objByKind := make(map[kube.ResourceKey]*unstructured.Unstructured)
for i := range objs {
objByKind[kube.GetResourceKey(&objs[i])] = &objs[i]
}
for i := range objs {
obj := &objs[i]
key := kube.GetResourceKey(&objs[i])
existingNode, exists := c.nodes[key]
c.onNodeUpdated(exists, existingNode, obj, key)
}
for key, existingNode := range c.nodes {
if key.Kind != gk.Kind || key.Group != gk.Group {
continue
}
if _, ok := objByKind[key]; !ok {
c.onNodeRemoved(key, existingNode)
}
}
info.resourceVersion = resourceVersion
}
}
func (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: "",
})
}
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.settings.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.syncLock.Lock()
defer c.syncLock.Unlock()
c.syncTime = nil
for i := range c.apisMeta {
c.apisMeta[i].watchCancel()
}
c.apisMeta = nil
}
func (c *clusterInfo) synced() bool {
if c.syncTime == nil {
return false
}
if c.syncError != nil {
return time.Now().Before(c.syncTime.Add(clusterRetryTimeout))
}
return time.Now().Before(c.syncTime.Add(clusterSyncTimeout))
}
func (c *clusterInfo) stopWatching(gk schema.GroupKind) {
c.syncLock.Lock()
defer c.syncLock.Unlock()
if info, ok := c.apisMeta[gk]; ok {
info.watchCancel()
delete(c.apisMeta, gk)
c.replaceResourceCache(gk, "", []unstructured.Unstructured{})
log.Warnf("Stop watching %s not found on %s.", gk, c.cluster.Server)
}
}
// startMissingWatches lists supported cluster resources and start watching for changes unless watch is already running
func (c *clusterInfo) startMissingWatches() error {
apis, err := c.kubectl.GetAPIResources(c.cluster.RESTConfig(), c.settings)
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
go c.watchEvents(ctx, api, info)
}
}
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) {
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.syncLock, func() error {
if info.resourceVersion == "" {
list, err := api.Interface.List(metav1.ListOptions{})
if err != nil {
return err
}
c.replaceResourceCache(api.GroupKind, list.GetResourceVersion(), list.Items)
}
return nil
})
if err != nil {
return err
}
w, err := api.Interface.Watch(metav1.ListOptions{ResourceVersion: info.resourceVersion})
if errors.IsNotFound(err) {
c.stopWatching(api.GroupKind)
return nil
}
err = runSynced(c.syncLock, func() error {
if errors.IsGone(err) {
info.resourceVersion = ""
log.Warnf("Resource version of %s on %s is too old.", api.GroupKind, c.cluster.Server)
}
return err
})
if 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()
err = c.processEvent(event.Type, obj)
if err != nil {
log.Warnf("Failed to process event %s %s/%s/%s: %v", event.Type, obj.GroupVersionKind(), obj.GetNamespace(), obj.GetName(), err)
continue
}
if kube.IsCRD(obj) {
if event.Type == watch.Deleted {
group, groupOk, groupErr := unstructured.NestedString(obj.Object, "spec", "group")
kind, kindOk, kindErr := unstructured.NestedString(obj.Object, "spec", "names", "kind")
if groupOk && groupErr == nil && kindOk && kindErr == nil {
gk := schema.GroupKind{Group: group, Kind: kind}
c.stopWatching(gk)
}
} else {
err = runSynced(c.syncLock, 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) 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)
apis, err := c.kubectl.GetAPIResources(c.cluster.RESTConfig(), c.settings)
if err != nil {
return err
}
lock := sync.Mutex{}
err = util.RunAllAsync(len(apis), func(i int) error {
api := apis[i]
list, err := api.Interface.List(metav1.ListOptions{})
if err != nil {
return err
}
lock.Lock()
for i := range list.Items {
c.setNode(c.createObjInfo(&list.Items[i], c.settings.GetAppInstanceLabelKey()))
}
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.syncLock.Lock()
defer c.syncLock.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) iterateHierarchy(key kube.ResourceKey, action func(child appv1.ResourceNode)) {
c.lock.Lock()
defer c.lock.Unlock()
if objInfo, ok := c.nodes[key]; ok {
action(objInfo.asResourceNode())
nsNodes := c.nsIndex[key.Namespace]
for _, child := range nsNodes {
if objInfo.isParentOf(child) {
action(child.asResourceNode())
child.iterateChildren(nsNodes, map[kube.ResourceKey]bool{objInfo.resourceKey(): true}, action)
}
}
}
}
func (c *clusterInfo) isNamespaced(obj *unstructured.Unstructured) bool {
if api, ok := c.apisMeta[kube.GetResourceKey(obj).GroupKind()]; 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))
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
}
}
}
}
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) error {
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)
}
return nil
}
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.settings.GetAppInstanceLabelKey())
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]
}
}
for name, full := range toNotify {
c.onAppUpdated(name, full, 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)
if appName != "" {
c.onAppUpdated(appName, n.isRootAppNode(), n.ref)
}
}
var (
ignoredRefreshResources = map[string]bool{
"/" + kube.EndpointsKind: true,
}
)
// 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]
}

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

@@ -0,0 +1,421 @@
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/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"
"github.com/argoproj/argo-cd/util/settings"
)
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:
name: helm-guestbook-pod
namespace: default
ownerReferences:
- apiVersion: extensions/v1beta1
kind: ReplicaSet
name: helm-guestbook-rs
resourceVersion: "123"`)
testRS = strToUnstructured(`
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: helm-guestbook-rs
namespace: default
ownerReferences:
- apiVersion: extensions/v1beta1
kind: Deployment
name: helm-guestbook
resourceVersion: "123"`)
testDeploy = strToUnstructured(`
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/instance: helm-guestbook
name: helm-guestbook
namespace: default
resourceVersion: "123"`)
testService = strToUnstructured(`
apiVersion: v1
kind: Service
metadata:
name: helm-guestbook
namespace: default
resourceVersion: "123"
spec:
selector:
app: guestbook
type: LoadBalancer
status:
loadBalancer:
ingress:
- hostname: localhost`)
testIngress = strToUnstructured(`
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: helm-guestbook
namespace: default
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"},
Interface: client.Resource(schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}),
Meta: metav1.APIResource{Namespaced: true},
}, {
GroupKind: schema.GroupKind{Group: "apps", Kind: "ReplicaSet"},
Interface: client.Resource(schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "replicasets"}),
Meta: metav1.APIResource{Namespaced: true},
}, {
GroupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"},
Interface: client.Resource(schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}),
Meta: metav1.APIResource{Namespaced: true},
}}
return newClusterExt(kubetest.MockKubectlCmd{APIResources: apiResources})
}
func newClusterExt(kubectl kube.Kubectl) *clusterInfo {
return &clusterInfo{
lock: &sync.Mutex{},
nodes: make(map[kube.ResourceKey]*node),
onAppUpdated: func(appName string, fullRefresh bool, reference corev1.ObjectReference) {},
kubectl: kubectl,
nsIndex: make(map[string]map[kube.ResourceKey]*node),
cluster: &appv1.Cluster{},
syncTime: nil,
syncLock: &sync.Mutex{},
apisMeta: make(map[schema.GroupKind]*apiMeta),
log: log.WithField("cluster", "test"),
settings: &settings.ArgoCDSettings{},
}
}
func getChildren(cluster *clusterInfo, un *unstructured.Unstructured) []appv1.ResourceNode {
hierarchy := make([]appv1.ResourceNode, 0)
cluster.iterateHierarchy(kube.GetResourceKey(un), func(child appv1.ResourceNode) {
hierarchy = append(hierarchy, child)
})
return hierarchy[1:]
}
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",
},
ParentRefs: []appv1.ResourceRef{{
Group: "apps",
Version: "",
Kind: "ReplicaSet",
Namespace: "default",
Name: "helm-guestbook-rs",
}},
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",
},
ResourceVersion: "123",
Health: &appv1.HealthStatus{Status: appv1.HealthStatusHealthy},
Info: []appv1.InfoItem{},
ParentRefs: []appv1.ResourceRef{{Group: "apps", Version: "", Kind: "Deployment", Namespace: "default", Name: "helm-guestbook"}},
}}, 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)
err = cluster.processEvent(watch.Deleted, testPod)
assert.Nil(t, err)
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:
name: helm-guestbook-pod2
namespace: default
ownerReferences:
- apiVersion: extensions/v1beta1
kind: ReplicaSet
name: helm-guestbook-rs
resourceVersion: "123"`)
err = cluster.processEvent(watch.Added, newPod)
assert.Nil(t, err)
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",
},
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",
}},
ResourceVersion: "123",
}, {
ResourceRef: appv1.ResourceRef{
Kind: "Pod",
Namespace: "default",
Name: "helm-guestbook-pod2",
Group: "",
Version: "v1",
},
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",
}},
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,
},
},
}},
}
err = cluster.processEvent(watch.Modified, mustToUnstructured(pod))
assert.Nil(t, err)
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.onAppUpdated = func(appName string, fullRefresh bool, _ corev1.ObjectReference) {
updatesReceived = append(updatesReceived, fmt.Sprintf("%s: %v", appName, fullRefresh))
}
err := cluster.ensureSynced()
assert.Nil(t, err)
err = cluster.processEvent(watch.Modified, mustToUnstructured(testPod))
assert.Nil(t, err)
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)
}

View File

@@ -3,40 +3,37 @@ package cache
import (
"fmt"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
"github.com/argoproj/gitops-engine/pkg/utils/text"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
k8snode "k8s.io/kubernetes/pkg/util/node"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/util/resource"
"github.com/argoproj/argo-cd/util"
"github.com/argoproj/argo-cd/util/kube"
)
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)})
}
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":
case "extensions":
switch gvk.Kind {
case kube.IngressKind:
populateIngressInfo(un, res)
populateIngressInfo(un, node)
return
}
}
node.info = []v1alpha1.InfoItem{}
}
func getIngress(un *unstructured.Unstructured) []v1.LoadBalancerIngress {
@@ -57,16 +54,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 +84,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
}
@@ -125,23 +122,14 @@ func populateIngressInfo(un *unstructured.Unstructured, res *ResourceInfo) {
stringPort = fmt.Sprintf("%v", port)
}
var externalURL string
switch stringPort {
case "80", "http":
externalURL = fmt.Sprintf("http://%s", host)
urlsSet[fmt.Sprintf("http://%s", host)] = true
case "443", "https":
externalURL = fmt.Sprintf("https://%s", host)
urlsSet[fmt.Sprintf("https://%s", host)] = true
default:
externalURL = fmt.Sprintf("http://%s:%s", host, stringPort)
urlsSet[fmt.Sprintf("http://%s:%s", host, stringPort)] = true
}
subPath := ""
if nestedPath, ok, err := unstructured.NestedString(path, "path"); ok && err == nil {
subPath = nestedPath
}
externalURL += subPath
urlsSet[externalURL] = true
}
}
}
@@ -154,13 +142,14 @@ 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 {
node.info = []v1alpha1.InfoItem{}
return
}
restarts := 0
@@ -180,9 +169,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
@@ -248,9 +237,10 @@ func populatePodInfo(un *unstructured.Unstructured, res *ResourceInfo) {
reason = "Terminating"
}
node.info = make([]v1alpha1.InfoItem, 0)
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"}},
@@ -124,8 +68,8 @@ func TestGetIngressInfo(t *testing.T) {
Kind: kube.ServiceKind,
Name: "helm-guestbook",
}},
ExternalURLs: []string{"https://helm-guestbook.com/"},
}, info.NetworkingInfo)
ExternalURLs: []string{"https://helm-guestbook.com"},
}, 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"}},
@@ -159,120 +103,6 @@ func TestGetIngressInfoNoHost(t *testing.T) {
Kind: kube.ServiceKind,
Name: "helm-guestbook",
}},
ExternalURLs: []string{"https://107.178.210.11/"},
}, info.NetworkingInfo)
}
func TestExternalUrlWithSubPath(t *testing.T) {
ingress := strToUnstructured(`
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: helm-guestbook
namespace: default
spec:
rules:
- http:
paths:
- backend:
serviceName: helm-guestbook
servicePort: 443
path: /my/sub/path/
status:
loadBalancer:
ingress:
- ip: 107.178.210.11`)
info := &ResourceInfo{}
populateNodeInfo(ingress, info)
expectedExternalUrls := []string{"https://107.178.210.11/my/sub/path/"}
assert.Equal(t, expectedExternalUrls, info.NetworkingInfo.ExternalURLs)
}
func TestExternalUrlWithMultipleSubPaths(t *testing.T) {
ingress := strToUnstructured(`
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: helm-guestbook
namespace: default
spec:
rules:
- host: helm-guestbook.com
http:
paths:
- backend:
serviceName: helm-guestbook
servicePort: 443
path: /my/sub/path/
- backend:
serviceName: helm-guestbook-2
servicePort: 443
path: /my/sub/path/2
- backend:
serviceName: helm-guestbook-3
servicePort: 443
status:
loadBalancer:
ingress:
- ip: 107.178.210.11`)
info := &ResourceInfo{}
populateNodeInfo(ingress, info)
expectedExternalUrls := []string{"https://helm-guestbook.com/my/sub/path/", "https://helm-guestbook.com/my/sub/path/2", "https://helm-guestbook.com"}
actualURLs := info.NetworkingInfo.ExternalURLs
sort.Strings(expectedExternalUrls)
sort.Strings(actualURLs)
assert.Equal(t, expectedExternalUrls, actualURLs)
}
func TestExternalUrlWithNoSubPath(t *testing.T) {
ingress := strToUnstructured(`
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: helm-guestbook
namespace: default
spec:
rules:
- http:
paths:
- backend:
serviceName: helm-guestbook
servicePort: 443
status:
loadBalancer:
ingress:
- ip: 107.178.210.11`)
info := &ResourceInfo{}
populateNodeInfo(ingress, info)
expectedExternalUrls := []string{"https://107.178.210.11"}
assert.Equal(t, expectedExternalUrls, info.NetworkingInfo.ExternalURLs)
}
func TestExternalUrlWithNetworkingApi(t *testing.T) {
ingress := strToUnstructured(`
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: helm-guestbook
namespace: default
spec:
rules:
- http:
paths:
- backend:
serviceName: helm-guestbook
servicePort: 443
status:
loadBalancer:
ingress:
- ip: 107.178.210.11`)
info := &ResourceInfo{}
populateNodeInfo(ingress, info)
expectedExternalUrls := []string{"https://107.178.210.11"}
assert.Equal(t, expectedExternalUrls, info.NetworkingInfo.ExternalURLs)
ExternalURLs: []string{"https://107.178.210.11"},
}, node.networkingInfo)
}

View File

@@ -2,63 +2,26 @@
package mocks
import (
context "context"
cache "github.com/argoproj/gitops-engine/pkg/cache"
kube "github.com/argoproj/gitops-engine/pkg/utils/kube"
mock "github.com/stretchr/testify/mock"
schema "k8s.io/apimachinery/pkg/runtime/schema"
unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
)
import context "context"
import kube "github.com/argoproj/argo-cd/util/kube"
import mock "github.com/stretchr/testify/mock"
import unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
import v1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
// LiveStateCache is an autogenerated mock type for the LiveStateCache type
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)
// Delete provides a mock function with given fields: server, obj
func (_m *LiveStateCache) Delete(server string, obj *unstructured.Unstructured) error {
ret := _m.Called(server, obj)
var r0 cache.ClusterCache
if rf, ok := ret.Get(0).(func(string) cache.ClusterCache); ok {
r0 = rf(server)
var r0 error
if rf, ok := ret.Get(0).(func(string, *unstructured.Unstructured) error); ok {
r0 = rf(server, obj)
} 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 {
ret := _m.Called()
var r0 []cache.ClusterInfo
if rf, ok := ret.Get(0).(func() []cache.ClusterInfo); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]cache.ClusterInfo)
}
r0 = ret.Error(0)
}
return r0
@@ -87,87 +50,25 @@ func (_m *LiveStateCache) GetManagedLiveObjs(a *v1alpha1.Application, targetObjs
return r0, r1
}
// GetNamespaceTopLevelResources provides a mock function with given fields: server, namespace
func (_m *LiveStateCache) GetNamespaceTopLevelResources(server string, namespace string) (map[kube.ResourceKey]v1alpha1.ResourceNode, error) {
ret := _m.Called(server, namespace)
var r0 map[kube.ResourceKey]v1alpha1.ResourceNode
if rf, ok := ret.Get(0).(func(string, string) map[kube.ResourceKey]v1alpha1.ResourceNode); ok {
r0 = rf(server, namespace)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[kube.ResourceKey]v1alpha1.ResourceNode)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(server, namespace)
} else {
r1 = ret.Error(1)
}
return r0, r1
// Invalidate provides a mock function with given fields:
func (_m *LiveStateCache) Invalidate() {
_m.Called()
}
// GetVersionsInfo provides a mock function with given fields: serverURL
func (_m *LiveStateCache) GetVersionsInfo(serverURL string) (string, []v1.APIGroup, error) {
ret := _m.Called(serverURL)
var r0 string
if rf, ok := ret.Get(0).(func(string) string); ok {
r0 = rf(serverURL)
} else {
r0 = ret.Get(0).(string)
}
var r1 []v1.APIGroup
if rf, ok := ret.Get(1).(func(string) []v1.APIGroup); ok {
r1 = rf(serverURL)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).([]v1.APIGroup)
}
}
var r2 error
if rf, ok := ret.Get(2).(func(string) error); ok {
r2 = rf(serverURL)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
// Init provides a mock function with given fields:
func (_m *LiveStateCache) Init() error {
ret := _m.Called()
var r0 error
if rf, ok := ret.Get(0).(func() error); ok {
r0 = rf()
} else {
r0 = ret.Error(0)
}
return r0
}
// IsNamespaced provides a mock function with given fields: server, gk
func (_m *LiveStateCache) IsNamespaced(server string, gk schema.GroupKind) (bool, error) {
ret := _m.Called(server, gk)
// IsNamespaced provides a mock function with given fields: server, obj
func (_m *LiveStateCache) IsNamespaced(server string, obj *unstructured.Unstructured) (bool, error) {
ret := _m.Called(server, obj)
var r0 bool
if rf, ok := ret.Get(0).(func(string, schema.GroupKind) bool); ok {
r0 = rf(server, gk)
if rf, ok := ret.Get(0).(func(string, *unstructured.Unstructured) bool); ok {
r0 = rf(server, obj)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, schema.GroupKind) error); ok {
r1 = rf(server, gk)
if rf, ok := ret.Get(1).(func(string, *unstructured.Unstructured) error); ok {
r1 = rf(server, obj)
} else {
r1 = ret.Error(1)
}
@@ -176,11 +77,11 @@ func (_m *LiveStateCache) IsNamespaced(server string, gk schema.GroupKind) (bool
}
// IterateHierarchy provides a mock function with given fields: server, key, action
func (_m *LiveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(v1alpha1.ResourceNode, string)) error {
func (_m *LiveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(v1alpha1.ResourceNode)) error {
ret := _m.Called(server, key, action)
var r0 error
if rf, ok := ret.Get(0).(func(string, kube.ResourceKey, func(v1alpha1.ResourceNode, string)) error); ok {
if rf, ok := ret.Get(0).(func(string, kube.ResourceKey, func(v1alpha1.ResourceNode)) error); ok {
r0 = rf(server, key, action)
} else {
r0 = ret.Error(0)
@@ -190,15 +91,6 @@ func (_m *LiveStateCache) IterateHierarchy(server string, key kube.ResourceKey,
}
// Run provides a mock function with given fields: ctx
func (_m *LiveStateCache) Run(ctx context.Context) error {
ret := _m.Called(ctx)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
r0 = rf(ctx)
} else {
r0 = ret.Error(0)
}
return r0
func (_m *LiveStateCache) Run(ctx context.Context) {
_m.Called(ctx)
}

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

@@ -0,0 +1,134 @@
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 _, ownerRef := range child.ownerRefs {
ownerGvk := schema.FromAPIVersionAndKind(ownerRef.APIVersion, ownerRef.Kind)
if kube.NewResourceKey(ownerGvk.Group, ownerRef.Kind, n.ref.Namespace, ownerRef.Name) == n.resourceKey() {
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}
}
return appv1.ResourceNode{
ResourceRef: appv1.ResourceRef{
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)) {
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.iterateChildren(ns, newResourceKeySet(parents, n.resourceKey()), action)
}
}
}
}

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

@@ -0,0 +1,29 @@
package cache
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/argoproj/argo-cd/util/settings"
)
var c = &clusterInfo{settings: &settings.ArgoCDSettings{}}
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 TestIsParentOfSameKindDifferentGroup(t *testing.T) {
rs := testRS.DeepCopy()
rs.SetAPIVersion("somecrd.io/v1")
child := c.createObjInfo(testPod, "")
invalidParent := c.createObjInfo(rs, "")
assert.False(t, invalidParent.isParentOf(child))
}

View File

@@ -1,116 +0,0 @@
package controller
import (
"context"
"time"
"github.com/argoproj/gitops-engine/pkg/cache"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"github.com/argoproj/argo-cd/controller/metrics"
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/pkg/client/listers/application/v1alpha1"
"github.com/argoproj/argo-cd/util/argo"
appstatecache "github.com/argoproj/argo-cd/util/cache/appstate"
"github.com/argoproj/argo-cd/util/db"
)
const (
secretUpdateInterval = 10 * time.Second
)
type clusterInfoUpdater struct {
infoSource metrics.HasClustersInfo
db db.ArgoDB
appLister v1alpha1.ApplicationNamespaceLister
cache *appstatecache.Cache
}
func NewClusterInfoUpdater(
infoSource metrics.HasClustersInfo,
db db.ArgoDB,
appLister v1alpha1.ApplicationNamespaceLister,
cache *appstatecache.Cache) *clusterInfoUpdater {
return &clusterInfoUpdater{infoSource, db, appLister, cache}
}
func (c *clusterInfoUpdater) Run(ctx context.Context) {
c.updateClusters()
ticker := time.NewTicker(secretUpdateInterval)
for {
select {
case <-ctx.Done():
ticker.Stop()
break
case <-ticker.C:
c.updateClusters()
}
}
}
func (c *clusterInfoUpdater) updateClusters() {
infoByServer := make(map[string]*cache.ClusterInfo)
clustersInfo := c.infoSource.GetClustersInfo()
for i := range clustersInfo {
info := clustersInfo[i]
infoByServer[info.Server] = &info
}
clusters, err := c.db.ListClusters(context.Background())
if err != nil {
log.Warnf("Failed to save clusters info: %v", err)
}
_ = kube.RunAllAsync(len(clusters.Items), func(i int) error {
cluster := clusters.Items[i]
if err := c.updateClusterInfo(cluster, infoByServer[cluster.Server]); err != nil {
log.Warnf("Failed to save clusters info: %v", err)
}
return nil
})
}
func (c *clusterInfoUpdater) updateClusterInfo(cluster appv1.Cluster, info *cache.ClusterInfo) error {
apps, err := c.appLister.List(labels.Everything())
if err != nil {
return err
}
var appCount int64
for _, a := range apps {
if err := argo.ValidateDestination(context.Background(), &a.Spec.Destination, c.db); err != nil {
continue
}
if a.Spec.Destination.Server == cluster.Server {
appCount += 1
}
}
now := metav1.Now()
clusterInfo := appv1.ClusterInfo{
ConnectionState: appv1.ConnectionState{ModifiedAt: &now},
ApplicationsCount: appCount,
}
if info != nil {
clusterInfo.ServerVersion = info.K8SVersion
if info.LastCacheSyncTime == nil {
clusterInfo.ConnectionState.Status = appv1.ConnectionStatusUnknown
} else if info.SyncError == nil {
clusterInfo.ConnectionState.Status = appv1.ConnectionStatusSuccessful
syncTime := metav1.NewTime(*info.LastCacheSyncTime)
clusterInfo.CacheInfo.LastCacheSyncTime = &syncTime
clusterInfo.CacheInfo.APIsCount = int64(info.APIsCount)
clusterInfo.CacheInfo.ResourcesCount = int64(info.ResourcesCount)
} else {
clusterInfo.ConnectionState.Status = appv1.ConnectionStatusFailed
clusterInfo.ConnectionState.Message = info.SyncError.Error()
}
} else {
clusterInfo.ConnectionState.Status = appv1.ConnectionStatusUnknown
if appCount == 0 {
clusterInfo.ConnectionState.Message = "Cluster has no application and not being monitored."
}
}
return c.cache.SetClusterInfo(cluster.Server, &clusterInfo)
}

View File

@@ -1,72 +0,0 @@
package controller
import (
"context"
"fmt"
"testing"
"time"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
appsfake "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/fake"
appinformers "github.com/argoproj/argo-cd/pkg/client/informers/externalversions/application/v1alpha1"
applisters "github.com/argoproj/argo-cd/pkg/client/listers/application/v1alpha1"
cacheutil "github.com/argoproj/argo-cd/util/cache"
"github.com/argoproj/argo-cd/util/cache/appstate"
"github.com/argoproj/argo-cd/util/db"
"github.com/argoproj/argo-cd/util/settings"
clustercache "github.com/argoproj/gitops-engine/pkg/cache"
"github.com/stretchr/testify/assert"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/tools/cache"
)
// Expect cluster cache update is persisted in cluster secret
func TestClusterSecretUpdater(t *testing.T) {
const fakeNamespace = "fake-ns"
const updatedK8sVersion = "1.0"
now := time.Now()
var tests = []struct {
LastCacheSyncTime *time.Time
SyncError error
ExpectedStatus v1alpha1.ConnectionStatus
}{
{nil, nil, v1alpha1.ConnectionStatusUnknown},
{&now, nil, v1alpha1.ConnectionStatusSuccessful},
{&now, fmt.Errorf("sync failed"), v1alpha1.ConnectionStatusFailed},
}
kubeclientset := fake.NewSimpleClientset()
appclientset := appsfake.NewSimpleClientset()
appInfomer := appinformers.NewApplicationInformer(appclientset, "", time.Minute, cache.Indexers{})
settingsManager := settings.NewSettingsManager(context.Background(), kubeclientset, fakeNamespace)
argoDB := db.NewDB(fakeNamespace, settingsManager, kubeclientset)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
appCache := appstate.NewCache(cacheutil.NewCache(cacheutil.NewInMemoryCache(time.Minute)), time.Minute)
cluster, err := argoDB.CreateCluster(ctx, &v1alpha1.Cluster{Server: "http://minikube"})
assert.NoError(t, err, "Test prepare test data create cluster failed")
for _, test := range tests {
info := &clustercache.ClusterInfo{
Server: cluster.Server,
K8SVersion: updatedK8sVersion,
LastCacheSyncTime: test.LastCacheSyncTime,
SyncError: test.SyncError,
}
lister := applisters.NewApplicationLister(appInfomer.GetIndexer()).Applications(fakeNamespace)
updater := NewClusterInfoUpdater(nil, argoDB, lister, appCache)
err = updater.updateClusterInfo(*cluster, info)
assert.NoError(t, err, "Invoking updateClusterInfo failed.")
var clusterInfo v1alpha1.ClusterInfo
err = appCache.GetClusterInfo(cluster.Server, &clusterInfo)
assert.NoError(t, err)
assert.Equal(t, updatedK8sVersion, clusterInfo.ServerVersion)
assert.Equal(t, test.ExpectedStatus, clusterInfo.ConnectionState.Status)
}
}

View File

@@ -1,95 +0,0 @@
package metrics
import (
"context"
"sync"
"time"
"github.com/argoproj/gitops-engine/pkg/cache"
"github.com/prometheus/client_golang/prometheus"
)
const (
metricsCollectionInterval = 30 * time.Second
)
var (
descClusterDefaultLabels = []string{"server"}
descClusterInfo = prometheus.NewDesc(
"argocd_cluster_info",
"Information about cluster.",
append(descClusterDefaultLabels, "k8s_version"),
nil,
)
descClusterCacheResources = prometheus.NewDesc(
"argocd_cluster_api_resource_objects",
"Number of k8s resource objects in the cache.",
descClusterDefaultLabels,
nil,
)
descClusterAPIs = prometheus.NewDesc(
"argocd_cluster_api_resources",
"Number of monitored kubernetes API resources.",
descClusterDefaultLabels,
nil,
)
descClusterCacheAgeSeconds = prometheus.NewDesc(
"argocd_cluster_cache_age_seconds",
"Cluster cache age in seconds.",
descClusterDefaultLabels,
nil,
)
)
type HasClustersInfo interface {
GetClustersInfo() []cache.ClusterInfo
}
type clusterCollector struct {
infoSource HasClustersInfo
info []cache.ClusterInfo
lock sync.Mutex
}
func (c *clusterCollector) Run(ctx context.Context) {
// FIXME: complains about SA1015
// nolint:staticcheck
tick := time.Tick(metricsCollectionInterval)
for {
select {
case <-ctx.Done():
break
case <-tick:
info := c.infoSource.GetClustersInfo()
c.lock.Lock()
c.info = info
c.lock.Unlock()
}
}
}
// Describe implements the prometheus.Collector interface
func (c *clusterCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- descClusterInfo
ch <- descClusterCacheResources
ch <- descClusterAPIs
ch <- descClusterCacheAgeSeconds
}
func (c *clusterCollector) Collect(ch chan<- prometheus.Metric) {
now := time.Now()
for _, c := range c.info {
defaultValues := []string{c.Server}
ch <- prometheus.MustNewConstMetric(descClusterInfo, prometheus.GaugeValue, 1, append(defaultValues, c.K8SVersion)...)
ch <- prometheus.MustNewConstMetric(descClusterCacheResources, prometheus.GaugeValue, float64(c.ResourcesCount), defaultValues...)
ch <- prometheus.MustNewConstMetric(descClusterAPIs, prometheus.GaugeValue, float64(c.APIsCount), defaultValues...)
cacheAgeSeconds := -1
if c.LastCacheSyncTime != nil {
cacheAgeSeconds = int(now.Sub(*c.LastCacheSyncTime).Seconds())
}
ch <- prometheus.MustNewConstMetric(descClusterCacheAgeSeconds, prometheus.GaugeValue, float64(cacheAgeSeconds), defaultValues...)
}
}

View File

@@ -1,13 +1,10 @@
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"
@@ -16,27 +13,18 @@ import (
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
applister "github.com/argoproj/argo-cd/pkg/client/listers/application/v1alpha1"
"github.com/argoproj/argo-cd/util/git"
"github.com/argoproj/argo-cd/util/healthz"
)
type MetricsServer struct {
*http.Server
syncCounter *prometheus.CounterVec
kubectlExecCounter *prometheus.CounterVec
kubectlExecPendingGauge *prometheus.GaugeVec
k8sRequestCounter *prometheus.CounterVec
clusterEventsCounter *prometheus.CounterVec
redisRequestCounter *prometheus.CounterVec
reconcileHistogram *prometheus.HistogramVec
redisRequestHistogram *prometheus.HistogramVec
registry *prometheus.Registry
syncCounter *prometheus.CounterVec
k8sRequestCounter *prometheus.CounterVec
reconcileHistogram *prometheus.HistogramVec
}
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,185 +35,93 @@ 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(
// NewMetricsServer returns a new prometheus server which collects application metrics
func NewMetricsServer(addr string, appLister applister.ApplicationLister) *MetricsServer {
mux := http.NewServeMux()
appRegistry := NewAppRegistry(appLister)
appRegistry.MustRegister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}))
appRegistry.MustRegister(prometheus.NewGoCollector())
mux.Handle(MetricsPath, promhttp.HandlerFor(appRegistry, promhttp.HandlerOpts{}))
syncCounter := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "argocd_app_sync_total",
Help: "Number of application syncs.",
},
append(descAppDefaultLabels, "dest_server", "phase"),
append(descAppDefaultLabels, "phase"),
)
k8sRequestCounter = prometheus.NewCounterVec(
appRegistry.MustRegister(syncCounter)
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"),
append(descAppDefaultLabels, "response_code"),
)
appRegistry.MustRegister(k8sRequestCounter)
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(
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"},
append(descAppDefaultLabels),
)
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
func NewMetricsServer(addr string, appLister applister.ApplicationLister, healthCheck func() error) *MetricsServer {
mux := http.NewServeMux()
registry := NewAppRegistry(appLister)
mux.Handle(MetricsPath, promhttp.HandlerFor(prometheus.Gatherers{
// contains app controller specific metrics
registry,
// contains process, golang and controller workqueues metrics
prometheus.DefaultGatherer,
}, promhttp.HandlerOpts{}))
healthz.ServeHealthCheck(mux, healthCheck)
registry.MustRegister(syncCounter)
registry.MustRegister(k8sRequestCounter)
registry.MustRegister(kubectlExecCounter)
registry.MustRegister(kubectlExecPendingGauge)
registry.MustRegister(reconcileHistogram)
registry.MustRegister(clusterEventsCounter)
registry.MustRegister(redisRequestCounter)
registry.MustRegister(redisRequestHistogram)
appRegistry.MustRegister(reconcileHistogram)
return &MetricsServer{
registry: registry,
Server: &http.Server{
Addr: addr,
Handler: mux,
},
syncCounter: syncCounter,
k8sRequestCounter: k8sRequestCounter,
kubectlExecCounter: kubectlExecCounter,
kubectlExecPendingGauge: kubectlExecPendingGauge,
reconcileHistogram: reconcileHistogram,
clusterEventsCounter: clusterEventsCounter,
redisRequestCounter: redisRequestCounter,
redisRequestHistogram: redisRequestHistogram,
syncCounter: syncCounter,
k8sRequestCounter: k8sRequestCounter,
reconcileHistogram: reconcileHistogram,
}
}
func (m *MetricsServer) RegisterClustersInfoSource(ctx context.Context, source HasClustersInfo) {
collector := &clusterCollector{infoSource: source}
go collector.Run(ctx)
m.registry.MustRegister(collector)
}
// IncSync increments the sync counter for an application
func (m *MetricsServer) IncSync(app *argoappv1.Application, state *argoappv1.OperationState) {
if !state.Phase.Completed() {
return
}
m.syncCounter.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject(), app.Spec.Destination.Server, string(state.Phase)).Inc()
}
func (m *MetricsServer) IncKubectlExec(command string) {
m.kubectlExecCounter.WithLabelValues(command).Inc()
}
func (m *MetricsServer) IncKubectlExecPending(command string) {
m.kubectlExecPendingGauge.WithLabelValues(command).Inc()
}
func (m *MetricsServer) DecKubectlExecPending(command string) {
m.kubectlExecPendingGauge.WithLabelValues(command).Dec()
}
// IncClusterEventsCount increments the number of cluster events
func (m *MetricsServer) IncClusterEventsCount(server, group, kind string) {
m.clusterEventsCounter.WithLabelValues(server, group, kind).Inc()
m.syncCounter.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject(), string(state.Phase)).Inc()
}
// IncKubernetesRequest increments the kubernetes requests counter for an application
func (m *MetricsServer) IncKubernetesRequest(app *argoappv1.Application, 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 +145,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 +179,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,42 @@ status:
status: Healthy
`
var noOpHealthCheck = func() error {
return nil
}
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
`
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,10 +130,10 @@ 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)
metricsServ := NewMetricsServer("localhost:8082", appLister)
req, err := http.NewRequest("GET", "/metrics", nil)
assert.NoError(t, err)
rr := httptest.NewRecorder()
@@ -158,81 +145,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
`
metricsServ := NewMetricsServer("localhost:8082", appLister)
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 +198,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
`
metricsServ := NewMetricsServer("localhost:8082", appLister)
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

@@ -1,40 +0,0 @@
package controller
import (
"sort"
"github.com/argoproj/gitops-engine/pkg/sync/syncwaves"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
type syncWaveSorter []*unstructured.Unstructured
func (s syncWaveSorter) Len() int {
return len(s)
}
func (s syncWaveSorter) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s syncWaveSorter) Less(i, j int) bool {
return syncwaves.Wave(s[i]) < syncwaves.Wave(s[j])
}
func FilterObjectsForDeletion(objs []*unstructured.Unstructured) []*unstructured.Unstructured {
if len(objs) <= 1 {
return objs
}
sort.Sort(sort.Reverse(syncWaveSorter(objs)))
currentSyncWave := syncwaves.Wave(objs[0])
filteredObjs := make([]*unstructured.Unstructured, 0)
for _, obj := range objs {
if syncwaves.Wave(obj) != currentSyncWave {
break
}
filteredObjs = append(filteredObjs, obj)
}
return filteredObjs
}

View File

@@ -1,41 +0,0 @@
package controller
import (
"reflect"
"testing"
"github.com/argoproj/gitops-engine/pkg/sync/common"
. "github.com/argoproj/gitops-engine/pkg/utils/testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestFilterObjectsForDeletion(t *testing.T) {
tests := []struct {
input []string
want []string
}{
{[]string{"1", "5", "7", "7", "4"}, []string{"7", "7"}},
{[]string{"1", "5", "2", "2", "4"}, []string{"5"}},
{[]string{"1"}, []string{"1"}},
{[]string{}, []string{}},
}
for _, tt := range tests {
in := sliceOfObjectsWithSyncWaves(tt.input)
need := sliceOfObjectsWithSyncWaves(tt.want)
if got := FilterObjectsForDeletion(in); !reflect.DeepEqual(got, need) {
t.Errorf("Received unexpected objects for deletion = %v, want %v", got, need)
}
}
}
func podWithSyncWave(wave string) *unstructured.Unstructured {
return Annotate(NewPod(), common.AnnotationSyncWave, wave)
}
func sliceOfObjectsWithSyncWaves(waves []string) []*unstructured.Unstructured {
objects := make([]*unstructured.Unstructured, 0)
for _, wave := range waves {
objects = append(objects, podWithSyncWave(wave))
}
return objects
}

View File

@@ -6,18 +6,9 @@ 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"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/cache"
@@ -27,22 +18,18 @@ import (
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
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/reposerver"
"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/db"
"github.com/argoproj/argo-cd/util/gpg"
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/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
@@ -55,108 +42,76 @@ type managedResource struct {
Hook bool
}
func GetLiveObjsForApplicationHealth(resources []managedResource, statuses []appv1.ResourceStatus) ([]*appv1.ResourceStatus, []*unstructured.Unstructured) {
liveObjs := make([]*unstructured.Unstructured, 0)
resStatuses := make([]*appv1.ResourceStatus, 0)
for i, resource := range resources {
if resource.Target != nil && hookutil.Skip(resource.Target) {
continue
}
liveObjs = append(liveObjs, resource.Live)
resStatuses = append(resStatuses, &statuses[i])
func GetLiveObjs(res []managedResource) []*unstructured.Unstructured {
objs := make([]*unstructured.Unstructured, len(res))
for i := range res {
objs[i] = res[i].Live
}
return resStatuses, liveObjs
return objs
}
type ResourceInfoProvider interface {
IsNamespaced(server string, obj *unstructured.Unstructured) (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
CompareAppState(app *v1alpha1.Application, revision string, source v1alpha1.ApplicationSource, noCache bool) (*comparisonResult, error)
SyncAppState(app *v1alpha1.Application, state *v1alpha1.OperationState)
}
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
}
func (res *comparisonResult) GetSyncStatus() *v1alpha1.SyncStatus {
return res.syncStatus
}
func (res *comparisonResult) GetHealthStatus() *v1alpha1.HealthStatus {
return res.healthStatus
reconciledAt metav1.Time
syncStatus *v1alpha1.SyncStatus
healthStatus *v1alpha1.HealthStatus
resources []v1alpha1.ResourceStatus
managedResources []managedResource
conditions []v1alpha1.ApplicationCondition
hooks []*unstructured.Unstructured
diffNormalizer diff.Normalizer
appSourceType v1alpha1.ApplicationSourceType
}
// appStateManager allows to compare applications to git
type appStateManager struct {
metricsServer *metrics.MetricsServer
db db.ArgoDB
settingsMgr *settings.SettingsManager
settings *settings.ArgoCDSettings
appclientset appclientset.Interface
projInformer cache.SharedIndexInformer
kubectl kubeutil.Kubectl
repoClientset apiclient.Clientset
repoClientset reposerver.Clientset
liveStateCache statecache.LiveStateCache
namespace string
}
func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1.ApplicationSource, appLabelKey, revision string, noCache, verifySignature bool) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error) {
ts := stats.NewTimingStats()
helmRepos, err := m.db.ListHelmRepositories(context.Background())
func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1.ApplicationSource, appLabelKey, revision string, noCache bool) ([]*unstructured.Unstructured, []*unstructured.Unstructured, *repository.ManifestResponse, error) {
helmRepos, err := m.db.ListHelmRepos(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
}
plugins, err := m.settingsMgr.GetConfigManagementPlugins()
if err != nil {
return nil, nil, err
}
ts.AddCheckpoint("plugins_ms")
tools := make([]*appv1.ConfigManagementPlugin, len(plugins))
for i := range plugins {
tools[i] = &plugins[i]
tools := make([]*appv1.ConfigManagementPlugin, len(m.settings.ConfigManagementPlugins))
for i := range m.settings.ConfigManagementPlugins {
tools[i] = &m.settings.ConfigManagementPlugins[i]
}
kustomizeSettings, err := m.settingsMgr.GetKustomizeSettings()
if err != nil {
return nil, nil, err
}
kustomizeOptions, err := kustomizeSettings.GetOptions(app.Spec.Source)
if err != nil {
return nil, nil, err
}
ts.AddCheckpoint("build_options_ms")
serverVersion, apiGroups, err := m.liveStateCache.GetVersionsInfo(app.Spec.Destination.Server)
if err != nil {
return nil, nil, err
}
ts.AddCheckpoint("version_ms")
manifestInfo, err := repoClient.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
manifestInfo, err := repoClient.GenerateManifest(context.Background(), &repository.ManifestRequest{
Repo: repo,
Repos: helmRepos,
HelmRepos: helmRepos,
Revision: revision,
NoCache: noCache,
AppLabelKey: appLabelKey,
@@ -164,75 +119,56 @@ 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),
VerifySignature: verifySignature,
})
if err != nil {
return nil, nil, err
}
targetObjs, 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
}
func unmarshalManifests(manifests []string) ([]*unstructured.Unstructured, error) {
targetObjs := make([]*unstructured.Unstructured, 0)
for _, manifest := range manifests {
hooks := make([]*unstructured.Unstructured, 0)
for _, manifest := range manifestInfo.Manifests {
obj, err := v1alpha1.UnmarshalToUnstructured(manifest)
if err != nil {
return nil, err
return nil, nil, nil, err
}
if hookutil.IsHook(obj) {
hooks = append(hooks, obj)
} else {
targetObjs = append(targetObjs, obj)
}
targetObjs = append(targetObjs, obj)
}
return targetObjs, nil
return targetObjs, hooks, manifestInfo, 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)
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)
result := make([]*unstructured.Unstructured, 0)
for key, targets := range targetByKey {
if len(targets) > 1 {
now := metav1.Now()
conditions = append(conditions, appv1.ApplicationCondition{
Type: appv1.ApplicationConditionRepeatedResourceWarning,
Message: fmt.Sprintf("Resource %s appeared %d times among application resources.", key.String(), len(targets)),
LastTransitionTime: &now,
Type: appv1.ApplicationConditionRepeatedResourceWarning,
Message: fmt.Sprintf("Resource %s appeared %d times among application resources.", key.String(), len(targets)),
})
}
result = append(result, targets[len(targets)-1])
@@ -241,212 +177,89 @@ func DeduplicateTargetObjects(
return result, conditions, nil
}
func (m *appStateManager) getComparisonSettings(app *appv1.Application) (string, map[string]v1alpha1.ResourceOverride, diff.Normalizer, *settings.ResourcesFilter, error) {
resourceOverrides, err := m.settingsMgr.GetResourceOverrides()
if err != nil {
return "", nil, nil, nil, err
}
appLabelKey, err := m.settingsMgr.GetAppInstanceLabelKey()
if err != nil {
return "", nil, nil, nil, err
}
diffNormalizer, err := argo.NewDiffNormalizer(app.Spec.IgnoreDifferences, resourceOverrides)
if err != nil {
return "", nil, nil, nil, err
}
resFilter, err := m.settingsMgr.GetResourcesFilter()
if err != nil {
return "", nil, nil, nil, err
}
return appLabelKey, resourceOverrides, diffNormalizer, resFilter, nil
}
// verifyGnuPGSignature verifies the result of a GnuPG operation for a given git
// revision.
func verifyGnuPGSignature(revision string, project *appv1.AppProject, manifestInfo *apiclient.ManifestResponse) []appv1.ApplicationCondition {
now := metav1.Now()
conditions := make([]appv1.ApplicationCondition, 0)
// We need to have some data in the verificatin result to parse, otherwise there was no signature
if manifestInfo.VerifyResult != "" {
verifyResult, err := gpg.ParseGitCommitVerification(manifestInfo.VerifyResult)
if err != nil {
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
log.Errorf("Error while verifying git commit for revision %s: %s", revision, err.Error())
} else {
switch verifyResult.Result {
case gpg.VerifyResultGood:
// This is the only case we allow to sync to, but we need to make sure signing key is allowed
validKey := false
for _, k := range project.Spec.SignatureKeys {
if gpg.KeyID(k.KeyID) == gpg.KeyID(verifyResult.KeyID) && gpg.KeyID(k.KeyID) != "" {
validKey = true
break
}
}
if !validKey {
msg := fmt.Sprintf("Found good signature made with %s key %s, but this key is not allowed in AppProject",
verifyResult.Cipher, verifyResult.KeyID)
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: msg, LastTransitionTime: &now})
}
case gpg.VerifyResultInvalid:
msg := fmt.Sprintf("Found signature made with %s key %s, but verification result was invalid: '%s'",
verifyResult.Cipher, verifyResult.KeyID, verifyResult.Message)
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: msg, LastTransitionTime: &now})
default:
msg := fmt.Sprintf("Could not verify commit signature on revision '%s', check logs for more information.", revision)
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: msg, LastTransitionTime: &now})
}
}
} else {
msg := fmt.Sprintf("Target revision %s in Git is not signed, but a signature is required", revision)
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: msg, LastTransitionTime: &now})
}
return conditions
}
// CompareAppState compares application git state to the live app state, using the specified
// revision and supplied source. If revision or overrides are empty, then compares against
// revision and overrides in the app spec.
func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *appv1.AppProject, revision string, source v1alpha1.ApplicationSource, noCache bool, localManifests []string) *comparisonResult {
ts := stats.NewTimingStats()
appLabelKey, resourceOverrides, diffNormalizer, resFilter, err := m.getComparisonSettings(app)
ts.AddCheckpoint("settings_ms")
// return unknown comparison result if basic comparison settings cannot be loaded
func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision string, source v1alpha1.ApplicationSource, noCache bool) (*comparisonResult, error) {
diffNormalizer, err := argo.NewDiffNormalizer(app.Spec.IgnoreDifferences, m.settings.ResourceOverrides)
if err != nil {
return &comparisonResult{
syncStatus: &v1alpha1.SyncStatus{
ComparedTo: appv1.ComparedTo{Source: source, Destination: app.Spec.Destination},
Status: appv1.SyncStatusCodeUnknown,
},
healthStatus: &appv1.HealthStatus{Status: health.HealthStatusUnknown},
}
return nil, err
}
// When signature keys are defined in the project spec, we need to verify the signature on the Git revision
verifySignature := false
if project.Spec.SignatureKeys != nil && len(project.Spec.SignatureKeys) > 0 && gpg.IsGPGEnabled() {
verifySignature = true
}
// do best effort loading live and target state to present as much information about app state as possible
failedToLoadObjs := false
conditions := make([]v1alpha1.ApplicationCondition, 0)
logCtx := log.WithField("application", app.Name)
logCtx.Infof("Comparing app state (cluster: %s, namespace: %s)", app.Spec.Destination.Server, app.Spec.Destination.Namespace)
var targetObjs []*unstructured.Unstructured
var manifestInfo *apiclient.ManifestResponse
now := metav1.Now()
if len(localManifests) == 0 {
targetObjs, manifestInfo, err = m.getRepoObjs(app, source, appLabelKey, revision, noCache, verifySignature)
if err != nil {
targetObjs = make([]*unstructured.Unstructured, 0)
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
failedToLoadObjs = true
}
} else {
// Prevent applying local manifests for now when signature verification is enabled
// This is also enforced on API level, but as a last resort, we also enforce it here
if gpg.IsGPGEnabled() && verifySignature {
msg := "Cannot use local manifests when signature verification is required"
targetObjs = make([]*unstructured.Unstructured, 0)
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: msg, LastTransitionTime: &now})
failedToLoadObjs = true
} else {
targetObjs, err = unmarshalManifests(localManifests)
if err != nil {
targetObjs = make([]*unstructured.Unstructured, 0)
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
failedToLoadObjs = true
}
}
manifestInfo = nil
}
ts.AddCheckpoint("git_ms")
var infoProvider kubeutil.ResourceInfoProvider
infoProvider, err = m.liveStateCache.GetClusterCache(app.Spec.Destination.Server)
observedAt := metav1.Now()
failedToLoadObjs := false
conditions := make([]v1alpha1.ApplicationCondition, 0)
appLabelKey := m.settings.GetAppInstanceLabelKey()
targetObjs, hooks, manifestInfo, err := m.getRepoObjs(app, source, appLabelKey, revision, noCache)
if err != nil {
infoProvider = &resourceInfoProviderStub{}
targetObjs = make([]*unstructured.Unstructured, 0)
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
failedToLoadObjs = true
}
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, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
}
conditions = append(conditions, dedupConditions...)
for i := len(targetObjs) - 1; i >= 0; i-- {
targetObj := targetObjs[i]
gvk := targetObj.GroupVersionKind()
if resFilter.IsExcludedResource(gvk.Group, gvk.Kind, app.Spec.Destination.Server) {
targetObjs = append(targetObjs[:i], targetObjs[i+1:]...)
conditions = append(conditions, v1alpha1.ApplicationCondition{
Type: v1alpha1.ApplicationConditionExcludedResourceWarning,
Message: fmt.Sprintf("Resource %s/%s %s is excluded in the settings", gvk.Group, gvk.Kind, targetObj.GetName()),
LastTransitionTime: &now,
})
}
}
ts.AddCheckpoint("dedup_ms")
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})
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
failedToLoadObjs = true
}
logCtx.Debugf("Retrieved lived manifests")
// filter out all resources which are not permitted in the application project
for k, v := range liveObjByKey {
if !project.IsLiveResourcePermitted(v, app.Spec.Destination.Server) {
delete(liveObjByKey, k)
}
}
for _, liveObj := range liveObjByKey {
if liveObj != nil {
appInstanceName := kubeutil.GetAppInstanceLabel(liveObj, appLabelKey)
if appInstanceName != "" && appInstanceName != app.Name {
conditions = append(conditions, v1alpha1.ApplicationCondition{
Type: v1alpha1.ApplicationConditionSharedResourceWarning,
Message: fmt.Sprintf("%s/%s is part of a different application: %s", liveObj.GetKind(), liveObj.GetName(), appInstanceName),
LastTransitionTime: &now,
Type: v1alpha1.ApplicationConditionSharedResourceWarning,
Message: fmt.Sprintf("%s/%s is part of a different application: %s", liveObj.GetKind(), liveObj.GetName(), appInstanceName),
})
}
}
}
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); 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")
// Do the actual comparison
diffResults, err := diff.DiffArray(reconciliation.Target, reconciliation.Live, diffNormalizer, compareOptions)
if err != nil {
diffResults = &diff.DiffResultList{}
failedToLoadObjs = true
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
// 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)
}
// Do the actual comparison
diffResults, err := diff.DiffArray(targetObjs, managedLiveObj, diffNormalizer)
if err != nil {
return nil, err
}
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]
obj := liveObj
managedResources := make([]managedResource, len(targetObjs))
resourceSummaries := make([]v1alpha1.ResourceStatus, len(targetObjs))
for i := 0; i < len(targetObjs); i++ {
obj := managedLiveObj[i]
if obj == nil {
obj = targetObj
obj = targetObjs[i]
}
if obj == nil {
continue
@@ -454,59 +267,35 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
gvk := obj.GroupVersionKind()
resState := v1alpha1.ResourceStatus{
Namespace: obj.GetNamespace(),
Name: obj.GetName(),
Kind: gvk.Kind,
Version: gvk.Version,
Group: gvk.Group,
Hook: hookutil.IsHook(obj),
RequiresPruning: targetObj == nil && liveObj != nil,
Namespace: obj.GetNamespace(),
Name: obj.GetName(),
Kind: gvk.Kind,
Version: gvk.Version,
Group: gvk.Group,
Hook: hookutil.IsHook(obj),
}
var diffResult diff.DiffResult
if i < len(diffResults.Diffs) {
diffResult = diffResults.Diffs[i]
} else {
diffResult = diff.DiffResult{Modified: false, NormalizedLive: []byte("{}"), PredictedLive: []byte("{}")}
}
if resState.Hook || ignore.Ignore(obj) || (targetObj != nil && hookutil.Skip(targetObj)) {
// For resource hooks or skipped resources, don't store sync status, and do not affect overall sync status
} else if diffResult.Modified || targetObj == nil || liveObj == nil {
diffResult := diffResults.Diffs[i]
if resState.Hook {
// For resource hooks, don't store sync status, and do not affect overall sync status
} else if diffResult.Modified || targetObjs[i] == nil || managedLiveObj[i] == nil {
// Set resource state to OutOfSync since one of the following is true:
// * target and live resource are different
// * target resource not defined and live resource is extra
// * target resource present but live resource is missing
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")) {
syncCode = v1alpha1.SyncStatusCodeOutOfSync
}
syncCode = v1alpha1.SyncStatusCodeOutOfSync
} else {
resState.Status = v1alpha1.SyncStatusCodeSynced
}
// set unknown status to all resource that are not permitted in the app project
isNamespaced, err := m.liveStateCache.IsNamespaced(app.Spec.Destination.Server, gvk.GroupKind())
if !project.IsGroupKindPermitted(gvk.GroupKind(), isNamespaced && err == nil) {
resState.Status = v1alpha1.SyncStatusCodeUnknown
}
if isNamespaced && obj.GetNamespace() == "" {
conditions = append(conditions, appv1.ApplicationCondition{Type: v1alpha1.ApplicationConditionInvalidSpecError, Message: fmt.Sprintf("Namespace for %s %s is missing.", obj.GetName(), gvk.String()), LastTransitionTime: &now})
}
// we can't say anything about the status if we were unable to get the target objects
if failedToLoadObjs {
resState.Status = v1alpha1.SyncStatusCodeUnknown
}
managedResources[i] = managedResource{
Name: resState.Name,
Namespace: resState.Namespace,
Group: resState.Group,
Kind: resState.Kind,
Version: resState.Version,
Live: liveObj,
Target: targetObj,
Live: managedLiveObj[i],
Target: targetObjs[i],
Diff: diffResult,
Hook: resState.Hook,
}
@@ -526,81 +315,67 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *ap
if manifestInfo != nil {
syncStatus.Revision = manifestInfo.Revision
}
ts.AddCheckpoint("sync_ms")
resSumForAppHealth, liveObjsForAppHealth := GetLiveObjsForApplicationHealth(managedResources, resourceSummaries)
healthStatus, err := argohealth.SetApplicationHealth(resSumForAppHealth, liveObjsForAppHealth, resourceOverrides, func(obj *unstructured.Unstructured) bool {
healthStatus, err := health.SetApplicationHealth(resourceSummaries, GetLiveObjs(managedResources), m.settings.ResourceOverrides, func(obj *unstructured.Unstructured) bool {
return !isSelfReferencedApp(app, kubeutil.GetObjectRef(obj))
})
if err != nil {
conditions = append(conditions, appv1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error(), LastTransitionTime: &now})
}
// Git has already performed the signature verification via its GPG interface, and the result is available
// in the manifest info received from the repository server. We now need to form our oppinion about the result
// and stop processing if we do not agree about the outcome.
if gpg.IsGPGEnabled() && verifySignature && manifestInfo != nil {
conditions = append(conditions, verifyGnuPGSignature(revision, project, manifestInfo)...)
conditions = append(conditions, appv1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
}
compRes := comparisonResult{
syncStatus: &syncStatus,
healthStatus: healthStatus,
resources: resourceSummaries,
managedResources: managedResources,
reconciliationResult: reconciliation,
diffNormalizer: diffNormalizer,
reconciledAt: observedAt,
syncStatus: &syncStatus,
healthStatus: healthStatus,
resources: resourceSummaries,
managedResources: managedResources,
conditions: conditions,
hooks: hooks,
diffNormalizer: diffNormalizer,
}
if manifestInfo != nil {
compRes.appSourceType = v1alpha1.ApplicationSourceType(manifestInfo.SourceType)
}
app.Status.SetConditions(conditions, map[appv1.ApplicationConditionType]bool{
appv1.ApplicationConditionComparisonError: true,
appv1.ApplicationConditionSharedResourceWarning: true,
appv1.ApplicationConditionRepeatedResourceWarning: true,
appv1.ApplicationConditionExcludedResourceWarning: true,
})
ts.AddCheckpoint("health_ms")
compRes.timings = ts.Timings()
return &compRes
return &compRes, nil
}
func (m *appStateManager) persistRevisionHistory(app *v1alpha1.Application, revision string, source v1alpha1.ApplicationSource, startedAt metav1.Time) error {
func (m *appStateManager) persistRevisionHistory(app *v1alpha1.Application, revision string, source v1alpha1.ApplicationSource) error {
var nextID int64
if len(app.Status.History) > 0 {
nextID = app.Status.History.LastRevisionHistory().ID + 1
nextID = app.Status.History[len(app.Status.History)-1].ID + 1
}
app.Status.History = append(app.Status.History, v1alpha1.RevisionHistory{
Revision: revision,
DeployedAt: metav1.NewTime(time.Now().UTC()),
DeployStartedAt: &startedAt,
ID: nextID,
Source: source,
history := append(app.Status.History, v1alpha1.RevisionHistory{
Revision: revision,
DeployedAt: metav1.NewTime(time.Now().UTC()),
ID: nextID,
Source: source,
})
app.Status.History = app.Status.History.Trunc(app.Spec.GetRevisionHistoryLimit())
if len(history) > common.RevisionHistoryLimit {
history = history[1 : common.RevisionHistoryLimit+1]
}
patch, err := json.Marshal(map[string]map[string][]v1alpha1.RevisionHistory{
"status": {
"history": app.Status.History,
"history": history,
},
})
if err != nil {
return err
}
_, err = m.appclientset.ArgoprojV1alpha1().Applications(m.namespace).Patch(context.Background(), app.Name, types.MergePatchType, patch, metav1.PatchOptions{})
_, err = m.appclientset.ArgoprojV1alpha1().Applications(m.namespace).Patch(app.Name, types.MergePatchType, patch)
return err
}
// NewAppStateManager creates new instance of AppStateManager
// NewAppStateManager creates new instance of Ksonnet app comparator
func NewAppStateManager(
db db.ArgoDB,
appclientset appclientset.Interface,
repoClientset apiclient.Clientset,
repoClientset reposerver.Clientset,
namespace string,
kubectl kubeutil.Kubectl,
settingsMgr *settings.SettingsManager,
settings *settings.ArgoCDSettings,
liveStateCache statecache.LiveStateCache,
projInformer cache.SharedIndexInformer,
metricsServer *metrics.MetricsServer,
@@ -612,7 +387,7 @@ func NewAppStateManager(
kubectl: kubectl,
repoClientset: repoClientset,
namespace: namespace,
settingsMgr: settingsMgr,
settings: settings,
projInformer: projInformer,
metricsServer: metricsServer,
}

View File

@@ -2,32 +2,27 @@ package controller
import (
"encoding/json"
"io/ioutil"
"os"
"testing"
"time"
"github.com/argoproj/gitops-engine/pkg/health"
synccommon "github.com/argoproj/gitops-engine/pkg/sync/common"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
. "github.com/argoproj/gitops-engine/pkg/utils/testing"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"github.com/argoproj/argo-cd/common"
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/reposerver/apiclient"
"github.com/argoproj/argo-cd/reposerver/repository"
"github.com/argoproj/argo-cd/test"
"github.com/argoproj/argo-cd/util/kube"
)
// TestCompareAppStateEmpty tests comparison when both git and live have no objects
func TestCompareAppStateEmpty(t *testing.T) {
app := newFakeApp()
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
manifestResponse: &repository.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
@@ -36,22 +31,22 @@ func TestCompareAppStateEmpty(t *testing.T) {
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
ctrl := newFakeController(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
assert.NoError(t, err)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 0)
assert.Len(t, compRes.managedResources, 0)
assert.Len(t, app.Status.Conditions, 0)
assert.Equal(t, 0, len(compRes.resources))
assert.Equal(t, 0, len(compRes.managedResources))
assert.Equal(t, 0, len(compRes.conditions))
}
// TestCompareAppStateMissing tests when there is a manifest defined in the repo which doesn't exist in live
// TestCompareAppStateMissing tests when there is a manifest defined in git which doesn't exist in live
func TestCompareAppStateMissing(t *testing.T) {
app := newFakeApp()
data := fakeData{
apps: []runtime.Object{app},
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{PodManifest},
manifestResponse: &repository.ManifestResponse{
Manifests: []string{string(test.PodManifest)},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
@@ -59,23 +54,23 @@ func TestCompareAppStateMissing(t *testing.T) {
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
ctrl := newFakeController(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
assert.NoError(t, err)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, argoappv1.SyncStatusCodeOutOfSync, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 1)
assert.Len(t, compRes.managedResources, 1)
assert.Len(t, app.Status.Conditions, 0)
assert.Equal(t, 1, len(compRes.resources))
assert.Equal(t, 1, len(compRes.managedResources))
assert.Equal(t, 0, len(compRes.conditions))
}
// 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}
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
manifestResponse: &repository.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
@@ -86,24 +81,25 @@ func TestCompareAppStateExtra(t *testing.T) {
},
}
ctrl := newFakeController(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
assert.NoError(t, err)
assert.NotNil(t, compRes)
assert.Equal(t, argoappv1.SyncStatusCodeOutOfSync, compRes.syncStatus.Status)
assert.Equal(t, 1, len(compRes.resources))
assert.Equal(t, 1, len(compRes.managedResources))
assert.Equal(t, 0, len(app.Status.Conditions))
assert.Equal(t, 0, len(compRes.conditions))
}
// 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{
apps: []runtime.Object{app},
manifestResponse: &apiclient.ManifestResponse{
manifestResponse: &repository.ManifestResponse{
Manifests: []string{string(podBytes)},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
@@ -112,77 +108,24 @@ func TestCompareAppStateHook(t *testing.T) {
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
ctrl := newFakeController(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
assert.NoError(t, err)
assert.NotNil(t, compRes)
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Equal(t, 0, len(compRes.resources))
assert.Equal(t, 0, len(compRes.managedResources))
assert.Equal(t, 1, len(compRes.reconciliationResult.Hooks))
assert.Equal(t, 0, len(app.Status.Conditions))
}
// TestCompareAppStateSkipHook checks that skipped resources are detected during manifest generation, and not
// considered as part of resources when assessing Synced status
func TestCompareAppStateSkipHook(t *testing.T) {
pod := NewPod()
pod.SetAnnotations(map[string]string{synccommon.AnnotationKeyHook: "Skip"})
podBytes, _ := json.Marshal(pod)
app := newFakeApp()
data := fakeData{
apps: []runtime.Object{app},
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{string(podBytes)},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
ctrl := newFakeController(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
assert.NotNil(t, compRes)
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Equal(t, 1, len(compRes.resources))
assert.Equal(t, 1, len(compRes.managedResources))
assert.Equal(t, 0, len(compRes.reconciliationResult.Hooks))
assert.Equal(t, 0, len(app.Status.Conditions))
}
// checks that ignore resources are detected, but excluded from status
func TestCompareAppStateCompareOptionIgnoreExtraneous(t *testing.T) {
pod := NewPod()
pod.SetAnnotations(map[string]string{common.AnnotationCompareOptions: "IgnoreExtraneous"})
app := newFakeApp()
data := fakeData{
apps: []runtime.Object{app},
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
ctrl := newFakeController(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
assert.NotNil(t, compRes)
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 0)
assert.Len(t, compRes.managedResources, 0)
assert.Len(t, app.Status.Conditions, 0)
assert.Equal(t, 0, len(compRes.conditions))
}
// 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}
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
manifestResponse: &repository.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
@@ -193,14 +136,13 @@ func TestCompareAppStateExtraHook(t *testing.T) {
},
}
ctrl := newFakeController(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
assert.NoError(t, err)
assert.NotNil(t, compRes)
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Equal(t, 1, len(compRes.resources))
assert.Equal(t, 1, len(compRes.managedResources))
assert.Equal(t, 0, len(compRes.reconciliationResult.Hooks))
assert.Equal(t, 0, len(app.Status.Conditions))
assert.Equal(t, 0, len(compRes.conditions))
}
func toJSON(t *testing.T, obj *unstructured.Unstructured) string {
@@ -210,22 +152,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)},
manifestResponse: &repository.ManifestResponse{
Manifests: []string{toJSON(t, obj1), toJSON(t, obj2), toJSON(t, obj3)},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
@@ -236,14 +172,14 @@ func TestCompareAppStateDuplicatedNamespacedResources(t *testing.T) {
},
}
ctrl := newFakeController(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
assert.NoError(t, err)
assert.NotNil(t, compRes)
assert.Equal(t, 1, len(app.Status.Conditions))
assert.NotNil(t, app.Status.Conditions[0].LastTransitionTime)
assert.Equal(t, argoappv1.ApplicationConditionRepeatedResourceWarning, app.Status.Conditions[0].Type)
assert.Equal(t, "Resource /Pod/fake-dest-ns/my-pod appeared 2 times among application resources.", app.Status.Conditions[0].Message)
assert.Equal(t, 4, len(compRes.resources))
assert.Contains(t, compRes.conditions, argoappv1.ApplicationCondition{
Message: "Resource /Pod/fake-dest-ns/my-pod appeared 2 times among application resources.",
Type: argoappv1.ApplicationConditionRepeatedResourceWarning,
})
assert.Equal(t, 2, len(compRes.resources))
}
var defaultProj = argoappv1.AppProject{
@@ -276,7 +212,7 @@ func TestSetHealth(t *testing.T) {
})
ctrl := newFakeController(&fakeData{
apps: []runtime.Object{app, &defaultProj},
manifestResponse: &apiclient.ManifestResponse{
manifestResponse: &repository.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
@@ -287,9 +223,10 @@ func TestSetHealth(t *testing.T) {
},
})
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
assert.NoError(t, err)
assert.Equal(t, compRes.healthStatus.Status, health.HealthStatusHealthy)
assert.Equal(t, compRes.healthStatus.Status, argoappv1.HealthStatusHealthy)
}
func TestSetHealthSelfReferencedApp(t *testing.T) {
@@ -307,7 +244,7 @@ func TestSetHealthSelfReferencedApp(t *testing.T) {
})
ctrl := newFakeController(&fakeData{
apps: []runtime.Object{app, &defaultProj},
manifestResponse: &apiclient.ManifestResponse{
manifestResponse: &repository.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
@@ -319,454 +256,8 @@ func TestSetHealthSelfReferencedApp(t *testing.T) {
},
})
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
assert.Equal(t, compRes.healthStatus.Status, health.HealthStatusHealthy)
}
func TestSetManagedResourcesWithOrphanedResources(t *testing.T) {
proj := defaultProj.DeepCopy()
proj.Spec.OrphanedResources = &argoappv1.OrphanedResourcesMonitorSettings{}
app := newFakeApp()
ctrl := newFakeController(&fakeData{
apps: []runtime.Object{app, proj},
namespacedResources: map[kube.ResourceKey]namespacedResource{
kube.NewResourceKey("apps", kube.DeploymentKind, app.Namespace, "guestbook"): {
ResourceNode: argoappv1.ResourceNode{
ResourceRef: argoappv1.ResourceRef{Kind: kube.DeploymentKind, Name: "guestbook", Namespace: app.Namespace},
},
AppName: "",
},
},
})
tree, err := ctrl.setAppManagedResources(app, &comparisonResult{managedResources: make([]managedResource, 0)})
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
assert.NoError(t, err)
assert.Equal(t, len(tree.OrphanedNodes), 1)
assert.Equal(t, "guestbook", tree.OrphanedNodes[0].Name)
assert.Equal(t, app.Namespace, tree.OrphanedNodes[0].Namespace)
}
func TestSetManagedResourcesWithResourcesOfAnotherApp(t *testing.T) {
proj := defaultProj.DeepCopy()
proj.Spec.OrphanedResources = &argoappv1.OrphanedResourcesMonitorSettings{}
app1 := newFakeApp()
app1.Name = "app1"
app2 := newFakeApp()
app2.Name = "app2"
ctrl := newFakeController(&fakeData{
apps: []runtime.Object{app1, app2, proj},
namespacedResources: map[kube.ResourceKey]namespacedResource{
kube.NewResourceKey("apps", kube.DeploymentKind, app2.Namespace, "guestbook"): {
ResourceNode: argoappv1.ResourceNode{
ResourceRef: argoappv1.ResourceRef{Kind: kube.DeploymentKind, Name: "guestbook", Namespace: app2.Namespace},
},
AppName: "app2",
},
},
})
tree, err := ctrl.setAppManagedResources(app1, &comparisonResult{managedResources: make([]managedResource, 0)})
assert.NoError(t, err)
assert.Equal(t, len(tree.OrphanedNodes), 0)
}
func TestReturnUnknownComparisonStateOnSettingLoadError(t *testing.T) {
proj := defaultProj.DeepCopy()
proj.Spec.OrphanedResources = &argoappv1.OrphanedResourcesMonitorSettings{}
app := newFakeApp()
ctrl := newFakeController(&fakeData{
apps: []runtime.Object{app, proj},
configMapData: map[string]string{
"resource.customizations": "invalid setting",
},
})
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
assert.Equal(t, health.HealthStatusUnknown, compRes.healthStatus.Status)
assert.Equal(t, argoappv1.SyncStatusCodeUnknown, compRes.syncStatus.Status)
}
func TestSetManagedResourcesKnownOrphanedResourceExceptions(t *testing.T) {
proj := defaultProj.DeepCopy()
proj.Spec.OrphanedResources = &argoappv1.OrphanedResourcesMonitorSettings{}
app := newFakeApp()
app.Namespace = "default"
ctrl := newFakeController(&fakeData{
apps: []runtime.Object{app, proj},
namespacedResources: map[kube.ResourceKey]namespacedResource{
kube.NewResourceKey("apps", kube.DeploymentKind, app.Namespace, "guestbook"): {
ResourceNode: argoappv1.ResourceNode{ResourceRef: argoappv1.ResourceRef{Group: "apps", Kind: kube.DeploymentKind, Name: "guestbook", Namespace: app.Namespace}},
},
kube.NewResourceKey("", kube.ServiceAccountKind, app.Namespace, "default"): {
ResourceNode: argoappv1.ResourceNode{ResourceRef: argoappv1.ResourceRef{Kind: kube.ServiceAccountKind, Name: "default", Namespace: app.Namespace}},
},
kube.NewResourceKey("", kube.ServiceKind, app.Namespace, "kubernetes"): {
ResourceNode: argoappv1.ResourceNode{ResourceRef: argoappv1.ResourceRef{Kind: kube.ServiceAccountKind, Name: "kubernetes", Namespace: app.Namespace}},
},
},
})
tree, err := ctrl.setAppManagedResources(app, &comparisonResult{managedResources: make([]managedResource, 0)})
assert.NoError(t, err)
assert.Len(t, tree.OrphanedNodes, 1)
assert.Equal(t, "guestbook", tree.OrphanedNodes[0].Name)
}
func Test_appStateManager_persistRevisionHistory(t *testing.T) {
app := newFakeApp()
ctrl := newFakeController(&fakeData{
apps: []runtime.Object{app},
})
manager := ctrl.appStateManager.(*appStateManager)
setRevisionHistoryLimit := func(value int) {
i := int64(value)
app.Spec.RevisionHistoryLimit = &i
}
addHistory := func() {
err := manager.persistRevisionHistory(app, "my-revision", argoappv1.ApplicationSource{}, metav1.Time{})
assert.NoError(t, err)
}
addHistory()
assert.Len(t, app.Status.History, 1)
addHistory()
assert.Len(t, app.Status.History, 2)
addHistory()
assert.Len(t, app.Status.History, 3)
addHistory()
assert.Len(t, app.Status.History, 4)
addHistory()
assert.Len(t, app.Status.History, 5)
addHistory()
assert.Len(t, app.Status.History, 6)
addHistory()
assert.Len(t, app.Status.History, 7)
addHistory()
assert.Len(t, app.Status.History, 8)
addHistory()
assert.Len(t, app.Status.History, 9)
addHistory()
assert.Len(t, app.Status.History, 10)
// default limit is 10
addHistory()
assert.Len(t, app.Status.History, 10)
// increase limit
setRevisionHistoryLimit(11)
addHistory()
assert.Len(t, app.Status.History, 11)
// decrease limit
setRevisionHistoryLimit(9)
addHistory()
assert.Len(t, app.Status.History, 9)
metav1NowTime := metav1.NewTime(time.Now())
err := manager.persistRevisionHistory(app, "my-revision", argoappv1.ApplicationSource{}, metav1NowTime)
assert.NoError(t, err)
assert.Equal(t, app.Status.History.LastRevisionHistory().DeployStartedAt, &metav1NowTime)
}
// helper function to read contents of a file to string
// panics on error
func mustReadFile(path string) string {
b, err := ioutil.ReadFile(path)
if err != nil {
panic(err.Error())
}
return string(b)
}
var signedProj = argoappv1.AppProject{
ObjectMeta: metav1.ObjectMeta{
Name: "default",
Namespace: test.FakeArgoCDNamespace,
},
Spec: argoappv1.AppProjectSpec{
SourceRepos: []string{"*"},
Destinations: []argoappv1.ApplicationDestination{
{
Server: "*",
Namespace: "*",
},
},
SignatureKeys: []argoappv1.SignatureKey{
{
KeyID: "4AEE18F83AFDEB23",
},
},
},
}
func TestSignedResponseNoSignatureRequired(t *testing.T) {
oldval := os.Getenv("ARGOCD_GPG_ENABLED")
os.Setenv("ARGOCD_GPG_ENABLED", "true")
defer os.Setenv("ARGOCD_GPG_ENABLED", oldval)
// We have a good signature response, but project does not require signed commits
{
app := newFakeApp()
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
VerifyResult: mustReadFile("../util/gpg/testdata/good_signature.txt"),
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
ctrl := newFakeController(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 0)
assert.Len(t, compRes.managedResources, 0)
assert.Len(t, app.Status.Conditions, 0)
}
// We have a bad signature response, but project does not require signed commits
{
app := newFakeApp()
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
VerifyResult: mustReadFile("../util/gpg/testdata/bad_signature_bad.txt"),
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
ctrl := newFakeController(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &defaultProj, "", app.Spec.Source, false, nil)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 0)
assert.Len(t, compRes.managedResources, 0)
assert.Len(t, app.Status.Conditions, 0)
}
}
func TestSignedResponseSignatureRequired(t *testing.T) {
oldval := os.Getenv("ARGOCD_GPG_ENABLED")
os.Setenv("ARGOCD_GPG_ENABLED", "true")
defer os.Setenv("ARGOCD_GPG_ENABLED", oldval)
// We have a good signature response, valid key, and signing is required - sync!
{
app := newFakeApp()
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
VerifyResult: mustReadFile("../util/gpg/testdata/good_signature.txt"),
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
ctrl := newFakeController(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "", app.Spec.Source, false, nil)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 0)
assert.Len(t, compRes.managedResources, 0)
assert.Len(t, app.Status.Conditions, 0)
}
// We have a bad signature response and signing is required - do not sync
{
app := newFakeApp()
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
VerifyResult: mustReadFile("../util/gpg/testdata/bad_signature_bad.txt"),
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
ctrl := newFakeController(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, nil)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 0)
assert.Len(t, compRes.managedResources, 0)
assert.Len(t, app.Status.Conditions, 1)
}
// We have a malformed signature response and signing is required - do not sync
{
app := newFakeApp()
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
VerifyResult: mustReadFile("../util/gpg/testdata/bad_signature_malformed1.txt"),
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
ctrl := newFakeController(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, nil)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 0)
assert.Len(t, compRes.managedResources, 0)
assert.Len(t, app.Status.Conditions, 1)
}
// We have no signature response (no signature made) and signing is required - do not sync
{
app := newFakeApp()
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
VerifyResult: "",
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
ctrl := newFakeController(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, nil)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 0)
assert.Len(t, compRes.managedResources, 0)
assert.Len(t, app.Status.Conditions, 1)
}
// We have a good signature and signing is required, but key is not allowed - do not sync
{
app := newFakeApp()
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
VerifyResult: mustReadFile("../util/gpg/testdata/good_signature.txt"),
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
ctrl := newFakeController(&data)
testProj := signedProj
testProj.Spec.SignatureKeys[0].KeyID = "4AEE18F83AFDEB24"
compRes := ctrl.appStateManager.CompareAppState(app, &testProj, "abc123", app.Spec.Source, false, nil)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 0)
assert.Len(t, compRes.managedResources, 0)
assert.Len(t, app.Status.Conditions, 1)
assert.Contains(t, app.Status.Conditions[0].Message, "key is not allowed")
}
// Signature required and local manifests supplied - do not sync
{
app := newFakeApp()
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
VerifyResult: "",
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
// it doesn't matter for our test whether local manifests are valid
localManifests := []string{"foobar"}
ctrl := newFakeController(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, localManifests)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, argoappv1.SyncStatusCodeUnknown, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 0)
assert.Len(t, compRes.managedResources, 0)
assert.Len(t, app.Status.Conditions, 1)
assert.Contains(t, app.Status.Conditions[0].Message, "Cannot use local manifests")
}
os.Setenv("ARGOCD_GPG_ENABLED", "false")
// We have a bad signature response and signing would be required, but GPG subsystem is disabled - sync
{
app := newFakeApp()
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
VerifyResult: mustReadFile("../util/gpg/testdata/bad_signature_bad.txt"),
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
ctrl := newFakeController(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, nil)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 0)
assert.Len(t, compRes.managedResources, 0)
assert.Len(t, app.Status.Conditions, 0)
}
// Signature required and local manifests supplied and GPG subystem is disabled - sync
{
app := newFakeApp()
data := fakeData{
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
VerifyResult: "",
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
// it doesn't matter for our test whether local manifests are valid
localManifests := []string{""}
ctrl := newFakeController(&data)
compRes := ctrl.appStateManager.CompareAppState(app, &signedProj, "abc123", app.Spec.Source, false, localManifests)
assert.NotNil(t, compRes)
assert.NotNil(t, compRes.syncStatus)
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
assert.Len(t, compRes.resources, 0)
assert.Len(t, compRes.managedResources, 0)
assert.Len(t, app.Status.Conditions, 0)
}
}
func TestComparisonResult_GetHealthStatus(t *testing.T) {
status := &argoappv1.HealthStatus{Status: health.HealthStatusMissing}
res := comparisonResult{
healthStatus: status,
}
assert.Equal(t, status, res.GetHealthStatus())
}
func TestComparisonResult_GetSyncStatus(t *testing.T) {
status := &argoappv1.SyncStatus{Status: argoappv1.SyncStatusCodeOutOfSync}
res := comparisonResult{
syncStatus: status,
}
assert.Equal(t, status, res.GetSyncStatus())
assert.Equal(t, compRes.healthStatus.Status, argoappv1.HealthStatusHealthy)
}

View File

@@ -3,41 +3,58 @@ package controller
import (
"context"
"fmt"
"sync/atomic"
"time"
"sort"
"sync"
"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"
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/client-go/discovery"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
cdcommon "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"
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/pkg/client/listers/application/v1alpha1"
"github.com/argoproj/argo-cd/util/argo"
"github.com/argoproj/argo-cd/util/lua"
"github.com/argoproj/argo-cd/util/rand"
hookutil "github.com/argoproj/argo-cd/util/hook"
"github.com/argoproj/argo-cd/util/kube"
)
var syncIdPrefix uint64 = 0
type syncContext struct {
appName string
proj *appv1.AppProject
compareResult *comparisonResult
config *rest.Config
dynamicIf dynamic.Interface
disco discovery.DiscoveryInterface
kubectl kube.Kubectl
namespace string
server string
syncOp *appv1.SyncOperation
syncRes *appv1.SyncOperationResult
syncResources []appv1.SyncOperationResource
opState *appv1.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) {
func (m *appStateManager) SyncAppState(app *appv1.Application, state *appv1.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
// concrete git commit SHA, the SHA is remembered in the status.operationState.syncResult field.
// This ensures that when resuming an operation, we sync to the same revision that we initially
// started with.
var revision string
var syncOp v1alpha1.SyncOperation
var syncRes *v1alpha1.SyncOperationResult
var source v1alpha1.ApplicationSource
var syncOp appv1.SyncOperation
var syncRes *appv1.SyncOperationResult
var syncResources []appv1.SyncOperationResource
var source appv1.ApplicationSource
if state.Operation.Sync == nil {
state.Phase = common.OperationFailed
state.Phase = appv1.OperationFailed
state.Message = "Invalid operation request: no operation specified"
return
}
@@ -49,12 +66,12 @@ 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
} else {
syncRes = &v1alpha1.SyncOperationResult{}
syncRes = &appv1.SyncOperationResult{}
// status.operationState.syncResult.source. must be set properly since auto-sync relies
// on this information to decide if it should sync (if source is different than the last
// sync attempt)
@@ -69,126 +86,539 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
revision = syncOp.Revision
}
proj, err := argo.GetAppProject(&app.Spec, listersv1alpha1.NewAppProjectLister(m.projInformer.GetIndexer()), m.namespace)
compareResult, err := m.CompareAppState(app, revision, source, false)
if err != nil {
state.Phase = common.OperationError
state.Message = fmt.Sprintf("Failed to load application project: %v", err)
return
}
compareResult := m.CompareAppState(app, proj, revision, source, false, syncOp.Manifests)
// We now have a concrete commit SHA. Save this in the sync result revision so that we remember
// what we should be syncing to when resuming operations.
syncRes.Revision = compareResult.syncStatus.Revision
// If there are any comparison or spec errors error conditions do not perform the operation
if errConditions := app.Status.GetConditions(map[v1alpha1.ApplicationConditionType]bool{
v1alpha1.ApplicationConditionComparisonError: true,
v1alpha1.ApplicationConditionInvalidSpecError: true,
}); len(errConditions) > 0 {
state.Phase = common.OperationError
state.Message = argo.FormatAppConditions(errConditions)
return
}
clst, err := m.db.GetCluster(context.Background(), app.Spec.Destination.Server)
if err != nil {
state.Phase = common.OperationError
state.Phase = appv1.OperationError
state.Message = err.Error()
return
}
rawConfig := clst.RawRestConfig()
restConfig := metrics.AddMetricsTransportWrapper(m.metricsServer, app, clst.RESTConfig())
resourceOverrides, err := m.settingsMgr.GetResourceOverrides()
if err != nil {
state.Phase = common.OperationError
state.Message = fmt.Sprintf("Failed to load resource overrides: %v", err)
// If there are any error conditions, do not perform the operation
errConditions := make([]appv1.ApplicationCondition, 0)
for i := range compareResult.conditions {
if compareResult.conditions[i].IsError() {
errConditions = append(errConditions, compareResult.conditions[i])
}
}
if len(errConditions) > 0 {
state.Phase = appv1.OperationError
state.Message = argo.FormatAppConditions(errConditions)
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, key.Namespace, schema.GroupVersionKind{Kind: key.Kind, Group: key.Group}, syncOp.Resources)
}),
sync.WithManifestValidation(!syncOp.SyncOptions.HasOption("Validate=false")),
sync.WithNamespaceCreation(syncOp.SyncOptions.HasOption("CreateNamespace=true"), func(un *unstructured.Unstructured) bool {
if un != nil && kube.GetAppInstanceLabel(un, cdcommon.LabelKeyAppInstance) != "" {
kube.UnsetLabel(un, cdcommon.LabelKeyAppInstance)
return true
}
return false
}),
)
// We now have a concrete commit SHA. Save this in the sync result revision so that we remember
// what we should be syncing to when resuming operations.
syncRes.Revision = compareResult.syncStatus.Revision
clst, err := m.db.GetCluster(context.Background(), app.Spec.Destination.Server)
if err != nil {
state.Phase = common.OperationError
state.Message = fmt.Sprintf("failed to record sync to history: %v", err)
state.Phase = appv1.OperationError
state.Message = err.Error()
return
}
start := time.Now()
restConfig := metrics.AddMetricsTransportWrapper(m.metricsServer, app, clst.RESTConfig())
dynamicIf, err := dynamic.NewForConfig(restConfig)
if err != nil {
state.Phase = appv1.OperationError
state.Message = fmt.Sprintf("Failed to initialize dynamic client: %v", err)
return
}
disco, err := discovery.NewDiscoveryClientForConfig(restConfig)
if err != nil {
state.Phase = appv1.OperationError
state.Message = fmt.Sprintf("Failed to initialize discovery client: %v", err)
return
}
if state.Phase == common.OperationTerminating {
syncCtx.Terminate()
proj, err := argo.GetAppProject(&app.Spec, v1alpha1.NewAppProjectLister(m.projInformer.GetIndexer()), m.namespace)
if err != nil {
state.Phase = appv1.OperationError
state.Message = fmt.Sprintf("Failed to load application project: %v", err)
return
}
syncCtx := syncContext{
appName: app.Name,
proj: proj,
compareResult: compareResult,
config: restConfig,
dynamicIf: dynamicIf,
disco: disco,
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}),
}
if state.Phase == appv1.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")
if !syncOp.DryRun && len(syncOp.Resources) == 0 && state.Phase.Successful() {
err := m.persistRevisionHistory(app, compareResult.syncStatus.Revision, source, state.StartedAt)
if !syncOp.DryRun && len(syncOp.Resources) == 0 && syncCtx.opState.Phase.Successful() {
err := m.persistRevisionHistory(app, compareResult.syncStatus.Revision, source)
if err != nil {
state.Phase = common.OperationError
state.Phase = appv1.OperationError
state.Message = fmt.Sprintf("failed to record sync to history: %v", err)
}
}
}
// 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 {
liveObj *unstructured.Unstructured
targetObj *unstructured.Unstructured
skipDryRun bool
}
// sync has performs the actual apply or hook based sync
func (sc *syncContext) sync() {
syncTasks, successful := sc.generateSyncTasks()
if !successful {
sc.setOperationPhase(appv1.OperationFailed, "one or more synchronization tasks are not valid")
return
}
// If no sync tasks were generated (e.g., in case all application manifests have been removed),
// set the sync operation as successful.
if len(syncTasks) == 0 {
sc.setOperationPhase(appv1.OperationSucceeded, "successfully synced (no manifests)")
return
}
// 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.
if !sc.startedPreSyncPhase() {
// Optimization: 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.doApplySync(syncTasks, true, false, sc.syncOp.DryRun) {
sc.setOperationPhase(appv1.OperationFailed, "one or more objects failed to apply (dry run)")
return
}
if sc.syncOp.DryRun {
sc.setOperationPhase(appv1.OperationSucceeded, "successfully synced (dry run)")
return
}
}
// All objects passed a `kubectl apply --dry-run`, so we are now ready to actually perform the sync.
if sc.syncOp.SyncStrategy == nil {
// default sync strategy to hook if no strategy
sc.syncOp.SyncStrategy = &appv1.SyncStrategy{Hook: &appv1.SyncStrategyHook{}}
}
if sc.syncOp.SyncStrategy.Apply != nil {
if !sc.startedSyncPhase() {
if !sc.doApplySync(syncTasks, false, sc.syncOp.SyncStrategy.Apply.Force, true) {
sc.setOperationPhase(appv1.OperationFailed, "one or more objects failed to apply")
return
}
// If apply was successful, return here and force an app refresh. This is so the app
// will become requeued into the workqueue, to force a new sync/health assessment before
// marking the operation as completed
return
}
sc.setOperationPhase(appv1.OperationSucceeded, "successfully synced")
} else if sc.syncOp.SyncStrategy.Hook != nil {
hooks, err := sc.getHooks()
if err != nil {
sc.setOperationPhase(appv1.OperationError, fmt.Sprintf("failed to generate hooks resources: %v", err))
return
}
sc.doHookSync(syncTasks, hooks)
} else {
sc.setOperationPhase(appv1.OperationFailed, "Unknown sync strategy")
return
}
}
// generateSyncTasks() generates the list of sync tasks we will be performing during this sync.
func (sc *syncContext) generateSyncTasks() ([]syncTask, bool) {
syncTasks := make([]syncTask, 0)
successful := true
for _, resourceState := range sc.compareResult.managedResources {
if resourceState.Hook {
continue
}
if sc.syncResources == nil ||
(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)) {
skipDryRun := false
var targetObj *unstructured.Unstructured
if resourceState.Target != nil {
targetObj = resourceState.Target.DeepCopy()
if 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`
targetObj.SetNamespace(sc.namespace)
}
gvk := targetObj.GroupVersionKind()
serverRes, err := kube.ServerResourceForGroupVersionKind(sc.disco, gvk)
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) && hasCRDOfGroupKind(sc.compareResult.managedResources, gvk.Group, gvk.Kind) {
skipDryRun = true
} else {
sc.setResourceDetails(&appv1.ResourceResult{
Name: targetObj.GetName(),
Group: gvk.Group,
Version: gvk.Version,
Kind: targetObj.GetKind(),
Namespace: targetObj.GetNamespace(),
Message: err.Error(),
Status: appv1.ResultCodeSyncFailed,
})
successful = false
}
} else {
if !sc.proj.IsResourcePermitted(metav1.GroupKind{Group: gvk.Group, Kind: gvk.Kind}, serverRes.Namespaced) {
sc.setResourceDetails(&appv1.ResourceResult{
Name: targetObj.GetName(),
Group: gvk.Group,
Version: gvk.Version,
Kind: targetObj.GetKind(),
Namespace: targetObj.GetNamespace(),
Message: fmt.Sprintf("Resource %s:%s is not permitted in project %s.", gvk.Group, gvk.Kind, sc.proj.Name),
Status: appv1.ResultCodeSyncFailed,
})
successful = false
}
if serverRes.Namespaced && !sc.proj.IsDestinationPermitted(appv1.ApplicationDestination{Namespace: targetObj.GetNamespace(), Server: sc.server}) {
sc.setResourceDetails(&appv1.ResourceResult{
Name: targetObj.GetName(),
Group: gvk.Group,
Version: gvk.Version,
Kind: targetObj.GetKind(),
Namespace: targetObj.GetNamespace(),
Message: fmt.Sprintf("namespace %v is not permitted in project '%s'", targetObj.GetNamespace(), sc.proj.Name),
Status: appv1.ResultCodeSyncFailed,
})
successful = false
}
}
}
syncTask := syncTask{
liveObj: resourceState.Live,
targetObj: targetObj,
skipDryRun: skipDryRun,
}
syncTasks = append(syncTasks, syncTask)
}
}
sort.Sort(newKindSorter(syncTasks, resourceOrder))
return syncTasks, successful
}
// startedPreSyncPhase detects if we already started the PreSync stage of a sync operation.
// This is equal to if we have anything in our resource or hook list
func (sc *syncContext) startedPreSyncPhase() bool {
return len(sc.syncRes.Resources) > 0
}
// startedSyncPhase detects if we have already started the Sync stage of a sync operation.
// This is equal to if the resource list is non-empty, or we we see Sync/PostSync hooks
func (sc *syncContext) startedSyncPhase() bool {
for _, res := range sc.syncRes.Resources {
if !res.IsHook() {
return true
}
if res.HookType == appv1.HookTypeSync || res.HookType == appv1.HookTypePostSync {
return true
}
}
return false
}
// startedPostSyncPhase detects if we have already started the PostSync stage. This is equal to if
// we see any PostSync hooks
func (sc *syncContext) startedPostSyncPhase() bool {
for _, res := range sc.syncRes.Resources {
if res.IsHook() && res.HookType == appv1.HookTypePostSync {
return true
}
}
return false
}
func (sc *syncContext) setOperationPhase(phase appv1.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
}
// applyObject performs a `kubectl apply` of a single resource
func (sc *syncContext) applyObject(targetObj *unstructured.Unstructured, dryRun bool, force bool) appv1.ResourceResult {
gvk := targetObj.GroupVersionKind()
resDetails := appv1.ResourceResult{
Name: targetObj.GetName(),
Group: gvk.Group,
Version: gvk.Version,
Kind: targetObj.GetKind(),
Namespace: targetObj.GetNamespace(),
}
message, err := sc.kubectl.ApplyResource(sc.config, targetObj, targetObj.GetNamespace(), dryRun, force)
if err != nil {
resDetails.Message = err.Error()
resDetails.Status = appv1.ResultCodeSyncFailed
return resDetails
}
resDetails.Message = message
resDetails.Status = appv1.ResultCodeSynced
return resDetails
}
// 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) appv1.ResourceResult {
gvk := liveObj.GroupVersionKind()
resDetails := appv1.ResourceResult{
Name: liveObj.GetName(),
Group: gvk.Group,
Version: gvk.Version,
Kind: liveObj.GetKind(),
Namespace: liveObj.GetNamespace(),
}
if prune {
if dryRun {
resDetails.Message = "pruned (dry run)"
resDetails.Status = appv1.ResultCodePruned
} else {
resDetails.Message = "pruned"
resDetails.Status = appv1.ResultCodePruned
// 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 {
resDetails.Message = err.Error()
resDetails.Status = appv1.ResultCodeSyncFailed
}
}
}
} else {
resDetails.Message = "ignored (requires pruning)"
resDetails.Status = appv1.ResultCodePruneSkipped
}
return resDetails
}
func hasCRDOfGroupKind(resources []managedResource, group string, kind string) bool {
for _, res := range resources {
if res.Target != nil && kube.IsCRD(res.Target) {
crdGroup, ok, err := unstructured.NestedString(res.Target.Object, "spec", "group")
if err != nil || !ok {
continue
}
crdKind, ok, err := unstructured.NestedString(res.Target.Object, "spec", "names", "kind")
if err != nil || !ok {
continue
}
if group == crdGroup && crdKind == kind {
return true
}
}
}
return false
}
// performs a apply based sync of the given sync tasks (possibly pruning the objects).
// If update is true, will updates the resource details with the result.
// Or if the prune/apply failed, will also update the result.
func (sc *syncContext) doApplySync(syncTasks []syncTask, dryRun, force, update bool) bool {
syncSuccessful := true
var createTasks []syncTask
var pruneTasks []syncTask
for _, syncTask := range syncTasks {
if syncTask.targetObj == nil {
pruneTasks = append(pruneTasks, syncTask)
} else {
createTasks = append(createTasks, syncTask)
}
}
var wg sync.WaitGroup
for _, task := range pruneTasks {
wg.Add(1)
go func(t syncTask) {
defer wg.Done()
resDetails := sc.pruneObject(t.liveObj, sc.syncOp.Prune, dryRun)
if !resDetails.Status.Successful() {
syncSuccessful = false
}
if update || !resDetails.Status.Successful() {
sc.setResourceDetails(&resDetails)
}
}(task)
}
wg.Wait()
processCreateTasks := func(tasks []syncTask) {
var createWg sync.WaitGroup
for i := range tasks {
if dryRun && tasks[i].skipDryRun {
continue
}
createWg.Add(1)
go func(t syncTask) {
defer createWg.Done()
if hookutil.IsHook(t.targetObj) {
return
}
resDetails := sc.applyObject(t.targetObj, dryRun, force)
if !resDetails.Status.Successful() {
syncSuccessful = false
}
if update || !resDetails.Status.Successful() {
sc.setResourceDetails(&resDetails)
}
}(tasks[i])
}
createWg.Wait()
}
var tasksGroup []syncTask
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.targetObj.GetKind() {
processCreateTasks(tasksGroup)
tasksGroup = []syncTask{task}
} else {
tasksGroup = append(tasksGroup, task)
}
}
if len(tasksGroup) > 0 {
processCreateTasks(tasksGroup)
}
return syncSuccessful
}
// setResourceDetails sets a resource details in the SyncResult.Resources list
func (sc *syncContext) setResourceDetails(details *appv1.ResourceResult) {
sc.lock.Lock()
defer sc.lock.Unlock()
for i, res := range sc.syncRes.Resources {
if res.Group == details.Group && res.Kind == details.Kind && res.Namespace == details.Namespace && res.Name == details.Name {
// update existing value
if res.Status != details.Status {
sc.log.Infof("updated resource %s/%s/%s status: %s -> %s", res.Kind, res.Namespace, res.Name, res.Status, details.Status)
}
if res.Message != details.Message {
sc.log.Infof("updated resource %s/%s/%s message: %s -> %s", res.Kind, res.Namespace, res.Name, res.Message, details.Message)
}
sc.syncRes.Resources[i] = details
return
}
}
sc.log.Infof("added resource %s/%s status: %s, message: %s", details.Kind, details.Name, details.Status, details.Message)
sc.syncRes.Resources = append(sc.syncRes.Resources, details)
}
// This code is mostly taken from https://github.com/helm/helm/blob/release-2.10/pkg/tiller/kind_sorter.go
// sortOrder is an ordering of Kinds.
type sortOrder []string
// resourceOrder represents the correct order of Kubernetes resources within a manifest
var resourceOrder sortOrder = []string{
"Namespace",
"ResourceQuota",
"LimitRange",
"PodSecurityPolicy",
"Secret",
"ConfigMap",
"StorageClass",
"PersistentVolume",
"PersistentVolumeClaim",
"ServiceAccount",
"CustomResourceDefinition",
"ClusterRole",
"ClusterRoleBinding",
"Role",
"RoleBinding",
"Service",
"DaemonSet",
"Pod",
"ReplicationController",
"ReplicaSet",
"Deployment",
"StatefulSet",
"Job",
"CronJob",
"Ingress",
"APIService",
}
type kindSorter struct {
ordering map[string]int
manifests []syncTask
}
func newKindSorter(m []syncTask, s sortOrder) *kindSorter {
o := make(map[string]int, len(s))
for v, k := range s {
o[k] = v
}
return &kindSorter{
manifests: m,
ordering: o,
}
}
func (k *kindSorter) Len() int { return len(k.manifests) }
func (k *kindSorter) Swap(i, j int) { k.manifests[i], k.manifests[j] = k.manifests[j], k.manifests[i] }
func (k *kindSorter) Less(i, j int) bool {
a := k.manifests[i].targetObj
if a == nil {
return false
}
b := k.manifests[j].targetObj
if b == nil {
return true
}
first, aok := k.ordering[a.GetKind()]
second, bok := k.ordering[b.GetKind()]
// if both are unknown and of different kind sort by kind alphabetically
if !aok && !bok && a.GetKind() != b.GetKind() {
return a.GetKind() < b.GetKind()
}
// unknown kind is last
if !aok {
return false
}
if !bok {
return true
}
// if same kind (including unknown) sub sort alphanumeric
if first == second {
return a.GetName() < b.GetName()
}
// sort different kinds
return first < second
}

View File

@@ -0,0 +1,66 @@
package controller
import (
"testing"
"github.com/stretchr/testify/assert"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/test"
"github.com/argoproj/argo-cd/util/kube/kubetest"
)
var clusterRoleHook = `
{
"apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "ClusterRole",
"metadata": {
"name": "cluster-role-hook",
"annotations": {
"argocd.argoproj.io/hook": "PostSync"
}
}
}`
func TestSyncHookProjectPermissions(t *testing.T) {
syncCtx := newTestSyncCtx(&v1.APIResourceList{
GroupVersion: "v1",
APIResources: []v1.APIResource{
{Name: "pod", Namespaced: true, Kind: "Pod", Group: "v1"},
},
}, &v1.APIResourceList{
GroupVersion: "rbac.authorization.k8s.io/v1",
APIResources: []v1.APIResource{
{Name: "clusterroles", Namespaced: false, Kind: "ClusterRole", Group: "rbac.authorization.k8s.io"},
},
})
syncCtx.kubectl = kubetest.MockKubectlCmd{}
crHook, _ := v1alpha1.UnmarshalToUnstructured(clusterRoleHook)
syncCtx.compareResult = &comparisonResult{
hooks: []*unstructured.Unstructured{
crHook,
},
managedResources: []managedResource{{
Target: test.NewPod(),
}},
}
syncCtx.proj.Spec.ClusterResourceWhitelist = []v1.GroupKind{}
syncCtx.syncOp.SyncStrategy = nil
syncCtx.sync()
assert.Equal(t, v1alpha1.OperationFailed, syncCtx.opState.Phase)
assert.Len(t, syncCtx.syncRes.Resources, 0)
assert.Contains(t, syncCtx.opState.Message, "not permitted in project")
// Now add the resource to the whitelist and try again. Resource should be created
syncCtx.proj.Spec.ClusterResourceWhitelist = []v1.GroupKind{
{Group: "rbac.authorization.k8s.io", Kind: "ClusterRole"},
}
syncCtx.syncOp.SyncStrategy = nil
syncCtx.sync()
assert.Len(t, syncCtx.syncRes.Resources, 1)
assert.Equal(t, v1alpha1.ResultCodeSynced, syncCtx.syncRes.Resources[0].Status)
}

560
controller/sync_hooks.go Normal file
View File

@@ -0,0 +1,560 @@
package controller
import (
"fmt"
"reflect"
"strings"
wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
apiv1 "k8s.io/api/core/v1"
apierr "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kubernetes/pkg/apis/batch"
"github.com/argoproj/argo-cd/common"
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/util"
hookutil "github.com/argoproj/argo-cd/util/hook"
"github.com/argoproj/argo-cd/util/kube"
)
// doHookSync initiates (or continues) a hook-based sync. This method will be invoked when there may
// already be in-flight (potentially incomplete) jobs/workflows, and should be idempotent.
func (sc *syncContext) doHookSync(syncTasks []syncTask, hooks []*unstructured.Unstructured) {
if !sc.startedPreSyncPhase() {
if !sc.verifyPermittedHooks(hooks) {
return
}
}
// 1. Run PreSync hooks
if !sc.runHooks(hooks, appv1.HookTypePreSync) {
return
}
// 2. Run Sync hooks (e.g. blue-green sync workflow)
// Before performing Sync hooks, apply any normal manifests which aren't annotated with a hook.
// We only want to do this once per operation.
shouldContinue := true
if !sc.startedSyncPhase() {
if !sc.syncNonHookTasks(syncTasks) {
sc.setOperationPhase(appv1.OperationFailed, "one or more objects failed to apply")
return
}
shouldContinue = false
}
if !sc.runHooks(hooks, appv1.HookTypeSync) {
shouldContinue = false
}
if !shouldContinue {
return
}
// 3. Run PostSync hooks
// Before running PostSync hooks, we want to make rollout is complete (app is healthy). If we
// already started the post-sync phase, then we do not need to perform the health check.
postSyncHooks, _ := sc.getHooks(appv1.HookTypePostSync)
if len(postSyncHooks) > 0 && !sc.startedPostSyncPhase() {
sc.log.Infof("PostSync application health check: %s", sc.compareResult.healthStatus.Status)
if sc.compareResult.healthStatus.Status != appv1.HealthStatusHealthy {
sc.setOperationPhase(appv1.OperationRunning, fmt.Sprintf("waiting for %s state to run %s hooks (current health: %s)",
appv1.HealthStatusHealthy, appv1.HookTypePostSync, sc.compareResult.healthStatus.Status))
return
}
}
if !sc.runHooks(hooks, appv1.HookTypePostSync) {
return
}
// if we get here, all hooks successfully completed
sc.setOperationPhase(appv1.OperationSucceeded, "successfully synced")
}
// verifyPermittedHooks verifies all hooks are permitted in the project
func (sc *syncContext) verifyPermittedHooks(hooks []*unstructured.Unstructured) bool {
for _, hook := range hooks {
gvk := hook.GroupVersionKind()
serverRes, err := kube.ServerResourceForGroupVersionKind(sc.disco, gvk)
if err != nil {
sc.setOperationPhase(appv1.OperationError, fmt.Sprintf("unable to identify api resource type: %v", gvk))
return false
}
if !sc.proj.IsResourcePermitted(metav1.GroupKind{Group: gvk.Group, Kind: gvk.Kind}, serverRes.Namespaced) {
sc.setOperationPhase(appv1.OperationFailed, fmt.Sprintf("Hook resource %s:%s is not permitted in project %s", gvk.Group, gvk.Kind, sc.proj.Name))
return false
}
if serverRes.Namespaced && !sc.proj.IsDestinationPermitted(appv1.ApplicationDestination{Namespace: hook.GetNamespace(), Server: sc.server}) {
gvk := hook.GroupVersionKind()
sc.setResourceDetails(&appv1.ResourceResult{
Name: hook.GetName(),
Group: gvk.Group,
Version: gvk.Version,
Kind: hook.GetKind(),
Namespace: hook.GetNamespace(),
Message: fmt.Sprintf("namespace %v is not permitted in project '%s'", hook.GetNamespace(), sc.proj.Name),
Status: appv1.ResultCodeSyncFailed,
})
return false
}
}
return true
}
// getHooks returns all Argo CD hooks, optionally filtered by ones of the specific type(s)
func (sc *syncContext) getHooks(hookTypes ...appv1.HookType) ([]*unstructured.Unstructured, error) {
var hooks []*unstructured.Unstructured
for _, hook := range sc.compareResult.hooks {
if hook.GetNamespace() == "" {
hook.SetNamespace(sc.namespace)
}
if !hookutil.IsArgoHook(hook) {
// TODO: in the future, if we want to map helm hooks to Argo CD lifecycles, we should
// include helm hooks in the returned list
continue
}
if len(hookTypes) > 0 {
match := false
for _, desiredType := range hookTypes {
if isHookType(hook, desiredType) {
match = true
break
}
}
if !match {
continue
}
}
hooks = append(hooks, hook)
}
return hooks, nil
}
// runHooks iterates & filters the target manifests for resources of the specified hook type, then
// creates the resource. Updates the sc.opRes.hooks with the current status. Returns whether or not
// we should continue to the next hook phase.
func (sc *syncContext) runHooks(hooks []*unstructured.Unstructured, hookType appv1.HookType) bool {
shouldContinue := true
for _, hook := range hooks {
if hookType == appv1.HookTypeSync && isHookType(hook, appv1.HookTypeSkip) {
// If we get here, we are invoking all sync hooks and reached a resource that is
// annotated with the Skip hook. This will update the resource details to indicate it
// was skipped due to annotation
gvk := hook.GroupVersionKind()
sc.setResourceDetails(&appv1.ResourceResult{
Name: hook.GetName(),
Group: gvk.Group,
Version: gvk.Version,
Kind: hook.GetKind(),
Namespace: hook.GetNamespace(),
Message: "Skipped",
})
continue
}
if !isHookType(hook, hookType) {
continue
}
updated, err := sc.runHook(hook, hookType)
if err != nil {
sc.setOperationPhase(appv1.OperationError, fmt.Sprintf("%s hook error: %v", hookType, err))
return false
}
if updated {
// If the result of running a hook, caused us to modify hook resource state, we should
// not proceed to the next hook phase. This is because before proceeding to the next
// phase, we want a full health assessment to happen. By returning early, we allow
// the application to get requeued into the controller workqueue, and on the next
// process iteration, a new CompareAppState() will be performed to get the most
// up-to-date live state. This enables us to accurately wait for an application to
// become Healthy before proceeding to run PostSync tasks.
shouldContinue = false
}
}
if !shouldContinue {
sc.log.Infof("Stopping after %s phase due to modifications to hook resource state", hookType)
return false
}
completed, successful := areHooksCompletedSuccessful(hookType, sc.syncRes.Resources)
if !completed {
return false
}
if !successful {
sc.setOperationPhase(appv1.OperationFailed, fmt.Sprintf("%s hook failed", hookType))
return false
}
return true
}
// syncNonHookTasks syncs or prunes the objects that are not handled by hooks using an apply sync.
// returns true if the sync was successful
func (sc *syncContext) syncNonHookTasks(syncTasks []syncTask) bool {
var nonHookTasks []syncTask
for _, task := range syncTasks {
if task.targetObj == nil {
nonHookTasks = append(nonHookTasks, task)
} else {
annotations := task.targetObj.GetAnnotations()
if annotations != nil && annotations[common.AnnotationKeyHook] != "" {
// we are doing a hook sync and this resource is annotated with a hook annotation
continue
}
// if we get here, this resource does not have any hook annotation so we
// should perform an `kubectl apply`
nonHookTasks = append(nonHookTasks, task)
}
}
return sc.doApplySync(nonHookTasks, false, sc.syncOp.SyncStrategy.Hook.Force, true)
}
// runHook runs the supplied hook and updates the hook status. Returns true if the result of
// invoking this method resulted in changes to any hook status
func (sc *syncContext) runHook(hook *unstructured.Unstructured, hookType appv1.HookType) (bool, error) {
// 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.
if hook.GetName() == "" {
postfix := strings.ToLower(fmt.Sprintf("%s-%s-%d", sc.syncRes.Revision[0:7], hookType, sc.opState.StartedAt.UTC().Unix()))
generatedName := hook.GetGenerateName()
hook = hook.DeepCopy()
hook.SetName(fmt.Sprintf("%s%s", generatedName, postfix))
}
// Check our hook statuses to see if we already completed this hook.
// If so, this method is a noop
prevStatus := sc.getHookStatus(hook, hookType)
if prevStatus != nil && prevStatus.HookPhase.Completed() {
return false, nil
}
gvk := hook.GroupVersionKind()
apiResource, err := kube.ServerResourceForGroupVersionKind(sc.disco, gvk)
if err != nil {
return false, err
}
resource := kube.ToGroupVersionResource(gvk.GroupVersion().String(), apiResource)
resIf := kube.ToResourceInterface(sc.dynamicIf, apiResource, resource, hook.GetNamespace())
var liveObj *unstructured.Unstructured
existing, err := resIf.Get(hook.GetName(), metav1.GetOptions{})
if err != nil {
if !apierr.IsNotFound(err) {
return false, fmt.Errorf("Failed to get status of %s hook %s '%s': %v", hookType, gvk, hook.GetName(), err)
}
_, err := sc.kubectl.ApplyResource(sc.config, hook, hook.GetNamespace(), false, false)
if err != nil {
return false, fmt.Errorf("Failed to create %s hook %s '%s': %v", hookType, gvk, hook.GetName(), err)
}
created, err := resIf.Get(hook.GetName(), metav1.GetOptions{})
if err != nil {
return true, fmt.Errorf("Failed to get status of %s hook %s '%s': %v", hookType, gvk, hook.GetName(), err)
}
sc.log.Infof("%s hook %s '%s' created", hookType, gvk, created.GetName())
sc.setOperationPhase(appv1.OperationRunning, fmt.Sprintf("running %s hooks", hookType))
liveObj = created
} else {
liveObj = existing
}
hookStatus := newHookStatus(liveObj, hookType)
if hookStatus.HookPhase.Completed() {
if enforceHookDeletePolicy(hook, hookStatus.HookPhase) {
err = sc.deleteHook(hook.GetName(), hook.GetNamespace(), hook.GroupVersionKind())
if err != nil {
hookStatus.HookPhase = appv1.OperationFailed
hookStatus.Message = fmt.Sprintf("failed to delete %s hook: %v", hookStatus.HookPhase, err)
}
}
}
return sc.updateHookStatus(hookStatus), nil
}
// enforceHookDeletePolicy examines the hook deletion policy of a object and deletes it based on the status
func enforceHookDeletePolicy(hook *unstructured.Unstructured, phase appv1.OperationPhase) bool {
annotations := hook.GetAnnotations()
if annotations == nil {
return false
}
deletePolicies := strings.Split(annotations[common.AnnotationKeyHookDeletePolicy], ",")
for _, dp := range deletePolicies {
policy := appv1.HookDeletePolicy(strings.TrimSpace(dp))
if policy == appv1.HookDeletePolicyHookSucceeded && phase == appv1.OperationSucceeded {
return true
}
if policy == appv1.HookDeletePolicyHookFailed && phase == appv1.OperationFailed {
return true
}
}
return false
}
// isHookType tells whether or not the supplied object is a hook of the specified type
func isHookType(hook *unstructured.Unstructured, hookType appv1.HookType) bool {
annotations := hook.GetAnnotations()
if annotations == nil {
return false
}
resHookTypes := strings.Split(annotations[common.AnnotationKeyHook], ",")
for _, ht := range resHookTypes {
if string(hookType) == strings.TrimSpace(ht) {
return true
}
}
return false
}
// newHookStatus returns a hook status from an _live_ unstructured object
func newHookStatus(hook *unstructured.Unstructured, hookType appv1.HookType) appv1.ResourceResult {
gvk := hook.GroupVersionKind()
hookStatus := appv1.ResourceResult{
Name: hook.GetName(),
Kind: hook.GetKind(),
Group: gvk.Group,
Version: gvk.Version,
HookType: hookType,
HookPhase: appv1.OperationRunning,
Namespace: hook.GetNamespace(),
}
if isBatchJob(gvk) {
updateStatusFromBatchJob(hook, &hookStatus)
} else if isArgoWorkflow(gvk) {
updateStatusFromArgoWorkflow(hook, &hookStatus)
} else if isPod(gvk) {
updateStatusFromPod(hook, &hookStatus)
} else {
hookStatus.HookPhase = appv1.OperationSucceeded
hookStatus.Message = fmt.Sprintf("%s created", hook.GetName())
}
return hookStatus
}
// isRunnable returns if the resource object is a runnable type which needs to be terminated
func isRunnable(res *appv1.ResourceResult) bool {
gvk := res.GroupVersionKind()
return isBatchJob(gvk) || isArgoWorkflow(gvk) || isPod(gvk)
}
func isBatchJob(gvk schema.GroupVersionKind) bool {
return gvk.Group == "batch" && gvk.Kind == "Job"
}
func updateStatusFromBatchJob(hook *unstructured.Unstructured, hookStatus *appv1.ResourceResult) {
var job batch.Job
err := runtime.DefaultUnstructuredConverter.FromUnstructured(hook.Object, &job)
if err != nil {
hookStatus.HookPhase = appv1.OperationError
hookStatus.Message = err.Error()
return
}
failed := false
var failMsg string
complete := false
var message string
for _, condition := range job.Status.Conditions {
switch condition.Type {
case batch.JobFailed:
failed = true
complete = true
failMsg = condition.Message
case batch.JobComplete:
complete = true
message = condition.Message
}
}
if !complete {
hookStatus.HookPhase = appv1.OperationRunning
hookStatus.Message = message
} else if failed {
hookStatus.HookPhase = appv1.OperationFailed
hookStatus.Message = failMsg
} else {
hookStatus.HookPhase = appv1.OperationSucceeded
hookStatus.Message = message
}
}
func isArgoWorkflow(gvk schema.GroupVersionKind) bool {
return gvk.Group == "argoproj.io" && gvk.Kind == "Workflow"
}
func updateStatusFromArgoWorkflow(hook *unstructured.Unstructured, hookStatus *appv1.ResourceResult) {
var wf wfv1.Workflow
err := runtime.DefaultUnstructuredConverter.FromUnstructured(hook.Object, &wf)
if err != nil {
hookStatus.HookPhase = appv1.OperationError
hookStatus.Message = err.Error()
return
}
switch wf.Status.Phase {
case wfv1.NodePending, wfv1.NodeRunning:
hookStatus.HookPhase = appv1.OperationRunning
case wfv1.NodeSucceeded:
hookStatus.HookPhase = appv1.OperationSucceeded
case wfv1.NodeFailed:
hookStatus.HookPhase = appv1.OperationFailed
case wfv1.NodeError:
hookStatus.HookPhase = appv1.OperationError
}
hookStatus.Message = wf.Status.Message
}
func isPod(gvk schema.GroupVersionKind) bool {
return gvk.Group == "" && gvk.Kind == "Pod"
}
func updateStatusFromPod(hook *unstructured.Unstructured, hookStatus *appv1.ResourceResult) {
var pod apiv1.Pod
err := runtime.DefaultUnstructuredConverter.FromUnstructured(hook.Object, &pod)
if err != nil {
hookStatus.HookPhase = appv1.OperationError
hookStatus.Message = err.Error()
return
}
getFailMessage := func(ctr *apiv1.ContainerStatus) string {
if ctr.State.Terminated != nil {
if ctr.State.Terminated.Message != "" {
return ctr.State.Terminated.Message
}
if ctr.State.Terminated.Reason == "OOMKilled" {
return ctr.State.Terminated.Reason
}
if ctr.State.Terminated.ExitCode != 0 {
return fmt.Sprintf("container %q failed with exit code %d", ctr.Name, ctr.State.Terminated.ExitCode)
}
}
return ""
}
switch pod.Status.Phase {
case apiv1.PodPending, apiv1.PodRunning:
hookStatus.HookPhase = appv1.OperationRunning
case apiv1.PodSucceeded:
hookStatus.HookPhase = appv1.OperationSucceeded
case apiv1.PodFailed:
hookStatus.HookPhase = appv1.OperationFailed
if pod.Status.Message != "" {
// Pod has a nice error message. Use that.
hookStatus.Message = pod.Status.Message
return
}
for _, ctr := range append(pod.Status.InitContainerStatuses, pod.Status.ContainerStatuses...) {
if msg := getFailMessage(&ctr); msg != "" {
hookStatus.Message = msg
return
}
}
case apiv1.PodUnknown:
hookStatus.HookPhase = appv1.OperationError
}
}
func (sc *syncContext) getHookStatus(hookObj *unstructured.Unstructured, hookType appv1.HookType) *appv1.ResourceResult {
for _, hr := range sc.syncRes.Resources {
if !hr.IsHook() {
continue
}
ns := util.FirstNonEmpty(hookObj.GetNamespace(), sc.namespace)
if hookEqual(hr, hookObj.GroupVersionKind().Group, hookObj.GetKind(), ns, hookObj.GetName(), hookType) {
return hr
}
}
return nil
}
func hookEqual(hr *appv1.ResourceResult, group, kind, namespace, name string, hookType appv1.HookType) bool {
return bool(
hr.Group == group &&
hr.Kind == kind &&
hr.Namespace == namespace &&
hr.Name == name &&
hr.HookType == hookType)
}
// updateHookStatus updates the status of a hook. Returns true if the hook was modified
func (sc *syncContext) updateHookStatus(hookStatus appv1.ResourceResult) bool {
sc.lock.Lock()
defer sc.lock.Unlock()
for i, prev := range sc.syncRes.Resources {
if !prev.IsHook() {
continue
}
if hookEqual(prev, hookStatus.Group, hookStatus.Kind, hookStatus.Namespace, hookStatus.Name, hookStatus.HookType) {
if reflect.DeepEqual(prev, hookStatus) {
return false
}
if prev.HookPhase != hookStatus.HookPhase {
sc.log.Infof("Hook %s %s/%s hookPhase: %s -> %s", hookStatus.HookType, prev.Kind, prev.Name, prev.HookPhase, hookStatus.HookPhase)
}
if prev.Status != hookStatus.Status {
sc.log.Infof("Hook %s %s/%s status: %s -> %s", hookStatus.HookType, prev.Kind, prev.Name, prev.Status, hookStatus.Status)
}
if prev.Message != hookStatus.Message {
sc.log.Infof("Hook %s %s/%s message: '%s' -> '%s'", hookStatus.HookType, prev.Kind, prev.Name, prev.Message, hookStatus.Message)
}
sc.syncRes.Resources[i] = &hookStatus
return true
}
}
sc.syncRes.Resources = append(sc.syncRes.Resources, &hookStatus)
sc.log.Infof("Set new hook %s %s/%s. phase: %s, message: %s", hookStatus.HookType, hookStatus.Kind, hookStatus.Name, hookStatus.HookPhase, hookStatus.Message)
return true
}
// areHooksCompletedSuccessful checks if all the hooks of the specified type are completed and successful
func areHooksCompletedSuccessful(hookType appv1.HookType, hookStatuses []*appv1.ResourceResult) (bool, bool) {
isSuccessful := true
for _, hookStatus := range hookStatuses {
if !hookStatus.IsHook() {
continue
}
if hookStatus.HookType != hookType {
continue
}
if !hookStatus.HookPhase.Completed() {
return false, false
}
if !hookStatus.HookPhase.Successful() {
isSuccessful = false
}
}
return true, isSuccessful
}
// terminate looks for any running jobs/workflow hooks and deletes the resource
func (sc *syncContext) terminate() {
terminateSuccessful := true
for _, hookStatus := range sc.syncRes.Resources {
if !hookStatus.IsHook() {
continue
}
if hookStatus.HookPhase.Completed() {
continue
}
if isRunnable(hookStatus) {
hookStatus.HookPhase = appv1.OperationFailed
err := sc.deleteHook(hookStatus.Name, hookStatus.Namespace, hookStatus.GroupVersionKind())
if err != nil {
hookStatus.Message = fmt.Sprintf("Failed to delete %s hook %s/%s: %v", hookStatus.HookType, hookStatus.Kind, hookStatus.Name, err)
terminateSuccessful = false
} else {
hookStatus.Message = fmt.Sprintf("Deleted %s hook %s/%s", hookStatus.HookType, hookStatus.Kind, hookStatus.Name)
}
sc.updateHookStatus(*hookStatus)
}
}
if terminateSuccessful {
sc.setOperationPhase(appv1.OperationFailed, "Operation terminated")
} else {
sc.setOperationPhase(appv1.OperationError, "Operation termination had errors")
}
}
func (sc *syncContext) deleteHook(name, namespace string, gvk schema.GroupVersionKind) error {
apiResource, err := kube.ServerResourceForGroupVersionKind(sc.disco, gvk)
if err != nil {
return err
}
resource := kube.ToGroupVersionResource(gvk.GroupVersion().String(), apiResource)
resIf := kube.ToResourceInterface(sc.dynamicIf, apiResource, resource, namespace)
propagationPolicy := metav1.DeletePropagationForeground
return resIf.Delete(name, &metav1.DeleteOptions{PropagationPolicy: &propagationPolicy})
}

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