Compare commits

..

48 Commits

Author SHA1 Message Date
github-actions[bot]
27d1e641b6 Bump version to 2.12.4 (#20115)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: ishitasequeira <46771830+ishitasequeira@users.noreply.github.com>
2024-09-26 02:25:44 -04:00
gcp-cherry-pick-bot[bot]
b76a09e070 fix: CVE-2024-45296 Backtracking regular expressions cause ReDoS by upgrading path-to-regexp from 1.8.0 to 1.9.0 (#20087) (#20090)
Signed-off-by: Cheng Fang <cfang@redhat.com>
Co-authored-by: Cheng Fang <cfang@redhat.com>
2024-09-24 23:28:03 -04:00
Ishita Sequeira
ff3ef717e2 cherry-pick chore(deps-dev): bump webpack from 5.84.1 to 5.94.0 in /ui (#20056)
Signed-off-by: Ishita Sequeira <ishiseq29@gmail.com>
2024-09-23 09:55:15 -04:00
gcp-cherry-pick-bot[bot]
08fe6f5aea chore(deps): bump dompurify from 2.3.6 to 2.5.6 in /ui (#19955) (#20016)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 2.3.6 to 2.5.6.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/2.3.6...2.5.6)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-19 21:18:42 -04:00
gcp-cherry-pick-bot[bot]
1568165166 fix(appset): Fix perpetual appset reconciliation (#19822) (#19995)
Golang maps do not guarantee the order of the application resources
from the applicationset which causes rapid sync activity for the applicationset
as the objects and hence their resourceVersions are updated after each reconcile loop.

This then triggers reconciliation of all objects watching the
ApplicationSet.

In order to prevent this behaviour, ensure that the ApplicationSet
reconciler provides an idempotent list of resources, ensuring objects
are not updated.

Fixes: #19757

Signed-off-by: Thibault Jamet <thibault.jamet@adevinta.com>
Signed-off-by: Fabián Sellés <fabian.selles@adevinta.com>
Co-authored-by: Thibault Jamet <tjamet@users.noreply.github.com>
Co-authored-by: Fabian Selles <fabian.sellesrosa@gmail.com>
Co-authored-by: Ariadna Rouco <ariadna.rouco@adevinta.com>
2024-09-19 13:11:28 +05:30
Cheng Fang
8590550a22 chore(deps): bump express from 4.19.2 to 4.20.0 in /ui (#19883) (#19987) 2024-09-18 16:18:26 -04:00
gcp-cherry-pick-bot[bot]
d56ef7641c fix: diffing should not fail if resource fail schema validation (#19714) (#19729)
Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
Co-authored-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
2024-08-29 06:14:49 -10:00
gcp-cherry-pick-bot[bot]
02b8336890 docs: note cluster scoping changes in 2.12x (#19684) (#19702)
* docs: note cluster scoping changes in 2.12x

Related to #18748,#19585 and #19587.



* docs: add note in projects doc.



---------

Signed-off-by: Blake Pettersson <blake.pettersson@gmail.com>
Co-authored-by: Blake Pettersson <blake.pettersson@gmail.com>
2024-08-28 13:30:13 +05:30
github-actions[bot]
6b9cd828c6 Bump version to 2.12.3 (#19694)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: ishitasequeira <46771830+ishitasequeira@users.noreply.github.com>
2024-08-27 07:56:43 -04:00
gcp-cherry-pick-bot[bot]
cafd35cea7 fix(AnyNameSpaceRegex): Additional Functions Glob to Regexexp (#19516) (#19665)
Signed-off-by: Arthur <arthur@arthurvardevanyan.com>
Co-authored-by: Arthur Vardevanyan <arthur@arthurvardevanyan.com>
2024-08-23 15:19:13 -04:00
gcp-cherry-pick-bot[bot]
343dec049a feat(sourceNamespace): Regex Support (#19016) (#19017) (#19664)
* feat(sourceNamespace): Regex Support



* feat(sourceNamespace): Separate exactMatch into patternMatch



---------

Signed-off-by: Arthur <arthur@arthurvardevanyan.com>
Co-authored-by: Arthur Vardevanyan <arthur@arthurvardevanyan.com>
2024-08-23 08:53:48 -04:00
github-actions[bot]
560953c37b Bump version to 2.12.2 (#19657)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: crenshaw-dev <350466+crenshaw-dev@users.noreply.github.com>
2024-08-22 23:29:03 -04:00
rumstead
7244c2d5e5 fix(appset): remove cache references (#19652)
Signed-off-by: rumstead <37445536+rumstead@users.noreply.github.com>
2024-08-22 18:09:56 -04:00
gcp-cherry-pick-bot[bot]
b068220503 fix(appset): informer is not a kubernetes informer (#18905) (#19618) (#19636)
Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
Co-authored-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
2024-08-21 20:17:21 -08:00
gcp-cherry-pick-bot[bot]
c873d5c68a fix: Floating title content incorrect for multi-sources (#17274) (#19623) (#19627)
Signed-off-by: Keith Chong <kykchong@redhat.com>
Co-authored-by: Keith Chong <kykchong@redhat.com>
2024-08-21 19:31:48 -04:00
gcp-cherry-pick-bot[bot]
88f85daf52 fix: Parse hostname correctly from repoURL to fetch correct CA cert (#19488) (#19602)
Signed-off-by: Siddhesh Ghadi <sghadi1203@gmail.com>
Co-authored-by: Siddhesh Ghadi <61187612+svghadi@users.noreply.github.com>
Co-authored-by: Jann Fischer <jann@mistrust.net>
2024-08-21 00:48:58 -04:00
github-actions[bot]
26b2039a55 Bump version to 2.12.1 (#19568)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: ishitasequeira <46771830+ishitasequeira@users.noreply.github.com>
2024-08-16 12:39:58 -04:00
Ishita Sequeira
952838cdde fix(appset): cherry-pick - fix appset-in-any-namespace issue with git generators (#19558)
* fix appset-in-any-namespace issue with git generators

Signed-off-by: Ishita Sequeira <ishiseq29@gmail.com>

* fix lint issue

Signed-off-by: Ishita Sequeira <ishiseq29@gmail.com>

---------

Signed-off-by: Ishita Sequeira <ishiseq29@gmail.com>
2024-08-15 17:14:05 -04:00
gcp-cherry-pick-bot[bot]
7af4526666 fix: appset gpg limitation for templated project fields (#19492) (#19534)
* document templating project field while using applicationset git generator and signature verification



* revert changes to generated mocks



* Add check for templated project field and add limitation to the docs



* optimize checks and rephrase documentation



* remove unwanted variable declaration



* Add unit tests



---------

Signed-off-by: Ishita Sequeira <ishiseq29@gmail.com>
Co-authored-by: Ishita Sequeira <46771830+ishitasequeira@users.noreply.github.com>
2024-08-14 12:27:32 -04:00
gcp-cherry-pick-bot[bot]
b156b61e22 fix(appset): missing permissions for cluster install (#19059) (#19430) (#19435)
Signed-off-by: Dmitry Khodorov <el1191@woyd.ru>
Co-authored-by: Dmitry Khodorov <el1191@woyd.ru>
2024-08-08 00:35:24 -04:00
Jae Ryong Song
fd478450e6 fix: docs version regex changed (#18756) (#19352)
Signed-off-by: jasong <jasong@student.42seoul.kr>
2024-08-07 20:36:13 -04:00
github-actions[bot]
ec30a48bce Bump version to 2.12.0 (#19383)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: ishitasequeira <46771830+ishitasequeira@users.noreply.github.com>
2024-08-05 09:24:37 -04:00
gcp-cherry-pick-bot[bot]
57e61b2d8a feat: Add custom health check for cluster-api AWSManagedControlPlane (#19304) (#19360)
* add custom health check for awsmanagedcontrolplane



* chore(deps): bump github.com/aws/aws-sdk-go from 1.55.3 to 1.55.4 (#19295)

Bumps [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) from 1.55.3 to 1.55.4.
- [Release notes](https://github.com/aws/aws-sdk-go/releases)
- [Commits](https://github.com/aws/aws-sdk-go/compare/v1.55.3...v1.55.4)

---
updated-dependencies:
- dependency-name: github.com/aws/aws-sdk-go
  dependency-type: direct:production
  update-type: version-update:semver-patch
...





* add new line at the end of health_test.yaml file



---------

Signed-off-by: Iulian Taiatu <itaiatu@adobe.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: itaiatu <140485521+itaiatu@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-02 10:55:46 -07:00
github-actions[bot]
6f2ae0dd46 Bump version to 2.12.0-rc5 (#19349)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: ishitasequeira <46771830+ishitasequeira@users.noreply.github.com>
2024-08-01 19:24:32 -04:00
gcp-cherry-pick-bot[bot]
3e31ce9470 fix: ArgoCD 2.11 - Loop of PATCH calls to Application objects (#19340) (#19343)
Signed-off-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
Co-authored-by: Alexander Matyushentsev <AMatyushentsev@gmail.com>
2024-08-01 16:11:47 -04:00
gcp-cherry-pick-bot[bot]
dee59f3002 feat(rbac): allow validation of fine-grained policy in project (#19338) (#19339)
Signed-off-by: Alexandre Gaudreault <alexandre_gaudreault@intuit.com>
Co-authored-by: Alexandre Gaudreault <alexandre_gaudreault@intuit.com>
2024-08-01 10:37:55 -04:00
pasha-codefresh
d6c37aab88 Merge commit from fork (#19330) 2024-08-01 01:11:15 +03:00
pasha-codefresh
eaa1972e69 feat: webhook ddos -2.12 (#19329) 2024-08-01 00:53:59 +03:00
gcp-cherry-pick-bot[bot]
004cabba47 fix(applicationset): ensure that older applicationStatus is updated with new required values (#19165) (#19216)
Signed-off-by: wparr-circle <william.parr@circle.com>
Co-authored-by: William Parr <william.parr@circle.com>
2024-07-25 10:12:31 -04:00
gcp-cherry-pick-bot[bot]
d9263101fc chore: revert "feat: removed legacy app tracking label (#13203)" (cherry-pick #19196)
This reverts commit 4d8436b7e1.

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
Co-authored-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
2024-07-24 16:31:36 -04:00
Blake Pettersson
cec8504df2 fix: cherry-pick #18761 (v2.12) (#19109)
* fix(applicationset): use requeue after if generate app errors out (#18761)

The `GenerateApplications` can call to external resources like Github
API for instance which might be rate limited or fail. If those requests
somehow fail we should requeue them after some time like (same
reason as e98d3b2a87/applicationset/controllers/applicationset_controller.go (L154)).

For instance, in our environments we were rate limited by Github and the ArgoCD
applicationset controller was logging the following error about every
second or less for every application set using the pull request generator
that we have:
```
time="2024-06-21T14:17:15Z" level=error msg="error generating params" error="error listing repos: error listing pull requests for LedgerHQ/xxx: GET https://api.github.com/repos/LedgerHQ/xxx/pulls?per_page=100: 403 API rate limit exceeded for installation ID xxx. If you reach out to GitHub Support for help, please include the request ID xxx and timestamp 2024-06-xx xxx UTC. [rate reset in 8m18s]" generator="&{0xc000d652c0 0x289a100 {0xc00087bdd0}  [] true}"
time="2024-06-21T14:17:15Z" level=error msg="error generating application from params" applicationset=argocd/xxx error="error listing repos: error listing pull requests for LedgerHQ/xxxx: GET https://api.github.com/repos/LedgerHQ/xxx/pulls?per_page=100: 403 API rate limit exceeded for installation ID xxx. If you reach out to GitHub Support for help, please include the request ID xxx and timestamp 2024-06-xx xxx UTC. [rate reset in 8m18s]" generator="{nil nil nil nil nil &PullRequestGenerator{Github:&PullRequestGeneratorGithub{Owner:LedgerHQ,Repo:xxx,API:,TokenRef:nil,AppSecretName:xxxx,Labels:[argocd/preview],},GitLab:nil,Gitea:nil,BitbucketServer:nil,Filters:[]PullRequestGeneratorFilter{},RequeueAfterSeconds:*1800,Template:ApplicationSetTemplate{ApplicationSetTemplateMeta:ApplicationSetTemplateMeta{Name:,Namespace:,Labels:map[string]string{},Annotations:map[string]string{},Finalizers:[],},Spec:ApplicationSpec{Source:nil,Destination:ApplicationDestination{Server:,Namespace:,Name:,},Project:,SyncPolicy:nil,IgnoreDifferences:[]ResourceIgnoreDifferences{},Info:[]Info{},RevisionHistoryLimit:nil,Sources:[]ApplicationSource{},},},Bitbucket:nil,AzureDevOps:nil,} nil nil nil nil}"
```

Signed-off-by: Arthur Outhenin-Chalandre <arthur.outhenin-chalandre@ledger.fr>

* test: cherry-pick fixes

Signed-off-by: Blake Pettersson <blake.pettersson@gmail.com>

* chore: please the linter

Signed-off-by: Blake Pettersson <blake.pettersson@gmail.com>

---------

Signed-off-by: Arthur Outhenin-Chalandre <arthur.outhenin-chalandre@ledger.fr>
Signed-off-by: Blake Pettersson <blake.pettersson@gmail.com>
Co-authored-by: Arthur Outhenin-Chalandre <arthur.outhenin-chalandre@ledger.fr>
2024-07-18 22:31:40 -04:00
github-actions[bot]
0704aa6506 Bump version to 2.12.0-rc4 (#19060)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: crenshaw-dev <350466+crenshaw-dev@users.noreply.github.com>
2024-07-15 13:28:20 -04:00
Alexandre Gaudreault
511d6e371c chore: bump gitops-engine (#19056)
Signed-off-by: Alexandre Gaudreault <alexandre_gaudreault@intuit.com>
2024-07-15 12:08:15 -04:00
gcp-cherry-pick-bot[bot]
065f8494cf fix(cli): Get Redis password from secret in loadClusters() (#18951) (#18955)
* Get Redis password from secret in `loadClusters()`



* feat: support redis password in admin stats command



* Simplify code



---------

Signed-off-by: David Wu <155603967+david-wu-octopus@users.noreply.github.com>
Signed-off-by: pashakostohrys <pavel@codefresh.io>
Co-authored-by: david-wu-octopus <155603967+david-wu-octopus@users.noreply.github.com>
Co-authored-by: pashakostohrys <pavel@codefresh.io>
2024-07-05 11:12:24 -04:00
gcp-cherry-pick-bot[bot]
beacacc43d fix(appset): missing permissions (#18829) (#18943) (#18944) 2024-07-04 10:44:58 -04:00
gcp-cherry-pick-bot[bot]
81d454215d remove unwanted updating of source-position in app set command (#18887) (#18897)
Signed-off-by: ishitasequeira <ishiseq29@gmail.com>
Co-authored-by: Ishita Sequeira <46771830+ishitasequeira@users.noreply.github.com>
2024-07-02 13:14:21 -04:00
github-actions[bot]
1237d4ea17 Bump version to 2.12.0-rc3 (#18893)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: crenshaw-dev <350466+crenshaw-dev@users.noreply.github.com>
2024-07-02 13:04:14 -04:00
Michael Crenshaw
b211d3e038 chore: bump gitops-engine (#18871)
Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
2024-07-01 13:22:54 -04:00
gcp-cherry-pick-bot[bot]
50c32b53f0 docs: Fix .path to .path.segments go template (#18872) (#18873)
Signed-off-by: Jaeseok Lee <devsunb@gmail.com>
Co-authored-by: Jaeseok Lee <devsunb@gmail.com>
2024-07-01 10:52:36 -04:00
gcp-cherry-pick-bot[bot]
444d332d0a fix: Handle nil health check in post-delete hooks (#18270) (#18767) (#18828)
* fix: Handle nil health check in post-delete hooks



* test: Validate deletion of RBAC resources for post-delete hook



* Update appcontroller_test.go



---------

Signed-off-by: Yonatan Sasson <yonatanxd7@gmail.com>
Signed-off-by: Yonatan Sasson <107778824+Yuni-sa@users.noreply.github.com>
Co-authored-by: Yonatan Sasson <107778824+Yuni-sa@users.noreply.github.com>
2024-06-26 17:21:19 +03:00
gcp-cherry-pick-bot[bot]
573c771d2b fix(webhook): bitbucket and azure not triggering refresh (#18289) (#18765) (#18818)
* fix(webhook): bitbucket and azure webhook not triggering refresh



* update unit test



* fix merge



* adjust logic for reposerver using ls-remote



---------

Signed-off-by: Alexandre Gaudreault <alexandre_gaudreault@intuit.com>
Co-authored-by: Alexandre Gaudreault <alexandre_gaudreault@intuit.com>
2024-06-26 08:48:16 -04:00
Michael Crenshaw
e616dff2d1 fix(appset): revert "keep reconciling even when params error occurred" (#17062) (#18781)
This reverts commit 86369ca71d.

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
2024-06-25 15:33:59 -04:00
gcp-cherry-pick-bot[bot]
e36e19de4e chore(deps): bump github.com/hashicorp/go-retryablehttp (#18805) (#18811)
Bumps [github.com/hashicorp/go-retryablehttp](https://github.com/hashicorp/go-retryablehttp) from 0.7.4 to 0.7.7.
- [Changelog](https://github.com/hashicorp/go-retryablehttp/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/go-retryablehttp/compare/v0.7.4...v0.7.7)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/go-retryablehttp
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-25 11:29:12 -04:00
github-actions[bot]
bbfa79ad9e Bump version to 2.12.0-rc2 (#18802)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: crenshaw-dev <350466+crenshaw-dev@users.noreply.github.com>
2024-06-24 16:31:31 -04:00
gcp-cherry-pick-bot[bot]
00fc93b8b2 fix: Bug in edit support in Sources tab; Input to loader (#17588) (#18800) (#18801)
Co-authored-by: Keith Chong <kykchong@redhat.com>
2024-06-24 14:02:06 -04:00
gcp-cherry-pick-bot[bot]
16c20a84b8 fix(server): could not find source for metadata revision (#18744) (#18763) (#18782)
* fix(server): could not find source for metadata revision (#18744)



* lint



* the linter behaves poorly



* fix test



* share logic with chart endpoint



* more intuitive check



* remove debug line



---------

Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
Co-authored-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
2024-06-24 09:55:56 -04:00
gcp-cherry-pick-bot[bot]
2a9a62eeb7 fix(ui): set project to empty string if undefined (#18732) (#18733)
There are some situations where the project will be `undefined`. When
that happens, attempting to delete a repo won't be possible, since
the backend will be looking for a project with the literal name
`undefined`. To fix this, set an empty string for `undefined|null`
values.

Signed-off-by: Blake Pettersson <blake.pettersson@gmail.com>
Co-authored-by: Blake Pettersson <blake.pettersson@gmail.com>
2024-06-20 12:00:39 +03:00
github-actions[bot]
fe60670885 Bump version to 2.12.0-rc1 (#18716)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: ishitasequeira <46771830+ishitasequeira@users.noreply.github.com>
2024-06-18 09:09:23 -04:00
2304 changed files with 97547 additions and 274680 deletions

18
.gitattributes vendored
View File

@@ -1,18 +0,0 @@
**/*.pb.go linguist-generated=true
**/mocks/*.go linguist-generated=true
assets/swagger.json linguist-generated=true
docs/operator-manual/resource_actions_builtin.md linguist-generated=true
docs/operator-manual/server-commands/argocd-*.md linguist-generated=true
docs/user-guide/commands/argocd_*.md linguist-generated=true
manifests/core-install.yaml linguist-generated=true
manifests/core-install-with-hydrator.yaml linguist-generated=true
manifests/crds/*-crd.yaml linguist-generated=true
manifests/ha/install.yaml linguist-generated=true
manifests/ha/install-with-hydrator.yaml linguist-generated=true
manifests/ha/namespace-install.yaml linguist-generated=true
manifests/ha/namespace-install-with-hydrator.yaml linguist-generated=true
manifests/install.yaml linguist-generated=true
manifests/install-with-hydrator.yaml linguist-generated=true
manifests/namespace-install.yaml linguist-generated=true
manifests/namespace-install-with-hydrator.yaml linguist-generated=true
pkg/apis/api-rules/violation_exceptions.list linguist-generated=true

View File

@@ -1,43 +0,0 @@
---
name: New Dev Tool Request
about: This is a request for adding a new tool for setting up a dev environment.
title: ''
labels: ''
assignees: ''
---
Checklist:
* [ ] I am willing to maintain this tool, or have another Argo CD maintainer who is.
* [ ] I have another Argo CD maintainer who is willing to help maintain this tool (there needs to be at least two maintainers willing to maintain this tool)
* [ ] I have a lead sponsor who is a core Argo CD maintainer
* [ ] There is a PR which adds said tool - this is so that the maintainers can assess the impact of having this in the tree
* [ ] I have given a motivation why this should be added
### The proposer
<-- The username(s) of the person(s) proposing the tool -->
### The proposed tool
<!-- The tool itself, with a link to the tools website -->
### Motivation
<!-- Why this tool would be useful to have in the tree. -->
### Link to PR (Optional)
<!-- A PR adding the tool to the tree -->
### Lead Sponsor(s)
Final approval requires sponsorship from at least one core maintainer.
- @<sponsor-1>
### Co-sponsors
These will be the co-maintainers of the specified tool.
- @<sponsor-1>

View File

@@ -4,13 +4,8 @@ updates:
directory: "/"
schedule:
interval: "daily"
open-pull-requests-limit: 20
ignore:
- dependency-name: k8s.io/*
groups:
otel:
patterns:
- "go.opentelemetry.io/*"
- package-ecosystem: "github-actions"
directory: "/"
@@ -22,21 +17,15 @@ updates:
schedule:
interval: "daily"
# Disabled since this code is rarely used.
# - package-ecosystem: "npm"
# directory: "/ui-test/"
# schedule:
# interval: "daily"
- package-ecosystem: "npm"
directory: "/ui-test/"
schedule:
interval: "daily"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "daily"
ignore:
# We use consistent go and node versions across a lot of different files, and updating via dependabot would cause
# drift among those files, instead we let renovate bot handle them.
- dependency-name: "library/golang"
- dependency-name: "library/node"
- package-ecosystem: "docker"
directory: "/test/container/"
@@ -53,8 +42,7 @@ updates:
schedule:
interval: "daily"
# Disabled since this code is rarely used.
# - package-ecosystem: "docker"
# directory: "/ui-test/"
# schedule:
# interval: "daily"
- package-ecosystem: "docker"
directory: "/ui-test/"
schedule:
interval: "daily"

View File

@@ -16,7 +16,7 @@
## 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
- 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

View File

@@ -1,89 +0,0 @@
name: Bump major version
on:
workflow_dispatch: {}
permissions: {}
jobs:
prepare-release:
permissions:
contents: write # for peter-evans/create-pull-request to create branch
pull-requests: write # for peter-evans/create-pull-request to create a PR
name: Automatically update major version
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
# Get the current major version from go.mod and save it as a variable.
- name: Get target version
id: get-target-version
run: |
set -ue
CURRENT_VERSION=$(grep 'module github.com/argoproj/argo-cd' go.mod | awk '{print $2}' | sed 's/.*\/v//')
echo "TARGET_VERSION=$((CURRENT_VERSION + 1))" >> $GITHUB_OUTPUT
- name: Copy source code to GOPATH
run: |
mkdir -p ~/go/src/github.com/argoproj
cp -a ../argo-cd ~/go/src/github.com/argoproj
- name: Run script to bump the version
run: |
hack/bump-major-version.sh
working-directory: /home/runner/go/src/github.com/argoproj/argo-cd
- name: Setup Golang
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
go-version: ${{ env.GOLANG_VERSION }}
- name: Add ~/go/bin to PATH
run: |
echo "/home/runner/go/bin" >> $GITHUB_PATH
- name: Add /usr/local/bin to PATH
run: |
echo "/usr/local/bin" >> $GITHUB_PATH
- name: Download & vendor dependencies
run: |
# We need to vendor go modules for codegen yet
go mod download
go mod vendor -v
working-directory: /home/runner/go/src/github.com/argoproj/argo-cd
- name: Install toolchain for codegen
run: |
make install-codegen-tools-local
make install-go-tools-local
working-directory: /home/runner/go/src/github.com/argoproj/argo-cd
# We install kustomize in the dist directory
- name: Add dist to PATH
run: |
echo "/home/runner/work/argo-cd/argo-cd/dist" >> $GITHUB_PATH
- name: Run codegen
run: |
set -x
export GOPATH=$(go env GOPATH)
make codegen-local
working-directory: /home/runner/go/src/github.com/argoproj/argo-cd
- name: Copy changes back
run: |
# Copy the contents back, but skip the .git directory
rsync -a --exclude=.git /home/runner/go/src/github.com/argoproj/argo-cd/ ../argo-cd
- name: Create pull request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
commit-message: "Bump major version to ${{ steps.get-target-version.outputs.TARGET_VERSION }}"
title: "Bump major version to ${{ steps.get-target-version.outputs.TARGET_VERSION }}"
body: |
Congrats! You've just bumped the major version to ${{ steps.get-target-version.outputs.TARGET_VERSION }}.
Next steps:
- [ ] Merge this PR
- [ ] Add an upgrade guide to the docs for this version
branch: bump-major-version
branch-suffix: random
signoff: true

View File

@@ -13,12 +13,11 @@ on:
env:
# Golang version to use across CI steps
# renovate: datasource=golang-version packageName=golang
GOLANG_VERSION: '1.24.4'
GOLANG_VERSION: '1.22'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
cancel-in-progress: true
permissions:
contents: read
@@ -32,14 +31,14 @@ jobs:
docs: ${{ steps.filter.outputs.docs_any_changed }}
steps:
- uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
- uses: tj-actions/changed-files@ed68ef82c095e0d48ec87eccea555d944a631a4c # v46.0.5
- uses: tj-actions/changed-files@d6babd6899969df1a11d14c368283ea4436bca78 # v44.5.2
id: filter
with:
# Any file which is not under docs/, ui/ or is not a markdown file is counted as a backend file
files_yaml: |
backend:
- '!ui/**'
- '!**.md'
- '!**.md'
- '!**/*.md'
- '!docs/**'
frontend:
@@ -57,7 +56,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
- name: Setup Golang
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version: ${{ env.GOLANG_VERSION }}
- name: Download all Go modules
@@ -78,11 +77,11 @@ jobs:
- name: Checkout code
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
- name: Setup Golang
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version: ${{ env.GOLANG_VERSION }}
- name: Restore go build cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: ~/.cache/go-build
key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
@@ -94,8 +93,8 @@ jobs:
lint-go:
permissions:
contents: read # for actions/checkout to fetch code
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
contents: read # for actions/checkout to fetch code
pull-requests: read # for golangci/golangci-lint-action to fetch pull requests
name: Lint Go code
if: ${{ needs.changes.outputs.backend == 'true' }}
runs-on: ubuntu-22.04
@@ -105,14 +104,13 @@ jobs:
- name: Checkout code
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
- name: Setup Golang
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version: ${{ env.GOLANG_VERSION }}
- name: Run golangci-lint
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
uses: golangci/golangci-lint-action@a4f60bb28d35aeee14e6880718e0c85ff1882e64 # v6.0.1
with:
# renovate: datasource=go packageName=github.com/golangci/golangci-lint versioning=regex:^v(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)?$
version: v2.1.6
version: v1.58.2
args: --verbose
test-go:
@@ -133,7 +131,7 @@ jobs:
- name: Create symlink in GOPATH
run: ln -s $(pwd) ~/go/src/github.com/argoproj/argo-cd
- name: Setup Golang
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version: ${{ env.GOLANG_VERSION }}
- name: Install required packages
@@ -153,7 +151,7 @@ jobs:
run: |
echo "/usr/local/bin" >> $GITHUB_PATH
- name: Restore go build cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: ~/.cache/go-build
key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
@@ -174,7 +172,7 @@ jobs:
- name: Run all unit tests
run: make test-local
- name: Generate test results artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: test-results
path: test-results
@@ -197,7 +195,7 @@ jobs:
- name: Create symlink in GOPATH
run: ln -s $(pwd) ~/go/src/github.com/argoproj/argo-cd
- name: Setup Golang
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version: ${{ env.GOLANG_VERSION }}
- name: Install required packages
@@ -217,7 +215,7 @@ jobs:
run: |
echo "/usr/local/bin" >> $GITHUB_PATH
- name: Restore go build cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: ~/.cache/go-build
key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
@@ -238,7 +236,7 @@ jobs:
- name: Run all unit tests
run: make test-race-local
- name: Generate test results artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: race-results
path: test-results/
@@ -253,7 +251,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
- name: Setup Golang
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version: ${{ env.GOLANG_VERSION }}
- name: Create symlink in GOPATH
@@ -305,13 +303,12 @@ jobs:
- name: Checkout code
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
- name: Setup NodeJS
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2
with:
# renovate: datasource=node-version packageName=node versioning=node
node-version: '22.9.0'
node-version: '21.6.1'
- name: Restore node dependency cache
id: cache-dependencies
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: ui/node_modules
key: ${{ runner.os }}-node-dep-v2-${{ hashFiles('**/yarn.lock') }}
@@ -326,22 +323,11 @@ jobs:
NODE_ENV: production
NODE_ONLINE_ENV: online
HOST_ARCH: amd64
# If we're on the master branch, set the codecov token so that we upload bundle analysis
CODECOV_TOKEN: ${{ github.ref == 'refs/heads/master' && secrets.CODECOV_TOKEN || '' }}
working-directory: ui/
- name: Run ESLint
run: yarn lint
working-directory: ui/
shellcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
- run: |
sudo apt-get install shellcheck
shellcheck -e SC2086 -e SC2046 -e SC2068 -e SC2206 -e SC2048 -e SC2059 -e SC2154 -e SC2034 -e SC2016 -e SC2128 -e SC1091 -e SC2207 $(find . -type f -name '*.sh') | tee sc.log
test ! -s sc.log
analyze:
name: Process & analyze test artifacts
if: ${{ needs.changes.outputs.backend == 'true' || needs.changes.outputs.frontend == 'true' }}
@@ -360,7 +346,7 @@ jobs:
fetch-depth: 0
- name: Restore node dependency cache
id: cache-dependencies
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: ui/node_modules
key: ${{ runner.os }}-node-dep-v2-${{ hashFiles('**/yarn.lock') }}
@@ -368,41 +354,33 @@ jobs:
run: |
rm -rf ui/node_modules/argo-ui/node_modules
- name: Get e2e code coverage
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
name: e2e-code-coverage
path: e2e-code-coverage
- name: Get unit test code coverage
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
name: test-results
path: test-results
- name: combine-go-coverage
# We generate coverage reports for all Argo CD components, but only the applicationset-controller,
# app-controller, repo-server, and commit-server report contain coverage data. The other components currently
# don't shut down gracefully, so no coverage data is produced. Once those components are fixed, we can add
# references to their coverage output directories.
# We generate coverage reports for all Argo CD components, but only the applicationset-controller report
# contains coverage data. The other components currently don't shut down gracefully, so no coverage data is
# produced. Once those components are fixed, we can add references to their coverage output directories.
run: |
go tool covdata percent -i=test-results,e2e-code-coverage/applicationset-controller,e2e-code-coverage/repo-server,e2e-code-coverage/app-controller,e2e-code-coverage/commit-server -o test-results/full-coverage.out
go tool covdata percent -i=test-results,e2e-code-coverage/applicationset-controller -o test-results/full-coverage.out
- name: Upload code coverage information to codecov.io
uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
with:
files: test-results/full-coverage.out
file: test-results/full-coverage.out
fail_ci_if_error: true
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- name: Upload test results to Codecov
if: github.ref == 'refs/heads/master' && github.event_name == 'push' && github.repository == 'argoproj/argo-cd'
uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1
with:
file: test-results/junit.xml
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
- name: Perform static code analysis using SonarCloud
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
uses: SonarSource/sonarqube-scan-action@2500896589ef8f7247069a56136f8dc177c27ccf # v5.2.0
uses: SonarSource/sonarqube-scan-action@540792c588b5c2740ad2bb4667db5cd46ae678f2 # v2.2
if: env.sonar_secret != ''
test-e2e:
name: Run end-to-end tests
@@ -411,45 +389,37 @@ jobs:
strategy:
fail-fast: false
matrix:
# latest: true means that this version mush upload the coverage report to codecov.io
# We designate the latest version because we only collect code coverage for that version.
k3s:
- version: v1.33.1
- version: v1.29.1
# We designate the latest version because we only collect code coverage for that version.
latest: true
- version: v1.32.1
- version: v1.28.6
latest: false
- version: v1.31.0
- version: v1.27.10
latest: false
- version: v1.30.4
- version: v1.26.13
latest: false
needs:
- build-go
- changes
env:
GOPATH: /home/runner/go
ARGOCD_FAKE_IN_CLUSTER: 'true'
ARGOCD_SSH_DATA_PATH: '/tmp/argo-e2e/app/config/ssh'
ARGOCD_TLS_DATA_PATH: '/tmp/argo-e2e/app/config/tls'
ARGOCD_E2E_SSH_KNOWN_HOSTS: '../fixture/certs/ssh_known_hosts'
ARGOCD_E2E_K3S: 'true'
ARGOCD_IN_CI: 'true'
ARGOCD_E2E_APISERVER_PORT: '8088'
ARGOCD_APPLICATION_NAMESPACES: 'argocd-e2e-external,argocd-e2e-external-2'
ARGOCD_SERVER: '127.0.0.1:8088'
ARGOCD_FAKE_IN_CLUSTER: "true"
ARGOCD_SSH_DATA_PATH: "/tmp/argo-e2e/app/config/ssh"
ARGOCD_TLS_DATA_PATH: "/tmp/argo-e2e/app/config/tls"
ARGOCD_E2E_SSH_KNOWN_HOSTS: "../fixture/certs/ssh_known_hosts"
ARGOCD_E2E_K3S: "true"
ARGOCD_IN_CI: "true"
ARGOCD_E2E_APISERVER_PORT: "8088"
ARGOCD_APPLICATION_NAMESPACES: "argocd-e2e-external,argocd-e2e-external-2"
ARGOCD_SERVER: "127.0.0.1:8088"
GITHUB_TOKEN: ${{ secrets.E2E_TEST_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
GITLAB_TOKEN: ${{ secrets.E2E_TEST_GITLAB_TOKEN }}
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with:
large-packages: false
docker-images: false
swap-storage: false
tool-cache: false
- name: Checkout code
uses: actions/checkout@8410ad0602e1e429cee44a835ae9f77f654a6694 # v4.0.0
- name: Setup Golang
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version: ${{ env.GOLANG_VERSION }}
- name: GH actions workaround - Kill XSP4 process
@@ -468,7 +438,7 @@ jobs:
sudo chmod go-r $HOME/.kube/config
kubectl version
- name: Restore go build cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: ~/.cache/go-build
key: ${{ runner.os }}-go-build-v1-${{ github.run_id }}
@@ -494,9 +464,9 @@ jobs:
git config --global user.email "john.doe@example.com"
- name: Pull Docker image required for tests
run: |
docker pull ghcr.io/dexidp/dex:v2.43.0
docker pull ghcr.io/dexidp/dex:v2.38.0
docker pull argoproj/argo-cd-ci-builder:v1.0.0
docker pull redis:7.2.7-alpine
docker pull redis:7.0.15-alpine
- name: Create target directory for binaries in the build-process
run: |
mkdir -p dist
@@ -526,13 +496,13 @@ jobs:
goreman run stop-all || echo "goreman trouble"
sleep 30
- name: Upload e2e coverage report
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: e2e-code-coverage
path: /tmp/coverage
if: ${{ matrix.k3s.latest }}
- name: Upload e2e-server logs
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: e2e-server-k8s${{ matrix.k3s.version }}.log
path: /tmp/e2e-server.log
@@ -559,4 +529,4 @@ jobs:
exit 0
else
exit 1
fi
fi

View File

@@ -23,7 +23,7 @@ jobs:
actions: read # for github/codeql-action/init to get workflow details
contents: read # for actions/checkout to fetch code
security-events: write # for github/codeql-action/autobuild to send a status report
if: github.repository == 'argoproj/argo-cd' || vars.enable_codeql
if: github.repository == 'argoproj/argo-cd'
# CodeQL runs on ubuntu-latest and windows-latest
runs-on: ubuntu-22.04
@@ -33,7 +33,7 @@ jobs:
# Use correct go version. https://github.com/github/codeql-action/issues/1842#issuecomment-1704398087
- name: Setup Golang
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version-file: go.mod

View File

@@ -17,9 +17,11 @@ on:
platforms:
required: true
type: string
default: linux/amd64
push:
required: true
type: boolean
default: false
target:
required: false
type: string
@@ -67,16 +69,15 @@ jobs:
if: ${{ github.ref_type != 'tag'}}
- name: Setup Golang
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version: ${{ inputs.go-version }}
cache: false
- name: Install cosign
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0
- uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
- uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0
- uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
- name: Setup tags for container image as a CSV type
run: |
@@ -103,7 +104,7 @@ jobs:
echo 'EOF' >> $GITHUB_ENV
- name: Login to Quay.io
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0
with:
registry: quay.io
username: ${{ secrets.quay_username }}
@@ -111,7 +112,7 @@ jobs:
if: ${{ inputs.quay_image_name && inputs.push }}
- name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0
with:
registry: ghcr.io
username: ${{ secrets.ghcr_username }}
@@ -119,7 +120,7 @@ jobs:
if: ${{ inputs.ghcr_image_name && inputs.push }}
- name: Login to dockerhub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@0d4c9c5ea7693da7b068278f7b52bda2a190a446 # v3.2.0
with:
username: ${{ secrets.docker_username }}
password: ${{ secrets.docker_password }}
@@ -142,7 +143,7 @@ jobs:
- name: Build and push container image
id: image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 #v6.18.0
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 #v5.4.0
with:
context: .
platforms: ${{ inputs.platforms }}

View File

@@ -7,7 +7,7 @@ on:
pull_request:
branches:
- master
types: [labeled, unlabeled, opened, synchronize, reopened]
types: [ labeled, unlabeled, opened, synchronize, reopened ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -46,14 +46,13 @@ jobs:
needs: [set-vars]
permissions:
contents: read
packages: write # for pushing packages to GHCR, which is used by cd.apps.argoproj.io to avoid polluting Quay with tags
packages: write # for pushing packages to GHCR, which is used by cd.apps.argoproj.io to avoid polluting Quay with tags
id-token: write # for creating OIDC tokens for signing.
if: ${{ github.repository == 'argoproj/argo-cd' && github.event_name != 'push' }}
uses: ./.github/workflows/image-reuse.yaml
with:
# Note: cannot use env variables to set go-version (https://docs.github.com/en/actions/using-workflows/reusing-workflows#limitations)
# renovate: datasource=golang-version packageName=golang
go-version: 1.24.4
go-version: 1.22
platforms: ${{ needs.set-vars.outputs.platforms }}
push: false
@@ -61,7 +60,7 @@ jobs:
needs: [set-vars]
permissions:
contents: read
packages: write # for pushing packages to GHCR, which is used by cd.apps.argoproj.io to avoid polluting Quay with tags
packages: write # for pushing packages to GHCR, which is used by cd.apps.argoproj.io to avoid polluting Quay with tags
id-token: write # for creating OIDC tokens for signing.
if: ${{ github.repository == 'argoproj/argo-cd' && github.event_name == 'push' }}
uses: ./.github/workflows/image-reuse.yaml
@@ -69,8 +68,7 @@ jobs:
quay_image_name: quay.io/argoproj/argocd:latest
ghcr_image_name: ghcr.io/argoproj/argo-cd/argocd:${{ needs.set-vars.outputs.image-tag }}
# Note: cannot use env variables to set go-version (https://docs.github.com/en/actions/using-workflows/reusing-workflows#limitations)
# renovate: datasource=golang-version packageName=golang
go-version: 1.24.4
go-version: 1.22
platforms: ${{ needs.set-vars.outputs.platforms }}
push: true
secrets:
@@ -88,7 +86,7 @@ jobs:
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' }}
# 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:
image: ghcr.io/argoproj/argo-cd/argocd
digest: ${{ needs.build-and-publish.outputs.image-digest }}
@@ -101,8 +99,8 @@ jobs:
- build-and-publish
- set-vars
permissions:
contents: write # for git to push upgrade commit if not already deployed
packages: write # for pushing packages to GHCR, which is used by cd.apps.argoproj.io to avoid polluting Quay with tags
contents: write # for git to push upgrade commit if not already deployed
packages: write # for pushing packages to GHCR, which is used by cd.apps.argoproj.io to avoid polluting Quay with tags
if: ${{ github.repository == 'argoproj/argo-cd' && github.event_name == 'push' }}
runs-on: ubuntu-22.04
steps:
@@ -116,3 +114,4 @@ jobs:
git config --global user.name 'CI'
git diff --exit-code && echo 'Already deployed' || (git commit -am 'Upgrade argocd to ${{ needs.set-vars.outputs.image-tag }}' && git push)
working-directory: argoproj-deployments/argocd

View File

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

View File

@@ -12,8 +12,8 @@ permissions: {}
# workflow being trigger a number of times. This limits it
# to one run per PR.
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
validate:
@@ -23,7 +23,7 @@ jobs:
name: Validate PR Title
runs-on: ubuntu-latest
steps:
- uses: thehanimo/pr-title-checker@7fbfe05602bdd86f926d3fb3bccb6f3aed43bc70 # v1.4.3
- uses: thehanimo/pr-title-checker@1d8cd483a2b73118406a187f54dca8a9415f1375 # v1.4.2
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
configuration_path: ".github/pr-title-checker-config.json"

View File

@@ -10,8 +10,7 @@ on:
permissions: {}
env:
# renovate: datasource=golang-version packageName=golang
GOLANG_VERSION: '1.24.4' # Note: go-version must also be set in job argocd-image.with.go-version
GOLANG_VERSION: '1.22' # Note: go-version must also be set in job argocd-image.with.go-version
jobs:
argocd-image:
@@ -24,8 +23,7 @@ jobs:
with:
quay_image_name: quay.io/argoproj/argocd:${{ github.ref_name }}
# Note: cannot use env variables to set go-version (https://docs.github.com/en/actions/using-workflows/reusing-workflows#limitations)
# renovate: datasource=golang-version packageName=golang
go-version: 1.24.4
go-version: 1.22
platforms: linux/amd64,linux/arm64,linux/s390x,linux/ppc64le
push: true
secrets:
@@ -33,20 +31,20 @@ jobs:
quay_password: ${{ secrets.RELEASE_QUAY_TOKEN }}
argocd-image-provenance:
needs: [argocd-image]
permissions:
actions: read # for detecting the Github Actions environment.
id-token: write # for creating OIDC tokens for signing.
packages: write # for uploading attestations. (https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#known-issues)
# Must be refernced by a tag. https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#referencing-the-slsa-generator
if: github.repository == 'argoproj/argo-cd'
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
with:
image: quay.io/argoproj/argocd
digest: ${{ needs.argocd-image.outputs.image-digest }}
secrets:
registry-username: ${{ secrets.RELEASE_QUAY_USERNAME }}
registry-password: ${{ secrets.RELEASE_QUAY_TOKEN }}
needs: [argocd-image]
permissions:
actions: read # for detecting the Github Actions environment.
id-token: write # for creating OIDC tokens for signing.
packages: write # for uploading attestations. (https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#known-issues)
# Must be refernced by a tag. https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#referencing-the-slsa-generator
if: github.repository == 'argoproj/argo-cd'
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0
with:
image: quay.io/argoproj/argocd
digest: ${{ needs.argocd-image.outputs.image-digest }}
secrets:
registry-username: ${{ secrets.RELEASE_QUAY_USERNAME }}
registry-password: ${{ secrets.RELEASE_QUAY_TOKEN }}
goreleaser:
needs:
@@ -69,17 +67,19 @@ jobs:
- name: Fetch all tags
run: git fetch --force --tags
- name: Setup Golang
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
with:
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 realease branches.
run: |
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=$GORELEASER_PREVIOUS_TAG" >> $GITHUB_ENV
if echo ${{ github.ref_name }} | grep -E -- '-rc1+$';then
echo "GORELEASER_PREVIOUS_TAG=$(git -c 'versionsort.suffix=-rc' tag --list --sort=version:refname | tail -n 2 | head -n 1)" >> $GITHUB_ENV
else
echo "This is not the first release on the branch, Using GoReleaser defaults"
fi
- name: Setup Golang
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version: ${{ env.GOLANG_VERSION }}
- name: Set environment variables for ldflags
id: set_ldflag
@@ -96,20 +96,20 @@ jobs:
tool-cache: false
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0
uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.0.0
id: run-goreleaser
with:
version: latest
args: release --clean --timeout 55m
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
KUBECTL_VERSION: ${{ env.KUBECTL_VERSION }}
KUBECTL_VERSION: ${{ env.KUBECTL_VERSION }}
GIT_TREE_STATE: ${{ env.GIT_TREE_STATE }}
- name: Generate subject for provenance
id: hash
env:
ARTIFACTS: '${{ steps.run-goreleaser.outputs.artifacts }}'
ARTIFACTS: "${{ steps.run-goreleaser.outputs.artifacts }}"
run: |
set -euo pipefail
@@ -128,10 +128,10 @@ jobs:
contents: write # Needed for release uploads
if: github.repository == 'argoproj/argo-cd'
# Must be refernced by a tag. https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#referencing-the-slsa-generator
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
with:
base64-subjects: '${{ needs.goreleaser.outputs.hashes }}'
provenance-name: 'argocd-cli.intoto.jsonl'
base64-subjects: "${{ needs.goreleaser.outputs.hashes }}"
provenance-name: "argocd-cli.intoto.jsonl"
upload-assets: true
generate-sbom:
@@ -153,10 +153,9 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Golang
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0
uses: actions/setup-go@cdcb36043654635271a94b9a6d1392de5bb323a7 # v5.0.1
with:
go-version: ${{ env.GOLANG_VERSION }}
cache: false
- name: Generate SBOM (spdx)
id: spdx-builder
@@ -167,7 +166,7 @@ jobs:
SIGS_BOM_VERSION: v0.2.1
# comma delimited list of project relative folders to inspect for package
# managers (gomod, yarn, npm).
PROJECT_FOLDERS: '.,./ui'
PROJECT_FOLDERS: ".,./ui"
# full qualified name of the docker image to be inspected
DOCKER_IMAGE: quay.io/argoproj/argocd:${{ github.ref_name }}
run: |
@@ -187,7 +186,7 @@ jobs:
fi
cd /tmp && tar -zcf sbom.tar.gz *.spdx
- name: Generate SBOM hash
shell: bash
id: sbom-hash
@@ -196,15 +195,15 @@ jobs:
# base64 -w0 encodes to base64 and outputs on a single line.
# sha256sum /tmp/sbom.tar.gz ... | base64 -w0
echo "hashes=$(sha256sum /tmp/sbom.tar.gz | base64 -w0)" >> "$GITHUB_OUTPUT"
- name: Upload SBOM
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
uses: softprops/action-gh-release@69320dbe05506a9a39fc8ae11030b214ec2d1f87 # v2.0.5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: |
/tmp/sbom.tar.gz
sbom-provenance:
needs: [generate-sbom]
permissions:
@@ -212,13 +211,13 @@ jobs:
id-token: write # Needed for provenance signing and ID
contents: write # Needed for release uploads
if: github.repository == 'argoproj/argo-cd'
# Must be referenced by a tag. https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/container/README.md#referencing-the-slsa-generator
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
# 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.0.0
with:
base64-subjects: '${{ needs.generate-sbom.outputs.hashes }}'
provenance-name: 'argocd-sbom.intoto.jsonl'
base64-subjects: "${{ needs.generate-sbom.outputs.hashes }}"
provenance-name: "argocd-sbom.intoto.jsonl"
upload-assets: true
post-release:
needs:
- argocd-image
@@ -292,14 +291,14 @@ jobs:
# Replace the 'project-release: vX.X.X-rcX' line in SECURITY-INSIGHTS.yml
sed -i "s/project-release: v.*$/project-release: v${{ env.NEW_VERSION }}/" SECURITY-INSIGHTS.yml
# Update the 'commit-hash: XXXXXXX' line in SECURITY-INSIGHTS.yml
sed -i "s/commit-hash: .*/commit-hash: ${{ env.COMMIT_HASH }}/" SECURITY-INSIGHTS.yml
sed -i "s/commit-hash: .*/commit-hash: ${{ env.NEW_VERSION }}/" SECURITY-INSIGHTS.yml
if: ${{ env.UPDATE_VERSION == 'true' }}
- 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@6d6857d36972b65feb161a90e484f2984215f83e # v6.0.5
with:
commit-message: Bump version in master
title: 'chore: Bump version in master'
title: "chore: Bump version in master"
body: All images built from master should indicate which version we are on track for.
signoff: true
branch: update-version

View File

@@ -35,7 +35,7 @@ jobs:
persist-credentials: false
- name: "Run analysis"
uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2
uses: ossf/scorecard-action@dc50aa9510b46c811795eb24b2f1ba02a914e534 # v2.3.3
with:
results_file: results.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
# format to the repository Actions tab.
- name: "Upload artifact"
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: SARIF file
path: results.sarif

5
.gitignore vendored
View File

@@ -1,14 +1,12 @@
.vscode/
.idea/
.DS_Store
.run/
vendor/
dist/*
ui/dist/app/*
!ui/dist/app/gitkeep
site/
*.iml
.tilt-bin/
# delve debug binaries
cmd/**/debug
debug.test
@@ -28,6 +26,3 @@ cmd/argocd/argocd
cmd/argocd-application-controller/argocd-application-controller
cmd/argocd-repo-server/argocd-repo-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:8dd34e72ae5b9e6f60d267dd6287befc2cf5ad1a11c64e9d93daa60c952a2154
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,253 +1,43 @@
formatters:
enable:
- gofumpt
- goimports
settings:
goimports:
local-prefixes:
- github.com/argoproj/argo-cd/v3
issues:
exclude:
- SA1019
- SA5011
max-issues-per-linter: 0
max-same-issues: 0
linters:
enable:
- errcheck
- errorlint
- exptostd
- gocritic
- gomodguard
- gofumpt
- goimports
- gosimple
- govet
- importas
- ineffassign
- misspell
- perfsprint
- revive
- staticcheck
- testifylint
- thelper
- tparallel
- unparam
- usestdlibvars
- usetesting
- whitespace
exclusions:
rules:
- linters:
- unparam
path: (.+)_test\.go
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
warn-unused: true
settings:
gocritic:
enable-all: true
# Most of these should probably be enabled one-by-one.
disabled-checks:
- appendAssign
- appendCombine # Leave disabled, multi-line assigns can be more readable.
- assignOp # Leave disabled, assign operations can be more confusing than helpful.
- commentedOutCode
- deferInLoop
- exitAfterDefer
- exposedSyncMutex
- hugeParam
- importShadow
- paramTypeCombine # Leave disabled, there are too many failures to be worth fixing.
- rangeValCopy
- tooManyResultsChecker
- unnamedResult
- whyNoLint
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
govet:
disable:
- fieldalignment
- shadow
enable-all: true
importas:
alias:
- pkg: github.com/golang-jwt/jwt/v5
alias: jwtgo
- pkg: k8s.io/api/apps/v1
alias: appsv1
- pkg: k8s.io/api/core/v1
alias: corev1
- 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"
- unused
- whitespace
linters-settings:
gocritic:
disabled-checks:
- appendAssign
- assignOp # Keep it disabled for readability
- badCond
- commentFormatting
- exitAfterDefer
- ifElseChain
- mapKey
- singleCaseSwitch
- typeSwitchVar
goimports:
local-prefixes: github.com/argoproj/argo-cd/v2
testifylint:
enable-all: true
disable:
- error-is-as
- float-compare
- go-require
run:
timeout: 50m

View File

@@ -16,16 +16,16 @@ builds:
flags:
- -v
ldflags:
- -X github.com/argoproj/argo-cd/v3/common.version={{ .Version }}
- -X github.com/argoproj/argo-cd/v3/common.buildDate={{ .Date }}
- -X github.com/argoproj/argo-cd/v3/common.gitCommit={{ .FullCommit }}
- -X github.com/argoproj/argo-cd/v3/common.gitTreeState={{ .Env.GIT_TREE_STATE }}
- -X github.com/argoproj/argo-cd/v3/common.kubectlVersion={{ .Env.KUBECTL_VERSION }}
- '{{ if or (eq .Runtime.Goos "linux") (eq .Runtime.Goos "windows") }}-extldflags="-static"{{ end }}'
- -X github.com/argoproj/argo-cd/v2/common.version={{ .Version }}
- -X github.com/argoproj/argo-cd/v2/common.buildDate={{ .Date }}
- -X github.com/argoproj/argo-cd/v2/common.gitCommit={{ .FullCommit }}
- -X github.com/argoproj/argo-cd/v2/common.gitTreeState={{ .Env.GIT_TREE_STATE }}
- -X github.com/argoproj/argo-cd/v2/common.kubectlVersion={{ .Env.KUBECTL_VERSION }}
- -extldflags="-static"
goos:
- linux
- windows
- darwin
- windows
goarch:
- amd64
- arm64
@@ -42,23 +42,14 @@ builds:
goarch: ppc64le
- goos: windows
goarch: arm64
overrides:
- goos: darwin
goarch: amd64
env:
- CGO_ENABLED=1
- goos: darwin
goarch: arm64
env:
- CGO_ENABLED=1
archives:
- id: argocd-archive
ids:
- argocd-cli
builds:
- argocd-cli
name_template: |-
{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}
formats: [ binary ]
format: binary
checksum:
name_template: 'cli_checksums.txt'
@@ -88,28 +79,25 @@ 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.
## 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)
## 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.
footer: |
**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>
snapshot: #### To be removed for PR
version_template: '2.6.0'
name_template: "2.6.0"
changelog:
use: github
use:
github
sort: asc
abbrev: 0
groups: # Regex use RE2 syntax as defined here: https://github.com/google/re2/wiki/Syntax.
- title: 'Breaking Changes'
regexp: '^.*?(\([[:word:]]+\))??!:.+$'
order: 0
- title: 'Features'
regexp: '^.*?feat(\([[:word:]]+\))??!?:.+$'
order: 100
@@ -129,4 +117,7 @@ changelog:
- '^test:'
- '^.*?Bump(\([[:word:]]+\))?.+$'
- '^.*?\[Bot\](\([[:word:]]+\))?.+$'
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json

View File

@@ -1,82 +0,0 @@
dir: '{{.InterfaceDir}}/mocks'
structname: '{{.InterfaceName}}'
filename: '{{.InterfaceName}}.go'
pkgname: mocks
template-data:
unroll-variadic: true
packages:
github.com/argoproj/argo-cd/v3/applicationset/generators:
interfaces:
Generator: {}
github.com/argoproj/argo-cd/v3/applicationset/services:
interfaces:
Repos: {}
github.com/argoproj/argo-cd/v3/applicationset/services/scm_provider:
config:
dir: applicationset/services/scm_provider/aws_codecommit/mocks
interfaces:
AWSCodeCommitClient: {}
AWSTaggingClient: {}
github.com/argoproj/argo-cd/v3/applicationset/utils:
interfaces:
Renderer: {}
github.com/argoproj/argo-cd/v3/commitserver/apiclient:
interfaces:
Clientset: {}
CommitServiceClient: {}
github.com/argoproj/argo-cd/v3/commitserver/commit:
interfaces:
RepoClientFactory: {}
github.com/argoproj/argo-cd/v3/controller/cache:
interfaces:
LiveStateCache: {}
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:
interfaces:
RepoServerService_GenerateManifestWithFilesClient: {}
RepoServerServiceClient: {}
github.com/argoproj/argo-cd/v3/server/application:
interfaces:
Broadcaster: {}
github.com/argoproj/argo-cd/v3/server/extension:
interfaces:
ApplicationGetter: {}
ExtensionMetricsRegistry: {}
ProjectGetter: {}
RbacEnforcer: {}
SettingsGetter: {}
UserGetter: {}
github.com/argoproj/argo-cd/v3/util/db:
interfaces:
ArgoDB: {}
github.com/argoproj/argo-cd/v3/util/git:
interfaces:
Client: {}
github.com/argoproj/argo-cd/v3/util/helm:
interfaces:
Client: {}
github.com/argoproj/argo-cd/v3/util/io:
interfaces:
TempPaths: {}
github.com/argoproj/argo-cd/v3/util/notification/argocd:
interfaces:
Service: {}
github.com/argoproj/argo-cd/v3/util/workloadidentity:
interfaces:
TokenProvider: {}
github.com/microsoft/azure-devops-go-api/azuredevops/v7/git:
config:
dir: applicationset/services/scm_provider/azure_devops/git/mocks
interfaces:
Client: {}

View File

@@ -2,11 +2,10 @@ version: 2
formats: all
mkdocs:
fail_on_warning: false
configuration: mkdocs.yml
python:
install:
- requirements: docs/requirements.txt
build:
os: "ubuntu-22.04"
tools:
python: "3.12"
python: "3.7"

View File

@@ -1,12 +1,10 @@
ARG BASE_IMAGE=docker.io/library/ubuntu:24.04@sha256:80dd3c3b9c6cecb9f1667e9290b3bc61b78c2678c02cbdae5f0fea92cc6734ab
ARG BASE_IMAGE=docker.io/library/ubuntu:24.04@sha256:3f85b7caad41a95462cf5b787d8a04604c8262cdcdf9a472b8c52ef83375fe15
####################################################################################################
# Builder image
# Initial stage which pulls prepares build dependencies and CLI tooling we need for our final image
# Also used as the image in CI jobs so needs all dependencies
####################################################################################################
FROM docker.io/library/golang:1.24.4@sha256:db5d0afbfb4ab648af2393b92e87eaae9ad5e01132803d80caef91b5752d289c AS builder
WORKDIR /tmp
FROM docker.io/library/golang:1.22.4@sha256:c2010b9c2342431a24a2e64e33d9eb2e484af49e72c820e200d332d214d5e61f AS builder
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 && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
WORKDIR /tmp
COPY hack/install.sh hack/tool-versions.sh ./
COPY hack/installers installers
@@ -40,8 +40,8 @@ LABEL org.opencontainers.image.source="https://github.com/argoproj/argo-cd"
USER root
ENV ARGOCD_USER_ID=999 \
DEBIAN_FRONTEND=noninteractive
ENV ARGOCD_USER_ID=999
ENV DEBIAN_FRONTEND=noninteractive
RUN groupadd -g $ARGOCD_USER_ID argocd && \
useradd -r -u $ARGOCD_USER_ID -g argocd argocd && \
@@ -55,13 +55,11 @@ RUN groupadd -g $ARGOCD_USER_ID argocd && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
COPY hack/gpg-wrapper.sh \
hack/git-verify-wrapper.sh \
entrypoint.sh \
/usr/local/bin/
COPY hack/gpg-wrapper.sh /usr/local/bin/gpg-wrapper.sh
COPY hack/git-verify-wrapper.sh /usr/local/bin/git-verify-wrapper.sh
COPY --from=builder /usr/local/bin/helm /usr/local/bin/helm
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
RUN ln -s /usr/local/bin/entrypoint.sh /usr/local/bin/uid_entrypoint.sh
@@ -85,7 +83,7 @@ WORKDIR /home/argocd
####################################################################################################
# Argo CD UI stage
####################################################################################################
FROM --platform=$BUILDPLATFORM docker.io/library/node:23.0.0@sha256:e643c0b70dca9704dff42e12b17f5b719dbe4f95e6392fc2dfa0c5f02ea8044d AS argocd-ui
FROM --platform=$BUILDPLATFORM docker.io/library/node:22.3.0@sha256:5e4044ff6001d06e7748e35bfa4f80c73cf5f5a7360a1b782995e038a01b0585 AS argocd-ui
WORKDIR /src
COPY ["ui/package.json", "ui/yarn.lock", "./"]
@@ -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
####################################################################################################
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.24.4@sha256:db5d0afbfb4ab648af2393b92e87eaae9ad5e01132803d80caef91b5752d289c AS argocd-build
FROM --platform=$BUILDPLATFORM docker.io/library/golang:1.22.4@sha256:c2010b9c2342431a24a2e64e33d9eb2e484af49e72c820e200d332d214d5e61f AS argocd-build
WORKDIR /go/src/github.com/argoproj/argo-cd
@@ -113,13 +111,13 @@ RUN go mod download
# Perform the build
COPY . .
COPY --from=argocd-ui /src/dist/app /go/src/github.com/argoproj/argo-cd/ui/dist/app
ARG TARGETOS \
TARGETARCH
ARG TARGETOS
ARG TARGETARCH
# These build args are optional; if not specified the defaults will be taken from the Makefile
ARG GIT_TAG \
BUILD_DATE \
GIT_TREE_STATE \
GIT_COMMIT
ARG GIT_TAG
ARG BUILD_DATE
ARG GIT_TREE_STATE
ARG GIT_COMMIT
RUN GIT_COMMIT=$GIT_COMMIT \
GIT_TREE_STATE=$GIT_TREE_STATE \
GIT_TAG=$GIT_TAG \
@@ -132,7 +130,6 @@ RUN GIT_COMMIT=$GIT_COMMIT \
# Final image
####################################################################################################
FROM argocd-base
ENTRYPOINT ["/usr/bin/tini", "--"]
COPY --from=argocd-build /go/src/github.com/argoproj/argo-cd/dist/argocd* /usr/local/bin/
USER root
@@ -143,7 +140,7 @@ RUN ln -s /usr/local/bin/argocd /usr/local/bin/argocd-server && \
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-k8s-auth && \
ln -s /usr/local/bin/argocd /usr/local/bin/argocd-commit-server
ln -s /usr/local/bin/argocd /usr/local/bin/argocd-k8s-auth
USER $ARGOCD_USER_ID
ENTRYPOINT ["/usr/bin/tini", "--"]

View File

@@ -1,62 +0,0 @@
FROM docker.io/library/golang:1.24.1@sha256:c5adecdb7b3f8c5ca3c88648a861882849cc8b02fed68ece31e25de88ad13418
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:24
WORKDIR /app/ui
COPY ui /app/ui
RUN yarn install
ENTRYPOINT ["yarn", "start"]

View File

@@ -1,18 +1,9 @@
PACKAGE=github.com/argoproj/argo-cd/v3/common
PACKAGE=github.com/argoproj/argo-cd/v2/common
CURRENT_DIR=$(shell pwd)
DIST_DIR=${CURRENT_DIR}/dist
CLI_NAME=argocd
BIN_NAME=argocd
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}
CGO_FLAG=0
GEN_RESOURCES_CLI_NAME=argocd-resources-gen
@@ -156,11 +147,7 @@ PATH:=$(PATH):$(PWD)/hack
DOCKER_PUSH?=false
IMAGE_NAMESPACE?=
# perform static compilation
DEFAULT_STATIC_BUILD:=true
ifeq ($(IS_DARWIN),true)
DEFAULT_STATIC_BUILD:=false
endif
STATIC_BUILD?=${DEFAULT_STATIC_BUILD}
STATIC_BUILD?=true
# build development images
DEV_IMAGE?=false
ARGOCD_GPG_ENABLED?=true
@@ -205,10 +192,6 @@ endif
.PHONY: all
all: cli image
.PHONY: mockgen
mockgen:
./hack/generate-mock.sh
.PHONY: gogen
gogen:
export GO111MODULE=off
@@ -246,16 +229,13 @@ clientgen:
clidocsgen:
go run tools/cmd-docs/main.go
.PHONY: actionsdocsgen
actionsdocsgen:
hack/generate-actions-list.sh
.PHONY: codegen-local
codegen-local: mod-vendor-local mockgen gogen protogen clientgen openapigen clidocsgen actionsdocsgen manifests-local notification-docs notification-catalog
codegen-local: mod-vendor-local gogen protogen clientgen openapigen clidocsgen manifests-local notification-docs notification-catalog
rm -rf vendor/
.PHONY: codegen-local-fast
codegen-local-fast: mockgen gogen protogen-fast clientgen openapigen clidocsgen manifests-local notification-docs notification-catalog
codegen-local-fast: gogen protogen-fast clientgen openapigen clidocsgen manifests-local notification-docs notification-catalog
.PHONY: codegen
codegen: test-tools-image
@@ -267,7 +247,7 @@ cli: test-tools-image
.PHONY: cli-local
cli-local: clean-debug
CGO_ENABLED=${CGO_FLAG} GODEBUG="tarinsecurepath=0,zipinsecurepath=0" go build -gcflags="all=-N -l" $(COVERAGE_FLAG) -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/${CLI_NAME} ./cmd
CGO_ENABLED=${CGO_FLAG} GODEBUG="tarinsecurepath=0,zipinsecurepath=0" go build $(COVERAGE_FLAG) -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/${CLI_NAME} ./cmd
.PHONY: gen-resources-cli-local
gen-resources-cli-local: clean-debug
@@ -368,6 +348,11 @@ mod-vendor: test-tools-image
mod-vendor-local: mod-download-local
go mod vendor
# Deprecated - replace by install-tools-local
.PHONY: install-lint-tools
install-lint-tools:
./hack/install.sh lint-tools
# Run linter on the code
.PHONY: lint
lint: test-tools-image
@@ -443,7 +428,7 @@ test-e2e:
test-e2e-local: cli-local
# NO_PROXY ensures all tests don't go out through a proxy if one is configured on the test system
export GO111MODULE=off
DIST_DIR=${DIST_DIR} RERUN_FAILS=5 PACKAGES="./test/e2e" ARGOCD_E2E_RECORD=${ARGOCD_E2E_RECORD} ARGOCD_CONFIG_DIR=$(HOME)/.config/argocd-e2e ARGOCD_GPG_ENABLED=true NO_PROXY=* ./hack/test.sh -timeout $(ARGOCD_E2E_TEST_TIMEOUT) -v -args -test.gocoverdir="$(PWD)/test-results"
DIST_DIR=${DIST_DIR} RERUN_FAILS=5 PACKAGES="./test/e2e" ARGOCD_E2E_RECORD=${ARGOCD_E2E_RECORD} ARGOCD_GPG_ENABLED=true NO_PROXY=* ./hack/test.sh -timeout $(ARGOCD_E2E_TEST_TIMEOUT) -v -args -test.gocoverdir="$(PWD)/test-results"
# Spawns a shell in the test server container for debugging purposes
debug-test-server: test-tools-image
@@ -480,7 +465,6 @@ start-e2e-local: mod-vendor-local dep-ui-local cli-local
mkdir -p /tmp/coverage/repo-server
mkdir -p /tmp/coverage/applicationset-controller
mkdir -p /tmp/coverage/notification
mkdir -p /tmp/coverage/commit-server
# set paths for locally managed ssh known hosts and tls certs data
ARGOCD_SSH_DATA_PATH=/tmp/argo-e2e/app/config/ssh \
ARGOCD_TLS_DATA_PATH=/tmp/argo-e2e/app/config/tls \
@@ -495,11 +479,8 @@ start-e2e-local: mod-vendor-local dep-ui-local cli-local
BIN_MODE=$(ARGOCD_BIN_MODE) \
ARGOCD_APPLICATION_NAMESPACES=argocd-e2e-external,argocd-e2e-external-2 \
ARGOCD_APPLICATIONSET_CONTROLLER_NAMESPACES=argocd-e2e-external,argocd-e2e-external-2 \
ARGOCD_APPLICATIONSET_CONTROLLER_TOKENREF_STRICT_MODE=true \
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_HYDRATOR_ENABLED=true \
ARGOCD_CLUSTER_CACHE_EVENTS_PROCESSING_INTERVAL=1ms \
goreman -f $(ARGOCD_PROCFILE) start ${ARGOCD_START}
ls -lrt /tmp/coverage
@@ -565,7 +546,7 @@ build-docs-local:
.PHONY: build-docs
build-docs:
$(DOCKER) run ${MKDOCS_RUN_ARGS} --rm -it -v ${CURRENT_DIR}:/docs -w /docs --entrypoint "" ${MKDOCS_DOCKER_IMAGE} sh -c 'pip install mkdocs; pip install $$(mkdocs get-deps); mkdocs build'
$(DOCKER) run ${MKDOCS_RUN_ARGS} --rm -it -v ${CURRENT_DIR}:/docs -w /docs --entrypoint "" ${MKDOCS_DOCKER_IMAGE} sh -c 'pip install -r docs/requirements.txt; mkdocs build'
.PHONY: serve-docs-local
serve-docs-local:
@@ -573,7 +554,7 @@ serve-docs-local:
.PHONY: serve-docs
serve-docs:
$(DOCKER) run ${MKDOCS_RUN_ARGS} --rm -it -p 8000:8000 -v ${CURRENT_DIR}:/docs -w /docs --entrypoint "" ${MKDOCS_DOCKER_IMAGE} sh -c 'pip install mkdocs; pip install $$(mkdocs get-deps); mkdocs serve -a $$(ip route get 1 | awk '\''{print $$7}'\''):8000'
$(DOCKER) run ${MKDOCS_RUN_ARGS} --rm -it -p 8000:8000 -v ${CURRENT_DIR}:/docs -w /docs --entrypoint "" ${MKDOCS_DOCKER_IMAGE} sh -c 'pip install -r docs/requirements.txt; mkdocs serve -a $$(ip route get 1 | awk '\''{print $$7}'\''):8000'
# Verify that kubectl can connect to your K8s cluster from Docker
.PHONY: verify-kube-connect
@@ -598,7 +579,6 @@ install-test-tools-local:
./hack/install.sh kustomize
./hack/install.sh helm
./hack/install.sh gotestsum
./hack/install.sh oras
# Installs all tools required for running codegen (Linux packages)
.PHONY: install-codegen-tools-local
@@ -609,7 +589,6 @@ install-codegen-tools-local:
.PHONY: install-go-tools-local
install-go-tools-local:
./hack/install.sh codegen-go-tools
./hack/install.sh lint-tools
.PHONY: dep-ui
dep-ui: test-tools-image
@@ -690,6 +669,7 @@ help:
@echo 'debug:'
@echo ' list -- list all make targets'
@echo ' install-tools-local -- install all the tools below'
@echo ' install-lint-tools(-local)'
@echo
@echo 'codegen:'
@echo ' codegen(-local) -- if using -local, run the following targets first'

1
OWNERS
View File

@@ -1,6 +1,5 @@
owners:
- alexmt
- crenshaw-dev
- jessesuen
approvers:

View File

@@ -1,14 +1,12 @@
controller: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "GOCOVERDIR=${ARGOCD_COVERAGE_DIR:-/tmp/coverage/app-controller} HOSTNAME=testappcontroller-1 FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-application-controller $COMMAND --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --commit-server localhost:${ARGOCD_E2E_COMMITSERVER_PORT:-8086} --otlp-address=${ARGOCD_OTLP_ADDRESS} --application-namespaces=${ARGOCD_APPLICATION_NAMESPACES:-''} --server-side-diff-enabled=${ARGOCD_APPLICATION_CONTROLLER_SERVER_SIDE_DIFF:-'false'} --hydrator-enabled=${ARGOCD_HYDRATOR_ENABLED:='false'}"
api-server: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "GOCOVERDIR=${ARGOCD_COVERAGE_DIR:-/tmp/coverage/api-server} FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-server $COMMAND --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --disable-auth=${ARGOCD_E2E_DISABLE_AUTH:-'true'} --insecure --dex-server http://localhost:${ARGOCD_E2E_DEX_PORT:-5556} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --port ${ARGOCD_E2E_APISERVER_PORT:-8080} --otlp-address=${ARGOCD_OTLP_ADDRESS} --application-namespaces=${ARGOCD_APPLICATION_NAMESPACES:-''} --hydrator-enabled=${ARGOCD_HYDRATOR_ENABLED:='false'}"
dex: sh -c "ARGOCD_BINARY_NAME=argocd-dex go run github.com/argoproj/argo-cd/v3/cmd gendexcfg -o `pwd`/dist/dex.yaml && (test -f dist/dex.yaml || { echo 'Failed to generate dex configuration'; exit 1; }) && docker run --rm -p ${ARGOCD_E2E_DEX_PORT:-5556}:${ARGOCD_E2E_DEX_PORT:-5556} -v `pwd`/dist/dex.yaml:/dex.yaml ghcr.io/dexidp/dex:$(grep "image: ghcr.io/dexidp/dex" manifests/base/dex/argocd-dex-server-deployment.yaml | cut -d':' -f3) dex serve /dex.yaml"
redis: hack/start-redis-with-password.sh
controller: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "GOCOVERDIR=${ARGOCD_COVERAGE_DIR:-/tmp/coverage/app-controller} HOSTNAME=testappcontroller-1 FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-application-controller $COMMAND --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --otlp-address=${ARGOCD_OTLP_ADDRESS} --application-namespaces=${ARGOCD_APPLICATION_NAMESPACES:-''} --server-side-diff-enabled=${ARGOCD_APPLICATION_CONTROLLER_SERVER_SIDE_DIFF:-'false'}"
api-server: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "GOCOVERDIR=${ARGOCD_COVERAGE_DIR:-/tmp/coverage/api-server} FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-server $COMMAND --loglevel debug --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --disable-auth=${ARGOCD_E2E_DISABLE_AUTH:-'true'} --insecure --dex-server http://localhost:${ARGOCD_E2E_DEX_PORT:-5556} --repo-server localhost:${ARGOCD_E2E_REPOSERVER_PORT:-8081} --port ${ARGOCD_E2E_APISERVER_PORT:-8080} --otlp-address=${ARGOCD_OTLP_ADDRESS} --application-namespaces=${ARGOCD_APPLICATION_NAMESPACES:-''}"
dex: sh -c "ARGOCD_BINARY_NAME=argocd-dex go run github.com/argoproj/argo-cd/v2/cmd gendexcfg -o `pwd`/dist/dex.yaml && (test -f dist/dex.yaml || { echo 'Failed to generate dex configuration'; exit 1; }) && docker run --rm -p ${ARGOCD_E2E_DEX_PORT:-5556}:${ARGOCD_E2E_DEX_PORT:-5556} -v `pwd`/dist/dex.yaml:/dex.yaml ghcr.io/dexidp/dex:$(grep "image: ghcr.io/dexidp/dex" manifests/base/dex/argocd-dex-server-deployment.yaml | cut -d':' -f3) dex serve /dex.yaml"
redis: bash -c "if [ \"$ARGOCD_REDIS_LOCAL\" = 'true' ]; then redis-server --save '' --appendonly no --port ${ARGOCD_E2E_REDIS_PORT:-6379}; else docker run --rm --name argocd-redis -i -p ${ARGOCD_E2E_REDIS_PORT:-6379}:${ARGOCD_E2E_REDIS_PORT:-6379} docker.io/library/redis:$(grep "image: redis" manifests/base/redis/argocd-redis-deployment.yaml | cut -d':' -f3) --save '' --appendonly no --port ${ARGOCD_E2E_REDIS_PORT:-6379}; fi"
repo-server: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "GOCOVERDIR=${ARGOCD_COVERAGE_DIR:-/tmp/coverage/repo-server} FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_GNUPGHOME=${ARGOCD_GNUPGHOME:-/tmp/argocd-local/gpg/keys} ARGOCD_PLUGINSOCKFILEPATH=${ARGOCD_PLUGINSOCKFILEPATH:-./test/cmp} ARGOCD_GPG_DATA_PATH=${ARGOCD_GPG_DATA_PATH:-/tmp/argocd-local/gpg/source} ARGOCD_TLS_DATA_PATH=${ARGOCD_TLS_DATA_PATH:-/tmp/argocd-local/tls} ARGOCD_SSH_DATA_PATH=${ARGOCD_SSH_DATA_PATH:-/tmp/argocd-local/ssh} ARGOCD_BINARY_NAME=argocd-repo-server ARGOCD_GPG_ENABLED=${ARGOCD_GPG_ENABLED:-false} $COMMAND --loglevel debug --port ${ARGOCD_E2E_REPOSERVER_PORT:-8081} --redis localhost:${ARGOCD_E2E_REDIS_PORT:-6379} --otlp-address=${ARGOCD_OTLP_ADDRESS}"
cmp-server: [ "$ARGOCD_E2E_TEST" = 'true' ] && exit 0 || [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "FORCE_LOG_COLORS=1 ARGOCD_FAKE_IN_CLUSTER=true ARGOCD_BINARY_NAME=argocd-cmp-server ARGOCD_PLUGINSOCKFILEPATH=${ARGOCD_PLUGINSOCKFILEPATH:-./test/cmp} $COMMAND --config-dir-path ./test/cmp --loglevel debug --otlp-address=${ARGOCD_OTLP_ADDRESS}"
commit-server: [ "$BIN_MODE" = 'true' ] && COMMAND=./dist/argocd || COMMAND='go run ./cmd/main.go' && sh -c "GOCOVERDIR=${ARGOCD_COVERAGE_DIR:-/tmp/coverage/commit-server} FORCE_LOG_COLORS=1 ARGOCD_BINARY_NAME=argocd-commit-server $COMMAND --loglevel debug --port ${ARGOCD_E2E_COMMITSERVER_PORT:-8086}"
ui: sh -c 'cd ui && ${ARGOCD_E2E_YARN_CMD:-yarn} start'
git-server: test/fixture/testrepos/start-git.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}
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'}"

View File

@@ -7,7 +7,8 @@
[![Integration tests](https://github.com/argoproj/argo-cd/workflows/Integration%20tests/badge.svg?branch=master)](https://github.com/argoproj/argo-cd/actions?query=workflow%3A%22Integration+tests%22)
[![codecov](https://codecov.io/gh/argoproj/argo-cd/branch/master/graph/badge.svg)](https://codecov.io/gh/argoproj/argo-cd)
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/4486/badge)](https://bestpractices.coreinfrastructure.org/projects/4486)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/argoproj/argo-cd/badge)](https://scorecard.dev/viewer/?uri=github.com/argoproj/argo-cd)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/argoproj/argo-cd/badge)](https://api.securityscorecards.dev/projects/github.com/argoproj/argo-cd)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fargoproj%2Fargo-cd.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fargoproj%2Fargo-cd?ref=badge_shield)
**Social:**
[![Twitter Follow](https://img.shields.io/twitter/follow/argoproj?style=social)](https://twitter.com/argoproj)
@@ -56,7 +57,7 @@ Participation in the Argo CD project is governed by the [CNCF Code of Conduct](h
### Blogs and Presentations
1. [Awesome-Argo: A Curated List of Awesome Projects and Resources Related to Argo](https://github.com/terrytangyuan/awesome-argo)
1. [Unveil the Secret Ingredients of Continuous Delivery at Enterprise Scale with Argo CD](https://akuity.io/blog/secret-ingredients-of-continuous-delivery-at-enterprise-scale-with-argocd/)
1. [Unveil the Secret Ingredients of Continuous Delivery at Enterprise Scale with Argo CD](https://akuity.io/blog/unveil-the-secret-ingredients-of-continuous-delivery-at-enterprise-scale-with-argocd-kubecon-china-2021/)
1. [GitOps Without Pipelines With ArgoCD Image Updater](https://youtu.be/avPUQin9kzU)
1. [Combining Argo CD (GitOps), Crossplane (Control Plane), And KubeVela (OAM)](https://youtu.be/eEcgn_gU3SM)
1. [How to Apply GitOps to Everything - Combining Argo CD and Crossplane](https://youtu.be/yrj4lmScKHQ)

View File

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

282
Tiltfile
View File

@@ -1,282 +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',
)
# 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_amd64', '/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,
)
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

@@ -11,13 +11,10 @@ Currently, the following organizations are **officially** using Argo CD:
1. [7shifts](https://www.7shifts.com/)
1. [Adevinta](https://www.adevinta.com/)
1. [Adfinis](https://adfinis.com)
1. [Adobe](https://www.adobe.com/)
1. [Adventure](https://jp.adventurekk.com/)
1. [Adyen](https://www.adyen.com)
1. [AirQo](https://airqo.net/)
1. [Akuity](https://akuity.io/)
1. [Alarm.com](https://alarm.com/)
1. [Alauda](https://alauda.io/)
1. [Albert Heijn](https://ah.nl/)
1. [Alibaba Group](https://www.alibabagroup.com/)
1. [Allianz Direct](https://www.allianzdirect.de/)
@@ -30,32 +27,25 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Ant Group](https://www.antgroup.com/)
1. [AppDirect](https://www.appdirect.com)
1. [Arctiq Inc.](https://www.arctiq.ca)
1. [Artemis Health by Nomi Health](https://www.artemishealth.com/)
1. [Arturia](https://www.arturia.com)
2. [Arturia](https://www.arturia.com)
1. [ARZ Allgemeines Rechenzentrum GmbH](https://www.arz.at/)
1. [Augury](https://www.augury.com/)
1. [Autodesk](https://www.autodesk.com)
1. [Axians ACSP](https://www.axians.fr)
1. [Axual B.V.](https://axual.com)
1. [Back Market](https://www.backmarket.com)
1. [Bajaj Finserv Health Ltd.](https://www.bajajfinservhealth.in)
1. [Baloise](https://www.baloise.com)
1. [BCDevExchange DevOps Platform](https://bcdevexchange.org/DevOpsPlatform)
1. [Beat](https://thebeat.co/en/)
1. [Beez Innovation Labs](https://www.beezlabs.com/)
1. [Bedag Informatik AG](https://www.bedag.ch/)
1. [Beleza Na Web](https://www.belezanaweb.com.br/)
1. [Believable Bots](https://believablebots.io)
1. [Bayer AG](https://bayer.com)
1. [BigPanda](https://bigpanda.io)
1. [BioBox Analytics](https://biobox.io)
1. [BMW Group](https://www.bmwgroup.com/)
1. [Boozt](https://www.booztgroup.com/)
1. [Bosch](https://www.bosch.com/)
1. [Boticario](https://www.boticario.com.br/)
1. [Broker Consulting, a.s.](https://www.bcas.cz/en/)
1. [Bulder Bank](https://bulderbank.no)
1. [Cabify](https://cabify.com/en)
1. [CAM](https://cam-inc.co.jp)
1. [Camptocamp](https://camptocamp.com)
1. [Candis](https://www.candis.io)
@@ -69,20 +59,16 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Chainnodes](https://chainnodes.org)
1. [Chargetrip](https://chargetrip.com)
1. [Chime](https://www.chime.com)
1. [Chronicle Labs](https://chroniclelabs.org)
1. [Cisco ET&I](https://eti.cisco.com/)
1. [Cloud Posse](https://www.cloudposse.com/)
1. [Cloud Scale](https://cloudscaleinc.com/)
1. [CloudScript](https://www.cloudscript.com.br/)
1. [CloudGeometry](https://www.cloudgeometry.io/)
1. [Cloudmate](https://cloudmt.co.kr/)
1. [Cloudogu](https://cloudogu.com/)
1. [Cobalt](https://www.cobalt.io/)
1. [Codefresh](https://www.codefresh.io/)
1. [Codility](https://www.codility.com/)
1. [Cognizant](https://www.cognizant.com/)
1. [Commonbond](https://commonbond.co/)
1. [Compatio.AI](https://compatio.ai/)
1. [Contlo](https://contlo.com/)
1. [Coralogix](https://coralogix.com/)
1. [Crédit Agricole CIB](https://www.ca-cib.com)
@@ -92,18 +78,13 @@ Currently, the following organizations are **officially** using Argo CD:
1. [D2iQ](https://www.d2iq.com)
1. [DaoCloud](https://daocloud.io/)
1. [Datarisk](https://www.datarisk.io/)
1. [Daydream](https://daydream.ing)
1. [Deloitte](https://www.deloitte.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. [Devtron Labs](https://github.com/devtron-labs/devtron)
1. [DigitalEd](https://www.digitaled.com)
1. [DigitalOcean](https://www.digitalocean.com)
1. [Divar](https://divar.ir)
1. [Divistant](https://divistant.com)
1. [Dott](https://ridedott.com)
1. [Doubble](https://www.doubble.app)
1. [Doximity](https://www.doximity.com/)
1. [EDF Renewables](https://www.edf-re.com/)
1. [edX](https://edx.org)
@@ -115,7 +96,6 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Energisme](https://energisme.com/)
1. [enigmo](https://enigmo.co.jp/)
1. [Envoy](https://envoy.com/)
1. [eSave](https://esave.es/)
1. [Factorial](https://factorialhr.com/)
1. [Farfetch](https://www.farfetch.com)
1. [Faro](https://www.faro.com/)
@@ -128,13 +108,10 @@ Currently, the following organizations are **officially** using Argo CD:
1. [freee](https://corp.freee.co.jp/en/company/)
1. [Freshop, Inc](https://www.freshop.com/)
1. [Future PLC](https://www.futureplc.com/)
1. [Flagler Health](https://www.flaglerhealth.io/)
1. [G DATA CyberDefense AG](https://www.gdata-software.com/)
1. [G-Research](https://www.gresearch.com/teams/open-source-software/)
1. [Garner](https://www.garnercorp.com)
1. [Generali Deutschland AG](https://www.generali.de/)
1. [Gepardec](https://gepardec.com/)
1. [Getir](https://getir.com)
1. [GetYourGuide](https://www.getyourguide.com/)
1. [Gitpod](https://www.gitpod.io)
1. [Gllue](https://gllue.com)
@@ -151,10 +128,8 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Groww](https://groww.in)
1. [Grupo MasMovil](https://grupomasmovil.com/en/)
1. [Handelsbanken](https://www.handelsbanken.se)
1. [Hazelcast](https://hazelcast.com/)
1. [Healy](https://www.healyworld.net)
1. [Helio](https://helio.exchange)
1. [hetao101](https://www.hetao101.com/)
1. [Hetki](https://hetki.ai)
1. [hipages](https://hipages.com.au/)
1. [Hiya](https://hiya.com)
@@ -163,7 +138,6 @@ Currently, the following organizations are **officially** using Argo CD:
1. [IABAI](https://www.iab.ai)
1. [IBM](https://www.ibm.com/)
1. [Ibotta](https://home.ibotta.com)
1. [Icelandair](https://www.icelandair.com)
1. [IFS](https://www.ifs.com)
1. [IITS-Consulting](https://iits-consulting.de)
1. [IllumiDesk](https://www.illumidesk.com)
@@ -172,7 +146,6 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Index Exchange](https://www.indexexchange.com/)
1. [Info Support](https://www.infosupport.com/)
1. [InsideBoard](https://www.insideboard.com)
1. [Instruqt](https://www.instruqt.com)
1. [Intuit](https://www.intuit.com/)
1. [Jellysmack](https://www.jellysmack.com)
1. [Joblift](https://joblift.com/)
@@ -182,7 +155,6 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Karrot](https://www.daangn.com/)
1. [KarrotPay](https://www.daangnpay.com/)
1. [Kasa](https://kasa.co.kr/)
1. [Kave Home](https://kavehome.com)
1. [Keeeb](https://www.keeeb.com/)
1. [KelkooGroup](https://www.kelkoogroup.com)
1. [Keptn](https://keptn.sh)
@@ -195,16 +167,14 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Kurly](https://www.kurly.com/)
1. [Kvist](https://kvistsolutions.com)
1. [Kyriba](https://www.kyriba.com/)
1. [LeFigaro](https://www.lefigaro.fr/)
1. [Lely](https://www.lely.com/)
1. [LexisNexis](https://www.lexisnexis.com/)
1. [Lian Chu Securities](https://lczq.com)
1. [Liatrio](https://www.liatrio.com)
1. [Lightricks](https://www.lightricks.com/)
1. [LINE](https://linecorp.com/en/)
1. [Loom](https://www.loom.com/)
1. [Lucid Motors](https://www.lucidmotors.com/)
1. [Lytt](https://www.lytt.co/)
1. [LY Corporation](https://www.lycorp.co.jp/en/)
1. [Magic Leap](https://www.magicleap.com/)
1. [Majid Al Futtaim](https://www.majidalfuttaim.com/)
1. [Major League Baseball](https://mlb.com)
@@ -226,16 +196,12 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Moengage](https://www.moengage.com/)
1. [Money Forward](https://corp.moneyforward.com/en/)
1. [MOO Print](https://www.moo.com/)
1. [Mozilla](https://www.mozilla.org)
1. [MTN Group](https://www.mtn.com/)
1. [Municipality of The Hague](https://www.denhaag.nl/)
1. [My Job Glasses](https://myjobglasses.com)
1. [Natura &Co](https://naturaeco.com/)
1. [Nethopper](https://nethopper.io)
1. [New Relic](https://newrelic.com/)
1. [Nextbasket](https://nextbasket.com)
1. [Nextdoor](https://nextdoor.com/)
1. [Next Fit Sistemas](https://nextfit.com.br/)
1. [Nikkei](https://www.nikkei.co.jp/nikkeiinfo/en/)
1. [Nitro](https://gonitro.com)
1. [NYCU, CS IT Center](https://it.cs.nycu.edu.tw)
@@ -247,7 +213,6 @@ Currently, the following organizations are **officially** using Argo CD:
1. [omegaUp](https://omegaUp.com)
1. [Omni](https://omni.se/)
1. [Oncourse Home Solutions](https://oncoursehome.com/)
1. [Open Analytics](https://openanalytics.eu)
1. [openEuler](https://openeuler.org)
1. [openGauss](https://opengauss.org/)
1. [OpenGov](https://opengov.com)
@@ -259,7 +224,6 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Optoro](https://www.optoro.com/)
1. [Orbital Insight](https://orbitalinsight.com/)
1. [Oscar Health Insurance](https://hioscar.com/)
1. [Outpost24](https://outpost24.com/)
1. [p3r](https://www.p3r.one/)
1. [Packlink](https://www.packlink.com/)
1. [PagerDuty](https://www.pagerduty.com/)
@@ -267,7 +231,6 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Patreon](https://www.patreon.com/)
1. [PayIt](https://payitgov.com/)
1. [PayPay](https://paypay.ne.jp/)
1. [Paystack](https://paystack.com/)
1. [Peloton Interactive](https://www.onepeloton.com/)
1. [Percona](https://percona.com/)
1. [PGS](https://www.pgs.com)
@@ -279,18 +242,15 @@ Currently, the following organizations are **officially** using Argo CD:
1. [PITS Globale Datenrettungsdienste](https://www.pitsdatenrettung.de/)
1. [Platform9 Systems](https://platform9.com/)
1. [Polarpoint.io](https://polarpoint.io)
1. [Pollinate](https://www.pollinate.global)
1. [PostFinance](https://github.com/postfinance)
1. [Preferred Networks](https://preferred.jp/en/)
1. [Previder BV](https://previder.nl)
1. [Priceline](https://priceline.com)
1. [Procore](https://www.procore.com)
1. [Productboard](https://www.productboard.com/)
1. [Prudential](https://prudential.com.sg)
1. [PT Boer Technology (Btech)](https://btech.id/)
1. [PUBG](https://www.pubg.com)
1. [Puzzle ITC](https://www.puzzle.ch/)
1. [Pvotal Technologies](https://pvotal.tech/)
1. [Qonto](https://qonto.com)
1. [QuintoAndar](https://quintoandar.com.br)
1. [Quipper](https://www.quipper.com/)
@@ -300,7 +260,6 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Redpill Linpro](https://www.redpill-linpro.com/)
1. [Reenigne Cloud](https://reenigne.ca)
1. [reev.com](https://www.reev.com/)
1. [Relex Solutions](https://www.relexsolutions.com/)
1. [RightRev](https://rightrev.com/)
1. [Rijkswaterstaat](https://www.rijkswaterstaat.nl/en)
1. [Rise](https://www.risecard.eu/)
@@ -314,21 +273,16 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Saloodo! GmbH](https://www.saloodo.com)
1. [Sap Labs](http://sap.com)
1. [Sauce Labs](https://saucelabs.com/)
1. [Schneider Electric](https://www.se.com)
1. [Schwarz IT](https://jobs.schwarz/it-mission)
1. [SCRM Lidl International Hub](https://scrm.lidl)
1. [SEEK](https://seek.com.au)
1. [SEKAI](https://www.sekai.io/)
1. [Semgrep](https://semgrep.com)
1. [Shield](https://shield.com)
1. [SI Analytics](https://si-analytics.ai)
1. [Sidewalk Entertainment](https://sidewalkplay.com/)
1. [Skit](https://skit.ai/)
1. [Skribble](https://skribble.com)
1. [Skyscanner](https://www.skyscanner.net/)
1. [Smart Pension](https://www.smartpension.co.uk/)
1. [Smilee.io](https://smilee.io)
1. [Smilegate Stove](https://www.onstove.com/)
1. [Smood.ch](https://www.smood.ch/)
1. [Snapp](https://snapp.ir/)
1. [Snyk](https://snyk.io/)
@@ -345,22 +299,17 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Sumo Logic](https://sumologic.com/)
1. [Sutpc](http://www.sutpc.com/)
1. [Swiss Post](https://github.com/swisspost)
1. [Swissblock Technologies](https://swissblock.net/)
1. [Swisscom](https://www.swisscom.ch)
1. [Swissquote](https://github.com/swissquote)
1. [Syncier](https://syncier.com/)
1. [Synergy](https://synergy.net.au)
1. [Syself](https://syself.com)
1. [T-ROC Global](https://trocglobal.com/)
1. [TableCheck](https://tablecheck.com/)
1. [Tailor Brands](https://www.tailorbrands.com)
1. [Tamkeen Technologies](https://tamkeentech.sa/)
1. [TBC Bank](https://tbcbank.ge/)
1. [Techcombank](https://www.techcombank.com.vn/trang-chu)
1. [Technacy](https://www.technacy.it/)
1. [Telavita](https://www.telavita.com.br/)
1. [Tesla](https://tesla.com/)
1. [TextNow](https://www.textnow.com/)
1. [The Scale Factory](https://www.scalefactory.com/)
1. [ThousandEyes](https://www.thousandeyes.com/)
1. [Ticketmaster](https://ticketmaster.com)
@@ -389,11 +338,9 @@ Currently, the following organizations are **officially** using Argo CD:
1. [Vinted](https://vinted.com/)
1. [Virtuo](https://www.govirtuo.com/)
1. [VISITS Technologies](https://visits.world/en)
1. [Viya](https://viya.me)
1. [Volvo Cars](https://www.volvocars.com/)
1. [Voyager Digital](https://www.investvoyager.com/)
1. [VSHN - The DevOps Company](https://vshn.ch/)
1. [Wakacje.pl](https://www.wakacje.pl/)
1. [Walkbase](https://www.walkbase.com/)
1. [Webstores](https://www.webstores.nl)
1. [Wehkamp](https://www.wehkamp.nl/)
@@ -405,12 +352,9 @@ Currently, the following organizations are **officially** using Argo CD:
1. [WooliesX](https://wooliesx.com.au/)
1. [Woolworths Group](https://www.woolworthsgroup.com.au/)
1. [WSpot](https://www.wspot.com.br/)
1. [X3M ads](https://x3mads.com)
1. [Yieldlab](https://www.yieldlab.de/)
1. [Youverify](https://youverify.co/)
1. [Yubo](https://www.yubo.live/)
1. [Yuno](https://y.uno/)
1. [ZDF](https://www.zdf.de/)
1. [Zimpler](https://www.zimpler.com/)
1. [ZipRecruiter](https://www.ziprecruiter.com/)
1. [ZOZO](https://corp.zozo.com/)

View File

@@ -1 +1 @@
3.1.0
2.12.4

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,6 @@ import (
"context"
"fmt"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
log "github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/types"
@@ -14,8 +12,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"github.com/argoproj/argo-cd/v3/common"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/applicationset/generators"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
// clusterSecretEventHandler is used when watching Secrets to check if they are ArgoCD Cluster Secrets, and if so
@@ -26,31 +24,31 @@ type clusterSecretEventHandler struct {
Client client.Client
}
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.RateLimitingInterface) {
h.queueRelatedAppGenerators(ctx, q, e.Object)
}
func (h *clusterSecretEventHandler) Update(ctx context.Context, e event.UpdateEvent, q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
func (h *clusterSecretEventHandler) Update(ctx context.Context, e event.UpdateEvent, q workqueue.RateLimitingInterface) {
h.queueRelatedAppGenerators(ctx, q, e.ObjectNew)
}
func (h *clusterSecretEventHandler) Delete(ctx context.Context, e event.DeleteEvent, q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
func (h *clusterSecretEventHandler) Delete(ctx context.Context, e event.DeleteEvent, q workqueue.RateLimitingInterface) {
h.queueRelatedAppGenerators(ctx, q, e.Object)
}
func (h *clusterSecretEventHandler) Generic(ctx context.Context, e event.GenericEvent, q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
func (h *clusterSecretEventHandler) Generic(ctx context.Context, e event.GenericEvent, q workqueue.RateLimitingInterface) {
h.queueRelatedAppGenerators(ctx, q, e.Object)
}
// addRateLimitingInterface defines the Add method of workqueue.RateLimitingInterface, allow us to easily mock
// it for testing purposes.
type addRateLimitingInterface[T comparable] interface {
Add(item T)
type addRateLimitingInterface interface {
Add(item interface{})
}
func (h *clusterSecretEventHandler) queueRelatedAppGenerators(ctx context.Context, q addRateLimitingInterface[reconcile.Request], object client.Object) {
func (h *clusterSecretEventHandler) queueRelatedAppGenerators(ctx context.Context, q addRateLimitingInterface, object client.Object) {
// Check for label, lookup all ApplicationSets that might match the cluster, queue them all
if object.GetLabels()[common.LabelKeySecretType] != common.LabelValueSecretTypeCluster {
if object.GetLabels()[generators.ArgoCDSecretTypeLabel] != generators.ArgoCDSecretTypeCluster {
return
}

View File

@@ -1,34 +1,25 @@
package controllers
import (
"context"
"testing"
argocommon "github.com/argoproj/argo-cd/v3/common"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
argov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/applicationset/generators"
argov1alpha1 "github.com/argoproj/argo-cd/v2/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) {
scheme := runtime.NewScheme()
err := argov1alpha1.AddToScheme(scheme)
@@ -47,11 +38,11 @@ func TestClusterEventHandler(t *testing.T) {
name: "no application sets should mean no requests",
items: []argov1alpha1.ApplicationSet{},
secret: corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Namespace: "argocd",
Name: "my-secret",
Labels: map[string]string{
argocommon.LabelKeySecretType: argocommon.LabelValueSecretTypeCluster,
generators.ArgoCDSecretTypeLabel: generators.ArgoCDSecretTypeCluster,
},
},
},
@@ -61,7 +52,7 @@ func TestClusterEventHandler(t *testing.T) {
name: "a cluster generator should produce a request",
items: []argov1alpha1.ApplicationSet{
{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Name: "my-app-set",
Namespace: "argocd",
},
@@ -75,11 +66,11 @@ func TestClusterEventHandler(t *testing.T) {
},
},
secret: corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Namespace: "argocd",
Name: "my-secret",
Labels: map[string]string{
argocommon.LabelKeySecretType: argocommon.LabelValueSecretTypeCluster,
generators.ArgoCDSecretTypeLabel: generators.ArgoCDSecretTypeCluster,
},
},
},
@@ -91,7 +82,7 @@ func TestClusterEventHandler(t *testing.T) {
name: "multiple cluster generators should produce multiple requests",
items: []argov1alpha1.ApplicationSet{
{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Name: "my-app-set",
Namespace: "argocd",
},
@@ -104,7 +95,7 @@ func TestClusterEventHandler(t *testing.T) {
},
},
{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Name: "my-app-set2",
Namespace: "argocd",
},
@@ -118,11 +109,11 @@ func TestClusterEventHandler(t *testing.T) {
},
},
secret: corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Namespace: "argocd",
Name: "my-secret",
Labels: map[string]string{
argocommon.LabelKeySecretType: argocommon.LabelValueSecretTypeCluster,
generators.ArgoCDSecretTypeLabel: generators.ArgoCDSecretTypeCluster,
},
},
},
@@ -135,7 +126,7 @@ func TestClusterEventHandler(t *testing.T) {
name: "non-cluster generator should not match",
items: []argov1alpha1.ApplicationSet{
{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Name: "my-app-set",
Namespace: "another-namespace",
},
@@ -148,7 +139,7 @@ func TestClusterEventHandler(t *testing.T) {
},
},
{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Name: "app-set-non-cluster",
Namespace: "argocd",
},
@@ -162,11 +153,11 @@ func TestClusterEventHandler(t *testing.T) {
},
},
secret: corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Namespace: "argocd",
Name: "my-secret",
Labels: map[string]string{
argocommon.LabelKeySecretType: argocommon.LabelValueSecretTypeCluster,
generators.ArgoCDSecretTypeLabel: generators.ArgoCDSecretTypeCluster,
},
},
},
@@ -178,7 +169,7 @@ func TestClusterEventHandler(t *testing.T) {
name: "non-argo cd secret should not match",
items: []argov1alpha1.ApplicationSet{
{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Name: "my-app-set",
Namespace: "another-namespace",
},
@@ -192,7 +183,7 @@ func TestClusterEventHandler(t *testing.T) {
},
},
secret: corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Namespace: "argocd",
Name: "my-non-argocd-secret",
},
@@ -203,7 +194,7 @@ func TestClusterEventHandler(t *testing.T) {
name: "a matrix generator with a cluster generator should produce a request",
items: []argov1alpha1.ApplicationSet{
{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Name: "my-app-set",
Namespace: "argocd",
},
@@ -223,11 +214,11 @@ func TestClusterEventHandler(t *testing.T) {
},
},
secret: corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Namespace: "argocd",
Name: "my-secret",
Labels: map[string]string{
argocommon.LabelKeySecretType: argocommon.LabelValueSecretTypeCluster,
generators.ArgoCDSecretTypeLabel: generators.ArgoCDSecretTypeCluster,
},
},
},
@@ -239,7 +230,7 @@ func TestClusterEventHandler(t *testing.T) {
name: "a matrix generator with non cluster generator should not match",
items: []argov1alpha1.ApplicationSet{
{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Name: "my-app-set",
Namespace: "argocd",
},
@@ -259,11 +250,11 @@ func TestClusterEventHandler(t *testing.T) {
},
},
secret: corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Namespace: "argocd",
Name: "my-secret",
Labels: map[string]string{
argocommon.LabelKeySecretType: argocommon.LabelValueSecretTypeCluster,
generators.ArgoCDSecretTypeLabel: generators.ArgoCDSecretTypeCluster,
},
},
},
@@ -273,7 +264,7 @@ func TestClusterEventHandler(t *testing.T) {
name: "a matrix generator with a nested matrix generator containing a cluster generator should produce a request",
items: []argov1alpha1.ApplicationSet{
{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Name: "my-app-set",
Namespace: "argocd",
},
@@ -309,11 +300,11 @@ func TestClusterEventHandler(t *testing.T) {
},
},
secret: corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Namespace: "argocd",
Name: "my-secret",
Labels: map[string]string{
argocommon.LabelKeySecretType: argocommon.LabelValueSecretTypeCluster,
generators.ArgoCDSecretTypeLabel: generators.ArgoCDSecretTypeCluster,
},
},
},
@@ -325,7 +316,7 @@ func TestClusterEventHandler(t *testing.T) {
name: "a matrix generator with a nested matrix generator containing non cluster generator should not match",
items: []argov1alpha1.ApplicationSet{
{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Name: "my-app-set",
Namespace: "argocd",
},
@@ -360,11 +351,11 @@ func TestClusterEventHandler(t *testing.T) {
},
},
secret: corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Namespace: "argocd",
Name: "my-secret",
Labels: map[string]string{
argocommon.LabelKeySecretType: argocommon.LabelValueSecretTypeCluster,
generators.ArgoCDSecretTypeLabel: generators.ArgoCDSecretTypeCluster,
},
},
},
@@ -374,7 +365,7 @@ func TestClusterEventHandler(t *testing.T) {
name: "a merge generator with a cluster generator should produce a request",
items: []argov1alpha1.ApplicationSet{
{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Name: "my-app-set",
Namespace: "argocd",
},
@@ -394,11 +385,11 @@ func TestClusterEventHandler(t *testing.T) {
},
},
secret: corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Namespace: "argocd",
Name: "my-secret",
Labels: map[string]string{
argocommon.LabelKeySecretType: argocommon.LabelValueSecretTypeCluster,
generators.ArgoCDSecretTypeLabel: generators.ArgoCDSecretTypeCluster,
},
},
},
@@ -410,7 +401,7 @@ func TestClusterEventHandler(t *testing.T) {
name: "a matrix generator with non cluster generator should not match",
items: []argov1alpha1.ApplicationSet{
{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Name: "my-app-set",
Namespace: "argocd",
},
@@ -430,11 +421,11 @@ func TestClusterEventHandler(t *testing.T) {
},
},
secret: corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Namespace: "argocd",
Name: "my-secret",
Labels: map[string]string{
argocommon.LabelKeySecretType: argocommon.LabelValueSecretTypeCluster,
generators.ArgoCDSecretTypeLabel: generators.ArgoCDSecretTypeCluster,
},
},
},
@@ -444,7 +435,7 @@ func TestClusterEventHandler(t *testing.T) {
name: "a merge generator with a nested merge generator containing a cluster generator should produce a request",
items: []argov1alpha1.ApplicationSet{
{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Name: "my-app-set",
Namespace: "argocd",
},
@@ -480,11 +471,11 @@ func TestClusterEventHandler(t *testing.T) {
},
},
secret: corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Namespace: "argocd",
Name: "my-secret",
Labels: map[string]string{
argocommon.LabelKeySecretType: argocommon.LabelValueSecretTypeCluster,
generators.ArgoCDSecretTypeLabel: generators.ArgoCDSecretTypeCluster,
},
},
},
@@ -496,7 +487,7 @@ func TestClusterEventHandler(t *testing.T) {
name: "a merge generator with a nested merge generator containing non cluster generator should not match",
items: []argov1alpha1.ApplicationSet{
{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Name: "my-app-set",
Namespace: "argocd",
},
@@ -531,11 +522,11 @@ func TestClusterEventHandler(t *testing.T) {
},
},
secret: corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
ObjectMeta: v1.ObjectMeta{
Namespace: "argocd",
Name: "my-secret",
Labels: map[string]string{
argocommon.LabelKeySecretType: argocommon.LabelValueSecretTypeCluster,
generators.ArgoCDSecretTypeLabel: generators.ArgoCDSecretTypeCluster,
},
},
},
@@ -558,13 +549,28 @@ func TestClusterEventHandler(t *testing.T) {
mockAddRateLimitingInterface := mockAddRateLimitingInterface{}
handler.queueRelatedAppGenerators(t.Context(), &mockAddRateLimitingInterface, &test.secret)
handler.queueRelatedAppGenerators(context.Background(), &mockAddRateLimitingInterface, &test.secret)
assert.False(t, mockAddRateLimitingInterface.errorOccurred)
assert.ElementsMatch(t, mockAddRateLimitingInterface.addedItems, test.expectedRequests)
})
}
}
// Add checks the type, and adds it to the internal list of received additions
func (obj *mockAddRateLimitingInterface) Add(item interface{}) {
if req, ok := item.(ctrl.Request); ok {
obj.addedItems = append(obj.addedItems, req)
} else {
obj.errorOccurred = true
}
}
type mockAddRateLimitingInterface struct {
errorOccurred bool
addedItems []ctrl.Request
}
func TestNestedGeneratorHasClusterGenerator_NestedClusterGenerator(t *testing.T) {
nested := argov1alpha1.ApplicationSetNestedGenerator{
Clusters: &argov1alpha1.ClusterGenerator{},

View File

@@ -1,6 +1,7 @@
package controllers
import (
"context"
"testing"
"time"
@@ -15,15 +16,14 @@ import (
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/argoproj/argo-cd/v3/applicationset/generators"
appsetmetrics "github.com/argoproj/argo-cd/v3/applicationset/metrics"
"github.com/argoproj/argo-cd/v3/applicationset/services/mocks"
argov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/applicationset/generators"
"github.com/argoproj/argo-cd/v2/applicationset/services/mocks"
argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
func TestRequeueAfter(t *testing.T) {
mockServer := &mocks.Repos{}
ctx := t.Context()
ctx := context.Background()
scheme := runtime.NewScheme()
err := argov1alpha1.AddToScheme(scheme)
require.NoError(t, err)
@@ -35,20 +35,20 @@ func TestRequeueAfter(t *testing.T) {
appClientset := kubefake.NewSimpleClientset()
k8sClient := fake.NewClientBuilder().Build()
duckType := &unstructured.Unstructured{
Object: map[string]any{
Object: map[string]interface{}{
"apiVersion": "v2quack",
"kind": "Duck",
"metadata": map[string]any{
"metadata": map[string]interface{}{
"name": "mightyduck",
"namespace": "namespace",
"labels": map[string]any{"duck": "all-species"},
"labels": map[string]interface{}{"duck": "all-species"},
},
"status": map[string]any{
"decisions": []any{
map[string]any{
"status": map[string]interface{}{
"decisions": []interface{}{
map[string]interface{}{
"clusterName": "staging-01",
},
map[string]any{
map[string]interface{}{
"clusterName": "production-01",
},
},
@@ -56,14 +56,14 @@ func TestRequeueAfter(t *testing.T) {
},
}
fakeDynClient := dynfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), gvrToListKind, duckType)
scmConfig := generators.NewSCMConfig("", []string{""}, true, true, nil, true)
terminalGenerators := map[string]generators.Generator{
"List": generators.NewListGenerator(),
"Clusters": generators.NewClusterGenerator(ctx, k8sClient, appClientset, "argocd"),
"Clusters": generators.NewClusterGenerator(k8sClient, ctx, appClientset, "argocd"),
"Git": generators.NewGitGenerator(mockServer, "namespace"),
"SCMProvider": generators.NewSCMProviderGenerator(fake.NewClientBuilder().WithObjects(&corev1.Secret{}).Build(), scmConfig),
"SCMProvider": generators.NewSCMProviderGenerator(fake.NewClientBuilder().WithObjects(&corev1.Secret{}).Build(), generators.SCMAuthProviders{}, "", []string{""}, true),
"ClusterDecisionResource": generators.NewDuckTypeGenerator(ctx, fakeDynClient, appClientset, "argocd"),
"PullRequest": generators.NewPullRequestGenerator(k8sClient, scmConfig),
"PullRequest": generators.NewPullRequestGenerator(k8sClient, generators.SCMAuthProviders{}, "", []string{""}, true),
}
nestedGenerators := map[string]generators.Generator{
@@ -89,18 +89,15 @@ func TestRequeueAfter(t *testing.T) {
}
client := fake.NewClientBuilder().WithScheme(scheme).Build()
metrics := appsetmetrics.NewFakeAppsetMetrics()
r := ApplicationSetReconciler{
Client: client,
Scheme: scheme,
Recorder: record.NewFakeRecorder(0),
Generators: topLevelGenerators,
Metrics: metrics,
}
type args struct {
appset *argov1alpha1.ApplicationSet
requeueAfterOverride string
appset *argov1alpha1.ApplicationSet
}
tests := []struct {
name string
@@ -108,13 +105,11 @@ func TestRequeueAfter(t *testing.T) {
want time.Duration
wantErr assert.ErrorAssertionFunc
}{
{name: "Cluster", args: args{
appset: &argov1alpha1.ApplicationSet{
Spec: argov1alpha1.ApplicationSetSpec{
Generators: []argov1alpha1.ApplicationSetGenerator{{Clusters: &argov1alpha1.ClusterGenerator{}}},
},
}, requeueAfterOverride: "",
}, want: generators.NoRequeueAfter, wantErr: assert.NoError},
{name: "Cluster", args: args{appset: &argov1alpha1.ApplicationSet{
Spec: argov1alpha1.ApplicationSetSpec{
Generators: []argov1alpha1.ApplicationSetGenerator{{Clusters: &argov1alpha1.ClusterGenerator{}}},
},
}}, want: generators.NoRequeueAfter, wantErr: assert.NoError},
{name: "ClusterMergeNested", args: args{&argov1alpha1.ApplicationSet{
Spec: argov1alpha1.ApplicationSetSpec{
Generators: []argov1alpha1.ApplicationSetGenerator{
@@ -129,7 +124,7 @@ func TestRequeueAfter(t *testing.T) {
}},
},
},
}, ""}, want: generators.DefaultRequeueAfter, wantErr: assert.NoError},
}}, want: generators.DefaultRequeueAfterSeconds, wantErr: assert.NoError},
{name: "ClusterMatrixNested", args: args{&argov1alpha1.ApplicationSet{
Spec: argov1alpha1.ApplicationSetSpec{
Generators: []argov1alpha1.ApplicationSetGenerator{
@@ -144,65 +139,15 @@ func TestRequeueAfter(t *testing.T) {
}},
},
},
}, ""}, want: generators.DefaultRequeueAfter, wantErr: assert.NoError},
}}, want: generators.DefaultRequeueAfterSeconds, wantErr: assert.NoError},
{name: "ListGenerator", args: args{appset: &argov1alpha1.ApplicationSet{
Spec: argov1alpha1.ApplicationSetSpec{
Generators: []argov1alpha1.ApplicationSetGenerator{{List: &argov1alpha1.ListGenerator{}}},
},
}}, want: generators.NoRequeueAfter, wantErr: assert.NoError},
{name: "DuckGenerator", args: args{appset: &argov1alpha1.ApplicationSet{
Spec: argov1alpha1.ApplicationSetSpec{
Generators: []argov1alpha1.ApplicationSetGenerator{{ClusterDecisionResource: &argov1alpha1.DuckTypeGenerator{}}},
},
}}, want: generators.DefaultRequeueAfter, wantErr: assert.NoError},
{name: "OverrideRequeueDuck", args: args{
appset: &argov1alpha1.ApplicationSet{
Spec: argov1alpha1.ApplicationSetSpec{
Generators: []argov1alpha1.ApplicationSetGenerator{{ClusterDecisionResource: &argov1alpha1.DuckTypeGenerator{}}},
},
}, requeueAfterOverride: "1h",
}, want: 1 * time.Hour, wantErr: assert.NoError},
{name: "OverrideRequeueGit", args: args{&argov1alpha1.ApplicationSet{
Spec: argov1alpha1.ApplicationSetSpec{
Generators: []argov1alpha1.ApplicationSetGenerator{
{Git: &argov1alpha1.GitGenerator{}},
},
},
}, "1h"}, want: 1 * time.Hour, wantErr: assert.NoError},
{name: "OverrideRequeueMatrix", args: args{&argov1alpha1.ApplicationSet{
Spec: argov1alpha1.ApplicationSetSpec{
Generators: []argov1alpha1.ApplicationSetGenerator{
{Clusters: &argov1alpha1.ClusterGenerator{}},
{Merge: &argov1alpha1.MergeGenerator{
Generators: []argov1alpha1.ApplicationSetNestedGenerator{
{
Clusters: &argov1alpha1.ClusterGenerator{},
Git: &argov1alpha1.GitGenerator{},
},
},
}},
},
},
}, "5m"}, want: 5 * time.Minute, wantErr: assert.NoError},
{name: "OverrideRequeueMerge", args: args{&argov1alpha1.ApplicationSet{
Spec: argov1alpha1.ApplicationSetSpec{
Generators: []argov1alpha1.ApplicationSetGenerator{
{Clusters: &argov1alpha1.ClusterGenerator{}},
{Merge: &argov1alpha1.MergeGenerator{
Generators: []argov1alpha1.ApplicationSetNestedGenerator{
{
Clusters: &argov1alpha1.ClusterGenerator{},
Git: &argov1alpha1.GitGenerator{},
},
},
}},
},
},
}, "12s"}, want: 12 * time.Second, wantErr: assert.NoError},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("ARGOCD_APPLICATIONSET_CONTROLLER_REQUEUE_AFTER", tt.args.requeueAfterOverride)
assert.Equalf(t, tt.want, r.getMinRequeueAfter(tt.args.appset), "getMinRequeueAfter(%v)", tt.args.appset)
})
}

View File

@@ -1,101 +0,0 @@
package template
import (
"fmt"
"sigs.k8s.io/controller-runtime/pkg/client"
log "github.com/sirupsen/logrus"
"github.com/argoproj/argo-cd/v3/applicationset/generators"
"github.com/argoproj/argo-cd/v3/applicationset/utils"
argov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
)
func GenerateApplications(logCtx *log.Entry, applicationSetInfo argov1alpha1.ApplicationSet, g map[string]generators.Generator, renderer utils.Renderer, client client.Client) ([]argov1alpha1.Application, argov1alpha1.ApplicationSetReasonType, error) {
var res []argov1alpha1.Application
var firstError error
var applicationSetReason argov1alpha1.ApplicationSetReasonType
for _, requestedGenerator := range applicationSetInfo.Spec.Generators {
t, err := generators.Transform(requestedGenerator, g, applicationSetInfo.Spec.Template, &applicationSetInfo, map[string]any{}, client)
if err != nil {
logCtx.WithError(err).WithField("generator", requestedGenerator).
Error("error generating application from params")
if firstError == nil {
firstError = err
applicationSetReason = argov1alpha1.ApplicationSetReasonApplicationParamsGenerationError
}
continue
}
for _, a := range t {
tmplApplication := GetTempApplication(a.Template)
for _, p := range a.Params {
app, err := renderer.RenderTemplateParams(tmplApplication, applicationSetInfo.Spec.SyncPolicy, p, applicationSetInfo.Spec.GoTemplate, applicationSetInfo.Spec.GoTemplateOptions)
if err != nil {
logCtx.WithError(err).WithField("params", a.Params).WithField("generator", requestedGenerator).
Error("error generating application from params")
if firstError == nil {
firstError = err
applicationSetReason = argov1alpha1.ApplicationSetReasonRenderTemplateParamsError
}
continue
}
if applicationSetInfo.Spec.TemplatePatch != nil {
patchedApplication, err := renderTemplatePatch(renderer, app, applicationSetInfo, p)
if err != nil {
log.WithError(err).WithField("params", a.Params).WithField("generator", requestedGenerator).
Error("error generating application from params")
if firstError == nil {
firstError = err
applicationSetReason = argov1alpha1.ApplicationSetReasonRenderTemplateParamsError
}
continue
}
app = patchedApplication
}
// The app's namespace must be the same as the AppSet's namespace to preserve the appsets-in-any-namespace
// security boundary.
app.Namespace = applicationSetInfo.Namespace
res = append(res, *app)
}
}
if log.IsLevelEnabled(log.DebugLevel) {
logCtx.WithField("generator", requestedGenerator).Debugf("apps from generator: %+v", res)
} else {
logCtx.Infof("generated %d applications", len(res))
}
}
return res, applicationSetReason, firstError
}
func renderTemplatePatch(r utils.Renderer, app *argov1alpha1.Application, applicationSetInfo argov1alpha1.ApplicationSet, params map[string]any) (*argov1alpha1.Application, error) {
replacedTemplate, err := r.Replace(*applicationSetInfo.Spec.TemplatePatch, params, applicationSetInfo.Spec.GoTemplate, applicationSetInfo.Spec.GoTemplateOptions)
if err != nil {
return nil, fmt.Errorf("error replacing values in templatePatch: %w", err)
}
return applyTemplatePatch(app, replacedTemplate)
}
func GetTempApplication(applicationSetTemplate argov1alpha1.ApplicationSetTemplate) *argov1alpha1.Application {
var tmplApplication argov1alpha1.Application
tmplApplication.Annotations = applicationSetTemplate.Annotations
tmplApplication.Labels = applicationSetTemplate.Labels
tmplApplication.Namespace = applicationSetTemplate.Namespace
tmplApplication.Name = applicationSetTemplate.Name
tmplApplication.Spec = applicationSetTemplate.Spec
tmplApplication.Finalizers = applicationSetTemplate.Finalizers
return &tmplApplication
}

View File

@@ -1,350 +0,0 @@
package template
import (
"errors"
"maps"
"testing"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"github.com/argoproj/argo-cd/v3/applicationset/generators"
genmock "github.com/argoproj/argo-cd/v3/applicationset/generators/mocks"
"github.com/argoproj/argo-cd/v3/applicationset/utils"
rendmock "github.com/argoproj/argo-cd/v3/applicationset/utils/mocks"
"github.com/argoproj/argo-cd/v3/pkg/apis/application"
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
)
func TestGenerateApplications(t *testing.T) {
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
err = v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
for _, c := range []struct {
name string
params []map[string]any
template v1alpha1.ApplicationSetTemplate
generateParamsError error
rendererError error
expectErr bool
expectedReason v1alpha1.ApplicationSetReasonType
}{
{
name: "Generate two applications",
params: []map[string]any{{"name": "app1"}, {"name": "app2"}},
template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{
Name: "name",
Namespace: "namespace",
Labels: map[string]string{"label_name": "label_value"},
},
Spec: v1alpha1.ApplicationSpec{},
},
expectedReason: "",
},
{
name: "Handles error from the generator",
generateParamsError: errors.New("error"),
expectErr: true,
expectedReason: v1alpha1.ApplicationSetReasonApplicationParamsGenerationError,
},
{
name: "Handles error from the render",
params: []map[string]any{{"name": "app1"}, {"name": "app2"}},
template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{
Name: "name",
Namespace: "namespace",
Labels: map[string]string{"label_name": "label_value"},
},
Spec: v1alpha1.ApplicationSpec{},
},
rendererError: errors.New("error"),
expectErr: true,
expectedReason: v1alpha1.ApplicationSetReasonRenderTemplateParamsError,
},
} {
cc := c
app := v1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "namespace",
},
TypeMeta: metav1.TypeMeta{
Kind: application.ApplicationKind,
APIVersion: "argoproj.io/v1alpha1",
},
}
t.Run(cc.name, func(t *testing.T) {
generatorMock := genmock.Generator{}
generator := v1alpha1.ApplicationSetGenerator{
List: &v1alpha1.ListGenerator{},
}
generatorMock.On("GenerateParams", &generator, mock.AnythingOfType("*v1alpha1.ApplicationSet"), mock.Anything).
Return(cc.params, cc.generateParamsError)
generatorMock.On("GetTemplate", &generator).
Return(&v1alpha1.ApplicationSetTemplate{})
rendererMock := rendmock.Renderer{}
var expectedApps []v1alpha1.Application
if cc.generateParamsError == nil {
for _, p := range cc.params {
if cc.rendererError != nil {
rendererMock.On("RenderTemplateParams", GetTempApplication(cc.template), mock.AnythingOfType("*v1alpha1.ApplicationSetSyncPolicy"), p, false, []string(nil)).
Return(nil, cc.rendererError)
} else {
rendererMock.On("RenderTemplateParams", GetTempApplication(cc.template), mock.AnythingOfType("*v1alpha1.ApplicationSetSyncPolicy"), p, false, []string(nil)).
Return(&app, nil)
expectedApps = append(expectedApps, app)
}
}
}
generators := map[string]generators.Generator{
"List": &generatorMock,
}
renderer := &rendererMock
got, reason, err := GenerateApplications(log.NewEntry(log.StandardLogger()), v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
},
Spec: v1alpha1.ApplicationSetSpec{
Generators: []v1alpha1.ApplicationSetGenerator{generator},
Template: cc.template,
},
},
generators,
renderer,
nil,
)
if cc.expectErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
assert.Equal(t, expectedApps, got)
assert.Equal(t, cc.expectedReason, reason)
generatorMock.AssertNumberOfCalls(t, "GenerateParams", 1)
if cc.generateParamsError == nil {
rendererMock.AssertNumberOfCalls(t, "RenderTemplateParams", len(cc.params))
}
})
}
}
func TestMergeTemplateApplications(t *testing.T) {
for _, c := range []struct {
name string
params []map[string]any
template v1alpha1.ApplicationSetTemplate
overrideTemplate v1alpha1.ApplicationSetTemplate
expectedMerged v1alpha1.ApplicationSetTemplate
expectedApps []v1alpha1.Application
}{
{
name: "Generate app",
params: []map[string]any{{"name": "app1"}},
template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{
Name: "name",
Namespace: "namespace",
Labels: map[string]string{"label_name": "label_value"},
},
Spec: v1alpha1.ApplicationSpec{},
},
overrideTemplate: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{
Name: "test",
Labels: map[string]string{"foo": "bar"},
},
Spec: v1alpha1.ApplicationSpec{},
},
expectedMerged: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{
Name: "test",
Namespace: "namespace",
Labels: map[string]string{"label_name": "label_value", "foo": "bar"},
},
Spec: v1alpha1.ApplicationSpec{},
},
expectedApps: []v1alpha1.Application{
{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "test",
Labels: map[string]string{"foo": "bar"},
},
Spec: v1alpha1.ApplicationSpec{},
},
},
},
} {
cc := c
t.Run(cc.name, func(t *testing.T) {
generatorMock := genmock.Generator{}
generator := v1alpha1.ApplicationSetGenerator{
List: &v1alpha1.ListGenerator{},
}
generatorMock.On("GenerateParams", &generator, mock.AnythingOfType("*v1alpha1.ApplicationSet"), mock.Anything).
Return(cc.params, nil)
generatorMock.On("GetTemplate", &generator).
Return(&cc.overrideTemplate)
rendererMock := rendmock.Renderer{}
rendererMock.On("RenderTemplateParams", GetTempApplication(cc.expectedMerged), mock.AnythingOfType("*v1alpha1.ApplicationSetSyncPolicy"), cc.params[0], false, []string(nil)).
Return(&cc.expectedApps[0], nil)
generators := map[string]generators.Generator{
"List": &generatorMock,
}
renderer := &rendererMock
got, _, _ := GenerateApplications(log.NewEntry(log.StandardLogger()), v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "namespace",
},
Spec: v1alpha1.ApplicationSetSpec{
Generators: []v1alpha1.ApplicationSetGenerator{generator},
Template: cc.template,
},
},
generators,
renderer,
nil,
)
assert.Equal(t, cc.expectedApps, got)
})
}
}
// Test app generation from a go template application set using a pull request generator
func TestGenerateAppsUsingPullRequestGenerator(t *testing.T) {
for _, cases := range []struct {
name string
params []map[string]any
template v1alpha1.ApplicationSetTemplate
expectedApp []v1alpha1.Application
}{
{
name: "Generate an application from a go template application set manifest using a pull request generator",
params: []map[string]any{
{
"number": "1",
"title": "title1",
"branch": "branch1",
"branch_slug": "branchSlug1",
"head_sha": "089d92cbf9ff857a39e6feccd32798ca700fb958",
"head_short_sha": "089d92cb",
"branch_slugify_default": "feat/a_really+long_pull_request_name_to_test_argo_slugification_and_branch_name_shortening_feature",
"branch_slugify_smarttruncate_disabled": "feat/areallylongpullrequestnametotestargoslugificationandbranchnameshorteningfeature",
"branch_slugify_smarttruncate_enabled": "feat/testwithsmarttruncateenabledramdomlonglistofcharacters",
"labels": []string{"label1"},
},
},
template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{
Name: "AppSet-{{.branch}}-{{.number}}",
Labels: map[string]string{
"app1": "{{index .labels 0}}",
"branch-test1": "AppSet-{{.branch_slugify_default | slugify }}",
"branch-test2": "AppSet-{{.branch_slugify_smarttruncate_disabled | slugify 49 false }}",
"branch-test3": "AppSet-{{.branch_slugify_smarttruncate_enabled | slugify 50 true }}",
},
},
Spec: v1alpha1.ApplicationSpec{
Source: &v1alpha1.ApplicationSource{
RepoURL: "https://testurl/testRepo",
TargetRevision: "{{.head_short_sha}}",
},
Destination: v1alpha1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: "AppSet-{{.branch_slug}}-{{.head_sha}}",
},
},
},
expectedApp: []v1alpha1.Application{
{
ObjectMeta: metav1.ObjectMeta{
Name: "AppSet-branch1-1",
Labels: map[string]string{
"app1": "label1",
"branch-test1": "AppSet-feat-a-really-long-pull-request-name-to-test-argo",
"branch-test2": "AppSet-feat-areallylongpullrequestnametotestargoslugific",
"branch-test3": "AppSet-feat",
},
},
Spec: v1alpha1.ApplicationSpec{
Source: &v1alpha1.ApplicationSource{
RepoURL: "https://testurl/testRepo",
TargetRevision: "089d92cb",
},
Destination: v1alpha1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: "AppSet-branchSlug1-089d92cbf9ff857a39e6feccd32798ca700fb958",
},
},
},
},
},
} {
t.Run(cases.name, func(t *testing.T) {
generatorMock := genmock.Generator{}
generator := v1alpha1.ApplicationSetGenerator{
PullRequest: &v1alpha1.PullRequestGenerator{},
}
generatorMock.On("GenerateParams", &generator, mock.AnythingOfType("*v1alpha1.ApplicationSet"), mock.Anything).
Return(cases.params, nil)
generatorMock.On("GetTemplate", &generator).
Return(&cases.template, nil)
generators := map[string]generators.Generator{
"PullRequest": &generatorMock,
}
renderer := &utils.Render{}
gotApp, _, _ := GenerateApplications(log.NewEntry(log.StandardLogger()), v1alpha1.ApplicationSet{
Spec: v1alpha1.ApplicationSetSpec{
GoTemplate: true,
Generators: []v1alpha1.ApplicationSetGenerator{{
PullRequest: &v1alpha1.PullRequestGenerator{},
}},
Template: cases.template,
},
},
generators,
renderer,
nil,
)
assert.Equal(t, cases.expectedApp[0].Name, gotApp[0].Name)
assert.Equal(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.True(t, maps.Equal(cases.expectedApp[0].Labels, gotApp[0].Labels))
})
}
}

View File

@@ -1,4 +1,4 @@
package template
package controllers
import (
"encoding/json"
@@ -6,8 +6,8 @@ import (
"k8s.io/apimachinery/pkg/util/strategicpatch"
"github.com/argoproj/argo-cd/v3/applicationset/utils"
appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/applicationset/utils"
appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
func applyTemplatePatch(app *appv1.Application, templatePatch string) (*appv1.Application, error) {

View File

@@ -1,4 +1,4 @@
package template
package controllers
import (
"testing"
@@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
appv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
func Test_ApplyTemplatePatch(t *testing.T) {
@@ -27,7 +27,7 @@ func Test_ApplyTemplatePatch(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
Name: "my-cluster-guestbook",
Namespace: "namespace",
Finalizers: []string{appv1.ResourcesFinalizerName},
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
},
Spec: appv1.ApplicationSpec{
Project: "default",
@@ -72,7 +72,7 @@ func Test_ApplyTemplatePatch(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
Name: "my-cluster-guestbook",
Namespace: "namespace",
Finalizers: []string{appv1.ResourcesFinalizerName},
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
Annotations: map[string]string{
"annotation-some-key": "annotation-some-value",
},
@@ -112,7 +112,7 @@ func Test_ApplyTemplatePatch(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
Name: "my-cluster-guestbook",
Namespace: "namespace",
Finalizers: []string{appv1.ResourcesFinalizerName},
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
},
Spec: appv1.ApplicationSpec{
Project: "default",
@@ -148,7 +148,7 @@ spec:
ObjectMeta: metav1.ObjectMeta{
Name: "my-cluster-guestbook",
Namespace: "namespace",
Finalizers: []string{appv1.ResourcesFinalizerName},
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
Annotations: map[string]string{
"annotation-some-key": "annotation-some-value",
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,14 +7,20 @@ import (
log "github.com/sirupsen/logrus"
"github.com/argoproj/argo-cd/v2/util/settings"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/argoproj/argo-cd/v3/applicationset/utils"
"github.com/argoproj/argo-cd/v3/common"
argoappsetv1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/applicationset/utils"
argoappsetv1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
const (
ArgoCDSecretTypeLabel = "argocd.argoproj.io/secret-type"
ArgoCDSecretTypeCluster = "cluster"
)
var _ Generator = (*ClusterGenerator)(nil)
@@ -25,24 +31,28 @@ type ClusterGenerator struct {
ctx context.Context
clientset kubernetes.Interface
// namespace is the Argo CD namespace
namespace string
namespace string
settingsManager *settings.SettingsManager
}
var render = &utils.Render{}
func NewClusterGenerator(ctx context.Context, c client.Client, clientset kubernetes.Interface, namespace string) Generator {
func NewClusterGenerator(c client.Client, ctx context.Context, clientset kubernetes.Interface, namespace string) Generator {
settingsManager := settings.NewSettingsManager(ctx, clientset, namespace)
g := &ClusterGenerator{
Client: c,
ctx: ctx,
clientset: clientset,
namespace: namespace,
Client: c,
ctx: ctx,
clientset: clientset,
namespace: namespace,
settingsManager: settingsManager,
}
return g
}
// GetRequeueAfter never requeue the cluster generator because the `clusterSecretEventHandler` will requeue the appsets
// when the cluster secrets change
func (g *ClusterGenerator) GetRequeueAfter(_ *argoappsetv1alpha1.ApplicationSetGenerator) time.Duration {
func (g *ClusterGenerator) GetRequeueAfter(appSetGenerator *argoappsetv1alpha1.ApplicationSetGenerator) time.Duration {
return NoRequeueAfter
}
@@ -50,21 +60,20 @@ func (g *ClusterGenerator) GetTemplate(appSetGenerator *argoappsetv1alpha1.Appli
return &appSetGenerator.Clusters.Template
}
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())
func (g *ClusterGenerator) GenerateParams(appSetGenerator *argoappsetv1alpha1.ApplicationSetGenerator, appSet *argoappsetv1alpha1.ApplicationSet, _ client.Client) ([]map[string]interface{}, error) {
if appSetGenerator == nil {
return nil, ErrEmptyAppSetGenerator
return nil, EmptyAppSetGeneratorError
}
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
// - Since local clusters do not have secrets, they do not have labels to match against
ignoreLocalClusters := len(appSetGenerator.Clusters.Selector.MatchExpressions) > 0 || len(appSetGenerator.Clusters.Selector.MatchLabels) > 0
// ListCluster 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
clustersFromArgoCD, err := utils.ListClusters(g.ctx, g.clientset, g.namespace)
if err != nil {
return nil, fmt.Errorf("error listing clusters: %w", err)
@@ -74,118 +83,94 @@ func (g *ClusterGenerator) GenerateParams(appSetGenerator *argoappsetv1alpha1.Ap
return nil, nil
}
clusterSecrets, err := g.getSecretsByClusterName(logCtx, appSetGenerator)
clusterSecrets, err := g.getSecretsByClusterName(appSetGenerator)
if err != nil {
return nil, fmt.Errorf("error getting cluster secrets: %w", err)
return nil, err
}
res := []map[string]any{}
res := []map[string]interface{}{}
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.Items {
// If there is a secret for this cluster, then it's a non-local cluster, so it will be
// handled by the next step.
if secretForCluster, exists := clusterSecrets[cluster.Name]; exists {
secretsFound = append(secretsFound, secretForCluster)
} else if !ignoreLocalClusters {
// If there is no secret for the cluster, it's the local cluster, so handle it here.
params := map[string]any{}
params := map[string]interface{}{}
params["name"] = cluster.Name
params["nameNormalized"] = cluster.Name
params["server"] = cluster.Server
params["project"] = ""
err = appendTemplatedValues(appSetGenerator.Clusters.Values, params, appSet.Spec.GoTemplate, appSet.Spec.GoTemplateOptions)
if err != nil {
return nil, fmt.Errorf("error appending templated values for local cluster: %w", err)
return nil, err
}
if isFlatMode {
clustersParams = append(clustersParams, params)
} else {
res = append(res, params)
}
res = append(res, params)
logCtx.WithField("cluster", "local cluster").Info("matched local cluster")
log.WithField("cluster", "local cluster").Info("matched local cluster")
}
}
// For each matching cluster secret (non-local clusters only)
for _, cluster := range secretsFound {
params := map[string]any{}
params := map[string]interface{}{}
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{}
meta := map[string]interface{}{}
if len(cluster.Annotations) > 0 {
meta["annotations"] = cluster.Annotations
if len(cluster.ObjectMeta.Annotations) > 0 {
meta["annotations"] = cluster.ObjectMeta.Annotations
}
if len(cluster.Labels) > 0 {
meta["labels"] = cluster.Labels
if len(cluster.ObjectMeta.Labels) > 0 {
meta["labels"] = cluster.ObjectMeta.Labels
}
params["metadata"] = meta
} else {
for key, value := range cluster.Annotations {
params["metadata.annotations."+key] = value
for key, value := range cluster.ObjectMeta.Annotations {
params[fmt.Sprintf("metadata.annotations.%s", key)] = value
}
for key, value := range cluster.Labels {
params["metadata.labels."+key] = value
for key, value := range cluster.ObjectMeta.Labels {
params[fmt.Sprintf("metadata.labels.%s", key)] = value
}
}
err = appendTemplatedValues(appSetGenerator.Clusters.Values, params, appSet.Spec.GoTemplate, appSet.Spec.GoTemplateOptions)
if err != nil {
return nil, fmt.Errorf("error appending templated values for cluster: %w", err)
return nil, err
}
if isFlatMode {
clustersParams = append(clustersParams, params)
} else {
res = append(res, params)
}
res = append(res, params)
logCtx.WithField("cluster", cluster.Name).Debug("matched cluster secret")
log.WithField("cluster", cluster.Name).Info("matched cluster secret")
}
if isFlatMode {
res = append(res, map[string]any{
"clusters": clustersParams,
})
}
return res, nil
}
func (g *ClusterGenerator) getSecretsByClusterName(log *log.Entry, appSetGenerator *argoappsetv1alpha1.ApplicationSetGenerator) (map[string]corev1.Secret, error) {
func (g *ClusterGenerator) getSecretsByClusterName(appSetGenerator *argoappsetv1alpha1.ApplicationSetGenerator) (map[string]corev1.Secret, error) {
// List all Clusters:
clusterSecretList := &corev1.SecretList{}
selector := metav1.AddLabelToSelector(&appSetGenerator.Clusters.Selector, common.LabelKeySecretType, common.LabelValueSecretTypeCluster)
selector := metav1.AddLabelToSelector(&appSetGenerator.Clusters.Selector, ArgoCDSecretTypeLabel, ArgoCDSecretTypeCluster)
secretSelector, err := metav1.LabelSelectorAsSelector(selector)
if err != nil {
return nil, fmt.Errorf("error converting label selector: %w", err)
}
if err := g.List(context.Background(), clusterSecretList, client.MatchingLabelsSelector{Selector: secretSelector}); err != nil {
return nil, err
}
log.Debugf("clusters matching labels: %d", len(clusterSecretList.Items))
if err := g.Client.List(context.Background(), clusterSecretList, client.MatchingLabelsSelector{Selector: secretSelector}); err != nil {
return nil, err
}
log.Debug("clusters matching labels", "count", len(clusterSecretList.Items))
res := map[string]corev1.Secret{}

View File

@@ -2,7 +2,7 @@ package generators
import (
"context"
"errors"
"fmt"
"testing"
corev1 "k8s.io/api/core/v1"
@@ -13,8 +13,8 @@ import (
kubefake "k8s.io/client-go/kubernetes/fake"
"github.com/argoproj/argo-cd/v3/applicationset/utils"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/applicationset/utils"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -27,7 +27,7 @@ type possiblyErroringFakeCtrlRuntimeClient struct {
func (p *possiblyErroringFakeCtrlRuntimeClient) List(ctx context.Context, secretList client.ObjectList, opts ...client.ListOption) error {
if p.shouldError {
return errors.New("could not list Secrets")
return fmt.Errorf("could not list Secrets")
}
return p.Client.List(ctx, secretList, opts...)
}
@@ -76,20 +76,18 @@ func TestGenerateParams(t *testing.T) {
},
},
Data: map[string][]byte{
"config": []byte("{}"),
"name": []byte("production_01/west"),
"server": []byte("https://production-01.example.com"),
"project": []byte("prod-project"),
"config": []byte("{}"),
"name": []byte("production_01/west"),
"server": []byte("https://production-01.example.com"),
},
Type: corev1.SecretType("Opaque"),
},
}
testCases := []struct {
name string
selector metav1.LabelSelector
isFlatMode bool
values map[string]string
expected []map[string]any
name string
selector metav1.LabelSelector
values map[string]string
expected []map[string]interface{}
// clientError is true if a k8s client error should be simulated
clientError bool
expectedError error
@@ -106,17 +104,18 @@ func TestGenerateParams(t *testing.T) {
"bat": "{{ metadata.labels.environment }}",
"aaa": "{{ server }}",
"no-op": "{{ this-does-not-exist }}",
}, expected: []map[string]any{
{"values.lol1": "lol", "values.lol2": "{{values.lol1}}{{values.lol1}}", "values.lol3": "{{values.lol2}}{{values.lol2}}{{values.lol2}}", "values.foo": "bar", "values.bar": "{{ metadata.annotations.foo.argoproj.io }}", "values.no-op": "{{ this-does-not-exist }}", "values.bat": "{{ metadata.labels.environment }}", "values.aaa": "https://kubernetes.default.svc", "nameNormalized": "in-cluster", "name": "in-cluster", "server": "https://kubernetes.default.svc", "project": ""},
}, expected: []map[string]interface{}{
{
"values.lol1": "lol", "values.lol2": "{{values.lol1}}{{values.lol1}}", "values.lol3": "{{values.lol2}}{{values.lol2}}{{values.lol2}}", "values.foo": "bar", "values.bar": "production", "values.no-op": "{{ this-does-not-exist }}", "values.bat": "production", "values.aaa": "https://production-01.example.com", "name": "production_01/west", "nameNormalized": "production-01-west", "server": "https://production-01.example.com", "metadata.labels.environment": "production", "metadata.labels.org": "bar",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production", "project": "prod-project",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production",
},
{
"values.lol1": "lol", "values.lol2": "{{values.lol1}}{{values.lol1}}", "values.lol3": "{{values.lol2}}{{values.lol2}}{{values.lol2}}", "values.foo": "bar", "values.bar": "staging", "values.no-op": "{{ this-does-not-exist }}", "values.bat": "staging", "values.aaa": "https://staging-01.example.com", "name": "staging-01", "nameNormalized": "staging-01", "server": "https://staging-01.example.com", "metadata.labels.environment": "staging", "metadata.labels.org": "foo",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "staging", "project": "",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "staging",
},
{"values.lol1": "lol", "values.lol2": "{{values.lol1}}{{values.lol1}}", "values.lol3": "{{values.lol2}}{{values.lol2}}{{values.lol2}}", "values.foo": "bar", "values.bar": "{{ metadata.annotations.foo.argoproj.io }}", "values.no-op": "{{ this-does-not-exist }}", "values.bat": "{{ metadata.labels.environment }}", "values.aaa": "https://kubernetes.default.svc", "nameNormalized": "in-cluster", "name": "in-cluster", "server": "https://kubernetes.default.svc"},
},
clientError: false,
expectedError: nil,
@@ -129,15 +128,15 @@ func TestGenerateParams(t *testing.T) {
},
},
values: nil,
expected: []map[string]any{
expected: []map[string]interface{}{
{
"name": "production_01/west", "nameNormalized": "production-01-west", "server": "https://production-01.example.com", "metadata.labels.environment": "production", "metadata.labels.org": "bar",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production", "project": "prod-project",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production",
},
{
"name": "staging-01", "nameNormalized": "staging-01", "server": "https://staging-01.example.com", "metadata.labels.environment": "staging", "metadata.labels.org": "foo",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "staging", "project": "",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "staging",
},
},
clientError: false,
@@ -153,10 +152,10 @@ func TestGenerateParams(t *testing.T) {
values: map[string]string{
"foo": "bar",
},
expected: []map[string]any{
expected: []map[string]interface{}{
{
"values.foo": "bar", "name": "production_01/west", "nameNormalized": "production-01-west", "server": "https://production-01.example.com", "metadata.labels.environment": "production", "metadata.labels.org": "bar",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production", "project": "prod-project",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production",
},
},
clientError: false,
@@ -179,14 +178,14 @@ func TestGenerateParams(t *testing.T) {
values: map[string]string{
"foo": "bar",
},
expected: []map[string]any{
expected: []map[string]interface{}{
{
"values.foo": "bar", "name": "staging-01", "nameNormalized": "staging-01", "server": "https://staging-01.example.com", "metadata.labels.environment": "staging", "metadata.labels.org": "foo",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "staging", "project": "",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "staging",
},
{
"values.foo": "bar", "name": "production_01/west", "nameNormalized": "production-01-west", "server": "https://production-01.example.com", "metadata.labels.environment": "production", "metadata.labels.org": "bar",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production", "project": "prod-project",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production",
},
},
clientError: false,
@@ -212,10 +211,10 @@ func TestGenerateParams(t *testing.T) {
values: map[string]string{
"name": "baz",
},
expected: []map[string]any{
expected: []map[string]interface{}{
{
"values.name": "baz", "name": "staging-01", "nameNormalized": "staging-01", "server": "https://staging-01.example.com", "metadata.labels.environment": "staging", "metadata.labels.org": "foo",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "staging", "project": "",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "staging",
},
},
clientError: false,
@@ -227,75 +226,7 @@ func TestGenerateParams(t *testing.T) {
values: nil,
expected: nil,
clientError: true,
expectedError: errors.New("error getting cluster secrets: could not list Secrets"),
},
{
name: "flat mode without selectors",
selector: metav1.LabelSelector{},
values: map[string]string{
"lol1": "lol",
"lol2": "{{values.lol1}}{{values.lol1}}",
"lol3": "{{values.lol2}}{{values.lol2}}{{values.lol2}}",
"foo": "bar",
"bar": "{{ metadata.annotations.foo.argoproj.io }}",
"bat": "{{ metadata.labels.environment }}",
"aaa": "{{ server }}",
"no-op": "{{ this-does-not-exist }}",
},
expected: []map[string]any{
{
"clusters": []map[string]any{
{"values.lol1": "lol", "values.lol2": "{{values.lol1}}{{values.lol1}}", "values.lol3": "{{values.lol2}}{{values.lol2}}{{values.lol2}}", "values.foo": "bar", "values.bar": "{{ metadata.annotations.foo.argoproj.io }}", "values.no-op": "{{ this-does-not-exist }}", "values.bat": "{{ metadata.labels.environment }}", "values.aaa": "https://kubernetes.default.svc", "nameNormalized": "in-cluster", "name": "in-cluster", "server": "https://kubernetes.default.svc", "project": ""},
{
"values.lol1": "lol", "values.lol2": "{{values.lol1}}{{values.lol1}}", "values.lol3": "{{values.lol2}}{{values.lol2}}{{values.lol2}}", "values.foo": "bar", "values.bar": "production", "values.no-op": "{{ this-does-not-exist }}", "values.bat": "production", "values.aaa": "https://production-01.example.com", "name": "production_01/west", "nameNormalized": "production-01-west", "server": "https://production-01.example.com", "metadata.labels.environment": "production", "metadata.labels.org": "bar",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production", "project": "prod-project",
},
{
"values.lol1": "lol", "values.lol2": "{{values.lol1}}{{values.lol1}}", "values.lol3": "{{values.lol2}}{{values.lol2}}{{values.lol2}}", "values.foo": "bar", "values.bar": "staging", "values.no-op": "{{ this-does-not-exist }}", "values.bat": "staging", "values.aaa": "https://staging-01.example.com", "name": "staging-01", "nameNormalized": "staging-01", "server": "https://staging-01.example.com", "metadata.labels.environment": "staging", "metadata.labels.org": "foo",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "staging", "project": "",
},
},
},
},
isFlatMode: true,
clientError: false,
expectedError: nil,
},
{
name: "production or staging with flat mode",
selector: metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "environment",
Operator: "In",
Values: []string{
"production",
"staging",
},
},
},
},
isFlatMode: true,
values: map[string]string{
"foo": "bar",
},
expected: []map[string]any{
{
"clusters": []map[string]any{
{
"values.foo": "bar", "name": "production_01/west", "nameNormalized": "production-01-west", "server": "https://production-01.example.com", "metadata.labels.environment": "production", "metadata.labels.org": "bar",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "production", "project": "prod-project",
},
{
"values.foo": "bar", "name": "staging-01", "nameNormalized": "staging-01", "server": "https://staging-01.example.com", "metadata.labels.environment": "staging", "metadata.labels.org": "foo",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster", "metadata.annotations.foo.argoproj.io": "staging", "project": "",
},
},
},
},
clientError: false,
expectedError: nil,
expectedError: fmt.Errorf("could not list Secrets"),
},
}
@@ -315,7 +246,7 @@ func TestGenerateParams(t *testing.T) {
testCase.clientError,
}
clusterGenerator := NewClusterGenerator(t.Context(), cl, appClientset, "namespace")
clusterGenerator := NewClusterGenerator(cl, context.Background(), appClientset, "namespace")
applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
@@ -328,7 +259,6 @@ func TestGenerateParams(t *testing.T) {
Clusters: &argoprojiov1alpha1.ClusterGenerator{
Selector: testCase.selector,
Values: testCase.values,
FlatList: testCase.isFlatMode,
},
}, &applicationSetInfo, nil)
@@ -394,11 +324,10 @@ func TestGenerateParamsGoTemplate(t *testing.T) {
},
}
testCases := []struct {
name string
selector metav1.LabelSelector
values map[string]string
isFlatMode bool
expected []map[string]any
name string
selector metav1.LabelSelector
values map[string]string
expected []map[string]interface{}
// clientError is true if a k8s client error should be simulated
clientError bool
expectedError error
@@ -415,13 +344,12 @@ func TestGenerateParamsGoTemplate(t *testing.T) {
"bat": "{{ if not (empty .metadata) }}{{.metadata.labels.environment}}{{ end }}",
"aaa": "{{ .server }}",
"no-op": "{{ .thisDoesNotExist }}",
}, expected: []map[string]any{
}, expected: []map[string]interface{}{
{
"name": "production_01/west",
"nameNormalized": "production-01-west",
"server": "https://production-01.example.com",
"project": "",
"metadata": map[string]any{
"metadata": map[string]interface{}{
"labels": map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
"environment": "production",
@@ -446,8 +374,7 @@ func TestGenerateParamsGoTemplate(t *testing.T) {
"name": "staging-01",
"nameNormalized": "staging-01",
"server": "https://staging-01.example.com",
"project": "",
"metadata": map[string]any{
"metadata": map[string]interface{}{
"labels": map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
"environment": "staging",
@@ -472,7 +399,6 @@ func TestGenerateParamsGoTemplate(t *testing.T) {
"nameNormalized": "in-cluster",
"name": "in-cluster",
"server": "https://kubernetes.default.svc",
"project": "",
"values": map[string]string{
"lol1": "lol",
"lol2": "<no value><no value>",
@@ -496,13 +422,12 @@ func TestGenerateParamsGoTemplate(t *testing.T) {
},
},
values: nil,
expected: []map[string]any{
expected: []map[string]interface{}{
{
"name": "production_01/west",
"nameNormalized": "production-01-west",
"server": "https://production-01.example.com",
"project": "",
"metadata": map[string]any{
"metadata": map[string]interface{}{
"labels": map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
"environment": "production",
@@ -517,8 +442,7 @@ func TestGenerateParamsGoTemplate(t *testing.T) {
"name": "staging-01",
"nameNormalized": "staging-01",
"server": "https://staging-01.example.com",
"project": "",
"metadata": map[string]any{
"metadata": map[string]interface{}{
"labels": map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
"environment": "staging",
@@ -543,13 +467,12 @@ func TestGenerateParamsGoTemplate(t *testing.T) {
values: map[string]string{
"foo": "bar",
},
expected: []map[string]any{
expected: []map[string]interface{}{
{
"name": "production_01/west",
"nameNormalized": "production-01-west",
"server": "https://production-01.example.com",
"project": "",
"metadata": map[string]any{
"metadata": map[string]interface{}{
"labels": map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
"environment": "production",
@@ -584,13 +507,12 @@ func TestGenerateParamsGoTemplate(t *testing.T) {
values: map[string]string{
"foo": "bar",
},
expected: []map[string]any{
expected: []map[string]interface{}{
{
"name": "production_01/west",
"nameNormalized": "production-01-west",
"server": "https://production-01.example.com",
"project": "",
"metadata": map[string]any{
"metadata": map[string]interface{}{
"labels": map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
"environment": "production",
@@ -608,8 +530,7 @@ func TestGenerateParamsGoTemplate(t *testing.T) {
"name": "staging-01",
"nameNormalized": "staging-01",
"server": "https://staging-01.example.com",
"project": "",
"metadata": map[string]any{
"metadata": map[string]interface{}{
"labels": map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
"environment": "staging",
@@ -647,13 +568,12 @@ func TestGenerateParamsGoTemplate(t *testing.T) {
values: map[string]string{
"name": "baz",
},
expected: []map[string]any{
expected: []map[string]interface{}{
{
"name": "staging-01",
"nameNormalized": "staging-01",
"server": "https://staging-01.example.com",
"project": "",
"metadata": map[string]any{
"metadata": map[string]interface{}{
"labels": map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
"environment": "staging",
@@ -677,163 +597,7 @@ func TestGenerateParamsGoTemplate(t *testing.T) {
values: nil,
expected: nil,
clientError: true,
expectedError: errors.New("error getting cluster secrets: could not list Secrets"),
},
{
name: "Clusters with flat list mode and no selector",
selector: metav1.LabelSelector{},
isFlatMode: true,
values: map[string]string{
"lol1": "lol",
"lol2": "{{ .values.lol1 }}{{ .values.lol1 }}",
"lol3": "{{ .values.lol2 }}{{ .values.lol2 }}{{ .values.lol2 }}",
"foo": "bar",
"bar": "{{ if not (empty .metadata) }}{{index .metadata.annotations \"foo.argoproj.io\" }}{{ end }}",
"bat": "{{ if not (empty .metadata) }}{{.metadata.labels.environment}}{{ end }}",
"aaa": "{{ .server }}",
"no-op": "{{ .thisDoesNotExist }}",
},
expected: []map[string]any{
{
"clusters": []map[string]any{
{
"nameNormalized": "in-cluster",
"name": "in-cluster",
"server": "https://kubernetes.default.svc",
"project": "",
"values": map[string]string{
"lol1": "lol",
"lol2": "<no value><no value>",
"lol3": "<no value><no value><no value>",
"foo": "bar",
"bar": "",
"bat": "",
"aaa": "https://kubernetes.default.svc",
"no-op": "<no value>",
},
},
{
"name": "production_01/west",
"nameNormalized": "production-01-west",
"server": "https://production-01.example.com",
"project": "",
"metadata": map[string]any{
"labels": map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
"environment": "production",
"org": "bar",
},
"annotations": map[string]string{
"foo.argoproj.io": "production",
},
},
"values": map[string]string{
"lol1": "lol",
"lol2": "<no value><no value>",
"lol3": "<no value><no value><no value>",
"foo": "bar",
"bar": "production",
"bat": "production",
"aaa": "https://production-01.example.com",
"no-op": "<no value>",
},
},
{
"name": "staging-01",
"nameNormalized": "staging-01",
"server": "https://staging-01.example.com",
"project": "",
"metadata": map[string]any{
"labels": map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
"environment": "staging",
"org": "foo",
},
"annotations": map[string]string{
"foo.argoproj.io": "staging",
},
},
"values": map[string]string{
"lol1": "lol",
"lol2": "<no value><no value>",
"lol3": "<no value><no value><no value>",
"foo": "bar",
"bar": "staging",
"bat": "staging",
"aaa": "https://staging-01.example.com",
"no-op": "<no value>",
},
},
},
},
},
clientError: false,
expectedError: nil,
},
{
name: "production or staging with flat mode",
selector: metav1.LabelSelector{
MatchExpressions: []metav1.LabelSelectorRequirement{
{
Key: "environment",
Operator: "In",
Values: []string{
"production",
"staging",
},
},
},
},
isFlatMode: true,
values: map[string]string{
"foo": "bar",
},
expected: []map[string]any{
{
"clusters": []map[string]any{
{
"name": "production_01/west",
"nameNormalized": "production-01-west",
"server": "https://production-01.example.com",
"project": "",
"metadata": map[string]any{
"labels": map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
"environment": "production",
"org": "bar",
},
"annotations": map[string]string{
"foo.argoproj.io": "production",
},
},
"values": map[string]string{
"foo": "bar",
},
},
{
"name": "staging-01",
"nameNormalized": "staging-01",
"server": "https://staging-01.example.com",
"project": "",
"metadata": map[string]any{
"labels": map[string]string{
"argocd.argoproj.io/secret-type": "cluster",
"environment": "staging",
"org": "foo",
},
"annotations": map[string]string{
"foo.argoproj.io": "staging",
},
},
"values": map[string]string{
"foo": "bar",
},
},
},
},
},
clientError: false,
expectedError: nil,
expectedError: fmt.Errorf("could not list Secrets"),
},
}
@@ -853,7 +617,7 @@ func TestGenerateParamsGoTemplate(t *testing.T) {
testCase.clientError,
}
clusterGenerator := NewClusterGenerator(t.Context(), cl, appClientset, "namespace")
clusterGenerator := NewClusterGenerator(cl, context.Background(), appClientset, "namespace")
applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
@@ -868,7 +632,6 @@ func TestGenerateParamsGoTemplate(t *testing.T) {
Clusters: &argoprojiov1alpha1.ClusterGenerator{
Selector: testCase.selector,
Values: testCase.values,
FlatList: testCase.isFlatMode,
},
}, &applicationSetInfo, nil)

View File

@@ -2,7 +2,6 @@ package generators
import (
"context"
"errors"
"fmt"
"strings"
"time"
@@ -10,33 +9,38 @@ import (
log "github.com/sirupsen/logrus"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/argoproj/argo-cd/v2/util/settings"
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/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"github.com/argoproj/argo-cd/v3/applicationset/utils"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/applicationset/utils"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
var _ Generator = (*DuckTypeGenerator)(nil)
// DuckTypeGenerator generates Applications for some or all clusters registered with ArgoCD.
type DuckTypeGenerator struct {
ctx context.Context
dynClient dynamic.Interface
clientset kubernetes.Interface
namespace string // namespace is the Argo CD namespace
ctx context.Context
dynClient dynamic.Interface
clientset kubernetes.Interface
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 {
settingsManager := settings.NewSettingsManager(ctx, clientset, namespace)
g := &DuckTypeGenerator{
ctx: ctx,
dynClient: dynClient,
clientset: clientset,
namespace: namespace,
ctx: ctx,
dynClient: dynClient,
clientset: clientset,
namespace: namespace,
settingsManager: settingsManager,
}
return g
}
@@ -48,21 +52,21 @@ func (g *DuckTypeGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.
return time.Duration(*appSetGenerator.ClusterDecisionResource.RequeueAfterSeconds) * time.Second
}
return getDefaultRequeueAfter()
return DefaultRequeueAfterSeconds
}
func (g *DuckTypeGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate {
return &appSetGenerator.ClusterDecisionResource.Template
}
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]interface{}, error) {
if appSetGenerator == nil {
return nil, ErrEmptyAppSetGenerator
return nil, EmptyAppSetGeneratorError
}
// Not likely to happen
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
@@ -92,13 +96,13 @@ func (g *DuckTypeGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.A
// Validate the fields
if kind == "" || versionIdx < 1 {
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, fmt.Errorf("There is a problem with the apiVersion, kind or resourceName provided")
}
if (resourceName == "" && labelSelector.MatchLabels == nil && labelSelector.MatchExpressions == 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)
return nil, errors.New("there is a problem with the definition of the ClusterDecisionResource generator")
return nil, fmt.Errorf("There is a problem with the definition of the ClusterDecisionResource generator")
}
// Split up the apiVersion
@@ -121,109 +125,102 @@ func (g *DuckTypeGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.A
duckResources, err := g.dynClient.Resource(duckGVR).Namespace(g.namespace).List(g.ctx, listOptions)
if err != nil {
log.WithField("GVK", duckGVR).Warning("resources were not found")
return nil, fmt.Errorf("failed to get dynamic resources: %w", err)
return nil, err
}
if len(duckResources.Items) == 0 {
log.Warning("no resource found, make sure you clusterDecisionResource is defined correctly")
return nil, errors.New("no clusterDecisionResources found")
return nil, fmt.Errorf("no clusterDecisionResources found")
}
// Override the duck type in the status of the resource
statusListKey := "clusters"
matchKey := cm.Data["matchKey"]
if cm.Data["statusListKey"] != "" {
statusListKey = cm.Data["statusListKey"]
}
matchKey := cm.Data["matchKey"]
if matchKey == "" {
log.WithField("matchKey", matchKey).Warning("matchKey not found in " + cm.Name)
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{}
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{}
res := []map[string]interface{}{}
clusterDecisions := []interface{}{}
// Build the decision slice
for _, duckResource := range duckResources.Items {
log.WithField("duckResourceName", duckResource.GetName()).Debug("found resource")
if duckResource.Object["status"] == nil || len(duckResource.Object["status"].(map[string]any)) == 0 {
if duckResource.Object["status"] == nil || len(duckResource.Object["status"].(map[string]interface{})) == 0 {
log.Warningf("clusterDecisionResource: %s, has no status", duckResource.GetName())
continue
}
log.WithField("duckResourceStatus", duckResource.Object["status"]).Debug("found resource")
clusterDecisions = append(clusterDecisions, duckResource.Object["status"].(map[string]any)[statusListKey].([]any)...)
clusterDecisions = append(clusterDecisions, duckResource.Object["status"].(map[string]interface{})[statusListKey].([]interface{})...)
}
log.Infof("Number of decisions found: %v", len(clusterDecisions))
return clusterDecisions
}
func findCluster(clustersFromArgoCD []utils.ClusterSpecifier, cluster any, matchKey string, statusListKey string) *utils.ClusterSpecifier {
log.Infof("cluster: %v", cluster)
matchValue := cluster.(map[string]any)[matchKey]
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
}
// Read this outside the loop to improve performance
argoClusters := clustersFromArgoCD.Items
strMatchValue := matchValue.(string)
log.WithField(matchKey, strMatchValue).Debug("validate against ArgoCD")
if len(clusterDecisions) > 0 {
for _, cluster := range clusterDecisions {
// generated instance of cluster params
params := map[string]interface{}{}
for _, argoCluster := range clustersFromArgoCD {
if argoCluster.Name == strMatchValue {
log.WithField(matchKey, argoCluster.Name).Info("matched cluster in ArgoCD")
return &argoCluster
log.Infof("cluster: %v", cluster)
matchValue := cluster.(map[string]interface{})[matchKey]
if matchValue == nil || matchValue.(string) == "" {
log.Warningf("matchKey=%v not found in \"%v\" list: %v\n", matchKey, statusListKey, cluster.(map[string]interface{}))
continue
}
strMatchValue := matchValue.(string)
log.WithField(matchKey, strMatchValue).Debug("validate against ArgoCD")
found := false
for _, argoCluster := range argoClusters {
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
}
}
if !found {
log.WithField(matchKey, strMatchValue).Warning("unmatched cluster in ArgoCD")
continue
}
for key, value := range cluster.(map[string]interface{}) {
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[fmt.Sprintf("values.%s", key)] = value
}
}
res = append(res, params)
}
}
log.WithField(matchKey, strMatchValue).Warning("unmatched cluster in ArgoCD")
return nil
}
func collectParams(appSet *argoprojiov1alpha1.ApplicationSet, params map[string]any, key string, value string) {
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
log.Warningf("clusterDecisionResource status." + statusListKey + " missing")
return nil, nil
}
return res, nil
}

View File

@@ -1,7 +1,8 @@
package generators
import (
"errors"
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
@@ -15,11 +16,11 @@ import (
kubefake "k8s.io/client-go/kubernetes/fake"
"sigs.k8s.io/controller-runtime/pkg/client"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
const (
resourceAPIVersion = "mallard.io/v1"
resourceApiVersion = "mallard.io/v1"
resourceKind = "ducks"
resourceName = "quak"
)
@@ -77,20 +78,20 @@ func TestGenerateParamsForDuckType(t *testing.T) {
}
duckType := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": resourceAPIVersion,
Object: map[string]interface{}{
"apiVersion": resourceApiVersion,
"kind": "Duck",
"metadata": map[string]any{
"metadata": map[string]interface{}{
"name": resourceName,
"namespace": "namespace",
"labels": map[string]any{"duck": "all-species"},
"labels": map[string]interface{}{"duck": "all-species"},
},
"status": map[string]any{
"decisions": []any{
map[string]any{
"status": map[string]interface{}{
"decisions": []interface{}{
map[string]interface{}{
"clusterName": "staging-01",
},
map[string]any{
map[string]interface{}{
"clusterName": "production-01",
},
},
@@ -99,17 +100,17 @@ func TestGenerateParamsForDuckType(t *testing.T) {
}
duckTypeProdOnly := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": resourceAPIVersion,
Object: map[string]interface{}{
"apiVersion": resourceApiVersion,
"kind": "Duck",
"metadata": map[string]any{
"metadata": map[string]interface{}{
"name": resourceName,
"namespace": "namespace",
"labels": map[string]any{"duck": "spotted"},
"labels": map[string]interface{}{"duck": "spotted"},
},
"status": map[string]any{
"decisions": []any{
map[string]any{
"status": map[string]interface{}{
"decisions": []interface{}{
map[string]interface{}{
"clusterName": "production-01",
},
},
@@ -118,15 +119,15 @@ func TestGenerateParamsForDuckType(t *testing.T) {
}
duckTypeEmpty := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": resourceAPIVersion,
Object: map[string]interface{}{
"apiVersion": resourceApiVersion,
"kind": "Duck",
"metadata": map[string]any{
"metadata": map[string]interface{}{
"name": resourceName,
"namespace": "namespace",
"labels": map[string]any{"duck": "canvasback"},
"labels": map[string]interface{}{"duck": "canvasback"},
},
"status": map[string]any{},
"status": map[string]interface{}{},
},
}
@@ -136,7 +137,7 @@ func TestGenerateParamsForDuckType(t *testing.T) {
Namespace: "namespace",
},
Data: map[string]string{
"apiVersion": resourceAPIVersion,
"apiVersion": resourceApiVersion,
"kind": resourceKind,
"statusListKey": "decisions",
"matchKey": "clusterName",
@@ -150,7 +151,7 @@ func TestGenerateParamsForDuckType(t *testing.T) {
labelSelector metav1.LabelSelector
resource *unstructured.Unstructured
values map[string]string
expected []map[string]any
expected []map[string]interface{}
expectedError error
}{
{
@@ -158,8 +159,8 @@ func TestGenerateParamsForDuckType(t *testing.T) {
resourceName: "",
resource: duckType,
values: nil,
expected: []map[string]any{},
expectedError: errors.New("there is a problem with the definition of the ClusterDecisionResource generator"),
expected: []map[string]interface{}{},
expectedError: fmt.Errorf("There is a problem with the definition of the ClusterDecisionResource generator"),
},
/*** This does not work with the FAKE runtime client, fieldSelectors are broken.
{
@@ -176,7 +177,7 @@ func TestGenerateParamsForDuckType(t *testing.T) {
resourceName: resourceName,
resource: duckType,
values: nil,
expected: []map[string]any{
expected: []map[string]interface{}{
{"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com"},
{"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com"},
@@ -190,7 +191,7 @@ func TestGenerateParamsForDuckType(t *testing.T) {
values: map[string]string{
"foo": "bar",
},
expected: []map[string]any{
expected: []map[string]interface{}{
{"clusterName": "production-01", "values.foo": "bar", "name": "production-01", "server": "https://production-01.example.com"},
},
expectedError: nil,
@@ -218,7 +219,7 @@ func TestGenerateParamsForDuckType(t *testing.T) {
labelSelector: metav1.LabelSelector{MatchLabels: map[string]string{"duck": "all-species"}},
resource: duckType,
values: nil,
expected: []map[string]any{
expected: []map[string]interface{}{
{"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com"},
{"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com"},
@@ -233,7 +234,7 @@ func TestGenerateParamsForDuckType(t *testing.T) {
values: map[string]string{
"foo": "bar",
},
expected: []map[string]any{
expected: []map[string]interface{}{
{"clusterName": "production-01", "values.foo": "bar", "name": "production-01", "server": "https://production-01.example.com"},
},
expectedError: nil,
@@ -250,7 +251,7 @@ func TestGenerateParamsForDuckType(t *testing.T) {
}},
resource: duckType,
values: nil,
expected: []map[string]any{
expected: []map[string]interface{}{
{"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com"},
{"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com"},
@@ -270,7 +271,7 @@ func TestGenerateParamsForDuckType(t *testing.T) {
resource: duckType,
values: nil,
expected: nil,
expectedError: errors.New("there is a problem with the definition of the ClusterDecisionResource generator"),
expectedError: fmt.Errorf("There is a problem with the definition of the ClusterDecisionResource generator"),
},
}
@@ -292,7 +293,7 @@ func TestGenerateParamsForDuckType(t *testing.T) {
fakeDynClient := dynfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), gvrToListKind, testCase.resource)
duckTypeGenerator := NewDuckTypeGenerator(t.Context(), fakeDynClient, appClientset, "namespace")
duckTypeGenerator := NewDuckTypeGenerator(context.Background(), fakeDynClient, appClientset, "namespace")
applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
@@ -373,20 +374,20 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
}
duckType := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": resourceAPIVersion,
Object: map[string]interface{}{
"apiVersion": resourceApiVersion,
"kind": "Duck",
"metadata": map[string]any{
"metadata": map[string]interface{}{
"name": resourceName,
"namespace": "namespace",
"labels": map[string]any{"duck": "all-species"},
"labels": map[string]interface{}{"duck": "all-species"},
},
"status": map[string]any{
"decisions": []any{
map[string]any{
"status": map[string]interface{}{
"decisions": []interface{}{
map[string]interface{}{
"clusterName": "staging-01",
},
map[string]any{
map[string]interface{}{
"clusterName": "production-01",
},
},
@@ -395,17 +396,17 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
}
duckTypeProdOnly := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": resourceAPIVersion,
Object: map[string]interface{}{
"apiVersion": resourceApiVersion,
"kind": "Duck",
"metadata": map[string]any{
"metadata": map[string]interface{}{
"name": resourceName,
"namespace": "namespace",
"labels": map[string]any{"duck": "spotted"},
"labels": map[string]interface{}{"duck": "spotted"},
},
"status": map[string]any{
"decisions": []any{
map[string]any{
"status": map[string]interface{}{
"decisions": []interface{}{
map[string]interface{}{
"clusterName": "production-01",
},
},
@@ -414,15 +415,15 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
}
duckTypeEmpty := &unstructured.Unstructured{
Object: map[string]any{
"apiVersion": resourceAPIVersion,
Object: map[string]interface{}{
"apiVersion": resourceApiVersion,
"kind": "Duck",
"metadata": map[string]any{
"metadata": map[string]interface{}{
"name": resourceName,
"namespace": "namespace",
"labels": map[string]any{"duck": "canvasback"},
"labels": map[string]interface{}{"duck": "canvasback"},
},
"status": map[string]any{},
"status": map[string]interface{}{},
},
}
@@ -432,7 +433,7 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
Namespace: "namespace",
},
Data: map[string]string{
"apiVersion": resourceAPIVersion,
"apiVersion": resourceApiVersion,
"kind": resourceKind,
"statusListKey": "decisions",
"matchKey": "clusterName",
@@ -446,7 +447,7 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
labelSelector metav1.LabelSelector
resource *unstructured.Unstructured
values map[string]string
expected []map[string]any
expected []map[string]interface{}
expectedError error
}{
{
@@ -454,8 +455,8 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
resourceName: "",
resource: duckType,
values: nil,
expected: []map[string]any{},
expectedError: errors.New("there is a problem with the definition of the ClusterDecisionResource generator"),
expected: []map[string]interface{}{},
expectedError: fmt.Errorf("There is a problem with the definition of the ClusterDecisionResource generator"),
},
/*** This does not work with the FAKE runtime client, fieldSelectors are broken.
{
@@ -472,7 +473,7 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
resourceName: resourceName,
resource: duckType,
values: nil,
expected: []map[string]any{
expected: []map[string]interface{}{
{"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com"},
{"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com"},
@@ -486,7 +487,7 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
values: map[string]string{
"foo": "bar",
},
expected: []map[string]any{
expected: []map[string]interface{}{
{"clusterName": "production-01", "values": map[string]string{"foo": "bar"}, "name": "production-01", "server": "https://production-01.example.com"},
},
expectedError: nil,
@@ -514,7 +515,7 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
labelSelector: metav1.LabelSelector{MatchLabels: map[string]string{"duck": "all-species"}},
resource: duckType,
values: nil,
expected: []map[string]any{
expected: []map[string]interface{}{
{"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com"},
{"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com"},
@@ -529,7 +530,7 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
values: map[string]string{
"foo": "bar",
},
expected: []map[string]any{
expected: []map[string]interface{}{
{"clusterName": "production-01", "values": map[string]string{"foo": "bar"}, "name": "production-01", "server": "https://production-01.example.com"},
},
expectedError: nil,
@@ -546,7 +547,7 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
}},
resource: duckType,
values: nil,
expected: []map[string]any{
expected: []map[string]interface{}{
{"clusterName": "production-01", "name": "production-01", "server": "https://production-01.example.com"},
{"clusterName": "staging-01", "name": "staging-01", "server": "https://staging-01.example.com"},
@@ -566,7 +567,7 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
resource: duckType,
values: nil,
expected: nil,
expectedError: errors.New("there is a problem with the definition of the ClusterDecisionResource generator"),
expectedError: fmt.Errorf("There is a problem with the definition of the ClusterDecisionResource generator"),
},
}
@@ -588,7 +589,7 @@ func TestGenerateParamsForDuckTypeGoTemplate(t *testing.T) {
fakeDynClient := dynfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), gvrToListKind, testCase.resource)
duckTypeGenerator := NewDuckTypeGenerator(t.Context(), fakeDynClient, appClientset, "namespace")
duckTypeGenerator := NewDuckTypeGenerator(context.Background(), fakeDynClient, appClientset, "namespace")
applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{

View File

@@ -7,13 +7,13 @@ import (
"github.com/jeremywohl/flatten"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/argoproj/argo-cd/v3/applicationset/utils"
"github.com/argoproj/argo-cd/v2/applicationset/utils"
"k8s.io/apimachinery/pkg/labels"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"dario.cat/mergo"
"github.com/imdario/mergo"
log "github.com/sirupsen/logrus"
)
@@ -22,12 +22,12 @@ const (
)
type TransformResult struct {
Params []map[string]any
Params []map[string]interface{}
Template argoprojiov1alpha1.ApplicationSetTemplate
}
// Transform a spec generator to list of paramSets and a template
func Transform(requestedGenerator argoprojiov1alpha1.ApplicationSetGenerator, allGenerators map[string]Generator, baseTemplate argoprojiov1alpha1.ApplicationSetTemplate, appSet *argoprojiov1alpha1.ApplicationSet, genParams map[string]any, client client.Client) ([]TransformResult, error) {
func Transform(requestedGenerator argoprojiov1alpha1.ApplicationSetGenerator, allGenerators map[string]Generator, baseTemplate argoprojiov1alpha1.ApplicationSetTemplate, appSet *argoprojiov1alpha1.ApplicationSet, genParams map[string]interface{}, client client.Client) ([]TransformResult, error) {
// This is a custom version of the `LabelSelectorAsSelector` that is in k8s.io/apimachinery. This has been copied
// verbatim from that package, with the difference that we do not have any restrictions on label values. This is done
// so that, among other things, we can match on cluster urls.
@@ -52,7 +52,7 @@ func Transform(requestedGenerator argoprojiov1alpha1.ApplicationSetGenerator, al
}
continue
}
var params []map[string]any
var params []map[string]interface{}
if len(genParams) != 0 {
tempInterpolatedGenerator, err := InterpolateGenerator(&requestedGenerator, genParams, appSet.Spec.GoTemplate, appSet.Spec.GoTemplateOptions)
interpolatedGenerator = &tempInterpolatedGenerator
@@ -74,7 +74,7 @@ func Transform(requestedGenerator argoprojiov1alpha1.ApplicationSetGenerator, al
}
continue
}
var filterParams []map[string]any
var filterParams []map[string]interface{}
for _, param := range params {
flatParam, err := flattenParameters(param)
if err != nil {
@@ -123,7 +123,7 @@ func GetRelevantGenerators(requestedGenerator *argoprojiov1alpha1.ApplicationSet
return res
}
func flattenParameters(in map[string]any) (map[string]string, error) {
func flattenParameters(in map[string]interface{}) (map[string]string, error) {
flat, err := flatten.Flatten(in, "", flatten.DotStyle)
if err != nil {
return nil, fmt.Errorf("error flatenning parameters: %w", err)
@@ -149,7 +149,7 @@ func mergeGeneratorTemplate(g Generator, requestedGenerator *argoprojiov1alpha1.
// InterpolateGenerator allows interpolating the matrix's 2nd child generator with values from the 1st child generator
// "params" parameter is an array, where each index corresponds to a generator. Each index contains a map w/ that generator's parameters.
func InterpolateGenerator(requestedGenerator *argoprojiov1alpha1.ApplicationSetGenerator, params map[string]any, useGoTemplate bool, goTemplateOptions []string) (argoprojiov1alpha1.ApplicationSetGenerator, error) {
func InterpolateGenerator(requestedGenerator *argoprojiov1alpha1.ApplicationSetGenerator, params map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (argoprojiov1alpha1.ApplicationSetGenerator, error) {
render := utils.Render{}
interpolatedGenerator, err := render.RenderGeneratorParams(requestedGenerator, params, useGoTemplate, goTemplateOptions)
if err != nil {
@@ -159,3 +159,16 @@ func InterpolateGenerator(requestedGenerator *argoprojiov1alpha1.ApplicationSetG
return *interpolatedGenerator, nil
}
// Fixes https://github.com/argoproj/argo-cd/issues/11982 while ensuring backwards compatibility.
// This is only a short-term solution and should be removed in a future major version.
func dropDisabledNestedSelectors(generators []argoprojiov1alpha1.ApplicationSetNestedGenerator) bool {
var foundSelector bool
for i := range generators {
if generators[i].Selector != nil {
foundSelector = true
generators[i].Selector = nil
}
}
return foundSelector
}

View File

@@ -10,9 +10,9 @@ import (
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/argoproj/argo-cd/v3/applicationset/services/mocks"
"github.com/argoproj/argo-cd/v2/applicationset/services/mocks"
argov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/stretchr/testify/mock"
corev1 "k8s.io/api/core/v1"
@@ -27,19 +27,19 @@ func TestMatchValues(t *testing.T) {
name string
elements []apiextensionsv1.JSON
selector *metav1.LabelSelector
expected []map[string]any
expected []map[string]interface{}
}{
{
name: "no filter",
elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url"}`)}},
selector: &metav1.LabelSelector{},
expected: []map[string]any{{"cluster": "cluster", "url": "url"}},
expected: []map[string]interface{}{{"cluster": "cluster", "url": "url"}},
},
{
name: "nil",
elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url"}`)}},
selector: nil,
expected: []map[string]any{{"cluster": "cluster", "url": "url"}},
expected: []map[string]interface{}{{"cluster": "cluster", "url": "url"}},
},
{
name: "values.foo should be foo but is ignore element",
@@ -49,7 +49,7 @@ func TestMatchValues(t *testing.T) {
"values.foo": "foo",
},
},
expected: []map[string]any{},
expected: []map[string]interface{}{},
},
{
name: "values.foo should be bar",
@@ -59,7 +59,7 @@ func TestMatchValues(t *testing.T) {
"values.foo": "bar",
},
},
expected: []map[string]any{{"cluster": "cluster", "url": "url", "values.foo": "bar"}},
expected: []map[string]interface{}{{"cluster": "cluster", "url": "url", "values.foo": "bar"}},
},
}
@@ -101,19 +101,19 @@ func TestMatchValuesGoTemplate(t *testing.T) {
name string
elements []apiextensionsv1.JSON
selector *metav1.LabelSelector
expected []map[string]any
expected []map[string]interface{}
}{
{
name: "no filter",
elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url"}`)}},
selector: &metav1.LabelSelector{},
expected: []map[string]any{{"cluster": "cluster", "url": "url"}},
expected: []map[string]interface{}{{"cluster": "cluster", "url": "url"}},
},
{
name: "nil",
elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url"}`)}},
selector: nil,
expected: []map[string]any{{"cluster": "cluster", "url": "url"}},
expected: []map[string]interface{}{{"cluster": "cluster", "url": "url"}},
},
{
name: "values.foo should be foo but is ignore element",
@@ -123,7 +123,7 @@ func TestMatchValuesGoTemplate(t *testing.T) {
"values.foo": "foo",
},
},
expected: []map[string]any{},
expected: []map[string]interface{}{},
},
{
name: "values.foo should be bar",
@@ -133,7 +133,7 @@ func TestMatchValuesGoTemplate(t *testing.T) {
"values.foo": "bar",
},
},
expected: []map[string]any{{"cluster": "cluster", "url": "url", "values": map[string]any{"foo": "bar"}}},
expected: []map[string]interface{}{{"cluster": "cluster", "url": "url", "values": map[string]interface{}{"foo": "bar"}}},
},
{
name: "values.0 should be bar",
@@ -143,7 +143,7 @@ func TestMatchValuesGoTemplate(t *testing.T) {
"values.0": "bar",
},
},
expected: []map[string]any{{"cluster": "cluster", "url": "url", "values": []any{"bar"}}},
expected: []map[string]interface{}{{"cluster": "cluster", "url": "url", "values": []interface{}{"bar"}}},
},
}
@@ -184,14 +184,14 @@ func TestTransForm(t *testing.T) {
testCases := []struct {
name string
selector *metav1.LabelSelector
expected []map[string]any
expected []map[string]interface{}
}{
{
name: "server filter",
selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"server": "https://production-01.example.com"},
},
expected: []map[string]any{{
expected: []map[string]interface{}{{
"metadata.annotations.foo.argoproj.io": "production",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster",
"metadata.labels.environment": "production",
@@ -199,7 +199,6 @@ func TestTransForm(t *testing.T) {
"name": "production_01/west",
"nameNormalized": "production-01-west",
"server": "https://production-01.example.com",
"project": "",
}},
},
{
@@ -207,7 +206,7 @@ func TestTransForm(t *testing.T) {
selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"server": "https://some-really-long-url-that-will-exceed-63-characters.com"},
},
expected: []map[string]any{{
expected: []map[string]interface{}{{
"metadata.annotations.foo.argoproj.io": "production",
"metadata.labels.argocd.argoproj.io/secret-type": "cluster",
"metadata.labels.environment": "production",
@@ -215,7 +214,6 @@ func TestTransForm(t *testing.T) {
"name": "some-really-long-server-url",
"nameNormalized": "some-really-long-server-url",
"server": "https://some-really-long-url-that-will-exceed-63-characters.com",
"project": "",
}},
},
}
@@ -342,7 +340,7 @@ func getMockClusterGenerator() Generator {
appClientset := kubefake.NewSimpleClientset(runtimeClusters...)
fakeClient := fake.NewClientBuilder().WithObjects(clusters...).Build()
return NewClusterGenerator(context.Background(), fakeClient, appClientset, "namespace")
return NewClusterGenerator(fakeClient, context.Background(), appClientset, "namespace")
}
func getMockGitGenerator() Generator {
@@ -413,7 +411,7 @@ func TestInterpolateGenerator(t *testing.T) {
},
},
}
gitGeneratorParams := map[string]any{
gitGeneratorParams := map[string]interface{}{
"path": "p1/p2/app3",
"path.basename": "app3",
"path[0]": "p1",
@@ -442,7 +440,7 @@ func TestInterpolateGenerator(t *testing.T) {
Template: argov1alpha1.ApplicationSetTemplate{},
},
}
clusterGeneratorParams := map[string]any{
clusterGeneratorParams := map[string]interface{}{
"name": "production_01/west", "server": "https://production-01.example.com",
}
interpolatedGenerator, err = InterpolateGenerator(requestedGenerator, clusterGeneratorParams, false, nil)
@@ -468,8 +466,8 @@ func TestInterpolateGenerator_go(t *testing.T) {
},
},
}
gitGeneratorParams := map[string]any{
"path": map[string]any{
gitGeneratorParams := map[string]interface{}{
"path": map[string]interface{}{
"path": "p1/p2/app3",
"segments": []string{"p1", "p2", "app3"},
},
@@ -497,7 +495,7 @@ func TestInterpolateGenerator_go(t *testing.T) {
Template: argov1alpha1.ApplicationSetTemplate{},
},
}
clusterGeneratorParams := map[string]any{
clusterGeneratorParams := map[string]interface{}{
"name": "production_01/west", "server": "https://production-01.example.com",
}
interpolatedGenerator, err = InterpolateGenerator(requestedGenerator, clusterGeneratorParams, true, nil)
@@ -512,7 +510,7 @@ func TestInterpolateGenerator_go(t *testing.T) {
func TestInterpolateGeneratorError(t *testing.T) {
type args struct {
requestedGenerator *argov1alpha1.ApplicationSetGenerator
params map[string]any
params map[string]interface{}
useGoTemplate bool
goTemplateOptions []string
}
@@ -530,7 +528,7 @@ func TestInterpolateGeneratorError(t *testing.T) {
}, want: argov1alpha1.ApplicationSetGenerator{}, expectedErrStr: "generator is empty"},
{name: "No Params", args: args{
requestedGenerator: &argov1alpha1.ApplicationSetGenerator{},
params: map[string]any{},
params: map[string]interface{}{},
useGoTemplate: false,
goTemplateOptions: nil,
}, want: argov1alpha1.ApplicationSetGenerator{}, expectedErrStr: ""},
@@ -545,7 +543,7 @@ func TestInterpolateGeneratorError(t *testing.T) {
"resolved": "{{ index .rmap (default .override .test) }}",
},
}},
params: map[string]any{
params: map[string]interface{}{
"name": "in-cluster",
"override": "foo",
},

View File

@@ -15,10 +15,10 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
"github.com/argoproj/argo-cd/v3/applicationset/services"
"github.com/argoproj/argo-cd/v3/applicationset/utils"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/util/gpg"
"github.com/argoproj/argo-cd/v2/applicationset/services"
"github.com/argoproj/argo-cd/v2/applicationset/utils"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/util/gpg"
)
var _ Generator = (*GitGenerator)(nil)
@@ -28,7 +28,6 @@ type GitGenerator struct {
namespace string
}
// NewGitGenerator creates a new instance of Git Generator
func NewGitGenerator(repos services.Repos, namespace string) Generator {
g := &GitGenerator{
repos: repos,
@@ -38,33 +37,27 @@ func NewGitGenerator(repos services.Repos, namespace string) Generator {
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 {
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 {
// Return a requeue default of 3 minutes, if no default is specified.
if appSetGenerator.Git.RequeueAfterSeconds != nil {
return time.Duration(*appSetGenerator.Git.RequeueAfterSeconds) * time.Second
}
return getDefaultRequeueAfter()
return DefaultRequeueAfterSeconds
}
// 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]interface{}, error) {
if appSetGenerator == nil {
return nil, ErrEmptyAppSetGenerator
return nil, EmptyAppSetGeneratorError
}
if appSetGenerator.Git == nil {
return nil, ErrEmptyAppSetGenerator
return nil, EmptyAppSetGeneratorError
}
noRevisionCache := appSet.RefreshRequired()
@@ -74,7 +67,6 @@ 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,
// but git generator cannot be called without verifying the commit signature.
// 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, "{{") {
project := appSet.Spec.Template.Spec.Project
appProject := &argoprojiov1alpha1.AppProject{}
@@ -86,22 +78,17 @@ func (g *GitGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.Applic
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
verifyCommit = len(appProject.Spec.SignatureKeys) > 0 && gpg.IsGPGEnabled()
verifyCommit = appProject.Spec.SignatureKeys != nil && len(appProject.Spec.SignatureKeys) > 0 && gpg.IsGPGEnabled()
}
// If the project field is templated, we cannot resolve the project name, so we pass an empty string to the repo-server.
// This means only "globally-scoped" repo credentials can be used for such appsets.
project := resolveProjectName(appSet.Spec.Template.Spec.Project)
var err error
var res []map[string]any
switch {
case len(appSetGenerator.Git.Directories) != 0:
res, err = g.generateParamsForGitDirectories(appSetGenerator, noRevisionCache, verifyCommit, appSet.Spec.GoTemplate, project, appSet.Spec.GoTemplateOptions)
case len(appSetGenerator.Git.Files) != 0:
res, err = g.generateParamsForGitFiles(appSetGenerator, noRevisionCache, verifyCommit, appSet.Spec.GoTemplate, project, appSet.Spec.GoTemplateOptions)
default:
return nil, ErrEmptyAppSetGenerator
var res []map[string]interface{}
if len(appSetGenerator.Git.Directories) != 0 {
res, err = g.generateParamsForGitDirectories(appSetGenerator, noRevisionCache, verifyCommit, appSet.Spec.GoTemplate, appSet.Spec.GoTemplateOptions)
} else if len(appSetGenerator.Git.Files) != 0 {
res, err = g.generateParamsForGitFiles(appSetGenerator, noRevisionCache, verifyCommit, appSet.Spec.GoTemplate, appSet.Spec.GoTemplateOptions)
} else {
return nil, EmptyAppSetGeneratorError
}
if err != nil {
return nil, fmt.Errorf("error generating params from git: %w", err)
@@ -110,11 +97,9 @@ func (g *GitGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.Applic
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) {
allPaths, err := g.repos.GetDirectories(context.TODO(), appSetGenerator.Git.RepoURL, appSetGenerator.Git.Revision, project, noRevisionCache, verifyCommit)
func (g *GitGenerator) generateParamsForGitDirectories(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, noRevisionCache, verifyCommit bool, useGoTemplate bool, goTemplateOptions []string) ([]map[string]interface{}, error) {
// Directories, not files
allPaths, err := g.repos.GetDirectories(context.TODO(), appSetGenerator.Git.RepoURL, appSetGenerator.Git.Revision, noRevisionCache, verifyCommit)
if err != nil {
return nil, fmt.Errorf("error getting directories from repo: %w", err)
}
@@ -137,96 +122,49 @@ func (g *GitGenerator) generateParamsForGitDirectories(appSetGenerator *argoproj
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) {
// fileContentMap maps absolute file paths to their byte content
fileContentMap := make(map[string][]byte)
var includePatterns []string
var excludePatterns []string
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,
)
func (g *GitGenerator) generateParamsForGitFiles(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, noRevisionCache, verifyCommit bool, useGoTemplate bool, goTemplateOptions []string) ([]map[string]interface{}, error) {
// Get all files that match the requested path string, removing duplicates
allFiles := make(map[string][]byte)
for _, requestedPath := range appSetGenerator.Git.Files {
files, err := g.repos.GetFiles(context.TODO(), appSetGenerator.Git.RepoURL, appSetGenerator.Git.Revision, requestedPath.Path, noRevisionCache, verifyCommit)
if err != nil {
return nil, err
}
for absPath, content := range retrievedFiles {
fileContentMap[absPath] = content
for filePath, content := range files {
allFiles[filePath] = content
}
}
// Now remove files matching any exclude pattern
for _, excludePattern := range excludePatterns {
matchingFiles, err := g.repos.GetFiles(
context.TODO(),
appSetGenerator.Git.RepoURL,
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)
}
// Extract the unduplicated map into a list, and sort by path to ensure a deterministic
// processing order in the subsequent step
allPaths := []string{}
for path := range allFiles {
allPaths = append(allPaths, path)
}
sort.Strings(allPaths)
// Get a sorted list of file paths to ensure deterministic processing order
var filePaths []string
for path := range fileContentMap {
filePaths = append(filePaths, path)
}
sort.Strings(filePaths)
var allParams []map[string]any
for _, filePath := range filePaths {
// Generate params from each path, and return
res := []map[string]interface{}{}
for _, path := range allPaths {
// 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 {
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) {
objectsFound := []map[string]any{}
func (g *GitGenerator) generateParamsFromGitFile(filePath string, fileContent []byte, values map[string]string, useGoTemplate bool, goTemplateOptions []string, pathParamPrefix string) ([]map[string]interface{}, error) {
objectsFound := []map[string]interface{}{}
// First, we attempt to parse as an array
err := yaml.Unmarshal(fileContent, &objectsFound)
if err != nil {
// If unable to parse as an array, attempt to parse as a single object
singleObj := make(map[string]any)
singleObj := make(map[string]interface{})
err = yaml.Unmarshal(fileContent, &singleObj)
if err != nil {
return nil, fmt.Errorf("unable to parse file: %w", err)
@@ -234,20 +172,20 @@ func (g *GitGenerator) generateParamsFromGitFile(filePath string, fileContent []
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{})
objectsFound = append(objectsFound, map[string]interface{}{})
}
res := []map[string]any{}
res := []map[string]interface{}{}
for _, objectFound := range objectsFound {
params := map[string]any{}
params := map[string]interface{}{}
if useGoTemplate {
for k, v := range objectFound {
params[k] = v
}
paramPath := map[string]any{}
paramPath := map[string]interface{}{}
paramPath["path"] = path.Dir(filePath)
paramPath["basename"] = path.Base(paramPath["path"].(string))
@@ -256,7 +194,7 @@ func (g *GitGenerator) generateParamsFromGitFile(filePath string, fileContent []
paramPath["filenameNormalized"] = utils.SanitizeName(path.Base(paramPath["filename"].(string)))
paramPath["segments"] = strings.Split(paramPath["path"].(string), "/")
if pathParamPrefix != "" {
params[pathParamPrefix] = map[string]any{"path": paramPath}
params[pathParamPrefix] = map[string]interface{}{"path": paramPath}
} else {
params["path"] = paramPath
}
@@ -278,7 +216,7 @@ func (g *GitGenerator) generateParamsFromGitFile(filePath string, fileContent []
params[pathParamName+".basenameNormalized"] = utils.SanitizeName(path.Base(params[pathParamName].(string)))
params[pathParamName+".filenameNormalized"] = utils.SanitizeName(path.Base(params[pathParamName+".filename"].(string)))
for k, v := range strings.Split(params[pathParamName].(string), "/") {
if v != "" {
if len(v) > 0 {
params[pathParamName+"["+strconv.Itoa(k)+"]"] = v
}
}
@@ -295,10 +233,8 @@ func (g *GitGenerator) generateParamsFromGitFile(filePath string, fileContent []
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 {
var res []string
res := []string{}
for _, appPath := range allPaths {
appInclude := false
appExclude := false
@@ -325,22 +261,19 @@ func (g *GitGenerator) filterApps(directories []argoprojiov1alpha1.GitDirectoryG
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) {
res := make([]map[string]any, len(requestedApps))
func (g *GitGenerator) generateParamsFromApps(requestedApps []string, appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, useGoTemplate bool, goTemplateOptions []string) ([]map[string]interface{}, error) {
res := make([]map[string]interface{}, len(requestedApps))
for i, a := range requestedApps {
params := make(map[string]any, 5)
params := make(map[string]interface{}, 5)
if useGoTemplate {
paramPath := map[string]any{}
paramPath := map[string]interface{}{}
paramPath["path"] = a
paramPath["basename"] = path.Base(a)
paramPath["basenameNormalized"] = utils.SanitizeName(path.Base(a))
paramPath["segments"] = strings.Split(paramPath["path"].(string), "/")
if appSetGenerator.Git.PathParamPrefix != "" {
params[appSetGenerator.Git.PathParamPrefix] = map[string]any{"path": paramPath}
params[appSetGenerator.Git.PathParamPrefix] = map[string]interface{}{"path": paramPath}
} else {
params["path"] = paramPath
}
@@ -353,7 +286,7 @@ func (g *GitGenerator) generateParamsFromApps(requestedApps []string, appSetGene
params[pathParamName+".basename"] = path.Base(a)
params[pathParamName+".basenameNormalized"] = utils.SanitizeName(path.Base(a))
for k, v := range strings.Split(params[pathParamName].(string), "/") {
if v != "" {
if len(v) > 0 {
params[pathParamName+"["+strconv.Itoa(k)+"]"] = v
}
}
@@ -369,12 +302,3 @@ func (g *GitGenerator) generateParamsFromApps(requestedApps []string, appSetGene
return res, nil
}
// resolveProjectName resolves a project name whether templated or not
func resolveProjectName(project string) string {
if strings.Contains(project, "{{") {
return ""
}
return project
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,22 @@
package generators
import (
"errors"
"fmt"
"time"
"sigs.k8s.io/controller-runtime/pkg/client"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/util/env"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
//go:generate go run github.com/vektra/mockery/v2@v2.40.2 --name=Generator
// Generator defines the interface implemented by all ApplicationSet generators.
type Generator interface {
// GenerateParams interprets the ApplicationSet and generates all relevant parameters for the application template.
// The expected / desired list of parameters is returned, it then will be render and reconciled
// against the current state of the Applications in the cluster.
GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet, client client.Client) ([]map[string]any, error)
GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, applicationSetInfo *argoprojiov1alpha1.ApplicationSet, client client.Client) ([]map[string]interface{}, error)
// GetRequeueAfter is the generator can controller the next reconciled loop
// In case there is more then one generator the time will be the minimum of the times.
@@ -27,15 +28,11 @@ type Generator interface {
}
var (
ErrEmptyAppSetGenerator = errors.New("ApplicationSet is empty")
NoRequeueAfter time.Duration
EmptyAppSetGeneratorError = fmt.Errorf("ApplicationSet is empty")
NoRequeueAfter time.Duration
)
// DefaultRequeueAfterSeconds is used when GetRequeueAfter is not specified, it is the default time to wait before the next reconcile loop
const (
DefaultRequeueAfter = 3 * time.Minute
DefaultRequeueAfterSeconds = 3 * time.Minute
)
func getDefaultRequeueAfter() time.Duration {
// 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)
}

View File

@@ -1,29 +0,0 @@
package generators
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func Test_getDefaultRequeueAfter(t *testing.T) {
tests := []struct {
name string
requeueAfterEnv string
want time.Duration
}{
{name: "Default", requeueAfterEnv: "", want: DefaultRequeueAfter},
{name: "Min", requeueAfterEnv: "1s", want: 1 * time.Second},
{name: "Max", requeueAfterEnv: "8760h", want: 8760 * time.Hour},
{name: "Override", requeueAfterEnv: "10m", want: 10 * time.Minute},
{name: "LessThanMin", requeueAfterEnv: "1ms", want: DefaultRequeueAfter},
{name: "MoreThanMax", requeueAfterEnv: "8761h", want: DefaultRequeueAfter},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("ARGOCD_APPLICATIONSET_CONTROLLER_REQUEUE_AFTER", tt.requeueAfterEnv)
assert.Equalf(t, tt.want, getDefaultRequeueAfter(), "getDefaultRequeueAfter()")
})
}
}

View File

@@ -2,14 +2,13 @@ package generators
import (
"encoding/json"
"errors"
"fmt"
"time"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/yaml"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
var _ Generator = (*ListGenerator)(nil)
@@ -21,7 +20,7 @@ func NewListGenerator() Generator {
return g
}
func (g *ListGenerator) GetRequeueAfter(_ *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration {
func (g *ListGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration {
return NoRequeueAfter
}
@@ -29,20 +28,20 @@ func (g *ListGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.Applicat
return &appSetGenerator.List.Template
}
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]interface{}, error) {
if appSetGenerator == nil {
return nil, ErrEmptyAppSetGenerator
return nil, EmptyAppSetGeneratorError
}
if appSetGenerator.List == nil {
return nil, ErrEmptyAppSetGenerator
return nil, EmptyAppSetGeneratorError
}
res := make([]map[string]any, len(appSetGenerator.List.Elements))
res := make([]map[string]interface{}, len(appSetGenerator.List.Elements))
for i, tmpItem := range appSetGenerator.List.Elements {
params := map[string]any{}
var element map[string]any
params := map[string]interface{}{}
var element map[string]interface{}
err := json.Unmarshal(tmpItem.Raw, &element)
if err != nil {
return nil, fmt.Errorf("error unmarshling list element %w", err)
@@ -53,16 +52,16 @@ func (g *ListGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.Appli
} else {
for key, value := range element {
if key == "values" {
values, ok := (value).(map[string]any)
values, ok := (value).(map[string]interface{})
if !ok {
return nil, errors.New("error parsing values map")
return nil, fmt.Errorf("error parsing values map")
}
for k, v := range values {
value, ok := v.(string)
if !ok {
return nil, fmt.Errorf("error parsing value as string %w", err)
}
params["values."+k] = value
params[fmt.Sprintf("values.%s", k)] = value
}
} else {
v, ok := value.(string)
@@ -77,8 +76,8 @@ func (g *ListGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.Appli
}
// Append elements from ElementsYaml to the response
if appSetGenerator.List.ElementsYaml != "" {
var yamlElements []map[string]any
if len(appSetGenerator.List.ElementsYaml) > 0 {
var yamlElements []map[string]interface{}
err := yaml.Unmarshal([]byte(appSetGenerator.List.ElementsYaml), &yamlElements)
if err != nil {
return nil, fmt.Errorf("error unmarshling decoded ElementsYaml %w", err)

View File

@@ -8,20 +8,20 @@ import (
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
func TestGenerateListParams(t *testing.T) {
testCases := []struct {
elements []apiextensionsv1.JSON
expected []map[string]any
expected []map[string]interface{}
}{
{
elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url"}`)}},
expected: []map[string]any{{"cluster": "cluster", "url": "url"}},
expected: []map[string]interface{}{{"cluster": "cluster", "url": "url"}},
}, {
elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url","values":{"foo":"bar"}}`)}},
expected: []map[string]any{{"cluster": "cluster", "url": "url", "values.foo": "bar"}},
expected: []map[string]interface{}{{"cluster": "cluster", "url": "url", "values.foo": "bar"}},
},
}
@@ -49,14 +49,14 @@ func TestGenerateListParams(t *testing.T) {
func TestGenerateListParamsGoTemplate(t *testing.T) {
testCases := []struct {
elements []apiextensionsv1.JSON
expected []map[string]any
expected []map[string]interface{}
}{
{
elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url"}`)}},
expected: []map[string]any{{"cluster": "cluster", "url": "url"}},
expected: []map[string]interface{}{{"cluster": "cluster", "url": "url"}},
}, {
elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "cluster","url": "url","values":{"foo":"bar"}}`)}},
expected: []map[string]any{{"cluster": "cluster", "url": "url", "values": map[string]any{"foo": "bar"}}},
expected: []map[string]interface{}{{"cluster": "cluster", "url": "url", "values": map[string]interface{}{"foo": "bar"}}},
},
}

View File

@@ -1,23 +1,24 @@
package generators
import (
"errors"
"fmt"
"time"
"dario.cat/mergo"
"github.com/imdario/mergo"
"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"
"github.com/argoproj/argo-cd/v2/applicationset/utils"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
log "github.com/sirupsen/logrus"
)
var _ Generator = (*MatrixGenerator)(nil)
var (
ErrMoreThanTwoGenerators = errors.New("found more than two generators, Matrix support only two")
ErrLessThanTwoGenerators = errors.New("found less than two generators, Matrix support only two")
ErrMoreThenOneInnerGenerators = errors.New("found more than one generator in matrix.Generators")
ErrMoreThanTwoGenerators = fmt.Errorf("found more than two generators, Matrix support only two")
ErrLessThanTwoGenerators = fmt.Errorf("found less than two generators, Matrix support only two")
ErrMoreThenOneInnerGenerators = fmt.Errorf("found more than one generator in matrix.Generators")
)
type MatrixGenerator struct {
@@ -32,9 +33,9 @@ func NewMatrixGenerator(supportedGenerators map[string]Generator) Generator {
return m
}
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]interface{}, error) {
if appSetGenerator.Matrix == nil {
return nil, ErrEmptyAppSetGenerator
return nil, EmptyAppSetGeneratorError
}
if len(appSetGenerator.Matrix.Generators) < 2 {
@@ -45,7 +46,7 @@ func (m *MatrixGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.App
return nil, ErrMoreThanTwoGenerators
}
res := []map[string]any{}
res := []map[string]interface{}{}
g0, err := m.getParams(appSetGenerator.Matrix.Generators[0], appSet, nil, client)
if err != nil {
@@ -58,7 +59,7 @@ func (m *MatrixGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.App
}
for _, b := range g1 {
if appSet.Spec.GoTemplate {
tmp := map[string]any{}
tmp := map[string]interface{}{}
if err := mergo.Merge(&tmp, b, mergo.WithOverride); err != nil {
return nil, fmt.Errorf("failed to merge params from the second generator in the matrix generator with temp map: %w", err)
}
@@ -71,7 +72,7 @@ func (m *MatrixGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.App
if err != nil {
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))
}
}
}
@@ -79,15 +80,27 @@ func (m *MatrixGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.App
return res, nil
}
func (m *MatrixGenerator) getParams(appSetBaseGenerator argoprojiov1alpha1.ApplicationSetNestedGenerator, appSet *argoprojiov1alpha1.ApplicationSet, params map[string]any, client client.Client) ([]map[string]any, error) {
func (m *MatrixGenerator) getParams(appSetBaseGenerator argoprojiov1alpha1.ApplicationSetNestedGenerator, appSet *argoprojiov1alpha1.ApplicationSet, params map[string]interface{}, client client.Client) ([]map[string]interface{}, error) {
matrixGen, err := getMatrixGenerator(appSetBaseGenerator)
if err != nil {
return nil, err
}
if matrixGen != nil && !appSet.Spec.ApplyNestedSelectors {
foundSelector := dropDisabledNestedSelectors(matrixGen.Generators)
if foundSelector {
log.Warnf("AppSet '%v' defines selector on nested matrix generator's generator without enabling them via 'spec.applyNestedSelectors', ignoring nested selectors", appSet.Name)
}
}
mergeGen, err := getMergeGenerator(appSetBaseGenerator)
if err != nil {
return nil, fmt.Errorf("error retrieving merge generator: %w", err)
}
if mergeGen != nil && !appSet.Spec.ApplyNestedSelectors {
foundSelector := dropDisabledNestedSelectors(mergeGen.Generators)
if foundSelector {
log.Warnf("AppSet '%v' defines selector on nested merge generator's generator without enabling them via 'spec.applyNestedSelectors', ignoring nested selectors", appSet.Name)
}
}
t, err := Transform(
argoprojiov1alpha1.ApplicationSetGenerator{
@@ -112,7 +125,7 @@ func (m *MatrixGenerator) getParams(appSetBaseGenerator argoprojiov1alpha1.Appli
}
if len(t) == 0 {
return nil, errors.New("child generator generated no parameters")
return nil, fmt.Errorf("child generator generated no parameters")
}
if len(t) > 1 {
@@ -155,8 +168,9 @@ func (m *MatrixGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.Ap
if found {
return res
} else {
return NoRequeueAfter
}
return NoRequeueAfter
}
func getMatrixGenerator(r argoprojiov1alpha1.ApplicationSetNestedGenerator) (*argoprojiov1alpha1.MatrixGenerator, error) {

View File

@@ -1,6 +1,7 @@
package generators
import (
"context"
"testing"
"time"
@@ -12,35 +13,36 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/argoproj/argo-cd/v3/applicationset/services/mocks"
"github.com/argoproj/argo-cd/v2/applicationset/services/mocks"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
func TestMatrixGenerate(t *testing.T) {
gitGenerator := &v1alpha1.GitGenerator{
gitGenerator := &argoprojiov1alpha1.GitGenerator{
RepoURL: "RepoURL",
Revision: "Revision",
Directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
Directories: []argoprojiov1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
}
listGenerator := &v1alpha1.ListGenerator{
listGenerator := &argoprojiov1alpha1.ListGenerator{
Elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "Cluster","url": "Url", "templated": "test-{{path.basenameNormalized}}"}`)}},
}
testCases := []struct {
name string
baseGenerators []v1alpha1.ApplicationSetNestedGenerator
baseGenerators []argoprojiov1alpha1.ApplicationSetNestedGenerator
expectedErr error
expected []map[string]any
expected []map[string]interface{}
}{
{
name: "happy flow - generate params",
baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
Git: gitGenerator,
},
@@ -48,16 +50,16 @@ func TestMatrixGenerate(t *testing.T) {
List: listGenerator,
},
},
expected: []map[string]any{
expected: []map[string]interface{}{
{"path": "app1", "path.basename": "app1", "path.basenameNormalized": "app1", "cluster": "Cluster", "url": "Url", "templated": "test-app1"},
{"path": "app2", "path.basename": "app2", "path.basenameNormalized": "app2", "cluster": "Cluster", "url": "Url", "templated": "test-app2"},
},
},
{
name: "happy flow - generate params from two lists",
baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
List: &v1alpha1.ListGenerator{
List: &argoprojiov1alpha1.ListGenerator{
Elements: []apiextensionsv1.JSON{
{Raw: []byte(`{"a": "1"}`)},
{Raw: []byte(`{"a": "2"}`)},
@@ -65,7 +67,7 @@ func TestMatrixGenerate(t *testing.T) {
},
},
{
List: &v1alpha1.ListGenerator{
List: &argoprojiov1alpha1.ListGenerator{
Elements: []apiextensionsv1.JSON{
{Raw: []byte(`{"b": "1"}`)},
{Raw: []byte(`{"b": "2"}`)},
@@ -73,7 +75,7 @@ func TestMatrixGenerate(t *testing.T) {
},
},
},
expected: []map[string]any{
expected: []map[string]interface{}{
{"a": "1", "b": "1"},
{"a": "1", "b": "2"},
{"a": "2", "b": "1"},
@@ -82,7 +84,7 @@ func TestMatrixGenerate(t *testing.T) {
},
{
name: "returns error if there is less than two base generators",
baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
Git: gitGenerator,
},
@@ -91,7 +93,7 @@ func TestMatrixGenerate(t *testing.T) {
},
{
name: "returns error if there is more than two base generators",
baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
List: listGenerator,
},
@@ -106,7 +108,7 @@ func TestMatrixGenerate(t *testing.T) {
},
{
name: "returns error if there is more than one inner generator in the first base generator",
baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
Git: gitGenerator,
List: listGenerator,
@@ -119,7 +121,7 @@ func TestMatrixGenerate(t *testing.T) {
},
{
name: "returns error if there is more than one inner generator in the second base generator",
baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
List: listGenerator,
},
@@ -137,19 +139,19 @@ func TestMatrixGenerate(t *testing.T) {
t.Run(testCaseCopy.name, func(t *testing.T) {
genMock := &generatorMock{}
appSet := &v1alpha1.ApplicationSet{
appSet := &argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "set",
},
Spec: v1alpha1.ApplicationSetSpec{},
Spec: argoprojiov1alpha1.ApplicationSetSpec{},
}
for _, g := range testCaseCopy.baseGenerators {
gitGeneratorSpec := v1alpha1.ApplicationSetGenerator{
gitGeneratorSpec := argoprojiov1alpha1.ApplicationSetGenerator{
Git: g.Git,
List: g.List,
}
genMock.On("GenerateParams", mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), appSet, mock.Anything).Return([]map[string]any{
genMock.On("GenerateParams", mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), appSet, mock.Anything).Return([]map[string]interface{}{
{
"path": "app1",
"path.basename": "app1",
@@ -163,7 +165,7 @@ func TestMatrixGenerate(t *testing.T) {
}, nil)
genMock.On("GetTemplate", &gitGeneratorSpec).
Return(&v1alpha1.ApplicationSetTemplate{})
Return(&argoprojiov1alpha1.ApplicationSetTemplate{})
}
matrixGenerator := NewMatrixGenerator(
@@ -173,10 +175,10 @@ func TestMatrixGenerate(t *testing.T) {
},
)
got, err := matrixGenerator.GenerateParams(&v1alpha1.ApplicationSetGenerator{
Matrix: &v1alpha1.MatrixGenerator{
got, err := matrixGenerator.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{
Matrix: &argoprojiov1alpha1.MatrixGenerator{
Generators: testCaseCopy.baseGenerators,
Template: v1alpha1.ApplicationSetTemplate{},
Template: argoprojiov1alpha1.ApplicationSetTemplate{},
},
}, appSet, nil)
@@ -191,25 +193,25 @@ func TestMatrixGenerate(t *testing.T) {
}
func TestMatrixGenerateGoTemplate(t *testing.T) {
gitGenerator := &v1alpha1.GitGenerator{
gitGenerator := &argoprojiov1alpha1.GitGenerator{
RepoURL: "RepoURL",
Revision: "Revision",
Directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
Directories: []argoprojiov1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
}
listGenerator := &v1alpha1.ListGenerator{
listGenerator := &argoprojiov1alpha1.ListGenerator{
Elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "Cluster","url": "Url"}`)}},
}
testCases := []struct {
name string
baseGenerators []v1alpha1.ApplicationSetNestedGenerator
baseGenerators []argoprojiov1alpha1.ApplicationSetNestedGenerator
expectedErr error
expected []map[string]any
expected []map[string]interface{}
}{
{
name: "happy flow - generate params",
baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
Git: gitGenerator,
},
@@ -217,7 +219,7 @@ func TestMatrixGenerateGoTemplate(t *testing.T) {
List: listGenerator,
},
},
expected: []map[string]any{
expected: []map[string]interface{}{
{
"path": map[string]string{
"path": "app1",
@@ -240,9 +242,9 @@ func TestMatrixGenerateGoTemplate(t *testing.T) {
},
{
name: "happy flow - generate params from two lists",
baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
List: &v1alpha1.ListGenerator{
List: &argoprojiov1alpha1.ListGenerator{
Elements: []apiextensionsv1.JSON{
{Raw: []byte(`{"a": "1"}`)},
{Raw: []byte(`{"a": "2"}`)},
@@ -250,7 +252,7 @@ func TestMatrixGenerateGoTemplate(t *testing.T) {
},
},
{
List: &v1alpha1.ListGenerator{
List: &argoprojiov1alpha1.ListGenerator{
Elements: []apiextensionsv1.JSON{
{Raw: []byte(`{"b": "1"}`)},
{Raw: []byte(`{"b": "2"}`)},
@@ -258,7 +260,7 @@ func TestMatrixGenerateGoTemplate(t *testing.T) {
},
},
},
expected: []map[string]any{
expected: []map[string]interface{}{
{"a": "1", "b": "1"},
{"a": "1", "b": "2"},
{"a": "2", "b": "1"},
@@ -267,29 +269,29 @@ func TestMatrixGenerateGoTemplate(t *testing.T) {
},
{
name: "parameter override: first list elements take precedence",
baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
List: &v1alpha1.ListGenerator{
List: &argoprojiov1alpha1.ListGenerator{
Elements: []apiextensionsv1.JSON{
{Raw: []byte(`{"booleanFalse": false, "booleanTrue": true, "stringFalse": "false", "stringTrue": "true"}`)},
},
},
},
{
List: &v1alpha1.ListGenerator{
List: &argoprojiov1alpha1.ListGenerator{
Elements: []apiextensionsv1.JSON{
{Raw: []byte(`{"booleanFalse": true, "booleanTrue": false, "stringFalse": "true", "stringTrue": "false"}`)},
},
},
},
},
expected: []map[string]any{
expected: []map[string]interface{}{
{"booleanFalse": false, "booleanTrue": true, "stringFalse": "false", "stringTrue": "true"},
},
},
{
name: "returns error if there is less than two base generators",
baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
Git: gitGenerator,
},
@@ -298,7 +300,7 @@ func TestMatrixGenerateGoTemplate(t *testing.T) {
},
{
name: "returns error if there is more than two base generators",
baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
List: listGenerator,
},
@@ -313,7 +315,7 @@ func TestMatrixGenerateGoTemplate(t *testing.T) {
},
{
name: "returns error if there is more than one inner generator in the first base generator",
baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
Git: gitGenerator,
List: listGenerator,
@@ -326,7 +328,7 @@ func TestMatrixGenerateGoTemplate(t *testing.T) {
},
{
name: "returns error if there is more than one inner generator in the second base generator",
baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
List: listGenerator,
},
@@ -344,21 +346,21 @@ func TestMatrixGenerateGoTemplate(t *testing.T) {
t.Run(testCaseCopy.name, func(t *testing.T) {
genMock := &generatorMock{}
appSet := &v1alpha1.ApplicationSet{
appSet := &argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "set",
},
Spec: v1alpha1.ApplicationSetSpec{
Spec: argoprojiov1alpha1.ApplicationSetSpec{
GoTemplate: true,
},
}
for _, g := range testCaseCopy.baseGenerators {
gitGeneratorSpec := v1alpha1.ApplicationSetGenerator{
gitGeneratorSpec := argoprojiov1alpha1.ApplicationSetGenerator{
Git: g.Git,
List: g.List,
}
genMock.On("GenerateParams", mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), appSet, mock.Anything).Return([]map[string]any{
genMock.On("GenerateParams", mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), appSet, mock.Anything).Return([]map[string]interface{}{
{
"path": map[string]string{
"path": "app1",
@@ -376,7 +378,7 @@ func TestMatrixGenerateGoTemplate(t *testing.T) {
}, nil)
genMock.On("GetTemplate", &gitGeneratorSpec).
Return(&v1alpha1.ApplicationSetTemplate{})
Return(&argoprojiov1alpha1.ApplicationSetTemplate{})
}
matrixGenerator := NewMatrixGenerator(
@@ -386,10 +388,10 @@ func TestMatrixGenerateGoTemplate(t *testing.T) {
},
)
got, err := matrixGenerator.GenerateParams(&v1alpha1.ApplicationSetGenerator{
Matrix: &v1alpha1.MatrixGenerator{
got, err := matrixGenerator.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{
Matrix: &argoprojiov1alpha1.MatrixGenerator{
Generators: testCaseCopy.baseGenerators,
Template: v1alpha1.ApplicationSetTemplate{},
Template: argoprojiov1alpha1.ApplicationSetTemplate{},
},
}, appSet, nil)
@@ -404,31 +406,31 @@ func TestMatrixGenerateGoTemplate(t *testing.T) {
}
func TestMatrixGetRequeueAfter(t *testing.T) {
gitGenerator := &v1alpha1.GitGenerator{
gitGenerator := &argoprojiov1alpha1.GitGenerator{
RepoURL: "RepoURL",
Revision: "Revision",
Directories: []v1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
Directories: []argoprojiov1alpha1.GitDirectoryGeneratorItem{{Path: "*"}},
}
listGenerator := &v1alpha1.ListGenerator{
listGenerator := &argoprojiov1alpha1.ListGenerator{
Elements: []apiextensionsv1.JSON{{Raw: []byte(`{"cluster": "Cluster","url": "Url"}`)}},
}
pullRequestGenerator := &v1alpha1.PullRequestGenerator{}
pullRequestGenerator := &argoprojiov1alpha1.PullRequestGenerator{}
scmGenerator := &v1alpha1.SCMProviderGenerator{}
scmGenerator := &argoprojiov1alpha1.SCMProviderGenerator{}
duckTypeGenerator := &v1alpha1.DuckTypeGenerator{}
duckTypeGenerator := &argoprojiov1alpha1.DuckTypeGenerator{}
testCases := []struct {
name string
baseGenerators []v1alpha1.ApplicationSetNestedGenerator
baseGenerators []argoprojiov1alpha1.ApplicationSetNestedGenerator
gitGetRequeueAfter time.Duration
expected time.Duration
}{
{
name: "return NoRequeueAfter if all the inner baseGenerators returns it",
baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
Git: gitGenerator,
},
@@ -441,7 +443,7 @@ func TestMatrixGetRequeueAfter(t *testing.T) {
},
{
name: "returns the minimal time",
baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
Git: gitGenerator,
},
@@ -454,7 +456,7 @@ func TestMatrixGetRequeueAfter(t *testing.T) {
},
{
name: "returns the minimal time for pull request",
baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
Git: gitGenerator,
},
@@ -467,7 +469,7 @@ func TestMatrixGetRequeueAfter(t *testing.T) {
},
{
name: "returns the default time if no requeueAfterSeconds is provided",
baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
Git: gitGenerator,
},
@@ -479,7 +481,7 @@ func TestMatrixGetRequeueAfter(t *testing.T) {
},
{
name: "returns the default time for duck type generator",
baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
Git: gitGenerator,
},
@@ -491,7 +493,7 @@ func TestMatrixGetRequeueAfter(t *testing.T) {
},
{
name: "returns the default time for scm generator",
baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
Git: gitGenerator,
},
@@ -510,7 +512,7 @@ func TestMatrixGetRequeueAfter(t *testing.T) {
mock := &generatorMock{}
for _, g := range testCaseCopy.baseGenerators {
gitGeneratorSpec := v1alpha1.ApplicationSetGenerator{
gitGeneratorSpec := argoprojiov1alpha1.ApplicationSetGenerator{
Git: g.Git,
List: g.List,
PullRequest: g.PullRequest,
@@ -530,10 +532,10 @@ func TestMatrixGetRequeueAfter(t *testing.T) {
},
)
got := matrixGenerator.GetRequeueAfter(&v1alpha1.ApplicationSetGenerator{
Matrix: &v1alpha1.MatrixGenerator{
got := matrixGenerator.GetRequeueAfter(&argoprojiov1alpha1.ApplicationSetGenerator{
Matrix: &argoprojiov1alpha1.MatrixGenerator{
Generators: testCaseCopy.baseGenerators,
Template: v1alpha1.ApplicationSetTemplate{},
Template: argoprojiov1alpha1.ApplicationSetTemplate{},
},
})
@@ -543,16 +545,16 @@ func TestMatrixGetRequeueAfter(t *testing.T) {
}
func TestInterpolatedMatrixGenerate(t *testing.T) {
interpolatedGitGenerator := &v1alpha1.GitGenerator{
interpolatedGitGenerator := &argoprojiov1alpha1.GitGenerator{
RepoURL: "RepoURL",
Revision: "Revision",
Files: []v1alpha1.GitFileGeneratorItem{
Files: []argoprojiov1alpha1.GitFileGeneratorItem{
{Path: "examples/git-generator-files-discovery/cluster-config/dev/config.json"},
{Path: "examples/git-generator-files-discovery/cluster-config/prod/config.json"},
},
}
interpolatedClusterGenerator := &v1alpha1.ClusterGenerator{
interpolatedClusterGenerator := &argoprojiov1alpha1.ClusterGenerator{
Selector: metav1.LabelSelector{
MatchLabels: map[string]string{"environment": "{{path.basename}}"},
MatchExpressions: nil,
@@ -560,14 +562,14 @@ func TestInterpolatedMatrixGenerate(t *testing.T) {
}
testCases := []struct {
name string
baseGenerators []v1alpha1.ApplicationSetNestedGenerator
baseGenerators []argoprojiov1alpha1.ApplicationSetNestedGenerator
expectedErr error
expected []map[string]any
expected []map[string]interface{}
clientError bool
}{
{
name: "happy flow - generate interpolated params",
baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
Git: interpolatedGitGenerator,
},
@@ -575,9 +577,9 @@ func TestInterpolatedMatrixGenerate(t *testing.T) {
Clusters: interpolatedClusterGenerator,
},
},
expected: []map[string]any{
{"path": "examples/git-generator-files-discovery/cluster-config/dev/config.json", "path.basename": "dev", "path.basenameNormalized": "dev", "name": "dev-01", "nameNormalized": "dev-01", "server": "https://dev-01.example.com", "metadata.labels.environment": "dev", "metadata.labels.argocd.argoproj.io/secret-type": "cluster", "project": ""},
{"path": "examples/git-generator-files-discovery/cluster-config/prod/config.json", "path.basename": "prod", "path.basenameNormalized": "prod", "name": "prod-01", "nameNormalized": "prod-01", "server": "https://prod-01.example.com", "metadata.labels.environment": "prod", "metadata.labels.argocd.argoproj.io/secret-type": "cluster", "project": ""},
expected: []map[string]interface{}{
{"path": "examples/git-generator-files-discovery/cluster-config/dev/config.json", "path.basename": "dev", "path.basenameNormalized": "dev", "name": "dev-01", "nameNormalized": "dev-01", "server": "https://dev-01.example.com", "metadata.labels.environment": "dev", "metadata.labels.argocd.argoproj.io/secret-type": "cluster"},
{"path": "examples/git-generator-files-discovery/cluster-config/prod/config.json", "path.basename": "prod", "path.basenameNormalized": "prod", "name": "prod-01", "nameNormalized": "prod-01", "server": "https://prod-01.example.com", "metadata.labels.environment": "prod", "metadata.labels.argocd.argoproj.io/secret-type": "cluster"},
},
clientError: false,
},
@@ -635,7 +637,7 @@ func TestInterpolatedMatrixGenerate(t *testing.T) {
t.Run(testCaseCopy.name, func(t *testing.T) {
genMock := &generatorMock{}
appSet := &v1alpha1.ApplicationSet{}
appSet := &argoprojiov1alpha1.ApplicationSet{}
appClientset := kubefake.NewSimpleClientset(runtimeClusters...)
fakeClient := fake.NewClientBuilder().WithObjects(clusters...).Build()
@@ -643,14 +645,14 @@ func TestInterpolatedMatrixGenerate(t *testing.T) {
fakeClient,
testCase.clientError,
}
clusterGenerator := NewClusterGenerator(t.Context(), cl, appClientset, "namespace")
clusterGenerator := NewClusterGenerator(cl, context.Background(), appClientset, "namespace")
for _, g := range testCaseCopy.baseGenerators {
gitGeneratorSpec := v1alpha1.ApplicationSetGenerator{
gitGeneratorSpec := argoprojiov1alpha1.ApplicationSetGenerator{
Git: g.Git,
Clusters: g.Clusters,
}
genMock.On("GenerateParams", mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), appSet).Return([]map[string]any{
genMock.On("GenerateParams", mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), appSet).Return([]map[string]interface{}{
{
"path": "examples/git-generator-files-discovery/cluster-config/dev/config.json",
"path.basename": "dev",
@@ -663,7 +665,7 @@ func TestInterpolatedMatrixGenerate(t *testing.T) {
},
}, nil)
genMock.On("GetTemplate", &gitGeneratorSpec).
Return(&v1alpha1.ApplicationSetTemplate{})
Return(&argoprojiov1alpha1.ApplicationSetTemplate{})
}
matrixGenerator := NewMatrixGenerator(
map[string]Generator{
@@ -672,10 +674,10 @@ func TestInterpolatedMatrixGenerate(t *testing.T) {
},
)
got, err := matrixGenerator.GenerateParams(&v1alpha1.ApplicationSetGenerator{
Matrix: &v1alpha1.MatrixGenerator{
got, err := matrixGenerator.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{
Matrix: &argoprojiov1alpha1.MatrixGenerator{
Generators: testCaseCopy.baseGenerators,
Template: v1alpha1.ApplicationSetTemplate{},
Template: argoprojiov1alpha1.ApplicationSetTemplate{},
},
}, appSet, nil)
@@ -690,16 +692,16 @@ func TestInterpolatedMatrixGenerate(t *testing.T) {
}
func TestInterpolatedMatrixGenerateGoTemplate(t *testing.T) {
interpolatedGitGenerator := &v1alpha1.GitGenerator{
interpolatedGitGenerator := &argoprojiov1alpha1.GitGenerator{
RepoURL: "RepoURL",
Revision: "Revision",
Files: []v1alpha1.GitFileGeneratorItem{
Files: []argoprojiov1alpha1.GitFileGeneratorItem{
{Path: "examples/git-generator-files-discovery/cluster-config/dev/config.json"},
{Path: "examples/git-generator-files-discovery/cluster-config/prod/config.json"},
},
}
interpolatedClusterGenerator := &v1alpha1.ClusterGenerator{
interpolatedClusterGenerator := &argoprojiov1alpha1.ClusterGenerator{
Selector: metav1.LabelSelector{
MatchLabels: map[string]string{"environment": "{{.path.basename}}"},
MatchExpressions: nil,
@@ -707,14 +709,14 @@ func TestInterpolatedMatrixGenerateGoTemplate(t *testing.T) {
}
testCases := []struct {
name string
baseGenerators []v1alpha1.ApplicationSetNestedGenerator
baseGenerators []argoprojiov1alpha1.ApplicationSetNestedGenerator
expectedErr error
expected []map[string]any
expected []map[string]interface{}
clientError bool
}{
{
name: "happy flow - generate interpolated params",
baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
Git: interpolatedGitGenerator,
},
@@ -722,7 +724,7 @@ func TestInterpolatedMatrixGenerateGoTemplate(t *testing.T) {
Clusters: interpolatedClusterGenerator,
},
},
expected: []map[string]any{
expected: []map[string]interface{}{
{
"path": map[string]string{
"path": "examples/git-generator-files-discovery/cluster-config/dev/config.json",
@@ -732,8 +734,7 @@ func TestInterpolatedMatrixGenerateGoTemplate(t *testing.T) {
"name": "dev-01",
"nameNormalized": "dev-01",
"server": "https://dev-01.example.com",
"project": "",
"metadata": map[string]any{
"metadata": map[string]interface{}{
"labels": map[string]string{
"environment": "dev",
"argocd.argoproj.io/secret-type": "cluster",
@@ -749,8 +750,7 @@ func TestInterpolatedMatrixGenerateGoTemplate(t *testing.T) {
"name": "prod-01",
"nameNormalized": "prod-01",
"server": "https://prod-01.example.com",
"project": "",
"metadata": map[string]any{
"metadata": map[string]interface{}{
"labels": map[string]string{
"environment": "prod",
"argocd.argoproj.io/secret-type": "cluster",
@@ -814,8 +814,8 @@ func TestInterpolatedMatrixGenerateGoTemplate(t *testing.T) {
t.Run(testCaseCopy.name, func(t *testing.T) {
genMock := &generatorMock{}
appSet := &v1alpha1.ApplicationSet{
Spec: v1alpha1.ApplicationSetSpec{
appSet := &argoprojiov1alpha1.ApplicationSet{
Spec: argoprojiov1alpha1.ApplicationSetSpec{
GoTemplate: true,
},
}
@@ -826,14 +826,14 @@ func TestInterpolatedMatrixGenerateGoTemplate(t *testing.T) {
fakeClient,
testCase.clientError,
}
clusterGenerator := NewClusterGenerator(t.Context(), cl, appClientset, "namespace")
clusterGenerator := NewClusterGenerator(cl, context.Background(), appClientset, "namespace")
for _, g := range testCaseCopy.baseGenerators {
gitGeneratorSpec := v1alpha1.ApplicationSetGenerator{
gitGeneratorSpec := argoprojiov1alpha1.ApplicationSetGenerator{
Git: g.Git,
Clusters: g.Clusters,
}
genMock.On("GenerateParams", mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), appSet).Return([]map[string]any{
genMock.On("GenerateParams", mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), appSet).Return([]map[string]interface{}{
{
"path": map[string]string{
"path": "examples/git-generator-files-discovery/cluster-config/dev/config.json",
@@ -850,7 +850,7 @@ func TestInterpolatedMatrixGenerateGoTemplate(t *testing.T) {
},
}, nil)
genMock.On("GetTemplate", &gitGeneratorSpec).
Return(&v1alpha1.ApplicationSetTemplate{})
Return(&argoprojiov1alpha1.ApplicationSetTemplate{})
}
matrixGenerator := NewMatrixGenerator(
map[string]Generator{
@@ -859,10 +859,10 @@ func TestInterpolatedMatrixGenerateGoTemplate(t *testing.T) {
},
)
got, err := matrixGenerator.GenerateParams(&v1alpha1.ApplicationSetGenerator{
Matrix: &v1alpha1.MatrixGenerator{
got, err := matrixGenerator.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{
Matrix: &argoprojiov1alpha1.MatrixGenerator{
Generators: testCaseCopy.baseGenerators,
Template: v1alpha1.ApplicationSetTemplate{},
Template: argoprojiov1alpha1.ApplicationSetTemplate{},
},
}, appSet, nil)
@@ -877,28 +877,28 @@ func TestInterpolatedMatrixGenerateGoTemplate(t *testing.T) {
}
func TestMatrixGenerateListElementsYaml(t *testing.T) {
gitGenerator := &v1alpha1.GitGenerator{
gitGenerator := &argoprojiov1alpha1.GitGenerator{
RepoURL: "RepoURL",
Revision: "Revision",
Files: []v1alpha1.GitFileGeneratorItem{
Files: []argoprojiov1alpha1.GitFileGeneratorItem{
{Path: "config.yaml"},
},
}
listGenerator := &v1alpha1.ListGenerator{
listGenerator := &argoprojiov1alpha1.ListGenerator{
Elements: []apiextensionsv1.JSON{},
ElementsYaml: "{{ .foo.bar | toJson }}",
}
testCases := []struct {
name string
baseGenerators []v1alpha1.ApplicationSetNestedGenerator
baseGenerators []argoprojiov1alpha1.ApplicationSetNestedGenerator
expectedErr error
expected []map[string]any
expected []map[string]interface{}
}{
{
name: "happy flow - generate params",
baseGenerators: []v1alpha1.ApplicationSetNestedGenerator{
baseGenerators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
Git: gitGenerator,
},
@@ -906,23 +906,23 @@ func TestMatrixGenerateListElementsYaml(t *testing.T) {
List: listGenerator,
},
},
expected: []map[string]any{
expected: []map[string]interface{}{
{
"chart": "a",
"version": "1",
"foo": map[string]any{
"bar": []any{
map[string]any{
"foo": map[string]interface{}{
"bar": []interface{}{
map[string]interface{}{
"chart": "a",
"version": "1",
},
map[string]any{
map[string]interface{}{
"chart": "b",
"version": "2",
},
},
},
"path": map[string]any{
"path": map[string]interface{}{
"basename": "dir",
"basenameNormalized": "dir",
"filename": "file_name.yaml",
@@ -937,19 +937,19 @@ func TestMatrixGenerateListElementsYaml(t *testing.T) {
{
"chart": "b",
"version": "2",
"foo": map[string]any{
"bar": []any{
map[string]any{
"foo": map[string]interface{}{
"bar": []interface{}{
map[string]interface{}{
"chart": "a",
"version": "1",
},
map[string]any{
map[string]interface{}{
"chart": "b",
"version": "2",
},
},
},
"path": map[string]any{
"path": map[string]interface{}{
"basename": "dir",
"basenameNormalized": "dir",
"filename": "file_name.yaml",
@@ -970,34 +970,34 @@ func TestMatrixGenerateListElementsYaml(t *testing.T) {
t.Run(testCaseCopy.name, func(t *testing.T) {
genMock := &generatorMock{}
appSet := &v1alpha1.ApplicationSet{
appSet := &argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "set",
},
Spec: v1alpha1.ApplicationSetSpec{
Spec: argoprojiov1alpha1.ApplicationSetSpec{
GoTemplate: true,
},
}
for _, g := range testCaseCopy.baseGenerators {
gitGeneratorSpec := v1alpha1.ApplicationSetGenerator{
gitGeneratorSpec := argoprojiov1alpha1.ApplicationSetGenerator{
Git: g.Git,
List: g.List,
}
genMock.On("GenerateParams", mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), appSet).Return([]map[string]any{{
"foo": map[string]any{
"bar": []any{
map[string]any{
"foo": map[string]interface{}{
"bar": []interface{}{
map[string]interface{}{
"chart": "a",
"version": "1",
},
map[string]any{
map[string]interface{}{
"chart": "b",
"version": "2",
},
},
},
"path": map[string]any{
"path": map[string]interface{}{
"basename": "dir",
"basenameNormalized": "dir",
"filename": "file_name.yaml",
@@ -1010,7 +1010,7 @@ func TestMatrixGenerateListElementsYaml(t *testing.T) {
},
}}, nil)
genMock.On("GetTemplate", &gitGeneratorSpec).
Return(&v1alpha1.ApplicationSetTemplate{})
Return(&argoprojiov1alpha1.ApplicationSetTemplate{})
}
matrixGenerator := NewMatrixGenerator(
@@ -1020,10 +1020,10 @@ func TestMatrixGenerateListElementsYaml(t *testing.T) {
},
)
got, err := matrixGenerator.GenerateParams(&v1alpha1.ApplicationSetGenerator{
Matrix: &v1alpha1.MatrixGenerator{
got, err := matrixGenerator.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{
Matrix: &argoprojiov1alpha1.MatrixGenerator{
Generators: testCaseCopy.baseGenerators,
Template: v1alpha1.ApplicationSetTemplate{},
Template: argoprojiov1alpha1.ApplicationSetTemplate{},
},
}, appSet, nil)
@@ -1041,19 +1041,19 @@ type generatorMock struct {
mock.Mock
}
func (g *generatorMock) GetTemplate(appSetGenerator *v1alpha1.ApplicationSetGenerator) *v1alpha1.ApplicationSetTemplate {
func (g *generatorMock) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate {
args := g.Called(appSetGenerator)
return args.Get(0).(*v1alpha1.ApplicationSetTemplate)
return args.Get(0).(*argoprojiov1alpha1.ApplicationSetTemplate)
}
func (g *generatorMock) GenerateParams(appSetGenerator *v1alpha1.ApplicationSetGenerator, appSet *v1alpha1.ApplicationSet, _ client.Client) ([]map[string]any, error) {
func (g *generatorMock) GenerateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, appSet *argoprojiov1alpha1.ApplicationSet, _ client.Client) ([]map[string]interface{}, error) {
args := g.Called(appSetGenerator, appSet)
return args.Get(0).([]map[string]any), args.Error(1)
return args.Get(0).([]map[string]interface{}), args.Error(1)
}
func (g *generatorMock) GetRequeueAfter(appSetGenerator *v1alpha1.ApplicationSetGenerator) time.Duration {
func (g *generatorMock) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration {
args := g.Called(appSetGenerator)
return args.Get(0).(time.Duration)
@@ -1073,20 +1073,20 @@ func TestGitGenerator_GenerateParams_list_x_git_matrix_generator(t *testing.T) {
// of that bug.
listGeneratorMock := &generatorMock{}
listGeneratorMock.On("GenerateParams", mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), mock.AnythingOfType("*v1alpha1.ApplicationSet"), mock.Anything).Return([]map[string]any{
listGeneratorMock.On("GenerateParams", mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), mock.AnythingOfType("*v1alpha1.ApplicationSet")).Return([]map[string]interface{}{
{"some": "value"},
}, nil)
listGeneratorMock.On("GetTemplate", mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator")).Return(&v1alpha1.ApplicationSetTemplate{})
listGeneratorMock.On("GetTemplate", mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator")).Return(&argoprojiov1alpha1.ApplicationSetTemplate{})
gitGeneratorSpec := &v1alpha1.GitGenerator{
gitGeneratorSpec := &argoprojiov1alpha1.GitGenerator{
RepoURL: "https://git.example.com",
Files: []v1alpha1.GitFileGeneratorItem{
Files: []argoprojiov1alpha1.GitFileGeneratorItem{
{Path: "some/path.json"},
},
}
repoServiceMock := &mocks.Repos{}
repoServiceMock.On("GetFiles", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(map[string][]byte{
repoServiceMock.On("GetFiles", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(map[string][]byte{
"some/path.json": []byte("test: content"),
}, nil)
gitGenerator := NewGitGenerator(repoServiceMock, "")
@@ -1096,10 +1096,10 @@ func TestGitGenerator_GenerateParams_list_x_git_matrix_generator(t *testing.T) {
"Git": gitGenerator,
})
matrixGeneratorSpec := &v1alpha1.MatrixGenerator{
Generators: []v1alpha1.ApplicationSetNestedGenerator{
matrixGeneratorSpec := &argoprojiov1alpha1.MatrixGenerator{
Generators: []argoprojiov1alpha1.ApplicationSetNestedGenerator{
{
List: &v1alpha1.ListGenerator{
List: &argoprojiov1alpha1.ListGenerator{
Elements: []apiextensionsv1.JSON{
{
Raw: []byte(`{"some": "value"}`),
@@ -1116,15 +1116,15 @@ func TestGitGenerator_GenerateParams_list_x_git_matrix_generator(t *testing.T) {
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
appProject := v1alpha1.AppProject{}
appProject := argoprojiov1alpha1.AppProject{}
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appProject).Build()
params, err := matrixGenerator.GenerateParams(&v1alpha1.ApplicationSetGenerator{
params, err := matrixGenerator.GenerateParams(&argoprojiov1alpha1.ApplicationSetGenerator{
Matrix: matrixGeneratorSpec,
}, &v1alpha1.ApplicationSet{}, client)
}, &argoprojiov1alpha1.ApplicationSet{}, client)
require.NoError(t, err)
assert.Equal(t, []map[string]any{{
assert.Equal(t, []map[string]interface{}{{
"path": "some",
"path.basename": "some",
"path.basenameNormalized": "some",

View File

@@ -2,23 +2,24 @@ package generators
import (
"encoding/json"
"errors"
"fmt"
"maps"
"time"
"dario.cat/mergo"
"github.com/imdario/mergo"
"sigs.k8s.io/controller-runtime/pkg/client"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/applicationset/utils"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
log "github.com/sirupsen/logrus"
)
var _ Generator = (*MergeGenerator)(nil)
var (
ErrLessThanTwoGeneratorsInMerge = errors.New("found less than two generators, Merge requires two or more")
ErrNoMergeKeys = errors.New("no merge keys were specified, Merge requires at least one")
ErrNonUniqueParamSets = errors.New("the parameters from a generator were not unique by the given mergeKeys, Merge requires all param sets to be unique")
ErrLessThanTwoGeneratorsInMerge = fmt.Errorf("found less than two generators, Merge requires two or more")
ErrNoMergeKeys = fmt.Errorf("no merge keys were specified, Merge requires at least one")
ErrNonUniqueParamSets = fmt.Errorf("the parameters from a generator were not unique by the given mergeKeys, Merge requires all param sets to be unique")
)
type MergeGenerator struct {
@@ -36,8 +37,8 @@ func NewMergeGenerator(supportedGenerators map[string]Generator) Generator {
// getParamSetsForAllGenerators generates params for each child generator in a MergeGenerator. Param sets are returned
// in slices ordered according to the order of the given generators.
func (m *MergeGenerator) getParamSetsForAllGenerators(generators []argoprojiov1alpha1.ApplicationSetNestedGenerator, appSet *argoprojiov1alpha1.ApplicationSet, client client.Client) ([][]map[string]any, error) {
var paramSets [][]map[string]any
func (m *MergeGenerator) getParamSetsForAllGenerators(generators []argoprojiov1alpha1.ApplicationSetNestedGenerator, appSet *argoprojiov1alpha1.ApplicationSet, client client.Client) ([][]map[string]interface{}, error) {
var paramSets [][]map[string]interface{}
for i, generator := range generators {
generatorParamSets, err := m.getParams(generator, appSet, client)
if err != nil {
@@ -50,9 +51,9 @@ func (m *MergeGenerator) getParamSetsForAllGenerators(generators []argoprojiov1a
}
// 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]interface{}, error) {
if appSetGenerator.Merge == nil {
return nil, ErrEmptyAppSetGenerator
return nil, EmptyAppSetGeneratorError
}
if len(appSetGenerator.Merge.Generators) < 2 {
@@ -83,18 +84,21 @@ func (m *MergeGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.Appl
}
baseParamSetsByMergeKey[mergeKeyValue] = baseParamSet
} else {
maps.Copy(baseParamSet, overrideParamSet)
baseParamSetsByMergeKey[mergeKeyValue] = baseParamSet
overriddenParamSet, err := utils.CombineStringMapsAllowDuplicates(baseParamSet, overrideParamSet)
if err != nil {
return nil, fmt.Errorf("error combining string maps: %w", err)
}
baseParamSetsByMergeKey[mergeKeyValue] = utils.ConvertToMapStringInterface(overriddenParamSet)
}
}
}
}
mergedParamSets := make([]map[string]any, len(baseParamSetsByMergeKey))
mergedParamSets := make([]map[string]interface{}, len(baseParamSetsByMergeKey))
i := 0
for _, mergedParamSet := range baseParamSetsByMergeKey {
mergedParamSets[i] = mergedParamSet
i++
i += 1
}
return mergedParamSets, nil
@@ -103,7 +107,7 @@ func (m *MergeGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha1.Appl
// getParamSetsByMergeKey converts the given list of parameter sets to a map of parameter sets where the key is the
// unique key of the parameter set as determined by the given mergeKeys. If any two parameter sets share the same merge
// key, getParamSetsByMergeKey will throw NonUniqueParamSets.
func getParamSetsByMergeKey(mergeKeys []string, paramSets []map[string]any) (map[string]map[string]any, error) {
func getParamSetsByMergeKey(mergeKeys []string, paramSets []map[string]interface{}) (map[string]map[string]interface{}, error) {
if len(mergeKeys) < 1 {
return nil, ErrNoMergeKeys
}
@@ -113,17 +117,17 @@ func getParamSetsByMergeKey(mergeKeys []string, paramSets []map[string]any) (map
deDuplicatedMergeKeys[mergeKey] = false
}
paramSetsByMergeKey := make(map[string]map[string]any, len(paramSets))
paramSetsByMergeKey := make(map[string]map[string]interface{}, len(paramSets))
for _, paramSet := range paramSets {
paramSetKey := make(map[string]any)
paramSetKey := make(map[string]interface{})
for mergeKey := range deDuplicatedMergeKeys {
paramSetKey[mergeKey] = paramSet[mergeKey]
}
paramSetKeyJSON, err := json.Marshal(paramSetKey)
paramSetKeyJson, err := json.Marshal(paramSetKey)
if err != nil {
return nil, fmt.Errorf("error marshalling param set key json: %w", err)
}
paramSetKeyString := string(paramSetKeyJSON)
paramSetKeyString := string(paramSetKeyJson)
if _, exists := paramSetsByMergeKey[paramSetKeyString]; exists {
return nil, fmt.Errorf("%w. Duplicate key was %s", ErrNonUniqueParamSets, paramSetKeyString)
}
@@ -134,15 +138,27 @@ func getParamSetsByMergeKey(mergeKeys []string, paramSets []map[string]any) (map
}
// getParams get the parameters generated by this generator.
func (m *MergeGenerator) getParams(appSetBaseGenerator argoprojiov1alpha1.ApplicationSetNestedGenerator, appSet *argoprojiov1alpha1.ApplicationSet, client client.Client) ([]map[string]any, error) {
func (m *MergeGenerator) getParams(appSetBaseGenerator argoprojiov1alpha1.ApplicationSetNestedGenerator, appSet *argoprojiov1alpha1.ApplicationSet, client client.Client) ([]map[string]interface{}, error) {
matrixGen, err := getMatrixGenerator(appSetBaseGenerator)
if err != nil {
return nil, err
}
if matrixGen != nil && !appSet.Spec.ApplyNestedSelectors {
foundSelector := dropDisabledNestedSelectors(matrixGen.Generators)
if foundSelector {
log.Warnf("AppSet '%v' defines selector on nested matrix generator's generator without enabling them via 'spec.applyNestedSelectors', ignoring nested selector", appSet.Name)
}
}
mergeGen, err := getMergeGenerator(appSetBaseGenerator)
if err != nil {
return nil, err
}
if mergeGen != nil && !appSet.Spec.ApplyNestedSelectors {
foundSelector := dropDisabledNestedSelectors(mergeGen.Generators)
if foundSelector {
log.Warnf("AppSet '%v' defines selector on nested merge generator's generator without enabling them via 'spec.applyNestedSelectors', ignoring nested selector", appSet.Name)
}
}
t, err := Transform(
argoprojiov1alpha1.ApplicationSetGenerator{
@@ -160,13 +176,13 @@ func (m *MergeGenerator) getParams(appSetBaseGenerator argoprojiov1alpha1.Applic
m.supportedGenerators,
argoprojiov1alpha1.ApplicationSetTemplate{},
appSet,
map[string]any{}, client)
map[string]interface{}{}, client)
if err != nil {
return nil, fmt.Errorf("child generator returned an error on parameter generation: %w", err)
}
if len(t) == 0 {
return nil, errors.New("child generator generated no parameters")
return nil, fmt.Errorf("child generator generated no parameters")
}
if len(t) > 1 {
@@ -207,8 +223,9 @@ func (m *MergeGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.App
if found {
return res
} else {
return NoRequeueAfter
}
return NoRequeueAfter
}
func getMergeGenerator(r argoprojiov1alpha1.ApplicationSetNestedGenerator) (*argoprojiov1alpha1.MergeGenerator, error) {

View File

@@ -9,7 +9,7 @@ import (
"github.com/stretchr/testify/require"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
func getNestedListGenerator(json string) *argoprojiov1alpha1.ApplicationSetNestedGenerator {
@@ -36,28 +36,26 @@ func getTerminalListGeneratorMultiple(jsons []string) argoprojiov1alpha1.Applica
return generator
}
func listOfMapsToSet(maps []map[string]any) (map[string]bool, error) {
func listOfMapsToSet(maps []map[string]interface{}) (map[string]bool, error) {
set := make(map[string]bool, len(maps))
for _, paramMap := range maps {
paramMapAsJSON, err := json.Marshal(paramMap)
paramMapAsJson, err := json.Marshal(paramMap)
if err != nil {
return nil, err
}
set[string(paramMapAsJSON)] = false
set[string(paramMapAsJson)] = false
}
return set, nil
}
func TestMergeGenerate(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
baseGenerators []argoprojiov1alpha1.ApplicationSetNestedGenerator
mergeKeys []string
expectedErr error
expected []map[string]any
expected []map[string]interface{}
}{
{
name: "no generators",
@@ -81,7 +79,7 @@ func TestMergeGenerate(t *testing.T) {
*getNestedListGenerator(`{"a": "3_1","b": "different","c": "3_3"}`), // gets ignored because its merge key value isn't in the base params set
},
mergeKeys: []string{"b"},
expected: []map[string]any{
expected: []map[string]interface{}{
{"a": "2_1", "b": "same", "c": "1_3"},
},
},
@@ -92,7 +90,7 @@ func TestMergeGenerate(t *testing.T) {
*getNestedListGenerator(`{"a": "a"}`),
},
mergeKeys: []string{"b"},
expected: []map[string]any{
expected: []map[string]interface{}{
{"a": "a"},
},
},
@@ -103,7 +101,7 @@ func TestMergeGenerate(t *testing.T) {
*getNestedListGenerator(`{"b": "b"}`),
},
mergeKeys: []string{"b"},
expected: []map[string]any{
expected: []map[string]interface{}{
{"a": "a"},
},
},
@@ -121,7 +119,7 @@ func TestMergeGenerate(t *testing.T) {
*getNestedListGenerator(`{"a": "1", "b": "1", "c": "added"}`),
},
mergeKeys: []string{"a", "b"},
expected: []map[string]any{
expected: []map[string]interface{}{
{"a": "1", "b": "1", "c": "added"},
{"a": "1", "b": "2"},
{"a": "2", "b": "1"},
@@ -143,7 +141,7 @@ func TestMergeGenerate(t *testing.T) {
*getNestedListGenerator(`{"a": "1", "b": "3", "d": "added"}`),
},
mergeKeys: []string{"a", "b"},
expected: []map[string]any{
expected: []map[string]interface{}{
{"a": "1", "b": "3", "c": "added", "d": "added"},
{"a": "2", "b": "2"},
},
@@ -198,8 +196,7 @@ func TestMergeGenerate(t *testing.T) {
}
}
func toAPIExtensionsJSON(t *testing.T, g any) *apiextensionsv1.JSON {
t.Helper()
func toAPIExtensionsJSON(t *testing.T, g interface{}) *apiextensionsv1.JSON {
resVal, err := json.Marshal(g)
if err != nil {
t.Error("unable to unmarshal json", g)
@@ -212,14 +209,12 @@ func toAPIExtensionsJSON(t *testing.T, g any) *apiextensionsv1.JSON {
}
func TestParamSetsAreUniqueByMergeKeys(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
mergeKeys []string
paramSets []map[string]any
paramSets []map[string]interface{}
expectedErr error
expected map[string]map[string]any
expected map[string]map[string]interface{}
}{
{
name: "no merge keys",
@@ -229,13 +224,13 @@ func TestParamSetsAreUniqueByMergeKeys(t *testing.T) {
{
name: "no paramSets",
mergeKeys: []string{"key"},
expected: make(map[string]map[string]any),
expected: make(map[string]map[string]interface{}),
},
{
name: "simple key, unique paramSets",
mergeKeys: []string{"key"},
paramSets: []map[string]any{{"key": "a"}, {"key": "b"}},
expected: map[string]map[string]any{
paramSets: []map[string]interface{}{{"key": "a"}, {"key": "b"}},
expected: map[string]map[string]interface{}{
`{"key":"a"}`: {"key": "a"},
`{"key":"b"}`: {"key": "b"},
},
@@ -243,23 +238,23 @@ func TestParamSetsAreUniqueByMergeKeys(t *testing.T) {
{
name: "simple key object, unique paramSets",
mergeKeys: []string{"key"},
paramSets: []map[string]any{{"key": map[string]any{"hello": "world"}}, {"key": "b"}},
expected: map[string]map[string]any{
`{"key":{"hello":"world"}}`: {"key": map[string]any{"hello": "world"}},
paramSets: []map[string]interface{}{{"key": map[string]interface{}{"hello": "world"}}, {"key": "b"}},
expected: map[string]map[string]interface{}{
`{"key":{"hello":"world"}}`: {"key": map[string]interface{}{"hello": "world"}},
`{"key":"b"}`: {"key": "b"},
},
},
{
name: "simple key, non-unique paramSets",
mergeKeys: []string{"key"},
paramSets: []map[string]any{{"key": "a"}, {"key": "b"}, {"key": "b"}},
paramSets: []map[string]interface{}{{"key": "a"}, {"key": "b"}, {"key": "b"}},
expectedErr: fmt.Errorf("%w. Duplicate key was %s", ErrNonUniqueParamSets, `{"key":"b"}`),
},
{
name: "simple key, duplicated key name, unique paramSets",
mergeKeys: []string{"key", "key"},
paramSets: []map[string]any{{"key": "a"}, {"key": "b"}},
expected: map[string]map[string]any{
paramSets: []map[string]interface{}{{"key": "a"}, {"key": "b"}},
expected: map[string]map[string]interface{}{
`{"key":"a"}`: {"key": "a"},
`{"key":"b"}`: {"key": "b"},
},
@@ -267,18 +262,18 @@ func TestParamSetsAreUniqueByMergeKeys(t *testing.T) {
{
name: "simple key, duplicated key name, non-unique paramSets",
mergeKeys: []string{"key", "key"},
paramSets: []map[string]any{{"key": "a"}, {"key": "b"}, {"key": "b"}},
paramSets: []map[string]interface{}{{"key": "a"}, {"key": "b"}, {"key": "b"}},
expectedErr: fmt.Errorf("%w. Duplicate key was %s", ErrNonUniqueParamSets, `{"key":"b"}`),
},
{
name: "compound key, unique paramSets",
mergeKeys: []string{"key1", "key2"},
paramSets: []map[string]any{
paramSets: []map[string]interface{}{
{"key1": "a", "key2": "a"},
{"key1": "a", "key2": "b"},
{"key1": "b", "key2": "a"},
},
expected: map[string]map[string]any{
expected: map[string]map[string]interface{}{
`{"key1":"a","key2":"a"}`: {"key1": "a", "key2": "a"},
`{"key1":"a","key2":"b"}`: {"key1": "a", "key2": "b"},
`{"key1":"b","key2":"a"}`: {"key1": "b", "key2": "a"},
@@ -287,13 +282,13 @@ func TestParamSetsAreUniqueByMergeKeys(t *testing.T) {
{
name: "compound key object, unique paramSets",
mergeKeys: []string{"key1", "key2"},
paramSets: []map[string]any{
{"key1": "a", "key2": map[string]any{"hello": "world"}},
paramSets: []map[string]interface{}{
{"key1": "a", "key2": map[string]interface{}{"hello": "world"}},
{"key1": "a", "key2": "b"},
{"key1": "b", "key2": "a"},
},
expected: map[string]map[string]any{
`{"key1":"a","key2":{"hello":"world"}}`: {"key1": "a", "key2": map[string]any{"hello": "world"}},
expected: map[string]map[string]interface{}{
`{"key1":"a","key2":{"hello":"world"}}`: {"key1": "a", "key2": map[string]interface{}{"hello": "world"}},
`{"key1":"a","key2":"b"}`: {"key1": "a", "key2": "b"},
`{"key1":"b","key2":"a"}`: {"key1": "b", "key2": "a"},
},
@@ -301,12 +296,12 @@ func TestParamSetsAreUniqueByMergeKeys(t *testing.T) {
{
name: "compound key, duplicate key names, unique paramSets",
mergeKeys: []string{"key1", "key1", "key2"},
paramSets: []map[string]any{
paramSets: []map[string]interface{}{
{"key1": "a", "key2": "a"},
{"key1": "a", "key2": "b"},
{"key1": "b", "key2": "a"},
},
expected: map[string]map[string]any{
expected: map[string]map[string]interface{}{
`{"key1":"a","key2":"a"}`: {"key1": "a", "key2": "a"},
`{"key1":"a","key2":"b"}`: {"key1": "a", "key2": "b"},
`{"key1":"b","key2":"a"}`: {"key1": "b", "key2": "a"},
@@ -315,7 +310,7 @@ func TestParamSetsAreUniqueByMergeKeys(t *testing.T) {
{
name: "compound key, non-unique paramSets",
mergeKeys: []string{"key1", "key2"},
paramSets: []map[string]any{
paramSets: []map[string]interface{}{
{"key1": "a", "key2": "a"},
{"key1": "a", "key2": "a"},
{"key1": "b", "key2": "a"},
@@ -325,7 +320,7 @@ func TestParamSetsAreUniqueByMergeKeys(t *testing.T) {
{
name: "compound key, duplicate key names, non-unique paramSets",
mergeKeys: []string{"key1", "key1", "key2"},
paramSets: []map[string]any{
paramSets: []map[string]interface{}{
{"key1": "a", "key2": "a"},
{"key1": "a", "key2": "a"},
{"key1": "b", "key2": "a"},

View File

@@ -1,17 +1,90 @@
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
// Code generated by mockery v2.40.2. DO NOT EDIT.
package mocks
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"
"sigs.k8s.io/controller-runtime/pkg/client"
time "time"
v1alpha1 "github.com/argoproj/argo-cd/v2/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.
// The first argument is typically a *testing.T value.
func NewGenerator(t interface {
@@ -25,194 +98,3 @@ func NewGenerator(t interface {
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

@@ -2,7 +2,6 @@ package generators
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
@@ -10,28 +9,33 @@ import (
"github.com/jeremywohl/flatten"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/controller-runtime/pkg/client"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v3/util/settings"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/util/settings"
"github.com/argoproj/argo-cd/v3/applicationset/services/plugin"
"github.com/argoproj/argo-cd/v2/applicationset/services/plugin"
)
const (
DefaultPluginRequeueAfter = 30 * time.Minute
DefaultPluginRequeueAfterSeconds = 30 * time.Minute
)
var _ Generator = (*PluginGenerator)(nil)
type PluginGenerator struct {
client client.Client
ctx context.Context
clientset kubernetes.Interface
namespace string
}
func NewPluginGenerator(client client.Client, namespace string) Generator {
func NewPluginGenerator(client client.Client, ctx context.Context, clientset kubernetes.Interface, namespace string) Generator {
g := &PluginGenerator{
client: client,
ctx: ctx,
clientset: clientset,
namespace: namespace,
}
return g
@@ -44,20 +48,20 @@ func (g *PluginGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.Ap
return time.Duration(*appSetGenerator.Plugin.RequeueAfterSeconds) * time.Second
}
return DefaultPluginRequeueAfter
return DefaultPluginRequeueAfterSeconds
}
func (g *PluginGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate {
return &appSetGenerator.Plugin.Template
}
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]interface{}, error) {
if appSetGenerator == nil {
return nil, ErrEmptyAppSetGenerator
return nil, EmptyAppSetGeneratorError
}
if appSetGenerator.Plugin == nil {
return nil, ErrEmptyAppSetGenerator
return nil, EmptyAppSetGeneratorError
}
ctx := context.Background()
@@ -101,18 +105,18 @@ func (g *PluginGenerator) getPluginFromGenerator(ctx context.Context, appSetName
}
}
pluginClient, err := plugin.NewPluginService(appSetName, cm["baseUrl"], token, requestTimeout)
pluginClient, err := plugin.NewPluginService(ctx, appSetName, cm["baseUrl"], token, requestTimeout)
if err != nil {
return nil, fmt.Errorf("error initializing plugin client: %w", err)
}
return pluginClient, nil
}
func (g *PluginGenerator) generateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, appSet *argoprojiov1alpha1.ApplicationSet, objectsFound []map[string]any, pluginParams argoprojiov1alpha1.PluginParameters, useGoTemplate bool) ([]map[string]any, error) {
res := []map[string]any{}
func (g *PluginGenerator) generateParams(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator, appSet *argoprojiov1alpha1.ApplicationSet, objectsFound []map[string]interface{}, pluginParams argoprojiov1alpha1.PluginParameters, useGoTemplate bool) ([]map[string]interface{}, error) {
res := []map[string]interface{}{}
for _, objectFound := range objectsFound {
params := map[string]any{}
params := map[string]interface{}{}
if useGoTemplate {
for k, v := range objectFound {
@@ -128,7 +132,7 @@ func (g *PluginGenerator) generateParams(appSetGenerator *argoprojiov1alpha1.App
}
}
params["generator"] = map[string]any{
params["generator"] = map[string]interface{}{
"input": map[string]argoprojiov1alpha1.PluginParameters{
"parameters": pluginParams,
},
@@ -188,14 +192,14 @@ func (g *PluginGenerator) getConfigMap(ctx context.Context, configMapRef string)
return nil, err
}
baseURL, ok := cm.Data["baseUrl"]
if !ok || baseURL == "" {
return nil, errors.New("baseUrl not found in ConfigMap")
baseUrl, ok := cm.Data["baseUrl"]
if !ok || baseUrl == "" {
return nil, fmt.Errorf("baseUrl not found in ConfigMap")
}
token, ok := cm.Data["token"]
if !ok || token == "" {
return nil, errors.New("token not found in ConfigMap")
return nil, fmt.Errorf("token not found in ConfigMap")
}
return cm.Data, nil

View File

@@ -1,8 +1,8 @@
package generators
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
@@ -11,31 +11,33 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
kubefake "k8s.io/client-go/kubernetes/fake"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/argoproj/argo-cd/v3/applicationset/services/plugin"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/applicationset/services/plugin"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
func TestPluginGenerateParams(t *testing.T) {
testCases := []struct {
name string
configmap *corev1.ConfigMap
secret *corev1.Secret
configmap *v1.ConfigMap
secret *v1.Secret
inputParameters map[string]apiextensionsv1.JSON
values map[string]string
gotemplate bool
expected []map[string]any
expected []map[string]interface{}
content []byte
expectedError error
}{
{
name: "simple case",
configmap: &corev1.ConfigMap{
configmap: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "first-plugin-cm",
Namespace: "default",
@@ -45,7 +47,7 @@ func TestPluginGenerateParams(t *testing.T) {
"token": "$plugin.token",
},
},
secret: &corev1.Secret{
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "argocd-secret",
Namespace: "default",
@@ -71,13 +73,13 @@ func TestPluginGenerateParams(t *testing.T) {
"key3": 123
}]
}}`),
expected: []map[string]any{
expected: []map[string]interface{}{
{
"key1": "val1",
"key2.key2_1": "val2_1",
"key2.key2_2.key2_2_1": "val2_2_1",
"key3": "123",
"generator": map[string]any{
"generator": map[string]interface{}{
"input": argoprojiov1alpha1.PluginInput{
Parameters: argoprojiov1alpha1.PluginParameters{
"pkey1": {Raw: []byte(`"val1"`)},
@@ -91,7 +93,7 @@ func TestPluginGenerateParams(t *testing.T) {
},
{
name: "simple case with values",
configmap: &corev1.ConfigMap{
configmap: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "first-plugin-cm",
Namespace: "default",
@@ -101,7 +103,7 @@ func TestPluginGenerateParams(t *testing.T) {
"token": "$plugin.token",
},
},
secret: &corev1.Secret{
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "argocd-secret",
Namespace: "default",
@@ -131,7 +133,7 @@ func TestPluginGenerateParams(t *testing.T) {
"key3": 123
}]
}}`),
expected: []map[string]any{
expected: []map[string]interface{}{
{
"key1": "val1",
"key2.key2_1": "val2_1",
@@ -139,7 +141,7 @@ func TestPluginGenerateParams(t *testing.T) {
"key3": "123",
"values.valuekey1": "valuevalue1",
"values.valuekey2": "templated-val1",
"generator": map[string]any{
"generator": map[string]interface{}{
"input": argoprojiov1alpha1.PluginInput{
Parameters: argoprojiov1alpha1.PluginParameters{
"pkey1": {Raw: []byte(`"val1"`)},
@@ -153,7 +155,7 @@ func TestPluginGenerateParams(t *testing.T) {
},
{
name: "simple case with gotemplate",
configmap: &corev1.ConfigMap{
configmap: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "first-plugin-cm",
Namespace: "default",
@@ -163,7 +165,7 @@ func TestPluginGenerateParams(t *testing.T) {
"token": "$plugin.token",
},
},
secret: &corev1.Secret{
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "argocd-secret",
Namespace: "default",
@@ -189,17 +191,17 @@ func TestPluginGenerateParams(t *testing.T) {
"key3": 123
}]
}}`),
expected: []map[string]any{
expected: []map[string]interface{}{
{
"key1": "val1",
"key2": map[string]any{
"key2": map[string]interface{}{
"key2_1": "val2_1",
"key2_2": map[string]any{
"key2_2": map[string]interface{}{
"key2_2_1": "val2_2_1",
},
},
"key3": float64(123),
"generator": map[string]any{
"generator": map[string]interface{}{
"input": argoprojiov1alpha1.PluginInput{
Parameters: argoprojiov1alpha1.PluginParameters{
"pkey1": {Raw: []byte(`"val1"`)},
@@ -213,7 +215,7 @@ func TestPluginGenerateParams(t *testing.T) {
},
{
name: "simple case with appended params",
configmap: &corev1.ConfigMap{
configmap: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "first-plugin-cm",
Namespace: "default",
@@ -223,7 +225,7 @@ func TestPluginGenerateParams(t *testing.T) {
"token": "$plugin.token",
},
},
secret: &corev1.Secret{
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "argocd-secret",
Namespace: "default",
@@ -248,14 +250,14 @@ func TestPluginGenerateParams(t *testing.T) {
"key3": 123,
"pkey2": "valplugin"
}]}}`),
expected: []map[string]any{
expected: []map[string]interface{}{
{
"key1": "val1",
"key2.key2_1": "val2_1",
"key2.key2_2.key2_2_1": "val2_2_1",
"key3": "123",
"pkey2": "valplugin",
"generator": map[string]any{
"generator": map[string]interface{}{
"input": argoprojiov1alpha1.PluginInput{
Parameters: argoprojiov1alpha1.PluginParameters{
"pkey1": {Raw: []byte(`"val1"`)},
@@ -269,7 +271,7 @@ func TestPluginGenerateParams(t *testing.T) {
},
{
name: "no params",
configmap: &corev1.ConfigMap{
configmap: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "first-plugin-cm",
Namespace: "default",
@@ -279,7 +281,7 @@ func TestPluginGenerateParams(t *testing.T) {
"token": "$plugin.token",
},
},
secret: &corev1.Secret{
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "argocd-secret",
Namespace: "default",
@@ -302,14 +304,14 @@ func TestPluginGenerateParams(t *testing.T) {
"key3": 123
}]
}}`),
expected: []map[string]any{
expected: []map[string]interface{}{
{
"key1": "val1",
"key2.key2_1": "val2_1",
"key2.key2_2.key2_2_1": "val2_2_1",
"key3": "123",
"generator": map[string]any{
"input": map[string]map[string]any{
"generator": map[string]interface{}{
"input": map[string]map[string]interface{}{
"parameters": {},
},
},
@@ -319,7 +321,7 @@ func TestPluginGenerateParams(t *testing.T) {
},
{
name: "empty return",
configmap: &corev1.ConfigMap{
configmap: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "first-plugin-cm",
Namespace: "default",
@@ -329,7 +331,7 @@ func TestPluginGenerateParams(t *testing.T) {
"token": "$plugin.token",
},
},
secret: &corev1.Secret{
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "argocd-secret",
Namespace: "default",
@@ -341,12 +343,12 @@ func TestPluginGenerateParams(t *testing.T) {
inputParameters: map[string]apiextensionsv1.JSON{},
gotemplate: false,
content: []byte(`{"input": {"parameters": []}}`),
expected: []map[string]any{},
expected: []map[string]interface{}{},
expectedError: nil,
},
{
name: "wrong return",
configmap: &corev1.ConfigMap{
configmap: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "first-plugin-cm",
Namespace: "default",
@@ -356,7 +358,7 @@ func TestPluginGenerateParams(t *testing.T) {
"token": "$plugin.token",
},
},
secret: &corev1.Secret{
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "argocd-secret",
Namespace: "default",
@@ -368,12 +370,12 @@ func TestPluginGenerateParams(t *testing.T) {
inputParameters: map[string]apiextensionsv1.JSON{},
gotemplate: false,
content: []byte(`wrong body ...`),
expected: []map[string]any{},
expectedError: errors.New("error listing params: error get api 'set': invalid character 'w' looking for beginning of value: wrong body ..."),
expected: []map[string]interface{}{},
expectedError: fmt.Errorf("error listing params: error get api 'set': invalid character 'w' looking for beginning of value: wrong body ..."),
},
{
name: "external secret",
configmap: &corev1.ConfigMap{
configmap: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "first-plugin-cm",
Namespace: "default",
@@ -383,7 +385,7 @@ func TestPluginGenerateParams(t *testing.T) {
"token": "$plugin-secret:plugin.token",
},
},
secret: &corev1.Secret{
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "plugin-secret",
Namespace: "default",
@@ -408,14 +410,14 @@ func TestPluginGenerateParams(t *testing.T) {
"key3": 123,
"pkey2": "valplugin"
}]}}`),
expected: []map[string]any{
expected: []map[string]interface{}{
{
"key1": "val1",
"key2.key2_1": "val2_1",
"key2.key2_2.key2_2_1": "val2_2_1",
"key3": "123",
"pkey2": "valplugin",
"generator": map[string]any{
"generator": map[string]interface{}{
"input": argoprojiov1alpha1.PluginInput{
Parameters: argoprojiov1alpha1.PluginParameters{
"pkey1": {Raw: []byte(`"val1"`)},
@@ -429,7 +431,7 @@ func TestPluginGenerateParams(t *testing.T) {
},
{
name: "no secret",
configmap: &corev1.ConfigMap{
configmap: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "first-plugin-cm",
Namespace: "default",
@@ -439,7 +441,7 @@ func TestPluginGenerateParams(t *testing.T) {
"token": "$plugin.token",
},
},
secret: &corev1.Secret{},
secret: &v1.Secret{},
inputParameters: map[string]apiextensionsv1.JSON{
"pkey1": {Raw: []byte(`"val1"`)},
"pkey2": {Raw: []byte(`"val2"`)},
@@ -457,13 +459,13 @@ func TestPluginGenerateParams(t *testing.T) {
"key3": 123
}]
}}`),
expected: []map[string]any{
expected: []map[string]interface{}{
{
"key1": "val1",
"key2.key2_1": "val2_1",
"key2.key2_2.key2_2_1": "val2_2_1",
"key3": "123",
"generator": map[string]any{
"generator": map[string]interface{}{
"input": argoprojiov1alpha1.PluginInput{
Parameters: argoprojiov1alpha1.PluginParameters{
"pkey1": {Raw: []byte(`"val1"`)},
@@ -473,12 +475,12 @@ func TestPluginGenerateParams(t *testing.T) {
},
},
},
expectedError: errors.New("error getting plugin from generator: error fetching Secret token: error fetching secret default/argocd-secret: secrets \"argocd-secret\" not found"),
expectedError: fmt.Errorf("error getting plugin from generator: error fetching Secret token: error fetching secret default/argocd-secret: secrets \"argocd-secret\" not found"),
},
{
name: "no configmap",
configmap: &corev1.ConfigMap{},
secret: &corev1.Secret{
configmap: &v1.ConfigMap{},
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "argocd-secret",
Namespace: "default",
@@ -504,13 +506,13 @@ func TestPluginGenerateParams(t *testing.T) {
"key3": 123
}]
}}`),
expected: []map[string]any{
expected: []map[string]interface{}{
{
"key1": "val1",
"key2.key2_1": "val2_1",
"key2.key2_2.key2_2_1": "val2_2_1",
"key3": "123",
"generator": map[string]any{
"generator": map[string]interface{}{
"input": argoprojiov1alpha1.PluginInput{
Parameters: argoprojiov1alpha1.PluginParameters{
"pkey1": {Raw: []byte(`"val1"`)},
@@ -520,11 +522,11 @@ func TestPluginGenerateParams(t *testing.T) {
},
},
},
expectedError: errors.New("error getting plugin from generator: error fetching ConfigMap: configmaps \"\" not found"),
expectedError: fmt.Errorf("error getting plugin from generator: error fetching ConfigMap: configmaps \"\" not found"),
},
{
name: "no baseUrl",
configmap: &corev1.ConfigMap{
configmap: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "first-plugin-cm",
Namespace: "default",
@@ -533,7 +535,7 @@ func TestPluginGenerateParams(t *testing.T) {
"token": "$plugin.token",
},
},
secret: &corev1.Secret{
secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "argocd-secret",
Namespace: "default",
@@ -559,13 +561,13 @@ func TestPluginGenerateParams(t *testing.T) {
"key3": 123
}]
}}`),
expected: []map[string]any{
expected: []map[string]interface{}{
{
"key1": "val1",
"key2.key2_1": "val2_1",
"key2.key2_2.key2_2_1": "val2_2_1",
"key3": "123",
"generator": map[string]any{
"generator": map[string]interface{}{
"input": argoprojiov1alpha1.PluginInput{
Parameters: argoprojiov1alpha1.PluginParameters{
"pkey1": {Raw: []byte(`"val1"`)},
@@ -575,11 +577,11 @@ func TestPluginGenerateParams(t *testing.T) {
},
},
},
expectedError: errors.New("error getting plugin from generator: error fetching ConfigMap: baseUrl not found in ConfigMap"),
expectedError: fmt.Errorf("error getting plugin from generator: error fetching ConfigMap: baseUrl not found in ConfigMap"),
},
{
name: "no token",
configmap: &corev1.ConfigMap{
configmap: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "first-plugin-cm",
Namespace: "default",
@@ -588,7 +590,7 @@ func TestPluginGenerateParams(t *testing.T) {
"baseUrl": "http://127.0.0.1",
},
},
secret: &corev1.Secret{},
secret: &v1.Secret{},
inputParameters: map[string]apiextensionsv1.JSON{
"pkey1": {Raw: []byte(`"val1"`)},
"pkey2": {Raw: []byte(`"val2"`)},
@@ -606,13 +608,13 @@ func TestPluginGenerateParams(t *testing.T) {
"key3": 123
}]
}}`),
expected: []map[string]any{
expected: []map[string]interface{}{
{
"key1": "val1",
"key2.key2_1": "val2_1",
"key2.key2_2.key2_2_1": "val2_2_1",
"key3": "123",
"generator": map[string]any{
"generator": map[string]interface{}{
"input": argoprojiov1alpha1.PluginInput{
Parameters: argoprojiov1alpha1.PluginParameters{
"pkey1": {Raw: []byte(`"val1"`)},
@@ -622,10 +624,12 @@ func TestPluginGenerateParams(t *testing.T) {
},
},
},
expectedError: errors.New("error getting plugin from generator: error fetching ConfigMap: token not found in ConfigMap"),
expectedError: fmt.Errorf("error getting plugin from generator: error fetching ConfigMap: token not found in ConfigMap"),
},
}
ctx := context.Background()
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
generatorConfig := argoprojiov1alpha1.ApplicationSetGenerator{
@@ -662,9 +666,11 @@ func TestPluginGenerateParams(t *testing.T) {
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()
pluginGenerator := NewPluginGenerator(fakeClientWithCache, "default")
pluginGenerator := NewPluginGenerator(fakeClientWithCache, ctx, fakeClient, "default")
applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
@@ -684,11 +690,11 @@ func TestPluginGenerateParams(t *testing.T) {
require.EqualError(t, err, testCase.expectedError.Error())
} else {
require.NoError(t, err)
expectedJSON, err := json.Marshal(testCase.expected)
expectedJson, err := json.Marshal(testCase.expected)
require.NoError(t, err)
gotJSON, err := json.Marshal(got)
gotJson, err := json.Marshal(got)
require.NoError(t, err)
assert.JSONEq(t, string(expectedJSON), string(gotJSON))
assert.Equal(t, string(expectedJson), string(gotJson))
}
})
}

View File

@@ -2,38 +2,41 @@ package generators
import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"time"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/gosimple/slug"
"github.com/argoproj/argo-cd/v3/applicationset/services"
pullrequest "github.com/argoproj/argo-cd/v3/applicationset/services/pull_request"
"github.com/argoproj/argo-cd/v3/applicationset/utils"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
pullrequest "github.com/argoproj/argo-cd/v2/applicationset/services/pull_request"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
var _ Generator = (*PullRequestGenerator)(nil)
const (
DefaultPullRequestRequeueAfter = 30 * time.Minute
DefaultPullRequestRequeueAfterSeconds = 30 * time.Minute
)
type PullRequestGenerator struct {
client client.Client
selectServiceProviderFunc func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error)
SCMConfig
auth SCMAuthProviders
scmRootCAPath string
allowedSCMProviders []string
enableSCMProviders bool
}
func NewPullRequestGenerator(client client.Client, scmConfig SCMConfig) Generator {
func NewPullRequestGenerator(client client.Client, auth SCMAuthProviders, scmRootCAPath string, allowedScmProviders []string, enableSCMProviders bool) Generator {
g := &PullRequestGenerator{
client: client,
SCMConfig: scmConfig,
client: client,
auth: auth,
scmRootCAPath: scmRootCAPath,
allowedSCMProviders: allowedScmProviders,
enableSCMProviders: enableSCMProviders,
}
g.selectServiceProviderFunc = g.selectServiceProvider
return g
@@ -46,20 +49,20 @@ func (g *PullRequestGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alph
return time.Duration(*appSetGenerator.PullRequest.RequeueAfterSeconds) * time.Second
}
return DefaultPullRequestRequeueAfter
return DefaultPullRequestRequeueAfterSeconds
}
func (g *PullRequestGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate {
return &appSetGenerator.PullRequest.Template
}
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]interface{}, error) {
if appSetGenerator == nil {
return nil, ErrEmptyAppSetGenerator
return nil, EmptyAppSetGeneratorError
}
if appSetGenerator.PullRequest == nil {
return nil, ErrEmptyAppSetGenerator
return nil, EmptyAppSetGeneratorError
}
ctx := context.Background()
@@ -72,7 +75,7 @@ func (g *PullRequestGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha
if err != nil {
return nil, fmt.Errorf("error listing repos: %w", err)
}
params := make([]map[string]any, 0, len(pulls))
params := make([]map[string]interface{}, 0, len(pulls))
// 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
@@ -98,9 +101,8 @@ func (g *PullRequestGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha
shortSHALength7 = len(pull.HeadSHA)
}
paramMap := map[string]any{
paramMap := map[string]interface{}{
"number": strconv.Itoa(pull.Number),
"title": pull.Title,
"branch": pull.Branch,
"branch_slug": slug.Make(pull.Branch),
"target_branch": pull.TargetBranch,
@@ -108,12 +110,6 @@ func (g *PullRequestGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha
"head_sha": pull.HeadSHA,
"head_short_sha": pull.HeadSHA[:shortSHALength],
"head_short_sha_7": pull.HeadSHA[:shortSHALength7],
"author": pull.Author,
}
err := appendTemplatedValues(appSetGenerator.PullRequest.Values, paramMap, applicationSetInfo.Spec.GoTemplate, applicationSetInfo.Spec.GoTemplateOptions)
if err != nil {
return nil, fmt.Errorf("failed to append templated values: %w", err)
}
// PR lables will only be supported for Go Template appsets, since fasttemplate will be deprecated.
@@ -139,115 +135,99 @@ func (g *PullRequestGenerator) selectServiceProvider(ctx context.Context, genera
}
if generatorConfig.GitLab != nil {
providerConfig := generatorConfig.GitLab
var caCerts []byte
var prErr error
if providerConfig.CARef != nil {
caCerts, prErr = utils.GetConfigMapData(ctx, g.client, providerConfig.CARef, applicationSetInfo.Namespace)
if prErr != nil {
return nil, fmt.Errorf("error fetching CA certificates from ConfigMap: %w", prErr)
}
}
token, err := utils.GetSecretRef(ctx, g.client, providerConfig.TokenRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
token, err := g.getSecretRef(ctx, providerConfig.TokenRef, applicationSetInfo.Namespace)
if err != nil {
return nil, fmt.Errorf("error fetching Secret token: %w", err)
}
return pullrequest.NewGitLabService(token, providerConfig.API, providerConfig.Project, providerConfig.Labels, providerConfig.PullRequestState, g.scmRootCAPath, providerConfig.Insecure, caCerts)
return pullrequest.NewGitLabService(ctx, token, providerConfig.API, providerConfig.Project, providerConfig.Labels, providerConfig.PullRequestState, g.scmRootCAPath, providerConfig.Insecure)
}
if generatorConfig.Gitea != nil {
providerConfig := generatorConfig.Gitea
token, err := utils.GetSecretRef(ctx, g.client, providerConfig.TokenRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
token, err := g.getSecretRef(ctx, providerConfig.TokenRef, applicationSetInfo.Namespace)
if err != nil {
return nil, fmt.Errorf("error fetching Secret token: %w", err)
}
return pullrequest.NewGiteaService(token, providerConfig.API, providerConfig.Owner, providerConfig.Repo, providerConfig.Labels, providerConfig.Insecure)
return pullrequest.NewGiteaService(ctx, token, providerConfig.API, providerConfig.Owner, providerConfig.Repo, providerConfig.Insecure)
}
if generatorConfig.BitbucketServer != nil {
providerConfig := generatorConfig.BitbucketServer
var caCerts []byte
var prErr error
if providerConfig.CARef != nil {
caCerts, prErr = utils.GetConfigMapData(ctx, g.client, providerConfig.CARef, applicationSetInfo.Namespace)
if prErr != nil {
return nil, fmt.Errorf("error fetching CA certificates from ConfigMap: %w", prErr)
}
}
if providerConfig.BearerToken != nil {
appToken, err := utils.GetSecretRef(ctx, g.client, providerConfig.BearerToken.TokenRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
if err != nil {
return nil, fmt.Errorf("error fetching Secret Bearer token: %w", err)
}
return pullrequest.NewBitbucketServiceBearerToken(ctx, appToken, providerConfig.API, providerConfig.Project, providerConfig.Repo, g.scmRootCAPath, providerConfig.Insecure, caCerts)
} else if providerConfig.BasicAuth != nil {
password, err := utils.GetSecretRef(ctx, g.client, providerConfig.BasicAuth.PasswordRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
if providerConfig.BasicAuth != nil {
password, err := g.getSecretRef(ctx, providerConfig.BasicAuth.PasswordRef, applicationSetInfo.Namespace)
if err != nil {
return nil, fmt.Errorf("error fetching Secret token: %w", err)
}
return pullrequest.NewBitbucketServiceBasicAuth(ctx, providerConfig.BasicAuth.Username, password, providerConfig.API, providerConfig.Project, providerConfig.Repo, g.scmRootCAPath, providerConfig.Insecure, caCerts)
return pullrequest.NewBitbucketServiceBasicAuth(ctx, providerConfig.BasicAuth.Username, password, providerConfig.API, providerConfig.Project, providerConfig.Repo)
} else {
return pullrequest.NewBitbucketServiceNoAuth(ctx, providerConfig.API, providerConfig.Project, providerConfig.Repo)
}
return pullrequest.NewBitbucketServiceNoAuth(ctx, providerConfig.API, providerConfig.Project, providerConfig.Repo, g.scmRootCAPath, providerConfig.Insecure, caCerts)
}
if generatorConfig.Bitbucket != nil {
providerConfig := generatorConfig.Bitbucket
if providerConfig.BearerToken != nil {
appToken, err := utils.GetSecretRef(ctx, g.client, providerConfig.BearerToken.TokenRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
appToken, err := g.getSecretRef(ctx, providerConfig.BearerToken.TokenRef, applicationSetInfo.Namespace)
if err != nil {
return nil, fmt.Errorf("error fetching Secret Bearer token: %w", err)
}
return pullrequest.NewBitbucketCloudServiceBearerToken(providerConfig.API, appToken, providerConfig.Owner, providerConfig.Repo)
} else if providerConfig.BasicAuth != nil {
password, err := utils.GetSecretRef(ctx, g.client, providerConfig.BasicAuth.PasswordRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
password, err := g.getSecretRef(ctx, providerConfig.BasicAuth.PasswordRef, applicationSetInfo.Namespace)
if err != nil {
return nil, fmt.Errorf("error fetching Secret token: %w", err)
}
return pullrequest.NewBitbucketCloudServiceBasicAuth(providerConfig.API, providerConfig.BasicAuth.Username, password, providerConfig.Owner, providerConfig.Repo)
} else {
return pullrequest.NewBitbucketCloudServiceNoAuth(providerConfig.API, providerConfig.Owner, providerConfig.Repo)
}
return pullrequest.NewBitbucketCloudServiceNoAuth(providerConfig.API, providerConfig.Owner, providerConfig.Repo)
}
if generatorConfig.AzureDevOps != nil {
providerConfig := generatorConfig.AzureDevOps
token, err := utils.GetSecretRef(ctx, g.client, providerConfig.TokenRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
token, err := g.getSecretRef(ctx, providerConfig.TokenRef, applicationSetInfo.Namespace)
if err != nil {
return nil, fmt.Errorf("error fetching Secret token: %w", err)
}
return pullrequest.NewAzureDevOpsService(token, providerConfig.API, providerConfig.Organization, providerConfig.Project, providerConfig.Repo, providerConfig.Labels)
return pullrequest.NewAzureDevOpsService(ctx, token, providerConfig.API, providerConfig.Organization, providerConfig.Project, providerConfig.Repo, providerConfig.Labels)
}
return nil, errors.New("no Pull Request provider implementation configured")
return nil, fmt.Errorf("no Pull Request provider implementation configured")
}
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
if cfg.AppSecretName != "" {
auth, err := g.GitHubApps.GetAuthSecret(ctx, cfg.AppSecretName)
auth, err := g.auth.GitHubApps.GetAuthSecret(ctx, cfg.AppSecretName)
if err != nil {
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)
}
// always default to token, even if not set (public access)
token, err := utils.GetSecretRef(ctx, g.client, cfg.TokenRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
token, err := g.getSecretRef(ctx, cfg.TokenRef, applicationSetInfo.Namespace)
if err != nil {
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(ctx, token, cfg.API, cfg.Owner, cfg.Repo, cfg.Labels)
}
// getSecretRef gets the value of the key for the specified Secret resource.
func (g *PullRequestGenerator) getSecretRef(ctx context.Context, ref *argoprojiov1alpha1.SecretRef, namespace string) (string, error) {
if ref == nil {
return "", nil
}
secret := &corev1.Secret{}
err := g.client.Get(
ctx,
client.ObjectKey{
Name: ref.SecretName,
Namespace: namespace,
},
secret)
if err != nil {
return "", fmt.Errorf("error fetching secret %s/%s: %w", namespace, ref.SecretName, err)
}
tokenBytes, ok := secret.Data[ref.Key]
if !ok {
return "", fmt.Errorf("key %q in secret %s/%s not found", ref.Key, namespace, ref.SecretName)
}
return string(tokenBytes), nil
}

View File

@@ -2,23 +2,24 @@ package generators
import (
"context"
"errors"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
pullrequest "github.com/argoproj/argo-cd/v3/applicationset/services/pull_request"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
pullrequest "github.com/argoproj/argo-cd/v2/applicationset/services/pull_request"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
func TestPullRequestGithubGenerateParams(t *testing.T) {
ctx := t.Context()
ctx := context.Background()
cases := []struct {
selectFunc func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error)
values map[string]string
expected []map[string]any
expected []map[string]interface{}
expectedErr error
applicationSet argoprojiov1alpha1.ApplicationSet
}{
@@ -29,20 +30,17 @@ func TestPullRequestGithubGenerateParams(t *testing.T) {
[]*pullrequest.PullRequest{
{
Number: 1,
Title: "title1",
Branch: "branch1",
TargetBranch: "master",
HeadSHA: "089d92cbf9ff857a39e6feccd32798ca700fb958",
Author: "testName",
},
},
nil,
)
},
expected: []map[string]any{
expected: []map[string]interface{}{
{
"number": "1",
"title": "title1",
"branch": "branch1",
"branch_slug": "branch1",
"target_branch": "master",
@@ -50,7 +48,6 @@ func TestPullRequestGithubGenerateParams(t *testing.T) {
"head_sha": "089d92cbf9ff857a39e6feccd32798ca700fb958",
"head_short_sha": "089d92cb",
"head_short_sha_7": "089d92c",
"author": "testName",
},
},
expectedErr: nil,
@@ -62,20 +59,17 @@ func TestPullRequestGithubGenerateParams(t *testing.T) {
[]*pullrequest.PullRequest{
{
Number: 2,
Title: "title2",
Branch: "feat/areally+long_pull_request_name_to_test_argo_slugification_and_branch_name_shortening_feature",
TargetBranch: "feat/anotherreally+long_pull_request_name_to_test_argo_slugification_and_branch_name_shortening_feature",
HeadSHA: "9b34ff5bd418e57d58891eb0aa0728043ca1e8be",
Author: "testName",
},
},
nil,
)
},
expected: []map[string]any{
expected: []map[string]interface{}{
{
"number": "2",
"title": "title2",
"branch": "feat/areally+long_pull_request_name_to_test_argo_slugification_and_branch_name_shortening_feature",
"branch_slug": "feat-areally-long-pull-request-name-to-test-argo",
"target_branch": "feat/anotherreally+long_pull_request_name_to_test_argo_slugification_and_branch_name_shortening_feature",
@@ -83,7 +77,6 @@ func TestPullRequestGithubGenerateParams(t *testing.T) {
"head_sha": "9b34ff5bd418e57d58891eb0aa0728043ca1e8be",
"head_short_sha": "9b34ff5b",
"head_short_sha_7": "9b34ff5",
"author": "testName",
},
},
expectedErr: nil,
@@ -95,20 +88,17 @@ func TestPullRequestGithubGenerateParams(t *testing.T) {
[]*pullrequest.PullRequest{
{
Number: 1,
Title: "title1",
Branch: "a-very-short-sha",
TargetBranch: "master",
HeadSHA: "abcd",
Author: "testName",
},
},
nil,
)
},
expected: []map[string]any{
expected: []map[string]interface{}{
{
"number": "1",
"title": "title1",
"branch": "a-very-short-sha",
"branch_slug": "a-very-short-sha",
"target_branch": "master",
@@ -116,46 +106,6 @@ func TestPullRequestGithubGenerateParams(t *testing.T) {
"head_sha": "abcd",
"head_short_sha": "abcd",
"head_short_sha_7": "abcd",
"author": "testName",
},
},
expectedErr: nil,
},
{
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",
},
},
nil,
)
},
values: map[string]string{
"foo": "bar",
"pr_branch": "{{ branch }}",
},
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",
"values.foo": "bar",
"values.pr_branch": "my_branch",
},
},
expectedErr: nil,
@@ -165,11 +115,11 @@ func TestPullRequestGithubGenerateParams(t *testing.T) {
return pullrequest.NewFakeService(
ctx,
nil,
errors.New("fake error"),
fmt.Errorf("fake error"),
)
},
expected: nil,
expectedErr: errors.New("error listing repos: fake error"),
expectedErr: fmt.Errorf("error listing repos: fake error"),
},
{
selectFunc: func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) {
@@ -178,21 +128,18 @@ func TestPullRequestGithubGenerateParams(t *testing.T) {
[]*pullrequest.PullRequest{
{
Number: 1,
Title: "title1",
Branch: "branch1",
TargetBranch: "master",
HeadSHA: "089d92cbf9ff857a39e6feccd32798ca700fb958",
Labels: []string{"preview"},
Author: "testName",
},
},
nil,
)
},
expected: []map[string]any{
expected: []map[string]interface{}{
{
"number": "1",
"title": "title1",
"branch": "branch1",
"branch_slug": "branch1",
"target_branch": "master",
@@ -201,7 +148,6 @@ func TestPullRequestGithubGenerateParams(t *testing.T) {
"head_short_sha": "089d92cb",
"head_short_sha_7": "089d92c",
"labels": []string{"preview"},
"author": "testName",
},
},
expectedErr: nil,
@@ -219,21 +165,18 @@ func TestPullRequestGithubGenerateParams(t *testing.T) {
[]*pullrequest.PullRequest{
{
Number: 1,
Title: "title1",
Branch: "branch1",
TargetBranch: "master",
HeadSHA: "089d92cbf9ff857a39e6feccd32798ca700fb958",
Labels: []string{"preview"},
Author: "testName",
},
},
nil,
)
},
expected: []map[string]any{
expected: []map[string]interface{}{
{
"number": "1",
"title": "title1",
"branch": "branch1",
"branch_slug": "branch1",
"target_branch": "master",
@@ -241,7 +184,6 @@ func TestPullRequestGithubGenerateParams(t *testing.T) {
"head_sha": "089d92cbf9ff857a39e6feccd32798ca700fb958",
"head_short_sha": "089d92cb",
"head_short_sha_7": "089d92c",
"author": "testName",
},
},
expectedErr: nil,
@@ -259,14 +201,12 @@ func TestPullRequestGithubGenerateParams(t *testing.T) {
selectServiceProviderFunc: c.selectFunc,
}
generatorConfig := argoprojiov1alpha1.ApplicationSetGenerator{
PullRequest: &argoprojiov1alpha1.PullRequestGenerator{
Values: c.values,
},
PullRequest: &argoprojiov1alpha1.PullRequestGenerator{},
}
got, gotErr := gen.GenerateParams(&generatorConfig, &c.applicationSet, nil)
if c.expectedErr != nil {
require.EqualError(t, gotErr, c.expectedErr.Error())
assert.Equal(t, c.expectedErr.Error(), gotErr.Error())
} else {
require.NoError(t, gotErr)
}
@@ -274,12 +214,76 @@ func TestPullRequestGithubGenerateParams(t *testing.T) {
}
}
func TestAllowedSCMProviderPullRequest(t *testing.T) {
t.Parallel()
func TestPullRequestGetSecretRef(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test-secret", Namespace: "test"},
Data: map[string][]byte{
"my-token": []byte("secret"),
},
}
gen := &PullRequestGenerator{client: fake.NewClientBuilder().WithObjects(secret).Build()}
ctx := context.Background()
cases := []struct {
name, namespace, token string
ref *argoprojiov1alpha1.SecretRef
hasError bool
}{
{
name: "valid ref",
ref: &argoprojiov1alpha1.SecretRef{SecretName: "test-secret", Key: "my-token"},
namespace: "test",
token: "secret",
hasError: false,
},
{
name: "nil ref",
ref: nil,
namespace: "test",
token: "",
hasError: false,
},
{
name: "wrong name",
ref: &argoprojiov1alpha1.SecretRef{SecretName: "other", Key: "my-token"},
namespace: "test",
token: "",
hasError: true,
},
{
name: "wrong key",
ref: &argoprojiov1alpha1.SecretRef{SecretName: "test-secret", Key: "other-token"},
namespace: "test",
token: "",
hasError: true,
},
{
name: "wrong namespace",
ref: &argoprojiov1alpha1.SecretRef{SecretName: "test-secret", Key: "my-token"},
namespace: "other",
token: "",
hasError: true,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
token, err := gen.getSecretRef(ctx, c.ref, c.namespace)
if c.hasError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
assert.Equal(t, c.token, token)
})
}
}
func TestAllowedSCMProviderPullRequest(t *testing.T) {
cases := []struct {
name string
providerConfig *argoprojiov1alpha1.PullRequestGenerator
expectedError error
}{
{
name: "Error Github",
@@ -288,6 +292,7 @@ func TestAllowedSCMProviderPullRequest(t *testing.T) {
API: "https://myservice.mynamespace.svc.cluster.local",
},
},
expectedError: &ErrDisallowedSCMProvider{},
},
{
name: "Error Gitlab",
@@ -296,6 +301,7 @@ func TestAllowedSCMProviderPullRequest(t *testing.T) {
API: "https://myservice.mynamespace.svc.cluster.local",
},
},
expectedError: &ErrDisallowedSCMProvider{},
},
{
name: "Error Gitea",
@@ -304,6 +310,7 @@ func TestAllowedSCMProviderPullRequest(t *testing.T) {
API: "https://myservice.mynamespace.svc.cluster.local",
},
},
expectedError: &ErrDisallowedSCMProvider{},
},
{
name: "Error Bitbucket",
@@ -312,6 +319,7 @@ func TestAllowedSCMProviderPullRequest(t *testing.T) {
API: "https://myservice.mynamespace.svc.cluster.local",
},
},
expectedError: &ErrDisallowedSCMProvider{},
},
}
@@ -321,13 +329,13 @@ func TestAllowedSCMProviderPullRequest(t *testing.T) {
t.Run(testCaseCopy.name, func(t *testing.T) {
t.Parallel()
pullRequestGenerator := NewPullRequestGenerator(nil, NewSCMConfig("", []string{
pullRequestGenerator := NewPullRequestGenerator(nil, SCMAuthProviders{}, "", []string{
"github.myorg.com",
"gitlab.myorg.com",
"gitea.myorg.com",
"bitbucket.myorg.com",
"azuredevops.myorg.com",
}, true, true, nil, true))
}, true)
applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
@@ -343,14 +351,13 @@ func TestAllowedSCMProviderPullRequest(t *testing.T) {
_, err := pullRequestGenerator.GenerateParams(&applicationSetInfo.Spec.Generators[0], &applicationSetInfo, nil)
require.Error(t, err, "Must return an error")
var expectedError ErrDisallowedSCMProvider
assert.ErrorAs(t, err, &expectedError)
assert.ErrorAs(t, err, testCaseCopy.expectedError)
})
}
}
func TestSCMProviderDisabled_PRGenerator(t *testing.T) {
generator := NewPullRequestGenerator(nil, NewSCMConfig("", []string{}, false, true, nil, true))
generator := NewPullRequestGenerator(nil, SCMAuthProviders{}, "", []string{}, false)
applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{

View File

@@ -4,66 +4,54 @@ import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"time"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
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/scm_provider"
"github.com/argoproj/argo-cd/v3/applicationset/utils"
"github.com/argoproj/argo-cd/v3/common"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/applicationset/services/github_app_auth"
"github.com/argoproj/argo-cd/v2/applicationset/services/scm_provider"
"github.com/argoproj/argo-cd/v2/applicationset/utils"
"github.com/argoproj/argo-cd/v2/common"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
var _ Generator = (*SCMProviderGenerator)(nil)
const (
DefaultSCMProviderRequeueAfter = 30 * time.Minute
DefaultSCMProviderRequeueAfterSeconds = 30 * time.Minute
)
type SCMProviderGenerator struct {
client client.Client
// Testing hooks.
overrideProvider scm_provider.SCMProviderService
SCMConfig
}
type SCMConfig struct {
scmRootCAPath string
allowedSCMProviders []string
enableSCMProviders bool
enableGitHubAPIMetrics bool
GitHubApps github_app_auth.Credentials
tokenRefStrictMode bool
SCMAuthProviders
scmRootCAPath string
allowedSCMProviders []string
enableSCMProviders bool
}
func NewSCMConfig(scmRootCAPath string, allowedSCMProviders []string, enableSCMProviders bool, enableGitHubAPIMetrics bool, gitHubApps github_app_auth.Credentials, tokenRefStrictMode bool) SCMConfig {
return SCMConfig{
scmRootCAPath: scmRootCAPath,
allowedSCMProviders: allowedSCMProviders,
enableSCMProviders: enableSCMProviders,
enableGitHubAPIMetrics: enableGitHubAPIMetrics,
GitHubApps: gitHubApps,
tokenRefStrictMode: tokenRefStrictMode,
}
type SCMAuthProviders struct {
GitHubApps github_app_auth.Credentials
}
func NewSCMProviderGenerator(client client.Client, scmConfig SCMConfig) Generator {
func NewSCMProviderGenerator(client client.Client, providers SCMAuthProviders, scmRootCAPath string, allowedSCMProviders []string, enableSCMProviders bool) Generator {
return &SCMProviderGenerator{
client: client,
SCMConfig: scmConfig,
client: client,
SCMAuthProviders: providers,
scmRootCAPath: scmRootCAPath,
allowedSCMProviders: allowedSCMProviders,
enableSCMProviders: enableSCMProviders,
}
}
// Testing generator
func NewTestSCMProviderGenerator(overrideProvider scm_provider.SCMProviderService) Generator {
return &SCMProviderGenerator{overrideProvider: overrideProvider, SCMConfig: SCMConfig{
enableSCMProviders: true,
}}
return &SCMProviderGenerator{overrideProvider: overrideProvider, enableSCMProviders: true}
}
func (g *SCMProviderGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration {
@@ -73,7 +61,7 @@ func (g *SCMProviderGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alph
return time.Duration(*appSetGenerator.SCMProvider.RequeueAfterSeconds) * time.Second
}
return DefaultSCMProviderRequeueAfter
return DefaultSCMProviderRequeueAfterSeconds
}
func (g *SCMProviderGenerator) GetTemplate(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) *argoprojiov1alpha1.ApplicationSetTemplate {
@@ -120,13 +108,13 @@ func ScmProviderAllowed(applicationSetInfo *argoprojiov1alpha1.ApplicationSet, g
return NewErrDisallowedSCMProvider(url, allowedScmProviders)
}
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]interface{}, error) {
if appSetGenerator == nil {
return nil, ErrEmptyAppSetGenerator
return nil, EmptyAppSetGeneratorError
}
if appSetGenerator.SCMProvider == nil {
return nil, ErrEmptyAppSetGenerator
return nil, EmptyAppSetGeneratorError
}
if !g.enableSCMProviders {
@@ -142,97 +130,73 @@ func (g *SCMProviderGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha
ctx := context.Background()
var provider scm_provider.SCMProviderService
switch {
case g.overrideProvider != nil:
if g.overrideProvider != nil {
provider = g.overrideProvider
case providerConfig.Github != nil:
} else if providerConfig.Github != nil {
var err error
provider, err = g.githubProvider(ctx, providerConfig.Github, applicationSetInfo)
if err != nil {
return nil, fmt.Errorf("scm provider: %w", err)
}
case providerConfig.Gitlab != nil:
providerConfig := providerConfig.Gitlab
var caCerts []byte
var scmError error
if providerConfig.CARef != nil {
caCerts, scmError = utils.GetConfigMapData(ctx, g.client, providerConfig.CARef, applicationSetInfo.Namespace)
if scmError != nil {
return nil, fmt.Errorf("error fetching CA certificates from ConfigMap: %w", scmError)
}
}
token, err := utils.GetSecretRef(ctx, g.client, providerConfig.TokenRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
} else if providerConfig.Gitlab != nil {
token, err := g.getSecretRef(ctx, providerConfig.Gitlab.TokenRef, applicationSetInfo.Namespace)
if err != nil {
return nil, fmt.Errorf("error fetching Gitlab token: %w", err)
}
provider, err = scm_provider.NewGitlabProvider(providerConfig.Group, token, providerConfig.API, providerConfig.AllBranches, providerConfig.IncludeSubgroups, providerConfig.WillIncludeSharedProjects(), providerConfig.Insecure, g.scmRootCAPath, providerConfig.Topic, caCerts)
provider, err = scm_provider.NewGitlabProvider(ctx, providerConfig.Gitlab.Group, token, providerConfig.Gitlab.API, providerConfig.Gitlab.AllBranches, providerConfig.Gitlab.IncludeSubgroups, providerConfig.Gitlab.WillIncludeSharedProjects(), providerConfig.Gitlab.Insecure, g.scmRootCAPath, providerConfig.Gitlab.Topic)
if err != nil {
return nil, fmt.Errorf("error initializing Gitlab service: %w", err)
}
case providerConfig.Gitea != nil:
token, err := utils.GetSecretRef(ctx, g.client, providerConfig.Gitea.TokenRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
} else if providerConfig.Gitea != nil {
token, err := g.getSecretRef(ctx, providerConfig.Gitea.TokenRef, applicationSetInfo.Namespace)
if err != nil {
return nil, fmt.Errorf("error fetching Gitea token: %w", err)
}
provider, err = scm_provider.NewGiteaProvider(providerConfig.Gitea.Owner, token, providerConfig.Gitea.API, providerConfig.Gitea.AllBranches, providerConfig.Gitea.Insecure)
provider, err = scm_provider.NewGiteaProvider(ctx, providerConfig.Gitea.Owner, token, providerConfig.Gitea.API, providerConfig.Gitea.AllBranches, providerConfig.Gitea.Insecure)
if err != nil {
return nil, fmt.Errorf("error initializing Gitea service: %w", err)
}
case providerConfig.BitbucketServer != nil:
} else if providerConfig.BitbucketServer != nil {
providerConfig := providerConfig.BitbucketServer
var caCerts []byte
var scmError error
if providerConfig.CARef != nil {
caCerts, scmError = utils.GetConfigMapData(ctx, g.client, providerConfig.CARef, applicationSetInfo.Namespace)
if scmError != nil {
return nil, fmt.Errorf("error fetching CA certificates from ConfigMap: %w", scmError)
}
}
switch {
case providerConfig.BearerToken != nil:
appToken, err := utils.GetSecretRef(ctx, g.client, providerConfig.BearerToken.TokenRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
if err != nil {
return nil, fmt.Errorf("error fetching Secret Bearer token: %w", err)
}
provider, scmError = scm_provider.NewBitbucketServerProviderBearerToken(ctx, appToken, providerConfig.API, providerConfig.Project, providerConfig.AllBranches, g.scmRootCAPath, providerConfig.Insecure, caCerts)
case providerConfig.BasicAuth != nil:
password, err := utils.GetSecretRef(ctx, g.client, providerConfig.BasicAuth.PasswordRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
if providerConfig.BasicAuth != nil {
password, err := g.getSecretRef(ctx, providerConfig.BasicAuth.PasswordRef, applicationSetInfo.Namespace)
if err != nil {
return nil, fmt.Errorf("error fetching Secret token: %w", err)
}
provider, scmError = scm_provider.NewBitbucketServerProviderBasicAuth(ctx, providerConfig.BasicAuth.Username, password, providerConfig.API, providerConfig.Project, providerConfig.AllBranches, g.scmRootCAPath, providerConfig.Insecure, caCerts)
default:
provider, scmError = scm_provider.NewBitbucketServerProviderNoAuth(ctx, providerConfig.API, providerConfig.Project, providerConfig.AllBranches, g.scmRootCAPath, providerConfig.Insecure, caCerts)
provider, scmError = scm_provider.NewBitbucketServerProviderBasicAuth(ctx, providerConfig.BasicAuth.Username, password, providerConfig.API, providerConfig.Project, providerConfig.AllBranches)
} else {
provider, scmError = scm_provider.NewBitbucketServerProviderNoAuth(ctx, providerConfig.API, providerConfig.Project, providerConfig.AllBranches)
}
if scmError != nil {
return nil, fmt.Errorf("error initializing Bitbucket Server service: %w", scmError)
}
case providerConfig.AzureDevOps != nil:
token, err := utils.GetSecretRef(ctx, g.client, providerConfig.AzureDevOps.AccessTokenRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
} else if providerConfig.AzureDevOps != nil {
token, err := g.getSecretRef(ctx, providerConfig.AzureDevOps.AccessTokenRef, applicationSetInfo.Namespace)
if err != nil {
return nil, fmt.Errorf("error fetching Azure Devops access token: %w", err)
}
provider, err = scm_provider.NewAzureDevOpsProvider(token, providerConfig.AzureDevOps.Organization, providerConfig.AzureDevOps.API, providerConfig.AzureDevOps.TeamProject, providerConfig.AzureDevOps.AllBranches)
provider, err = scm_provider.NewAzureDevOpsProvider(ctx, token, providerConfig.AzureDevOps.Organization, providerConfig.AzureDevOps.API, providerConfig.AzureDevOps.TeamProject, providerConfig.AzureDevOps.AllBranches)
if err != nil {
return nil, fmt.Errorf("error initializing Azure Devops service: %w", err)
}
case providerConfig.Bitbucket != nil:
appPassword, err := utils.GetSecretRef(ctx, g.client, providerConfig.Bitbucket.AppPasswordRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
} else if providerConfig.Bitbucket != nil {
appPassword, err := g.getSecretRef(ctx, providerConfig.Bitbucket.AppPasswordRef, applicationSetInfo.Namespace)
if err != nil {
return nil, fmt.Errorf("error fetching Bitbucket cloud appPassword: %w", err)
}
provider, err = scm_provider.NewBitBucketCloudProvider(providerConfig.Bitbucket.Owner, providerConfig.Bitbucket.User, appPassword, providerConfig.Bitbucket.AllBranches)
provider, err = scm_provider.NewBitBucketCloudProvider(ctx, providerConfig.Bitbucket.Owner, providerConfig.Bitbucket.User, appPassword, providerConfig.Bitbucket.AllBranches)
if err != nil {
return nil, fmt.Errorf("error initializing Bitbucket cloud service: %w", err)
}
case providerConfig.AWSCodeCommit != nil:
} else if providerConfig.AWSCodeCommit != nil {
var awsErr error
provider, awsErr = scm_provider.NewAWSCodeCommitProvider(ctx, providerConfig.AWSCodeCommit.TagFilters, providerConfig.AWSCodeCommit.Role, providerConfig.AWSCodeCommit.Region, providerConfig.AWSCodeCommit.AllBranches)
if awsErr != nil {
return nil, fmt.Errorf("error initializing AWS codecommit service: %w", awsErr)
}
default:
return nil, errors.New("no SCM provider implementation configured")
} else {
return nil, fmt.Errorf("no SCM provider implementation configured")
}
// Find all the available repos.
@@ -240,7 +204,7 @@ func (g *SCMProviderGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha
if err != nil {
return nil, fmt.Errorf("error listing repos: %w", err)
}
paramsArray := make([]map[string]any, 0, len(repos))
paramsArray := make([]map[string]interface{}, 0, len(repos))
var shortSHALength int
var shortSHALength7 int
for _, repo := range repos {
@@ -254,10 +218,9 @@ func (g *SCMProviderGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha
shortSHALength7 = len(repo.SHA)
}
params := map[string]any{
params := map[string]interface{}{
"organization": repo.Organization,
"repository": repo.Repository,
"repository_id": repo.RepositoryId,
"url": repo.URL,
"branch": repo.Branch,
"sha": repo.SHA,
@@ -277,37 +240,47 @@ func (g *SCMProviderGenerator) GenerateParams(appSetGenerator *argoprojiov1alpha
return paramsArray, nil
}
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)
func (g *SCMProviderGenerator) getSecretRef(ctx context.Context, ref *argoprojiov1alpha1.SecretRef, namespace string) (string, error) {
if ref == nil {
return "", nil
}
secret := &corev1.Secret{}
err := g.client.Get(
ctx,
client.ObjectKey{
Name: ref.SecretName,
Namespace: namespace,
},
secret)
if err != nil {
return "", fmt.Errorf("error fetching secret %s/%s: %w", namespace, ref.SecretName, err)
}
tokenBytes, ok := secret.Data[ref.Key]
if !ok {
return "", fmt.Errorf("key %q in secret %s/%s not found", ref.Key, namespace, ref.SecretName)
}
return string(tokenBytes), nil
}
func (g *SCMProviderGenerator) githubProvider(ctx context.Context, github *argoprojiov1alpha1.SCMProviderGeneratorGithub, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) (scm_provider.SCMProviderService, error) {
if github.AppSecretName != "" {
auth, err := g.GitHubApps.GetAuthSecret(ctx, github.AppSecretName)
if err != nil {
return nil, fmt.Errorf("error fetching Github app secret: %w", err)
}
if g.enableGitHubAPIMetrics {
return scm_provider.NewGithubAppProviderFor(*auth, github.Organization, github.API, github.AllBranches, httpClient)
}
return scm_provider.NewGithubAppProviderFor(*auth, github.Organization, github.API, github.AllBranches)
return scm_provider.NewGithubAppProviderFor(
*auth,
github.Organization,
github.API,
github.AllBranches,
)
}
token, err := utils.GetSecretRef(ctx, g.client, github.TokenRef, applicationSetInfo.Namespace, g.tokenRefStrictMode)
token, err := g.getSecretRef(ctx, github.TokenRef, applicationSetInfo.Namespace)
if err != nil {
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(ctx, github.Organization, token, github.API, github.AllBranches)
}

View File

@@ -1,24 +1,90 @@
package generators
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/argoproj/argo-cd/v3/applicationset/services/scm_provider"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/applicationset/services/scm_provider"
argoprojiov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
func TestSCMProviderGenerateParams(t *testing.T) {
t.Parallel()
func TestSCMProviderGetSecretRef(t *testing.T) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{Name: "test-secret", Namespace: "test"},
Data: map[string][]byte{
"my-token": []byte("secret"),
},
}
gen := &SCMProviderGenerator{client: fake.NewClientBuilder().WithObjects(secret).Build()}
ctx := context.Background()
cases := []struct {
name, namespace, token string
ref *argoprojiov1alpha1.SecretRef
hasError bool
}{
{
name: "valid ref",
ref: &argoprojiov1alpha1.SecretRef{SecretName: "test-secret", Key: "my-token"},
namespace: "test",
token: "secret",
hasError: false,
},
{
name: "nil ref",
ref: nil,
namespace: "test",
token: "",
hasError: false,
},
{
name: "wrong name",
ref: &argoprojiov1alpha1.SecretRef{SecretName: "other", Key: "my-token"},
namespace: "test",
token: "",
hasError: true,
},
{
name: "wrong key",
ref: &argoprojiov1alpha1.SecretRef{SecretName: "test-secret", Key: "other-token"},
namespace: "test",
token: "",
hasError: true,
},
{
name: "wrong namespace",
ref: &argoprojiov1alpha1.SecretRef{SecretName: "test-secret", Key: "my-token"},
namespace: "other",
token: "",
hasError: true,
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
token, err := gen.getSecretRef(ctx, c.ref, c.namespace)
if c.hasError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
assert.Equal(t, c.token, token)
})
}
}
func TestSCMProviderGenerateParams(t *testing.T) {
cases := []struct {
name string
repos []*scm_provider.Repository
values map[string]string
expected []map[string]any
expected []map[string]interface{}
expectedError error
}{
{
@@ -27,7 +93,6 @@ func TestSCMProviderGenerateParams(t *testing.T) {
{
Organization: "myorg",
Repository: "repo1",
RepositoryId: 190320251,
URL: "git@github.com:myorg/repo1.git",
Branch: "main",
SHA: "0bc57212c3cbbec69d20b34c507284bd300def5b",
@@ -36,17 +101,15 @@ func TestSCMProviderGenerateParams(t *testing.T) {
{
Organization: "myorg",
Repository: "repo2",
RepositoryId: 190320252,
URL: "git@github.com:myorg/repo2.git",
Branch: "main",
SHA: "59d0",
},
},
expected: []map[string]any{
expected: []map[string]interface{}{
{
"organization": "myorg",
"repository": "repo1",
"repository_id": 190320251,
"url": "git@github.com:myorg/repo1.git",
"branch": "main",
"branchNormalized": "main",
@@ -58,7 +121,6 @@ func TestSCMProviderGenerateParams(t *testing.T) {
{
"organization": "myorg",
"repository": "repo2",
"repository_id": 190320252,
"url": "git@github.com:myorg/repo2.git",
"branch": "main",
"branchNormalized": "main",
@@ -75,7 +137,6 @@ func TestSCMProviderGenerateParams(t *testing.T) {
{
Organization: "myorg",
Repository: "repo3",
RepositoryId: 190320253,
URL: "git@github.com:myorg/repo3.git",
Branch: "main",
SHA: "0bc57212c3cbbec69d20b34c507284bd300def5b",
@@ -86,11 +147,10 @@ func TestSCMProviderGenerateParams(t *testing.T) {
"foo": "bar",
"should_i_force_push_to": "{{ branch }}?",
},
expected: []map[string]any{
expected: []map[string]interface{}{
{
"organization": "myorg",
"repository": "repo3",
"repository_id": 190320253,
"url": "git@github.com:myorg/repo3.git",
"branch": "main",
"branchNormalized": "main",
@@ -103,52 +163,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 {
@@ -160,7 +174,7 @@ func TestSCMProviderGenerateParams(t *testing.T) {
mockProvider := &scm_provider.MockProvider{
Repos: testCaseCopy.repos,
}
scmGenerator := &SCMProviderGenerator{overrideProvider: mockProvider, SCMConfig: SCMConfig{enableSCMProviders: true}}
scmGenerator := &SCMProviderGenerator{overrideProvider: mockProvider, enableSCMProviders: true}
applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "set",
@@ -187,11 +201,10 @@ func TestSCMProviderGenerateParams(t *testing.T) {
}
func TestAllowedSCMProvider(t *testing.T) {
t.Parallel()
cases := []struct {
name string
providerConfig *argoprojiov1alpha1.SCMProviderGenerator
expectedError error
}{
{
name: "Error Github",
@@ -200,6 +213,7 @@ func TestAllowedSCMProvider(t *testing.T) {
API: "https://myservice.mynamespace.svc.cluster.local",
},
},
expectedError: &ErrDisallowedSCMProvider{},
},
{
name: "Error Gitlab",
@@ -208,6 +222,7 @@ func TestAllowedSCMProvider(t *testing.T) {
API: "https://myservice.mynamespace.svc.cluster.local",
},
},
expectedError: &ErrDisallowedSCMProvider{},
},
{
name: "Error Gitea",
@@ -216,6 +231,7 @@ func TestAllowedSCMProvider(t *testing.T) {
API: "https://myservice.mynamespace.svc.cluster.local",
},
},
expectedError: &ErrDisallowedSCMProvider{},
},
{
name: "Error Bitbucket",
@@ -224,6 +240,7 @@ func TestAllowedSCMProvider(t *testing.T) {
API: "https://myservice.mynamespace.svc.cluster.local",
},
},
expectedError: &ErrDisallowedSCMProvider{},
},
{
name: "Error AzureDevops",
@@ -232,6 +249,7 @@ func TestAllowedSCMProvider(t *testing.T) {
API: "https://myservice.mynamespace.svc.cluster.local",
},
},
expectedError: &ErrDisallowedSCMProvider{},
},
}
@@ -242,16 +260,14 @@ func TestAllowedSCMProvider(t *testing.T) {
t.Parallel()
scmGenerator := &SCMProviderGenerator{
SCMConfig: SCMConfig{
allowedSCMProviders: []string{
"github.myorg.com",
"gitlab.myorg.com",
"gitea.myorg.com",
"bitbucket.myorg.com",
"azuredevops.myorg.com",
},
enableSCMProviders: true,
allowedSCMProviders: []string{
"github.myorg.com",
"gitlab.myorg.com",
"gitea.myorg.com",
"bitbucket.myorg.com",
"azuredevops.myorg.com",
},
enableSCMProviders: true,
}
applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
@@ -268,14 +284,13 @@ func TestAllowedSCMProvider(t *testing.T) {
_, err := scmGenerator.GenerateParams(&applicationSetInfo.Spec.Generators[0], &applicationSetInfo, nil)
require.Error(t, err, "Must return an error")
var expectedError ErrDisallowedSCMProvider
assert.ErrorAs(t, err, &expectedError)
assert.ErrorAs(t, err, testCaseCopy.expectedError)
})
}
}
func TestSCMProviderDisabled_SCMGenerator(t *testing.T) {
generator := &SCMProviderGenerator{SCMConfig: SCMConfig{enableSCMProviders: false}}
generator := &SCMProviderGenerator{enableSCMProviders: false}
applicationSetInfo := argoprojiov1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{

View File

@@ -1,5 +1,5 @@
package generators
type SCMGeneratorWithCustomApiUrl interface { //nolint:revive //FIXME(var-naming)
type SCMGeneratorWithCustomApiUrl interface {
CustomApiUrl() string
}

View File

@@ -1,49 +0,0 @@
package generators
import (
"context"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/argoproj/argo-cd/v3/applicationset/services"
)
func GetGenerators(ctx context.Context, c client.Client, k8sClient kubernetes.Interface, namespace string, argoCDService services.Repos, dynamicClient dynamic.Interface, scmConfig SCMConfig) map[string]Generator {
terminalGenerators := map[string]Generator{
"List": NewListGenerator(),
"Clusters": NewClusterGenerator(ctx, c, k8sClient, namespace),
"Git": NewGitGenerator(argoCDService, namespace),
"SCMProvider": NewSCMProviderGenerator(c, scmConfig),
"ClusterDecisionResource": NewDuckTypeGenerator(ctx, dynamicClient, k8sClient, namespace),
"PullRequest": NewPullRequestGenerator(c, scmConfig),
"Plugin": NewPluginGenerator(c, namespace),
}
nestedGenerators := map[string]Generator{
"List": terminalGenerators["List"],
"Clusters": terminalGenerators["Clusters"],
"Git": terminalGenerators["Git"],
"SCMProvider": terminalGenerators["SCMProvider"],
"ClusterDecisionResource": terminalGenerators["ClusterDecisionResource"],
"PullRequest": terminalGenerators["PullRequest"],
"Plugin": terminalGenerators["Plugin"],
"Matrix": NewMatrixGenerator(terminalGenerators),
"Merge": NewMergeGenerator(terminalGenerators),
}
topLevelGenerators := map[string]Generator{
"List": terminalGenerators["List"],
"Clusters": terminalGenerators["Clusters"],
"Git": terminalGenerators["Git"],
"SCMProvider": terminalGenerators["SCMProvider"],
"ClusterDecisionResource": terminalGenerators["ClusterDecisionResource"],
"PullRequest": terminalGenerators["PullRequest"],
"Plugin": terminalGenerators["Plugin"],
"Matrix": NewMatrixGenerator(nestedGenerators),
"Merge": NewMergeGenerator(nestedGenerators),
}
return topLevelGenerators
}

View File

@@ -4,11 +4,11 @@ import (
"fmt"
)
func appendTemplatedValues(values map[string]string, params map[string]any, useGoTemplate bool, goTemplateOptions []string) error {
func appendTemplatedValues(values map[string]string, params map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) error {
// We create a local map to ensure that we do not fall victim to a billion-laughs attack. We iterate through the
// cluster values map and only replace values in said map if it has already been allowlisted in the params map.
// Once we iterate through all the cluster values we can then safely merge the `tmp` map into the main params map.
tmp := map[string]any{}
tmp := map[string]interface{}{}
for key, value := range values {
result, err := replaceTemplatedString(value, params, useGoTemplate, goTemplateOptions)
@@ -22,7 +22,7 @@ func appendTemplatedValues(values map[string]string, params map[string]any, useG
}
tmp["values"].(map[string]string)[key] = result
} else {
tmp["values."+key] = result
tmp[fmt.Sprintf("values.%s", key)] = result
}
}
@@ -33,7 +33,7 @@ func appendTemplatedValues(values map[string]string, params map[string]any, useG
return nil
}
func replaceTemplatedString(value string, params map[string]any, useGoTemplate bool, goTemplateOptions []string) (string, error) {
func replaceTemplatedString(value string, params map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (string, error) {
replacedTmplStr, err := render.Replace(value, params, useGoTemplate, goTemplateOptions)
if err != nil {
return "", fmt.Errorf("failed to replace templated string with rendered values: %w", err)

View File

@@ -11,18 +11,18 @@ func TestValueInterpolation(t *testing.T) {
testCases := []struct {
name string
values map[string]string
params map[string]any
expected map[string]any
params map[string]interface{}
expected map[string]interface{}
}{
{
name: "Simple interpolation",
values: map[string]string{
"hello": "{{ world }}",
},
params: map[string]any{
params: map[string]interface{}{
"world": "world!",
},
expected: map[string]any{
expected: map[string]interface{}{
"world": "world!",
"values.hello": "world!",
},
@@ -32,8 +32,8 @@ func TestValueInterpolation(t *testing.T) {
values: map[string]string{
"non-existent": "{{ non-existent }}",
},
params: map[string]any{},
expected: map[string]any{
params: map[string]interface{}{},
expected: map[string]interface{}{
"values.non-existent": "{{ non-existent }}",
},
},
@@ -44,8 +44,8 @@ func TestValueInterpolation(t *testing.T) {
"lol2": "{{values.lol1}}{{values.lol1}}",
"lol3": "{{values.lol2}}{{values.lol2}}{{values.lol2}}",
},
params: map[string]any{},
expected: map[string]any{
params: map[string]interface{}{},
expected: map[string]interface{}{
"values.lol1": "lol",
"values.lol2": "{{values.lol1}}{{values.lol1}}",
"values.lol3": "{{values.lol2}}{{values.lol2}}{{values.lol2}}",
@@ -57,7 +57,7 @@ func TestValueInterpolation(t *testing.T) {
t.Run(testCase.name, func(t *testing.T) {
err := appendTemplatedValues(testCase.values, testCase.params, false, nil)
require.NoError(t, err)
assert.Equal(t, testCase.expected, testCase.params)
assert.EqualValues(t, testCase.expected, testCase.params)
})
}
}
@@ -66,18 +66,18 @@ func TestValueInterpolationWithGoTemplating(t *testing.T) {
testCases := []struct {
name string
values map[string]string
params map[string]any
expected map[string]any
params map[string]interface{}
expected map[string]interface{}
}{
{
name: "Simple interpolation",
values: map[string]string{
"hello": "{{ .world }}",
},
params: map[string]any{
params: map[string]interface{}{
"world": "world!",
},
expected: map[string]any{
expected: map[string]interface{}{
"world": "world!",
"values": map[string]string{
"hello": "world!",
@@ -89,8 +89,8 @@ func TestValueInterpolationWithGoTemplating(t *testing.T) {
values: map[string]string{
"non_existent": "{{ default \"bar\" .non_existent }}",
},
params: map[string]any{},
expected: map[string]any{
params: map[string]interface{}{},
expected: map[string]interface{}{
"values": map[string]string{
"non_existent": "bar",
},
@@ -103,8 +103,8 @@ func TestValueInterpolationWithGoTemplating(t *testing.T) {
"lol2": "{{.values.lol1}}{{.values.lol1}}",
"lol3": "{{.values.lol2}}{{.values.lol2}}{{.values.lol2}}",
},
params: map[string]any{},
expected: map[string]any{
params: map[string]interface{}{},
expected: map[string]interface{}{
"values": map[string]string{
"lol1": "lol",
"lol2": "<no value><no value>",
@@ -118,7 +118,7 @@ func TestValueInterpolationWithGoTemplating(t *testing.T) {
t.Run(testCase.name, func(t *testing.T) {
err := appendTemplatedValues(testCase.values, testCase.params, true, nil)
require.NoError(t, err)
assert.Equal(t, testCase.expected, testCase.params)
assert.EqualValues(t, testCase.expected, testCase.params)
})
}
}

View File

@@ -1,21 +0,0 @@
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
)
// Fake implementation for testing
func NewFakeAppsetMetrics() *ApplicationsetMetrics {
reconcileHistogram := prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "argocd_appset_reconcile",
Help: "Application reconciliation performance in seconds.",
// Buckets can be set later on after observing median time
},
[]string{"name", "namespace"},
)
return &ApplicationsetMetrics{
reconcileHistogram: reconcileHistogram,
}
}

View File

@@ -1,135 +0,0 @@
package metrics
import (
"time"
"github.com/prometheus/client_golang/prometheus"
"k8s.io/apimachinery/pkg/labels"
"sigs.k8s.io/controller-runtime/pkg/metrics"
argoappv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
applisters "github.com/argoproj/argo-cd/v3/pkg/client/listers/application/v1alpha1"
metricsutil "github.com/argoproj/argo-cd/v3/util/metrics"
"github.com/argoproj/argo-cd/v3/util/metrics/kubectl"
)
var (
descAppsetLabels *prometheus.Desc
descAppsetDefaultLabels = []string{"namespace", "name"}
descAppsetInfo = prometheus.NewDesc(
"argocd_appset_info",
"Information about applicationset",
append(descAppsetDefaultLabels, "resource_update_status"),
nil,
)
descAppsetGeneratedApps = prometheus.NewDesc(
"argocd_appset_owned_applications",
"Number of applications owned by the applicationset",
descAppsetDefaultLabels,
nil,
)
)
type ApplicationsetMetrics struct {
reconcileHistogram *prometheus.HistogramVec
}
type appsetCollector struct {
lister applisters.ApplicationSetLister
// appsClientSet appclientset.Interface
labels []string
filter func(appset *argoappv1.ApplicationSet) bool
}
func NewApplicationsetMetrics(appsetLister applisters.ApplicationSetLister, appsetLabels []string, appsetFilter func(appset *argoappv1.ApplicationSet) bool) ApplicationsetMetrics {
reconcileHistogram := prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "argocd_appset_reconcile",
Help: "Application reconciliation performance in seconds.",
// Buckets can be set later on after observing median time
},
descAppsetDefaultLabels,
)
appsetCollector := newAppsetCollector(appsetLister, appsetLabels, appsetFilter)
// Register collectors and metrics
metrics.Registry.MustRegister(reconcileHistogram)
metrics.Registry.MustRegister(appsetCollector)
kubectl.RegisterWithClientGo()
kubectl.RegisterWithPrometheus(metrics.Registry)
return ApplicationsetMetrics{
reconcileHistogram: reconcileHistogram,
}
}
func (m *ApplicationsetMetrics) ObserveReconcile(appset *argoappv1.ApplicationSet, duration time.Duration) {
m.reconcileHistogram.WithLabelValues(appset.Namespace, appset.Name).Observe(duration.Seconds())
}
func newAppsetCollector(lister applisters.ApplicationSetLister, labels []string, filter func(appset *argoappv1.ApplicationSet) bool) *appsetCollector {
descAppsetDefaultLabels = []string{"namespace", "name"}
if len(labels) > 0 {
descAppsetLabels = prometheus.NewDesc(
"argocd_appset_labels",
"Applicationset labels translated to Prometheus labels",
append(descAppsetDefaultLabels, metricsutil.NormalizeLabels("label", labels)...),
nil,
)
}
return &appsetCollector{
lister: lister,
labels: labels,
filter: filter,
}
}
// Describe implements the prometheus.Collector interface
func (c *appsetCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- descAppsetInfo
ch <- descAppsetGeneratedApps
if len(c.labels) > 0 {
ch <- descAppsetLabels
}
}
// Collect implements the prometheus.Collector interface
func (c *appsetCollector) Collect(ch chan<- prometheus.Metric) {
appsets, _ := c.lister.List(labels.NewSelector())
for _, appset := range appsets {
if c.filter(appset) {
collectAppset(appset, c.labels, ch)
}
}
}
func collectAppset(appset *argoappv1.ApplicationSet, labelsToCollect []string, ch chan<- prometheus.Metric) {
labelValues := make([]string, 0)
commonLabelValues := []string{appset.Namespace, appset.Name}
for _, label := range labelsToCollect {
labelValues = append(labelValues, appset.GetLabels()[label])
}
resourceUpdateStatus := "Unknown"
for _, condition := range appset.Status.Conditions {
if condition.Type == argoappv1.ApplicationSetConditionResourcesUpToDate {
resourceUpdateStatus = condition.Reason
}
}
if len(labelsToCollect) > 0 {
ch <- prometheus.MustNewConstMetric(descAppsetLabels, prometheus.GaugeValue, 1, append(commonLabelValues, labelValues...)...)
}
ch <- prometheus.MustNewConstMetric(descAppsetInfo, prometheus.GaugeValue, 1, appset.Namespace, appset.Name, resourceUpdateStatus)
ch <- prometheus.MustNewConstMetric(descAppsetGeneratedApps, prometheus.GaugeValue, float64(len(appset.Status.Resources)), appset.Namespace, appset.Name)
}

View File

@@ -1,252 +0,0 @@
package metrics
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/runtime"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/metrics"
"sigs.k8s.io/yaml"
"github.com/argoproj/argo-cd/v3/applicationset/utils"
argoappv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
metricsutil "github.com/argoproj/argo-cd/v3/util/metrics"
)
var (
applicationsetNamespaces = []string{"argocd", "test-namespace1"}
filter = func(appset *argoappv1.ApplicationSet) bool {
return utils.IsNamespaceAllowed(applicationsetNamespaces, appset.Namespace)
}
collectedLabels = []string{"included/test"}
)
const fakeAppsetList = `
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: test1
namespace: argocd
labels:
included/test: test
not-included.label/test: test
spec:
generators:
- git:
directories:
- path: test/*
repoURL: https://github.com/test/test.git
revision: HEAD
template:
metadata:
name: '{{.path.basename}}'
spec:
destination:
namespace: '{{.path.basename}}'
server: https://kubernetes.default.svc
project: default
source:
path: '{{.path.path}}'
repoURL: https://github.com/test/test.git
targetRevision: HEAD
status:
resources:
- group: argoproj.io
health:
status: Missing
kind: Application
name: test-app1
namespace: argocd
status: OutOfSync
version: v1alpha1
- group: argoproj.io
health:
status: Missing
kind: Application
name: test-app2
namespace: argocd
status: OutOfSync
version: v1alpha1
conditions:
- lastTransitionTime: "2024-01-01T00:00:00Z"
message: Successfully generated parameters for all Applications
reason: ApplicationSetUpToDate
status: "False"
type: ErrorOccurred
- lastTransitionTime: "2024-01-01T00:00:00Z"
message: Successfully generated parameters for all Applications
reason: ParametersGenerated
status: "True"
type: ParametersGenerated
- lastTransitionTime: "2024-01-01T00:00:00Z"
message: ApplicationSet up to date
reason: ApplicationSetUpToDate
status: "True"
type: ResourcesUpToDate
---
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: test2
namespace: argocd
labels:
not-included.label/test: test
spec:
generators:
- git:
directories:
- path: test/*
repoURL: https://github.com/test/test.git
revision: HEAD
template:
metadata:
name: '{{.path.basename}}'
spec:
destination:
namespace: '{{.path.basename}}'
server: https://kubernetes.default.svc
project: default
source:
path: '{{.path.path}}'
repoURL: https://github.com/test/test.git
targetRevision: HEAD
---
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: should-be-filtered-out
namespace: not-allowed
spec:
generators:
- git:
directories:
- path: test/*
repoURL: https://github.com/test/test.git
revision: HEAD
template:
metadata:
name: '{{.path.basename}}'
spec:
destination:
namespace: '{{.path.basename}}'
server: https://kubernetes.default.svc
project: default
source:
path: '{{.path.path}}'
repoURL: https://github.com/test/test.git
targetRevision: HEAD
`
func newFakeAppsets(fakeAppsetYAML string) []argoappv1.ApplicationSet {
var results []argoappv1.ApplicationSet
appsetRawYamls := strings.Split(fakeAppsetYAML, "---")
for _, appsetRawYaml := range appsetRawYamls {
var appset argoappv1.ApplicationSet
err := yaml.Unmarshal([]byte(appsetRawYaml), &appset)
if err != nil {
panic(err)
}
results = append(results, appset)
}
return results
}
func TestApplicationsetCollector(t *testing.T) {
appsetList := newFakeAppsets(fakeAppsetList)
client := initializeClient(appsetList)
metrics.Registry = prometheus.NewRegistry()
appsetCollector := newAppsetCollector(utils.NewAppsetLister(client), collectedLabels, filter)
metrics.Registry.MustRegister(appsetCollector)
req, err := http.NewRequest(http.MethodGet, "/metrics", http.NoBody)
require.NoError(t, err)
rr := httptest.NewRecorder()
handler := promhttp.HandlerFor(metrics.Registry, promhttp.HandlerOpts{})
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
// Test correct appset_info and owned applications
assert.Contains(t, rr.Body.String(), `
argocd_appset_info{name="test1",namespace="argocd",resource_update_status="ApplicationSetUpToDate"} 1
`)
assert.Contains(t, rr.Body.String(), `
argocd_appset_owned_applications{name="test1",namespace="argocd"} 2
`)
// Test labels collection - should not include labels not included in the list of collected labels and include the ones that do.
assert.Contains(t, rr.Body.String(), `
argocd_appset_labels{label_included_test="test",name="test1",namespace="argocd"} 1
`)
assert.NotContains(t, rr.Body.String(), normalizeLabel("not-included.label/test"))
// If collected label is not present on the applicationset the value should be empty
assert.Contains(t, rr.Body.String(), `
argocd_appset_labels{label_included_test="",name="test2",namespace="argocd"} 1
`)
// If ResourcesUpToDate condition is not present on the applicationset the status should be reported as 'Unknown'
assert.Contains(t, rr.Body.String(), `
argocd_appset_info{name="test2",namespace="argocd",resource_update_status="Unknown"} 1
`)
// If there are no resources on the applicationset the owned application gague should return 0
assert.Contains(t, rr.Body.String(), `
argocd_appset_owned_applications{name="test2",namespace="argocd"} 0
`)
// Test that filter is working
assert.NotContains(t, rr.Body.String(), `name="should-be-filtered-out"`)
}
func TestObserveReconcile(t *testing.T) {
appsetList := newFakeAppsets(fakeAppsetList)
client := initializeClient(appsetList)
metrics.Registry = prometheus.NewRegistry()
appsetMetrics := NewApplicationsetMetrics(utils.NewAppsetLister(client), collectedLabels, filter)
req, err := http.NewRequest(http.MethodGet, "/metrics", http.NoBody)
require.NoError(t, err)
rr := httptest.NewRecorder()
handler := promhttp.HandlerFor(metrics.Registry, promhttp.HandlerOpts{})
appsetMetrics.ObserveReconcile(&appsetList[0], 5*time.Second)
handler.ServeHTTP(rr, req)
assert.Contains(t, rr.Body.String(), `
argocd_appset_reconcile_sum{name="test1",namespace="argocd"} 5
`)
// If there are no resources on the applicationset the owned application gague should return 0
assert.Contains(t, rr.Body.String(), `
argocd_appset_reconcile_count{name="test1",namespace="argocd"} 1
`)
}
func initializeClient(appsets []argoappv1.ApplicationSet) ctrlclient.WithWatch {
scheme := runtime.NewScheme()
err := argoappv1.AddToScheme(scheme)
if err != nil {
panic(err)
}
var clientObjects []ctrlclient.Object
for _, appset := range appsets {
clientObjects = append(clientObjects, appset.DeepCopy())
}
return fake.NewClientBuilder().WithScheme(scheme).WithObjects(clientObjects...).Build()
}
func normalizeLabel(label string) string {
return metricsutil.NormalizeLabels("label", []string{label})[0]
}

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"
"github.com/bradleyfalzon/ghinstallation/v2"
"github.com/google/go-github/v69/github"
"github.com/google/go-github/v35/github"
"github.com/argoproj/argo-cd/v3/applicationset/services/github_app_auth"
appsetutils "github.com/argoproj/argo-cd/v3/applicationset/utils"
"github.com/argoproj/argo-cd/v2/applicationset/services/github_app_auth"
)
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.
func Client(g github_app_auth.Authentication, url string, optionalHTTPClient ...*http.Client) (*github.Client, error) {
httpClient, transport := getOptionalHTTPClientAndTransport(optionalHTTPClient...)
rt, err := ghinstallation.New(transport, g.Id, g.InstallationId, []byte(g.PrivateKey))
func Client(g github_app_auth.Authentication, url string) (*github.Client, error) {
rt, err := ghinstallation.New(http.DefaultTransport, g.Id, g.InstallationId, []byte(g.PrivateKey))
if err != nil {
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
}
var client *github.Client
httpClient.Transport = rt
if url == "" {
client = github.NewClient(httpClient)
httpClient := http.Client{Transport: rt}
client = github.NewClient(&httpClient)
} else {
rt.BaseURL = url
client, err = github.NewClient(httpClient).WithEnterpriseURLs(url, url)
httpClient := http.Client{Transport: rt}
client, err = github.NewEnterpriseClient(url, url, &httpClient)
if err != nil {
return nil, fmt.Errorf("failed to create github enterprise client: %w", err)
}

View File

@@ -65,7 +65,7 @@ func newClient(baseURL string, options ...ClientOptionFunc) (*Client, error) {
return c, nil
}
func (c *Client) NewRequestWithContext(ctx context.Context, method, path string, body any) (*http.Request, error) {
func (c *Client) NewRequest(method, path string, body interface{}, options []ClientOptionFunc) (*http.Request, error) {
// Make sure the given URL end with a slash
if !strings.HasSuffix(c.baseURL, "/") {
c.baseURL += "/"
@@ -82,7 +82,7 @@ func (c *Client) NewRequestWithContext(ctx context.Context, method, path string,
}
}
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, buf)
req, err := http.NewRequest(method, c.baseURL+path, buf)
if err != nil {
return nil, err
}
@@ -91,7 +91,7 @@ func (c *Client) NewRequestWithContext(ctx context.Context, method, path string,
req.Header.Set("Content-Type", "application/json")
}
if c.token != "" {
if len(c.token) != 0 {
req.Header.Set("Authorization", "Bearer "+c.token)
}
@@ -102,7 +102,7 @@ func (c *Client) NewRequestWithContext(ctx context.Context, method, path string,
return req, nil
}
func (c *Client) Do(req *http.Request, v any) (*http.Response, error) {
func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*http.Response, error) {
resp, err := c.client.Do(req)
if err != nil {
return nil, err
@@ -134,7 +134,7 @@ func (c *Client) Do(req *http.Request, v any) (*http.Response, error) {
// CheckResponse checks the API response for errors, and returns them if present.
func CheckResponse(resp *http.Response) error {
if c := resp.StatusCode; http.StatusOK <= c && c < http.StatusMultipleChoices {
if c := resp.StatusCode; 200 <= c && c <= 299 {
return nil
}
@@ -143,7 +143,7 @@ func CheckResponse(resp *http.Response) error {
return fmt.Errorf("API error with status code %d: %w", resp.StatusCode, err)
}
var raw map[string]any
var raw map[string]interface{}
if err := json.Unmarshal(data, &raw); err != nil {
return fmt.Errorf("API error with status code %d: %s", resp.StatusCode, string(data))
}

View File

@@ -2,7 +2,7 @@ package http
import (
"bytes"
"errors"
"context"
"fmt"
"io"
"net/http"
@@ -10,11 +10,10 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestClient(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte("Hello, World!"))
if err != nil {
@@ -25,17 +24,21 @@ func TestClient(t *testing.T) {
var clientOptionFns []ClientOptionFunc
_, err := NewClient(server.URL, clientOptionFns...)
require.NoError(t, err, "Failed to create client")
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}
}
func TestClientDo(t *testing.T) {
ctx := context.Background()
for _, c := range []struct {
name string
params map[string]string
content []byte
fakeServer *httptest.Server
clientOptionFns []ClientOptionFunc
expected []map[string]any
expected []map[string]interface{}
expectedCode int
expectedError error
}{
@@ -45,7 +48,7 @@ func TestClientDo(t *testing.T) {
"pkey1": "val1",
"pkey2": "val2",
},
fakeServer: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
fakeServer: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte(`[{
"key1": "val1",
@@ -62,19 +65,19 @@ func TestClientDo(t *testing.T) {
}
})),
clientOptionFns: nil,
expected: []map[string]any{
expected: []map[string]interface{}{
{
"key1": "val1",
"key2": map[string]any{
"key2": map[string]interface{}{
"key2_1": "val2_1",
"key2_2": map[string]any{
"key2_2": map[string]interface{}{
"key2_2_1": "val2_2_1",
},
},
"key3": float64(123),
},
},
expectedCode: http.StatusOK,
expectedCode: 200,
expectedError: nil,
},
{
@@ -105,9 +108,9 @@ func TestClientDo(t *testing.T) {
}
})),
clientOptionFns: nil,
expected: []map[string]any(nil),
expectedCode: http.StatusUnauthorized,
expectedError: errors.New("API error with status code 401: "),
expected: []map[string]interface{}(nil),
expectedCode: 401,
expectedError: fmt.Errorf("API error with status code 401: "),
},
} {
cc := c
@@ -115,14 +118,18 @@ func TestClientDo(t *testing.T) {
defer cc.fakeServer.Close()
client, err := NewClient(cc.fakeServer.URL, cc.clientOptionFns...)
require.NoError(t, err, "NewClient returned unexpected error")
if err != nil {
t.Fatalf("NewClient returned unexpected error: %v", err)
}
req, err := client.NewRequestWithContext(t.Context(), http.MethodPost, "", cc.params)
require.NoError(t, err, "NewRequest returned unexpected error")
req, err := client.NewRequest("POST", "", cc.params, nil)
if err != nil {
t.Fatalf("NewRequest returned unexpected error: %v", err)
}
var data []map[string]any
var data []map[string]interface{}
resp, err := client.Do(req, &data)
resp, err := client.Do(ctx, req, &data)
if cc.expectedError != nil {
assert.EqualError(t, err, cc.expectedError.Error())
@@ -142,5 +149,12 @@ func TestCheckResponse(t *testing.T) {
}
err := CheckResponse(resp)
require.EqualError(t, err, "API error with status code 400: invalid_request")
if err == nil {
t.Error("Expected an error, got nil")
}
expected := "API error with status code 400: invalid_request"
if err.Error() != expected {
t.Errorf("Expected error '%s', got '%s'", expected, err.Error())
}
}

View File

@@ -1,15 +1,78 @@
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
// Code generated by mockery v2.40.2. DO NOT EDIT.
package mocks
import (
"context"
context "context"
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, noRevisionCache, verifyCommit
func (_m *Repos) GetDirectories(ctx context.Context, repoURL string, revision string, noRevisionCache bool, verifyCommit bool) ([]string, error) {
ret := _m.Called(ctx, repoURL, revision, 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, bool, bool) ([]string, error)); ok {
return rf(ctx, repoURL, revision, noRevisionCache, verifyCommit)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, bool, bool) []string); ok {
r0 = rf(ctx, repoURL, revision, noRevisionCache, verifyCommit)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
if rf, ok := ret.Get(1).(func(context.Context, string, string, bool, bool) error); ok {
r1 = rf(ctx, repoURL, revision, noRevisionCache, verifyCommit)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetFiles provides a mock function with given fields: ctx, repoURL, revision, pattern, noRevisionCache, verifyCommit
func (_m *Repos) GetFiles(ctx context.Context, repoURL string, revision string, pattern string, noRevisionCache bool, verifyCommit bool) (map[string][]byte, error) {
ret := _m.Called(ctx, repoURL, revision, 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, bool, bool) (map[string][]byte, error)); ok {
return rf(ctx, repoURL, revision, pattern, noRevisionCache, verifyCommit)
}
if rf, ok := ret.Get(0).(func(context.Context, string, string, string, bool, bool) map[string][]byte); ok {
r0 = rf(ctx, repoURL, revision, 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, bool, bool) error); ok {
r1 = rf(ctx, repoURL, revision, 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.
// The first argument is typically a *testing.T value.
func NewRepos(t interface {
@@ -23,206 +86,3 @@ func NewRepos(t interface {
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,8 +5,8 @@ import (
"fmt"
"net/http"
internalhttp "github.com/argoproj/argo-cd/v3/applicationset/services/internal/http"
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
internalhttp "github.com/argoproj/argo-cd/v2/applicationset/services/internal/http"
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
// ServiceRequest is the request object sent to the plugin service.
@@ -20,7 +20,7 @@ type ServiceRequest struct {
type Output struct {
// Parameters is the list of parameter sets returned by the plugin.
Parameters []map[string]any `json:"parameters"`
Parameters []map[string]interface{} `json:"parameters"`
}
// ServiceResponse is the response object returned by the plugin service.
@@ -34,7 +34,7 @@ type Service struct {
appSetName string
}
func NewPluginService(appSetName string, baseURL string, token string, requestTimeout int) (*Service, error) {
func NewPluginService(ctx context.Context, appSetName string, baseURL string, token string, requestTimeout int) (*Service, error) {
var clientOptionFns []internalhttp.ClientOptionFunc
clientOptionFns = append(clientOptionFns, internalhttp.WithToken(token))
@@ -55,14 +55,14 @@ func NewPluginService(appSetName string, baseURL string, token string, requestTi
}
func (p *Service) List(ctx context.Context, parameters v1alpha1.PluginParameters) (*ServiceResponse, error) {
req, err := p.client.NewRequestWithContext(ctx, http.MethodPost, "api/v1/getparams.execute", ServiceRequest{ApplicationSetName: p.appSetName, Input: v1alpha1.PluginInput{Parameters: parameters}})
req, err := p.client.NewRequest(http.MethodPost, "api/v1/getparams.execute", ServiceRequest{ApplicationSetName: p.appSetName, Input: v1alpha1.PluginInput{Parameters: parameters}}, nil)
if err != nil {
return nil, fmt.Errorf("NewRequest returned unexpected error: %w", err)
}
var data ServiceResponse
_, err = p.client.Do(req, &data)
_, err = p.client.Do(ctx, req, &data)
if err != nil {
return nil, fmt.Errorf("error get api '%s': %w", p.appSetName, err)
}

View File

@@ -1,6 +1,7 @@
package plugin
import (
"context"
"encoding/json"
"fmt"
"net/http"
@@ -8,7 +9,6 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPlugin(t *testing.T) {
@@ -30,14 +30,20 @@ func TestPlugin(t *testing.T) {
ts := httptest.NewServer(handler)
defer ts.Close()
client, err := NewPluginService("plugin-test", ts.URL, token, 0)
require.NoError(t, err)
client, err := NewPluginService(context.Background(), "plugin-test", ts.URL, token, 0)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
data, err := client.List(t.Context(), nil)
require.NoError(t, err)
data, err := client.List(context.Background(), nil)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
var expectedData ServiceResponse
err = json.Unmarshal([]byte(expectedJSON), &expectedData)
require.NoError(t, err)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, &expectedData, data)
}

View File

@@ -1,9 +1,10 @@
package plugin
import (
"fmt"
"strings"
"github.com/argoproj/argo-cd/v3/common"
"github.com/argoproj/argo-cd/v2/common"
)
// ParseSecretKey retrieves secret appSetName if different from common ArgoCDSecretName.
@@ -11,7 +12,7 @@ func ParseSecretKey(key string) (secretName string, tokenKey string) {
if strings.Contains(key, ":") {
parts := strings.Split(key, ":")
secretName = parts[0][1:]
tokenKey = "$" + parts[1]
tokenKey = fmt.Sprintf("$%s", parts[1])
} else {
secretName = common.ArgoCDSecretName
tokenKey = key

View File

@@ -5,9 +5,9 @@ import (
"fmt"
"strings"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/core"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/git"
"github.com/microsoft/azure-devops-go-api/azuredevops"
core "github.com/microsoft/azure-devops-go-api/azuredevops/core"
git "github.com/microsoft/azure-devops-go-api/azuredevops/git"
)
const AZURE_DEVOPS_DEFAULT_URL = "https://dev.azure.com"
@@ -41,14 +41,14 @@ var (
_ AzureDevOpsClientFactory = &devopsFactoryImpl{}
)
func NewAzureDevOpsService(token, url, organization, project, repo string, labels []string) (PullRequestService, error) {
organizationURL := buildURL(url, organization)
func NewAzureDevOpsService(ctx context.Context, token, url, organization, project, repo string, labels []string) (PullRequestService, error) {
organizationUrl := buildURL(url, organization)
var connection *azuredevops.Connection
if token == "" {
connection = azuredevops.NewAnonymousConnection(organizationURL)
connection = azuredevops.NewAnonymousConnection(organizationUrl)
} else {
connection = azuredevops.NewPatConnection(organizationURL, token)
connection = azuredevops.NewPatConnection(organizationUrl, token)
}
return &AzureDevOpsService{
@@ -82,7 +82,6 @@ func (a *AzureDevOpsService) List(ctx context.Context) ([]*PullRequest, error) {
pr.Repository.Name == nil ||
pr.PullRequestId == nil ||
pr.SourceRefName == nil ||
pr.TargetRefName == nil ||
pr.LastMergeSourceCommit == nil ||
pr.LastMergeSourceCommit.CommitId == nil {
continue
@@ -95,13 +94,10 @@ func (a *AzureDevOpsService) List(ctx context.Context) ([]*PullRequest, error) {
if *pr.Repository.Name == a.repo {
pullRequests = append(pullRequests, &PullRequest{
Number: *pr.PullRequestId,
Title: *pr.Title,
Branch: strings.Replace(*pr.SourceRefName, "refs/heads/", "", 1),
TargetBranch: strings.Replace(*pr.TargetRefName, "refs/heads/", "", 1),
HeadSHA: *pr.LastMergeSourceCommit.CommitId,
Labels: azureDevOpsLabels,
Author: strings.Split(*pr.CreatedBy.UniqueName, "@")[0], // Get the part before the @ in the email-address
Number: *pr.PullRequestId,
Branch: strings.Replace(*pr.SourceRefName, "refs/heads/", "", 1),
HeadSHA: *pr.LastMergeSourceCommit.CommitId,
Labels: azureDevOpsLabels,
})
}
}

View File

@@ -4,14 +4,13 @@ import (
"context"
"testing"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/core"
"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"
git "github.com/microsoft/azure-devops-go-api/azuredevops/git"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
azureMock "github.com/argoproj/argo-cd/v3/applicationset/services/scm_provider/azure_devops/git/mocks"
azureMock "github.com/argoproj/argo-cd/v2/applicationset/services/scm_provider/azure_devops/git/mocks"
)
func createBoolPtr(x bool) *bool {
@@ -30,10 +29,6 @@ func createLabelsPtr(x []core.WebApiTagDefinition) *[]core.WebApiTagDefinition {
return &x
}
func createUniqueNamePtr(x string) *string {
return &x
}
type AzureClientFactoryMock struct {
mock *mock.Mock
}
@@ -60,28 +55,21 @@ func (m *AzureClientFactoryMock) GetClient(ctx context.Context) (git.Client, err
func TestListPullRequest(t *testing.T) {
teamProject := "myorg_project"
repoName := "myorg_project_repo"
prID := 123
prTitle := "feat(123)"
prHeadSha := "cd4973d9d14a08ffe6b641a89a68891d6aac8056"
ctx := t.Context()
uniqueName := "testName"
pr_id := 123
pr_head_sha := "cd4973d9d14a08ffe6b641a89a68891d6aac8056"
ctx := context.Background()
pullRequestMock := []git.GitPullRequest{
{
PullRequestId: createIntPtr(prID),
Title: createStringPtr(prTitle),
PullRequestId: createIntPtr(pr_id),
SourceRefName: createStringPtr("refs/heads/feature-branch"),
TargetRefName: createStringPtr("refs/heads/main"),
LastMergeSourceCommit: &git.GitCommitRef{
CommitId: createStringPtr(prHeadSha),
CommitId: createStringPtr(pr_head_sha),
},
Labels: &[]core.WebApiTagDefinition{},
Repository: &git.GitRepository{
Name: createStringPtr(repoName),
},
CreatedBy: &webapi.IdentityRef{
UniqueName: createUniqueNamePtr(uniqueName + "@example.com"),
},
},
}
@@ -106,11 +94,8 @@ func TestListPullRequest(t *testing.T) {
require.NoError(t, err)
assert.Len(t, list, 1)
assert.Equal(t, "feature-branch", list[0].Branch)
assert.Equal(t, "main", list[0].TargetBranch)
assert.Equal(t, prHeadSha, list[0].HeadSHA)
assert.Equal(t, "feat(123)", list[0].Title)
assert.Equal(t, prID, list[0].Number)
assert.Equal(t, uniqueName, list[0].Author)
assert.Equal(t, pr_head_sha, list[0].HeadSHA)
assert.Equal(t, pr_id, list[0].Number)
}
func TestConvertLabes(t *testing.T) {

View File

@@ -3,7 +3,6 @@ package pull_request
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
@@ -17,19 +16,8 @@ type BitbucketCloudService struct {
}
type BitbucketCloudPullRequest struct {
ID int `json:"id"`
Title string `json:"title"`
Source BitbucketCloudPullRequestSource `json:"source"`
Author BitbucketCloudPullRequestAuthor `json:"author"`
Destination BitbucketCloudPullRequestDestination `json:"destination"`
}
type BitbucketCloudPullRequestDestination struct {
Branch BitbucketCloudPullRequestDestinationBranch `json:"branch"`
}
type BitbucketCloudPullRequestDestinationBranch struct {
Name string `json:"name"`
ID int `json:"id"`
Source BitbucketCloudPullRequestSource `json:"source"`
}
type BitbucketCloudPullRequestSource struct {
@@ -45,11 +33,6 @@ type BitbucketCloudPullRequestSourceCommit struct {
Hash string `json:"hash"`
}
// Also have display_name and uuid, but don't plan to use them.
type BitbucketCloudPullRequestAuthor struct {
Nickname string `json:"nickname"`
}
type PullRequestResponse struct {
Page int32 `json:"page"`
Size int32 `json:"size"`
@@ -61,7 +44,7 @@ type PullRequestResponse struct {
var _ PullRequestService = (*BitbucketCloudService)(nil)
func parseURL(uri string) (*url.URL, error) {
func parseUrl(uri string) (*url.URL, error) {
if uri == "" {
uri = "https://api.bitbucket.org/2.0"
}
@@ -74,10 +57,10 @@ func parseURL(uri string) (*url.URL, error) {
return url, nil
}
func NewBitbucketCloudServiceBasicAuth(baseURL, username, password, owner, repositorySlug string) (PullRequestService, error) {
url, err := parseURL(baseURL)
func NewBitbucketCloudServiceBasicAuth(baseUrl, username, password, owner, repositorySlug string) (PullRequestService, error) {
url, err := parseUrl(baseUrl)
if err != nil {
return nil, fmt.Errorf("error parsing base url of %s for %s/%s: %w", baseURL, owner, repositorySlug, err)
return nil, fmt.Errorf("error parsing base url of %s for %s/%s: %w", baseUrl, owner, repositorySlug, err)
}
bitbucketClient := bitbucket.NewBasicAuth(username, password)
@@ -90,10 +73,10 @@ func NewBitbucketCloudServiceBasicAuth(baseURL, username, password, owner, repos
}, nil
}
func NewBitbucketCloudServiceBearerToken(baseURL, bearerToken, owner, repositorySlug string) (PullRequestService, error) {
url, err := parseURL(baseURL)
func NewBitbucketCloudServiceBearerToken(baseUrl, bearerToken, owner, repositorySlug string) (PullRequestService, error) {
url, err := parseUrl(baseUrl)
if err != nil {
return nil, fmt.Errorf("error parsing base url of %s for %s/%s: %w", baseURL, owner, repositorySlug, err)
return nil, fmt.Errorf("error parsing base url of %s for %s/%s: %w", baseUrl, owner, repositorySlug, err)
}
bitbucketClient := bitbucket.NewOAuthbearerToken(bearerToken)
@@ -106,9 +89,9 @@ func NewBitbucketCloudServiceBearerToken(baseURL, bearerToken, owner, repository
}, nil
}
func NewBitbucketCloudServiceNoAuth(baseURL, owner, repositorySlug string) (PullRequestService, error) {
func NewBitbucketCloudServiceNoAuth(baseUrl, owner, repositorySlug string) (PullRequestService, error) {
// There is currently no method to explicitly not require auth
return NewBitbucketCloudServiceBearerToken(baseURL, "", owner, repositorySlug)
return NewBitbucketCloudServiceBearerToken(baseUrl, "", owner, repositorySlug)
}
func (b *BitbucketCloudService) List(_ context.Context) ([]*PullRequest, error) {
@@ -122,14 +105,14 @@ func (b *BitbucketCloudService) List(_ context.Context) ([]*PullRequest, error)
return nil, fmt.Errorf("error listing pull requests for %s/%s: %w", b.owner, b.repositorySlug, err)
}
resp, ok := response.(map[string]any)
resp, ok := response.(map[string]interface{})
if !ok {
return nil, errors.New("unknown type returned from bitbucket pull requests")
return nil, fmt.Errorf("unknown type returned from bitbucket pull requests")
}
repoArray, ok := resp["values"].([]any)
repoArray, ok := resp["values"].([]interface{})
if !ok {
return nil, errors.New("unknown type returned from response values")
return nil, fmt.Errorf("unknown type returned from response values")
}
jsonStr, err := json.Marshal(repoArray)
@@ -145,12 +128,9 @@ func (b *BitbucketCloudService) List(_ context.Context) ([]*PullRequest, error)
pullRequests := []*PullRequest{}
for _, pull := range pulls {
pullRequests = append(pullRequests, &PullRequest{
Number: pull.ID,
Title: pull.Title,
Branch: pull.Source.Branch.Name,
TargetBranch: pull.Destination.Branch.Name,
HeadSHA: pull.Source.Commit.Hash,
Author: pull.Author.Nickname,
Number: pull.ID,
Branch: pull.Source.Branch.Name,
HeadSHA: pull.Source.Commit.Hash,
})
}

View File

@@ -1,6 +1,7 @@
package pull_request
import (
"context"
"fmt"
"io"
"net/http"
@@ -10,11 +11,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
func defaultHandlerCloud(t *testing.T) func(http.ResponseWriter, *http.Request) {
t.Helper()
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var err error
@@ -27,7 +27,6 @@ func defaultHandlerCloud(t *testing.T) func(http.ResponseWriter, *http.Request)
"values": [
{
"id": 101,
"title": "feat(foo-bar)",
"source": {
"branch": {
"name": "feature/foo-bar"
@@ -36,9 +35,6 @@ func defaultHandlerCloud(t *testing.T) func(http.ResponseWriter, *http.Request)
"type": "commit",
"hash": "1a8dd249c04a"
}
},
"author": {
"nickname": "testName"
}
}
]
@@ -53,11 +49,11 @@ func defaultHandlerCloud(t *testing.T) func(http.ResponseWriter, *http.Request)
}
func TestParseUrlEmptyUrl(t *testing.T) {
url, err := parseURL("")
bitbucketURL, _ := url.Parse("https://api.bitbucket.org/2.0")
url, err := parseUrl("")
bitbucketUrl, _ := url.Parse("https://api.bitbucket.org/2.0")
require.NoError(t, err)
assert.Equal(t, bitbucketURL, url)
assert.Equal(t, bitbucketUrl, url)
}
func TestInvalidBaseUrlBasicAuthCloud(t *testing.T) {
@@ -86,14 +82,12 @@ func TestListPullRequestBearerTokenCloud(t *testing.T) {
defer ts.Close()
svc, err := NewBitbucketCloudServiceBearerToken(ts.URL, "TOKEN", "OWNER", "REPO")
require.NoError(t, err)
pullRequests, err := ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{})
pullRequests, err := ListPullRequests(context.Background(), svc, []v1alpha1.PullRequestGeneratorFilter{})
require.NoError(t, err)
assert.Len(t, pullRequests, 1)
assert.Equal(t, 101, pullRequests[0].Number)
assert.Equal(t, "feat(foo-bar)", pullRequests[0].Title)
assert.Equal(t, "feature/foo-bar", pullRequests[0].Branch)
assert.Equal(t, "1a8dd249c04a", pullRequests[0].HeadSHA)
assert.Equal(t, "testName", pullRequests[0].Author)
}
func TestListPullRequestNoAuthCloud(t *testing.T) {
@@ -104,14 +98,12 @@ func TestListPullRequestNoAuthCloud(t *testing.T) {
defer ts.Close()
svc, err := NewBitbucketCloudServiceNoAuth(ts.URL, "OWNER", "REPO")
require.NoError(t, err)
pullRequests, err := ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{})
pullRequests, err := ListPullRequests(context.Background(), svc, []v1alpha1.PullRequestGeneratorFilter{})
require.NoError(t, err)
assert.Len(t, pullRequests, 1)
assert.Equal(t, 101, pullRequests[0].Number)
assert.Equal(t, "feat(foo-bar)", pullRequests[0].Title)
assert.Equal(t, "feature/foo-bar", pullRequests[0].Branch)
assert.Equal(t, "1a8dd249c04a", pullRequests[0].HeadSHA)
assert.Equal(t, "testName", pullRequests[0].Author)
}
func TestListPullRequestBasicAuthCloud(t *testing.T) {
@@ -122,14 +114,12 @@ func TestListPullRequestBasicAuthCloud(t *testing.T) {
defer ts.Close()
svc, err := NewBitbucketCloudServiceBasicAuth(ts.URL, "user", "password", "OWNER", "REPO")
require.NoError(t, err)
pullRequests, err := ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{})
pullRequests, err := ListPullRequests(context.Background(), svc, []v1alpha1.PullRequestGeneratorFilter{})
require.NoError(t, err)
assert.Len(t, pullRequests, 1)
assert.Equal(t, 101, pullRequests[0].Number)
assert.Equal(t, "feat(foo-bar)", pullRequests[0].Title)
assert.Equal(t, "feature/foo-bar", pullRequests[0].Branch)
assert.Equal(t, "1a8dd249c04a", pullRequests[0].HeadSHA)
assert.Equal(t, "testName", pullRequests[0].Author)
}
func TestListPullRequestPaginationCloud(t *testing.T) {
@@ -138,7 +128,7 @@ func TestListPullRequestPaginationCloud(t *testing.T) {
var err error
switch r.RequestURI {
case "/repositories/OWNER/REPO/pullrequests/":
_, err = fmt.Fprintf(w, `{
_, err = io.WriteString(w, fmt.Sprintf(`{
"size": 2,
"pagelen": 1,
"page": 1,
@@ -146,7 +136,6 @@ func TestListPullRequestPaginationCloud(t *testing.T) {
"values": [
{
"id": 101,
"title": "feat(101)",
"source": {
"branch": {
"name": "feature-101"
@@ -155,14 +144,10 @@ func TestListPullRequestPaginationCloud(t *testing.T) {
"type": "commit",
"hash": "1a8dd249c04a"
}
},
"author": {
"nickname": "testName"
}
},
{
"id": 102,
"title": "feat(102)",
"source": {
"branch": {
"name": "feature-102"
@@ -171,15 +156,12 @@ func TestListPullRequestPaginationCloud(t *testing.T) {
"type": "commit",
"hash": "4cf807e67a6d"
}
},
"author": {
"nickname": "testName"
}
}
]
}`, r.Host)
}`, r.Host))
case "/repositories/OWNER/REPO/pullrequests/?pagelen=1&page=2":
_, err = fmt.Fprintf(w, `{
_, err = io.WriteString(w, fmt.Sprintf(`{
"size": 2,
"pagelen": 1,
"page": 2,
@@ -187,7 +169,6 @@ func TestListPullRequestPaginationCloud(t *testing.T) {
"values": [
{
"id": 103,
"title": "feat(103)",
"source": {
"branch": {
"name": "feature-103"
@@ -196,13 +177,10 @@ func TestListPullRequestPaginationCloud(t *testing.T) {
"type": "commit",
"hash": "6344d9623e3b"
}
},
"author": {
"nickname": "testName"
}
}
]
}`, r.Host)
}`, r.Host))
default:
t.Fail()
}
@@ -213,39 +191,33 @@ func TestListPullRequestPaginationCloud(t *testing.T) {
defer ts.Close()
svc, err := NewBitbucketCloudServiceNoAuth(ts.URL, "OWNER", "REPO")
require.NoError(t, err)
pullRequests, err := ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{})
pullRequests, err := ListPullRequests(context.Background(), svc, []v1alpha1.PullRequestGeneratorFilter{})
require.NoError(t, err)
assert.Len(t, pullRequests, 3)
assert.Equal(t, PullRequest{
Number: 101,
Title: "feat(101)",
Branch: "feature-101",
HeadSHA: "1a8dd249c04a",
Author: "testName",
}, *pullRequests[0])
assert.Equal(t, PullRequest{
Number: 102,
Title: "feat(102)",
Branch: "feature-102",
HeadSHA: "4cf807e67a6d",
Author: "testName",
}, *pullRequests[1])
assert.Equal(t, PullRequest{
Number: 103,
Title: "feat(103)",
Branch: "feature-103",
HeadSHA: "6344d9623e3b",
Author: "testName",
}, *pullRequests[2])
}
func TestListResponseErrorCloud(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(500)
}))
defer ts.Close()
svc, _ := NewBitbucketCloudServiceNoAuth(ts.URL, "OWNER", "REPO")
_, err := ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{})
_, err := ListPullRequests(context.Background(), svc, []v1alpha1.PullRequestGeneratorFilter{})
require.Error(t, err)
}
@@ -269,7 +241,7 @@ func TestListResponseMalformedCloud(t *testing.T) {
}))
defer ts.Close()
svc, _ := NewBitbucketCloudServiceNoAuth(ts.URL, "OWNER", "REPO")
_, err := ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{})
_, err := ListPullRequests(context.Background(), svc, []v1alpha1.PullRequestGeneratorFilter{})
require.Error(t, err)
}
@@ -293,7 +265,7 @@ func TestListResponseMalformedValuesCloud(t *testing.T) {
}))
defer ts.Close()
svc, _ := NewBitbucketCloudServiceNoAuth(ts.URL, "OWNER", "REPO")
_, err := ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{})
_, err := ListPullRequests(context.Background(), svc, []v1alpha1.PullRequestGeneratorFilter{})
require.Error(t, err)
}
@@ -318,7 +290,7 @@ func TestListResponseEmptyCloud(t *testing.T) {
defer ts.Close()
svc, err := NewBitbucketCloudServiceNoAuth(ts.URL, "OWNER", "REPO")
require.NoError(t, err)
pullRequests, err := ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{})
pullRequests, err := ListPullRequests(context.Background(), svc, []v1alpha1.PullRequestGeneratorFilter{})
require.NoError(t, err)
assert.Empty(t, pullRequests)
}
@@ -329,7 +301,7 @@ func TestListPullRequestBranchMatchCloud(t *testing.T) {
var err error
switch r.RequestURI {
case "/repositories/OWNER/REPO/pullrequests/":
_, err = fmt.Fprintf(w, `{
_, err = io.WriteString(w, fmt.Sprintf(`{
"size": 2,
"pagelen": 1,
"page": 1,
@@ -337,7 +309,6 @@ func TestListPullRequestBranchMatchCloud(t *testing.T) {
"values": [
{
"id": 101,
"title": "feat(101)",
"source": {
"branch": {
"name": "feature-101"
@@ -346,19 +317,10 @@ func TestListPullRequestBranchMatchCloud(t *testing.T) {
"type": "commit",
"hash": "1a8dd249c04a"
}
},
"author": {
"nickname": "testName"
},
"destination": {
"branch": {
"name": "master"
}
}
},
{
"id": 200,
"title": "feat(200)",
"source": {
"branch": {
"name": "feature-200"
@@ -367,20 +329,12 @@ func TestListPullRequestBranchMatchCloud(t *testing.T) {
"type": "commit",
"hash": "4cf807e67a6d"
}
},
"author": {
"nickname": "testName"
},
"destination": {
"branch": {
"name": "branch-200"
}
}
}
]
}`, r.Host)
}`, r.Host))
case "/repositories/OWNER/REPO/pullrequests/?pagelen=1&page=2":
_, err = fmt.Fprintf(w, `{
_, err = io.WriteString(w, fmt.Sprintf(`{
"size": 2,
"pagelen": 1,
"page": 2,
@@ -388,7 +342,6 @@ func TestListPullRequestBranchMatchCloud(t *testing.T) {
"values": [
{
"id": 102,
"title": "feat(102)",
"source": {
"branch": {
"name": "feature-102"
@@ -397,18 +350,10 @@ func TestListPullRequestBranchMatchCloud(t *testing.T) {
"type": "commit",
"hash": "6344d9623e3b"
}
},
"author": {
"nickname": "testName"
},
"destination": {
"branch": {
"name": "master"
}
}
}
]
}`, r.Host)
}`, r.Host))
default:
t.Fail()
}
@@ -420,7 +365,7 @@ func TestListPullRequestBranchMatchCloud(t *testing.T) {
regexp := `feature-1[\d]{2}`
svc, err := NewBitbucketCloudServiceNoAuth(ts.URL, "OWNER", "REPO")
require.NoError(t, err)
pullRequests, err := ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{
pullRequests, err := ListPullRequests(context.Background(), svc, []v1alpha1.PullRequestGeneratorFilter{
{
BranchMatch: &regexp,
},
@@ -428,26 +373,20 @@ func TestListPullRequestBranchMatchCloud(t *testing.T) {
require.NoError(t, err)
assert.Len(t, pullRequests, 2)
assert.Equal(t, PullRequest{
Number: 101,
Title: "feat(101)",
Branch: "feature-101",
HeadSHA: "1a8dd249c04a",
Author: "testName",
TargetBranch: "master",
Number: 101,
Branch: "feature-101",
HeadSHA: "1a8dd249c04a",
}, *pullRequests[0])
assert.Equal(t, PullRequest{
Number: 102,
Title: "feat(102)",
Branch: "feature-102",
HeadSHA: "6344d9623e3b",
Author: "testName",
TargetBranch: "master",
Number: 102,
Branch: "feature-102",
HeadSHA: "6344d9623e3b",
}, *pullRequests[1])
regexp = `.*2$`
svc, err = NewBitbucketCloudServiceNoAuth(ts.URL, "OWNER", "REPO")
require.NoError(t, err)
pullRequests, err = ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{
pullRequests, err = ListPullRequests(context.Background(), svc, []v1alpha1.PullRequestGeneratorFilter{
{
BranchMatch: &regexp,
},
@@ -455,40 +394,18 @@ func TestListPullRequestBranchMatchCloud(t *testing.T) {
require.NoError(t, err)
assert.Len(t, pullRequests, 1)
assert.Equal(t, PullRequest{
Number: 102,
Title: "feat(102)",
Branch: "feature-102",
HeadSHA: "6344d9623e3b",
Author: "testName",
TargetBranch: "master",
Number: 102,
Branch: "feature-102",
HeadSHA: "6344d9623e3b",
}, *pullRequests[0])
regexp = `[\d{2}`
svc, err = NewBitbucketCloudServiceNoAuth(ts.URL, "OWNER", "REPO")
require.NoError(t, err)
_, err = ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{
_, err = ListPullRequests(context.Background(), svc, []v1alpha1.PullRequestGeneratorFilter{
{
BranchMatch: &regexp,
},
})
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])
}

View File

@@ -3,12 +3,11 @@ package pull_request
import (
"context"
"fmt"
"net/http"
bitbucketv1 "github.com/gfleury/go-bitbucket-v1"
log "github.com/sirupsen/logrus"
"github.com/argoproj/argo-cd/v3/applicationset/utils"
"github.com/argoproj/argo-cd/v2/applicationset/utils"
)
type BitbucketService struct {
@@ -21,7 +20,7 @@ type BitbucketService struct {
var _ PullRequestService = (*BitbucketService)(nil)
func NewBitbucketServiceBasicAuth(ctx context.Context, username, password, url, projectKey, repositorySlug string, scmRootCAPath string, insecure bool, caCerts []byte) (PullRequestService, error) {
func NewBitbucketServiceBasicAuth(ctx context.Context, username, password, url, projectKey, repositorySlug string) (PullRequestService, error) {
bitbucketConfig := bitbucketv1.NewConfiguration(url)
// Avoid the XSRF check
bitbucketConfig.AddDefaultHeader("x-atlassian-token", "no-check")
@@ -31,29 +30,15 @@ func NewBitbucketServiceBasicAuth(ctx context.Context, username, password, url,
UserName: username,
Password: password,
})
return newBitbucketService(ctx, bitbucketConfig, projectKey, repositorySlug, scmRootCAPath, insecure, caCerts)
return newBitbucketService(ctx, bitbucketConfig, projectKey, repositorySlug)
}
func NewBitbucketServiceBearerToken(ctx context.Context, bearerToken, url, projectKey, repositorySlug string, scmRootCAPath string, insecure bool, caCerts []byte) (PullRequestService, error) {
bitbucketConfig := bitbucketv1.NewConfiguration(url)
// Avoid the XSRF check
bitbucketConfig.AddDefaultHeader("x-atlassian-token", "no-check")
bitbucketConfig.AddDefaultHeader("x-requested-with", "XMLHttpRequest")
ctx = context.WithValue(ctx, bitbucketv1.ContextAccessToken, bearerToken)
return newBitbucketService(ctx, bitbucketConfig, projectKey, repositorySlug, scmRootCAPath, insecure, caCerts)
func NewBitbucketServiceNoAuth(ctx context.Context, url, projectKey, repositorySlug string) (PullRequestService, error) {
return newBitbucketService(ctx, bitbucketv1.NewConfiguration(url), projectKey, repositorySlug)
}
func NewBitbucketServiceNoAuth(ctx context.Context, url, projectKey, repositorySlug string, scmRootCAPath string, insecure bool, caCerts []byte) (PullRequestService, error) {
return newBitbucketService(ctx, bitbucketv1.NewConfiguration(url), projectKey, repositorySlug, scmRootCAPath, insecure, caCerts)
}
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) (PullRequestService, error) {
bitbucketConfig.BasePath = utils.NormalizeBitbucketBasePath(bitbucketConfig.BasePath)
tlsConfig := utils.GetTlsConfig(scmRootCAPath, insecure, caCerts)
bitbucketConfig.HTTPClient = &http.Client{Transport: &http.Transport{
TLSClientConfig: tlsConfig,
}}
bitbucketClient := bitbucketv1.NewAPIClient(ctx, bitbucketConfig)
return &BitbucketService{
@@ -64,7 +49,7 @@ func newBitbucketService(ctx context.Context, bitbucketConfig *bitbucketv1.Confi
}
func (b *BitbucketService) List(_ context.Context) ([]*PullRequest, error) {
paged := map[string]any{
paged := map[string]interface{}{
"limit": 100,
}
@@ -83,12 +68,10 @@ func (b *BitbucketService) List(_ context.Context) ([]*PullRequest, error) {
for _, pull := range pulls {
pullRequests = append(pullRequests, &PullRequest{
Number: pull.ID,
Title: pull.Title,
Branch: pull.FromRef.DisplayID, // ID: refs/heads/main DisplayID: main
TargetBranch: pull.ToRef.DisplayID,
HeadSHA: pull.FromRef.LatestCommit, // This is not defined in the official docs, but works in practice
Labels: []string{}, // Not supported by library
Author: pull.Author.User.Name,
})
}

View File

@@ -1,8 +1,7 @@
package pull_request
import (
"crypto/x509"
"encoding/pem"
"context"
"io"
"net/http"
"net/http/httptest"
@@ -11,11 +10,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
)
func defaultHandler(t *testing.T) func(http.ResponseWriter, *http.Request) {
t.Helper()
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var err error
@@ -28,7 +26,6 @@ func defaultHandler(t *testing.T) func(http.ResponseWriter, *http.Request) {
"values": [
{
"id": 101,
"title": "feat(ABC) : 123",
"toRef": {
"latestCommit": "5b766e3564a3453808f3cd3dd3f2e5fad8ef0e7a",
"displayId": "master",
@@ -38,11 +35,6 @@ func defaultHandler(t *testing.T) func(http.ResponseWriter, *http.Request) {
"id": "refs/heads/feature-ABC-123",
"displayId": "feature-ABC-123",
"latestCommit": "cb3cf2e4d1517c83e720d2585b9402dbef71f992"
},
"author": {
"user": {
"name": "testName"
}
}
}
],
@@ -63,17 +55,15 @@ func TestListPullRequestNoAuth(t *testing.T) {
defaultHandler(t)(w, r)
}))
defer ts.Close()
svc, err := NewBitbucketServiceNoAuth(t.Context(), ts.URL, "PROJECT", "REPO", "", false, nil)
svc, err := NewBitbucketServiceNoAuth(context.Background(), ts.URL, "PROJECT", "REPO")
require.NoError(t, err)
pullRequests, err := ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{})
pullRequests, err := ListPullRequests(context.Background(), svc, []v1alpha1.PullRequestGeneratorFilter{})
require.NoError(t, err)
assert.Len(t, pullRequests, 1)
assert.Equal(t, 101, pullRequests[0].Number)
assert.Equal(t, "feat(ABC) : 123", pullRequests[0].Title)
assert.Equal(t, "feature-ABC-123", pullRequests[0].Branch)
assert.Equal(t, "master", pullRequests[0].TargetBranch)
assert.Equal(t, "cb3cf2e4d1517c83e720d2585b9402dbef71f992", pullRequests[0].HeadSHA)
assert.Equal(t, "testName", pullRequests[0].Author)
}
func TestListPullRequestPagination(t *testing.T) {
@@ -89,7 +79,6 @@ func TestListPullRequestPagination(t *testing.T) {
"values": [
{
"id": 101,
"title": "feat(101)",
"toRef": {
"latestCommit": "5b766e3564a3453808f3cd3dd3f2e5fad8ef0e7a",
"displayId": "master",
@@ -99,16 +88,10 @@ func TestListPullRequestPagination(t *testing.T) {
"id": "refs/heads/feature-101",
"displayId": "feature-101",
"latestCommit": "ab3cf2e4d1517c83e720d2585b9402dbef71f992"
},
"author": {
"user": {
"name": "testName"
}
}
},
{
"id": 102,
"title": "feat(102)",
"toRef": {
"latestCommit": "5b766e3564a3453808f3cd3dd3f2e5fad8ef0e7a",
"displayId": "branch",
@@ -118,11 +101,6 @@ func TestListPullRequestPagination(t *testing.T) {
"id": "refs/heads/feature-102",
"displayId": "feature-102",
"latestCommit": "bb3cf2e4d1517c83e720d2585b9402dbef71f992"
},
"author": {
"user": {
"name": "testName"
}
}
}
],
@@ -136,7 +114,6 @@ func TestListPullRequestPagination(t *testing.T) {
"values": [
{
"id": 200,
"title": "feat(200)",
"toRef": {
"latestCommit": "5b766e3564a3453808f3cd3dd3f2e5fad8ef0e7a",
"displayId": "master",
@@ -146,11 +123,6 @@ func TestListPullRequestPagination(t *testing.T) {
"id": "refs/heads/feature-200",
"displayId": "feature-200",
"latestCommit": "cb3cf2e4d1517c83e720d2585b9402dbef71f992"
},
"author": {
"user": {
"name": "testName"
}
}
}
],
@@ -164,37 +136,31 @@ func TestListPullRequestPagination(t *testing.T) {
}
}))
defer ts.Close()
svc, err := NewBitbucketServiceNoAuth(t.Context(), ts.URL, "PROJECT", "REPO", "", false, nil)
svc, err := NewBitbucketServiceNoAuth(context.Background(), ts.URL, "PROJECT", "REPO")
require.NoError(t, err)
pullRequests, err := ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{})
pullRequests, err := ListPullRequests(context.Background(), svc, []v1alpha1.PullRequestGeneratorFilter{})
require.NoError(t, err)
assert.Len(t, pullRequests, 3)
assert.Equal(t, PullRequest{
Number: 101,
Title: "feat(101)",
Branch: "feature-101",
TargetBranch: "master",
HeadSHA: "ab3cf2e4d1517c83e720d2585b9402dbef71f992",
Labels: []string{},
Author: "testName",
}, *pullRequests[0])
assert.Equal(t, PullRequest{
Number: 102,
Title: "feat(102)",
Branch: "feature-102",
TargetBranch: "branch",
HeadSHA: "bb3cf2e4d1517c83e720d2585b9402dbef71f992",
Labels: []string{},
Author: "testName",
}, *pullRequests[1])
assert.Equal(t, PullRequest{
Number: 200,
Title: "feat(200)",
Branch: "feature-200",
TargetBranch: "master",
HeadSHA: "cb3cf2e4d1517c83e720d2585b9402dbef71f992",
Labels: []string{},
Author: "testName",
}, *pullRequests[2])
}
@@ -206,9 +172,9 @@ func TestListPullRequestBasicAuth(t *testing.T) {
defaultHandler(t)(w, r)
}))
defer ts.Close()
svc, err := NewBitbucketServiceBasicAuth(t.Context(), "user", "password", ts.URL, "PROJECT", "REPO", "", false, nil)
svc, err := NewBitbucketServiceBasicAuth(context.Background(), "user", "password", ts.URL, "PROJECT", "REPO")
require.NoError(t, err)
pullRequests, err := ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{})
pullRequests, err := ListPullRequests(context.Background(), svc, []v1alpha1.PullRequestGeneratorFilter{})
require.NoError(t, err)
assert.Len(t, pullRequests, 1)
assert.Equal(t, 101, pullRequests[0].Number)
@@ -216,98 +182,13 @@ func TestListPullRequestBasicAuth(t *testing.T) {
assert.Equal(t, "cb3cf2e4d1517c83e720d2585b9402dbef71f992", pullRequests[0].HeadSHA)
}
func TestListPullRequestBearerAuth(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "Bearer tolkien", r.Header.Get("Authorization"))
assert.Equal(t, "no-check", r.Header.Get("X-Atlassian-Token"))
defaultHandler(t)(w, r)
}))
defer ts.Close()
svc, err := NewBitbucketServiceBearerToken(t.Context(), "tolkien", ts.URL, "PROJECT", "REPO", "", false, nil)
require.NoError(t, err)
pullRequests, err := ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{})
require.NoError(t, err)
assert.Len(t, pullRequests, 1)
assert.Equal(t, 101, pullRequests[0].Number)
assert.Equal(t, "feat(ABC) : 123", pullRequests[0].Title)
assert.Equal(t, "feature-ABC-123", pullRequests[0].Branch)
assert.Equal(t, "cb3cf2e4d1517c83e720d2585b9402dbef71f992", pullRequests[0].HeadSHA)
}
func TestListPullRequestTLS(t *testing.T) {
tests := []struct {
name string
tlsInsecure bool
passCerts bool
requireErr bool
}{
{
name: "TLS Insecure: true, No Certs",
tlsInsecure: true,
passCerts: false,
requireErr: false,
},
{
name: "TLS Insecure: true, With Certs",
tlsInsecure: true,
passCerts: true,
requireErr: false,
},
{
name: "TLS Insecure: false, With Certs",
tlsInsecure: false,
passCerts: true,
requireErr: false,
},
{
name: "TLS Insecure: false, No Certs",
tlsInsecure: false,
passCerts: false,
requireErr: true,
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defaultHandler(t)(w, r)
}))
defer ts.Close()
var certs []byte
if test.passCerts {
for _, cert := range ts.TLS.Certificates {
for _, c := range cert.Certificate {
parsedCert, err := x509.ParseCertificate(c)
require.NoError(t, err, "Failed to parse certificate")
certs = append(certs, pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: parsedCert.Raw,
})...)
}
}
}
svc, err := NewBitbucketServiceBasicAuth(t.Context(), "user", "password", ts.URL, "PROJECT", "REPO", "", test.tlsInsecure, certs)
require.NoError(t, err)
_, err = ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{})
if test.requireErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
func TestListResponseError(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
defer ts.Close()
svc, _ := NewBitbucketServiceNoAuth(t.Context(), ts.URL, "PROJECT", "REPO", "", false, nil)
_, err := ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{})
svc, _ := NewBitbucketServiceNoAuth(context.Background(), ts.URL, "PROJECT", "REPO")
_, err := ListPullRequests(context.Background(), svc, []v1alpha1.PullRequestGeneratorFilter{})
require.Error(t, err)
}
@@ -331,8 +212,8 @@ func TestListResponseMalformed(t *testing.T) {
}
}))
defer ts.Close()
svc, _ := NewBitbucketServiceNoAuth(t.Context(), ts.URL, "PROJECT", "REPO", "", false, nil)
_, err := ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{})
svc, _ := NewBitbucketServiceNoAuth(context.Background(), ts.URL, "PROJECT", "REPO")
_, err := ListPullRequests(context.Background(), svc, []v1alpha1.PullRequestGeneratorFilter{})
require.Error(t, err)
}
@@ -356,9 +237,9 @@ func TestListResponseEmpty(t *testing.T) {
}
}))
defer ts.Close()
svc, err := NewBitbucketServiceNoAuth(t.Context(), ts.URL, "PROJECT", "REPO", "", false, nil)
svc, err := NewBitbucketServiceNoAuth(context.Background(), ts.URL, "PROJECT", "REPO")
require.NoError(t, err)
pullRequests, err := ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{})
pullRequests, err := ListPullRequests(context.Background(), svc, []v1alpha1.PullRequestGeneratorFilter{})
require.NoError(t, err)
assert.Empty(t, pullRequests)
}
@@ -376,7 +257,6 @@ func TestListPullRequestBranchMatch(t *testing.T) {
"values": [
{
"id": 101,
"title": "feat(101)",
"toRef": {
"latestCommit": "5b766e3564a3453808f3cd3dd3f2e5fad8ef0e7a",
"displayId": "master",
@@ -386,16 +266,10 @@ func TestListPullRequestBranchMatch(t *testing.T) {
"id": "refs/heads/feature-101",
"displayId": "feature-101",
"latestCommit": "ab3cf2e4d1517c83e720d2585b9402dbef71f992"
},
"author": {
"user": {
"name": "testName"
}
}
},
{
"id": 102,
"title": "feat(102)",
"toRef": {
"latestCommit": "5b766e3564a3453808f3cd3dd3f2e5fad8ef0e7a",
"displayId": "branch",
@@ -405,11 +279,6 @@ func TestListPullRequestBranchMatch(t *testing.T) {
"id": "refs/heads/feature-102",
"displayId": "feature-102",
"latestCommit": "bb3cf2e4d1517c83e720d2585b9402dbef71f992"
},
"author": {
"user": {
"name": "testName"
}
}
}
],
@@ -423,7 +292,6 @@ func TestListPullRequestBranchMatch(t *testing.T) {
"values": [
{
"id": 200,
"title": "feat(200)",
"toRef": {
"latestCommit": "5b766e3564a3453808f3cd3dd3f2e5fad8ef0e7a",
"displayId": "master",
@@ -433,11 +301,6 @@ func TestListPullRequestBranchMatch(t *testing.T) {
"id": "refs/heads/feature-200",
"displayId": "feature-200",
"latestCommit": "cb3cf2e4d1517c83e720d2585b9402dbef71f992"
},
"author": {
"user": {
"name": "testName"
}
}
}
],
@@ -452,9 +315,9 @@ func TestListPullRequestBranchMatch(t *testing.T) {
}))
defer ts.Close()
regexp := `feature-1[\d]{2}`
svc, err := NewBitbucketServiceNoAuth(t.Context(), ts.URL, "PROJECT", "REPO", "", false, nil)
svc, err := NewBitbucketServiceNoAuth(context.Background(), ts.URL, "PROJECT", "REPO")
require.NoError(t, err)
pullRequests, err := ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{
pullRequests, err := ListPullRequests(context.Background(), svc, []v1alpha1.PullRequestGeneratorFilter{
{
BranchMatch: &regexp,
},
@@ -463,27 +326,23 @@ func TestListPullRequestBranchMatch(t *testing.T) {
assert.Len(t, pullRequests, 2)
assert.Equal(t, PullRequest{
Number: 101,
Title: "feat(101)",
Branch: "feature-101",
TargetBranch: "master",
HeadSHA: "ab3cf2e4d1517c83e720d2585b9402dbef71f992",
Labels: []string{},
Author: "testName",
}, *pullRequests[0])
assert.Equal(t, PullRequest{
Number: 102,
Title: "feat(102)",
Branch: "feature-102",
TargetBranch: "branch",
HeadSHA: "bb3cf2e4d1517c83e720d2585b9402dbef71f992",
Labels: []string{},
Author: "testName",
}, *pullRequests[1])
regexp = `.*2$`
svc, err = NewBitbucketServiceNoAuth(t.Context(), ts.URL, "PROJECT", "REPO", "", false, nil)
svc, err = NewBitbucketServiceNoAuth(context.Background(), ts.URL, "PROJECT", "REPO")
require.NoError(t, err)
pullRequests, err = ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{
pullRequests, err = ListPullRequests(context.Background(), svc, []v1alpha1.PullRequestGeneratorFilter{
{
BranchMatch: &regexp,
},
@@ -492,18 +351,16 @@ func TestListPullRequestBranchMatch(t *testing.T) {
assert.Len(t, pullRequests, 1)
assert.Equal(t, PullRequest{
Number: 102,
Title: "feat(102)",
Branch: "feature-102",
TargetBranch: "branch",
HeadSHA: "bb3cf2e4d1517c83e720d2585b9402dbef71f992",
Labels: []string{},
Author: "testName",
}, *pullRequests[0])
regexp = `[\d{2}`
svc, err = NewBitbucketServiceNoAuth(t.Context(), ts.URL, "PROJECT", "REPO", "", false, nil)
svc, err = NewBitbucketServiceNoAuth(context.Background(), ts.URL, "PROJECT", "REPO")
require.NoError(t, err)
_, err = ListPullRequests(t.Context(), svc, []v1alpha1.PullRequestGeneratorFilter{
_, err = ListPullRequests(context.Background(), svc, []v1alpha1.PullRequestGeneratorFilter{
{
BranchMatch: &regexp,
},

View File

@@ -18,6 +18,6 @@ func NewFakeService(_ context.Context, listPullReuests []*PullRequest, listError
}, nil
}
func (g *FakeService) List(_ context.Context) ([]*PullRequest, error) {
func (g *FakeService) List(ctx context.Context) ([]*PullRequest, error) {
return g.listPullReuests, g.listError
}

View File

@@ -14,12 +14,11 @@ type GiteaService struct {
client *gitea.Client
owner string
repo string
labels []string
}
var _ PullRequestService = (*GiteaService)(nil)
func NewGiteaService(token, url, owner, repo string, labels []string, insecure bool) (PullRequestService, error) {
func NewGiteaService(ctx context.Context, token, url, owner, repo string, insecure bool) (PullRequestService, error) {
if token == "" {
token = os.Getenv("GITEA_TOKEN")
}
@@ -43,7 +42,6 @@ func NewGiteaService(token, url, owner, repo string, labels []string, insecure b
client: client,
owner: owner,
repo: repo,
labels: labels,
}, nil
}
@@ -51,44 +49,23 @@ func (g *GiteaService) List(ctx context.Context) ([]*PullRequest, error) {
opts := gitea.ListPullRequestsOptions{
State: gitea.StateOpen,
}
g.client.SetContext(ctx)
prs, _, err := g.client.ListRepoPullRequests(g.owner, g.repo, opts)
if err != nil {
return nil, err
}
list := []*PullRequest{}
for _, pr := range prs {
if !giteaContainLabels(g.labels, pr.Labels) {
continue
}
list = append(list, &PullRequest{
Number: int(pr.Index),
Title: pr.Title,
Branch: pr.Head.Ref,
TargetBranch: pr.Base.Ref,
HeadSHA: pr.Head.Sha,
Labels: getGiteaPRLabelNames(pr.Labels),
Author: pr.Poster.UserName,
})
}
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.
func getGiteaPRLabelNames(giteaLabels []*gitea.Label) []string {
var labelNames []string

View File

@@ -1,6 +1,7 @@
package pull_request
import (
"context"
"fmt"
"io"
"net/http"
@@ -13,7 +14,6 @@ import (
)
func giteaMockHandler(t *testing.T) func(http.ResponseWriter, *http.Request) {
t.Helper()
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Println(r.RequestURI)
@@ -52,7 +52,7 @@ func giteaMockHandler(t *testing.T) func(http.ResponseWriter, *http.Request) {
},
"title": "add an empty file",
"body": "",
"labels": [{"id": 1, "name": "label1", "color": "00aabb", "description": "foo", "url": ""}],
"labels": [],
"milestone": null,
"assignee": null,
"assignees": null,
@@ -246,69 +246,19 @@ 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) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
giteaMockHandler(t)(w, r)
}))
host, err := NewGiteaService("", ts.URL, "test-argocd", "pr-test", []string{"label1"}, false)
host, err := NewGiteaService(context.Background(), "", ts.URL, "test-argocd", "pr-test", false)
require.NoError(t, err)
prs, err := host.List(t.Context())
prs, err := host.List(context.Background())
require.NoError(t, err)
assert.Len(t, prs, 1)
assert.Equal(t, 1, prs[0].Number)
assert.Equal(t, "add an empty file", prs[0].Title)
assert.Equal(t, "test", prs[0].Branch)
assert.Equal(t, "main", prs[0].TargetBranch)
assert.Equal(t, "7bbaf62d92ddfafd9cc8b340c619abaec32bc09f", prs[0].HeadSHA)
assert.Equal(t, "graytshirt", prs[0].Author)
}
func TestGetGiteaPRLabelNames(t *testing.T) {

View File

@@ -3,12 +3,10 @@ package pull_request
import (
"context"
"fmt"
"net/http"
"os"
"github.com/google/go-github/v69/github"
appsetutils "github.com/argoproj/argo-cd/v3/applicationset/utils"
"github.com/google/go-github/v35/github"
"golang.org/x/oauth2"
)
type GithubService struct {
@@ -20,24 +18,24 @@ type GithubService struct {
var _ PullRequestService = (*GithubService)(nil)
func NewGithubService(token, url, owner, repo string, labels []string, optionalHTTPClient ...*http.Client) (PullRequestService, error) {
func NewGithubService(ctx context.Context, token, url, owner, repo string, labels []string) (PullRequestService, error) {
var ts oauth2.TokenSource
// Undocumented environment variable to set a default token, to be used in testing to dodge anonymous rate limits.
if token == "" {
token = os.Getenv("GITHUB_TOKEN")
}
if token != "" {
ts = oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: token},
)
}
httpClient := oauth2.NewClient(ctx, ts)
var client *github.Client
httpClient := appsetutils.GetOptionalHTTPClient(optionalHTTPClient...)
if url == "" {
if token == "" {
client = github.NewClient(httpClient)
} else {
client = github.NewClient(httpClient).WithAuthToken(token)
}
client = github.NewClient(httpClient)
} else {
var err error
client, err = github.NewClient(httpClient).WithEnterpriseURLs(url, url)
client, err = github.NewEnterpriseClient(url, url, httpClient)
if err != nil {
return nil, err
}
@@ -68,12 +66,10 @@ func (g *GithubService) List(ctx context.Context) ([]*PullRequest, error) {
}
pullRequests = append(pullRequests, &PullRequest{
Number: *pull.Number,
Title: *pull.Title,
Branch: *pull.Head.Ref,
TargetBranch: *pull.Base.Ref,
HeadSHA: *pull.Head.SHA,
Labels: getGithubPRLabelNames(pull.Labels),
Author: *pull.User.Login,
})
}
if resp.NextPage == 0 {

View File

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

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