Compare commits

..

1 Commits

Author SHA1 Message Date
Michael Crenshaw
19b19fda8d docs: Add step to update 'stable' tag after release
Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
2025-11-04 09:56:13 -05:00
639 changed files with 36617 additions and 85002 deletions

View File

@@ -9,79 +9,19 @@ assignees: ''
Target RC1 date: ___. __, ____
Target GA date: ___. __, ____
## RC1 Release Checklist
- [ ] 1wk before feature freeze post in #argo-contributors that PRs must be merged by DD-MM-YYYY to be included in the release - ask approvers to drop items from milestone they can't merge
- [ ] 1wk before feature freeze post in #argo-contributors that PRs must be merged by DD-MM-YYYY to be included in the release - ask approvers to drop items from milestone they cant merge
- [ ] At least two days before RC1 date, draft RC blog post and submit it for review (or delegate this task)
- [ ] Create new release branch (or delegate this task to an Approver)
- [ ] Add the release branch to ReadTheDocs
- [ ] Cut RC1 (or delegate this task to an Approver and coordinate timing)
- [ ] Run the [Init ArgoCD Release workflow](https://github.com/argoproj/argo-cd/actions/workflows/init-release.yaml) from the release branch
- [ ] Review and merge the generated version bump PR
- [ ] Run `./hack/trigger-release.sh` to push the release tag
- [ ] Monitor the [Publish ArgoCD Release workflow](https://github.com/argoproj/argo-cd/actions/workflows/release.yaml)
- [ ] Verify the release on [GitHub releases](https://github.com/argoproj/argo-cd/releases)
- [ ] Verify the container image on [Quay.io](https://quay.io/repository/argoproj/argocd?tab=tags)
- [ ] Confirm the new version appears in [Read the Docs](https://argo-cd.readthedocs.io/)
- [ ] Verify the docs release build in https://app.readthedocs.org/projects/argo-cd/ succeeded and retry if failed (requires an Approver with admin creds to readthedocs)
- [ ] Announce RC1 release
- [ ] Confirm that tweet and blog post are ready
- [ ] Publish tweet and blog post
- [ ] Post in #argo-cd and #argo-announcements requesting help testing:
```
:mega: Argo CD v{MAJOR}.{MINOR}.{PATCH}-rc{RC_NUMBER} is OUT NOW! :argocd::tada:
Please go through the following resources to know more about the release:
Release notes: https://github.com/argoproj/argo-cd/releases/tag/v{VERSION}
Blog: {BLOG_POST_URL}
We'd love your help testing this release candidate! Please try it out in your environments and report any issues you find. This helps us ensure a stable GA release.
Thanks to all the folks who spent their time contributing to this release in any way possible!
```
- [ ] Monitor support channels for issues, cherry-picking bugfixes and docs fixes as appropriate during the RC period (or delegate this task to an Approver and coordinate timing)
- [ ] After creating the RC, open a documentation PR for the next minor version using [this](../../docs/operator-manual/templates/minor_version_upgrade.md) template.
## GA Release Checklist
- [ ] At GA release date, evaluate if any bugs justify delaying the release
- [ ] Prepare for EOL version (version that is 3 releases old)
- [ ] If unreleased changes are on the release branch for {current minor version minus 3}, cut a final patch release for that series (or delegate this task to an Approver and coordinate timing)
- [ ] Edit the final patch release on GitHub and add the following notice at the top:
```markdown
> [!IMPORTANT]
> **END OF LIFE NOTICE**
>
> This is the final release of the {EOL_SERIES} release series. As of {GA_DATE}, this version has reached end of life and will no longer receive bug fixes or security updates.
>
> **Action Required**: Please upgrade to a [supported version](https://argo-cd.readthedocs.io/en/stable/operator-manual/upgrading/overview/) (v{SUPPORTED_VERSION_1}, v{SUPPORTED_VERSION_2}, or v{NEW_VERSION}).
```
- [ ] Cut GA release (or delegate this task to an Approver and coordinate timing)
- [ ] Run the [Init ArgoCD Release workflow](https://github.com/argoproj/argo-cd/actions/workflows/init-release.yaml) from the release branch
- [ ] Review and merge the generated version bump PR
- [ ] Run `./hack/trigger-release.sh` to push the release tag
- [ ] Monitor the [Publish ArgoCD Release workflow](https://github.com/argoproj/argo-cd/actions/workflows/release.yaml)
- [ ] Verify the release on [GitHub releases](https://github.com/argoproj/argo-cd/releases)
- [ ] Verify the container image on [Quay.io](https://quay.io/repository/argoproj/argocd?tab=tags)
- [ ] Verify the `stable` tag has been updated
- [ ] Confirm the new version appears in [Read the Docs](https://argo-cd.readthedocs.io/)
- [ ] Verify the docs release build in https://app.readthedocs.org/projects/argo-cd/ succeeded and retry if failed (requires an Approver with admin creds to readthedocs)
- [ ] Announce GA release with EOL notice
- [ ] Confirm that tweet and blog post are ready
- [ ] Publish tweet and blog post
- [ ] Post in #argo-cd and #argo-announcements announcing the release and EOL:
```
:mega: Argo CD v{MAJOR}.{MINOR} is OUT NOW! :argocd::tada:
Please go through the following resources to know more about the release:
Upgrade instructions: https://argo-cd.readthedocs.io/en/latest/operator-manual/upgrading/{PREV_MINOR}-{MAJOR}.{MINOR}/
Blog: {BLOG_POST_URL}
:warning: IMPORTANT: With the release of Argo CD v{MAJOR}.{MINOR}, support for Argo CD v{EOL_VERSION} has officially reached End of Life (EOL).
Thanks to all the folks who spent their time contributing to this release in any way possible!
```
- [ ] Create new release branch
- [ ] Add the release branch to ReadTheDocs
- [ ] Confirm that tweet and blog post are ready
- [ ] Trigger the release
- [ ] After the release is finished, publish tweet and blog post
- [ ] Post in #argo-cd and #argo-announcements with lots of emojis announcing the release and requesting help testing
- [ ] Monitor support channels for issues, cherry-picking bugfixes and docs fixes as appropriate (or delegate this task to an Approver and coordinate timing)
- [ ] At release date, evaluate if any bugs justify delaying the release. If not, cut the release (or delegate this task to an Approver and coordinate timing)
- [ ] If unreleased changes are on the release branch for {current minor version minus 3}, cut a final patch release for that series (or delegate this task to an Approver and coordinate timing)
- [ ] After the release, post in #argo-cd that the {current minor version minus 3} has reached EOL (example: https://cloud-native.slack.com/archives/C01TSERG0KZ/p1667336234059729)
- [ ] Update the `stable` tag to be the GA release you just pushed
- [ ] (For the next release champion) Review the [items scheduled for the next release](https://github.com/orgs/argoproj/projects/25). If any item does not have an assignee who can commit to finish the feature, move it to the next release.
- [ ] (For the next release champion) Schedule a time mid-way through the release cycle to review items again.

View File

@@ -10,10 +10,10 @@ jobs:
contents: write # for peter-evans/create-pull-request to create branch
pull-requests: write # for peter-evans/create-pull-request to create a PR
name: Automatically update major version
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
@@ -37,7 +37,7 @@ jobs:
working-directory: /home/runner/go/src/github.com/argoproj/argo-cd
- name: Setup Golang
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: ${{ env.GOLANG_VERSION }}
- name: Add ~/go/bin to PATH
@@ -74,7 +74,7 @@ jobs:
rsync -a --exclude=.git /home/runner/go/src/github.com/argoproj/argo-cd/ ../argo-cd
- name: Create pull request
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
commit-message: "Bump major version to ${{ steps.get-target-version.outputs.TARGET_VERSION }}"
title: "Bump major version to ${{ steps.get-target-version.outputs.TARGET_VERSION }}"

View File

@@ -28,7 +28,7 @@ on:
jobs:
cherry-pick:
name: Cherry Pick to ${{ inputs.version_number }}
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- name: Generate a token
id: generate-token
@@ -38,7 +38,7 @@ jobs:
private-key: ${{ secrets.CHERRYPICK_APP_PRIVATE_KEY }}
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
token: ${{ steps.generate-token.outputs.token }}

View File

@@ -14,7 +14,7 @@ jobs:
(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-24.04
runs-on: ubuntu-latest
outputs:
labels: ${{ steps.extract-labels.outputs.labels }}
steps:

View File

@@ -14,7 +14,7 @@ on:
env:
# Golang version to use across CI steps
# renovate: datasource=golang-version packageName=golang
GOLANG_VERSION: '1.25.6'
GOLANG_VERSION: '1.25.3'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -25,14 +25,14 @@ permissions:
jobs:
changes:
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
outputs:
backend: ${{ steps.filter.outputs.backend_any_changed }}
frontend: ${{ steps.filter.outputs.frontend_any_changed }}
docs: ${{ steps.filter.outputs.docs_any_changed }}
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: tj-actions/changed-files@e0021407031f5be11a464abee9a0776171c79891 # v47.0.1
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- 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
@@ -50,14 +50,14 @@ jobs:
check-go:
name: Ensure Go modules synchronicity
if: ${{ needs.changes.outputs.backend == 'true' }}
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
needs:
- changes
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Golang
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: ${{ env.GOLANG_VERSION }}
- name: Download all Go modules
@@ -70,18 +70,18 @@ jobs:
build-go:
name: Build & cache Go code
if: ${{ needs.changes.outputs.backend == 'true' }}
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
needs:
- changes
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Golang
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: ${{ env.GOLANG_VERSION }}
- name: Restore go build cache
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/go-build
key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
@@ -97,18 +97,18 @@ jobs:
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
name: Lint Go code
if: ${{ needs.changes.outputs.backend == 'true' }}
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
needs:
- changes
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Golang
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: ${{ env.GOLANG_VERSION }}
- name: Run golangci-lint
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
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.5.0
@@ -117,7 +117,7 @@ jobs:
test-go:
name: Run unit tests for Go packages
if: ${{ needs.changes.outputs.backend == 'true' }}
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
needs:
- build-go
- changes
@@ -128,11 +128,11 @@ jobs:
- name: Create checkout directory
run: mkdir -p ~/go/src/github.com/argoproj
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Create symlink in GOPATH
run: ln -s $(pwd) ~/go/src/github.com/argoproj/argo-cd
- name: Setup Golang
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: ${{ env.GOLANG_VERSION }}
- name: Install required packages
@@ -152,7 +152,7 @@ jobs:
run: |
echo "/usr/local/bin" >> $GITHUB_PATH
- name: Restore go build cache
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/go-build
key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
@@ -173,7 +173,7 @@ jobs:
- name: Run all unit tests
run: make test-local
- name: Generate test results artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: test-results
path: test-results
@@ -181,7 +181,7 @@ jobs:
test-go-race:
name: Run unit tests with -race for Go packages
if: ${{ needs.changes.outputs.backend == 'true' }}
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
needs:
- build-go
- changes
@@ -192,11 +192,11 @@ jobs:
- name: Create checkout directory
run: mkdir -p ~/go/src/github.com/argoproj
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Create symlink in GOPATH
run: ln -s $(pwd) ~/go/src/github.com/argoproj/argo-cd
- name: Setup Golang
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: ${{ env.GOLANG_VERSION }}
- name: Install required packages
@@ -216,7 +216,7 @@ jobs:
run: |
echo "/usr/local/bin" >> $GITHUB_PATH
- name: Restore go build cache
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/go-build
key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
@@ -237,7 +237,7 @@ jobs:
- name: Run all unit tests
run: make test-race-local
- name: Generate test results artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: race-results
path: test-results/
@@ -245,21 +245,20 @@ jobs:
codegen:
name: Check changes to generated code
if: ${{ needs.changes.outputs.backend == 'true' || needs.changes.outputs.docs == 'true'}}
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
needs:
- changes
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Golang
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: ${{ env.GOLANG_VERSION }}
- name: Create symlink in GOPATH
# generalizing repo name for forks: ${{ github.event.repository.name }}
run: |
mkdir -p ~/go/src/github.com/argoproj
cp -a ../${{ github.event.repository.name }} ~/go/src/github.com/argoproj
cp -a ../argo-cd ~/go/src/github.com/argoproj
- name: Add ~/go/bin to PATH
run: |
echo "/home/runner/go/bin" >> $GITHUB_PATH
@@ -271,14 +270,12 @@ jobs:
# We need to vendor go modules for codegen yet
go mod download
go mod vendor -v
# generalizing repo name for forks: ${{ github.event.repository.name }}
working-directory: /home/runner/go/src/github.com/argoproj/${{ github.event.repository.name }}
working-directory: /home/runner/go/src/github.com/argoproj/argo-cd
- name: Install toolchain for codegen
run: |
make install-codegen-tools-local
make install-go-tools-local
# generalizing repo name for forks: ${{ github.event.repository.name }}
working-directory: /home/runner/go/src/github.com/argoproj/${{ github.event.repository.name }}
working-directory: /home/runner/go/src/github.com/argoproj/argo-cd
# We install kustomize in the dist directory
- name: Add dist to PATH
run: |
@@ -289,33 +286,31 @@ jobs:
export GOPATH=$(go env GOPATH)
git checkout -- go.mod go.sum
make codegen-local
# generalizing repo name for forks: ${{ github.event.repository.name }}
working-directory: /home/runner/go/src/github.com/argoproj/${{ github.event.repository.name }}
working-directory: /home/runner/go/src/github.com/argoproj/argo-cd
- name: Check nothing has changed
run: |
set -xo pipefail
git diff --exit-code -- . ':!go.sum' ':!go.mod' ':!assets/swagger.json' | tee codegen.patch
# generalizing repo name for forks: ${{ github.event.repository.name }}
working-directory: /home/runner/go/src/github.com/argoproj/${{ github.event.repository.name }}
working-directory: /home/runner/go/src/github.com/argoproj/argo-cd
build-ui:
name: Build, test & lint UI code
# We run UI logic for backend changes so that we have a complete set of coverage documents to send to codecov.
if: ${{ needs.changes.outputs.backend == 'true' || needs.changes.outputs.frontend == 'true' }}
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
needs:
- changes
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup NodeJS
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.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@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ui/node_modules
key: ${{ runner.os }}-node-dep-v2-${{ hashFiles('**/yarn.lock') }}
@@ -338,9 +333,9 @@ jobs:
working-directory: ui/
shellcheck:
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- run: |
sudo apt-get install shellcheck
shellcheck -e SC2059 -e SC2154 -e SC2034 -e SC2016 -e SC1091 $(find . -type f -name '*.sh' | grep -v './ui/node_modules') | tee sc.log
@@ -349,7 +344,7 @@ jobs:
analyze:
name: Process & analyze test artifacts
if: ${{ needs.changes.outputs.backend == 'true' || needs.changes.outputs.frontend == 'true' }}
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
needs:
- test-go
- build-ui
@@ -357,35 +352,30 @@ jobs:
- test-e2e
env:
sonar_secret: ${{ secrets.SONAR_TOKEN }}
codecov_secret: ${{ secrets.CODECOV_TOKEN }}
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
- name: Restore node dependency cache
id: cache-dependencies
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ui/node_modules
key: ${{ runner.os }}-node-dep-v2-${{ hashFiles('**/yarn.lock') }}
if: env.codecov_secret != ''
- name: Remove other node_modules directory
run: |
rm -rf ui/node_modules/argo-ui/node_modules
if: env.codecov_secret != ''
- name: Get e2e code coverage
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: e2e-code-coverage
path: e2e-code-coverage
if: env.codecov_secret != ''
- name: Get unit test code coverage
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: test-results
path: test-results
if: env.codecov_secret != ''
- name: combine-go-coverage
# We generate coverage reports for all Argo CD components, but only the applicationset-controller,
# app-controller, repo-server, and commit-server report contain coverage data. The other components currently
@@ -393,53 +383,53 @@ jobs:
# references to their coverage output directories.
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
if: env.codecov_secret != ''
- name: Upload code coverage information to codecov.io
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
with:
files: test-results/full-coverage.out
fail_ci_if_error: true
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
if: env.codecov_secret != ''
- name: Upload test results to Codecov
# Codecov uploads test results to Codecov.io on upstream master branch and on fork master branch if the token is configured.
if: env.codecov_secret != '' && github.ref == 'refs/heads/master' && github.event_name == 'push'
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
if: github.ref == 'refs/heads/master' && github.event_name == 'push' && github.repository == 'argoproj/argo-cd'
uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1
with:
files: test-results/junit.xml
file: test-results/junit.xml
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
report_type: test_results
- name: Perform static code analysis using SonarCloud
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
uses: SonarSource/sonarqube-scan-action@a31c9398be7ace6bbfaf30c0bd5d415f843d45e9 # v7.0.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: ${{ github.repository == 'argoproj/argo-cd' && 'oracle-vm-16cpu-64gb-x86-64' || 'ubuntu-24.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.34.2
latest: true
- version: v1.33.1
latest: false
latest: true
- 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/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'
ARGOCD_E2E_SSH_KNOWN_HOSTS: '../fixture/certs/ssh_known_hosts'
ARGOCD_E2E_K3S: 'true'
ARGOCD_IN_CI: 'true'
ARGOCD_E2E_APISERVER_PORT: '8088'
@@ -456,14 +446,11 @@ jobs:
swap-storage: false
tool-cache: false
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Golang
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: ${{ env.GOLANG_VERSION }}
- name: Set GOPATH
run: |
echo "GOPATH=$HOME/go" >> $GITHUB_ENV
- name: GH actions workaround - Kill XSP4 process
run: |
sudo pkill mono || true
@@ -474,19 +461,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 $(whoami) $HOME/.kube
sudo mkdir -p $HOME/.kube && sudo chown -R ubuntu $HOME/.kube
sudo k3s kubectl config view --raw > $HOME/.kube/config
sudo chown $(whoami) $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@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/go-build
key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
- name: Add ~/go/bin to PATH
run: |
echo "$HOME/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
@@ -508,11 +495,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:8.2.3-alpine
docker pull redis:8.2.1-alpine
- name: Create target directory for binaries in the build-process
run: |
mkdir -p dist
chown $(whoami) dist
chown ubuntu dist
- name: Run E2E server and wait for it being available
timeout-minutes: 30
run: |
@@ -538,13 +525,13 @@ jobs:
goreman run stop-all || echo "goreman trouble"
sleep 30
- name: Upload e2e coverage report
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: e2e-code-coverage
path: /tmp/coverage
if: ${{ matrix.k3s.latest }}
- name: Upload e2e-server logs
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: e2e-server-k8s${{ matrix.k3s.version }}.log
path: /tmp/e2e-server.log
@@ -562,7 +549,7 @@ jobs:
needs:
- test-e2e
- changes
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
steps:
- run: |
result="${{ needs.test-e2e.result }}"

View File

@@ -26,14 +26,14 @@ jobs:
if: github.repository == 'argoproj/argo-cd' || vars.enable_codeql
# CodeQL runs on ubuntu-latest and windows-latest
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
# Use correct go version. https://github.com/github/codeql-action/issues/1842#issuecomment-1704398087
- name: Setup Golang
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version-file: go.mod

View File

@@ -51,23 +51,23 @@ jobs:
contents: read
packages: write # Used to push images to `ghcr.io` if used.
id-token: write # Needed to create an OIDC token for keyless signing
runs-on: ubuntu-24.04
outputs:
runs-on: ubuntu-22.04
outputs:
image-digest: ${{ steps.image.outputs.digest }}
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
if: ${{ github.ref_type == 'tag'}}
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
if: ${{ github.ref_type != 'tag'}}
- name: Setup Golang
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: ${{ inputs.go-version }}
cache: false
@@ -76,7 +76,7 @@ jobs:
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
- uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Setup tags for container image as a CSV type
run: |

View File

@@ -19,49 +19,16 @@ jobs:
set-vars:
permissions:
contents: read
# Always run to calculate variables - other jobs check outputs
runs-on: ubuntu-24.04
if: github.repository == 'argoproj/argo-cd'
runs-on: ubuntu-22.04
outputs:
image-tag: ${{ steps.image.outputs.tag}}
platforms: ${{ steps.platforms.outputs.platforms }}
image_namespace: ${{ steps.image.outputs.image_namespace }}
image_repository: ${{ steps.image.outputs.image_repository }}
quay_image_name: ${{ steps.image.outputs.quay_image_name }}
ghcr_image_name: ${{ steps.image.outputs.ghcr_image_name }}
ghcr_provenance_image: ${{ steps.image.outputs.ghcr_provenance_image }}
allow_ghcr_publish: ${{ steps.image.outputs.allow_ghcr_publish }}
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set image tag and names
run: |
# Calculate image tag
TAG="$(cat ./VERSION)-${GITHUB_SHA::8}"
echo "tag=$TAG" >> $GITHUB_OUTPUT
# Calculate image names with defaults
IMAGE_NAMESPACE="${{ vars.IMAGE_NAMESPACE || 'argoproj' }}"
IMAGE_REPOSITORY="${{ vars.IMAGE_REPOSITORY || 'argocd' }}"
GHCR_NAMESPACE="${{ vars.GHCR_NAMESPACE || github.repository }}"
GHCR_REPOSITORY="${{ vars.GHCR_REPOSITORY || 'argocd' }}"
echo "image_namespace=$IMAGE_NAMESPACE" >> $GITHUB_OUTPUT
echo "image_repository=$IMAGE_REPOSITORY" >> $GITHUB_OUTPUT
# Construct image name
echo "quay_image_name=quay.io/$IMAGE_NAMESPACE/$IMAGE_REPOSITORY:latest" >> $GITHUB_OUTPUT
ALLOW_GHCR_PUBLISH=false
if [[ "${{ github.repository }}" == "argoproj/argo-cd" || "$GHCR_NAMESPACE" != argoproj/* ]]; then
ALLOW_GHCR_PUBLISH=true
echo "ghcr_image_name=ghcr.io/$GHCR_NAMESPACE/$GHCR_REPOSITORY:$TAG" >> $GITHUB_OUTPUT
echo "ghcr_provenance_image=ghcr.io/$GHCR_NAMESPACE/$GHCR_REPOSITORY" >> $GITHUB_OUTPUT
else
echo "GhCR publish skipped: refusing to push to namespace '$GHCR_NAMESPACE'. Please override GHCR_* for forks." >&2
echo "ghcr_image_name=" >> $GITHUB_OUTPUT
echo "ghcr_provenance_image=" >> $GITHUB_OUTPUT
fi
echo "allow_ghcr_publish=$ALLOW_GHCR_PUBLISH" >> $GITHUB_OUTPUT
- name: Set image tag for ghcr
run: echo "tag=$(cat ./VERSION)-${GITHUB_SHA::8}" >> $GITHUB_OUTPUT
id: image
- name: Determine image platforms to use
@@ -81,12 +48,12 @@ jobs:
contents: read
packages: write # for pushing packages to GHCR, which is used by cd.apps.argoproj.io to avoid polluting Quay with tags
id-token: write # for creating OIDC tokens for signing.
if: ${{ (github.repository == 'argoproj/argo-cd' || needs.set-vars.outputs.image_namespace != 'argoproj') && github.event_name != 'push' }}
if: ${{ github.repository == 'argoproj/argo-cd' && github.event_name != 'push' }}
uses: ./.github/workflows/image-reuse.yaml
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.25.6
go-version: 1.25.3
platforms: ${{ needs.set-vars.outputs.platforms }}
push: false
@@ -96,14 +63,14 @@ jobs:
contents: read
packages: write # for pushing packages to GHCR, which is used by cd.apps.argoproj.io to avoid polluting Quay with tags
id-token: write # for creating OIDC tokens for signing.
if: ${{ (github.repository == 'argoproj/argo-cd' || needs.set-vars.outputs.image_namespace != 'argoproj') && github.event_name == 'push' }}
if: ${{ github.repository == 'argoproj/argo-cd' && github.event_name == 'push' }}
uses: ./.github/workflows/image-reuse.yaml
with:
quay_image_name: ${{ needs.set-vars.outputs.quay_image_name }}
ghcr_image_name: ${{ needs.set-vars.outputs.ghcr_image_name }}
quay_image_name: quay.io/argoproj/argocd:latest
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.25.6
go-version: 1.25.3
platforms: ${{ needs.set-vars.outputs.platforms }}
push: true
secrets:
@@ -114,17 +81,16 @@ jobs:
build-and-publish-provenance: # Push attestations to GHCR, latest image is polluting quay.io
needs:
- set-vars
- build-and-publish
permissions:
actions: read # for detecting the Github Actions environment.
id-token: write # for creating OIDC tokens for signing.
packages: write # for uploading attestations. (https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#known-issues)
if: ${{ (github.repository == 'argoproj/argo-cd' || needs.set-vars.outputs.image_namespace != 'argoproj') && github.event_name == 'push' && needs.set-vars.outputs.allow_ghcr_publish == 'true'}}
if: ${{ github.repository == 'argoproj/argo-cd' && github.event_name == 'push' }}
# Must be refernced by a tag. https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#referencing-the-slsa-generator
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
with:
image: ${{ needs.set-vars.outputs.ghcr_provenance_image }}
image: ghcr.io/argoproj/argo-cd/argocd
digest: ${{ needs.build-and-publish.outputs.image-digest }}
registry-username: ${{ github.actor }}
secrets:
@@ -138,9 +104,9 @@ jobs:
contents: write # for git to push upgrade commit if not already deployed
packages: write # for pushing packages to GHCR, which is used by cd.apps.argoproj.io to avoid polluting Quay with tags
if: ${{ github.repository == 'argoproj/argo-cd' && github.event_name == 'push' }}
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- run: git clone "https://$TOKEN@github.com/argoproj/argoproj-deployments"
env:
TOKEN: ${{ secrets.TOKEN }}

View File

@@ -20,16 +20,10 @@ jobs:
contents: write # for peter-evans/create-pull-request to create branch
pull-requests: write # for peter-evans/create-pull-request to create a PR
name: Automatically generate version and manifests on ${{ inputs.TARGET_BRANCH }}
runs-on: ubuntu-24.04
env:
# Calculate image names with defaults, this will be used in the make manifests-local command
# to generate the correct image name in the manifests
IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'quay.io' }}
IMAGE_NAMESPACE: ${{ vars.IMAGE_NAMESPACE || 'argoproj' }}
IMAGE_REPOSITORY: ${{ vars.IMAGE_REPOSITORY || 'argocd' }}
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
@@ -70,7 +64,7 @@ jobs:
git stash pop
- name: Create pull request
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
commit-message: "Bump version to ${{ inputs.TARGET_VERSION }}"
title: "Bump version to ${{ inputs.TARGET_VERSION }} on ${{ inputs.TARGET_BRANCH }} branch"

View File

@@ -21,7 +21,7 @@ jobs:
contents: read
pull-requests: read
name: Validate PR Title
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
steps:
- uses: thehanimo/pr-title-checker@7fbfe05602bdd86f926d3fb3bccb6f3aed43bc70 # v1.4.3
with:

View File

@@ -11,22 +11,21 @@ permissions: {}
env:
# renovate: datasource=golang-version packageName=golang
GOLANG_VERSION: '1.25.6' # Note: go-version must also be set in job argocd-image.with.go-version
GOLANG_VERSION: '1.25.3' # Note: go-version must also be set in job argocd-image.with.go-version
jobs:
argocd-image:
needs: [setup-variables]
permissions:
contents: read
id-token: write # for creating OIDC tokens for signing.
packages: write # used to push images to `ghcr.io` if used.
if: github.repository == 'argoproj/argo-cd' || needs.setup-variables.outputs.allow_fork_release == 'true'
if: github.repository == 'argoproj/argo-cd'
uses: ./.github/workflows/image-reuse.yaml
with:
quay_image_name: ${{ needs.setup-variables.outputs.quay_image_name }}
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.25.6
go-version: 1.25.3
platforms: linux/amd64,linux/arm64,linux/s390x,linux/ppc64le
push: true
secrets:
@@ -35,20 +34,14 @@ jobs:
setup-variables:
name: Setup Release Variables
if: github.repository == 'argoproj/argo-cd' || (github.repository_owner != 'argoproj' && vars.ENABLE_FORK_RELEASES == 'true' && vars.IMAGE_NAMESPACE && vars.IMAGE_NAMESPACE != 'argoproj')
runs-on: ubuntu-24.04
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 }}
enable_fork_releases: ${{ steps.var.outputs.enable_fork_releases }}
image_namespace: ${{ steps.var.outputs.image_namespace }}
image_repository: ${{ steps.var.outputs.image_repository }}
quay_image_name: ${{ steps.var.outputs.quay_image_name }}
provenance_image: ${{ steps.var.outputs.provenance_image }}
allow_fork_release: ${{ steps.var.outputs.allow_fork_release }}
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
@@ -74,36 +67,18 @@ jobs:
fi
echo "is_pre_release=$PRE_RELEASE" >> $GITHUB_OUTPUT
echo "is_latest_release=$IS_LATEST" >> $GITHUB_OUTPUT
# Calculate configuration with defaults
ENABLE_FORK_RELEASES="${{ vars.ENABLE_FORK_RELEASES || 'false' }}"
IMAGE_NAMESPACE="${{ vars.IMAGE_NAMESPACE || 'argoproj' }}"
IMAGE_REPOSITORY="${{ vars.IMAGE_REPOSITORY || 'argocd' }}"
echo "enable_fork_releases=$ENABLE_FORK_RELEASES" >> $GITHUB_OUTPUT
echo "image_namespace=$IMAGE_NAMESPACE" >> $GITHUB_OUTPUT
echo "image_repository=$IMAGE_REPOSITORY" >> $GITHUB_OUTPUT
echo "quay_image_name=quay.io/$IMAGE_NAMESPACE/$IMAGE_REPOSITORY:${{ github.ref_name }}" >> $GITHUB_OUTPUT
echo "provenance_image=quay.io/$IMAGE_NAMESPACE/$IMAGE_REPOSITORY" >> $GITHUB_OUTPUT
ALLOW_FORK_RELEASE=false
if [[ "${{ github.repository_owner }}" != "argoproj" && "$ENABLE_FORK_RELEASES" == "true" && "$IMAGE_NAMESPACE" != "argoproj" && "${{ github.ref }}" == refs/tags/* ]]; then
ALLOW_FORK_RELEASE=true
fi
echo "allow_fork_release=$ALLOW_FORK_RELEASE" >> $GITHUB_OUTPUT
argocd-image-provenance:
needs: [setup-variables, argocd-image]
needs: [argocd-image]
permissions:
actions: read # for detecting the Github Actions environment.
id-token: write # for creating OIDC tokens for signing.
packages: write # for uploading attestations. (https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#known-issues)
# Must be refernced by a tag. https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#referencing-the-slsa-generator
if: github.repository == 'argoproj/argo-cd' || needs.setup-variables.outputs.allow_fork_release == 'true'
if: github.repository == 'argoproj/argo-cd'
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
with:
image: ${{ needs.setup-variables.outputs.provenance_image }}
image: quay.io/argoproj/argocd
digest: ${{ needs.argocd-image.outputs.image-digest }}
secrets:
registry-username: ${{ secrets.RELEASE_QUAY_USERNAME }}
@@ -116,15 +91,15 @@ jobs:
- argocd-image-provenance
permissions:
contents: write # used for uploading assets
if: github.repository == 'argoproj/argo-cd' || needs.setup-variables.outputs.allow_fork_release == 'true'
runs-on: ubuntu-24.04
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
@@ -133,7 +108,7 @@ jobs:
run: git fetch --force --tags
- name: Setup Golang
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: ${{ env.GOLANG_VERSION }}
cache: false
@@ -168,8 +143,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
KUBECTL_VERSION: ${{ env.KUBECTL_VERSION }}
GIT_TREE_STATE: ${{ env.GIT_TREE_STATE }}
# Used to determine the current repository in the goreleaser config to display correct manifest links
GORELEASER_CURRENT_REPOSITORY: ${{ github.repository }}
- name: Generate subject for provenance
id: hash
@@ -186,12 +159,12 @@ jobs:
echo "hashes=$hashes" >> $GITHUB_OUTPUT
goreleaser-provenance:
needs: [goreleaser, setup-variables]
needs: [goreleaser]
permissions:
actions: read # for detecting the Github Actions environment
id-token: write # Needed for provenance signing and ID
contents: write # Needed for release uploads
if: github.repository == 'argoproj/argo-cd' || needs.setup-variables.outputs.allow_fork_release == 'true'
if: github.repository == 'argoproj/argo-cd'
# Must be refernced by a tag. https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#referencing-the-slsa-generator
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
with:
@@ -204,22 +177,21 @@ jobs:
needs:
- argocd-image
- goreleaser
- setup-variables
permissions:
contents: write # Needed for release uploads
outputs:
hashes: ${{ steps.sbom-hash.outputs.hashes }}
if: github.repository == 'argoproj/argo-cd' || needs.setup-variables.outputs.allow_fork_release == 'true'
runs-on: ubuntu-24.04
if: github.repository == 'argoproj/argo-cd'
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Golang
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: ${{ env.GOLANG_VERSION }}
cache: false
@@ -235,7 +207,7 @@ jobs:
# managers (gomod, yarn, npm).
PROJECT_FOLDERS: '.,./ui'
# full qualified name of the docker image to be inspected
DOCKER_IMAGE: ${{ needs.setup-variables.outputs.quay_image_name }}
DOCKER_IMAGE: quay.io/argoproj/argocd:${{ github.ref_name }}
run: |
yarn install --cwd ./ui
go install github.com/spdx/spdx-sbom-generator/cmd/generator@$SPDX_GEN_VERSION
@@ -264,7 +236,7 @@ jobs:
echo "hashes=$(sha256sum /tmp/sbom.tar.gz | base64 -w0)" >> "$GITHUB_OUTPUT"
- name: Upload SBOM
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -272,12 +244,12 @@ jobs:
/tmp/sbom.tar.gz
sbom-provenance:
needs: [generate-sbom, setup-variables]
needs: [generate-sbom]
permissions:
actions: read # for detecting the Github Actions environment
id-token: write # Needed for provenance signing and ID
contents: write # Needed for release uploads
if: github.repository == 'argoproj/argo-cd' || needs.setup-variables.outputs.allow_fork_release == 'true'
if: github.repository == 'argoproj/argo-cd'
# Must be referenced by a tag. https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#referencing-the-slsa-generator
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
with:
@@ -294,13 +266,13 @@ jobs:
permissions:
contents: write # Needed to push commit to update stable tag
pull-requests: write # Needed to create PR for VERSION update.
if: github.repository == 'argoproj/argo-cd' || needs.setup-variables.outputs.allow_fork_release == 'true'
runs-on: ubuntu-24.04
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
@@ -344,7 +316,7 @@ jobs:
if: ${{ env.UPDATE_VERSION == 'true' }}
- name: Create PR to update VERSION on master branch
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
commit-message: Bump version in master
title: 'chore: Bump version in master'

View File

@@ -9,7 +9,7 @@ permissions:
jobs:
renovate:
runs-on: ubuntu-24.04
runs-on: ubuntu-latest
if: github.repository == 'argoproj/argo-cd'
steps:
- name: Get token
@@ -20,17 +20,17 @@ jobs:
private-key: ${{ secrets.RENOVATE_APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # 5.0.0
# Some codegen commands require Go to be setup
- name: Setup Golang
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
# renovate: datasource=golang-version packageName=golang
go-version: 1.25.6
go-version: 1.25.3
- name: Self-hosted Renovate
uses: renovatebot/github-action@66387ab8c2464d575b933fa44e9e5a86b2822809 #44.2.4
uses: renovatebot/github-action@ea850436a5fe75c0925d583c7a02c60a5865461d #43.0.20
with:
configurationFile: .github/configs/renovate-config.js
token: '${{ steps.get_token.outputs.token }}'

View File

@@ -17,7 +17,7 @@ permissions: read-all
jobs:
analysis:
name: Scorecards analysis
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
permissions:
# Needed to upload the results to code-scanning dashboard.
security-events: write
@@ -30,7 +30,7 @@ jobs:
steps:
- name: "Checkout code"
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
@@ -54,7 +54,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: SARIF file
path: results.sarif

View File

@@ -14,10 +14,10 @@ jobs:
pull-requests: write
if: github.repository == 'argoproj/argo-cd'
name: Update Snyk report in the docs directory
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Build reports

View File

@@ -66,14 +66,14 @@ release:
```shell
kubectl create namespace argocd
kubectl apply -n argocd --server-side --force-conflicts -f https://raw.githubusercontent.com/{{ .Env.GORELEASER_CURRENT_REPOSITORY }}/{{.Tag}}/manifests/install.yaml
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/{{.Tag}}/manifests/install.yaml
```
### HA:
```shell
kubectl create namespace argocd
kubectl apply -n argocd --server-side --force-conflicts -f https://raw.githubusercontent.com/{{ .Env.GORELEASER_CURRENT_REPOSITORY }}/{{.Tag}}/manifests/ha/install.yaml
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/{{.Tag}}/manifests/ha/install.yaml
```
## Release Signatures and Provenance
@@ -87,7 +87,7 @@ release:
If upgrading from a different minor version, be sure to read the [upgrading](https://argo-cd.readthedocs.io/en/stable/operator-manual/upgrading/overview/) documentation.
footer: |
**Full Changelog**: https://github.com/{{ .Env.GORELEASER_CURRENT_REPOSITORY }}/compare/{{ .PreviousTag }}...{{ .Tag }}
**Full Changelog**: https://github.com/argoproj/argo-cd/compare/{{ .PreviousTag }}...{{ .Tag }}
<a href="https://argoproj.github.io/cd/"><img src="https://raw.githubusercontent.com/argoproj/argo-site/master/content/pages/cd/gitops-cd.png" width="25%" ></a>

View File

@@ -1,6 +1,5 @@
dir: '{{.InterfaceDir}}/mocks'
filename: '{{.InterfaceName}}.go'
include-auto-generated: true # Needed since mockery 3.6.1
packages:
github.com/argoproj/argo-cd/v3/applicationset/generators:
interfaces:
@@ -32,9 +31,6 @@ packages:
github.com/argoproj/argo-cd/v3/pkg/apiclient/cluster:
interfaces:
ClusterServiceServer: {}
github.com/argoproj/argo-cd/v3/pkg/apiclient/project:
interfaces:
ProjectServiceClient: {}
github.com/argoproj/argo-cd/v3/pkg/apiclient/session:
interfaces:
SessionServiceClient: {}

View File

@@ -1,10 +1,10 @@
ARG BASE_IMAGE=docker.io/library/ubuntu:25.10@sha256:5922638447b1e3ba114332c896a2c7288c876bb94adec923d70d58a17d2fec5e
ARG BASE_IMAGE=docker.io/library/ubuntu:25.04@sha256:27771fb7b40a58237c98e8d3e6b9ecdd9289cec69a857fccfb85ff36294dac20
####################################################################################################
# Builder image
# Initial stage which pulls prepares build dependencies and CLI tooling we need for our final image
# Also used as the image in CI jobs so needs all dependencies
####################################################################################################
FROM docker.io/library/golang:1.25.6@sha256:fc24d3881a021e7b968a4610fc024fba749f98fe5c07d4f28e6cfa14dc65a84c AS builder
FROM docker.io/library/golang:1.25.3@sha256:6bac879c5b77e0fc9c556a5ed8920e89dab1709bd510a854903509c828f67f96 AS builder
WORKDIR /tmp
@@ -50,10 +50,10 @@ RUN groupadd -g $ARGOCD_USER_ID argocd && \
chmod g=u /home/argocd && \
apt-get update && \
apt-get dist-upgrade -y && \
apt-get install --no-install-recommends -y \
git git-lfs tini ca-certificates gpg gpg-agent tzdata connect-proxy openssh-client && \
apt-get install -y \
git git-lfs tini gpg tzdata connect-proxy && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/*
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
COPY hack/gpg-wrapper.sh \
hack/git-verify-wrapper.sh \
@@ -85,7 +85,7 @@ WORKDIR /home/argocd
####################################################################################################
# Argo CD UI stage
####################################################################################################
FROM --platform=$BUILDPLATFORM docker.io/library/node:23.0.0@sha256:9d09fa506f5b8465c5221cbd6f980e29ae0ce9a3119e2b9bc0842e6a3f37bb59 AS argocd-ui
FROM --platform=$BUILDPLATFORM docker.io/library/node:23.0.0@sha256:e643c0b70dca9704dff42e12b17f5b719dbe4f95e6392fc2dfa0c5f02ea8044d AS argocd-ui
WORKDIR /src
COPY ["ui/package.json", "ui/yarn.lock", "./"]
@@ -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.25.6@sha256:fc24d3881a021e7b968a4610fc024fba749f98fe5c07d4f28e6cfa14dc65a84c AS argocd-build
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.25.3@sha256:6bac879c5b77e0fc9c556a5ed8920e89dab1709bd510a854903509c828f67f96 AS argocd-build
WORKDIR /go/src/github.com/argoproj/argo-cd

View File

@@ -1,4 +1,4 @@
FROM docker.io/library/golang:1.25.6@sha256:fc24d3881a021e7b968a4610fc024fba749f98fe5c07d4f28e6cfa14dc65a84c
FROM docker.io/library/golang:1.25.3@sha256:6bac879c5b77e0fc9c556a5ed8920e89dab1709bd510a854903509c828f67f96
ENV DEBIAN_FRONTEND=noninteractive

View File

@@ -1,39 +0,0 @@
# Argo CD Maintainers
This document lists the maintainers of the Argo CD project.
## Maintainers
| Maintainer | GitHub ID | Project Roles | Affiliation |
|---------------------------|---------------------------------------------------------|----------------------|-------------------------------------------------|
| Zach Aller | [zachaller](https://github.com/zachaller) | Reviewer | [Intuit](https://www.github.com/intuit/) |
| Leonardo Luz Almeida | [leoluz](https://github.com/leoluz) | Approver | [Intuit](https://www.github.com/intuit/) |
| Chetan Banavikalmutt | [chetan-rns](https://github.com/chetan-rns) | Reviewer | [Red Hat](https://redhat.com/) |
| Keith Chong | [keithchong](https://github.com/keithchong) | Approver | [Red Hat](https://redhat.com/) |
| Alex Collins | [alexec](https://github.com/alexec) | Approver | [Intuit](https://www.github.com/intuit/) |
| Michael Crenshaw | [crenshaw-dev](https://github.com/crenshaw-dev) | Lead | [Intuit](https://www.github.com/intuit/) |
| Soumya Ghosh Dastidar | [gdsoumya](https://github.com/gdsoumya) | Approver | [Akuity](https://akuity.io/) |
| Eugene Doudine | [dudinea](https://github.com/dudinea) | Reviewer | [Octopus Deploy](https://octopus.com/) |
| Jann Fischer | [jannfis](https://github.com/jannfis) | Approver | [Red Hat](https://redhat.com/) |
| Dan Garfield | [todaywasawesome](https://github.com/todaywasawesome) | Approver(docs) | [Octopus Deploy](https://octopus.com/) |
| Alexandre Gaudreault | [agaudreault](https://github.com/agaudreault) | Approver | [Intuit](https://www.github.com/intuit/) |
| Christian Hernandez | [christianh814](https://github.com/christianh814) | Reviewer(docs) | [Akuity](https://akuity.io/) |
| Peter Jiang | [pjiang](https://github.com/pjiang) | Reviewer | [Intuit](https://www.intuit.com/) |
| Andrii Korotkov | [andrii-korotkov](https://github.com/andrii-korotkov) | Reviewer | [Verkada](https://www.verkada.com/) |
| Pasha Kostohrys | [pasha-codefresh](https://github.com/pasha-codefresh) | Approver | [Codefresh](https://www.github.com/codefresh/) |
| Nitish Kumar | [nitishfy](https://github.com/nitishfy) | Approver(cli,docs) | [Akuity](https://akuity.io/) |
| Justin Marquis | [34fathombelow](https://github.com/34fathombelow) | Approver(docs/ci) | [Akuity](https://akuity.io/) |
| Alexander Matyushentsev | [alexmt](https://github.com/alexmt) | Lead | [Akuity](https://akuity.io/) |
| Nicholas Morey | [morey-tech](https://github.com/morey-tech) | Reviewer(docs) | [Akuity](https://akuity.io/) |
| Papapetrou Patroklos | [ppapapetrou76](https://github.com/ppapapetrou76) | Reviewer | [Octopus Deploy](https://octopus.com/) |
| Blake Pettersson | [blakepettersson](https://github.com/blakepettersson) | Approver | [Akuity](https://akuity.io/) |
| Ishita Sequeira | [ishitasequeira](https://github.com/ishitasequeira) | Approver | [Red Hat](https://redhat.com/) |
| Ashutosh Singh | [ashutosh16](https://github.com/ashutosh16) | Approver(docs) | [Intuit](https://www.github.com/intuit/) |
| Linghao Su | [linghaoSu](https://github.com/linghaoSu) | Reviewer | [DaoCloud](https://daocloud.io) |
| Jesse Suen | [jessesuen](https://github.com/jessesuen) | Approver | [Akuity](https://akuity.io/) |
| Yuan Tang | [terrytangyuan](https://github.com/terrytangyuan) | Reviewer | [Red Hat](https://redhat.com/) |
| William Tam | [wtam2018](https://github.com/wtam2018) | Reviewer | [Red Hat](https://redhat.com/) |
| Ryan Umstead | [rumstead](https://github.com/rumstead) | Approver | [Black Rock](https://www.github.com/blackrock/) |
| Regina Voloshin | [reggie-k](https://github.com/reggie-k) | Approver | [Octopus Deploy](https://octopus.com/) |
| Hong Wang | [wanghong230](https://github.com/wanghong230) | Reviewer | [Akuity](https://akuity.io/) |
| Jonathan West | [jgwest](https://github.com/jgwest) | Approver | [Red Hat](https://redhat.com/) |

View File

@@ -56,8 +56,8 @@ endif
ARGOCD_PROCFILE?=Procfile
# pointing to python 3.12 to match https://github.com/argoproj/argo-cd/blob/master/.readthedocs.yaml
MKDOCS_DOCKER_IMAGE?=python:3.12-alpine
# pointing to python 3.7 to match https://github.com/argoproj/argo-cd/blob/master/.readthedocs.yml
MKDOCS_DOCKER_IMAGE?=python:3.7-alpine
MKDOCS_RUN_ARGS?=
# Configuration for building argocd-test-tools image
@@ -76,15 +76,15 @@ ARGOCD_E2E_REDIS_PORT?=6379
ARGOCD_E2E_DEX_PORT?=5556
ARGOCD_E2E_YARN_HOST?=localhost
ARGOCD_E2E_DISABLE_AUTH?=
ARGOCD_E2E_DIR?=/tmp/argo-e2e
ARGOCD_E2E_TEST_TIMEOUT?=90m
ARGOCD_E2E_RERUN_FAILS?=5
ARGOCD_IN_CI?=false
ARGOCD_TEST_E2E?=true
ARGOCD_BIN_MODE?=true
ARGOCD_LINT_GOGC?=20
# Depending on where we are (legacy or non-legacy pwd), we need to use
# different Docker volume mounts for our source tree
LEGACY_PATH=$(GOPATH)/src/github.com/argoproj/argo-cd
@@ -144,6 +144,7 @@ define run-in-test-client
-e ARGOCD_E2E_K3S=$(ARGOCD_E2E_K3S) \
-e GITHUB_TOKEN \
-e GOCACHE=/tmp/go-build-cache \
-e ARGOCD_LINT_GOGC=$(ARGOCD_LINT_GOGC) \
-v ${DOCKER_SRC_MOUNT} \
-v ${GOPATH}/pkg/mod:/go/pkg/mod${VOLUME_MOUNT} \
-v ${GOCACHE}:/tmp/go-build-cache${VOLUME_MOUNT} \
@@ -197,40 +198,19 @@ endif
ifneq (${GIT_TAG},)
IMAGE_TAG=${GIT_TAG}
override LDFLAGS += -X ${PACKAGE}.gitTag=${GIT_TAG}
LDFLAGS += -X ${PACKAGE}.gitTag=${GIT_TAG}
else
IMAGE_TAG?=latest
endif
# defaults for building images and manifests
ifeq (${DOCKER_PUSH},true)
ifndef IMAGE_NAMESPACE
$(error IMAGE_NAMESPACE must be set to push images (e.g. IMAGE_NAMESPACE=argoproj))
endif
endif
# Consruct prefix for docker image
# Note: keeping same logic as in hacks/update_manifests.sh
ifdef IMAGE_REGISTRY
ifdef IMAGE_NAMESPACE
IMAGE_PREFIX=${IMAGE_REGISTRY}/${IMAGE_NAMESPACE}/
else
$(error IMAGE_NAMESPACE must be set when IMAGE_REGISTRY is set (e.g. IMAGE_NAMESPACE=argoproj))
endif
else
ifdef IMAGE_NAMESPACE
# for backwards compatibility with the old way like IMAGE_NAMESPACE='quay.io/argoproj'
IMAGE_PREFIX=${IMAGE_NAMESPACE}/
else
# Neither namespace nor registry given - apply the default values
IMAGE_REGISTRY="quay.io"
IMAGE_NAMESPACE="argoproj"
IMAGE_PREFIX=${IMAGE_REGISTRY}/${IMAGE_NAMESPACE}/
endif
endif
ifndef IMAGE_REPOSITORY
IMAGE_REPOSITORY=argocd
endif
.PHONY: all
@@ -328,11 +308,12 @@ endif
.PHONY: manifests-local
manifests-local:
./hack/update-manifests.sh
.PHONY: manifests
manifests: test-tools-image
$(call run-in-test-client,make manifests-local IMAGE_REGISTRY='${IMAGE_REGISTRY}' IMAGE_NAMESPACE='${IMAGE_NAMESPACE}' IMAGE_REPOSITORY='${IMAGE_REPOSITORY}' IMAGE_TAG='${IMAGE_TAG}')
# consolidated binary for cli, util, server, repo-server, controller
$(call run-in-test-client,make manifests-local IMAGE_NAMESPACE='${IMAGE_NAMESPACE}' IMAGE_TAG='${IMAGE_TAG}')
# consolidated binary for cli, util, server, repo-server, controller
.PHONY: argocd-all
argocd-all: clean-debug
CGO_ENABLED=${CGO_FLAG} GOOS=${GOOS} GOARCH=${GOARCH} GODEBUG="tarinsecurepath=0,zipinsecurepath=0" go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/${BIN_NAME} ./cmd
@@ -363,23 +344,23 @@ ifeq ($(DEV_IMAGE), true)
IMAGE_TAG="dev-$(shell git describe --always --dirty)"
image: build-ui
DOCKER_BUILDKIT=1 $(DOCKER) build --platform=$(TARGET_ARCH) -t argocd-base --target argocd-base .
GOOS=linux GOARCH=$(TARGET_ARCH:linux/%=%) GODEBUG="tarinsecurepath=0,zipinsecurepath=0" go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd ./cmd
CGO_ENABLED=${CGO_FLAG} GOOS=linux GOARCH=amd64 GODEBUG="tarinsecurepath=0,zipinsecurepath=0" go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd ./cmd
ln -sfn ${DIST_DIR}/argocd ${DIST_DIR}/argocd-server
ln -sfn ${DIST_DIR}/argocd ${DIST_DIR}/argocd-application-controller
ln -sfn ${DIST_DIR}/argocd ${DIST_DIR}/argocd-repo-server
ln -sfn ${DIST_DIR}/argocd ${DIST_DIR}/argocd-cmp-server
ln -sfn ${DIST_DIR}/argocd ${DIST_DIR}/argocd-dex
cp Dockerfile.dev dist
DOCKER_BUILDKIT=1 $(DOCKER) build --platform=$(TARGET_ARCH) -t $(IMAGE_PREFIX)$(IMAGE_REPOSITORY):$(IMAGE_TAG) -f dist/Dockerfile.dev dist
DOCKER_BUILDKIT=1 $(DOCKER) build --platform=$(TARGET_ARCH) -t $(IMAGE_PREFIX)argocd:$(IMAGE_TAG) -f dist/Dockerfile.dev dist
else
image:
DOCKER_BUILDKIT=1 $(DOCKER) build -t $(IMAGE_PREFIX)$(IMAGE_REPOSITORY):$(IMAGE_TAG) --platform=$(TARGET_ARCH) .
DOCKER_BUILDKIT=1 $(DOCKER) build -t $(IMAGE_PREFIX)argocd:$(IMAGE_TAG) --platform=$(TARGET_ARCH) .
endif
@if [ "$(DOCKER_PUSH)" = "true" ] ; then $(DOCKER) push $(IMAGE_PREFIX)$(IMAGE_REPOSITORY):$(IMAGE_TAG) ; fi
@if [ "$(DOCKER_PUSH)" = "true" ] ; then $(DOCKER) push $(IMAGE_PREFIX)argocd:$(IMAGE_TAG) ; fi
.PHONY: armimage
armimage:
$(DOCKER) build -t $(IMAGE_PREFIX)(IMAGE_REPOSITORY):$(IMAGE_TAG)-arm .
$(DOCKER) build -t $(IMAGE_PREFIX)argocd:$(IMAGE_TAG)-arm .
.PHONY: builder-image
builder-image:
@@ -411,7 +392,9 @@ lint: test-tools-image
.PHONY: lint-local
lint-local:
golangci-lint --version
golangci-lint run --fix --verbose
# NOTE: If you get a "Killed" OOM message, try reducing the value of GOGC
# See https://github.com/golangci/golangci-lint#memory-usage-of-golangci-lint
GOGC=$(ARGOCD_LINT_GOGC) GOMAXPROCS=2 golangci-lint run --fix --verbose
.PHONY: lint-ui
lint-ui: test-tools-image
@@ -443,19 +426,13 @@ test: test-tools-image
# Run all unit tests (local version)
.PHONY: test-local
test-local: test-gitops-engine
test-local:
if test "$(TEST_MODULE)" = ""; then \
DIST_DIR=${DIST_DIR} RERUN_FAILS=0 PACKAGES=`go list ./... | grep -v 'test/e2e'` ./hack/test.sh -args -test.gocoverdir="$(PWD)/test-results"; \
else \
DIST_DIR=${DIST_DIR} RERUN_FAILS=0 PACKAGES="$(TEST_MODULE)" ./hack/test.sh -args -test.gocoverdir="$(PWD)/test-results" "$(TEST_MODULE)"; \
fi
# Run gitops-engine unit tests
.PHONY: test-gitops-engine
test-gitops-engine:
mkdir -p $(PWD)/test-results
cd gitops-engine && go test -race -cover ./... -args -test.gocoverdir="$(PWD)/test-results"
.PHONY: test-race
test-race: test-tools-image
mkdir -p $(GOCACHE)
@@ -481,7 +458,7 @@ test-e2e:
test-e2e-local: cli-local
# NO_PROXY ensures all tests don't go out through a proxy if one is configured on the test system
export GO111MODULE=off
DIST_DIR=${DIST_DIR} RERUN_FAILS=$(ARGOCD_E2E_RERUN_FAILS) PACKAGES="./test/e2e" ARGOCD_E2E_RECORD=${ARGOCD_E2E_RECORD} ARGOCD_CONFIG_DIR=$(HOME)/.config/argocd-e2e ARGOCD_GPG_ENABLED=true NO_PROXY=* ./hack/test.sh -timeout $(ARGOCD_E2E_TEST_TIMEOUT) -v -args -test.gocoverdir="$(PWD)/test-results"
DIST_DIR=${DIST_DIR} RERUN_FAILS=5 PACKAGES="./test/e2e" ARGOCD_E2E_RECORD=${ARGOCD_E2E_RECORD} ARGOCD_CONFIG_DIR=$(HOME)/.config/argocd-e2e ARGOCD_GPG_ENABLED=true NO_PROXY=* ./hack/test.sh -timeout $(ARGOCD_E2E_TEST_TIMEOUT) -v -args -test.gocoverdir="$(PWD)/test-results"
# Spawns a shell in the test server container for debugging purposes
debug-test-server: test-tools-image
@@ -505,13 +482,13 @@ start-e2e-local: mod-vendor-local dep-ui-local cli-local
kubectl create ns argocd-e2e-external || true
kubectl create ns argocd-e2e-external-2 || true
kubectl config set-context --current --namespace=argocd-e2e
kustomize build test/manifests/base | kubectl apply --server-side --force-conflicts -f -
kustomize build test/manifests/base | kubectl apply -f -
kubectl apply -f https://raw.githubusercontent.com/open-cluster-management/api/a6845f2ebcb186ec26b832f60c988537a58f3859/cluster/v1alpha1/0000_04_clusters.open-cluster-management.io_placementdecisions.crd.yaml
# Create GPG keys and source directories
if test -d $(ARGOCD_E2E_DIR)/app/config/gpg; then rm -rf $(ARGOCD_E2E_DIR)/app/config/gpg/*; fi
mkdir -p $(ARGOCD_E2E_DIR)/app/config/gpg/keys && chmod 0700 $(ARGOCD_E2E_DIR)/app/config/gpg/keys
mkdir -p $(ARGOCD_E2E_DIR)/app/config/gpg/source && chmod 0700 $(ARGOCD_E2E_DIR)/app/config/gpg/source
mkdir -p $(ARGOCD_E2E_DIR)/app/config/plugin && chmod 0700 $(ARGOCD_E2E_DIR)/app/config/plugin
if test -d /tmp/argo-e2e/app/config/gpg; then rm -rf /tmp/argo-e2e/app/config/gpg/*; fi
mkdir -p /tmp/argo-e2e/app/config/gpg/keys && chmod 0700 /tmp/argo-e2e/app/config/gpg/keys
mkdir -p /tmp/argo-e2e/app/config/gpg/source && chmod 0700 /tmp/argo-e2e/app/config/gpg/source
mkdir -p /tmp/argo-e2e/app/config/plugin && chmod 0700 /tmp/argo-e2e/app/config/plugin
# create folders to hold go coverage results for each component
mkdir -p /tmp/coverage/app-controller
mkdir -p /tmp/coverage/api-server
@@ -520,15 +497,13 @@ start-e2e-local: mod-vendor-local dep-ui-local cli-local
mkdir -p /tmp/coverage/notification
mkdir -p /tmp/coverage/commit-server
# set paths for locally managed ssh known hosts and tls certs data
ARGOCD_E2E_DIR=$(ARGOCD_E2E_DIR) \
ARGOCD_SSH_DATA_PATH=$(ARGOCD_E2E_DIR)/app/config/ssh \
ARGOCD_TLS_DATA_PATH=$(ARGOCD_E2E_DIR)/app/config/tls \
ARGOCD_GPG_DATA_PATH=$(ARGOCD_E2E_DIR)/app/config/gpg/source \
ARGOCD_GNUPGHOME=$(ARGOCD_E2E_DIR)/app/config/gpg/keys \
ARGOCD_SSH_DATA_PATH=/tmp/argo-e2e/app/config/ssh \
ARGOCD_TLS_DATA_PATH=/tmp/argo-e2e/app/config/tls \
ARGOCD_GPG_DATA_PATH=/tmp/argo-e2e/app/config/gpg/source \
ARGOCD_GNUPGHOME=/tmp/argo-e2e/app/config/gpg/keys \
ARGOCD_GPG_ENABLED=$(ARGOCD_GPG_ENABLED) \
ARGOCD_PLUGINCONFIGFILEPATH=$(ARGOCD_E2E_DIR)/app/config/plugin \
ARGOCD_PLUGINSOCKFILEPATH=$(ARGOCD_E2E_DIR)/app/config/plugin \
ARGOCD_GIT_CONFIG=$(PWD)/test/e2e/fixture/gitconfig \
ARGOCD_PLUGINCONFIGFILEPATH=/tmp/argo-e2e/app/config/plugin \
ARGOCD_PLUGINSOCKFILEPATH=/tmp/argo-e2e/app/config/plugin \
ARGOCD_E2E_DISABLE_AUTH=false \
ARGOCD_ZJWT_FEATURE_FLAG=always \
ARGOCD_IN_CI=$(ARGOCD_IN_CI) \
@@ -605,7 +580,7 @@ build-docs-local:
.PHONY: build-docs
build-docs:
$(DOCKER) run ${MKDOCS_RUN_ARGS} --rm -it -v ${CURRENT_DIR}:/docs -w /docs --entrypoint "" ${MKDOCS_DOCKER_IMAGE} sh -c 'pip install -r docs/requirements.txt; mkdocs build'
$(DOCKER) run ${MKDOCS_RUN_ARGS} --rm -it -v ${CURRENT_DIR}:/docs -w /docs --entrypoint "" ${MKDOCS_DOCKER_IMAGE} sh -c 'pip install mkdocs; pip install $$(mkdocs get-deps); mkdocs build'
.PHONY: serve-docs-local
serve-docs-local:
@@ -613,7 +588,7 @@ serve-docs-local:
.PHONY: serve-docs
serve-docs:
$(DOCKER) run ${MKDOCS_RUN_ARGS} --rm -it -p 8000:8000 -v ${CURRENT_DIR}:/docs -w /docs --entrypoint "" ${MKDOCS_DOCKER_IMAGE} sh -c 'pip install -r docs/requirements.txt; mkdocs serve -a $$(ip route get 1 | awk '\''{print $$7}'\''):8000'
$(DOCKER) run ${MKDOCS_RUN_ARGS} --rm -it -p 8000:8000 -v ${CURRENT_DIR}:/docs -w /docs --entrypoint "" ${MKDOCS_DOCKER_IMAGE} sh -c 'pip install mkdocs; pip install $$(mkdocs get-deps); mkdocs serve -a $$(ip route get 1 | awk '\''{print $$7}'\''):8000'
# Verify that kubectl can connect to your K8s cluster from Docker
.PHONY: verify-kube-connect

View File

@@ -2,7 +2,7 @@ controller: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run
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'}"
dex: sh -c "ARGOCD_BINARY_NAME=argocd-dex go run github.com/argoproj/argo-cd/v3/cmd gendexcfg -o `pwd`/dist/dex.yaml && (test -f dist/dex.yaml || { echo 'Failed to generate dex configuration'; exit 1; }) && docker run --rm -p ${ARGOCD_E2E_DEX_PORT:-5556}:${ARGOCD_E2E_DEX_PORT:-5556} -v `pwd`/dist/dex.yaml:/dex.yaml ghcr.io/dexidp/dex:$(grep "image: ghcr.io/dexidp/dex" manifests/base/dex/argocd-dex-server-deployment.yaml | cut -d':' -f3) dex serve /dex.yaml"
redis: hack/start-redis-with-password.sh
repo-server: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "export PATH=./dist:\$PATH && [ -n \"\$ARGOCD_GIT_CONFIG\" ] && export GIT_CONFIG_GLOBAL=\$ARGOCD_GIT_CONFIG && export GIT_CONFIG_NOSYSTEM=1; GOCOVERDIR=${ARGOCD_COVERAGE_DIR:-/tmp/coverage/repo-server} FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_GNUPGHOME=${ARGOCD_GNUPGHOME:-/tmp/argocd-local/gpg/keys} ARGOCD_PLUGINSOCKFILEPATH=${ARGOCD_PLUGINSOCKFILEPATH:-./test/cmp} ARGOCD_GPG_DATA_PATH=${ARGOCD_GPG_DATA_PATH:-/tmp/argocd-local/gpg/source} ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-repo-server ARGOCD_GPG_ENABLED=${ARGOCD_GPG_ENABLED:-false} $COMMAND --loglevel debug --port ${ARGOCD_E2E_REPOSERVER_PORT:-8081} --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --otlp-address=${ARGOCD_OTLP_ADDRESS}"
repo-server: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "GOCOVERDIR=${ARGOCD_COVERAGE_DIR:-/tmp/coverage/repo-server} FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_GNUPGHOME=${ARGOCD_GNUPGHOME:-/tmp/argocd-local/gpg/keys} ARGOCD_PLUGINSOCKFILEPATH=${ARGOCD_PLUGINSOCKFILEPATH:-./test/cmp} ARGOCD_GPG_DATA_PATH=${ARGOCD_GPG_DATA_PATH:-/tmp/argocd-local/gpg/source} ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-repo-server ARGOCD_GPG_ENABLED=${ARGOCD_GPG_ENABLED:-false} $COMMAND --loglevel debug --port ${ARGOCD_E2E_REPOSERVER_PORT:-8081} --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --otlp-address=${ARGOCD_OTLP_ADDRESS}"
cmp-server: [ "$ARGOCD_E2E_TEST" = 'true' ] && exit 0 || [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_BINARY_NAME=argocd-cmp-server ARGOCD_PLUGINSOCKFILEPATH=${ARGOCD_PLUGINSOCKFILEPATH:-./test/cmp} $COMMAND --config-dir-path ./test/cmp --loglevel debug --otlp-address=${ARGOCD_OTLP_ADDRESS}"
commit-server: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "GOCOVERDIR=${ARGOCD_COVERAGE_DIR:-/tmp/coverage/commit-server} FORCE_LOG_COLORS=1 ARGOCD_BINARY_NAME=argocd-commit-server $COMMAND --loglevel debug --port ${ARGOCD_E2E_COMMITSERVER_PORT:-8086}"
ui: sh -c 'cd ui && ${ARGOCD_E2E_YARN_CMD:-yarn} start'
@@ -11,4 +11,4 @@ helm-registry: test/fixture/testrepos/start-helm-registry.sh
oci-registry: test/fixture/testrepos/start-authenticated-helm-registry.sh
dev-mounter: [ "$ARGOCD_E2E_TEST" != "true" ] && go run hack/dev-mounter/main.go --configmap argocd-ssh-known-hosts-cm=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} --configmap argocd-tls-certs-cm=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} --configmap argocd-gpg-keys-cm=${ARGOCD_GPG_DATA_PATH:-/tmp/argocd-local/gpg/source}
applicationset-controller: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "GOCOVERDIR=${ARGOCD_COVERAGE_DIR:-/tmp/coverage/applicationset-controller} FORCE_LOG_COLORS=4 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-applicationset-controller $COMMAND --loglevel debug --metrics-addr localhost:12345 --probe-addr localhost:12346 --argocd-repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081}"
notification: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "GOCOVERDIR=${ARGOCD_COVERAGE_DIR:-/tmp/coverage/notification} FORCE_LOG_COLORS=4 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_BINARY_NAME=argocd-notifications $COMMAND --loglevel debug --application-namespaces=${ARGOCD_APPLICATION_NAMESPACES:-''} --self-service-notification-enabled=${ARGOCD_NOTIFICATION_CONTROLLER_SELF_SERVICE_NOTIFICATION_ENABLED:-'false'}"
notification: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "GOCOVERDIR=${ARGOCD_COVERAGE_DIR:-/tmp/coverage/notification} FORCE_LOG_COLORS=4 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_BINARY_NAME=argocd-notifications $COMMAND --loglevel debug --application-namespaces=${ARGOCD_APPLICATION_NAMESPACES:-''} --self-service-notification-enabled=${ARGOCD_NOTIFICATION_CONTROLLER_SELF_SERVICE_NOTIFICATION_ENABLED:-'false'}"

View File

@@ -13,7 +13,6 @@
[![Twitter Follow](https://img.shields.io/twitter/follow/argoproj?style=social)](https://twitter.com/argoproj)
[![Slack](https://img.shields.io/badge/slack-argoproj-brightgreen.svg?logo=slack)](https://argoproj.github.io/community/join-slack)
[![LinkedIn](https://img.shields.io/badge/LinkedIn-argoproj-blue.svg?logo=linkedin)](https://www.linkedin.com/company/argoproj/)
[![Bluesky](https://img.shields.io/badge/Bluesky-argoproj-blue.svg?style=social&logo=bluesky)](https://bsky.app/profile/argoproj.bsky.social)
# Argo CD - Declarative Continuous Delivery for Kubernetes

View File

@@ -3,9 +3,9 @@ header:
expiration-date: '2024-10-31T00:00:00.000Z' # One year from initial release.
last-updated: '2023-10-27'
last-reviewed: '2023-10-27'
commit-hash: 814db444c36503851dc3d45cf9c44394821ca1a4
commit-hash: 06ef059f9fc7cf9da2dfaef2a505ee1e3c693485
project-url: https://github.com/argoproj/argo-cd
project-release: v3.4.0
project-release: v3.3.0
changelog: https://github.com/argoproj/argo-cd/releases
license: https://github.com/argoproj/argo-cd/blob/master/LICENSE
project-lifecycle:

View File

@@ -123,7 +123,6 @@ k8s_resource(
'9345:2345',
'8083:8083'
],
resource_deps=['build']
)
# track crds
@@ -149,7 +148,6 @@ k8s_resource(
'9346:2345',
'8084:8084'
],
resource_deps=['build']
)
# track argocd-redis resources and port forward
@@ -164,7 +162,6 @@ k8s_resource(
port_forwards=[
'6379:6379',
],
resource_deps=['build']
)
# track argocd-applicationset-controller resources
@@ -183,7 +180,6 @@ k8s_resource(
'8085:8080',
'7000:7000'
],
resource_deps=['build']
)
# track argocd-application-controller resources
@@ -201,7 +197,6 @@ k8s_resource(
'9348:2345',
'8086:8082',
],
resource_deps=['build']
)
# track argocd-notifications-controller resources
@@ -219,7 +214,6 @@ k8s_resource(
'9349:2345',
'8087:9001',
],
resource_deps=['build']
)
# track argocd-dex-server resources
@@ -231,7 +225,6 @@ k8s_resource(
'argocd-dex-server:role',
'argocd-dex-server:rolebinding',
],
resource_deps=['build']
)
# track argocd-commit-server resources
@@ -246,19 +239,6 @@ k8s_resource(
'8088:8087',
'8089:8086',
],
resource_deps=['build']
)
# ui dependencies
local_resource(
'node-modules',
'yarn',
dir='ui',
deps = [
'ui/package.json',
'ui/yarn.lock',
],
allow_parallel=True,
)
# docker for ui
@@ -280,7 +260,6 @@ k8s_resource(
port_forwards=[
'4000:4000',
],
resource_deps=['node-modules'],
)
# linting
@@ -299,7 +278,6 @@ local_resource(
'ui',
],
allow_parallel=True,
resource_deps=['node-modules'],
)
local_resource(
@@ -309,6 +287,5 @@ local_resource(
'go.mod',
'go.sum',
],
allow_parallel=True,
)

View File

@@ -31,7 +31,6 @@ Currently, the following organizations are **officially** using Argo CD:
1. [ANSTO - Australian Synchrotron](https://www.synchrotron.org.au/)
1. [Ant Group](https://www.antgroup.com/)
1. [AppDirect](https://www.appdirect.com)
1. [Arcadia](https://www.arcadia.io)
1. [Arctiq Inc.](https://www.arctiq.ca)
1. [Artemis Health by Nomi Health](https://www.artemishealth.com/)
1. [Arturia](https://www.arturia.com)
@@ -87,7 +86,6 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Codefresh](https://www.codefresh.io/)
1. [Codility](https://www.codility.com/)
1. [Cognizant](https://www.cognizant.com/)
1. [Collins Aerospace](https://www.collinsaerospace.com/)
1. [Commonbond](https://commonbond.co/)
1. [Compatio.AI](https://compatio.ai/)
1. [Contlo](https://contlo.com/)
@@ -101,7 +99,6 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Datarisk](https://www.datarisk.io/)
1. [Daydream](https://daydream.ing)
1. [Deloitte](https://www.deloitte.com/)
1. [Dematic](https://www.dematic.com)
1. [Deutsche Telekom AG](https://telekom.com)
1. [Deutsche Bank AG](https://www.deutsche-bank.de/)
1. [Devopsi - Poland Software/DevOps Consulting](https://devopsi.pl/)
@@ -110,7 +107,6 @@ Currently, the following organizations are **officially** using Argo CD:
1. [DigitalOcean](https://www.digitalocean.com)
1. [Divar](https://divar.ir)
1. [Divistant](https://divistant.com)
2. [DocNetwork](https://docnetwork.org/)
1. [Dott](https://ridedott.com)
1. [Doubble](https://www.doubble.app)
1. [Doximity](https://www.doximity.com/)
@@ -125,7 +121,6 @@ Currently, the following organizations are **officially** using Argo CD:
1. [enigmo](https://enigmo.co.jp/)
1. [Envoy](https://envoy.com/)
1. [eSave](https://esave.es/)
1. [Expedia](https://www.expedia.com)
1. [Factorial](https://factorialhr.com/)
1. [Farfetch](https://www.farfetch.com)
1. [Faro](https://www.faro.com/)
@@ -186,7 +181,6 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Instruqt](https://www.instruqt.com)
1. [Intel](https://www.intel.com)
1. [Intuit](https://www.intuit.com/)
1. [IQVIA](https://www.iqvia.com/)
1. [Jellysmack](https://www.jellysmack.com)
1. [Joblift](https://joblift.com/)
1. [JovianX](https://www.jovianx.com/)
@@ -208,7 +202,6 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Kurly](https://www.kurly.com/)
1. [Kvist](https://kvistsolutions.com)
1. [Kyriba](https://www.kyriba.com/)
1. [Lattice](https://lattice.com)
1. [LeFigaro](https://www.lefigaro.fr/)
1. [Lely](https://www.lely.com/)
1. [LexisNexis](https://www.lexisnexis.com/)
@@ -239,14 +232,12 @@ Currently, the following organizations are **officially** using Argo CD:
1. [mixi Group](https://mixi.co.jp/)
1. [Moengage](https://www.moengage.com/)
1. [Money Forward](https://corp.moneyforward.com/en/)
1. [MongoDB](https://www.mongodb.com/)
1. [MOO Print](https://www.moo.com/)
1. [Mozilla](https://www.mozilla.org)
1. [MTN Group](https://www.mtn.com/)
1. [Municipality of The Hague](https://www.denhaag.nl/)
1. [My Job Glasses](https://myjobglasses.com)
1. [Natura &Co](https://naturaeco.com/)
1. [Netease Cloud Music](https://music.163.com/)
1. [Nethopper](https://nethopper.io)
1. [New Relic](https://newrelic.com/)
1. [Nextbasket](https://nextbasket.com)
@@ -320,7 +311,6 @@ Currently, the following organizations are **officially** using Argo CD:
1. [RightRev](https://rightrev.com/)
1. [Rijkswaterstaat](https://www.rijkswaterstaat.nl/en)
1. Rise
1. [RISK IDENT](https://riskident.com/)
1. [Riskified](https://www.riskified.com/)
1. [Robotinfra](https://www.robotinfra.com)
1. [Rocket.Chat](https://rocket.chat)
@@ -387,7 +377,6 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Ticketmaster](https://ticketmaster.com)
1. [Tiger Analytics](https://www.tigeranalytics.com/)
1. [Tigera](https://www.tigera.io/)
1. [Topicus.Education](https://topicus.nl/en/sectors/education)
1. [Toss](https://toss.im/en)
1. [Trendyol](https://www.trendyol.com/)
1. [tru.ID](https://tru.id)

View File

@@ -1 +1 @@
3.4.0
3.3.0

View File

@@ -57,7 +57,6 @@ import (
"github.com/argoproj/argo-cd/v3/common"
applog "github.com/argoproj/argo-cd/v3/util/app/log"
"github.com/argoproj/argo-cd/v3/util/db"
"github.com/argoproj/argo-cd/v3/util/settings"
argov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
argoutil "github.com/argoproj/argo-cd/v3/util/argo"
@@ -76,15 +75,9 @@ const (
AllAtOnceDeletionOrder = "AllAtOnce"
)
var defaultPreservedFinalizers = []string{
argov1alpha1.PreDeleteFinalizerName,
argov1alpha1.PostDeleteFinalizerName,
}
var defaultPreservedAnnotations = []string{
NotifiedAnnotationKey,
argov1alpha1.AnnotationKeyRefresh,
argov1alpha1.AnnotationKeyHydrate,
}
type deleteInOrder struct {
@@ -111,7 +104,6 @@ type ApplicationSetReconciler struct {
GlobalPreservedLabels []string
Metrics *metrics.ApplicationsetMetrics
MaxResourcesStatusCount int
ClusterInformer *settings.ClusterInformer
}
// +kubebuilder:rbac:groups=argoproj.io,resources=applicationsets,verbs=get;list;watch;create;update;patch;delete
@@ -184,16 +176,6 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
return ctrl.Result{}, err
}
// ensure finalizer exists if deletionOrder is set as Reverse
if r.EnableProgressiveSyncs && isProgressiveSyncDeletionOrderReversed(&applicationSetInfo) {
if !controllerutil.ContainsFinalizer(&applicationSetInfo, argov1alpha1.ResourcesFinalizerName) {
controllerutil.AddFinalizer(&applicationSetInfo, argov1alpha1.ResourcesFinalizerName)
if err := r.Update(ctx, &applicationSetInfo); err != nil {
return ctrl.Result{}, err
}
}
}
// Log a warning if there are unrecognized generators
_ = utils.CheckInvalidGenerators(&applicationSetInfo)
// desiredApplications is the main list of all expected Applications from all generators in this appset.
@@ -671,9 +653,8 @@ func (r *ApplicationSetReconciler) SetupWithManager(mgr ctrl.Manager, enableProg
Watches(
&corev1.Secret{},
&clusterSecretEventHandler{
Client: mgr.GetClient(),
Log: log.WithField("type", "createSecretEventHandler"),
ApplicationSetNamespaces: r.ApplicationSetNamespaces,
Client: mgr.GetClient(),
Log: log.WithField("type", "createSecretEventHandler"),
}).
Complete(r)
}
@@ -750,19 +731,21 @@ func (r *ApplicationSetReconciler) createOrUpdateInCluster(ctx context.Context,
}
}
// Preserve deleting finalizers and avoid diff conflicts
for _, finalizer := range defaultPreservedFinalizers {
for _, f := range found.Finalizers {
// For finalizers, use prefix matching in case it contains "/" stages
if strings.HasPrefix(f, finalizer) {
generatedApp.Finalizers = append(generatedApp.Finalizers, f)
// Preserve post-delete finalizers:
// https://github.com/argoproj/argo-cd/issues/17181
for _, finalizer := range found.Finalizers {
if strings.HasPrefix(finalizer, argov1alpha1.PostDeleteFinalizerName) {
if generatedApp.Finalizers == nil {
generatedApp.Finalizers = []string{}
}
generatedApp.Finalizers = append(generatedApp.Finalizers, finalizer)
}
}
found.Annotations = generatedApp.Annotations
found.Labels = generatedApp.Labels
found.Finalizers = generatedApp.Finalizers
found.Labels = generatedApp.Labels
return controllerutil.SetControllerReference(&applicationSet, found, r.Scheme)
})
@@ -827,7 +810,7 @@ func (r *ApplicationSetReconciler) getCurrentApplications(ctx context.Context, a
// deleteInCluster will delete Applications that are currently on the cluster, but not in appList.
// The function must be called after all generators had been called and generated applications
func (r *ApplicationSetReconciler) deleteInCluster(ctx context.Context, logCtx *log.Entry, applicationSet argov1alpha1.ApplicationSet, desiredApplications []argov1alpha1.Application) error {
clusterList, err := utils.ListClusters(r.ClusterInformer)
clusterList, err := utils.ListClusters(ctx, r.KubeClientset, r.ArgoCDNamespace)
if err != nil {
return fmt.Errorf("error listing clusters: %w", err)
}
@@ -893,14 +876,16 @@ func (r *ApplicationSetReconciler) removeFinalizerOnInvalidDestination(ctx conte
// Detect if the destination's server field does not match an existing cluster
matchingCluster := false
for _, cluster := range clusterList {
// A cluster matches if either the server matches OR the name matches
// This handles cases where:
// 1. The cluster is the in-cluster (server=https://kubernetes.default.svc, name=in-cluster)
// 2. A custom cluster has the same server as in-cluster but a different name
if destCluster.Server == cluster.Server || (destCluster.Name != "" && cluster.Name != "" && destCluster.Name == cluster.Name) {
matchingCluster = true
break
if destCluster.Server != cluster.Server {
continue
}
if destCluster.Name != cluster.Name {
continue
}
matchingCluster = true
break
}
if !matchingCluster {

View File

@@ -1,7 +1,6 @@
package controllers
import (
"context"
"encoding/json"
"errors"
"fmt"
@@ -20,7 +19,6 @@ import (
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
kubefake "k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
crtclient "sigs.k8s.io/controller-runtime/pkg/client"
@@ -590,72 +588,6 @@ 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{
@@ -1078,7 +1010,7 @@ func TestCreateOrUpdateInCluster(t *testing.T) {
},
},
{
name: "Ensure that argocd pre-delete and post-delete finalizers are preserved from an existing app",
name: "Ensure that argocd post-delete finalizers are preserved from an existing app",
appSet: v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
@@ -1103,11 +1035,8 @@ func TestCreateOrUpdateInCluster(t *testing.T) {
Namespace: "namespace",
ResourceVersion: "2",
Finalizers: []string{
"non-argo-finalizer",
v1alpha1.PreDeleteFinalizerName,
v1alpha1.PreDeleteFinalizerName + "/stage1",
v1alpha1.PostDeleteFinalizerName,
v1alpha1.PostDeleteFinalizerName + "/stage2",
v1alpha1.PostDeleteFinalizerName + "/mystage",
},
},
Spec: v1alpha1.ApplicationSpec{
@@ -1135,12 +1064,10 @@ func TestCreateOrUpdateInCluster(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
Name: "app1",
Namespace: "namespace",
ResourceVersion: "3",
ResourceVersion: "2",
Finalizers: []string{
v1alpha1.PreDeleteFinalizerName,
v1alpha1.PreDeleteFinalizerName + "/stage1",
v1alpha1.PostDeleteFinalizerName,
v1alpha1.PostDeleteFinalizerName + "/stage2",
v1alpha1.PostDeleteFinalizerName + "/mystage",
},
},
Spec: v1alpha1.ApplicationSpec{
@@ -1190,8 +1117,6 @@ func TestRemoveFinalizerOnInvalidDestination_FinalizerTypes(t *testing.T) {
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
err = corev1.AddToScheme(scheme)
require.NoError(t, err)
for _, c := range []struct {
// name is human-readable test name
@@ -1248,6 +1173,9 @@ func TestRemoveFinalizerOnInvalidDestination_FinalizerTypes(t *testing.T) {
},
}
initObjs := []crtclient.Object{&app, &appSet}
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(initObjs...).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build()
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "my-secret",
@@ -1265,23 +1193,11 @@ func TestRemoveFinalizerOnInvalidDestination_FinalizerTypes(t *testing.T) {
},
}
initObjs := []crtclient.Object{&app, &appSet, secret}
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(initObjs...).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build()
objects := append([]runtime.Object{}, secret)
kubeclientset := kubefake.NewClientset(objects...)
kubeclientset := kubefake.NewSimpleClientset(objects...)
metrics := appsetmetrics.NewFakeAppsetMetrics()
settingsMgr := settings.NewSettingsManager(t.Context(), kubeclientset, "argocd")
// Initialize the settings manager to ensure cluster cache is ready
_ = settingsMgr.ResyncInformers()
argodb := db.NewDB("argocd", settingsMgr, kubeclientset)
clusterInformer, err := settings.NewClusterInformer(kubeclientset, "namespace")
require.NoError(t, err)
defer startAndSyncInformer(t, clusterInformer)()
argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)
r := ApplicationSetReconciler{
Client: client,
@@ -1291,7 +1207,7 @@ func TestRemoveFinalizerOnInvalidDestination_FinalizerTypes(t *testing.T) {
Metrics: metrics,
ArgoDB: argodb,
}
clusterList, err := utils.ListClusters(clusterInformer)
clusterList, err := utils.ListClusters(t.Context(), kubeclientset, "namespace")
require.NoError(t, err)
appLog := log.WithFields(applog.GetAppLogFields(&app)).WithField("appSet", "")
@@ -1308,7 +1224,7 @@ func TestRemoveFinalizerOnInvalidDestination_FinalizerTypes(t *testing.T) {
// App on the cluster should have the expected finalizers
assert.ElementsMatch(t, c.expectedFinalizers, retrievedApp.Finalizers)
// App object passed in as a parameter should have the expected finalizers
// App object passed in as a parameter should have the expected finaliers
assert.ElementsMatch(t, c.expectedFinalizers, appInputParam.Finalizers)
bytes, _ := json.MarshalIndent(retrievedApp, "", " ")
@@ -1321,8 +1237,6 @@ func TestRemoveFinalizerOnInvalidDestination_DestinationTypes(t *testing.T) {
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
err = corev1.AddToScheme(scheme)
require.NoError(t, err)
for _, c := range []struct {
// name is human-readable test name
@@ -1415,6 +1329,9 @@ func TestRemoveFinalizerOnInvalidDestination_DestinationTypes(t *testing.T) {
},
}
initObjs := []crtclient.Object{&app, &appSet}
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(initObjs...).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build()
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "my-secret",
@@ -1432,22 +1349,10 @@ func TestRemoveFinalizerOnInvalidDestination_DestinationTypes(t *testing.T) {
},
}
initObjs := []crtclient.Object{&app, &appSet, secret}
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(initObjs...).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build()
kubeclientset := getDefaultTestClientSet(secret)
metrics := appsetmetrics.NewFakeAppsetMetrics()
settingsMgr := settings.NewSettingsManager(t.Context(), kubeclientset, "argocd")
// Initialize the settings manager to ensure cluster cache is ready
_ = settingsMgr.ResyncInformers()
argodb := db.NewDB("argocd", settingsMgr, kubeclientset)
clusterInformer, err := settings.NewClusterInformer(kubeclientset, "argocd")
require.NoError(t, err)
defer startAndSyncInformer(t, clusterInformer)()
argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)
r := ApplicationSetReconciler{
Client: client,
@@ -1458,7 +1363,7 @@ func TestRemoveFinalizerOnInvalidDestination_DestinationTypes(t *testing.T) {
ArgoDB: argodb,
}
clusterList, err := utils.ListClusters(clusterInformer)
clusterList, err := utils.ListClusters(t.Context(), kubeclientset, "argocd")
require.NoError(t, err)
appLog := log.WithFields(applog.GetAppLogFields(&app)).WithField("appSet", "")
@@ -1763,8 +1668,6 @@ func TestDeleteInCluster(t *testing.T) {
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
err = corev1.AddToScheme(scheme)
require.NoError(t, err)
for _, c := range []struct {
// appSet is the application set on which the delete function is called
@@ -1877,19 +1780,12 @@ func TestDeleteInCluster(t *testing.T) {
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(initObjs...).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build()
metrics := appsetmetrics.NewFakeAppsetMetrics()
kubeclientset := kubefake.NewClientset()
clusterInformer, err := settings.NewClusterInformer(kubeclientset, "namespace")
require.NoError(t, err)
defer startAndSyncInformer(t, clusterInformer)()
r := ApplicationSetReconciler{
Client: client,
Scheme: scheme,
Recorder: record.NewFakeRecorder(len(initObjs) + len(c.expected)),
KubeClientset: kubeclientset,
Metrics: metrics,
ClusterInformer: clusterInformer,
Client: client,
Scheme: scheme,
Recorder: record.NewFakeRecorder(len(initObjs) + len(c.expected)),
KubeClientset: kubefake.NewSimpleClientset(),
Metrics: metrics,
}
err = r.deleteInCluster(t.Context(), log.NewEntry(log.StandardLogger()), c.appSet, c.desiredApps)
@@ -2041,7 +1937,7 @@ func TestValidateGeneratedApplications(t *testing.T) {
Server: "*",
},
},
ClusterResourceWhitelist: []v1alpha1.ClusterResourceRestrictionItem{
ClusterResourceWhitelist: []metav1.GroupKind{
{
Group: "*",
Kind: "*",
@@ -2220,8 +2116,6 @@ func TestReconcilerValidationProjectErrorBehaviour(t *testing.T) {
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
err = corev1.AddToScheme(scheme)
require.NoError(t, err)
project := v1alpha1.AppProject{
ObjectMeta: metav1.ObjectMeta{Name: "good-project", Namespace: "argocd"},
@@ -2265,9 +2159,6 @@ func TestReconcilerValidationProjectErrorBehaviour(t *testing.T) {
argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)
clusterInformer, err := settings.NewClusterInformer(kubeclientset, "argocd")
require.NoError(t, err)
r := ApplicationSetReconciler{
Client: client,
Scheme: scheme,
@@ -2281,7 +2172,6 @@ func TestReconcilerValidationProjectErrorBehaviour(t *testing.T) {
Policy: v1alpha1.ApplicationsSyncPolicySync,
ArgoCDNamespace: "argocd",
Metrics: metrics,
ClusterInformer: clusterInformer,
}
req := ctrl.Request{
@@ -2312,7 +2202,7 @@ func TestSetApplicationSetStatusCondition(t *testing.T) {
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
kubeclientset := kubefake.NewClientset([]runtime.Object{}...)
kubeclientset := kubefake.NewSimpleClientset([]runtime.Object{}...)
someTime := &metav1.Time{Time: time.Now().Add(-5 * time.Minute)}
existingParameterGeneratedCondition := getParametersGeneratedCondition(true, "")
existingParameterGeneratedCondition.LastTransitionTime = someTime
@@ -2783,8 +2673,6 @@ func applicationsUpdateSyncPolicyTest(t *testing.T, applicationsSyncPolicy v1alp
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
err = corev1.AddToScheme(scheme)
require.NoError(t, err)
defaultProject := v1alpha1.AppProject{
ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "argocd"},
@@ -2841,14 +2729,10 @@ func applicationsUpdateSyncPolicyTest(t *testing.T, applicationsSyncPolicy v1alp
kubeclientset := getDefaultTestClientSet(secret)
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appSet, &defaultProject, secret).WithStatusSubresource(&appSet).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build()
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appSet, &defaultProject).WithStatusSubresource(&appSet).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build()
metrics := appsetmetrics.NewFakeAppsetMetrics()
argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)
clusterInformer, err := settings.NewClusterInformer(kubeclientset, "argocd")
require.NoError(t, err)
defer startAndSyncInformer(t, clusterInformer)()
r := ApplicationSetReconciler{
Client: client,
@@ -2864,7 +2748,6 @@ func applicationsUpdateSyncPolicyTest(t *testing.T, applicationsSyncPolicy v1alp
Policy: v1alpha1.ApplicationsSyncPolicySync,
EnablePolicyOverride: allowPolicyOverride,
Metrics: metrics,
ClusterInformer: clusterInformer,
}
req := ctrl.Request{
@@ -2965,8 +2848,6 @@ func applicationsDeleteSyncPolicyTest(t *testing.T, applicationsSyncPolicy v1alp
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
err = corev1.AddToScheme(scheme)
require.NoError(t, err)
defaultProject := v1alpha1.AppProject{
ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "argocd"},
@@ -3023,16 +2904,11 @@ func applicationsDeleteSyncPolicyTest(t *testing.T, applicationsSyncPolicy v1alp
kubeclientset := getDefaultTestClientSet(secret)
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appSet, &defaultProject, secret).WithStatusSubresource(&appSet).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build()
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appSet, &defaultProject).WithStatusSubresource(&appSet).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build()
metrics := appsetmetrics.NewFakeAppsetMetrics()
argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)
clusterInformer, err := settings.NewClusterInformer(kubeclientset, "argocd")
require.NoError(t, err)
defer startAndSyncInformer(t, clusterInformer)()
r := ApplicationSetReconciler{
Client: client,
Scheme: scheme,
@@ -3047,7 +2923,6 @@ func applicationsDeleteSyncPolicyTest(t *testing.T, applicationsSyncPolicy v1alp
Policy: v1alpha1.ApplicationsSyncPolicySync,
EnablePolicyOverride: allowPolicyOverride,
Metrics: metrics,
ClusterInformer: clusterInformer,
}
req := ctrl.Request{
@@ -3140,8 +3015,6 @@ func TestPolicies(t *testing.T) {
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
err = corev1.AddToScheme(scheme)
require.NoError(t, err)
defaultProject := v1alpha1.AppProject{
ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "argocd"},
@@ -3225,11 +3098,6 @@ func TestPolicies(t *testing.T) {
argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)
clusterInformer, err := settings.NewClusterInformer(kubeclientset, "argocd")
require.NoError(t, err)
defer startAndSyncInformer(t, clusterInformer)()
r := ApplicationSetReconciler{
Client: client,
Scheme: scheme,
@@ -3242,7 +3110,6 @@ func TestPolicies(t *testing.T) {
ArgoCDNamespace: "argocd",
KubeClientset: kubeclientset,
Policy: policy,
ClusterInformer: clusterInformer,
Metrics: metrics,
}
@@ -3252,27 +3119,27 @@ func TestPolicies(t *testing.T) {
Name: "name",
},
}
ctx := t.Context()
// Check if the application is created
res, err := r.Reconcile(ctx, req)
// Check if Application is created
res, err := r.Reconcile(t.Context(), req)
require.NoError(t, err)
assert.Equal(t, time.Duration(0), res.RequeueAfter)
var app v1alpha1.Application
err = r.Get(ctx, crtclient.ObjectKey{Namespace: "argocd", Name: "my-app"}, &app)
err = r.Get(t.Context(), crtclient.ObjectKey{Namespace: "argocd", Name: "my-app"}, &app)
require.NoError(t, err)
assert.Equal(t, "value", app.Annotations["key"])
// Check if the Application is updated
// Check if Application is updated
app.Annotations["key"] = "edited"
err = r.Update(ctx, &app)
err = r.Update(t.Context(), &app)
require.NoError(t, err)
res, err = r.Reconcile(ctx, req)
res, err = r.Reconcile(t.Context(), req)
require.NoError(t, err)
assert.Equal(t, time.Duration(0), res.RequeueAfter)
err = r.Get(ctx, crtclient.ObjectKey{Namespace: "argocd", Name: "my-app"}, &app)
err = r.Get(t.Context(), crtclient.ObjectKey{Namespace: "argocd", Name: "my-app"}, &app)
require.NoError(t, err)
if c.allowedUpdate {
@@ -3281,22 +3148,22 @@ func TestPolicies(t *testing.T) {
assert.Equal(t, "edited", app.Annotations["key"])
}
// Check if the Application is deleted
err = r.Get(ctx, crtclient.ObjectKey{Namespace: "argocd", Name: "name"}, &appSet)
// Check if Application is deleted
err = r.Get(t.Context(), crtclient.ObjectKey{Namespace: "argocd", Name: "name"}, &appSet)
require.NoError(t, err)
appSet.Spec.Generators[0] = v1alpha1.ApplicationSetGenerator{
List: &v1alpha1.ListGenerator{
Elements: []apiextensionsv1.JSON{},
},
}
err = r.Update(ctx, &appSet)
err = r.Update(t.Context(), &appSet)
require.NoError(t, err)
res, err = r.Reconcile(ctx, req)
res, err = r.Reconcile(t.Context(), req)
require.NoError(t, err)
assert.Equal(t, time.Duration(0), res.RequeueAfter)
err = r.Get(ctx, crtclient.ObjectKey{Namespace: "argocd", Name: "my-app"}, &app)
err = r.Get(t.Context(), crtclient.ObjectKey{Namespace: "argocd", Name: "my-app"}, &app)
require.NoError(t, err)
if c.allowedDelete {
assert.NotNil(t, app.DeletionTimestamp)
@@ -3312,7 +3179,7 @@ func TestSetApplicationSetApplicationStatus(t *testing.T) {
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
kubeclientset := kubefake.NewClientset([]runtime.Object{}...)
kubeclientset := kubefake.NewSimpleClientset([]runtime.Object{}...)
for _, cc := range []struct {
name string
@@ -4189,7 +4056,7 @@ func TestBuildAppDependencyList(t *testing.T) {
},
} {
t.Run(cc.name, func(t *testing.T) {
kubeclientset := kubefake.NewClientset([]runtime.Object{}...)
kubeclientset := kubefake.NewSimpleClientset([]runtime.Object{}...)
argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)
@@ -4624,7 +4491,7 @@ func TestGetAppsToSync(t *testing.T) {
},
} {
t.Run(cc.name, func(t *testing.T) {
kubeclientset := kubefake.NewClientset([]runtime.Object{}...)
kubeclientset := kubefake.NewSimpleClientset([]runtime.Object{}...)
argodb := db.NewDB("argocd", settings.NewSettingsManager(t.Context(), kubeclientset, "argocd"), kubeclientset)
@@ -5311,7 +5178,7 @@ func TestUpdateApplicationSetApplicationStatus(t *testing.T) {
},
} {
t.Run(cc.name, func(t *testing.T) {
kubeclientset := kubefake.NewClientset([]runtime.Object{}...)
kubeclientset := kubefake.NewSimpleClientset([]runtime.Object{}...)
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&cc.appSet).WithStatusSubresource(&cc.appSet).Build()
metrics := appsetmetrics.NewFakeAppsetMetrics()
@@ -6064,7 +5931,7 @@ func TestUpdateApplicationSetApplicationStatusProgress(t *testing.T) {
},
} {
t.Run(cc.name, func(t *testing.T) {
kubeclientset := kubefake.NewClientset([]runtime.Object{}...)
kubeclientset := kubefake.NewSimpleClientset([]runtime.Object{}...)
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&cc.appSet).WithStatusSubresource(&cc.appSet).Build()
metrics := appsetmetrics.NewFakeAppsetMetrics()
@@ -6337,7 +6204,7 @@ func TestUpdateResourceStatus(t *testing.T) {
},
} {
t.Run(cc.name, func(t *testing.T) {
kubeclientset := kubefake.NewClientset([]runtime.Object{}...)
kubeclientset := kubefake.NewSimpleClientset([]runtime.Object{}...)
client := fake.NewClientBuilder().WithScheme(scheme).WithStatusSubresource(&cc.appSet).WithObjects(&cc.appSet).Build()
metrics := appsetmetrics.NewFakeAppsetMetrics()
@@ -6427,7 +6294,7 @@ func TestResourceStatusAreOrdered(t *testing.T) {
},
} {
t.Run(cc.name, func(t *testing.T) {
kubeclientset := kubefake.NewClientset([]runtime.Object{}...)
kubeclientset := kubefake.NewSimpleClientset([]runtime.Object{}...)
client := fake.NewClientBuilder().WithScheme(scheme).WithStatusSubresource(&cc.appSet).WithObjects(&cc.appSet).Build()
metrics := appsetmetrics.NewFakeAppsetMetrics()
@@ -7410,229 +7277,12 @@ func TestIsRollingSyncDeletionReversed(t *testing.T) {
}
}
func TestReconcileAddsFinalizer_WhenDeletionOrderReverse(t *testing.T) {
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
kubeclientset := kubefake.NewClientset([]runtime.Object{}...)
for _, cc := range []struct {
name string
appSet v1alpha1.ApplicationSet
progressiveSyncEnabled bool
expectedFinalizers []string
}{
{
name: "adds finalizer when DeletionOrder is Reverse",
appSet: v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "test-appset",
Namespace: "argocd",
// No finalizers initially
},
Spec: v1alpha1.ApplicationSetSpec{
Strategy: &v1alpha1.ApplicationSetStrategy{
Type: "RollingSync",
RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
Steps: []v1alpha1.ApplicationSetRolloutStep{
{
MatchExpressions: []v1alpha1.ApplicationMatchExpression{
{
Key: "env",
Operator: "In",
Values: []string{"dev"},
},
},
},
},
},
DeletionOrder: ReverseDeletionOrder,
},
Template: v1alpha1.ApplicationSetTemplate{},
},
},
progressiveSyncEnabled: true,
expectedFinalizers: []string{v1alpha1.ResourcesFinalizerName},
},
{
name: "does not add finalizer when already exists and DeletionOrder is Reverse",
appSet: v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "test-appset",
Namespace: "argocd",
Finalizers: []string{
v1alpha1.ResourcesFinalizerName,
},
},
Spec: v1alpha1.ApplicationSetSpec{
Strategy: &v1alpha1.ApplicationSetStrategy{
Type: "RollingSync",
RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{
Steps: []v1alpha1.ApplicationSetRolloutStep{
{
MatchExpressions: []v1alpha1.ApplicationMatchExpression{
{
Key: "env",
Operator: "In",
Values: []string{"dev"},
},
},
},
},
},
DeletionOrder: ReverseDeletionOrder,
},
Template: v1alpha1.ApplicationSetTemplate{},
},
},
progressiveSyncEnabled: true,
expectedFinalizers: []string{v1alpha1.ResourcesFinalizerName},
},
{
name: "does not add finalizer when DeletionOrder is AllAtOnce",
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: "env",
Operator: "In",
Values: []string{"dev"},
},
},
},
},
},
DeletionOrder: AllAtOnceDeletionOrder,
},
Template: v1alpha1.ApplicationSetTemplate{},
},
},
progressiveSyncEnabled: true,
expectedFinalizers: nil,
},
{
name: "does not add finalizer when DeletionOrder is not set",
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: "env",
Operator: "In",
Values: []string{"dev"},
},
},
},
},
},
},
Template: v1alpha1.ApplicationSetTemplate{},
},
},
progressiveSyncEnabled: true,
expectedFinalizers: nil,
},
{
name: "does not add finalizer when progressive sync not enabled",
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: "env",
Operator: "In",
Values: []string{"dev"},
},
},
},
},
},
DeletionOrder: ReverseDeletionOrder,
},
Template: v1alpha1.ApplicationSetTemplate{},
},
},
progressiveSyncEnabled: false,
expectedFinalizers: nil,
},
} {
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.progressiveSyncEnabled,
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: cc.appSet.Namespace,
Name: cc.appSet.Name,
},
}
// 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 finalizers
assert.Equal(t, cc.expectedFinalizers, updatedAppSet.Finalizers,
"finalizers should match expected value")
})
}
}
func TestReconcileProgressiveSyncDisabled(t *testing.T) {
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
kubeclientset := kubefake.NewClientset([]runtime.Object{}...)
kubeclientset := kubefake.NewSimpleClientset([]runtime.Object{}...)
for _, cc := range []struct {
name string
@@ -7705,14 +7355,3 @@ func TestReconcileProgressiveSyncDisabled(t *testing.T) {
})
}
}
func startAndSyncInformer(t *testing.T, informer cache.SharedIndexInformer) context.CancelFunc {
t.Helper()
ctx, cancel := context.WithCancel(t.Context())
go informer.Run(ctx.Done())
if !cache.WaitForCacheSync(ctx.Done(), informer.HasSynced) {
cancel()
t.Fatal("Timed out waiting for caches to sync")
}
return cancel
}

View File

@@ -14,7 +14,6 @@ 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"
)
@@ -23,9 +22,8 @@ import (
// requeue any related ApplicationSets.
type clusterSecretEventHandler struct {
// handler.EnqueueRequestForOwner
Log log.FieldLogger
Client client.Client
ApplicationSetNamespaces []string
Log log.FieldLogger
Client client.Client
}
func (h *clusterSecretEventHandler) Create(ctx context.Context, e event.CreateEvent, q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
@@ -70,10 +68,6 @@ 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 {

View File

@@ -137,7 +137,7 @@ func TestClusterEventHandler(t *testing.T) {
{
ObjectMeta: metav1.ObjectMeta{
Name: "my-app-set",
Namespace: "argocd",
Namespace: "another-namespace",
},
Spec: argov1alpha1.ApplicationSetSpec{
Generators: []argov1alpha1.ApplicationSetGenerator{
@@ -171,37 +171,9 @@ func TestClusterEventHandler(t *testing.T) {
},
},
expectedRequests: []reconcile.Request{
{NamespacedName: types.NamespacedName{Namespace: "argocd", Name: "my-app-set"}},
{NamespacedName: types.NamespacedName{Namespace: "another-namespace", 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{
@@ -580,9 +552,8 @@ func TestClusterEventHandler(t *testing.T) {
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithLists(&appSetList).Build()
handler := &clusterSecretEventHandler{
Client: fakeClient,
Log: log.WithField("type", "createSecretEventHandler"),
ApplicationSetNamespaces: []string{"argocd"},
Client: fakeClient,
Log: log.WithField("type", "createSecretEventHandler"),
}
mockAddRateLimitingInterface := mockAddRateLimitingInterface{}

View File

@@ -19,7 +19,6 @@ import (
appsetmetrics "github.com/argoproj/argo-cd/v3/applicationset/metrics"
"github.com/argoproj/argo-cd/v3/applicationset/services/mocks"
argov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/util/settings"
)
func TestRequeueAfter(t *testing.T) {
@@ -58,17 +57,12 @@ func TestRequeueAfter(t *testing.T) {
}
fakeDynClient := dynfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), gvrToListKind, duckType)
scmConfig := generators.NewSCMConfig("", []string{""}, true, true, nil, true)
clusterInformer, err := settings.NewClusterInformer(appClientset, "argocd")
require.NoError(t, err)
defer startAndSyncInformer(t, clusterInformer)()
terminalGenerators := map[string]generators.Generator{
"List": generators.NewListGenerator(),
"Clusters": generators.NewClusterGenerator(k8sClient, "argocd"),
"Clusters": generators.NewClusterGenerator(ctx, k8sClient, appClientset, "argocd"),
"Git": generators.NewGitGenerator(mockServer, "namespace"),
"SCMProvider": generators.NewSCMProviderGenerator(fake.NewClientBuilder().WithObjects(&corev1.Secret{}).Build(), scmConfig),
"ClusterDecisionResource": generators.NewDuckTypeGenerator(ctx, fakeDynClient, appClientset, "argocd", clusterInformer),
"ClusterDecisionResource": generators.NewDuckTypeGenerator(ctx, fakeDynClient, appClientset, "argocd"),
"PullRequest": generators.NewPullRequestGenerator(k8sClient, scmConfig),
}

View File

@@ -9,6 +9,7 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/argoproj/argo-cd/v3/applicationset/utils"
@@ -21,15 +22,19 @@ var _ Generator = (*ClusterGenerator)(nil)
// ClusterGenerator generates Applications for some or all clusters registered with ArgoCD.
type ClusterGenerator struct {
client.Client
ctx context.Context
clientset kubernetes.Interface
// namespace is the Argo CD namespace
namespace string
}
var render = &utils.Render{}
func NewClusterGenerator(c client.Client, namespace string) Generator {
func NewClusterGenerator(ctx context.Context, c client.Client, clientset kubernetes.Interface, namespace string) Generator {
g := &ClusterGenerator{
Client: c,
ctx: ctx,
clientset: clientset,
namespace: namespace,
}
return g
@@ -59,7 +64,16 @@ func (g *ClusterGenerator) GenerateParams(appSetGenerator *argoappsetv1alpha1.Ap
// - Since local clusters do not have secrets, they do not have labels to match against
ignoreLocalClusters := len(appSetGenerator.Clusters.Selector.MatchExpressions) > 0 || len(appSetGenerator.Clusters.Selector.MatchLabels) > 0
// Get cluster secrets using the cached controller-runtime client
// ListCluster will include the local cluster in the list of clusters
clustersFromArgoCD, err := utils.ListClusters(g.ctx, g.clientset, g.namespace)
if err != nil {
return nil, fmt.Errorf("error listing clusters: %w", err)
}
if clustersFromArgoCD == nil {
return nil, nil
}
clusterSecrets, err := g.getSecretsByClusterName(logCtx, appSetGenerator)
if err != nil {
return nil, fmt.Errorf("error getting cluster secrets: %w", err)
@@ -68,14 +82,32 @@ func (g *ClusterGenerator) GenerateParams(appSetGenerator *argoappsetv1alpha1.Ap
paramHolder := &paramHolder{isFlatMode: appSetGenerator.Clusters.FlatList}
logCtx.Debugf("Using flat mode = %t for cluster generator", paramHolder.isFlatMode)
// Convert map values to slice to check for an in-cluster secret
secretsList := make([]corev1.Secret, 0, len(clusterSecrets))
for _, secret := range clusterSecrets {
secretsList = append(secretsList, secret)
secretsFound := []corev1.Secret{}
for _, cluster := range clustersFromArgoCD {
// If there is a secret for this cluster, then it's a non-local cluster, so it will be
// handled by the next step.
if secretForCluster, exists := clusterSecrets[cluster.Name]; exists {
secretsFound = append(secretsFound, secretForCluster)
} else if !ignoreLocalClusters {
// If there is no secret for the cluster, it's the local cluster, so handle it here.
params := map[string]any{}
params["name"] = cluster.Name
params["nameNormalized"] = cluster.Name
params["server"] = cluster.Server
params["project"] = ""
err = appendTemplatedValues(appSetGenerator.Clusters.Values, params, appSet.Spec.GoTemplate, appSet.Spec.GoTemplateOptions)
if err != nil {
return nil, fmt.Errorf("error appending templated values for local cluster: %w", err)
}
paramHolder.append(params)
logCtx.WithField("cluster", "local cluster").Info("matched local cluster")
}
}
// For each matching cluster secret (non-local clusters only)
for _, cluster := range clusterSecrets {
for _, cluster := range secretsFound {
params := g.getClusterParameters(cluster, appSet)
err = appendTemplatedValues(appSetGenerator.Clusters.Values, params, appSet.Spec.GoTemplate, appSet.Spec.GoTemplateOptions)
@@ -87,23 +119,6 @@ func (g *ClusterGenerator) GenerateParams(appSetGenerator *argoappsetv1alpha1.Ap
logCtx.WithField("cluster", cluster.Name).Debug("matched cluster secret")
}
// Add the in-cluster last if it doesn't have a secret, and we're not ignoring in-cluster
if !ignoreLocalClusters && !utils.SecretsContainInClusterCredentials(secretsList) {
params := map[string]any{}
params["name"] = argoappsetv1alpha1.KubernetesInClusterName
params["nameNormalized"] = argoappsetv1alpha1.KubernetesInClusterName
params["server"] = argoappsetv1alpha1.KubernetesInternalAPIServerAddr
params["project"] = ""
err = appendTemplatedValues(appSetGenerator.Clusters.Values, params, appSet.Spec.GoTemplate, appSet.Spec.GoTemplateOptions)
if err != nil {
return nil, fmt.Errorf("error appending templated values for local cluster: %w", err)
}
paramHolder.append(params)
logCtx.WithField("cluster", "local cluster").Info("matched local cluster")
}
return paramHolder.consolidate(), nil
}
@@ -171,7 +186,7 @@ func (g *ClusterGenerator) getSecretsByClusterName(log *log.Entry, appSetGenerat
return nil, fmt.Errorf("error converting label selector: %w", err)
}
if err := g.List(context.Background(), clusterSecretList, client.InNamespace(g.namespace), client.MatchingLabelsSelector{Selector: secretSelector}); err != nil {
if err := g.List(context.Background(), clusterSecretList, client.MatchingLabelsSelector{Selector: secretSelector}); err != nil {
return nil, err
}
log.Debugf("clusters matching labels: %d", len(clusterSecretList.Items))

View File

@@ -7,9 +7,12 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
kubefake "k8s.io/client-go/kubernetes/fake"
"github.com/argoproj/argo-cd/v3/applicationset/utils"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
@@ -296,15 +299,23 @@ func TestGenerateParams(t *testing.T) {
},
}
// convert []client.Object to []runtime.Object, for use by kubefake package
runtimeClusters := []runtime.Object{}
for _, clientCluster := range clusters {
runtimeClusters = append(runtimeClusters, clientCluster)
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
appClientset := kubefake.NewSimpleClientset(runtimeClusters...)
fakeClient := fake.NewClientBuilder().WithObjects(clusters...).Build()
cl := &possiblyErroringFakeCtrlRuntimeClient{
fakeClient,
testCase.clientError,
}
clusterGenerator := NewClusterGenerator(cl, "namespace")
clusterGenerator := NewClusterGenerator(t.Context(), cl, appClientset, "namespace")
applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
@@ -325,25 +336,12 @@ func TestGenerateParams(t *testing.T) {
require.EqualError(t, err, testCase.expectedError.Error())
} else {
require.NoError(t, err)
assertEqualParamsFlat(t, testCase.expected, got, testCase.isFlatMode)
assert.ElementsMatch(t, testCase.expected, got)
}
})
}
}
func assertEqualParamsFlat(t *testing.T, expected, got []map[string]any, isFlatMode bool) {
t.Helper()
if isFlatMode && len(expected) == 1 && len(got) == 1 {
expectedClusters, ok1 := expected[0]["clusters"].([]map[string]any)
gotClusters, ok2 := got[0]["clusters"].([]map[string]any)
if ok1 && ok2 {
assert.ElementsMatch(t, expectedClusters, gotClusters)
return
}
}
assert.ElementsMatch(t, expected, got)
}
func TestGenerateParamsGoTemplate(t *testing.T) {
clusters := []client.Object{
&corev1.Secret{
@@ -839,15 +837,23 @@ func TestGenerateParamsGoTemplate(t *testing.T) {
},
}
// convert []client.Object to []runtime.Object, for use by kubefake package
runtimeClusters := []runtime.Object{}
for _, clientCluster := range clusters {
runtimeClusters = append(runtimeClusters, clientCluster)
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
appClientset := kubefake.NewSimpleClientset(runtimeClusters...)
fakeClient := fake.NewClientBuilder().WithObjects(clusters...).Build()
cl := &possiblyErroringFakeCtrlRuntimeClient{
fakeClient,
testCase.clientError,
}
clusterGenerator := NewClusterGenerator(cl, "namespace")
clusterGenerator := NewClusterGenerator(t.Context(), cl, appClientset, "namespace")
applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
@@ -870,7 +876,7 @@ func TestGenerateParamsGoTemplate(t *testing.T) {
require.EqualError(t, err, testCase.expectedError.Error())
} else {
require.NoError(t, err)
assertEqualParamsFlat(t, testCase.expected, got, testCase.isFlatMode)
assert.ElementsMatch(t, testCase.expected, got)
}
})
}

View File

@@ -19,27 +19,24 @@ import (
"github.com/argoproj/argo-cd/v3/applicationset/utils"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/util/settings"
)
var _ Generator = (*DuckTypeGenerator)(nil)
// DuckTypeGenerator generates Applications for some or all clusters registered with ArgoCD.
type DuckTypeGenerator struct {
ctx context.Context
dynClient dynamic.Interface
clientset kubernetes.Interface
namespace string // namespace is the Argo CD namespace
clusterInformer *settings.ClusterInformer
ctx context.Context
dynClient dynamic.Interface
clientset kubernetes.Interface
namespace string // namespace is the Argo CD namespace
}
func NewDuckTypeGenerator(ctx context.Context, dynClient dynamic.Interface, clientset kubernetes.Interface, namespace string, clusterInformer *settings.ClusterInformer) Generator {
func NewDuckTypeGenerator(ctx context.Context, dynClient dynamic.Interface, clientset kubernetes.Interface, namespace string) Generator {
g := &DuckTypeGenerator{
ctx: ctx,
dynClient: dynClient,
clientset: clientset,
namespace: namespace,
clusterInformer: clusterInformer,
ctx: ctx,
dynClient: dynClient,
clientset: clientset,
namespace: namespace,
}
return g
}
@@ -68,7 +65,8 @@ func (g *DuckTypeGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.A
return nil, ErrEmptyAppSetGenerator
}
clustersFromArgoCD, err := utils.ListClusters(g.clusterInformer)
// ListCluster from Argo CD's util/db package will include the local cluster in the list of clusters
clustersFromArgoCD, err := utils.ListClusters(g.ctx, g.clientset, g.namespace)
if err != nil {
return nil, fmt.Errorf("error listing clusters: %w", err)
}

View File

@@ -11,13 +11,11 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic/fake"
dynfake "k8s.io/client-go/dynamic/fake"
kubefake "k8s.io/client-go/kubernetes/fake"
"sigs.k8s.io/controller-runtime/pkg/client"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/test"
"github.com/argoproj/argo-cd/v3/util/settings"
)
const (
@@ -292,14 +290,9 @@ func TestGenerateParamsForDuckType(t *testing.T) {
Resource: "ducks",
}: "DuckList"}
fakeDynClient := fake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), gvrToListKind, testCase.resource)
fakeDynClient := dynfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), gvrToListKind, testCase.resource)
clusterInformer, err := settings.NewClusterInformer(appClientset, "namespace")
require.NoError(t, err)
defer test.StartInformer(clusterInformer)()
duckTypeGenerator := NewDuckTypeGenerator(t.Context(), fakeDynClient, appClientset, "namespace", clusterInformer)
duckTypeGenerator := NewDuckTypeGenerator(t.Context(), fakeDynClient, appClientset, "namespace")
applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
@@ -593,14 +586,9 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
Resource: "ducks",
}: "DuckList"}
fakeDynClient := fake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), gvrToListKind, testCase.resource)
fakeDynClient := dynfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), gvrToListKind, testCase.resource)
clusterInformer, err := settings.NewClusterInformer(appClientset, "namespace")
require.NoError(t, err)
defer test.StartInformer(clusterInformer)()
duckTypeGenerator := NewDuckTypeGenerator(t.Context(), fakeDynClient, appClientset, "namespace", clusterInformer)
duckTypeGenerator := NewDuckTypeGenerator(t.Context(), fakeDynClient, appClientset, "namespace")
applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{

View File

@@ -1,6 +1,7 @@
package generators
import (
"context"
"testing"
log "github.com/sirupsen/logrus"
@@ -15,6 +16,8 @@ import (
"github.com/stretchr/testify/mock"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
kubefake "k8s.io/client-go/kubernetes/fake"
crtclient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)
@@ -220,7 +223,7 @@ func TestTransForm(t *testing.T) {
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
testGenerators := map[string]Generator{
"Clusters": getMockClusterGenerator(),
"Clusters": getMockClusterGenerator(t.Context()),
}
applicationSetInfo := argov1alpha1.ApplicationSet{
@@ -257,7 +260,7 @@ func emptyTemplate() argov1alpha1.ApplicationSetTemplate {
}
}
func getMockClusterGenerator() Generator {
func getMockClusterGenerator(ctx context.Context) Generator {
clusters := []crtclient.Object{
&corev1.Secret{
TypeMeta: metav1.TypeMeta{
@@ -332,8 +335,14 @@ func getMockClusterGenerator() Generator {
Type: corev1.SecretType("Opaque"),
},
}
runtimeClusters := []runtime.Object{}
for _, clientCluster := range clusters {
runtimeClusters = append(runtimeClusters, clientCluster)
}
appClientset := kubefake.NewSimpleClientset(runtimeClusters...)
fakeClient := fake.NewClientBuilder().WithObjects(clusters...).Build()
return NewClusterGenerator(fakeClient, "namespace")
return NewClusterGenerator(ctx, fakeClient, appClientset, "namespace")
}
func getMockGitGenerator() Generator {
@@ -345,7 +354,7 @@ func getMockGitGenerator() Generator {
func TestGetRelevantGenerators(t *testing.T) {
testGenerators := map[string]Generator{
"Clusters": getMockClusterGenerator(),
"Clusters": getMockClusterGenerator(t.Context()),
"Git": getMockGitGenerator(),
}
@@ -542,7 +551,7 @@ func TestInterpolateGeneratorError(t *testing.T) {
},
useGoTemplate: true,
goTemplateOptions: []string{},
}, want: argov1alpha1.ApplicationSetGenerator{}, expectedErrStr: "failed to replace parameters in generator: failed to execute go template {{ index .rmap (default .override .test) }}: template: base:1:3: executing \"base\" at <index .rmap (default .override .test)>: error calling index: index of untyped nil"},
}, want: argov1alpha1.ApplicationSetGenerator{}, expectedErrStr: "failed to replace parameters in generator: failed to execute go template {{ index .rmap (default .override .test) }}: template: :1:3: executing \"\" at <index .rmap (default .override .test)>: error calling index: index of untyped nil"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@@ -316,7 +316,7 @@ func (g *GitGenerator) filterApps(directories []argoprojiov1alpha1.GitDirectoryG
appExclude = true
}
}
// Whenever there is a path with exclude: true it won't be included, even if it is included in a different path pattern
// Whenever there is a path with exclude: true it wont be included, even if it is included in a different path pattern
if appInclude && !appExclude {
res = append(res, appPath)
}

View File

@@ -8,6 +8,7 @@ import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
kubefake "k8s.io/client-go/kubernetes/fake"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
@@ -623,6 +624,11 @@ func TestInterpolatedMatrixGenerate(t *testing.T) {
Type: corev1.SecretType("Opaque"),
},
}
// convert []client.Object to []runtime.Object, for use by kubefake package
runtimeClusters := []runtime.Object{}
for _, clientCluster := range clusters {
runtimeClusters = append(runtimeClusters, clientCluster)
}
for _, testCase := range testCases {
testCaseCopy := testCase // Since tests may run in parallel
@@ -631,12 +637,13 @@ func TestInterpolatedMatrixGenerate(t *testing.T) {
genMock := &generatorsMock.Generator{}
appSet := &v1alpha1.ApplicationSet{}
appClientset := kubefake.NewSimpleClientset(runtimeClusters...)
fakeClient := fake.NewClientBuilder().WithObjects(clusters...).Build()
cl := &possiblyErroringFakeCtrlRuntimeClient{
fakeClient,
testCase.clientError,
}
clusterGenerator := NewClusterGenerator(cl, "namespace")
clusterGenerator := NewClusterGenerator(t.Context(), cl, appClientset, "namespace")
for _, g := range testCaseCopy.baseGenerators {
gitGeneratorSpec := v1alpha1.ApplicationSetGenerator{
@@ -796,6 +803,11 @@ func TestInterpolatedMatrixGenerateGoTemplate(t *testing.T) {
Type: corev1.SecretType("Opaque"),
},
}
// convert []client.Object to []runtime.Object, for use by kubefake package
runtimeClusters := []runtime.Object{}
for _, clientCluster := range clusters {
runtimeClusters = append(runtimeClusters, clientCluster)
}
for _, testCase := range testCases {
testCaseCopy := testCase // Since tests may run in parallel
@@ -808,12 +820,13 @@ func TestInterpolatedMatrixGenerateGoTemplate(t *testing.T) {
},
}
appClientset := kubefake.NewSimpleClientset(runtimeClusters...)
fakeClient := fake.NewClientBuilder().WithObjects(clusters...).Build()
cl := &possiblyErroringFakeCtrlRuntimeClient{
fakeClient,
testCase.clientError,
}
clusterGenerator := NewClusterGenerator(cl, "namespace")
clusterGenerator := NewClusterGenerator(t.Context(), cl, appClientset, "namespace")
for _, g := range testCaseCopy.baseGenerators {
gitGeneratorSpec := v1alpha1.ApplicationSetGenerator{

View File

@@ -107,7 +107,7 @@ func (g *PullRequestGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha
}
paramMap := map[string]any{
"number": strconv.FormatInt(pull.Number, 10),
"number": strconv.Itoa(pull.Number),
"title": pull.Title,
"branch": pull.Branch,
"branch_slug": slug.Make(pull.Branch),
@@ -243,9 +243,9 @@ func (g *PullRequestGenerator) github(ctx context.Context, cfg *argoprojiov1alph
}
if g.enableGitHubAPIMetrics {
return pullrequest.NewGithubAppService(ctx, *auth, cfg.API, cfg.Owner, cfg.Repo, cfg.Labels, httpClient)
return pullrequest.NewGithubAppService(*auth, cfg.API, cfg.Owner, cfg.Repo, cfg.Labels, httpClient)
}
return pullrequest.NewGithubAppService(ctx, *auth, cfg.API, cfg.Owner, cfg.Repo, cfg.Labels)
return pullrequest.NewGithubAppService(*auth, cfg.API, cfg.Owner, cfg.Repo, cfg.Labels)
}
// always default to token, even if not set (public access)

View File

@@ -296,9 +296,9 @@ func (g *SCMProviderGenerator) githubProvider(ctx context.Context, github *argop
}
if g.enableGitHubAPIMetrics {
return scm_provider.NewGithubAppProviderFor(ctx, *auth, github.Organization, github.API, github.AllBranches, httpClient)
return scm_provider.NewGithubAppProviderFor(*auth, github.Organization, github.API, github.AllBranches, httpClient)
}
return scm_provider.NewGithubAppProviderFor(ctx, *auth, github.Organization, github.API, github.AllBranches)
return scm_provider.NewGithubAppProviderFor(*auth, github.Organization, github.API, github.AllBranches)
}
token, err := utils.GetSecretRef(ctx, g.client, github.TokenRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)

View File

@@ -8,16 +8,15 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/argoproj/argo-cd/v3/applicationset/services"
"github.com/argoproj/argo-cd/v3/util/settings"
)
func GetGenerators(ctx context.Context, c client.Client, k8sClient kubernetes.Interface, controllerNamespace string, argoCDService services.Repos, dynamicClient dynamic.Interface, scmConfig SCMConfig, clusterInformer *settings.ClusterInformer) 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(c, controllerNamespace),
"Clusters": NewClusterGenerator(ctx, c, k8sClient, controllerNamespace),
"Git": NewGitGenerator(argoCDService, controllerNamespace),
"SCMProvider": NewSCMProviderGenerator(c, scmConfig),
"ClusterDecisionResource": NewDuckTypeGenerator(ctx, dynamicClient, k8sClient, controllerNamespace, clusterInformer),
"ClusterDecisionResource": NewDuckTypeGenerator(ctx, dynamicClient, k8sClient, controllerNamespace),
"PullRequest": NewPullRequestGenerator(c, scmConfig),
"Plugin": NewPluginGenerator(c, controllerNamespace),
}

View File

@@ -1,8 +1,6 @@
package github_app
import (
"context"
"errors"
"fmt"
"net/http"
@@ -10,65 +8,40 @@ import (
"github.com/google/go-github/v69/github"
"github.com/argoproj/argo-cd/v3/applicationset/services/github_app_auth"
"github.com/argoproj/argo-cd/v3/util/git"
appsetutils "github.com/argoproj/argo-cd/v3/applicationset/utils"
)
// getInstallationClient creates a new GitHub client with the specified installation ID.
// It also returns a ghinstallation.Transport, which can be used for git requests.
func getInstallationClient(g github_app_auth.Authentication, url string, httpClient ...*http.Client) (*github.Client, error) {
if g.InstallationId <= 0 {
return nil, errors.New("installation ID is required for github")
func getOptionalHTTPClientAndTransport(optionalHTTPClient ...*http.Client) (*http.Client, http.RoundTripper) {
httpClient := appsetutils.GetOptionalHTTPClient(optionalHTTPClient...)
if len(optionalHTTPClient) > 0 && optionalHTTPClient[0] != nil && optionalHTTPClient[0].Transport != nil {
// will either use the provided custom httpClient and it's transport
return httpClient, optionalHTTPClient[0].Transport
}
// Use provided HTTP client's transport or default
var transport http.RoundTripper
if len(httpClient) > 0 && httpClient[0] != nil && httpClient[0].Transport != nil {
transport = httpClient[0].Transport
} else {
transport = http.DefaultTransport
}
itr, err := ghinstallation.New(transport, g.Id, g.InstallationId, []byte(g.PrivateKey))
if err != nil {
return nil, fmt.Errorf("failed to create GitHub installation transport: %w", err)
}
if url == "" {
url = g.EnterpriseBaseURL
}
var client *github.Client
if url == "" {
client = github.NewClient(&http.Client{Transport: itr})
return client, nil
}
itr.BaseURL = url
client, err = github.NewClient(&http.Client{Transport: itr}).WithEnterpriseURLs(url, url)
if err != nil {
return nil, fmt.Errorf("failed to create GitHub enterprise client: %w", err)
}
return client, nil
// or the default httpClient and transport
return httpClient, http.DefaultTransport
}
// Client builds a github client for the given app authentication.
func Client(ctx context.Context, g github_app_auth.Authentication, url, org string, optionalHTTPClient ...*http.Client) (*github.Client, error) {
func Client(g github_app_auth.Authentication, url string, optionalHTTPClient ...*http.Client) (*github.Client, error) {
httpClient, transport := getOptionalHTTPClientAndTransport(optionalHTTPClient...)
rt, err := ghinstallation.New(transport, g.Id, g.InstallationId, []byte(g.PrivateKey))
if err != nil {
return nil, fmt.Errorf("failed to create github app install: %w", err)
}
if url == "" {
url = g.EnterpriseBaseURL
}
// If an installation ID is already provided, use it directly.
if g.InstallationId != 0 {
return getInstallationClient(g, url, optionalHTTPClient...)
var client *github.Client
httpClient.Transport = rt
if url == "" {
client = github.NewClient(httpClient)
} else {
rt.BaseURL = url
client, err = github.NewClient(httpClient).WithEnterpriseURLs(url, url)
if err != nil {
return nil, fmt.Errorf("failed to create github enterprise client: %w", err)
}
}
// Auto-discover installation ID using shared utility
// Pass optional HTTP client for metrics tracking
installationId, err := git.DiscoverGitHubAppInstallationID(ctx, g.Id, g.PrivateKey, url, org, optionalHTTPClient...)
if err != nil {
return nil, err
}
g.InstallationId = installationId
return getInstallationClient(g, url, optionalHTTPClient...)
return client, nil
}

View File

@@ -107,7 +107,7 @@ func (a *AzureDevOpsService) List(ctx context.Context) ([]*PullRequest, error) {
if *pr.Repository.Name == a.repo {
pullRequests = append(pullRequests, &PullRequest{
Number: int64(*pr.PullRequestId),
Number: *pr.PullRequestId,
Title: *pr.Title,
Branch: strings.Replace(*pr.SourceRefName, "refs/heads/", "", 1),
TargetBranch: strings.Replace(*pr.TargetRefName, "refs/heads/", "", 1),

View File

@@ -87,7 +87,7 @@ func TestListPullRequest(t *testing.T) {
assert.Equal(t, "main", list[0].TargetBranch)
assert.Equal(t, prHeadSha, list[0].HeadSHA)
assert.Equal(t, "feat(123)", list[0].Title)
assert.Equal(t, int64(prID), list[0].Number)
assert.Equal(t, prID, list[0].Number)
assert.Equal(t, uniqueName, list[0].Author)
}

View File

@@ -81,10 +81,7 @@ func NewBitbucketCloudServiceBasicAuth(baseURL, username, password, owner, repos
return nil, fmt.Errorf("error parsing base url of %s for %s/%s: %w", baseURL, owner, repositorySlug, err)
}
bitbucketClient, err := bitbucket.NewBasicAuth(username, password)
if err != nil {
return nil, fmt.Errorf("error creating BitBucket Cloud client with basic auth: %w", err)
}
bitbucketClient := bitbucket.NewBasicAuth(username, password)
bitbucketClient.SetApiBaseURL(*url)
return &BitbucketCloudService{
@@ -100,13 +97,14 @@ func NewBitbucketCloudServiceBearerToken(baseURL, bearerToken, owner, repository
return nil, fmt.Errorf("error parsing base url of %s for %s/%s: %w", baseURL, owner, repositorySlug, err)
}
bitbucketClient, err := bitbucket.NewOAuthbearerToken(bearerToken)
if err != nil {
return nil, fmt.Errorf("error creating BitBucket Cloud client with oauth bearer token: %w", err)
}
bitbucketClient := bitbucket.NewOAuthbearerToken(bearerToken)
bitbucketClient.SetApiBaseURL(*url)
return &BitbucketCloudService{client: bitbucketClient, owner: owner, repositorySlug: repositorySlug}, nil
return &BitbucketCloudService{
client: bitbucketClient,
owner: owner,
repositorySlug: repositorySlug,
}, nil
}
func NewBitbucketCloudServiceNoAuth(baseURL, owner, repositorySlug string) (PullRequestService, error) {
@@ -156,7 +154,7 @@ func (b *BitbucketCloudService) List(_ context.Context) ([]*PullRequest, error)
for _, pull := range pulls {
pullRequests = append(pullRequests, &PullRequest{
Number: int64(pull.ID),
Number: pull.ID,
Title: pull.Title,
Branch: pull.Source.Branch.Name,
TargetBranch: pull.Destination.Branch.Name,

View File

@@ -89,7 +89,7 @@ func TestListPullRequestBearerTokenCloud(t *testing.T) {
pullRequests, err := ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{})
require.NoError(t, err)
assert.Len(t, pullRequests, 1)
assert.Equal(t, int64(101), pullRequests[0].Number)
assert.Equal(t, 101, pullRequests[0].Number)
assert.Equal(t, "feat(foo-bar)", pullRequests[0].Title)
assert.Equal(t, "feature/foo-bar", pullRequests[0].Branch)
assert.Equal(t, "1a8dd249c04a", pullRequests[0].HeadSHA)
@@ -107,7 +107,7 @@ func TestListPullRequestNoAuthCloud(t *testing.T) {
pullRequests, err := ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{})
require.NoError(t, err)
assert.Len(t, pullRequests, 1)
assert.Equal(t, int64(101), pullRequests[0].Number)
assert.Equal(t, 101, pullRequests[0].Number)
assert.Equal(t, "feat(foo-bar)", pullRequests[0].Title)
assert.Equal(t, "feature/foo-bar", pullRequests[0].Branch)
assert.Equal(t, "1a8dd249c04a", pullRequests[0].HeadSHA)
@@ -125,7 +125,7 @@ func TestListPullRequestBasicAuthCloud(t *testing.T) {
pullRequests, err := ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{})
require.NoError(t, err)
assert.Len(t, pullRequests, 1)
assert.Equal(t, int64(101), pullRequests[0].Number)
assert.Equal(t, 101, pullRequests[0].Number)
assert.Equal(t, "feat(foo-bar)", pullRequests[0].Title)
assert.Equal(t, "feature/foo-bar", pullRequests[0].Branch)
assert.Equal(t, "1a8dd249c04a", pullRequests[0].HeadSHA)

View File

@@ -82,7 +82,7 @@ func (b *BitbucketService) List(_ context.Context) ([]*PullRequest, error) {
for _, pull := range pulls {
pullRequests = append(pullRequests, &PullRequest{
Number: int64(pull.ID),
Number: pull.ID,
Title: pull.Title,
Branch: pull.FromRef.DisplayID, // ID: refs/heads/main DisplayID: main
TargetBranch: pull.ToRef.DisplayID,

View File

@@ -68,7 +68,7 @@ func TestListPullRequestNoAuth(t *testing.T) {
pullRequests, err := ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{})
require.NoError(t, err)
assert.Len(t, pullRequests, 1)
assert.Equal(t, int64(101), pullRequests[0].Number)
assert.Equal(t, 101, pullRequests[0].Number)
assert.Equal(t, "feat(ABC) : 123", pullRequests[0].Title)
assert.Equal(t, "feature-ABC-123", pullRequests[0].Branch)
assert.Equal(t, "master", pullRequests[0].TargetBranch)
@@ -211,7 +211,7 @@ func TestListPullRequestBasicAuth(t *testing.T) {
pullRequests, err := ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{})
require.NoError(t, err)
assert.Len(t, pullRequests, 1)
assert.Equal(t, int64(101), pullRequests[0].Number)
assert.Equal(t, 101, pullRequests[0].Number)
assert.Equal(t, "feature-ABC-123", pullRequests[0].Branch)
assert.Equal(t, "cb3cf2e4d1517c83e720d2585b9402dbef71f992", pullRequests[0].HeadSHA)
}
@@ -228,7 +228,7 @@ func TestListPullRequestBearerAuth(t *testing.T) {
pullRequests, err := ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{})
require.NoError(t, err)
assert.Len(t, pullRequests, 1)
assert.Equal(t, int64(101), pullRequests[0].Number)
assert.Equal(t, 101, pullRequests[0].Number)
assert.Equal(t, "feat(ABC) : 123", pullRequests[0].Title)
assert.Equal(t, "feature-ABC-123", pullRequests[0].Branch)
assert.Equal(t, "cb3cf2e4d1517c83e720d2585b9402dbef71f992", pullRequests[0].HeadSHA)

View File

@@ -68,7 +68,7 @@ func (g *GiteaService) List(ctx context.Context) ([]*PullRequest, error) {
continue
}
list = append(list, &PullRequest{
Number: int64(pr.Index),
Number: int(pr.Index),
Title: pr.Title,
Branch: pr.Head.Ref,
TargetBranch: pr.Base.Ref,

View File

@@ -303,7 +303,7 @@ func TestGiteaList(t *testing.T) {
prs, err := host.List(t.Context())
require.NoError(t, err)
assert.Len(t, prs, 1)
assert.Equal(t, int64(1), prs[0].Number)
assert.Equal(t, 1, prs[0].Number)
assert.Equal(t, "add an empty file", prs[0].Title)
assert.Equal(t, "test", prs[0].Branch)
assert.Equal(t, "main", prs[0].TargetBranch)

View File

@@ -76,7 +76,7 @@ func (g *GithubService) List(ctx context.Context) ([]*PullRequest, error) {
continue
}
pullRequests = append(pullRequests, &PullRequest{
Number: int64(*pull.Number),
Number: *pull.Number,
Title: *pull.Title,
Branch: *pull.Head.Ref,
TargetBranch: *pull.Base.Ref,

View File

@@ -1,7 +1,6 @@
package pull_request
import (
"context"
"net/http"
"github.com/argoproj/argo-cd/v3/applicationset/services/github_app_auth"
@@ -9,9 +8,9 @@ import (
appsetutils "github.com/argoproj/argo-cd/v3/applicationset/utils"
)
func NewGithubAppService(ctx context.Context, g github_app_auth.Authentication, url, owner, repo string, labels []string, optionalHTTPClient ...*http.Client) (PullRequestService, error) {
func NewGithubAppService(g github_app_auth.Authentication, url, owner, repo string, labels []string, optionalHTTPClient ...*http.Client) (PullRequestService, error) {
httpClient := appsetutils.GetOptionalHTTPClient(optionalHTTPClient...)
client, err := github_app.Client(ctx, g, url, owner, httpClient)
client, err := github_app.Client(g, url, httpClient)
if err != nil {
return nil, err
}

View File

@@ -61,15 +61,11 @@ func (g *GitLabService) List(ctx context.Context) ([]*PullRequest, error) {
var labelsList gitlab.LabelOptions = g.labels
labels = &labelsList
}
snippetsListOptions := gitlab.ExploreSnippetsOptions{
opts := &gitlab.ListProjectMergeRequestsOptions{
ListOptions: gitlab.ListOptions{
PerPage: 100,
},
}
opts := &gitlab.ListProjectMergeRequestsOptions{
ListOptions: snippetsListOptions.ListOptions,
Labels: labels,
Labels: labels,
}
if g.pullRequestState != "" {

View File

@@ -78,7 +78,7 @@ func TestList(t *testing.T) {
prs, err := svc.List(t.Context())
require.NoError(t, err)
assert.Len(t, prs, 1)
assert.Equal(t, int64(15442), prs[0].Number)
assert.Equal(t, 15442, prs[0].Number)
assert.Equal(t, "Draft: Use structured logging for DB load balancer", prs[0].Title)
assert.Equal(t, "use-structured-logging-for-db-load-balancer", prs[0].Branch)
assert.Equal(t, "master", prs[0].TargetBranch)

View File

@@ -7,8 +7,7 @@ import (
type PullRequest struct {
// Number is a number that will be the ID of the pull request.
// Gitlab uses int64 for the pull request number.
Number int64
Number int
// Title of the pull request.
Title string
// Branch is the name of the branch from which the pull request originated.

View File

@@ -53,12 +53,8 @@ func (c *ExtendedClient) GetContents(repo *Repository, path string) (bool, error
var _ SCMProviderService = &BitBucketCloudProvider{}
func NewBitBucketCloudProvider(owner string, user string, password string, allBranches bool) (*BitBucketCloudProvider, error) {
bitbucketClient, err := bitbucket.NewBasicAuth(user, password)
if err != nil {
return nil, fmt.Errorf("error creating BitBucket Cloud client with basic auth: %w", err)
}
client := &ExtendedClient{
bitbucketClient,
bitbucket.NewBasicAuth(user, password),
user,
password,
owner,

View File

@@ -1,7 +1,6 @@
package scm_provider
import (
"context"
"net/http"
"github.com/argoproj/argo-cd/v3/applicationset/services/github_app_auth"
@@ -9,9 +8,9 @@ import (
appsetutils "github.com/argoproj/argo-cd/v3/applicationset/utils"
)
func NewGithubAppProviderFor(ctx context.Context, g github_app_auth.Authentication, organization string, url string, allBranches bool, optionalHTTPClient ...*http.Client) (*GithubProvider, error) {
func NewGithubAppProviderFor(g github_app_auth.Authentication, organization string, url string, allBranches bool, optionalHTTPClient ...*http.Client) (*GithubProvider, error) {
httpClient := appsetutils.GetOptionalHTTPClient(optionalHTTPClient...)
client, err := github_app.Client(ctx, g, url, organization, httpClient)
client, err := github_app.Client(g, url, httpClient)
if err != nil {
return nil, err
}

View File

@@ -76,13 +76,8 @@ func (g *GitlabProvider) GetBranches(ctx context.Context, repo *Repository) ([]*
}
func (g *GitlabProvider) ListRepos(_ context.Context, cloneProtocol string) ([]*Repository, error) {
snippetsListOptions := gitlab.ExploreSnippetsOptions{
ListOptions: gitlab.ListOptions{
PerPage: 100,
},
}
opt := &gitlab.ListGroupProjectsOptions{
ListOptions: snippetsListOptions.ListOptions,
ListOptions: gitlab.ListOptions{PerPage: 100},
IncludeSubGroups: &g.includeSubgroups,
WithShared: &g.includeSharedProjects,
Topic: &g.topic,
@@ -178,13 +173,8 @@ func (g *GitlabProvider) listBranches(_ context.Context, repo *Repository) ([]gi
return branches, nil
}
// Otherwise, scrape the ListBranches API.
snippetsListOptions := gitlab.ExploreSnippetsOptions{
ListOptions: gitlab.ListOptions{
PerPage: 100,
},
}
opt := &gitlab.ListBranchesOptions{
ListOptions: snippetsListOptions.ListOptions,
ListOptions: gitlab.ListOptions{PerPage: 100},
}
for {
gitlabBranches, resp, err := g.client.Branches.ListBranches(repo.RepositoryId, opt)

View File

@@ -1,12 +1,15 @@
package utils
import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
"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/settings"
"github.com/argoproj/argo-cd/v3/util/db"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
// ClusterSpecifier contains only the name and server URL of a cluster. We use this struct to avoid partially-populating
@@ -16,44 +19,42 @@ type ClusterSpecifier struct {
Server string
}
// SecretsContainInClusterCredentials checks if any of the provided secrets represent the in-cluster configuration.
func SecretsContainInClusterCredentials(secrets []corev1.Secret) bool {
for _, secret := range secrets {
if string(secret.Data["server"]) == appv1.KubernetesInternalAPIServerAddr {
return true
}
}
return false
}
// ListClusters returns a list of cluster specifiers using the ClusterInformer.
func ListClusters(clusterInformer *settings.ClusterInformer) ([]ClusterSpecifier, error) {
clusters, err := clusterInformer.ListClusters()
func ListClusters(ctx context.Context, clientset kubernetes.Interface, namespace string) ([]ClusterSpecifier, error) {
clusterSecretsList, err := clientset.CoreV1().Secrets(namespace).List(ctx,
metav1.ListOptions{LabelSelector: common.LabelKeySecretType + "=" + common.LabelValueSecretTypeCluster})
if err != nil {
return nil, fmt.Errorf("error listing clusters: %w", err)
return nil, err
}
// len of clusters +1 for the in cluster secret
clusterList := make([]ClusterSpecifier, 0, len(clusters)+1)
hasInCluster := false
for _, cluster := range clusters {
clusterList = append(clusterList, ClusterSpecifier{
if clusterSecretsList == nil {
return nil, nil
}
clusterSecrets := clusterSecretsList.Items
clusterList := make([]ClusterSpecifier, len(clusterSecrets))
hasInClusterCredentials := false
for i, clusterSecret := range clusterSecrets {
cluster, err := db.SecretToCluster(&clusterSecret)
if err != nil || cluster == nil {
return nil, fmt.Errorf("unable to convert cluster secret to cluster object '%s': %w", clusterSecret.Name, err)
}
clusterList[i] = ClusterSpecifier{
Name: cluster.Name,
Server: cluster.Server,
})
}
if cluster.Server == appv1.KubernetesInternalAPIServerAddr {
hasInCluster = true
hasInClusterCredentials = true
}
}
if !hasInCluster {
if !hasInClusterCredentials {
// There was no secret for the in-cluster config, so we add it here. We don't fully-populate the Cluster struct,
// since only the name and server fields are used by the generator.
clusterList = append(clusterList, ClusterSpecifier{
Name: appv1.KubernetesInClusterName,
Name: "in-cluster",
Server: appv1.KubernetesInternalAPIServerAddr,
})
}
return clusterList, nil
}

View File

@@ -30,10 +30,6 @@ import (
var sprigFuncMap = sprig.GenericFuncMap() // a singleton for better performance
// baseTemplate is a pre-initialized template with all sprig functions loaded.
// Cloning this is much faster than calling Funcs() on a new template each time.
var baseTemplate *template.Template
func init() {
// Avoid allowing the user to learn things about the environment.
delete(sprigFuncMap, "env")
@@ -44,10 +40,6 @@ func init() {
sprigFuncMap["toYaml"] = toYAML
sprigFuncMap["fromYaml"] = fromYAML
sprigFuncMap["fromYamlArray"] = fromYAMLArray
// Initialize the base template with sprig functions once at startup.
// This must be done after modifying sprigFuncMap above.
baseTemplate = template.New("base").Funcs(sprigFuncMap)
}
type Renderer interface {
@@ -317,21 +309,16 @@ var isTemplatedRegex = regexp.MustCompile(".*{{.*}}.*")
// remaining in the substituted template.
func (r *Render) Replace(tmpl string, replaceMap map[string]any, useGoTemplate bool, goTemplateOptions []string) (string, error) {
if useGoTemplate {
// Clone the base template which has sprig funcs pre-loaded
cloned, err := baseTemplate.Clone()
if err != nil {
return "", fmt.Errorf("failed to clone base template: %w", err)
}
for _, option := range goTemplateOptions {
cloned = cloned.Option(option)
}
parsed, err := cloned.Parse(tmpl)
template, err := template.New("").Funcs(sprigFuncMap).Parse(tmpl)
if err != nil {
return "", fmt.Errorf("failed to parse template %s: %w", tmpl, err)
}
for _, option := range goTemplateOptions {
template = template.Option(option)
}
var replacedTmplBuffer bytes.Buffer
if err = parsed.Execute(&replacedTmplBuffer, replaceMap); err != nil {
if err = template.Execute(&replacedTmplBuffer, replaceMap); err != nil {
return "", fmt.Errorf("failed to execute go template %s: %w", tmpl, err)
}

View File

@@ -514,7 +514,7 @@ func TestRenderTemplateParamsGoTemplate(t *testing.T) {
params: map[string]any{
"data": `a data string`,
},
errorMessage: `failed to parse template {{functiondoesnotexist}}: template: base:1: function "functiondoesnotexist" not defined`,
errorMessage: `failed to parse template {{functiondoesnotexist}}: template: :1: function "functiondoesnotexist" not defined`,
},
{
name: "Test template error",
@@ -523,7 +523,7 @@ func TestRenderTemplateParamsGoTemplate(t *testing.T) {
params: map[string]any{
"data": `a data string`,
},
errorMessage: `failed to execute go template {{.data.test}}: template: base:1:7: executing "base" at <.data.test>: can't evaluate field test in type interface {}`,
errorMessage: `failed to execute go template {{.data.test}}: template: :1:7: executing "" at <.data.test>: can't evaluate field test in type interface {}`,
},
{
name: "lookup missing value with missingkey=default",
@@ -543,7 +543,7 @@ func TestRenderTemplateParamsGoTemplate(t *testing.T) {
"unused": "this is not used",
},
templateOptions: []string{"missingkey=error"},
errorMessage: `failed to execute go template --> {{.doesnotexist}} <--: template: base:1:6: executing "base" at <.doesnotexist>: map has no entry for key "doesnotexist"`,
errorMessage: `failed to execute go template --> {{.doesnotexist}} <--: template: :1:6: executing "" at <.doesnotexist>: map has no entry for key "doesnotexist"`,
},
{
name: "toYaml",
@@ -563,7 +563,7 @@ func TestRenderTemplateParamsGoTemplate(t *testing.T) {
name: "toYaml Error",
fieldVal: `{{ toYaml . | indent 2 }}`,
expectedVal: " foo:\n bar:\n bool: true\n number: 2\n str: Hello world",
errorMessage: "failed to execute go template {{ toYaml . | indent 2 }}: template: base:1:3: executing \"base\" at <toYaml .>: error calling toYaml: error marshaling into JSON: json: unsupported type: func(*string)",
errorMessage: "failed to execute go template {{ toYaml . | indent 2 }}: template: :1:3: executing \"\" at <toYaml .>: error calling toYaml: error marshaling into JSON: json: unsupported type: func(*string)",
params: map[string]any{
"foo": func(_ *string) {
},
@@ -581,7 +581,7 @@ func TestRenderTemplateParamsGoTemplate(t *testing.T) {
name: "fromYaml error",
fieldVal: `{{ get (fromYaml .value) "hello" }}`,
expectedVal: "world",
errorMessage: "failed to execute go template {{ get (fromYaml .value) \"hello\" }}: template: base:1:8: executing \"base\" at <fromYaml .value>: error calling fromYaml: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type map[string]interface {}",
errorMessage: "failed to execute go template {{ get (fromYaml .value) \"hello\" }}: template: :1:8: executing \"\" at <fromYaml .value>: error calling fromYaml: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type map[string]interface {}",
params: map[string]any{
"value": "non\n compliant\n yaml",
},
@@ -598,7 +598,7 @@ func TestRenderTemplateParamsGoTemplate(t *testing.T) {
name: "fromYamlArray error",
fieldVal: `{{ fromYamlArray .value | last }}`,
expectedVal: "bonjour tout le monde",
errorMessage: "failed to execute go template {{ fromYamlArray .value | last }}: template: base:1:3: executing \"base\" at <fromYamlArray .value>: error calling fromYamlArray: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type []interface {}",
errorMessage: "failed to execute go template {{ fromYamlArray .value | last }}: template: :1:3: executing \"\" at <fromYamlArray .value>: error calling fromYamlArray: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type []interface {}",
params: map[string]any{
"value": "non\n compliant\n yaml",
},

79
assets/swagger.json generated
View File

@@ -2265,44 +2265,6 @@
}
}
},
"/api/v1/applicationsets/{name}/events": {
"get": {
"tags": [
"ApplicationSetService"
],
"summary": "ListResourceEvents returns a list of event resources",
"operationId": "ApplicationSetService_ListResourceEvents",
"parameters": [
{
"type": "string",
"description": "the applicationsets's name",
"name": "name",
"in": "path",
"required": true
},
{
"type": "string",
"description": "The application set namespace. Default empty is argocd control plane namespace.",
"name": "appsetNamespace",
"in": "query"
}
],
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/v1EventList"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/runtimeError"
}
}
}
}
},
"/api/v1/applicationsets/{name}/resource-tree": {
"get": {
"tags": [
@@ -6867,14 +6829,14 @@
"type": "array",
"title": "ClusterResourceBlacklist contains list of blacklisted cluster level resources",
"items": {
"$ref": "#/definitions/v1alpha1ClusterResourceRestrictionItem"
"$ref": "#/definitions/v1GroupKind"
}
},
"clusterResourceWhitelist": {
"type": "array",
"title": "ClusterResourceWhitelist contains list of whitelisted cluster level resources",
"items": {
"$ref": "#/definitions/v1alpha1ClusterResourceRestrictionItem"
"$ref": "#/definitions/v1GroupKind"
}
},
"description": {
@@ -7088,7 +7050,7 @@
},
"v1alpha1ApplicationSet": {
"type": "object",
"title": "ApplicationSet is a set of Application resources.\n+genclient\n+genclient:noStatus\n+k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object\n+kubebuilder:resource:path=applicationsets,shortName=appset;appsets\n+kubebuilder:subresource:status",
"title": "ApplicationSet is a set of Application resources\n+genclient\n+genclient:noStatus\n+k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object\n+kubebuilder:resource:path=applicationsets,shortName=appset;appsets\n+kubebuilder:subresource:status",
"properties": {
"metadata": {
"$ref": "#/definitions/v1ObjectMeta"
@@ -7299,7 +7261,7 @@
"type": "object",
"properties": {
"applyNestedSelectors": {
"description": "ApplyNestedSelectors enables selectors defined within the generators of two level-nested matrix or merge generators.\n\nDeprecated: This field is ignored, and the behavior is always enabled. The field will be removed in a future\nversion of the ApplicationSet CRD.",
"description": "ApplyNestedSelectors enables selectors defined within the generators of two level-nested matrix or merge generators\nDeprecated: This field is ignored, and the behavior is always enabled. The field will be removed in a future\nversion of the ApplicationSet CRD.",
"type": "boolean"
},
"generators": {
@@ -7357,9 +7319,6 @@
"$ref": "#/definitions/v1alpha1ApplicationSetCondition"
}
},
"health": {
"$ref": "#/definitions/v1alpha1HealthStatus"
},
"resources": {
"description": "Resources is a list of Applications resources managed by this application set.",
"type": "array",
@@ -8205,22 +8164,6 @@
}
}
},
"v1alpha1ClusterResourceRestrictionItem": {
"type": "object",
"title": "ClusterResourceRestrictionItem is a cluster resource that is restricted by the project's whitelist or blacklist",
"properties": {
"group": {
"type": "string"
},
"kind": {
"type": "string"
},
"name": {
"description": "Name is the name of the restricted resource. Glob patterns using Go's filepath.Match syntax are supported.\nUnlike the group and kind fields, if no name is specified, all resources of the specified group/kind are matched.",
"type": "string"
}
}
},
"v1alpha1Command": {
"type": "object",
"title": "Command holds binary path and arguments list",
@@ -8346,22 +8289,10 @@
"description": "DrySource specifies a location for dry \"don't repeat yourself\" manifest source information.",
"type": "object",
"properties": {
"directory": {
"$ref": "#/definitions/v1alpha1ApplicationSourceDirectory"
},
"helm": {
"$ref": "#/definitions/v1alpha1ApplicationSourceHelm"
},
"kustomize": {
"$ref": "#/definitions/v1alpha1ApplicationSourceKustomize"
},
"path": {
"type": "string",
"title": "Path is a directory path within the Git repository where the manifests are located"
},
"plugin": {
"$ref": "#/definitions/v1alpha1ApplicationSourcePlugin"
},
"repoURL": {
"type": "string",
"title": "RepoURL is the URL to the git repository that contains the application manifests"
@@ -9506,7 +9437,7 @@
"title": "TLSClientCertKey specifies the TLS client cert key for authenticating at the repo server"
},
"type": {
"description": "Type specifies the type of the repoCreds. Can be either \"git\", \"helm\" or \"oci\". \"git\" is assumed if empty or absent.",
"description": "Type specifies the type of the repoCreds. Can be either \"git\" or \"helm. \"git\" is assumed if empty or absent.",
"type": "string"
},
"url": {

View File

@@ -202,6 +202,7 @@ func NewCommand() *cobra.Command {
time.Duration(appResyncJitter)*time.Second,
time.Duration(selfHealTimeoutSeconds)*time.Second,
selfHealBackoff,
time.Duration(selfHealBackoffCooldownSeconds)*time.Second,
time.Duration(syncTimeout)*time.Second,
time.Duration(repoErrorGracePeriod)*time.Second,
metricsPort,
@@ -274,7 +275,6 @@ func NewCommand() *cobra.Command {
command.Flags().IntVar(&selfHealBackoffFactor, "self-heal-backoff-factor", env.ParseNumFromEnv("ARGOCD_APPLICATION_CONTROLLER_SELF_HEAL_BACKOFF_FACTOR", 3, 0, math.MaxInt32), "Specifies factor of exponential timeout between application self heal attempts")
command.Flags().IntVar(&selfHealBackoffCapSeconds, "self-heal-backoff-cap-seconds", env.ParseNumFromEnv("ARGOCD_APPLICATION_CONTROLLER_SELF_HEAL_BACKOFF_CAP_SECONDS", 300, 0, math.MaxInt32), "Specifies max timeout of exponential backoff between application self heal attempts")
command.Flags().IntVar(&selfHealBackoffCooldownSeconds, "self-heal-backoff-cooldown-seconds", env.ParseNumFromEnv("ARGOCD_APPLICATION_CONTROLLER_SELF_HEAL_BACKOFF_COOLDOWN_SECONDS", 330, 0, math.MaxInt32), "Specifies period of time the app needs to stay synced before the self heal backoff can reset")
errors.CheckError(command.Flags().MarkDeprecated("self-heal-backoff-cooldown-seconds", "This flag is deprecated and has no effect."))
command.Flags().IntVar(&syncTimeout, "sync-timeout", env.ParseNumFromEnv("ARGOCD_APPLICATION_CONTROLLER_SYNC_TIMEOUT", 0, 0, math.MaxInt32), "Specifies the timeout after which a sync would be terminated. 0 means no timeout (default 0).")
command.Flags().Int64Var(&kubectlParallelismLimit, "kubectl-parallelism-limit", env.ParseInt64FromEnv("ARGOCD_APPLICATION_CONTROLLER_KUBECTL_PARALLELISM_LIMIT", 20, 0, math.MaxInt64), "Number of allowed concurrent kubectl fork/execs. Any value less than 1 means no limit.")
command.Flags().BoolVar(&repoServerPlaintext, "repo-server-plaintext", env.ParseBoolFromEnv("ARGOCD_APPLICATION_CONTROLLER_REPO_SERVER_PLAINTEXT", false), "Disable TLS on connections to repo server")

View File

@@ -32,7 +32,6 @@ import (
"k8s.io/client-go/kubernetes"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/clientcmd"
ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
@@ -106,12 +105,7 @@ func NewCommand() *cobra.Command {
)
cli.SetLogFormat(cmdutil.LogFormat)
if debugLog {
cli.SetLogLevel("debug")
} else {
cli.SetLogLevel(cmdutil.LogLevel)
}
cli.SetLogLevel(cmdutil.LogLevel)
ctrl.SetLogger(logutils.NewLogrusLogger(logutils.NewWithCurrentConfig()))
@@ -194,18 +188,6 @@ func NewCommand() *cobra.Command {
argoSettingsMgr := argosettings.NewSettingsManager(ctx, k8sClient, namespace)
argoCDDB := db.NewDB(namespace, argoSettingsMgr, k8sClient)
clusterInformer, err := argosettings.NewClusterInformer(k8sClient, namespace)
if err != nil {
log.Error(err, "unable to create cluster informer")
os.Exit(1)
}
go clusterInformer.Run(ctx.Done())
if !cache.WaitForCacheSync(ctx.Done(), clusterInformer.HasSynced) {
log.Error("Timed out waiting for cluster cache to sync")
os.Exit(1)
}
scmConfig := generators.NewSCMConfig(scmRootCAPath, allowedScmProviders, enableScmProviders, enableGitHubAPIMetrics, github_app.NewAuthCredentials(argoCDDB.(db.RepoCredsDB)), tokenRefStrictMode)
tlsConfig := apiclient.TLSConfiguration{
@@ -225,7 +207,7 @@ func NewCommand() *cobra.Command {
repoClientset := apiclient.NewRepoServerClientset(argocdRepoServer, repoServerTimeoutSeconds, tlsConfig)
argoCDService := services.NewArgoCDService(argoCDDB, gitSubmoduleEnabled, repoClientset, enableNewGitFileGlobbing)
topLevelGenerators := generators.GetGenerators(ctx, mgr.GetClient(), k8sClient, namespace, argoCDService, dynamicClient, scmConfig, clusterInformer)
topLevelGenerators := generators.GetGenerators(ctx, mgr.GetClient(), k8sClient, namespace, argoCDService, dynamicClient, scmConfig)
// start a webhook server that listens to incoming webhook payloads
webhookHandler, err := webhook.NewWebhookHandler(webhookParallelism, argoSettingsMgr, mgr.GetClient(), topLevelGenerators)
@@ -261,7 +243,6 @@ func NewCommand() *cobra.Command {
GlobalPreservedLabels: globalPreservedLabels,
Metrics: &metrics,
MaxResourcesStatusCount: maxResourcesStatusCount,
ClusterInformer: clusterInformer,
}).SetupWithManager(mgr, enableProgressiveSyncs, maxConcurrentReconciliations); err != nil {
log.Error(err, "unable to create controller", "controller", "ApplicationSet")
os.Exit(1)

View File

@@ -80,7 +80,6 @@ func NewCommand() *cobra.Command {
includeHiddenDirectories bool
cmpUseManifestGeneratePaths bool
ociMediaTypes []string
enableBuiltinGitConfig bool
)
command := cobra.Command{
Use: cliName,
@@ -156,7 +155,6 @@ func NewCommand() *cobra.Command {
IncludeHiddenDirectories: includeHiddenDirectories,
CMPUseManifestGeneratePaths: cmpUseManifestGeneratePaths,
OCIMediaTypes: ociMediaTypes,
EnableBuiltinGitConfig: enableBuiltinGitConfig,
}, askPassServer)
errors.CheckError(err)
@@ -267,7 +265,6 @@ 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) {

View File

@@ -84,7 +84,7 @@ func newAppProject() *unstructured.Unstructured {
Server: "*",
},
},
ClusterResourceWhitelist: []v1alpha1.ClusterResourceRestrictionItem{
ClusterResourceWhitelist: []metav1.GroupKind{
{
Group: "*",
Kind: "*",
@@ -295,8 +295,7 @@ spec:
spec:
destination: {}
project: ""
status:
health: {}
status: {}
---
`,
},
@@ -326,8 +325,7 @@ spec:
spec:
destination: {}
project: ""
status:
health: {}
status: {}
---
`,
},

View File

@@ -60,7 +60,8 @@ func Test_loadClusters(t *testing.T) {
require.NoError(t, err)
for i := range clusters {
// This changes, nil it to avoid testing it.
clusters[i].Info.ConnectionState.ModifiedAt = nil
//nolint:staticcheck
clusters[i].ConnectionState.ModifiedAt = nil
}
expected := []ClusterWithInfo{{
@@ -68,13 +69,11 @@ func Test_loadClusters(t *testing.T) {
ID: "",
Server: "https://kubernetes.default.svc",
Name: "in-cluster",
Info: v1alpha1.ClusterInfo{
ConnectionState: v1alpha1.ConnectionState{
Status: "Successful",
},
ServerVersion: ".",
ConnectionState: v1alpha1.ConnectionState{
Status: "Successful",
},
Shard: ptr.To(int64(0)),
ServerVersion: ".",
Shard: ptr.To(int64(0)),
},
Namespaces: []string{"test"},
}}

View File

@@ -77,15 +77,6 @@ func NewGenRepoSpecCommand() *cobra.Command {
# Add a private HTTP OCI repository named 'stable'
argocd admin repo generate-spec oci://helm-oci-registry.cn-zhangjiakou.cr.aliyuncs.com --type oci --name stable --username test --password test --insecure-oci-force-http
# Add a private Git repository on GitHub.com via GitHub App. github-app-installation-id is optional, if not provided, the installation id will be fetched from the GitHub API.
argocd admin repo generate-spec https://git.example.com/repos/repo --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem
# Add a private Git repository on GitHub Enterprise via GitHub App. github-app-installation-id is optional, if not provided, the installation id will be fetched from the GitHub API.
argocd admin repo generate-spec https://ghe.example.com/repos/repo --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem --github-app-enterprise-base-url https://ghe.example.com/api/v3
# Add a private Git repository on Google Cloud Sources via GCP service account credentials
argocd admin repo generate-spec https://source.developers.google.com/p/my-google-cloud-project/r/my-repo --gcp-service-account-key-path service-account-key.json
`
command := &cobra.Command{

View File

@@ -17,8 +17,6 @@ import (
"time"
"unicode/utf8"
"golang.org/x/sync/errgroup"
"github.com/argoproj/gitops-engine/pkg/health"
"github.com/argoproj/gitops-engine/pkg/sync/common"
"github.com/argoproj/gitops-engine/pkg/sync/hook"
@@ -1277,32 +1275,24 @@ type objKeyLiveTarget struct {
target *unstructured.Unstructured
}
// addServerSideDiffPerfFlags adds server-side diff performance tuning flags to a command
func addServerSideDiffPerfFlags(command *cobra.Command, serverSideDiffConcurrency *int, serverSideDiffMaxBatchKB *int) {
command.Flags().IntVar(serverSideDiffConcurrency, "server-side-diff-concurrency", -1, "Max concurrent batches for server-side diff. -1 = unlimited, 1 = sequential, 2+ = concurrent (0 = invalid)")
command.Flags().IntVar(serverSideDiffMaxBatchKB, "server-side-diff-max-batch-kb", 250, "Max batch size in KB for server-side diff. Smaller values are safer for proxies")
}
// NewApplicationDiffCommand returns a new instance of an `argocd app diff` command
func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
refresh bool
hardRefresh bool
exitCode bool
diffExitCode int
local string
revision string
localRepoRoot string
serverSideGenerate bool
serverSideDiff bool
serverSideDiffConcurrency int
serverSideDiffMaxBatchKB int
localIncludes []string
appNamespace string
revisions []string
sourcePositions []int64
sourceNames []string
ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts
refresh bool
hardRefresh bool
exitCode bool
diffExitCode int
local string
revision string
localRepoRoot string
serverSideGenerate bool
serverSideDiff bool
localIncludes []string
appNamespace string
revisions []string
sourcePositions []int64
sourceNames []string
ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts
)
shortDesc := "Perform a diff against the target and live state."
command := &cobra.Command{
@@ -1433,7 +1423,7 @@ 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, serverSideDiff, appIf, app.GetName(), app.GetNamespace(), serverSideDiffConcurrency, serverSideDiffMaxBatchKB)
foundDiffs := findAndPrintDiff(ctx, app, proj.Project, resources, argoSettings, diffOption, ignoreNormalizerOpts, serverSideDiff, appIf, app.GetName(), app.GetNamespace())
if foundDiffs && exitCode {
os.Exit(diffExitCode)
}
@@ -1448,7 +1438,6 @@ func NewApplicationDiffCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
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.")
addServerSideDiffPerfFlags(command, &serverSideDiffConcurrency, &serverSideDiffMaxBatchKB)
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")
@@ -1465,14 +1454,7 @@ func printResourceDiff(group, kind, namespace, name string, live, target *unstru
}
// 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, maxConcurrency int, maxBatchSizeKB int) bool {
if maxConcurrency == 0 {
errors.CheckError(stderrors.New("invalid value for --server-side-diff-concurrency: 0 is not allowed (use -1 for unlimited, or a positive number to limit concurrency)"))
}
liveResources := make([]*argoappv1.ResourceDiff, 0, len(items))
targetManifests := make([]string, 0, len(items))
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 {
@@ -1506,7 +1488,6 @@ func findAndPrintServerSideDiff(ctx context.Context, app *argoappv1.Application,
Modified: true,
}
}
liveResources = append(liveResources, liveResource)
if item.target != nil {
jsonBytes, err := json.Marshal(item.target)
@@ -1515,63 +1496,23 @@ func findAndPrintServerSideDiff(ctx context.Context, app *argoappv1.Application,
}
targetManifest = string(jsonBytes)
}
targetManifests = append(targetManifests, targetManifest)
}
if len(liveResources) == 0 {
return false
}
// Batch by size to avoid proxy limits
maxBatchSize := maxBatchSizeKB * 1024
var batches []struct{ start, end int }
for i := 0; i < len(liveResources); {
start := i
size := 0
for i < len(liveResources) {
resourceSize := len(liveResources[i].LiveState) + len(targetManifests[i])
if size+resourceSize > maxBatchSize && i > start {
break
}
size += resourceSize
i++
// 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},
}
batches = append(batches, struct{ start, end int }{start, i})
}
// Process batches in parallel
g, errGroupCtx := errgroup.WithContext(ctx)
g.SetLimit(maxConcurrency)
serverSideDiffRes, err := appIf.ServerSideDiff(ctx, serverSideDiffQuery)
if err != nil {
errors.CheckError(err)
}
results := make([][]*argoappv1.ResourceDiff, len(batches))
for idx, batch := range batches {
i := idx
b := batch
g.Go(func() error {
// Call server-side diff for this batch of resources
serverSideDiffQuery := &application.ApplicationServerSideDiffQuery{
AppName: &appName,
AppNamespace: &appNs,
Project: &app.Spec.Project,
LiveResources: liveResources[b.start:b.end],
TargetManifests: targetManifests[b.start:b.end],
}
serverSideDiffRes, err := appIf.ServerSideDiff(errGroupCtx, serverSideDiffQuery)
if err != nil {
return err
}
results[i] = serverSideDiffRes.Items
return nil
})
}
if err := g.Wait(); err != nil {
errors.CheckError(err)
}
for _, items := range results {
for _, resultItem := range items {
// Extract diff for this resource
for _, resultItem := range serverSideDiffRes.Items {
if resultItem.Hook || (!resultItem.Modified && resultItem.TargetState != "" && resultItem.LiveState != "") {
continue
}
@@ -1581,12 +1522,13 @@ func findAndPrintServerSideDiff(ctx context.Context, app *argoappv1.Application,
if resultItem.TargetState != "" && resultItem.TargetState != "null" {
target = &unstructured.Unstructured{}
err := json.Unmarshal([]byte(resultItem.TargetState), target)
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)
err = json.Unmarshal([]byte(resultItem.LiveState), live)
errors.CheckError(err)
}
@@ -1612,14 +1554,14 @@ type DifferenceOption struct {
}
// 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, serverSideDiffConcurrency int, serverSideDiffMaxBatchKB int) bool {
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
items, err := prepareObjectsForDiff(ctx, app, proj, resources, argoSettings, diffOptions)
errors.CheckError(err)
if useServerSideDiff {
return findAndPrintServerSideDiff(ctx, app, items, resources, appIf, appName, appNs, serverSideDiffConcurrency, serverSideDiffMaxBatchKB)
return findAndPrintServerSideDiff(ctx, app, items, resources, appIf, appName, appNs)
}
for _, item := range items {
@@ -2133,38 +2075,36 @@ func printTreeViewDetailed(nodeMapping map[string]argoappv1.ResourceNode, parent
// NewApplicationSyncCommand returns a new instance of an `argocd app sync` command
func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
var (
revision string
revisions []string
sourcePositions []int64
sourceNames []string
resources []string
labels []string
selector string
prune bool
dryRun bool
timeout uint
strategy string
force bool
replace bool
serverSideApply bool
applyOutOfSyncOnly bool
async bool
retryLimit int64
retryRefresh bool
retryBackoffDuration time.Duration
retryBackoffMaxDuration time.Duration
retryBackoffFactor int64
local string
localRepoRoot string
infos []string
diffChanges bool
diffChangesConfirm bool
projects []string
output string
appNamespace string
ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts
serverSideDiffConcurrency int
serverSideDiffMaxBatchKB int
revision string
revisions []string
sourcePositions []int64
sourceNames []string
resources []string
labels []string
selector string
prune bool
dryRun bool
timeout uint
strategy string
force bool
replace bool
serverSideApply bool
applyOutOfSyncOnly bool
async bool
retryLimit int64
retryRefresh bool
retryBackoffDuration time.Duration
retryBackoffMaxDuration time.Duration
retryBackoffFactor int64
local string
localRepoRoot string
infos []string
diffChanges bool
diffChangesConfirm bool
projects []string
output string
appNamespace string
ignoreNormalizerOpts normalizers.IgnoreNormalizerOpts
)
command := &cobra.Command{
Use: "sync [APPNAME... | -l selector | --project project-name]",
@@ -2453,7 +2393,7 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
// 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, serverSideDiffConcurrency, serverSideDiffMaxBatchKB)
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
@@ -2518,7 +2458,6 @@ func NewApplicationSyncCommand(clientOpts *argocdclient.ClientOptions) *cobra.Co
command.Flags().StringArrayVar(&revisions, "revisions", []string{}, "Show manifests at specific revisions for source position in source-positions")
command.Flags().Int64SliceVar(&sourcePositions, "source-positions", []int64{}, "List of source positions. Default is empty array. Counting start at 1.")
command.Flags().StringArrayVar(&sourceNames, "source-names", []string{}, "List of source names. Default is an empty array.")
addServerSideDiffPerfFlags(command, &serverSideDiffConcurrency, &serverSideDiffMaxBatchKB)
return command
}
@@ -3283,7 +3222,8 @@ func NewApplicationManifestsCommand(clientOpts *argocdclient.ClientOptions) *cob
errors.CheckError(err)
proj := getProject(ctx, c, clientOpts, app.Spec.Project)
unstructureds = getLocalObjects(context.Background(), app, proj.Project, local, localRepoRoot, argoSettings.AppLabelKey, cluster.Info.ServerVersion, cluster.Info.APIVersions, argoSettings.KustomizeOptions, argoSettings.TrackingMethod)
//nolint:staticcheck
unstructureds = getLocalObjects(context.Background(), app, proj.Project, local, localRepoRoot, argoSettings.AppLabelKey, cluster.ServerVersion, cluster.Info.APIVersions, argoSettings.KustomizeOptions, argoSettings.TrackingMethod)
case len(revisions) > 0 && len(sourcePositions) > 0:
q := application.ApplicationManifestQuery{
Name: &appName,

View File

@@ -33,7 +33,6 @@ func NewApplicationGetResourceCommand(clientOpts *argocdclient.ClientOptions) *c
var (
resourceName string
kind string
group string
project string
filteredFields []string
showManagedFields bool
@@ -89,7 +88,7 @@ func NewApplicationGetResourceCommand(clientOpts *argocdclient.ClientOptions) *c
var resources []unstructured.Unstructured
var fetchedStr string
for _, r := range tree.Nodes {
if (resourceName != "" && r.Name != resourceName) || (group != "" && r.Group != group) || r.Kind != kind {
if (resourceName != "" && r.Name != resourceName) || r.Kind != kind {
continue
}
resource, err := appIf.GetResource(ctx, &applicationpkg.ApplicationResourceRequest{
@@ -132,7 +131,6 @@ func NewApplicationGetResourceCommand(clientOpts *argocdclient.ClientOptions) *c
command.Flags().StringVar(&kind, "kind", "", "Kind of resource [REQUIRED]")
err := command.MarkFlagRequired("kind")
errors.CheckError(err)
command.Flags().StringVar(&group, "group", "", "Group")
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")

View File

@@ -395,12 +395,12 @@ func printApplicationSetNames(apps []arogappsetv1.ApplicationSet) {
func printApplicationSetTable(apps []arogappsetv1.ApplicationSet, output *string) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
var fmtStr string
headers := []any{"NAME", "PROJECT", "SYNCPOLICY", "HEALTH", "CONDITIONS"}
headers := []any{"NAME", "PROJECT", "SYNCPOLICY", "CONDITIONS"}
if *output == "wide" {
fmtStr = "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n"
fmtStr = "%s\t%s\t%s\t%s\t%s\t%s\t%s\n"
headers = append(headers, "REPO", "PATH", "TARGET")
} else {
fmtStr = "%s\t%s\t%s\t%s\t%s\n"
fmtStr = "%s\t%s\t%s\t%s\n"
}
_, _ = fmt.Fprintf(w, fmtStr, headers...)
for _, app := range apps {
@@ -414,7 +414,6 @@ func printApplicationSetTable(apps []arogappsetv1.ApplicationSet, output *string
app.QualifiedName(),
app.Spec.Template.Spec.Project,
app.Spec.SyncPolicy,
app.Status.Health.Status,
conditions,
}
if *output == "wide" {
@@ -438,7 +437,6 @@ func printAppSetSummaryTable(appSet *arogappsetv1.ApplicationSet) {
fmt.Printf(printOpFmtStr, "Project:", appSet.Spec.Template.Spec.GetProject())
fmt.Printf(printOpFmtStr, "Server:", getServerForAppSet(appSet))
fmt.Printf(printOpFmtStr, "Namespace:", appSet.Spec.Template.Spec.Destination.Namespace)
fmt.Printf(printOpFmtStr, "Health Status:", appSet.Status.Health.Status)
if !appSet.Spec.Template.Spec.HasMultipleSources() {
fmt.Println("Source:")
} else {

View File

@@ -107,7 +107,7 @@ func TestPrintApplicationSetTable(t *testing.T) {
return nil
})
require.NoError(t, err)
expectation := "NAME PROJECT SYNCPOLICY HEALTH CONDITIONS\napp-name default nil [{ResourcesUpToDate <nil> True }]\nteam-two/app-name default nil [{ResourcesUpToDate <nil> True }]\n"
expectation := "NAME PROJECT SYNCPOLICY CONDITIONS\napp-name default nil [{ResourcesUpToDate <nil> True }]\nteam-two/app-name default nil [{ResourcesUpToDate <nil> True }]\n"
assert.Equal(t, expectation, output)
}
@@ -200,7 +200,6 @@ func TestPrintAppSetSummaryTable(t *testing.T) {
Project: default
Server:
Namespace:
Health Status:
Source:
- Repo:
Target:
@@ -214,7 +213,6 @@ SyncPolicy: <none>
Project: default
Server:
Namespace:
Health Status:
Source:
- Repo:
Target:
@@ -228,7 +226,6 @@ SyncPolicy: Automated
Project: default
Server:
Namespace:
Health Status:
Source:
- Repo:
Target:
@@ -242,7 +239,6 @@ SyncPolicy: Automated
Project: default
Server:
Namespace:
Health Status:
Source:
- Repo: test1
Target: master1
@@ -257,7 +253,6 @@ SyncPolicy: <none>
Project: default
Server:
Namespace:
Health Status:
Sources:
- Repo: test1
Target: master1

View File

@@ -380,7 +380,8 @@ func printClusterDetails(clusters []argoappv1.Cluster) {
fmt.Printf("Cluster information\n\n")
fmt.Printf(" Server URL: %s\n", cluster.Server)
fmt.Printf(" Server Name: %s\n", strWithDefault(cluster.Name, "-"))
fmt.Printf(" Server Version: %s\n", cluster.Info.ServerVersion)
//nolint:staticcheck
fmt.Printf(" Server Version: %s\n", cluster.ServerVersion)
fmt.Printf(" Namespaces: %s\n", formatNamespaces(cluster))
fmt.Printf("\nTLS configuration\n\n")
fmt.Printf(" Client cert: %v\n", len(cluster.Config.CertData) != 0)
@@ -474,7 +475,8 @@ func printClusterTable(clusters []argoappv1.Cluster) {
if len(c.Namespaces) > 0 {
server = fmt.Sprintf("%s (%d namespaces)", c.Server, len(c.Namespaces))
}
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", server, c.Name, c.Info.ServerVersion, c.Info.ConnectionState.Status, c.Info.ConnectionState.Message, c.Project)
//nolint:staticcheck
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", server, c.Name, c.ServerVersion, c.ConnectionState.Status, c.ConnectionState.Message, c.Project)
}
_ = w.Flush()
}

View File

@@ -39,14 +39,12 @@ func Test_printClusterTable(_ *testing.T) {
AWSAuthConfig: nil,
DisableCompression: false,
},
Info: v1alpha1.ClusterInfo{
ConnectionState: v1alpha1.ConnectionState{
Status: "my-status",
Message: "my-message",
ModifiedAt: &metav1.Time{},
},
ServerVersion: "my-version",
ConnectionState: v1alpha1.ConnectionState{
Status: "my-status",
Message: "my-message",
ModifiedAt: &metav1.Time{},
},
ServerVersion: "my-version",
},
})
}

View File

@@ -223,19 +223,6 @@ $ source _argocd
$ argocd completion fish > ~/.config/fish/completions/argocd.fish
$ source ~/.config/fish/completions/argocd.fish
# For powershell
$ mkdir -Force "$HOME\Documents\PowerShell" | Out-Null
$ argocd completion powershell > $HOME\Documents\PowerShell\argocd_completion.ps1
Add the following lines to your powershell profile
$ # ArgoCD tab completion
if (Test-Path "$HOME\Documents\PowerShell\argocd_completion.ps1") {
. "$HOME\Documents\PowerShell\argocd_completion.ps1"
}
Then reload your profile
$ . $PROFILE
`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 1 {
@@ -246,10 +233,9 @@ $ . $PROFILE
rootCommand := NewCommand()
rootCommand.BashCompletionFunction = bashCompletionFunc
availableCompletions := map[string]func(out io.Writer, cmd *cobra.Command) error{
"bash": runCompletionBash,
"zsh": runCompletionZsh,
"fish": runCompletionFish,
"powershell": runCompletionPowershell,
"bash": runCompletionBash,
"zsh": runCompletionZsh,
"fish": runCompletionFish,
}
completion, ok := availableCompletions[shell]
if !ok {
@@ -276,7 +262,3 @@ func runCompletionZsh(out io.Writer, cmd *cobra.Command) error {
func runCompletionFish(out io.Writer, cmd *cobra.Command) error {
return cmd.GenFishCompletion(out, true)
}
func runCompletionPowershell(out io.Writer, cmd *cobra.Command) error {
return cmd.GenPowerShellCompletionWithDesc(out)
}

View File

@@ -34,10 +34,6 @@ argocd context cd.argoproj.io --delete`,
Run: func(c *cobra.Command, args []string) {
localCfg, err := localconfig.ReadLocalConfig(clientOpts.ConfigPath)
errors.CheckError(err)
if localCfg == nil {
fmt.Println("No local configuration found")
os.Exit(1)
}
if deletion {
if len(args) == 0 {

View File

@@ -41,9 +41,8 @@ type policyOpts struct {
// NewProjectCommand returns a new instance of an `argocd proj` command
func NewProjectCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
command := &cobra.Command{
Use: "proj",
Short: "Manage projects",
Aliases: []string{"project"},
Use: "proj",
Short: "Manage projects",
Example: templates.Examples(`
# List all available projects
argocd proj list
@@ -591,15 +590,17 @@ func NewProjectRemoveSourceNamespace(clientOpts *argocdclient.ClientOptions) *co
return command
}
func modifyNamespacedResourcesList(list *[]metav1.GroupKind, add bool, listAction string, group string, kind string) (bool, string) {
func modifyResourcesList(list *[]metav1.GroupKind, add bool, listDesc string, group string, kind string) bool {
if add {
for _, item := range *list {
if item.Group == group && item.Kind == kind {
return false, fmt.Sprintf("Group '%s' and kind '%s' already present in %s namespaced resources", group, kind, listAction)
fmt.Printf("Group '%s' and kind '%s' already present in %s resources\n", group, kind, listDesc)
return false
}
}
fmt.Printf("Group '%s' and kind '%s' is added to %s resources\n", group, kind, listDesc)
*list = append(*list, metav1.GroupKind{Group: group, Kind: kind})
return true, fmt.Sprintf("Group '%s' and kind '%s' is added to %s namespaced resources", group, kind, listAction)
return true
}
index := -1
for i, item := range *list {
@@ -609,37 +610,15 @@ func modifyNamespacedResourcesList(list *[]metav1.GroupKind, add bool, listActio
}
}
if index == -1 {
return false, fmt.Sprintf("Group '%s' and kind '%s' not in %s namespaced resources", group, kind, listAction)
fmt.Printf("Group '%s' and kind '%s' not in %s resources\n", group, kind, listDesc)
return false
}
*list = append((*list)[:index], (*list)[index+1:]...)
return true, fmt.Sprintf("Group '%s' and kind '%s' is removed from %s namespaced resources", group, kind, listAction)
fmt.Printf("Group '%s' and kind '%s' is removed from %s resources\n", group, kind, listDesc)
return true
}
func modifyClusterResourcesList(list *[]v1alpha1.ClusterResourceRestrictionItem, add bool, listAction string, group string, kind string, name string) (bool, string) {
if add {
for _, item := range *list {
if item.Group == group && item.Kind == kind && item.Name == name {
return false, fmt.Sprintf("Group '%s', kind '%s', and name '%s' is already present in %s cluster resources", group, kind, name, listAction)
}
}
*list = append(*list, v1alpha1.ClusterResourceRestrictionItem{Group: group, Kind: kind, Name: name})
return true, fmt.Sprintf("Group '%s', kind '%s', and name '%s' is added to %s cluster resources", group, kind, name, listAction)
}
index := -1
for i, item := range *list {
if item.Group == group && item.Kind == kind && item.Name == name {
index = i
break
}
}
if index == -1 {
return false, fmt.Sprintf("Group '%s', kind '%s', and name '%s' not in %s cluster resources", group, kind, name, listAction)
}
*list = append((*list)[:index], (*list)[index+1:]...)
return true, fmt.Sprintf("Group '%s', kind '%s', and name '%s' is removed from %s cluster resources", group, kind, name, listAction)
}
func modifyResourceListCmd(getProjIf func(*cobra.Command) (io.Closer, projectpkg.ProjectServiceClient), cmdUse, cmdDesc, examples string, allow bool, namespacedList bool) *cobra.Command {
func modifyResourceListCmd(cmdUse, cmdDesc, examples string, clientOpts *argocdclient.ClientOptions, allow bool, namespacedList bool) *cobra.Command {
var (
listType string
defaultList string
@@ -656,61 +635,38 @@ func modifyResourceListCmd(getProjIf func(*cobra.Command) (io.Closer, projectpkg
Run: func(c *cobra.Command, args []string) {
ctx := c.Context()
if namespacedList && len(args) != 3 {
if len(args) != 3 {
c.HelpFunc()(c, args)
os.Exit(1)
}
if !namespacedList && (len(args) < 3 || len(args) > 4) {
// Cluster-scoped resource command can have an optional NAME argument.
c.HelpFunc()(c, args)
os.Exit(1)
}
projName, group, kind := args[0], args[1], args[2]
var name string
if !namespacedList && len(args) > 3 {
name = args[3]
}
conn, projIf := getProjIf(c)
conn, projIf := headless.NewClientOrDie(clientOpts, c).NewProjectClientOrDie()
defer utilio.Close(conn)
proj, err := projIf.Get(ctx, &projectpkg.ProjectQuery{Name: projName})
errors.CheckError(err)
var list, allowList, denyList *[]metav1.GroupKind
var clusterList *[]v1alpha1.ClusterResourceRestrictionItem
var clusterAllowList, clusterDenyList *[]v1alpha1.ClusterResourceRestrictionItem
var listAction string
var listAction, listDesc string
var add bool
if namespacedList {
allowList, denyList = &proj.Spec.NamespaceResourceWhitelist, &proj.Spec.NamespaceResourceBlacklist
listDesc = "namespaced"
} else {
clusterAllowList, clusterDenyList = &proj.Spec.ClusterResourceWhitelist, &proj.Spec.ClusterResourceBlacklist
allowList, denyList = &proj.Spec.ClusterResourceWhitelist, &proj.Spec.ClusterResourceBlacklist
listDesc = "cluster"
}
if (listType == "allow") || (listType == "white") {
list = allowList
clusterList = clusterAllowList
listAction = "allowed"
add = allow
} else {
list = denyList
clusterList = clusterDenyList
listAction = "denied"
add = !allow
}
if !namespacedList {
if ok, msg := modifyClusterResourcesList(clusterList, add, listAction, group, kind, name); ok {
c.Println(msg)
_, err = projIf.Update(ctx, &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
}
return
}
if ok, msg := modifyNamespacedResourcesList(list, add, listAction, group, kind); ok {
c.Println(msg)
if modifyResourcesList(list, add, listAction+" "+listDesc, group, kind) {
_, err = projIf.Update(ctx, &projectpkg.ProjectUpdateRequest{Project: proj})
errors.CheckError(err)
}
@@ -728,10 +684,7 @@ func NewProjectAllowNamespaceResourceCommand(clientOpts *argocdclient.ClientOpti
# Removes a namespaced API resource with specified GROUP and KIND from the deny list or add a namespaced API resource to the allow list for project PROJECT
argocd proj allow-namespace-resource PROJECT GROUP KIND
`
getProjIf := func(cmd *cobra.Command) (io.Closer, projectpkg.ProjectServiceClient) {
return headless.NewClientOrDie(clientOpts, cmd).NewProjectClientOrDie()
}
return modifyResourceListCmd(getProjIf, use, desc, examples, true, true)
return modifyResourceListCmd(use, desc, examples, clientOpts, true, true)
}
// NewProjectDenyNamespaceResourceCommand returns a new instance of an `argocd proj deny-namespace-resource` command
@@ -742,10 +695,7 @@ func NewProjectDenyNamespaceResourceCommand(clientOpts *argocdclient.ClientOptio
# Adds a namespaced API resource with specified GROUP and KIND from the deny list or removes a namespaced API resource from the allow list for project PROJECT
argocd proj deny-namespace-resource PROJECT GROUP KIND
`
getProjIf := func(cmd *cobra.Command) (io.Closer, projectpkg.ProjectServiceClient) {
return headless.NewClientOrDie(clientOpts, cmd).NewProjectClientOrDie()
}
return modifyResourceListCmd(getProjIf, use, desc, examples, false, true)
return modifyResourceListCmd(use, desc, examples, clientOpts, false, true)
}
// NewProjectDenyClusterResourceCommand returns a new instance of an `deny-cluster-resource` command
@@ -756,27 +706,18 @@ func NewProjectDenyClusterResourceCommand(clientOpts *argocdclient.ClientOptions
# Removes a cluster-scoped API resource with specified GROUP and KIND from the allow list and adds it to deny list for project PROJECT
argocd proj deny-cluster-resource PROJECT GROUP KIND
`
getProjIf := func(cmd *cobra.Command) (io.Closer, projectpkg.ProjectServiceClient) {
return headless.NewClientOrDie(clientOpts, cmd).NewProjectClientOrDie()
}
return modifyResourceListCmd(getProjIf, use, desc, examples, false, false)
return modifyResourceListCmd(use, desc, examples, clientOpts, false, false)
}
// NewProjectAllowClusterResourceCommand returns a new instance of an `argocd proj allow-cluster-resource` command
func NewProjectAllowClusterResourceCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
use := "allow-cluster-resource PROJECT GROUP KIND [NAME]"
use := "allow-cluster-resource PROJECT GROUP KIND"
desc := "Adds a cluster-scoped API resource to the allow list and removes it from deny list"
examples := `
# Adds a cluster-scoped API resource with specified GROUP and KIND to the allow list and removes it from deny list for project PROJECT
argocd proj allow-cluster-resource PROJECT GROUP KIND
# Adds a cluster-scoped API resource with specified GROUP, KIND and NAME pattern to the allow list and removes it from deny list for project PROJECT
argocd proj allow-cluster-resource PROJECT GROUP KIND NAME
`
getProjIf := func(cmd *cobra.Command) (io.Closer, projectpkg.ProjectServiceClient) {
return headless.NewClientOrDie(clientOpts, cmd).NewProjectClientOrDie()
}
return modifyResourceListCmd(getProjIf, use, desc, examples, true, false)
return modifyResourceListCmd(use, desc, examples, clientOpts, true, false)
}
// NewProjectRemoveSourceCommand returns a new instance of an `argocd proj remove-src` command
@@ -885,7 +826,7 @@ func NewProjectListCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comman
# List all available projects
argocd proj list
# List all available projects in yaml format (other options are "json" and "name")
# List all available projects in yaml format
argocd proj list -o yaml
`),
Run: func(c *cobra.Command, _ []string) {

View File

@@ -1,256 +0,0 @@
package commands
import (
"bytes"
"io"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
projectpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/project"
projectmocks "github.com/argoproj/argo-cd/v3/pkg/apiclient/project/mocks"
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
)
func TestModifyResourceListCmd_AddClusterAllowItemWithName(t *testing.T) {
// Create a mock project client
mockProjClient := projectmocks.NewProjectServiceClient(t)
// Mock project data
projectName := "test-project"
mockProject := &v1alpha1.AppProject{
Spec: v1alpha1.AppProjectSpec{
ClusterResourceWhitelist: []v1alpha1.ClusterResourceRestrictionItem{},
},
}
// Mock Get and Update calls
mockProjClient.On("Get", mock.Anything, mock.Anything).Return(mockProject, nil)
mockProjClient.On("Update", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
req := args.Get(1).(*projectpkg.ProjectUpdateRequest)
mockProject.Spec.ClusterResourceWhitelist = req.Project.Spec.ClusterResourceWhitelist
}).Return(mockProject, nil)
getProjIf := func(_ *cobra.Command) (io.Closer, projectpkg.ProjectServiceClient) {
return io.NopCloser(bytes.NewBufferString("")), mockProjClient
}
// Create the command
cmd := modifyResourceListCmd(
getProjIf,
"allow-cluster-resource",
"Test command",
"Example usage",
true,
false,
)
// Set up the command arguments
args := []string{projectName, "apps", "Deployment", "example-deployment"}
cmd.SetArgs(args)
// Capture the output
var output bytes.Buffer
cmd.SetOut(&output)
// Execute the command
err := cmd.ExecuteContext(t.Context())
require.NoError(t, err)
// Verify the project was updated correctly
expected := []v1alpha1.ClusterResourceRestrictionItem{
{Group: "apps", Kind: "Deployment", Name: "example-deployment"},
}
assert.Equal(t, expected, mockProject.Spec.ClusterResourceWhitelist)
// Verify the output
assert.Contains(t, output.String(), "Group 'apps', kind 'Deployment', and name 'example-deployment' is added to allowed cluster resources")
}
func Test_modifyNamespacedResourceList(t *testing.T) {
tests := []struct {
name string
initialList []metav1.GroupKind
add bool
group string
kind string
expectedList []metav1.GroupKind
expectedResult bool
}{
{
name: "Add new item to empty list",
initialList: []metav1.GroupKind{},
add: true,
group: "apps",
kind: "Deployment",
expectedList: []metav1.GroupKind{
{Group: "apps", Kind: "Deployment"},
},
expectedResult: true,
},
{
name: "Add duplicate item",
initialList: []metav1.GroupKind{
{Group: "apps", Kind: "Deployment"},
},
add: true,
group: "apps",
kind: "Deployment",
expectedList: []metav1.GroupKind{
{Group: "apps", Kind: "Deployment"},
},
expectedResult: false,
},
{
name: "Remove existing item",
initialList: []metav1.GroupKind{
{Group: "apps", Kind: "Deployment"},
},
add: false,
group: "apps",
kind: "Deployment",
expectedList: []metav1.GroupKind{},
expectedResult: true,
},
{
name: "Remove non-existent item",
initialList: []metav1.GroupKind{
{Group: "apps", Kind: "Deployment"},
},
add: false,
group: "apps",
kind: "StatefulSet",
expectedList: []metav1.GroupKind{
{Group: "apps", Kind: "Deployment"},
},
expectedResult: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
list := tt.initialList
result, _ := modifyNamespacedResourcesList(&list, tt.add, "", tt.group, tt.kind)
assert.Equal(t, tt.expectedResult, result)
assert.Equal(t, tt.expectedList, list)
})
}
}
func Test_modifyAllowClusterResourceList(t *testing.T) {
tests := []struct {
name string
initialList []v1alpha1.ClusterResourceRestrictionItem
add bool
group string
kind string
resourceName string
expectedList []v1alpha1.ClusterResourceRestrictionItem
expectedResult bool
}{
{
name: "Add new item to empty list",
initialList: []v1alpha1.ClusterResourceRestrictionItem{},
add: true,
group: "apps",
kind: "Deployment",
resourceName: "",
expectedList: []v1alpha1.ClusterResourceRestrictionItem{
{Group: "apps", Kind: "Deployment", Name: ""},
},
expectedResult: true,
},
{
name: "Add duplicate item",
initialList: []v1alpha1.ClusterResourceRestrictionItem{
{Group: "apps", Kind: "Deployment", Name: ""},
},
add: true,
group: "apps",
kind: "Deployment",
resourceName: "",
expectedList: []v1alpha1.ClusterResourceRestrictionItem{
{Group: "apps", Kind: "Deployment", Name: ""},
},
expectedResult: false,
},
{
name: "Remove existing item",
initialList: []v1alpha1.ClusterResourceRestrictionItem{
{Group: "apps", Kind: "Deployment", Name: ""},
},
add: false,
group: "apps",
kind: "Deployment",
resourceName: "",
expectedList: []v1alpha1.ClusterResourceRestrictionItem{},
expectedResult: true,
},
{
name: "Remove non-existent item",
initialList: []v1alpha1.ClusterResourceRestrictionItem{
{Group: "apps", Kind: "Deployment", Name: ""},
},
add: false,
group: "apps",
kind: "StatefulSet",
resourceName: "",
expectedList: []v1alpha1.ClusterResourceRestrictionItem{
{Group: "apps", Kind: "Deployment", Name: ""},
},
expectedResult: false,
},
{
name: "Add item with name",
initialList: []v1alpha1.ClusterResourceRestrictionItem{},
add: true,
group: "apps",
kind: "Deployment",
resourceName: "example-deployment",
expectedList: []v1alpha1.ClusterResourceRestrictionItem{
{Group: "apps", Kind: "Deployment", Name: "example-deployment"},
},
expectedResult: true,
},
{
name: "Remove item with name",
initialList: []v1alpha1.ClusterResourceRestrictionItem{
{Group: "apps", Kind: "Deployment", Name: "example-deployment"},
},
add: false,
group: "apps",
kind: "Deployment",
resourceName: "example-deployment",
expectedList: []v1alpha1.ClusterResourceRestrictionItem{},
expectedResult: true,
},
{
name: "Attempt to remove item with name but only group and kind exist",
initialList: []v1alpha1.ClusterResourceRestrictionItem{
{Group: "apps", Kind: "Deployment", Name: ""},
},
add: false,
group: "apps",
kind: "Deployment",
resourceName: "example-deployment",
expectedList: []v1alpha1.ClusterResourceRestrictionItem{
{Group: "apps", Kind: "Deployment", Name: ""},
},
expectedResult: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
list := tt.initialList
result, _ := modifyClusterResourcesList(&list, tt.add, "", tt.group, tt.kind, tt.resourceName)
assert.Equal(t, tt.expectedResult, result)
assert.Equal(t, tt.expectedList, list)
})
}
}

View File

@@ -94,10 +94,10 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
# Add a private HTTP OCI repository named 'stable'
argocd repo add oci://helm-oci-registry.cn-zhangjiakou.cr.aliyuncs.com --type oci --name stable --username test --password test --insecure-oci-force-http
# Add a private Git repository on GitHub.com via GitHub App. github-app-installation-id is optional, if not provided, the installation id will be fetched from the GitHub API.
# Add a private Git repository on GitHub.com via GitHub App
argocd repo add https://git.example.com/repos/repo --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem
# Add a private Git repository on GitHub Enterprise via GitHub App. github-app-installation-id is optional, if not provided, the installation id will be fetched from the GitHub API.
# Add a private Git repository on GitHub Enterprise via GitHub App
argocd repo add https://ghe.example.com/repos/repo --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem --github-app-enterprise-base-url https://ghe.example.com/api/v3
# Add a private Git repository on Google Cloud Sources via GCP service account credentials

View File

@@ -72,10 +72,10 @@ func NewRepoCredsAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comma
# Add credentials with SSH private key authentication to use for all repositories under ssh://git@git.example.com/repos
argocd repocreds add ssh://git@git.example.com/repos/ --ssh-private-key-path ~/.ssh/id_rsa
# Add credentials with GitHub App authentication to use for all repositories under https://github.com/repos. github-app-installation-id is optional, if not provided, the installation id will be fetched from the GitHub API.
# Add credentials with GitHub App authentication to use for all repositories under https://github.com/repos
argocd repocreds add https://github.com/repos/ --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem
# Add credentials with GitHub App authentication to use for all repositories under https://ghe.example.com/repos. github-app-installation-id is optional, if not provided, the installation id will be fetched from the GitHub API.
# Add credentials with GitHub App authentication to use for all repositories under https://ghe.example.com/repos
argocd repocreds add https://ghe.example.com/repos/ --github-app-id 1 --github-app-installation-id 2 --github-app-private-key-path test.private-key.pem --github-app-enterprise-base-url https://ghe.example.com/api/v3
# Add credentials with helm oci registry so that these oci registry urls do not need to be added as repos individually.
@@ -191,7 +191,7 @@ func NewRepoCredsAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comma
command.Flags().StringVar(&tlsClientCertPath, "tls-client-cert-path", "", "path to the TLS client cert (must be PEM format)")
command.Flags().StringVar(&tlsClientCertKeyPath, "tls-client-cert-key-path", "", "path to the TLS client cert's key (must be PEM format)")
command.Flags().Int64Var(&repo.GithubAppId, "github-app-id", 0, "id of the GitHub Application")
command.Flags().Int64Var(&repo.GithubAppInstallationId, "github-app-installation-id", 0, "installation id of the GitHub Application (optional, will be auto-discovered if not provided)")
command.Flags().Int64Var(&repo.GithubAppInstallationId, "github-app-installation-id", 0, "installation id of the GitHub Application")
command.Flags().StringVar(&githubAppPrivateKeyPath, "github-app-private-key-path", "", "private key of the GitHub Application")
command.Flags().StringVar(&repo.GitHubAppEnterpriseBaseURL, "github-app-enterprise-base-url", "", "base url to use when using GitHub Enterprise (e.g. https://ghe.example.com/api/v3")
command.Flags().BoolVar(&upsert, "upsert", false, "Override an existing repository with the same name even if the spec differs")

View File

@@ -43,8 +43,8 @@ func AddProjFlags(command *cobra.Command, opts *ProjectOpts) {
command.Flags().StringSliceVar(&opts.SignatureKeys, "signature-keys", []string{}, "GnuPG public key IDs for commit signature verification")
command.Flags().BoolVar(&opts.orphanedResourcesEnabled, "orphaned-resources", false, "Enables orphaned resources monitoring")
command.Flags().BoolVar(&opts.orphanedResourcesWarn, "orphaned-resources-warn", false, "Specifies if applications should have a warning condition when orphaned resources detected")
command.Flags().StringArrayVar(&opts.allowedClusterResources, "allow-cluster-resource", []string{}, "List of allowed cluster level resources, optionally with group and name (e.g. ClusterRole, apiextensions.k8s.io/CustomResourceDefinition, /Namespace/team1-*)")
command.Flags().StringArrayVar(&opts.deniedClusterResources, "deny-cluster-resource", []string{}, "List of denied cluster level resources, optionally with group and name (e.g. ClusterRole, apiextensions.k8s.io/CustomResourceDefinition, /Namespace/kube-*)")
command.Flags().StringArrayVar(&opts.allowedClusterResources, "allow-cluster-resource", []string{}, "List of allowed cluster level resources")
command.Flags().StringArrayVar(&opts.deniedClusterResources, "deny-cluster-resource", []string{}, "List of denied cluster level resources")
command.Flags().StringArrayVar(&opts.allowedNamespacedResources, "allow-namespaced-resource", []string{}, "List of allowed namespaced resources")
command.Flags().StringArrayVar(&opts.deniedNamespacedResources, "deny-namespaced-resource", []string{}, "List of denied namespaced resources")
command.Flags().StringSliceVar(&opts.SourceNamespaces, "source-namespaces", []string{}, "List of source namespaces for applications")
@@ -64,26 +64,12 @@ func getGroupKindList(values []string) []metav1.GroupKind {
return res
}
func getClusterResourceRestrictionItemList(values []string) []v1alpha1.ClusterResourceRestrictionItem {
var res []v1alpha1.ClusterResourceRestrictionItem
for _, val := range values {
if parts := strings.Split(val, "/"); len(parts) == 3 {
res = append(res, v1alpha1.ClusterResourceRestrictionItem{Group: parts[0], Kind: parts[1], Name: parts[2]})
} else if parts = strings.Split(val, "/"); len(parts) == 2 {
res = append(res, v1alpha1.ClusterResourceRestrictionItem{Group: parts[0], Kind: parts[1]})
} else if len(parts) == 1 {
res = append(res, v1alpha1.ClusterResourceRestrictionItem{Kind: parts[0]})
}
}
return res
func (opts *ProjectOpts) GetAllowedClusterResources() []metav1.GroupKind {
return getGroupKindList(opts.allowedClusterResources)
}
func (opts *ProjectOpts) GetAllowedClusterResources() []v1alpha1.ClusterResourceRestrictionItem {
return getClusterResourceRestrictionItemList(opts.allowedClusterResources)
}
func (opts *ProjectOpts) GetDeniedClusterResources() []v1alpha1.ClusterResourceRestrictionItem {
return getClusterResourceRestrictionItemList(opts.deniedClusterResources)
func (opts *ProjectOpts) GetDeniedClusterResources() []metav1.GroupKind {
return getGroupKindList(opts.deniedClusterResources)
}
func (opts *ProjectOpts) GetAllowedNamespacedResources() []metav1.GroupKind {

View File

@@ -19,8 +19,8 @@ func TestProjectOpts_ResourceLists(t *testing.T) {
assert.ElementsMatch(t, []metav1.GroupKind{{Kind: "ConfigMap"}}, opts.GetAllowedNamespacedResources())
assert.ElementsMatch(t, []metav1.GroupKind{{Group: "apps", Kind: "DaemonSet"}}, opts.GetDeniedNamespacedResources())
assert.ElementsMatch(t, []v1alpha1.ClusterResourceRestrictionItem{{Group: "apiextensions.k8s.io", Kind: "CustomResourceDefinition"}}, opts.GetAllowedClusterResources())
assert.ElementsMatch(t, []v1alpha1.ClusterResourceRestrictionItem{{Group: "rbac.authorization.k8s.io", Kind: "ClusterRole"}}, opts.GetDeniedClusterResources())
assert.ElementsMatch(t, []metav1.GroupKind{{Group: "apiextensions.k8s.io", Kind: "CustomResourceDefinition"}}, opts.GetAllowedClusterResources())
assert.ElementsMatch(t, []metav1.GroupKind{{Group: "rbac.authorization.k8s.io", Kind: "ClusterRole"}}, opts.GetDeniedClusterResources())
}
func TestProjectOpts_GetDestinationServiceAccounts(t *testing.T) {

View File

@@ -45,7 +45,7 @@ func AddRepoFlags(command *cobra.Command, opts *RepoOptions) {
command.Flags().BoolVar(&opts.EnableLfs, "enable-lfs", false, "enable git-lfs (Large File Support) on this repository")
command.Flags().BoolVar(&opts.EnableOci, "enable-oci", false, "enable helm-oci (Helm OCI-Based Repository) (only valid for helm type repositories)")
command.Flags().Int64Var(&opts.GithubAppId, "github-app-id", 0, "id of the GitHub Application")
command.Flags().Int64Var(&opts.GithubAppInstallationId, "github-app-installation-id", 0, "installation id of the GitHub Application (optional, will be auto-discovered if not provided)")
command.Flags().Int64Var(&opts.GithubAppInstallationId, "github-app-installation-id", 0, "installation id of the GitHub Application")
command.Flags().StringVar(&opts.GithubAppPrivateKeyPath, "github-app-private-key-path", "", "private key of the GitHub Application")
command.Flags().StringVar(&opts.GitHubAppEnterpriseBaseURL, "github-app-enterprise-base-url", "", "base url to use when using GitHub Enterprise (e.g. https://ghe.example.com/api/v3")
command.Flags().StringVar(&opts.Proxy, "proxy", "", "use proxy to access repository")

View File

@@ -1,209 +0,0 @@
package commit
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/argoproj/argo-cd/v3/util/git"
)
// TestAddNoteConcurrentStaggered tests that when multiple AddNote operations run
// with slightly staggered timing, all notes persist correctly.
// Each operation gets its own git clone, simulating multiple concurrent hydration requests.
func TestAddNoteConcurrentStaggered(t *testing.T) {
t.Parallel()
remotePath, localPath := setupRepoWithRemote(t)
// Create 3 branches with commits (simulating different hydration targets)
branches := []string{"env/dev", "env/staging", "env/prod"}
commitSHAs := make([]string, 3)
for i, branch := range branches {
commitSHAs[i] = commitAndPushBranch(t, localPath, branch)
}
// Create separate clones for concurrent operations
cloneClients := make([]git.Client, 3)
for i := 0; i < 3; i++ {
cloneClients[i] = getClientForClone(t, remotePath)
}
// Add notes concurrently with slight stagger
var wg sync.WaitGroup
errors := make([]error, 3)
for i := 0; i < 3; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
time.Sleep(time.Duration(idx*50) * time.Millisecond)
errors[idx] = AddNote(cloneClients[idx], fmt.Sprintf("dry-sha-%d", idx), commitSHAs[idx])
}(i)
}
wg.Wait()
// Verify all notes persisted
verifyClient := getClientForClone(t, remotePath)
for i, commitSHA := range commitSHAs {
note, err := verifyClient.GetCommitNote(commitSHA, NoteNamespace)
require.NoError(t, err, "Note should exist for commit %d", i)
assert.Contains(t, note, fmt.Sprintf("dry-sha-%d", i))
}
}
// TestAddNoteConcurrentSimultaneous tests that when multiple AddNote operations run
// simultaneously (without delays), all notes persist correctly.
// Each operation gets its own git clone, simulating multiple concurrent hydration requests.
func TestAddNoteConcurrentSimultaneous(t *testing.T) {
t.Parallel()
remotePath, localPath := setupRepoWithRemote(t)
// Create 3 branches with commits (simulating different hydration targets)
branches := []string{"env/dev", "env/staging", "env/prod"}
commitSHAs := make([]string, 3)
for i, branch := range branches {
commitSHAs[i] = commitAndPushBranch(t, localPath, branch)
}
// Create separate clones for concurrent operations
cloneClients := make([]git.Client, 3)
for i := 0; i < 3; i++ {
cloneClients[i] = getClientForClone(t, remotePath)
}
// Add notes concurrently without delays
var wg sync.WaitGroup
startChan := make(chan struct{})
for i := 0; i < 3; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
<-startChan
_ = AddNote(cloneClients[idx], fmt.Sprintf("dry-sha-%d", idx), commitSHAs[idx])
}(i)
}
close(startChan)
wg.Wait()
// Verify all notes persisted
verifyClient := getClientForClone(t, remotePath)
for i, commitSHA := range commitSHAs {
note, err := verifyClient.GetCommitNote(commitSHA, NoteNamespace)
require.NoError(t, err, "Note should exist for commit %d", i)
assert.Contains(t, note, fmt.Sprintf("dry-sha-%d", i))
}
}
// setupRepoWithRemote creates a bare remote repo and a local repo configured to push to it.
// Returns the remote path and local path.
func setupRepoWithRemote(t *testing.T) (remotePath, localPath string) {
t.Helper()
ctx := t.Context()
// Create bare remote repository
remoteDir := t.TempDir()
remotePath = filepath.Join(remoteDir, "remote.git")
err := os.MkdirAll(remotePath, 0o755)
require.NoError(t, err)
_, err = runGitCmd(ctx, remotePath, "init", "--bare")
require.NoError(t, err)
// Create local repository
localDir := t.TempDir()
localPath = filepath.Join(localDir, "local")
err = os.MkdirAll(localPath, 0o755)
require.NoError(t, err)
_, err = runGitCmd(ctx, localPath, "init")
require.NoError(t, err)
_, err = runGitCmd(ctx, localPath, "config", "user.name", "Test User")
require.NoError(t, err)
_, err = runGitCmd(ctx, localPath, "config", "user.email", "test@example.com")
require.NoError(t, err)
_, err = runGitCmd(ctx, localPath, "remote", "add", "origin", remotePath)
require.NoError(t, err)
return remotePath, localPath
}
// commitAndPushBranch writes a file, commits it, creates a branch, and pushes to remote.
// Returns the commit SHA.
func commitAndPushBranch(t *testing.T, localPath, branch string) string {
t.Helper()
ctx := t.Context()
testFile := filepath.Join(localPath, "test.txt")
err := os.WriteFile(testFile, []byte("content for "+branch), 0o644)
require.NoError(t, err)
_, err = runGitCmd(ctx, localPath, "add", ".")
require.NoError(t, err)
_, err = runGitCmd(ctx, localPath, "commit", "-m", "commit "+branch)
require.NoError(t, err)
sha, err := runGitCmd(ctx, localPath, "rev-parse", "HEAD")
require.NoError(t, err)
_, err = runGitCmd(ctx, localPath, "branch", branch)
require.NoError(t, err)
_, err = runGitCmd(ctx, localPath, "push", "origin", branch)
require.NoError(t, err)
return sha
}
// getClientForClone creates a git client with a fresh clone of the remote repo.
func getClientForClone(t *testing.T, remotePath string) git.Client {
t.Helper()
ctx := t.Context()
workDir := t.TempDir()
client, err := git.NewClientExt(remotePath, workDir, &git.NopCreds{}, false, false, "", "")
require.NoError(t, err)
err = client.Init()
require.NoError(t, err)
_, err = runGitCmd(ctx, workDir, "config", "user.name", "Test User")
require.NoError(t, err)
_, err = runGitCmd(ctx, workDir, "config", "user.email", "test@example.com")
require.NoError(t, err)
err = client.Fetch("", 0)
require.NoError(t, err)
return client
}
// runGitCmd is a helper function to run git commands
func runGitCmd(ctx context.Context, dir string, args ...string) (string, error) {
cmd := exec.CommandContext(ctx, "git", args...)
cmd.Dir = dir
output, err := cmd.CombinedOutput()
return strings.TrimSpace(string(output)), err
}

View File

@@ -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"
@@ -17,11 +19,6 @@ import (
"github.com/argoproj/argo-cd/v3/util/io/files"
)
const (
NoteNamespace = "hydrator.metadata" // NoteNamespace is the custom git notes namespace used by the hydrator to store and retrieve commit-related metadata.
ManifestYaml = "manifest.yaml" // ManifestYaml constant for the manifest yaml
)
// Service is the service that handles commit requests.
type Service struct {
metricsServer *metrics.Server
@@ -50,13 +47,6 @@ type hydratorMetadataFile struct {
References []v1alpha1.RevisionReference `json:"references,omitempty"`
}
// CommitNote represents the structure of the git note associated with a hydrated commit.
// This struct is used to serialize/deserialize commit metadata (such as the dry run SHA)
// stored in the custom note namespace by the hydrator.
type CommitNote struct {
DrySHA string `json:"drySha"` // SHA of original commit that triggerd the hydrator
}
// TODO: make this configurable via ConfigMap.
var manifestHydrationReadmeTemplate = `# Manifest Hydration
@@ -167,45 +157,33 @@ func (s *Service) handleCommitRequest(logCtx *log.Entry, r *apiclient.CommitHydr
return out, "", fmt.Errorf("failed to checkout target branch: %w", err)
}
hydratedSha, err := gitClient.CommitSHA()
if err != nil {
return "", "", fmt.Errorf("failed to get commit SHA: %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)
}
/* git note changes
1. Get the git note
2. If found, short-circuit, log a warn and return
3. If not, get the last manifest from git for every path, compare it with the hydrated manifest
3a. If manifest has no changes, continue.. no need to commit it
3b. Else, hydrate the manifest.
3c. Push the updated note
*/
isHydrated, err := IsHydrated(gitClient, r.DrySha, hydratedSha)
if err != nil {
return "", "", fmt.Errorf("failed to get notes from git %w", err)
}
// short-circuit if already hydrated
if isHydrated {
logCtx.Debugf("this dry sha %s is already hydrated", r.DrySha)
return "", hydratedSha, nil
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")
shouldCommit, err := WriteForPaths(root, r.Repo.Repo, r.DrySha, r.DryCommitMetadata, r.Paths, gitClient)
// When there are no new manifests to commit, err will be nil and success will be false as nothing to commit. Else or every other error err will not be nil
err = WriteForPaths(root, r.Repo.Repo, r.DrySha, r.DryCommitMetadata, r.Paths)
if err != nil {
return "", "", fmt.Errorf("failed to write manifests: %w", err)
}
if !shouldCommit {
// Manifests did not change, so we don't need to create a new commit.
// Add a git note to track that this dry SHA has been processed, and return the existing hydrated SHA.
logCtx.Debug("Adding commit note")
err = AddNote(gitClient, r.DrySha, hydratedSha)
if err != nil {
return "", "", fmt.Errorf("failed to add commit note: %w", err)
}
return "", hydratedSha, nil
}
logCtx.Debug("Committing and pushing changes")
out, err = gitClient.CommitAndPush(r.TargetBranch, r.CommitMessage)
if err != nil {
@@ -217,12 +195,7 @@ func (s *Service) handleCommitRequest(logCtx *log.Entry, r *apiclient.CommitHydr
if err != nil {
return "", "", fmt.Errorf("failed to get commit SHA: %w", err)
}
// add the commit note
logCtx.Debug("Adding commit note")
err = AddNote(gitClient, r.DrySha, sha)
if err != nil {
return "", "", fmt.Errorf("failed to add commit note: %w", err)
}
return "", sha, nil
}

View File

@@ -1,7 +1,6 @@
package commit
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
@@ -100,15 +99,14 @@ func Test_CommitHydratedManifests(t *testing.T) {
mockGitClient.EXPECT().SetAuthor("Argo CD", "argo-cd@example.com").Return("", nil).Once()
mockGitClient.EXPECT().CheckoutOrOrphan("env/test", false).Return("", nil).Once()
mockGitClient.EXPECT().CheckoutOrNew("main", "env/test", false).Return("", nil).Once()
mockGitClient.EXPECT().GetCommitNote(mock.Anything, mock.Anything).Return("", fmt.Errorf("test %w", git.ErrNoNoteFound)).Once()
mockGitClient.EXPECT().AddAndPushNote(mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
mockGitClient.EXPECT().CommitAndPush("main", "test commit message").Return("", nil).Once()
mockGitClient.EXPECT().CommitSHA().Return("it-worked!", nil).Once()
mockRepoClientFactory.EXPECT().NewClient(mock.Anything, mock.Anything).Return(mockGitClient, nil).Once()
resp, err := service.CommitHydratedManifests(t.Context(), validRequest)
require.NoError(t, err)
require.NotNil(t, resp)
assert.Equal(t, "it-worked!", resp.HydratedSha, "Should return existing hydrated SHA for no-op")
assert.Equal(t, "it-worked!", resp.HydratedSha)
})
t.Run("root path with dot and blank - no directory removal", func(t *testing.T) {
@@ -121,11 +119,8 @@ func Test_CommitHydratedManifests(t *testing.T) {
mockGitClient.EXPECT().SetAuthor("Argo CD", "argo-cd@example.com").Return("", nil).Once()
mockGitClient.EXPECT().CheckoutOrOrphan("env/test", false).Return("", nil).Once()
mockGitClient.EXPECT().CheckoutOrNew("main", "env/test", false).Return("", nil).Once()
mockGitClient.EXPECT().GetCommitNote(mock.Anything, mock.Anything).Return("", fmt.Errorf("test %w", git.ErrNoNoteFound)).Once()
mockGitClient.EXPECT().HasFileChanged(mock.Anything).Return(true, nil).Twice()
mockGitClient.EXPECT().CommitAndPush("main", "test commit message").Return("", nil).Once()
mockGitClient.EXPECT().AddAndPushNote(mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
mockGitClient.EXPECT().CommitSHA().Return("root-and-blank-sha", nil).Twice()
mockGitClient.EXPECT().CommitSHA().Return("root-and-blank-sha", nil).Once()
mockRepoClientFactory.EXPECT().NewClient(mock.Anything, mock.Anything).Return(mockGitClient, nil).Once()
requestWithRootAndBlank := &apiclient.CommitHydratedManifestsRequest{
@@ -163,6 +158,7 @@ func Test_CommitHydratedManifests(t *testing.T) {
t.Run("subdirectory path - triggers directory removal", func(t *testing.T) {
t.Parallel()
service, mockRepoClientFactory := newServiceWithMocks(t)
mockGitClient := gitmocks.NewClient(t)
mockGitClient.EXPECT().Init().Return(nil).Once()
@@ -170,20 +166,17 @@ func Test_CommitHydratedManifests(t *testing.T) {
mockGitClient.EXPECT().SetAuthor("Argo CD", "argo-cd@example.com").Return("", nil).Once()
mockGitClient.EXPECT().CheckoutOrOrphan("env/test", false).Return("", nil).Once()
mockGitClient.EXPECT().CheckoutOrNew("main", "env/test", false).Return("", nil).Once()
mockGitClient.EXPECT().GetCommitNote(mock.Anything, mock.Anything).Return("", fmt.Errorf("test %w", git.ErrNoNoteFound)).Once()
mockGitClient.EXPECT().HasFileChanged(mock.Anything).Return(true, nil).Once()
mockGitClient.EXPECT().RemoveContents([]string{"apps/staging"}).Return("", nil).Once()
mockGitClient.EXPECT().CommitAndPush("main", "test commit message").Return("", nil).Once()
mockGitClient.EXPECT().AddAndPushNote(mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
mockGitClient.EXPECT().CommitSHA().Return("subdir-path-sha", nil).Twice()
mockGitClient.EXPECT().CommitSHA().Return("subdir-path-sha", nil).Once()
mockRepoClientFactory.EXPECT().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",
TargetBranch: "main",
SyncBranch: "env/test",
CommitMessage: "test commit message",
Paths: []*apiclient.PathDetails{
{
@@ -213,11 +206,9 @@ func Test_CommitHydratedManifests(t *testing.T) {
mockGitClient.EXPECT().SetAuthor("Argo CD", "argo-cd@example.com").Return("", nil).Once()
mockGitClient.EXPECT().CheckoutOrOrphan("env/test", false).Return("", nil).Once()
mockGitClient.EXPECT().CheckoutOrNew("main", "env/test", false).Return("", nil).Once()
mockGitClient.EXPECT().GetCommitNote(mock.Anything, mock.Anything).Return("", fmt.Errorf("test %w", git.ErrNoNoteFound)).Once()
mockGitClient.EXPECT().HasFileChanged(mock.Anything).Return(true, nil).Times(3)
mockGitClient.EXPECT().RemoveContents([]string{"apps/production", "apps/staging"}).Return("", nil).Once()
mockGitClient.EXPECT().CommitAndPush("main", "test commit message").Return("", nil).Once()
mockGitClient.EXPECT().AddAndPushNote(mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
mockGitClient.EXPECT().CommitSHA().Return("mixed-paths-sha", nil).Twice()
mockGitClient.EXPECT().CommitSHA().Return("mixed-paths-sha", nil).Once()
mockRepoClientFactory.EXPECT().NewClient(mock.Anything, mock.Anything).Return(mockGitClient, nil).Once()
requestWithMixedPaths := &apiclient.CommitHydratedManifestsRequest{
@@ -271,9 +262,8 @@ func Test_CommitHydratedManifests(t *testing.T) {
mockGitClient.EXPECT().SetAuthor("Argo CD", "argo-cd@example.com").Return("", nil).Once()
mockGitClient.EXPECT().CheckoutOrOrphan("env/test", false).Return("", nil).Once()
mockGitClient.EXPECT().CheckoutOrNew("main", "env/test", false).Return("", nil).Once()
mockGitClient.EXPECT().GetCommitNote(mock.Anything, mock.Anything).Return("", fmt.Errorf("test %w", git.ErrNoNoteFound)).Once()
mockGitClient.EXPECT().AddAndPushNote(mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
mockGitClient.EXPECT().CommitSHA().Return("empty-paths-sha", nil).Once()
mockGitClient.EXPECT().CommitAndPush("main", "test commit message").Return("", nil).Once()
mockGitClient.EXPECT().CommitSHA().Return("it-worked!", nil).Once()
mockRepoClientFactory.EXPECT().NewClient(mock.Anything, mock.Anything).Return(mockGitClient, nil).Once()
requestWithEmptyPaths := &apiclient.CommitHydratedManifestsRequest{
@@ -283,97 +273,12 @@ func Test_CommitHydratedManifests(t *testing.T) {
TargetBranch: "main",
SyncBranch: "env/test",
CommitMessage: "test commit message",
DrySha: "dry-sha-456",
}
resp, err := service.CommitHydratedManifests(t.Context(), requestWithEmptyPaths)
require.NoError(t, err)
require.NotNil(t, resp)
assert.Equal(t, "empty-paths-sha", resp.HydratedSha, "Should return existing hydrated SHA for no-op")
})
t.Run("duplicate request already hydrated", func(t *testing.T) {
t.Parallel()
strnote := "{\"drySha\":\"abc123\"}"
service, mockRepoClientFactory := newServiceWithMocks(t)
mockGitClient := gitmocks.NewClient(t)
mockGitClient.EXPECT().Init().Return(nil).Once()
mockGitClient.EXPECT().Fetch(mock.Anything, mock.Anything).Return(nil).Once()
mockGitClient.EXPECT().SetAuthor("Argo CD", "argo-cd@example.com").Return("", nil).Once()
mockGitClient.EXPECT().CheckoutOrOrphan("env/test", false).Return("", nil).Once()
mockGitClient.EXPECT().CheckoutOrNew("main", "env/test", false).Return("", nil).Once()
mockGitClient.EXPECT().GetCommitNote(mock.Anything, mock.Anything).Return(strnote, nil).Once()
mockGitClient.EXPECT().CommitSHA().Return("dupe-test-sha", nil).Once()
mockRepoClientFactory.EXPECT().NewClient(mock.Anything, mock.Anything).Return(mockGitClient, nil).Once()
request := &apiclient.CommitHydratedManifestsRequest{
Repo: &v1alpha1.Repository{
Repo: "https://github.com/argoproj/argocd-example-apps.git",
},
TargetBranch: "main",
SyncBranch: "env/test",
DrySha: "abc123",
CommitMessage: "test commit message",
Paths: []*apiclient.PathDetails{
{
Path: ".",
Manifests: []*apiclient.HydratedManifestDetails{
{
ManifestJSON: `{"apiVersion":"v1","kind":"Deployment","metadata":{"name":"test-app"}}`,
},
},
},
},
}
resp, err := service.CommitHydratedManifests(t.Context(), request)
require.NoError(t, err)
require.NotNil(t, resp)
assert.Equal(t, "dupe-test-sha", resp.HydratedSha, "Should return existing hydrated SHA when already hydrated")
})
t.Run("root path with dot - no changes to manifest - should commit note only", func(t *testing.T) {
t.Parallel()
service, mockRepoClientFactory := newServiceWithMocks(t)
mockGitClient := gitmocks.NewClient(t)
mockGitClient.EXPECT().Init().Return(nil).Once()
mockGitClient.EXPECT().Fetch(mock.Anything, mock.Anything).Return(nil).Once()
mockGitClient.EXPECT().SetAuthor("Argo CD", "argo-cd@example.com").Return("", nil).Once()
mockGitClient.EXPECT().CheckoutOrOrphan("env/test", false).Return("", nil).Once()
mockGitClient.EXPECT().CheckoutOrNew("main", "env/test", false).Return("", nil).Once()
mockGitClient.EXPECT().GetCommitNote(mock.Anything, mock.Anything).Return("", fmt.Errorf("test %w", git.ErrNoNoteFound)).Once()
mockGitClient.EXPECT().HasFileChanged(mock.Anything).Return(false, nil).Once()
mockGitClient.EXPECT().AddAndPushNote(mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
mockGitClient.EXPECT().CommitSHA().Return("root-and-blank-sha", nil).Once()
mockRepoClientFactory.EXPECT().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",
DrySha: "dry-sha-123",
Paths: []*apiclient.PathDetails{
{
Path: ".",
Manifests: []*apiclient.HydratedManifestDetails{
{
ManifestJSON: `{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"test-dot"}}`,
},
},
},
},
}
resp, err := service.CommitHydratedManifests(t.Context(), requestWithRootAndBlank)
require.NoError(t, err)
require.NotNil(t, resp)
// BUG FIX: When manifests don't change (no-op), the existing hydrated SHA should be returned.
assert.Equal(t, "root-and-blank-sha", resp.HydratedSha, "Should return existing hydrated SHA for no-op")
assert.Equal(t, "it-worked!", resp.HydratedSha)
})
}

View File

@@ -13,7 +13,7 @@ func getCredentialType(repo *v1alpha1.Repository) string {
if repo.SSHPrivateKey != "" {
return "ssh"
}
if repo.GithubAppPrivateKey != "" && repo.GithubAppId != 0 { // Promoter MVP: remove github-app-installation-id check since it is no longer a required field
if repo.GithubAppPrivateKey != "" && repo.GithubAppId != 0 && repo.GithubAppInstallationId != 0 {
return "github-app"
}
if repo.GCPServiceAccountKey != "" {

View File

@@ -2,7 +2,6 @@ package commit
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
@@ -16,15 +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.
@@ -35,24 +33,24 @@ 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, gitClient git.Client) (bool, error) { //nolint:revive //FIXME(var-naming)
func WriteForPaths(root *os.Root, repoUrl, drySha string, dryCommitMetadata *appv1.RevisionMetadata, paths []*apiclient.PathDetails) error { //nolint:revive //FIXME(var-naming)
hydratorMetadata, err := hydrator.GetCommitMetadata(repoUrl, drySha, dryCommitMetadata)
if err != nil {
return false, fmt.Errorf("failed to retrieve hydrator metadata: %w", err)
return fmt.Errorf("failed to retrieve hydrator metadata: %w", err)
}
// Write the top-level readme.
err = writeMetadata(root, "", hydratorMetadata)
if err != nil {
return false, fmt.Errorf("failed to write top-level hydrator metadata: %w", err)
return fmt.Errorf("failed to write top-level hydrator metadata: %w", err)
}
// Write .gitattributes
err = writeGitAttributes(root)
if err != nil {
return false, fmt.Errorf("failed to write git attributes: %w", err)
return fmt.Errorf("failed to write git attributes: %w", err)
}
var atleastOneManifestChanged bool
for _, p := range paths {
hydratePath := p.Path
if hydratePath == "." {
@@ -63,26 +61,15 @@ func WriteForPaths(root *os.Root, repoUrl, drySha string, dryCommitMetadata *app
if hydratePath != "" {
err = root.MkdirAll(hydratePath, 0o755)
if err != nil {
return false, fmt.Errorf("failed to create path: %w", err)
return fmt.Errorf("failed to create path: %w", err)
}
}
// Write the manifests
err := writeManifests(root, hydratePath, p.Manifests)
err = writeManifests(root, hydratePath, p.Manifests)
if err != nil {
return false, fmt.Errorf("failed to write manifests: %w", err)
return fmt.Errorf("failed to write manifests: %w", err)
}
// Check if the manifest file has been modified compared to the git index
changed, err := gitClient.HasFileChanged(filepath.Join(hydratePath, ManifestYaml))
if err != nil {
return false, fmt.Errorf("failed to check if anything changed on the manifest: %w", err)
}
if !changed {
continue
}
// If any manifest has changed, signal that a commit should occur. If none have changed, skip committing.
atleastOneManifestChanged = changed
// Write hydrator.metadata containing information about the hydration process.
hydratorMetadata := hydrator.HydratorCommitMetadata{
@@ -92,20 +79,16 @@ func WriteForPaths(root *os.Root, repoUrl, drySha string, dryCommitMetadata *app
}
err = writeMetadata(root, hydratePath, hydratorMetadata)
if err != nil {
return false, fmt.Errorf("failed to write hydrator metadata: %w", err)
return fmt.Errorf("failed to write hydrator metadata: %w", err)
}
// Write README
err = writeReadme(root, hydratePath, hydratorMetadata)
if err != nil {
return false, fmt.Errorf("failed to write readme: %w", err)
return fmt.Errorf("failed to write readme: %w", err)
}
}
// if no manifest changes then skip commit
if !atleastOneManifestChanged {
return false, nil
}
return atleastOneManifestChanged, nil
return nil
}
// writeMetadata writes the metadata to the hydrator.metadata file.
@@ -180,7 +163,7 @@ func writeGitAttributes(root *os.Root) error {
func writeManifests(root *os.Root, dirPath string, manifests []*apiclient.HydratedManifestDetails) error {
// If the file exists, truncate it.
// No need to use SecureJoin here, as the path is already sanitized.
manifestPath := filepath.Join(dirPath, ManifestYaml)
manifestPath := filepath.Join(dirPath, "manifest.yaml")
file, err := root.OpenFile(manifestPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm)
if err != nil {
@@ -213,39 +196,6 @@ func writeManifests(root *os.Root, dirPath string, manifests []*apiclient.Hydrat
return fmt.Errorf("failed to encode manifest: %w", err)
}
}
return nil
}
// IsHydrated checks whether the given commit (commitSha) has already been hydrated with the specified Dry SHA (drySha).
// It does this by retrieving the commit note in the NoteNamespace and examining the DrySHA value.
// Returns true if the stored DrySHA matches the provided drySha, false if not or if no note exists.
// Gracefully handles missing notes as a normal outcome (not an error), but returns an error on retrieval or parse failures.
func IsHydrated(gitClient git.Client, drySha, commitSha string) (bool, error) {
note, err := gitClient.GetCommitNote(commitSha, NoteNamespace)
if err != nil {
// note not found is a valid and acceptable outcome in this context so returning false and nil to let the hydration continue
unwrappedError := errors.Unwrap(err)
if unwrappedError != nil && errors.Is(unwrappedError, git.ErrNoNoteFound) {
return false, nil
}
return false, err
}
var commitNote CommitNote
err = json.Unmarshal([]byte(note), &commitNote)
if err != nil {
return false, fmt.Errorf("json unmarshal failed %w", err)
}
return commitNote.DrySHA == drySha, nil
}
// AddNote attaches a commit note containing the specified dry SHA (`drySha`) to the given commit (`commitSha`)
// in the configured note namespace. The note is marshaled as JSON and pushed to the remote repository using
// the provided gitClient. Returns an error if marshalling or note addition fails.
func AddNote(gitClient git.Client, drySha, commitSha string) error {
note := CommitNote{DrySHA: drySha}
jsonBytes, err := json.Marshal(note)
if err != nil {
return fmt.Errorf("failed to marshal commit note: %w", err)
}
return gitClient.AddAndPushNote(commitSha, NoteNamespace, string(jsonBytes)) // nolint:wrapcheck // wrapping the error wouldn't add any information
}

View File

@@ -5,25 +5,19 @@ import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"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/git"
gitmocks "github.com/argoproj/argo-cd/v3/util/git/mocks"
"github.com/argoproj/argo-cd/v3/util/hydrator"
)
@@ -98,12 +92,9 @@ Argocd-reference-commit-sha: abc123
},
},
}
mockGitClient := gitmocks.NewClient(t)
mockGitClient.On("HasFileChanged", mock.Anything).Return(true, nil).Times(len(paths))
shouldCommit, err := WriteForPaths(root, repoURL, drySha, metadata, paths, mockGitClient)
err := WriteForPaths(root, repoURL, drySha, metadata, paths)
require.NoError(t, err)
require.True(t, shouldCommit)
// Check if the top-level hydrator.metadata exists and contains the repo URL and dry SHA
topMetadataPath := filepath.Join(root.Name(), "hydrator.metadata")
@@ -151,117 +142,6 @@ Argocd-reference-commit-sha: abc123
}
}
func TestWriteForPaths_WithOneManifestMatchesExisting(t *testing.T) {
root := tempRoot(t)
repoURL := "https://github.com/example/repo"
drySha := "abc123"
paths := []*apiclient.PathDetails{
{
Path: "path1",
Manifests: []*apiclient.HydratedManifestDetails{
{ManifestJSON: `{"kind":"Pod","apiVersion":"v1"}`},
},
Commands: []string{"command1", "command2"},
},
{
Path: "path2",
Manifests: []*apiclient.HydratedManifestDetails{
{ManifestJSON: `{"kind":"Service","apiVersion":"v1"}`},
},
Commands: []string{"command3"},
},
{
Path: "path3/nested",
Manifests: []*apiclient.HydratedManifestDetails{
{ManifestJSON: `{"kind":"Deployment","apiVersion":"apps/v1"}`},
},
Commands: []string{"command4"},
},
}
now := metav1.NewTime(time.Now())
metadata := &appsv1.RevisionMetadata{
Author: "test-author",
Date: &now,
Message: `test-message
Signed-off-by: Test User <test@example.com>
Argocd-reference-commit-sha: abc123
`,
References: []appsv1.RevisionReference{
{
Commit: &appsv1.CommitMetadata{
Author: "test-code-author <test-email-author@example.com>",
Date: now.Format(time.RFC3339),
Subject: "test-code-subject",
SHA: "test-code-sha",
RepoURL: "https://example.com/test/repo.git",
},
},
},
}
mockGitClient := gitmocks.NewClient(t)
mockGitClient.On("HasFileChanged", "path1/manifest.yaml").Return(true, nil).Once()
mockGitClient.On("HasFileChanged", "path2/manifest.yaml").Return(true, nil).Once()
mockGitClient.On("HasFileChanged", "path3/nested/manifest.yaml").Return(false, nil).Once()
shouldCommit, err := WriteForPaths(root, repoURL, drySha, metadata, paths, mockGitClient)
require.NoError(t, err)
require.True(t, shouldCommit)
// Check if the top-level hydrator.metadata exists and contains the repo URL and dry SHA
topMetadataPath := filepath.Join(root.Name(), "hydrator.metadata")
topMetadataBytes, err := os.ReadFile(topMetadataPath)
require.NoError(t, err)
var topMetadata hydratorMetadataFile
err = json.Unmarshal(topMetadataBytes, &topMetadata)
require.NoError(t, err)
assert.Equal(t, repoURL, topMetadata.RepoURL)
assert.Equal(t, drySha, topMetadata.DrySHA)
assert.Equal(t, metadata.Author, topMetadata.Author)
assert.Equal(t, "test-message", topMetadata.Subject)
// The body should exclude the Argocd- trailers.
assert.Equal(t, "Signed-off-by: Test User <test@example.com>\n", topMetadata.Body)
assert.Equal(t, metadata.Date.Format(time.RFC3339), topMetadata.Date)
assert.Equal(t, metadata.References, topMetadata.References)
for _, p := range paths {
fullHydratePath := filepath.Join(root.Name(), p.Path)
if p.Path == "path3/nested" {
assert.DirExists(t, fullHydratePath)
manifestPath := path.Join(fullHydratePath, "manifest.yaml")
_, err := os.ReadFile(manifestPath)
require.NoError(t, err)
continue
}
// Check if each path directory exists
assert.DirExists(t, fullHydratePath)
// Check if each path contains a hydrator.metadata file and contains the repo URL
metadataPath := path.Join(fullHydratePath, "hydrator.metadata")
metadataBytes, err := os.ReadFile(metadataPath)
require.NoError(t, err)
var readMetadata hydratorMetadataFile
err = json.Unmarshal(metadataBytes, &readMetadata)
require.NoError(t, err)
assert.Equal(t, repoURL, readMetadata.RepoURL)
// Check if each path contains a README.md file and contains the repo URL
readmePath := path.Join(fullHydratePath, "README.md")
readmeBytes, err := os.ReadFile(readmePath)
require.NoError(t, err)
assert.Contains(t, string(readmeBytes), repoURL)
// Check if each path contains a manifest.yaml file and contains the word kind
manifestPath := path.Join(fullHydratePath, "manifest.yaml")
manifestBytes, err := os.ReadFile(manifestPath)
require.NoError(t, err)
assert.Contains(t, string(manifestBytes), "kind")
}
}
func TestWriteMetadata(t *testing.T) {
root := tempRoot(t)
@@ -354,186 +234,6 @@ 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")
}
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)
}
})
}
}
func TestIsHydrated(t *testing.T) {
mockGitClient := gitmocks.NewClient(t)
drySha := "abc123"
commitSha := "fff456"
commitShaNoNoteFoundErr := "abc456"
commitShaErr := "abc999"
strnote := "{\"drySha\":\"abc123\"}"
mockGitClient.On("GetCommitNote", commitSha, mock.Anything).Return(strnote, nil).Once()
mockGitClient.On("GetCommitNote", commitShaNoNoteFoundErr, mock.Anything).Return("", fmt.Errorf("wrapped error %w", git.ErrNoNoteFound)).Once()
// an existing note
isHydrated, err := IsHydrated(mockGitClient, drySha, commitSha)
require.NoError(t, err)
assert.True(t, isHydrated)
// no note found treated as success.. no error returned
isHydrated, err = IsHydrated(mockGitClient, drySha, commitShaNoNoteFoundErr)
require.NoError(t, err)
assert.False(t, isHydrated)
// Test that non-ErrNoNoteFound errors are propagated: when GetCommitNote fails with
// an error other than "no note found", IsHydrated should return that error to the caller
err = errors.New("some other error")
mockGitClient.On("GetCommitNote", commitShaErr, mock.Anything).Return("", fmt.Errorf("wrapped error %w", err)).Once()
isHydrated, err = IsHydrated(mockGitClient, drySha, commitShaErr)
require.Error(t, err)
assert.False(t, isHydrated)
}
func TestAddNote(t *testing.T) {
mockGitClient := gitmocks.NewClient(t)
drySha := "abc123"
commitSha := "fff456"
commitShaErr := "abc456"
err := errors.New("test error")
mockGitClient.On("AddAndPushNote", commitSha, mock.Anything, mock.Anything).Return(nil).Once()
mockGitClient.On("AddAndPushNote", commitShaErr, mock.Anything, mock.Anything).Return(err).Once()
// success
err = AddNote(mockGitClient, drySha, commitSha)
require.NoError(t, err)
// failure
err = AddNote(mockGitClient, drySha, commitShaErr)
require.Error(t, err)
}
// TestWriteForPaths_NoOpScenario tests that when manifests don't change between two hydrations,
// shouldCommit returns false. This reproduces the bug where a new DRY commit that doesn't affect
// manifests should not create a new hydrated commit.
func TestWriteForPaths_NoOpScenario(t *testing.T) {
root := tempRoot(t)
repoURL := "https://github.com/example/repo"
drySha1 := "abc123"
drySha2 := "def456" // Different dry SHA
paths := []*apiclient.PathDetails{
{
Path: "guestbook",
Manifests: []*apiclient.HydratedManifestDetails{
{ManifestJSON: `{"apiVersion":"v1","kind":"Service","metadata":{"name":"guestbook-ui"}}`},
{ManifestJSON: `{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"guestbook-ui"}}`},
},
Commands: []string{"kustomize build ."},
},
}
now1 := metav1.NewTime(time.Now())
metadata1 := &appsv1.RevisionMetadata{
Author: "test-author",
Date: &now1,
Message: "Initial commit",
}
// First hydration - manifests are new, so HasFileChanged should return true
mockGitClient1 := gitmocks.NewClient(t)
mockGitClient1.On("HasFileChanged", "guestbook/manifest.yaml").Return(true, nil).Once()
shouldCommit1, err := WriteForPaths(root, repoURL, drySha1, metadata1, paths, mockGitClient1)
require.NoError(t, err)
require.True(t, shouldCommit1, "First hydration should commit because manifests are new")
// Second hydration - same manifest content but different dry SHA and metadata
// Simulate adding a README.md to the dry source (which doesn't affect manifests)
now2 := metav1.NewTime(time.Now().Add(1 * time.Hour)) // Different timestamp
metadata2 := &appsv1.RevisionMetadata{
Author: "test-author",
Date: &now2,
Message: "Add README.md", // Different commit message
}
// The manifests are identical, so HasFileChanged should return false
mockGitClient2 := gitmocks.NewClient(t)
mockGitClient2.On("HasFileChanged", "guestbook/manifest.yaml").Return(false, nil).Once()
shouldCommit2, err := WriteForPaths(root, repoURL, drySha2, metadata2, paths, mockGitClient2)
require.NoError(t, err)
require.False(t, shouldCommit2, "Second hydration should NOT commit because manifests didn't change")
// Verify that the root-level metadata WAS updated (even though we're not committing)
// The files get written to the working directory, but since shouldCommit is false, they won't be committed
topMetadataPath := filepath.Join(root.Name(), "hydrator.metadata")
topMetadataBytes, err := os.ReadFile(topMetadataPath)
require.NoError(t, err)
var topMetadata hydratorMetadataFile
err = json.Unmarshal(topMetadataBytes, &topMetadata)
require.NoError(t, err)
// The top-level metadata should have the NEW dry SHA (files are written, just not committed)
assert.Equal(t, drySha2, topMetadata.DrySHA)
assert.Equal(t, metadata2.Date.Format(time.RFC3339), topMetadata.Date)
assert.Contains(t, string(gitAttributesBytes), "*/README.md linguist-generated=true")
assert.Contains(t, string(gitAttributesBytes), "*/hydrator.metadata linguist-generated=true")
}

View File

@@ -225,10 +225,6 @@ const (
// Ex: "http://grafana.example.com/d/yu5UH4MMz/deployments"
// Ex: "Go to Dashboard|http://grafana.example.com/d/yu5UH4MMz/deployments"
AnnotationKeyLinkPrefix = "link.argocd.argoproj.io/"
// AnnotationKeyIgnoreDefaultLinks tells the Application to not add autogenerated links from this object into its externalURLs
// This applies to ingress objects and takes effect if set to "true"
// This only disables the default behavior of generating links based on the ingress spec, and does not disable AnnotationKeyLinkPrefix
AnnotationKeyIgnoreDefaultLinks = "argocd.argoproj.io/ignore-default-links"
// AnnotationKeyAppSkipReconcile tells the Application to skip the Application controller reconcile.
// Skip reconcile when the value is "true" or any other string values that can be strconv.ParseBool() to be true.

View File

@@ -128,6 +128,7 @@ type ApplicationController struct {
statusRefreshJitter time.Duration
selfHealTimeout time.Duration
selfHealBackoff *wait.Backoff
selfHealBackoffCooldown time.Duration
syncTimeout time.Duration
db db.ArgoDB
settingsMgr *settings_util.SettingsManager
@@ -163,6 +164,7 @@ func NewApplicationController(
appResyncJitter time.Duration,
selfHealTimeout time.Duration,
selfHealBackoff *wait.Backoff,
selfHealBackoffCooldown time.Duration,
syncTimeout time.Duration,
repoErrorGracePeriod time.Duration,
metricsPort int,
@@ -209,6 +211,7 @@ func NewApplicationController(
settingsMgr: settingsMgr,
selfHealTimeout: selfHealTimeout,
selfHealBackoff: selfHealBackoff,
selfHealBackoffCooldown: selfHealBackoffCooldown,
syncTimeout: syncTimeout,
clusterSharding: clusterSharding,
projByNameCache: sync.Map{},
@@ -609,10 +612,8 @@ func (ctrl *ApplicationController) getResourceTree(destCluster *appv1.Cluster, a
managedResourcesKeys = append(managedResourcesKeys, kube.GetResourceKey(live))
}
}
// Process managed resources and their children, including cross-namespace relationships
// from cluster-scoped parents (e.g., Crossplane CompositeResourceDefinitions)
err = ctrl.stateCache.IterateHierarchyV2(destCluster, managedResourcesKeys, func(child appv1.ResourceNode, _ string) bool {
permitted, _ := proj.IsResourcePermitted(schema.GroupKind{Group: child.Group, Kind: child.Kind}, child.Name, child.Namespace, destCluster, func(project string) ([]*appv1.Cluster, error) {
permitted, _ := proj.IsResourcePermitted(schema.GroupKind{Group: child.Group, Kind: child.Kind}, child.Namespace, destCluster, func(project string) ([]*appv1.Cluster, error) {
clusters, err := ctrl.db.GetProjectClusters(context.TODO(), project)
if err != nil {
return nil, fmt.Errorf("failed to get project clusters: %w", err)
@@ -632,11 +633,10 @@ func (ctrl *ApplicationController) getResourceTree(destCluster *appv1.Cluster, a
orphanedNodes := make([]appv1.ResourceNode, 0)
orphanedNodesKeys := make([]kube.ResourceKey, 0)
for k := range orphanedNodesMap {
if k.Namespace != "" && proj.IsGroupKindNamePermitted(k.GroupKind(), k.Name, true) && !isKnownOrphanedResourceExclusion(k, proj) {
if k.Namespace != "" && proj.IsGroupKindPermitted(k.GroupKind(), true) && !isKnownOrphanedResourceExclusion(k, proj) {
orphanedNodesKeys = append(orphanedNodesKeys, k)
}
}
// Process orphaned resources
err = ctrl.stateCache.IterateHierarchyV2(destCluster, orphanedNodesKeys, func(child appv1.ResourceNode, appName string) bool {
belongToAnotherApp := false
if appName != "" {
@@ -650,7 +650,7 @@ func (ctrl *ApplicationController) getResourceTree(destCluster *appv1.Cluster, a
return false
}
permitted, _ := proj.IsResourcePermitted(schema.GroupKind{Group: child.Group, Kind: child.Kind}, child.Name, child.Namespace, destCluster, func(project string) ([]*appv1.Cluster, error) {
permitted, _ := proj.IsResourcePermitted(schema.GroupKind{Group: child.Group, Kind: child.Kind}, child.Namespace, destCluster, func(project string) ([]*appv1.Cluster, error) {
return ctrl.db.GetProjectClusters(context.TODO(), project)
})
@@ -1062,9 +1062,6 @@ func (ctrl *ApplicationController) processAppOperationQueueItem() (processNext b
})
message := fmt.Sprintf("Unable to delete application resources: %v", err.Error())
ctrl.logAppEvent(context.TODO(), app, argo.EventInfo{Reason: argo.EventReasonStatusRefreshed, Type: corev1.EventTypeWarning}, message)
} else {
// Clear DeletionError condition if deletion is progressing successfully
app.Status.SetConditions([]appv1.ApplicationCondition{}, map[appv1.ApplicationConditionType]bool{appv1.ApplicationConditionDeletionError: true})
}
ts.AddCheckpoint("finalize_application_deletion_ms")
}
@@ -1206,21 +1203,17 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic
if err != nil {
return err
}
// Get destination cluster
destCluster, err := argo.GetDestinationCluster(context.Background(), app.Spec.Destination, ctrl.db)
if err != nil {
logCtx.WithError(err).Warn("Unable to get destination cluster")
app.UnSetCascadedDeletion()
app.UnSetPostDeleteFinalizerAll()
app.UnSetPreDeleteFinalizerAll()
if err := ctrl.updateFinalizers(app); err != nil {
return err
}
logCtx.Infof("Resource entries removed from undefined cluster")
return nil
}
clusterRESTConfig, err := destCluster.RESTConfig()
if err != nil {
return err
@@ -1232,30 +1225,9 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic
return fmt.Errorf("cannot apply impersonation: %w", err)
}
// Handle PreDelete hooks - run them before any deletion occurs
if app.HasPreDeleteFinalizer() {
objsMap, err := ctrl.getPermittedAppLiveObjects(destCluster, app, proj, projectClusters)
if err != nil {
return fmt.Errorf("error getting permitted app live objects: %w", err)
}
done, err := ctrl.executePreDeleteHooks(app, proj, objsMap, config, logCtx)
if err != nil {
return fmt.Errorf("error executing pre-delete hooks: %w", err)
}
if !done {
// PreDelete hooks are still running - wait for them to complete
return nil
}
// PreDelete hooks are done - remove the finalizer so we can continue with deletion
app.UnSetPreDeleteFinalizer()
if err := ctrl.updateFinalizers(app); err != nil {
return fmt.Errorf("error updating pre-delete finalizers: %w", err)
}
}
if app.CascadedDeletion() {
deletionApproved := app.IsDeletionConfirmed(app.DeletionTimestamp.Time)
logCtx.Infof("Deleting resources")
// ApplicationDestination points to a valid cluster, so we may clean up the live objects
objs := make([]*unstructured.Unstructured, 0)
@@ -1332,23 +1304,6 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic
return ctrl.updateFinalizers(app)
}
if app.HasPreDeleteFinalizer("cleanup") {
objsMap, err := ctrl.getPermittedAppLiveObjects(destCluster, app, proj, projectClusters)
if err != nil {
return fmt.Errorf("error getting permitted app live objects for pre-delete cleanup: %w", err)
}
done, err := ctrl.cleanupPreDeleteHooks(objsMap, config, logCtx)
if err != nil {
return fmt.Errorf("error cleaning up pre-delete hooks: %w", err)
}
if !done {
return nil
}
app.UnSetPreDeleteFinalizer("cleanup")
return ctrl.updateFinalizers(app)
}
if app.HasPostDeleteFinalizer("cleanup") {
objsMap, err := ctrl.getPermittedAppLiveObjects(destCluster, app, proj, projectClusters)
if err != nil {
@@ -1366,7 +1321,7 @@ func (ctrl *ApplicationController) finalizeApplicationDeletion(app *appv1.Applic
return ctrl.updateFinalizers(app)
}
if !app.CascadedDeletion() && !app.HasPostDeleteFinalizer() && !app.HasPreDeleteFinalizer() {
if !app.CascadedDeletion() && !app.HasPostDeleteFinalizer() {
if err := ctrl.cache.SetAppManagedResources(app.Name, nil); err != nil {
return err
}
@@ -1880,25 +1835,10 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
app.Status.SourceTypes = compareResult.appSourceTypes
app.Status.ControllerNamespace = ctrl.namespace
ts.AddCheckpoint("app_status_update_ms")
// Update finalizers BEFORE persisting status to avoid race condition where app shows "Synced"
// but doesn't have finalizers yet, which would allow deletion without running pre-delete hooks
if (compareResult.hasPreDeleteHooks != app.HasPreDeleteFinalizer() ||
compareResult.hasPreDeleteHooks != app.HasPreDeleteFinalizer("cleanup")) &&
app.GetDeletionTimestamp() == nil {
if compareResult.hasPreDeleteHooks {
app.SetPreDeleteFinalizer()
app.SetPreDeleteFinalizer("cleanup")
} else {
app.UnSetPreDeleteFinalizer()
app.UnSetPreDeleteFinalizer("cleanup")
}
if err := ctrl.updateFinalizers(app); err != nil {
logCtx.Errorf("Failed to update pre-delete finalizers: %v", err)
}
}
if (compareResult.hasPostDeleteHooks != app.HasPostDeleteFinalizer() ||
compareResult.hasPostDeleteHooks != app.HasPostDeleteFinalizer("cleanup")) &&
patchDuration = ctrl.persistAppStatus(origApp, &app.Status)
// This is a partly a duplicate of patch_ms, but more descriptive and allows to have measurement for the next step.
ts.AddCheckpoint("persist_app_status_ms")
if (compareResult.hasPostDeleteHooks != app.HasPostDeleteFinalizer() || compareResult.hasPostDeleteHooks != app.HasPostDeleteFinalizer("cleanup")) &&
app.GetDeletionTimestamp() == nil {
if compareResult.hasPostDeleteHooks {
app.SetPostDeleteFinalizer()
@@ -1909,13 +1849,10 @@ func (ctrl *ApplicationController) processAppRefreshQueueItem() (processNext boo
}
if err := ctrl.updateFinalizers(app); err != nil {
logCtx.WithError(err).Error("Failed to update post-delete finalizers")
logCtx.WithError(err).Error("Failed to update finalizers")
}
}
ts.AddCheckpoint("process_finalizers_ms")
patchDuration = ctrl.persistAppStatus(origApp, &app.Status)
// This is a partly a duplicate of patch_ms, but more descriptive and allows to have measurement for the next step.
ts.AddCheckpoint("persist_app_status_ms")
return processNext
}
@@ -2249,8 +2186,12 @@ func (ctrl *ApplicationController) autoSync(app *appv1.Application, syncStatus *
// Self heal will trigger a new sync operation when the desired state changes and cause the application to
// be OutOfSync when it was previously synced Successfully. This means SelfHeal should only ever be attempted
// when the revisions have not changed, and where the previous sync to these revision was successful
if app.Status.OperationState != nil && app.Status.OperationState.Operation.Sync != nil {
op.Sync.SelfHealAttemptsCount = app.Status.OperationState.Operation.Sync.SelfHealAttemptsCount
// Only carry SelfHealAttemptsCount to be increased when the selfHealBackoffCooldown has not elapsed yet
if !ctrl.selfHealBackoffCooldownElapsed(app) {
if app.Status.OperationState != nil && app.Status.OperationState.Operation.Sync != nil {
op.Sync.SelfHealAttemptsCount = app.Status.OperationState.Operation.Sync.SelfHealAttemptsCount
}
}
if remainingTime := ctrl.selfHealRemainingBackoff(app, int(op.Sync.SelfHealAttemptsCount)); remainingTime > 0 {
@@ -2386,6 +2327,19 @@ func (ctrl *ApplicationController) selfHealRemainingBackoff(app *appv1.Applicati
return retryAfter
}
// selfHealBackoffCooldownElapsed returns true when the last successful sync has occurred since longer
// than then self heal cooldown. This means that the application has been in sync for long enough to
// reset the self healing backoff to its initial state
func (ctrl *ApplicationController) selfHealBackoffCooldownElapsed(app *appv1.Application) bool {
if app.Status.OperationState == nil || app.Status.OperationState.FinishedAt == nil {
// Something is in progress, or about to be. In that case, selfHeal attempt should be zero anyway
return true
}
timeSinceLastOperation := time.Since(app.Status.OperationState.FinishedAt.Time)
return timeSinceLastOperation >= ctrl.selfHealBackoffCooldown && app.Status.OperationState.Phase.Successful()
}
// isAppNamespaceAllowed returns whether the application is allowed in the
// namespace it's residing in.
func (ctrl *ApplicationController) isAppNamespaceAllowed(app *appv1.Application) bool {

View File

@@ -159,8 +159,6 @@ func newFakeControllerWithResync(ctx context.Context, data *fakeData, appResyncP
runtimeObjs = append(runtimeObjs, data.additionalObjs...)
kubeClient := fake.NewClientset(runtimeObjs...)
settingsMgr := settings.NewSettingsManager(ctx, kubeClient, test.FakeArgoCDNamespace)
// Initialize the settings manager to ensure cluster cache is ready
_ = settingsMgr.ResyncInformers()
kubectl := &MockKubectl{Kubectl: &kubetest.MockKubectlCmd{}}
ctrl, err := NewApplicationController(
test.FakeArgoCDNamespace,
@@ -179,6 +177,7 @@ func newFakeControllerWithResync(ctx context.Context, data *fakeData, appResyncP
time.Second,
time.Minute,
nil,
time.Minute,
0,
time.Second*10,
common.DefaultPortArgoCDMetrics,
@@ -408,37 +407,6 @@ metadata:
data:
`
var fakePreDeleteHook = `
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": "pre-delete-hook",
"namespace": "default",
"labels": {
"app.kubernetes.io/instance": "my-app"
},
"annotations": {
"argocd.argoproj.io/hook": "PreDelete"
}
},
"spec": {
"containers": [
{
"name": "pre-delete-hook",
"image": "busybox",
"restartPolicy": "Never",
"command": [
"/bin/sh",
"-c",
"sleep 5 && echo hello from the pre-delete-hook pod"
]
}
]
}
}
`
var fakePostDeleteHook = `
{
"apiVersion": "batch/v1",
@@ -589,15 +557,6 @@ func newFakeCM() map[string]any {
return cm
}
func newFakePreDeleteHook() map[string]any {
var cm map[string]any
err := yaml.Unmarshal([]byte(fakePreDeleteHook), &cm)
if err != nil {
panic(err)
}
return cm
}
func newFakePostDeleteHook() map[string]any {
var hook map[string]any
err := yaml.Unmarshal([]byte(fakePostDeleteHook), &hook)
@@ -1155,40 +1114,6 @@ func TestFinalizeAppDeletion(t *testing.T) {
testShouldDelete(app3)
})
t.Run("PreDelete_HookIsCreated", func(t *testing.T) {
app := newFakeApp()
app.SetPreDeleteFinalizer()
app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
ctrl := newFakeController(context.Background(), &fakeData{
manifestResponses: []*apiclient.ManifestResponse{{
Manifests: []string{fakePreDeleteHook},
}},
apps: []runtime.Object{app, &defaultProj},
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{},
}, nil)
patched := false
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
defaultReactor := fakeAppCs.ReactionChain[0]
fakeAppCs.ReactionChain = nil
fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
return defaultReactor.React(action)
})
fakeAppCs.AddReactor("patch", "*", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) {
patched = true
return true, &v1alpha1.Application{}, nil
})
err := ctrl.finalizeApplicationDeletion(app, func(_ string) ([]*v1alpha1.Cluster, error) {
return []*v1alpha1.Cluster{}, nil
})
require.NoError(t, err)
// finalizer is not deleted
assert.False(t, patched)
// pre-delete hook is created
require.Len(t, ctrl.kubectl.(*MockKubectl).CreatedResources, 1)
require.Equal(t, "pre-delete-hook", ctrl.kubectl.(*MockKubectl).CreatedResources[0].GetName())
})
t.Run("PostDelete_HookIsCreated", func(t *testing.T) {
app := newFakeApp()
app.SetPostDeleteFinalizer()
@@ -1223,41 +1148,6 @@ func TestFinalizeAppDeletion(t *testing.T) {
require.Equal(t, "post-delete-hook", ctrl.kubectl.(*MockKubectl).CreatedResources[0].GetName())
})
t.Run("PreDelete_HookIsExecuted", func(t *testing.T) {
app := newFakeApp()
app.SetPreDeleteFinalizer()
app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
liveHook := &unstructured.Unstructured{Object: newFakePreDeleteHook()}
require.NoError(t, unstructured.SetNestedField(liveHook.Object, "Succeeded", "status", "phase"))
ctrl := newFakeController(context.Background(), &fakeData{
manifestResponses: []*apiclient.ManifestResponse{{
Manifests: []string{fakePreDeleteHook},
}},
apps: []runtime.Object{app, &defaultProj},
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
kube.GetResourceKey(liveHook): liveHook,
},
}, nil)
patched := false
fakeAppCs := ctrl.applicationClientset.(*appclientset.Clientset)
defaultReactor := fakeAppCs.ReactionChain[0]
fakeAppCs.ReactionChain = nil
fakeAppCs.AddReactor("get", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
return defaultReactor.React(action)
})
fakeAppCs.AddReactor("patch", "*", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) {
patched = true
return true, &v1alpha1.Application{}, nil
})
err := ctrl.finalizeApplicationDeletion(app, func(_ string) ([]*v1alpha1.Cluster, error) {
return []*v1alpha1.Cluster{}, nil
})
require.NoError(t, err)
// finalizer is removed
assert.True(t, patched)
})
t.Run("PostDelete_HookIsExecuted", func(t *testing.T) {
app := newFakeApp()
app.SetPostDeleteFinalizer()
@@ -3147,3 +3037,46 @@ func TestSelfHealRemainingBackoff(t *testing.T) {
})
}
}
func TestSelfHealBackoffCooldownElapsed(t *testing.T) {
cooldown := time.Second * 30
ctrl := newFakeController(t.Context(), &fakeData{}, nil)
ctrl.selfHealBackoffCooldown = cooldown
app := &v1alpha1.Application{
Status: v1alpha1.ApplicationStatus{
OperationState: &v1alpha1.OperationState{
Phase: synccommon.OperationSucceeded,
},
},
}
t.Run("operation not completed", func(t *testing.T) {
app := app.DeepCopy()
app.Status.OperationState.FinishedAt = nil
elapsed := ctrl.selfHealBackoffCooldownElapsed(app)
assert.True(t, elapsed)
})
t.Run("successful operation finised after cooldown", func(t *testing.T) {
app := app.DeepCopy()
app.Status.OperationState.FinishedAt = &metav1.Time{Time: time.Now().Add(-cooldown)}
elapsed := ctrl.selfHealBackoffCooldownElapsed(app)
assert.True(t, elapsed)
})
t.Run("unsuccessful operation finised after cooldown", func(t *testing.T) {
app := app.DeepCopy()
app.Status.OperationState.Phase = synccommon.OperationFailed
app.Status.OperationState.FinishedAt = &metav1.Time{Time: time.Now().Add(-cooldown)}
elapsed := ctrl.selfHealBackoffCooldownElapsed(app)
assert.False(t, elapsed)
})
t.Run("successful operation finised before cooldown", func(t *testing.T) {
app := app.DeepCopy()
app.Status.OperationState.FinishedAt = &metav1.Time{Time: time.Now()}
elapsed := ctrl.selfHealBackoffCooldownElapsed(app)
assert.False(t, elapsed)
})
}

View File

@@ -270,7 +270,7 @@ func (c *liveStateCache) loadCacheSettings() (*cacheSettings, error) {
return &cacheSettings{clusterSettings, appInstanceLabelKey, appv1.TrackingMethod(trackingMethod), installationID, resourceUpdatesOverrides, ignoreResourceUpdatesEnabled}, nil
}
func asResourceNode(r *clustercache.Resource, namespaceResources map[kube.ResourceKey]*clustercache.Resource) appv1.ResourceNode {
func asResourceNode(r *clustercache.Resource) appv1.ResourceNode {
gv, err := schema.ParseGroupVersion(r.Ref.APIVersion)
if err != nil {
gv = schema.GroupVersion{}
@@ -278,30 +278,14 @@ func asResourceNode(r *clustercache.Resource, namespaceResources map[kube.Resour
parentRefs := make([]appv1.ResourceRef, len(r.OwnerRefs))
for i, ownerRef := range r.OwnerRefs {
ownerGvk := schema.FromAPIVersionAndKind(ownerRef.APIVersion, ownerRef.Kind)
parentRef := appv1.ResourceRef{
Group: ownerGvk.Group,
Kind: ownerGvk.Kind,
Version: ownerGvk.Version,
Name: ownerRef.Name,
UID: string(ownerRef.UID),
parentRefs[i] = appv1.ResourceRef{
Group: ownerGvk.Group,
Kind: ownerGvk.Kind,
Version: ownerGvk.Version,
Namespace: r.Ref.Namespace,
Name: ownerRef.Name,
UID: string(ownerRef.UID),
}
// Look up the parent in namespace resources
// If found, it's namespaced and we use its namespace
// If not found, it must be cluster-scoped (namespace = "")
parentKey := kube.NewResourceKey(ownerGvk.Group, ownerGvk.Kind, r.Ref.Namespace, ownerRef.Name)
if parent, ok := namespaceResources[parentKey]; ok {
parentRef.Namespace = parent.Ref.Namespace
} else {
// Not in namespace => must be cluster-scoped
parentRef.Namespace = ""
// Debug logging for cross-namespace relationships
if r.Ref.Namespace != "" {
log.Debugf("Cross-namespace ref: %s/%s in namespace %s has parent %s/%s (cluster-scoped)",
r.Ref.Kind, r.Ref.Name, r.Ref.Namespace, ownerGvk.Kind, ownerRef.Name)
}
}
parentRefs[i] = parentRef
}
var resHealth *appv1.HealthStatus
resourceInfo := resInfo(r)
@@ -689,7 +673,7 @@ func (c *liveStateCache) IterateHierarchyV2(server *appv1.Cluster, keys []kube.R
return err
}
clusterInfo.IterateHierarchyV2(keys, func(resource *clustercache.Resource, namespaceResources map[kube.ResourceKey]*clustercache.Resource) bool {
return action(asResourceNode(resource, namespaceResources), getApp(resource, namespaceResources))
return action(asResourceNode(resource), getApp(resource, namespaceResources))
})
return nil
}
@@ -714,15 +698,9 @@ func (c *liveStateCache) GetNamespaceTopLevelResources(server *appv1.Cluster, na
return nil, err
}
resources := clusterInfo.FindResources(namespace, clustercache.TopLevelResource)
// Get all namespace resources for parent lookups
namespaceResources := clusterInfo.FindResources(namespace, func(_ *clustercache.Resource) bool {
return true
})
res := make(map[kube.ResourceKey]appv1.ResourceNode)
for k, r := range resources {
res[k] = asResourceNode(r, namespaceResources)
res[k] = asResourceNode(r)
}
return res, nil
}

View File

@@ -323,7 +323,7 @@ func Test_asResourceNode_owner_refs(t *testing.T) {
CreationTimestamp: nil,
Info: nil,
Resource: nil,
}, nil)
})
expected := appv1.ResourceNode{
ResourceRef: appv1.ResourceRef{
Version: "v1",
@@ -842,113 +842,3 @@ func Test_ownerRefGV(t *testing.T) {
})
}
}
func Test_asResourceNode_cross_namespace_parent(t *testing.T) {
// Test that a namespaced resource with a cluster-scoped parent
// correctly sets the parent namespace to empty string
// Create a Role (namespaced) with an owner reference to a ClusterRole (cluster-scoped)
roleResource := &cache.Resource{
Ref: corev1.ObjectReference{
APIVersion: "rbac.authorization.k8s.io/v1",
Kind: "Role",
Namespace: "my-namespace",
Name: "my-role",
},
OwnerRefs: []metav1.OwnerReference{
{
APIVersion: "rbac.authorization.k8s.io/v1",
Kind: "ClusterRole",
Name: "my-cluster-role",
UID: "cluster-role-uid",
},
},
}
// Create namespace resources map (ClusterRole won't be in here since it's cluster-scoped)
namespaceResources := map[kube.ResourceKey]*cache.Resource{
// Add some other namespace resources but not the ClusterRole
{
Group: "rbac.authorization.k8s.io",
Kind: "Role",
Namespace: "my-namespace",
Name: "other-role",
}: {
Ref: corev1.ObjectReference{
APIVersion: "rbac.authorization.k8s.io/v1",
Kind: "Role",
Namespace: "my-namespace",
Name: "other-role",
},
},
}
resNode := asResourceNode(roleResource, namespaceResources)
// The parent reference should have empty namespace since ClusterRole is cluster-scoped
assert.Len(t, resNode.ParentRefs, 1)
assert.Equal(t, "ClusterRole", resNode.ParentRefs[0].Kind)
assert.Equal(t, "my-cluster-role", resNode.ParentRefs[0].Name)
assert.Empty(t, resNode.ParentRefs[0].Namespace, "ClusterRole parent should have empty namespace")
}
func Test_asResourceNode_same_namespace_parent(t *testing.T) {
// Test that a namespaced resource with a namespaced parent in the same namespace
// correctly sets the parent namespace
// Create a ReplicaSet with an owner reference to a Deployment (both namespaced)
rsResource := &cache.Resource{
Ref: corev1.ObjectReference{
APIVersion: "apps/v1",
Kind: "ReplicaSet",
Namespace: "my-namespace",
Name: "my-rs",
},
OwnerRefs: []metav1.OwnerReference{
{
APIVersion: "apps/v1",
Kind: "Deployment",
Name: "my-deployment",
UID: "deployment-uid",
},
},
}
// Create namespace resources map with the Deployment
deploymentKey := kube.ResourceKey{
Group: "apps",
Kind: "Deployment",
Namespace: "my-namespace",
Name: "my-deployment",
}
namespaceResources := map[kube.ResourceKey]*cache.Resource{
deploymentKey: {
Ref: corev1.ObjectReference{
APIVersion: "apps/v1",
Kind: "Deployment",
Namespace: "my-namespace",
Name: "my-deployment",
UID: "deployment-uid",
},
Resource: &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]any{
"name": "my-deployment",
"namespace": "my-namespace",
"uid": "deployment-uid",
},
},
},
},
}
resNode := asResourceNode(rsResource, namespaceResources)
// The parent reference should have the same namespace
assert.Len(t, resNode.ParentRefs, 1)
assert.Equal(t, "Deployment", resNode.ParentRefs[0].Kind)
assert.Equal(t, "my-deployment", resNode.ParentRefs[0].Name)
assert.Equal(t, "my-namespace", resNode.ParentRefs[0].Namespace, "Deployment parent should have same namespace")
}

View File

@@ -225,19 +225,9 @@ func populateIngressInfo(un *unstructured.Unstructured, res *ResourceInfo) {
if res.NetworkingInfo != nil {
urls = res.NetworkingInfo.ExternalURLs
}
enableDefaultExternalURLs := true
if ignoreVal, ok := un.GetAnnotations()[common.AnnotationKeyIgnoreDefaultLinks]; ok {
if ignoreDefaultLinks, err := strconv.ParseBool(ignoreVal); err == nil {
enableDefaultExternalURLs = !ignoreDefaultLinks
}
for url := range urlsSet {
urls = append(urls, url)
}
if enableDefaultExternalURLs {
for url := range urlsSet {
urls = append(urls, url)
}
}
res.NetworkingInfo = &v1alpha1.ResourceNetworkingInfo{TargetRefs: targets, Ingress: ingress, ExternalURLs: urls}
}

View File

@@ -126,40 +126,6 @@ var (
ingress:
- ip: 107.178.210.11`)
testIgnoreDefaultLinksIngress = strToUnstructured(`
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: helm-guestbook
namespace: default
uid: "4"
annotations:
link.argocd.argoproj.io/external-link: http://my-grafana.example.com/ingress-link
argocd.argoproj.io/ignore-default-links: "true"
spec:
backend:
serviceName: not-found-service
servicePort: 443
rules:
- host: helm-guestbook.example.com
http:
paths:
- backend:
serviceName: helm-guestbook
servicePort: 443
path: /
- backend:
serviceName: helm-guestbook
servicePort: https
path: /
tls:
- host: helm-guestbook.example.com
secretName: my-tls-secret
status:
loadBalancer:
ingress:
- ip: 107.178.210.11`)
testIngressWildCardPath = strToUnstructured(`
apiVersion: extensions/v1beta1
kind: Ingress
@@ -1234,30 +1200,6 @@ func TestGetLinkAnnotatedIngressInfo(t *testing.T) {
}, info.NetworkingInfo)
}
func TestGetIgnoreDefaultLinksIngressInfo(t *testing.T) {
info := &ResourceInfo{}
populateNodeInfo(testIgnoreDefaultLinksIngress, info, []string{})
assert.Empty(t, info.Info)
sort.Slice(info.NetworkingInfo.TargetRefs, func(i, j int) bool {
return info.NetworkingInfo.TargetRefs[i].Name < info.NetworkingInfo.TargetRefs[j].Name
})
assert.Equal(t, &v1alpha1.ResourceNetworkingInfo{
Ingress: []corev1.LoadBalancerIngress{{IP: "107.178.210.11"}},
TargetRefs: []v1alpha1.ResourceRef{{
Namespace: "default",
Group: "",
Kind: kube.ServiceKind,
Name: "helm-guestbook",
}, {
Namespace: "default",
Group: "",
Kind: kube.ServiceKind,
Name: "not-found-service",
}},
ExternalURLs: []string{"http://my-grafana.example.com/ingress-link"},
}, info.NetworkingInfo)
}
func TestGetIngressInfoWildCardPath(t *testing.T) {
info := &ResourceInfo{}
populateNodeInfo(testIngressWildCardPath, info, []string{})

View File

@@ -8,6 +8,7 @@ import (
"github.com/argoproj/gitops-engine/pkg/sync/ignore"
kubeutil "github.com/argoproj/gitops-engine/pkg/utils/kube"
log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/argoproj/argo-cd/v3/common"
"github.com/argoproj/argo-cd/v3/pkg/apis/application"
@@ -20,35 +21,27 @@ import (
func setApplicationHealth(resources []managedResource, statuses []appv1.ResourceStatus, resourceOverrides map[string]appv1.ResourceOverride, app *appv1.Application, persistResourceHealth bool) (health.HealthStatusCode, error) {
var savedErr error
var errCount uint
var containsResources, containsLiveResources bool
appHealthStatus := health.HealthStatusHealthy
for i, res := range resources {
if res.Target != nil && hookutil.Skip(res.Target) {
continue
}
if res.Live != nil && (hookutil.IsHook(res.Live) || ignore.Ignore(res.Live)) {
if res.Live != nil && res.Live.GetAnnotations() != nil && res.Live.GetAnnotations()[common.AnnotationIgnoreHealthCheck] == "true" {
continue
}
// Contains actual resources that are not hooks
containsResources = true
if res.Live != nil {
containsLiveResources = true
}
// Do not aggregate the health of the resource if the annotation to ignore health check is set to true
if res.Live != nil && res.Live.GetAnnotations() != nil && res.Live.GetAnnotations()[common.AnnotationIgnoreHealthCheck] == "true" {
if res.Live != nil && (hookutil.IsHook(res.Live) || ignore.Ignore(res.Live)) {
continue
}
var healthStatus *health.HealthStatus
var err error
healthOverrides := lua.ResourceHealthOverrides(resourceOverrides)
gvk := schema.GroupVersionKind{Group: res.Group, Version: res.Version, Kind: res.Kind}
if res.Live == nil {
healthStatus = &health.HealthStatus{Status: health.HealthStatusMissing}
} else {
// App that manages itself should not affect own health
// App the manages itself should not affect own health
if isSelfReferencedApp(app, kubeutil.GetObjectRef(res.Live)) {
continue
}
@@ -72,8 +65,8 @@ func setApplicationHealth(resources []managedResource, statuses []appv1.Resource
statuses[i].Health = nil
}
// Missing resources should not affect parent app health - the OutOfSync status already indicates resources are missing
if res.Live == nil && healthStatus.Status == health.HealthStatusMissing {
// Is health status is missing but resource has not built-in/custom health check then it should not affect parent app health
if _, hasOverride := healthOverrides[lua.GetConfigMapKey(gvk)]; healthStatus.Status == health.HealthStatusMissing && !hasOverride && health.GetHealthCheckFunc(gvk) == nil {
continue
}
@@ -86,12 +79,6 @@ func setApplicationHealth(resources []managedResource, statuses []appv1.Resource
appHealthStatus = healthStatus.Status
}
}
// If the app is expected to have resources but does not contain any live resources, set the app health to missing
if containsResources && !containsLiveResources && health.IsWorse(appHealthStatus, health.HealthStatusMissing) {
appHealthStatus = health.HealthStatusMissing
}
if persistResourceHealth {
app.Status.ResourceHealthSource = appv1.ResourceHealthLocationInline
} else {

View File

@@ -16,7 +16,6 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/yaml"
"github.com/argoproj/argo-cd/v3/common"
"github.com/argoproj/argo-cd/v3/pkg/apis/application"
appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/util/lua"
@@ -104,103 +103,12 @@ func TestSetApplicationHealth_ResourceHealthNotPersisted(t *testing.T) {
assert.Nil(t, resourceStatuses[0].Health)
}
func TestSetApplicationHealth_NoResource(t *testing.T) {
resources := []managedResource{}
resourceStatuses := initStatuses(resources)
healthStatus, err := setApplicationHealth(resources, resourceStatuses, lua.ResourceHealthOverrides{}, app, true)
require.NoError(t, err)
assert.Equal(t, health.HealthStatusHealthy, healthStatus)
}
func TestSetApplicationHealth_OnlyHooks(t *testing.T) {
pod := resourceFromFile("./testdata/pod-running-restart-always.yaml")
pod.SetAnnotations(map[string]string{synccommon.AnnotationKeyHook: string(synccommon.HookTypeSync)})
resources := []managedResource{{
Group: "", Version: "v1", Kind: "Pod", Target: &pod, Live: &pod,
}}
resourceStatuses := initStatuses(resources)
healthStatus, err := setApplicationHealth(resources, resourceStatuses, lua.ResourceHealthOverrides{}, app, true)
require.NoError(t, err)
assert.Equal(t, health.HealthStatusHealthy, healthStatus)
}
func TestSetApplicationHealth_MissingResource(t *testing.T) {
pod := resourceFromFile("./testdata/pod-running-restart-always.yaml")
pod2 := pod.DeepCopy()
pod2.SetName("pod2")
resources := []managedResource{
{Group: "", Version: "v1", Kind: "Pod", Target: &pod},
{Group: "", Version: "v1", Kind: "Pod", Target: pod2, Live: pod2},
}
resourceStatuses := initStatuses(resources)
healthStatus, err := setApplicationHealth(resources, resourceStatuses, lua.ResourceHealthOverrides{}, app, true)
require.NoError(t, err)
assert.Equal(t, health.HealthStatusHealthy, healthStatus)
}
func TestSetApplicationHealth_MissingResource_WithIgnoreHealthcheck(t *testing.T) {
pod := resourceFromFile("./testdata/pod-running-restart-always.yaml")
pod2 := pod.DeepCopy()
pod2.SetName("pod2")
pod2.SetAnnotations(map[string]string{common.AnnotationIgnoreHealthCheck: "true"})
resources := []managedResource{
{Group: "", Version: "v1", Kind: "Pod", Target: &pod},
{Group: "", Version: "v1", Kind: "Pod", Target: pod2, Live: pod2},
}
resourceStatuses := initStatuses(resources)
healthStatus, err := setApplicationHealth(resources, resourceStatuses, lua.ResourceHealthOverrides{}, app, true)
require.NoError(t, err)
assert.Equal(t, health.HealthStatusHealthy, healthStatus)
}
func TestSetApplicationHealth_MissingResource_WithChildApp(t *testing.T) {
childApp := newAppLiveObj(health.HealthStatusUnknown)
pod := resourceFromFile("./testdata/pod-running-restart-always.yaml")
resources := []managedResource{
{Group: application.Group, Version: "v1alpha1", Kind: application.ApplicationKind, Target: childApp, Live: childApp},
{Group: "", Version: "v1", Kind: "Pod", Target: &pod},
}
resourceStatuses := initStatuses(resources)
healthStatus, err := setApplicationHealth(resources, resourceStatuses, lua.ResourceHealthOverrides{}, app, true)
require.NoError(t, err)
assert.Equal(t, health.HealthStatusHealthy, healthStatus)
}
func TestSetApplicationHealth_AllMissingResources(t *testing.T) {
pod := resourceFromFile("./testdata/pod-running-restart-always.yaml")
pod2 := pod.DeepCopy()
pod2.SetName("pod2")
resources := []managedResource{
{Group: "", Version: "v1", Kind: "Pod", Target: &pod},
{Group: "", Version: "v1", Kind: "Pod", Target: pod2},
}
resourceStatuses := initStatuses(resources)
healthStatus, err := setApplicationHealth(resources, resourceStatuses, lua.ResourceHealthOverrides{}, app, true)
require.NoError(t, err)
assert.Equal(t, health.HealthStatusMissing, healthStatus)
}
func TestSetApplicationHealth_AllMissingResources_WithHooks(t *testing.T) {
pod := resourceFromFile("./testdata/pod-running-restart-always.yaml")
pod2 := pod.DeepCopy()
pod2.SetName("pod2")
pod2.SetAnnotations(map[string]string{synccommon.AnnotationKeyHook: string(synccommon.HookTypeSync)})
resources := []managedResource{{
Group: "", Version: "v1", Kind: "Pod", Target: &pod,
}, {
Group: "", Version: "v1", Kind: "Pod", Target: pod2, Live: pod2,
}}
}, {}}
resourceStatuses := initStatuses(resources)
healthStatus, err := setApplicationHealth(resources, resourceStatuses, lua.ResourceHealthOverrides{}, app, true)
@@ -241,6 +149,32 @@ func TestSetApplicationHealth_HealthImproves(t *testing.T) {
}
}
func TestSetApplicationHealth_MissingResourceNoBuiltHealthCheck(t *testing.T) {
cm := resourceFromFile("./testdata/configmap.yaml")
resources := []managedResource{{
Group: "", Version: "v1", Kind: "ConfigMap", Target: &cm,
}}
resourceStatuses := initStatuses(resources)
t.Run("NoOverride", func(t *testing.T) {
healthStatus, err := setApplicationHealth(resources, resourceStatuses, lua.ResourceHealthOverrides{}, app, true)
require.NoError(t, err)
assert.Equal(t, health.HealthStatusHealthy, healthStatus)
assert.Equal(t, health.HealthStatusMissing, resourceStatuses[0].Health.Status)
})
t.Run("HasOverride", func(t *testing.T) {
healthStatus, err := setApplicationHealth(resources, resourceStatuses, lua.ResourceHealthOverrides{
lua.GetConfigMapKey(schema.GroupVersionKind{Version: "v1", Kind: "ConfigMap"}): appv1.ResourceOverride{
HealthLua: "some health check",
},
}, app, true)
require.NoError(t, err)
assert.Equal(t, health.HealthStatusMissing, healthStatus)
})
}
func newAppLiveObj(status health.HealthStatusCode) *unstructured.Unstructured {
app := appv1.Application{
ObjectMeta: metav1.ObjectMeta{
@@ -280,9 +214,9 @@ return hs`,
}
t.Run("ChildAppDegraded", func(t *testing.T) {
childApp := newAppLiveObj(health.HealthStatusDegraded)
degradedApp := newAppLiveObj(health.HealthStatusDegraded)
resources := []managedResource{{
Group: application.Group, Version: "v1alpha1", Kind: application.ApplicationKind, Live: childApp,
Group: application.Group, Version: "v1alpha1", Kind: application.ApplicationKind, Live: degradedApp,
}, {}}
resourceStatuses := initStatuses(resources)
@@ -292,21 +226,9 @@ return hs`,
})
t.Run("ChildAppMissing", func(t *testing.T) {
childApp := newAppLiveObj(health.HealthStatusMissing)
degradedApp := newAppLiveObj(health.HealthStatusMissing)
resources := []managedResource{{
Group: application.Group, Version: "v1alpha1", Kind: application.ApplicationKind, Live: childApp,
}, {}}
resourceStatuses := initStatuses(resources)
healthStatus, err := setApplicationHealth(resources, resourceStatuses, overrides, app, true)
require.NoError(t, err)
assert.Equal(t, health.HealthStatusHealthy, healthStatus)
})
t.Run("ChildAppUnknown", func(t *testing.T) {
childApp := newAppLiveObj(health.HealthStatusUnknown)
resources := []managedResource{{
Group: application.Group, Version: "v1alpha1", Kind: application.ApplicationKind, Live: childApp,
Group: application.Group, Version: "v1alpha1", Kind: application.ApplicationKind, Live: degradedApp,
}, {}}
resourceStatuses := initStatuses(resources)

View File

@@ -2,8 +2,6 @@ package controller
import (
"context"
"fmt"
"strings"
"github.com/argoproj/gitops-engine/pkg/health"
"github.com/argoproj/gitops-engine/pkg/sync/common"
@@ -16,33 +14,26 @@ import (
"github.com/argoproj/argo-cd/v3/util/lua"
appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
)
type HookType string
const (
PreDeleteHookType HookType = "PreDelete"
PostDeleteHookType HookType = "PostDelete"
)
var hookTypeAnnotations = map[HookType]map[string]string{
PreDeleteHookType: {
"argocd.argoproj.io/hook": string(PreDeleteHookType),
"helm.sh/hook": "pre-delete",
},
PostDeleteHookType: {
"argocd.argoproj.io/hook": string(PostDeleteHookType),
var (
postDeleteHook = "PostDelete"
postDeleteHooks = map[string]string{
"argocd.argoproj.io/hook": postDeleteHook,
"helm.sh/hook": "post-delete",
},
}
)
func isHook(obj *unstructured.Unstructured) bool {
return hook.IsHook(obj) || isPostDeleteHook(obj)
}
func isHookOfType(obj *unstructured.Unstructured, hookType HookType) bool {
func isPostDeleteHook(obj *unstructured.Unstructured) bool {
if obj == nil || obj.GetAnnotations() == nil {
return false
}
for k, v := range hookTypeAnnotations[hookType] {
for k, v := range postDeleteHooks {
if val, ok := obj.GetAnnotations()[k]; ok && val == v {
return true
}
@@ -50,34 +41,11 @@ func isHookOfType(obj *unstructured.Unstructured, hookType HookType) bool {
return false
}
func isHook(obj *unstructured.Unstructured) bool {
if hook.IsHook(obj) {
return true
}
for hookType := range hookTypeAnnotations {
if isHookOfType(obj, hookType) {
return true
}
}
return false
}
func isPreDeleteHook(obj *unstructured.Unstructured) bool {
return isHookOfType(obj, PreDeleteHookType)
}
func isPostDeleteHook(obj *unstructured.Unstructured) bool {
return isHookOfType(obj, PostDeleteHookType)
}
// executeHooks is a generic function to execute hooks of a specified type
func (ctrl *ApplicationController) executeHooks(hookType HookType, app *appv1.Application, proj *appv1.AppProject, liveObjs map[kube.ResourceKey]*unstructured.Unstructured, config *rest.Config, logCtx *log.Entry) (bool, error) {
func (ctrl *ApplicationController) executePostDeleteHooks(app *v1alpha1.Application, proj *v1alpha1.AppProject, liveObjs map[kube.ResourceKey]*unstructured.Unstructured, config *rest.Config, logCtx *log.Entry) (bool, error) {
appLabelKey, err := ctrl.settingsMgr.GetAppInstanceLabelKey()
if err != nil {
return false, err
}
var revisions []string
for _, src := range app.Spec.GetSources() {
revisions = append(revisions, src.TargetRevision)
@@ -87,62 +55,44 @@ func (ctrl *ApplicationController) executeHooks(hookType HookType, app *appv1.Ap
if err != nil {
return false, err
}
// Find existing hooks of the specified type
runningHooks := map[kube.ResourceKey]*unstructured.Unstructured{}
for key, obj := range liveObjs {
if isHookOfType(obj, hookType) {
if isPostDeleteHook(obj) {
runningHooks[key] = obj
}
}
// Find expected hooks that need to be created
expectedHook := map[kube.ResourceKey]*unstructured.Unstructured{}
for _, obj := range targets {
if obj.GetNamespace() == "" {
obj.SetNamespace(app.Spec.Destination.Namespace)
}
if !isHookOfType(obj, hookType) {
if !isPostDeleteHook(obj) {
continue
}
if runningHook := runningHooks[kube.GetResourceKey(obj)]; runningHook == nil {
expectedHook[kube.GetResourceKey(obj)] = obj
}
}
// Create hooks that don't exist yet
createdCnt := 0
for _, obj := range expectedHook {
// Add app instance label so the hook can be tracked and cleaned up
labels := obj.GetLabels()
if labels == nil {
labels = make(map[string]string)
}
labels[appLabelKey] = app.InstanceName(ctrl.namespace)
obj.SetLabels(labels)
_, err = ctrl.kubectl.CreateResource(context.Background(), config, obj.GroupVersionKind(), obj.GetName(), obj.GetNamespace(), obj, metav1.CreateOptions{})
if err != nil {
return false, err
}
createdCnt++
}
if createdCnt > 0 {
logCtx.Infof("Created %d %s hooks", createdCnt, hookType)
logCtx.Infof("Created %d post-delete hooks", createdCnt)
return false, nil
}
// Check health of running hooks
resourceOverrides, err := ctrl.settingsMgr.GetResourceOverrides()
if err != nil {
return false, err
}
healthOverrides := lua.ResourceHealthOverrides(resourceOverrides)
progressingHooksCount := 0
var failedHooks []string
var failedHookObjects []*unstructured.Unstructured
progressingHooksCnt := 0
for _, obj := range runningHooks {
hookHealth, err := health.GetResourceHealth(obj, healthOverrides)
if err != nil {
@@ -160,37 +110,19 @@ func (ctrl *ApplicationController) executeHooks(hookType HookType, app *appv1.Ap
Status: health.HealthStatusHealthy,
}
}
switch hookHealth.Status {
case health.HealthStatusProgressing:
progressingHooksCount++
case health.HealthStatusDegraded:
failedHooks = append(failedHooks, fmt.Sprintf("%s/%s", obj.GetNamespace(), obj.GetName()))
failedHookObjects = append(failedHookObjects, obj)
if hookHealth.Status == health.HealthStatusProgressing {
progressingHooksCnt++
}
}
if len(failedHooks) > 0 {
// Delete failed hooks to allow retry with potentially fixed hook definitions
logCtx.Infof("Deleting %d failed %s hook(s) to allow retry", len(failedHookObjects), hookType)
for _, obj := range failedHookObjects {
err = ctrl.kubectl.DeleteResource(context.Background(), config, obj.GroupVersionKind(), obj.GetName(), obj.GetNamespace(), metav1.DeleteOptions{})
if err != nil {
logCtx.WithError(err).Warnf("Failed to delete failed hook %s/%s", obj.GetNamespace(), obj.GetName())
}
}
return false, fmt.Errorf("%s hook(s) failed: %s", hookType, strings.Join(failedHooks, ", "))
}
if progressingHooksCount > 0 {
logCtx.Infof("Waiting for %d %s hooks to complete", progressingHooksCount, hookType)
if progressingHooksCnt > 0 {
logCtx.Infof("Waiting for %d post-delete hooks to complete", progressingHooksCnt)
return false, nil
}
return true, nil
}
// cleanupHooks is a generic function to clean up hooks of a specified type
func (ctrl *ApplicationController) cleanupHooks(hookType HookType, liveObjs map[kube.ResourceKey]*unstructured.Unstructured, config *rest.Config, logCtx *log.Entry) (bool, error) {
func (ctrl *ApplicationController) cleanupPostDeleteHooks(liveObjs map[kube.ResourceKey]*unstructured.Unstructured, config *rest.Config, logCtx *log.Entry) (bool, error) {
resourceOverrides, err := ctrl.settingsMgr.GetResourceOverrides()
if err != nil {
return false, err
@@ -200,10 +132,8 @@ func (ctrl *ApplicationController) cleanupHooks(hookType HookType, liveObjs map[
pendingDeletionCount := 0
aggregatedHealth := health.HealthStatusHealthy
var hooks []*unstructured.Unstructured
// Collect hooks and determine overall health
for _, obj := range liveObjs {
if !isHookOfType(obj, hookType) {
if !isPostDeleteHook(obj) {
continue
}
hookHealth, err := health.GetResourceHealth(obj, healthOverrides)
@@ -221,60 +151,25 @@ func (ctrl *ApplicationController) cleanupHooks(hookType HookType, liveObjs map[
hooks = append(hooks, obj)
}
// Process hooks for deletion
for _, obj := range hooks {
deletePolicies := hook.DeletePolicies(obj)
shouldDelete := false
if len(deletePolicies) == 0 {
// If no delete policy is specified, always delete hooks during cleanup phase
shouldDelete = true
} else {
// Check if any delete policy matches the current hook state
for _, policy := range deletePolicies {
if (policy == common.HookDeletePolicyHookFailed && aggregatedHealth == health.HealthStatusDegraded) ||
(policy == common.HookDeletePolicyHookSucceeded && aggregatedHealth == health.HealthStatusHealthy) {
shouldDelete = true
break
}
for _, policy := range hook.DeletePolicies(obj) {
if (policy != common.HookDeletePolicyHookFailed || aggregatedHealth != health.HealthStatusDegraded) && (policy != common.HookDeletePolicyHookSucceeded || aggregatedHealth != health.HealthStatusHealthy) {
continue
}
}
if shouldDelete {
pendingDeletionCount++
if obj.GetDeletionTimestamp() != nil {
continue
}
logCtx.Infof("Deleting %s hook %s/%s", hookType, obj.GetNamespace(), obj.GetName())
logCtx.Infof("Deleting post-delete hook %s/%s", obj.GetNamespace(), obj.GetName())
err = ctrl.kubectl.DeleteResource(context.Background(), config, obj.GroupVersionKind(), obj.GetName(), obj.GetNamespace(), metav1.DeleteOptions{})
if err != nil {
return false, err
}
}
}
if pendingDeletionCount > 0 {
logCtx.Infof("Waiting for %d %s hooks to be deleted", pendingDeletionCount, hookType)
logCtx.Infof("Waiting for %d post-delete hooks to be deleted", pendingDeletionCount)
return false, nil
}
return true, nil
}
// Execute and cleanup hooks for pre-delete and post-delete operations
func (ctrl *ApplicationController) executePreDeleteHooks(app *appv1.Application, proj *appv1.AppProject, liveObjs map[kube.ResourceKey]*unstructured.Unstructured, config *rest.Config, logCtx *log.Entry) (bool, error) {
return ctrl.executeHooks(PreDeleteHookType, app, proj, liveObjs, config, logCtx)
}
func (ctrl *ApplicationController) cleanupPreDeleteHooks(liveObjs map[kube.ResourceKey]*unstructured.Unstructured, config *rest.Config, logCtx *log.Entry) (bool, error) {
return ctrl.cleanupHooks(PreDeleteHookType, liveObjs, config, logCtx)
}
func (ctrl *ApplicationController) executePostDeleteHooks(app *appv1.Application, proj *appv1.AppProject, liveObjs map[kube.ResourceKey]*unstructured.Unstructured, config *rest.Config, logCtx *log.Entry) (bool, error) {
return ctrl.executeHooks(PostDeleteHookType, app, proj, liveObjs, config, logCtx)
}
func (ctrl *ApplicationController) cleanupPostDeleteHooks(liveObjs map[kube.ResourceKey]*unstructured.Unstructured, config *rest.Config, logCtx *log.Entry) (bool, error) {
return ctrl.cleanupHooks(PostDeleteHookType, liveObjs, config, logCtx)
}

View File

@@ -1,173 +0,0 @@
package controller
import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestIsHookOfType(t *testing.T) {
tests := []struct {
name string
hookType HookType
annot map[string]string
expected bool
}{
{
name: "ArgoCD PreDelete hook",
hookType: PreDeleteHookType,
annot: map[string]string{"argocd.argoproj.io/hook": "PreDelete"},
expected: true,
},
{
name: "Helm PreDelete hook",
hookType: PreDeleteHookType,
annot: map[string]string{"helm.sh/hook": "pre-delete"},
expected: true,
},
{
name: "ArgoCD PostDelete hook",
hookType: PostDeleteHookType,
annot: map[string]string{"argocd.argoproj.io/hook": "PostDelete"},
expected: true,
},
{
name: "Helm PostDelete hook",
hookType: PostDeleteHookType,
annot: map[string]string{"helm.sh/hook": "post-delete"},
expected: true,
},
{
name: "Not a hook",
hookType: PreDeleteHookType,
annot: map[string]string{"some-other": "annotation"},
expected: false,
},
{
name: "Wrong hook type",
hookType: PreDeleteHookType,
annot: map[string]string{"argocd.argoproj.io/hook": "PostDelete"},
expected: false,
},
{
name: "Nil annotations",
hookType: PreDeleteHookType,
annot: nil,
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
obj := &unstructured.Unstructured{}
obj.SetAnnotations(tt.annot)
result := isHookOfType(obj, tt.hookType)
assert.Equal(t, tt.expected, result)
})
}
}
func TestIsHook(t *testing.T) {
tests := []struct {
name string
annot map[string]string
expected bool
}{
{
name: "ArgoCD PreDelete hook",
annot: map[string]string{"argocd.argoproj.io/hook": "PreDelete"},
expected: true,
},
{
name: "ArgoCD PostDelete hook",
annot: map[string]string{"argocd.argoproj.io/hook": "PostDelete"},
expected: true,
},
{
name: "ArgoCD PreSync hook",
annot: map[string]string{"argocd.argoproj.io/hook": "PreSync"},
expected: true,
},
{
name: "Not a hook",
annot: map[string]string{"some-other": "annotation"},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
obj := &unstructured.Unstructured{}
obj.SetAnnotations(tt.annot)
result := isHook(obj)
assert.Equal(t, tt.expected, result)
})
}
}
func TestIsPreDeleteHook(t *testing.T) {
tests := []struct {
name string
annot map[string]string
expected bool
}{
{
name: "ArgoCD PreDelete hook",
annot: map[string]string{"argocd.argoproj.io/hook": "PreDelete"},
expected: true,
},
{
name: "Helm PreDelete hook",
annot: map[string]string{"helm.sh/hook": "pre-delete"},
expected: true,
},
{
name: "ArgoCD PostDelete hook",
annot: map[string]string{"argocd.argoproj.io/hook": "PostDelete"},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
obj := &unstructured.Unstructured{}
obj.SetAnnotations(tt.annot)
result := isPreDeleteHook(obj)
assert.Equal(t, tt.expected, result)
})
}
}
func TestIsPostDeleteHook(t *testing.T) {
tests := []struct {
name string
annot map[string]string
expected bool
}{
{
name: "ArgoCD PostDelete hook",
annot: map[string]string{"argocd.argoproj.io/hook": "PostDelete"},
expected: true,
},
{
name: "Helm PostDelete hook",
annot: map[string]string{"helm.sh/hook": "post-delete"},
expected: true,
},
{
name: "ArgoCD PreDelete hook",
annot: map[string]string{"argocd.argoproj.io/hook": "PreDelete"},
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
obj := &unstructured.Unstructured{}
obj.SetAnnotations(tt.annot)
result := isPostDeleteHook(obj)
assert.Equal(t, tt.expected, result)
})
}
}

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