mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-04-05 00:08:49 +02:00
Compare commits
359 Commits
temp-cherr
...
v3.2.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1515e91ce8 | ||
|
|
a35e85dcd6 | ||
|
|
5fca1ce7d8 | ||
|
|
65378e6d14 | ||
|
|
e7d33de05c | ||
|
|
48549a2035 | ||
|
|
10c3fd02f4 | ||
|
|
ca08f90e96 | ||
|
|
1f03b27fd5 | ||
|
|
9c128e2d4c | ||
|
|
75eddbd910 | ||
|
|
65b029342d | ||
|
|
2ff406ae33 | ||
|
|
76fc92f655 | ||
|
|
ad117b88a6 | ||
|
|
508da9c791 | ||
|
|
20866f4557 | ||
|
|
e3b108b738 | ||
|
|
c56f4400f2 | ||
|
|
e9d03a633e | ||
|
|
030b4f982b | ||
|
|
fafbd44489 | ||
|
|
d7d9674e33 | ||
|
|
e6f54030f0 | ||
|
|
b4146969ed | ||
|
|
51c6375130 | ||
|
|
b67eb40a45 | ||
|
|
8a0633b74a | ||
|
|
0d4f505954 | ||
|
|
2b6251dfed | ||
|
|
8f903c3a11 | ||
|
|
8d0dde1388 | ||
|
|
784f62ca6d | ||
|
|
33b5043405 | ||
|
|
88fe638aff | ||
|
|
a29703877e | ||
|
|
95e7cdb16f | ||
|
|
122f4db3db | ||
|
|
2d65b26420 | ||
|
|
0ace9bb9a3 | ||
|
|
6398ec3dcb | ||
|
|
732b16fb2a | ||
|
|
024c7e6020 | ||
|
|
26b7fb2c61 | ||
|
|
8c4ab63a9c | ||
|
|
29f869c82f | ||
|
|
c11e67d4bf | ||
|
|
a0a18438ab | ||
|
|
dabdf39772 | ||
|
|
cd8df1721c | ||
|
|
27c5065308 | ||
|
|
1545390cd8 | ||
|
|
7bd02d7f02 | ||
|
|
86c9994394 | ||
|
|
6dd5e7a6d2 | ||
|
|
66b2f302d9 | ||
|
|
a1df57df93 | ||
|
|
8884b27381 | ||
|
|
be8e79eb31 | ||
|
|
6aa9c20e47 | ||
|
|
1963030721 | ||
|
|
b227ef1559 | ||
|
|
d1251f407a | ||
|
|
3db95b1fbe | ||
|
|
7628473802 | ||
|
|
059e8d220e | ||
|
|
1ba3929520 | ||
|
|
a42ccaeeca | ||
|
|
d75bcfd7b2 | ||
|
|
35e3897f61 | ||
|
|
dc309cbe0d | ||
|
|
a1f42488d9 | ||
|
|
973eccee0a | ||
|
|
8f8a1ecacb | ||
|
|
46409ae734 | ||
|
|
5f5d46c78b | ||
|
|
722036d447 | ||
|
|
001bfda068 | ||
|
|
4821d71e3d | ||
|
|
ef8ac49807 | ||
|
|
feab307df3 | ||
|
|
087378c669 | ||
|
|
f3c8e1d5e3 | ||
|
|
28510cdda6 | ||
|
|
6a2df4380a | ||
|
|
cd87a13a0d | ||
|
|
1453367645 | ||
|
|
50531e6ab3 | ||
|
|
bf9f927d55 | ||
|
|
ee0de13be4 | ||
|
|
4ac3f920d5 | ||
|
|
06ef059f9f | ||
|
|
45c8fd9d2b | ||
|
|
2bba563a76 | ||
|
|
f13aa46e7f | ||
|
|
3af3a056a2 | ||
|
|
aed63c628d | ||
|
|
22d3ef0ef6 | ||
|
|
e8875bbe7b | ||
|
|
5c10b47d27 | ||
|
|
1680134dc2 | ||
|
|
a330ae4355 | ||
|
|
cd3dc7a1cf | ||
|
|
f4541a60c0 | ||
|
|
81da5ea740 | ||
|
|
c4d99bb224 | ||
|
|
21ec075fd9 | ||
|
|
b834987db9 | ||
|
|
139debe3bb | ||
|
|
f4c4c66f38 | ||
|
|
0793efb5e4 | ||
|
|
15a35daf16 | ||
|
|
4e5b201ba5 | ||
|
|
bb56b9ea67 | ||
|
|
b18ea682c4 | ||
|
|
7fafc99a7a | ||
|
|
ba38778d8c | ||
|
|
48933252b4 | ||
|
|
adf89ea322 | ||
|
|
e492587fb1 | ||
|
|
fed3c7eef7 | ||
|
|
922e459665 | ||
|
|
0fe2a2110c | ||
|
|
1e4cfcc4a0 | ||
|
|
8d018bbf2e | ||
|
|
41f664493e | ||
|
|
939d88c5c6 | ||
|
|
0174fccf28 | ||
|
|
6212ea2afb | ||
|
|
873c2fcfc7 | ||
|
|
2229f9d6fc | ||
|
|
5a8b427322 | ||
|
|
2e5601f932 | ||
|
|
7ae14c89d9 | ||
|
|
8b8d04ecfa | ||
|
|
c64183717b | ||
|
|
d54c8afc09 | ||
|
|
762114c6df | ||
|
|
564e507dd7 | ||
|
|
e4eb86d2db | ||
|
|
bed3d56d17 | ||
|
|
f401a0ee11 | ||
|
|
bc4775468a | ||
|
|
17e5c1f68f | ||
|
|
df324c07d8 | ||
|
|
6028dea3a5 | ||
|
|
b68601255c | ||
|
|
e24d8d4024 | ||
|
|
93148b52c4 | ||
|
|
12d3f5dba1 | ||
|
|
f5a562ac30 | ||
|
|
1268dd9bff | ||
|
|
8b2799c51c | ||
|
|
993344e232 | ||
|
|
670d383f69 | ||
|
|
7829e2c6c1 | ||
|
|
ed752cb540 | ||
|
|
12b1bf5f34 | ||
|
|
52683fdd3e | ||
|
|
3247474212 | ||
|
|
c69d30e52d | ||
|
|
2cfc70afa9 | ||
|
|
728674f922 | ||
|
|
cb2b7faa6d | ||
|
|
a608753071 | ||
|
|
cc39e63e24 | ||
|
|
db7acf8501 | ||
|
|
23f3472f25 | ||
|
|
b96401bb76 | ||
|
|
f953976d92 | ||
|
|
26b970b5bd | ||
|
|
f44de4b854 | ||
|
|
634ef6ff1c | ||
|
|
4a3884f516 | ||
|
|
fd4355baae | ||
|
|
d954789d47 | ||
|
|
66f7b4caa1 | ||
|
|
c447628913 | ||
|
|
9922336968 | ||
|
|
8fa3e47d17 | ||
|
|
ae16c00916 | ||
|
|
267eb2ff0f | ||
|
|
1cec174803 | ||
|
|
05385b3dd8 | ||
|
|
c07768cd64 | ||
|
|
b88527cb39 | ||
|
|
e8f86101f5 | ||
|
|
5e5c4b7d03 | ||
|
|
c7588ffb44 | ||
|
|
6f6c39d8f4 | ||
|
|
4c9291152b | ||
|
|
f2233ccd67 | ||
|
|
871b0b434c | ||
|
|
35331553bf | ||
|
|
3e70033247 | ||
|
|
22c652cf97 | ||
|
|
2ffaf43c1d | ||
|
|
4445dbafb2 | ||
|
|
04cf408264 | ||
|
|
5b8e4b57ac | ||
|
|
88a32d6aab | ||
|
|
51fa4e8a54 | ||
|
|
56320a7b08 | ||
|
|
87faf58733 | ||
|
|
1db5f2e618 | ||
|
|
d269e6f936 | ||
|
|
e6a7c1d4e2 | ||
|
|
91b8bba570 | ||
|
|
29805b0e8f | ||
|
|
69f24f007b | ||
|
|
7168674403 | ||
|
|
24d4cb57c5 | ||
|
|
d135f73160 | ||
|
|
37b0f0f767 | ||
|
|
8c3b78ef88 | ||
|
|
68e5a4a12c | ||
|
|
d154627681 | ||
|
|
e85e353b81 | ||
|
|
c39fde74f0 | ||
|
|
08cd547750 | ||
|
|
4362e8ccb7 | ||
|
|
a06dfeb832 | ||
|
|
b20fd4342f | ||
|
|
1c5d7f1f65 | ||
|
|
49f3c05d7d | ||
|
|
9bc35de19d | ||
|
|
a4919edffb | ||
|
|
9e804f99f0 | ||
|
|
ef8d03cea5 | ||
|
|
6265da106e | ||
|
|
b1b157068e | ||
|
|
7129a2c147 | ||
|
|
6c38186f7f | ||
|
|
d3bdc9d5f3 | ||
|
|
728262ac55 | ||
|
|
928aee5dff | ||
|
|
5ce60ca6e3 | ||
|
|
88fccc91c6 | ||
|
|
7a2dc7e80f | ||
|
|
8657798324 | ||
|
|
98f2760d50 | ||
|
|
7ed0f2300e | ||
|
|
5d5d17ae35 | ||
|
|
bfe8b30d9a | ||
|
|
65a082b12c | ||
|
|
14a22ad926 | ||
|
|
70c8f4612f | ||
|
|
b1a9fab70c | ||
|
|
1e5761c1d0 | ||
|
|
8c8902b93f | ||
|
|
13c47ee244 | ||
|
|
82391027d9 | ||
|
|
0c82f4079b | ||
|
|
97af89a3b3 | ||
|
|
d737f8fe43 | ||
|
|
57cccb65c2 | ||
|
|
9bca4859e0 | ||
|
|
940a489cfa | ||
|
|
7dae82dfd3 | ||
|
|
0984b03805 | ||
|
|
b74c0a0e1a | ||
|
|
eaef25c3eb | ||
|
|
a8cae97da0 | ||
|
|
b2b6d9822b | ||
|
|
da7f11a826 | ||
|
|
2b1f5959bd | ||
|
|
5e2a8a86d0 | ||
|
|
d3de4435ce | ||
|
|
5510bdfd71 | ||
|
|
c67763b069 | ||
|
|
1d6ba890a8 | ||
|
|
2e90919fe6 | ||
|
|
34bc56352c | ||
|
|
e039293b7e | ||
|
|
2a0eac0ca9 | ||
|
|
6a2077642e | ||
|
|
24b0ecc657 | ||
|
|
d7364b4662 | ||
|
|
f78cddf736 | ||
|
|
45a7a18256 | ||
|
|
04d1ca4733 | ||
|
|
6d9b5bdf53 | ||
|
|
90123bac04 | ||
|
|
37b67fa4a5 | ||
|
|
48faed19f1 | ||
|
|
0e42012778 | ||
|
|
1b3ced9261 | ||
|
|
ad2e4450f2 | ||
|
|
90e2148667 | ||
|
|
2558e80f41 | ||
|
|
3c6449da89 | ||
|
|
e5417e1eb3 | ||
|
|
69f7d39717 | ||
|
|
390ea4ff54 | ||
|
|
7e868da310 | ||
|
|
03ac864dde | ||
|
|
27b70cf56e | ||
|
|
8ed3a24d49 | ||
|
|
9a990b7e89 | ||
|
|
ec80ebdf67 | ||
|
|
4dfab5d136 | ||
|
|
8f23c885b6 | ||
|
|
48a7030125 | ||
|
|
38ad19fd95 | ||
|
|
24c08922e5 | ||
|
|
323f993816 | ||
|
|
6ec53193fd | ||
|
|
559744a65e | ||
|
|
3f03097983 | ||
|
|
9928c906a2 | ||
|
|
99710b5183 | ||
|
|
42d4cfb857 | ||
|
|
751550562c | ||
|
|
72d054d772 | ||
|
|
f3dbc6f9de | ||
|
|
16cc1b15af | ||
|
|
aff3ae3f4d | ||
|
|
b8decb798a | ||
|
|
4393f7deb8 | ||
|
|
4024fe7c22 | ||
|
|
678f61b8d3 | ||
|
|
31e0f428e8 | ||
|
|
998253aa41 | ||
|
|
69d1d88807 | ||
|
|
58b0116d75 | ||
|
|
b9daeac44e | ||
|
|
a78a616566 | ||
|
|
d2b881ae4a | ||
|
|
796f72c3d4 | ||
|
|
7da3ecc08f | ||
|
|
fca42e3fd4 | ||
|
|
7f5072f286 | ||
|
|
fe6aaad4f0 | ||
|
|
90eae48c77 | ||
|
|
9895f55781 | ||
|
|
660295f656 | ||
|
|
be2c243ac8 | ||
|
|
8eac64d54c | ||
|
|
c68ec277d4 | ||
|
|
51d88197d7 | ||
|
|
4e63bc7563 | ||
|
|
19415979e8 | ||
|
|
5ac055d2a2 | ||
|
|
dcf1965c52 | ||
|
|
0a1572b9d9 | ||
|
|
853b8dddd3 | ||
|
|
9fffcd50d3 | ||
|
|
835c1fbd3c | ||
|
|
2ed67e8fac | ||
|
|
dd1547fcb4 | ||
|
|
635b9fe8fb | ||
|
|
195b238a37 | ||
|
|
cb61611816 | ||
|
|
a37a4d4073 | ||
|
|
2de6819422 | ||
|
|
df3a45ac02 | ||
|
|
f8aea44398 | ||
|
|
02de363d9c | ||
|
|
79943d8189 |
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -2,7 +2,7 @@
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 'bug'
|
||||
labels: ['bug', 'triage/pending']
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
@@ -10,9 +10,9 @@ assignees: ''
|
||||
|
||||
Checklist:
|
||||
|
||||
* [ ] I've searched in the docs and FAQ for my answer: https://bit.ly/argocd-faq.
|
||||
* [ ] I've included steps to reproduce the bug.
|
||||
* [ ] I've pasted the output of `argocd version`.
|
||||
- [ ] I've searched in the docs and FAQ for my answer: https://bit.ly/argocd-faq.
|
||||
- [ ] I've included steps to reproduce the bug.
|
||||
- [ ] I've pasted the output of `argocd version`.
|
||||
|
||||
**Describe the bug**
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
name: Enhancement proposal
|
||||
about: Propose an enhancement for this project
|
||||
title: ''
|
||||
labels: 'enhancement'
|
||||
labels: ['enhancement', 'triage/pending']
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
# Summary
|
||||
|
||||
What change you think needs making.
|
||||
@@ -15,4 +16,4 @@ Please give examples of your use case, e.g. when would you use this.
|
||||
|
||||
# Proposal
|
||||
|
||||
How do you think this should be implemented?
|
||||
How do you think this should be implemented?
|
||||
|
||||
14
.github/ISSUE_TEMPLATE/new_dev_tool.md
vendored
14
.github/ISSUE_TEMPLATE/new_dev_tool.md
vendored
@@ -2,17 +2,17 @@
|
||||
name: New Dev Tool Request
|
||||
about: This is a request for adding a new tool for setting up a dev environment.
|
||||
title: ''
|
||||
labels: ''
|
||||
labels: ['component:dev-env', 'triage/pending']
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
Checklist:
|
||||
|
||||
* [ ] I am willing to maintain this tool, or have another Argo CD maintainer who is.
|
||||
* [ ] I have another Argo CD maintainer who is willing to help maintain this tool (there needs to be at least two maintainers willing to maintain this tool)
|
||||
* [ ] I have a lead sponsor who is a core Argo CD maintainer
|
||||
* [ ] There is a PR which adds said tool - this is so that the maintainers can assess the impact of having this in the tree
|
||||
* [ ] I have given a motivation why this should be added
|
||||
- [ ] I am willing to maintain this tool, or have another Argo CD maintainer who is.
|
||||
- [ ] I have another Argo CD maintainer who is willing to help maintain this tool (there needs to be at least two maintainers willing to maintain this tool)
|
||||
- [ ] I have a lead sponsor who is a core Argo CD maintainer
|
||||
- [ ] There is a PR which adds said tool - this is so that the maintainers can assess the impact of having this in the tree
|
||||
- [ ] I have given a motivation why this should be added
|
||||
|
||||
### The proposer
|
||||
|
||||
@@ -24,7 +24,7 @@ Checklist:
|
||||
|
||||
### Motivation
|
||||
|
||||
<!-- Why this tool would be useful to have in the tree. -->
|
||||
<!-- Why this tool would be useful to have in the tree. -->
|
||||
|
||||
### Link to PR (Optional)
|
||||
|
||||
|
||||
8
.github/ISSUE_TEMPLATE/security_logs.md
vendored
8
.github/ISSUE_TEMPLATE/security_logs.md
vendored
@@ -1,10 +1,11 @@
|
||||
---
|
||||
name: Security log
|
||||
about: Propose adding security-related logs or tagging existing logs with security fields
|
||||
title: "seclog: [Event Description]"
|
||||
labels: security-log
|
||||
assignees: notfromstatefarm
|
||||
title: 'seclog: [Event Description]'
|
||||
labels: ['security', 'triage/pending']
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
# Event to be logged
|
||||
|
||||
Specify the event that needs to be logged or existing logs that need to be tagged.
|
||||
@@ -16,4 +17,3 @@ What security level should these events be logged under? Refer to https://argo-c
|
||||
# Common Weakness Enumeration
|
||||
|
||||
Is there an associated [CWE](https://cwe.mitre.org/) that could be tagged as well?
|
||||
|
||||
|
||||
3
.github/cherry-pick-bot.yml
vendored
3
.github/cherry-pick-bot.yml
vendored
@@ -1,3 +0,0 @@
|
||||
enabled: true
|
||||
preservePullRequestTitle: true
|
||||
|
||||
15
.github/configs/renovate-config.js
vendored
Normal file
15
.github/configs/renovate-config.js
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
platform: 'github',
|
||||
gitAuthor: 'renovate[bot] <renovate[bot]@users.noreply.github.com>',
|
||||
autodiscover: false,
|
||||
allowPostUpgradeCommandTemplating: true,
|
||||
allowedPostUpgradeCommands: ["make mockgen"],
|
||||
extends: [
|
||||
"github>argoproj/argo-cd//renovate-presets/commons.json5",
|
||||
"github>argoproj/argo-cd//renovate-presets/custom-managers/shell.json5",
|
||||
"github>argoproj/argo-cd//renovate-presets/custom-managers/yaml.json5",
|
||||
"github>argoproj/argo-cd//renovate-presets/fix/disable-all-updates.json5",
|
||||
"github>argoproj/argo-cd//renovate-presets/devtool.json5",
|
||||
"github>argoproj/argo-cd//renovate-presets/docs.json5"
|
||||
]
|
||||
}
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -8,7 +8,7 @@ Checklist:
|
||||
|
||||
* [ ] Either (a) I've created an [enhancement proposal](https://github.com/argoproj/argo-cd/issues/new/choose) and discussed it with the community, (b) this is a bug fix, or (c) this does not need to be in the release notes.
|
||||
* [ ] The title of the PR states what changed and the related issues number (used for the release note).
|
||||
* [ ] The title of the PR conforms to the [Toolchain Guide](https://argo-cd.readthedocs.io/en/latest/developer-guide/toolchain-guide/#title-of-the-pr)
|
||||
* [ ] The title of the PR conforms to the [Title of the PR](https://argo-cd.readthedocs.io/en/latest/developer-guide/submit-your-pr/#title-of-the-pr)
|
||||
* [ ] I've included "Closes [ISSUE #]" or "Fixes [ISSUE #]" in the description to automatically close the associated issue.
|
||||
* [ ] I've updated both the CLI and UI to expose my feature, or I plan to submit a second PR with them.
|
||||
* [ ] Does this PR require documentation updates?
|
||||
|
||||
2
.github/workflows/bump-major-version.yaml
vendored
2
.github/workflows/bump-major-version.yaml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
working-directory: /home/runner/go/src/github.com/argoproj/argo-cd
|
||||
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: ${{ env.GOLANG_VERSION }}
|
||||
- name: Add ~/go/bin to PATH
|
||||
|
||||
114
.github/workflows/cherry-pick-single.yml
vendored
Normal file
114
.github/workflows/cherry-pick-single.yml
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
name: Cherry Pick Single
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
merge_commit_sha:
|
||||
required: true
|
||||
type: string
|
||||
description: "The merge commit SHA to cherry-pick"
|
||||
version_number:
|
||||
required: true
|
||||
type: string
|
||||
description: "The version number (from cherry-pick/ label)"
|
||||
pr_number:
|
||||
required: true
|
||||
type: string
|
||||
description: "The original PR number"
|
||||
pr_title:
|
||||
required: true
|
||||
type: string
|
||||
description: "The original PR title"
|
||||
secrets:
|
||||
CHERRYPICK_APP_ID:
|
||||
required: true
|
||||
CHERRYPICK_APP_PRIVATE_KEY:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
cherry-pick:
|
||||
name: Cherry Pick to ${{ inputs.version_number }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
|
||||
with:
|
||||
app-id: ${{ secrets.CHERRYPICK_APP_ID }}
|
||||
private-key: ${{ secrets.CHERRYPICK_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Cherry pick commit
|
||||
id: cherry-pick
|
||||
run: |
|
||||
set -e
|
||||
|
||||
MERGE_COMMIT="${{ inputs.merge_commit_sha }}"
|
||||
TARGET_BRANCH="release-${{ inputs.version_number }}"
|
||||
|
||||
echo "🍒 Cherry-picking commit $MERGE_COMMIT to branch $TARGET_BRANCH"
|
||||
|
||||
# Check if target branch exists
|
||||
if ! git show-ref --verify --quiet "refs/remotes/origin/$TARGET_BRANCH"; then
|
||||
echo "❌ Target branch '$TARGET_BRANCH' does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create new branch for cherry-pick
|
||||
CHERRY_PICK_BRANCH="cherry-pick-${{ inputs.pr_number }}-to-${TARGET_BRANCH}"
|
||||
git checkout -b "$CHERRY_PICK_BRANCH" "origin/$TARGET_BRANCH"
|
||||
|
||||
# Perform cherry-pick
|
||||
if git cherry-pick -m 1 "$MERGE_COMMIT"; then
|
||||
echo "✅ Cherry-pick successful"
|
||||
|
||||
# Extract Signed-off-by from the cherry-pick commit
|
||||
SIGNOFF=$(git log -1 --pretty=format:"%B" | grep -E '^Signed-off-by:' || echo "")
|
||||
|
||||
# Push the new branch
|
||||
git push origin "$CHERRY_PICK_BRANCH"
|
||||
|
||||
# Save data for PR creation
|
||||
echo "branch_name=$CHERRY_PICK_BRANCH" >> "$GITHUB_OUTPUT"
|
||||
echo "signoff=$SIGNOFF" >> "$GITHUB_OUTPUT"
|
||||
echo "target_branch=$TARGET_BRANCH" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "❌ Cherry-pick failed due to conflicts"
|
||||
git cherry-pick --abort
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create Pull Request
|
||||
run: |
|
||||
# Create cherry-pick PR
|
||||
gh pr create \
|
||||
--title "${{ inputs.pr_title }} (cherry-pick #${{ inputs.pr_number }} for ${{ inputs.version_number }})" \
|
||||
--body "Cherry-picked ${{ inputs.pr_title }} (#${{ inputs.pr_number }})
|
||||
|
||||
${{ steps.cherry-pick.outputs.signoff }}" \
|
||||
--base "${{ steps.cherry-pick.outputs.target_branch }}" \
|
||||
--head "${{ steps.cherry-pick.outputs.branch_name }}"
|
||||
|
||||
# Comment on original PR
|
||||
gh pr comment ${{ inputs.pr_number }} \
|
||||
--body "🍒 Cherry-pick PR created for ${{ inputs.version_number }}: #$(gh pr list --head ${{ steps.cherry-pick.outputs.branch_name }} --json number --jq '.[0].number')"
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Comment on failure
|
||||
if: failure()
|
||||
run: |
|
||||
gh pr comment ${{ inputs.pr_number }} \
|
||||
--body "❌ Cherry-pick failed for ${{ inputs.version_number }}. Please check the workflow logs for details."
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||
53
.github/workflows/cherry-pick.yml
vendored
Normal file
53
.github/workflows/cherry-pick.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Cherry Pick
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- master
|
||||
types: ["labeled", "closed"]
|
||||
|
||||
jobs:
|
||||
find-labels:
|
||||
name: Find Cherry Pick Labels
|
||||
if: |
|
||||
github.event.pull_request.merged == true && (
|
||||
(github.event.action == 'labeled' && startsWith(github.event.label.name, 'cherry-pick/')) ||
|
||||
(github.event.action == 'closed' && contains(toJSON(github.event.pull_request.labels.*.name), 'cherry-pick/'))
|
||||
)
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
labels: ${{ steps.extract-labels.outputs.labels }}
|
||||
steps:
|
||||
- name: Extract cherry-pick labels
|
||||
id: extract-labels
|
||||
run: |
|
||||
if [[ "${{ github.event.action }}" == "labeled" ]]; then
|
||||
# Label was just added - use it directly
|
||||
LABEL_NAME="${{ github.event.label.name }}"
|
||||
VERSION="${LABEL_NAME#cherry-pick/}"
|
||||
CHERRY_PICK_DATA='[{"label":"'$LABEL_NAME'","version":"'$VERSION'"}]'
|
||||
else
|
||||
# PR was closed - find all cherry-pick labels
|
||||
CHERRY_PICK_DATA=$(echo '${{ toJSON(github.event.pull_request.labels) }}' | jq -c '[.[] | select(.name | startswith("cherry-pick/")) | {label: .name, version: (.name | sub("cherry-pick/"; ""))}]')
|
||||
fi
|
||||
|
||||
echo "labels=$CHERRY_PICK_DATA" >> "$GITHUB_OUTPUT"
|
||||
echo "Found cherry-pick data: $CHERRY_PICK_DATA"
|
||||
|
||||
cherry-pick:
|
||||
name: Cherry Pick
|
||||
needs: find-labels
|
||||
if: needs.find-labels.outputs.labels != '[]'
|
||||
strategy:
|
||||
matrix:
|
||||
include: ${{ fromJSON(needs.find-labels.outputs.labels) }}
|
||||
fail-fast: false
|
||||
uses: ./.github/workflows/cherry-pick-single.yml
|
||||
with:
|
||||
merge_commit_sha: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
version_number: ${{ matrix.version }}
|
||||
pr_number: ${{ github.event.pull_request.number }}
|
||||
pr_title: ${{ github.event.pull_request.title }}
|
||||
secrets:
|
||||
CHERRYPICK_APP_ID: ${{ vars.CHERRYPICK_APP_ID }}
|
||||
CHERRYPICK_APP_PRIVATE_KEY: ${{ secrets.CHERRYPICK_APP_PRIVATE_KEY }}
|
||||
62
.github/workflows/ci-build.yaml
vendored
62
.github/workflows/ci-build.yaml
vendored
@@ -14,7 +14,7 @@ on:
|
||||
env:
|
||||
# Golang version to use across CI steps
|
||||
# renovate: datasource=golang-version packageName=golang
|
||||
GOLANG_VERSION: '1.24.4'
|
||||
GOLANG_VERSION: '1.25.6'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
docs: ${{ steps.filter.outputs.docs_any_changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
|
||||
- uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
|
||||
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0
|
||||
id: filter
|
||||
with:
|
||||
# Any file which is not under docs/, ui/ or is not a markdown file is counted as a backend file
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: ${{ env.GOLANG_VERSION }}
|
||||
- name: Download all Go modules
|
||||
@@ -78,11 +78,11 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: ${{ env.GOLANG_VERSION }}
|
||||
- name: Restore go build cache
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: ~/.cache/go-build
|
||||
key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
|
||||
@@ -105,14 +105,14 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: ${{ env.GOLANG_VERSION }}
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
|
||||
with:
|
||||
# renovate: datasource=go packageName=github.com/golangci/golangci-lint versioning=regex:^v(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)?$
|
||||
version: v2.3.0
|
||||
version: v2.4.0
|
||||
args: --verbose
|
||||
|
||||
test-go:
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
- name: Create symlink in GOPATH
|
||||
run: ln -s $(pwd) ~/go/src/github.com/argoproj/argo-cd
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: ${{ env.GOLANG_VERSION }}
|
||||
- name: Install required packages
|
||||
@@ -153,7 +153,7 @@ jobs:
|
||||
run: |
|
||||
echo "/usr/local/bin" >> $GITHUB_PATH
|
||||
- name: Restore go build cache
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: ~/.cache/go-build
|
||||
key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
|
||||
@@ -197,7 +197,7 @@ jobs:
|
||||
- name: Create symlink in GOPATH
|
||||
run: ln -s $(pwd) ~/go/src/github.com/argoproj/argo-cd
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: ${{ env.GOLANG_VERSION }}
|
||||
- name: Install required packages
|
||||
@@ -217,7 +217,7 @@ jobs:
|
||||
run: |
|
||||
echo "/usr/local/bin" >> $GITHUB_PATH
|
||||
- name: Restore go build cache
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: ~/.cache/go-build
|
||||
key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
|
||||
@@ -253,7 +253,7 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: ${{ env.GOLANG_VERSION }}
|
||||
- name: Create symlink in GOPATH
|
||||
@@ -305,13 +305,13 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
|
||||
- name: Setup NodeJS
|
||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
|
||||
with:
|
||||
# renovate: datasource=node-version packageName=node versioning=node
|
||||
node-version: '22.9.0'
|
||||
- name: Restore node dependency cache
|
||||
id: cache-dependencies
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: ui/node_modules
|
||||
key: ${{ runner.os }}-node-dep-v2-${{ hashFiles('**/yarn.lock') }}
|
||||
@@ -360,7 +360,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: Restore node dependency cache
|
||||
id: cache-dependencies
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: ui/node_modules
|
||||
key: ${{ runner.os }}-node-dep-v2-${{ hashFiles('**/yarn.lock') }}
|
||||
@@ -368,12 +368,12 @@ jobs:
|
||||
run: |
|
||||
rm -rf ui/node_modules/argo-ui/node_modules
|
||||
- name: Get e2e code coverage
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: e2e-code-coverage
|
||||
path: e2e-code-coverage
|
||||
- name: Get unit test code coverage
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: test-results
|
||||
path: test-results
|
||||
@@ -385,7 +385,7 @@ jobs:
|
||||
run: |
|
||||
go tool covdata percent -i=test-results,e2e-code-coverage/applicationset-controller,e2e-code-coverage/repo-server,e2e-code-coverage/app-controller,e2e-code-coverage/commit-server -o test-results/full-coverage.out
|
||||
- name: Upload code coverage information to codecov.io
|
||||
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
|
||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||
with:
|
||||
files: test-results/full-coverage.out
|
||||
fail_ci_if_error: true
|
||||
@@ -402,31 +402,31 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
uses: SonarSource/sonarqube-scan-action@2500896589ef8f7247069a56136f8dc177c27ccf # v5.2.0
|
||||
uses: SonarSource/sonarqube-scan-action@1a6d90ebcb0e6a6b1d87e37ba693fe453195ae25 # v5.3.1
|
||||
if: env.sonar_secret != ''
|
||||
test-e2e:
|
||||
name: Run end-to-end tests
|
||||
if: ${{ needs.changes.outputs.backend == 'true' }}
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: oracle-vm-16cpu-64gb-x86-64
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# latest: true means that this version mush upload the coverage report to codecov.io
|
||||
# We designate the latest version because we only collect code coverage for that version.
|
||||
k3s:
|
||||
- version: v1.33.1
|
||||
- version: v1.34.2
|
||||
latest: true
|
||||
- version: v1.33.1
|
||||
latest: false
|
||||
- version: v1.32.1
|
||||
latest: false
|
||||
- version: v1.31.0
|
||||
latest: false
|
||||
- version: v1.30.4
|
||||
latest: false
|
||||
needs:
|
||||
- build-go
|
||||
- changes
|
||||
env:
|
||||
GOPATH: /home/runner/go
|
||||
GOPATH: /home/ubuntu/go
|
||||
ARGOCD_FAKE_IN_CLUSTER: 'true'
|
||||
ARGOCD_SSH_DATA_PATH: '/tmp/argo-e2e/app/config/ssh'
|
||||
ARGOCD_TLS_DATA_PATH: '/tmp/argo-e2e/app/config/tls'
|
||||
@@ -449,7 +449,7 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: ${{ env.GOLANG_VERSION }}
|
||||
- name: GH actions workaround - Kill XSP4 process
|
||||
@@ -462,19 +462,19 @@ jobs:
|
||||
set -x
|
||||
curl -sfL https://get.k3s.io | sh -
|
||||
sudo chmod -R a+rw /etc/rancher/k3s
|
||||
sudo mkdir -p $HOME/.kube && sudo chown -R runner $HOME/.kube
|
||||
sudo mkdir -p $HOME/.kube && sudo chown -R ubuntu $HOME/.kube
|
||||
sudo k3s kubectl config view --raw > $HOME/.kube/config
|
||||
sudo chown runner $HOME/.kube/config
|
||||
sudo chown ubuntu $HOME/.kube/config
|
||||
sudo chmod go-r $HOME/.kube/config
|
||||
kubectl version
|
||||
- name: Restore go build cache
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: ~/.cache/go-build
|
||||
key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
|
||||
- name: Add ~/go/bin to PATH
|
||||
run: |
|
||||
echo "/home/runner/go/bin" >> $GITHUB_PATH
|
||||
echo "/home/ubuntu/go/bin" >> $GITHUB_PATH
|
||||
- name: Add /usr/local/bin to PATH
|
||||
run: |
|
||||
echo "/usr/local/bin" >> $GITHUB_PATH
|
||||
@@ -496,11 +496,11 @@ jobs:
|
||||
run: |
|
||||
docker pull ghcr.io/dexidp/dex:v2.43.0
|
||||
docker pull argoproj/argo-cd-ci-builder:v1.0.0
|
||||
docker pull redis:7.2.7-alpine
|
||||
docker pull redis:8.2.2-alpine
|
||||
- name: Create target directory for binaries in the build-process
|
||||
run: |
|
||||
mkdir -p dist
|
||||
chown runner dist
|
||||
chown ubuntu dist
|
||||
- name: Run E2E server and wait for it being available
|
||||
timeout-minutes: 30
|
||||
run: |
|
||||
|
||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
# Use correct go version. https://github.com/github/codeql-action/issues/1842#issuecomment-1704398087
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
|
||||
10
.github/workflows/image-reuse.yaml
vendored
10
.github/workflows/image-reuse.yaml
vendored
@@ -67,13 +67,13 @@ jobs:
|
||||
if: ${{ github.ref_type != 'tag'}}
|
||||
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: ${{ inputs.go-version }}
|
||||
cache: false
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@d58896d6a1865668819e1d91763c7751a165e159 # v3.9.2
|
||||
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
||||
|
||||
- uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
|
||||
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
echo 'EOF' >> $GITHUB_ENV
|
||||
|
||||
- name: Login to Quay.io
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.quay_username }}
|
||||
@@ -111,7 +111,7 @@ jobs:
|
||||
if: ${{ inputs.quay_image_name && inputs.push }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ secrets.ghcr_username }}
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
if: ${{ inputs.ghcr_image_name && inputs.push }}
|
||||
|
||||
- name: Login to dockerhub Container Registry
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
with:
|
||||
username: ${{ secrets.docker_username }}
|
||||
password: ${{ secrets.docker_password }}
|
||||
|
||||
4
.github/workflows/image.yaml
vendored
4
.github/workflows/image.yaml
vendored
@@ -53,7 +53,7 @@ jobs:
|
||||
with:
|
||||
# Note: cannot use env variables to set go-version (https://docs.github.com/en/actions/using-workflows/reusing-workflows#limitations)
|
||||
# renovate: datasource=golang-version packageName=golang
|
||||
go-version: 1.24.4
|
||||
go-version: 1.25.6
|
||||
platforms: ${{ needs.set-vars.outputs.platforms }}
|
||||
push: false
|
||||
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
ghcr_image_name: ghcr.io/argoproj/argo-cd/argocd:${{ needs.set-vars.outputs.image-tag }}
|
||||
# Note: cannot use env variables to set go-version (https://docs.github.com/en/actions/using-workflows/reusing-workflows#limitations)
|
||||
# renovate: datasource=golang-version packageName=golang
|
||||
go-version: 1.24.4
|
||||
go-version: 1.25.6
|
||||
platforms: ${{ needs.set-vars.outputs.platforms }}
|
||||
push: true
|
||||
secrets:
|
||||
|
||||
78
.github/workflows/release.yaml
vendored
78
.github/workflows/release.yaml
vendored
@@ -11,7 +11,7 @@ permissions: {}
|
||||
|
||||
env:
|
||||
# renovate: datasource=golang-version packageName=golang
|
||||
GOLANG_VERSION: '1.24.4' # Note: go-version must also be set in job argocd-image.with.go-version
|
||||
GOLANG_VERSION: '1.25.6' # Note: go-version must also be set in job argocd-image.with.go-version
|
||||
|
||||
jobs:
|
||||
argocd-image:
|
||||
@@ -25,13 +25,49 @@ jobs:
|
||||
quay_image_name: quay.io/argoproj/argocd:${{ github.ref_name }}
|
||||
# Note: cannot use env variables to set go-version (https://docs.github.com/en/actions/using-workflows/reusing-workflows#limitations)
|
||||
# renovate: datasource=golang-version packageName=golang
|
||||
go-version: 1.24.4
|
||||
go-version: 1.25.6
|
||||
platforms: linux/amd64,linux/arm64,linux/s390x,linux/ppc64le
|
||||
push: true
|
||||
secrets:
|
||||
quay_username: ${{ secrets.RELEASE_QUAY_USERNAME }}
|
||||
quay_password: ${{ secrets.RELEASE_QUAY_TOKEN }}
|
||||
|
||||
setup-variables:
|
||||
name: Setup Release Variables
|
||||
if: github.repository == 'argoproj/argo-cd'
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
is_pre_release: ${{ steps.var.outputs.is_pre_release }}
|
||||
is_latest_release: ${{ steps.var.outputs.is_latest_release }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Setup variables
|
||||
id: var
|
||||
run: |
|
||||
set -xue
|
||||
# Fetch all tag information
|
||||
git fetch --prune --tags --force
|
||||
|
||||
LATEST_RELEASE_TAG=$(git -c 'versionsort.suffix=-rc' tag --list --sort=version:refname | grep -v '-' | tail -n1)
|
||||
|
||||
PRE_RELEASE=false
|
||||
# Check if latest tag is a pre-release
|
||||
if echo ${{ github.ref_name }} | grep -E -- '-rc[0-9]+$';then
|
||||
PRE_RELEASE=true
|
||||
fi
|
||||
|
||||
IS_LATEST=false
|
||||
# Ensure latest release tag matches github.ref_name
|
||||
if [[ $LATEST_RELEASE_TAG == ${{ github.ref_name }} ]];then
|
||||
IS_LATEST=true
|
||||
fi
|
||||
echo "is_pre_release=$PRE_RELEASE" >> $GITHUB_OUTPUT
|
||||
echo "is_latest_release=$IS_LATEST" >> $GITHUB_OUTPUT
|
||||
|
||||
argocd-image-provenance:
|
||||
needs: [argocd-image]
|
||||
permissions:
|
||||
@@ -50,15 +86,17 @@ jobs:
|
||||
|
||||
goreleaser:
|
||||
needs:
|
||||
- setup-variables
|
||||
- argocd-image
|
||||
- argocd-image-provenance
|
||||
permissions:
|
||||
contents: write # used for uploading assets
|
||||
if: github.repository == 'argoproj/argo-cd'
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
GORELEASER_MAKE_LATEST: ${{ needs.setup-variables.outputs.is_latest_release }}
|
||||
outputs:
|
||||
hashes: ${{ steps.hash.outputs.hashes }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
|
||||
@@ -70,7 +108,7 @@ jobs:
|
||||
run: git fetch --force --tags
|
||||
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: ${{ env.GOLANG_VERSION }}
|
||||
cache: false
|
||||
@@ -96,7 +134,7 @@ jobs:
|
||||
tool-cache: false
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0
|
||||
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
|
||||
id: run-goreleaser
|
||||
with:
|
||||
version: latest
|
||||
@@ -142,7 +180,7 @@ jobs:
|
||||
permissions:
|
||||
contents: write # Needed for release uploads
|
||||
outputs:
|
||||
hashes: ${{ steps.sbom-hash.outputs.hashes}}
|
||||
hashes: ${{ steps.sbom-hash.outputs.hashes }}
|
||||
if: github.repository == 'argoproj/argo-cd'
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
@@ -153,7 +191,7 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
|
||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
||||
with:
|
||||
go-version: ${{ env.GOLANG_VERSION }}
|
||||
cache: false
|
||||
@@ -198,7 +236,7 @@ jobs:
|
||||
echo "hashes=$(sha256sum /tmp/sbom.tar.gz | base64 -w0)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload SBOM
|
||||
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
|
||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
@@ -221,6 +259,7 @@ jobs:
|
||||
|
||||
post-release:
|
||||
needs:
|
||||
- setup-variables
|
||||
- argocd-image
|
||||
- goreleaser
|
||||
- generate-sbom
|
||||
@@ -229,6 +268,8 @@ jobs:
|
||||
pull-requests: write # Needed to create PR for VERSION update.
|
||||
if: github.repository == 'argoproj/argo-cd'
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
TAG_STABLE: ${{ needs.setup-variables.outputs.is_latest_release }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
|
||||
@@ -242,27 +283,6 @@ jobs:
|
||||
git config --global user.email 'ci@argoproj.com'
|
||||
git config --global user.name 'CI'
|
||||
|
||||
- name: Check if tag is the latest version and not a pre-release
|
||||
run: |
|
||||
set -xue
|
||||
# Fetch all tag information
|
||||
git fetch --prune --tags --force
|
||||
|
||||
LATEST_TAG=$(git -c 'versionsort.suffix=-rc' tag --list --sort=version:refname | tail -n1)
|
||||
|
||||
PRE_RELEASE=false
|
||||
# Check if latest tag is a pre-release
|
||||
if echo $LATEST_TAG | grep -E -- '-rc[0-9]+$';then
|
||||
PRE_RELEASE=true
|
||||
fi
|
||||
|
||||
# Ensure latest tag matches github.ref_name & not a pre-release
|
||||
if [[ $LATEST_TAG == ${{ github.ref_name }} ]] && [[ $PRE_RELEASE != 'true' ]];then
|
||||
echo "TAG_STABLE=true" >> $GITHUB_ENV
|
||||
else
|
||||
echo "TAG_STABLE=false" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Update stable tag to latest version
|
||||
run: |
|
||||
git tag -f stable ${{ github.ref_name }}
|
||||
|
||||
38
.github/workflows/renovate.yaml
vendored
Normal file
38
.github/workflows/renovate.yaml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Renovate
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 * * * *'
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
renovate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get token
|
||||
id: get_token
|
||||
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
|
||||
with:
|
||||
app-id: ${{ vars.RENOVATE_APP_ID }}
|
||||
private-key: ${{ secrets.RENOVATE_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1
|
||||
|
||||
# Some codegen commands require Go to be setup
|
||||
- name: Setup Golang
|
||||
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
|
||||
with:
|
||||
# renovate: datasource=golang-version packageName=golang
|
||||
go-version: 1.25.6
|
||||
|
||||
- name: Self-hosted Renovate
|
||||
uses: renovatebot/github-action@f8af9272cd94a4637c29f60dea8731afd3134473 #43.0.12
|
||||
with:
|
||||
configurationFile: .github/configs/renovate-config.js
|
||||
token: '${{ steps.get_token.outputs.token }}'
|
||||
env:
|
||||
LOG_LEVEL: 'debug'
|
||||
RENOVATE_REPOSITORIES: '${{ github.repository }}'
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -20,6 +20,7 @@ node_modules/
|
||||
.kube/
|
||||
./test/cmp/*.sock
|
||||
.envrc.remote
|
||||
.mirrord/
|
||||
.*.swp
|
||||
rerunreport.txt
|
||||
|
||||
|
||||
@@ -58,7 +58,6 @@ linters:
|
||||
- commentedOutCode
|
||||
- deferInLoop
|
||||
- exitAfterDefer
|
||||
- exposedSyncMutex
|
||||
- hugeParam
|
||||
- importShadow
|
||||
- paramTypeCombine # Leave disabled, there are too many failures to be worth fixing.
|
||||
|
||||
@@ -49,13 +49,14 @@ archives:
|
||||
- argocd-cli
|
||||
name_template: |-
|
||||
{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}
|
||||
formats: [ binary ]
|
||||
formats: [binary]
|
||||
|
||||
checksum:
|
||||
name_template: 'cli_checksums.txt'
|
||||
algorithm: sha256
|
||||
|
||||
release:
|
||||
make_latest: '{{ .Env.GORELEASER_MAKE_LATEST }}'
|
||||
prerelease: auto
|
||||
draft: false
|
||||
header: |
|
||||
|
||||
@@ -24,7 +24,6 @@ packages:
|
||||
Renderer: {}
|
||||
github.com/argoproj/argo-cd/v3/commitserver/apiclient:
|
||||
interfaces:
|
||||
Clientset: {}
|
||||
CommitServiceClient: {}
|
||||
github.com/argoproj/argo-cd/v3/commitserver/commit:
|
||||
interfaces:
|
||||
@@ -35,6 +34,7 @@ packages:
|
||||
github.com/argoproj/argo-cd/v3/controller/hydrator:
|
||||
interfaces:
|
||||
Dependencies: {}
|
||||
RepoGetter: {}
|
||||
github.com/argoproj/argo-cd/v3/pkg/apiclient/cluster:
|
||||
interfaces:
|
||||
ClusterServiceServer: {}
|
||||
|
||||
@@ -12,3 +12,8 @@
|
||||
/.github/** @argoproj/argocd-approvers @argoproj/argocd-approvers-ci
|
||||
/.goreleaser.yaml @argoproj/argocd-approvers @argoproj/argocd-approvers-ci
|
||||
/sonar-project.properties @argoproj/argocd-approvers @argoproj/argocd-approvers-ci
|
||||
|
||||
# CLI
|
||||
/cmd/argocd/** @argoproj/argocd-approvers @argoproj/argocd-approvers-cli
|
||||
/cmd/main.go @argoproj/argocd-approvers @argoproj/argocd-approvers-cli
|
||||
/docs/operator-manual/ @argoproj/argocd-approvers @argoproj/argocd-approvers-cli
|
||||
@@ -4,7 +4,7 @@ ARG BASE_IMAGE=docker.io/library/ubuntu:25.04@sha256:10bb10bb062de665d4dc3e0ea36
|
||||
# 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 docker.io/library/golang:1.24.4@sha256:db5d0afbfb4ab648af2393b92e87eaae9ad5e01132803d80caef91b5752d289c AS builder
|
||||
FROM docker.io/library/golang:1.25.6@sha256:fc24d3881a021e7b968a4610fc024fba749f98fe5c07d4f28e6cfa14dc65a84c AS builder
|
||||
|
||||
WORKDIR /tmp
|
||||
|
||||
@@ -103,7 +103,7 @@ RUN HOST_ARCH=$TARGETARCH NODE_ENV='production' NODE_ONLINE_ENV='online' NODE_OP
|
||||
####################################################################################################
|
||||
# Argo CD Build stage which performs the actual build of Argo CD binaries
|
||||
####################################################################################################
|
||||
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.24.4@sha256:db5d0afbfb4ab648af2393b92e87eaae9ad5e01132803d80caef91b5752d289c AS argocd-build
|
||||
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.25.6@sha256:fc24d3881a021e7b968a4610fc024fba749f98fe5c07d4f28e6cfa14dc65a84c AS argocd-build
|
||||
|
||||
WORKDIR /go/src/github.com/argoproj/argo-cd
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM docker.io/library/golang:1.24.1@sha256:c5adecdb7b3f8c5ca3c88648a861882849cc8b02fed68ece31e25de88ad13418
|
||||
FROM docker.io/library/golang:1.25.6@sha256:fc24d3881a021e7b968a4610fc024fba749f98fe5c07d4f28e6cfa14dc65a84c
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
|
||||
15
Makefile
15
Makefile
@@ -43,6 +43,17 @@ endif
|
||||
DOCKER_SRCDIR?=$(GOPATH)/src
|
||||
DOCKER_WORKDIR?=/go/src/github.com/argoproj/argo-cd
|
||||
|
||||
# Allows you to control which Docker network the test-util containers attach to.
|
||||
# This is particularly useful if you are running Kubernetes in Docker (e.g., k3d)
|
||||
# and want the test containers to reach the Kubernetes API via an already-existing Docker network.
|
||||
DOCKER_NETWORK ?= default
|
||||
|
||||
ifneq ($(DOCKER_NETWORK),default)
|
||||
DOCKER_NETWORK_ARG := --network $(DOCKER_NETWORK)
|
||||
else
|
||||
DOCKER_NETWORK_ARG :=
|
||||
endif
|
||||
|
||||
ARGOCD_PROCFILE?=Procfile
|
||||
|
||||
# pointing to python 3.7 to match https://github.com/argoproj/argo-cd/blob/master/.readthedocs.yml
|
||||
@@ -113,11 +124,11 @@ define run-in-test-server
|
||||
-v ${GOPATH}/pkg/mod:/go/pkg/mod${VOLUME_MOUNT} \
|
||||
-v ${GOCACHE}:/tmp/go-build-cache${VOLUME_MOUNT} \
|
||||
-v ${HOME}/.kube:/home/user/.kube${VOLUME_MOUNT} \
|
||||
-v /tmp:/tmp${VOLUME_MOUNT} \
|
||||
-w ${DOCKER_WORKDIR} \
|
||||
-p ${ARGOCD_E2E_APISERVER_PORT}:8080 \
|
||||
-p 4000:4000 \
|
||||
-p 5000:5000 \
|
||||
$(DOCKER_NETWORK_ARG)\
|
||||
$(PODMAN_ARGS) \
|
||||
$(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE):$(TEST_TOOLS_TAG) \
|
||||
bash -c "$(1)"
|
||||
@@ -138,8 +149,8 @@ define run-in-test-client
|
||||
-v ${GOPATH}/pkg/mod:/go/pkg/mod${VOLUME_MOUNT} \
|
||||
-v ${GOCACHE}:/tmp/go-build-cache${VOLUME_MOUNT} \
|
||||
-v ${HOME}/.kube:/home/user/.kube${VOLUME_MOUNT} \
|
||||
-v /tmp:/tmp${VOLUME_MOUNT} \
|
||||
-w ${DOCKER_WORKDIR} \
|
||||
$(DOCKER_NETWORK_ARG)\
|
||||
$(PODMAN_ARGS) \
|
||||
$(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE):$(TEST_TOOLS_TAG) \
|
||||
bash -c "$(1)"
|
||||
|
||||
8
Tiltfile
8
Tiltfile
@@ -10,6 +10,14 @@ cmd_button(
|
||||
text='make codegen-local',
|
||||
)
|
||||
|
||||
cmd_button(
|
||||
'make test-local',
|
||||
argv=['sh', '-c', 'make test-local'],
|
||||
location=location.NAV,
|
||||
icon_name='science',
|
||||
text='make test-local',
|
||||
)
|
||||
|
||||
# add ui button in web ui to run make codegen-local (top nav)
|
||||
cmd_button(
|
||||
'make cli-local',
|
||||
|
||||
8
USERS.md
8
USERS.md
@@ -5,8 +5,10 @@ PR with your organization name if you are using Argo CD.
|
||||
|
||||
Currently, the following organizations are **officially** using Argo CD:
|
||||
|
||||
1. [100ms](https://www.100ms.ai/)
|
||||
1. [127Labs](https://127labs.com/)
|
||||
1. [3Rein](https://www.3rein.com/)
|
||||
1. [42 School](https://42.fr/)
|
||||
1. [4data](https://4data.ch/)
|
||||
1. [7shifts](https://www.7shifts.com/)
|
||||
1. [Adevinta](https://www.adevinta.com/)
|
||||
@@ -40,6 +42,7 @@ Currently, the following organizations are **officially** using Argo CD:
|
||||
1. [Back Market](https://www.backmarket.com)
|
||||
1. [Bajaj Finserv Health Ltd.](https://www.bajajfinservhealth.in)
|
||||
1. [Baloise](https://www.baloise.com)
|
||||
1. [Batumbu](https://batumbu.id)
|
||||
1. [BCDevExchange DevOps Platform](https://bcdevexchange.org/DevOpsPlatform)
|
||||
1. [Beat](https://thebeat.co/en/)
|
||||
1. [Beez Innovation Labs](https://www.beezlabs.com/)
|
||||
@@ -71,6 +74,7 @@ Currently, the following organizations are **officially** using Argo CD:
|
||||
1. [Chime](https://www.chime.com)
|
||||
1. [Chronicle Labs](https://chroniclelabs.org)
|
||||
1. [Cisco ET&I](https://eti.cisco.com/)
|
||||
1. [Close](https://www.close.com/)
|
||||
1. [Cloud Posse](https://www.cloudposse.com/)
|
||||
1. [Cloud Scale](https://cloudscaleinc.com/)
|
||||
1. [CloudScript](https://www.cloudscript.com.br/)
|
||||
@@ -160,6 +164,7 @@ Currently, the following organizations are **officially** using Argo CD:
|
||||
1. [Hiya](https://hiya.com)
|
||||
1. [Honestbank](https://honestbank.com)
|
||||
1. [Hostinger](https://www.hostinger.com)
|
||||
1. [Hotjar](https://www.hotjar.com)
|
||||
1. [IABAI](https://www.iab.ai)
|
||||
1. [IBM](https://www.ibm.com/)
|
||||
1. [Ibotta](https://home.ibotta.com)
|
||||
@@ -321,8 +326,10 @@ Currently, the following organizations are **officially** using Argo CD:
|
||||
1. [SEEK](https://seek.com.au)
|
||||
1. [SEKAI](https://www.sekai.io/)
|
||||
1. [Semgrep](https://semgrep.com)
|
||||
1. [Seznam.cz](https://o-seznam.cz/)
|
||||
1. [Shield](https://shield.com)
|
||||
1. [Shipfox](https://www.shipfox.io)
|
||||
1. [Shock Media](https://www.shockmedia.nl)
|
||||
1. [SI Analytics](https://si-analytics.ai)
|
||||
1. [Sidewalk Entertainment](https://sidewalkplay.com/)
|
||||
1. [Skit](https://skit.ai/)
|
||||
@@ -335,6 +342,7 @@ Currently, the following organizations are **officially** using Argo CD:
|
||||
1. [Snapp](https://snapp.ir/)
|
||||
1. [Snyk](https://snyk.io/)
|
||||
1. [Softway Medical](https://www.softwaymedical.fr/)
|
||||
1. [Sophotech](https://sopho.tech)
|
||||
1. [South China Morning Post (SCMP)](https://www.scmp.com/)
|
||||
1. [Speee](https://speee.jp/)
|
||||
1. [Spendesk](https://spendesk.com/)
|
||||
|
||||
@@ -75,6 +75,7 @@ const (
|
||||
var defaultPreservedAnnotations = []string{
|
||||
NotifiedAnnotationKey,
|
||||
argov1alpha1.AnnotationKeyRefresh,
|
||||
argov1alpha1.AnnotationKeyHydrate,
|
||||
}
|
||||
|
||||
type deleteInOrder struct {
|
||||
@@ -100,6 +101,7 @@ type ApplicationSetReconciler struct {
|
||||
GlobalPreservedAnnotations []string
|
||||
GlobalPreservedLabels []string
|
||||
Metrics *metrics.ApplicationsetMetrics
|
||||
MaxResourcesStatusCount int
|
||||
}
|
||||
|
||||
// +kubebuilder:rbac:groups=argoproj.io,resources=applicationsets,verbs=get;list;watch;create;update;patch;delete
|
||||
@@ -251,6 +253,16 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
|
||||
return ctrl.Result{}, fmt.Errorf("failed to perform progressive sync reconciliation for application set: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Progressive Sync is disabled, clear any existing applicationStatus to prevent stale data
|
||||
if len(applicationSetInfo.Status.ApplicationStatus) > 0 {
|
||||
logCtx.Infof("Progressive Sync disabled, removing %v AppStatus entries from ApplicationSet %v", len(applicationSetInfo.Status.ApplicationStatus), applicationSetInfo.Name)
|
||||
|
||||
err := r.setAppSetApplicationStatus(ctx, logCtx, &applicationSetInfo, []argov1alpha1.ApplicationSetApplicationStatus{})
|
||||
if err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("failed to clear AppSet application statuses when Progressive Sync is disabled for %v: %w", applicationSetInfo.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var validApps []argov1alpha1.Application
|
||||
@@ -640,8 +652,9 @@ func (r *ApplicationSetReconciler) SetupWithManager(mgr ctrl.Manager, enableProg
|
||||
Watches(
|
||||
&corev1.Secret{},
|
||||
&clusterSecretEventHandler{
|
||||
Client: mgr.GetClient(),
|
||||
Log: log.WithField("type", "createSecretEventHandler"),
|
||||
Client: mgr.GetClient(),
|
||||
Log: log.WithField("type", "createSecretEventHandler"),
|
||||
ApplicationSetNamespaces: r.ApplicationSetNamespaces,
|
||||
}).
|
||||
Complete(r)
|
||||
}
|
||||
@@ -857,7 +870,7 @@ func (r *ApplicationSetReconciler) removeFinalizerOnInvalidDestination(ctx conte
|
||||
|
||||
// Detect if the destination is invalid (name doesn't correspond to a matching cluster)
|
||||
if destCluster, err := argoutil.GetDestinationCluster(ctx, app.Spec.Destination, r.ArgoDB); err != nil {
|
||||
appLog.Warnf("The destination cluster for %s couldn't be found: %v", app.Name, err)
|
||||
appLog.Warnf("The destination cluster for %s could not be found: %v", app.Name, err)
|
||||
validDestination = false
|
||||
} else {
|
||||
// Detect if the destination's server field does not match an existing cluster
|
||||
@@ -876,7 +889,7 @@ func (r *ApplicationSetReconciler) removeFinalizerOnInvalidDestination(ctx conte
|
||||
}
|
||||
|
||||
if !matchingCluster {
|
||||
appLog.Warnf("A match for the destination cluster for %s, by server url, couldn't be found", app.Name)
|
||||
appLog.Warnf("A match for the destination cluster for %s, by server url, could not be found", app.Name)
|
||||
}
|
||||
|
||||
validDestination = matchingCluster
|
||||
@@ -1398,7 +1411,13 @@ func (r *ApplicationSetReconciler) updateResourcesStatus(ctx context.Context, lo
|
||||
sort.Slice(statuses, func(i, j int) bool {
|
||||
return statuses[i].Name < statuses[j].Name
|
||||
})
|
||||
resourcesCount := int64(len(statuses))
|
||||
if r.MaxResourcesStatusCount > 0 && len(statuses) > r.MaxResourcesStatusCount {
|
||||
logCtx.Warnf("Truncating ApplicationSet %s resource status from %d to max allowed %d entries", appset.Name, len(statuses), r.MaxResourcesStatusCount)
|
||||
statuses = statuses[:r.MaxResourcesStatusCount]
|
||||
}
|
||||
appset.Status.Resources = statuses
|
||||
appset.Status.ResourcesCount = resourcesCount
|
||||
// DefaultRetry will retry 5 times with a backoff factor of 1, jitter of 0.1 and a duration of 10ms
|
||||
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
|
||||
namespacedName := types.NamespacedName{Namespace: appset.Namespace, Name: appset.Name}
|
||||
@@ -1411,6 +1430,7 @@ func (r *ApplicationSetReconciler) updateResourcesStatus(ctx context.Context, lo
|
||||
}
|
||||
|
||||
updatedAppset.Status.Resources = appset.Status.Resources
|
||||
updatedAppset.Status.ResourcesCount = resourcesCount
|
||||
|
||||
// Update the newly fetched object with new status resources
|
||||
err := r.Client.Status().Update(ctx, updatedAppset)
|
||||
@@ -1433,17 +1453,37 @@ func (r *ApplicationSetReconciler) setAppSetApplicationStatus(ctx context.Contex
|
||||
needToUpdateStatus := false
|
||||
|
||||
if len(applicationStatuses) != len(applicationSet.Status.ApplicationStatus) {
|
||||
logCtx.WithFields(log.Fields{
|
||||
"current_count": len(applicationSet.Status.ApplicationStatus),
|
||||
"expected_count": len(applicationStatuses),
|
||||
}).Debug("application status count changed")
|
||||
needToUpdateStatus = true
|
||||
} else {
|
||||
for i := range applicationStatuses {
|
||||
appStatus := applicationStatuses[i]
|
||||
idx := findApplicationStatusIndex(applicationSet.Status.ApplicationStatus, appStatus.Application)
|
||||
if idx == -1 {
|
||||
logCtx.WithFields(log.Fields{"application": appStatus.Application}).Debug("application not found in current status")
|
||||
needToUpdateStatus = true
|
||||
break
|
||||
}
|
||||
currentStatus := applicationSet.Status.ApplicationStatus[idx]
|
||||
if currentStatus.Message != appStatus.Message || currentStatus.Status != appStatus.Status || currentStatus.Step != appStatus.Step {
|
||||
statusChanged := currentStatus.Status != appStatus.Status
|
||||
stepChanged := currentStatus.Step != appStatus.Step
|
||||
messageChanged := currentStatus.Message != appStatus.Message
|
||||
|
||||
if statusChanged || stepChanged || messageChanged {
|
||||
if statusChanged {
|
||||
logCtx.WithFields(log.Fields{"application": appStatus.Application, "previous_status": currentStatus.Status, "new_status": appStatus.Status}).
|
||||
Debug("application status changed")
|
||||
}
|
||||
if stepChanged {
|
||||
logCtx.WithFields(log.Fields{"application": appStatus.Application, "previous_step": currentStatus.Step, "new_step": appStatus.Step}).
|
||||
Debug("application step changed")
|
||||
}
|
||||
if messageChanged {
|
||||
logCtx.WithFields(log.Fields{"application": appStatus.Application}).Debug("application message changed")
|
||||
}
|
||||
needToUpdateStatus = true
|
||||
break
|
||||
}
|
||||
|
||||
@@ -589,6 +589,72 @@ func TestCreateOrUpdateInCluster(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Ensure that hydrate annotation is preserved from an existing app",
|
||||
appSet: v1alpha1.ApplicationSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "name",
|
||||
Namespace: "namespace",
|
||||
},
|
||||
Spec: v1alpha1.ApplicationSetSpec{
|
||||
Template: v1alpha1.ApplicationSetTemplate{
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Project: "project",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
existingApps: []v1alpha1.Application{
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: application.ApplicationKind,
|
||||
APIVersion: "argoproj.io/v1alpha1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app1",
|
||||
Namespace: "namespace",
|
||||
ResourceVersion: "2",
|
||||
Annotations: map[string]string{
|
||||
"annot-key": "annot-value",
|
||||
v1alpha1.AnnotationKeyHydrate: string(v1alpha1.RefreshTypeNormal),
|
||||
},
|
||||
},
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Project: "project",
|
||||
},
|
||||
},
|
||||
},
|
||||
desiredApps: []v1alpha1.Application{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app1",
|
||||
Namespace: "namespace",
|
||||
},
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Project: "project",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []v1alpha1.Application{
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: application.ApplicationKind,
|
||||
APIVersion: "argoproj.io/v1alpha1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app1",
|
||||
Namespace: "namespace",
|
||||
ResourceVersion: "3",
|
||||
Annotations: map[string]string{
|
||||
v1alpha1.AnnotationKeyHydrate: string(v1alpha1.RefreshTypeNormal),
|
||||
},
|
||||
},
|
||||
Spec: v1alpha1.ApplicationSpec{
|
||||
Project: "project",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Ensure that configured preserved annotations are preserved from an existing app",
|
||||
appSet: v1alpha1.ApplicationSet{
|
||||
@@ -6410,10 +6476,11 @@ func TestUpdateResourceStatus(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, cc := range []struct {
|
||||
name string
|
||||
appSet v1alpha1.ApplicationSet
|
||||
apps []v1alpha1.Application
|
||||
expectedResources []v1alpha1.ResourceStatus
|
||||
name string
|
||||
appSet v1alpha1.ApplicationSet
|
||||
apps []v1alpha1.Application
|
||||
expectedResources []v1alpha1.ResourceStatus
|
||||
maxResourcesStatusCount int
|
||||
}{
|
||||
{
|
||||
name: "handles an empty application list",
|
||||
@@ -6577,6 +6644,73 @@ func TestUpdateResourceStatus(t *testing.T) {
|
||||
apps: []v1alpha1.Application{},
|
||||
expectedResources: nil,
|
||||
},
|
||||
{
|
||||
name: "truncates resources status list to",
|
||||
appSet: v1alpha1.ApplicationSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "name",
|
||||
Namespace: "argocd",
|
||||
},
|
||||
Status: v1alpha1.ApplicationSetStatus{
|
||||
Resources: []v1alpha1.ResourceStatus{
|
||||
{
|
||||
Name: "app1",
|
||||
Status: v1alpha1.SyncStatusCodeOutOfSync,
|
||||
Health: &v1alpha1.HealthStatus{
|
||||
Status: health.HealthStatusProgressing,
|
||||
Message: "this is progressing",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "app2",
|
||||
Status: v1alpha1.SyncStatusCodeOutOfSync,
|
||||
Health: &v1alpha1.HealthStatus{
|
||||
Status: health.HealthStatusProgressing,
|
||||
Message: "this is progressing",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
apps: []v1alpha1.Application{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app1",
|
||||
},
|
||||
Status: v1alpha1.ApplicationStatus{
|
||||
Sync: v1alpha1.SyncStatus{
|
||||
Status: v1alpha1.SyncStatusCodeSynced,
|
||||
},
|
||||
Health: v1alpha1.AppHealthStatus{
|
||||
Status: health.HealthStatusHealthy,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "app2",
|
||||
},
|
||||
Status: v1alpha1.ApplicationStatus{
|
||||
Sync: v1alpha1.SyncStatus{
|
||||
Status: v1alpha1.SyncStatusCodeSynced,
|
||||
},
|
||||
Health: v1alpha1.AppHealthStatus{
|
||||
Status: health.HealthStatusHealthy,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedResources: []v1alpha1.ResourceStatus{
|
||||
{
|
||||
Name: "app1",
|
||||
Status: v1alpha1.SyncStatusCodeSynced,
|
||||
Health: &v1alpha1.HealthStatus{
|
||||
Status: health.HealthStatusHealthy,
|
||||
},
|
||||
},
|
||||
},
|
||||
maxResourcesStatusCount: 1,
|
||||
},
|
||||
} {
|
||||
t.Run(cc.name, func(t *testing.T) {
|
||||
kubeclientset := kubefake.NewSimpleClientset([]runtime.Object{}...)
|
||||
@@ -6587,13 +6721,14 @@ func TestUpdateResourceStatus(t *testing.T) {
|
||||
argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)
|
||||
|
||||
r := ApplicationSetReconciler{
|
||||
Client: client,
|
||||
Scheme: scheme,
|
||||
Recorder: record.NewFakeRecorder(1),
|
||||
Generators: map[string]generators.Generator{},
|
||||
ArgoDB: argodb,
|
||||
KubeClientset: kubeclientset,
|
||||
Metrics: metrics,
|
||||
Client: client,
|
||||
Scheme: scheme,
|
||||
Recorder: record.NewFakeRecorder(1),
|
||||
Generators: map[string]generators.Generator{},
|
||||
ArgoDB: argodb,
|
||||
KubeClientset: kubeclientset,
|
||||
Metrics: metrics,
|
||||
MaxResourcesStatusCount: cc.maxResourcesStatusCount,
|
||||
}
|
||||
|
||||
err := r.updateResourcesStatus(t.Context(), log.NewEntry(log.StandardLogger()), &cc.appSet, cc.apps)
|
||||
@@ -7544,109 +7679,81 @@ func TestSyncApplication(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsRollingSyncDeletionReversed(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
appset *v1alpha1.ApplicationSet
|
||||
expected bool
|
||||
func TestReconcileProgressiveSyncDisabled(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
err := v1alpha1.AddToScheme(scheme)
|
||||
require.NoError(t, err)
|
||||
|
||||
kubeclientset := kubefake.NewSimpleClientset([]runtime.Object{}...)
|
||||
|
||||
for _, cc := range []struct {
|
||||
name string
|
||||
appSet v1alpha1.ApplicationSet
|
||||
enableProgressiveSyncs bool
|
||||
expectedAppStatuses []v1alpha1.ApplicationSetApplicationStatus
|
||||
}{
|
||||
{
|
||||
name: "Deletion Order on strategy is set as Reverse",
|
||||
appset: &v1alpha1.ApplicationSet{
|
||||
name: "clears applicationStatus when Progressive Sync is disabled",
|
||||
appSet: v1alpha1.ApplicationSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-appset",
|
||||
Namespace: "argocd",
|
||||
},
|
||||
Spec: v1alpha1.ApplicationSetSpec{
|
||||
Strategy: &v1alpha1.ApplicationSetStrategy{
|
||||
Type: "RollingSync",
|
||||
RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
|
||||
Steps: []v1alpha1.ApplicationSetRolloutStep{
|
||||
{
|
||||
MatchExpressions: []v1alpha1.ApplicationMatchExpression{
|
||||
{
|
||||
Key: "environment",
|
||||
Operator: "In",
|
||||
Values: []string{
|
||||
"dev",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
MatchExpressions: []v1alpha1.ApplicationMatchExpression{
|
||||
{
|
||||
Key: "environment",
|
||||
Operator: "In",
|
||||
Values: []string{
|
||||
"staging",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Generators: []v1alpha1.ApplicationSetGenerator{},
|
||||
Template: v1alpha1.ApplicationSetTemplate{},
|
||||
},
|
||||
Status: v1alpha1.ApplicationSetStatus{
|
||||
ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
|
||||
{
|
||||
Application: "test-appset-guestbook",
|
||||
Message: "Application resource became Healthy, updating status from Progressing to Healthy.",
|
||||
Status: "Healthy",
|
||||
Step: "1",
|
||||
},
|
||||
DeletionOrder: ReverseDeletionOrder,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
enableProgressiveSyncs: false,
|
||||
expectedAppStatuses: nil,
|
||||
},
|
||||
{
|
||||
name: "Deletion Order on strategy is set as AllAtOnce",
|
||||
appset: &v1alpha1.ApplicationSet{
|
||||
Spec: v1alpha1.ApplicationSetSpec{
|
||||
Strategy: &v1alpha1.ApplicationSetStrategy{
|
||||
Type: "RollingSync",
|
||||
RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
|
||||
Steps: []v1alpha1.ApplicationSetRolloutStep{},
|
||||
},
|
||||
DeletionOrder: AllAtOnceDeletionOrder,
|
||||
},
|
||||
} {
|
||||
t.Run(cc.name, func(t *testing.T) {
|
||||
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&cc.appSet).WithStatusSubresource(&cc.appSet).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build()
|
||||
metrics := appsetmetrics.NewFakeAppsetMetrics()
|
||||
|
||||
argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)
|
||||
|
||||
r := ApplicationSetReconciler{
|
||||
Client: client,
|
||||
Scheme: scheme,
|
||||
Renderer: &utils.Render{},
|
||||
Recorder: record.NewFakeRecorder(1),
|
||||
Generators: map[string]generators.Generator{},
|
||||
ArgoDB: argodb,
|
||||
KubeClientset: kubeclientset,
|
||||
Metrics: metrics,
|
||||
EnableProgressiveSyncs: cc.enableProgressiveSyncs,
|
||||
}
|
||||
|
||||
req := ctrl.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Namespace: cc.appSet.Namespace,
|
||||
Name: cc.appSet.Name,
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Deletion Order on strategy is set as Reverse but no steps in RollingSync",
|
||||
appset: &v1alpha1.ApplicationSet{
|
||||
Spec: v1alpha1.ApplicationSetSpec{
|
||||
Strategy: &v1alpha1.ApplicationSetStrategy{
|
||||
Type: "RollingSync",
|
||||
RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
|
||||
Steps: []v1alpha1.ApplicationSetRolloutStep{},
|
||||
},
|
||||
DeletionOrder: ReverseDeletionOrder,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Deletion Order on strategy is set as Reverse, but AllAtOnce is explicitly set",
|
||||
appset: &v1alpha1.ApplicationSet{
|
||||
Spec: v1alpha1.ApplicationSetSpec{
|
||||
Strategy: &v1alpha1.ApplicationSetStrategy{
|
||||
Type: "AllAtOnce",
|
||||
RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
|
||||
Steps: []v1alpha1.ApplicationSetRolloutStep{},
|
||||
},
|
||||
DeletionOrder: ReverseDeletionOrder,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Strategy is Nil",
|
||||
appset: &v1alpha1.ApplicationSet{
|
||||
Spec: v1alpha1.ApplicationSetSpec{
|
||||
Strategy: &v1alpha1.ApplicationSetStrategy{},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isProgressiveSyncDeletionOrderReversed(tt.appset)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
}
|
||||
|
||||
// Run reconciliation
|
||||
_, err = r.Reconcile(t.Context(), req)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Fetch the updated ApplicationSet
|
||||
var updatedAppSet v1alpha1.ApplicationSet
|
||||
err = r.Get(t.Context(), req.NamespacedName, &updatedAppSet)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the applicationStatus field
|
||||
assert.Equal(t, cc.expectedAppStatuses, updatedAppSet.Status.ApplicationStatus, "applicationStatus should match expected value")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/applicationset/utils"
|
||||
"github.com/argoproj/argo-cd/v3/common"
|
||||
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
@@ -22,8 +23,9 @@ import (
|
||||
// requeue any related ApplicationSets.
|
||||
type clusterSecretEventHandler struct {
|
||||
// handler.EnqueueRequestForOwner
|
||||
Log log.FieldLogger
|
||||
Client client.Client
|
||||
Log log.FieldLogger
|
||||
Client client.Client
|
||||
ApplicationSetNamespaces []string
|
||||
}
|
||||
|
||||
func (h *clusterSecretEventHandler) Create(ctx context.Context, e event.CreateEvent, q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
|
||||
@@ -68,6 +70,10 @@ func (h *clusterSecretEventHandler) queueRelatedAppGenerators(ctx context.Contex
|
||||
|
||||
h.Log.WithField("count", len(appSetList.Items)).Info("listed ApplicationSets")
|
||||
for _, appSet := range appSetList.Items {
|
||||
if !utils.IsNamespaceAllowed(h.ApplicationSetNamespaces, appSet.GetNamespace()) {
|
||||
// Ignore it as not part of the allowed list of namespaces in which to watch Appsets
|
||||
continue
|
||||
}
|
||||
foundClusterGenerator := false
|
||||
for _, generator := range appSet.Spec.Generators {
|
||||
if generator.Clusters != nil {
|
||||
|
||||
@@ -137,7 +137,7 @@ func TestClusterEventHandler(t *testing.T) {
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-app-set",
|
||||
Namespace: "another-namespace",
|
||||
Namespace: "argocd",
|
||||
},
|
||||
Spec: argov1alpha1.ApplicationSetSpec{
|
||||
Generators: []argov1alpha1.ApplicationSetGenerator{
|
||||
@@ -171,9 +171,37 @@ func TestClusterEventHandler(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expectedRequests: []reconcile.Request{
|
||||
{NamespacedName: types.NamespacedName{Namespace: "another-namespace", Name: "my-app-set"}},
|
||||
{NamespacedName: types.NamespacedName{Namespace: "argocd", Name: "my-app-set"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "cluster generators in other namespaces should not match",
|
||||
items: []argov1alpha1.ApplicationSet{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-app-set",
|
||||
Namespace: "my-namespace-not-allowed",
|
||||
},
|
||||
Spec: argov1alpha1.ApplicationSetSpec{
|
||||
Generators: []argov1alpha1.ApplicationSetGenerator{
|
||||
{
|
||||
Clusters: &argov1alpha1.ClusterGenerator{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
secret: corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "argocd",
|
||||
Name: "my-secret",
|
||||
Labels: map[string]string{
|
||||
argocommon.LabelKeySecretType: argocommon.LabelValueSecretTypeCluster,
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedRequests: []reconcile.Request{},
|
||||
},
|
||||
{
|
||||
name: "non-argo cd secret should not match",
|
||||
items: []argov1alpha1.ApplicationSet{
|
||||
@@ -552,8 +580,9 @@ func TestClusterEventHandler(t *testing.T) {
|
||||
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithLists(&appSetList).Build()
|
||||
|
||||
handler := &clusterSecretEventHandler{
|
||||
Client: fakeClient,
|
||||
Log: log.WithField("type", "createSecretEventHandler"),
|
||||
Client: fakeClient,
|
||||
Log: log.WithField("type", "createSecretEventHandler"),
|
||||
ApplicationSetNamespaces: []string{"argocd"},
|
||||
}
|
||||
|
||||
mockAddRateLimitingInterface := mockAddRateLimitingInterface{}
|
||||
|
||||
@@ -29,10 +29,10 @@ type GitGenerator struct {
|
||||
}
|
||||
|
||||
// NewGitGenerator creates a new instance of Git Generator
|
||||
func NewGitGenerator(repos services.Repos, namespace string) Generator {
|
||||
func NewGitGenerator(repos services.Repos, controllerNamespace string) Generator {
|
||||
g := &GitGenerator{
|
||||
repos: repos,
|
||||
namespace: namespace,
|
||||
namespace: controllerNamespace,
|
||||
}
|
||||
|
||||
return g
|
||||
@@ -78,11 +78,11 @@ func (g *GitGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.Applic
|
||||
if !strings.Contains(appSet.Spec.Template.Spec.Project, "{{") {
|
||||
project := appSet.Spec.Template.Spec.Project
|
||||
appProject := &argoprojiov1alpha1.AppProject{}
|
||||
namespace := g.namespace
|
||||
if namespace == "" {
|
||||
namespace = appSet.Namespace
|
||||
controllerNamespace := g.namespace
|
||||
if controllerNamespace == "" {
|
||||
controllerNamespace = appSet.Namespace
|
||||
}
|
||||
if err := client.Get(context.TODO(), types.NamespacedName{Name: project, Namespace: namespace}, appProject); err != nil {
|
||||
if err := client.Get(context.TODO(), types.NamespacedName{Name: project, Namespace: controllerNamespace}, appProject); err != nil {
|
||||
return nil, fmt.Errorf("error getting project %s: %w", project, err)
|
||||
}
|
||||
// we need to verify the signature on the Git revision if GPG is enabled
|
||||
|
||||
@@ -119,15 +119,15 @@ func (g *PullRequestGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha
|
||||
"author": pull.Author,
|
||||
}
|
||||
|
||||
err := appendTemplatedValues(appSetGenerator.PullRequest.Values, paramMap, applicationSetInfo.Spec.GoTemplate, applicationSetInfo.Spec.GoTemplateOptions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to append templated values: %w", err)
|
||||
}
|
||||
|
||||
// PR lables will only be supported for Go Template appsets, since fasttemplate will be deprecated.
|
||||
if applicationSetInfo != nil && applicationSetInfo.Spec.GoTemplate {
|
||||
paramMap["labels"] = pull.Labels
|
||||
}
|
||||
|
||||
err := appendTemplatedValues(appSetGenerator.PullRequest.Values, paramMap, applicationSetInfo.Spec.GoTemplate, applicationSetInfo.Spec.GoTemplateOptions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to append templated values: %w", err)
|
||||
}
|
||||
params = append(params, paramMap)
|
||||
}
|
||||
return params, nil
|
||||
|
||||
@@ -277,6 +277,51 @@ func TestPullRequestGithubGenerateParams(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
selectFunc: func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) {
|
||||
return pullrequest.NewFakeService(
|
||||
ctx,
|
||||
[]*pullrequest.PullRequest{
|
||||
{
|
||||
Number: 1,
|
||||
Title: "title1",
|
||||
Branch: "my_branch",
|
||||
TargetBranch: "master",
|
||||
HeadSHA: "abcd",
|
||||
Author: "testName",
|
||||
Labels: []string{"preview", "preview:team1"},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
)
|
||||
},
|
||||
values: map[string]string{
|
||||
"preview_env": "{{ regexFind \"(team1|team2)\" (.labels | join \",\") }}",
|
||||
},
|
||||
expected: []map[string]any{
|
||||
{
|
||||
"number": "1",
|
||||
"title": "title1",
|
||||
"branch": "my_branch",
|
||||
"branch_slug": "my-branch",
|
||||
"target_branch": "master",
|
||||
"target_branch_slug": "master",
|
||||
"head_sha": "abcd",
|
||||
"head_short_sha": "abcd",
|
||||
"head_short_sha_7": "abcd",
|
||||
"author": "testName",
|
||||
"labels": []string{"preview", "preview:team1"},
|
||||
"values": map[string]string{"preview_env": "team1"},
|
||||
},
|
||||
},
|
||||
expectedErr: nil,
|
||||
applicationSet: argoprojiov1alpha1.ApplicationSet{
|
||||
Spec: argoprojiov1alpha1.ApplicationSetSpec{
|
||||
// Application set is using fasttemplate.
|
||||
GoTemplate: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
|
||||
@@ -10,15 +10,15 @@ import (
|
||||
"github.com/argoproj/argo-cd/v3/applicationset/services"
|
||||
)
|
||||
|
||||
func GetGenerators(ctx context.Context, c client.Client, k8sClient kubernetes.Interface, namespace string, argoCDService services.Repos, dynamicClient dynamic.Interface, scmConfig SCMConfig) map[string]Generator {
|
||||
func GetGenerators(ctx context.Context, c client.Client, k8sClient kubernetes.Interface, controllerNamespace string, argoCDService services.Repos, dynamicClient dynamic.Interface, scmConfig SCMConfig) map[string]Generator {
|
||||
terminalGenerators := map[string]Generator{
|
||||
"List": NewListGenerator(),
|
||||
"Clusters": NewClusterGenerator(ctx, c, k8sClient, namespace),
|
||||
"Git": NewGitGenerator(argoCDService, namespace),
|
||||
"Clusters": NewClusterGenerator(ctx, c, k8sClient, controllerNamespace),
|
||||
"Git": NewGitGenerator(argoCDService, controllerNamespace),
|
||||
"SCMProvider": NewSCMProviderGenerator(c, scmConfig),
|
||||
"ClusterDecisionResource": NewDuckTypeGenerator(ctx, dynamicClient, k8sClient, namespace),
|
||||
"ClusterDecisionResource": NewDuckTypeGenerator(ctx, dynamicClient, k8sClient, controllerNamespace),
|
||||
"PullRequest": NewPullRequestGenerator(c, scmConfig),
|
||||
"Plugin": NewPluginGenerator(c, namespace),
|
||||
"Plugin": NewPluginGenerator(c, controllerNamespace),
|
||||
}
|
||||
|
||||
nestedGenerators := map[string]Generator{
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
bitbucketv1 "github.com/gfleury/go-bitbucket-v1"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/applicationset/utils"
|
||||
"github.com/argoproj/argo-cd/v3/applicationset/services"
|
||||
)
|
||||
|
||||
type BitbucketService struct {
|
||||
@@ -49,15 +49,10 @@ func NewBitbucketServiceNoAuth(ctx context.Context, url, projectKey, repositoryS
|
||||
}
|
||||
|
||||
func newBitbucketService(ctx context.Context, bitbucketConfig *bitbucketv1.Configuration, projectKey, repositorySlug string, scmRootCAPath string, insecure bool, caCerts []byte) (PullRequestService, error) {
|
||||
bitbucketConfig.BasePath = utils.NormalizeBitbucketBasePath(bitbucketConfig.BasePath)
|
||||
tlsConfig := utils.GetTlsConfig(scmRootCAPath, insecure, caCerts)
|
||||
bitbucketConfig.HTTPClient = &http.Client{Transport: &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
}}
|
||||
bitbucketClient := bitbucketv1.NewAPIClient(ctx, bitbucketConfig)
|
||||
bbClient := services.SetupBitbucketClient(ctx, bitbucketConfig, scmRootCAPath, insecure, caCerts)
|
||||
|
||||
return &BitbucketService{
|
||||
client: bitbucketClient,
|
||||
client: bbClient,
|
||||
projectKey: projectKey,
|
||||
repositorySlug: repositorySlug,
|
||||
}, nil
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
bitbucketv1 "github.com/gfleury/go-bitbucket-v1"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/applicationset/utils"
|
||||
"github.com/argoproj/argo-cd/v3/applicationset/services"
|
||||
)
|
||||
|
||||
type BitbucketServerProvider struct {
|
||||
@@ -49,15 +49,10 @@ func NewBitbucketServerProviderNoAuth(ctx context.Context, url, projectKey strin
|
||||
}
|
||||
|
||||
func newBitbucketServerProvider(ctx context.Context, bitbucketConfig *bitbucketv1.Configuration, projectKey string, allBranches bool, scmRootCAPath string, insecure bool, caCerts []byte) (*BitbucketServerProvider, error) {
|
||||
bitbucketConfig.BasePath = utils.NormalizeBitbucketBasePath(bitbucketConfig.BasePath)
|
||||
tlsConfig := utils.GetTlsConfig(scmRootCAPath, insecure, caCerts)
|
||||
bitbucketConfig.HTTPClient = &http.Client{Transport: &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
}}
|
||||
bitbucketClient := bitbucketv1.NewAPIClient(ctx, bitbucketConfig)
|
||||
bbClient := services.SetupBitbucketClient(ctx, bitbucketConfig, scmRootCAPath, insecure, caCerts)
|
||||
|
||||
return &BitbucketServerProvider{
|
||||
client: bitbucketClient,
|
||||
client: bbClient,
|
||||
projectKey: projectKey,
|
||||
allBranches: allBranches,
|
||||
}, nil
|
||||
|
||||
22
applicationset/services/util.go
Normal file
22
applicationset/services/util.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
bitbucketv1 "github.com/gfleury/go-bitbucket-v1"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/applicationset/utils"
|
||||
)
|
||||
|
||||
// SetupBitbucketClient configures and creates a Bitbucket API client with TLS settings
|
||||
func SetupBitbucketClient(ctx context.Context, config *bitbucketv1.Configuration, scmRootCAPath string, insecure bool, caCerts []byte) *bitbucketv1.APIClient {
|
||||
config.BasePath = utils.NormalizeBitbucketBasePath(config.BasePath)
|
||||
tlsConfig := utils.GetTlsConfig(scmRootCAPath, insecure, caCerts)
|
||||
|
||||
transport := http.DefaultTransport.(*http.Transport).Clone()
|
||||
transport.TLSClientConfig = tlsConfig
|
||||
config.HTTPClient = &http.Client{Transport: transport}
|
||||
|
||||
return bitbucketv1.NewAPIClient(ctx, config)
|
||||
}
|
||||
37
applicationset/services/util_test.go
Normal file
37
applicationset/services/util_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
bitbucketv1 "github.com/gfleury/go-bitbucket-v1"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSetupBitbucketClient(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
cfg := &bitbucketv1.Configuration{}
|
||||
|
||||
// Act
|
||||
client := SetupBitbucketClient(ctx, cfg, "", false, nil)
|
||||
|
||||
// Assert
|
||||
require.NotNil(t, client, "expected client to be created")
|
||||
require.NotNil(t, cfg.HTTPClient, "expected HTTPClient to be set")
|
||||
|
||||
// The transport should be a clone of DefaultTransport
|
||||
tr, ok := cfg.HTTPClient.Transport.(*http.Transport)
|
||||
require.True(t, ok, "expected HTTPClient.Transport to be *http.Transport")
|
||||
require.NotSame(t, http.DefaultTransport, tr, "transport should be a clone, not the global DefaultTransport")
|
||||
|
||||
// Ensure TLSClientConfig is set
|
||||
require.IsType(t, &tls.Config{}, tr.TLSClientConfig)
|
||||
|
||||
// Defaults from http.DefaultTransport.Clone() should be preserved
|
||||
require.Greater(t, tr.IdleConnTimeout, time.Duration(0), "IdleConnTimeout should be non-zero")
|
||||
require.Positive(t, tr.MaxIdleConns, "MaxIdleConns should be non-zero")
|
||||
require.Greater(t, tr.TLSHandshakeTimeout, time.Duration(0), "TLSHandshakeTimeout should be non-zero")
|
||||
}
|
||||
@@ -399,19 +399,19 @@ func addInvalidGeneratorNames(names map[string]bool, applicationSetInfo *argoapp
|
||||
var values map[string]any
|
||||
err := json.Unmarshal([]byte(config), &values)
|
||||
if err != nil {
|
||||
log.Warnf("couldn't unmarshal kubectl.kubernetes.io/last-applied-configuration: %+v", config)
|
||||
log.Warnf("could not unmarshal kubectl.kubernetes.io/last-applied-configuration: %+v", config)
|
||||
return
|
||||
}
|
||||
|
||||
spec, ok := values["spec"].(map[string]any)
|
||||
if !ok {
|
||||
log.Warn("coundn't get spec from kubectl.kubernetes.io/last-applied-configuration annotation")
|
||||
log.Warn("could not get spec from kubectl.kubernetes.io/last-applied-configuration annotation")
|
||||
return
|
||||
}
|
||||
|
||||
generators, ok := spec["generators"].([]any)
|
||||
if !ok {
|
||||
log.Warn("coundn't get generators from kubectl.kubernetes.io/last-applied-configuration annotation")
|
||||
log.Warn("could not get generators from kubectl.kubernetes.io/last-applied-configuration annotation")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -422,7 +422,7 @@ func addInvalidGeneratorNames(names map[string]bool, applicationSetInfo *argoapp
|
||||
|
||||
generator, ok := generators[index].(map[string]any)
|
||||
if !ok {
|
||||
log.Warn("coundn't get generator from kubectl.kubernetes.io/last-applied-configuration annotation")
|
||||
log.Warn("could not get generator from kubectl.kubernetes.io/last-applied-configuration annotation")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -26,10 +26,14 @@ import (
|
||||
"github.com/go-playground/webhooks/v6/github"
|
||||
"github.com/go-playground/webhooks/v6/gitlab"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/util/guard"
|
||||
)
|
||||
|
||||
const payloadQueueSize = 50000
|
||||
|
||||
const panicMsgAppSet = "panic while processing applicationset-controller webhook event"
|
||||
|
||||
type WebhookHandler struct {
|
||||
sync.WaitGroup // for testing
|
||||
github *github.Webhook
|
||||
@@ -74,15 +78,15 @@ func NewWebhookHandler(webhookParallelism int, argocdSettingsMgr *argosettings.S
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get argocd settings: %w", err)
|
||||
}
|
||||
githubHandler, err := github.New(github.Options.Secret(argocdSettings.WebhookGitHubSecret))
|
||||
githubHandler, err := github.New(github.Options.Secret(argocdSettings.GetWebhookGitHubSecret()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to init GitHub webhook: %w", err)
|
||||
}
|
||||
gitlabHandler, err := gitlab.New(gitlab.Options.Secret(argocdSettings.WebhookGitLabSecret))
|
||||
gitlabHandler, err := gitlab.New(gitlab.Options.Secret(argocdSettings.GetWebhookGitLabSecret()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to init GitLab webhook: %w", err)
|
||||
}
|
||||
azuredevopsHandler, err := azuredevops.New(azuredevops.Options.BasicAuth(argocdSettings.WebhookAzureDevOpsUsername, argocdSettings.WebhookAzureDevOpsPassword))
|
||||
azuredevopsHandler, err := azuredevops.New(azuredevops.Options.BasicAuth(argocdSettings.GetWebhookAzureDevOpsUsername(), argocdSettings.GetWebhookAzureDevOpsPassword()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to init Azure DevOps webhook: %w", err)
|
||||
}
|
||||
@@ -102,6 +106,7 @@ func NewWebhookHandler(webhookParallelism int, argocdSettingsMgr *argosettings.S
|
||||
}
|
||||
|
||||
func (h *WebhookHandler) startWorkerPool(webhookParallelism int) {
|
||||
compLog := log.WithField("component", "applicationset-webhook")
|
||||
for i := 0; i < webhookParallelism; i++ {
|
||||
h.Add(1)
|
||||
go func() {
|
||||
@@ -111,7 +116,7 @@ func (h *WebhookHandler) startWorkerPool(webhookParallelism int) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
h.HandleEvent(payload)
|
||||
guard.RecoverAndLog(func() { h.HandleEvent(payload) }, compLog, panicMsgAppSet)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -339,7 +344,7 @@ func genRevisionHasChanged(gen *v1alpha1.GitGenerator, revision string, touchedH
|
||||
|
||||
func gitGeneratorUsesURL(gen *v1alpha1.GitGenerator, webURL string, repoRegexp *regexp.Regexp) bool {
|
||||
if !repoRegexp.MatchString(gen.RepoURL) {
|
||||
log.Debugf("%s does not match %s", gen.RepoURL, repoRegexp.String())
|
||||
log.Warnf("%s does not match %s", gen.RepoURL, repoRegexp.String())
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
# p, <role/user/group>, <resource>, <action>, <object>, <allow/deny>
|
||||
|
||||
p, role:readonly, applications, get, */*, allow
|
||||
p, role:readonly, applicationsets, get, */*, allow
|
||||
p, role:readonly, certificates, get, *, allow
|
||||
p, role:readonly, clusters, get, *, allow
|
||||
p, role:readonly, repositories, get, *, allow
|
||||
|
||||
|
98
assets/swagger.json
generated
98
assets/swagger.json
generated
@@ -374,6 +374,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/applications/{appName}/server-side-diff": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"ApplicationService"
|
||||
],
|
||||
"summary": "ServerSideDiff performs server-side diff calculation using dry-run apply",
|
||||
"operationId": "ApplicationService_ServerSideDiff",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"name": "appName",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "appNamespace",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"name": "project",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"collectionFormat": "multi",
|
||||
"name": "targetManifests",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "A successful response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/applicationApplicationServerSideDiffResponse"
|
||||
}
|
||||
},
|
||||
"default": {
|
||||
"description": "An unexpected error response.",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/runtimeError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/applications/{application.metadata.name}": {
|
||||
"put": {
|
||||
"tags": [
|
||||
@@ -999,6 +1049,11 @@
|
||||
"collectionFormat": "multi",
|
||||
"name": "revisions",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"name": "noCache",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
@@ -5019,6 +5074,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"applicationApplicationServerSideDiffResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/v1alpha1ResourceDiff"
|
||||
}
|
||||
},
|
||||
"modified": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"applicationApplicationSyncRequest": {
|
||||
"type": "object",
|
||||
"title": "ApplicationSyncRequest is a request to apply the config state to live state",
|
||||
@@ -7253,6 +7322,11 @@
|
||||
"items": {
|
||||
"$ref": "#/definitions/applicationv1alpha1ResourceStatus"
|
||||
}
|
||||
},
|
||||
"resourcesCount": {
|
||||
"description": "ResourcesCount is the total number of resources managed by this application set. The count may be higher than actual number of items in the Resources field when\nthe number of managed resources exceeds the limit imposed by the controller (to avoid making the status field too large).",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -9594,21 +9668,9 @@
|
||||
"description": "ResourceActionParam represents a parameter for a resource action.\nIt includes a name, value, type, and an optional default value for the parameter.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"default": {
|
||||
"description": "Default is the default value of the parameter, if any.",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "Name is the name of the parameter.",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"description": "Type is the type of the parameter (e.g., string, integer).",
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"description": "Value is the value of the parameter.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -9908,6 +9970,10 @@
|
||||
"description": "Limit is the maximum number of attempts for retrying a failed sync. If set to 0, no retries will be performed.",
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"refresh": {
|
||||
"type": "boolean",
|
||||
"title": "Refresh indicates if the latest revision should be used on retry instead of the initial one (default: false)"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -10488,7 +10554,7 @@
|
||||
"type": "boolean",
|
||||
"title": "AllowEmpty allows apps have zero live resources (default: false)"
|
||||
},
|
||||
"enable": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"title": "Enable allows apps to explicitly control automated sync"
|
||||
},
|
||||
@@ -10507,12 +10573,12 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"description": "Path is a directory path within the git repository where hydrated manifests should be committed to and synced\nfrom. If hydrateTo is set, this is just the path from which hydrated manifests will be synced.",
|
||||
"description": "Path is a directory path within the git repository where hydrated manifests should be committed to and synced\nfrom. The Path should never point to the root of the repo. If hydrateTo is set, this is just the path from which\nhydrated manifests will be synced.\n\n+kubebuilder:validation:Required\n+kubebuilder:validation:MinLength=1\n+kubebuilder:validation:Pattern=`^.{2,}|[^./]$`",
|
||||
"type": "string"
|
||||
},
|
||||
"targetBranch": {
|
||||
"type": "string",
|
||||
"title": "TargetBranch is the branch to which hydrated manifests should be committed"
|
||||
"description": "TargetBranch is the branch from which hydrated manifests will be synced.\nIf HydrateTo is not set, this is also the branch to which hydrated manifests are committed.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
|
||||
logutils "github.com/argoproj/argo-cd/v3/util/log"
|
||||
"github.com/argoproj/argo-cd/v3/util/profile"
|
||||
"github.com/argoproj/argo-cd/v3/util/tls"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/applicationset/controllers"
|
||||
@@ -79,6 +80,7 @@ func NewCommand() *cobra.Command {
|
||||
enableScmProviders bool
|
||||
webhookParallelism int
|
||||
tokenRefStrictMode bool
|
||||
maxResourcesStatusCount int
|
||||
)
|
||||
scheme := runtime.NewScheme()
|
||||
_ = clientgoscheme.AddToScheme(scheme)
|
||||
@@ -169,6 +171,15 @@ func NewCommand() *cobra.Command {
|
||||
log.Error(err, "unable to start manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
pprofMux := http.NewServeMux()
|
||||
profile.RegisterProfiler(pprofMux)
|
||||
// This looks a little strange. Eg, not using ctrl.Options PprofBindAddress and then adding the pprof mux
|
||||
// to the metrics server. However, it allows for the controller to dynamically expose the pprof endpoints
|
||||
// and use the existing metrics server, the same pattern that the application controller and api-server follow.
|
||||
if err = mgr.AddMetricsServerExtraHandler("/debug/pprof/", pprofMux); err != nil {
|
||||
log.Error(err, "failed to register pprof handlers")
|
||||
}
|
||||
dynamicClient, err := dynamic.NewForConfig(mgr.GetConfig())
|
||||
errors.CheckError(err)
|
||||
k8sClient, err := kubernetes.NewForConfig(mgr.GetConfig())
|
||||
@@ -231,6 +242,7 @@ func NewCommand() *cobra.Command {
|
||||
GlobalPreservedAnnotations: globalPreservedAnnotations,
|
||||
GlobalPreservedLabels: globalPreservedLabels,
|
||||
Metrics: &metrics,
|
||||
MaxResourcesStatusCount: maxResourcesStatusCount,
|
||||
}).SetupWithManager(mgr, enableProgressiveSyncs, maxConcurrentReconciliations); err != nil {
|
||||
log.Error(err, "unable to create controller", "controller", "ApplicationSet")
|
||||
os.Exit(1)
|
||||
@@ -275,6 +287,7 @@ func NewCommand() *cobra.Command {
|
||||
command.Flags().IntVar(&webhookParallelism, "webhook-parallelism-limit", env.ParseNumFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_WEBHOOK_PARALLELISM_LIMIT", 50, 1, 1000), "Number of webhook requests processed concurrently")
|
||||
command.Flags().StringSliceVar(&metricsAplicationsetLabels, "metrics-applicationset-labels", []string{}, "List of Application labels that will be added to the argocd_applicationset_labels metric")
|
||||
command.Flags().BoolVar(&enableGitHubAPIMetrics, "enable-github-api-metrics", env.ParseBoolFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_GITHUB_API_METRICS", false), "Enable GitHub API metrics for generators that use the GitHub API")
|
||||
command.Flags().IntVar(&maxResourcesStatusCount, "max-resources-status-count", env.ParseNumFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_MAX_RESOURCES_STATUS_COUNT", 0, 0, math.MaxInt), "Max number of resources stored in appset status.")
|
||||
|
||||
return &command
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func NewCommand() *cobra.Command {
|
||||
if nonce == "" {
|
||||
errors.CheckError(fmt.Errorf("%s is not set", askpass.ASKPASS_NONCE_ENV))
|
||||
}
|
||||
conn, err := grpc_util.BlockingDial(ctx, "unix", askpass.SocketPath, nil, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
conn, err := grpc_util.BlockingNewClient(ctx, "unix", askpass.SocketPath, nil, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
errors.CheckError(err)
|
||||
defer utilio.Close(conn)
|
||||
client := askpass.NewAskPassServiceClient(conn)
|
||||
|
||||
@@ -11,16 +11,6 @@ import (
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/common"
|
||||
"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/util/env"
|
||||
"github.com/argoproj/argo-cd/v3/util/errors"
|
||||
service "github.com/argoproj/argo-cd/v3/util/notification/argocd"
|
||||
"github.com/argoproj/argo-cd/v3/util/tls"
|
||||
|
||||
notificationscontroller "github.com/argoproj/argo-cd/v3/notification_controller/controller"
|
||||
|
||||
"github.com/argoproj/notifications-engine/pkg/controller"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
@@ -30,27 +20,25 @@ import (
|
||||
"k8s.io/client-go/kubernetes"
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/common"
|
||||
notificationscontroller "github.com/argoproj/argo-cd/v3/notification_controller/controller"
|
||||
"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/v3/util/cli"
|
||||
"github.com/argoproj/argo-cd/v3/util/env"
|
||||
"github.com/argoproj/argo-cd/v3/util/errors"
|
||||
service "github.com/argoproj/argo-cd/v3/util/notification/argocd"
|
||||
"github.com/argoproj/argo-cd/v3/util/tls"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultMetricsPort = 9001
|
||||
)
|
||||
|
||||
func addK8SFlagsToCmd(cmd *cobra.Command) clientcmd.ClientConfig {
|
||||
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||
loadingRules.DefaultClientConfig = &clientcmd.DefaultClientConfig
|
||||
overrides := clientcmd.ConfigOverrides{}
|
||||
kflags := clientcmd.RecommendedConfigOverrideFlags("")
|
||||
cmd.PersistentFlags().StringVar(&loadingRules.ExplicitPath, "kubeconfig", "", "Path to a kube config. Only required if out-of-cluster")
|
||||
clientcmd.BindOverrideFlags(&overrides, cmd.PersistentFlags(), kflags)
|
||||
return clientcmd.NewInteractiveDeferredLoadingClientConfig(loadingRules, &overrides, os.Stdin)
|
||||
}
|
||||
|
||||
func NewCommand() *cobra.Command {
|
||||
var (
|
||||
clientConfig clientcmd.ClientConfig
|
||||
processorsCount int
|
||||
namespace string
|
||||
appLabelSelector string
|
||||
logLevel string
|
||||
logFormat string
|
||||
@@ -175,10 +163,9 @@ func NewCommand() *cobra.Command {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
clientConfig = addK8SFlagsToCmd(&command)
|
||||
clientConfig = cli.AddKubectlFlagsToCmd(&command)
|
||||
command.Flags().IntVar(&processorsCount, "processors-count", 1, "Processors count.")
|
||||
command.Flags().StringVar(&appLabelSelector, "app-label-selector", "", "App label selector.")
|
||||
command.Flags().StringVar(&namespace, "namespace", "", "Namespace which controller handles. Current namespace if empty.")
|
||||
command.Flags().StringVar(&logLevel, "loglevel", env.StringFromEnv("ARGOCD_NOTIFICATIONS_CONTROLLER_LOGLEVEL", "info"), "Set the logging level. One of: debug|info|warn|error")
|
||||
command.Flags().StringVar(&logFormat, "logformat", env.StringFromEnv("ARGOCD_NOTIFICATIONS_CONTROLLER_LOGFORMAT", "json"), "Set the logging format. One of: json|text")
|
||||
command.Flags().IntVar(&metricsPort, "metrics-port", defaultMetricsPort, "Metrics port")
|
||||
|
||||
@@ -80,6 +80,7 @@ func NewCommand() *cobra.Command {
|
||||
includeHiddenDirectories bool
|
||||
cmpUseManifestGeneratePaths bool
|
||||
ociMediaTypes []string
|
||||
enableBuiltinGitConfig bool
|
||||
)
|
||||
command := cobra.Command{
|
||||
Use: cliName,
|
||||
@@ -155,6 +156,7 @@ func NewCommand() *cobra.Command {
|
||||
IncludeHiddenDirectories: includeHiddenDirectories,
|
||||
CMPUseManifestGeneratePaths: cmpUseManifestGeneratePaths,
|
||||
OCIMediaTypes: ociMediaTypes,
|
||||
EnableBuiltinGitConfig: enableBuiltinGitConfig,
|
||||
}, askPassServer)
|
||||
errors.CheckError(err)
|
||||
|
||||
@@ -264,6 +266,7 @@ func NewCommand() *cobra.Command {
|
||||
command.Flags().BoolVar(&includeHiddenDirectories, "include-hidden-directories", env.ParseBoolFromEnv("ARGOCD_REPO_SERVER_INCLUDE_HIDDEN_DIRECTORIES", false), "Include hidden directories from Git")
|
||||
command.Flags().BoolVar(&cmpUseManifestGeneratePaths, "plugin-use-manifest-generate-paths", env.ParseBoolFromEnv("ARGOCD_REPO_SERVER_PLUGIN_USE_MANIFEST_GENERATE_PATHS", false), "Pass the resources described in argocd.argoproj.io/manifest-generate-paths value to the cmpserver to generate the application manifests.")
|
||||
command.Flags().StringSliceVar(&ociMediaTypes, "oci-layer-media-types", env.StringsFromEnv("ARGOCD_REPO_SERVER_OCI_LAYER_MEDIA_TYPES", []string{"application/vnd.oci.image.layer.v1.tar", "application/vnd.oci.image.layer.v1.tar+gzip", "application/vnd.cncf.helm.chart.content.v1.tar+gzip"}, ","), "Comma separated list of allowed media types for OCI media types. This only accounts for media types within layers.")
|
||||
command.Flags().BoolVar(&enableBuiltinGitConfig, "enable-builtin-git-config", env.ParseBoolFromEnv("ARGOCD_REPO_SERVER_ENABLE_BUILTIN_GIT_CONFIG", true), "Enable builtin git configuration options that are required for correct argocd-repo-server operation.")
|
||||
tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(&command)
|
||||
cacheSrc = reposervercache.AddCacheFlagsToCmd(&command, cacheutil.Options{
|
||||
OnClientCreated: func(client *redis.Client) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/redis/go-redis/v9"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/fake"
|
||||
@@ -608,7 +609,31 @@ func NewGenClusterConfigCommand(pathOpts *clientcmd.PathOptions) *cobra.Command
|
||||
clientConfig := clientcmd.NewDefaultClientConfig(*cfgAccess, &overrides)
|
||||
conf, err := clientConfig.ClientConfig()
|
||||
errors.CheckError(err)
|
||||
kubeClientset := fake.NewClientset()
|
||||
// Seed a minimal in-memory Argo CD environment so settings retrieval succeeds
|
||||
argoCDCM := &corev1.ConfigMap{
|
||||
TypeMeta: metav1.TypeMeta{Kind: "ConfigMap", APIVersion: "v1"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: common.ArgoCDConfigMapName,
|
||||
Namespace: ArgoCDNamespace,
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/part-of": "argocd",
|
||||
},
|
||||
},
|
||||
}
|
||||
argoCDSecret := &corev1.Secret{
|
||||
TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: common.ArgoCDSecretName,
|
||||
Namespace: ArgoCDNamespace,
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/part-of": "argocd",
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"server.secretkey": []byte("test"),
|
||||
},
|
||||
}
|
||||
kubeClientset := fake.NewClientset(argoCDCM, argoCDSecret)
|
||||
|
||||
var awsAuthConf *v1alpha1.AWSAuthConfig
|
||||
var execProviderConf *v1alpha1.ExecProviderConfig
|
||||
|
||||
@@ -30,11 +30,12 @@ func NewNotificationsCommand() *cobra.Command {
|
||||
)
|
||||
|
||||
var argocdService service.Service
|
||||
|
||||
toolsCommand := cmd.NewToolsCommand(
|
||||
"notifications",
|
||||
"argocd admin notifications",
|
||||
applications,
|
||||
settings.GetFactorySettingsForCLI(argocdService, "argocd-notifications-secret", "argocd-notifications-cm", false),
|
||||
settings.GetFactorySettingsForCLI(func() service.Service { return argocdService }, "argocd-notifications-secret", "argocd-notifications-cm", false),
|
||||
func(clientConfig clientcmd.ClientConfig) {
|
||||
k8sCfg, err := clientConfig.ClientConfig()
|
||||
if err != nil {
|
||||
|
||||
@@ -39,9 +39,13 @@ import (
|
||||
"github.com/argoproj/argo-cd/v3/cmd/argocd/commands/headless"
|
||||
"github.com/argoproj/argo-cd/v3/cmd/argocd/commands/utils"
|
||||
cmdutil "github.com/argoproj/argo-cd/v3/cmd/util"
|
||||
argocommon "github.com/argoproj/argo-cd/v3/common"
|
||||
"github.com/argoproj/argo-cd/v3/controller"
|
||||
argocdclient "github.com/argoproj/argo-cd/v3/pkg/apiclient"
|
||||
"github.com/argoproj/argo-cd/v3/pkg/apiclient/application"
|
||||
|
||||
resourceutil "github.com/argoproj/gitops-engine/pkg/sync/resource"
|
||||
|
||||
clusterpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/cluster"
|
||||
projectpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/project"
|
||||
"github.com/argoproj/argo-cd/v3/pkg/apiclient/settings"
|
||||
@@ -95,6 +99,7 @@ func NewApplicationCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
command.AddCommand(NewApplicationTerminateOpCommand(clientOpts))
|
||||
command.AddCommand(NewApplicationEditCommand(clientOpts))
|
||||
command.AddCommand(NewApplicationPatchCommand(clientOpts))
|
||||
command.AddCommand(NewApplicationGetResourceCommand(clientOpts))
|
||||
command.AddCommand(NewApplicationPatchResourceCommand(clientOpts))
|
||||
command.AddCommand(NewApplicationDeleteResourceCommand(clientOpts))
|
||||
command.AddCommand(NewApplicationResourceActionsCommand(clientOpts))
|
||||
@@ -348,7 +353,7 @@ func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
|
||||
command := &cobra.Command{
|
||||
Use: "get APPNAME",
|
||||
Short: "Get application details",
|
||||
Example: templates.Examples(`
|
||||
Example: templates.Examples(`
|
||||
# Get basic details about the application "my-app" in wide format
|
||||
argocd app get my-app -o wide
|
||||
|
||||
@@ -378,7 +383,7 @@ func NewApplicationGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
|
||||
|
||||
# Get application details and display them in a tree format
|
||||
argocd app get my-app --output tree
|
||||
|
||||
|
||||
# Get application details and display them in a detailed tree format
|
||||
argocd app get my-app --output tree=detailed
|
||||
`),
|
||||
@@ -536,7 +541,7 @@ func NewApplicationLogsCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
command := &cobra.Command{
|
||||
Use: "logs APPNAME",
|
||||
Short: "Get logs of application pods",
|
||||
Example: templates.Examples(`
|
||||
Example: templates.Examples(`
|
||||
# Get logs of pods associated with the application "my-app"
|
||||
argocd app logs my-app
|
||||
|
||||
@@ -850,7 +855,7 @@ func NewApplicationSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
|
||||
command := &cobra.Command{
|
||||
Use: "set APPNAME",
|
||||
Short: "Set application parameters",
|
||||
Example: templates.Examples(`
|
||||
Example: templates.Examples(`
|
||||
# Set application parameters for the application "my-app"
|
||||
argocd app set my-app --parameter key1=value1 --parameter key2=value2
|
||||
|
||||
@@ -1281,6 +1286,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
revision string
|
||||
localRepoRoot string
|
||||
serverSideGenerate bool
|
||||
serverSideDiff bool
|
||||
localIncludes []string
|
||||
appNamespace string
|
||||
revisions []string
|
||||
@@ -1343,6 +1349,22 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
argoSettings, err := settingsIf.Get(ctx, &settings.SettingsQuery{})
|
||||
errors.CheckError(err)
|
||||
diffOption := &DifferenceOption{}
|
||||
|
||||
hasServerSideDiffAnnotation := resourceutil.HasAnnotationOption(app, argocommon.AnnotationCompareOptions, "ServerSideDiff=true")
|
||||
|
||||
// Use annotation if flag not explicitly set
|
||||
if !c.Flags().Changed("server-side-diff") {
|
||||
serverSideDiff = hasServerSideDiffAnnotation
|
||||
} else if serverSideDiff && !hasServerSideDiffAnnotation {
|
||||
// Flag explicitly set to true, but app annotation is not set
|
||||
fmt.Fprintf(os.Stderr, "Warning: Application does not have ServerSideDiff=true annotation.\n")
|
||||
}
|
||||
|
||||
// Server side diff with local requires server side generate to be set as there will be a mismatch with client-generated manifests.
|
||||
if serverSideDiff && local != "" && !serverSideGenerate {
|
||||
log.Fatal("--server-side-diff with --local requires --server-side-generate.")
|
||||
}
|
||||
|
||||
switch {
|
||||
case app.Spec.HasMultipleSources() && len(revisions) > 0 && len(sourcePositions) > 0:
|
||||
numOfSources := int64(len(app.Spec.GetSources()))
|
||||
@@ -1357,6 +1379,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
AppNamespace: &appNs,
|
||||
Revisions: revisions,
|
||||
SourcePositions: sourcePositions,
|
||||
NoCache: &hardRefresh,
|
||||
}
|
||||
res, err := appIf.GetManifests(ctx, &q)
|
||||
errors.CheckError(err)
|
||||
@@ -1368,6 +1391,7 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
Name: &appName,
|
||||
Revision: &revision,
|
||||
AppNamespace: &appNs,
|
||||
NoCache: &hardRefresh,
|
||||
}
|
||||
res, err := appIf.GetManifests(ctx, &q)
|
||||
errors.CheckError(err)
|
||||
@@ -1398,7 +1422,8 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
}
|
||||
}
|
||||
proj := getProject(ctx, c, clientOpts, app.Spec.Project)
|
||||
foundDiffs := findandPrintDiff(ctx, app, proj.Project, resources, argoSettings, diffOption, ignoreNormalizerOpts)
|
||||
|
||||
foundDiffs := findAndPrintDiff(ctx, app, proj.Project, resources, argoSettings, diffOption, ignoreNormalizerOpts, serverSideDiff, appIf, app.GetName(), app.GetNamespace())
|
||||
if foundDiffs && exitCode {
|
||||
os.Exit(diffExitCode)
|
||||
}
|
||||
@@ -1407,11 +1432,12 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
command.Flags().BoolVar(&refresh, "refresh", false, "Refresh application data when retrieving")
|
||||
command.Flags().BoolVar(&hardRefresh, "hard-refresh", false, "Refresh application data as well as target manifests cache")
|
||||
command.Flags().BoolVar(&exitCode, "exit-code", true, "Return non-zero exit code when there is a diff. May also return non-zero exit code if there is an error.")
|
||||
command.Flags().IntVar(&diffExitCode, "diff-exit-code", 1, "Return specified exit code when there is a diff. Typical error code is 20.")
|
||||
command.Flags().IntVar(&diffExitCode, "diff-exit-code", 1, "Return specified exit code when there is a diff. Typical error code is 20 but use another exit code if you want to differentiate from the generic exit code (20) returned by all CLI commands.")
|
||||
command.Flags().StringVar(&local, "local", "", "Compare live app to a local manifests")
|
||||
command.Flags().StringVar(&revision, "revision", "", "Compare live app to a particular revision")
|
||||
command.Flags().StringVar(&localRepoRoot, "local-repo-root", "/", "Path to the repository root. Used together with --local allows setting the repository root")
|
||||
command.Flags().BoolVar(&serverSideGenerate, "server-side-generate", false, "Used with --local, this will send your manifests to the server for diffing")
|
||||
command.Flags().BoolVar(&serverSideDiff, "server-side-diff", false, "Use server-side diff to calculate the diff. This will default to true if the ServerSideDiff annotation is set on the application.")
|
||||
command.Flags().StringArrayVar(&localIncludes, "local-include", []string{"*.yaml", "*.yml", "*.json"}, "Used with --server-side-generate, specify patterns of filenames to send. Matching is based on filename and not path.")
|
||||
command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Only render the difference in namespace")
|
||||
command.Flags().StringArrayVar(&revisions, "revisions", []string{}, "Show manifests at specific revisions for source position in source-positions")
|
||||
@@ -1421,6 +1447,101 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
return command
|
||||
}
|
||||
|
||||
// printResourceDiff prints the diff header and calls cli.PrintDiff for a resource
|
||||
func printResourceDiff(group, kind, namespace, name string, live, target *unstructured.Unstructured) {
|
||||
fmt.Printf("\n===== %s/%s %s/%s ======\n", group, kind, namespace, name)
|
||||
_ = cli.PrintDiff(name, live, target)
|
||||
}
|
||||
|
||||
// findAndPrintServerSideDiff performs a server-side diff by making requests to the api server and prints the response
|
||||
func findAndPrintServerSideDiff(ctx context.Context, app *argoappv1.Application, items []objKeyLiveTarget, resources *application.ManagedResourcesResponse, appIf application.ApplicationServiceClient, appName, appNs string) bool {
|
||||
// Process each item for server-side diff
|
||||
foundDiffs := false
|
||||
for _, item := range items {
|
||||
if item.target != nil && hook.IsHook(item.target) || item.live != nil && hook.IsHook(item.live) {
|
||||
continue
|
||||
}
|
||||
|
||||
// For server-side diff, we need to create aligned arrays for this specific resource
|
||||
var liveResource *argoappv1.ResourceDiff
|
||||
var targetManifest string
|
||||
|
||||
if item.live != nil {
|
||||
for _, res := range resources.Items {
|
||||
if res.Group == item.key.Group && res.Kind == item.key.Kind &&
|
||||
res.Namespace == item.key.Namespace && res.Name == item.key.Name {
|
||||
liveResource = res
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if liveResource == nil {
|
||||
// Create empty live resource for creation case
|
||||
liveResource = &argoappv1.ResourceDiff{
|
||||
Group: item.key.Group,
|
||||
Kind: item.key.Kind,
|
||||
Namespace: item.key.Namespace,
|
||||
Name: item.key.Name,
|
||||
LiveState: "",
|
||||
TargetState: "",
|
||||
Modified: true,
|
||||
}
|
||||
}
|
||||
|
||||
if item.target != nil {
|
||||
jsonBytes, err := json.Marshal(item.target)
|
||||
if err != nil {
|
||||
errors.CheckError(fmt.Errorf("error marshaling target object: %w", err))
|
||||
}
|
||||
targetManifest = string(jsonBytes)
|
||||
}
|
||||
|
||||
// Call server-side diff for this individual resource
|
||||
serverSideDiffQuery := &application.ApplicationServerSideDiffQuery{
|
||||
AppName: &appName,
|
||||
AppNamespace: &appNs,
|
||||
Project: &app.Spec.Project,
|
||||
LiveResources: []*argoappv1.ResourceDiff{liveResource},
|
||||
TargetManifests: []string{targetManifest},
|
||||
}
|
||||
|
||||
serverSideDiffRes, err := appIf.ServerSideDiff(ctx, serverSideDiffQuery)
|
||||
if err != nil {
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
// Extract diff for this resource
|
||||
for _, resultItem := range serverSideDiffRes.Items {
|
||||
if resultItem.Hook || (!resultItem.Modified && resultItem.TargetState != "" && resultItem.LiveState != "") {
|
||||
continue
|
||||
}
|
||||
|
||||
if resultItem.Modified || resultItem.TargetState == "" || resultItem.LiveState == "" {
|
||||
var live, target *unstructured.Unstructured
|
||||
|
||||
if resultItem.TargetState != "" && resultItem.TargetState != "null" {
|
||||
target = &unstructured.Unstructured{}
|
||||
err = json.Unmarshal([]byte(resultItem.TargetState), target)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
if resultItem.LiveState != "" && resultItem.LiveState != "null" {
|
||||
live = &unstructured.Unstructured{}
|
||||
err = json.Unmarshal([]byte(resultItem.LiveState), live)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
// Print resulting diff for this resource
|
||||
foundDiffs = true
|
||||
printResourceDiff(resultItem.Group, resultItem.Kind, resultItem.Namespace, resultItem.Name, live, target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return foundDiffs
|
||||
}
|
||||
|
||||
// DifferenceOption struct to store diff options
|
||||
type DifferenceOption struct {
|
||||
local string
|
||||
@@ -1432,47 +1553,15 @@ type DifferenceOption struct {
|
||||
revisions []string
|
||||
}
|
||||
|
||||
// findandPrintDiff ... Prints difference between application current state and state stored in git or locally, returns boolean as true if difference is found else returns false
|
||||
func findandPrintDiff(ctx context.Context, app *argoappv1.Application, proj *argoappv1.AppProject, resources *application.ManagedResourcesResponse, argoSettings *settings.Settings, diffOptions *DifferenceOption, ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts) bool {
|
||||
// findAndPrintDiff ... Prints difference between application current state and state stored in git or locally, returns boolean as true if difference is found else returns false
|
||||
func findAndPrintDiff(ctx context.Context, app *argoappv1.Application, proj *argoappv1.AppProject, resources *application.ManagedResourcesResponse, argoSettings *settings.Settings, diffOptions *DifferenceOption, ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts, useServerSideDiff bool, appIf application.ApplicationServiceClient, appName, appNs string) bool {
|
||||
var foundDiffs bool
|
||||
liveObjs, err := cmdutil.LiveObjects(resources.Items)
|
||||
|
||||
items, err := prepareObjectsForDiff(ctx, app, proj, resources, argoSettings, diffOptions)
|
||||
errors.CheckError(err)
|
||||
items := make([]objKeyLiveTarget, 0)
|
||||
switch {
|
||||
case diffOptions.local != "":
|
||||
localObjs := groupObjsByKey(getLocalObjects(ctx, app, proj, diffOptions.local, diffOptions.localRepoRoot, argoSettings.AppLabelKey, diffOptions.cluster.Info.ServerVersion, diffOptions.cluster.Info.APIVersions, argoSettings.KustomizeOptions, argoSettings.TrackingMethod), liveObjs, app.Spec.Destination.Namespace)
|
||||
items = groupObjsForDiff(resources, localObjs, items, argoSettings, app.InstanceName(argoSettings.ControllerNamespace), app.Spec.Destination.Namespace)
|
||||
case diffOptions.revision != "" || len(diffOptions.revisions) > 0:
|
||||
var unstructureds []*unstructured.Unstructured
|
||||
for _, mfst := range diffOptions.res.Manifests {
|
||||
obj, err := argoappv1.UnmarshalToUnstructured(mfst)
|
||||
errors.CheckError(err)
|
||||
unstructureds = append(unstructureds, obj)
|
||||
}
|
||||
groupedObjs := groupObjsByKey(unstructureds, liveObjs, app.Spec.Destination.Namespace)
|
||||
items = groupObjsForDiff(resources, groupedObjs, items, argoSettings, app.InstanceName(argoSettings.ControllerNamespace), app.Spec.Destination.Namespace)
|
||||
case diffOptions.serversideRes != nil:
|
||||
var unstructureds []*unstructured.Unstructured
|
||||
for _, mfst := range diffOptions.serversideRes.Manifests {
|
||||
obj, err := argoappv1.UnmarshalToUnstructured(mfst)
|
||||
errors.CheckError(err)
|
||||
unstructureds = append(unstructureds, obj)
|
||||
}
|
||||
groupedObjs := groupObjsByKey(unstructureds, liveObjs, app.Spec.Destination.Namespace)
|
||||
items = groupObjsForDiff(resources, groupedObjs, items, argoSettings, app.InstanceName(argoSettings.ControllerNamespace), app.Spec.Destination.Namespace)
|
||||
default:
|
||||
for i := range resources.Items {
|
||||
res := resources.Items[i]
|
||||
live := &unstructured.Unstructured{}
|
||||
err := json.Unmarshal([]byte(res.NormalizedLiveState), &live)
|
||||
errors.CheckError(err)
|
||||
|
||||
target := &unstructured.Unstructured{}
|
||||
err = json.Unmarshal([]byte(res.TargetState), &target)
|
||||
errors.CheckError(err)
|
||||
|
||||
items = append(items, objKeyLiveTarget{kube.NewResourceKey(res.Group, res.Kind, res.Namespace, res.Name), live, target})
|
||||
}
|
||||
if useServerSideDiff {
|
||||
return findAndPrintServerSideDiff(ctx, app, items, resources, appIf, appName, appNs)
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
@@ -1499,7 +1588,6 @@ func findandPrintDiff(ctx context.Context, app *argoappv1.Application, proj *arg
|
||||
errors.CheckError(err)
|
||||
|
||||
if diffRes.Modified || item.target == nil || item.live == nil {
|
||||
fmt.Printf("\n===== %s/%s %s/%s ======\n", item.key.Group, item.key.Kind, item.key.Namespace, item.key.Name)
|
||||
var live *unstructured.Unstructured
|
||||
var target *unstructured.Unstructured
|
||||
if item.target != nil && item.live != nil {
|
||||
@@ -1511,10 +1599,8 @@ func findandPrintDiff(ctx context.Context, app *argoappv1.Application, proj *arg
|
||||
live = item.live
|
||||
target = item.target
|
||||
}
|
||||
if !foundDiffs {
|
||||
foundDiffs = true
|
||||
}
|
||||
_ = cli.PrintDiff(item.key.Name, live, target)
|
||||
foundDiffs = true
|
||||
printResourceDiff(item.key.Group, item.key.Kind, item.key.Namespace, item.key.Name, live, target)
|
||||
}
|
||||
}
|
||||
return foundDiffs
|
||||
@@ -2001,6 +2087,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
applyOutOfSyncOnly bool
|
||||
async bool
|
||||
retryLimit int64
|
||||
retryRefresh bool
|
||||
retryBackoffDuration time.Duration
|
||||
retryBackoffMaxDuration time.Duration
|
||||
retryBackoffFactor int64
|
||||
@@ -2272,9 +2359,10 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
default:
|
||||
log.Fatalf("Unknown sync strategy: '%s'", strategy)
|
||||
}
|
||||
if retryLimit > 0 {
|
||||
if retryLimit != 0 {
|
||||
syncReq.RetryStrategy = &argoappv1.RetryStrategy{
|
||||
Limit: retryLimit,
|
||||
Limit: retryLimit,
|
||||
Refresh: retryRefresh,
|
||||
Backoff: &argoappv1.Backoff{
|
||||
Duration: retryBackoffDuration.String(),
|
||||
MaxDuration: retryBackoffMaxDuration.String(),
|
||||
@@ -2296,7 +2384,11 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
fmt.Printf("====== Previewing differences between live and desired state of application %s ======\n", appQualifiedName)
|
||||
|
||||
proj := getProject(ctx, c, clientOpts, app.Spec.Project)
|
||||
foundDiffs = findandPrintDiff(ctx, app, proj.Project, resources, argoSettings, diffOption, ignoreNormalizerOpts)
|
||||
|
||||
// Check if application has ServerSideDiff annotation
|
||||
serverSideDiff := resourceutil.HasAnnotationOption(app, argocommon.AnnotationCompareOptions, "ServerSideDiff=true")
|
||||
|
||||
foundDiffs = findAndPrintDiff(ctx, app, proj.Project, resources, argoSettings, diffOption, ignoreNormalizerOpts, serverSideDiff, appIf, appName, appNs)
|
||||
if !foundDiffs {
|
||||
fmt.Printf("====== No Differences found ======\n")
|
||||
// if no differences found, then no need to sync
|
||||
@@ -2339,6 +2431,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
|
||||
command.Flags().StringArrayVar(&labels, "label", []string{}, "Sync only specific resources with a label. This option may be specified repeatedly.")
|
||||
command.Flags().UintVar(&timeout, "timeout", defaultCheckTimeoutSeconds, "Time out after this many seconds")
|
||||
command.Flags().Int64Var(&retryLimit, "retry-limit", 0, "Max number of allowed sync retries")
|
||||
command.Flags().BoolVar(&retryRefresh, "retry-refresh", false, "Indicates if the latest revision should be used on retry instead of the initial one")
|
||||
command.Flags().DurationVar(&retryBackoffDuration, "retry-backoff-duration", argoappv1.DefaultSyncRetryDuration, "Retry backoff base duration. Input needs to be a duration (e.g. 2m, 1h)")
|
||||
command.Flags().DurationVar(&retryBackoffMaxDuration, "retry-backoff-max-duration", argoappv1.DefaultSyncRetryMaxDuration, "Max retry backoff duration. Input needs to be a duration (e.g. 2m, 1h)")
|
||||
command.Flags().Int64Var(&retryBackoffFactor, "retry-backoff-factor", argoappv1.DefaultSyncRetryFactor, "Factor multiplies the base duration after each failed retry")
|
||||
@@ -3396,7 +3489,7 @@ func NewApplicationRemoveSourceCommand(clientOpts *argocdclient.ClientOptions) *
|
||||
Short: "Remove a source from multiple sources application.",
|
||||
Example: ` # Remove the source at position 1 from application's sources. Counting starts at 1.
|
||||
argocd app remove-source myapplication --source-position 1
|
||||
|
||||
|
||||
# Remove the source named "test" from application's sources.
|
||||
argocd app remove-source myapplication --source-name test`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
@@ -3519,3 +3612,60 @@ func NewApplicationConfirmDeletionCommand(clientOpts *argocdclient.ClientOptions
|
||||
command.Flags().StringVarP(&appNamespace, "app-namespace", "N", "", "Namespace of the target application where the source will be appended")
|
||||
return command
|
||||
}
|
||||
|
||||
// prepareObjectsForDiff prepares objects for diffing using the switch statement
|
||||
// to handle different diff options and building the objKeyLiveTarget items
|
||||
func prepareObjectsForDiff(ctx context.Context, app *argoappv1.Application, proj *argoappv1.AppProject, resources *application.ManagedResourcesResponse, argoSettings *settings.Settings, diffOptions *DifferenceOption) ([]objKeyLiveTarget, error) {
|
||||
liveObjs, err := cmdutil.LiveObjects(resources.Items)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]objKeyLiveTarget, 0)
|
||||
|
||||
switch {
|
||||
case diffOptions.local != "":
|
||||
localObjs := groupObjsByKey(getLocalObjects(ctx, app, proj, diffOptions.local, diffOptions.localRepoRoot, argoSettings.AppLabelKey, diffOptions.cluster.Info.ServerVersion, diffOptions.cluster.Info.APIVersions, argoSettings.KustomizeOptions, argoSettings.TrackingMethod), liveObjs, app.Spec.Destination.Namespace)
|
||||
items = groupObjsForDiff(resources, localObjs, items, argoSettings, app.InstanceName(argoSettings.ControllerNamespace), app.Spec.Destination.Namespace)
|
||||
case diffOptions.revision != "" || len(diffOptions.revisions) > 0:
|
||||
var unstructureds []*unstructured.Unstructured
|
||||
for _, mfst := range diffOptions.res.Manifests {
|
||||
obj, err := argoappv1.UnmarshalToUnstructured(mfst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
unstructureds = append(unstructureds, obj)
|
||||
}
|
||||
groupedObjs := groupObjsByKey(unstructureds, liveObjs, app.Spec.Destination.Namespace)
|
||||
items = groupObjsForDiff(resources, groupedObjs, items, argoSettings, app.InstanceName(argoSettings.ControllerNamespace), app.Spec.Destination.Namespace)
|
||||
case diffOptions.serversideRes != nil:
|
||||
var unstructureds []*unstructured.Unstructured
|
||||
for _, mfst := range diffOptions.serversideRes.Manifests {
|
||||
obj, err := argoappv1.UnmarshalToUnstructured(mfst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
unstructureds = append(unstructureds, obj)
|
||||
}
|
||||
groupedObjs := groupObjsByKey(unstructureds, liveObjs, app.Spec.Destination.Namespace)
|
||||
items = groupObjsForDiff(resources, groupedObjs, items, argoSettings, app.InstanceName(argoSettings.ControllerNamespace), app.Spec.Destination.Namespace)
|
||||
default:
|
||||
for i := range resources.Items {
|
||||
res := resources.Items[i]
|
||||
live := &unstructured.Unstructured{}
|
||||
err := json.Unmarshal([]byte(res.NormalizedLiveState), &live)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
target := &unstructured.Unstructured{}
|
||||
err = json.Unmarshal([]byte(res.TargetState), &target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items = append(items, objKeyLiveTarget{kube.NewResourceKey(res.Group, res.Kind, res.Namespace, res.Name), live, target})
|
||||
}
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@ package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
)
|
||||
@@ -117,3 +120,561 @@ func TestPrintResourcesTree(t *testing.T) {
|
||||
|
||||
assert.Equal(t, expectation, output)
|
||||
}
|
||||
|
||||
func TestFilterFieldsFromObject(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
obj unstructured.Unstructured
|
||||
filteredFields []string
|
||||
expectedFields []string
|
||||
unexpectedFields []string
|
||||
}{
|
||||
{
|
||||
name: "filter nested field",
|
||||
obj: unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "vX",
|
||||
"kind": "kind",
|
||||
"metadata": map[string]any{
|
||||
"name": "test",
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"testfield": map[string]any{
|
||||
"nestedtest": "test",
|
||||
},
|
||||
"testfield2": "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
filteredFields: []string{"spec.testfield.nestedtest"},
|
||||
expectedFields: []string{"spec.testfield.nestedtest"},
|
||||
unexpectedFields: []string{"spec.testfield2"},
|
||||
},
|
||||
{
|
||||
name: "filter multiple fields",
|
||||
obj: unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "vX",
|
||||
"kind": "kind",
|
||||
"metadata": map[string]any{
|
||||
"name": "test",
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"testfield": map[string]any{
|
||||
"nestedtest": "test",
|
||||
},
|
||||
"testfield2": "test",
|
||||
"testfield3": "deleteme",
|
||||
},
|
||||
},
|
||||
},
|
||||
filteredFields: []string{"spec.testfield.nestedtest", "spec.testfield3"},
|
||||
expectedFields: []string{"spec.testfield.nestedtest"},
|
||||
unexpectedFields: []string{"spec.testfield2"},
|
||||
},
|
||||
{
|
||||
name: "filter nested list object",
|
||||
obj: unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "vX",
|
||||
"kind": "kind",
|
||||
"metadata": map[string]any{
|
||||
"name": "test",
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"testfield": map[string]any{
|
||||
"nestedtest": "test",
|
||||
},
|
||||
"testfield2": "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
filteredFields: []string{"spec.testfield.nestedtest"},
|
||||
expectedFields: []string{"spec.testfield.nestedtest"},
|
||||
unexpectedFields: []string{"spec.testfield2"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.obj.SetName("test-object")
|
||||
|
||||
filtered := filterFieldsFromObject(&tt.obj, tt.filteredFields)
|
||||
|
||||
for _, field := range tt.expectedFields {
|
||||
fieldPath := strings.Split(field, ".")
|
||||
_, exists, err := unstructured.NestedFieldCopy(filtered.Object, fieldPath...)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, exists, "Expected field %s to exist", field)
|
||||
}
|
||||
|
||||
for _, field := range tt.unexpectedFields {
|
||||
fieldPath := strings.Split(field, ".")
|
||||
_, exists, err := unstructured.NestedFieldCopy(filtered.Object, fieldPath...)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, exists, "Expected field %s to not exist", field)
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.obj.GetName(), filtered.GetName())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractNestedItem(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
obj map[string]any
|
||||
fields []string
|
||||
depth int
|
||||
expected map[string]any
|
||||
}{
|
||||
{
|
||||
name: "extract simple nested item",
|
||||
obj: map[string]any{
|
||||
"listofitems": []any{
|
||||
map[string]any{
|
||||
"extract": "123",
|
||||
"dontextract": "abc",
|
||||
},
|
||||
map[string]any{
|
||||
"extract": "456",
|
||||
"dontextract": "def",
|
||||
},
|
||||
map[string]any{
|
||||
"extract": "789",
|
||||
"dontextract": "ghi",
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: []string{"listofitems", "extract"},
|
||||
depth: 0,
|
||||
expected: map[string]any{
|
||||
"listofitems": []any{
|
||||
map[string]any{
|
||||
"extract": "123",
|
||||
},
|
||||
map[string]any{
|
||||
"extract": "456",
|
||||
},
|
||||
map[string]any{
|
||||
"extract": "789",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "double nested list of objects",
|
||||
obj: map[string]any{
|
||||
"listofitems": []any{
|
||||
map[string]any{
|
||||
"doublenested": []any{
|
||||
map[string]any{
|
||||
"extract": "123",
|
||||
},
|
||||
},
|
||||
"dontextract": "abc",
|
||||
},
|
||||
map[string]any{
|
||||
"doublenested": []any{
|
||||
map[string]any{
|
||||
"extract": "456",
|
||||
},
|
||||
},
|
||||
"dontextract": "def",
|
||||
},
|
||||
map[string]any{
|
||||
"doublenested": []any{
|
||||
map[string]any{
|
||||
"extract": "789",
|
||||
},
|
||||
},
|
||||
"dontextract": "ghi",
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: []string{"listofitems", "doublenested", "extract"},
|
||||
depth: 0,
|
||||
expected: map[string]any{
|
||||
"listofitems": []any{
|
||||
map[string]any{
|
||||
"doublenested": []any{
|
||||
map[string]any{
|
||||
"extract": "123",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"doublenested": []any{
|
||||
map[string]any{
|
||||
"extract": "456",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"doublenested": []any{
|
||||
map[string]any{
|
||||
"extract": "789",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "depth is greater then list of field size",
|
||||
obj: map[string]any{"test1": "1234567890"},
|
||||
fields: []string{"test1"},
|
||||
depth: 4,
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
filteredObj := extractNestedItem(tt.obj, tt.fields, tt.depth)
|
||||
assert.Equal(t, tt.expected, filteredObj, "Did not get the correct filtered obj")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractItemsFromList(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
list []any
|
||||
fields []string
|
||||
expected []any
|
||||
}{
|
||||
{
|
||||
name: "test simple field",
|
||||
list: []any{
|
||||
map[string]any{"extract": "value1", "dontextract": "valueA"},
|
||||
map[string]any{"extract": "value2", "dontextract": "valueB"},
|
||||
map[string]any{"extract": "value3", "dontextract": "valueC"},
|
||||
},
|
||||
fields: []string{"extract"},
|
||||
expected: []any{
|
||||
map[string]any{"extract": "value1"},
|
||||
map[string]any{"extract": "value2"},
|
||||
map[string]any{"extract": "value3"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test simple field with some depth",
|
||||
list: []any{
|
||||
map[string]any{
|
||||
"test1": map[string]any{
|
||||
"test2": map[string]any{
|
||||
"extract": "123",
|
||||
"dontextract": "abc",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"test1": map[string]any{
|
||||
"test2": map[string]any{
|
||||
"extract": "456",
|
||||
"dontextract": "def",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"test1": map[string]any{
|
||||
"test2": map[string]any{
|
||||
"extract": "789",
|
||||
"dontextract": "ghi",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: []string{"test1", "test2", "extract"},
|
||||
expected: []any{
|
||||
map[string]any{
|
||||
"test1": map[string]any{
|
||||
"test2": map[string]any{
|
||||
"extract": "123",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"test1": map[string]any{
|
||||
"test2": map[string]any{
|
||||
"extract": "456",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"test1": map[string]any{
|
||||
"test2": map[string]any{
|
||||
"extract": "789",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "test a missing field",
|
||||
list: []any{
|
||||
map[string]any{"test1": "123"},
|
||||
map[string]any{"test1": "456"},
|
||||
map[string]any{"test1": "789"},
|
||||
},
|
||||
fields: []string{"test2"},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "test getting an object",
|
||||
list: []any{
|
||||
map[string]any{
|
||||
"extract": map[string]any{
|
||||
"keyA": "valueA",
|
||||
"keyB": "valueB",
|
||||
"keyC": "valueC",
|
||||
},
|
||||
"dontextract": map[string]any{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
"key3": "value3",
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"extract": map[string]any{
|
||||
"keyD": "valueD",
|
||||
"keyE": "valueE",
|
||||
"keyF": "valueF",
|
||||
},
|
||||
"dontextract": map[string]any{
|
||||
"key4": "value4",
|
||||
"key5": "value5",
|
||||
"key6": "value6",
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: []string{"extract"},
|
||||
expected: []any{
|
||||
map[string]any{
|
||||
"extract": map[string]any{
|
||||
"keyA": "valueA",
|
||||
"keyB": "valueB",
|
||||
"keyC": "valueC",
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"extract": map[string]any{
|
||||
"keyD": "valueD",
|
||||
"keyE": "valueE",
|
||||
"keyF": "valueF",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
extractedList := extractItemsFromList(tt.list, tt.fields)
|
||||
assert.Equal(t, tt.expected, extractedList, "Lists were not equal")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconstructObject(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
extracted []any
|
||||
fields []string
|
||||
depth int
|
||||
expected map[string]any
|
||||
}{
|
||||
{
|
||||
name: "simple single field at depth 0",
|
||||
extracted: []any{"value1", "value2"},
|
||||
fields: []string{"items"},
|
||||
depth: 0,
|
||||
expected: map[string]any{
|
||||
"items": []any{"value1", "value2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "object nested at depth 1",
|
||||
extracted: []any{map[string]any{"key": "value"}},
|
||||
fields: []string{"test1", "test2"},
|
||||
depth: 1,
|
||||
expected: map[string]any{
|
||||
"test1": map[string]any{
|
||||
"test2": []any{map[string]any{"key": "value"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty list of extracted items",
|
||||
extracted: []any{},
|
||||
fields: []string{"test1"},
|
||||
depth: 0,
|
||||
expected: map[string]any{
|
||||
"test1": []any{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "complex object nesteed at depth 2",
|
||||
extracted: []any{map[string]any{
|
||||
"obj1": map[string]any{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
"obj2": map[string]any{
|
||||
"keyA": "valueA",
|
||||
"keyB": "valueB",
|
||||
},
|
||||
}},
|
||||
fields: []string{"test1", "test2", "test3"},
|
||||
depth: 2,
|
||||
expected: map[string]any{
|
||||
"test1": map[string]any{
|
||||
"test2": map[string]any{
|
||||
"test3": []any{
|
||||
map[string]any{
|
||||
"obj1": map[string]any{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
"obj2": map[string]any{
|
||||
"keyA": "valueA",
|
||||
"keyB": "valueB",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
filteredObj := reconstructObject(tt.extracted, tt.fields, tt.depth)
|
||||
assert.Equal(t, tt.expected, filteredObj, "objects were not equal")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintManifests(t *testing.T) {
|
||||
obj := unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "vX",
|
||||
"kind": "test",
|
||||
"metadata": map[string]any{
|
||||
"name": "unit-test",
|
||||
},
|
||||
"spec": map[string]any{
|
||||
"testfield": "testvalue",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expectedYAML := `apiVersion: vX
|
||||
kind: test
|
||||
metadata:
|
||||
name: unit-test
|
||||
spec:
|
||||
testfield: testvalue
|
||||
`
|
||||
|
||||
output, _ := captureOutput(func() error {
|
||||
printManifests(&[]unstructured.Unstructured{obj}, false, true, "yaml")
|
||||
return nil
|
||||
})
|
||||
assert.Equal(t, expectedYAML+"\n", output, "Incorrect yaml output for printManifests")
|
||||
|
||||
output, _ = captureOutput(func() error {
|
||||
printManifests(&[]unstructured.Unstructured{obj, obj}, false, true, "yaml")
|
||||
return nil
|
||||
})
|
||||
assert.Equal(t, expectedYAML+"\n---\n"+expectedYAML+"\n", output, "Incorrect yaml output with multiple objs.")
|
||||
|
||||
expectedJSON := `{
|
||||
"apiVersion": "vX",
|
||||
"kind": "test",
|
||||
"metadata": {
|
||||
"name": "unit-test"
|
||||
},
|
||||
"spec": {
|
||||
"testfield": "testvalue"
|
||||
}
|
||||
}`
|
||||
|
||||
output, _ = captureOutput(func() error {
|
||||
printManifests(&[]unstructured.Unstructured{obj}, false, true, "json")
|
||||
return nil
|
||||
})
|
||||
assert.Equal(t, expectedJSON+"\n", output, "Incorrect json output.")
|
||||
|
||||
output, _ = captureOutput(func() error {
|
||||
printManifests(&[]unstructured.Unstructured{obj, obj}, false, true, "json")
|
||||
return nil
|
||||
})
|
||||
assert.Equal(t, expectedJSON+"\n---\n"+expectedJSON+"\n", output, "Incorrect json output with multiple objs.")
|
||||
|
||||
output, _ = captureOutput(func() error {
|
||||
printManifests(&[]unstructured.Unstructured{obj}, true, true, "wide")
|
||||
return nil
|
||||
})
|
||||
assert.Contains(t, output, "FIELD RESOURCE NAME VALUE", "Missing or incorrect header line for table print with showing names.")
|
||||
assert.Contains(t, output, "apiVersion unit-test vX", "Missing or incorrect row in table related to apiVersion with showing names.")
|
||||
assert.Contains(t, output, "kind unit-test test", "Missing or incorrect line in the table related to kind with showing names.")
|
||||
assert.Contains(t, output, "spec.testfield unit-test testvalue", "Missing or incorrect line in the table related to spec.testfield with showing names.")
|
||||
assert.NotContains(t, output, "metadata.name unit-test testvalue", "Missing or incorrect line in the table related to metadata.name with showing names.")
|
||||
|
||||
output, _ = captureOutput(func() error {
|
||||
printManifests(&[]unstructured.Unstructured{obj}, true, false, "wide")
|
||||
return nil
|
||||
})
|
||||
assert.Contains(t, output, "FIELD VALUE", "Missing or incorrect header line for table print with not showing names.")
|
||||
assert.Contains(t, output, "apiVersion vX", "Missing or incorrect row in table related to apiVersion with not showing names.")
|
||||
assert.Contains(t, output, "kind test", "Missing or incorrect row in the table related to kind with not showing names.")
|
||||
assert.Contains(t, output, "spec.testfield testvalue", "Missing or incorrect row in the table related to spec.testefield with not showing names.")
|
||||
assert.NotContains(t, output, "metadata.name testvalue", "Missing or incorrect row in the tbale related to metadata.name with not showing names.")
|
||||
}
|
||||
|
||||
func TestPrintManifests_FilterNestedListObject_Wide(t *testing.T) {
|
||||
obj := unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "vX",
|
||||
"kind": "kind",
|
||||
"metadata": map[string]any{
|
||||
"name": "unit-test",
|
||||
},
|
||||
"status": map[string]any{
|
||||
"podIPs": []map[string]any{
|
||||
{
|
||||
"IP": "127.0.0.1",
|
||||
},
|
||||
{
|
||||
"IP": "127.0.0.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
output, _ := captureOutput(func() error {
|
||||
v, err := json.Marshal(&obj)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var obj2 *unstructured.Unstructured
|
||||
err = json.Unmarshal([]byte(v), &obj2)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
printManifests(&[]unstructured.Unstructured{*obj2}, false, true, "wide")
|
||||
return nil
|
||||
})
|
||||
|
||||
// Verify table header
|
||||
assert.Contains(t, output, "FIELD RESOURCE NAME VALUE", "Missing a line in the table")
|
||||
assert.Contains(t, output, "apiVersion unit-test vX", "Test for apiVersion field failed for wide output")
|
||||
assert.Contains(t, output, "kind unit-test kind", "Test for kind field failed for wide output")
|
||||
assert.Contains(t, output, "status.podIPs[0].IP unit-test 127.0.0.1", "Test for podIP array index 0 field failed for wide output")
|
||||
assert.Contains(t, output, "status.podIPs[1].IP unit-test 127.0.0.2", "Test for podIP array index 1 field failed for wide output")
|
||||
}
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/cmd/argocd/commands/utils"
|
||||
"github.com/argoproj/argo-cd/v3/cmd/util"
|
||||
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
@@ -22,15 +28,273 @@ import (
|
||||
utilio "github.com/argoproj/argo-cd/v3/util/io"
|
||||
)
|
||||
|
||||
// NewApplicationGetResourceCommand returns a new instance of the `app get-resource` command
|
||||
func NewApplicationGetResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
resourceName string
|
||||
kind string
|
||||
project string
|
||||
filteredFields []string
|
||||
showManagedFields bool
|
||||
output string
|
||||
)
|
||||
command := &cobra.Command{
|
||||
Use: "get-resource APPNAME",
|
||||
Short: "Get details about the live Kubernetes manifests of a resource in an application. The filter-fields flag can be used to only display fields you want to see.",
|
||||
Example: `
|
||||
# Get a specific resource, Pod my-app-pod, in 'my-app' by name in wide format
|
||||
argocd app get-resource my-app --kind Pod --resource-name my-app-pod
|
||||
|
||||
# Get a specific resource, Pod my-app-pod, in 'my-app' by name in yaml format
|
||||
argocd app get-resource my-app --kind Pod --resource-name my-app-pod -o yaml
|
||||
|
||||
# Get a specific resource, Pod my-app-pod, in 'my-app' by name in json format
|
||||
argocd app get-resource my-app --kind Pod --resource-name my-app-pod -o json
|
||||
|
||||
# Get details about all Pods in the application
|
||||
argocd app get-resource my-app --kind Pod
|
||||
|
||||
# Get a specific resource with managed fields, Pod my-app-pod, in 'my-app' by name in wide format
|
||||
argocd app get-resource my-app --kind Pod --resource-name my-app-pod --show-managed-fields
|
||||
|
||||
# Get the the details of a specific field in a resource in 'my-app' in the wide format
|
||||
argocd app get-resource my-app --kind Pod --filter-fields status.podIP
|
||||
|
||||
# Get the details of multiple specific fields in a specific resource in 'my-app' in the wide format
|
||||
argocd app get-resource my-app --kind Pod --resource-name my-app-pod --filter-fields status.podIP,status.hostIP`,
|
||||
}
|
||||
|
||||
command.Run = func(c *cobra.Command, args []string) {
|
||||
ctx := c.Context()
|
||||
|
||||
if len(args) != 1 {
|
||||
c.HelpFunc()(c, args)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
appName, appNs := argo.ParseFromQualifiedName(args[0], "")
|
||||
|
||||
conn, appIf := headless.NewClientOrDie(clientOpts, c).NewApplicationClientOrDie()
|
||||
defer utilio.Close(conn)
|
||||
|
||||
tree, err := appIf.ResourceTree(ctx, &applicationpkg.ResourcesQuery{
|
||||
ApplicationName: &appName,
|
||||
AppNamespace: &appNs,
|
||||
})
|
||||
errors.CheckError(err)
|
||||
|
||||
// Get manifests of resources
|
||||
// If resource name is "" find all resources of that kind
|
||||
var resources []unstructured.Unstructured
|
||||
var fetchedStr string
|
||||
for _, r := range tree.Nodes {
|
||||
if (resourceName != "" && r.Name != resourceName) || r.Kind != kind {
|
||||
continue
|
||||
}
|
||||
resource, err := appIf.GetResource(ctx, &applicationpkg.ApplicationResourceRequest{
|
||||
Name: &appName,
|
||||
AppNamespace: &appNs,
|
||||
Group: &r.Group,
|
||||
Kind: &r.Kind,
|
||||
Namespace: &r.Namespace,
|
||||
Project: &project,
|
||||
ResourceName: &r.Name,
|
||||
Version: &r.Version,
|
||||
})
|
||||
errors.CheckError(err)
|
||||
manifest := resource.GetManifest()
|
||||
|
||||
var obj *unstructured.Unstructured
|
||||
err = json.Unmarshal([]byte(manifest), &obj)
|
||||
errors.CheckError(err)
|
||||
|
||||
if !showManagedFields {
|
||||
unstructured.RemoveNestedField(obj.Object, "metadata", "managedFields")
|
||||
}
|
||||
|
||||
if len(filteredFields) != 0 {
|
||||
obj = filterFieldsFromObject(obj, filteredFields)
|
||||
}
|
||||
|
||||
fetchedStr += obj.GetName() + ", "
|
||||
resources = append(resources, *obj)
|
||||
}
|
||||
printManifests(&resources, len(filteredFields) > 0, resourceName == "", output)
|
||||
|
||||
if fetchedStr != "" {
|
||||
fetchedStr = strings.TrimSuffix(fetchedStr, ", ")
|
||||
}
|
||||
log.Infof("Resources '%s' fetched", fetchedStr)
|
||||
}
|
||||
|
||||
command.Flags().StringVar(&resourceName, "resource-name", "", "Name of resource, if none is included will output details of all resources with specified kind")
|
||||
command.Flags().StringVar(&kind, "kind", "", "Kind of resource [REQUIRED]")
|
||||
err := command.MarkFlagRequired("kind")
|
||||
errors.CheckError(err)
|
||||
command.Flags().StringVar(&project, "project", "", "Project of resource")
|
||||
command.Flags().StringSliceVar(&filteredFields, "filter-fields", nil, "A comma separated list of fields to display, if not provided will output the entire manifest")
|
||||
command.Flags().BoolVar(&showManagedFields, "show-managed-fields", false, "Show managed fields in the output manifest")
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Format of the output, wide, yaml, or json")
|
||||
return command
|
||||
}
|
||||
|
||||
// filterFieldsFromObject creates a new unstructured object containing only the specified fields from the source object.
|
||||
func filterFieldsFromObject(obj *unstructured.Unstructured, filteredFields []string) *unstructured.Unstructured {
|
||||
var filteredObj unstructured.Unstructured
|
||||
filteredObj.Object = make(map[string]any)
|
||||
|
||||
for _, f := range filteredFields {
|
||||
fields := strings.Split(f, ".")
|
||||
|
||||
value, exists, err := unstructured.NestedFieldCopy(obj.Object, fields...)
|
||||
if exists {
|
||||
errors.CheckError(err)
|
||||
err = unstructured.SetNestedField(filteredObj.Object, value, fields...)
|
||||
errors.CheckError(err)
|
||||
} else {
|
||||
// If doesn't exist assume its a nested inside a list of objects
|
||||
value := extractNestedItem(obj.Object, fields, 0)
|
||||
filteredObj.Object = value
|
||||
}
|
||||
}
|
||||
filteredObj.SetName(obj.GetName())
|
||||
return &filteredObj
|
||||
}
|
||||
|
||||
// extractNestedItem recursively extracts an item that may be nested inside a list of objects.
|
||||
func extractNestedItem(obj map[string]any, fields []string, depth int) map[string]any {
|
||||
if depth >= len(fields) {
|
||||
return nil
|
||||
}
|
||||
|
||||
value, exists, _ := unstructured.NestedFieldCopy(obj, fields[:depth+1]...)
|
||||
list, ok := value.([]any)
|
||||
if !exists || !ok {
|
||||
return extractNestedItem(obj, fields, depth+1)
|
||||
}
|
||||
|
||||
extractedItems := extractItemsFromList(list, fields[depth+1:])
|
||||
if len(extractedItems) == 0 {
|
||||
for _, e := range list {
|
||||
if o, ok := e.(map[string]any); ok {
|
||||
result := extractNestedItem(o, fields[depth+1:], 0)
|
||||
extractedItems = append(extractedItems, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filteredObj := reconstructObject(extractedItems, fields, depth)
|
||||
return filteredObj
|
||||
}
|
||||
|
||||
// extractItemsFromList processes a list of objects and extracts specific fields from each item.
|
||||
func extractItemsFromList(list []any, fields []string) []any {
|
||||
var extratedObjs []any
|
||||
for _, e := range list {
|
||||
extractedObj := make(map[string]any)
|
||||
if o, ok := e.(map[string]any); ok {
|
||||
value, exists, _ := unstructured.NestedFieldCopy(o, fields...)
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
err := unstructured.SetNestedField(extractedObj, value, fields...)
|
||||
errors.CheckError(err)
|
||||
extratedObjs = append(extratedObjs, extractedObj)
|
||||
}
|
||||
}
|
||||
return extratedObjs
|
||||
}
|
||||
|
||||
// reconstructObject rebuilds the original object structure by placing extracted items back into their proper nested location.
|
||||
func reconstructObject(extracted []any, fields []string, depth int) map[string]any {
|
||||
obj := make(map[string]any)
|
||||
err := unstructured.SetNestedField(obj, extracted, fields[:depth+1]...)
|
||||
errors.CheckError(err)
|
||||
return obj
|
||||
}
|
||||
|
||||
// printManifests outputs resource manifests in the specified format (wide, JSON, or YAML).
|
||||
func printManifests(objs *[]unstructured.Unstructured, filteredFields bool, showName bool, output string) {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
if showName {
|
||||
fmt.Fprintf(w, "FIELD\tRESOURCE NAME\tVALUE\n")
|
||||
} else {
|
||||
fmt.Fprintf(w, "FIELD\tVALUE\n")
|
||||
}
|
||||
|
||||
for i, o := range *objs {
|
||||
if output == "json" || output == "yaml" {
|
||||
var formattedManifest []byte
|
||||
var err error
|
||||
if output == "json" {
|
||||
formattedManifest, err = json.MarshalIndent(o.Object, "", " ")
|
||||
} else {
|
||||
formattedManifest, err = yaml.Marshal(o.Object)
|
||||
}
|
||||
errors.CheckError(err)
|
||||
|
||||
fmt.Println(string(formattedManifest))
|
||||
if len(*objs) > 1 && i != len(*objs)-1 {
|
||||
fmt.Println("---")
|
||||
}
|
||||
} else {
|
||||
name := o.GetName()
|
||||
if filteredFields {
|
||||
unstructured.RemoveNestedField(o.Object, "metadata", "name")
|
||||
}
|
||||
|
||||
printManifestAsTable(w, name, showName, o.Object, "")
|
||||
}
|
||||
}
|
||||
|
||||
if output != "json" && output != "yaml" {
|
||||
err := w.Flush()
|
||||
errors.CheckError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// printManifestAsTable recursively prints a manifest object as a tabular view with nested fields flattened.
|
||||
func printManifestAsTable(w *tabwriter.Writer, name string, showName bool, obj map[string]any, parentField string) {
|
||||
for key, value := range obj {
|
||||
field := parentField + key
|
||||
switch v := value.(type) {
|
||||
case map[string]any:
|
||||
printManifestAsTable(w, name, showName, v, field+".")
|
||||
case []any:
|
||||
for i, e := range v {
|
||||
index := "[" + strconv.Itoa(i) + "]"
|
||||
|
||||
if innerObj, ok := e.(map[string]any); ok {
|
||||
printManifestAsTable(w, name, showName, innerObj, field+index+".")
|
||||
} else {
|
||||
if showName {
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\n", field+index, name, e)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%v\t%v\n", field+index, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
if showName {
|
||||
fmt.Fprintf(w, "%v\t%v\t%v\n", field, name, v)
|
||||
} else {
|
||||
fmt.Fprintf(w, "%v\t%v\n", field, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func NewApplicationPatchResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var patch string
|
||||
var patchType string
|
||||
var resourceName string
|
||||
var namespace string
|
||||
var kind string
|
||||
var group string
|
||||
var all bool
|
||||
var project string
|
||||
var (
|
||||
patch string
|
||||
patchType string
|
||||
resourceName string
|
||||
namespace string
|
||||
kind string
|
||||
group string
|
||||
all bool
|
||||
project string
|
||||
)
|
||||
command := &cobra.Command{
|
||||
Use: "patch-resource APPNAME",
|
||||
Short: "Patch resource in an application",
|
||||
@@ -90,14 +354,16 @@ func NewApplicationPatchResourceCommand(clientOpts *argocdclient.ClientOptions)
|
||||
}
|
||||
|
||||
func NewApplicationDeleteResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var resourceName string
|
||||
var namespace string
|
||||
var kind string
|
||||
var group string
|
||||
var force bool
|
||||
var orphan bool
|
||||
var all bool
|
||||
var project string
|
||||
var (
|
||||
resourceName string
|
||||
namespace string
|
||||
kind string
|
||||
group string
|
||||
force bool
|
||||
orphan bool
|
||||
all bool
|
||||
project string
|
||||
)
|
||||
command := &cobra.Command{
|
||||
Use: "delete-resource APPNAME",
|
||||
Short: "Delete resource in an application",
|
||||
@@ -253,13 +519,16 @@ func printResources(listAll bool, orphaned bool, appResourceTree *v1alpha1.Appli
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = w.Flush()
|
||||
err := w.Flush()
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
func NewApplicationListResourcesCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var orphaned bool
|
||||
var output string
|
||||
var project string
|
||||
var (
|
||||
orphaned bool
|
||||
output string
|
||||
project string
|
||||
)
|
||||
command := &cobra.Command{
|
||||
Use: "resources APPNAME",
|
||||
Short: "List resource of application",
|
||||
|
||||
@@ -2253,6 +2253,10 @@ func (c *fakeAppServiceClient) ListResourceLinks(_ context.Context, _ *applicati
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *fakeAppServiceClient) ServerSideDiff(_ context.Context, _ *applicationpkg.ApplicationServerSideDiffQuery, _ ...grpc.CallOption) (*applicationpkg.ApplicationServerSideDiffResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type fakeAcdClient struct {
|
||||
simulateTimeout uint
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/util/cli"
|
||||
)
|
||||
|
||||
// NewBcryptCmd represents the bcrypt command
|
||||
@@ -15,8 +17,15 @@ func NewBcryptCmd() *cobra.Command {
|
||||
Use: "bcrypt",
|
||||
Short: "Generate bcrypt hash for any password",
|
||||
Example: `# Generate bcrypt hash for any password
|
||||
argocd account bcrypt --password YOUR_PASSWORD`,
|
||||
argocd account bcrypt --password YOUR_PASSWORD
|
||||
|
||||
# Prompt for password input
|
||||
argocd account bcrypt
|
||||
|
||||
# Read password from stdin
|
||||
echo -e "password" | argocd account bcrypt`,
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
password = cli.PromptPassword(password)
|
||||
bytePassword := []byte(password)
|
||||
// Hashing the password
|
||||
hash, err := bcrypt.GenerateFromPassword(bytePassword, bcrypt.DefaultCost)
|
||||
@@ -28,9 +37,5 @@ argocd account bcrypt --password YOUR_PASSWORD`,
|
||||
}
|
||||
|
||||
bcryptCmd.Flags().StringVar(&password, "password", "", "Password for which bcrypt hash is generated")
|
||||
err := bcryptCmd.MarkFlagRequired("password")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return bcryptCmd
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@ package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@@ -20,3 +22,27 @@ func TestGeneratePassword(t *testing.T) {
|
||||
err = bcrypt.CompareHashAndPassword(output.Bytes(), []byte("abc"))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGeneratePasswordWithStdin(t *testing.T) {
|
||||
oldStdin := os.Stdin
|
||||
defer func() {
|
||||
os.Stdin = oldStdin
|
||||
}()
|
||||
|
||||
input := bytes.NewBufferString("abc\n")
|
||||
r, w, _ := os.Pipe()
|
||||
_, _ = w.Write(input.Bytes())
|
||||
w.Close()
|
||||
os.Stdin = r
|
||||
|
||||
bcryptCmd := NewBcryptCmd()
|
||||
bcryptCmd.SetArgs([]string{})
|
||||
output := new(bytes.Buffer)
|
||||
bcryptCmd.SetOut(output)
|
||||
|
||||
err := bcryptCmd.Execute()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = bcrypt.CompareHashAndPassword(output.Bytes(), []byte("abc"))
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@@ -27,6 +28,10 @@ argocd configure --prompts-enabled=false`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
localCfg, err := localconfig.ReadLocalConfig(globalClientOpts.ConfigPath)
|
||||
errors.CheckError(err)
|
||||
if localCfg == nil {
|
||||
fmt.Println("No local configuration found")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
localCfg.PromptsEnabled = promptsEnabled
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ func NewLoginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comman
|
||||
username string
|
||||
password string
|
||||
sso bool
|
||||
callback string
|
||||
ssoPort int
|
||||
skipTestTLS bool
|
||||
ssoLaunchBrowser bool
|
||||
@@ -138,7 +139,7 @@ argocd login cd.argoproj.io --core`,
|
||||
errors.CheckError(err)
|
||||
oauth2conf, provider, err := acdClient.OIDCConfig(ctx, acdSet)
|
||||
errors.CheckError(err)
|
||||
tokenString, refreshToken = oauth2Login(ctx, ssoPort, acdSet.GetOIDCConfig(), oauth2conf, provider, ssoLaunchBrowser)
|
||||
tokenString, refreshToken = oauth2Login(ctx, callback, ssoPort, acdSet.GetOIDCConfig(), oauth2conf, provider, ssoLaunchBrowser)
|
||||
}
|
||||
parser := jwt.NewParser(jwt.WithoutClaimsValidation())
|
||||
claims := jwt.MapClaims{}
|
||||
@@ -185,6 +186,7 @@ argocd login cd.argoproj.io --core`,
|
||||
command.Flags().StringVar(&password, "password", "", "The password of an account to authenticate")
|
||||
command.Flags().BoolVar(&sso, "sso", false, "Perform SSO login")
|
||||
command.Flags().IntVar(&ssoPort, "sso-port", DefaultSSOLocalPort, "Port to run local OAuth2 login application")
|
||||
command.Flags().StringVar(&callback, "callback", "", "Scheme, Host and Port for the callback URL")
|
||||
command.Flags().BoolVar(&skipTestTLS, "skip-test-tls", false, "Skip testing whether the server is configured with TLS (this can help when the command hangs for no apparent reason)")
|
||||
command.Flags().BoolVar(&ssoLaunchBrowser, "sso-launch-browser", true, "Automatically launch the system default browser when performing SSO login")
|
||||
return command
|
||||
@@ -204,13 +206,19 @@ func userDisplayName(claims jwt.MapClaims) string {
|
||||
// returns the JWT token and a refresh token (if supported)
|
||||
func oauth2Login(
|
||||
ctx context.Context,
|
||||
callback string,
|
||||
port int,
|
||||
oidcSettings *settingspkg.OIDCConfig,
|
||||
oauth2conf *oauth2.Config,
|
||||
provider *oidc.Provider,
|
||||
ssoLaunchBrowser bool,
|
||||
) (string, string) {
|
||||
oauth2conf.RedirectURL = fmt.Sprintf("http://localhost:%d/auth/callback", port)
|
||||
redirectBase := callback
|
||||
if redirectBase == "" {
|
||||
redirectBase = "http://localhost:" + strconv.Itoa(port)
|
||||
}
|
||||
|
||||
oauth2conf.RedirectURL = redirectBase + "/auth/callback"
|
||||
oidcConf, err := oidcutil.ParseConfig(provider)
|
||||
errors.CheckError(err)
|
||||
log.Debug("OIDC Configuration:")
|
||||
|
||||
@@ -19,9 +19,12 @@ func NewLogoutCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comma
|
||||
Use: "logout CONTEXT",
|
||||
Short: "Log out from Argo CD",
|
||||
Long: "Log out from Argo CD",
|
||||
Example: `# To log out of argocd
|
||||
$ argocd logout
|
||||
Example: `# Logout from the active Argo CD context
|
||||
# This can be helpful for security reasons or when you want to switch between different Argo CD contexts or accounts.
|
||||
argocd logout CONTEXT
|
||||
|
||||
# Logout from a specific context named 'cd.argoproj.io'
|
||||
argocd logout cd.argoproj.io
|
||||
`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
if len(args) == 0 {
|
||||
|
||||
@@ -605,8 +605,17 @@ ID ISSUED-AT EXPIRES-AT
|
||||
fmt.Printf(printRoleFmtStr, "Description:", role.Description)
|
||||
fmt.Printf("Policies:\n")
|
||||
fmt.Printf("%s\n", proj.ProjectPoliciesString())
|
||||
fmt.Printf("Groups:\n")
|
||||
// if the group exists in the role
|
||||
// range over each group and print it
|
||||
if v1alpha1.RoleGroupExists(role) {
|
||||
for _, group := range role.Groups {
|
||||
fmt.Printf(" - %s\n", group)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("<none>")
|
||||
}
|
||||
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 proj.Status.JWTTokensByRole[roleName].Items {
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
func NewReloginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
var (
|
||||
password string
|
||||
callback string
|
||||
ssoPort int
|
||||
ssoLaunchBrowser bool
|
||||
)
|
||||
@@ -73,7 +74,7 @@ func NewReloginCommand(globalClientOpts *argocdclient.ClientOptions) *cobra.Comm
|
||||
errors.CheckError(err)
|
||||
oauth2conf, provider, err := acdClient.OIDCConfig(ctx, acdSet)
|
||||
errors.CheckError(err)
|
||||
tokenString, refreshToken = oauth2Login(ctx, ssoPort, acdSet.GetOIDCConfig(), oauth2conf, provider, ssoLaunchBrowser)
|
||||
tokenString, refreshToken = oauth2Login(ctx, callback, ssoPort, acdSet.GetOIDCConfig(), oauth2conf, provider, ssoLaunchBrowser)
|
||||
}
|
||||
|
||||
localCfg.UpsertUser(localconfig.User{
|
||||
@@ -100,6 +101,7 @@ argocd login cd.argoproj.io --core
|
||||
}
|
||||
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")
|
||||
command.Flags().StringVar(&callback, "callback", "", "Host and Port for the callback URL")
|
||||
command.Flags().BoolVar(&ssoLaunchBrowser, "sso-launch-browser", true, "Automatically launch the default browser when performing SSO login")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -270,6 +270,19 @@ func NewRepoRemoveCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command
|
||||
command := &cobra.Command{
|
||||
Use: "rm REPO ...",
|
||||
Short: "Remove configured repositories",
|
||||
Example: `
|
||||
# Remove a single repository
|
||||
argocd repo rm https://github.com/yourusername/your-repo.git
|
||||
|
||||
# Remove multiple repositories
|
||||
argocd repo rm https://github.com/yourusername/your-repo.git https://git.example.com/repo2.git
|
||||
|
||||
# Remove repositories for a specific project
|
||||
argocd repo rm https://github.com/yourusername/your-repo.git --project myproject
|
||||
|
||||
# Remove repository using SSH URL
|
||||
argocd repo rm git@github.com:yourusername/your-repo.git
|
||||
`,
|
||||
Run: func(c *cobra.Command, args []string) {
|
||||
ctx := c.Context()
|
||||
|
||||
@@ -355,16 +368,19 @@ func NewRepoListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
conn, repoIf := headless.NewClientOrDie(clientOpts, c).NewRepoClientOrDie()
|
||||
defer utilio.Close(conn)
|
||||
forceRefresh := false
|
||||
|
||||
switch refresh {
|
||||
case "":
|
||||
case "hard":
|
||||
forceRefresh = true
|
||||
default:
|
||||
err := stderrors.New("--refresh must be one of: 'hard'")
|
||||
err := fmt.Errorf("unknown refresh value: %s. Supported values: hard", refresh)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
|
||||
repos, err := repoIf.ListRepositories(ctx, &repositorypkg.RepoQuery{ForceRefresh: forceRefresh})
|
||||
errors.CheckError(err)
|
||||
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResourceList(repos.Items, output, false)
|
||||
@@ -375,12 +391,12 @@ func NewRepoListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
case "wide", "":
|
||||
printRepoTable(repos.Items)
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s. Supported formats: yaml|json|url|wide", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|url")
|
||||
command.Flags().StringVar(&refresh, "refresh", "", "Force a cache refresh on connection status , must be one of: 'hard'")
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. Supported formats: yaml|json|url|wide")
|
||||
command.Flags().StringVar(&refresh, "refresh", "", "Force a cache refresh on connection status. Supported values: hard")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -429,11 +445,12 @@ func NewRepoGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
case "hard":
|
||||
forceRefresh = true
|
||||
default:
|
||||
err := stderrors.New("--refresh must be one of: 'hard'")
|
||||
err := fmt.Errorf("unknown refresh value: %s. Supported values: hard", refresh)
|
||||
errors.CheckError(err)
|
||||
}
|
||||
repo, err := repoIf.Get(ctx, &repositorypkg.RepoQuery{Repo: repoURL, ForceRefresh: forceRefresh, AppProject: project})
|
||||
errors.CheckError(err)
|
||||
|
||||
switch output {
|
||||
case "yaml", "json":
|
||||
err := PrintResource(repo, output)
|
||||
@@ -444,13 +461,13 @@ func NewRepoGetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
|
||||
case "wide", "":
|
||||
printRepoTable(appsv1.Repositories{repo})
|
||||
default:
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s", output))
|
||||
errors.CheckError(fmt.Errorf("unknown output format: %s. Supported formats: yaml|json|url|wide", output))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
command.Flags().StringVar(&project, "project", "", "project of the repository")
|
||||
command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide|url")
|
||||
command.Flags().StringVar(&refresh, "refresh", "", "Force a cache refresh on connection status , must be one of: 'hard'")
|
||||
command.Flags().StringVar(&refresh, "refresh", "", "Force a cache refresh on connection status. Supported values: hard")
|
||||
return command
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
reposerver "github.com/argoproj/argo-cd/v3/cmd/argocd-repo-server/commands"
|
||||
apiserver "github.com/argoproj/argo-cd/v3/cmd/argocd-server/commands"
|
||||
cli "github.com/argoproj/argo-cd/v3/cmd/argocd/commands"
|
||||
"github.com/argoproj/argo-cd/v3/cmd/util"
|
||||
"github.com/argoproj/argo-cd/v3/util/log"
|
||||
)
|
||||
|
||||
@@ -74,7 +73,6 @@ func main() {
|
||||
command = cli.NewCommand()
|
||||
isArgocdCLI = true
|
||||
}
|
||||
util.SetAutoMaxProcs(isArgocdCLI)
|
||||
|
||||
if isArgocdCLI {
|
||||
// silence errors and usages since we'll be printing them manually.
|
||||
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/automaxprocs/maxprocs"
|
||||
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
"github.com/argoproj/gitops-engine/pkg/utils/kube"
|
||||
@@ -92,6 +90,7 @@ type AppOptions struct {
|
||||
retryBackoffDuration time.Duration
|
||||
retryBackoffMaxDuration time.Duration
|
||||
retryBackoffFactor int64
|
||||
retryRefresh bool
|
||||
ref string
|
||||
SourceName string
|
||||
drySourceRepo string
|
||||
@@ -102,19 +101,6 @@ type AppOptions struct {
|
||||
hydrateToBranch string
|
||||
}
|
||||
|
||||
// SetAutoMaxProcs sets the GOMAXPROCS value based on the binary name.
|
||||
// It suppresses logs for CLI binaries and logs the setting for services.
|
||||
func SetAutoMaxProcs(isCLI bool) {
|
||||
if isCLI {
|
||||
_, _ = maxprocs.Set() // Intentionally ignore errors for CLI binaries
|
||||
} else {
|
||||
_, err := maxprocs.Set(maxprocs.Logger(log.Infof))
|
||||
if err != nil {
|
||||
log.Errorf("Error setting GOMAXPROCS: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func AddAppFlags(command *cobra.Command, opts *AppOptions) {
|
||||
command.Flags().StringVar(&opts.repoURL, "repo", "", "Repository URL, ignored if a file is set")
|
||||
command.Flags().StringVar(&opts.appPath, "path", "", "Path in repository to the app directory, ignored if a file is set")
|
||||
@@ -183,6 +169,7 @@ func AddAppFlags(command *cobra.Command, opts *AppOptions) {
|
||||
command.Flags().DurationVar(&opts.retryBackoffDuration, "sync-retry-backoff-duration", argoappv1.DefaultSyncRetryDuration, "Sync retry backoff base duration. Input needs to be a duration (e.g. 2m, 1h)")
|
||||
command.Flags().DurationVar(&opts.retryBackoffMaxDuration, "sync-retry-backoff-max-duration", argoappv1.DefaultSyncRetryMaxDuration, "Max sync retry backoff duration. Input needs to be a duration (e.g. 2m, 1h)")
|
||||
command.Flags().Int64Var(&opts.retryBackoffFactor, "sync-retry-backoff-factor", argoappv1.DefaultSyncRetryFactor, "Factor multiplies the base duration after each failed sync retry")
|
||||
command.Flags().BoolVar(&opts.retryRefresh, "sync-retry-refresh", false, "Indicates if the latest revision should be used on retry instead of the initial one")
|
||||
command.Flags().StringVar(&opts.ref, "ref", "", "Ref is reference to another source within sources field")
|
||||
command.Flags().StringVar(&opts.SourceName, "source-name", "", "Name of the source from the list of sources of the app.")
|
||||
}
|
||||
@@ -276,6 +263,7 @@ func SetAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, ap
|
||||
MaxDuration: appOpts.retryBackoffMaxDuration.String(),
|
||||
Factor: ptr.To(appOpts.retryBackoffFactor),
|
||||
},
|
||||
Refresh: appOpts.retryRefresh,
|
||||
}
|
||||
case appOpts.retryLimit == 0:
|
||||
if spec.SyncPolicy.IsZero() {
|
||||
@@ -286,6 +274,14 @@ func SetAppSpecOptions(flags *pflag.FlagSet, spec *argoappv1.ApplicationSpec, ap
|
||||
default:
|
||||
log.Fatalf("Invalid sync-retry-limit [%d]", appOpts.retryLimit)
|
||||
}
|
||||
case "sync-retry-refresh":
|
||||
if spec.SyncPolicy == nil {
|
||||
spec.SyncPolicy = &argoappv1.SyncPolicy{}
|
||||
}
|
||||
if spec.SyncPolicy.Retry == nil {
|
||||
spec.SyncPolicy.Retry = &argoappv1.RetryStrategy{}
|
||||
}
|
||||
spec.SyncPolicy.Retry.Refresh = appOpts.retryRefresh
|
||||
}
|
||||
})
|
||||
if flags.Changed("auto-prune") {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
@@ -275,6 +274,13 @@ func Test_setAppSpecOptions(t *testing.T) {
|
||||
require.NoError(t, f.SetFlag("sync-retry-limit", "0"))
|
||||
assert.Nil(t, f.spec.SyncPolicy.Retry)
|
||||
})
|
||||
t.Run("RetryRefresh", func(t *testing.T) {
|
||||
require.NoError(t, f.SetFlag("sync-retry-refresh", "true"))
|
||||
assert.True(t, f.spec.SyncPolicy.Retry.Refresh)
|
||||
|
||||
require.NoError(t, f.SetFlag("sync-retry-refresh", "false"))
|
||||
assert.False(t, f.spec.SyncPolicy.Retry.Refresh)
|
||||
})
|
||||
t.Run("Kustomize", func(t *testing.T) {
|
||||
require.NoError(t, f.SetFlag("kustomize-replica", "my-deployment=2"))
|
||||
require.NoError(t, f.SetFlag("kustomize-replica", "my-statefulset=4"))
|
||||
@@ -573,27 +579,3 @@ func TestFilterResources(t *testing.T) {
|
||||
assert.Nil(t, filteredResources)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetAutoMaxProcs(t *testing.T) {
|
||||
t.Run("CLI mode ignores errors", func(t *testing.T) {
|
||||
logBuffer := &bytes.Buffer{}
|
||||
oldLogger := log.Default()
|
||||
log.SetOutput(logBuffer)
|
||||
defer log.SetOutput(oldLogger.Writer())
|
||||
|
||||
SetAutoMaxProcs(true)
|
||||
|
||||
assert.Empty(t, logBuffer.String(), "Expected no log output when isCLI is true")
|
||||
})
|
||||
|
||||
t.Run("Non-CLI mode logs error on failure", func(t *testing.T) {
|
||||
logBuffer := &bytes.Buffer{}
|
||||
oldLogger := log.Default()
|
||||
log.SetOutput(logBuffer)
|
||||
defer log.SetOutput(oldLogger.Writer())
|
||||
|
||||
SetAutoMaxProcs(false)
|
||||
|
||||
assert.NotContains(t, logBuffer.String(), "Error setting GOMAXPROCS", "Unexpected log output detected")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func NewConnection(address string) (*grpc.ClientConn, error) {
|
||||
}
|
||||
|
||||
dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
conn, err := grpc_util.BlockingDial(context.Background(), "unix", address, nil, dialOpts...)
|
||||
conn, err := grpc_util.BlockingNewClient(context.Background(), "unix", address, nil, dialOpts...)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to connect to config management plugin service with address %s", address)
|
||||
return nil, err
|
||||
|
||||
@@ -40,9 +40,7 @@ func NewConnection(address string) (*grpc.ClientConn, error) {
|
||||
var opts []grpc.DialOption
|
||||
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
|
||||
// TODO: switch to grpc.NewClient.
|
||||
//nolint:staticcheck
|
||||
conn, err := grpc.Dial(address, opts...)
|
||||
conn, err := grpc.NewClient(address, opts...)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to connect to commit service with address %s", address)
|
||||
return nil, err
|
||||
|
||||
95
commitserver/apiclient/mocks/Clientset.go
generated
95
commitserver/apiclient/mocks/Clientset.go
generated
@@ -1,101 +1,14 @@
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"github.com/argoproj/argo-cd/v3/commitserver/apiclient"
|
||||
"github.com/argoproj/argo-cd/v3/util/io"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
utilio "github.com/argoproj/argo-cd/v3/util/io"
|
||||
)
|
||||
|
||||
// NewClientset creates a new instance of Clientset. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewClientset(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *Clientset {
|
||||
mock := &Clientset{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// Clientset is an autogenerated mock type for the Clientset type
|
||||
type Clientset struct {
|
||||
mock.Mock
|
||||
CommitServiceClient apiclient.CommitServiceClient
|
||||
}
|
||||
|
||||
type Clientset_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *Clientset) EXPECT() *Clientset_Expecter {
|
||||
return &Clientset_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// NewCommitServerClient provides a mock function for the type Clientset
|
||||
func (_mock *Clientset) NewCommitServerClient() (io.Closer, apiclient.CommitServiceClient, error) {
|
||||
ret := _mock.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for NewCommitServerClient")
|
||||
}
|
||||
|
||||
var r0 io.Closer
|
||||
var r1 apiclient.CommitServiceClient
|
||||
var r2 error
|
||||
if returnFunc, ok := ret.Get(0).(func() (io.Closer, apiclient.CommitServiceClient, error)); ok {
|
||||
return returnFunc()
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func() io.Closer); ok {
|
||||
r0 = returnFunc()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(io.Closer)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func() apiclient.CommitServiceClient); ok {
|
||||
r1 = returnFunc()
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(apiclient.CommitServiceClient)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(2).(func() error); ok {
|
||||
r2 = returnFunc()
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
return r0, r1, r2
|
||||
}
|
||||
|
||||
// Clientset_NewCommitServerClient_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NewCommitServerClient'
|
||||
type Clientset_NewCommitServerClient_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// NewCommitServerClient is a helper method to define mock.On call
|
||||
func (_e *Clientset_Expecter) NewCommitServerClient() *Clientset_NewCommitServerClient_Call {
|
||||
return &Clientset_NewCommitServerClient_Call{Call: _e.mock.On("NewCommitServerClient")}
|
||||
}
|
||||
|
||||
func (_c *Clientset_NewCommitServerClient_Call) Run(run func()) *Clientset_NewCommitServerClient_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Clientset_NewCommitServerClient_Call) Return(closer io.Closer, commitServiceClient apiclient.CommitServiceClient, err error) *Clientset_NewCommitServerClient_Call {
|
||||
_c.Call.Return(closer, commitServiceClient, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Clientset_NewCommitServerClient_Call) RunAndReturn(run func() (io.Closer, apiclient.CommitServiceClient, error)) *Clientset_NewCommitServerClient_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
func (c *Clientset) NewCommitServerClient() (utilio.Closer, apiclient.CommitServiceClient, error) {
|
||||
return utilio.NopCloser, c.CommitServiceClient, nil
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/controller/hydrator"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/commitserver/apiclient"
|
||||
@@ -31,6 +33,43 @@ func NewService(gitCredsStore git.CredsStore, metricsServer *metrics.Server) *Se
|
||||
}
|
||||
}
|
||||
|
||||
type hydratorMetadataFile struct {
|
||||
RepoURL string `json:"repoURL,omitempty"`
|
||||
DrySHA string `json:"drySha,omitempty"`
|
||||
Commands []string `json:"commands,omitempty"`
|
||||
Author string `json:"author,omitempty"`
|
||||
Date string `json:"date,omitempty"`
|
||||
// Subject is the subject line of the DRY commit message, i.e. `git show --format=%s`.
|
||||
Subject string `json:"subject,omitempty"`
|
||||
// Body is the body of the DRY commit message, excluding the subject line, i.e. `git show --format=%b`.
|
||||
// Known Argocd- trailers with valid values are removed, but all other trailers are kept.
|
||||
Body string `json:"body,omitempty"`
|
||||
References []v1alpha1.RevisionReference `json:"references,omitempty"`
|
||||
}
|
||||
|
||||
// TODO: make this configurable via ConfigMap.
|
||||
var manifestHydrationReadmeTemplate = `# Manifest Hydration
|
||||
|
||||
To hydrate the manifests in this repository, run the following commands:
|
||||
|
||||
` + "```shell" + `
|
||||
git clone {{ .RepoURL }}
|
||||
# cd into the cloned directory
|
||||
git checkout {{ .DrySHA }}
|
||||
{{ range $command := .Commands -}}
|
||||
{{ $command }}
|
||||
{{ end -}}` + "```" + `
|
||||
{{ if .References -}}
|
||||
|
||||
## References
|
||||
|
||||
{{ range $ref := .References -}}
|
||||
{{ if $ref.Commit -}}
|
||||
* [{{ $ref.Commit.SHA | mustRegexFind "[0-9a-f]+" | trunc 7 }}]({{ $ref.Commit.RepoURL }}): {{ $ref.Commit.Subject }} ({{ $ref.Commit.Author }})
|
||||
{{ end -}}
|
||||
{{ end -}}
|
||||
{{ end -}}`
|
||||
|
||||
// CommitHydratedManifests handles a commit request. It clones the repository, checks out the sync branch, checks out
|
||||
// the target branch, clears the repository contents, writes the manifests to the repository, commits the changes, and
|
||||
// pushes the changes. It returns the hydrated revision SHA and an error if one occurred.
|
||||
@@ -118,10 +157,25 @@ func (s *Service) handleCommitRequest(logCtx *log.Entry, r *apiclient.CommitHydr
|
||||
return out, "", fmt.Errorf("failed to checkout target branch: %w", err)
|
||||
}
|
||||
|
||||
logCtx.Debug("Clearing repo contents")
|
||||
out, err = gitClient.RemoveContents()
|
||||
if err != nil {
|
||||
return out, "", fmt.Errorf("failed to clear repo: %w", err)
|
||||
logCtx.Debug("Clearing and preparing paths")
|
||||
var pathsToClear []string
|
||||
// range over the paths configured and skip those application
|
||||
// paths that are referencing to root path
|
||||
for _, p := range r.Paths {
|
||||
if hydrator.IsRootPath(p.Path) {
|
||||
// skip adding paths that are referencing root directory
|
||||
logCtx.Debugf("Path %s is referencing root directory, ignoring the path", p.Path)
|
||||
continue
|
||||
}
|
||||
pathsToClear = append(pathsToClear, p.Path)
|
||||
}
|
||||
|
||||
if len(pathsToClear) > 0 {
|
||||
logCtx.Debugf("Clearing paths: %v", pathsToClear)
|
||||
out, err := gitClient.RemoveContents(pathsToClear)
|
||||
if err != nil {
|
||||
return out, "", fmt.Errorf("failed to clear paths %v: %w", pathsToClear, err)
|
||||
}
|
||||
}
|
||||
|
||||
logCtx.Debug("Writing manifests")
|
||||
@@ -210,40 +264,3 @@ func (s *Service) initGitClient(logCtx *log.Entry, r *apiclient.CommitHydratedMa
|
||||
|
||||
return gitClient, dirPath, cleanupOrLog, nil
|
||||
}
|
||||
|
||||
type hydratorMetadataFile struct {
|
||||
RepoURL string `json:"repoURL,omitempty"`
|
||||
DrySHA string `json:"drySha,omitempty"`
|
||||
Commands []string `json:"commands,omitempty"`
|
||||
Author string `json:"author,omitempty"`
|
||||
Date string `json:"date,omitempty"`
|
||||
// Subject is the subject line of the DRY commit message, i.e. `git show --format=%s`.
|
||||
Subject string `json:"subject,omitempty"`
|
||||
// Body is the body of the DRY commit message, excluding the subject line, i.e. `git show --format=%b`.
|
||||
// Known Argocd- trailers with valid values are removed, but all other trailers are kept.
|
||||
Body string `json:"body,omitempty"`
|
||||
References []v1alpha1.RevisionReference `json:"references,omitempty"`
|
||||
}
|
||||
|
||||
// TODO: make this configurable via ConfigMap.
|
||||
var manifestHydrationReadmeTemplate = `# Manifest Hydration
|
||||
|
||||
To hydrate the manifests in this repository, run the following commands:
|
||||
|
||||
` + "```shell" + `
|
||||
git clone {{ .RepoURL }}
|
||||
# cd into the cloned directory
|
||||
git checkout {{ .DrySHA }}
|
||||
{{ range $command := .Commands -}}
|
||||
{{ $command }}
|
||||
{{ end -}}` + "```" + `
|
||||
{{ if .References -}}
|
||||
|
||||
## References
|
||||
|
||||
{{ range $ref := .References -}}
|
||||
{{ if $ref.Commit -}}
|
||||
* [{{ $ref.Commit.SHA | mustRegexFind "[0-9a-f]+" | trunc 7 }}]({{ $ref.Commit.RepoURL }}): {{ $ref.Commit.Subject }} ({{ $ref.Commit.Author }})
|
||||
{{ end -}}
|
||||
{{ end -}}
|
||||
{{ end -}}`
|
||||
|
||||
@@ -99,7 +99,6 @@ func Test_CommitHydratedManifests(t *testing.T) {
|
||||
mockGitClient.On("SetAuthor", "Argo CD", "argo-cd@example.com").Return("", nil).Once()
|
||||
mockGitClient.On("CheckoutOrOrphan", "env/test", false).Return("", nil).Once()
|
||||
mockGitClient.On("CheckoutOrNew", "main", "env/test", false).Return("", nil).Once()
|
||||
mockGitClient.On("RemoveContents").Return("", nil).Once()
|
||||
mockGitClient.On("CommitAndPush", "main", "test commit message").Return("", nil).Once()
|
||||
mockGitClient.On("CommitSHA").Return("it-worked!", nil).Once()
|
||||
mockRepoClientFactory.On("NewClient", mock.Anything, mock.Anything).Return(mockGitClient, nil).Once()
|
||||
@@ -109,6 +108,178 @@ func Test_CommitHydratedManifests(t *testing.T) {
|
||||
require.NotNil(t, resp)
|
||||
assert.Equal(t, "it-worked!", resp.HydratedSha)
|
||||
})
|
||||
|
||||
t.Run("root path with dot and blank - no directory removal", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service, mockRepoClientFactory := newServiceWithMocks(t)
|
||||
mockGitClient := gitmocks.NewClient(t)
|
||||
mockGitClient.On("Init").Return(nil).Once()
|
||||
mockGitClient.On("Fetch", mock.Anything).Return(nil).Once()
|
||||
mockGitClient.On("SetAuthor", "Argo CD", "argo-cd@example.com").Return("", nil).Once()
|
||||
mockGitClient.On("CheckoutOrOrphan", "env/test", false).Return("", nil).Once()
|
||||
mockGitClient.On("CheckoutOrNew", "main", "env/test", false).Return("", nil).Once()
|
||||
mockGitClient.On("CommitAndPush", "main", "test commit message").Return("", nil).Once()
|
||||
mockGitClient.On("CommitSHA").Return("root-and-blank-sha", nil).Once()
|
||||
mockRepoClientFactory.On("NewClient", mock.Anything, mock.Anything).Return(mockGitClient, nil).Once()
|
||||
|
||||
requestWithRootAndBlank := &apiclient.CommitHydratedManifestsRequest{
|
||||
Repo: &v1alpha1.Repository{
|
||||
Repo: "https://github.com/argoproj/argocd-example-apps.git",
|
||||
},
|
||||
TargetBranch: "main",
|
||||
SyncBranch: "env/test",
|
||||
CommitMessage: "test commit message",
|
||||
Paths: []*apiclient.PathDetails{
|
||||
{
|
||||
Path: ".",
|
||||
Manifests: []*apiclient.HydratedManifestDetails{
|
||||
{
|
||||
ManifestJSON: `{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"test-dot"}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "",
|
||||
Manifests: []*apiclient.HydratedManifestDetails{
|
||||
{
|
||||
ManifestJSON: `{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"test-blank"}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := service.CommitHydratedManifests(t.Context(), requestWithRootAndBlank)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
assert.Equal(t, "root-and-blank-sha", resp.HydratedSha)
|
||||
})
|
||||
|
||||
t.Run("subdirectory path - triggers directory removal", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service, mockRepoClientFactory := newServiceWithMocks(t)
|
||||
mockGitClient := gitmocks.NewClient(t)
|
||||
mockGitClient.On("Init").Return(nil).Once()
|
||||
mockGitClient.On("Fetch", mock.Anything).Return(nil).Once()
|
||||
mockGitClient.On("SetAuthor", "Argo CD", "argo-cd@example.com").Return("", nil).Once()
|
||||
mockGitClient.On("CheckoutOrOrphan", "env/test", false).Return("", nil).Once()
|
||||
mockGitClient.On("CheckoutOrNew", "main", "env/test", false).Return("", nil).Once()
|
||||
mockGitClient.On("RemoveContents", []string{"apps/staging"}).Return("", nil).Once()
|
||||
mockGitClient.On("CommitAndPush", "main", "test commit message").Return("", nil).Once()
|
||||
mockGitClient.On("CommitSHA").Return("subdir-path-sha", nil).Once()
|
||||
mockRepoClientFactory.On("NewClient", mock.Anything, mock.Anything).Return(mockGitClient, nil).Once()
|
||||
|
||||
requestWithSubdirPath := &apiclient.CommitHydratedManifestsRequest{
|
||||
Repo: &v1alpha1.Repository{
|
||||
Repo: "https://github.com/argoproj/argocd-example-apps.git",
|
||||
},
|
||||
TargetBranch: "main",
|
||||
SyncBranch: "env/test",
|
||||
CommitMessage: "test commit message",
|
||||
Paths: []*apiclient.PathDetails{
|
||||
{
|
||||
Path: "apps/staging", // subdirectory path
|
||||
Manifests: []*apiclient.HydratedManifestDetails{
|
||||
{
|
||||
ManifestJSON: `{"apiVersion":"v1","kind":"Deployment","metadata":{"name":"test-app"}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := service.CommitHydratedManifests(t.Context(), requestWithSubdirPath)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
assert.Equal(t, "subdir-path-sha", resp.HydratedSha)
|
||||
})
|
||||
|
||||
t.Run("mixed paths - root and subdirectory", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service, mockRepoClientFactory := newServiceWithMocks(t)
|
||||
mockGitClient := gitmocks.NewClient(t)
|
||||
mockGitClient.On("Init").Return(nil).Once()
|
||||
mockGitClient.On("Fetch", mock.Anything).Return(nil).Once()
|
||||
mockGitClient.On("SetAuthor", "Argo CD", "argo-cd@example.com").Return("", nil).Once()
|
||||
mockGitClient.On("CheckoutOrOrphan", "env/test", false).Return("", nil).Once()
|
||||
mockGitClient.On("CheckoutOrNew", "main", "env/test", false).Return("", nil).Once()
|
||||
mockGitClient.On("RemoveContents", []string{"apps/production", "apps/staging"}).Return("", nil).Once()
|
||||
mockGitClient.On("CommitAndPush", "main", "test commit message").Return("", nil).Once()
|
||||
mockGitClient.On("CommitSHA").Return("mixed-paths-sha", nil).Once()
|
||||
mockRepoClientFactory.On("NewClient", mock.Anything, mock.Anything).Return(mockGitClient, nil).Once()
|
||||
|
||||
requestWithMixedPaths := &apiclient.CommitHydratedManifestsRequest{
|
||||
Repo: &v1alpha1.Repository{
|
||||
Repo: "https://github.com/argoproj/argocd-example-apps.git",
|
||||
},
|
||||
TargetBranch: "main",
|
||||
SyncBranch: "env/test",
|
||||
CommitMessage: "test commit message",
|
||||
Paths: []*apiclient.PathDetails{
|
||||
{
|
||||
Path: ".", // root path - should NOT trigger removal
|
||||
Manifests: []*apiclient.HydratedManifestDetails{
|
||||
{
|
||||
ManifestJSON: `{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"global-config"}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "apps/production", // subdirectory path - SHOULD trigger removal
|
||||
Manifests: []*apiclient.HydratedManifestDetails{
|
||||
{
|
||||
ManifestJSON: `{"apiVersion":"v1","kind":"Deployment","metadata":{"name":"prod-app"}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "apps/staging", // another subdirectory path - SHOULD trigger removal
|
||||
Manifests: []*apiclient.HydratedManifestDetails{
|
||||
{
|
||||
ManifestJSON: `{"apiVersion":"v1","kind":"Deployment","metadata":{"name":"staging-app"}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := service.CommitHydratedManifests(t.Context(), requestWithMixedPaths)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
assert.Equal(t, "mixed-paths-sha", resp.HydratedSha)
|
||||
})
|
||||
|
||||
t.Run("empty paths array", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
service, mockRepoClientFactory := newServiceWithMocks(t)
|
||||
mockGitClient := gitmocks.NewClient(t)
|
||||
mockGitClient.On("Init").Return(nil).Once()
|
||||
mockGitClient.On("Fetch", mock.Anything).Return(nil).Once()
|
||||
mockGitClient.On("SetAuthor", "Argo CD", "argo-cd@example.com").Return("", nil).Once()
|
||||
mockGitClient.On("CheckoutOrOrphan", "env/test", false).Return("", nil).Once()
|
||||
mockGitClient.On("CheckoutOrNew", "main", "env/test", false).Return("", nil).Once()
|
||||
mockGitClient.On("CommitAndPush", "main", "test commit message").Return("", nil).Once()
|
||||
mockGitClient.On("CommitSHA").Return("it-worked!", nil).Once()
|
||||
mockRepoClientFactory.On("NewClient", mock.Anything, mock.Anything).Return(mockGitClient, nil).Once()
|
||||
|
||||
requestWithEmptyPaths := &apiclient.CommitHydratedManifestsRequest{
|
||||
Repo: &v1alpha1.Repository{
|
||||
Repo: "https://github.com/argoproj/argocd-example-apps.git",
|
||||
},
|
||||
TargetBranch: "main",
|
||||
SyncBranch: "env/test",
|
||||
CommitMessage: "test commit message",
|
||||
}
|
||||
|
||||
resp, err := service.CommitHydratedManifests(t.Context(), requestWithEmptyPaths)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
assert.Equal(t, "it-worked!", resp.HydratedSha)
|
||||
})
|
||||
}
|
||||
|
||||
func newServiceWithMocks(t *testing.T) (*Service, *mocks.RepoClientFactory) {
|
||||
|
||||
@@ -2,14 +2,10 @@ package commit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -19,14 +15,14 @@ import (
|
||||
"github.com/argoproj/argo-cd/v3/commitserver/apiclient"
|
||||
"github.com/argoproj/argo-cd/v3/common"
|
||||
appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v3/util/git"
|
||||
"github.com/argoproj/argo-cd/v3/util/hydrator"
|
||||
"github.com/argoproj/argo-cd/v3/util/io"
|
||||
)
|
||||
|
||||
var sprigFuncMap = sprig.GenericFuncMap() // a singleton for better performance
|
||||
|
||||
const gitAttributesContents = `*/README.md linguist-generated=true
|
||||
*/hydrator.metadata linguist-generated=true`
|
||||
const gitAttributesContents = `**/README.md linguist-generated=true
|
||||
**/hydrator.metadata linguist-generated=true`
|
||||
|
||||
func init() {
|
||||
// Avoid allowing the user to learn things about the environment.
|
||||
@@ -38,25 +34,13 @@ func init() {
|
||||
// WriteForPaths writes the manifests, hydrator.metadata, and README.md files for each path in the provided paths. It
|
||||
// also writes a root-level hydrator.metadata file containing the repo URL and dry SHA.
|
||||
func WriteForPaths(root *os.Root, repoUrl, drySha string, dryCommitMetadata *appv1.RevisionMetadata, paths []*apiclient.PathDetails) error { //nolint:revive //FIXME(var-naming)
|
||||
author := ""
|
||||
message := ""
|
||||
date := ""
|
||||
var references []appv1.RevisionReference
|
||||
if dryCommitMetadata != nil {
|
||||
author = dryCommitMetadata.Author
|
||||
message = dryCommitMetadata.Message
|
||||
if dryCommitMetadata.Date != nil {
|
||||
date = dryCommitMetadata.Date.Format(time.RFC3339)
|
||||
}
|
||||
references = dryCommitMetadata.References
|
||||
hydratorMetadata, err := hydrator.GetCommitMetadata(repoUrl, drySha, dryCommitMetadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to retrieve hydrator metadata: %w", err)
|
||||
}
|
||||
|
||||
subject, body, _ := strings.Cut(message, "\n\n")
|
||||
|
||||
_, bodyMinusTrailers := git.GetReferences(log.WithFields(log.Fields{"repo": repoUrl, "revision": drySha}), body)
|
||||
|
||||
// Write the top-level readme.
|
||||
err := writeMetadata(root, "", hydratorMetadataFile{DrySHA: drySha, RepoURL: repoUrl, Author: author, Subject: subject, Body: bodyMinusTrailers, Date: date, References: references})
|
||||
err = writeMetadata(root, "", hydratorMetadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write top-level hydrator metadata: %w", err)
|
||||
}
|
||||
@@ -73,9 +57,12 @@ func WriteForPaths(root *os.Root, repoUrl, drySha string, dryCommitMetadata *app
|
||||
hydratePath = ""
|
||||
}
|
||||
|
||||
err = mkdirAll(root, hydratePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create path: %w", err)
|
||||
// Only create directory if path is not empty (root directory case)
|
||||
if hydratePath != "" {
|
||||
err = root.MkdirAll(hydratePath, 0o755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create path: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Write the manifests
|
||||
@@ -85,7 +72,7 @@ func WriteForPaths(root *os.Root, repoUrl, drySha string, dryCommitMetadata *app
|
||||
}
|
||||
|
||||
// Write hydrator.metadata containing information about the hydration process.
|
||||
hydratorMetadata := hydratorMetadataFile{
|
||||
hydratorMetadata := hydrator.HydratorCommitMetadata{
|
||||
Commands: p.Commands,
|
||||
DrySHA: drySha,
|
||||
RepoURL: repoUrl,
|
||||
@@ -105,7 +92,7 @@ func WriteForPaths(root *os.Root, repoUrl, drySha string, dryCommitMetadata *app
|
||||
}
|
||||
|
||||
// writeMetadata writes the metadata to the hydrator.metadata file.
|
||||
func writeMetadata(root *os.Root, dirPath string, metadata hydratorMetadataFile) error {
|
||||
func writeMetadata(root *os.Root, dirPath string, metadata hydrator.HydratorCommitMetadata) error {
|
||||
hydratorMetadataPath := filepath.Join(dirPath, "hydrator.metadata")
|
||||
f, err := root.Create(hydratorMetadataPath)
|
||||
if err != nil {
|
||||
@@ -124,7 +111,7 @@ func writeMetadata(root *os.Root, dirPath string, metadata hydratorMetadataFile)
|
||||
}
|
||||
|
||||
// writeReadme writes the readme to the README.md file.
|
||||
func writeReadme(root *os.Root, dirPath string, metadata hydratorMetadataFile) error {
|
||||
func writeReadme(root *os.Root, dirPath string, metadata hydrator.HydratorCommitMetadata) error {
|
||||
readmeTemplate, err := template.New("readme").Funcs(sprigFuncMap).Parse(manifestHydrationReadmeTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse readme template: %w", err)
|
||||
@@ -212,25 +199,3 @@ func writeManifests(root *os.Root, dirPath string, manifests []*apiclient.Hydrat
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// mkdirAll creates the directory and all its parents if they do not exist. It returns an error if the directory
|
||||
// cannot be.
|
||||
func mkdirAll(root *os.Root, dirPath string) error {
|
||||
parts := strings.Split(dirPath, string(os.PathSeparator))
|
||||
builtPath := ""
|
||||
for _, part := range parts {
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
builtPath = filepath.Join(builtPath, part)
|
||||
err := root.Mkdir(builtPath, os.ModePerm)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrExist) {
|
||||
log.WithError(err).Warnf("path %s already exists, skipping", dirPath)
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("failed to create path: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -18,6 +20,7 @@ import (
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/commitserver/apiclient"
|
||||
appsv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v3/util/hydrator"
|
||||
)
|
||||
|
||||
// tempRoot creates a temporary directory and returns an os.Root object for it.
|
||||
@@ -144,7 +147,7 @@ Argocd-reference-commit-sha: abc123
|
||||
func TestWriteMetadata(t *testing.T) {
|
||||
root := tempRoot(t)
|
||||
|
||||
metadata := hydratorMetadataFile{
|
||||
metadata := hydrator.HydratorCommitMetadata{
|
||||
RepoURL: "https://github.com/example/repo",
|
||||
DrySHA: "abc123",
|
||||
}
|
||||
@@ -156,7 +159,7 @@ func TestWriteMetadata(t *testing.T) {
|
||||
metadataBytes, err := os.ReadFile(metadataPath)
|
||||
require.NoError(t, err)
|
||||
|
||||
var readMetadata hydratorMetadataFile
|
||||
var readMetadata hydrator.HydratorCommitMetadata
|
||||
err = json.Unmarshal(metadataBytes, &readMetadata)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, metadata, readMetadata)
|
||||
@@ -171,7 +174,7 @@ func TestWriteReadme(t *testing.T) {
|
||||
hash := sha256.Sum256(randomData)
|
||||
sha := hex.EncodeToString(hash[:])
|
||||
|
||||
metadata := hydratorMetadataFile{
|
||||
metadata := hydrator.HydratorCommitMetadata{
|
||||
RepoURL: "https://github.com/example/repo",
|
||||
DrySHA: "abc123",
|
||||
References: []appsv1.RevisionReference{
|
||||
@@ -233,6 +236,74 @@ func TestWriteGitAttributes(t *testing.T) {
|
||||
gitAttributesPath := filepath.Join(root.Name(), ".gitattributes")
|
||||
gitAttributesBytes, err := os.ReadFile(gitAttributesPath)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(gitAttributesBytes), "*/README.md linguist-generated=true")
|
||||
assert.Contains(t, string(gitAttributesBytes), "*/hydrator.metadata linguist-generated=true")
|
||||
assert.Contains(t, string(gitAttributesBytes), "README.md linguist-generated=true")
|
||||
assert.Contains(t, string(gitAttributesBytes), "hydrator.metadata linguist-generated=true")
|
||||
}
|
||||
|
||||
func TestWriteGitAttributes_MatchesAllDepths(t *testing.T) {
|
||||
root := tempRoot(t)
|
||||
|
||||
err := writeGitAttributes(root)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The gitattributes pattern needs to match files at all depths:
|
||||
// - hydrator.metadata (root level)
|
||||
// - path1/hydrator.metadata (one level deep)
|
||||
// - path1/nested/deep/hydrator.metadata (multiple levels deep)
|
||||
// Same for README.md files
|
||||
//
|
||||
// The pattern "**/hydrator.metadata" matches at any depth including root
|
||||
// The pattern "*/hydrator.metadata" only matches exactly one directory level deep
|
||||
|
||||
// Test actual Git behavior using git check-attr
|
||||
// Initialize a git repo
|
||||
ctx := t.Context()
|
||||
repoPath := root.Name()
|
||||
cmd := exec.CommandContext(ctx, "git", "init")
|
||||
cmd.Dir = repoPath
|
||||
output, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, "Failed to init git repo: %s", string(output))
|
||||
|
||||
// Test files at different depths
|
||||
testCases := []struct {
|
||||
path string
|
||||
shouldMatch bool
|
||||
description string
|
||||
}{
|
||||
{"hydrator.metadata", true, "root level hydrator.metadata"},
|
||||
{"README.md", true, "root level README.md"},
|
||||
{"path1/hydrator.metadata", true, "one level deep hydrator.metadata"},
|
||||
{"path1/README.md", true, "one level deep README.md"},
|
||||
{"path1/nested/hydrator.metadata", true, "two levels deep hydrator.metadata"},
|
||||
{"path1/nested/README.md", true, "two levels deep README.md"},
|
||||
{"path1/nested/deep/hydrator.metadata", true, "three levels deep hydrator.metadata"},
|
||||
{"path1/nested/deep/README.md", true, "three levels deep README.md"},
|
||||
{"manifest.yaml", false, "manifest.yaml should not match"},
|
||||
{"path1/manifest.yaml", false, "nested manifest.yaml should not match"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
// Use git check-attr to verify if linguist-generated attribute is set
|
||||
cmd := exec.CommandContext(ctx, "git", "check-attr", "linguist-generated", tc.path)
|
||||
cmd.Dir = repoPath
|
||||
output, err := cmd.CombinedOutput()
|
||||
require.NoError(t, err, "Failed to run git check-attr: %s", string(output))
|
||||
|
||||
// Output format: <path>: <attribute>: <value>
|
||||
// Example: "hydrator.metadata: linguist-generated: true"
|
||||
outputStr := strings.TrimSpace(string(output))
|
||||
|
||||
if tc.shouldMatch {
|
||||
expectedOutput := tc.path + ": linguist-generated: true"
|
||||
assert.Equal(t, expectedOutput, outputStr,
|
||||
"File %s should have linguist-generated=true attribute", tc.path)
|
||||
} else {
|
||||
// Attribute should be unspecified
|
||||
expectedOutput := tc.path + ": linguist-generated: unspecified"
|
||||
assert.Equal(t, expectedOutput, outputStr,
|
||||
"File %s should not have linguist-generated=true attribute", tc.path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,12 @@ const (
|
||||
PluginConfigFileName = "plugin.yaml"
|
||||
)
|
||||
|
||||
// consts for podrequests metrics in cache/info
|
||||
const (
|
||||
PodRequestsCPU = "cpu"
|
||||
PodRequestsMEM = "memory"
|
||||
)
|
||||
|
||||
// Argo CD application related constants
|
||||
const (
|
||||
|
||||
@@ -186,6 +192,8 @@ const (
|
||||
LabelValueSecretTypeRepoCreds = "repo-creds"
|
||||
// LabelValueSecretTypeRepositoryWrite indicates a secret type of repository credentials for writing
|
||||
LabelValueSecretTypeRepositoryWrite = "repository-write"
|
||||
// LabelValueSecretTypeRepoCredsWrite indicates a secret type of repository credentials for writing for templating
|
||||
LabelValueSecretTypeRepoCredsWrite = "repo-write-creds"
|
||||
// LabelValueSecretTypeSCMCreds indicates a secret type of SCM credentials
|
||||
LabelValueSecretTypeSCMCreds = "scm-creds"
|
||||
|
||||
|
||||
@@ -603,6 +603,9 @@ func (ctrl *ApplicationController) getResourceTree(destCluster *appv1.Cluster, a
|
||||
Group: managedResource.Group,
|
||||
Namespace: managedResource.Namespace,
|
||||
},
|
||||
Health: &appv1.HealthStatus{
|
||||
Status: health.HealthStatusMissing,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
managedResourcesKeys = append(managedResourcesKeys, kube.GetResourceKey(live))
|
||||
@@ -1203,7 +1206,7 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic
|
||||
if err != nil {
|
||||
logCtx.Warnf("Unable to get destination cluster: %v", err)
|
||||
app.UnSetCascadedDeletion()
|
||||
app.UnSetPostDeleteFinalizer()
|
||||
app.UnSetPostDeleteFinalizerAll()
|
||||
if err := ctrl.updateFinalizers(app); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1392,12 +1395,19 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli
|
||||
logCtx = logCtx.WithField("time_ms", time.Since(ts.StartTime).Milliseconds())
|
||||
logCtx.Debug("Finished processing requested app operation")
|
||||
}()
|
||||
terminating := false
|
||||
terminatingCause := ""
|
||||
if isOperationInProgress(app) {
|
||||
state = app.Status.OperationState.DeepCopy()
|
||||
terminating = state.Phase == synccommon.OperationTerminating
|
||||
switch {
|
||||
case state.FinishedAt != nil && !terminating:
|
||||
case state.Phase == synccommon.OperationTerminating:
|
||||
logCtx.Infof("Resuming in-progress operation. phase: %s, message: %s", state.Phase, state.Message)
|
||||
case ctrl.syncTimeout != time.Duration(0) && time.Now().After(state.StartedAt.Add(ctrl.syncTimeout)):
|
||||
state.Phase = synccommon.OperationTerminating
|
||||
state.Message = "operation is terminating due to timeout"
|
||||
terminatingCause = "controller sync timeout"
|
||||
ctrl.setOperationState(app, state)
|
||||
logCtx.Infof("Terminating in-progress operation due to timeout. Started at: %v, timeout: %v", state.StartedAt, ctrl.syncTimeout)
|
||||
case state.Phase == synccommon.OperationRunning && state.FinishedAt != nil:
|
||||
// Failed operation with retry strategy might be in-progress and has completion time
|
||||
retryAt, err := app.Status.OperationState.Operation.Retry.NextRetryAt(state.FinishedAt.Time, state.RetryCount)
|
||||
if err != nil {
|
||||
@@ -1407,21 +1417,28 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli
|
||||
return
|
||||
}
|
||||
retryAfter := time.Until(retryAt)
|
||||
|
||||
if retryAfter > 0 {
|
||||
logCtx.Infof("Skipping retrying in-progress operation. Attempting again at: %s", retryAt.Format(time.RFC3339))
|
||||
ctrl.requestAppRefresh(app.QualifiedName(), CompareWithLatest.Pointer(), &retryAfter)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove the desired revisions if the sync failed and we are retrying. The latest revision from the source will be used.
|
||||
extraMsg := ""
|
||||
if state.Operation.Retry.Refresh {
|
||||
extraMsg += " with latest revisions"
|
||||
state.Operation.Sync.Revision = ""
|
||||
state.Operation.Sync.Revisions = nil
|
||||
}
|
||||
|
||||
// Get rid of sync results and null out previous operation completion time
|
||||
// This will start the retry attempt
|
||||
state.Message = fmt.Sprintf("Retrying operation%s. Attempt #%d", extraMsg, state.RetryCount)
|
||||
state.FinishedAt = nil
|
||||
state.SyncResult = nil
|
||||
ctrl.setOperationState(app, state)
|
||||
case ctrl.syncTimeout != time.Duration(0) && time.Now().After(state.StartedAt.Add(ctrl.syncTimeout)) && !terminating:
|
||||
state.Phase = synccommon.OperationTerminating
|
||||
state.Message = "operation is terminating due to timeout"
|
||||
ctrl.setOperationState(app, state)
|
||||
logCtx.Infof("Terminating in-progress operation due to timeout. Started at: %v, timeout: %v", state.StartedAt, ctrl.syncTimeout)
|
||||
logCtx.Infof("Retrying operation%s. Attempt #%d", extraMsg, state.RetryCount)
|
||||
default:
|
||||
logCtx.Infof("Resuming in-progress operation. phase: %s, message: %s", state.Phase, state.Message)
|
||||
}
|
||||
@@ -1436,6 +1453,7 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli
|
||||
}
|
||||
ts.AddCheckpoint("initial_operation_stage_ms")
|
||||
|
||||
terminating := state.Phase == synccommon.OperationTerminating
|
||||
project, err := ctrl.getAppProj(app)
|
||||
if err == nil {
|
||||
// Start or resume the sync
|
||||
@@ -1463,17 +1481,24 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli
|
||||
case synccommon.OperationFailed, synccommon.OperationError:
|
||||
if !terminating && (state.RetryCount < state.Operation.Retry.Limit || state.Operation.Retry.Limit < 0) {
|
||||
now := metav1.Now()
|
||||
state.FinishedAt = &now
|
||||
if retryAt, err := state.Operation.Retry.NextRetryAt(now.Time, state.RetryCount); err != nil {
|
||||
state.Phase = synccommon.OperationError
|
||||
state.Message = fmt.Sprintf("%s (failed to retry: %v)", state.Message, err)
|
||||
} else {
|
||||
// Set FinishedAt explicitly on a Running phase. This is a unique condition that will allow this
|
||||
// function to perform a retry the next time the operation is processed.
|
||||
state.Phase = synccommon.OperationRunning
|
||||
state.FinishedAt = &now
|
||||
state.RetryCount++
|
||||
state.Message = fmt.Sprintf("%s due to application controller sync timeout. Retrying attempt #%d at %s.", state.Message, state.RetryCount, retryAt.Format(time.Kitchen))
|
||||
state.Message = fmt.Sprintf("%s. Retrying attempt #%d at %s.", state.Message, state.RetryCount, retryAt.Format(time.Kitchen))
|
||||
}
|
||||
} else {
|
||||
if terminating && terminatingCause != "" {
|
||||
state.Message = fmt.Sprintf("%s, triggered by %s", state.Message, terminatingCause)
|
||||
}
|
||||
if state.RetryCount > 0 {
|
||||
state.Message = fmt.Sprintf("%s (retried %d times).", state.Message, state.RetryCount)
|
||||
}
|
||||
} else if state.RetryCount > 0 {
|
||||
state.Message = fmt.Sprintf("%s (retried %d times).", state.Message, state.RetryCount)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1483,8 +1508,18 @@ func (ctrl *ApplicationController) processRequestedAppOperation(app *appv1.Appli
|
||||
// if we just completed an operation, force a refresh so that UI will report up-to-date
|
||||
// sync/health information
|
||||
if _, err := cache.MetaNamespaceKeyFunc(app); err == nil {
|
||||
// force app refresh with using CompareWithLatest comparison type and trigger app reconciliation loop
|
||||
ctrl.requestAppRefresh(app.QualifiedName(), CompareWithLatestForceResolve.Pointer(), nil)
|
||||
var compareWith CompareWith
|
||||
if state.Operation.InitiatedBy.Automated {
|
||||
// Do not force revision resolution on automated operations because
|
||||
// this would cause excessive Ls-Remote requests on monorepo commits
|
||||
compareWith = CompareWithLatest
|
||||
} else {
|
||||
// Force app refresh with using most recent resolved revision after sync,
|
||||
// so UI won't show a just synced application being out of sync if it was
|
||||
// synced after commit but before app. refresh (see #18153)
|
||||
compareWith = CompareWithLatestForceResolve
|
||||
}
|
||||
ctrl.requestAppRefresh(app.QualifiedName(), compareWith.Pointer(), nil)
|
||||
} else {
|
||||
logCtx.Warnf("Fails to requeue application: %v", err)
|
||||
}
|
||||
@@ -1760,7 +1795,7 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
|
||||
logCtx = logCtx.WithField(k, v.Milliseconds())
|
||||
}
|
||||
|
||||
ctrl.normalizeApplication(origApp, app)
|
||||
ctrl.normalizeApplication(app)
|
||||
ts.AddCheckpoint("normalize_application_ms")
|
||||
|
||||
tree, err := ctrl.setAppManagedResources(destCluster, app, compareResult)
|
||||
@@ -1853,7 +1888,7 @@ func (ctrl *ApplicationController) processAppHydrateQueueItem() (processNext boo
|
||||
return
|
||||
}
|
||||
|
||||
ctrl.hydrator.ProcessAppHydrateQueueItem(origApp)
|
||||
ctrl.hydrator.ProcessAppHydrateQueueItem(origApp.DeepCopy())
|
||||
|
||||
log.WithFields(applog.GetAppLogFields(origApp)).Debug("Successfully processed app hydrate queue item")
|
||||
return
|
||||
@@ -1980,7 +2015,8 @@ func (ctrl *ApplicationController) refreshAppConditions(app *appv1.Application)
|
||||
}
|
||||
|
||||
// normalizeApplication normalizes an application.spec and additionally persists updates if it changed
|
||||
func (ctrl *ApplicationController) normalizeApplication(orig, app *appv1.Application) {
|
||||
func (ctrl *ApplicationController) normalizeApplication(app *appv1.Application) {
|
||||
orig := app.DeepCopy()
|
||||
app.Spec = *argo.NormalizeApplicationSpec(&app.Spec)
|
||||
logCtx := log.WithFields(applog.GetAppLogFields(app))
|
||||
|
||||
@@ -2112,16 +2148,20 @@ func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus *
|
||||
}
|
||||
}
|
||||
|
||||
source := ptr.To(app.Spec.GetSource())
|
||||
desiredRevisions := []string{syncStatus.Revision}
|
||||
if app.Spec.HasMultipleSources() {
|
||||
source = nil
|
||||
desiredRevisions = syncStatus.Revisions
|
||||
}
|
||||
|
||||
op := appv1.Operation{
|
||||
Sync: &appv1.SyncOperation{
|
||||
Source: source,
|
||||
Revision: syncStatus.Revision,
|
||||
Prune: app.Spec.SyncPolicy.Automated.Prune,
|
||||
SyncOptions: app.Spec.SyncPolicy.SyncOptions,
|
||||
Sources: app.Spec.Sources,
|
||||
Revisions: syncStatus.Revisions,
|
||||
},
|
||||
InitiatedBy: appv1.OperationInitiator{Automated: true},
|
||||
|
||||
@@ -1373,6 +1373,9 @@ func TestGetResourceTree_HasOrphanedResources(t *testing.T) {
|
||||
|
||||
managedDeploy := v1alpha1.ResourceNode{
|
||||
ResourceRef: v1alpha1.ResourceRef{Group: "apps", Kind: "Deployment", Namespace: "default", Name: "nginx-deployment", Version: "v1"},
|
||||
Health: &v1alpha1.HealthStatus{
|
||||
Status: health.HealthStatusMissing,
|
||||
},
|
||||
}
|
||||
orphanedDeploy1 := v1alpha1.ResourceNode{
|
||||
ResourceRef: v1alpha1.ResourceRef{Group: "apps", Kind: "Deployment", Namespace: "default", Name: "deploy1"},
|
||||
@@ -2093,7 +2096,9 @@ func TestProcessRequestedAppOperation_FailedNoRetries(t *testing.T) {
|
||||
ctrl.processRequestedAppOperation(app)
|
||||
|
||||
phase, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "phase")
|
||||
message, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "message")
|
||||
assert.Equal(t, string(synccommon.OperationError), phase)
|
||||
assert.Equal(t, "Failed to load application project: error getting app project \"default\": appproject.argoproj.io \"default\" not found", message)
|
||||
}
|
||||
|
||||
func TestProcessRequestedAppOperation_InvalidDestination(t *testing.T) {
|
||||
@@ -2122,8 +2127,8 @@ func TestProcessRequestedAppOperation_InvalidDestination(t *testing.T) {
|
||||
ctrl.processRequestedAppOperation(app)
|
||||
|
||||
phase, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "phase")
|
||||
assert.Equal(t, string(synccommon.OperationError), phase)
|
||||
message, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "message")
|
||||
assert.Equal(t, string(synccommon.OperationError), phase)
|
||||
assert.Contains(t, message, "application destination can't have both name and server defined: another-cluster https://localhost:6443")
|
||||
}
|
||||
|
||||
@@ -2147,20 +2152,24 @@ func TestProcessRequestedAppOperation_FailedHasRetries(t *testing.T) {
|
||||
ctrl.processRequestedAppOperation(app)
|
||||
|
||||
phase, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "phase")
|
||||
assert.Equal(t, string(synccommon.OperationRunning), phase)
|
||||
message, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "message")
|
||||
assert.Contains(t, message, "due to application controller sync timeout. Retrying attempt #1")
|
||||
retryCount, _, _ := unstructured.NestedFloat64(receivedPatch, "status", "operationState", "retryCount")
|
||||
assert.Equal(t, string(synccommon.OperationRunning), phase)
|
||||
assert.Contains(t, message, "Failed to load application project: error getting app project \"invalid-project\": appproject.argoproj.io \"invalid-project\" not found. Retrying attempt #1")
|
||||
assert.InEpsilon(t, float64(1), retryCount, 0.0001)
|
||||
}
|
||||
|
||||
func TestProcessRequestedAppOperation_RunningPreviouslyFailed(t *testing.T) {
|
||||
failedAttemptFinisedAt := time.Now().Add(-time.Minute * 5)
|
||||
app := newFakeApp()
|
||||
app.Operation = &v1alpha1.Operation{
|
||||
Sync: &v1alpha1.SyncOperation{},
|
||||
Retry: v1alpha1.RetryStrategy{Limit: 1},
|
||||
}
|
||||
app.Status.OperationState.Operation = *app.Operation
|
||||
app.Status.OperationState.Phase = synccommon.OperationRunning
|
||||
app.Status.OperationState.RetryCount = 1
|
||||
app.Status.OperationState.FinishedAt = &metav1.Time{Time: failedAttemptFinisedAt}
|
||||
app.Status.OperationState.SyncResult.Resources = []*v1alpha1.ResourceResult{{
|
||||
Name: "guestbook",
|
||||
Kind: "Deployment",
|
||||
@@ -2190,7 +2199,58 @@ func TestProcessRequestedAppOperation_RunningPreviouslyFailed(t *testing.T) {
|
||||
ctrl.processRequestedAppOperation(app)
|
||||
|
||||
phase, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "phase")
|
||||
message, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "message")
|
||||
finishedAtStr, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "finishedAt")
|
||||
finishedAt, err := time.Parse(time.RFC3339, finishedAtStr)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, string(synccommon.OperationSucceeded), phase)
|
||||
assert.Equal(t, "successfully synced (no more tasks)", message)
|
||||
assert.Truef(t, finishedAt.After(failedAttemptFinisedAt), "finishedAt was expected to be updated. The retry was not performed.")
|
||||
}
|
||||
|
||||
func TestProcessRequestedAppOperation_RunningPreviouslyFailedBackoff(t *testing.T) {
|
||||
failedAttemptFinisedAt := time.Now().Add(-time.Second)
|
||||
app := newFakeApp()
|
||||
app.Operation = &v1alpha1.Operation{
|
||||
Sync: &v1alpha1.SyncOperation{},
|
||||
Retry: v1alpha1.RetryStrategy{
|
||||
Limit: 1,
|
||||
Backoff: &v1alpha1.Backoff{
|
||||
Duration: "1h",
|
||||
Factor: ptr.To(int64(100)),
|
||||
MaxDuration: "1h",
|
||||
},
|
||||
},
|
||||
}
|
||||
app.Status.OperationState.Operation = *app.Operation
|
||||
app.Status.OperationState.Phase = synccommon.OperationRunning
|
||||
app.Status.OperationState.Message = "pending retry"
|
||||
app.Status.OperationState.RetryCount = 1
|
||||
app.Status.OperationState.FinishedAt = &metav1.Time{Time: failedAttemptFinisedAt}
|
||||
app.Status.OperationState.SyncResult.Resources = []*v1alpha1.ResourceResult{{
|
||||
Name: "guestbook",
|
||||
Kind: "Deployment",
|
||||
Group: "apps",
|
||||
Status: synccommon.ResultCodeSyncFailed,
|
||||
}}
|
||||
|
||||
data := &fakeData{
|
||||
apps: []runtime.Object{app, &defaultProj},
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(data, nil)
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
fakeAppCs.PrependReactor("patch", "*", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
require.FailNow(t, "A patch should not have been called if the backoff has not passed")
|
||||
return true, &v1alpha1.Application{}, nil
|
||||
})
|
||||
|
||||
ctrl.processRequestedAppOperation(app)
|
||||
}
|
||||
|
||||
func TestProcessRequestedAppOperation_HasRetriesTerminated(t *testing.T) {
|
||||
@@ -2199,6 +2259,7 @@ func TestProcessRequestedAppOperation_HasRetriesTerminated(t *testing.T) {
|
||||
Sync: &v1alpha1.SyncOperation{},
|
||||
Retry: v1alpha1.RetryStrategy{Limit: 10},
|
||||
}
|
||||
app.Status.OperationState.Operation = *app.Operation
|
||||
app.Status.OperationState.Phase = synccommon.OperationTerminating
|
||||
|
||||
data := &fakeData{
|
||||
@@ -2223,7 +2284,9 @@ func TestProcessRequestedAppOperation_HasRetriesTerminated(t *testing.T) {
|
||||
ctrl.processRequestedAppOperation(app)
|
||||
|
||||
phase, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "phase")
|
||||
message, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "message")
|
||||
assert.Equal(t, string(synccommon.OperationFailed), phase)
|
||||
assert.Equal(t, "Operation terminated", message)
|
||||
}
|
||||
|
||||
func TestProcessRequestedAppOperation_Successful(t *testing.T) {
|
||||
@@ -2250,12 +2313,126 @@ func TestProcessRequestedAppOperation_Successful(t *testing.T) {
|
||||
ctrl.processRequestedAppOperation(app)
|
||||
|
||||
phase, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "phase")
|
||||
message, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "message")
|
||||
assert.Equal(t, string(synccommon.OperationSucceeded), phase)
|
||||
assert.Equal(t, "successfully synced (no more tasks)", message)
|
||||
ok, level := ctrl.isRefreshRequested(ctrl.toAppKey(app.Name))
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, CompareWithLatestForceResolve, level)
|
||||
}
|
||||
|
||||
func TestProcessRequestedAppAutomatedOperation_Successful(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Spec.Project = "default"
|
||||
app.Operation = &v1alpha1.Operation{
|
||||
Sync: &v1alpha1.SyncOperation{},
|
||||
InitiatedBy: v1alpha1.OperationInitiator{
|
||||
Automated: true,
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(&fakeData{
|
||||
apps: []runtime.Object{app, &defaultProj},
|
||||
manifestResponses: []*apiclient.ManifestResponse{{
|
||||
Manifests: []string{},
|
||||
}},
|
||||
}, nil)
|
||||
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
|
||||
receivedPatch := map[string]any{}
|
||||
fakeAppCs.PrependReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
if patchAction, ok := action.(kubetesting.PatchAction); ok {
|
||||
require.NoError(t, json.Unmarshal(patchAction.GetPatch(), &receivedPatch))
|
||||
}
|
||||
return true, &v1alpha1.Application{}, nil
|
||||
})
|
||||
|
||||
ctrl.processRequestedAppOperation(app)
|
||||
|
||||
phase, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "phase")
|
||||
message, _, _ := unstructured.NestedString(receivedPatch, "status", "operationState", "message")
|
||||
assert.Equal(t, string(synccommon.OperationSucceeded), phase)
|
||||
assert.Equal(t, "successfully synced (no more tasks)", message)
|
||||
ok, level := ctrl.isRefreshRequested(ctrl.toAppKey(app.Name))
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, CompareWithLatest, level)
|
||||
}
|
||||
|
||||
func TestProcessRequestedAppOperation_SyncTimeout(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
startedSince time.Duration
|
||||
syncTimeout time.Duration
|
||||
retryAttempt int
|
||||
currentPhase synccommon.OperationPhase
|
||||
expectedPhase synccommon.OperationPhase
|
||||
expectedMessage string
|
||||
}{{
|
||||
name: "Continue when running operation has not exceeded timeout",
|
||||
syncTimeout: time.Minute,
|
||||
startedSince: 30 * time.Second,
|
||||
currentPhase: synccommon.OperationRunning,
|
||||
expectedPhase: synccommon.OperationSucceeded,
|
||||
expectedMessage: "successfully synced (no more tasks)",
|
||||
}, {
|
||||
name: "Continue when terminating operation has exceeded timeout",
|
||||
syncTimeout: time.Minute,
|
||||
startedSince: 2 * time.Minute,
|
||||
currentPhase: synccommon.OperationTerminating,
|
||||
expectedPhase: synccommon.OperationFailed,
|
||||
expectedMessage: "Operation terminated",
|
||||
}, {
|
||||
name: "Terminate when running operation exceeded timeout",
|
||||
syncTimeout: time.Minute,
|
||||
startedSince: 2 * time.Minute,
|
||||
currentPhase: synccommon.OperationRunning,
|
||||
expectedPhase: synccommon.OperationFailed,
|
||||
expectedMessage: "Operation terminated, triggered by controller sync timeout",
|
||||
}, {
|
||||
name: "Terminate when retried operation exceeded timeout",
|
||||
syncTimeout: time.Minute,
|
||||
startedSince: 15 * time.Minute,
|
||||
currentPhase: synccommon.OperationRunning,
|
||||
retryAttempt: 1,
|
||||
expectedPhase: synccommon.OperationFailed,
|
||||
expectedMessage: "Operation terminated, triggered by controller sync timeout (retried 1 times).",
|
||||
}}
|
||||
for i := range testCases {
|
||||
tc := testCases[i]
|
||||
t.Run(fmt.Sprintf("case %d: %s", i, tc.name), func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Spec.Project = "default"
|
||||
app.Operation = &v1alpha1.Operation{
|
||||
Sync: &v1alpha1.SyncOperation{
|
||||
Revision: "HEAD",
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(&fakeData{
|
||||
apps: []runtime.Object{app, &defaultProj},
|
||||
manifestResponses: []*apiclient.ManifestResponse{{
|
||||
Manifests: []string{},
|
||||
}},
|
||||
}, nil)
|
||||
|
||||
ctrl.syncTimeout = tc.syncTimeout
|
||||
app.Status.OperationState = &v1alpha1.OperationState{
|
||||
Operation: *app.Operation,
|
||||
Phase: tc.currentPhase,
|
||||
StartedAt: metav1.NewTime(time.Now().Add(-tc.startedSince)),
|
||||
}
|
||||
if tc.retryAttempt > 0 {
|
||||
app.Status.OperationState.FinishedAt = ptr.To(metav1.NewTime(time.Now().Add(-tc.startedSince)))
|
||||
app.Status.OperationState.RetryCount = int64(tc.retryAttempt)
|
||||
}
|
||||
|
||||
ctrl.processRequestedAppOperation(app)
|
||||
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.ObjectMeta.Namespace).Get(t.Context(), app.Name, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tc.expectedPhase, app.Status.OperationState.Phase)
|
||||
assert.Equal(t, tc.expectedMessage, app.Status.OperationState.Message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAppHosts(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
data := &fakeData{
|
||||
@@ -2823,54 +3000,3 @@ func TestSelfHealBackoffCooldownElapsed(t *testing.T) {
|
||||
assert.False(t, elapsed)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSyncTimeout(t *testing.T) {
|
||||
testCases := []struct {
|
||||
delta time.Duration
|
||||
expectedPhase synccommon.OperationPhase
|
||||
expectedMessage string
|
||||
}{{
|
||||
delta: 2 * time.Minute,
|
||||
expectedPhase: synccommon.OperationFailed,
|
||||
expectedMessage: "Operation terminated",
|
||||
}, {
|
||||
delta: 30 * time.Second,
|
||||
expectedPhase: synccommon.OperationSucceeded,
|
||||
expectedMessage: "successfully synced (no more tasks)",
|
||||
}}
|
||||
for i := range testCases {
|
||||
tc := testCases[i]
|
||||
t.Run(fmt.Sprintf("test case %d", i), func(t *testing.T) {
|
||||
app := newFakeApp()
|
||||
app.Spec.Project = "default"
|
||||
app.Operation = &v1alpha1.Operation{
|
||||
Sync: &v1alpha1.SyncOperation{
|
||||
Revision: "HEAD",
|
||||
},
|
||||
}
|
||||
ctrl := newFakeController(&fakeData{
|
||||
apps: []runtime.Object{app, &defaultProj},
|
||||
manifestResponses: []*apiclient.ManifestResponse{{
|
||||
Manifests: []string{},
|
||||
}},
|
||||
}, nil)
|
||||
|
||||
ctrl.syncTimeout = time.Minute
|
||||
app.Status.OperationState = &v1alpha1.OperationState{
|
||||
Operation: v1alpha1.Operation{
|
||||
Sync: &v1alpha1.SyncOperation{
|
||||
Revision: "HEAD",
|
||||
},
|
||||
},
|
||||
Phase: synccommon.OperationRunning,
|
||||
StartedAt: metav1.NewTime(time.Now().Add(-tc.delta)),
|
||||
}
|
||||
ctrl.processRequestedAppOperation(app)
|
||||
|
||||
app, err := ctrl.applicationClientset.ArgoprojV1alpha1().Applications(app.ObjectMeta.Namespace).Get(t.Context(), app.Name, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expectedPhase, app.Status.OperationState.Phase)
|
||||
require.Equal(t, tc.expectedMessage, app.Status.OperationState.Message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
13
controller/cache/cache.go
vendored
13
controller/cache/cache.go
vendored
@@ -137,8 +137,6 @@ type LiveStateCache interface {
|
||||
IsNamespaced(server *appv1.Cluster, gk schema.GroupKind) (bool, error)
|
||||
// Returns synced cluster cache
|
||||
GetClusterCache(server *appv1.Cluster) (clustercache.ClusterCache, error)
|
||||
// Executes give callback against resource specified by the key and all its children
|
||||
IterateHierarchy(server *appv1.Cluster, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string) bool) error
|
||||
// Executes give callback against resources specified by the keys and all its children
|
||||
IterateHierarchyV2(server *appv1.Cluster, keys []kube.ResourceKey, action func(child appv1.ResourceNode, appName string) bool) error
|
||||
// Returns state of live nodes which correspond for target nodes of specified application.
|
||||
@@ -669,17 +667,6 @@ func (c *liveStateCache) IsNamespaced(server *appv1.Cluster, gk schema.GroupKind
|
||||
return clusterInfo.IsNamespaced(gk)
|
||||
}
|
||||
|
||||
func (c *liveStateCache) IterateHierarchy(server *appv1.Cluster, key kube.ResourceKey, action func(child appv1.ResourceNode, appName string) bool) error {
|
||||
clusterInfo, err := c.getSyncedCluster(server)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clusterInfo.IterateHierarchy(key, func(resource *clustercache.Resource, namespaceResources map[kube.ResourceKey]*clustercache.Resource) bool {
|
||||
return action(asResourceNode(resource), getApp(resource, namespaceResources))
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *liveStateCache) IterateHierarchyV2(server *appv1.Cluster, keys []kube.ResourceKey, action func(child appv1.ResourceNode, appName string) bool) error {
|
||||
clusterInfo, err := c.getSyncedCluster(server)
|
||||
if err != nil {
|
||||
|
||||
12
controller/cache/info.go
vendored
12
controller/cache/info.go
vendored
@@ -446,6 +446,7 @@ func populatePodInfo(un *unstructured.Unstructured, res *ResourceInfo) {
|
||||
}
|
||||
|
||||
req, _ := resourcehelper.PodRequestsAndLimits(&pod)
|
||||
|
||||
res.PodInfo = &PodInfo{NodeName: pod.Spec.NodeName, ResourceRequests: req, Phase: pod.Status.Phase}
|
||||
|
||||
res.Info = append(res.Info, v1alpha1.InfoItem{Name: "Node", Value: pod.Spec.NodeName})
|
||||
@@ -454,6 +455,17 @@ func populatePodInfo(un *unstructured.Unstructured, res *ResourceInfo) {
|
||||
res.Info = append(res.Info, v1alpha1.InfoItem{Name: "Restart Count", Value: strconv.Itoa(restarts)})
|
||||
}
|
||||
|
||||
// Requests are relevant even for pods in the init phase or pending state (e.g., due to insufficient resources),
|
||||
// as they help with diagnosing scheduling and startup issues.
|
||||
// requests will be released for terminated pods either with success or failed state termination.
|
||||
if !isPodPhaseTerminal(pod.Status.Phase) {
|
||||
CPUReq := req[corev1.ResourceCPU]
|
||||
MemoryReq := req[corev1.ResourceMemory]
|
||||
|
||||
res.Info = append(res.Info, v1alpha1.InfoItem{Name: common.PodRequestsCPU, Value: strconv.FormatInt(CPUReq.MilliValue(), 10)})
|
||||
res.Info = append(res.Info, v1alpha1.InfoItem{Name: common.PodRequestsMEM, Value: strconv.FormatInt(MemoryReq.MilliValue(), 10)})
|
||||
}
|
||||
|
||||
var urls []string
|
||||
if res.NetworkingInfo != nil {
|
||||
urls = res.NetworkingInfo.ExternalURLs
|
||||
|
||||
172
controller/cache/info_test.go
vendored
172
controller/cache/info_test.go
vendored
@@ -12,6 +12,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"sigs.k8s.io/yaml"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/common"
|
||||
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v3/util/argo/normalizers"
|
||||
"github.com/argoproj/argo-cd/v3/util/errors"
|
||||
@@ -304,6 +305,8 @@ func TestGetPodInfo(t *testing.T) {
|
||||
assert.Equal(t, []v1alpha1.InfoItem{
|
||||
{Name: "Node", Value: "minikube"},
|
||||
{Name: "Containers", Value: "0/1"},
|
||||
{Name: common.PodRequestsCPU, Value: "0"}, // strings imported from common
|
||||
{Name: common.PodRequestsMEM, Value: "134217728000"},
|
||||
}, info.Info)
|
||||
assert.Equal(t, []string{"bar"}, info.Images)
|
||||
assert.Equal(t, &PodInfo{
|
||||
@@ -365,9 +368,81 @@ func TestGetPodInfo(t *testing.T) {
|
||||
{Name: "Status Reason", Value: "Running"},
|
||||
{Name: "Node", Value: "minikube"},
|
||||
{Name: "Containers", Value: "1/1"},
|
||||
{Name: common.PodRequestsCPU, Value: "0"},
|
||||
{Name: common.PodRequestsMEM, Value: "0"},
|
||||
}, info.Info)
|
||||
})
|
||||
|
||||
t.Run("TestGetPodWithInitialContainerInfoWithResources", func(t *testing.T) {
|
||||
pod := strToUnstructured(`
|
||||
apiVersion: "v1"
|
||||
kind: "Pod"
|
||||
metadata:
|
||||
labels:
|
||||
app: "app-with-initial-container"
|
||||
name: "app-with-initial-container-5f46976fdb-vd6rv"
|
||||
namespace: "default"
|
||||
ownerReferences:
|
||||
- apiVersion: "apps/v1"
|
||||
kind: "ReplicaSet"
|
||||
name: "app-with-initial-container-5f46976fdb"
|
||||
spec:
|
||||
containers:
|
||||
- image: "alpine:latest"
|
||||
imagePullPolicy: "Always"
|
||||
name: "app-with-initial-container"
|
||||
resources:
|
||||
requests:
|
||||
cpu: "100m"
|
||||
memory: "128Mi"
|
||||
limits:
|
||||
cpu: "500m"
|
||||
memory: "512Mi"
|
||||
initContainers:
|
||||
- image: "alpine:latest"
|
||||
imagePullPolicy: "Always"
|
||||
name: "app-with-initial-container-logshipper"
|
||||
resources:
|
||||
requests:
|
||||
cpu: "50m"
|
||||
memory: "64Mi"
|
||||
limits:
|
||||
cpu: "250m"
|
||||
memory: "256Mi"
|
||||
nodeName: "minikube"
|
||||
status:
|
||||
containerStatuses:
|
||||
- image: "alpine:latest"
|
||||
name: "app-with-initial-container"
|
||||
ready: true
|
||||
restartCount: 0
|
||||
started: true
|
||||
state:
|
||||
running:
|
||||
startedAt: "2024-10-08T08:44:25Z"
|
||||
initContainerStatuses:
|
||||
- image: "alpine:latest"
|
||||
name: "app-with-initial-container-logshipper"
|
||||
ready: true
|
||||
restartCount: 0
|
||||
started: false
|
||||
state:
|
||||
terminated:
|
||||
exitCode: 0
|
||||
reason: "Completed"
|
||||
phase: "Running"
|
||||
`)
|
||||
|
||||
info := &ResourceInfo{}
|
||||
populateNodeInfo(pod, info, []string{})
|
||||
assert.Equal(t, []v1alpha1.InfoItem{
|
||||
{Name: "Status Reason", Value: "Running"},
|
||||
{Name: "Node", Value: "minikube"},
|
||||
{Name: "Containers", Value: "1/1"},
|
||||
{Name: common.PodRequestsCPU, Value: "100"},
|
||||
{Name: common.PodRequestsMEM, Value: "134217728000"},
|
||||
}, info.Info)
|
||||
})
|
||||
t.Run("TestGetPodInfoWithSidecar", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -422,6 +497,8 @@ func TestGetPodInfo(t *testing.T) {
|
||||
{Name: "Status Reason", Value: "Running"},
|
||||
{Name: "Node", Value: "minikube"},
|
||||
{Name: "Containers", Value: "2/2"},
|
||||
{Name: common.PodRequestsCPU, Value: "0"},
|
||||
{Name: common.PodRequestsMEM, Value: "0"},
|
||||
}, info.Info)
|
||||
})
|
||||
|
||||
@@ -480,6 +557,8 @@ func TestGetPodInfo(t *testing.T) {
|
||||
{Name: "Status Reason", Value: "Init:0/1"},
|
||||
{Name: "Node", Value: "minikube"},
|
||||
{Name: "Containers", Value: "0/1"},
|
||||
{Name: common.PodRequestsCPU, Value: "0"},
|
||||
{Name: common.PodRequestsMEM, Value: "0"},
|
||||
}, info.Info)
|
||||
})
|
||||
|
||||
@@ -537,6 +616,8 @@ func TestGetPodInfo(t *testing.T) {
|
||||
{Name: "Node", Value: "minikube"},
|
||||
{Name: "Containers", Value: "0/3"},
|
||||
{Name: "Restart Count", Value: "3"},
|
||||
{Name: common.PodRequestsCPU, Value: "0"},
|
||||
{Name: common.PodRequestsMEM, Value: "0"},
|
||||
}, info.Info)
|
||||
})
|
||||
|
||||
@@ -594,6 +675,8 @@ func TestGetPodInfo(t *testing.T) {
|
||||
{Name: "Node", Value: "minikube"},
|
||||
{Name: "Containers", Value: "0/3"},
|
||||
{Name: "Restart Count", Value: "3"},
|
||||
{Name: common.PodRequestsCPU, Value: "0"},
|
||||
{Name: common.PodRequestsMEM, Value: "0"},
|
||||
}, info.Info)
|
||||
})
|
||||
|
||||
@@ -654,6 +737,8 @@ func TestGetPodInfo(t *testing.T) {
|
||||
{Name: "Node", Value: "minikube"},
|
||||
{Name: "Containers", Value: "1/3"},
|
||||
{Name: "Restart Count", Value: "7"},
|
||||
{Name: common.PodRequestsCPU, Value: "0"},
|
||||
{Name: common.PodRequestsMEM, Value: "0"},
|
||||
}, info.Info)
|
||||
})
|
||||
|
||||
@@ -696,6 +781,8 @@ func TestGetPodInfo(t *testing.T) {
|
||||
{Name: "Node", Value: "minikube"},
|
||||
{Name: "Containers", Value: "0/1"},
|
||||
{Name: "Restart Count", Value: "3"},
|
||||
{Name: common.PodRequestsCPU, Value: "0"},
|
||||
{Name: common.PodRequestsMEM, Value: "0"},
|
||||
}, info.Info)
|
||||
})
|
||||
|
||||
@@ -731,6 +818,45 @@ func TestGetPodInfo(t *testing.T) {
|
||||
}, info.Info)
|
||||
})
|
||||
|
||||
// Test pod condition succeed which had some allocated resources
|
||||
t.Run("TestPodConditionSucceededWithResources", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
pod := strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: test8
|
||||
spec:
|
||||
nodeName: minikube
|
||||
containers:
|
||||
- name: container
|
||||
resources:
|
||||
requests:
|
||||
cpu: "50m"
|
||||
memory: "64Mi"
|
||||
limits:
|
||||
cpu: "250m"
|
||||
memory: "256Mi"
|
||||
status:
|
||||
phase: Succeeded
|
||||
containerStatuses:
|
||||
- ready: false
|
||||
restartCount: 0
|
||||
state:
|
||||
terminated:
|
||||
reason: Completed
|
||||
exitCode: 0
|
||||
`)
|
||||
info := &ResourceInfo{}
|
||||
populateNodeInfo(pod, info, []string{})
|
||||
assert.Equal(t, []v1alpha1.InfoItem{
|
||||
{Name: "Status Reason", Value: "Completed"},
|
||||
{Name: "Node", Value: "minikube"},
|
||||
{Name: "Containers", Value: "0/1"},
|
||||
}, info.Info)
|
||||
})
|
||||
|
||||
// Test pod condition failed
|
||||
t.Run("TestPodConditionFailed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -763,6 +889,46 @@ func TestGetPodInfo(t *testing.T) {
|
||||
}, info.Info)
|
||||
})
|
||||
|
||||
// Test pod condition failed with allocated resources
|
||||
|
||||
t.Run("TestPodConditionFailedWithResources", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
pod := strToUnstructured(`
|
||||
apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: test9
|
||||
spec:
|
||||
nodeName: minikube
|
||||
containers:
|
||||
- name: container
|
||||
resources:
|
||||
requests:
|
||||
cpu: "50m"
|
||||
memory: "64Mi"
|
||||
limits:
|
||||
cpu: "250m"
|
||||
memory: "256Mi"
|
||||
status:
|
||||
phase: Failed
|
||||
containerStatuses:
|
||||
- ready: false
|
||||
restartCount: 0
|
||||
state:
|
||||
terminated:
|
||||
reason: Error
|
||||
exitCode: 1
|
||||
`)
|
||||
info := &ResourceInfo{}
|
||||
populateNodeInfo(pod, info, []string{})
|
||||
assert.Equal(t, []v1alpha1.InfoItem{
|
||||
{Name: "Status Reason", Value: "Error"},
|
||||
{Name: "Node", Value: "minikube"},
|
||||
{Name: "Containers", Value: "0/1"},
|
||||
}, info.Info)
|
||||
})
|
||||
|
||||
// Test pod condition succeed with deletion
|
||||
t.Run("TestPodConditionSucceededWithDeletion", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
@@ -824,6 +990,8 @@ func TestGetPodInfo(t *testing.T) {
|
||||
{Name: "Status Reason", Value: "Terminating"},
|
||||
{Name: "Node", Value: "minikube"},
|
||||
{Name: "Containers", Value: "0/1"},
|
||||
{Name: common.PodRequestsCPU, Value: "0"},
|
||||
{Name: common.PodRequestsMEM, Value: "0"},
|
||||
}, info.Info)
|
||||
})
|
||||
|
||||
@@ -850,6 +1018,8 @@ func TestGetPodInfo(t *testing.T) {
|
||||
{Name: "Status Reason", Value: "Terminating"},
|
||||
{Name: "Node", Value: "minikube"},
|
||||
{Name: "Containers", Value: "0/1"},
|
||||
{Name: common.PodRequestsCPU, Value: "0"},
|
||||
{Name: common.PodRequestsMEM, Value: "0"},
|
||||
}, info.Info)
|
||||
})
|
||||
|
||||
@@ -880,6 +1050,8 @@ func TestGetPodInfo(t *testing.T) {
|
||||
{Name: "Status Reason", Value: "SchedulingGated"},
|
||||
{Name: "Node", Value: "minikube"},
|
||||
{Name: "Containers", Value: "0/2"},
|
||||
{Name: common.PodRequestsCPU, Value: "0"},
|
||||
{Name: common.PodRequestsMEM, Value: "0"},
|
||||
}, info.Info)
|
||||
})
|
||||
}
|
||||
|
||||
63
controller/cache/mocks/LiveStateCache.go
generated
vendored
63
controller/cache/mocks/LiveStateCache.go
generated
vendored
@@ -471,69 +471,6 @@ func (_c *LiveStateCache_IsNamespaced_Call) RunAndReturn(run func(server *v1alph
|
||||
return _c
|
||||
}
|
||||
|
||||
// IterateHierarchy provides a mock function for the type LiveStateCache
|
||||
func (_mock *LiveStateCache) IterateHierarchy(server *v1alpha1.Cluster, key kube.ResourceKey, action func(child v1alpha1.ResourceNode, appName string) bool) error {
|
||||
ret := _mock.Called(server, key, action)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for IterateHierarchy")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if returnFunc, ok := ret.Get(0).(func(*v1alpha1.Cluster, kube.ResourceKey, func(child v1alpha1.ResourceNode, appName string) bool) error); ok {
|
||||
r0 = returnFunc(server, key, action)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
return r0
|
||||
}
|
||||
|
||||
// LiveStateCache_IterateHierarchy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IterateHierarchy'
|
||||
type LiveStateCache_IterateHierarchy_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// IterateHierarchy is a helper method to define mock.On call
|
||||
// - server *v1alpha1.Cluster
|
||||
// - key kube.ResourceKey
|
||||
// - action func(child v1alpha1.ResourceNode, appName string) bool
|
||||
func (_e *LiveStateCache_Expecter) IterateHierarchy(server interface{}, key interface{}, action interface{}) *LiveStateCache_IterateHierarchy_Call {
|
||||
return &LiveStateCache_IterateHierarchy_Call{Call: _e.mock.On("IterateHierarchy", server, key, action)}
|
||||
}
|
||||
|
||||
func (_c *LiveStateCache_IterateHierarchy_Call) Run(run func(server *v1alpha1.Cluster, key kube.ResourceKey, action func(child v1alpha1.ResourceNode, appName string) bool)) *LiveStateCache_IterateHierarchy_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 *v1alpha1.Cluster
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(*v1alpha1.Cluster)
|
||||
}
|
||||
var arg1 kube.ResourceKey
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(kube.ResourceKey)
|
||||
}
|
||||
var arg2 func(child v1alpha1.ResourceNode, appName string) bool
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(func(child v1alpha1.ResourceNode, appName string) bool)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *LiveStateCache_IterateHierarchy_Call) Return(err error) *LiveStateCache_IterateHierarchy_Call {
|
||||
_c.Call.Return(err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *LiveStateCache_IterateHierarchy_Call) RunAndReturn(run func(server *v1alpha1.Cluster, key kube.ResourceKey, action func(child v1alpha1.ResourceNode, appName string) bool) error) *LiveStateCache_IterateHierarchy_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// IterateHierarchyV2 provides a mock function for the type LiveStateCache
|
||||
func (_mock *LiveStateCache) IterateHierarchyV2(server *v1alpha1.Cluster, keys []kube.ResourceKey, action func(child v1alpha1.ResourceNode, appName string) bool) error {
|
||||
ret := _mock.Called(server, keys, action)
|
||||
|
||||
@@ -27,10 +27,9 @@ func setApplicationHealth(resources []managedResource, statuses []appv1.Resource
|
||||
if res.Target != nil && hookutil.Skip(res.Target) {
|
||||
continue
|
||||
}
|
||||
if res.Target != nil && res.Target.GetAnnotations() != nil && res.Target.GetAnnotations()[common.AnnotationIgnoreHealthCheck] == "true" {
|
||||
if res.Live != nil && res.Live.GetAnnotations() != nil && res.Live.GetAnnotations()[common.AnnotationIgnoreHealthCheck] == "true" {
|
||||
continue
|
||||
}
|
||||
|
||||
if res.Live != nil && (hookutil.IsHook(res.Live) || ignore.Ignore(res.Live)) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ func TestSetApplicationHealth(t *testing.T) {
|
||||
// The app is considered healthy
|
||||
failedJob.SetAnnotations(nil)
|
||||
failedJobIgnoreHealthcheck := resourceFromFile("./testdata/job-failed-ignore-healthcheck.yaml")
|
||||
resources[1].Target = &failedJobIgnoreHealthcheck
|
||||
resources[1].Live = &failedJobIgnoreHealthcheck
|
||||
healthStatus, err = setApplicationHealth(resources, resourceStatuses, nil, app, true)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, health.HealthStatusHealthy, healthStatus)
|
||||
|
||||
@@ -51,7 +51,7 @@ func (ctrl *ApplicationController) executePostDeleteHooks(app *v1alpha1.Applicat
|
||||
revisions = append(revisions, src.TargetRevision)
|
||||
}
|
||||
|
||||
targets, _, _, err := ctrl.appStateManager.GetRepoObjs(app, app.Spec.GetSources(), appLabelKey, revisions, false, false, false, proj, true)
|
||||
targets, _, _, err := ctrl.appStateManager.GetRepoObjs(context.Background(), app, app.Spec.GetSources(), appLabelKey, revisions, false, false, false, proj, true)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -4,8 +4,14 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
@@ -16,6 +22,7 @@ import (
|
||||
"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
|
||||
applog "github.com/argoproj/argo-cd/v3/util/app/log"
|
||||
"github.com/argoproj/argo-cd/v3/util/git"
|
||||
"github.com/argoproj/argo-cd/v3/util/hydrator"
|
||||
utilio "github.com/argoproj/argo-cd/v3/util/io"
|
||||
)
|
||||
|
||||
@@ -43,7 +50,7 @@ type Dependencies interface {
|
||||
|
||||
// GetRepoObjs returns the repository objects for the given application, source, and revision. It calls the repo-
|
||||
// server and gets the manifests (objects).
|
||||
GetRepoObjs(app *appv1.Application, source appv1.ApplicationSource, revision string, project *appv1.AppProject) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error)
|
||||
GetRepoObjs(ctx context.Context, app *appv1.Application, source appv1.ApplicationSource, revision string, project *appv1.AppProject) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error)
|
||||
|
||||
// GetWriteCredentials returns the repository credentials for the given repository URL and project. These are to be
|
||||
// sent to the commit server to write the hydrated manifests.
|
||||
@@ -59,6 +66,9 @@ type Dependencies interface {
|
||||
// AddHydrationQueueItem adds a hydration queue item to the queue. This is used to trigger the hydration process for
|
||||
// a group of applications which are hydrating to the same repo and target branch.
|
||||
AddHydrationQueueItem(key types.HydrationQueueKey)
|
||||
|
||||
// GetHydratorCommitMessageTemplate gets the configured template for rendering commit messages.
|
||||
GetHydratorCommitMessageTemplate() (string, error)
|
||||
}
|
||||
|
||||
// Hydrator is the main struct that implements the hydration logic. It uses the Dependencies interface to access the
|
||||
@@ -91,101 +101,135 @@ func NewHydrator(dependencies Dependencies, statusRefreshTimeout time.Duration,
|
||||
// It's likely that multiple applications will trigger hydration at the same time. The hydration queue key is meant to
|
||||
// dedupe these requests.
|
||||
func (h *Hydrator) ProcessAppHydrateQueueItem(origApp *appv1.Application) {
|
||||
origApp = origApp.DeepCopy()
|
||||
app := origApp.DeepCopy()
|
||||
|
||||
if app.Spec.SourceHydrator == nil {
|
||||
return
|
||||
}
|
||||
|
||||
logCtx := log.WithFields(applog.GetAppLogFields(app))
|
||||
|
||||
logCtx.Debug("Processing app hydrate queue item")
|
||||
|
||||
// TODO: don't reuse statusRefreshTimeout. Create a new timeout for hydration.
|
||||
needsHydration, reason := appNeedsHydration(origApp, h.statusRefreshTimeout)
|
||||
if !needsHydration {
|
||||
return
|
||||
needsHydration, reason := appNeedsHydration(app)
|
||||
if needsHydration {
|
||||
app.Status.SourceHydrator.CurrentOperation = &appv1.HydrateOperation{
|
||||
StartedAt: metav1.Now(),
|
||||
FinishedAt: nil,
|
||||
Phase: appv1.HydrateOperationPhaseHydrating,
|
||||
SourceHydrator: *app.Spec.SourceHydrator,
|
||||
}
|
||||
h.dependencies.PersistAppHydratorStatus(origApp, &app.Status.SourceHydrator)
|
||||
}
|
||||
|
||||
logCtx.WithField("reason", reason).Info("Hydrating app")
|
||||
|
||||
app.Status.SourceHydrator.CurrentOperation = &appv1.HydrateOperation{
|
||||
StartedAt: metav1.Now(),
|
||||
FinishedAt: nil,
|
||||
Phase: appv1.HydrateOperationPhaseHydrating,
|
||||
SourceHydrator: *app.Spec.SourceHydrator,
|
||||
needsRefresh := app.Status.SourceHydrator.CurrentOperation.Phase == appv1.HydrateOperationPhaseHydrating && metav1.Now().Sub(app.Status.SourceHydrator.CurrentOperation.StartedAt.Time) > h.statusRefreshTimeout
|
||||
if needsHydration || needsRefresh {
|
||||
logCtx.WithField("reason", reason).Info("Hydrating app")
|
||||
h.dependencies.AddHydrationQueueItem(getHydrationQueueKey(app))
|
||||
} else {
|
||||
logCtx.WithField("reason", reason).Debug("Skipping hydration")
|
||||
}
|
||||
h.dependencies.PersistAppHydratorStatus(origApp, &app.Status.SourceHydrator)
|
||||
origApp.Status.SourceHydrator = app.Status.SourceHydrator
|
||||
h.dependencies.AddHydrationQueueItem(getHydrationQueueKey(app))
|
||||
|
||||
logCtx.Debug("Successfully processed app hydrate queue item")
|
||||
}
|
||||
|
||||
func getHydrationQueueKey(app *appv1.Application) types.HydrationQueueKey {
|
||||
destinationBranch := app.Spec.SourceHydrator.SyncSource.TargetBranch
|
||||
if app.Spec.SourceHydrator.HydrateTo != nil {
|
||||
destinationBranch = app.Spec.SourceHydrator.HydrateTo.TargetBranch
|
||||
}
|
||||
key := types.HydrationQueueKey{
|
||||
SourceRepoURL: git.NormalizeGitURLAllowInvalid(app.Spec.SourceHydrator.DrySource.RepoURL),
|
||||
SourceTargetRevision: app.Spec.SourceHydrator.DrySource.TargetRevision,
|
||||
DestinationBranch: destinationBranch,
|
||||
DestinationBranch: app.Spec.GetHydrateToSource().TargetRevision,
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// uniqueHydrationDestination is used to detect duplicate hydrate destinations.
|
||||
type uniqueHydrationDestination struct {
|
||||
// sourceRepoURL must be normalized with git.NormalizeGitURL to ensure that two apps with different URL formats
|
||||
// don't end up in two different hydration queue items. Failing to normalize would result in one hydrated commit for
|
||||
// each unique URL.
|
||||
//nolint:unused // used as part of a map key
|
||||
sourceRepoURL string
|
||||
//nolint:unused // used as part of a map key
|
||||
sourceTargetRevision string
|
||||
//nolint:unused // used as part of a map key
|
||||
destinationBranch string
|
||||
//nolint:unused // used as part of a map key
|
||||
destinationPath string
|
||||
}
|
||||
|
||||
// ProcessHydrationQueueItem processes a hydration queue item. It retrieves the relevant applications for the given
|
||||
// hydration key, hydrates their latest commit, and updates their status accordingly. If the hydration fails, it marks
|
||||
// the operation as failed and logs the error. If successful, it updates the operation to indicate that hydration was
|
||||
// successful and requests a refresh of the applications to pick up the new hydrated commit.
|
||||
func (h *Hydrator) ProcessHydrationQueueItem(hydrationKey types.HydrationQueueKey) (processNext bool) {
|
||||
func (h *Hydrator) ProcessHydrationQueueItem(hydrationKey types.HydrationQueueKey) {
|
||||
logCtx := log.WithFields(log.Fields{
|
||||
"sourceRepoURL": hydrationKey.SourceRepoURL,
|
||||
"sourceTargetRevision": hydrationKey.SourceTargetRevision,
|
||||
"destinationBranch": hydrationKey.DestinationBranch,
|
||||
})
|
||||
|
||||
relevantApps, drySHA, hydratedSHA, err := h.hydrateAppsLatestCommit(logCtx, hydrationKey)
|
||||
if drySHA != "" {
|
||||
logCtx = logCtx.WithField("drySHA", drySHA)
|
||||
}
|
||||
// Get all applications sharing the same hydration key
|
||||
apps, err := h.getAppsForHydrationKey(hydrationKey)
|
||||
if err != nil {
|
||||
logCtx.WithField("appCount", len(relevantApps)).WithError(err).Error("Failed to hydrate apps")
|
||||
for _, app := range relevantApps {
|
||||
origApp := app.DeepCopy()
|
||||
app.Status.SourceHydrator.CurrentOperation.Phase = appv1.HydrateOperationPhaseFailed
|
||||
failedAt := metav1.Now()
|
||||
app.Status.SourceHydrator.CurrentOperation.FinishedAt = &failedAt
|
||||
app.Status.SourceHydrator.CurrentOperation.Message = fmt.Sprintf("Failed to hydrate revision %q: %v", drySHA, err.Error())
|
||||
// We may or may not have gotten far enough in the hydration process to get a non-empty SHA, but set it just
|
||||
// in case we did.
|
||||
app.Status.SourceHydrator.CurrentOperation.DrySHA = drySHA
|
||||
h.dependencies.PersistAppHydratorStatus(origApp, &app.Status.SourceHydrator)
|
||||
logCtx = logCtx.WithFields(applog.GetAppLogFields(app))
|
||||
logCtx.Errorf("Failed to hydrate app: %v", err)
|
||||
// If we get an error here, we cannot proceed with hydration and we do not know
|
||||
// which apps to update with the failure. The best we can do is log an error in
|
||||
// the controller and wait for statusRefreshTimeout to retry
|
||||
logCtx.WithError(err).Error("failed to get apps for hydration")
|
||||
return
|
||||
}
|
||||
logCtx.WithField("appCount", len(apps))
|
||||
|
||||
// FIXME: we might end up in a race condition here where an HydrationQueueItem is processed
|
||||
// before all applications had their CurrentOperation set by ProcessAppHydrateQueueItem.
|
||||
// This would cause this method to update "old" CurrentOperation.
|
||||
// It should only start hydration if all apps are in the HydrateOperationPhaseHydrating phase.
|
||||
raceDetected := false
|
||||
for _, app := range apps {
|
||||
if app.Status.SourceHydrator.CurrentOperation == nil || app.Status.SourceHydrator.CurrentOperation.Phase != appv1.HydrateOperationPhaseHydrating {
|
||||
raceDetected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if raceDetected {
|
||||
logCtx.Warn("race condition detected: not all apps are in HydrateOperationPhaseHydrating phase")
|
||||
}
|
||||
|
||||
// validate all the applications to make sure they are all correctly configured.
|
||||
// All applications sharing the same hydration key must succeed for the hydration to be processed.
|
||||
projects, validationErrors := h.validateApplications(apps)
|
||||
if len(validationErrors) > 0 {
|
||||
// For the applications that have an error, set the specific error in their status.
|
||||
// Applications without error will still fail with a generic error since the hydration cannot be partial
|
||||
genericError := genericHydrationError(validationErrors)
|
||||
for _, app := range apps {
|
||||
if err, ok := validationErrors[app.QualifiedName()]; ok {
|
||||
logCtx = logCtx.WithFields(applog.GetAppLogFields(app))
|
||||
logCtx.Errorf("failed to validate hydration app: %v", err)
|
||||
h.setAppHydratorError(app, err)
|
||||
} else {
|
||||
h.setAppHydratorError(app, genericError)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
logCtx.WithField("appCount", len(relevantApps)).Debug("Successfully hydrated apps")
|
||||
|
||||
// Hydrate all the apps
|
||||
drySHA, hydratedSHA, appErrors, err := h.hydrate(logCtx, apps, projects)
|
||||
if err != nil {
|
||||
// If there is a single error, it affects each applications
|
||||
for i := range apps {
|
||||
appErrors[apps[i].QualifiedName()] = err
|
||||
}
|
||||
}
|
||||
if drySHA != "" {
|
||||
logCtx = logCtx.WithField("drySHA", drySHA)
|
||||
}
|
||||
if len(appErrors) > 0 {
|
||||
// For the applications that have an error, set the specific error in their status.
|
||||
// Applications without error will still fail with a generic error since the hydration cannot be partial
|
||||
genericError := genericHydrationError(appErrors)
|
||||
for _, app := range apps {
|
||||
if drySHA != "" {
|
||||
// If we have a drySHA, we can set it on the app status
|
||||
app.Status.SourceHydrator.CurrentOperation.DrySHA = drySHA
|
||||
}
|
||||
if err, ok := appErrors[app.QualifiedName()]; ok {
|
||||
logCtx = logCtx.WithFields(applog.GetAppLogFields(app))
|
||||
logCtx.Errorf("failed to hydrate app: %v", err)
|
||||
h.setAppHydratorError(app, err)
|
||||
} else {
|
||||
h.setAppHydratorError(app, genericError)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
logCtx.Debug("Successfully hydrated apps")
|
||||
finishedAt := metav1.Now()
|
||||
for _, app := range relevantApps {
|
||||
for _, app := range apps {
|
||||
origApp := app.DeepCopy()
|
||||
operation := &appv1.HydrateOperation{
|
||||
StartedAt: app.Status.SourceHydrator.CurrentOperation.StartedAt,
|
||||
@@ -203,30 +247,32 @@ func (h *Hydrator) ProcessHydrationQueueItem(hydrationKey types.HydrationQueueKe
|
||||
SourceHydrator: app.Status.SourceHydrator.CurrentOperation.SourceHydrator,
|
||||
}
|
||||
h.dependencies.PersistAppHydratorStatus(origApp, &app.Status.SourceHydrator)
|
||||
|
||||
// Request a refresh since we pushed a new commit.
|
||||
err := h.dependencies.RequestAppRefresh(app.Name, app.Namespace)
|
||||
if err != nil {
|
||||
logCtx.WithField("app", app.QualifiedName()).WithError(err).Error("Failed to request app refresh after hydration")
|
||||
logCtx.WithFields(applog.GetAppLogFields(app)).WithError(err).Error("Failed to request app refresh after hydration")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (h *Hydrator) hydrateAppsLatestCommit(logCtx *log.Entry, hydrationKey types.HydrationQueueKey) ([]*appv1.Application, string, string, error) {
|
||||
relevantApps, err := h.getRelevantAppsForHydration(logCtx, hydrationKey)
|
||||
if err != nil {
|
||||
return nil, "", "", fmt.Errorf("failed to get relevant apps for hydration: %w", err)
|
||||
// setAppHydratorError updates the CurrentOperation with the error information.
|
||||
func (h *Hydrator) setAppHydratorError(app *appv1.Application, err error) {
|
||||
// if the operation is not in progress, we do not update the status
|
||||
if app.Status.SourceHydrator.CurrentOperation.Phase != appv1.HydrateOperationPhaseHydrating {
|
||||
return
|
||||
}
|
||||
|
||||
dryRevision, hydratedRevision, err := h.hydrate(logCtx, relevantApps)
|
||||
if err != nil {
|
||||
return relevantApps, dryRevision, "", fmt.Errorf("failed to hydrate apps: %w", err)
|
||||
}
|
||||
|
||||
return relevantApps, dryRevision, hydratedRevision, nil
|
||||
origApp := app.DeepCopy()
|
||||
app.Status.SourceHydrator.CurrentOperation.Phase = appv1.HydrateOperationPhaseFailed
|
||||
failedAt := metav1.Now()
|
||||
app.Status.SourceHydrator.CurrentOperation.FinishedAt = &failedAt
|
||||
app.Status.SourceHydrator.CurrentOperation.Message = fmt.Sprintf("Failed to hydrate: %v", err.Error())
|
||||
h.dependencies.PersistAppHydratorStatus(origApp, &app.Status.SourceHydrator)
|
||||
}
|
||||
|
||||
func (h *Hydrator) getRelevantAppsForHydration(logCtx *log.Entry, hydrationKey types.HydrationQueueKey) ([]*appv1.Application, error) {
|
||||
// getAppsForHydrationKey returns the applications matching the hydration key.
|
||||
func (h *Hydrator) getAppsForHydrationKey(hydrationKey types.HydrationQueueKey) ([]*appv1.Application, error) {
|
||||
// Get all apps
|
||||
apps, err := h.dependencies.GetProcessableApps()
|
||||
if err != nil {
|
||||
@@ -234,105 +280,113 @@ func (h *Hydrator) getRelevantAppsForHydration(logCtx *log.Entry, hydrationKey t
|
||||
}
|
||||
|
||||
var relevantApps []*appv1.Application
|
||||
uniqueDestinations := make(map[uniqueHydrationDestination]bool, len(apps.Items))
|
||||
for _, app := range apps.Items {
|
||||
if app.Spec.SourceHydrator == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !git.SameURL(app.Spec.SourceHydrator.DrySource.RepoURL, hydrationKey.SourceRepoURL) ||
|
||||
app.Spec.SourceHydrator.DrySource.TargetRevision != hydrationKey.SourceTargetRevision {
|
||||
appKey := getHydrationQueueKey(&app)
|
||||
if appKey != hydrationKey {
|
||||
continue
|
||||
}
|
||||
destinationBranch := app.Spec.SourceHydrator.SyncSource.TargetBranch
|
||||
if app.Spec.SourceHydrator.HydrateTo != nil {
|
||||
destinationBranch = app.Spec.SourceHydrator.HydrateTo.TargetBranch
|
||||
}
|
||||
if destinationBranch != hydrationKey.DestinationBranch {
|
||||
continue
|
||||
}
|
||||
|
||||
var proj *appv1.AppProject
|
||||
proj, err = h.dependencies.GetProcessableAppProj(&app)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get project %q for app %q: %w", app.Spec.Project, app.QualifiedName(), err)
|
||||
}
|
||||
permitted := proj.IsSourcePermitted(app.Spec.GetSource())
|
||||
if !permitted {
|
||||
// Log and skip. We don't want to fail the entire operation because of one app.
|
||||
logCtx.Warnf("App %q is not permitted to use source %q", app.QualifiedName(), app.Spec.Source.String())
|
||||
continue
|
||||
}
|
||||
|
||||
uniqueDestinationKey := uniqueHydrationDestination{
|
||||
sourceRepoURL: git.NormalizeGitURLAllowInvalid(app.Spec.SourceHydrator.DrySource.RepoURL),
|
||||
sourceTargetRevision: app.Spec.SourceHydrator.DrySource.TargetRevision,
|
||||
destinationBranch: destinationBranch,
|
||||
destinationPath: app.Spec.SourceHydrator.SyncSource.Path,
|
||||
}
|
||||
// TODO: test the dupe detection
|
||||
if _, ok := uniqueDestinations[uniqueDestinationKey]; ok {
|
||||
return nil, fmt.Errorf("multiple app hydrators use the same destination: %v", uniqueDestinationKey)
|
||||
}
|
||||
uniqueDestinations[uniqueDestinationKey] = true
|
||||
|
||||
relevantApps = append(relevantApps, &app)
|
||||
}
|
||||
return relevantApps, nil
|
||||
}
|
||||
|
||||
func (h *Hydrator) hydrate(logCtx *log.Entry, apps []*appv1.Application) (string, string, error) {
|
||||
if len(apps) == 0 {
|
||||
return "", "", nil
|
||||
}
|
||||
repoURL := apps[0].Spec.SourceHydrator.DrySource.RepoURL
|
||||
syncBranch := apps[0].Spec.SourceHydrator.SyncSource.TargetBranch
|
||||
targetBranch := apps[0].Spec.GetHydrateToSource().TargetRevision
|
||||
var paths []*commitclient.PathDetails
|
||||
projects := make(map[string]bool, len(apps))
|
||||
var targetRevision string
|
||||
// TODO: parallelize this loop
|
||||
// validateApplications checks that all applications are valid for hydration.
|
||||
func (h *Hydrator) validateApplications(apps []*appv1.Application) (map[string]*appv1.AppProject, map[string]error) {
|
||||
projects := make(map[string]*appv1.AppProject)
|
||||
errors := make(map[string]error)
|
||||
uniquePaths := make(map[string]string, len(apps))
|
||||
|
||||
for _, app := range apps {
|
||||
project, err := h.dependencies.GetProcessableAppProj(app)
|
||||
// Get the project for the app and validate if the app is allowed to use the source.
|
||||
// We can't short-circuit this even if we have seen this project before, because we need to verify that this
|
||||
// particular app is allowed to use this project.
|
||||
proj, err := h.dependencies.GetProcessableAppProj(app)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get project: %w", err)
|
||||
errors[app.QualifiedName()] = fmt.Errorf("failed to get project %q: %w", app.Spec.Project, err)
|
||||
continue
|
||||
}
|
||||
projects[project.Name] = true
|
||||
drySource := appv1.ApplicationSource{
|
||||
RepoURL: app.Spec.SourceHydrator.DrySource.RepoURL,
|
||||
Path: app.Spec.SourceHydrator.DrySource.Path,
|
||||
TargetRevision: app.Spec.SourceHydrator.DrySource.TargetRevision,
|
||||
permitted := proj.IsSourcePermitted(app.Spec.GetSource())
|
||||
if !permitted {
|
||||
errors[app.QualifiedName()] = fmt.Errorf("application repo %s is not permitted in project '%s'", app.Spec.GetSource().RepoURL, proj.Name)
|
||||
continue
|
||||
}
|
||||
if targetRevision == "" {
|
||||
targetRevision = app.Spec.SourceHydrator.DrySource.TargetRevision
|
||||
projects[app.Spec.Project] = proj
|
||||
|
||||
// Disallow hydrating to the repository root.
|
||||
// Hydrating to root would overwrite or delete files at the top level of the repo,
|
||||
// which can break other applications or shared configuration.
|
||||
// Every hydrated app must write into a subdirectory instead.
|
||||
destPath := app.Spec.SourceHydrator.SyncSource.Path
|
||||
if IsRootPath(destPath) {
|
||||
errors[app.QualifiedName()] = fmt.Errorf("app is configured to hydrate to the repository root (branch %q, path %q) which is not allowed", app.Spec.GetHydrateToSource().TargetRevision, destPath)
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO: enable signature verification
|
||||
objs, resp, err := h.dependencies.GetRepoObjs(app, drySource, targetRevision, project)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get repo objects for app %q: %w", app.QualifiedName(), err)
|
||||
// TODO: test the dupe detection
|
||||
// TODO: normalize the path to avoid "path/.." from being treated as different from "."
|
||||
if appName, ok := uniquePaths[destPath]; ok {
|
||||
errors[app.QualifiedName()] = fmt.Errorf("app %s hydrator use the same destination: %v", appName, app.Spec.SourceHydrator.SyncSource.Path)
|
||||
errors[appName] = fmt.Errorf("app %s hydrator use the same destination: %v", app.QualifiedName(), app.Spec.SourceHydrator.SyncSource.Path)
|
||||
continue
|
||||
}
|
||||
uniquePaths[destPath] = app.QualifiedName()
|
||||
}
|
||||
|
||||
// This should be the DRY SHA. We set it here so that after processing the first app, all apps are hydrated
|
||||
// using the same SHA.
|
||||
targetRevision = resp.Revision
|
||||
// If there are any errors, return nil for projects to avoid possible partial processing.
|
||||
if len(errors) > 0 {
|
||||
projects = nil
|
||||
}
|
||||
|
||||
// Set up a ManifestsRequest
|
||||
manifestDetails := make([]*commitclient.HydratedManifestDetails, len(objs))
|
||||
for i, obj := range objs {
|
||||
objJSON, err := json.Marshal(obj)
|
||||
return projects, errors
|
||||
}
|
||||
|
||||
func (h *Hydrator) hydrate(logCtx *log.Entry, apps []*appv1.Application, projects map[string]*appv1.AppProject) (string, string, map[string]error, error) {
|
||||
errors := make(map[string]error)
|
||||
if len(apps) == 0 {
|
||||
return "", "", nil, nil
|
||||
}
|
||||
|
||||
// These values are the same for all apps being hydrated together, so just get them from the first app.
|
||||
repoURL := apps[0].Spec.GetHydrateToSource().RepoURL
|
||||
targetBranch := apps[0].Spec.GetHydrateToSource().TargetRevision
|
||||
// FIXME: As a convenience, the commit server will create the syncBranch if it does not exist. If the
|
||||
// targetBranch does not exist, it will create it based on the syncBranch. On the next line, we take
|
||||
// the `syncBranch` from the first app and assume that they're all configured the same. Instead, if any
|
||||
// app has a different syncBranch, we should send the commit server an empty string and allow it to
|
||||
// create the targetBranch as an orphan since we can't reliable determine a reasonable base.
|
||||
syncBranch := apps[0].Spec.SourceHydrator.SyncSource.TargetBranch
|
||||
|
||||
// Get a static SHA revision from the first app so that all apps are hydrated from the same revision.
|
||||
targetRevision, pathDetails, err := h.getManifests(context.Background(), apps[0], "", projects[apps[0].Spec.Project])
|
||||
if err != nil {
|
||||
errors[apps[0].QualifiedName()] = fmt.Errorf("failed to get manifests: %w", err)
|
||||
return "", "", errors, nil
|
||||
}
|
||||
paths := []*commitclient.PathDetails{pathDetails}
|
||||
|
||||
eg, ctx := errgroup.WithContext(context.Background())
|
||||
var mu sync.Mutex
|
||||
|
||||
for _, app := range apps[1:] {
|
||||
app := app
|
||||
eg.Go(func() error {
|
||||
_, pathDetails, err = h.getManifests(ctx, app, targetRevision, projects[app.Spec.Project])
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to marshal object: %w", err)
|
||||
errors[app.QualifiedName()] = fmt.Errorf("failed to get manifests: %w", err)
|
||||
return errors[app.QualifiedName()]
|
||||
}
|
||||
manifestDetails[i] = &commitclient.HydratedManifestDetails{ManifestJSON: string(objJSON)}
|
||||
}
|
||||
|
||||
paths = append(paths, &commitclient.PathDetails{
|
||||
Path: app.Spec.SourceHydrator.SyncSource.Path,
|
||||
Manifests: manifestDetails,
|
||||
Commands: resp.Commands,
|
||||
paths = append(paths, pathDetails)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
if err := eg.Wait(); err != nil {
|
||||
return targetRevision, "", errors, nil
|
||||
}
|
||||
|
||||
// If all the apps are under the same project, use that project. Otherwise, use an empty string to indicate that we
|
||||
// need global creds.
|
||||
@@ -340,18 +394,19 @@ func (h *Hydrator) hydrate(logCtx *log.Entry, apps []*appv1.Application) (string
|
||||
if len(projects) == 1 {
|
||||
for p := range projects {
|
||||
project = p
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Get the commit metadata for the target revision.
|
||||
revisionMetadata, err := h.getRevisionMetadata(context.Background(), repoURL, project, targetRevision)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get revision metadata for %q: %w", targetRevision, err)
|
||||
return targetRevision, "", errors, fmt.Errorf("failed to get revision metadata for %q: %w", targetRevision, err)
|
||||
}
|
||||
|
||||
repo, err := h.dependencies.GetWriteCredentials(context.Background(), repoURL, project)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get hydrator credentials: %w", err)
|
||||
return targetRevision, "", errors, fmt.Errorf("failed to get hydrator credentials: %w", err)
|
||||
}
|
||||
if repo == nil {
|
||||
// Try without credentials.
|
||||
@@ -360,27 +415,73 @@ func (h *Hydrator) hydrate(logCtx *log.Entry, apps []*appv1.Application) (string
|
||||
}
|
||||
logCtx.Warn("no credentials found for repo, continuing without credentials")
|
||||
}
|
||||
// get the commit message template
|
||||
commitMessageTemplate, err := h.dependencies.GetHydratorCommitMessageTemplate()
|
||||
if err != nil {
|
||||
return targetRevision, "", errors, fmt.Errorf("failed to get hydrated commit message template: %w", err)
|
||||
}
|
||||
commitMessage, errMsg := getTemplatedCommitMessage(repoURL, targetRevision, commitMessageTemplate, revisionMetadata)
|
||||
if errMsg != nil {
|
||||
return targetRevision, "", errors, fmt.Errorf("failed to get hydrator commit templated message: %w", errMsg)
|
||||
}
|
||||
|
||||
manifestsRequest := commitclient.CommitHydratedManifestsRequest{
|
||||
Repo: repo,
|
||||
SyncBranch: syncBranch,
|
||||
TargetBranch: targetBranch,
|
||||
DrySha: targetRevision,
|
||||
CommitMessage: "[Argo CD Bot] hydrate " + targetRevision,
|
||||
CommitMessage: commitMessage,
|
||||
Paths: paths,
|
||||
DryCommitMetadata: revisionMetadata,
|
||||
}
|
||||
|
||||
closer, commitService, err := h.commitClientset.NewCommitServerClient()
|
||||
if err != nil {
|
||||
return targetRevision, "", fmt.Errorf("failed to create commit service: %w", err)
|
||||
return targetRevision, "", errors, fmt.Errorf("failed to create commit service: %w", err)
|
||||
}
|
||||
defer utilio.Close(closer)
|
||||
resp, err := commitService.CommitHydratedManifests(context.Background(), &manifestsRequest)
|
||||
if err != nil {
|
||||
return targetRevision, "", fmt.Errorf("failed to commit hydrated manifests: %w", err)
|
||||
return targetRevision, "", errors, fmt.Errorf("failed to commit hydrated manifests: %w", err)
|
||||
}
|
||||
return targetRevision, resp.HydratedSha, nil
|
||||
return targetRevision, resp.HydratedSha, errors, nil
|
||||
}
|
||||
|
||||
// getManifests gets the manifests for the given application and target revision. It returns the resolved revision
|
||||
// (a git SHA), and path details for the commit server.
|
||||
//
|
||||
// If the given target revision is empty, it uses the target revision from the app dry source spec.
|
||||
func (h *Hydrator) getManifests(ctx context.Context, app *appv1.Application, targetRevision string, project *appv1.AppProject) (revision string, pathDetails *commitclient.PathDetails, err error) {
|
||||
drySource := appv1.ApplicationSource{
|
||||
RepoURL: app.Spec.SourceHydrator.DrySource.RepoURL,
|
||||
Path: app.Spec.SourceHydrator.DrySource.Path,
|
||||
TargetRevision: app.Spec.SourceHydrator.DrySource.TargetRevision,
|
||||
}
|
||||
if targetRevision == "" {
|
||||
targetRevision = app.Spec.SourceHydrator.DrySource.TargetRevision
|
||||
}
|
||||
|
||||
// TODO: enable signature verification
|
||||
objs, resp, err := h.dependencies.GetRepoObjs(ctx, app, drySource, targetRevision, project)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to get repo objects for app %q: %w", app.QualifiedName(), err)
|
||||
}
|
||||
|
||||
// Set up a ManifestsRequest
|
||||
manifestDetails := make([]*commitclient.HydratedManifestDetails, len(objs))
|
||||
for i, obj := range objs {
|
||||
objJSON, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to marshal object: %w", err)
|
||||
}
|
||||
manifestDetails[i] = &commitclient.HydratedManifestDetails{ManifestJSON: string(objJSON)}
|
||||
}
|
||||
|
||||
return resp.Revision, &commitclient.PathDetails{
|
||||
Path: app.Spec.SourceHydrator.SyncSource.Path,
|
||||
Manifests: manifestDetails,
|
||||
Commands: resp.Commands,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *Hydrator) getRevisionMetadata(ctx context.Context, repoURL, project, revision string) (*appv1.RevisionMetadata, error) {
|
||||
@@ -406,28 +507,56 @@ func (h *Hydrator) getRevisionMetadata(ctx context.Context, repoURL, project, re
|
||||
}
|
||||
|
||||
// appNeedsHydration answers if application needs manifests hydrated.
|
||||
func appNeedsHydration(app *appv1.Application, statusHydrateTimeout time.Duration) (needsHydration bool, reason string) {
|
||||
if app.Spec.SourceHydrator == nil {
|
||||
return false, "source hydrator not configured"
|
||||
}
|
||||
|
||||
var hydratedAt *metav1.Time
|
||||
if app.Status.SourceHydrator.CurrentOperation != nil {
|
||||
hydratedAt = &app.Status.SourceHydrator.CurrentOperation.StartedAt
|
||||
}
|
||||
|
||||
func appNeedsHydration(app *appv1.Application) (needsHydration bool, reason string) {
|
||||
switch {
|
||||
case app.IsHydrateRequested():
|
||||
return true, "hydrate requested"
|
||||
case app.Spec.SourceHydrator == nil:
|
||||
return false, "source hydrator not configured"
|
||||
case app.Status.SourceHydrator.CurrentOperation == nil:
|
||||
return true, "no previous hydrate operation"
|
||||
case app.Status.SourceHydrator.CurrentOperation.Phase == appv1.HydrateOperationPhaseHydrating:
|
||||
return false, "hydration operation already in progress"
|
||||
case app.IsHydrateRequested():
|
||||
return true, "hydrate requested"
|
||||
case !app.Spec.SourceHydrator.DeepEquals(app.Status.SourceHydrator.CurrentOperation.SourceHydrator):
|
||||
return true, "spec.sourceHydrator differs"
|
||||
case app.Status.SourceHydrator.CurrentOperation.Phase == appv1.HydrateOperationPhaseFailed && metav1.Now().Sub(app.Status.SourceHydrator.CurrentOperation.FinishedAt.Time) > 2*time.Minute:
|
||||
return true, "previous hydrate operation failed more than 2 minutes ago"
|
||||
case hydratedAt == nil || hydratedAt.Add(statusHydrateTimeout).Before(time.Now().UTC()):
|
||||
return true, "hydration expired"
|
||||
}
|
||||
|
||||
return false, ""
|
||||
return false, "hydration not needed"
|
||||
}
|
||||
|
||||
// getTemplatedCommitMessage gets the multi-line commit message based on the template defined in the configmap. It is a two step process:
|
||||
// 1. Get the metadata template engine would use to render the template
|
||||
// 2. Pass the output of Step 1 and Step 2 to template Render
|
||||
func getTemplatedCommitMessage(repoURL, revision, commitMessageTemplate string, dryCommitMetadata *appv1.RevisionMetadata) (string, error) {
|
||||
hydratorCommitMetadata, err := hydrator.GetCommitMetadata(repoURL, revision, dryCommitMetadata)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get hydrated commit message: %w", err)
|
||||
}
|
||||
templatedCommitMsg, err := hydrator.Render(commitMessageTemplate, hydratorCommitMetadata)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse template %s: %w", commitMessageTemplate, err)
|
||||
}
|
||||
return templatedCommitMsg, nil
|
||||
}
|
||||
|
||||
// genericHydrationError returns an error that summarizes the hydration errors for all applications.
|
||||
func genericHydrationError(validationErrors map[string]error) error {
|
||||
if len(validationErrors) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
keys := slices.Sorted(maps.Keys(validationErrors))
|
||||
remainder := "has an error"
|
||||
if len(keys) > 1 {
|
||||
remainder = fmt.Sprintf("and %d more have errors", len(keys)-1)
|
||||
}
|
||||
return fmt.Errorf("cannot hydrate because application %s %s", keys[0], remainder)
|
||||
}
|
||||
|
||||
// IsRootPath returns whether the path references a root path
|
||||
func IsRootPath(path string) bool {
|
||||
clean := filepath.Clean(path)
|
||||
return clean == "" || clean == "." || clean == string(filepath.Separator)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
103
controller/hydrator/mocks/Dependencies.go
generated
103
controller/hydrator/mocks/Dependencies.go
generated
@@ -81,6 +81,59 @@ func (_c *Dependencies_AddHydrationQueueItem_Call) RunAndReturn(run func(key typ
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetHydratorCommitMessageTemplate provides a mock function for the type Dependencies
|
||||
func (_mock *Dependencies) GetHydratorCommitMessageTemplate() (string, error) {
|
||||
ret := _mock.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetHydratorCommitMessageTemplate")
|
||||
}
|
||||
|
||||
var r0 string
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func() (string, error)); ok {
|
||||
return returnFunc()
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = returnFunc()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = returnFunc()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Dependencies_GetHydratorCommitMessageTemplate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetHydratorCommitMessageTemplate'
|
||||
type Dependencies_GetHydratorCommitMessageTemplate_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetHydratorCommitMessageTemplate is a helper method to define mock.On call
|
||||
func (_e *Dependencies_Expecter) GetHydratorCommitMessageTemplate() *Dependencies_GetHydratorCommitMessageTemplate_Call {
|
||||
return &Dependencies_GetHydratorCommitMessageTemplate_Call{Call: _e.mock.On("GetHydratorCommitMessageTemplate")}
|
||||
}
|
||||
|
||||
func (_c *Dependencies_GetHydratorCommitMessageTemplate_Call) Run(run func()) *Dependencies_GetHydratorCommitMessageTemplate_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Dependencies_GetHydratorCommitMessageTemplate_Call) Return(s string, err error) *Dependencies_GetHydratorCommitMessageTemplate_Call {
|
||||
_c.Call.Return(s, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Dependencies_GetHydratorCommitMessageTemplate_Call) RunAndReturn(run func() (string, error)) *Dependencies_GetHydratorCommitMessageTemplate_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetProcessableAppProj provides a mock function for the type Dependencies
|
||||
func (_mock *Dependencies) GetProcessableAppProj(app *v1alpha1.Application) (*v1alpha1.AppProject, error) {
|
||||
ret := _mock.Called(app)
|
||||
@@ -199,8 +252,8 @@ func (_c *Dependencies_GetProcessableApps_Call) RunAndReturn(run func() (*v1alph
|
||||
}
|
||||
|
||||
// GetRepoObjs provides a mock function for the type Dependencies
|
||||
func (_mock *Dependencies) GetRepoObjs(app *v1alpha1.Application, source v1alpha1.ApplicationSource, revision string, project *v1alpha1.AppProject) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error) {
|
||||
ret := _mock.Called(app, source, revision, project)
|
||||
func (_mock *Dependencies) GetRepoObjs(ctx context.Context, app *v1alpha1.Application, source v1alpha1.ApplicationSource, revision string, project *v1alpha1.AppProject) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error) {
|
||||
ret := _mock.Called(ctx, app, source, revision, project)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetRepoObjs")
|
||||
@@ -209,25 +262,25 @@ func (_mock *Dependencies) GetRepoObjs(app *v1alpha1.Application, source v1alpha
|
||||
var r0 []*unstructured.Unstructured
|
||||
var r1 *apiclient.ManifestResponse
|
||||
var r2 error
|
||||
if returnFunc, ok := ret.Get(0).(func(*v1alpha1.Application, v1alpha1.ApplicationSource, string, *v1alpha1.AppProject) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error)); ok {
|
||||
return returnFunc(app, source, revision, project)
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, *v1alpha1.Application, v1alpha1.ApplicationSource, string, *v1alpha1.AppProject) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error)); ok {
|
||||
return returnFunc(ctx, app, source, revision, project)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(*v1alpha1.Application, v1alpha1.ApplicationSource, string, *v1alpha1.AppProject) []*unstructured.Unstructured); ok {
|
||||
r0 = returnFunc(app, source, revision, project)
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, *v1alpha1.Application, v1alpha1.ApplicationSource, string, *v1alpha1.AppProject) []*unstructured.Unstructured); ok {
|
||||
r0 = returnFunc(ctx, app, source, revision, project)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*unstructured.Unstructured)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(*v1alpha1.Application, v1alpha1.ApplicationSource, string, *v1alpha1.AppProject) *apiclient.ManifestResponse); ok {
|
||||
r1 = returnFunc(app, source, revision, project)
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, *v1alpha1.Application, v1alpha1.ApplicationSource, string, *v1alpha1.AppProject) *apiclient.ManifestResponse); ok {
|
||||
r1 = returnFunc(ctx, app, source, revision, project)
|
||||
} else {
|
||||
if ret.Get(1) != nil {
|
||||
r1 = ret.Get(1).(*apiclient.ManifestResponse)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(2).(func(*v1alpha1.Application, v1alpha1.ApplicationSource, string, *v1alpha1.AppProject) error); ok {
|
||||
r2 = returnFunc(app, source, revision, project)
|
||||
if returnFunc, ok := ret.Get(2).(func(context.Context, *v1alpha1.Application, v1alpha1.ApplicationSource, string, *v1alpha1.AppProject) error); ok {
|
||||
r2 = returnFunc(ctx, app, source, revision, project)
|
||||
} else {
|
||||
r2 = ret.Error(2)
|
||||
}
|
||||
@@ -240,37 +293,43 @@ type Dependencies_GetRepoObjs_Call struct {
|
||||
}
|
||||
|
||||
// GetRepoObjs is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - app *v1alpha1.Application
|
||||
// - source v1alpha1.ApplicationSource
|
||||
// - revision string
|
||||
// - project *v1alpha1.AppProject
|
||||
func (_e *Dependencies_Expecter) GetRepoObjs(app interface{}, source interface{}, revision interface{}, project interface{}) *Dependencies_GetRepoObjs_Call {
|
||||
return &Dependencies_GetRepoObjs_Call{Call: _e.mock.On("GetRepoObjs", app, source, revision, project)}
|
||||
func (_e *Dependencies_Expecter) GetRepoObjs(ctx interface{}, app interface{}, source interface{}, revision interface{}, project interface{}) *Dependencies_GetRepoObjs_Call {
|
||||
return &Dependencies_GetRepoObjs_Call{Call: _e.mock.On("GetRepoObjs", ctx, app, source, revision, project)}
|
||||
}
|
||||
|
||||
func (_c *Dependencies_GetRepoObjs_Call) Run(run func(app *v1alpha1.Application, source v1alpha1.ApplicationSource, revision string, project *v1alpha1.AppProject)) *Dependencies_GetRepoObjs_Call {
|
||||
func (_c *Dependencies_GetRepoObjs_Call) Run(run func(ctx context.Context, app *v1alpha1.Application, source v1alpha1.ApplicationSource, revision string, project *v1alpha1.AppProject)) *Dependencies_GetRepoObjs_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 *v1alpha1.Application
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(*v1alpha1.Application)
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 v1alpha1.ApplicationSource
|
||||
var arg1 *v1alpha1.Application
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(v1alpha1.ApplicationSource)
|
||||
arg1 = args[1].(*v1alpha1.Application)
|
||||
}
|
||||
var arg2 string
|
||||
var arg2 v1alpha1.ApplicationSource
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(string)
|
||||
arg2 = args[2].(v1alpha1.ApplicationSource)
|
||||
}
|
||||
var arg3 *v1alpha1.AppProject
|
||||
var arg3 string
|
||||
if args[3] != nil {
|
||||
arg3 = args[3].(*v1alpha1.AppProject)
|
||||
arg3 = args[3].(string)
|
||||
}
|
||||
var arg4 *v1alpha1.AppProject
|
||||
if args[4] != nil {
|
||||
arg4 = args[4].(*v1alpha1.AppProject)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
arg3,
|
||||
arg4,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
@@ -281,7 +340,7 @@ func (_c *Dependencies_GetRepoObjs_Call) Return(unstructureds []*unstructured.Un
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Dependencies_GetRepoObjs_Call) RunAndReturn(run func(app *v1alpha1.Application, source v1alpha1.ApplicationSource, revision string, project *v1alpha1.AppProject) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error)) *Dependencies_GetRepoObjs_Call {
|
||||
func (_c *Dependencies_GetRepoObjs_Call) RunAndReturn(run func(ctx context.Context, app *v1alpha1.Application, source v1alpha1.ApplicationSource, revision string, project *v1alpha1.AppProject) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error)) *Dependencies_GetRepoObjs_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
113
controller/hydrator/mocks/RepoGetter.go
generated
Normal file
113
controller/hydrator/mocks/RepoGetter.go
generated
Normal file
@@ -0,0 +1,113 @@
|
||||
// Code generated by mockery; DO NOT EDIT.
|
||||
// github.com/vektra/mockery
|
||||
// template: testify
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// NewRepoGetter creates a new instance of RepoGetter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewRepoGetter(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *RepoGetter {
|
||||
mock := &RepoGetter{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
|
||||
// RepoGetter is an autogenerated mock type for the RepoGetter type
|
||||
type RepoGetter struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type RepoGetter_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *RepoGetter) EXPECT() *RepoGetter_Expecter {
|
||||
return &RepoGetter_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// GetRepository provides a mock function for the type RepoGetter
|
||||
func (_mock *RepoGetter) GetRepository(ctx context.Context, repoURL string, project string) (*v1alpha1.Repository, error) {
|
||||
ret := _mock.Called(ctx, repoURL, project)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetRepository")
|
||||
}
|
||||
|
||||
var r0 *v1alpha1.Repository
|
||||
var r1 error
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (*v1alpha1.Repository, error)); ok {
|
||||
return returnFunc(ctx, repoURL, project)
|
||||
}
|
||||
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) *v1alpha1.Repository); ok {
|
||||
r0 = returnFunc(ctx, repoURL, project)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*v1alpha1.Repository)
|
||||
}
|
||||
}
|
||||
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok {
|
||||
r1 = returnFunc(ctx, repoURL, project)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// RepoGetter_GetRepository_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRepository'
|
||||
type RepoGetter_GetRepository_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetRepository is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - repoURL string
|
||||
// - project string
|
||||
func (_e *RepoGetter_Expecter) GetRepository(ctx interface{}, repoURL interface{}, project interface{}) *RepoGetter_GetRepository_Call {
|
||||
return &RepoGetter_GetRepository_Call{Call: _e.mock.On("GetRepository", ctx, repoURL, project)}
|
||||
}
|
||||
|
||||
func (_c *RepoGetter_GetRepository_Call) Run(run func(ctx context.Context, repoURL string, project string)) *RepoGetter_GetRepository_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
var arg0 context.Context
|
||||
if args[0] != nil {
|
||||
arg0 = args[0].(context.Context)
|
||||
}
|
||||
var arg1 string
|
||||
if args[1] != nil {
|
||||
arg1 = args[1].(string)
|
||||
}
|
||||
var arg2 string
|
||||
if args[2] != nil {
|
||||
arg2 = args[2].(string)
|
||||
}
|
||||
run(
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
)
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *RepoGetter_GetRepository_Call) Return(repository *v1alpha1.Repository, err error) *RepoGetter_GetRepository_Call {
|
||||
_c.Call.Return(repository, err)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *RepoGetter_GetRepository_Call) RunAndReturn(run func(ctx context.Context, repoURL string, project string) (*v1alpha1.Repository, error)) *RepoGetter_GetRepository_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
@@ -31,7 +31,7 @@ func (ctrl *ApplicationController) GetProcessableApps() (*appv1.ApplicationList,
|
||||
return ctrl.getAppList(metav1.ListOptions{})
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) GetRepoObjs(origApp *appv1.Application, drySource appv1.ApplicationSource, revision string, project *appv1.AppProject) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error) {
|
||||
func (ctrl *ApplicationController) GetRepoObjs(ctx context.Context, origApp *appv1.Application, drySource appv1.ApplicationSource, revision string, project *appv1.AppProject) ([]*unstructured.Unstructured, *apiclient.ManifestResponse, error) {
|
||||
drySources := []appv1.ApplicationSource{drySource}
|
||||
dryRevisions := []string{revision}
|
||||
|
||||
@@ -50,10 +50,19 @@ func (ctrl *ApplicationController) GetRepoObjs(origApp *appv1.Application, drySo
|
||||
delete(app.Annotations, appv1.AnnotationKeyManifestGeneratePaths)
|
||||
|
||||
// FIXME: use cache and revision cache
|
||||
objs, resp, _, err := ctrl.appStateManager.GetRepoObjs(app, drySources, appLabelKey, dryRevisions, true, true, false, project, false)
|
||||
objs, resp, _, err := ctrl.appStateManager.GetRepoObjs(ctx, app, drySources, appLabelKey, dryRevisions, true, true, false, project, false)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get repo objects: %w", err)
|
||||
}
|
||||
trackingMethod, err := ctrl.settingsMgr.GetTrackingMethod()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get tracking method: %w", err)
|
||||
}
|
||||
for _, obj := range objs {
|
||||
if err := argoutil.NewResourceTracking().RemoveAppInstance(obj, trackingMethod); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to remove the app instance value: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(resp) != 1 {
|
||||
return nil, nil, fmt.Errorf("expected one manifest response, got %d", len(resp))
|
||||
@@ -88,3 +97,12 @@ func (ctrl *ApplicationController) PersistAppHydratorStatus(orig *appv1.Applicat
|
||||
func (ctrl *ApplicationController) AddHydrationQueueItem(key types.HydrationQueueKey) {
|
||||
ctrl.hydrationQueue.AddRateLimited(key)
|
||||
}
|
||||
|
||||
func (ctrl *ApplicationController) GetHydratorCommitMessageTemplate() (string, error) {
|
||||
sourceHydratorCommitMessageKey, err := ctrl.settingsMgr.GetSourceHydratorCommitMessageTemplate()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get sourceHydrator commit message template key: %w", err)
|
||||
}
|
||||
|
||||
return sourceHydratorCommitMessageKey, nil
|
||||
}
|
||||
|
||||
123
controller/hydrator_dependencies_test.go
Normal file
123
controller/hydrator_dependencies_test.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/argoproj/argo-cd/v3/common"
|
||||
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
||||
"github.com/argoproj/argo-cd/v3/reposerver/apiclient"
|
||||
"github.com/argoproj/argo-cd/v3/test"
|
||||
"github.com/argoproj/argo-cd/v3/util/settings"
|
||||
)
|
||||
|
||||
func TestGetRepoObjs(t *testing.T) {
|
||||
cm := test.NewConfigMap()
|
||||
cm.SetAnnotations(map[string]string{
|
||||
"custom-annotation": "custom-value",
|
||||
common.AnnotationInstallationID: "id", // tracking annotation should be removed
|
||||
common.AnnotationKeyAppInstance: "my-app", // tracking annotation should be removed
|
||||
})
|
||||
cmBytes, _ := json.Marshal(cm)
|
||||
|
||||
app := newFakeApp()
|
||||
// Enable the manifest-generate-paths annotation and set a synced revision
|
||||
app.SetAnnotations(map[string]string{v1alpha1.AnnotationKeyManifestGeneratePaths: "."})
|
||||
app.Status.Sync = v1alpha1.SyncStatus{
|
||||
Revision: "abc123",
|
||||
Status: v1alpha1.SyncStatusCodeSynced,
|
||||
}
|
||||
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{string(cmBytes)},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
}
|
||||
|
||||
ctrl := newFakeControllerWithResync(&data, time.Minute, nil, errors.New("this should not be called"))
|
||||
source := app.Spec.GetSource()
|
||||
source.RepoURL = "oci://example.com/argo/argo-cd"
|
||||
|
||||
objs, resp, err := ctrl.GetRepoObjs(t.Context(), app, source, "abc123", &v1alpha1.AppProject{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "default",
|
||||
Namespace: test.FakeArgoCDNamespace,
|
||||
},
|
||||
Spec: v1alpha1.AppProjectSpec{
|
||||
SourceRepos: []string{"*"},
|
||||
Destinations: []v1alpha1.ApplicationDestination{
|
||||
{
|
||||
Server: "*",
|
||||
Namespace: "*",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, resp)
|
||||
assert.Equal(t, "abc123", resp.Revision)
|
||||
assert.Len(t, objs, 1)
|
||||
|
||||
annotations := objs[0].GetAnnotations()
|
||||
|
||||
// only the tracking annotations set by Argo CD should be removed
|
||||
// and not the custom annotations set by user
|
||||
require.NotNil(t, annotations)
|
||||
assert.Equal(t, "custom-value", annotations["custom-annotation"])
|
||||
assert.NotContains(t, annotations, common.AnnotationInstallationID)
|
||||
assert.NotContains(t, annotations, common.AnnotationKeyAppInstance)
|
||||
|
||||
assert.Equal(t, "ConfigMap", objs[0].GetKind())
|
||||
}
|
||||
|
||||
func TestGetHydratorCommitMessageTemplate_WhenTemplateisNotDefined_FallbackToDefault(t *testing.T) {
|
||||
cm := test.NewConfigMap()
|
||||
cmBytes, _ := json.Marshal(cm)
|
||||
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{string(cmBytes)},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
}
|
||||
|
||||
ctrl := newFakeControllerWithResync(&data, time.Minute, nil, errors.New("this should not be called"))
|
||||
|
||||
tmpl, err := ctrl.GetHydratorCommitMessageTemplate()
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, tmpl) // should fallback to default
|
||||
assert.Equal(t, settings.CommitMessageTemplate, tmpl)
|
||||
}
|
||||
|
||||
func TestGetHydratorCommitMessageTemplate(t *testing.T) {
|
||||
cm := test.NewFakeConfigMap()
|
||||
cm.Data["sourceHydrator.commitMessageTemplate"] = settings.CommitMessageTemplate
|
||||
cmBytes, _ := json.Marshal(cm)
|
||||
|
||||
data := fakeData{
|
||||
manifestResponse: &apiclient.ManifestResponse{
|
||||
Manifests: []string{string(cmBytes)},
|
||||
Namespace: test.FakeDestNamespace,
|
||||
Server: test.FakeClusterURL,
|
||||
Revision: "abc123",
|
||||
},
|
||||
configMapData: cm.Data,
|
||||
}
|
||||
|
||||
ctrl := newFakeControllerWithResync(&data, time.Minute, nil, errors.New("this should not be called"))
|
||||
|
||||
tmpl, err := ctrl.GetHydratorCommitMessageTemplate()
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, tmpl)
|
||||
}
|
||||
@@ -37,8 +37,7 @@ type Consistent struct {
|
||||
loadMap map[string]*Host
|
||||
totalLoad int64
|
||||
replicationFactor int
|
||||
|
||||
sync.RWMutex
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
type item struct {
|
||||
@@ -68,8 +67,8 @@ func NewWithReplicationFactor(replicationFactor int) *Consistent {
|
||||
}
|
||||
|
||||
func (c *Consistent) Add(server string) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
if _, ok := c.loadMap[server]; ok {
|
||||
return
|
||||
@@ -87,8 +86,8 @@ func (c *Consistent) Add(server string) {
|
||||
// As described in https://en.wikipedia.org/wiki/Consistent_hashing
|
||||
// It returns ErrNoHosts if the ring has no servers in it.
|
||||
func (c *Consistent) Get(client string) (string, error) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
|
||||
if c.clients.Len() == 0 {
|
||||
return "", ErrNoHosts
|
||||
@@ -116,8 +115,8 @@ func (c *Consistent) Get(client string) (string, error) {
|
||||
// https://research.googleblog.com/2017/04/consistent-hashing-with-bounded-loads.html
|
||||
// It returns ErrNoHosts if the ring has no hosts in it.
|
||||
func (c *Consistent) GetLeast(client string) (string, error) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
|
||||
if c.clients.Len() == 0 {
|
||||
return "", ErrNoHosts
|
||||
@@ -151,8 +150,8 @@ func (c *Consistent) GetLeast(client string) (string, error) {
|
||||
|
||||
// Sets the load of `server` to the given `load`
|
||||
func (c *Consistent) UpdateLoad(server string, load int64) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
if _, ok := c.loadMap[server]; !ok {
|
||||
return
|
||||
@@ -166,8 +165,8 @@ func (c *Consistent) UpdateLoad(server string, load int64) {
|
||||
//
|
||||
// should only be used with if you obtained a host with GetLeast
|
||||
func (c *Consistent) Inc(server string) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
if _, ok := c.loadMap[server]; !ok {
|
||||
return
|
||||
@@ -180,8 +179,8 @@ func (c *Consistent) Inc(server string) {
|
||||
//
|
||||
// should only be used with if you obtained a host with GetLeast
|
||||
func (c *Consistent) Done(server string) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
if _, ok := c.loadMap[server]; !ok {
|
||||
return
|
||||
@@ -192,8 +191,8 @@ func (c *Consistent) Done(server string) {
|
||||
|
||||
// Deletes host from the ring
|
||||
func (c *Consistent) Remove(server string) bool {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
|
||||
for i := 0; i < c.replicationFactor; i++ {
|
||||
h := c.hash(fmt.Sprintf("%s%d", server, i))
|
||||
@@ -206,8 +205,8 @@ func (c *Consistent) Remove(server string) bool {
|
||||
|
||||
// Return the list of servers in the ring
|
||||
func (c *Consistent) Servers() (servers []string) {
|
||||
c.RLock()
|
||||
defer c.RUnlock()
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
for k := range c.loadMap {
|
||||
servers = append(servers, k)
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ type managedResource struct {
|
||||
type AppStateManager interface {
|
||||
CompareAppState(app *v1alpha1.Application, project *v1alpha1.AppProject, revisions []string, sources []v1alpha1.ApplicationSource, noCache bool, noRevisionCache bool, localObjects []string, hasMultipleSources bool) (*comparisonResult, error)
|
||||
SyncAppState(app *v1alpha1.Application, project *v1alpha1.AppProject, state *v1alpha1.OperationState)
|
||||
GetRepoObjs(app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache, verifySignature bool, proj *v1alpha1.AppProject, sendRuntimeState bool) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, bool, error)
|
||||
GetRepoObjs(ctx context.Context, app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache, verifySignature bool, proj *v1alpha1.AppProject, sendRuntimeState bool) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, bool, error)
|
||||
}
|
||||
|
||||
// comparisonResult holds the state of an application after the reconciliation
|
||||
@@ -127,9 +127,9 @@ type appStateManager struct {
|
||||
// task to the repo-server. It returns the list of generated manifests as unstructured
|
||||
// objects. It also returns the full response from all calls to the repo server as the
|
||||
// second argument.
|
||||
func (m *appStateManager) GetRepoObjs(app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache, verifySignature bool, proj *v1alpha1.AppProject, sendRuntimeState bool) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, bool, error) {
|
||||
func (m *appStateManager) GetRepoObjs(ctx context.Context, app *v1alpha1.Application, sources []v1alpha1.ApplicationSource, appLabelKey string, revisions []string, noCache, noRevisionCache, verifySignature bool, proj *v1alpha1.AppProject, sendRuntimeState bool) ([]*unstructured.Unstructured, []*apiclient.ManifestResponse, bool, error) {
|
||||
ts := stats.NewTimingStats()
|
||||
helmRepos, err := m.db.ListHelmRepositories(context.Background())
|
||||
helmRepos, err := m.db.ListHelmRepositories(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, false, fmt.Errorf("failed to list Helm repositories: %w", err)
|
||||
}
|
||||
@@ -138,7 +138,7 @@ func (m *appStateManager) GetRepoObjs(app *v1alpha1.Application, sources []v1alp
|
||||
return nil, nil, false, fmt.Errorf("failed to get permitted Helm repositories for project %q: %w", proj.Name, err)
|
||||
}
|
||||
|
||||
ociRepos, err := m.db.ListOCIRepositories(context.Background())
|
||||
ociRepos, err := m.db.ListOCIRepositories(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, false, fmt.Errorf("failed to list OCI repositories: %w", err)
|
||||
}
|
||||
@@ -148,7 +148,7 @@ func (m *appStateManager) GetRepoObjs(app *v1alpha1.Application, sources []v1alp
|
||||
}
|
||||
|
||||
ts.AddCheckpoint("repo_ms")
|
||||
helmRepositoryCredentials, err := m.db.GetAllHelmRepositoryCredentials(context.Background())
|
||||
helmRepositoryCredentials, err := m.db.GetAllHelmRepositoryCredentials(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, false, fmt.Errorf("failed to get Helm credentials: %w", err)
|
||||
}
|
||||
@@ -157,7 +157,7 @@ func (m *appStateManager) GetRepoObjs(app *v1alpha1.Application, sources []v1alp
|
||||
return nil, nil, false, fmt.Errorf("failed to get permitted Helm credentials for project %q: %w", proj.Name, err)
|
||||
}
|
||||
|
||||
ociRepositoryCredentials, err := m.db.GetAllOCIRepositoryCredentials(context.Background())
|
||||
ociRepositoryCredentials, err := m.db.GetAllOCIRepositoryCredentials(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, false, fmt.Errorf("failed to get OCI credentials: %w", err)
|
||||
}
|
||||
@@ -192,7 +192,7 @@ func (m *appStateManager) GetRepoObjs(app *v1alpha1.Application, sources []v1alp
|
||||
return nil, nil, false, fmt.Errorf("failed to get installation ID: %w", err)
|
||||
}
|
||||
|
||||
destCluster, err := argo.GetDestinationCluster(context.Background(), app.Spec.Destination, m.db)
|
||||
destCluster, err := argo.GetDestinationCluster(ctx, app.Spec.Destination, m.db)
|
||||
if err != nil {
|
||||
return nil, nil, false, fmt.Errorf("failed to get destination cluster: %w", err)
|
||||
}
|
||||
@@ -218,7 +218,7 @@ func (m *appStateManager) GetRepoObjs(app *v1alpha1.Application, sources []v1alp
|
||||
// Store the map of all sources having ref field into a map for applications with sources field
|
||||
// If it's for a rollback process, the refSources[*].targetRevision fields are the desired
|
||||
// revisions for the rollback
|
||||
refSources, err := argo.GetRefSources(context.Background(), sources, app.Spec.Project, m.db.GetRepository, revisions)
|
||||
refSources, err := argo.GetRefSources(ctx, sources, app.Spec.Project, m.db.GetRepository, revisions)
|
||||
if err != nil {
|
||||
return nil, nil, false, fmt.Errorf("failed to get ref sources: %w", err)
|
||||
}
|
||||
@@ -231,7 +231,7 @@ func (m *appStateManager) GetRepoObjs(app *v1alpha1.Application, sources []v1alp
|
||||
if len(revisions) < len(sources) || revisions[i] == "" {
|
||||
revisions[i] = source.TargetRevision
|
||||
}
|
||||
repo, err := m.db.GetRepository(context.Background(), source.RepoURL, proj.Name)
|
||||
repo, err := m.db.GetRepository(ctx, source.RepoURL, proj.Name)
|
||||
if err != nil {
|
||||
return nil, nil, false, fmt.Errorf("failed to get repo %q: %w", source.RepoURL, err)
|
||||
}
|
||||
@@ -255,12 +255,12 @@ func (m *appStateManager) GetRepoObjs(app *v1alpha1.Application, sources []v1alp
|
||||
|
||||
if !source.IsHelm() && !source.IsOCI() && syncedRevision != "" && keyManifestGenerateAnnotationExists && keyManifestGenerateAnnotationVal != "" {
|
||||
// Validate the manifest-generate-path annotation to avoid generating manifests if it has not changed.
|
||||
updateRevisionResult, err := repoClient.UpdateRevisionForPaths(context.Background(), &apiclient.UpdateRevisionForPathsRequest{
|
||||
updateRevisionResult, err := repoClient.UpdateRevisionForPaths(ctx, &apiclient.UpdateRevisionForPathsRequest{
|
||||
Repo: repo,
|
||||
Revision: revision,
|
||||
SyncedRevision: syncedRevision,
|
||||
NoRevisionCache: noRevisionCache,
|
||||
Paths: path.GetAppRefreshPaths(app),
|
||||
Paths: path.GetSourceRefreshPaths(app, source),
|
||||
AppLabelKey: appLabelKey,
|
||||
AppName: app.InstanceName(m.namespace),
|
||||
Namespace: appNamespace,
|
||||
@@ -301,7 +301,7 @@ func (m *appStateManager) GetRepoObjs(app *v1alpha1.Application, sources []v1alp
|
||||
}
|
||||
|
||||
log.Debugf("Generating Manifest for source %s revision %s", source, revision)
|
||||
manifestInfo, err := repoClient.GenerateManifest(context.Background(), &apiclient.ManifestRequest{
|
||||
manifestInfo, err := repoClient.GenerateManifest(ctx, &apiclient.ManifestRequest{
|
||||
Repo: repo,
|
||||
Repos: repos,
|
||||
Revision: revision,
|
||||
@@ -558,7 +558,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
|
||||
appLabelKey, resourceOverrides, resFilter, installationID, trackingMethod, err := m.getComparisonSettings()
|
||||
ts.AddCheckpoint("settings_ms")
|
||||
if err != nil {
|
||||
// return unknown comparison result if basic comparison settings cannot be loaded
|
||||
log.Infof("Basic comparison settings cannot be loaded, using unknown comparison: %s", err.Error())
|
||||
return &comparisonResult{syncStatus: syncStatus, healthStatus: health.HealthStatusUnknown}, nil
|
||||
}
|
||||
|
||||
@@ -594,7 +594,7 @@ func (m *appStateManager) CompareAppState(app *v1alpha1.Application, project *v1
|
||||
}
|
||||
}
|
||||
|
||||
targetObjs, manifestInfos, revisionsMayHaveChanges, err = m.GetRepoObjs(app, sources, appLabelKey, revisions, noCache, noRevisionCache, verifySignature, project, true)
|
||||
targetObjs, manifestInfos, revisionsMayHaveChanges, err = m.GetRepoObjs(context.Background(), app, sources, appLabelKey, revisions, noCache, noRevisionCache, verifySignature, project, true)
|
||||
if err != nil {
|
||||
targetObjs = make([]*unstructured.Unstructured, 0)
|
||||
msg := "Failed to load target state: " + err.Error()
|
||||
|
||||
@@ -1845,6 +1845,6 @@ func TestCompareAppState_DoesNotCallUpdateRevisionForPaths_ForOCI(t *testing.T)
|
||||
sources := make([]v1alpha1.ApplicationSource, 0)
|
||||
sources = append(sources, source)
|
||||
|
||||
_, _, _, err := ctrl.appStateManager.GetRepoObjs(app, sources, "abc123", []string{"123456"}, false, false, false, &defaultProj, false)
|
||||
_, _, _, err := ctrl.appStateManager.GetRepoObjs(t.Context(), app, sources, "abc123", []string{"123456"}, false, false, false, &defaultProj, false)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ applied to all new issues in our tracker.
|
||||
|
||||
If it is not yet possible to classify the bug, i.e. because more information is
|
||||
required to correctly classify the bug, you should always set the label
|
||||
`bug/in-triage` to make it clear that triage process has started but could not
|
||||
`triage/pending` to make it clear that triage process has started but could not
|
||||
yet be completed.
|
||||
|
||||
### Issue type
|
||||
|
||||
@@ -7,7 +7,7 @@ You can find the Swagger docs by setting the path to `/swagger-ui` in your Argo
|
||||
You'll need to authorize your API using a bearer token. To get a token:
|
||||
|
||||
```bash
|
||||
$ curl $ARGOCD_SERVER/api/v1/session -d $'{"username":"admin","password":"password"}'
|
||||
$ curl -H "Content-Type: application/json" $ARGOCD_SERVER/api/v1/session -d $'{"username":"admin","password":"password"}'
|
||||
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1Njc4MTIzODcsImlzcyI6ImFyZ29jZCIsIm5iZiI6MTU2NzgxMjM4Nywic3ViIjoiYWRtaW4ifQ.ejyTgFxLhuY9mOBtKhcnvobg3QZXJ4_RusN_KIdVwao"}
|
||||
```
|
||||
|
||||
@@ -29,4 +29,4 @@ is specified, and the specified Application does not exist, the API will return
|
||||
|
||||
Additionally, if the `project` query string parameter is specified and the Application exists but is not in
|
||||
the given `project`, the API will return a `403` error. This is to prevent leaking information about the
|
||||
existence of Applications to users who do not have access to them.
|
||||
existence of Applications to users who do not have access to them.
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
# Contributors Quick-Start
|
||||
|
||||
This guide is a starting point for first-time contributors running Argo CD locally for the first time.
|
||||
|
||||
It skips advanced topics such as codegen, which are covered in the [running locally guide](running-locally.md)
|
||||
and the [toolchain guide](toolchain-guide.md).
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Before starting, ensure you have the following tools installed with the specified minimum versions:
|
||||
|
||||
* Git (v2.0.0+)
|
||||
* Go (version specified in `go.mod` - check with `go version`)
|
||||
* Docker (v20.10.0+) Or Podman (v3.0.0+)
|
||||
* Kind (v0.11.0+) Or Minikube (v1.23.0+)
|
||||
* Yarn (v1.22.0+)
|
||||
* Goreman (latest version)
|
||||
|
||||
### Fork and Clone the Repository
|
||||
|
||||
1. Fork the Argo CD repository to your personal GitHub Account
|
||||
|
||||
2. Clone the forked repository:
|
||||
```shell
|
||||
git clone https://github.com/YOUR-USERNAME/argo-cd.git
|
||||
```
|
||||
|
||||
Please note that the local build process uses GOPATH and that path should not be used, unless the Argo CD repository was directly cloned in it.
|
||||
|
||||
3. Add the upstream remote for rebasing:
|
||||
```shell
|
||||
cd argo-cd
|
||||
git remote add upstream https://github.com/argoproj/argo-cd.git
|
||||
```
|
||||
|
||||
### Install Required Tools
|
||||
|
||||
1. Install development tools:
|
||||
```shell
|
||||
make install-go-tools-local
|
||||
make install-codegen-tools-local
|
||||
```
|
||||
|
||||
### Install Go
|
||||
|
||||
<https://go.dev/doc/install/>
|
||||
|
||||
Install Go with a version equal to or greater than the version listed in `go.mod` (verify go version with `go version`).
|
||||
|
||||
|
||||
### Install Docker or Podman
|
||||
|
||||
#### Installation guide for docker:
|
||||
|
||||
<https://docs.docker.com/engine/install/>
|
||||
|
||||
#### Installation guide for podman:
|
||||
|
||||
<https://podman.io/docs/installation>
|
||||
|
||||
### Install or Upgrade a Tool for Running Local Clusters (e.g. kind or minikube)
|
||||
|
||||
#### Installation guide for kind:
|
||||
|
||||
<https://kind.sigs.k8s.io/docs/user/quick-start/>
|
||||
|
||||
#### Installation guide for minikube:
|
||||
|
||||
<https://minikube.sigs.k8s.io/docs/start/>
|
||||
|
||||
### Start Your Local Cluster
|
||||
|
||||
For example, if you are using kind:
|
||||
```shell
|
||||
kind create cluster
|
||||
```
|
||||
|
||||
Or, if you are using minikube:
|
||||
|
||||
```shell
|
||||
minikube start
|
||||
```
|
||||
|
||||
Or, if you are using minikube with podman driver:
|
||||
|
||||
```shell
|
||||
minikube start --driver=podman
|
||||
```
|
||||
|
||||
### Install Argo CD
|
||||
|
||||
```shell
|
||||
kubectl create namespace argocd &&
|
||||
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/master/manifests/install.yaml
|
||||
```
|
||||
|
||||
Set kubectl config to avoid specifying the namespace in every kubectl command.
|
||||
All following commands in this guide assume the namespace is already set.
|
||||
|
||||
```shell
|
||||
kubectl config set-context --current --namespace=argocd
|
||||
```
|
||||
|
||||
### Install `yarn`
|
||||
|
||||
<https://classic.yarnpkg.com/lang/en/docs/install/>
|
||||
|
||||
### Install `goreman`
|
||||
|
||||
<https://github.com/mattn/goreman#getting-started>
|
||||
|
||||
### Run Argo CD
|
||||
|
||||
```shell
|
||||
cd argo-cd
|
||||
make start-local ARGOCD_GPG_ENABLED=false
|
||||
```
|
||||
|
||||
By default, Argo CD uses Docker. To use Podman instead, set the `DOCKER` environment variable to `podman` before running the `make` command:
|
||||
|
||||
```shell
|
||||
cd argo-cd
|
||||
DOCKER=podman make start-local ARGOCD_GPG_ENABLED=false
|
||||
```
|
||||
|
||||
- Navigate to [localhost:4000](http://localhost:4000) in your browser to load the Argo CD UI
|
||||
- It may take a few minutes for the UI to be responsive
|
||||
|
||||
!!! note
|
||||
If the UI is not working, check the logs from `make start-local`. The logs are `DEBUG` level by default. If the logs are
|
||||
too noisy to find the problem, try editing log levels for the commands in the `Procfile` in the root of the Argo CD repo.
|
||||
|
||||
## Common Make Targets
|
||||
|
||||
Here are some frequently used make targets (all will run on your machine):
|
||||
|
||||
### Local Toolchain Make Targets
|
||||
|
||||
* `make build-local` - Build Argo CD binaries
|
||||
* `make test-local` - Run unit tests
|
||||
* `make codegen-local` - Re-generate auto generated Swagger and Protobuf (after changing API code)
|
||||
* `make lint-local` - Run linting
|
||||
* `make pre-commit-local` - Run pre-commit checks
|
||||
* `make start-e2e-local` - Start server for end-to-end tests
|
||||
* `make test-e2e-local` - Run end-to-end tests
|
||||
* `make serve-docs-local` - Serve documentation
|
||||
* `make start-local` - Start Argo CD
|
||||
|
||||
### Virtualized Toolchain Make Targets
|
||||
|
||||
* `make build` - Build Argo CD binaries
|
||||
* `make test` - Run unit tests
|
||||
* `make codegen` - Re-generate auto generated Swagger and Protobuf (after changing API code)
|
||||
* `make lint` - Run linting
|
||||
* `make pre-commit` - Run pre-commit checks
|
||||
* `make start-e2e` - Start server for end-to-end tests
|
||||
* `make test-e2e` - Run end-to-end tests
|
||||
* `make serve-docs` - Serve documentation
|
||||
* `make start` - Start Argo CD
|
||||
|
||||
## Making Changes
|
||||
|
||||
### Before Submitting a PR
|
||||
|
||||
1. Rebase your branch against upstream main:
|
||||
```shell
|
||||
git fetch upstream
|
||||
git rebase upstream/main
|
||||
```
|
||||
|
||||
2. Run pre-commit checks:
|
||||
```shell
|
||||
make pre-commit-local
|
||||
```
|
||||
|
||||
### Docs Changes
|
||||
|
||||
Modifying the docs auto-reloads the changes on the [documentation website](https://argo-cd.readthedocs.io/) that can be locally built using `make serve-docs` command.
|
||||
Once running, you can view your locally built documentation on port 8000.
|
||||
|
||||
Read more about this [here](https://argo-cd.readthedocs.io/en/latest/developer-guide/docs-site/).
|
||||
|
||||
### UI Changes
|
||||
|
||||
Modifying the User-Interface (by editing .tsx or .scss files) auto-reloads the changes on port 4000.
|
||||
|
||||
### Backend Changes
|
||||
|
||||
Modifying the API server, repo server, or a controller requires restarting the current `make start-local` session to reflect the changes.
|
||||
|
||||
### CLI Changes
|
||||
|
||||
Modifying the CLI requires restarting the current `make start-local` session to reflect the changes.
|
||||
|
||||
To test most CLI commands, you will need to log in.
|
||||
|
||||
First, get the auto-generated secret:
|
||||
|
||||
```shell
|
||||
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d; echo
|
||||
```
|
||||
|
||||
Then log in using that password and username `admin`:
|
||||
|
||||
```shell
|
||||
dist/argocd login localhost:8080
|
||||
```
|
||||
|
||||
---
|
||||
Congrats on making it to the end of this runbook! 🚀
|
||||
|
||||
For more on Argo CD, find us in Slack - <https://slack.cncf.io/> [#argo-contributors](https://cloud-native.slack.com/archives/C020XM04CUW)
|
||||
146
docs/developer-guide/debugging-locally.md
Normal file
146
docs/developer-guide/debugging-locally.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# Debugging a local Argo CD instance
|
||||
|
||||
## Prerequisites
|
||||
1. [Development Environment](development-environment.md)
|
||||
2. [Toolchain Guide](toolchain-guide.md)
|
||||
3. [Development Cycle](development-cycle.md)
|
||||
4. [Running Locally](running-locally.md)
|
||||
|
||||
## Preface
|
||||
Please make sure you are familiar with running Argo CD locally using the [local toolchain](running-locally.md#start-local-services-local-toolchain).
|
||||
|
||||
When running Argo CD locally for manual tests, the quickest way to do so is to run all the Argo CD components together, as described in [Running Locally](running-locally.md),
|
||||
|
||||
However, when you need to debug a single Argo CD component (for example, `api-server`, `repo-server`, etc), you will need to run this component separately in your IDE, using your IDE launch and debug configuration, while the other components will be running as described previously, using the local toolchain.
|
||||
|
||||
For the next steps, we will use Argo CD `api-server` as an example of running a component in an IDE.
|
||||
|
||||
## Configure your IDE
|
||||
|
||||
### Locate your component configuration in `Procfile`
|
||||
The `Procfile` is used by Goreman when running Argo CD locally with the local toolchain. The file is located in the top-level directory in your cloned Argo CD repo folder, you can view it's latest version [here](https://github.com/argoproj/argo-cd/blob/master/Procfile). It contains all the needed component run configuration, and you will need to copy parts of this configuration to your IDE.
|
||||
|
||||
Example for `api-server` configuration in `Procfile`:
|
||||
``` text
|
||||
api-server: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "GOCOVERDIR=${ARGOCD_COVERAGE_DIR:-/tmp/coverage/api-server} FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-server $COMMAND --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --disable-auth=${ARGOCD_E2E_DISABLE_AUTH:-'true'} --insecure --dex-server http://localhost:${ARGOCD_E2E_DEX_PORT:-5556} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --port ${ARGOCD_E2E_APISERVER_PORT:-8080} --otlp-address=${ARGOCD_OTLP_ADDRESS} --application-namespaces=${ARGOCD_APPLICATION_NAMESPACES:-''} --hydrator-enabled=${ARGOCD_HYDRATOR_ENABLED:='false'}"
|
||||
```
|
||||
This configuration example will be used as the basis for the next steps.
|
||||
|
||||
!!! note
|
||||
The Procfile for a component may change with time. Please go through the Procfile and make sure you use the latest configuration for debugging.
|
||||
|
||||
### Configure component env variables
|
||||
The component that you will run in your IDE for debugging (`api-server` in our case) will need env variables. Copy the env variables from `Procfile`, located in the `argo-cd` root folder of your development branch. The env variables are located before the `$COMMAND` section in the `sh -c` section of the component run command.
|
||||
You can keep them in `.env` file and then have the IDE launch configuration point to that file. Obviously, you can adjust the env variables to your needs when debugging a specific configuration.
|
||||
|
||||
Example for an `api-server.env` file:
|
||||
``` bash
|
||||
ARGOCD_BINARY_NAME=argocd-server
|
||||
ARGOCD_FAKE_IN_CLUSTER=true
|
||||
ARGOCD_GNUPGHOME=/tmp/argocd-local/gpg/keys
|
||||
ARGOCD_GPG_DATA_PATH=/tmp/argocd-local/gpg/source
|
||||
ARGOCD_GPG_ENABLED=false
|
||||
ARGOCD_LOG_FORMAT_ENABLE_FULL_TIMESTAMP=1
|
||||
ARGOCD_SSH_DATA_PATH=/tmp/argocd-local/ssh
|
||||
ARGOCD_TLS_DATA_PATH=/tmp/argocd-local/tls
|
||||
ARGOCD_TRACING_ENABLED=1
|
||||
FORCE_LOG_COLORS=1
|
||||
KUBECONFIG=/Users/<YOUR_USERNAME>/.kube/config # Must be an absolute full path
|
||||
...
|
||||
# and so on, for example: when you test the app-in-any-namespace feature,
|
||||
# you'll need to add ARGOCD_APPLICATION_NAMESPACES to this list
|
||||
# only for testing this functionality and remove it afterwards.
|
||||
```
|
||||
|
||||
### Install DotENV / EnvFile plugin
|
||||
Using the market place / plugin manager of your IDE. The below example configurations require the plugin to be installed.
|
||||
|
||||
|
||||
### Configure component IDE launch configuration
|
||||
#### VSCode example
|
||||
Next, you will need to create a launch configuration, with the relevant args. Copy the args from `Procfile`, located in the `argo-cd` root folder of your development branch. The args are located after the `$COMMAND` section in the `sh -c` section of the component run command.
|
||||
Example for an `api-server` launch configuration, based on our above example for `api-server` configuration in `Procfile`:
|
||||
``` json
|
||||
{
|
||||
"name": "api-server",
|
||||
"type": "go",
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "YOUR_CLONED_ARGO_CD_REPO_PATH/argo-cd/cmd",
|
||||
"args": [
|
||||
"--loglevel",
|
||||
"debug",
|
||||
"--redis",
|
||||
"localhost:6379",
|
||||
"--repo-server",
|
||||
"localhost:8081",
|
||||
"--dex-server",
|
||||
"http://localhost:5556",
|
||||
"--port",
|
||||
"8080",
|
||||
"--insecure"
|
||||
],
|
||||
"envFile": "YOUR_ENV_FILES_PATH/api-server.env", # Assuming you installed DotENV plugin
|
||||
}
|
||||
```
|
||||
|
||||
#### Goland example
|
||||
Next, you will need to create a launch configuration, with the relevant parameters. Copy the parameters from `Procfile`, located in the `argo-cd` root folder of your development branch. The parameters are located after the `$COMMAND` section in the `sh -c` section of the component run command.
|
||||
Example for an `api-server` launch configuration snippet, based on our above example for `api-server` configuration in `Procfile`:
|
||||
``` xml
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="api-server" type="GoApplicationRunConfiguration" factoryName="Go Application">
|
||||
<module name="argo-cd" />
|
||||
<working_directory value="$PROJECT_DIR$" />
|
||||
<parameters value="--loglevel debug --redis localhost:6379 --insecure --dex-server http://localhost:5556 --repo-server localhost:8081 --port 8080" />
|
||||
<EXTENSION ID="net.ashald.envfile"> <!-- Assuming you installed the EnvFile plugin-->
|
||||
<option name="IS_ENABLED" value="true" />
|
||||
<option name="IS_SUBST" value="false" />
|
||||
<option name="IS_PATH_MACRO_SUPPORTED" value="false" />
|
||||
<option name="IS_IGNORE_MISSING_FILES" value="false" />
|
||||
<option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
|
||||
<ENTRIES>
|
||||
<ENTRY IS_ENABLED="true" PARSER="runconfig" IS_EXECUTABLE="false" />
|
||||
<ENTRY IS_ENABLED="true" PARSER="env" IS_EXECUTABLE="false" PATH="<YOUR_ENV_FILES_PATH>/api-server.env" />
|
||||
</ENTRIES>
|
||||
</EXTENSION>
|
||||
<kind value="DIRECTORY" />
|
||||
<directory value="$PROJECT_DIR$/cmd" />
|
||||
<filePath value="$PROJECT_DIR$" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
```
|
||||
|
||||
!!! note
|
||||
As an alternative to importing the above file to Goland, you can create a Run/Debug Configuration using the official [Goland docs](https://www.jetbrains.com/help/go/go-build.html) and just copy the `parameters`, `directory` and `PATH` sections from the example above (specifying `Run kind` as `Directory` in the Run/Debug Configurations wizard)
|
||||
|
||||
## Run Argo CD without the debugged component
|
||||
Next, we need to run all Argo CD components, except for the debugged component (cause we will run this component separately in the IDE).
|
||||
There is a mix-and-match approach to running the other components - you can run them in your K8s cluster or locally with the local toolchain.
|
||||
Below are the different options.
|
||||
|
||||
### Run the other components locally
|
||||
#### Run with "make start-local"
|
||||
`make start-local` runs all the components by default, but it is also possible to run it with a whitelist of components, enabling the separation we need.
|
||||
|
||||
So for the case of debugging the `api-server`, run:
|
||||
`make start-local ARGOCD_START="notification applicationset-controller repo-server redis dex controller ui"`
|
||||
|
||||
#### Run with "make run"
|
||||
`make run` runs all the components by default, but it is also possible to run it with a blacklist of components, enabling the separation we need.
|
||||
|
||||
So for the case of debugging the `api-server`, run:
|
||||
`make run exclude=api-server`
|
||||
|
||||
#### Run with "goreman start"
|
||||
`goreman start` runs all the components by default, but it is also possible to run it with a whitelist of components, enabling separation as needed.
|
||||
|
||||
To debug the `api-server`, run:
|
||||
`goreman start notification applicationset-controller repo-server redis dex controller ui`
|
||||
|
||||
## Run Argo CD debugged component from your IDE
|
||||
Finally, run the component you wish to debug from your IDE and make sure it does not have any errors.
|
||||
|
||||
## Important
|
||||
When running Argo CD components separately, ensure components aren't creating conflicts - each component needs to be up exactly once, be it running locally with the local toolchain or running from your IDE. Otherwise you may get errors about ports not available or even debugging a process that does not contain your code changes.
|
||||
@@ -33,21 +33,61 @@ After your GitOps Engine PR has been merged, ArgoCD needs to be updated to pull
|
||||
- See https://github.com/argoproj/argo-cd/pull/4434 as an example
|
||||
- The PR might require additional, dependent changes in ArgoCD that are directly impacted by the changes made in the engine.
|
||||
|
||||
## Argo UI Components
|
||||
## Notifications Engine (`github.com/argoproj/notifications-engine`)
|
||||
|
||||
### Repository
|
||||
|
||||
https://github.com/argoproj/argo-ui
|
||||
[notifications-engine](https://github.com/argoproj/notifications-engine)
|
||||
|
||||
### Pulling changes from Argo UI into Argo CD
|
||||
### Pulling changes from `notifications-engine`
|
||||
|
||||
If you make changes to the Argo UI component, and your Argo CD changes depend on those changes, follow these steps:
|
||||
After your Notifications Engine PR has been merged, ArgoCD needs to be updated to pull in the version of the notifications engine that contains your change. Here are the steps:
|
||||
|
||||
1. Make changes to Argo UI and submit the PR request.
|
||||
2. Also, prepare your Argo CD changes, but don't create the PR just yet.
|
||||
3. **After** the Argo UI PR has been merged to master, then as part of your Argo CD changes:
|
||||
- Run `yarn add git+https://github.com/argoproj/argo-ui.git` in the `ui/` directory, and then,
|
||||
- Check in the regenerated `yarn.lock` file as part of your Argo CD commit
|
||||
4. Create the Argo CD PR when you are ready. The PR build and test checks should pass.
|
||||
- Retrieve the SHA hash for your commit. You will use this in the next step.
|
||||
- From the `argo-cd` folder, run the following command
|
||||
|
||||
If your Argo UI change is a 'stand-alone' fix, and you simply want Argo CD to pull in your change, then simply create an Argo CD PR with the `yarn.lock` file change.
|
||||
`go get github.com/argoproj/notifications-engine@<git-commit-sha>`
|
||||
|
||||
If you get an error message `invalid version: unknown revision` then you got the wrong SHA hash
|
||||
|
||||
- Run:
|
||||
|
||||
`go mod tidy`
|
||||
|
||||
- The following files are changed:
|
||||
|
||||
- `go.mod`
|
||||
- `go.sum`
|
||||
|
||||
- If your notifications engine PR included docs changes, run `make codegen` or `make codegen-local`.
|
||||
|
||||
- Create an ArgoCD PR with a `refactor:` type in its title for the above file changes.
|
||||
|
||||
## Argo UI Components (`github.com/argoproj/argo-ui`)
|
||||
### Contributing to Argo CD UI
|
||||
|
||||
Argo CD, along with Argo Workflows, uses shared React components from [Argo UI](https://github.com/argoproj/argo-ui). Examples of some of these components include buttons, containers, form controls,
|
||||
and others. Although you can make changes to these files and run them locally, in order to have these changes added to the Argo CD repo, you will need to follow these steps.
|
||||
|
||||
1. Fork and clone the [Argo UI repository](https://github.com/argoproj/argo-ui).
|
||||
|
||||
2. `cd` into your `argo-ui` directory, and then run `yarn install`.
|
||||
|
||||
3. Make your file changes.
|
||||
|
||||
4. Run `yarn start` to start a [storybook](https://storybook.js.org/) dev server and view the components in your browser. Make sure all your changes work as expected.
|
||||
|
||||
5. Use [yarn link](https://classic.yarnpkg.com/en/docs/cli/link/) to link Argo UI package to your Argo CD repository. (Commands below assume that `argo-ui` and `argo-cd` are both located within the same parent folder)
|
||||
|
||||
* `cd argo-ui`
|
||||
* `yarn link`
|
||||
* `cd ../argo-cd/ui`
|
||||
* `yarn link argo-ui`
|
||||
|
||||
Once the `argo-ui` package has been successfully linked, test changes in your local development environment.
|
||||
|
||||
6. Commit changes and open a PR to [Argo UI](https://github.com/argoproj/argo-ui).
|
||||
|
||||
7. Once your PR has been merged in Argo UI, `cd` into your `argo-cd/ui` folder and run `yarn add git+https://github.com/argoproj/argo-ui.git`. This will update the commit SHA in the `ui/yarn.lock` file to use the latest master commit for argo-ui.
|
||||
|
||||
8. Submit changes to `ui/yarn.lock`in a PR to Argo CD.
|
||||
|
||||
119
docs/developer-guide/development-cycle.md
Normal file
119
docs/developer-guide/development-cycle.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# The Development Cycle
|
||||
|
||||
## Prerequisites
|
||||
1. [Development Environment](development-environment.md)
|
||||
2. [Toolchain Guide](toolchain-guide.md)
|
||||
|
||||
## Preface
|
||||
When you have developed and possibly manually tested the code you want to contribute, you should ensure that everything builds correctly. Commit your changes locally and perform the following steps, for each step the commands for both local and virtualized toolchain are listed.
|
||||
|
||||
### Docker priviliges for virtualized toolchain users
|
||||
[These instructions](toolchain-guide.md#docker-privileges) are relevant for most of the steps below
|
||||
|
||||
### Using Podman for virtualized toolchain users
|
||||
[These instructions](toolchain-guide.md#using-podman) are relevant for most of the steps below
|
||||
|
||||
## Development cycle steps
|
||||
### Set kubectl context to argocd namespace
|
||||
|
||||
Setting kubectl config to the argocd namespace is required for these steps to succeed.
|
||||
All following commands in this guide assume the namespace is already set.
|
||||
|
||||
```shell
|
||||
kubectl config set-context --current --namespace=argocd
|
||||
```
|
||||
|
||||
### Pull in all build dependencies
|
||||
|
||||
As build dependencies change over time, you have to synchronize your development environment with the current specification. In order to pull in all required dependencies, issue:
|
||||
|
||||
* `make dep-ui` or `make dep-ui-local`
|
||||
|
||||
Argo CD recently migrated to Go modules. Usually, dependencies will be downloaded at build time, but the Makefile provides two targets to download and vendor all dependencies:
|
||||
|
||||
* `make mod-download` or `make mod-download-local` will download all required Go modules and
|
||||
* `make mod-vendor` or `make mod-vendor-local` will vendor those dependencies into the Argo CD source tree
|
||||
|
||||
### Generate API glue code and other assets
|
||||
|
||||
Argo CD relies on Google's [Protocol Buffers](https://developers.google.com/protocol-buffers) for its API, and this makes heavy use of auto-generated glue code and stubs. Whenever you touched parts of the API code, you must re-generate the auto generated code.
|
||||
|
||||
* Run `make codegen` or `make codegen-local`, this might take a while
|
||||
* Check if something has changed by running `git status` or `git diff`
|
||||
* Commit any possible changes to your local Git branch, an appropriate commit message would be `Changes from codegen`, for example.
|
||||
|
||||
!!!note
|
||||
There are a few non-obvious assets that are auto-generated. You should not change the autogenerated assets, as they will be overwritten by a subsequent run of `make codegen`. Instead, change their source files. Prominent examples of non-obvious auto-generated code are `swagger.json` or the installation manifest YAMLs.
|
||||
|
||||
### Build your code and run unit tests
|
||||
|
||||
After the code glue has been generated, your code should build and the unit tests should run without any errors. Execute the following statements:
|
||||
|
||||
* `make build` or `make build-local`
|
||||
* `make test` or `make test-local`
|
||||
|
||||
These steps are non-modifying, so there's no need to check for changes afterward.
|
||||
|
||||
### Lint your code base
|
||||
|
||||
In order to keep a consistent code style in our source tree, your code must be well-formed in accordance to some widely accepted rules, which are applied by a Linter.
|
||||
|
||||
The Linter might make some automatic changes to your code, such as indentation fixes. Some other errors reported by the Linter have to be fixed manually.
|
||||
|
||||
* Run `make lint` or `make lint-local` and observe any errors reported by the Linter
|
||||
* Fix any of the errors reported and commit to your local branch
|
||||
* Finally, after the Linter reports no errors, run `git status` or `git diff` to check for any changes made automatically by Lint
|
||||
* If there were automatic changes, commit them to your local branch
|
||||
|
||||
If you touched UI code, you should also run the Yarn linter on it:
|
||||
|
||||
* Run `make lint-ui` or `make lint-ui-local`
|
||||
* Fix any of the errors reported by it
|
||||
|
||||
### Run end-to-end tests
|
||||
|
||||
The final step is running the End-to-End testsuite, which ensures that your Kubernetes dependencies are working properly. This will involve starting all the Argo CD components on your computer. The end-to-end tests consists of two parts: a server component, and a client component.
|
||||
|
||||
* First, start the End-to-End server: `make start-e2e` or `make start-e2e-local`. This will spawn a number of processes and services on your system.
|
||||
* When all components have started, run `make test-e2e` or `make test-e2e-local` to run the end-to-end tests against your local services.
|
||||
|
||||
To run a single test with a local toolchain, you can use `TEST_FLAGS="-run TestName" make test-e2e-local`.
|
||||
|
||||
For more information about End-to-End tests, refer to the [End-to-End test documentation](test-e2e.md).
|
||||
|
||||
## Common Make Targets
|
||||
|
||||
Here are some frequently used make targets (all will run on your machine):
|
||||
|
||||
### Local Toolchain Make Targets
|
||||
|
||||
* `make install-tools-local` - Install testing and building tools for the local toolchain
|
||||
* `make build-local` - Build Argo CD binaries
|
||||
* `make test-local` - Run unit tests
|
||||
* `make codegen-local` - Re-generate auto generated Swagger and Protobuf (after changing API code)
|
||||
* `make lint-local` - Run linting
|
||||
* `make pre-commit-local` - Run pre-commit checks
|
||||
* `make start-e2e-local` - Start server for end-to-end tests
|
||||
* `make test-e2e-local` - Run end-to-end tests
|
||||
* `make serve-docs-local` - Serve documentation
|
||||
* `make start-local` - Start Argo CD
|
||||
* `make cli-local` - Build Argo CD CLI binary
|
||||
|
||||
### Virtualized Toolchain Make Targets
|
||||
|
||||
* `make verify-kube-connect` - Test whether the virtualized toolchain has access to your K8s cluster
|
||||
* `make test-tools-image` - Prepare the environment of the virtualized chain
|
||||
* `make build` - Build Argo CD binaries
|
||||
* `make test` - Run unit tests
|
||||
* `make codegen` - Re-generate auto generated Swagger and Protobuf (after changing API code)
|
||||
* `make lint` - Run linting
|
||||
* `make pre-commit` - Run pre-commit checks
|
||||
* `make start-e2e` - Start server for end-to-end tests
|
||||
* `make test-e2e` - Run end-to-end tests
|
||||
* `make serve-docs` - Serve documentation
|
||||
* `make start` - Start Argo CD
|
||||
|
||||
---
|
||||
Congrats on making it to the end of this runbook! 🚀
|
||||
|
||||
For more on Argo CD, find us in Slack - <https://slack.cncf.io/> [#argo-contributors](https://cloud-native.slack.com/archives/C020XM04CUW)
|
||||
120
docs/developer-guide/development-environment.md
Normal file
120
docs/developer-guide/development-environment.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Setting Up the Development Environment
|
||||
|
||||
## Required Tools Overview
|
||||
|
||||
You will need to install the following tools with the specified minimum versions:
|
||||
|
||||
* Git (v2.0.0+)
|
||||
* Go (version specified in `go.mod` - check with `go version`)
|
||||
* Docker (v20.10.0+) Or Podman (v3.0.0+)
|
||||
* Kind (v0.11.0+) Or Minikube (v1.23.0+) Or K3d (v5.7.3+)
|
||||
|
||||
|
||||
|
||||
## Install Required Tools
|
||||
|
||||
### Install Git
|
||||
|
||||
Obviously, you will need a `git` client for pulling source code and pushing back your changes.
|
||||
|
||||
<https://github.com/git-guides/install-git>
|
||||
|
||||
|
||||
### Install Go
|
||||
|
||||
You will need a Go SDK and related tools (such as GNU `make`) installed and working on your development environment.
|
||||
|
||||
<https://go.dev/doc/install/>
|
||||
|
||||
Install Go with a version equal to or greater than the version listed in `go.mod` (verify go version with `go version`).
|
||||
We will assume that your Go workspace is at `~/go`.
|
||||
|
||||
Verify: run `go version`
|
||||
|
||||
### Install Docker or Podman
|
||||
|
||||
#### Installation guide for docker
|
||||
|
||||
<https://docs.docker.com/engine/install/>
|
||||
|
||||
You will need a working Docker runtime environment, to be able to build and run images. Argo CD is using multi-stage builds.
|
||||
|
||||
Verify: run `docker version`
|
||||
|
||||
#### Installation guide for podman
|
||||
|
||||
<https://podman.io/docs/installation>
|
||||
|
||||
### Install a Local K8s Cluster
|
||||
|
||||
You won't need a fully blown multi-master, multi-node cluster, but you will need something like K3S, K3d, Minikube, Kind or microk8s. You will also need a working Kubernetes client (`kubectl`) configuration in your development environment. The configuration must reside in `~/.kube/config`.
|
||||
|
||||
#### Kind
|
||||
|
||||
##### [Installation guide](https://kind.sigs.k8s.io/docs/user/quick-start)
|
||||
|
||||
You can use `kind` to run Kubernetes inside Docker. But pointing to any other development cluster works fine as well as long as Argo CD can reach it.
|
||||
|
||||
##### Start the Cluster
|
||||
```shell
|
||||
kind create cluster
|
||||
```
|
||||
|
||||
#### Minikube
|
||||
|
||||
##### [Installation guide](https://minikube.sigs.k8s.io/docs/start)
|
||||
|
||||
##### Start the Cluster
|
||||
```shell
|
||||
minikube start
|
||||
```
|
||||
|
||||
Or, if you are using minikube with podman driver:
|
||||
|
||||
```shell
|
||||
minikube start --driver=podman
|
||||
```
|
||||
|
||||
#### K3d
|
||||
|
||||
##### [Installation guide](https://k3d.io/stable/#quick-start)
|
||||
|
||||
### Verify cluster installation
|
||||
|
||||
* Run `kubectl version`
|
||||
|
||||
## Fork and Clone the Repository
|
||||
1. Fork the Argo CD repository to your personal GitHub Account
|
||||
2. Clone the forked repository:
|
||||
```shell
|
||||
git clone https://github.com/YOUR-USERNAME/argo-cd.git
|
||||
```
|
||||
Please note that the local build process uses GOPATH and that path should not be used, unless the Argo CD repository was directly cloned in it.
|
||||
|
||||
3. While everyone has their own Git workflow, the author of this document recommends to create a remote called `upstream` in your local copy pointing to the original Argo CD repository. This way, you can easily keep your local branches up-to-date by merging in latest changes from the Argo CD repository, i.e. by doing a `git pull upstream master` in your locally checked out branch.
|
||||
To create the remote, run:
|
||||
```shell
|
||||
cd argo-cd
|
||||
git remote add upstream https://github.com/argoproj/argo-cd.git
|
||||
```
|
||||
|
||||
## Install Additional Required Development Tools
|
||||
|
||||
```shell
|
||||
make install-go-tools-local
|
||||
make install-codegen-tools-local
|
||||
```
|
||||
|
||||
## Install Latest Argo CD on Your Local Cluster
|
||||
|
||||
```shell
|
||||
kubectl create namespace argocd &&
|
||||
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/master/manifests/install.yaml
|
||||
```
|
||||
|
||||
Set kubectl config to avoid specifying the namespace in every kubectl command.
|
||||
|
||||
```shell
|
||||
kubectl config set-context --current --namespace=argocd
|
||||
```
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Proxy Extensions
|
||||
|
||||
!!! warning "Beta Feature (Since 2.7.0)"
|
||||
This feature is in the [Beta](https://github.com/argoproj/argoproj/blob/main/community/feature-status.md#beta) stage.
|
||||
|
||||
This feature is in the [Beta](https://github.com/argoproj/argoproj/blob/main/community/feature-status.md#beta) stage.
|
||||
It is generally considered stable, but there may be unhandled edge cases.
|
||||
|
||||
## Overview
|
||||
@@ -29,7 +30,7 @@ metadata:
|
||||
name: argocd-cmd-params-cm
|
||||
namespace: argocd
|
||||
data:
|
||||
server.enable.proxy.extension: "true"
|
||||
server.enable.proxy.extension: 'true'
|
||||
```
|
||||
|
||||
Once the proxy extension is enabled, it can be configured in the main
|
||||
@@ -102,11 +103,12 @@ respect the new configuration.
|
||||
|
||||
Every configuration entry is explained below:
|
||||
|
||||
#### `extensions` (*list*)
|
||||
#### `extensions` (_list_)
|
||||
|
||||
Defines configurations for all extensions enabled.
|
||||
|
||||
#### `extensions.name` (*string*)
|
||||
#### `extensions.name` (_string_)
|
||||
|
||||
(mandatory)
|
||||
|
||||
Defines the endpoint that will be used to register the extension
|
||||
@@ -116,54 +118,61 @@ following url:
|
||||
|
||||
<argocd-host>/extensions/my-extension
|
||||
|
||||
#### `extensions.backend.connectionTimeout` (*duration string*)
|
||||
#### `extensions.backend.connectionTimeout` (_duration string_)
|
||||
|
||||
(optional. Default: 2s)
|
||||
|
||||
Is the maximum amount of time a dial to the extension server will wait
|
||||
for a connect to complete.
|
||||
for a connect to complete.
|
||||
|
||||
#### `extensions.backend.keepAlive` (_duration string_)
|
||||
|
||||
#### `extensions.backend.keepAlive` (*duration string*)
|
||||
(optional. Default: 15s)
|
||||
|
||||
Specifies the interval between keep-alive probes for an active network
|
||||
connection between the API server and the extension server.
|
||||
|
||||
#### `extensions.backend.idleConnectionTimeout` (*duration string*)
|
||||
#### `extensions.backend.idleConnectionTimeout` (_duration string_)
|
||||
|
||||
(optional. Default: 60s)
|
||||
|
||||
Is the maximum amount of time an idle (keep-alive) connection between
|
||||
the API server and the extension server will remain idle before
|
||||
closing itself.
|
||||
|
||||
#### `extensions.backend.maxIdleConnections` (*int*)
|
||||
#### `extensions.backend.maxIdleConnections` (_int_)
|
||||
|
||||
(optional. Default: 30)
|
||||
|
||||
Controls the maximum number of idle (keep-alive) connections between
|
||||
the API server and the extension server.
|
||||
|
||||
#### `extensions.backend.services` (*list*)
|
||||
#### `extensions.backend.services` (_list_)
|
||||
|
||||
Defines a list with backend url by cluster.
|
||||
|
||||
#### `extensions.backend.services.url` (*string*)
|
||||
#### `extensions.backend.services.url` (_string_)
|
||||
|
||||
(mandatory)
|
||||
|
||||
Is the address where the extension backend must be available.
|
||||
|
||||
#### `extensions.backend.services.headers` (*list*)
|
||||
#### `extensions.backend.services.headers` (_list_)
|
||||
|
||||
If provided, the headers list will be added on all outgoing requests
|
||||
for this service config. Existing headers in the incoming request with
|
||||
the same name will be overridden by the one in this list. Reserved header
|
||||
names will be ignored (see the [headers](#incoming-request-headers) below).
|
||||
|
||||
#### `extensions.backend.services.headers.name` (*string*)
|
||||
#### `extensions.backend.services.headers.name` (_string_)
|
||||
|
||||
(mandatory)
|
||||
|
||||
Defines the name of the header. It is a mandatory field if a header is
|
||||
provided.
|
||||
|
||||
#### `extensions.backend.services.headers.value` (*string*)
|
||||
#### `extensions.backend.services.headers.value` (_string_)
|
||||
|
||||
(mandatory)
|
||||
|
||||
Defines the value of the header. It is a mandatory field if a header is
|
||||
@@ -178,7 +187,8 @@ Example:
|
||||
In the example above, the value will be replaced with the one from
|
||||
the argocd-secret with key 'some.argocd.secret.key'.
|
||||
|
||||
#### `extensions.backend.services.cluster` (*object*)
|
||||
#### `extensions.backend.services.cluster` (_object_)
|
||||
|
||||
(optional)
|
||||
|
||||
If provided, and multiple services are configured, will have to match
|
||||
@@ -190,17 +200,19 @@ send requests to the proper backend service. If only one backend
|
||||
service is configured, this field is ignored, and all requests are
|
||||
forwarded to the configured one.
|
||||
|
||||
#### `extensions.backend.services.cluster.name` (*string*)
|
||||
#### `extensions.backend.services.cluster.name` (_string_)
|
||||
|
||||
(optional)
|
||||
|
||||
It will be matched with the value from
|
||||
`Application.Spec.Destination.Name`
|
||||
|
||||
#### `extensions.backend.services.cluster.server` (*string*)
|
||||
#### `extensions.backend.services.cluster.server` (_string_)
|
||||
|
||||
(optional)
|
||||
|
||||
It will be matched with the value from
|
||||
`Application.Spec.Destination.Server`.
|
||||
`Application.Spec.Destination.Server`.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -245,7 +257,7 @@ Argo CD UI keeps the authentication token stored in a cookie
|
||||
(`argocd.token`). This value needs to be sent in the `Cookie` header
|
||||
so the API server can validate its authenticity.
|
||||
|
||||
Example:
|
||||
Example:
|
||||
|
||||
Cookie: argocd.token=eyJhbGciOiJIUzI1Ni...
|
||||
|
||||
@@ -299,11 +311,16 @@ section for more details.
|
||||
|
||||
#### `Argocd-Username`
|
||||
|
||||
Will be populated with the username logged in Argo CD.
|
||||
Will be populated with the username logged in Argo CD. This is primarily useful for display purposes.
|
||||
To identify a user for programmatic needs, `Argocd-User-Id` is probably a better choice.
|
||||
|
||||
#### `Argocd-User-Id`
|
||||
|
||||
Will be populated with the internal user id, most often defined by the `sub` claim, logged in Argo CD.
|
||||
|
||||
#### `Argocd-User-Groups`
|
||||
|
||||
Will be populated with the 'groups' claim from the user logged in Argo CD.
|
||||
Will be populated with the configured RBAC scopes, most often the `groups` claim, from the user logged in Argo CD.
|
||||
|
||||
### Multi Backend Use-Case
|
||||
|
||||
|
||||
@@ -1,25 +1,102 @@
|
||||
# Overview
|
||||
|
||||
!!! warning "You probably don't want to be reading this section of the docs."
|
||||
This part of the manual is aimed at helping people contribute to Argo CD, the documentation, or to develop third-party applications that interact with Argo CD, e.g.
|
||||
!!! warning "As an Argo CD user, you probably don't want to be reading this section of the docs."
|
||||
This part of the manual is aimed at helping people contribute to Argo CD, documentation, or to develop third-party applications that interact with Argo CD, e.g.
|
||||
|
||||
* A chat bot
|
||||
* A Slack integration
|
||||
|
||||
|
||||
## Contributing to Argo CD
|
||||
* [Code Contribution Guide](code-contributions/)
|
||||
* [Contributors Quickstart](contributors-quickstart/)
|
||||
* [Running Argo CD Locally](running-locally/)
|
||||
## Preface
|
||||
#### Understand the [Code Contribution Guide](code-contributions.md)
|
||||
#### Understand the [Code Contribution Preface](submit-your-pr.md#preface)
|
||||
|
||||
## Contributing to Argo CD documentation
|
||||
|
||||
This guide will help you get started quickly with contributing documentation changes, performing the minimum setup you'll need.
|
||||
For backend and frontend contributions, that require a full building-testing-running-locally cycle, please refer to [Contributing to Argo CD backend and frontend ](index.md#contributing-to-argo-cd-backend-and-frontend)
|
||||
|
||||
### Fork and clone Argo CD repository
|
||||
- [Fork and clone Argo CD repository](development-environment.md#fork-and-clone-the-repository)
|
||||
|
||||
### Submit your PR
|
||||
- [Before submitting a PR](submit-your-pr.md#before-submitting-a-pr)
|
||||
- [Choose a correct title for your PR](submit-your-pr.md#choose-a-correct-title-for-your-pr)
|
||||
- [Perform the PR template checklist](submit-your-pr.md#perform-the-PR-template-checklist)
|
||||
|
||||
## Contributing to Argo CD Notifications documentation
|
||||
|
||||
This guide will help you get started quickly with contributing documentation changes, performing the minimum setup you'll need.
|
||||
The notificaions docs are located in [notifications-engine](https://github.com/argoproj/notifications-engine) Git repository and require 2 pull requests: one for the `notifications-engine` repo and one for the `argo-cd` repo.
|
||||
For backend and frontend contributions, that require a full building-testing-running-locally cycle, please refer to [Contributing to Argo CD backend and frontend ](index.md#contributing-to-argo-cd-backend-and-frontend)
|
||||
|
||||
### Fork and clone Argo CD repository
|
||||
- [Fork and clone Argo CD repository](development-environment.md#fork-and-clone-the-repository)
|
||||
|
||||
### Submit your PR to notifications-engine
|
||||
- [Before submitting a PR](submit-your-pr.md#before-submitting-a-pr)
|
||||
- [Choose a correct title for your PR](submit-your-pr.md#choose-a-correct-title-for-your-pr)
|
||||
- [Perform the PR template checklist](submit-your-pr.md#perform-the-PR-template-checklist)
|
||||
|
||||
### Install Go on your machine
|
||||
- [Install Go](development-environment.md#install-go)
|
||||
|
||||
### Submit your PR to argo-cd
|
||||
- [Contributing to notifications-engine](dependencies.md#notifications-engine-githubcomargoprojnotifications-engine)
|
||||
- [Before submitting a PR](submit-your-pr.md#before-submitting-a-pr)
|
||||
- [Choose a correct title for your PR](submit-your-pr.md#choose-a-correct-title-for-your-pr)
|
||||
- [Perform the PR template checklist](submit-your-pr.md#perform-the-PR-template-checklist)
|
||||
|
||||
## Contributing to Argo CD backend and frontend
|
||||
|
||||
This guide will help you set up your build & test environment, so that you can start developing and testing bug fixes and feature enhancements without having to make too much effort in setting up a local toolchain.
|
||||
|
||||
As is the case with the development process, this document is under constant change. If you notice any error, or if you think this document is out-of-date, or if you think it is missing something: Feel free to submit a PR or submit a bug to our GitHub issue tracker.
|
||||
|
||||
### Set up your development environment
|
||||
- [Install required tools (Git, Go, Docker, etc)](development-environment.md#install-required-tools)
|
||||
- [Install and start a local K8s cluster (Kind, Minikube or K3d)](development-environment.md#install-a-local-k8s-cluster)
|
||||
- [Fork and clone Argo CD repository](development-environment.md#fork-and-clone-the-repository)
|
||||
- [Install additional required development tools](development-environment.md#install-additional-required-development-tools)
|
||||
- [Install latest Argo CD on your local cluster](development-environment.md#install-latest-argo-cd-on-your-local-cluster)
|
||||
|
||||
### Set up a development toolchain (local or virtualized)
|
||||
- [Understand the differences between the toolchains](toolchain-guide.md#local-vs-virtualized-toolchain)
|
||||
- Choose a development toolchain
|
||||
|
||||
- Either [set up a local toolchain](toolchain-guide.md#setting-up-a-local-toolchain)
|
||||
- Or [set up a virtualized toolchain](toolchain-guide.md#setting-up-a-virtualized-toolchain)
|
||||
|
||||
### Perform the development cycle
|
||||
- [Set kubectl context to argocd namespace](development-cycle.md#set-kubectl-context-to-argocd-namespace)
|
||||
- [Pull in all build dependencies](development-cycle.md#pull-in-all-build-dependencies)
|
||||
- [Generate API glue code and other assets](development-cycle.md#generate-API-glue-code-and-other-assets)
|
||||
- [Build your code and run unit tests](development-cycle.md#build-your-code-and-run-unit-tests)
|
||||
- [Lint your code base](development-cycle.md#lint-your-code-base)
|
||||
- [Run e2e tests](development-cycle.md#run-end-to-end-tests)
|
||||
- How to contribute to documentation: [build and run documentation site](docs-site/) on your machine for manual testing
|
||||
|
||||
### Run and debug Argo CD locally
|
||||
- [Run Argo CD on your machine for manual testing](running-locally.md)
|
||||
- [Debug Argo CD in an IDE on your machine](debugging-locally.md)
|
||||
|
||||
### Submit your PR
|
||||
- [Before submitting a PR](submit-your-pr.md#before-submitting-a-pr)
|
||||
- [Understand the Continuous Integration process](submit-your-pr.md#understand-the-continuous-integration-process)
|
||||
- [Choose a correct title for your PR](submit-your-pr.md#choose-a-correct-title-for-your-pr)
|
||||
- [Perform the PR template checklist](submit-your-pr.md#perform-the-PR-template-checklist)
|
||||
- [Understand the CI automated builds & tests](submit-your-pr.md#automated-builds-&-tests)
|
||||
- [Understand & make sure your PR meets the CI code test coverage requirements](submit-your-pr.md#code-test-coverage)
|
||||
|
||||
Need help? Start with the [Contributors FAQ](faq/)
|
||||
|
||||
## Contributing to the Documentation
|
||||
* [Building and Running Documentation Site Locally](docs-site/)
|
||||
## Contributing to Argo CD dependencies
|
||||
- [Contributing to argo-ui](dependencies.md#argo-ui-components-githubcomargoprojargo-ui)
|
||||
- [Contributing to gitops-engine](dependencies.md#gitops-engine-githubcomargoprojgitops-engine)
|
||||
- [Contributing to notifications-engine](dependencies.md#notifications-engine-githubcomargoprojnotifications-engine)
|
||||
|
||||
## Extensions and Third-Party Applications
|
||||
* [UI Extensions](ui-extensions/)
|
||||
* [Proxy Extensions](proxy-extensions/)
|
||||
* [UI Extensions](extensions/ui-extensions.md)
|
||||
* [Proxy Extensions](extensions/proxy-extensions.md)
|
||||
* [Config Management Plugins](../operator-manual/config-management-plugins/)
|
||||
|
||||
## Contributing to Argo Website
|
||||
|
||||
20
docs/developer-guide/mac-users.md
Normal file
20
docs/developer-guide/mac-users.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# MacOS users
|
||||
Below are known issues specific to macOS
|
||||
|
||||
## Port 5000
|
||||
|
||||
You may get an error listening on port 5000 on macOS:
|
||||
|
||||
```text
|
||||
docker: Error response from daemon: Ports are not available: exposing port TCP 0.0.0.0:5000 -> 0.0.0.0:0: listen tcp 0.0.0.0:5000: bind: address already in use.
|
||||
```
|
||||
|
||||
In that case, you can disable "AirPlay Receiver" in macOS System Preferences.
|
||||
|
||||
## Firewall dialogs
|
||||
If you get firewall dialogs, you can click "Deny", since no access from outside your computer is typically desired.
|
||||
|
||||
## Exec format error for virtualized toolchain make targets
|
||||
If you get `/go/src/github.com/argoproj/argo-cd/dist/mockery: cannot execute binary file: Exec format error`, this typically means that you ran a virtualized `make` target after you ran the a local `make` target.
|
||||
To fix this and continue with the virtualized toolchain, delete the contents of `argo-cd/dist` folder.
|
||||
If later on you wish to run `make` targets of the local toolchain again, run `make install-tools-local` to re-populate the contents of the `argo-cd/dist` folder.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user