Compare commits
283 Commits
v0.2.0
...
release-0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20693fd44c | ||
|
|
37bb65b0ac | ||
|
|
da7be2e3ca | ||
|
|
d138c10eb6 | ||
|
|
4af13eba60 | ||
|
|
e998e499db | ||
|
|
53cdced69b | ||
|
|
e726da46a5 | ||
|
|
b0d6a7092e | ||
|
|
1fe870c0d7 | ||
|
|
469cf1d164 | ||
|
|
00299707e5 | ||
|
|
9f5a718323 | ||
|
|
231d86e249 | ||
|
|
5fb8b3f73c | ||
|
|
6fc345f555 | ||
|
|
c9d5f2ec9e | ||
|
|
b0a71612b7 | ||
|
|
88ff4b28b9 | ||
|
|
6905029c17 | ||
|
|
5d75dc02b1 | ||
|
|
0e78172665 | ||
|
|
3ad036aacb | ||
|
|
a364f8ab49 | ||
|
|
7b6b945cbf | ||
|
|
6a7df88cf4 | ||
|
|
a7c7523a8c | ||
|
|
36589f75f4 | ||
|
|
b3af671803 | ||
|
|
5dde0f6bd8 | ||
|
|
2343818ab5 | ||
|
|
11bb1e3e56 | ||
|
|
3dbb6f3002 | ||
|
|
0591f2bcc5 | ||
|
|
a48151f07a | ||
|
|
82fda1c7af | ||
|
|
d108129972 | ||
|
|
6124ab1b3e | ||
|
|
e294a315fc | ||
|
|
3baed5295e | ||
|
|
a5334ecde7 | ||
|
|
9f35bad93f | ||
|
|
972f639051 | ||
|
|
99cc4f8d39 | ||
|
|
273f99b293 | ||
|
|
48ef2e919e | ||
|
|
65b1b083ee | ||
|
|
c0367ed595 | ||
|
|
062b13e92a | ||
|
|
39b9f4d31a | ||
|
|
39701d0455 | ||
|
|
6fbf78ef52 | ||
|
|
7e0cd01758 | ||
|
|
76bf77eded | ||
|
|
97189f300e | ||
|
|
078b2339bd | ||
|
|
4915490cbb | ||
|
|
f65859bc25 | ||
|
|
543ae3cf13 | ||
|
|
610a4510cf | ||
|
|
7b92977889 | ||
|
|
a17806c37c | ||
|
|
d6b87b2047 | ||
|
|
9d921f65f3 | ||
|
|
32ba7f468c | ||
|
|
9cddd4c368 | ||
|
|
8db465c699 | ||
|
|
eb1caf2231 | ||
|
|
adb84f3d03 | ||
|
|
67acd29541 | ||
|
|
41976122d5 | ||
|
|
d5b973c15a | ||
|
|
3a6892a011 | ||
|
|
b092e1bc7d | ||
|
|
9d7d1989e9 | ||
|
|
7d4dd0fdd5 | ||
|
|
364415f83a | ||
|
|
d633f6b299 | ||
|
|
bf2fe9d33f | ||
|
|
c2fde1ddc0 | ||
|
|
ebb47580a0 | ||
|
|
4e533c90c2 | ||
|
|
9a14134f65 | ||
|
|
e58bdab492 | ||
|
|
279b01a180 | ||
|
|
f046884ae0 | ||
|
|
f4ce59650d | ||
|
|
bc49af56c0 | ||
|
|
3e7116c427 | ||
|
|
12b3ec6989 | ||
|
|
e536cc183a | ||
|
|
63348fa903 | ||
|
|
ab00aef75e | ||
|
|
653f9d3913 | ||
|
|
21c3fb905b | ||
|
|
4353f736d4 | ||
|
|
f3712313ba | ||
|
|
b2d8fcb36e | ||
|
|
81021839d5 | ||
|
|
7c36dd6c56 | ||
|
|
687e0b0dea | ||
|
|
834e22d7b1 | ||
|
|
1e29f98924 | ||
|
|
933f3da538 | ||
|
|
a7fa2fd256 | ||
|
|
0de1a3b20a | ||
|
|
bcc114ec60 | ||
|
|
1148fae419 | ||
|
|
d7188c29f8 | ||
|
|
4b97732659 | ||
|
|
8ff98cc6e1 | ||
|
|
cf0c324a74 | ||
|
|
69119a21cd | ||
|
|
16fa41d25b | ||
|
|
4e170c2033 | ||
|
|
3fbbe940a1 | ||
|
|
271b57e5c5 | ||
|
|
df0e2e4015 | ||
|
|
9fa622d63b | ||
|
|
fed2149174 | ||
|
|
aa4291183b | ||
|
|
0d3fc9648f | ||
|
|
339138b576 | ||
|
|
666769f9d9 | ||
|
|
8fc594bd2b | ||
|
|
8cf8ad7e24 | ||
|
|
0818f698e6 | ||
|
|
44a33b0a5f | ||
|
|
85078bdb66 | ||
|
|
30a3dba7ad | ||
|
|
0afc671723 | ||
|
|
12e7447e9f | ||
|
|
b675e79b89 | ||
|
|
febdccfb58 | ||
|
|
54835a0d93 | ||
|
|
423fe3487c | ||
|
|
98cb3f7950 | ||
|
|
371492bf5c | ||
|
|
7df831e96d | ||
|
|
f0be1bd251 | ||
|
|
948341a885 | ||
|
|
1b2bf8ce0e | ||
|
|
4f68a0f634 | ||
|
|
e785abeb8f | ||
|
|
3acca5095e | ||
|
|
5a62286127 | ||
|
|
5452aff0be | ||
|
|
0f4f1262af | ||
|
|
4e7f68ccba | ||
|
|
96c05babe0 | ||
|
|
6b78cddb19 | ||
|
|
12596ff936 | ||
|
|
a240f1b2b9 | ||
|
|
f6da19672e | ||
|
|
e81d30be9b | ||
|
|
13b090e3bd | ||
|
|
d0479e6ddc | ||
|
|
1432827006 | ||
|
|
89bf4eac71 | ||
|
|
07aac0bdae | ||
|
|
61220b8d0d | ||
|
|
4e470aaf09 | ||
|
|
76922b620b | ||
|
|
ac0f623eda | ||
|
|
afd5450882 | ||
|
|
c17266fc21 | ||
|
|
f62c825495 | ||
|
|
45f44dd4be | ||
|
|
9c0daebfe0 | ||
|
|
f2a0ca5609 | ||
|
|
e04d315853 | ||
|
|
daec697658 | ||
|
|
7ad5670710 | ||
|
|
8036423373 | ||
|
|
8039228a9d | ||
|
|
b1103af429 | ||
|
|
d67ad5acfd | ||
|
|
c9df9c17b7 | ||
|
|
8fa46b02b0 | ||
|
|
92c481330d | ||
|
|
2664db3e40 | ||
|
|
6b554e5f4e | ||
|
|
2bc0dff135 | ||
|
|
c61795f71a | ||
|
|
a8a7491bf0 | ||
|
|
d1c7c4fcaf | ||
|
|
3dbbcf8918 | ||
|
|
0cfd1ad05f | ||
|
|
bfa4e233b7 | ||
|
|
dc662da3d6 | ||
|
|
4107d2422b | ||
|
|
b83eac5dc2 | ||
|
|
bb51837c56 | ||
|
|
5bbb4fe1a1 | ||
|
|
b5c20e9b46 | ||
|
|
1e1ab636e0 | ||
|
|
7348553897 | ||
|
|
5f65a5128a | ||
|
|
d9c12e7271 | ||
|
|
fb2d6b4aff | ||
|
|
3f4ec0ab22 | ||
|
|
ac938fe8a3 | ||
|
|
dc1e8796fb | ||
|
|
8c5d59c60c | ||
|
|
13558b7ce8 | ||
|
|
3b2b3dacf5 | ||
|
|
1b2f89995c | ||
|
|
0479fcdf82 | ||
|
|
a04465466d | ||
|
|
670921df90 | ||
|
|
18f7e17d7a | ||
|
|
a2aede0441 | ||
|
|
1d876c7729 | ||
|
|
70465a0520 | ||
|
|
3c9845719f | ||
|
|
a36cc8946c | ||
|
|
9567b539d1 | ||
|
|
fdf9515de2 | ||
|
|
b320238487 | ||
|
|
7872a60499 | ||
|
|
5fea3846d1 | ||
|
|
86a4e0baaa | ||
|
|
917f1df250 | ||
|
|
11260f2476 | ||
|
|
38d20d0f04 | ||
|
|
938f40e817 | ||
|
|
5f9c8b862e | ||
|
|
d96d67bb9a | ||
|
|
19c3b87676 | ||
|
|
7d08ab4e2b | ||
|
|
efea09d216 | ||
|
|
2adaef547b | ||
|
|
d71bbf0d9a | ||
|
|
36b3b2b853 | ||
|
|
63dafa08cc | ||
|
|
afddbbe875 | ||
|
|
34811cafca | ||
|
|
8a2851169c | ||
|
|
1a85a2d805 | ||
|
|
cf2d00e1e0 | ||
|
|
266c948add | ||
|
|
dd564ee9dd | ||
|
|
b9d48cabb9 | ||
|
|
276e0674c3 | ||
|
|
f3c4a69327 | ||
|
|
1c60a69866 | ||
|
|
050f937a24 | ||
|
|
b24e478224 | ||
|
|
e34380ed76 | ||
|
|
338a1b826f | ||
|
|
b87c63c897 | ||
|
|
f6ed150bb7 | ||
|
|
d5c683bc76 | ||
|
|
3ac95f3f84 | ||
|
|
3be872ad32 | ||
|
|
80964a79b2 | ||
|
|
e719035ea5 | ||
|
|
f2bcf63b26 | ||
|
|
2c9843f1a0 | ||
|
|
0560406d81 | ||
|
|
db8083c657 | ||
|
|
fcc9f50b3f | ||
|
|
c1ffbad8d8 | ||
|
|
d7cdb1a5af | ||
|
|
6c41ce5e08 | ||
|
|
685a814f38 | ||
|
|
06b64047a4 | ||
|
|
8a90b32446 | ||
|
|
7e47b1ebae | ||
|
|
150b51a3ac | ||
|
|
0002f8db9e | ||
|
|
59ed50d230 | ||
|
|
820b4bac1a | ||
|
|
19c5ecdbfa | ||
|
|
66b0702c24 | ||
|
|
5b5dc0efc4 | ||
|
|
0febf05160 | ||
|
|
ee924bda6e | ||
|
|
ecfe571e75 | ||
|
|
173ecd9397 | ||
|
|
ba3db35ba0 | ||
|
|
074053dac7 | ||
|
|
6bc98f91b1 |
@@ -21,9 +21,7 @@ spec:
|
||||
- name: cmd
|
||||
value: "{{item}}"
|
||||
withItems:
|
||||
- make controller-image
|
||||
- make server-image
|
||||
- make repo-server-image
|
||||
- make controller-image server-image repo-server-image
|
||||
- name: test
|
||||
template: ci-builder
|
||||
arguments:
|
||||
@@ -31,8 +29,13 @@ spec:
|
||||
- name: cmd
|
||||
value: "{{item}}"
|
||||
withItems:
|
||||
- dep ensure && make lint
|
||||
- dep ensure && make test
|
||||
- dep ensure && make cli lint test
|
||||
- name: test-e2e
|
||||
template: ci-builder
|
||||
arguments:
|
||||
parameters:
|
||||
- name: cmd
|
||||
value: "dep ensure && make test-e2e"
|
||||
|
||||
- name: ci-builder
|
||||
inputs:
|
||||
@@ -49,6 +52,10 @@ spec:
|
||||
command: [sh, -c]
|
||||
args: ["{{inputs.parameters.cmd}}"]
|
||||
workingDir: /go/src/github.com/argoproj/argo-cd
|
||||
resources:
|
||||
requests:
|
||||
memory: 1024Mi
|
||||
cpu: 200m
|
||||
|
||||
- name: ci-dind
|
||||
inputs:
|
||||
@@ -68,6 +75,10 @@ spec:
|
||||
env:
|
||||
- name: DOCKER_HOST
|
||||
value: 127.0.0.1
|
||||
resources:
|
||||
requests:
|
||||
memory: 1024Mi
|
||||
cpu: 200m
|
||||
sidecars:
|
||||
- name: dind
|
||||
image: docker:17.10-dind
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
# Prevent vendor directory from being copied to ensure we are not not pulling unexpected cruft from
|
||||
# a user's workspace, and are only building off of what is locked by dep.
|
||||
vendor
|
||||
Dockerfile-argocd
|
||||
dist
|
||||
|
||||
161
CHANGELOG.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# Changelog
|
||||
|
||||
## v0.7.2 (2018-08-21)
|
||||
- API discovery becomes best effort when partial resource list is returned (issue #524)
|
||||
|
||||
## v0.7.1 (2018-08-03)
|
||||
+ Surface helm parameters to the application level (#485)
|
||||
+ [UI] Improve application creation wizard (#459)
|
||||
+ [UI] Show indicator when refresh is still in progress (#493)
|
||||
* [UI] Improve data loading error notification (#446)
|
||||
* Infer username from claims during an `argocd relogin` (#475)
|
||||
* Expand RBAC role to be able to create application events. Fix username claims extraction
|
||||
- Fix scalability issues with the ListApps API (#494)
|
||||
- Fix issue where application server was retrieving events from incorrect cluster (#478)
|
||||
- Fix failure in identifying app source type when path was '.'
|
||||
- AppProjectSpec SourceRepos mislabeled (#490)
|
||||
- Failed e2e test was not failing CI workflow
|
||||
* Fix linux download link in getting_started.md (#487) (@chocopowwwa)
|
||||
|
||||
## v0.7.0 (2018-07-27)
|
||||
+ Support helm charts and yaml directories as an application source
|
||||
+ Audit trails in the form of API call logs
|
||||
+ Generate kubernetes events for application state changes
|
||||
+ Add ksonnet version to version endpoint (#433)
|
||||
+ Show CLI progress for sync and rollback
|
||||
+ Make use of dex refresh tokens and store them into local config
|
||||
+ Expire local superuser tokens when their password changes
|
||||
+ Add `argocd relogin` command as a convenience around login to current context
|
||||
- Fix saving default connection status for repos and clusters
|
||||
- Fix undesired fail-fast behavior of health check
|
||||
- Fix memory leak in the cluster resource watch
|
||||
- Health check for StatefulSets, DaemonSet, and ReplicaSets were failing due to use of wrong converters
|
||||
|
||||
## v0.6.2 (2018-07-23)
|
||||
- Health check for StatefulSets, DaemonSet, and ReplicaSets were failing due to use of wrong converters
|
||||
|
||||
## v0.6.1 (2018-07-18)
|
||||
- Fix regression where deployment health check incorrectly reported Healthy
|
||||
+ Intercept dex SSO errors and present them in Argo login page
|
||||
|
||||
## v0.6.0 (2018-07-16)
|
||||
+ Support PreSync, Sync, PostSync resource hooks
|
||||
+ Introduce Application Projects for finer grain RBAC controls
|
||||
+ Swagger Docs & UI
|
||||
+ Support in-cluster deployments internal kubernetes service name
|
||||
+ Refactoring & Improvements
|
||||
* Improved error handling, status and condition reporting
|
||||
* Remove installer in favor of kubectl apply instructions
|
||||
* Add validation when setting application parameters
|
||||
* Cascade deletion is decided during app deletion, instead of app creation
|
||||
- Fix git authentication implementation when using using SSH key
|
||||
- app-name label was inadvertently injected into spec.selector if selector was omitted from v1beta1 specs
|
||||
|
||||
## v0.5.4 (2018-06-27)
|
||||
- Refresh flag to sync should be optional, not required
|
||||
|
||||
## v0.5.3 (2018-06-20)
|
||||
+ Support cluster management using the internal k8s API address https://kubernetes.default.svc (#307)
|
||||
+ Support diffing a local ksonnet app to the live application state (resolves #239) (#298)
|
||||
+ Add ability to show last operation result in app get. Show path in app list -o wide (#297)
|
||||
+ Update dependencies: ksonnet v0.11, golang v1.10, debian v9.4 (#296)
|
||||
+ Add ability to force a refresh of an app during get (resolves #269) (#293)
|
||||
+ Automatically restart API server upon certificate changes (#292)
|
||||
|
||||
## v0.5.2 (2018-06-14)
|
||||
+ Resource events tab on application details page (#286)
|
||||
+ Display pod status on application details page (#231)
|
||||
|
||||
## v0.5.1 (2018-06-13)
|
||||
- API server incorrectly compose application fully qualified name for RBAC check (#283)
|
||||
- UI crash while rendering application operation info if operation failed
|
||||
|
||||
## v0.5.0 (2018-06-12)
|
||||
+ RBAC access control
|
||||
+ Repository/Cluster state monitoring
|
||||
+ ArgoCD settings import/export
|
||||
+ Application creation UI wizard
|
||||
+ argocd app manifests for printing the application manifests
|
||||
+ argocd app unset command to unset parameter overrides
|
||||
+ Fail app sync if prune flag is required (#276)
|
||||
+ Take into account number of unavailable replicas to decided if deployment is healthy or not #270
|
||||
+ Add ability to show parameters and overrides in CLI (resolves #240)
|
||||
- Repo names containing underscores were not being accepted (#258)
|
||||
- Cookie token was not parsed properly when mixed with other site cookies
|
||||
|
||||
## v0.4.7 (2018-06-07)
|
||||
- Fix argocd app wait health checking logic
|
||||
|
||||
## v0.4.6 (2018-06-06)
|
||||
- Retry argocd app wait connection errors from EOF watch. Show detailed state changes
|
||||
|
||||
## v0.4.5 (2018-05-31)
|
||||
+ Add argocd app unset command to unset parameter overrides
|
||||
- Cookie token was not parsed properly when mixed with other site cookies
|
||||
|
||||
## v0.4.4 (2018-05-30)
|
||||
+ Add ability to show parameters and overrides in CLI (resolves #240)
|
||||
+ Add Events API endpoint
|
||||
+ Issue #238 - add upsert flag to 'argocd app create' command
|
||||
+ Add repo browsing endpoint (#229)
|
||||
+ Support subscribing to settings updates and auto-restart of dex and API server
|
||||
- Issue #233 - Controller does not persist rollback operation result
|
||||
- App sync frequently fails due to concurrent app modification
|
||||
|
||||
## v0.4.3 (2018-05-21)
|
||||
- Move local branch deletion as part of git Reset() (resolves #185) (#222)
|
||||
- Fix exit code for app wait (#219)
|
||||
|
||||
## v0.4.2 (2018-05-21)
|
||||
+ Show URL in argocd app get
|
||||
- Remove interactive context name prompt during login which broke login automation
|
||||
* Rename force flag to cascade in argocd app delete
|
||||
|
||||
## v0.4.1 (2018-05-18)
|
||||
+ Implemented argocd app wait command
|
||||
|
||||
## v0.4.0 (2018-05-17)
|
||||
+ SSO Integration
|
||||
+ GitHub Webhook
|
||||
+ Add application health status
|
||||
+ Sync/Rollback/Delete is asynchronously handled by controller
|
||||
* Refactor CRUD operation on clusters and repos
|
||||
* Sync will always perform kubectl apply
|
||||
* Synced Status considers last-applied-configuration annotatoin
|
||||
* Server & namespace are mandatory fields (still inferred from app.yaml)
|
||||
* Manifests are memoized in repo server
|
||||
- Fix connection timeouts to SSH repos
|
||||
|
||||
## v0.3.2 (2018-05-03)
|
||||
+ Application sync should delete 'unexpected' resources #139
|
||||
+ Update ksonnet to v0.10.1
|
||||
+ Detect unexpected resources
|
||||
- Fix: App sync frequently fails due to concurrent app modification #147
|
||||
- Fix: improve app state comparator: #136, #132
|
||||
|
||||
## v0.3.1 (2018-04-24)
|
||||
+ Add new rollback RPC with numeric identifiers
|
||||
+ New argo app history and argo app rollback command
|
||||
+ Switch to gogo/protobuf for golang code generation
|
||||
- Fix: create .argocd directory during argo login (issue #123)
|
||||
- Fix: Allow overriding server or namespace separately (issue #110)
|
||||
|
||||
## v0.3.0 (2018-04-23)
|
||||
+ Auth support
|
||||
+ TLS support
|
||||
+ DAG-based application view
|
||||
+ Bulk watch
|
||||
+ ksonnet v0.10.0-alpha.3
|
||||
+ kubectl apply deployment strategy
|
||||
+ CLI improvements for app management
|
||||
|
||||
## v0.2.0 (2018-04-03)
|
||||
+ Rollback UI
|
||||
+ Override parameters
|
||||
|
||||
## v0.1.0 (2018-03-12)
|
||||
+ Define app in Github with dev and preprod environment using KSonnet
|
||||
+ Add cluster Diff App with a cluster Deploy app in a cluster
|
||||
+ Deploy a new version of the app in the cluster
|
||||
+ App sync based on Github app config change - polling only
|
||||
+ Basic UI: App diff between Git and k8s cluster for all environments Basic GUI
|
||||
@@ -1,10 +1,22 @@
|
||||
## Requirements
|
||||
Make sure you have following tools installed [golang](https://golang.org/), [dep](https://github.com/golang/dep), [protobuf](https://developers.google.com/protocol-buffers/),
|
||||
[kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/).
|
||||
Make sure you have following tools installed
|
||||
* [docker](https://docs.docker.com/install/#supported-platforms)
|
||||
* [golang](https://golang.org/)
|
||||
* [dep](https://github.com/golang/dep)
|
||||
* [protobuf](https://developers.google.com/protocol-buffers/)
|
||||
* [ksonnet](https://github.com/ksonnet/ksonnet#install)
|
||||
* [helm](https://github.com/helm/helm/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/).
|
||||
|
||||
```
|
||||
$ brew install go dep protobuf kubectl
|
||||
$ brew tap go-swagger/go-swagger
|
||||
$ brew install go dep protobuf kubectl ksonnet/tap/ks kubernetes-helm jq go-swagger
|
||||
$ 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
|
||||
```
|
||||
|
||||
Nice to have [gometalinter](https://github.com/alecthomas/gometalinter) and [goreman](https://github.com/mattn/goreman):
|
||||
@@ -20,6 +32,23 @@ $ go get -u github.com/argoproj/argo-cd
|
||||
$ dep ensure
|
||||
$ make
|
||||
```
|
||||
NOTE: The make command can take a while, and we recommend building the specific component you are working on
|
||||
* `make cli` - Make the argocd CLI tool
|
||||
* `make server` - Make the API/repo/controller server
|
||||
* `make codegen` - Builds protobuf and swagger files
|
||||
* `make argocd-util` - Make the administrator's utility, used for certain tasks such as import/export
|
||||
|
||||
## Generating ArgoCD manifests for a specific image repository/tag
|
||||
|
||||
During development, the `update-manifests.sh` script, can be used to conveniently regenerate the
|
||||
ArgoCD installation manifests with a customized image namespace and tag. This enables developers
|
||||
to easily apply manifests which are using the images that they pushed into their personal container
|
||||
repository.
|
||||
|
||||
```
|
||||
$ IMAGE_NAMESPACE=jessesuen IMAGE_TAG=latest ./hack/update-manifests.sh
|
||||
$ kubectl apply -n argocd -f ./manifests/install.yaml
|
||||
```
|
||||
|
||||
## Running locally
|
||||
|
||||
@@ -38,3 +67,10 @@ $ kubectl create -f install/manifests/01_application-crd.yaml
|
||||
```
|
||||
$ goreman start
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
* Ensure argocd is installed: ./dist/argocd install
|
||||
* Ensure you're logged in: ./dist/argocd login --username admin --password <whatever password you set at install> localhost:8080
|
||||
* Ensure that roles are configured: kubectl create -f install/manifests/02c_argocd-rbac-cm.yaml
|
||||
* Ensure minikube is running: minikube stop && minikube start
|
||||
* Ensure Argo CD is aware of minikube: ./dist/argocd cluster add minikube
|
||||
|
||||
13
COPYRIGHT
@@ -1,13 +0,0 @@
|
||||
Copyright 2017-2018 The Argo Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License..
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM debian:9.3 as builder
|
||||
FROM debian:9.4 as builder
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
@@ -10,7 +10,7 @@ RUN apt-get update && apt-get install -y \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# Install go
|
||||
ENV GO_VERSION 1.9.3
|
||||
ENV GO_VERSION 1.10.3
|
||||
ENV GO_ARCH amd64
|
||||
ENV GOPATH /root/go
|
||||
ENV PATH ${GOPATH}/bin:/usr/local/go/bin:${PATH}
|
||||
@@ -25,7 +25,7 @@ RUN cd /usr/local && \
|
||||
unzip protoc-*.zip && \
|
||||
wget https://github.com/golang/dep/releases/download/v0.4.1/dep-linux-amd64 -O /usr/local/bin/dep && \
|
||||
chmod +x /usr/local/bin/dep && \
|
||||
wget https://github.com/gobuffalo/packr/releases/download/v1.10.4/packr_1.10.4_linux_amd64.tar.gz && \
|
||||
wget https://github.com/gobuffalo/packr/releases/download/v1.11.0/packr_1.11.0_linux_amd64.tar.gz && \
|
||||
tar -vxf packr*.tar.gz -C /tmp/ && \
|
||||
mv /tmp/packr /usr/local/bin/packr
|
||||
|
||||
@@ -39,22 +39,50 @@ RUN cd ${GOPATH}/src/dummy && \
|
||||
mv vendor/* ${GOPATH}/src/ && \
|
||||
rmdir vendor
|
||||
|
||||
ARG MAKE_TARGET
|
||||
|
||||
# Perform the build
|
||||
WORKDIR /root/go/src/github.com/argoproj/argo-cd
|
||||
COPY . .
|
||||
ARG MAKE_TARGET="cli server controller repo-server argocd-util"
|
||||
RUN make ${MAKE_TARGET}
|
||||
RUN wget https://github.com/ksonnet/ksonnet/releases/download/v0.9.1/ks_0.9.1_linux_amd64.tar.gz && tar -C /tmp/ -xf ks_0.9.1_linux_amd64.tar.gz
|
||||
|
||||
|
||||
##############################################################
|
||||
# This stage will pull in or build any CLI tooling we need for our final image
|
||||
|
||||
FROM golang:1.10 as cli-tooling
|
||||
|
||||
# NOTE: we frequently switch between tip of master ksonnet vs. official builds. Comment/uncomment
|
||||
# the corresponding section to switch between the two options:
|
||||
|
||||
# Option 1: build ksonnet ourselves
|
||||
#RUN go get -v -u github.com/ksonnet/ksonnet && mv ${GOPATH}/bin/ksonnet /ks
|
||||
|
||||
# Option 2: use official tagged ksonnet release
|
||||
env KSONNET_VERSION=0.11.0
|
||||
RUN wget https://github.com/ksonnet/ksonnet/releases/download/v${KSONNET_VERSION}/ks_${KSONNET_VERSION}_linux_amd64.tar.gz && \
|
||||
tar -C /tmp/ -xf ks_${KSONNET_VERSION}_linux_amd64.tar.gz && \
|
||||
mv /tmp/ks_${KSONNET_VERSION}_linux_amd64/ks /ks
|
||||
|
||||
RUN curl -o /kubectl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl && \
|
||||
chmod +x /kubectl
|
||||
|
||||
env HELM_VERSION=2.9.1
|
||||
RUN wget https://storage.googleapis.com/kubernetes-helm/helm-v${HELM_VERSION}-linux-amd64.tar.gz && \
|
||||
tar -C /tmp/ -xf helm-v${HELM_VERSION}-linux-amd64.tar.gz && \
|
||||
mv /tmp/linux-amd64/helm /helm
|
||||
|
||||
##############################################################
|
||||
FROM debian:9.3
|
||||
RUN apt-get update && apt-get install -y git && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
ARG BINARY
|
||||
COPY --from=builder /root/go/src/github.com/argoproj/argo-cd/dist/${BINARY} /${BINARY}
|
||||
COPY --from=builder /tmp/ks_0.9.1_linux_amd64/ks /usr/local/bin/ks
|
||||
|
||||
COPY --from=cli-tooling /ks /usr/local/bin/ks
|
||||
COPY --from=cli-tooling /helm /usr/local/bin/helm
|
||||
COPY --from=cli-tooling /kubectl /usr/local/bin/kubectl
|
||||
# workaround ksonnet issue https://github.com/ksonnet/ksonnet/issues/298
|
||||
ENV USER=root
|
||||
ENV BINARY=$BINARY
|
||||
CMD /$BINARY
|
||||
|
||||
COPY --from=builder /root/go/src/github.com/argoproj/argo-cd/dist/* /
|
||||
ARG BINARY
|
||||
CMD /${BINARY}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.9.2
|
||||
FROM golang:1.10.3
|
||||
|
||||
WORKDIR /tmp
|
||||
|
||||
@@ -10,7 +10,19 @@ RUN curl -O https://get.docker.com/builds/Linux/x86_64/docker-1.13.1.tgz && \
|
||||
go get -u gopkg.in/alecthomas/gometalinter.v2 && \
|
||||
gometalinter.v2 --install
|
||||
|
||||
# Install kubectl
|
||||
RUN curl -o /kubectl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl && \
|
||||
chmod +x /kubectl && mv /kubectl /usr/local/bin/kubectl
|
||||
|
||||
# Install ksonnet
|
||||
# TODO(jessesuen): pin this once we decide on a stable version
|
||||
RUN go get -v -u github.com/ksonnet/ksonnet && \
|
||||
mv ${GOPATH}/bin/ksonnet /usr/local/bin/ks
|
||||
env KSONNET_VERSION=0.11.0
|
||||
RUN wget https://github.com/ksonnet/ksonnet/releases/download/v${KSONNET_VERSION}/ks_${KSONNET_VERSION}_linux_amd64.tar.gz && \
|
||||
tar -C /tmp/ -xf ks_${KSONNET_VERSION}_linux_amd64.tar.gz && \
|
||||
mv /tmp/ks_${KSONNET_VERSION}_linux_amd64/ks /usr/local/bin/ks && \
|
||||
rm -rf /tmp/ks_${KSONNET_VERSION}
|
||||
|
||||
# Install helm
|
||||
env HELM_VERSION=2.9.1
|
||||
RUN wget https://storage.googleapis.com/kubernetes-helm/helm-v${HELM_VERSION}-linux-amd64.tar.gz && \
|
||||
tar -C /tmp/ -xf helm-v${HELM_VERSION}-linux-amd64.tar.gz && \
|
||||
mv /tmp/linux-amd64/helm /usr/local/bin/helm
|
||||
|
||||
818
Gopkg.lock
generated
36
Gopkg.toml
@@ -1,7 +1,11 @@
|
||||
required = [
|
||||
"github.com/gogo/protobuf/protoc-gen-gofast",
|
||||
"github.com/gogo/protobuf/protoc-gen-gogofast",
|
||||
"golang.org/x/sync/errgroup",
|
||||
"k8s.io/code-generator/cmd/go-to-protobuf",
|
||||
"github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway",
|
||||
"github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger",
|
||||
"github.com/golang/protobuf/protoc-gen-go",
|
||||
]
|
||||
|
||||
[[constraint]]
|
||||
@@ -12,24 +16,25 @@ required = [
|
||||
name = "github.com/grpc-ecosystem/grpc-gateway"
|
||||
version = "v1.3.1"
|
||||
|
||||
[[constraint]]
|
||||
branch = "release-1.9"
|
||||
# override argo outdated dependency
|
||||
[[override]]
|
||||
branch = "release-1.10"
|
||||
name = "k8s.io/api"
|
||||
|
||||
[[override]]
|
||||
branch = "release-1.10"
|
||||
name = "k8s.io/apimachinery"
|
||||
|
||||
[[constraint]]
|
||||
branch = "release-1.9"
|
||||
name = "k8s.io/api"
|
||||
|
||||
[[constraint]]
|
||||
name = "k8s.io/apiextensions-apiserver"
|
||||
branch = "release-1.9"
|
||||
branch = "release-1.10"
|
||||
|
||||
[[constraint]]
|
||||
branch = "release-1.9"
|
||||
branch = "release-1.10"
|
||||
name = "k8s.io/code-generator"
|
||||
|
||||
[[constraint]]
|
||||
branch = "release-6.0"
|
||||
[[override]]
|
||||
branch = "release-7.0"
|
||||
name = "k8s.io/client-go"
|
||||
|
||||
[[constraint]]
|
||||
@@ -38,8 +43,13 @@ required = [
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/ksonnet/ksonnet"
|
||||
branch = "master"
|
||||
version = "v0.11.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/pborman/uuid"
|
||||
version = "1.1.0"
|
||||
name = "github.com/gobuffalo/packr"
|
||||
version = "v1.11.0"
|
||||
|
||||
# override ksonnet's logrus dependency
|
||||
[[override]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
revision = "ea8897e79973357ba785ac2533559a6297e83c44"
|
||||
|
||||
2
LICENSE
@@ -187,7 +187,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
Copyright 2017-2018 The Argo Authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
67
Makefile
@@ -1,6 +1,7 @@
|
||||
PACKAGE=github.com/argoproj/argo-cd
|
||||
CURRENT_DIR=$(shell pwd)
|
||||
DIST_DIR=${CURRENT_DIR}/dist
|
||||
CLI_NAME=argocd
|
||||
|
||||
VERSION=$(shell cat ${CURRENT_DIR}/VERSION)
|
||||
BUILD_DATE=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
@@ -40,7 +41,7 @@ IMAGE_PREFIX=${IMAGE_NAMESPACE}/
|
||||
endif
|
||||
|
||||
.PHONY: all
|
||||
all: cli server-image controller-image repo-server-image
|
||||
all: cli server-image controller-image repo-server-image argocd-util
|
||||
|
||||
.PHONY: protogen
|
||||
protogen:
|
||||
@@ -56,16 +57,39 @@ codegen: protogen clientgen
|
||||
# NOTE: we use packr to do the build instead of go, since we embed .yaml files into the go binary.
|
||||
# This enables ease of maintenance of the yaml files.
|
||||
.PHONY: cli
|
||||
cli:
|
||||
CGO_ENABLED=0 ${PACKR_CMD} build -v -i -ldflags '${LDFLAGS} -extldflags "-static"' -o ${DIST_DIR}/argocd ./cmd/argocd
|
||||
cli: clean-debug
|
||||
${PACKR_CMD} build -v -i -ldflags '${LDFLAGS} -extldflags "-static"' -o ${DIST_DIR}/${CLI_NAME} ./cmd/argocd
|
||||
|
||||
.PHONY: cli-linux
|
||||
cli-linux: clean-debug
|
||||
docker build --iidfile /tmp/argocd-linux-id --target builder --build-arg MAKE_TARGET="cli IMAGE_TAG=$(IMAGE_TAG) IMAGE_NAMESPACE=$(IMAGE_NAMESPACE) CLI_NAME=argocd-linux-amd64" -f Dockerfile-argocd .
|
||||
docker create --name tmp-argocd-linux `cat /tmp/argocd-linux-id`
|
||||
docker cp tmp-argocd-linux:/root/go/src/github.com/argoproj/argo-cd/dist/argocd-linux-amd64 dist/
|
||||
docker rm tmp-argocd-linux
|
||||
|
||||
.PHONY: cli-darwin
|
||||
cli-darwin: clean-debug
|
||||
docker build --iidfile /tmp/argocd-darwin-id --target builder --build-arg MAKE_TARGET="cli GOOS=darwin IMAGE_TAG=$(IMAGE_TAG) IMAGE_NAMESPACE=$(IMAGE_NAMESPACE) CLI_NAME=argocd-darwin-amd64" -f Dockerfile-argocd .
|
||||
docker create --name tmp-argocd-darwin `cat /tmp/argocd-darwin-id`
|
||||
docker cp tmp-argocd-darwin:/root/go/src/github.com/argoproj/argo-cd/dist/argocd-darwin-amd64 dist/
|
||||
docker rm tmp-argocd-darwin
|
||||
|
||||
.PHONY: argocd-util
|
||||
argocd-util: clean-debug
|
||||
CGO_ENABLED=0 go build -v -i -ldflags '${LDFLAGS} -extldflags "-static"' -o ${DIST_DIR}/argocd-util ./cmd/argocd-util
|
||||
|
||||
.PHONY: install-manifest
|
||||
install-manifest:
|
||||
if [ "${IMAGE_NAMESPACE}" = "" ] ; then echo "IMAGE_NAMESPACE must be set to build install manifest" ; exit 1 ; fi
|
||||
./hack/update-manifests.sh
|
||||
|
||||
.PHONY: server
|
||||
server:
|
||||
CGO_ENABLED=0 go build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-server ./cmd/argocd-server
|
||||
|
||||
server: clean-debug
|
||||
CGO_ENABLED=0 ${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-server ./cmd/argocd-server
|
||||
|
||||
.PHONY: server-image
|
||||
server-image:
|
||||
docker build --build-arg BINARY=argocd-server --build-arg MAKE_TARGET=server -t $(IMAGE_PREFIX)argocd-server:$(IMAGE_TAG) -f Dockerfile-argocd .
|
||||
docker build --build-arg BINARY=argocd-server -t $(IMAGE_PREFIX)argocd-server:$(IMAGE_TAG) -f Dockerfile-argocd .
|
||||
@if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argocd-server:$(IMAGE_TAG) ; fi
|
||||
|
||||
.PHONY: repo-server
|
||||
@@ -74,7 +98,7 @@ repo-server:
|
||||
|
||||
.PHONY: repo-server-image
|
||||
repo-server-image:
|
||||
docker build --build-arg BINARY=argocd-repo-server --build-arg MAKE_TARGET=repo-server -t $(IMAGE_PREFIX)argocd-repo-server:$(IMAGE_TAG) -f Dockerfile-argocd .
|
||||
docker build --build-arg BINARY=argocd-repo-server -t $(IMAGE_PREFIX)argocd-repo-server:$(IMAGE_TAG) -f Dockerfile-argocd .
|
||||
@if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argocd-repo-server:$(IMAGE_TAG) ; fi
|
||||
|
||||
.PHONY: controller
|
||||
@@ -83,9 +107,14 @@ controller:
|
||||
|
||||
.PHONY: controller-image
|
||||
controller-image:
|
||||
docker build --build-arg BINARY=argocd-application-controller --build-arg MAKE_TARGET=controller -t $(IMAGE_PREFIX)argocd-application-controller:$(IMAGE_TAG) -f Dockerfile-argocd .
|
||||
docker build --build-arg BINARY=argocd-application-controller -t $(IMAGE_PREFIX)argocd-application-controller:$(IMAGE_TAG) -f Dockerfile-argocd .
|
||||
@if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argocd-application-controller:$(IMAGE_TAG) ; fi
|
||||
|
||||
.PHONY: cli-image
|
||||
cli-image:
|
||||
docker build --build-arg BINARY=argocd -t $(IMAGE_PREFIX)argocd-cli:$(IMAGE_TAG) -f Dockerfile-argocd .
|
||||
@if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argocd-cli:$(IMAGE_TAG) ; fi
|
||||
|
||||
.PHONY: builder-image
|
||||
builder-image:
|
||||
docker build -t $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG) -f Dockerfile-ci-builder .
|
||||
@@ -96,12 +125,28 @@ lint:
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test ./...
|
||||
go test -v `go list ./... | grep -v "github.com/argoproj/argo-cd/test/e2e"`
|
||||
|
||||
.PHONY: test-e2e
|
||||
test-e2e:
|
||||
go test -v -failfast -timeout 20m ./test/e2e
|
||||
|
||||
# Cleans VSCode debug.test files from sub-dirs to prevent them from being included in packr boxes
|
||||
.PHONY: clean-debug
|
||||
clean-debug:
|
||||
-find ${CURRENT_DIR} -name debug.test | xargs rm -f
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
clean: clean-debug
|
||||
-rm -rf ${CURRENT_DIR}/dist
|
||||
|
||||
.PHONY: precheckin
|
||||
precheckin: test lint
|
||||
|
||||
.PHONY: release-precheck
|
||||
release-precheck: install-manifest
|
||||
@if [ "$(GIT_TREE_STATE)" != "clean" ]; then echo 'git tree state is $(GIT_TREE_STATE)' ; exit 1; fi
|
||||
@if [ -z "$(GIT_TAG)" ]; then echo 'commit must be tagged to perform release' ; exit 1; fi
|
||||
|
||||
.PHONY: release
|
||||
release: release-precheck precheckin cli-darwin cli-linux server-image controller-image repo-server-image cli-image
|
||||
|
||||
8
OWNERS
Normal file
@@ -0,0 +1,8 @@
|
||||
owners:
|
||||
- alexmt
|
||||
- jessesuen
|
||||
|
||||
approvers:
|
||||
- alexmt
|
||||
- jessesuen
|
||||
- merenbach
|
||||
7
Procfile
@@ -1,3 +1,4 @@
|
||||
controller: go run ./cmd/argocd-application-controller/main.go --app-resync 10
|
||||
api-server: go run ./cmd/argocd-server/main.go
|
||||
repo-server: go run ./cmd/argocd-repo-server/main.go
|
||||
controller: go run ./cmd/argocd-application-controller/main.go
|
||||
api-server: go run ./cmd/argocd-server/main.go --insecure --disable-auth
|
||||
repo-server: go run ./cmd/argocd-repo-server/main.go --loglevel debug
|
||||
dex: sh -c "go run ./cmd/argocd-util/main.go gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p 5556:5556 -p 5557:5557 -v `pwd`/dist/dex.yaml:/dex.yaml quay.io/coreos/dex:v2.10.0 serve /dex.yaml"
|
||||
|
||||
88
README.md
@@ -1,9 +1,11 @@
|
||||
|
||||
# Argo CD - GitOps Continuous Delivery for Kubernetes
|
||||
# Argo CD - Declarative Continuous Delivery for Kubernetes
|
||||
|
||||
## What is Argo CD?
|
||||
|
||||
Argo CD is a declarative, continuous delivery service based on ksonnet for Kubernetes.
|
||||
Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes.
|
||||
|
||||

|
||||
|
||||
## Why Argo CD?
|
||||
|
||||
@@ -12,76 +14,56 @@ Application deployment and lifecycle management should be automated, auditable,
|
||||
|
||||
## Getting Started
|
||||
|
||||
Follow our [getting started guide](docs/GETTING_STARTED.md).
|
||||
Follow our [getting started guide](docs/getting_started.md). Further [documentation](docs/)
|
||||
is provided for additional features.
|
||||
|
||||
## How it works
|
||||
|
||||
Argo CD uses git repositories as the source of truth for defining the desired application state as
|
||||
well as the target deployment environments. Kubernetes manifests are specified as
|
||||
[ksonnet](https://ksonnet.io) applications. Argo CD automates the deployment of the desired
|
||||
application states in the specified target environments.
|
||||
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:
|
||||
* [ksonnet](https://ksonnet.io) applications
|
||||
* [helm](https://helm.sh) charts
|
||||
* Simple directory of YAML/json manifests
|
||||
|
||||
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.
|
||||
|
||||
## 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 out-of-sync.
|
||||
Argo CD reports & visualizes the differences as well as providing facilities to automatically or
|
||||
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).
|
||||
For additional details, see [architecture overview](docs/architecture.md).
|
||||
|
||||
## Features
|
||||
|
||||
* Automated deployment of applications to specified target environments
|
||||
* Continuous monitoring of deployed applications
|
||||
* Automated or manual syncing of applications to its target state
|
||||
* Web and CLI based visualization of applications and differences between live vs. target state
|
||||
* Automated or manual syncing of applications to its desired state
|
||||
* Web and CLI based visualization of applications and differences between live vs. desired state
|
||||
* Rollback/Roll-anywhere to any application state committed in the git repository
|
||||
|
||||
## What is ksonnet?
|
||||
|
||||
* [Jsonnet](http://jsonnet.org), the basis for ksonnet, is a domain specific configuration language,
|
||||
which provides extreme flexibility for composing and manipulating JSON/YAML specifications.
|
||||
* [Ksonnet](http://ksonnet.io) goes one step further by applying Jsonnet principles to Kubernetes
|
||||
manifests. It provides an opinionated file & directory structure to organize applications into
|
||||
reusable components, parameters, and environments. Environments can be hierarchical, which promotes
|
||||
both re-use and granular customization of application and environment specifications.
|
||||
|
||||
## Why ksonnet?
|
||||
|
||||
Application configuration management is a hard problem and grows rapidly in complexity as you deploy
|
||||
more applications, against more and more environments. Current templating systems, such as Jinja,
|
||||
and Golang templating, are unnatural ways to maintain kubernetes manifests, and are not well suited to
|
||||
capture subtle configuration differences between environments. Its ability to compose and re-use
|
||||
application and environment configurations is also very limited.
|
||||
|
||||
Imagine we have a single guestbook application deployed in following environments:
|
||||
|
||||
| Environment | K8s Version | Application Image | DB Connection String | Environment Vars | Sidecars |
|
||||
|---------------|-------------|------------------------|-----------------------|------------------|---------------|
|
||||
| minikube | 1.10.0 | jesse/guestbook:latest | sql://locahost/db | DEBUG=true | |
|
||||
| dev | 1.9.0 | app/guestbook:latest | sql://dev-test/db | DEBUG=true | |
|
||||
| staging | 1.8.0 | app/guestbook:e3c0263 | sql://staging/db | | istio,dnsmasq |
|
||||
| us-west-1 | 1.8.0 | app/guestbook:abc1234 | sql://prod/db | FOO_FEATURE=true | istio,dnsmasq |
|
||||
| us-west-2 | 1.8.0 | app/guestbook:abc1234 | sql://prod/db | | istio,dnsmasq |
|
||||
| us-east-1 | 1.9.0 | app/guestbook:abc1234 | sql://prod/db | BAR_FEATURE=true | istio,dnsmasq |
|
||||
|
||||
Ksonnet:
|
||||
* Enables composition and re-use of common YAML specifications
|
||||
* Allows overrides, additions, and subtractions of YAML sub-components specific to each environment
|
||||
* Guarantees proper generation of K8s manifests suitable for the corresponding Kubernetes API version
|
||||
* Provides [kubernetes-specific jsonnet libraries](https://github.com/ksonnet/ksonnet-lib) to enable
|
||||
concise definition of kubernetes manifests
|
||||
* Health assessment statuses on all components of the application
|
||||
* SSO Integration (OIDC, OAuth2, LDAP, SAML 2.0, GitLab, Microsoft, LinkedIn)
|
||||
* Webhook Integration (GitHub, BitBucket, GitLab)
|
||||
* PreSync, Sync, PostSync hooks to support complex application rollouts (e.g.blue/green & canary upgrades)
|
||||
* Audit trails for application events and API calls
|
||||
* Parameter overrides for overriding ksonnet/helm parameters in git
|
||||
|
||||
## Development Status
|
||||
* Argo CD is in early development
|
||||
* Argo CD is being used in production to deploy SaaS services at Intuit
|
||||
|
||||
## Roadmap
|
||||
* PreSync, PostSync, OutOfSync hooks
|
||||
* Customized application actions as Argo workflows
|
||||
* Blue/Green & canary upgrades
|
||||
* SSO Integration
|
||||
* GitHub & Docker webhooks
|
||||
* Auto-sync toggle to directly apply git state changes to live state
|
||||
* Service account/access key management for CI pipelines
|
||||
* Support for additional config management tools (Kustomize?)
|
||||
* Revamped UI, and feature parity with CLI
|
||||
* Customizable application actions
|
||||
|
||||
@@ -2,46 +2,61 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
argocd "github.com/argoproj/argo-cd"
|
||||
"github.com/argoproj/argo-cd/controller"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
"github.com/argoproj/argo-cd/server/cluster"
|
||||
apirepository "github.com/argoproj/argo-cd/server/repository"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
// load the gcp plugin (required to authenticate against GKE clusters).
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
||||
// load the oidc plugin (required to authenticate with OpenID Connect).
|
||||
"github.com/argoproj/argo-cd/reposerver"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
|
||||
|
||||
"github.com/argoproj/argo-cd"
|
||||
"github.com/argoproj/argo-cd/controller"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
"github.com/argoproj/argo-cd/reposerver"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
)
|
||||
|
||||
const (
|
||||
// CLIName is the name of the CLI
|
||||
cliName = "argocd-application-controller"
|
||||
// Default time in seconds for application resync period
|
||||
defaultAppResyncPeriod = 600
|
||||
defaultAppResyncPeriod = 180
|
||||
)
|
||||
|
||||
func newCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
appResyncPeriod int64
|
||||
repoServerAddress string
|
||||
clientConfig clientcmd.ClientConfig
|
||||
appResyncPeriod int64
|
||||
repoServerAddress string
|
||||
statusProcessors int
|
||||
operationProcessors int
|
||||
logLevel string
|
||||
glogLevel int
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: cliName,
|
||||
Short: "application-controller is a controller to operate on applications CRD",
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
level, err := log.ParseLevel(logLevel)
|
||||
errors.CheckError(err)
|
||||
log.SetLevel(level)
|
||||
|
||||
// Set the glog level for the k8s go-client
|
||||
_ = flag.CommandLine.Parse([]string{})
|
||||
_ = flag.Lookup("logtostderr").Value.Set("true")
|
||||
_ = flag.Lookup("v").Value.Set(strconv.Itoa(glogLevel))
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
|
||||
@@ -56,25 +71,32 @@ func newCommand() *cobra.Command {
|
||||
Namespace: namespace,
|
||||
InstanceID: "",
|
||||
}
|
||||
db := db.NewDB(namespace, kubeClient)
|
||||
resyncDuration := time.Duration(appResyncPeriod) * time.Second
|
||||
apiRepoServer := apirepository.NewServer(namespace, kubeClient, appClient)
|
||||
clusterService := cluster.NewServer(namespace, kubeClient, appClient)
|
||||
appComparator := controller.NewKsonnetAppComparator(clusterService)
|
||||
repoClientset := reposerver.NewRepositoryServerClientset(repoServerAddress)
|
||||
appStateManager := controller.NewAppStateManager(db, appClient, repoClientset, namespace)
|
||||
|
||||
appController := controller.NewApplicationController(
|
||||
namespace,
|
||||
kubeClient,
|
||||
appClient,
|
||||
reposerver.NewRepositoryServerClientset(repoServerAddress),
|
||||
apiRepoServer,
|
||||
appComparator,
|
||||
repoClientset,
|
||||
db,
|
||||
appStateManager,
|
||||
resyncDuration,
|
||||
&controllerConfig)
|
||||
secretController := controller.NewSecretController(kubeClient, repoClientset, resyncDuration, namespace)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
log.Infof("Application Controller (version: %s) starting (namespace: %s)", argocd.GetVersion(), namespace)
|
||||
go appController.Run(ctx, 1)
|
||||
stats.RegisterStackDumper()
|
||||
stats.StartStatsTicker(10 * time.Minute)
|
||||
stats.RegisterHeapDumper("memprofile")
|
||||
|
||||
go secretController.Run(ctx)
|
||||
go appController.Run(ctx, statusProcessors, operationProcessors)
|
||||
// Wait forever
|
||||
select {}
|
||||
},
|
||||
@@ -83,6 +105,10 @@ 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", "localhost:8081", "Repo server address.")
|
||||
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")
|
||||
command.Flags().IntVar(&glogLevel, "gloglevel", 0, "Set the glog logging level")
|
||||
return &command
|
||||
}
|
||||
|
||||
|
||||
@@ -4,21 +4,19 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
// load the gcp plugin (required to authenticate against GKE clusters).
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
||||
// load the oidc plugin (required to authenticate with OpenID Connect).
|
||||
"github.com/argoproj/argo-cd"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/reposerver"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
|
||||
"github.com/argoproj/argo-cd/reposerver/repository"
|
||||
"github.com/argoproj/argo-cd/util/cache"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
"github.com/argoproj/argo-cd/util/ksonnet"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -29,8 +27,7 @@ const (
|
||||
|
||||
func newCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
logLevel string
|
||||
logLevel string
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: cliName,
|
||||
@@ -40,33 +37,39 @@ func newCommand() *cobra.Command {
|
||||
errors.CheckError(err)
|
||||
log.SetLevel(level)
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
|
||||
kubeClientset := kubernetes.NewForConfigOrDie(config)
|
||||
|
||||
server := reposerver.NewServer(kubeClientset, namespace)
|
||||
nativeGitClient, err := git.NewNativeGitClient()
|
||||
errors.CheckError(err)
|
||||
grpc := server.CreateGRPC(nativeGitClient)
|
||||
server := reposerver.NewServer(git.NewFactory(), newCache())
|
||||
grpc := server.CreateGRPC()
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
errors.CheckError(err)
|
||||
|
||||
log.Infof("argocd-repo-server %s serving on %s (namespace: %s)", argocd.GetVersion(), listener.Addr(), namespace)
|
||||
ksVers, err := ksonnet.KsonnetVersion()
|
||||
errors.CheckError(err)
|
||||
|
||||
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")
|
||||
err = grpc.Serve(listener)
|
||||
errors.CheckError(err)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(&command)
|
||||
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
|
||||
return &command
|
||||
}
|
||||
|
||||
func newCache() cache.Cache {
|
||||
return cache.NewInMemoryCache(repository.DefaultRepoCacheExpiration)
|
||||
// client := redis.NewClient(&redis.Options{
|
||||
// Addr: "localhost:6379",
|
||||
// Password: "",
|
||||
// DB: 0,
|
||||
// })
|
||||
// return cache.NewRedisCache(client, repository.DefaultRepoCacheExpiration)
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := newCommand().Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
|
||||
@@ -1,25 +1,34 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
"github.com/argoproj/argo-cd/reposerver"
|
||||
"github.com/argoproj/argo-cd/server"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
)
|
||||
|
||||
// NewCommand returns a new instance of an argocd command
|
||||
func NewCommand() *cobra.Command {
|
||||
var (
|
||||
insecure bool
|
||||
logLevel string
|
||||
glogLevel int
|
||||
clientConfig clientcmd.ClientConfig
|
||||
staticAssetsDir string
|
||||
repoServerAddress string
|
||||
configMapName string
|
||||
disableAuth bool
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: cliName,
|
||||
@@ -30,6 +39,11 @@ func NewCommand() *cobra.Command {
|
||||
errors.CheckError(err)
|
||||
log.SetLevel(level)
|
||||
|
||||
// Set the glog level for the k8s go-client
|
||||
_ = flag.CommandLine.Parse([]string{})
|
||||
_ = flag.Lookup("logtostderr").Value.Set("true")
|
||||
_ = flag.Lookup("v").Value.Set(strconv.Itoa(glogLevel))
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
|
||||
@@ -40,16 +54,37 @@ func NewCommand() *cobra.Command {
|
||||
appclientset := appclientset.NewForConfigOrDie(config)
|
||||
repoclientset := reposerver.NewRepositoryServerClientset(repoServerAddress)
|
||||
|
||||
argocd := server.NewServer(kubeclientset, appclientset, repoclientset, namespace, staticAssetsDir, configMapName)
|
||||
argocd.Run()
|
||||
argoCDOpts := server.ArgoCDServerOpts{
|
||||
Insecure: insecure,
|
||||
Namespace: namespace,
|
||||
StaticAssetsDir: staticAssetsDir,
|
||||
KubeClientset: kubeclientset,
|
||||
AppClientset: appclientset,
|
||||
RepoClientset: repoclientset,
|
||||
DisableAuth: disableAuth,
|
||||
}
|
||||
|
||||
stats.RegisterStackDumper()
|
||||
stats.StartStatsTicker(10 * time.Minute)
|
||||
stats.RegisterHeapDumper("memprofile")
|
||||
|
||||
for {
|
||||
argocd := server.NewServer(argoCDOpts)
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
argocd.Run(ctx, 8080)
|
||||
cancel()
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(command)
|
||||
command.Flags().BoolVar(&insecure, "insecure", false, "Run server without TLS")
|
||||
command.Flags().StringVar(&staticAssetsDir, "staticassets", "", "Static assets directory path")
|
||||
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
|
||||
command.Flags().IntVar(&glogLevel, "gloglevel", 0, "Set the glog logging level")
|
||||
command.Flags().StringVar(&repoServerAddress, "repo-server", "localhost:8081", "Repo server address.")
|
||||
command.Flags().StringVar(&configMapName, "config-map", "", "Name of a Kubernetes config map to use.")
|
||||
command.Flags().BoolVar(&disableAuth, "disable-auth", false, "Disable client authentication")
|
||||
command.AddCommand(cli.NewVersionCmd(cliName))
|
||||
return command
|
||||
}
|
||||
|
||||
384
cmd/argocd-util/main.go
Normal file
@@ -0,0 +1,384 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"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"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
"github.com/ghodss/yaml"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
// load the gcp plugin (required to authenticate against GKE clusters).
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
||||
// load the oidc plugin (required to authenticate with OpenID Connect).
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
|
||||
)
|
||||
|
||||
const (
|
||||
// CLIName is the name of the CLI
|
||||
cliName = "argocd-util"
|
||||
|
||||
// YamlSeparator separates sections of a YAML file
|
||||
yamlSeparator = "\n---\n"
|
||||
)
|
||||
|
||||
// NewCommand returns a new instance of an argocd command
|
||||
func NewCommand() *cobra.Command {
|
||||
var (
|
||||
logLevel string
|
||||
)
|
||||
|
||||
var command = &cobra.Command{
|
||||
Use: cliName,
|
||||
Short: "argocd-util has internal tools used by ArgoCD",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
},
|
||||
}
|
||||
|
||||
command.AddCommand(cli.NewVersionCmd(cliName))
|
||||
command.AddCommand(NewRunDexCommand())
|
||||
command.AddCommand(NewGenDexConfigCommand())
|
||||
command.AddCommand(NewImportCommand())
|
||||
command.AddCommand(NewExportCommand())
|
||||
command.AddCommand(NewSettingsCommand())
|
||||
|
||||
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
|
||||
return command
|
||||
}
|
||||
|
||||
func NewRunDexCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: "rundex",
|
||||
Short: "Runs dex generating a config using settings from the ArgoCD configmap and secret",
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
_, err := exec.LookPath("dex")
|
||||
errors.CheckError(err)
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
kubeClientset := kubernetes.NewForConfigOrDie(config)
|
||||
settingsMgr := settings.NewSettingsManager(kubeClientset, namespace)
|
||||
settings, err := settingsMgr.GetSettings()
|
||||
errors.CheckError(err)
|
||||
ctx := context.Background()
|
||||
settingsMgr.StartNotifier(ctx, settings)
|
||||
updateCh := make(chan struct{}, 1)
|
||||
settingsMgr.Subscribe(updateCh)
|
||||
|
||||
for {
|
||||
var cmd *exec.Cmd
|
||||
dexCfgBytes, err := dex.GenerateDexConfigYAML(settings)
|
||||
errors.CheckError(err)
|
||||
if len(dexCfgBytes) == 0 {
|
||||
log.Infof("dex is not configured")
|
||||
} else {
|
||||
err = ioutil.WriteFile("/tmp/dex.yaml", dexCfgBytes, 0644)
|
||||
errors.CheckError(err)
|
||||
log.Info(string(dexCfgBytes))
|
||||
cmd = exec.Command("dex", "serve", "/tmp/dex.yaml")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
err = cmd.Start()
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
// loop until the dex config changes
|
||||
for {
|
||||
<-updateCh
|
||||
newDexCfgBytes, err := dex.GenerateDexConfigYAML(settings)
|
||||
errors.CheckError(err)
|
||||
if string(newDexCfgBytes) != string(dexCfgBytes) {
|
||||
log.Infof("dex config modified. restarting dex")
|
||||
if cmd != nil && cmd.Process != nil {
|
||||
err = cmd.Process.Signal(syscall.SIGTERM)
|
||||
errors.CheckError(err)
|
||||
_, err = cmd.Process.Wait()
|
||||
errors.CheckError(err)
|
||||
}
|
||||
break
|
||||
} else {
|
||||
log.Infof("dex config unmodified")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(&command)
|
||||
return &command
|
||||
}
|
||||
|
||||
func NewGenDexConfigCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
out string
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: "gendexcfg",
|
||||
Short: "Generates a dex config from ArgoCD settings",
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
kubeClientset := kubernetes.NewForConfigOrDie(config)
|
||||
settingsMgr := settings.NewSettingsManager(kubeClientset, namespace)
|
||||
settings, err := settingsMgr.GetSettings()
|
||||
errors.CheckError(err)
|
||||
dexCfgBytes, err := dex.GenerateDexConfigYAML(settings)
|
||||
errors.CheckError(err)
|
||||
if len(dexCfgBytes) == 0 {
|
||||
log.Infof("dex is not configured")
|
||||
return nil
|
||||
}
|
||||
if out == "" {
|
||||
fmt.Printf(string(dexCfgBytes))
|
||||
} else {
|
||||
err = ioutil.WriteFile(out, dexCfgBytes, 0644)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(&command)
|
||||
command.Flags().StringVarP(&out, "out", "o", "", "Output to the specified file instead of stdout")
|
||||
return &command
|
||||
}
|
||||
|
||||
// NewImportCommand defines a new command for exporting Kubernetes and Argo CD resources.
|
||||
func NewImportCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
)
|
||||
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 {
|
||||
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()
|
||||
errors.CheckError(err)
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
kubeClientset := kubernetes.NewForConfigOrDie(config)
|
||||
|
||||
settingsMgr := settings.NewSettingsManager(kubeClientset, namespace)
|
||||
err = settingsMgr.SaveSettings(newSettings)
|
||||
errors.CheckError(err)
|
||||
db := db.NewDB(namespace, kubeClientset)
|
||||
|
||||
_, err = kubeClientset.CoreV1().ConfigMaps(namespace).Create(newRBACCM)
|
||||
errors.CheckError(err)
|
||||
|
||||
for _, repo := range newRepos {
|
||||
_, err := db.CreateRepository(context.Background(), repo)
|
||||
if err != nil {
|
||||
log.Warn(err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, cluster := range newClusters {
|
||||
_, err := db.CreateCluster(context.Background(), cluster)
|
||||
if err != nil {
|
||||
log.Warn(err)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
return &command
|
||||
}
|
||||
|
||||
// NewExportCommand defines a new command for exporting Kubernetes and Argo CD resources.
|
||||
func NewExportCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
out string
|
||||
)
|
||||
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 {
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
kubeClientset := kubernetes.NewForConfigOrDie(config)
|
||||
|
||||
settingsMgr := settings.NewSettingsManager(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, kubeClientset)
|
||||
clusters, err := db.ListClusters(context.Background())
|
||||
errors.CheckError(err)
|
||||
|
||||
repos, err := db.ListRepositories(context.Background())
|
||||
errors.CheckError(err)
|
||||
|
||||
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, repos.Items, clusters.Items, apps.Items, rbacCM)
|
||||
|
||||
if out == "-" {
|
||||
fmt.Println(output)
|
||||
} else {
|
||||
err = ioutil.WriteFile(out, []byte(output), 0644)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(&command)
|
||||
command.Flags().StringVarP(&out, "out", "o", "-", "Output to the specified file instead of stdout")
|
||||
|
||||
return &command
|
||||
}
|
||||
|
||||
// NewSettingsCommand returns a new instance of `argocd-util settings` command
|
||||
func NewSettingsCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
updateSuperuser bool
|
||||
superuserPassword string
|
||||
updateSignature bool
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "settings",
|
||||
Short: "Creates or updates ArgoCD settings",
|
||||
Long: "Creates or updates ArgoCD settings",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
conf, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
namespace, wasSpecified, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
if !(wasSpecified) {
|
||||
namespace = "argocd"
|
||||
}
|
||||
|
||||
kubeclientset, err := kubernetes.NewForConfig(conf)
|
||||
errors.CheckError(err)
|
||||
settingsMgr := settings.NewSettingsManager(kubeclientset, namespace)
|
||||
|
||||
_ = settings.UpdateSettings(superuserPassword, settingsMgr, updateSignature, updateSuperuser, namespace)
|
||||
},
|
||||
}
|
||||
command.Flags().BoolVar(&updateSuperuser, "update-superuser", false, "force updating the superuser password")
|
||||
command.Flags().StringVar(&superuserPassword, "superuser-password", "", "password for super user")
|
||||
command.Flags().BoolVar(&updateSignature, "update-signature", false, "force updating the server-side token signing signature")
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(command)
|
||||
return command
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := NewCommand().Execute(); err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
72
cmd/argocd/commands/account.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/server/account"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
func NewAccountCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "account",
|
||||
Short: "Manage account settings",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
},
|
||||
}
|
||||
command.AddCommand(NewAccountUpdatePasswordCommand(clientOpts))
|
||||
return command
|
||||
}
|
||||
|
||||
func NewAccountUpdatePasswordCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
currentPassword string
|
||||
newPassword string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "update-password",
|
||||
Short: "Update password",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if currentPassword == "" {
|
||||
fmt.Print("*** Enter current password: ")
|
||||
password, err := terminal.ReadPassword(syscall.Stdin)
|
||||
errors.CheckError(err)
|
||||
currentPassword = string(password)
|
||||
fmt.Print("\n")
|
||||
}
|
||||
if newPassword == "" {
|
||||
newPassword = settings.ReadAndConfirmPassword()
|
||||
}
|
||||
|
||||
updatePasswordRequest := account.UpdatePasswordRequest{
|
||||
NewPassword: newPassword,
|
||||
CurrentPassword: currentPassword,
|
||||
}
|
||||
|
||||
conn, usrIf := argocdclient.NewClientOrDie(clientOpts).NewAccountClientOrDie()
|
||||
defer util.Close(conn)
|
||||
_, err := usrIf.UpdatePassword(context.Background(), &updatePasswordRequest)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Password updated\n")
|
||||
},
|
||||
}
|
||||
|
||||
command.Flags().StringVar(¤tPassword, "current-password", "", "current password you wish to change")
|
||||
command.Flags().StringVar(&newPassword, "new-password", "", "new password you want to update to")
|
||||
return command
|
||||
}
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
func NewClusterCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clientcmd.PathOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "cluster",
|
||||
Short: fmt.Sprintf("%s cluster COMMAND", cliName),
|
||||
Short: "Manage cluster credentials",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
@@ -42,6 +42,10 @@ func NewClusterCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clientc
|
||||
|
||||
// NewClusterAddCommand returns a new instance of an `argocd cluster add` command
|
||||
func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clientcmd.PathOptions) *cobra.Command {
|
||||
var (
|
||||
inCluster bool
|
||||
upsert bool
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "add",
|
||||
Short: fmt.Sprintf("%s cluster add CONTEXT", cliName),
|
||||
@@ -49,7 +53,7 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
var configAccess clientcmd.ConfigAccess = pathOpts
|
||||
if len(args) == 0 {
|
||||
log.Error("Choose a context name from:")
|
||||
printContexts(configAccess)
|
||||
printKubeContexts(configAccess)
|
||||
os.Exit(1)
|
||||
}
|
||||
config, err := configAccess.GetStartingConfig()
|
||||
@@ -65,29 +69,36 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
conf, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
|
||||
if conf.Username == "" || conf.Password == "" {
|
||||
// Install RBAC resources for managing the cluster if username and password are not specified
|
||||
conf.BearerToken = common.InstallClusterManagerRBAC(conf)
|
||||
}
|
||||
// Install RBAC resources for managing the cluster
|
||||
managerBearerToken := common.InstallClusterManagerRBAC(conf)
|
||||
|
||||
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
|
||||
defer util.Close(conn)
|
||||
clst := NewCluster(args[0], conf)
|
||||
clst, err = clusterIf.Create(context.Background(), clst)
|
||||
clst := NewCluster(args[0], conf, managerBearerToken)
|
||||
if inCluster {
|
||||
clst.Server = common.KubernetesInternalAPIServerAddr
|
||||
}
|
||||
clstCreateReq := cluster.ClusterCreateRequest{
|
||||
Cluster: clst,
|
||||
Upsert: upsert,
|
||||
}
|
||||
clst, err = clusterIf.Create(context.Background(), &clstCreateReq)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Cluster '%s' added\n", clst.Name)
|
||||
},
|
||||
}
|
||||
command.PersistentFlags().StringVar(&pathOpts.LoadingRules.ExplicitPath, pathOpts.ExplicitFileFlag, pathOpts.LoadingRules.ExplicitPath, "use a particular kubeconfig file")
|
||||
command.Flags().BoolVar(&inCluster, "in-cluster", false, "Indicates ArgoCD resides inside this cluster and should connect using the internal k8s hostname (kubernetes.default.svc)")
|
||||
command.Flags().BoolVar(&upsert, "upsert", false, "Override an existing cluster with the same name even if the spec differs")
|
||||
return command
|
||||
}
|
||||
|
||||
func printContexts(ca clientcmd.ConfigAccess) {
|
||||
func printKubeContexts(ca clientcmd.ConfigAccess) {
|
||||
config, err := ca.GetStartingConfig()
|
||||
errors.CheckError(err)
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
defer func() { _ = w.Flush() }()
|
||||
columnNames := []string{"CURRENT", "NAME", "CLUSTER", "AUTHINFO", "NAMESPACE"}
|
||||
columnNames := []string{"CURRENT", "NAME", "CLUSTER", "SERVER"}
|
||||
_, err = fmt.Fprintf(w, "%s\n", strings.Join(columnNames, "\t"))
|
||||
errors.CheckError(err)
|
||||
|
||||
@@ -98,18 +109,30 @@ func printContexts(ca clientcmd.ConfigAccess) {
|
||||
}
|
||||
sort.Strings(contextNames)
|
||||
|
||||
if config.Clusters == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, name := range contextNames {
|
||||
// ignore malformed kube config entries
|
||||
context := config.Contexts[name]
|
||||
if context == nil {
|
||||
continue
|
||||
}
|
||||
cluster := config.Clusters[context.Cluster]
|
||||
if cluster == nil {
|
||||
continue
|
||||
}
|
||||
prefix := " "
|
||||
if config.CurrentContext == name {
|
||||
prefix = "*"
|
||||
}
|
||||
_, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", prefix, name, context.Cluster, context.AuthInfo, context.Namespace)
|
||||
_, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", prefix, name, context.Cluster, cluster.Server)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
}
|
||||
|
||||
func NewCluster(name string, conf *rest.Config) *argoappv1.Cluster {
|
||||
func NewCluster(name string, conf *rest.Config, managerBearerToken string) *argoappv1.Cluster {
|
||||
tlsClientConfig := argoappv1.TLSClientConfig{
|
||||
Insecure: conf.TLSClientConfig.Insecure,
|
||||
ServerName: conf.TLSClientConfig.ServerName,
|
||||
@@ -136,9 +159,7 @@ func NewCluster(name string, conf *rest.Config) *argoappv1.Cluster {
|
||||
Server: conf.Host,
|
||||
Name: name,
|
||||
Config: argoappv1.ClusterConfig{
|
||||
Username: conf.Username,
|
||||
Password: conf.Password,
|
||||
BearerToken: conf.BearerToken,
|
||||
BearerToken: managerBearerToken,
|
||||
TLSClientConfig: tlsClientConfig,
|
||||
},
|
||||
}
|
||||
@@ -149,7 +170,7 @@ func NewCluster(name string, conf *rest.Config) *argoappv1.Cluster {
|
||||
func NewClusterGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "get",
|
||||
Short: fmt.Sprintf("%s cluster get SERVER", cliName),
|
||||
Short: "Get cluster information",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
@@ -173,7 +194,7 @@ func NewClusterGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
func NewClusterRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "rm",
|
||||
Short: fmt.Sprintf("%s cluster rm SERVER", cliName),
|
||||
Short: "Remove cluster credentials",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
@@ -196,16 +217,16 @@ func NewClusterRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
func NewClusterListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: fmt.Sprintf("%s cluster list", cliName),
|
||||
Short: "List configured clusters",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
|
||||
defer util.Close(conn)
|
||||
clusters, err := clusterIf.List(context.Background(), &cluster.ClusterQuery{})
|
||||
errors.CheckError(err)
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "SERVER\tNAME\n")
|
||||
fmt.Fprintf(w, "SERVER\tNAME\tSTATUS\tMESSAGE\n")
|
||||
for _, c := range clusters.Items {
|
||||
fmt.Fprintf(w, "%s\t%s\n", c.Server, c.Name)
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", c.Server, c.Name, c.ConnectionState.Status, c.ConnectionState.Message)
|
||||
}
|
||||
_ = w.Flush()
|
||||
},
|
||||
|
||||
84
cmd/argocd/commands/context.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewContextCommand returns a new instance of an `argocd ctx` command
|
||||
func NewContextCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "context",
|
||||
Aliases: []string{"ctx"},
|
||||
Short: "Switch between contexts",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
printArgoCDContexts(clientOpts.ConfigPath)
|
||||
return
|
||||
}
|
||||
ctxName := args[0]
|
||||
argoCDDir, err := localconfig.DefaultConfigDir()
|
||||
errors.CheckError(err)
|
||||
prevCtxFile := path.Join(argoCDDir, ".prev-ctx")
|
||||
|
||||
if ctxName == "-" {
|
||||
prevCtxBytes, err := ioutil.ReadFile(prevCtxFile)
|
||||
errors.CheckError(err)
|
||||
ctxName = string(prevCtxBytes)
|
||||
}
|
||||
localCfg, err := localconfig.ReadLocalConfig(clientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
if localCfg.CurrentContext == ctxName {
|
||||
fmt.Printf("Already at context '%s'\n", localCfg.CurrentContext)
|
||||
return
|
||||
}
|
||||
if _, err = localCfg.ResolveContext(ctxName); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
prevCtx := localCfg.CurrentContext
|
||||
localCfg.CurrentContext = ctxName
|
||||
err = localconfig.WriteLocalConfig(*localCfg, clientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
err = ioutil.WriteFile(prevCtxFile, []byte(prevCtx), 0644)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Switched to context '%s'\n", localCfg.CurrentContext)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
func printArgoCDContexts(configPath string) {
|
||||
localCfg, err := localconfig.ReadLocalConfig(configPath)
|
||||
errors.CheckError(err)
|
||||
if localCfg == nil {
|
||||
log.Fatalf("No contexts defined in %s", configPath)
|
||||
}
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
defer func() { _ = w.Flush() }()
|
||||
columnNames := []string{"CURRENT", "NAME", "SERVER"}
|
||||
_, err = fmt.Fprintf(w, "%s\n", strings.Join(columnNames, "\t"))
|
||||
errors.CheckError(err)
|
||||
|
||||
for _, contextRef := range localCfg.Contexts {
|
||||
context, err := localCfg.ResolveContext(contextRef.Name)
|
||||
if err != nil {
|
||||
log.Warnf("Context '%s' had error: %v", contextRef.Name, err)
|
||||
}
|
||||
prefix := " "
|
||||
if localCfg.CurrentContext == context.Name {
|
||||
prefix = "*"
|
||||
}
|
||||
_, err = fmt.Fprintf(w, "%s\t%s\t%s\n", prefix, context.Name, context.Server.Server)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/install"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
// NewInstallCommand returns a new instance of `argocd install` command
|
||||
func NewInstallCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
installOpts install.InstallOptions
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "install",
|
||||
Short: "Install Argo CD",
|
||||
Long: "Install Argo CD",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
conf, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
installer, err := install.NewInstaller(conf, installOpts)
|
||||
errors.CheckError(err)
|
||||
installer.Install()
|
||||
},
|
||||
}
|
||||
command.Flags().BoolVar(&installOpts.Upgrade, "upgrade", false, "upgrade controller/ui deployments and configmap if already installed")
|
||||
command.Flags().BoolVar(&installOpts.DryRun, "dry-run", false, "print the kubernetes manifests to stdout instead of installing")
|
||||
command.Flags().BoolVar(&installOpts.ConfigSuperuser, "config-superuser", false, "create or update a superuser username and password")
|
||||
command.Flags().StringVar(&installOpts.ConfigMap, "config-map", "", "apply settings from a Kubernetes config map")
|
||||
command.Flags().StringVar(&installOpts.Namespace, "install-namespace", install.DefaultInstallNamespace, "install into a specific namespace")
|
||||
command.Flags().StringVar(&installOpts.ControllerImage, "controller-image", install.DefaultControllerImage, "use a specified controller image")
|
||||
command.Flags().StringVar(&installOpts.ServerImage, "server-image", install.DefaultServerImage, "use a specified api server image")
|
||||
command.Flags().StringVar(&installOpts.UIImage, "ui-image", install.DefaultUIImage, "use a specified ui image")
|
||||
command.Flags().StringVar(&installOpts.RepoServerImage, "repo-server-image", install.DefaultRepoServerImage, "use a specified repo server image")
|
||||
command.Flags().StringVar(&installOpts.ImagePullPolicy, "image-pull-policy", "", "set the image pull policy of the pod specs")
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(command)
|
||||
return command
|
||||
}
|
||||
262
cmd/argocd/commands/login.go
Normal file
@@ -0,0 +1,262 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/server/session"
|
||||
"github.com/argoproj/argo-cd/server/settings"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
grpc_util "github.com/argoproj/argo-cd/util/grpc"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
// NewLoginCommand returns a new instance of `argocd login` command
|
||||
func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
ctxName string
|
||||
username string
|
||||
password string
|
||||
sso bool
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "login SERVER",
|
||||
Short: "Log in to Argo CD",
|
||||
Long: "Log in to Argo CD",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
server := args[0]
|
||||
tlsTestResult, err := grpc_util.TestTLS(server)
|
||||
errors.CheckError(err)
|
||||
if !tlsTestResult.TLS {
|
||||
if !globalClientOpts.PlainText {
|
||||
if !cli.AskToProceed("WARNING: server is not configured with TLS. Proceed (y/n)? ") {
|
||||
os.Exit(1)
|
||||
}
|
||||
globalClientOpts.PlainText = true
|
||||
}
|
||||
} else if tlsTestResult.InsecureErr != nil {
|
||||
if !globalClientOpts.Insecure {
|
||||
if !cli.AskToProceed(fmt.Sprintf("WARNING: server certificate had error: %s. Proceed insecurely (y/n)? ", tlsTestResult.InsecureErr)) {
|
||||
os.Exit(1)
|
||||
}
|
||||
globalClientOpts.Insecure = true
|
||||
}
|
||||
}
|
||||
clientOpts := argocdclient.ClientOptions{
|
||||
ConfigPath: "",
|
||||
ServerAddr: server,
|
||||
Insecure: globalClientOpts.Insecure,
|
||||
PlainText: globalClientOpts.PlainText,
|
||||
}
|
||||
acdClient := argocdclient.NewClientOrDie(&clientOpts)
|
||||
setConn, setIf := acdClient.NewSettingsClientOrDie()
|
||||
defer util.Close(setConn)
|
||||
|
||||
if ctxName == "" {
|
||||
ctxName = server
|
||||
}
|
||||
|
||||
// Perform the login
|
||||
var tokenString string
|
||||
var refreshToken string
|
||||
if !sso {
|
||||
tokenString = passwordLogin(acdClient, username, password)
|
||||
} else {
|
||||
acdSet, err := setIf.Get(context.Background(), &settings.SettingsQuery{})
|
||||
errors.CheckError(err)
|
||||
if !ssoConfigured(acdSet) {
|
||||
log.Fatalf("ArgoCD instance is not configured with SSO")
|
||||
}
|
||||
tokenString, refreshToken = oauth2Login(server, clientOpts.PlainText)
|
||||
}
|
||||
|
||||
parser := &jwt.Parser{
|
||||
SkipClaimsValidation: true,
|
||||
}
|
||||
claims := jwt.MapClaims{}
|
||||
_, _, err = parser.ParseUnverified(tokenString, &claims)
|
||||
errors.CheckError(err)
|
||||
|
||||
fmt.Printf("'%s' logged in successfully\n", userDisplayName(claims))
|
||||
// login successful. Persist the config
|
||||
localCfg, err := localconfig.ReadLocalConfig(globalClientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
if localCfg == nil {
|
||||
localCfg = &localconfig.LocalConfig{}
|
||||
}
|
||||
localCfg.UpsertServer(localconfig.Server{
|
||||
Server: server,
|
||||
PlainText: globalClientOpts.PlainText,
|
||||
Insecure: globalClientOpts.Insecure,
|
||||
})
|
||||
localCfg.UpsertUser(localconfig.User{
|
||||
Name: ctxName,
|
||||
AuthToken: tokenString,
|
||||
RefreshToken: refreshToken,
|
||||
})
|
||||
if ctxName == "" {
|
||||
ctxName = server
|
||||
}
|
||||
localCfg.CurrentContext = ctxName
|
||||
localCfg.UpsertContext(localconfig.ContextRef{
|
||||
Name: ctxName,
|
||||
User: ctxName,
|
||||
Server: server,
|
||||
})
|
||||
err = localconfig.WriteLocalConfig(*localCfg, globalClientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Context '%s' updated\n", ctxName)
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&ctxName, "name", "", "name to use for the context")
|
||||
command.Flags().StringVar(&username, "username", "", "the username of an account to authenticate")
|
||||
command.Flags().StringVar(&password, "password", "", "the password of an account to authenticate")
|
||||
command.Flags().BoolVar(&sso, "sso", false, "Perform SSO login")
|
||||
return command
|
||||
}
|
||||
|
||||
func userDisplayName(claims jwt.MapClaims) string {
|
||||
if email, ok := claims["email"]; ok && email != nil {
|
||||
return email.(string)
|
||||
}
|
||||
if name, ok := claims["name"]; ok && name != nil {
|
||||
return name.(string)
|
||||
}
|
||||
return claims["sub"].(string)
|
||||
}
|
||||
|
||||
func ssoConfigured(set *settings.Settings) bool {
|
||||
return set.DexConfig != nil && len(set.DexConfig.Connectors) > 0
|
||||
}
|
||||
|
||||
// getFreePort asks the kernel for a free open port that is ready to use.
|
||||
func getFreePort() (int, error) {
|
||||
ln, err := net.Listen("tcp", "[::]:0")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return ln.Addr().(*net.TCPAddr).Port, ln.Close()
|
||||
}
|
||||
|
||||
// oauth2Login opens a browser, runs a temporary HTTP server to delegate OAuth2 login flow and
|
||||
// returns the JWT token and a refresh token (if supported)
|
||||
func oauth2Login(host string, plaintext bool) (string, string) {
|
||||
ctx := context.Background()
|
||||
port, err := getFreePort()
|
||||
errors.CheckError(err)
|
||||
var scheme = "https"
|
||||
if plaintext {
|
||||
scheme = "http"
|
||||
}
|
||||
conf := &oauth2.Config{
|
||||
ClientID: common.ArgoCDCLIClientAppID,
|
||||
Scopes: []string{"openid", "profile", "email", "groups", "offline_access"},
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: fmt.Sprintf("%s://%s%s/auth", scheme, host, common.DexAPIEndpoint),
|
||||
TokenURL: fmt.Sprintf("%s://%s%s/token", scheme, host, common.DexAPIEndpoint),
|
||||
},
|
||||
RedirectURL: fmt.Sprintf("http://localhost:%d/auth/callback", port),
|
||||
}
|
||||
srv := &http.Server{Addr: ":" + strconv.Itoa(port)}
|
||||
var tokenString string
|
||||
var refreshToken string
|
||||
loginCompleted := make(chan struct{})
|
||||
|
||||
callbackHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
loginCompleted <- struct{}{}
|
||||
}()
|
||||
|
||||
// Authorization redirect callback from OAuth2 auth flow.
|
||||
if errMsg := r.FormValue("error"); errMsg != "" {
|
||||
http.Error(w, errMsg+": "+r.FormValue("error_description"), http.StatusBadRequest)
|
||||
log.Fatal(errMsg)
|
||||
return
|
||||
}
|
||||
code := r.FormValue("code")
|
||||
if code == "" {
|
||||
errMsg := fmt.Sprintf("no code in request: %q", r.Form)
|
||||
http.Error(w, errMsg, http.StatusBadRequest)
|
||||
log.Fatal(errMsg)
|
||||
return
|
||||
}
|
||||
tok, err := conf.Exchange(ctx, code)
|
||||
errors.CheckError(err)
|
||||
log.Info("Authentication successful")
|
||||
|
||||
var ok bool
|
||||
tokenString, ok = tok.Extra("id_token").(string)
|
||||
if !ok {
|
||||
errMsg := "no id_token in token response"
|
||||
http.Error(w, errMsg, http.StatusInternalServerError)
|
||||
log.Fatal(errMsg)
|
||||
return
|
||||
}
|
||||
refreshToken, _ = tok.Extra("refresh_token").(string)
|
||||
log.Debugf("Token: %s", tokenString)
|
||||
log.Debugf("Refresh Token: %s", tokenString)
|
||||
successPage := `
|
||||
<div style="height:100px; width:100%!; display:flex; flex-direction: column; justify-content: center; align-items:center; background-color:#2ecc71; color:white; font-size:22"><div>Authentication successful!</div></div>
|
||||
<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)
|
||||
}
|
||||
http.HandleFunc("/auth/callback", callbackHandler)
|
||||
|
||||
// add transport for self-signed certificate to context
|
||||
sslcli := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, sslcli)
|
||||
|
||||
// Redirect user to login & consent page to ask for permission for the scopes specified above.
|
||||
log.Info("Opening browser for authentication")
|
||||
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline)
|
||||
log.Infof("Authentication URL: %s", url)
|
||||
time.Sleep(1 * time.Second)
|
||||
err = open.Run(url)
|
||||
errors.CheckError(err)
|
||||
go func() {
|
||||
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
|
||||
log.Fatalf("listen: %s\n", err)
|
||||
}
|
||||
}()
|
||||
<-loginCompleted
|
||||
_ = srv.Shutdown(ctx)
|
||||
return tokenString, refreshToken
|
||||
}
|
||||
|
||||
func passwordLogin(acdClient argocdclient.Client, username, password string) string {
|
||||
username, password = cli.PromptCredentials(username, password)
|
||||
sessConn, sessionIf := acdClient.NewSessionClientOrDie()
|
||||
defer util.Close(sessConn)
|
||||
sessionRequest := session.SessionCreateRequest{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
createdSession, err := sessionIf.Create(context.Background(), &sessionRequest)
|
||||
errors.CheckError(err)
|
||||
return createdSession.Token
|
||||
}
|
||||
335
cmd/argocd/commands/project.go
Normal file
@@ -0,0 +1,335 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"strings"
|
||||
|
||||
"context"
|
||||
|
||||
"fmt"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/server/project"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
"github.com/spf13/pflag"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
type projectOpts struct {
|
||||
description string
|
||||
destinations []string
|
||||
sources []string
|
||||
}
|
||||
|
||||
func (opts *projectOpts) GetDestinations() []v1alpha1.ApplicationDestination {
|
||||
destinations := make([]v1alpha1.ApplicationDestination, 0)
|
||||
for _, destStr := range opts.destinations {
|
||||
parts := strings.Split(destStr, ",")
|
||||
if len(parts) != 2 {
|
||||
log.Fatalf("Expected destination of the form: server,namespace. Received: %s", destStr)
|
||||
} else {
|
||||
destinations = append(destinations, v1alpha1.ApplicationDestination{
|
||||
Server: parts[0],
|
||||
Namespace: parts[1],
|
||||
})
|
||||
}
|
||||
}
|
||||
return destinations
|
||||
}
|
||||
|
||||
// NewProjectCommand returns a new instance of an `argocd proj` command
|
||||
func NewProjectCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "proj",
|
||||
Short: "Manage projects",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
},
|
||||
}
|
||||
command.AddCommand(NewProjectCreateCommand(clientOpts))
|
||||
command.AddCommand(NewProjectDeleteCommand(clientOpts))
|
||||
command.AddCommand(NewProjectListCommand(clientOpts))
|
||||
command.AddCommand(NewProjectSetCommand(clientOpts))
|
||||
command.AddCommand(NewProjectAddDestinationCommand(clientOpts))
|
||||
command.AddCommand(NewProjectRemoveDestinationCommand(clientOpts))
|
||||
command.AddCommand(NewProjectAddSourceCommand(clientOpts))
|
||||
command.AddCommand(NewProjectRemoveSourceCommand(clientOpts))
|
||||
return command
|
||||
}
|
||||
|
||||
func addProjFlags(command *cobra.Command, opts *projectOpts) {
|
||||
command.Flags().StringVarP(&opts.description, "description", "", "desc", "Project description")
|
||||
command.Flags().StringArrayVarP(&opts.destinations, "dest", "d", []string{},
|
||||
"Allowed deployment destination. Includes comma separated server url and namespace (e.g. https://192.168.99.100:8443,default")
|
||||
command.Flags().StringArrayVarP(&opts.sources, "src", "s", []string{}, "Allowed deployment source repository URL.")
|
||||
}
|
||||
|
||||
// NewProjectCreateCommand returns a new instance of an `argocd proj create` command
|
||||
func NewProjectCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
opts projectOpts
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "create PROJECT",
|
||||
Short: "Create a project",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
proj := v1alpha1.AppProject{
|
||||
ObjectMeta: v1.ObjectMeta{Name: projName},
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
Description: opts.description,
|
||||
Destinations: opts.GetDestinations(),
|
||||
SourceRepos: opts.sources,
|
||||
},
|
||||
}
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
_, err := projIf.Create(context.Background(), &project.ProjectCreateRequest{Project: &proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
addProjFlags(command, &opts)
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectSetCommand returns a new instance of an `argocd proj set` command
|
||||
func NewProjectSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
opts projectOpts
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "set PROJECT",
|
||||
Short: "Set project parameters",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
visited := 0
|
||||
c.Flags().Visit(func(f *pflag.Flag) {
|
||||
visited++
|
||||
switch f.Name {
|
||||
case "description":
|
||||
proj.Spec.Description = opts.description
|
||||
case "dest":
|
||||
proj.Spec.Destinations = opts.GetDestinations()
|
||||
case "src":
|
||||
proj.Spec.SourceRepos = opts.sources
|
||||
}
|
||||
})
|
||||
if visited == 0 {
|
||||
log.Error("Please set at least one option to update")
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
addProjFlags(command, &opts)
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectAddDestinationCommand returns a new instance of an `argocd proj add-destination` command
|
||||
func NewProjectAddDestinationCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "add-destination PROJECT SERVER NAMESPACE",
|
||||
Short: "Add project destination",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
server := args[1]
|
||||
namespace := args[2]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
for _, dest := range proj.Spec.Destinations {
|
||||
if dest.Namespace == namespace && dest.Server == server {
|
||||
log.Fatal("Specified destination is already defined in project")
|
||||
}
|
||||
}
|
||||
proj.Spec.Destinations = append(proj.Spec.Destinations, v1alpha1.ApplicationDestination{Server: server, Namespace: namespace})
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectRemoveDestinationCommand returns a new instance of an `argocd proj remove-destination` command
|
||||
func NewProjectRemoveDestinationCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "remove-destination PROJECT SERVER NAMESPACE",
|
||||
Short: "Remove project destination",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
server := args[1]
|
||||
namespace := args[2]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
index := -1
|
||||
for i, dest := range proj.Spec.Destinations {
|
||||
if dest.Namespace == namespace && dest.Server == server {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
log.Fatal("Specified destination does not exist in project")
|
||||
} else {
|
||||
proj.Spec.Destinations = append(proj.Spec.Destinations[:index], proj.Spec.Destinations[index+1:]...)
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectAddSourceCommand returns a new instance of an `argocd proj add-src` command
|
||||
func NewProjectAddSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "add-source PROJECT URL",
|
||||
Short: "Add project source repository",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
url := args[1]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
for _, item := range proj.Spec.SourceRepos {
|
||||
if item == git.NormalizeGitURL(url) {
|
||||
log.Fatal("Specified source repository is already defined in project")
|
||||
}
|
||||
}
|
||||
proj.Spec.SourceRepos = append(proj.Spec.SourceRepos, url)
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectRemoveSourceCommand returns a new instance of an `argocd proj remove-src` command
|
||||
func NewProjectRemoveSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "remove-source PROJECT URL",
|
||||
Short: "Remove project source repository",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
url := args[1]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
index := -1
|
||||
for i, item := range proj.Spec.SourceRepos {
|
||||
if item == git.NormalizeGitURL(url) {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
log.Fatal("Specified source repository does not exist in project")
|
||||
} else {
|
||||
proj.Spec.SourceRepos = append(proj.Spec.SourceRepos[:index], proj.Spec.SourceRepos[index+1:]...)
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectDeleteCommand returns a new instance of an `argocd proj delete` command
|
||||
func NewProjectDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "delete PROJECT",
|
||||
Short: "Delete project",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
for _, name := range args {
|
||||
_, err := projIf.Delete(context.Background(), &project.ProjectQuery{Name: name})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectListCommand returns a new instance of an `argocd proj list` command
|
||||
func NewProjectListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List projects",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
projects, err := projIf.List(context.Background(), &project.ProjectQuery{})
|
||||
errors.CheckError(err)
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "NAME\tDESCRIPTION\tDESTINATIONS\n")
|
||||
for _, p := range projects.Items {
|
||||
fmt.Fprintf(w, "%s\t%s\t%v\n", p.Name, p.Spec.Description, p.Spec.Destinations)
|
||||
}
|
||||
_ = w.Flush()
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
75
cmd/argocd/commands/relogin.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
"github.com/argoproj/argo-cd/util/session"
|
||||
)
|
||||
|
||||
// NewReloginCommand returns a new instance of `argocd relogin` command
|
||||
func NewReloginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
password string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "relogin",
|
||||
Short: "Refresh an expired authenticate token",
|
||||
Long: "Refresh an expired authenticate token",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
localCfg, err := localconfig.ReadLocalConfig(globalClientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
if localCfg == nil {
|
||||
log.Fatalf("No context found. Login using `argocd login`")
|
||||
}
|
||||
configCtx, err := localCfg.ResolveContext(localCfg.CurrentContext)
|
||||
errors.CheckError(err)
|
||||
|
||||
parser := &jwt.Parser{
|
||||
SkipClaimsValidation: true,
|
||||
}
|
||||
claims := jwt.StandardClaims{}
|
||||
_, _, err = parser.ParseUnverified(configCtx.User.AuthToken, &claims)
|
||||
errors.CheckError(err)
|
||||
|
||||
var tokenString string
|
||||
var refreshToken string
|
||||
if claims.Issuer == session.SessionManagerClaimsIssuer {
|
||||
clientOpts := argocdclient.ClientOptions{
|
||||
ConfigPath: "",
|
||||
ServerAddr: configCtx.Server.Server,
|
||||
Insecure: configCtx.Server.Insecure,
|
||||
PlainText: configCtx.Server.PlainText,
|
||||
}
|
||||
acdClient := argocdclient.NewClientOrDie(&clientOpts)
|
||||
fmt.Printf("Relogging in as '%s'\n", claims.Subject)
|
||||
tokenString = passwordLogin(acdClient, claims.Subject, password)
|
||||
} else {
|
||||
fmt.Println("Reinitiating SSO login")
|
||||
tokenString, refreshToken = oauth2Login(configCtx.Server.Server, configCtx.Server.PlainText)
|
||||
}
|
||||
|
||||
localCfg.UpsertUser(localconfig.User{
|
||||
Name: localCfg.CurrentContext,
|
||||
AuthToken: tokenString,
|
||||
RefreshToken: refreshToken,
|
||||
})
|
||||
err = localconfig.WriteLocalConfig(*localCfg, globalClientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Context '%s' updated\n", localCfg.CurrentContext)
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&password, "password", "", "the password of an account to authenticate")
|
||||
return command
|
||||
}
|
||||
@@ -1,30 +1,29 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"syscall"
|
||||
"text/tabwriter"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
appsv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/server/repository"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
// NewRepoCommand returns a new instance of an `argocd repo` command
|
||||
func NewRepoCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "repo",
|
||||
Short: fmt.Sprintf("%s repo COMMAND", cliName),
|
||||
Short: "Manage git repository credentials",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
@@ -41,11 +40,12 @@ func NewRepoCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
repo appsv1.Repository
|
||||
upsert bool
|
||||
sshPrivateKeyPath string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "add",
|
||||
Short: fmt.Sprintf("%s repo add REPO", cliName),
|
||||
Use: "add REPO",
|
||||
Short: "Add git repository credentials",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
@@ -59,20 +59,28 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
}
|
||||
repo.SSHPrivateKey = string(keyData)
|
||||
}
|
||||
err := git.TestRepo(repo.Repo, repo.Username, repo.Password, repo.SSHPrivateKey)
|
||||
// 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)
|
||||
if err != nil {
|
||||
if repo.Username != "" && repo.Password != "" || git.IsSshURL(repo.Repo) {
|
||||
// if everything was supplied or repo URL is SSH url, one of the inputs was definitely bad
|
||||
if git.IsSSHURL(repo.Repo) {
|
||||
// If we failed using git SSH credentials, then the repo is automatically bad
|
||||
log.Fatal(err)
|
||||
}
|
||||
// If we can't test the repo, it's probably private. Prompt for credentials and try again.
|
||||
promptCredentials(&repo)
|
||||
err = git.TestRepo(repo.Repo, repo.Username, repo.Password, repo.SSHPrivateKey)
|
||||
// If we can't test the repo, it's probably private. Prompt for credentials and
|
||||
// let the server test it.
|
||||
repo.Username, repo.Password = cli.PromptCredentials(repo.Username, repo.Password)
|
||||
}
|
||||
errors.CheckError(err)
|
||||
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoClientOrDie()
|
||||
defer util.Close(conn)
|
||||
createdRepo, err := repoIf.Create(context.Background(), &repo)
|
||||
repoCreateReq := repository.RepoCreateRequest{
|
||||
Repo: &repo,
|
||||
Upsert: upsert,
|
||||
}
|
||||
createdRepo, err := repoIf.Create(context.Background(), &repoCreateReq)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("repository '%s' added\n", createdRepo.Repo)
|
||||
},
|
||||
@@ -80,28 +88,15 @@ 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, "sshPrivateKeyPath", "", "path to the private ssh key (e.g. ~/.ssh/id_rsa)")
|
||||
command.Flags().BoolVar(&upsert, "upsert", false, "Override an existing repository with the same name even if the spec differs")
|
||||
return command
|
||||
}
|
||||
|
||||
func promptCredentials(repo *appsv1.Repository) {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
if repo.Username == "" {
|
||||
fmt.Print("Username: ")
|
||||
username, _ := reader.ReadString('\n')
|
||||
repo.Username = username
|
||||
}
|
||||
if repo.Password == "" {
|
||||
fmt.Print("Password: ")
|
||||
bytePassword, _ := terminal.ReadPassword(syscall.Stdin)
|
||||
repo.Password = string(bytePassword)
|
||||
}
|
||||
}
|
||||
|
||||
// NewRepoRemoveCommand returns a new instance of an `argocd repo list` command
|
||||
func NewRepoRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "rm",
|
||||
Short: fmt.Sprintf("%s repo rm REPO", cliName),
|
||||
Use: "rm REPO",
|
||||
Short: "Remove git repository credentials",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
c.HelpFunc()(c, args)
|
||||
@@ -122,16 +117,16 @@ func NewRepoRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
func NewRepoListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: fmt.Sprintf("%s repo list", cliName),
|
||||
Short: "List configured repositories",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
conn, repoIf := argocdclient.NewClientOrDie(clientOpts).NewRepoClientOrDie()
|
||||
defer util.Close(conn)
|
||||
repos, err := repoIf.List(context.Background(), &repository.RepoQuery{})
|
||||
errors.CheckError(err)
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "REPO\tUSER\n")
|
||||
fmt.Fprintf(w, "REPO\tUSER\tSTATUS\tMESSAGE\n")
|
||||
for _, r := range repos.Items {
|
||||
fmt.Fprintf(w, "%s\t%s\n", r.Repo, r.Username)
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", r.Repo, r.Username, r.ConnectionState.Status, r.ConnectionState.Message)
|
||||
}
|
||||
_ = w.Flush()
|
||||
},
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
@@ -22,15 +23,24 @@ func NewCommand() *cobra.Command {
|
||||
},
|
||||
}
|
||||
|
||||
command.AddCommand(cli.NewVersionCmd(cliName))
|
||||
command.AddCommand(NewVersionCmd(&clientOpts))
|
||||
command.AddCommand(NewClusterCommand(&clientOpts, pathOpts))
|
||||
command.AddCommand(NewApplicationCommand(&clientOpts))
|
||||
command.AddCommand(NewLoginCommand(&clientOpts))
|
||||
command.AddCommand(NewReloginCommand(&clientOpts))
|
||||
command.AddCommand(NewRepoCommand(&clientOpts))
|
||||
command.AddCommand(NewInstallCommand())
|
||||
command.AddCommand(NewUninstallCommand())
|
||||
command.AddCommand(NewContextCommand(&clientOpts))
|
||||
command.AddCommand(NewProjectCommand(&clientOpts))
|
||||
command.AddCommand(NewAccountCommand(&clientOpts))
|
||||
|
||||
defaultLocalConfigPath, err := localconfig.DefaultLocalConfigPath()
|
||||
errors.CheckError(err)
|
||||
command.PersistentFlags().StringVar(&clientOpts.ConfigPath, "config", defaultLocalConfigPath, "Path to ArgoCD config")
|
||||
command.PersistentFlags().StringVar(&clientOpts.ServerAddr, "server", "", "ArgoCD server address")
|
||||
command.PersistentFlags().BoolVar(&clientOpts.Insecure, "insecure", true, "Disable transport security for the client connection")
|
||||
command.PersistentFlags().BoolVar(&clientOpts.PlainText, "plaintext", false, "Disable TLS")
|
||||
command.PersistentFlags().BoolVar(&clientOpts.Insecure, "insecure", false, "Skip server certificate and domain verification")
|
||||
command.PersistentFlags().StringVar(&clientOpts.CertFile, "server-crt", "", "Server certificate file")
|
||||
command.PersistentFlags().StringVar(&clientOpts.AuthToken, "auth-token", "", "Authentication token")
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/install"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
// NewUninstallCommand returns a new instance of `argocd install` command
|
||||
func NewUninstallCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
installOpts install.InstallOptions
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "uninstall",
|
||||
Short: "Uninstall Argo CD",
|
||||
Long: "Uninstall Argo CD",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
conf, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
installer, err := install.NewInstaller(conf, installOpts)
|
||||
errors.CheckError(err)
|
||||
installer.Uninstall()
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&installOpts.Namespace, "install-namespace", install.DefaultInstallNamespace, "uninstall from a specific namespace")
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(command)
|
||||
return command
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/ghodss/yaml"
|
||||
)
|
||||
|
||||
// unmarshalApplication tries to convert a YAML or JSON byte array into an Application struct.
|
||||
func unmarshalApplication(data []byte, app *argoappv1.Application) {
|
||||
// first, try unmarshaling as JSON
|
||||
// Based on technique from Kubectl, which supports both YAML and JSON:
|
||||
// https://mlafeldt.github.io/blog/teaching-go-programs-to-love-json-and-yaml/
|
||||
// http://ghodss.com/2014/the-right-way-to-handle-yaml-in-golang/
|
||||
// Short version: JSON unmarshaling won't zero out null fields; YAML unmarshaling will.
|
||||
// This may have unintended effects or hard-to-catch issues when populating our application object.
|
||||
data, err := yaml.YAMLToJSON(data)
|
||||
if err != nil {
|
||||
log.Fatal("Could not decode valid JSON or YAML Kubernetes manifest")
|
||||
}
|
||||
err = json.Unmarshal(data, &app)
|
||||
if err != nil {
|
||||
log.Fatalf("Could not unmarshal Kubernetes manifest: %s", string(data))
|
||||
}
|
||||
}
|
||||
|
||||
// readLocalFile reads a file from disk and returns its contents as a byte array.
|
||||
// The caller is responsible for checking error return values.
|
||||
func readLocalFile(path string) (data []byte, err error) {
|
||||
data, err = ioutil.ReadFile(path)
|
||||
return
|
||||
}
|
||||
|
||||
// readRemoteFile issues a GET request to retrieve the contents of the specified URL as a byte array.
|
||||
// The caller is responsible for checking error return values.
|
||||
func readRemoteFile(url string) (data []byte, err error) {
|
||||
resp, err := http.Get(url)
|
||||
if err == nil {
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
data, err = ioutil.ReadAll(resp.Body)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReadLocalFile(t *testing.T) {
|
||||
sentinel := "Hello, world!"
|
||||
|
||||
file, err := ioutil.TempFile(os.TempDir(), "")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer func() {
|
||||
_ = os.Remove(file.Name())
|
||||
}()
|
||||
|
||||
_, _ = file.WriteString(sentinel)
|
||||
_ = file.Sync()
|
||||
|
||||
data, err := readLocalFile(file.Name())
|
||||
if string(data) != sentinel {
|
||||
t.Errorf("Test data did not match (err = %v)! Expected \"%s\" and received \"%s\"", err, sentinel, string(data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadRemoteFile(t *testing.T) {
|
||||
sentinel := "Hello, world!"
|
||||
|
||||
serve := func(c chan<- string) {
|
||||
// listen on first available dynamic (unprivileged) port
|
||||
listener, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// send back the address so that it can be used
|
||||
c <- listener.Addr().String()
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// return the sentinel text at root URL
|
||||
fmt.Fprint(w, sentinel)
|
||||
})
|
||||
|
||||
panic(http.Serve(listener, nil))
|
||||
}
|
||||
|
||||
c := make(chan string, 1)
|
||||
|
||||
// run a local webserver to test data retrieval
|
||||
go serve(c)
|
||||
|
||||
address := <-c
|
||||
data, err := readRemoteFile("http://" + address)
|
||||
t.Logf("Listening at address: %s", address)
|
||||
if string(data) != sentinel {
|
||||
t.Errorf("Test data did not match (err = %v)! Expected \"%s\" and received \"%s\"", err, sentinel, string(data))
|
||||
}
|
||||
}
|
||||
65
cmd/argocd/commands/version.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
argocd "github.com/argoproj/argo-cd"
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// NewVersionCmd returns a new `version` command to be used as a sub-command to root
|
||||
func NewVersionCmd(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var short bool
|
||||
var client bool
|
||||
|
||||
versionCmd := cobra.Command{
|
||||
Use: "version",
|
||||
Short: fmt.Sprintf("Print version information"),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
version := argocd.GetVersion()
|
||||
fmt.Printf("%s: %s\n", cliName, version)
|
||||
if !short {
|
||||
fmt.Printf(" BuildDate: %s\n", version.BuildDate)
|
||||
fmt.Printf(" GitCommit: %s\n", version.GitCommit)
|
||||
fmt.Printf(" GitTreeState: %s\n", version.GitTreeState)
|
||||
if version.GitTag != "" {
|
||||
fmt.Printf(" GitTag: %s\n", version.GitTag)
|
||||
}
|
||||
fmt.Printf(" GoVersion: %s\n", version.GoVersion)
|
||||
fmt.Printf(" Compiler: %s\n", version.Compiler)
|
||||
fmt.Printf(" Platform: %s\n", version.Platform)
|
||||
}
|
||||
if client {
|
||||
return
|
||||
}
|
||||
|
||||
// Get Server version
|
||||
conn, versionIf := argocdclient.NewClientOrDie(clientOpts).NewVersionClientOrDie()
|
||||
defer util.Close(conn)
|
||||
serverVers, err := versionIf.Version(context.Background(), &empty.Empty{})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("%s: %s\n", "argocd-server", serverVers.Version)
|
||||
if !short {
|
||||
fmt.Printf(" BuildDate: %s\n", serverVers.BuildDate)
|
||||
fmt.Printf(" GitCommit: %s\n", serverVers.GitCommit)
|
||||
fmt.Printf(" GitTreeState: %s\n", serverVers.GitTreeState)
|
||||
if version.GitTag != "" {
|
||||
fmt.Printf(" GitTag: %s\n", serverVers.GitTag)
|
||||
}
|
||||
fmt.Printf(" GoVersion: %s\n", serverVers.GoVersion)
|
||||
fmt.Printf(" Compiler: %s\n", serverVers.Compiler)
|
||||
fmt.Printf(" Platform: %s\n", serverVers.Platform)
|
||||
fmt.Printf(" Ksonnet Version: %s\n", serverVers.KsonnetVersion)
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
versionCmd.Flags().BoolVar(&short, "short", false, "print just the version number")
|
||||
versionCmd.Flags().BoolVar(&client, "client", false, "client version only (no server required)")
|
||||
return &versionCmd
|
||||
}
|
||||
104
common/common.go
@@ -1,8 +1,9 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -14,6 +15,44 @@ const (
|
||||
|
||||
// SecretTypeCluster indicates a secret type of cluster
|
||||
SecretTypeCluster = "cluster"
|
||||
|
||||
// AuthCookieName is the HTTP cookie name where we store our auth token
|
||||
AuthCookieName = "argocd.token"
|
||||
// ResourcesFinalizerName is a number of application CRD finalizer
|
||||
ResourcesFinalizerName = "resources-finalizer." + MetadataPrefix
|
||||
|
||||
// KubernetesInternalAPIServerAddr is address of the k8s API server when accessing internal to the cluster
|
||||
KubernetesInternalAPIServerAddr = "https://kubernetes.default.svc"
|
||||
)
|
||||
|
||||
const (
|
||||
ArgoCDAdminUsername = "admin"
|
||||
ArgoCDSecretName = "argocd-secret"
|
||||
ArgoCDConfigMapName = "argocd-cm"
|
||||
ArgoCDRBACConfigMapName = "argocd-rbac-cm"
|
||||
)
|
||||
|
||||
const (
|
||||
// DexAPIEndpoint is the endpoint where we serve the Dex API server
|
||||
DexAPIEndpoint = "/api/dex"
|
||||
// LoginEndpoint is ArgoCD's shorthand login endpoint which redirects to dex's OAuth 2.0 provider's consent page
|
||||
LoginEndpoint = "/auth/login"
|
||||
// CallbackEndpoint is ArgoCD's final callback endpoint we reach after OAuth 2.0 login flow has been completed
|
||||
CallbackEndpoint = "/auth/callback"
|
||||
// ArgoCDClientAppName is name of the Oauth client app used when registering our web app to dex
|
||||
ArgoCDClientAppName = "ArgoCD"
|
||||
// ArgoCDClientAppID is the Oauth client ID we will use when registering our app to dex
|
||||
ArgoCDClientAppID = "argo-cd"
|
||||
// ArgoCDCLIClientAppName is name of the Oauth client app used when registering our CLI to dex
|
||||
ArgoCDCLIClientAppName = "ArgoCD CLI"
|
||||
// ArgoCDCLIClientAppID is the Oauth client ID we will use when registering our CLI to dex
|
||||
ArgoCDCLIClientAppID = "argo-cd-cli"
|
||||
// EnvVarSSODebug is an environment variable to enable additional OAuth debugging in the API server
|
||||
EnvVarSSODebug = "ARGOCD_SSO_DEBUG"
|
||||
// EnvVarRBACDebug is an environment variable to enable additional RBAC debugging in the API server
|
||||
EnvVarRBACDebug = "ARGOCD_RBAC_DEBUG"
|
||||
// DefaultAppProjectName contains name of default app project. The default app project allows deploying application to any cluster.
|
||||
DefaultAppProjectName = "default"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -23,10 +62,30 @@ var (
|
||||
// LabelKeySecretType contains the type of argocd secret (either 'cluster' or 'repo')
|
||||
LabelKeySecretType = MetadataPrefix + "/secret-type"
|
||||
|
||||
// AnnotationConnectionStatus contains connection state status
|
||||
AnnotationConnectionStatus = MetadataPrefix + "/connection-status"
|
||||
// AnnotationConnectionMessage contains additional information about connection status
|
||||
AnnotationConnectionMessage = MetadataPrefix + "/connection-message"
|
||||
// AnnotationConnectionModifiedAt contains timestamp when connection state had been modified
|
||||
AnnotationConnectionModifiedAt = MetadataPrefix + "/connection-modified-at"
|
||||
|
||||
// AnnotationHook contains the hook type of a resource
|
||||
AnnotationHook = MetadataPrefix + "/hook"
|
||||
// AnnotationHookDeletePolicy is the policy of deleting a hook
|
||||
AnnotationHookDeletePolicy = MetadataPrefix + "/hook-delete-policy"
|
||||
// AnnotationHelmHook is the helm hook annotation
|
||||
AnnotationHelmHook = "helm.sh/hook"
|
||||
|
||||
// LabelKeyApplicationControllerInstanceID is the label which allows to separate application among multiple running application controllers.
|
||||
LabelKeyApplicationControllerInstanceID = application.ApplicationFullName + "/controller-instanceid"
|
||||
|
||||
// LabelApplicationName is the label which indicates that resource belongs to application with the specified name
|
||||
LabelApplicationName = application.ApplicationFullName + "/app-name"
|
||||
|
||||
// AnnotationKeyRefresh is the annotation key in the application which is updated with an
|
||||
// arbitrary value (i.e. timestamp) on a git event, to force the controller to wake up and
|
||||
// re-evaluate the application
|
||||
AnnotationKeyRefresh = application.ApplicationFullName + "/refresh"
|
||||
)
|
||||
|
||||
// ArgoCDManagerServiceAccount is the name of the service account for managing a cluster
|
||||
@@ -44,46 +103,3 @@ var ArgoCDManagerPolicyRules = []rbacv1.PolicyRule{
|
||||
Verbs: []string{"*"},
|
||||
},
|
||||
}
|
||||
|
||||
const (
|
||||
ArgoCDServerServiceAccount = "argocd-server"
|
||||
ArgoCDServerRole = "argocd-server-role"
|
||||
ArgoCDServerRoleBinding = "argocd-server-role-binding"
|
||||
)
|
||||
|
||||
var ArgoCDServerPolicyRules = []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"pods", "pods/exec", "pods/log"},
|
||||
Verbs: []string{"get", "list", "watch"},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"secrets"},
|
||||
Verbs: []string{"create", "get", "list", "watch", "update", "patch", "delete"},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{"argoproj.io"},
|
||||
Resources: []string{"applications"},
|
||||
Verbs: []string{"create", "get", "list", "watch", "update", "patch", "delete"},
|
||||
},
|
||||
}
|
||||
|
||||
const (
|
||||
ApplicationControllerServiceAccount = "application-controller"
|
||||
ApplicationControllerRole = "application-controller-role"
|
||||
ApplicationControllerRoleBinding = "application-controller-role-binding"
|
||||
)
|
||||
|
||||
var ApplicationControllerPolicyRules = []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{""},
|
||||
Resources: []string{"secrets"},
|
||||
Verbs: []string{"get"},
|
||||
},
|
||||
{
|
||||
APIGroups: []string{"argoproj.io"},
|
||||
Resources: []string{"applications"},
|
||||
Verbs: []string{"create", "get", "list", "watch", "update", "patch", "delete"},
|
||||
},
|
||||
}
|
||||
|
||||
702
controller/appcontroller.go
Normal file
@@ -0,0 +1,702 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"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"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/strategicpatch"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
appinformers "github.com/argoproj/argo-cd/pkg/client/informers/externalversions"
|
||||
"github.com/argoproj/argo-cd/reposerver"
|
||||
"github.com/argoproj/argo-cd/util/argo"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/health"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
)
|
||||
|
||||
const (
|
||||
watchResourcesRetryTimeout = 10 * time.Second
|
||||
updateOperationStateTimeout = 1 * time.Second
|
||||
)
|
||||
|
||||
// ApplicationController is the controller for application resources.
|
||||
type ApplicationController struct {
|
||||
namespace string
|
||||
kubeClientset kubernetes.Interface
|
||||
applicationClientset appclientset.Interface
|
||||
auditLogger *argo.AuditLogger
|
||||
appRefreshQueue workqueue.RateLimitingInterface
|
||||
appOperationQueue workqueue.RateLimitingInterface
|
||||
appInformer cache.SharedIndexInformer
|
||||
appStateManager AppStateManager
|
||||
statusRefreshTimeout time.Duration
|
||||
repoClientset reposerver.Clientset
|
||||
db db.ArgoDB
|
||||
forceRefreshApps map[string]bool
|
||||
forceRefreshAppsMutex *sync.Mutex
|
||||
}
|
||||
|
||||
type ApplicationControllerConfig struct {
|
||||
InstanceID string
|
||||
Namespace string
|
||||
}
|
||||
|
||||
// NewApplicationController creates new instance of ApplicationController.
|
||||
func NewApplicationController(
|
||||
namespace string,
|
||||
kubeClientset kubernetes.Interface,
|
||||
applicationClientset appclientset.Interface,
|
||||
repoClientset reposerver.Clientset,
|
||||
db db.ArgoDB,
|
||||
appStateManager AppStateManager,
|
||||
appResyncPeriod time.Duration,
|
||||
config *ApplicationControllerConfig,
|
||||
) *ApplicationController {
|
||||
appRefreshQueue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())
|
||||
appOperationQueue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())
|
||||
return &ApplicationController{
|
||||
namespace: namespace,
|
||||
kubeClientset: kubeClientset,
|
||||
applicationClientset: applicationClientset,
|
||||
repoClientset: repoClientset,
|
||||
appRefreshQueue: appRefreshQueue,
|
||||
appOperationQueue: appOperationQueue,
|
||||
appStateManager: appStateManager,
|
||||
appInformer: newApplicationInformer(applicationClientset, appRefreshQueue, appOperationQueue, appResyncPeriod, config),
|
||||
db: db,
|
||||
statusRefreshTimeout: appResyncPeriod,
|
||||
forceRefreshApps: make(map[string]bool),
|
||||
forceRefreshAppsMutex: &sync.Mutex{},
|
||||
auditLogger: argo.NewAuditLogger(namespace, kubeClientset, "application-controller"),
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts the Application CRD controller.
|
||||
func (ctrl *ApplicationController) Run(ctx context.Context, statusProcessors int, operationProcessors int) {
|
||||
defer runtime.HandleCrash()
|
||||
defer ctrl.appRefreshQueue.ShutDown()
|
||||
|
||||
go ctrl.appInformer.Run(ctx.Done())
|
||||
go ctrl.watchAppsResources()
|
||||
|
||||
if !cache.WaitForCacheSync(ctx.Done(), ctrl.appInformer.HasSynced) {
|
||||
log.Error("Timed out waiting for caches to sync")
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < statusProcessors; i++ {
|
||||
go wait.Until(func() {
|
||||
for ctrl.processAppRefreshQueueItem() {
|
||||
}
|
||||
}, time.Second, ctx.Done())
|
||||
}
|
||||
|
||||
for i := 0; i < operationProcessors; i++ {
|
||||
go wait.Until(func() {
|
||||
for ctrl.processAppOperationQueueItem() {
|
||||
}
|
||||
}, time.Second, ctx.Done())
|
||||
}
|
||||
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) forceAppRefresh(appName string) {
|
||||
ctrl.forceRefreshAppsMutex.Lock()
|
||||
defer ctrl.forceRefreshAppsMutex.Unlock()
|
||||
ctrl.forceRefreshApps[appName] = true
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) isRefreshForced(appName string) bool {
|
||||
ctrl.forceRefreshAppsMutex.Lock()
|
||||
defer ctrl.forceRefreshAppsMutex.Unlock()
|
||||
_, ok := ctrl.forceRefreshApps[appName]
|
||||
if ok {
|
||||
delete(ctrl.forceRefreshApps, appName)
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
// watchClusterResources watches for resource changes annotated with application label on specified cluster and schedule corresponding app refresh.
|
||||
func (ctrl *ApplicationController) watchClusterResources(ctx context.Context, item appv1.Cluster) {
|
||||
config := item.RESTConfig()
|
||||
retryUntilSucceed(func() error {
|
||||
ch, err := kube.WatchResourcesWithLabel(ctx, config, "", common.LabelApplicationName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for event := range ch {
|
||||
eventObj := event.Object.(*unstructured.Unstructured)
|
||||
objLabels := eventObj.GetLabels()
|
||||
if objLabels == nil {
|
||||
objLabels = make(map[string]string)
|
||||
}
|
||||
if appName, ok := objLabels[common.LabelApplicationName]; ok {
|
||||
ctrl.forceAppRefresh(appName)
|
||||
ctrl.appRefreshQueue.Add(ctrl.namespace + "/" + appName)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("resource updates channel has closed")
|
||||
}, fmt.Sprintf("watch app resources on %s", config.Host), ctx, watchResourcesRetryTimeout)
|
||||
|
||||
}
|
||||
|
||||
// WatchAppsResources watches for resource changes annotated with application label on all registered clusters and schedule corresponding app refresh.
|
||||
func (ctrl *ApplicationController) watchAppsResources() {
|
||||
watchingClusters := make(map[string]context.CancelFunc)
|
||||
|
||||
retryUntilSucceed(func() error {
|
||||
return ctrl.db.WatchClusters(context.Background(), func(event *db.ClusterEvent) {
|
||||
cancel, ok := watchingClusters[event.Cluster.Server]
|
||||
if event.Type == watch.Deleted && ok {
|
||||
cancel()
|
||||
delete(watchingClusters, event.Cluster.Server)
|
||||
} else if event.Type != watch.Deleted && !ok {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
watchingClusters[event.Cluster.Server] = cancel
|
||||
go ctrl.watchClusterResources(ctx, *event.Cluster)
|
||||
}
|
||||
})
|
||||
}, "watch clusters", context.Background(), watchResourcesRetryTimeout)
|
||||
|
||||
<-context.Background().Done()
|
||||
}
|
||||
|
||||
// retryUntilSucceed keep retrying given action with specified timeout until action succeed or specified context is done.
|
||||
func retryUntilSucceed(action func() error, desc string, ctx context.Context, timeout time.Duration) {
|
||||
ctxCompleted := false
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
ctxCompleted = true
|
||||
}
|
||||
}()
|
||||
for {
|
||||
err := action()
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
if ctxCompleted {
|
||||
log.Infof("Stop retrying %s", desc)
|
||||
return
|
||||
} else {
|
||||
log.Warnf("Failed to %s: %+v, retrying in %v", desc, err, timeout)
|
||||
time.Sleep(timeout)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) processAppOperationQueueItem() (processNext bool) {
|
||||
appKey, shutdown := ctrl.appOperationQueue.Get()
|
||||
if shutdown {
|
||||
processNext = false
|
||||
return
|
||||
} else {
|
||||
processNext = true
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
|
||||
}
|
||||
ctrl.appOperationQueue.Done(appKey)
|
||||
}()
|
||||
|
||||
obj, exists, err := ctrl.appInformer.GetIndexer().GetByKey(appKey.(string))
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get application '%s' from informer index: %+v", appKey, err)
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
// This happens after app was deleted, but the work queue still had an entry for it.
|
||||
return
|
||||
}
|
||||
app, ok := obj.(*appv1.Application)
|
||||
if !ok {
|
||||
log.Warnf("Key '%s' in index is not an application", appKey)
|
||||
return
|
||||
}
|
||||
if app.Operation != nil {
|
||||
ctrl.processRequestedAppOperation(app)
|
||||
} else if app.DeletionTimestamp != nil && app.CascadedDeletion() {
|
||||
ctrl.finalizeApplicationDeletion(app)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Application) {
|
||||
log.Infof("Deleting resources for application %s", app.Name)
|
||||
// Get refreshed application info, since informer app copy might be stale
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Get(app.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if !errors.IsNotFound(err) {
|
||||
log.Errorf("Unable to get refreshed application info prior deleting resources: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
clst, err := ctrl.db.GetCluster(context.Background(), app.Spec.Destination.Server)
|
||||
|
||||
if err == nil {
|
||||
config := clst.RESTConfig()
|
||||
err = kube.DeleteResourceWithLabel(config, app.Spec.Destination.Namespace, common.LabelApplicationName, app.Name)
|
||||
if err == nil {
|
||||
app.SetCascadedDeletion(false)
|
||||
var patch []byte
|
||||
patch, err = json.Marshal(map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"finalizers": app.Finalizers,
|
||||
},
|
||||
})
|
||||
if err == nil {
|
||||
_, err = ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Patch(app.Name, types.MergePatchType, patch)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("Unable to delete application resources: %v", err)
|
||||
ctrl.setAppCondition(app, appv1.ApplicationCondition{
|
||||
Type: appv1.ApplicationConditionDeletionError,
|
||||
Message: err.Error(),
|
||||
})
|
||||
ctrl.auditLogger.LogAppEvent(app, argo.EventInfo{Reason: argo.EventReasonStatusRefreshed, Action: "refresh_status"}, v1.EventTypeWarning)
|
||||
} else {
|
||||
log.Infof("Successfully deleted resources for application %s", app.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) setAppCondition(app *appv1.Application, condition appv1.ApplicationCondition) {
|
||||
index := -1
|
||||
for i, exiting := range app.Status.Conditions {
|
||||
if exiting.Type == condition.Type {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index > -1 {
|
||||
app.Status.Conditions[index] = condition
|
||||
} else {
|
||||
app.Status.Conditions = append(app.Status.Conditions, condition)
|
||||
}
|
||||
var patch []byte
|
||||
patch, err := json.Marshal(map[string]interface{}{
|
||||
"status": map[string]interface{}{
|
||||
"conditions": app.Status.Conditions,
|
||||
},
|
||||
})
|
||||
if err == nil {
|
||||
_, err = ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Patch(app.Name, types.MergePatchType, patch)
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("Unable to set application condition: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Application) {
|
||||
var state *appv1.OperationState
|
||||
// Recover from any unexpected panics and automatically set the status to be failed
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
|
||||
state.Phase = appv1.OperationError
|
||||
if rerr, ok := r.(error); ok {
|
||||
state.Message = rerr.Error()
|
||||
} else {
|
||||
state.Message = fmt.Sprintf("%v", r)
|
||||
}
|
||||
ctrl.setOperationState(app, state)
|
||||
}
|
||||
}()
|
||||
if isOperationInProgress(app) {
|
||||
// If we get here, we are about process an operation but we notice it is already in progress.
|
||||
// We need to detect if the app object we pulled off the informer is stale and doesn't
|
||||
// reflect the fact that the operation is completed. We don't want to perform the operation
|
||||
// again. To detect this, always retrieve the latest version to ensure it is not stale.
|
||||
freshApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(ctrl.namespace).Get(app.ObjectMeta.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
log.Errorf("Failed to retrieve latest application state: %v", err)
|
||||
return
|
||||
}
|
||||
if !isOperationInProgress(freshApp) {
|
||||
log.Infof("Skipping operation on stale application state (%s)", app.ObjectMeta.Name)
|
||||
return
|
||||
}
|
||||
app = freshApp
|
||||
state = app.Status.OperationState.DeepCopy()
|
||||
log.Infof("Resuming in-progress operation. app: %s, phase: %s, message: %s", app.ObjectMeta.Name, state.Phase, state.Message)
|
||||
} else {
|
||||
state = &appv1.OperationState{Phase: appv1.OperationRunning, Operation: *app.Operation, StartedAt: metav1.Now()}
|
||||
ctrl.setOperationState(app, state)
|
||||
log.Infof("Initialized new operation. app: %s, operation: %v", app.ObjectMeta.Name, *app.Operation)
|
||||
}
|
||||
ctrl.appStateManager.SyncAppState(app, state)
|
||||
|
||||
if state.Phase == appv1.OperationRunning {
|
||||
// It's possible for an app to be terminated while we were operating on it. We do not want
|
||||
// to clobber the Terminated state with Running. Get the latest app state to check for this.
|
||||
freshApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(ctrl.namespace).Get(app.ObjectMeta.Name, metav1.GetOptions{})
|
||||
if err == nil {
|
||||
if freshApp.Status.OperationState != nil && freshApp.Status.OperationState.Phase == appv1.OperationTerminating {
|
||||
state.Phase = appv1.OperationTerminating
|
||||
state.Message = "operation is terminating"
|
||||
// after this, we will get requeued to the workqueue, but next time the
|
||||
// SyncAppState will operate in a Terminating phase, allowing the worker to perform
|
||||
// cleanup (e.g. delete jobs, workflows, etc...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctrl.setOperationState(app, state)
|
||||
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.forceAppRefresh(app.ObjectMeta.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) setOperationState(app *appv1.Application, state *appv1.OperationState) {
|
||||
retryUntilSucceed(func() error {
|
||||
if state.Phase == "" {
|
||||
// expose any bugs where we neglect to set phase
|
||||
panic("no phase was set")
|
||||
}
|
||||
if state.Phase.Completed() {
|
||||
now := metav1.Now()
|
||||
state.FinishedAt = &now
|
||||
}
|
||||
patch := map[string]interface{}{
|
||||
"status": map[string]interface{}{
|
||||
"operationState": state,
|
||||
},
|
||||
}
|
||||
if state.Phase.Completed() {
|
||||
// If operation is completed, clear the operation field to indicate no operation is
|
||||
// in progress.
|
||||
patch["operation"] = nil
|
||||
ctrl.auditLogger.LogAppEvent(app, argo.EventInfo{Reason: argo.EventReasonResourceUpdated, Action: "refresh_status"}, v1.EventTypeNormal)
|
||||
}
|
||||
if reflect.DeepEqual(app.Status.OperationState, state) {
|
||||
log.Infof("No operation updates necessary to '%s'. Skipping patch", app.Name)
|
||||
return nil
|
||||
}
|
||||
patchJSON, err := json.Marshal(patch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
appClient := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(ctrl.namespace)
|
||||
_, err = appClient.Patch(app.Name, types.MergePatchType, patchJSON)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("updated '%s' operation (phase: %s)", app.Name, state.Phase)
|
||||
return nil
|
||||
}, "Update application operation state", context.Background(), updateOperationStateTimeout)
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext bool) {
|
||||
appKey, shutdown := ctrl.appRefreshQueue.Get()
|
||||
if shutdown {
|
||||
processNext = false
|
||||
return
|
||||
}
|
||||
processNext = true
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
|
||||
}
|
||||
ctrl.appRefreshQueue.Done(appKey)
|
||||
}()
|
||||
|
||||
obj, exists, err := ctrl.appInformer.GetIndexer().GetByKey(appKey.(string))
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get application '%s' from informer index: %+v", appKey, err)
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
// This happens after app was deleted, but the work queue still had an entry for it.
|
||||
return
|
||||
}
|
||||
app, ok := obj.(*appv1.Application)
|
||||
if !ok {
|
||||
log.Warnf("Key '%s' in index is not an application", appKey)
|
||||
return
|
||||
}
|
||||
if !ctrl.needRefreshAppStatus(app, ctrl.statusRefreshTimeout) {
|
||||
return
|
||||
}
|
||||
|
||||
app = app.DeepCopy()
|
||||
conditions, hasErrors := ctrl.refreshAppConditions(app)
|
||||
if hasErrors {
|
||||
comparisonResult := app.Status.ComparisonResult.DeepCopy()
|
||||
comparisonResult.Status = appv1.ComparisonStatusUnknown
|
||||
health := app.Status.Health.DeepCopy()
|
||||
health.Status = appv1.HealthStatusUnknown
|
||||
ctrl.updateAppStatus(app, comparisonResult, health, nil, conditions)
|
||||
return
|
||||
}
|
||||
|
||||
comparisonResult, manifestInfo, compConditions, err := ctrl.appStateManager.CompareAppState(app, "", nil)
|
||||
if err != nil {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{Type: appv1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
} else {
|
||||
conditions = append(conditions, compConditions...)
|
||||
}
|
||||
|
||||
var parameters []*appv1.ComponentParameter
|
||||
if manifestInfo != nil {
|
||||
parameters = manifestInfo.Params
|
||||
}
|
||||
|
||||
healthState, err := setApplicationHealth(comparisonResult)
|
||||
if err != nil {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{Type: appv1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
}
|
||||
ctrl.updateAppStatus(app, comparisonResult, healthState, parameters, conditions)
|
||||
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 {
|
||||
var reason string
|
||||
if ctrl.isRefreshForced(app.Name) {
|
||||
reason = "force refresh"
|
||||
} else if app.Status.ComparisonResult.Status == appv1.ComparisonStatusUnknown {
|
||||
reason = "comparison status unknown"
|
||||
} else if !app.Spec.Source.Equals(app.Status.ComparisonResult.ComparedTo) {
|
||||
reason = "spec.source differs"
|
||||
} else if app.Status.ComparisonResult.ComparedAt.Add(statusRefreshTimeout).Before(time.Now().UTC()) {
|
||||
reason = fmt.Sprintf("comparison expired. comparedAt: %v, expiry: %v", app.Status.ComparisonResult.ComparedAt, statusRefreshTimeout)
|
||||
}
|
||||
if reason != "" {
|
||||
log.Infof("Refreshing application '%s' status (%s)", app.Name, reason)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) refreshAppConditions(app *appv1.Application) ([]appv1.ApplicationCondition, bool) {
|
||||
conditions := make([]appv1.ApplicationCondition, 0)
|
||||
proj, err := argo.GetAppProject(&app.Spec, ctrl.applicationClientset, ctrl.namespace)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{
|
||||
Type: appv1.ApplicationConditionInvalidSpecError,
|
||||
Message: fmt.Sprintf("Application referencing project %s which does not exist", app.Spec.Project),
|
||||
})
|
||||
} else {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{
|
||||
Type: appv1.ApplicationConditionUnknownError,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
specConditions, err := argo.GetSpecErrors(context.Background(), &app.Spec, proj, ctrl.repoClientset, ctrl.db)
|
||||
if err != nil {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{
|
||||
Type: appv1.ApplicationConditionUnknownError,
|
||||
Message: err.Error(),
|
||||
})
|
||||
} else {
|
||||
conditions = append(conditions, specConditions...)
|
||||
}
|
||||
}
|
||||
|
||||
// List of condition types which have to be reevaluated by controller; all remaining conditions should stay as is.
|
||||
reevaluateTypes := map[appv1.ApplicationConditionType]bool{
|
||||
appv1.ApplicationConditionInvalidSpecError: true,
|
||||
appv1.ApplicationConditionUnknownError: true,
|
||||
appv1.ApplicationConditionComparisonError: true,
|
||||
appv1.ApplicationConditionSharedResourceWarning: true,
|
||||
}
|
||||
appConditions := make([]appv1.ApplicationCondition, 0)
|
||||
for i := 0; i < len(app.Status.Conditions); i++ {
|
||||
condition := app.Status.Conditions[i]
|
||||
if _, ok := reevaluateTypes[condition.Type]; !ok {
|
||||
appConditions = append(appConditions, condition)
|
||||
}
|
||||
}
|
||||
hasErrors := false
|
||||
for i := range conditions {
|
||||
condition := conditions[i]
|
||||
appConditions = append(appConditions, condition)
|
||||
if condition.IsError() {
|
||||
hasErrors = true
|
||||
}
|
||||
|
||||
}
|
||||
return appConditions, hasErrors
|
||||
}
|
||||
|
||||
// setApplicationHealth updates the health statuses of all resources performed in the comparison
|
||||
func setApplicationHealth(comparisonResult *appv1.ComparisonResult) (*appv1.HealthStatus, error) {
|
||||
var savedErr error
|
||||
appHealth := appv1.HealthStatus{Status: appv1.HealthStatusHealthy}
|
||||
if comparisonResult.Status == appv1.ComparisonStatusUnknown {
|
||||
appHealth.Status = appv1.HealthStatusUnknown
|
||||
}
|
||||
for i, resource := range comparisonResult.Resources {
|
||||
if resource.LiveState == "null" {
|
||||
resource.Health = appv1.HealthStatus{Status: appv1.HealthStatusMissing}
|
||||
} else {
|
||||
var obj unstructured.Unstructured
|
||||
err := json.Unmarshal([]byte(resource.LiveState), &obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
healthState, err := health.GetAppHealth(&obj)
|
||||
if err != nil && savedErr == nil {
|
||||
savedErr = err
|
||||
}
|
||||
resource.Health = *healthState
|
||||
}
|
||||
comparisonResult.Resources[i] = resource
|
||||
if health.IsWorse(appHealth.Status, resource.Health.Status) {
|
||||
appHealth.Status = resource.Health.Status
|
||||
}
|
||||
}
|
||||
return &appHealth, savedErr
|
||||
}
|
||||
|
||||
// updateAppStatus persists updates to application status. Detects if there patch
|
||||
func (ctrl *ApplicationController) updateAppStatus(
|
||||
app *appv1.Application,
|
||||
comparisonResult *appv1.ComparisonResult,
|
||||
healthState *appv1.HealthStatus,
|
||||
parameters []*appv1.ComponentParameter,
|
||||
conditions []appv1.ApplicationCondition,
|
||||
) {
|
||||
modifiedApp := app.DeepCopy()
|
||||
if comparisonResult != nil {
|
||||
modifiedApp.Status.ComparisonResult = *comparisonResult
|
||||
log.Infof("App %s comparison result: prev: %s. current: %s", app.Name, app.Status.ComparisonResult.Status, comparisonResult.Status)
|
||||
}
|
||||
if healthState != nil {
|
||||
modifiedApp.Status.Health = *healthState
|
||||
}
|
||||
if parameters != nil {
|
||||
modifiedApp.Status.Parameters = make([]appv1.ComponentParameter, len(parameters))
|
||||
for i := range parameters {
|
||||
modifiedApp.Status.Parameters[i] = *parameters[i]
|
||||
}
|
||||
}
|
||||
if conditions != nil {
|
||||
modifiedApp.Status.Conditions = conditions
|
||||
}
|
||||
origBytes, err := json.Marshal(app)
|
||||
if err != nil {
|
||||
log.Errorf("Error updating application %s (marshal orig app): %v", app.Name, err)
|
||||
return
|
||||
}
|
||||
modifiedBytes, err := json.Marshal(modifiedApp)
|
||||
if err != nil {
|
||||
log.Errorf("Error updating application %s (marshal modified app): %v", app.Name, err)
|
||||
return
|
||||
}
|
||||
patch, err := strategicpatch.CreateTwoWayMergePatch(origBytes, modifiedBytes, appv1.Application{})
|
||||
if err != nil {
|
||||
log.Errorf("Error calculating patch for app %s update: %v", app.Name, err)
|
||||
return
|
||||
}
|
||||
if string(patch) == "{}" {
|
||||
log.Infof("No status changes to %s. Skipping patch", app.Name)
|
||||
return
|
||||
}
|
||||
appClient := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace)
|
||||
_, err = appClient.Patch(app.Name, types.MergePatchType, patch)
|
||||
if err != nil {
|
||||
log.Warnf("Error updating application %s: %v", app.Name, err)
|
||||
} else {
|
||||
log.Infof("Application %s update successful", app.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func newApplicationInformer(
|
||||
appClientset appclientset.Interface,
|
||||
appQueue workqueue.RateLimitingInterface,
|
||||
appOperationQueue workqueue.RateLimitingInterface,
|
||||
appResyncPeriod time.Duration,
|
||||
config *ApplicationControllerConfig) cache.SharedIndexInformer {
|
||||
|
||||
appInformerFactory := appinformers.NewFilteredSharedInformerFactory(
|
||||
appClientset,
|
||||
appResyncPeriod,
|
||||
config.Namespace,
|
||||
func(options *metav1.ListOptions) {
|
||||
var instanceIDReq *labels.Requirement
|
||||
var err error
|
||||
if config.InstanceID != "" {
|
||||
instanceIDReq, err = labels.NewRequirement(common.LabelKeyApplicationControllerInstanceID, selection.Equals, []string{config.InstanceID})
|
||||
} else {
|
||||
instanceIDReq, err = labels.NewRequirement(common.LabelKeyApplicationControllerInstanceID, selection.DoesNotExist, nil)
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
options.FieldSelector = fields.Everything().String()
|
||||
labelSelector := labels.NewSelector().Add(*instanceIDReq)
|
||||
options.LabelSelector = labelSelector.String()
|
||||
},
|
||||
)
|
||||
informer := appInformerFactory.Argoproj().V1alpha1().Applications().Informer()
|
||||
informer.AddEventHandler(
|
||||
cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(obj interface{}) {
|
||||
key, err := cache.MetaNamespaceKeyFunc(obj)
|
||||
if err == nil {
|
||||
appQueue.Add(key)
|
||||
appOperationQueue.Add(key)
|
||||
}
|
||||
},
|
||||
UpdateFunc: func(old, new interface{}) {
|
||||
key, err := cache.MetaNamespaceKeyFunc(new)
|
||||
if err == nil {
|
||||
appQueue.Add(key)
|
||||
appOperationQueue.Add(key)
|
||||
}
|
||||
},
|
||||
DeleteFunc: func(obj interface{}) {
|
||||
// IndexerInformer uses a delta queue, therefore for deletes we have to use this
|
||||
// key function.
|
||||
key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
|
||||
if err == nil {
|
||||
appQueue.Add(key)
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
return informer
|
||||
}
|
||||
|
||||
func isOperationInProgress(app *appv1.Application) bool {
|
||||
return app.Status.OperationState != nil && !app.Status.OperationState.Phase.Completed()
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/server/cluster"
|
||||
"github.com/argoproj/argo-cd/util/diff"
|
||||
kubeutil "github.com/argoproj/argo-cd/util/kube"
|
||||
log "github.com/sirupsen/logrus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
)
|
||||
|
||||
// AppComparator defines methods which allow to compare application spec and actual application state.
|
||||
type AppComparator interface {
|
||||
CompareAppState(server string, namespace string, targetObjs []*unstructured.Unstructured, app *v1alpha1.Application) (*v1alpha1.ComparisonResult, error)
|
||||
}
|
||||
|
||||
// KsonnetAppComparator allows to compare application using KSonnet CLI
|
||||
type KsonnetAppComparator struct {
|
||||
clusterService cluster.ClusterServiceServer
|
||||
}
|
||||
|
||||
// CompareAppState compares application spec and real app state using KSonnet
|
||||
func (ks *KsonnetAppComparator) CompareAppState(
|
||||
server string,
|
||||
namespace string,
|
||||
targetObjs []*unstructured.Unstructured,
|
||||
app *v1alpha1.Application) (*v1alpha1.ComparisonResult, error) {
|
||||
|
||||
log.Infof("Comparing app %s state in cluster %s (namespace: %s)", app.ObjectMeta.Name, server, namespace)
|
||||
// Get the REST config for the cluster corresponding to the environment
|
||||
clst, err := ks.clusterService.Get(context.Background(), &cluster.ClusterQuery{Server: server})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Retrieve the live versions of the objects
|
||||
liveObjs, err := kubeutil.GetLiveResources(clst.RESTConfig(), targetObjs, namespace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Do the actual comparison
|
||||
diffResults, err := diff.DiffArray(targetObjs, liveObjs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resources := make([]v1alpha1.ResourceState, len(targetObjs))
|
||||
for i := 0; i < len(targetObjs); i++ {
|
||||
resState := v1alpha1.ResourceState{}
|
||||
targetObjBytes, err := json.Marshal(targetObjs[i].Object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resState.TargetState = string(targetObjBytes)
|
||||
if liveObjs[i] == nil {
|
||||
resState.LiveState = "null"
|
||||
} else {
|
||||
liveObjBytes, err := json.Marshal(liveObjs[i].Object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resState.LiveState = string(liveObjBytes)
|
||||
}
|
||||
diffResult := diffResults.Diffs[i]
|
||||
if diffResult.Modified {
|
||||
resState.Status = v1alpha1.ComparisonStatusOutOfSync
|
||||
} else {
|
||||
resState.Status = v1alpha1.ComparisonStatusSynced
|
||||
}
|
||||
resources[i] = resState
|
||||
}
|
||||
compResult := v1alpha1.ComparisonResult{
|
||||
ComparedTo: app.Spec.Source,
|
||||
ComparedAt: metav1.Time{Time: time.Now().UTC()},
|
||||
Server: clst.Server,
|
||||
Namespace: namespace,
|
||||
Resources: resources,
|
||||
}
|
||||
if diffResults.Modified {
|
||||
compResult.Status = v1alpha1.ComparisonStatusOutOfSync
|
||||
} else {
|
||||
compResult.Status = v1alpha1.ComparisonStatusSynced
|
||||
}
|
||||
return &compResult, nil
|
||||
}
|
||||
|
||||
// NewKsonnetAppComparator creates new instance of Ksonnet app comparator
|
||||
func NewKsonnetAppComparator(clusterService cluster.ClusterServiceServer) AppComparator {
|
||||
return &KsonnetAppComparator{
|
||||
clusterService: clusterService,
|
||||
}
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
appinformers "github.com/argoproj/argo-cd/pkg/client/informers/externalversions"
|
||||
"github.com/argoproj/argo-cd/reposerver"
|
||||
"github.com/argoproj/argo-cd/reposerver/repository"
|
||||
apireposerver "github.com/argoproj/argo-cd/server/repository"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
argoutil "github.com/argoproj/argo-cd/util/argo"
|
||||
log "github.com/sirupsen/logrus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
)
|
||||
|
||||
// ApplicationController is the controller for application resources.
|
||||
type ApplicationController struct {
|
||||
repoClientset reposerver.Clientset
|
||||
kubeClientset kubernetes.Interface
|
||||
applicationClientset appclientset.Interface
|
||||
appQueue workqueue.RateLimitingInterface
|
||||
appInformer cache.SharedIndexInformer
|
||||
appComparator AppComparator
|
||||
statusRefreshTimeout time.Duration
|
||||
apiRepoService apireposerver.RepositoryServiceServer
|
||||
}
|
||||
|
||||
type ApplicationControllerConfig struct {
|
||||
InstanceID string
|
||||
Namespace string
|
||||
}
|
||||
|
||||
// NewApplicationController creates new instance of ApplicationController.
|
||||
func NewApplicationController(
|
||||
kubeClientset kubernetes.Interface,
|
||||
applicationClientset appclientset.Interface,
|
||||
repoClientset reposerver.Clientset,
|
||||
apiRepoService apireposerver.RepositoryServiceServer,
|
||||
appComparator AppComparator,
|
||||
appResyncPeriod time.Duration,
|
||||
config *ApplicationControllerConfig,
|
||||
) *ApplicationController {
|
||||
appQueue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())
|
||||
return &ApplicationController{
|
||||
kubeClientset: kubeClientset,
|
||||
applicationClientset: applicationClientset,
|
||||
repoClientset: repoClientset,
|
||||
appQueue: appQueue,
|
||||
apiRepoService: apiRepoService,
|
||||
appComparator: appComparator,
|
||||
appInformer: newApplicationInformer(applicationClientset, appQueue, appResyncPeriod, config),
|
||||
statusRefreshTimeout: appResyncPeriod,
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts the Application CRD controller.
|
||||
func (ctrl *ApplicationController) Run(ctx context.Context, appWorkers int) {
|
||||
defer runtime.HandleCrash()
|
||||
defer ctrl.appQueue.ShutDown()
|
||||
|
||||
go ctrl.appInformer.Run(ctx.Done())
|
||||
|
||||
if !cache.WaitForCacheSync(ctx.Done(), ctrl.appInformer.HasSynced) {
|
||||
log.Error("Timed out waiting for caches to sync")
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < appWorkers; i++ {
|
||||
go wait.Until(ctrl.runWorker, time.Second, ctx.Done())
|
||||
}
|
||||
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) processNextItem() bool {
|
||||
appKey, shutdown := ctrl.appQueue.Get()
|
||||
if shutdown {
|
||||
return false
|
||||
}
|
||||
|
||||
defer ctrl.appQueue.Done(appKey)
|
||||
|
||||
obj, exists, err := ctrl.appInformer.GetIndexer().GetByKey(appKey.(string))
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get application '%s' from informer index: %+v", appKey, err)
|
||||
return true
|
||||
}
|
||||
if !exists {
|
||||
// This happens after app was deleted, but the work queue still had an entry for it.
|
||||
return true
|
||||
}
|
||||
app, ok := obj.(*appv1.Application)
|
||||
if !ok {
|
||||
log.Warnf("Key '%s' in index is not an application", appKey)
|
||||
return true
|
||||
}
|
||||
|
||||
if app.NeedRefreshAppStatus(ctrl.statusRefreshTimeout) {
|
||||
updatedApp := app.DeepCopy()
|
||||
status, err := ctrl.tryRefreshAppStatus(updatedApp)
|
||||
if err != nil {
|
||||
updatedApp.Status.ComparisonResult = appv1.ComparisonResult{
|
||||
Status: appv1.ComparisonStatusError,
|
||||
Error: fmt.Sprintf("Failed to get application status for application '%s': %v", app.Name, err),
|
||||
ComparedTo: app.Spec.Source,
|
||||
ComparedAt: metav1.Time{Time: time.Now().UTC()},
|
||||
}
|
||||
} else {
|
||||
updatedApp.Status = *status
|
||||
}
|
||||
ctrl.persistApp(updatedApp)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) tryRefreshAppStatus(app *appv1.Application) (*appv1.ApplicationStatus, error) {
|
||||
conn, client, err := ctrl.repoClientset.NewRepositoryClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer util.Close(conn)
|
||||
repo, err := ctrl.apiRepoService.Get(context.Background(), &apireposerver.RepoQuery{Repo: app.Spec.Source.RepoURL})
|
||||
if err != nil {
|
||||
// If we couldn't retrieve from the repo service, assume public repositories
|
||||
repo = &appv1.Repository{
|
||||
Repo: app.Spec.Source.RepoURL,
|
||||
Username: "",
|
||||
Password: "",
|
||||
}
|
||||
}
|
||||
overrides := make([]*appv1.ComponentParameter, len(app.Spec.Source.ComponentParameterOverrides))
|
||||
if app.Spec.Source.ComponentParameterOverrides != nil {
|
||||
for i := range app.Spec.Source.ComponentParameterOverrides {
|
||||
item := app.Spec.Source.ComponentParameterOverrides[i]
|
||||
overrides[i] = &item
|
||||
}
|
||||
}
|
||||
revision := app.Spec.Source.TargetRevision
|
||||
manifestInfo, err := client.GenerateManifest(context.Background(), &repository.ManifestRequest{
|
||||
Repo: repo,
|
||||
Revision: revision,
|
||||
Path: app.Spec.Source.Path,
|
||||
Environment: app.Spec.Source.Environment,
|
||||
AppLabel: app.Name,
|
||||
ComponentParameterOverrides: overrides,
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("Failed to load application manifest %v", err)
|
||||
return nil, err
|
||||
}
|
||||
targetObjs := make([]*unstructured.Unstructured, len(manifestInfo.Manifests))
|
||||
for i, manifestStr := range manifestInfo.Manifests {
|
||||
var obj unstructured.Unstructured
|
||||
if err := json.Unmarshal([]byte(manifestStr), &obj); err != nil {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
targetObjs[i] = &obj
|
||||
}
|
||||
|
||||
server, namespace := argoutil.ResolveServerNamespace(app.Spec.Destination, manifestInfo)
|
||||
comparisonResult, err := ctrl.appComparator.CompareAppState(server, namespace, targetObjs, app)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Infof("App %s comparison result: prev: %s. current: %s", app.Name, app.Status.ComparisonResult.Status, comparisonResult.Status)
|
||||
newStatus := app.Status
|
||||
newStatus.ComparisonResult = *comparisonResult
|
||||
return &newStatus, nil
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) runWorker() {
|
||||
for ctrl.processNextItem() {
|
||||
}
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) persistApp(app *appv1.Application) {
|
||||
appClient := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.ObjectMeta.Namespace)
|
||||
_, err := appClient.Update(app)
|
||||
if err != nil {
|
||||
log.Warnf("Error updating application: %v", err)
|
||||
}
|
||||
log.Info("Application update successful")
|
||||
}
|
||||
|
||||
func newApplicationInformer(
|
||||
appClientset appclientset.Interface, appQueue workqueue.RateLimitingInterface, appResyncPeriod time.Duration, config *ApplicationControllerConfig) cache.SharedIndexInformer {
|
||||
|
||||
appInformerFactory := appinformers.NewFilteredSharedInformerFactory(
|
||||
appClientset,
|
||||
appResyncPeriod,
|
||||
config.Namespace,
|
||||
func(options *metav1.ListOptions) {
|
||||
var instanceIDReq *labels.Requirement
|
||||
var err error
|
||||
if config.InstanceID != "" {
|
||||
instanceIDReq, err = labels.NewRequirement(common.LabelKeyApplicationControllerInstanceID, selection.Equals, []string{config.InstanceID})
|
||||
} else {
|
||||
instanceIDReq, err = labels.NewRequirement(common.LabelKeyApplicationControllerInstanceID, selection.DoesNotExist, nil)
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
options.FieldSelector = fields.Everything().String()
|
||||
labelSelector := labels.NewSelector().Add(*instanceIDReq)
|
||||
options.LabelSelector = labelSelector.String()
|
||||
},
|
||||
)
|
||||
informer := appInformerFactory.Argoproj().V1alpha1().Applications().Informer()
|
||||
informer.AddEventHandler(
|
||||
cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(obj interface{}) {
|
||||
key, err := cache.MetaNamespaceKeyFunc(obj)
|
||||
if err == nil {
|
||||
appQueue.Add(key)
|
||||
}
|
||||
},
|
||||
UpdateFunc: func(old, new interface{}) {
|
||||
key, err := cache.MetaNamespaceKeyFunc(new)
|
||||
if err == nil {
|
||||
appQueue.Add(key)
|
||||
}
|
||||
},
|
||||
DeleteFunc: func(obj interface{}) {
|
||||
// IndexerInformer uses a delta queue, therefore for deletes we have to use this
|
||||
// key function.
|
||||
key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
|
||||
if err == nil {
|
||||
appQueue.Add(key)
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
return informer
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// Code generated by mockery v1.0.0
|
||||
package mocks
|
||||
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
import v1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
|
||||
// AppComparator is an autogenerated mock type for the AppComparator type
|
||||
type AppComparator struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// CompareAppState provides a mock function with given fields: appRepoPath, app
|
||||
func (_m *AppComparator) CompareAppState(appRepoPath string, app *v1alpha1.Application) (*v1alpha1.ComparisonResult, error) {
|
||||
ret := _m.Called(appRepoPath, app)
|
||||
|
||||
var r0 *v1alpha1.ComparisonResult
|
||||
if rf, ok := ret.Get(0).(func(string, *v1alpha1.Application) *v1alpha1.ComparisonResult); ok {
|
||||
r0 = rf(appRepoPath, app)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*v1alpha1.ComparisonResult)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, *v1alpha1.Application) error); ok {
|
||||
r1 = rf(appRepoPath, app)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
205
controller/secretcontroller.go
Normal file
@@ -0,0 +1,205 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/reposerver"
|
||||
"github.com/argoproj/argo-cd/reposerver/repository"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
log "github.com/sirupsen/logrus"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/selection"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
)
|
||||
|
||||
type SecretController struct {
|
||||
kubeClient kubernetes.Interface
|
||||
secretQueue workqueue.RateLimitingInterface
|
||||
secretInformer cache.SharedIndexInformer
|
||||
repoClientset reposerver.Clientset
|
||||
namespace string
|
||||
}
|
||||
|
||||
func (ctrl *SecretController) Run(ctx context.Context) {
|
||||
go ctrl.secretInformer.Run(ctx.Done())
|
||||
if !cache.WaitForCacheSync(ctx.Done(), ctrl.secretInformer.HasSynced) {
|
||||
log.Error("Timed out waiting for caches to sync")
|
||||
return
|
||||
}
|
||||
go wait.Until(func() {
|
||||
for ctrl.processSecret() {
|
||||
}
|
||||
}, time.Second, ctx.Done())
|
||||
}
|
||||
|
||||
func (ctrl *SecretController) processSecret() (processNext bool) {
|
||||
secretKey, shutdown := ctrl.secretQueue.Get()
|
||||
if shutdown {
|
||||
processNext = false
|
||||
return
|
||||
} else {
|
||||
processNext = true
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
|
||||
}
|
||||
ctrl.secretQueue.Done(secretKey)
|
||||
}()
|
||||
obj, exists, err := ctrl.secretInformer.GetIndexer().GetByKey(secretKey.(string))
|
||||
if err != nil {
|
||||
log.Errorf("Failed to get secret '%s' from informer index: %+v", secretKey, err)
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
// This happens after secret was deleted, but the work queue still had an entry for it.
|
||||
return
|
||||
}
|
||||
secret, ok := obj.(*corev1.Secret)
|
||||
if !ok {
|
||||
log.Warnf("Key '%s' in index is not an secret", secretKey)
|
||||
return
|
||||
}
|
||||
|
||||
if secret.Labels[common.LabelKeySecretType] == common.SecretTypeCluster {
|
||||
cluster := db.SecretToCluster(secret)
|
||||
ctrl.updateState(secret, ctrl.getClusterState(cluster))
|
||||
} else if secret.Labels[common.LabelKeySecretType] == common.SecretTypeRepository {
|
||||
repo := db.SecretToRepo(secret)
|
||||
ctrl.updateState(secret, ctrl.getRepoConnectionState(repo))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (ctrl *SecretController) getRepoConnectionState(repo *v1alpha1.Repository) v1alpha1.ConnectionState {
|
||||
state := v1alpha1.ConnectionState{
|
||||
ModifiedAt: repo.ConnectionState.ModifiedAt,
|
||||
Status: v1alpha1.ConnectionStatusUnknown,
|
||||
}
|
||||
closer, client, err := ctrl.repoClientset.NewRepositoryClient()
|
||||
if err != nil {
|
||||
log.Errorf("Unable to create repository client: %v", err)
|
||||
return state
|
||||
}
|
||||
|
||||
defer util.Close(closer)
|
||||
|
||||
_, err = client.ListDir(context.Background(), &repository.ListDirRequest{Repo: repo, Path: ".gitignore"})
|
||||
if err == nil {
|
||||
state.Status = v1alpha1.ConnectionStatusSuccessful
|
||||
} else {
|
||||
state.Status = v1alpha1.ConnectionStatusFailed
|
||||
state.Message = err.Error()
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
func (ctrl *SecretController) getClusterState(cluster *v1alpha1.Cluster) v1alpha1.ConnectionState {
|
||||
state := v1alpha1.ConnectionState{
|
||||
ModifiedAt: cluster.ConnectionState.ModifiedAt,
|
||||
Status: v1alpha1.ConnectionStatusUnknown,
|
||||
}
|
||||
kubeClientset, err := kubernetes.NewForConfig(cluster.RESTConfig())
|
||||
if err == nil {
|
||||
_, err = kubeClientset.Discovery().ServerVersion()
|
||||
}
|
||||
if err == nil {
|
||||
state.Status = v1alpha1.ConnectionStatusSuccessful
|
||||
} else {
|
||||
state.Status = v1alpha1.ConnectionStatusFailed
|
||||
state.Message = err.Error()
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
func (ctrl *SecretController) updateState(secret *corev1.Secret, state v1alpha1.ConnectionState) {
|
||||
annotationsPatch := make(map[string]string)
|
||||
for key, value := range db.AnnotationsFromConnectionState(&state) {
|
||||
if secret.Annotations[key] != value {
|
||||
annotationsPatch[key] = value
|
||||
}
|
||||
}
|
||||
if len(annotationsPatch) > 0 {
|
||||
annotationsPatch[common.AnnotationConnectionModifiedAt] = metav1.Now().Format(time.RFC3339)
|
||||
patchData, err := json.Marshal(map[string]interface{}{
|
||||
"metadata": map[string]interface{}{
|
||||
"annotations": annotationsPatch,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Warnf("Unable to prepare secret state annotation patch: %v", err)
|
||||
} else {
|
||||
_, err := ctrl.kubeClient.CoreV1().Secrets(secret.Namespace).Patch(secret.Name, types.MergePatchType, patchData)
|
||||
if err != nil {
|
||||
log.Warnf("Unable to patch secret state annotation: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newSecretInformer(client kubernetes.Interface, resyncPeriod time.Duration, namespace string, secretQueue workqueue.RateLimitingInterface) cache.SharedIndexInformer {
|
||||
informerFactory := informers.NewFilteredSharedInformerFactory(
|
||||
client,
|
||||
resyncPeriod,
|
||||
namespace,
|
||||
func(options *metav1.ListOptions) {
|
||||
var req *labels.Requirement
|
||||
req, err := labels.NewRequirement(common.LabelKeySecretType, selection.In, []string{common.SecretTypeCluster, common.SecretTypeRepository})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
options.FieldSelector = fields.Everything().String()
|
||||
labelSelector := labels.NewSelector().Add(*req)
|
||||
options.LabelSelector = labelSelector.String()
|
||||
},
|
||||
)
|
||||
informer := informerFactory.Core().V1().Secrets().Informer()
|
||||
informer.AddEventHandler(
|
||||
cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(obj interface{}) {
|
||||
key, err := cache.MetaNamespaceKeyFunc(obj)
|
||||
if err == nil {
|
||||
secretQueue.Add(key)
|
||||
}
|
||||
},
|
||||
UpdateFunc: func(old, new interface{}) {
|
||||
key, err := cache.MetaNamespaceKeyFunc(new)
|
||||
if err == nil {
|
||||
secretQueue.Add(key)
|
||||
}
|
||||
},
|
||||
},
|
||||
)
|
||||
return informer
|
||||
}
|
||||
|
||||
func NewSecretController(kubeClient kubernetes.Interface, repoClientset reposerver.Clientset, resyncPeriod time.Duration, namespace string) *SecretController {
|
||||
secretQueue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())
|
||||
return &SecretController{
|
||||
kubeClient: kubeClient,
|
||||
secretQueue: secretQueue,
|
||||
secretInformer: newSecretInformer(kubeClient, resyncPeriod, namespace, secretQueue),
|
||||
namespace: namespace,
|
||||
repoClientset: repoClientset,
|
||||
}
|
||||
}
|
||||
421
controller/state.go
Normal file
@@ -0,0 +1,421 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/dynamic"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
"github.com/argoproj/argo-cd/reposerver"
|
||||
"github.com/argoproj/argo-cd/reposerver/repository"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/diff"
|
||||
kubeutil "github.com/argoproj/argo-cd/util/kube"
|
||||
)
|
||||
|
||||
const (
|
||||
maxHistoryCnt = 5
|
||||
)
|
||||
|
||||
// AppStateManager defines methods which allow to compare application spec and actual application state.
|
||||
type AppStateManager interface {
|
||||
CompareAppState(app *v1alpha1.Application, revision string, overrides []v1alpha1.ComponentParameter) (
|
||||
*v1alpha1.ComparisonResult, *repository.ManifestResponse, []v1alpha1.ApplicationCondition, error)
|
||||
SyncAppState(app *v1alpha1.Application, state *v1alpha1.OperationState)
|
||||
}
|
||||
|
||||
// ksonnetAppStateManager allows to compare application using KSonnet CLI
|
||||
type ksonnetAppStateManager struct {
|
||||
db db.ArgoDB
|
||||
appclientset appclientset.Interface
|
||||
repoClientset reposerver.Clientset
|
||||
namespace string
|
||||
}
|
||||
|
||||
// groupLiveObjects deduplicate list of kubernetes resources and choose correct version of resource: if resource has corresponding expected application resource then method pick
|
||||
// kubernetes resource with matching version, otherwise chooses single kubernetes resource with any version
|
||||
func groupLiveObjects(liveObjs []*unstructured.Unstructured, targetObjs []*unstructured.Unstructured) map[string]*unstructured.Unstructured {
|
||||
targetByFullName := make(map[string]*unstructured.Unstructured)
|
||||
for _, obj := range targetObjs {
|
||||
targetByFullName[getResourceFullName(obj)] = obj
|
||||
}
|
||||
|
||||
liveListByFullName := make(map[string][]*unstructured.Unstructured)
|
||||
for _, obj := range liveObjs {
|
||||
list := liveListByFullName[getResourceFullName(obj)]
|
||||
if list == nil {
|
||||
list = make([]*unstructured.Unstructured, 0)
|
||||
}
|
||||
list = append(list, obj)
|
||||
liveListByFullName[getResourceFullName(obj)] = list
|
||||
}
|
||||
|
||||
liveByFullName := make(map[string]*unstructured.Unstructured)
|
||||
|
||||
for fullName, list := range liveListByFullName {
|
||||
targetObj := targetByFullName[fullName]
|
||||
var liveObj *unstructured.Unstructured
|
||||
if targetObj != nil {
|
||||
for i := range list {
|
||||
if list[i].GetAPIVersion() == targetObj.GetAPIVersion() {
|
||||
liveObj = list[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
liveObj = list[0]
|
||||
}
|
||||
if liveObj != nil {
|
||||
liveByFullName[getResourceFullName(liveObj)] = liveObj
|
||||
}
|
||||
}
|
||||
return liveByFullName
|
||||
}
|
||||
|
||||
func (s *ksonnetAppStateManager) getTargetObjs(app *v1alpha1.Application, revision string, overrides []v1alpha1.ComponentParameter) ([]*unstructured.Unstructured, *repository.ManifestResponse, error) {
|
||||
repo := s.getRepo(app.Spec.Source.RepoURL)
|
||||
conn, repoClient, err := s.repoClientset.NewRepositoryClient()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer util.Close(conn)
|
||||
|
||||
if revision == "" {
|
||||
revision = app.Spec.Source.TargetRevision
|
||||
}
|
||||
|
||||
// Decide what overrides to compare with.
|
||||
var mfReqOverrides []*v1alpha1.ComponentParameter
|
||||
if overrides != nil {
|
||||
// If overrides is supplied, use that
|
||||
mfReqOverrides = make([]*v1alpha1.ComponentParameter, len(overrides))
|
||||
for i := range overrides {
|
||||
item := overrides[i]
|
||||
mfReqOverrides[i] = &item
|
||||
}
|
||||
} else {
|
||||
// Otherwise, use the overrides in the app spec
|
||||
mfReqOverrides = make([]*v1alpha1.ComponentParameter, len(app.Spec.Source.ComponentParameterOverrides))
|
||||
for i := range app.Spec.Source.ComponentParameterOverrides {
|
||||
item := app.Spec.Source.ComponentParameterOverrides[i]
|
||||
mfReqOverrides[i] = &item
|
||||
}
|
||||
}
|
||||
|
||||
manifestInfo, err := repoClient.GenerateManifest(context.Background(), &repository.ManifestRequest{
|
||||
Repo: repo,
|
||||
Environment: app.Spec.Source.Environment,
|
||||
Path: app.Spec.Source.Path,
|
||||
Revision: revision,
|
||||
ComponentParameterOverrides: mfReqOverrides,
|
||||
AppLabel: app.Name,
|
||||
ValueFiles: app.Spec.Source.ValuesFiles,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
targetObjs := make([]*unstructured.Unstructured, 0)
|
||||
for _, manifest := range manifestInfo.Manifests {
|
||||
obj, err := v1alpha1.UnmarshalToUnstructured(manifest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if isHook(obj) {
|
||||
continue
|
||||
}
|
||||
targetObjs = append(targetObjs, obj)
|
||||
}
|
||||
return targetObjs, manifestInfo, nil
|
||||
}
|
||||
|
||||
func (s *ksonnetAppStateManager) getLiveObjs(app *v1alpha1.Application, targetObjs []*unstructured.Unstructured) (
|
||||
[]*unstructured.Unstructured, map[string]*unstructured.Unstructured, error) {
|
||||
|
||||
// Get the REST config for the cluster corresponding to the environment
|
||||
clst, err := s.db.GetCluster(context.Background(), app.Spec.Destination.Server)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
restConfig := clst.RESTConfig()
|
||||
|
||||
// Retrieve the live versions of the objects. exclude any hook objects
|
||||
labeledObjs, err := kubeutil.GetResourcesWithLabel(restConfig, app.Spec.Destination.Namespace, common.LabelApplicationName, app.Name)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
liveObjs := make([]*unstructured.Unstructured, 0)
|
||||
for _, obj := range labeledObjs {
|
||||
if isHook(obj) {
|
||||
continue
|
||||
}
|
||||
liveObjs = append(liveObjs, obj)
|
||||
}
|
||||
|
||||
liveObjByFullName := groupLiveObjects(liveObjs, targetObjs)
|
||||
|
||||
controlledLiveObj := make([]*unstructured.Unstructured, len(targetObjs))
|
||||
|
||||
// Move live resources which have corresponding target object to controlledLiveObj
|
||||
dynClientPool := dynamic.NewDynamicClientPool(restConfig)
|
||||
disco, err := discovery.NewDiscoveryClientForConfig(restConfig)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for i, targetObj := range targetObjs {
|
||||
fullName := getResourceFullName(targetObj)
|
||||
liveObj := liveObjByFullName[fullName]
|
||||
if liveObj == nil && targetObj.GetName() != "" {
|
||||
// If we get here, it indicates we did not find the live resource when querying using
|
||||
// our app label. However, it is possible that the resource was created/modified outside
|
||||
// of ArgoCD. In order to determine that it is truly missing, we fall back to perform a
|
||||
// direct lookup of the resource by name. See issue #141
|
||||
gvk := targetObj.GroupVersionKind()
|
||||
dclient, err := dynClientPool.ClientForGroupVersionKind(gvk)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
apiResource, err := kubeutil.ServerResourceForGroupVersionKind(disco, gvk)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
liveObj, err = kubeutil.GetLiveResource(dclient, targetObj, apiResource, app.Spec.Destination.Namespace)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
controlledLiveObj[i] = liveObj
|
||||
delete(liveObjByFullName, fullName)
|
||||
}
|
||||
|
||||
return controlledLiveObj, liveObjByFullName, nil
|
||||
}
|
||||
|
||||
// CompareAppState compares application git state to the live app state, using the specified
|
||||
// revision and supplied overrides. If revision or overrides are empty, then compares against
|
||||
// revision and overrides in the app spec.
|
||||
func (s *ksonnetAppStateManager) CompareAppState(app *v1alpha1.Application, revision string, overrides []v1alpha1.ComponentParameter) (
|
||||
*v1alpha1.ComparisonResult, *repository.ManifestResponse, []v1alpha1.ApplicationCondition, error) {
|
||||
|
||||
failedToLoadObjs := false
|
||||
conditions := make([]v1alpha1.ApplicationCondition, 0)
|
||||
targetObjs, manifestInfo, err := s.getTargetObjs(app, revision, overrides)
|
||||
if err != nil {
|
||||
targetObjs = make([]*unstructured.Unstructured, 0)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
failedToLoadObjs = true
|
||||
}
|
||||
|
||||
controlledLiveObj, liveObjByFullName, err := s.getLiveObjs(app, targetObjs)
|
||||
if err != nil {
|
||||
controlledLiveObj = make([]*unstructured.Unstructured, len(targetObjs))
|
||||
liveObjByFullName = make(map[string]*unstructured.Unstructured)
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
failedToLoadObjs = true
|
||||
}
|
||||
|
||||
for _, liveObj := range controlledLiveObj {
|
||||
if liveObj != nil && liveObj.GetLabels() != nil {
|
||||
if appLabelVal, ok := liveObj.GetLabels()[common.LabelApplicationName]; ok && appLabelVal != "" && appLabelVal != app.Name {
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{
|
||||
Type: v1alpha1.ApplicationConditionSharedResourceWarning,
|
||||
Message: fmt.Sprintf("Resource %s/%s is controller by applications '%s' and '%s'", liveObj.GetKind(), liveObj.GetName(), app.Name, appLabelVal),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move root level live resources to controlledLiveObj and add nil to targetObjs to indicate that target object is missing
|
||||
for fullName := range liveObjByFullName {
|
||||
liveObj := liveObjByFullName[fullName]
|
||||
if !hasParent(liveObj) {
|
||||
targetObjs = append(targetObjs, nil)
|
||||
controlledLiveObj = append(controlledLiveObj, liveObj)
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("Comparing app %s state in cluster %s (namespace: %s)", app.ObjectMeta.Name, app.Spec.Destination.Server, app.Spec.Destination.Namespace)
|
||||
|
||||
// Do the actual comparison
|
||||
diffResults, err := diff.DiffArray(targetObjs, controlledLiveObj)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
comparisonStatus := v1alpha1.ComparisonStatusSynced
|
||||
|
||||
resources := make([]v1alpha1.ResourceState, len(targetObjs))
|
||||
for i := 0; i < len(targetObjs); i++ {
|
||||
resState := v1alpha1.ResourceState{
|
||||
ChildLiveResources: make([]v1alpha1.ResourceNode, 0),
|
||||
}
|
||||
diffResult := diffResults.Diffs[i]
|
||||
if diffResult.Modified {
|
||||
// Set resource state to 'OutOfSync' since target and corresponding live resource are different
|
||||
resState.Status = v1alpha1.ComparisonStatusOutOfSync
|
||||
comparisonStatus = v1alpha1.ComparisonStatusOutOfSync
|
||||
} else {
|
||||
resState.Status = v1alpha1.ComparisonStatusSynced
|
||||
}
|
||||
|
||||
if targetObjs[i] == nil {
|
||||
resState.TargetState = "null"
|
||||
// Set resource state to 'OutOfSync' since target resource is missing and live resource is unexpected
|
||||
resState.Status = v1alpha1.ComparisonStatusOutOfSync
|
||||
comparisonStatus = v1alpha1.ComparisonStatusOutOfSync
|
||||
} else {
|
||||
targetObjBytes, err := json.Marshal(targetObjs[i].Object)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
resState.TargetState = string(targetObjBytes)
|
||||
}
|
||||
|
||||
if controlledLiveObj[i] == nil {
|
||||
resState.LiveState = "null"
|
||||
// Set resource state to 'OutOfSync' since target resource present but corresponding live resource is missing
|
||||
resState.Status = v1alpha1.ComparisonStatusOutOfSync
|
||||
comparisonStatus = v1alpha1.ComparisonStatusOutOfSync
|
||||
} else {
|
||||
liveObjBytes, err := json.Marshal(controlledLiveObj[i].Object)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
resState.LiveState = string(liveObjBytes)
|
||||
}
|
||||
|
||||
resources[i] = resState
|
||||
}
|
||||
|
||||
for i, resource := range resources {
|
||||
liveResource := controlledLiveObj[i]
|
||||
if liveResource != nil {
|
||||
childResources, err := getChildren(liveResource, liveObjByFullName)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
resource.ChildLiveResources = childResources
|
||||
resources[i] = resource
|
||||
}
|
||||
}
|
||||
if failedToLoadObjs {
|
||||
comparisonStatus = v1alpha1.ComparisonStatusUnknown
|
||||
}
|
||||
compResult := v1alpha1.ComparisonResult{
|
||||
ComparedTo: app.Spec.Source,
|
||||
ComparedAt: metav1.Time{Time: time.Now().UTC()},
|
||||
Resources: resources,
|
||||
Status: comparisonStatus,
|
||||
}
|
||||
return &compResult, manifestInfo, conditions, nil
|
||||
}
|
||||
|
||||
func hasParent(obj *unstructured.Unstructured) bool {
|
||||
// TODO: remove special case after Service and Endpoint get explicit relationship ( https://github.com/kubernetes/kubernetes/issues/28483 )
|
||||
return obj.GetKind() == kubeutil.EndpointsKind || metav1.GetControllerOf(obj) != nil
|
||||
}
|
||||
|
||||
func isControlledBy(obj *unstructured.Unstructured, parent *unstructured.Unstructured) bool {
|
||||
// TODO: remove special case after Service and Endpoint get explicit relationship ( https://github.com/kubernetes/kubernetes/issues/28483 )
|
||||
if obj.GetKind() == kubeutil.EndpointsKind && parent.GetKind() == kubeutil.ServiceKind {
|
||||
return obj.GetName() == parent.GetName()
|
||||
}
|
||||
return metav1.IsControlledBy(obj, parent)
|
||||
}
|
||||
|
||||
func getChildren(parent *unstructured.Unstructured, liveObjByFullName map[string]*unstructured.Unstructured) ([]v1alpha1.ResourceNode, error) {
|
||||
children := make([]v1alpha1.ResourceNode, 0)
|
||||
for fullName, obj := range liveObjByFullName {
|
||||
if isControlledBy(obj, parent) {
|
||||
delete(liveObjByFullName, fullName)
|
||||
childResource := v1alpha1.ResourceNode{}
|
||||
json, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
childResource.State = string(json)
|
||||
childResourceChildren, err := getChildren(obj, liveObjByFullName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
childResource.Children = childResourceChildren
|
||||
children = append(children, childResource)
|
||||
}
|
||||
}
|
||||
return children, nil
|
||||
}
|
||||
|
||||
func getResourceFullName(obj *unstructured.Unstructured) string {
|
||||
return fmt.Sprintf("%s:%s", obj.GetKind(), obj.GetName())
|
||||
}
|
||||
|
||||
func (s *ksonnetAppStateManager) getRepo(repoURL string) *v1alpha1.Repository {
|
||||
repo, err := s.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 (s *ksonnetAppStateManager) persistDeploymentInfo(
|
||||
app *v1alpha1.Application, revision string, envParams []*v1alpha1.ComponentParameter, overrides *[]v1alpha1.ComponentParameter) error {
|
||||
|
||||
params := make([]v1alpha1.ComponentParameter, len(envParams))
|
||||
for i := range envParams {
|
||||
param := *envParams[i]
|
||||
params[i] = param
|
||||
}
|
||||
var nextID int64 = 0
|
||||
if len(app.Status.History) > 0 {
|
||||
nextID = app.Status.History[len(app.Status.History)-1].ID + 1
|
||||
}
|
||||
history := append(app.Status.History, v1alpha1.DeploymentInfo{
|
||||
ComponentParameterOverrides: app.Spec.Source.ComponentParameterOverrides,
|
||||
Revision: revision,
|
||||
Params: params,
|
||||
DeployedAt: metav1.NewTime(time.Now().UTC()),
|
||||
ID: nextID,
|
||||
})
|
||||
|
||||
if len(history) > maxHistoryCnt {
|
||||
history = history[1 : maxHistoryCnt+1]
|
||||
}
|
||||
|
||||
patch, err := json.Marshal(map[string]map[string][]v1alpha1.DeploymentInfo{
|
||||
"status": {
|
||||
"history": history,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.appclientset.ArgoprojV1alpha1().Applications(s.namespace).Patch(app.Name, types.MergePatchType, patch)
|
||||
return err
|
||||
}
|
||||
|
||||
// NewAppStateManager creates new instance of Ksonnet app comparator
|
||||
func NewAppStateManager(
|
||||
db db.ArgoDB,
|
||||
appclientset appclientset.Interface,
|
||||
repoClientset reposerver.Clientset,
|
||||
namespace string,
|
||||
) AppStateManager {
|
||||
return &ksonnetAppStateManager{
|
||||
db: db,
|
||||
appclientset: appclientset,
|
||||
repoClientset: repoClientset,
|
||||
namespace: namespace,
|
||||
}
|
||||
}
|
||||
858
controller/sync.go
Normal file
@@ -0,0 +1,858 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
|
||||
log "github.com/sirupsen/logrus"
|
||||
apierr "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/kubernetes/pkg/apis/batch"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/reposerver/repository"
|
||||
"github.com/argoproj/argo-cd/util/argo"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
)
|
||||
|
||||
type syncContext struct {
|
||||
appName string
|
||||
comparison *appv1.ComparisonResult
|
||||
config *rest.Config
|
||||
dynClientPool dynamic.ClientPool
|
||||
disco *discovery.DiscoveryClient
|
||||
namespace string
|
||||
syncOp *appv1.SyncOperation
|
||||
syncRes *appv1.SyncOperationResult
|
||||
opState *appv1.OperationState
|
||||
manifestInfo *repository.ManifestResponse
|
||||
log *log.Entry
|
||||
// lock to protect concurrent updates of the result list
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func (s *ksonnetAppStateManager) SyncAppState(app *appv1.Application, state *appv1.OperationState) {
|
||||
// Sync requests are usually requested with ambiguous revisions (e.g. master, HEAD, v1.2.3).
|
||||
// This can change meaning when resuming operations (e.g a hook sync). After calculating a
|
||||
// concrete git commit SHA, the SHA is remembered in the status.operationState.syncResult and
|
||||
// rollbackResult fields. This ensures that when resuming an operation, we sync to the same
|
||||
// revision that we initially started with.
|
||||
var revision string
|
||||
var syncOp appv1.SyncOperation
|
||||
var syncRes *appv1.SyncOperationResult
|
||||
var overrides []appv1.ComponentParameter
|
||||
|
||||
if state.Operation.Sync != nil {
|
||||
syncOp = *state.Operation.Sync
|
||||
if state.SyncResult != nil {
|
||||
syncRes = state.SyncResult
|
||||
revision = state.SyncResult.Revision
|
||||
} else {
|
||||
syncRes = &appv1.SyncOperationResult{}
|
||||
state.SyncResult = syncRes
|
||||
}
|
||||
} else if state.Operation.Rollback != nil {
|
||||
var deploymentInfo *appv1.DeploymentInfo
|
||||
for _, info := range app.Status.History {
|
||||
if info.ID == app.Operation.Rollback.ID {
|
||||
deploymentInfo = &info
|
||||
break
|
||||
}
|
||||
}
|
||||
if deploymentInfo == nil {
|
||||
state.Phase = appv1.OperationFailed
|
||||
state.Message = fmt.Sprintf("application %s does not have deployment with id %v", app.Name, app.Operation.Rollback.ID)
|
||||
return
|
||||
}
|
||||
// Rollback is just a convenience around Sync
|
||||
syncOp = appv1.SyncOperation{
|
||||
Revision: deploymentInfo.Revision,
|
||||
DryRun: state.Operation.Rollback.DryRun,
|
||||
Prune: state.Operation.Rollback.Prune,
|
||||
SyncStrategy: &appv1.SyncStrategy{Apply: &appv1.SyncStrategyApply{}},
|
||||
}
|
||||
overrides = deploymentInfo.ComponentParameterOverrides
|
||||
if state.RollbackResult != nil {
|
||||
syncRes = state.RollbackResult
|
||||
revision = state.RollbackResult.Revision
|
||||
} else {
|
||||
syncRes = &appv1.SyncOperationResult{}
|
||||
state.RollbackResult = syncRes
|
||||
}
|
||||
} else {
|
||||
state.Phase = appv1.OperationFailed
|
||||
state.Message = "Invalid operation request: no operation specified"
|
||||
return
|
||||
}
|
||||
|
||||
if revision == "" {
|
||||
// if we get here, it means we did not remember a commit SHA which we should be syncing to.
|
||||
// This typically indicates we are just about to begin a brand new sync/rollback operation.
|
||||
// Take the value in the requested operation. We will resolve this to a SHA later.
|
||||
revision = syncOp.Revision
|
||||
}
|
||||
comparison, manifestInfo, conditions, err := s.CompareAppState(app, revision, overrides)
|
||||
if err != nil {
|
||||
state.Phase = appv1.OperationError
|
||||
state.Message = err.Error()
|
||||
return
|
||||
}
|
||||
errConditions := make([]appv1.ApplicationCondition, 0)
|
||||
for i := range conditions {
|
||||
if conditions[i].IsError() {
|
||||
errConditions = append(errConditions, conditions[i])
|
||||
}
|
||||
}
|
||||
if len(errConditions) > 0 {
|
||||
state.Phase = appv1.OperationError
|
||||
state.Message = argo.FormatAppConditions(errConditions)
|
||||
return
|
||||
}
|
||||
// We now have a concrete commit SHA. Set this in the sync result revision so that we remember
|
||||
// what we should be syncing to when resuming operations.
|
||||
syncRes.Revision = manifestInfo.Revision
|
||||
|
||||
clst, err := s.db.GetCluster(context.Background(), app.Spec.Destination.Server)
|
||||
if err != nil {
|
||||
state.Phase = appv1.OperationError
|
||||
state.Message = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
restConfig := clst.RESTConfig()
|
||||
dynClientPool := dynamic.NewDynamicClientPool(restConfig)
|
||||
disco, err := discovery.NewDiscoveryClientForConfig(restConfig)
|
||||
if err != nil {
|
||||
state.Phase = appv1.OperationError
|
||||
state.Message = fmt.Sprintf("Failed to initialize dynamic client: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
syncCtx := syncContext{
|
||||
appName: app.Name,
|
||||
comparison: comparison,
|
||||
config: restConfig,
|
||||
dynClientPool: dynClientPool,
|
||||
disco: disco,
|
||||
namespace: app.Spec.Destination.Namespace,
|
||||
syncOp: &syncOp,
|
||||
syncRes: syncRes,
|
||||
opState: state,
|
||||
manifestInfo: manifestInfo,
|
||||
log: log.WithFields(log.Fields{"application": app.Name}),
|
||||
}
|
||||
|
||||
if state.Phase == appv1.OperationTerminating {
|
||||
syncCtx.terminate()
|
||||
} else {
|
||||
syncCtx.sync()
|
||||
}
|
||||
|
||||
if !syncOp.DryRun && syncCtx.opState.Phase.Successful() {
|
||||
err := s.persistDeploymentInfo(app, manifestInfo.Revision, manifestInfo.Params, nil)
|
||||
if err != nil {
|
||||
state.Phase = appv1.OperationError
|
||||
state.Message = fmt.Sprintf("failed to record sync to history: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// syncTask holds the live and target object. At least one should be non-nil. A targetObj of nil
|
||||
// indicates the live object needs to be pruned. A liveObj of nil indicates the object has yet to
|
||||
// be deployed
|
||||
type syncTask struct {
|
||||
liveObj *unstructured.Unstructured
|
||||
targetObj *unstructured.Unstructured
|
||||
}
|
||||
|
||||
// sync has performs the actual apply or hook based sync
|
||||
func (sc *syncContext) sync() {
|
||||
syncTasks, successful := sc.generateSyncTasks()
|
||||
if !successful {
|
||||
return
|
||||
}
|
||||
// Perform a `kubectl apply --dry-run` against all the manifests. This will detect most (but
|
||||
// not all) validation issues with the user's manifests (e.g. will detect syntax issues, but
|
||||
// will not not detect if they are mutating immutable fields). If anything fails, we will refuse
|
||||
// to perform the sync.
|
||||
if !sc.startedPreSyncPhase() {
|
||||
// Optimization: we only wish to do this once per operation, performing additional dry-runs
|
||||
// is harmless, but redundant. The indicator we use to detect if we have already performed
|
||||
// the dry-run for this operation, is if the resource or hook list is empty.
|
||||
if !sc.doApplySync(syncTasks, true, false, sc.syncOp.DryRun) {
|
||||
sc.setOperationPhase(appv1.OperationFailed, "one or more objects failed to apply (dry run)")
|
||||
return
|
||||
}
|
||||
if sc.syncOp.DryRun {
|
||||
sc.setOperationPhase(appv1.OperationSucceeded, "successfully synced (dry run)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// All objects passed a `kubectl apply --dry-run`, so we are now ready to actually perform the sync.
|
||||
if sc.syncOp.SyncStrategy == nil {
|
||||
// default sync strategy to hook if no strategy
|
||||
sc.syncOp.SyncStrategy = &appv1.SyncStrategy{Hook: &appv1.SyncStrategyHook{}}
|
||||
}
|
||||
if sc.syncOp.SyncStrategy.Apply != nil {
|
||||
if !sc.startedSyncPhase() {
|
||||
if !sc.doApplySync(syncTasks, false, sc.syncOp.SyncStrategy.Apply.Force, true) {
|
||||
sc.setOperationPhase(appv1.OperationFailed, "one or more objects failed to apply")
|
||||
return
|
||||
}
|
||||
// If apply was successful, return here and force an app refresh. This is so the app
|
||||
// will become requeued into the workqueue, to force a new sync/health assessment before
|
||||
// marking the operation as completed
|
||||
return
|
||||
}
|
||||
sc.setOperationPhase(appv1.OperationSucceeded, "successfully synced")
|
||||
} else if sc.syncOp.SyncStrategy.Hook != nil {
|
||||
hooks, err := sc.getHooks()
|
||||
if err != nil {
|
||||
sc.setOperationPhase(appv1.OperationError, fmt.Sprintf("failed to generate hooks resources: %v", err))
|
||||
return
|
||||
}
|
||||
sc.doHookSync(syncTasks, hooks)
|
||||
} else {
|
||||
sc.setOperationPhase(appv1.OperationFailed, "Unknown sync strategy")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *syncContext) forceAppRefresh() {
|
||||
sc.comparison.ComparedAt = metav1.Time{}
|
||||
}
|
||||
|
||||
// generateSyncTasks() generates the list of sync tasks we will be performing during this sync.
|
||||
func (sc *syncContext) generateSyncTasks() ([]syncTask, bool) {
|
||||
syncTasks := make([]syncTask, 0)
|
||||
for _, resourceState := range sc.comparison.Resources {
|
||||
liveObj, err := resourceState.LiveObject()
|
||||
if err != nil {
|
||||
sc.setOperationPhase(appv1.OperationError, fmt.Sprintf("Failed to unmarshal live object: %v", err))
|
||||
return nil, false
|
||||
}
|
||||
targetObj, err := resourceState.TargetObject()
|
||||
if err != nil {
|
||||
sc.setOperationPhase(appv1.OperationError, fmt.Sprintf("Failed to unmarshal target object: %v", err))
|
||||
return nil, false
|
||||
}
|
||||
syncTask := syncTask{
|
||||
liveObj: liveObj,
|
||||
targetObj: targetObj,
|
||||
}
|
||||
syncTasks = append(syncTasks, syncTask)
|
||||
}
|
||||
return syncTasks, true
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
if len(sc.syncRes.Hooks) > 0 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// startedSyncPhase detects if we have already started the Sync stage of a sync operation.
|
||||
// This is equal to if the resource list is non-empty, or we we see Sync/PostSync hooks
|
||||
func (sc *syncContext) startedSyncPhase() bool {
|
||||
if len(sc.syncRes.Resources) > 0 {
|
||||
return true
|
||||
}
|
||||
for _, hookStatus := range sc.syncRes.Hooks {
|
||||
if hookStatus.Type == appv1.HookTypeSync || hookStatus.Type == appv1.HookTypePostSync {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// startedPostSyncPhase detects if we have already started the PostSync stage. This is equal to if
|
||||
// we see any PostSync hooks
|
||||
func (sc *syncContext) startedPostSyncPhase() bool {
|
||||
for _, hookStatus := range sc.syncRes.Hooks {
|
||||
if hookStatus.Type == appv1.HookTypePostSync {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (sc *syncContext) setOperationPhase(phase appv1.OperationPhase, message string) {
|
||||
if sc.opState.Phase != phase || sc.opState.Message != message {
|
||||
sc.log.Infof("Updating operation state. phase: %s -> %s, message: '%s' -> '%s'", sc.opState.Phase, phase, sc.opState.Message, message)
|
||||
}
|
||||
sc.opState.Phase = phase
|
||||
sc.opState.Message = message
|
||||
}
|
||||
|
||||
// applyObject performs a `kubectl apply` of a single resource
|
||||
func (sc *syncContext) applyObject(targetObj *unstructured.Unstructured, dryRun bool, force bool) appv1.ResourceDetails {
|
||||
resDetails := appv1.ResourceDetails{
|
||||
Name: targetObj.GetName(),
|
||||
Kind: targetObj.GetKind(),
|
||||
Namespace: sc.namespace,
|
||||
}
|
||||
message, err := kube.ApplyResource(sc.config, targetObj, sc.namespace, dryRun, force)
|
||||
if err != nil {
|
||||
resDetails.Message = err.Error()
|
||||
resDetails.Status = appv1.ResourceDetailsSyncFailed
|
||||
return resDetails
|
||||
}
|
||||
resDetails.Message = message
|
||||
resDetails.Status = appv1.ResourceDetailsSynced
|
||||
return resDetails
|
||||
}
|
||||
|
||||
// pruneObject deletes the object if both prune is true and dryRun is false. Otherwise appropriate message
|
||||
func (sc *syncContext) pruneObject(liveObj *unstructured.Unstructured, prune, dryRun bool) appv1.ResourceDetails {
|
||||
resDetails := appv1.ResourceDetails{
|
||||
Name: liveObj.GetName(),
|
||||
Kind: liveObj.GetKind(),
|
||||
Namespace: liveObj.GetNamespace(),
|
||||
}
|
||||
if prune {
|
||||
if dryRun {
|
||||
resDetails.Message = "pruned (dry run)"
|
||||
resDetails.Status = appv1.ResourceDetailsSyncedAndPruned
|
||||
} else {
|
||||
err := kube.DeleteResource(sc.config, liveObj, sc.namespace)
|
||||
if err != nil {
|
||||
resDetails.Message = err.Error()
|
||||
resDetails.Status = appv1.ResourceDetailsSyncFailed
|
||||
} else {
|
||||
resDetails.Message = "pruned"
|
||||
resDetails.Status = appv1.ResourceDetailsSyncedAndPruned
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resDetails.Message = "ignored (requires pruning)"
|
||||
resDetails.Status = appv1.ResourceDetailsPruningRequired
|
||||
}
|
||||
return resDetails
|
||||
}
|
||||
|
||||
// performs a apply based sync of the given sync tasks (possibly pruning the objects).
|
||||
// If update is true, will updates the resource details with the result.
|
||||
// Or if the prune/apply failed, will also update the result.
|
||||
func (sc *syncContext) doApplySync(syncTasks []syncTask, dryRun, force, update bool) bool {
|
||||
syncSuccessful := true
|
||||
// apply all resources in parallel
|
||||
var wg sync.WaitGroup
|
||||
for _, task := range syncTasks {
|
||||
wg.Add(1)
|
||||
go func(t syncTask) {
|
||||
defer wg.Done()
|
||||
var resDetails appv1.ResourceDetails
|
||||
if t.targetObj == nil {
|
||||
resDetails = sc.pruneObject(t.liveObj, sc.syncOp.Prune, dryRun)
|
||||
} else {
|
||||
if isHook(t.targetObj) {
|
||||
return
|
||||
}
|
||||
resDetails = sc.applyObject(t.targetObj, dryRun, force)
|
||||
}
|
||||
if !resDetails.Status.Successful() {
|
||||
syncSuccessful = false
|
||||
}
|
||||
if update || !resDetails.Status.Successful() {
|
||||
sc.setResourceDetails(&resDetails)
|
||||
}
|
||||
}(task)
|
||||
}
|
||||
wg.Wait()
|
||||
return syncSuccessful
|
||||
}
|
||||
|
||||
// doHookSync initiates (or continues) a hook-based sync. This method will be invoked when there may
|
||||
// already be in-flight (potentially incomplete) jobs/workflows, and should be idempotent.
|
||||
func (sc *syncContext) doHookSync(syncTasks []syncTask, hooks []*unstructured.Unstructured) {
|
||||
// 1. Run PreSync hooks
|
||||
if !sc.runHooks(hooks, appv1.HookTypePreSync) {
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Run Sync hooks (e.g. blue-green sync workflow)
|
||||
// Before performing Sync hooks, apply any normal manifests which aren't annotated with a hook.
|
||||
// We only want to do this once per operation.
|
||||
shouldContinue := true
|
||||
if !sc.startedSyncPhase() {
|
||||
if !sc.syncNonHookTasks(syncTasks) {
|
||||
sc.setOperationPhase(appv1.OperationFailed, "one or more objects failed to apply")
|
||||
return
|
||||
}
|
||||
shouldContinue = false
|
||||
}
|
||||
if !sc.runHooks(hooks, appv1.HookTypeSync) {
|
||||
shouldContinue = false
|
||||
}
|
||||
if !shouldContinue {
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Run PostSync hooks
|
||||
// Before running PostSync hooks, we want to make rollout is complete (app is healthy). If we
|
||||
// already started the post-sync phase, then we do not need to perform the health check.
|
||||
postSyncHooks, _ := sc.getHooks(appv1.HookTypePostSync)
|
||||
if len(postSyncHooks) > 0 && !sc.startedPostSyncPhase() {
|
||||
healthState, err := setApplicationHealth(sc.comparison)
|
||||
sc.log.Infof("PostSync application health check: %s", healthState.Status)
|
||||
if err != nil {
|
||||
sc.setOperationPhase(appv1.OperationError, fmt.Sprintf("failed to check application health: %v", err))
|
||||
return
|
||||
}
|
||||
if healthState.Status != appv1.HealthStatusHealthy {
|
||||
sc.setOperationPhase(appv1.OperationRunning, fmt.Sprintf("waiting for %s state to run %s hooks (current health: %s)", appv1.HealthStatusHealthy, appv1.HookTypePostSync, healthState.Status))
|
||||
return
|
||||
}
|
||||
}
|
||||
if !sc.runHooks(hooks, appv1.HookTypePostSync) {
|
||||
return
|
||||
}
|
||||
|
||||
// if we get here, all hooks successfully completed
|
||||
sc.setOperationPhase(appv1.OperationSucceeded, "successfully synced")
|
||||
}
|
||||
|
||||
// getHooks returns all ArgoCD hooks, optionally filtered by ones of the specific type(s)
|
||||
func (sc *syncContext) getHooks(hookTypes ...appv1.HookType) ([]*unstructured.Unstructured, error) {
|
||||
var hooks []*unstructured.Unstructured
|
||||
for _, manifest := range sc.manifestInfo.Manifests {
|
||||
var hook unstructured.Unstructured
|
||||
err := json.Unmarshal([]byte(manifest), &hook)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !isArgoHook(&hook) {
|
||||
// TODO: in the future, if we want to map helm hooks to ArgoCD lifecycles, we should
|
||||
// include helm hooks in the returned list
|
||||
continue
|
||||
}
|
||||
if len(hookTypes) > 0 {
|
||||
match := false
|
||||
for _, desiredType := range hookTypes {
|
||||
if isHookType(&hook, desiredType) {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
}
|
||||
hooks = append(hooks, &hook)
|
||||
}
|
||||
return hooks, nil
|
||||
}
|
||||
|
||||
// runHooks iterates & filters the target manifests for resources of the specified hook type, then
|
||||
// creates the resource. Updates the sc.opRes.hooks with the current status. Returns whether or not
|
||||
// we should continue to the next hook phase.
|
||||
func (sc *syncContext) runHooks(hooks []*unstructured.Unstructured, hookType appv1.HookType) bool {
|
||||
shouldContinue := true
|
||||
for _, hook := range hooks {
|
||||
if hookType == appv1.HookTypeSync && isHookType(hook, appv1.HookTypeSkip) {
|
||||
// If we get here, we are invoking all sync hooks and reached a resource that is
|
||||
// annotated with the Skip hook. This will update the resource details to indicate it
|
||||
// was skipped due to annotation
|
||||
sc.setResourceDetails(&appv1.ResourceDetails{
|
||||
Name: hook.GetName(),
|
||||
Kind: hook.GetKind(),
|
||||
Namespace: sc.namespace,
|
||||
Message: "Skipped",
|
||||
})
|
||||
continue
|
||||
}
|
||||
if !isHookType(hook, hookType) {
|
||||
continue
|
||||
}
|
||||
updated, err := sc.runHook(hook, hookType)
|
||||
if err != nil {
|
||||
sc.setOperationPhase(appv1.OperationError, fmt.Sprintf("%s hook error: %v", hookType, err))
|
||||
return false
|
||||
}
|
||||
if updated {
|
||||
// If the result of running a hook, caused us to modify hook resource state, we should
|
||||
// not proceed to the next hook phase. This is because before proceeding to the next
|
||||
// phase, we want a full health assessment to happen. By returning early, we allow
|
||||
// the application to get requeued into the controller workqueue, and on the next
|
||||
// process iteration, a new CompareAppState() will be performed to get the most
|
||||
// up-to-date live state. This enables us to accurately wait for an application to
|
||||
// become Healthy before proceeding to run PostSync tasks.
|
||||
shouldContinue = false
|
||||
}
|
||||
}
|
||||
if !shouldContinue {
|
||||
sc.log.Infof("Stopping after %s phase due to modifications to hook resource state", hookType)
|
||||
return false
|
||||
}
|
||||
completed, successful := areHooksCompletedSuccessful(hookType, sc.syncRes.Hooks)
|
||||
if !completed {
|
||||
return false
|
||||
}
|
||||
if !successful {
|
||||
sc.setOperationPhase(appv1.OperationFailed, fmt.Sprintf("%s hook failed", hookType))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// runHook runs the supplied hook and updates the hook status. Returns true if the result of
|
||||
// invoking this method resulted in changes to any hook status
|
||||
func (sc *syncContext) runHook(hook *unstructured.Unstructured, hookType appv1.HookType) (bool, error) {
|
||||
// Hook resources names are deterministic, whether they are defined by the user (metadata.name),
|
||||
// or formulated at the time of the operation (metadata.generateName). If user specifies
|
||||
// metadata.generateName, then we will generate a formulated metadata.name before submission.
|
||||
if hook.GetName() == "" {
|
||||
postfix := strings.ToLower(fmt.Sprintf("%s-%s-%d", sc.syncRes.Revision[0:7], hookType, sc.opState.StartedAt.UTC().Unix()))
|
||||
generatedName := hook.GetGenerateName()
|
||||
hook = hook.DeepCopy()
|
||||
hook.SetName(fmt.Sprintf("%s%s", generatedName, postfix))
|
||||
}
|
||||
// Check our hook statuses to see if we already completed this hook.
|
||||
// If so, this method is a noop
|
||||
prevStatus := sc.getHookStatus(hook, hookType)
|
||||
if prevStatus != nil && prevStatus.Status.Completed() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
gvk := hook.GroupVersionKind()
|
||||
dclient, err := sc.dynClientPool.ClientForGroupVersionKind(gvk)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
apiResource, err := kube.ServerResourceForGroupVersionKind(sc.disco, gvk)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
resIf := dclient.Resource(apiResource, sc.namespace)
|
||||
|
||||
var liveObj *unstructured.Unstructured
|
||||
existing, err := resIf.Get(hook.GetName(), metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if !apierr.IsNotFound(err) {
|
||||
return false, fmt.Errorf("Failed to get status of %s hook %s '%s': %v", hookType, gvk, hook.GetName(), err)
|
||||
}
|
||||
hook = hook.DeepCopy()
|
||||
err = kube.SetLabel(hook, common.LabelApplicationName, sc.appName)
|
||||
if err != nil {
|
||||
sc.log.Warnf("Failed to set application label on hook %v: %v", hook, err)
|
||||
}
|
||||
_, err := kube.ApplyResource(sc.config, hook, sc.namespace, false, false)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("Failed to create %s hook %s '%s': %v", hookType, gvk, hook.GetName(), err)
|
||||
}
|
||||
created, err := resIf.Get(hook.GetName(), metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return true, fmt.Errorf("Failed to get status of %s hook %s '%s': %v", hookType, gvk, hook.GetName(), err)
|
||||
}
|
||||
sc.log.Infof("%s hook %s '%s' created", hookType, gvk, created.GetName())
|
||||
sc.setOperationPhase(appv1.OperationRunning, fmt.Sprintf("running %s hooks", hookType))
|
||||
liveObj = created
|
||||
} else {
|
||||
liveObj = existing
|
||||
}
|
||||
hookStatus := newHookStatus(liveObj, hookType)
|
||||
if hookStatus.Status.Completed() {
|
||||
if enforceDeletePolicy(hook, hookStatus.Status) {
|
||||
err = sc.deleteHook(hook.GetName(), hook.GetKind(), hook.GetAPIVersion())
|
||||
if err != nil {
|
||||
hookStatus.Status = appv1.OperationFailed
|
||||
hookStatus.Message = fmt.Sprintf("failed to delete %s hook: %v", hookStatus.Status, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return sc.updateHookStatus(hookStatus), nil
|
||||
}
|
||||
|
||||
// enforceDeletePolicy examines the hook deletion policy of a object and deletes it based on the status
|
||||
func enforceDeletePolicy(hook *unstructured.Unstructured, phase appv1.OperationPhase) bool {
|
||||
annotations := hook.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return false
|
||||
}
|
||||
deletePolicies := strings.Split(annotations[common.AnnotationHookDeletePolicy], ",")
|
||||
for _, dp := range deletePolicies {
|
||||
policy := appv1.HookDeletePolicy(strings.TrimSpace(dp))
|
||||
if policy == appv1.HookDeletePolicyHookSucceeded && phase == appv1.OperationSucceeded {
|
||||
return true
|
||||
}
|
||||
if policy == appv1.HookDeletePolicyHookFailed && phase == appv1.OperationFailed {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isHookType tells whether or not the supplied object is a hook of the specified type
|
||||
func isHookType(hook *unstructured.Unstructured, hookType appv1.HookType) bool {
|
||||
annotations := hook.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return false
|
||||
}
|
||||
resHookTypes := strings.Split(annotations[common.AnnotationHook], ",")
|
||||
for _, ht := range resHookTypes {
|
||||
if string(hookType) == strings.TrimSpace(ht) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isHook indicates if the object is either a ArgoCD or Helm hook
|
||||
func isHook(obj *unstructured.Unstructured) bool {
|
||||
return isArgoHook(obj) || isHelmHook(obj)
|
||||
}
|
||||
|
||||
// isHelmHook indicates if the supplied object is a helm hook
|
||||
func isHelmHook(obj *unstructured.Unstructured) bool {
|
||||
annotations := obj.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return false
|
||||
}
|
||||
_, ok := annotations[common.AnnotationHook]
|
||||
return ok
|
||||
}
|
||||
|
||||
// isArgoHook indicates if the supplied object is an ArgoCD application lifecycle hook
|
||||
// (vs. a normal, synced application resource)
|
||||
func isArgoHook(obj *unstructured.Unstructured) bool {
|
||||
annotations := obj.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return false
|
||||
}
|
||||
resHookTypes := strings.Split(annotations[common.AnnotationHook], ",")
|
||||
for _, hookType := range resHookTypes {
|
||||
hookType = strings.TrimSpace(hookType)
|
||||
switch appv1.HookType(hookType) {
|
||||
case appv1.HookTypePreSync, appv1.HookTypeSync, appv1.HookTypePostSync:
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// syncNonHookTasks syncs or prunes the objects that are not handled by hooks using an apply sync.
|
||||
// returns true if the sync was successful
|
||||
func (sc *syncContext) syncNonHookTasks(syncTasks []syncTask) bool {
|
||||
var nonHookTasks []syncTask
|
||||
for _, task := range syncTasks {
|
||||
if task.targetObj == nil {
|
||||
nonHookTasks = append(nonHookTasks, task)
|
||||
} else {
|
||||
annotations := task.targetObj.GetAnnotations()
|
||||
if annotations != nil && annotations[common.AnnotationHook] != "" {
|
||||
// we are doing a hook sync and this resource is annotated with a hook annotation
|
||||
continue
|
||||
}
|
||||
// if we get here, this resource does not have any hook annotation so we
|
||||
// should perform an `kubectl apply`
|
||||
nonHookTasks = append(nonHookTasks, task)
|
||||
}
|
||||
}
|
||||
return sc.doApplySync(nonHookTasks, false, sc.syncOp.SyncStrategy.Hook.Force, true)
|
||||
}
|
||||
|
||||
// setResourceDetails sets a resource details in the SyncResult.Resources list
|
||||
func (sc *syncContext) setResourceDetails(details *appv1.ResourceDetails) {
|
||||
sc.lock.Lock()
|
||||
defer sc.lock.Unlock()
|
||||
for i, res := range sc.syncRes.Resources {
|
||||
if res.Kind == details.Kind && res.Name == details.Name {
|
||||
// update existing value
|
||||
if res.Status != details.Status {
|
||||
sc.log.Infof("updated resource %s/%s status: %s -> %s", res.Kind, res.Name, res.Status, details.Status)
|
||||
}
|
||||
if res.Message != details.Message {
|
||||
sc.log.Infof("updated resource %s/%s message: %s -> %s", res.Kind, res.Name, res.Message, details.Message)
|
||||
}
|
||||
sc.syncRes.Resources[i] = details
|
||||
return
|
||||
}
|
||||
}
|
||||
sc.log.Infof("added resource %s/%s status: %s, message: %s", details.Kind, details.Name, details.Status, details.Message)
|
||||
sc.syncRes.Resources = append(sc.syncRes.Resources, details)
|
||||
}
|
||||
|
||||
func (sc *syncContext) getHookStatus(hookObj *unstructured.Unstructured, hookType appv1.HookType) *appv1.HookStatus {
|
||||
for _, hr := range sc.syncRes.Hooks {
|
||||
if hr.Name == hookObj.GetName() && hr.Kind == hookObj.GetKind() && hr.Type == hookType {
|
||||
return hr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newHookStatus(hook *unstructured.Unstructured, hookType appv1.HookType) appv1.HookStatus {
|
||||
hookStatus := appv1.HookStatus{
|
||||
Name: hook.GetName(),
|
||||
Kind: hook.GetKind(),
|
||||
APIVersion: hook.GetAPIVersion(),
|
||||
Type: hookType,
|
||||
}
|
||||
switch hookStatus.Kind {
|
||||
case "Job":
|
||||
var job batch.Job
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(hook.Object, &job)
|
||||
if err != nil {
|
||||
hookStatus.Status = appv1.OperationError
|
||||
hookStatus.Message = err.Error()
|
||||
} else {
|
||||
failed := false
|
||||
var failMsg string
|
||||
complete := false
|
||||
var message string
|
||||
for _, condition := range job.Status.Conditions {
|
||||
switch condition.Type {
|
||||
case batch.JobFailed:
|
||||
failed = true
|
||||
complete = true
|
||||
failMsg = condition.Message
|
||||
case batch.JobComplete:
|
||||
complete = true
|
||||
message = condition.Message
|
||||
}
|
||||
}
|
||||
if !complete {
|
||||
hookStatus.Status = appv1.OperationRunning
|
||||
hookStatus.Message = message
|
||||
} else if failed {
|
||||
hookStatus.Status = appv1.OperationFailed
|
||||
hookStatus.Message = failMsg
|
||||
} else {
|
||||
hookStatus.Status = appv1.OperationSucceeded
|
||||
hookStatus.Message = message
|
||||
}
|
||||
}
|
||||
case "Workflow":
|
||||
var wf wfv1.Workflow
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(hook.Object, &wf)
|
||||
if err != nil {
|
||||
hookStatus.Status = appv1.OperationError
|
||||
hookStatus.Message = err.Error()
|
||||
} else {
|
||||
switch wf.Status.Phase {
|
||||
case wfv1.NodeRunning:
|
||||
hookStatus.Status = appv1.OperationRunning
|
||||
case wfv1.NodeSucceeded:
|
||||
hookStatus.Status = appv1.OperationSucceeded
|
||||
case wfv1.NodeFailed:
|
||||
hookStatus.Status = appv1.OperationFailed
|
||||
case wfv1.NodeError:
|
||||
hookStatus.Status = appv1.OperationError
|
||||
}
|
||||
hookStatus.Message = wf.Status.Message
|
||||
}
|
||||
default:
|
||||
hookStatus.Status = appv1.OperationSucceeded
|
||||
hookStatus.Message = fmt.Sprintf("%s created", hook.GetName())
|
||||
}
|
||||
return hookStatus
|
||||
}
|
||||
|
||||
// updateHookStatus updates the status of a hook. Returns true if the hook was modified
|
||||
func (sc *syncContext) updateHookStatus(hookStatus appv1.HookStatus) bool {
|
||||
sc.lock.Lock()
|
||||
defer sc.lock.Unlock()
|
||||
for i, prev := range sc.syncRes.Hooks {
|
||||
if prev.Name == hookStatus.Name && prev.Kind == hookStatus.Kind && prev.Type == hookStatus.Type {
|
||||
if reflect.DeepEqual(prev, hookStatus) {
|
||||
return false
|
||||
}
|
||||
if prev.Status != hookStatus.Status {
|
||||
sc.log.Infof("Hook %s %s/%s status: %s -> %s", hookStatus.Type, prev.Kind, prev.Name, prev.Status, hookStatus.Status)
|
||||
}
|
||||
if prev.Message != hookStatus.Message {
|
||||
sc.log.Infof("Hook %s %s/%s message: '%s' -> '%s'", hookStatus.Type, prev.Kind, prev.Name, prev.Message, hookStatus.Message)
|
||||
}
|
||||
sc.syncRes.Hooks[i] = &hookStatus
|
||||
return true
|
||||
}
|
||||
}
|
||||
sc.syncRes.Hooks = append(sc.syncRes.Hooks, &hookStatus)
|
||||
sc.log.Infof("Set new hook %s %s/%s. status: %s, message: %s", hookStatus.Type, hookStatus.Kind, hookStatus.Name, hookStatus.Status, hookStatus.Message)
|
||||
return true
|
||||
}
|
||||
|
||||
// areHooksCompletedSuccessful checks if all the hooks of the specified type are completed and successful
|
||||
func areHooksCompletedSuccessful(hookType appv1.HookType, hookStatuses []*appv1.HookStatus) (bool, bool) {
|
||||
isSuccessful := true
|
||||
for _, hookStatus := range hookStatuses {
|
||||
if hookStatus.Type != hookType {
|
||||
continue
|
||||
}
|
||||
if !hookStatus.Status.Completed() {
|
||||
return false, false
|
||||
}
|
||||
if !hookStatus.Status.Successful() {
|
||||
isSuccessful = false
|
||||
}
|
||||
}
|
||||
return true, isSuccessful
|
||||
}
|
||||
|
||||
// terminate looks for any running jobs/workflow hooks and deletes the resource
|
||||
func (sc *syncContext) terminate() {
|
||||
terminateSuccessful := true
|
||||
for _, hookStatus := range sc.syncRes.Hooks {
|
||||
if hookStatus.Status.Completed() {
|
||||
continue
|
||||
}
|
||||
switch hookStatus.Kind {
|
||||
case "Job", "Workflow":
|
||||
hookStatus.Status = appv1.OperationFailed
|
||||
err := sc.deleteHook(hookStatus.Name, hookStatus.Kind, hookStatus.APIVersion)
|
||||
if err != nil {
|
||||
hookStatus.Message = fmt.Sprintf("Failed to delete %s hook %s/%s: %v", hookStatus.Type, hookStatus.Kind, hookStatus.Name, err)
|
||||
terminateSuccessful = false
|
||||
} else {
|
||||
hookStatus.Message = fmt.Sprintf("Deleted %s hook %s/%s", hookStatus.Type, hookStatus.Kind, hookStatus.Name)
|
||||
}
|
||||
sc.updateHookStatus(*hookStatus)
|
||||
}
|
||||
}
|
||||
if terminateSuccessful {
|
||||
sc.setOperationPhase(appv1.OperationFailed, "Operation terminated")
|
||||
} else {
|
||||
sc.setOperationPhase(appv1.OperationError, "Operation termination had errors")
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *syncContext) deleteHook(name, kind, apiVersion string) error {
|
||||
groupVersion := strings.Split(apiVersion, "/")
|
||||
if len(groupVersion) != 2 {
|
||||
return fmt.Errorf("Failed to terminate app. Unrecognized group/version: %s", apiVersion)
|
||||
}
|
||||
gvk := schema.GroupVersionKind{
|
||||
Group: groupVersion[0],
|
||||
Version: groupVersion[1],
|
||||
Kind: kind,
|
||||
}
|
||||
dclient, err := sc.dynClientPool.ClientForGroupVersionKind(gvk)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiResource, err := kube.ServerResourceForGroupVersionKind(sc.disco, gvk)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resIf := dclient.Resource(apiResource, sc.namespace)
|
||||
return resIf.Delete(name, &metav1.DeleteOptions{})
|
||||
}
|
||||
26
controller/sync_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
func newTestSyncCtx() *syncContext {
|
||||
return &syncContext{
|
||||
comparison: &v1alpha1.ComparisonResult{},
|
||||
config: &rest.Config{},
|
||||
namespace: "test-namespace",
|
||||
syncOp: &v1alpha1.SyncOperation{},
|
||||
opState: &v1alpha1.OperationState{},
|
||||
log: log.WithFields(log.Fields{"application": "fake-app"}),
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWorkflows(t *testing.T) {
|
||||
// syncCtx := newTestSyncCtx()
|
||||
// syncCtx.doWorkflowSync(nil, nil)
|
||||
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
# Argo CD Getting Started
|
||||
|
||||
An example Ksonnet guestbook application is provided to demonstrates how Argo CD works.
|
||||
|
||||
## Requirements
|
||||
* Installed [minikube](https://github.com/kubernetes/minikube#installation)
|
||||
* Installed the [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. Download Argo CD
|
||||
|
||||
Download the latest Argo CD version from the [releases](https://github.com/argoproj/argo-cd/releases) page.
|
||||
|
||||
## 2. Install the Argo CD components
|
||||
```
|
||||
$ argocd install
|
||||
```
|
||||
This will create a new namespace, `argocd`, where Argo CD services and application resources will live.
|
||||
|
||||
## 3. Open access to Argo CD API server
|
||||
|
||||
By default, the Argo CD API server is not exposed with an external IP. To expose the API server,
|
||||
change service type to `LoadBalancer`:
|
||||
|
||||
```
|
||||
$ kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "LoadBalancer"}}'
|
||||
```
|
||||
|
||||
Export API server URL into `ARGOCD_SERVER` environment variable, which the CLI looks to for
|
||||
connection information:
|
||||
|
||||
```
|
||||
$ export ARGOCD_SERVER=$(minikube service argocd-server -n argocd --url | cut -d'/' -f 3)
|
||||
```
|
||||
|
||||
Now, Argo CD is able to talk to API server and you can deploy your first application.
|
||||
|
||||
## 4. Connect and deploy the Guestbook application
|
||||
|
||||
1. Register the minikube cluster to Argo CD:
|
||||
|
||||
```
|
||||
$ argocd cluster add minikube
|
||||
```
|
||||
The `argocd cluster add CONTEXT` command installs an `argocd-manager` ServiceAccount and ClusterRole into
|
||||
the cluster associated with the supplied kubectl context. Argo CD then uses the associated service account
|
||||
token to perform its required management tasks (i.e. deploy/monitoring).
|
||||
|
||||
2. Add the guestbook application and github repository containing the Guestbook application
|
||||
|
||||
```
|
||||
$ argocd app add --name guestbook --repo https://github.com/argoproj/argo-cd.git --path examples/guestbook --env minikube --dest-server https://$(minikube ip):8443
|
||||
```
|
||||
|
||||
Once the application is added, you can now see its status:
|
||||
|
||||
```
|
||||
$ argocd app list
|
||||
$ argocd app sync guestbook
|
||||
```
|
||||
|
||||
The application status is initially in an `OutOfSync` state, since the application has yet to be
|
||||
deployed, and no Kubernetes resouces have been created. To sync (deploy) the application, run:
|
||||
|
||||
```
|
||||
$ argocd app sync guestbook
|
||||
```
|
||||
|
||||
[](https://asciinema.org/a/uYnbFMy5WI2rc9S49oEAyGLb0)
|
||||
|
||||
Argo CD also allows to view and manager applications using web UI. Get the web UI URL by running:
|
||||
|
||||
```
|
||||
$ minikube service argocd-server -n argocd --url
|
||||
```
|
||||
|
||||

|
||||
16
docs/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# ArgoCD Documentation
|
||||
|
||||
## [Getting Started](getting_started.md)
|
||||
|
||||
## Concepts
|
||||
* [Architecture](architecture.md)
|
||||
* [Tracking Strategies](tracking_strategies.md)
|
||||
|
||||
## Features
|
||||
* [Application Sources](application_sources.md)
|
||||
* [Application Parameters](parameters.md)
|
||||
* [Resource Health](health.md)
|
||||
* [Resource Hooks](resource_hooks.md)
|
||||
* [Single Sign On](sso.md)
|
||||
* [Webhooks](webhook.md)
|
||||
* [RBAC](rbac.md)
|
||||
102
docs/application_sources.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# Application Source Types
|
||||
|
||||
ArgoCD supports several different ways in which kubernetes manifests can be defined:
|
||||
|
||||
* [ksonnet](https://ksonnet.io) applications
|
||||
* [helm](https://helm.sh) charts
|
||||
* Simple directory of YAML/json manifests
|
||||
|
||||
Some additional considerations should be made when deploying apps of a particular type:
|
||||
|
||||
## Ksonnet
|
||||
|
||||
### Environments
|
||||
Ksonnet has a first class concept of an "environment." To create an application from a ksonnet
|
||||
app directory, an environment must be specified. For example, the following command creates the
|
||||
"guestbook-default" app, which points to the `default` environment:
|
||||
|
||||
```
|
||||
argocd app create guestbook-default --repo https://github.com/argoproj/argocd-example-apps.git --path guestbook --env default
|
||||
```
|
||||
|
||||
### Parameters
|
||||
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:
|
||||
|
||||
```
|
||||
$ ks param list
|
||||
COMPONENT PARAM VALUE
|
||||
========= ===== =====
|
||||
guestbook-ui containerPort 80
|
||||
guestbook-ui image "gcr.io/heptio-images/ks-guestbook-demo:0.1"
|
||||
guestbook-ui name "guestbook-ui"
|
||||
guestbook-ui replicas 1
|
||||
guestbook-ui servicePort 80
|
||||
guestbook-ui type "LoadBalancer"
|
||||
```
|
||||
|
||||
When overriding ksonnet parameters in ArgoCD, the component name should also be specified in the
|
||||
`argocd app set` command, in the form of `-p COMPONENT=PARAM=VALUE`. For example:
|
||||
```
|
||||
argocd app set guestbook-default -p guestbook-ui=image=gcr.io/heptio-images/ks-guestbook-demo:0.1
|
||||
```
|
||||
|
||||
## Helm
|
||||
|
||||
### Values Files
|
||||
|
||||
Helm has the ability to use a different, or even multiple "values.yaml" files to derive its
|
||||
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:
|
||||
|
||||
```
|
||||
argocd app set helm-guestbook --values values-production.yaml
|
||||
```
|
||||
|
||||
### Helm Parameters
|
||||
|
||||
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:
|
||||
```
|
||||
helm template . --set service.type=LoadBalancer
|
||||
```
|
||||
Similarly ArgoCD can override values in the `values.yaml` parameters using `argo app set` command,
|
||||
in the form of `-p PARAM=VALUE`. For example:
|
||||
```
|
||||
argocd app set helm-guestbook -p service.type=LoadBalancer
|
||||
```
|
||||
|
||||
### Helm Hooks
|
||||
|
||||
Helm hooks are equivalent in concept to [ArgoCD resource hooks](resource_hooks.md). In helm, a hook
|
||||
is any normal kubernetes resource annotated with the `helm.sh/hook` annotation. When ArgoCD deploys
|
||||
helm application which contains helm hooks, all helm hook resources are currently ignored during
|
||||
the `kubectl apply` of the manifests. There is an
|
||||
[open issue](https://github.com/argoproj/argo-cd/issues/355) to map Helm hooks to ArgoCD's concept
|
||||
of Pre/Post/Sync hooks.
|
||||
|
||||
### Random Data
|
||||
|
||||
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):
|
||||
|
||||
```
|
||||
data:
|
||||
{{- if .Values.password }}
|
||||
redis-password: {{ .Values.password | b64enc | quote }}
|
||||
{{- else }}
|
||||
redis-password: {{ randAlphaNum 10 | b64enc | quote }}
|
||||
{{- end }}
|
||||
```
|
||||
|
||||
The ArgoCD 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:
|
||||
|
||||
```
|
||||
argocd app set redis -p password=abc123
|
||||
```
|
||||
@@ -9,9 +9,10 @@
|
||||
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 actions (e.g. manual sync, user-defined actions)
|
||||
* invoking of application operations (e.g. sync, rollback, user-defined actions)
|
||||
* repository and cluster credential management (stored as K8s secrets)
|
||||
* authentication and RBAC enforcement, with eventual integration with external identity providers
|
||||
* authentication and auth delegation to external identity providers
|
||||
* RBAC enforcement
|
||||
* listener/forwarder for git webhook events
|
||||
|
||||
### Repository Server
|
||||
@@ -21,15 +22,31 @@ manifests when provided the following inputs:
|
||||
* repository URL
|
||||
* git revision (commit, tag, branch)
|
||||
* application path
|
||||
* application environment
|
||||
* 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 out-of-sync application state and optionally takes corrective action. It
|
||||
is responsible for invoking any user-defined handlers (argo workflows) for Sync, OutOfSync events
|
||||
the git 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)
|
||||
|
||||
### Application CRD (Custom Resource Definition)
|
||||
The Application CRD is the Kubernetes resource object representing a deployed application instance
|
||||
in an environment. It holds a reference to the desired target state (repo, revision, app, environment)
|
||||
of which the application controller will enforce state against.
|
||||
in an environment. It is defined by two key pieces of information:
|
||||
* `source` reference to the desired state in git (repository, revision, path, environment)
|
||||
* `destination` reference to the target cluster and namespace.
|
||||
|
||||
An example spec is as follows:
|
||||
|
||||
```
|
||||
spec:
|
||||
project: default
|
||||
source:
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps.git
|
||||
targetRevision: HEAD
|
||||
path: guestbook
|
||||
environment: default
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: default
|
||||
```
|
||||
BIN
docs/argocd-ui.gif
Normal file
|
After Width: | Height: | Size: 3.5 MiB |
|
Before Width: | Height: | Size: 109 KiB |
BIN
docs/assets/connect_repo.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
docs/assets/create_app.png
Normal file
|
After Width: | Height: | Size: 99 KiB |
BIN
docs/assets/guestbook-app.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
docs/assets/guestbook-tree.png
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
docs/assets/oauth2-config.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
docs/assets/register-app.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
docs/assets/select_app.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
docs/assets/select_env.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
docs/assets/select_repo.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
docs/assets/webhook-config.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
177
docs/getting_started.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# ArgoCD Getting Started
|
||||
|
||||
An example guestbook application is provided to demonstrate how ArgoCD works.
|
||||
|
||||
## 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 ArgoCD
|
||||
```
|
||||
kubectl create namespace argocd
|
||||
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/v0.7.1/manifests/install.yaml
|
||||
```
|
||||
This will create a new namespace, `argocd`, where ArgoCD services and application resources will live.
|
||||
|
||||
NOTE:
|
||||
* On GKE with RBAC enabled, you may need to grant your account the ability to create new cluster roles
|
||||
```
|
||||
kubectl create clusterrolebinding YOURNAME-cluster-admin-binding --clusterrole=cluster-admin --user=YOUREMAIL@gmail.com
|
||||
```
|
||||
|
||||
## 2. Download ArgoCD CLI
|
||||
|
||||
Download the latest ArgoCD version:
|
||||
|
||||
On Mac:
|
||||
```
|
||||
brew install argoproj/tap/argocd
|
||||
```
|
||||
|
||||
On Linux:
|
||||
|
||||
```
|
||||
curl -sSL -o /usr/local/bin/argocd https://github.com/argoproj/argo-cd/releases/download/v0.7.1/argocd-linux-amd64
|
||||
chmod +x /usr/local/bin/argocd
|
||||
```
|
||||
|
||||
## 3. Open access to ArgoCD API server
|
||||
|
||||
By default, the ArgoCD API server is not exposed with an external IP. To expose the API server,
|
||||
change the service type to `LoadBalancer`:
|
||||
|
||||
```
|
||||
kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "LoadBalancer"}}'
|
||||
```
|
||||
|
||||
## 4. Login to the server from the CLI
|
||||
|
||||
Login with using the `admin` user. The initial password is autogenerated to be the pod name of the
|
||||
ArgoCD API server. This can be retrieved with the command:
|
||||
```
|
||||
kubectl get pods -n argocd -l app=argocd-server -o name | cut -d'/' -f 2
|
||||
```
|
||||
|
||||
Using the above password, login to ArgoCD's external IP:
|
||||
|
||||
On Minikube:
|
||||
```
|
||||
argocd login $(minikube service argocd-server -n argocd --url | cut -d'/' -f 3) --name minikube
|
||||
```
|
||||
Other clusters:
|
||||
```
|
||||
kubectl get svc -n argocd argocd-server
|
||||
argocd login <EXTERNAL-IP>
|
||||
```
|
||||
|
||||
After logging in, change the password using the command:
|
||||
```
|
||||
argocd account update-password
|
||||
argocd relogin
|
||||
```
|
||||
|
||||
|
||||
## 5. Register a cluster to deploy apps to
|
||||
|
||||
We will now register a cluster to deploy applications to. First list all clusters contexts in your
|
||||
kubconfig:
|
||||
```
|
||||
argocd cluster add
|
||||
```
|
||||
|
||||
Choose a context name from the list and supply it to `argocd cluster add CONTEXTNAME`. For example,
|
||||
for minikube context, run:
|
||||
```
|
||||
argocd cluster add minikube --in-cluster
|
||||
```
|
||||
|
||||
The above command installs an `argocd-manager` ServiceAccount and ClusterRole into the cluster
|
||||
associated with the supplied kubectl context. ArgoCD uses the service account token to perform its
|
||||
management tasks (i.e. deploy/monitoring).
|
||||
|
||||
The `--in-cluster` option indicates that the cluster we are registering, is the same cluster that
|
||||
ArgoCD is running in. This allows ArgoCD to connect to the cluster using the internal kubernetes
|
||||
hostname (kubernetes.default.svc). When registering a cluster external to ArgoCD, the `--in-cluster`
|
||||
flag should be omitted.
|
||||
|
||||
## 6. Create the application from a git repository
|
||||
|
||||
### Creating apps via UI
|
||||
|
||||
Open a browser to the ArgoCD external UI, and login using the credentials set in step 4.
|
||||
|
||||
On Minikube:
|
||||
```
|
||||
minikube service argocd-server -n argocd
|
||||
```
|
||||
|
||||
Connect a git repository containing your apps. An example repository containing a sample
|
||||
guestbook application is available at https://github.com/argoproj/argocd-example-apps.git.
|
||||
|
||||

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

|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
### Creating apps via CLI
|
||||
|
||||
Applications can be also be created using the ArgoCD CLI:
|
||||
|
||||
```
|
||||
argocd app create guestbook-default --repo https://github.com/argoproj/argocd-example-apps.git --path guestbook --env default
|
||||
```
|
||||
|
||||
## 7. Sync (deploy) the application
|
||||
|
||||
Once the guestbook application is created, you can now view its status:
|
||||
|
||||
From UI:
|
||||

|
||||
|
||||
From CLI:
|
||||
```
|
||||
$ argocd app get guestbook-default
|
||||
Name: guestbook-default
|
||||
Server: https://kubernetes.default.svc
|
||||
Namespace: default
|
||||
URL: https://192.168.64.36:31880/applications/argocd/guestbook-default
|
||||
Environment: default
|
||||
Repo: https://github.com/argoproj/argocd-example-apps.git
|
||||
Path: guestbook
|
||||
Target: HEAD
|
||||
|
||||
KIND NAME STATUS HEALTH
|
||||
Service guestbook-ui OutOfSync
|
||||
Deployment guestbook-ui OutOfSync
|
||||
```
|
||||
|
||||
The application status is initially in an `OutOfSync` state, since the application has yet to be
|
||||
deployed, and no Kubernetes resources have been created. To sync (deploy) the application, run:
|
||||
|
||||
```
|
||||
$ argocd app sync guestbook-default
|
||||
Application: guestbook-default
|
||||
Operation: Sync
|
||||
Phase: Succeeded
|
||||
Message: successfully synced
|
||||
|
||||
KIND NAME MESSAGE
|
||||
Service guestbook-ui service "guestbook-ui" created
|
||||
Deployment guestbook-ui deployment.apps "guestbook-ui" created
|
||||
```
|
||||
|
||||
This command retrieves the manifests from git 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:
|
||||
|
||||

|
||||
|
||||
## 8. Next Steps
|
||||
|
||||
ArgoCD supports additional features such as SSO, WebHooks, RBAC, Projects. See the rest of
|
||||
the [documentation](./) for details.
|
||||
18
docs/health.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# Resource Health
|
||||
|
||||
## Overview
|
||||
ArgoCD provides built-in health assessment for several standard Kubernetes types, which is then
|
||||
surfaced to the overall Application health status as a whole. The following checks are made for
|
||||
specific types of kuberentes resources:
|
||||
|
||||
### Deployment, ReplicaSet, StatefulSet DaemonSet
|
||||
|
||||
* Observed generation is equal to desired generation.
|
||||
* Number of **updated** replicas equals the number of desired replicas.
|
||||
|
||||
### Service
|
||||
* If service type is of type `LoadBalancer`, the `status.loadBalancer.ingress` list is non-empty,
|
||||
with at least one value for `hostname` or `IP`.
|
||||
|
||||
### Ingress
|
||||
* The `status.loadBalancer.ingress` list is non-empty, with at least one value for `hostname` or `IP`.
|
||||
39
docs/parameters.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Parameter Overrides
|
||||
|
||||
ArgoCD provides a mechanism to override the parameters of a ksonnet/helm app. This gives some extra
|
||||
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 redeploying an application by changing application parameters via ArgoCD, instead
|
||||
of making the 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 ArgoCD parameter
|
||||
overrides feature is provided mainly convenience to developers and is intended to be used more for
|
||||
dev/test environments, vs. production environments.
|
||||
|
||||
To use parameter overrides, run the `argocd app set -p (COMPONENT=)PARAM=VALUE` command:
|
||||
```
|
||||
argocd app set guestbook -p guestbook=image=example/guestbook:abcd123
|
||||
argocd app sync guestbook
|
||||
```
|
||||
|
||||
The following are situations where parameter overrides would be useful:
|
||||
|
||||
1. A team maintains a "dev" environment, which needs to be continually updated with the latest
|
||||
version of their guestbook application after every build in the tip of master. To address this use
|
||||
case, the application would expose an 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 systems. 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.
|
||||
|
||||
2. A repository of helm manifests is already publicly available (e.g. https://github.com/helm/charts).
|
||||
Since commit access to the repository is unavailable, it is useful to be able to install charts from
|
||||
the public repository, customizing the deployment with different parameters, without resorting to
|
||||
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:
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
103
docs/rbac.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# RBAC
|
||||
|
||||
## Overview
|
||||
|
||||
The feature RBAC allows restricting access to ArgoCD resources. ArgoCD does not have own user management system and has only one built-in user `admin`. The `admin` user is a
|
||||
superuser and it has full access. RBAC requires configuring [SSO](./sso.md) integration. Once [SSO](./sso.md) is connected you can define RBAC roles and map roles to groups.
|
||||
|
||||
## Configure RBAC
|
||||
|
||||
RBAC configuration allows defining roles and groups. ArgoCD has two pre-defined roles: role `role:readonly` which provides read-only access to all resources and role `role:admin`
|
||||
which provides full access. Role definitions are available in [builtin-policy.csv](../util/rbac/builtin-policy.csv) file.
|
||||
|
||||
Additional roles and groups can be configured in `argocd-rbac-cm` ConfigMap. The example below custom role `org-admin`. The role is assigned to any user which belongs to
|
||||
`your-github-org:your-team` group. All other users get `role:readonly` and cannot modify ArgoCD settings.
|
||||
|
||||
*ConfigMap `argocd-rbac-cm` example:*
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
data:
|
||||
policy.default: role:readonly
|
||||
policy.csv: |
|
||||
p, role:org-admin, applications, *, */*
|
||||
p, role:org-admin, applications/*, *, */*
|
||||
|
||||
p, role:org-admin, clusters, get, *
|
||||
p, role:org-admin, repositories, get, *
|
||||
p, role:org-admin, repositories/apps, get, *
|
||||
|
||||
p, role:org-admin, repositories, create, *
|
||||
p, role:org-admin, repositories, update, *
|
||||
p, role:org-admin, repositories, delete, *
|
||||
|
||||
g, your-github-org:your-team, role:org-admin
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-rbac-cm
|
||||
```
|
||||
|
||||
## Configure Projects
|
||||
|
||||
Argo projects allow grouping applications which is useful if ArgoCD is used by multiple teams. Additionally, projects restrict source repositories and destination
|
||||
Kubernetes clusters which can be used by applications belonging to the project.
|
||||
|
||||
### 1. Create new project
|
||||
|
||||
Following command creates project `myproject` which can deploy applications to namespace `default` of cluster `https://kubernetes.default.svc`. The valid application source is defined in the `https://github.com/argoproj/argocd-example-apps.git` repository.
|
||||
|
||||
```
|
||||
argocd proj create myproject -d https://kubernetes.default.svc,default -s https://github.com/argoproj/argocd-example-apps.git
|
||||
```
|
||||
|
||||
Project sources and destinations can be managed using commands
|
||||
```
|
||||
argocd project add-destination
|
||||
argocd project remove-destination
|
||||
argocd project add-source
|
||||
argocd project remove-source
|
||||
```
|
||||
|
||||
### 2. Assign application to a project
|
||||
|
||||
Each application belongs to a project. By default, all application belongs to the default project which provides access to any source repo/cluster. The application project can be
|
||||
changes using `app set` command:
|
||||
|
||||
```
|
||||
argocd app set guestbook-default --project myproject
|
||||
```
|
||||
|
||||
### 3. Update RBAC rules
|
||||
|
||||
Following example configure admin access for two teams. Each team has access only two application of one project (`team1` can access `default` project and `team2` can access
|
||||
`myproject` project).
|
||||
|
||||
*ConfigMap `argocd-rbac-cm` example:*
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
data:
|
||||
policy.default: ""
|
||||
policy.csv: |
|
||||
p, role:team1-admin, applications, *, default/*
|
||||
p, role:team1-admin, applications/*, *, default/*
|
||||
|
||||
p, role:team1-admin, applications, *, myproject/*
|
||||
p, role:team1-admin, applications/*, *, myproject/*
|
||||
|
||||
p, role:org-admin, clusters, get, *
|
||||
p, role:org-admin, repositories, get, *
|
||||
p, role:org-admin, repositories/apps, get, *
|
||||
|
||||
p, role:org-admin, repositories, create, *
|
||||
p, role:org-admin, repositories, update, *
|
||||
p, role:org-admin, repositories, delete, *
|
||||
|
||||
g, role:team1-admin, org-admin
|
||||
g, role:team2-admin, org-admin
|
||||
g, your-github-org:your-team1, role:team1-admin
|
||||
g, your-github-org:your-team2, role:team2-admin
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-rbac-cm
|
||||
```
|
||||
61
docs/resource_hooks.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Resource Hooks
|
||||
|
||||
## Overview
|
||||
|
||||
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).
|
||||
* Using a `PostSync` hook to run integration and health checks after a deployment.
|
||||
|
||||
## Usage
|
||||
Hooks are simply kubernetes manifests annotated with the `argocd.argoproj.io/hook` annotation. To
|
||||
make use of hooks, simply add the annotation to any resource:
|
||||
|
||||
```yaml
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
generateName: schema-migrate-
|
||||
annotations:
|
||||
argocd.argoproj.io/hook: PreSync
|
||||
```
|
||||
|
||||
During a Sync operation, ArgoCD will create the resource during the appropriate stage of the
|
||||
deployment. Hooks can be any type of Kuberentes resource kind, but tend to be most useful as
|
||||
[Kubernetes Jobs](https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/)
|
||||
or [Argo Workflows](https://github.com/argoproj/argo). Multiple hooks can be specified as a comma
|
||||
separated list.
|
||||
|
||||
## Available Hooks
|
||||
The following hooks are defined:
|
||||
|
||||
| Hook | Description |
|
||||
|------|-------------|
|
||||
| `PreSync` | Executes prior to the apply of the manifests. |
|
||||
| `Sync` | Executes after all `PreSync` hooks completed and were successful. Occurs in conjuction with the apply of the manifests. |
|
||||
| `Skip` | Indicates to ArgoCD to skip the apply of the manifest. This is typically used in conjunction with a `Sync` hook which is presumably handling the deployment in an alternate way (e.g. blue-green deployment) |
|
||||
| `PostSync` | Executes after all `Sync` hooks completed and were successful, a succcessful apply, and all resources in a `Healthy` state. |
|
||||
|
||||
|
||||
## Hook Deletion Policies
|
||||
|
||||
Hooks can be deleted in an automatic fashion using the annotation: `argocd.argoproj.io/hook-delete-policy`.
|
||||
|
||||
```yaml
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
generateName: integration-test-
|
||||
annotations:
|
||||
argocd.argoproj.io/hook: PostSync
|
||||
argocd.argoproj.io/hook-delete-policy: OnSuccess
|
||||
```
|
||||
|
||||
The following policies define when the hook will be deleted.
|
||||
|
||||
| Policy | Description |
|
||||
|--------|-------------|
|
||||
| `OnSuccess` | The hook resource is deleted after the hook succeeded (e.g. Job/Workflow completed successfully). |
|
||||
| `OnFailure` | The hook resource is deleted after the hook failed. |
|
||||
87
docs/sso.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# SSO Configuration
|
||||
|
||||
## Overview
|
||||
|
||||
ArgoCD embeds and bundles [Dex](https://github.com/coreos/dex) as part of its installation, for the
|
||||
purpose of delegating authentication to an external identity provider. Multiple types of identity
|
||||
providers are supported (OIDC, SAML, LDAP, GitHub, etc...). SSO configuration of ArgoCD requires
|
||||
editing the `argocd-cm` ConfigMap with
|
||||
[Dex connector](https://github.com/coreos/dex/tree/master/Documentation/connectors) settings.
|
||||
|
||||
This document describes how to configure ArgoCD SSO using GitHub (OAuth2) as an example, but the
|
||||
steps should be similar for other identity providers.
|
||||
|
||||
### 1. Register the application in the identity provider
|
||||
|
||||
In GitHub, register a new application. The callback address should be the `/api/dex/callback`
|
||||
endpoint of your ArgoCD 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 ArgoCD configmap.
|
||||
|
||||

|
||||
|
||||
### 2. Configure ArgoCD for SSO
|
||||
|
||||
Edit the argocd-cm configmap:
|
||||
```
|
||||
kubectl edit configmap argocd-cm
|
||||
```
|
||||
|
||||
* In the `url` key, input the base URL of ArgoCD. In this example, it is https://argocd.example.com
|
||||
* In the `dex.config` key, add the `github` connector to the `connectors` sub field. See Dex's
|
||||
[GitHub connector](https://github.com/coreos/dex/blob/master/Documentation/connectors/github.md)
|
||||
documentation for explanation of the fields. A minimal config should populate the clientID,
|
||||
clientSecret generated in Step 1.
|
||||
* You will very likely want to restrict logins to one ore more GitHub organization. In the
|
||||
`connectors.config.orgs` list, add one or more GitHub organizations. Any member of the org will
|
||||
then be able to login to ArgoCD to perform management tasks.
|
||||
|
||||
```
|
||||
data:
|
||||
url: https://argocd.example.com
|
||||
|
||||
dex.config: |
|
||||
connectors:
|
||||
# GitHub example
|
||||
- type: github
|
||||
id: github
|
||||
name: GitHub
|
||||
config:
|
||||
clientID: aabbccddeeff00112233
|
||||
clientSecret: $dex.github.clientSecret
|
||||
orgs:
|
||||
- name: your-github-org
|
||||
|
||||
# GitHub enterprise example
|
||||
- type: github
|
||||
id: acme-github
|
||||
name: Acme GitHub
|
||||
config:
|
||||
hostName: github.acme.com
|
||||
clientID: abcdefghijklmnopqrst
|
||||
clientSecret: $dex.acme.clientSecret
|
||||
orgs:
|
||||
- name: your-github-org
|
||||
|
||||
# OIDC example (e.g. Okta)
|
||||
- type: oidc
|
||||
id: okta
|
||||
name: Okta
|
||||
config:
|
||||
issuer: https://dev-123456.oktapreview.com
|
||||
clientID: aaaabbbbccccddddeee
|
||||
clientSecret: $dex.okta.clientSecret
|
||||
```
|
||||
|
||||
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.
|
||||
ArgoCD will automatically use the correct `redirectURI` for any OAuth2 connectors, to match the
|
||||
correct external callback URL (e.g. https://argocd.example.com/api/dex/callback)
|
||||
|
||||
45
docs/tracking_strategies.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Tracking and Deployment Strategies
|
||||
|
||||
An ArgoCD 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
|
||||
the target environment.
|
||||
|
||||
## Branch Tracking
|
||||
|
||||
If a branch name is specified, ArgoCD will continually compare live state against the resource
|
||||
manifests defined at the tip of the specified branch.
|
||||
|
||||
To redeploy an application, a user makes changes to the manifests, and commit/pushes those the
|
||||
changes to the tracked branch, which will then be detected by ArgoCD controller.
|
||||
|
||||
## Tag Tracking
|
||||
|
||||
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
|
||||
different commit SHA. ArgoCD 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
|
||||
the specified commit. This is the most restrictive of the techniques and is typically used to
|
||||
control production environments.
|
||||
|
||||
Since commit SHAs cannot change meaning, the only way to change the live state of an application
|
||||
which is pinned to a commit, is by updating the tracking revision in the application to a different
|
||||
commit containing the new manifests. Note that [parameter overrides](parameters.md) can still be set
|
||||
on an application which is pinned to a revision.
|
||||
|
||||
## Auto-Sync [(Not Yet Implemented)]((https://github.com/argoproj/argo-cd/issues/79))
|
||||
|
||||
In all tracking strategies, the application will have the option to sync automatically. If auto-sync
|
||||
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
|
||||
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.
|
||||
63
docs/webhook.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# Git Webhook Configuration
|
||||
|
||||
## Overview
|
||||
|
||||
ArgoCD will poll git repositories every three minutes for changes to the manifests. To eliminate
|
||||
this delay from polling, the API server can be configured to receive webhook events. ArgoCD 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.
|
||||
|
||||
### 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 ArgoCD instance
|
||||
(e.g. https://argocd.example.com/api/webhook). Input an arbitrary value in the secret. The same
|
||||
value will be used when configuring the webhook in step 2.
|
||||
|
||||

|
||||
|
||||
### 2. Configure ArgoCD with the webhook secret
|
||||
|
||||
In the `argocd-secret` kubernetes secret, configure one of the following keys with the git provider
|
||||
webhook secret configured in step 1.
|
||||
|
||||
| Provider | K8s Secret Key |
|
||||
|---------- | ------------------------ |
|
||||
| GitHub | `github.webhook.secret` |
|
||||
| GitLab | `gitlab.webhook.secret` |
|
||||
| BitBucket | `bitbucket.webhook.uuid` |
|
||||
|
||||
Edit the ArgoCD kubernetes secret:
|
||||
```
|
||||
kubectl edit secret argocd-secret
|
||||
```
|
||||
|
||||
TIP: for ease of entering secrets, kubernetes supports inputting secrets in the `stringData` field,
|
||||
which saves you the trouble of base64 encoding the values and copying it to the `data` field.
|
||||
Simply copy the shared webhook secret created in step 1, to the corresponding
|
||||
GitHub/GitLab/BitBucket key under the `stringData` field:
|
||||
|
||||
|
||||
```
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: argocd-secret
|
||||
namespace: argocd
|
||||
type: Opaque
|
||||
data:
|
||||
...
|
||||
|
||||
stringData:
|
||||
# github webhook secret
|
||||
github.webhook.secret: shhhh! it's a github secret
|
||||
|
||||
# gitlab webhook secret
|
||||
gitlab.webhook.secret: shhhh! it's a gitlab secret
|
||||
|
||||
# bitbucket webhook secret
|
||||
bitbucket.webhook.uuid: your-bitbucket-uuid
|
||||
|
||||
```
|
||||
|
||||
After saving, the changes should take affect automatically.
|
||||
@@ -23,7 +23,7 @@ local appDeployment = deployment
|
||||
params.replicas,
|
||||
container
|
||||
.new(params.name, params.image)
|
||||
.withPorts(containerPort.new(targetPort)),
|
||||
labels);
|
||||
.withPorts(containerPort.new(targetPort)) + if params.command != null then { command: [ params.command ] } else {},
|
||||
labels).withProgressDeadlineSeconds(5);
|
||||
|
||||
k.core.v1.list.new([appService, appDeployment])
|
||||
@@ -12,7 +12,8 @@
|
||||
name: "guestbook-ui",
|
||||
replicas: 1,
|
||||
servicePort: 80,
|
||||
type: "LoadBalancer",
|
||||
type: "ClusterIP",
|
||||
command: null,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"Vendor": true,
|
||||
"DisableAll": true,
|
||||
"Deadline": "3m",
|
||||
"Deadline": "8m",
|
||||
"Enable": [
|
||||
"vet",
|
||||
"gofmt",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#! /usr/bin/env bash
|
||||
|
||||
# This script auto-generates protobuf related files. It is intended to be run manually when either
|
||||
# API types are added/modified, or server gRPC calls are added. The generated files should then
|
||||
@@ -39,18 +39,28 @@ go-to-protobuf \
|
||||
--apimachinery-packages=$(IFS=, ; echo "${APIMACHINERY_PKGS[*]}") \
|
||||
--proto-import=./vendor
|
||||
|
||||
# protoc-gen-go or protoc-gen-gofast is used to build server/*/<service>.pb.go from .proto files
|
||||
# NOTE: it is possible to use golang/protobuf or gogo/protobuf interchangeably
|
||||
go build -i -o dist/protoc-gen-gofast ./vendor/github.com/gogo/protobuf/protoc-gen-gofast
|
||||
# Either protoc-gen-go, protoc-gen-gofast, or protoc-gen-gogofast can be used to build
|
||||
# server/*/<service>.pb.go from .proto files. golang/protobuf and gogo/protobuf can be used
|
||||
# interchangeably. The difference in the options are:
|
||||
# 1. protoc-gen-go - official golang/protobuf
|
||||
#go build -i -o dist/protoc-gen-go ./vendor/github.com/golang/protobuf/protoc-gen-go
|
||||
#GOPROTOBINARY=go
|
||||
# 2. protoc-gen-gofast - fork of golang golang/protobuf. Faster code generation
|
||||
#go build -i -o dist/protoc-gen-gofast ./vendor/github.com/gogo/protobuf/protoc-gen-gofast
|
||||
#GOPROTOBINARY=gofast
|
||||
# 3. protoc-gen-gogofast - faster code generation and gogo extensions and flexibility in controlling
|
||||
# the generated go code (e.g. customizing field names, nullable fields)
|
||||
go build -i -o dist/protoc-gen-gogofast ./vendor/github.com/gogo/protobuf/protoc-gen-gogofast
|
||||
GOPROTOBINARY=gogofast
|
||||
|
||||
# protoc-gen-grpc-gateway is used to build <service>.pb.gw.go files from from .proto files
|
||||
go build -i -o dist/protoc-gen-grpc-gateway ./vendor/github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
|
||||
# protoc-gen-swagger is used to build swagger.json
|
||||
go build -i -o dist/protoc-gen-swagger ./vendor/github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
|
||||
|
||||
# Generate server/<service>/(<service>.pb.go|<service>.pb.gw.go)
|
||||
PROTO_FILES=$(find $PROJECT_ROOT \( -name "*.proto" -and -path '*/server/*' -or -path '*/reposerver/*' -and -name "*.proto" \))
|
||||
for i in ${PROTO_FILES}; do
|
||||
|
||||
# Path to the google API gateway annotations.proto will be different depending if we are
|
||||
# building natively (e.g. from workspace) vs. part of a docker build.
|
||||
if [ -f /.dockerenv ]; then
|
||||
@@ -67,7 +77,46 @@ for i in ${PROTO_FILES}; do
|
||||
-I$GOPATH/src \
|
||||
-I${GOOGLE_PROTO_API_PATH} \
|
||||
-I${GOGO_PROTOBUF_PATH} \
|
||||
--go_out=plugins=grpc:$GOPATH/src \
|
||||
--${GOPROTOBINARY}_out=plugins=grpc:$GOPATH/src \
|
||||
--grpc-gateway_out=logtostderr=true:$GOPATH/src \
|
||||
--swagger_out=logtostderr=true:. \
|
||||
$i
|
||||
done
|
||||
|
||||
# collect_swagger gathers swagger files into a subdirectory
|
||||
collect_swagger() {
|
||||
SWAGGER_ROOT="$1"
|
||||
EXPECTED_COLLISIONS="$2"
|
||||
SWAGGER_OUT="${SWAGGER_ROOT}/swagger.json"
|
||||
PRIMARY_SWAGGER=`mktemp`
|
||||
COMBINED_SWAGGER=`mktemp`
|
||||
|
||||
cat <<EOF > "${PRIMARY_SWAGGER}"
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"title": "Consolidate Services",
|
||||
"description": "Description of all APIs",
|
||||
"version": "version not set"
|
||||
},
|
||||
"paths": {}
|
||||
}
|
||||
EOF
|
||||
|
||||
/bin/rm -f "${SWAGGER_OUT}"
|
||||
|
||||
/usr/bin/find "${SWAGGER_ROOT}" -name '*.swagger.json' -exec /usr/local/bin/swagger mixin -c "${EXPECTED_COLLISIONS}" "${PRIMARY_SWAGGER}" '{}' \+ > "${COMBINED_SWAGGER}"
|
||||
/usr/local/bin/jq -r 'del(.definitions[].properties[]? | select(."$ref"!=null and .description!=null).description) | del(.definitions[].properties[]? | select(."$ref"!=null and .title!=null).title)' "${COMBINED_SWAGGER}" > "${SWAGGER_OUT}"
|
||||
|
||||
/bin/rm "${PRIMARY_SWAGGER}" "${COMBINED_SWAGGER}"
|
||||
}
|
||||
|
||||
# clean up generated swagger files (should come after collect_swagger)
|
||||
clean_swagger() {
|
||||
SWAGGER_ROOT="$1"
|
||||
/usr/bin/find "${SWAGGER_ROOT}" -name '*.swagger.json' -delete
|
||||
}
|
||||
|
||||
collect_swagger server 15
|
||||
clean_swagger server
|
||||
clean_swagger reposerver
|
||||
|
||||
168
hack/migrate/migrate0.3.0to0.4.0.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/server/repository"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
|
||||
// origRepoURLToSecretName hashes repo URL to the secret name using a formula.
|
||||
// Part of the original repo name is incorporated for debugging purposes
|
||||
func origRepoURLToSecretName(repo string) string {
|
||||
repo = git.NormalizeGitURL(repo)
|
||||
h := fnv.New32a()
|
||||
_, _ = h.Write([]byte(repo))
|
||||
parts := strings.Split(strings.TrimSuffix(repo, ".git"), "/")
|
||||
return fmt.Sprintf("repo-%s-%v", strings.ToLower(parts[len(parts)-1]), h.Sum32())
|
||||
}
|
||||
|
||||
// repoURLToSecretName hashes repo URL to the secret name using a formula.
|
||||
// Part of the original repo name is incorporated for debugging purposes
|
||||
func repoURLToSecretName(repo string) string {
|
||||
repo = strings.ToLower(git.NormalizeGitURL(repo))
|
||||
h := fnv.New32a()
|
||||
_, _ = h.Write([]byte(repo))
|
||||
parts := strings.Split(strings.TrimSuffix(repo, ".git"), "/")
|
||||
return fmt.Sprintf("repo-%s-%v", parts[len(parts)-1], h.Sum32())
|
||||
}
|
||||
|
||||
// RenameSecret renames a Kubernetes secret in a given namespace.
|
||||
func renameSecret(namespace, oldName, newName string) {
|
||||
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||
loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig
|
||||
overrides := clientcmd.ConfigOverrides{}
|
||||
clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &overrides)
|
||||
|
||||
log.Printf("Renaming secret %q to %q in namespace %q\n", oldName, newName, namespace)
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
if err != nil {
|
||||
log.Println("Could not retrieve client config: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
kubeclientset := kubernetes.NewForConfigOrDie(config)
|
||||
repoSecret, err := kubeclientset.CoreV1().Secrets(namespace).Get(oldName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
log.Println("Could not retrieve old secret: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
repoSecret.ObjectMeta.Name = newName
|
||||
repoSecret.ObjectMeta.ResourceVersion = ""
|
||||
|
||||
repoSecret, err = kubeclientset.CoreV1().Secrets(namespace).Create(repoSecret)
|
||||
if err != nil {
|
||||
log.Println("Could not create new secret: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = kubeclientset.CoreV1().Secrets(namespace).Delete(oldName, &metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
log.Println("Could not remove old secret: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// RenameRepositorySecrets ensures that repository secrets use the new naming format.
|
||||
func renameRepositorySecrets(clientOpts argocdclient.ClientOptions, namespace string) {
|
||||
conn, repoIf := argocdclient.NewClientOrDie(&clientOpts).NewRepoClientOrDie()
|
||||
defer util.Close(conn)
|
||||
repos, err := repoIf.List(context.Background(), &repository.RepoQuery{})
|
||||
if err != nil {
|
||||
log.Println("An error occurred, so skipping secret renaming: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("Renaming repository secrets...")
|
||||
for _, repo := range repos.Items {
|
||||
oldSecretName := origRepoURLToSecretName(repo.Repo)
|
||||
newSecretName := repoURLToSecretName(repo.Repo)
|
||||
if oldSecretName != newSecretName {
|
||||
log.Printf("Repo %q had its secret name change, so updating\n", repo.Repo)
|
||||
renameSecret(namespace, oldSecretName, newSecretName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// PopulateAppDestinations ensures that apps have a Server and Namespace set explicitly.
|
||||
func populateAppDestinations(clientOpts argocdclient.ClientOptions) {
|
||||
conn, appIf := argocdclient.NewClientOrDie(&clientOpts).NewApplicationClientOrDie()
|
||||
defer util.Close(conn)
|
||||
apps, err := appIf.List(context.Background(), &application.ApplicationQuery{})
|
||||
if err != nil {
|
||||
log.Println("An error occurred, so skipping destination population: ", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("Populating app Destination fields")
|
||||
for _, app := range apps.Items {
|
||||
changed := false
|
||||
|
||||
log.Printf("Ensuring destination field is populated on app %q\n", app.ObjectMeta.Name)
|
||||
if app.Spec.Destination.Server == "" {
|
||||
if app.Status.ComparisonResult.Status == appv1.ComparisonStatusUnknown || app.Status.ComparisonResult.Status == appv1.ComparisonStatusError {
|
||||
log.Printf("App %q was missing Destination.Server, but could not fill it in: %s", app.ObjectMeta.Name, app.Status.ComparisonResult.Status)
|
||||
} else {
|
||||
log.Printf("App %q was missing Destination.Server, so setting to %q\n", app.ObjectMeta.Name, app.Status.ComparisonResult.Server)
|
||||
app.Spec.Destination.Server = app.Status.ComparisonResult.Server
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if app.Spec.Destination.Namespace == "" {
|
||||
if app.Status.ComparisonResult.Status == appv1.ComparisonStatusUnknown || app.Status.ComparisonResult.Status == appv1.ComparisonStatusError {
|
||||
log.Printf("App %q was missing Destination.Namespace, but could not fill it in: %s", app.ObjectMeta.Name, app.Status.ComparisonResult.Status)
|
||||
} else {
|
||||
log.Printf("App %q was missing Destination.Namespace, so setting to %q\n", app.ObjectMeta.Name, app.Status.ComparisonResult.Namespace)
|
||||
app.Spec.Destination.Namespace = app.Status.ComparisonResult.Namespace
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
if changed {
|
||||
_, err = appIf.UpdateSpec(context.Background(), &application.ApplicationSpecRequest{
|
||||
AppName: app.Name,
|
||||
Spec: &app.Spec,
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("An error occurred (but continuing anyway): ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 3 {
|
||||
log.Fatalf("USAGE: %s SERVER NAMESPACE\n", os.Args[0])
|
||||
}
|
||||
server, namespace := os.Args[1], os.Args[2]
|
||||
log.Printf("Using argocd server %q and namespace %q\n", server, namespace)
|
||||
|
||||
isLocalhost := false
|
||||
switch {
|
||||
case strings.HasPrefix(server, "localhost:"):
|
||||
isLocalhost = true
|
||||
case strings.HasPrefix(server, "127.0.0.1:"):
|
||||
isLocalhost = true
|
||||
}
|
||||
|
||||
clientOpts := argocdclient.ClientOptions{
|
||||
ServerAddr: server,
|
||||
Insecure: true,
|
||||
PlainText: isLocalhost,
|
||||
}
|
||||
renameRepositorySecrets(clientOpts, namespace)
|
||||
//populateAppDestinations(clientOpts)
|
||||
}
|
||||
11
hack/update-manifests.sh
Executable file
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
|
||||
IMAGE_NAMESPACE=${IMAGE_NAMESPACE:='argoproj'}
|
||||
IMAGE_TAG=${IMAGE_TAG:='latest'}
|
||||
|
||||
for i in "$(ls manifests/components/*.yaml)"; do
|
||||
sed -i '' 's@\( image: \(.*\)/\(argocd-.*\):.*\)@ image: '"${IMAGE_NAMESPACE}"'/\3:'"${IMAGE_TAG}"'@g' $i
|
||||
done
|
||||
|
||||
echo "# This is an auto-generated file. DO NOT EDIT" > manifests/install.yaml
|
||||
cat manifests/components/*.yaml >> manifests/install.yaml
|
||||
@@ -1,324 +0,0 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/diff"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/gobuffalo/packr"
|
||||
"github.com/yudai/gojsondiff/formatter"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
appsv1beta2 "k8s.io/api/apps/v1beta2"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
|
||||
apierr "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
var (
|
||||
// These values will be overridden by the link flags during build
|
||||
// (e.g. imageTag will use the official release tag on tagged builds)
|
||||
imageNamespace = "argoproj"
|
||||
imageTag = "latest"
|
||||
|
||||
// Default namespace and image names which `argocd install` uses during install
|
||||
DefaultInstallNamespace = "argocd"
|
||||
DefaultControllerImage = imageNamespace + "/argocd-application-controller:" + imageTag
|
||||
DefaultUIImage = imageNamespace + "/argocd-ui:" + imageTag
|
||||
DefaultServerImage = imageNamespace + "/argocd-server:" + imageTag
|
||||
DefaultRepoServerImage = imageNamespace + "/argocd-repo-server:" + imageTag
|
||||
)
|
||||
|
||||
// InstallOptions stores a collection of installation settings.
|
||||
type InstallOptions struct {
|
||||
DryRun bool
|
||||
Upgrade bool
|
||||
ConfigSuperuser bool
|
||||
ConfigMap string
|
||||
Namespace string
|
||||
ControllerImage string
|
||||
UIImage string
|
||||
ServerImage string
|
||||
RepoServerImage string
|
||||
ImagePullPolicy string
|
||||
}
|
||||
|
||||
type Installer struct {
|
||||
InstallOptions
|
||||
box packr.Box
|
||||
config *rest.Config
|
||||
dynClientPool dynamic.ClientPool
|
||||
disco discovery.DiscoveryInterface
|
||||
}
|
||||
|
||||
func NewInstaller(config *rest.Config, opts InstallOptions) (*Installer, error) {
|
||||
shallowCopy := *config
|
||||
inst := Installer{
|
||||
InstallOptions: opts,
|
||||
box: packr.NewBox("./manifests"),
|
||||
config: &shallowCopy,
|
||||
}
|
||||
var err error
|
||||
inst.dynClientPool = dynamic.NewDynamicClientPool(inst.config)
|
||||
inst.disco, err = discovery.NewDiscoveryClientForConfig(inst.config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &inst, nil
|
||||
}
|
||||
|
||||
func (i *Installer) Install() {
|
||||
i.InstallNamespace()
|
||||
i.InstallApplicationCRD()
|
||||
i.InstallApplicationController()
|
||||
i.InstallArgoCDServer()
|
||||
i.InstallArgoCDRepoServer()
|
||||
}
|
||||
|
||||
func (i *Installer) Uninstall() {
|
||||
manifests := i.box.List()
|
||||
for _, manifestPath := range manifests {
|
||||
if strings.HasSuffix(manifestPath, ".yaml") || strings.HasSuffix(manifestPath, ".yml") {
|
||||
var obj unstructured.Unstructured
|
||||
i.unmarshalManifest(manifestPath, &obj)
|
||||
if obj.GetKind() == "Namespace" {
|
||||
// Don't delete namespaces
|
||||
continue
|
||||
}
|
||||
i.MustUninstallResource(&obj)
|
||||
}
|
||||
}
|
||||
|
||||
// i.InstallNamespace()
|
||||
// i.InstallApplicationCRD()
|
||||
// i.InstallApplicationController()
|
||||
// i.InstallArgoCDServer()
|
||||
// i.InstallArgoCDRepoServer()
|
||||
}
|
||||
|
||||
func (i *Installer) InstallNamespace() {
|
||||
if i.Namespace != DefaultInstallNamespace {
|
||||
// don't create namespace if a different one was supplied
|
||||
return
|
||||
}
|
||||
var namespace apiv1.Namespace
|
||||
i.unmarshalManifest("00_namespace.yaml", &namespace)
|
||||
namespace.ObjectMeta.Name = i.Namespace
|
||||
i.MustInstallResource(kube.MustToUnstructured(&namespace))
|
||||
}
|
||||
|
||||
func (i *Installer) InstallApplicationCRD() {
|
||||
var applicationCRD apiextensionsv1beta1.CustomResourceDefinition
|
||||
i.unmarshalManifest("01_application-crd.yaml", &applicationCRD)
|
||||
i.MustInstallResource(kube.MustToUnstructured(&applicationCRD))
|
||||
}
|
||||
|
||||
func (i *Installer) InstallApplicationController() {
|
||||
var applicationControllerServiceAccount apiv1.ServiceAccount
|
||||
var applicationControllerRole rbacv1.Role
|
||||
var applicationControllerRoleBinding rbacv1.RoleBinding
|
||||
var applicationControllerDeployment appsv1beta2.Deployment
|
||||
i.unmarshalManifest("02a_application-controller-sa.yaml", &applicationControllerServiceAccount)
|
||||
i.unmarshalManifest("02b_application-controller-role.yaml", &applicationControllerRole)
|
||||
i.unmarshalManifest("02c_application-controller-rolebinding.yaml", &applicationControllerRoleBinding)
|
||||
i.unmarshalManifest("02d_application-controller-deployment.yaml", &applicationControllerDeployment)
|
||||
applicationControllerDeployment.Spec.Template.Spec.Containers[0].Image = i.ControllerImage
|
||||
applicationControllerDeployment.Spec.Template.Spec.Containers[0].ImagePullPolicy = apiv1.PullPolicy(i.ImagePullPolicy)
|
||||
i.MustInstallResource(kube.MustToUnstructured(&applicationControllerServiceAccount))
|
||||
i.MustInstallResource(kube.MustToUnstructured(&applicationControllerRole))
|
||||
i.MustInstallResource(kube.MustToUnstructured(&applicationControllerRoleBinding))
|
||||
i.MustInstallResource(kube.MustToUnstructured(&applicationControllerDeployment))
|
||||
}
|
||||
|
||||
func (i *Installer) InstallArgoCDServer() {
|
||||
var argoCDServerServiceAccount apiv1.ServiceAccount
|
||||
var argoCDServerControllerRole rbacv1.Role
|
||||
var argoCDServerControllerRoleBinding rbacv1.RoleBinding
|
||||
var argoCDServerControllerDeployment appsv1beta2.Deployment
|
||||
var argoCDServerService apiv1.Service
|
||||
i.unmarshalManifest("03a_argocd-server-sa.yaml", &argoCDServerServiceAccount)
|
||||
i.unmarshalManifest("03b_argocd-server-role.yaml", &argoCDServerControllerRole)
|
||||
i.unmarshalManifest("03c_argocd-server-rolebinding.yaml", &argoCDServerControllerRoleBinding)
|
||||
i.unmarshalManifest("03d_argocd-server-deployment.yaml", &argoCDServerControllerDeployment)
|
||||
i.unmarshalManifest("03e_argocd-server-service.yaml", &argoCDServerService)
|
||||
argoCDServerControllerDeployment.Spec.Template.Spec.InitContainers[0].Image = i.UIImage
|
||||
argoCDServerControllerDeployment.Spec.Template.Spec.InitContainers[0].ImagePullPolicy = apiv1.PullPolicy(i.ImagePullPolicy)
|
||||
argoCDServerControllerDeployment.Spec.Template.Spec.Containers[0].Image = i.ServerImage
|
||||
argoCDServerControllerDeployment.Spec.Template.Spec.Containers[0].ImagePullPolicy = apiv1.PullPolicy(i.ImagePullPolicy)
|
||||
|
||||
kubeclientset, err := kubernetes.NewForConfig(i.config)
|
||||
errors.CheckError(err)
|
||||
|
||||
configManager := util.NewConfigManager(kubeclientset, i.Namespace, i.ConfigMap)
|
||||
errors.CheckError(err)
|
||||
|
||||
if i.InstallOptions.ConfigMap != "" {
|
||||
quotedConfigMapName := strconv.Quote(i.InstallOptions.ConfigMap)
|
||||
container := &argoCDServerControllerDeployment.Spec.Template.Spec.Containers[0]
|
||||
container.Command = append(container.Command, "--config-map", quotedConfigMapName)
|
||||
}
|
||||
|
||||
i.MustInstallResource(kube.MustToUnstructured(&argoCDServerServiceAccount))
|
||||
i.MustInstallResource(kube.MustToUnstructured(&argoCDServerControllerRole))
|
||||
i.MustInstallResource(kube.MustToUnstructured(&argoCDServerControllerRoleBinding))
|
||||
i.MustInstallResource(kube.MustToUnstructured(&argoCDServerControllerDeployment))
|
||||
i.MustInstallResource(kube.MustToUnstructured(&argoCDServerService))
|
||||
|
||||
if i.InstallOptions.ConfigSuperuser {
|
||||
inputReader := bufio.NewReader(os.Stdin)
|
||||
|
||||
fmt.Print("*** Please enter a superuser username: ")
|
||||
rootUsername, err := inputReader.ReadString('\n')
|
||||
errors.CheckError(err)
|
||||
rootUsername = strings.Trim(rootUsername, "\n")
|
||||
|
||||
fmt.Print("*** Please enter a superuser password: ")
|
||||
rawPassword, err := terminal.ReadPassword(syscall.Stdin)
|
||||
errors.CheckError(err)
|
||||
fmt.Print("\n")
|
||||
|
||||
err = configManager.SetRootUserCredentials(rootUsername, string(rawPassword))
|
||||
errors.CheckError(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Installer) InstallArgoCDRepoServer() {
|
||||
var argoCDRepoServerControllerDeployment appsv1beta2.Deployment
|
||||
var argoCDRepoServerService apiv1.Service
|
||||
i.unmarshalManifest("04a_argocd-repo-server-deployment.yaml", &argoCDRepoServerControllerDeployment)
|
||||
i.unmarshalManifest("04b_argocd-repo-server-service.yaml", &argoCDRepoServerService)
|
||||
argoCDRepoServerControllerDeployment.Spec.Template.Spec.Containers[0].Image = i.RepoServerImage
|
||||
argoCDRepoServerControllerDeployment.Spec.Template.Spec.Containers[0].ImagePullPolicy = apiv1.PullPolicy(i.ImagePullPolicy)
|
||||
i.MustInstallResource(kube.MustToUnstructured(&argoCDRepoServerControllerDeployment))
|
||||
i.MustInstallResource(kube.MustToUnstructured(&argoCDRepoServerService))
|
||||
}
|
||||
|
||||
func (i *Installer) unmarshalManifest(fileName string, obj interface{}) {
|
||||
yamlBytes, err := i.box.MustBytes(fileName)
|
||||
errors.CheckError(err)
|
||||
err = yaml.Unmarshal(yamlBytes, obj)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
func (i *Installer) MustInstallResource(obj *unstructured.Unstructured) *unstructured.Unstructured {
|
||||
obj, err := i.InstallResource(obj)
|
||||
errors.CheckError(err)
|
||||
return obj
|
||||
}
|
||||
|
||||
func isNamespaced(obj *unstructured.Unstructured) bool {
|
||||
switch obj.GetKind() {
|
||||
case "Namespace", "ClusterRole", "ClusterRoleBinding", "CustomResourceDefinition":
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// InstallResource creates or updates a resource. If installed resource is up-to-date, does nothing
|
||||
func (i *Installer) InstallResource(obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
|
||||
if isNamespaced(obj) {
|
||||
obj.SetNamespace(i.Namespace)
|
||||
}
|
||||
// remove 'creationTimestamp' and 'status' fields from object so that the diff will not be modified
|
||||
obj.SetCreationTimestamp(metav1.Time{})
|
||||
delete(obj.Object, "status")
|
||||
if i.DryRun {
|
||||
printYAML(obj)
|
||||
return nil, nil
|
||||
}
|
||||
gvk := obj.GroupVersionKind()
|
||||
dclient, err := i.dynClientPool.ClientForGroupVersionKind(gvk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apiResource, err := kube.ServerResourceForGroupVersionKind(i.disco, gvk)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reIf := dclient.Resource(apiResource, i.Namespace)
|
||||
liveObj, err := reIf.Create(obj)
|
||||
if err == nil {
|
||||
fmt.Printf("%s '%s' created\n", liveObj.GetKind(), liveObj.GetName())
|
||||
return liveObj, nil
|
||||
}
|
||||
if !apierr.IsAlreadyExists(err) {
|
||||
return nil, err
|
||||
}
|
||||
liveObj, err = reIf.Get(obj.GetName(), metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
diffRes := diff.Diff(obj, liveObj)
|
||||
if !diffRes.Modified {
|
||||
fmt.Printf("%s '%s' up-to-date\n", liveObj.GetKind(), liveObj.GetName())
|
||||
return liveObj, nil
|
||||
}
|
||||
if !i.Upgrade {
|
||||
log.Println(diffRes.ASCIIFormat(obj, formatter.AsciiFormatterConfig{}))
|
||||
return nil, fmt.Errorf("%s '%s' already exists. Rerun with --upgrade to update", obj.GetKind(), obj.GetName())
|
||||
|
||||
}
|
||||
liveObj, err = reIf.Update(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Printf("%s '%s' updated\n", liveObj.GetKind(), liveObj.GetName())
|
||||
return liveObj, nil
|
||||
}
|
||||
|
||||
func (i *Installer) MustUninstallResource(obj *unstructured.Unstructured) {
|
||||
err := i.UninstallResource(obj)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
// UninstallResource deletes a resource from the cluster
|
||||
func (i *Installer) UninstallResource(obj *unstructured.Unstructured) error {
|
||||
if isNamespaced(obj) {
|
||||
obj.SetNamespace(i.Namespace)
|
||||
}
|
||||
gvk := obj.GroupVersionKind()
|
||||
dclient, err := i.dynClientPool.ClientForGroupVersionKind(gvk)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiResource, err := kube.ServerResourceForGroupVersionKind(i.disco, gvk)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reIf := dclient.Resource(apiResource, i.Namespace)
|
||||
deletePolicy := metav1.DeletePropagationForeground
|
||||
err = reIf.Delete(obj.GetName(), &metav1.DeleteOptions{PropagationPolicy: &deletePolicy})
|
||||
if err != nil {
|
||||
if apierr.IsNotFound(err) {
|
||||
fmt.Printf("%s '%s' not found\n", obj.GetKind(), obj.GetName())
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
fmt.Printf("%s '%s' deleted\n", obj.GetKind(), obj.GetName())
|
||||
return nil
|
||||
}
|
||||
|
||||
func printYAML(obj interface{}) {
|
||||
objBytes, err := yaml.Marshal(obj)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to marshal %v", obj)
|
||||
}
|
||||
fmt.Printf("---\n%s\n", string(objBytes))
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: argocd
|
||||
@@ -1,32 +0,0 @@
|
||||
apiVersion: apps/v1beta2
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: argocd-server
|
||||
namespace: argocd
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: argocd-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: argocd-server
|
||||
spec:
|
||||
serviceAccountName: argocd-server
|
||||
initContainers:
|
||||
- command: [cp, -r, /app, /shared]
|
||||
image: argoproj/argocd-ui:latest
|
||||
name: argocd-server-ui
|
||||
volumeMounts:
|
||||
- mountPath: /shared
|
||||
name: static-files
|
||||
containers:
|
||||
- command: [/argocd-server, --staticassets, /shared/app, --repo-server, 'argocd-repo-server:8081']
|
||||
image: argoproj/argocd-server:latest
|
||||
name: argocd-server
|
||||
volumeMounts:
|
||||
- mountPath: /shared
|
||||
name: static-files
|
||||
volumes:
|
||||
- emptyDir: {}
|
||||
name: static-files
|
||||
@@ -1,3 +1,4 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1beta1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
15
manifests/components/01b_appproject-crd.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1beta1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: appprojects.argoproj.io
|
||||
spec:
|
||||
group: argoproj.io
|
||||
names:
|
||||
kind: AppProject
|
||||
plural: appprojects
|
||||
shortNames:
|
||||
- appproj
|
||||
- appprojs
|
||||
scope: Namespaced
|
||||
version: v1alpha1
|
||||
14
manifests/components/02a_argocd-cm.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-cm
|
||||
data:
|
||||
# See https://github.com/argoproj/argo-cd/blob/master/docs/sso.md#2-configure-argocd-for-sso
|
||||
# for more details about how to setup data config needed for sso
|
||||
|
||||
# URL is the external URL of ArgoCD
|
||||
#url:
|
||||
|
||||
# A dex connector configuration
|
||||
#dex.config:
|
||||
24
manifests/components/02b_argocd-secret.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
# NOTE: the values in this secret will be populated by the initial startup of the API
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: argocd-secret
|
||||
type: Opaque
|
||||
# bcrypt hash of the admin password
|
||||
#admin.password:
|
||||
|
||||
# random server signature key for session validation
|
||||
#server.secretkey:
|
||||
|
||||
# TLS certificate and private key for API server
|
||||
#server.crt:
|
||||
#server.key:
|
||||
|
||||
# The following keys hold the shared secret for authenticating GitHub/GitLab/BitBucket webhook
|
||||
# events. To enable webhooks, configure one or more of the following keys with the shared git
|
||||
# provider webhook secret. The payload URL configured in the git provider should use the
|
||||
# /api/webhook endpoint of your ArgoCD instance (e.g. https://argocd.example.com/api/webhook)
|
||||
#github.webhook.secret:
|
||||
#gitlab.webhook.secret:
|
||||
#bitbucket.webhook.uuid:
|
||||
26
manifests/components/02c_argocd-rbac-cm.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-rbac-cm
|
||||
data:
|
||||
# policy.csv holds the CSV file policy file which contains additional policy and role definitions.
|
||||
# ArgoCD defines two built-in roles:
|
||||
# * role:readonly - readonly access to all objects
|
||||
# * role:admin - admin access to all objects
|
||||
# The built-in policy can be seen under util/rbac/builtin-policy.csv
|
||||
#policy.csv: ""
|
||||
|
||||
# There are two policy formats:
|
||||
# 1. Applications (which belong to a project):
|
||||
# p, <user/group>, <resource>, <action>, <project>/<object>
|
||||
# 2. All other resources:
|
||||
# p, <user/group>, <resource>, <action>, <object>
|
||||
|
||||
# For example, the following rule gives all members of 'my-org:team1' the ability to sync
|
||||
# applications in the project named: my-project
|
||||
# p, my-org:team1, applications, sync, my-project/*
|
||||
|
||||
# policy.default holds the default policy which will ArgoCD will fall back to, when authorizing
|
||||
# a user for API requests
|
||||
policy.default: role:readonly
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: application-controller
|
||||
namespace: argocd
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: application-controller-role
|
||||
namespace: argocd
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
@@ -10,10 +10,15 @@ rules:
|
||||
- secrets
|
||||
verbs:
|
||||
- get
|
||||
- watch
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- argoproj.io
|
||||
resources:
|
||||
- applications
|
||||
- appprojects
|
||||
verbs:
|
||||
- create
|
||||
- get
|
||||
@@ -22,3 +27,11 @@ rules:
|
||||
- update
|
||||
- patch
|
||||
- delete
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- events
|
||||
verbs:
|
||||
- create
|
||||
- list
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: application-controller-role-binding
|
||||
namespace: argocd
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
@@ -10,4 +10,3 @@ roleRef:
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: application-controller
|
||||
namespace: argocd
|
||||
@@ -1,8 +1,8 @@
|
||||
apiVersion: apps/v1beta2
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: application-controller
|
||||
namespace: argocd
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
@@ -14,6 +14,6 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- command: [/argocd-application-controller, --repo-server, 'argocd-repo-server:8081']
|
||||
image: argoproj/argocd-application-controller:latest
|
||||
image: argoproj/argocd-application-controller:v0.7.2
|
||||
name: application-controller
|
||||
serviceAccountName: application-controller
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: argocd-server
|
||||
namespace: argocd
|
||||
@@ -1,34 +1,13 @@
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: argocd-server-role
|
||||
namespace: argocd
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- pods
|
||||
- pods/exec
|
||||
- pods/log
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- secrets
|
||||
verbs:
|
||||
- create
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- update
|
||||
- patch
|
||||
- delete
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- configmaps
|
||||
verbs:
|
||||
- create
|
||||
@@ -42,6 +21,7 @@ rules:
|
||||
- argoproj.io
|
||||
resources:
|
||||
- applications
|
||||
- appprojects
|
||||
verbs:
|
||||
- create
|
||||
- get
|
||||
@@ -50,3 +30,10 @@ rules:
|
||||
- update
|
||||
- delete
|
||||
- patch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- events
|
||||
verbs:
|
||||
- create
|
||||
- list
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: argocd-server-role-binding
|
||||
namespace: argocd
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
@@ -10,4 +10,3 @@ roleRef:
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: argocd-server
|
||||
namespace: argocd
|
||||
44
manifests/components/04d_argocd-server-deployment.yaml
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: argocd-server
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: argocd-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: argocd-server
|
||||
spec:
|
||||
serviceAccountName: argocd-server
|
||||
initContainers:
|
||||
- name: copyutil
|
||||
image: argoproj/argocd-server:v0.7.2
|
||||
command: [cp, /argocd-util, /shared]
|
||||
volumeMounts:
|
||||
- mountPath: /shared
|
||||
name: static-files
|
||||
- name: ui
|
||||
image: argoproj/argocd-ui:v0.7.2
|
||||
command: [cp, -r, /app, /shared]
|
||||
volumeMounts:
|
||||
- mountPath: /shared
|
||||
name: static-files
|
||||
containers:
|
||||
- name: argocd-server
|
||||
image: argoproj/argocd-server:v0.7.2
|
||||
command: [/argocd-server, --staticassets, /shared/app, --repo-server, 'argocd-repo-server:8081']
|
||||
volumeMounts:
|
||||
- mountPath: /shared
|
||||
name: static-files
|
||||
- name: dex
|
||||
image: quay.io/coreos/dex:v2.10.0
|
||||
command: [/shared/argocd-util, rundex]
|
||||
volumeMounts:
|
||||
- mountPath: /shared
|
||||
name: static-files
|
||||
volumes:
|
||||
- emptyDir: {}
|
||||
name: static-files
|
||||
@@ -1,11 +1,17 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: argocd-server
|
||||
namespace: argocd
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
- name: http
|
||||
protocol: TCP
|
||||
port: 80
|
||||
targetPort: 8080
|
||||
- name: https
|
||||
protocol: TCP
|
||||
port: 443
|
||||
targetPort: 8080
|
||||
selector:
|
||||
app: argocd-server
|
||||
@@ -1,8 +1,8 @@
|
||||
apiVersion: apps/v1beta2
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: argocd-repo-server
|
||||
namespace: argocd
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
@@ -13,8 +13,8 @@ spec:
|
||||
app: argocd-repo-server
|
||||
spec:
|
||||
containers:
|
||||
- command: [/argocd-repo-server]
|
||||
image: argoproj/argocd-repo-server:latest
|
||||
name: argocd-repo-server
|
||||
- name: argocd-repo-server
|
||||
image: argoproj/argocd-repo-server:v0.7.2
|
||||
command: [/argocd-repo-server]
|
||||
ports:
|
||||
- containerPort: 8081
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: argocd-repo-server
|
||||
namespace: argocd
|
||||
spec:
|
||||
ports:
|
||||
- port: 8081
|
||||
targetPort: 8081
|
||||
selector:
|
||||
app: argocd-repo-server
|
||||
315
manifests/install.yaml
Normal file
@@ -0,0 +1,315 @@
|
||||
# This is an auto-generated file. DO NOT EDIT
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1beta1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: applications.argoproj.io
|
||||
spec:
|
||||
group: argoproj.io
|
||||
names:
|
||||
kind: Application
|
||||
plural: applications
|
||||
shortNames:
|
||||
- app
|
||||
scope: Namespaced
|
||||
version: v1alpha1
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1beta1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: appprojects.argoproj.io
|
||||
spec:
|
||||
group: argoproj.io
|
||||
names:
|
||||
kind: AppProject
|
||||
plural: appprojects
|
||||
shortNames:
|
||||
- appproj
|
||||
- appprojs
|
||||
scope: Namespaced
|
||||
version: v1alpha1
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-cm
|
||||
data:
|
||||
# See https://github.com/argoproj/argo-cd/blob/master/docs/sso.md#2-configure-argocd-for-sso
|
||||
# for more details about how to setup data config needed for sso
|
||||
|
||||
# URL is the external URL of ArgoCD
|
||||
#url:
|
||||
|
||||
# A dex connector configuration
|
||||
#dex.config:
|
||||
---
|
||||
# NOTE: the values in this secret will be populated by the initial startup of the API
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: argocd-secret
|
||||
type: Opaque
|
||||
# bcrypt hash of the admin password
|
||||
#admin.password:
|
||||
|
||||
# random server signature key for session validation
|
||||
#server.secretkey:
|
||||
|
||||
# TLS certificate and private key for API server
|
||||
#server.crt:
|
||||
#server.key:
|
||||
|
||||
# The following keys hold the shared secret for authenticating GitHub/GitLab/BitBucket webhook
|
||||
# events. To enable webhooks, configure one or more of the following keys with the shared git
|
||||
# provider webhook secret. The payload URL configured in the git provider should use the
|
||||
# /api/webhook endpoint of your ArgoCD instance (e.g. https://argocd.example.com/api/webhook)
|
||||
#github.webhook.secret:
|
||||
#gitlab.webhook.secret:
|
||||
#bitbucket.webhook.uuid:
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-rbac-cm
|
||||
data:
|
||||
# policy.csv holds the CSV file policy file which contains additional policy and role definitions.
|
||||
# ArgoCD defines two built-in roles:
|
||||
# * role:readonly - readonly access to all objects
|
||||
# * role:admin - admin access to all objects
|
||||
# The built-in policy can be seen under util/rbac/builtin-policy.csv
|
||||
#policy.csv: ""
|
||||
|
||||
# There are two policy formats:
|
||||
# 1. Applications (which belong to a project):
|
||||
# p, <user/group>, <resource>, <action>, <project>/<object>
|
||||
# 2. All other resources:
|
||||
# p, <user/group>, <resource>, <action>, <object>
|
||||
|
||||
# For example, the following rule gives all members of 'my-org:team1' the ability to sync
|
||||
# applications in the project named: my-project
|
||||
# p, my-org:team1, applications, sync, my-project/*
|
||||
|
||||
# policy.default holds the default policy which will ArgoCD will fall back to, when authorizing
|
||||
# a user for API requests
|
||||
policy.default: role:readonly
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: application-controller
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: application-controller-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- secrets
|
||||
verbs:
|
||||
- get
|
||||
- watch
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- argoproj.io
|
||||
resources:
|
||||
- applications
|
||||
- appprojects
|
||||
verbs:
|
||||
- create
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- update
|
||||
- patch
|
||||
- delete
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- events
|
||||
verbs:
|
||||
- create
|
||||
- list
|
||||
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: application-controller-role-binding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: application-controller-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: application-controller
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: application-controller
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: application-controller
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: application-controller
|
||||
spec:
|
||||
containers:
|
||||
- command: [/argocd-application-controller, --repo-server, 'argocd-repo-server:8081']
|
||||
image: argoproj/argocd-application-controller:v0.7.2
|
||||
name: application-controller
|
||||
serviceAccountName: application-controller
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: argocd-server
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: argocd-server-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- secrets
|
||||
- configmaps
|
||||
verbs:
|
||||
- create
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- update
|
||||
- patch
|
||||
- delete
|
||||
- apiGroups:
|
||||
- argoproj.io
|
||||
resources:
|
||||
- applications
|
||||
- appprojects
|
||||
verbs:
|
||||
- create
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- update
|
||||
- delete
|
||||
- patch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- events
|
||||
verbs:
|
||||
- create
|
||||
- list
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: argocd-server-role-binding
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: argocd-server-role
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: argocd-server
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: argocd-server
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: argocd-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: argocd-server
|
||||
spec:
|
||||
serviceAccountName: argocd-server
|
||||
initContainers:
|
||||
- name: copyutil
|
||||
image: argoproj/argocd-server:v0.7.2
|
||||
command: [cp, /argocd-util, /shared]
|
||||
volumeMounts:
|
||||
- mountPath: /shared
|
||||
name: static-files
|
||||
- name: ui
|
||||
image: argoproj/argocd-ui:v0.7.2
|
||||
command: [cp, -r, /app, /shared]
|
||||
volumeMounts:
|
||||
- mountPath: /shared
|
||||
name: static-files
|
||||
containers:
|
||||
- name: argocd-server
|
||||
image: argoproj/argocd-server:v0.7.2
|
||||
command: [/argocd-server, --staticassets, /shared/app, --repo-server, 'argocd-repo-server:8081']
|
||||
volumeMounts:
|
||||
- mountPath: /shared
|
||||
name: static-files
|
||||
- name: dex
|
||||
image: quay.io/coreos/dex:v2.10.0
|
||||
command: [/shared/argocd-util, rundex]
|
||||
volumeMounts:
|
||||
- mountPath: /shared
|
||||
name: static-files
|
||||
volumes:
|
||||
- emptyDir: {}
|
||||
name: static-files
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: argocd-server
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
protocol: TCP
|
||||
port: 80
|
||||
targetPort: 8080
|
||||
- name: https
|
||||
protocol: TCP
|
||||
port: 443
|
||||
targetPort: 8080
|
||||
selector:
|
||||
app: argocd-server
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: argocd-repo-server
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: argocd-repo-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: argocd-repo-server
|
||||
spec:
|
||||
containers:
|
||||
- name: argocd-repo-server
|
||||
image: argoproj/argocd-repo-server:v0.7.2
|
||||
command: [/argocd-repo-server]
|
||||
ports:
|
||||
- containerPort: 8081
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: argocd-repo-server
|
||||
spec:
|
||||
ports:
|
||||
- port: 8081
|
||||
targetPort: 8081
|
||||
selector:
|
||||
app: argocd-repo-server
|
||||
@@ -1,22 +1,50 @@
|
||||
package apiclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/oauth2"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/server/account"
|
||||
"github.com/argoproj/argo-cd/server/application"
|
||||
"github.com/argoproj/argo-cd/server/cluster"
|
||||
"github.com/argoproj/argo-cd/server/project"
|
||||
"github.com/argoproj/argo-cd/server/repository"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"google.golang.org/grpc"
|
||||
"github.com/argoproj/argo-cd/server/session"
|
||||
"github.com/argoproj/argo-cd/server/settings"
|
||||
"github.com/argoproj/argo-cd/server/version"
|
||||
grpc_util "github.com/argoproj/argo-cd/util/grpc"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
)
|
||||
|
||||
const (
|
||||
MetaDataTokenKey = "token"
|
||||
// EnvArgoCDServer is the environment variable to look for an ArgoCD server address
|
||||
EnvArgoCDServer = "ARGOCD_SERVER"
|
||||
// EnvArgoCDAuthToken is the environment variable to look for an ArgoCD auth token
|
||||
EnvArgoCDAuthToken = "ARGOCD_AUTH_TOKEN"
|
||||
)
|
||||
|
||||
type ServerClient interface {
|
||||
// Client defines an interface for interaction with an Argo CD server.
|
||||
type Client interface {
|
||||
ClientOptions() ClientOptions
|
||||
NewConn() (*grpc.ClientConn, error)
|
||||
NewRepoClient() (*grpc.ClientConn, repository.RepositoryServiceClient, error)
|
||||
NewRepoClientOrDie() (*grpc.ClientConn, repository.RepositoryServiceClient)
|
||||
@@ -24,31 +52,196 @@ type ServerClient interface {
|
||||
NewClusterClientOrDie() (*grpc.ClientConn, cluster.ClusterServiceClient)
|
||||
NewApplicationClient() (*grpc.ClientConn, application.ApplicationServiceClient, error)
|
||||
NewApplicationClientOrDie() (*grpc.ClientConn, application.ApplicationServiceClient)
|
||||
NewSessionClient() (*grpc.ClientConn, session.SessionServiceClient, error)
|
||||
NewSessionClientOrDie() (*grpc.ClientConn, session.SessionServiceClient)
|
||||
NewSettingsClient() (*grpc.ClientConn, settings.SettingsServiceClient, error)
|
||||
NewSettingsClientOrDie() (*grpc.ClientConn, settings.SettingsServiceClient)
|
||||
NewVersionClient() (*grpc.ClientConn, version.VersionServiceClient, error)
|
||||
NewVersionClientOrDie() (*grpc.ClientConn, version.VersionServiceClient)
|
||||
NewProjectClient() (*grpc.ClientConn, project.ProjectServiceClient, error)
|
||||
NewProjectClientOrDie() (*grpc.ClientConn, project.ProjectServiceClient)
|
||||
NewAccountClient() (*grpc.ClientConn, account.AccountServiceClient, error)
|
||||
NewAccountClientOrDie() (*grpc.ClientConn, account.AccountServiceClient)
|
||||
}
|
||||
|
||||
// ClientOptions hold address, security, and other settings for the API client.
|
||||
type ClientOptions struct {
|
||||
ServerAddr string
|
||||
PlainText bool
|
||||
Insecure bool
|
||||
CertFile string
|
||||
AuthToken string
|
||||
ConfigPath string
|
||||
Context string
|
||||
}
|
||||
|
||||
type client struct {
|
||||
ClientOptions
|
||||
ServerAddr string
|
||||
PlainText bool
|
||||
Insecure bool
|
||||
CertPEMData []byte
|
||||
AuthToken string
|
||||
RefreshToken string
|
||||
}
|
||||
|
||||
func NewClient(opts *ClientOptions) (ServerClient, error) {
|
||||
clientOpts := *opts
|
||||
if clientOpts.ServerAddr == "" {
|
||||
clientOpts.ServerAddr = os.Getenv(EnvArgoCDServer)
|
||||
// NewClient creates a new API client from a set of config options.
|
||||
func NewClient(opts *ClientOptions) (Client, error) {
|
||||
var c client
|
||||
localCfg, err := localconfig.ReadLocalConfig(opts.ConfigPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if clientOpts.ServerAddr == "" {
|
||||
return nil, errors.New("Argo CD server address not supplied")
|
||||
var ctxName string
|
||||
if localCfg != nil {
|
||||
configCtx, err := localCfg.ResolveContext(opts.Context)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if configCtx != nil {
|
||||
c.ServerAddr = configCtx.Server.Server
|
||||
if configCtx.Server.CACertificateAuthorityData != "" {
|
||||
c.CertPEMData, err = base64.StdEncoding.DecodeString(configCtx.Server.CACertificateAuthorityData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
c.PlainText = configCtx.Server.PlainText
|
||||
c.Insecure = configCtx.Server.Insecure
|
||||
c.AuthToken = configCtx.User.AuthToken
|
||||
c.RefreshToken = configCtx.User.RefreshToken
|
||||
ctxName = configCtx.Name
|
||||
}
|
||||
}
|
||||
return &client{
|
||||
ClientOptions: clientOpts,
|
||||
}, nil
|
||||
// Override server address if specified in env or CLI flag
|
||||
if serverFromEnv := os.Getenv(EnvArgoCDServer); serverFromEnv != "" {
|
||||
c.ServerAddr = serverFromEnv
|
||||
}
|
||||
if opts.ServerAddr != "" {
|
||||
c.ServerAddr = opts.ServerAddr
|
||||
}
|
||||
// Make sure we got the server address and auth token from somewhere
|
||||
if c.ServerAddr == "" {
|
||||
return nil, errors.New("ArgoCD server address unspecified")
|
||||
}
|
||||
if parts := strings.Split(c.ServerAddr, ":"); len(parts) == 1 {
|
||||
// If port is unspecified, assume the most likely port
|
||||
c.ServerAddr += ":443"
|
||||
}
|
||||
// Override auth-token if specified in env variable or CLI flag
|
||||
if authFromEnv := os.Getenv(EnvArgoCDAuthToken); authFromEnv != "" {
|
||||
c.AuthToken = authFromEnv
|
||||
}
|
||||
if opts.AuthToken != "" {
|
||||
c.AuthToken = opts.AuthToken
|
||||
}
|
||||
// Override certificate data if specified from CLI flag
|
||||
if opts.CertFile != "" {
|
||||
b, err := ioutil.ReadFile(opts.CertFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.CertPEMData = b
|
||||
}
|
||||
// Override insecure/plaintext options if specified from CLI
|
||||
if opts.PlainText {
|
||||
c.PlainText = true
|
||||
}
|
||||
if opts.Insecure {
|
||||
c.Insecure = true
|
||||
}
|
||||
if localCfg != nil {
|
||||
err = c.refreshAuthToken(localCfg, ctxName, opts.ConfigPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func NewClientOrDie(opts *ClientOptions) ServerClient {
|
||||
// refreshAuthToken refreshes a JWT auth token if it is invalid (e.g. expired)
|
||||
func (c *client) refreshAuthToken(localCfg *localconfig.LocalConfig, ctxName, configPath string) error {
|
||||
configCtx, err := localCfg.ResolveContext(ctxName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.RefreshToken == "" {
|
||||
// If we have no refresh token, there's no point in doing anything
|
||||
return nil
|
||||
}
|
||||
parser := &jwt.Parser{
|
||||
SkipClaimsValidation: true,
|
||||
}
|
||||
var claims jwt.StandardClaims
|
||||
_, _, err = parser.ParseUnverified(configCtx.User.AuthToken, &claims)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if claims.Valid() == nil {
|
||||
// token is still valid
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debug("Auth token no longer valid. Refreshing")
|
||||
tlsConfig, err := c.tlsConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
},
|
||||
}
|
||||
ctx := oidc.ClientContext(context.Background(), httpClient)
|
||||
var scheme string
|
||||
if c.PlainText {
|
||||
scheme = "http"
|
||||
} else {
|
||||
scheme = "https"
|
||||
}
|
||||
conf := &oauth2.Config{
|
||||
ClientID: common.ArgoCDCLIClientAppID,
|
||||
Scopes: []string{"openid", "profile", "email", "groups", "offline_access"},
|
||||
Endpoint: oauth2.Endpoint{
|
||||
AuthURL: fmt.Sprintf("%s://%s%s/auth", scheme, c.ServerAddr, common.DexAPIEndpoint),
|
||||
TokenURL: fmt.Sprintf("%s://%s%s/token", scheme, c.ServerAddr, common.DexAPIEndpoint),
|
||||
},
|
||||
RedirectURL: fmt.Sprintf("%s://%s/auth/callback", scheme, c.ServerAddr),
|
||||
}
|
||||
t := &oauth2.Token{
|
||||
RefreshToken: c.RefreshToken,
|
||||
}
|
||||
token, err := conf.TokenSource(ctx, t).Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rawIDToken, ok := token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return errors.New("no id_token in token response")
|
||||
}
|
||||
refreshToken, _ := token.Extra("refresh_token").(string)
|
||||
c.AuthToken = rawIDToken
|
||||
c.RefreshToken = refreshToken
|
||||
localCfg.UpsertUser(localconfig.User{
|
||||
Name: ctxName,
|
||||
AuthToken: c.AuthToken,
|
||||
RefreshToken: c.RefreshToken,
|
||||
})
|
||||
err = localconfig.WriteLocalConfig(*localCfg, configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewClientOrDie creates a new API client from a set of config options, or fails fatally if the new client creation fails.
|
||||
func NewClientOrDie(opts *ClientOptions) Client {
|
||||
client, err := NewClient(opts)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -56,16 +249,59 @@ func NewClientOrDie(opts *ClientOptions) ServerClient {
|
||||
return client
|
||||
}
|
||||
|
||||
// JwtCredentials implements the gRPC credentials.Credentials interface which we is used to do
|
||||
// grpc.WithPerRPCCredentials(), for authentication
|
||||
type jwtCredentials struct {
|
||||
Token string
|
||||
}
|
||||
|
||||
func (c jwtCredentials) RequireTransportSecurity() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c jwtCredentials) GetRequestMetadata(context.Context, ...string) (map[string]string, error) {
|
||||
return map[string]string{
|
||||
MetaDataTokenKey: c.Token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *client) NewConn() (*grpc.ClientConn, error) {
|
||||
var dialOpts []grpc.DialOption
|
||||
var creds credentials.TransportCredentials
|
||||
if !c.PlainText {
|
||||
tlsConfig, err := c.tlsConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
creds = credentials.NewTLS(tlsConfig)
|
||||
}
|
||||
endpointCredentials := jwtCredentials{
|
||||
Token: c.AuthToken,
|
||||
}
|
||||
return grpc_util.BlockingDial(context.Background(), "tcp", c.ServerAddr, creds, grpc.WithPerRPCCredentials(endpointCredentials))
|
||||
}
|
||||
|
||||
func (c *client) tlsConfig() (*tls.Config, error) {
|
||||
var tlsConfig tls.Config
|
||||
if len(c.CertPEMData) > 0 {
|
||||
cp := x509.NewCertPool()
|
||||
if !cp.AppendCertsFromPEM(c.CertPEMData) {
|
||||
return nil, fmt.Errorf("credentials: failed to append certificates")
|
||||
}
|
||||
tlsConfig.RootCAs = cp
|
||||
}
|
||||
if c.Insecure {
|
||||
dialOpts = append(dialOpts, grpc.WithInsecure())
|
||||
} else {
|
||||
return nil, errors.New("secure authentication unsupported")
|
||||
} // else if opts.Credentials != nil {
|
||||
// dialOpts = append(dialOpts, grpc.WithTransportCredentials(opts.Credentials))
|
||||
//}
|
||||
return grpc.Dial(c.ServerAddr, dialOpts...)
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
}
|
||||
return &tlsConfig, nil
|
||||
}
|
||||
|
||||
func (c *client) ClientOptions() ClientOptions {
|
||||
return ClientOptions{
|
||||
ServerAddr: c.ServerAddr,
|
||||
PlainText: c.PlainText,
|
||||
Insecure: c.Insecure,
|
||||
AuthToken: c.AuthToken,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) NewRepoClient() (*grpc.ClientConn, repository.RepositoryServiceClient, error) {
|
||||
@@ -118,3 +354,88 @@ func (c *client) NewApplicationClientOrDie() (*grpc.ClientConn, application.Appl
|
||||
}
|
||||
return conn, repoIf
|
||||
}
|
||||
|
||||
func (c *client) NewSessionClient() (*grpc.ClientConn, session.SessionServiceClient, error) {
|
||||
conn, err := c.NewConn()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
sessionIf := session.NewSessionServiceClient(conn)
|
||||
return conn, sessionIf, nil
|
||||
}
|
||||
|
||||
func (c *client) NewSessionClientOrDie() (*grpc.ClientConn, session.SessionServiceClient) {
|
||||
conn, sessionIf, err := c.NewSessionClient()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to establish connection to %s: %v", c.ServerAddr, err)
|
||||
}
|
||||
return conn, sessionIf
|
||||
}
|
||||
|
||||
func (c *client) NewSettingsClient() (*grpc.ClientConn, settings.SettingsServiceClient, error) {
|
||||
conn, err := c.NewConn()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
setIf := settings.NewSettingsServiceClient(conn)
|
||||
return conn, setIf, nil
|
||||
}
|
||||
|
||||
func (c *client) NewSettingsClientOrDie() (*grpc.ClientConn, settings.SettingsServiceClient) {
|
||||
conn, setIf, err := c.NewSettingsClient()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to establish connection to %s: %v", c.ServerAddr, err)
|
||||
}
|
||||
return conn, setIf
|
||||
}
|
||||
|
||||
func (c *client) NewVersionClient() (*grpc.ClientConn, version.VersionServiceClient, error) {
|
||||
conn, err := c.NewConn()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
versionIf := version.NewVersionServiceClient(conn)
|
||||
return conn, versionIf, nil
|
||||
}
|
||||
|
||||
func (c *client) NewVersionClientOrDie() (*grpc.ClientConn, version.VersionServiceClient) {
|
||||
conn, versionIf, err := c.NewVersionClient()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to establish connection to %s: %v", c.ServerAddr, err)
|
||||
}
|
||||
return conn, versionIf
|
||||
}
|
||||
|
||||
func (c *client) NewProjectClient() (*grpc.ClientConn, project.ProjectServiceClient, error) {
|
||||
conn, err := c.NewConn()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
projIf := project.NewProjectServiceClient(conn)
|
||||
return conn, projIf, nil
|
||||
}
|
||||
|
||||
func (c *client) NewProjectClientOrDie() (*grpc.ClientConn, project.ProjectServiceClient) {
|
||||
conn, projIf, err := c.NewProjectClient()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to establish connection to %s: %v", c.ServerAddr, err)
|
||||
}
|
||||
return conn, projIf
|
||||
}
|
||||
|
||||
func (c *client) NewAccountClient() (*grpc.ClientConn, account.AccountServiceClient, error) {
|
||||
conn, err := c.NewConn()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
usrIf := account.NewAccountServiceClient(conn)
|
||||
return conn, usrIf, nil
|
||||
}
|
||||
|
||||
func (c *client) NewAccountClientOrDie() (*grpc.ClientConn, account.AccountServiceClient) {
|
||||
conn, usrIf, err := c.NewAccountClient()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to establish connection to %s: %v", c.ServerAddr, err)
|
||||
}
|
||||
return conn, usrIf
|
||||
}
|
||||
|
||||
@@ -11,10 +11,10 @@ const (
|
||||
ApplicationShortName string = "app"
|
||||
ApplicationFullName string = ApplicationPlural + "." + Group
|
||||
|
||||
// Cluster constants
|
||||
ClusterKind string = "Cluster"
|
||||
ClusterSingular string = "cluster"
|
||||
ClusterPlural string = "clusters"
|
||||
ClusterShortName string = "cluster"
|
||||
ClusterFullName string = ClusterPlural + "." + Group
|
||||
// AppProject constants
|
||||
AppProjectKind string = "AppProject"
|
||||
AppProjectSingular string = "appproject"
|
||||
AppProjectPlural string = "appprojects"
|
||||
AppProjectShortName string = "appproject"
|
||||
AppProjectFullName string = AppProjectPlural + "." + Group
|
||||
)
|
||||
|
||||
@@ -13,6 +13,36 @@ import "k8s.io/apimachinery/pkg/util/intstr/generated.proto";
|
||||
// Package-wide variables from generator "generated".
|
||||
option go_package = "v1alpha1";
|
||||
|
||||
// AppProject is a definition of AppProject resource.
|
||||
// +genclient
|
||||
// +genclient:noStatus
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
message AppProject {
|
||||
optional k8s.io.apimachinery.pkg.apis.meta.v1.ObjectMeta metadata = 1;
|
||||
|
||||
optional AppProjectSpec spec = 2;
|
||||
}
|
||||
|
||||
// AppProjectList is list of AppProject resources
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
message AppProjectList {
|
||||
optional k8s.io.apimachinery.pkg.apis.meta.v1.ListMeta metadata = 1;
|
||||
|
||||
repeated AppProject items = 2;
|
||||
}
|
||||
|
||||
// AppProjectSpec represents
|
||||
message AppProjectSpec {
|
||||
// SourceRepos contains list of git repository URLs which can be used for deployment
|
||||
repeated string sourceRepos = 1;
|
||||
|
||||
// Destinations contains list of destinations available for deployment
|
||||
repeated ApplicationDestination destinations = 2;
|
||||
|
||||
// Description contains optional project description
|
||||
optional string description = 3;
|
||||
}
|
||||
|
||||
// Application is a definition of Application resource.
|
||||
// +genclient
|
||||
// +genclient:noStatus
|
||||
@@ -23,6 +53,17 @@ message Application {
|
||||
optional ApplicationSpec spec = 2;
|
||||
|
||||
optional ApplicationStatus status = 3;
|
||||
|
||||
optional Operation operation = 4;
|
||||
}
|
||||
|
||||
// ApplicationCondition contains details about current application condition
|
||||
message ApplicationCondition {
|
||||
// Type is an application condition type
|
||||
optional string type = 1;
|
||||
|
||||
// Message contains human-readable message indicating details about condition
|
||||
optional string message = 2;
|
||||
}
|
||||
|
||||
// ApplicationDestination contains deployment destination information
|
||||
@@ -44,21 +85,24 @@ message ApplicationList {
|
||||
|
||||
// ApplicationSource contains information about github repository, path within repository and target application environment.
|
||||
message ApplicationSource {
|
||||
// RepoURL is the repository URL containing the ksonnet application.
|
||||
// RepoURL is the git repository URL of the application manifests
|
||||
optional string repoURL = 1;
|
||||
|
||||
// Path is a directory path within repository which contains ksonnet application.
|
||||
// Path is a directory path within the repository containing a
|
||||
optional string path = 2;
|
||||
|
||||
// Environment is a ksonnet application environment name.
|
||||
// Environment is a ksonnet application environment name
|
||||
optional string environment = 3;
|
||||
|
||||
// TargetRevision defines the commit, tag, or branch in which to sync the application to.
|
||||
// If omitted, will sync to HEAD
|
||||
optional string targetRevision = 4;
|
||||
|
||||
// Environment parameter override values
|
||||
// ComponentParameterOverrides are a list of parameter override values
|
||||
repeated ComponentParameter componentParameterOverrides = 5;
|
||||
|
||||
// ValuesFiles is a list of Helm values files to use when generating a template
|
||||
repeated string valuesFiles = 6;
|
||||
}
|
||||
|
||||
// ApplicationSpec represents desired application state. Contains link to repository with application definition and additional parameters link definition revision.
|
||||
@@ -67,15 +111,25 @@ message ApplicationSpec {
|
||||
optional ApplicationSource source = 1;
|
||||
|
||||
// Destination overrides the kubernetes server and namespace defined in the environment ksonnet app.yaml
|
||||
// This field is optional. If omitted, uses the server and namespace defined in the environment
|
||||
optional ApplicationDestination destination = 2;
|
||||
|
||||
// Project is a application project name. Empty name means that application belongs to 'default' project.
|
||||
optional string project = 3;
|
||||
}
|
||||
|
||||
// ApplicationStatus contains information about application status in target environment.
|
||||
message ApplicationStatus {
|
||||
optional ComparisonResult comparisonResult = 1;
|
||||
|
||||
repeated DeploymentInfo recentDeployment = 2;
|
||||
repeated DeploymentInfo history = 2;
|
||||
|
||||
repeated ComponentParameter parameters = 3;
|
||||
|
||||
optional HealthStatus health = 4;
|
||||
|
||||
optional OperationState operationState = 5;
|
||||
|
||||
repeated ApplicationCondition conditions = 6;
|
||||
}
|
||||
|
||||
// ApplicationWatchEvent contains information about application change.
|
||||
@@ -100,6 +154,9 @@ message Cluster {
|
||||
|
||||
// Config holds cluster information for connecting to a cluster
|
||||
optional ClusterConfig config = 3;
|
||||
|
||||
// ConnectionState contains information about cluster connection state
|
||||
optional ConnectionState connectionState = 4;
|
||||
}
|
||||
|
||||
// ClusterConfig is the configuration attributes. This structure is subset of the go-client
|
||||
@@ -132,15 +189,9 @@ message ComparisonResult {
|
||||
|
||||
optional ApplicationSource comparedTo = 2;
|
||||
|
||||
optional string server = 3;
|
||||
|
||||
optional string namespace = 4;
|
||||
|
||||
optional string status = 5;
|
||||
|
||||
repeated ResourceState resources = 6;
|
||||
|
||||
optional string error = 7;
|
||||
}
|
||||
|
||||
// ComponentParameter contains information about component parameter value
|
||||
@@ -152,6 +203,15 @@ message ComponentParameter {
|
||||
optional string value = 3;
|
||||
}
|
||||
|
||||
// ConnectionState contains information about remote resource connection state
|
||||
message ConnectionState {
|
||||
optional string status = 1;
|
||||
|
||||
optional string message = 2;
|
||||
|
||||
optional k8s.io.apimachinery.pkg.apis.meta.v1.Time attemptedAt = 3;
|
||||
}
|
||||
|
||||
// DeploymentInfo contains information relevant to an application deployment
|
||||
message DeploymentInfo {
|
||||
repeated ComponentParameter params = 1;
|
||||
@@ -161,6 +221,66 @@ message DeploymentInfo {
|
||||
repeated ComponentParameter componentParameterOverrides = 3;
|
||||
|
||||
optional k8s.io.apimachinery.pkg.apis.meta.v1.Time deployedAt = 4;
|
||||
|
||||
optional int64 id = 5;
|
||||
}
|
||||
|
||||
message HealthStatus {
|
||||
optional string status = 1;
|
||||
|
||||
optional string statusDetails = 2;
|
||||
}
|
||||
|
||||
// HookStatus contains status about a hook invocation
|
||||
message HookStatus {
|
||||
// Name is the resource name
|
||||
optional string name = 1;
|
||||
|
||||
// Kind is the resource kind
|
||||
optional string kind = 2;
|
||||
|
||||
// APIVersion is the resource API version
|
||||
optional string apiVersion = 3;
|
||||
|
||||
// Type is the type of hook (e.g. PreSync, Sync, PostSync, Skip)
|
||||
optional string type = 4;
|
||||
|
||||
// Status a simple, high-level summary of where the resource is in its lifecycle
|
||||
optional string status = 5;
|
||||
|
||||
// A human readable message indicating details about why the resource is in this condition.
|
||||
optional string message = 6;
|
||||
}
|
||||
|
||||
// Operation contains requested operation parameters.
|
||||
message Operation {
|
||||
optional SyncOperation sync = 1;
|
||||
|
||||
optional RollbackOperation rollback = 2;
|
||||
}
|
||||
|
||||
// OperationState contains information about state of currently performing operation on application.
|
||||
message OperationState {
|
||||
// Operation is the original requested operation
|
||||
optional Operation operation = 1;
|
||||
|
||||
// Phase is the current phase of the operation
|
||||
optional string phase = 2;
|
||||
|
||||
// Message hold any pertinent messages when attempting to perform operation (typically errors).
|
||||
optional string message = 3;
|
||||
|
||||
// SyncResult is the result of a Sync operation
|
||||
optional SyncOperationResult syncResult = 4;
|
||||
|
||||
// RollbackResult is the result of a Rollback operation
|
||||
optional SyncOperationResult rollbackResult = 5;
|
||||
|
||||
// StartedAt contains time of operation start
|
||||
optional k8s.io.apimachinery.pkg.apis.meta.v1.Time startedAt = 6;
|
||||
|
||||
// FinishedAt contains time of operation completion
|
||||
optional k8s.io.apimachinery.pkg.apis.meta.v1.Time finishedAt = 7;
|
||||
}
|
||||
|
||||
// Repository is a Git repository holding application configurations
|
||||
@@ -172,6 +292,8 @@ message Repository {
|
||||
optional string password = 3;
|
||||
|
||||
optional string sshPrivateKey = 4;
|
||||
|
||||
optional ConnectionState connectionState = 5;
|
||||
}
|
||||
|
||||
// RepositoryList is a collection of Repositories.
|
||||
@@ -181,6 +303,25 @@ message RepositoryList {
|
||||
repeated Repository items = 2;
|
||||
}
|
||||
|
||||
message ResourceDetails {
|
||||
optional string name = 1;
|
||||
|
||||
optional string kind = 2;
|
||||
|
||||
optional string namespace = 3;
|
||||
|
||||
optional string message = 4;
|
||||
|
||||
optional string status = 5;
|
||||
}
|
||||
|
||||
// ResourceNode contains information about live resource and its children
|
||||
message ResourceNode {
|
||||
optional string state = 1;
|
||||
|
||||
repeated ResourceNode children = 2;
|
||||
}
|
||||
|
||||
// ResourceState holds the target state of a resource and live state of a resource
|
||||
message ResourceState {
|
||||
optional string targetState = 1;
|
||||
@@ -188,6 +329,69 @@ message ResourceState {
|
||||
optional string liveState = 2;
|
||||
|
||||
optional string status = 3;
|
||||
|
||||
repeated ResourceNode childLiveResources = 4;
|
||||
|
||||
optional HealthStatus health = 5;
|
||||
}
|
||||
|
||||
message RollbackOperation {
|
||||
optional int64 id = 1;
|
||||
|
||||
optional bool prune = 2;
|
||||
|
||||
optional bool dryRun = 3;
|
||||
}
|
||||
|
||||
// SyncOperation contains sync operation details.
|
||||
message SyncOperation {
|
||||
// Revision is the git revision in which to sync the application to
|
||||
optional string revision = 1;
|
||||
|
||||
// Prune deletes resources that are no longer tracked in git
|
||||
optional bool prune = 2;
|
||||
|
||||
// DryRun will perform a `kubectl apply --dry-run` without actually performing the sync
|
||||
optional bool dryRun = 3;
|
||||
|
||||
// SyncStrategy describes how to perform the sync
|
||||
optional SyncStrategy syncStrategy = 4;
|
||||
}
|
||||
|
||||
// SyncOperationResult represent result of sync operation
|
||||
message SyncOperationResult {
|
||||
// Resources holds the sync result of each individual resource
|
||||
repeated ResourceDetails resources = 1;
|
||||
|
||||
// Revision holds the git commit SHA of the sync
|
||||
optional string revision = 2;
|
||||
|
||||
// Hooks contains list of hook resource statuses associated with this operation
|
||||
repeated HookStatus hooks = 3;
|
||||
}
|
||||
|
||||
// SyncStrategy indicates the
|
||||
message SyncStrategy {
|
||||
// Apply wil perform a `kubectl apply` to perform the sync. This is the default strategy
|
||||
optional SyncStrategyApply apply = 1;
|
||||
|
||||
// Hook will submit any referenced resources to perform the sync
|
||||
optional SyncStrategyHook hook = 2;
|
||||
}
|
||||
|
||||
// SyncStrategyApply uses `kubectl apply` to perform the apply
|
||||
message SyncStrategyApply {
|
||||
// Force indicates whether or not to supply the --force flag to `kubectl apply`.
|
||||
// The --force flag deletes and re-create the resource, when PATCH encounters conflict and has
|
||||
// retried for 5 times.
|
||||
optional bool force = 1;
|
||||
}
|
||||
|
||||
// SyncStrategyHook will perform a sync using hooks annotations.
|
||||
// If no hook annotation is specified falls back to `kubectl apply`.
|
||||
message SyncStrategyHook {
|
||||
// Embed SyncStrategyApply type to inherit any `apply` options
|
||||
optional SyncStrategyApply syncStrategyApply = 1;
|
||||
}
|
||||
|
||||
// TLSClientConfig contains settings to enable transport layer security
|
||||
|
||||
@@ -11,7 +11,9 @@ type objectMeta struct {
|
||||
}
|
||||
|
||||
func (a *Application) GetMetadata() *objectMeta {
|
||||
return &objectMeta{
|
||||
Name: &a.Name,
|
||||
var om objectMeta
|
||||
if a != nil {
|
||||
om.Name = &a.Name
|
||||
}
|
||||
return &om
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ var (
|
||||
// SchemeGroupVersion is group version used to register these objects
|
||||
SchemeGroupVersion = schema.GroupVersion{Group: application.Group, Version: "v1alpha1"}
|
||||
ApplicationSchemaGroupVersionKind = schema.GroupVersionKind{Group: application.Group, Version: "v1alpha1", Kind: application.ApplicationKind}
|
||||
AppProjectSchemaGroupVersionKind = schema.GroupVersionKind{Group: application.Group, Version: "v1alpha1", Kind: application.AppProjectKind}
|
||||
)
|
||||
|
||||
// Resource takes an unqualified resource and returns a Group-qualified GroupResource.
|
||||
@@ -29,6 +30,8 @@ func addKnownTypes(scheme *runtime.Scheme) error {
|
||||
scheme.AddKnownTypes(SchemeGroupVersion,
|
||||
&Application{},
|
||||
&ApplicationList{},
|
||||
&AppProject{},
|
||||
&AppProjectList{},
|
||||
)
|
||||
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
|
||||
return nil
|
||||
|
||||
@@ -2,21 +2,180 @@ package v1alpha1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"time"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/rest"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
)
|
||||
|
||||
// SyncOperation contains sync operation details.
|
||||
type SyncOperation struct {
|
||||
// Revision is the git revision in which to sync the application to
|
||||
Revision string `json:"revision,omitempty" protobuf:"bytes,1,opt,name=revision"`
|
||||
// Prune deletes resources that are no longer tracked in git
|
||||
Prune bool `json:"prune,omitempty" protobuf:"bytes,2,opt,name=prune"`
|
||||
// DryRun will perform a `kubectl apply --dry-run` without actually performing the sync
|
||||
DryRun bool `json:"dryRun,omitempty" protobuf:"bytes,3,opt,name=dryRun"`
|
||||
// SyncStrategy describes how to perform the sync
|
||||
SyncStrategy *SyncStrategy `json:"syncStrategy,omitempty" protobuf:"bytes,4,opt,name=syncStrategy"`
|
||||
}
|
||||
|
||||
type RollbackOperation struct {
|
||||
ID int64 `json:"id" protobuf:"bytes,1,opt,name=id"`
|
||||
Prune bool `json:"prune,omitempty" protobuf:"bytes,2,opt,name=prune"`
|
||||
DryRun bool `json:"dryRun,omitempty" protobuf:"bytes,3,opt,name=dryRun"`
|
||||
}
|
||||
|
||||
// Operation contains requested operation parameters.
|
||||
type Operation struct {
|
||||
Sync *SyncOperation `json:"sync,omitempty" protobuf:"bytes,1,opt,name=sync"`
|
||||
Rollback *RollbackOperation `json:"rollback,omitempty" protobuf:"bytes,2,opt,name=rollback"`
|
||||
}
|
||||
|
||||
type OperationPhase string
|
||||
|
||||
const (
|
||||
OperationRunning OperationPhase = "Running"
|
||||
OperationTerminating OperationPhase = "Terminating"
|
||||
OperationFailed OperationPhase = "Failed"
|
||||
OperationError OperationPhase = "Error"
|
||||
OperationSucceeded OperationPhase = "Succeeded"
|
||||
)
|
||||
|
||||
func (os OperationPhase) Completed() bool {
|
||||
switch os {
|
||||
case OperationFailed, OperationError, OperationSucceeded:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (os OperationPhase) Successful() bool {
|
||||
return os == OperationSucceeded
|
||||
}
|
||||
|
||||
// OperationState contains information about state of currently performing operation on application.
|
||||
type OperationState struct {
|
||||
// Operation is the original requested operation
|
||||
Operation Operation `json:"operation" protobuf:"bytes,1,opt,name=operation"`
|
||||
// Phase is the current phase of the operation
|
||||
Phase OperationPhase `json:"phase" protobuf:"bytes,2,opt,name=phase"`
|
||||
// Message hold any pertinent messages when attempting to perform operation (typically errors).
|
||||
Message string `json:"message,omitempty" protobuf:"bytes,3,opt,name=message"`
|
||||
// SyncResult is the result of a Sync operation
|
||||
SyncResult *SyncOperationResult `json:"syncResult,omitempty" protobuf:"bytes,4,opt,name=syncResult"`
|
||||
// RollbackResult is the result of a Rollback operation
|
||||
RollbackResult *SyncOperationResult `json:"rollbackResult,omitempty" protobuf:"bytes,5,opt,name=rollbackResult"`
|
||||
// StartedAt contains time of operation start
|
||||
StartedAt metav1.Time `json:"startedAt" protobuf:"bytes,6,opt,name=startedAt"`
|
||||
// FinishedAt contains time of operation completion
|
||||
FinishedAt *metav1.Time `json:"finishedAt" protobuf:"bytes,7,opt,name=finishedAt"`
|
||||
}
|
||||
|
||||
// SyncStrategy indicates the
|
||||
type SyncStrategy struct {
|
||||
// Apply wil perform a `kubectl apply` to perform the sync. This is the default strategy
|
||||
Apply *SyncStrategyApply `json:"apply,omitempty" protobuf:"bytes,1,opt,name=apply"`
|
||||
// Hook will submit any referenced resources to perform the sync
|
||||
Hook *SyncStrategyHook `json:"hook,omitempty" protobuf:"bytes,2,opt,name=hook"`
|
||||
}
|
||||
|
||||
// SyncStrategyApply uses `kubectl apply` to perform the apply
|
||||
type SyncStrategyApply struct {
|
||||
// Force indicates whether or not to supply the --force flag to `kubectl apply`.
|
||||
// The --force flag deletes and re-create the resource, when PATCH encounters conflict and has
|
||||
// retried for 5 times.
|
||||
Force bool `json:"force,omitempty" protobuf:"bytes,1,opt,name=force"`
|
||||
}
|
||||
|
||||
// SyncStrategyHook will perform a sync using hooks annotations.
|
||||
// If no hook annotation is specified falls back to `kubectl apply`.
|
||||
type SyncStrategyHook struct {
|
||||
// Embed SyncStrategyApply type to inherit any `apply` options
|
||||
SyncStrategyApply `protobuf:"bytes,1,opt,name=syncStrategyApply"`
|
||||
}
|
||||
|
||||
type HookType string
|
||||
|
||||
const (
|
||||
HookTypePreSync HookType = "PreSync"
|
||||
HookTypeSync HookType = "Sync"
|
||||
HookTypePostSync HookType = "PostSync"
|
||||
HookTypeSkip HookType = "Skip"
|
||||
|
||||
// NOTE: we may consider adding SyncFail hook. With a SyncFail hook, finalizer-like logic could
|
||||
// be implemented by specifying both PostSync,SyncFail in the hook annotation:
|
||||
// (e.g.: argocd.argoproj.io/hook: PostSync,SyncFail)
|
||||
//HookTypeSyncFail HookType = "SyncFail"
|
||||
)
|
||||
|
||||
type HookDeletePolicy string
|
||||
|
||||
const (
|
||||
HookDeletePolicyHookSucceeded HookDeletePolicy = "HookSucceeded"
|
||||
HookDeletePolicyHookFailed HookDeletePolicy = "HookFailed"
|
||||
)
|
||||
|
||||
// HookStatus contains status about a hook invocation
|
||||
type HookStatus struct {
|
||||
// Name is the resource name
|
||||
Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
|
||||
// Kind is the resource kind
|
||||
Kind string `json:"kind" protobuf:"bytes,2,opt,name=kind"`
|
||||
// APIVersion is the resource API version
|
||||
APIVersion string `json:"apiVersion" protobuf:"bytes,3,opt,name=apiVersion"`
|
||||
// Type is the type of hook (e.g. PreSync, Sync, PostSync, Skip)
|
||||
Type HookType `json:"type" protobuf:"bytes,4,opt,name=type"`
|
||||
// Status a simple, high-level summary of where the resource is in its lifecycle
|
||||
Status OperationPhase `json:"status" protobuf:"bytes,5,opt,name=status"`
|
||||
// A human readable message indicating details about why the resource is in this condition.
|
||||
Message string `json:"message,omitempty" protobuf:"bytes,6,opt,name=message"`
|
||||
}
|
||||
|
||||
// SyncOperationResult represent result of sync operation
|
||||
type SyncOperationResult struct {
|
||||
// Resources holds the sync result of each individual resource
|
||||
Resources []*ResourceDetails `json:"resources,omitempty" protobuf:"bytes,1,opt,name=resources"`
|
||||
// Revision holds the git commit SHA of the sync
|
||||
Revision string `json:"revision" protobuf:"bytes,2,opt,name=revision"`
|
||||
// Hooks contains list of hook resource statuses associated with this operation
|
||||
Hooks []*HookStatus `json:"hooks,omitempty" protobuf:"bytes,3,opt,name=hooks"`
|
||||
}
|
||||
|
||||
type ResourceSyncStatus string
|
||||
|
||||
const (
|
||||
ResourceDetailsSynced ResourceSyncStatus = "Synced"
|
||||
ResourceDetailsSyncFailed ResourceSyncStatus = "SyncFailed"
|
||||
ResourceDetailsSyncedAndPruned ResourceSyncStatus = "SyncedAndPruned"
|
||||
ResourceDetailsPruningRequired ResourceSyncStatus = "PruningRequired"
|
||||
)
|
||||
|
||||
func (s ResourceSyncStatus) Successful() bool {
|
||||
return s != ResourceDetailsSyncFailed
|
||||
}
|
||||
|
||||
type ResourceDetails struct {
|
||||
Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
|
||||
Kind string `json:"kind" protobuf:"bytes,2,opt,name=kind"`
|
||||
Namespace string `json:"namespace" protobuf:"bytes,3,opt,name=namespace"`
|
||||
Message string `json:"message,omitempty" protobuf:"bytes,4,opt,name=message"`
|
||||
Status ResourceSyncStatus `json:"status,omitempty" protobuf:"bytes,5,opt,name=status"`
|
||||
}
|
||||
|
||||
// DeploymentInfo contains information relevant to an application deployment
|
||||
type DeploymentInfo struct {
|
||||
Params []ComponentParameter `json:"params" protobuf:"bytes,1,name=params"`
|
||||
Revision string `json:"revision" protobuf:"bytes,2,opt,name=revision"`
|
||||
ComponentParameterOverrides []ComponentParameter `json:"componentParameterOverrides,omitempty" protobuf:"bytes,3,opt,name=componentParameterOverrides"`
|
||||
DeployedAt metav1.Time `json:"deployedAt" protobuf:"bytes,4,opt,name=deployedAt"`
|
||||
ID int64 `json:"id" protobuf:"bytes,5,opt,name=id"`
|
||||
}
|
||||
|
||||
// Application is a definition of Application resource.
|
||||
@@ -28,6 +187,7 @@ type Application struct {
|
||||
metav1.ObjectMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"`
|
||||
Spec ApplicationSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"`
|
||||
Status ApplicationStatus `json:"status" protobuf:"bytes,3,opt,name=status"`
|
||||
Operation *Operation `json:"operation,omitempty" protobuf:"bytes,4,opt,name=operation"`
|
||||
}
|
||||
|
||||
// ApplicationWatchEvent contains information about application change.
|
||||
@@ -55,30 +215,33 @@ type ApplicationSpec struct {
|
||||
// Source is a reference to the location ksonnet application definition
|
||||
Source ApplicationSource `json:"source" protobuf:"bytes,1,opt,name=source"`
|
||||
// Destination overrides the kubernetes server and namespace defined in the environment ksonnet app.yaml
|
||||
// This field is optional. If omitted, uses the server and namespace defined in the environment
|
||||
Destination *ApplicationDestination `json:"destination,omitempty" protobuf:"bytes,2,opt,name=destination"`
|
||||
Destination ApplicationDestination `json:"destination" protobuf:"bytes,2,name=destination"`
|
||||
// Project is a application project name. Empty name means that application belongs to 'default' project.
|
||||
Project string `json:"project" protobuf:"bytes,3,name=project"`
|
||||
}
|
||||
|
||||
// ComponentParameter contains information about component parameter value
|
||||
type ComponentParameter struct {
|
||||
Component string `json:"component" protobuf:"bytes,1,opt,name=component"`
|
||||
Component string `json:"component,omitempty" protobuf:"bytes,1,opt,name=component"`
|
||||
Name string `json:"name" protobuf:"bytes,2,opt,name=name"`
|
||||
Value string `json:"value" protobuf:"bytes,3,opt,name=value"`
|
||||
}
|
||||
|
||||
// ApplicationSource contains information about github repository, path within repository and target application environment.
|
||||
type ApplicationSource struct {
|
||||
// RepoURL is the repository URL containing the ksonnet application.
|
||||
// RepoURL is the git repository URL of the application manifests
|
||||
RepoURL string `json:"repoURL" protobuf:"bytes,1,opt,name=repoURL"`
|
||||
// Path is a directory path within repository which contains ksonnet application.
|
||||
// Path is a directory path within the repository containing a
|
||||
Path string `json:"path" protobuf:"bytes,2,opt,name=path"`
|
||||
// Environment is a ksonnet application environment name.
|
||||
Environment string `json:"environment" protobuf:"bytes,3,opt,name=environment"`
|
||||
// Environment is a ksonnet application environment name
|
||||
Environment string `json:"environment,omitempty" protobuf:"bytes,3,opt,name=environment"`
|
||||
// TargetRevision defines the commit, tag, or branch in which to sync the application to.
|
||||
// If omitted, will sync to HEAD
|
||||
TargetRevision string `json:"targetRevision,omitempty" protobuf:"bytes,4,opt,name=targetRevision"`
|
||||
// Environment parameter override values
|
||||
// ComponentParameterOverrides are a list of parameter override values
|
||||
ComponentParameterOverrides []ComponentParameter `json:"componentParameterOverrides,omitempty" protobuf:"bytes,5,opt,name=componentParameterOverrides"`
|
||||
// ValuesFiles is a list of Helm values files to use when generating a template
|
||||
ValuesFiles []string `json:"valuesFiles,omitempty" protobuf:"bytes,6,opt,name=valuesFiles"`
|
||||
}
|
||||
|
||||
// ApplicationDestination contains deployment destination information
|
||||
@@ -94,34 +257,100 @@ type ComparisonStatus string
|
||||
|
||||
// Possible comparison results
|
||||
const (
|
||||
ComparisonStatusUnknown ComparisonStatus = ""
|
||||
ComparisonStatusError ComparisonStatus = "Error"
|
||||
ComparisonStatusUnknown ComparisonStatus = "Unknown"
|
||||
ComparisonStatusSynced ComparisonStatus = "Synced"
|
||||
ComparisonStatusOutOfSync ComparisonStatus = "OutOfSync"
|
||||
)
|
||||
|
||||
// ApplicationStatus contains information about application status in target environment.
|
||||
type ApplicationStatus struct {
|
||||
ComparisonResult ComparisonResult `json:"comparisonResult" protobuf:"bytes,1,opt,name=comparisonResult"`
|
||||
RecentDeployments []DeploymentInfo `json:"recentDeployments" protobuf:"bytes,2,opt,name=recentDeployment"`
|
||||
ComparisonResult ComparisonResult `json:"comparisonResult" protobuf:"bytes,1,opt,name=comparisonResult"`
|
||||
History []DeploymentInfo `json:"history" protobuf:"bytes,2,opt,name=history"`
|
||||
Parameters []ComponentParameter `json:"parameters,omitempty" protobuf:"bytes,3,opt,name=parameters"`
|
||||
Health HealthStatus `json:"health,omitempty" protobuf:"bytes,4,opt,name=health"`
|
||||
OperationState *OperationState `json:"operationState,omitempty" protobuf:"bytes,5,opt,name=operationState"`
|
||||
Conditions []ApplicationCondition `json:"conditions,omitempty" protobuf:"bytes,6,opt,name=conditions"`
|
||||
}
|
||||
|
||||
// ApplicationConditionType represents type of application condition. Type name has following convention:
|
||||
// prefix "Error" means error condition
|
||||
// prefix "Warning" means warning condition
|
||||
// prefix "Info" means informational condition
|
||||
type ApplicationConditionType = string
|
||||
|
||||
const (
|
||||
// ApplicationConditionDeletionError indicates that controller failed to delete application
|
||||
ApplicationConditionDeletionError = "DeletionError"
|
||||
// ApplicationConditionInvalidSpecError indicates that application source is invalid
|
||||
ApplicationConditionInvalidSpecError = "InvalidSpecError"
|
||||
// ApplicationComparisonError indicates controller failed to compare application state
|
||||
ApplicationConditionComparisonError = "ComparisonError"
|
||||
// ApplicationConditionUnknownError indicates an unknown controller error
|
||||
ApplicationConditionUnknownError = "UnknownError"
|
||||
// ApplicationConditionSharedResourceWarning indicates that controller detected resources which belongs to more than one application
|
||||
ApplicationConditionSharedResourceWarning = "SharedResourceWarning"
|
||||
)
|
||||
|
||||
// ApplicationCondition contains details about current application condition
|
||||
type ApplicationCondition struct {
|
||||
// Type is an application condition type
|
||||
Type ApplicationConditionType `json:"type" protobuf:"bytes,1,opt,name=type"`
|
||||
// Message contains human-readable message indicating details about condition
|
||||
Message string `json:"message" protobuf:"bytes,2,opt,name=message"`
|
||||
}
|
||||
|
||||
// ComparisonResult is a comparison result of application spec and deployed application.
|
||||
type ComparisonResult struct {
|
||||
ComparedAt metav1.Time `json:"comparedAt" protobuf:"bytes,1,opt,name=comparedAt"`
|
||||
ComparedTo ApplicationSource `json:"comparedTo" protobuf:"bytes,2,opt,name=comparedTo"`
|
||||
Server string `json:"server" protobuf:"bytes,3,opt,name=server"`
|
||||
Namespace string `json:"namespace" protobuf:"bytes,4,opt,name=namespace"`
|
||||
Status ComparisonStatus `json:"status" protobuf:"bytes,5,opt,name=status,casttype=ComparisonStatus"`
|
||||
Resources []ResourceState `json:"resources" protobuf:"bytes,6,opt,name=resources"`
|
||||
Error string `json:"error,omitempty" protobuf:"bytes,7,opt,name=error"`
|
||||
}
|
||||
|
||||
type HealthStatus struct {
|
||||
Status HealthStatusCode `json:"status,omitempty" protobuf:"bytes,1,opt,name=status"`
|
||||
StatusDetails string `json:"statusDetails,omitempty" protobuf:"bytes,2,opt,name=statusDetails"`
|
||||
}
|
||||
|
||||
type HealthStatusCode = string
|
||||
|
||||
const (
|
||||
HealthStatusUnknown = "Unknown"
|
||||
HealthStatusProgressing = "Progressing"
|
||||
HealthStatusHealthy = "Healthy"
|
||||
HealthStatusDegraded = "Degraded"
|
||||
HealthStatusMissing = "Missing"
|
||||
)
|
||||
|
||||
// ResourceNode contains information about live resource and its children
|
||||
type ResourceNode struct {
|
||||
State string `json:"state,omitempty" protobuf:"bytes,1,opt,name=state"`
|
||||
Children []ResourceNode `json:"children,omitempty" protobuf:"bytes,2,opt,name=children"`
|
||||
}
|
||||
|
||||
// ResourceState holds the target state of a resource and live state of a resource
|
||||
type ResourceState struct {
|
||||
TargetState string `json:"targetState,omitempty" protobuf:"bytes,1,opt,name=targetState"`
|
||||
LiveState string `json:"liveState,omitempty" protobuf:"bytes,2,opt,name=liveState"`
|
||||
Status ComparisonStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
|
||||
TargetState string `json:"targetState,omitempty" protobuf:"bytes,1,opt,name=targetState"`
|
||||
LiveState string `json:"liveState,omitempty" protobuf:"bytes,2,opt,name=liveState"`
|
||||
Status ComparisonStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
|
||||
ChildLiveResources []ResourceNode `json:"childLiveResources,omitempty" protobuf:"bytes,4,opt,name=childLiveResources"`
|
||||
Health HealthStatus `json:"health,omitempty" protobuf:"bytes,5,opt,name=health"`
|
||||
}
|
||||
|
||||
// ConnectionStatus represents connection status
|
||||
type ConnectionStatus = string
|
||||
|
||||
const (
|
||||
ConnectionStatusUnknown = "Unknown"
|
||||
ConnectionStatusSuccessful = "Successful"
|
||||
ConnectionStatusFailed = "Failed"
|
||||
)
|
||||
|
||||
// ConnectionState contains information about remote resource connection state
|
||||
type ConnectionState struct {
|
||||
Status ConnectionStatus `json:"status" protobuf:"bytes,1,opt,name=status"`
|
||||
Message string `json:"message" protobuf:"bytes,2,opt,name=message"`
|
||||
ModifiedAt *metav1.Time `json:"attemptedAt" protobuf:"bytes,3,opt,name=attemptedAt"`
|
||||
}
|
||||
|
||||
// Cluster is the definition of a cluster resource
|
||||
@@ -134,6 +363,9 @@ type Cluster struct {
|
||||
|
||||
// Config holds cluster information for connecting to a cluster
|
||||
Config ClusterConfig `json:"config" protobuf:"bytes,3,opt,name=config"`
|
||||
|
||||
// ConnectionState contains information about cluster connection state
|
||||
ConnectionState ConnectionState `json:"connectionState,omitempty" protobuf:"bytes,4,opt,name=connectionState"`
|
||||
}
|
||||
|
||||
// ClusterList is a collection of Clusters.
|
||||
@@ -180,10 +412,11 @@ type TLSClientConfig struct {
|
||||
|
||||
// Repository is a Git repository holding application configurations
|
||||
type Repository struct {
|
||||
Repo string `json:"repo" protobuf:"bytes,1,opt,name=repo"`
|
||||
Username string `json:"username" protobuf:"bytes,2,opt,name=username"`
|
||||
Password string `json:"password" protobuf:"bytes,3,opt,name=password"`
|
||||
SSHPrivateKey string `json:"sshPrivateKey" protobuf:"bytes,4,opt,name=sshPrivateKey"`
|
||||
Repo string `json:"repo" protobuf:"bytes,1,opt,name=repo"`
|
||||
Username string `json:"username,omitempty" protobuf:"bytes,2,opt,name=username"`
|
||||
Password string `json:"password,omitempty" protobuf:"bytes,3,opt,name=password"`
|
||||
SSHPrivateKey string `json:"sshPrivateKey,omitempty" protobuf:"bytes,4,opt,name=sshPrivateKey"`
|
||||
ConnectionState ConnectionState `json:"connectionState,omitempty" protobuf:"bytes,5,opt,name=connectionState"`
|
||||
}
|
||||
|
||||
// RepositoryList is a collection of Repositories.
|
||||
@@ -192,19 +425,132 @@ type RepositoryList struct {
|
||||
Items []Repository `json:"items" protobuf:"bytes,2,rep,name=items"`
|
||||
}
|
||||
|
||||
// NeedRefreshAppStatus answers if application status needs to be refreshed. Returns true if application never been compared, has changed or comparison result has expired.
|
||||
func (app *Application) NeedRefreshAppStatus(statusRefreshTimeout time.Duration) bool {
|
||||
return app.Status.ComparisonResult.Status == ComparisonStatusUnknown ||
|
||||
!app.Spec.Source.Equals(app.Status.ComparisonResult.ComparedTo) ||
|
||||
app.Status.ComparisonResult.ComparedAt.Add(statusRefreshTimeout).Before(time.Now())
|
||||
// AppProjectList is list of AppProject resources
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type AppProjectList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"`
|
||||
Items []AppProject `json:"items" protobuf:"bytes,2,rep,name=items"`
|
||||
}
|
||||
|
||||
// AppProject is a definition of AppProject resource.
|
||||
// +genclient
|
||||
// +genclient:noStatus
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
type AppProject struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"`
|
||||
Spec AppProjectSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"`
|
||||
}
|
||||
|
||||
// AppProjectSpec represents
|
||||
type AppProjectSpec struct {
|
||||
// SourceRepos contains list of git repository URLs which can be used for deployment
|
||||
SourceRepos []string `json:"sourceRepos" protobuf:"bytes,1,name=sourceRepos"`
|
||||
|
||||
// Destinations contains list of destinations available for deployment
|
||||
Destinations []ApplicationDestination `json:"destinations" protobuf:"bytes,2,name=destination"`
|
||||
|
||||
// Description contains optional project description
|
||||
Description string `json:"description,omitempty" protobuf:"bytes,3,opt,name=description"`
|
||||
}
|
||||
|
||||
func GetDefaultProject(namespace string) AppProject {
|
||||
return AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: common.DefaultAppProjectName,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (app *Application) getFinalizerIndex(name string) int {
|
||||
for i, finalizer := range app.Finalizers {
|
||||
if finalizer == name {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// CascadedDeletion indicates if resources finalizer is set and controller should delete app resources before deleting app
|
||||
func (app *Application) CascadedDeletion() bool {
|
||||
return app.getFinalizerIndex(common.ResourcesFinalizerName) > -1
|
||||
}
|
||||
|
||||
// SetCascadedDeletion sets or remove resources finalizer
|
||||
func (app *Application) SetCascadedDeletion(prune bool) {
|
||||
index := app.getFinalizerIndex(common.ResourcesFinalizerName)
|
||||
if prune != (index > -1) {
|
||||
if index > -1 {
|
||||
app.Finalizers[index] = app.Finalizers[len(app.Finalizers)-1]
|
||||
app.Finalizers = app.Finalizers[:len(app.Finalizers)-1]
|
||||
} else {
|
||||
app.Finalizers = append(app.Finalizers, common.ResourcesFinalizerName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetErrorConditions returns list of application error conditions
|
||||
func (status *ApplicationStatus) GetErrorConditions() []ApplicationCondition {
|
||||
result := make([]ApplicationCondition, 0)
|
||||
for i := range status.Conditions {
|
||||
condition := status.Conditions[i]
|
||||
if condition.IsError() {
|
||||
result = append(result, condition)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// IsError returns true if condition is error condition
|
||||
func (condition *ApplicationCondition) IsError() bool {
|
||||
return strings.HasSuffix(condition.Type, "Error")
|
||||
}
|
||||
|
||||
// Equals compares two instances of ApplicationSource and return true if instances are equal.
|
||||
func (source ApplicationSource) Equals(other ApplicationSource) bool {
|
||||
return source.TargetRevision == other.TargetRevision &&
|
||||
source.RepoURL == other.RepoURL &&
|
||||
source.Path == other.Path &&
|
||||
source.Environment == other.Environment
|
||||
return reflect.DeepEqual(source, other)
|
||||
}
|
||||
|
||||
func (spec ApplicationSpec) BelongsToDefaultProject() bool {
|
||||
return spec.GetProject() == common.DefaultAppProjectName
|
||||
}
|
||||
|
||||
func (spec ApplicationSpec) GetProject() string {
|
||||
if spec.Project == "" {
|
||||
return common.DefaultAppProjectName
|
||||
}
|
||||
return spec.Project
|
||||
}
|
||||
|
||||
func (proj AppProject) IsDefault() bool {
|
||||
return proj.Name == "" || proj.Name == common.DefaultAppProjectName
|
||||
}
|
||||
|
||||
func (proj AppProject) IsSourcePermitted(src ApplicationSource) bool {
|
||||
if proj.IsDefault() {
|
||||
return true
|
||||
}
|
||||
normalizedURL := git.NormalizeGitURL(src.RepoURL)
|
||||
for _, repoURL := range proj.Spec.SourceRepos {
|
||||
if git.NormalizeGitURL(repoURL) == normalizedURL {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (proj AppProject) IsDestinationPermitted(dst ApplicationDestination) bool {
|
||||
if proj.IsDefault() {
|
||||
return true
|
||||
}
|
||||
for _, item := range proj.Spec.Destinations {
|
||||
if item.Server == dst.Server && item.Namespace == dst.Namespace {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RESTConfig returns a go-client REST config from cluster
|
||||
@@ -228,7 +574,7 @@ func (c *Cluster) RESTConfig() *rest.Config {
|
||||
func (cr *ComparisonResult) TargetObjects() ([]*unstructured.Unstructured, error) {
|
||||
objs := make([]*unstructured.Unstructured, len(cr.Resources))
|
||||
for i, resState := range cr.Resources {
|
||||
obj, err := UnmarshalToUnstructured(resState.TargetState)
|
||||
obj, err := resState.TargetObject()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -241,7 +587,7 @@ func (cr *ComparisonResult) TargetObjects() ([]*unstructured.Unstructured, error
|
||||
func (cr *ComparisonResult) LiveObjects() ([]*unstructured.Unstructured, error) {
|
||||
objs := make([]*unstructured.Unstructured, len(cr.Resources))
|
||||
for i, resState := range cr.Resources {
|
||||
obj, err := UnmarshalToUnstructured(resState.LiveState)
|
||||
obj, err := resState.LiveObject()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -261,3 +607,11 @@ func UnmarshalToUnstructured(resource string) (*unstructured.Unstructured, error
|
||||
}
|
||||
return &obj, nil
|
||||
}
|
||||
|
||||
func (r ResourceState) LiveObject() (*unstructured.Unstructured, error) {
|
||||
return UnmarshalToUnstructured(r.LiveState)
|
||||
}
|
||||
|
||||
func (r ResourceState) TargetObject() (*unstructured.Unstructured, error) {
|
||||
return UnmarshalToUnstructured(r.TargetState)
|
||||
}
|
||||
|
||||
@@ -5,9 +5,96 @@
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *AppProject) DeepCopyInto(out *AppProject) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppProject.
|
||||
func (in *AppProject) DeepCopy() *AppProject {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(AppProject)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *AppProject) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *AppProjectList) DeepCopyInto(out *AppProjectList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
out.ListMeta = in.ListMeta
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]AppProject, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppProjectList.
|
||||
func (in *AppProjectList) DeepCopy() *AppProjectList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(AppProjectList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *AppProjectList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *AppProjectSpec) DeepCopyInto(out *AppProjectSpec) {
|
||||
*out = *in
|
||||
if in.SourceRepos != nil {
|
||||
in, out := &in.SourceRepos, &out.SourceRepos
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Destinations != nil {
|
||||
in, out := &in.Destinations, &out.Destinations
|
||||
*out = make([]ApplicationDestination, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppProjectSpec.
|
||||
func (in *AppProjectSpec) DeepCopy() *AppProjectSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(AppProjectSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Application) DeepCopyInto(out *Application) {
|
||||
*out = *in
|
||||
@@ -15,6 +102,15 @@ func (in *Application) DeepCopyInto(out *Application) {
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
if in.Operation != nil {
|
||||
in, out := &in.Operation, &out.Operation
|
||||
if *in == nil {
|
||||
*out = nil
|
||||
} else {
|
||||
*out = new(Operation)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -36,6 +132,22 @@ func (in *Application) DeepCopyObject() runtime.Object {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ApplicationCondition) DeepCopyInto(out *ApplicationCondition) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ApplicationCondition.
|
||||
func (in *ApplicationCondition) DeepCopy() *ApplicationCondition {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ApplicationCondition)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ApplicationDestination) DeepCopyInto(out *ApplicationDestination) {
|
||||
*out = *in
|
||||
@@ -93,6 +205,11 @@ func (in *ApplicationSource) DeepCopyInto(out *ApplicationSource) {
|
||||
*out = make([]ComponentParameter, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.ValuesFiles != nil {
|
||||
in, out := &in.ValuesFiles, &out.ValuesFiles
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -110,15 +227,7 @@ func (in *ApplicationSource) DeepCopy() *ApplicationSource {
|
||||
func (in *ApplicationSpec) DeepCopyInto(out *ApplicationSpec) {
|
||||
*out = *in
|
||||
in.Source.DeepCopyInto(&out.Source)
|
||||
if in.Destination != nil {
|
||||
in, out := &in.Destination, &out.Destination
|
||||
if *in == nil {
|
||||
*out = nil
|
||||
} else {
|
||||
*out = new(ApplicationDestination)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
out.Destination = in.Destination
|
||||
return
|
||||
}
|
||||
|
||||
@@ -136,13 +245,33 @@ func (in *ApplicationSpec) DeepCopy() *ApplicationSpec {
|
||||
func (in *ApplicationStatus) DeepCopyInto(out *ApplicationStatus) {
|
||||
*out = *in
|
||||
in.ComparisonResult.DeepCopyInto(&out.ComparisonResult)
|
||||
if in.RecentDeployments != nil {
|
||||
in, out := &in.RecentDeployments, &out.RecentDeployments
|
||||
if in.History != nil {
|
||||
in, out := &in.History, &out.History
|
||||
*out = make([]DeploymentInfo, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.Parameters != nil {
|
||||
in, out := &in.Parameters, &out.Parameters
|
||||
*out = make([]ComponentParameter, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
out.Health = in.Health
|
||||
if in.OperationState != nil {
|
||||
in, out := &in.OperationState, &out.OperationState
|
||||
if *in == nil {
|
||||
*out = nil
|
||||
} else {
|
||||
*out = new(OperationState)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
if in.Conditions != nil {
|
||||
in, out := &in.Conditions, &out.Conditions
|
||||
*out = make([]ApplicationCondition, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -177,6 +306,7 @@ func (in *ApplicationWatchEvent) DeepCopy() *ApplicationWatchEvent {
|
||||
func (in *Cluster) DeepCopyInto(out *Cluster) {
|
||||
*out = *in
|
||||
in.Config.DeepCopyInto(&out.Config)
|
||||
in.ConnectionState.DeepCopyInto(&out.ConnectionState)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -239,7 +369,9 @@ func (in *ComparisonResult) DeepCopyInto(out *ComparisonResult) {
|
||||
if in.Resources != nil {
|
||||
in, out := &in.Resources, &out.Resources
|
||||
*out = make([]ResourceState, len(*in))
|
||||
copy(*out, *in)
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -270,6 +402,31 @@ func (in *ComponentParameter) DeepCopy() *ComponentParameter {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ConnectionState) DeepCopyInto(out *ConnectionState) {
|
||||
*out = *in
|
||||
if in.ModifiedAt != nil {
|
||||
in, out := &in.ModifiedAt, &out.ModifiedAt
|
||||
if *in == nil {
|
||||
*out = nil
|
||||
} else {
|
||||
*out = new(v1.Time)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConnectionState.
|
||||
func (in *ConnectionState) DeepCopy() *ConnectionState {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ConnectionState)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *DeploymentInfo) DeepCopyInto(out *DeploymentInfo) {
|
||||
*out = *in
|
||||
@@ -297,9 +454,121 @@ func (in *DeploymentInfo) DeepCopy() *DeploymentInfo {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *HealthStatus) DeepCopyInto(out *HealthStatus) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthStatus.
|
||||
func (in *HealthStatus) DeepCopy() *HealthStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(HealthStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *HookStatus) DeepCopyInto(out *HookStatus) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HookStatus.
|
||||
func (in *HookStatus) DeepCopy() *HookStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(HookStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Operation) DeepCopyInto(out *Operation) {
|
||||
*out = *in
|
||||
if in.Sync != nil {
|
||||
in, out := &in.Sync, &out.Sync
|
||||
if *in == nil {
|
||||
*out = nil
|
||||
} else {
|
||||
*out = new(SyncOperation)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
if in.Rollback != nil {
|
||||
in, out := &in.Rollback, &out.Rollback
|
||||
if *in == nil {
|
||||
*out = nil
|
||||
} else {
|
||||
*out = new(RollbackOperation)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Operation.
|
||||
func (in *Operation) DeepCopy() *Operation {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Operation)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *OperationState) DeepCopyInto(out *OperationState) {
|
||||
*out = *in
|
||||
in.Operation.DeepCopyInto(&out.Operation)
|
||||
if in.SyncResult != nil {
|
||||
in, out := &in.SyncResult, &out.SyncResult
|
||||
if *in == nil {
|
||||
*out = nil
|
||||
} else {
|
||||
*out = new(SyncOperationResult)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
if in.RollbackResult != nil {
|
||||
in, out := &in.RollbackResult, &out.RollbackResult
|
||||
if *in == nil {
|
||||
*out = nil
|
||||
} else {
|
||||
*out = new(SyncOperationResult)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
in.StartedAt.DeepCopyInto(&out.StartedAt)
|
||||
if in.FinishedAt != nil {
|
||||
in, out := &in.FinishedAt, &out.FinishedAt
|
||||
if *in == nil {
|
||||
*out = nil
|
||||
} else {
|
||||
*out = new(v1.Time)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperationState.
|
||||
func (in *OperationState) DeepCopy() *OperationState {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(OperationState)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Repository) DeepCopyInto(out *Repository) {
|
||||
*out = *in
|
||||
in.ConnectionState.DeepCopyInto(&out.ConnectionState)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -320,7 +589,9 @@ func (in *RepositoryList) DeepCopyInto(out *RepositoryList) {
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]Repository, len(*in))
|
||||
copy(*out, *in)
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -335,9 +606,56 @@ func (in *RepositoryList) DeepCopy() *RepositoryList {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ResourceDetails) DeepCopyInto(out *ResourceDetails) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceDetails.
|
||||
func (in *ResourceDetails) DeepCopy() *ResourceDetails {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ResourceDetails)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ResourceNode) DeepCopyInto(out *ResourceNode) {
|
||||
*out = *in
|
||||
if in.Children != nil {
|
||||
in, out := &in.Children, &out.Children
|
||||
*out = make([]ResourceNode, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceNode.
|
||||
func (in *ResourceNode) DeepCopy() *ResourceNode {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ResourceNode)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ResourceState) DeepCopyInto(out *ResourceState) {
|
||||
*out = *in
|
||||
if in.ChildLiveResources != nil {
|
||||
in, out := &in.ChildLiveResources, &out.ChildLiveResources
|
||||
*out = make([]ResourceNode, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
out.Health = in.Health
|
||||
return
|
||||
}
|
||||
|
||||
@@ -351,6 +669,154 @@ func (in *ResourceState) DeepCopy() *ResourceState {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *RollbackOperation) DeepCopyInto(out *RollbackOperation) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RollbackOperation.
|
||||
func (in *RollbackOperation) DeepCopy() *RollbackOperation {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(RollbackOperation)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SyncOperation) DeepCopyInto(out *SyncOperation) {
|
||||
*out = *in
|
||||
if in.SyncStrategy != nil {
|
||||
in, out := &in.SyncStrategy, &out.SyncStrategy
|
||||
if *in == nil {
|
||||
*out = nil
|
||||
} else {
|
||||
*out = new(SyncStrategy)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SyncOperation.
|
||||
func (in *SyncOperation) DeepCopy() *SyncOperation {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SyncOperation)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SyncOperationResult) DeepCopyInto(out *SyncOperationResult) {
|
||||
*out = *in
|
||||
if in.Resources != nil {
|
||||
in, out := &in.Resources, &out.Resources
|
||||
*out = make([]*ResourceDetails, len(*in))
|
||||
for i := range *in {
|
||||
if (*in)[i] == nil {
|
||||
(*out)[i] = nil
|
||||
} else {
|
||||
(*out)[i] = new(ResourceDetails)
|
||||
(*in)[i].DeepCopyInto((*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
if in.Hooks != nil {
|
||||
in, out := &in.Hooks, &out.Hooks
|
||||
*out = make([]*HookStatus, len(*in))
|
||||
for i := range *in {
|
||||
if (*in)[i] == nil {
|
||||
(*out)[i] = nil
|
||||
} else {
|
||||
(*out)[i] = new(HookStatus)
|
||||
(*in)[i].DeepCopyInto((*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SyncOperationResult.
|
||||
func (in *SyncOperationResult) DeepCopy() *SyncOperationResult {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SyncOperationResult)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SyncStrategy) DeepCopyInto(out *SyncStrategy) {
|
||||
*out = *in
|
||||
if in.Apply != nil {
|
||||
in, out := &in.Apply, &out.Apply
|
||||
if *in == nil {
|
||||
*out = nil
|
||||
} else {
|
||||
*out = new(SyncStrategyApply)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
if in.Hook != nil {
|
||||
in, out := &in.Hook, &out.Hook
|
||||
if *in == nil {
|
||||
*out = nil
|
||||
} else {
|
||||
*out = new(SyncStrategyHook)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SyncStrategy.
|
||||
func (in *SyncStrategy) DeepCopy() *SyncStrategy {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SyncStrategy)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SyncStrategyApply) DeepCopyInto(out *SyncStrategyApply) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SyncStrategyApply.
|
||||
func (in *SyncStrategyApply) DeepCopy() *SyncStrategyApply {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SyncStrategyApply)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SyncStrategyHook) DeepCopyInto(out *SyncStrategyHook) {
|
||||
*out = *in
|
||||
out.SyncStrategyApply = in.SyncStrategyApply
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SyncStrategyHook.
|
||||
func (in *SyncStrategyHook) DeepCopy() *SyncStrategyHook {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SyncStrategyHook)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *TLSClientConfig) DeepCopyInto(out *TLSClientConfig) {
|
||||
*out = *in
|
||||
|
||||