mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-03-14 04:18:48 +01:00
Compare commits
175 Commits
release-0.
...
v1.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0bd546a07 | ||
|
|
984829fcc8 | ||
|
|
c48e27f265 | ||
|
|
5fe1447b72 | ||
|
|
539516bd43 | ||
|
|
8a57d544ff | ||
|
|
cd77e2a048 | ||
|
|
a52f766815 | ||
|
|
646fd37e16 | ||
|
|
c74ca22023 | ||
|
|
2d170be242 | ||
|
|
079101522d | ||
|
|
1bea98e01b | ||
|
|
7c09221f7c | ||
|
|
6355e910d4 | ||
|
|
891e0320d7 | ||
|
|
486323ae58 | ||
|
|
4ef875aa0b | ||
|
|
e756b8db7a | ||
|
|
8023f8ac8d | ||
|
|
803408904a | ||
|
|
702f9095da | ||
|
|
0b9ee1ae6d | ||
|
|
2f003e08ff | ||
|
|
e090857d6b | ||
|
|
4e29fff5a3 | ||
|
|
e279377696 | ||
|
|
5e52839ce3 | ||
|
|
3ca632a552 | ||
|
|
cfe55357ac | ||
|
|
0f6d768eca | ||
|
|
75330da328 | ||
|
|
a1bcbab0e5 | ||
|
|
db9272032a | ||
|
|
d6d6c655ff | ||
|
|
58acc92790 | ||
|
|
c3074c0977 | ||
|
|
af254f3047 | ||
|
|
c140976eeb | ||
|
|
05c22d4ddc | ||
|
|
3e08938a20 | ||
|
|
5937bb574d | ||
|
|
ded55b26d1 | ||
|
|
d79ed65de0 | ||
|
|
3b71bd05a4 | ||
|
|
e75a7a5dea | ||
|
|
60273ba84f | ||
|
|
c33604f2ef | ||
|
|
eea804b3f6 | ||
|
|
25edf8ac3f | ||
|
|
3ed6dc91dd | ||
|
|
13e5348177 | ||
|
|
90e44c092a | ||
|
|
8027882c1c | ||
|
|
ad9ed33f8d | ||
|
|
ddf5f0cf46 | ||
|
|
76811a992e | ||
|
|
2eac7bf457 | ||
|
|
53cbcd362d | ||
|
|
11c878b847 | ||
|
|
25d5333894 | ||
|
|
4541ca664a | ||
|
|
97422b4148 | ||
|
|
4df07a278d | ||
|
|
efa418c58b | ||
|
|
be40dbc8cc | ||
|
|
0bd7023b66 | ||
|
|
db82456dde | ||
|
|
a51441546c | ||
|
|
0bd323140d | ||
|
|
ad22949925 | ||
|
|
0726ee8995 | ||
|
|
c120004084 | ||
|
|
e15b97ee08 | ||
|
|
bbc7d39928 | ||
|
|
b53c34c3f7 | ||
|
|
d2928d5b31 | ||
|
|
7e76d6de33 | ||
|
|
3eac376a41 | ||
|
|
7084e3af5c | ||
|
|
ac3d12c746 | ||
|
|
41a3352516 | ||
|
|
311ff8caed | ||
|
|
197bbda02e | ||
|
|
56cd8fcc95 | ||
|
|
3c4b42de75 | ||
|
|
e4b8a9d895 | ||
|
|
76d25d3795 | ||
|
|
97a59ca753 | ||
|
|
544bd47e94 | ||
|
|
56916a0321 | ||
|
|
eff83a45cd | ||
|
|
9df1e27191 | ||
|
|
abe25f62d0 | ||
|
|
ad5d26f08a | ||
|
|
dea731a6b2 | ||
|
|
1d19447e8e | ||
|
|
ac938c8738 | ||
|
|
88a1c2a593 | ||
|
|
1e8db87320 | ||
|
|
7382ebce27 | ||
|
|
9988b3d8e6 | ||
|
|
6b69449175 | ||
|
|
85a5fb5a41 | ||
|
|
f5bc901dd7 | ||
|
|
4ac062d09e | ||
|
|
a15ca7259c | ||
|
|
d4ee7972ca | ||
|
|
86f6b657e2 | ||
|
|
ac7906fdea | ||
|
|
4d494f3a1b | ||
|
|
57ff5b25e4 | ||
|
|
28fa4a7571 | ||
|
|
723228598e | ||
|
|
790cdd1d45 | ||
|
|
81e21a551d | ||
|
|
7cf3f6cd19 | ||
|
|
506d95da10 | ||
|
|
36b4683e84 | ||
|
|
2becacd48d | ||
|
|
ae41425c77 | ||
|
|
59837cb513 | ||
|
|
66e5d51329 | ||
|
|
15dfa79708 | ||
|
|
896d46525e | ||
|
|
a8b70b411c | ||
|
|
cd25c4b3c9 | ||
|
|
b28d8361f5 | ||
|
|
b40ba175a3 | ||
|
|
dfa91d87cf | ||
|
|
102c24cc29 | ||
|
|
7d3b6cc8e0 | ||
|
|
9ef7064cc4 | ||
|
|
56f0ff204e | ||
|
|
af896533df | ||
|
|
aa099f3fc0 | ||
|
|
e07a877e73 | ||
|
|
1f675f4bb9 | ||
|
|
e482d74d19 | ||
|
|
50bff3e540 | ||
|
|
0d7c42ba54 | ||
|
|
ec7cbf8e15 | ||
|
|
d60fb2b449 | ||
|
|
dc989dbebc | ||
|
|
09164cae6c | ||
|
|
80f0f779db | ||
|
|
c605e892b6 | ||
|
|
80fe3e1877 | ||
|
|
8f7a7ef6a4 | ||
|
|
2aad4d0ab5 | ||
|
|
df7b0c6682 | ||
|
|
ea1519de82 | ||
|
|
b60067af97 | ||
|
|
22ddd53ea5 | ||
|
|
cafe24da86 | ||
|
|
c33acf749c | ||
|
|
dab3b688f0 | ||
|
|
a34d2c750b | ||
|
|
5210c678b9 | ||
|
|
baf157901c | ||
|
|
e457dd6f6c | ||
|
|
2724aeef32 | ||
|
|
1d3ec93ec7 | ||
|
|
fea3899f26 | ||
|
|
f016acdade | ||
|
|
0c4d5009a2 | ||
|
|
815ba879e6 | ||
|
|
3df86a7918 | ||
|
|
5e7b48c9a2 | ||
|
|
0f248e9149 | ||
|
|
461d8c980f | ||
|
|
9a7fecef06 | ||
|
|
39c63371bf | ||
|
|
80b0e1138c | ||
|
|
3acc0b3af2 |
@@ -10,28 +10,94 @@ spec:
|
||||
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
|
||||
template: ci-dind
|
||||
arguments:
|
||||
parameters:
|
||||
- name: cmd
|
||||
value: make image
|
||||
- - 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"
|
||||
- name: test-e2e
|
||||
template: ci-builder
|
||||
arguments:
|
||||
parameters:
|
||||
- name: cmd
|
||||
value: "dep ensure && make test-e2e"
|
||||
|
||||
# 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:
|
||||
@@ -44,7 +110,7 @@ spec:
|
||||
repo: "{{workflow.parameters.repo}}"
|
||||
revision: "{{workflow.parameters.revision}}"
|
||||
container:
|
||||
image: argoproj/argo-cd-ci-builder:v0.12.0
|
||||
image: argoproj/argo-cd-ci-builder:v0.13.1
|
||||
imagePullPolicy: Always
|
||||
command: [bash, -c]
|
||||
args: ["{{inputs.parameters.cmd}}"]
|
||||
@@ -73,7 +139,7 @@ spec:
|
||||
repo: "{{workflow.parameters.repo}}"
|
||||
revision: "{{workflow.parameters.revision}}"
|
||||
container:
|
||||
image: argoproj/argo-cd-ci-builder:v0.12.0
|
||||
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}}"]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
ignore:
|
||||
- "**/*.pb.go"
|
||||
- "**/*.pb.gw.go"
|
||||
- "**/*_test.go"
|
||||
- "pkg/apis/.*"
|
||||
- "pkg/client/.*"
|
||||
|
||||
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
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.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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.
|
||||
1
.github/no-response.yml
vendored
Normal file
1
.github/no-response.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
# See https://github.com/probot/no-response
|
||||
1
.github/stale.yml
vendored
Normal file
1
.github/stale.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
# See https://github.com/probot/stale
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,6 +3,7 @@
|
||||
.DS_Store
|
||||
vendor/
|
||||
dist/
|
||||
site/
|
||||
*.iml
|
||||
# delve debug binaries
|
||||
cmd/**/debug
|
||||
|
||||
21
.golangci.yml
Normal file
21
.golangci.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
run:
|
||||
deadline: 8m
|
||||
skip-files:
|
||||
- ".*\\.pb\\.go"
|
||||
skip-dirs:
|
||||
- pkg/client
|
||||
- vendor
|
||||
linter-settings:
|
||||
goimports:
|
||||
local-prefixes: github.com/argoproj/argo-cd
|
||||
linters:
|
||||
enable:
|
||||
- vet
|
||||
- gofmt
|
||||
- goimports
|
||||
- deadcode
|
||||
- varcheck
|
||||
- structcheck
|
||||
- ineffassign
|
||||
- unconvert
|
||||
- misspell
|
||||
286
CHANGELOG.md
286
CHANGELOG.md
@@ -1,5 +1,291 @@
|
||||
# Changelog
|
||||
|
||||
## v1.0.0
|
||||
|
||||
### New Features
|
||||
|
||||
#### Network View
|
||||
|
||||
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
|
||||
|
||||
* 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.
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Remove deprecated componentParameterOverrides field #1372
|
||||
|
||||
### Changes since v0.12.2
|
||||
|
||||
#### Enhancements
|
||||
|
||||
* `argocd app wait` should have `--resource` flag like sync #1206
|
||||
* Adds support for `kustomize edit set image`. Closes #1275 (#1324)
|
||||
* Allow wait to return on health or suspended (#1392)
|
||||
* Application warning when a manifest is defined twice #1070
|
||||
* Create new documentation website #1390
|
||||
* Default view should resource view instead of diff view #1354
|
||||
* Display number of errors on resource tab #1477
|
||||
* Displays resources that are being deleted as "Progressing". Closes #1410 (#1426)
|
||||
* Generate random name for grpc proxy unix socket file instead of time stamp (#1455)
|
||||
* Issue #357 - Expose application nodes networking information (#1333)
|
||||
* Issue #1404 - App controller unnecessary set namespace to cluster level resources (#1405)
|
||||
* Nils health if the resource does not provide it. Closes #1383 (#1408)
|
||||
* Perform health assessments on all resource nodes in the tree. Closes #1382 (#1422)
|
||||
* Remove deprecated componentParameterOverrides field #1372
|
||||
* Shows the health of the application. Closes #1433 (#1434)
|
||||
* Surface Service/Ingress external IPs, hostname to application #908
|
||||
* Surface pod status to tree view #1358
|
||||
* Support for customizable resource actions as Lua scripts #86
|
||||
* UI / API Errors Truncated, Time Out #1386
|
||||
* 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
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- Don't compare secrets in the CLI, since argo-cd doesn't have access to their data (#1459)
|
||||
- Dropdown menu should not have sync item for unmanaged resources #1357
|
||||
- Fixes goroutine leak. Closes #1381 (#1457)
|
||||
- Improve input style #1217
|
||||
- Issue #908 - Surface Service/Ingress external IPs, hostname to application (#1347)
|
||||
- kustomization fields are all mandatory #1504
|
||||
- Resource node details is crashing if live resource is missing $1505
|
||||
- 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
|
||||
|
||||
## v0.12.2 (2019-04-22)
|
||||
|
||||
### Changes since v0.12.1
|
||||
|
||||
- Fix racing condition in controller cache (#1498)
|
||||
- "bind: address already in use" after switching to gRPC-Web (#1451)
|
||||
- Annoying warning while using --grpc-web flag (#1420)
|
||||
- Delete helm temp directories (#1446)
|
||||
- Fix null pointer exception in secret normalization function (#1389)
|
||||
- Argo CD should not delete CRDs(#1425)
|
||||
- UI is unable to load cluster level resource manifest (#1429)
|
||||
|
||||
## v0.12.1 (2019-04-09)
|
||||
|
||||
### Changes since v0.12.0
|
||||
|
||||
- [UI] applications view blows up when user does not have permissions (#1368)
|
||||
- Add k8s objects circular dependency protection to getApp method (#1374)
|
||||
- App controller unnecessary set namespace to cluster level resources (#1404)
|
||||
- Changing SSO login URL to be a relative link so it's affected by basehref (#101) (@arnarg)
|
||||
- CLI diff should take into account resource customizations (#1294)
|
||||
- Don't try deleting application resource if it already has `deletionTimestamp` (#1406)
|
||||
- Fix invalid group filtering in 'patch-resource' command (#1319)
|
||||
- Fix null pointer dereference error in 'argocd app wait' (#1366)
|
||||
- kubectl v1.13 fails to convert extensions/NetworkPolicy (#1012)
|
||||
- Patch APIs are not audited (#1397)
|
||||
|
||||
+ 'argocd app wait' should fail sooner if app transitioned to Degraded state (#733)
|
||||
+ Add mapping to new canonical Ingress API group - kubernetes 1.14 support (#1348) (@twz123)
|
||||
+ Adds support for `kustomize edit set image`. (#1275)
|
||||
+ Allow using any name for secrets which store cluster credentials (#1218)
|
||||
+ Update argocd-util import/export to support proper backup and restore (#1048)
|
||||
|
||||
## v0.12.0 (2019-03-20)
|
||||
|
||||
### New Features
|
||||
|
||||
#### Improved UI
|
||||
|
||||
Many improvements to the UI were made, including:
|
||||
|
||||
* Table view when viewing applications
|
||||
* Filters on applications
|
||||
* Table view when viewing application resources
|
||||
* YAML editor in UI
|
||||
* Switch to text-based diff instead of json diff
|
||||
* Ability to edit application specs
|
||||
|
||||
#### Custom Health Assessments (CRD Health)
|
||||
|
||||
Argo CD has long been able to perform health assessments on resources, however this could only
|
||||
assess the health for a few native kubernetes types (deployments, statefulsets, daemonsets, etc...).
|
||||
Now, Argo CD can be extended to gain understanding of any CRD health, in the form of Lua scripts.
|
||||
For example, using this feature, Argo CD now understands the CertManager Certificate CRD and will
|
||||
report a Degraded status when there are issues with the cert.
|
||||
|
||||
#### Configuration Management Plugins
|
||||
|
||||
Argo CD introduces Config Management Plugins to support custom configuration management tools other
|
||||
than the set that Argo CD provides out-of-the-box (Helm, Kustomize, Ksonnet, Jsonnet). Using config
|
||||
management plugins, Argo CD can be configured to run specified commands to render manifests. This
|
||||
makes it possible for Argo CD to support other config management tools (kubecfg, kapitan, shell
|
||||
scripts, etc...).
|
||||
|
||||
#### High Availability
|
||||
|
||||
Argo CD is now fully HA. A set HA of manifests are provided for users who wish to run Argo CD in
|
||||
a highly available manner. NOTE: The HA installation will require at least three different nodes due
|
||||
to pod anti-affinity roles in the specs.
|
||||
|
||||
#### Improved Application Source
|
||||
|
||||
* Support for Kustomize 2
|
||||
* YAML/JSON/Jsonnet Directories can now be recursed
|
||||
* Support for Jsonnet external variables and top-level arguments
|
||||
|
||||
#### Additional Prometheus Metrics
|
||||
|
||||
Argo CD provides the following additional prometheus metrics:
|
||||
* Sync counter to track sync activity and results over time
|
||||
* Application reconciliation (refresh) performance to track Argo CD performance and controller activity
|
||||
* Argo CD API Server metrics for monitoring HTTP/gRPC requests
|
||||
|
||||
#### Fuzzy Diff Logic
|
||||
|
||||
Argo CD can now be configured to ignore known differences for resource types by specifying a json
|
||||
pointer to the field path to ignore. This helps prevent OutOfSync conditions when a user has no
|
||||
control over the manifests. Ignored differences can be configured either at an application level,
|
||||
or a system level, based on a group/kind.
|
||||
|
||||
#### Resource Exclusions
|
||||
|
||||
Argo CD can now be configured to completely ignore entire classes of resources group/kinds.
|
||||
Excluding high-volume resources improves performance and memory usage, and reduces load and
|
||||
bandwidth to the Kubernetes API server. It also allows users to fine-tune the permissions that
|
||||
Argo CD needs to a cluster by preventing Argo CD from attempting to watch resources of that
|
||||
group/kind.
|
||||
|
||||
#### gRPC-Web Support
|
||||
|
||||
The argocd CLI can be now configured to communicate to the Argo CD API server using gRPC-Web
|
||||
(HTTP1.1) using a new CLI flag `--grpc-web`. This resolves some compatibility issues users were
|
||||
experiencing with ingresses and gRPC (HTTP2), and should enable argocd CLI to work with virtually
|
||||
any load balancer, ingress controller, or API gateway.
|
||||
|
||||
#### CLI features
|
||||
|
||||
Argo CD introduces some additional CLI commands:
|
||||
|
||||
* `argocd app edit APPNAME` - to edit an application spec using preferred EDITOR
|
||||
* `argocd proj edit PROJNAME` - to edit an project spec using preferred EDITOR
|
||||
* `argocd app patch APPNAME` - to patch an application spec
|
||||
* `argocd app patch-resource APPNAME` - to patch a specific resource which is part of an application
|
||||
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
#### Label selector changes, dex-server rename
|
||||
|
||||
The label selectors for deployments were been renamed to use kubernetes common labels
|
||||
(`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
|
||||
instructions to upgrade from v0.11 to v0.12 without introducing downtime:
|
||||
|
||||
```
|
||||
# delete the deployments with cascade=false. this orphan the replicasets, but leaves the pods running
|
||||
kubectl delete deploy --cascade=false argocd-server argocd-repo-server argocd-application-controller
|
||||
|
||||
# apply the new manifests and wait for them to finish rolling out
|
||||
kubectl apply <new install manifests>
|
||||
kubectl rollout status deploy/argocd-application-controller
|
||||
kubectl rollout status deploy/argocd-repo-server
|
||||
kubectl rollout status deploy/argocd-application-controller
|
||||
|
||||
# delete old replicasets which are using the legacy label
|
||||
kubectl delete rs -l app=argocd-server
|
||||
kubectl delete rs -l app=argocd-repo-server
|
||||
kubectl delete rs -l app=argocd-application-controller
|
||||
|
||||
# delete the legacy dex-server which was renamed
|
||||
kubectl delete deploy dex-server
|
||||
```
|
||||
|
||||
#### Deprecation of spec.source.componentParameterOverrides
|
||||
|
||||
For declarative application specs, the `spec.source.componentParameterOverrides` field is now
|
||||
deprecated in favor of application source specific config. They are replaced with new fields
|
||||
specific to their respective config management. For example, a Helm application spec using the
|
||||
legacy field:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
source:
|
||||
componentParameterOverrides:
|
||||
- name: image.tag
|
||||
value: v1.2
|
||||
```
|
||||
|
||||
should move to:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
source:
|
||||
helm:
|
||||
parameters:
|
||||
- name: image.tag
|
||||
value: v1.2
|
||||
```
|
||||
|
||||
Argo CD will automatically duplicate the legacy field values to the new locations (and vice versa)
|
||||
as part of automatic migration. The legacy `spec.source.componentParameterOverrides` field will be
|
||||
kept around for the v0.12 release (for migration purposes) and will be removed in the next Argo CD
|
||||
release.
|
||||
|
||||
#### Removal of spec.source.environment and spec.source.valuesFiles
|
||||
|
||||
The `spec.source.environment` and `spec.source.valuesFiles` fields, which were deprecated in v0.11,
|
||||
are now completely removed from the Application spec.
|
||||
|
||||
|
||||
#### API/CLI compatibility
|
||||
|
||||
Due to API spec changes related to the deprecation of componentParameterOverrides, Argo CD v0.12
|
||||
has a minimum client version of v0.12.0. Older CLI clients will be rejected.
|
||||
|
||||
|
||||
### Changes since v0.11:
|
||||
+ Improved UI
|
||||
+ Custom Health Assessments (CRD Health)
|
||||
+ Configuration Management Plugins
|
||||
+ High Availability
|
||||
+ Fuzzy Diff Logic
|
||||
+ Resource Exclusions
|
||||
+ gRPC-Web Support
|
||||
+ CLI features
|
||||
+ Additional prometheus metrics
|
||||
+ Sample Grafana dashboard (#1277) (@hartman17)
|
||||
+ Support for Kustomize 2
|
||||
+ YAML/JSON/Jsonnet Directories can now be recursed
|
||||
+ Support for Jsonnet external variables and top-level arguments
|
||||
+ Optimized reconciliation performance for applications with very active resources (#1267)
|
||||
+ Support a separate OAuth2 CLI clientID different from server (#1307)
|
||||
+ argocd diff: only print to stdout, if there is a diff + exit code (#1288) (@marcb1)
|
||||
+ Detection and handling of duplicated resource definitions (#1284)
|
||||
+ Support kustomize apps with remote bases in private repos in the same host (#1264)
|
||||
+ Support patching resource using REST API (#1186)
|
||||
* Deprecate componentParameterOverrides in favor of source specific config (#1207)
|
||||
* Support talking to Dex using local cluster address instead of public address (#1211)
|
||||
* Use Recreate deployment strategy for controller (#1315)
|
||||
* Honor os environment variables for helm commands (#1306) (@1337andre)
|
||||
* Disable CGO_ENABLED for server/controller binaries (#1286)
|
||||
* Documentation fixes and improvements (@twz123, @yann-soubeyrand, @OmerKahani, @dulltz)
|
||||
- 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 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)
|
||||
- invalidate repo cache on delete (#1182) (@narg95)
|
||||
|
||||
## v0.11.2 (2019-02-19)
|
||||
+ Adds client retry. Fixes #959 (#1119)
|
||||
- Prevent deletion hotloop (#1115)
|
||||
|
||||
18
Dockerfile
18
Dockerfile
@@ -42,8 +42,7 @@ RUN wget https://github.com/gobuffalo/packr/releases/download/v${PACKR_VERSION}/
|
||||
|
||||
# Install kubectl
|
||||
# NOTE: keep the version synced with https://storage.googleapis.com/kubernetes-release/release/stable.txt
|
||||
# Keep version at 1.12.X until https://github.com/argoproj/argo-cd/issues/1012 is resolved
|
||||
ENV KUBECTL_VERSION=1.12.4
|
||||
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
|
||||
@@ -69,7 +68,7 @@ RUN curl -L -o /usr/local/bin/kustomize1 https://github.com/kubernetes-sigs/kust
|
||||
kustomize1 version
|
||||
|
||||
|
||||
ENV KUSTOMIZE_VERSION=2.0.2
|
||||
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
|
||||
@@ -79,6 +78,17 @@ 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
|
||||
@@ -94,6 +104,8 @@ RUN groupadd -g 999 argocd && \
|
||||
apt-get clean && \
|
||||
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 --from=builder /usr/local/bin/ks /usr/local/bin/ks
|
||||
COPY --from=builder /usr/local/bin/helm /usr/local/bin/helm
|
||||
COPY --from=builder /usr/local/bin/kubectl /usr/local/bin/kubectl
|
||||
|
||||
78
Gopkg.lock
generated
78
Gopkg.lock
generated
@@ -50,22 +50,11 @@
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:a74730e052a45a3fab1d310fdef2ec17ae3d6af16228421e238320846f2aaec8"
|
||||
name = "github.com/alecthomas/template"
|
||||
packages = [
|
||||
".",
|
||||
"parse",
|
||||
]
|
||||
digest = "1:0cac9c70f3308d54ed601878aa66423eb95c374616fdd7d2ea4e2d18b045ae62"
|
||||
name = "github.com/ant31/crd-validation"
|
||||
packages = ["pkg"]
|
||||
pruneopts = ""
|
||||
revision = "a0175ee3bccc567396460bf5acd36800cb10c49c"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:8483994d21404c8a1d489f6be756e25bfccd3b45d65821f25695577791a08e68"
|
||||
name = "github.com/alecthomas/units"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "2efee857e7cfd4f3d0138cc3cbb1b4966962b93a"
|
||||
revision = "38f6a293f140402953f884b015014e0cd519bbb3"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
@@ -760,7 +749,6 @@
|
||||
packages = [
|
||||
"expfmt",
|
||||
"internal/bitbucket.org/ww/goautoneg",
|
||||
"log",
|
||||
"model",
|
||||
]
|
||||
pruneopts = ""
|
||||
@@ -992,8 +980,6 @@
|
||||
packages = [
|
||||
"unix",
|
||||
"windows",
|
||||
"windows/registry",
|
||||
"windows/svc/eventlog",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "d0be0721c37eeb5299f245a996a483160fc36940"
|
||||
@@ -1115,14 +1101,6 @@
|
||||
revision = "8dea3dc473e90c8179e519d91302d0597c0ca1d1"
|
||||
version = "v1.15.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:15d017551627c8bb091bde628215b2861bed128855343fdd570c62d08871f6e1"
|
||||
name = "gopkg.in/alecthomas/kingpin.v2"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "947dcec5ba9c011838740e680966fd7087a71d0d"
|
||||
version = "v2.2.6"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:bf7444e1e6a36e633f4f1624a67b9e4734cfb879c27ac0a2082ac16aff8462ac"
|
||||
name = "gopkg.in/go-playground/webhooks.v3"
|
||||
@@ -1248,7 +1226,7 @@
|
||||
|
||||
[[projects]]
|
||||
branch = "release-1.12"
|
||||
digest = "1:ed04c5203ecbf6358fb6a774b0ecd40ea992d6dcc42adc1d3b7cf9eceb66b6c8"
|
||||
digest = "1:3e3e9df293bd6f9fd64effc9fa1f0edcd97e6c74145cd9ab05d35719004dc41f"
|
||||
name = "k8s.io/api"
|
||||
packages = [
|
||||
"admission/v1beta1",
|
||||
@@ -1286,22 +1264,19 @@
|
||||
"storage/v1beta1",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "475331a8afff5587f47d0470a93f79c60c573c03"
|
||||
revision = "6db15a15d2d3874a6c3ddb2140ac9f3bc7058428"
|
||||
|
||||
[[projects]]
|
||||
branch = "release-1.12"
|
||||
digest = "1:39be82077450762b5e14b5268e679a14ac0e9c7d3286e2fcface437556a29e4c"
|
||||
branch = "master"
|
||||
digest = "1:49e0fcdcaeaf937c6c608d1da19eb80de74fe990021278d49d46e10288659be6"
|
||||
name = "k8s.io/apiextensions-apiserver"
|
||||
packages = [
|
||||
"pkg/apis/apiextensions",
|
||||
"pkg/apis/apiextensions/v1beta1",
|
||||
"pkg/client/clientset/clientset",
|
||||
"pkg/client/clientset/clientset/scheme",
|
||||
"pkg/client/clientset/clientset/typed/apiextensions/v1beta1",
|
||||
"pkg/features",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "ca1024863b48cf0701229109df75ac5f0bb4907e"
|
||||
revision = "7f7d2b94eca3a7a1c49840e119a8bc03c3afb1e3"
|
||||
|
||||
[[projects]]
|
||||
branch = "release-1.12"
|
||||
@@ -1360,15 +1335,15 @@
|
||||
revision = "f71dbbc36e126f5a371b85f6cca96bc8c57db2b6"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:cb3ac215bfac54696f64a6e5c46524a7fc0f7a8f9b7a22cccb2e1e83ac2d013f"
|
||||
branch = "release-1.12"
|
||||
digest = "1:b2c55ff9df6d053e40094b943f949c257c3f7dcdbb035c11487c93c96df9eade"
|
||||
name = "k8s.io/apiserver"
|
||||
packages = [
|
||||
"pkg/features",
|
||||
"pkg/util/feature",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "19cf388d0a374e95329bf7d98e9bfd7da8853be0"
|
||||
revision = "5e1c1f41ee34b3bb153f928f8c91c2a6dd9482a9"
|
||||
|
||||
[[projects]]
|
||||
branch = "release-9.0"
|
||||
@@ -1378,6 +1353,7 @@
|
||||
"discovery",
|
||||
"discovery/fake",
|
||||
"dynamic",
|
||||
"dynamic/fake",
|
||||
"informers/core/v1",
|
||||
"informers/internalinterfaces",
|
||||
"kubernetes",
|
||||
@@ -1485,7 +1461,7 @@
|
||||
|
||||
[[projects]]
|
||||
branch = "release-1.12"
|
||||
digest = "1:e6fffdf0dfeb0d189a7c6d735e76e7564685d3b6513f8b19d3651191cb6b084b"
|
||||
digest = "1:8108815d1aef9159daabdb3f0fcef04a88765536daf0c0cd29a31fdba135ee54"
|
||||
name = "k8s.io/code-generator"
|
||||
packages = [
|
||||
"cmd/go-to-protobuf",
|
||||
@@ -1494,21 +1470,22 @@
|
||||
"third_party/forked/golang/reflect",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "3dcf91f64f638563e5106f21f50c31fa361c918d"
|
||||
revision = "b1289fc74931d4b6b04bd1a259acfc88a2cb0a66"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:15710582bd5ceff07eee4726884f75f97f90366fde9307b8dd09500c75722456"
|
||||
digest = "1:6a2a63e09a59caff3fd2d36d69b7b92c2fe7cf783390f0b7349fb330820f9a8e"
|
||||
name = "k8s.io/gengo"
|
||||
packages = [
|
||||
"args",
|
||||
"examples/set-gen/sets",
|
||||
"generator",
|
||||
"namer",
|
||||
"parser",
|
||||
"types",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "8394c995ab8fbe52216f38d0e1a37de36d820528"
|
||||
revision = "e17681d19d3ac4837a019ece36c2a0ec31ffe985"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:4f5eb833037cc0ba0bf8fe9cae6be9df62c19dd1c869415275c708daa8ccfda5"
|
||||
@@ -1519,15 +1496,19 @@
|
||||
version = "v0.1.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:9a648ff9eb89673d2870c22fc011ec5db0fcff6c4e5174a650298e51be71bbf1"
|
||||
digest = "1:aae856b28533bfdb39112514290f7722d00a55a99d2d0b897c6ee82e6feb87b3"
|
||||
name = "k8s.io/kube-openapi"
|
||||
packages = [
|
||||
"cmd/openapi-gen",
|
||||
"cmd/openapi-gen/args",
|
||||
"pkg/common",
|
||||
"pkg/generators",
|
||||
"pkg/generators/rules",
|
||||
"pkg/util/proto",
|
||||
"pkg/util/sets",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "50ae88d24ede7b8bad68e23c805b5d3da5c8abaf"
|
||||
revision = "master"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:6061aa42761235df375f20fa4a1aa6d1845cba3687575f3adb2ef3f3bc540af5"
|
||||
@@ -1566,6 +1547,7 @@
|
||||
input-imports = [
|
||||
"github.com/Masterminds/semver",
|
||||
"github.com/TomOnTime/utfutil",
|
||||
"github.com/ant31/crd-validation/pkg",
|
||||
"github.com/argoproj/argo/pkg/apis/workflow/v1alpha1",
|
||||
"github.com/argoproj/argo/util",
|
||||
"github.com/argoproj/pkg/exec",
|
||||
@@ -1580,6 +1562,7 @@
|
||||
"github.com/ghodss/yaml",
|
||||
"github.com/go-openapi/loads",
|
||||
"github.com/go-openapi/runtime/middleware",
|
||||
"github.com/go-openapi/spec",
|
||||
"github.com/go-redis/cache",
|
||||
"github.com/go-redis/redis",
|
||||
"github.com/gobuffalo/packr",
|
||||
@@ -1588,6 +1571,7 @@
|
||||
"github.com/gogo/protobuf/proto",
|
||||
"github.com/gogo/protobuf/protoc-gen-gofast",
|
||||
"github.com/gogo/protobuf/protoc-gen-gogofast",
|
||||
"github.com/gogo/protobuf/sortkeys",
|
||||
"github.com/golang/protobuf/proto",
|
||||
"github.com/golang/protobuf/protoc-gen-go",
|
||||
"github.com/golang/protobuf/ptypes/empty",
|
||||
@@ -1610,7 +1594,6 @@
|
||||
"github.com/pkg/errors",
|
||||
"github.com/prometheus/client_golang/prometheus",
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp",
|
||||
"github.com/prometheus/common/log",
|
||||
"github.com/sirupsen/logrus",
|
||||
"github.com/skratchdot/open-golang/open",
|
||||
"github.com/soheilhy/cmux",
|
||||
@@ -1654,7 +1637,7 @@
|
||||
"k8s.io/api/core/v1",
|
||||
"k8s.io/api/extensions/v1beta1",
|
||||
"k8s.io/api/rbac/v1",
|
||||
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset",
|
||||
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1",
|
||||
"k8s.io/apimachinery/pkg/api/equality",
|
||||
"k8s.io/apimachinery/pkg/api/errors",
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1",
|
||||
@@ -1673,6 +1656,7 @@
|
||||
"k8s.io/client-go/discovery",
|
||||
"k8s.io/client-go/discovery/fake",
|
||||
"k8s.io/client-go/dynamic",
|
||||
"k8s.io/client-go/dynamic/fake",
|
||||
"k8s.io/client-go/informers/core/v1",
|
||||
"k8s.io/client-go/kubernetes",
|
||||
"k8s.io/client-go/kubernetes/fake",
|
||||
@@ -1687,6 +1671,8 @@
|
||||
"k8s.io/client-go/util/flowcontrol",
|
||||
"k8s.io/client-go/util/workqueue",
|
||||
"k8s.io/code-generator/cmd/go-to-protobuf",
|
||||
"k8s.io/kube-openapi/cmd/openapi-gen",
|
||||
"k8s.io/kube-openapi/pkg/common",
|
||||
"k8s.io/kubernetes/pkg/api/v1/pod",
|
||||
"k8s.io/kubernetes/pkg/apis/apps",
|
||||
"k8s.io/kubernetes/pkg/apis/batch",
|
||||
|
||||
@@ -7,6 +7,7 @@ required = [
|
||||
"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",
|
||||
@@ -38,10 +39,6 @@ required = [
|
||||
branch = "release-1.12"
|
||||
name = "k8s.io/api"
|
||||
|
||||
[[constraint]]
|
||||
name = "k8s.io/apiextensions-apiserver"
|
||||
branch = "release-1.12"
|
||||
|
||||
[[constraint]]
|
||||
branch = "release-1.12"
|
||||
name = "k8s.io/code-generator"
|
||||
@@ -65,3 +62,7 @@ required = [
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/yudai/gojsondiff"
|
||||
|
||||
[[override]]
|
||||
revision = "master"
|
||||
name = "k8s.io/kube-openapi"
|
||||
|
||||
52
Makefile
52
Makefile
@@ -9,6 +9,9 @@ GIT_COMMIT=$(shell git rev-parse HEAD)
|
||||
GIT_TAG=$(shell if [ -z "`git status --porcelain`" ]; then git describe --exact-match --tags HEAD 2>/dev/null; fi)
|
||||
GIT_TREE_STATE=$(shell if [ -z "`git status --porcelain`" ]; then echo "clean" ; else echo "dirty"; fi)
|
||||
PACKR_CMD=$(shell if [ "`which packr`" ]; then echo "packr"; else echo "go run vendor/github.com/gobuffalo/packr/packr/main.go"; fi)
|
||||
TEST_CMD=$(shell [ "`which gotestsum`" != "" ] && echo gotestsum -- || echo go test)
|
||||
|
||||
PATH:=$(PATH):$(PWD)/hack
|
||||
|
||||
# docker image publishing options
|
||||
DOCKER_PUSH=false
|
||||
@@ -50,12 +53,16 @@ all: cli image argocd-util
|
||||
protogen:
|
||||
./hack/generate-proto.sh
|
||||
|
||||
.PHONY: openapigen
|
||||
openapigen:
|
||||
./hack/update-openapi.sh
|
||||
|
||||
.PHONY: clientgen
|
||||
clientgen:
|
||||
./hack/update-codegen.sh
|
||||
|
||||
.PHONY: codegen
|
||||
codegen: protogen clientgen format-code
|
||||
codegen: protogen clientgen openapigen
|
||||
|
||||
.PHONY: cli
|
||||
cli: clean-debug
|
||||
@@ -81,15 +88,15 @@ manifests:
|
||||
# files into the go binary
|
||||
.PHONY: server
|
||||
server: clean-debug
|
||||
${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-server ./cmd/argocd-server
|
||||
CGO_ENABLED=0 ${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-server ./cmd/argocd-server
|
||||
|
||||
.PHONY: repo-server
|
||||
repo-server:
|
||||
go 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:
|
||||
${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-application-controller ./cmd/argocd-application-controller
|
||||
CGO_ENABLED=0 ${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-application-controller ./cmd/argocd-application-controller
|
||||
|
||||
.PHONY: packr
|
||||
packr:
|
||||
@@ -119,26 +126,35 @@ endif
|
||||
.PHONY: builder-image
|
||||
builder-image:
|
||||
docker build -t $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG) --target builder .
|
||||
docker push $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG)
|
||||
|
||||
.PHONY: dep-ensure
|
||||
dep-ensure:
|
||||
dep ensure
|
||||
|
||||
.PHONY: format-code
|
||||
format-code:
|
||||
./hack/format-code.sh
|
||||
dep ensure -no-vendor
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
gometalinter.v2 --config gometalinter.json ./...
|
||||
golangci-lint run --fix
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
go build `go list ./... | grep -v resource_customizations`
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test -covermode=count -coverprofile=coverage.out `go list ./... | grep -v "github.com/argoproj/argo-cd/test/e2e"`
|
||||
$(TEST_CMD) -covermode=count -coverprofile=coverage.out `go list ./... | grep -v "github.com/argoproj/argo-cd/test/e2e"`
|
||||
|
||||
.PHONY: test-e2e
|
||||
test-e2e:
|
||||
go test -v -failfast -timeout 20m ./test/e2e
|
||||
test-e2e: cli
|
||||
$(TEST_CMD) -v -failfast -timeout 20m ./test/e2e
|
||||
|
||||
.PHONY: start-e2e
|
||||
start-e2e: cli
|
||||
killall goreman || true
|
||||
kubectl create ns argocd-e2e || true
|
||||
kubens argocd-e2e
|
||||
kustomize build test/manifests/base | kubectl apply -f -
|
||||
make start
|
||||
|
||||
# Cleans VSCode debug.test files from sub-dirs to prevent them from being included in packr boxes
|
||||
.PHONY: clean-debug
|
||||
@@ -149,8 +165,14 @@ clean-debug:
|
||||
clean: clean-debug
|
||||
-rm -rf ${CURRENT_DIR}/dist
|
||||
|
||||
.PHONY: start
|
||||
start:
|
||||
killall goreman || true
|
||||
kubens argocd
|
||||
goreman start
|
||||
|
||||
.PHONY: pre-commit
|
||||
pre-commit: dep-ensure codegen format-code test lint
|
||||
pre-commit: dep-ensure codegen build lint test
|
||||
|
||||
.PHONY: release-precheck
|
||||
release-precheck: manifests
|
||||
@@ -159,4 +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: release-precheck precheckin image release-cli
|
||||
release: release-precheck pre-commit image release-cli
|
||||
|
||||
8
Procfile
8
Procfile
@@ -1,5 +1,5 @@
|
||||
controller: go run ./cmd/argocd-application-controller/main.go --redis localhost:6379 --repo-server localhost:8081
|
||||
api-server: go run ./cmd/argocd-server/main.go --redis localhost:6379 --disable-auth --insecure --dex-server http://localhost:5556 --repo-server localhost:8081 --staticassets ../argo-cd-ui/dist/app
|
||||
repo-server: go run ./cmd/argocd-repo-server/main.go --loglevel debug --redis localhost:6379
|
||||
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 -i -p 6379:6379 redis:5.0.3-alpine --save "" --appendonly no
|
||||
redis: docker run --rm --name argocd-redis -i -p 6379:6379 redis:5.0.3-alpine --save ""--appendonly no
|
||||
|
||||
98
README.md
98
README.md
@@ -7,100 +7,26 @@
|
||||
|
||||
Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes.
|
||||
|
||||

|
||||

|
||||
|
||||
## Why Argo CD?
|
||||
|
||||
Application definitions, configurations, and environments should be declarative and version controlled.
|
||||
Application deployment and lifecycle management should be automated, auditable, and easy to understand.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Quickstart
|
||||
## Who uses Argo CD?
|
||||
|
||||
```bash
|
||||
kubectl create namespace argocd
|
||||
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
|
||||
```
|
||||
Organizations below are **officially** using Argo CD. Please send a PR with your organization name if you are using Argo CD.
|
||||
|
||||
Follow our [getting started guide](docs/getting_started.md). Further [documentation](docs/)
|
||||
is provided for additional features.
|
||||
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/)
|
||||
|
||||
## How it works
|
||||
## Documentation
|
||||
|
||||
Argo CD follows the **GitOps** pattern of using git repositories as the source of truth for defining
|
||||
the desired application state. Kubernetes manifests can be specified in several ways:
|
||||
* [kustomize](https://kustomize.io) applications
|
||||
* [helm](https://helm.sh) charts
|
||||
* [ksonnet](https://ksonnet.io) applications
|
||||
* [jsonnet](https://jsonnet.org) files
|
||||
* Plain directory of YAML/json manifests
|
||||
* Any custom config management tool configured as a config management plugin
|
||||
|
||||
Argo CD automates the deployment of the desired application states in the specified target environments.
|
||||
Application deployments can track updates to branches, tags, or pinned to a specific version of
|
||||
manifests at a git commit. See [tracking strategies](docs/tracking_strategies.md) for additional
|
||||
details about the different tracking strategies available.
|
||||
|
||||
For a quick 10 minute overview of Argo CD, check out the demo presented to the Sig Apps community
|
||||
meeting:
|
||||
[](https://youtu.be/aWDIQMbp1cc?t=1m4s)
|
||||
|
||||
|
||||
## Architecture
|
||||
|
||||

|
||||
|
||||
Argo CD is implemented as a kubernetes controller which continuously monitors running applications
|
||||
and compares the current, live state against the desired target state (as specified in the git repo).
|
||||
A deployed application whose live state deviates from the target state is considered `OutOfSync`.
|
||||
Argo CD reports & visualizes the differences, while providing facilities to automatically or
|
||||
manually sync the live state back to the desired target state. Any modifications made to the desired
|
||||
target state in the git repo can be automatically applied and reflected in the specified target
|
||||
environments.
|
||||
|
||||
For additional details, see [architecture overview](docs/architecture.md).
|
||||
|
||||
## Features
|
||||
|
||||
* Automated deployment of applications to specified target environments
|
||||
* Support for multiple config management/templating tools (Kustomize, Helm, Ksonnet, Jsonnet, plain-YAML)
|
||||
* Ability to manage and deploy to multiple clusters
|
||||
* SSO Integration (OIDC, OAuth2, LDAP, SAML 2.0, GitHub, GitLab, Microsoft, LinkedIn)
|
||||
* Multi-tenancy and RBAC policies for authorization
|
||||
* Rollback/Roll-anywhere to any application configuration committed in git repository
|
||||
* Health status analysis of application resources
|
||||
* Automated configuration drift detection and visualization
|
||||
* Automated or manual syncing of applications to its desired state
|
||||
* Web UI which provides real-time view of application activity
|
||||
* CLI for automation and CI integration
|
||||
* Webhook integration (GitHub, BitBucket, GitLab)
|
||||
* Access tokens for automation
|
||||
* PreSync, Sync, PostSync hooks to support complex application rollouts (e.g.blue/green & canary upgrades)
|
||||
* Audit trails for application events and API calls
|
||||
* Prometheus metrics
|
||||
* Parameter overrides for overriding ksonnet/helm parameters in git
|
||||
|
||||
## Community Blogs and Presentations
|
||||
* GitOps with Argo CD: [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/)
|
||||
* KubeCon talk: [CI/CD in Light Speed with K8s and Argo CD](https://www.youtube.com/watch?v=OdzH82VpMwI&feature=youtu.be)
|
||||
* KubeCon talk: [Machine Learning as Code](https://www.youtube.com/watch?v=VXrGp5er1ZE&t=0s&index=135&list=PLj6h78yzYM2PZf9eA7bhWnIh_mK1vyOfU)
|
||||
* Among other things, desribes how Kubeflow uses Argo CD to implement GitOPs for ML
|
||||
* SIG Apps demo: [Argo CD - GitOps Continuous Delivery for Kubernetes](https://www.youtube.com/watch?v=aWDIQMbp1cc&feature=youtu.be&t=1m4s)
|
||||
|
||||
## Project Resources
|
||||
* Argo GitHub: https://github.com/argoproj
|
||||
* Argo Slack: [click here to join](https://argoproj.github.io/community/join-slack)
|
||||
* Argo website: https://argoproj.github.io/
|
||||
|
||||
## Development Status
|
||||
* Argo CD is actively developed and is being used in production to deploy SaaS services at Intuit
|
||||
|
||||
## Roadmap
|
||||
|
||||
### v0.12
|
||||
* Support for custom K8S manifest templating engines
|
||||
* Support for custom health assessments (e.g. CRD health)
|
||||
* Improved prometheus metrics
|
||||
* Higher availability
|
||||
* UI improvements
|
||||
To learn more about Argo CD [go to the complete documentation](https://argoproj.github.io/argo-cd/).
|
||||
|
||||
@@ -68,6 +68,11 @@
|
||||
},
|
||||
"name": "project",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "resourceVersion",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -180,7 +185,7 @@
|
||||
"200": {
|
||||
"description": "(empty)",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/applicationResourceTreeResponse"
|
||||
"$ref": "#/definitions/v1alpha1ApplicationTree"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,6 +217,11 @@
|
||||
},
|
||||
"name": "project",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "resourceVersion",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -550,6 +560,85 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/applications/{name}/resource/actions": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"ApplicationService"
|
||||
],
|
||||
"operationId": "ListResourceActions",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "namespace",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "resourceName",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "version",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "group",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "kind",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "(empty)",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/applicationResourceActionsListResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"ApplicationService"
|
||||
],
|
||||
"operationId": "RunResourceAction",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "(empty)",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/applicationApplicationResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/applications/{name}/rollback": {
|
||||
"post": {
|
||||
"tags": [
|
||||
@@ -698,33 +787,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/clusters-kubeconfig": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"ClusterService"
|
||||
],
|
||||
"summary": "CreateFromKubeConfig installs the argocd-manager service account into the cluster specified in the given kubeconfig and context",
|
||||
"operationId": "CreateFromKubeConfig",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/clusterClusterCreateFromKubeConfigRequest"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "(empty)",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/v1alpha1Cluster"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/clusters/{cluster.server}": {
|
||||
"put": {
|
||||
"tags": [
|
||||
@@ -1207,6 +1269,11 @@
|
||||
},
|
||||
"name": "helm.valueFiles",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "ksonnet.environment",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -1303,6 +1370,11 @@
|
||||
},
|
||||
"name": "project",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "resourceVersion",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -1445,36 +1517,17 @@
|
||||
"applicationOperationTerminateResponse": {
|
||||
"type": "object"
|
||||
},
|
||||
"applicationResourceTreeResponse": {
|
||||
"applicationResourceActionsListResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"actions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v1alpha1ResourceNode"
|
||||
"$ref": "#/definitions/v1alpha1ResourceAction"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"clusterClusterCreateFromKubeConfigRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"context": {
|
||||
"type": "string"
|
||||
},
|
||||
"inCluster": {
|
||||
"type": "boolean",
|
||||
"format": "boolean"
|
||||
},
|
||||
"kubeconfig": {
|
||||
"type": "string"
|
||||
},
|
||||
"upsert": {
|
||||
"type": "boolean",
|
||||
"format": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"clusterClusterResponse": {
|
||||
"type": "object"
|
||||
},
|
||||
@@ -1503,6 +1556,9 @@
|
||||
"clusterOIDCConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cliClientID": {
|
||||
"type": "string"
|
||||
},
|
||||
"clientID": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -1511,6 +1567,12 @@
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"scopes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1526,6 +1588,12 @@
|
||||
"oidcConfig": {
|
||||
"$ref": "#/definitions/clusterOIDCConfig"
|
||||
},
|
||||
"resourceOverrides": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/definitions/v1alpha1ResourceOverride"
|
||||
}
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -1631,6 +1699,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repositoryKsonnetAppDetailsQuery": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"environment": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repositoryKsonnetAppSpec": {
|
||||
"type": "object",
|
||||
"title": "KsonnetAppSpec contains Ksonnet app response\nThis roughly reflects: ksonnet/ksonnet/metadata/app/schema.go",
|
||||
@@ -1693,11 +1769,19 @@
|
||||
"title": "KustomizeAppSpec contains kustomize app name and path in source repo",
|
||||
"properties": {
|
||||
"imageTags": {
|
||||
"description": "imageTags is a list of available image tags. This is only populated for Kustomize 1.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v1alpha1KustomizeImageTag"
|
||||
}
|
||||
},
|
||||
"images": {
|
||||
"description": "images is a list of available images. This is only populated for Kustomize 2.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
@@ -1950,6 +2034,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1LoadBalancerIngress": {
|
||||
"description": "LoadBalancerIngress represents the status of a load-balancer ingress point:\ntraffic intended for the service should be sent to an ingress point.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hostname": {
|
||||
"type": "string",
|
||||
"title": "Hostname is set for load-balancer ingress points that are DNS based\n(typically AWS load-balancers)\n+optional"
|
||||
},
|
||||
"ip": {
|
||||
"type": "string",
|
||||
"title": "IP is set for load-balancer ingress points that are IP based\n(typically GCE or OpenStack load-balancers)\n+optional"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1MicroTime": {
|
||||
"description": "MicroTime is version of Time with microsecond level precision.\n\n+protobuf.options.marshal=false\n+protobuf.as=Timestamp\n+protobuf.options.(gogoproto.goproto_stringer)=false",
|
||||
"type": "object",
|
||||
@@ -2361,13 +2459,6 @@
|
||||
"description": "ApplicationSource contains information about github repository, path within repository and target application environment.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"componentParameterOverrides": {
|
||||
"type": "array",
|
||||
"title": "ComponentParameterOverrides are a list of parameter override values\nDEPRECATED: use app source specific config instead",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v1alpha1ComponentParameter"
|
||||
}
|
||||
},
|
||||
"directory": {
|
||||
"$ref": "#/definitions/v1alpha1ApplicationSourceDirectory"
|
||||
},
|
||||
@@ -2477,6 +2568,13 @@
|
||||
"$ref": "#/definitions/v1alpha1KustomizeImageTag"
|
||||
}
|
||||
},
|
||||
"images": {
|
||||
"type": "array",
|
||||
"title": "Images are kustomize 2.0 image overrides",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"namePrefix": {
|
||||
"type": "string",
|
||||
"title": "NamePrefix is a prefix appended to resources for kustomize apps"
|
||||
@@ -2543,17 +2641,57 @@
|
||||
"operationState": {
|
||||
"$ref": "#/definitions/v1alpha1OperationState"
|
||||
},
|
||||
"reconciledAt": {
|
||||
"$ref": "#/definitions/v1Time"
|
||||
},
|
||||
"resources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v1alpha1ResourceStatus"
|
||||
}
|
||||
},
|
||||
"sourceType": {
|
||||
"type": "string"
|
||||
},
|
||||
"summary": {
|
||||
"$ref": "#/definitions/v1alpha1ApplicationSummary"
|
||||
},
|
||||
"sync": {
|
||||
"$ref": "#/definitions/v1alpha1SyncStatus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1ApplicationSummary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"externalURLs": {
|
||||
"description": "ExternalURLs holds all external URLs of application child resources.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"images": {
|
||||
"description": "Images holds all images of application child resources.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1ApplicationTree": {
|
||||
"type": "object",
|
||||
"title": "ApplicationTree holds nodes which belongs to the application",
|
||||
"properties": {
|
||||
"nodes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v1alpha1ResourceNode"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1ApplicationWatchEvent": {
|
||||
"description": "ApplicationWatchEvent contains information about application change.",
|
||||
"type": "object",
|
||||
@@ -2636,21 +2774,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1ComponentParameter": {
|
||||
"type": "object",
|
||||
"title": "ComponentParameter contains information about component parameter value",
|
||||
"properties": {
|
||||
"component": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1ConnectionState": {
|
||||
"type": "object",
|
||||
"title": "ConnectionState contains information about remote resource connection state",
|
||||
@@ -2841,6 +2964,10 @@
|
||||
"connectionState": {
|
||||
"$ref": "#/definitions/v1alpha1ConnectionState"
|
||||
},
|
||||
"insecureIgnoreHostKey": {
|
||||
"type": "boolean",
|
||||
"format": "boolean"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -2870,6 +2997,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1ResourceAction": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v1alpha1ResourceActionParam"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1ResourceActionParam": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"default": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1ResourceDiff": {
|
||||
"type": "object",
|
||||
"title": "ResourceDiff holds the diff of a live and target resource object",
|
||||
@@ -2921,25 +3079,101 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1ResourceNetworkingInfo": {
|
||||
"type": "object",
|
||||
"title": "ResourceNetworkingInfo holds networking resource related information",
|
||||
"properties": {
|
||||
"externalURLs": {
|
||||
"description": "ExternalURLs holds list of URLs which should be available externally. List is populated for ingress resources using rules hostnames.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"ingress": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v1LoadBalancerIngress"
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"targetLabels": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"targetRefs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v1alpha1ResourceRef"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1ResourceNode": {
|
||||
"type": "object",
|
||||
"title": "ResourceNode contains information about live resource and its children",
|
||||
"properties": {
|
||||
"children": {
|
||||
"health": {
|
||||
"$ref": "#/definitions/v1alpha1HealthStatus"
|
||||
},
|
||||
"images": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v1alpha1ResourceNode"
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"group": {
|
||||
"type": "string"
|
||||
},
|
||||
"info": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v1alpha1InfoItem"
|
||||
}
|
||||
},
|
||||
"networkingInfo": {
|
||||
"$ref": "#/definitions/v1alpha1ResourceNetworkingInfo"
|
||||
},
|
||||
"parentRefs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v1alpha1ResourceRef"
|
||||
}
|
||||
},
|
||||
"resourceRef": {
|
||||
"$ref": "#/definitions/v1alpha1ResourceRef"
|
||||
},
|
||||
"resourceVersion": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1ResourceOverride": {
|
||||
"type": "object",
|
||||
"title": "ResourceOverride holds configuration to customize resource diffing and health assessment",
|
||||
"properties": {
|
||||
"actions": {
|
||||
"type": "string"
|
||||
},
|
||||
"healthLua": {
|
||||
"type": "string"
|
||||
},
|
||||
"ignoreDifferences": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1alpha1ResourceRef": {
|
||||
"type": "object",
|
||||
"title": "ResourceRef includes fields which unique identify resource",
|
||||
"properties": {
|
||||
"group": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -2949,9 +3183,6 @@
|
||||
"namespace": {
|
||||
"type": "string"
|
||||
},
|
||||
"resourceVersion": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
|
||||
@@ -37,14 +37,15 @@ const (
|
||||
|
||||
func newCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
appResyncPeriod int64
|
||||
repoServerAddress string
|
||||
statusProcessors int
|
||||
operationProcessors int
|
||||
logLevel string
|
||||
glogLevel int
|
||||
cacheSrc func() (*cache.Cache, error)
|
||||
clientConfig clientcmd.ClientConfig
|
||||
appResyncPeriod int64
|
||||
repoServerAddress string
|
||||
repoServerTimeoutSeconds int
|
||||
statusProcessors int
|
||||
operationProcessors int
|
||||
logLevel string
|
||||
glogLevel int
|
||||
cacheSrc func() (*cache.Cache, error)
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: cliName,
|
||||
@@ -54,9 +55,9 @@ func newCommand() *cobra.Command {
|
||||
cli.SetGLogLevel(glogLevel)
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
config.QPS = common.K8sClientConfigQPS
|
||||
config.Burst = common.K8sClientConfigBurst
|
||||
errors.CheckError(err)
|
||||
|
||||
kubeClient := kubernetes.NewForConfigOrDie(config)
|
||||
appClient := appclientset.NewForConfigOrDie(config)
|
||||
@@ -65,7 +66,7 @@ func newCommand() *cobra.Command {
|
||||
errors.CheckError(err)
|
||||
|
||||
resyncDuration := time.Duration(appResyncPeriod) * time.Second
|
||||
repoClientset := reposerver.NewRepoServerClientset(repoServerAddress)
|
||||
repoClientset := reposerver.NewRepoServerClientset(repoServerAddress, repoServerTimeoutSeconds)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
@@ -98,6 +99,7 @@ func newCommand() *cobra.Command {
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(&command)
|
||||
command.Flags().Int64Var(&appResyncPeriod, "app-resync", defaultAppResyncPeriod, "Time period in seconds for application resync.")
|
||||
command.Flags().StringVar(&repoServerAddress, "repo-server", common.DefaultRepoServerAddr, "Repo server address.")
|
||||
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(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
|
||||
|
||||
@@ -3,19 +3,21 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd"
|
||||
argocd "github.com/argoproj/argo-cd"
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/reposerver"
|
||||
"github.com/argoproj/argo-cd/util/cache"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
"github.com/argoproj/argo-cd/util/ksonnet"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
"github.com/argoproj/argo-cd/util/tls"
|
||||
)
|
||||
@@ -23,7 +25,6 @@ import (
|
||||
const (
|
||||
// CLIName is the name of the CLI
|
||||
cliName = "argocd-repo-server"
|
||||
port = 8081
|
||||
)
|
||||
|
||||
func newCommand() *cobra.Command {
|
||||
@@ -48,14 +49,13 @@ func newCommand() *cobra.Command {
|
||||
server, err := reposerver.NewServer(git.NewFactory(), cache, tlsConfigCustomizer, parallelismLimit)
|
||||
errors.CheckError(err)
|
||||
grpc := server.CreateGRPC()
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", common.PortRepoServer))
|
||||
errors.CheckError(err)
|
||||
|
||||
ksVers, err := ksonnet.KsonnetVersion()
|
||||
errors.CheckError(err)
|
||||
http.Handle("/metrics", promhttp.Handler())
|
||||
go func() { errors.CheckError(http.ListenAndServe(fmt.Sprintf(":%d", common.PortRepoServerMetrics), nil)) }()
|
||||
|
||||
log.Infof("argocd-repo-server %s serving on %s", argocd.GetVersion(), listener.Addr())
|
||||
log.Infof("ksonnet version: %s", ksVers)
|
||||
stats.RegisterStackDumper()
|
||||
stats.StartStatsTicker(10 * time.Minute)
|
||||
stats.RegisterHeapDumper("memprofile")
|
||||
|
||||
@@ -57,7 +57,7 @@ func NewCommand() *cobra.Command {
|
||||
|
||||
kubeclientset := kubernetes.NewForConfigOrDie(config)
|
||||
appclientset := appclientset.NewForConfigOrDie(config)
|
||||
repoclientset := reposerver.NewRepoServerClientset(repoServerAddress)
|
||||
repoclientset := reposerver.NewRepoServerClientset(repoServerAddress, 0)
|
||||
|
||||
argoCDOpts := server.ArgoCDServerOpts{
|
||||
Insecure: insecure,
|
||||
@@ -81,7 +81,7 @@ func NewCommand() *cobra.Command {
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
argocd := server.NewServer(ctx, argoCDOpts)
|
||||
argocd.Run(ctx, 8080)
|
||||
argocd.Run(ctx, common.PortAPIServer)
|
||||
cancel()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
@@ -14,13 +15,18 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
apiv1 "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/client-go/dynamic"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/dex"
|
||||
@@ -36,9 +42,15 @@ import (
|
||||
const (
|
||||
// CLIName is the name of the CLI
|
||||
cliName = "argocd-util"
|
||||
|
||||
// YamlSeparator separates sections of a YAML file
|
||||
yamlSeparator = "\n---\n"
|
||||
yamlSeparator = "---\n"
|
||||
)
|
||||
|
||||
var (
|
||||
configMapResource = schema.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"}
|
||||
secretResource = schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"}
|
||||
applicationsResource = schema.GroupVersionResource{Group: "argoproj.io", Version: "v1alpha1", Resource: "applications"}
|
||||
appprojectsResource = schema.GroupVersionResource{Group: "argoproj.io", Version: "v1alpha1", Resource: "appprojects"}
|
||||
)
|
||||
|
||||
// NewCommand returns a new instance of an argocd command
|
||||
@@ -177,7 +189,7 @@ func NewGenDexConfigCommand() *cobra.Command {
|
||||
errors.CheckError(err)
|
||||
maskedDexCfgBytes, err := yaml.Marshal(dexCfg)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf(string(maskedDexCfgBytes))
|
||||
fmt.Print(string(maskedDexCfgBytes))
|
||||
} else {
|
||||
err = ioutil.WriteFile(out, dexCfgBytes, 0644)
|
||||
errors.CheckError(err)
|
||||
@@ -195,94 +207,153 @@ func NewGenDexConfigCommand() *cobra.Command {
|
||||
func NewImportCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
prune bool
|
||||
dryRun bool
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: "import SOURCE",
|
||||
Short: "Import Argo CD data from stdin (specify `-') or a file",
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var (
|
||||
input []byte
|
||||
err error
|
||||
newSettings *settings.ArgoCDSettings
|
||||
newRepos []*v1alpha1.Repository
|
||||
newClusters []*v1alpha1.Cluster
|
||||
newApps []*v1alpha1.Application
|
||||
newRBACCM *apiv1.ConfigMap
|
||||
)
|
||||
|
||||
if in := args[0]; in == "-" {
|
||||
input, err = ioutil.ReadAll(os.Stdin)
|
||||
errors.CheckError(err)
|
||||
} else {
|
||||
input, err = ioutil.ReadFile(in)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
inputStrings := strings.Split(string(input), yamlSeparator)
|
||||
|
||||
err = yaml.Unmarshal([]byte(inputStrings[0]), &newSettings)
|
||||
errors.CheckError(err)
|
||||
|
||||
err = yaml.Unmarshal([]byte(inputStrings[1]), &newRepos)
|
||||
errors.CheckError(err)
|
||||
|
||||
err = yaml.Unmarshal([]byte(inputStrings[2]), &newClusters)
|
||||
errors.CheckError(err)
|
||||
|
||||
err = yaml.Unmarshal([]byte(inputStrings[3]), &newApps)
|
||||
errors.CheckError(err)
|
||||
|
||||
err = yaml.Unmarshal([]byte(inputStrings[4]), &newRBACCM)
|
||||
errors.CheckError(err)
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
config.QPS = 100
|
||||
config.Burst = 50
|
||||
errors.CheckError(err)
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
kubeClientset := kubernetes.NewForConfigOrDie(config)
|
||||
acdClients := newArgoCDClientsets(config, namespace)
|
||||
|
||||
settingsMgr := settings.NewSettingsManager(context.Background(), kubeClientset, namespace)
|
||||
err = settingsMgr.SaveSettings(newSettings)
|
||||
var input []byte
|
||||
if in := args[0]; in == "-" {
|
||||
input, err = ioutil.ReadAll(os.Stdin)
|
||||
} else {
|
||||
input, err = ioutil.ReadFile(in)
|
||||
}
|
||||
errors.CheckError(err)
|
||||
db := db.NewDB(namespace, settingsMgr, kubeClientset)
|
||||
var dryRunMsg string
|
||||
if dryRun {
|
||||
dryRunMsg = " (dry run)"
|
||||
}
|
||||
|
||||
_, err = kubeClientset.CoreV1().ConfigMaps(namespace).Create(newRBACCM)
|
||||
// pruneObjects tracks live objects and it's current resource version. any remaining
|
||||
// items in this map indicates the resource should be pruned since it no longer appears
|
||||
// in the backup
|
||||
pruneObjects := make(map[kube.ResourceKey]string)
|
||||
configMaps, err := acdClients.configMaps.List(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, cm := range configMaps.Items {
|
||||
cmName := cm.GetName()
|
||||
if cmName == common.ArgoCDConfigMapName || cmName == common.ArgoCDRBACConfigMapName {
|
||||
pruneObjects[kube.ResourceKey{Group: "", Kind: "ConfigMap", Name: cm.GetName()}] = cm.GetResourceVersion()
|
||||
}
|
||||
}
|
||||
secrets, err := acdClients.secrets.List(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, secret := range secrets.Items {
|
||||
if isArgoCDSecret(nil, secret) {
|
||||
pruneObjects[kube.ResourceKey{Group: "", Kind: "Secret", Name: secret.GetName()}] = secret.GetResourceVersion()
|
||||
}
|
||||
}
|
||||
applications, err := acdClients.applications.List(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, app := range applications.Items {
|
||||
pruneObjects[kube.ResourceKey{Group: "argoproj.io", Kind: "Application", Name: app.GetName()}] = app.GetResourceVersion()
|
||||
}
|
||||
projects, err := acdClients.projects.List(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, proj := range projects.Items {
|
||||
pruneObjects[kube.ResourceKey{Group: "argoproj.io", Kind: "AppProject", Name: proj.GetName()}] = proj.GetResourceVersion()
|
||||
}
|
||||
|
||||
for _, repo := range newRepos {
|
||||
_, err := db.CreateRepository(context.Background(), repo)
|
||||
if err != nil {
|
||||
log.Warn(err)
|
||||
// Create or replace existing object
|
||||
objs, err := kube.SplitYAML(string(input))
|
||||
errors.CheckError(err)
|
||||
for _, obj := range objs {
|
||||
gvk := obj.GroupVersionKind()
|
||||
key := kube.ResourceKey{Group: gvk.Group, Kind: gvk.Kind, Name: obj.GetName()}
|
||||
resourceVersion, exists := pruneObjects[key]
|
||||
delete(pruneObjects, key)
|
||||
var dynClient dynamic.ResourceInterface
|
||||
switch obj.GetKind() {
|
||||
case "Secret":
|
||||
dynClient = acdClients.secrets
|
||||
case "ConfigMap":
|
||||
dynClient = acdClients.configMaps
|
||||
case "AppProject":
|
||||
dynClient = acdClients.projects
|
||||
case "Application":
|
||||
dynClient = acdClients.applications
|
||||
}
|
||||
if !exists {
|
||||
if !dryRun {
|
||||
_, err = dynClient.Create(obj, metav1.CreateOptions{})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
fmt.Printf("%s/%s %s created%s\n", gvk.Group, gvk.Kind, obj.GetName(), dryRunMsg)
|
||||
} else {
|
||||
if !dryRun {
|
||||
obj.SetResourceVersion(resourceVersion)
|
||||
_, err = dynClient.Update(obj, metav1.UpdateOptions{})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
fmt.Printf("%s/%s %s replaced%s\n", gvk.Group, gvk.Kind, obj.GetName(), dryRunMsg)
|
||||
}
|
||||
}
|
||||
|
||||
for _, cluster := range newClusters {
|
||||
_, err := db.CreateCluster(context.Background(), cluster)
|
||||
if err != nil {
|
||||
log.Warn(err)
|
||||
// Delete objects not in backup
|
||||
for key := range pruneObjects {
|
||||
if prune {
|
||||
var dynClient dynamic.ResourceInterface
|
||||
switch key.Kind {
|
||||
case "Secret":
|
||||
dynClient = acdClients.secrets
|
||||
case "AppProject":
|
||||
dynClient = acdClients.projects
|
||||
case "Application":
|
||||
dynClient = acdClients.applications
|
||||
default:
|
||||
log.Fatalf("Unexpected kind '%s' in prune list", key.Kind)
|
||||
}
|
||||
if !dryRun {
|
||||
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)
|
||||
} else {
|
||||
fmt.Printf("%s/%s %s needs pruning\n", key.Group, key.Kind, key.Name)
|
||||
}
|
||||
}
|
||||
|
||||
appClientset := appclientset.NewForConfigOrDie(config)
|
||||
for _, app := range newApps {
|
||||
out, err := appClientset.ArgoprojV1alpha1().Applications(namespace).Create(app)
|
||||
errors.CheckError(err)
|
||||
log.Println(out)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(&command)
|
||||
command.Flags().BoolVar(&dryRun, "dry-run", false, "Print what will be performed")
|
||||
command.Flags().BoolVar(&prune, "prune", false, "Prune secrets, applications and projects which do not appear in the backup")
|
||||
|
||||
return &command
|
||||
}
|
||||
|
||||
type argoCDClientsets struct {
|
||||
configMaps dynamic.ResourceInterface
|
||||
secrets dynamic.ResourceInterface
|
||||
applications dynamic.ResourceInterface
|
||||
projects dynamic.ResourceInterface
|
||||
}
|
||||
|
||||
func newArgoCDClientsets(config *rest.Config, namespace string) *argoCDClientsets {
|
||||
dynamicIf, err := dynamic.NewForConfig(config)
|
||||
errors.CheckError(err)
|
||||
return &argoCDClientsets{
|
||||
configMaps: dynamicIf.Resource(configMapResource).Namespace(namespace),
|
||||
secrets: dynamicIf.Resource(secretResource).Namespace(namespace),
|
||||
applications: dynamicIf.Resource(applicationsResource).Namespace(namespace),
|
||||
projects: dynamicIf.Resource(appprojectsResource).Namespace(namespace),
|
||||
}
|
||||
}
|
||||
|
||||
// NewExportCommand defines a new command for exporting Kubernetes and Argo CD resources.
|
||||
func NewExportCommand() *cobra.Command {
|
||||
var (
|
||||
@@ -292,75 +363,48 @@ func NewExportCommand() *cobra.Command {
|
||||
var command = cobra.Command{
|
||||
Use: "export",
|
||||
Short: "Export all Argo CD data to stdout (default) or a file",
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
kubeClientset := kubernetes.NewForConfigOrDie(config)
|
||||
|
||||
settingsMgr := settings.NewSettingsManager(context.Background(), kubeClientset, namespace)
|
||||
settings, err := settingsMgr.GetSettings()
|
||||
errors.CheckError(err)
|
||||
// certificate data is included in secrets that are exported alongside
|
||||
settings.Certificate = nil
|
||||
|
||||
db := db.NewDB(namespace, settingsMgr, kubeClientset)
|
||||
clusters, err := db.ListClusters(context.Background())
|
||||
errors.CheckError(err)
|
||||
|
||||
repoURLs, err := db.ListRepoURLs(context.Background())
|
||||
errors.CheckError(err)
|
||||
repos := make([]*v1alpha1.Repository, len(repoURLs))
|
||||
for i := range repoURLs {
|
||||
repo, err := db.GetRepository(context.Background(), repoURLs[i])
|
||||
errors.CheckError(err)
|
||||
repos = append(repos, repo)
|
||||
}
|
||||
|
||||
appClientset := appclientset.NewForConfigOrDie(config)
|
||||
apps, err := appClientset.ArgoprojV1alpha1().Applications(namespace).List(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
|
||||
rbacCM, err := kubeClientset.CoreV1().ConfigMaps(namespace).Get(common.ArgoCDRBACConfigMapName, metav1.GetOptions{})
|
||||
errors.CheckError(err)
|
||||
|
||||
// remove extraneous cruft from output
|
||||
rbacCM.ObjectMeta = metav1.ObjectMeta{
|
||||
Name: rbacCM.ObjectMeta.Name,
|
||||
}
|
||||
|
||||
// remove extraneous cruft from output
|
||||
for idx, app := range apps.Items {
|
||||
apps.Items[idx].ObjectMeta = metav1.ObjectMeta{
|
||||
Name: app.ObjectMeta.Name,
|
||||
Finalizers: app.ObjectMeta.Finalizers,
|
||||
}
|
||||
apps.Items[idx].Status = v1alpha1.ApplicationStatus{
|
||||
History: app.Status.History,
|
||||
}
|
||||
apps.Items[idx].Operation = nil
|
||||
}
|
||||
|
||||
// take a list of exportable objects, marshal them to YAML,
|
||||
// and return a string joined by a delimiter
|
||||
output := func(delimiter string, oo ...interface{}) string {
|
||||
out := make([]string, 0)
|
||||
for _, o := range oo {
|
||||
data, err := yaml.Marshal(o)
|
||||
errors.CheckError(err)
|
||||
out = append(out, string(data))
|
||||
}
|
||||
return strings.Join(out, delimiter)
|
||||
}(yamlSeparator, settings, clusters.Items, repos, apps.Items, rbacCM)
|
||||
|
||||
var writer io.Writer
|
||||
if out == "-" {
|
||||
fmt.Println(output)
|
||||
writer = os.Stdout
|
||||
} else {
|
||||
err = ioutil.WriteFile(out, []byte(output), 0644)
|
||||
f, err := os.Create(out)
|
||||
errors.CheckError(err)
|
||||
defer util.Close(f)
|
||||
writer = bufio.NewWriter(f)
|
||||
}
|
||||
|
||||
acdClients := newArgoCDClientsets(config, namespace)
|
||||
acdConfigMap, err := acdClients.configMaps.Get(common.ArgoCDConfigMapName, metav1.GetOptions{})
|
||||
errors.CheckError(err)
|
||||
export(writer, *acdConfigMap)
|
||||
acdRBACConfigMap, err := acdClients.configMaps.Get(common.ArgoCDRBACConfigMapName, metav1.GetOptions{})
|
||||
errors.CheckError(err)
|
||||
export(writer, *acdRBACConfigMap)
|
||||
|
||||
referencedSecrets := getReferencedSecrets(*acdConfigMap)
|
||||
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(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, proj := range projects.Items {
|
||||
export(writer, proj)
|
||||
}
|
||||
applications, err := acdClients.applications.List(metav1.ListOptions{})
|
||||
errors.CheckError(err)
|
||||
for _, app := range applications.Items {
|
||||
export(writer, app)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -370,13 +414,109 @@ func NewExportCommand() *cobra.Command {
|
||||
return &command
|
||||
}
|
||||
|
||||
// NewClusterConfig returns a new instance of `argocd-util cluster-kubeconfig` command
|
||||
// getReferencedSecrets examines the argocd-cm config for any referenced repo secrets and returns a
|
||||
// map of all referenced secrets.
|
||||
func getReferencedSecrets(un unstructured.Unstructured) map[string]bool {
|
||||
var cm apiv1.ConfigMap
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, &cm)
|
||||
errors.CheckError(err)
|
||||
referencedSecrets := make(map[string]bool)
|
||||
if reposRAW, ok := cm.Data["repositories"]; ok {
|
||||
repoCreds := make([]settings.RepoCredentials, 0)
|
||||
err := yaml.Unmarshal([]byte(reposRAW), &repoCreds)
|
||||
errors.CheckError(err)
|
||||
for _, cred := range repoCreds {
|
||||
if cred.PasswordSecret != nil {
|
||||
referencedSecrets[cred.PasswordSecret.Name] = true
|
||||
}
|
||||
if cred.SSHPrivateKeySecret != nil {
|
||||
referencedSecrets[cred.SSHPrivateKeySecret.Name] = true
|
||||
}
|
||||
if cred.UsernameSecret != nil {
|
||||
referencedSecrets[cred.UsernameSecret.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if helmReposRAW, ok := cm.Data["helm.repositories"]; ok {
|
||||
helmRepoCreds := make([]settings.HelmRepoCredentials, 0)
|
||||
err := yaml.Unmarshal([]byte(helmReposRAW), &helmRepoCreds)
|
||||
errors.CheckError(err)
|
||||
for _, cred := range helmRepoCreds {
|
||||
if cred.CASecret != nil {
|
||||
referencedSecrets[cred.CASecret.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.PasswordSecret != nil {
|
||||
referencedSecrets[cred.PasswordSecret.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return referencedSecrets
|
||||
}
|
||||
|
||||
// isArgoCDSecret returns whether or not the given secret is a part of Argo CD configuration
|
||||
// (e.g. argocd-secret, repo credentials, or cluster credentials)
|
||||
func isArgoCDSecret(repoSecretRefs map[string]bool, un unstructured.Unstructured) bool {
|
||||
secretName := un.GetName()
|
||||
if secretName == common.ArgoCDSecretName {
|
||||
return true
|
||||
}
|
||||
if repoSecretRefs != nil {
|
||||
if _, ok := repoSecretRefs[secretName]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if labels := un.GetLabels(); labels != nil {
|
||||
if _, ok := labels[common.LabelKeySecretType]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if annotations := un.GetAnnotations(); annotations != nil {
|
||||
if annotations[common.AnnotationKeyManagedBy] == common.AnnotationValueManagedByArgoCD {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// export writes the unstructured object and removes extraneous cruft from output before writing
|
||||
func export(w io.Writer, un unstructured.Unstructured) {
|
||||
name := un.GetName()
|
||||
finalizers := un.GetFinalizers()
|
||||
apiVersion := un.GetAPIVersion()
|
||||
kind := un.GetKind()
|
||||
labels := un.GetLabels()
|
||||
annotations := un.GetAnnotations()
|
||||
unstructured.RemoveNestedField(un.Object, "metadata")
|
||||
un.SetName(name)
|
||||
un.SetFinalizers(finalizers)
|
||||
un.SetAPIVersion(apiVersion)
|
||||
un.SetKind(kind)
|
||||
un.SetLabels(labels)
|
||||
un.SetAnnotations(annotations)
|
||||
data, err := yaml.Marshal(un.Object)
|
||||
errors.CheckError(err)
|
||||
_, err = w.Write(data)
|
||||
errors.CheckError(err)
|
||||
_, err = w.Write([]byte(yamlSeparator))
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
// NewClusterConfig returns a new instance of `argocd-util kubeconfig` command
|
||||
func NewClusterConfig() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "cluster-kubeconfig CLUSTER_URL OUTPUT_PATH",
|
||||
Use: "kubeconfig CLUSTER_URL OUTPUT_PATH",
|
||||
Short: "Generates kubeconfig for the specified cluster",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
@@ -387,12 +527,8 @@ func NewClusterConfig() *cobra.Command {
|
||||
output := args[1]
|
||||
conf, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
namespace, wasSpecified, err := clientConfig.Namespace()
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
if !(wasSpecified) {
|
||||
namespace = "argocd"
|
||||
}
|
||||
|
||||
kubeclientset, err := kubernetes.NewForConfig(conf)
|
||||
errors.CheckError(err)
|
||||
|
||||
|
||||
@@ -27,13 +27,13 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"github.com/argoproj/argo-cd/controller"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/reposerver/repository"
|
||||
"github.com/argoproj/argo-cd/server/application"
|
||||
apirepository "github.com/argoproj/argo-cd/server/repository"
|
||||
"github.com/argoproj/argo-cd/server/settings"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/argo"
|
||||
@@ -43,14 +43,27 @@ import (
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
"github.com/argoproj/argo-cd/util/hook"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
argosettings "github.com/argoproj/argo-cd/util/settings"
|
||||
"github.com/argoproj/argo-cd/util/templates"
|
||||
)
|
||||
|
||||
var (
|
||||
appExample = templates.Examples(`
|
||||
# List all the applications.
|
||||
argocd app list
|
||||
|
||||
# Get the details of a application
|
||||
argocd app get my-app
|
||||
|
||||
# Set an override parameter
|
||||
argocd app set my-app -p image.tag=v1.0.1`)
|
||||
)
|
||||
|
||||
// NewApplicationCommand returns a new instance of an `argocd app` command
|
||||
func NewApplicationCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "app",
|
||||
Short: "Manage applications",
|
||||
Use: "app",
|
||||
Short: "Manage applications",
|
||||
Example: appExample,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
@@ -72,6 +85,7 @@ func NewApplicationCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
command.AddCommand(NewApplicationEditCommand(clientOpts))
|
||||
command.AddCommand(NewApplicationPatchCommand(clientOpts))
|
||||
command.AddCommand(NewApplicationPatchResourceCommand(clientOpts))
|
||||
command.AddCommand(NewApplicationResourceActionsCommand(clientOpts))
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -116,7 +130,7 @@ func NewApplicationCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.
|
||||
},
|
||||
}
|
||||
setAppOptions(c.Flags(), &app, &appOpts)
|
||||
setParameterOverrides(&app, argocdClient, appOpts.parameters)
|
||||
setParameterOverrides(&app, appOpts.parameters)
|
||||
}
|
||||
if app.Name == "" {
|
||||
c.HelpFunc()(c, args)
|
||||
@@ -225,6 +239,7 @@ func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
|
||||
|
||||
func printAppSummaryTable(app *argoappv1.Application, appURL string) {
|
||||
fmt.Printf(printOpFmtStr, "Name:", app.Name)
|
||||
fmt.Printf(printOpFmtStr, "Project:", app.Spec.GetProject())
|
||||
fmt.Printf(printOpFmtStr, "Server:", app.Spec.Destination.Server)
|
||||
fmt.Printf(printOpFmtStr, "Namespace:", app.Spec.Destination.Namespace)
|
||||
fmt.Printf(printOpFmtStr, "URL:", appURL)
|
||||
@@ -353,7 +368,7 @@ func NewApplicationSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
setParameterOverrides(app, argocdClient, appOpts.parameters)
|
||||
setParameterOverrides(app, appOpts.parameters)
|
||||
_, err = appIf.UpdateSpec(ctx, &application.ApplicationUpdateSpecRequest{
|
||||
Name: &app.Name,
|
||||
Spec: app.Spec,
|
||||
@@ -521,9 +536,6 @@ func NewApplicationUnsetCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(app.Spec.Source.Ksonnet.Parameters) == 0 {
|
||||
app.Spec.Source.ComponentParameterOverrides = nil
|
||||
}
|
||||
}
|
||||
if app.Spec.Source.Helm != nil {
|
||||
for _, paramStr := range parameters {
|
||||
@@ -536,9 +548,6 @@ func NewApplicationUnsetCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(app.Spec.Source.Helm.Parameters) == 0 {
|
||||
app.Spec.Source.ComponentParameterOverrides = nil
|
||||
}
|
||||
specValueFiles := app.Spec.Source.Helm.ValueFiles
|
||||
for _, valuesFile := range valuesFiles {
|
||||
for i, vf := range specValueFiles {
|
||||
@@ -611,6 +620,17 @@ func getLocalObjects(app *argoappv1.Application, local string, appLabelKey strin
|
||||
return objs
|
||||
}
|
||||
|
||||
type resourceInfoProvider struct {
|
||||
namespacedByGk map[schema.GroupKind]bool
|
||||
}
|
||||
|
||||
// Infer if obj is namespaced or not from corresponding live objects list. If corresponding live object has namespace then target object is also namespaced.
|
||||
// If live object is missing then it does not matter if target is namespaced or not.
|
||||
func (p *resourceInfoProvider) IsNamespaced(server string, obj *unstructured.Unstructured) (bool, error) {
|
||||
key := kube.GetResourceKey(obj)
|
||||
return p.namespacedByGk[key.GroupKind()], nil
|
||||
}
|
||||
|
||||
func groupLocalObjs(localObs []*unstructured.Unstructured, liveObjs []*unstructured.Unstructured, appNamespace string) map[kube.ResourceKey]*unstructured.Unstructured {
|
||||
namespacedByGk := make(map[schema.GroupKind]bool)
|
||||
for i := range liveObjs {
|
||||
@@ -619,22 +639,13 @@ func groupLocalObjs(localObs []*unstructured.Unstructured, liveObjs []*unstructu
|
||||
namespacedByGk[schema.GroupKind{Group: key.Group, Kind: key.Kind}] = key.Namespace != ""
|
||||
}
|
||||
}
|
||||
localObs, _, err := controller.DeduplicateTargetObjects("", appNamespace, localObs, &resourceInfoProvider{namespacedByGk: namespacedByGk})
|
||||
errors.CheckError(err)
|
||||
objByKey := make(map[kube.ResourceKey]*unstructured.Unstructured)
|
||||
for i := range localObs {
|
||||
obj := localObs[i]
|
||||
gk := obj.GroupVersionKind().GroupKind()
|
||||
// Infer if obj is namespaced or not from corresponding live objects list. If corresponding live object has namespace then target object is also namespaced.
|
||||
// If live object is missing then it does not matter if target is namespaced or not.
|
||||
namespace := obj.GetNamespace()
|
||||
if !namespacedByGk[gk] {
|
||||
namespace = ""
|
||||
} else {
|
||||
if namespace == "" {
|
||||
namespace = appNamespace
|
||||
}
|
||||
}
|
||||
if !hook.IsHook(obj) {
|
||||
objByKey[kube.NewResourceKey(gk.Group, gk.Kind, namespace, obj.GetName())] = obj
|
||||
objByKey[kube.GetResourceKey(obj)] = obj
|
||||
}
|
||||
}
|
||||
return objByKey
|
||||
@@ -651,11 +662,11 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
var command = &cobra.Command{
|
||||
Use: "diff APPNAME",
|
||||
Short: shortDesc,
|
||||
Long: shortDesc + "\nUses 'diff' to render the difference. KUBECTL_EXTERNAL_DIFF environment variable can be used to select your own diff tool.",
|
||||
Long: shortDesc + "\nUses 'diff' to render the difference. KUBECTL_EXTERNAL_DIFF environment variable can be used to select your own diff tool.\nReturns the following exit codes: 2 on general errors, 1 when a diff is found, and 0 when no diff is found",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
clientset := argocdclient.NewClientOrDie(clientOpts)
|
||||
@@ -673,20 +684,35 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
live *unstructured.Unstructured
|
||||
target *unstructured.Unstructured
|
||||
}, 0)
|
||||
|
||||
conn, settingsIf := clientset.NewSettingsClientOrDie()
|
||||
defer util.Close(conn)
|
||||
argoSettings, err := settingsIf.Get(context.Background(), &settings.SettingsQuery{})
|
||||
errors.CheckError(err)
|
||||
|
||||
if local != "" {
|
||||
conn, settingsIf := clientset.NewSettingsClientOrDie()
|
||||
defer util.Close(conn)
|
||||
argoSettings, err := settingsIf.Get(context.Background(), &settings.SettingsQuery{})
|
||||
errors.CheckError(err)
|
||||
localObjs := groupLocalObjs(getLocalObjects(app, local, argoSettings.AppLabelKey), liveObjs, app.Spec.Destination.Namespace)
|
||||
for _, res := range resources.Items {
|
||||
var live = &unstructured.Unstructured{}
|
||||
err := json.Unmarshal([]byte(res.LiveState), &live)
|
||||
errors.CheckError(err)
|
||||
|
||||
key := kube.NewResourceKey(res.Group, res.Kind, res.Namespace, res.Name)
|
||||
var key kube.ResourceKey
|
||||
if live != nil {
|
||||
key = kube.GetResourceKey(live)
|
||||
} else {
|
||||
var target = &unstructured.Unstructured{}
|
||||
err = json.Unmarshal([]byte(res.TargetState), &target)
|
||||
errors.CheckError(err)
|
||||
key = kube.GetResourceKey(target)
|
||||
}
|
||||
if key.Kind == kube.SecretKind && key.Group == "" {
|
||||
// Don't bother comparing secrets, argo-cd doesn't have access to k8s secret data
|
||||
delete(localObjs, key)
|
||||
continue
|
||||
}
|
||||
if local, ok := localObjs[key]; ok || live != nil {
|
||||
if local != nil {
|
||||
if local != nil && !kube.IsCRD(local) {
|
||||
err = kube.SetAppInstanceLabel(local, argoSettings.AppLabelKey, appName)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
@@ -737,15 +763,20 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
}
|
||||
}
|
||||
|
||||
foundDiffs := false
|
||||
for i := range items {
|
||||
item := items[i]
|
||||
// TODO (amatyushentsev): use resource overrides exposed from API server
|
||||
normalizer, err := argo.NewDiffNormalizer(app.Spec.IgnoreDifferences, make(map[string]argosettings.ResourceOverride))
|
||||
overrides := make(map[string]argoappv1.ResourceOverride)
|
||||
for k := range argoSettings.ResourceOverrides {
|
||||
val := argoSettings.ResourceOverrides[k]
|
||||
overrides[k] = *val
|
||||
}
|
||||
normalizer, err := argo.NewDiffNormalizer(app.Spec.IgnoreDifferences, overrides)
|
||||
errors.CheckError(err)
|
||||
// Diff is already available in ResourceDiff Diff field but we have to recalculate diff again due to https://github.com/yudai/gojsondiff/issues/31
|
||||
diffRes := diff.Diff(item.target, item.live, normalizer)
|
||||
fmt.Printf("===== %s/%s %s/%s ======\n", item.key.Group, item.key.Kind, item.key.Namespace, item.key.Name)
|
||||
if diffRes.Modified || item.target == nil || item.live == nil {
|
||||
fmt.Printf("===== %s/%s %s/%s ======\n", item.key.Group, item.key.Kind, item.key.Namespace, item.key.Name)
|
||||
var live *unstructured.Unstructured
|
||||
var target *unstructured.Unstructured
|
||||
if item.target != nil && item.live != nil {
|
||||
@@ -757,9 +788,13 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
target = item.target
|
||||
}
|
||||
|
||||
foundDiffs = true
|
||||
printDiff(item.key.Name, target, live)
|
||||
}
|
||||
}
|
||||
if foundDiffs {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
@@ -773,7 +808,7 @@ func printDiff(name string, live *unstructured.Unstructured, target *unstructure
|
||||
tempDir, err := ioutil.TempDir("", "argocd-diff")
|
||||
errors.CheckError(err)
|
||||
|
||||
targetFile := path.Join(tempDir, fmt.Sprintf("%s", name))
|
||||
targetFile := path.Join(tempDir, name)
|
||||
targetData := []byte("")
|
||||
if target != nil {
|
||||
targetData, err = yaml.Marshal(target)
|
||||
@@ -919,6 +954,47 @@ func formatConditionsSummary(app argoappv1.Application) string {
|
||||
return summary
|
||||
}
|
||||
|
||||
const (
|
||||
resourceFieldDelimiter = ":"
|
||||
resourceFieldCount = 3
|
||||
labelFieldDelimiter = "="
|
||||
)
|
||||
|
||||
func parseSelectedResources(resources []string) []argoappv1.SyncOperationResource {
|
||||
var selectedResources []argoappv1.SyncOperationResource
|
||||
if resources != nil {
|
||||
selectedResources = []argoappv1.SyncOperationResource{}
|
||||
for _, r := range resources {
|
||||
fields := strings.Split(r, resourceFieldDelimiter)
|
||||
if len(fields) != resourceFieldCount {
|
||||
log.Fatalf("Resource should have GROUP%sKIND%sNAME, but instead got: %s", resourceFieldDelimiter, resourceFieldDelimiter, r)
|
||||
}
|
||||
rsrc := argoappv1.SyncOperationResource{
|
||||
Group: fields[0],
|
||||
Kind: fields[1],
|
||||
Name: fields[2],
|
||||
}
|
||||
selectedResources = append(selectedResources, rsrc)
|
||||
}
|
||||
}
|
||||
return selectedResources
|
||||
}
|
||||
|
||||
func parseLabels(labels []string) (map[string]string, error) {
|
||||
var selectedLabels map[string]string
|
||||
if labels != nil {
|
||||
selectedLabels = map[string]string{}
|
||||
for _, r := range labels {
|
||||
fields := strings.Split(r, labelFieldDelimiter)
|
||||
if len(fields) != 2 {
|
||||
return nil, fmt.Errorf("labels should have key%svalue, but instead got: %s", labelFieldDelimiter, r)
|
||||
}
|
||||
selectedLabels[fields[0]] = fields[1]
|
||||
}
|
||||
}
|
||||
return selectedLabels, nil
|
||||
}
|
||||
|
||||
// NewApplicationWaitCommand returns a new instance of an `argocd app wait` command
|
||||
func NewApplicationWaitCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
@@ -927,6 +1003,7 @@ func NewApplicationWaitCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
watchSuspended bool
|
||||
watchOperations bool
|
||||
timeout uint
|
||||
resources []string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "wait APPNAME",
|
||||
@@ -936,26 +1013,23 @@ func NewApplicationWaitCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
if watchSuspended && watchHealth {
|
||||
log.Fatal("Wait command can not have both the --health and --suspended flags set")
|
||||
|
||||
}
|
||||
if !watchSync && !watchHealth && !watchOperations && !watchSuspended {
|
||||
watchSync = true
|
||||
watchHealth = true
|
||||
watchOperations = true
|
||||
watchSuspended = false
|
||||
}
|
||||
selectedResources := parseSelectedResources(resources)
|
||||
appName := args[0]
|
||||
acdClient := argocdclient.NewClientOrDie(clientOpts)
|
||||
_, err := waitOnApplicationStatus(acdClient, appName, timeout, watchSync, watchHealth, watchOperations, watchSuspended, nil)
|
||||
_, err := waitOnApplicationStatus(acdClient, appName, timeout, watchSync, watchHealth, watchOperations, watchSuspended, selectedResources)
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
command.Flags().BoolVar(&watchSync, "sync", false, "Wait for sync")
|
||||
command.Flags().BoolVar(&watchHealth, "health", false, "Wait for health")
|
||||
command.Flags().BoolVar(&watchSuspended, "suspended", false, "Wait for suspended")
|
||||
|
||||
command.Flags().StringArrayVar(&resources, "resource", []string{}, fmt.Sprintf("Sync only specific resources as GROUP%sKIND%sNAME. Fields may be blank. This option may be specified repeatedly", resourceFieldDelimiter, resourceFieldDelimiter))
|
||||
command.Flags().BoolVar(&watchOperations, "operation", false, "Wait for pending operations")
|
||||
command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds")
|
||||
return command
|
||||
@@ -988,11 +1062,15 @@ func printAppResources(w io.Writer, app *argoappv1.Application, showOperation bo
|
||||
fmt.Fprintf(w, "GROUP\tKIND\tNAMESPACE\tNAME\tSTATUS\tHEALTH\n")
|
||||
}
|
||||
for _, res := range app.Status.Resources {
|
||||
healthStatus := ""
|
||||
if res.Health != nil {
|
||||
healthStatus = res.Health.Status
|
||||
}
|
||||
if showOperation {
|
||||
message := messages[fmt.Sprintf("%s/%s/%s/%s", res.Group, res.Kind, res.Namespace, res.Name)]
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s", res.Group, res.Kind, res.Namespace, res.Name, res.Status, res.Health.Status, "", message)
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s", res.Group, res.Kind, res.Namespace, res.Name, res.Status, healthStatus, "", message)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s", res.Group, res.Kind, res.Namespace, res.Name, res.Status, res.Health.Status)
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s", res.Group, res.Kind, res.Namespace, res.Name, res.Status, healthStatus)
|
||||
}
|
||||
fmt.Fprint(w, "\n")
|
||||
}
|
||||
@@ -1010,17 +1088,14 @@ func printAppResources(w io.Writer, app *argoappv1.Application, showOperation bo
|
||||
func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
revision string
|
||||
resources *[]string
|
||||
resources []string
|
||||
labels []string
|
||||
prune bool
|
||||
dryRun bool
|
||||
timeout uint
|
||||
strategy string
|
||||
force bool
|
||||
)
|
||||
const (
|
||||
resourceFieldDelimiter = ":"
|
||||
resourceFieldCount = 3
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "sync APPNAME",
|
||||
Short: "Sync an application to its target state",
|
||||
@@ -1032,28 +1107,57 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
acdClient := argocdclient.NewClientOrDie(clientOpts)
|
||||
conn, appIf := acdClient.NewApplicationClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
appName := args[0]
|
||||
var syncResources []argoappv1.SyncOperationResource
|
||||
if resources != nil {
|
||||
syncResources = []argoappv1.SyncOperationResource{}
|
||||
for _, r := range *resources {
|
||||
fields := strings.Split(r, resourceFieldDelimiter)
|
||||
if len(fields) != resourceFieldCount {
|
||||
log.Fatalf("Resource should have GROUP%sKIND%sNAME, but instead got: %s", resourceFieldDelimiter, resourceFieldDelimiter, r)
|
||||
|
||||
selectedLabels, parseErr := parseLabels(labels)
|
||||
if parseErr != nil {
|
||||
log.Fatal(parseErr)
|
||||
}
|
||||
|
||||
if len(selectedLabels) > 0 {
|
||||
ctx := context.Background()
|
||||
|
||||
if revision == "" {
|
||||
revision = "HEAD"
|
||||
}
|
||||
|
||||
q := application.ApplicationManifestQuery{
|
||||
Name: &appName,
|
||||
Revision: revision,
|
||||
}
|
||||
|
||||
res, err := appIf.GetManifests(ctx, &q)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, mfst := range res.Manifests {
|
||||
obj, err := argoappv1.UnmarshalToUnstructured(mfst)
|
||||
errors.CheckError(err)
|
||||
for key, selectedValue := range selectedLabels {
|
||||
if objectValue, ok := obj.GetLabels()[key]; ok && selectedValue == objectValue {
|
||||
gvk := obj.GroupVersionKind()
|
||||
resources = append(resources, fmt.Sprintf("%s:%s:%s", gvk.Group, gvk.Kind, obj.GetName()))
|
||||
}
|
||||
}
|
||||
rsrc := argoappv1.SyncOperationResource{
|
||||
Group: fields[0],
|
||||
Kind: fields[1],
|
||||
Name: fields[2],
|
||||
}
|
||||
syncResources = append(syncResources, rsrc)
|
||||
}
|
||||
|
||||
// If labels are provided and none are found return error only if specific resources were also not
|
||||
// specified.
|
||||
if len(resources) == 0 {
|
||||
log.Fatalf("No matching resources found for labels: %v", labels)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
selectedResources := parseSelectedResources(resources)
|
||||
|
||||
syncReq := application.ApplicationSyncRequest{
|
||||
Name: &appName,
|
||||
DryRun: dryRun,
|
||||
Revision: revision,
|
||||
Resources: syncResources,
|
||||
Resources: selectedResources,
|
||||
Prune: prune,
|
||||
}
|
||||
switch strategy {
|
||||
@@ -1070,28 +1174,32 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
_, err := appIf.Sync(ctx, &syncReq)
|
||||
errors.CheckError(err)
|
||||
|
||||
app, err := waitOnApplicationStatus(acdClient, appName, timeout, false, false, true, false, syncResources)
|
||||
app, err := waitOnApplicationStatus(acdClient, appName, timeout, false, false, true, false, selectedResources)
|
||||
errors.CheckError(err)
|
||||
|
||||
pruningRequired := 0
|
||||
for _, resDetails := range app.Status.OperationState.SyncResult.Resources {
|
||||
if resDetails.Status == argoappv1.ResultCodePruneSkipped {
|
||||
pruningRequired++
|
||||
// Only get resources to be pruned if sync was application-wide
|
||||
if len(selectedResources) == 0 {
|
||||
pruningRequired := 0
|
||||
for _, resDetails := range app.Status.OperationState.SyncResult.Resources {
|
||||
if resDetails.Status == argoappv1.ResultCodePruneSkipped {
|
||||
pruningRequired++
|
||||
}
|
||||
}
|
||||
if pruningRequired > 0 {
|
||||
log.Fatalf("%d resources require pruning", pruningRequired)
|
||||
}
|
||||
}
|
||||
if pruningRequired > 0 {
|
||||
log.Fatalf("%d resources require pruning", pruningRequired)
|
||||
}
|
||||
|
||||
if !app.Status.OperationState.Phase.Successful() && !dryRun {
|
||||
os.Exit(1)
|
||||
if !app.Status.OperationState.Phase.Successful() && !dryRun {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().BoolVar(&dryRun, "dry-run", false, "Preview apply without affecting cluster")
|
||||
command.Flags().BoolVar(&prune, "prune", false, "Allow deleting unexpected resources")
|
||||
command.Flags().StringVar(&revision, "revision", "", "Sync to a specific revision. Preserves parameter overrides")
|
||||
resources = command.Flags().StringArray("resource", nil, fmt.Sprintf("Sync only specific resources as GROUP%sKIND%sNAME. Fields may be blank. This option may be specified repeatedly", resourceFieldDelimiter, resourceFieldDelimiter))
|
||||
command.Flags().StringArrayVar(&resources, "resource", []string{}, fmt.Sprintf("Sync only specific resources as GROUP%sKIND%sNAME. Fields may be blank. This option may be specified repeatedly", resourceFieldDelimiter, resourceFieldDelimiter))
|
||||
command.Flags().StringArrayVar(&labels, "label", []string{}, fmt.Sprintf("Sync only specific resources with a label. This option may be specified repeatedly."))
|
||||
command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds")
|
||||
command.Flags().StringVar(&strategy, "strategy", "", "Sync strategy (one of: apply|hook)")
|
||||
command.Flags().BoolVar(&force, "force", false, "Use a force apply")
|
||||
@@ -1111,13 +1219,17 @@ type resourceState struct {
|
||||
}
|
||||
|
||||
func newResourceStateFromStatus(res *argoappv1.ResourceStatus) *resourceState {
|
||||
healthStatus := ""
|
||||
if res.Health != nil {
|
||||
healthStatus = res.Health.Status
|
||||
}
|
||||
return &resourceState{
|
||||
Group: res.Group,
|
||||
Kind: res.Kind,
|
||||
Namespace: res.Namespace,
|
||||
Name: res.Name,
|
||||
Status: string(res.Status),
|
||||
Health: res.Health.Status,
|
||||
Health: healthStatus,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1161,20 +1273,8 @@ func (rs *resourceState) Merge(newState *resourceState) bool {
|
||||
return updated
|
||||
}
|
||||
|
||||
func calculateResourceStates(app *argoappv1.Application, syncResources []argoappv1.SyncOperationResource) map[string]*resourceState {
|
||||
resStates := make(map[string]*resourceState)
|
||||
for _, res := range app.Status.Resources {
|
||||
if len(syncResources) > 0 && !argo.ContainsSyncResource(res.Name, res.GroupVersionKind(), syncResources) {
|
||||
continue
|
||||
}
|
||||
newState := newResourceStateFromStatus(&res)
|
||||
key := newState.Key()
|
||||
if prev, ok := resStates[key]; ok {
|
||||
prev.Merge(newState)
|
||||
} else {
|
||||
resStates[key] = newState
|
||||
}
|
||||
}
|
||||
func calculateResourceStates(app *argoappv1.Application, selectedResources []argoappv1.SyncOperationResource) map[string]*resourceState {
|
||||
resStates := getResourceStates(app, selectedResources)
|
||||
|
||||
var opResult *argoappv1.SyncOperationResult
|
||||
if app.Status.OperationState != nil {
|
||||
@@ -1199,9 +1299,42 @@ func calculateResourceStates(app *argoappv1.Application, syncResources []argoapp
|
||||
return resStates
|
||||
}
|
||||
|
||||
func getResourceStates(app *argoappv1.Application, selectedResources []argoappv1.SyncOperationResource) map[string]*resourceState {
|
||||
resStates := make(map[string]*resourceState)
|
||||
for _, res := range app.Status.Resources {
|
||||
if len(selectedResources) > 0 && !argo.ContainsSyncResource(res.Name, res.GroupVersionKind(), selectedResources) {
|
||||
continue
|
||||
}
|
||||
newState := newResourceStateFromStatus(&res)
|
||||
key := newState.Key()
|
||||
if prev, ok := resStates[key]; ok {
|
||||
prev.Merge(newState)
|
||||
} else {
|
||||
resStates[key] = newState
|
||||
}
|
||||
}
|
||||
return resStates
|
||||
}
|
||||
|
||||
func checkResourceStatus(watchSync bool, watchHealth bool, watchOperation bool, watchSuspended bool, healthStatus string, syncStatus string, operationStatus *argoappv1.Operation) bool {
|
||||
healthCheckPassed := true
|
||||
if watchSuspended && watchHealth {
|
||||
healthCheckPassed = healthStatus == argoappv1.HealthStatusHealthy ||
|
||||
healthStatus == argoappv1.HealthStatusSuspended
|
||||
} else if watchSuspended {
|
||||
healthCheckPassed = healthStatus == argoappv1.HealthStatusSuspended
|
||||
} else if watchHealth {
|
||||
healthCheckPassed = healthStatus == argoappv1.HealthStatusHealthy
|
||||
}
|
||||
|
||||
synced := !watchSync || syncStatus == string(argoappv1.SyncStatusCodeSynced)
|
||||
operational := !watchOperation || operationStatus == nil
|
||||
return synced && healthCheckPassed && operational
|
||||
}
|
||||
|
||||
const waitFormatString = "%s\t%5s\t%10s\t%10s\t%20s\t%8s\t%7s\t%10s\t%s\n"
|
||||
|
||||
func waitOnApplicationStatus(acdClient apiclient.Client, appName string, timeout uint, watchSync bool, watchHealth bool, watchOperation bool, watchSuspened bool, syncResources []argoappv1.SyncOperationResource) (*argoappv1.Application, error) {
|
||||
func waitOnApplicationStatus(acdClient apiclient.Client, appName string, timeout uint, watchSync bool, watchHealth bool, watchOperation bool, watchSuspended bool, selectedResources []argoappv1.SyncOperationResource) (*argoappv1.Application, error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
@@ -1256,21 +1389,38 @@ func waitOnApplicationStatus(acdClient apiclient.Client, appName string, timeout
|
||||
if app.Operation != nil {
|
||||
refresh = true
|
||||
}
|
||||
// consider skipped checks successful
|
||||
synced := !watchSync || app.Status.Sync.Status == argoappv1.SyncStatusCodeSynced
|
||||
healthy := !watchHealth || app.Status.Health.Status == argoappv1.HealthStatusHealthy
|
||||
operational := !watchOperation || appEvent.Application.Operation == nil
|
||||
suspended := !watchSuspened || app.Status.Health.Status == argoappv1.HealthStatusSuspended
|
||||
if len(app.Status.GetErrorConditions()) == 0 && synced && healthy && operational && suspended {
|
||||
|
||||
var selectedResourcesAreReady bool
|
||||
|
||||
// If selected resources are included, wait only on those resources, otherwise wait on the application as a whole.
|
||||
if len(selectedResources) > 0 {
|
||||
selectedResourcesAreReady = true
|
||||
for _, state := range getResourceStates(app, selectedResources) {
|
||||
resourceIsReady := checkResourceStatus(watchSync, watchHealth, false, watchSuspended, state.Health, state.Status, nil)
|
||||
if !resourceIsReady {
|
||||
selectedResourcesAreReady = false
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Wait on the application as a whole
|
||||
selectedResourcesAreReady = checkResourceStatus(watchSync, watchHealth, watchOperation, watchSuspended, app.Status.Health.Status, string(app.Status.Sync.Status), appEvent.Application.Operation)
|
||||
}
|
||||
|
||||
if len(app.Status.GetErrorConditions()) == 0 && selectedResourcesAreReady {
|
||||
printFinalStatus(app)
|
||||
return app, nil
|
||||
}
|
||||
|
||||
newStates := calculateResourceStates(app, syncResources)
|
||||
newStates := calculateResourceStates(app, selectedResources)
|
||||
for _, newState := range newStates {
|
||||
var doPrint bool
|
||||
stateKey := newState.Key()
|
||||
if prevState, found := prevStates[stateKey]; found {
|
||||
if watchHealth && prevState.Health != argoappv1.HealthStatusUnknown && prevState.Health != argoappv1.HealthStatusDegraded && newState.Health == argoappv1.HealthStatusDegraded {
|
||||
printFinalStatus(app)
|
||||
return nil, fmt.Errorf("Application '%s' health state has transitioned from %s to %s", appName, prevState.Health, newState.Health)
|
||||
}
|
||||
doPrint = prevState.Merge(newState)
|
||||
} else {
|
||||
prevStates[stateKey] = newState
|
||||
@@ -1290,21 +1440,30 @@ func waitOnApplicationStatus(acdClient apiclient.Client, appName string, timeout
|
||||
// If the app is a ksonnet app, then parameters are expected to be in the form: component=param=value
|
||||
// Otherwise, the app is assumed to be a helm app and is expected to be in the form:
|
||||
// param=value
|
||||
func setParameterOverrides(app *argoappv1.Application, argocdClient argocdclient.Client, parameters []string) {
|
||||
func setParameterOverrides(app *argoappv1.Application, parameters []string) {
|
||||
if len(parameters) == 0 {
|
||||
return
|
||||
}
|
||||
conn, repoIf := argocdClient.NewRepoClientOrDie()
|
||||
defer util.Close(conn)
|
||||
var sourceType argoappv1.ApplicationSourceType
|
||||
if st, _ := app.Spec.Source.ExplicitType(); st != nil {
|
||||
sourceType = *st
|
||||
} else if app.Status.SourceType != "" {
|
||||
sourceType = app.Status.SourceType
|
||||
} else {
|
||||
// HACK: we don't know the source type, so make an educated guess based on the supplied
|
||||
// parameter string. This code handles the corner case where app doesn't exist yet, and the
|
||||
// command is something like: `argocd app create MYAPP -p foo=bar`
|
||||
// This logic is not foolproof, but when ksonnet is deprecated, this will no longer matter
|
||||
// since helm will remain as the only source type which has parameters.
|
||||
if len(strings.SplitN(parameters[0], "=", 3)) == 3 {
|
||||
sourceType = argoappv1.ApplicationSourceTypeKsonnet
|
||||
} else if len(strings.SplitN(parameters[0], "=", 2)) == 2 {
|
||||
sourceType = argoappv1.ApplicationSourceTypeHelm
|
||||
}
|
||||
}
|
||||
|
||||
appDetails, err := repoIf.GetAppDetails(context.Background(), &apirepository.RepoAppDetailsQuery{
|
||||
Repo: app.Spec.Source.RepoURL,
|
||||
Revision: app.Spec.Source.TargetRevision,
|
||||
Path: app.Spec.Source.Path,
|
||||
})
|
||||
errors.CheckError(err)
|
||||
|
||||
if appDetails.Ksonnet != nil {
|
||||
switch sourceType {
|
||||
case argoappv1.ApplicationSourceTypeKsonnet:
|
||||
if app.Spec.Source.Ksonnet == nil {
|
||||
app.Spec.Source.Ksonnet = &argoappv1.ApplicationSourceKsonnet{}
|
||||
}
|
||||
@@ -1330,7 +1489,7 @@ func setParameterOverrides(app *argoappv1.Application, argocdClient argocdclient
|
||||
app.Spec.Source.Ksonnet.Parameters = append(app.Spec.Source.Ksonnet.Parameters, newParam)
|
||||
}
|
||||
}
|
||||
} else if appDetails.Helm != nil {
|
||||
case argoappv1.ApplicationSourceTypeHelm:
|
||||
if app.Spec.Source.Helm == nil {
|
||||
app.Spec.Source.Helm = &argoappv1.ApplicationSourceHelm{}
|
||||
}
|
||||
@@ -1355,7 +1514,7 @@ func setParameterOverrides(app *argoappv1.Application, argocdClient argocdclient
|
||||
app.Spec.Source.Helm.Parameters = append(app.Spec.Source.Helm.Parameters, newParam)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
log.Fatalf("Parameters can only be set against Ksonnet or Helm applications")
|
||||
}
|
||||
}
|
||||
@@ -1448,6 +1607,9 @@ const printOpFmtStr = "%-20s%s\n"
|
||||
const defaultCheckTimeoutSeconds = 0
|
||||
|
||||
func printOperationResult(opState *argoappv1.OperationState) {
|
||||
if opState == nil {
|
||||
return
|
||||
}
|
||||
if opState.SyncResult != nil {
|
||||
fmt.Printf(printOpFmtStr, "Operation:", "Sync")
|
||||
fmt.Printf(printOpFmtStr, "Sync Revision:", opState.SyncResult.Revision)
|
||||
@@ -1623,6 +1785,43 @@ func NewApplicationPatchCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
|
||||
return &command
|
||||
}
|
||||
|
||||
func filterResources(command *cobra.Command, resources []*argoappv1.ResourceDiff, group, kind, namespace, resourceName string, all bool) []*unstructured.Unstructured {
|
||||
liveObjs, err := liveObjects(resources)
|
||||
errors.CheckError(err)
|
||||
filteredObjects := make([]*unstructured.Unstructured, 0)
|
||||
for i := range liveObjs {
|
||||
obj := liveObjs[i]
|
||||
gvk := obj.GroupVersionKind()
|
||||
if command.Flags().Changed("group") && group != gvk.Group {
|
||||
continue
|
||||
}
|
||||
if namespace != "" && namespace != obj.GetNamespace() {
|
||||
continue
|
||||
}
|
||||
if resourceName != "" && resourceName != obj.GetName() {
|
||||
continue
|
||||
}
|
||||
if kind == gvk.Kind {
|
||||
copy := obj.DeepCopy()
|
||||
filteredObjects = append(filteredObjects, copy)
|
||||
}
|
||||
}
|
||||
if len(filteredObjects) == 0 {
|
||||
log.Fatal("No matching resource found")
|
||||
}
|
||||
if len(filteredObjects) > 1 && !all {
|
||||
log.Fatal("Multiple resources match inputs. Use the --all flag to patch multiple resources")
|
||||
}
|
||||
firstGroup := filteredObjects[0].GroupVersionKind().Group
|
||||
for i := range filteredObjects {
|
||||
obj := filteredObjects[i]
|
||||
if obj.GroupVersionKind().Group != firstGroup {
|
||||
log.Fatal("Multiple groups found in objects to patch. Specify which group to patch with --group flag")
|
||||
}
|
||||
}
|
||||
return filteredObjects
|
||||
}
|
||||
|
||||
func NewApplicationPatchResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var patch string
|
||||
var patchType string
|
||||
@@ -1631,7 +1830,7 @@ func NewApplicationPatchResourceCommand(clientOpts *argocdclient.ClientOptions)
|
||||
var kind string
|
||||
var group string
|
||||
var all bool
|
||||
command := cobra.Command{
|
||||
command := &cobra.Command{
|
||||
Use: "patch-resource APPNAME",
|
||||
Short: "Patch resource in an application",
|
||||
}
|
||||
@@ -1646,7 +1845,7 @@ func NewApplicationPatchResourceCommand(clientOpts *argocdclient.ClientOptions)
|
||||
errors.CheckError(err)
|
||||
command.Flags().StringVar(&group, "group", "", "Group")
|
||||
command.Flags().StringVar(&namespace, "namespace", "", "Namespace")
|
||||
command.Flags().BoolVar(&all, "all", false, "Indicates where to patch multiple matching of resources")
|
||||
command.Flags().BoolVar(&all, "all", false, "Indicates whether to patch multiple matching of resources")
|
||||
command.Run = func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
@@ -1659,39 +1858,7 @@ func NewApplicationPatchResourceCommand(clientOpts *argocdclient.ClientOptions)
|
||||
ctx := context.Background()
|
||||
resources, err := appIf.ManagedResources(ctx, &application.ResourcesQuery{ApplicationName: &appName})
|
||||
errors.CheckError(err)
|
||||
liveObjs, err := liveObjects(resources.Items)
|
||||
errors.CheckError(err)
|
||||
objectsToPatch := make([]*unstructured.Unstructured, 0)
|
||||
for i := range liveObjs {
|
||||
obj := liveObjs[i]
|
||||
gvk := obj.GroupVersionKind()
|
||||
if command.Flags().Changed("group") && kind != gvk.Group {
|
||||
continue
|
||||
}
|
||||
if namespace != "" && namespace != obj.GetNamespace() {
|
||||
continue
|
||||
}
|
||||
if resourceName != "" && resourceName != obj.GetName() {
|
||||
continue
|
||||
}
|
||||
if kind == gvk.Kind {
|
||||
copy := obj.DeepCopy()
|
||||
objectsToPatch = append(objectsToPatch, copy)
|
||||
}
|
||||
}
|
||||
if len(objectsToPatch) == 0 {
|
||||
log.Fatal("No matching resource found to patch")
|
||||
}
|
||||
if len(objectsToPatch) > 1 && !all {
|
||||
log.Fatal("Multiple resources match inputs. Use the --all flag to patch multiple resources")
|
||||
}
|
||||
group := objectsToPatch[0].GroupVersionKind().Group
|
||||
for i := range objectsToPatch {
|
||||
obj := objectsToPatch[i]
|
||||
if obj.GroupVersionKind().Group != group {
|
||||
log.Fatal("Multiple groups found in objects to patch. Specify which group to patch with --group flag")
|
||||
}
|
||||
}
|
||||
objectsToPatch := filterResources(command, resources.Items, group, kind, namespace, resourceName, all)
|
||||
for i := range objectsToPatch {
|
||||
obj := objectsToPatch[i]
|
||||
gvk := obj.GroupVersionKind()
|
||||
@@ -1710,5 +1877,5 @@ func NewApplicationPatchResourceCommand(clientOpts *argocdclient.ClientOptions)
|
||||
}
|
||||
}
|
||||
|
||||
return &command
|
||||
return command
|
||||
}
|
||||
|
||||
150
cmd/argocd/commands/app_actions.go
Normal file
150
cmd/argocd/commands/app_actions.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/server/application"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
)
|
||||
|
||||
// NewApplicationResourceActionsCommand returns a new instance of an `argocd app actions` command
|
||||
func NewApplicationResourceActionsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "actions",
|
||||
Short: "Manage Resource actions",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
},
|
||||
}
|
||||
command.AddCommand(NewApplicationResourceActionsListCommand(clientOpts))
|
||||
command.AddCommand(NewApplicationResourceActionsRunCommand(clientOpts))
|
||||
return command
|
||||
}
|
||||
|
||||
// NewApplicationResourceActionsListCommand returns a new instance of an `argocd app actions list` command
|
||||
func NewApplicationResourceActionsListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var namespace string
|
||||
var kind string
|
||||
var group string
|
||||
var resourceName string
|
||||
var all bool
|
||||
var command = &cobra.Command{
|
||||
Use: "list APPNAME",
|
||||
Short: "Lists available actions on a resource",
|
||||
}
|
||||
command.Run = func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
appName := args[0]
|
||||
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
|
||||
defer util.Close(conn)
|
||||
ctx := context.Background()
|
||||
resources, err := appIf.ManagedResources(ctx, &application.ResourcesQuery{ApplicationName: &appName})
|
||||
errors.CheckError(err)
|
||||
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, &application.ApplicationResourceRequest{
|
||||
Name: &appName,
|
||||
Namespace: obj.GetNamespace(),
|
||||
ResourceName: obj.GetName(),
|
||||
Group: gvk.Group,
|
||||
Kind: gvk.Kind,
|
||||
})
|
||||
errors.CheckError(err)
|
||||
availableActions[obj.GetName()] = availActionsForResource.Actions
|
||||
}
|
||||
|
||||
var keys []string
|
||||
for key := range availableActions {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "RESOURCE\tACTION\n")
|
||||
fmt.Println()
|
||||
for key := range availableActions {
|
||||
for i := range availableActions[key] {
|
||||
action := availableActions[key][i]
|
||||
fmt.Fprintf(w, "%s\t%s\n", key, action.Name)
|
||||
|
||||
}
|
||||
}
|
||||
_ = w.Flush()
|
||||
}
|
||||
command.Flags().StringVar(&resourceName, "resource-name", "", "Name of resource")
|
||||
command.Flags().StringVar(&kind, "kind", "", "Kind")
|
||||
err := command.MarkFlagRequired("kind")
|
||||
errors.CheckError(err)
|
||||
command.Flags().StringVar(&group, "group", "", "Group")
|
||||
command.Flags().StringVar(&namespace, "namespace", "", "Namespace")
|
||||
command.Flags().BoolVar(&all, "all", false, "Indicates whether to list actions on multiple matching resources")
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
// NewApplicationResourceActionsRunCommand returns a new instance of an `argocd app actions run` command
|
||||
func NewApplicationResourceActionsRunCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var namespace string
|
||||
var kind string
|
||||
var group string
|
||||
var resourceName string
|
||||
var all bool
|
||||
var command = &cobra.Command{
|
||||
Use: "run APPNAME ACTION",
|
||||
Short: "Runs an available action on resource(s)",
|
||||
}
|
||||
|
||||
command.Flags().StringVar(&resourceName, "resource-name", "", "Name of resource")
|
||||
command.Flags().StringVar(&kind, "kind", "", "Kind")
|
||||
err := command.MarkFlagRequired("kind")
|
||||
errors.CheckError(err)
|
||||
command.Flags().StringVar(&group, "group", "", "Group")
|
||||
command.Flags().StringVar(&namespace, "namespace", "", "Namespace")
|
||||
command.Flags().BoolVar(&all, "all", false, "Indicates whether to run the action on multiple matching resources")
|
||||
|
||||
command.Run = func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
appName := args[0]
|
||||
actionName := args[1]
|
||||
conn, appIf := argocdclient.NewClientOrDie(clientOpts).NewApplicationClientOrDie()
|
||||
defer util.Close(conn)
|
||||
ctx := context.Background()
|
||||
resources, err := appIf.ManagedResources(ctx, &application.ResourcesQuery{ApplicationName: &appName})
|
||||
errors.CheckError(err)
|
||||
filteredObjects := filterResources(command, resources.Items, group, kind, namespace, resourceName, all)
|
||||
for i := range filteredObjects {
|
||||
obj := filteredObjects[i]
|
||||
gvk := obj.GroupVersionKind()
|
||||
objResourceName := obj.GetName()
|
||||
_, err := appIf.RunResourceAction(context.Background(), &application.ResourceActionRunRequest{
|
||||
Name: &appName,
|
||||
Namespace: obj.GetNamespace(),
|
||||
ResourceName: objResourceName,
|
||||
Group: gvk.Group,
|
||||
Kind: gvk.Kind,
|
||||
Action: actionName,
|
||||
})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
}
|
||||
return command
|
||||
}
|
||||
25
cmd/argocd/commands/app_test.go
Normal file
25
cmd/argocd/commands/app_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
}
|
||||
@@ -154,20 +154,8 @@ func NewCluster(name string, conf *rest.Config, managerBearerToken string, awsAu
|
||||
tlsClientConfig := argoappv1.TLSClientConfig{
|
||||
Insecure: conf.TLSClientConfig.Insecure,
|
||||
ServerName: conf.TLSClientConfig.ServerName,
|
||||
CertData: conf.TLSClientConfig.CertData,
|
||||
KeyData: conf.TLSClientConfig.KeyData,
|
||||
CAData: conf.TLSClientConfig.CAData,
|
||||
}
|
||||
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
|
||||
}
|
||||
if len(conf.TLSClientConfig.CAData) == 0 && conf.TLSClientConfig.CAFile != "" {
|
||||
data, err := ioutil.ReadFile(conf.TLSClientConfig.CAFile)
|
||||
errors.CheckError(err)
|
||||
|
||||
@@ -233,7 +233,7 @@ func oauth2Login(ctx context.Context, port int, oauth2conf *oauth2.Config, provi
|
||||
<p style="margin-top:20px; font-size:18; text-align:center">Authentication was successful, you can now return to CLI. This page will close automatically</p>
|
||||
<script>window.onload=function(){setTimeout(this.close, 4000)}</script>
|
||||
`
|
||||
fmt.Fprintf(w, successPage)
|
||||
fmt.Fprint(w, successPage)
|
||||
completionChan <- ""
|
||||
}
|
||||
srv := &http.Server{Addr: "localhost:" + strconv.Itoa(port)}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
|
||||
@@ -39,9 +39,10 @@ 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
|
||||
repo appsv1.Repository
|
||||
upsert bool
|
||||
sshPrivateKeyPath string
|
||||
insecureIgnoreHostKey bool
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "add REPO",
|
||||
@@ -59,14 +60,15 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
}
|
||||
repo.SSHPrivateKey = string(keyData)
|
||||
}
|
||||
repo.InsecureIgnoreHostKey = insecureIgnoreHostKey
|
||||
// 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)
|
||||
err := git.TestRepo(repo.Repo, "", "", repo.SSHPrivateKey, repo.InsecureIgnoreHostKey)
|
||||
if err != nil {
|
||||
if git.IsSSHURL(repo.Repo) {
|
||||
if yes, _ := git.IsSSHURL(repo.Repo); yes {
|
||||
// If we failed using git SSH credentials, then the repo is automatically bad
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -88,6 +90,7 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
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().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
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ const (
|
||||
PortRepoServer = 8081
|
||||
PortArgoCDMetrics = 8082
|
||||
PortArgoCDAPIServerMetrics = 8083
|
||||
PortRepoServerMetrics = 8084
|
||||
)
|
||||
|
||||
// Argo CD application related constants
|
||||
@@ -108,8 +109,8 @@ 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 = "0.12.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 = "0.12.0"
|
||||
CacheVersion = "1.0.0"
|
||||
)
|
||||
|
||||
@@ -138,9 +138,8 @@ func CreateClusterRoleBinding(
|
||||
// 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"
|
||||
var err error
|
||||
|
||||
err = CreateServiceAccount(clientset, ArgoCDManagerServiceAccount, ns)
|
||||
err := CreateServiceAccount(clientset, ArgoCDManagerServiceAccount, ns)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apierr "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -112,74 +112,96 @@ func NewApplicationController(
|
||||
}
|
||||
appInformer, appLister := ctrl.newApplicationInformerAndLister()
|
||||
projInformer := v1alpha1.NewAppProjectInformer(applicationClientset, namespace, appResyncPeriod, cache.Indexers{})
|
||||
stateCache := statecache.NewLiveStateCache(db, appInformer, ctrl.settings, kubectlCmd, func(appName string) {
|
||||
ctrl.requestAppRefresh(appName)
|
||||
ctrl.appRefreshQueue.Add(fmt.Sprintf("%s/%s", ctrl.namespace, appName))
|
||||
})
|
||||
appStateManager := NewAppStateManager(db, applicationClientset, repoClientset, namespace, kubectlCmd, ctrl.settings, stateCache, projInformer)
|
||||
metricsAddr := fmt.Sprintf("0.0.0.0:%d", common.PortArgoCDMetrics)
|
||||
ctrl.metricsServer = metrics.NewMetricsServer(metricsAddr, appLister)
|
||||
stateCache := statecache.NewLiveStateCache(db, appInformer, ctrl.settings, kubectlCmd, ctrl.metricsServer, ctrl.handleAppUpdated)
|
||||
appStateManager := NewAppStateManager(db, applicationClientset, repoClientset, namespace, kubectlCmd, ctrl.settings, stateCache, projInformer, ctrl.metricsServer)
|
||||
ctrl.appInformer = appInformer
|
||||
ctrl.appLister = appLister
|
||||
ctrl.projInformer = projInformer
|
||||
ctrl.appStateManager = appStateManager
|
||||
ctrl.stateCache = stateCache
|
||||
metricsAddr := fmt.Sprintf("0.0.0.0:%d", common.PortArgoCDMetrics)
|
||||
ctrl.metricsServer = metrics.NewMetricsServer(metricsAddr, ctrl.appLister)
|
||||
|
||||
return &ctrl, nil
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) getApp(name string) (*appv1.Application, error) {
|
||||
obj, exists, err := ctrl.appInformer.GetStore().GetByKey(fmt.Sprintf("%s/%s", ctrl.namespace, name))
|
||||
func isSelfReferencedApp(app *appv1.Application, ref v1.ObjectReference) bool {
|
||||
gvk := ref.GroupVersionKind()
|
||||
return ref.UID == app.UID &&
|
||||
ref.Name == app.Name &&
|
||||
ref.Namespace == app.Namespace &&
|
||||
gvk.Group == application.Group &&
|
||||
gvk.Kind == application.ApplicationKind
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) handleAppUpdated(appName string, fullRefresh bool, ref v1.ObjectReference) {
|
||||
skipForceRefresh := false
|
||||
|
||||
obj, exists, err := ctrl.appInformer.GetIndexer().GetByKey(ctrl.namespace + "/" + appName)
|
||||
if app, ok := obj.(*appv1.Application); exists && err == nil && ok && isSelfReferencedApp(app, ref) {
|
||||
// Don't force refresh app if related resource is application itself. This prevents infinite reconciliation loop.
|
||||
skipForceRefresh = true
|
||||
}
|
||||
|
||||
if !skipForceRefresh {
|
||||
ctrl.requestAppRefresh(appName, fullRefresh)
|
||||
}
|
||||
ctrl.appRefreshQueue.Add(fmt.Sprintf("%s/%s", ctrl.namespace, appName))
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) setAppManagedResources(a *appv1.Application, comparisonResult *comparisonResult) (*appv1.ApplicationTree, error) {
|
||||
managedResources, err := ctrl.managedResources(a, comparisonResult)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
return nil, status.Error(codes.NotFound, fmt.Sprintf("unable to find application with name %s", name))
|
||||
}
|
||||
a, ok := (obj).(*appv1.Application)
|
||||
if !ok {
|
||||
return nil, status.Errorf(codes.Internal, fmt.Sprintf("unexpected object type in app informer"))
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) setAppManagedResources(a *appv1.Application, comparisonResult *comparisonResult) error {
|
||||
tree, err := ctrl.resourceTree(a, comparisonResult.managedResources)
|
||||
tree, err := ctrl.getResourceTree(a, managedResources)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
managedResources, err := ctrl.managedResources(a, comparisonResult)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
err = ctrl.cache.SetAppResourcesTree(a.Name, tree)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
return ctrl.cache.SetAppManagedResources(a.Name, managedResources)
|
||||
return tree, ctrl.cache.SetAppManagedResources(a.Name, managedResources)
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) resourceTree(a *appv1.Application, resources []managedResource) ([]*appv1.ResourceNode, error) {
|
||||
items := make([]*appv1.ResourceNode, 0)
|
||||
for i := range resources {
|
||||
managedResource := resources[i]
|
||||
node := appv1.ResourceNode{
|
||||
Name: managedResource.Name,
|
||||
Version: managedResource.Version,
|
||||
Kind: managedResource.Kind,
|
||||
Group: managedResource.Group,
|
||||
Namespace: managedResource.Namespace,
|
||||
func (ctrl *ApplicationController) getResourceTree(a *appv1.Application, managedResources []*appv1.ResourceDiff) (*appv1.ApplicationTree, error) {
|
||||
nodes := make([]appv1.ResourceNode, 0)
|
||||
|
||||
for i := range managedResources {
|
||||
managedResource := managedResources[i]
|
||||
var live = &unstructured.Unstructured{}
|
||||
err := json.Unmarshal([]byte(managedResource.LiveState), &live)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if managedResource.Live != nil {
|
||||
node.ResourceVersion = managedResource.Live.GetResourceVersion()
|
||||
children, err := ctrl.stateCache.GetChildren(a.Spec.Destination.Server, managedResource.Live)
|
||||
var target = &unstructured.Unstructured{}
|
||||
err = json.Unmarshal([]byte(managedResource.TargetState), &target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if live == nil {
|
||||
nodes = append(nodes, appv1.ResourceNode{
|
||||
ResourceRef: appv1.ResourceRef{
|
||||
Version: target.GroupVersionKind().Version,
|
||||
Name: managedResource.Name,
|
||||
Kind: managedResource.Kind,
|
||||
Group: managedResource.Group,
|
||||
Namespace: managedResource.Namespace,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
err := ctrl.stateCache.IterateHierarchy(a.Spec.Destination.Server, kube.GetResourceKey(live), func(child appv1.ResourceNode) {
|
||||
nodes = append(nodes, child)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
node.Children = children
|
||||
|
||||
}
|
||||
items = append(items, &node)
|
||||
}
|
||||
return items, nil
|
||||
return &appv1.ApplicationTree{Nodes: nodes}, nil
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) managedResources(a *appv1.Application, comparisonResult *comparisonResult) ([]*appv1.ResourceDiff, error) {
|
||||
@@ -269,20 +291,20 @@ func (ctrl *ApplicationController) Run(ctx context.Context, statusProcessors int
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) requestAppRefresh(appName string) {
|
||||
func (ctrl *ApplicationController) requestAppRefresh(appName string, fullRefresh bool) {
|
||||
ctrl.refreshRequestedAppsMutex.Lock()
|
||||
defer ctrl.refreshRequestedAppsMutex.Unlock()
|
||||
ctrl.refreshRequestedApps[appName] = true
|
||||
ctrl.refreshRequestedApps[appName] = fullRefresh || ctrl.refreshRequestedApps[appName]
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) isRefreshRequested(appName string) bool {
|
||||
func (ctrl *ApplicationController) isRefreshRequested(appName string) (bool, bool) {
|
||||
ctrl.refreshRequestedAppsMutex.Lock()
|
||||
defer ctrl.refreshRequestedAppsMutex.Unlock()
|
||||
_, ok := ctrl.refreshRequestedApps[appName]
|
||||
fullRefresh, ok := ctrl.refreshRequestedApps[appName]
|
||||
if ok {
|
||||
delete(ctrl.refreshRequestedApps, appName)
|
||||
}
|
||||
return ok
|
||||
return ok, fullRefresh
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) processAppOperationQueueItem() (processNext bool) {
|
||||
@@ -329,6 +351,10 @@ func (ctrl *ApplicationController) processAppOperationQueueItem() (processNext b
|
||||
return
|
||||
}
|
||||
|
||||
func shouldBeDeleted(app *appv1.Application, obj *unstructured.Unstructured) bool {
|
||||
return !kube.IsCRD(obj) && !isSelfReferencedApp(app, kube.GetObjectRef(obj))
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Application) error {
|
||||
logCtx := log.WithField("application", app.Name)
|
||||
logCtx.Infof("Deleting resources")
|
||||
@@ -347,11 +373,20 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic
|
||||
}
|
||||
objs := make([]*unstructured.Unstructured, 0)
|
||||
for k := range objsMap {
|
||||
objs = append(objs, objsMap[k])
|
||||
if objsMap[k].GetDeletionTimestamp() == nil && shouldBeDeleted(app, objsMap[k]) {
|
||||
objs = append(objs, objsMap[k])
|
||||
}
|
||||
}
|
||||
|
||||
cluster, err := ctrl.db.GetCluster(context.Background(), app.Spec.Destination.Server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config := metrics.AddMetricsTransportWrapper(ctrl.metricsServer, app, cluster.RESTConfig())
|
||||
|
||||
err = util.RunAllAsync(len(objs), func(i int) error {
|
||||
obj := objs[i]
|
||||
return ctrl.stateCache.Delete(app.Spec.Destination.Server, obj)
|
||||
return ctrl.kubectl.DeleteResource(config, obj.GroupVersionKind(), obj.GetName(), obj.GetNamespace(), false)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -361,6 +396,11 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for k, obj := range objsMap {
|
||||
if !shouldBeDeleted(app, obj) {
|
||||
delete(objsMap, k)
|
||||
}
|
||||
}
|
||||
if len(objsMap) > 0 {
|
||||
logCtx.Infof("%d objects remaining for deletion", len(objsMap))
|
||||
return nil
|
||||
@@ -475,7 +515,7 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli
|
||||
if state.Phase.Completed() {
|
||||
// if we just completed an operation, force a refresh so that UI will report up-to-date
|
||||
// sync/health information
|
||||
ctrl.requestAppRefresh(app.ObjectMeta.Name)
|
||||
ctrl.requestAppRefresh(app.ObjectMeta.Name, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,19 +606,40 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
log.Warnf("Key '%s' in index is not an application", appKey)
|
||||
return
|
||||
}
|
||||
needRefresh, refreshType := ctrl.needRefreshAppStatus(origApp, ctrl.statusRefreshTimeout)
|
||||
needRefresh, refreshType, fullRefresh := ctrl.needRefreshAppStatus(origApp, ctrl.statusRefreshTimeout)
|
||||
|
||||
if !needRefresh {
|
||||
return
|
||||
}
|
||||
|
||||
startTime := time.Now()
|
||||
defer func() {
|
||||
reconcileDuration := time.Now().Sub(startTime)
|
||||
reconcileDuration := time.Since(startTime)
|
||||
ctrl.metricsServer.IncReconcile(origApp, reconcileDuration)
|
||||
logCtx := log.WithFields(log.Fields{"application": origApp.Name, "time_ms": reconcileDuration.Seconds() * 1e3})
|
||||
logCtx := log.WithFields(log.Fields{"application": origApp.Name, "time_ms": reconcileDuration.Seconds() * 1e3, "full": fullRefresh})
|
||||
logCtx.Info("Reconciliation completed")
|
||||
}()
|
||||
|
||||
app := origApp.DeepCopy()
|
||||
logCtx := log.WithFields(log.Fields{"application": app.Name})
|
||||
if !fullRefresh {
|
||||
if managedResources, err := ctrl.cache.GetAppManagedResources(app.Name); err != nil {
|
||||
logCtx.Warnf("Failed to get cached managed resources for tree reconciliation, fallback to full reconciliation")
|
||||
} else {
|
||||
if tree, err := ctrl.getResourceTree(app, managedResources); err != nil {
|
||||
app.Status.Conditions = []appv1.ApplicationCondition{{Type: appv1.ApplicationConditionComparisonError, Message: err.Error()}}
|
||||
} else {
|
||||
app.Status.Summary = tree.GetSummary()
|
||||
if err = ctrl.cache.SetAppResourcesTree(app.Name, tree); err != nil {
|
||||
logCtx.Errorf("Failed to cache resources tree: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
app.Status.ObservedAt = metav1.Now()
|
||||
ctrl.persistAppStatus(origApp, &app.Status)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
conditions, hasErrors := ctrl.refreshAppConditions(app)
|
||||
if hasErrors {
|
||||
@@ -596,9 +657,11 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
ctrl.normalizeApplication(origApp, app, compareResult.appSourceType)
|
||||
conditions = append(conditions, compareResult.conditions...)
|
||||
}
|
||||
err = ctrl.setAppManagedResources(app, compareResult)
|
||||
tree, err := ctrl.setAppManagedResources(app, compareResult)
|
||||
if err != nil {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{Type: appv1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
logCtx.Errorf("Failed to cache app resources: %v", err)
|
||||
} else {
|
||||
app.Status.Summary = tree.GetSummary()
|
||||
}
|
||||
|
||||
syncErrCond := ctrl.autoSync(app, compareResult.syncStatus)
|
||||
@@ -606,26 +669,32 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
conditions = append(conditions, *syncErrCond)
|
||||
}
|
||||
|
||||
app.Status.ObservedAt = compareResult.observedAt
|
||||
app.Status.ObservedAt = compareResult.reconciledAt
|
||||
app.Status.ReconciledAt = compareResult.reconciledAt
|
||||
app.Status.Sync = *compareResult.syncStatus
|
||||
app.Status.Health = *compareResult.healthStatus
|
||||
app.Status.Resources = compareResult.resources
|
||||
app.Status.Conditions = conditions
|
||||
app.Status.SourceType = compareResult.appSourceType
|
||||
ctrl.persistAppStatus(origApp, &app.Status)
|
||||
return
|
||||
}
|
||||
|
||||
// needRefreshAppStatus answers if application status needs to be refreshed.
|
||||
// Returns true if application never been compared, has changed or comparison result has expired.
|
||||
func (ctrl *ApplicationController) needRefreshAppStatus(app *appv1.Application, statusRefreshTimeout time.Duration) (bool, appv1.RefreshType) {
|
||||
// Additionally returns whether full refresh was requested or not.
|
||||
// If full refresh is requested then target and live state should be reconciled, else only live state tree should be updated.
|
||||
func (ctrl *ApplicationController) needRefreshAppStatus(app *appv1.Application, statusRefreshTimeout time.Duration) (bool, appv1.RefreshType, bool) {
|
||||
logCtx := log.WithFields(log.Fields{"application": app.Name})
|
||||
var reason string
|
||||
fullRefresh := true
|
||||
refreshType := appv1.RefreshTypeNormal
|
||||
expired := app.Status.ObservedAt.Add(statusRefreshTimeout).Before(time.Now().UTC())
|
||||
expired := app.Status.ReconciledAt.Add(statusRefreshTimeout).Before(time.Now().UTC())
|
||||
if requestedType, ok := app.IsRefreshRequested(); ok {
|
||||
refreshType = requestedType
|
||||
reason = fmt.Sprintf("%s refresh requested", refreshType)
|
||||
} else if ctrl.isRefreshRequested(app.Name) {
|
||||
} else if requested, full := ctrl.isRefreshRequested(app.Name); requested {
|
||||
fullRefresh = full
|
||||
reason = fmt.Sprintf("controller refresh requested")
|
||||
} else if app.Status.Sync.Status == appv1.SyncStatusCodeUnknown && expired {
|
||||
reason = "comparison status unknown"
|
||||
@@ -634,13 +703,13 @@ func (ctrl *ApplicationController) needRefreshAppStatus(app *appv1.Application,
|
||||
} else if !app.Spec.Destination.Equals(app.Status.Sync.ComparedTo.Destination) {
|
||||
reason = "spec.destination differs"
|
||||
} else if expired {
|
||||
reason = fmt.Sprintf("comparison expired. observedAt: %v, expiry: %v", app.Status.ObservedAt, statusRefreshTimeout)
|
||||
reason = fmt.Sprintf("comparison expired. reconciledAt: %v, expiry: %v", app.Status.ReconciledAt, statusRefreshTimeout)
|
||||
}
|
||||
if reason != "" {
|
||||
logCtx.Infof("Refreshing app status (%s)", reason)
|
||||
return true, refreshType
|
||||
return true, refreshType, fullRefresh
|
||||
}
|
||||
return false, refreshType
|
||||
return false, refreshType, fullRefresh
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) refreshAppConditions(app *appv1.Application) ([]appv1.ApplicationCondition, bool) {
|
||||
@@ -659,7 +728,7 @@ func (ctrl *ApplicationController) refreshAppConditions(app *appv1.Application)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
specConditions, err := argo.GetSpecErrors(context.Background(), &app.Spec, proj, ctrl.repoClientset, ctrl.db)
|
||||
specConditions, err := argo.ValidatePermissions(context.Background(), &app.Spec, proj, ctrl.db)
|
||||
if err != nil {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{
|
||||
Type: appv1.ApplicationConditionUnknownError,
|
||||
@@ -672,11 +741,12 @@ func (ctrl *ApplicationController) refreshAppConditions(app *appv1.Application)
|
||||
|
||||
// List of condition types which have to be reevaluated by controller; all remaining conditions should stay as is.
|
||||
reevaluateTypes := map[appv1.ApplicationConditionType]bool{
|
||||
appv1.ApplicationConditionInvalidSpecError: true,
|
||||
appv1.ApplicationConditionUnknownError: true,
|
||||
appv1.ApplicationConditionComparisonError: true,
|
||||
appv1.ApplicationConditionSharedResourceWarning: true,
|
||||
appv1.ApplicationConditionSyncError: true,
|
||||
appv1.ApplicationConditionInvalidSpecError: true,
|
||||
appv1.ApplicationConditionUnknownError: true,
|
||||
appv1.ApplicationConditionComparisonError: true,
|
||||
appv1.ApplicationConditionSharedResourceWarning: true,
|
||||
appv1.ApplicationConditionSyncError: true,
|
||||
appv1.ApplicationConditionRepeatedResourceWarning: true,
|
||||
}
|
||||
appConditions := make([]appv1.ApplicationCondition, 0)
|
||||
for i := 0; i < len(app.Status.Conditions); i++ {
|
||||
@@ -824,10 +894,7 @@ func alreadyAttemptedSync(app *appv1.Application, commitSHA string) bool {
|
||||
specSource.TargetRevision = ""
|
||||
syncResSource := app.Status.OperationState.SyncResult.Source.DeepCopy()
|
||||
syncResSource.TargetRevision = ""
|
||||
if !reflect.DeepEqual(app.Spec.Source, app.Status.OperationState.SyncResult.Source) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return reflect.DeepEqual(app.Spec.Source, app.Status.OperationState.SyncResult.Source)
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) newApplicationInformerAndLister() (cache.SharedIndexInformer, applisters.ApplicationLister) {
|
||||
@@ -858,7 +925,7 @@ func (ctrl *ApplicationController) newApplicationInformerAndLister() (cache.Shar
|
||||
if oldOK && newOK {
|
||||
if toggledAutomatedSync(oldApp, newApp) {
|
||||
log.WithField("application", newApp.Name).Info("Enabled automated sync")
|
||||
ctrl.requestAppRefresh(newApp.Name)
|
||||
ctrl.requestAppRefresh(newApp.Name, true)
|
||||
}
|
||||
}
|
||||
ctrl.appRefreshQueue.Add(key)
|
||||
@@ -900,6 +967,7 @@ func (ctrl *ApplicationController) watchSettings(ctx context.Context) {
|
||||
ctrl.settingsMgr.Subscribe(updateCh)
|
||||
prevAppLabelKey := ctrl.settings.GetAppInstanceLabelKey()
|
||||
prevResourceExclusions := ctrl.settings.ResourceExclusions
|
||||
prevResourceInclusions := ctrl.settings.ResourceInclusions
|
||||
done := false
|
||||
for !done {
|
||||
select {
|
||||
@@ -912,10 +980,15 @@ func (ctrl *ApplicationController) watchSettings(ctx context.Context) {
|
||||
prevAppLabelKey = newAppLabelKey
|
||||
}
|
||||
if !reflect.DeepEqual(prevResourceExclusions, newSettings.ResourceExclusions) {
|
||||
log.Infof("resource exclusions modified")
|
||||
log.WithFields(log.Fields{"prevResourceExclusions": prevResourceExclusions, "newResourceExclusions": newSettings.ResourceExclusions}).Info("resource exclusions modified")
|
||||
ctrl.stateCache.Invalidate()
|
||||
prevResourceExclusions = newSettings.ResourceExclusions
|
||||
}
|
||||
if !reflect.DeepEqual(prevResourceInclusions, newSettings.ResourceInclusions) {
|
||||
log.WithFields(log.Fields{"prevResourceInclusions": prevResourceInclusions, "newResourceInclusions": newSettings.ResourceInclusions}).Info("resource inclusions modified")
|
||||
ctrl.stateCache.Invalidate()
|
||||
prevResourceInclusions = newSettings.ResourceInclusions
|
||||
}
|
||||
case <-ctx.Done():
|
||||
done = true
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
@@ -105,12 +107,12 @@ data:
|
||||
# minikube
|
||||
name: aHR0cHM6Ly9sb2NhbGhvc3Q6NjQ0Mw==
|
||||
# https://localhost:6443
|
||||
server: aHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3Zj
|
||||
server: aHR0cHM6Ly9sb2NhbGhvc3Q6NjQ0Mw==
|
||||
kind: Secret
|
||||
metadata:
|
||||
labels:
|
||||
argocd.argoproj.io/secret-type: cluster
|
||||
name: localhost-6443
|
||||
name: some-secret
|
||||
namespace: ` + test.FakeArgoCDNamespace + `
|
||||
type: Opaque
|
||||
`
|
||||
@@ -119,6 +121,7 @@ var fakeApp = `
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
uid: "123"
|
||||
name: my-app
|
||||
namespace: ` + test.FakeArgoCDNamespace + `
|
||||
spec:
|
||||
@@ -353,20 +356,26 @@ func TestAutoSyncParameterOverrides(t *testing.T) {
|
||||
// TestFinalizeAppDeletion verifies application deletion
|
||||
func TestFinalizeAppDeletion(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
|
||||
appObj := kube.MustToUnstructured(&app)
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}, managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
||||
kube.GetResourceKey(appObj): appObj,
|
||||
}})
|
||||
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
patched := false
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
defaultReactor := fakeAppCs.ReactionChain[0]
|
||||
fakeAppCs.ReactionChain = nil
|
||||
fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
return defaultReactor.React(action)
|
||||
})
|
||||
fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
patched = true
|
||||
return true, nil, nil
|
||||
})
|
||||
err := ctrl.finalizeApplicationDeletion(app)
|
||||
// TODO: use an interface to fake out the calls to GetResourcesWithLabel and DeleteResourceWithLabel
|
||||
// For now just ensure we have an expected error condition
|
||||
assert.Error(t, err) // Change this to assert.Nil when we stub out GetResourcesWithLabel/DeleteResourceWithLabel
|
||||
assert.False(t, patched) // Change this to assert.True when we stub out GetResourcesWithLabel/DeleteResourceWithLabel
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, patched)
|
||||
}
|
||||
|
||||
// TestNormalizeApplication verifies we normalize an application during reconciliation
|
||||
@@ -442,3 +451,18 @@ func TestNormalizeApplication(t *testing.T) {
|
||||
assert.False(t, normalized)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleAppUpdated(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
|
||||
app.Spec.Destination.Server = common.KubernetesInternalAPIServerAddr
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
|
||||
ctrl.handleAppUpdated(app.Name, true, kube.GetObjectRef(kube.MustToUnstructured(app)))
|
||||
isRequested, _ := ctrl.isRefreshRequested(app.Name)
|
||||
assert.False(t, isRequested)
|
||||
|
||||
ctrl.handleAppUpdated(app.Name, true, corev1.ObjectReference{UID: "test", Kind: kube.DeploymentKind, Name: "test", Namespace: "default"})
|
||||
isRequested, _ = ctrl.isRefreshRequested(app.Name)
|
||||
assert.True(t, isRequested)
|
||||
}
|
||||
|
||||
224
controller/cache/cache.go
vendored
224
controller/cache/cache.go
vendored
@@ -2,15 +2,16 @@ package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
v1 "k8s.io/api/core/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/controller/metrics"
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
@@ -19,19 +20,19 @@ import (
|
||||
)
|
||||
|
||||
type LiveStateCache interface {
|
||||
IsNamespaced(server string, gvk schema.GroupVersionKind) (bool, error)
|
||||
// Returns child nodes for a given k8s resource
|
||||
GetChildren(server string, obj *unstructured.Unstructured) ([]appv1.ResourceNode, 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)) 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)
|
||||
// Starts watching resources of each controlled cluster.
|
||||
Run(ctx context.Context)
|
||||
// Deletes specified resource from cluster.
|
||||
Delete(server string, obj *unstructured.Unstructured) error
|
||||
// Invalidate invalidates the entire cluster state cache
|
||||
Invalidate()
|
||||
}
|
||||
|
||||
type AppUpdatedHandler = func(appName string, fullRefresh bool, ref v1.ObjectReference)
|
||||
|
||||
func GetTargetObjKey(a *appv1.Application, un *unstructured.Unstructured, isNamespaced bool) kube.ResourceKey {
|
||||
key := kube.GetResourceKey(un)
|
||||
if !isNamespaced {
|
||||
@@ -43,41 +44,35 @@ func GetTargetObjKey(a *appv1.Application, un *unstructured.Unstructured, isName
|
||||
return key
|
||||
}
|
||||
|
||||
func NewLiveStateCache(db db.ArgoDB, appInformer cache.SharedIndexInformer, settings *settings.ArgoCDSettings, kubectl kube.Kubectl, onAppUpdated func(appName string)) LiveStateCache {
|
||||
func NewLiveStateCache(
|
||||
db db.ArgoDB,
|
||||
appInformer cache.SharedIndexInformer,
|
||||
settings *settings.ArgoCDSettings,
|
||||
kubectl kube.Kubectl,
|
||||
metricsServer *metrics.MetricsServer,
|
||||
onAppUpdated AppUpdatedHandler) LiveStateCache {
|
||||
|
||||
return &liveStateCache{
|
||||
appInformer: appInformer,
|
||||
db: db,
|
||||
clusters: make(map[string]*clusterInfo),
|
||||
lock: &sync.Mutex{},
|
||||
onAppUpdated: onAppUpdated,
|
||||
kubectl: kubectl,
|
||||
settings: settings,
|
||||
appInformer: appInformer,
|
||||
db: db,
|
||||
clusters: make(map[string]*clusterInfo),
|
||||
lock: &sync.Mutex{},
|
||||
onAppUpdated: onAppUpdated,
|
||||
kubectl: kubectl,
|
||||
settings: settings,
|
||||
metricsServer: metricsServer,
|
||||
}
|
||||
}
|
||||
|
||||
type liveStateCache struct {
|
||||
db db.ArgoDB
|
||||
clusters map[string]*clusterInfo
|
||||
lock *sync.Mutex
|
||||
appInformer cache.SharedIndexInformer
|
||||
onAppUpdated func(appName string)
|
||||
kubectl kube.Kubectl
|
||||
settings *settings.ArgoCDSettings
|
||||
}
|
||||
|
||||
func (c *liveStateCache) processEvent(event watch.EventType, obj *unstructured.Unstructured, url string) error {
|
||||
info, err := c.getSyncedCluster(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return info.processEvent(event, obj)
|
||||
}
|
||||
|
||||
func (c *liveStateCache) removeCluster(server string) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
delete(c.clusters, server)
|
||||
log.Infof("Dropped cluster %s cache", server)
|
||||
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) getCluster(server string) (*clusterInfo, error) {
|
||||
@@ -90,7 +85,7 @@ func (c *liveStateCache) getCluster(server string) (*clusterInfo, error) {
|
||||
return nil, err
|
||||
}
|
||||
info = &clusterInfo{
|
||||
apis: make(map[schema.GroupKind]*gkInfo),
|
||||
apisMeta: make(map[schema.GroupKind]*apiMeta),
|
||||
lock: &sync.Mutex{},
|
||||
nodes: make(map[kube.ResourceKey]*node),
|
||||
nsIndex: make(map[string]map[kube.ResourceKey]*node),
|
||||
@@ -132,28 +127,21 @@ func (c *liveStateCache) Invalidate() {
|
||||
log.Info("live state cache invalidated")
|
||||
}
|
||||
|
||||
func (c *liveStateCache) Delete(server string, obj *unstructured.Unstructured) error {
|
||||
clusterInfo, err := c.getSyncedCluster(server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return clusterInfo.delete(obj)
|
||||
}
|
||||
|
||||
func (c *liveStateCache) IsNamespaced(server string, gvk schema.GroupVersionKind) (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(gvk.GroupKind()), nil
|
||||
return clusterInfo.isNamespaced(obj), nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) GetChildren(server string, obj *unstructured.Unstructured) ([]appv1.ResourceNode, 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 nil, err
|
||||
return err
|
||||
}
|
||||
return clusterInfo.getChildren(obj), nil
|
||||
clusterInfo.iterateHierarchy(key, action)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) GetManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
|
||||
@@ -161,7 +149,7 @@ func (c *liveStateCache) GetManagedLiveObjs(a *appv1.Application, targetObjs []*
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return clusterInfo.getManagedLiveObjs(a, targetObjs)
|
||||
return clusterInfo.getManagedLiveObjs(a, targetObjs, c.metricsServer)
|
||||
}
|
||||
|
||||
func isClusterHasApps(apps []interface{}, cluster *appv1.Cluster) bool {
|
||||
@@ -175,135 +163,29 @@ func isClusterHasApps(apps []interface{}, cluster *appv1.Cluster) bool {
|
||||
|
||||
// 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) {
|
||||
watchingClustersLock := sync.Mutex{}
|
||||
watchingClusters := make(map[string]struct {
|
||||
cancel context.CancelFunc
|
||||
cluster *appv1.Cluster
|
||||
})
|
||||
|
||||
util.RetryUntilSucceed(func() error {
|
||||
clusterEventCallback := func(event *db.ClusterEvent) {
|
||||
info, ok := watchingClusters[event.Cluster.Server]
|
||||
hasApps := isClusterHasApps(c.appInformer.GetStore().List(), event.Cluster)
|
||||
|
||||
// cluster resources must be watched only if cluster has at least one app
|
||||
if (event.Type == watch.Deleted || !hasApps) && ok {
|
||||
info.cancel()
|
||||
watchingClustersLock.Lock()
|
||||
delete(watchingClusters, event.Cluster.Server)
|
||||
watchingClustersLock.Unlock()
|
||||
} else if event.Type != watch.Deleted && !ok && hasApps {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
watchingClustersLock.Lock()
|
||||
watchingClusters[event.Cluster.Server] = struct {
|
||||
cancel context.CancelFunc
|
||||
cluster *appv1.Cluster
|
||||
}{
|
||||
cancel: func() {
|
||||
c.removeCluster(event.Cluster.Server)
|
||||
cancel()
|
||||
},
|
||||
cluster: event.Cluster,
|
||||
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()
|
||||
}
|
||||
watchingClustersLock.Unlock()
|
||||
go c.watchClusterResources(ctx, *event.Cluster)
|
||||
} 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)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
onAppModified := func(obj interface{}) {
|
||||
if app, ok := obj.(*appv1.Application); ok {
|
||||
var cluster *appv1.Cluster
|
||||
info, infoOk := watchingClusters[app.Spec.Destination.Server]
|
||||
if infoOk {
|
||||
cluster = info.cluster
|
||||
} else {
|
||||
cluster, _ = c.db.GetCluster(ctx, app.Spec.Destination.Server)
|
||||
}
|
||||
if cluster != nil {
|
||||
// trigger cluster event every time when app created/deleted to either start or stop watching resources
|
||||
clusterEventCallback(&db.ClusterEvent{Cluster: cluster, Type: watch.Modified})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.appInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: onAppModified,
|
||||
UpdateFunc: func(oldObj, newObj interface{}) {
|
||||
oldApp, oldOk := oldObj.(*appv1.Application)
|
||||
newApp, newOk := newObj.(*appv1.Application)
|
||||
if oldOk && newOk {
|
||||
if oldApp.Spec.Destination.Server != newApp.Spec.Destination.Server {
|
||||
onAppModified(oldObj)
|
||||
onAppModified(newApp)
|
||||
}
|
||||
}
|
||||
},
|
||||
DeleteFunc: onAppModified,
|
||||
})
|
||||
|
||||
return c.db.WatchClusters(ctx, clusterEventCallback)
|
||||
|
||||
}, "watch clusters", ctx, clusterRetryTimeout)
|
||||
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
// watchClusterResources watches for resource changes annotated with application label on specified cluster and schedule corresponding app refresh.
|
||||
func (c *liveStateCache) watchClusterResources(ctx context.Context, item appv1.Cluster) {
|
||||
util.RetryUntilSucceed(func() (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("Recovered from panic: %v\n", r)
|
||||
}
|
||||
}()
|
||||
config := item.RESTConfig()
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
ch, err := c.kubectl.WatchResources(ctx, config, c.settings, func(gk schema.GroupKind) (s string, e error) {
|
||||
clusterInfo, err := c.getSyncedCluster(item.Server)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return clusterInfo.getResourceVersion(gk), nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for event := range ch {
|
||||
if event.WatchEvent != nil {
|
||||
eventObj := event.WatchEvent.Object.(*unstructured.Unstructured)
|
||||
if kube.IsCRD(eventObj) {
|
||||
// restart if new CRD has been created after watch started
|
||||
if event.WatchEvent.Type == watch.Added {
|
||||
c.removeCluster(item.Server)
|
||||
return fmt.Errorf("Restarting the watch because a new CRD %s was added", eventObj.GetName())
|
||||
} else if event.WatchEvent.Type == watch.Deleted {
|
||||
c.removeCluster(item.Server)
|
||||
return fmt.Errorf("Restarting the watch because CRD %s was deleted", eventObj.GetName())
|
||||
}
|
||||
}
|
||||
err = c.processEvent(event.WatchEvent.Type, eventObj, item.Server)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to process event %s for obj %v: %v", event.WatchEvent.Type, event.WatchEvent.Object, err)
|
||||
}
|
||||
} else {
|
||||
err = c.updateCache(item.Server, event.CacheRefresh.GVK.GroupKind(), event.CacheRefresh.ResourceVersion, event.CacheRefresh.Objects)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to process event %s for obj %v: %v", event.WatchEvent.Type, event.WatchEvent.Object, err)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return fmt.Errorf("resource updates channel has closed")
|
||||
}, fmt.Sprintf("watch app resources on %s", item.Server), ctx, clusterRetryTimeout)
|
||||
}
|
||||
|
||||
func (c *liveStateCache) updateCache(server string, gk schema.GroupKind, resourceVersion string, objs []unstructured.Unstructured) error {
|
||||
clusterInfo, err := c.getSyncedCluster(server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clusterInfo.updateCache(gk, resourceVersion, objs)
|
||||
return nil
|
||||
}
|
||||
|
||||
129
controller/cache/cache_test.go
vendored
129
controller/cache/cache_test.go
vendored
@@ -1,129 +0,0 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/kube/kubetest"
|
||||
)
|
||||
|
||||
const (
|
||||
pollInterval = 500 * time.Millisecond
|
||||
)
|
||||
|
||||
func TestWatchClusterResourcesHandlesResourceEvents(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
events := make(chan kube.WatchEvent)
|
||||
defer func() {
|
||||
cancel()
|
||||
close(events)
|
||||
}()
|
||||
|
||||
pod := testPod.DeepCopy()
|
||||
|
||||
kubeMock := &kubetest.MockKubectlCmd{
|
||||
Resources: []kube.ResourcesBatch{{
|
||||
GVK: pod.GroupVersionKind(),
|
||||
Objects: make([]unstructured.Unstructured, 0),
|
||||
}},
|
||||
Events: events,
|
||||
}
|
||||
|
||||
server := "https://test"
|
||||
clusterCache := newClusterExt(kubeMock)
|
||||
|
||||
cache := &liveStateCache{
|
||||
clusters: map[string]*clusterInfo{server: clusterCache},
|
||||
lock: &sync.Mutex{},
|
||||
kubectl: kubeMock,
|
||||
}
|
||||
|
||||
go cache.watchClusterResources(ctx, v1alpha1.Cluster{Server: server})
|
||||
|
||||
assert.False(t, clusterCache.synced())
|
||||
|
||||
events <- kube.WatchEvent{WatchEvent: &watch.Event{Object: pod, Type: watch.Added}}
|
||||
|
||||
err := wait.Poll(pollInterval, wait.ForeverTestTimeout, func() (bool, error) {
|
||||
_, hasPod := clusterCache.nodes[kube.GetResourceKey(pod)]
|
||||
return hasPod, nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
|
||||
pod.SetResourceVersion("updated-resource-version")
|
||||
events <- kube.WatchEvent{WatchEvent: &watch.Event{Object: pod, Type: watch.Modified}}
|
||||
|
||||
err = wait.Poll(pollInterval, wait.ForeverTestTimeout, func() (bool, error) {
|
||||
updatedPodInfo, hasPod := clusterCache.nodes[kube.GetResourceKey(pod)]
|
||||
return hasPod && updatedPodInfo.resourceVersion == "updated-resource-version", nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
|
||||
events <- kube.WatchEvent{WatchEvent: &watch.Event{Object: pod, Type: watch.Deleted}}
|
||||
|
||||
err = wait.Poll(pollInterval, wait.ForeverTestTimeout, func() (bool, error) {
|
||||
_, hasPod := clusterCache.nodes[kube.GetResourceKey(pod)]
|
||||
return !hasPod, nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestClusterCacheDroppedOnCreatedDeletedCRD(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
events := make(chan kube.WatchEvent)
|
||||
defer func() {
|
||||
cancel()
|
||||
close(events)
|
||||
}()
|
||||
|
||||
kubeMock := &kubetest.MockKubectlCmd{
|
||||
Resources: []kube.ResourcesBatch{{
|
||||
GVK: testCRD.GroupVersionKind(),
|
||||
Objects: make([]unstructured.Unstructured, 0),
|
||||
}},
|
||||
Events: events,
|
||||
}
|
||||
|
||||
server := "https://test"
|
||||
clusterCache := newClusterExt(kubeMock)
|
||||
|
||||
cache := &liveStateCache{
|
||||
clusters: map[string]*clusterInfo{server: clusterCache},
|
||||
lock: &sync.Mutex{},
|
||||
kubectl: kubeMock,
|
||||
}
|
||||
|
||||
go cache.watchClusterResources(ctx, v1alpha1.Cluster{Server: server})
|
||||
|
||||
err := clusterCache.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
events <- kube.WatchEvent{WatchEvent: &watch.Event{Object: testCRD, Type: watch.Added}}
|
||||
err = wait.Poll(pollInterval, wait.ForeverTestTimeout, func() (bool, error) {
|
||||
cache.lock.Lock()
|
||||
defer cache.lock.Unlock()
|
||||
_, hasCache := cache.clusters[server]
|
||||
return !hasCache, nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
|
||||
cache.clusters[server] = clusterCache
|
||||
|
||||
events <- kube.WatchEvent{WatchEvent: &watch.Event{Object: testCRD, Type: watch.Deleted}}
|
||||
err = wait.Poll(pollInterval, wait.ForeverTestTimeout, func() (bool, error) {
|
||||
cache.lock.Lock()
|
||||
defer cache.lock.Unlock()
|
||||
_, hasCache := cache.clusters[server]
|
||||
return !hasCache, nil
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
331
controller/cache/cluster.go
vendored
331
controller/cache/cluster.go
vendored
@@ -1,13 +1,15 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/argo-cd/controller/metrics"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
@@ -16,49 +18,44 @@ import (
|
||||
|
||||
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
|
||||
clusterSyncTimeout = 24 * time.Hour
|
||||
clusterRetryTimeout = 10 * time.Second
|
||||
watchResourcesRetryTimeout = 1 * time.Second
|
||||
)
|
||||
|
||||
type gkInfo struct {
|
||||
resource metav1.APIResource
|
||||
type apiMeta struct {
|
||||
namespaced bool
|
||||
resourceVersion string
|
||||
watchCancel context.CancelFunc
|
||||
}
|
||||
|
||||
type clusterInfo struct {
|
||||
apis map[schema.GroupKind]*gkInfo
|
||||
nodes map[kube.ResourceKey]*node
|
||||
nsIndex map[string]map[kube.ResourceKey]*node
|
||||
lock *sync.Mutex
|
||||
onAppUpdated func(appName string)
|
||||
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
|
||||
syncLock *sync.Mutex
|
||||
syncTime *time.Time
|
||||
syncError error
|
||||
log *log.Entry
|
||||
settings *settings.ArgoCDSettings
|
||||
}
|
||||
|
||||
func (c *clusterInfo) getResourceVersion(gk schema.GroupKind) string {
|
||||
func (c *clusterInfo) replaceResourceCache(gk schema.GroupKind, resourceVersion string, objs []unstructured.Unstructured) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
info, ok := c.apis[gk]
|
||||
if ok {
|
||||
return info.resourceVersion
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *clusterInfo) updateCache(gk schema.GroupKind, resourceVersion string, objs []unstructured.Unstructured) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
info, ok := c.apis[gk]
|
||||
info, ok := c.apisMeta[gk]
|
||||
if ok {
|
||||
objByKind := make(map[kube.ResourceKey]*unstructured.Unstructured)
|
||||
for i := range objs {
|
||||
@@ -85,7 +82,7 @@ func (c *clusterInfo) updateCache(gk schema.GroupKind, resourceVersion string, o
|
||||
}
|
||||
}
|
||||
|
||||
func createObjInfo(un *unstructured.Unstructured, appInstanceLabel string) *node {
|
||||
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 {
|
||||
@@ -95,23 +92,19 @@ func createObjInfo(un *unstructured.Unstructured, appInstanceLabel string) *node
|
||||
APIVersion: "",
|
||||
})
|
||||
}
|
||||
info := &node{
|
||||
nodeInfo := &node{
|
||||
resourceVersion: un.GetResourceVersion(),
|
||||
ref: v1.ObjectReference{
|
||||
APIVersion: un.GetAPIVersion(),
|
||||
Kind: un.GetKind(),
|
||||
Name: un.GetName(),
|
||||
Namespace: un.GetNamespace(),
|
||||
},
|
||||
ownerRefs: ownerRefs,
|
||||
info: getNodeInfo(un),
|
||||
ref: kube.GetObjectRef(un),
|
||||
ownerRefs: ownerRefs,
|
||||
}
|
||||
populateNodeInfo(un, nodeInfo)
|
||||
appName := kube.GetAppInstanceLabel(un, appInstanceLabel)
|
||||
if len(ownerRefs) == 0 && appName != "" {
|
||||
info.appName = appName
|
||||
info.resource = un
|
||||
nodeInfo.appName = appName
|
||||
nodeInfo.resource = un
|
||||
}
|
||||
return info
|
||||
nodeInfo.health, _ = health.GetResourceHealth(un, c.settings.ResourceOverrides)
|
||||
return nodeInfo
|
||||
}
|
||||
|
||||
func (c *clusterInfo) setNode(n *node) {
|
||||
@@ -136,7 +129,13 @@ func (c *clusterInfo) removeNode(key kube.ResourceKey) {
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -149,38 +148,163 @@ func (c *clusterInfo) synced() bool {
|
||||
return time.Now().Before(c.syncTime.Add(clusterSyncTimeout))
|
||||
}
|
||||
|
||||
func (c *clusterInfo) sync() (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
|
||||
}
|
||||
}()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
c.log.Info("Start syncing cluster")
|
||||
// startMissingWatches lists supported cluster resources and start watching for changes unless watch is already running
|
||||
func (c *clusterInfo) startMissingWatches() error {
|
||||
|
||||
c.apis = make(map[schema.GroupKind]*gkInfo)
|
||||
c.nodes = make(map[kube.ResourceKey]*node)
|
||||
|
||||
resources, err := c.kubectl.GetResources(c.cluster.RESTConfig(), c.settings, "")
|
||||
apis, err := c.kubectl.GetAPIResources(c.cluster.RESTConfig(), c.settings)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to sync cluster %s: %v", c.cluster.Server, err)
|
||||
return err
|
||||
}
|
||||
|
||||
appLabelKey := c.settings.GetAppInstanceLabelKey()
|
||||
for res := range resources {
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
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)
|
||||
}
|
||||
if _, ok := c.apis[res.GVK.GroupKind()]; !ok {
|
||||
c.apis[res.GVK.GroupKind()] = &gkInfo{
|
||||
resourceVersion: res.ListResourceVersion,
|
||||
resource: res.ResourceInfo,
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := range res.Objects {
|
||||
c.setNode(createObjInfo(&res.Objects[i], appLabelKey))
|
||||
|
||||
}, 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")
|
||||
@@ -188,9 +312,6 @@ func (c *clusterInfo) sync() (err error) {
|
||||
}
|
||||
|
||||
func (c *clusterInfo) ensureSynced() error {
|
||||
if c.synced() {
|
||||
return c.syncError
|
||||
}
|
||||
c.syncLock.Lock()
|
||||
defer c.syncLock.Unlock()
|
||||
if c.synced() {
|
||||
@@ -204,29 +325,29 @@ func (c *clusterInfo) ensureSynced() error {
|
||||
return c.syncError
|
||||
}
|
||||
|
||||
func (c *clusterInfo) getChildren(obj *unstructured.Unstructured) []appv1.ResourceNode {
|
||||
func (c *clusterInfo) iterateHierarchy(key kube.ResourceKey, action func(child appv1.ResourceNode)) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
children := make([]appv1.ResourceNode, 0)
|
||||
if objInfo, ok := c.nodes[kube.GetResourceKey(obj)]; ok {
|
||||
nsNodes := c.nsIndex[obj.GetNamespace()]
|
||||
if objInfo, ok := c.nodes[key]; ok {
|
||||
action(objInfo.asResourceNode())
|
||||
nsNodes := c.nsIndex[key.Namespace]
|
||||
for _, child := range nsNodes {
|
||||
if objInfo.isParentOf(child) {
|
||||
children = append(children, child.childResourceNodes(nsNodes, map[kube.ResourceKey]bool{objInfo.resourceKey(): true}))
|
||||
action(child.asResourceNode())
|
||||
child.iterateChildren(nsNodes, map[kube.ResourceKey]bool{objInfo.resourceKey(): true}, action)
|
||||
}
|
||||
}
|
||||
}
|
||||
return children
|
||||
}
|
||||
|
||||
func (c *clusterInfo) isNamespaced(gk schema.GroupKind) bool {
|
||||
if api, ok := c.apis[gk]; ok && !api.resource.Namespaced {
|
||||
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) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
|
||||
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()
|
||||
|
||||
@@ -237,12 +358,13 @@ func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*uns
|
||||
managedObjs[key] = o.resource
|
||||
}
|
||||
}
|
||||
config := metrics.AddMetricsTransportWrapper(metricsServer, a, c.cluster.RESTConfig())
|
||||
// iterate target objects and identify ones that already exist in the cluster,\
|
||||
// but are simply missing our label
|
||||
lock := &sync.Mutex{}
|
||||
err := util.RunAllAsync(len(targetObjs), func(i int) error {
|
||||
targetObj := targetObjs[i]
|
||||
key := GetTargetObjKey(a, targetObj, c.isNamespaced(targetObj.GroupVersionKind().GroupKind()))
|
||||
key := GetTargetObjKey(a, targetObj, c.isNamespaced(targetObj))
|
||||
lock.Lock()
|
||||
managedObj := managedObjs[key]
|
||||
lock.Unlock()
|
||||
@@ -253,10 +375,9 @@ func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*uns
|
||||
managedObj = existingObj.resource
|
||||
} else {
|
||||
var err error
|
||||
managedObj, err = c.kubectl.GetResource(c.cluster.RESTConfig(), targetObj.GroupVersionKind(), existingObj.ref.Name, existingObj.ref.Namespace)
|
||||
managedObj, err = c.kubectl.GetResource(config, targetObj.GroupVersionKind(), existingObj.ref.Name, existingObj.ref.Namespace)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
c.checkAndInvalidateStaleCache(targetObj.GroupVersionKind(), existingObj.ref.Namespace, existingObj.ref.Name)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
@@ -266,9 +387,19 @@ func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*uns
|
||||
}
|
||||
|
||||
if managedObj != nil {
|
||||
managedObj, err := c.kubectl.ConvertToVersion(managedObj, targetObj.GroupVersionKind().Group, targetObj.GroupVersionKind().Version)
|
||||
converted, err := c.kubectl.ConvertToVersion(managedObj, targetObj.GroupVersionKind().Group, targetObj.GroupVersionKind().Version)
|
||||
if err != nil {
|
||||
return err
|
||||
// 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
|
||||
@@ -283,37 +414,10 @@ func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*uns
|
||||
return managedObjs, nil
|
||||
}
|
||||
|
||||
func (c *clusterInfo) delete(obj *unstructured.Unstructured) error {
|
||||
err := c.kubectl.DeleteResource(c.cluster.RESTConfig(), obj.GroupVersionKind(), obj.GetName(), obj.GetNamespace(), false)
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
// a delete request came in for an object which does not exist. it's possible that our cache
|
||||
// is stale. Check and invalidate if it is
|
||||
c.lock.Lock()
|
||||
c.checkAndInvalidateStaleCache(obj.GroupVersionKind(), obj.GetNamespace(), obj.GetName())
|
||||
c.lock.Unlock()
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// checkAndInvalidateStaleCache checks if our cache is stale and invalidate it based on error
|
||||
// should be called whenever we suspect our cache is stale
|
||||
func (c *clusterInfo) checkAndInvalidateStaleCache(gvk schema.GroupVersionKind, namespace string, name string) {
|
||||
if _, ok := c.nodes[kube.NewResourceKey(gvk.Group, gvk.Kind, namespace, name)]; ok {
|
||||
if c.syncTime != nil {
|
||||
c.log.Warnf("invalidated stale cache due to mismatch of %s, %s/%s", gvk, namespace, name)
|
||||
c.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *clusterInfo) processEvent(event watch.EventType, un *unstructured.Unstructured) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
key := kube.GetResourceKey(un)
|
||||
if info, ok := c.apis[schema.GroupKind{Group: key.Group, Kind: key.Kind}]; ok {
|
||||
info.resourceVersion = un.GetResourceVersion()
|
||||
}
|
||||
existingNode, exists := c.nodes[key]
|
||||
if event == watch.Deleted {
|
||||
if exists {
|
||||
@@ -331,7 +435,7 @@ func (c *clusterInfo) onNodeUpdated(exists bool, existingNode *node, un *unstruc
|
||||
if exists {
|
||||
nodes = append(nodes, existingNode)
|
||||
}
|
||||
newObj := createObjInfo(un, c.settings.GetAppInstanceLabelKey())
|
||||
newObj := c.createObjInfo(un, c.settings.GetAppInstanceLabelKey())
|
||||
c.setNode(newObj)
|
||||
nodes = append(nodes, newObj)
|
||||
toNotify := make(map[string]bool)
|
||||
@@ -342,18 +446,23 @@ func (c *clusterInfo) onNodeUpdated(exists bool, existingNode *node, un *unstruc
|
||||
if app == "" || skipAppRequeing(key) {
|
||||
continue
|
||||
}
|
||||
toNotify[app] = true
|
||||
toNotify[app] = n.isRootAppNode() || toNotify[app]
|
||||
}
|
||||
}
|
||||
for name := range toNotify {
|
||||
c.onAppUpdated(name)
|
||||
for name, full := range toNotify {
|
||||
c.onAppUpdated(name, full, newObj.ref)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *clusterInfo) onNodeRemoved(key kube.ResourceKey, existingNode *node) {
|
||||
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 existingNode.appName != "" {
|
||||
c.onAppUpdated(existingNode.appName)
|
||||
if appName != "" {
|
||||
c.onAppUpdated(appName, n.isRootAppNode(), n.ref)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
221
controller/cache/cluster_test.go
vendored
221
controller/cache/cluster_test.go
vendored
@@ -1,6 +1,7 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -12,8 +13,10 @@ import (
|
||||
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"
|
||||
@@ -49,8 +52,7 @@ var (
|
||||
resourceVersion: "123"`)
|
||||
|
||||
testRS = strToUnstructured(`
|
||||
apiVersion: v1
|
||||
apiVersion: extensions/v1beta1
|
||||
apiVersion: apps/v1
|
||||
kind: ReplicaSet
|
||||
metadata:
|
||||
name: helm-guestbook-rs
|
||||
@@ -62,7 +64,7 @@ var (
|
||||
resourceVersion: "123"`)
|
||||
|
||||
testDeploy = strToUnstructured(`
|
||||
apiVersion: extensions/v1beta1
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
@@ -71,76 +73,140 @@ var (
|
||||
namespace: default
|
||||
resourceVersion: "123"`)
|
||||
|
||||
testCRD = strToUnstructured(`
|
||||
apiVersion: apiextensions.k8s.io/v1beta1
|
||||
kind: CustomResourceDefinition
|
||||
testService = strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: my-custom-resource-definition
|
||||
resourceVersion: "123"`)
|
||||
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 {
|
||||
resByGVK := make(map[schema.GroupVersionKind][]unstructured.Unstructured)
|
||||
runtimeObjs := make([]runtime.Object, len(objs))
|
||||
for i := range objs {
|
||||
resByGVK[objs[i].GroupVersionKind()] = append(resByGVK[objs[i].GroupVersionKind()], *objs[i])
|
||||
runtimeObjs[i] = objs[i]
|
||||
}
|
||||
resources := make([]kube.ResourcesBatch, 0)
|
||||
for gvk, objects := range resByGVK {
|
||||
resources = append(resources, kube.ResourcesBatch{
|
||||
ListResourceVersion: "1",
|
||||
GVK: gvk,
|
||||
Objects: objects,
|
||||
})
|
||||
}
|
||||
return newClusterExt(kubetest.MockKubectlCmd{
|
||||
Resources: resources,
|
||||
})
|
||||
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) {},
|
||||
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{},
|
||||
apis: make(map[schema.GroupKind]*gkInfo),
|
||||
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 := cluster.getChildren(testRS)
|
||||
rsChildren := getChildren(cluster, testRS)
|
||||
assert.Equal(t, []appv1.ResourceNode{{
|
||||
Kind: "Pod",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-pod",
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
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"}},
|
||||
Children: make([]appv1.ResourceNode, 0),
|
||||
ResourceVersion: "123",
|
||||
}}, rsChildren)
|
||||
deployChildren := cluster.getChildren(testDeploy)
|
||||
deployChildren := getChildren(cluster, testDeploy)
|
||||
|
||||
assert.Equal(t, []appv1.ResourceNode{{
|
||||
Kind: "ReplicaSet",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-rs",
|
||||
Group: "extensions",
|
||||
Version: "v1beta1",
|
||||
assert.Equal(t, append([]appv1.ResourceNode{{
|
||||
ResourceRef: appv1.ResourceRef{
|
||||
Kind: "ReplicaSet",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-rs",
|
||||
Group: "apps",
|
||||
Version: "v1",
|
||||
},
|
||||
ResourceVersion: "123",
|
||||
Children: rsChildren,
|
||||
Health: &appv1.HealthStatus{Status: appv1.HealthStatusHealthy},
|
||||
Info: []appv1.InfoItem{},
|
||||
}}, deployChildren)
|
||||
ParentRefs: []appv1.ResourceRef{{Group: "apps", Version: "", Kind: "Deployment", Namespace: "default", Name: "helm-guestbook"}},
|
||||
}}, rsChildren...), deployChildren)
|
||||
}
|
||||
|
||||
func TestGetManagedLiveObjs(t *testing.T) {
|
||||
@@ -149,7 +215,7 @@ func TestGetManagedLiveObjs(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
|
||||
targetDeploy := strToUnstructured(`
|
||||
apiVersion: extensions/v1beta1
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
@@ -163,7 +229,7 @@ metadata:
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
}, []*unstructured.Unstructured{targetDeploy})
|
||||
}, []*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,
|
||||
@@ -178,7 +244,7 @@ func TestChildDeletedEvent(t *testing.T) {
|
||||
err = cluster.processEvent(watch.Deleted, testPod)
|
||||
assert.Nil(t, err)
|
||||
|
||||
rsChildren := cluster.getChildren(testRS)
|
||||
rsChildren := getChildren(cluster, testRS)
|
||||
assert.Equal(t, []appv1.ResourceNode{}, rsChildren)
|
||||
}
|
||||
|
||||
@@ -202,27 +268,47 @@ func TestProcessNewChildEvent(t *testing.T) {
|
||||
err = cluster.processEvent(watch.Added, newPod)
|
||||
assert.Nil(t, err)
|
||||
|
||||
rsChildren := cluster.getChildren(testRS)
|
||||
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{{
|
||||
Kind: "Pod",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-pod",
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Info: []appv1.InfoItem{{Name: "Containers", Value: "0/0"}},
|
||||
Children: make([]appv1.ResourceNode, 0),
|
||||
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",
|
||||
}, {
|
||||
Kind: "Pod",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-pod2",
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Info: []appv1.InfoItem{{Name: "Containers", Value: "0/0"}},
|
||||
Children: make([]appv1.ResourceNode, 0),
|
||||
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)
|
||||
}
|
||||
@@ -269,8 +355,8 @@ func TestUpdateResourceTags(t *testing.T) {
|
||||
func TestUpdateAppResource(t *testing.T) {
|
||||
updatesReceived := make([]string, 0)
|
||||
cluster := newCluster(testPod, testRS, testDeploy)
|
||||
cluster.onAppUpdated = func(appName string) {
|
||||
updatesReceived = append(updatesReceived, appName)
|
||||
cluster.onAppUpdated = func(appName string, fullRefresh bool, _ corev1.ObjectReference) {
|
||||
updatesReceived = append(updatesReceived, fmt.Sprintf("%s: %v", appName, fullRefresh))
|
||||
}
|
||||
|
||||
err := cluster.ensureSynced()
|
||||
@@ -279,7 +365,7 @@ func TestUpdateAppResource(t *testing.T) {
|
||||
err = cluster.processEvent(watch.Modified, mustToUnstructured(testPod))
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, []string{"helm-guestbook"}, updatesReceived)
|
||||
assert.Contains(t, updatesReceived, "helm-guestbook: false")
|
||||
}
|
||||
|
||||
func TestCircularReference(t *testing.T) {
|
||||
@@ -294,8 +380,13 @@ func TestCircularReference(t *testing.T) {
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
children := cluster.getChildren(dep)
|
||||
assert.Len(t, children, 1)
|
||||
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) {
|
||||
@@ -316,7 +407,7 @@ func TestWatchCacheUpdated(t *testing.T) {
|
||||
|
||||
podGroupKind := testPod.GroupVersionKind().GroupKind()
|
||||
|
||||
cluster.updateCache(podGroupKind, "updated-list-version", []unstructured.Unstructured{*updated, *added})
|
||||
cluster.replaceResourceCache(podGroupKind, "updated-list-version", []unstructured.Unstructured{*updated, *added})
|
||||
|
||||
_, ok := cluster.nodes[kube.GetResourceKey(removed)]
|
||||
assert.False(t, ok)
|
||||
@@ -327,6 +418,4 @@ func TestWatchCacheUpdated(t *testing.T) {
|
||||
|
||||
_, ok = cluster.nodes[kube.GetResourceKey(added)]
|
||||
assert.True(t, ok)
|
||||
|
||||
assert.Equal(t, cluster.getResourceVersion(podGroupKind), "updated-list-version")
|
||||
}
|
||||
|
||||
161
controller/cache/info.go
vendored
161
controller/cache/info.go
vendored
@@ -3,29 +3,154 @@ package cache
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
k8snode "k8s.io/kubernetes/pkg/util/node"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
)
|
||||
|
||||
func getNodeInfo(un *unstructured.Unstructured) []v1alpha1.InfoItem {
|
||||
gvk := un.GroupVersionKind()
|
||||
func populateNodeInfo(un *unstructured.Unstructured, node *node) {
|
||||
|
||||
if gvk.Kind == kube.PodKind && gvk.Group == "" {
|
||||
return getPodInfo(un)
|
||||
gvk := un.GroupVersionKind()
|
||||
switch gvk.Group {
|
||||
case "":
|
||||
switch gvk.Kind {
|
||||
case kube.PodKind:
|
||||
populatePodInfo(un, node)
|
||||
return
|
||||
case kube.ServiceKind:
|
||||
populateServiceInfo(un, node)
|
||||
return
|
||||
}
|
||||
case "extensions":
|
||||
switch gvk.Kind {
|
||||
case kube.IngressKind:
|
||||
populateIngressInfo(un, node)
|
||||
return
|
||||
}
|
||||
}
|
||||
return []v1alpha1.InfoItem{}
|
||||
node.info = []v1alpha1.InfoItem{}
|
||||
}
|
||||
|
||||
func getPodInfo(un *unstructured.Unstructured) []v1alpha1.InfoItem {
|
||||
func getIngress(un *unstructured.Unstructured) []v1.LoadBalancerIngress {
|
||||
ingress, ok, err := unstructured.NestedSlice(un.Object, "status", "loadBalancer", "ingress")
|
||||
if !ok || err != nil {
|
||||
return nil
|
||||
}
|
||||
res := make([]v1.LoadBalancerIngress, 0)
|
||||
for _, item := range ingress {
|
||||
if lbIngress, ok := item.(map[string]interface{}); ok {
|
||||
if hostname := lbIngress["hostname"]; hostname != nil {
|
||||
res = append(res, v1.LoadBalancerIngress{Hostname: fmt.Sprintf("%s", hostname)})
|
||||
} else if ip := lbIngress["ip"]; ip != nil {
|
||||
res = append(res, v1.LoadBalancerIngress{IP: fmt.Sprintf("%s", ip)})
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
node.networkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetLabels: targetLabels, Ingress: ingress}
|
||||
}
|
||||
|
||||
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 {
|
||||
targetsMap[v1alpha1.ResourceRef{
|
||||
Group: "",
|
||||
Kind: kube.ServiceKind,
|
||||
Namespace: un.GetNamespace(),
|
||||
Name: fmt.Sprintf("%s", backend["serviceName"]),
|
||||
}] = true
|
||||
}
|
||||
urlsSet := make(map[string]bool)
|
||||
if rules, ok, err := unstructured.NestedSlice(un.Object, "spec", "rules"); ok && err == nil {
|
||||
for i := range rules {
|
||||
rule, ok := rules[i].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
host := rule["host"]
|
||||
if host == nil || host == "" {
|
||||
for i := range ingress {
|
||||
host = util.FirstNonEmpty(ingress[i].Hostname, ingress[i].IP)
|
||||
if host != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
paths, ok, err := unstructured.NestedSlice(rule, "http", "paths")
|
||||
if !ok || err != nil {
|
||||
continue
|
||||
}
|
||||
for i := range paths {
|
||||
path, ok := paths[i].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if serviceName, ok, err := unstructured.NestedString(path, "backend", "serviceName"); ok && err == nil {
|
||||
targetsMap[v1alpha1.ResourceRef{
|
||||
Group: "",
|
||||
Kind: kube.ServiceKind,
|
||||
Namespace: un.GetNamespace(),
|
||||
Name: serviceName,
|
||||
}] = true
|
||||
}
|
||||
|
||||
if port, ok, err := unstructured.NestedFieldNoCopy(path, "backend", "servicePort"); ok && err == nil && host != "" && host != nil {
|
||||
stringPort := ""
|
||||
switch typedPod := port.(type) {
|
||||
case int64:
|
||||
stringPort = fmt.Sprintf("%d", typedPod)
|
||||
case float64:
|
||||
stringPort = fmt.Sprintf("%d", int64(typedPod))
|
||||
case string:
|
||||
stringPort = typedPod
|
||||
default:
|
||||
stringPort = fmt.Sprintf("%v", port)
|
||||
}
|
||||
|
||||
switch stringPort {
|
||||
case "80", "http":
|
||||
urlsSet[fmt.Sprintf("http://%s", host)] = true
|
||||
case "443", "https":
|
||||
urlsSet[fmt.Sprintf("https://%s", host)] = true
|
||||
default:
|
||||
urlsSet[fmt.Sprintf("http://%s:%s", host, stringPort)] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
targets := make([]v1alpha1.ResourceRef, 0)
|
||||
for target := range targetsMap {
|
||||
targets = append(targets, target)
|
||||
}
|
||||
urls := make([]string, 0)
|
||||
for url := range urlsSet {
|
||||
urls = append(urls, url)
|
||||
}
|
||||
node.networkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetRefs: targets, Ingress: ingress, ExternalURLs: urls}
|
||||
}
|
||||
|
||||
func populatePodInfo(un *unstructured.Unstructured, node *node) {
|
||||
pod := v1.Pod{}
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, &pod)
|
||||
if err != nil {
|
||||
return []v1alpha1.InfoItem{}
|
||||
node.info = []v1alpha1.InfoItem{}
|
||||
return
|
||||
}
|
||||
restarts := 0
|
||||
totalContainers := len(pod.Spec.Containers)
|
||||
@@ -36,6 +161,19 @@ func getPodInfo(un *unstructured.Unstructured) []v1alpha1.InfoItem {
|
||||
reason = pod.Status.Reason
|
||||
}
|
||||
|
||||
imagesSet := make(map[string]bool)
|
||||
for _, container := range pod.Spec.InitContainers {
|
||||
imagesSet[container.Image] = true
|
||||
}
|
||||
for _, container := range pod.Spec.Containers {
|
||||
imagesSet[container.Image] = true
|
||||
}
|
||||
|
||||
node.images = nil
|
||||
for image := range imagesSet {
|
||||
node.images = append(node.images, image)
|
||||
}
|
||||
|
||||
initializing := false
|
||||
for i := range pod.Status.InitContainerStatuses {
|
||||
container := pod.Status.InitContainerStatuses[i]
|
||||
@@ -99,9 +237,10 @@ func getPodInfo(un *unstructured.Unstructured) []v1alpha1.InfoItem {
|
||||
reason = "Terminating"
|
||||
}
|
||||
|
||||
info := make([]v1alpha1.InfoItem, 0)
|
||||
node.info = make([]v1alpha1.InfoItem, 0)
|
||||
if reason != "" {
|
||||
info = append(info, v1alpha1.InfoItem{Name: "Status Reason", Value: reason})
|
||||
node.info = append(node.info, v1alpha1.InfoItem{Name: "Status Reason", Value: reason})
|
||||
}
|
||||
return append(info, v1alpha1.InfoItem{Name: "Containers", Value: fmt.Sprintf("%d/%d", readyContainers, totalContainers)})
|
||||
node.info = append(node.info, v1alpha1.InfoItem{Name: "Containers", Value: fmt.Sprintf("%d/%d", readyContainers, totalContainers)})
|
||||
node.networkingInfo = &v1alpha1.ResourceNetworkingInfo{Labels: un.GetLabels()}
|
||||
}
|
||||
|
||||
108
controller/cache/info_test.go
vendored
Normal file
108
controller/cache/info_test.go
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetPodInfo(t *testing.T) {
|
||||
pod := strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: helm-guestbook-pod
|
||||
namespace: default
|
||||
ownerReferences:
|
||||
- apiVersion: extensions/v1beta1
|
||||
kind: ReplicaSet
|
||||
name: helm-guestbook-rs
|
||||
resourceVersion: "123"
|
||||
labels:
|
||||
app: guestbook
|
||||
spec:
|
||||
containers:
|
||||
- image: bar`)
|
||||
|
||||
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) {
|
||||
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"}},
|
||||
}, node.networkingInfo)
|
||||
}
|
||||
|
||||
func TestGetIngressInfo(t *testing.T) {
|
||||
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"}},
|
||||
TargetRefs: []v1alpha1.ResourceRef{{
|
||||
Namespace: "default",
|
||||
Group: "",
|
||||
Kind: kube.ServiceKind,
|
||||
Name: "not-found-service",
|
||||
}, {
|
||||
Namespace: "default",
|
||||
Group: "",
|
||||
Kind: kube.ServiceKind,
|
||||
Name: "helm-guestbook",
|
||||
}},
|
||||
ExternalURLs: []string{"https://helm-guestbook.com"},
|
||||
}, node.networkingInfo)
|
||||
}
|
||||
|
||||
func TestGetIngressInfoNoHost(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: /
|
||||
status:
|
||||
loadBalancer:
|
||||
ingress:
|
||||
- ip: 107.178.210.11`)
|
||||
|
||||
node := &node{}
|
||||
populateNodeInfo(ingress, node)
|
||||
|
||||
assert.Equal(t, &v1alpha1.ResourceNetworkingInfo{
|
||||
Ingress: []v1.LoadBalancerIngress{{IP: "107.178.210.11"}},
|
||||
TargetRefs: []v1alpha1.ResourceRef{{
|
||||
Namespace: "default",
|
||||
Group: "",
|
||||
Kind: kube.ServiceKind,
|
||||
Name: "helm-guestbook",
|
||||
}},
|
||||
ExternalURLs: []string{"https://107.178.210.11"},
|
||||
}, node.networkingInfo)
|
||||
}
|
||||
64
controller/cache/mocks/LiveStateCache.go
vendored
64
controller/cache/mocks/LiveStateCache.go
vendored
@@ -2,14 +2,11 @@
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
import "github.com/argoproj/argo-cd/util/kube"
|
||||
import "github.com/stretchr/testify/mock"
|
||||
import "k8s.io/apimachinery/pkg/runtime/schema"
|
||||
import "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
import "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 {
|
||||
@@ -30,29 +27,6 @@ func (_m *LiveStateCache) Delete(server string, obj *unstructured.Unstructured)
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetChildren provides a mock function with given fields: server, obj
|
||||
func (_m *LiveStateCache) GetChildren(server string, obj *unstructured.Unstructured) ([]v1alpha1.ResourceNode, error) {
|
||||
ret := _m.Called(server, obj)
|
||||
|
||||
var r0 []v1alpha1.ResourceNode
|
||||
if rf, ok := ret.Get(0).(func(string, *unstructured.Unstructured) []v1alpha1.ResourceNode); ok {
|
||||
r0 = rf(server, obj)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]v1alpha1.ResourceNode)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, *unstructured.Unstructured) error); ok {
|
||||
r1 = rf(server, obj)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetManagedLiveObjs provides a mock function with given fields: a, targetObjs
|
||||
func (_m *LiveStateCache) GetManagedLiveObjs(a *v1alpha1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
|
||||
ret := _m.Called(a, targetObjs)
|
||||
@@ -81,20 +55,20 @@ func (_m *LiveStateCache) Invalidate() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// IsNamespaced provides a mock function with given fields: server, gvk
|
||||
func (_m *LiveStateCache) IsNamespaced(server string, gvk schema.GroupVersionKind) (bool, error) {
|
||||
ret := _m.Called(server, gvk)
|
||||
// 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.GroupVersionKind) bool); ok {
|
||||
r0 = rf(server, gvk)
|
||||
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.GroupVersionKind) error); ok {
|
||||
r1 = rf(server, gvk)
|
||||
if rf, ok := ret.Get(1).(func(string, *unstructured.Unstructured) error); ok {
|
||||
r1 = rf(server, obj)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
@@ -102,6 +76,20 @@ func (_m *LiveStateCache) IsNamespaced(server string, gvk schema.GroupVersionKin
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// IterateHierarchy provides a mock function with given fields: server, key, action
|
||||
func (_m *LiveStateCache) IterateHierarchy(server string, key kube.ResourceKey, action func(v1alpha1.ResourceNode)) error {
|
||||
ret := _m.Called(server, key, action)
|
||||
|
||||
var r0 error
|
||||
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)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Run provides a mock function with given fields: ctx
|
||||
func (_m *LiveStateCache) Run(ctx context.Context) {
|
||||
_m.Called(ctx)
|
||||
|
||||
78
controller/cache/node.go
vendored
78
controller/cache/node.go
vendored
@@ -6,7 +6,7 @@ import (
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
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"
|
||||
@@ -18,7 +18,16 @@ type node struct {
|
||||
ownerRefs []metav1.OwnerReference
|
||||
info []appv1.InfoItem
|
||||
appName string
|
||||
resource *unstructured.Unstructured
|
||||
// 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 {
|
||||
@@ -26,8 +35,8 @@ func (n *node) resourceKey() kube.ResourceKey {
|
||||
}
|
||||
|
||||
func (n *node) isParentOf(child *node) bool {
|
||||
ownerGvk := n.ref.GroupVersionKind()
|
||||
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
|
||||
}
|
||||
@@ -45,13 +54,24 @@ func ownerRefGV(ownerRef metav1.OwnerReference) schema.GroupVersion {
|
||||
}
|
||||
|
||||
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.getApp(ns)
|
||||
app := parent.getAppRecursive(ns, visited)
|
||||
if app != "" {
|
||||
return app
|
||||
}
|
||||
@@ -71,30 +91,44 @@ func newResourceKeySet(set map[kube.ResourceKey]bool, keys ...kube.ResourceKey)
|
||||
return newSet
|
||||
}
|
||||
|
||||
func (n *node) childResourceNodes(ns map[kube.ResourceKey]*node, parents map[kube.ResourceKey]bool) appv1.ResourceNode {
|
||||
children := make([]appv1.ResourceNode, 0)
|
||||
for childKey := range ns {
|
||||
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 {
|
||||
children = append(children, ns[childKey].childResourceNodes(ns, newResourceKeySet(parents, n.resourceKey())))
|
||||
action(child.asResourceNode())
|
||||
child.iterateChildren(ns, newResourceKeySet(parents, n.resourceKey()), action)
|
||||
}
|
||||
}
|
||||
}
|
||||
gv, err := schema.ParseGroupVersion(n.ref.APIVersion)
|
||||
if err != nil {
|
||||
gv = schema.GroupVersion{}
|
||||
}
|
||||
return appv1.ResourceNode{
|
||||
Name: n.ref.Name,
|
||||
Group: gv.Group,
|
||||
Version: gv.Version,
|
||||
Kind: n.ref.Kind,
|
||||
Namespace: n.ref.Namespace,
|
||||
Info: n.info,
|
||||
Children: children,
|
||||
ResourceVersion: n.resourceVersion,
|
||||
}
|
||||
}
|
||||
|
||||
29
controller/cache/node_test.go
vendored
Normal file
29
controller/cache/node_test.go
vendored
Normal 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))
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package metrics
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
type MetricsServer struct {
|
||||
*http.Server
|
||||
syncCounter *prometheus.CounterVec
|
||||
k8sRequestCounter *prometheus.CounterVec
|
||||
reconcileHistogram *prometheus.HistogramVec
|
||||
}
|
||||
|
||||
@@ -60,6 +62,8 @@ var (
|
||||
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(
|
||||
@@ -70,6 +74,14 @@ func NewMetricsServer(addr string, appLister applister.ApplicationLister) *Metri
|
||||
append(descAppDefaultLabels, "phase"),
|
||||
)
|
||||
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, "response_code"),
|
||||
)
|
||||
appRegistry.MustRegister(k8sRequestCounter)
|
||||
|
||||
reconcileHistogram := prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
@@ -89,6 +101,7 @@ func NewMetricsServer(addr string, appLister applister.ApplicationLister) *Metri
|
||||
Handler: mux,
|
||||
},
|
||||
syncCounter: syncCounter,
|
||||
k8sRequestCounter: k8sRequestCounter,
|
||||
reconcileHistogram: reconcileHistogram,
|
||||
}
|
||||
}
|
||||
@@ -101,6 +114,11 @@ func (m *MetricsServer) IncSync(app *argoappv1.Application, state *argoappv1.Ope
|
||||
m.syncCounter.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject(), string(state.Phase)).Inc()
|
||||
}
|
||||
|
||||
// IncKubernetesRequest increments the kubernetes requests counter for an application
|
||||
func (m *MetricsServer) IncKubernetesRequest(app *argoappv1.Application, statusCode int) {
|
||||
m.k8sRequestCounter.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject(), strconv.Itoa(statusCode)).Inc()
|
||||
}
|
||||
|
||||
// IncReconcile increments the reconcile counter for an application
|
||||
func (m *MetricsServer) IncReconcile(app *argoappv1.Application, duration time.Duration) {
|
||||
m.reconcileHistogram.WithLabelValues(app.Namespace, app.Name, app.Spec.GetProject()).Observe(duration.Seconds())
|
||||
|
||||
@@ -220,7 +220,7 @@ func TestReconcileMetrics(t *testing.T) {
|
||||
metricsServ := NewMetricsServer("localhost:8082", appLister)
|
||||
|
||||
fakeApp := newFakeApp(fakeApp)
|
||||
metricsServ.IncReconcile(fakeApp, time.Duration(5*time.Second))
|
||||
metricsServ.IncReconcile(fakeApp, 5*time.Second)
|
||||
|
||||
req, err := http.NewRequest("GET", "/metrics", nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
37
controller/metrics/transportwrapper.go
Normal file
37
controller/metrics/transportwrapper.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"k8s.io/client-go/rest"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
|
||||
type metricsRoundTripper struct {
|
||||
roundTripper http.RoundTripper
|
||||
app *v1alpha1.Application
|
||||
metricsServer *MetricsServer
|
||||
}
|
||||
|
||||
func (mrt *metricsRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
resp, err := mrt.roundTripper.RoundTrip(r)
|
||||
statusCode := 0
|
||||
if resp != nil {
|
||||
statusCode = resp.StatusCode
|
||||
}
|
||||
mrt.metricsServer.IncKubernetesRequest(mrt.app, statusCode)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// AddMetricsTransportWrapper adds a transport wrapper which increments 'argocd_app_k8s_request_total' counter on each kubernetes request
|
||||
func AddMetricsTransportWrapper(server *MetricsServer, app *v1alpha1.Application, config *rest.Config) *rest.Config {
|
||||
wrap := config.WrapTransport
|
||||
config.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
|
||||
if wrap != nil {
|
||||
rt = wrap(rt)
|
||||
}
|
||||
return &metricsRoundTripper{roundTripper: rt, metricsServer: server, app: app}
|
||||
}
|
||||
return config
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
statecache "github.com/argoproj/argo-cd/controller/cache"
|
||||
"github.com/argoproj/argo-cd/controller/metrics"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
@@ -49,6 +50,10 @@ func GetLiveObjs(res []managedResource) []*unstructured.Unstructured {
|
||||
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, revision string, source v1alpha1.ApplicationSource, noCache bool) (*comparisonResult, error)
|
||||
@@ -56,7 +61,7 @@ type AppStateManager interface {
|
||||
}
|
||||
|
||||
type comparisonResult struct {
|
||||
observedAt metav1.Time
|
||||
reconciledAt metav1.Time
|
||||
syncStatus *v1alpha1.SyncStatus
|
||||
healthStatus *v1alpha1.HealthStatus
|
||||
resources []v1alpha1.ResourceStatus
|
||||
@@ -69,6 +74,7 @@ type comparisonResult struct {
|
||||
|
||||
// appStateManager allows to compare applications to git
|
||||
type appStateManager struct {
|
||||
metricsServer *metrics.MetricsServer
|
||||
db db.ArgoDB
|
||||
settings *settings.ArgoCDSettings
|
||||
appclientset appclientset.Interface
|
||||
@@ -84,7 +90,10 @@ func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
repo := m.getRepo(source.RepoURL)
|
||||
repo, err := m.db.GetRepository(context.Background(), source.RepoURL)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
conn, repoClient, err := m.repoClientset.NewRepoServerClient()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
@@ -131,6 +140,43 @@ func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, source v1alpha1
|
||||
return targetObjs, hooks, manifestInfo, nil
|
||||
}
|
||||
|
||||
func DeduplicateTargetObjects(
|
||||
server string,
|
||||
namespace string,
|
||||
objs []*unstructured.Unstructured,
|
||||
infoProvider ResourceInfoProvider,
|
||||
) ([]*unstructured.Unstructured, []v1alpha1.ApplicationCondition, error) {
|
||||
|
||||
targetByKey := make(map[kubeutil.ResourceKey][]*unstructured.Unstructured)
|
||||
for i := range objs {
|
||||
obj := objs[i]
|
||||
isNamespaced, err := infoProvider.IsNamespaced(server, obj)
|
||||
if err != nil {
|
||||
return objs, nil, err
|
||||
}
|
||||
if !isNamespaced {
|
||||
obj.SetNamespace("")
|
||||
} else if obj.GetNamespace() == "" {
|
||||
obj.SetNamespace(namespace)
|
||||
}
|
||||
key := kubeutil.GetResourceKey(obj)
|
||||
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 {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{
|
||||
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])
|
||||
}
|
||||
|
||||
return result, conditions, nil
|
||||
}
|
||||
|
||||
// CompareAppState compares application git state to the live app state, using the specified
|
||||
// revision and supplied source. If revision or overrides are empty, then compares against
|
||||
// revision and overrides in the app spec.
|
||||
@@ -151,6 +197,12 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
failedToLoadObjs = true
|
||||
}
|
||||
targetObjs, dedupConditions, err := DeduplicateTargetObjects(app.Spec.Destination.Server, app.Spec.Destination.Namespace, targetObjs, m.liveStateCache)
|
||||
if err != nil {
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
}
|
||||
conditions = append(conditions, dedupConditions...)
|
||||
|
||||
logCtx.Debugf("Generated config manifests")
|
||||
liveObjByKey, err := m.liveStateCache.GetManagedLiveObjs(app, targetObjs)
|
||||
if err != nil {
|
||||
@@ -175,7 +227,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
for i, obj := range targetObjs {
|
||||
gvk := obj.GroupVersionKind()
|
||||
ns := util.FirstNonEmpty(obj.GetNamespace(), app.Spec.Destination.Namespace)
|
||||
if namespaced, err := m.liveStateCache.IsNamespaced(app.Spec.Destination.Server, obj.GroupVersionKind()); err == nil && !namespaced {
|
||||
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())
|
||||
@@ -215,7 +267,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
gvk := obj.GroupVersionKind()
|
||||
|
||||
resState := v1alpha1.ResourceStatus{
|
||||
Namespace: util.FirstNonEmpty(obj.GetNamespace(), app.Spec.Destination.Namespace),
|
||||
Namespace: obj.GetNamespace(),
|
||||
Name: obj.GetName(),
|
||||
Kind: gvk.Kind,
|
||||
Version: gvk.Version,
|
||||
@@ -264,13 +316,16 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
syncStatus.Revision = manifestInfo.Revision
|
||||
}
|
||||
|
||||
healthStatus, err := health.SetApplicationHealth(resourceSummaries, GetLiveObjs(managedResources), m.settings.ResourceOverrides)
|
||||
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()})
|
||||
}
|
||||
|
||||
compRes := comparisonResult{
|
||||
observedAt: observedAt,
|
||||
reconciledAt: observedAt,
|
||||
syncStatus: &syncStatus,
|
||||
healthStatus: healthStatus,
|
||||
resources: resourceSummaries,
|
||||
@@ -278,20 +333,13 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision st
|
||||
conditions: conditions,
|
||||
hooks: hooks,
|
||||
diffNormalizer: diffNormalizer,
|
||||
appSourceType: v1alpha1.ApplicationSourceType(manifestInfo.SourceType),
|
||||
}
|
||||
if manifestInfo != nil {
|
||||
compRes.appSourceType = v1alpha1.ApplicationSourceType(manifestInfo.SourceType)
|
||||
}
|
||||
return &compRes, nil
|
||||
}
|
||||
|
||||
func (m *appStateManager) getRepo(repoURL string) *v1alpha1.Repository {
|
||||
repo, err := m.db.GetRepository(context.Background(), repoURL)
|
||||
if err != nil {
|
||||
// If we couldn't retrieve from the repo service, assume public repositories
|
||||
repo = &v1alpha1.Repository{Repo: repoURL}
|
||||
}
|
||||
return repo
|
||||
}
|
||||
|
||||
func (m *appStateManager) persistRevisionHistory(app *v1alpha1.Application, revision string, source v1alpha1.ApplicationSource) error {
|
||||
var nextID int64
|
||||
if len(app.Status.History) > 0 {
|
||||
@@ -330,6 +378,7 @@ func NewAppStateManager(
|
||||
settings *settings.ArgoCDSettings,
|
||||
liveStateCache statecache.LiveStateCache,
|
||||
projInformer cache.SharedIndexInformer,
|
||||
metricsServer *metrics.MetricsServer,
|
||||
) AppStateManager {
|
||||
return &appStateManager{
|
||||
liveStateCache: liveStateCache,
|
||||
@@ -340,5 +389,6 @@ func NewAppStateManager(
|
||||
namespace: namespace,
|
||||
settings: settings,
|
||||
projInformer: projInformer,
|
||||
metricsServer: metricsServer,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
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"
|
||||
@@ -141,3 +144,120 @@ func TestCompareAppStateExtraHook(t *testing.T) {
|
||||
assert.Equal(t, 1, len(compRes.managedResources))
|
||||
assert.Equal(t, 0, len(compRes.conditions))
|
||||
}
|
||||
|
||||
func toJSON(t *testing.T, obj *unstructured.Unstructured) string {
|
||||
data, err := json.Marshal(obj)
|
||||
assert.NoError(t, err)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func TestCompareAppStateDuplicatedNamespacedResources(t *testing.T) {
|
||||
obj1 := test.NewPod()
|
||||
obj1.SetNamespace(test.FakeDestNamespace)
|
||||
obj2 := test.NewPod()
|
||||
obj3 := test.NewPod()
|
||||
obj3.SetNamespace("kube-system")
|
||||
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
Manifests: []string{toJSON(t, obj1), toJSON(t, obj2), toJSON(t, obj3)},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
||||
kube.GetResourceKey(obj1): obj1,
|
||||
kube.GetResourceKey(obj3): obj3,
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.Contains(t, compRes.conditions, argoappv1.ApplicationCondition{
|
||||
Message: "Resource /Pod/fake-dest-ns/my-pod appeared 2 times among application resources.",
|
||||
Type: argoappv1.ApplicationConditionRepeatedResourceWarning,
|
||||
})
|
||||
assert.Equal(t, 2, len(compRes.resources))
|
||||
}
|
||||
|
||||
var defaultProj = argoappv1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
},
|
||||
Spec: argoappv1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []argoappv1.ApplicationDestination{
|
||||
{
|
||||
Server: "*",
|
||||
Namespace: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestSetHealth(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
deployment := kube.MustToUnstructured(&v1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "apps/v1beta1",
|
||||
Kind: "Deployment",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "demo",
|
||||
Namespace: "default",
|
||||
},
|
||||
})
|
||||
ctrl := newFakeController(&fakeData{
|
||||
apps: []runtime.Object{app, &defaultProj},
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
||||
kube.GetResourceKey(deployment): deployment,
|
||||
},
|
||||
})
|
||||
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, compRes.healthStatus.Status, argoappv1.HealthStatusHealthy)
|
||||
}
|
||||
|
||||
func TestSetHealthSelfReferencedApp(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
unstructuredApp := kube.MustToUnstructured(app)
|
||||
deployment := kube.MustToUnstructured(&v1.Deployment{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "apps/v1beta1",
|
||||
Kind: "Deployment",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "demo",
|
||||
Namespace: "default",
|
||||
},
|
||||
})
|
||||
ctrl := newFakeController(&fakeData{
|
||||
apps: []runtime.Object{app, &defaultProj},
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
||||
kube.GetResourceKey(deployment): deployment,
|
||||
kube.GetResourceKey(unstructuredApp): unstructuredApp,
|
||||
},
|
||||
})
|
||||
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", app.Spec.Source, false)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, compRes.healthStatus.Status, argoappv1.HealthStatusHealthy)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/rest"
|
||||
|
||||
"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"
|
||||
@@ -116,7 +117,7 @@ func (m *appStateManager) SyncAppState(app *appv1.Application, state *appv1.Oper
|
||||
return
|
||||
}
|
||||
|
||||
restConfig := clst.RESTConfig()
|
||||
restConfig := metrics.AddMetricsTransportWrapper(m.metricsServer, app, clst.RESTConfig())
|
||||
dynamicIf, err := dynamic.NewForConfig(restConfig)
|
||||
if err != nil {
|
||||
state.Phase = appv1.OperationError
|
||||
@@ -329,10 +330,7 @@ func (sc *syncContext) generateSyncTasks() ([]syncTask, bool) {
|
||||
// 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 {
|
||||
if len(sc.syncRes.Resources) > 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return len(sc.syncRes.Resources) > 0
|
||||
}
|
||||
|
||||
// startedSyncPhase detects if we have already started the Sync stage of a sync operation.
|
||||
@@ -464,8 +462,7 @@ func (sc *syncContext) doApplySync(syncTasks []syncTask, dryRun, force, update b
|
||||
wg.Add(1)
|
||||
go func(t syncTask) {
|
||||
defer wg.Done()
|
||||
var resDetails appv1.ResourceResult
|
||||
resDetails = sc.pruneObject(t.liveObj, sc.syncOp.Prune, dryRun)
|
||||
resDetails := sc.pruneObject(t.liveObj, sc.syncOp.Prune, dryRun)
|
||||
if !resDetails.Status.Successful() {
|
||||
syncSuccessful = false
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
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"
|
||||
|
||||
@@ -18,37 +18,42 @@ Install:
|
||||
* [kustomize](https://github.com/kubernetes-sigs/kustomize/releases)
|
||||
* [go-swagger](https://github.com/go-swagger/go-swagger/blob/master/docs/install.md)
|
||||
* [jq](https://stedolan.github.io/jq/)
|
||||
* [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/).
|
||||
* [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
|
||||
* [kubectx](https://kubectx.dev)
|
||||
* [minikube](https://kubernetes.io/docs/setup/minikube/) or Docker for Desktop
|
||||
|
||||
```
|
||||
Brew users can quickly install the lot:
|
||||
|
||||
```bash
|
||||
brew tap go-swagger/go-swagger
|
||||
brew install go dep protobuf kubectl ksonnet/tap/ks kubernetes-helm jq go-swagger
|
||||
brew install go dep protobuf kubectl kubectx ksonnet/tap/ks kubernetes-helm jq go-swagger
|
||||
```
|
||||
|
||||
!!! note "Kustomize"
|
||||
Since Argo CD supports Kustomize v1.0 and v2.0, you will need to install both versions in order for the unit tests to run. The Kustomize 1 unit test expects to find a `kustomize1` binary in the path. You can use this [link](https://github.com/argoproj/argo-cd/blob/master/Dockerfile#L66-L69) to find the Kustomize 1 currently used by Argo CD and modify the curl command to download the correct OS.
|
||||
|
||||
Set up environment variables (e.g. is `~/.bashrc`):
|
||||
|
||||
```
|
||||
```bash
|
||||
export GOPATH=~/go
|
||||
export PATH=$PATH:$GOPATH/bin
|
||||
```
|
||||
|
||||
Install go dependencies:
|
||||
|
||||
```
|
||||
```bash
|
||||
go get -u github.com/golang/protobuf/protoc-gen-go
|
||||
go get -u github.com/go-swagger/go-swagger/cmd/swagger
|
||||
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
|
||||
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
|
||||
go get -u gopkg.in/alecthomas/gometalinter.v2
|
||||
go get -u github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||
go get -u github.com/mattn/goreman
|
||||
|
||||
gometalinter.v2 --install
|
||||
go get -u gotest.tools/gotestsum
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
```bash
|
||||
go get -u github.com/argoproj/argo-cd
|
||||
dep ensure
|
||||
make
|
||||
@@ -65,23 +70,19 @@ The make command can take a while, and we recommend building the specific compon
|
||||
|
||||
To run unit tests:
|
||||
|
||||
```
|
||||
```bash
|
||||
make test
|
||||
```
|
||||
|
||||
To run e2e tests:
|
||||
|
||||
```
|
||||
make test-e2e
|
||||
```
|
||||
Check out the following [documentation](https://github.com/argoproj/argo-cd/blob/master/docs/developer-guide/test-e2e.md) for instructions on running the e2e tests.
|
||||
|
||||
## Running Locally
|
||||
|
||||
It is much easier to run and debug if you run ArgoCD on your local machine than in the Kubernetes cluster.
|
||||
|
||||
You should scale the deployemnts to zero:
|
||||
You should scale the deployments to zero:
|
||||
|
||||
```
|
||||
```bash
|
||||
kubectl -n argocd scale deployment.extensions/argocd-application-controller --replicas 0
|
||||
kubectl -n argocd scale deployment.extensions/argocd-dex-server --replicas 0
|
||||
kubectl -n argocd scale deployment.extensions/argocd-repo-server --replicas 0
|
||||
@@ -102,14 +103,14 @@ Note: you'll need to use the https://localhost:6443 cluster now.
|
||||
|
||||
Then start the services:
|
||||
|
||||
```
|
||||
```bash
|
||||
cd ~/go/src/github.com/argoproj/argo-cd
|
||||
goreman start
|
||||
make start
|
||||
```
|
||||
|
||||
You can now execute `argocd` command against your locally running ArgoCD by appending `--server localhost:8080 --plaintext --insecure`, e.g.:
|
||||
|
||||
```
|
||||
```bash
|
||||
argocd app set guestbook --path guestbook --repo https://github.com/argoproj/argocd-example-apps.git --dest-server https://localhost:6443 --dest-namespace default --server localhost:8080 --plaintext --insecure
|
||||
```
|
||||
|
||||
@@ -123,19 +124,19 @@ You may need to run containers locally, so here's how:
|
||||
|
||||
Create login to Docker Hub, then login.
|
||||
|
||||
```
|
||||
```bash
|
||||
docker login
|
||||
```
|
||||
|
||||
Add your username as the environment variable, e.g. to your `~/.bash_profile`:
|
||||
|
||||
```
|
||||
```bash
|
||||
export IMAGE_NAMESPACE=alexcollinsintuit
|
||||
```
|
||||
|
||||
If you have not built the UI image (see [the UI README](https://github.com/argoproj/argo-cd-ui/blob/master/README.md)), then do the following:
|
||||
|
||||
```
|
||||
```bash
|
||||
docker pull argoproj/argocd-ui:latest
|
||||
docker tag argoproj/argocd-ui:latest $IMAGE_NAMESPACE/argocd-ui:latest
|
||||
docker push $IMAGE_NAMESPACE/argocd-ui:latest
|
||||
@@ -143,25 +144,25 @@ docker push $IMAGE_NAMESPACE/argocd-ui:latest
|
||||
|
||||
Build the images:
|
||||
|
||||
```
|
||||
```bash
|
||||
DOCKER_PUSH=true make image
|
||||
```
|
||||
|
||||
Update the manifests:
|
||||
|
||||
```
|
||||
```bash
|
||||
make manifests
|
||||
```
|
||||
|
||||
Install the manifests:
|
||||
|
||||
```
|
||||
```bash
|
||||
kubectl -n argocd apply --force -f manifests/install.yaml
|
||||
```
|
||||
|
||||
Scale your deployments up:
|
||||
|
||||
```
|
||||
```bash
|
||||
kubectl -n argocd scale deployment.extensions/argocd-application-controller --replicas 1
|
||||
kubectl -n argocd scale deployment.extensions/argocd-dex-server --replicas 1
|
||||
kubectl -n argocd scale deployment.extensions/argocd-repo-server --replicas 1
|
||||
@@ -169,13 +170,13 @@ kubectl -n argocd scale deployment.extensions/argocd-server --replicas 1
|
||||
kubectl -n argocd scale deployment.extensions/argocd-redis --replicas 1
|
||||
```
|
||||
|
||||
Now you can set-up the port-forwarding (see [README](README.md)) and open the UI or CLI.
|
||||
Now you can set-up the port-forwarding and open the UI or CLI.
|
||||
|
||||
## Pre-commit Checks
|
||||
|
||||
Before you commit, make sure you've formatted and linted your code, or your PR will fail CI:
|
||||
|
||||
```
|
||||
```bash
|
||||
STAGED_GO_FILES=$(git diff --cached --name-only | grep ".go$")
|
||||
|
||||
gofmt -w $STAGED_GO_FILES
|
||||
@@ -1,38 +0,0 @@
|
||||
# Argo CD Documentation
|
||||
|
||||
## [Getting Started](getting_started.md)
|
||||
|
||||
## Concepts
|
||||
* [Architecture](architecture.md)
|
||||
* [Tracking Strategies](tracking_strategies.md)
|
||||
|
||||
## Quick Reference
|
||||
| Name | Kind | Description |
|
||||
|------|------|-------------|
|
||||
| [`argocd-cm.yaml`](argocd-cm.yaml) | ConfigMap | General Argo CD configuration |
|
||||
| [`argocd-secret.yaml`](argocd-secret.yaml) | Secret | Password, Certificates, Signing Key |
|
||||
| [`argocd-rbac-cm.yaml`](argocd-rbac-cm.yaml) | ConfigMap | RBAC Configuration |
|
||||
| [`application.yaml`](application.yaml) | Application | Example application spec |
|
||||
| [`project.yaml`](argocd-rbac-cm.yaml) | AppProject | Example project spec |
|
||||
|
||||
## Features
|
||||
* [Application Sources](application_sources.md)
|
||||
* [Application Parameters](parameters.md)
|
||||
* [Projects](projects.md)
|
||||
* [Automated Sync](auto_sync.md)
|
||||
* [Resource Health](health.md)
|
||||
* [Resource Hooks](resource_hooks.md)
|
||||
* [Resource Diffing](diffing.md)
|
||||
* [Single Sign On](sso.md)
|
||||
* [Webhooks](webhook.md)
|
||||
* [RBAC](rbac.md)
|
||||
* [Declarative Setup](declarative-setup.md)
|
||||
* [Prometheus Metrics](metrics.md)
|
||||
* [Custom Tooling](custom_tools.md)
|
||||
|
||||
## Other
|
||||
* [Security](security.md)
|
||||
* [Best Practices](best_practices.md)
|
||||
* [Configuring Ingress](ingress.md)
|
||||
* [Integration with CI Pipelines](ci_automation.md)
|
||||
* [F.A.Q.](faq.md)
|
||||
6
docs/SUPPORT.md
Normal file
6
docs/SUPPORT.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Support
|
||||
|
||||
1. Make sure you've read [understanding the basics](understand_the_basics.md) the [getting started guide](getting_started.md).
|
||||
2. Looked for an answer [the frequently asked questions](faq.md).
|
||||
3. Ask a question in [the Argo CD Slack channel ⧉](https://argoproj.slack.com/messages/CASHNF6MS).
|
||||
4. [Read issues, report a bug, or request a feature ⧉](https://github.com/argoproj/argo-cd/issues)
|
||||
@@ -1,61 +0,0 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: guestbook
|
||||
spec:
|
||||
# The project the application belongs to.
|
||||
project: default
|
||||
|
||||
# Source of the application manifests
|
||||
source:
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps.git
|
||||
targetRevision: HEAD
|
||||
path: guestbook
|
||||
|
||||
# helm specific config
|
||||
helm:
|
||||
valueFiles:
|
||||
- values-prod.yaml
|
||||
|
||||
# kustomize specific config
|
||||
kustomize:
|
||||
namePrefix: prod-
|
||||
|
||||
# directory
|
||||
directory:
|
||||
recurse: true
|
||||
jsonnet:
|
||||
# A list of Jsonnet External Variables
|
||||
extVars:
|
||||
- name: foo
|
||||
value: bar
|
||||
# You can use "code to determine if the value is either string (false, the default) or Jsonnet code (if code is true).
|
||||
- code: true
|
||||
name: baz
|
||||
value: "true"
|
||||
# A list of Jsonnet Top-level Arguments
|
||||
tlas:
|
||||
- code: false
|
||||
name: foo
|
||||
value: bar
|
||||
|
||||
# plugin specific config
|
||||
plugin:
|
||||
- name: mypluginname
|
||||
|
||||
# Destination cluster and namespace to deploy the application
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: guestbook
|
||||
|
||||
# Sync policy
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
|
||||
# Ignore differences at the specified json pointers
|
||||
ignoreDifferences:
|
||||
- group: apps
|
||||
kind: Deployment
|
||||
jsonPointers:
|
||||
- /spec/replicas
|
||||
|
Before Width: | Height: | Size: 3.5 MiB After Width: | Height: | Size: 3.5 MiB |
|
Before Width: | Height: | Size: 119 KiB After Width: | Height: | Size: 119 KiB |
BIN
docs/assets/dashboard.jpg
Normal file
BIN
docs/assets/dashboard.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 321 KiB |
BIN
docs/assets/logo.png
Normal file
BIN
docs/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
@@ -1,58 +0,0 @@
|
||||
# Automation from CI Pipelines
|
||||
|
||||
Argo CD follows the GitOps model of deployment, where desired configuration changes are first
|
||||
pushed to git, and the cluster state then syncs to the desired state in git. This is a departure
|
||||
from imperative pipelines which do not traditionally use git repositories to hold application
|
||||
config.
|
||||
|
||||
To push new container images into to a cluster managed by Argo CD, the following workflow (or
|
||||
variations), might be used:
|
||||
|
||||
|
||||
1. Build and publish a new container image
|
||||
|
||||
```
|
||||
docker build -t mycompany/guestbook:v2.0 .
|
||||
docker push mycompany/guestbook:v2.0
|
||||
```
|
||||
|
||||
2. Update the local manifests using your preferred templating tool, and push the changes to git.
|
||||
|
||||
NOTE: the use of a different git repository to hold your kubernetes manifests (separate from
|
||||
your application source code), is highly recommended. See [best practices](best_practices.md)
|
||||
for further rationale.
|
||||
|
||||
```
|
||||
git clone https://github.com/mycompany/guestbook-config.git
|
||||
cd guestbook-config
|
||||
|
||||
# kustomize
|
||||
kustomize edit set imagetag mycompany/guestbook:v2.0
|
||||
|
||||
# ksonnet
|
||||
ks param set guestbook image mycompany/guestbook:v2.0
|
||||
|
||||
# plain yaml
|
||||
kubectl patch --local -f config-deployment.yaml -p '{"spec":{"template":{"spec":{"containers":[{"name":"guestbook","image":"mycompany/guestbook:v2.0"}]}}}}' -o yaml
|
||||
|
||||
git add . -m "Update guestbook to v2.0"
|
||||
git push
|
||||
```
|
||||
|
||||
3. Synchronize the app (Optional)
|
||||
|
||||
For convenience, the argocd CLI can be downloaded directly from the API server. This is
|
||||
useful so that the CLI used in the CI pipeline is always kept in-sync and uses argocd binary
|
||||
that is always compatible with the Argo CD API server.
|
||||
|
||||
```
|
||||
export ARGOCD_SERVER=argocd.mycompany.com
|
||||
export ARGOCD_AUTH_TOKEN=<JWT token generated from project>
|
||||
curl -sSL -o /usr/local/bin/argocd https://${ARGOCD_SERVER}/download/argocd-linux-amd64
|
||||
argocd app sync guestbook
|
||||
argocd app wait guestbook
|
||||
```
|
||||
|
||||
If [automated synchronization](auto_sync.md) is configured for the application, this step is
|
||||
unnecessary. The controller will automatically detect the new config (fast tracked using a
|
||||
[webhook](webhook.md), or polled every 3 minutes), and automatically sync the new manifests.
|
||||
16
docs/core_concepts.md
Normal file
16
docs/core_concepts.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Core Concepts
|
||||
|
||||
Let's assume you're familiar with core Git, Docker, Kubernetes, Continuous Delivery, and GitOps concepts.
|
||||
|
||||
* **Application** A group of Kubernetes resources as defined by a manifest. This is a Custom Resource Definition (CRD).
|
||||
* **Application source type** Which **Tool** is used to build the application.
|
||||
* **Target state** The desired state of an application, as represented by files in a Git repository.
|
||||
* **Live state** The live state of that application. What pods etc are deployed.
|
||||
* **Sync status** Whether or not the live state matches the target state. Is the deployed application the same as Git says it should be?
|
||||
* **Sync** The process of making an application move to its target state. E.g. by applying changes to a Kubernetes cluster.
|
||||
* **Sync operation status** Whether or not a sync succeeded.
|
||||
* **Refresh** Compare the latest code in Git with the live state. Figure out what is different.
|
||||
* **Health** The health the application, is it running correctly? Can it serve requests?
|
||||
* **Tool** A tool to create manifests from a directory of files. E.g. Kustomize or Ksonnet. See **Application Source Type**.
|
||||
* **Configuration management tool** See **Tool**.
|
||||
* **Configuration management plugin** A custom tool.
|
||||
3
docs/developer-guide/api-docs.md
Normal file
3
docs/developer-guide/api-docs.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# API Docs
|
||||
|
||||
You can find Swagger docs but setting the path `/swagger-ui` to your Argo CD UI's. E.g. [http://localhost:8080/swagger-ui](http://localhost:8080/swagger-ui).
|
||||
10
docs/developer-guide/index.md
Normal file
10
docs/developer-guide/index.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Overview
|
||||
|
||||
!!! warning "You probably don't want to be reading this section of the docs."
|
||||
This part of the manual is aimed at people wanting to develop third-party applications that interact with Argo CD, e.g.
|
||||
|
||||
* An chat bot
|
||||
* An Slack integration
|
||||
|
||||
!!! note
|
||||
Please make sure you've completed the [getting started guide](../getting_started.md).
|
||||
@@ -1,4 +1,4 @@
|
||||
# Argo CD Release Instructions
|
||||
# Releasing
|
||||
|
||||
1. Tag, build, and push argo-cd-ui
|
||||
```bash
|
||||
30
docs/developer-guide/site.md
Normal file
30
docs/developer-guide/site.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Site
|
||||
|
||||
## Developing And Testing
|
||||
|
||||
The web site is build using `mkdocs` and `mkdocs-material`.
|
||||
|
||||
To test:
|
||||
|
||||
```bash
|
||||
mkdocs serve
|
||||
```
|
||||
|
||||
Check for broken external links:
|
||||
|
||||
```bash
|
||||
find docs -name '*.md' -exec grep -l http {} + | xargs awesome_bot -t 3 --allow-dupe --allow-redirect -w argocd.example.com:443,argocd.example.com,kubernetes.default.svc:443,kubernetes.default.svc,mycluster.com,https://github.com/argoproj/my-private-repository,192.168.0.20,storage.googleapis.com,localhost:8080,localhost:6443,your-kubernetes-cluster-addr,10.97.164.88 --skip-save-results --
|
||||
```
|
||||
|
||||
## Deploying
|
||||
|
||||
```bash
|
||||
mkdocs gh-deploy
|
||||
```
|
||||
|
||||
## Analytics
|
||||
|
||||
!!! tip
|
||||
Don't forget to disable your ad-blocker when testing.
|
||||
|
||||
We collect [Google Analytics](https://analytics.google.com/analytics/web/#/report-home/a105170809w198079555p192782995).
|
||||
19
docs/developer-guide/test-e2e.md
Normal file
19
docs/developer-guide/test-e2e.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# E2E Tests
|
||||
|
||||
The directory contains E2E tests and test applications. The test assume that Argo CD services are installed into `argocd-e2e` namespace or cluster in current context. One throw-away
|
||||
namespace `argocd-e2e***` is created prior to tests execute. The throw-away namespace is used as a target namespace for test applications.
|
||||
|
||||
The `test/e2e/testdata` directory contains various Argo CD applications. Before test execution directory is copies into `/tmp/argocd-e2e***` temp directory and used in tests as a
|
||||
Git repository via file url: `file:///tmp/argocd-e2e***`.
|
||||
|
||||
## Running Tests Locally
|
||||
|
||||
1. Start the e2e version `make start-e2e`
|
||||
1. Run the tests: `make test-e2e`
|
||||
|
||||
You can observe the tests by using the UI [http://localhost:8080/applications](http://localhost:8080/applications).
|
||||
|
||||
## CI Set-up
|
||||
|
||||
The tests are executed by Argo Workflow defined at `.argo-ci/ci.yaml`. CI job The builds an Argo CD image, deploy argo cd components into throw-away kubernetes cluster provisioned
|
||||
using k3s and run e2e tests against it.
|
||||
34
docs/faq.md
34
docs/faq.md
@@ -2,7 +2,7 @@
|
||||
|
||||
## Why is my application still `OutOfSync` immediately after a successful Sync?
|
||||
|
||||
See [Diffing](diffing.md) documentation for reasons resources can be OutOfSync, and ways to configure
|
||||
See [Diffing](user-guide/diffing.md) documentation for reasons resources can be OutOfSync, and ways to configure
|
||||
Argo CD to ignore fields when differences are expected.
|
||||
|
||||
|
||||
@@ -19,10 +19,40 @@ to return `Progressing` state instead of `Healthy`.
|
||||
[kubernetes/kubernetes#68573](https://github.com/kubernetes/kubernetes/issues/68573) the `status.updatedReplicas` is not populated. So unless you run Kubernetes version which
|
||||
include the fix [kubernetes/kubernetes#67570](https://github.com/kubernetes/kubernetes/pull/67570) `StatefulSet` might stay in `Progressing` state.
|
||||
|
||||
As workaround Argo CD allows providing [health check](health.md) customization which overrides default behavior.
|
||||
As workaround Argo CD allows providing [health check](operator-manual/health.md) customization which overrides default behavior.
|
||||
|
||||
## I forgot the admin password, how do I reset it?
|
||||
|
||||
Edit the `argocd-secret` secret and update the `admin.password` field with a new bcrypt hash. You
|
||||
can use a site like https://www.browserling.com/tools/bcrypt to generate a new hash. Another option
|
||||
is to delete both the `admin.password` and `admin.passwordMtime` keys and restart argocd-server.
|
||||
|
||||
## Argo CD cannot deploy Helm Chart based applications without internet access, how can I solve it?
|
||||
|
||||
Argo CD might fail to generate Helm chart manifests if the chart has dependencies located in external repositories. To solve the problem you need to make sure that `requirements.yaml`
|
||||
uses only internally available Helm repositories. Even if the chart uses only dependencies from internal repos Helm might decide to refresh `stable` repo. As workaround override
|
||||
`stable` repo URL in `argocd-cm` config map:
|
||||
|
||||
```yaml
|
||||
data:
|
||||
helm.repositories: |
|
||||
- url: http://<internal-helm-repo-host>:8080
|
||||
name: stable
|
||||
```
|
||||
|
||||
## I've configured [cluster secret](./operator-manual/declarative-setup.md#clusters) but it does not show up in CLI/UI, how do I fix it?
|
||||
|
||||
Check if cluster secret has `argocd.argoproj.io/secret-type: cluster` label. If secret has the label but the cluster is still not visible then make sure it might be a
|
||||
permission issue. Try to list clusters using `admin` user (e.g. `argocd login --username admin && argocd cluster list`).
|
||||
|
||||
## Argo CD is unable to connect to my cluster, how do I troubleshoot it?
|
||||
|
||||
Use the following steps to reconstruct configured cluster config and connect to your cluster manually using kubectl:
|
||||
|
||||
```bash
|
||||
kubectl exec -it <argocd-pod-name> bash # ssh into any argocd server pod
|
||||
argocd-util kubeconfig https://<cluster-url> /tmp/config --namespace argocd # generate your cluster config
|
||||
KUBECONFIG=/tmp/config kubectl get pods # test connection manually
|
||||
```
|
||||
|
||||
Now you can manually verify that cluster is accessible from the Argo CD pod.
|
||||
|
||||
@@ -1,40 +1,46 @@
|
||||
# Argo CD Getting Started
|
||||
# Getting Started
|
||||
|
||||
!!! tip
|
||||
This guide assumes you have a grounding in the tools that Argo CD is based on. Please read the [understanding the basics](understand_the_basics.md).
|
||||
|
||||
## Requirements
|
||||
|
||||
* Installed [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) command-line tool
|
||||
* Have a [kubeconfig](https://kubernetes.io/docs/tasks/access-application-cluster/configure-access-multiple-clusters/) file (default location is `~/.kube/config`).
|
||||
|
||||
## 1. Install Argo CD
|
||||
|
||||
```bash
|
||||
kubectl create namespace argocd
|
||||
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
|
||||
```
|
||||
|
||||
This will create a new namespace, `argocd`, where Argo CD services and application resources will live.
|
||||
|
||||
NOTE:
|
||||
* On GKE, you will need grant your account the ability to create new cluster roles:
|
||||
```bash
|
||||
kubectl create clusterrolebinding YOURNAME-cluster-admin-binding --clusterrole=cluster-admin --user=YOUREMAIL@gmail.com
|
||||
```
|
||||
|
||||
On GKE, you will need grant your account the ability to create new cluster roles:
|
||||
|
||||
```bash
|
||||
kubectl create clusterrolebinding YOURNAME-cluster-admin-binding --clusterrole=cluster-admin --user=YOUREMAIL@gmail.com
|
||||
```
|
||||
|
||||
## 2. Download Argo CD CLI
|
||||
|
||||
Download the latest Argo CD version from https://github.com/argoproj/argo-cd/releases/latest.
|
||||
Download the latest Argo CD version from [https://github.com/argoproj/argo-cd/releases/latest].
|
||||
|
||||
Also available in Mac Homebrew:
|
||||
|
||||
```bash
|
||||
brew tap argoproj/tap
|
||||
brew install argoproj/tap/argocd
|
||||
```
|
||||
|
||||
|
||||
## 3. Access the Argo CD API server
|
||||
## 3. Access The Argo CD API Server
|
||||
|
||||
By default, the Argo CD API server is not exposed with an external IP. To access the API server,
|
||||
choose one of the following techniques to expose the Argo CD API server:
|
||||
|
||||
### Service Type LoadBalancer
|
||||
### Service Type Load Balancer
|
||||
Change the argocd-server service type to `LoadBalancer`:
|
||||
|
||||
```bash
|
||||
@@ -42,7 +48,7 @@ kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "LoadBalancer"}}
|
||||
```
|
||||
|
||||
### Ingress
|
||||
Follow the [ingress documentation](ingress.md) on how to configure Argo CD with ingress.
|
||||
Follow the [ingress documentation](operator-manual/ingress.md) on how to configure Argo CD with ingress.
|
||||
|
||||
### Port Forwarding
|
||||
Kubectl port-forwarding can also be used to connect to the API server without exposing the service.
|
||||
@@ -50,29 +56,33 @@ Kubectl port-forwarding can also be used to connect to the API server without ex
|
||||
```bash
|
||||
kubectl port-forward svc/argocd-server -n argocd 8080:443
|
||||
```
|
||||
|
||||
The API server can then be accessed using the localhost:8080
|
||||
|
||||
|
||||
## 4. Login using the CLI
|
||||
## 4. Login Using The CLI
|
||||
|
||||
Login as the `admin` user. The initial password is autogenerated to be the pod name of the
|
||||
Argo CD API server. This can be retrieved with the command:
|
||||
|
||||
```bash
|
||||
kubectl get pods -n argocd -l app.kubernetes.io/name=argocd-server -o name | cut -d'/' -f 2
|
||||
```
|
||||
|
||||
Using the above password, login to Argo CD's IP or hostname:
|
||||
|
||||
```bash
|
||||
argocd login <ARGOCD_SERVER>
|
||||
```
|
||||
|
||||
Change the password using the command:
|
||||
|
||||
```bash
|
||||
argocd account update-password
|
||||
```
|
||||
|
||||
|
||||
## 5. Register a cluster to deploy apps to (optional)
|
||||
## 5. Register A Cluster To Deploy Apps To (Optional)
|
||||
|
||||
This step registers a cluster's credentials to Argo CD, and is only necessary when deploying to
|
||||
an external cluster. When deploying internally (to the same cluster that Argo CD is running in),
|
||||
@@ -93,39 +103,39 @@ The above command installs a ServiceAccount (`argocd-manager`), into the kube-sy
|
||||
that kubectl context, and binds the service account to an admin-level ClusterRole. Argo CD uses this
|
||||
service account token to perform its management tasks (i.e. deploy/monitoring).
|
||||
|
||||
> NOTE: the rules of the `argocd-manager-role` role can be modified such that it only has
|
||||
`create`, `update`, `patch`, `delete` privileges to a limited set of namespaces, groups, kinds.
|
||||
However `get`, `list`, `watch` privileges are required at the cluster-scope for Argo CD to function.
|
||||
!!! note
|
||||
The rules of the `argocd-manager-role` role can be modified such that it only has `create`, `update`, `patch`, `delete` privileges to a limited set of namespaces, groups, kinds.
|
||||
However `get`, `list`, `watch` privileges are required at the cluster-scope for Argo CD to function.
|
||||
|
||||
## 6. Create an application from a git repository
|
||||
## 6. Create An Application From A Git Repository
|
||||
|
||||
An example git repository containing a guestbook application is available at
|
||||
An example repository containing a guestbook application is available at
|
||||
https://github.com/argoproj/argocd-example-apps.git to demonstrate how Argo CD works.
|
||||
|
||||
### Creating apps via CLI
|
||||
### Creating Apps Via CLI
|
||||
|
||||
```bash
|
||||
~~~bash
|
||||
argocd app create guestbook \
|
||||
--repo https://github.com/argoproj/argocd-example-apps.git \
|
||||
--path guestbook \
|
||||
--dest-server https://kubernetes.default.svc \
|
||||
--dest-namespace default
|
||||
```
|
||||
~~~
|
||||
|
||||
### Creating apps via UI
|
||||
### Creating Apps Via UI
|
||||
|
||||
Open a browser to the Argo CD external UI, and login using the credentials, IP/hostname set in step 4.
|
||||
Connect the https://github.com/argoproj/argocd-example-apps.git repo to Argo CD:
|
||||
|
||||

|
||||
|
||||
After connecting a git repository, select the guestbook application for creation:
|
||||
After connecting a repository, select the guestbook application for creation:
|
||||
|
||||

|
||||

|
||||
|
||||
|
||||
## 7. Sync (deploy) the application
|
||||
## 7. Sync (Deploy) The Application
|
||||
|
||||
Once the guestbook application is created, you can now view its status:
|
||||
|
||||
@@ -154,15 +164,12 @@ deployed, and no Kubernetes resources have been created. To sync (deploy) the ap
|
||||
argocd app sync guestbook
|
||||
```
|
||||
|
||||
This command retrieves the manifests from git repository and performs a `kubectl apply` of the
|
||||
This command retrieves the manifests from the repository and performs a `kubectl apply` of the
|
||||
manifests. The guestbook app is now running and you can now view its resource components, logs,
|
||||
events, and assessed health status:
|
||||
|
||||
### From UI:
|
||||
|
||||

|
||||

|
||||
|
||||
## 8. Next Steps
|
||||
|
||||
Argo CD supports additional features such as automated sync, SSO, WebHooks, RBAC, Projects. See the
|
||||
rest of the [documentation](./) for details.
|
||||
|
||||
93
docs/index.md
Normal file
93
docs/index.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# Overview
|
||||
|
||||
## What Is Argo CD?
|
||||
|
||||
Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes.
|
||||
|
||||

|
||||
|
||||
## Why Argo CD?
|
||||
|
||||
Application definitions, configurations, and environments should be declarative and version controlled.
|
||||
Application deployment and lifecycle management should be automated, auditable, and easy to understand.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
kubectl create namespace argocd
|
||||
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
|
||||
```
|
||||
|
||||
Follow our [getting started guide](getting_started.md). Further [documentation](docs/)
|
||||
is provided for additional features.
|
||||
|
||||
## How it works
|
||||
|
||||
Argo CD follows the **GitOps** pattern of using Git repositories as the source of truth for defining
|
||||
the desired application state. Kubernetes manifests can be specified in several ways:
|
||||
|
||||
* [kustomize](https://kustomize.io) applications
|
||||
* [helm](https://helm.sh) charts
|
||||
* [ksonnet](https://ksonnet.io) applications
|
||||
* [jsonnet](https://jsonnet.org) files
|
||||
* Plain directory of YAML/json manifests
|
||||
* Any custom config management tool configured as a config management plugin
|
||||
|
||||
Argo CD automates the deployment of the desired application states in the specified target environments.
|
||||
Application deployments can track updates to branches, tags, or pinned to a specific version of
|
||||
manifests at a Git commit. See [tracking strategies](user-guide/tracking_strategies.md) for additional
|
||||
details about the different tracking strategies available.
|
||||
|
||||
For a quick 10 minute overview of Argo CD, check out the demo presented to the Sig Apps community
|
||||
meeting:
|
||||
[](https://youtu.be/aWDIQMbp1cc?t=1m4s)
|
||||
|
||||
|
||||
## Architecture
|
||||
|
||||

|
||||
|
||||
Argo CD is implemented as a kubernetes controller which continuously monitors running applications
|
||||
and compares the current, live state against the desired target state (as specified in the Git repo).
|
||||
A deployed application whose live state deviates from the target state is considered `OutOfSync`.
|
||||
Argo CD reports & visualizes the differences, while providing facilities to automatically or
|
||||
manually sync the live state back to the desired target state. Any modifications made to the desired
|
||||
target state in the Git repo can be automatically applied and reflected in the specified target
|
||||
environments.
|
||||
|
||||
For additional details, see [architecture overview](operator-manual/architecture.md).
|
||||
|
||||
## Features
|
||||
|
||||
* Automated deployment of applications to specified target environments
|
||||
* Support for multiple config management/templating tools (Kustomize, Helm, Ksonnet, Jsonnet, plain-YAML)
|
||||
* Ability to manage and deploy to multiple clusters
|
||||
* SSO Integration (OIDC, OAuth2, LDAP, SAML 2.0, GitHub, GitLab, Microsoft, LinkedIn)
|
||||
* Multi-tenancy and RBAC policies for authorization
|
||||
* Rollback/Roll-anywhere to any application configuration committed in Git repository
|
||||
* Health status analysis of application resources
|
||||
* Automated configuration drift detection and visualization
|
||||
* Automated or manual syncing of applications to its desired state
|
||||
* Web UI which provides real-time view of application activity
|
||||
* CLI for automation and CI integration
|
||||
* Webhook integration (GitHub, BitBucket, GitLab)
|
||||
* Access tokens for automation
|
||||
* PreSync, Sync, PostSync hooks to support complex application rollouts (e.g.blue/green & canary upgrades)
|
||||
* Audit trails for application events and API calls
|
||||
* Prometheus metrics
|
||||
* Parameter overrides for overriding ksonnet/helm parameters in Git
|
||||
|
||||
## Community Blogs And Presentations
|
||||
|
||||
* GitOps with Argo CD: [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/)
|
||||
* KubeCon talk: [CI/CD in Light Speed with K8s and Argo CD](https://www.youtube.com/watch?v=OdzH82VpMwI&feature=youtu.be)
|
||||
* KubeCon talk: [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
|
||||
* SIG Apps demo: [Argo CD - GitOps Continuous Delivery for Kubernetes](https://www.youtube.com/watch?v=aWDIQMbp1cc&feature=youtu.be&t=1m4s)
|
||||
|
||||
## Development Status
|
||||
|
||||
Argo CD is actively developed and is being used in production to deploy SaaS services at Intuit
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
# Prometheus Metrics
|
||||
|
||||
Argo CD exposes two sets of prometheus metrics
|
||||
|
||||
## Application Metrics
|
||||
Metrics about applications. Scraped at the `argocd-metrics:8082/metrics` endpoint.
|
||||
|
||||
* Gauge for application health status
|
||||
* Gauge for application sync status
|
||||
* Counter for application sync history
|
||||
|
||||
## API Server Metrics
|
||||
Metrics about API Server API request and response activity (request totals, response codes, etc...).
|
||||
Scraped at the `argocd-server-metrics:8083/metrics` endpoint.
|
||||
75
docs/operator-manual/application.yaml
Normal file
75
docs/operator-manual/application.yaml
Normal file
@@ -0,0 +1,75 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: guestbook
|
||||
# You'll usually want to add your resources to the argocd namespace.
|
||||
namespace: argocd
|
||||
# Add a this finalizer ONLY if you want these to cascade delete.
|
||||
finalizers:
|
||||
- resources-finalizer.argocd.argoproj.io
|
||||
spec:
|
||||
# The project the application belongs to.
|
||||
project: default
|
||||
|
||||
# Source of the application manifests
|
||||
source:
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps.git
|
||||
targetRevision: HEAD
|
||||
path: guestbook
|
||||
|
||||
# helm specific config
|
||||
helm:
|
||||
valueFiles:
|
||||
- values-prod.yaml
|
||||
|
||||
# kustomize specific config
|
||||
kustomize:
|
||||
# Optional image name prefix
|
||||
namePrefix: prod-
|
||||
# Optional image tags passed to "kustomize edit set imagetag" is Kustomize 1 only.
|
||||
imageTags:
|
||||
- name: gcr.io/heptio-images/ks-guestbook-demo
|
||||
value: "0.2"
|
||||
# Optional images passed to "kustomize edit set image" is Kustomize 2 only.
|
||||
images:
|
||||
- gcr.io/heptio-images/ks-guestbook-demo:0.2
|
||||
|
||||
# directory
|
||||
directory:
|
||||
recurse: true
|
||||
|
||||
jsonnet:
|
||||
# A list of Jsonnet External Variables
|
||||
extVars:
|
||||
- name: foo
|
||||
value: bar
|
||||
# You can use "code to determine if the value is either string (false, the default) or Jsonnet code (if code is true).
|
||||
- code: true
|
||||
name: baz
|
||||
value: "true"
|
||||
# A list of Jsonnet Top-level Arguments
|
||||
tlas:
|
||||
- code: false
|
||||
name: foo
|
||||
value: bar
|
||||
|
||||
# plugin specific config
|
||||
plugin:
|
||||
- name: mypluginname
|
||||
|
||||
# Destination cluster and namespace to deploy the application
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: guestbook
|
||||
|
||||
# Sync policy
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
|
||||
# Ignore differences at the specified json pointers
|
||||
ignoreDifferences:
|
||||
- group: apps
|
||||
kind: Deployment
|
||||
jsonPointers:
|
||||
- /spec/replicas
|
||||
@@ -1,32 +1,34 @@
|
||||
|
||||
# Argo CD - Architectural Overview
|
||||
# Architectural Overview
|
||||
|
||||

|
||||

|
||||
|
||||
## Components
|
||||
|
||||
### API Server
|
||||
The API server is a gRPC/REST server which exposes the API consumed by the Web UI, CLI, and CI/CD
|
||||
systems. It has the following responsibilities:
|
||||
|
||||
* application management and status reporting
|
||||
* invoking of application operations (e.g. sync, rollback, user-defined actions)
|
||||
* repository and cluster credential management (stored as K8s secrets)
|
||||
* authentication and auth delegation to external identity providers
|
||||
* RBAC enforcement
|
||||
* listener/forwarder for git webhook events
|
||||
* listener/forwarder for Git webhook events
|
||||
|
||||
### Repository Server
|
||||
The repository server is an internal service which maintains a local cache of the git repository
|
||||
The repository server is an internal service which maintains a local cache of the Git repository
|
||||
holding the application manifests. It is responsible for generating and returning the Kubernetes
|
||||
manifests when provided the following inputs:
|
||||
|
||||
* repository URL
|
||||
* git revision (commit, tag, branch)
|
||||
* revision (commit, tag, branch)
|
||||
* application path
|
||||
* template specific settings: parameters, ksonnet environments, helm values.yaml
|
||||
|
||||
### Application Controller
|
||||
The application controller is a Kubernetes controller which continuously monitors running
|
||||
applications and compares the current, live state against the desired target state (as specified in
|
||||
the git repo). It detects `OutOfSync` application state and optionally takes corrective action. It
|
||||
the repo). It detects `OutOfSync` application state and optionally takes corrective action. It
|
||||
is responsible for invoking any user-defined hooks for lifcecycle events (PreSync, Sync, PostSync)
|
||||
|
||||
@@ -29,6 +29,8 @@ data:
|
||||
issuer: https://dev-123456.oktapreview.com
|
||||
clientID: aaaabbbbccccddddeee
|
||||
clientSecret: $oidc.okta.clientSecret
|
||||
# Optional set of OIDC scopes to request. If omitted, defaults to: ["openid", "profile", "email", "groups"]
|
||||
requestedScopes: ["openid", "profile", "email"]
|
||||
|
||||
# Git repositories configure Argo CD with (optional).
|
||||
# This list is updated when configuring/removing repos from the UI/CLI
|
||||
@@ -61,7 +63,7 @@ data:
|
||||
resource.customizations: |
|
||||
admissionregistration.k8s.io/MutatingWebhookConfiguration:
|
||||
# List of json pointers in the object to ignore differences
|
||||
ignoreDifferences:
|
||||
ignoreDifferences: |
|
||||
jsonPointers:
|
||||
- webhooks/0/clientConfig/caBundle
|
||||
certmanager.k8s.io/Certificate:
|
||||
@@ -19,3 +19,8 @@ data:
|
||||
# authorizing API requests (optional). If omitted or empty, users may be still be able to login,
|
||||
# but will see no apps, projects, etc...
|
||||
policy.default: role:readonly
|
||||
|
||||
# scopes controls which OIDC scopes to examine during rbac enforcement (in addition to `sub` scope).
|
||||
# If omitted, defaults to: `[groups]`. The scope value can be a string, or a list of strings.
|
||||
scopes: [cognito:groups, email]
|
||||
|
||||
@@ -7,12 +7,12 @@ other than what Argo CD bundles. Some reasons to do this might be:
|
||||
* To upgrade/downgrade to a specific version of a tool due to bugs or bug fixes.
|
||||
* To install additional dependencies which to be used by kustomize's configmap/secret generators
|
||||
(e.g. curl, vault, gpg, AWS CLI)
|
||||
* In the future, to install a [custom templating tool](https://github.com/argoproj/argo-cd/issues/701)
|
||||
* To install a [config management plugin](../user-guide/application_sources.md#config-management-plugins)
|
||||
|
||||
As the Argo CD repo-server is the single service responsible for generating Kubernetes manifests, it
|
||||
can be customized to use alternative toolchain required by your environment.
|
||||
|
||||
## Adding tools via volume mounts
|
||||
## Adding Tools Via Volume Mounts
|
||||
|
||||
The first technique is to use an `init` container and a `volumeMount` to copy a different verison of
|
||||
a tool into the repo-server container. In the following example, an init container is overwriting
|
||||
@@ -44,7 +44,7 @@ the helm binary with a different version than what is bundled in Argo CD:
|
||||
subPath: helm
|
||||
```
|
||||
|
||||
## BYOI (build your own image)
|
||||
## BYOI (Build Your Own Image)
|
||||
|
||||
Sometimes replacing a binary isn't sufficient and you need to install other dependencies. The
|
||||
following example builds an entirely customized repo-server from a Dockerfile, installing extra
|
||||
@@ -9,13 +9,14 @@ Argo CD applications, projects and settings can be defined declaratively using K
|
||||
| [`argocd-secret.yaml`](argocd-secret.yaml) | Secret | Password, Certificates, Signing Key |
|
||||
| [`argocd-rbac-cm.yaml`](argocd-rbac-cm.yaml) | ConfigMap | RBAC Configuration |
|
||||
| [`application.yaml`](application.yaml) | Application | Example application spec |
|
||||
| [`project.yaml`](argocd-rbac-cm.yaml) | AppProject | Example project spec |
|
||||
| [`project.yaml`](project.yaml) | AppProject | Example project spec |
|
||||
|
||||
## Applications
|
||||
|
||||
The Application CRD is the Kubernetes resource object representing a deployed application instance
|
||||
in an environment. It is defined by two key pieces of information:
|
||||
* `source` reference to the desired state in git (repository, revision, path, environment)
|
||||
|
||||
* `source` reference to the desired state in Git (repository, revision, path, environment)
|
||||
* `destination` reference to the target cluster and namespace.
|
||||
|
||||
A minimal Application spec is as follows:
|
||||
@@ -38,9 +39,24 @@ spec:
|
||||
|
||||
See [application.yaml](application.yaml) for additional fields
|
||||
|
||||
!!! warning
|
||||
By default, deleting an application will not perform a cascade delete, thereby deleting its resources. You must add the finalizer if you want this behaviour - which you may well not want.
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
finalizers:
|
||||
- resources-finalizer.argocd.argoproj.io
|
||||
```
|
||||
|
||||
### App of Apps of Apps
|
||||
|
||||
You can create an application that creates other applications, which in turn can create other applications.
|
||||
This allows you to declaratively manage a group of applications that can be deployed and configured in concert.
|
||||
|
||||
## Projects
|
||||
The AppProject CRD is the Kubernetes resource object representing a logical grouping of applications.
|
||||
It is defined by the following key pieces of information:
|
||||
|
||||
* `sourceRepos` reference to the repositories that applications within the project can pull manifests from.
|
||||
* `destinations` reference to clusters and namespaces that applications within the project can deploy into.
|
||||
* `roles` list of entities with definitions of their access to resources within the project.
|
||||
@@ -54,7 +70,7 @@ metadata:
|
||||
name: my-project
|
||||
spec:
|
||||
description: Example Project
|
||||
# Allow manifests to deploy from any git repos
|
||||
# Allow manifests to deploy from any Git repos
|
||||
sourceRepos:
|
||||
- '*'
|
||||
# Only permit applications to deploy to the guestbook namespace in the same cluster
|
||||
@@ -66,7 +82,7 @@ spec:
|
||||
- group: ''
|
||||
kind: Namespace
|
||||
# Allow all namespaced-scoped resources to be created, except for ResourceQuota, LimitRange, NetworkPolicy
|
||||
clusterResourceWhitelist:
|
||||
namespaceResourceBlacklist:
|
||||
- group: ''
|
||||
kind: ResourceQuota
|
||||
- group: ''
|
||||
@@ -99,10 +115,9 @@ Repository credentials are stored in secret. Use following steps to configure a
|
||||
|
||||
1. Create secret which contains repository credentials. Consider using [bitnami-labs/sealed-secrets](https://github.com/bitnami-labs/sealed-secrets) to store encrypted secret
|
||||
definition as a Kubernetes manifest.
|
||||
2. Register repository in the `argocd-cm` config map. Each repository must have `url` field and, depending on whether you connect using HTTPS or SSH, `usernameSecret` and `passwordSecret` (for HTTPS) or `sshPrivateKeySecret` (for SSH).
|
||||
|
||||
2. Register repository in `argocd-cm` config map. Each repository must have `url` field and `usernameSecret`, `passwordSecret` or `sshPrivateKeySecret`.
|
||||
|
||||
Example:
|
||||
Example for HTTPS:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
@@ -118,17 +133,112 @@ data:
|
||||
usernameSecret:
|
||||
name: my-secret
|
||||
key: username
|
||||
```
|
||||
|
||||
Example for SSH:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-cm
|
||||
data:
|
||||
repositories: |
|
||||
- url: git@github.com:argoproj/my-private-repository
|
||||
sshPrivateKeySecret:
|
||||
name: my-secret
|
||||
key: sshPrivateKey
|
||||
```
|
||||
|
||||
!!! tip
|
||||
The Kubernetes documentation has [instructions for creating a secret containing a private key](https://kubernetes.io/docs/concepts/configuration/secret/#use-case-pod-with-ssh-keys).
|
||||
|
||||
|
||||
### Repository Credentials (v1.1+)
|
||||
|
||||
If you want to use the same credentials for multiple repositories, you can use `repository.credentials`:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-cm
|
||||
data:
|
||||
repositories: |
|
||||
- url: https://github.com/argoproj/private-repo
|
||||
- url: https://github.com/argoproj/other-private-repo
|
||||
repository.credentials: |
|
||||
- url: https://github.com/argoproj
|
||||
passwordSecret:
|
||||
name: my-secret
|
||||
key: password
|
||||
usernameSecret:
|
||||
name: my-secret
|
||||
key: username
|
||||
```
|
||||
|
||||
Argo CD will only use the credentials if you omit `usernameSecret`, `passwordSecret`, and `sshPrivateKeySecret` fields (`insecureIgnoreHostKey` is ignored).
|
||||
|
||||
A credential may be match if it's URL is the prefix of the repository's URL. The means that credentials may match, e.g in the above example both [https://github.com/argoproj](https://github.com/argoproj) and [https://github.com](https://github.com) would match. Argo CD selects the first one that matches.
|
||||
|
||||
!!! tip
|
||||
Order your credentials with the most specific at the top and the least specific at the bottom.
|
||||
|
||||
A complete example.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-cm
|
||||
data:
|
||||
repositories: |
|
||||
# this has it's own credentials
|
||||
- url: https://github.com/argoproj/private-repo
|
||||
passwordSecret:
|
||||
name: private-repo-secret
|
||||
key: password
|
||||
usernameSecret:
|
||||
name: private-repo-secret
|
||||
key: username
|
||||
sshPrivateKeySecret:
|
||||
name: private-repo-secret
|
||||
key: sshPrivateKey
|
||||
- url: https://github.com/argoproj/other-private-repo
|
||||
- url: https://github.com/otherproj/another-private-repo
|
||||
repository.credentials: |
|
||||
# this will be used for the second repo
|
||||
- url: https://github.com/argoproj
|
||||
passwordSecret:
|
||||
name: other-private-repo-secret
|
||||
key: password
|
||||
usernameSecret:
|
||||
name: other-private-repo-secret
|
||||
key: username
|
||||
sshPrivateKeySecret:
|
||||
name: other-private-repo-secret
|
||||
key: sshPrivateKey
|
||||
# this will be used for the third repo
|
||||
- url: https://github.com
|
||||
passwordSecret:
|
||||
name: another-private-repo-secret
|
||||
key: password
|
||||
usernameSecret:
|
||||
name: another-private-repo-secret
|
||||
key: username
|
||||
sshPrivateKeySecret:
|
||||
name: another-private-repo-secret
|
||||
key: sshPrivateKey
|
||||
```
|
||||
|
||||
|
||||
## Clusters
|
||||
|
||||
Cluster credentials are stored in secrets same as repository credentials but does not require entry in `argocd-cm` config map. Each secret must have label
|
||||
`argocd.argoproj.io/secret-type: cluster` and name which is following convention: `<hostname>-<port>`.
|
||||
`argocd.argoproj.io/secret-type: cluster`.
|
||||
|
||||
The secret data must include following fields:
|
||||
|
||||
* `name` - cluster name
|
||||
* `server` - cluster api server url
|
||||
* `config` - JSON representation of following data structure:
|
||||
@@ -166,7 +276,7 @@ Cluster secret example:
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: mycluster.com-443
|
||||
name: mycluster-secret
|
||||
labels:
|
||||
argocd.argoproj.io/secret-type: cluster
|
||||
type: Opaque
|
||||
@@ -183,7 +293,7 @@ stringData:
|
||||
}
|
||||
```
|
||||
|
||||
## Helm Chart repositories
|
||||
## Helm Chart Repositories
|
||||
|
||||
Non standard Helm Chart repositories have to be registered under the `helm.repositories` key in the
|
||||
`argocd-cm` ConfigMap. Each repository must have `url` and `name` fields. For private Helm repos you
|
||||
@@ -268,10 +378,10 @@ Notes:
|
||||
* SSO configuration details: [SSO](sso.md)
|
||||
* RBAC configuration details: [RBAC](rbac.md)
|
||||
|
||||
## Manage Argo CD using Argo CD
|
||||
## Manage Argo CD Using Argo CD
|
||||
|
||||
Argo CD is able to manage itself since all settings are represented by Kubernetes manifests. The suggested way is to create [Kustomize](https://github.com/kubernetes-sigs/kustomize)
|
||||
based application which uses base Argo CD manifests from https://github.com/argoproj/argo-cd and apply required changes on top.
|
||||
based application which uses base Argo CD manifests from [https://github.com/argoproj/argo-cd] and apply required changes on top.
|
||||
|
||||
Example of `kustomization.yaml`:
|
||||
|
||||
@@ -291,4 +401,6 @@ patchesStrategicMerge:
|
||||
|
||||
The live example of self managed Argo CD config is available at https://cd.apps.argoproj.io and with configuration
|
||||
stored at [argoproj/argoproj-deployments](https://github.com/argoproj/argoproj-deployments/tree/master/argocd).
|
||||
> NOTE: You will need to sign-in using your github account to get access to https://cd.apps.argoproj.io
|
||||
|
||||
!!! note
|
||||
You will need to sign-in using your github account to get access to https://cd.apps.argoproj.io
|
||||
@@ -63,9 +63,9 @@ The `obj` is a global variable which contains the resource. The script must retu
|
||||
|
||||
NOTE: as a security measure you don't have access to most of the standard Lua libraries.
|
||||
|
||||
### Way 2. Contribute a Custom Health Check to https://github.com/argoproj/argo-cd
|
||||
### Way 2. Contribute a Custom Health Check
|
||||
|
||||
A health check can be bundled into Argo CD. Custom health check scripts are located in the `resource_customizations` directory. This must have the following directory structure:
|
||||
A health check can be bundled into Argo CD. Custom health check scripts are located in the `resource_customizations` directory of [https://github.com/argoproj/argo-cd](https://github.com/argoproj/argo-cd). This must have the following directory structure:
|
||||
|
||||
```
|
||||
argo-cd
|
||||
6
docs/operator-manual/index.md
Normal file
6
docs/operator-manual/index.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Overview
|
||||
|
||||
This guide is for administrator and operator wanting to install and configure Argo CD for other developers.
|
||||
|
||||
!!! note
|
||||
Please make sure you've completed the [getting started guide](../getting_started.md).
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
Argo CD runs both a gRPC server (used by the CLI), as well as a HTTP/HTTPS server (used by the UI).
|
||||
Both protocols are exposed by the argocd-server service object on the following ports:
|
||||
|
||||
* 443 - gRPC/HTTPS
|
||||
* 80 - HTTP (redirects to HTTPS)
|
||||
|
||||
@@ -9,7 +10,7 @@ There are several ways how Ingress can be configured.
|
||||
|
||||
## [kubernetes/ingress-nginx](https://github.com/kubernetes/ingress-nginx)
|
||||
|
||||
### Option 1: ssl-passthrough
|
||||
### Option 1: SSL-Passthrough
|
||||
|
||||
Because Argo CD serves multiple protocols (gRPC/HTTPS) on the same port (443), this provides a
|
||||
challenge when attempting to define a single nginx ingress object and rule for the argocd-service,
|
||||
@@ -44,7 +45,7 @@ and responds appropriately. Note that the `nginx.ingress.kubernetes.io/ssl-passt
|
||||
requires that the `--enable-ssl-passthrough` flag be added to the command line arguments to
|
||||
`nginx-ingress-controller`.
|
||||
|
||||
### Option 2: Multiple ingress objects and hosts
|
||||
### Option 2: Multiple Ingress Objects And Hosts
|
||||
|
||||
Since ingress-nginx Ingress supports only a single protocol per Ingress object, an alternative
|
||||
way would be to define two Ingress objects. One for HTTP/HTTPS, and the other for gRPC:
|
||||
@@ -119,14 +120,14 @@ the API server -- one for gRPC and the other for HTTP/HTTPS. However it allow TL
|
||||
happen at the ingress controller.
|
||||
|
||||
|
||||
## AWS Application Load Balancers (ALBs) and Classic ELB (HTTP mode)
|
||||
## AWS Application Load Balancers (ALBs) And Classic ELB (HTTP Mode)
|
||||
|
||||
Neither ALBs and Classic ELB in HTTP mode, do not have full support for HTTP2/gRPC which is the
|
||||
protocol used by the `argocd` CLI. Thus, when using an AWS load balancer, either Classic ELB in
|
||||
passthrough mode is needed, or NLBs.
|
||||
|
||||
|
||||
## UI base path
|
||||
## UI Base Path
|
||||
|
||||
If Argo CD UI is available under non-root path (e.g. `/argo-cd` instead of `/`) then UI path should be configured in API server.
|
||||
To configure UI path add `--basehref` flag into `argocd-server` deployment command:
|
||||
70
docs/operator-manual/metrics.md
Normal file
70
docs/operator-manual/metrics.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Metrics
|
||||
|
||||
Argo CD exposes two sets of Prometheus metrics
|
||||
|
||||
## Application Metrics
|
||||
Metrics about applications. Scraped at the `argocd-metrics:8082/metrics` endpoint.
|
||||
|
||||
* Gauge for application health status
|
||||
* Gauge for application sync status
|
||||
* Counter for application sync history
|
||||
|
||||
## API Server Metrics
|
||||
Metrics about API Server API request and response activity (request totals, response codes, etc...).
|
||||
Scraped at the `argocd-server-metrics:8083/metrics` endpoint.
|
||||
|
||||
## Prometheus Operator
|
||||
|
||||
If using Prometheus Operator, the following ServiceMonitor example manifests can be used.
|
||||
Change `metadata.labels.release` to the name of label selected by your Prometheus.
|
||||
|
||||
```yaml
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: argocd-metrics
|
||||
labels:
|
||||
release: prometheus-operator
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: argocd-metrics
|
||||
endpoints:
|
||||
- port: metrics
|
||||
```
|
||||
|
||||
```yaml
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: argocd-server-metrics
|
||||
labels:
|
||||
release: prometheus-operator
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: argocd-server-metrics
|
||||
endpoints:
|
||||
- port: metrics
|
||||
```
|
||||
|
||||
```yaml
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: argocd-repo-server-metrics
|
||||
labels:
|
||||
release: prometheus-operator
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: argocd-repo-server
|
||||
endpoints:
|
||||
- port: metrics
|
||||
```
|
||||
|
||||
## Dashboards
|
||||
|
||||
You can find an example Grafana dashboard [here](https://github.com/argoproj/argo-cd/blob/master/examples/dashboard.json)
|
||||
|
||||

|
||||
@@ -6,7 +6,7 @@ spec:
|
||||
# Project description
|
||||
description: Example Project
|
||||
|
||||
# Allow manifests to deploy from any git repos
|
||||
# Allow manifests to deploy from any Git repos
|
||||
sourceRepos:
|
||||
- '*'
|
||||
|
||||
@@ -21,7 +21,7 @@ spec:
|
||||
kind: Namespace
|
||||
|
||||
# Allow all namespaced-scoped resources to be created, except for ResourceQuota, LimitRange, NetworkPolicy
|
||||
clusterResourceWhitelist:
|
||||
namespaceResourceBlacklist:
|
||||
- group: ''
|
||||
kind: ResourceQuota
|
||||
- group: ''
|
||||
@@ -48,4 +48,4 @@ spec:
|
||||
# NOTE: JWT tokens can only be generated by the API server and the token is not persisted
|
||||
# anywhere by Argo CD. It can be prematurely revoked by removing the entry from this list.
|
||||
jwtTokens:
|
||||
- iat: 1535390316
|
||||
- iat: 1535390316
|
||||
@@ -10,9 +10,11 @@ configured, additional RBAC roles can be defined, and SSO groups can man be mapp
|
||||
## Configure RBAC
|
||||
|
||||
RBAC configuration allows defining roles and groups. Argo CD has two pre-defined roles:
|
||||
|
||||
* `role:readonly` - read-only access to all resources
|
||||
* `role:admin` - unrestricted access to all resources
|
||||
These role definitions can be seen in [builtin-policy.csv](../assets/builtin-policy.csv)
|
||||
|
||||
These role definitions can be seen in [builtin-policy.csv](https://github.com/argoproj/argo-cd/blob/master/assets/builtin-policy.csv)
|
||||
|
||||
Additional roles and groups can be configured in `argocd-rbac-cm` ConfigMap. The example below
|
||||
configures a custom role, named `org-admin`. The role is assigned to any user which belongs to
|
||||
@@ -14,7 +14,7 @@ in one of the following ways:
|
||||
1. For the local `admin` user, a username/password is exchanged for a JWT using the `/api/v1/session`
|
||||
endpoint. This token is signed & issued by the Argo CD API server itself, and has no expiration.
|
||||
When the admin password is updated, all existing admin JWT tokens are immediately revoked.
|
||||
The password is stored as a bcrypt hash in the [`argocd-secret`](../manifests/base/argocd-secret.yaml) Secret.
|
||||
The password is stored as a bcrypt hash in the [`argocd-secret`](https://github.com/argoproj/argo-cd/blob/master/manifests/base/config/argocd-secret.yaml) Secret.
|
||||
|
||||
2. For Single Sign-On users, the user completes an OAuth2 login flow to the configured OIDC identity
|
||||
provider (either delegated through the bundled Dex provider, or directly to a self-managed OIDC
|
||||
@@ -48,8 +48,9 @@ API server can enforce the use of TLS 1.2 using the flag: `--tlsminversion 1.2`.
|
||||
|
||||
Argo CD never returns sensitive data from its API, and redacts all sensitive data in API payloads
|
||||
and logs. This includes:
|
||||
|
||||
* cluster credentials
|
||||
* git credentials
|
||||
* Git credentials
|
||||
* OAuth2 client secrets
|
||||
* Kubernetes Secret values
|
||||
|
||||
@@ -90,8 +91,9 @@ argocd cluster rm https://your-kubernetes-cluster-addr
|
||||
|
||||
## Cluster RBAC
|
||||
|
||||
By default, Argo CD uses a [clusteradmin level role](../manifests/cluster-install/application-controller/argocd-application-controller-clusterrole.yaml)
|
||||
By default, Argo CD uses a [clusteradmin level role](https://github.com/argoproj/argo-cd/blob/master/manifests/base/application-controller/argocd-application-controller-role.yaml)
|
||||
in order to:
|
||||
|
||||
1. watch & operate on cluster state
|
||||
2. deploy resources to the cluster
|
||||
|
||||
@@ -117,19 +119,18 @@ kubectl edit clusterrole argocd-server
|
||||
kubectl edit clusterrole argocd-application-controller
|
||||
```
|
||||
|
||||
Note:
|
||||
|
||||
* If you to deny ArgoCD access to a kind of resource then add it as an [excluded resource](declarative-setup.md#resource-exclusion).
|
||||
!!! tip
|
||||
If you want to deny ArgoCD access to a kind of resource then add it as an [excluded resource](declarative-setup.md#resource-exclusion).
|
||||
|
||||
## Auditing
|
||||
|
||||
As a GitOps deployment tool, the git commit history provides a natural audit log of what changes
|
||||
As a GitOps deployment tool, the Git commit history provides a natural audit log of what changes
|
||||
were made to application configuration, when they were made, and by whom. However, this audit log
|
||||
only applies to what happened in git and does not necessarily correlate one-to-one with events
|
||||
only applies to what happened in Git and does not necessarily correlate one-to-one with events
|
||||
that happen in a cluster. For example, User A could have made multiple commits to application
|
||||
manifests, but User B could have just only synced those changes to the cluster sometime later.
|
||||
|
||||
To complement the git revision history, Argo CD emits Kubernetes Events of application activity,
|
||||
To complement the Git revision history, Argo CD emits Kubernetes Events of application activity,
|
||||
indicating the responsible actor when applicable. For example:
|
||||
|
||||
```bash
|
||||
@@ -161,6 +162,7 @@ at three minute intervals, just fast-tracked by the webhook event.
|
||||
## Reporting Vulnerabilities
|
||||
|
||||
Please report security vulnerabilities by e-mailing:
|
||||
* Jesse_Suen@intuit.com
|
||||
* Alexander_Matyushentsev@intuit.com
|
||||
* Edward_Lee@intuit.com
|
||||
|
||||
* [Jesse_Suen@intuit.com](mailto:Jesse_Suen@intuit.com)
|
||||
* [Alexander_Matyushentsev@intuit.com](mailto:Alexander_Matyushentsev@intuit.com)
|
||||
* [Edward_Lee@intuit.com](mailto:Edward_Lee@intuit.com)
|
||||
@@ -5,7 +5,7 @@
|
||||
Argo CD does not have any local users other than the built-in `admin` user. All other users are
|
||||
expected to login via SSO. There are two ways that SSO can be configured:
|
||||
|
||||
* Bundled Dex OIDC provider - use this option your current provider does not support OIDC (e.g. SAML,
|
||||
* Bundled Dex OIDC provider - use this option if your current provider does not support OIDC (e.g. SAML,
|
||||
LDAP) or if you wish to leverage any of Dex's connector features (e.g. the ability to map GitHub
|
||||
organizations and teams to OIDC groups claims).
|
||||
|
||||
@@ -28,17 +28,18 @@ steps should be similar for other identity providers.
|
||||
In GitHub, register a new application. The callback address should be the `/api/dex/callback`
|
||||
endpoint of your Argo CD URL (e.g. https://argocd.example.com/api/dex/callback).
|
||||
|
||||

|
||||

|
||||
|
||||
After registering the app, you will receive an OAuth2 client ID and secret. These values will be
|
||||
inputted into the Argo CD configmap.
|
||||
|
||||

|
||||

|
||||
|
||||
### 2. Configure Argo CD for SSO
|
||||
|
||||
Edit the argocd-cm configmap:
|
||||
```
|
||||
|
||||
```bash
|
||||
kubectl edit configmap argocd-cm -n argocd
|
||||
```
|
||||
|
||||
@@ -51,7 +52,7 @@ kubectl edit configmap argocd-cm -n argocd
|
||||
`connectors.config.orgs` list, add one or more GitHub organizations. Any member of the org will
|
||||
then be able to login to Argo CD to perform management tasks.
|
||||
|
||||
```
|
||||
```yaml
|
||||
data:
|
||||
url: https://argocd.example.com
|
||||
|
||||
@@ -82,6 +83,7 @@ data:
|
||||
After saving, the changes should take affect automatically.
|
||||
|
||||
NOTES:
|
||||
|
||||
* Any values which start with '$' will look to a key in argocd-secret of the same name (minus the $),
|
||||
to obtain the actual value. This allows you to store the `clientSecret` as a kubernetes secret.
|
||||
* There is no need to set `redirectURI` in the `connectors.config` as shown in the dex documentation.
|
||||
@@ -89,12 +91,12 @@ NOTES:
|
||||
correct external callback URL (e.g. https://argocd.example.com/api/dex/callback)
|
||||
|
||||
|
||||
## Existing OIDC provider
|
||||
## Existing OIDC Provider
|
||||
|
||||
To configure Argo CD to delegate authenticate to your existing OIDC provider, add the OAuth2
|
||||
configuration to the `argocd-cm` ConfigMap under the `oidc.config` key:
|
||||
|
||||
```
|
||||
```yaml
|
||||
data:
|
||||
url: https://argocd.example.com
|
||||
|
||||
@@ -103,4 +105,10 @@ data:
|
||||
issuer: https://dev-123456.oktapreview.com
|
||||
clientID: aaaabbbbccccddddeee
|
||||
clientSecret: $oidc.okta.clientSecret
|
||||
|
||||
# Some OIDC providers require a separate clientID for different callback URLs.
|
||||
# For example, if configuring Argo CD with self-hosted Dex, you will need a separate client ID
|
||||
# for the 'localhost' (CLI) client to Dex. This field is optional. If omitted, the CLI will
|
||||
# use the same clientID as the Argo CD server
|
||||
cliClientID: vvvvwwwwxxxxyyyyzzzz
|
||||
```
|
||||
@@ -2,29 +2,29 @@
|
||||
|
||||
## Overview
|
||||
|
||||
Argo CD polls git repositories every three minutes to detect changes to the manifests. To eliminate
|
||||
Argo CD polls Git repositories every three minutes to detect changes to the manifests. To eliminate
|
||||
this delay from polling, the API server can be configured to receive webhook events. Argo CD supports
|
||||
git webhook notifications from GitHub, GitLab, and BitBucket. The following explains how to configure
|
||||
a git webhook for GitHub, but the same process should be applicable to other providers.
|
||||
Git webhook notifications from GitHub, GitLab, and BitBucket. The following explains how to configure
|
||||
a Git webhook for GitHub, but the same process should be applicable to other providers.
|
||||
|
||||
### 1. Create the webhook in the git provider
|
||||
### 1. Create The WebHook In The Git Provider
|
||||
|
||||
In your git provider, navigate to the settings page where webhooks can be configured. The payload
|
||||
URL configured in the git provider should use the `/api/webhook` endpoint of your Argo CD instance
|
||||
(e.g. https://argocd.example.com/api/webhook). If you wish to use a shared secret, input an
|
||||
In your Git provider, navigate to the settings page where webhooks can be configured. The payload
|
||||
URL configured in the Git provider should use the `/api/webhook` endpoint of your Argo CD instance
|
||||
(e.g. [https://argocd.example.com/api/webhook]). If you wish to use a shared secret, input an
|
||||
arbitrary value in the secret. This value will be used when configuring the webhook in the next step.
|
||||
|
||||

|
||||

|
||||
|
||||
### 2. Configure Argo CD with the webhook secret (optional)
|
||||
### 2. Configure Argo CD With The WebHook Secret Optional)
|
||||
|
||||
Configuring a webhook shared secret is optional, since Argo CD will still refresh applications
|
||||
related to the git repository, even with unauthenticated webhook events. This is safe to do since
|
||||
related to the Git repository, even with unauthenticated webhook events. This is safe to do since
|
||||
the contents of webhook payloads are considered untrusted, and will only result in a refresh of the
|
||||
application (a process which already occurs at three-minute intervals). If Argo CD is publicly
|
||||
accessible, then configuring a webhook secret is recommended to prevent a DDoS attack.
|
||||
|
||||
In the `argocd-secret` kubernetes secret, configure one of the following keys with the git
|
||||
In the `argocd-secret` kubernetes secret, configure one of the following keys with the Git
|
||||
provider's webhook secret configured in step 1.
|
||||
|
||||
| Provider | K8s Secret Key |
|
||||
@@ -34,7 +34,8 @@ provider's webhook secret configured in step 1.
|
||||
| BitBucket | `bitbucket.webhook.uuid` |
|
||||
|
||||
Edit the Argo CD kubernetes secret:
|
||||
```
|
||||
|
||||
```bash
|
||||
kubectl edit secret argocd-secret -n argocd
|
||||
```
|
||||
|
||||
@@ -43,7 +44,7 @@ which saves you the trouble of base64 encoding the values and copying it to the
|
||||
Simply copy the shared webhook secret created in step 1, to the corresponding
|
||||
GitHub/GitLab/BitBucket key under the `stringData` field:
|
||||
|
||||
```
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
@@ -62,7 +63,6 @@ stringData:
|
||||
|
||||
# bitbucket webhook secret
|
||||
bitbucket.webhook.uuid: your-bitbucket-uuid
|
||||
|
||||
```
|
||||
|
||||
After saving, the changes should take affect automatically.
|
||||
17
docs/understand_the_basics.md
Normal file
17
docs/understand_the_basics.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Understand The Basics
|
||||
|
||||
Before effectively using Argo CD, it is necessary to understand the underlying technology that the platform is built on. It is also necessary to understand the features being provided to you and how to use them. The section below provides some useful links to build up this understanding.
|
||||
|
||||
## Learn The Fundamentals
|
||||
|
||||
* Go through the online Docker and Kubernetes tutorials
|
||||
* [A Beginner-Friendly Introduction to Containers, VMs and Docker](https://medium.freecodecamp.org/a-beginner-friendly-introduction-to-containers-vms-and-docker-79a9e3e119b)
|
||||
* [Introduction to Kubernetes](https://courses.edx.org/courses/course-v1:LinuxFoundationX+LFS158x+2T2017/course/)
|
||||
* [Tutorials](https://kubernetes.io/docs/tutorials/)
|
||||
* [Hands on labs](https://katacoda.com/courses/kubernetes/)
|
||||
* Depending on how you plan to template your applications:
|
||||
* [Kustomize](https://kustomize.io)
|
||||
* [Helm](https://helm.sh)
|
||||
* [Ksonnet](https://ksonnet.io)
|
||||
* If you're integrating with Jenkins:
|
||||
* [Jenkins User Guide](https://jenkins.io)
|
||||
@@ -2,14 +2,18 @@
|
||||
|
||||
Argo CD supports several different ways in which kubernetes manifests can be defined:
|
||||
|
||||
* [ksonnet](https://ksonnet.io) applications
|
||||
* [kustomize](https://kustomize.io) applications
|
||||
* [helm](https://helm.sh) charts
|
||||
* Directory of YAML/json/jsonnet manifests
|
||||
* **[Ksonnet](https://ksonnet.io)** applications
|
||||
* **[Kustomize](https://kustomize.io)** applications
|
||||
* **[Helm](https://helm.sh)** charts
|
||||
* **Directory** of YAML/json/jsonnet manifests
|
||||
* Any custom config management tool configured as a config management plugin
|
||||
|
||||
Some additional considerations should be made when deploying apps of a particular type:
|
||||
|
||||
## Kustomize
|
||||
|
||||
Ops. We haven't got around to writing this part yet.
|
||||
|
||||
## Ksonnet
|
||||
|
||||
### Environments
|
||||
@@ -17,7 +21,7 @@ Ksonnet has a first class concept of an "environment." To create an application
|
||||
app directory, an environment must be specified. For example, the following command creates the
|
||||
"guestbook-default" app, which points to the `default` environment:
|
||||
|
||||
```
|
||||
```bash
|
||||
argocd app create guestbook-default --repo https://github.com/argoproj/argocd-example-apps.git --path guestbook --env default
|
||||
```
|
||||
|
||||
@@ -25,7 +29,7 @@ argocd app create guestbook-default --repo https://github.com/argoproj/argocd-ex
|
||||
Ksonnet parameters all belong to a component. For example, the following are the parameters
|
||||
available in the guestbook app, all of which belong to the `guestbook-ui` component:
|
||||
|
||||
```
|
||||
```bash
|
||||
$ ks param list
|
||||
COMPONENT PARAM VALUE
|
||||
========= ===== =====
|
||||
@@ -39,7 +43,8 @@ guestbook-ui type "LoadBalancer"
|
||||
|
||||
When overriding ksonnet parameters in Argo CD, the component name should also be specified in the
|
||||
`argocd app set` command, in the form of `-p COMPONENT=PARAM=VALUE`. For example:
|
||||
```
|
||||
|
||||
```bash
|
||||
argocd app set guestbook-default -p guestbook-ui=image=gcr.io/heptio-images/ks-guestbook-demo:0.1
|
||||
```
|
||||
|
||||
@@ -51,7 +56,7 @@ Helm has the ability to use a different, or even multiple "values.yaml" files to
|
||||
parameters from. Alternate or multiple values file(s), can be specified using the `--values`
|
||||
flag. The flag can be repeated to support multiple values files:
|
||||
|
||||
```
|
||||
```bash
|
||||
argocd app set helm-guestbook --values values-production.yaml
|
||||
```
|
||||
|
||||
@@ -59,12 +64,15 @@ argocd app set helm-guestbook --values values-production.yaml
|
||||
|
||||
Helm has the ability to set parameter values, which override any values in
|
||||
a `values.yaml`. For example, `service.type` is a common parameter which is exposed in a Helm chart:
|
||||
```
|
||||
|
||||
```bash
|
||||
helm template . --set service.type=LoadBalancer
|
||||
```
|
||||
|
||||
Similarly Argo CD can override values in the `values.yaml` parameters using `argo app set` command,
|
||||
in the form of `-p PARAM=VALUE`. For example:
|
||||
```
|
||||
|
||||
```bash
|
||||
argocd app set helm-guestbook -p service.type=LoadBalancer
|
||||
```
|
||||
|
||||
@@ -82,9 +90,9 @@ of Pre/Post/Sync hooks.
|
||||
Helm templating has the ability to generate random data during chart rendering via the
|
||||
`randAlphaNum` function. Many helm charts from the [charts repository](https://github.com/helm/charts)
|
||||
make use of this feature. For example, the following is the secret for the
|
||||
[redis helm chart](https://github.com/helm/charts/blob/master/stable/redis/templates/secrets.yaml):
|
||||
[redis helm chart](https://github.com/helm/charts/blob/master/stable/redis/templates/secret.yaml):
|
||||
|
||||
```
|
||||
```yaml
|
||||
data:
|
||||
{{- if .Values.password }}
|
||||
redis-password: {{ .Values.password | b64enc | quote }}
|
||||
@@ -93,13 +101,13 @@ data:
|
||||
{{- end }}
|
||||
```
|
||||
|
||||
The Argo CD application controller periodically compares git state against the live state, running
|
||||
The Argo CD application controller periodically compares Git state against the live state, running
|
||||
the `helm template <CHART>` command to generate the helm manifests. Because the random value is
|
||||
regenerated every time the comparison is made, any application which makes use of the `randAlphaNum`
|
||||
function will always be in an `OutOfSync` state. This can be mitigated by explicitly setting a
|
||||
value, in the values.yaml such that the value is stable between each comparison. For example:
|
||||
|
||||
```
|
||||
```bash
|
||||
argocd app set redis -p password=abc123
|
||||
```
|
||||
|
||||
@@ -107,7 +115,7 @@ argocd app set redis -p password=abc123
|
||||
|
||||
Argo CD allows integrating more config management tools using config management plugins. Following changes are required to configure new plugin:
|
||||
|
||||
* Make sure required binaries are available in `argocd-repo-server` pod. The binaries can be added via volume mounts or using custom image (see [custom_tools](custom_tools.md)).
|
||||
* Make sure required binaries are available in `argocd-repo-server` pod. The binaries can be added via volume mounts or using custom image (see [custom_tools](../operator-manual/custom_tools.md)).
|
||||
* Register a new plugin in `argocd-cm` ConfigMap:
|
||||
|
||||
```yaml
|
||||
@@ -129,7 +137,7 @@ Commands have access to system environment variables and following additional va
|
||||
|
||||
* Create an application and specify required config management plugin name.
|
||||
|
||||
```
|
||||
```bash
|
||||
argocd app create <appName> --config-management-plugin <pluginName>
|
||||
```
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Automated Sync Policy
|
||||
|
||||
Argo CD has the ability to automatically sync an application when it detects differences between
|
||||
the desired manifests in git, and the live state in the cluster. A benefit of automatic sync is that
|
||||
the desired manifests in Git, and the live state in the cluster. A benefit of automatic sync is that
|
||||
CI/CD pipelines no longer need direct access to the Argo CD API server to perform the deployment.
|
||||
Instead, the pipeline makes a commit and push to the git repository with the changes to the
|
||||
manifests in the tracking git repo.
|
||||
Instead, the pipeline makes a commit and push to the Git repository with the changes to the
|
||||
manifests in the tracking Git repo.
|
||||
|
||||
To configure automated sync run:
|
||||
```bash
|
||||
@@ -22,7 +22,7 @@ spec:
|
||||
## Automatic Pruning
|
||||
|
||||
By default (and as a safety mechanism), automated sync will not delete resources when Argo CD detects
|
||||
the resource is no longer defined in git. To prune the resources, a manual sync can always be
|
||||
the resource is no longer defined in Git. To prune the resources, a manual sync can always be
|
||||
performed (with pruning checked). Pruning can also be enabled to happen automatically as part of the
|
||||
automated sync by running:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Best Practices
|
||||
|
||||
## Separating config vs. source code repositories
|
||||
## Separating Config Vs. Source Code Repositories
|
||||
|
||||
Using a separate git repository to hold your kubernetes manifests, keeping the config separate
|
||||
Using a separate Git repository to hold your kubernetes manifests, keeping the config separate
|
||||
from your application source code, is highly recommended for the following reasons:
|
||||
|
||||
1. It provides a clean separation of application code vs. application config. There will be times
|
||||
@@ -11,10 +11,10 @@ from your application source code, is highly recommended for the following reaso
|
||||
a Deployment spec.
|
||||
|
||||
2. Cleaner audit log. For auditing purposes, a repo which only holds configuration will have a much
|
||||
cleaner git history of what changes were made, without the noise coming from check-ins due to
|
||||
cleaner Git history of what changes were made, without the noise coming from check-ins due to
|
||||
normal development activity.
|
||||
|
||||
3. Your application may be comprised of services built from multiple git repositories, but is
|
||||
3. Your application may be comprised of services built from multiple Git repositories, but is
|
||||
deployed as a single unit. Oftentimes, microservices applications are comprised of services
|
||||
with different versioning schemes, and release cycles (e.g. ELK, Kafka + Zookeeper). It may not
|
||||
make sense to store the manifests in one of the source code repositories of a single component.
|
||||
@@ -24,17 +24,17 @@ from your application source code, is highly recommended for the following reaso
|
||||
unintentionally. By having separate repos, commit access can be given to the source code repo,
|
||||
and not the application config repo.
|
||||
|
||||
5. If you are automating your CI pipeline, pushing manifest changes to the same git repository can
|
||||
trigger an infinite loop of build jobs and git commit triggers. Having a separate repo to push
|
||||
5. If you are automating your CI pipeline, pushing manifest changes to the same Cit repository can
|
||||
trigger an infinite loop of build jobs and Git commit triggers. Having a separate repo to push
|
||||
config changes to, prevents this from happening.
|
||||
|
||||
|
||||
## Leaving room for imperativeness
|
||||
## Leaving Room For Imperativeness
|
||||
|
||||
It may be desired to leave room for some imperativeness/automation, and not have everything defined
|
||||
in your git manifests. For example, if you want the number of your deployment's replicas to be
|
||||
in your Git manifests. For example, if you want the number of your deployment's replicas to be
|
||||
managed by [Horizontal Pod Autoscaler](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/),
|
||||
then you would not want to track `replicas` in git.
|
||||
then you would not want to track `replicas` in Git.
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
@@ -54,14 +54,14 @@ spec:
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
## Ensuring manifests at git revisions are truly immutable
|
||||
## Ensuring Manifests At Git Revisions Are Truly Immutable
|
||||
|
||||
When using templating tools like `helm` or `kustomize`, it is possible for manifests to change
|
||||
their meaning from one day to the next. This is typically caused by changes made to an upstream helm
|
||||
repository or kustomize base.
|
||||
|
||||
For example, consider the following kustomization.yaml
|
||||
|
||||
```yaml
|
||||
bases:
|
||||
- github.com/argoproj/argo-cd//manifests/cluster-install
|
||||
@@ -69,9 +69,10 @@ bases:
|
||||
|
||||
The above kustomization has a remote base to he HEAD revision of the argo-cd repo. Since this
|
||||
is not stable target, the manifests for this kustomize application can suddenly change meaning, even without
|
||||
any changes to your own git repository.
|
||||
any changes to your own Git repository.
|
||||
|
||||
A better version would be to use a Git tag or commit SHA. For example:
|
||||
|
||||
A better version would be to use a git tag or commit SHA. For example:
|
||||
```yaml
|
||||
bases:
|
||||
- github.com/argoproj/argo-cd//manifests/cluster-install?ref=v0.11.1
|
||||
58
docs/user-guide/ci_automation.md
Normal file
58
docs/user-guide/ci_automation.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Automation from CI Pipelines
|
||||
|
||||
Argo CD follows the GitOps model of deployment, where desired configuration changes are first
|
||||
pushed to Git, and the cluster state then syncs to the desired state in git. This is a departure
|
||||
from imperative pipelines which do not traditionally use Git repositories to hold application
|
||||
config.
|
||||
|
||||
To push new container images into to a cluster managed by Argo CD, the following workflow (or
|
||||
variations), might be used:
|
||||
|
||||
## Build And Publish A New Container Image
|
||||
|
||||
```bash
|
||||
docker build -t mycompany/guestbook:v2.0 .
|
||||
docker push mycompany/guestbook:v2.0
|
||||
```
|
||||
|
||||
## Update The Local Manifests Using Your Preferred Templating Tool, And Push The Changes To Git
|
||||
|
||||
!!! tip
|
||||
The use of a different Git repository to hold your kubernetes manifests (separate from
|
||||
your application source code), is highly recommended. See [best practices](best_practices.md)
|
||||
for further rationale.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/mycompany/guestbook-config.git
|
||||
cd guestbook-config
|
||||
|
||||
# kustomize
|
||||
kustomize edit set imagetag mycompany/guestbook:v2.0
|
||||
|
||||
# ksonnet
|
||||
ks param set guestbook image mycompany/guestbook:v2.0
|
||||
|
||||
# plain yaml
|
||||
kubectl patch --local -f config-deployment.yaml -p '{"spec":{"template":{"spec":{"containers":[{"name":"guestbook","image":"mycompany/guestbook:v2.0"}]}}}}' -o yaml
|
||||
|
||||
git add . -m "Update guestbook to v2.0"
|
||||
git push
|
||||
```
|
||||
|
||||
## Synchronize The App (Optional)
|
||||
|
||||
For convenience, the argocd CLI can be downloaded directly from the API server. This is
|
||||
useful so that the CLI used in the CI pipeline is always kept in-sync and uses argocd binary
|
||||
that is always compatible with the Argo CD API server.
|
||||
|
||||
```bash
|
||||
export ARGOCD_SERVER=argocd.mycompany.com
|
||||
export ARGOCD_AUTH_TOKEN=<JWT token generated from project>
|
||||
curl -sSL -o /usr/local/bin/argocd https://${ARGOCD_SERVER}/download/argocd-linux-amd64
|
||||
argocd app sync guestbook
|
||||
argocd app wait guestbook
|
||||
```
|
||||
|
||||
If [automated synchronization](auto_sync.md) is configured for the application, this step is
|
||||
unnecessary. The controller will automatically detect the new config (fast tracked using a
|
||||
[webhook](../operator-manual/webhook.md), or polled every 3 minutes), and automatically sync the new manifests.
|
||||
@@ -11,13 +11,13 @@ submitted to Kubernetes in a manner which contradicts Git.
|
||||
which generates different data every time `helm template` is invoked.
|
||||
* For Horizontal Pod Autoscaling (HPA) objects, the HPA controller is known to reorder `spec.metrics`
|
||||
in a specific order. See [kubernetes issue #74099](https://github.com/kubernetes/kubernetes/issues/74099).
|
||||
To work around this, you can order `spec.replicas` in git in the same order that the controller
|
||||
To work around this, you can order `spec.replicas` in Git in the same order that the controller
|
||||
prefers.
|
||||
|
||||
In case it is impossible to fix the upstream issue, Argo CD allows you to optionally ignore differences of problematic resources.
|
||||
The diffing customization can be configured for single or multiple application resources or at a system level.
|
||||
|
||||
## Application level configuration
|
||||
## Application Level Configuration
|
||||
|
||||
Argo CD allows ignoring differences at a specific JSON path. The following sample application is configured to ignore differences in `spec.replicas` for all deployments:
|
||||
|
||||
@@ -43,7 +43,7 @@ spec:
|
||||
- /spec/replicas
|
||||
```
|
||||
|
||||
## System-level configuration
|
||||
## System-Level Configuration
|
||||
|
||||
The comparison of resources with well-known issues can be customized at a system level. Ignored differences can be configured for a specified group and kind
|
||||
in `resource.customizations` key of `argocd-cm` ConfigMap. Following is an example of a customization which ignores the `caBundle` field
|
||||
@@ -53,7 +53,7 @@ of a `MutatingWebhookConfiguration` webhooks:
|
||||
data:
|
||||
resource.customizations: |
|
||||
admissionregistration.k8s.io/MutatingWebhookConfiguration:
|
||||
ignoreDifferences:
|
||||
ignoreDifferences: |
|
||||
jsonPointers:
|
||||
- webhooks/0/clientConfig/caBundle
|
||||
- /webhooks/0/clientConfig/caBundle
|
||||
```
|
||||
6
docs/user-guide/index.md
Normal file
6
docs/user-guide/index.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Overview
|
||||
|
||||
This guide is for developers who have Argo CD installed for them and are managing applications.
|
||||
|
||||
!!! note
|
||||
Please make sure you've completed the [getting started guide](../getting_started.md).
|
||||
@@ -1,18 +1,20 @@
|
||||
# Parameter Overrides
|
||||
|
||||
Argo CD provides a mechanism to override the parameters of a ksonnet/helm app. This provides flexibility
|
||||
in having most of the application manifests defined in git, while leaving room for *some* parts of the
|
||||
k8s manifests determined dynamically, or outside of git. It also serves as an alternative way of
|
||||
in having most of the application manifests defined in Git, while leaving room for *some* parts of the
|
||||
k8s manifests determined dynamically, or outside of Git. It also serves as an alternative way of
|
||||
redeploying an application by changing application parameters via Argo CD, instead of making the
|
||||
changes to the manifests in git.
|
||||
changes to the manifests in Git.
|
||||
|
||||
**NOTE:** many consider this mode of operation as an anti-pattern to GitOps, since the source of
|
||||
truth becomes a union of the git repository, and the application overrides. The Argo CD parameter
|
||||
overrides feature is provided mainly as a convenience to developers and is intended to be used in
|
||||
dev/test environments, vs. production environments.
|
||||
!!! tip
|
||||
Many consider this mode of operation as an anti-pattern to GitOps, since the source of
|
||||
truth becomes a union of the Git repository, and the application overrides. The Argo CD parameter
|
||||
overrides feature is provided mainly as a convenience to developers and is intended to be used in
|
||||
dev/test environments, vs. production environments.
|
||||
|
||||
To use parameter overrides, run the `argocd app set -p (COMPONENT=)PARAM=VALUE` command:
|
||||
```
|
||||
|
||||
```bash
|
||||
argocd app set guestbook -p guestbook=image=example/guestbook:abcd123
|
||||
argocd app sync guestbook
|
||||
```
|
||||
@@ -30,7 +32,7 @@ The following are situations where parameter overrides would be useful:
|
||||
version of their guestbook application after every build in the tip of master. To address this use
|
||||
case, the application would expose a parameter named `image`, whose value used in the `dev`
|
||||
environment contains a placeholder value (e.g. `example/guestbook:replaceme`). The placeholder value
|
||||
would be determined externally (outside of git) such as a build system. Then, as part of the build
|
||||
would be determined externally (outside of Git) such as a build system. Then, as part of the build
|
||||
pipeline, the parameter value of the `image` would be continually updated to the freshly built image
|
||||
(e.g. `argocd app set guestbook -p guestbook=image=example/guestbook:abcd123`). A sync operation
|
||||
would result in the application being redeployed with the new image.
|
||||
@@ -41,6 +43,6 @@ the public repository and customize the deployment with different parameters, wi
|
||||
forking the repository to make the changes. For example, to install Redis from the Helm chart
|
||||
repository and customize the the database password, you would run:
|
||||
|
||||
```
|
||||
```bash
|
||||
argocd app create redis --repo https://github.com/helm/charts.git --path stable/redis --dest-server https://kubernetes.default.svc --dest-namespace default -p password=abc123
|
||||
```
|
||||
71
docs/user-guide/private-repositories.md
Normal file
71
docs/user-guide/private-repositories.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Private Repositories
|
||||
|
||||
## Credentials
|
||||
|
||||
If application manifests are located in private repository then repository credentials have to be configured. Argo CD supports both HTTP and SSH Git credentials.
|
||||
|
||||
### HTTP Username And Password Credential
|
||||
|
||||
Private repositories that require a username and password typically have a URL that start with "https://" rather than "git@" or "ssh://".
|
||||
|
||||
Credentials can be configured using Argo CD CLI:
|
||||
|
||||
```bash
|
||||
argocd repo add https://github.com/argoproj/argocd-example-apps --username <username> --password <password>
|
||||
```
|
||||
|
||||
or UI:
|
||||
|
||||
1. Navigate to `Settings/Repositories`
|
||||
1. Click `Connect Repo` button and enter HTTP credentials
|
||||
|
||||

|
||||
|
||||
#### Access Token
|
||||
|
||||
Instead of using username and password you might use access token. Following instructions of your Git hosting service to generate the token:
|
||||
|
||||
* [Github](https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line)
|
||||
* [Gitlab](https://docs.gitlab.com/ee/user/project/deploy_tokens/)
|
||||
* [Bitbucket](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html)
|
||||
|
||||
Then, connect the repository using an empty string as a username and access token value as a password.
|
||||
|
||||
### SSH Private Key Credential
|
||||
|
||||
Private repositories that require an SSH private key have a URL that typically start with "git@" or "ssh://" rather than "https://".
|
||||
|
||||
The Argo CD UI don't support configuring SSH credentials. The SSH credentials can only be configured using the Argo CD CLI:
|
||||
|
||||
```
|
||||
argocd repo add git@github.com:argoproj/argocd-example-apps.git --ssh-private-key-path ~/.ssh/id_rsa
|
||||
```
|
||||
|
||||
## Self-Signed Certificates
|
||||
|
||||
If you are using self-hosted Git hosting service with the self-signed certificate then you need to disable certificate validation for that Git host.
|
||||
Following options are available:
|
||||
|
||||
Add repository using Argo CD CLI and `--insecure-ignore-host-key` flag:
|
||||
|
||||
|
||||
```bash
|
||||
argocd repo add git@github.com:argoproj/argocd-example-apps.git --ssh-private-key-path ~/.ssh/id_rsa
|
||||
```
|
||||
|
||||
The flag disables certificate validation only for specified repository.
|
||||
|
||||
!!! warning
|
||||
The `--insecure-ignore-host-key` flag does not work for HTTPS Git URLs. See [#1513](https://github.com/argoproj/argo-cd/issues/1513).
|
||||
|
||||
You can add Git service hostname to the `/etc/ssh/ssh_known_hosts` in each Argo CD deployment and disables cert validation for Git SSL URLs. For more information see
|
||||
[example](https://github.com/argoproj/argo-cd/tree/master/examples/known-hosts) which demonstrates how `/etc/ssh/ssh_known_hosts` can be customized.
|
||||
|
||||
!!! note
|
||||
The `/etc/ssh/ssh_known_hosts` should include Git host on each Argo CD deployment as well as on a computer where `argocd repo add` is executed. After resolving issue
|
||||
[#1514](https://github.com/argoproj/argo-cd/issues/1514) only `argocd-repo-server` deployment has to be customized.
|
||||
|
||||
## Declarative Configuration
|
||||
|
||||
See [declarative setup](../operator-manual/declarative-setup#Repositories)
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
Projects provide a logical grouping of applications, which is useful when Argo CD is used by multiple
|
||||
teams. Projects provide the following features:
|
||||
|
||||
* restrict *what* may be deployed (trusted git source repositories)
|
||||
* restrict *what* may be deployed (trusted Git source repositories)
|
||||
* restrict *where* apps may be deployed to (destination clusters and namespaces)
|
||||
* restrict what kinds of objects may or may not be deployed (e.g. RBAC, CRDs, DaemonSets, NetworkPolicy etc...)
|
||||
* defining project roles to provide application RBAC (bound to OIDC groups and/or JWT tokens)
|
||||
|
||||
### The default project
|
||||
### The Default Project
|
||||
|
||||
Every application belongs to a single project. If unspecified, an application belongs to the
|
||||
`default` project, which is created automatically and by default, permits deployments from any
|
||||
@@ -31,16 +31,16 @@ spec:
|
||||
|
||||
Additional projects can be created to give separate teams different levels of access to namespaces.
|
||||
The following command creates a new project `myproject` which can deploy applications to namespace
|
||||
`mynamespace` of cluster `https://kubernetes.default.svc`. The permitted git source repository is
|
||||
`mynamespace` of cluster `https://kubernetes.default.svc`. The permitted Git source repository is
|
||||
set to `https://github.com/argoproj/argocd-example-apps.git` repository.
|
||||
|
||||
```
|
||||
```bash
|
||||
argocd proj create myproject -d https://kubernetes.default.svc,mynamespace -s https://github.com/argoproj/argocd-example-apps.git
|
||||
```
|
||||
|
||||
### Managing Projects
|
||||
|
||||
Permitted source git repositories are managed using commands:
|
||||
Permitted source Git repositories are managed using commands:
|
||||
|
||||
```bash
|
||||
argocd proj add-source <PROJECT> <REPO>
|
||||
@@ -48,7 +48,8 @@ argocd proj remove-source <PROJECT> <REPO>
|
||||
```
|
||||
|
||||
Permitted destination clusters and namespaces are managed with the commands:
|
||||
```
|
||||
|
||||
```bash
|
||||
argocd proj add-destination <PROJECT> <CLUSTER>,<NAMESPACE>
|
||||
argocd proj remove-destination <PROJECT> <CLUSTER>,<NAMESPACE>
|
||||
```
|
||||
@@ -56,14 +57,15 @@ argocd proj remove-destination <PROJECT> <CLUSTER>,<NAMESPACE>
|
||||
Permitted destination K8s resource kinds are managed with the commands. Note that namespaced-scoped
|
||||
resources are restricted via a blacklist, whereas cluster-scoped resources are restricted via
|
||||
whitelist.
|
||||
```
|
||||
|
||||
```bash
|
||||
argocd proj allow-cluster-resource <PROJECT> <GROUP> <KIND>
|
||||
argocd proj allow-namespace-resource <PROJECT> <GROUP> <KIND>
|
||||
argocd proj deny-cluster-resource <PROJECT> <GROUP> <KIND>
|
||||
argocd proj deny-namespace-resource <PROJECT> <GROUP> <KIND>
|
||||
```
|
||||
|
||||
### Assign application to a project
|
||||
### Assign Application To A Project
|
||||
|
||||
The application project can be changed using `app set` command. In order to change the project of
|
||||
an app, the user must have permissions to access the new project.
|
||||
@@ -72,7 +74,7 @@ an app, the user must have permissions to access the new project.
|
||||
argocd app set guestbook-default --project myproject
|
||||
```
|
||||
|
||||
### Configuring RBAC with projects
|
||||
### Configuring RBAC With Projects
|
||||
|
||||
Once projects have been defined, RBAC rules can be written to restrict access to the applications
|
||||
in the project. The following example configures RBAC for two GitHub teams: `team1` and `team2`,
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
Synchronization can be configured using resource hooks. Hooks are ways to interject custom logic before, during,
|
||||
and after a Sync operation. Some use cases for hooks are:
|
||||
|
||||
* Using a `PreSync` hook to perform a database schema migration before deploying a new version of the app.
|
||||
* Using a `Sync` hook to orchestrate a complex deployment requiring more sophistication than the
|
||||
kubernetes rolling update strategy (e.g. a blue/green deployment).
|
||||
18
docs/user-guide/tool_detection.md
Normal file
18
docs/user-guide/tool_detection.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Tool Detection
|
||||
|
||||
The tool used to build an application is detected as follows:
|
||||
|
||||
If a specific tool is explicitly configured, then that tool is selected to create your application's manifests.
|
||||
|
||||
If not, then the tool is detected implicitly as follows:
|
||||
|
||||
* **Ksonnet** if there are two files, one named `app.yaml` and one named `components/params.libsonnet`.
|
||||
* **Helm** if there's a file matching `Chart.yaml`.
|
||||
* **Kustomize** if there's a `kustomization.yaml`, `kustomization.yml`, or `Kustomization`
|
||||
|
||||
Otherwise it is assumed to be a plain **directory** application.
|
||||
|
||||
## References
|
||||
|
||||
* [reposerver/repository/repository.go/GetAppSourceType](https://github.com/argoproj/argo-cd/blob/master/reposerver/repository/repository.go#L286)
|
||||
* [server/repository/repository.go/listAppTypes](https://github.com/argoproj/argo-cd/blob/master/server/repository/repository.go#L97)
|
||||
@@ -1,7 +1,7 @@
|
||||
# Tracking and Deployment Strategies
|
||||
|
||||
An Argo CD application spec provides several different ways of track kubernetes resource manifests in
|
||||
git. This document describes the different techniques and the means of deploying those manifests to
|
||||
Git. This document describes the different techniques and the means of deploying those manifests to
|
||||
the target environment.
|
||||
|
||||
## HEAD / Branch Tracking
|
||||
@@ -15,17 +15,17 @@ changes to the tracked branch/symbolic reference, which will then be detected by
|
||||
|
||||
## Tag Tracking
|
||||
|
||||
If a tag is specified, the manifests at the specified git tag will be used to perform the sync
|
||||
If a tag is specified, the manifests at the specified Git tag will be used to perform the sync
|
||||
comparison. This provides some advantages over branch tracking in that a tag is generally considered
|
||||
more stable, and less frequently updated, with some manual judgement of what constitutes a tag.
|
||||
|
||||
To redeploy an application, the user uses git to change the meaning of a tag by retagging it to a
|
||||
To redeploy an application, the user uses Git to change the meaning of a tag by retagging it to a
|
||||
different commit SHA. Argo CD will detect the new meaning of the tag when performing the
|
||||
comparison/sync.
|
||||
|
||||
## Commit Pinning
|
||||
|
||||
If a git commit SHA is specified, the application is effectively pinned to the manifests defined at
|
||||
If a Git commit SHA is specified, the application is effectively pinned to the manifests defined at
|
||||
the specified commit. This is the most restrictive of the techniques and is typically used to
|
||||
control production environments.
|
||||
|
||||
@@ -38,9 +38,9 @@ on an application which is pinned to a revision.
|
||||
|
||||
In all tracking strategies, the application has the option to sync automatically. If [auto-sync](auto_sync.md)
|
||||
is configured, the new resources manifests will be applied automatically -- as soon as a difference
|
||||
is detected between the target state (git) and live state. If auto-sync is disabled, a manual sync
|
||||
is detected between the target state (Git) and live state. If auto-sync is disabled, a manual sync
|
||||
will be needed using the Argo UI, CLI, or API.
|
||||
|
||||
## Parameter Overrides
|
||||
Note that in all tracking strategies, any [parameter overrides](parameters.md) set in the
|
||||
application instance take precedence over the git state.
|
||||
application instance take precedence over the Git state.
|
||||
2611
examples/dashboard.json
Normal file
2611
examples/dashboard.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,39 +0,0 @@
|
||||
apiVersion: 0.1.0
|
||||
gitVersion:
|
||||
commitSha: 422d521c05aa905df949868143b26445f5e4eda5
|
||||
refSpec: master
|
||||
kind: ksonnet.io/registry
|
||||
libraries:
|
||||
apache:
|
||||
path: apache
|
||||
version: master
|
||||
efk:
|
||||
path: efk
|
||||
version: master
|
||||
mariadb:
|
||||
path: mariadb
|
||||
version: master
|
||||
memcached:
|
||||
path: memcached
|
||||
version: master
|
||||
mongodb:
|
||||
path: mongodb
|
||||
version: master
|
||||
mysql:
|
||||
path: mysql
|
||||
version: master
|
||||
nginx:
|
||||
path: nginx
|
||||
version: master
|
||||
node:
|
||||
path: node
|
||||
version: master
|
||||
postgres:
|
||||
path: postgres
|
||||
version: master
|
||||
redis:
|
||||
path: redis
|
||||
version: master
|
||||
tomcat:
|
||||
path: tomcat
|
||||
version: master
|
||||
@@ -1,18 +0,0 @@
|
||||
apiVersion: 0.1.0
|
||||
environments:
|
||||
minikube:
|
||||
destination:
|
||||
namespace: default
|
||||
server: https://192.168.99.100:8443
|
||||
k8sVersion: v1.7.0
|
||||
path: minikube
|
||||
kind: ksonnet.io/app
|
||||
name: test-app
|
||||
registries:
|
||||
incubator:
|
||||
gitVersion:
|
||||
commitSha: 422d521c05aa905df949868143b26445f5e4eda5
|
||||
refSpec: master
|
||||
protocol: github
|
||||
uri: github.com/ksonnet/parts/tree/master/incubator
|
||||
version: 0.0.1
|
||||
@@ -1,29 +0,0 @@
|
||||
local env = std.extVar("__ksonnet/environments");
|
||||
local params = std.extVar("__ksonnet/params").components["guestbook-ui"];
|
||||
local k = import "k.libsonnet";
|
||||
local deployment = k.apps.v1beta1.deployment;
|
||||
local container = k.apps.v1beta1.deployment.mixin.spec.template.spec.containersType;
|
||||
local containerPort = container.portsType;
|
||||
local service = k.core.v1.service;
|
||||
local servicePort = k.core.v1.service.mixin.spec.portsType;
|
||||
|
||||
local targetPort = params.containerPort;
|
||||
local labels = {app: params.name};
|
||||
|
||||
local appService = service
|
||||
.new(
|
||||
params.name,
|
||||
labels,
|
||||
servicePort.new(params.servicePort, targetPort))
|
||||
.withType(params.type);
|
||||
|
||||
local appDeployment = deployment
|
||||
.new(
|
||||
params.name,
|
||||
params.replicas,
|
||||
container
|
||||
.new(params.name, params.image)
|
||||
.withPorts(containerPort.new(targetPort)) + if params.command != null then { command: [ params.command ] } else {},
|
||||
labels).withProgressDeadlineSeconds(5);
|
||||
|
||||
k.core.v1.list.new([appService, appDeployment])
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user