Compare commits

...

25 Commits

Author SHA1 Message Date
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
51 changed files with 1447 additions and 502 deletions

View File

@@ -1 +1 @@
2.12.0
2.12.0-rc5

View File

@@ -126,23 +126,26 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
return ctrl.Result{}, nil
}
if err := r.migrateStatus(ctx, &applicationSetInfo); err != nil {
logCtx.Errorf("failed to migrate status subresource %v", err)
return ctrl.Result{}, err
}
// Log a warning if there are unrecognized generators
_ = utils.CheckInvalidGenerators(&applicationSetInfo)
// desiredApplications is the main list of all expected Applications from all generators in this appset.
desiredApplications, applicationSetReason, generatorsErr := r.generateApplications(logCtx, applicationSetInfo)
if generatorsErr != nil {
desiredApplications, applicationSetReason, err := r.generateApplications(logCtx, applicationSetInfo)
if err != nil {
_ = r.setApplicationSetStatusCondition(ctx,
&applicationSetInfo,
argov1alpha1.ApplicationSetCondition{
Type: argov1alpha1.ApplicationSetConditionErrorOccurred,
Message: generatorsErr.Error(),
Message: err.Error(),
Reason: string(applicationSetReason),
Status: argov1alpha1.ApplicationSetConditionStatusTrue,
}, parametersGenerated,
)
if len(desiredApplications) < 1 {
return ctrl.Result{}, generatorsErr
}
return ctrl.Result{RequeueAfter: ReconcileRequeueOnValidationError}, err
}
parametersGenerated = true
@@ -320,7 +323,7 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque
requeueAfter := r.getMinRequeueAfter(&applicationSetInfo)
if len(validateErrors) == 0 && generatorsErr == nil {
if len(validateErrors) == 0 {
if err := r.setApplicationSetStatusCondition(ctx,
&applicationSetInfo,
argov1alpha1.ApplicationSetCondition{
@@ -1153,6 +1156,13 @@ func (r *ApplicationSetReconciler) updateApplicationSetApplicationStatus(ctx con
} else {
// we have an existing AppStatus
currentAppStatus = applicationSet.Status.ApplicationStatus[idx]
// upgrade any existing AppStatus that might have been set by an older argo-cd version
// note: currentAppStatus.TargetRevisions may be set to empty list earlier during migrations,
// to prevent other usage of r.Client.Status().Update to fail before reaching here.
if currentAppStatus.TargetRevisions == nil || len(currentAppStatus.TargetRevisions) == 0 {
currentAppStatus.TargetRevisions = app.Status.GetRevisions()
}
}
appOutdated := false
@@ -1350,6 +1360,27 @@ func findApplicationStatusIndex(appStatuses []argov1alpha1.ApplicationSetApplica
return -1
}
// migrateStatus run migrations on the status subresource of ApplicationSet early during the run of ApplicationSetReconciler.Reconcile
// this handles any defaulting of values - which would otherwise cause the references to r.Client.Status().Update to fail given missing required fields.
func (r *ApplicationSetReconciler) migrateStatus(ctx context.Context, appset *argov1alpha1.ApplicationSet) error {
update := false
if statusList := appset.Status.ApplicationStatus; statusList != nil {
for idx := range statusList {
if statusList[idx].TargetRevisions == nil {
statusList[idx].TargetRevisions = []string{}
update = true
}
}
}
if update {
if err := r.Client.Status().Update(ctx, appset); err != nil {
return fmt.Errorf("unable to set application set status: %w", err)
}
}
return nil
}
func (r *ApplicationSetReconciler) updateResourcesStatus(ctx context.Context, logCtx *log.Entry, appset *argov1alpha1.ApplicationSet, apps []argov1alpha1.Application) error {
statusMap := getResourceStatusMap(appset)
statusMap = buildResourceStatus(statusMap, apps)

View File

@@ -9,6 +9,8 @@ import (
"testing"
"time"
"github.com/argoproj/argo-cd/v2/applicationset/generators/mocks"
log "github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@@ -2139,6 +2141,58 @@ func TestGetMinRequeueAfter(t *testing.T) {
assert.Equal(t, time.Duration(1)*time.Second, got)
}
func TestRequeueGeneratorFails(t *testing.T) {
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
err = v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
appSet := v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "argocd",
},
Spec: v1alpha1.ApplicationSetSpec{
Generators: []v1alpha1.ApplicationSetGenerator{{
PullRequest: &v1alpha1.PullRequestGenerator{},
}},
},
}
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appSet).Build()
generator := v1alpha1.ApplicationSetGenerator{
PullRequest: &v1alpha1.PullRequestGenerator{},
}
generatorMock := mocks.Generator{}
generatorMock.On("GetTemplate", &generator).
Return(&v1alpha1.ApplicationSetTemplate{})
generatorMock.On("GenerateParams", &generator, mock.AnythingOfType("*v1alpha1.ApplicationSet"), mock.Anything).
Return([]map[string]interface{}{}, fmt.Errorf("Simulated error generating params that could be related to an external service/API call"))
r := ApplicationSetReconciler{
Client: client,
Scheme: scheme,
Recorder: record.NewFakeRecorder(0),
Cache: &fakeCache{},
Generators: map[string]generators.Generator{
"PullRequest": &generatorMock,
},
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "argocd",
Name: "name",
},
}
res, err := r.Reconcile(context.Background(), req)
require.Error(t, err)
assert.Equal(t, ReconcileRequeueOnValidationError, res.RequeueAfter)
}
func TestValidateGeneratedApplications(t *testing.T) {
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
@@ -2471,90 +2525,6 @@ func TestReconcilerValidationProjectErrorBehaviour(t *testing.T) {
require.Error(t, err)
}
func TestReconcilerCreateAppsRecoveringRenderError(t *testing.T) {
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
err = v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
project := v1alpha1.AppProject{
ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "argocd"},
}
appSet := v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "argocd",
},
Spec: v1alpha1.ApplicationSetSpec{
GoTemplate: true,
Generators: []v1alpha1.ApplicationSetGenerator{
{
List: &v1alpha1.ListGenerator{
Elements: []apiextensionsv1.JSON{{
Raw: []byte(`{"name": "very-good-app"}`),
}, {
Raw: []byte(`{"name": "bad-app"}`),
}},
},
},
},
Template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{
Name: "{{ index (splitList \"-\" .name ) 2 }}",
Namespace: "argocd",
},
Spec: v1alpha1.ApplicationSpec{
Source: &v1alpha1.ApplicationSource{RepoURL: "https://github.com/argoproj/argocd-example-apps", Path: "guestbook"},
Project: "default",
Destination: v1alpha1.ApplicationDestination{Server: "https://kubernetes.default.svc"},
},
},
},
}
kubeclientset := kubefake.NewSimpleClientset()
argoDBMock := dbmocks.ArgoDB{}
argoObjs := []runtime.Object{&project}
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appSet).WithStatusSubresource(&appSet).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build()
r := ApplicationSetReconciler{
Client: client,
Scheme: scheme,
Renderer: &utils.Render{},
Recorder: record.NewFakeRecorder(1),
Cache: &fakeCache{},
Generators: map[string]generators.Generator{
"List": generators.NewListGenerator(),
},
ArgoDB: &argoDBMock,
ArgoAppClientset: appclientset.NewSimpleClientset(argoObjs...),
KubeClientset: kubeclientset,
Policy: v1alpha1.ApplicationsSyncPolicySync,
ArgoCDNamespace: "argocd",
}
req := ctrl.Request{
NamespacedName: types.NamespacedName{
Namespace: "argocd",
Name: "name",
},
}
// Verify that on generatorsError, no error is returned, but the object is requeued
res, err := r.Reconcile(context.Background(), req)
require.NoError(t, err)
assert.Equal(t, ReconcileRequeueOnValidationError, res.RequeueAfter)
var app v1alpha1.Application
// make sure good app got created
err = r.Client.Get(context.TODO(), crtclient.ObjectKey{Namespace: "argocd", Name: "app"}, &app)
require.NoError(t, err)
assert.Equal(t, "app", app.Name)
}
func TestSetApplicationSetStatusCondition(t *testing.T) {
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
@@ -4791,6 +4761,58 @@ func TestUpdateApplicationSetApplicationStatus(t *testing.T) {
},
},
},
{
name: "handles an outdated list of statuses with a healthy application, setting required variables",
appSet: v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
Namespace: "argocd",
},
Spec: v1alpha1.ApplicationSetSpec{
Strategy: &v1alpha1.ApplicationSetStrategy{
Type: "RollingSync",
RollingSync: &v1alpha1.ApplicationSetRolloutStrategy{},
},
},
Status: v1alpha1.ApplicationSetStatus{
ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
{
Application: "app1",
Message: "Application resource is already Healthy, updating status from Waiting to Healthy.",
Status: "Healthy",
Step: "1",
},
},
},
},
apps: []v1alpha1.Application{
{
ObjectMeta: metav1.ObjectMeta{
Name: "app1",
},
Status: v1alpha1.ApplicationStatus{
Health: v1alpha1.HealthStatus{
Status: health.HealthStatusHealthy,
},
OperationState: &v1alpha1.OperationState{
Phase: common.OperationSucceeded,
},
Sync: v1alpha1.SyncStatus{
Status: v1alpha1.SyncStatusCodeSynced,
},
},
},
},
expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
{
Application: "app1",
Message: "Application resource is already Healthy, updating status from Waiting to Healthy.",
Status: "Healthy",
Step: "1",
TargetRevisions: []string{},
},
},
},
{
name: "progresses an OutOfSync RollingSync application to waiting",
appSet: v1alpha1.ApplicationSet{
@@ -4880,10 +4902,11 @@ func TestUpdateApplicationSetApplicationStatus(t *testing.T) {
Status: v1alpha1.ApplicationSetStatus{
ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
{
Application: "app1",
Message: "",
Status: "Pending",
Step: "1",
Application: "app1",
Message: "",
Status: "Pending",
Step: "1",
TargetRevisions: []string{"Next"},
},
},
},
@@ -4902,15 +4925,16 @@ func TestUpdateApplicationSetApplicationStatus(t *testing.T) {
},
expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
{
Application: "app1",
Message: "Application resource became Progressing, updating status from Pending to Progressing.",
Status: "Progressing",
Step: "1",
Application: "app1",
Message: "Application resource became Progressing, updating status from Pending to Progressing.",
Status: "Progressing",
Step: "1",
TargetRevisions: []string{"Next"},
},
},
},
{
name: "progresses a pending syncing application to progressing",
name: "progresses a pending synced application to progressing",
appSet: v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "name",
@@ -4925,10 +4949,11 @@ func TestUpdateApplicationSetApplicationStatus(t *testing.T) {
Status: v1alpha1.ApplicationSetStatus{
ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
{
Application: "app1",
Message: "",
Status: "Pending",
Step: "1",
Application: "app1",
Message: "",
Status: "Pending",
Step: "1",
TargetRevisions: []string{"Current"},
},
},
},
@@ -4953,10 +4978,11 @@ func TestUpdateApplicationSetApplicationStatus(t *testing.T) {
},
expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
{
Application: "app1",
Message: "Application resource became Progressing, updating status from Pending to Progressing.",
Status: "Progressing",
Step: "1",
Application: "app1",
Message: "Application resource became Progressing, updating status from Pending to Progressing.",
Status: "Progressing",
Step: "1",
TargetRevisions: []string{"Current"},
},
},
},
@@ -4976,10 +5002,11 @@ func TestUpdateApplicationSetApplicationStatus(t *testing.T) {
Status: v1alpha1.ApplicationSetStatus{
ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
{
Application: "app1",
Message: "",
Status: "Progressing",
Step: "1",
Application: "app1",
Message: "",
Status: "Progressing",
Step: "1",
TargetRevisions: []string{"Next"},
},
},
},
@@ -5004,10 +5031,11 @@ func TestUpdateApplicationSetApplicationStatus(t *testing.T) {
},
expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
{
Application: "app1",
Message: "Application resource became Healthy, updating status from Progressing to Healthy.",
Status: "Healthy",
Step: "1",
Application: "app1",
Message: "Application resource became Healthy, updating status from Progressing to Healthy.",
Status: "Healthy",
Step: "1",
TargetRevisions: []string{"Next"},
},
},
},
@@ -5027,10 +5055,11 @@ func TestUpdateApplicationSetApplicationStatus(t *testing.T) {
Status: v1alpha1.ApplicationSetStatus{
ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
{
Application: "app1",
Message: "",
Status: "Waiting",
Step: "1",
Application: "app1",
Message: "",
Status: "Waiting",
Step: "1",
TargetRevisions: []string{"Current"},
},
},
},
@@ -5055,10 +5084,11 @@ func TestUpdateApplicationSetApplicationStatus(t *testing.T) {
},
expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
{
Application: "app1",
Message: "Application resource is already Healthy, updating status from Waiting to Healthy.",
Status: "Healthy",
Step: "1",
Application: "app1",
Message: "Application resource is already Healthy, updating status from Waiting to Healthy.",
Status: "Healthy",
Step: "1",
TargetRevisions: []string{"Current"},
},
},
},
@@ -5334,16 +5364,18 @@ func TestUpdateApplicationSetApplicationStatus(t *testing.T) {
Status: v1alpha1.ApplicationSetStatus{
ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
{
Application: "app1",
Message: "Application has pending changes, setting status to Waiting.",
Status: "Waiting",
Step: "1",
Application: "app1",
Message: "Application has pending changes, setting status to Waiting.",
Status: "Waiting",
Step: "1",
TargetRevisions: []string{"Current"},
},
{
Application: "app2",
Message: "Application has pending changes, setting status to Waiting.",
Status: "Waiting",
Step: "1",
Application: "app2",
Message: "Application has pending changes, setting status to Waiting.",
Status: "Waiting",
Step: "1",
TargetRevisions: []string{"Current"},
},
},
},
@@ -5368,10 +5400,11 @@ func TestUpdateApplicationSetApplicationStatus(t *testing.T) {
},
expectedAppStatus: []v1alpha1.ApplicationSetApplicationStatus{
{
Application: "app1",
Message: "Application resource is already Healthy, updating status from Waiting to Healthy.",
Status: "Healthy",
Step: "1",
Application: "app1",
Message: "Application resource is already Healthy, updating status from Waiting to Healthy.",
Status: "Healthy",
Step: "1",
TargetRevisions: []string{"Current"},
},
},
},
@@ -5533,9 +5566,10 @@ func TestUpdateApplicationSetApplicationStatusProgress(t *testing.T) {
Status: v1alpha1.ApplicationSetStatus{
ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
{
Application: "app1",
Message: "Application is out of date with the current AppSet generation, setting status to Waiting.",
Status: "Waiting",
Application: "app1",
Message: "Application is out of date with the current AppSet generation, setting status to Waiting.",
Status: "Waiting",
TargetRevisions: []string{"Next"},
},
},
},
@@ -5553,6 +5587,7 @@ func TestUpdateApplicationSetApplicationStatusProgress(t *testing.T) {
Message: "Application moved to Pending status, watching for the Application resource to start Progressing.",
Status: "Pending",
Step: "1",
TargetRevisions: []string{"Next"},
},
},
},
@@ -6559,3 +6594,74 @@ func TestOwnsHandler(t *testing.T) {
})
}
}
func TestMigrateStatus(t *testing.T) {
scheme := runtime.NewScheme()
err := v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
err = v1alpha1.AddToScheme(scheme)
require.NoError(t, err)
for _, tc := range []struct {
name string
appset v1alpha1.ApplicationSet
expectedStatus v1alpha1.ApplicationSetStatus
}{
{
name: "status without applicationstatus target revisions set will default to empty list",
appset: v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "test",
},
Status: v1alpha1.ApplicationSetStatus{
ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
{},
},
},
},
expectedStatus: v1alpha1.ApplicationSetStatus{
ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
{
TargetRevisions: []string{},
},
},
},
},
{
name: "status with applicationstatus target revisions set will do nothing",
appset: v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "test",
},
Status: v1alpha1.ApplicationSetStatus{
ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
{
TargetRevisions: []string{"Current"},
},
},
},
},
expectedStatus: v1alpha1.ApplicationSetStatus{
ApplicationStatus: []v1alpha1.ApplicationSetApplicationStatus{
{
TargetRevisions: []string{"Current"},
},
},
},
},
} {
t.Run(tc.name, func(t *testing.T) {
client := fake.NewClientBuilder().WithScheme(scheme).WithStatusSubresource(&tc.appset).WithObjects(&tc.appset).Build()
r := ApplicationSetReconciler{
Client: client,
}
err := r.migrateStatus(context.Background(), &tc.appset)
require.NoError(t, err)
assert.Equal(t, tc.expectedStatus, tc.appset.Status)
})
}
}

