mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-03-09 18:08:48 +01:00
Compare commits
269 Commits
release-0.
...
release-0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e5d08cf78 | ||
|
|
73c1677432 | ||
|
|
1901e52593 | ||
|
|
95c952a50d | ||
|
|
d9a395f473 | ||
|
|
83bcf461dd | ||
|
|
5af774dad0 | ||
|
|
bdeecc2ddb | ||
|
|
a5fbddab75 | ||
|
|
3577ea94a2 | ||
|
|
dec46cfae0 | ||
|
|
e1579824e9 | ||
|
|
aa3c1dd0ae | ||
|
|
66f3a0dad2 | ||
|
|
20678dc22e | ||
|
|
d47ad35208 | ||
|
|
265b2c8c29 | ||
|
|
fe078e7842 | ||
|
|
59d7abf65c | ||
|
|
e29f438134 | ||
|
|
fde6c5c741 | ||
|
|
0f23ca055a | ||
|
|
117cfb27bb | ||
|
|
689dce0e8f | ||
|
|
056e1d218d | ||
|
|
e859766fdf | ||
|
|
93d89bc3f1 | ||
|
|
d82974a266 | ||
|
|
71b646dfae | ||
|
|
f76a92944e | ||
|
|
f41af8bcc3 | ||
|
|
1a7a086da0 | ||
|
|
0ca2259fd3 | ||
|
|
3ee437e7f4 | ||
|
|
86aa44891a | ||
|
|
3f7c60c375 | ||
|
|
317dd15978 | ||
|
|
c5db14db3a | ||
|
|
2a3ad769b5 | ||
|
|
9fb7b99e6d | ||
|
|
4840a9c5fc | ||
|
|
5fe99816da | ||
|
|
0013b20394 | ||
|
|
db08f8b4fe | ||
|
|
7c2b92f70c | ||
|
|
b451c4fe53 | ||
|
|
de26eac63f | ||
|
|
71194d7fac | ||
|
|
ff2de56543 | ||
|
|
c8ad7ccf39 | ||
|
|
58b77718f8 | ||
|
|
5fff89bd12 | ||
|
|
c2975bd258 | ||
|
|
99daa3e315 | ||
|
|
d21c8d09fa | ||
|
|
75dc868ecf | ||
|
|
43064ac126 | ||
|
|
21902828ee | ||
|
|
d250155672 | ||
|
|
a8221c33ab | ||
|
|
f463589348 | ||
|
|
3a6dadd803 | ||
|
|
d265404e23 | ||
|
|
09d8a8ee42 | ||
|
|
dbac3d4905 | ||
|
|
ec0e64975e | ||
|
|
6802e1ed35 | ||
|
|
d09f8f974e | ||
|
|
7a6951dfe2 | ||
|
|
476e351d12 | ||
|
|
e0bc94dcf6 | ||
|
|
457e137dda | ||
|
|
4a06693994 | ||
|
|
0f39f1032a | ||
|
|
4f12660c1b | ||
|
|
832564864f | ||
|
|
51638bc6f5 | ||
|
|
8ea474a3c9 | ||
|
|
689c498549 | ||
|
|
eb2a716661 | ||
|
|
0af3836f74 | ||
|
|
11b0c2848c | ||
|
|
636968d381 | ||
|
|
7eb211eb94 | ||
|
|
5cedfb8ead | ||
|
|
c20a49c531 | ||
|
|
874fe69683 | ||
|
|
cfefec06a9 | ||
|
|
0a7d14040d | ||
|
|
974ab11b76 | ||
|
|
7d40d228da | ||
|
|
c73ea2de1d | ||
|
|
c308165816 | ||
|
|
38720b2e46 | ||
|
|
e7b2e9f639 | ||
|
|
cbaf8a0bc8 | ||
|
|
f5861aa708 | ||
|
|
2fba6abc8d | ||
|
|
f762188b89 | ||
|
|
d987416c9b | ||
|
|
246392f0f6 | ||
|
|
0693a6dd70 | ||
|
|
4fa33f300b | ||
|
|
2c8e9fa9ac | ||
|
|
6ac3e8ec45 | ||
|
|
c66b444213 | ||
|
|
64be0913ad | ||
|
|
ec70110ab2 | ||
|
|
93ad11095a | ||
|
|
26af75061e | ||
|
|
83fb5fb388 | ||
|
|
6dede28f72 | ||
|
|
800c4b1d48 | ||
|
|
3a9196ce18 | ||
|
|
76c5df087a | ||
|
|
6368a3e548 | ||
|
|
cde040e10f | ||
|
|
477ca61da5 | ||
|
|
2b23812e6e | ||
|
|
b53ad60b48 | ||
|
|
db92f0b569 | ||
|
|
456ae3ab84 | ||
|
|
9347f4157e | ||
|
|
3b447a19ea | ||
|
|
a70cab7ac1 | ||
|
|
4b2aecc9bc | ||
|
|
9cb445a71a | ||
|
|
bcd9cd6cd7 | ||
|
|
16ff90c070 | ||
|
|
925f9486e3 | ||
|
|
b439424cef | ||
|
|
12423e9358 | ||
|
|
bb82919131 | ||
|
|
be4f9d85ae | ||
|
|
275b9e194d | ||
|
|
44087fa1b5 | ||
|
|
d3043461a6 | ||
|
|
3f7a0d3c97 | ||
|
|
361931f104 | ||
|
|
641b7fd060 | ||
|
|
17bcb3bdbf | ||
|
|
7520a8a4f8 | ||
|
|
8734f287eb | ||
|
|
7552a3610f | ||
|
|
f4387d5394 | ||
|
|
ffa0c340b8 | ||
|
|
34aad3f0df | ||
|
|
49e022a2c0 | ||
|
|
317d2a8aa8 | ||
|
|
f52589c128 | ||
|
|
75d2c57688 | ||
|
|
92d0df1412 | ||
|
|
34bb60f064 | ||
|
|
1206926ac2 | ||
|
|
958096aaa8 | ||
|
|
2c8c2fcd64 | ||
|
|
d7b3a5c6eb | ||
|
|
5c7a3329f3 | ||
|
|
a0b5af0dae | ||
|
|
37c25383b7 | ||
|
|
2498f60c57 | ||
|
|
7e390e76d0 | ||
|
|
ce7d02c94a | ||
|
|
b578de77f7 | ||
|
|
d055efba3c | ||
|
|
93bc108a24 | ||
|
|
3fd528de2b | ||
|
|
1844be638b | ||
|
|
dca1996640 | ||
|
|
cb3656b45c | ||
|
|
5c2eaf202e | ||
|
|
5abba4f85b | ||
|
|
22b77f5b34 | ||
|
|
c76db90437 | ||
|
|
080f7ff4e0 | ||
|
|
c5730c8f5f | ||
|
|
d46f284d9f | ||
|
|
221f19ae15 | ||
|
|
550cb277df | ||
|
|
9c79af9340 | ||
|
|
1ba52c8880 | ||
|
|
92629067f7 | ||
|
|
93a808e65a | ||
|
|
bf99b251f8 | ||
|
|
f491540636 | ||
|
|
7f84f7d541 | ||
|
|
42b01f7126 | ||
|
|
7e5c17939b | ||
|
|
d6937ec629 | ||
|
|
f5a32f47d3 | ||
|
|
316fcc6126 | ||
|
|
e163177a12 | ||
|
|
1fe257c71e | ||
|
|
1eaa813f28 | ||
|
|
924dad8980 | ||
|
|
1ba10a1a20 | ||
|
|
ab02e10791 | ||
|
|
dd94e5e5c3 | ||
|
|
1fcb90c4d9 | ||
|
|
523c7ddf82 | ||
|
|
3577a68d2d | ||
|
|
d963f5fcc5 | ||
|
|
bed82d68df | ||
|
|
359271dfa8 | ||
|
|
0af77a0706 | ||
|
|
e6efd79ad8 | ||
|
|
c953934d2e | ||
|
|
5b4742d42b | ||
|
|
269f70df51 | ||
|
|
67177f933b | ||
|
|
606fdcded7 | ||
|
|
70b9db68b4 | ||
|
|
dc8a2f5d62 | ||
|
|
8830cf9556 | ||
|
|
bfb558eb92 | ||
|
|
505866a4c6 | ||
|
|
acd2de80fb | ||
|
|
0b08bf4537 | ||
|
|
223091482c | ||
|
|
4699946e1b | ||
|
|
097f87fd52 | ||
|
|
66b4f3a685 | ||
|
|
02116d4bfc | ||
|
|
fb17589af6 | ||
|
|
15ce7ea880 | ||
|
|
57a3123a55 | ||
|
|
32e96e4bb2 | ||
|
|
47ee26a77a | ||
|
|
9cd5d52fbc | ||
|
|
aa2afcd47b | ||
|
|
fd510e7933 | ||
|
|
e29d5b9634 | ||
|
|
2f9891b15b | ||
|
|
c3ecd615ff | ||
|
|
4e22a3cb21 | ||
|
|
bc98b65190 | ||
|
|
02b756ef40 | ||
|
|
954706570c | ||
|
|
e2faf6970f | ||
|
|
a528ae9c12 | ||
|
|
0a5871eba4 | ||
|
|
27471d5249 | ||
|
|
ed484c00db | ||
|
|
b868f26ca4 | ||
|
|
d7c04ae24c | ||
|
|
5bcf8c40e0 | ||
|
|
b8e30ed953 | ||
|
|
e3adb30ca7 | ||
|
|
1e8c570c8a | ||
|
|
40f2220f1d | ||
|
|
4da779c44c | ||
|
|
b54a5a3e25 | ||
|
|
f572bcff58 | ||
|
|
d47b7e6128 | ||
|
|
8d9e4faae9 | ||
|
|
cf630055b0 | ||
|
|
a5870c894f | ||
|
|
c236ee99d4 | ||
|
|
130e242aa9 | ||
|
|
39f0a17d0d | ||
|
|
da0682afa7 | ||
|
|
3c755a2002 | ||
|
|
66f64fbf15 | ||
|
|
4c0a0e09e2 | ||
|
|
f8de6084ed | ||
|
|
36624f9d89 | ||
|
|
cbf1e3419b | ||
|
|
7c8cc41d4c | ||
|
|
5dbbd0a76f |
@@ -19,17 +19,13 @@ spec:
|
||||
arguments:
|
||||
parameters:
|
||||
- name: cmd
|
||||
value: "{{item}}"
|
||||
withItems:
|
||||
- make controller-image server-image repo-server-image
|
||||
value: make image
|
||||
- name: test
|
||||
template: ci-builder
|
||||
arguments:
|
||||
parameters:
|
||||
- name: cmd
|
||||
value: "{{item}}"
|
||||
withItems:
|
||||
- dep ensure && make cli lint test
|
||||
value: "dep ensure && make lint test && bash <(curl -s https://codecov.io/bash) -f coverage.out"
|
||||
- name: test-e2e
|
||||
template: ci-builder
|
||||
arguments:
|
||||
@@ -49,13 +45,21 @@ spec:
|
||||
revision: "{{workflow.parameters.revision}}"
|
||||
container:
|
||||
image: argoproj/argo-cd-ci-builder:latest
|
||||
command: [sh, -c]
|
||||
command: [bash, -c]
|
||||
args: ["{{inputs.parameters.cmd}}"]
|
||||
workingDir: /go/src/github.com/argoproj/argo-cd
|
||||
env:
|
||||
- name: CODECOV_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: codecov-token
|
||||
key: codecov-token
|
||||
resources:
|
||||
requests:
|
||||
memory: 1024Mi
|
||||
cpu: 200m
|
||||
archiveLocation:
|
||||
archiveLogs: true
|
||||
|
||||
- name: ci-dind
|
||||
inputs:
|
||||
@@ -81,8 +85,9 @@ spec:
|
||||
cpu: 200m
|
||||
sidecars:
|
||||
- name: dind
|
||||
image: docker:17.10-dind
|
||||
image: docker:18.09-dind
|
||||
securityContext:
|
||||
privileged: true
|
||||
mirrorVolumeMounts: true
|
||||
|
||||
archiveLocation:
|
||||
archiveLogs: true
|
||||
|
||||
6
.codecov.yml
Normal file
6
.codecov.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
ignore:
|
||||
- "**/*.pb.go"
|
||||
- "**/*_test.go"
|
||||
- "pkg/apis/.*"
|
||||
- "pkg/client/.*"
|
||||
- "test/.*"
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@ dist/
|
||||
# delve debug binaries
|
||||
cmd/**/debug
|
||||
debug.test
|
||||
coverage.out
|
||||
|
||||
290
CHANGELOG.md
290
CHANGELOG.md
@@ -1,5 +1,293 @@
|
||||
# Changelog
|
||||
|
||||
## v0.11.0
|
||||
This is Argo CD's biggest release ever and introduces a completely redesigned controller architecture.
|
||||
|
||||
### New Features
|
||||
|
||||
#### New application controller architecture
|
||||
The application controller has a completely redesigned architecture for better scalability, and
|
||||
improve performance during application reconciliation. This was achieved by maintaining an
|
||||
in-memory, live state cache of lightweight Kubernetes object metadata. During reconciliation, the
|
||||
controller no longer performs expensive, in-line queries of app labeled resources in K8s API server,
|
||||
instead relying on the metadata in the local state cache. This dramatically improves performance
|
||||
and responsiveness, and is less burdensome the K8s API server. A second benefit to this, is that the
|
||||
relationship between object when computing the resource tree, can be displayed, even for custom
|
||||
resources.
|
||||
|
||||
#### Multi-namespaced applications
|
||||
Argo CD will now honor any explicitly set namespace in a mainfest. Resources without a namespace
|
||||
will continue to be deployed to the namespace specified in `spec.destination.namespace`. This
|
||||
enables support for a class of applications that install to multiple namespaces. For example,
|
||||
Argo CD now supports the istio helm chart, which deploys some resources to an explit `istio-system`
|
||||
namespace.
|
||||
|
||||
#### Large application support
|
||||
Full resource objects are no longer stored in the Application CRD object status. Instead, only
|
||||
lightweight metadata is stored in the status, such as a resource's sync and health status.
|
||||
This change enables Argo CD to support applications with a very large number of resources
|
||||
(e.g. istio), and reduces the bandwidth requirements when listing applications in the UI.
|
||||
|
||||
#### Resource lifecycle hook improvements
|
||||
Resource hooks are now visible from the UI. Additionally, bare Pods with a restart policy of Never
|
||||
can now be used as a resource hook, as an alternative to Jobs, Workflows.
|
||||
|
||||
#### K8s recommended application labels
|
||||
Resource labeling has been changed to use `app.kubernetes.io/instance` as recommended in
|
||||
[Kubernetes recommended labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/),
|
||||
(changed from `applications.argoproj.io/app-name`). This will enable applications created by Argo CD
|
||||
to interoperate with other tooling that are also converging on this labeling, such as the Kubernetes
|
||||
dashboard. Additionally, Argo CD will no longer inject any tracking labels at the
|
||||
`spec.template.metadata` level.
|
||||
|
||||
#### External OIDC provider support
|
||||
Argo CD now supports auth delegation to an existing, external OIDC providers without the need for
|
||||
Dex (e.g. Okta, OneLogin, Auth0, Microsoft, etc...)
|
||||
|
||||
The optional, [Dex IDP OIDC provider](https://github.com/dexidp/dex) is still bundled as part of the
|
||||
default installation, in order to provide a seamless out-of-box experience, and enables Argo CD to
|
||||
integrate with non-OIDC providers, or to benefit from Dex's full range of
|
||||
[connectors](https://github.com/dexidp/dex/tree/master/Documentation/connectors).
|
||||
|
||||
#### OIDC group claims bindings to Project Roles
|
||||
Group claims from the OIDC provider can now be bound to Argo CD project roles. Previously, group
|
||||
claims were managed at the centralized ConfigMap, `argocd-rbac-cm`. This enables project admins to
|
||||
self service access to applications within a project.
|
||||
|
||||
#### Declarative Argo CD configuration
|
||||
Argo CD settings can be now be configured either declaratively, or imperatively. The `argocd-cm`
|
||||
ConfigMap now has a `repositories` field, which can reference credentials in a normal Kubernetes
|
||||
secret which you can create declaratively, outside of Argo CD.
|
||||
|
||||
#### Helm repository support
|
||||
Helm repositories can be configured at the system level, enabling the deployment of helm charts
|
||||
which have a dependency to external helm repositories.
|
||||
|
||||
### Breaking changes:
|
||||
|
||||
* As a consequence to moving to recommended kubernetes labels, when upgrading from v0.10 to v0.11,
|
||||
all applications will immediately be OutOfSync due to the change in labeling techniques. This will
|
||||
correct itself with another sync of the application. However, since Pods will be recreated, please
|
||||
take this into consideration, especially if your applications is configured with auto-sync.
|
||||
|
||||
* There was significant reworking of the `app.status` fields to simplify the datastructure and
|
||||
remove fields which were no longer used by the controller. No breaking changes were made in
|
||||
`app.spec`.
|
||||
|
||||
* An older Argo CD CLI (v0.10 and below) will not be compatible with an Argo CD v0.11. To keep
|
||||
CI pipelines in sync with the API server, it is recommended to have pipelines download the CLI
|
||||
directly from the API server https://${ARGOCD_SERVER}/download/argocd-linux-amd64 during the CI
|
||||
pipeline.
|
||||
|
||||
### Changes since v0.10:
|
||||
+ Declarative setup and configuration of ArgoCD (#536)
|
||||
+ Declaratively add helm repositories (#747)
|
||||
+ Switch to k8s recommended app.kubernetes.io/instance label (#857)
|
||||
+ Ability for a single application to deploy into multiple namespaces (#696)
|
||||
+ Self service group access to project applications (#742)
|
||||
+ Support for Pods as a sync hook (#801)
|
||||
+ Support 'crd-install' helm hook (#355)
|
||||
* Remove resources state from application CRD (#758)
|
||||
* Refactor, consolidate and rename resource type data structures
|
||||
* Improve Application state reconciliation performance (#806)
|
||||
* API server & UI should serve argocd binaries instead of linking to GitHub (#716)
|
||||
- Failed to deploy helm chart with local dependencies and no internet access (#786)
|
||||
- Out of sync reported if Secrets with stringData are used (#763)
|
||||
- Unable to delete application in K8s v1.12 (#718)
|
||||
|
||||
|
||||
## v0.10.6 (2018-11-14)
|
||||
- Fix issue preventing in-cluster app sync due to go-client changes (issue #774)
|
||||
|
||||
## v0.10.5 (2018-11-13)
|
||||
+ Increase concurrency of application controller
|
||||
* Update dependencies to k8s v1.12 and client-go v9.0 (#729)
|
||||
- add argo cluster permission to view logs (#766) (@conorfennell)
|
||||
- Fix issue where applications could not be deleted on k8s v1.12
|
||||
- Allow 'syncApplication' action to reference target revision rather then hard-coding to 'HEAD' (#69) (@chrisgarland)
|
||||
- Issue #768 - Fix application wizard crash
|
||||
|
||||
## v0.10.4 (2018-11-07)
|
||||
* Upgrade to Helm v0.11.0 (@amarrella)
|
||||
- Health check is not discerning apiVersion when assessing CRDs (issue #753)
|
||||
- Fix nil pointer dereference in util/health (@mduarte)
|
||||
|
||||
## v0.10.3 (2018-10-28)
|
||||
* Fix applying TLS version settings
|
||||
* Update to kustomize 1.0.10 (@twz123)
|
||||
|
||||
## v0.10.2 (2018-10-25)
|
||||
* Update to kustomize 1.0.9 (@twz123)
|
||||
- Fix app refresh err when k8s patch is too slow
|
||||
|
||||
## v0.10.1 (2018-10-24)
|
||||
|
||||
- Handle case where OIDC settings become invalid after dex server restart (issue #710)
|
||||
- git clean also needs to clean files under gitignore (issue #711)
|
||||
|
||||
## v0.10.0 (2018-10-19)
|
||||
|
||||
### Changes since v0.9:
|
||||
|
||||
+ Allow more fine-grained sync (issue #508)
|
||||
+ Display init container logs (issue #681)
|
||||
+ Redirect to /auth/login instead of /login when SSO token is used for authenticaion (issue #348)
|
||||
+ Support ability to use a helm values files from a URL (issue #624)
|
||||
+ Support public not-connected repo in app creation UI (issue #426)
|
||||
+ Use ksonnet CLI instead of ksonnet libs (issue #626)
|
||||
+ We should be able to select the order of the `yaml` files while creating a Helm App (#664)
|
||||
* Remove default params from app history (issue #556)
|
||||
* Update to ksonnet v0.13.0
|
||||
* Update to kustomize 1.0.8
|
||||
- API Server fails to return apps due to grpc max message size limit (issue #690)
|
||||
- App Creation UI for Helm Apps shows only files prefixed with `values-` (issue #663)
|
||||
- App creation UI should allow specifying values files outside of helm app directory bug (issue #658)
|
||||
- argocd-server logs credentials in plain text when adding git repositories (issue #653)
|
||||
- Azure Repos do not work as a repository (issue #643)
|
||||
- Better update conflict error handing during app editing (issue #685)
|
||||
- Cluster watch needs to be restarted when CRDs get created (issue #627)
|
||||
- Credentials not being accepted for Google Source Repositories (issue #651)
|
||||
- Default project is created without permission to deploy cluster level resources (issue #679)
|
||||
- Generate role token click resets policy changes (issue #655)
|
||||
- Input type text instead of password on Connect repo panel (issue #693)
|
||||
- Metrics endpoint not reachable through the metrics kubernetes service (issue #672)
|
||||
- Operation stuck in 'in progress' state if application has no resources (issue #682)
|
||||
- Project should influence options for cluster and namespace during app creation (issue #592)
|
||||
- Repo server unable to execute ls-remote for private repos (issue #639)
|
||||
- Resource is always out of sync if it has only 'ksonnet.io/component' label (issue #686)
|
||||
- Resource nodes are 'jumping' on app details page (issue #683)
|
||||
- Sync always suggest using latest revision instead of target UI bug (issue #669)
|
||||
- Temporary ignore service catalog resources (issue #650)
|
||||
|
||||
## v0.9.2 (2018-09-28)
|
||||
|
||||
* Update to kustomize 1.0.8
|
||||
- Fix issue where argocd-server logged credentials in plain text during repo add (issue #653)
|
||||
- Credentials not being accepted for Google Source Repositories (issue #651)
|
||||
- Azure Repos do not work as a repository (issue #643)
|
||||
- Temporary ignore service catalog resources (issue #650)
|
||||
- Normalize policies by always adding space after comma
|
||||
|
||||
## v0.9.1 (2018-09-24)
|
||||
|
||||
- Repo server unable to execute ls-remote for private repos (issue #639)
|
||||
|
||||
## v0.9.0 (2018-09-24)
|
||||
|
||||
### Notes about upgrading from v0.8
|
||||
* Cluster wide resources should be allowed in default project (due to issue #330):
|
||||
|
||||
```
|
||||
argocd project allow-cluster-resource default '*' '*'
|
||||
```
|
||||
|
||||
* Projects now provide the ability to allow or deny deployments of cluster-scoped resources
|
||||
(e.g. Namespaces, ClusterRoles, CustomResourceDefinitions). When upgrading from v0.8 to v0.9, to
|
||||
match the behavior of v0.8 (which did not have restrictions on deploying resources) and continue to
|
||||
allow deployment of cluster-scoped resources, an additional command should be run:
|
||||
|
||||
```bash
|
||||
argocd proj allow-cluster-resource default '*' '*'
|
||||
```
|
||||
|
||||
The above command allows the `default` project to deploy any cluster-scoped resources which matches
|
||||
the behavior of v0.8.
|
||||
|
||||
* The secret keys in the argocd-secret containing the TLS certificate and key, has been renamed from
|
||||
`server.crt` and `server.key` to the standard `tls.crt` and `tls.key` keys. This enables Argo CD
|
||||
to integrate better with Ingress and cert-manager. When upgrading to v0.9, the `server.crt` and
|
||||
`server.key` keys in argocd-secret should be renamed to the new keys.
|
||||
|
||||
### Changes since v0.8:
|
||||
+ Auto-sync option in application CRD instance (issue #79)
|
||||
+ Support raw jsonnet as an application source (issue #540)
|
||||
+ Reorder K8s resources to correct creation order (issue #102)
|
||||
+ Redact K8s secrets from API server payloads (issue #470)
|
||||
+ Support --in-cluster authentication without providing a kubeconfig (issue #527)
|
||||
+ Special handling of CustomResourceDefinitions (issue #613)
|
||||
+ Argo CD should download helm chart dependencies (issue #582)
|
||||
+ Export Argo CD stats as prometheus style metrics (issue #513)
|
||||
+ Support restricting TLS version (issue #609)
|
||||
+ Use 'kubectl auth reconcile' before 'kubectl apply' (issue #523)
|
||||
+ Projects need controls on cluster-scoped resources (issue #330)
|
||||
+ Support IAM Authentication for managing external K8s clusters (issue #482)
|
||||
+ Compatibility with cert manager (issue #617)
|
||||
* Enable TLS for repo server (issue #553)
|
||||
* Split out dex into it's own deployment (instead of sidecar) (issue #555)
|
||||
+ [UI] Support selection of helm values files in App creation wizard (issue #499)
|
||||
+ [UI] Support specifying source revision in App creation wizard allow (issue #503)
|
||||
+ [UI] Improve resource diff rendering (issue #457)
|
||||
+ [UI] Indicate number of ready containers in pod (issue #539)
|
||||
+ [UI] Indicate when app is overriding parameters (issue #503)
|
||||
+ [UI] Provide a YAML view of resources (issue #396)
|
||||
+ [UI] Project Role/Token management from UI (issue #548)
|
||||
+ [UI] App creation wizard should allow specifying source revision (issue #562)
|
||||
+ [UI] Ability to modify application from UI (issue #615)
|
||||
+ [UI] indicate when operation is in progress or has failed (issue #566)
|
||||
- Fix issue where changes were not pulled when tracking a branch (issue #567)
|
||||
- Lazy enforcement of unknown cluster/namespace restricted resources (issue #599)
|
||||
- Fix controller hot loop when app source contains bad manifests (issue #568)
|
||||
- Fix issue where Argo CD fails to deploy when resources are in a K8s list format (issue #584)
|
||||
- Fix comparison failure when app contains unregistered custom resource (issue #583)
|
||||
- Fix issue where helm hooks were being deployed as part of sync (issue #605)
|
||||
- Fix race conditions in kube.GetResourcesWithLabel and DeleteResourceWithLabel (issue #587)
|
||||
- [UI] Fix issue where projects filter does not work when application got changed
|
||||
- [UI] Creating apps from directories is not obvious (issue #565)
|
||||
- Helm hooks are being deployed as resources (issue #605)
|
||||
- Disagreement in three way diff calculation (issue #597)
|
||||
- SIGSEGV in kube.GetResourcesWithLabel (issue #587)
|
||||
- Argo CD fails to deploy resources list (issue #584)
|
||||
- Branch tracking not working properly (issue #567)
|
||||
- Controller hot loop when application source has bad manifests (issue #568)
|
||||
|
||||
## v0.8.2 (2018-09-12)
|
||||
- Downgrade ksonnet from v0.12.0 to v0.11.0 due to quote unescape regression
|
||||
- Fix CLI panic when performing an initial `argocd sync/wait`
|
||||
|
||||
## v0.8.1 (2018-09-10)
|
||||
+ [UI] Support selection of helm values files in App creation wizard (issue #499)
|
||||
+ [UI] Support specifying source revision in App creation wizard allow (issue #503)
|
||||
+ [UI] Improve resource diff rendering (issue #457)
|
||||
+ [UI] Indicate number of ready containers in pod (issue #539)
|
||||
+ [UI] Indicate when app is overriding parameters (issue #503)
|
||||
+ [UI] Provide a YAML view of resources (issue #396)
|
||||
- Fix issue where changes were not pulled when tracking a branch (issue #567)
|
||||
- Fix controller hot loop when app source contains bad manifests (issue #568)
|
||||
- [UI] Fix issue where projects filter does not work when application got changed
|
||||
|
||||
## v0.8.0 (2018-09-04)
|
||||
|
||||
### Notes about upgrading from v0.7
|
||||
* The RBAC model has been improved to support explicit denies. What this means is that any previous
|
||||
RBAC policy rules, need to be rewritten to include one extra column with the effect:
|
||||
`allow` or `deny`. For example, if a rule was written like this:
|
||||
```
|
||||
p, my-org:my-team, applications, get, */*
|
||||
```
|
||||
It should be rewritten to look like this:
|
||||
```
|
||||
p, my-org:my-team, applications, get, */*, allow
|
||||
```
|
||||
|
||||
### Changes since v0.7:
|
||||
+ Support kustomize as an application source (issue #510)
|
||||
+ Introduce project tokens for automation access (issue #498)
|
||||
+ Add ability to delete a single application resource to support immutable updates (issue #262)
|
||||
+ Update RBAC model to support explicit denies (issue #497)
|
||||
+ Ability to view Kubernetes events related to application projects for auditing
|
||||
+ Add PVC healthcheck to controller (issue #501)
|
||||
+ Run all containers as an unprivileged user (issue #528)
|
||||
* Upgrade ksonnet to v0.12.0
|
||||
* Add readiness probes to API server (issue #522)
|
||||
* Use gRPC error codes instead of fmt.Errorf (#532)
|
||||
- API discovery becomes best effort when partial resource list is returned (issue #524)
|
||||
- Fix `argocd app wait` printing incorrect Sync output (issue #542)
|
||||
- Fix issue where argocd could not sync to a tag (#541)
|
||||
- Fix issue where static assets were browser cached between upgrades (issue #489)
|
||||
|
||||
## 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)
|
||||
@@ -70,7 +358,7 @@
|
||||
## v0.5.0 (2018-06-12)
|
||||
+ RBAC access control
|
||||
+ Repository/Cluster state monitoring
|
||||
+ ArgoCD settings import/export
|
||||
+ Argo CD settings import/export
|
||||
+ Application creation UI wizard
|
||||
+ argocd app manifests for printing the application manifests
|
||||
+ argocd app unset command to unset parameter overrides
|
||||
|
||||
@@ -6,6 +6,7 @@ Make sure you have following tools installed
|
||||
* [protobuf](https://developers.google.com/protocol-buffers/)
|
||||
* [ksonnet](https://github.com/ksonnet/ksonnet#install)
|
||||
* [helm](https://github.com/helm/helm/releases)
|
||||
* [kustomize](https://github.com/kubernetes-sigs/kustomize/releases)
|
||||
* [go-swagger](https://github.com/go-swagger/go-swagger/blob/master/docs/install.md)
|
||||
* [jq](https://stedolan.github.io/jq/)
|
||||
* [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/).
|
||||
@@ -38,10 +39,10 @@ NOTE: The make command can take a while, and we recommend building the specific
|
||||
* `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
|
||||
## Generating Argo CD 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
|
||||
Argo CD 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.
|
||||
|
||||
|
||||
128
Dockerfile
Normal file
128
Dockerfile
Normal file
@@ -0,0 +1,128 @@
|
||||
####################################################################################################
|
||||
# Builder image
|
||||
# Initial stage which pulls prepares build dependencies and CLI tooling we need for our final image
|
||||
# Also used as the image in CI jobs so needs all dependencies
|
||||
####################################################################################################
|
||||
FROM golang:1.11.4 as builder
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
make \
|
||||
wget \
|
||||
gcc \
|
||||
zip && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
WORKDIR /tmp
|
||||
|
||||
# Install docker
|
||||
ENV DOCKER_VERSION=18.06.0
|
||||
RUN curl -O https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}-ce.tgz && \
|
||||
tar -xzf docker-${DOCKER_VERSION}-ce.tgz && \
|
||||
mv docker/docker /usr/local/bin/docker && \
|
||||
rm -rf ./docker
|
||||
|
||||
# Install dep
|
||||
ENV DEP_VERSION=0.5.0
|
||||
RUN wget https://github.com/golang/dep/releases/download/v${DEP_VERSION}/dep-linux-amd64 -O /usr/local/bin/dep && \
|
||||
chmod +x /usr/local/bin/dep
|
||||
|
||||
# Install gometalinter
|
||||
ENV GOMETALINTER_VERSION=2.0.12
|
||||
RUN curl -sLo- https://github.com/alecthomas/gometalinter/releases/download/v${GOMETALINTER_VERSION}/gometalinter-${GOMETALINTER_VERSION}-linux-amd64.tar.gz | \
|
||||
tar -xzC "$GOPATH/bin" --exclude COPYING --exclude README.md --strip-components 1 -f- && \
|
||||
ln -s $GOPATH/bin/gometalinter $GOPATH/bin/gometalinter.v2
|
||||
|
||||
# Install packr
|
||||
ENV PACKR_VERSION=1.21.9
|
||||
RUN wget https://github.com/gobuffalo/packr/releases/download/v${PACKR_VERSION}/packr_${PACKR_VERSION}_linux_amd64.tar.gz && \
|
||||
tar -vxf packr*.tar.gz -C /tmp/ && \
|
||||
mv /tmp/packr /usr/local/bin/packr
|
||||
|
||||
# Install kubectl
|
||||
# NOTE: keep the version synced with https://storage.googleapis.com/kubernetes-release/release/stable.txt
|
||||
# Keep version at 1.12.X until https://github.com/argoproj/argo-cd/issues/1012 is resolved
|
||||
ENV KUBECTL_VERSION=1.12.4
|
||||
RUN curl -L -o /usr/local/bin/kubectl -LO https://storage.googleapis.com/kubernetes-release/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl && \
|
||||
chmod +x /usr/local/bin/kubectl
|
||||
|
||||
# Install ksonnet
|
||||
ENV KSONNET_VERSION=0.13.1
|
||||
RUN wget https://github.com/ksonnet/ksonnet/releases/download/v${KSONNET_VERSION}/ks_${KSONNET_VERSION}_linux_amd64.tar.gz && \
|
||||
tar -C /tmp/ -xf ks_${KSONNET_VERSION}_linux_amd64.tar.gz && \
|
||||
mv /tmp/ks_${KSONNET_VERSION}_linux_amd64/ks /usr/local/bin/ks
|
||||
# NOTE: we occasionally switch between tip of master ksonnet vs. official builds. Run the following
|
||||
# to use tip instead of official release:
|
||||
#RUN go get -v -u github.com/ksonnet/ksonnet && mv ${GOPATH}/bin/ksonnet /usr/local/bin/ks
|
||||
|
||||
# Install helm
|
||||
ENV HELM_VERSION=2.12.1
|
||||
RUN wget https://storage.googleapis.com/kubernetes-helm/helm-v${HELM_VERSION}-linux-amd64.tar.gz && \
|
||||
tar -C /tmp/ -xf helm-v${HELM_VERSION}-linux-amd64.tar.gz && \
|
||||
mv /tmp/linux-amd64/helm /usr/local/bin/helm
|
||||
|
||||
# Install kustomize
|
||||
ENV KUSTOMIZE_VERSION=1.0.11
|
||||
RUN curl -L -o /usr/local/bin/kustomize https://github.com/kubernetes-sigs/kustomize/releases/download/v${KUSTOMIZE_VERSION}/kustomize_${KUSTOMIZE_VERSION}_linux_amd64 && \
|
||||
chmod +x /usr/local/bin/kustomize
|
||||
|
||||
# Install AWS IAM Authenticator
|
||||
ENV AWS_IAM_AUTHENTICATOR_VERSION=0.4.0-alpha.1
|
||||
RUN curl -L -o /usr/local/bin/aws-iam-authenticator https://github.com/kubernetes-sigs/aws-iam-authenticator/releases/download/${AWS_IAM_AUTHENTICATOR_VERSION}/aws-iam-authenticator_${AWS_IAM_AUTHENTICATOR_VERSION}_linux_amd64 && \
|
||||
chmod +x /usr/local/bin/aws-iam-authenticator
|
||||
|
||||
|
||||
####################################################################################################
|
||||
# Argo CD Build stage which performs the actual build of Argo CD binaries
|
||||
####################################################################################################
|
||||
FROM golang:1.11.4 as argocd-build
|
||||
|
||||
COPY --from=builder /usr/local/bin/dep /usr/local/bin/dep
|
||||
COPY --from=builder /usr/local/bin/packr /usr/local/bin/packr
|
||||
|
||||
# A dummy directory is created under $GOPATH/src/dummy so we are able to use dep
|
||||
# to install all the packages of our dep lock file
|
||||
COPY Gopkg.toml ${GOPATH}/src/dummy/Gopkg.toml
|
||||
COPY Gopkg.lock ${GOPATH}/src/dummy/Gopkg.lock
|
||||
|
||||
RUN cd ${GOPATH}/src/dummy && \
|
||||
dep ensure -vendor-only && \
|
||||
mv vendor/* ${GOPATH}/src/ && \
|
||||
rmdir vendor
|
||||
|
||||
# Perform the build
|
||||
WORKDIR /go/src/github.com/argoproj/argo-cd
|
||||
COPY . .
|
||||
RUN make cli server controller repo-server argocd-util && \
|
||||
make CLI_NAME=argocd-darwin-amd64 GOOS=darwin cli
|
||||
|
||||
|
||||
####################################################################################################
|
||||
# Final image
|
||||
####################################################################################################
|
||||
FROM debian:9.5-slim
|
||||
|
||||
RUN groupadd -g 999 argocd && \
|
||||
useradd -r -u 999 -g argocd argocd && \
|
||||
mkdir -p /home/argocd && \
|
||||
chown argocd:argocd /home/argocd && \
|
||||
apt-get update && \
|
||||
apt-get install -y git && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
COPY --from=builder /usr/local/bin/ks /usr/local/bin/ks
|
||||
COPY --from=builder /usr/local/bin/helm /usr/local/bin/helm
|
||||
COPY --from=builder /usr/local/bin/kubectl /usr/local/bin/kubectl
|
||||
COPY --from=builder /usr/local/bin/kustomize /usr/local/bin/kustomize
|
||||
COPY --from=builder /usr/local/bin/aws-iam-authenticator /usr/local/bin/aws-iam-authenticator
|
||||
|
||||
# workaround ksonnet issue https://github.com/ksonnet/ksonnet/issues/298
|
||||
ENV USER=argocd
|
||||
|
||||
COPY --from=argocd-build /go/src/github.com/argoproj/argo-cd/dist/* /usr/local/bin/
|
||||
|
||||
USER argocd
|
||||
|
||||
WORKDIR /home/argocd
|
||||
@@ -1,88 +0,0 @@
|
||||
FROM debian:9.4 as builder
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
make \
|
||||
wget \
|
||||
gcc \
|
||||
zip && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
# Install go
|
||||
ENV GO_VERSION 1.10.3
|
||||
ENV GO_ARCH amd64
|
||||
ENV GOPATH /root/go
|
||||
ENV PATH ${GOPATH}/bin:/usr/local/go/bin:${PATH}
|
||||
RUN wget https://storage.googleapis.com/golang/go${GO_VERSION}.linux-${GO_ARCH}.tar.gz && \
|
||||
tar -C /usr/local/ -xf /go${GO_VERSION}.linux-${GO_ARCH}.tar.gz && \
|
||||
rm /go${GO_VERSION}.linux-${GO_ARCH}.tar.gz
|
||||
|
||||
# Install protoc, dep, packr
|
||||
ENV PROTOBUF_VERSION 3.5.1
|
||||
RUN cd /usr/local && \
|
||||
wget https://github.com/google/protobuf/releases/download/v${PROTOBUF_VERSION}/protoc-${PROTOBUF_VERSION}-linux-x86_64.zip && \
|
||||
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.11.0/packr_1.11.0_linux_amd64.tar.gz && \
|
||||
tar -vxf packr*.tar.gz -C /tmp/ && \
|
||||
mv /tmp/packr /usr/local/bin/packr
|
||||
|
||||
# A dummy directory is created under $GOPATH/src/dummy so we are able to use dep
|
||||
# to install all the packages of our dep lock file
|
||||
COPY Gopkg.toml ${GOPATH}/src/dummy/Gopkg.toml
|
||||
COPY Gopkg.lock ${GOPATH}/src/dummy/Gopkg.lock
|
||||
|
||||
RUN cd ${GOPATH}/src/dummy && \
|
||||
dep ensure -vendor-only && \
|
||||
mv vendor/* ${GOPATH}/src/ && \
|
||||
rmdir vendor
|
||||
|
||||
# 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}
|
||||
|
||||
|
||||
##############################################################
|
||||
# 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/*
|
||||
|
||||
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
|
||||
|
||||
COPY --from=builder /root/go/src/github.com/argoproj/argo-cd/dist/* /
|
||||
ARG BINARY
|
||||
CMD /${BINARY}
|
||||
@@ -1,28 +0,0 @@
|
||||
FROM golang:1.10.3
|
||||
|
||||
WORKDIR /tmp
|
||||
|
||||
RUN curl -O https://get.docker.com/builds/Linux/x86_64/docker-1.13.1.tgz && \
|
||||
tar -xzf docker-1.13.1.tgz && \
|
||||
mv docker/docker /usr/local/bin/docker && \
|
||||
rm -rf ./docker && \
|
||||
go get -u github.com/golang/dep/cmd/dep && \
|
||||
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
|
||||
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
|
||||
920
Gopkg.lock
generated
920
Gopkg.lock
generated
File diff suppressed because it is too large
Load Diff
58
Gopkg.toml
58
Gopkg.toml
@@ -1,55 +1,67 @@
|
||||
# Packages should only be added to the following list when we use them *outside* of our go code.
|
||||
# (e.g. we want to build the binary to invoke as part of the build process, such as in
|
||||
# generate-proto.sh). Normal use of golang packages should be added via `dep ensure`, and pinned
|
||||
# with a [[constraint]] or [[override]] when version is important.
|
||||
required = [
|
||||
"github.com/golang/protobuf/protoc-gen-go",
|
||||
"github.com/gogo/protobuf/protoc-gen-gofast",
|
||||
"github.com/gogo/protobuf/protoc-gen-gogofast",
|
||||
"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",
|
||||
"golang.org/x/sync/errgroup",
|
||||
]
|
||||
|
||||
[[constraint]]
|
||||
name = "google.golang.org/grpc"
|
||||
version = "1.9.2"
|
||||
version = "1.15.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gogo/protobuf"
|
||||
version = "1.1.1"
|
||||
|
||||
# override github.com/grpc-ecosystem/go-grpc-middleware's constraint on master
|
||||
[[override]]
|
||||
name = "github.com/golang/protobuf"
|
||||
version = "1.2.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/grpc-ecosystem/grpc-gateway"
|
||||
version = "v1.3.1"
|
||||
|
||||
# override argo outdated dependency
|
||||
[[override]]
|
||||
branch = "release-1.10"
|
||||
name = "k8s.io/api"
|
||||
# prometheus does not believe in semversioning yet
|
||||
[[constraint]]
|
||||
name = "github.com/prometheus/client_golang"
|
||||
revision = "7858729281ec582767b20e0d696b6041d995d5e0"
|
||||
|
||||
[[override]]
|
||||
branch = "release-1.10"
|
||||
name = "k8s.io/apimachinery"
|
||||
[[constraint]]
|
||||
branch = "release-1.12"
|
||||
name = "k8s.io/api"
|
||||
|
||||
[[constraint]]
|
||||
name = "k8s.io/apiextensions-apiserver"
|
||||
branch = "release-1.10"
|
||||
branch = "release-1.12"
|
||||
|
||||
[[constraint]]
|
||||
branch = "release-1.10"
|
||||
branch = "release-1.12"
|
||||
name = "k8s.io/code-generator"
|
||||
|
||||
[[override]]
|
||||
branch = "release-7.0"
|
||||
[[constraint]]
|
||||
branch = "release-9.0"
|
||||
name = "k8s.io/client-go"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/stretchr/testify"
|
||||
version = "1.2.1"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/ksonnet/ksonnet"
|
||||
version = "v0.11.0"
|
||||
version = "1.2.2"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/gobuffalo/packr"
|
||||
version = "v1.11.0"
|
||||
|
||||
# override ksonnet's logrus dependency
|
||||
[[override]]
|
||||
name = "github.com/sirupsen/logrus"
|
||||
revision = "ea8897e79973357ba785ac2533559a6297e83c44"
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/argoproj/pkg"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/yudai/gojsondiff"
|
||||
|
||||
73
Makefile
73
Makefile
@@ -23,12 +23,6 @@ ifneq (${GIT_TAG},)
|
||||
IMAGE_TAG=${GIT_TAG}
|
||||
LDFLAGS += -X ${PACKAGE}.gitTag=${GIT_TAG}
|
||||
endif
|
||||
ifneq (${IMAGE_NAMESPACE},)
|
||||
override LDFLAGS += -X ${PACKAGE}/install.imageNamespace=${IMAGE_NAMESPACE}
|
||||
endif
|
||||
ifneq (${IMAGE_TAG},)
|
||||
override LDFLAGS += -X ${PACKAGE}/install.imageTag=${IMAGE_TAG}
|
||||
endif
|
||||
|
||||
ifeq (${DOCKER_PUSH},true)
|
||||
ifndef IMAGE_NAMESPACE
|
||||
@@ -41,7 +35,7 @@ IMAGE_PREFIX=${IMAGE_NAMESPACE}/
|
||||
endif
|
||||
|
||||
.PHONY: all
|
||||
all: cli server-image controller-image repo-server-image argocd-util
|
||||
all: cli image argocd-util
|
||||
|
||||
.PHONY: protogen
|
||||
protogen:
|
||||
@@ -54,70 +48,48 @@ clientgen:
|
||||
.PHONY: codegen
|
||||
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: 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/
|
||||
.PHONY: release-cli
|
||||
release-cli: clean-debug image
|
||||
docker create --name tmp-argocd-linux $(IMAGE_PREFIX)argocd:$(IMAGE_TAG)
|
||||
docker cp tmp-argocd-linux:/usr/local/bin/argocd ${DIST_DIR}/argocd-linux-amd64
|
||||
docker cp tmp-argocd-linux:/usr/local/bin/argocd-darwin-amd64 ${DIST_DIR}/argocd-darwin-amd64
|
||||
docker 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
|
||||
# Build argocd-util as a statically linked binary, so it could run within the alpine-based dex container (argoproj/argo-cd#844)
|
||||
CGO_ENABLED=0 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
|
||||
.PHONY: manifests
|
||||
manifests:
|
||||
./hack/update-manifests.sh
|
||||
|
||||
# NOTE: we use packr to do the build instead of go, since we embed swagger files and policy.csv
|
||||
# files into the go binary
|
||||
.PHONY: server
|
||||
server: clean-debug
|
||||
CGO_ENABLED=0 ${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-server ./cmd/argocd-server
|
||||
${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 -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
|
||||
repo-server:
|
||||
CGO_ENABLED=0 go build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-repo-server ./cmd/argocd-repo-server
|
||||
|
||||
.PHONY: repo-server-image
|
||||
repo-server-image:
|
||||
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
|
||||
go build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-repo-server ./cmd/argocd-repo-server
|
||||
|
||||
.PHONY: controller
|
||||
controller:
|
||||
CGO_ENABLED=0 go build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-application-controller ./cmd/argocd-application-controller
|
||||
${PACKR_CMD} build -v -i -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd-application-controller ./cmd/argocd-application-controller
|
||||
|
||||
.PHONY: controller-image
|
||||
controller-image:
|
||||
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: image
|
||||
image:
|
||||
docker build -t $(IMAGE_PREFIX)argocd:$(IMAGE_TAG) .
|
||||
@if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argocd:$(IMAGE_TAG) ; fi
|
||||
|
||||
.PHONY: builder-image
|
||||
builder-image:
|
||||
docker build -t $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG) -f Dockerfile-ci-builder .
|
||||
docker build -t $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG) --target builder .
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
@@ -125,7 +97,7 @@ lint:
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test -v `go list ./... | grep -v "github.com/argoproj/argo-cd/test/e2e"`
|
||||
go test -covermode=count -coverprofile=coverage.out `go list ./... | grep -v "github.com/argoproj/argo-cd/test/e2e"`
|
||||
|
||||
.PHONY: test-e2e
|
||||
test-e2e:
|
||||
@@ -144,9 +116,10 @@ clean: clean-debug
|
||||
precheckin: test lint
|
||||
|
||||
.PHONY: release-precheck
|
||||
release-precheck: install-manifest
|
||||
release-precheck: manifests
|
||||
@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
|
||||
@if [ "$(GIT_TAG)" != "v`cat VERSION`" ]; then echo 'VERSION does not match git tag'; exit 1; fi
|
||||
|
||||
.PHONY: release
|
||||
release: release-precheck precheckin cli-darwin cli-linux server-image controller-image repo-server-image cli-image
|
||||
release: release-precheck precheckin image release-cli
|
||||
|
||||
6
Procfile
6
Procfile
@@ -1,4 +1,4 @@
|
||||
controller: go run ./cmd/argocd-application-controller/main.go
|
||||
api-server: go run ./cmd/argocd-server/main.go --insecure --disable-auth
|
||||
controller: go run ./cmd/argocd-application-controller/main.go --repo-server localhost:8081
|
||||
api-server: go run ./cmd/argocd-server/main.go --insecure --disable-auth --dex-server http://localhost:5556 --repo-server localhost:8081 --app-controller-server localhost:8083 --staticassets ../argo-cd-ui/dist/app
|
||||
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"
|
||||
dex: sh -c "go run ./cmd/argocd-util/main.go gendexcfg -o `pwd`/dist/dex.yaml && docker run --rm -p 5556:5556 -v `pwd`/dist/dex.yaml:/dex.yaml quay.io/dexidp/dex:v2.12.0 serve /dex.yaml"
|
||||
|
||||
36
README.md
36
README.md
@@ -1,3 +1,5 @@
|
||||
[](https://argoproj.github.io/community/join-slack)
|
||||
[](https://codecov.io/gh/argoproj/argo-cd)
|
||||
|
||||
# Argo CD - Declarative Continuous Delivery for Kubernetes
|
||||
|
||||
@@ -22,14 +24,21 @@ is provided for additional features.
|
||||
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
|
||||
* [kustomize](https://kustomize.io) applications
|
||||
* [helm](https://helm.sh) charts
|
||||
* Simple directory of YAML/json manifests
|
||||
* Plain 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
|
||||
Application deployments can track updates to branches, tags, or pinned to a specific version of
|
||||
manifests at a git commit. See [tracking strategies](docs/tracking_strategies.md) for additional
|
||||
details about the different tracking strategies available.
|
||||
|
||||
For a quick 10 minute overview of Argo CD, check out the demo presented to the Sig Apps community
|
||||
meeting:
|
||||
[](https://youtu.be/aWDIQMbp1cc?t=1m4s)
|
||||
|
||||
|
||||
|
||||
## Architecture
|
||||
|
||||

|
||||
@@ -47,6 +56,7 @@ For additional details, see [architecture overview](docs/architecture.md).
|
||||
## Features
|
||||
|
||||
* Automated deployment of applications to specified target environments
|
||||
* Flexibility in support for multiple config management tools (Ksonnet, Kustomize, Helm, plain-YAML)
|
||||
* Continuous monitoring of deployed applications
|
||||
* Automated or manual syncing of applications to its desired state
|
||||
* Web and CLI based visualization of applications and differences between live vs. desired state
|
||||
@@ -57,13 +67,25 @@ For additional details, see [architecture overview](docs/architecture.md).
|
||||
* 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
|
||||
* Service account/access key management for CI pipelines
|
||||
|
||||
## Development Status
|
||||
* Argo CD is being used in production to deploy SaaS services at Intuit
|
||||
|
||||
## Roadmap
|
||||
* 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
|
||||
### v0.11
|
||||
|
||||
* New application controller architecture
|
||||
* Multi-namespaced applications
|
||||
* Large application support
|
||||
* Resource lifecycle hook improvements
|
||||
* K8s recommended application labels
|
||||
* External OIDC provider support
|
||||
* OIDC group claims bindings to Project Roles
|
||||
* Declarative Argo CD configuration
|
||||
* Helm repository support
|
||||
|
||||
### v0.12
|
||||
* UI improvements
|
||||
* Support for custom K8S manifest templating engines
|
||||
|
||||
|
||||
@@ -2,29 +2,31 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
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).
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
|
||||
|
||||
"github.com/argoproj/argo-cd"
|
||||
argocd "github.com/argoproj/argo-cd"
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"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/settings"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
"github.com/argoproj/argo-cd/util/tls"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -36,28 +38,25 @@ const (
|
||||
|
||||
func newCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
appResyncPeriod int64
|
||||
repoServerAddress string
|
||||
statusProcessors int
|
||||
operationProcessors int
|
||||
logLevel string
|
||||
glogLevel int
|
||||
clientConfig clientcmd.ClientConfig
|
||||
appResyncPeriod int64
|
||||
repoServerAddress string
|
||||
statusProcessors int
|
||||
operationProcessors int
|
||||
logLevel string
|
||||
glogLevel int
|
||||
tlsConfigCustomizerSrc func() (tls.ConfigCustomizer, error)
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: cliName,
|
||||
Short: "application-controller is a controller to operate on applications CRD",
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
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))
|
||||
cli.SetLogLevel(logLevel)
|
||||
cli.SetGLogLevel(glogLevel)
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
config.QPS = common.K8sClientConfigQPS
|
||||
config.Burst = common.K8sClientConfigBurst
|
||||
errors.CheckError(err)
|
||||
|
||||
kubeClient := kubernetes.NewForConfigOrDie(config)
|
||||
@@ -66,37 +65,40 @@ func newCommand() *cobra.Command {
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
|
||||
// TODO (amatyushentsev): Use config map to store controller configuration
|
||||
controllerConfig := controller.ApplicationControllerConfig{
|
||||
Namespace: namespace,
|
||||
InstanceID: "",
|
||||
}
|
||||
db := db.NewDB(namespace, kubeClient)
|
||||
resyncDuration := time.Duration(appResyncPeriod) * time.Second
|
||||
repoClientset := reposerver.NewRepositoryServerClientset(repoServerAddress)
|
||||
appStateManager := controller.NewAppStateManager(db, appClient, repoClientset, namespace)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
appController := controller.NewApplicationController(
|
||||
settingsMgr := settings.NewSettingsManager(ctx, kubeClient, namespace)
|
||||
appController, err := controller.NewApplicationController(
|
||||
namespace,
|
||||
settingsMgr,
|
||||
kubeClient,
|
||||
appClient,
|
||||
repoClientset,
|
||||
db,
|
||||
appStateManager,
|
||||
resyncDuration,
|
||||
&controllerConfig)
|
||||
secretController := controller.NewSecretController(kubeClient, repoClientset, resyncDuration, namespace)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
resyncDuration)
|
||||
errors.CheckError(err)
|
||||
|
||||
log.Infof("Application Controller (version: %s) starting (namespace: %s)", argocd.GetVersion(), namespace)
|
||||
stats.RegisterStackDumper()
|
||||
stats.StartStatsTicker(10 * time.Minute)
|
||||
stats.RegisterHeapDumper("memprofile")
|
||||
|
||||
go secretController.Run(ctx)
|
||||
go appController.Run(ctx, statusProcessors, operationProcessors)
|
||||
go func() {
|
||||
tlsConfigCustomizer, err := tlsConfigCustomizerSrc()
|
||||
errors.CheckError(err)
|
||||
server, err := appController.CreateGRPC(tlsConfigCustomizer)
|
||||
errors.CheckError(err)
|
||||
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", 8083))
|
||||
errors.CheckError(err)
|
||||
log.Infof("application-controller %s serving on %s", argocd.GetVersion(), listener.Addr())
|
||||
|
||||
err = server.Serve(listener)
|
||||
errors.CheckError(err)
|
||||
}()
|
||||
// Wait forever
|
||||
select {}
|
||||
},
|
||||
@@ -104,11 +106,12 @@ 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().StringVar(&repoServerAddress, "repo-server", common.DefaultRepoServerAddr, "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")
|
||||
tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(&command)
|
||||
return &command
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
@@ -14,9 +15,11 @@ import (
|
||||
"github.com/argoproj/argo-cd/reposerver"
|
||||
"github.com/argoproj/argo-cd/reposerver/repository"
|
||||
"github.com/argoproj/argo-cd/util/cache"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/git"
|
||||
"github.com/argoproj/argo-cd/util/ksonnet"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
"github.com/argoproj/argo-cd/util/tls"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -27,17 +30,24 @@ const (
|
||||
|
||||
func newCommand() *cobra.Command {
|
||||
var (
|
||||
logLevel string
|
||||
logLevel string
|
||||
redisAddress string
|
||||
sentinelAddresses []string
|
||||
sentinelMaster string
|
||||
parallelismLimit int64
|
||||
tlsConfigCustomizerSrc func() (tls.ConfigCustomizer, error)
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: cliName,
|
||||
Short: "Run argocd-repo-server",
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
level, err := log.ParseLevel(logLevel)
|
||||
errors.CheckError(err)
|
||||
log.SetLevel(level)
|
||||
cli.SetLogLevel(logLevel)
|
||||
|
||||
server := reposerver.NewServer(git.NewFactory(), newCache())
|
||||
tlsConfigCustomizer, err := tlsConfigCustomizerSrc()
|
||||
errors.CheckError(err)
|
||||
|
||||
server, err := reposerver.NewServer(git.NewFactory(), newCache(redisAddress, sentinelAddresses, sentinelMaster), tlsConfigCustomizer, parallelismLimit)
|
||||
errors.CheckError(err)
|
||||
grpc := server.CreateGRPC()
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
errors.CheckError(err)
|
||||
@@ -57,17 +67,30 @@ func newCommand() *cobra.Command {
|
||||
}
|
||||
|
||||
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
|
||||
command.Flags().StringVar(&redisAddress, "redis", "", "Redis server hostname and port (e.g. argocd-redis:6379). ")
|
||||
command.Flags().StringArrayVar(&sentinelAddresses, "sentinel", []string{}, "Redis sentinel hostname and port (e.g. argocd-redis-ha-announce-0:6379). ")
|
||||
command.Flags().StringVar(&sentinelMaster, "sentinelmaster", "master", "Redis sentinel master group name.")
|
||||
command.Flags().Int64Var(¶llelismLimit, "parallelismlimit", 0, "Limit on number of concurrent manifests generate requests. Any value less the 1 means no limit.")
|
||||
tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(&command)
|
||||
return &command
|
||||
}
|
||||
|
||||
func newCache() cache.Cache {
|
||||
func newCache(redisAddress string, sentinelAddresses []string, sentinelMaster string) cache.Cache {
|
||||
if redisAddress != "" {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: redisAddress,
|
||||
Password: "",
|
||||
DB: 0,
|
||||
})
|
||||
return cache.NewRedisCache(client, repository.DefaultRepoCacheExpiration)
|
||||
} else if len(sentinelAddresses) > 0 {
|
||||
client := redis.NewFailoverClient(&redis.FailoverOptions{
|
||||
MasterName: sentinelMaster,
|
||||
SentinelAddrs: sentinelAddresses,
|
||||
})
|
||||
return cache.NewRedisCache(client, repository.DefaultRepoCacheExpiration)
|
||||
}
|
||||
return cache.NewInMemoryCache(repository.DefaultRepoCacheExpiration)
|
||||
// client := redis.NewClient(&redis.Options{
|
||||
// Addr: "localhost:6379",
|
||||
// Password: "",
|
||||
// DB: 0,
|
||||
// })
|
||||
// return cache.NewRedisCache(client, repository.DefaultRepoCacheExpiration)
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -2,66 +2,74 @@ 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/common"
|
||||
"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/server"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/stats"
|
||||
"github.com/argoproj/argo-cd/util/tls"
|
||||
)
|
||||
|
||||
// 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
|
||||
disableAuth bool
|
||||
insecure bool
|
||||
logLevel string
|
||||
glogLevel int
|
||||
clientConfig clientcmd.ClientConfig
|
||||
staticAssetsDir string
|
||||
baseHRef string
|
||||
repoServerAddress string
|
||||
appControllerServerAddress string
|
||||
dexServerAddress string
|
||||
disableAuth bool
|
||||
tlsConfigCustomizerSrc func() (tls.ConfigCustomizer, error)
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: cliName,
|
||||
Short: "Run the argocd API server",
|
||||
Long: "Run the argocd API server",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
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))
|
||||
cli.SetLogLevel(logLevel)
|
||||
cli.SetGLogLevel(glogLevel)
|
||||
|
||||
config, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
config.QPS = common.K8sClientConfigQPS
|
||||
config.Burst = common.K8sClientConfigBurst
|
||||
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
|
||||
tlsConfigCustomizer, err := tlsConfigCustomizerSrc()
|
||||
errors.CheckError(err)
|
||||
|
||||
kubeclientset := kubernetes.NewForConfigOrDie(config)
|
||||
appclientset := appclientset.NewForConfigOrDie(config)
|
||||
repoclientset := reposerver.NewRepositoryServerClientset(repoServerAddress)
|
||||
appcontrollerclientset := controller.NewAppControllerClientset(appControllerServerAddress)
|
||||
|
||||
argoCDOpts := server.ArgoCDServerOpts{
|
||||
Insecure: insecure,
|
||||
Namespace: namespace,
|
||||
StaticAssetsDir: staticAssetsDir,
|
||||
KubeClientset: kubeclientset,
|
||||
AppClientset: appclientset,
|
||||
RepoClientset: repoclientset,
|
||||
DisableAuth: disableAuth,
|
||||
Insecure: insecure,
|
||||
Namespace: namespace,
|
||||
StaticAssetsDir: staticAssetsDir,
|
||||
BaseHRef: baseHRef,
|
||||
KubeClientset: kubeclientset,
|
||||
AppClientset: appclientset,
|
||||
RepoClientset: repoclientset,
|
||||
DexServerAddr: dexServerAddress,
|
||||
DisableAuth: disableAuth,
|
||||
TLSConfigCustomizer: tlsConfigCustomizer,
|
||||
AppControllerClientset: appcontrollerclientset,
|
||||
}
|
||||
|
||||
stats.RegisterStackDumper()
|
||||
@@ -69,9 +77,9 @@ func NewCommand() *cobra.Command {
|
||||
stats.RegisterHeapDumper("memprofile")
|
||||
|
||||
for {
|
||||
argocd := server.NewServer(argoCDOpts)
|
||||
ctx := context.Background()
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
argocd := server.NewServer(ctx, argoCDOpts)
|
||||
argocd.Run(ctx, 8080)
|
||||
cancel()
|
||||
}
|
||||
@@ -81,10 +89,14 @@ func NewCommand() *cobra.Command {
|
||||
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(&baseHRef, "basehref", "/", "Value for base href in index.html. Used if Argo CD is running behind reverse proxy under subpath different from /")
|
||||
command.Flags().StringVar(&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(&repoServerAddress, "repo-server", common.DefaultRepoServerAddr, "Repo server address")
|
||||
command.Flags().StringVar(&appControllerServerAddress, "app-controller-server", common.DefaultAppControllerServerAddr, "App controller server address")
|
||||
command.Flags().StringVar(&dexServerAddress, "dex-server", common.DefaultDexServerAddr, "Dex server address")
|
||||
command.Flags().BoolVar(&disableAuth, "disable-auth", false, "Disable client authentication")
|
||||
command.AddCommand(cli.NewVersionCmd(cliName))
|
||||
tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(command)
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/dex"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
"github.com/ghodss/yaml"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -47,7 +48,7 @@ func NewCommand() *cobra.Command {
|
||||
|
||||
var command = &cobra.Command{
|
||||
Use: cliName,
|
||||
Short: "argocd-util has internal tools used by ArgoCD",
|
||||
Short: "argocd-util has internal tools used by Argo CD",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
},
|
||||
@@ -58,7 +59,7 @@ func NewCommand() *cobra.Command {
|
||||
command.AddCommand(NewGenDexConfigCommand())
|
||||
command.AddCommand(NewImportCommand())
|
||||
command.AddCommand(NewExportCommand())
|
||||
command.AddCommand(NewSettingsCommand())
|
||||
command.AddCommand(NewClusterConfig())
|
||||
|
||||
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
|
||||
return command
|
||||
@@ -70,7 +71,7 @@ func NewRunDexCommand() *cobra.Command {
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: "rundex",
|
||||
Short: "Runs dex generating a config using settings from the ArgoCD configmap and secret",
|
||||
Short: "Runs dex generating a config using settings from the Argo CD configmap and secret",
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
_, err := exec.LookPath("dex")
|
||||
errors.CheckError(err)
|
||||
@@ -79,17 +80,15 @@ func NewRunDexCommand() *cobra.Command {
|
||||
namespace, _, err := clientConfig.Namespace()
|
||||
errors.CheckError(err)
|
||||
kubeClientset := kubernetes.NewForConfigOrDie(config)
|
||||
settingsMgr := settings.NewSettingsManager(kubeClientset, namespace)
|
||||
settings, err := settingsMgr.GetSettings()
|
||||
settingsMgr := settings.NewSettingsManager(context.Background(), kubeClientset, namespace)
|
||||
prevSettings, err := settingsMgr.GetSettings()
|
||||
errors.CheckError(err)
|
||||
ctx := context.Background()
|
||||
settingsMgr.StartNotifier(ctx, settings)
|
||||
updateCh := make(chan struct{}, 1)
|
||||
updateCh := make(chan *settings.ArgoCDSettings, 1)
|
||||
settingsMgr.Subscribe(updateCh)
|
||||
|
||||
for {
|
||||
var cmd *exec.Cmd
|
||||
dexCfgBytes, err := dex.GenerateDexConfigYAML(settings)
|
||||
dexCfgBytes, err := dex.GenerateDexConfigYAML(prevSettings)
|
||||
errors.CheckError(err)
|
||||
if len(dexCfgBytes) == 0 {
|
||||
log.Infof("dex is not configured")
|
||||
@@ -106,10 +105,11 @@ func NewRunDexCommand() *cobra.Command {
|
||||
|
||||
// loop until the dex config changes
|
||||
for {
|
||||
<-updateCh
|
||||
newDexCfgBytes, err := dex.GenerateDexConfigYAML(settings)
|
||||
newSettings := <-updateCh
|
||||
newDexCfgBytes, err := dex.GenerateDexConfigYAML(newSettings)
|
||||
errors.CheckError(err)
|
||||
if string(newDexCfgBytes) != string(dexCfgBytes) {
|
||||
prevSettings = newSettings
|
||||
log.Infof("dex config modified. restarting dex")
|
||||
if cmd != nil && cmd.Process != nil {
|
||||
err = cmd.Process.Signal(syscall.SIGTERM)
|
||||
@@ -137,14 +137,14 @@ func NewGenDexConfigCommand() *cobra.Command {
|
||||
)
|
||||
var command = cobra.Command{
|
||||
Use: "gendexcfg",
|
||||
Short: "Generates a dex config from ArgoCD settings",
|
||||
Short: "Generates a dex config from Argo CD 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)
|
||||
settingsMgr := settings.NewSettingsManager(context.Background(), kubeClientset, namespace)
|
||||
settings, err := settingsMgr.GetSettings()
|
||||
errors.CheckError(err)
|
||||
dexCfgBytes, err := dex.GenerateDexConfigYAML(settings)
|
||||
@@ -222,10 +222,10 @@ func NewImportCommand() *cobra.Command {
|
||||
errors.CheckError(err)
|
||||
kubeClientset := kubernetes.NewForConfigOrDie(config)
|
||||
|
||||
settingsMgr := settings.NewSettingsManager(kubeClientset, namespace)
|
||||
settingsMgr := settings.NewSettingsManager(context.Background(), kubeClientset, namespace)
|
||||
err = settingsMgr.SaveSettings(newSettings)
|
||||
errors.CheckError(err)
|
||||
db := db.NewDB(namespace, kubeClientset)
|
||||
db := db.NewDB(namespace, settingsMgr, kubeClientset)
|
||||
|
||||
_, err = kubeClientset.CoreV1().ConfigMaps(namespace).Create(newRBACCM)
|
||||
errors.CheckError(err)
|
||||
@@ -276,18 +276,24 @@ func NewExportCommand() *cobra.Command {
|
||||
errors.CheckError(err)
|
||||
kubeClientset := kubernetes.NewForConfigOrDie(config)
|
||||
|
||||
settingsMgr := settings.NewSettingsManager(kubeClientset, namespace)
|
||||
settingsMgr := settings.NewSettingsManager(context.Background(), kubeClientset, namespace)
|
||||
settings, err := settingsMgr.GetSettings()
|
||||
errors.CheckError(err)
|
||||
// certificate data is included in secrets that are exported alongside
|
||||
settings.Certificate = nil
|
||||
|
||||
db := db.NewDB(namespace, kubeClientset)
|
||||
db := db.NewDB(namespace, settingsMgr, kubeClientset)
|
||||
clusters, err := db.ListClusters(context.Background())
|
||||
errors.CheckError(err)
|
||||
|
||||
repos, err := db.ListRepositories(context.Background())
|
||||
repoURLs, err := db.ListRepoURLs(context.Background())
|
||||
errors.CheckError(err)
|
||||
repos := make([]*v1alpha1.Repository, len(repoURLs))
|
||||
for i := range repoURLs {
|
||||
repo, err := db.GetRepository(context.Background(), repoURLs[i])
|
||||
errors.CheckError(err)
|
||||
repos = append(repos, repo)
|
||||
}
|
||||
|
||||
appClientset := appclientset.NewForConfigOrDie(config)
|
||||
apps, err := appClientset.ArgoprojV1alpha1().Applications(namespace).List(metav1.ListOptions{})
|
||||
@@ -323,7 +329,7 @@ func NewExportCommand() *cobra.Command {
|
||||
out = append(out, string(data))
|
||||
}
|
||||
return strings.Join(out, delimiter)
|
||||
}(yamlSeparator, settings, repos.Items, clusters.Items, apps.Items, rbacCM)
|
||||
}(yamlSeparator, settings, clusters.Items, repos, apps.Items, rbacCM)
|
||||
|
||||
if out == "-" {
|
||||
fmt.Println(output)
|
||||
@@ -341,19 +347,21 @@ func NewExportCommand() *cobra.Command {
|
||||
return &command
|
||||
}
|
||||
|
||||
// NewSettingsCommand returns a new instance of `argocd-util settings` command
|
||||
func NewSettingsCommand() *cobra.Command {
|
||||
// NewClusterConfig returns a new instance of `argocd-util cluster-kubeconfig` command
|
||||
func NewClusterConfig() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
updateSuperuser bool
|
||||
superuserPassword string
|
||||
updateSignature bool
|
||||
clientConfig clientcmd.ClientConfig
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "settings",
|
||||
Short: "Creates or updates ArgoCD settings",
|
||||
Long: "Creates or updates ArgoCD settings",
|
||||
Use: "cluster-kubeconfig CLUSTER_URL OUTPUT_PATH",
|
||||
Short: "Generates kubeconfig for the specified cluster",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
serverUrl := args[0]
|
||||
output := args[1]
|
||||
conf, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
namespace, wasSpecified, err := clientConfig.Namespace()
|
||||
@@ -364,14 +372,13 @@ func NewSettingsCommand() *cobra.Command {
|
||||
|
||||
kubeclientset, err := kubernetes.NewForConfig(conf)
|
||||
errors.CheckError(err)
|
||||
settingsMgr := settings.NewSettingsManager(kubeclientset, namespace)
|
||||
|
||||
_ = settings.UpdateSettings(superuserPassword, settingsMgr, updateSignature, updateSuperuser, namespace)
|
||||
cluster, err := db.NewDB(namespace, settings.NewSettingsManager(context.Background(), kubeclientset, namespace), kubeclientset).GetCluster(context.Background(), serverUrl)
|
||||
errors.CheckError(err)
|
||||
err = kube.WriteKubeConfig(cluster.RESTConfig(), namespace, output)
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ import (
|
||||
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/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
@@ -50,7 +51,9 @@ func NewAccountUpdatePasswordCommand(clientOpts *argocdclient.ClientOptions) *co
|
||||
fmt.Print("\n")
|
||||
}
|
||||
if newPassword == "" {
|
||||
newPassword = settings.ReadAndConfirmPassword()
|
||||
var err error
|
||||
newPassword, err = cli.ReadAndConfirmPassword()
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
updatePasswordRequest := account.UpdatePasswordRequest{
|
||||
@@ -58,11 +61,30 @@ func NewAccountUpdatePasswordCommand(clientOpts *argocdclient.ClientOptions) *co
|
||||
CurrentPassword: currentPassword,
|
||||
}
|
||||
|
||||
conn, usrIf := argocdclient.NewClientOrDie(clientOpts).NewAccountClientOrDie()
|
||||
acdClient := argocdclient.NewClientOrDie(clientOpts)
|
||||
conn, usrIf := acdClient.NewAccountClientOrDie()
|
||||
defer util.Close(conn)
|
||||
_, err := usrIf.UpdatePassword(context.Background(), &updatePasswordRequest)
|
||||
|
||||
ctx := context.Background()
|
||||
_, err := usrIf.UpdatePassword(ctx, &updatePasswordRequest)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Password updated\n")
|
||||
|
||||
// Get a new JWT token after updating the password
|
||||
localCfg, err := localconfig.ReadLocalConfig(clientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
configCtx, err := localCfg.ResolveContext(clientOpts.Context)
|
||||
errors.CheckError(err)
|
||||
claims, err := configCtx.User.Claims()
|
||||
errors.CheckError(err)
|
||||
tokenString := passwordLogin(acdClient, claims.Subject, newPassword)
|
||||
localCfg.UpsertUser(localconfig.User{
|
||||
Name: localCfg.CurrentContext,
|
||||
AuthToken: tokenString,
|
||||
})
|
||||
err = localconfig.WriteLocalConfig(*localCfg, clientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Context '%s' updated\n", localCfg.CurrentContext)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/ghodss/yaml"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
)
|
||||
@@ -43,8 +44,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
|
||||
inCluster bool
|
||||
upsert bool
|
||||
awsRoleArn string
|
||||
awsClusterName string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "add",
|
||||
@@ -62,6 +65,7 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
if clstContext == nil {
|
||||
log.Fatalf("Context %s does not exist in kubeconfig", args[0])
|
||||
}
|
||||
|
||||
overrides := clientcmd.ConfigOverrides{
|
||||
Context: *clstContext,
|
||||
}
|
||||
@@ -69,12 +73,23 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
conf, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
|
||||
// Install RBAC resources for managing the cluster
|
||||
managerBearerToken := common.InstallClusterManagerRBAC(conf)
|
||||
|
||||
managerBearerToken := ""
|
||||
var awsAuthConf *argoappv1.AWSAuthConfig
|
||||
if awsClusterName != "" {
|
||||
awsAuthConf = &argoappv1.AWSAuthConfig{
|
||||
ClusterName: awsClusterName,
|
||||
RoleARN: awsRoleArn,
|
||||
}
|
||||
} else {
|
||||
// Install RBAC resources for managing the cluster
|
||||
clientset, err := kubernetes.NewForConfig(conf)
|
||||
errors.CheckError(err)
|
||||
managerBearerToken, err = common.InstallClusterManagerRBAC(clientset)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
|
||||
defer util.Close(conn)
|
||||
clst := NewCluster(args[0], conf, managerBearerToken)
|
||||
clst := NewCluster(args[0], conf, managerBearerToken, awsAuthConf)
|
||||
if inCluster {
|
||||
clst.Server = common.KubernetesInternalAPIServerAddr
|
||||
}
|
||||
@@ -88,8 +103,10 @@ func NewClusterAddCommand(clientOpts *argocdclient.ClientOptions, pathOpts *clie
|
||||
},
|
||||
}
|
||||
command.PersistentFlags().StringVar(&pathOpts.LoadingRules.ExplicitPath, pathOpts.ExplicitFileFlag, pathOpts.LoadingRules.ExplicitPath, "use a particular kubeconfig file")
|
||||
command.Flags().BoolVar(&inCluster, "in-cluster", false, "Indicates ArgoCD resides inside this cluster and should connect using the internal k8s hostname (kubernetes.default.svc)")
|
||||
command.Flags().BoolVar(&inCluster, "in-cluster", false, "Indicates Argo CD resides inside this cluster and should connect using the internal k8s hostname (kubernetes.default.svc)")
|
||||
command.Flags().BoolVar(&upsert, "upsert", false, "Override an existing cluster with the same name even if the spec differs")
|
||||
command.Flags().StringVar(&awsClusterName, "aws-cluster-name", "", "AWS Cluster name if set then aws-iam-authenticator will be used to access cluster")
|
||||
command.Flags().StringVar(&awsRoleArn, "aws-role-arn", "", "Optional AWS role arn. If set then AWS IAM Authenticator assume a role to perform cluster operations instead of the default AWS credential provider chain.")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -132,7 +149,7 @@ func printKubeContexts(ca clientcmd.ConfigAccess) {
|
||||
}
|
||||
}
|
||||
|
||||
func NewCluster(name string, conf *rest.Config, managerBearerToken string) *argoappv1.Cluster {
|
||||
func NewCluster(name string, conf *rest.Config, managerBearerToken string, awsAuthConf *argoappv1.AWSAuthConfig) *argoappv1.Cluster {
|
||||
tlsClientConfig := argoappv1.TLSClientConfig{
|
||||
Insecure: conf.TLSClientConfig.Insecure,
|
||||
ServerName: conf.TLSClientConfig.ServerName,
|
||||
@@ -161,6 +178,7 @@ func NewCluster(name string, conf *rest.Config, managerBearerToken string) *argo
|
||||
Config: argoappv1.ClusterConfig{
|
||||
BearerToken: managerBearerToken,
|
||||
TLSClientConfig: tlsClientConfig,
|
||||
AWSAuthConfig: awsAuthConf,
|
||||
},
|
||||
}
|
||||
return &clst
|
||||
@@ -202,9 +220,14 @@ func NewClusterRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
}
|
||||
conn, clusterIf := argocdclient.NewClientOrDie(clientOpts).NewClusterClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
// clientset, err := kubernetes.NewForConfig(conf)
|
||||
// errors.CheckError(err)
|
||||
|
||||
for _, clusterName := range args {
|
||||
// TODO(jessesuen): find the right context and remove manager RBAC artifacts
|
||||
// common.UninstallClusterManagerRBAC(conf)
|
||||
// err := common.UninstallClusterManagerRBAC(clientset)
|
||||
// errors.CheckError(err)
|
||||
_, err := clusterIf.Delete(context.Background(), &cluster.ClusterQuery{Server: clusterName})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
@@ -2,4 +2,8 @@ package commands
|
||||
|
||||
const (
|
||||
cliName = "argocd"
|
||||
|
||||
// DefaultSSOLocalPort is the localhost port to listen on for the temporary web server performing
|
||||
// the OAuth2 login flow.
|
||||
DefaultSSOLocalPort = 8085
|
||||
)
|
||||
|
||||
@@ -2,15 +2,19 @@ package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/server/session"
|
||||
@@ -19,11 +23,8 @@ import (
|
||||
"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"
|
||||
oidcutil "github.com/argoproj/argo-cd/util/oidc"
|
||||
"github.com/argoproj/argo-cd/util/rand"
|
||||
)
|
||||
|
||||
// NewLoginCommand returns a new instance of `argocd login` command
|
||||
@@ -33,6 +34,7 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
username string
|
||||
password string
|
||||
sso bool
|
||||
ssoPort int
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "login SERVER",
|
||||
@@ -81,12 +83,15 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
if !sso {
|
||||
tokenString = passwordLogin(acdClient, username, password)
|
||||
} else {
|
||||
acdSet, err := setIf.Get(context.Background(), &settings.SettingsQuery{})
|
||||
ctx := context.Background()
|
||||
httpClient, err := acdClient.HTTPClient()
|
||||
errors.CheckError(err)
|
||||
if !ssoConfigured(acdSet) {
|
||||
log.Fatalf("ArgoCD instance is not configured with SSO")
|
||||
}
|
||||
tokenString, refreshToken = oauth2Login(server, clientOpts.PlainText)
|
||||
ctx = oidc.ClientContext(ctx, httpClient)
|
||||
acdSet, err := setIf.Get(ctx, &settings.SettingsQuery{})
|
||||
errors.CheckError(err)
|
||||
oauth2conf, provider, err := acdClient.OIDCConfig(ctx, acdSet)
|
||||
errors.CheckError(err)
|
||||
tokenString, refreshToken = oauth2Login(ctx, ssoPort, oauth2conf, provider)
|
||||
}
|
||||
|
||||
parser := &jwt.Parser{
|
||||
@@ -130,7 +135,8 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
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")
|
||||
command.Flags().BoolVar(&sso, "sso", false, "perform SSO login")
|
||||
command.Flags().IntVar(&ssoPort, "sso-port", DefaultSSOLocalPort, "port to run local OAuth2 login application")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -144,97 +150,107 @@ func userDisplayName(claims jwt.MapClaims) 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()
|
||||
func oauth2Login(ctx context.Context, port int, oauth2conf *oauth2.Config, provider *oidc.Provider) (string, string) {
|
||||
oauth2conf.RedirectURL = fmt.Sprintf("http://localhost:%d/auth/callback", port)
|
||||
oidcConf, err := oidcutil.ParseConfig(provider)
|
||||
errors.CheckError(err)
|
||||
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)}
|
||||
log.Debug("OIDC Configuration:")
|
||||
log.Debugf(" supported_scopes: %v", oidcConf.ScopesSupported)
|
||||
log.Debugf(" response_types_supported: %v", oidcConf.ResponseTypesSupported)
|
||||
|
||||
// handledRequests ensures we do not handle more requests than necessary
|
||||
handledRequests := 0
|
||||
// completionChan is to signal flow completed. Non-empty string indicates error
|
||||
completionChan := make(chan string)
|
||||
// stateNonce is an OAuth2 state nonce
|
||||
stateNonce := rand.RandString(10)
|
||||
var tokenString string
|
||||
var refreshToken string
|
||||
loginCompleted := make(chan struct{})
|
||||
|
||||
handleErr := func(w http.ResponseWriter, errMsg string) {
|
||||
http.Error(w, errMsg, http.StatusBadRequest)
|
||||
completionChan <- errMsg
|
||||
}
|
||||
|
||||
// Authorization redirect callback from OAuth2 auth flow.
|
||||
// Handles both implicit and authorization code flow
|
||||
callbackHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
loginCompleted <- struct{}{}
|
||||
}()
|
||||
log.Debugf("Callback: %s", r.URL)
|
||||
|
||||
// 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)
|
||||
if formErr := r.FormValue("error"); formErr != "" {
|
||||
handleErr(w, formErr+": "+r.FormValue("error_description"))
|
||||
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)
|
||||
handledRequests++
|
||||
if handledRequests > 2 {
|
||||
// Since implicit flow will redirect back to ourselves, this counter ensures we do not
|
||||
// fallinto a redirect loop (e.g. user visits the page by hand)
|
||||
handleErr(w, "Unable to complete login flow: too many redirects")
|
||||
return
|
||||
}
|
||||
refreshToken, _ = tok.Extra("refresh_token").(string)
|
||||
log.Debugf("Token: %s", tokenString)
|
||||
log.Debugf("Refresh Token: %s", tokenString)
|
||||
|
||||
if len(r.Form) == 0 {
|
||||
// If we get here, no form data was set. We presume to be performing an implicit login
|
||||
// flow where the id_token is contained in a URL fragment, making it inaccessible to be
|
||||
// read from the request. This javascript will redirect the browser to send the
|
||||
// fragments as query parameters so our callback handler can read and return token.
|
||||
fmt.Fprintf(w, `<script>window.location.search = window.location.hash.substring(1)</script>`)
|
||||
return
|
||||
}
|
||||
|
||||
if state := r.FormValue("state"); state != stateNonce {
|
||||
handleErr(w, "Unknown state nonce")
|
||||
return
|
||||
}
|
||||
|
||||
tokenString = r.FormValue("id_token")
|
||||
if tokenString == "" {
|
||||
code := r.FormValue("code")
|
||||
if code == "" {
|
||||
handleErr(w, fmt.Sprintf("no code in request: %q", r.Form))
|
||||
return
|
||||
}
|
||||
tok, err := oauth2conf.Exchange(ctx, code)
|
||||
if err != nil {
|
||||
handleErr(w, err.Error())
|
||||
return
|
||||
}
|
||||
var ok bool
|
||||
tokenString, ok = tok.Extra("id_token").(string)
|
||||
if !ok {
|
||||
handleErr(w, "no id_token in token response")
|
||||
return
|
||||
}
|
||||
refreshToken, _ = tok.Extra("refresh_token").(string)
|
||||
}
|
||||
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)
|
||||
completionChan <- ""
|
||||
}
|
||||
srv := &http.Server{Addr: "localhost:" + strconv.Itoa(port)}
|
||||
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)
|
||||
fmt.Printf("Opening browser for authentication\n")
|
||||
|
||||
var url string
|
||||
grantType := oidcutil.InferGrantType(oauth2conf, oidcConf)
|
||||
switch grantType {
|
||||
case oidcutil.GrantTypeAuthorizationCode:
|
||||
url = oauth2conf.AuthCodeURL(stateNonce, oauth2.AccessTypeOffline)
|
||||
case oidcutil.GrantTypeImplicit:
|
||||
url = oidcutil.ImplicitFlowURL(oauth2conf, stateNonce, oauth2.AccessTypeOffline)
|
||||
default:
|
||||
log.Fatalf("Unsupported grant type: %v", grantType)
|
||||
}
|
||||
fmt.Printf("Performing %s flow login: %s\n", grantType, url)
|
||||
time.Sleep(1 * time.Second)
|
||||
err = open.Run(url)
|
||||
errors.CheckError(err)
|
||||
@@ -243,8 +259,16 @@ func oauth2Login(host string, plaintext bool) (string, string) {
|
||||
log.Fatalf("listen: %s\n", err)
|
||||
}
|
||||
}()
|
||||
<-loginCompleted
|
||||
errMsg := <-completionChan
|
||||
if errMsg != "" {
|
||||
log.Fatal(errMsg)
|
||||
}
|
||||
fmt.Printf("Authentication successful\n")
|
||||
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
|
||||
defer cancel()
|
||||
_ = srv.Shutdown(ctx)
|
||||
log.Debugf("Token: %s", tokenString)
|
||||
log.Debugf("Refresh Token: %s", refreshToken)
|
||||
return tokenString, refreshToken
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"strings"
|
||||
|
||||
"context"
|
||||
|
||||
"fmt"
|
||||
"text/tabwriter"
|
||||
"github.com/spf13/pflag"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
@@ -19,8 +21,6 @@ import (
|
||||
"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 {
|
||||
@@ -29,6 +29,12 @@ type projectOpts struct {
|
||||
sources []string
|
||||
}
|
||||
|
||||
type policyOpts struct {
|
||||
action string
|
||||
permission string
|
||||
object string
|
||||
}
|
||||
|
||||
func (opts *projectOpts) GetDestinations() []v1alpha1.ApplicationDestination {
|
||||
destinations := make([]v1alpha1.ApplicationDestination, 0)
|
||||
for _, destStr := range opts.destinations {
|
||||
@@ -55,7 +61,9 @@ func NewProjectCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
os.Exit(1)
|
||||
},
|
||||
}
|
||||
command.AddCommand(NewProjectRoleCommand(clientOpts))
|
||||
command.AddCommand(NewProjectCreateCommand(clientOpts))
|
||||
command.AddCommand(NewProjectGetCommand(clientOpts))
|
||||
command.AddCommand(NewProjectDeleteCommand(clientOpts))
|
||||
command.AddCommand(NewProjectListCommand(clientOpts))
|
||||
command.AddCommand(NewProjectSetCommand(clientOpts))
|
||||
@@ -63,14 +71,29 @@ func NewProjectCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
command.AddCommand(NewProjectRemoveDestinationCommand(clientOpts))
|
||||
command.AddCommand(NewProjectAddSourceCommand(clientOpts))
|
||||
command.AddCommand(NewProjectRemoveSourceCommand(clientOpts))
|
||||
command.AddCommand(NewProjectAllowClusterResourceCommand(clientOpts))
|
||||
command.AddCommand(NewProjectDenyClusterResourceCommand(clientOpts))
|
||||
command.AddCommand(NewProjectAllowNamespaceResourceCommand(clientOpts))
|
||||
command.AddCommand(NewProjectDenyNamespaceResourceCommand(clientOpts))
|
||||
return command
|
||||
}
|
||||
|
||||
func addProjFlags(command *cobra.Command, opts *projectOpts) {
|
||||
command.Flags().StringVarP(&opts.description, "description", "", "desc", "Project description")
|
||||
command.Flags().StringVarP(&opts.description, "description", "", "", "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.")
|
||||
"Permitted destination server and namespace (e.g. https://192.168.99.100:8443,default)")
|
||||
command.Flags().StringArrayVarP(&opts.sources, "src", "s", []string{}, "Permitted git source repository URL")
|
||||
}
|
||||
|
||||
func addPolicyFlags(command *cobra.Command, opts *policyOpts) {
|
||||
command.Flags().StringVarP(&opts.action, "action", "a", "", "Action to grant/deny permission on (e.g. get, create, list, update, delete)")
|
||||
command.Flags().StringVarP(&opts.permission, "permission", "p", "allow", "Whether to allow or deny access to object with the action. This can only be 'allow' or 'deny'")
|
||||
command.Flags().StringVarP(&opts.object, "object", "o", "", "Object within the project to grant/deny access. Use '*' for a wildcard. Will want access to '<project>/<object>'")
|
||||
}
|
||||
|
||||
func humanizeTimestamp(epoch int64) string {
|
||||
ts := time.Unix(epoch, 0)
|
||||
return fmt.Sprintf("%s (%s)", ts.Format(time.RFC3339), humanize.Time(ts))
|
||||
}
|
||||
|
||||
// NewProjectCreateCommand returns a new instance of an `argocd proj create` command
|
||||
@@ -242,8 +265,13 @@ func NewProjectAddSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
|
||||
errors.CheckError(err)
|
||||
|
||||
for _, item := range proj.Spec.SourceRepos {
|
||||
if item == git.NormalizeGitURL(url) {
|
||||
log.Fatal("Specified source repository is already defined in project")
|
||||
if item == "*" && item == url {
|
||||
fmt.Printf("Source repository '*' already allowed in project\n")
|
||||
return
|
||||
}
|
||||
if git.SameURL(item, url) {
|
||||
fmt.Printf("Source repository '%s' already allowed in project\n", item)
|
||||
return
|
||||
}
|
||||
}
|
||||
proj.Spec.SourceRepos = append(proj.Spec.SourceRepos, url)
|
||||
@@ -254,6 +282,104 @@ func NewProjectAddSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.C
|
||||
return command
|
||||
}
|
||||
|
||||
func modifyProjectResourceCmd(cmdUse, cmdDesc string, clientOpts *argocdclient.ClientOptions, action func(proj *v1alpha1.AppProject, group string, kind string) bool) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: cmdUse,
|
||||
Short: cmdDesc,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName, group, kind := args[0], args[1], args[2]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
|
||||
if action(proj, group, kind) {
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewProjectAllowNamespaceResourceCommand returns a new instance of an `deny-cluster-resources` command
|
||||
func NewProjectAllowNamespaceResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
use := "allow-namespace-resource PROJECT GROUP KIND"
|
||||
desc := "Removes a namespaced API resource from the blacklist"
|
||||
return modifyProjectResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
|
||||
index := -1
|
||||
for i, item := range proj.Spec.NamespaceResourceBlacklist {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
fmt.Printf("Group '%s' and kind '%s' not in blacklisted namespaced resources\n", group, kind)
|
||||
return false
|
||||
}
|
||||
proj.Spec.NamespaceResourceBlacklist = append(proj.Spec.NamespaceResourceBlacklist[:index], proj.Spec.NamespaceResourceBlacklist[index+1:]...)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// NewProjectDenyNamespaceResourceCommand returns a new instance of an `argocd proj deny-namespace-resource` command
|
||||
func NewProjectDenyNamespaceResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
use := "deny-namespace-resource PROJECT GROUP KIND"
|
||||
desc := "Adds a namespaced API resource to the blacklist"
|
||||
return modifyProjectResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
|
||||
for _, item := range proj.Spec.NamespaceResourceBlacklist {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
fmt.Printf("Group '%s' and kind '%s' already present in blacklisted namespaced resources\n", group, kind)
|
||||
return false
|
||||
}
|
||||
}
|
||||
proj.Spec.NamespaceResourceBlacklist = append(proj.Spec.NamespaceResourceBlacklist, v1.GroupKind{Group: group, Kind: kind})
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// NewProjectDenyClusterResourceCommand returns a new instance of an `deny-cluster-resource` command
|
||||
func NewProjectDenyClusterResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
use := "deny-cluster-resource PROJECT GROUP KIND"
|
||||
desc := "Removes a cluster-scoped API resource from the whitelist"
|
||||
return modifyProjectResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
|
||||
index := -1
|
||||
for i, item := range proj.Spec.ClusterResourceWhitelist {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
fmt.Printf("Group '%s' and kind '%s' not in whitelisted cluster resources\n", group, kind)
|
||||
return false
|
||||
}
|
||||
proj.Spec.ClusterResourceWhitelist = append(proj.Spec.ClusterResourceWhitelist[:index], proj.Spec.ClusterResourceWhitelist[index+1:]...)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// NewProjectAllowClusterResourceCommand returns a new instance of an `argocd proj allow-cluster-resource` command
|
||||
func NewProjectAllowClusterResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
use := "allow-cluster-resource PROJECT GROUP KIND"
|
||||
desc := "Adds a cluster-scoped API resource to the whitelist"
|
||||
return modifyProjectResourceCmd(use, desc, clientOpts, func(proj *v1alpha1.AppProject, group string, kind string) bool {
|
||||
for _, item := range proj.Spec.ClusterResourceWhitelist {
|
||||
if item.Group == group && item.Kind == kind {
|
||||
fmt.Printf("Group '%s' and kind '%s' already present in whitelisted cluster resources\n", group, kind)
|
||||
return false
|
||||
}
|
||||
}
|
||||
proj.Spec.ClusterResourceWhitelist = append(proj.Spec.ClusterResourceWhitelist, v1.GroupKind{Group: group, Kind: kind})
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// NewProjectRemoveSourceCommand returns a new instance of an `argocd proj remove-src` command
|
||||
func NewProjectRemoveSourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
@@ -274,13 +400,13 @@ func NewProjectRemoveSourceCommand(clientOpts *argocdclient.ClientOptions) *cobr
|
||||
|
||||
index := -1
|
||||
for i, item := range proj.Spec.SourceRepos {
|
||||
if item == git.NormalizeGitURL(url) {
|
||||
if item == url {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index == -1 {
|
||||
log.Fatal("Specified source repository does not exist in project")
|
||||
fmt.Printf("Source repository '%s' does not exist in project\n", url)
|
||||
} else {
|
||||
proj.Spec.SourceRepos = append(proj.Spec.SourceRepos[:index], proj.Spec.SourceRepos[index+1:]...)
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
@@ -324,12 +450,110 @@ func NewProjectListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
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")
|
||||
fmt.Fprintf(w, "NAME\tDESCRIPTION\tDESTINATIONS\tSOURCES\tCLUSTER-RESOURCE-WHITELIST\tNAMESPACE-RESOURCE-BLACKLIST\n")
|
||||
for _, p := range projects.Items {
|
||||
fmt.Fprintf(w, "%s\t%s\t%v\n", p.Name, p.Spec.Description, p.Spec.Destinations)
|
||||
printProjectLine(w, &p)
|
||||
}
|
||||
_ = w.Flush()
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
func printProjectLine(w io.Writer, p *v1alpha1.AppProject) {
|
||||
var destinations, sourceRepos, clusterWhitelist, namespaceBlacklist string
|
||||
switch len(p.Spec.Destinations) {
|
||||
case 0:
|
||||
destinations = "<none>"
|
||||
case 1:
|
||||
destinations = fmt.Sprintf("%s,%s", p.Spec.Destinations[0].Server, p.Spec.Destinations[0].Namespace)
|
||||
default:
|
||||
destinations = fmt.Sprintf("%d destinations", len(p.Spec.Destinations))
|
||||
}
|
||||
switch len(p.Spec.SourceRepos) {
|
||||
case 0:
|
||||
sourceRepos = "<none>"
|
||||
case 1:
|
||||
sourceRepos = p.Spec.SourceRepos[0]
|
||||
default:
|
||||
sourceRepos = fmt.Sprintf("%d repos", len(p.Spec.SourceRepos))
|
||||
}
|
||||
switch len(p.Spec.ClusterResourceWhitelist) {
|
||||
case 0:
|
||||
clusterWhitelist = "<none>"
|
||||
case 1:
|
||||
clusterWhitelist = fmt.Sprintf("%s/%s", p.Spec.ClusterResourceWhitelist[0].Group, p.Spec.ClusterResourceWhitelist[0].Kind)
|
||||
default:
|
||||
clusterWhitelist = fmt.Sprintf("%d resources", len(p.Spec.ClusterResourceWhitelist))
|
||||
}
|
||||
switch len(p.Spec.NamespaceResourceBlacklist) {
|
||||
case 0:
|
||||
namespaceBlacklist = "<none>"
|
||||
default:
|
||||
namespaceBlacklist = fmt.Sprintf("%d resources", len(p.Spec.NamespaceResourceBlacklist))
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%v\t%v\t%v\t%v\n", p.Name, p.Spec.Description, destinations, sourceRepos, clusterWhitelist, namespaceBlacklist)
|
||||
}
|
||||
|
||||
// NewProjectGetCommand returns a new instance of an `argocd proj get` command
|
||||
func NewProjectGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
const printProjFmtStr = "%-34s%s\n"
|
||||
var command = &cobra.Command{
|
||||
Use: "get PROJECT",
|
||||
Short: "Get project details",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
p, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf(printProjFmtStr, "Name:", p.Name)
|
||||
fmt.Printf(printProjFmtStr, "Description:", p.Spec.Description)
|
||||
|
||||
// Print destinations
|
||||
dest0 := "<none>"
|
||||
if len(p.Spec.Destinations) > 0 {
|
||||
dest0 = fmt.Sprintf("%s,%s", p.Spec.Destinations[0].Server, p.Spec.Destinations[0].Namespace)
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Destinations:", dest0)
|
||||
for i := 1; i < len(p.Spec.Destinations); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s,%s", p.Spec.Destinations[i].Server, p.Spec.Destinations[i].Namespace))
|
||||
}
|
||||
|
||||
// Print sources
|
||||
src0 := "<none>"
|
||||
if len(p.Spec.SourceRepos) > 0 {
|
||||
src0 = p.Spec.SourceRepos[0]
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Repositories:", src0)
|
||||
for i := 1; i < len(p.Spec.SourceRepos); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", p.Spec.SourceRepos[i])
|
||||
}
|
||||
|
||||
// Print whitelisted cluster resources
|
||||
cwl0 := "<none>"
|
||||
if len(p.Spec.ClusterResourceWhitelist) > 0 {
|
||||
cwl0 = fmt.Sprintf("%s/%s", p.Spec.ClusterResourceWhitelist[0].Group, p.Spec.ClusterResourceWhitelist[0].Kind)
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Whitelisted Cluster Resources:", cwl0)
|
||||
for i := 1; i < len(p.Spec.ClusterResourceWhitelist); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s/%s", p.Spec.ClusterResourceWhitelist[i].Group, p.Spec.ClusterResourceWhitelist[i].Kind))
|
||||
}
|
||||
|
||||
// Print blacklisted namespaced resources
|
||||
rbl0 := "<none>"
|
||||
if len(p.Spec.NamespaceResourceBlacklist) > 0 {
|
||||
rbl0 = fmt.Sprintf("%s/%s", p.Spec.NamespaceResourceBlacklist[0].Group, p.Spec.NamespaceResourceBlacklist[0].Kind)
|
||||
}
|
||||
fmt.Printf(printProjFmtStr, "Blacklisted Namespaced Resources:", rbl0)
|
||||
for i := 1; i < len(p.Spec.NamespaceResourceBlacklist); i++ {
|
||||
fmt.Printf(printProjFmtStr, "", fmt.Sprintf("%s/%s", p.Spec.NamespaceResourceBlacklist[i].Group, p.Spec.NamespaceResourceBlacklist[i].Kind))
|
||||
}
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
377
cmd/argocd/commands/project_role.go
Normal file
377
cmd/argocd/commands/project_role.go
Normal file
@@ -0,0 +1,377 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"text/tabwriter"
|
||||
|
||||
timeutil "github.com/argoproj/pkg/time"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"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"
|
||||
projectutil "github.com/argoproj/argo-cd/util/project"
|
||||
)
|
||||
|
||||
const (
|
||||
policyTemplate = "p, proj:%s:%s, applications, %s, %s/%s, %s"
|
||||
)
|
||||
|
||||
// NewProjectRoleCommand returns a new instance of the `argocd proj role` command
|
||||
func NewProjectRoleCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
roleCommand := &cobra.Command{
|
||||
Use: "role",
|
||||
Short: "Manage a project's roles",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
},
|
||||
}
|
||||
roleCommand.AddCommand(NewProjectRoleListCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectRoleGetCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectRoleCreateCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectRoleDeleteCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectRoleCreateTokenCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectRoleDeleteTokenCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectRoleAddPolicyCommand(clientOpts))
|
||||
roleCommand.AddCommand(NewProjectRoleRemovePolicyCommand(clientOpts))
|
||||
return roleCommand
|
||||
}
|
||||
|
||||
// NewProjectRoleAddPolicyCommand returns a new instance of an `argocd proj role add-policy` command
|
||||
func NewProjectRoleAddPolicyCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
opts policyOpts
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "add-policy PROJECT ROLE-NAME",
|
||||
Short: "Add a policy to a project role",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
roleName := 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)
|
||||
|
||||
role, roleIndex, err := projectutil.GetRoleByName(proj, roleName)
|
||||
errors.CheckError(err)
|
||||
|
||||
policy := fmt.Sprintf(policyTemplate, proj.Name, role.Name, opts.action, proj.Name, opts.object, opts.permission)
|
||||
proj.Spec.Roles[roleIndex].Policies = append(role.Policies, policy)
|
||||
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
addPolicyFlags(command, &opts)
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectRoleRemovePolicyCommand returns a new instance of an `argocd proj role remove-policy` command
|
||||
func NewProjectRoleRemovePolicyCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
opts policyOpts
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "remove-policy PROJECT ROLE-NAME",
|
||||
Short: "Remove a policy from a role within a project",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
roleName := 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)
|
||||
|
||||
role, roleIndex, err := projectutil.GetRoleByName(proj, roleName)
|
||||
errors.CheckError(err)
|
||||
|
||||
policyToRemove := fmt.Sprintf(policyTemplate, proj.Name, role.Name, opts.action, proj.Name, opts.object, opts.permission)
|
||||
duplicateIndex := -1
|
||||
for i, policy := range role.Policies {
|
||||
if policy == policyToRemove {
|
||||
duplicateIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if duplicateIndex < 0 {
|
||||
return
|
||||
}
|
||||
role.Policies[duplicateIndex] = role.Policies[len(role.Policies)-1]
|
||||
proj.Spec.Roles[roleIndex].Policies = role.Policies[:len(role.Policies)-1]
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
addPolicyFlags(command, &opts)
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectRoleCreateCommand returns a new instance of an `argocd proj role create` command
|
||||
func NewProjectRoleCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
description string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "create PROJECT ROLE-NAME",
|
||||
Short: "Create a project role",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
roleName := 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)
|
||||
|
||||
_, _, err = projectutil.GetRoleByName(proj, roleName)
|
||||
if err == nil {
|
||||
fmt.Printf("Role '%s' already exists\n", roleName)
|
||||
return
|
||||
}
|
||||
proj.Spec.Roles = append(proj.Spec.Roles, v1alpha1.ProjectRole{Name: roleName, Description: description})
|
||||
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Role '%s' created\n", roleName)
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&description, "description", "", "", "Project description")
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectRoleDeleteCommand returns a new instance of an `argocd proj role delete` command
|
||||
func NewProjectRoleDeleteCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "delete PROJECT ROLE-NAME",
|
||||
Short: "Delete a project role",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
roleName := 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, err := projectutil.GetRoleByName(proj, roleName)
|
||||
if err != nil {
|
||||
fmt.Printf("Role '%s' does not exist in project\n", roleName)
|
||||
return
|
||||
}
|
||||
proj.Spec.Roles[index] = proj.Spec.Roles[len(proj.Spec.Roles)-1]
|
||||
proj.Spec.Roles = proj.Spec.Roles[:len(proj.Spec.Roles)-1]
|
||||
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Role '%s' deleted\n", roleName)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectRoleCreateTokenCommand returns a new instance of an `argocd proj role create-token` command
|
||||
func NewProjectRoleCreateTokenCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
expiresIn string
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "create-token PROJECT ROLE-NAME",
|
||||
Short: "Create a project token",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
roleName := args[1]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
duration, err := timeutil.ParseDuration(expiresIn)
|
||||
errors.CheckError(err)
|
||||
token, err := projIf.CreateToken(context.Background(), &project.ProjectTokenCreateRequest{Project: projName, Role: roleName, ExpiresIn: int64(duration.Seconds())})
|
||||
errors.CheckError(err)
|
||||
fmt.Println(token.Token)
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&expiresIn, "expires-in", "e", "0s", "Duration before the token will expire. (Default: No expiration)")
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectRoleDeleteTokenCommand returns a new instance of an `argocd proj role delete-token` command
|
||||
func NewProjectRoleDeleteTokenCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "delete-token PROJECT ROLE-NAME ISSUED-AT",
|
||||
Short: "Delete a project token",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
roleName := args[1]
|
||||
issuedAt, err := strconv.ParseInt(args[2], 10, 64)
|
||||
errors.CheckError(err)
|
||||
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
_, err = projIf.DeleteToken(context.Background(), &project.ProjectTokenDeleteRequest{Project: projName, Role: roleName, Iat: issuedAt})
|
||||
errors.CheckError(err)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectRoleListCommand returns a new instance of an `argocd proj roles list` command
|
||||
func NewProjectRoleListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "list PROJECT",
|
||||
Short: "List all the roles in a project",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
|
||||
project, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "ROLE-NAME\tDESCRIPTION\n")
|
||||
for _, role := range project.Spec.Roles {
|
||||
fmt.Fprintf(w, "%s\t%s\n", role.Name, role.Description)
|
||||
}
|
||||
_ = w.Flush()
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectRoleGetCommand returns a new instance of an `argocd proj roles get` command
|
||||
func NewProjectRoleGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "get PROJECT ROLE-NAME",
|
||||
Short: "Get the details of a specific role",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName := args[0]
|
||||
roleName := 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)
|
||||
|
||||
role, _, err := projectutil.GetRoleByName(proj, roleName)
|
||||
errors.CheckError(err)
|
||||
|
||||
printRoleFmtStr := "%-15s%s\n"
|
||||
fmt.Printf(printRoleFmtStr, "Role Name:", roleName)
|
||||
fmt.Printf(printRoleFmtStr, "Description:", role.Description)
|
||||
fmt.Printf("Policies:\n")
|
||||
fmt.Printf("%s\n", proj.ProjectPoliciesString())
|
||||
fmt.Printf("JWT Tokens:\n")
|
||||
// TODO(jessesuen): print groups
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintf(w, "ID\tISSUED-AT\tEXPIRES-AT\n")
|
||||
for _, token := range role.JWTTokens {
|
||||
expiresAt := "<none>"
|
||||
if token.ExpiresAt > 0 {
|
||||
expiresAt = humanizeTimestamp(token.ExpiresAt)
|
||||
}
|
||||
fmt.Fprintf(w, "%d\t%s\t%s\n", token.IssuedAt, humanizeTimestamp(token.IssuedAt), expiresAt)
|
||||
}
|
||||
_ = w.Flush()
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectRoleAddGroupCommand returns a new instance of an `argocd proj role add-group` command
|
||||
func NewProjectRoleAddGroupCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "add-group PROJECT ROLE-NAME GROUP-CLAIM",
|
||||
Short: "Add a policy to a project role",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName, roleName, groupName := args[0], args[1], args[2]
|
||||
conn, projIf := argocdclient.NewClientOrDie(clientOpts).NewProjectClientOrDie()
|
||||
defer util.Close(conn)
|
||||
proj, err := projIf.Get(context.Background(), &project.ProjectQuery{Name: projName})
|
||||
errors.CheckError(err)
|
||||
updated, err := projectutil.AddGroupToRole(proj, roleName, groupName)
|
||||
errors.CheckError(err)
|
||||
if updated {
|
||||
fmt.Printf("Group '%s' already present in role '%s'\n", groupName, roleName)
|
||||
return
|
||||
}
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Group '%s' added to role '%s'\n", groupName, roleName)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
|
||||
// NewProjectRoleRemoveGroupCommand returns a new instance of an `argocd proj role remove-group` command
|
||||
func NewProjectRoleRemoveGroupCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var command = &cobra.Command{
|
||||
Use: "remove-group PROJECT ROLE-NAME GROUP-CLAIM",
|
||||
Short: "Remove a group claim from a role within a project",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) != 3 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
projName, roleName, groupName := args[0], args[1], 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)
|
||||
updated, err := projectutil.RemoveGroupFromRole(proj, roleName, groupName)
|
||||
errors.CheckError(err)
|
||||
if !updated {
|
||||
fmt.Printf("Group '%s' not present in role '%s'\n", groupName, roleName)
|
||||
return
|
||||
}
|
||||
_, err = projIf.Update(context.Background(), &project.ProjectUpdateRequest{Project: proj})
|
||||
errors.CheckError(err)
|
||||
fmt.Printf("Group '%s' removed from role '%s'\n", groupName, roleName)
|
||||
},
|
||||
}
|
||||
return command
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
oidc "github.com/coreos/go-oidc"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/server/settings"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
"github.com/argoproj/argo-cd/util/session"
|
||||
)
|
||||
@@ -18,6 +21,7 @@ import (
|
||||
func NewReloginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
password string
|
||||
ssoPort int
|
||||
)
|
||||
var command = &cobra.Command{
|
||||
Use: "relogin",
|
||||
@@ -36,28 +40,33 @@ func NewReloginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
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
|
||||
clientOpts := argocdclient.ClientOptions{
|
||||
ConfigPath: "",
|
||||
ServerAddr: configCtx.Server.Server,
|
||||
Insecure: configCtx.Server.Insecure,
|
||||
PlainText: configCtx.Server.PlainText,
|
||||
}
|
||||
acdClient := argocdclient.NewClientOrDie(&clientOpts)
|
||||
claims, err := configCtx.User.Claims()
|
||||
errors.CheckError(err)
|
||||
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)
|
||||
setConn, setIf := acdClient.NewSettingsClientOrDie()
|
||||
defer util.Close(setConn)
|
||||
ctx := context.Background()
|
||||
httpClient, err := acdClient.HTTPClient()
|
||||
errors.CheckError(err)
|
||||
ctx = oidc.ClientContext(ctx, httpClient)
|
||||
acdSet, err := setIf.Get(ctx, &settings.SettingsQuery{})
|
||||
errors.CheckError(err)
|
||||
oauth2conf, provider, err := acdClient.OIDCConfig(ctx, acdSet)
|
||||
errors.CheckError(err)
|
||||
tokenString, refreshToken = oauth2Login(ctx, ssoPort, oauth2conf, provider)
|
||||
}
|
||||
|
||||
localCfg.UpsertUser(localconfig.User{
|
||||
@@ -71,5 +80,6 @@ func NewReloginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
},
|
||||
}
|
||||
command.Flags().StringVar(&password, "password", "", "the password of an account to authenticate")
|
||||
command.Flags().IntVar(&ssoPort, "sso-port", DefaultSSOLocalPort, "port to run local OAuth2 login application")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
}
|
||||
command.Flags().StringVar(&repo.Username, "username", "", "username to the repository")
|
||||
command.Flags().StringVar(&repo.Password, "password", "", "password to the repository")
|
||||
command.Flags().StringVar(&sshPrivateKeyPath, "sshPrivateKeyPath", "", "path to the private ssh key (e.g. ~/.ssh/id_rsa)")
|
||||
command.Flags().StringVar(&sshPrivateKeyPath, "ssh-private-key-path", "", "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
|
||||
}
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
argocdclient "github.com/argoproj/argo-cd/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/util/cli"
|
||||
"github.com/argoproj/argo-cd/util/localconfig"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cobra.OnInitialize(initConfig)
|
||||
}
|
||||
|
||||
var logLevel string
|
||||
|
||||
func initConfig() {
|
||||
cli.SetLogLevel(logLevel)
|
||||
}
|
||||
|
||||
// NewCommand returns a new instance of an argocd command
|
||||
func NewCommand() *cobra.Command {
|
||||
var (
|
||||
@@ -17,7 +29,7 @@ func NewCommand() *cobra.Command {
|
||||
|
||||
var command = &cobra.Command{
|
||||
Use: cliName,
|
||||
Short: "argocd controls a ArgoCD server",
|
||||
Short: "argocd controls a Argo CD server",
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
c.HelpFunc()(c, args)
|
||||
},
|
||||
@@ -35,12 +47,12 @@ func NewCommand() *cobra.Command {
|
||||
|
||||
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().StringVar(&clientOpts.ConfigPath, "config", defaultLocalConfigPath, "Path to Argo CD config")
|
||||
command.PersistentFlags().StringVar(&clientOpts.ServerAddr, "server", "", "Argo CD server address")
|
||||
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")
|
||||
|
||||
command.PersistentFlags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
|
||||
return command
|
||||
}
|
||||
|
||||
153
common/common.go
153
common/common.go
@@ -1,105 +1,98 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application"
|
||||
// Default service addresses and URLS of Argo CD internal services
|
||||
const (
|
||||
// DefaultAppControllerServerAddr is the gRPC address of the Argo CD app controller server
|
||||
DefaultAppControllerServerAddr = "argocd-application-controller:8083"
|
||||
// DefaultRepoServerAddr is the gRPC address of the Argo CD repo server
|
||||
DefaultRepoServerAddr = "argocd-repo-server:8081"
|
||||
// DefaultDexServerAddr is the HTTP address of the Dex OIDC server, which we run a reverse proxy against
|
||||
DefaultDexServerAddr = "http://dex-server:5556"
|
||||
)
|
||||
|
||||
// Kubernetes ConfigMap and Secret resource names which hold Argo CD settings
|
||||
const (
|
||||
// MetadataPrefix is the prefix used for our labels and annotations
|
||||
MetadataPrefix = "argocd.argoproj.io"
|
||||
|
||||
// SecretTypeRepository indicates a secret type of repository
|
||||
SecretTypeRepository = "repository"
|
||||
|
||||
// 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"
|
||||
ArgoCDSecretName = "argocd-secret"
|
||||
ArgoCDRBACConfigMapName = "argocd-rbac-cm"
|
||||
)
|
||||
|
||||
// Argo CD application related constants
|
||||
const (
|
||||
// KubernetesInternalAPIServerAddr is address of the k8s API server when accessing internal to the cluster
|
||||
KubernetesInternalAPIServerAddr = "https://kubernetes.default.svc"
|
||||
// DefaultAppProjectName contains name of 'default' app project, which is available in every Argo CD installation
|
||||
DefaultAppProjectName = "default"
|
||||
// ArgoCDAdminUsername is the username of the 'admin' user
|
||||
ArgoCDAdminUsername = "admin"
|
||||
// ArgoCDUserAgentName is the default user-agent name used by the gRPC API client library and grpc-gateway
|
||||
ArgoCDUserAgentName = "argocd-client"
|
||||
// AuthCookieName is the HTTP cookie name where we store our auth token
|
||||
AuthCookieName = "argocd.token"
|
||||
// RevisionHistoryLimit is the max number of successful sync to keep in history
|
||||
RevisionHistoryLimit = 10
|
||||
// K8sClientConfigQPS controls the QPS to be used in K8s REST client configs
|
||||
K8sClientConfigQPS = 25
|
||||
// K8sClientConfigBurst controls the burst to be used in K8s REST client configs
|
||||
K8sClientConfigBurst = 50
|
||||
)
|
||||
|
||||
// Dex related constants
|
||||
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 is Argo CD'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 is Argo CD'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"
|
||||
ArgoCDClientAppName = "Argo CD"
|
||||
// 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"
|
||||
ArgoCDCLIClientAppName = "Argo CD CLI"
|
||||
// ArgoCDCLIClientAppID is the Oauth client ID we will use when registering our CLI to dex
|
||||
ArgoCDCLIClientAppID = "argo-cd-cli"
|
||||
)
|
||||
|
||||
// Resource metadata labels and annotations (keys and values) used by Argo CD components
|
||||
const (
|
||||
// LabelKeyAppInstance is the label key to use to uniquely identify the instance of an application
|
||||
// The Argo CD application name is used as the instance name
|
||||
LabelKeyAppInstance = "app.kubernetes.io/instance"
|
||||
// LegacyLabelApplicationName is the legacy label (v0.10 and below) and is superceded by 'app.kubernetes.io/instance'
|
||||
LabelKeyLegacyApplicationName = "applications.argoproj.io/app-name"
|
||||
// LabelKeySecretType contains the type of argocd secret (currently: 'cluster')
|
||||
LabelKeySecretType = "argocd.argoproj.io/secret-type"
|
||||
// LabelValueSecretTypeCluster indicates a secret type of cluster
|
||||
LabelValueSecretTypeCluster = "cluster"
|
||||
|
||||
// AnnotationKeyHook contains the hook type of a resource
|
||||
AnnotationKeyHook = "argocd.argoproj.io/hook"
|
||||
// AnnotationKeyHookDeletePolicy is the policy of deleting a hook
|
||||
AnnotationKeyHookDeletePolicy = "argocd.argoproj.io/hook-delete-policy"
|
||||
// AnnotationKeyRefresh is the annotation key which indicates that app needs to be refreshed. Removed by application controller after app is refreshed.
|
||||
// Might take values 'normal'/'hard'. Value 'hard' means manifest cache and target cluster state cache should be invalidated before refresh.
|
||||
AnnotationKeyRefresh = "argocd.argoproj.io/refresh"
|
||||
// AnnotationKeyManagedBy is annotation name which indicates that k8s resource is managed by an application.
|
||||
AnnotationKeyManagedBy = "managed-by"
|
||||
// AnnotationValueManagedByArgoCD is a 'managed-by' annotation value for resources managed by Argo CD
|
||||
AnnotationValueManagedByArgoCD = "argocd.argoproj.io"
|
||||
// AnnotationKeyHelmHook is the helm hook annotation
|
||||
AnnotationKeyHelmHook = "helm.sh/hook"
|
||||
// AnnotationValueHelmHookCRDInstall is a value of crd helm hook
|
||||
AnnotationValueHelmHookCRDInstall = "crd-install"
|
||||
// ResourcesFinalizerName the finalizer value which we inject to finalize deletion of an application
|
||||
ResourcesFinalizerName = "resources-finalizer.argocd.argoproj.io"
|
||||
)
|
||||
|
||||
// Environment variables for tuning and debugging Argo CD
|
||||
const (
|
||||
// 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"
|
||||
// EnvVarFakeInClusterConfig is an environment variable to fake an in-cluster RESTConfig using
|
||||
// the current kubectl context (for development purposes)
|
||||
EnvVarFakeInClusterConfig = "ARGOCD_FAKE_IN_CLUSTER"
|
||||
)
|
||||
|
||||
var (
|
||||
// LabelKeyAppInstance refers to the application instance resource name
|
||||
LabelKeyAppInstance = MetadataPrefix + "/app-instance"
|
||||
|
||||
// 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
|
||||
const (
|
||||
ArgoCDManagerServiceAccount = "argocd-manager"
|
||||
ArgoCDManagerClusterRole = "argocd-manager-role"
|
||||
ArgoCDManagerClusterRoleBinding = "argocd-manager-role-binding"
|
||||
)
|
||||
|
||||
// ArgoCDManagerPolicyRules are the policies to give argocd-manager
|
||||
var ArgoCDManagerPolicyRules = []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{"*"},
|
||||
Resources: []string{"*"},
|
||||
Verbs: []string{"*"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
@@ -12,15 +11,34 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// ArgoCDManagerServiceAccount is the name of the service account for managing a cluster
|
||||
const (
|
||||
ArgoCDManagerServiceAccount = "argocd-manager"
|
||||
ArgoCDManagerClusterRole = "argocd-manager-role"
|
||||
ArgoCDManagerClusterRoleBinding = "argocd-manager-role-binding"
|
||||
)
|
||||
|
||||
// ArgoCDManagerPolicyRules are the policies to give argocd-manager
|
||||
var ArgoCDManagerPolicyRules = []rbacv1.PolicyRule{
|
||||
{
|
||||
APIGroups: []string{"*"},
|
||||
Resources: []string{"*"},
|
||||
Verbs: []string{"*"},
|
||||
},
|
||||
{
|
||||
NonResourceURLs: []string{"*"},
|
||||
Verbs: []string{"*"},
|
||||
},
|
||||
}
|
||||
|
||||
// CreateServiceAccount creates a service account
|
||||
func CreateServiceAccount(
|
||||
clientset kubernetes.Interface,
|
||||
serviceAccountName string,
|
||||
namespace string,
|
||||
) {
|
||||
) error {
|
||||
serviceAccount := apiv1.ServiceAccount{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "v1",
|
||||
@@ -34,12 +52,13 @@ func CreateServiceAccount(
|
||||
_, err := clientset.CoreV1().ServiceAccounts(namespace).Create(&serviceAccount)
|
||||
if err != nil {
|
||||
if !apierr.IsAlreadyExists(err) {
|
||||
log.Fatalf("Failed to create service account '%s': %v\n", serviceAccountName, err)
|
||||
return fmt.Errorf("Failed to create service account %q: %v", serviceAccountName, err)
|
||||
}
|
||||
fmt.Printf("ServiceAccount '%s' already exists\n", serviceAccountName)
|
||||
return
|
||||
log.Infof("ServiceAccount %q already exists", serviceAccountName)
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("ServiceAccount '%s' created\n", serviceAccountName)
|
||||
log.Infof("ServiceAccount %q created", serviceAccountName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateClusterRole creates a cluster role
|
||||
@@ -47,7 +66,7 @@ func CreateClusterRole(
|
||||
clientset kubernetes.Interface,
|
||||
clusterRoleName string,
|
||||
rules []rbacv1.PolicyRule,
|
||||
) {
|
||||
) error {
|
||||
clusterRole := rbacv1.ClusterRole{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
@@ -62,16 +81,17 @@ func CreateClusterRole(
|
||||
_, err := crclient.Create(&clusterRole)
|
||||
if err != nil {
|
||||
if !apierr.IsAlreadyExists(err) {
|
||||
log.Fatalf("Failed to create ClusterRole '%s': %v\n", clusterRoleName, err)
|
||||
return fmt.Errorf("Failed to create ClusterRole %q: %v", clusterRoleName, err)
|
||||
}
|
||||
_, err = crclient.Update(&clusterRole)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to update ClusterRole '%s': %v\n", clusterRoleName, err)
|
||||
return fmt.Errorf("Failed to update ClusterRole %q: %v", clusterRoleName, err)
|
||||
}
|
||||
fmt.Printf("ClusterRole '%s' updated\n", clusterRoleName)
|
||||
log.Infof("ClusterRole %q updated", clusterRoleName)
|
||||
} else {
|
||||
fmt.Printf("ClusterRole '%s' created\n", clusterRoleName)
|
||||
log.Infof("ClusterRole %q created", clusterRoleName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateClusterRoleBinding create a ClusterRoleBinding
|
||||
@@ -81,7 +101,7 @@ func CreateClusterRoleBinding(
|
||||
serviceAccountName,
|
||||
clusterRoleName string,
|
||||
namespace string,
|
||||
) {
|
||||
) error {
|
||||
roleBinding := rbacv1.ClusterRoleBinding{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
APIVersion: "rbac.authorization.k8s.io/v1",
|
||||
@@ -106,22 +126,34 @@ func CreateClusterRoleBinding(
|
||||
_, err := clientset.RbacV1().ClusterRoleBindings().Create(&roleBinding)
|
||||
if err != nil {
|
||||
if !apierr.IsAlreadyExists(err) {
|
||||
log.Fatalf("Failed to create ClusterRoleBinding %s: %v\n", clusterBindingRoleName, err)
|
||||
return fmt.Errorf("Failed to create ClusterRoleBinding %s: %v", clusterBindingRoleName, err)
|
||||
}
|
||||
fmt.Printf("ClusterRoleBinding '%s' already exists\n", clusterBindingRoleName)
|
||||
return
|
||||
log.Infof("ClusterRoleBinding %q already exists", clusterBindingRoleName)
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("ClusterRoleBinding '%s' created, bound '%s' to '%s'\n", clusterBindingRoleName, serviceAccountName, clusterRoleName)
|
||||
log.Infof("ClusterRoleBinding %q created, bound %q to %q", clusterBindingRoleName, serviceAccountName, clusterRoleName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// InstallClusterManagerRBAC installs RBAC resources for a cluster manager to operate a cluster. Returns a token
|
||||
func InstallClusterManagerRBAC(conf *rest.Config) string {
|
||||
func InstallClusterManagerRBAC(clientset kubernetes.Interface) (string, error) {
|
||||
const ns = "kube-system"
|
||||
clientset, err := kubernetes.NewForConfig(conf)
|
||||
errors.CheckError(err)
|
||||
CreateServiceAccount(clientset, ArgoCDManagerServiceAccount, ns)
|
||||
CreateClusterRole(clientset, ArgoCDManagerClusterRole, ArgoCDManagerPolicyRules)
|
||||
CreateClusterRoleBinding(clientset, ArgoCDManagerClusterRoleBinding, ArgoCDManagerServiceAccount, ArgoCDManagerClusterRole, ns)
|
||||
var err error
|
||||
|
||||
err = CreateServiceAccount(clientset, ArgoCDManagerServiceAccount, ns)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = CreateClusterRole(clientset, ArgoCDManagerClusterRole, ArgoCDManagerPolicyRules)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = CreateClusterRoleBinding(clientset, ArgoCDManagerClusterRoleBinding, ArgoCDManagerServiceAccount, ArgoCDManagerClusterRole, ns)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var serviceAccount *apiv1.ServiceAccount
|
||||
var secretName string
|
||||
@@ -137,52 +169,51 @@ func InstallClusterManagerRBAC(conf *rest.Config) string {
|
||||
return true, nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to wait for service account secret: %v", err)
|
||||
return "", fmt.Errorf("Failed to wait for service account secret: %v", err)
|
||||
}
|
||||
secret, err := clientset.CoreV1().Secrets(ns).Get(secretName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to retrieve secret '%s': %v", secretName, err)
|
||||
return "", fmt.Errorf("Failed to retrieve secret %q: %v", secretName, err)
|
||||
}
|
||||
token, ok := secret.Data["token"]
|
||||
if !ok {
|
||||
log.Fatalf("Secret '%s' for service account '%s' did not have a token", secretName, serviceAccount)
|
||||
return "", fmt.Errorf("Secret %q for service account %q did not have a token", secretName, serviceAccount)
|
||||
}
|
||||
return string(token)
|
||||
return string(token), nil
|
||||
}
|
||||
|
||||
// UninstallClusterManagerRBAC removes RBAC resources for a cluster manager to operate a cluster
|
||||
func UninstallClusterManagerRBAC(conf *rest.Config) {
|
||||
clientset, err := kubernetes.NewForConfig(conf)
|
||||
errors.CheckError(err)
|
||||
UninstallRBAC(clientset, "kube-system", ArgoCDManagerClusterRoleBinding, ArgoCDManagerClusterRole, ArgoCDManagerServiceAccount)
|
||||
func UninstallClusterManagerRBAC(clientset kubernetes.Interface) error {
|
||||
return UninstallRBAC(clientset, "kube-system", ArgoCDManagerClusterRoleBinding, ArgoCDManagerClusterRole, ArgoCDManagerServiceAccount)
|
||||
}
|
||||
|
||||
// UninstallRBAC uninstalls RBAC related resources for a binding, role, and service account
|
||||
func UninstallRBAC(clientset kubernetes.Interface, namespace, bindingName, roleName, serviceAccount string) {
|
||||
func UninstallRBAC(clientset kubernetes.Interface, namespace, bindingName, roleName, serviceAccount string) error {
|
||||
if err := clientset.RbacV1().ClusterRoleBindings().Delete(bindingName, &metav1.DeleteOptions{}); err != nil {
|
||||
if !apierr.IsNotFound(err) {
|
||||
log.Fatalf("Failed to delete ClusterRoleBinding: %v\n", err)
|
||||
return fmt.Errorf("Failed to delete ClusterRoleBinding: %v", err)
|
||||
}
|
||||
fmt.Printf("ClusterRoleBinding '%s' not found\n", bindingName)
|
||||
log.Infof("ClusterRoleBinding %q not found", bindingName)
|
||||
} else {
|
||||
fmt.Printf("ClusterRoleBinding '%s' deleted\n", bindingName)
|
||||
log.Infof("ClusterRoleBinding %q deleted", bindingName)
|
||||
}
|
||||
|
||||
if err := clientset.RbacV1().ClusterRoles().Delete(roleName, &metav1.DeleteOptions{}); err != nil {
|
||||
if !apierr.IsNotFound(err) {
|
||||
log.Fatalf("Failed to delete ClusterRole: %v\n", err)
|
||||
return fmt.Errorf("Failed to delete ClusterRole: %v", err)
|
||||
}
|
||||
fmt.Printf("ClusterRole '%s' not found\n", roleName)
|
||||
log.Infof("ClusterRole %q not found", roleName)
|
||||
} else {
|
||||
fmt.Printf("ClusterRole '%s' deleted\n", roleName)
|
||||
log.Infof("ClusterRole %q deleted", roleName)
|
||||
}
|
||||
|
||||
if err := clientset.CoreV1().ServiceAccounts(namespace).Delete(serviceAccount, &metav1.DeleteOptions{}); err != nil {
|
||||
if !apierr.IsNotFound(err) {
|
||||
log.Fatalf("Failed to delete ServiceAccount: %v\n", err)
|
||||
return fmt.Errorf("Failed to delete ServiceAccount: %v", err)
|
||||
}
|
||||
fmt.Printf("ServiceAccount '%s' in namespace '%s' not found\n", serviceAccount, namespace)
|
||||
log.Infof("ServiceAccount %q in namespace %q not found", serviceAccount, namespace)
|
||||
} else {
|
||||
fmt.Printf("ServiceAccount '%s' deleted\n", serviceAccount)
|
||||
log.Infof("ServiceAccount %q deleted", serviceAccount)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
462
controller/appcontroller_test.go
Normal file
462
controller/appcontroller_test.go
Normal file
@@ -0,0 +1,462 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
kubetesting "k8s.io/client-go/testing"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
mockstatecache "github.com/argoproj/argo-cd/controller/cache/mocks"
|
||||
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/fake"
|
||||
mockreposerver "github.com/argoproj/argo-cd/reposerver/mocks"
|
||||
"github.com/argoproj/argo-cd/reposerver/repository"
|
||||
mockrepoclient "github.com/argoproj/argo-cd/reposerver/repository/mocks"
|
||||
"github.com/argoproj/argo-cd/test"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
type fakeData struct {
|
||||
apps []runtime.Object
|
||||
manifestResponse *repository.ManifestResponse
|
||||
managedLiveObjs map[kube.ResourceKey]*unstructured.Unstructured
|
||||
}
|
||||
|
||||
func newFakeController(data *fakeData) *ApplicationController {
|
||||
var clust corev1.Secret
|
||||
err := yaml.Unmarshal([]byte(fakeCluster), &clust)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Mock out call to GenerateManifest
|
||||
mockRepoClient := mockrepoclient.RepositoryServiceClient{}
|
||||
mockRepoClient.On("GenerateManifest", mock.Anything, mock.Anything).Return(data.manifestResponse, nil)
|
||||
mockRepoClientset := mockreposerver.Clientset{}
|
||||
mockRepoClientset.On("NewRepositoryClient").Return(&fakeCloser{}, &mockRepoClient, nil)
|
||||
|
||||
secret := corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "argocd-secret",
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"admin.password": []byte("test"),
|
||||
"server.secretkey": []byte("test"),
|
||||
},
|
||||
}
|
||||
cm := corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "argocd-cm",
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
},
|
||||
Data: nil,
|
||||
}
|
||||
kubeClient := fake.NewSimpleClientset(&clust, &cm, &secret)
|
||||
settingsMgr := settings.NewSettingsManager(context.Background(), kubeClient, test.FakeArgoCDNamespace)
|
||||
ctrl, err := NewApplicationController(
|
||||
test.FakeArgoCDNamespace,
|
||||
settingsMgr,
|
||||
kubeClient,
|
||||
appclientset.NewSimpleClientset(data.apps...),
|
||||
&mockRepoClientset,
|
||||
time.Minute,
|
||||
)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
go ctrl.projInformer.Run(ctx.Done())
|
||||
cache.WaitForCacheSync(ctx.Done(), ctrl.projInformer.HasSynced)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Mock out call to GetManagedLiveObjs if fake data supplied
|
||||
if data.managedLiveObjs != nil {
|
||||
mockStateCache := mockstatecache.LiveStateCache{}
|
||||
mockStateCache.On("GetManagedLiveObjs", mock.Anything, mock.Anything).Return(data.managedLiveObjs, nil)
|
||||
mockStateCache.On("IsNamespaced", mock.Anything, mock.Anything).Return(true, nil)
|
||||
ctrl.stateCache = &mockStateCache
|
||||
ctrl.appStateManager.(*appStateManager).liveStateCache = &mockStateCache
|
||||
|
||||
}
|
||||
|
||||
return ctrl
|
||||
}
|
||||
|
||||
type fakeCloser struct{}
|
||||
|
||||
func (f *fakeCloser) Close() error { return nil }
|
||||
|
||||
var fakeCluster = `
|
||||
apiVersion: v1
|
||||
data:
|
||||
# {"bearerToken":"fake","tlsClientConfig":{"insecure":true},"awsAuthConfig":null}
|
||||
config: eyJiZWFyZXJUb2tlbiI6ImZha2UiLCJ0bHNDbGllbnRDb25maWciOnsiaW5zZWN1cmUiOnRydWV9LCJhd3NBdXRoQ29uZmlnIjpudWxsfQ==
|
||||
# minikube
|
||||
name: aHR0cHM6Ly9sb2NhbGhvc3Q6NjQ0Mw==
|
||||
# https://localhost:6443
|
||||
server: aHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3Zj
|
||||
kind: Secret
|
||||
metadata:
|
||||
labels:
|
||||
argocd.argoproj.io/secret-type: cluster
|
||||
name: localhost-6443
|
||||
namespace: ` + test.FakeArgoCDNamespace + `
|
||||
type: Opaque
|
||||
`
|
||||
|
||||
var fakeApp = `
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: my-app
|
||||
namespace: ` + test.FakeArgoCDNamespace + `
|
||||
spec:
|
||||
destination:
|
||||
namespace: ` + test.FakeDestNamespace + `
|
||||
server: https://localhost:6443
|
||||
project: default
|
||||
source:
|
||||
path: some/path
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps.git
|
||||
syncPolicy:
|
||||
automated: {}
|
||||
status:
|
||||
operationState:
|
||||
finishedAt: 2018-09-21T23:50:29Z
|
||||
message: successfully synced
|
||||
operation:
|
||||
sync:
|
||||
revision: HEAD
|
||||
phase: Succeeded
|
||||
startedAt: 2018-09-21T23:50:25Z
|
||||
syncResult:
|
||||
resources:
|
||||
- kind: RoleBinding
|
||||
message: |-
|
||||
rolebinding.rbac.authorization.k8s.io/always-outofsync reconciled
|
||||
rolebinding.rbac.authorization.k8s.io/always-outofsync configured
|
||||
name: always-outofsync
|
||||
namespace: default
|
||||
status: Synced
|
||||
revision: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
`
|
||||
|
||||
func newFakeApp() *argoappv1.Application {
|
||||
var app argoappv1.Application
|
||||
err := yaml.Unmarshal([]byte(fakeApp), &app)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &app
|
||||
}
|
||||
|
||||
func TestAutoSync(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus := argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus)
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, app.Operation)
|
||||
assert.NotNil(t, app.Operation.Sync)
|
||||
assert.False(t, app.Operation.Sync.Prune)
|
||||
}
|
||||
|
||||
func TestSkipAutoSync(t *testing.T) {
|
||||
// Verify we skip when we previously synced to it in our most recent history
|
||||
// Set current to 'aaaaa', desired to 'aaaa' and mark system OutOfSync
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus := argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus)
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
|
||||
// Verify we skip when we are already Synced (even if revision is different)
|
||||
app = newFakeApp()
|
||||
ctrl = newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus = argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeSynced,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond = ctrl.autoSync(app, &syncStatus)
|
||||
assert.Nil(t, cond)
|
||||
app, err = ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
|
||||
// Verify we skip when auto-sync is disabled
|
||||
app = newFakeApp()
|
||||
app.Spec.SyncPolicy = nil
|
||||
ctrl = newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus = argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond = ctrl.autoSync(app, &syncStatus)
|
||||
assert.Nil(t, cond)
|
||||
app, err = ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
|
||||
// Verify we skip when previous sync attempt failed and return error condition
|
||||
// Set current to 'aaaaa', desired to 'bbbbb' and add 'bbbbb' to failure history
|
||||
app = newFakeApp()
|
||||
app.Status.OperationState = &argoappv1.OperationState{
|
||||
Operation: argoappv1.Operation{
|
||||
Sync: &argoappv1.SyncOperation{},
|
||||
},
|
||||
Phase: argoappv1.OperationFailed,
|
||||
SyncResult: &argoappv1.SyncOperationResult{
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
},
|
||||
}
|
||||
ctrl = newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus = argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
}
|
||||
cond = ctrl.autoSync(app, &syncStatus)
|
||||
assert.NotNil(t, cond)
|
||||
app, err = ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
}
|
||||
|
||||
// TestAutoSyncIndicateError verifies we skip auto-sync and return error condition if previous sync failed
|
||||
func TestAutoSyncIndicateError(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Spec.Source.ComponentParameterOverrides = []argoappv1.ComponentParameter{
|
||||
{
|
||||
Name: "a",
|
||||
Value: "1",
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus := argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
}
|
||||
app.Status.OperationState = &argoappv1.OperationState{
|
||||
Operation: argoappv1.Operation{
|
||||
Sync: &argoappv1.SyncOperation{
|
||||
ParameterOverrides: argoappv1.ParameterOverrides{
|
||||
{
|
||||
Name: "a",
|
||||
Value: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Phase: argoappv1.OperationFailed,
|
||||
SyncResult: &argoappv1.SyncOperationResult{
|
||||
Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
},
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus)
|
||||
assert.NotNil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, app.Operation)
|
||||
}
|
||||
|
||||
// TestAutoSyncParameterOverrides verifies we auto-sync if revision is same but parameter overrides are different
|
||||
func TestAutoSyncParameterOverrides(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Spec.Source.ComponentParameterOverrides = []argoappv1.ComponentParameter{
|
||||
{
|
||||
Name: "a",
|
||||
Value: "1",
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
syncStatus := argoappv1.SyncStatus{
|
||||
Status: argoappv1.SyncStatusCodeOutOfSync,
|
||||
Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
}
|
||||
app.Status.OperationState = &argoappv1.OperationState{
|
||||
Operation: argoappv1.Operation{
|
||||
Sync: &argoappv1.SyncOperation{
|
||||
ParameterOverrides: argoappv1.ParameterOverrides{
|
||||
{
|
||||
Name: "a",
|
||||
Value: "2", // this value changed
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Phase: argoappv1.OperationFailed,
|
||||
SyncResult: &argoappv1.SyncOperationResult{
|
||||
Revision: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
},
|
||||
}
|
||||
cond := ctrl.autoSync(app, &syncStatus)
|
||||
assert.Nil(t, cond)
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(test.FakeArgoCDNamespace).Get("my-app", metav1.GetOptions{})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, app.Operation)
|
||||
}
|
||||
|
||||
// TestFinalizeAppDeletion verifies application deletion
|
||||
func TestFinalizeAppDeletion(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
patched := false
|
||||
fakeAppCs.ReactionChain = nil
|
||||
fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
patched = true
|
||||
return true, nil, nil
|
||||
})
|
||||
err := ctrl.finalizeApplicationDeletion(app)
|
||||
// TODO: use an interface to fake out the calls to GetResourcesWithLabel and DeleteResourceWithLabel
|
||||
// For now just ensure we have an expected error condition
|
||||
assert.Error(t, err) // Change this to assert.Nil when we stub out GetResourcesWithLabel/DeleteResourceWithLabel
|
||||
assert.False(t, patched) // Change this to assert.True when we stub out GetResourcesWithLabel/DeleteResourceWithLabel
|
||||
}
|
||||
|
||||
// TestNormalizeApplication verifies we normalize an application during reconciliation
|
||||
func TestNormalizeApplication(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Spec.Project = ""
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
cancel := test.StartInformer(ctrl.appInformer)
|
||||
defer cancel()
|
||||
key, _ := cache.MetaNamespaceKeyFunc(app)
|
||||
ctrl.appRefreshQueue.Add(key)
|
||||
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
fakeAppCs.ReactionChain = nil
|
||||
normalized := false
|
||||
fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
if patchAction, ok := action.(kubetesting.PatchAction); ok {
|
||||
if string(patchAction.GetPatch()) == `{"spec":{"project":"default"}}` {
|
||||
normalized = true
|
||||
}
|
||||
}
|
||||
return true, nil, nil
|
||||
})
|
||||
ctrl.processAppRefreshQueueItem()
|
||||
assert.True(t, normalized)
|
||||
}
|
||||
|
||||
// TestDontNormalizeApplication verifies we dont unnecessarily normalize an application
|
||||
func TestDontNormalizeApplication(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Spec.Project = "default"
|
||||
ctrl := newFakeController(&fakeData{apps: []runtime.Object{app}})
|
||||
cancel := test.StartInformer(ctrl.appInformer)
|
||||
defer cancel()
|
||||
key, _ := cache.MetaNamespaceKeyFunc(app)
|
||||
ctrl.appRefreshQueue.Add(key)
|
||||
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
fakeAppCs.ReactionChain = nil
|
||||
normalized := false
|
||||
fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
if patchAction, ok := action.(kubetesting.PatchAction); ok {
|
||||
if strings.HasPrefix(string(patchAction.GetPatch()), `{"spec":`) {
|
||||
normalized = true
|
||||
}
|
||||
}
|
||||
return true, nil, nil
|
||||
})
|
||||
ctrl.processAppRefreshQueueItem()
|
||||
assert.False(t, normalized)
|
||||
}
|
||||
|
||||
func createSecret(data map[string]string) *unstructured.Unstructured {
|
||||
secret := corev1.Secret{TypeMeta: metav1.TypeMeta{Kind: kube.SecretKind}}
|
||||
if data != nil {
|
||||
secret.Data = make(map[string][]byte)
|
||||
for k, v := range data {
|
||||
secret.Data[k] = []byte(v)
|
||||
}
|
||||
}
|
||||
|
||||
return kube.MustToUnstructured(&secret)
|
||||
}
|
||||
|
||||
func secretData(obj *unstructured.Unstructured) map[string]interface{} {
|
||||
data, _, _ := unstructured.NestedMap(obj.Object, "data")
|
||||
return data
|
||||
}
|
||||
|
||||
const (
|
||||
replacement1 = "*********"
|
||||
replacement2 = "**********"
|
||||
replacement3 = "***********"
|
||||
)
|
||||
|
||||
func TestHideSecretDataSameKeysDifferentValues(t *testing.T) {
|
||||
target, live, err := hideSecretData(
|
||||
createSecret(map[string]string{"key1": "test", "key2": "test"}),
|
||||
createSecret(map[string]string{"key1": "test-1", "key2": "test-1"}))
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, map[string]interface{}{"key1": replacement1, "key2": replacement1}, secretData(target))
|
||||
assert.Equal(t, map[string]interface{}{"key1": replacement2, "key2": replacement2}, secretData(live))
|
||||
}
|
||||
|
||||
func TestHideSecretDataSameKeysSameValues(t *testing.T) {
|
||||
target, live, err := hideSecretData(
|
||||
createSecret(map[string]string{"key1": "test", "key2": "test"}),
|
||||
createSecret(map[string]string{"key1": "test", "key2": "test"}))
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, map[string]interface{}{"key1": replacement1, "key2": replacement1}, secretData(target))
|
||||
assert.Equal(t, map[string]interface{}{"key1": replacement1, "key2": replacement1}, secretData(live))
|
||||
}
|
||||
|
||||
func TestHideSecretDataDifferentKeysDifferentValues(t *testing.T) {
|
||||
target, live, err := hideSecretData(
|
||||
createSecret(map[string]string{"key1": "test", "key2": "test"}),
|
||||
createSecret(map[string]string{"key2": "test-1", "key3": "test-1"}))
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, map[string]interface{}{"key1": replacement1, "key2": replacement1}, secretData(target))
|
||||
assert.Equal(t, map[string]interface{}{"key2": replacement2, "key3": replacement1}, secretData(live))
|
||||
}
|
||||
|
||||
func TestHideSecretDataLastAppliedConfig(t *testing.T) {
|
||||
lastAppliedSecret := createSecret(map[string]string{"key1": "test1"})
|
||||
targetSecret := createSecret(map[string]string{"key1": "test2"})
|
||||
liveSecret := createSecret(map[string]string{"key1": "test3"})
|
||||
lastAppliedStr, err := json.Marshal(lastAppliedSecret)
|
||||
assert.Nil(t, err)
|
||||
liveSecret.SetAnnotations(map[string]string{corev1.LastAppliedConfigAnnotation: string(lastAppliedStr)})
|
||||
|
||||
target, live, err := hideSecretData(targetSecret, liveSecret)
|
||||
assert.Nil(t, err)
|
||||
err = json.Unmarshal([]byte(live.GetAnnotations()[corev1.LastAppliedConfigAnnotation]), &lastAppliedSecret)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, map[string]interface{}{"key1": replacement1}, secretData(target))
|
||||
assert.Equal(t, map[string]interface{}{"key1": replacement2}, secretData(live))
|
||||
assert.Equal(t, map[string]interface{}{"key1": replacement3}, secretData(lastAppliedSecret))
|
||||
|
||||
}
|
||||
307
controller/cache/cache.go
vendored
Normal file
307
controller/cache/cache.go
vendored
Normal file
@@ -0,0 +1,307 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/db"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
type LiveStateCache interface {
|
||||
IsNamespaced(server string, gvk schema.GroupVersionKind) (bool, error)
|
||||
// Returns child nodes for a given k8s resource
|
||||
GetChildren(server string, obj *unstructured.Unstructured) ([]appv1.ResourceNode, error)
|
||||
// Returns state of live nodes which correspond for target nodes of specified application.
|
||||
GetManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error)
|
||||
// Starts watching resources of each controlled cluster.
|
||||
Run(ctx context.Context)
|
||||
// Deletes specified resource from cluster.
|
||||
Delete(server string, obj *unstructured.Unstructured) error
|
||||
// Invalidate invalidates the entire cluster state cache
|
||||
Invalidate()
|
||||
}
|
||||
|
||||
func GetTargetObjKey(a *appv1.Application, un *unstructured.Unstructured, isNamespaced bool) kube.ResourceKey {
|
||||
key := kube.GetResourceKey(un)
|
||||
if !isNamespaced {
|
||||
key.Namespace = ""
|
||||
} else if isNamespaced && key.Namespace == "" {
|
||||
key.Namespace = a.Spec.Destination.Namespace
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
func NewLiveStateCache(db db.ArgoDB, appInformer cache.SharedIndexInformer, settings *settings.ArgoCDSettings, kubectl kube.Kubectl, onAppUpdated func(appName string)) LiveStateCache {
|
||||
return &liveStateCache{
|
||||
appInformer: appInformer,
|
||||
db: db,
|
||||
clusters: make(map[string]*clusterInfo),
|
||||
lock: &sync.Mutex{},
|
||||
onAppUpdated: onAppUpdated,
|
||||
kubectl: kubectl,
|
||||
settings: settings,
|
||||
}
|
||||
}
|
||||
|
||||
type liveStateCache struct {
|
||||
db db.ArgoDB
|
||||
clusters map[string]*clusterInfo
|
||||
lock *sync.Mutex
|
||||
appInformer cache.SharedIndexInformer
|
||||
onAppUpdated func(appName string)
|
||||
kubectl kube.Kubectl
|
||||
settings *settings.ArgoCDSettings
|
||||
}
|
||||
|
||||
func (c *liveStateCache) processEvent(event watch.EventType, obj *unstructured.Unstructured, url string) error {
|
||||
info, err := c.getSyncedCluster(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return info.processEvent(event, obj)
|
||||
}
|
||||
|
||||
func (c *liveStateCache) removeCluster(server string) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
delete(c.clusters, server)
|
||||
log.Infof("Dropped cluster %s cache", server)
|
||||
}
|
||||
|
||||
func (c *liveStateCache) getCluster(server string) (*clusterInfo, error) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
info, ok := c.clusters[server]
|
||||
if !ok {
|
||||
cluster, err := c.db.GetCluster(context.Background(), server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info = &clusterInfo{
|
||||
apis: make(map[schema.GroupVersionKind]metav1.APIResource),
|
||||
lock: &sync.Mutex{},
|
||||
nodes: make(map[kube.ResourceKey]*node),
|
||||
nsIndex: make(map[string]map[kube.ResourceKey]*node),
|
||||
onAppUpdated: c.onAppUpdated,
|
||||
kubectl: c.kubectl,
|
||||
cluster: cluster,
|
||||
syncTime: nil,
|
||||
syncLock: &sync.Mutex{},
|
||||
log: log.WithField("server", cluster.Server),
|
||||
settings: c.settings,
|
||||
}
|
||||
|
||||
c.clusters[cluster.Server] = info
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) getSyncedCluster(server string) (*clusterInfo, error) {
|
||||
info, err := c.getCluster(server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = info.ensureSynced()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) Invalidate() {
|
||||
log.Info("invalidating live state cache")
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
for _, clust := range c.clusters {
|
||||
clust.lock.Lock()
|
||||
clust.invalidate()
|
||||
clust.lock.Unlock()
|
||||
}
|
||||
log.Info("live state cache invalidated")
|
||||
}
|
||||
|
||||
func (c *liveStateCache) Delete(server string, obj *unstructured.Unstructured) error {
|
||||
clusterInfo, err := c.getSyncedCluster(server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return clusterInfo.delete(obj)
|
||||
}
|
||||
|
||||
func (c *liveStateCache) IsNamespaced(server string, gvk schema.GroupVersionKind) (bool, error) {
|
||||
clusterInfo, err := c.getSyncedCluster(server)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return clusterInfo.isNamespaced(gvk), nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) GetChildren(server string, obj *unstructured.Unstructured) ([]appv1.ResourceNode, error) {
|
||||
clusterInfo, err := c.getSyncedCluster(server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return clusterInfo.getChildren(obj), nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) GetManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
|
||||
clusterInfo, err := c.getSyncedCluster(a.Spec.Destination.Server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return clusterInfo.getManagedLiveObjs(a, targetObjs)
|
||||
}
|
||||
|
||||
func isClusterHasApps(apps []interface{}, cluster *appv1.Cluster) bool {
|
||||
for _, obj := range apps {
|
||||
if app, ok := obj.(*appv1.Application); ok && app.Spec.Destination.Server == cluster.Server {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Run watches for resource changes annotated with application label on all registered clusters and schedule corresponding app refresh.
|
||||
func (c *liveStateCache) Run(ctx context.Context) {
|
||||
watchingClusters := make(map[string]struct {
|
||||
cancel context.CancelFunc
|
||||
cluster *appv1.Cluster
|
||||
})
|
||||
|
||||
util.RetryUntilSucceed(func() error {
|
||||
clusterEventCallback := func(event *db.ClusterEvent) {
|
||||
info, ok := watchingClusters[event.Cluster.Server]
|
||||
hasApps := isClusterHasApps(c.appInformer.GetStore().List(), event.Cluster)
|
||||
|
||||
// cluster resources must be watched only if cluster has at least one app
|
||||
if (event.Type == watch.Deleted || !hasApps) && ok {
|
||||
info.cancel()
|
||||
delete(watchingClusters, event.Cluster.Server)
|
||||
} else if event.Type != watch.Deleted && !ok && hasApps {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
watchingClusters[event.Cluster.Server] = struct {
|
||||
cancel context.CancelFunc
|
||||
cluster *appv1.Cluster
|
||||
}{
|
||||
cancel: func() {
|
||||
c.removeCluster(event.Cluster.Server)
|
||||
cancel()
|
||||
},
|
||||
cluster: event.Cluster,
|
||||
}
|
||||
go c.watchClusterResources(ctx, *event.Cluster)
|
||||
}
|
||||
}
|
||||
|
||||
onAppModified := func(obj interface{}) {
|
||||
if app, ok := obj.(*appv1.Application); ok {
|
||||
var cluster *appv1.Cluster
|
||||
info, infoOk := watchingClusters[app.Spec.Destination.Server]
|
||||
if infoOk {
|
||||
cluster = info.cluster
|
||||
} else {
|
||||
cluster, _ = c.db.GetCluster(ctx, app.Spec.Destination.Server)
|
||||
}
|
||||
if cluster != nil {
|
||||
// trigger cluster event every time when app created/deleted to either start or stop watching resources
|
||||
clusterEventCallback(&db.ClusterEvent{Cluster: cluster, Type: watch.Modified})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.appInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: onAppModified,
|
||||
UpdateFunc: func(oldObj, newObj interface{}) {
|
||||
oldApp, oldOk := oldObj.(*appv1.Application)
|
||||
newApp, newOk := newObj.(*appv1.Application)
|
||||
if oldOk && newOk {
|
||||
if oldApp.Spec.Destination.Server != newApp.Spec.Destination.Server {
|
||||
onAppModified(oldObj)
|
||||
onAppModified(newApp)
|
||||
}
|
||||
}
|
||||
},
|
||||
DeleteFunc: onAppModified,
|
||||
})
|
||||
|
||||
return c.db.WatchClusters(ctx, clusterEventCallback)
|
||||
|
||||
}, "watch clusters", ctx, clusterRetryTimeout)
|
||||
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
// watchClusterResources watches for resource changes annotated with application label on specified cluster and schedule corresponding app refresh.
|
||||
func (c *liveStateCache) watchClusterResources(ctx context.Context, item appv1.Cluster) {
|
||||
util.RetryUntilSucceed(func() (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("Recovered from panic: %v\n", r)
|
||||
}
|
||||
}()
|
||||
config := item.RESTConfig()
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
knownCRDs, err := getCRDs(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ch, err := c.kubectl.WatchResources(ctx, config, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for event := range ch {
|
||||
eventObj := event.Object.(*unstructured.Unstructured)
|
||||
if kube.IsCRD(eventObj) {
|
||||
// restart if new CRD has been created after watch started
|
||||
if event.Type == watch.Added {
|
||||
if !knownCRDs[eventObj.GetName()] {
|
||||
c.removeCluster(item.Server)
|
||||
return fmt.Errorf("Restarting the watch because a new CRD %s was added", eventObj.GetName())
|
||||
} else {
|
||||
log.Infof("CRD %s updated", eventObj.GetName())
|
||||
}
|
||||
} else if event.Type == watch.Deleted {
|
||||
c.removeCluster(item.Server)
|
||||
return fmt.Errorf("Restarting the watch because CRD %s was deleted", eventObj.GetName())
|
||||
}
|
||||
}
|
||||
err = c.processEvent(event.Type, eventObj, item.Server)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to process event %s for obj %v: %v", event.Type, event.Object, err)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("resource updates channel has closed")
|
||||
}, fmt.Sprintf("watch app resources on %s", item.Server), ctx, clusterRetryTimeout)
|
||||
}
|
||||
|
||||
// getCRDs returns a map of crds
|
||||
func getCRDs(config *rest.Config) (map[string]bool, error) {
|
||||
crdsByName := make(map[string]bool)
|
||||
apiextensionsClientset := apiextensionsclient.NewForConfigOrDie(config)
|
||||
crds, err := apiextensionsClientset.ApiextensionsV1beta1().CustomResourceDefinitions().List(metav1.ListOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, crd := range crds.Items {
|
||||
crdsByName[crd.Name] = true
|
||||
}
|
||||
// TODO: support api service, like ServiceCatalog
|
||||
return crdsByName, nil
|
||||
}
|
||||
321
controller/cache/cluster.go
vendored
Normal file
321
controller/cache/cluster.go
vendored
Normal file
@@ -0,0 +1,321 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
const (
|
||||
clusterSyncTimeout = 1 * time.Hour
|
||||
clusterRetryTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
type clusterInfo struct {
|
||||
apis map[schema.GroupVersionKind]metav1.APIResource
|
||||
nodes map[kube.ResourceKey]*node
|
||||
nsIndex map[string]map[kube.ResourceKey]*node
|
||||
lock *sync.Mutex
|
||||
onAppUpdated func(appName string)
|
||||
kubectl kube.Kubectl
|
||||
cluster *appv1.Cluster
|
||||
syncLock *sync.Mutex
|
||||
syncTime *time.Time
|
||||
syncError error
|
||||
log *log.Entry
|
||||
settings *settings.ArgoCDSettings
|
||||
}
|
||||
|
||||
func createObjInfo(un *unstructured.Unstructured, appInstanceLabel string) *node {
|
||||
ownerRefs := un.GetOwnerReferences()
|
||||
// Special case for endpoint. Remove after https://github.com/kubernetes/kubernetes/issues/28483 is fixed
|
||||
if un.GroupVersionKind().Group == "" && un.GetKind() == kube.EndpointsKind && len(un.GetOwnerReferences()) == 0 {
|
||||
ownerRefs = append(ownerRefs, metav1.OwnerReference{
|
||||
Name: un.GetName(),
|
||||
Kind: kube.ServiceKind,
|
||||
APIVersion: "",
|
||||
})
|
||||
}
|
||||
info := &node{
|
||||
resourceVersion: un.GetResourceVersion(),
|
||||
ref: v1.ObjectReference{
|
||||
APIVersion: un.GetAPIVersion(),
|
||||
Kind: un.GetKind(),
|
||||
Name: un.GetName(),
|
||||
Namespace: un.GetNamespace(),
|
||||
},
|
||||
ownerRefs: ownerRefs,
|
||||
info: getNodeInfo(un),
|
||||
}
|
||||
appName := kube.GetAppInstanceLabel(un, appInstanceLabel)
|
||||
if len(ownerRefs) == 0 && appName != "" {
|
||||
info.appName = appName
|
||||
info.resource = un
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func (c *clusterInfo) setNode(n *node) {
|
||||
key := n.resourceKey()
|
||||
c.nodes[key] = n
|
||||
ns, ok := c.nsIndex[key.Namespace]
|
||||
if !ok {
|
||||
ns = make(map[kube.ResourceKey]*node)
|
||||
c.nsIndex[key.Namespace] = ns
|
||||
}
|
||||
ns[key] = n
|
||||
}
|
||||
|
||||
func (c *clusterInfo) removeNode(key kube.ResourceKey) {
|
||||
delete(c.nodes, key)
|
||||
if ns, ok := c.nsIndex[key.Namespace]; ok {
|
||||
delete(ns, key)
|
||||
if len(ns) == 0 {
|
||||
delete(c.nsIndex, key.Namespace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *clusterInfo) invalidate() {
|
||||
c.syncTime = nil
|
||||
}
|
||||
|
||||
func (c *clusterInfo) synced() bool {
|
||||
if c.syncTime == nil {
|
||||
return false
|
||||
}
|
||||
if c.syncError != nil {
|
||||
return time.Now().Before(c.syncTime.Add(clusterRetryTimeout))
|
||||
}
|
||||
return time.Now().Before(c.syncTime.Add(clusterSyncTimeout))
|
||||
}
|
||||
|
||||
func (c *clusterInfo) sync() (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("Recovered from panic: %+v\n%s", r, debug.Stack())
|
||||
}
|
||||
}()
|
||||
|
||||
c.log.Info("Start syncing cluster")
|
||||
|
||||
clusterResources, err := c.kubectl.GetAPIResources(c.cluster.RESTConfig())
|
||||
if err != nil {
|
||||
if len(clusterResources) == 0 {
|
||||
return err
|
||||
}
|
||||
log.Warnf("Partial success when getting API resources during sync: %v", err)
|
||||
}
|
||||
c.apis = make(map[schema.GroupVersionKind]metav1.APIResource)
|
||||
for _, r := range clusterResources {
|
||||
gv, err := schema.ParseGroupVersion(r.GroupVersion)
|
||||
if err != nil {
|
||||
gv = schema.GroupVersion{}
|
||||
}
|
||||
for i := range r.APIResources {
|
||||
c.apis[gv.WithKind(r.APIResources[i].Kind)] = r.APIResources[i]
|
||||
}
|
||||
}
|
||||
|
||||
c.nodes = make(map[kube.ResourceKey]*node)
|
||||
resources, err := c.kubectl.GetResources(c.cluster.RESTConfig(), "")
|
||||
if err != nil {
|
||||
log.Errorf("Failed to sync cluster %s: %v", c.cluster.Server, err)
|
||||
return err
|
||||
}
|
||||
|
||||
appLabelKey := c.settings.GetAppInstanceLabelKey()
|
||||
for i := range resources {
|
||||
c.setNode(createObjInfo(resources[i], appLabelKey))
|
||||
}
|
||||
|
||||
c.log.Info("Cluster successfully synced")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *clusterInfo) ensureSynced() error {
|
||||
if c.synced() {
|
||||
return c.syncError
|
||||
}
|
||||
c.syncLock.Lock()
|
||||
defer c.syncLock.Unlock()
|
||||
if c.synced() {
|
||||
return c.syncError
|
||||
}
|
||||
|
||||
err := c.sync()
|
||||
syncTime := time.Now()
|
||||
c.syncTime = &syncTime
|
||||
c.syncError = err
|
||||
return c.syncError
|
||||
}
|
||||
|
||||
func (c *clusterInfo) getChildren(obj *unstructured.Unstructured) []appv1.ResourceNode {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
children := make([]appv1.ResourceNode, 0)
|
||||
if objInfo, ok := c.nodes[kube.GetResourceKey(obj)]; ok {
|
||||
nsNodes := c.nsIndex[obj.GetNamespace()]
|
||||
for _, child := range nsNodes {
|
||||
if objInfo.isParentOf(child) {
|
||||
children = append(children, child.childResourceNodes(nsNodes, map[kube.ResourceKey]bool{objInfo.resourceKey(): true}))
|
||||
}
|
||||
}
|
||||
}
|
||||
return children
|
||||
}
|
||||
|
||||
func (c *clusterInfo) isNamespaced(gvk schema.GroupVersionKind) bool {
|
||||
if api, ok := c.apis[gvk]; ok && !api.Namespaced {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *clusterInfo) getManagedLiveObjs(a *appv1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
managedObjs := make(map[kube.ResourceKey]*unstructured.Unstructured)
|
||||
// iterate all objects in live state cache to find ones associated with app
|
||||
for key, o := range c.nodes {
|
||||
if o.appName == a.Name && o.resource != nil && len(o.ownerRefs) == 0 {
|
||||
managedObjs[key] = o.resource
|
||||
}
|
||||
}
|
||||
// iterate target objects and identify ones that already exist in the cluster,\
|
||||
// but are simply missing our label
|
||||
lock := &sync.Mutex{}
|
||||
err := util.RunAllAsync(len(targetObjs), func(i int) error {
|
||||
targetObj := targetObjs[i]
|
||||
key := GetTargetObjKey(a, targetObj, c.isNamespaced(targetObj.GroupVersionKind()))
|
||||
lock.Lock()
|
||||
managedObj := managedObjs[key]
|
||||
lock.Unlock()
|
||||
|
||||
if managedObj == nil {
|
||||
if existingObj, exists := c.nodes[key]; exists {
|
||||
if existingObj.resource != nil {
|
||||
managedObj = existingObj.resource
|
||||
} else {
|
||||
var err error
|
||||
managedObj, err = c.kubectl.GetResource(c.cluster.RESTConfig(), targetObj.GroupVersionKind(), existingObj.ref.Name, existingObj.ref.Namespace)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
c.checkAndInvalidateStaleCache(targetObj.GroupVersionKind(), existingObj.ref.Namespace, existingObj.ref.Name)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if managedObj != nil {
|
||||
managedObj, err := c.kubectl.ConvertToVersion(managedObj, targetObj.GroupVersionKind().Group, targetObj.GroupVersionKind().Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lock.Lock()
|
||||
managedObjs[key] = managedObj
|
||||
lock.Unlock()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return managedObjs, nil
|
||||
}
|
||||
|
||||
func (c *clusterInfo) delete(obj *unstructured.Unstructured) error {
|
||||
err := c.kubectl.DeleteResource(c.cluster.RESTConfig(), obj.GroupVersionKind(), obj.GetName(), obj.GetNamespace(), false)
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
// a delete request came in for an object which does not exist. it's possible that our cache
|
||||
// is stale. Check and invalidate if it is
|
||||
c.lock.Lock()
|
||||
c.checkAndInvalidateStaleCache(obj.GroupVersionKind(), obj.GetNamespace(), obj.GetName())
|
||||
c.lock.Unlock()
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// checkAndInvalidateStaleCache checks if our cache is stale and invalidate it based on error
|
||||
// should be called whenever we suspect our cache is stale
|
||||
func (c *clusterInfo) checkAndInvalidateStaleCache(gvk schema.GroupVersionKind, namespace string, name string) {
|
||||
if _, ok := c.nodes[kube.NewResourceKey(gvk.Group, gvk.Kind, namespace, name)]; ok {
|
||||
if c.syncTime != nil {
|
||||
c.log.Warnf("invalidated stale cache due to mismatch of %s, %s/%s", gvk, namespace, name)
|
||||
c.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *clusterInfo) processEvent(event watch.EventType, un *unstructured.Unstructured) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
key := kube.GetResourceKey(un)
|
||||
existingNode, exists := c.nodes[key]
|
||||
if event == watch.Deleted {
|
||||
if exists {
|
||||
c.removeNode(key)
|
||||
if existingNode.appName != "" {
|
||||
c.onAppUpdated(existingNode.appName)
|
||||
}
|
||||
}
|
||||
} else if event != watch.Deleted {
|
||||
nodes := make([]*node, 0)
|
||||
if exists {
|
||||
nodes = append(nodes, existingNode)
|
||||
}
|
||||
newObj := createObjInfo(un, c.settings.GetAppInstanceLabelKey())
|
||||
c.setNode(newObj)
|
||||
nodes = append(nodes, newObj)
|
||||
|
||||
toNotify := make(map[string]bool)
|
||||
for i := range nodes {
|
||||
n := nodes[i]
|
||||
if ns, ok := c.nsIndex[n.ref.Namespace]; ok {
|
||||
app := n.getApp(ns)
|
||||
if app == "" || skipAppRequeing(key) {
|
||||
continue
|
||||
}
|
||||
toNotify[app] = true
|
||||
}
|
||||
}
|
||||
|
||||
for name := range toNotify {
|
||||
c.onAppUpdated(name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
ignoredRefreshResources = map[string]bool{
|
||||
"/" + kube.EndpointsKind: true,
|
||||
}
|
||||
)
|
||||
|
||||
// skipAppRequeing checks if the object is an API type which we want to skip requeuing against.
|
||||
// We ignore API types which have a high churn rate, and/or whose updates are irrelevant to the app
|
||||
func skipAppRequeing(key kube.ResourceKey) bool {
|
||||
return ignoredRefreshResources[key.Group+"/"+key.Kind]
|
||||
}
|
||||
276
controller/cache/cluster_test.go
vendored
Normal file
276
controller/cache/cluster_test.go
vendored
Normal file
@@ -0,0 +1,276 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/stretchr/testify/assert"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
|
||||
"github.com/argoproj/argo-cd/errors"
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/kube/kubetest"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func strToUnstructured(jsonStr string) *unstructured.Unstructured {
|
||||
obj := make(map[string]interface{})
|
||||
err := yaml.Unmarshal([]byte(jsonStr), &obj)
|
||||
errors.CheckError(err)
|
||||
return &unstructured.Unstructured{Object: obj}
|
||||
}
|
||||
|
||||
func mustToUnstructured(obj interface{}) *unstructured.Unstructured {
|
||||
un, err := kube.ToUnstructured(obj)
|
||||
errors.CheckError(err)
|
||||
return un
|
||||
}
|
||||
|
||||
var (
|
||||
testPod = strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: helm-guestbook-pod
|
||||
namespace: default
|
||||
ownerReferences:
|
||||
- apiVersion: extensions/v1beta1
|
||||
kind: ReplicaSet
|
||||
name: helm-guestbook-rs
|
||||
resourceVersion: "123"`)
|
||||
|
||||
testRS = strToUnstructured(`
|
||||
apiVersion: v1
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: ReplicaSet
|
||||
metadata:
|
||||
name: helm-guestbook-rs
|
||||
namespace: default
|
||||
ownerReferences:
|
||||
- apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
name: helm-guestbook
|
||||
resourceVersion: "123"`)
|
||||
|
||||
testDeploy = strToUnstructured(`
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: helm-guestbook
|
||||
name: helm-guestbook
|
||||
namespace: default
|
||||
resourceVersion: "123"`)
|
||||
)
|
||||
|
||||
func newCluster(resources ...*unstructured.Unstructured) *clusterInfo {
|
||||
return &clusterInfo{
|
||||
lock: &sync.Mutex{},
|
||||
nodes: make(map[kube.ResourceKey]*node),
|
||||
onAppUpdated: func(appName string) {},
|
||||
kubectl: kubetest.MockKubectlCmd{
|
||||
Resources: resources,
|
||||
},
|
||||
nsIndex: make(map[string]map[kube.ResourceKey]*node),
|
||||
cluster: &appv1.Cluster{},
|
||||
syncTime: nil,
|
||||
syncLock: &sync.Mutex{},
|
||||
apis: make(map[schema.GroupVersionKind]metav1.APIResource),
|
||||
log: log.WithField("cluster", "test"),
|
||||
settings: &settings.ArgoCDSettings{},
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetChildren(t *testing.T) {
|
||||
cluster := newCluster(testPod, testRS, testDeploy)
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
rsChildren := cluster.getChildren(testRS)
|
||||
assert.Equal(t, []appv1.ResourceNode{{
|
||||
Kind: "Pod",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-pod",
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Info: []appv1.InfoItem{{Name: "Containers", Value: "0/0"}},
|
||||
Children: make([]appv1.ResourceNode, 0),
|
||||
ResourceVersion: "123",
|
||||
}}, rsChildren)
|
||||
deployChildren := cluster.getChildren(testDeploy)
|
||||
|
||||
assert.Equal(t, []appv1.ResourceNode{{
|
||||
Kind: "ReplicaSet",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-rs",
|
||||
Group: "extensions",
|
||||
Version: "v1beta1",
|
||||
ResourceVersion: "123",
|
||||
Children: rsChildren,
|
||||
Info: []appv1.InfoItem{},
|
||||
}}, deployChildren)
|
||||
}
|
||||
|
||||
func TestGetManagedLiveObjs(t *testing.T) {
|
||||
cluster := newCluster(testPod, testRS, testDeploy)
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
targetDeploy := strToUnstructured(`
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: helm-guestbook
|
||||
labels:
|
||||
app: helm-guestbook`)
|
||||
|
||||
managedObjs, err := cluster.getManagedLiveObjs(&appv1.Application{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "helm-guestbook"},
|
||||
Spec: appv1.ApplicationSpec{
|
||||
Destination: appv1.ApplicationDestination{
|
||||
Namespace: "default",
|
||||
},
|
||||
},
|
||||
}, []*unstructured.Unstructured{targetDeploy})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, managedObjs, map[kube.ResourceKey]*unstructured.Unstructured{
|
||||
kube.NewResourceKey("apps", "Deployment", "default", "helm-guestbook"): testDeploy,
|
||||
})
|
||||
}
|
||||
|
||||
func TestChildDeletedEvent(t *testing.T) {
|
||||
cluster := newCluster(testPod, testRS, testDeploy)
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = cluster.processEvent(watch.Deleted, testPod)
|
||||
assert.Nil(t, err)
|
||||
|
||||
rsChildren := cluster.getChildren(testRS)
|
||||
assert.Equal(t, []appv1.ResourceNode{}, rsChildren)
|
||||
}
|
||||
|
||||
func TestProcessNewChildEvent(t *testing.T) {
|
||||
cluster := newCluster(testPod, testRS, testDeploy)
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
newPod := strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: helm-guestbook-pod2
|
||||
namespace: default
|
||||
ownerReferences:
|
||||
- apiVersion: extensions/v1beta1
|
||||
kind: ReplicaSet
|
||||
name: helm-guestbook-rs
|
||||
resourceVersion: "123"`)
|
||||
|
||||
err = cluster.processEvent(watch.Added, newPod)
|
||||
assert.Nil(t, err)
|
||||
|
||||
rsChildren := cluster.getChildren(testRS)
|
||||
sort.Slice(rsChildren, func(i, j int) bool {
|
||||
return strings.Compare(rsChildren[i].Name, rsChildren[j].Name) < 0
|
||||
})
|
||||
assert.Equal(t, []appv1.ResourceNode{{
|
||||
Kind: "Pod",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-pod",
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Info: []appv1.InfoItem{{Name: "Containers", Value: "0/0"}},
|
||||
Children: make([]appv1.ResourceNode, 0),
|
||||
ResourceVersion: "123",
|
||||
}, {
|
||||
Kind: "Pod",
|
||||
Namespace: "default",
|
||||
Name: "helm-guestbook-pod2",
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Info: []appv1.InfoItem{{Name: "Containers", Value: "0/0"}},
|
||||
Children: make([]appv1.ResourceNode, 0),
|
||||
ResourceVersion: "123",
|
||||
}}, rsChildren)
|
||||
}
|
||||
|
||||
func TestUpdateResourceTags(t *testing.T) {
|
||||
pod := &corev1.Pod{
|
||||
TypeMeta: metav1.TypeMeta{Kind: "Pod", APIVersion: "v1"},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "testPod", Namespace: "default"},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{{
|
||||
Name: "test",
|
||||
Image: "test",
|
||||
}},
|
||||
},
|
||||
}
|
||||
cluster := newCluster(mustToUnstructured(pod))
|
||||
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
podNode := cluster.nodes[kube.GetResourceKey(mustToUnstructured(pod))]
|
||||
|
||||
assert.NotNil(t, podNode)
|
||||
assert.Equal(t, []appv1.InfoItem{{Name: "Containers", Value: "0/1"}}, podNode.info)
|
||||
|
||||
pod.Status = corev1.PodStatus{
|
||||
ContainerStatuses: []corev1.ContainerStatus{{
|
||||
State: corev1.ContainerState{
|
||||
Terminated: &corev1.ContainerStateTerminated{
|
||||
ExitCode: -1,
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
err = cluster.processEvent(watch.Modified, mustToUnstructured(pod))
|
||||
assert.Nil(t, err)
|
||||
|
||||
podNode = cluster.nodes[kube.GetResourceKey(mustToUnstructured(pod))]
|
||||
|
||||
assert.NotNil(t, podNode)
|
||||
assert.Equal(t, []appv1.InfoItem{{Name: "Status Reason", Value: "ExitCode:-1"}, {Name: "Containers", Value: "0/1"}}, podNode.info)
|
||||
}
|
||||
|
||||
func TestUpdateAppResource(t *testing.T) {
|
||||
updatesReceived := make([]string, 0)
|
||||
cluster := newCluster(testPod, testRS, testDeploy)
|
||||
cluster.onAppUpdated = func(appName string) {
|
||||
updatesReceived = append(updatesReceived, appName)
|
||||
}
|
||||
|
||||
err := cluster.ensureSynced()
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = cluster.processEvent(watch.Modified, mustToUnstructured(testPod))
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, []string{"helm-guestbook"}, updatesReceived)
|
||||
}
|
||||
|
||||
func TestCircularReference(t *testing.T) {
|
||||
dep := testDeploy.DeepCopy()
|
||||
dep.SetOwnerReferences([]metav1.OwnerReference{{
|
||||
Name: testPod.GetName(),
|
||||
Kind: testPod.GetKind(),
|
||||
APIVersion: testPod.GetAPIVersion(),
|
||||
}})
|
||||
cluster := newCluster(testPod, testRS, dep)
|
||||
err := cluster.ensureSynced()
|
||||
|
||||
assert.Nil(t, err)
|
||||
|
||||
children := cluster.getChildren(dep)
|
||||
assert.Len(t, children, 1)
|
||||
}
|
||||
107
controller/cache/info.go
vendored
Normal file
107
controller/cache/info.go
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
k8snode "k8s.io/kubernetes/pkg/util/node"
|
||||
)
|
||||
|
||||
func getNodeInfo(un *unstructured.Unstructured) []v1alpha1.InfoItem {
|
||||
gvk := un.GroupVersionKind()
|
||||
|
||||
if gvk.Kind == kube.PodKind && gvk.Group == "" {
|
||||
return getPodInfo(un)
|
||||
}
|
||||
return []v1alpha1.InfoItem{}
|
||||
}
|
||||
|
||||
func getPodInfo(un *unstructured.Unstructured) []v1alpha1.InfoItem {
|
||||
pod := v1.Pod{}
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(un.Object, &pod)
|
||||
if err != nil {
|
||||
return []v1alpha1.InfoItem{}
|
||||
}
|
||||
restarts := 0
|
||||
totalContainers := len(pod.Spec.Containers)
|
||||
readyContainers := 0
|
||||
|
||||
reason := string(pod.Status.Phase)
|
||||
if pod.Status.Reason != "" {
|
||||
reason = pod.Status.Reason
|
||||
}
|
||||
|
||||
initializing := false
|
||||
for i := range pod.Status.InitContainerStatuses {
|
||||
container := pod.Status.InitContainerStatuses[i]
|
||||
restarts += int(container.RestartCount)
|
||||
switch {
|
||||
case container.State.Terminated != nil && container.State.Terminated.ExitCode == 0:
|
||||
continue
|
||||
case container.State.Terminated != nil:
|
||||
// initialization is failed
|
||||
if len(container.State.Terminated.Reason) == 0 {
|
||||
if container.State.Terminated.Signal != 0 {
|
||||
reason = fmt.Sprintf("Init:Signal:%d", container.State.Terminated.Signal)
|
||||
} else {
|
||||
reason = fmt.Sprintf("Init:ExitCode:%d", container.State.Terminated.ExitCode)
|
||||
}
|
||||
} else {
|
||||
reason = "Init:" + container.State.Terminated.Reason
|
||||
}
|
||||
initializing = true
|
||||
case container.State.Waiting != nil && len(container.State.Waiting.Reason) > 0 && container.State.Waiting.Reason != "PodInitializing":
|
||||
reason = "Init:" + container.State.Waiting.Reason
|
||||
initializing = true
|
||||
default:
|
||||
reason = fmt.Sprintf("Init:%d/%d", i, len(pod.Spec.InitContainers))
|
||||
initializing = true
|
||||
}
|
||||
break
|
||||
}
|
||||
if !initializing {
|
||||
restarts = 0
|
||||
hasRunning := false
|
||||
for i := len(pod.Status.ContainerStatuses) - 1; i >= 0; i-- {
|
||||
container := pod.Status.ContainerStatuses[i]
|
||||
|
||||
restarts += int(container.RestartCount)
|
||||
if container.State.Waiting != nil && container.State.Waiting.Reason != "" {
|
||||
reason = container.State.Waiting.Reason
|
||||
} else if container.State.Terminated != nil && container.State.Terminated.Reason != "" {
|
||||
reason = container.State.Terminated.Reason
|
||||
} else if container.State.Terminated != nil && container.State.Terminated.Reason == "" {
|
||||
if container.State.Terminated.Signal != 0 {
|
||||
reason = fmt.Sprintf("Signal:%d", container.State.Terminated.Signal)
|
||||
} else {
|
||||
reason = fmt.Sprintf("ExitCode:%d", container.State.Terminated.ExitCode)
|
||||
}
|
||||
} else if container.Ready && container.State.Running != nil {
|
||||
hasRunning = true
|
||||
readyContainers++
|
||||
}
|
||||
}
|
||||
|
||||
// change pod status back to "Running" if there is at least one container still reporting as "Running" status
|
||||
if reason == "Completed" && hasRunning {
|
||||
reason = "Running"
|
||||
}
|
||||
}
|
||||
|
||||
if pod.DeletionTimestamp != nil && pod.Status.Reason == k8snode.NodeUnreachablePodReason {
|
||||
reason = "Unknown"
|
||||
} else if pod.DeletionTimestamp != nil {
|
||||
reason = "Terminating"
|
||||
}
|
||||
|
||||
info := make([]v1alpha1.InfoItem, 0)
|
||||
if reason != "" {
|
||||
info = append(info, v1alpha1.InfoItem{Name: "Status Reason", Value: reason})
|
||||
}
|
||||
return append(info, v1alpha1.InfoItem{Name: "Containers", Value: fmt.Sprintf("%d/%d", readyContainers, totalContainers)})
|
||||
}
|
||||
106
controller/cache/mocks/LiveStateCache.go
vendored
Normal file
106
controller/cache/mocks/LiveStateCache.go
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
// Code generated by mockery v1.0.0. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import context "context"
|
||||
import kube "github.com/argoproj/argo-cd/util/kube"
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
import schema "k8s.io/apimachinery/pkg/runtime/schema"
|
||||
import unstructured "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
import v1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
|
||||
// LiveStateCache is an autogenerated mock type for the LiveStateCache type
|
||||
type LiveStateCache struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: server, obj
|
||||
func (_m *LiveStateCache) Delete(server string, obj *unstructured.Unstructured) error {
|
||||
ret := _m.Called(server, obj)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, *unstructured.Unstructured) error); ok {
|
||||
r0 = rf(server, obj)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetChildren provides a mock function with given fields: server, obj
|
||||
func (_m *LiveStateCache) GetChildren(server string, obj *unstructured.Unstructured) ([]v1alpha1.ResourceNode, error) {
|
||||
ret := _m.Called(server, obj)
|
||||
|
||||
var r0 []v1alpha1.ResourceNode
|
||||
if rf, ok := ret.Get(0).(func(string, *unstructured.Unstructured) []v1alpha1.ResourceNode); ok {
|
||||
r0 = rf(server, obj)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]v1alpha1.ResourceNode)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, *unstructured.Unstructured) error); ok {
|
||||
r1 = rf(server, obj)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetManagedLiveObjs provides a mock function with given fields: a, targetObjs
|
||||
func (_m *LiveStateCache) GetManagedLiveObjs(a *v1alpha1.Application, targetObjs []*unstructured.Unstructured) (map[kube.ResourceKey]*unstructured.Unstructured, error) {
|
||||
ret := _m.Called(a, targetObjs)
|
||||
|
||||
var r0 map[kube.ResourceKey]*unstructured.Unstructured
|
||||
if rf, ok := ret.Get(0).(func(*v1alpha1.Application, []*unstructured.Unstructured) map[kube.ResourceKey]*unstructured.Unstructured); ok {
|
||||
r0 = rf(a, targetObjs)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(map[kube.ResourceKey]*unstructured.Unstructured)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(*v1alpha1.Application, []*unstructured.Unstructured) error); ok {
|
||||
r1 = rf(a, targetObjs)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Invalidate provides a mock function with given fields:
|
||||
func (_m *LiveStateCache) Invalidate() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// IsNamespaced provides a mock function with given fields: server, gvk
|
||||
func (_m *LiveStateCache) IsNamespaced(server string, gvk schema.GroupVersionKind) (bool, error) {
|
||||
ret := _m.Called(server, gvk)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(string, schema.GroupVersionKind) bool); ok {
|
||||
r0 = rf(server, gvk)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, schema.GroupVersionKind) error); ok {
|
||||
r1 = rf(server, gvk)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Run provides a mock function with given fields: ctx
|
||||
func (_m *LiveStateCache) Run(ctx context.Context) {
|
||||
_m.Called(ctx)
|
||||
}
|
||||
100
controller/cache/node.go
vendored
Normal file
100
controller/cache/node.go
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
|
||||
"k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
type node struct {
|
||||
resourceVersion string
|
||||
ref v1.ObjectReference
|
||||
ownerRefs []metav1.OwnerReference
|
||||
info []appv1.InfoItem
|
||||
appName string
|
||||
resource *unstructured.Unstructured
|
||||
}
|
||||
|
||||
func (n *node) resourceKey() kube.ResourceKey {
|
||||
return kube.NewResourceKey(n.ref.GroupVersionKind().Group, n.ref.Kind, n.ref.Namespace, n.ref.Name)
|
||||
}
|
||||
|
||||
func (n *node) isParentOf(child *node) bool {
|
||||
ownerGvk := n.ref.GroupVersionKind()
|
||||
for _, ownerRef := range child.ownerRefs {
|
||||
if kube.NewResourceKey(ownerGvk.Group, ownerRef.Kind, n.ref.Namespace, ownerRef.Name) == n.resourceKey() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func ownerRefGV(ownerRef metav1.OwnerReference) schema.GroupVersion {
|
||||
gv, err := schema.ParseGroupVersion(ownerRef.APIVersion)
|
||||
if err != nil {
|
||||
gv = schema.GroupVersion{}
|
||||
}
|
||||
return gv
|
||||
}
|
||||
|
||||
func (n *node) getApp(ns map[kube.ResourceKey]*node) string {
|
||||
if n.appName != "" {
|
||||
return n.appName
|
||||
}
|
||||
for _, ownerRef := range n.ownerRefs {
|
||||
gv := ownerRefGV(ownerRef)
|
||||
if parent, ok := ns[kube.NewResourceKey(gv.Group, ownerRef.Kind, n.ref.Namespace, ownerRef.Name)]; ok {
|
||||
app := parent.getApp(ns)
|
||||
if app != "" {
|
||||
return app
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func newResourceKeySet(set map[kube.ResourceKey]bool, keys ...kube.ResourceKey) map[kube.ResourceKey]bool {
|
||||
newSet := make(map[kube.ResourceKey]bool)
|
||||
for k, v := range set {
|
||||
newSet[k] = v
|
||||
}
|
||||
for i := range keys {
|
||||
newSet[keys[i]] = true
|
||||
}
|
||||
return newSet
|
||||
}
|
||||
|
||||
func (n *node) childResourceNodes(ns map[kube.ResourceKey]*node, parents map[kube.ResourceKey]bool) appv1.ResourceNode {
|
||||
children := make([]appv1.ResourceNode, 0)
|
||||
for childKey := range ns {
|
||||
if n.isParentOf(ns[childKey]) {
|
||||
if parents[childKey] {
|
||||
key := n.resourceKey()
|
||||
log.Warnf("Circular dependency detected. %s is child and parent of %s", childKey.String(), key.String())
|
||||
} else {
|
||||
children = append(children, ns[childKey].childResourceNodes(ns, newResourceKeySet(parents, n.resourceKey())))
|
||||
}
|
||||
}
|
||||
}
|
||||
gv, err := schema.ParseGroupVersion(n.ref.APIVersion)
|
||||
if err != nil {
|
||||
gv = schema.GroupVersion{}
|
||||
}
|
||||
return appv1.ResourceNode{
|
||||
Name: n.ref.Name,
|
||||
Group: gv.Group,
|
||||
Version: gv.Version,
|
||||
Kind: n.ref.Kind,
|
||||
Namespace: n.ref.Namespace,
|
||||
Info: n.info,
|
||||
Children: children,
|
||||
ResourceVersion: n.resourceVersion,
|
||||
}
|
||||
}
|
||||
36
controller/clientset.go
Normal file
36
controller/clientset.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/argoproj/argo-cd/controller/services"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
|
||||
// Clientset represents controller server api clients
|
||||
type Clientset interface {
|
||||
NewApplicationServiceClient() (util.Closer, services.ApplicationServiceClient, error)
|
||||
}
|
||||
|
||||
type clientSet struct {
|
||||
address string
|
||||
}
|
||||
|
||||
func (c *clientSet) NewApplicationServiceClient() (util.Closer, services.ApplicationServiceClient, error) {
|
||||
conn, err := grpc.Dial(c.address, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{InsecureSkipVerify: true})))
|
||||
if err != nil {
|
||||
log.Errorf("Unable to connect to repository service with address %s", c.address)
|
||||
return nil, nil, err
|
||||
}
|
||||
return conn, services.NewApplicationServiceClient(conn), nil
|
||||
}
|
||||
|
||||
// NewAppControllerClientset creates new instance of controller server Clientset
|
||||
func NewAppControllerClientset(address string) Clientset {
|
||||
return &clientSet{address: address}
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
814
controller/services/application.pb.go
Normal file
814
controller/services/application.pb.go
Normal file
@@ -0,0 +1,814 @@
|
||||
// Code generated by protoc-gen-gogo. DO NOT EDIT.
|
||||
// source: controller/services/application.proto
|
||||
|
||||
package services // import "github.com/argoproj/argo-cd/controller/services"
|
||||
|
||||
import proto "github.com/gogo/protobuf/proto"
|
||||
import fmt "fmt"
|
||||
import math "math"
|
||||
import v1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
|
||||
import context "golang.org/x/net/context"
|
||||
import grpc "google.golang.org/grpc"
|
||||
|
||||
import io "io"
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ = proto.Marshal
|
||||
var _ = fmt.Errorf
|
||||
var _ = math.Inf
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the proto package it is being compiled against.
|
||||
// A compilation error at this line likely means your copy of the
|
||||
// proto package needs to be updated.
|
||||
const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package
|
||||
|
||||
type ResourcesQuery struct {
|
||||
ApplicationName string `protobuf:"bytes,1,opt,name=applicationName,proto3" json:"applicationName,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *ResourcesQuery) Reset() { *m = ResourcesQuery{} }
|
||||
func (m *ResourcesQuery) String() string { return proto.CompactTextString(m) }
|
||||
func (*ResourcesQuery) ProtoMessage() {}
|
||||
func (*ResourcesQuery) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_application_22f1e591d294b941, []int{0}
|
||||
}
|
||||
func (m *ResourcesQuery) XXX_Unmarshal(b []byte) error {
|
||||
return m.Unmarshal(b)
|
||||
}
|
||||
func (m *ResourcesQuery) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
if deterministic {
|
||||
return xxx_messageInfo_ResourcesQuery.Marshal(b, m, deterministic)
|
||||
} else {
|
||||
b = b[:cap(b)]
|
||||
n, err := m.MarshalTo(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b[:n], nil
|
||||
}
|
||||
}
|
||||
func (dst *ResourcesQuery) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_ResourcesQuery.Merge(dst, src)
|
||||
}
|
||||
func (m *ResourcesQuery) XXX_Size() int {
|
||||
return m.Size()
|
||||
}
|
||||
func (m *ResourcesQuery) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_ResourcesQuery.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_ResourcesQuery proto.InternalMessageInfo
|
||||
|
||||
func (m *ResourcesQuery) GetApplicationName() string {
|
||||
if m != nil {
|
||||
return m.ApplicationName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ResourceTreeResponse struct {
|
||||
Items []*v1alpha1.ResourceNode `protobuf:"bytes,1,rep,name=items" json:"items,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *ResourceTreeResponse) Reset() { *m = ResourceTreeResponse{} }
|
||||
func (m *ResourceTreeResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*ResourceTreeResponse) ProtoMessage() {}
|
||||
func (*ResourceTreeResponse) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_application_22f1e591d294b941, []int{1}
|
||||
}
|
||||
func (m *ResourceTreeResponse) XXX_Unmarshal(b []byte) error {
|
||||
return m.Unmarshal(b)
|
||||
}
|
||||
func (m *ResourceTreeResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
if deterministic {
|
||||
return xxx_messageInfo_ResourceTreeResponse.Marshal(b, m, deterministic)
|
||||
} else {
|
||||
b = b[:cap(b)]
|
||||
n, err := m.MarshalTo(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b[:n], nil
|
||||
}
|
||||
}
|
||||
func (dst *ResourceTreeResponse) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_ResourceTreeResponse.Merge(dst, src)
|
||||
}
|
||||
func (m *ResourceTreeResponse) XXX_Size() int {
|
||||
return m.Size()
|
||||
}
|
||||
func (m *ResourceTreeResponse) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_ResourceTreeResponse.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_ResourceTreeResponse proto.InternalMessageInfo
|
||||
|
||||
func (m *ResourceTreeResponse) GetItems() []*v1alpha1.ResourceNode {
|
||||
if m != nil {
|
||||
return m.Items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ManagedResourcesResponse struct {
|
||||
Items []*v1alpha1.ResourceDiff `protobuf:"bytes,1,rep,name=items" json:"items,omitempty"`
|
||||
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||
XXX_unrecognized []byte `json:"-"`
|
||||
XXX_sizecache int32 `json:"-"`
|
||||
}
|
||||
|
||||
func (m *ManagedResourcesResponse) Reset() { *m = ManagedResourcesResponse{} }
|
||||
func (m *ManagedResourcesResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*ManagedResourcesResponse) ProtoMessage() {}
|
||||
func (*ManagedResourcesResponse) Descriptor() ([]byte, []int) {
|
||||
return fileDescriptor_application_22f1e591d294b941, []int{2}
|
||||
}
|
||||
func (m *ManagedResourcesResponse) XXX_Unmarshal(b []byte) error {
|
||||
return m.Unmarshal(b)
|
||||
}
|
||||
func (m *ManagedResourcesResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||
if deterministic {
|
||||
return xxx_messageInfo_ManagedResourcesResponse.Marshal(b, m, deterministic)
|
||||
} else {
|
||||
b = b[:cap(b)]
|
||||
n, err := m.MarshalTo(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b[:n], nil
|
||||
}
|
||||
}
|
||||
func (dst *ManagedResourcesResponse) XXX_Merge(src proto.Message) {
|
||||
xxx_messageInfo_ManagedResourcesResponse.Merge(dst, src)
|
||||
}
|
||||
func (m *ManagedResourcesResponse) XXX_Size() int {
|
||||
return m.Size()
|
||||
}
|
||||
func (m *ManagedResourcesResponse) XXX_DiscardUnknown() {
|
||||
xxx_messageInfo_ManagedResourcesResponse.DiscardUnknown(m)
|
||||
}
|
||||
|
||||
var xxx_messageInfo_ManagedResourcesResponse proto.InternalMessageInfo
|
||||
|
||||
func (m *ManagedResourcesResponse) GetItems() []*v1alpha1.ResourceDiff {
|
||||
if m != nil {
|
||||
return m.Items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*ResourcesQuery)(nil), "github.com.argoproj.argo_cd.controller.services.ResourcesQuery")
|
||||
proto.RegisterType((*ResourceTreeResponse)(nil), "github.com.argoproj.argo_cd.controller.services.ResourceTreeResponse")
|
||||
proto.RegisterType((*ManagedResourcesResponse)(nil), "github.com.argoproj.argo_cd.controller.services.ManagedResourcesResponse")
|
||||
}
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ context.Context
|
||||
var _ grpc.ClientConn
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
const _ = grpc.SupportPackageIsVersion4
|
||||
|
||||
// Client API for ApplicationService service
|
||||
|
||||
type ApplicationServiceClient interface {
|
||||
ResourceTree(ctx context.Context, in *ResourcesQuery, opts ...grpc.CallOption) (*ResourceTreeResponse, error)
|
||||
ManagedResources(ctx context.Context, in *ResourcesQuery, opts ...grpc.CallOption) (*ManagedResourcesResponse, error)
|
||||
}
|
||||
|
||||
type applicationServiceClient struct {
|
||||
cc *grpc.ClientConn
|
||||
}
|
||||
|
||||
func NewApplicationServiceClient(cc *grpc.ClientConn) ApplicationServiceClient {
|
||||
return &applicationServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *applicationServiceClient) ResourceTree(ctx context.Context, in *ResourcesQuery, opts ...grpc.CallOption) (*ResourceTreeResponse, error) {
|
||||
out := new(ResourceTreeResponse)
|
||||
err := c.cc.Invoke(ctx, "/github.com.argoproj.argo_cd.controller.services.ApplicationService/ResourceTree", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *applicationServiceClient) ManagedResources(ctx context.Context, in *ResourcesQuery, opts ...grpc.CallOption) (*ManagedResourcesResponse, error) {
|
||||
out := new(ManagedResourcesResponse)
|
||||
err := c.cc.Invoke(ctx, "/github.com.argoproj.argo_cd.controller.services.ApplicationService/ManagedResources", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Server API for ApplicationService service
|
||||
|
||||
type ApplicationServiceServer interface {
|
||||
ResourceTree(context.Context, *ResourcesQuery) (*ResourceTreeResponse, error)
|
||||
ManagedResources(context.Context, *ResourcesQuery) (*ManagedResourcesResponse, error)
|
||||
}
|
||||
|
||||
func RegisterApplicationServiceServer(s *grpc.Server, srv ApplicationServiceServer) {
|
||||
s.RegisterService(&_ApplicationService_serviceDesc, srv)
|
||||
}
|
||||
|
||||
func _ApplicationService_ResourceTree_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ResourcesQuery)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ApplicationServiceServer).ResourceTree(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/github.com.argoproj.argo_cd.controller.services.ApplicationService/ResourceTree",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApplicationServiceServer).ResourceTree(ctx, req.(*ResourcesQuery))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _ApplicationService_ManagedResources_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ResourcesQuery)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ApplicationServiceServer).ManagedResources(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/github.com.argoproj.argo_cd.controller.services.ApplicationService/ManagedResources",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ApplicationServiceServer).ManagedResources(ctx, req.(*ResourcesQuery))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
var _ApplicationService_serviceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "github.com.argoproj.argo_cd.controller.services.ApplicationService",
|
||||
HandlerType: (*ApplicationServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "ResourceTree",
|
||||
Handler: _ApplicationService_ResourceTree_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ManagedResources",
|
||||
Handler: _ApplicationService_ManagedResources_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "controller/services/application.proto",
|
||||
}
|
||||
|
||||
func (m *ResourcesQuery) Marshal() (dAtA []byte, err error) {
|
||||
size := m.Size()
|
||||
dAtA = make([]byte, size)
|
||||
n, err := m.MarshalTo(dAtA)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dAtA[:n], nil
|
||||
}
|
||||
|
||||
func (m *ResourcesQuery) MarshalTo(dAtA []byte) (int, error) {
|
||||
var i int
|
||||
_ = i
|
||||
var l int
|
||||
_ = l
|
||||
if len(m.ApplicationName) > 0 {
|
||||
dAtA[i] = 0xa
|
||||
i++
|
||||
i = encodeVarintApplication(dAtA, i, uint64(len(m.ApplicationName)))
|
||||
i += copy(dAtA[i:], m.ApplicationName)
|
||||
}
|
||||
if m.XXX_unrecognized != nil {
|
||||
i += copy(dAtA[i:], m.XXX_unrecognized)
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (m *ResourceTreeResponse) Marshal() (dAtA []byte, err error) {
|
||||
size := m.Size()
|
||||
dAtA = make([]byte, size)
|
||||
n, err := m.MarshalTo(dAtA)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dAtA[:n], nil
|
||||
}
|
||||
|
||||
func (m *ResourceTreeResponse) MarshalTo(dAtA []byte) (int, error) {
|
||||
var i int
|
||||
_ = i
|
||||
var l int
|
||||
_ = l
|
||||
if len(m.Items) > 0 {
|
||||
for _, msg := range m.Items {
|
||||
dAtA[i] = 0xa
|
||||
i++
|
||||
i = encodeVarintApplication(dAtA, i, uint64(msg.Size()))
|
||||
n, err := msg.MarshalTo(dAtA[i:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
i += n
|
||||
}
|
||||
}
|
||||
if m.XXX_unrecognized != nil {
|
||||
i += copy(dAtA[i:], m.XXX_unrecognized)
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (m *ManagedResourcesResponse) Marshal() (dAtA []byte, err error) {
|
||||
size := m.Size()
|
||||
dAtA = make([]byte, size)
|
||||
n, err := m.MarshalTo(dAtA)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dAtA[:n], nil
|
||||
}
|
||||
|
||||
func (m *ManagedResourcesResponse) MarshalTo(dAtA []byte) (int, error) {
|
||||
var i int
|
||||
_ = i
|
||||
var l int
|
||||
_ = l
|
||||
if len(m.Items) > 0 {
|
||||
for _, msg := range m.Items {
|
||||
dAtA[i] = 0xa
|
||||
i++
|
||||
i = encodeVarintApplication(dAtA, i, uint64(msg.Size()))
|
||||
n, err := msg.MarshalTo(dAtA[i:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
i += n
|
||||
}
|
||||
}
|
||||
if m.XXX_unrecognized != nil {
|
||||
i += copy(dAtA[i:], m.XXX_unrecognized)
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func encodeVarintApplication(dAtA []byte, offset int, v uint64) int {
|
||||
for v >= 1<<7 {
|
||||
dAtA[offset] = uint8(v&0x7f | 0x80)
|
||||
v >>= 7
|
||||
offset++
|
||||
}
|
||||
dAtA[offset] = uint8(v)
|
||||
return offset + 1
|
||||
}
|
||||
func (m *ResourcesQuery) Size() (n int) {
|
||||
var l int
|
||||
_ = l
|
||||
l = len(m.ApplicationName)
|
||||
if l > 0 {
|
||||
n += 1 + l + sovApplication(uint64(l))
|
||||
}
|
||||
if m.XXX_unrecognized != nil {
|
||||
n += len(m.XXX_unrecognized)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (m *ResourceTreeResponse) Size() (n int) {
|
||||
var l int
|
||||
_ = l
|
||||
if len(m.Items) > 0 {
|
||||
for _, e := range m.Items {
|
||||
l = e.Size()
|
||||
n += 1 + l + sovApplication(uint64(l))
|
||||
}
|
||||
}
|
||||
if m.XXX_unrecognized != nil {
|
||||
n += len(m.XXX_unrecognized)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (m *ManagedResourcesResponse) Size() (n int) {
|
||||
var l int
|
||||
_ = l
|
||||
if len(m.Items) > 0 {
|
||||
for _, e := range m.Items {
|
||||
l = e.Size()
|
||||
n += 1 + l + sovApplication(uint64(l))
|
||||
}
|
||||
}
|
||||
if m.XXX_unrecognized != nil {
|
||||
n += len(m.XXX_unrecognized)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func sovApplication(x uint64) (n int) {
|
||||
for {
|
||||
n++
|
||||
x >>= 7
|
||||
if x == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
func sozApplication(x uint64) (n int) {
|
||||
return sovApplication(uint64((x << 1) ^ uint64((int64(x) >> 63))))
|
||||
}
|
||||
func (m *ResourcesQuery) Unmarshal(dAtA []byte) error {
|
||||
l := len(dAtA)
|
||||
iNdEx := 0
|
||||
for iNdEx < l {
|
||||
preIndex := iNdEx
|
||||
var wire uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowApplication
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
wire |= (uint64(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
fieldNum := int32(wire >> 3)
|
||||
wireType := int(wire & 0x7)
|
||||
if wireType == 4 {
|
||||
return fmt.Errorf("proto: ResourcesQuery: wiretype end group for non-group")
|
||||
}
|
||||
if fieldNum <= 0 {
|
||||
return fmt.Errorf("proto: ResourcesQuery: illegal tag %d (wire type %d)", fieldNum, wire)
|
||||
}
|
||||
switch fieldNum {
|
||||
case 1:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field ApplicationName", wireType)
|
||||
}
|
||||
var stringLen uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowApplication
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
stringLen |= (uint64(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
intStringLen := int(stringLen)
|
||||
if intStringLen < 0 {
|
||||
return ErrInvalidLengthApplication
|
||||
}
|
||||
postIndex := iNdEx + intStringLen
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.ApplicationName = string(dAtA[iNdEx:postIndex])
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipApplication(dAtA[iNdEx:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if skippy < 0 {
|
||||
return ErrInvalidLengthApplication
|
||||
}
|
||||
if (iNdEx + skippy) > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...)
|
||||
iNdEx += skippy
|
||||
}
|
||||
}
|
||||
|
||||
if iNdEx > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (m *ResourceTreeResponse) Unmarshal(dAtA []byte) error {
|
||||
l := len(dAtA)
|
||||
iNdEx := 0
|
||||
for iNdEx < l {
|
||||
preIndex := iNdEx
|
||||
var wire uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowApplication
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
wire |= (uint64(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
fieldNum := int32(wire >> 3)
|
||||
wireType := int(wire & 0x7)
|
||||
if wireType == 4 {
|
||||
return fmt.Errorf("proto: ResourceTreeResponse: wiretype end group for non-group")
|
||||
}
|
||||
if fieldNum <= 0 {
|
||||
return fmt.Errorf("proto: ResourceTreeResponse: illegal tag %d (wire type %d)", fieldNum, wire)
|
||||
}
|
||||
switch fieldNum {
|
||||
case 1:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field Items", wireType)
|
||||
}
|
||||
var msglen int
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowApplication
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
msglen |= (int(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if msglen < 0 {
|
||||
return ErrInvalidLengthApplication
|
||||
}
|
||||
postIndex := iNdEx + msglen
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.Items = append(m.Items, &v1alpha1.ResourceNode{})
|
||||
if err := m.Items[len(m.Items)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
|
||||
return err
|
||||
}
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipApplication(dAtA[iNdEx:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if skippy < 0 {
|
||||
return ErrInvalidLengthApplication
|
||||
}
|
||||
if (iNdEx + skippy) > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...)
|
||||
iNdEx += skippy
|
||||
}
|
||||
}
|
||||
|
||||
if iNdEx > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (m *ManagedResourcesResponse) Unmarshal(dAtA []byte) error {
|
||||
l := len(dAtA)
|
||||
iNdEx := 0
|
||||
for iNdEx < l {
|
||||
preIndex := iNdEx
|
||||
var wire uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowApplication
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
wire |= (uint64(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
fieldNum := int32(wire >> 3)
|
||||
wireType := int(wire & 0x7)
|
||||
if wireType == 4 {
|
||||
return fmt.Errorf("proto: ManagedResourcesResponse: wiretype end group for non-group")
|
||||
}
|
||||
if fieldNum <= 0 {
|
||||
return fmt.Errorf("proto: ManagedResourcesResponse: illegal tag %d (wire type %d)", fieldNum, wire)
|
||||
}
|
||||
switch fieldNum {
|
||||
case 1:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field Items", wireType)
|
||||
}
|
||||
var msglen int
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowApplication
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
msglen |= (int(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
if msglen < 0 {
|
||||
return ErrInvalidLengthApplication
|
||||
}
|
||||
postIndex := iNdEx + msglen
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.Items = append(m.Items, &v1alpha1.ResourceDiff{})
|
||||
if err := m.Items[len(m.Items)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil {
|
||||
return err
|
||||
}
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipApplication(dAtA[iNdEx:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if skippy < 0 {
|
||||
return ErrInvalidLengthApplication
|
||||
}
|
||||
if (iNdEx + skippy) > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...)
|
||||
iNdEx += skippy
|
||||
}
|
||||
}
|
||||
|
||||
if iNdEx > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func skipApplication(dAtA []byte) (n int, err error) {
|
||||
l := len(dAtA)
|
||||
iNdEx := 0
|
||||
for iNdEx < l {
|
||||
var wire uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return 0, ErrIntOverflowApplication
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
wire |= (uint64(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
wireType := int(wire & 0x7)
|
||||
switch wireType {
|
||||
case 0:
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return 0, ErrIntOverflowApplication
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
iNdEx++
|
||||
if dAtA[iNdEx-1] < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return iNdEx, nil
|
||||
case 1:
|
||||
iNdEx += 8
|
||||
return iNdEx, nil
|
||||
case 2:
|
||||
var length int
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return 0, ErrIntOverflowApplication
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
length |= (int(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
iNdEx += length
|
||||
if length < 0 {
|
||||
return 0, ErrInvalidLengthApplication
|
||||
}
|
||||
return iNdEx, nil
|
||||
case 3:
|
||||
for {
|
||||
var innerWire uint64
|
||||
var start int = iNdEx
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return 0, ErrIntOverflowApplication
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return 0, io.ErrUnexpectedEOF
|
||||
}
|
||||
b := dAtA[iNdEx]
|
||||
iNdEx++
|
||||
innerWire |= (uint64(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
innerWireType := int(innerWire & 0x7)
|
||||
if innerWireType == 4 {
|
||||
break
|
||||
}
|
||||
next, err := skipApplication(dAtA[start:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
iNdEx = start + next
|
||||
}
|
||||
return iNdEx, nil
|
||||
case 4:
|
||||
return iNdEx, nil
|
||||
case 5:
|
||||
iNdEx += 4
|
||||
return iNdEx, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("proto: illegal wireType %d", wireType)
|
||||
}
|
||||
}
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
var (
|
||||
ErrInvalidLengthApplication = fmt.Errorf("proto: negative length found during unmarshaling")
|
||||
ErrIntOverflowApplication = fmt.Errorf("proto: integer overflow")
|
||||
)
|
||||
|
||||
func init() {
|
||||
proto.RegisterFile("controller/services/application.proto", fileDescriptor_application_22f1e591d294b941)
|
||||
}
|
||||
|
||||
var fileDescriptor_application_22f1e591d294b941 = []byte{
|
||||
// 337 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x53, 0xcd, 0x4a, 0x33, 0x31,
|
||||
0x14, 0x6d, 0xbe, 0x0f, 0x05, 0xa3, 0xa8, 0x04, 0x17, 0xa5, 0x8b, 0x52, 0x06, 0x84, 0x6e, 0x4c,
|
||||
0x68, 0xdd, 0x09, 0x22, 0x8a, 0x22, 0x5d, 0x58, 0x70, 0x74, 0x25, 0x88, 0xa4, 0x99, 0xdb, 0x34,
|
||||
0x76, 0x3a, 0x09, 0x49, 0xa6, 0xd0, 0x37, 0x71, 0xe9, 0xe3, 0xb8, 0x14, 0x9f, 0x40, 0xfa, 0x24,
|
||||
0x62, 0xeb, 0x38, 0xd3, 0xa2, 0x85, 0x2a, 0xee, 0x2e, 0x21, 0xe7, 0x27, 0xe7, 0xdc, 0xe0, 0x5d,
|
||||
0xa1, 0x13, 0x6f, 0x75, 0x1c, 0x83, 0x65, 0x0e, 0xec, 0x50, 0x09, 0x70, 0x8c, 0x1b, 0x13, 0x2b,
|
||||
0xc1, 0xbd, 0xd2, 0x09, 0x35, 0x56, 0x7b, 0x4d, 0x98, 0x54, 0xbe, 0x97, 0x76, 0xa8, 0xd0, 0x03,
|
||||
0xca, 0xad, 0xd4, 0xc6, 0xea, 0xfb, 0xc9, 0x70, 0x27, 0x22, 0x9a, 0x53, 0xd0, 0x8c, 0xa2, 0xd2,
|
||||
0xca, 0x01, 0x2c, 0x03, 0x4c, 0x86, 0x3d, 0x11, 0x31, 0xd3, 0x97, 0x8c, 0x1b, 0x35, 0x23, 0xc4,
|
||||
0x86, 0x0d, 0x1e, 0x9b, 0x1e, 0x6f, 0x30, 0x09, 0x09, 0x58, 0xee, 0x21, 0x9a, 0x6a, 0x07, 0x07,
|
||||
0x78, 0x33, 0x04, 0xa7, 0x53, 0x2b, 0xc0, 0x5d, 0xa6, 0x60, 0x47, 0xa4, 0x8e, 0xb7, 0x0a, 0xc8,
|
||||
0x36, 0x1f, 0x40, 0x19, 0xd5, 0x50, 0x7d, 0x2d, 0x9c, 0x3f, 0x0e, 0x52, 0xbc, 0x93, 0x61, 0xaf,
|
||||
0x2d, 0x40, 0x08, 0xce, 0xe8, 0xc4, 0x01, 0xb9, 0xc5, 0x2b, 0xca, 0xc3, 0xc0, 0x95, 0x51, 0xed,
|
||||
0x7f, 0x7d, 0xbd, 0x79, 0x4e, 0x17, 0xbd, 0xcf, 0xf4, 0x25, 0x7d, 0xb7, 0x4b, 0x8b, 0xb9, 0x64,
|
||||
0x76, 0x69, 0xc6, 0xdf, 0xd6, 0x11, 0x84, 0x53, 0xd6, 0x60, 0x84, 0xcb, 0x17, 0x3c, 0xe1, 0x12,
|
||||
0xa2, 0x4f, 0xe7, 0x7f, 0x29, 0x7d, 0xaa, 0xba, 0xdd, 0x0f, 0xe9, 0xe6, 0xcb, 0x3f, 0x4c, 0x8e,
|
||||
0xf3, 0xcb, 0x57, 0xd3, 0x42, 0xc8, 0x03, 0xc2, 0x1b, 0xc5, 0x24, 0xc8, 0x11, 0x5d, 0xb2, 0x52,
|
||||
0x3a, 0x5b, 0x42, 0xe5, 0xec, 0xc7, 0x04, 0xc5, 0x26, 0x82, 0x12, 0x79, 0x44, 0x78, 0x7b, 0x3e,
|
||||
0xad, 0xdf, 0xdb, 0x6b, 0x2d, 0x4d, 0xf0, 0x5d, 0x63, 0x41, 0xe9, 0xe4, 0xf0, 0x69, 0x5c, 0x45,
|
||||
0xcf, 0xe3, 0x2a, 0x7a, 0x1d, 0x57, 0xd1, 0x0d, 0x5b, 0xb4, 0xdb, 0x5f, 0xfc, 0xa7, 0xce, 0xea,
|
||||
0x64, 0x91, 0xf7, 0xdf, 0x02, 0x00, 0x00, 0xff, 0xff, 0x6c, 0x5d, 0xbf, 0x7d, 0x6d, 0x03, 0x00,
|
||||
0x00,
|
||||
}
|
||||
29
controller/services/application.proto
Normal file
29
controller/services/application.proto
Normal file
@@ -0,0 +1,29 @@
|
||||
syntax = "proto3";
|
||||
option go_package = "github.com/argoproj/argo-cd/controller/services";
|
||||
|
||||
package github.com.argoproj.argo_cd.controller.services;
|
||||
|
||||
import "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1/generated.proto";
|
||||
|
||||
message ResourcesQuery {
|
||||
string applicationName = 1;
|
||||
}
|
||||
|
||||
message ResourceTreeResponse {
|
||||
repeated github.com.argoproj.argo_cd.pkg.apis.application.v1alpha1.ResourceNode items = 1;
|
||||
}
|
||||
|
||||
message ManagedResourcesResponse {
|
||||
repeated github.com.argoproj.argo_cd.pkg.apis.application.v1alpha1.ResourceDiff items = 1;
|
||||
}
|
||||
|
||||
|
||||
// ApplicationService returns information about application
|
||||
service ApplicationService {
|
||||
|
||||
rpc ResourceTree(ResourcesQuery) returns (ResourceTreeResponse) {
|
||||
}
|
||||
|
||||
rpc ManagedResources(ResourcesQuery) returns (ManagedResourcesResponse) {
|
||||
}
|
||||
}
|
||||
@@ -10,84 +10,81 @@ import (
|
||||
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"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
statecache "github.com/argoproj/argo-cd/controller/cache"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
appclientset "github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
|
||||
"github.com/argoproj/argo-cd/reposerver"
|
||||
"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"
|
||||
"github.com/argoproj/argo-cd/util/health"
|
||||
hookutil "github.com/argoproj/argo-cd/util/hook"
|
||||
kubeutil "github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/settings"
|
||||
)
|
||||
|
||||
const (
|
||||
maxHistoryCnt = 5
|
||||
)
|
||||
type managedResource struct {
|
||||
Target *unstructured.Unstructured
|
||||
Live *unstructured.Unstructured
|
||||
Diff diff.DiffResult
|
||||
Group string
|
||||
Version string
|
||||
Kind string
|
||||
Namespace string
|
||||
Name string
|
||||
Hook bool
|
||||
}
|
||||
|
||||
func GetLiveObjs(res []managedResource) []*unstructured.Unstructured {
|
||||
objs := make([]*unstructured.Unstructured, len(res))
|
||||
for i := range res {
|
||||
objs[i] = res[i].Live
|
||||
}
|
||||
return objs
|
||||
}
|
||||
|
||||
// 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)
|
||||
CompareAppState(app *v1alpha1.Application, revision string, overrides []v1alpha1.ComponentParameter, noCache bool) (*comparisonResult, 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
|
||||
type comparisonResult struct {
|
||||
observedAt metav1.Time
|
||||
syncStatus *v1alpha1.SyncStatus
|
||||
healthStatus *v1alpha1.HealthStatus
|
||||
resources []v1alpha1.ResourceStatus
|
||||
managedResources []managedResource
|
||||
conditions []v1alpha1.ApplicationCondition
|
||||
hooks []*unstructured.Unstructured
|
||||
}
|
||||
|
||||
// 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
|
||||
// appStateManager allows to compare applications to git
|
||||
type appStateManager struct {
|
||||
db db.ArgoDB
|
||||
settings *settings.ArgoCDSettings
|
||||
appclientset appclientset.Interface
|
||||
projInformer cache.SharedIndexInformer
|
||||
kubectl kubeutil.Kubectl
|
||||
repoClientset reposerver.Clientset
|
||||
liveStateCache statecache.LiveStateCache
|
||||
namespace string
|
||||
}
|
||||
|
||||
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()
|
||||
func (m *appStateManager) getRepoObjs(app *v1alpha1.Application, appLabelKey, revision string, overrides []v1alpha1.ComponentParameter, noCache bool) ([]*unstructured.Unstructured, []*unstructured.Unstructured, *repository.ManifestResponse, error) {
|
||||
helmRepos, err := m.db.ListHelmRepos(context.Background())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
repo := m.getRepo(app.Spec.Source.RepoURL)
|
||||
conn, repoClient, err := m.repoClientset.NewRepositoryClient()
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
defer util.Close(conn)
|
||||
|
||||
@@ -115,253 +112,183 @@ func (s *ksonnetAppStateManager) getTargetObjs(app *v1alpha1.Application, revisi
|
||||
|
||||
manifestInfo, err := repoClient.GenerateManifest(context.Background(), &repository.ManifestRequest{
|
||||
Repo: repo,
|
||||
Environment: app.Spec.Source.Environment,
|
||||
Path: app.Spec.Source.Path,
|
||||
HelmRepos: helmRepos,
|
||||
Revision: revision,
|
||||
NoCache: noCache,
|
||||
ComponentParameterOverrides: mfReqOverrides,
|
||||
AppLabel: app.Name,
|
||||
ValueFiles: app.Spec.Source.ValuesFiles,
|
||||
AppLabelKey: appLabelKey,
|
||||
AppLabelValue: app.Name,
|
||||
Namespace: app.Spec.Destination.Namespace,
|
||||
ApplicationSource: &app.Spec.Source,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
targetObjs := make([]*unstructured.Unstructured, 0)
|
||||
hooks := make([]*unstructured.Unstructured, 0)
|
||||
for _, manifest := range manifestInfo.Manifests {
|
||||
obj, err := v1alpha1.UnmarshalToUnstructured(manifest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
if isHook(obj) {
|
||||
continue
|
||||
if hookutil.IsHook(obj) {
|
||||
hooks = append(hooks, obj)
|
||||
} else {
|
||||
targetObjs = append(targetObjs, obj)
|
||||
}
|
||||
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
|
||||
return targetObjs, hooks, manifestInfo, 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) {
|
||||
|
||||
func (m *appStateManager) CompareAppState(app *v1alpha1.Application, revision string, overrides []v1alpha1.ComponentParameter, noCache bool) (*comparisonResult, error) {
|
||||
logCtx := log.WithField("application", app.Name)
|
||||
logCtx.Infof("Comparing app state (cluster: %s, namespace: %s)", app.Spec.Destination.Server, app.Spec.Destination.Namespace)
|
||||
observedAt := metav1.Now()
|
||||
failedToLoadObjs := false
|
||||
conditions := make([]v1alpha1.ApplicationCondition, 0)
|
||||
targetObjs, manifestInfo, err := s.getTargetObjs(app, revision, overrides)
|
||||
appLabelKey := m.settings.GetAppInstanceLabelKey()
|
||||
targetObjs, hooks, manifestInfo, err := m.getRepoObjs(app, appLabelKey, revision, overrides, noCache)
|
||||
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)
|
||||
logCtx.Debugf("Generated config manifests")
|
||||
liveObjByKey, err := m.liveStateCache.GetManagedLiveObjs(app, targetObjs)
|
||||
if err != nil {
|
||||
controlledLiveObj = make([]*unstructured.Unstructured, len(targetObjs))
|
||||
liveObjByFullName = make(map[string]*unstructured.Unstructured)
|
||||
liveObjByKey = make(map[kubeutil.ResourceKey]*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 {
|
||||
logCtx.Debugf("Retrieved lived manifests")
|
||||
for _, liveObj := range liveObjByKey {
|
||||
if liveObj != nil {
|
||||
appInstanceName := kubeutil.GetAppInstanceLabel(liveObj, appLabelKey)
|
||||
if appInstanceName != "" && appInstanceName != app.Name {
|
||||
conditions = append(conditions, v1alpha1.ApplicationCondition{
|
||||
Type: v1alpha1.ApplicationConditionSharedResourceWarning,
|
||||
Message: fmt.Sprintf("Resource %s/%s is controller by applications '%s' and '%s'", liveObj.GetKind(), liveObj.GetName(), app.Name, appLabelVal),
|
||||
Message: fmt.Sprintf("%s/%s is part of a different application: %s", liveObj.GetKind(), liveObj.GetName(), appInstanceName),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
managedLiveObj := make([]*unstructured.Unstructured, len(targetObjs))
|
||||
for i, obj := range targetObjs {
|
||||
gvk := obj.GroupVersionKind()
|
||||
ns := util.FirstNonEmpty(obj.GetNamespace(), app.Spec.Destination.Namespace)
|
||||
if namespaced, err := m.liveStateCache.IsNamespaced(app.Spec.Destination.Server, obj.GroupVersionKind()); err == nil && !namespaced {
|
||||
ns = ""
|
||||
}
|
||||
key := kubeutil.NewResourceKey(gvk.Group, gvk.Kind, ns, obj.GetName())
|
||||
if liveObj, ok := liveObjByKey[key]; ok {
|
||||
managedLiveObj[i] = liveObj
|
||||
delete(liveObjByKey, key)
|
||||
} else {
|
||||
managedLiveObj[i] = nil
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("Comparing app %s state in cluster %s (namespace: %s)", app.ObjectMeta.Name, app.Spec.Destination.Server, app.Spec.Destination.Namespace)
|
||||
logCtx.Debugf("built managed objects list")
|
||||
// Everything remaining in liveObjByKey are "extra" resources that aren't tracked in git.
|
||||
// The following adds all the extras to the managedLiveObj list and backfills the targetObj
|
||||
// list with nils, so that the lists are of equal lengths for comparison purposes.
|
||||
for _, obj := range liveObjByKey {
|
||||
targetObjs = append(targetObjs, nil)
|
||||
managedLiveObj = append(managedLiveObj, obj)
|
||||
}
|
||||
|
||||
// Do the actual comparison
|
||||
diffResults, err := diff.DiffArray(targetObjs, controlledLiveObj)
|
||||
diffResults, err := diff.DiffArray(targetObjs, managedLiveObj)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
comparisonStatus := v1alpha1.ComparisonStatusSynced
|
||||
|
||||
resources := make([]v1alpha1.ResourceState, len(targetObjs))
|
||||
syncCode := v1alpha1.SyncStatusCodeSynced
|
||||
managedResources := make([]managedResource, len(targetObjs))
|
||||
resourceSummaries := make([]v1alpha1.ResourceStatus, len(targetObjs))
|
||||
for i := 0; i < len(targetObjs); i++ {
|
||||
resState := v1alpha1.ResourceState{
|
||||
ChildLiveResources: make([]v1alpha1.ResourceNode, 0),
|
||||
obj := managedLiveObj[i]
|
||||
if obj == nil {
|
||||
obj = targetObjs[i]
|
||||
}
|
||||
if obj == nil {
|
||||
continue
|
||||
}
|
||||
gvk := obj.GroupVersionKind()
|
||||
|
||||
resState := v1alpha1.ResourceStatus{
|
||||
Namespace: util.FirstNonEmpty(obj.GetNamespace(), app.Spec.Destination.Namespace),
|
||||
Name: obj.GetName(),
|
||||
Kind: gvk.Kind,
|
||||
Version: gvk.Version,
|
||||
Group: gvk.Group,
|
||||
Hook: hookutil.IsHook(obj),
|
||||
}
|
||||
|
||||
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
|
||||
if resState.Hook {
|
||||
// For resource hooks, don't store sync status, and do not affect overall sync status
|
||||
} else if diffResult.Modified || targetObjs[i] == nil || managedLiveObj[i] == nil {
|
||||
// Set resource state to OutOfSync since one of the following is true:
|
||||
// * target and live resource are different
|
||||
// * target resource not defined and live resource is extra
|
||||
// * target resource present but live resource is missing
|
||||
resState.Status = v1alpha1.SyncStatusCodeOutOfSync
|
||||
syncCode = v1alpha1.SyncStatusCodeOutOfSync
|
||||
} else {
|
||||
resState.Status = v1alpha1.ComparisonStatusSynced
|
||||
resState.Status = v1alpha1.SyncStatusCodeSynced
|
||||
}
|
||||
|
||||
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)
|
||||
managedResources[i] = managedResource{
|
||||
Name: resState.Name,
|
||||
Namespace: resState.Namespace,
|
||||
Group: resState.Group,
|
||||
Kind: resState.Kind,
|
||||
Version: resState.Version,
|
||||
Live: managedLiveObj[i],
|
||||
Target: targetObjs[i],
|
||||
Diff: diffResult,
|
||||
Hook: resState.Hook,
|
||||
}
|
||||
|
||||
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
|
||||
resourceSummaries[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
|
||||
syncCode = v1alpha1.SyncStatusCodeUnknown
|
||||
}
|
||||
compResult := v1alpha1.ComparisonResult{
|
||||
ComparedTo: app.Spec.Source,
|
||||
ComparedAt: metav1.Time{Time: time.Now().UTC()},
|
||||
Resources: resources,
|
||||
Status: comparisonStatus,
|
||||
syncStatus := v1alpha1.SyncStatus{
|
||||
ComparedTo: appv1.ComparedTo{
|
||||
Source: app.Spec.Source,
|
||||
Destination: app.Spec.Destination,
|
||||
},
|
||||
Status: syncCode,
|
||||
}
|
||||
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()
|
||||
if manifestInfo != nil {
|
||||
syncStatus.Revision = manifestInfo.Revision
|
||||
}
|
||||
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)
|
||||
}
|
||||
healthStatus, err := health.SetApplicationHealth(resourceSummaries, GetLiveObjs(managedResources))
|
||||
if err != nil {
|
||||
conditions = append(conditions, appv1.ApplicationCondition{Type: v1alpha1.ApplicationConditionComparisonError, Message: err.Error()})
|
||||
}
|
||||
return children, nil
|
||||
|
||||
compRes := comparisonResult{
|
||||
observedAt: observedAt,
|
||||
syncStatus: &syncStatus,
|
||||
healthStatus: healthStatus,
|
||||
resources: resourceSummaries,
|
||||
managedResources: managedResources,
|
||||
conditions: conditions,
|
||||
hooks: hooks,
|
||||
}
|
||||
return &compRes, 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)
|
||||
func (m *appStateManager) getRepo(repoURL string) *v1alpha1.Repository {
|
||||
repo, err := m.db.GetRepository(context.Background(), repoURL)
|
||||
if err != nil {
|
||||
// If we couldn't retrieve from the repo service, assume public repositories
|
||||
repo = &v1alpha1.Repository{Repo: repoURL}
|
||||
@@ -369,31 +296,27 @@ func (s *ksonnetAppStateManager) getRepo(repoURL string) *v1alpha1.Repository {
|
||||
return repo
|
||||
}
|
||||
|
||||
func (s *ksonnetAppStateManager) persistDeploymentInfo(
|
||||
app *v1alpha1.Application, revision string, envParams []*v1alpha1.ComponentParameter, overrides *[]v1alpha1.ComponentParameter) error {
|
||||
func (m *appStateManager) persistRevisionHistory(app *v1alpha1.Application, revision string, 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,
|
||||
if overrides == nil {
|
||||
overrides = app.Spec.Source.ComponentParameterOverrides
|
||||
}
|
||||
history := append(app.Status.History, v1alpha1.RevisionHistory{
|
||||
ComponentParameterOverrides: overrides,
|
||||
Revision: revision,
|
||||
Params: params,
|
||||
DeployedAt: metav1.NewTime(time.Now().UTC()),
|
||||
ID: nextID,
|
||||
})
|
||||
|
||||
if len(history) > maxHistoryCnt {
|
||||
history = history[1 : maxHistoryCnt+1]
|
||||
if len(history) > common.RevisionHistoryLimit {
|
||||
history = history[1 : common.RevisionHistoryLimit+1]
|
||||
}
|
||||
|
||||
patch, err := json.Marshal(map[string]map[string][]v1alpha1.DeploymentInfo{
|
||||
patch, err := json.Marshal(map[string]map[string][]v1alpha1.RevisionHistory{
|
||||
"status": {
|
||||
"history": history,
|
||||
},
|
||||
@@ -401,7 +324,7 @@ func (s *ksonnetAppStateManager) persistDeploymentInfo(
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.appclientset.ArgoprojV1alpha1().Applications(s.namespace).Patch(app.Name, types.MergePatchType, patch)
|
||||
_, err = m.appclientset.ArgoprojV1alpha1().Applications(m.namespace).Patch(app.Name, types.MergePatchType, patch)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -411,11 +334,19 @@ func NewAppStateManager(
|
||||
appclientset appclientset.Interface,
|
||||
repoClientset reposerver.Clientset,
|
||||
namespace string,
|
||||
kubectl kubeutil.Kubectl,
|
||||
settings *settings.ArgoCDSettings,
|
||||
liveStateCache statecache.LiveStateCache,
|
||||
projInformer cache.SharedIndexInformer,
|
||||
) AppStateManager {
|
||||
return &ksonnetAppStateManager{
|
||||
db: db,
|
||||
appclientset: appclientset,
|
||||
repoClientset: repoClientset,
|
||||
namespace: namespace,
|
||||
return &appStateManager{
|
||||
liveStateCache: liveStateCache,
|
||||
db: db,
|
||||
appclientset: appclientset,
|
||||
kubectl: kubectl,
|
||||
repoClientset: repoClientset,
|
||||
namespace: namespace,
|
||||
settings: settings,
|
||||
projInformer: projInformer,
|
||||
}
|
||||
}
|
||||
|
||||
143
controller/state_test.go
Normal file
143
controller/state_test.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
argoappv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/reposerver/repository"
|
||||
"github.com/argoproj/argo-cd/test"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
)
|
||||
|
||||
// TestCompareAppStateEmpty tests comparison when both git and live have no objects
|
||||
func TestCompareAppStateEmpty(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", nil, false)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Equal(t, 0, len(compRes.resources))
|
||||
assert.Equal(t, 0, len(compRes.managedResources))
|
||||
assert.Equal(t, 0, len(compRes.conditions))
|
||||
}
|
||||
|
||||
// TestCompareAppStateMissing tests when there is a manifest defined in git which doesn't exist in live
|
||||
func TestCompareAppStateMissing(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
apps: []runtime.Object{app},
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
Manifests: []string{string(test.PodManifest)},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", nil, false)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeOutOfSync, compRes.syncStatus.Status)
|
||||
assert.Equal(t, 1, len(compRes.resources))
|
||||
assert.Equal(t, 1, len(compRes.managedResources))
|
||||
assert.Equal(t, 0, len(compRes.conditions))
|
||||
}
|
||||
|
||||
// TestCompareAppStateExtra tests when there is an extra object in live but not defined in git
|
||||
func TestCompareAppStateExtra(t *testing.T) {
|
||||
pod := test.NewPod()
|
||||
pod.SetNamespace(test.FakeDestNamespace)
|
||||
app := newFakeApp()
|
||||
key := kube.ResourceKey{Group: "", Kind: "Pod", Namespace: test.FakeDestNamespace, Name: app.Name}
|
||||
data := fakeData{
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
||||
key: pod,
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", nil, false)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeOutOfSync, compRes.syncStatus.Status)
|
||||
assert.Equal(t, 1, len(compRes.resources))
|
||||
assert.Equal(t, 1, len(compRes.managedResources))
|
||||
assert.Equal(t, 0, len(compRes.conditions))
|
||||
}
|
||||
|
||||
// TestCompareAppStateHook checks that hooks are detected during manifest generation, and not
|
||||
// considered as part of resources when assessing Synced status
|
||||
func TestCompareAppStateHook(t *testing.T) {
|
||||
pod := test.NewPod()
|
||||
pod.SetAnnotations(map[string]string{common.AnnotationKeyHook: "PreSync"})
|
||||
podBytes, _ := json.Marshal(pod)
|
||||
app := newFakeApp()
|
||||
data := fakeData{
|
||||
apps: []runtime.Object{app},
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
Manifests: []string{string(podBytes)},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", nil, false)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Equal(t, 0, len(compRes.resources))
|
||||
assert.Equal(t, 0, len(compRes.managedResources))
|
||||
assert.Equal(t, 0, len(compRes.conditions))
|
||||
}
|
||||
|
||||
// TestCompareAppStateExtraHook tests when there is an extra _hook_ object in live but not defined in git
|
||||
func TestCompareAppStateExtraHook(t *testing.T) {
|
||||
pod := test.NewPod()
|
||||
pod.SetAnnotations(map[string]string{common.AnnotationKeyHook: "PreSync"})
|
||||
pod.SetNamespace(test.FakeDestNamespace)
|
||||
app := newFakeApp()
|
||||
key := kube.ResourceKey{Group: "", Kind: "Pod", Namespace: test.FakeDestNamespace, Name: app.Name}
|
||||
data := fakeData{
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
|
||||
key: pod,
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
compRes, err := ctrl.appStateManager.CompareAppState(app, "", nil, false)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, compRes)
|
||||
assert.Equal(t, argoappv1.SyncStatusCodeSynced, compRes.syncStatus.Status)
|
||||
assert.Equal(t, 1, len(compRes.resources))
|
||||
assert.Equal(t, 1, len(compRes.managedResources))
|
||||
assert.Equal(t, 0, len(compRes.conditions))
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
66
controller/sync_hook_test.go
Normal file
66
controller/sync_hook_test.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/test"
|
||||
"github.com/argoproj/argo-cd/util/kube/kubetest"
|
||||
)
|
||||
|
||||
var clusterRoleHook = `
|
||||
{
|
||||
"apiVersion": "rbac.authorization.k8s.io/v1",
|
||||
"kind": "ClusterRole",
|
||||
"metadata": {
|
||||
"name": "cluster-role-hook",
|
||||
"annotations": {
|
||||
"argocd.argoproj.io/hook": "PostSync"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
func TestSyncHookProjectPermissions(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx(&v1.APIResourceList{
|
||||
GroupVersion: "v1",
|
||||
APIResources: []v1.APIResource{
|
||||
{Name: "pod", Namespaced: true, Kind: "Pod", Group: "v1"},
|
||||
},
|
||||
}, &v1.APIResourceList{
|
||||
GroupVersion: "rbac.authorization.k8s.io/v1",
|
||||
APIResources: []v1.APIResource{
|
||||
{Name: "clusterroles", Namespaced: false, Kind: "ClusterRole", Group: "rbac.authorization.k8s.io"},
|
||||
},
|
||||
})
|
||||
|
||||
syncCtx.kubectl = kubetest.MockKubectlCmd{}
|
||||
crHook, _ := v1alpha1.UnmarshalToUnstructured(clusterRoleHook)
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
hooks: []*unstructured.Unstructured{
|
||||
crHook,
|
||||
},
|
||||
managedResources: []managedResource{{
|
||||
Target: test.NewPod(),
|
||||
}},
|
||||
}
|
||||
syncCtx.proj.Spec.ClusterResourceWhitelist = []v1.GroupKind{}
|
||||
|
||||
syncCtx.syncOp.SyncStrategy = nil
|
||||
syncCtx.sync()
|
||||
assert.Equal(t, v1alpha1.OperationFailed, syncCtx.opState.Phase)
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 0)
|
||||
assert.Contains(t, syncCtx.opState.Message, "not permitted in project")
|
||||
|
||||
// Now add the resource to the whitelist and try again. Resource should be created
|
||||
syncCtx.proj.Spec.ClusterResourceWhitelist = []v1.GroupKind{
|
||||
{Group: "rbac.authorization.k8s.io", Kind: "ClusterRole"},
|
||||
}
|
||||
syncCtx.syncOp.SyncStrategy = nil
|
||||
syncCtx.sync()
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 1)
|
||||
assert.Equal(t, v1alpha1.ResultCodeSynced, syncCtx.syncRes.Resources[0].Status)
|
||||
}
|
||||
560
controller/sync_hooks.go
Normal file
560
controller/sync_hooks.go
Normal file
@@ -0,0 +1,560 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
wfv1 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
apierr "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/kubernetes/pkg/apis/batch"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/util"
|
||||
hookutil "github.com/argoproj/argo-cd/util/hook"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
)
|
||||
|
||||
// doHookSync initiates (or continues) a hook-based sync. This method will be invoked when there may
|
||||
// already be in-flight (potentially incomplete) jobs/workflows, and should be idempotent.
|
||||
func (sc *syncContext) doHookSync(syncTasks []syncTask, hooks []*unstructured.Unstructured) {
|
||||
if !sc.startedPreSyncPhase() {
|
||||
if !sc.verifyPermittedHooks(hooks) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// 1. Run PreSync hooks
|
||||
if !sc.runHooks(hooks, appv1.HookTypePreSync) {
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Run Sync hooks (e.g. blue-green sync workflow)
|
||||
// Before performing Sync hooks, apply any normal manifests which aren't annotated with a hook.
|
||||
// We only want to do this once per operation.
|
||||
shouldContinue := true
|
||||
if !sc.startedSyncPhase() {
|
||||
if !sc.syncNonHookTasks(syncTasks) {
|
||||
sc.setOperationPhase(appv1.OperationFailed, "one or more objects failed to apply")
|
||||
return
|
||||
}
|
||||
shouldContinue = false
|
||||
}
|
||||
if !sc.runHooks(hooks, appv1.HookTypeSync) {
|
||||
shouldContinue = false
|
||||
}
|
||||
if !shouldContinue {
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Run PostSync hooks
|
||||
// Before running PostSync hooks, we want to make rollout is complete (app is healthy). If we
|
||||
// already started the post-sync phase, then we do not need to perform the health check.
|
||||
postSyncHooks, _ := sc.getHooks(appv1.HookTypePostSync)
|
||||
if len(postSyncHooks) > 0 && !sc.startedPostSyncPhase() {
|
||||
sc.log.Infof("PostSync application health check: %s", sc.compareResult.healthStatus.Status)
|
||||
if sc.compareResult.healthStatus.Status != appv1.HealthStatusHealthy {
|
||||
sc.setOperationPhase(appv1.OperationRunning, fmt.Sprintf("waiting for %s state to run %s hooks (current health: %s)",
|
||||
appv1.HealthStatusHealthy, appv1.HookTypePostSync, sc.compareResult.healthStatus.Status))
|
||||
return
|
||||
}
|
||||
}
|
||||
if !sc.runHooks(hooks, appv1.HookTypePostSync) {
|
||||
return
|
||||
}
|
||||
|
||||
// if we get here, all hooks successfully completed
|
||||
sc.setOperationPhase(appv1.OperationSucceeded, "successfully synced")
|
||||
}
|
||||
|
||||
// verifyPermittedHooks verifies all hooks are permitted in the project
|
||||
func (sc *syncContext) verifyPermittedHooks(hooks []*unstructured.Unstructured) bool {
|
||||
for _, hook := range hooks {
|
||||
gvk := hook.GroupVersionKind()
|
||||
serverRes, err := kube.ServerResourceForGroupVersionKind(sc.disco, gvk)
|
||||
if err != nil {
|
||||
sc.setOperationPhase(appv1.OperationError, fmt.Sprintf("unable to identify api resource type: %v", gvk))
|
||||
return false
|
||||
}
|
||||
if !sc.proj.IsResourcePermitted(metav1.GroupKind{Group: gvk.Group, Kind: gvk.Kind}, serverRes.Namespaced) {
|
||||
sc.setOperationPhase(appv1.OperationFailed, fmt.Sprintf("Hook resource %s:%s is not permitted in project %s", gvk.Group, gvk.Kind, sc.proj.Name))
|
||||
return false
|
||||
}
|
||||
|
||||
if serverRes.Namespaced && !sc.proj.IsDestinationPermitted(appv1.ApplicationDestination{Namespace: hook.GetNamespace(), Server: sc.server}) {
|
||||
gvk := hook.GroupVersionKind()
|
||||
sc.setResourceDetails(&appv1.ResourceResult{
|
||||
Name: hook.GetName(),
|
||||
Group: gvk.Group,
|
||||
Version: gvk.Version,
|
||||
Kind: hook.GetKind(),
|
||||
Namespace: hook.GetNamespace(),
|
||||
Message: fmt.Sprintf("namespace %v is not permitted in project '%s'", hook.GetNamespace(), sc.proj.Name),
|
||||
Status: appv1.ResultCodeSyncFailed,
|
||||
})
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// getHooks returns all Argo CD hooks, optionally filtered by ones of the specific type(s)
|
||||
func (sc *syncContext) getHooks(hookTypes ...appv1.HookType) ([]*unstructured.Unstructured, error) {
|
||||
var hooks []*unstructured.Unstructured
|
||||
for _, hook := range sc.compareResult.hooks {
|
||||
if hook.GetNamespace() == "" {
|
||||
hook.SetNamespace(sc.namespace)
|
||||
}
|
||||
if !hookutil.IsArgoHook(hook) {
|
||||
// TODO: in the future, if we want to map helm hooks to Argo CD lifecycles, we should
|
||||
// include helm hooks in the returned list
|
||||
continue
|
||||
}
|
||||
if len(hookTypes) > 0 {
|
||||
match := false
|
||||
for _, desiredType := range hookTypes {
|
||||
if isHookType(hook, desiredType) {
|
||||
match = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
}
|
||||
hooks = append(hooks, hook)
|
||||
}
|
||||
return hooks, nil
|
||||
}
|
||||
|
||||
// runHooks iterates & filters the target manifests for resources of the specified hook type, then
|
||||
// creates the resource. Updates the sc.opRes.hooks with the current status. Returns whether or not
|
||||
// we should continue to the next hook phase.
|
||||
func (sc *syncContext) runHooks(hooks []*unstructured.Unstructured, hookType appv1.HookType) bool {
|
||||
shouldContinue := true
|
||||
for _, hook := range hooks {
|
||||
if hookType == appv1.HookTypeSync && isHookType(hook, appv1.HookTypeSkip) {
|
||||
// If we get here, we are invoking all sync hooks and reached a resource that is
|
||||
// annotated with the Skip hook. This will update the resource details to indicate it
|
||||
// was skipped due to annotation
|
||||
gvk := hook.GroupVersionKind()
|
||||
sc.setResourceDetails(&appv1.ResourceResult{
|
||||
Name: hook.GetName(),
|
||||
Group: gvk.Group,
|
||||
Version: gvk.Version,
|
||||
Kind: hook.GetKind(),
|
||||
Namespace: hook.GetNamespace(),
|
||||
Message: "Skipped",
|
||||
})
|
||||
continue
|
||||
}
|
||||
if !isHookType(hook, hookType) {
|
||||
continue
|
||||
}
|
||||
updated, err := sc.runHook(hook, hookType)
|
||||
if err != nil {
|
||||
sc.setOperationPhase(appv1.OperationError, fmt.Sprintf("%s hook error: %v", hookType, err))
|
||||
return false
|
||||
}
|
||||
if updated {
|
||||
// If the result of running a hook, caused us to modify hook resource state, we should
|
||||
// not proceed to the next hook phase. This is because before proceeding to the next
|
||||
// phase, we want a full health assessment to happen. By returning early, we allow
|
||||
// the application to get requeued into the controller workqueue, and on the next
|
||||
// process iteration, a new CompareAppState() will be performed to get the most
|
||||
// up-to-date live state. This enables us to accurately wait for an application to
|
||||
// become Healthy before proceeding to run PostSync tasks.
|
||||
shouldContinue = false
|
||||
}
|
||||
}
|
||||
if !shouldContinue {
|
||||
sc.log.Infof("Stopping after %s phase due to modifications to hook resource state", hookType)
|
||||
return false
|
||||
}
|
||||
completed, successful := areHooksCompletedSuccessful(hookType, sc.syncRes.Resources)
|
||||
if !completed {
|
||||
return false
|
||||
}
|
||||
if !successful {
|
||||
sc.setOperationPhase(appv1.OperationFailed, fmt.Sprintf("%s hook failed", hookType))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// syncNonHookTasks syncs or prunes the objects that are not handled by hooks using an apply sync.
|
||||
// returns true if the sync was successful
|
||||
func (sc *syncContext) syncNonHookTasks(syncTasks []syncTask) bool {
|
||||
var nonHookTasks []syncTask
|
||||
for _, task := range syncTasks {
|
||||
if task.targetObj == nil {
|
||||
nonHookTasks = append(nonHookTasks, task)
|
||||
} else {
|
||||
annotations := task.targetObj.GetAnnotations()
|
||||
if annotations != nil && annotations[common.AnnotationKeyHook] != "" {
|
||||
// we are doing a hook sync and this resource is annotated with a hook annotation
|
||||
continue
|
||||
}
|
||||
// if we get here, this resource does not have any hook annotation so we
|
||||
// should perform an `kubectl apply`
|
||||
nonHookTasks = append(nonHookTasks, task)
|
||||
}
|
||||
}
|
||||
return sc.doApplySync(nonHookTasks, false, sc.syncOp.SyncStrategy.Hook.Force, true)
|
||||
}
|
||||
|
||||
// runHook runs the supplied hook and updates the hook status. Returns true if the result of
|
||||
// invoking this method resulted in changes to any hook status
|
||||
func (sc *syncContext) runHook(hook *unstructured.Unstructured, hookType appv1.HookType) (bool, error) {
|
||||
// Hook resources names are deterministic, whether they are defined by the user (metadata.name),
|
||||
// or formulated at the time of the operation (metadata.generateName). If user specifies
|
||||
// metadata.generateName, then we will generate a formulated metadata.name before submission.
|
||||
if hook.GetName() == "" {
|
||||
postfix := strings.ToLower(fmt.Sprintf("%s-%s-%d", sc.syncRes.Revision[0:7], hookType, sc.opState.StartedAt.UTC().Unix()))
|
||||
generatedName := hook.GetGenerateName()
|
||||
hook = hook.DeepCopy()
|
||||
hook.SetName(fmt.Sprintf("%s%s", generatedName, postfix))
|
||||
}
|
||||
// Check our hook statuses to see if we already completed this hook.
|
||||
// If so, this method is a noop
|
||||
prevStatus := sc.getHookStatus(hook, hookType)
|
||||
if prevStatus != nil && prevStatus.HookPhase.Completed() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
gvk := hook.GroupVersionKind()
|
||||
apiResource, err := kube.ServerResourceForGroupVersionKind(sc.disco, gvk)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
resource := kube.ToGroupVersionResource(gvk.GroupVersion().String(), apiResource)
|
||||
resIf := kube.ToResourceInterface(sc.dynamicIf, apiResource, resource, hook.GetNamespace())
|
||||
|
||||
var liveObj *unstructured.Unstructured
|
||||
existing, err := resIf.Get(hook.GetName(), metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if !apierr.IsNotFound(err) {
|
||||
return false, fmt.Errorf("Failed to get status of %s hook %s '%s': %v", hookType, gvk, hook.GetName(), err)
|
||||
}
|
||||
_, err := sc.kubectl.ApplyResource(sc.config, hook, hook.GetNamespace(), false, false)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("Failed to create %s hook %s '%s': %v", hookType, gvk, hook.GetName(), err)
|
||||
}
|
||||
created, err := resIf.Get(hook.GetName(), metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return true, fmt.Errorf("Failed to get status of %s hook %s '%s': %v", hookType, gvk, hook.GetName(), err)
|
||||
}
|
||||
sc.log.Infof("%s hook %s '%s' created", hookType, gvk, created.GetName())
|
||||
sc.setOperationPhase(appv1.OperationRunning, fmt.Sprintf("running %s hooks", hookType))
|
||||
liveObj = created
|
||||
} else {
|
||||
liveObj = existing
|
||||
}
|
||||
hookStatus := newHookStatus(liveObj, hookType)
|
||||
if hookStatus.HookPhase.Completed() {
|
||||
if enforceHookDeletePolicy(hook, hookStatus.HookPhase) {
|
||||
err = sc.deleteHook(hook.GetName(), hook.GetNamespace(), hook.GroupVersionKind())
|
||||
if err != nil {
|
||||
hookStatus.HookPhase = appv1.OperationFailed
|
||||
hookStatus.Message = fmt.Sprintf("failed to delete %s hook: %v", hookStatus.HookPhase, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return sc.updateHookStatus(hookStatus), nil
|
||||
}
|
||||
|
||||
// enforceHookDeletePolicy examines the hook deletion policy of a object and deletes it based on the status
|
||||
func enforceHookDeletePolicy(hook *unstructured.Unstructured, phase appv1.OperationPhase) bool {
|
||||
annotations := hook.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return false
|
||||
}
|
||||
deletePolicies := strings.Split(annotations[common.AnnotationKeyHookDeletePolicy], ",")
|
||||
for _, dp := range deletePolicies {
|
||||
policy := appv1.HookDeletePolicy(strings.TrimSpace(dp))
|
||||
if policy == appv1.HookDeletePolicyHookSucceeded && phase == appv1.OperationSucceeded {
|
||||
return true
|
||||
}
|
||||
if policy == appv1.HookDeletePolicyHookFailed && phase == appv1.OperationFailed {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isHookType tells whether or not the supplied object is a hook of the specified type
|
||||
func isHookType(hook *unstructured.Unstructured, hookType appv1.HookType) bool {
|
||||
annotations := hook.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return false
|
||||
}
|
||||
resHookTypes := strings.Split(annotations[common.AnnotationKeyHook], ",")
|
||||
for _, ht := range resHookTypes {
|
||||
if string(hookType) == strings.TrimSpace(ht) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// newHookStatus returns a hook status from an _live_ unstructured object
|
||||
func newHookStatus(hook *unstructured.Unstructured, hookType appv1.HookType) appv1.ResourceResult {
|
||||
gvk := hook.GroupVersionKind()
|
||||
hookStatus := appv1.ResourceResult{
|
||||
Name: hook.GetName(),
|
||||
Kind: hook.GetKind(),
|
||||
Group: gvk.Group,
|
||||
Version: gvk.Version,
|
||||
HookType: hookType,
|
||||
HookPhase: appv1.OperationRunning,
|
||||
Namespace: hook.GetNamespace(),
|
||||
}
|
||||
if isBatchJob(gvk) {
|
||||
updateStatusFromBatchJob(hook, &hookStatus)
|
||||
} else if isArgoWorkflow(gvk) {
|
||||
updateStatusFromArgoWorkflow(hook, &hookStatus)
|
||||
} else if isPod(gvk) {
|
||||
updateStatusFromPod(hook, &hookStatus)
|
||||
} else {
|
||||
hookStatus.HookPhase = appv1.OperationSucceeded
|
||||
hookStatus.Message = fmt.Sprintf("%s created", hook.GetName())
|
||||
}
|
||||
return hookStatus
|
||||
}
|
||||
|
||||
// isRunnable returns if the resource object is a runnable type which needs to be terminated
|
||||
func isRunnable(res *appv1.ResourceResult) bool {
|
||||
gvk := res.GroupVersionKind()
|
||||
return isBatchJob(gvk) || isArgoWorkflow(gvk) || isPod(gvk)
|
||||
}
|
||||
|
||||
func isBatchJob(gvk schema.GroupVersionKind) bool {
|
||||
return gvk.Group == "batch" && gvk.Kind == "Job"
|
||||
}
|
||||
|
||||
func updateStatusFromBatchJob(hook *unstructured.Unstructured, hookStatus *appv1.ResourceResult) {
|
||||
var job batch.Job
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(hook.Object, &job)
|
||||
if err != nil {
|
||||
hookStatus.HookPhase = appv1.OperationError
|
||||
hookStatus.Message = err.Error()
|
||||
return
|
||||
}
|
||||
failed := false
|
||||
var failMsg string
|
||||
complete := false
|
||||
var message string
|
||||
for _, condition := range job.Status.Conditions {
|
||||
switch condition.Type {
|
||||
case batch.JobFailed:
|
||||
failed = true
|
||||
complete = true
|
||||
failMsg = condition.Message
|
||||
case batch.JobComplete:
|
||||
complete = true
|
||||
message = condition.Message
|
||||
}
|
||||
}
|
||||
if !complete {
|
||||
hookStatus.HookPhase = appv1.OperationRunning
|
||||
hookStatus.Message = message
|
||||
} else if failed {
|
||||
hookStatus.HookPhase = appv1.OperationFailed
|
||||
hookStatus.Message = failMsg
|
||||
} else {
|
||||
hookStatus.HookPhase = appv1.OperationSucceeded
|
||||
hookStatus.Message = message
|
||||
}
|
||||
}
|
||||
|
||||
func isArgoWorkflow(gvk schema.GroupVersionKind) bool {
|
||||
return gvk.Group == "argoproj.io" && gvk.Kind == "Workflow"
|
||||
}
|
||||
|
||||
func updateStatusFromArgoWorkflow(hook *unstructured.Unstructured, hookStatus *appv1.ResourceResult) {
|
||||
var wf wfv1.Workflow
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(hook.Object, &wf)
|
||||
if err != nil {
|
||||
hookStatus.HookPhase = appv1.OperationError
|
||||
hookStatus.Message = err.Error()
|
||||
return
|
||||
}
|
||||
switch wf.Status.Phase {
|
||||
case wfv1.NodePending, wfv1.NodeRunning:
|
||||
hookStatus.HookPhase = appv1.OperationRunning
|
||||
case wfv1.NodeSucceeded:
|
||||
hookStatus.HookPhase = appv1.OperationSucceeded
|
||||
case wfv1.NodeFailed:
|
||||
hookStatus.HookPhase = appv1.OperationFailed
|
||||
case wfv1.NodeError:
|
||||
hookStatus.HookPhase = appv1.OperationError
|
||||
}
|
||||
hookStatus.Message = wf.Status.Message
|
||||
}
|
||||
|
||||
func isPod(gvk schema.GroupVersionKind) bool {
|
||||
return gvk.Group == "" && gvk.Kind == "Pod"
|
||||
}
|
||||
|
||||
func updateStatusFromPod(hook *unstructured.Unstructured, hookStatus *appv1.ResourceResult) {
|
||||
var pod apiv1.Pod
|
||||
err := runtime.DefaultUnstructuredConverter.FromUnstructured(hook.Object, &pod)
|
||||
if err != nil {
|
||||
hookStatus.HookPhase = appv1.OperationError
|
||||
hookStatus.Message = err.Error()
|
||||
return
|
||||
}
|
||||
getFailMessage := func(ctr *apiv1.ContainerStatus) string {
|
||||
if ctr.State.Terminated != nil {
|
||||
if ctr.State.Terminated.Message != "" {
|
||||
return ctr.State.Terminated.Message
|
||||
}
|
||||
if ctr.State.Terminated.Reason == "OOMKilled" {
|
||||
return ctr.State.Terminated.Reason
|
||||
}
|
||||
if ctr.State.Terminated.ExitCode != 0 {
|
||||
return fmt.Sprintf("container %q failed with exit code %d", ctr.Name, ctr.State.Terminated.ExitCode)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
switch pod.Status.Phase {
|
||||
case apiv1.PodPending, apiv1.PodRunning:
|
||||
hookStatus.HookPhase = appv1.OperationRunning
|
||||
case apiv1.PodSucceeded:
|
||||
hookStatus.HookPhase = appv1.OperationSucceeded
|
||||
case apiv1.PodFailed:
|
||||
hookStatus.HookPhase = appv1.OperationFailed
|
||||
if pod.Status.Message != "" {
|
||||
// Pod has a nice error message. Use that.
|
||||
hookStatus.Message = pod.Status.Message
|
||||
return
|
||||
}
|
||||
for _, ctr := range append(pod.Status.InitContainerStatuses, pod.Status.ContainerStatuses...) {
|
||||
if msg := getFailMessage(&ctr); msg != "" {
|
||||
hookStatus.Message = msg
|
||||
return
|
||||
}
|
||||
}
|
||||
case apiv1.PodUnknown:
|
||||
hookStatus.HookPhase = appv1.OperationError
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *syncContext) getHookStatus(hookObj *unstructured.Unstructured, hookType appv1.HookType) *appv1.ResourceResult {
|
||||
for _, hr := range sc.syncRes.Resources {
|
||||
if !hr.IsHook() {
|
||||
continue
|
||||
}
|
||||
ns := util.FirstNonEmpty(hookObj.GetNamespace(), sc.namespace)
|
||||
if hookEqual(hr, hookObj.GroupVersionKind().Group, hookObj.GetKind(), ns, hookObj.GetName(), hookType) {
|
||||
return hr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hookEqual(hr *appv1.ResourceResult, group, kind, namespace, name string, hookType appv1.HookType) bool {
|
||||
return bool(
|
||||
hr.Group == group &&
|
||||
hr.Kind == kind &&
|
||||
hr.Namespace == namespace &&
|
||||
hr.Name == name &&
|
||||
hr.HookType == hookType)
|
||||
}
|
||||
|
||||
// updateHookStatus updates the status of a hook. Returns true if the hook was modified
|
||||
func (sc *syncContext) updateHookStatus(hookStatus appv1.ResourceResult) bool {
|
||||
sc.lock.Lock()
|
||||
defer sc.lock.Unlock()
|
||||
for i, prev := range sc.syncRes.Resources {
|
||||
if !prev.IsHook() {
|
||||
continue
|
||||
}
|
||||
if hookEqual(prev, hookStatus.Group, hookStatus.Kind, hookStatus.Namespace, hookStatus.Name, hookStatus.HookType) {
|
||||
if reflect.DeepEqual(prev, hookStatus) {
|
||||
return false
|
||||
}
|
||||
if prev.HookPhase != hookStatus.HookPhase {
|
||||
sc.log.Infof("Hook %s %s/%s hookPhase: %s -> %s", hookStatus.HookType, prev.Kind, prev.Name, prev.HookPhase, hookStatus.HookPhase)
|
||||
}
|
||||
if prev.Status != hookStatus.Status {
|
||||
sc.log.Infof("Hook %s %s/%s status: %s -> %s", hookStatus.HookType, prev.Kind, prev.Name, prev.Status, hookStatus.Status)
|
||||
}
|
||||
if prev.Message != hookStatus.Message {
|
||||
sc.log.Infof("Hook %s %s/%s message: '%s' -> '%s'", hookStatus.HookType, prev.Kind, prev.Name, prev.Message, hookStatus.Message)
|
||||
}
|
||||
sc.syncRes.Resources[i] = &hookStatus
|
||||
return true
|
||||
}
|
||||
}
|
||||
sc.syncRes.Resources = append(sc.syncRes.Resources, &hookStatus)
|
||||
sc.log.Infof("Set new hook %s %s/%s. phase: %s, message: %s", hookStatus.HookType, hookStatus.Kind, hookStatus.Name, hookStatus.HookPhase, hookStatus.Message)
|
||||
return true
|
||||
}
|
||||
|
||||
// areHooksCompletedSuccessful checks if all the hooks of the specified type are completed and successful
|
||||
func areHooksCompletedSuccessful(hookType appv1.HookType, hookStatuses []*appv1.ResourceResult) (bool, bool) {
|
||||
isSuccessful := true
|
||||
for _, hookStatus := range hookStatuses {
|
||||
if !hookStatus.IsHook() {
|
||||
continue
|
||||
}
|
||||
if hookStatus.HookType != hookType {
|
||||
continue
|
||||
}
|
||||
if !hookStatus.HookPhase.Completed() {
|
||||
return false, false
|
||||
}
|
||||
if !hookStatus.HookPhase.Successful() {
|
||||
isSuccessful = false
|
||||
}
|
||||
}
|
||||
return true, isSuccessful
|
||||
}
|
||||
|
||||
// terminate looks for any running jobs/workflow hooks and deletes the resource
|
||||
func (sc *syncContext) terminate() {
|
||||
terminateSuccessful := true
|
||||
for _, hookStatus := range sc.syncRes.Resources {
|
||||
if !hookStatus.IsHook() {
|
||||
continue
|
||||
}
|
||||
if hookStatus.HookPhase.Completed() {
|
||||
continue
|
||||
}
|
||||
if isRunnable(hookStatus) {
|
||||
hookStatus.HookPhase = appv1.OperationFailed
|
||||
err := sc.deleteHook(hookStatus.Name, hookStatus.Namespace, hookStatus.GroupVersionKind())
|
||||
if err != nil {
|
||||
hookStatus.Message = fmt.Sprintf("Failed to delete %s hook %s/%s: %v", hookStatus.HookType, hookStatus.Kind, hookStatus.Name, err)
|
||||
terminateSuccessful = false
|
||||
} else {
|
||||
hookStatus.Message = fmt.Sprintf("Deleted %s hook %s/%s", hookStatus.HookType, hookStatus.Kind, hookStatus.Name)
|
||||
}
|
||||
sc.updateHookStatus(*hookStatus)
|
||||
}
|
||||
}
|
||||
if terminateSuccessful {
|
||||
sc.setOperationPhase(appv1.OperationFailed, "Operation terminated")
|
||||
} else {
|
||||
sc.setOperationPhase(appv1.OperationError, "Operation termination had errors")
|
||||
}
|
||||
}
|
||||
|
||||
func (sc *syncContext) deleteHook(name, namespace string, gvk schema.GroupVersionKind) error {
|
||||
apiResource, err := kube.ServerResourceForGroupVersionKind(sc.disco, gvk)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resource := kube.ToGroupVersionResource(gvk.GroupVersion().String(), apiResource)
|
||||
resIf := kube.ToResourceInterface(sc.dynamicIf, apiResource, resource, namespace)
|
||||
propagationPolicy := metav1.DeletePropagationForeground
|
||||
return resIf.Delete(name, &metav1.DeleteOptions{PropagationPolicy: &propagationPolicy})
|
||||
}
|
||||
@@ -1,26 +1,485 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
fakedisco "k8s.io/client-go/discovery/fake"
|
||||
"k8s.io/client-go/rest"
|
||||
testcore "k8s.io/client-go/testing"
|
||||
|
||||
"github.com/argoproj/argo-cd/common"
|
||||
"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/reposerver/repository"
|
||||
"github.com/argoproj/argo-cd/test"
|
||||
"github.com/argoproj/argo-cd/util/kube"
|
||||
"github.com/argoproj/argo-cd/util/kube/kubetest"
|
||||
)
|
||||
|
||||
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 newTestSyncCtx(resources ...*v1.APIResourceList) *syncContext {
|
||||
fakeDisco := &fakedisco.FakeDiscovery{Fake: &testcore.Fake{}}
|
||||
fakeDisco.Resources = append(resources,
|
||||
&v1.APIResourceList{
|
||||
GroupVersion: "v1",
|
||||
APIResources: []v1.APIResource{
|
||||
{Kind: "Pod", Group: "", Version: "v1", Namespaced: true},
|
||||
{Kind: "Service", Group: "", Version: "v1", Namespaced: true},
|
||||
},
|
||||
},
|
||||
&v1.APIResourceList{
|
||||
GroupVersion: "apps/v1",
|
||||
APIResources: []v1.APIResource{
|
||||
{Kind: "Deployment", Group: "apps", Version: "v1", Namespaced: true},
|
||||
},
|
||||
})
|
||||
sc := syncContext{
|
||||
config: &rest.Config{},
|
||||
namespace: test.FakeArgoCDNamespace,
|
||||
server: test.FakeClusterURL,
|
||||
syncRes: &v1alpha1.SyncOperationResult{},
|
||||
syncOp: &v1alpha1.SyncOperation{
|
||||
Prune: true,
|
||||
SyncStrategy: &v1alpha1.SyncStrategy{
|
||||
Apply: &v1alpha1.SyncStrategyApply{},
|
||||
},
|
||||
},
|
||||
proj: &v1alpha1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test",
|
||||
},
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
Destinations: []v1alpha1.ApplicationDestination{{
|
||||
Server: test.FakeClusterURL,
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
}},
|
||||
ClusterResourceWhitelist: []v1.GroupKind{
|
||||
{Group: "*", Kind: "*"},
|
||||
},
|
||||
},
|
||||
},
|
||||
opState: &v1alpha1.OperationState{},
|
||||
disco: fakeDisco,
|
||||
log: log.WithFields(log.Fields{"application": "fake-app"}),
|
||||
}
|
||||
sc.kubectl = kubetest.MockKubectlCmd{}
|
||||
return &sc
|
||||
}
|
||||
|
||||
func TestSyncNotPermittedNamespace(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
targetPod := test.NewPod()
|
||||
targetPod.SetNamespace("kube-system")
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: nil,
|
||||
Target: targetPod,
|
||||
}, {
|
||||
Live: nil,
|
||||
Target: test.NewService(),
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Equal(t, v1alpha1.OperationFailed, syncCtx.opState.Phase)
|
||||
assert.Contains(t, syncCtx.syncRes.Resources[0].Message, "not permitted in project")
|
||||
}
|
||||
|
||||
func TestSyncCreateInSortedOrder(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: nil,
|
||||
Target: test.NewPod(),
|
||||
}, {
|
||||
Live: nil,
|
||||
Target: test.NewService(),
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 2)
|
||||
for i := range syncCtx.syncRes.Resources {
|
||||
if syncCtx.syncRes.Resources[i].Kind == "Pod" {
|
||||
assert.Equal(t, v1alpha1.ResultCodeSynced, syncCtx.syncRes.Resources[i].Status)
|
||||
} else if syncCtx.syncRes.Resources[i].Kind == "Service" {
|
||||
assert.Equal(t, v1alpha1.ResultCodeSynced, syncCtx.syncRes.Resources[i].Status)
|
||||
} else {
|
||||
t.Error("Resource isn't a pod or a service")
|
||||
}
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Equal(t, syncCtx.opState.Phase, v1alpha1.OperationSucceeded)
|
||||
}
|
||||
|
||||
func TestSyncCreateNotWhitelistedClusterResources(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx(&v1.APIResourceList{
|
||||
GroupVersion: v1alpha1.SchemeGroupVersion.String(),
|
||||
APIResources: []v1.APIResource{
|
||||
{Name: "workflows", Namespaced: false, Kind: "Workflow", Group: "argoproj.io"},
|
||||
{Name: "application", Namespaced: false, Kind: "Application", Group: "argoproj.io"},
|
||||
},
|
||||
}, &v1.APIResourceList{
|
||||
GroupVersion: "rbac.authorization.k8s.io/v1",
|
||||
APIResources: []v1.APIResource{
|
||||
{Name: "clusterroles", Namespaced: false, Kind: "ClusterRole", Group: "rbac.authorization.k8s.io"},
|
||||
},
|
||||
})
|
||||
|
||||
syncCtx.proj.Spec.ClusterResourceWhitelist = []v1.GroupKind{
|
||||
{Group: "argoproj.io", Kind: "*"},
|
||||
}
|
||||
|
||||
syncCtx.kubectl = kubetest.MockKubectlCmd{}
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: nil,
|
||||
Target: kube.MustToUnstructured(&rbacv1.ClusterRole{
|
||||
TypeMeta: metav1.TypeMeta{Kind: "ClusterRole", APIVersion: "rbac.authorization.k8s.io/v1"},
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "argo-ui-cluster-role"}}),
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 1)
|
||||
assert.Equal(t, v1alpha1.ResultCodeSyncFailed, syncCtx.syncRes.Resources[0].Status)
|
||||
assert.Contains(t, syncCtx.syncRes.Resources[0].Message, "not permitted in project")
|
||||
}
|
||||
|
||||
func TestSyncBlacklistedNamespacedResources(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
|
||||
syncCtx.proj.Spec.NamespaceResourceBlacklist = []v1.GroupKind{
|
||||
{Group: "*", Kind: "Deployment"},
|
||||
}
|
||||
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: nil,
|
||||
Target: test.NewDeployment(),
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 1)
|
||||
assert.Equal(t, v1alpha1.ResultCodeSyncFailed, syncCtx.syncRes.Resources[0].Status)
|
||||
assert.Contains(t, syncCtx.syncRes.Resources[0].Message, "not permitted in project")
|
||||
}
|
||||
|
||||
func TestSyncSuccessfully(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: nil,
|
||||
Target: test.NewService(),
|
||||
}, {
|
||||
Live: test.NewPod(),
|
||||
Target: nil,
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 2)
|
||||
for i := range syncCtx.syncRes.Resources {
|
||||
if syncCtx.syncRes.Resources[i].Kind == "Pod" {
|
||||
assert.Equal(t, v1alpha1.ResultCodePruned, syncCtx.syncRes.Resources[i].Status)
|
||||
} else if syncCtx.syncRes.Resources[i].Kind == "Service" {
|
||||
assert.Equal(t, v1alpha1.ResultCodeSynced, syncCtx.syncRes.Resources[i].Status)
|
||||
} else {
|
||||
t.Error("Resource isn't a pod or a service")
|
||||
}
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Equal(t, syncCtx.opState.Phase, v1alpha1.OperationSucceeded)
|
||||
}
|
||||
|
||||
func TestSyncDeleteSuccessfully(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: test.NewService(),
|
||||
Target: nil,
|
||||
}, {
|
||||
Live: test.NewPod(),
|
||||
Target: nil,
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
for i := range syncCtx.syncRes.Resources {
|
||||
if syncCtx.syncRes.Resources[i].Kind == "Pod" {
|
||||
assert.Equal(t, v1alpha1.ResultCodePruned, syncCtx.syncRes.Resources[i].Status)
|
||||
} else if syncCtx.syncRes.Resources[i].Kind == "Service" {
|
||||
assert.Equal(t, v1alpha1.ResultCodePruned, syncCtx.syncRes.Resources[i].Status)
|
||||
} else {
|
||||
t.Error("Resource isn't a pod or a service")
|
||||
}
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Equal(t, syncCtx.opState.Phase, v1alpha1.OperationSucceeded)
|
||||
}
|
||||
|
||||
func TestSyncCreateFailure(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.kubectl = kubetest.MockKubectlCmd{
|
||||
Commands: map[string]kubetest.KubectlOutput{
|
||||
"test-service": {
|
||||
Output: "",
|
||||
Err: fmt.Errorf("error: error validating \"test.yaml\": error validating data: apiVersion not set; if you choose to ignore these errors, turn validation off with --validate=false"),
|
||||
},
|
||||
},
|
||||
}
|
||||
testSvc := test.NewService()
|
||||
testSvc.SetAPIVersion("")
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: nil,
|
||||
Target: testSvc,
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 1)
|
||||
assert.Equal(t, v1alpha1.ResultCodeSyncFailed, syncCtx.syncRes.Resources[0].Status)
|
||||
}
|
||||
|
||||
func TestSyncPruneFailure(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
syncCtx.kubectl = kubetest.MockKubectlCmd{
|
||||
Commands: map[string]kubetest.KubectlOutput{
|
||||
"test-service": {
|
||||
Output: "",
|
||||
Err: fmt.Errorf(" error: timed out waiting for \"test-service\" to be synced"),
|
||||
},
|
||||
},
|
||||
}
|
||||
testSvc := test.NewService()
|
||||
testSvc.SetName("test-service")
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: testSvc,
|
||||
Target: nil,
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 1)
|
||||
assert.Equal(t, v1alpha1.ResultCodeSyncFailed, syncCtx.syncRes.Resources[0].Status)
|
||||
}
|
||||
|
||||
func unsortedManifest() []syncTask {
|
||||
return []syncTask{
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "Pod",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "Service",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "PersistentVolume",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "ConfigMap",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunWorkflows(t *testing.T) {
|
||||
// syncCtx := newTestSyncCtx()
|
||||
// syncCtx.doWorkflowSync(nil, nil)
|
||||
func sortedManifest() []syncTask {
|
||||
return []syncTask{
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "ConfigMap",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "PersistentVolume",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "Service",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "Pod",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortKubernetesResourcesSuccessfully(t *testing.T) {
|
||||
unsorted := unsortedManifest()
|
||||
ks := newKindSorter(unsorted, resourceOrder)
|
||||
sort.Sort(ks)
|
||||
|
||||
expectedOrder := sortedManifest()
|
||||
assert.Equal(t, len(unsorted), len(expectedOrder))
|
||||
for i, sorted := range unsorted {
|
||||
assert.Equal(t, expectedOrder[i], sorted)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestSortManifestHandleNil(t *testing.T) {
|
||||
task := syncTask{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "Service",
|
||||
},
|
||||
},
|
||||
}
|
||||
manifest := []syncTask{
|
||||
{},
|
||||
task,
|
||||
}
|
||||
ks := newKindSorter(manifest, resourceOrder)
|
||||
sort.Sort(ks)
|
||||
assert.Equal(t, task, manifest[0])
|
||||
assert.Nil(t, manifest[1].targetObj)
|
||||
}
|
||||
|
||||
func TestSyncNamespaceAgainstCRD(t *testing.T) {
|
||||
crd := syncTask{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": "argoproj.io/alpha1",
|
||||
"kind": "Workflow",
|
||||
},
|
||||
}}
|
||||
namespace := syncTask{
|
||||
targetObj: &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"GroupVersion": apiv1.SchemeGroupVersion.String(),
|
||||
"kind": "Namespace",
|
||||
},
|
||||
},
|
||||
}
|
||||
unsorted := []syncTask{crd, namespace}
|
||||
ks := newKindSorter(unsorted, resourceOrder)
|
||||
sort.Sort(ks)
|
||||
|
||||
expectedOrder := []syncTask{namespace, crd}
|
||||
assert.Equal(t, len(unsorted), len(expectedOrder))
|
||||
for i, sorted := range unsorted {
|
||||
assert.Equal(t, expectedOrder[i], sorted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDontSyncOrPruneHooks(t *testing.T) {
|
||||
syncCtx := newTestSyncCtx()
|
||||
targetPod := test.NewPod()
|
||||
targetPod.SetName("dont-create-me")
|
||||
targetPod.SetAnnotations(map[string]string{common.AnnotationKeyHook: "PreSync"})
|
||||
liveSvc := test.NewService()
|
||||
liveSvc.SetName("dont-prune-me")
|
||||
liveSvc.SetAnnotations(map[string]string{common.AnnotationKeyHook: "PreSync"})
|
||||
|
||||
syncCtx.compareResult = &comparisonResult{
|
||||
managedResources: []managedResource{{
|
||||
Live: nil,
|
||||
Target: targetPod,
|
||||
Hook: true,
|
||||
}, {
|
||||
Live: liveSvc,
|
||||
Target: nil,
|
||||
Hook: true,
|
||||
}},
|
||||
}
|
||||
syncCtx.sync()
|
||||
assert.Len(t, syncCtx.syncRes.Resources, 0)
|
||||
syncCtx.sync()
|
||||
assert.Equal(t, syncCtx.opState.Phase, v1alpha1.OperationSucceeded)
|
||||
}
|
||||
|
||||
func TestPersistRevisionHistory(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
defaultProject := &v1alpha1.AppProject{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
Name: "default",
|
||||
},
|
||||
}
|
||||
data := fakeData{
|
||||
apps: []runtime.Object{app, defaultProject},
|
||||
manifestResponse: &repository.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
|
||||
}
|
||||
ctrl := newFakeController(&data)
|
||||
|
||||
ctrl.appStateManager.SyncAppState(app, &v1alpha1.OperationState{Operation: v1alpha1.Operation{
|
||||
Sync: &v1alpha1.SyncOperation{},
|
||||
}})
|
||||
|
||||
updatedApp, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Get(app.Name, v1.GetOptions{})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(updatedApp.Status.History))
|
||||
assert.Equal(t, 0, len(updatedApp.Status.History[0].ComponentParameterOverrides))
|
||||
assert.Equal(t, "abc123", updatedApp.Status.History[0].Revision)
|
||||
|
||||
overrides := []v1alpha1.ComponentParameter{{Name: "test", Value: "123"}}
|
||||
ctrl.appStateManager.SyncAppState(app, &v1alpha1.OperationState{Operation: v1alpha1.Operation{
|
||||
Sync: &v1alpha1.SyncOperation{
|
||||
ParameterOverrides: overrides,
|
||||
},
|
||||
}})
|
||||
|
||||
updatedApp, err = ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.Namespace).Get(app.Name, v1.GetOptions{})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, len(updatedApp.Status.History))
|
||||
assert.ElementsMatch(t, overrides, updatedApp.Status.History[0].ComponentParameterOverrides)
|
||||
assert.Equal(t, "abc123", updatedApp.Status.History[0].Revision)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# ArgoCD Documentation
|
||||
# Argo CD Documentation
|
||||
|
||||
## [Getting Started](getting_started.md)
|
||||
|
||||
@@ -9,8 +9,18 @@
|
||||
## Features
|
||||
* [Application Sources](application_sources.md)
|
||||
* [Application Parameters](parameters.md)
|
||||
* [Projects](projects.md)
|
||||
* [Automated Sync](auto_sync.md)
|
||||
* [Resource Health](health.md)
|
||||
* [Resource Hooks](resource_hooks.md)
|
||||
* [Single Sign On](sso.md)
|
||||
* [Webhooks](webhook.md)
|
||||
* [RBAC](rbac.md)
|
||||
* [RBAC](rbac.md)
|
||||
* [Declarative Setup](declarative-setup.md)
|
||||
|
||||
## Other
|
||||
* [Best Practices](best_practices.md)
|
||||
* [Configuring Ingress](ingress.md)
|
||||
* [Automation from CI Pipelines](ci_automation.md)
|
||||
* [Custom Tooling](custom_tools.md)
|
||||
* [F.A.Q.](faq.md)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Application Source Types
|
||||
|
||||
ArgoCD supports several different ways in which kubernetes manifests can be defined:
|
||||
Argo CD supports several different ways in which kubernetes manifests can be defined:
|
||||
|
||||
* [ksonnet](https://ksonnet.io) applications
|
||||
* [kustomize](https://kustomize.io) applications
|
||||
* [helm](https://helm.sh) charts
|
||||
* Simple directory of YAML/json manifests
|
||||
* Directory of YAML/json/jsonnet manifests
|
||||
|
||||
Some additional considerations should be made when deploying apps of a particular type:
|
||||
|
||||
@@ -35,7 +36,7 @@ guestbook-ui servicePort 80
|
||||
guestbook-ui type "LoadBalancer"
|
||||
```
|
||||
|
||||
When overriding ksonnet parameters in ArgoCD, the component name should also be specified in the
|
||||
When overriding ksonnet parameters in Argo CD, the component name should also be specified in the
|
||||
`argocd app set` command, in the form of `-p COMPONENT=PARAM=VALUE`. For example:
|
||||
```
|
||||
argocd app set guestbook-default -p guestbook-ui=image=gcr.io/heptio-images/ks-guestbook-demo:0.1
|
||||
@@ -60,7 +61,7 @@ a `values.yaml`. For example, `service.type` is a common parameter which is expo
|
||||
```
|
||||
helm template . --set service.type=LoadBalancer
|
||||
```
|
||||
Similarly ArgoCD can override values in the `values.yaml` parameters using `argo app set` command,
|
||||
Similarly Argo CD can override values in the `values.yaml` parameters using `argo app set` command,
|
||||
in the form of `-p PARAM=VALUE`. For example:
|
||||
```
|
||||
argocd app set helm-guestbook -p service.type=LoadBalancer
|
||||
@@ -68,16 +69,16 @@ 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 hooks are equivalent in concept to [Argo CD resource hooks](resource_hooks.md). In helm, a hook
|
||||
is any normal kubernetes resource annotated with the `helm.sh/hook` annotation. When Argo CD 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
|
||||
the `kubectl apply` of the manifests. There is an
|
||||
[open issue](https://github.com/argoproj/argo-cd/issues/355) to map Helm hooks to Argo CD's concept
|
||||
of Pre/Post/Sync hooks.
|
||||
|
||||
|
||||
### Random Data
|
||||
|
||||
Helm templating has the ability to generate random data during chart rendering via the
|
||||
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):
|
||||
@@ -91,7 +92,7 @@ data:
|
||||
{{- end }}
|
||||
```
|
||||
|
||||
The ArgoCD application controller periodically compares git state against the live state, running
|
||||
The Argo CD application controller periodically compares git state against the live state, running
|
||||
the `helm template <CHART>` command to generate the helm manifests. Because the random value is
|
||||
regenerated every time the comparison is made, any application which makes use of the `randAlphaNum`
|
||||
function will always be in an `OutOfSync` state. This can be mitigated by explicitly setting a
|
||||
|
||||
@@ -45,8 +45,33 @@ spec:
|
||||
repoURL: https://github.com/argoproj/argocd-example-apps.git
|
||||
targetRevision: HEAD
|
||||
path: guestbook
|
||||
environment: default
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: default
|
||||
```
|
||||
|
||||
### AppProject CRD (Custom Resource Definition)
|
||||
The AppProject CRD is the Kubernetes resource object representing a grouping of applications. It is defined by three key pieces of information:
|
||||
* `sourceRepos` reference to the reposities that applications within the project can pull manifests from.
|
||||
* `destinations` reference to clusters and namespaces that applications within the project can deploy into.
|
||||
* `roles` list of entities with defintions of their access to resources within the project.
|
||||
|
||||
An example spec is as follows:
|
||||
|
||||
```
|
||||
spec:
|
||||
description: Description of the project
|
||||
destinations:
|
||||
- namespace: default
|
||||
server: https://kubernetes.default.svc
|
||||
roles:
|
||||
- description: Description of the role
|
||||
jwtTokens:
|
||||
- iat: 1535390316
|
||||
name: role-name
|
||||
policies:
|
||||
- p, proj:proj-name:role-name, applications, get, proj-name/*, allow
|
||||
- p, proj:proj-name:role-name, applications, sync, proj-name/*, deny
|
||||
sourceRepos:
|
||||
- https://github.com/argoproj/argocd-example-apps.git
|
||||
```
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 58 KiB |
51
docs/auto_sync.md
Normal file
51
docs/auto_sync.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Automated Sync Policy
|
||||
|
||||
Argo CD has the ability to automatically sync an application when it detects differences between
|
||||
the desired manifests in git, and the live state in the cluster. A benefit of automatic sync is that
|
||||
CI/CD pipelines no longer need direct access to the Argo CD API server to perform the deployment.
|
||||
Instead, the pipeline makes a commit and push to the git repository with the changes to the
|
||||
manifests in the tracking git repo.
|
||||
|
||||
To configure automated sync run:
|
||||
```bash
|
||||
argocd app set <APPNAME> --sync-policy automated
|
||||
```
|
||||
|
||||
Alternatively, if creating the application an application manifest, specify a syncPolicy with an
|
||||
`automated` policy.
|
||||
```yaml
|
||||
spec:
|
||||
syncPolicy:
|
||||
automated: {}
|
||||
```
|
||||
|
||||
## Automatic Pruning
|
||||
|
||||
By default (and as a safety mechanism), automated sync will not delete resources when Argo CD detects
|
||||
the resource is no longer defined in git. To prune the resources, a manual sync can always be
|
||||
performed (with pruning checked). Pruning can also be enabled to happen automatically as part of the
|
||||
automated sync by running:
|
||||
|
||||
```bash
|
||||
argocd app set <APPNAME> --auto-prune
|
||||
```
|
||||
|
||||
Or by setting the prune option to true in the automated sync policy:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
```
|
||||
|
||||
## Automated Sync Semantics
|
||||
|
||||
* An automated sync will only be performed if the application is OutOfSync. Applications in a
|
||||
Synced or error state will not attempt automated sync.
|
||||
* Automated sync will only attempt one synchronization per unique combination of commit SHA1 and
|
||||
application parameters. If the most recent successful sync in the history was already performed
|
||||
against the same commit-SHA and parameters, a second sync will not be attempted.
|
||||
* Automatic sync will not reattempt a sync if the previous sync attempt against the same commit-SHA
|
||||
and parameters had failed.
|
||||
* Rollback cannot be performed against an application with automated sync enabled.
|
||||
53
docs/best_practices.md
Normal file
53
docs/best_practices.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Best Practices
|
||||
|
||||
## Separating config vs. source code repositories
|
||||
|
||||
Using a separate git repository to hold your kubernetes manifests, keeping the config separate
|
||||
from your application source code, is highly recommended for the following reasons:
|
||||
|
||||
1. It provides a clean separation of application code vs. application config. There will be times
|
||||
when you wish to change over and not other. For example, you likely do _not_ want to trigger
|
||||
a build if you are updating an annotation in a spec.
|
||||
|
||||
2. Cleaner audit log. For auditing purposes, a repo which only holds configuration will have a much
|
||||
cleaner git history of what changes were made, without the noise stemming from check-ins of
|
||||
normal development activity.
|
||||
|
||||
2. Your application may be comprised of services built from multiple git repositories, but is
|
||||
deployed as a single unit. Often times, microservices applications are comprised of services
|
||||
with different versioning schemes, and release cycles (e.g. ELK, Kafka + Zookeeper). It may not
|
||||
make sense to store the manifests in one of the source code repositories of a single component.
|
||||
|
||||
3. Separate repositories enables separation of access. The person who is developing the app, may
|
||||
not necessarily be the same person who can/should affect production environment, either
|
||||
intentionally or unintentionally.
|
||||
|
||||
4. If you are automating your CI pipeline, pushing manifest changes to the same git repository will
|
||||
likely trigger an infinite loop of build jobs and git commit triggers. Pushing config changes to
|
||||
a separate repo prevent this from happening.
|
||||
|
||||
|
||||
## Leaving room for imperativeness
|
||||
|
||||
It may be desired to leave room for some imperativeness/automation, and not have everything defined
|
||||
in your git manifests. For example, if you want the number of your deployment's replicas to be
|
||||
managed by [Horizontal Pod Autoscaler](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/),
|
||||
then you would not want to track `replicas` in git.
|
||||
|
||||
```
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx-deployment
|
||||
spec:
|
||||
# do not include replicas in the manifests if you want replicas to be controlled by HPA
|
||||
# replicas: 1
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- image: nginx:1.7.9
|
||||
name: nginx
|
||||
ports:
|
||||
- containerPort: 80
|
||||
...
|
||||
```
|
||||
58
docs/ci_automation.md
Normal file
58
docs/ci_automation.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Automation from CI Pipelines
|
||||
|
||||
Argo CD follows the GitOps model of deployment, where desired configuration changes are first
|
||||
pushed to git, and the cluster state then syncs to the desired state in git. This is a departure
|
||||
from imperative pipelines which do not traditionally use git repositories to hold application
|
||||
config.
|
||||
|
||||
To push new container images into to a cluster managed by Argo CD, the following workflow (or
|
||||
variations), might be used:
|
||||
|
||||
|
||||
1. Build and publish a new container image
|
||||
|
||||
```
|
||||
docker build -t mycompany/guestbook:v2.0 .
|
||||
docker push -t mycompany/guestbook:v2.0 .
|
||||
```
|
||||
|
||||
2. Update the local manifests using your preferred templating tool, and push the changes to git.
|
||||
|
||||
NOTE: the use of a different git repository to hold your kubernetes manifests (separate from
|
||||
your application source code), is highly recommended. See [best practices](best_practices.md)
|
||||
for further rationale.
|
||||
|
||||
```
|
||||
git clone https://github.com/mycompany/guestbook-config.git
|
||||
cd guestbook-config
|
||||
|
||||
# kustomize
|
||||
kustomize edit set imagetag mycompany/guestbook:v2.0
|
||||
|
||||
# ksonnet
|
||||
ks param set guestbook image mycompany/guestbook:v2.0
|
||||
|
||||
# plain yaml
|
||||
kubectl patch --local -f config-deployment.yaml -p '{"spec":{"template":{"spec":{"containers":[{"name":"guestbook","image":"mycompany/guestbook:v2.0"}]}}}}' -o yaml
|
||||
|
||||
git add . -m "Update guestbook to v2.0"
|
||||
git push
|
||||
```
|
||||
|
||||
3. Synchronize the app (Optional)
|
||||
|
||||
For convenience, the argocd CLI can be downloaded directly from the API server. This is
|
||||
useful so that the CLI used in the CI pipeline is always kept in-sync and uses argocd binary
|
||||
that is always compatible with the Argo CD API server.
|
||||
|
||||
```
|
||||
export ARGOCD_SERVER=argocd.mycompany.com
|
||||
export ARGOCD_AUTH_TOKEN=<JWT token generated from project>
|
||||
curl -sSL -o /usr/local/bin/argocd https://${ARGOCD_SERVER}/download/argocd-linux-amd64
|
||||
argocd app sync guestbook
|
||||
argocd app wait guestbook
|
||||
```
|
||||
|
||||
If [automated synchronization](auto_sync.md) is configured for the application, this step is
|
||||
unnecessary. The controller will automatically detect the new config (fast tracked using a
|
||||
[webhook](webhook.md), or polled every 3 minutes), and automatically sync the new manifests.
|
||||
38
docs/custom_tools.md
Normal file
38
docs/custom_tools.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Custom Tooling
|
||||
|
||||
Argo CD bundles preferred versions of its supported templating tools (helm, kustomize, ks, jsonnet)
|
||||
as part of its container images. Sometimes, it may be desired to use a specific version of a tool
|
||||
other than what Argo CD bundles. Some reasons to do this might be:
|
||||
|
||||
* To upgrade/downgrade to a specific version of a tool due to bugs or bug fixes.
|
||||
* To install additional dependencies which to be used by kustomize's configmap/secret generators
|
||||
(e.g. curl, vault)
|
||||
|
||||
As the Argo CD repo-server is the single service responsible for generating Kubernetes manifests, it
|
||||
can be customized to use alternative toolchain required by your environment.
|
||||
|
||||
The following example describes how the repo-server manifest can be customized to use a different
|
||||
version of helm than what is bundled in Argo CD:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
# 1. Define an emptyDir volume which will hold the custom binaries
|
||||
volumes:
|
||||
- name: custom-tools
|
||||
emptyDir: {}
|
||||
# 2. Use an init container to download and/or copy the custom binaries into the emptyDir
|
||||
initContainers:
|
||||
- name: download-tools
|
||||
image: lachlanevenson/k8s-helm:v2.10.0
|
||||
command: [cp, /usr/local/bin/helm, /custom-tools]
|
||||
volumeMounts:
|
||||
- mountPath: /custom-tools
|
||||
name: custom-tools
|
||||
# 3. Volume mount the custom binary to the bin directory (overriding the existing version)
|
||||
containers:
|
||||
- name: argocd-repo-server
|
||||
volumeMounts:
|
||||
- mountPath: /usr/local/bin/helm
|
||||
name: custom-tools
|
||||
subPath: helm
|
||||
```
|
||||
162
docs/declarative-setup.md
Normal file
162
docs/declarative-setup.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# Declarative Setup
|
||||
|
||||
Argo CD settings might be defined declaratively using Kubernetes manifests.
|
||||
|
||||
## Repositories
|
||||
|
||||
Repository credentials are stored in secret. Use following steps to configure a repo:
|
||||
|
||||
1. Create secret which contains repository credentials. Consider using [bitnami-labs/sealed-secrets](https://github.com/bitnami-labs/sealed-secrets) to store encrypted secret
|
||||
definition as a Kubernetes manifest.
|
||||
|
||||
2. Register repository in `argocd-cm` config map. Each repository must have `url` field and `usernameSecret`, `passwordSecret` or `sshPrivateKeySecret`.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
data:
|
||||
dex.config: |
|
||||
connectors:
|
||||
- type: github
|
||||
id: github
|
||||
name: GitHub
|
||||
config:
|
||||
clientID: e8f597564a82e99ba9aa
|
||||
clientSecret: e551007c6c6dbc666bdade281ff095caec150159
|
||||
repositories: |
|
||||
- passwordSecret:
|
||||
key: password
|
||||
name: my-secret
|
||||
url: https://github.com/argoproj/my-private-repository
|
||||
usernameSecret:
|
||||
key: username
|
||||
name: my-secret
|
||||
url: http://localhost:4000
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-cm
|
||||
```
|
||||
|
||||
## Clusters
|
||||
|
||||
Cluster credentials are stored in secrets same as repository credentials but does not require entry in `argocd-cm` config map. Each secret must have label
|
||||
`argocd.argoproj.io/secret-type: cluster` and name which is following convention: `<hostname>-<port>`.
|
||||
|
||||
The secret data must include following fields:
|
||||
* `name` - cluster name
|
||||
* `server` - cluster api server url
|
||||
* `config` - JSON representation of following data structure:
|
||||
|
||||
```yaml
|
||||
# Basic authentication settings
|
||||
username: string
|
||||
password: string
|
||||
# Bearer authentication settings
|
||||
bearerToken: string
|
||||
# IAM authentication configuration
|
||||
awsAuthConfig:
|
||||
clusterName: string
|
||||
roleARN: string
|
||||
# Transport layer security configuration settings
|
||||
tlsClientConfig:
|
||||
# PEM-encoded bytes (typically read from a client certificate file).
|
||||
caData: string
|
||||
# PEM-encoded bytes (typically read from a client certificate file).
|
||||
certData: string
|
||||
# Server should be accessed without verifying the TLS certificate
|
||||
insecure: boolean
|
||||
# PEM-encoded bytes (typically read from a client certificate key file).
|
||||
keyData: string
|
||||
# ServerName is passed to the server for SNI and is used in the client to check server
|
||||
# ceritificates against. If ServerName is empty, the hostname used to contact the
|
||||
# server is used.
|
||||
serverName: string
|
||||
```
|
||||
|
||||
|
||||
Cluster secret example:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
stringData:
|
||||
config: |||
|
||||
{
|
||||
"bearerToken": "<authentication token>",
|
||||
"tlsClientConfig": {
|
||||
"insecure": false,
|
||||
"caData": "<base64 encoded certificate>"
|
||||
}
|
||||
}
|
||||
|||
|
||||
name: mycluster.com
|
||||
server: https://mycluster.com
|
||||
kind: Secret
|
||||
metadata:
|
||||
labels:
|
||||
argocd.argoproj.io/secret-type: cluster
|
||||
name: mycluster.com-443
|
||||
type: Opaque
|
||||
```
|
||||
|
||||
## Helm Chart repositories
|
||||
|
||||
Non standard Helm Chart repositories have to be registered using `helm.repositories` in `argocd-cm` config map. Each repository must have `url` and `name` fields.
|
||||
For private Helm repos you might configure access credentials and HTTPS settings using `usernameSecret`, `passwordSecret`, `caSecret`, `certSecret` and `keySecret` fields.
|
||||
|
||||
Example:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
data:
|
||||
helm.repositories: |
|
||||
- url: https://argoproj.github.io/argo-helm
|
||||
name: argo
|
||||
caUsername:
|
||||
name: my-secret
|
||||
key: username
|
||||
caPassword:
|
||||
name: my-secret
|
||||
key: password
|
||||
caSecret:
|
||||
name: my-secret
|
||||
key: ca
|
||||
certSecret:
|
||||
name: my-secret
|
||||
key: cert
|
||||
keySecret:
|
||||
name: my-secret
|
||||
key: key
|
||||
metadata:
|
||||
name: argocd-cm
|
||||
```
|
||||
|
||||
## SSO & RBAC
|
||||
|
||||
* SSO configuration details: [SSO](sso.md)
|
||||
* RBAC configuration details: [RBAC](rbac.md)
|
||||
|
||||
## Manage Argo CD using Argo CD
|
||||
|
||||
Argo CD is able to manage itself since all settings are represented by Kubernetes manifests. The suggested way is to create [Kustomize](https://github.com/kubernetes-sigs/kustomize)
|
||||
based application which uses base Argo CD manifests from https://github.com/argoproj/argo-cd and apply required changes on top.
|
||||
|
||||
Example of `kustomization.yaml`:
|
||||
|
||||
```yaml
|
||||
bases:
|
||||
- github.com/argoproj/argo-cd//manifests/cluster-install?ref=v0.10.6
|
||||
|
||||
# additional resources like ingress rules, cluster and repository secrets.
|
||||
resources:
|
||||
- clusters-secrets.yaml
|
||||
- repos-secrets.yaml
|
||||
|
||||
# changes to config maps
|
||||
patchesStrategicMerge:
|
||||
- overlays/argo-cd-cm.yaml
|
||||
```
|
||||
|
||||
The live example of self managed Argo CD config is available at https://cd.apps.argoproj.io and with configuration
|
||||
stored at [argoproj/argoproj-deployments](https://github.com/argoproj/argoproj-deployments/tree/master/argocd).
|
||||
> NOTE: You will need to sign-in using your github account to get access to https://cd.apps.argoproj.io
|
||||
16
docs/faq.md
Normal file
16
docs/faq.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# FAQ
|
||||
|
||||
## Why is my application still `OutOfSync` immediately after a successful Sync?
|
||||
|
||||
It is possible for an application to still be `OutOfSync` even immediately after a successful Sync
|
||||
operation. Some reasons for this might be:
|
||||
* There may be problems in manifests themselves, which may contain extra/unknown fields from the
|
||||
actual K8s spec. These extra fields would get dropped when querying Kubernetes for the live state,
|
||||
resulting in an `OutOfSync` status indicating a missing field was detected.
|
||||
* The sync was performed (with pruning disabled), and there are resources which need to be deleted.
|
||||
* A mutating webhook altered the manifest after it was submitted to Kubernetes
|
||||
|
||||
To debug `OutOfSync` issues, run the `app diff` command to see the differences between git and live:
|
||||
```
|
||||
argocd app diff APPNAME
|
||||
```
|
||||
@@ -1,111 +1,110 @@
|
||||
# ArgoCD Getting Started
|
||||
# Argo CD Getting Started
|
||||
|
||||
An example guestbook application is provided to demonstrate how ArgoCD works.
|
||||
An example guestbook application is provided to demonstrate how Argo CD 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
|
||||
```
|
||||
## 1. Install Argo CD
|
||||
```bash
|
||||
kubectl create namespace argocd
|
||||
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/v0.7.1/manifests/install.yaml
|
||||
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/v0.10.6/manifests/install.yaml
|
||||
```
|
||||
This will create a new namespace, `argocd`, where ArgoCD services and application resources will live.
|
||||
This will create a new namespace, `argocd`, where Argo CD 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
|
||||
```
|
||||
```bash
|
||||
kubectl create clusterrolebinding YOURNAME-cluster-admin-binding --clusterrole=cluster-admin --user=YOUREMAIL@gmail.com
|
||||
```
|
||||
|
||||
## 2. Download ArgoCD CLI
|
||||
## 2. Download Argo CD CLI
|
||||
|
||||
Download the latest ArgoCD version:
|
||||
Download the latest Argo CD version:
|
||||
|
||||
On Mac:
|
||||
```
|
||||
```bash
|
||||
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
|
||||
```bash
|
||||
curl -sSL -o /usr/local/bin/argocd https://github.com/argoproj/argo-cd/releases/download/v0.10.6/argocd-linux-amd64
|
||||
chmod +x /usr/local/bin/argocd
|
||||
```
|
||||
|
||||
## 3. Open access to ArgoCD API server
|
||||
## 3. Access the Argo CD 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`:
|
||||
By default, the Argo CD API server is not exposed with an external IP. To access the API server,
|
||||
choose one of the following means to expose the Argo CD API server:
|
||||
|
||||
```
|
||||
### Service Type LoadBalancer
|
||||
Change the argocd-server service type to `LoadBalancer`:
|
||||
|
||||
```bash
|
||||
kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "LoadBalancer"}}'
|
||||
```
|
||||
|
||||
## 4. Login to the server from the CLI
|
||||
### Ingress
|
||||
Follow the [ingress documentation](ingress.md) on how to configure Argo CD with ingress.
|
||||
|
||||
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:
|
||||
```
|
||||
### Port Forwarding
|
||||
`kubectl port-forward` can also be used to connect to the API server without exposing the service.
|
||||
The API server can be accessed using the localhost address/port.
|
||||
|
||||
|
||||
## 4. Login using the CLI
|
||||
|
||||
Login as the `admin` user. The initial password is autogenerated to be the pod name of the
|
||||
Argo CD API server. This can be retrieved with the command:
|
||||
```bash
|
||||
kubectl get pods -n argocd -l app=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:
|
||||
```
|
||||
Using the above password, login to Argo CD's external IP:
|
||||
```bash
|
||||
kubectl get svc -n argocd argocd-server
|
||||
argocd login <EXTERNAL-IP>
|
||||
```
|
||||
|
||||
After logging in, change the password using the command:
|
||||
```
|
||||
```bash
|
||||
argocd account update-password
|
||||
argocd relogin
|
||||
```
|
||||
|
||||
|
||||
## 5. Register a cluster to deploy apps to
|
||||
## 5. Register a cluster to deploy apps to (optional)
|
||||
|
||||
We will now register a cluster to deploy applications to. First list all clusters contexts in your
|
||||
kubconfig:
|
||||
```
|
||||
This step registers a cluster's credentials to Argo CD, and is only necessary when deploying to
|
||||
an external cluster. When deploying internally (to the same cluster that Argo CD is running in),
|
||||
https://kubernetes.default.svc should be used as the application's K8s API server address.
|
||||
|
||||
First list all clusters contexts in your current kubconfig:
|
||||
```bash
|
||||
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
|
||||
for docker-for-desktop context, run:
|
||||
```bash
|
||||
argocd cluster add docker-for-desktop
|
||||
```
|
||||
|
||||
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
|
||||
associated with the supplied kubectl context. Argo CD uses this 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
|
||||
## 6. Create an application from a git repository location
|
||||
|
||||
### Creating apps via UI
|
||||
|
||||
Open a browser to the ArgoCD external UI, and login using the credentials set in step 4.
|
||||
Open a browser to the Argo CD external UI, and login using the credentials set in step 4, and the
|
||||
external IP/hostname 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
|
||||
Connect a git repository containing your apps if repository is private. An example repository containing a sample
|
||||
guestbook application is available at https://github.com/argoproj/argocd-example-apps.git.
|
||||
|
||||

|
||||
@@ -120,10 +119,10 @@ 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:
|
||||
Applications can be also be created using the Argo CD CLI:
|
||||
|
||||
```
|
||||
argocd app create guestbook-default --repo https://github.com/argoproj/argocd-example-apps.git --path guestbook --env default
|
||||
```bash
|
||||
argocd app create guestbook-default --repo https://github.com/argoproj/argocd-example-apps.git --path ksonnet-guestbook
|
||||
```
|
||||
|
||||
## 7. Sync (deploy) the application
|
||||
@@ -131,10 +130,10 @@ argocd app create guestbook-default --repo https://github.com/argoproj/argocd-ex
|
||||
Once the guestbook application is created, you can now view its status:
|
||||
|
||||
From UI:
|
||||

|
||||

|
||||
|
||||
From CLI:
|
||||
```
|
||||
```bash
|
||||
$ argocd app get guestbook-default
|
||||
Name: guestbook-default
|
||||
Server: https://kubernetes.default.svc
|
||||
@@ -153,7 +152,7 @@ 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:
|
||||
|
||||
```
|
||||
```bash
|
||||
$ argocd app sync guestbook-default
|
||||
Application: guestbook-default
|
||||
Operation: Sync
|
||||
@@ -165,13 +164,13 @@ 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:
|
||||
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 status:
|
||||
|
||||

|
||||
|
||||
## 8. Next Steps
|
||||
|
||||
ArgoCD supports additional features such as SSO, WebHooks, RBAC, Projects. See the rest of
|
||||
the [documentation](./) for details.
|
||||
Argo CD supports additional features such as automated sync, SSO, WebHooks, RBAC, Projects. See the
|
||||
rest of the [documentation](./) for details.
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
# Resource Health
|
||||
|
||||
## Overview
|
||||
ArgoCD provides built-in health assessment for several standard Kubernetes types, which is then
|
||||
Argo CD 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.
|
||||
|
||||
@@ -16,3 +15,6 @@ 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`.
|
||||
|
||||
### PersistentVolumeClaim
|
||||
* The `status.phase` is `Bound`
|
||||
|
||||
176
docs/ingress.md
Normal file
176
docs/ingress.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# Ingress Configuration
|
||||
|
||||
Argo CD runs both a gRPC server (used by the CLI), as well as a HTTP/HTTPS server (used by the UI).
|
||||
Both protocols are exposed by the argocd-server service object on the following ports:
|
||||
* 443 - gRPC/HTTPS
|
||||
* 80 - HTTP (redirects to HTTPS)
|
||||
|
||||
There are several ways how Ingress can be configured.
|
||||
|
||||
## [kubernetes/ingress-nginx](https://github.com/kubernetes/ingress-nginx)
|
||||
|
||||
### Option 1: ssl-passthrough
|
||||
|
||||
Because Argo CD serves multiple protocols (gRPC/HTTPS) on the same port (443), this provides a
|
||||
challenge when attempting to define a single nginx ingress object and rule for the argocd-service,
|
||||
since the `nginx.ingress.kubernetes.io/backend-protocol` [annotation](https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#backend-protocol)
|
||||
accepts only a single value for the backend protocol (e.g. HTTP, HTTPS, GRPC, GRPCS).
|
||||
|
||||
In order to expose the Argo CD API server with a single ingress rule and hostname, the
|
||||
`nginx.ingress.kubernetes.io/ssl-passthrough` [annotation](https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#ssl-passthrough)
|
||||
must be used to passthrough TLS connections and terminate TLS at the Argo CD API server.
|
||||
|
||||
```yaml
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: argocd-server-ingress
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: nginx
|
||||
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||
nginx.ingress.kubernetes.io/ssl-passthrough: "true"
|
||||
spec:
|
||||
rules:
|
||||
- host: argocd.example.com
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: argocd-server
|
||||
servicePort: https
|
||||
```
|
||||
|
||||
The above rule terminates TLS at the Argo CD API server, which detects the protocol being used,
|
||||
and responds appropriately. Note that the `nginx.ingress.kubernetes.io/ssl-passthrough` annotation
|
||||
requires that the `--enable-ssl-passthrough` flag be added to the command line arguments to
|
||||
`nginx-ingress-controller`.
|
||||
|
||||
### Option 2: Multiple ingress objects and hosts
|
||||
|
||||
Since ingress-nginx Ingress supports only a single protocol per Ingress object, an alternative
|
||||
way would be to define two Ingress objects. One for HTTP/HTTPS, and the other for gRPC:
|
||||
|
||||
HTTP/HTTPS Ingress:
|
||||
```yaml
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: argocd-server-http-ingress
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: "nginx"
|
||||
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
|
||||
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
|
||||
spec:
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: argocd-server
|
||||
servicePort: http
|
||||
host: argocd.example.com
|
||||
tls:
|
||||
- hosts:
|
||||
- argocd.example.com
|
||||
secretName: argocd-secret
|
||||
```
|
||||
|
||||
gRPC Ingress:
|
||||
```yaml
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: argocd-server-grpc-ingress
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: "nginx"
|
||||
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
|
||||
spec:
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: argocd-server
|
||||
servicePort: https
|
||||
host: grpc.argocd.example.com
|
||||
tls:
|
||||
- hosts:
|
||||
- grpc.argocd.example.com
|
||||
secretName: argocd-secret
|
||||
```
|
||||
|
||||
The API server should then be run with TLS disabled. Edit the `argocd-server` deployment to add the
|
||||
`--insecure` flag to the argocd-server command:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
name: argocd-server
|
||||
containers:
|
||||
- command:
|
||||
- /argocd-server
|
||||
- --staticassets
|
||||
- /shared/app
|
||||
- --repo-server
|
||||
- argocd-repo-server:8081
|
||||
- --insecure
|
||||
```
|
||||
|
||||
The obvious disadvantage to this approach is that this technique require two separate hostnames for
|
||||
the API server -- one for gRPC and the other for HTTP/HTTPS. However it allow TLS termination to
|
||||
happen at the ingress controller.
|
||||
|
||||
|
||||
## AWS Application Load Balancers (ALBs) and Classic ELB (HTTP mode)
|
||||
|
||||
Neither ALBs and Classic ELB in HTTP mode, do not have full support for HTTP2/gRPC which is the
|
||||
protocol used by the `argocd` CLI. Thus, when using an AWS load balancer, either Classic ELB in
|
||||
passthrough mode is needed, or NLBs.
|
||||
|
||||
|
||||
## UI base path
|
||||
|
||||
If Argo CD UI is available under non-root path (e.g. `/argo-cd` instead of `/`) then UI path should be configured in API server.
|
||||
To configure UI path add `--basehref` flag into `argocd-server` deployment command:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
name: argocd-server
|
||||
containers:
|
||||
- command:
|
||||
- /argocd-server
|
||||
- --staticassets
|
||||
- /shared/app
|
||||
- --repo-server
|
||||
- argocd-repo-server:8081
|
||||
- --base-href
|
||||
- /argo-cd
|
||||
```
|
||||
|
||||
NOTE: flag `--basehref` only changes UI base URL. API server keep using `/` path so you need to add URL rewrite rule to proxy config.
|
||||
Example nginx.conf with URL rewrite:
|
||||
|
||||
```
|
||||
worker_processes 1;
|
||||
|
||||
events { worker_connections 1024; }
|
||||
|
||||
http {
|
||||
|
||||
sendfile on;
|
||||
|
||||
server {
|
||||
listen 443;
|
||||
|
||||
location /argo-cd {
|
||||
rewrite /argo-cd/(.*) /$1 break;
|
||||
proxy_pass https://localhost:8080;
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Host $server_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
46
docs/internal/releasing.md
Normal file
46
docs/internal/releasing.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Argo CD Release Instructions
|
||||
|
||||
1. Tag, build, and push argo-cd-ui
|
||||
```bash
|
||||
cd argo-cd-ui
|
||||
git checkout -b release-X.Y
|
||||
git tag vX.Y.Z
|
||||
git push upstream release-X.Y --tags
|
||||
IMAGE_NAMESPACE=argoproj IMAGE_TAG=vX.Y.Z DOCKER_PUSH=true yarn docker
|
||||
```
|
||||
|
||||
2. Create release-X.Y branch (if creating initial X.Y release)
|
||||
```bash
|
||||
git checkout -b release-X.Y
|
||||
git push upstream release-X.Y
|
||||
```
|
||||
|
||||
3. Update VERSION and manifests with new version
|
||||
```bash
|
||||
vi VERSION # ensure value is desired X.Y.Z semantic version
|
||||
vi manifests/base/kustomization.yaml # update with new image tags
|
||||
make manifests
|
||||
git commit -a -m "Update manifests to vX.Y.Z"
|
||||
git push upstream release-X.Y
|
||||
```
|
||||
|
||||
4. Tag, build, and push release to docker hub
|
||||
```bash
|
||||
git tag vX.Y.Z
|
||||
make release IMAGE_NAMESPACE=argoproj IMAGE_TAG=vX.Y.Z DOCKER_PUSH=true
|
||||
git push upstream vX.Y.Z
|
||||
```
|
||||
|
||||
5. Update argocd brew formula
|
||||
```bash
|
||||
git clone https://github.com/argoproj/homebrew-tap
|
||||
cd homebrew-tap
|
||||
./update.sh ~/go/src/github.com/argoproj/argo-cd/dist/argocd-darwin-amd64
|
||||
git commit -a -m "Update argocd to vX.Y.Z"
|
||||
git push
|
||||
```
|
||||
|
||||
6. Update documentation:
|
||||
* Edit CHANGELOG.md with release notes
|
||||
* Update getting_started.md with new version
|
||||
* Create GitHub release from new tag and upload binaries (e.g. dist/argocd-darwin-amd64)
|
||||
@@ -1,14 +1,14 @@
|
||||
# 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.
|
||||
Argo CD provides a mechanism to override the parameters of a ksonnet/helm app. This provides flexibility
|
||||
in having most of the application manifests defined in git, while leaving room for *some* parts of the
|
||||
k8s manifests determined dynamically, or outside of git. It also serves as an alternative way of
|
||||
redeploying an application by changing application parameters via Argo CD, 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
|
||||
truth becomes a union of the git repository, and the application overrides. The Argo CD parameter
|
||||
overrides feature is provided mainly as a convenience to developers and is intended to be used in
|
||||
dev/test environments, vs. production environments.
|
||||
|
||||
To use parameter overrides, run the `argocd app set -p (COMPONENT=)PARAM=VALUE` command:
|
||||
@@ -17,21 +17,28 @@ argocd app set guestbook -p guestbook=image=example/guestbook:abcd123
|
||||
argocd app sync guestbook
|
||||
```
|
||||
|
||||
The following are situations where parameter overrides would be useful:
|
||||
The `PARAM` is expected to be a normal YAML path
|
||||
|
||||
```bash
|
||||
argocd app set guestbook -p guestbook=ingress.enabled=true
|
||||
argocd app set guestbook -p guestbook=ingress.hosts[0]=guestbook.myclusterurl
|
||||
```
|
||||
|
||||
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`
|
||||
case, the application would expose a parameter named `image`, whose value used in the `dev`
|
||||
environment contains a placeholder value (e.g. `example/guestbook:replaceme`). The placeholder value
|
||||
would be determined externally (outside of git) such as a build systems. Then, as part of the build
|
||||
pipeline, the parameter value of the `image` would be continually updated to the freshly built image
|
||||
would be determined externally (outside of git) such as a build system. Then, as part of the build
|
||||
pipeline, the parameter value of the `image` would be continually updated to the freshly built image
|
||||
(e.g. `argocd app set guestbook -p guestbook=image=example/guestbook:abcd123`). A sync operation
|
||||
would result in the application being redeployed with the new image.
|
||||
|
||||
2. A repository of helm manifests is already publicly available (e.g. https://github.com/helm/charts).
|
||||
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
|
||||
the public repository and customize 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:
|
||||
|
||||
```
|
||||
|
||||
181
docs/projects.md
Normal file
181
docs/projects.md
Normal file
@@ -0,0 +1,181 @@
|
||||
## Projects
|
||||
|
||||
Projects provide a logical grouping of applications, which is useful when Argo CD is used by multiple
|
||||
teams. Projects provide the following features:
|
||||
|
||||
* ability to restrict *what* may be deployed (the git source repositories)
|
||||
* ability to restrict *where* apps may be deployed (the destination clusters and namespaces)
|
||||
* ability to control what type of objects may be deployed (e.g. RBAC, CRDs, DaemonSets, NetworkPolicy etc...)
|
||||
|
||||
### The default project
|
||||
|
||||
Every application belongs to a single project. If unspecified, an application belongs to the
|
||||
`default` project, which is created automatically and by default, permits deployments from any
|
||||
source repo, to any cluster, and all resource Kinds. The default project can be modified, but not
|
||||
deleted. When initially created, it's specification is configured to be the most permissive:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
sourceRepos:
|
||||
- '*'
|
||||
destinations:
|
||||
- namespace: '*'
|
||||
server: '*'
|
||||
clusterResourceWhitelist:
|
||||
- group: '*'
|
||||
kind: '*'
|
||||
```
|
||||
|
||||
### Creating Projects
|
||||
|
||||
Additional projects can be created to give separate teams different levels of access to namespaces.
|
||||
The following command creates a new project `myproject` which can deploy applications to namespace
|
||||
`mynamespace` of cluster `https://kubernetes.default.svc`. The permitted git source repository is
|
||||
set to `https://github.com/argoproj/argocd-example-apps.git` repository.
|
||||
|
||||
```
|
||||
argocd proj create myproject -d https://kubernetes.default.svc,mynamespace -s https://github.com/argoproj/argocd-example-apps.git
|
||||
```
|
||||
|
||||
### Managing Projects
|
||||
|
||||
Permitted source git repositories are managed using commands:
|
||||
|
||||
```bash
|
||||
argocd proj add-source <PROJECT> <REPO>
|
||||
argocd proj remove-source <PROJECT> <REPO>
|
||||
```
|
||||
|
||||
Permitted destination clusters and namespaces are managed with the commands:
|
||||
```
|
||||
argocd proj add-destination <PROJECT> <CLUSTER>,<NAMESPACE>
|
||||
argocd proj remove-destination <PROJECT> <CLUSTER>,<NAMESPACE>
|
||||
```
|
||||
|
||||
Permitted destination K8s resource kinds are managed with the commands. Note that namespaced-scoped
|
||||
resources are restricted via a blacklist, whereas cluster-scoped resources are restricted via
|
||||
whitelist.
|
||||
```
|
||||
argocd proj allow-cluster-resource <PROJECT> <GROUP> <KIND>
|
||||
argocd proj allow-namespace-resource <PROJECT> <GROUP> <KIND>
|
||||
argocd proj deny-cluster-resource <PROJECT> <GROUP> <KIND>
|
||||
argocd proj deny-namespace-resource <PROJECT> <GROUP> <KIND>
|
||||
```
|
||||
|
||||
### Assign application to a project
|
||||
|
||||
The application project can be changed using `app set` command. In order to change the project of
|
||||
an app, the user must have permissions to access the new project.
|
||||
|
||||
```
|
||||
argocd app set guestbook-default --project myproject
|
||||
```
|
||||
|
||||
### Configuring RBAC with projects
|
||||
|
||||
Once projects have been defined, RBAC rules can be written to restrict access to the applications
|
||||
in the project. The following example configures RBAC for two GitHub teams: `team1` and `team2`,
|
||||
both in the GitHub org, `some-github-org`. There are two projects, `project-a` and `project-b`.
|
||||
`team1` can only manage applications in `project-a`, while `team2` can only manage applications in
|
||||
`project-b`. Both `team1` and `team2` have the ability to manage repositories.
|
||||
|
||||
*ConfigMap `argocd-rbac-cm` example:*
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-rbac-cm
|
||||
data:
|
||||
policy.default: ""
|
||||
policy.csv: |
|
||||
p, some-github-org:team1, applications, *, project-a/*, allow
|
||||
p, some-github-org:team2, applications, *, project-a/*, allow
|
||||
|
||||
p, role:org-admin, repositories, get, *, allow
|
||||
p, role:org-admin, repositories, create, *, allow
|
||||
p, role:org-admin, repositories, update, *, allow
|
||||
p, role:org-admin, repositories, delete, *, allow
|
||||
|
||||
g, some-github-org:team1, org-admin
|
||||
g, some-github-org:team2, org-admin
|
||||
```
|
||||
|
||||
## Project Roles
|
||||
|
||||
Projects include a feature called roles that enable automated access to a project's applications.
|
||||
These can be used to give a CI pipeline a restricted set of permissions. For example, a CI system
|
||||
may only be able to sync a single app (but not change its source or destination).
|
||||
|
||||
Projects can have multiple roles, and those roles can have different access granted to them. These
|
||||
permissions are called policies, and they are stored within the role as a list of policy strings.
|
||||
A role's policy can only grant access to that role and are limited to applications within the role's
|
||||
project. However, the policies have an option for granting wildcard access to any application
|
||||
within a project.
|
||||
|
||||
In order to create roles in a project and add policies to a role, a user will need permission to
|
||||
update a project. The following commands can be used to manage a role.
|
||||
|
||||
```bash
|
||||
argoproj proj role list
|
||||
argoproj proj role get
|
||||
argoproj proj role create
|
||||
argoproj proj role delete
|
||||
argoproj proj role add-policy
|
||||
argoproj proj role remove-policy
|
||||
```
|
||||
|
||||
Project roles in itself are not useful without generating a token to associate to that role. Argo CD
|
||||
supports JWT tokens as the means to authenticate to a role. Since the JWT token is
|
||||
associated with a role's policies, any changes to the role's policies will immediately take effect
|
||||
for that JWT token.
|
||||
|
||||
The following commands are used to manage the JWT tokens.
|
||||
|
||||
```bash
|
||||
argoproj proj role create-token PROJECT ROLE-NAME
|
||||
argoproj proj role delete-token PROJECT ROLE-NAME ISSUED-AT
|
||||
```
|
||||
|
||||
Since the JWT tokens aren't stored in Argo CD, they can only be retrieved when they are created. A
|
||||
user can leverage them in the cli by either passing them in using the `--auth-token` flag or setting
|
||||
the ARGOCD_AUTH_TOKEN environment variable. The JWT tokens can be used until they expire or are
|
||||
revoked. The JWT tokens can created with or without an expiration, but the default on the cli is
|
||||
creates them without an expirations date. Even if a token has not expired, it cannot be used if
|
||||
the token has been revoked.
|
||||
|
||||
Below is an example of leveraging a JWT token to access a guestbook application. It makes the
|
||||
assumption that the user already has a project named myproject and an application called
|
||||
guestbook-default.
|
||||
|
||||
```bash
|
||||
PROJ=myproject
|
||||
APP=guestbook-default
|
||||
ROLE=get-role
|
||||
argocd proj role create $PROJ $ROLE
|
||||
argocd proj role create-token $PROJ $ROLE -e 10m
|
||||
JWT=<value from command above>
|
||||
argocd proj role list $PROJ
|
||||
argocd proj role get $PROJ $ROLE
|
||||
|
||||
# This command will fail because the JWT Token associated with the project role does not have a policy to allow access to the application
|
||||
argocd app get $APP --auth-token $JWT
|
||||
# Adding a policy to grant access to the application for the new role
|
||||
argocd proj role add-policy $PROJ $ROLE --action get --permission allow --object $APP
|
||||
argocd app get $PROJ-$ROLE --auth-token $JWT
|
||||
|
||||
# Removing the policy we added and adding one with a wildcard.
|
||||
argocd proj role remove-policy $PROJ $TOKEN -a get -o $PROJ-$TOKEN
|
||||
argocd proj role remove-policy $PROJ $TOKEN -a get -o '*'
|
||||
# The wildcard allows us to access the application due to the wildcard.
|
||||
argocd app get $PROJ-$TOKEN --auth-token $JWT
|
||||
argocd proj role get $PROJ
|
||||
|
||||
|
||||
argocd proj role get $PROJ $ROLE
|
||||
# Revoking the JWT token
|
||||
argocd proj role delete-token $PROJ $ROLE <id field from the last command>
|
||||
# This will fail since the JWT Token was deleted for the project role.
|
||||
argocd app get $APP --auth-token $JWT
|
||||
```
|
||||
|
||||
105
docs/rbac.md
105
docs/rbac.md
@@ -2,102 +2,39 @@
|
||||
|
||||
## 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.
|
||||
The RBAC feature enables restriction of access to Argo CD resources. Argo CD does not have its own
|
||||
user management system and has only one built-in user `admin`. The `admin` user is a superuser and
|
||||
it has unrestricted access to the system. RBAC requires [SSO configuration](./sso.md). Once SSO is
|
||||
configured, additional RBAC roles can be defined, and SSO groups can man be mapped to roles.
|
||||
|
||||
## 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.
|
||||
RBAC configuration allows defining roles and groups. Argo CD has two pre-defined roles:
|
||||
* `role:readonly` - read-only access to all resources
|
||||
* `role:admin` - unrestricted access to all resources
|
||||
These role definitions can be seen in [builtin-policy.csv](../util/rbac/builtin-policy.csv)
|
||||
|
||||
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.
|
||||
Additional roles and groups can be configured in `argocd-rbac-cm` ConfigMap. The example below
|
||||
configures a custom role, named `org-admin`. The role is assigned to any user which belongs to
|
||||
`your-github-org:your-team` group. All other users get the default policy of `role:readonly`,
|
||||
which cannot modify Argo CD settings.
|
||||
|
||||
*ConfigMap `argocd-rbac-cm` example:*
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-rbac-cm
|
||||
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, *
|
||||
p, role:org-admin, applications, *, */*, allow
|
||||
p, role:org-admin, clusters, get, *, allow
|
||||
p, role:org-admin, repositories, get, *, allow
|
||||
p, role:org-admin, repositories, create, *, allow
|
||||
p, role:org-admin, repositories, update, *, allow
|
||||
p, role:org-admin, repositories, delete, *, allow
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
@@ -22,9 +22,9 @@ metadata:
|
||||
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/)
|
||||
During a Sync operation, Argo CD 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 a Pod,
|
||||
[Job](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.
|
||||
|
||||
@@ -35,7 +35,7 @@ The following hooks are defined:
|
||||
|------|-------------|
|
||||
| `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) |
|
||||
| `Skip` | Indicates to Argo CD 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. |
|
||||
|
||||
|
||||
@@ -50,12 +50,12 @@ metadata:
|
||||
generateName: integration-test-
|
||||
annotations:
|
||||
argocd.argoproj.io/hook: PostSync
|
||||
argocd.argoproj.io/hook-delete-policy: OnSuccess
|
||||
argocd.argoproj.io/hook-delete-policy: HookSucceeded
|
||||
```
|
||||
|
||||
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. |
|
||||
| `HookSucceeded` | The hook resource is deleted after the hook succeeded (e.g. Job/Workflow completed successfully). |
|
||||
| `HookFailed` | The hook resource is deleted after the hook failed. |
|
||||
|
||||
62
docs/sso.md
62
docs/sso.md
@@ -2,42 +2,53 @@
|
||||
|
||||
## 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.
|
||||
There are two ways that SSO can be configured:
|
||||
|
||||
This document describes how to configure ArgoCD SSO using GitHub (OAuth2) as an example, but the
|
||||
* Bundled Dex OIDC provider - use this option your current provider does not support OIDC (e.g. SAML,
|
||||
LDAP) or if you wish to leverage any of Dex's connector features (e.g. the ability to map GitHub
|
||||
organizations and teams to OIDC groups claims).
|
||||
|
||||
* Existing OIDC provider - use this if you already have an OIDC provider which you are using (e.g.
|
||||
Okta, OneLogin, Auth0, Microsoft), where you manage your users, groups, and memberships.
|
||||
|
||||
## Dex
|
||||
|
||||
Argo CD 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 Argo CD 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 Argo CD 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).
|
||||
endpoint of your Argo CD URL (e.g. https://argocd.example.com/api/dex/callback).
|
||||
|
||||

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

|
||||
|
||||
### 2. Configure ArgoCD for SSO
|
||||
### 2. Configure Argo CD 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 `url` key, input the base URL of Argo CD. 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
|
||||
* You will very likely want to restrict logins to one or 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.
|
||||
then be able to login to Argo CD to perform management tasks.
|
||||
|
||||
```
|
||||
data:
|
||||
@@ -65,15 +76,6 @@ data:
|
||||
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.
|
||||
@@ -82,6 +84,22 @@ 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
|
||||
Argo CD 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)
|
||||
|
||||
|
||||
## Existing OIDC provider
|
||||
|
||||
To configure Argo CD to delegate authenticate to your existing OIDC provider, add the OAuth2
|
||||
configuration to the `argocd-cm` ConfigMap under the `oidc.config` key:
|
||||
|
||||
```
|
||||
data:
|
||||
url: https://argocd.example.com
|
||||
|
||||
oidc.config: |
|
||||
name: Okta
|
||||
issuer: https://dev-123456.oktapreview.com
|
||||
clientID: aaaabbbbccccddddeee
|
||||
clientSecret: $oidc.okta.clientSecret
|
||||
```
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
# Tracking and Deployment Strategies
|
||||
|
||||
An ArgoCD application spec provides several different ways of track kubernetes resource manifests in
|
||||
An Argo CD application spec provides several different ways of track kubernetes resource manifests in
|
||||
git. This document describes the different techniques and the means of deploying those manifests to
|
||||
the target environment.
|
||||
|
||||
## Branch Tracking
|
||||
## HEAD / 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.
|
||||
If a branch name, or a symbolic reference (like HEAD) is specified, Argo CD will continually compare
|
||||
live state against the resource manifests defined at the tip of the specified branch or the
|
||||
dereferenced commit of the symbolic reference.
|
||||
|
||||
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.
|
||||
changes to the tracked branch/symbolic reference, which will then be detected by Argo CD controller.
|
||||
|
||||
## Tag Tracking
|
||||
|
||||
If a tag is specified, the manifests at the specified git tag will be used to perform the sync
|
||||
If a tag is specified, the manifests at the specified git tag will be used to perform the sync
|
||||
comparison. This provides some advantages over branch tracking in that a tag is generally considered
|
||||
more stable, and less frequently updated, with some manual judgement of what constitutes a tag.
|
||||
|
||||
To redeploy an application, the user uses git to change the meaning of a tag by retagging it to a
|
||||
different commit SHA. ArgoCD will detect the new meaning of the tag when performing the
|
||||
different commit SHA. Argo CD will detect the new meaning of the tag when performing the
|
||||
comparison/sync.
|
||||
|
||||
## Commit Pinning
|
||||
@@ -33,9 +34,9 @@ which is pinned to a commit, is by updating the tracking revision in the applica
|
||||
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))
|
||||
## Automated Sync
|
||||
|
||||
In all tracking strategies, the application will have the option to sync automatically. If auto-sync
|
||||
In all tracking strategies, the application has the option to sync automatically. If [auto-sync](auto_sync.md)
|
||||
is configured, the new resources manifests will be applied automatically -- as soon as a difference
|
||||
is detected between the target state (git) and live state. If auto-sync is disabled, a manual sync
|
||||
will be needed using the Argo UI, CLI, or API.
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
|
||||
## 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
|
||||
Argo CD 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. Argo CD supports
|
||||
git webhook notifications from GitHub, GitLab, and BitBucket. The following explains how to configure
|
||||
a git webhook for GitHub, but the same process should be applicable to other providers.
|
||||
|
||||
### 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
|
||||
URL configured in the git provider should use the /api/webhook endpoint of your Argo CD instance
|
||||
(e.g. https://argocd.example.com/api/webhook). 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
|
||||
### 2. Configure Argo CD 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.
|
||||
@@ -27,14 +27,14 @@ webhook secret configured in step 1.
|
||||
| GitLab | `gitlab.webhook.secret` |
|
||||
| BitBucket | `bitbucket.webhook.uuid` |
|
||||
|
||||
Edit the ArgoCD kubernetes secret:
|
||||
Edit the Argo CD 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
|
||||
Simply copy the shared webhook secret created in step 1, to the corresponding
|
||||
GitHub/GitLab/BitBucket key under the `stringData` field:
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"Exclude": [
|
||||
"pkg/client",
|
||||
"vendor/",
|
||||
".pb.go"
|
||||
".pb.go",
|
||||
".*warning.*fmt.Fprint"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -59,13 +59,13 @@ go build -i -o dist/protoc-gen-grpc-gateway ./vendor/github.com/grpc-ecosystem/g
|
||||
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" \))
|
||||
PROTO_FILES=$(find $PROJECT_ROOT \( -name "*.proto" -and -path '*/server/*' -or -path '*/reposerver/*' -and -name "*.proto" -or -path '*/controller/*' -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
|
||||
GOOGLE_PROTO_API_PATH=/root/go/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis
|
||||
GOGO_PROTOBUF_PATH=/root/go/src/github.com/gogo/protobuf
|
||||
GOOGLE_PROTO_API_PATH=$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis
|
||||
GOGO_PROTOBUF_PATH=$GOPATH/src/github.com/gogo/protobuf
|
||||
else
|
||||
GOOGLE_PROTO_API_PATH=${PROJECT_ROOT}/vendor/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis
|
||||
GOGO_PROTOBUF_PATH=${PROJECT_ROOT}/vendor/github.com/gogo/protobuf
|
||||
@@ -117,6 +117,7 @@ clean_swagger() {
|
||||
/usr/bin/find "${SWAGGER_ROOT}" -name '*.swagger.json' -delete
|
||||
}
|
||||
|
||||
collect_swagger server 15
|
||||
collect_swagger server 21
|
||||
clean_swagger server
|
||||
clean_swagger reposerver
|
||||
clean_swagger controller
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,11 +1,25 @@
|
||||
#!/bin/sh
|
||||
|
||||
IMAGE_NAMESPACE=${IMAGE_NAMESPACE:='argoproj'}
|
||||
IMAGE_TAG=${IMAGE_TAG:='latest'}
|
||||
set -e
|
||||
|
||||
for i in "$(ls manifests/components/*.yaml)"; do
|
||||
sed -i '' 's@\( image: \(.*\)/\(argocd-.*\):.*\)@ image: '"${IMAGE_NAMESPACE}"'/\3:'"${IMAGE_TAG}"'@g' $i
|
||||
done
|
||||
SRCROOT="$( CDPATH='' cd -- "$(dirname "$0")/.." && pwd -P )"
|
||||
AUTOGENMSG="# This is an auto-generated file. DO NOT EDIT"
|
||||
|
||||
echo "# This is an auto-generated file. DO NOT EDIT" > manifests/install.yaml
|
||||
cat manifests/components/*.yaml >> manifests/install.yaml
|
||||
update_image () {
|
||||
if [ ! -z "${IMAGE_NAMESPACE}" ]; then
|
||||
sed 's| image: \(.*\)/\(argocd.*\)| image: '"${IMAGE_NAMESPACE}"'/\2|g' "${1}" > "${1}.bak"
|
||||
mv "${1}.bak" "${1}"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ ! -z "${IMAGE_TAG}" ]; then
|
||||
(cd ${SRCROOT}/manifests/base && kustomize edit set imagetag argoproj/argocd:${IMAGE_TAG} argoproj/argocd-ui:${IMAGE_TAG})
|
||||
fi
|
||||
|
||||
echo "${AUTOGENMSG}" > "${SRCROOT}/manifests/install.yaml"
|
||||
kustomize build "${SRCROOT}/manifests/cluster-install" >> "${SRCROOT}/manifests/install.yaml"
|
||||
update_image "${SRCROOT}/manifests/install.yaml"
|
||||
|
||||
echo "${AUTOGENMSG}" > "${SRCROOT}/manifests/namespace-install.yaml"
|
||||
kustomize build "${SRCROOT}/manifests/namespace-install" >> "${SRCROOT}/manifests/namespace-install.yaml"
|
||||
update_image "${SRCROOT}/manifests/namespace-install.yaml"
|
||||
|
||||
16
manifests/README.md
Normal file
16
manifests/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Argo CD Installation Manifests
|
||||
|
||||
Two sets of installation manifests are provided:
|
||||
|
||||
* [install.yaml](install.yaml) - Standard Argo CD installation with cluster-admin access. Use this
|
||||
manifest set if you plan to use Argo CD to deploy applications in the same cluster that Argo CD runs
|
||||
in (i.e. kubernetes.svc.default). Will still be able to deploy to external clusters with inputted
|
||||
credentials.
|
||||
|
||||
* [namespace-install.yaml](namespace-install.yaml) - Installation of Argo CD which requires only
|
||||
namespace level privileges (does not need cluster roles). Use this manifest set if you do not
|
||||
need Argo CD to deploy applications in the same cluster that Argo CD runs in, and will rely solely
|
||||
on inputted cluster credentials. An example of using this set of manifests is if you run several
|
||||
Argo CD instances for different teams, where each instance will bedeploying applications to
|
||||
external clusters. Will still be possible to deploy to the same cluster (kubernetes.svc.default)
|
||||
with inputted credentials (i.e. `argocd cluster add <CONTEXT> --in-cluster`).
|
||||
31
manifests/base/argocd-application-controller-deployment.yaml
Normal file
31
manifests/base/argocd-application-controller-deployment.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: argocd-application-controller
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: argocd-application-controller
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: argocd-application-controller
|
||||
spec:
|
||||
containers:
|
||||
- command:
|
||||
- argocd-application-controller
|
||||
- --status-processors
|
||||
- "20"
|
||||
- --operation-processors
|
||||
- "10"
|
||||
image: argoproj/argocd:latest
|
||||
imagePullPolicy: Always
|
||||
name: argocd-application-controller
|
||||
ports:
|
||||
- containerPort: 8083
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: 8083
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
serviceAccountName: argocd-application-controller
|
||||
@@ -1,19 +1,17 @@
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: application-controller-role
|
||||
name: argocd-application-controller
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- secrets
|
||||
- configmaps
|
||||
verbs:
|
||||
- get
|
||||
- watch
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- argoproj.io
|
||||
resources:
|
||||
@@ -1,12 +1,11 @@
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: application-controller-role-binding
|
||||
name: argocd-application-controller
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: application-controller-role
|
||||
name: argocd-application-controller
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: application-controller
|
||||
name: argocd-application-controller
|
||||
4
manifests/base/argocd-application-controller-sa.yaml
Normal file
4
manifests/base/argocd-application-controller-sa.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: argocd-application-controller
|
||||
10
manifests/base/argocd-application-controller-service.yaml
Normal file
10
manifests/base/argocd-application-controller-service.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: argocd-application-controller
|
||||
spec:
|
||||
ports:
|
||||
- port: 8083
|
||||
targetPort: 8083
|
||||
selector:
|
||||
app: argocd-application-controller
|
||||
23
manifests/base/argocd-cm.yaml
Normal file
23
manifests/base/argocd-cm.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-cm
|
||||
# data:
|
||||
# # Argo CD's externally facing base URL. Required for configuring SSO
|
||||
# # url: https://argo-cd-demo.argoproj.io
|
||||
#
|
||||
# # A dex connector configuration. See documentation on how to configure SSO:
|
||||
# # https://github.com/argoproj/argo-cd/blob/master/docs/sso.md#2-configure-argocd-for-sso
|
||||
# dex.config: |
|
||||
# connectors:
|
||||
# # GitHub example
|
||||
# - type: github
|
||||
# id: github
|
||||
# name: GitHub
|
||||
# config:
|
||||
# clientID: aabbccddeeff00112233
|
||||
# clientSecret: $dex.github.clientSecret
|
||||
# orgs:
|
||||
# - name: your-github-org
|
||||
# teams:
|
||||
# - red-team
|
||||
14
manifests/base/argocd-metrics-service.yaml
Normal file
14
manifests/base/argocd-metrics-service.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: argocd-metrics
|
||||
name: argocd-metrics
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
protocol: TCP
|
||||
port: 8082
|
||||
targetPort: 8082
|
||||
selector:
|
||||
app: argocd-server
|
||||
15
manifests/base/argocd-rbac-cm.yaml
Normal file
15
manifests/base/argocd-rbac-cm.yaml
Normal file
@@ -0,0 +1,15 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: argocd-rbac-cm
|
||||
# data:
|
||||
# # An RBAC policy .csv file containing additional policy and role definitions.
|
||||
# # See https://github.com/argoproj/argo-cd/blob/master/docs/rbac.md on how to write RBAC policies.
|
||||
# policy.csv: |
|
||||
# # Give all members of "my-org:team-alpha" the ability to sync apps in "my-project"
|
||||
# p, my-org:team-alpha, applications, sync, my-project/*, allow
|
||||
# # Make all members of "my-org:team-beta" admins
|
||||
# g, my-org:team-beta, role:admin
|
||||
#
|
||||
# # The default role Argo CD will fall back to, when authorizing API requests
|
||||
# policy.default: role:readonly
|
||||
26
manifests/base/argocd-repo-server-deployment.yaml
Normal file
26
manifests/base/argocd-repo-server-deployment.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
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:
|
||||
automountServiceAccountToken: false
|
||||
containers:
|
||||
- name: argocd-repo-server
|
||||
image: argoproj/argocd:latest
|
||||
imagePullPolicy: Always
|
||||
command: [argocd-repo-server]
|
||||
ports:
|
||||
- containerPort: 8081
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: 8081
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
@@ -1,4 +1,3 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
26
manifests/base/argocd-secret.yaml
Normal file
26
manifests/base/argocd-secret.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: argocd-secret
|
||||
type: Opaque
|
||||
# data:
|
||||
# # TLS certificate and private key for API server.
|
||||
# # Autogenerated with a self-signed ceritificate if keys are missing.
|
||||
# tls.crt:
|
||||
# tls.key:
|
||||
#
|
||||
# # bcrypt hash of the admin password and it's last modified time. Autogenerated on initial
|
||||
# # startup. To reset a forgotten password, delete both keys and restart argocd-server.
|
||||
# admin.password:
|
||||
# admin.passwordMtime:
|
||||
#
|
||||
# # random server signature key for session validation. Autogenerated on initial startup
|
||||
# server.secretkey:
|
||||
#
|
||||
# # 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 Argo CD instance (e.g. https://argocd.example.com/api/webhook)
|
||||
# github.webhook.secret:
|
||||
# gitlab.webhook.secret:
|
||||
# bitbucket.webhook.uuid:
|
||||
@@ -1,4 +1,3 @@
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
@@ -14,31 +13,29 @@ spec:
|
||||
spec:
|
||||
serviceAccountName: argocd-server
|
||||
initContainers:
|
||||
- name: copyutil
|
||||
image: argoproj/argocd-server:v0.7.1
|
||||
command: [cp, /argocd-util, /shared]
|
||||
volumeMounts:
|
||||
- mountPath: /shared
|
||||
name: static-files
|
||||
- name: ui
|
||||
image: argoproj/argocd-ui:v0.7.1
|
||||
image: argoproj/argocd-ui:latest
|
||||
imagePullPolicy: Always
|
||||
command: [cp, -r, /app, /shared]
|
||||
volumeMounts:
|
||||
- mountPath: /shared
|
||||
name: static-files
|
||||
containers:
|
||||
- name: argocd-server
|
||||
image: argoproj/argocd-server:v0.7.1
|
||||
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]
|
||||
image: argoproj/argocd:latest
|
||||
imagePullPolicy: Always
|
||||
command: [argocd-server, --staticassets, /shared/app]
|
||||
volumeMounts:
|
||||
- mountPath: /shared
|
||||
name: static-files
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8080
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 30
|
||||
volumes:
|
||||
- emptyDir: {}
|
||||
name: static-files
|
||||
@@ -1,8 +1,7 @@
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: argocd-server-role
|
||||
name: argocd-server
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
@@ -1,12 +1,11 @@
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: argocd-server-role-binding
|
||||
name: argocd-server
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: argocd-server-role
|
||||
name: argocd-server
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: argocd-server
|
||||
@@ -1,4 +1,3 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
@@ -1,4 +1,3 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
36
manifests/base/dex-server-deployment.yaml
Normal file
36
manifests/base/dex-server-deployment.yaml
Normal file
@@ -0,0 +1,36 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: dex-server
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: dex-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: dex-server
|
||||
spec:
|
||||
serviceAccountName: dex-server
|
||||
initContainers:
|
||||
- name: copyutil
|
||||
image: argoproj/argocd:latest
|
||||
imagePullPolicy: Always
|
||||
command: [cp, /usr/local/bin/argocd-util, /shared]
|
||||
volumeMounts:
|
||||
- mountPath: /shared
|
||||
name: static-files
|
||||
containers:
|
||||
- name: dex
|
||||
image: quay.io/dexidp/dex:v2.12.0
|
||||
imagePullPolicy: Always
|
||||
command: [/shared/argocd-util, rundex]
|
||||
ports:
|
||||
- containerPort: 5556
|
||||
- containerPort: 5557
|
||||
volumeMounts:
|
||||
- mountPath: /shared
|
||||
name: static-files
|
||||
volumes:
|
||||
- emptyDir: {}
|
||||
name: static-files
|
||||
14
manifests/base/dex-server-role.yaml
Normal file
14
manifests/base/dex-server-role.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: dex-server
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- secrets
|
||||
- configmaps
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
11
manifests/base/dex-server-rolebinding.yaml
Normal file
11
manifests/base/dex-server-rolebinding.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: dex-server
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: dex-server
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: dex-server
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: application-controller
|
||||
name: dex-server
|
||||
16
manifests/base/dex-server-service.yaml
Normal file
16
manifests/base/dex-server-service.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: dex-server
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
protocol: TCP
|
||||
port: 5556
|
||||
targetPort: 5556
|
||||
- name: grpc
|
||||
protocol: TCP
|
||||
port: 5557
|
||||
targetPort: 5557
|
||||
selector:
|
||||
app: dex-server
|
||||
28
manifests/base/kustomization.yaml
Normal file
28
manifests/base/kustomization.yaml
Normal file
@@ -0,0 +1,28 @@
|
||||
resources:
|
||||
- argocd-cm.yaml
|
||||
- argocd-secret.yaml
|
||||
- argocd-rbac-cm.yaml
|
||||
- argocd-application-controller-sa.yaml
|
||||
- argocd-application-controller-role.yaml
|
||||
- argocd-application-controller-rolebinding.yaml
|
||||
- argocd-application-controller-deployment.yaml
|
||||
- argocd-application-controller-service.yaml
|
||||
- argocd-server-sa.yaml
|
||||
- argocd-server-role.yaml
|
||||
- argocd-server-rolebinding.yaml
|
||||
- argocd-server-deployment.yaml
|
||||
- argocd-server-service.yaml
|
||||
- argocd-metrics-service.yaml
|
||||
- argocd-repo-server-deployment.yaml
|
||||
- argocd-repo-server-service.yaml
|
||||
- dex-server-sa.yaml
|
||||
- dex-server-role.yaml
|
||||
- dex-server-rolebinding.yaml
|
||||
- dex-server-deployment.yaml
|
||||
- dex-server-service.yaml
|
||||
|
||||
imageTags:
|
||||
- name: argoproj/argocd
|
||||
newTag: v0.11.2
|
||||
- name: argoproj/argocd-ui
|
||||
newTag: v0.11.2
|
||||
@@ -0,0 +1,15 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: argocd-application-controller
|
||||
rules:
|
||||
- apiGroups:
|
||||
- '*'
|
||||
resources:
|
||||
- '*'
|
||||
verbs:
|
||||
- '*'
|
||||
- nonResourceURLs:
|
||||
- '*'
|
||||
verbs:
|
||||
- '*'
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: argocd-application-controller
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: argocd-application-controller
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: argocd-application-controller
|
||||
namespace: argocd
|
||||
25
manifests/cluster-install/argocd-server-clusterrole.yaml
Normal file
25
manifests/cluster-install/argocd-server-clusterrole.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: argocd-server
|
||||
rules:
|
||||
- apiGroups:
|
||||
- '*'
|
||||
resources:
|
||||
- '*'
|
||||
verbs:
|
||||
- delete
|
||||
- get
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- events
|
||||
verbs:
|
||||
- list
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- pods
|
||||
- pods/log
|
||||
verbs:
|
||||
- get
|
||||
@@ -0,0 +1,12 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: argocd-server
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: argocd-server
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: argocd-server
|
||||
namespace: argocd
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user