mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-02-20 09:38:49 +01:00
Compare commits
25 Commits
master
...
v2.12.0-rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f2ae0dd46 | ||
|
|
3e31ce9470 | ||
|
|
dee59f3002 | ||
|
|
d6c37aab88 | ||
|
|
eaa1972e69 | ||
|
|
004cabba47 | ||
|
|
d9263101fc | ||
|
|
cec8504df2 | ||
|
|
0704aa6506 | ||
|
|
511d6e371c | ||
|
|
065f8494cf | ||
|
|
beacacc43d | ||
|
|
81d454215d | ||
|
|
1237d4ea17 | ||
|
|
b211d3e038 | ||
|
|
50c32b53f0 | ||
|
|
444d332d0a | ||
|
|
573c771d2b | ||
|
|
e616dff2d1 | ||
|
|
e36e19de4e | ||
|
|
bbfa79ad9e | ||
|
|
00fc93b8b2 | ||
|
|
16c20a84b8 | ||
|
|
2a9a62eeb7 | ||
|
|
fe60670885 |
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
100
applicationset/generators/mocks/Generator.go
Normal file
100
applicationset/generators/mocks/Generator.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
6
go.mod
6
go.mod
@@ -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
20
go.sum
@@ -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=
|
||||
|
||||
@@ -27,6 +27,8 @@ rules:
|
||||
- appprojects
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- argoproj.io
|
||||
resources:
|
||||
@@ -62,4 +64,4 @@ rules:
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- watch
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -12,4 +12,4 @@ resources:
|
||||
images:
|
||||
- name: quay.io/argoproj/argocd
|
||||
newName: quay.io/argoproj/argocd
|
||||
newTag: latest
|
||||
newTag: v2.12.0-rc5
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{}{},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) &&
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -153,7 +153,9 @@ export const ApplicationDeploymentHistory = ({
|
||||
</React.Fragment>
|
||||
))
|
||||
)
|
||||
) : null}
|
||||
) : (
|
||||
<p>Click to see source details.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user