View File

@@ -9,6 +9,8 @@ import (
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.

View File

@@ -0,0 +1,100 @@
// Code generated by mockery v2.40.2. DO NOT EDIT.
package mocks
import (
client "sigs.k8s.io/controller-runtime/pkg/client"
mock "github.com/stretchr/testify/mock"
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 {
mock.TestingT
Cleanup(func())
}) *Generator {
mock := &Generator{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -104,7 +104,17 @@ func loadClusters(ctx context.Context, kubeClient *kubernetes.Clientset, appClie
if err != nil {
return nil, err
}
client := redis.NewClient(&redis.Options{Addr: fmt.Sprintf("localhost:%d", port)})
redisOptions := &redis.Options{Addr: fmt.Sprintf("localhost:%d", port)}
secret, err := kubeClient.CoreV1().Secrets(namespace).Get(context.Background(), defaulRedisInitialPasswordSecretName, v1.GetOptions{})
if err == nil {
if _, ok := secret.Data[defaultResisInitialPasswordKey]; ok {
redisOptions.Password = string(secret.Data[defaultResisInitialPasswordKey])
}
}
client := redis.NewClient(redisOptions)
compressionType, err := cacheutil.CompressionTypeFromString(redisCompressionStr)
if err != nil {
return nil, err

View File

@@ -776,8 +776,6 @@ func NewApplicationSetCommand(clientOpts *argocdclient.ClientOptions) *cobra.Com
}
}
// sourcePosition startes with 1, thus, it needs to be decreased by 1 to find the correct index in the list of sources
sourcePosition = sourcePosition - 1
visited := cmdutil.SetAppSpecOptions(c.Flags(), &app.Spec, &appOpts, sourcePosition)
if visited == 0 {
log.Error("Please set at least one option to update")

View File

@@ -1767,6 +1767,22 @@ func (ctrl *ApplicationController) normalizeApplication(orig, app *appv1.Applica
}
}
func createMergePatch(orig, new interface{}) ([]byte, bool, error) {
origBytes, err := json.Marshal(orig)
if err != nil {
return nil, false, err
}
newBytes, err := json.Marshal(new)
if err != nil {
return nil, false, err
}
patch, err := jsonpatch.CreateMergePatch(origBytes, newBytes)
if err != nil {
return nil, false, err
}
return patch, string(patch) != "{}", nil
}
// persistAppStatus persists updates to application status. If no changes were made, it is a no-op
func (ctrl *ApplicationController) persistAppStatus(orig *appv1.Application, newStatus *appv1.ApplicationStatus) (patchMs time.Duration) {
logCtx := getAppLog(orig)
@@ -1786,9 +1802,9 @@ func (ctrl *ApplicationController) persistAppStatus(orig *appv1.Application, new
}
delete(newAnnotations, appv1.AnnotationKeyRefresh)
}
patch, modified, err := diff.CreateTwoWayMergePatch(
patch, modified, err := createMergePatch(
&appv1.Application{ObjectMeta: metav1.ObjectMeta{Annotations: orig.GetAnnotations()}, Status: orig.Status},
&appv1.Application{ObjectMeta: metav1.ObjectMeta{Annotations: newAnnotations}, Status: *newStatus}, appv1.Application{})
&appv1.Application{ObjectMeta: metav1.ObjectMeta{Annotations: newAnnotations}, Status: *newStatus})
if err != nil {
logCtx.Errorf("Error constructing app status patch: %v", err)
return

View File

@@ -374,8 +374,8 @@ data:
var fakePostDeleteHook = `
{
"apiVersion": "v1",
"kind": "Pod",
"apiVersion": "batch/v1",
"kind": "Job",
"metadata": {
"name": "post-delete-hook",
"namespace": "default",
@@ -388,22 +388,93 @@ var fakePostDeleteHook = `
}
},
"spec": {
"containers": [
{
"name": "post-delete-hook",
"image": "busybox",
"restartPolicy": "Never",
"command": [
"/bin/sh",
"-c",
"sleep 5 && echo hello from the post-delete-hook pod"
]
"template": {
"metadata": {
"name": "post-delete-hook"
},
"spec": {
"containers": [
{
"name": "post-delete-hook",
"image": "busybox",
"command": [
"/bin/sh",
"-c",
"sleep 5 && echo hello from the post-delete-hook job"
]
}
],
"restartPolicy": "Never"
}
]
}
}
}
`
var fakeServiceAccount = `
{
"apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": {
"name": "hook-serviceaccount",
"namespace": "default",
"annotations": {
"argocd.argoproj.io/hook": "PostDelete",
"argocd.argoproj.io/hook-delete-policy": "BeforeHookCreation,HookSucceeded"
}
}
}
`
var fakeRole = `
{
"apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "Role",
"metadata": {
"name": "hook-role",
"namespace": "default",
"annotations": {
"argocd.argoproj.io/hook": "PostDelete",
"argocd.argoproj.io/hook-delete-policy": "BeforeHookCreation,HookSucceeded"
}
},
"rules": [
{
"apiGroups": [""],
"resources": ["secrets"],
"verbs": ["get", "delete", "list"]
}
]
}
`
var fakeRoleBinding = `
{
"apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "RoleBinding",
"metadata": {
"name": "hook-rolebinding",
"namespace": "default",
"annotations": {
"argocd.argoproj.io/hook": "PostDelete",
"argocd.argoproj.io/hook-delete-policy": "BeforeHookCreation,HookSucceeded"
}
},
"roleRef": {
"apiGroup": "rbac.authorization.k8s.io",
"kind": "Role",
"name": "hook-role"
},
"subjects": [
{
"kind": "ServiceAccount",
"name": "hook-serviceaccount",
"namespace": "default"
}
]
}
`
func newFakeApp() *v1alpha1.Application {
return createFakeApp(fakeApp)
}
@@ -439,12 +510,39 @@ func newFakeCM() map[string]interface{} {
}
func newFakePostDeleteHook() map[string]interface{} {
var cm map[string]interface{}
err := yaml.Unmarshal([]byte(fakePostDeleteHook), &cm)
var hook map[string]interface{}
err := yaml.Unmarshal([]byte(fakePostDeleteHook), &hook)
if err != nil {
panic(err)
}
return cm
return hook
}
func newFakeRoleBinding() map[string]interface{} {
var roleBinding map[string]interface{}
err := yaml.Unmarshal([]byte(fakeRoleBinding), &roleBinding)
if err != nil {
panic(err)
}
return roleBinding
}
func newFakeRole() map[string]interface{} {
var role map[string]interface{}
err := yaml.Unmarshal([]byte(fakeRole), &role)
if err != nil {
panic(err)
}
return role
}
func newFakeServiceAccount() map[string]interface{} {
var serviceAccount map[string]interface{}
err := yaml.Unmarshal([]byte(fakeServiceAccount), &serviceAccount)
if err != nil {
panic(err)
}
return serviceAccount
}
func TestAutoSync(t *testing.T) {
@@ -721,7 +819,7 @@ func TestFinalizeAppDeletion(t *testing.T) {
// Ensure any stray resources irregularly labeled with instance label of app are not deleted upon deleting,
// when app project restriction is in place
t.Run("ProjectRestrictionEnforced", func(*testing.T) {
t.Run("ProjectRestrictionEnforced", func(t *testing.T) {
restrictedProj := v1alpha1.AppProject{
ObjectMeta: metav1.ObjectMeta{
Name: "restricted",
@@ -882,7 +980,13 @@ func TestFinalizeAppDeletion(t *testing.T) {
app.SetPostDeleteFinalizer()
app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
liveHook := &unstructured.Unstructured{Object: newFakePostDeleteHook()}
require.NoError(t, unstructured.SetNestedField(liveHook.Object, "Succeeded", "status", "phase"))
conditions := []interface{}{
map[string]interface{}{
"type": "Complete",
"status": "True",
},
}
require.NoError(t, unstructured.SetNestedField(liveHook.Object, conditions, "status", "conditions"))
ctrl := newFakeController(&fakeData{
manifestResponses: []*apiclient.ManifestResponse{{
Manifests: []string{fakePostDeleteHook},
@@ -916,15 +1020,27 @@ func TestFinalizeAppDeletion(t *testing.T) {
app := newFakeApp()
app.SetPostDeleteFinalizer("cleanup")
app.Spec.Destination.Namespace = test.FakeArgoCDNamespace
liveRoleBinding := &unstructured.Unstructured{Object: newFakeRoleBinding()}
liveRole := &unstructured.Unstructured{Object: newFakeRole()}
liveServiceAccount := &unstructured.Unstructured{Object: newFakeServiceAccount()}
liveHook := &unstructured.Unstructured{Object: newFakePostDeleteHook()}
require.NoError(t, unstructured.SetNestedField(liveHook.Object, "Succeeded", "status", "phase"))
conditions := []interface{}{
map[string]interface{}{
"type": "Complete",
"status": "True",
},
}
require.NoError(t, unstructured.SetNestedField(liveHook.Object, conditions, "status", "conditions"))
ctrl := newFakeController(&fakeData{
manifestResponses: []*apiclient.ManifestResponse{{
Manifests: []string{fakePostDeleteHook},
Manifests: []string{fakeRoleBinding, fakeRole, fakeServiceAccount, fakePostDeleteHook},
}},
apps: []runtime.Object{app, &defaultProj},
managedLiveObjs: map[kube.ResourceKey]*unstructured.Unstructured{
kube.GetResourceKey(liveHook): liveHook,
kube.GetResourceKey(liveRoleBinding): liveRoleBinding,
kube.GetResourceKey(liveRole): liveRole,
kube.GetResourceKey(liveServiceAccount): liveServiceAccount,
kube.GetResourceKey(liveHook): liveHook,
},
}, nil)
@@ -943,9 +1059,14 @@ func TestFinalizeAppDeletion(t *testing.T) {
return []*v1alpha1.Cluster{}, nil
})
require.NoError(t, err)
// post-delete hook is deleted
require.Len(t, ctrl.kubectl.(*MockKubectl).DeletedResources, 1)
require.Equal(t, "post-delete-hook", ctrl.kubectl.(*MockKubectl).DeletedResources[0].Name)
// post-delete hooks are deleted
require.Len(t, ctrl.kubectl.(*MockKubectl).DeletedResources, 4)
deletedResources := []string{}
for _, res := range ctrl.kubectl.(*MockKubectl).DeletedResources {
deletedResources = append(deletedResources, res.Name)
}
expectedNames := []string{"hook-rolebinding", "hook-role", "hook-serviceaccount", "post-delete-hook"}
require.ElementsMatch(t, expectedNames, deletedResources, "Deleted resources should match expected names")
// finalizer is not removed
assert.False(t, patched)
})
@@ -992,7 +1113,7 @@ func TestNormalizeApplication(t *testing.T) {
normalized := false
fakeAppCs.AddReactor("patch", "*", func(action kubetesting.Action) (handled bool, ret runtime.Object, err error) {
if patchAction, ok := action.(kubetesting.PatchAction); ok {
if string(patchAction.GetPatch()) == `{"spec":{"project":"default"},"status":{"sync":{"comparedTo":{"destination":{},"source":{"repoURL":""}}}}}` {
if string(patchAction.GetPatch()) == `{"spec":{"project":"default"}}` {
normalized = true
}
}
@@ -1949,3 +2070,65 @@ func TestAddControllerNamespace(t *testing.T) {
assert.Equal(t, test.FakeArgoCDNamespace, updatedApp.Status.ControllerNamespace)
})
}
func TestHelmValuesObjectHasReplaceStrategy(t *testing.T) {
app := v1alpha1.Application{
Status: v1alpha1.ApplicationStatus{Sync: v1alpha1.SyncStatus{ComparedTo: v1alpha1.ComparedTo{
Source: v1alpha1.ApplicationSource{
Helm: &v1alpha1.ApplicationSourceHelm{
ValuesObject: &runtime.RawExtension{
Object: &unstructured.Unstructured{Object: map[string]interface{}{"key": []string{"value"}}},
},
},
},
}}},
}
appModified := v1alpha1.Application{
Status: v1alpha1.ApplicationStatus{Sync: v1alpha1.SyncStatus{ComparedTo: v1alpha1.ComparedTo{
Source: v1alpha1.ApplicationSource{
Helm: &v1alpha1.ApplicationSourceHelm{
ValuesObject: &runtime.RawExtension{
Object: &unstructured.Unstructured{Object: map[string]interface{}{"key": []string{"value-modified1"}}},
},
},
},
}}},
}
patch, _, err := createMergePatch(
app,
appModified)
require.NoError(t, err)
assert.Equal(t, `{"status":{"sync":{"comparedTo":{"source":{"helm":{"valuesObject":{"key":["value-modified1"]}}}}}}}`, string(patch))
}
func TestAppStatusIsReplaced(t *testing.T) {
original := &v1alpha1.ApplicationStatus{Sync: v1alpha1.SyncStatus{
ComparedTo: v1alpha1.ComparedTo{
Destination: v1alpha1.ApplicationDestination{
Server: "https://mycluster",
},
},
}}
updated := &v1alpha1.ApplicationStatus{Sync: v1alpha1.SyncStatus{
ComparedTo: v1alpha1.ComparedTo{
Destination: v1alpha1.ApplicationDestination{
Name: "mycluster",
},
},
}}
patchData, ok, err := createMergePatch(original, updated)
require.NoError(t, err)
require.True(t, ok)
patchObj := map[string]interface{}{}
require.NoError(t, json.Unmarshal(patchData, &patchObj))
val, has, err := unstructured.NestedFieldNoCopy(patchObj, "sync", "comparedTo", "destination", "server")
require.NoError(t, err)
require.True(t, has)
require.Nil(t, val)
}

View File

@@ -98,6 +98,18 @@ func (ctrl *ApplicationController) executePostDeleteHooks(app *v1alpha1.Applicat
if err != nil {
return false, err
}
if hookHealth == nil {
logCtx.WithFields(log.Fields{
"group": obj.GroupVersionKind().Group,
"version": obj.GroupVersionKind().Version,
"kind": obj.GetKind(),
"name": obj.GetName(),
"namespace": obj.GetNamespace(),
}).Info("No health check defined for resource, considering it healthy")
hookHealth = &health.HealthStatus{
Status: health.HealthStatusHealthy,
}
}
if hookHealth.Status == health.HealthStatusProgressing {
progressingHooksCnt++
}
@@ -128,6 +140,11 @@ func (ctrl *ApplicationController) cleanupPostDeleteHooks(liveObjs map[kube.Reso
if err != nil {
return false, err
}
if hookHealth == nil {
hookHealth = &health.HealthStatus{
Status: health.HealthStatusHealthy,
}
}
if health.IsWorse(aggregatedHealth, hookHealth.Status) {
aggregatedHealth = hookHealth.Status
}

View File

@@ -326,7 +326,7 @@ As with other generators, clusters *must* already be defined within Argo CD, in
In addition to the flattened key/value pairs from the configuration file, the following generator parameters are provided:
- `{{.path.path}}`: The path to the directory containing matching configuration file within the Git repository. Example: `/clusters/clusterA`, if the config file was `/clusters/clusterA/config.json`
- `{{index .path n}}`: The path to the matching configuration file within the Git repository, split into array elements (`n` - array index). Example: `index .path 0: clusters`, `index .path 1: clusterA`
- `{{index .path.segments n}}`: The path to the matching configuration file within the Git repository, split into array elements (`n` - array index). Example: `index .path.segments 0: clusters`, `index .path.segments 1: clusterA`
- `{{.path.basename}}`: Basename of the path to the directory containing the configuration file (e.g. `clusterA`, with the above example.)
- `{{.path.basenameNormalized}}`: This field is the same as `.path.basename` with unsupported characters replaced with `-` (e.g. a `path` of `/directory/directory_2`, and `.path.basename` of `directory_2` would produce `directory-2` here).
- `{{.path.filename}}`: The matched filename. e.g., `config.json` in the above example.
@@ -360,7 +360,7 @@ spec:
files:
- path: "applicationset/examples/git-generator-files-discovery/cluster-config/**/config.json"
values:
base_dir: "{{index .path 0}}/{{index .path 1}}/{{index .path 2}}"
base_dir: "{{index .path.segments 0}}/{{index .path.segments 1}}/{{index .path.segments 2}}"
template:
metadata:
name: '{{.cluster.name}}-guestbook'

View File

@@ -420,3 +420,5 @@ data:
cluster:
name: some-cluster
server: https://some-cluster
# The maximum size of the payload that can be sent to the webhook server.
webhook.maxPayloadSizeMB: 1024

View File

@@ -1,6 +1,5 @@
| Argo CD version | Kubernetes versions |
|-----------------|---------------------|
| 2.7 | v1.26, v1.25, v1.24, v1.23 |
| 2.6 | v1.24, v1.23, v1.22 |
| 2.5 | v1.24, v1.23, v1.22 |
| 2.12 | |
| 2.11 | v1.29, v1.28, v1.27, v1.26, v1.25 |
| 2.10 | v1.28, v1.27, v1.26, v1.25 |

View File

@@ -19,6 +19,8 @@ URL configured in the Git provider should use the `/api/webhook` endpoint of you
(e.g. `https://argocd.example.com/api/webhook`). If you wish to use a shared secret, input an
arbitrary value in the secret. This value will be used when configuring the webhook in the next step.
To prevent DDoS attacks with unauthenticated webhook events (the `/api/webhook` endpoint currently lacks rate limiting protection), it is recommended to limit the payload size. You can achieve this by configuring the `argocd-cm` ConfigMap with the `webhook.maxPayloadSizeMB` attribute. The default value is 1GB.
## Github
![Add Webhook](../assets/webhook-config.png "Add Webhook")

6
go.mod
View File

@@ -11,7 +11,7 @@ require (
github.com/TomOnTime/utfutil v0.0.0-20180511104225-09c41003ee1d
github.com/alicebob/miniredis/v2 v2.30.4
github.com/antonmedv/expr v1.15.2
github.com/argoproj/gitops-engine v0.7.1-0.20240615185936-83ce6ca8cedc
github.com/argoproj/gitops-engine v0.7.1-0.20240714153147-adb68bcaab73
github.com/argoproj/notifications-engine v0.4.1-0.20240606074338-0802cd427621
github.com/argoproj/pkg v0.13.7-0.20230626144333-d56162821bd1
github.com/aws/aws-sdk-go v1.50.8
@@ -52,14 +52,14 @@ require (
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
github.com/grpc-ecosystem/grpc-gateway v1.16.0
github.com/hashicorp/go-retryablehttp v0.7.4
github.com/hashicorp/go-retryablehttp v0.7.7
github.com/imdario/mergo v0.3.16
github.com/improbable-eng/grpc-web v0.15.0
github.com/itchyny/gojq v0.12.13
github.com/jeremywohl/flatten v1.0.1
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/ktrysmt/go-bitbucket v0.9.67
github.com/mattn/go-isatty v0.0.19
github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-zglob v0.0.4
github.com/microsoft/azure-devops-go-api/azuredevops v1.0.0-b5
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1

20
go.sum
View File

@@ -695,8 +695,8 @@ github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU=
github.com/appscode/go v0.0.0-20191119085241-0887d8ec2ecc/go.mod h1:OawnOmAL4ZX3YaPdN+8HTNwBveT1jMsqP74moa9XUbE=
github.com/argoproj/gitops-engine v0.7.1-0.20240615185936-83ce6ca8cedc h1:J7LJp2Gh9A9/eQN7Lg74JW+YOVO5NEjq5/cudGAiOwk=
github.com/argoproj/gitops-engine v0.7.1-0.20240615185936-83ce6ca8cedc/go.mod h1:ByLmH5B1Gs361tgI5x5f8oSFuBEXDYENYpG3zFDWtHU=
github.com/argoproj/gitops-engine v0.7.1-0.20240714153147-adb68bcaab73 h1:7kyTgFsPjvb6noafslp2pr7fBCS9s8OJ759LdLzrOro=
github.com/argoproj/gitops-engine v0.7.1-0.20240714153147-adb68bcaab73/go.mod h1:xMIbuLg9Qj2e0egTy+8NcukbhRaVmWwK9vm3aAQZoi4=
github.com/argoproj/notifications-engine v0.4.1-0.20240606074338-0802cd427621 h1:Yg1nt+D2uDK1SL2jSlfukA4yc7db184TTN7iWy3voRE=
github.com/argoproj/notifications-engine v0.4.1-0.20240606074338-0802cd427621/go.mod h1:N0A4sEws2soZjEpY4hgZpQS8mRIEw6otzwfkgc3g9uQ=
github.com/argoproj/pkg v0.13.7-0.20230626144333-d56162821bd1 h1:qsHwwOJ21K2Ao0xPju1sNuqphyMnMYkyB3ZLoLtxWpo=
@@ -892,6 +892,8 @@ github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+ne
github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
@@ -1242,14 +1244,14 @@ github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-retryablehttp v0.5.1/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA=
github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
@@ -1383,13 +1385,15 @@ github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsI
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=

View File

@@ -27,6 +27,8 @@ rules:
- appprojects
verbs:
- get
- list
- watch
- apiGroups:
- argoproj.io
resources:
@@ -62,4 +64,4 @@ rules:
verbs:
- get
- list
- watch
- watch

View File

@@ -5,7 +5,7 @@ kind: Kustomization
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: latest
newTag: v2.12.0-rc5
resources:
- ./application-controller
- ./dex

View File

@@ -20822,6 +20822,8 @@ rules:
- appprojects
verbs:
- get
- list
- watch
- apiGroups:
- argoproj.io
resources:
@@ -21268,7 +21270,7 @@ spec:
key: applicationsetcontroller.enable.scm.providers
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -21386,7 +21388,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -21639,7 +21641,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -21691,7 +21693,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -21963,7 +21965,7 @@ spec:
key: controller.ignore.normalizer.jq.timeout
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -12,4 +12,4 @@ resources:
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: latest
newTag: v2.12.0-rc5

View File

@@ -12,7 +12,7 @@ patches:
images:
- name: quay.io/argoproj/argocd
newName: quay.io/argoproj/argocd
newTag: latest
newTag: v2.12.0-rc5
resources:
- ../../base/application-controller
- ../../base/applicationset-controller

View File

@@ -20860,6 +20860,8 @@ rules:
- appprojects
verbs:
- get
- list
- watch
- apiGroups:
- argoproj.io
resources:
@@ -22609,7 +22611,7 @@ spec:
key: applicationsetcontroller.enable.scm.providers
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -22732,7 +22734,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -22814,7 +22816,7 @@ spec:
key: notificationscontroller.selfservice.enabled
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -22933,7 +22935,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -23214,7 +23216,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -23266,7 +23268,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -23590,7 +23592,7 @@ spec:
key: server.api.content.types
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -23889,7 +23891,7 @@ spec:
key: controller.ignore.normalizer.jq.timeout
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -149,6 +149,8 @@ rules:
- appprojects
verbs:
- get
- list
- watch
- apiGroups:
- argoproj.io
resources:
@@ -1686,7 +1688,7 @@ spec:
key: applicationsetcontroller.enable.scm.providers
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -1809,7 +1811,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -1891,7 +1893,7 @@ spec:
key: notificationscontroller.selfservice.enabled
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -2010,7 +2012,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -2291,7 +2293,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -2343,7 +2345,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -2667,7 +2669,7 @@ spec:
key: server.api.content.types
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -2966,7 +2968,7 @@ spec:
key: controller.ignore.normalizer.jq.timeout
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -20849,6 +20849,8 @@ rules:
- appprojects
verbs:
- get
- list
- watch
- apiGroups:
- argoproj.io
resources:
@@ -21726,7 +21728,7 @@ spec:
key: applicationsetcontroller.enable.scm.providers
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -21849,7 +21851,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -21931,7 +21933,7 @@ spec:
key: notificationscontroller.selfservice.enabled
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -22031,7 +22033,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -22284,7 +22286,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -22336,7 +22338,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -22658,7 +22660,7 @@ spec:
key: server.api.content.types
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -22957,7 +22959,7 @@ spec:
key: controller.ignore.normalizer.jq.timeout
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -138,6 +138,8 @@ rules:
- appprojects
verbs:
- get
- list
- watch
- apiGroups:
- argoproj.io
resources:
@@ -803,7 +805,7 @@ spec:
key: applicationsetcontroller.enable.scm.providers
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
name: argocd-applicationset-controller
ports:
@@ -926,7 +928,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /shared/argocd-dex
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
name: copyutil
securityContext:
@@ -1008,7 +1010,7 @@ spec:
key: notificationscontroller.selfservice.enabled
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
livenessProbe:
tcpSocket:
@@ -1108,7 +1110,7 @@ spec:
- argocd
- admin
- redis-initial-password
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: IfNotPresent
name: secret-init
securityContext:
@@ -1361,7 +1363,7 @@ spec:
value: /helm-working-dir
- name: HELM_DATA_HOME
value: /helm-working-dir
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
livenessProbe:
failureThreshold: 3
@@ -1413,7 +1415,7 @@ spec:
- -n
- /usr/local/bin/argocd
- /var/run/argocd/argocd-cmp-server
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
name: copyutil
securityContext:
allowPrivilegeEscalation: false
@@ -1735,7 +1737,7 @@ spec:
key: server.api.content.types
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
livenessProbe:
httpGet:
@@ -2034,7 +2036,7 @@ spec:
key: controller.ignore.normalizer.jq.timeout
name: argocd-cmd-params-cm
optional: true
image: quay.io/argoproj/argocd:latest
image: quay.io/argoproj/argocd:v2.12.0-rc5
imagePullPolicy: Always
name: argocd-application-controller
ports:

View File

@@ -2241,7 +2241,6 @@ message SyncStatus {
optional string status = 1;
// ComparedTo contains information about what has been compared
// +patchStrategy=replace
optional ComparedTo comparedTo = 2;
// Revision contains information about the revision the comparison has been performed to

View File

@@ -7758,11 +7758,6 @@ func schema_pkg_apis_application_v1alpha1_SyncStatus(ref common.ReferenceCallbac
},
},
"comparedTo": {
VendorExtensible: spec.VendorExtensible{
Extensions: spec.Extensions{
"x-kubernetes-patch-strategy": "replace",
},
},
SchemaProps: spec.SchemaProps{
Description: "ComparedTo contains information about what has been compared",
Default: map[string]interface{}{},

View File

@@ -1519,8 +1519,7 @@ type SyncStatus struct {
// Status is the sync state of the comparison
Status SyncStatusCode `json:"status" protobuf:"bytes,1,opt,name=status,casttype=SyncStatusCode"`
// ComparedTo contains information about what has been compared
// +patchStrategy=replace
ComparedTo ComparedTo `json:"comparedTo,omitempty" protobuf:"bytes,2,opt,name=comparedTo" patchStrategy:"replace"`
ComparedTo ComparedTo `json:"comparedTo,omitempty" protobuf:"bytes,2,opt,name=comparedTo"`
// Revision contains information about the revision the comparison has been performed to
Revision string `json:"revision,omitempty" protobuf:"bytes,3,opt,name=revision"`
// Revisions contains information about the revisions of multiple sources the comparison has been performed to
@@ -2073,6 +2072,8 @@ var validActions = map[string]bool{
var validActionPatterns = []*regexp.Regexp{
regexp.MustCompile("action/.*"),
regexp.MustCompile("update/.*"),
regexp.MustCompile("delete/.*"),
}
func isValidAction(action string) bool {

View File

@@ -11,10 +11,7 @@ import (
"testing"
"time"
"github.com/argoproj/gitops-engine/pkg/diff"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/utils/ptr"
argocdcommon "github.com/argoproj/argo-cd/v2/common"
@@ -827,8 +824,12 @@ func TestAppProject_ValidPolicyRules(t *testing.T) {
"p, proj:my-proj:my-role, applications, *, my-proj/foo, allow",
"p, proj:my-proj:my-role, applications, create, my-proj/foo, allow",
"p, proj:my-proj:my-role, applications, update, my-proj/foo, allow",
"p, proj:my-proj:my-role, applications, update/*, my-proj/foo, allow",
"p, proj:my-proj:my-role, applications, update/*/Pod/*, my-proj/foo, allow",
"p, proj:my-proj:my-role, applications, sync, my-proj/foo, allow",
"p, proj:my-proj:my-role, applications, delete, my-proj/foo, allow",
"p, proj:my-proj:my-role, applications, delete/*, my-proj/foo, allow",
"p, proj:my-proj:my-role, applications, delete/*/Pod/*, my-proj/foo, allow",
"p, proj:my-proj:my-role, applications, action/*, my-proj/foo, allow",
"p, proj:my-proj:my-role, applications, action/apps/Deployment/restart, my-proj/foo, allow",
}
@@ -3089,7 +3090,7 @@ func TestOrphanedResourcesMonitorSettings_IsWarn(t *testing.T) {
assert.True(t, settings.IsWarn())
}
func Test_isValidPolicy(t *testing.T) {
func Test_isValidPolicyObject(t *testing.T) {
policyTests := []struct {
name string
policy string
@@ -3734,35 +3735,3 @@ func TestApplicationSpec_GetSourcePtrByIndex(t *testing.T) {
})
}
}
func TestHelmValuesObjectHasReplaceStrategy(t *testing.T) {
app := Application{
Status: ApplicationStatus{Sync: SyncStatus{ComparedTo: ComparedTo{
Source: ApplicationSource{
Helm: &ApplicationSourceHelm{
ValuesObject: &runtime.RawExtension{
Object: &unstructured.Unstructured{Object: map[string]interface{}{"key": []string{"value"}}},
},
},
},
}}},
}
appModified := Application{
Status: ApplicationStatus{Sync: SyncStatus{ComparedTo: ComparedTo{
Source: ApplicationSource{
Helm: &ApplicationSourceHelm{
ValuesObject: &runtime.RawExtension{
Object: &unstructured.Unstructured{Object: map[string]interface{}{"key": []string{"value-modified1"}}},
},
},
},
}}},
}
patch, _, err := diff.CreateTwoWayMergePatch(
app,
appModified, Application{})
require.NoError(t, err)
assert.Equal(t, `{"status":{"sync":{"comparedTo":{"destination":{},"source":{"helm":{"valuesObject":{"key":["value-modified1"]}},"repoURL":""}}}}}`, string(patch))
}

View File

@@ -2769,7 +2769,10 @@ func (s *Service) UpdateRevisionForPaths(_ context.Context, request *apiclient.U
return nil, status.Errorf(codes.Internal, "unable to get changed files for repo %s with revision %s: %v", repo.Repo, revision, err)
}
changed := apppathutil.AppFilesHaveChanged(refreshPaths, files)
changed := false
if len(files) != 0 {
changed = apppathutil.AppFilesHaveChanged(refreshPaths, files)
}
if !changed {
logCtx.Debugf("no changes found for application %s in repo %s from revision %s to revision %s", request.AppName, repo.Repo, syncedRevision, revision)

View File

@@ -1495,71 +1495,9 @@ func (s *Server) RevisionMetadata(ctx context.Context, q *application.RevisionMe
return nil, err
}
var versionId int64 = 0
if q.VersionId != nil {
versionId = int64(*q.VersionId)
}
var source *v1alpha1.ApplicationSource
// To support changes between single source and multi source revisions
// we have to calculate if the operation has to be done as multisource or not.
// There are 2 different scenarios, checking current revision and historic revision
// - Current revision (VersionId is nil or 0):
// - The application is multi source and required version too -> multi source
// - The application is single source and the required version too -> single source
// - The application is multi source and the required version is single source -> single source
// - The application is single source and the required version is multi source -> multi source
// - Historic revision:
// - The application is multi source and the previous one too -> multi source
// - The application is single source and the previous one too -> single source
// - The application is multi source and the previous one is single source -> multi source
// - The application is single source and the previous one is multi source -> single source
isRevisionMultiSource := a.Spec.HasMultipleSources()
emptyHistory := len(a.Status.History) == 0
if !emptyHistory {
for _, h := range a.Status.History {
if h.ID == versionId {
isRevisionMultiSource = len(h.Revisions) > 0
break
}
}
}
// If the historical data is empty (because the app hasn't been synced yet)
// we can use the source, if not (the app has been synced at least once)
// we have to use the history because sources can be added/removed
if emptyHistory {
if isRevisionMultiSource {
source = &a.Spec.Sources[*q.SourceIndex]
} else {
s := a.Spec.GetSource()
source = &s
}
} else {
// the source count can change during the time, we cannot just trust in .status.sync
// because if a source has been added/removed, the revisions there won't match
// as this is only used for the UI and not internally, we can use the historical data
// using the specific revisionId
for _, h := range a.Status.History {
if h.ID == versionId {
// The iteration values are assigned to the respective iteration variables as in an assignment statement.
// The iteration variables may be declared by the “range” clause using a form of short variable declaration (:=).
// In this case their types are set to the types of the respective iteration values and their scope is the block of the "for" statement;
// they are re-used in each iteration. If the iteration variables are declared outside the "for" statement,
// after execution their values will be those of the last iteration.
// https://golang.org/ref/spec#For_statements
h := h
if isRevisionMultiSource {
source = &h.Sources[*q.SourceIndex]
} else {
source = &h.Source
}
}
}
}
if source == nil {
return nil, fmt.Errorf("revision not found: %w", err)
source, err := getAppSourceBySourceIndexAndVersionId(a, q.SourceIndex, q.VersionId)
if err != nil {
return nil, fmt.Errorf("error getting app source by source index and version ID: %w", err)
}
repo, err := s.db.GetRepository(ctx, source.RepoURL, proj.Name)
@@ -1585,22 +1523,9 @@ func (s *Server) RevisionChartDetails(ctx context.Context, q *application.Revisi
return nil, err
}
var source *v1alpha1.ApplicationSource
if a.Spec.HasMultipleSources() {
// the source count can change during the time, we cannot just trust in .status.sync
// because if a source has been added/removed, the revisions there won't match
// as this is only used for the UI and not internally, we can use the historical data
// using the specific revisionId
for _, h := range a.Status.History {
if h.ID == int64(*q.VersionId) {
source = &h.Sources[*q.SourceIndex]
}
}
if source == nil {
return nil, fmt.Errorf("revision not found: %w", err)
}
} else {
source = a.Spec.Source
source, err := getAppSourceBySourceIndexAndVersionId(a, q.SourceIndex, q.VersionId)
if err != nil {
return nil, fmt.Errorf("error getting app source by source index and version ID: %w", err)
}
if source.Chart == "" {
@@ -1622,6 +1547,76 @@ func (s *Server) RevisionChartDetails(ctx context.Context, q *application.Revisi
})
}
// getAppSourceBySourceIndexAndVersionId returns the source for a specific source index and version ID. Source index and
// version ID are optional. If the source index is not specified, it defaults to 0. If the version ID is not specified,
// we use the source(s) currently configured for the app. If the version ID is specified, we find the source for that
// version ID. If the version ID is not found, we return an error. If the source index is out of bounds for whichever
// source we choose (configured sources or sources for a specific version), we return an error.
func getAppSourceBySourceIndexAndVersionId(a *appv1.Application, sourceIndexMaybe *int32, versionIdMaybe *int32) (appv1.ApplicationSource, error) {
// Start with all the app's configured sources.
sources := a.Spec.GetSources()
// If the user specified a version, get the sources for that version. If the version is not found, return an error.
if versionIdMaybe != nil {
versionId := int64(*versionIdMaybe)
var err error
sources, err = getSourcesByVersionId(a, versionId)
if err != nil {
return appv1.ApplicationSource{}, fmt.Errorf("error getting source by version ID: %w", err)
}
}
// Start by assuming we want the first source.
sourceIndex := 0
// If the user specified a source index, use that instead.
if sourceIndexMaybe != nil {
sourceIndex = int(*sourceIndexMaybe)
if sourceIndex >= len(sources) {
if len(sources) == 1 {
return appv1.ApplicationSource{}, fmt.Errorf("source index %d not found because there is only 1 source", sourceIndex)
}
return appv1.ApplicationSource{}, fmt.Errorf("source index %d not found because there are only %d sources", sourceIndex, len(sources))
}
}
source := sources[sourceIndex]
return source, nil
}
// getRevisionHistoryByVersionId returns the revision history for a specific version ID.
// If the version ID is not found, it returns an empty revision history and false.
func getRevisionHistoryByVersionId(histories v1alpha1.RevisionHistories, versionId int64) (appv1.RevisionHistory, bool) {
for _, h := range histories {
if h.ID == versionId {
return h, true
}
}
return appv1.RevisionHistory{}, false
}
// getSourcesByVersionId returns the sources for a specific version ID. If there is no history, it returns an error.
// If the version ID is not found, it returns an error. If the version ID is found, and there are multiple sources,
// it returns the sources for that version ID. If the version ID is found, and there is only one source, it returns
// a slice with just the single source.
func getSourcesByVersionId(a *appv1.Application, versionId int64) ([]appv1.ApplicationSource, error) {
if len(a.Status.History) == 0 {
return nil, fmt.Errorf("version ID %d not found because the app has no history", versionId)
}
h, ok := getRevisionHistoryByVersionId(a.Status.History, versionId)
if !ok {
return nil, fmt.Errorf("revision history not found for version ID %d", versionId)
}
if len(h.Sources) > 0 {
return h.Sources, nil
}
return []v1alpha1.ApplicationSource{h.Source}, nil
}
func isMatchingResource(q *application.ResourcesQuery, key kube.ResourceKey) bool {
return (q.GetName() == "" || q.GetName() == key.Name) &&
(q.GetNamespace() == "" || q.GetNamespace() == key.Namespace) &&

View File

@@ -10,6 +10,8 @@ import (
"testing"
"time"
"k8s.io/utils/pointer"
"k8s.io/apimachinery/pkg/labels"
"github.com/argoproj/gitops-engine/pkg/health"
@@ -3025,3 +3027,265 @@ func TestServer_ResolveSourceRevisions_SingleSource(t *testing.T) {
assert.Equal(t, ([]string)(nil), sourceRevisions)
assert.Equal(t, ([]string)(nil), displayRevisions)
}
func Test_RevisionMetadata(t *testing.T) {
singleSourceApp := newTestApp()
singleSourceApp.Name = "single-source-app"
singleSourceApp.Spec = appv1.ApplicationSpec{
Source: &appv1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
Path: "helm-guestbook",
TargetRevision: "HEAD",
},
}
multiSourceApp := newTestApp()
multiSourceApp.Name = "multi-source-app"
multiSourceApp.Spec = appv1.ApplicationSpec{
Sources: []appv1.ApplicationSource{
{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
Path: "helm-guestbook",
TargetRevision: "HEAD",
},
{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
Path: "kustomize-guestbook",
TargetRevision: "HEAD",
},
},
}
singleSourceHistory := []appv1.RevisionHistory{
{
ID: 1,
Source: singleSourceApp.Spec.GetSource(),
Revision: "a",
},
}
multiSourceHistory := []appv1.RevisionHistory{
{
ID: 1,
Sources: multiSourceApp.Spec.GetSources(),
Revisions: []string{"a", "b"},
},
}
testCases := []struct {
name string
multiSource bool
history *struct {
matchesSourceType bool
}
sourceIndex *int32
versionId *int32
expectErrorContains *string
}{
{
name: "single-source app without history, no source index, no version ID",
multiSource: false,
},
{
name: "single-source app without history, no source index, missing version ID",
multiSource: false,
versionId: pointer.Int32(999),
expectErrorContains: pointer.String("the app has no history"),
},
{
name: "single source app without history, present source index, no version ID",
multiSource: false,
sourceIndex: pointer.Int32(0),
},
{
name: "single source app without history, invalid source index, no version ID",
multiSource: false,
sourceIndex: pointer.Int32(999),
expectErrorContains: pointer.String("source index 999 not found"),
},
{
name: "single source app with matching history, no source index, no version ID",
multiSource: false,
history: &struct{ matchesSourceType bool }{true},
},
{
name: "single source app with matching history, no source index, missing version ID",
multiSource: false,
history: &struct{ matchesSourceType bool }{true},
versionId: pointer.Int32(999),
expectErrorContains: pointer.String("history not found for version ID 999"),
},
{
name: "single source app with matching history, no source index, present version ID",
multiSource: false,
history: &struct{ matchesSourceType bool }{true},
versionId: pointer.Int32(1),
},
{
name: "single source app with multi-source history, no source index, no version ID",
multiSource: false,
history: &struct{ matchesSourceType bool }{false},
},
{
name: "single source app with multi-source history, no source index, missing version ID",
multiSource: false,
history: &struct{ matchesSourceType bool }{false},
versionId: pointer.Int32(999),
expectErrorContains: pointer.String("history not found for version ID 999"),
},
{
name: "single source app with multi-source history, no source index, present version ID",
multiSource: false,
history: &struct{ matchesSourceType bool }{false},
versionId: pointer.Int32(1),
},
{
name: "single-source app with multi-source history, source index 1, no version ID",
multiSource: false,
sourceIndex: pointer.Int32(1),
history: &struct{ matchesSourceType bool }{false},
// Since the user requested source index 1, but no version ID, we'll get an error when looking at the live
// source, because the live source is single-source.
expectErrorContains: pointer.String("there is only 1 source"),
},
{
name: "single-source app with multi-source history, invalid source index, no version ID",
multiSource: false,
sourceIndex: pointer.Int32(999),
history: &struct{ matchesSourceType bool }{false},
expectErrorContains: pointer.String("source index 999 not found"),
},
{
name: "single-source app with multi-source history, valid source index, present version ID",
multiSource: false,
sourceIndex: pointer.Int32(1),
history: &struct{ matchesSourceType bool }{false},
versionId: pointer.Int32(1),
},
{
name: "multi-source app without history, no source index, no version ID",
multiSource: true,
},
{
name: "multi-source app without history, no source index, missing version ID",
multiSource: true,
versionId: pointer.Int32(999),
expectErrorContains: pointer.String("the app has no history"),
},
{
name: "multi-source app without history, present source index, no version ID",
multiSource: true,
sourceIndex: pointer.Int32(1),
},
{
name: "multi-source app without history, invalid source index, no version ID",
multiSource: true,
sourceIndex: pointer.Int32(999),
expectErrorContains: pointer.String("source index 999 not found"),
},
{
name: "multi-source app with matching history, no source index, no version ID",
multiSource: true,
history: &struct{ matchesSourceType bool }{true},
},
{
name: "multi-source app with matching history, no source index, missing version ID",
multiSource: true,
history: &struct{ matchesSourceType bool }{true},
versionId: pointer.Int32(999),
expectErrorContains: pointer.String("history not found for version ID 999"),
},
{
name: "multi-source app with matching history, no source index, present version ID",
multiSource: true,
history: &struct{ matchesSourceType bool }{true},
versionId: pointer.Int32(1),
},
{
name: "multi-source app with single-source history, no source index, no version ID",
multiSource: true,
history: &struct{ matchesSourceType bool }{false},
},
{
name: "multi-source app with single-source history, no source index, missing version ID",
multiSource: true,
history: &struct{ matchesSourceType bool }{false},
versionId: pointer.Int32(999),
expectErrorContains: pointer.String("history not found for version ID 999"),
},
{
name: "multi-source app with single-source history, no source index, present version ID",
multiSource: true,
history: &struct{ matchesSourceType bool }{false},
versionId: pointer.Int32(1),
},
{
name: "multi-source app with single-source history, source index 1, no version ID",
multiSource: true,
sourceIndex: pointer.Int32(1),
history: &struct{ matchesSourceType bool }{false},
},
{
name: "multi-source app with single-source history, invalid source index, no version ID",
multiSource: true,
sourceIndex: pointer.Int32(999),
history: &struct{ matchesSourceType bool }{false},
expectErrorContains: pointer.String("source index 999 not found"),
},
{
name: "multi-source app with single-source history, valid source index, present version ID",
multiSource: true,
sourceIndex: pointer.Int32(0),
history: &struct{ matchesSourceType bool }{false},
versionId: pointer.Int32(1),
},
{
name: "multi-source app with single-source history, source index 1, present version ID",
multiSource: true,
sourceIndex: pointer.Int32(1),
history: &struct{ matchesSourceType bool }{false},
versionId: pointer.Int32(1),
expectErrorContains: pointer.String("source index 1 not found"),
},
}
for _, tc := range testCases {
tcc := tc
t.Run(tcc.name, func(t *testing.T) {
app := singleSourceApp
if tcc.multiSource {
app = multiSourceApp
}
if tcc.history != nil {
if tcc.history.matchesSourceType {
if tcc.multiSource {
app.Status.History = multiSourceHistory
} else {
app.Status.History = singleSourceHistory
}
} else {
if tcc.multiSource {
app.Status.History = singleSourceHistory
} else {
app.Status.History = multiSourceHistory
}
}
}
s := newTestAppServer(t, app)
request := &application.RevisionMetadataQuery{
Name: pointer.String(app.Name),
Revision: pointer.String("HEAD"),
SourceIndex: tcc.sourceIndex,
VersionId: tcc.versionId,
}
_, err := s.RevisionMetadata(context.Background(), request)
if tcc.expectErrorContains != nil {
require.ErrorContains(t, err, *tcc.expectErrorContains)
} else {
require.NoError(t, err)
}
})
}
}

View File

@@ -229,7 +229,7 @@ func (s *terminalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fieldLog.Info("terminal session starting")
session, err := newTerminalSession(w, r, nil, s.sessionManager)
session, err := newTerminalSession(ctx, w, r, nil, s.sessionManager, appRBACName, s.enf)
if err != nil {
http.Error(w, "Failed to start terminal session", http.StatusBadRequest)
return

View File

@@ -1,12 +1,16 @@
package application
import (
"context"
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
"github.com/argoproj/argo-cd/v2/server/rbacpolicy"
"github.com/argoproj/argo-cd/v2/util/rbac"
"github.com/argoproj/argo-cd/v2/common"
httputil "github.com/argoproj/argo-cd/v2/util/http"
util_session "github.com/argoproj/argo-cd/v2/util/session"
@@ -32,6 +36,7 @@ var upgrader = func() websocket.Upgrader {
// terminalSession implements PtyHandler
type terminalSession struct {
ctx context.Context
wsConn *websocket.Conn
sizeChan chan remotecommand.TerminalSize
doneChan chan struct{}
@@ -40,6 +45,8 @@ type terminalSession struct {
writeLock sync.Mutex
sessionManager *util_session.SessionManager
token *string
appRBACName string
enf *rbac.Enforcer
}
// getToken get auth token from web socket request
@@ -49,7 +56,7 @@ func getToken(r *http.Request) (string, error) {
}
// newTerminalSession create terminalSession
func newTerminalSession(w http.ResponseWriter, r *http.Request, responseHeader http.Header, sessionManager *util_session.SessionManager) (*terminalSession, error) {
func newTerminalSession(ctx context.Context, w http.ResponseWriter, r *http.Request, responseHeader http.Header, sessionManager *util_session.SessionManager, appRBACName string, enf *rbac.Enforcer) (*terminalSession, error) {
token, err := getToken(r)
if err != nil {
return nil, err
@@ -60,12 +67,15 @@ func newTerminalSession(w http.ResponseWriter, r *http.Request, responseHeader h
return nil, err
}
session := &terminalSession{
ctx: ctx,
wsConn: conn,
tty: true,
sizeChan: make(chan remotecommand.TerminalSize),
doneChan: make(chan struct{}),
sessionManager: sessionManager,
token: &token,
appRBACName: appRBACName,
enf: enf,
}
return session, nil
}
@@ -126,6 +136,29 @@ func (t *terminalSession) reconnect() (int, error) {
return 0, nil
}
func (t *terminalSession) validatePermissions(p []byte) (int, error) {
permissionDeniedMessage, _ := json.Marshal(TerminalMessage{
Operation: "stdout",
Data: "Permission denied",
})
if err := t.enf.EnforceErr(t.ctx.Value("claims"), rbacpolicy.ResourceApplications, rbacpolicy.ActionGet, t.appRBACName); err != nil {
err = t.wsConn.WriteMessage(websocket.TextMessage, permissionDeniedMessage)
if err != nil {
log.Errorf("permission denied message err: %v", err)
}
return copy(p, EndOfTransmission), permissionDeniedErr
}
if err := t.enf.EnforceErr(t.ctx.Value("claims"), rbacpolicy.ResourceExec, rbacpolicy.ActionCreate, t.appRBACName); err != nil {
err = t.wsConn.WriteMessage(websocket.TextMessage, permissionDeniedMessage)
if err != nil {
log.Errorf("permission denied message err: %v", err)
}
return copy(p, EndOfTransmission), permissionDeniedErr
}
return 0, nil
}
// Read called in a loop from remotecommand as long as the process is running
func (t *terminalSession) Read(p []byte) (int, error) {
// check if token still valid
@@ -136,6 +169,12 @@ func (t *terminalSession) Read(p []byte) (int, error) {
return t.reconnect()
}
// validate permissions
code, err := t.validatePermissions(p)
if err != nil {
return code, err
}
t.readLock.Lock()
_, message, err := t.wsConn.ReadMessage()
t.readLock.Unlock()

View File

@@ -1,25 +1,65 @@
package application
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
"github.com/argoproj/argo-cd/v2/common"
"github.com/argoproj/argo-cd/v2/util/assets"
"github.com/argoproj/argo-cd/v2/util/rbac"
"github.com/golang-jwt/jwt/v4"
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func reconnect(w http.ResponseWriter, r *http.Request) {
func newTestTerminalSession(w http.ResponseWriter, r *http.Request) terminalSession {
upgrader := websocket.Upgrader{}
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
return terminalSession{}
}
ts := terminalSession{wsConn: c}
return terminalSession{wsConn: c}
}
func newEnforcer() *rbac.Enforcer {
additionalConfig := make(map[string]string, 0)
kubeclientset := fake.NewSimpleClientset(&v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: testNamespace,
Name: "argocd-cm",
Labels: map[string]string{
"app.kubernetes.io/part-of": "argocd",
},
},
Data: additionalConfig,
}, &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "argocd-secret",
Namespace: testNamespace,
},
Data: map[string][]byte{
"admin.password": []byte("test"),
"server.secretkey": []byte("test"),
},
})
enforcer := rbac.NewEnforcer(kubeclientset, testNamespace, common.ArgoCDRBACConfigMapName, nil)
return enforcer
}
func reconnect(w http.ResponseWriter, r *http.Request) {
ts := newTestTerminalSession(w, r)
_, _ = ts.reconnect()
}
@@ -44,3 +84,71 @@ func TestReconnect(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, ReconnectMessage, message.Data)
}
func TestValidateWithAdminPermissions(t *testing.T) {
validate := func(w http.ResponseWriter, r *http.Request) {
enf := newEnforcer()
_ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV)
enf.SetDefaultRole("role:admin")
enf.SetClaimsEnforcerFunc(func(claims jwt.Claims, rvals ...interface{}) bool {
return true
})
ts := newTestTerminalSession(w, r)
ts.enf = enf
ts.appRBACName = "test"
// nolint:staticcheck
ts.ctx = context.WithValue(context.Background(), "claims", &jwt.MapClaims{"groups": []string{"admin"}})
_, err := ts.validatePermissions([]byte{})
require.NoError(t, err)
}
s := httptest.NewServer(http.HandlerFunc(validate))
defer s.Close()
u := "ws" + strings.TrimPrefix(s.URL, "http")
// Connect to the server
ws, _, err := websocket.DefaultDialer.Dial(u, nil)
require.NoError(t, err)
defer ws.Close()
}
func TestValidateWithoutPermissions(t *testing.T) {
validate := func(w http.ResponseWriter, r *http.Request) {
enf := newEnforcer()
_ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV)
enf.SetDefaultRole("role:test")
enf.SetClaimsEnforcerFunc(func(claims jwt.Claims, rvals ...interface{}) bool {
return false
})
ts := newTestTerminalSession(w, r)
ts.enf = enf
ts.appRBACName = "test"
// nolint:staticcheck
ts.ctx = context.WithValue(context.Background(), "claims", &jwt.MapClaims{"groups": []string{"test"}})
_, err := ts.validatePermissions([]byte{})
require.Error(t, err)
assert.Equal(t, permissionDeniedErr.Error(), err.Error())
}
s := httptest.NewServer(http.HandlerFunc(validate))
defer s.Close()
u := "ws" + strings.TrimPrefix(s.URL, "http")
// Connect to the server
ws, _, err := websocket.DefaultDialer.Dial(u, nil)
require.NoError(t, err)
defer ws.Close()
_, p, _ := ws.ReadMessage()
var message TerminalMessage
err = json.Unmarshal(p, &message)
require.NoError(t, err)
assert.Equal(t, "Permission denied", message.Data)
}

View File

@@ -1049,7 +1049,7 @@ func (a *ArgoCDServer) newHTTPServer(ctx context.Context, port int, grpcWebHandl
// Webhook handler for git events (Note: cache timeouts are hardcoded because API server does not write to cache and not really using them)
argoDB := db.NewDB(a.Namespace, a.settingsMgr, a.KubeClientset)
acdWebhookHandler := webhook.NewHandler(a.Namespace, a.ArgoCDServerOpts.ApplicationNamespaces, a.AppClientset, a.settings, a.settingsMgr, a.RepoServerCache, a.Cache, argoDB)
acdWebhookHandler := webhook.NewHandler(a.Namespace, a.ArgoCDServerOpts.ApplicationNamespaces, a.AppClientset, a.settings, a.settingsMgr, a.RepoServerCache, a.Cache, argoDB, a.settingsMgr.GetMaxWebhookPayloadSize())
mux.HandleFunc("/api/webhook", acdWebhookHandler.Handler)

View File

@@ -522,101 +522,6 @@ func TestSimpleListGeneratorGoTemplate(t *testing.T) {
Delete().Then().Expect(ApplicationsDoNotExist([]argov1alpha1.Application{*expectedAppNewMetadata}))
}
func TestCreateApplicationDespiteParamsError(t *testing.T) {
expectedErrorMessage := `failed to execute go template {{.cluster}}-guestbook: template: :1:2: executing "" at <.cluster>: map has no entry for key "cluster"`
expectedConditionsParamsError := []v1alpha1.ApplicationSetCondition{
{
Type: v1alpha1.ApplicationSetConditionErrorOccurred,
Status: v1alpha1.ApplicationSetConditionStatusTrue,
Message: expectedErrorMessage,
Reason: v1alpha1.ApplicationSetReasonRenderTemplateParamsError,
},
{
Type: v1alpha1.ApplicationSetConditionParametersGenerated,
Status: v1alpha1.ApplicationSetConditionStatusFalse,
Message: expectedErrorMessage,
Reason: v1alpha1.ApplicationSetReasonErrorOccurred,
},
{
Type: v1alpha1.ApplicationSetConditionResourcesUpToDate,
Status: v1alpha1.ApplicationSetConditionStatusFalse,
Message: expectedErrorMessage,
Reason: v1alpha1.ApplicationSetReasonRenderTemplateParamsError,
},
}
expectedApp := argov1alpha1.Application{
TypeMeta: metav1.TypeMeta{
Kind: application.ApplicationKind,
APIVersion: "argoproj.io/v1alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "my-cluster-guestbook",
Namespace: fixture.TestNamespace(),
Finalizers: []string{"resources-finalizer.argocd.argoproj.io"},
},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: &argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Server: "https://kubernetes.default.svc",
Namespace: "guestbook",
},
},
}
Given(t).
// Create a ListGenerator-based ApplicationSet
When().Create(v1alpha1.ApplicationSet{
ObjectMeta: metav1.ObjectMeta{
Name: "simple-list-generator",
},
Spec: v1alpha1.ApplicationSetSpec{
GoTemplate: true,
GoTemplateOptions: []string{"missingkey=error"},
Template: v1alpha1.ApplicationSetTemplate{
ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{.cluster}}-guestbook"},
Spec: argov1alpha1.ApplicationSpec{
Project: "default",
Source: &argov1alpha1.ApplicationSource{
RepoURL: "https://github.com/argoproj/argocd-example-apps.git",
TargetRevision: "HEAD",
Path: "guestbook",
},
Destination: argov1alpha1.ApplicationDestination{
Server: "{{.url}}",
Namespace: "guestbook",
},
},
},
Generators: []v1alpha1.ApplicationSetGenerator{
{
List: &v1alpha1.ListGenerator{
Elements: []apiextensionsv1.JSON{
{
Raw: []byte(`{"cluster": "my-cluster","url": "https://kubernetes.default.svc"}`),
},
{
Raw: []byte(`{"invalidCluster": "invalid-cluster","url": "https://kubernetes.default.svc"}`),
},
},
},
},
},
},
}).Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedApp})).
// verify the ApplicationSet status conditions were set correctly
Expect(ApplicationSetHasConditions("simple-list-generator", expectedConditionsParamsError)).
// Delete the ApplicationSet, and verify it deletes the Applications
When().
Delete().Then().Expect(ApplicationsDoNotExist([]argov1alpha1.Application{expectedApp}))
}
func TestRenderHelmValuesObject(t *testing.T) {
expectedApp := argov1alpha1.Application{
TypeMeta: metav1.TypeMeta{

View File

@@ -153,7 +153,9 @@ export const ApplicationDeploymentHistory = ({
</React.Fragment>
))
)
) : null}
) : (
<p>Click to see source details.</p>
)}
</div>
</div>
))}

View File

@@ -192,7 +192,14 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
)
);
const getContentForChart = (aRevision: string, aSourceIndex: number, aVersionId: number, indx: number, aSource: models.ApplicationSource, sourceHeader?: JSX.Element) => {
const getContentForChart = (
aRevision: string,
aSourceIndex: number | null,
aVersionId: number | null,
indx: number,
aSource: models.ApplicationSource,
sourceHeader?: JSX.Element
) => {
const showChartNonMetadataInfo = (aRevision: string, aRepoUrl: string) => {
return (
<>
@@ -366,9 +373,9 @@ export class ApplicationDetails extends React.Component<RouteComponentProps<{app
return <>{cont}</>;
} else if (application.spec.source) {
if (source.chart) {
cont.push(getContentForChart(revision, 0, 0, 0, source));
cont.push(getContentForChart(revision, null, null, 0, source));
} else {
cont.push(getContentForNonChart(revision, 0, getAppCurrentVersion(application), 0, source));
cont.push(getContentForNonChart(revision, null, getAppCurrentVersion(application), 0, source));
}
return <>{cont}</>;
} else {

View File

@@ -247,7 +247,7 @@ export const ApplicationParameters = (props: {
</div>
</React.Fragment>
)}
<DataLoader input={app} load={application => getSourceFromSources(application, index)}>
<DataLoader input={app.spec.sources[index]} load={src => getSourceFromAppSources(src, app.metadata.name, app.spec.project, index, 0)}>
{(details: models.RepoAppDetails) => getEditablePanelForOneSource(details, index, source)}
</DataLoader>
</div>
@@ -986,17 +986,12 @@ function gatherDetails(
}
// For Sources field. Get one source with index i from the list
async function getSourceFromSources(app: models.Application, i: number) {
const sources: models.ApplicationSource[] = app.spec.sources;
if (sources && i < sources.length) {
const aSource = sources[i];
const repoDetail = await services.repos.appDetails(aSource, app.metadata.name, app.spec.project, i, 0).catch(() => ({
type: 'Directory' as models.AppSourceType,
path: aSource.path
}));
return repoDetail;
}
return null;
async function getSourceFromAppSources(aSource: models.ApplicationSource, name: string, project: string, index: number, version: number) {
const repoDetail = await services.repos.appDetails(aSource, name, project, index, version).catch(() => ({
type: 'Directory' as models.AppSourceType,
path: aSource.path
}));
return repoDetail;
}
// Delete when source field is removed

View File

@@ -1131,9 +1131,9 @@ export function getAppDefaultOperationSyncRevision(app?: appModels.Application)
// getAppCurrentVersion gets the first app revisions from `status.sync.revisions` or, if that list is missing or empty, the `revision`
// field.
export function getAppCurrentVersion(app?: appModels.Application) {
if (!app || !app.status || !app.status.history) {
return 0;
export function getAppCurrentVersion(app?: appModels.Application): number | null {
if (!app || !app.status || !app.status.history || app.status.history.length === 0) {
return null;
}
return app.status.history[app.status.history.length - 1].id;
}

View File

@@ -857,7 +857,7 @@ export class ReposList extends React.Component<
const confirmed = await this.appContext.apis.popup.confirm('Disconnect repository', `Are you sure you want to disconnect '${repo}'?`);
if (confirmed) {
try {
await services.repos.delete(repo, project);
await services.repos.delete(repo, project || '');
this.repoLoader.reload();
} catch (e) {
this.appContext.apis.notifications.show({

View File

@@ -53,22 +53,26 @@ export class ApplicationsService {
.then(res => res.body as models.ApplicationSyncWindowState);
}
public revisionMetadata(name: string, appNamespace: string, revision: string, sourceIndex: number, versionId: number): Promise<models.RevisionMetadata> {
return requests
.get(`/applications/${name}/revisions/${revision || 'HEAD'}/metadata`)
.query({appNamespace})
.query({sourceIndex})
.query({versionId})
.then(res => res.body as models.RevisionMetadata);
public revisionMetadata(name: string, appNamespace: string, revision: string, sourceIndex: number | null, versionId: number | null): Promise<models.RevisionMetadata> {
let r = requests.get(`/applications/${name}/revisions/${revision || 'HEAD'}/metadata`).query({appNamespace});
if (sourceIndex !== null) {
r = r.query({sourceIndex});
}
if (versionId !== null) {
r = r.query({versionId});
}
return r.then(res => res.body as models.RevisionMetadata);
}
public revisionChartDetails(name: string, appNamespace: string, revision: string, sourceIndex: number, versionId: number): Promise<models.ChartDetails> {
return requests
.get(`/applications/${name}/revisions/${revision || 'HEAD'}/chartdetails`)
.query({appNamespace})
.query({sourceIndex})
.query({versionId})
.then(res => res.body as models.ChartDetails);
let r = requests.get(`/applications/${name}/revisions/${revision || 'HEAD'}/chartdetails`).query({appNamespace});
if (sourceIndex !== null) {
r = r.query({sourceIndex});
}
if (versionId !== null) {
r = r.query({versionId});
}
return r.then(res => res.body as models.ChartDetails);
}
public resourceTree(name: string, appNamespace: string): Promise<models.ApplicationTree> {

View File

@@ -112,12 +112,12 @@ func GetAppRefreshPaths(app *v1alpha1.Application) []string {
}
// AppFilesHaveChanged returns true if any of the changed files are under the given refresh paths
// If refreshPaths is empty, it will always return true
// If refreshPaths or changedFiles are empty, it will always return true
func AppFilesHaveChanged(refreshPaths []string, changedFiles []string) bool {
// empty slice means there was no changes to any files
// so we should not refresh
// an empty slice of changed files means that the payload didn't include a list
// of changed files and we have to assume that a refresh is required
if len(changedFiles) == 0 {
return false
return true
}
if len(refreshPaths) == 0 {

View File

@@ -134,7 +134,7 @@ func Test_AppFilesHaveChanged(t *testing.T) {
changeExpected bool
}{
{"default no path", &v1alpha1.Application{}, []string{"README.md"}, true},
{"no files changed", getApp(".", "source/path"), []string{}, false},
{"no files changed", getApp(".", "source/path"), []string{}, true},
{"relative path - matching", getApp(".", "source/path"), []string{"source/path/my-deployment.yaml"}, true},
{"relative path, multi source - matching #1", getMultiSourceApp(".", "source/path", "other/path"), []string{"source/path/my-deployment.yaml"}, true},
{"relative path, multi source - matching #2", getMultiSourceApp(".", "other/path", "source/path"), []string{"source/path/my-deployment.yaml"}, true},

View File

@@ -149,6 +149,7 @@ func (rt *resourceTracking) SetAppInstance(un *unstructured.Unstructured, key, v
if err != nil {
return fmt.Errorf("failed to set app instance label: %w", err)
}
return nil
case TrackingMethodAnnotation:
return setAppInstanceAnnotation()
case TrackingMethodAnnotationAndLabel:
@@ -171,13 +172,14 @@ func (rt *resourceTracking) SetAppInstance(un *unstructured.Unstructured, key, v
if err != nil {
return fmt.Errorf("failed to set app instance label: %w", err)
}
return nil
default:
err := argokube.SetAppInstanceLabel(un, key, val)
if err != nil {
return fmt.Errorf("failed to set app instance label: %w", err)
}
return nil
}
return nil
}
// BuildAppInstanceValue build resource tracking id in format <application-name>;<group>/<kind>/<namespace>/<name>

View File

@@ -4,7 +4,11 @@ import (
"fmt"
"regexp"
"github.com/argoproj/gitops-engine/pkg/utils/kube"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/argoproj/argo-cd/v2/common"
)
var resourceNamePattern = regexp.MustCompile("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")
@@ -15,6 +19,7 @@ func IsValidResourceName(name string) bool {
}
// SetAppInstanceLabel the recommended app.kubernetes.io/instance label against an unstructured object
// Uses the legacy labeling if environment variable is set
func SetAppInstanceLabel(target *unstructured.Unstructured, key, val string) error {
labels, _, err := nestedNullableStringMap(target.Object, "metadata", "labels")
if err != nil {
@@ -25,10 +30,75 @@ func SetAppInstanceLabel(target *unstructured.Unstructured, key, val string) err
}
labels[key] = val
target.SetLabels(labels)
if key != common.LabelKeyLegacyApplicationName {
// we no longer label the pod template sub resources in v0.11
return nil
}
gvk := schema.FromAPIVersionAndKind(target.GetAPIVersion(), target.GetKind())
// special case for deployment and job types: make sure that derived replicaset, and pod has
// the application label
switch gvk.Group {
case "apps", "extensions":
switch gvk.Kind {
case kube.DeploymentKind, kube.ReplicaSetKind, kube.StatefulSetKind, kube.DaemonSetKind:
templateLabels, ok, err := unstructured.NestedMap(target.UnstructuredContent(), "spec", "template", "metadata", "labels")
if err != nil {
return err
}
if !ok || templateLabels == nil {
templateLabels = make(map[string]interface{})
}
templateLabels[key] = val
err = unstructured.SetNestedMap(target.UnstructuredContent(), templateLabels, "spec", "template", "metadata", "labels")
if err != nil {
return err
}
// The following is a workaround for issue #335. In API version extensions/v1beta1 or
// apps/v1beta1, if a spec omits spec.selector then k8s will default the
// spec.selector.matchLabels to match spec.template.metadata.labels. This means Argo CD
// labels can potentially make their way into spec.selector.matchLabels, which is a bad
// thing. The following logic prevents this behavior.
switch target.GetAPIVersion() {
case "apps/v1beta1", "extensions/v1beta1":
selector, _, err := unstructured.NestedMap(target.UnstructuredContent(), "spec", "selector")
if err != nil {
return err
}
if len(selector) == 0 {
// If we get here, user did not set spec.selector in their manifest. We do not want
// our Argo CD labels to get defaulted by kubernetes, so we explicitly set the labels
// for them (minus the Argo CD labels).
delete(templateLabels, key)
err = unstructured.SetNestedMap(target.UnstructuredContent(), templateLabels, "spec", "selector", "matchLabels")
if err != nil {
return err
}
}
}
}
case "batch":
switch gvk.Kind {
case kube.JobKind:
templateLabels, ok, err := unstructured.NestedMap(target.UnstructuredContent(), "spec", "template", "metadata", "labels")
if err != nil {
return err
}
if !ok || templateLabels == nil {
templateLabels = make(map[string]interface{})
}
templateLabels[key] = val
err = unstructured.SetNestedMap(target.UnstructuredContent(), templateLabels, "spec", "template", "metadata", "labels")
if err != nil {
return err
}
}
}
return nil
}
// SetAppInstanceAnnotation the recommended app.kubernetes.io/instance annotation against an unstructured object
// Uses the legacy labeling if environment variable is set
func SetAppInstanceAnnotation(target *unstructured.Unstructured, key, val string) error {
annotations, _, err := nestedNullableStringMap(target.Object, "metadata", "annotations")
if err != nil {

View File

@@ -84,6 +84,56 @@ func TestSetLabels(t *testing.T) {
}
}
func TestSetLegacyLabels(t *testing.T) {
for _, yamlStr := range []string{depWithoutSelector, depWithSelector} {
var obj unstructured.Unstructured
err := yaml.Unmarshal([]byte(yamlStr), &obj)
require.NoError(t, err)
err = SetAppInstanceLabel(&obj, common.LabelKeyLegacyApplicationName, "my-app")
require.NoError(t, err)
manifestBytes, err := json.MarshalIndent(obj.Object, "", " ")
require.NoError(t, err)
log.Println(string(manifestBytes))
var depV1Beta1 extv1beta1.Deployment
err = json.Unmarshal(manifestBytes, &depV1Beta1)
require.NoError(t, err)
assert.Len(t, depV1Beta1.Spec.Selector.MatchLabels, 1)
assert.Equal(t, "nginx", depV1Beta1.Spec.Selector.MatchLabels["app"])
assert.Len(t, depV1Beta1.Spec.Template.Labels, 2)
assert.Equal(t, "nginx", depV1Beta1.Spec.Template.Labels["app"])
assert.Equal(t, "my-app", depV1Beta1.Spec.Template.Labels[common.LabelKeyLegacyApplicationName])
}
}
func TestSetLegacyJobLabel(t *testing.T) {
yamlBytes, err := os.ReadFile("testdata/job.yaml")
require.NoError(t, err)
var obj unstructured.Unstructured
err = yaml.Unmarshal(yamlBytes, &obj)
require.NoError(t, err)
err = SetAppInstanceLabel(&obj, common.LabelKeyLegacyApplicationName, "my-app")
require.NoError(t, err)
manifestBytes, err := json.MarshalIndent(obj.Object, "", " ")
require.NoError(t, err)
log.Println(string(manifestBytes))
job := unstructured.Unstructured{}
err = json.Unmarshal(manifestBytes, &job)
require.NoError(t, err)
labels := job.GetLabels()
assert.Equal(t, "my-app", labels[common.LabelKeyLegacyApplicationName])
templateLabels, ok, err := unstructured.NestedMap(job.UnstructuredContent(), "spec", "template", "metadata", "labels")
assert.True(t, ok)
require.NoError(t, err)
assert.Equal(t, "my-app", templateLabels[common.LabelKeyLegacyApplicationName])
}
func TestSetSvcLabel(t *testing.T) {
yamlBytes, err := os.ReadFile("testdata/svc.yaml")
require.NoError(t, err)

View File

@@ -434,6 +434,8 @@ const (
settingsWebhookAzureDevOpsUsernameKey = "webhook.azuredevops.username"
// settingsWebhookAzureDevOpsPasswordKey is the key for Azure DevOps webhook password
settingsWebhookAzureDevOpsPasswordKey = "webhook.azuredevops.password"
// settingsWebhookMaxPayloadSize is the key for the maximum payload size for webhooks in MB
settingsWebhookMaxPayloadSizeMB = "webhook.maxPayloadSizeMB"
// settingsApplicationInstanceLabelKey is the key to configure injected app instance label key
settingsApplicationInstanceLabelKey = "application.instanceLabelKey"
// settingsResourceTrackingMethodKey is the key to configure tracking method for application resources
@@ -517,6 +519,11 @@ const (
RespectRBACValueNormal = "normal"
)
const (
// default max webhook payload size is 1GB
defaultMaxWebhookPayloadSize = int64(1) * 1024 * 1024 * 1024
)
var sourceTypeToEnableGenerationKey = map[v1alpha1.ApplicationSourceType]string{
v1alpha1.ApplicationSourceTypeKustomize: "kustomize.enable",
v1alpha1.ApplicationSourceTypeHelm: "helm.enable",
@@ -761,11 +768,6 @@ func (mgr *SettingsManager) GetAppInstanceLabelKey() (string, error) {
if label == "" {
return common.LabelKeyAppInstance, nil
}
// return new label key if user is still using legacy key
if label == common.LabelKeyLegacyApplicationName {
log.Warnf("deprecated legacy application instance tracking key(%v) is present in configmap, new key(%v) will be used automatically", common.LabelKeyLegacyApplicationName, common.LabelKeyAppInstance)
return common.LabelKeyAppInstance, nil
}
return label, nil
}
@@ -2257,3 +2259,22 @@ func (mgr *SettingsManager) GetExcludeEventLabelKeys() []string {
}
return labelKeys
}
func (mgr *SettingsManager) GetMaxWebhookPayloadSize() int64 {
argoCDCM, err := mgr.getConfigMap()
if err != nil {
return defaultMaxWebhookPayloadSize
}
if argoCDCM.Data[settingsWebhookMaxPayloadSizeMB] == "" {
return defaultMaxWebhookPayloadSize
}
maxPayloadSizeMB, err := strconv.ParseInt(argoCDCM.Data[settingsWebhookMaxPayloadSizeMB], 10, 64)
if err != nil {
log.Warnf("Failed to parse '%s' key: %v", settingsWebhookMaxPayloadSizeMB, err)
return defaultMaxWebhookPayloadSize
}
return maxPayloadSizeMB * 1024 * 1024
}

View File

@@ -61,9 +61,10 @@ type ArgoCDWebhookHandler struct {
azuredevopsAuthHandler func(r *http.Request) error
gogs *gogs.Webhook
settingsSrc settingsSource
maxWebhookPayloadSizeB int64
}
func NewHandler(namespace string, applicationNamespaces []string, appClientset appclientset.Interface, set *settings.ArgoCDSettings, settingsSrc settingsSource, repoCache *cache.Cache, serverCache *servercache.Cache, argoDB db.ArgoDB) *ArgoCDWebhookHandler {
func NewHandler(namespace string, applicationNamespaces []string, appClientset appclientset.Interface, set *settings.ArgoCDSettings, settingsSrc settingsSource, repoCache *cache.Cache, serverCache *servercache.Cache, argoDB db.ArgoDB, maxWebhookPayloadSizeB int64) *ArgoCDWebhookHandler {
githubWebhook, err := github.New(github.Options.Secret(set.WebhookGitHubSecret))
if err != nil {
log.Warnf("Unable to init the GitHub webhook")
@@ -113,6 +114,7 @@ func NewHandler(namespace string, applicationNamespaces []string, appClientset a
repoCache: repoCache,
serverCache: serverCache,
db: argoDB,
maxWebhookPayloadSizeB: maxWebhookPayloadSizeB,
}
return &acdWebhook
@@ -393,6 +395,8 @@ func (a *ArgoCDWebhookHandler) Handler(w http.ResponseWriter, r *http.Request) {
var payload interface{}
var err error
r.Body = http.MaxBytesReader(w, r.Body, a.maxWebhookPayloadSizeB)
switch {
case r.Header.Get("X-Vss-Activityid") != "":
if err = a.azuredevopsAuthHandler(r); err != nil {
@@ -435,6 +439,14 @@ func (a *ArgoCDWebhookHandler) Handler(w http.ResponseWriter, r *http.Request) {
}
if err != nil {
// If the error is due to a large payload, return a more user-friendly error message
if err.Error() == "error parsing payload" {
msg := fmt.Sprintf("Webhook processing failed: The payload is either too large or corrupted. Please check the payload size (must be under %v MB) and ensure it is valid JSON", a.maxWebhookPayloadSizeB/1024/1024)
log.WithField(common.SecurityField, common.SecurityHigh).Warn(msg)
http.Error(w, msg, http.StatusBadRequest)
return
}
log.Infof("Webhook processing failed: %s", err)
status := http.StatusBadRequest
if r.Method != http.MethodPost {

View File

@@ -56,6 +56,11 @@ type reactorDef struct {
}
func NewMockHandler(reactor *reactorDef, applicationNamespaces []string, objects ...runtime.Object) *ArgoCDWebhookHandler {
defaultMaxPayloadSize := int64(1) * 1024 * 1024 * 1024
return NewMockHandlerWithPayloadLimit(reactor, applicationNamespaces, defaultMaxPayloadSize, objects...)
}
func NewMockHandlerWithPayloadLimit(reactor *reactorDef, applicationNamespaces []string, maxPayloadSize int64, objects ...runtime.Object) *ArgoCDWebhookHandler {
appClientset := appclientset.NewSimpleClientset(objects...)
if reactor != nil {
defaultReactor := appClientset.ReactionChain[0]
@@ -72,7 +77,7 @@ func NewMockHandler(reactor *reactorDef, applicationNamespaces []string, objects
1*time.Minute,
1*time.Minute,
10*time.Second,
), servercache.NewCache(appstate.NewCache(cacheClient, time.Minute), time.Minute, time.Minute, time.Minute), &mocks.ArgoDB{})
), servercache.NewCache(appstate.NewCache(cacheClient, time.Minute), time.Minute, time.Minute, time.Minute), &mocks.ArgoDB{}, maxPayloadSize)
}
func TestGitHubCommitEvent(t *testing.T) {
@@ -393,7 +398,7 @@ func TestInvalidEvent(t *testing.T) {
w := httptest.NewRecorder()
h.Handler(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
expectedLogResult := "Webhook processing failed: error parsing payload"
expectedLogResult := "Webhook processing failed: The payload is either too large or corrupted. Please check the payload size (must be under 1024 MB) and ensure it is valid JSON"
assert.Equal(t, expectedLogResult, hook.LastEntry().Message)
assert.Equal(t, expectedLogResult+"\n", w.Body.String())
hook.Reset()
@@ -604,3 +609,20 @@ func Test_getWebUrlRegex(t *testing.T) {
})
}
}
func TestGitHubCommitEventMaxPayloadSize(t *testing.T) {
hook := test.NewGlobal()
maxPayloadSize := int64(100)
h := NewMockHandlerWithPayloadLimit(nil, []string{}, maxPayloadSize)
req := httptest.NewRequest(http.MethodPost, "/api/webhook", nil)
req.Header.Set("X-GitHub-Event", "push")
eventJSON, err := os.ReadFile("testdata/github-commit-event.json")
require.NoError(t, err)
req.Body = io.NopCloser(bytes.NewReader(eventJSON))
w := httptest.NewRecorder()
h.Handler(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
expectedLogResult := "Webhook processing failed: The payload is either too large or corrupted. Please check the payload size (must be under 0 MB) and ensure it is valid JSON"
assert.Equal(t, expectedLogResult, hook.LastEntry().Message)
hook.Reset()
}