Compare commits

..

1 Commits

Author SHA1 Message Date
Dan Garfield
37efa1880b Update argocd_appset_generate.md
Signed-off-by: Dan Garfield <dan@codefresh.io>
2025-03-13 11:42:19 -06:00
1572 changed files with 63191 additions and 102742 deletions

View File

@@ -2,7 +2,7 @@
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: '' title: ''
labels: ['bug', 'triage/pending'] labels: 'bug'
assignees: '' assignees: ''
--- ---
@@ -10,9 +10,9 @@ assignees: ''
Checklist: Checklist:
- [ ] I've searched in the docs and FAQ for my answer: https://bit.ly/argocd-faq. * [ ] I've searched in the docs and FAQ for my answer: https://bit.ly/argocd-faq.
- [ ] I've included steps to reproduce the bug. * [ ] I've included steps to reproduce the bug.
- [ ] I've pasted the output of `argocd version`. * [ ] I've pasted the output of `argocd version`.
**Describe the bug** **Describe the bug**

View File

@@ -2,10 +2,9 @@
name: Enhancement proposal name: Enhancement proposal
about: Propose an enhancement for this project about: Propose an enhancement for this project
title: '' title: ''
labels: ['enhancement', 'triage/pending'] labels: 'enhancement'
assignees: '' assignees: ''
--- ---
# Summary # Summary
What change you think needs making. What change you think needs making.
@@ -16,4 +15,4 @@ Please give examples of your use case, e.g. when would you use this.
# Proposal # Proposal
How do you think this should be implemented? How do you think this should be implemented?

View File

@@ -2,17 +2,17 @@
name: New Dev Tool Request name: New Dev Tool Request
about: This is a request for adding a new tool for setting up a dev environment. about: This is a request for adding a new tool for setting up a dev environment.
title: '' title: ''
labels: ['component:dev-env', 'triage/pending'] labels: ''
assignees: '' assignees: ''
--- ---
Checklist: Checklist:
- [ ] I am willing to maintain this tool, or have another Argo CD maintainer who is. * [ ] I am willing to maintain this tool, or have another Argo CD maintainer who is.
- [ ] I have another Argo CD maintainer who is willing to help maintain this tool (there needs to be at least two maintainers willing to maintain this tool) * [ ] I have another Argo CD maintainer who is willing to help maintain this tool (there needs to be at least two maintainers willing to maintain this tool)
- [ ] I have a lead sponsor who is a core Argo CD maintainer * [ ] I have a lead sponsor who is a core Argo CD maintainer
- [ ] There is a PR which adds said tool - this is so that the maintainers can assess the impact of having this in the tree * [ ] There is a PR which adds said tool - this is so that the maintainers can assess the impact of having this in the tree
- [ ] I have given a motivation why this should be added * [ ] I have given a motivation why this should be added
### The proposer ### The proposer
@@ -24,7 +24,7 @@ Checklist:
### Motivation ### Motivation
<!-- Why this tool would be useful to have in the tree. --> <!-- Why this tool would be useful to have in the tree. -->
### Link to PR (Optional) ### Link to PR (Optional)

View File

@@ -1,11 +1,10 @@
--- ---
name: Security log name: Security log
about: Propose adding security-related logs or tagging existing logs with security fields about: Propose adding security-related logs or tagging existing logs with security fields
title: 'seclog: [Event Description]' title: "seclog: [Event Description]"
labels: ['security', 'triage/pending'] labels: security-log
assignees: '' assignees: notfromstatefarm
--- ---
# Event to be logged # Event to be logged
Specify the event that needs to be logged or existing logs that need to be tagged. Specify the event that needs to be logged or existing logs that need to be tagged.
@@ -17,3 +16,4 @@ What security level should these events be logged under? Refer to https://argo-c
# Common Weakness Enumeration # Common Weakness Enumeration
Is there an associated [CWE](https://cwe.mitre.org/) that could be tagged as well? Is there an associated [CWE](https://cwe.mitre.org/) that could be tagged as well?

3
.github/cherry-pick-bot.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
enabled: true
preservePullRequestTitle: true

View File

@@ -1,15 +0,0 @@
module.exports = {
platform: 'github',
gitAuthor: 'renovate[bot] <renovate[bot]@users.noreply.github.com>',
autodiscover: false,
allowPostUpgradeCommandTemplating: true,
allowedPostUpgradeCommands: ["make mockgen"],
extends: [
"github>argoproj/argo-cd//renovate-presets/commons.json5",
"github>argoproj/argo-cd//renovate-presets/custom-managers/shell.json5",
"github>argoproj/argo-cd//renovate-presets/custom-managers/yaml.json5",
"github>argoproj/argo-cd//renovate-presets/fix/disable-all-updates.json5",
"github>argoproj/argo-cd//renovate-presets/devtool.json5",
"github>argoproj/argo-cd//renovate-presets/docs.json5"
]
}

View File

@@ -10,7 +10,7 @@ updates:
groups: groups:
otel: otel:
patterns: patterns:
- "go.opentelemetry.io/*" - "^go.opentelemetry.io/.*"
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/" directory: "/"
@@ -22,11 +22,10 @@ updates:
schedule: schedule:
interval: "daily" interval: "daily"
# Disabled since this code is rarely used. - package-ecosystem: "npm"
# - package-ecosystem: "npm" directory: "/ui-test/"
# directory: "/ui-test/" schedule:
# schedule: interval: "daily"
# interval: "daily"
- package-ecosystem: "docker" - package-ecosystem: "docker"
directory: "/" directory: "/"
@@ -53,8 +52,7 @@ updates:
schedule: schedule:
interval: "daily" interval: "daily"
# Disabled since this code is rarely used. - package-ecosystem: "docker"
# - package-ecosystem: "docker" directory: "/ui-test/"
# directory: "/ui-test/" schedule:
# schedule: interval: "daily"
# interval: "daily"

View File

@@ -8,7 +8,7 @@ Checklist:
* [ ] Either (a) I've created an [enhancement proposal](https://github.com/argoproj/argo-cd/issues/new/choose) and discussed it with the community, (b) this is a bug fix, or (c) this does not need to be in the release notes. * [ ] Either (a) I've created an [enhancement proposal](https://github.com/argoproj/argo-cd/issues/new/choose) and discussed it with the community, (b) this is a bug fix, or (c) this does not need to be in the release notes.
* [ ] The title of the PR states what changed and the related issues number (used for the release note). * [ ] The title of the PR states what changed and the related issues number (used for the release note).
* [ ] The title of the PR conforms to the [Title of the PR](https://argo-cd.readthedocs.io/en/latest/developer-guide/submit-your-pr/#title-of-the-pr) * [ ] The title of the PR conforms to the [Toolchain Guide](https://argo-cd.readthedocs.io/en/latest/developer-guide/toolchain-guide/#title-of-the-pr)
* [ ] I've included "Closes [ISSUE #]" or "Fixes [ISSUE #]" in the description to automatically close the associated issue. * [ ] I've included "Closes [ISSUE #]" or "Fixes [ISSUE #]" in the description to automatically close the associated issue.
* [ ] I've updated both the CLI and UI to expose my feature, or I plan to submit a second PR with them. * [ ] I've updated both the CLI and UI to expose my feature, or I plan to submit a second PR with them.
* [ ] Does this PR require documentation updates? * [ ] Does this PR require documentation updates?

View File

@@ -16,7 +16,7 @@
## image-reuse.yaml ## image-reuse.yaml
- The reusable workflow can be used to publish or build images with multiple container registries(Quay,GHCR, dockerhub), and then sign them with cosign when an image is published. - The resuable workflow can be used to publish or build images with multiple container registries(Quay,GHCR, dockerhub), and then sign them with cosign when an image is published.
- A GO version `must` be specified e.g. 1.21 - A GO version `must` be specified e.g. 1.21
- The image name for each registry *must* contain the tag. Note: multiple tags are allowed for each registry using a CSV type. - The image name for each registry *must* contain the tag. Note: multiple tags are allowed for each registry using a CSV type.
- Multiple platforms can be specified e.g. linux/amd64,linux/arm64 - Multiple platforms can be specified e.g. linux/amd64,linux/arm64

View File

@@ -37,7 +37,7 @@ jobs:
working-directory: /home/runner/go/src/github.com/argoproj/argo-cd working-directory: /home/runner/go/src/github.com/argoproj/argo-cd
- name: Setup Golang - name: Setup Golang
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
with: with:
go-version: ${{ env.GOLANG_VERSION }} go-version: ${{ env.GOLANG_VERSION }}
- name: Add ~/go/bin to PATH - 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 rsync -a --exclude=.git /home/runner/go/src/github.com/argoproj/argo-cd/ ../argo-cd
- name: Create pull request - name: Create pull request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5
with: with:
commit-message: "Bump major version to ${{ steps.get-target-version.outputs.TARGET_VERSION }}" 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 }}" title: "Bump major version to ${{ steps.get-target-version.outputs.TARGET_VERSION }}"

View File

@@ -1,114 +0,0 @@
name: Cherry Pick Single
on:
workflow_call:
inputs:
merge_commit_sha:
required: true
type: string
description: "The merge commit SHA to cherry-pick"
version_number:
required: true
type: string
description: "The version number (from cherry-pick/ label)"
pr_number:
required: true
type: string
description: "The original PR number"
pr_title:
required: true
type: string
description: "The original PR title"
secrets:
CHERRYPICK_APP_ID:
required: true
CHERRYPICK_APP_PRIVATE_KEY:
required: true
jobs:
cherry-pick:
name: Cherry Pick to ${{ inputs.version_number }}
runs-on: ubuntu-latest
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
with:
app-id: ${{ secrets.CHERRYPICK_APP_ID }}
private-key: ${{ secrets.CHERRYPICK_APP_PRIVATE_KEY }}
- name: Checkout repository
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
with:
fetch-depth: 0
token: ${{ steps.generate-token.outputs.token }}
- name: Configure Git
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
- name: Cherry pick commit
id: cherry-pick
run: |
set -e
MERGE_COMMIT="${{ inputs.merge_commit_sha }}"
TARGET_BRANCH="release-${{ inputs.version_number }}"
echo "🍒 Cherry-picking commit $MERGE_COMMIT to branch $TARGET_BRANCH"
# Check if target branch exists
if ! git show-ref --verify --quiet "refs/remotes/origin/$TARGET_BRANCH"; then
echo "❌ Target branch '$TARGET_BRANCH' does not exist"
exit 1
fi
# Create new branch for cherry-pick
CHERRY_PICK_BRANCH="cherry-pick-${{ inputs.pr_number }}-to-${TARGET_BRANCH}"
git checkout -b "$CHERRY_PICK_BRANCH" "origin/$TARGET_BRANCH"
# Perform cherry-pick
if git cherry-pick -m 1 "$MERGE_COMMIT"; then
echo "✅ Cherry-pick successful"
# Extract Signed-off-by from the cherry-pick commit
SIGNOFF=$(git log -1 --pretty=format:"%B" | grep -E '^Signed-off-by:' || echo "")
# Push the new branch
git push origin "$CHERRY_PICK_BRANCH"
# Save data for PR creation
echo "branch_name=$CHERRY_PICK_BRANCH" >> "$GITHUB_OUTPUT"
echo "signoff=$SIGNOFF" >> "$GITHUB_OUTPUT"
echo "target_branch=$TARGET_BRANCH" >> "$GITHUB_OUTPUT"
else
echo "❌ Cherry-pick failed due to conflicts"
git cherry-pick --abort
exit 1
fi
- name: Create Pull Request
run: |
# Create cherry-pick PR
gh pr create \
--title "${{ inputs.pr_title }} (cherry-pick #${{ inputs.pr_number }} for ${{ inputs.version_number }})" \
--body "Cherry-picked ${{ inputs.pr_title }} (#${{ inputs.pr_number }})
${{ steps.cherry-pick.outputs.signoff }}" \
--base "${{ steps.cherry-pick.outputs.target_branch }}" \
--head "${{ steps.cherry-pick.outputs.branch_name }}"
# Comment on original PR
gh pr comment ${{ inputs.pr_number }} \
--body "🍒 Cherry-pick PR created for ${{ inputs.version_number }}: #$(gh pr list --head ${{ steps.cherry-pick.outputs.branch_name }} --json number --jq '.[0].number')"
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}
- name: Comment on failure
if: failure()
run: |
gh pr comment ${{ inputs.pr_number }} \
--body "❌ Cherry-pick failed for ${{ inputs.version_number }}. Please check the workflow logs for details."
env:
GH_TOKEN: ${{ steps.generate-token.outputs.token }}

View File

@@ -1,53 +0,0 @@
name: Cherry Pick
on:
pull_request_target:
branches:
- master
types: ["labeled", "closed"]
jobs:
find-labels:
name: Find Cherry Pick Labels
if: |
github.event.pull_request.merged == true && (
(github.event.action == 'labeled' && startsWith(github.event.label.name, 'cherry-pick/')) ||
(github.event.action == 'closed' && contains(toJSON(github.event.pull_request.labels.*.name), 'cherry-pick/'))
)
runs-on: ubuntu-latest
outputs:
labels: ${{ steps.extract-labels.outputs.labels }}
steps:
- name: Extract cherry-pick labels
id: extract-labels
run: |
if [[ "${{ github.event.action }}" == "labeled" ]]; then
# Label was just added - use it directly
LABEL_NAME="${{ github.event.label.name }}"
VERSION="${LABEL_NAME#cherry-pick/}"
CHERRY_PICK_DATA='[{"label":"'$LABEL_NAME'","version":"'$VERSION'"}]'
else
# PR was closed - find all cherry-pick labels
CHERRY_PICK_DATA=$(echo '${{ toJSON(github.event.pull_request.labels) }}' | jq -c '[.[] | select(.name | startswith("cherry-pick/")) | {label: .name, version: (.name | sub("cherry-pick/"; ""))}]')
fi
echo "labels=$CHERRY_PICK_DATA" >> "$GITHUB_OUTPUT"
echo "Found cherry-pick data: $CHERRY_PICK_DATA"
cherry-pick:
name: Cherry Pick
needs: find-labels
if: needs.find-labels.outputs.labels != '[]'
strategy:
matrix:
include: ${{ fromJSON(needs.find-labels.outputs.labels) }}
fail-fast: false
uses: ./.github/workflows/cherry-pick-single.yml
with:
merge_commit_sha: ${{ github.event.pull_request.merge_commit_sha }}
version_number: ${{ matrix.version }}
pr_number: ${{ github.event.pull_request.number }}
pr_title: ${{ github.event.pull_request.title }}
secrets:
CHERRYPICK_APP_ID: ${{ vars.CHERRYPICK_APP_ID }}
CHERRYPICK_APP_PRIVATE_KEY: ${{ secrets.CHERRYPICK_APP_PRIVATE_KEY }}

View File

@@ -14,11 +14,11 @@ on:
env: env:
# Golang version to use across CI steps # Golang version to use across CI steps
# renovate: datasource=golang-version packageName=golang # renovate: datasource=golang-version packageName=golang
GOLANG_VERSION: '1.25.6' GOLANG_VERSION: '1.24.1'
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} cancel-in-progress: true
permissions: permissions:
contents: read contents: read
@@ -32,7 +32,7 @@ jobs:
docs: ${{ steps.filter.outputs.docs_any_changed }} docs: ${{ steps.filter.outputs.docs_any_changed }}
steps: steps:
- uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0 - uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
- uses: tj-actions/changed-files@24d32ffd492484c1d75e0c0b894501ddb9d30d62 # v47.0.0 - uses: tj-actions/changed-files@bab30c2299617f6615ec02a68b9a40d10bd21366 # v45.0.5
id: filter id: filter
with: with:
# Any file which is not under docs/, ui/ or is not a markdown file is counted as a backend file # Any file which is not under docs/, ui/ or is not a markdown file is counted as a backend file
@@ -57,7 +57,7 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0 uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
- name: Setup Golang - name: Setup Golang
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
with: with:
go-version: ${{ env.GOLANG_VERSION }} go-version: ${{ env.GOLANG_VERSION }}
- name: Download all Go modules - name: Download all Go modules
@@ -78,11 +78,11 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0 uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
- name: Setup Golang - name: Setup Golang
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
with: with:
go-version: ${{ env.GOLANG_VERSION }} go-version: ${{ env.GOLANG_VERSION }}
- name: Restore go build cache - name: Restore go build cache
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with: with:
path: ~/.cache/go-build path: ~/.cache/go-build
key: ${{ runner.os }}-go-build-v1-${{ github.run_id }} key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
@@ -105,14 +105,14 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0 uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
- name: Setup Golang - name: Setup Golang
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
with: with:
go-version: ${{ env.GOLANG_VERSION }} go-version: ${{ env.GOLANG_VERSION }}
- name: Run golangci-lint - name: Run golangci-lint
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0 uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 # v6.1.1
with: with:
# renovate: datasource=go packageName=github.com/golangci/golangci-lint versioning=regex:^v(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)?$ # renovate: datasource=go packageName=github.com/golangci/golangci-lint versioning=regex:^v(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)?$
version: v2.4.0 version: v1.64.7
args: --verbose args: --verbose
test-go: test-go:
@@ -133,7 +133,7 @@ jobs:
- name: Create symlink in GOPATH - name: Create symlink in GOPATH
run: ln -s $(pwd) ~/go/src/github.com/argoproj/argo-cd run: ln -s $(pwd) ~/go/src/github.com/argoproj/argo-cd
- name: Setup Golang - name: Setup Golang
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
with: with:
go-version: ${{ env.GOLANG_VERSION }} go-version: ${{ env.GOLANG_VERSION }}
- name: Install required packages - name: Install required packages
@@ -153,7 +153,7 @@ jobs:
run: | run: |
echo "/usr/local/bin" >> $GITHUB_PATH echo "/usr/local/bin" >> $GITHUB_PATH
- name: Restore go build cache - name: Restore go build cache
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with: with:
path: ~/.cache/go-build path: ~/.cache/go-build
key: ${{ runner.os }}-go-build-v1-${{ github.run_id }} key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
@@ -174,7 +174,7 @@ jobs:
- name: Run all unit tests - name: Run all unit tests
run: make test-local run: make test-local
- name: Generate test results artifacts - name: Generate test results artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with: with:
name: test-results name: test-results
path: test-results path: test-results
@@ -197,7 +197,7 @@ jobs:
- name: Create symlink in GOPATH - name: Create symlink in GOPATH
run: ln -s $(pwd) ~/go/src/github.com/argoproj/argo-cd run: ln -s $(pwd) ~/go/src/github.com/argoproj/argo-cd
- name: Setup Golang - name: Setup Golang
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
with: with:
go-version: ${{ env.GOLANG_VERSION }} go-version: ${{ env.GOLANG_VERSION }}
- name: Install required packages - name: Install required packages
@@ -217,7 +217,7 @@ jobs:
run: | run: |
echo "/usr/local/bin" >> $GITHUB_PATH echo "/usr/local/bin" >> $GITHUB_PATH
- name: Restore go build cache - name: Restore go build cache
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with: with:
path: ~/.cache/go-build path: ~/.cache/go-build
key: ${{ runner.os }}-go-build-v1-${{ github.run_id }} key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
@@ -238,7 +238,7 @@ jobs:
- name: Run all unit tests - name: Run all unit tests
run: make test-race-local run: make test-race-local
- name: Generate test results artifacts - name: Generate test results artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with: with:
name: race-results name: race-results
path: test-results/ path: test-results/
@@ -253,7 +253,7 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0 uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
- name: Setup Golang - name: Setup Golang
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
with: with:
go-version: ${{ env.GOLANG_VERSION }} go-version: ${{ env.GOLANG_VERSION }}
- name: Create symlink in GOPATH - name: Create symlink in GOPATH
@@ -305,13 +305,13 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0 uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
- name: Setup NodeJS - name: Setup NodeJS
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with: with:
# renovate: datasource=node-version packageName=node versioning=node # renovate: datasource=node-version packageName=node versioning=node
node-version: '22.9.0' node-version: '22.9.0'
- name: Restore node dependency cache - name: Restore node dependency cache
id: cache-dependencies id: cache-dependencies
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with: with:
path: ui/node_modules path: ui/node_modules
key: ${{ runner.os }}-node-dep-v2-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-node-dep-v2-${{ hashFiles('**/yarn.lock') }}
@@ -333,15 +333,6 @@ jobs:
run: yarn lint run: yarn lint
working-directory: ui/ working-directory: ui/
shellcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.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
test ! -s sc.log
analyze: analyze:
name: Process & analyze test artifacts name: Process & analyze test artifacts
if: ${{ needs.changes.outputs.backend == 'true' || needs.changes.outputs.frontend == 'true' }} if: ${{ needs.changes.outputs.backend == 'true' || needs.changes.outputs.frontend == 'true' }}
@@ -360,7 +351,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Restore node dependency cache - name: Restore node dependency cache
id: cache-dependencies id: cache-dependencies
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with: with:
path: ui/node_modules path: ui/node_modules
key: ${{ runner.os }}-node-dep-v2-${{ hashFiles('**/yarn.lock') }} key: ${{ runner.os }}-node-dep-v2-${{ hashFiles('**/yarn.lock') }}
@@ -368,12 +359,12 @@ jobs:
run: | run: |
rm -rf ui/node_modules/argo-ui/node_modules rm -rf ui/node_modules/argo-ui/node_modules
- name: Get e2e code coverage - name: Get e2e code coverage
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with: with:
name: e2e-code-coverage name: e2e-code-coverage
path: e2e-code-coverage path: e2e-code-coverage
- name: Get unit test code coverage - name: Get unit test code coverage
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with: with:
name: test-results name: test-results
path: test-results path: test-results
@@ -385,15 +376,15 @@ jobs:
run: | 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 go tool covdata percent -i=test-results,e2e-code-coverage/applicationset-controller,e2e-code-coverage/repo-server,e2e-code-coverage/app-controller,e2e-code-coverage/commit-server -o test-results/full-coverage.out
- name: Upload code coverage information to codecov.io - name: Upload code coverage information to codecov.io
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
with: with:
files: test-results/full-coverage.out file: test-results/full-coverage.out
fail_ci_if_error: true fail_ci_if_error: true
env: env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- name: Upload test results to Codecov - name: Upload test results to Codecov
if: github.ref == 'refs/heads/master' && github.event_name == 'push' && github.repository == 'argoproj/argo-cd' if: github.ref == 'refs/heads/master' && github.event_name == 'push' && github.repository == 'argoproj/argo-cd'
uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1 uses: codecov/test-results-action@9739113ad922ea0a9abb4b2c0f8bf6a4aa8ef820 # v1.0.1
with: with:
file: test-results/junit.xml file: test-results/junit.xml
fail_ci_if_error: true fail_ci_if_error: true
@@ -402,31 +393,31 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
uses: SonarSource/sonarqube-scan-action@1a6d90ebcb0e6a6b1d87e37ba693fe453195ae25 # v5.3.1 uses: SonarSource/sonarqube-scan-action@bfd4e558cda28cda6b5defafb9232d191be8c203 # v4.2.1
if: env.sonar_secret != '' if: env.sonar_secret != ''
test-e2e: test-e2e:
name: Run end-to-end tests name: Run end-to-end tests
if: ${{ needs.changes.outputs.backend == 'true' }} if: ${{ needs.changes.outputs.backend == 'true' }}
runs-on: oracle-vm-16cpu-64gb-x86-64 runs-on: ubuntu-22.04
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
# latest: true means that this version mush upload the coverage report to codecov.io # 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. # We designate the latest version because we only collect code coverage for that version.
k3s: k3s:
- version: v1.34.2
latest: true
- version: v1.33.1
latest: false
- version: v1.32.1 - version: v1.32.1
latest: false latest: true
- version: v1.31.0 - version: v1.31.0
latest: false latest: false
- version: v1.30.4
latest: false
- version: v1.29.8
latest: false
needs: needs:
- build-go - build-go
- changes - changes
env: env:
GOPATH: /home/ubuntu/go GOPATH: /home/runner/go
ARGOCD_FAKE_IN_CLUSTER: 'true' ARGOCD_FAKE_IN_CLUSTER: 'true'
ARGOCD_SSH_DATA_PATH: '/tmp/argo-e2e/app/config/ssh' ARGOCD_SSH_DATA_PATH: '/tmp/argo-e2e/app/config/ssh'
ARGOCD_TLS_DATA_PATH: '/tmp/argo-e2e/app/config/tls' ARGOCD_TLS_DATA_PATH: '/tmp/argo-e2e/app/config/tls'
@@ -449,7 +440,7 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0 uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
- name: Setup Golang - name: Setup Golang
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
with: with:
go-version: ${{ env.GOLANG_VERSION }} go-version: ${{ env.GOLANG_VERSION }}
- name: GH actions workaround - Kill XSP4 process - name: GH actions workaround - Kill XSP4 process
@@ -462,19 +453,19 @@ jobs:
set -x set -x
curl -sfL https://get.k3s.io | sh - curl -sfL https://get.k3s.io | sh -
sudo chmod -R a+rw /etc/rancher/k3s sudo chmod -R a+rw /etc/rancher/k3s
sudo mkdir -p $HOME/.kube && sudo chown -R ubuntu $HOME/.kube sudo mkdir -p $HOME/.kube && sudo chown -R runner $HOME/.kube
sudo k3s kubectl config view --raw > $HOME/.kube/config sudo k3s kubectl config view --raw > $HOME/.kube/config
sudo chown ubuntu $HOME/.kube/config sudo chown runner $HOME/.kube/config
sudo chmod go-r $HOME/.kube/config sudo chmod go-r $HOME/.kube/config
kubectl version kubectl version
- name: Restore go build cache - name: Restore go build cache
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4 uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with: with:
path: ~/.cache/go-build path: ~/.cache/go-build
key: ${{ runner.os }}-go-build-v1-${{ github.run_id }} key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
- name: Add ~/go/bin to PATH - name: Add ~/go/bin to PATH
run: | run: |
echo "/home/ubuntu/go/bin" >> $GITHUB_PATH echo "/home/runner/go/bin" >> $GITHUB_PATH
- name: Add /usr/local/bin to PATH - name: Add /usr/local/bin to PATH
run: | run: |
echo "/usr/local/bin" >> $GITHUB_PATH echo "/usr/local/bin" >> $GITHUB_PATH
@@ -494,13 +485,13 @@ jobs:
git config --global user.email "john.doe@example.com" git config --global user.email "john.doe@example.com"
- name: Pull Docker image required for tests - name: Pull Docker image required for tests
run: | run: |
docker pull ghcr.io/dexidp/dex:v2.43.0 docker pull ghcr.io/dexidp/dex:v2.41.1
docker pull argoproj/argo-cd-ci-builder:v1.0.0 docker pull argoproj/argo-cd-ci-builder:v1.0.0
docker pull redis:8.2.2-alpine docker pull redis:7.2.7-alpine
- name: Create target directory for binaries in the build-process - name: Create target directory for binaries in the build-process
run: | run: |
mkdir -p dist mkdir -p dist
chown ubuntu dist chown runner dist
- name: Run E2E server and wait for it being available - name: Run E2E server and wait for it being available
timeout-minutes: 30 timeout-minutes: 30
run: | run: |
@@ -526,13 +517,13 @@ jobs:
goreman run stop-all || echo "goreman trouble" goreman run stop-all || echo "goreman trouble"
sleep 30 sleep 30
- name: Upload e2e coverage report - name: Upload e2e coverage report
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with: with:
name: e2e-code-coverage name: e2e-code-coverage
path: /tmp/coverage path: /tmp/coverage
if: ${{ matrix.k3s.latest }} if: ${{ matrix.k3s.latest }}
- name: Upload e2e-server logs - name: Upload e2e-server logs
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with: with:
name: e2e-server-k8s${{ matrix.k3s.version }}.log name: e2e-server-k8s${{ matrix.k3s.version }}.log
path: /tmp/e2e-server.log path: /tmp/e2e-server.log

View File

@@ -33,7 +33,7 @@ jobs:
# Use correct go version. https://github.com/github/codeql-action/issues/1842#issuecomment-1704398087 # Use correct go version. https://github.com/github/codeql-action/issues/1842#issuecomment-1704398087
- name: Setup Golang - name: Setup Golang
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
with: with:
go-version-file: go.mod go-version-file: go.mod

View File

@@ -67,16 +67,15 @@ jobs:
if: ${{ github.ref_type != 'tag'}} if: ${{ github.ref_type != 'tag'}}
- name: Setup Golang - name: Setup Golang
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
with: with:
go-version: ${{ inputs.go-version }} go-version: ${{ inputs.go-version }}
cache: false
- name: Install cosign - name: Install cosign
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0 uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0
- uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 - uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 - uses: docker/setup-buildx-action@6524bf65af31da8d45b59e8c27de4bd072b392f5 # v3.8.0
- name: Setup tags for container image as a CSV type - name: Setup tags for container image as a CSV type
run: | run: |
@@ -103,7 +102,7 @@ jobs:
echo 'EOF' >> $GITHUB_ENV echo 'EOF' >> $GITHUB_ENV
- name: Login to Quay.io - name: Login to Quay.io
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with: with:
registry: quay.io registry: quay.io
username: ${{ secrets.quay_username }} username: ${{ secrets.quay_username }}
@@ -111,7 +110,7 @@ jobs:
if: ${{ inputs.quay_image_name && inputs.push }} if: ${{ inputs.quay_image_name && inputs.push }}
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ secrets.ghcr_username }} username: ${{ secrets.ghcr_username }}
@@ -119,7 +118,7 @@ jobs:
if: ${{ inputs.ghcr_image_name && inputs.push }} if: ${{ inputs.ghcr_image_name && inputs.push }}
- name: Login to dockerhub Container Registry - name: Login to dockerhub Container Registry
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with: with:
username: ${{ secrets.docker_username }} username: ${{ secrets.docker_username }}
password: ${{ secrets.docker_password }} password: ${{ secrets.docker_password }}
@@ -142,7 +141,7 @@ jobs:
- name: Build and push container image - name: Build and push container image
id: image id: image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 #v6.18.0 uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 #v6.10.0
with: with:
context: . context: .
platforms: ${{ inputs.platforms }} platforms: ${{ inputs.platforms }}

View File

@@ -53,7 +53,7 @@ jobs:
with: with:
# Note: cannot use env variables to set go-version (https://docs.github.com/en/actions/using-workflows/reusing-workflows#limitations) # 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 # renovate: datasource=golang-version packageName=golang
go-version: 1.25.6 go-version: 1.24.1
platforms: ${{ needs.set-vars.outputs.platforms }} platforms: ${{ needs.set-vars.outputs.platforms }}
push: false push: false
@@ -70,7 +70,7 @@ jobs:
ghcr_image_name: ghcr.io/argoproj/argo-cd/argocd:${{ needs.set-vars.outputs.image-tag }} 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) # 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 # renovate: datasource=golang-version packageName=golang
go-version: 1.25.6 go-version: 1.24.1
platforms: ${{ needs.set-vars.outputs.platforms }} platforms: ${{ needs.set-vars.outputs.platforms }}
push: true push: true
secrets: secrets:
@@ -88,7 +88,7 @@ jobs:
packages: write # for uploading attestations. (https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#known-issues) 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' && github.event_name == 'push' }} 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 # 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 uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0
with: with:
image: ghcr.io/argoproj/argo-cd/argocd image: ghcr.io/argoproj/argo-cd/argocd
digest: ${{ needs.build-and-publish.outputs.image-digest }} digest: ${{ needs.build-and-publish.outputs.image-digest }}

View File

@@ -64,7 +64,7 @@ jobs:
git stash pop git stash pop
- name: Create pull request - name: Create pull request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5
with: with:
commit-message: "Bump version to ${{ inputs.TARGET_VERSION }}" commit-message: "Bump version to ${{ inputs.TARGET_VERSION }}"
title: "Bump version to ${{ inputs.TARGET_VERSION }} on ${{ inputs.TARGET_BRANCH }} branch" title: "Bump version to ${{ inputs.TARGET_VERSION }} on ${{ inputs.TARGET_BRANCH }} branch"

View File

@@ -11,7 +11,7 @@ permissions: {}
env: env:
# renovate: datasource=golang-version packageName=golang # 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.24.1' # Note: go-version must also be set in job argocd-image.with.go-version
jobs: jobs:
argocd-image: argocd-image:
@@ -25,49 +25,13 @@ jobs:
quay_image_name: quay.io/argoproj/argocd:${{ github.ref_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) # 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 # renovate: datasource=golang-version packageName=golang
go-version: 1.25.6 go-version: 1.24.1
platforms: linux/amd64,linux/arm64,linux/s390x,linux/ppc64le platforms: linux/amd64,linux/arm64,linux/s390x,linux/ppc64le
push: true push: true
secrets: secrets:
quay_username: ${{ secrets.RELEASE_QUAY_USERNAME }} quay_username: ${{ secrets.RELEASE_QUAY_USERNAME }}
quay_password: ${{ secrets.RELEASE_QUAY_TOKEN }} quay_password: ${{ secrets.RELEASE_QUAY_TOKEN }}
setup-variables:
name: Setup Release Variables
if: github.repository == 'argoproj/argo-cd'
runs-on: ubuntu-22.04
outputs:
is_pre_release: ${{ steps.var.outputs.is_pre_release }}
is_latest_release: ${{ steps.var.outputs.is_latest_release }}
steps:
- name: Checkout code
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup variables
id: var
run: |
set -xue
# Fetch all tag information
git fetch --prune --tags --force
LATEST_RELEASE_TAG=$(git -c 'versionsort.suffix=-rc' tag --list --sort=version:refname | grep -v '-' | tail -n1)
PRE_RELEASE=false
# Check if latest tag is a pre-release
if echo ${{ github.ref_name }} | grep -E -- '-rc[0-9]+$';then
PRE_RELEASE=true
fi
IS_LATEST=false
# Ensure latest release tag matches github.ref_name
if [[ $LATEST_RELEASE_TAG == ${{ github.ref_name }} ]];then
IS_LATEST=true
fi
echo "is_pre_release=$PRE_RELEASE" >> $GITHUB_OUTPUT
echo "is_latest_release=$IS_LATEST" >> $GITHUB_OUTPUT
argocd-image-provenance: argocd-image-provenance:
needs: [argocd-image] needs: [argocd-image]
permissions: permissions:
@@ -76,7 +40,7 @@ jobs:
packages: write # for uploading attestations. (https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#known-issues) 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 # 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' if: github.repository == 'argoproj/argo-cd'
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0 uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0
with: with:
image: quay.io/argoproj/argocd image: quay.io/argoproj/argocd
digest: ${{ needs.argocd-image.outputs.image-digest }} digest: ${{ needs.argocd-image.outputs.image-digest }}
@@ -86,17 +50,15 @@ jobs:
goreleaser: goreleaser:
needs: needs:
- setup-variables
- argocd-image - argocd-image
- argocd-image-provenance - argocd-image-provenance
permissions: permissions:
contents: write # used for uploading assets contents: write # used for uploading assets
if: github.repository == 'argoproj/argo-cd' if: github.repository == 'argoproj/argo-cd'
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
env:
GORELEASER_MAKE_LATEST: ${{ needs.setup-variables.outputs.is_latest_release }}
outputs: outputs:
hashes: ${{ steps.hash.outputs.hashes }} hashes: ${{ steps.hash.outputs.hashes }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0 uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
@@ -108,16 +70,14 @@ jobs:
run: git fetch --force --tags run: git fetch --force --tags
- name: Setup Golang - name: Setup Golang
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
with: with:
go-version: ${{ env.GOLANG_VERSION }} go-version: ${{ env.GOLANG_VERSION }}
cache: false
- name: Set GORELEASER_PREVIOUS_TAG # Workaround, GoReleaser uses 'git-describe' to determine a previous tag. Our tags are created in release branches. - name: Set GORELEASER_PREVIOUS_TAG # Workaround, GoReleaser uses 'git-describe' to determine a previous tag. Our tags are created in release branches.
run: | run: |
set -xue set -xue
GORELEASER_PREVIOUS_TAG=$(go run hack/get-previous-release/get-previous-version-for-release-notes.go ${{ github.ref_name }}) || exit 1 echo "GORELEASER_PREVIOUS_TAG=$(go run hack/get-previous-release/get-previous-version-for-release-notes.go ${{ github.ref_name }})" >> $GITHUB_ENV
echo "GORELEASER_PREVIOUS_TAG=$GORELEASER_PREVIOUS_TAG" >> $GITHUB_ENV
- name: Set environment variables for ldflags - name: Set environment variables for ldflags
id: set_ldflag id: set_ldflag
@@ -134,7 +94,7 @@ jobs:
tool-cache: false tool-cache: false
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 uses: goreleaser/goreleaser-action@9ed2f89a662bf1735a48bc8557fd212fa902bebf # v6.1.0
id: run-goreleaser id: run-goreleaser
with: with:
version: latest version: latest
@@ -166,7 +126,7 @@ jobs:
contents: write # Needed for release uploads contents: write # Needed for release uploads
if: github.repository == 'argoproj/argo-cd' 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 # 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 uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
with: with:
base64-subjects: '${{ needs.goreleaser.outputs.hashes }}' base64-subjects: '${{ needs.goreleaser.outputs.hashes }}'
provenance-name: 'argocd-cli.intoto.jsonl' provenance-name: 'argocd-cli.intoto.jsonl'
@@ -180,7 +140,7 @@ jobs:
permissions: permissions:
contents: write # Needed for release uploads contents: write # Needed for release uploads
outputs: outputs:
hashes: ${{ steps.sbom-hash.outputs.hashes }} hashes: ${{ steps.sbom-hash.outputs.hashes}}
if: github.repository == 'argoproj/argo-cd' if: github.repository == 'argoproj/argo-cd'
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
@@ -191,10 +151,9 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Golang - name: Setup Golang
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a # v5.2.0
with: with:
go-version: ${{ env.GOLANG_VERSION }} go-version: ${{ env.GOLANG_VERSION }}
cache: false
- name: Generate SBOM (spdx) - name: Generate SBOM (spdx)
id: spdx-builder id: spdx-builder
@@ -236,7 +195,7 @@ jobs:
echo "hashes=$(sha256sum /tmp/sbom.tar.gz | base64 -w0)" >> "$GITHUB_OUTPUT" echo "hashes=$(sha256sum /tmp/sbom.tar.gz | base64 -w0)" >> "$GITHUB_OUTPUT"
- name: Upload SBOM - name: Upload SBOM
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 # v2.3.3 uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
@@ -251,7 +210,7 @@ jobs:
contents: write # Needed for release uploads contents: write # Needed for release uploads
if: github.repository == 'argoproj/argo-cd' 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 # 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 uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
with: with:
base64-subjects: '${{ needs.generate-sbom.outputs.hashes }}' base64-subjects: '${{ needs.generate-sbom.outputs.hashes }}'
provenance-name: 'argocd-sbom.intoto.jsonl' provenance-name: 'argocd-sbom.intoto.jsonl'
@@ -259,7 +218,6 @@ jobs:
post-release: post-release:
needs: needs:
- setup-variables
- argocd-image - argocd-image
- goreleaser - goreleaser
- generate-sbom - generate-sbom
@@ -268,8 +226,6 @@ jobs:
pull-requests: write # Needed to create PR for VERSION update. pull-requests: write # Needed to create PR for VERSION update.
if: github.repository == 'argoproj/argo-cd' if: github.repository == 'argoproj/argo-cd'
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
env:
TAG_STABLE: ${{ needs.setup-variables.outputs.is_latest_release }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0 uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
@@ -283,6 +239,27 @@ jobs:
git config --global user.email 'ci@argoproj.com' git config --global user.email 'ci@argoproj.com'
git config --global user.name 'CI' git config --global user.name 'CI'
- name: Check if tag is the latest version and not a pre-release
run: |
set -xue
# Fetch all tag information
git fetch --prune --tags --force
LATEST_TAG=$(git -c 'versionsort.suffix=-rc' tag --list --sort=version:refname | tail -n1)
PRE_RELEASE=false
# Check if latest tag is a pre-release
if echo $LATEST_TAG | grep -E -- '-rc[0-9]+$';then
PRE_RELEASE=true
fi
# Ensure latest tag matches github.ref_name & not a pre-release
if [[ $LATEST_TAG == ${{ github.ref_name }} ]] && [[ $PRE_RELEASE != 'true' ]];then
echo "TAG_STABLE=true" >> $GITHUB_ENV
else
echo "TAG_STABLE=false" >> $GITHUB_ENV
fi
- name: Update stable tag to latest version - name: Update stable tag to latest version
run: | run: |
git tag -f stable ${{ github.ref_name }} git tag -f stable ${{ github.ref_name }}
@@ -316,7 +293,7 @@ jobs:
if: ${{ env.UPDATE_VERSION == 'true' }} if: ${{ env.UPDATE_VERSION == 'true' }}
- name: Create PR to update VERSION on master branch - name: Create PR to update VERSION on master branch
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8 uses: peter-evans/create-pull-request@5e914681df9dc83aa4e4905692ca88beb2f9e91f # v7.0.5
with: with:
commit-message: Bump version in master commit-message: Bump version in master
title: 'chore: Bump version in master' title: 'chore: Bump version in master'

View File

@@ -1,38 +0,0 @@
name: Renovate
on:
schedule:
- cron: '0 * * * *'
workflow_dispatch: {}
permissions:
contents: read
jobs:
renovate:
runs-on: ubuntu-latest
steps:
- name: Get token
id: get_token
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1
with:
app-id: ${{ vars.RENOVATE_APP_ID }}
private-key: ${{ secrets.RENOVATE_APP_PRIVATE_KEY }}
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # 6.0.1
# Some codegen commands require Go to be setup
- name: Setup Golang
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
# renovate: datasource=golang-version packageName=golang
go-version: 1.25.6
- name: Self-hosted Renovate
uses: renovatebot/github-action@f8af9272cd94a4637c29f60dea8731afd3134473 #43.0.12
with:
configurationFile: .github/configs/renovate-config.js
token: '${{ steps.get_token.outputs.token }}'
env:
LOG_LEVEL: 'debug'
RENOVATE_REPOSITORIES: '${{ github.repository }}'

View File

@@ -35,7 +35,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: "Run analysis" - name: "Run analysis"
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
with: with:
results_file: results.sarif results_file: results.sarif
results_format: sarif results_format: sarif
@@ -54,7 +54,7 @@ jobs:
# Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF
# format to the repository Actions tab. # format to the repository Actions tab.
- name: "Upload artifact" - name: "Upload artifact"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with: with:
name: SARIF file name: SARIF file
path: results.sarif path: results.sarif

5
.gitignore vendored
View File

@@ -8,7 +8,6 @@ ui/dist/app/*
!ui/dist/app/gitkeep !ui/dist/app/gitkeep
site/ site/
*.iml *.iml
.tilt-bin/
# delve debug binaries # delve debug binaries
cmd/**/debug cmd/**/debug
debug.test debug.test
@@ -20,7 +19,6 @@ node_modules/
.kube/ .kube/
./test/cmp/*.sock ./test/cmp/*.sock
.envrc.remote .envrc.remote
.mirrord/
.*.swp .*.swp
rerunreport.txt rerunreport.txt
@@ -29,6 +27,3 @@ cmd/argocd/argocd
cmd/argocd-application-controller/argocd-application-controller cmd/argocd-application-controller/argocd-application-controller
cmd/argocd-repo-server/argocd-repo-server cmd/argocd-repo-server/argocd-repo-server
cmd/argocd-server/argocd-server cmd/argocd-server/argocd-server
# ignore generated `.argocd-helm-dep-up` marker file; this should not be committed to git
reposerver/repository/testdata/**/.argocd-helm-dep-up

21
.gitpod.Dockerfile vendored Normal file
View File

@@ -0,0 +1,21 @@
FROM gitpod/workspace-full@sha256:a47a68ee7f9da10cd889ccce4661bc73f2c0d5a98d3d087e8bdfc0230b27964c
USER root
RUN curl -o /usr/local/bin/kubectl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && \
chmod +x /usr/local/bin/kubectl
RUN curl -L https://github.com/kubernetes-sigs/kubebuilder/releases/download/v2.3.1/kubebuilder_2.3.1_$(go env GOOS)_$(go env GOARCH).tar.gz | \
tar -xz -C /tmp/ && mv /tmp/kubebuilder_2.3.1_$(go env GOOS)_$(go env GOARCH) /usr/local/kubebuilder
ENV GOCACHE=/go-build-cache
RUN apt-get install redis-server -y
RUN go install github.com/mattn/goreman@latest
RUN chown -R gitpod:gitpod /go-build-cache
USER gitpod
ENV ARGOCD_REDIS_LOCAL=true
ENV KUBECONFIG=/tmp/kubeconfig

6
.gitpod.yml Normal file
View File

@@ -0,0 +1,6 @@
image:
file: .gitpod.Dockerfile
tasks:
- init: make mod-download-local dep-ui-local && GO111MODULE=off go install github.com/mattn/goreman@latest
command: make start-test-k8s

View File

@@ -1,252 +1,156 @@
formatters:
enable:
- gofumpt
- goimports
settings:
goimports:
local-prefixes:
- github.com/argoproj/argo-cd/v3
issues: issues:
exclude:
- SA5011
max-issues-per-linter: 0 max-issues-per-linter: 0
max-same-issues: 0 max-same-issues: 0
exclude-rules:
- path: '(.+)_test\.go'
linters:
- unparam
linters: linters:
enable: enable:
- errcheck
- errorlint - errorlint
- exptostd
- gocritic - gocritic
- gofumpt
- goimports
- gomodguard - gomodguard
- gosimple
- govet - govet
- importas - importas
- ineffassign
- misspell - misspell
# Disabled because of https://github.com/argoproj/argo-cd/issues/21705
# - nolintlint
- perfsprint - perfsprint
- revive - revive
- staticcheck - staticcheck
- testifylint - testifylint
- thelper - thelper
- tparallel
- unparam - unparam
- unused
- usestdlibvars - usestdlibvars
- usetesting - usetesting
- whitespace - whitespace
linters-settings:
exclusions: gocritic:
disabled-checks:
- appendAssign
- assignOp # Keep it disabled for readability
- exitAfterDefer
- mapKey
- typeSwitchVar
goimports:
local-prefixes: github.com/argoproj/argo-cd/v3
gomodguard:
blocked:
modules:
- github.com/golang-jwt/jwt/v4:
recommendations:
- github.com/golang-jwt/jwt/v5
- github.com/imdario/mergo:
recommendations:
- dario.cat/mergo
reason: '`github.com/imdario/mergo` has been renamed.'
- github.com/pkg/errors:
recommendations:
- errors
importas:
alias:
- alias: jwtgo
pkg: github.com/golang-jwt/jwt/v5
- alias: appsv1
pkg: k8s.io/api/apps/v1
- alias: corev1
pkg: k8s.io/api/core/v1
- alias: rbacv1
pkg: k8s.io/api/rbac/v1
- alias: apierrors
pkg: k8s.io/apimachinery/pkg/api/errors
- alias: apiextensionsv1
pkg: k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1
- alias: metav1
pkg: k8s.io/apimachinery/pkg/apis/meta/v1
- alias: informersv1
pkg: k8s.io/client-go/informers/core/v1
- alias: stderrors
pkg: errors
nolintlint:
require-specific: true
perfsprint:
# Optimizes even if it requires an int or uint type cast.
int-conversion: true
# Optimizes into `err.Error()` even if it is only equivalent for non-nil errors.
err-error: true
# Optimizes `fmt.Errorf`.
errorf: true
# Optimizes `fmt.Sprintf` with only one argument.
sprintf1: true
# Optimizes into strings concatenation.
strconcat: true
revive:
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md
rules: rules:
- linters: - name: bool-literal-in-expr
- unparam - name: blank-imports
path: (.+)_test\.go disabled: true
- name: context-as-argument
presets: arguments:
- comments - allowTypesBefore: '*testing.T,testing.TB'
- common-false-positives - name: context-keys-type
- legacy disabled: true
- std-error-handling - name: dot-imports
disabled: true
warn-unused: true - name: duplicated-imports
- name: early-return
settings: arguments:
gocritic: - 'preserveScope'
enable-all: true - name: empty-block
# Most of these should probably be enabled one-by-one. disabled: true
disabled-checks: - name: error-naming
- appendAssign disabled: true
- appendCombine # Leave disabled, multi-line assigns can be more readable. - name: error-return
- assignOp # Leave disabled, assign operations can be more confusing than helpful. - name: error-strings
- commentedOutCode disabled: true
- deferInLoop - name: errorf
- exitAfterDefer - name: identical-branches
- hugeParam - name: if-return
- importShadow - name: increment-decrement
- paramTypeCombine # Leave disabled, there are too many failures to be worth fixing. - name: indent-error-flow
- rangeValCopy arguments:
- tooManyResultsChecker - 'preserveScope'
- unnamedResult - name: modifies-parameter
- whyNoLint - name: optimize-operands-order
- name: range
gomodguard: - name: receiver-naming
blocked: - name: redefines-builtin-id
modules: disabled: true
- github.com/golang-jwt/jwt/v4: - name: redundant-import-alias
recommendations: - name: superfluous-else
- github.com/golang-jwt/jwt/v5 arguments:
- 'preserveScope'
- github.com/imdario/mergo: - name: time-equal
recommendations: - name: time-naming
- dario.cat/mergo disabled: true
reason: '`github.com/imdario/mergo` has been renamed.' - name: unexported-return
disabled: true
- github.com/pkg/errors: - name: unnecessary-stmt
recommendations: - name: unreachable-code
- errors - name: unused-parameter
- name: use-any
govet: - name: useless-break
disable: - name: var-declaration
- fieldalignment - name: var-naming
- shadow arguments:
enable-all: true - ["ID"]
- ["VM"]
importas: - - skipPackageNameChecks: true
alias: upperCaseConst: true
- pkg: github.com/golang-jwt/jwt/v5 testifylint:
alias: jwtgo enable-all: true
disable:
- pkg: k8s.io/api/apps/v1 - go-require
alias: appsv1 usetesting:
os-mkdir-temp: false
- pkg: k8s.io/api/core/v1 run:
alias: corev1 timeout: 50m
- pkg: k8s.io/api/rbac/v1
alias: rbacv1
- pkg: k8s.io/apimachinery/pkg/api/errors
alias: apierrors
- pkg: k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1
alias: apiextensionsv1
- pkg: k8s.io/apimachinery/pkg/apis/meta/v1
alias: metav1
- pkg: k8s.io/client-go/informers/core/v1
alias: informersv1
- pkg: errors
alias: stderrors
- pkg: github.com/argoproj/argo-cd/v3/util/io
alias: utilio
nolintlint:
require-specific: true
perfsprint:
# Optimizes even if it requires an int or uint type cast.
int-conversion: true
# Optimizes into `err.Error()` even if it is only equivalent for non-nil errors.
err-error: true
# Optimizes `fmt.Errorf`.
errorf: true
# Optimizes `fmt.Sprintf` with only one argument.
sprintf1: true
# Optimizes into strings concatenation.
strconcat: true
revive:
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md
rules:
- name: bool-literal-in-expr
- name: blank-imports
disabled: true
- name: context-as-argument
arguments:
- allowTypesBefore: '*testing.T,testing.TB'
- name: context-keys-type
disabled: true
- name: dot-imports
disabled: true
- name: duplicated-imports
- name: early-return
arguments:
- preserveScope
- name: empty-block
disabled: true
- name: error-naming
disabled: true
- name: error-return
- name: error-strings
disabled: true
- name: errorf
- name: identical-branches
- name: if-return
- name: increment-decrement
- name: indent-error-flow
arguments:
- preserveScope
- name: modifies-parameter
- name: optimize-operands-order
- name: range
- name: receiver-naming
- name: redefines-builtin-id
disabled: true
- name: redundant-import-alias
- name: superfluous-else
arguments:
- preserveScope
- name: time-equal
- name: time-naming
disabled: true
- name: unexported-return
disabled: true
- name: unnecessary-stmt
- name: unreachable-code
- name: unused-parameter
- name: use-any
- name: useless-break
- name: var-declaration
- name: var-naming
arguments:
- - ID
- - VM
- - skipPackageNameChecks: true
upperCaseConst: true
staticcheck:
checks:
- all
- -SA5011
- -ST1003
- -ST1016
testifylint:
enable-all: true
disable:
- go-require
unused:
field-writes-are-uses: false
exported-fields-are-used: false
usetesting:
os-mkdir-temp: false
output:
show-stats: false
version: "2"

View File

@@ -24,8 +24,8 @@ builds:
- -extldflags="-static" - -extldflags="-static"
goos: goos:
- linux - linux
- windows
- darwin - darwin
- windows
goarch: goarch:
- amd64 - amd64
- arm64 - arm64
@@ -45,18 +45,17 @@ builds:
archives: archives:
- id: argocd-archive - id: argocd-archive
ids: builds:
- argocd-cli - argocd-cli
name_template: |- name_template: |-
{{ .ProjectName }}-{{ .Os }}-{{ .Arch }} {{ .ProjectName }}-{{ .Os }}-{{ .Arch }}
formats: [binary] format: binary
checksum: checksum:
name_template: 'cli_checksums.txt' name_template: 'cli_checksums.txt'
algorithm: sha256 algorithm: sha256
release: release:
make_latest: '{{ .Env.GORELEASER_MAKE_LATEST }}'
prerelease: auto prerelease: auto
draft: false draft: false
header: | header: |
@@ -81,27 +80,26 @@ release:
All Argo CD container images are signed by cosign. A Provenance is generated for container images and CLI binaries which meet the SLSA Level 3 specifications. See the [documentation](https://argo-cd.readthedocs.io/en/stable/operator-manual/signed-release-assets) on how to verify. All Argo CD container images are signed by cosign. A Provenance is generated for container images and CLI binaries which meet the SLSA Level 3 specifications. See the [documentation](https://argo-cd.readthedocs.io/en/stable/operator-manual/signed-release-assets) on how to verify.
## Release Notes Blog Post ## Release Notes Blog Post
For a detailed breakdown of the key changes and improvements in this release, check out the [official blog post](https://blog.argoproj.io/argo-cd-v3-0-release-candidate-a0b933f4e58f) For a detailed breakdown of the key changes and improvements in this release, check out the [official blog post](https://blog.argoproj.io/argo-cd-v2-14-release-candidate-57a664791e2a)
## Upgrading ## Upgrading
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. 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: | footer: |
**Full Changelog**: https://github.com/argoproj/argo-cd/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> <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>
snapshot: #### To be removed for PR snapshot: #### To be removed for PR
version_template: '2.6.0' name_template: "2.6.0"
changelog: changelog:
use: github use:
github
sort: asc sort: asc
abbrev: 0 abbrev: 0
groups: # Regex use RE2 syntax as defined here: https://github.com/google/re2/wiki/Syntax. groups: # Regex use RE2 syntax as defined here: https://github.com/google/re2/wiki/Syntax.
- title: 'Breaking Changes'
regexp: '^.*?(\([[:word:]]+\))??!:.+$'
order: 0
- title: 'Features' - title: 'Features'
regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$' regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$'
order: 100 order: 100
@@ -121,4 +119,7 @@ changelog:
- '^test:' - '^test:'
- '^.*?Bump(\([[:word:]]+\))?.+$' - '^.*?Bump(\([[:word:]]+\))?.+$'
- '^.*?\[Bot\](\([[:word:]]+\))?.+$' - '^.*?\[Bot\](\([[:word:]]+\))?.+$'
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json # yaml-language-server: $schema=https://goreleaser.com/static/schema.json

View File

@@ -1,88 +1,82 @@
dir: '{{.InterfaceDir}}/mocks' # global config
structname: '{{.InterfaceName}}' filename: "{{.InterfaceName}}.go"
filename: '{{.InterfaceName}}.go' dir: "{{.InterfaceDir}}/mocks"
pkgname: mocks outpkg: "mocks"
mockname: "{{.InterfaceName}}"
template-data: with-expecter: false
unroll-variadic: true # individual interface config
packages: packages:
github.com/argoproj/argo-cd/v3/applicationset/generators: github.com/argoproj/argo-cd/v3/applicationset/generators:
interfaces: interfaces:
Generator: {} Generator:
github.com/argoproj/argo-cd/v3/applicationset/services: github.com/argoproj/argo-cd/v3/applicationset/services:
interfaces: interfaces:
Repos: {} Repos:
github.com/argoproj/argo-cd/v3/applicationset/services/scm_provider: github.com/argoproj/argo-cd/v3/applicationset/services/scm_provider:
config: config:
dir: applicationset/services/scm_provider/aws_codecommit/mocks dir: "applicationset/services/scm_provider/aws_codecommit/mocks"
interfaces: interfaces:
AWSCodeCommitClient: {} AWSCodeCommitClient:
AWSTaggingClient: {} AWSTaggingClient:
github.com/microsoft/azure-devops-go-api/azuredevops/git:
config:
dir: "applicationset/services/scm_provider/azure_devops/git/mocks"
interfaces:
Client:
github.com/argoproj/argo-cd/v3/applicationset/utils: github.com/argoproj/argo-cd/v3/applicationset/utils:
interfaces: interfaces:
Renderer: {} Renderer:
github.com/argoproj/argo-cd/v3/commitserver/apiclient:
interfaces:
CommitServiceClient: {}
github.com/argoproj/argo-cd/v3/commitserver/commit: github.com/argoproj/argo-cd/v3/commitserver/commit:
interfaces: interfaces:
RepoClientFactory: {} RepoClientFactory:
github.com/argoproj/argo-cd/v3/commitserver/apiclient:
interfaces:
CommitServiceClient:
Clientset:
github.com/argoproj/argo-cd/v3/controller/cache: github.com/argoproj/argo-cd/v3/controller/cache:
interfaces: interfaces:
LiveStateCache: {} LiveStateCache:
github.com/argoproj/argo-cd/v3/controller/hydrator:
interfaces:
Dependencies: {}
RepoGetter: {}
github.com/argoproj/argo-cd/v3/pkg/apiclient/cluster:
interfaces:
ClusterServiceServer: {}
github.com/argoproj/argo-cd/v3/pkg/apiclient/session:
interfaces:
SessionServiceClient: {}
SessionServiceServer: {}
github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned/typed/application/v1alpha1:
interfaces:
AppProjectInterface: {}
github.com/argoproj/argo-cd/v3/reposerver/apiclient: github.com/argoproj/argo-cd/v3/reposerver/apiclient:
interfaces: interfaces:
RepoServerService_GenerateManifestWithFilesClient: {} RepoServerServiceClient:
RepoServerServiceClient: {} RepoServerService_GenerateManifestWithFilesClient:
github.com/argoproj/argo-cd/v3/server/application: github.com/argoproj/argo-cd/v3/server/application:
interfaces: interfaces:
Broadcaster: {} Broadcaster:
github.com/argoproj/argo-cd/v3/server/extension: github.com/argoproj/argo-cd/v3/server/extension:
interfaces: interfaces:
ApplicationGetter: {} ApplicationGetter:
ExtensionMetricsRegistry: {} ExtensionMetricsRegistry:
ProjectGetter: {} ProjectGetter:
RbacEnforcer: {} RbacEnforcer:
SettingsGetter: {} SettingsGetter:
UserGetter: {} UserGetter:
github.com/argoproj/argo-cd/v3/util/db: github.com/argoproj/argo-cd/v3/util/db:
interfaces: interfaces:
ArgoDB: {} ArgoDB:
github.com/argoproj/argo-cd/v3/util/git: github.com/argoproj/argo-cd/v3/util/git:
interfaces: interfaces:
Client: {} Client:
github.com/argoproj/argo-cd/v3/util/helm: github.com/argoproj/argo-cd/v3/util/helm:
interfaces: interfaces:
Client: {} Client:
github.com/argoproj/argo-cd/v3/util/oci:
interfaces:
Client: {}
github.com/argoproj/argo-cd/v3/util/io: github.com/argoproj/argo-cd/v3/util/io:
interfaces: interfaces:
TempPaths: {} TempPaths:
github.com/argoproj/argo-cd/v3/util/notification/argocd: github.com/argoproj/argo-cd/v3/util/notification/argocd:
interfaces: interfaces:
Service: {} Service:
github.com/argoproj/argo-cd/v3/util/workloadidentity: github.com/argoproj/argo-cd/v3/util/workloadidentity:
interfaces: interfaces:
TokenProvider: {} TokenProvider:
github.com/microsoft/azure-devops-go-api/azuredevops/v7/git: # These mocks are not currently used, but they are part of the public API of this package.
config: github.com/argoproj/argo-cd/v3/pkg/apiclient/session:
dir: applicationset/services/scm_provider/azure_devops/git/mocks
interfaces: interfaces:
Client: {} SessionServiceServer:
SessionServiceClient:
github.com/argoproj/argo-cd/v3/pkg/apiclient/cluster:
interfaces:
ClusterServiceServer:
github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned/typed/application/v1alpha1:
interfaces:
AppProjectInterface:

View File

@@ -12,8 +12,3 @@
/.github/** @argoproj/argocd-approvers @argoproj/argocd-approvers-ci /.github/** @argoproj/argocd-approvers @argoproj/argocd-approvers-ci
/.goreleaser.yaml @argoproj/argocd-approvers @argoproj/argocd-approvers-ci /.goreleaser.yaml @argoproj/argocd-approvers @argoproj/argocd-approvers-ci
/sonar-project.properties @argoproj/argocd-approvers @argoproj/argocd-approvers-ci /sonar-project.properties @argoproj/argocd-approvers @argoproj/argocd-approvers-ci
# CLI
/cmd/argocd/** @argoproj/argocd-approvers @argoproj/argocd-approvers-cli
/cmd/main.go @argoproj/argocd-approvers @argoproj/argocd-approvers-cli
/docs/operator-manual/ @argoproj/argocd-approvers @argoproj/argocd-approvers-cli

View File

@@ -1,12 +1,10 @@
ARG BASE_IMAGE=docker.io/library/ubuntu:25.04@sha256:10bb10bb062de665d4dc3e0ea36715270ead632cfcb74d08ca2273712a0dfb42 ARG BASE_IMAGE=docker.io/library/ubuntu:24.04@sha256:80dd3c3b9c6cecb9f1667e9290b3bc61b78c2678c02cbdae5f0fea92cc6734ab
#################################################################################################### ####################################################################################################
# Builder image # Builder image
# Initial stage which pulls prepares build dependencies and CLI tooling we need for our final 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 # 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.24.1@sha256:c5adecdb7b3f8c5ca3c88648a861882849cc8b02fed68ece31e25de88ad13418 AS builder
WORKDIR /tmp
RUN echo 'deb http://archive.debian.org/debian buster-backports main' >> /etc/apt/sources.list RUN echo 'deb http://archive.debian.org/debian buster-backports main' >> /etc/apt/sources.list
@@ -25,6 +23,8 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
apt-get clean && \ apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
WORKDIR /tmp
COPY hack/install.sh hack/tool-versions.sh ./ COPY hack/install.sh hack/tool-versions.sh ./
COPY hack/installers installers COPY hack/installers installers
@@ -40,8 +40,8 @@ LABEL org.opencontainers.image.source="https://github.com/argoproj/argo-cd"
USER root USER root
ENV ARGOCD_USER_ID=999 \ ENV ARGOCD_USER_ID=999
DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
RUN groupadd -g $ARGOCD_USER_ID argocd && \ RUN groupadd -g $ARGOCD_USER_ID argocd && \
useradd -r -u $ARGOCD_USER_ID -g argocd argocd && \ useradd -r -u $ARGOCD_USER_ID -g argocd argocd && \
@@ -55,13 +55,11 @@ RUN groupadd -g $ARGOCD_USER_ID argocd && \
apt-get clean && \ apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
COPY hack/gpg-wrapper.sh \ COPY hack/gpg-wrapper.sh /usr/local/bin/gpg-wrapper.sh
hack/git-verify-wrapper.sh \ COPY hack/git-verify-wrapper.sh /usr/local/bin/git-verify-wrapper.sh
entrypoint.sh \
/usr/local/bin/
COPY --from=builder /usr/local/bin/helm /usr/local/bin/helm COPY --from=builder /usr/local/bin/helm /usr/local/bin/helm
COPY --from=builder /usr/local/bin/kustomize /usr/local/bin/kustomize COPY --from=builder /usr/local/bin/kustomize /usr/local/bin/kustomize
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
# keep uid_entrypoint.sh for backward compatibility # keep uid_entrypoint.sh for backward compatibility
RUN ln -s /usr/local/bin/entrypoint.sh /usr/local/bin/uid_entrypoint.sh RUN ln -s /usr/local/bin/entrypoint.sh /usr/local/bin/uid_entrypoint.sh
@@ -103,7 +101,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 # 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.24.1@sha256:c5adecdb7b3f8c5ca3c88648a861882849cc8b02fed68ece31e25de88ad13418 AS argocd-build
WORKDIR /go/src/github.com/argoproj/argo-cd WORKDIR /go/src/github.com/argoproj/argo-cd
@@ -113,13 +111,13 @@ RUN go mod download
# Perform the build # Perform the build
COPY . . COPY . .
COPY --from=argocd-ui /src/dist/app /go/src/github.com/argoproj/argo-cd/ui/dist/app COPY --from=argocd-ui /src/dist/app /go/src/github.com/argoproj/argo-cd/ui/dist/app
ARG TARGETOS \ ARG TARGETOS
TARGETARCH ARG TARGETARCH
# These build args are optional; if not specified the defaults will be taken from the Makefile # These build args are optional; if not specified the defaults will be taken from the Makefile
ARG GIT_TAG \ ARG GIT_TAG
BUILD_DATE \ ARG BUILD_DATE
GIT_TREE_STATE \ ARG GIT_TREE_STATE
GIT_COMMIT ARG GIT_COMMIT
RUN GIT_COMMIT=$GIT_COMMIT \ RUN GIT_COMMIT=$GIT_COMMIT \
GIT_TREE_STATE=$GIT_TREE_STATE \ GIT_TREE_STATE=$GIT_TREE_STATE \
GIT_TAG=$GIT_TAG \ GIT_TAG=$GIT_TAG \
@@ -132,7 +130,6 @@ RUN GIT_COMMIT=$GIT_COMMIT \
# Final image # Final image
#################################################################################################### ####################################################################################################
FROM argocd-base FROM argocd-base
ENTRYPOINT ["/usr/bin/tini", "--"]
COPY --from=argocd-build /go/src/github.com/argoproj/argo-cd/dist/argocd* /usr/local/bin/ COPY --from=argocd-build /go/src/github.com/argoproj/argo-cd/dist/argocd* /usr/local/bin/
USER root USER root
@@ -147,3 +144,4 @@ RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-server && \
ln -s /usr/local/bin/argocd /usr/local/bin/argocd-commit-server ln -s /usr/local/bin/argocd /usr/local/bin/argocd-commit-server
USER $ARGOCD_USER_ID USER $ARGOCD_USER_ID
ENTRYPOINT ["/usr/bin/tini", "--"]

View File

@@ -1,62 +0,0 @@
FROM docker.io/library/golang:1.25.6@sha256:fc24d3881a021e7b968a4610fc024fba749f98fe5c07d4f28e6cfa14dc65a84c
ENV DEBIAN_FRONTEND=noninteractive
RUN echo 'deb http://archive.debian.org/debian buster-backports main' >> /etc/apt/sources.list
RUN apt-get update && apt-get install --no-install-recommends -y \
curl \
openssh-server \
nginx \
unzip \
fcgiwrap \
git \
git-lfs \
make \
wget \
gcc \
sudo \
zip \
tini \
gpg \
tzdata \
connect-proxy
RUN go install github.com/go-delve/delve/cmd/dlv@latest
COPY hack/install.sh hack/tool-versions.sh ./
COPY hack/installers installers
RUN ./install.sh helm && \
INSTALL_PATH=/usr/local/bin ./install.sh kustomize
COPY hack/gpg-wrapper.sh \
hack/git-verify-wrapper.sh \
entrypoint.sh \
/usr/local/bin/
# support for mounting configuration from a configmap
WORKDIR /app/config/ssh
RUN touch ssh_known_hosts && \
ln -s /app/config/ssh/ssh_known_hosts /etc/ssh/ssh_known_hosts
WORKDIR /app/config
RUN mkdir -p tls && \
mkdir -p gpg/source && \
mkdir -p gpg/keys
COPY .tilt-bin/argocd_linux /usr/local/bin/argocd
RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-server && \
ln -s /usr/local/bin/argocd /usr/local/bin/argocd-repo-server && \
ln -s /usr/local/bin/argocd /usr/local/bin/argocd-application-controller && \
ln -s /usr/local/bin/argocd /usr/local/bin/argocd-dex && \
ln -s /usr/local/bin/argocd /usr/local/bin/argocd-notifications && \
ln -s /usr/local/bin/argocd /usr/local/bin/argocd-applicationset-controller && \
ln -s /usr/local/bin/argocd /usr/local/bin/argocd-commit-server
# directory for Tilt restart file
RUN mkdir -p /tilt
# overridden by Tiltfile
ENTRYPOINT ["/usr/bin/tini", "-s", "--", "dlv", "exec", "--continue", "--accept-multiclient", "--headless", "--listen=:2345", "--api-version=2"]

View File

@@ -1,9 +0,0 @@
FROM node:20
WORKDIR /app/ui
COPY ui /app/ui
RUN yarn install
ENTRYPOINT ["yarn", "start"]

View File

@@ -3,16 +3,7 @@ CURRENT_DIR=$(shell pwd)
DIST_DIR=${CURRENT_DIR}/dist DIST_DIR=${CURRENT_DIR}/dist
CLI_NAME=argocd CLI_NAME=argocd
BIN_NAME=argocd BIN_NAME=argocd
CGO_FLAG=0
UNAME_S:=$(shell uname)
IS_DARWIN:=$(if $(filter Darwin, $(UNAME_S)),true,false)
# When using OSX/Darwin, you might need to enable CGO for local builds
DEFAULT_CGO_FLAG:=0
ifeq ($(IS_DARWIN),true)
DEFAULT_CGO_FLAG:=1
endif
CGO_FLAG?=${DEFAULT_CGO_FLAG}
GEN_RESOURCES_CLI_NAME=argocd-resources-gen GEN_RESOURCES_CLI_NAME=argocd-resources-gen
@@ -43,17 +34,6 @@ endif
DOCKER_SRCDIR?=$(GOPATH)/src DOCKER_SRCDIR?=$(GOPATH)/src
DOCKER_WORKDIR?=/go/src/github.com/argoproj/argo-cd DOCKER_WORKDIR?=/go/src/github.com/argoproj/argo-cd
# Allows you to control which Docker network the test-util containers attach to.
# This is particularly useful if you are running Kubernetes in Docker (e.g., k3d)
# and want the test containers to reach the Kubernetes API via an already-existing Docker network.
DOCKER_NETWORK ?= default
ifneq ($(DOCKER_NETWORK),default)
DOCKER_NETWORK_ARG := --network $(DOCKER_NETWORK)
else
DOCKER_NETWORK_ARG :=
endif
ARGOCD_PROCFILE?=Procfile ARGOCD_PROCFILE?=Procfile
# pointing to python 3.7 to match https://github.com/argoproj/argo-cd/blob/master/.readthedocs.yml # pointing to python 3.7 to match https://github.com/argoproj/argo-cd/blob/master/.readthedocs.yml
@@ -124,11 +104,11 @@ define run-in-test-server
-v ${GOPATH}/pkg/mod:/go/pkg/mod${VOLUME_MOUNT} \ -v ${GOPATH}/pkg/mod:/go/pkg/mod${VOLUME_MOUNT} \
-v ${GOCACHE}:/tmp/go-build-cache${VOLUME_MOUNT} \ -v ${GOCACHE}:/tmp/go-build-cache${VOLUME_MOUNT} \
-v ${HOME}/.kube:/home/user/.kube${VOLUME_MOUNT} \ -v ${HOME}/.kube:/home/user/.kube${VOLUME_MOUNT} \
-v /tmp:/tmp${VOLUME_MOUNT} \
-w ${DOCKER_WORKDIR} \ -w ${DOCKER_WORKDIR} \
-p ${ARGOCD_E2E_APISERVER_PORT}:8080 \ -p ${ARGOCD_E2E_APISERVER_PORT}:8080 \
-p 4000:4000 \ -p 4000:4000 \
-p 5000:5000 \ -p 5000:5000 \
$(DOCKER_NETWORK_ARG)\
$(PODMAN_ARGS) \ $(PODMAN_ARGS) \
$(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE):$(TEST_TOOLS_TAG) \ $(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE):$(TEST_TOOLS_TAG) \
bash -c "$(1)" bash -c "$(1)"
@@ -149,8 +129,8 @@ define run-in-test-client
-v ${GOPATH}/pkg/mod:/go/pkg/mod${VOLUME_MOUNT} \ -v ${GOPATH}/pkg/mod:/go/pkg/mod${VOLUME_MOUNT} \
-v ${GOCACHE}:/tmp/go-build-cache${VOLUME_MOUNT} \ -v ${GOCACHE}:/tmp/go-build-cache${VOLUME_MOUNT} \
-v ${HOME}/.kube:/home/user/.kube${VOLUME_MOUNT} \ -v ${HOME}/.kube:/home/user/.kube${VOLUME_MOUNT} \
-v /tmp:/tmp${VOLUME_MOUNT} \
-w ${DOCKER_WORKDIR} \ -w ${DOCKER_WORKDIR} \
$(DOCKER_NETWORK_ARG)\
$(PODMAN_ARGS) \ $(PODMAN_ARGS) \
$(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE):$(TEST_TOOLS_TAG) \ $(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE):$(TEST_TOOLS_TAG) \
bash -c "$(1)" bash -c "$(1)"
@@ -167,11 +147,7 @@ PATH:=$(PATH):$(PWD)/hack
DOCKER_PUSH?=false DOCKER_PUSH?=false
IMAGE_NAMESPACE?= IMAGE_NAMESPACE?=
# perform static compilation # perform static compilation
DEFAULT_STATIC_BUILD:=true STATIC_BUILD?=true
ifeq ($(IS_DARWIN),true)
DEFAULT_STATIC_BUILD:=false
endif
STATIC_BUILD?=${DEFAULT_STATIC_BUILD}
# build development images # build development images
DEV_IMAGE?=false DEV_IMAGE?=false
ARGOCD_GPG_ENABLED?=true ARGOCD_GPG_ENABLED?=true
@@ -510,7 +486,6 @@ start-e2e-local: mod-vendor-local dep-ui-local cli-local
ARGOCD_APPLICATIONSET_CONTROLLER_ALLOWED_SCM_PROVIDERS=http://127.0.0.1:8341,http://127.0.0.1:8342,http://127.0.0.1:8343,http://127.0.0.1:8344 \ ARGOCD_APPLICATIONSET_CONTROLLER_ALLOWED_SCM_PROVIDERS=http://127.0.0.1:8341,http://127.0.0.1:8342,http://127.0.0.1:8343,http://127.0.0.1:8344 \
ARGOCD_E2E_TEST=true \ ARGOCD_E2E_TEST=true \
ARGOCD_HYDRATOR_ENABLED=true \ ARGOCD_HYDRATOR_ENABLED=true \
ARGOCD_CLUSTER_CACHE_EVENTS_PROCESSING_INTERVAL=1ms \
goreman -f $(ARGOCD_PROCFILE) start ${ARGOCD_START} goreman -f $(ARGOCD_PROCFILE) start ${ARGOCD_START}
ls -lrt /tmp/coverage ls -lrt /tmp/coverage
@@ -609,13 +584,11 @@ install-test-tools-local:
./hack/install.sh kustomize ./hack/install.sh kustomize
./hack/install.sh helm ./hack/install.sh helm
./hack/install.sh gotestsum ./hack/install.sh gotestsum
./hack/install.sh oras
# Installs all tools required for running codegen (Linux packages) # Installs all tools required for running codegen (Linux packages)
.PHONY: install-codegen-tools-local .PHONY: install-codegen-tools-local
install-codegen-tools-local: install-codegen-tools-local:
./hack/install.sh codegen-tools ./hack/install.sh codegen-tools
./hack/install.sh codegen-go-tools
# Installs all tools required for running codegen (Go packages) # Installs all tools required for running codegen (Go packages)
.PHONY: install-go-tools-local .PHONY: install-go-tools-local

View File

@@ -8,7 +8,6 @@ commit-server: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go
ui: sh -c 'cd ui && ${ARGOCD_E2E_YARN_CMD:-yarn} start' ui: sh -c 'cd ui && ${ARGOCD_E2E_YARN_CMD:-yarn} start'
git-server: test/fixture/testrepos/start-git.sh git-server: test/fixture/testrepos/start-git.sh
helm-registry: test/fixture/testrepos/start-helm-registry.sh 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} 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}" 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

@@ -3,9 +3,9 @@ header:
expiration-date: '2024-10-31T00:00:00.000Z' # One year from initial release. expiration-date: '2024-10-31T00:00:00.000Z' # One year from initial release.
last-updated: '2023-10-27' last-updated: '2023-10-27'
last-reviewed: '2023-10-27' last-reviewed: '2023-10-27'
commit-hash: 320f46f06beaf75f9c406e3a47e2e09d36e2047a commit-hash: 74a367d10e7110209610ba3ec225539ebe5f7522
project-url: https://github.com/argoproj/argo-cd project-url: https://github.com/argoproj/argo-cd
project-release: v3.2.0 project-release: v2.14.0
changelog: https://github.com/argoproj/argo-cd/releases changelog: https://github.com/argoproj/argo-cd/releases
license: https://github.com/argoproj/argo-cd/blob/master/LICENSE license: https://github.com/argoproj/argo-cd/blob/master/LICENSE
project-lifecycle: project-lifecycle:

291
Tiltfile
View File

@@ -1,291 +0,0 @@
load('ext://restart_process', 'docker_build_with_restart')
load('ext://uibutton', 'cmd_button', 'location')
# add ui button in web ui to run make codegen-local (top nav)
cmd_button(
'make codegen-local',
argv=['sh', '-c', 'make codegen-local'],
location=location.NAV,
icon_name='terminal',
text='make codegen-local',
)
cmd_button(
'make test-local',
argv=['sh', '-c', 'make test-local'],
location=location.NAV,
icon_name='science',
text='make test-local',
)
# add ui button in web ui to run make codegen-local (top nav)
cmd_button(
'make cli-local',
argv=['sh', '-c', 'make cli-local'],
location=location.NAV,
icon_name='terminal',
text='make cli-local',
)
# detect cluster architecture for build
cluster_version = decode_yaml(local('kubectl version -o yaml'))
platform = cluster_version['serverVersion']['platform']
arch = platform.split('/')[1]
# build the argocd binary on code changes
code_deps = [
'applicationset',
'cmd',
'cmpserver',
'commitserver',
'common',
'controller',
'notification-controller',
'pkg',
'reposerver',
'server',
'util',
'go.mod',
'go.sum',
]
local_resource(
'build',
'CGO_ENABLED=0 GOOS=linux GOARCH=' + arch + ' go build -gcflags="all=-N -l" -mod=readonly -o .tilt-bin/argocd_linux cmd/main.go',
deps = code_deps,
allow_parallel=True,
)
# deploy the argocd manifests
k8s_yaml(kustomize('manifests/dev-tilt'))
# build dev image
docker_build_with_restart(
'argocd',
context='.',
dockerfile='Dockerfile.tilt',
entrypoint=[
"/usr/bin/tini",
"-s",
"--",
"dlv",
"exec",
"--continue",
"--accept-multiclient",
"--headless",
"--listen=:2345",
"--api-version=2"
],
platform=platform,
live_update=[
sync('.tilt-bin/argocd_linux', '/usr/local/bin/argocd'),
],
only=[
'.tilt-bin',
'hack',
'entrypoint.sh',
],
restart_file='/tilt/.restart-proc'
)
# build image for argocd-cli jobs
docker_build(
'argocd-job',
context='.',
dockerfile='Dockerfile.tilt',
platform=platform,
only=[
'.tilt-bin',
'hack',
'entrypoint.sh',
]
)
# track argocd-server resources and port forward
k8s_resource(
workload='argocd-server',
objects=[
'argocd-server:serviceaccount',
'argocd-server:role',
'argocd-server:rolebinding',
'argocd-cm:configmap',
'argocd-cmd-params-cm:configmap',
'argocd-gpg-keys-cm:configmap',
'argocd-rbac-cm:configmap',
'argocd-ssh-known-hosts-cm:configmap',
'argocd-tls-certs-cm:configmap',
'argocd-secret:secret',
'argocd-server-network-policy:networkpolicy',
'argocd-server:clusterrolebinding',
'argocd-server:clusterrole',
],
port_forwards=[
'8080:8080',
'9345:2345',
'8083:8083'
],
)
# track crds
k8s_resource(
new_name='cluster-resources',
objects=[
'applications.argoproj.io:customresourcedefinition',
'applicationsets.argoproj.io:customresourcedefinition',
'appprojects.argoproj.io:customresourcedefinition',
'argocd:namespace'
]
)
# track argocd-repo-server resources and port forward
k8s_resource(
workload='argocd-repo-server',
objects=[
'argocd-repo-server:serviceaccount',
'argocd-repo-server-network-policy:networkpolicy',
],
port_forwards=[
'8081:8081',
'9346:2345',
'8084:8084'
],
)
# track argocd-redis resources and port forward
k8s_resource(
workload='argocd-redis',
objects=[
'argocd-redis:serviceaccount',
'argocd-redis:role',
'argocd-redis:rolebinding',
'argocd-redis-network-policy:networkpolicy',
],
port_forwards=[
'6379:6379',
],
)
# track argocd-applicationset-controller resources
k8s_resource(
workload='argocd-applicationset-controller',
objects=[
'argocd-applicationset-controller:serviceaccount',
'argocd-applicationset-controller-network-policy:networkpolicy',
'argocd-applicationset-controller:role',
'argocd-applicationset-controller:rolebinding',
'argocd-applicationset-controller:clusterrolebinding',
'argocd-applicationset-controller:clusterrole',
],
port_forwards=[
'9347:2345',
'8085:8080',
'7000:7000'
],
)
# track argocd-application-controller resources
k8s_resource(
workload='argocd-application-controller',
objects=[
'argocd-application-controller:serviceaccount',
'argocd-application-controller-network-policy:networkpolicy',
'argocd-application-controller:role',
'argocd-application-controller:rolebinding',
'argocd-application-controller:clusterrolebinding',
'argocd-application-controller:clusterrole',
],
port_forwards=[
'9348:2345',
'8086:8082',
],
)
# track argocd-notifications-controller resources
k8s_resource(
workload='argocd-notifications-controller',
objects=[
'argocd-notifications-controller:serviceaccount',
'argocd-notifications-controller-network-policy:networkpolicy',
'argocd-notifications-controller:role',
'argocd-notifications-controller:rolebinding',
'argocd-notifications-cm:configmap',
'argocd-notifications-secret:secret',
],
port_forwards=[
'9349:2345',
'8087:9001',
],
)
# track argocd-dex-server resources
k8s_resource(
workload='argocd-dex-server',
objects=[
'argocd-dex-server:serviceaccount',
'argocd-dex-server-network-policy:networkpolicy',
'argocd-dex-server:role',
'argocd-dex-server:rolebinding',
],
)
# track argocd-commit-server resources
k8s_resource(
workload='argocd-commit-server',
objects=[
'argocd-commit-server:serviceaccount',
'argocd-commit-server-network-policy:networkpolicy',
],
port_forwards=[
'9350:2345',
'8088:8087',
'8089:8086',
],
)
# docker for ui
docker_build(
'argocd-ui',
context='.',
dockerfile='Dockerfile.ui.tilt',
entrypoint=['sh', '-c', 'cd /app/ui && yarn start'],
only=['ui'],
live_update=[
sync('ui', '/app/ui'),
run('sh -c "cd /app/ui && yarn install"', trigger=['/app/ui/package.json', '/app/ui/yarn.lock']),
],
)
# track argocd-ui resources and port forward
k8s_resource(
workload='argocd-ui',
port_forwards=[
'4000:4000',
],
)
# linting
local_resource(
'lint',
'make lint-local',
deps = code_deps,
allow_parallel=True,
resource_deps=['vendor']
)
local_resource(
'lint-ui',
'make lint-ui-local',
deps = [
'ui',
],
allow_parallel=True,
)
local_resource(
'vendor',
'go mod vendor',
deps = [
'go.mod',
'go.sum',
],
)

View File

@@ -5,10 +5,8 @@ PR with your organization name if you are using Argo CD.
Currently, the following organizations are **officially** using Argo CD: Currently, the following organizations are **officially** using Argo CD:
1. [100ms](https://www.100ms.ai/)
1. [127Labs](https://127labs.com/) 1. [127Labs](https://127labs.com/)
1. [3Rein](https://www.3rein.com/) 1. [3Rein](https://www.3rein.com/)
1. [42 School](https://42.fr/)
1. [4data](https://4data.ch/) 1. [4data](https://4data.ch/)
1. [7shifts](https://www.7shifts.com/) 1. [7shifts](https://www.7shifts.com/)
1. [Adevinta](https://www.adevinta.com/) 1. [Adevinta](https://www.adevinta.com/)
@@ -32,7 +30,6 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Ant Group](https://www.antgroup.com/) 1. [Ant Group](https://www.antgroup.com/)
1. [AppDirect](https://www.appdirect.com) 1. [AppDirect](https://www.appdirect.com)
1. [Arctiq Inc.](https://www.arctiq.ca) 1. [Arctiq Inc.](https://www.arctiq.ca)
1. [Artemis Health by Nomi Health](https://www.artemishealth.com/)
1. [Arturia](https://www.arturia.com) 1. [Arturia](https://www.arturia.com)
1. [ARZ Allgemeines Rechenzentrum GmbH](https://www.arz.at/) 1. [ARZ Allgemeines Rechenzentrum GmbH](https://www.arz.at/)
1. [Augury](https://www.augury.com/) 1. [Augury](https://www.augury.com/)
@@ -42,14 +39,12 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Back Market](https://www.backmarket.com) 1. [Back Market](https://www.backmarket.com)
1. [Bajaj Finserv Health Ltd.](https://www.bajajfinservhealth.in) 1. [Bajaj Finserv Health Ltd.](https://www.bajajfinservhealth.in)
1. [Baloise](https://www.baloise.com) 1. [Baloise](https://www.baloise.com)
1. [Batumbu](https://batumbu.id)
1. [BCDevExchange DevOps Platform](https://bcdevexchange.org/DevOpsPlatform) 1. [BCDevExchange DevOps Platform](https://bcdevexchange.org/DevOpsPlatform)
1. [Beat](https://thebeat.co/en/) 1. [Beat](https://thebeat.co/en/)
1. [Beez Innovation Labs](https://www.beezlabs.com/) 1. [Beez Innovation Labs](https://www.beezlabs.com/)
1. [Bedag Informatik AG](https://www.bedag.ch/) 1. [Bedag Informatik AG](https://www.bedag.ch/)
1. [Beleza Na Web](https://www.belezanaweb.com.br/) 1. [Beleza Na Web](https://www.belezanaweb.com.br/)
1. [Believable Bots](https://believablebots.io) 1. [Believable Bots](https://believablebots.io)
1. [Bayer AG](https://bayer.com)
1. [BigPanda](https://bigpanda.io) 1. [BigPanda](https://bigpanda.io)
1. [BioBox Analytics](https://biobox.io) 1. [BioBox Analytics](https://biobox.io)
1. [BMW Group](https://www.bmwgroup.com/) 1. [BMW Group](https://www.bmwgroup.com/)
@@ -72,9 +67,7 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Chainnodes](https://chainnodes.org) 1. [Chainnodes](https://chainnodes.org)
1. [Chargetrip](https://chargetrip.com) 1. [Chargetrip](https://chargetrip.com)
1. [Chime](https://www.chime.com) 1. [Chime](https://www.chime.com)
1. [Chronicle Labs](https://chroniclelabs.org)
1. [Cisco ET&I](https://eti.cisco.com/) 1. [Cisco ET&I](https://eti.cisco.com/)
1. [Close](https://www.close.com/)
1. [Cloud Posse](https://www.cloudposse.com/) 1. [Cloud Posse](https://www.cloudposse.com/)
1. [Cloud Scale](https://cloudscaleinc.com/) 1. [Cloud Scale](https://cloudscaleinc.com/)
1. [CloudScript](https://www.cloudscript.com.br/) 1. [CloudScript](https://www.cloudscript.com.br/)
@@ -99,15 +92,12 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Daydream](https://daydream.ing) 1. [Daydream](https://daydream.ing)
1. [Deloitte](https://www.deloitte.com/) 1. [Deloitte](https://www.deloitte.com/)
1. [Deutsche Telekom AG](https://telekom.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/) 1. [Devopsi - Poland Software/DevOps Consulting](https://devopsi.pl/)
1. [Devtron Labs](https://github.com/devtron-labs/devtron) 1. [Devtron Labs](https://github.com/devtron-labs/devtron)
1. [DigitalEd](https://www.digitaled.com)
1. [DigitalOcean](https://www.digitalocean.com) 1. [DigitalOcean](https://www.digitalocean.com)
1. [Divar](https://divar.ir) 1. [Divar](https://divar.ir)
1. [Divistant](https://divistant.com) 1. [Divistant](https://divistant.com)
1. [Dott](https://ridedott.com) 1. [Dott](https://ridedott.com)
1. [Doubble](https://www.doubble.app)
1. [Doximity](https://www.doximity.com/) 1. [Doximity](https://www.doximity.com/)
1. [EDF Renewables](https://www.edf-re.com/) 1. [EDF Renewables](https://www.edf-re.com/)
1. [edX](https://edx.org) 1. [edX](https://edx.org)
@@ -119,7 +109,6 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Energisme](https://energisme.com/) 1. [Energisme](https://energisme.com/)
1. [enigmo](https://enigmo.co.jp/) 1. [enigmo](https://enigmo.co.jp/)
1. [Envoy](https://envoy.com/) 1. [Envoy](https://envoy.com/)
1. [eSave](https://esave.es/)
1. [Factorial](https://factorialhr.com/) 1. [Factorial](https://factorialhr.com/)
1. [Farfetch](https://www.farfetch.com) 1. [Farfetch](https://www.farfetch.com)
1. [Faro](https://www.faro.com/) 1. [Faro](https://www.faro.com/)
@@ -164,11 +153,9 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Hiya](https://hiya.com) 1. [Hiya](https://hiya.com)
1. [Honestbank](https://honestbank.com) 1. [Honestbank](https://honestbank.com)
1. [Hostinger](https://www.hostinger.com) 1. [Hostinger](https://www.hostinger.com)
1. [Hotjar](https://www.hotjar.com)
1. [IABAI](https://www.iab.ai) 1. [IABAI](https://www.iab.ai)
1. [IBM](https://www.ibm.com/) 1. [IBM](https://www.ibm.com/)
1. [Ibotta](https://home.ibotta.com) 1. [Ibotta](https://home.ibotta.com)
1. [Icelandair](https://www.icelandair.com)
1. [IFS](https://www.ifs.com) 1. [IFS](https://www.ifs.com)
1. [IITS-Consulting](https://iits-consulting.de) 1. [IITS-Consulting](https://iits-consulting.de)
1. [IllumiDesk](https://www.illumidesk.com) 1. [IllumiDesk](https://www.illumidesk.com)
@@ -178,7 +165,6 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Info Support](https://www.infosupport.com/) 1. [Info Support](https://www.infosupport.com/)
1. [InsideBoard](https://www.insideboard.com) 1. [InsideBoard](https://www.insideboard.com)
1. [Instruqt](https://www.instruqt.com) 1. [Instruqt](https://www.instruqt.com)
1. [Intel](https://www.intel.com)
1. [Intuit](https://www.intuit.com/) 1. [Intuit](https://www.intuit.com/)
1. [Jellysmack](https://www.jellysmack.com) 1. [Jellysmack](https://www.jellysmack.com)
1. [Joblift](https://joblift.com/) 1. [Joblift](https://joblift.com/)
@@ -273,7 +259,6 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Patreon](https://www.patreon.com/) 1. [Patreon](https://www.patreon.com/)
1. [PayIt](https://payitgov.com/) 1. [PayIt](https://payitgov.com/)
1. [PayPay](https://paypay.ne.jp/) 1. [PayPay](https://paypay.ne.jp/)
1. [Paystack](https://paystack.com/)
1. [Peloton Interactive](https://www.onepeloton.com/) 1. [Peloton Interactive](https://www.onepeloton.com/)
1. [Percona](https://percona.com/) 1. [Percona](https://percona.com/)
1. [PGS](https://www.pgs.com) 1. [PGS](https://www.pgs.com)
@@ -320,20 +305,16 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Saloodo! GmbH](https://www.saloodo.com) 1. [Saloodo! GmbH](https://www.saloodo.com)
1. [Sap Labs](http://sap.com) 1. [Sap Labs](http://sap.com)
1. [Sauce Labs](https://saucelabs.com/) 1. [Sauce Labs](https://saucelabs.com/)
1. [Schneider Electric](https://www.se.com)
1. [Schwarz IT](https://jobs.schwarz/it-mission) 1. [Schwarz IT](https://jobs.schwarz/it-mission)
1. [SCRM Lidl International Hub](https://scrm.lidl) 1. [SCRM Lidl International Hub](https://scrm.lidl)
1. [SEEK](https://seek.com.au) 1. [SEEK](https://seek.com.au)
1. [SEKAI](https://www.sekai.io/) 1. [SEKAI](https://www.sekai.io/)
1. [Semgrep](https://semgrep.com) 1. [Semgrep](https://semgrep.com)
1. [Seznam.cz](https://o-seznam.cz/)
1. [Shield](https://shield.com) 1. [Shield](https://shield.com)
1. [Shipfox](https://www.shipfox.io)
1. [Shock Media](https://www.shockmedia.nl)
1. [SI Analytics](https://si-analytics.ai) 1. [SI Analytics](https://si-analytics.ai)
1. [Sidewalk Entertainment](https://sidewalkplay.com/) 1. [Sidewalk Entertainment](https://sidewalkplay.com/)
1. [Skit](https://skit.ai/) 1. [Skit](https://skit.ai/)
1. [Skribble](https://skribble.com) 1. [Skribble](https://skribble.com)
1. [Skyscanner](https://www.skyscanner.net/) 1. [Skyscanner](https://www.skyscanner.net/)
1. [Smart Pension](https://www.smartpension.co.uk/) 1. [Smart Pension](https://www.smartpension.co.uk/)
1. [Smilee.io](https://smilee.io) 1. [Smilee.io](https://smilee.io)
@@ -342,7 +323,6 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Snapp](https://snapp.ir/) 1. [Snapp](https://snapp.ir/)
1. [Snyk](https://snyk.io/) 1. [Snyk](https://snyk.io/)
1. [Softway Medical](https://www.softwaymedical.fr/) 1. [Softway Medical](https://www.softwaymedical.fr/)
1. [Sophotech](https://sopho.tech)
1. [South China Morning Post (SCMP)](https://www.scmp.com/) 1. [South China Morning Post (SCMP)](https://www.scmp.com/)
1. [Speee](https://speee.jp/) 1. [Speee](https://speee.jp/)
1. [Spendesk](https://spendesk.com/) 1. [Spendesk](https://spendesk.com/)
@@ -355,13 +335,11 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Sumo Logic](https://sumologic.com/) 1. [Sumo Logic](https://sumologic.com/)
1. [Sutpc](http://www.sutpc.com/) 1. [Sutpc](http://www.sutpc.com/)
1. [Swiss Post](https://github.com/swisspost) 1. [Swiss Post](https://github.com/swisspost)
1. [Swissblock Technologies](https://swissblock.net/)
1. [Swisscom](https://www.swisscom.ch) 1. [Swisscom](https://www.swisscom.ch)
1. [Swissquote](https://github.com/swissquote) 1. [Swissquote](https://github.com/swissquote)
1. [Syncier](https://syncier.com/) 1. [Syncier](https://syncier.com/)
1. [Synergy](https://synergy.net.au) 1. [Synergy](https://synergy.net.au)
1. [Syself](https://syself.com) 1. [Syself](https://syself.com)
1. [T-ROC Global](https://trocglobal.com/)
1. [TableCheck](https://tablecheck.com/) 1. [TableCheck](https://tablecheck.com/)
1. [Tailor Brands](https://www.tailorbrands.com) 1. [Tailor Brands](https://www.tailorbrands.com)
1. [Tamkeen Technologies](https://tamkeentech.sa/) 1. [Tamkeen Technologies](https://tamkeentech.sa/)
@@ -403,7 +381,6 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Volvo Cars](https://www.volvocars.com/) 1. [Volvo Cars](https://www.volvocars.com/)
1. [Voyager Digital](https://www.investvoyager.com/) 1. [Voyager Digital](https://www.investvoyager.com/)
1. [VSHN - The DevOps Company](https://vshn.ch/) 1. [VSHN - The DevOps Company](https://vshn.ch/)
1. [Wakacje.pl](https://www.wakacje.pl/)
1. [Walkbase](https://www.walkbase.com/) 1. [Walkbase](https://www.walkbase.com/)
1. [Webstores](https://www.webstores.nl) 1. [Webstores](https://www.webstores.nl)
1. [Wehkamp](https://www.wehkamp.nl/) 1. [Wehkamp](https://www.wehkamp.nl/)
@@ -419,8 +396,8 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Yieldlab](https://www.yieldlab.de/) 1. [Yieldlab](https://www.yieldlab.de/)
1. [Youverify](https://youverify.co/) 1. [Youverify](https://youverify.co/)
1. [Yubo](https://www.yubo.live/) 1. [Yubo](https://www.yubo.live/)
1. [Yuno](https://y.uno/)
1. [ZDF](https://www.zdf.de/) 1. [ZDF](https://www.zdf.de/)
1. [Zimpler](https://www.zimpler.com/) 1. [Zimpler](https://www.zimpler.com/)
1. [ZipRecruiter](https://www.ziprecruiter.com/) 1. [ZipRecuiter](https://www.ziprecruiter.com/)
1. [ZOZO](https://corp.zozo.com/) 1. [ZOZO](https://corp.zozo.com/)

View File

@@ -1 +1 @@
3.2.8 3.0.0

View File

@@ -16,7 +16,6 @@ package controllers
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"reflect" "reflect"
"runtime/debug" "runtime/debug"
@@ -52,7 +51,6 @@ import (
"github.com/argoproj/argo-cd/v3/applicationset/status" "github.com/argoproj/argo-cd/v3/applicationset/status"
"github.com/argoproj/argo-cd/v3/applicationset/utils" "github.com/argoproj/argo-cd/v3/applicationset/utils"
"github.com/argoproj/argo-cd/v3/common" "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/db"
argov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" argov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
@@ -68,19 +66,11 @@ const (
// https://github.com/argoproj-labs/argocd-notifications/blob/33d345fa838829bb50fca5c08523aba380d2c12b/pkg/controller/state.go#L17 // https://github.com/argoproj-labs/argocd-notifications/blob/33d345fa838829bb50fca5c08523aba380d2c12b/pkg/controller/state.go#L17
NotifiedAnnotationKey = "notified.notifications.argoproj.io" NotifiedAnnotationKey = "notified.notifications.argoproj.io"
ReconcileRequeueOnValidationError = time.Minute * 3 ReconcileRequeueOnValidationError = time.Minute * 3
ReverseDeletionOrder = "Reverse"
AllAtOnceDeletionOrder = "AllAtOnce"
) )
var defaultPreservedAnnotations = []string{ var defaultPreservedAnnotations = []string{
NotifiedAnnotationKey, NotifiedAnnotationKey,
argov1alpha1.AnnotationKeyRefresh, argov1alpha1.AnnotationKeyRefresh,
argov1alpha1.AnnotationKeyHydrate,
}
type deleteInOrder struct {
AppName string
Step int
} }
// ApplicationSetReconciler reconciles a ApplicationSet object // ApplicationSetReconciler reconciles a ApplicationSet object
@@ -101,7 +91,6 @@ type ApplicationSetReconciler struct {
GlobalPreservedAnnotations []string GlobalPreservedAnnotations []string
GlobalPreservedLabels []string GlobalPreservedLabels []string
Metrics *metrics.ApplicationsetMetrics Metrics *metrics.ApplicationsetMetrics
MaxResourcesStatusCount int
} }
// +kubebuilder:rbac:groups=argoproj.io,resources=applicationsets,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=argoproj.io,resources=applicationsets,verbs=get;list;watch;create;update;patch;delete
@@ -138,8 +127,8 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
}() }()
// Do not attempt to further reconcile the ApplicationSet if it is being deleted. // Do not attempt to further reconcile the ApplicationSet if it is being deleted.
if applicationSetInfo.DeletionTimestamp != nil { if applicationSetInfo.ObjectMeta.DeletionTimestamp != nil {
appsetName := applicationSetInfo.Name appsetName := applicationSetInfo.ObjectMeta.Name
logCtx.Debugf("DeletionTimestamp is set on %s", appsetName) logCtx.Debugf("DeletionTimestamp is set on %s", appsetName)
deleteAllowed := utils.DefaultPolicy(applicationSetInfo.Spec.SyncPolicy, r.Policy, r.EnablePolicyOverride).AllowDelete() deleteAllowed := utils.DefaultPolicy(applicationSetInfo.Spec.SyncPolicy, r.Policy, r.EnablePolicyOverride).AllowDelete()
if !deleteAllowed { if !deleteAllowed {
@@ -149,19 +138,6 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
} }
logCtx.Debugf("ownerReferences referring %s is deleted from generated applications", appsetName) logCtx.Debugf("ownerReferences referring %s is deleted from generated applications", appsetName)
} }
if isProgressiveSyncDeletionOrderReversed(&applicationSetInfo) {
logCtx.Debugf("DeletionOrder is set as Reverse on %s", appsetName)
currentApplications, err := r.getCurrentApplications(ctx, applicationSetInfo)
if err != nil {
return ctrl.Result{}, err
}
requeueTime, err := r.performReverseDeletion(ctx, logCtx, applicationSetInfo, currentApplications)
if err != nil {
return ctrl.Result{}, err
} else if requeueTime > 0 {
return ctrl.Result{RequeueAfter: requeueTime}, err
}
}
controllerutil.RemoveFinalizer(&applicationSetInfo, argov1alpha1.ResourcesFinalizerName) controllerutil.RemoveFinalizer(&applicationSetInfo, argov1alpha1.ResourcesFinalizerName)
if err := r.Update(ctx, &applicationSetInfo); err != nil { if err := r.Update(ctx, &applicationSetInfo); err != nil {
return ctrl.Result{}, err return ctrl.Result{}, err
@@ -177,7 +153,7 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
// Log a warning if there are unrecognized generators // Log a warning if there are unrecognized generators
_ = utils.CheckInvalidGenerators(&applicationSetInfo) _ = utils.CheckInvalidGenerators(&applicationSetInfo)
// desiredApplications is the main list of all expected Applications from all generators in this appset. // desiredApplications is the main list of all expected Applications from all generators in this appset.
generatedApplications, applicationSetReason, err := template.GenerateApplications(logCtx, applicationSetInfo, r.Generators, r.Renderer, r.Client) desiredApplications, applicationSetReason, err := template.GenerateApplications(logCtx, applicationSetInfo, r.Generators, r.Renderer, r.Client)
if err != nil { if err != nil {
logCtx.Errorf("unable to generate applications: %v", err) logCtx.Errorf("unable to generate applications: %v", err)
_ = r.setApplicationSetStatusCondition(ctx, _ = r.setApplicationSetStatusCondition(ctx,
@@ -195,7 +171,7 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
parametersGenerated = true parametersGenerated = true
validateErrors, err := r.validateGeneratedApplications(ctx, generatedApplications, applicationSetInfo) validateErrors, err := r.validateGeneratedApplications(ctx, desiredApplications, applicationSetInfo)
if err != nil { if err != nil {
// While some generators may return an error that requires user intervention, // While some generators may return an error that requires user intervention,
// other generators reference external resources that may change to cause // other generators reference external resources that may change to cause
@@ -234,55 +210,39 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
appSyncMap := map[string]bool{} appSyncMap := map[string]bool{}
if r.EnableProgressiveSyncs { if r.EnableProgressiveSyncs {
if !isRollingSyncStrategy(&applicationSetInfo) && len(applicationSetInfo.Status.ApplicationStatus) > 0 { if applicationSetInfo.Spec.Strategy == nil && len(applicationSetInfo.Status.ApplicationStatus) > 0 {
// If an appset was previously syncing with a `RollingSync` strategy but it has switched to the default strategy, clean up the progressive sync application statuses // If appset used progressive sync but stopped, clean up the progressive sync application statuses
logCtx.Infof("Removing %v unnecessary AppStatus entries from ApplicationSet %v", len(applicationSetInfo.Status.ApplicationStatus), applicationSetInfo.Name) logCtx.Infof("Removing %v unnecessary AppStatus entries from ApplicationSet %v", len(applicationSetInfo.Status.ApplicationStatus), applicationSetInfo.Name)
err := r.setAppSetApplicationStatus(ctx, logCtx, &applicationSetInfo, []argov1alpha1.ApplicationSetApplicationStatus{}) err := r.setAppSetApplicationStatus(ctx, logCtx, &applicationSetInfo, []argov1alpha1.ApplicationSetApplicationStatus{})
if err != nil { if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to clear previous AppSet application statuses for %v: %w", applicationSetInfo.Name, err) return ctrl.Result{}, fmt.Errorf("failed to clear previous AppSet application statuses for %v: %w", applicationSetInfo.Name, err)
} }
} else if isRollingSyncStrategy(&applicationSetInfo) { } else if applicationSetInfo.Spec.Strategy != nil {
// The appset uses progressive sync with `RollingSync` strategy // appset uses progressive sync
for _, app := range currentApplications { for _, app := range currentApplications {
appMap[app.Name] = app appMap[app.Name] = app
} }
appSyncMap, err = r.performProgressiveSyncs(ctx, logCtx, applicationSetInfo, currentApplications, generatedApplications, appMap) appSyncMap, err = r.performProgressiveSyncs(ctx, logCtx, applicationSetInfo, currentApplications, desiredApplications, appMap)
if err != nil { if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to perform progressive sync reconciliation for application set: %w", err) return ctrl.Result{}, fmt.Errorf("failed to perform progressive sync reconciliation for application set: %w", err)
} }
} }
} else {
// Progressive Sync is disabled, clear any existing applicationStatus to prevent stale data
if len(applicationSetInfo.Status.ApplicationStatus) > 0 {
logCtx.Infof("Progressive Sync disabled, removing %v AppStatus entries from ApplicationSet %v", len(applicationSetInfo.Status.ApplicationStatus), applicationSetInfo.Name)
err := r.setAppSetApplicationStatus(ctx, logCtx, &applicationSetInfo, []argov1alpha1.ApplicationSetApplicationStatus{})
if err != nil {
return ctrl.Result{}, fmt.Errorf("failed to clear AppSet application statuses when Progressive Sync is disabled for %v: %w", applicationSetInfo.Name, err)
}
}
} }
var validApps []argov1alpha1.Application var validApps []argov1alpha1.Application
for i := range generatedApplications { for i := range desiredApplications {
if validateErrors[generatedApplications[i].QualifiedName()] == nil { if validateErrors[i] == nil {
validApps = append(validApps, generatedApplications[i]) validApps = append(validApps, desiredApplications[i])
} }
} }
if len(validateErrors) > 0 { if len(validateErrors) > 0 {
errorApps := make([]string, 0, len(validateErrors))
for key := range validateErrors {
errorApps = append(errorApps, key)
}
sort.Strings(errorApps)
var message string var message string
for _, appName := range errorApps { for _, v := range validateErrors {
message = validateErrors[appName].Error() message = v.Error()
logCtx.WithField("application", appName).Errorf("validation error found during application validation: %s", message) logCtx.Errorf("validation error found during application validation: %s", message)
} }
if len(validateErrors) > 1 { if len(validateErrors) > 1 {
// Only the last message gets added to the appset status, to keep the size reasonable. // Only the last message gets added to the appset status, to keep the size reasonable.
@@ -337,12 +297,12 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
} }
if utils.DefaultPolicy(applicationSetInfo.Spec.SyncPolicy, r.Policy, r.EnablePolicyOverride).AllowDelete() { if utils.DefaultPolicy(applicationSetInfo.Spec.SyncPolicy, r.Policy, r.EnablePolicyOverride).AllowDelete() {
err = r.deleteInCluster(ctx, logCtx, applicationSetInfo, generatedApplications) err = r.deleteInCluster(ctx, logCtx, applicationSetInfo, desiredApplications)
if err != nil { if err != nil {
_ = r.setApplicationSetStatusCondition(ctx, _ = r.setApplicationSetStatusCondition(ctx,
&applicationSetInfo, &applicationSetInfo,
argov1alpha1.ApplicationSetCondition{ argov1alpha1.ApplicationSetCondition{
Type: argov1alpha1.ApplicationSetConditionErrorOccurred, Type: argov1alpha1.ApplicationSetConditionResourcesUpToDate,
Message: err.Error(), Message: err.Error(),
Reason: argov1alpha1.ApplicationSetReasonDeleteApplicationError, Reason: argov1alpha1.ApplicationSetReasonDeleteApplicationError,
Status: argov1alpha1.ApplicationSetConditionStatusTrue, Status: argov1alpha1.ApplicationSetConditionStatusTrue,
@@ -354,7 +314,7 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
if applicationSetInfo.RefreshRequired() { if applicationSetInfo.RefreshRequired() {
delete(applicationSetInfo.Annotations, common.AnnotationApplicationSetRefresh) delete(applicationSetInfo.Annotations, common.AnnotationApplicationSetRefresh)
err := r.Update(ctx, &applicationSetInfo) err := r.Client.Update(ctx, &applicationSetInfo)
if err != nil { if err != nil {
logCtx.Warnf("error occurred while updating ApplicationSet: %v", err) logCtx.Warnf("error occurred while updating ApplicationSet: %v", err)
_ = r.setApplicationSetStatusCondition(ctx, _ = r.setApplicationSetStatusCondition(ctx,
@@ -396,169 +356,120 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
}, nil }, nil
} }
func (r *ApplicationSetReconciler) performReverseDeletion(ctx context.Context, logCtx *log.Entry, appset argov1alpha1.ApplicationSet, currentApps []argov1alpha1.Application) (time.Duration, error) {
requeueTime := 10 * time.Second
stepLength := len(appset.Spec.Strategy.RollingSync.Steps)
// map applications by name using current applications
appMap := make(map[string]*argov1alpha1.Application)
for _, app := range currentApps {
appMap[app.Name] = &app
}
// Get Rolling Sync Step Maps
_, appStepMap := r.buildAppDependencyList(logCtx, appset, currentApps)
// reverse the AppStepMap to perform deletion
var reverseDeleteAppSteps []deleteInOrder
for appName, appStep := range appStepMap {
reverseDeleteAppSteps = append(reverseDeleteAppSteps, deleteInOrder{appName, stepLength - appStep - 1})
}
sort.Slice(reverseDeleteAppSteps, func(i, j int) bool {
return reverseDeleteAppSteps[i].Step < reverseDeleteAppSteps[j].Step
})
for _, step := range reverseDeleteAppSteps {
logCtx.Infof("step %v : app %v", step.Step, step.AppName)
app := appMap[step.AppName]
retrievedApp := argov1alpha1.Application{}
if err := r.Get(ctx, types.NamespacedName{Name: app.Name, Namespace: app.Namespace}, &retrievedApp); err != nil {
if apierrors.IsNotFound(err) {
logCtx.Infof("application %s successfully deleted", step.AppName)
continue
}
}
// Check if the application is already being deleted
if retrievedApp.DeletionTimestamp != nil {
logCtx.Infof("application %s has been marked for deletion, but object not removed yet", step.AppName)
if time.Since(retrievedApp.DeletionTimestamp.Time) > 2*time.Minute {
return 0, errors.New("application has not been deleted in over 2 minutes")
}
}
// The application has not been deleted yet, trigger its deletion
if err := r.Delete(ctx, &retrievedApp); err != nil {
return 0, err
}
return requeueTime, nil
}
logCtx.Infof("completed reverse deletion for ApplicationSet %v", appset.Name)
return 0, nil
}
func getParametersGeneratedCondition(parametersGenerated bool, message string) argov1alpha1.ApplicationSetCondition { func getParametersGeneratedCondition(parametersGenerated bool, message string) argov1alpha1.ApplicationSetCondition {
var parametersGeneratedCondition argov1alpha1.ApplicationSetCondition var paramtersGeneratedCondition argov1alpha1.ApplicationSetCondition
if parametersGenerated { if parametersGenerated {
parametersGeneratedCondition = argov1alpha1.ApplicationSetCondition{ paramtersGeneratedCondition = argov1alpha1.ApplicationSetCondition{
Type: argov1alpha1.ApplicationSetConditionParametersGenerated, Type: argov1alpha1.ApplicationSetConditionParametersGenerated,
Message: "Successfully generated parameters for all Applications", Message: "Successfully generated parameters for all Applications",
Reason: argov1alpha1.ApplicationSetReasonParametersGenerated, Reason: argov1alpha1.ApplicationSetReasonParametersGenerated,
Status: argov1alpha1.ApplicationSetConditionStatusTrue, Status: argov1alpha1.ApplicationSetConditionStatusTrue,
} }
} else { } else {
parametersGeneratedCondition = argov1alpha1.ApplicationSetCondition{ paramtersGeneratedCondition = argov1alpha1.ApplicationSetCondition{
Type: argov1alpha1.ApplicationSetConditionParametersGenerated, Type: argov1alpha1.ApplicationSetConditionParametersGenerated,
Message: message, Message: message,
Reason: argov1alpha1.ApplicationSetReasonErrorOccurred, Reason: argov1alpha1.ApplicationSetReasonErrorOccurred,
Status: argov1alpha1.ApplicationSetConditionStatusFalse, Status: argov1alpha1.ApplicationSetConditionStatusFalse,
} }
} }
return parametersGeneratedCondition return paramtersGeneratedCondition
} }
func (r *ApplicationSetReconciler) setApplicationSetStatusCondition(ctx context.Context, applicationSet *argov1alpha1.ApplicationSet, condition argov1alpha1.ApplicationSetCondition, parametersGenerated bool) error { func getResourceUpToDateCondition(errorOccurred bool, message string, reason string) argov1alpha1.ApplicationSetCondition {
// Initialize the default condition types that this method evaluates var resourceUpToDateCondition argov1alpha1.ApplicationSetCondition
if errorOccurred {
resourceUpToDateCondition = argov1alpha1.ApplicationSetCondition{
Type: argov1alpha1.ApplicationSetConditionResourcesUpToDate,
Message: message,
Reason: reason,
Status: argov1alpha1.ApplicationSetConditionStatusFalse,
}
} else {
resourceUpToDateCondition = argov1alpha1.ApplicationSetCondition{
Type: argov1alpha1.ApplicationSetConditionResourcesUpToDate,
Message: "ApplicationSet up to date",
Reason: argov1alpha1.ApplicationSetReasonApplicationSetUpToDate,
Status: argov1alpha1.ApplicationSetConditionStatusTrue,
}
}
return resourceUpToDateCondition
}
func (r *ApplicationSetReconciler) setApplicationSetStatusCondition(ctx context.Context, applicationSet *argov1alpha1.ApplicationSet, condition argov1alpha1.ApplicationSetCondition, paramtersGenerated bool) error {
// check if error occurred during reconcile process
errOccurred := condition.Type == argov1alpha1.ApplicationSetConditionErrorOccurred
var errOccurredCondition argov1alpha1.ApplicationSetCondition
if errOccurred {
errOccurredCondition = condition
} else {
errOccurredCondition = argov1alpha1.ApplicationSetCondition{
Type: argov1alpha1.ApplicationSetConditionErrorOccurred,
Message: "Successfully generated parameters for all Applications",
Reason: argov1alpha1.ApplicationSetReasonApplicationSetUpToDate,
Status: argov1alpha1.ApplicationSetConditionStatusFalse,
}
}
paramtersGeneratedCondition := getParametersGeneratedCondition(paramtersGenerated, condition.Message)
resourceUpToDateCondition := getResourceUpToDateCondition(errOccurred, condition.Message, condition.Reason)
evaluatedTypes := map[argov1alpha1.ApplicationSetConditionType]bool{ evaluatedTypes := map[argov1alpha1.ApplicationSetConditionType]bool{
argov1alpha1.ApplicationSetConditionErrorOccurred: true,
argov1alpha1.ApplicationSetConditionParametersGenerated: true, argov1alpha1.ApplicationSetConditionParametersGenerated: true,
argov1alpha1.ApplicationSetConditionErrorOccurred: false, argov1alpha1.ApplicationSetConditionResourcesUpToDate: true,
argov1alpha1.ApplicationSetConditionResourcesUpToDate: false,
argov1alpha1.ApplicationSetConditionRolloutProgressing: false,
} }
// Evaluate current condition newConditions := []argov1alpha1.ApplicationSetCondition{errOccurredCondition, paramtersGeneratedCondition, resourceUpToDateCondition}
evaluatedTypes[condition.Type] = true
newConditions := []argov1alpha1.ApplicationSetCondition{condition}
if !isRollingSyncStrategy(applicationSet) { if progressiveSyncsRollingSyncStrategyEnabled(applicationSet) {
// Progressing sync is always evaluated so conditions are removed when it is not enabled
evaluatedTypes[argov1alpha1.ApplicationSetConditionRolloutProgressing] = true evaluatedTypes[argov1alpha1.ApplicationSetConditionRolloutProgressing] = true
}
// Evaluate ParametersGenerated since it is always provided if condition.Type == argov1alpha1.ApplicationSetConditionRolloutProgressing {
if condition.Type != argov1alpha1.ApplicationSetConditionParametersGenerated { newConditions = append(newConditions, condition)
newConditions = append(newConditions, getParametersGeneratedCondition(parametersGenerated, condition.Message))
}
// Evaluate dependencies between conditions.
switch condition.Type {
case argov1alpha1.ApplicationSetConditionResourcesUpToDate:
if condition.Status == argov1alpha1.ApplicationSetConditionStatusTrue {
// If the resources are up to date, we know there was no errors
evaluatedTypes[argov1alpha1.ApplicationSetConditionErrorOccurred] = true
newConditions = append(newConditions, argov1alpha1.ApplicationSetCondition{
Type: argov1alpha1.ApplicationSetConditionErrorOccurred,
Status: argov1alpha1.ApplicationSetConditionStatusFalse,
Reason: condition.Reason,
Message: condition.Message,
})
}
case argov1alpha1.ApplicationSetConditionErrorOccurred:
if condition.Status == argov1alpha1.ApplicationSetConditionStatusTrue {
// If there is an error anywhere in the reconciliation, we cannot consider the resources up to date
evaluatedTypes[argov1alpha1.ApplicationSetConditionResourcesUpToDate] = true
newConditions = append(newConditions, argov1alpha1.ApplicationSetCondition{
Type: argov1alpha1.ApplicationSetConditionResourcesUpToDate,
Status: argov1alpha1.ApplicationSetConditionStatusFalse,
Reason: argov1alpha1.ApplicationSetReasonErrorOccurred,
Message: condition.Message,
})
}
case argov1alpha1.ApplicationSetConditionRolloutProgressing:
if !isRollingSyncStrategy(applicationSet) {
// if the condition is a rolling sync and it is disabled, ignore it
evaluatedTypes[condition.Type] = false
} }
} }
// Update the applicationSet conditions needToUpdateConditions := false
previousConditions := applicationSet.Status.Conditions for _, condition := range newConditions {
applicationSet.Status.SetConditions(newConditions, evaluatedTypes) // do nothing if appset already has same condition
for _, c := range applicationSet.Status.Conditions {
// Try to not call get/update if nothing has changed if c.Type == condition.Type && (c.Reason != condition.Reason || c.Status != condition.Status || c.Message != condition.Message) {
needToUpdateConditions := len(applicationSet.Status.Conditions) != len(previousConditions)
if !needToUpdateConditions {
for i, c := range applicationSet.Status.Conditions {
previous := previousConditions[i]
if c.Type != previous.Type || c.Reason != previous.Reason || c.Status != previous.Status || c.Message != previous.Message {
needToUpdateConditions = true needToUpdateConditions = true
break break
} }
} }
} }
if !needToUpdateConditions { if needToUpdateConditions || len(applicationSet.Status.Conditions) < len(newConditions) {
return nil // fetch updated Application Set object before updating it
} // DefaultRetry will retry 5 times with a backoff factor of 1, jitter of 0.1 and a duration of 10ms
// DefaultRetry will retry 5 times with a backoff factor of 1, jitter of 0.1 and a duration of 10ms err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
err := retry.RetryOnConflict(retry.DefaultRetry, func() error { namespacedName := types.NamespacedName{Namespace: applicationSet.Namespace, Name: applicationSet.Name}
updatedAppset := &argov1alpha1.ApplicationSet{} updatedAppset := &argov1alpha1.ApplicationSet{}
if err := r.Get(ctx, types.NamespacedName{Namespace: applicationSet.Namespace, Name: applicationSet.Name}, updatedAppset); err != nil { if err := r.Get(ctx, namespacedName, updatedAppset); err != nil {
if client.IgnoreNotFound(err) != nil { if client.IgnoreNotFound(err) != nil {
return nil return nil
}
return fmt.Errorf("error fetching updated application set: %w", err)
} }
return fmt.Errorf("error fetching updated application set: %w", err)
}
updatedAppset.Status.SetConditions(newConditions, evaluatedTypes) updatedAppset.Status.SetConditions(
newConditions, evaluatedTypes,
)
// Update the newly fetched object with new set of conditions // Update the newly fetched object with new set of conditions
err := r.Client.Status().Update(ctx, updatedAppset) err := r.Client.Status().Update(ctx, updatedAppset)
if err != nil { if err != nil {
return err return err
}
updatedAppset.DeepCopyInto(applicationSet)
return nil
})
if err != nil && !apierrors.IsNotFound(err) {
return fmt.Errorf("unable to set application set condition: %w", err)
} }
updatedAppset.DeepCopyInto(applicationSet)
return nil
})
if err != nil && !apierrors.IsNotFound(err) {
return fmt.Errorf("unable to set application set condition: %w", err)
} }
return nil return nil
@@ -566,33 +477,33 @@ func (r *ApplicationSetReconciler) setApplicationSetStatusCondition(ctx context.
// validateGeneratedApplications uses the Argo CD validation functions to verify the correctness of the // validateGeneratedApplications uses the Argo CD validation functions to verify the correctness of the
// generated applications. // generated applications.
func (r *ApplicationSetReconciler) validateGeneratedApplications(ctx context.Context, desiredApplications []argov1alpha1.Application, applicationSetInfo argov1alpha1.ApplicationSet) (map[string]error, error) { func (r *ApplicationSetReconciler) validateGeneratedApplications(ctx context.Context, desiredApplications []argov1alpha1.Application, applicationSetInfo argov1alpha1.ApplicationSet) (map[int]error, error) {
errorsByApp := map[string]error{} errorsByIndex := map[int]error{}
namesSet := map[string]bool{} namesSet := map[string]bool{}
for i := range desiredApplications { for i, app := range desiredApplications {
app := &desiredApplications[i]
if namesSet[app.Name] { if namesSet[app.Name] {
errorsByApp[app.QualifiedName()] = fmt.Errorf("ApplicationSet %s contains applications with duplicate name: %s", applicationSetInfo.Name, app.Name) errorsByIndex[i] = fmt.Errorf("ApplicationSet %s contains applications with duplicate name: %s", applicationSetInfo.Name, app.Name)
continue continue
} }
namesSet[app.Name] = true namesSet[app.Name] = true
appProject := &argov1alpha1.AppProject{} appProject := &argov1alpha1.AppProject{}
err := r.Get(ctx, types.NamespacedName{Name: app.Spec.Project, Namespace: r.ArgoCDNamespace}, appProject) err := r.Client.Get(ctx, types.NamespacedName{Name: app.Spec.Project, Namespace: r.ArgoCDNamespace}, appProject)
if err != nil { if err != nil {
if apierrors.IsNotFound(err) { if apierrors.IsNotFound(err) {
errorsByApp[app.QualifiedName()] = fmt.Errorf("application references project %s which does not exist", app.Spec.Project) errorsByIndex[i] = fmt.Errorf("application references project %s which does not exist", app.Spec.Project)
continue continue
} }
return nil, err return nil, err
} }
if _, err = argoutil.GetDestinationCluster(ctx, app.Spec.Destination, r.ArgoDB); err != nil { if _, err = argoutil.GetDestinationCluster(ctx, app.Spec.Destination, r.ArgoDB); err != nil {
errorsByApp[app.QualifiedName()] = fmt.Errorf("application destination spec is invalid: %s", err.Error()) errorsByIndex[i] = fmt.Errorf("application destination spec is invalid: %s", err.Error())
continue continue
} }
} }
return errorsByApp, nil return errorsByIndex, nil
} }
func (r *ApplicationSetReconciler) getMinRequeueAfter(applicationSetInfo *argov1alpha1.ApplicationSet) time.Duration { func (r *ApplicationSetReconciler) getMinRequeueAfter(applicationSetInfo *argov1alpha1.ApplicationSet) time.Duration {
@@ -642,7 +553,7 @@ func (r *ApplicationSetReconciler) SetupWithManager(mgr ctrl.Manager, enableProg
} }
appOwnsHandler := getApplicationOwnsHandler(enableProgressiveSyncs) appOwnsHandler := getApplicationOwnsHandler(enableProgressiveSyncs)
appSetOwnsHandler := getApplicationSetOwnsHandler(enableProgressiveSyncs) appSetOwnsHandler := getApplicationSetOwnsHandler()
return ctrl.NewControllerManagedBy(mgr).WithOptions(controller.Options{ return ctrl.NewControllerManagedBy(mgr).WithOptions(controller.Options{
MaxConcurrentReconciles: maxConcurrentReconciliations, MaxConcurrentReconciles: maxConcurrentReconciliations,
@@ -652,9 +563,8 @@ func (r *ApplicationSetReconciler) SetupWithManager(mgr ctrl.Manager, enableProg
Watches( Watches(
&corev1.Secret{}, &corev1.Secret{},
&clusterSecretEventHandler{ &clusterSecretEventHandler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
Log: log.WithField("type", "createSecretEventHandler"), Log: log.WithField("type", "createSecretEventHandler"),
ApplicationSetNamespaces: r.ApplicationSetNamespaces,
}). }).
Complete(r) Complete(r)
} }
@@ -667,7 +577,7 @@ func (r *ApplicationSetReconciler) createOrUpdateInCluster(ctx context.Context,
var firstError error var firstError error
// Creates or updates the application in appList // Creates or updates the application in appList
for _, generatedApp := range desiredApplications { for _, generatedApp := range desiredApplications {
appLog := logCtx.WithFields(applog.GetAppLogFields(&generatedApp)) appLog := logCtx.WithFields(log.Fields{"app": generatedApp.QualifiedName()})
// Normalize to avoid fighting with the application controller. // Normalize to avoid fighting with the application controller.
generatedApp.Spec = *argoutil.NormalizeApplicationSpec(&generatedApp.Spec) generatedApp.Spec = *argoutil.NormalizeApplicationSpec(&generatedApp.Spec)
@@ -714,7 +624,7 @@ func (r *ApplicationSetReconciler) createOrUpdateInCluster(ctx context.Context,
preservedAnnotations = append(preservedAnnotations, defaultPreservedAnnotations...) preservedAnnotations = append(preservedAnnotations, defaultPreservedAnnotations...)
for _, key := range preservedAnnotations { for _, key := range preservedAnnotations {
if state, exists := found.Annotations[key]; exists { if state, exists := found.ObjectMeta.Annotations[key]; exists {
if generatedApp.Annotations == nil { if generatedApp.Annotations == nil {
generatedApp.Annotations = map[string]string{} generatedApp.Annotations = map[string]string{}
} }
@@ -723,7 +633,7 @@ func (r *ApplicationSetReconciler) createOrUpdateInCluster(ctx context.Context,
} }
for _, key := range preservedLabels { for _, key := range preservedLabels {
if state, exists := found.Labels[key]; exists { if state, exists := found.ObjectMeta.Labels[key]; exists {
if generatedApp.Labels == nil { if generatedApp.Labels == nil {
generatedApp.Labels = map[string]string{} generatedApp.Labels = map[string]string{}
} }
@@ -733,7 +643,7 @@ func (r *ApplicationSetReconciler) createOrUpdateInCluster(ctx context.Context,
// Preserve post-delete finalizers: // Preserve post-delete finalizers:
// https://github.com/argoproj/argo-cd/issues/17181 // https://github.com/argoproj/argo-cd/issues/17181
for _, finalizer := range found.Finalizers { for _, finalizer := range found.ObjectMeta.Finalizers {
if strings.HasPrefix(finalizer, argov1alpha1.PostDeleteFinalizerName) { if strings.HasPrefix(finalizer, argov1alpha1.PostDeleteFinalizerName) {
if generatedApp.Finalizers == nil { if generatedApp.Finalizers == nil {
generatedApp.Finalizers = []string{} generatedApp.Finalizers = []string{}
@@ -742,10 +652,10 @@ func (r *ApplicationSetReconciler) createOrUpdateInCluster(ctx context.Context,
} }
} }
found.Annotations = generatedApp.Annotations found.ObjectMeta.Annotations = generatedApp.Annotations
found.Finalizers = generatedApp.Finalizers found.ObjectMeta.Finalizers = generatedApp.Finalizers
found.Labels = generatedApp.Labels found.ObjectMeta.Labels = generatedApp.Labels
return controllerutil.SetControllerReference(&applicationSet, found, r.Scheme) return controllerutil.SetControllerReference(&applicationSet, found, r.Scheme)
}) })
@@ -799,7 +709,7 @@ func (r *ApplicationSetReconciler) createInCluster(ctx context.Context, logCtx *
func (r *ApplicationSetReconciler) getCurrentApplications(ctx context.Context, applicationSet argov1alpha1.ApplicationSet) ([]argov1alpha1.Application, error) { func (r *ApplicationSetReconciler) getCurrentApplications(ctx context.Context, applicationSet argov1alpha1.ApplicationSet) ([]argov1alpha1.Application, error) {
var current argov1alpha1.ApplicationList var current argov1alpha1.ApplicationList
err := r.List(ctx, &current, client.MatchingFields{".metadata.controller": applicationSet.Name}, client.InNamespace(applicationSet.Namespace)) err := r.Client.List(ctx, &current, client.MatchingFields{".metadata.controller": applicationSet.Name}, client.InNamespace(applicationSet.Namespace))
if err != nil { if err != nil {
return nil, fmt.Errorf("error retrieving applications: %w", err) return nil, fmt.Errorf("error retrieving applications: %w", err)
} }
@@ -821,7 +731,7 @@ func (r *ApplicationSetReconciler) deleteInCluster(ctx context.Context, logCtx *
return fmt.Errorf("error getting current applications: %w", err) return fmt.Errorf("error getting current applications: %w", err)
} }
m := make(map[string]bool) // will hold the app names in appList for the deletion process m := make(map[string]bool) // Will holds the app names in appList for the deletion process
for _, app := range desiredApplications { for _, app := range desiredApplications {
m[app.Name] = true m[app.Name] = true
@@ -830,7 +740,7 @@ func (r *ApplicationSetReconciler) deleteInCluster(ctx context.Context, logCtx *
// Delete apps that are not in m[string]bool // Delete apps that are not in m[string]bool
var firstError error var firstError error
for _, app := range current { for _, app := range current {
logCtx = logCtx.WithFields(applog.GetAppLogFields(&app)) logCtx = logCtx.WithField("app", app.QualifiedName())
_, exists := m[app.Name] _, exists := m[app.Name]
if !exists { if !exists {
@@ -844,7 +754,7 @@ func (r *ApplicationSetReconciler) deleteInCluster(ctx context.Context, logCtx *
continue continue
} }
err = r.Delete(ctx, &app) err = r.Client.Delete(ctx, &app)
if err != nil { if err != nil {
logCtx.WithError(err).Error("failed to delete Application") logCtx.WithError(err).Error("failed to delete Application")
if firstError != nil { if firstError != nil {
@@ -870,7 +780,7 @@ func (r *ApplicationSetReconciler) removeFinalizerOnInvalidDestination(ctx conte
// Detect if the destination is invalid (name doesn't correspond to a matching cluster) // Detect if the destination is invalid (name doesn't correspond to a matching cluster)
if destCluster, err := argoutil.GetDestinationCluster(ctx, app.Spec.Destination, r.ArgoDB); err != nil { if destCluster, err := argoutil.GetDestinationCluster(ctx, app.Spec.Destination, r.ArgoDB); err != nil {
appLog.Warnf("The destination cluster for %s could not be found: %v", app.Name, err) appLog.Warnf("The destination cluster for %s couldn't be found: %v", app.Name, err)
validDestination = false validDestination = false
} else { } else {
// Detect if the destination's server field does not match an existing cluster // Detect if the destination's server field does not match an existing cluster
@@ -889,7 +799,7 @@ func (r *ApplicationSetReconciler) removeFinalizerOnInvalidDestination(ctx conte
} }
if !matchingCluster { if !matchingCluster {
appLog.Warnf("A match for the destination cluster for %s, by server url, could not be found", app.Name) appLog.Warnf("A match for the destination cluster for %s, by server url, couldn't be found.", app.Name)
} }
validDestination = matchingCluster validDestination = matchingCluster
@@ -913,7 +823,7 @@ func (r *ApplicationSetReconciler) removeFinalizerOnInvalidDestination(ctx conte
if log.IsLevelEnabled(log.DebugLevel) { if log.IsLevelEnabled(log.DebugLevel) {
utils.LogPatch(appLog, patch, updated) utils.LogPatch(appLog, patch, updated)
} }
if err := r.Patch(ctx, updated, patch); err != nil { if err := r.Client.Patch(ctx, updated, patch); err != nil {
return fmt.Errorf("error updating finalizers: %w", err) return fmt.Errorf("error updating finalizers: %w", err)
} }
// Application must have updated list of finalizers // Application must have updated list of finalizers
@@ -935,7 +845,7 @@ func (r *ApplicationSetReconciler) removeOwnerReferencesOnDeleteAppSet(ctx conte
for _, app := range applications { for _, app := range applications {
app.SetOwnerReferences([]metav1.OwnerReference{}) app.SetOwnerReferences([]metav1.OwnerReference{})
err := r.Update(ctx, &app) err := r.Client.Update(ctx, &app)
if err != nil { if err != nil {
return fmt.Errorf("error updating application: %w", err) return fmt.Errorf("error updating application: %w", err)
} }
@@ -1090,19 +1000,8 @@ func appSyncEnabledForNextStep(appset *argov1alpha1.ApplicationSet, app argov1al
return true return true
} }
func isRollingSyncStrategy(appset *argov1alpha1.ApplicationSet) bool {
// It's only RollingSync if the type specifically sets it
return appset.Spec.Strategy != nil && appset.Spec.Strategy.Type == "RollingSync" && appset.Spec.Strategy.RollingSync != nil
}
func progressiveSyncsRollingSyncStrategyEnabled(appset *argov1alpha1.ApplicationSet) bool { func progressiveSyncsRollingSyncStrategyEnabled(appset *argov1alpha1.ApplicationSet) bool {
// ProgressiveSync is enabled if the strategy is set to `RollingSync` + steps slice is not empty return appset.Spec.Strategy != nil && appset.Spec.Strategy.RollingSync != nil && appset.Spec.Strategy.Type == "RollingSync" && len(appset.Spec.Strategy.RollingSync.Steps) > 0
return isRollingSyncStrategy(appset) && len(appset.Spec.Strategy.RollingSync.Steps) > 0
}
func isProgressiveSyncDeletionOrderReversed(appset *argov1alpha1.ApplicationSet) bool {
// When progressive sync is enabled + deletionOrder is set to Reverse (case-insensitive)
return progressiveSyncsRollingSyncStrategyEnabled(appset) && strings.EqualFold(appset.Spec.Strategy.DeletionOrder, ReverseDeletionOrder)
} }
func isApplicationHealthy(app argov1alpha1.Application) bool { func isApplicationHealthy(app argov1alpha1.Application) bool {
@@ -1234,10 +1133,15 @@ func (r *ApplicationSetReconciler) updateApplicationSetApplicationStatusProgress
// if we have no RollingUpdate steps, clear out the existing ApplicationStatus entries // if we have no RollingUpdate steps, clear out the existing ApplicationStatus entries
if progressiveSyncsRollingSyncStrategyEnabled(applicationSet) { if progressiveSyncsRollingSyncStrategyEnabled(applicationSet) {
updateCountMap := []int{}
totalCountMap := []int{}
length := len(applicationSet.Spec.Strategy.RollingSync.Steps) length := len(applicationSet.Spec.Strategy.RollingSync.Steps)
updateCountMap := make([]int, length) for s := 0; s < length; s++ {
totalCountMap := make([]int, length) updateCountMap = append(updateCountMap, 0)
totalCountMap = append(totalCountMap, 0)
}
// populate updateCountMap with counts of existing Pending and Progressing Applications // populate updateCountMap with counts of existing Pending and Progressing Applications
for _, appStatus := range applicationSet.Status.ApplicationStatus { for _, appStatus := range applicationSet.Status.ApplicationStatus {
@@ -1296,56 +1200,44 @@ func (r *ApplicationSetReconciler) updateApplicationSetApplicationStatusProgress
} }
func (r *ApplicationSetReconciler) updateApplicationSetApplicationStatusConditions(ctx context.Context, applicationSet *argov1alpha1.ApplicationSet) []argov1alpha1.ApplicationSetCondition { func (r *ApplicationSetReconciler) updateApplicationSetApplicationStatusConditions(ctx context.Context, applicationSet *argov1alpha1.ApplicationSet) []argov1alpha1.ApplicationSetCondition {
if !isRollingSyncStrategy(applicationSet) { appSetProgressing := false
return applicationSet.Status.Conditions
}
completedWaves := map[string]bool{}
for _, appStatus := range applicationSet.Status.ApplicationStatus { for _, appStatus := range applicationSet.Status.ApplicationStatus {
if v, ok := completedWaves[appStatus.Step]; !ok { if appStatus.Status != "Healthy" {
completedWaves[appStatus.Step] = appStatus.Status == "Healthy" appSetProgressing = true
} else {
completedWaves[appStatus.Step] = v && appStatus.Status == "Healthy"
}
}
isProgressing := false
progressingStep := ""
for i := range applicationSet.Spec.Strategy.RollingSync.Steps {
step := strconv.Itoa(i + 1)
isCompleted, ok := completedWaves[step]
if !ok {
// Step has no applications, so it is completed
continue
}
if !isCompleted {
isProgressing = true
progressingStep = step
break break
} }
} }
if isProgressing { appSetConditionProgressing := false
for _, appSetCondition := range applicationSet.Status.Conditions {
if appSetCondition.Type == argov1alpha1.ApplicationSetConditionRolloutProgressing && appSetCondition.Status == argov1alpha1.ApplicationSetConditionStatusTrue {
appSetConditionProgressing = true
break
}
}
if appSetProgressing && !appSetConditionProgressing {
_ = r.setApplicationSetStatusCondition(ctx, _ = r.setApplicationSetStatusCondition(ctx,
applicationSet, applicationSet,
argov1alpha1.ApplicationSetCondition{ argov1alpha1.ApplicationSetCondition{
Type: argov1alpha1.ApplicationSetConditionRolloutProgressing, Type: argov1alpha1.ApplicationSetConditionRolloutProgressing,
Message: "ApplicationSet is performing rollout of step " + progressingStep, Message: "ApplicationSet Rollout Rollout started",
Reason: argov1alpha1.ApplicationSetReasonApplicationSetModified, Reason: argov1alpha1.ApplicationSetReasonApplicationSetModified,
Status: argov1alpha1.ApplicationSetConditionStatusTrue, Status: argov1alpha1.ApplicationSetConditionStatusTrue,
}, true, }, true,
) )
} else { } else if !appSetProgressing && appSetConditionProgressing {
_ = r.setApplicationSetStatusCondition(ctx, _ = r.setApplicationSetStatusCondition(ctx,
applicationSet, applicationSet,
argov1alpha1.ApplicationSetCondition{ argov1alpha1.ApplicationSetCondition{
Type: argov1alpha1.ApplicationSetConditionRolloutProgressing, Type: argov1alpha1.ApplicationSetConditionRolloutProgressing,
Message: "ApplicationSet Rollout has completed", Message: "ApplicationSet Rollout Rollout complete",
Reason: argov1alpha1.ApplicationSetReasonApplicationSetRolloutComplete, Reason: argov1alpha1.ApplicationSetReasonApplicationSetRolloutComplete,
Status: argov1alpha1.ApplicationSetConditionStatusFalse, Status: argov1alpha1.ApplicationSetConditionStatusFalse,
}, true, }, true,
) )
} }
return applicationSet.Status.Conditions return applicationSet.Status.Conditions
} }
@@ -1411,13 +1303,7 @@ func (r *ApplicationSetReconciler) updateResourcesStatus(ctx context.Context, lo
sort.Slice(statuses, func(i, j int) bool { sort.Slice(statuses, func(i, j int) bool {
return statuses[i].Name < statuses[j].Name return statuses[i].Name < statuses[j].Name
}) })
resourcesCount := int64(len(statuses))
if r.MaxResourcesStatusCount > 0 && len(statuses) > r.MaxResourcesStatusCount {
logCtx.Warnf("Truncating ApplicationSet %s resource status from %d to max allowed %d entries", appset.Name, len(statuses), r.MaxResourcesStatusCount)
statuses = statuses[:r.MaxResourcesStatusCount]
}
appset.Status.Resources = statuses appset.Status.Resources = statuses
appset.Status.ResourcesCount = resourcesCount
// DefaultRetry will retry 5 times with a backoff factor of 1, jitter of 0.1 and a duration of 10ms // DefaultRetry will retry 5 times with a backoff factor of 1, jitter of 0.1 and a duration of 10ms
err := retry.RetryOnConflict(retry.DefaultRetry, func() error { err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
namespacedName := types.NamespacedName{Namespace: appset.Namespace, Name: appset.Name} namespacedName := types.NamespacedName{Namespace: appset.Namespace, Name: appset.Name}
@@ -1430,7 +1316,6 @@ func (r *ApplicationSetReconciler) updateResourcesStatus(ctx context.Context, lo
} }
updatedAppset.Status.Resources = appset.Status.Resources updatedAppset.Status.Resources = appset.Status.Resources
updatedAppset.Status.ResourcesCount = resourcesCount
// Update the newly fetched object with new status resources // Update the newly fetched object with new status resources
err := r.Client.Status().Update(ctx, updatedAppset) err := r.Client.Status().Update(ctx, updatedAppset)
@@ -1453,37 +1338,17 @@ func (r *ApplicationSetReconciler) setAppSetApplicationStatus(ctx context.Contex
needToUpdateStatus := false needToUpdateStatus := false
if len(applicationStatuses) != len(applicationSet.Status.ApplicationStatus) { if len(applicationStatuses) != len(applicationSet.Status.ApplicationStatus) {
logCtx.WithFields(log.Fields{
"current_count": len(applicationSet.Status.ApplicationStatus),
"expected_count": len(applicationStatuses),
}).Debug("application status count changed")
needToUpdateStatus = true needToUpdateStatus = true
} else { } else {
for i := range applicationStatuses { for i := range applicationStatuses {
appStatus := applicationStatuses[i] appStatus := applicationStatuses[i]
idx := findApplicationStatusIndex(applicationSet.Status.ApplicationStatus, appStatus.Application) idx := findApplicationStatusIndex(applicationSet.Status.ApplicationStatus, appStatus.Application)
if idx == -1 { if idx == -1 {
logCtx.WithFields(log.Fields{"application": appStatus.Application}).Debug("application not found in current status")
needToUpdateStatus = true needToUpdateStatus = true
break break
} }
currentStatus := applicationSet.Status.ApplicationStatus[idx] currentStatus := applicationSet.Status.ApplicationStatus[idx]
statusChanged := currentStatus.Status != appStatus.Status if currentStatus.Message != appStatus.Message || currentStatus.Status != appStatus.Status || currentStatus.Step != appStatus.Step {
stepChanged := currentStatus.Step != appStatus.Step
messageChanged := currentStatus.Message != appStatus.Message
if statusChanged || stepChanged || messageChanged {
if statusChanged {
logCtx.WithFields(log.Fields{"application": appStatus.Application, "previous_status": currentStatus.Status, "new_status": appStatus.Status}).
Debug("application status changed")
}
if stepChanged {
logCtx.WithFields(log.Fields{"application": appStatus.Application, "previous_step": currentStatus.Step, "new_step": appStatus.Step}).
Debug("application step changed")
}
if messageChanged {
logCtx.WithFields(log.Fields{"application": appStatus.Application}).Debug("application message changed")
}
needToUpdateStatus = true needToUpdateStatus = true
break break
} }
@@ -1491,17 +1356,17 @@ func (r *ApplicationSetReconciler) setAppSetApplicationStatus(ctx context.Contex
} }
if needToUpdateStatus { if needToUpdateStatus {
// sort to make sure the array is always in the same order namespacedName := types.NamespacedName{Namespace: applicationSet.Namespace, Name: applicationSet.Name}
applicationSet.Status.ApplicationStatus = make([]argov1alpha1.ApplicationSetApplicationStatus, len(applicationStatuses))
copy(applicationSet.Status.ApplicationStatus, applicationStatuses)
sort.Slice(applicationSet.Status.ApplicationStatus, func(i, j int) bool {
return applicationSet.Status.ApplicationStatus[i].Application < applicationSet.Status.ApplicationStatus[j].Application
})
// rebuild ApplicationStatus from scratch, we don't need any previous status history
applicationSet.Status.ApplicationStatus = []argov1alpha1.ApplicationSetApplicationStatus{}
for i := range applicationStatuses {
applicationSet.Status.SetApplicationStatus(applicationStatuses[i])
}
// DefaultRetry will retry 5 times with a backoff factor of 1, jitter of 0.1 and a duration of 10ms // DefaultRetry will retry 5 times with a backoff factor of 1, jitter of 0.1 and a duration of 10ms
err := retry.RetryOnConflict(retry.DefaultRetry, func() error { err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
updatedAppset := &argov1alpha1.ApplicationSet{} updatedAppset := &argov1alpha1.ApplicationSet{}
if err := r.Get(ctx, types.NamespacedName{Namespace: applicationSet.Namespace, Name: applicationSet.Name}, updatedAppset); err != nil { if err := r.Get(ctx, namespacedName, updatedAppset); err != nil {
if client.IgnoreNotFound(err) != nil { if client.IgnoreNotFound(err) != nil {
return nil return nil
} }
@@ -1533,7 +1398,7 @@ func (r *ApplicationSetReconciler) syncValidApplications(logCtx *log.Entry, appl
pruneEnabled := false pruneEnabled := false
// ensure that Applications generated with RollingSync do not have an automated sync policy, since the AppSet controller will handle triggering the sync operation instead // ensure that Applications generated with RollingSync do not have an automated sync policy, since the AppSet controller will handle triggering the sync operation instead
if validApps[i].Spec.SyncPolicy != nil && validApps[i].Spec.SyncPolicy.IsAutomatedSyncEnabled() { if validApps[i].Spec.SyncPolicy != nil && validApps[i].Spec.SyncPolicy.Automated != nil {
pruneEnabled = validApps[i].Spec.SyncPolicy.Automated.Prune pruneEnabled = validApps[i].Spec.SyncPolicy.Automated.Prune
validApps[i].Spec.SyncPolicy.Automated = nil validApps[i].Spec.SyncPolicy.Automated = nil
} }
@@ -1565,14 +1430,10 @@ func syncApplication(application argov1alpha1.Application, prune bool) argov1alp
Info: []*argov1alpha1.Info{ Info: []*argov1alpha1.Info{
{ {
Name: "Reason", Name: "Reason",
Value: "ApplicationSet RollingSync triggered a sync of this Application resource", Value: "ApplicationSet RollingSync triggered a sync of this Application resource.",
}, },
}, },
Sync: &argov1alpha1.SyncOperation{}, Sync: &argov1alpha1.SyncOperation{},
// Set a retry limit of 5, aligning with the default in Argo CD's appcontroller auto-sync behavior.
// This provides consistency for retry behavior across controllers.
// See: https://github.com/argoproj/argo-cd/blob/af9ebac0bb35dc16eb034c1cefaf7c92d1029927/controller/appcontroller.go#L2126
Retry: argov1alpha1.RetryStrategy{Limit: 5},
} }
if application.Spec.SyncPolicy != nil { if application.Spec.SyncPolicy != nil {
@@ -1595,23 +1456,23 @@ func getApplicationOwnsHandler(enableProgressiveSyncs bool) predicate.Funcs {
// if we are the owner and there is a create event, we most likely created it and do not need to // if we are the owner and there is a create event, we most likely created it and do not need to
// re-reconcile // re-reconcile
if log.IsLevelEnabled(log.DebugLevel) { if log.IsLevelEnabled(log.DebugLevel) {
logFields := log.Fields{"app": ""} var appName string
app, isApp := e.Object.(*argov1alpha1.Application) app, isApp := e.Object.(*argov1alpha1.Application)
if isApp { if isApp {
logFields = applog.GetAppLogFields(app) appName = app.QualifiedName()
} }
log.WithFields(logFields).Debugln("received create event from owning an application") log.WithField("app", appName).Debugln("received create event from owning an application")
} }
return false return false
}, },
DeleteFunc: func(e event.DeleteEvent) bool { DeleteFunc: func(e event.DeleteEvent) bool {
if log.IsLevelEnabled(log.DebugLevel) { if log.IsLevelEnabled(log.DebugLevel) {
logFields := log.Fields{"app": ""} var appName string
app, isApp := e.Object.(*argov1alpha1.Application) app, isApp := e.Object.(*argov1alpha1.Application)
if isApp { if isApp {
logFields = applog.GetAppLogFields(app) appName = app.QualifiedName()
} }
log.WithFields(logFields).Debugln("received delete event from owning an application") log.WithField("app", appName).Debugln("received delete event from owning an application")
} }
return true return true
}, },
@@ -1620,7 +1481,7 @@ func getApplicationOwnsHandler(enableProgressiveSyncs bool) predicate.Funcs {
if !isApp { if !isApp {
return false return false
} }
logCtx := log.WithFields(applog.GetAppLogFields(appOld)) logCtx := log.WithField("app", appOld.QualifiedName())
logCtx.Debugln("received update event from owning an application") logCtx.Debugln("received update event from owning an application")
appNew, isApp := e.ObjectNew.(*argov1alpha1.Application) appNew, isApp := e.ObjectNew.(*argov1alpha1.Application)
if !isApp { if !isApp {
@@ -1632,12 +1493,12 @@ func getApplicationOwnsHandler(enableProgressiveSyncs bool) predicate.Funcs {
}, },
GenericFunc: func(e event.GenericEvent) bool { GenericFunc: func(e event.GenericEvent) bool {
if log.IsLevelEnabled(log.DebugLevel) { if log.IsLevelEnabled(log.DebugLevel) {
logFields := log.Fields{} var appName string
app, isApp := e.Object.(*argov1alpha1.Application) app, isApp := e.Object.(*argov1alpha1.Application)
if isApp { if isApp {
logFields = applog.GetAppLogFields(app) appName = app.QualifiedName()
} }
log.WithFields(logFields).Debugln("received generic event from owning an application") log.WithField("app", appName).Debugln("received generic event from owning an application")
} }
return true return true
}, },
@@ -1660,9 +1521,9 @@ func shouldRequeueForApplication(appOld *argov1alpha1.Application, appNew *argov
// https://pkg.go.dev/reflect#DeepEqual // https://pkg.go.dev/reflect#DeepEqual
// ApplicationDestination has an unexported field so we can just use the == for comparison // ApplicationDestination has an unexported field so we can just use the == for comparison
if !cmp.Equal(appOld.Spec, appNew.Spec, cmpopts.EquateEmpty(), cmpopts.EquateComparable(argov1alpha1.ApplicationDestination{})) || if !cmp.Equal(appOld.Spec, appNew.Spec, cmpopts.EquateEmpty(), cmpopts.EquateComparable(argov1alpha1.ApplicationDestination{})) ||
!cmp.Equal(appOld.GetAnnotations(), appNew.GetAnnotations(), cmpopts.EquateEmpty()) || !cmp.Equal(appOld.ObjectMeta.GetAnnotations(), appNew.ObjectMeta.GetAnnotations(), cmpopts.EquateEmpty()) ||
!cmp.Equal(appOld.GetLabels(), appNew.GetLabels(), cmpopts.EquateEmpty()) || !cmp.Equal(appOld.ObjectMeta.GetLabels(), appNew.ObjectMeta.GetLabels(), cmpopts.EquateEmpty()) ||
!cmp.Equal(appOld.GetFinalizers(), appNew.GetFinalizers(), cmpopts.EquateEmpty()) { !cmp.Equal(appOld.ObjectMeta.GetFinalizers(), appNew.ObjectMeta.GetFinalizers(), cmpopts.EquateEmpty()) {
return true return true
} }
@@ -1683,7 +1544,7 @@ func shouldRequeueForApplication(appOld *argov1alpha1.Application, appNew *argov
return false return false
} }
func getApplicationSetOwnsHandler(enableProgressiveSyncs bool) predicate.Funcs { func getApplicationSetOwnsHandler() predicate.Funcs {
return predicate.Funcs{ return predicate.Funcs{
CreateFunc: func(e event.CreateEvent) bool { CreateFunc: func(e event.CreateEvent) bool {
appSet, isApp := e.Object.(*argov1alpha1.ApplicationSet) appSet, isApp := e.Object.(*argov1alpha1.ApplicationSet)
@@ -1712,7 +1573,7 @@ func getApplicationSetOwnsHandler(enableProgressiveSyncs bool) predicate.Funcs {
if !isAppSet { if !isAppSet {
return false return false
} }
requeue := shouldRequeueForApplicationSet(appSetOld, appSetNew, enableProgressiveSyncs) requeue := shouldRequeueForApplicationSet(appSetOld, appSetNew)
log.WithField("applicationset", appSetNew.QualifiedName()). log.WithField("applicationset", appSetNew.QualifiedName()).
WithField("requeue", requeue).Debugln("received update event") WithField("requeue", requeue).Debugln("received update event")
return requeue return requeue
@@ -1730,33 +1591,24 @@ func getApplicationSetOwnsHandler(enableProgressiveSyncs bool) predicate.Funcs {
} }
// shouldRequeueForApplicationSet determines when we need to requeue an applicationset // shouldRequeueForApplicationSet determines when we need to requeue an applicationset
func shouldRequeueForApplicationSet(appSetOld, appSetNew *argov1alpha1.ApplicationSet, enableProgressiveSyncs bool) bool { func shouldRequeueForApplicationSet(appSetOld, appSetNew *argov1alpha1.ApplicationSet) bool {
if appSetOld == nil || appSetNew == nil { if appSetOld == nil || appSetNew == nil {
return false return false
} }
// only compare the applicationset spec, annotations, labels and finalizers, specifically avoiding
// Requeue if any ApplicationStatus.Status changed for Progressive sync strategy
if enableProgressiveSyncs {
if !cmp.Equal(appSetOld.Status.ApplicationStatus, appSetNew.Status.ApplicationStatus, cmpopts.EquateEmpty()) {
return true
}
}
// only compare the applicationset spec, annotations, labels and finalizers, deletionTimestamp, specifically avoiding
// the status field. status is owned by the applicationset controller, // the status field. status is owned by the applicationset controller,
// and we do not need to requeue when it does bookkeeping // and we do not need to requeue when it does bookkeeping
// NB: the ApplicationDestination comes from the ApplicationSpec being embedded // NB: the ApplicationDestination comes from the ApplicationSpec being embedded
// in the ApplicationSetTemplate from the generators // in the ApplicationSetTemplate from the generators
if !cmp.Equal(appSetOld.Spec, appSetNew.Spec, cmpopts.EquateEmpty(), cmpopts.EquateComparable(argov1alpha1.ApplicationDestination{})) || if !cmp.Equal(appSetOld.Spec, appSetNew.Spec, cmpopts.EquateEmpty(), cmpopts.EquateComparable(argov1alpha1.ApplicationDestination{})) ||
!cmp.Equal(appSetOld.GetLabels(), appSetNew.GetLabels(), cmpopts.EquateEmpty()) || !cmp.Equal(appSetOld.ObjectMeta.GetLabels(), appSetNew.ObjectMeta.GetLabels(), cmpopts.EquateEmpty()) ||
!cmp.Equal(appSetOld.GetFinalizers(), appSetNew.GetFinalizers(), cmpopts.EquateEmpty()) || !cmp.Equal(appSetOld.ObjectMeta.GetFinalizers(), appSetNew.ObjectMeta.GetFinalizers(), cmpopts.EquateEmpty()) {
!cmp.Equal(appSetOld.DeletionTimestamp, appSetNew.DeletionTimestamp, cmpopts.EquateEmpty()) {
return true return true
} }
// Requeue only when the refresh annotation is newly added to the ApplicationSet. // Requeue only when the refresh annotation is newly added to the ApplicationSet.
// Changes to other annotations made simultaneously might be missed, but such cases are rare. // Changes to other annotations made simultaneously might be missed, but such cases are rare.
if !cmp.Equal(appSetOld.GetAnnotations(), appSetNew.GetAnnotations(), cmpopts.EquateEmpty()) { if !cmp.Equal(appSetOld.ObjectMeta.GetAnnotations(), appSetNew.ObjectMeta.GetAnnotations(), cmpopts.EquateEmpty()) {
_, oldHasRefreshAnnotation := appSetOld.Annotations[common.AnnotationApplicationSetRefresh] _, oldHasRefreshAnnotation := appSetOld.Annotations[common.AnnotationApplicationSetRefresh]
_, newHasRefreshAnnotation := appSetNew.Annotations[common.AnnotationApplicationSetRefresh] _, newHasRefreshAnnotation := appSetNew.Annotations[common.AnnotationApplicationSetRefresh]

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/event"
"github.com/argoproj/argo-cd/v3/applicationset/utils"
"github.com/argoproj/argo-cd/v3/common" "github.com/argoproj/argo-cd/v3/common"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
) )
@@ -23,9 +22,8 @@ import (
// requeue any related ApplicationSets. // requeue any related ApplicationSets.
type clusterSecretEventHandler struct { type clusterSecretEventHandler struct {
// handler.EnqueueRequestForOwner // handler.EnqueueRequestForOwner
Log log.FieldLogger Log log.FieldLogger
Client client.Client Client client.Client
ApplicationSetNamespaces []string
} }
func (h *clusterSecretEventHandler) Create(ctx context.Context, e event.CreateEvent, q workqueue.TypedRateLimitingInterface[reconcile.Request]) { 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") h.Log.WithField("count", len(appSetList.Items)).Info("listed ApplicationSets")
for _, appSet := range appSetList.Items { 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 foundClusterGenerator := false
for _, generator := range appSet.Spec.Generators { for _, generator := range appSet.Spec.Generators {
if generator.Clusters != nil { if generator.Clusters != nil {

View File

@@ -20,15 +20,6 @@ import (
argov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" argov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
) )
type mockAddRateLimitingInterface struct {
addedItems []reconcile.Request
}
// Add checks the type, and adds it to the internal list of received additions
func (obj *mockAddRateLimitingInterface) Add(item reconcile.Request) {
obj.addedItems = append(obj.addedItems, item)
}
func TestClusterEventHandler(t *testing.T) { func TestClusterEventHandler(t *testing.T) {
scheme := runtime.NewScheme() scheme := runtime.NewScheme()
err := argov1alpha1.AddToScheme(scheme) err := argov1alpha1.AddToScheme(scheme)
@@ -137,7 +128,7 @@ func TestClusterEventHandler(t *testing.T) {
{ {
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: "my-app-set", Name: "my-app-set",
Namespace: "argocd", Namespace: "another-namespace",
}, },
Spec: argov1alpha1.ApplicationSetSpec{ Spec: argov1alpha1.ApplicationSetSpec{
Generators: []argov1alpha1.ApplicationSetGenerator{ Generators: []argov1alpha1.ApplicationSetGenerator{
@@ -171,37 +162,9 @@ func TestClusterEventHandler(t *testing.T) {
}, },
}, },
expectedRequests: []reconcile.Request{ 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", name: "non-argo cd secret should not match",
items: []argov1alpha1.ApplicationSet{ items: []argov1alpha1.ApplicationSet{
@@ -580,9 +543,8 @@ func TestClusterEventHandler(t *testing.T) {
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithLists(&appSetList).Build() fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithLists(&appSetList).Build()
handler := &clusterSecretEventHandler{ handler := &clusterSecretEventHandler{
Client: fakeClient, Client: fakeClient,
Log: log.WithField("type", "createSecretEventHandler"), Log: log.WithField("type", "createSecretEventHandler"),
ApplicationSetNamespaces: []string{"argocd"},
} }
mockAddRateLimitingInterface := mockAddRateLimitingInterface{} mockAddRateLimitingInterface := mockAddRateLimitingInterface{}
@@ -594,6 +556,15 @@ func TestClusterEventHandler(t *testing.T) {
} }
} }
// Add checks the type, and adds it to the internal list of received additions
func (obj *mockAddRateLimitingInterface) Add(item reconcile.Request) {
obj.addedItems = append(obj.addedItems, item)
}
type mockAddRateLimitingInterface struct {
addedItems []reconcile.Request
}
func TestNestedGeneratorHasClusterGenerator_NestedClusterGenerator(t *testing.T) { func TestNestedGeneratorHasClusterGenerator_NestedClusterGenerator(t *testing.T) {
nested := argov1alpha1.ApplicationSetNestedGenerator{ nested := argov1alpha1.ApplicationSetNestedGenerator{
Clusters: &argov1alpha1.ClusterGenerator{}, Clusters: &argov1alpha1.ClusterGenerator{},

View File

@@ -56,7 +56,7 @@ func TestRequeueAfter(t *testing.T) {
}, },
} }
fakeDynClient := dynfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), gvrToListKind, duckType) fakeDynClient := dynfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), gvrToListKind, duckType)
scmConfig := generators.NewSCMConfig("", []string{""}, true, true, nil, true) scmConfig := generators.NewSCMConfig("", []string{""}, true, nil, true)
terminalGenerators := map[string]generators.Generator{ terminalGenerators := map[string]generators.Generator{
"List": generators.NewListGenerator(), "List": generators.NewListGenerator(),
"Clusters": generators.NewClusterGenerator(ctx, k8sClient, appClientset, "argocd"), "Clusters": generators.NewClusterGenerator(ctx, k8sClient, appClientset, "argocd"),
@@ -129,7 +129,7 @@ func TestRequeueAfter(t *testing.T) {
}}, }},
}, },
}, },
}, ""}, want: generators.DefaultRequeueAfter, wantErr: assert.NoError}, }, ""}, want: generators.DefaultRequeueAfterSeconds, wantErr: assert.NoError},
{name: "ClusterMatrixNested", args: args{&argov1alpha1.ApplicationSet{ {name: "ClusterMatrixNested", args: args{&argov1alpha1.ApplicationSet{
Spec: argov1alpha1.ApplicationSetSpec{ Spec: argov1alpha1.ApplicationSetSpec{
Generators: []argov1alpha1.ApplicationSetGenerator{ Generators: []argov1alpha1.ApplicationSetGenerator{
@@ -144,7 +144,7 @@ func TestRequeueAfter(t *testing.T) {
}}, }},
}, },
}, },
}, ""}, want: generators.DefaultRequeueAfter, wantErr: assert.NoError}, }, ""}, want: generators.DefaultRequeueAfterSeconds, wantErr: assert.NoError},
{name: "ListGenerator", args: args{appset: &argov1alpha1.ApplicationSet{ {name: "ListGenerator", args: args{appset: &argov1alpha1.ApplicationSet{
Spec: argov1alpha1.ApplicationSetSpec{ Spec: argov1alpha1.ApplicationSetSpec{
Generators: []argov1alpha1.ApplicationSetGenerator{{List: &argov1alpha1.ListGenerator{}}}, Generators: []argov1alpha1.ApplicationSetGenerator{{List: &argov1alpha1.ListGenerator{}}},
@@ -154,7 +154,7 @@ func TestRequeueAfter(t *testing.T) {
Spec: argov1alpha1.ApplicationSetSpec{ Spec: argov1alpha1.ApplicationSetSpec{
Generators: []argov1alpha1.ApplicationSetGenerator{{ClusterDecisionResource: &argov1alpha1.DuckTypeGenerator{}}}, Generators: []argov1alpha1.ApplicationSetGenerator{{ClusterDecisionResource: &argov1alpha1.DuckTypeGenerator{}}},
}, },
}}, want: generators.DefaultRequeueAfter, wantErr: assert.NoError}, }}, want: generators.DefaultRequeueAfterSeconds, wantErr: assert.NoError},
{name: "OverrideRequeueDuck", args: args{ {name: "OverrideRequeueDuck", args: args{
appset: &argov1alpha1.ApplicationSet{ appset: &argov1alpha1.ApplicationSet{
Spec: argov1alpha1.ApplicationSetSpec{ Spec: argov1alpha1.ApplicationSetSpec{

View File

@@ -27,7 +27,7 @@ func Test_ApplyTemplatePatch(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: "my-cluster-guestbook", Name: "my-cluster-guestbook",
Namespace: "namespace", Namespace: "namespace",
Finalizers: []string{appv1.ResourcesFinalizerName}, Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
}, },
Spec: appv1.ApplicationSpec{ Spec: appv1.ApplicationSpec{
Project: "default", Project: "default",
@@ -72,7 +72,7 @@ func Test_ApplyTemplatePatch(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: "my-cluster-guestbook", Name: "my-cluster-guestbook",
Namespace: "namespace", Namespace: "namespace",
Finalizers: []string{appv1.ResourcesFinalizerName}, Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
Annotations: map[string]string{ Annotations: map[string]string{
"annotation-some-key": "annotation-some-value", "annotation-some-key": "annotation-some-value",
}, },
@@ -112,7 +112,7 @@ func Test_ApplyTemplatePatch(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: "my-cluster-guestbook", Name: "my-cluster-guestbook",
Namespace: "namespace", Namespace: "namespace",
Finalizers: []string{appv1.ResourcesFinalizerName}, Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
}, },
Spec: appv1.ApplicationSpec{ Spec: appv1.ApplicationSpec{
Project: "default", Project: "default",
@@ -148,7 +148,7 @@ spec:
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: "my-cluster-guestbook", Name: "my-cluster-guestbook",
Namespace: "namespace", Namespace: "namespace",
Finalizers: []string{appv1.ResourcesFinalizerName}, Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
Annotations: map[string]string{ Annotations: map[string]string{
"annotation-some-key": "annotation-some-value", "annotation-some-key": "annotation-some-value",
}, },

View File

@@ -341,10 +341,10 @@ func TestGenerateAppsUsingPullRequestGenerator(t *testing.T) {
renderer, renderer,
nil, nil,
) )
assert.Equal(t, cases.expectedApp[0].Name, gotApp[0].Name) assert.EqualValues(t, cases.expectedApp[0].ObjectMeta.Name, gotApp[0].ObjectMeta.Name)
assert.Equal(t, cases.expectedApp[0].Spec.Source.TargetRevision, gotApp[0].Spec.Source.TargetRevision) assert.EqualValues(t, cases.expectedApp[0].Spec.Source.TargetRevision, gotApp[0].Spec.Source.TargetRevision)
assert.Equal(t, cases.expectedApp[0].Spec.Destination.Namespace, gotApp[0].Spec.Destination.Namespace) assert.EqualValues(t, cases.expectedApp[0].Spec.Destination.Namespace, gotApp[0].Spec.Destination.Namespace)
assert.True(t, maps.Equal(cases.expectedApp[0].Labels, gotApp[0].Labels)) assert.True(t, maps.Equal(cases.expectedApp[0].ObjectMeta.Labels, gotApp[0].ObjectMeta.Labels))
}) })
} }
} }

View File

@@ -14,7 +14,7 @@ spec:
app: guestbook-ui app: guestbook-ui
spec: spec:
containers: containers:
- image: quay.io/argoprojlabs/argocd-e2e-container:0.2 - image: gcr.io/heptio-images/ks-guestbook-demo:0.2
name: guestbook-ui name: guestbook-ui
ports: ports:
- containerPort: 80 - containerPort: 80

View File

@@ -14,7 +14,7 @@ spec:
app: guestbook-ui app: guestbook-ui
spec: spec:
containers: containers:
- image: quay.io/argoprojlabs/argocd-e2e-container:0.2 - image: gcr.io/heptio-images/ks-guestbook-demo:0.2
name: guestbook-ui name: guestbook-ui
ports: ports:
- containerPort: 80 - containerPort: 80

View File

@@ -5,7 +5,7 @@
replicaCount: 1 replicaCount: 1
image: image:
repository: quay.io/argoprojlabs/argocd-e2e-container repository: gcr.io/heptio-images/ks-guestbook-demo
tag: 0.1 tag: 0.1
pullPolicy: IfNotPresent pullPolicy: IfNotPresent

View File

@@ -14,7 +14,7 @@ spec:
app: guestbook-ui app: guestbook-ui
spec: spec:
containers: containers:
- image: quay.io/argoprojlabs/argocd-e2e-container:0.2 - image: gcr.io/heptio-images/ks-guestbook-demo:0.2
name: guestbook-ui name: guestbook-ui
ports: ports:
- containerPort: 80 - containerPort: 80

View File

@@ -1,26 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: guestbook
spec:
generators:
- git:
repoURL: https://github.com/argoproj/argo-cd.git
revision: HEAD
files:
- path: "applicationset/examples/git-generator-files-discovery/cluster-config/**/config.json"
- path: "applicationset/examples/git-generator-files-discovery/cluster-config/*/dev/config.json"
exclude: true
template:
metadata:
name: '{{cluster.name}}-guestbook'
spec:
project: default
source:
repoURL: https://github.com/argoproj/argo-cd.git
targetRevision: HEAD
path: "applicationset/examples/git-generator-files-discovery/apps/guestbook"
destination:
server: https://kubernetes.default.svc
#server: '{{cluster.address}}'
namespace: guestbook

View File

@@ -1,27 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: guestbook
spec:
goTemplate: true
goTemplateOptions: ["missingkey=error"]
generators:
- git:
repoURL: https://github.com/argoproj/argo-cd.git
revision: HEAD
files:
- path: "applicationset/examples/git-generator-files-discovery/cluster-config/**/config.json"
- path: "applicationset/examples/git-generator-files-discovery/cluster-config/*/dev/config.json"
exclude: true
template:
metadata:
name: '{{.cluster.name}}-guestbook'
spec:
project: default
source:
repoURL: https://github.com/argoproj/argo-cd.git
targetRevision: HEAD
path: "applicationset/examples/git-generator-files-discovery/apps/guestbook"
destination:
server: https://kubernetes.default.svc
namespace: guestbook

View File

@@ -14,7 +14,7 @@ spec:
app: guestbook-ui app: guestbook-ui
spec: spec:
containers: containers:
- image: quay.io/argoprojlabs/argocd-e2e-container:0.2 - image: gcr.io/heptio-images/ks-guestbook-demo:0.2
name: guestbook-ui name: guestbook-ui
ports: ports:
- containerPort: 80 - containerPort: 80

View File

@@ -14,7 +14,7 @@ spec:
app: guestbook-ui app: guestbook-ui
spec: spec:
containers: containers:
- image: quay.io/argoprojlabs/argocd-e2e-container:0.2 - image: gcr.io/heptio-images/ks-guestbook-demo:0.2
name: guestbook-ui name: guestbook-ui
ports: ports:
- containerPort: 80 - containerPort: 80

View File

@@ -14,7 +14,7 @@ spec:
app: guestbook-ui app: guestbook-ui
spec: spec:
containers: containers:
- image: quay.io/argoprojlabs/argocd-e2e-container:0.2 - image: gcr.io/heptio-images/ks-guestbook-demo:0.2
name: guestbook-ui name: guestbook-ui
ports: ports:
- containerPort: 80 - containerPort: 80

View File

@@ -14,7 +14,7 @@ spec:
app: guestbook-ui app: guestbook-ui
spec: spec:
containers: containers:
- image: quay.io/argoprojlabs/argocd-e2e-container:0.2 - image: gcr.io/heptio-images/ks-guestbook-demo:0.2
name: guestbook-ui name: guestbook-ui
ports: ports:
- containerPort: 80 - containerPort: 80

View File

@@ -7,6 +7,8 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/argoproj/argo-cd/v3/util/settings"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
@@ -25,17 +27,21 @@ type ClusterGenerator struct {
ctx context.Context ctx context.Context
clientset kubernetes.Interface clientset kubernetes.Interface
// namespace is the Argo CD namespace // namespace is the Argo CD namespace
namespace string namespace string
settingsManager *settings.SettingsManager
} }
var render = &utils.Render{} var render = &utils.Render{}
func NewClusterGenerator(ctx context.Context, c client.Client, clientset kubernetes.Interface, namespace string) Generator { func NewClusterGenerator(ctx context.Context, c client.Client, clientset kubernetes.Interface, namespace string) Generator {
settingsManager := settings.NewSettingsManager(ctx, clientset, namespace)
g := &ClusterGenerator{ g := &ClusterGenerator{
Client: c, Client: c,
ctx: ctx, ctx: ctx,
clientset: clientset, clientset: clientset,
namespace: namespace, namespace: namespace,
settingsManager: settingsManager,
} }
return g return g
} }
@@ -53,11 +59,11 @@ func (g *ClusterGenerator) GetTemplate(appSetGenerator *argoappsetv1alpha1.Appli
func (g *ClusterGenerator) GenerateParams(appSetGenerator *argoappsetv1alpha1.ApplicationSetGenerator, appSet *argoappsetv1alpha1.ApplicationSet, _ client.Client) ([]map[string]any, error) { func (g *ClusterGenerator) GenerateParams(appSetGenerator *argoappsetv1alpha1.ApplicationSetGenerator, appSet *argoappsetv1alpha1.ApplicationSet, _ client.Client) ([]map[string]any, error) {
logCtx := log.WithField("applicationset", appSet.GetName()).WithField("namespace", appSet.GetNamespace()) logCtx := log.WithField("applicationset", appSet.GetName()).WithField("namespace", appSet.GetNamespace())
if appSetGenerator == nil { if appSetGenerator == nil {
return nil, ErrEmptyAppSetGenerator return nil, EmptyAppSetGeneratorError
} }
if appSetGenerator.Clusters == nil { if appSetGenerator.Clusters == nil {
return nil, ErrEmptyAppSetGenerator return nil, EmptyAppSetGeneratorError
} }
// Do not include the local cluster in the cluster parameters IF there is a non-empty selector // Do not include the local cluster in the cluster parameters IF there is a non-empty selector
@@ -79,10 +85,14 @@ func (g *ClusterGenerator) GenerateParams(appSetGenerator *argoappsetv1alpha1.Ap
return nil, fmt.Errorf("error getting cluster secrets: %w", err) return nil, fmt.Errorf("error getting cluster secrets: %w", err)
} }
paramHolder := &paramHolder{isFlatMode: appSetGenerator.Clusters.FlatList} res := []map[string]any{}
logCtx.Debugf("Using flat mode = %t for cluster generator", paramHolder.isFlatMode)
secretsFound := []corev1.Secret{} secretsFound := []corev1.Secret{}
isFlatMode := appSetGenerator.Clusters.FlatList
logCtx.Debugf("Using flat mode = %t for cluster generator", isFlatMode)
clustersParams := make([]map[string]any, 0)
for _, cluster := range clustersFromArgoCD { for _, cluster := range clustersFromArgoCD {
// If there is a secret for this cluster, then it's a non-local cluster, so it will be // If there is a secret for this cluster, then it's a non-local cluster, so it will be
// handled by the next step. // handled by the next step.
@@ -101,80 +111,72 @@ func (g *ClusterGenerator) GenerateParams(appSetGenerator *argoappsetv1alpha1.Ap
return nil, fmt.Errorf("error appending templated values for local cluster: %w", err) return nil, fmt.Errorf("error appending templated values for local cluster: %w", err)
} }
paramHolder.append(params) if isFlatMode {
clustersParams = append(clustersParams, params)
} else {
res = append(res, params)
}
logCtx.WithField("cluster", "local cluster").Info("matched local cluster") logCtx.WithField("cluster", "local cluster").Info("matched local cluster")
} }
} }
// For each matching cluster secret (non-local clusters only) // For each matching cluster secret (non-local clusters only)
for _, cluster := range secretsFound { for _, cluster := range secretsFound {
params := g.getClusterParameters(cluster, appSet) params := map[string]any{}
params["name"] = string(cluster.Data["name"])
params["nameNormalized"] = utils.SanitizeName(string(cluster.Data["name"]))
params["server"] = string(cluster.Data["server"])
project, ok := cluster.Data["project"]
if ok {
params["project"] = string(project)
} else {
params["project"] = ""
}
if appSet.Spec.GoTemplate {
meta := map[string]any{}
if len(cluster.ObjectMeta.Annotations) > 0 {
meta["annotations"] = cluster.ObjectMeta.Annotations
}
if len(cluster.ObjectMeta.Labels) > 0 {
meta["labels"] = cluster.ObjectMeta.Labels
}
params["metadata"] = meta
} else {
for key, value := range cluster.ObjectMeta.Annotations {
params["metadata.annotations."+key] = value
}
for key, value := range cluster.ObjectMeta.Labels {
params["metadata.labels."+key] = value
}
}
err = appendTemplatedValues(appSetGenerator.Clusters.Values, params, appSet.Spec.GoTemplate, appSet.Spec.GoTemplateOptions) err = appendTemplatedValues(appSetGenerator.Clusters.Values, params, appSet.Spec.GoTemplate, appSet.Spec.GoTemplateOptions)
if err != nil { if err != nil {
return nil, fmt.Errorf("error appending templated values for cluster: %w", err) return nil, fmt.Errorf("error appending templated values for cluster: %w", err)
} }
paramHolder.append(params) if isFlatMode {
clustersParams = append(clustersParams, params)
} else {
res = append(res, params)
}
logCtx.WithField("cluster", cluster.Name).Debug("matched cluster secret") logCtx.WithField("cluster", cluster.Name).Debug("matched cluster secret")
} }
return paramHolder.consolidate(), nil if isFlatMode {
} res = append(res, map[string]any{
"clusters": clustersParams,
type paramHolder struct { })
isFlatMode bool
params []map[string]any
}
func (p *paramHolder) append(params map[string]any) {
p.params = append(p.params, params)
}
func (p *paramHolder) consolidate() []map[string]any {
if p.isFlatMode {
p.params = []map[string]any{
{"clusters": p.params},
}
} }
return p.params return res, nil
}
func (g *ClusterGenerator) getClusterParameters(cluster corev1.Secret, appSet *argoappsetv1alpha1.ApplicationSet) map[string]any {
params := map[string]any{}
params["name"] = string(cluster.Data["name"])
params["nameNormalized"] = utils.SanitizeName(string(cluster.Data["name"]))
params["server"] = string(cluster.Data["server"])
project, ok := cluster.Data["project"]
if ok {
params["project"] = string(project)
} else {
params["project"] = ""
}
if appSet.Spec.GoTemplate {
meta := map[string]any{}
if len(cluster.Annotations) > 0 {
meta["annotations"] = cluster.Annotations
}
if len(cluster.Labels) > 0 {
meta["labels"] = cluster.Labels
}
params["metadata"] = meta
} else {
for key, value := range cluster.Annotations {
params["metadata.annotations."+key] = value
}
for key, value := range cluster.Labels {
params["metadata.labels."+key] = value
}
}
return params
} }
func (g *ClusterGenerator) getSecretsByClusterName(log *log.Entry, appSetGenerator *argoappsetv1alpha1.ApplicationSetGenerator) (map[string]corev1.Secret, error) { func (g *ClusterGenerator) getSecretsByClusterName(log *log.Entry, appSetGenerator *argoappsetv1alpha1.ApplicationSetGenerator) (map[string]corev1.Secret, error) {
@@ -186,7 +188,7 @@ func (g *ClusterGenerator) getSecretsByClusterName(log *log.Entry, appSetGenerat
return nil, fmt.Errorf("error converting label selector: %w", err) return nil, fmt.Errorf("error converting label selector: %w", err)
} }
if err := g.List(context.Background(), clusterSecretList, client.MatchingLabelsSelector{Selector: secretSelector}); err != nil { if err := g.Client.List(context.Background(), clusterSecretList, client.MatchingLabelsSelector{Selector: secretSelector}); err != nil {
return nil, err return nil, err
} }
log.Debugf("clusters matching labels: %d", len(clusterSecretList.Items)) log.Debugf("clusters matching labels: %d", len(clusterSecretList.Items))

View File

@@ -10,8 +10,9 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/argoproj/argo-cd/v3/util/settings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
@@ -25,18 +26,22 @@ var _ Generator = (*DuckTypeGenerator)(nil)
// DuckTypeGenerator generates Applications for some or all clusters registered with ArgoCD. // DuckTypeGenerator generates Applications for some or all clusters registered with ArgoCD.
type DuckTypeGenerator struct { type DuckTypeGenerator struct {
ctx context.Context ctx context.Context
dynClient dynamic.Interface dynClient dynamic.Interface
clientset kubernetes.Interface clientset kubernetes.Interface
namespace string // namespace is the Argo CD namespace namespace string // namespace is the Argo CD namespace
settingsManager *settings.SettingsManager
} }
func NewDuckTypeGenerator(ctx context.Context, dynClient dynamic.Interface, clientset kubernetes.Interface, namespace string) Generator { func NewDuckTypeGenerator(ctx context.Context, dynClient dynamic.Interface, clientset kubernetes.Interface, namespace string) Generator {
settingsManager := settings.NewSettingsManager(ctx, clientset, namespace)
g := &DuckTypeGenerator{ g := &DuckTypeGenerator{
ctx: ctx, ctx: ctx,
dynClient: dynClient, dynClient: dynClient,
clientset: clientset, clientset: clientset,
namespace: namespace, namespace: namespace,
settingsManager: settingsManager,
} }
return g return g
} }
@@ -57,12 +62,12 @@ func (g *DuckTypeGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.Appl
func (g *DuckTypeGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, appSet *argoprojiov1alpha1.ApplicationSet, _ client.Client) ([]map[string]any, error) { func (g *DuckTypeGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, appSet *argoprojiov1alpha1.ApplicationSet, _ client.Client) ([]map[string]any, error) {
if appSetGenerator == nil { if appSetGenerator == nil {
return nil, ErrEmptyAppSetGenerator return nil, EmptyAppSetGeneratorError
} }
// Not likely to happen // Not likely to happen
if appSetGenerator.ClusterDecisionResource == nil { if appSetGenerator.ClusterDecisionResource == nil {
return nil, ErrEmptyAppSetGenerator return nil, EmptyAppSetGeneratorError
} }
// ListCluster from Argo CD's util/db package will include the local cluster in the list of clusters // ListCluster from Argo CD's util/db package will include the local cluster in the list of clusters
@@ -92,13 +97,13 @@ func (g *DuckTypeGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.A
// Validate the fields // Validate the fields
if kind == "" || versionIdx < 1 { if kind == "" || versionIdx < 1 {
log.Warningf("kind=%v, resourceName=%v, versionIdx=%v", kind, resourceName, versionIdx) log.Warningf("kind=%v, resourceName=%v, versionIdx=%v", kind, resourceName, versionIdx)
return nil, errors.New("there is a problem with the apiVersion, kind or resourceName provided") return nil, errors.New("There is a problem with the apiVersion, kind or resourceName provided")
} }
if (resourceName == "" && labelSelector.MatchLabels == nil && labelSelector.MatchExpressions == nil) || if (resourceName == "" && labelSelector.MatchLabels == nil && labelSelector.MatchExpressions == nil) ||
(resourceName != "" && (labelSelector.MatchExpressions != nil || labelSelector.MatchLabels != nil)) { (resourceName != "" && (labelSelector.MatchExpressions != nil || labelSelector.MatchLabels != nil)) {
log.Warningf("You must choose either resourceName=%v, labelSelector.matchLabels=%v or labelSelect.matchExpressions=%v", resourceName, labelSelector.MatchLabels, labelSelector.MatchExpressions) log.Warningf("You must choose either resourceName=%v, labelSelector.matchLabels=%v or labelSelect.matchExpressions=%v", resourceName, labelSelector.MatchLabels, labelSelector.MatchExpressions)
return nil, errors.New("there is a problem with the definition of the ClusterDecisionResource generator") return nil, errors.New("There is a problem with the definition of the ClusterDecisionResource generator")
} }
// Split up the apiVersion // Split up the apiVersion
@@ -131,51 +136,18 @@ func (g *DuckTypeGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.A
// Override the duck type in the status of the resource // Override the duck type in the status of the resource
statusListKey := "clusters" statusListKey := "clusters"
matchKey := cm.Data["matchKey"]
if cm.Data["statusListKey"] != "" { if cm.Data["statusListKey"] != "" {
statusListKey = cm.Data["statusListKey"] statusListKey = cm.Data["statusListKey"]
} }
matchKey := cm.Data["matchKey"]
if matchKey == "" { if matchKey == "" {
log.WithField("matchKey", matchKey).Warning("matchKey not found in " + cm.Name) log.WithField("matchKey", matchKey).Warning("matchKey not found in " + cm.Name)
return nil, nil return nil, nil
} }
clusterDecisions := buildClusterDecisions(duckResources, statusListKey)
if len(clusterDecisions) == 0 {
log.Warningf("clusterDecisionResource status.%s missing", statusListKey)
return nil, nil
}
res := []map[string]any{} res := []map[string]any{}
for _, clusterDecision := range clusterDecisions {
cluster := findCluster(clustersFromArgoCD, clusterDecision, matchKey, statusListKey)
// if no cluster is found, move to the next cluster
if cluster == nil {
continue
}
// generated instance of cluster params
params := map[string]any{
"name": cluster.Name,
"server": cluster.Server,
}
for key, value := range clusterDecision.(map[string]any) {
params[key] = value.(string)
}
for key, value := range appSetGenerator.ClusterDecisionResource.Values {
collectParams(appSet, params, key, value)
}
res = append(res, params)
}
return res, nil
}
func buildClusterDecisions(duckResources *unstructured.UnstructuredList, statusListKey string) []any {
clusterDecisions := []any{} clusterDecisions := []any{}
// Build the decision slice // Build the decision slice
@@ -192,38 +164,60 @@ func buildClusterDecisions(duckResources *unstructured.UnstructuredList, statusL
clusterDecisions = append(clusterDecisions, duckResource.Object["status"].(map[string]any)[statusListKey].([]any)...) clusterDecisions = append(clusterDecisions, duckResource.Object["status"].(map[string]any)[statusListKey].([]any)...)
} }
log.Infof("Number of decisions found: %v", len(clusterDecisions)) log.Infof("Number of decisions found: %v", len(clusterDecisions))
return clusterDecisions
}
func findCluster(clustersFromArgoCD []utils.ClusterSpecifier, cluster any, matchKey string, statusListKey string) *utils.ClusterSpecifier { if len(clusterDecisions) == 0 {
log.Infof("cluster: %v", cluster) log.Warningf("clusterDecisionResource status.%s missing", statusListKey)
matchValue := cluster.(map[string]any)[matchKey] return nil, nil
if matchValue == nil || matchValue.(string) == "" {
log.Warningf("matchKey=%v not found in \"%v\" list: %v\n", matchKey, statusListKey, cluster.(map[string]any))
return nil // no match
} }
for _, cluster := range clusterDecisions {
// generated instance of cluster params
params := map[string]any{}
strMatchValue := matchValue.(string) log.Infof("cluster: %v", cluster)
log.WithField(matchKey, strMatchValue).Debug("validate against ArgoCD") matchValue := cluster.(map[string]any)[matchKey]
if matchValue == nil || matchValue.(string) == "" {
for _, argoCluster := range clustersFromArgoCD { log.Warningf("matchKey=%v not found in \"%v\" list: %v\n", matchKey, statusListKey, cluster.(map[string]any))
if argoCluster.Name == strMatchValue { continue
log.WithField(matchKey, argoCluster.Name).Info("matched cluster in ArgoCD")
return &argoCluster
} }
}
log.WithField(matchKey, strMatchValue).Warning("unmatched cluster in ArgoCD") strMatchValue := matchValue.(string)
return nil log.WithField(matchKey, strMatchValue).Debug("validate against ArgoCD")
}
func collectParams(appSet *argoprojiov1alpha1.ApplicationSet, params map[string]any, key string, value string) { found := false
if appSet.Spec.GoTemplate {
if params["values"] == nil { for _, argoCluster := range clustersFromArgoCD {
params["values"] = map[string]string{} if argoCluster.Name == strMatchValue {
log.WithField(matchKey, argoCluster.Name).Info("matched cluster in ArgoCD")
params["name"] = argoCluster.Name
params["server"] = argoCluster.Server
found = true
break // Stop looking
}
} }
params["values"].(map[string]string)[key] = value
} else { if !found {
params["values."+key] = value log.WithField(matchKey, strMatchValue).Warning("unmatched cluster in ArgoCD")
continue
}
for key, value := range cluster.(map[string]any) {
params[key] = value.(string)
}
for key, value := range appSetGenerator.ClusterDecisionResource.Values {
if appSet.Spec.GoTemplate {
if params["values"] == nil {
params["values"] = map[string]string{}
}
params["values"].(map[string]string)[key] = value
} else {
params["values."+key] = value
}
}
res = append(res, params)
} }
return res, nil
} }

View File

@@ -159,7 +159,7 @@ func TestGenerateParamsForDuckType(t *testing.T) {
resource: duckType, resource: duckType,
values: nil, values: nil,
expected: []map[string]any{}, expected: []map[string]any{},
expectedError: errors.New("there is a problem with the definition of the ClusterDecisionResource generator"), expectedError: errors.New("There is a problem with the definition of the ClusterDecisionResource generator"),
}, },
/*** This does not work with the FAKE runtime client, fieldSelectors are broken. /*** This does not work with the FAKE runtime client, fieldSelectors are broken.
{ {
@@ -270,7 +270,7 @@ func TestGenerateParamsForDuckType(t *testing.T) {
resource: duckType, resource: duckType,
values: nil, values: nil,
expected: nil, expected: nil,
expectedError: errors.New("there is a problem with the definition of the ClusterDecisionResource generator"), expectedError: errors.New("There is a problem with the definition of the ClusterDecisionResource generator"),
}, },
} }
@@ -455,7 +455,7 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
resource: duckType, resource: duckType,
values: nil, values: nil,
expected: []map[string]any{}, expected: []map[string]any{},
expectedError: errors.New("there is a problem with the definition of the ClusterDecisionResource generator"), expectedError: errors.New("There is a problem with the definition of the ClusterDecisionResource generator"),
}, },
/*** This does not work with the FAKE runtime client, fieldSelectors are broken. /*** This does not work with the FAKE runtime client, fieldSelectors are broken.
{ {
@@ -566,7 +566,7 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
resource: duckType, resource: duckType,
values: nil, values: nil,
expected: nil, expected: nil,
expectedError: errors.New("there is a problem with the definition of the ClusterDecisionResource generator"), expectedError: errors.New("There is a problem with the definition of the ClusterDecisionResource generator"),
}, },
} }

View File

@@ -28,27 +28,22 @@ type GitGenerator struct {
namespace string namespace string
} }
// NewGitGenerator creates a new instance of Git Generator func NewGitGenerator(repos services.Repos, namespace string) Generator {
func NewGitGenerator(repos services.Repos, controllerNamespace string) Generator {
g := &GitGenerator{ g := &GitGenerator{
repos: repos, repos: repos,
namespace: controllerNamespace, namespace: namespace,
} }
return g return g
} }
// GetTemplate returns the ApplicationSetTemplate associated with the Git generator
// from the provided ApplicationSetGenerator. This template defines how each
// generated Argo CD Application should be rendered.
func (g *GitGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate { func (g *GitGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate {
return &appSetGenerator.Git.Template return &appSetGenerator.Git.Template
} }
// GetRequeueAfter returns the duration after which the Git generator should be
// requeued for reconciliation. If RequeueAfterSeconds is set in the generator spec,
// it uses that value. Otherwise, it falls back to a default requeue interval (3 minutes).
func (g *GitGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration { func (g *GitGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration {
// Return a requeue default of 3 minutes, if no default is specified.
if appSetGenerator.Git.RequeueAfterSeconds != nil { if appSetGenerator.Git.RequeueAfterSeconds != nil {
return time.Duration(*appSetGenerator.Git.RequeueAfterSeconds) * time.Second return time.Duration(*appSetGenerator.Git.RequeueAfterSeconds) * time.Second
} }
@@ -56,15 +51,13 @@ func (g *GitGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.Appli
return getDefaultRequeueAfter() return getDefaultRequeueAfter()
} }
// GenerateParams generates a list of parameter maps for the ApplicationSet by evaluating the Git generator's configuration.
// It supports both directory-based and file-based Git generators.
func (g *GitGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, appSet *argoprojiov1alpha1.ApplicationSet, client client.Client) ([]map[string]any, error) { func (g *GitGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, appSet *argoprojiov1alpha1.ApplicationSet, client client.Client) ([]map[string]any, error) {
if appSetGenerator == nil { if appSetGenerator == nil {
return nil, ErrEmptyAppSetGenerator return nil, EmptyAppSetGeneratorError
} }
if appSetGenerator.Git == nil { if appSetGenerator.Git == nil {
return nil, ErrEmptyAppSetGenerator return nil, EmptyAppSetGeneratorError
} }
noRevisionCache := appSet.RefreshRequired() noRevisionCache := appSet.RefreshRequired()
@@ -74,15 +67,14 @@ func (g *GitGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.Applic
// When the project field is templated, the contents of the git repo are required to run the git generator and get the templated value, // When the project field is templated, the contents of the git repo are required to run the git generator and get the templated value,
// but git generator cannot be called without verifying the commit signature. // but git generator cannot be called without verifying the commit signature.
// In this case, we skip the signature verification. // In this case, we skip the signature verification.
// If the project is templated, we skip the commit verification
if !strings.Contains(appSet.Spec.Template.Spec.Project, "{{") { if !strings.Contains(appSet.Spec.Template.Spec.Project, "{{") {
project := appSet.Spec.Template.Spec.Project project := appSet.Spec.Template.Spec.Project
appProject := &argoprojiov1alpha1.AppProject{} appProject := &argoprojiov1alpha1.AppProject{}
controllerNamespace := g.namespace namespace := g.namespace
if controllerNamespace == "" { if namespace == "" {
controllerNamespace = appSet.Namespace namespace = appSet.Namespace
} }
if err := client.Get(context.TODO(), types.NamespacedName{Name: project, Namespace: controllerNamespace}, appProject); err != nil { if err := client.Get(context.TODO(), types.NamespacedName{Name: project, Namespace: namespace}, appProject); err != nil {
return nil, fmt.Errorf("error getting project %s: %w", project, err) return nil, fmt.Errorf("error getting project %s: %w", project, err)
} }
// we need to verify the signature on the Git revision if GPG is enabled // we need to verify the signature on the Git revision if GPG is enabled
@@ -101,7 +93,7 @@ func (g *GitGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.Applic
case len(appSetGenerator.Git.Files) != 0: case len(appSetGenerator.Git.Files) != 0:
res, err = g.generateParamsForGitFiles(appSetGenerator, noRevisionCache, verifyCommit, appSet.Spec.GoTemplate, project, appSet.Spec.GoTemplateOptions) res, err = g.generateParamsForGitFiles(appSetGenerator, noRevisionCache, verifyCommit, appSet.Spec.GoTemplate, project, appSet.Spec.GoTemplateOptions)
default: default:
return nil, ErrEmptyAppSetGenerator return nil, EmptyAppSetGeneratorError
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("error generating params from git: %w", err) return nil, fmt.Errorf("error generating params from git: %w", err)
@@ -110,10 +102,8 @@ func (g *GitGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.Applic
return res, nil return res, nil
} }
// generateParamsForGitDirectories generates parameters for an ApplicationSet using a directory-based Git generator.
// It fetches all directories from the given Git repository and revision, optionally using a revision cache and verifying commits.
// It then filters the directories based on the generator's configuration and renders parameters for the resulting applications
func (g *GitGenerator) generateParamsForGitDirectories(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, noRevisionCache, verifyCommit, useGoTemplate bool, project string, goTemplateOptions []string) ([]map[string]any, error) { func (g *GitGenerator) generateParamsForGitDirectories(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, noRevisionCache, verifyCommit, useGoTemplate bool, project string, goTemplateOptions []string) ([]map[string]any, error) {
// Directories, not files
allPaths, err := g.repos.GetDirectories(context.TODO(), appSetGenerator.Git.RepoURL, appSetGenerator.Git.Revision, project, noRevisionCache, verifyCommit) allPaths, err := g.repos.GetDirectories(context.TODO(), appSetGenerator.Git.RepoURL, appSetGenerator.Git.Revision, project, noRevisionCache, verifyCommit)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting directories from repo: %w", err) return nil, fmt.Errorf("error getting directories from repo: %w", err)
@@ -137,103 +127,57 @@ func (g *GitGenerator) generateParamsForGitDirectories(appSetGenerator *argoproj
return res, nil return res, nil
} }
// generateParamsForGitFiles generates parameters for an ApplicationSet using a file-based Git generator.
// It retrieves and processes specified files from the Git repository, supporting both YAML and JSON formats,
// and returns a list of parameter maps extracted from the content.
func (g *GitGenerator) generateParamsForGitFiles(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, noRevisionCache, verifyCommit, useGoTemplate bool, project string, goTemplateOptions []string) ([]map[string]any, error) { func (g *GitGenerator) generateParamsForGitFiles(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, noRevisionCache, verifyCommit, useGoTemplate bool, project string, goTemplateOptions []string) ([]map[string]any, error) {
// fileContentMap maps absolute file paths to their byte content // Get all files that match the requested path string, removing duplicates
fileContentMap := make(map[string][]byte) allFiles := make(map[string][]byte)
var includePatterns []string for _, requestedPath := range appSetGenerator.Git.Files {
var excludePatterns []string files, err := g.repos.GetFiles(context.TODO(), appSetGenerator.Git.RepoURL, appSetGenerator.Git.Revision, project, requestedPath.Path, noRevisionCache, verifyCommit)
for _, req := range appSetGenerator.Git.Files {
if req.Exclude {
excludePatterns = append(excludePatterns, req.Path)
} else {
includePatterns = append(includePatterns, req.Path)
}
}
// Fetch all files from include patterns
for _, includePattern := range includePatterns {
retrievedFiles, err := g.repos.GetFiles(
context.TODO(),
appSetGenerator.Git.RepoURL,
appSetGenerator.Git.Revision,
project,
includePattern,
noRevisionCache,
verifyCommit,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for absPath, content := range retrievedFiles { for filePath, content := range files {
fileContentMap[absPath] = content allFiles[filePath] = content
} }
} }
// Now remove files matching any exclude pattern // Extract the unduplicated map into a list, and sort by path to ensure a deterministic
for _, excludePattern := range excludePatterns { // processing order in the subsequent step
matchingFiles, err := g.repos.GetFiles( allPaths := []string{}
context.TODO(), for path := range allFiles {
appSetGenerator.Git.RepoURL, allPaths = append(allPaths, path)
appSetGenerator.Git.Revision,
project,
excludePattern,
noRevisionCache,
verifyCommit,
)
if err != nil {
return nil, err
}
for absPath := range matchingFiles {
// if the file doesn't exist already and you try to delete it from the map
// the operation is a no-op. Its safe and doesn't return an error or panic.
// Hence, we can simply try to delete the file from the path without checking
// if that file already exists in the map.
delete(fileContentMap, absPath)
}
} }
sort.Strings(allPaths)
// Get a sorted list of file paths to ensure deterministic processing order // Generate params from each path, and return
var filePaths []string res := []map[string]any{}
for path := range fileContentMap { for _, path := range allPaths {
filePaths = append(filePaths, path)
}
sort.Strings(filePaths)
var allParams []map[string]any
for _, filePath := range filePaths {
// A JSON / YAML file path can contain multiple sets of parameters (ie it is an array) // A JSON / YAML file path can contain multiple sets of parameters (ie it is an array)
paramsFromFileArray, err := g.generateParamsFromGitFile(filePath, fileContentMap[filePath], appSetGenerator.Git.Values, useGoTemplate, goTemplateOptions, appSetGenerator.Git.PathParamPrefix) paramsArray, err := g.generateParamsFromGitFile(path, allFiles[path], appSetGenerator.Git.Values, useGoTemplate, goTemplateOptions, appSetGenerator.Git.PathParamPrefix)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to process file '%s': %w", filePath, err) return nil, fmt.Errorf("unable to process file '%s': %w", path, err)
} }
allParams = append(allParams, paramsFromFileArray...)
}
return allParams, nil res = append(res, paramsArray...)
}
return res, nil
} }
// generateParamsFromGitFile parses the content of a Git-tracked file and generates a slice of parameter maps.
// The file can contain a single YAML/JSON object or an array of such objects. Depending on the useGoTemplate flag,
// it either preserves structure for Go templating or flattens the objects for use as plain key-value parameters.
func (g *GitGenerator) generateParamsFromGitFile(filePath string, fileContent []byte, values map[string]string, useGoTemplate bool, goTemplateOptions []string, pathParamPrefix string) ([]map[string]any, error) { func (g *GitGenerator) generateParamsFromGitFile(filePath string, fileContent []byte, values map[string]string, useGoTemplate bool, goTemplateOptions []string, pathParamPrefix string) ([]map[string]any, error) {
objectsFound := []map[string]any{} objectsFound := []map[string]any{}
// First, we attempt to parse as a single object. // First, we attempt to parse as an array
// This will also succeed for empty files. err := yaml.Unmarshal(fileContent, &objectsFound)
singleObj := map[string]any{} if err != nil {
err := yaml.Unmarshal(fileContent, &singleObj) // If unable to parse as an array, attempt to parse as a single object
if err == nil { singleObj := make(map[string]any)
objectsFound = append(objectsFound, singleObj) err = yaml.Unmarshal(fileContent, &singleObj)
} else {
// If unable to parse as an object, try to parse as an array
err = yaml.Unmarshal(fileContent, &objectsFound)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to parse file: %w", err) return nil, fmt.Errorf("unable to parse file: %w", err)
} }
objectsFound = append(objectsFound, singleObj)
} else if len(objectsFound) == 0 {
// If file is valid but empty, add a default empty item
objectsFound = append(objectsFound, map[string]any{})
} }
res := []map[string]any{} res := []map[string]any{}
@@ -277,7 +221,7 @@ func (g *GitGenerator) generateParamsFromGitFile(filePath string, fileContent []
params[pathParamName+".basenameNormalized"] = utils.SanitizeName(path.Base(params[pathParamName].(string))) params[pathParamName+".basenameNormalized"] = utils.SanitizeName(path.Base(params[pathParamName].(string)))
params[pathParamName+".filenameNormalized"] = utils.SanitizeName(path.Base(params[pathParamName+".filename"].(string))) params[pathParamName+".filenameNormalized"] = utils.SanitizeName(path.Base(params[pathParamName+".filename"].(string)))
for k, v := range strings.Split(params[pathParamName].(string), "/") { for k, v := range strings.Split(params[pathParamName].(string), "/") {
if v != "" { if len(v) > 0 {
params[pathParamName+"["+strconv.Itoa(k)+"]"] = v params[pathParamName+"["+strconv.Itoa(k)+"]"] = v
} }
} }
@@ -294,10 +238,8 @@ func (g *GitGenerator) generateParamsFromGitFile(filePath string, fileContent []
return res, nil return res, nil
} }
// filterApps filters the list of all application paths based on inclusion and exclusion rules
// defined in GitDirectoryGeneratorItems. Each item can either include or exclude matching paths.
func (g *GitGenerator) filterApps(directories []argoprojiov1alpha1.GitDirectoryGeneratorItem, allPaths []string) []string { func (g *GitGenerator) filterApps(directories []argoprojiov1alpha1.GitDirectoryGeneratorItem, allPaths []string) []string {
var res []string res := []string{}
for _, appPath := range allPaths { for _, appPath := range allPaths {
appInclude := false appInclude := false
appExclude := false appExclude := false
@@ -324,9 +266,6 @@ func (g *GitGenerator) filterApps(directories []argoprojiov1alpha1.GitDirectoryG
return res return res
} }
// generateParamsFromApps generates a list of parameter maps based on the given app paths.
// Each app path is converted into a parameter object with path metadata (basename, segments, etc.).
// It supports both Go templates and flat key-value parameters.
func (g *GitGenerator) generateParamsFromApps(requestedApps []string, appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, useGoTemplate bool, goTemplateOptions []string) ([]map[string]any, error) { func (g *GitGenerator) generateParamsFromApps(requestedApps []string, appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, useGoTemplate bool, goTemplateOptions []string) ([]map[string]any, error) {
res := make([]map[string]any, len(requestedApps)) res := make([]map[string]any, len(requestedApps))
for i, a := range requestedApps { for i, a := range requestedApps {
@@ -352,7 +291,7 @@ func (g *GitGenerator) generateParamsFromApps(requestedApps []string, appSetGene
params[pathParamName+".basename"] = path.Base(a) params[pathParamName+".basename"] = path.Base(a)
params[pathParamName+".basenameNormalized"] = utils.SanitizeName(path.Base(a)) params[pathParamName+".basenameNormalized"] = utils.SanitizeName(path.Base(a))
for k, v := range strings.Split(params[pathParamName].(string), "/") { for k, v := range strings.Split(params[pathParamName].(string), "/") {
if v != "" { if len(v) > 0 {
params[pathParamName+"["+strconv.Itoa(k)+"]"] = v params[pathParamName+"["+strconv.Itoa(k)+"]"] = v
} }
} }
@@ -369,7 +308,6 @@ func (g *GitGenerator) generateParamsFromApps(requestedApps []string, appSetGene
return res, nil return res, nil
} }
// resolveProjectName resolves a project name whether templated or not
func resolveProjectName(project string) string { func resolveProjectName(project string) string {
if strings.Contains(project, "{{") { if strings.Contains(project, "{{") {
return "" return ""

File diff suppressed because it is too large Load Diff

View File

@@ -27,15 +27,15 @@ type Generator interface {
} }
var ( var (
ErrEmptyAppSetGenerator = errors.New("ApplicationSet is empty") EmptyAppSetGeneratorError = errors.New("ApplicationSet is empty")
NoRequeueAfter time.Duration NoRequeueAfter time.Duration
) )
const ( const (
DefaultRequeueAfter = 3 * time.Minute DefaultRequeueAfterSeconds = 3 * time.Minute
) )
func getDefaultRequeueAfter() time.Duration { func getDefaultRequeueAfter() time.Duration {
// Default is 3 minutes, min is 1 second, max is 1 year // Default is 3 minutes, min is 1 second, max is 1 year
return env.ParseDurationFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_REQUEUE_AFTER", DefaultRequeueAfter, 1*time.Second, 8760*time.Hour) return env.ParseDurationFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_REQUEUE_AFTER", DefaultRequeueAfterSeconds, 1*time.Second, 8760*time.Hour)
} }

View File

@@ -13,12 +13,12 @@ func Test_getDefaultRequeueAfter(t *testing.T) {
requeueAfterEnv string requeueAfterEnv string
want time.Duration want time.Duration
}{ }{
{name: "Default", requeueAfterEnv: "", want: DefaultRequeueAfter}, {name: "Default", requeueAfterEnv: "", want: DefaultRequeueAfterSeconds},
{name: "Min", requeueAfterEnv: "1s", want: 1 * time.Second}, {name: "Min", requeueAfterEnv: "1s", want: 1 * time.Second},
{name: "Max", requeueAfterEnv: "8760h", want: 8760 * time.Hour}, {name: "Max", requeueAfterEnv: "8760h", want: 8760 * time.Hour},
{name: "Override", requeueAfterEnv: "10m", want: 10 * time.Minute}, {name: "Override", requeueAfterEnv: "10m", want: 10 * time.Minute},
{name: "LessThanMin", requeueAfterEnv: "1ms", want: DefaultRequeueAfter}, {name: "LessThanMin", requeueAfterEnv: "1ms", want: DefaultRequeueAfterSeconds},
{name: "MoreThanMax", requeueAfterEnv: "8761h", want: DefaultRequeueAfter}, {name: "MoreThanMax", requeueAfterEnv: "8761h", want: DefaultRequeueAfterSeconds},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

View File

@@ -31,11 +31,11 @@ func (g *ListGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.Applicat
func (g *ListGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, appSet *argoprojiov1alpha1.ApplicationSet, _ client.Client) ([]map[string]any, error) { func (g *ListGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, appSet *argoprojiov1alpha1.ApplicationSet, _ client.Client) ([]map[string]any, error) {
if appSetGenerator == nil { if appSetGenerator == nil {
return nil, ErrEmptyAppSetGenerator return nil, EmptyAppSetGeneratorError
} }
if appSetGenerator.List == nil { if appSetGenerator.List == nil {
return nil, ErrEmptyAppSetGenerator return nil, EmptyAppSetGeneratorError
} }
res := make([]map[string]any, len(appSetGenerator.List.Elements)) res := make([]map[string]any, len(appSetGenerator.List.Elements))
@@ -77,7 +77,7 @@ func (g *ListGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.Appli
} }
// Append elements from ElementsYaml to the response // Append elements from ElementsYaml to the response
if appSetGenerator.List.ElementsYaml != "" { if len(appSetGenerator.List.ElementsYaml) > 0 {
var yamlElements []map[string]any var yamlElements []map[string]any
err := yaml.Unmarshal([]byte(appSetGenerator.List.ElementsYaml), &yamlElements) err := yaml.Unmarshal([]byte(appSetGenerator.List.ElementsYaml), &yamlElements)
if err != nil { if err != nil {

View File

@@ -34,7 +34,7 @@ func NewMatrixGenerator(supportedGenerators map[string]Generator) Generator {
func (m *MatrixGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, appSet *argoprojiov1alpha1.ApplicationSet, client client.Client) ([]map[string]any, error) { func (m *MatrixGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, appSet *argoprojiov1alpha1.ApplicationSet, client client.Client) ([]map[string]any, error) {
if appSetGenerator.Matrix == nil { if appSetGenerator.Matrix == nil {
return nil, ErrEmptyAppSetGenerator return nil, EmptyAppSetGeneratorError
} }
if len(appSetGenerator.Matrix.Generators) < 2 { if len(appSetGenerator.Matrix.Generators) < 2 {
@@ -71,7 +71,7 @@ func (m *MatrixGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.App
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to combine string maps with merging params for the matrix generator: %w", err) return nil, fmt.Errorf("failed to combine string maps with merging params for the matrix generator: %w", err)
} }
res = append(res, val) res = append(res, utils.ConvertToMapStringInterface(val))
} }
} }
} }

View File

@@ -4,12 +4,12 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"maps"
"time" "time"
"dario.cat/mergo" "dario.cat/mergo"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/argoproj/argo-cd/v3/applicationset/utils"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
) )
@@ -52,7 +52,7 @@ func (m *MergeGenerator) getParamSetsForAllGenerators(generators []argoprojiov1a
// GenerateParams gets the params produced by the MergeGenerator. // GenerateParams gets the params produced by the MergeGenerator.
func (m *MergeGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, appSet *argoprojiov1alpha1.ApplicationSet, client client.Client) ([]map[string]any, error) { func (m *MergeGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, appSet *argoprojiov1alpha1.ApplicationSet, client client.Client) ([]map[string]any, error) {
if appSetGenerator.Merge == nil { if appSetGenerator.Merge == nil {
return nil, ErrEmptyAppSetGenerator return nil, EmptyAppSetGeneratorError
} }
if len(appSetGenerator.Merge.Generators) < 2 { if len(appSetGenerator.Merge.Generators) < 2 {
@@ -83,8 +83,11 @@ func (m *MergeGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.Appl
} }
baseParamSetsByMergeKey[mergeKeyValue] = baseParamSet baseParamSetsByMergeKey[mergeKeyValue] = baseParamSet
} else { } else {
maps.Copy(baseParamSet, overrideParamSet) overriddenParamSet, err := utils.CombineStringMapsAllowDuplicates(baseParamSet, overrideParamSet)
baseParamSetsByMergeKey[mergeKeyValue] = baseParamSet if err != nil {
return nil, fmt.Errorf("error combining string maps: %w", err)
}
baseParamSetsByMergeKey[mergeKeyValue] = utils.ConvertToMapStringInterface(overriddenParamSet)
} }
} }
} }

View File

@@ -50,8 +50,6 @@ func listOfMapsToSet(maps []map[string]any) (map[string]bool, error) {
} }
func TestMergeGenerate(t *testing.T) { func TestMergeGenerate(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
baseGenerators []argoprojiov1alpha1.ApplicationSetNestedGenerator baseGenerators []argoprojiov1alpha1.ApplicationSetNestedGenerator
@@ -212,8 +210,6 @@ func toAPIExtensionsJSON(t *testing.T, g any) *apiextensionsv1.JSON {
} }
func TestParamSetsAreUniqueByMergeKeys(t *testing.T) { func TestParamSetsAreUniqueByMergeKeys(t *testing.T) {
t.Parallel()
testCases := []struct { testCases := []struct {
name string name string
mergeKeys []string mergeKeys []string

View File

@@ -1,17 +1,90 @@
// Code generated by mockery; DO NOT EDIT. // Code generated by mockery v2.52.4. DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package mocks package mocks
import ( import (
"time" client "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
mock "github.com/stretchr/testify/mock" mock "github.com/stretchr/testify/mock"
"sigs.k8s.io/controller-runtime/pkg/client"
time "time"
v1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
) )
// Generator is an autogenerated mock type for the Generator type
type Generator struct {
mock.Mock
}
// GenerateParams provides a mock function with given fields: appSetGenerator, applicationSetInfo, _a2
func (_m *Generator) GenerateParams(appSetGenerator *v1alpha1.ApplicationSetGenerator, applicationSetInfo *v1alpha1.ApplicationSet, _a2 client.Client) ([]map[string]interface{}, error) {
ret := _m.Called(appSetGenerator, applicationSetInfo, _a2)
if len(ret) == 0 {
panic("no return value specified for GenerateParams")
}
var r0 []map[string]interface{}
var r1 error
if rf, ok := ret.Get(0).(func(*v1alpha1.ApplicationSetGenerator, *v1alpha1.ApplicationSet, client.Client) ([]map[string]interface{}, error)); ok {
return rf(appSetGenerator, applicationSetInfo, _a2)
}
if rf, ok := ret.Get(0).(func(*v1alpha1.ApplicationSetGenerator, *v1alpha1.ApplicationSet, client.Client) []map[string]interface{}); ok {
r0 = rf(appSetGenerator, applicationSetInfo, _a2)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]map[string]interface{})
}
}
if rf, ok := ret.Get(1).(func(*v1alpha1.ApplicationSetGenerator, *v1alpha1.ApplicationSet, client.Client) error); ok {
r1 = rf(appSetGenerator, applicationSetInfo, _a2)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetRequeueAfter provides a mock function with given fields: appSetGenerator
func (_m *Generator) GetRequeueAfter(appSetGenerator *v1alpha1.ApplicationSetGenerator) time.Duration {
ret := _m.Called(appSetGenerator)
if len(ret) == 0 {
panic("no return value specified for GetRequeueAfter")
}
var r0 time.Duration
if rf, ok := ret.Get(0).(func(*v1alpha1.ApplicationSetGenerator) time.Duration); ok {
r0 = rf(appSetGenerator)
} else {
r0 = ret.Get(0).(time.Duration)
}
return r0
}
// GetTemplate provides a mock function with given fields: appSetGenerator
func (_m *Generator) GetTemplate(appSetGenerator *v1alpha1.ApplicationSetGenerator) *v1alpha1.ApplicationSetTemplate {
ret := _m.Called(appSetGenerator)
if len(ret) == 0 {
panic("no return value specified for GetTemplate")
}
var r0 *v1alpha1.ApplicationSetTemplate
if rf, ok := ret.Get(0).(func(*v1alpha1.ApplicationSetGenerator) *v1alpha1.ApplicationSetTemplate); ok {
r0 = rf(appSetGenerator)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v1alpha1.ApplicationSetTemplate)
}
}
return r0
}
// NewGenerator creates a new instance of Generator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // NewGenerator creates a new instance of Generator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value. // The first argument is typically a *testing.T value.
func NewGenerator(t interface { func NewGenerator(t interface {
@@ -25,194 +98,3 @@ func NewGenerator(t interface {
return mock return mock
} }
// Generator is an autogenerated mock type for the Generator type
type Generator struct {
mock.Mock
}
type Generator_Expecter struct {
mock *mock.Mock
}
func (_m *Generator) EXPECT() *Generator_Expecter {
return &Generator_Expecter{mock: &_m.Mock}
}
// GenerateParams provides a mock function for the type Generator
func (_mock *Generator) GenerateParams(appSetGenerator *v1alpha1.ApplicationSetGenerator, applicationSetInfo *v1alpha1.ApplicationSet, client1 client.Client) ([]map[string]any, error) {
ret := _mock.Called(appSetGenerator, applicationSetInfo, client1)
if len(ret) == 0 {
panic("no return value specified for GenerateParams")
}
var r0 []map[string]any
var r1 error
if returnFunc, ok := ret.Get(0).(func(*v1alpha1.ApplicationSetGenerator, *v1alpha1.ApplicationSet, client.Client) ([]map[string]any, error)); ok {
return returnFunc(appSetGenerator, applicationSetInfo, client1)
}
if returnFunc, ok := ret.Get(0).(func(*v1alpha1.ApplicationSetGenerator, *v1alpha1.ApplicationSet, client.Client) []map[string]any); ok {
r0 = returnFunc(appSetGenerator, applicationSetInfo, client1)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]map[string]any)
}
}
if returnFunc, ok := ret.Get(1).(func(*v1alpha1.ApplicationSetGenerator, *v1alpha1.ApplicationSet, client.Client) error); ok {
r1 = returnFunc(appSetGenerator, applicationSetInfo, client1)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Generator_GenerateParams_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GenerateParams'
type Generator_GenerateParams_Call struct {
*mock.Call
}
// GenerateParams is a helper method to define mock.On call
// - appSetGenerator *v1alpha1.ApplicationSetGenerator
// - applicationSetInfo *v1alpha1.ApplicationSet
// - client1 client.Client
func (_e *Generator_Expecter) GenerateParams(appSetGenerator interface{}, applicationSetInfo interface{}, client1 interface{}) *Generator_GenerateParams_Call {
return &Generator_GenerateParams_Call{Call: _e.mock.On("GenerateParams", appSetGenerator, applicationSetInfo, client1)}
}
func (_c *Generator_GenerateParams_Call) Run(run func(appSetGenerator *v1alpha1.ApplicationSetGenerator, applicationSetInfo *v1alpha1.ApplicationSet, client1 client.Client)) *Generator_GenerateParams_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 *v1alpha1.ApplicationSetGenerator
if args[0] != nil {
arg0 = args[0].(*v1alpha1.ApplicationSetGenerator)
}
var arg1 *v1alpha1.ApplicationSet
if args[1] != nil {
arg1 = args[1].(*v1alpha1.ApplicationSet)
}
var arg2 client.Client
if args[2] != nil {
arg2 = args[2].(client.Client)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *Generator_GenerateParams_Call) Return(stringToVs []map[string]any, err error) *Generator_GenerateParams_Call {
_c.Call.Return(stringToVs, err)
return _c
}
func (_c *Generator_GenerateParams_Call) RunAndReturn(run func(appSetGenerator *v1alpha1.ApplicationSetGenerator, applicationSetInfo *v1alpha1.ApplicationSet, client1 client.Client) ([]map[string]any, error)) *Generator_GenerateParams_Call {
_c.Call.Return(run)
return _c
}
// GetRequeueAfter provides a mock function for the type Generator
func (_mock *Generator) GetRequeueAfter(appSetGenerator *v1alpha1.ApplicationSetGenerator) time.Duration {
ret := _mock.Called(appSetGenerator)
if len(ret) == 0 {
panic("no return value specified for GetRequeueAfter")
}
var r0 time.Duration
if returnFunc, ok := ret.Get(0).(func(*v1alpha1.ApplicationSetGenerator) time.Duration); ok {
r0 = returnFunc(appSetGenerator)
} else {
r0 = ret.Get(0).(time.Duration)
}
return r0
}
// Generator_GetRequeueAfter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRequeueAfter'
type Generator_GetRequeueAfter_Call struct {
*mock.Call
}
// GetRequeueAfter is a helper method to define mock.On call
// - appSetGenerator *v1alpha1.ApplicationSetGenerator
func (_e *Generator_Expecter) GetRequeueAfter(appSetGenerator interface{}) *Generator_GetRequeueAfter_Call {
return &Generator_GetRequeueAfter_Call{Call: _e.mock.On("GetRequeueAfter", appSetGenerator)}
}
func (_c *Generator_GetRequeueAfter_Call) Run(run func(appSetGenerator *v1alpha1.ApplicationSetGenerator)) *Generator_GetRequeueAfter_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 *v1alpha1.ApplicationSetGenerator
if args[0] != nil {
arg0 = args[0].(*v1alpha1.ApplicationSetGenerator)
}
run(
arg0,
)
})
return _c
}
func (_c *Generator_GetRequeueAfter_Call) Return(duration time.Duration) *Generator_GetRequeueAfter_Call {
_c.Call.Return(duration)
return _c
}
func (_c *Generator_GetRequeueAfter_Call) RunAndReturn(run func(appSetGenerator *v1alpha1.ApplicationSetGenerator) time.Duration) *Generator_GetRequeueAfter_Call {
_c.Call.Return(run)
return _c
}
// GetTemplate provides a mock function for the type Generator
func (_mock *Generator) GetTemplate(appSetGenerator *v1alpha1.ApplicationSetGenerator) *v1alpha1.ApplicationSetTemplate {
ret := _mock.Called(appSetGenerator)
if len(ret) == 0 {
panic("no return value specified for GetTemplate")
}
var r0 *v1alpha1.ApplicationSetTemplate
if returnFunc, ok := ret.Get(0).(func(*v1alpha1.ApplicationSetGenerator) *v1alpha1.ApplicationSetTemplate); ok {
r0 = returnFunc(appSetGenerator)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*v1alpha1.ApplicationSetTemplate)
}
}
return r0
}
// Generator_GetTemplate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTemplate'
type Generator_GetTemplate_Call struct {
*mock.Call
}
// GetTemplate is a helper method to define mock.On call
// - appSetGenerator *v1alpha1.ApplicationSetGenerator
func (_e *Generator_Expecter) GetTemplate(appSetGenerator interface{}) *Generator_GetTemplate_Call {
return &Generator_GetTemplate_Call{Call: _e.mock.On("GetTemplate", appSetGenerator)}
}
func (_c *Generator_GetTemplate_Call) Run(run func(appSetGenerator *v1alpha1.ApplicationSetGenerator)) *Generator_GetTemplate_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 *v1alpha1.ApplicationSetGenerator
if args[0] != nil {
arg0 = args[0].(*v1alpha1.ApplicationSetGenerator)
}
run(
arg0,
)
})
return _c
}
func (_c *Generator_GetTemplate_Call) Return(applicationSetTemplate *v1alpha1.ApplicationSetTemplate) *Generator_GetTemplate_Call {
_c.Call.Return(applicationSetTemplate)
return _c
}
func (_c *Generator_GetTemplate_Call) RunAndReturn(run func(appSetGenerator *v1alpha1.ApplicationSetGenerator) *v1alpha1.ApplicationSetTemplate) *Generator_GetTemplate_Call {
_c.Call.Return(run)
return _c
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/jeremywohl/flatten" "github.com/jeremywohl/flatten"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
@@ -19,19 +20,23 @@ import (
) )
const ( const (
DefaultPluginRequeueAfter = 30 * time.Minute DefaultPluginRequeueAfterSeconds = 30 * time.Minute
) )
var _ Generator = (*PluginGenerator)(nil) var _ Generator = (*PluginGenerator)(nil)
type PluginGenerator struct { type PluginGenerator struct {
client client.Client client client.Client
ctx context.Context
clientset kubernetes.Interface
namespace string namespace string
} }
func NewPluginGenerator(client client.Client, namespace string) Generator { func NewPluginGenerator(ctx context.Context, client client.Client, clientset kubernetes.Interface, namespace string) Generator {
g := &PluginGenerator{ g := &PluginGenerator{
client: client, client: client,
ctx: ctx,
clientset: clientset,
namespace: namespace, namespace: namespace,
} }
return g return g
@@ -44,7 +49,7 @@ func (g *PluginGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.Ap
return time.Duration(*appSetGenerator.Plugin.RequeueAfterSeconds) * time.Second return time.Duration(*appSetGenerator.Plugin.RequeueAfterSeconds) * time.Second
} }
return DefaultPluginRequeueAfter return DefaultPluginRequeueAfterSeconds
} }
func (g *PluginGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate { func (g *PluginGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate {
@@ -53,11 +58,11 @@ func (g *PluginGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.Applic
func (g *PluginGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet, _ client.Client) ([]map[string]any, error) { func (g *PluginGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet, _ client.Client) ([]map[string]any, error) {
if appSetGenerator == nil { if appSetGenerator == nil {
return nil, ErrEmptyAppSetGenerator return nil, EmptyAppSetGeneratorError
} }
if appSetGenerator.Plugin == nil { if appSetGenerator.Plugin == nil {
return nil, ErrEmptyAppSetGenerator return nil, EmptyAppSetGeneratorError
} }
ctx := context.Background() ctx := context.Background()

View File

@@ -14,6 +14,8 @@ import (
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/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"
"sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/client/fake"
@@ -626,6 +628,8 @@ func TestPluginGenerateParams(t *testing.T) {
}, },
} }
ctx := t.Context()
for _, testCase := range testCases { for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) { t.Run(testCase.name, func(t *testing.T) {
generatorConfig := argoprojiov1alpha1.ApplicationSetGenerator{ generatorConfig := argoprojiov1alpha1.ApplicationSetGenerator{
@@ -662,9 +666,11 @@ func TestPluginGenerateParams(t *testing.T) {
testCase.configmap.Data["baseUrl"] = fakeServer.URL testCase.configmap.Data["baseUrl"] = fakeServer.URL
} }
fakeClient := kubefake.NewSimpleClientset(append([]runtime.Object{}, testCase.configmap, testCase.secret)...)
fakeClientWithCache := fake.NewClientBuilder().WithObjects([]client.Object{testCase.configmap, testCase.secret}...).Build() fakeClientWithCache := fake.NewClientBuilder().WithObjects([]client.Object{testCase.configmap, testCase.secret}...).Build()
pluginGenerator := NewPluginGenerator(fakeClientWithCache, "default") pluginGenerator := NewPluginGenerator(ctx, fakeClientWithCache, fakeClient, "default")
applicationSetInfo := argoprojiov1alpha1.ApplicationSet{ applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{

View File

@@ -4,23 +4,22 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"net/http"
"strconv" "strconv"
"time" "time"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"github.com/gosimple/slug" "github.com/gosimple/slug"
log "github.com/sirupsen/logrus"
"github.com/argoproj/argo-cd/v3/applicationset/services"
pullrequest "github.com/argoproj/argo-cd/v3/applicationset/services/pull_request" pullrequest "github.com/argoproj/argo-cd/v3/applicationset/services/pull_request"
"github.com/argoproj/argo-cd/v3/applicationset/utils" "github.com/argoproj/argo-cd/v3/applicationset/utils"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
) )
var _ Generator = (*PullRequestGenerator)(nil)
const ( const (
DefaultPullRequestRequeueAfter = 30 * time.Minute DefaultPullRequestRequeueAfterSeconds = 30 * time.Minute
) )
type PullRequestGenerator struct { type PullRequestGenerator struct {
@@ -45,11 +44,7 @@ func (g *PullRequestGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alph
return time.Duration(*appSetGenerator.PullRequest.RequeueAfterSeconds) * time.Second return time.Duration(*appSetGenerator.PullRequest.RequeueAfterSeconds) * time.Second
} }
return DefaultPullRequestRequeueAfter return DefaultPullRequestRequeueAfterSeconds
}
func (g *PullRequestGenerator) GetContinueOnRepoNotFoundError(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) bool {
return appSetGenerator.PullRequest.ContinueOnRepoNotFoundError
} }
func (g *PullRequestGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate { func (g *PullRequestGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate {
@@ -58,11 +53,11 @@ func (g *PullRequestGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.A
func (g *PullRequestGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet, _ client.Client) ([]map[string]any, error) { func (g *PullRequestGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet, _ client.Client) ([]map[string]any, error) {
if appSetGenerator == nil { if appSetGenerator == nil {
return nil, ErrEmptyAppSetGenerator return nil, EmptyAppSetGeneratorError
} }
if appSetGenerator.PullRequest == nil { if appSetGenerator.PullRequest == nil {
return nil, ErrEmptyAppSetGenerator return nil, EmptyAppSetGeneratorError
} }
ctx := context.Background() ctx := context.Background()
@@ -72,15 +67,10 @@ func (g *PullRequestGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha
} }
pulls, err := pullrequest.ListPullRequests(ctx, svc, appSetGenerator.PullRequest.Filters) pulls, err := pullrequest.ListPullRequests(ctx, svc, appSetGenerator.PullRequest.Filters)
params := make([]map[string]any, 0, len(pulls))
if err != nil { if err != nil {
if pullrequest.IsRepositoryNotFoundError(err) && g.GetContinueOnRepoNotFoundError(appSetGenerator) {
log.WithError(err).WithField("generator", g).
Warn("Skipping params generation for this repository since it was not found.")
return params, nil
}
return nil, fmt.Errorf("error listing repos: %w", err) return nil, fmt.Errorf("error listing repos: %w", err)
} }
params := make([]map[string]any, 0, len(pulls))
// In order to follow the DNS label standard as defined in RFC 1123, // In order to follow the DNS label standard as defined in RFC 1123,
// we need to limit the 'branch' to 50 to give room to append/suffix-ing it // we need to limit the 'branch' to 50 to give room to append/suffix-ing it
@@ -119,15 +109,15 @@ func (g *PullRequestGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha
"author": pull.Author, "author": pull.Author,
} }
// PR lables will only be supported for Go Template appsets, since fasttemplate will be deprecated.
if applicationSetInfo != nil && applicationSetInfo.Spec.GoTemplate {
paramMap["labels"] = pull.Labels
}
err := appendTemplatedValues(appSetGenerator.PullRequest.Values, paramMap, applicationSetInfo.Spec.GoTemplate, applicationSetInfo.Spec.GoTemplateOptions) err := appendTemplatedValues(appSetGenerator.PullRequest.Values, paramMap, applicationSetInfo.Spec.GoTemplate, applicationSetInfo.Spec.GoTemplateOptions)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to append templated values: %w", err) return nil, fmt.Errorf("failed to append templated values: %w", err)
} }
// PR lables will only be supported for Go Template appsets, since fasttemplate will be deprecated.
if applicationSetInfo != nil && applicationSetInfo.Spec.GoTemplate {
paramMap["labels"] = pull.Labels
}
params = append(params, paramMap) params = append(params, paramMap)
} }
return params, nil return params, nil
@@ -167,8 +157,7 @@ func (g *PullRequestGenerator) selectServiceProvider(ctx context.Context, genera
if err != nil { if err != nil {
return nil, fmt.Errorf("error fetching Secret token: %w", err) return nil, fmt.Errorf("error fetching Secret token: %w", err)
} }
return pullrequest.NewGiteaService(token, providerConfig.API, providerConfig.Owner, providerConfig.Repo, providerConfig.Insecure)
return pullrequest.NewGiteaService(token, providerConfig.API, providerConfig.Owner, providerConfig.Repo, providerConfig.Labels, providerConfig.Insecure)
} }
if generatorConfig.BitbucketServer != nil { if generatorConfig.BitbucketServer != nil {
providerConfig := generatorConfig.BitbucketServer providerConfig := generatorConfig.BitbucketServer
@@ -224,27 +213,12 @@ func (g *PullRequestGenerator) selectServiceProvider(ctx context.Context, genera
} }
func (g *PullRequestGenerator) github(ctx context.Context, cfg *argoprojiov1alpha1.PullRequestGeneratorGithub, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) { func (g *PullRequestGenerator) github(ctx context.Context, cfg *argoprojiov1alpha1.PullRequestGeneratorGithub, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) {
var metricsCtx *services.MetricsContext
var httpClient *http.Client
if g.enableGitHubAPIMetrics {
metricsCtx = &services.MetricsContext{
AppSetNamespace: applicationSetInfo.Namespace,
AppSetName: applicationSetInfo.Name,
}
httpClient = services.NewGitHubMetricsClient(metricsCtx)
}
// use an app if it was configured // use an app if it was configured
if cfg.AppSecretName != "" { if cfg.AppSecretName != "" {
auth, err := g.GitHubApps.GetAuthSecret(ctx, cfg.AppSecretName) auth, err := g.GitHubApps.GetAuthSecret(ctx, cfg.AppSecretName)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting GitHub App secret: %w", err) return nil, fmt.Errorf("error getting GitHub App secret: %w", err)
} }
if g.enableGitHubAPIMetrics {
return pullrequest.NewGithubAppService(*auth, cfg.API, cfg.Owner, cfg.Repo, cfg.Labels, httpClient)
}
return pullrequest.NewGithubAppService(*auth, cfg.API, cfg.Owner, cfg.Repo, cfg.Labels) return pullrequest.NewGithubAppService(*auth, cfg.API, cfg.Owner, cfg.Repo, cfg.Labels)
} }
@@ -253,9 +227,5 @@ func (g *PullRequestGenerator) github(ctx context.Context, cfg *argoprojiov1alph
if err != nil { if err != nil {
return nil, fmt.Errorf("error fetching Secret token: %w", err) return nil, fmt.Errorf("error fetching Secret token: %w", err)
} }
if g.enableGitHubAPIMetrics {
return pullrequest.NewGithubService(token, cfg.API, cfg.Owner, cfg.Repo, cfg.Labels, httpClient)
}
return pullrequest.NewGithubService(token, cfg.API, cfg.Owner, cfg.Repo, cfg.Labels) return pullrequest.NewGithubService(token, cfg.API, cfg.Owner, cfg.Repo, cfg.Labels)
} }

View File

@@ -16,12 +16,11 @@ import (
func TestPullRequestGithubGenerateParams(t *testing.T) { func TestPullRequestGithubGenerateParams(t *testing.T) {
ctx := t.Context() ctx := t.Context()
cases := []struct { cases := []struct {
selectFunc func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) selectFunc func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error)
values map[string]string values map[string]string
expected []map[string]any expected []map[string]any
expectedErr error expectedErr error
applicationSet argoprojiov1alpha1.ApplicationSet applicationSet argoprojiov1alpha1.ApplicationSet
continueOnRepoNotFoundError bool
}{ }{
{ {
selectFunc: func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) { selectFunc: func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) {
@@ -172,30 +171,6 @@ func TestPullRequestGithubGenerateParams(t *testing.T) {
expected: nil, expected: nil,
expectedErr: errors.New("error listing repos: fake error"), expectedErr: errors.New("error listing repos: fake error"),
}, },
{
selectFunc: func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) {
return pullrequest.NewFakeService(
ctx,
nil,
pullrequest.NewRepositoryNotFoundError(errors.New("repository not found")),
)
},
expected: []map[string]any{},
expectedErr: nil,
continueOnRepoNotFoundError: true,
},
{
selectFunc: func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) {
return pullrequest.NewFakeService(
ctx,
nil,
pullrequest.NewRepositoryNotFoundError(errors.New("repository not found")),
)
},
expected: nil,
expectedErr: errors.New("error listing repos: repository not found"),
continueOnRepoNotFoundError: false,
},
{ {
selectFunc: func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) { selectFunc: func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) {
return pullrequest.NewFakeService( return pullrequest.NewFakeService(
@@ -277,51 +252,6 @@ func TestPullRequestGithubGenerateParams(t *testing.T) {
}, },
}, },
}, },
{
selectFunc: func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) {
return pullrequest.NewFakeService(
ctx,
[]*pullrequest.PullRequest{
{
Number: 1,
Title: "title1",
Branch: "my_branch",
TargetBranch: "master",
HeadSHA: "abcd",
Author: "testName",
Labels: []string{"preview", "preview:team1"},
},
},
nil,
)
},
values: map[string]string{
"preview_env": "{{ regexFind \"(team1|team2)\" (.labels | join \",\") }}",
},
expected: []map[string]any{
{
"number": "1",
"title": "title1",
"branch": "my_branch",
"branch_slug": "my-branch",
"target_branch": "master",
"target_branch_slug": "master",
"head_sha": "abcd",
"head_short_sha": "abcd",
"head_short_sha_7": "abcd",
"author": "testName",
"labels": []string{"preview", "preview:team1"},
"values": map[string]string{"preview_env": "team1"},
},
},
expectedErr: nil,
applicationSet: argoprojiov1alpha1.ApplicationSet{
Spec: argoprojiov1alpha1.ApplicationSetSpec{
// Application set is using fasttemplate.
GoTemplate: true,
},
},
},
} }
for _, c := range cases { for _, c := range cases {
@@ -330,8 +260,7 @@ func TestPullRequestGithubGenerateParams(t *testing.T) {
} }
generatorConfig := argoprojiov1alpha1.ApplicationSetGenerator{ generatorConfig := argoprojiov1alpha1.ApplicationSetGenerator{
PullRequest: &argoprojiov1alpha1.PullRequestGenerator{ PullRequest: &argoprojiov1alpha1.PullRequestGenerator{
Values: c.values, Values: c.values,
ContinueOnRepoNotFoundError: c.continueOnRepoNotFoundError,
}, },
} }
@@ -346,8 +275,6 @@ func TestPullRequestGithubGenerateParams(t *testing.T) {
} }
func TestAllowedSCMProviderPullRequest(t *testing.T) { func TestAllowedSCMProviderPullRequest(t *testing.T) {
t.Parallel()
cases := []struct { cases := []struct {
name string name string
providerConfig *argoprojiov1alpha1.PullRequestGenerator providerConfig *argoprojiov1alpha1.PullRequestGenerator
@@ -398,7 +325,7 @@ func TestAllowedSCMProviderPullRequest(t *testing.T) {
"gitea.myorg.com", "gitea.myorg.com",
"bitbucket.myorg.com", "bitbucket.myorg.com",
"azuredevops.myorg.com", "azuredevops.myorg.com",
}, true, true, nil, true)) }, true, nil, true))
applicationSetInfo := argoprojiov1alpha1.ApplicationSet{ applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
@@ -421,7 +348,7 @@ func TestAllowedSCMProviderPullRequest(t *testing.T) {
} }
func TestSCMProviderDisabled_PRGenerator(t *testing.T) { func TestSCMProviderDisabled_PRGenerator(t *testing.T) {
generator := NewPullRequestGenerator(nil, NewSCMConfig("", []string{}, false, true, nil, true)) generator := NewPullRequestGenerator(nil, NewSCMConfig("", []string{}, false, nil, true))
applicationSetInfo := argoprojiov1alpha1.ApplicationSet{ applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"net/http"
"strings" "strings"
"time" "time"
@@ -12,7 +11,6 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/argoproj/argo-cd/v3/applicationset/services"
"github.com/argoproj/argo-cd/v3/applicationset/services/github_app_auth" "github.com/argoproj/argo-cd/v3/applicationset/services/github_app_auth"
"github.com/argoproj/argo-cd/v3/applicationset/services/scm_provider" "github.com/argoproj/argo-cd/v3/applicationset/services/scm_provider"
"github.com/argoproj/argo-cd/v3/applicationset/utils" "github.com/argoproj/argo-cd/v3/applicationset/utils"
@@ -23,7 +21,7 @@ import (
var _ Generator = (*SCMProviderGenerator)(nil) var _ Generator = (*SCMProviderGenerator)(nil)
const ( const (
DefaultSCMProviderRequeueAfter = 30 * time.Minute DefaultSCMProviderRequeueAfterSeconds = 30 * time.Minute
) )
type SCMProviderGenerator struct { type SCMProviderGenerator struct {
@@ -33,22 +31,20 @@ type SCMProviderGenerator struct {
SCMConfig SCMConfig
} }
type SCMConfig struct { type SCMConfig struct {
scmRootCAPath string scmRootCAPath string
allowedSCMProviders []string allowedSCMProviders []string
enableSCMProviders bool enableSCMProviders bool
enableGitHubAPIMetrics bool GitHubApps github_app_auth.Credentials
GitHubApps github_app_auth.Credentials tokenRefStrictMode bool
tokenRefStrictMode bool
} }
func NewSCMConfig(scmRootCAPath string, allowedSCMProviders []string, enableSCMProviders bool, enableGitHubAPIMetrics bool, gitHubApps github_app_auth.Credentials, tokenRefStrictMode bool) SCMConfig { func NewSCMConfig(scmRootCAPath string, allowedSCMProviders []string, enableSCMProviders bool, gitHubApps github_app_auth.Credentials, tokenRefStrictMode bool) SCMConfig {
return SCMConfig{ return SCMConfig{
scmRootCAPath: scmRootCAPath, scmRootCAPath: scmRootCAPath,
allowedSCMProviders: allowedSCMProviders, allowedSCMProviders: allowedSCMProviders,
enableSCMProviders: enableSCMProviders, enableSCMProviders: enableSCMProviders,
enableGitHubAPIMetrics: enableGitHubAPIMetrics, GitHubApps: gitHubApps,
GitHubApps: gitHubApps, tokenRefStrictMode: tokenRefStrictMode,
tokenRefStrictMode: tokenRefStrictMode,
} }
} }
@@ -73,7 +69,7 @@ func (g *SCMProviderGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alph
return time.Duration(*appSetGenerator.SCMProvider.RequeueAfterSeconds) * time.Second return time.Duration(*appSetGenerator.SCMProvider.RequeueAfterSeconds) * time.Second
} }
return DefaultSCMProviderRequeueAfter return DefaultSCMProviderRequeueAfterSeconds
} }
func (g *SCMProviderGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate { func (g *SCMProviderGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate {
@@ -122,11 +118,11 @@ func ScmProviderAllowed(applicationSetInfo *argoprojiov1alpha1.ApplicationSet, g
func (g *SCMProviderGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet, _ client.Client) ([]map[string]any, error) { func (g *SCMProviderGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet, _ client.Client) ([]map[string]any, error) {
if appSetGenerator == nil { if appSetGenerator == nil {
return nil, ErrEmptyAppSetGenerator return nil, EmptyAppSetGeneratorError
} }
if appSetGenerator.SCMProvider == nil { if appSetGenerator.SCMProvider == nil {
return nil, ErrEmptyAppSetGenerator return nil, EmptyAppSetGeneratorError
} }
if !g.enableSCMProviders { if !g.enableSCMProviders {
@@ -257,7 +253,6 @@ func (g *SCMProviderGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha
params := map[string]any{ params := map[string]any{
"organization": repo.Organization, "organization": repo.Organization,
"repository": repo.Repository, "repository": repo.Repository,
"repository_id": repo.RepositoryId,
"url": repo.URL, "url": repo.URL,
"branch": repo.Branch, "branch": repo.Branch,
"sha": repo.SHA, "sha": repo.SHA,
@@ -278,36 +273,23 @@ func (g *SCMProviderGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha
} }
func (g *SCMProviderGenerator) githubProvider(ctx context.Context, github *argoprojiov1alpha1.SCMProviderGeneratorGithub, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) (scm_provider.SCMProviderService, error) { func (g *SCMProviderGenerator) githubProvider(ctx context.Context, github *argoprojiov1alpha1.SCMProviderGeneratorGithub, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) (scm_provider.SCMProviderService, error) {
var metricsCtx *services.MetricsContext
var httpClient *http.Client
if g.enableGitHubAPIMetrics {
metricsCtx = &services.MetricsContext{
AppSetNamespace: applicationSetInfo.Namespace,
AppSetName: applicationSetInfo.Name,
}
httpClient = services.NewGitHubMetricsClient(metricsCtx)
}
if github.AppSecretName != "" { if github.AppSecretName != "" {
auth, err := g.GitHubApps.GetAuthSecret(ctx, github.AppSecretName) auth, err := g.GitHubApps.GetAuthSecret(ctx, github.AppSecretName)
if err != nil { if err != nil {
return nil, fmt.Errorf("error fetching Github app secret: %w", err) return nil, fmt.Errorf("error fetching Github app secret: %w", err)
} }
if g.enableGitHubAPIMetrics { return scm_provider.NewGithubAppProviderFor(
return scm_provider.NewGithubAppProviderFor(*auth, github.Organization, github.API, github.AllBranches, httpClient) *auth,
} github.Organization,
return scm_provider.NewGithubAppProviderFor(*auth, github.Organization, github.API, github.AllBranches) github.API,
github.AllBranches,
)
} }
token, err := utils.GetSecretRef(ctx, g.client, github.TokenRef, applicationSetInfo.Namespace, g.tokenRefStrictMode) token, err := utils.GetSecretRef(ctx, g.client, github.TokenRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
if err != nil { if err != nil {
return nil, fmt.Errorf("error fetching Github token: %w", err) return nil, fmt.Errorf("error fetching Github token: %w", err)
} }
if g.enableGitHubAPIMetrics {
return scm_provider.NewGithubProvider(github.Organization, token, github.API, github.AllBranches, httpClient)
}
return scm_provider.NewGithubProvider(github.Organization, token, github.API, github.AllBranches) return scm_provider.NewGithubProvider(github.Organization, token, github.API, github.AllBranches)
} }

View File

@@ -12,8 +12,6 @@ import (
) )
func TestSCMProviderGenerateParams(t *testing.T) { func TestSCMProviderGenerateParams(t *testing.T) {
t.Parallel()
cases := []struct { cases := []struct {
name string name string
repos []*scm_provider.Repository repos []*scm_provider.Repository
@@ -27,7 +25,6 @@ func TestSCMProviderGenerateParams(t *testing.T) {
{ {
Organization: "myorg", Organization: "myorg",
Repository: "repo1", Repository: "repo1",
RepositoryId: 190320251,
URL: "git@github.com:myorg/repo1.git", URL: "git@github.com:myorg/repo1.git",
Branch: "main", Branch: "main",
SHA: "0bc57212c3cbbec69d20b34c507284bd300def5b", SHA: "0bc57212c3cbbec69d20b34c507284bd300def5b",
@@ -36,7 +33,6 @@ func TestSCMProviderGenerateParams(t *testing.T) {
{ {
Organization: "myorg", Organization: "myorg",
Repository: "repo2", Repository: "repo2",
RepositoryId: 190320252,
URL: "git@github.com:myorg/repo2.git", URL: "git@github.com:myorg/repo2.git",
Branch: "main", Branch: "main",
SHA: "59d0", SHA: "59d0",
@@ -46,7 +42,6 @@ func TestSCMProviderGenerateParams(t *testing.T) {
{ {
"organization": "myorg", "organization": "myorg",
"repository": "repo1", "repository": "repo1",
"repository_id": 190320251,
"url": "git@github.com:myorg/repo1.git", "url": "git@github.com:myorg/repo1.git",
"branch": "main", "branch": "main",
"branchNormalized": "main", "branchNormalized": "main",
@@ -58,7 +53,6 @@ func TestSCMProviderGenerateParams(t *testing.T) {
{ {
"organization": "myorg", "organization": "myorg",
"repository": "repo2", "repository": "repo2",
"repository_id": 190320252,
"url": "git@github.com:myorg/repo2.git", "url": "git@github.com:myorg/repo2.git",
"branch": "main", "branch": "main",
"branchNormalized": "main", "branchNormalized": "main",
@@ -75,7 +69,6 @@ func TestSCMProviderGenerateParams(t *testing.T) {
{ {
Organization: "myorg", Organization: "myorg",
Repository: "repo3", Repository: "repo3",
RepositoryId: 190320253,
URL: "git@github.com:myorg/repo3.git", URL: "git@github.com:myorg/repo3.git",
Branch: "main", Branch: "main",
SHA: "0bc57212c3cbbec69d20b34c507284bd300def5b", SHA: "0bc57212c3cbbec69d20b34c507284bd300def5b",
@@ -90,7 +83,6 @@ func TestSCMProviderGenerateParams(t *testing.T) {
{ {
"organization": "myorg", "organization": "myorg",
"repository": "repo3", "repository": "repo3",
"repository_id": 190320253,
"url": "git@github.com:myorg/repo3.git", "url": "git@github.com:myorg/repo3.git",
"branch": "main", "branch": "main",
"branchNormalized": "main", "branchNormalized": "main",
@@ -103,52 +95,6 @@ func TestSCMProviderGenerateParams(t *testing.T) {
}, },
}, },
}, },
{
name: "Repos with and without id",
repos: []*scm_provider.Repository{
{
Organization: "myorg",
Repository: "repo4",
RepositoryId: "idaz09",
URL: "git@github.com:myorg/repo4.git",
Branch: "main",
SHA: "0bc57212c3cbbec69d20b34c507284bd300def5b",
},
{
Organization: "myorg",
Repository: "repo5",
URL: "git@github.com:myorg/repo5.git",
Branch: "main",
SHA: "0bc57212c3cbbec69d20b34c507284bd300def5b",
},
},
expected: []map[string]any{
{
"organization": "myorg",
"repository": "repo4",
"repository_id": "idaz09",
"url": "git@github.com:myorg/repo4.git",
"branch": "main",
"branchNormalized": "main",
"sha": "0bc57212c3cbbec69d20b34c507284bd300def5b",
"short_sha": "0bc57212",
"short_sha_7": "0bc5721",
"labels": "",
},
{
"organization": "myorg",
"repository": "repo5",
"repository_id": nil,
"url": "git@github.com:myorg/repo5.git",
"branch": "main",
"branchNormalized": "main",
"sha": "0bc57212c3cbbec69d20b34c507284bd300def5b",
"short_sha": "0bc57212",
"short_sha_7": "0bc5721",
"labels": "",
},
},
},
} }
for _, testCase := range cases { for _, testCase := range cases {
@@ -187,8 +133,6 @@ func TestSCMProviderGenerateParams(t *testing.T) {
} }
func TestAllowedSCMProvider(t *testing.T) { func TestAllowedSCMProvider(t *testing.T) {
t.Parallel()
cases := []struct { cases := []struct {
name string name string
providerConfig *argoprojiov1alpha1.SCMProviderGenerator providerConfig *argoprojiov1alpha1.SCMProviderGenerator

View File

@@ -10,15 +10,15 @@ import (
"github.com/argoproj/argo-cd/v3/applicationset/services" "github.com/argoproj/argo-cd/v3/applicationset/services"
) )
func GetGenerators(ctx context.Context, c client.Client, k8sClient kubernetes.Interface, controllerNamespace string, argoCDService services.Repos, dynamicClient dynamic.Interface, scmConfig SCMConfig) map[string]Generator { func GetGenerators(ctx context.Context, c client.Client, k8sClient kubernetes.Interface, namespace string, argoCDService services.Repos, dynamicClient dynamic.Interface, scmConfig SCMConfig) map[string]Generator {
terminalGenerators := map[string]Generator{ terminalGenerators := map[string]Generator{
"List": NewListGenerator(), "List": NewListGenerator(),
"Clusters": NewClusterGenerator(ctx, c, k8sClient, controllerNamespace), "Clusters": NewClusterGenerator(ctx, c, k8sClient, namespace),
"Git": NewGitGenerator(argoCDService, controllerNamespace), "Git": NewGitGenerator(argoCDService, namespace),
"SCMProvider": NewSCMProviderGenerator(c, scmConfig), "SCMProvider": NewSCMProviderGenerator(c, scmConfig),
"ClusterDecisionResource": NewDuckTypeGenerator(ctx, dynamicClient, k8sClient, controllerNamespace), "ClusterDecisionResource": NewDuckTypeGenerator(ctx, dynamicClient, k8sClient, namespace),
"PullRequest": NewPullRequestGenerator(c, scmConfig), "PullRequest": NewPullRequestGenerator(c, scmConfig),
"Plugin": NewPluginGenerator(c, controllerNamespace), "Plugin": NewPluginGenerator(ctx, c, k8sClient, namespace),
} }
nestedGenerators := map[string]Generator{ nestedGenerators := map[string]Generator{

View File

@@ -57,7 +57,7 @@ func TestValueInterpolation(t *testing.T) {
t.Run(testCase.name, func(t *testing.T) { t.Run(testCase.name, func(t *testing.T) {
err := appendTemplatedValues(testCase.values, testCase.params, false, nil) err := appendTemplatedValues(testCase.values, testCase.params, false, nil)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, testCase.expected, testCase.params) assert.EqualValues(t, testCase.expected, testCase.params)
}) })
} }
} }
@@ -118,7 +118,7 @@ func TestValueInterpolationWithGoTemplating(t *testing.T) {
t.Run(testCase.name, func(t *testing.T) { t.Run(testCase.name, func(t *testing.T) {
err := appendTemplatedValues(testCase.values, testCase.params, true, nil) err := appendTemplatedValues(testCase.values, testCase.params, true, nil)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, testCase.expected, testCase.params) assert.EqualValues(t, testCase.expected, testCase.params)
}) })
} }
} }

View File

@@ -58,7 +58,8 @@ func NewApplicationsetMetrics(appsetLister applisters.ApplicationSetLister, apps
metrics.Registry.MustRegister(reconcileHistogram) metrics.Registry.MustRegister(reconcileHistogram)
metrics.Registry.MustRegister(appsetCollector) metrics.Registry.MustRegister(appsetCollector)
kubectl.RegisterWithClientGo() kubectlMetricsServer := kubectl.NewKubectlMetrics()
kubectlMetricsServer.RegisterWithClientGo()
kubectl.RegisterWithPrometheus(metrics.Registry) kubectl.RegisterWithPrometheus(metrics.Registry)
return ApplicationsetMetrics{ return ApplicationsetMetrics{

View File

@@ -174,7 +174,7 @@ func TestApplicationsetCollector(t *testing.T) {
appsetCollector := newAppsetCollector(utils.NewAppsetLister(client), collectedLabels, filter) appsetCollector := newAppsetCollector(utils.NewAppsetLister(client), collectedLabels, filter)
metrics.Registry.MustRegister(appsetCollector) metrics.Registry.MustRegister(appsetCollector)
req, err := http.NewRequest(http.MethodGet, "/metrics", http.NoBody) req, err := http.NewRequest(http.MethodGet, "/metrics", nil)
require.NoError(t, err) require.NoError(t, err)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
handler := promhttp.HandlerFor(metrics.Registry, promhttp.HandlerOpts{}) handler := promhttp.HandlerFor(metrics.Registry, promhttp.HandlerOpts{})
@@ -216,7 +216,7 @@ func TestObserveReconcile(t *testing.T) {
appsetMetrics := NewApplicationsetMetrics(utils.NewAppsetLister(client), collectedLabels, filter) appsetMetrics := NewApplicationsetMetrics(utils.NewAppsetLister(client), collectedLabels, filter)
req, err := http.NewRequest(http.MethodGet, "/metrics", http.NoBody) req, err := http.NewRequest(http.MethodGet, "/metrics", nil)
require.NoError(t, err) require.NoError(t, err)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
handler := promhttp.HandlerFor(metrics.Registry, promhttp.HandlerOpts{}) handler := promhttp.HandlerFor(metrics.Registry, promhttp.HandlerOpts{})

View File

@@ -1,241 +0,0 @@
package services
import (
"net/http"
"strconv"
"time"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
"sigs.k8s.io/controller-runtime/pkg/metrics"
)
// Doc for the GitHub API rate limit headers:
// https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#checking-the-status-of-your-rate-limit
// Metric names as constants
const (
githubAPIRequestTotalMetricName = "argocd_github_api_requests_total"
githubAPIRequestDurationMetricName = "argocd_github_api_request_duration_seconds"
githubAPIRateLimitRemainingMetricName = "argocd_github_api_rate_limit_remaining"
githubAPIRateLimitLimitMetricName = "argocd_github_api_rate_limit_limit"
githubAPIRateLimitResetMetricName = "argocd_github_api_rate_limit_reset_seconds"
githubAPIRateLimitUsedMetricName = "argocd_github_api_rate_limit_used"
)
// GitHubMetrics groups all metric vectors for easier injection and registration
type GitHubMetrics struct {
RequestTotal *prometheus.CounterVec
RequestDuration *prometheus.HistogramVec
RateLimitRemaining *prometheus.GaugeVec
RateLimitLimit *prometheus.GaugeVec
RateLimitReset *prometheus.GaugeVec
RateLimitUsed *prometheus.GaugeVec
}
// Factory for a new set of GitHub metrics (for tests or custom registries)
func NewGitHubMetrics() *GitHubMetrics {
return &GitHubMetrics{
RequestTotal: NewGitHubAPIRequestTotal(),
RequestDuration: NewGitHubAPIRequestDuration(),
RateLimitRemaining: NewGitHubAPIRateLimitRemaining(),
RateLimitLimit: NewGitHubAPIRateLimitLimit(),
RateLimitReset: NewGitHubAPIRateLimitReset(),
RateLimitUsed: NewGitHubAPIRateLimitUsed(),
}
}
// Factory functions for each metric vector
func NewGitHubAPIRequestTotal() *prometheus.CounterVec {
return prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: githubAPIRequestTotalMetricName,
Help: "Total number of GitHub API requests",
},
[]string{"method", "endpoint", "status", "appset_namespace", "appset_name"},
)
}
func NewGitHubAPIRequestDuration() *prometheus.HistogramVec {
return prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: githubAPIRequestDurationMetricName,
Help: "GitHub API request duration in seconds",
Buckets: prometheus.DefBuckets,
},
[]string{"method", "endpoint", "appset_namespace", "appset_name"},
)
}
func NewGitHubAPIRateLimitRemaining() *prometheus.GaugeVec {
return prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: githubAPIRateLimitRemainingMetricName,
Help: "The number of requests remaining in the current rate limit window",
},
[]string{"endpoint", "appset_namespace", "appset_name", "resource"},
)
}
func NewGitHubAPIRateLimitLimit() *prometheus.GaugeVec {
return prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: githubAPIRateLimitLimitMetricName,
Help: "The maximum number of requests that you can make per hour",
},
[]string{"endpoint", "appset_namespace", "appset_name", "resource"},
)
}
func NewGitHubAPIRateLimitReset() *prometheus.GaugeVec {
return prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: githubAPIRateLimitResetMetricName,
Help: "The time left till the current rate limit window resets, in seconds",
},
[]string{"endpoint", "appset_namespace", "appset_name", "resource"},
)
}
func NewGitHubAPIRateLimitUsed() *prometheus.GaugeVec {
return prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: githubAPIRateLimitUsedMetricName,
Help: "The number of requests used in the current rate limit window",
},
[]string{"endpoint", "appset_namespace", "appset_name", "resource"},
)
}
// Global metrics (registered with the default registry)
var globalGitHubMetrics = NewGitHubMetrics()
func init() {
log.Debug("Registering GitHub API AppSet metrics")
metrics.Registry.MustRegister(globalGitHubMetrics.RequestTotal)
metrics.Registry.MustRegister(globalGitHubMetrics.RequestDuration)
metrics.Registry.MustRegister(globalGitHubMetrics.RateLimitRemaining)
metrics.Registry.MustRegister(globalGitHubMetrics.RateLimitLimit)
metrics.Registry.MustRegister(globalGitHubMetrics.RateLimitReset)
metrics.Registry.MustRegister(globalGitHubMetrics.RateLimitUsed)
}
type MetricsContext struct {
AppSetNamespace string
AppSetName string
}
// GitHubMetricsTransport is a custom http.RoundTripper that collects GitHub API metrics
type GitHubMetricsTransport struct {
transport http.RoundTripper
metricsContext *MetricsContext
metrics *GitHubMetrics
}
// RoundTrip implements http.RoundTripper interface and collects metrics along with debug logging
func (t *GitHubMetricsTransport) RoundTrip(req *http.Request) (*http.Response, error) {
endpoint := req.URL.Path
method := req.Method
appsetNamespace := "unknown"
appsetName := "unknown"
if t.metricsContext != nil {
appsetNamespace = t.metricsContext.AppSetNamespace
appsetName = t.metricsContext.AppSetName
}
log.WithFields(log.Fields{
"method": method,
"endpoint": endpoint,
"applicationset": map[string]string{"name": appsetName, "namespace": appsetNamespace},
}).Debugf("Invoking GitHub API")
startTime := time.Now()
resp, err := t.transport.RoundTrip(req)
duration := time.Since(startTime)
// Record metrics
t.metrics.RequestDuration.WithLabelValues(method, endpoint, appsetNamespace, appsetName).Observe(duration.Seconds())
status := "0"
if resp != nil {
status = strconv.Itoa(resp.StatusCode)
}
t.metrics.RequestTotal.WithLabelValues(method, endpoint, status, appsetNamespace, appsetName).Inc()
if resp != nil {
resetHumanReadableTime := ""
remainingInt := 0
limitInt := 0
usedInt := 0
resource := resp.Header.Get("X-RateLimit-Resource")
// Record rate limit metrics if available
if resetTime := resp.Header.Get("X-RateLimit-Reset"); resetTime != "" {
if resetUnix, err := strconv.ParseInt(resetTime, 10, 64); err == nil {
// Calculate seconds until reset (reset timestamp - current time)
secondsUntilReset := resetUnix - time.Now().Unix()
t.metrics.RateLimitReset.WithLabelValues(endpoint, appsetNamespace, appsetName, resource).Set(float64(secondsUntilReset))
resetHumanReadableTime = time.Unix(resetUnix, 0).Local().Format("2006-01-02 15:04:05 MST")
}
}
if remaining := resp.Header.Get("X-RateLimit-Remaining"); remaining != "" {
if remainingInt, err = strconv.Atoi(remaining); err == nil {
t.metrics.RateLimitRemaining.WithLabelValues(endpoint, appsetNamespace, appsetName, resource).Set(float64(remainingInt))
}
}
if limit := resp.Header.Get("X-RateLimit-Limit"); limit != "" {
if limitInt, err = strconv.Atoi(limit); err == nil {
t.metrics.RateLimitLimit.WithLabelValues(endpoint, appsetNamespace, appsetName, resource).Set(float64(limitInt))
}
}
if used := resp.Header.Get("X-RateLimit-Used"); used != "" {
if usedInt, err = strconv.Atoi(used); err == nil {
t.metrics.RateLimitUsed.WithLabelValues(endpoint, appsetNamespace, appsetName, resource).Set(float64(usedInt))
}
}
log.WithFields(log.Fields{
"endpoint": endpoint,
"reset": resetHumanReadableTime,
"remaining": remainingInt,
"limit": limitInt,
"used": usedInt,
"resource": resource,
"applicationset": map[string]string{"name": appsetName, "namespace": appsetNamespace},
}).Debugf("GitHub API rate limit info")
}
return resp, err
}
// Full constructor (for tests and advanced use)
func NewGitHubMetricsTransport(
transport http.RoundTripper,
metricsContext *MetricsContext,
metrics *GitHubMetrics,
) *GitHubMetricsTransport {
return &GitHubMetricsTransport{
transport: transport,
metricsContext: metricsContext,
metrics: metrics,
}
}
// Default constructor
func NewDefaultGitHubMetricsTransport(transport http.RoundTripper, metricsContext *MetricsContext) *GitHubMetricsTransport {
return NewGitHubMetricsTransport(
transport,
metricsContext,
globalGitHubMetrics,
)
}
// NewGitHubMetricsClient wraps an http.Client with metrics middleware
func NewGitHubMetricsClient(metricsContext *MetricsContext) *http.Client {
log.Debug("Creating new GitHub metrics client")
return &http.Client{
Transport: NewDefaultGitHubMetricsTransport(http.DefaultTransport, metricsContext),
}
}

View File

@@ -1,221 +0,0 @@
package services
import (
"io"
"net/http"
"net/http/httptest"
"sort"
"strconv"
"strings"
"testing"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/stretchr/testify/assert"
)
type Metric struct {
name string
labels []string
value string
}
var (
endpointLabel = "endpoint=\"/api/test\""
URL = "/api/test"
appsetNamespaceLabel = "appset_namespace=\"test-ns\""
appsetNamespace = "test-ns"
appsetName = "test-appset"
appsetNameLabel = "appset_name=\"test-appset\""
resourceLabel = "resource=\"core\""
rateLimitMetrics = []Metric{
{
name: githubAPIRateLimitRemainingMetricName,
labels: []string{endpointLabel, appsetNamespaceLabel, appsetNameLabel, resourceLabel},
value: "42",
},
{
name: githubAPIRateLimitLimitMetricName,
labels: []string{endpointLabel, appsetNamespaceLabel, appsetNameLabel, resourceLabel},
value: "100",
},
{
name: githubAPIRateLimitUsedMetricName,
labels: []string{endpointLabel, appsetNamespaceLabel, appsetNameLabel, resourceLabel},
value: "58",
},
{
name: githubAPIRateLimitResetMetricName,
labels: []string{endpointLabel, appsetNamespaceLabel, appsetNameLabel, resourceLabel},
value: "1",
},
}
successRequestMetrics = Metric{
name: githubAPIRequestTotalMetricName,
labels: []string{"method=\"GET\"", endpointLabel, "status=\"201\"", appsetNamespaceLabel, appsetNameLabel},
value: "1",
}
failureRequestMetrics = Metric{
name: githubAPIRequestTotalMetricName,
labels: []string{"method=\"GET\"", endpointLabel, "status=\"0\"", appsetNamespaceLabel, appsetNameLabel},
value: "1",
}
)
func TestGitHubMetrics_CollectorApproach_Success(t *testing.T) {
metrics := NewGitHubMetrics()
reg := prometheus.NewRegistry()
reg.MustRegister(
metrics.RequestTotal,
metrics.RequestDuration,
metrics.RateLimitRemaining,
metrics.RateLimitLimit,
metrics.RateLimitReset,
metrics.RateLimitUsed,
)
// Setup a fake HTTP server
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("X-RateLimit-Reset", strconv.FormatInt(time.Now().Unix()+1, 10))
w.Header().Set("X-RateLimit-Remaining", "42")
w.Header().Set("X-RateLimit-Limit", "100")
w.Header().Set("X-RateLimit-Used", "58")
w.Header().Set("X-RateLimit-Resource", "core")
w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte("ok"))
}))
defer ts.Close()
metricsCtx := &MetricsContext{AppSetNamespace: appsetNamespace, AppSetName: appsetName}
client := &http.Client{
Transport: NewGitHubMetricsTransport(
http.DefaultTransport,
metricsCtx,
metrics,
),
}
req, _ := http.NewRequest(http.MethodGet, ts.URL+URL, http.NoBody)
resp, err := client.Do(req)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
resp.Body.Close()
// Expose and scrape metrics
handler := promhttp.HandlerFor(reg, promhttp.HandlerOpts{})
server := httptest.NewServer(handler)
defer server.Close()
resp, err = http.Get(server.URL)
if err != nil {
t.Fatalf("failed to scrape metrics: %v", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
metricsOutput := string(body)
sort.Strings(successRequestMetrics.labels)
assert.Contains(t, metricsOutput, successRequestMetrics.name+"{"+strings.Join(successRequestMetrics.labels, ",")+"} "+successRequestMetrics.value)
for _, metric := range rateLimitMetrics {
sort.Strings(metric.labels)
assert.Contains(t, metricsOutput, metric.name+"{"+strings.Join(metric.labels, ",")+"} "+metric.value)
}
}
type RoundTripperFunc func(*http.Request) (*http.Response, error)
func (f RoundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
func TestGitHubMetrics_CollectorApproach_NoRateLimitMetricsOnNilResponse(t *testing.T) {
metrics := NewGitHubMetrics()
reg := prometheus.NewRegistry()
reg.MustRegister(
metrics.RequestTotal,
metrics.RequestDuration,
metrics.RateLimitRemaining,
metrics.RateLimitLimit,
metrics.RateLimitReset,
metrics.RateLimitUsed,
)
client := &http.Client{
Transport: &GitHubMetricsTransport{
transport: RoundTripperFunc(func(*http.Request) (*http.Response, error) {
return nil, http.ErrServerClosed
}),
metricsContext: &MetricsContext{AppSetNamespace: appsetNamespace, AppSetName: appsetName},
metrics: metrics,
},
}
req, _ := http.NewRequest(http.MethodGet, URL, http.NoBody)
_, _ = client.Do(req)
handler := promhttp.HandlerFor(reg, promhttp.HandlerOpts{})
server := httptest.NewServer(handler)
defer server.Close()
resp, err := http.Get(server.URL)
if err != nil {
t.Fatalf("failed to scrape metrics: %v", err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
metricsOutput := string(body)
// Verify request metric exists with status "0"
sort.Strings(failureRequestMetrics.labels)
assert.Contains(t, metricsOutput, failureRequestMetrics.name+"{"+strings.Join(failureRequestMetrics.labels, ",")+"} "+failureRequestMetrics.value)
// Verify rate limit metrics don't exist
for _, metric := range rateLimitMetrics {
sort.Strings(metric.labels)
assert.NotContains(t, metricsOutput, metric.name+"{"+strings.Join(metric.labels, ",")+"} "+metric.value)
}
}
func TestNewGitHubMetricsClient(t *testing.T) {
// Test cases
testCases := []struct {
name string
metricsCtx *MetricsContext
}{
{
name: "with metrics context",
metricsCtx: &MetricsContext{
AppSetNamespace: appsetNamespace,
AppSetName: appsetName,
},
},
{
name: "with nil metrics context",
metricsCtx: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Create client
client := NewGitHubMetricsClient(tc.metricsCtx)
// Assert client is not nil
assert.NotNil(t, client)
// Assert transport is properly configured
transport, ok := client.Transport.(*GitHubMetricsTransport)
assert.True(t, ok, "Transport should be GitHubMetricsTransport")
// Verify transport configuration
assert.Equal(t, tc.metricsCtx, transport.metricsContext)
assert.NotNil(t, transport.metrics, "Metrics should not be nil")
assert.Equal(t, http.DefaultTransport, transport.transport, "Base transport should be http.DefaultTransport")
// Verify metrics are global metrics
assert.Equal(t, globalGitHubMetrics, transport.metrics, "Should use global metrics")
})
}
}

View File

@@ -5,27 +5,14 @@ import (
"net/http" "net/http"
"github.com/bradleyfalzon/ghinstallation/v2" "github.com/bradleyfalzon/ghinstallation/v2"
"github.com/google/go-github/v69/github" "github.com/google/go-github/v66/github"
"github.com/argoproj/argo-cd/v3/applicationset/services/github_app_auth" "github.com/argoproj/argo-cd/v3/applicationset/services/github_app_auth"
appsetutils "github.com/argoproj/argo-cd/v3/applicationset/utils"
) )
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
}
// or the default httpClient and transport
return httpClient, http.DefaultTransport
}
// Client builds a github client for the given app authentication. // Client builds a github client for the given app authentication.
func Client(g github_app_auth.Authentication, url string, optionalHTTPClient ...*http.Client) (*github.Client, error) { func Client(g github_app_auth.Authentication, url string) (*github.Client, error) {
httpClient, transport := getOptionalHTTPClientAndTransport(optionalHTTPClient...) rt, err := ghinstallation.New(http.DefaultTransport, g.Id, g.InstallationId, []byte(g.PrivateKey))
rt, err := ghinstallation.New(transport, g.Id, g.InstallationId, []byte(g.PrivateKey))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create github app install: %w", err) return nil, fmt.Errorf("failed to create github app install: %w", err)
} }
@@ -33,12 +20,13 @@ func Client(g github_app_auth.Authentication, url string, optionalHTTPClient ...
url = g.EnterpriseBaseURL url = g.EnterpriseBaseURL
} }
var client *github.Client var client *github.Client
httpClient.Transport = rt
if url == "" { if url == "" {
client = github.NewClient(httpClient) httpClient := http.Client{Transport: rt}
client = github.NewClient(&httpClient)
} else { } else {
rt.BaseURL = url rt.BaseURL = url
client, err = github.NewClient(httpClient).WithEnterpriseURLs(url, url) httpClient := http.Client{Transport: rt}
client, err = github.NewClient(&httpClient).WithEnterpriseURLs(url, url)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create github enterprise client: %w", err) return nil, fmt.Errorf("failed to create github enterprise client: %w", err)
} }

View File

@@ -91,7 +91,7 @@ func (c *Client) NewRequestWithContext(ctx context.Context, method, path string,
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
} }
if c.token != "" { if len(c.token) != 0 {
req.Header.Set("Authorization", "Bearer "+c.token) req.Header.Set("Authorization", "Bearer "+c.token)
} }

View File

@@ -1,15 +1,78 @@
// Code generated by mockery; DO NOT EDIT. // Code generated by mockery v2.52.4. DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package mocks package mocks
import ( import (
"context" context "context"
mock "github.com/stretchr/testify/mock" mock "github.com/stretchr/testify/mock"
) )
// Repos is an autogenerated mock type for the Repos type
type Repos struct {
mock.Mock
}
// GetDirectories provides a mock function with given fields: ctx, repoURL, revision, project, noRevisionCache, verifyCommit
func (_m *Repos) GetDirectories(ctx context.Context, repoURL string, revision string, project string, noRevisionCache bool, verifyCommit bool) ([]string, error) {
ret := _m.Called(ctx, repoURL, revision, project, noRevisionCache, verifyCommit)
if len(ret) == 0 {
panic("no return value specified for GetDirectories")
}
var r0 []string
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, bool, bool) ([]string, error)); ok {
return rf(ctx, repoURL, revision, project, noRevisionCache, verifyCommit)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, bool, bool) []string); ok {
r0 = rf(ctx, repoURL, revision, project, noRevisionCache, verifyCommit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, string, string, bool, bool) error); ok {
r1 = rf(ctx, repoURL, revision, project, noRevisionCache, verifyCommit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetFiles provides a mock function with given fields: ctx, repoURL, revision, project, pattern, noRevisionCache, verifyCommit
func (_m *Repos) GetFiles(ctx context.Context, repoURL string, revision string, project string, pattern string, noRevisionCache bool, verifyCommit bool) (map[string][]byte, error) {
ret := _m.Called(ctx, repoURL, revision, project, pattern, noRevisionCache, verifyCommit)
if len(ret) == 0 {
panic("no return value specified for GetFiles")
}
var r0 map[string][]byte
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, bool, bool) (map[string][]byte, error)); ok {
return rf(ctx, repoURL, revision, project, pattern, noRevisionCache, verifyCommit)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, string, bool, bool) map[string][]byte); ok {
r0 = rf(ctx, repoURL, revision, project, pattern, noRevisionCache, verifyCommit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string][]byte)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, string, string, string, bool, bool) error); ok {
r1 = rf(ctx, repoURL, revision, project, pattern, noRevisionCache, verifyCommit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewRepos creates a new instance of Repos. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // NewRepos creates a new instance of Repos. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value. // The first argument is typically a *testing.T value.
func NewRepos(t interface { func NewRepos(t interface {
@@ -23,206 +86,3 @@ func NewRepos(t interface {
return mock return mock
} }
// Repos is an autogenerated mock type for the Repos type
type Repos struct {
mock.Mock
}
type Repos_Expecter struct {
mock *mock.Mock
}
func (_m *Repos) EXPECT() *Repos_Expecter {
return &Repos_Expecter{mock: &_m.Mock}
}
// GetDirectories provides a mock function for the type Repos
func (_mock *Repos) GetDirectories(ctx context.Context, repoURL string, revision string, project string, noRevisionCache bool, verifyCommit bool) ([]string, error) {
ret := _mock.Called(ctx, repoURL, revision, project, noRevisionCache, verifyCommit)
if len(ret) == 0 {
panic("no return value specified for GetDirectories")
}
var r0 []string
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, bool, bool) ([]string, error)); ok {
return returnFunc(ctx, repoURL, revision, project, noRevisionCache, verifyCommit)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, bool, bool) []string); ok {
r0 = returnFunc(ctx, repoURL, revision, project, noRevisionCache, verifyCommit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, string, bool, bool) error); ok {
r1 = returnFunc(ctx, repoURL, revision, project, noRevisionCache, verifyCommit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Repos_GetDirectories_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetDirectories'
type Repos_GetDirectories_Call struct {
*mock.Call
}
// GetDirectories is a helper method to define mock.On call
// - ctx context.Context
// - repoURL string
// - revision string
// - project string
// - noRevisionCache bool
// - verifyCommit bool
func (_e *Repos_Expecter) GetDirectories(ctx interface{}, repoURL interface{}, revision interface{}, project interface{}, noRevisionCache interface{}, verifyCommit interface{}) *Repos_GetDirectories_Call {
return &Repos_GetDirectories_Call{Call: _e.mock.On("GetDirectories", ctx, repoURL, revision, project, noRevisionCache, verifyCommit)}
}
func (_c *Repos_GetDirectories_Call) Run(run func(ctx context.Context, repoURL string, revision string, project string, noRevisionCache bool, verifyCommit bool)) *Repos_GetDirectories_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
var arg3 string
if args[3] != nil {
arg3 = args[3].(string)
}
var arg4 bool
if args[4] != nil {
arg4 = args[4].(bool)
}
var arg5 bool
if args[5] != nil {
arg5 = args[5].(bool)
}
run(
arg0,
arg1,
arg2,
arg3,
arg4,
arg5,
)
})
return _c
}
func (_c *Repos_GetDirectories_Call) Return(strings []string, err error) *Repos_GetDirectories_Call {
_c.Call.Return(strings, err)
return _c
}
func (_c *Repos_GetDirectories_Call) RunAndReturn(run func(ctx context.Context, repoURL string, revision string, project string, noRevisionCache bool, verifyCommit bool) ([]string, error)) *Repos_GetDirectories_Call {
_c.Call.Return(run)
return _c
}
// GetFiles provides a mock function for the type Repos
func (_mock *Repos) GetFiles(ctx context.Context, repoURL string, revision string, project string, pattern string, noRevisionCache bool, verifyCommit bool) (map[string][]byte, error) {
ret := _mock.Called(ctx, repoURL, revision, project, pattern, noRevisionCache, verifyCommit)
if len(ret) == 0 {
panic("no return value specified for GetFiles")
}
var r0 map[string][]byte
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, string, bool, bool) (map[string][]byte, error)); ok {
return returnFunc(ctx, repoURL, revision, project, pattern, noRevisionCache, verifyCommit)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, string, string, bool, bool) map[string][]byte); ok {
r0 = returnFunc(ctx, repoURL, revision, project, pattern, noRevisionCache, verifyCommit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string][]byte)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string, string, string, string, bool, bool) error); ok {
r1 = returnFunc(ctx, repoURL, revision, project, pattern, noRevisionCache, verifyCommit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Repos_GetFiles_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetFiles'
type Repos_GetFiles_Call struct {
*mock.Call
}
// GetFiles is a helper method to define mock.On call
// - ctx context.Context
// - repoURL string
// - revision string
// - project string
// - pattern string
// - noRevisionCache bool
// - verifyCommit bool
func (_e *Repos_Expecter) GetFiles(ctx interface{}, repoURL interface{}, revision interface{}, project interface{}, pattern interface{}, noRevisionCache interface{}, verifyCommit interface{}) *Repos_GetFiles_Call {
return &Repos_GetFiles_Call{Call: _e.mock.On("GetFiles", ctx, repoURL, revision, project, pattern, noRevisionCache, verifyCommit)}
}
func (_c *Repos_GetFiles_Call) Run(run func(ctx context.Context, repoURL string, revision string, project string, pattern string, noRevisionCache bool, verifyCommit bool)) *Repos_GetFiles_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 string
if args[1] != nil {
arg1 = args[1].(string)
}
var arg2 string
if args[2] != nil {
arg2 = args[2].(string)
}
var arg3 string
if args[3] != nil {
arg3 = args[3].(string)
}
var arg4 string
if args[4] != nil {
arg4 = args[4].(string)
}
var arg5 bool
if args[5] != nil {
arg5 = args[5].(bool)
}
var arg6 bool
if args[6] != nil {
arg6 = args[6].(bool)
}
run(
arg0,
arg1,
arg2,
arg3,
arg4,
arg5,
arg6,
)
})
return _c
}
func (_c *Repos_GetFiles_Call) Return(stringToBytes map[string][]byte, err error) *Repos_GetFiles_Call {
_c.Call.Return(stringToBytes, err)
return _c
}
func (_c *Repos_GetFiles_Call) RunAndReturn(run func(ctx context.Context, repoURL string, revision string, project string, pattern string, noRevisionCache bool, verifyCommit bool) (map[string][]byte, error)) *Repos_GetFiles_Call {
_c.Call.Return(run)
return _c
}

View File

@@ -5,15 +5,12 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7" "github.com/microsoft/azure-devops-go-api/azuredevops"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/core" "github.com/microsoft/azure-devops-go-api/azuredevops/core"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/git" "github.com/microsoft/azure-devops-go-api/azuredevops/git"
) )
const ( const AZURE_DEVOPS_DEFAULT_URL = "https://dev.azure.com"
AZURE_DEVOPS_DEFAULT_URL = "https://dev.azure.com"
AZURE_DEVOPS_PROJECT_NOT_FOUND_ERROR = "The following project does not exist"
)
type AzureDevOpsClientFactory interface { type AzureDevOpsClientFactory interface {
// Returns an Azure Devops Client interface. // Returns an Azure Devops Client interface.
@@ -73,22 +70,13 @@ func (a *AzureDevOpsService) List(ctx context.Context) ([]*PullRequest, error) {
SearchCriteria: &git.GitPullRequestSearchCriteria{}, SearchCriteria: &git.GitPullRequestSearchCriteria{},
} }
pullRequests := []*PullRequest{}
azurePullRequests, err := client.GetPullRequestsByProject(ctx, args) azurePullRequests, err := client.GetPullRequestsByProject(ctx, args)
if err != nil { if err != nil {
// A standard Http 404 error is not returned for Azure DevOps,
// so checking the error message for a specific pattern.
// NOTE: Since the repos are filtered later, only existence of the project
// is relevant for AzureDevOps
if strings.Contains(err.Error(), AZURE_DEVOPS_PROJECT_NOT_FOUND_ERROR) {
// return a custom error indicating that the repository is not found,
// but also return the empty result since the decision to continue or not in this case is made by the caller
return pullRequests, NewRepositoryNotFoundError(err)
}
return nil, fmt.Errorf("failed to get pull requests by project: %w", err) return nil, fmt.Errorf("failed to get pull requests by project: %w", err)
} }
pullRequests := []*PullRequest{}
for _, pr := range *azurePullRequests { for _, pr := range *azurePullRequests {
if pr.Repository == nil || if pr.Repository == nil ||
pr.Repository.Name == nil || pr.Repository.Name == nil ||

View File

@@ -2,12 +2,12 @@ package pull_request
import ( import (
"context" "context"
"errors"
"testing" "testing"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/core" "github.com/microsoft/azure-devops-go-api/azuredevops/webapi"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/git"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/webapi" "github.com/microsoft/azure-devops-go-api/azuredevops/core"
"github.com/microsoft/azure-devops-go-api/azuredevops/git"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -236,36 +236,3 @@ func TestBuildURL(t *testing.T) {
}) })
} }
} }
func TestAzureDevOpsListReturnsRepositoryNotFoundError(t *testing.T) {
args := git.GetPullRequestsByProjectArgs{
Project: createStringPtr("nonexistent"),
SearchCriteria: &git.GitPullRequestSearchCriteria{},
}
pullRequestMock := []git.GitPullRequest{}
gitClientMock := azureMock.Client{}
clientFactoryMock := &AzureClientFactoryMock{mock: &mock.Mock{}}
clientFactoryMock.mock.On("GetClient", mock.Anything).Return(&gitClientMock, nil)
// Mock the GetPullRequestsByProject to return an error containing "404"
gitClientMock.On("GetPullRequestsByProject", t.Context(), args).Return(&pullRequestMock,
errors.New("The following project does not exist:"))
provider := AzureDevOpsService{
clientFactory: clientFactoryMock,
project: "nonexistent",
repo: "nonexistent",
labels: nil,
}
prs, err := provider.List(t.Context())
// Should return empty pull requests list
assert.Empty(t, prs)
// Should return RepositoryNotFoundError
require.Error(t, err)
assert.True(t, IsRepositoryNotFoundError(err), "Expected RepositoryNotFoundError but got: %v", err)
}

View File

@@ -6,7 +6,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/url" "net/url"
"strings"
"github.com/ktrysmt/go-bitbucket" "github.com/ktrysmt/go-bitbucket"
) )
@@ -18,19 +17,10 @@ type BitbucketCloudService struct {
} }
type BitbucketCloudPullRequest struct { type BitbucketCloudPullRequest struct {
ID int `json:"id"` ID int `json:"id"`
Title string `json:"title"` Title string `json:"title"`
Source BitbucketCloudPullRequestSource `json:"source"` Source BitbucketCloudPullRequestSource `json:"source"`
Author BitbucketCloudPullRequestAuthor `json:"author"` Author BitbucketCloudPullRequestAuthor `json:"author"`
Destination BitbucketCloudPullRequestDestination `json:"destination"`
}
type BitbucketCloudPullRequestDestination struct {
Branch BitbucketCloudPullRequestDestinationBranch `json:"branch"`
}
type BitbucketCloudPullRequestDestinationBranch struct {
Name string `json:"name"`
} }
type BitbucketCloudPullRequestSource struct { type BitbucketCloudPullRequestSource struct {
@@ -118,17 +108,8 @@ func (b *BitbucketCloudService) List(_ context.Context) ([]*PullRequest, error)
RepoSlug: b.repositorySlug, RepoSlug: b.repositorySlug,
} }
pullRequests := []*PullRequest{}
response, err := b.client.Repositories.PullRequests.Gets(opts) response, err := b.client.Repositories.PullRequests.Gets(opts)
if err != nil { if err != nil {
// A standard Http 404 error is not returned for Bitbucket Cloud,
// so checking the error message for a specific pattern
if strings.Contains(err.Error(), "404 Not Found") {
// return a custom error indicating that the repository is not found,
// but also return the empty result since the decision to continue or not in this case is made by the caller
return pullRequests, NewRepositoryNotFoundError(err)
}
return nil, fmt.Errorf("error listing pull requests for %s/%s: %w", b.owner, b.repositorySlug, err) return nil, fmt.Errorf("error listing pull requests for %s/%s: %w", b.owner, b.repositorySlug, err)
} }
@@ -152,14 +133,14 @@ func (b *BitbucketCloudService) List(_ context.Context) ([]*PullRequest, error)
return nil, fmt.Errorf("error unmarshalling json to type '[]BitbucketCloudPullRequest': %w", err) return nil, fmt.Errorf("error unmarshalling json to type '[]BitbucketCloudPullRequest': %w", err)
} }
pullRequests := []*PullRequest{}
for _, pull := range pulls { for _, pull := range pulls {
pullRequests = append(pullRequests, &PullRequest{ pullRequests = append(pullRequests, &PullRequest{
Number: pull.ID, Number: pull.ID,
Title: pull.Title, Title: pull.Title,
Branch: pull.Source.Branch.Name, Branch: pull.Source.Branch.Name,
TargetBranch: pull.Destination.Branch.Name, HeadSHA: pull.Source.Commit.Hash,
HeadSHA: pull.Source.Commit.Hash, Author: pull.Author.Nickname,
Author: pull.Author.Nickname,
}) })
} }

View File

@@ -138,7 +138,7 @@ func TestListPullRequestPaginationCloud(t *testing.T) {
var err error var err error
switch r.RequestURI { switch r.RequestURI {
case "/repositories/OWNER/REPO/pullrequests/": case "/repositories/OWNER/REPO/pullrequests/":
_, err = fmt.Fprintf(w, `{ _, err = io.WriteString(w, fmt.Sprintf(`{
"size": 2, "size": 2,
"pagelen": 1, "pagelen": 1,
"page": 1, "page": 1,
@@ -177,9 +177,9 @@ func TestListPullRequestPaginationCloud(t *testing.T) {
} }
} }
] ]
}`, r.Host) }`, r.Host))
case "/repositories/OWNER/REPO/pullrequests/?pagelen=1&page=2": case "/repositories/OWNER/REPO/pullrequests/?pagelen=1&page=2":
_, err = fmt.Fprintf(w, `{ _, err = io.WriteString(w, fmt.Sprintf(`{
"size": 2, "size": 2,
"pagelen": 1, "pagelen": 1,
"page": 2, "page": 2,
@@ -202,7 +202,7 @@ func TestListPullRequestPaginationCloud(t *testing.T) {
} }
} }
] ]
}`, r.Host) }`, r.Host))
default: default:
t.Fail() t.Fail()
} }
@@ -329,7 +329,7 @@ func TestListPullRequestBranchMatchCloud(t *testing.T) {
var err error var err error
switch r.RequestURI { switch r.RequestURI {
case "/repositories/OWNER/REPO/pullrequests/": case "/repositories/OWNER/REPO/pullrequests/":
_, err = fmt.Fprintf(w, `{ _, err = io.WriteString(w, fmt.Sprintf(`{
"size": 2, "size": 2,
"pagelen": 1, "pagelen": 1,
"page": 1, "page": 1,
@@ -349,11 +349,6 @@ func TestListPullRequestBranchMatchCloud(t *testing.T) {
}, },
"author": { "author": {
"nickname": "testName" "nickname": "testName"
},
"destination": {
"branch": {
"name": "master"
}
} }
}, },
{ {
@@ -370,17 +365,12 @@ func TestListPullRequestBranchMatchCloud(t *testing.T) {
}, },
"author": { "author": {
"nickname": "testName" "nickname": "testName"
},
"destination": {
"branch": {
"name": "branch-200"
}
} }
} }
] ]
}`, r.Host) }`, r.Host))
case "/repositories/OWNER/REPO/pullrequests/?pagelen=1&page=2": case "/repositories/OWNER/REPO/pullrequests/?pagelen=1&page=2":
_, err = fmt.Fprintf(w, `{ _, err = io.WriteString(w, fmt.Sprintf(`{
"size": 2, "size": 2,
"pagelen": 1, "pagelen": 1,
"page": 2, "page": 2,
@@ -400,15 +390,10 @@ func TestListPullRequestBranchMatchCloud(t *testing.T) {
}, },
"author": { "author": {
"nickname": "testName" "nickname": "testName"
},
"destination": {
"branch": {
"name": "master"
}
} }
} }
] ]
}`, r.Host) }`, r.Host))
default: default:
t.Fail() t.Fail()
} }
@@ -428,20 +413,18 @@ func TestListPullRequestBranchMatchCloud(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, pullRequests, 2) assert.Len(t, pullRequests, 2)
assert.Equal(t, PullRequest{ assert.Equal(t, PullRequest{
Number: 101, Number: 101,
Title: "feat(101)", Title: "feat(101)",
Branch: "feature-101", Branch: "feature-101",
HeadSHA: "1a8dd249c04a", HeadSHA: "1a8dd249c04a",
Author: "testName", Author: "testName",
TargetBranch: "master",
}, *pullRequests[0]) }, *pullRequests[0])
assert.Equal(t, PullRequest{ assert.Equal(t, PullRequest{
Number: 102, Number: 102,
Title: "feat(102)", Title: "feat(102)",
Branch: "feature-102", Branch: "feature-102",
HeadSHA: "6344d9623e3b", HeadSHA: "6344d9623e3b",
Author: "testName", Author: "testName",
TargetBranch: "master",
}, *pullRequests[1]) }, *pullRequests[1])
regexp = `.*2$` regexp = `.*2$`
@@ -455,12 +438,11 @@ func TestListPullRequestBranchMatchCloud(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, pullRequests, 1) assert.Len(t, pullRequests, 1)
assert.Equal(t, PullRequest{ assert.Equal(t, PullRequest{
Number: 102, Number: 102,
Title: "feat(102)", Title: "feat(102)",
Branch: "feature-102", Branch: "feature-102",
HeadSHA: "6344d9623e3b", HeadSHA: "6344d9623e3b",
Author: "testName", Author: "testName",
TargetBranch: "master",
}, *pullRequests[0]) }, *pullRequests[0])
regexp = `[\d{2}` regexp = `[\d{2}`
@@ -472,49 +454,4 @@ func TestListPullRequestBranchMatchCloud(t *testing.T) {
}, },
}) })
require.Error(t, err) require.Error(t, err)
regexp = `feature-2[\d]{2}`
targetRegexp := `branch.*`
pullRequests, err = ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{
{
BranchMatch: &regexp,
TargetBranchMatch: &targetRegexp,
},
})
require.NoError(t, err)
assert.Len(t, pullRequests, 1)
assert.Equal(t, PullRequest{
Number: 200,
Title: "feat(200)",
Branch: "feature-200",
HeadSHA: "4cf807e67a6d",
Author: "testName",
TargetBranch: "branch-200",
}, *pullRequests[0])
}
func TestBitbucketCloudListReturnsRepositoryNotFoundError(t *testing.T) {
mux := http.NewServeMux()
server := httptest.NewServer(mux)
defer server.Close()
path := "/repositories/nonexistent/nonexistent/pullrequests/"
mux.HandleFunc(path, func(w http.ResponseWriter, _ *http.Request) {
// Return 404 status to simulate repository not found
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"message": "404 Project Not Found"}`))
})
svc, err := NewBitbucketCloudServiceNoAuth(server.URL, "nonexistent", "nonexistent")
require.NoError(t, err)
prs, err := svc.List(t.Context())
// Should return empty pull requests list
assert.Empty(t, prs)
// Should return RepositoryNotFoundError
require.Error(t, err)
assert.True(t, IsRepositoryNotFoundError(err), "Expected RepositoryNotFoundError but got: %v", err)
} }

View File

@@ -8,7 +8,7 @@ import (
bitbucketv1 "github.com/gfleury/go-bitbucket-v1" bitbucketv1 "github.com/gfleury/go-bitbucket-v1"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/argoproj/argo-cd/v3/applicationset/services" "github.com/argoproj/argo-cd/v3/applicationset/utils"
) )
type BitbucketService struct { type BitbucketService struct {
@@ -49,10 +49,15 @@ func NewBitbucketServiceNoAuth(ctx context.Context, url, projectKey, repositoryS
} }
func newBitbucketService(ctx context.Context, bitbucketConfig *bitbucketv1.Configuration, projectKey, repositorySlug string, scmRootCAPath string, insecure bool, caCerts []byte) (PullRequestService, error) { func newBitbucketService(ctx context.Context, bitbucketConfig *bitbucketv1.Configuration, projectKey, repositorySlug string, scmRootCAPath string, insecure bool, caCerts []byte) (PullRequestService, error) {
bbClient := services.SetupBitbucketClient(ctx, bitbucketConfig, scmRootCAPath, insecure, caCerts) bitbucketConfig.BasePath = utils.NormalizeBitbucketBasePath(bitbucketConfig.BasePath)
tlsConfig := utils.GetTlsConfig(scmRootCAPath, insecure, caCerts)
bitbucketConfig.HTTPClient = &http.Client{Transport: &http.Transport{
TLSClientConfig: tlsConfig,
}}
bitbucketClient := bitbucketv1.NewAPIClient(ctx, bitbucketConfig)
return &BitbucketService{ return &BitbucketService{
client: bbClient, client: bitbucketClient,
projectKey: projectKey, projectKey: projectKey,
repositorySlug: repositorySlug, repositorySlug: repositorySlug,
}, nil }, nil
@@ -67,11 +72,6 @@ func (b *BitbucketService) List(_ context.Context) ([]*PullRequest, error) {
for { for {
response, err := b.client.DefaultApi.GetPullRequestsPage(b.projectKey, b.repositorySlug, paged) response, err := b.client.DefaultApi.GetPullRequestsPage(b.projectKey, b.repositorySlug, paged)
if err != nil { if err != nil {
if response != nil && response.Response != nil && response.StatusCode == http.StatusNotFound {
// return a custom error indicating that the repository is not found,
// but also return the empty result since the decision to continue or not in this case is made by the caller
return pullRequests, NewRepositoryNotFoundError(err)
}
return nil, fmt.Errorf("error listing pull requests for %s/%s: %w", b.projectKey, b.repositorySlug, err) return nil, fmt.Errorf("error listing pull requests for %s/%s: %w", b.projectKey, b.repositorySlug, err)
} }
pulls, err := bitbucketv1.GetPullRequestsResponse(response) pulls, err := bitbucketv1.GetPullRequestsResponse(response)

View File

@@ -510,29 +510,3 @@ func TestListPullRequestBranchMatch(t *testing.T) {
}) })
require.Error(t, err) require.Error(t, err)
} }
func TestBitbucketServerListReturnsRepositoryNotFoundError(t *testing.T) {
mux := http.NewServeMux()
server := httptest.NewServer(mux)
defer server.Close()
path := "/rest/api/1.0/projects/nonexistent/repos/nonexistent/pull-requests?limit=100"
mux.HandleFunc(path, func(w http.ResponseWriter, _ *http.Request) {
// Return 404 status to simulate repository not found
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"message": "404 Project Not Found"}`))
})
svc, err := NewBitbucketServiceNoAuth(t.Context(), server.URL, "nonexistent", "nonexistent", "", false, nil)
require.NoError(t, err)
prs, err := svc.List(t.Context())
// Should return empty pull requests list
assert.Empty(t, prs)
// Should return RepositoryNotFoundError
require.Error(t, err)
assert.True(t, IsRepositoryNotFoundError(err), "Expected RepositoryNotFoundError but got: %v", err)
}

View File

@@ -1,23 +0,0 @@
package pull_request
import "errors"
// RepositoryNotFoundError represents an error when a repository is not found by a pull request provider
type RepositoryNotFoundError struct {
causingError error
}
func (e *RepositoryNotFoundError) Error() string {
return e.causingError.Error()
}
// NewRepositoryNotFoundError creates a new repository not found error
func NewRepositoryNotFoundError(err error) error {
return &RepositoryNotFoundError{causingError: err}
}
// IsRepositoryNotFoundError checks if the given error is a repository not found error
func IsRepositoryNotFoundError(err error) bool {
var repoErr *RepositoryNotFoundError
return errors.As(err, &repoErr)
}

View File

@@ -1,48 +0,0 @@
package pull_request
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRepositoryNotFoundError(t *testing.T) {
t.Run("NewRepositoryNotFoundError creates correct error type", func(t *testing.T) {
originalErr := errors.New("repository does not exist")
repoNotFoundErr := NewRepositoryNotFoundError(originalErr)
require.Error(t, repoNotFoundErr)
assert.Equal(t, "repository does not exist", repoNotFoundErr.Error())
})
t.Run("IsRepositoryNotFoundError identifies RepositoryNotFoundError", func(t *testing.T) {
originalErr := errors.New("repository does not exist")
repoNotFoundErr := NewRepositoryNotFoundError(originalErr)
assert.True(t, IsRepositoryNotFoundError(repoNotFoundErr))
})
t.Run("IsRepositoryNotFoundError returns false for regular errors", func(t *testing.T) {
regularErr := errors.New("some other error")
assert.False(t, IsRepositoryNotFoundError(regularErr))
})
t.Run("IsRepositoryNotFoundError returns false for nil error", func(t *testing.T) {
assert.False(t, IsRepositoryNotFoundError(nil))
})
t.Run("IsRepositoryNotFoundError works with wrapped errors", func(t *testing.T) {
originalErr := errors.New("repository does not exist")
repoNotFoundErr := NewRepositoryNotFoundError(originalErr)
wrappedErr := errors.New("wrapped: " + repoNotFoundErr.Error())
// Direct RepositoryNotFoundError should be identified
assert.True(t, IsRepositoryNotFoundError(repoNotFoundErr))
// Wrapped string error should not be identified (this is expected behavior)
assert.False(t, IsRepositoryNotFoundError(wrappedErr))
})
}

View File

@@ -14,12 +14,11 @@ type GiteaService struct {
client *gitea.Client client *gitea.Client
owner string owner string
repo string repo string
labels []string
} }
var _ PullRequestService = (*GiteaService)(nil) var _ PullRequestService = (*GiteaService)(nil)
func NewGiteaService(token, url, owner, repo string, labels []string, insecure bool) (PullRequestService, error) { func NewGiteaService(token, url, owner, repo string, insecure bool) (PullRequestService, error) {
if token == "" { if token == "" {
token = os.Getenv("GITEA_TOKEN") token = os.Getenv("GITEA_TOKEN")
} }
@@ -43,7 +42,6 @@ func NewGiteaService(token, url, owner, repo string, labels []string, insecure b
client: client, client: client,
owner: owner, owner: owner,
repo: repo, repo: repo,
labels: labels,
}, nil }, nil
} }
@@ -52,21 +50,12 @@ func (g *GiteaService) List(ctx context.Context) ([]*PullRequest, error) {
State: gitea.StateOpen, State: gitea.StateOpen,
} }
g.client.SetContext(ctx) g.client.SetContext(ctx)
list := []*PullRequest{} prs, _, err := g.client.ListRepoPullRequests(g.owner, g.repo, opts)
prs, resp, err := g.client.ListRepoPullRequests(g.owner, g.repo, opts)
if err != nil { if err != nil {
if resp != nil && resp.StatusCode == http.StatusNotFound {
// return a custom error indicating that the repository is not found,
// but also returning the empty result since the decision to continue or not in this case is made by the caller
return list, NewRepositoryNotFoundError(err)
}
return nil, err return nil, err
} }
list := []*PullRequest{}
for _, pr := range prs { for _, pr := range prs {
if !giteaContainLabels(g.labels, pr.Labels) {
continue
}
list = append(list, &PullRequest{ list = append(list, &PullRequest{
Number: int(pr.Index), Number: int(pr.Index),
Title: pr.Title, Title: pr.Title,
@@ -80,21 +69,6 @@ func (g *GiteaService) List(ctx context.Context) ([]*PullRequest, error) {
return list, nil return list, nil
} }
// containLabels returns true if gotLabels contains expectedLabels
func giteaContainLabels(expectedLabels []string, gotLabels []*gitea.Label) bool {
gotLabelNamesMap := make(map[string]bool)
for i := 0; i < len(gotLabels); i++ {
gotLabelNamesMap[gotLabels[i].Name] = true
}
for _, expected := range expectedLabels {
v, ok := gotLabelNamesMap[expected]
if !v || !ok {
return false
}
}
return true
}
// Get the Gitea pull request label names. // Get the Gitea pull request label names.
func getGiteaPRLabelNames(giteaLabels []*gitea.Label) []string { func getGiteaPRLabelNames(giteaLabels []*gitea.Label) []string {
var labelNames []string var labelNames []string

View File

@@ -52,7 +52,7 @@ func giteaMockHandler(t *testing.T) func(http.ResponseWriter, *http.Request) {
}, },
"title": "add an empty file", "title": "add an empty file",
"body": "", "body": "",
"labels": [{"id": 1, "name": "label1", "color": "00aabb", "description": "foo", "url": ""}], "labels": [],
"milestone": null, "milestone": null,
"assignee": null, "assignee": null,
"assignees": null, "assignees": null,
@@ -246,59 +246,11 @@ func giteaMockHandler(t *testing.T) func(http.ResponseWriter, *http.Request) {
} }
} }
func TestGiteaContainLabels(t *testing.T) {
cases := []struct {
Name string
Labels []string
PullLabels []*gitea.Label
Expect bool
}{
{
Name: "Match labels",
Labels: []string{"label1", "label2"},
PullLabels: []*gitea.Label{
{Name: "label1"},
{Name: "label2"},
{Name: "label3"},
},
Expect: true,
},
{
Name: "Not match labels",
Labels: []string{"label1", "label4"},
PullLabels: []*gitea.Label{
{Name: "label1"},
{Name: "label2"},
{Name: "label3"},
},
Expect: false,
},
{
Name: "No specify",
Labels: []string{},
PullLabels: []*gitea.Label{
{Name: "label1"},
{Name: "label2"},
{Name: "label3"},
},
Expect: true,
},
}
for _, c := range cases {
t.Run(c.Name, func(t *testing.T) {
if got := giteaContainLabels(c.Labels, c.PullLabels); got != c.Expect {
t.Errorf("expect: %v, got: %v", c.Expect, got)
}
})
}
}
func TestGiteaList(t *testing.T) { func TestGiteaList(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
giteaMockHandler(t)(w, r) giteaMockHandler(t)(w, r)
})) }))
host, err := NewGiteaService("", ts.URL, "test-argocd", "pr-test", []string{"label1"}, false) host, err := NewGiteaService("", ts.URL, "test-argocd", "pr-test", false)
require.NoError(t, err) require.NoError(t, err)
prs, err := host.List(t.Context()) prs, err := host.List(t.Context())
require.NoError(t, err) require.NoError(t, err)
@@ -339,35 +291,3 @@ func TestGetGiteaPRLabelNames(t *testing.T) {
}) })
} }
} }
func TestGiteaListReturnsRepositoryNotFoundError(t *testing.T) {
mux := http.NewServeMux()
server := httptest.NewServer(mux)
defer server.Close()
// Handle version endpoint that Gitea client calls first
mux.HandleFunc("/api/v1/version", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"version":"1.17.0+dev-452-g1f0541780"}`))
})
path := "/api/v1/repos/nonexistent/nonexistent/pulls?limit=0&page=1&state=open"
mux.HandleFunc(path, func(w http.ResponseWriter, _ *http.Request) {
// Return 404 status to simulate repository not found
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"message": "404 Project Not Found"}`))
})
svc, err := NewGiteaService("", server.URL, "nonexistent", "nonexistent", []string{}, false)
require.NoError(t, err)
prs, err := svc.List(t.Context())
// Should return empty pull requests list
assert.Empty(t, prs)
// Should return RepositoryNotFoundError
require.Error(t, err)
assert.True(t, IsRepositoryNotFoundError(err), "Expected RepositoryNotFoundError but got: %v", err)
}

View File

@@ -6,9 +6,7 @@ import (
"net/http" "net/http"
"os" "os"
"github.com/google/go-github/v69/github" "github.com/google/go-github/v66/github"
appsetutils "github.com/argoproj/argo-cd/v3/applicationset/utils"
) )
type GithubService struct { type GithubService struct {
@@ -20,28 +18,18 @@ type GithubService struct {
var _ PullRequestService = (*GithubService)(nil) var _ PullRequestService = (*GithubService)(nil)
func NewGithubService(token, url, owner, repo string, labels []string, optionalHTTPClient ...*http.Client) (PullRequestService, error) { func NewGithubService(token, url, owner, repo string, labels []string) (PullRequestService, error) {
// Undocumented environment variable to set a default token, to be used in testing to dodge anonymous rate limits. // Undocumented environment variable to set a default token, to be used in testing to dodge anonymous rate limits.
if token == "" { if token == "" {
token = os.Getenv("GITHUB_TOKEN") token = os.Getenv("GITHUB_TOKEN")
} }
httpClient := &http.Client{}
var client *github.Client var client *github.Client
httpClient := appsetutils.GetOptionalHTTPClient(optionalHTTPClient...)
if url == "" { if url == "" {
if token == "" { client = github.NewClient(httpClient).WithAuthToken(token)
client = github.NewClient(httpClient)
} else {
client = github.NewClient(httpClient).WithAuthToken(token)
}
} else { } else {
var err error var err error
if token == "" { client, err = github.NewClient(httpClient).WithEnterpriseURLs(url, url)
client, err = github.NewClient(httpClient).WithEnterpriseURLs(url, url)
} else {
client, err = github.NewClient(httpClient).WithAuthToken(token).WithEnterpriseURLs(url, url)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -64,11 +52,6 @@ func (g *GithubService) List(ctx context.Context) ([]*PullRequest, error) {
for { for {
pulls, resp, err := g.client.PullRequests.List(ctx, g.owner, g.repo, opts) pulls, resp, err := g.client.PullRequests.List(ctx, g.owner, g.repo, opts)
if err != nil { if err != nil {
if resp != nil && resp.StatusCode == http.StatusNotFound {
// return a custom error indicating that the repository is not found,
// but also returning the empty result since the decision to continue or not in this case is made by the caller
return pullRequests, NewRepositoryNotFoundError(err)
}
return nil, fmt.Errorf("error listing pull requests for %s/%s: %w", g.owner, g.repo, err) return nil, fmt.Errorf("error listing pull requests for %s/%s: %w", g.owner, g.repo, err)
} }
for _, pull := range pulls { for _, pull := range pulls {

View File

@@ -1,16 +1,12 @@
package pull_request package pull_request
import ( import (
"net/http"
"github.com/argoproj/argo-cd/v3/applicationset/services/github_app_auth" "github.com/argoproj/argo-cd/v3/applicationset/services/github_app_auth"
"github.com/argoproj/argo-cd/v3/applicationset/services/internal/github_app" "github.com/argoproj/argo-cd/v3/applicationset/services/internal/github_app"
appsetutils "github.com/argoproj/argo-cd/v3/applicationset/utils"
) )
func NewGithubAppService(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) (PullRequestService, error) {
httpClient := appsetutils.GetOptionalHTTPClient(optionalHTTPClient...) client, err := github_app.Client(g, url)
client, err := github_app.Client(g, url, httpClient)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -1,12 +1,9 @@
package pull_request package pull_request
import ( import (
"net/http"
"net/http/httptest"
"testing" "testing"
"github.com/google/go-github/v69/github" "github.com/google/go-github/v66/github"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -89,29 +86,3 @@ func TestGetGitHubPRLabelNames(t *testing.T) {
}) })
} }
} }
func TestGitHubListReturnsRepositoryNotFoundError(t *testing.T) {
mux := http.NewServeMux()
server := httptest.NewServer(mux)
defer server.Close()
path := "/repos/nonexistent/nonexistent/pulls"
mux.HandleFunc(path, func(w http.ResponseWriter, _ *http.Request) {
// Return 404 status to simulate repository not found
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"message": "404 Project Not Found"}`))
})
svc, err := NewGithubService("", server.URL, "nonexistent", "nonexistent", []string{}, nil)
require.NoError(t, err)
prs, err := svc.List(t.Context())
// Should return empty pull requests list
assert.Empty(t, prs)
// Should return RepositoryNotFoundError
require.Error(t, err)
assert.True(t, IsRepositoryNotFoundError(err), "Expected RepositoryNotFoundError but got: %v", err)
}

View File

@@ -76,11 +76,6 @@ func (g *GitLabService) List(ctx context.Context) ([]*PullRequest, error) {
for { for {
mrs, resp, err := g.client.MergeRequests.ListProjectMergeRequests(g.project, opts, gitlab.WithContext(ctx)) mrs, resp, err := g.client.MergeRequests.ListProjectMergeRequests(g.project, opts, gitlab.WithContext(ctx))
if err != nil { if err != nil {
if resp != nil && resp.StatusCode == http.StatusNotFound {
// return a custom error indicating that the repository is not found,
// but also returning the empty result since the decision to continue or not in this case is made by the caller
return pullRequests, NewRepositoryNotFoundError(err)
}
return nil, fmt.Errorf("error listing merge requests for project '%s': %w", g.project, err) return nil, fmt.Errorf("error listing merge requests for project '%s': %w", g.project, err)
} }
for _, mr := range mrs { for _, mr := range mrs {

View File

@@ -191,29 +191,3 @@ func TestListWithStateTLS(t *testing.T) {
}) })
} }
} }
func TestGitLabListReturnsRepositoryNotFoundError(t *testing.T) {
mux := http.NewServeMux()
server := httptest.NewServer(mux)
defer server.Close()
path := "/api/v4/projects/nonexistent/merge_requests"
mux.HandleFunc(path, func(w http.ResponseWriter, _ *http.Request) {
// Return 404 status to simulate repository not found
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"message": "404 Project Not Found"}`))
})
svc, err := NewGitLabService("", server.URL, "nonexistent", []string{}, "", "", false, nil)
require.NoError(t, err)
prs, err := svc.List(t.Context())
// Should return empty pull requests list
assert.Empty(t, prs)
// Should return RepositoryNotFoundError
require.Error(t, err)
assert.True(t, IsRepositoryNotFoundError(err), "Expected RepositoryNotFoundError but got: %v", err)
}

View File

@@ -30,5 +30,4 @@ type PullRequestService interface {
type Filter struct { type Filter struct {
BranchMatch *regexp.Regexp BranchMatch *regexp.Regexp
TargetBranchMatch *regexp.Regexp TargetBranchMatch *regexp.Regexp
TitleMatch *regexp.Regexp
} }

View File

@@ -25,12 +25,6 @@ func compileFilters(filters []argoprojiov1alpha1.PullRequestGeneratorFilter) ([]
return nil, fmt.Errorf("error compiling TargetBranchMatch regexp %q: %w", *filter.TargetBranchMatch, err) return nil, fmt.Errorf("error compiling TargetBranchMatch regexp %q: %w", *filter.TargetBranchMatch, err)
} }
} }
if filter.TitleMatch != nil {
outFilter.TitleMatch, err = regexp.Compile(*filter.TitleMatch)
if err != nil {
return nil, fmt.Errorf("error compiling TitleMatch regexp %q: %w", *filter.TitleMatch, err)
}
}
outFilters = append(outFilters, outFilter) outFilters = append(outFilters, outFilter)
} }
return outFilters, nil return outFilters, nil
@@ -43,9 +37,6 @@ func matchFilter(pullRequest *PullRequest, filter *Filter) bool {
if filter.TargetBranchMatch != nil && !filter.TargetBranchMatch.MatchString(pullRequest.TargetBranch) { if filter.TargetBranchMatch != nil && !filter.TargetBranchMatch.MatchString(pullRequest.TargetBranch) {
return false return false
} }
if filter.TitleMatch != nil && !filter.TitleMatch.MatchString(pullRequest.Title) {
return false
}
return true return true
} }

View File

@@ -137,110 +137,6 @@ func TestFilterTargetBranchMatch(t *testing.T) {
assert.Equal(t, "two", pullRequests[0].Branch) assert.Equal(t, "two", pullRequests[0].Branch)
} }
func TestFilterTitleMatch(t *testing.T) {
provider, _ := NewFakeService(
t.Context(),
[]*PullRequest{
{
Number: 1,
Title: "PR one - filter",
Branch: "one",
TargetBranch: "master",
HeadSHA: "189d92cbf9ff857a39e6feccd32798ca700fb958",
Author: "name1",
},
{
Number: 2,
Title: "PR two - ignore",
Branch: "two",
TargetBranch: "branch1",
HeadSHA: "289d92cbf9ff857a39e6feccd32798ca700fb958",
Author: "name2",
},
{
Number: 3,
Title: "[filter] PR three",
Branch: "three",
TargetBranch: "branch2",
HeadSHA: "389d92cbf9ff857a39e6feccd32798ca700fb958",
Author: "name3",
},
{
Number: 4,
Title: "[ignore] PR four",
Branch: "four",
TargetBranch: "branch3",
HeadSHA: "489d92cbf9ff857a39e6feccd32798ca700fb958",
Author: "name4",
},
},
nil,
)
filters := []argoprojiov1alpha1.PullRequestGeneratorFilter{
{
TitleMatch: strp("\\[filter]"),
},
}
pullRequests, err := ListPullRequests(t.Context(), provider, filters)
require.NoError(t, err)
assert.Len(t, pullRequests, 1)
assert.Equal(t, "three", pullRequests[0].Branch)
}
func TestMultiFilterOrWithTitle(t *testing.T) {
provider, _ := NewFakeService(
t.Context(),
[]*PullRequest{
{
Number: 1,
Title: "PR one - filter",
Branch: "one",
TargetBranch: "master",
HeadSHA: "189d92cbf9ff857a39e6feccd32798ca700fb958",
Author: "name1",
},
{
Number: 2,
Title: "PR two - ignore",
Branch: "two",
TargetBranch: "branch1",
HeadSHA: "289d92cbf9ff857a39e6feccd32798ca700fb958",
Author: "name2",
},
{
Number: 3,
Title: "[filter] PR three",
Branch: "three",
TargetBranch: "branch2",
HeadSHA: "389d92cbf9ff857a39e6feccd32798ca700fb958",
Author: "name3",
},
{
Number: 4,
Title: "[ignore] PR four",
Branch: "four",
TargetBranch: "branch3",
HeadSHA: "489d92cbf9ff857a39e6feccd32798ca700fb958",
Author: "name4",
},
},
nil,
)
filters := []argoprojiov1alpha1.PullRequestGeneratorFilter{
{
TitleMatch: strp("\\[filter]"),
},
{
TitleMatch: strp("- filter"),
},
}
pullRequests, err := ListPullRequests(t.Context(), provider, filters)
require.NoError(t, err)
assert.Len(t, pullRequests, 2)
assert.Equal(t, "one", pullRequests[0].Branch)
assert.Equal(t, "three", pullRequests[1].Branch)
}
func TestMultiFilterOr(t *testing.T) { func TestMultiFilterOr(t *testing.T) {
provider, _ := NewFakeService( provider, _ := NewFakeService(
t.Context(), t.Context(),
@@ -296,7 +192,7 @@ func TestMultiFilterOr(t *testing.T) {
assert.Equal(t, "four", pullRequests[2].Branch) assert.Equal(t, "four", pullRequests[2].Branch)
} }
func TestMultiFilterOrWithTargetBranchFilterOrWithTitleFilter(t *testing.T) { func TestMultiFilterOrWithTargetBranchFilter(t *testing.T) {
provider, _ := NewFakeService( provider, _ := NewFakeService(
t.Context(), t.Context(),
[]*PullRequest{ []*PullRequest{
@@ -332,14 +228,6 @@ func TestMultiFilterOrWithTargetBranchFilterOrWithTitleFilter(t *testing.T) {
HeadSHA: "489d92cbf9ff857a39e6feccd32798ca700fb958", HeadSHA: "489d92cbf9ff857a39e6feccd32798ca700fb958",
Author: "name4", Author: "name4",
}, },
{
Number: 5,
Title: "PR title is different than branch name",
Branch: "five",
TargetBranch: "branch3",
HeadSHA: "489d92cbf9ff857a39e6feccd32798ca700fb958",
Author: "name5",
},
}, },
nil, nil,
) )
@@ -352,21 +240,12 @@ func TestMultiFilterOrWithTargetBranchFilterOrWithTitleFilter(t *testing.T) {
BranchMatch: strp("r"), BranchMatch: strp("r"),
TargetBranchMatch: strp("3"), TargetBranchMatch: strp("3"),
}, },
{
TitleMatch: strp("two"),
},
{
BranchMatch: strp("five"),
TitleMatch: strp("PR title is different than branch name"),
},
} }
pullRequests, err := ListPullRequests(t.Context(), provider, filters) pullRequests, err := ListPullRequests(t.Context(), provider, filters)
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, pullRequests, 3) assert.Len(t, pullRequests, 2)
assert.Equal(t, "two", pullRequests[0].Branch) assert.Equal(t, "two", pullRequests[0].Branch)
assert.Equal(t, "four", pullRequests[1].Branch) assert.Equal(t, "four", pullRequests[1].Branch)
assert.Equal(t, "five", pullRequests[2].Branch)
assert.Equal(t, "PR title is different than branch name", pullRequests[2].Title)
} }
func TestNoFilters(t *testing.T) { func TestNoFilters(t *testing.T) {

View File

@@ -7,7 +7,7 @@ import (
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/reposerver/apiclient" "github.com/argoproj/argo-cd/v3/reposerver/apiclient"
"github.com/argoproj/argo-cd/v3/util/db" "github.com/argoproj/argo-cd/v3/util/db"
utilio "github.com/argoproj/argo-cd/v3/util/io" "github.com/argoproj/argo-cd/v3/util/io"
) )
type argoCDService struct { type argoCDService struct {
@@ -36,7 +36,7 @@ func NewArgoCDService(db db.ArgoDB, submoduleEnabled bool, repoClientset apiclie
if err != nil { if err != nil {
return nil, fmt.Errorf("error initializing new repo server client: %w", err) return nil, fmt.Errorf("error initializing new repo server client: %w", err)
} }
defer utilio.Close(closer) defer io.Close(closer)
return client.GetGitFiles(ctx, fileRequest) return client.GetGitFiles(ctx, fileRequest)
}, },
getGitDirectoriesFromRepoServer: func(ctx context.Context, dirRequest *apiclient.GitDirectoriesRequest) (*apiclient.GitDirectoriesResponse, error) { getGitDirectoriesFromRepoServer: func(ctx context.Context, dirRequest *apiclient.GitDirectoriesRequest) (*apiclient.GitDirectoriesResponse, error) {
@@ -44,7 +44,7 @@ func NewArgoCDService(db db.ArgoDB, submoduleEnabled bool, repoClientset apiclie
if err != nil { if err != nil {
return nil, fmt.Errorf("error initialising new repo server client: %w", err) return nil, fmt.Errorf("error initialising new repo server client: %w", err)
} }
defer utilio.Close(closer) defer io.Close(closer)
return client.GetGitDirectories(ctx, dirRequest) return client.GetGitDirectories(ctx, dirRequest)
}, },
} }

View File

@@ -4,21 +4,22 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"maps"
pathpkg "path" pathpkg "path"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/aws/arn"
"github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds" "github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/codecommit" "github.com/aws/aws-sdk-go/service/codecommit"
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi" "github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
"k8s.io/utils/strings/slices"
application "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" application "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
) )
@@ -298,7 +299,7 @@ func (p *AWSCodeCommitProvider) getTagFilters() []*resourcegroupstaggingapi.TagF
filter.Values = append(filter.Values, aws.String(tagFilter.Value)) filter.Values = append(filter.Values, aws.String(tagFilter.Value))
} }
} }
return slices.Collect(maps.Values(filters)) return maps.Values(filters)
} }
func getCodeCommitRepoName(repoArn string) (string, error) { func getCodeCommitRepoName(repoArn string) (string, error) {
@@ -340,7 +341,7 @@ func toAbsolutePath(path string) string {
if filepath.IsAbs(path) { if filepath.IsAbs(path) {
return path return path
} }
return filepath.ToSlash(filepath.Join("/", path)) //nolint:gocritic // Prepend slash to have an absolute path return filepath.ToSlash(filepath.Join("/", path))
} }
func createAWSDiscoveryClients(_ context.Context, role string, region string) (*resourcegroupstaggingapi.ResourceGroupsTaggingAPI, *codecommit.CodeCommit, error) { func createAWSDiscoveryClients(_ context.Context, role string, region string) (*resourcegroupstaggingapi.ResourceGroupsTaggingAPI, *codecommit.CodeCommit, error) {

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