diff --git a/applicationset/controllers/applicationset_controller.go b/applicationset/controllers/applicationset_controller.go index e0cba5955fe56..86ebb939cbede 100644 --- a/applicationset/controllers/applicationset_controller.go +++ b/applicationset/controllers/applicationset_controller.go @@ -43,7 +43,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/predicate" + "github.com/argoproj/argo-cd/v2/applicationset/controllers/template" "github.com/argoproj/argo-cd/v2/applicationset/generators" + "github.com/argoproj/argo-cd/v2/applicationset/status" "github.com/argoproj/argo-cd/v2/applicationset/utils" "github.com/argoproj/argo-cd/v2/common" "github.com/argoproj/argo-cd/v2/util/db" @@ -129,7 +131,7 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque // 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) + desiredApplications, applicationSetReason, generatorsErr := template.GenerateApplications(logCtx, applicationSetInfo, r.Generators, r.Renderer, r.Client) if generatorsErr != nil { _ = r.setApplicationSetStatusCondition(ctx, &applicationSetInfo, @@ -495,88 +497,6 @@ func (r *ApplicationSetReconciler) getMinRequeueAfter(applicationSetInfo *argov1 return res } -func getTempApplication(applicationSetTemplate argov1alpha1.ApplicationSetTemplate) *argov1alpha1.Application { - var tmplApplication argov1alpha1.Application - tmplApplication.Annotations = applicationSetTemplate.Annotations - tmplApplication.Labels = applicationSetTemplate.Labels - tmplApplication.Namespace = applicationSetTemplate.Namespace - tmplApplication.Name = applicationSetTemplate.Name - tmplApplication.Spec = applicationSetTemplate.Spec - tmplApplication.Finalizers = applicationSetTemplate.Finalizers - - return &tmplApplication -} - -func (r *ApplicationSetReconciler) generateApplications(logCtx *log.Entry, applicationSetInfo argov1alpha1.ApplicationSet) ([]argov1alpha1.Application, argov1alpha1.ApplicationSetReasonType, error) { - var res []argov1alpha1.Application - - var firstError error - var applicationSetReason argov1alpha1.ApplicationSetReasonType - - for _, requestedGenerator := range applicationSetInfo.Spec.Generators { - t, err := generators.Transform(requestedGenerator, r.Generators, applicationSetInfo.Spec.Template, &applicationSetInfo, map[string]interface{}{}, r.Client) - if err != nil { - logCtx.WithError(err).WithField("generator", requestedGenerator). - Error("error generating application from params") - if firstError == nil { - firstError = err - applicationSetReason = argov1alpha1.ApplicationSetReasonApplicationParamsGenerationError - } - continue - } - - for _, a := range t { - tmplApplication := getTempApplication(a.Template) - - for _, p := range a.Params { - app, err := r.Renderer.RenderTemplateParams(tmplApplication, applicationSetInfo.Spec.SyncPolicy, p, applicationSetInfo.Spec.GoTemplate, applicationSetInfo.Spec.GoTemplateOptions) - if err != nil { - logCtx.WithError(err).WithField("params", a.Params).WithField("generator", requestedGenerator). - Error("error generating application from params") - - if firstError == nil { - firstError = err - applicationSetReason = argov1alpha1.ApplicationSetReasonRenderTemplateParamsError - } - continue - } - - if applicationSetInfo.Spec.TemplatePatch != nil { - patchedApplication, err := r.applyTemplatePatch(app, applicationSetInfo, p) - if err != nil { - log.WithError(err).WithField("params", a.Params).WithField("generator", requestedGenerator). - Error("error generating application from params") - - if firstError == nil { - firstError = err - applicationSetReason = argov1alpha1.ApplicationSetReasonRenderTemplateParamsError - } - continue - } - - app = patchedApplication - } - - res = append(res, *app) - } - } - - logCtx.WithField("generator", requestedGenerator).Infof("generated %d applications", len(res)) - logCtx.WithField("generator", requestedGenerator).Debugf("apps from generator: %+v", res) - } - - return res, applicationSetReason, firstError -} - -func (r *ApplicationSetReconciler) applyTemplatePatch(app *argov1alpha1.Application, applicationSetInfo argov1alpha1.ApplicationSet, params map[string]interface{}) (*argov1alpha1.Application, error) { - replacedTemplate, err := r.Renderer.Replace(*applicationSetInfo.Spec.TemplatePatch, params, applicationSetInfo.Spec.GoTemplate, applicationSetInfo.Spec.GoTemplateOptions) - if err != nil { - return nil, fmt.Errorf("error replacing values in templatePatch: %w", err) - } - - return applyTemplatePatch(app, replacedTemplate) -} - func ignoreNotAllowedNamespaces(namespaces []string) predicate.Predicate { return predicate.Funcs{ CreateFunc: func(e event.CreateEvent) bool { @@ -650,10 +570,6 @@ func (r *ApplicationSetReconciler) createOrUpdateInCluster(ctx context.Context, var firstError error // Creates or updates the application in appList for _, generatedApp := range desiredApplications { - // The app's namespace must be the same as the AppSet's namespace to preserve the appsets-in-any-namespace - // security boundary. - generatedApp.Namespace = applicationSet.Namespace - appLog := logCtx.WithFields(log.Fields{"app": generatedApp.QualifiedName()}) // Normalize to avoid fighting with the application controller. @@ -1351,8 +1267,8 @@ func findApplicationStatusIndex(appStatuses []argov1alpha1.ApplicationSetApplica } func (r *ApplicationSetReconciler) updateResourcesStatus(ctx context.Context, logCtx *log.Entry, appset *argov1alpha1.ApplicationSet, apps []argov1alpha1.Application) error { - statusMap := getResourceStatusMap(appset) - statusMap = buildResourceStatus(statusMap, apps) + statusMap := status.GetResourceStatusMap(appset) + statusMap = status.BuildResourceStatus(statusMap, apps) statuses := []argov1alpha1.ResourceStatus{} for _, status := range statusMap { @@ -1377,58 +1293,6 @@ func (r *ApplicationSetReconciler) updateResourcesStatus(ctx context.Context, lo return nil } -func buildResourceStatus(statusMap map[string]argov1alpha1.ResourceStatus, apps []argov1alpha1.Application) map[string]argov1alpha1.ResourceStatus { - appMap := map[string]argov1alpha1.Application{} - for _, app := range apps { - appCopy := app - appMap[app.Name] = app - - gvk := app.GroupVersionKind() - // Create status if it does not exist - status, ok := statusMap[app.Name] - if !ok { - status = argov1alpha1.ResourceStatus{ - Group: gvk.Group, - Version: gvk.Version, - Kind: gvk.Kind, - Name: app.Name, - Namespace: app.Namespace, - Status: app.Status.Sync.Status, - Health: &appCopy.Status.Health, - } - } - - status.Group = gvk.Group - status.Version = gvk.Version - status.Kind = gvk.Kind - status.Name = app.Name - status.Namespace = app.Namespace - status.Status = app.Status.Sync.Status - status.Health = &appCopy.Status.Health - - statusMap[app.Name] = status - } - cleanupDeletedApplicationStatuses(statusMap, appMap) - - return statusMap -} - -func getResourceStatusMap(appset *argov1alpha1.ApplicationSet) map[string]argov1alpha1.ResourceStatus { - statusMap := map[string]argov1alpha1.ResourceStatus{} - for _, status := range appset.Status.Resources { - statusMap[status.Name] = status - } - return statusMap -} - -func cleanupDeletedApplicationStatuses(statusMap map[string]argov1alpha1.ResourceStatus, apps map[string]argov1alpha1.Application) { - for name := range statusMap { - if _, ok := apps[name]; !ok { - delete(statusMap, name) - } - } -} - // setApplicationSetApplicationStatus updates the ApplicationSet's status field // with any new/changed Application statuses. func (r *ApplicationSetReconciler) setAppSetApplicationStatus(ctx context.Context, logCtx *log.Entry, applicationSet *argov1alpha1.ApplicationSet, applicationStatuses []argov1alpha1.ApplicationSetApplicationStatus) error { diff --git a/applicationset/controllers/applicationset_controller_test.go b/applicationset/controllers/applicationset_controller_test.go index 41f6a54e02743..cd72b2ddd7362 100644 --- a/applicationset/controllers/applicationset_controller_test.go +++ b/applicationset/controllers/applicationset_controller_test.go @@ -25,7 +25,6 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/cache" crtcache "sigs.k8s.io/controller-runtime/pkg/cache" - "sigs.k8s.io/controller-runtime/pkg/client" crtclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -35,11 +34,11 @@ import ( "github.com/argoproj/gitops-engine/pkg/sync/common" "github.com/argoproj/argo-cd/v2/applicationset/generators" + "github.com/argoproj/argo-cd/v2/applicationset/generators/mocks" "github.com/argoproj/argo-cd/v2/applicationset/utils" "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned/fake" - "github.com/argoproj/argo-cd/v2/util/collections" dbmocks "github.com/argoproj/argo-cd/v2/util/db/mocks" "github.com/argoproj/argo-cd/v2/pkg/apis/application" @@ -73,287 +72,6 @@ func (f *fakeCache) GetInformer(ctx context.Context, obj crtclient.Object, opt . return &fakeInformer{}, nil } -type generatorMock struct { - mock.Mock -} - -func (g *generatorMock) GetTemplate(appSetGenerator *v1alpha1.ApplicationSetGenerator) *v1alpha1.ApplicationSetTemplate { - args := g.Called(appSetGenerator) - - return args.Get(0).(*v1alpha1.ApplicationSetTemplate) -} - -func (g *generatorMock) GenerateParams(appSetGenerator *v1alpha1.ApplicationSetGenerator, _ *v1alpha1.ApplicationSet, client client.Client) ([]map[string]interface{}, error) { - args := g.Called(appSetGenerator) - - return args.Get(0).([]map[string]interface{}), args.Error(1) -} - -func (g *generatorMock) Replace(tmpl string, replaceMap map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (string, error) { - args := g.Called(tmpl, replaceMap, useGoTemplate, goTemplateOptions) - - return args.Get(0).(string), args.Error(1) -} - -type rendererMock struct { - mock.Mock -} - -func (g *generatorMock) GetRequeueAfter(appSetGenerator *v1alpha1.ApplicationSetGenerator) time.Duration { - args := g.Called(appSetGenerator) - - return args.Get(0).(time.Duration) -} - -func (r *rendererMock) RenderTemplateParams(tmpl *v1alpha1.Application, syncPolicy *v1alpha1.ApplicationSetSyncPolicy, params map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (*v1alpha1.Application, error) { - args := r.Called(tmpl, params, useGoTemplate, goTemplateOptions) - - if args.Error(1) != nil { - return nil, args.Error(1) - } - - return args.Get(0).(*v1alpha1.Application), args.Error(1) -} - -func (r *rendererMock) Replace(tmpl string, replaceMap map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (string, error) { - args := r.Called(tmpl, replaceMap, useGoTemplate, goTemplateOptions) - - return args.Get(0).(string), args.Error(1) -} - -func TestExtractApplications(t *testing.T) { - scheme := runtime.NewScheme() - err := v1alpha1.AddToScheme(scheme) - require.NoError(t, err) - - for _, c := range []struct { - name string - params []map[string]interface{} - template v1alpha1.ApplicationSetTemplate - generateParamsError error - rendererError error - expectErr bool - expectedReason v1alpha1.ApplicationSetReasonType - }{ - { - name: "Generate two applications", - params: []map[string]interface{}{{"name": "app1"}, {"name": "app2"}}, - template: v1alpha1.ApplicationSetTemplate{ - ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{ - Name: "name", - Namespace: "namespace", - Labels: map[string]string{"label_name": "label_value"}, - }, - Spec: v1alpha1.ApplicationSpec{}, - }, - expectedReason: "", - }, - { - name: "Handles error from the generator", - generateParamsError: fmt.Errorf("error"), - expectErr: true, - expectedReason: v1alpha1.ApplicationSetReasonApplicationParamsGenerationError, - }, - { - name: "Handles error from the render", - params: []map[string]interface{}{{"name": "app1"}, {"name": "app2"}}, - template: v1alpha1.ApplicationSetTemplate{ - ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{ - Name: "name", - Namespace: "namespace", - Labels: map[string]string{"label_name": "label_value"}, - }, - Spec: v1alpha1.ApplicationSpec{}, - }, - rendererError: fmt.Errorf("error"), - expectErr: true, - expectedReason: v1alpha1.ApplicationSetReasonRenderTemplateParamsError, - }, - } { - cc := c - app := v1alpha1.Application{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - }, - } - - t.Run(cc.name, func(t *testing.T) { - appSet := &v1alpha1.ApplicationSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "name", - Namespace: "namespace", - }, - } - - client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(appSet).Build() - - generatorMock := generatorMock{} - generator := v1alpha1.ApplicationSetGenerator{ - List: &v1alpha1.ListGenerator{}, - } - - generatorMock.On("GenerateParams", &generator). - Return(cc.params, cc.generateParamsError) - - generatorMock.On("GetTemplate", &generator). - Return(&v1alpha1.ApplicationSetTemplate{}) - - rendererMock := rendererMock{} - - var expectedApps []v1alpha1.Application - - if cc.generateParamsError == nil { - for _, p := range cc.params { - if cc.rendererError != nil { - rendererMock.On("RenderTemplateParams", getTempApplication(cc.template), p, false, []string(nil)). - Return(nil, cc.rendererError) - } else { - rendererMock.On("RenderTemplateParams", getTempApplication(cc.template), p, false, []string(nil)). - Return(&app, nil) - expectedApps = append(expectedApps, app) - } - } - } - - r := ApplicationSetReconciler{ - Client: client, - Scheme: scheme, - Recorder: record.NewFakeRecorder(1), - Generators: map[string]generators.Generator{ - "List": &generatorMock, - }, - Renderer: &rendererMock, - KubeClientset: kubefake.NewSimpleClientset(), - Cache: &fakeCache{}, - } - - got, reason, err := r.generateApplications(log.NewEntry(log.StandardLogger()), v1alpha1.ApplicationSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "name", - Namespace: "namespace", - }, - Spec: v1alpha1.ApplicationSetSpec{ - Generators: []v1alpha1.ApplicationSetGenerator{generator}, - Template: cc.template, - }, - }) - - if cc.expectErr { - require.Error(t, err) - } else { - require.NoError(t, err) - } - assert.Equal(t, expectedApps, got) - assert.Equal(t, cc.expectedReason, reason) - generatorMock.AssertNumberOfCalls(t, "GenerateParams", 1) - - if cc.generateParamsError == nil { - rendererMock.AssertNumberOfCalls(t, "RenderTemplateParams", len(cc.params)) - } - }) - } -} - -func TestMergeTemplateApplications(t *testing.T) { - scheme := runtime.NewScheme() - _ = v1alpha1.AddToScheme(scheme) - _ = v1alpha1.AddToScheme(scheme) - - client := fake.NewClientBuilder().WithScheme(scheme).Build() - - for _, c := range []struct { - name string - params []map[string]interface{} - template v1alpha1.ApplicationSetTemplate - overrideTemplate v1alpha1.ApplicationSetTemplate - expectedMerged v1alpha1.ApplicationSetTemplate - expectedApps []v1alpha1.Application - }{ - { - name: "Generate app", - params: []map[string]interface{}{{"name": "app1"}}, - template: v1alpha1.ApplicationSetTemplate{ - ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{ - Name: "name", - Namespace: "namespace", - Labels: map[string]string{"label_name": "label_value"}, - }, - Spec: v1alpha1.ApplicationSpec{}, - }, - overrideTemplate: v1alpha1.ApplicationSetTemplate{ - ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{ - Name: "test", - Labels: map[string]string{"foo": "bar"}, - }, - Spec: v1alpha1.ApplicationSpec{}, - }, - expectedMerged: v1alpha1.ApplicationSetTemplate{ - ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{ - Name: "test", - Namespace: "namespace", - Labels: map[string]string{"label_name": "label_value", "foo": "bar"}, - }, - Spec: v1alpha1.ApplicationSpec{}, - }, - expectedApps: []v1alpha1.Application{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "test", - Labels: map[string]string{"foo": "bar"}, - }, - Spec: v1alpha1.ApplicationSpec{}, - }, - }, - }, - } { - cc := c - - t.Run(cc.name, func(t *testing.T) { - generatorMock := generatorMock{} - generator := v1alpha1.ApplicationSetGenerator{ - List: &v1alpha1.ListGenerator{}, - } - - generatorMock.On("GenerateParams", &generator). - Return(cc.params, nil) - - generatorMock.On("GetTemplate", &generator). - Return(&cc.overrideTemplate) - - rendererMock := rendererMock{} - - rendererMock.On("RenderTemplateParams", getTempApplication(cc.expectedMerged), cc.params[0], false, []string(nil)). - Return(&cc.expectedApps[0], nil) - - r := ApplicationSetReconciler{ - Client: client, - Scheme: scheme, - Recorder: record.NewFakeRecorder(1), - Generators: map[string]generators.Generator{ - "List": &generatorMock, - }, - Renderer: &rendererMock, - KubeClientset: kubefake.NewSimpleClientset(), - } - - got, _, _ := r.generateApplications(log.NewEntry(log.StandardLogger()), v1alpha1.ApplicationSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "name", - Namespace: "namespace", - }, - Spec: v1alpha1.ApplicationSetSpec{ - Generators: []v1alpha1.ApplicationSetGenerator{generator}, - Template: cc.template, - }, - }, - ) - - assert.Equal(t, cc.expectedApps, got) - }) - } -} - func TestCreateOrUpdateInCluster(t *testing.T) { scheme := runtime.NewScheme() err := v1alpha1.AddToScheme(scheme) @@ -386,8 +104,10 @@ func TestCreateOrUpdateInCluster(t *testing.T) { desiredApps: []v1alpha1.Application{ { ObjectMeta: metav1.ObjectMeta{ - Name: "app1", + Name: "app1", + Namespace: "namespace", }, + Spec: v1alpha1.ApplicationSpec{Project: "default"}, }, }, expected: []v1alpha1.Application{ @@ -439,7 +159,8 @@ func TestCreateOrUpdateInCluster(t *testing.T) { desiredApps: []v1alpha1.Application{ { ObjectMeta: metav1.ObjectMeta{ - Name: "app1", + Name: "app1", + Namespace: "namespace", }, Spec: v1alpha1.ApplicationSpec{ Project: "project", @@ -497,7 +218,8 @@ func TestCreateOrUpdateInCluster(t *testing.T) { desiredApps: []v1alpha1.Application{ { ObjectMeta: metav1.ObjectMeta{ - Name: "app2", + Name: "app2", + Namespace: "namespace", }, Spec: v1alpha1.ApplicationSpec{ Project: "project", @@ -556,6 +278,7 @@ func TestCreateOrUpdateInCluster(t *testing.T) { { ObjectMeta: metav1.ObjectMeta{ Name: "app1", + Namespace: "namespace", Labels: map[string]string{"label-key": "label-value"}, Annotations: map[string]string{"annot-key": "annot-value"}, }, @@ -619,7 +342,8 @@ func TestCreateOrUpdateInCluster(t *testing.T) { desiredApps: []v1alpha1.Application{ { ObjectMeta: metav1.ObjectMeta{ - Name: "app1", + Name: "app1", + Namespace: "namespace", }, Spec: v1alpha1.ApplicationSpec{ Project: "project", @@ -685,7 +409,8 @@ func TestCreateOrUpdateInCluster(t *testing.T) { desiredApps: []v1alpha1.Application{ { ObjectMeta: metav1.ObjectMeta{ - Name: "app1", + Name: "app1", + Namespace: "namespace", }, Spec: v1alpha1.ApplicationSpec{ Project: "project", @@ -758,6 +483,7 @@ func TestCreateOrUpdateInCluster(t *testing.T) { { ObjectMeta: metav1.ObjectMeta{ Name: "app1", + Namespace: "namespace", Labels: map[string]string{"label-key": "label-value"}, Annotations: map[string]string{"annot-key": "annot-value"}, }, @@ -835,7 +561,8 @@ func TestCreateOrUpdateInCluster(t *testing.T) { desiredApps: []v1alpha1.Application{ { ObjectMeta: metav1.ObjectMeta{ - Name: "app1", + Name: "app1", + Namespace: "namespace", }, Spec: v1alpha1.ApplicationSpec{ Project: "project", @@ -904,7 +631,8 @@ func TestCreateOrUpdateInCluster(t *testing.T) { desiredApps: []v1alpha1.Application{ { ObjectMeta: metav1.ObjectMeta{ - Name: "app1", + Name: "app1", + Namespace: "namespace", }, Spec: v1alpha1.ApplicationSpec{ Project: "project", @@ -954,7 +682,8 @@ func TestCreateOrUpdateInCluster(t *testing.T) { desiredApps: []v1alpha1.Application{ { ObjectMeta: metav1.ObjectMeta{ - Name: "app1", + Name: "app1", + Namespace: "namespace", }, Spec: v1alpha1.ApplicationSpec{ Project: "project", @@ -1032,7 +761,8 @@ func TestCreateOrUpdateInCluster(t *testing.T) { desiredApps: []v1alpha1.Application{ { ObjectMeta: metav1.ObjectMeta{ - Name: "app1", + Name: "app1", + Namespace: "namespace", }, Spec: v1alpha1.ApplicationSpec{ Project: "project", @@ -1135,7 +865,8 @@ func TestCreateOrUpdateInCluster(t *testing.T) { desiredApps: []v1alpha1.Application{ { ObjectMeta: metav1.ObjectMeta{ - Name: "app1", + Name: "app1", + Namespace: "namespace", }, Spec: v1alpha1.ApplicationSpec{ Project: "project", @@ -1236,7 +967,8 @@ func TestCreateOrUpdateInCluster(t *testing.T) { desiredApps: []v1alpha1.Application{ { ObjectMeta: metav1.ObjectMeta{ - Name: "app1", + Name: "app1", + Namespace: "namespace", }, Spec: v1alpha1.ApplicationSpec{ Project: "project", @@ -1317,7 +1049,8 @@ func TestCreateOrUpdateInCluster(t *testing.T) { desiredApps: []v1alpha1.Application{ { ObjectMeta: metav1.ObjectMeta{ - Name: "app1", + Name: "app1", + Namespace: "namespace", }, Spec: v1alpha1.ApplicationSpec{ Project: "project", @@ -1761,7 +1494,8 @@ func TestCreateApplications(t *testing.T) { apps: []v1alpha1.Application{ { ObjectMeta: metav1.ObjectMeta{ - Name: "app1", + Name: "app1", + Namespace: "namespace", }, }, }, @@ -1816,7 +1550,8 @@ func TestCreateApplications(t *testing.T) { apps: []v1alpha1.Application{ { ObjectMeta: metav1.ObjectMeta{ - Name: "app1", + Name: "app1", + Namespace: "namespace", }, Spec: v1alpha1.ApplicationSpec{ Project: "project", @@ -1874,7 +1609,8 @@ func TestCreateApplications(t *testing.T) { apps: []v1alpha1.Application{ { ObjectMeta: metav1.ObjectMeta{ - Name: "app2", + Name: "app2", + Namespace: "namespace", }, Spec: v1alpha1.ApplicationSpec{ Project: "project", @@ -2106,15 +1842,15 @@ func TestGetMinRequeueAfter(t *testing.T) { Clusters: &v1alpha1.ClusterGenerator{}, } - generatorMock0 := generatorMock{} + generatorMock0 := mocks.Generator{} generatorMock0.On("GetRequeueAfter", &generator). Return(generators.NoRequeueAfter) - generatorMock1 := generatorMock{} + generatorMock1 := mocks.Generator{} generatorMock1.On("GetRequeueAfter", &generator). Return(time.Duration(1) * time.Second) - generatorMock10 := generatorMock{} + generatorMock10 := mocks.Generator{} generatorMock10.On("GetRequeueAfter", &generator). Return(time.Duration(10) * time.Second) @@ -2933,120 +2669,6 @@ func TestDeletePerformedWithSyncPolicyCreateOnlyAndAllowPolicyOverrideFalse(t *t assert.Empty(t, apps.Items) } -// Test app generation from a go template application set using a pull request generator -func TestGenerateAppsUsingPullRequestGenerator(t *testing.T) { - scheme := runtime.NewScheme() - client := fake.NewClientBuilder().WithScheme(scheme).Build() - - for _, cases := range []struct { - name string - params []map[string]interface{} - template v1alpha1.ApplicationSetTemplate - expectedApp []v1alpha1.Application - }{ - { - name: "Generate an application from a go template application set manifest using a pull request generator", - params: []map[string]interface{}{ - { - "number": "1", - "branch": "branch1", - "branch_slug": "branchSlug1", - "head_sha": "089d92cbf9ff857a39e6feccd32798ca700fb958", - "head_short_sha": "089d92cb", - "branch_slugify_default": "feat/a_really+long_pull_request_name_to_test_argo_slugification_and_branch_name_shortening_feature", - "branch_slugify_smarttruncate_disabled": "feat/areallylongpullrequestnametotestargoslugificationandbranchnameshorteningfeature", - "branch_slugify_smarttruncate_enabled": "feat/testwithsmarttruncateenabledramdomlonglistofcharacters", - "labels": []string{"label1"}, - }, - }, - template: v1alpha1.ApplicationSetTemplate{ - ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{ - Name: "AppSet-{{.branch}}-{{.number}}", - Labels: map[string]string{ - "app1": "{{index .labels 0}}", - "branch-test1": "AppSet-{{.branch_slugify_default | slugify }}", - "branch-test2": "AppSet-{{.branch_slugify_smarttruncate_disabled | slugify 49 false }}", - "branch-test3": "AppSet-{{.branch_slugify_smarttruncate_enabled | slugify 50 true }}", - }, - }, - Spec: v1alpha1.ApplicationSpec{ - Source: &v1alpha1.ApplicationSource{ - RepoURL: "https://testurl/testRepo", - TargetRevision: "{{.head_short_sha}}", - }, - Destination: v1alpha1.ApplicationDestination{ - Server: "https://kubernetes.default.svc", - Namespace: "AppSet-{{.branch_slug}}-{{.head_sha}}", - }, - }, - }, - expectedApp: []v1alpha1.Application{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "AppSet-branch1-1", - Labels: map[string]string{ - "app1": "label1", - "branch-test1": "AppSet-feat-a-really-long-pull-request-name-to-test-argo", - "branch-test2": "AppSet-feat-areallylongpullrequestnametotestargoslugific", - "branch-test3": "AppSet-feat", - }, - }, - Spec: v1alpha1.ApplicationSpec{ - Source: &v1alpha1.ApplicationSource{ - RepoURL: "https://testurl/testRepo", - TargetRevision: "089d92cb", - }, - Destination: v1alpha1.ApplicationDestination{ - Server: "https://kubernetes.default.svc", - Namespace: "AppSet-branchSlug1-089d92cbf9ff857a39e6feccd32798ca700fb958", - }, - }, - }, - }, - }, - } { - t.Run(cases.name, func(t *testing.T) { - generatorMock := generatorMock{} - generator := v1alpha1.ApplicationSetGenerator{ - PullRequest: &v1alpha1.PullRequestGenerator{}, - } - - generatorMock.On("GenerateParams", &generator). - Return(cases.params, nil) - - generatorMock.On("GetTemplate", &generator). - Return(&cases.template, nil) - - appSetReconciler := ApplicationSetReconciler{ - Client: client, - Scheme: scheme, - Recorder: record.NewFakeRecorder(1), - Cache: &fakeCache{}, - Generators: map[string]generators.Generator{ - "PullRequest": &generatorMock, - }, - Renderer: &utils.Render{}, - KubeClientset: kubefake.NewSimpleClientset(), - } - - gotApp, _, _ := appSetReconciler.generateApplications(log.NewEntry(log.StandardLogger()), v1alpha1.ApplicationSet{ - Spec: v1alpha1.ApplicationSetSpec{ - GoTemplate: true, - Generators: []v1alpha1.ApplicationSetGenerator{{ - PullRequest: &v1alpha1.PullRequestGenerator{}, - }}, - Template: cases.template, - }, - }, - ) - assert.EqualValues(t, cases.expectedApp[0].ObjectMeta.Name, gotApp[0].ObjectMeta.Name) - assert.EqualValues(t, cases.expectedApp[0].Spec.Source.TargetRevision, gotApp[0].Spec.Source.TargetRevision) - assert.EqualValues(t, cases.expectedApp[0].Spec.Destination.Namespace, gotApp[0].Spec.Destination.Namespace) - assert.True(t, collections.StringMapsEqual(cases.expectedApp[0].ObjectMeta.Labels, gotApp[0].ObjectMeta.Labels)) - }) - } -} - func TestPolicies(t *testing.T) { scheme := runtime.NewScheme() err := v1alpha1.AddToScheme(scheme) diff --git a/applicationset/controllers/requeue_after_test.go b/applicationset/controllers/requeue_after_test.go index 10067c119b78d..360861ff3ad5c 100644 --- a/applicationset/controllers/requeue_after_test.go +++ b/applicationset/controllers/requeue_after_test.go @@ -56,14 +56,14 @@ func TestRequeueAfter(t *testing.T) { }, } fakeDynClient := dynfake.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), gvrToListKind, duckType) - + scmConfig := generators.NewSCMConfig("", []string{""}, true, nil) terminalGenerators := map[string]generators.Generator{ "List": generators.NewListGenerator(), "Clusters": generators.NewClusterGenerator(k8sClient, ctx, appClientset, "argocd"), "Git": generators.NewGitGenerator(mockServer), - "SCMProvider": generators.NewSCMProviderGenerator(fake.NewClientBuilder().WithObjects(&corev1.Secret{}).Build(), generators.SCMAuthProviders{}, "", []string{""}, true), + "SCMProvider": generators.NewSCMProviderGenerator(fake.NewClientBuilder().WithObjects(&corev1.Secret{}).Build(), scmConfig), "ClusterDecisionResource": generators.NewDuckTypeGenerator(ctx, fakeDynClient, appClientset, "argocd"), - "PullRequest": generators.NewPullRequestGenerator(k8sClient, generators.SCMAuthProviders{}, "", []string{""}, true), + "PullRequest": generators.NewPullRequestGenerator(k8sClient, scmConfig), } nestedGenerators := map[string]generators.Generator{ diff --git a/applicationset/controllers/templatePatch.go b/applicationset/controllers/template/patch.go similarity index 98% rename from applicationset/controllers/templatePatch.go rename to applicationset/controllers/template/patch.go index 39058ac187022..b9d1166f1f237 100644 --- a/applicationset/controllers/templatePatch.go +++ b/applicationset/controllers/template/patch.go @@ -1,4 +1,4 @@ -package controllers +package template import ( "encoding/json" diff --git a/applicationset/controllers/templatePatch_test.go b/applicationset/controllers/template/patch_test.go similarity index 99% rename from applicationset/controllers/templatePatch_test.go rename to applicationset/controllers/template/patch_test.go index c1a794077c8ee..456fe445994c8 100644 --- a/applicationset/controllers/templatePatch_test.go +++ b/applicationset/controllers/template/patch_test.go @@ -1,4 +1,4 @@ -package controllers +package template import ( "testing" diff --git a/applicationset/controllers/template/template.go b/applicationset/controllers/template/template.go new file mode 100644 index 0000000000000..bb4bc155d4e59 --- /dev/null +++ b/applicationset/controllers/template/template.go @@ -0,0 +1,99 @@ +package template + +import ( + "fmt" + + "sigs.k8s.io/controller-runtime/pkg/client" + + log "github.com/sirupsen/logrus" + + "github.com/argoproj/argo-cd/v2/applicationset/generators" + "github.com/argoproj/argo-cd/v2/applicationset/utils" + + argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" +) + +func GenerateApplications(logCtx *log.Entry, applicationSetInfo argov1alpha1.ApplicationSet, g map[string]generators.Generator, renderer utils.Renderer, client client.Client) ([]argov1alpha1.Application, argov1alpha1.ApplicationSetReasonType, error) { + var res []argov1alpha1.Application + + var firstError error + var applicationSetReason argov1alpha1.ApplicationSetReasonType + + for _, requestedGenerator := range applicationSetInfo.Spec.Generators { + t, err := generators.Transform(requestedGenerator, g, applicationSetInfo.Spec.Template, &applicationSetInfo, map[string]interface{}{}, client) + if err != nil { + logCtx.WithError(err).WithField("generator", requestedGenerator). + Error("error generating application from params") + if firstError == nil { + firstError = err + applicationSetReason = argov1alpha1.ApplicationSetReasonApplicationParamsGenerationError + } + continue + } + + for _, a := range t { + tmplApplication := GetTempApplication(a.Template) + + for _, p := range a.Params { + app, err := renderer.RenderTemplateParams(tmplApplication, applicationSetInfo.Spec.SyncPolicy, p, applicationSetInfo.Spec.GoTemplate, applicationSetInfo.Spec.GoTemplateOptions) + if err != nil { + logCtx.WithError(err).WithField("params", a.Params).WithField("generator", requestedGenerator). + Error("error generating application from params") + + if firstError == nil { + firstError = err + applicationSetReason = argov1alpha1.ApplicationSetReasonRenderTemplateParamsError + } + continue + } + + if applicationSetInfo.Spec.TemplatePatch != nil { + patchedApplication, err := renderTemplatePatch(renderer, app, applicationSetInfo, p) + if err != nil { + log.WithError(err).WithField("params", a.Params).WithField("generator", requestedGenerator). + Error("error generating application from params") + + if firstError == nil { + firstError = err + applicationSetReason = argov1alpha1.ApplicationSetReasonRenderTemplateParamsError + } + continue + } + + app = patchedApplication + } + + // The app's namespace must be the same as the AppSet's namespace to preserve the appsets-in-any-namespace + // security boundary. + app.Namespace = applicationSetInfo.Namespace + res = append(res, *app) + } + } + + logCtx.WithField("generator", requestedGenerator).Infof("generated %d applications", len(res)) + logCtx.WithField("generator", requestedGenerator).Debugf("apps from generator: %+v", res) + } + + return res, applicationSetReason, firstError +} + +func renderTemplatePatch(r utils.Renderer, app *argov1alpha1.Application, applicationSetInfo argov1alpha1.ApplicationSet, params map[string]interface{}) (*argov1alpha1.Application, error) { + replacedTemplate, err := r.Replace(*applicationSetInfo.Spec.TemplatePatch, params, applicationSetInfo.Spec.GoTemplate, applicationSetInfo.Spec.GoTemplateOptions) + if err != nil { + return nil, fmt.Errorf("error replacing values in templatePatch: %w", err) + } + + return applyTemplatePatch(app, replacedTemplate) +} + +func GetTempApplication(applicationSetTemplate argov1alpha1.ApplicationSetTemplate) *argov1alpha1.Application { + var tmplApplication argov1alpha1.Application + tmplApplication.Annotations = applicationSetTemplate.Annotations + tmplApplication.Labels = applicationSetTemplate.Labels + tmplApplication.Namespace = applicationSetTemplate.Namespace + tmplApplication.Name = applicationSetTemplate.Name + tmplApplication.Spec = applicationSetTemplate.Spec + tmplApplication.Finalizers = applicationSetTemplate.Finalizers + + return &tmplApplication +} diff --git a/applicationset/controllers/template/template_test.go b/applicationset/controllers/template/template_test.go new file mode 100644 index 0000000000000..4c45c5f7618f9 --- /dev/null +++ b/applicationset/controllers/template/template_test.go @@ -0,0 +1,349 @@ +package template + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + log "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/argoproj/argo-cd/v2/applicationset/generators" + genmock "github.com/argoproj/argo-cd/v2/applicationset/generators/mocks" + "github.com/argoproj/argo-cd/v2/applicationset/utils" + rendmock "github.com/argoproj/argo-cd/v2/applicationset/utils/mocks" + "github.com/argoproj/argo-cd/v2/pkg/apis/application" + "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" + "github.com/argoproj/argo-cd/v2/util/collections" +) + +func TestGenerateApplications(t *testing.T) { + scheme := runtime.NewScheme() + err := v1alpha1.AddToScheme(scheme) + require.NoError(t, err) + + err = v1alpha1.AddToScheme(scheme) + require.NoError(t, err) + + for _, c := range []struct { + name string + params []map[string]interface{} + template v1alpha1.ApplicationSetTemplate + generateParamsError error + rendererError error + expectErr bool + expectedReason v1alpha1.ApplicationSetReasonType + }{ + { + name: "Generate two applications", + params: []map[string]interface{}{{"name": "app1"}, {"name": "app2"}}, + template: v1alpha1.ApplicationSetTemplate{ + ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{ + Name: "name", + Namespace: "namespace", + Labels: map[string]string{"label_name": "label_value"}, + }, + Spec: v1alpha1.ApplicationSpec{}, + }, + expectedReason: "", + }, + { + name: "Handles error from the generator", + generateParamsError: fmt.Errorf("error"), + expectErr: true, + expectedReason: v1alpha1.ApplicationSetReasonApplicationParamsGenerationError, + }, + { + name: "Handles error from the render", + params: []map[string]interface{}{{"name": "app1"}, {"name": "app2"}}, + template: v1alpha1.ApplicationSetTemplate{ + ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{ + Name: "name", + Namespace: "namespace", + Labels: map[string]string{"label_name": "label_value"}, + }, + Spec: v1alpha1.ApplicationSpec{}, + }, + rendererError: fmt.Errorf("error"), + expectErr: true, + expectedReason: v1alpha1.ApplicationSetReasonRenderTemplateParamsError, + }, + } { + cc := c + app := v1alpha1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "namespace", + }, + TypeMeta: metav1.TypeMeta{ + Kind: application.ApplicationKind, + APIVersion: "argoproj.io/v1alpha1", + }, + } + + t.Run(cc.name, func(t *testing.T) { + generatorMock := genmock.Generator{} + generator := v1alpha1.ApplicationSetGenerator{ + List: &v1alpha1.ListGenerator{}, + } + + generatorMock.On("GenerateParams", &generator, mock.AnythingOfType("*v1alpha1.ApplicationSet"), mock.Anything). + Return(cc.params, cc.generateParamsError) + + generatorMock.On("GetTemplate", &generator). + Return(&v1alpha1.ApplicationSetTemplate{}) + + rendererMock := rendmock.Renderer{} + + var expectedApps []v1alpha1.Application + + if cc.generateParamsError == nil { + for _, p := range cc.params { + if cc.rendererError != nil { + rendererMock.On("RenderTemplateParams", GetTempApplication(cc.template), mock.AnythingOfType("*v1alpha1.ApplicationSetSyncPolicy"), p, false, []string(nil)). + Return(nil, cc.rendererError) + } else { + rendererMock.On("RenderTemplateParams", GetTempApplication(cc.template), mock.AnythingOfType("*v1alpha1.ApplicationSetSyncPolicy"), p, false, []string(nil)). + Return(&app, nil) + expectedApps = append(expectedApps, app) + } + } + } + + generators := map[string]generators.Generator{ + "List": &generatorMock, + } + renderer := &rendererMock + + got, reason, err := GenerateApplications(log.NewEntry(log.StandardLogger()), v1alpha1.ApplicationSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + Namespace: "namespace", + }, + Spec: v1alpha1.ApplicationSetSpec{ + Generators: []v1alpha1.ApplicationSetGenerator{generator}, + Template: cc.template, + }, + }, + generators, + renderer, + nil, + ) + + if cc.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + assert.Equal(t, expectedApps, got) + assert.Equal(t, cc.expectedReason, reason) + generatorMock.AssertNumberOfCalls(t, "GenerateParams", 1) + + if cc.generateParamsError == nil { + rendererMock.AssertNumberOfCalls(t, "RenderTemplateParams", len(cc.params)) + } + }) + } +} + +func TestMergeTemplateApplications(t *testing.T) { + for _, c := range []struct { + name string + params []map[string]interface{} + template v1alpha1.ApplicationSetTemplate + overrideTemplate v1alpha1.ApplicationSetTemplate + expectedMerged v1alpha1.ApplicationSetTemplate + expectedApps []v1alpha1.Application + }{ + { + name: "Generate app", + params: []map[string]interface{}{{"name": "app1"}}, + template: v1alpha1.ApplicationSetTemplate{ + ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{ + Name: "name", + Namespace: "namespace", + Labels: map[string]string{"label_name": "label_value"}, + }, + Spec: v1alpha1.ApplicationSpec{}, + }, + overrideTemplate: v1alpha1.ApplicationSetTemplate{ + ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{ + Name: "test", + Labels: map[string]string{"foo": "bar"}, + }, + Spec: v1alpha1.ApplicationSpec{}, + }, + expectedMerged: v1alpha1.ApplicationSetTemplate{ + ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{ + Name: "test", + Namespace: "namespace", + Labels: map[string]string{"label_name": "label_value", "foo": "bar"}, + }, + Spec: v1alpha1.ApplicationSpec{}, + }, + expectedApps: []v1alpha1.Application{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + Labels: map[string]string{"foo": "bar"}, + }, + Spec: v1alpha1.ApplicationSpec{}, + }, + }, + }, + } { + cc := c + + t.Run(cc.name, func(t *testing.T) { + generatorMock := genmock.Generator{} + generator := v1alpha1.ApplicationSetGenerator{ + List: &v1alpha1.ListGenerator{}, + } + + generatorMock.On("GenerateParams", &generator, mock.AnythingOfType("*v1alpha1.ApplicationSet"), mock.Anything). + Return(cc.params, nil) + + generatorMock.On("GetTemplate", &generator). + Return(&cc.overrideTemplate) + + rendererMock := rendmock.Renderer{} + + rendererMock.On("RenderTemplateParams", GetTempApplication(cc.expectedMerged), mock.AnythingOfType("*v1alpha1.ApplicationSetSyncPolicy"), cc.params[0], false, []string(nil)). + Return(&cc.expectedApps[0], nil) + + generators := map[string]generators.Generator{ + "List": &generatorMock, + } + renderer := &rendererMock + + got, _, _ := GenerateApplications(log.NewEntry(log.StandardLogger()), v1alpha1.ApplicationSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + Namespace: "namespace", + }, + Spec: v1alpha1.ApplicationSetSpec{ + Generators: []v1alpha1.ApplicationSetGenerator{generator}, + Template: cc.template, + }, + }, + generators, + renderer, + nil, + ) + + assert.Equal(t, cc.expectedApps, got) + }) + } +} + +// Test app generation from a go template application set using a pull request generator +func TestGenerateAppsUsingPullRequestGenerator(t *testing.T) { + for _, cases := range []struct { + name string + params []map[string]interface{} + template v1alpha1.ApplicationSetTemplate + expectedApp []v1alpha1.Application + }{ + { + name: "Generate an application from a go template application set manifest using a pull request generator", + params: []map[string]interface{}{ + { + "number": "1", + "branch": "branch1", + "branch_slug": "branchSlug1", + "head_sha": "089d92cbf9ff857a39e6feccd32798ca700fb958", + "head_short_sha": "089d92cb", + "branch_slugify_default": "feat/a_really+long_pull_request_name_to_test_argo_slugification_and_branch_name_shortening_feature", + "branch_slugify_smarttruncate_disabled": "feat/areallylongpullrequestnametotestargoslugificationandbranchnameshorteningfeature", + "branch_slugify_smarttruncate_enabled": "feat/testwithsmarttruncateenabledramdomlonglistofcharacters", + "labels": []string{"label1"}, + }, + }, + template: v1alpha1.ApplicationSetTemplate{ + ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{ + Name: "AppSet-{{.branch}}-{{.number}}", + Labels: map[string]string{ + "app1": "{{index .labels 0}}", + "branch-test1": "AppSet-{{.branch_slugify_default | slugify }}", + "branch-test2": "AppSet-{{.branch_slugify_smarttruncate_disabled | slugify 49 false }}", + "branch-test3": "AppSet-{{.branch_slugify_smarttruncate_enabled | slugify 50 true }}", + }, + }, + Spec: v1alpha1.ApplicationSpec{ + Source: &v1alpha1.ApplicationSource{ + RepoURL: "https://testurl/testRepo", + TargetRevision: "{{.head_short_sha}}", + }, + Destination: v1alpha1.ApplicationDestination{ + Server: "https://kubernetes.default.svc", + Namespace: "AppSet-{{.branch_slug}}-{{.head_sha}}", + }, + }, + }, + expectedApp: []v1alpha1.Application{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "AppSet-branch1-1", + Labels: map[string]string{ + "app1": "label1", + "branch-test1": "AppSet-feat-a-really-long-pull-request-name-to-test-argo", + "branch-test2": "AppSet-feat-areallylongpullrequestnametotestargoslugific", + "branch-test3": "AppSet-feat", + }, + }, + Spec: v1alpha1.ApplicationSpec{ + Source: &v1alpha1.ApplicationSource{ + RepoURL: "https://testurl/testRepo", + TargetRevision: "089d92cb", + }, + Destination: v1alpha1.ApplicationDestination{ + Server: "https://kubernetes.default.svc", + Namespace: "AppSet-branchSlug1-089d92cbf9ff857a39e6feccd32798ca700fb958", + }, + }, + }, + }, + }, + } { + t.Run(cases.name, func(t *testing.T) { + generatorMock := genmock.Generator{} + generator := v1alpha1.ApplicationSetGenerator{ + PullRequest: &v1alpha1.PullRequestGenerator{}, + } + + generatorMock.On("GenerateParams", &generator, mock.AnythingOfType("*v1alpha1.ApplicationSet"), mock.Anything). + Return(cases.params, nil) + + generatorMock.On("GetTemplate", &generator). + Return(&cases.template, nil) + + generators := map[string]generators.Generator{ + "PullRequest": &generatorMock, + } + renderer := &utils.Render{} + + gotApp, _, _ := GenerateApplications(log.NewEntry(log.StandardLogger()), v1alpha1.ApplicationSet{ + Spec: v1alpha1.ApplicationSetSpec{ + GoTemplate: true, + Generators: []v1alpha1.ApplicationSetGenerator{{ + PullRequest: &v1alpha1.PullRequestGenerator{}, + }}, + Template: cases.template, + }, + }, + generators, + renderer, + nil, + ) + assert.EqualValues(t, cases.expectedApp[0].ObjectMeta.Name, gotApp[0].ObjectMeta.Name) + assert.EqualValues(t, cases.expectedApp[0].Spec.Source.TargetRevision, gotApp[0].Spec.Source.TargetRevision) + assert.EqualValues(t, cases.expectedApp[0].Spec.Destination.Namespace, gotApp[0].Spec.Destination.Namespace) + assert.True(t, collections.StringMapsEqual(cases.expectedApp[0].ObjectMeta.Labels, gotApp[0].ObjectMeta.Labels)) + }) + } +} diff --git a/applicationset/generators/interface.go b/applicationset/generators/interface.go index ea105c7842279..14093bac45d98 100644 --- a/applicationset/generators/interface.go +++ b/applicationset/generators/interface.go @@ -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. diff --git a/applicationset/generators/matrix_test.go b/applicationset/generators/matrix_test.go index 440fafd3f3ad2..0e37f01ff06d3 100644 --- a/applicationset/generators/matrix_test.go +++ b/applicationset/generators/matrix_test.go @@ -1073,7 +1073,7 @@ func TestGitGenerator_GenerateParams_list_x_git_matrix_generator(t *testing.T) { // of that bug. listGeneratorMock := &generatorMock{} - listGeneratorMock.On("GenerateParams", mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), mock.AnythingOfType("*v1alpha1.ApplicationSet")).Return([]map[string]interface{}{ + listGeneratorMock.On("GenerateParams", mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator"), mock.AnythingOfType("*v1alpha1.ApplicationSet"), mock.Anything).Return([]map[string]interface{}{ {"some": "value"}, }, nil) listGeneratorMock.On("GetTemplate", mock.AnythingOfType("*v1alpha1.ApplicationSetGenerator")).Return(&argoprojiov1alpha1.ApplicationSetTemplate{}) diff --git a/applicationset/generators/mocks/generator.go b/applicationset/generators/mocks/generator.go new file mode 100644 index 0000000000000..df337fccb44b2 --- /dev/null +++ b/applicationset/generators/mocks/generator.go @@ -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 +} diff --git a/applicationset/generators/pull_request.go b/applicationset/generators/pull_request.go index 54fcb73a2d77f..a453edfc83d55 100644 --- a/applicationset/generators/pull_request.go +++ b/applicationset/generators/pull_request.go @@ -24,19 +24,13 @@ const ( type PullRequestGenerator struct { client client.Client selectServiceProviderFunc func(context.Context, *argoprojiov1alpha1.PullRequestGenerator, *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) - auth SCMAuthProviders - scmRootCAPath string - allowedSCMProviders []string - enableSCMProviders bool + SCMConfig } -func NewPullRequestGenerator(client client.Client, auth SCMAuthProviders, scmRootCAPath string, allowedScmProviders []string, enableSCMProviders bool) Generator { +func NewPullRequestGenerator(client client.Client, scmConfig SCMConfig) Generator { g := &PullRequestGenerator{ - client: client, - auth: auth, - scmRootCAPath: scmRootCAPath, - allowedSCMProviders: allowedScmProviders, - enableSCMProviders: enableSCMProviders, + client: client, + SCMConfig: scmConfig, } g.selectServiceProviderFunc = g.selectServiceProvider return g @@ -193,7 +187,7 @@ func (g *PullRequestGenerator) selectServiceProvider(ctx context.Context, genera func (g *PullRequestGenerator) github(ctx context.Context, cfg *argoprojiov1alpha1.PullRequestGeneratorGithub, applicationSetInfo *argoprojiov1alpha1.ApplicationSet) (pullrequest.PullRequestService, error) { // use an app if it was configured if cfg.AppSecretName != "" { - auth, err := g.auth.GitHubApps.GetAuthSecret(ctx, cfg.AppSecretName) + auth, err := g.GitHubApps.GetAuthSecret(ctx, cfg.AppSecretName) if err != nil { return nil, fmt.Errorf("error getting GitHub App secret: %w", err) } diff --git a/applicationset/generators/pull_request_test.go b/applicationset/generators/pull_request_test.go index b293623077009..2911ea3f49486 100644 --- a/applicationset/generators/pull_request_test.go +++ b/applicationset/generators/pull_request_test.go @@ -329,13 +329,13 @@ func TestAllowedSCMProviderPullRequest(t *testing.T) { t.Run(testCaseCopy.name, func(t *testing.T) { t.Parallel() - pullRequestGenerator := NewPullRequestGenerator(nil, SCMAuthProviders{}, "", []string{ + pullRequestGenerator := NewPullRequestGenerator(nil, NewSCMConfig("", []string{ "github.myorg.com", "gitlab.myorg.com", "gitea.myorg.com", "bitbucket.myorg.com", "azuredevops.myorg.com", - }, true) + }, true, nil)) applicationSetInfo := argoprojiov1alpha1.ApplicationSet{ ObjectMeta: metav1.ObjectMeta{ @@ -357,7 +357,7 @@ func TestAllowedSCMProviderPullRequest(t *testing.T) { } func TestSCMProviderDisabled_PRGenerator(t *testing.T) { - generator := NewPullRequestGenerator(nil, SCMAuthProviders{}, "", []string{}, false) + generator := NewPullRequestGenerator(nil, NewSCMConfig("", []string{}, false, nil)) applicationSetInfo := argoprojiov1alpha1.ApplicationSet{ ObjectMeta: metav1.ObjectMeta{ diff --git a/applicationset/generators/scm_provider.go b/applicationset/generators/scm_provider.go index 0f90240fdb25c..7a3c66754f90c 100644 --- a/applicationset/generators/scm_provider.go +++ b/applicationset/generators/scm_provider.go @@ -29,29 +29,36 @@ type SCMProviderGenerator struct { client client.Client // Testing hooks. overrideProvider scm_provider.SCMProviderService - SCMAuthProviders + SCMConfig +} +type SCMConfig struct { scmRootCAPath string allowedSCMProviders []string enableSCMProviders bool + GitHubApps github_app_auth.Credentials } -type SCMAuthProviders struct { - GitHubApps github_app_auth.Credentials -} - -func NewSCMProviderGenerator(client client.Client, providers SCMAuthProviders, scmRootCAPath string, allowedSCMProviders []string, enableSCMProviders bool) Generator { - return &SCMProviderGenerator{ - client: client, - SCMAuthProviders: providers, +func NewSCMConfig(scmRootCAPath string, allowedSCMProviders []string, enableSCMProviders bool, gitHubApps github_app_auth.Credentials) SCMConfig { + return SCMConfig{ scmRootCAPath: scmRootCAPath, allowedSCMProviders: allowedSCMProviders, enableSCMProviders: enableSCMProviders, + GitHubApps: gitHubApps, + } +} + +func NewSCMProviderGenerator(client client.Client, scmConfig SCMConfig) Generator { + return &SCMProviderGenerator{ + client: client, + SCMConfig: scmConfig, } } // Testing generator func NewTestSCMProviderGenerator(overrideProvider scm_provider.SCMProviderService) Generator { - return &SCMProviderGenerator{overrideProvider: overrideProvider, enableSCMProviders: true} + return &SCMProviderGenerator{overrideProvider: overrideProvider, SCMConfig: SCMConfig{ + enableSCMProviders: true, + }} } func (g *SCMProviderGenerator) GetRequeueAfter(appSetGenerator *argoprojiov1alpha1.ApplicationSetGenerator) time.Duration { diff --git a/applicationset/generators/scm_provider_test.go b/applicationset/generators/scm_provider_test.go index 04291b6303e83..61e0bc9c83351 100644 --- a/applicationset/generators/scm_provider_test.go +++ b/applicationset/generators/scm_provider_test.go @@ -174,7 +174,7 @@ func TestSCMProviderGenerateParams(t *testing.T) { mockProvider := &scm_provider.MockProvider{ Repos: testCaseCopy.repos, } - scmGenerator := &SCMProviderGenerator{overrideProvider: mockProvider, enableSCMProviders: true} + scmGenerator := &SCMProviderGenerator{overrideProvider: mockProvider, SCMConfig: SCMConfig{enableSCMProviders: true}} applicationSetInfo := argoprojiov1alpha1.ApplicationSet{ ObjectMeta: metav1.ObjectMeta{ Name: "set", @@ -260,14 +260,16 @@ func TestAllowedSCMProvider(t *testing.T) { t.Parallel() scmGenerator := &SCMProviderGenerator{ - allowedSCMProviders: []string{ - "github.myorg.com", - "gitlab.myorg.com", - "gitea.myorg.com", - "bitbucket.myorg.com", - "azuredevops.myorg.com", + SCMConfig: SCMConfig{ + allowedSCMProviders: []string{ + "github.myorg.com", + "gitlab.myorg.com", + "gitea.myorg.com", + "bitbucket.myorg.com", + "azuredevops.myorg.com", + }, + enableSCMProviders: true, }, - enableSCMProviders: true, } applicationSetInfo := argoprojiov1alpha1.ApplicationSet{ @@ -290,7 +292,7 @@ func TestAllowedSCMProvider(t *testing.T) { } func TestSCMProviderDisabled_SCMGenerator(t *testing.T) { - generator := &SCMProviderGenerator{enableSCMProviders: false} + generator := &SCMProviderGenerator{SCMConfig: SCMConfig{enableSCMProviders: false}} applicationSetInfo := argoprojiov1alpha1.ApplicationSet{ ObjectMeta: metav1.ObjectMeta{ diff --git a/applicationset/generators/utils.go b/applicationset/generators/utils.go new file mode 100644 index 0000000000000..3d106d0a79144 --- /dev/null +++ b/applicationset/generators/utils.go @@ -0,0 +1,49 @@ +package generators + +import ( + "context" + + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/argoproj/argo-cd/v2/applicationset/services" +) + +func GetGenerators(ctx context.Context, c client.Client, k8sClient kubernetes.Interface, namespace string, argoCDService services.Repos, dynamicClient dynamic.Interface, scmConfig SCMConfig) map[string]Generator { + terminalGenerators := map[string]Generator{ + "List": NewListGenerator(), + "Clusters": NewClusterGenerator(c, ctx, k8sClient, namespace), + "Git": NewGitGenerator(argoCDService), + "SCMProvider": NewSCMProviderGenerator(c, scmConfig), + "ClusterDecisionResource": NewDuckTypeGenerator(ctx, dynamicClient, k8sClient, namespace), + "PullRequest": NewPullRequestGenerator(c, scmConfig), + "Plugin": NewPluginGenerator(c, ctx, k8sClient, namespace), + } + + nestedGenerators := map[string]Generator{ + "List": terminalGenerators["List"], + "Clusters": terminalGenerators["Clusters"], + "Git": terminalGenerators["Git"], + "SCMProvider": terminalGenerators["SCMProvider"], + "ClusterDecisionResource": terminalGenerators["ClusterDecisionResource"], + "PullRequest": terminalGenerators["PullRequest"], + "Plugin": terminalGenerators["Plugin"], + "Matrix": NewMatrixGenerator(terminalGenerators), + "Merge": NewMergeGenerator(terminalGenerators), + } + + topLevelGenerators := map[string]Generator{ + "List": terminalGenerators["List"], + "Clusters": terminalGenerators["Clusters"], + "Git": terminalGenerators["Git"], + "SCMProvider": terminalGenerators["SCMProvider"], + "ClusterDecisionResource": terminalGenerators["ClusterDecisionResource"], + "PullRequest": terminalGenerators["PullRequest"], + "Plugin": terminalGenerators["Plugin"], + "Matrix": NewMatrixGenerator(nestedGenerators), + "Merge": NewMergeGenerator(nestedGenerators), + } + + return topLevelGenerators +} diff --git a/applicationset/status/resource_status.go b/applicationset/status/resource_status.go new file mode 100644 index 0000000000000..4e9db5ff560e9 --- /dev/null +++ b/applicationset/status/resource_status.go @@ -0,0 +1,57 @@ +package status + +import ( + argov1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" +) + +func BuildResourceStatus(statusMap map[string]argov1alpha1.ResourceStatus, apps []argov1alpha1.Application) map[string]argov1alpha1.ResourceStatus { + appMap := map[string]argov1alpha1.Application{} + for _, app := range apps { + appCopy := app + appMap[app.Name] = app + + gvk := app.GroupVersionKind() + // Create status if it does not exist + status, ok := statusMap[app.Name] + if !ok { + status = argov1alpha1.ResourceStatus{ + Group: gvk.Group, + Version: gvk.Version, + Kind: gvk.Kind, + Name: app.Name, + Namespace: app.Namespace, + Status: app.Status.Sync.Status, + Health: &appCopy.Status.Health, + } + } + + status.Group = gvk.Group + status.Version = gvk.Version + status.Kind = gvk.Kind + status.Name = app.Name + status.Namespace = app.Namespace + status.Status = app.Status.Sync.Status + status.Health = &appCopy.Status.Health + + statusMap[app.Name] = status + } + cleanupDeletedApplicationStatuses(statusMap, appMap) + + return statusMap +} + +func GetResourceStatusMap(appset *argov1alpha1.ApplicationSet) map[string]argov1alpha1.ResourceStatus { + statusMap := map[string]argov1alpha1.ResourceStatus{} + for _, status := range appset.Status.Resources { + statusMap[status.Name] = status + } + return statusMap +} + +func cleanupDeletedApplicationStatuses(statusMap map[string]argov1alpha1.ResourceStatus, apps map[string]argov1alpha1.Application) { + for name := range statusMap { + if _, ok := apps[name]; !ok { + delete(statusMap, name) + } + } +} diff --git a/applicationset/utils/mocks/renderer.go b/applicationset/utils/mocks/renderer.go new file mode 100644 index 0000000000000..a8b5fccead5a2 --- /dev/null +++ b/applicationset/utils/mocks/renderer.go @@ -0,0 +1,86 @@ +// Code generated by mockery v2.40.2. DO NOT EDIT. + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + + v1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" +) + +// Renderer is an autogenerated mock type for the Renderer type +type Renderer struct { + mock.Mock +} + +// RenderTemplateParams provides a mock function with given fields: tmpl, syncPolicy, params, useGoTemplate, goTemplateOptions +func (_m *Renderer) RenderTemplateParams(tmpl *v1alpha1.Application, syncPolicy *v1alpha1.ApplicationSetSyncPolicy, params map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (*v1alpha1.Application, error) { + ret := _m.Called(tmpl, syncPolicy, params, useGoTemplate, goTemplateOptions) + + if len(ret) == 0 { + panic("no return value specified for RenderTemplateParams") + } + + var r0 *v1alpha1.Application + var r1 error + if rf, ok := ret.Get(0).(func(*v1alpha1.Application, *v1alpha1.ApplicationSetSyncPolicy, map[string]interface{}, bool, []string) (*v1alpha1.Application, error)); ok { + return rf(tmpl, syncPolicy, params, useGoTemplate, goTemplateOptions) + } + if rf, ok := ret.Get(0).(func(*v1alpha1.Application, *v1alpha1.ApplicationSetSyncPolicy, map[string]interface{}, bool, []string) *v1alpha1.Application); ok { + r0 = rf(tmpl, syncPolicy, params, useGoTemplate, goTemplateOptions) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1alpha1.Application) + } + } + + if rf, ok := ret.Get(1).(func(*v1alpha1.Application, *v1alpha1.ApplicationSetSyncPolicy, map[string]interface{}, bool, []string) error); ok { + r1 = rf(tmpl, syncPolicy, params, useGoTemplate, goTemplateOptions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Replace provides a mock function with given fields: tmpl, replaceMap, useGoTemplate, goTemplateOptions +func (_m *Renderer) Replace(tmpl string, replaceMap map[string]interface{}, useGoTemplate bool, goTemplateOptions []string) (string, error) { + ret := _m.Called(tmpl, replaceMap, useGoTemplate, goTemplateOptions) + + if len(ret) == 0 { + panic("no return value specified for Replace") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(string, map[string]interface{}, bool, []string) (string, error)); ok { + return rf(tmpl, replaceMap, useGoTemplate, goTemplateOptions) + } + if rf, ok := ret.Get(0).(func(string, map[string]interface{}, bool, []string) string); ok { + r0 = rf(tmpl, replaceMap, useGoTemplate, goTemplateOptions) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(string, map[string]interface{}, bool, []string) error); ok { + r1 = rf(tmpl, replaceMap, useGoTemplate, goTemplateOptions) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewRenderer creates a new instance of Renderer. 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 NewRenderer(t interface { + mock.TestingT + Cleanup(func()) +}) *Renderer { + mock := &Renderer{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/applicationset/utils/utils.go b/applicationset/utils/utils.go index dfcc11cbdd35a..c387c2c47ebb6 100644 --- a/applicationset/utils/utils.go +++ b/applicationset/utils/utils.go @@ -25,6 +25,8 @@ import ( argoappsv1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" ) +//go:generate go run github.com/vektra/mockery/v2@v2.40.2 --name=Renderer + var sprigFuncMap = sprig.GenericFuncMap() // a singleton for better performance func init() { diff --git a/assets/swagger.json b/assets/swagger.json index 7d863dc71a028..fb3f457322cac 100644 --- a/assets/swagger.json +++ b/assets/swagger.json @@ -1967,6 +1967,11 @@ "type": "boolean", "name": "upsert", "in": "query" + }, + { + "type": "boolean", + "name": "dryRun", + "in": "query" } ], "responses": { @@ -6960,7 +6965,7 @@ }, "serverVersion": { "type": "string", - "title": "DEPRECATED: use Info.ServerVersion field instead.\nThe server version" + "title": "Deprecated: use Info.ServerVersion field instead.\nThe server version" }, "shard": { "description": "Shard contains optional shard number. Calculated on the fly by the application controller if not specified.", diff --git a/cmd/argocd-applicationset-controller/commands/applicationset_controller.go b/cmd/argocd-applicationset-controller/commands/applicationset_controller.go index b90bd7dbd1f65..5d4c8fe15c487 100644 --- a/cmd/argocd-applicationset-controller/commands/applicationset_controller.go +++ b/cmd/argocd-applicationset-controller/commands/applicationset_controller.go @@ -152,9 +152,7 @@ func NewCommand() *cobra.Command { appSetConfig := appclientset.NewForConfigOrDie(mgr.GetConfig()) argoCDDB := db.NewDB(namespace, argoSettingsMgr, k8sClient) - scmAuth := generators.SCMAuthProviders{ - GitHubApps: github_app.NewAuthCredentials(argoCDDB.(db.RepoCredsDB)), - } + scmConfig := generators.NewSCMConfig(scmRootCAPath, allowedScmProviders, enableScmProviders, github_app.NewAuthCredentials(argoCDDB.(db.RepoCredsDB))) tlsConfig := apiclient.TLSConfiguration{ DisableTLS: repoServerPlaintext, @@ -174,39 +172,7 @@ func NewCommand() *cobra.Command { argoCDService, err := services.NewArgoCDService(argoCDDB.GetRepository, gitSubmoduleEnabled, repoClientset, enableNewGitFileGlobbing) errors.CheckError(err) - terminalGenerators := map[string]generators.Generator{ - "List": generators.NewListGenerator(), - "Clusters": generators.NewClusterGenerator(mgr.GetClient(), ctx, k8sClient, namespace), - "Git": generators.NewGitGenerator(argoCDService), - "SCMProvider": generators.NewSCMProviderGenerator(mgr.GetClient(), scmAuth, scmRootCAPath, allowedScmProviders, enableScmProviders), - "ClusterDecisionResource": generators.NewDuckTypeGenerator(ctx, dynamicClient, k8sClient, namespace), - "PullRequest": generators.NewPullRequestGenerator(mgr.GetClient(), scmAuth, scmRootCAPath, allowedScmProviders, enableScmProviders), - "Plugin": generators.NewPluginGenerator(mgr.GetClient(), ctx, k8sClient, namespace), - } - - nestedGenerators := map[string]generators.Generator{ - "List": terminalGenerators["List"], - "Clusters": terminalGenerators["Clusters"], - "Git": terminalGenerators["Git"], - "SCMProvider": terminalGenerators["SCMProvider"], - "ClusterDecisionResource": terminalGenerators["ClusterDecisionResource"], - "PullRequest": terminalGenerators["PullRequest"], - "Plugin": terminalGenerators["Plugin"], - "Matrix": generators.NewMatrixGenerator(terminalGenerators), - "Merge": generators.NewMergeGenerator(terminalGenerators), - } - - topLevelGenerators := map[string]generators.Generator{ - "List": terminalGenerators["List"], - "Clusters": terminalGenerators["Clusters"], - "Git": terminalGenerators["Git"], - "SCMProvider": terminalGenerators["SCMProvider"], - "ClusterDecisionResource": terminalGenerators["ClusterDecisionResource"], - "PullRequest": terminalGenerators["PullRequest"], - "Plugin": terminalGenerators["Plugin"], - "Matrix": generators.NewMatrixGenerator(nestedGenerators), - "Merge": generators.NewMergeGenerator(nestedGenerators), - } + topLevelGenerators := generators.GetGenerators(ctx, mgr.GetClient(), k8sClient, namespace, argoCDService, dynamicClient, scmConfig) // start a webhook server that listens to incoming webhook payloads webhookHandler, err := webhook.NewWebhookHandler(namespace, argoSettingsMgr, mgr.GetClient(), topLevelGenerators) diff --git a/cmd/argocd-server/commands/argocd_server.go b/cmd/argocd-server/commands/argocd_server.go index 707cc2d80eaf5..e3ba8141c51c8 100644 --- a/cmd/argocd-server/commands/argocd_server.go +++ b/cmd/argocd-server/commands/argocd_server.go @@ -12,8 +12,10 @@ import ( "github.com/argoproj/pkg/stats" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" cmdutil "github.com/argoproj/argo-cd/v2/cmd/util" "github.com/argoproj/argo-cd/v2/common" @@ -42,6 +44,7 @@ const ( var ( failureRetryCount = env.ParseNumFromEnv(failureRetryCountEnv, 0, 0, 10) failureRetryPeriodMilliSeconds = env.ParseNumFromEnv(failureRetryPeriodMilliSecondsEnv, 100, 0, 1000) + gitSubmoduleEnabled = env.ParseBoolFromEnv(common.EnvGitSubmoduleEnabled, true) ) // NewCommand returns a new instance of an argocd command @@ -79,6 +82,12 @@ func NewCommand() *cobra.Command { staticAssetsDir string applicationNamespaces []string enableProxyExtension bool + + // ApplicationSet + enableNewGitFileGlobbing bool + scmRootCAPath string + allowedScmProviders []string + enableScmProviders bool ) command := &cobra.Command{ Use: cliName, @@ -130,6 +139,12 @@ func NewCommand() *cobra.Command { StrictValidation: repoServerStrictTLS, } + dynamicClient := dynamic.NewForConfigOrDie(config) + + controllerClient, err := client.New(config, client.Options{}) + errors.CheckError(err) + controllerClient = client.NewDryRunClient(controllerClient) + // Load CA information to use for validating connections to the // repository server, if strict TLS validation was requested. if !repoServerPlaintext && repoServerStrictTLS { @@ -179,37 +194,47 @@ func NewCommand() *cobra.Command { } argoCDOpts := server.ArgoCDServerOpts{ - Insecure: insecure, - ListenPort: listenPort, - ListenHost: listenHost, - MetricsPort: metricsPort, - MetricsHost: metricsHost, - Namespace: namespace, - BaseHRef: baseHRef, - RootPath: rootPath, - KubeClientset: kubeclientset, - AppClientset: appClientSet, - RepoClientset: repoclientset, - DexServerAddr: dexServerAddress, - DexTLSConfig: dexTlsConfig, - DisableAuth: disableAuth, - ContentTypes: contentTypesList, - EnableGZip: enableGZip, - TLSConfigCustomizer: tlsConfigCustomizer, - Cache: cache, - RepoServerCache: repoServerCache, - XFrameOptions: frameOptions, - ContentSecurityPolicy: contentSecurityPolicy, - RedisClient: redisClient, - StaticAssetsDir: staticAssetsDir, - ApplicationNamespaces: applicationNamespaces, - EnableProxyExtension: enableProxyExtension, + Insecure: insecure, + ListenPort: listenPort, + ListenHost: listenHost, + MetricsPort: metricsPort, + MetricsHost: metricsHost, + Namespace: namespace, + BaseHRef: baseHRef, + RootPath: rootPath, + DynamicClientset: dynamicClient, + KubeControllerClientset: controllerClient, + KubeClientset: kubeclientset, + AppClientset: appClientSet, + RepoClientset: repoclientset, + DexServerAddr: dexServerAddress, + DexTLSConfig: dexTlsConfig, + DisableAuth: disableAuth, + ContentTypes: contentTypesList, + EnableGZip: enableGZip, + TLSConfigCustomizer: tlsConfigCustomizer, + Cache: cache, + RepoServerCache: repoServerCache, + XFrameOptions: frameOptions, + ContentSecurityPolicy: contentSecurityPolicy, + RedisClient: redisClient, + StaticAssetsDir: staticAssetsDir, + ApplicationNamespaces: applicationNamespaces, + EnableProxyExtension: enableProxyExtension, + } + + appsetOpts := server.ApplicationSetOpts{ + GitSubmoduleEnabled: gitSubmoduleEnabled, + EnableNewGitFileGlobbing: enableNewGitFileGlobbing, + ScmRootCAPath: scmRootCAPath, + AllowedScmProviders: allowedScmProviders, + EnableScmProviders: enableScmProviders, } stats.RegisterStackDumper() stats.StartStatsTicker(10 * time.Minute) stats.RegisterHeapDumper("memprofile") - argocd := server.NewServer(ctx, argoCDOpts) + argocd := server.NewServer(ctx, argoCDOpts, appsetOpts) argocd.Init(ctx) lns, err := argocd.Listen() errors.CheckError(err) @@ -232,7 +257,7 @@ func NewCommand() *cobra.Command { Example: templates.Examples(` # Start the Argo CD API server with default settings $ argocd-server - + # Start the Argo CD API server on a custom port and enable tracing $ argocd-server --port 8888 --otlp-address localhost:4317 `), @@ -269,6 +294,13 @@ func NewCommand() *cobra.Command { command.Flags().BoolVar(&dexServerStrictTLS, "dex-server-strict-tls", env.ParseBoolFromEnv("ARGOCD_SERVER_DEX_SERVER_STRICT_TLS", false), "Perform strict validation of TLS certificates when connecting to dex server") command.Flags().StringSliceVar(&applicationNamespaces, "application-namespaces", env.StringsFromEnv("ARGOCD_APPLICATION_NAMESPACES", []string{}, ","), "List of additional namespaces where application resources can be managed in") command.Flags().BoolVar(&enableProxyExtension, "enable-proxy-extension", env.ParseBoolFromEnv("ARGOCD_SERVER_ENABLE_PROXY_EXTENSION", false), "Enable Proxy Extension feature") + + // Flags related to the applicationSet component. + command.Flags().StringVar(&scmRootCAPath, "appset-scm-root-ca-path", env.StringFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_SCM_ROOT_CA_PATH", ""), "Provide Root CA Path for self-signed TLS Certificates") + command.Flags().BoolVar(&enableScmProviders, "appset-enable-scm-providers", env.ParseBoolFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_SCM_PROVIDERS", true), "Enable retrieving information from SCM providers, used by the SCM and PR generators (Default: true)") + command.Flags().StringSliceVar(&allowedScmProviders, "appset-allowed-scm-providers", env.StringsFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_ALLOWED_SCM_PROVIDERS", []string{}, ","), "The list of allowed custom SCM provider API URLs. This restriction does not apply to SCM or PR generators which do not accept a custom API URL. (Default: Empty = all)") + command.Flags().BoolVar(&enableNewGitFileGlobbing, "appset-enable-new-git-file-globbing", env.ParseBoolFromEnv("ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_NEW_GIT_FILE_GLOBBING", false), "Enable new globbing in Git files generator.") + tlsConfigCustomizerSrc = tls.AddTLSFlagsToCmd(command) cacheSrc = servercache.AddCacheFlagsToCmd(command, cacheutil.Options{ OnClientCreated: func(client *redis.Client) { diff --git a/cmd/argocd/commands/applicationset.go b/cmd/argocd/commands/applicationset.go index ed125629b9393..bd9dff90891a0 100644 --- a/cmd/argocd/commands/applicationset.go +++ b/cmd/argocd/commands/applicationset.go @@ -114,13 +114,17 @@ func NewApplicationSetGetCommand(clientOpts *argocdclient.ClientOptions) *cobra. // NewApplicationSetCreateCommand returns a new instance of an `argocd appset create` command func NewApplicationSetCreateCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command { - var upsert bool + var output string + var upsert, dryRun bool command := &cobra.Command{ Use: "create", Short: "Create one or more ApplicationSets", Example: templates.Examples(` # Create ApplicationSets argocd appset create (...) + + # Dry-run AppSet creation to see what applications would be managed + argocd appset create --dry-run -o json | jq -r '.status.resources[].name' `), Run: func(c *cobra.Command, args []string) { ctx := c.Context() @@ -157,10 +161,16 @@ func NewApplicationSetCreateCommand(clientOpts *argocdclient.ClientOptions) *cob appSetCreateRequest := applicationset.ApplicationSetCreateRequest{ Applicationset: appset, Upsert: upsert, + DryRun: dryRun, } created, err := appIf.Create(ctx, &appSetCreateRequest) errors.CheckError(err) + dryRunMsg := "" + if dryRun { + dryRunMsg = " (dry-run)" + } + var action string if existing == nil { action = "created" @@ -170,11 +180,31 @@ func NewApplicationSetCreateCommand(clientOpts *argocdclient.ClientOptions) *cob action = "updated" } - fmt.Printf("ApplicationSet '%s' %s\n", created.ObjectMeta.Name, action) + c.PrintErrf("ApplicationSet '%s' %s%s\n", created.ObjectMeta.Name, action, dryRunMsg) + + switch output { + case "yaml", "json": + err := PrintResource(created, output) + errors.CheckError(err) + case "wide", "": + printAppSetSummaryTable(created) + + if len(created.Status.Conditions) > 0 { + fmt.Println() + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + printAppSetConditions(w, created) + _ = w.Flush() + fmt.Println() + } + default: + errors.CheckError(fmt.Errorf("unknown output format: %s", output)) + } } }, } command.Flags().BoolVar(&upsert, "upsert", false, "Allows to override ApplicationSet with the same name even if supplied ApplicationSet spec is different from existing spec") + command.Flags().BoolVar(&dryRun, "dry-run", false, "Allows to evaluate the ApplicationSet template on the server to get a preview of the applications that would be created") + command.Flags().StringVarP(&output, "output", "o", "wide", "Output format. One of: json|yaml|wide") return command } @@ -189,7 +219,7 @@ func NewApplicationSetListCommand(clientOpts *argocdclient.ClientOptions) *cobra command := &cobra.Command{ Use: "list", Short: "List ApplicationSets", - Example: templates.Examples(` + Example: templates.Examples(` # List all ApplicationSets argocd appset list `), @@ -230,7 +260,7 @@ func NewApplicationSetDeleteCommand(clientOpts *argocdclient.ClientOptions) *cob command := &cobra.Command{ Use: "delete", Short: "Delete one or more ApplicationSets", - Example: templates.Examples(` + Example: templates.Examples(` # Delete an applicationset argocd appset delete APPSETNAME (APPSETNAME...) `), diff --git a/cmd/argocd/commands/headless/headless.go b/cmd/argocd/commands/headless/headless.go index d148e527abab4..9ef70ae1c4748 100644 --- a/cmd/argocd/commands/headless/headless.go +++ b/cmd/argocd/commands/headless/headless.go @@ -9,6 +9,7 @@ import ( "time" "github.com/spf13/cobra" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/argoproj/argo-cd/v2/cmd/argocd/commands/initialize" "github.com/argoproj/argo-cd/v2/common" @@ -20,6 +21,7 @@ import ( "github.com/spf13/pflag" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" cache2 "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/clientcmd" @@ -231,6 +233,17 @@ func MaybeStartLocalServer(ctx context.Context, clientOpts *apiclient.ClientOpti return fmt.Errorf("error creating kubernetes clientset: %w", err) } + dynamicClientset, err := dynamic.NewForConfig(restConfig) + if err != nil { + return fmt.Errorf("error creating kubernetes dynamic clientset: %w", err) + } + + controllerClientset, err := client.New(restConfig, client.Options{}) + if err != nil { + return fmt.Errorf("error creating kubernetes controller clientset: %w", err) + } + controllerClientset = client.NewDryRunClient(controllerClientset) + namespace, _, err := clientConfig.Namespace() if err != nil { return fmt.Errorf("error getting namespace: %w", err) @@ -242,19 +255,21 @@ func MaybeStartLocalServer(ctx context.Context, clientOpts *apiclient.ClientOpti } appstateCache := appstatecache.NewCache(cache.NewCache(&forwardCacheClient{namespace: namespace, context: ctxStr, compression: compression, redisHaProxyName: clientOpts.RedisHaProxyName, redisName: clientOpts.RedisName}), time.Hour) srv := server.NewServer(ctx, server.ArgoCDServerOpts{ - EnableGZip: false, - Namespace: namespace, - ListenPort: *port, - AppClientset: appClientset, - DisableAuth: true, - RedisClient: redis.NewClient(&redis.Options{Addr: mr.Addr()}), - Cache: servercache.NewCache(appstateCache, 0, 0, 0), - KubeClientset: kubeClientset, - Insecure: true, - ListenHost: *address, - RepoClientset: &forwardRepoClientset{namespace: namespace, context: ctxStr, repoServerName: clientOpts.RepoServerName, kubeClientset: kubeClientset}, - EnableProxyExtension: false, - }) + EnableGZip: false, + Namespace: namespace, + ListenPort: *port, + AppClientset: appClientset, + DisableAuth: true, + RedisClient: redis.NewClient(&redis.Options{Addr: mr.Addr()}), + Cache: servercache.NewCache(appstateCache, 0, 0, 0), + KubeClientset: kubeClientset, + DynamicClientset: dynamicClientset, + KubeControllerClientset: controllerClientset, + Insecure: true, + ListenHost: *address, + RepoClientset: &forwardRepoClientset{namespace: namespace, context: ctxStr, repoServerName: clientOpts.RepoServerName, kubeClientset: kubeClientset}, + EnableProxyExtension: false, + }, server.ApplicationSetOpts{}) srv.Init(ctx) lns, err := srv.Listen() diff --git a/docs/operator-manual/applicationset/Controlling-Resource-Modification.md b/docs/operator-manual/applicationset/Controlling-Resource-Modification.md index ae65fa3462e5b..1636d348cb009 100644 --- a/docs/operator-manual/applicationset/Controlling-Resource-Modification.md +++ b/docs/operator-manual/applicationset/Controlling-Resource-Modification.md @@ -343,3 +343,15 @@ metadata: data: applicationsetcontroller.log.level: debug ``` + +## Previewing changes + +To preview changes that the ApplicationSet controller would make to Applications, you can create the AppSet in dry-run +mode. This works whether the AppSet already exists or not. + +```shell +argocd appset create --dry-run ./appset.yaml -o json | jq -r '.status.resources[].name' +``` + +The dry-run will populate the returned ApplicationSet's status with the Applications which would be managed with the +given config. You can compare to the existing Applications to see what would change. diff --git a/docs/operator-manual/server-commands/argocd-server.md b/docs/operator-manual/server-commands/argocd-server.md index 5b3fd72ebff00..882603d080d89 100644 --- a/docs/operator-manual/server-commands/argocd-server.md +++ b/docs/operator-manual/server-commands/argocd-server.md @@ -29,6 +29,10 @@ argocd-server [flags] --api-content-types string Semicolon separated list of allowed content types for non GET api requests. Any content type is allowed if empty. (default "application/json") --app-state-cache-expiration duration Cache expiration for app state (default 1h0m0s) --application-namespaces strings List of additional namespaces where application resources can be managed in + --appset-allowed-scm-providers strings The list of allowed custom SCM provider API URLs. This restriction does not apply to SCM or PR generators which do not accept a custom API URL. (Default: Empty = all) + --appset-enable-new-git-file-globbing Enable new globbing in Git files generator. + --appset-enable-scm-providers Enable retrieving information from SCM providers, used by the SCM and PR generators (Default: true) (default true) + --appset-scm-root-ca-path string Provide Root CA Path for self-signed TLS Certificates --as string Username to impersonate for the operation --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. --as-uid string UID to impersonate for the operation diff --git a/docs/user-guide/commands/argocd_appset_create.md b/docs/user-guide/commands/argocd_appset_create.md index fccc03fcc971c..70c9d6b61c84f 100644 --- a/docs/user-guide/commands/argocd_appset_create.md +++ b/docs/user-guide/commands/argocd_appset_create.md @@ -13,13 +13,18 @@ argocd appset create [flags] ``` # Create ApplicationSets argocd appset create (...) + + # Dry-run AppSet creation to see what applications would be managed + argocd appset create --dry-run -o json | jq -r '.status.resources[].name' ``` ### Options ``` - -h, --help help for create - --upsert Allows to override ApplicationSet with the same name even if supplied ApplicationSet spec is different from existing spec + --dry-run Allows to evaluate the ApplicationSet template on the server to get a preview of the applications that would be created + -h, --help help for create + -o, --output string Output format. One of: json|yaml|wide (default "wide") + --upsert Allows to override ApplicationSet with the same name even if supplied ApplicationSet spec is different from existing spec ``` ### Options inherited from parent commands diff --git a/manifests/base/server/argocd-server-deployment.yaml b/manifests/base/server/argocd-server-deployment.yaml index 1107323b2e3b9..b9486e7b4b2c6 100644 --- a/manifests/base/server/argocd-server-deployment.yaml +++ b/manifests/base/server/argocd-server-deployment.yaml @@ -17,332 +17,356 @@ spec: spec: serviceAccountName: argocd-server containers: - - name: argocd-server - image: quay.io/argoproj/argocd:latest - imagePullPolicy: Always - args: - - /usr/local/bin/argocd-server - env: - - name: REDIS_PASSWORD - valueFrom: - secretKeyRef: - key: auth - name: argocd-redis - - name: ARGOCD_SERVER_INSECURE - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.insecure - optional: true - - name: ARGOCD_SERVER_BASEHREF - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.basehref - optional: true - - name: ARGOCD_SERVER_ROOTPATH - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.rootpath - optional: true - - name: ARGOCD_SERVER_LOGFORMAT - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.log.format - optional: true - - name: ARGOCD_SERVER_LOG_LEVEL - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.log.level - optional: true - - name: ARGOCD_SERVER_REPO_SERVER - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: repo.server - optional: true - - name: ARGOCD_SERVER_DEX_SERVER - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.dex.server - optional: true - - name: ARGOCD_SERVER_DISABLE_AUTH - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.disable.auth - optional: true - - name: ARGOCD_SERVER_ENABLE_GZIP - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.enable.gzip - optional: true - - name: ARGOCD_SERVER_REPO_SERVER_TIMEOUT_SECONDS - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.repo.server.timeout.seconds - optional: true - - name: ARGOCD_SERVER_X_FRAME_OPTIONS - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.x.frame.options - optional: true - - name: ARGOCD_SERVER_CONTENT_SECURITY_POLICY - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.content.security.policy - optional: true - - name: ARGOCD_SERVER_REPO_SERVER_PLAINTEXT - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.repo.server.plaintext - optional: true - - name: ARGOCD_SERVER_REPO_SERVER_STRICT_TLS - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.repo.server.strict.tls - optional: true - - name: ARGOCD_SERVER_DEX_SERVER_PLAINTEXT - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.dex.server.plaintext - optional: true - - name: ARGOCD_SERVER_DEX_SERVER_STRICT_TLS - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.dex.server.strict.tls - optional: true - - name: ARGOCD_TLS_MIN_VERSION - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.tls.minversion - optional: true - - name: ARGOCD_TLS_MAX_VERSION - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.tls.maxversion - optional: true - - name: ARGOCD_TLS_CIPHERS - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.tls.ciphers - optional: true - - name: ARGOCD_SERVER_CONNECTION_STATUS_CACHE_EXPIRATION - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.connection.status.cache.expiration - optional: true - - name: ARGOCD_SERVER_OIDC_CACHE_EXPIRATION - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.oidc.cache.expiration - optional: true - - name: ARGOCD_SERVER_LOGIN_ATTEMPTS_EXPIRATION - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.login.attempts.expiration - optional: true - - name: ARGOCD_SERVER_STATIC_ASSETS - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.staticassets - optional: true - - name: ARGOCD_APP_STATE_CACHE_EXPIRATION - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.app.state.cache.expiration - optional: true - - name: REDIS_SERVER - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: redis.server - optional: true - - name: REDIS_COMPRESSION - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: redis.compression - optional: true - - name: REDISDB - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: redis.db - optional: true - - name: ARGOCD_DEFAULT_CACHE_EXPIRATION - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.default.cache.expiration - optional: true - - name: ARGOCD_MAX_COOKIE_NUMBER - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.http.cookie.maxnumber - optional: true - - name: ARGOCD_SERVER_LISTEN_ADDRESS - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.listen.address - optional: true - - name: ARGOCD_SERVER_METRICS_LISTEN_ADDRESS - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.metrics.listen.address - optional: true - - name: ARGOCD_SERVER_OTLP_ADDRESS - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: otlp.address - optional: true - - name: ARGOCD_SERVER_OTLP_INSECURE - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: otlp.insecure - optional: true - - name: ARGOCD_SERVER_OTLP_HEADERS - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: otlp.headers - optional: true - - name: ARGOCD_APPLICATION_NAMESPACES - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: application.namespaces - optional: true - - name: ARGOCD_SERVER_ENABLE_PROXY_EXTENSION - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.enable.proxy.extension - optional: true - - name: ARGOCD_K8SCLIENT_RETRY_MAX - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.k8sclient.retry.max - optional: true - - name: ARGOCD_K8SCLIENT_RETRY_BASE_BACKOFF - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.k8sclient.retry.base.backoff - optional: true - - name: ARGOCD_API_CONTENT_TYPES - valueFrom: - configMapKeyRef: - name: argocd-cmd-params-cm - key: server.api.content.types - optional: true - volumeMounts: + - name: argocd-server + image: quay.io/argoproj/argocd:latest + imagePullPolicy: Always + args: + - /usr/local/bin/argocd-server + env: + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + key: auth + name: argocd-redis + - name: ARGOCD_SERVER_INSECURE + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.insecure + optional: true + - name: ARGOCD_SERVER_BASEHREF + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.basehref + optional: true + - name: ARGOCD_SERVER_ROOTPATH + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.rootpath + optional: true + - name: ARGOCD_SERVER_LOGFORMAT + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.log.format + optional: true + - name: ARGOCD_SERVER_LOG_LEVEL + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.log.level + optional: true + - name: ARGOCD_SERVER_REPO_SERVER + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: repo.server + optional: true + - name: ARGOCD_SERVER_DEX_SERVER + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.dex.server + optional: true + - name: ARGOCD_SERVER_DISABLE_AUTH + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.disable.auth + optional: true + - name: ARGOCD_SERVER_ENABLE_GZIP + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.enable.gzip + optional: true + - name: ARGOCD_SERVER_REPO_SERVER_TIMEOUT_SECONDS + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.repo.server.timeout.seconds + optional: true + - name: ARGOCD_SERVER_X_FRAME_OPTIONS + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.x.frame.options + optional: true + - name: ARGOCD_SERVER_CONTENT_SECURITY_POLICY + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.content.security.policy + optional: true + - name: ARGOCD_SERVER_REPO_SERVER_PLAINTEXT + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.repo.server.plaintext + optional: true + - name: ARGOCD_SERVER_REPO_SERVER_STRICT_TLS + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.repo.server.strict.tls + optional: true + - name: ARGOCD_SERVER_DEX_SERVER_PLAINTEXT + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.dex.server.plaintext + optional: true + - name: ARGOCD_SERVER_DEX_SERVER_STRICT_TLS + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.dex.server.strict.tls + optional: true + - name: ARGOCD_TLS_MIN_VERSION + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.tls.minversion + optional: true + - name: ARGOCD_TLS_MAX_VERSION + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.tls.maxversion + optional: true + - name: ARGOCD_TLS_CIPHERS + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.tls.ciphers + optional: true + - name: ARGOCD_SERVER_CONNECTION_STATUS_CACHE_EXPIRATION + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.connection.status.cache.expiration + optional: true + - name: ARGOCD_SERVER_OIDC_CACHE_EXPIRATION + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.oidc.cache.expiration + optional: true + - name: ARGOCD_SERVER_LOGIN_ATTEMPTS_EXPIRATION + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.login.attempts.expiration + optional: true + - name: ARGOCD_SERVER_STATIC_ASSETS + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.staticassets + optional: true + - name: ARGOCD_APP_STATE_CACHE_EXPIRATION + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.app.state.cache.expiration + optional: true + - name: REDIS_SERVER + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: redis.server + optional: true + - name: REDIS_COMPRESSION + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: redis.compression + optional: true + - name: REDISDB + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: redis.db + optional: true + - name: ARGOCD_DEFAULT_CACHE_EXPIRATION + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.default.cache.expiration + optional: true + - name: ARGOCD_MAX_COOKIE_NUMBER + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.http.cookie.maxnumber + optional: true + - name: ARGOCD_SERVER_LISTEN_ADDRESS + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.listen.address + optional: true + - name: ARGOCD_SERVER_METRICS_LISTEN_ADDRESS + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.metrics.listen.address + optional: true + - name: ARGOCD_SERVER_OTLP_ADDRESS + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: otlp.address + optional: true + - name: ARGOCD_SERVER_OTLP_INSECURE + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: otlp.insecure + optional: true + - name: ARGOCD_SERVER_OTLP_HEADERS + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: otlp.headers + optional: true + - name: ARGOCD_APPLICATION_NAMESPACES + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: application.namespaces + optional: true + - name: ARGOCD_SERVER_ENABLE_PROXY_EXTENSION + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.enable.proxy.extension + optional: true + - name: ARGOCD_K8SCLIENT_RETRY_MAX + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.k8sclient.retry.max + optional: true + - name: ARGOCD_K8SCLIENT_RETRY_BASE_BACKOFF + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.k8sclient.retry.base.backoff + optional: true + - name: ARGOCD_API_CONTENT_TYPES + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: server.api.content.types + optional: true + - name: ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_NEW_GIT_FILE_GLOBBING + valueFrom: + configMapKeyRef: + key: applicationsetcontroller.enable.new.git.file.globbing + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_APPLICATIONSET_CONTROLLER_SCM_ROOT_CA_PATH + valueFrom: + configMapKeyRef: + key: applicationsetcontroller.scm.root.ca.path + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_APPLICATIONSET_CONTROLLER_ALLOWED_SCM_PROVIDERS + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: applicationsetcontroller.allowed.scm.providers + optional: true + - name: ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_SCM_PROVIDERS + valueFrom: + configMapKeyRef: + name: argocd-cmd-params-cm + key: applicationsetcontroller.enable.scm.providers + optional: true + volumeMounts: + - name: ssh-known-hosts + mountPath: /app/config/ssh + - name: tls-certs + mountPath: /app/config/tls + - name: argocd-repo-server-tls + mountPath: /app/config/server/tls + - name: argocd-dex-server-tls + mountPath: /app/config/dex/tls + - mountPath: /home/argocd + name: plugins-home + - mountPath: /tmp + name: tmp + ports: + - containerPort: 8080 + - containerPort: 8083 + livenessProbe: + httpGet: + path: /healthz?full=true + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 30 + timeoutSeconds: 5 + readinessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 30 + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + volumes: + - emptyDir: {} + name: plugins-home + - emptyDir: {} + name: tmp - name: ssh-known-hosts - mountPath: /app/config/ssh + configMap: + name: argocd-ssh-known-hosts-cm - name: tls-certs - mountPath: /app/config/tls + configMap: + name: argocd-tls-certs-cm - name: argocd-repo-server-tls - mountPath: /app/config/server/tls + secret: + secretName: argocd-repo-server-tls + optional: true + items: + - key: tls.crt + path: tls.crt + - key: tls.key + path: tls.key + - key: ca.crt + path: ca.crt - name: argocd-dex-server-tls - mountPath: /app/config/dex/tls - - mountPath: /home/argocd - name: plugins-home - - mountPath: /tmp - name: tmp - ports: - - containerPort: 8080 - - containerPort: 8083 - livenessProbe: - httpGet: - path: /healthz?full=true - port: 8080 - initialDelaySeconds: 3 - periodSeconds: 30 - timeoutSeconds: 5 - readinessProbe: - httpGet: - path: /healthz - port: 8080 - initialDelaySeconds: 3 - periodSeconds: 30 - securityContext: - allowPrivilegeEscalation: false - readOnlyRootFilesystem: true - runAsNonRoot: true - capabilities: - drop: - - ALL - seccompProfile: - type: RuntimeDefault - volumes: - - emptyDir: {} - name: plugins-home - - emptyDir: {} - name: tmp - - name: ssh-known-hosts - configMap: - name: argocd-ssh-known-hosts-cm - - name: tls-certs - configMap: - name: argocd-tls-certs-cm - - name: argocd-repo-server-tls - secret: - secretName: argocd-repo-server-tls - optional: true - items: - - key: tls.crt - path: tls.crt - - key: tls.key - path: tls.key - - key: ca.crt - path: ca.crt - - name: argocd-dex-server-tls - secret: - secretName: argocd-dex-server-tls - optional: true - items: - - key: tls.crt - path: tls.crt - - key: ca.crt - path: ca.crt + secret: + secretName: argocd-dex-server-tls + optional: true + items: + - key: tls.crt + path: tls.crt + - key: ca.crt + path: ca.crt affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - - weight: 100 - podAffinityTerm: - labelSelector: - matchLabels: - app.kubernetes.io/name: argocd-server - topologyKey: kubernetes.io/hostname - - weight: 5 - podAffinityTerm: - labelSelector: - matchLabels: - app.kubernetes.io/part-of: argocd - topologyKey: kubernetes.io/hostname + - weight: 100 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: argocd-server + topologyKey: kubernetes.io/hostname + - weight: 5 + podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/part-of: argocd + topologyKey: kubernetes.io/hostname diff --git a/manifests/ha/install.yaml b/manifests/ha/install.yaml index d77bb1b84fabb..31c131e973b7f 100644 --- a/manifests/ha/install.yaml +++ b/manifests/ha/install.yaml @@ -23590,6 +23590,30 @@ spec: key: server.api.content.types name: argocd-cmd-params-cm optional: true + - name: ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_NEW_GIT_FILE_GLOBBING + valueFrom: + configMapKeyRef: + key: applicationsetcontroller.enable.new.git.file.globbing + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_APPLICATIONSET_CONTROLLER_SCM_ROOT_CA_PATH + valueFrom: + configMapKeyRef: + key: applicationsetcontroller.scm.root.ca.path + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_APPLICATIONSET_CONTROLLER_ALLOWED_SCM_PROVIDERS + valueFrom: + configMapKeyRef: + key: applicationsetcontroller.allowed.scm.providers + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_SCM_PROVIDERS + valueFrom: + configMapKeyRef: + key: applicationsetcontroller.enable.scm.providers + name: argocd-cmd-params-cm + optional: true image: quay.io/argoproj/argocd:latest imagePullPolicy: Always livenessProbe: diff --git a/manifests/ha/namespace-install.yaml b/manifests/ha/namespace-install.yaml index bd0f32fdadd70..afedf6b961f04 100644 --- a/manifests/ha/namespace-install.yaml +++ b/manifests/ha/namespace-install.yaml @@ -2667,6 +2667,30 @@ spec: key: server.api.content.types name: argocd-cmd-params-cm optional: true + - name: ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_NEW_GIT_FILE_GLOBBING + valueFrom: + configMapKeyRef: + key: applicationsetcontroller.enable.new.git.file.globbing + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_APPLICATIONSET_CONTROLLER_SCM_ROOT_CA_PATH + valueFrom: + configMapKeyRef: + key: applicationsetcontroller.scm.root.ca.path + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_APPLICATIONSET_CONTROLLER_ALLOWED_SCM_PROVIDERS + valueFrom: + configMapKeyRef: + key: applicationsetcontroller.allowed.scm.providers + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_SCM_PROVIDERS + valueFrom: + configMapKeyRef: + key: applicationsetcontroller.enable.scm.providers + name: argocd-cmd-params-cm + optional: true image: quay.io/argoproj/argocd:latest imagePullPolicy: Always livenessProbe: diff --git a/manifests/install.yaml b/manifests/install.yaml index 585483dbc76b0..b56f483cf97a4 100644 --- a/manifests/install.yaml +++ b/manifests/install.yaml @@ -22658,6 +22658,30 @@ spec: key: server.api.content.types name: argocd-cmd-params-cm optional: true + - name: ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_NEW_GIT_FILE_GLOBBING + valueFrom: + configMapKeyRef: + key: applicationsetcontroller.enable.new.git.file.globbing + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_APPLICATIONSET_CONTROLLER_SCM_ROOT_CA_PATH + valueFrom: + configMapKeyRef: + key: applicationsetcontroller.scm.root.ca.path + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_APPLICATIONSET_CONTROLLER_ALLOWED_SCM_PROVIDERS + valueFrom: + configMapKeyRef: + key: applicationsetcontroller.allowed.scm.providers + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_SCM_PROVIDERS + valueFrom: + configMapKeyRef: + key: applicationsetcontroller.enable.scm.providers + name: argocd-cmd-params-cm + optional: true image: quay.io/argoproj/argocd:latest imagePullPolicy: Always livenessProbe: diff --git a/manifests/namespace-install.yaml b/manifests/namespace-install.yaml index a1b2b31f0265d..0cc94b9415b89 100644 --- a/manifests/namespace-install.yaml +++ b/manifests/namespace-install.yaml @@ -1735,6 +1735,30 @@ spec: key: server.api.content.types name: argocd-cmd-params-cm optional: true + - name: ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_NEW_GIT_FILE_GLOBBING + valueFrom: + configMapKeyRef: + key: applicationsetcontroller.enable.new.git.file.globbing + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_APPLICATIONSET_CONTROLLER_SCM_ROOT_CA_PATH + valueFrom: + configMapKeyRef: + key: applicationsetcontroller.scm.root.ca.path + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_APPLICATIONSET_CONTROLLER_ALLOWED_SCM_PROVIDERS + valueFrom: + configMapKeyRef: + key: applicationsetcontroller.allowed.scm.providers + name: argocd-cmd-params-cm + optional: true + - name: ARGOCD_APPLICATIONSET_CONTROLLER_ENABLE_SCM_PROVIDERS + valueFrom: + configMapKeyRef: + key: applicationsetcontroller.enable.scm.providers + name: argocd-cmd-params-cm + optional: true image: quay.io/argoproj/argocd:latest imagePullPolicy: Always livenessProbe: diff --git a/pkg/apiclient/applicationset/applicationset.pb.go b/pkg/apiclient/applicationset/applicationset.pb.go index 68db654fe9c4e..b15e27f3ebc10 100644 --- a/pkg/apiclient/applicationset/applicationset.pb.go +++ b/pkg/apiclient/applicationset/applicationset.pb.go @@ -214,6 +214,7 @@ func (m *ApplicationSetResponse) GetApplicationset() *v1alpha1.ApplicationSet { type ApplicationSetCreateRequest struct { Applicationset *v1alpha1.ApplicationSet `protobuf:"bytes,1,opt,name=applicationset,proto3" json:"applicationset,omitempty"` Upsert bool `protobuf:"varint,2,opt,name=upsert,proto3" json:"upsert,omitempty"` + DryRun bool `protobuf:"varint,3,opt,name=dryRun,proto3" json:"dryRun,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -266,6 +267,13 @@ func (m *ApplicationSetCreateRequest) GetUpsert() bool { return false } +func (m *ApplicationSetCreateRequest) GetDryRun() bool { + if m != nil { + return m.DryRun + } + return false +} + type ApplicationSetDeleteRequest struct { Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // The application set namespace. Default empty is argocd control plane namespace @@ -392,43 +400,44 @@ func init() { } var fileDescriptor_eacb9df0ce5738fa = []byte{ - // 573 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x95, 0x4f, 0x8b, 0x13, 0x3f, - 0x18, 0xc7, 0xc9, 0x76, 0xe9, 0x6f, 0x37, 0x3f, 0x51, 0x08, 0xb8, 0x5b, 0x47, 0xa9, 0x65, 0x0e, - 0x6b, 0x5d, 0xdd, 0x84, 0x56, 0x4f, 0x7a, 0xf2, 0x0f, 0x2c, 0x42, 0x11, 0x9d, 0x15, 0x05, 0x3d, - 0x48, 0x76, 0xfa, 0x30, 0x3b, 0xee, 0x74, 0x12, 0x93, 0x74, 0x40, 0x16, 0x2f, 0x82, 0xaf, 0xc0, - 0x77, 0xa0, 0x17, 0xc1, 0xab, 0x77, 0xaf, 0x1e, 0x05, 0xdf, 0x80, 0x54, 0x5f, 0x88, 0x4c, 0x66, - 0xda, 0xee, 0x84, 0x6e, 0x2b, 0x58, 0x6f, 0x79, 0xf2, 0xe7, 0x79, 0x3e, 0x79, 0x9e, 0xef, 0x93, - 0xe0, 0x6d, 0x0d, 0x2a, 0x03, 0xc5, 0xb8, 0x94, 0x49, 0x1c, 0x72, 0x13, 0x8b, 0x54, 0x83, 0x71, - 0x4c, 0x2a, 0x95, 0x30, 0x82, 0x9c, 0xae, 0xce, 0x7a, 0x17, 0x22, 0x21, 0xa2, 0x04, 0x18, 0x97, - 0x31, 0xe3, 0x69, 0x2a, 0x4c, 0xb1, 0x52, 0xec, 0xf6, 0x7a, 0x51, 0x6c, 0x0e, 0x86, 0xfb, 0x34, - 0x14, 0x03, 0xc6, 0x55, 0x24, 0xa4, 0x12, 0x2f, 0xec, 0x60, 0x27, 0xec, 0xb3, 0xac, 0xcb, 0xe4, - 0x61, 0x94, 0x9f, 0xd4, 0xc7, 0x63, 0xb1, 0xac, 0xc3, 0x13, 0x79, 0xc0, 0x3b, 0x2c, 0x82, 0x14, - 0x14, 0x37, 0xd0, 0x2f, 0xbc, 0xf9, 0x8f, 0xf1, 0xc6, 0xad, 0xe9, 0xbe, 0x3d, 0x30, 0xbb, 0x60, - 0x1e, 0x0e, 0x41, 0xbd, 0x22, 0x04, 0xaf, 0xa6, 0x7c, 0x00, 0x0d, 0xd4, 0x42, 0xed, 0xf5, 0xc0, - 0x8e, 0x49, 0x1b, 0x9f, 0xe1, 0x52, 0x6a, 0x30, 0xf7, 0xf9, 0x00, 0xb4, 0xe4, 0x21, 0x34, 0x56, - 0xec, 0xb2, 0x3b, 0xed, 0x1f, 0xe1, 0xcd, 0xaa, 0xdf, 0x5e, 0xac, 0x4b, 0xc7, 0x1e, 0x5e, 0xcb, - 0x99, 0x21, 0x34, 0xba, 0x81, 0x5a, 0xb5, 0xf6, 0x7a, 0x30, 0xb1, 0xf3, 0x35, 0x0d, 0x09, 0x84, - 0x46, 0xa8, 0xd2, 0xf3, 0xc4, 0x9e, 0x15, 0xbc, 0x36, 0x3b, 0xf8, 0x47, 0xe4, 0xde, 0x2a, 0x00, - 0x2d, 0xf3, 0xe4, 0x92, 0x06, 0xfe, 0xaf, 0x0c, 0x56, 0x5e, 0x6c, 0x6c, 0x12, 0x83, 0x9d, 0x3a, - 0x58, 0x80, 0xff, 0xbb, 0x3d, 0x3a, 0x4d, 0x38, 0x1d, 0x27, 0xdc, 0x0e, 0x9e, 0x87, 0x7d, 0x9a, - 0x75, 0xa9, 0x3c, 0x8c, 0x68, 0x9e, 0x70, 0x7a, 0xec, 0x38, 0x1d, 0x27, 0x9c, 0x3a, 0x1c, 0x4e, - 0x0c, 0xff, 0x13, 0xc2, 0xe7, 0xab, 0x5b, 0xee, 0x28, 0xe0, 0x06, 0x02, 0x78, 0x39, 0x04, 0x3d, - 0x8b, 0x0a, 0xfd, 0x7b, 0x2a, 0xb2, 0x81, 0xeb, 0x43, 0xa9, 0x41, 0x15, 0x39, 0x58, 0x0b, 0x4a, - 0xcb, 0x7f, 0xe6, 0xc2, 0xde, 0x85, 0x04, 0xa6, 0xb0, 0x7f, 0x27, 0x99, 0x27, 0xae, 0x64, 0x1e, - 0x29, 0x80, 0x25, 0x68, 0xb1, 0xfb, 0xb3, 0x8e, 0xcf, 0x56, 0x3d, 0xef, 0x81, 0xca, 0xe2, 0x10, - 0xc8, 0x07, 0x84, 0x6b, 0xbb, 0x60, 0xc8, 0x16, 0x75, 0x1a, 0x73, 0x76, 0x4f, 0x78, 0x4b, 0xcd, - 0xba, 0xbf, 0xf5, 0xe6, 0xfb, 0xaf, 0x77, 0x2b, 0x2d, 0xd2, 0xb4, 0x9d, 0x9e, 0x75, 0x9c, 0xd7, - 0x41, 0xb3, 0xa3, 0xfc, 0xa2, 0xaf, 0xc9, 0x7b, 0x84, 0x57, 0xf3, 0xf6, 0x21, 0x97, 0xe6, 0x63, - 0x4e, 0x5a, 0xcc, 0x7b, 0xb0, 0x4c, 0xce, 0xdc, 0xad, 0x7f, 0xd1, 0xb2, 0x9e, 0x23, 0x9b, 0x27, - 0xb0, 0x92, 0xcf, 0x08, 0xd7, 0x0b, 0xe9, 0x92, 0x2b, 0xf3, 0x31, 0x2b, 0x02, 0x5f, 0x72, 0x4a, - 0x99, 0xc5, 0xbc, 0xec, 0x9f, 0x84, 0x79, 0xc3, 0x55, 0xfa, 0x5b, 0x84, 0xeb, 0x85, 0x88, 0x17, - 0x61, 0x57, 0xa4, 0xee, 0x2d, 0x50, 0xcc, 0xf8, 0xbd, 0x19, 0xd7, 0x78, 0x7b, 0x51, 0x8d, 0xbf, - 0x20, 0x7c, 0x2a, 0x00, 0x2d, 0x86, 0x2a, 0x84, 0x5c, 0xf7, 0x8b, 0x6a, 0x3d, 0xe9, 0x8d, 0xe5, - 0xd6, 0x3a, 0x77, 0xeb, 0x5f, 0xb7, 0xcc, 0x94, 0x5c, 0x9d, 0xcf, 0xcc, 0x54, 0xc9, 0xbb, 0x63, - 0x14, 0xc0, 0xed, 0x7b, 0x5f, 0x47, 0x4d, 0xf4, 0x6d, 0xd4, 0x44, 0x3f, 0x46, 0x4d, 0xf4, 0xf4, - 0xe6, 0x9f, 0xfd, 0x52, 0x61, 0x12, 0x43, 0xea, 0x7e, 0x8b, 0xfb, 0x75, 0xfb, 0x37, 0x5d, 0xfb, - 0x1d, 0x00, 0x00, 0xff, 0xff, 0xfa, 0x8f, 0x0f, 0xad, 0x45, 0x07, 0x00, 0x00, + // 586 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x95, 0xcd, 0x8a, 0x13, 0x41, + 0x10, 0xc7, 0xe9, 0xcd, 0x12, 0xb3, 0xad, 0x28, 0x34, 0xb8, 0x1b, 0x47, 0x89, 0x61, 0x0e, 0x6b, + 0x5c, 0xdd, 0x1e, 0x12, 0x3d, 0xe9, 0xc9, 0x0f, 0x58, 0x84, 0x20, 0x3a, 0x2b, 0x0a, 0x7a, 0x90, + 0xde, 0x49, 0x31, 0x3b, 0xee, 0x64, 0xa6, 0xed, 0xee, 0x19, 0x58, 0x16, 0x2f, 0x82, 0x4f, 0xe0, + 0x1b, 0xe8, 0xc5, 0x07, 0xf0, 0xee, 0xc1, 0x8b, 0x47, 0xc1, 0x17, 0x90, 0xe8, 0x83, 0x48, 0xf7, + 0x4c, 0x92, 0x9d, 0x26, 0x9b, 0x08, 0xc6, 0xdb, 0x54, 0x77, 0x4f, 0xd5, 0xaf, 0xaa, 0xfe, 0xd5, + 0x8d, 0xb7, 0x24, 0x88, 0x1c, 0x84, 0xc7, 0x38, 0x8f, 0xa3, 0x80, 0xa9, 0x28, 0x4d, 0x24, 0x28, + 0xcb, 0xa4, 0x5c, 0xa4, 0x2a, 0x25, 0x67, 0xab, 0xab, 0xce, 0xa5, 0x30, 0x4d, 0xc3, 0x18, 0x3c, + 0xc6, 0x23, 0x8f, 0x25, 0x49, 0xaa, 0x8a, 0x9d, 0xe2, 0xb4, 0xd3, 0x0f, 0x23, 0xb5, 0x9f, 0xed, + 0xd1, 0x20, 0x1d, 0x7a, 0x4c, 0x84, 0x29, 0x17, 0xe9, 0x2b, 0xf3, 0xb1, 0x1d, 0x0c, 0xbc, 0xbc, + 0xe7, 0xf1, 0x83, 0x50, 0xff, 0x29, 0x8f, 0xc7, 0xf2, 0xf2, 0x2e, 0x8b, 0xf9, 0x3e, 0xeb, 0x7a, + 0x21, 0x24, 0x20, 0x98, 0x82, 0x41, 0xe1, 0xcd, 0x7d, 0x8a, 0xd7, 0xef, 0x4c, 0xcf, 0xed, 0x82, + 0xda, 0x01, 0xf5, 0x38, 0x03, 0x71, 0x48, 0x08, 0x5e, 0x4d, 0xd8, 0x10, 0x9a, 0xa8, 0x8d, 0x3a, + 0x6b, 0xbe, 0xf9, 0x26, 0x1d, 0x7c, 0x8e, 0x71, 0x2e, 0x41, 0x3d, 0x64, 0x43, 0x90, 0x9c, 0x05, + 0xd0, 0x5c, 0x31, 0xdb, 0xf6, 0xb2, 0x7b, 0x84, 0x37, 0xaa, 0x7e, 0xfb, 0x91, 0x2c, 0x1d, 0x3b, + 0xb8, 0xa1, 0x99, 0x21, 0x50, 0xb2, 0x89, 0xda, 0xb5, 0xce, 0x9a, 0x3f, 0xb1, 0xf5, 0x9e, 0x84, + 0x18, 0x02, 0x95, 0x8a, 0xd2, 0xf3, 0xc4, 0x9e, 0x15, 0xbc, 0x36, 0x3b, 0xf8, 0x27, 0x64, 0x67, + 0xe5, 0x83, 0xe4, 0xba, 0xb8, 0xa4, 0x89, 0x4f, 0x95, 0xc1, 0xca, 0xc4, 0xc6, 0x26, 0x51, 0xd8, + 0xea, 0x83, 0x01, 0x38, 0xdd, 0xeb, 0xd3, 0x69, 0xc1, 0xe9, 0xb8, 0xe0, 0xe6, 0xe3, 0x65, 0x30, + 0xa0, 0x79, 0x8f, 0xf2, 0x83, 0x90, 0xea, 0x82, 0xd3, 0x63, 0xbf, 0xd3, 0x71, 0xc1, 0xa9, 0xc5, + 0x61, 0xc5, 0x70, 0xbf, 0x22, 0x7c, 0xb1, 0x7a, 0xe4, 0x9e, 0x00, 0xa6, 0xc0, 0x87, 0xd7, 0x19, + 0xc8, 0x59, 0x54, 0xe8, 0xff, 0x53, 0x91, 0x75, 0x5c, 0xcf, 0xb8, 0x04, 0x51, 0xd4, 0xa0, 0xe1, + 0x97, 0x96, 0x5e, 0x1f, 0x88, 0x43, 0x3f, 0x4b, 0x4c, 0xe5, 0x1b, 0x7e, 0x69, 0xb9, 0x2f, 0xec, + 0x24, 0xee, 0x43, 0x0c, 0xd3, 0x24, 0xfe, 0x4d, 0x4a, 0xcf, 0x6c, 0x29, 0x3d, 0x11, 0x00, 0x4b, + 0xd0, 0x68, 0xef, 0x57, 0x1d, 0x9f, 0xaf, 0x7a, 0xde, 0x05, 0x91, 0x47, 0x01, 0x90, 0x8f, 0x08, + 0xd7, 0x76, 0x40, 0x91, 0x4d, 0x6a, 0x0d, 0xec, 0xec, 0x59, 0x71, 0x96, 0xda, 0x0d, 0x77, 0xf3, + 0xed, 0x8f, 0xdf, 0xef, 0x57, 0xda, 0xa4, 0x65, 0x6e, 0x80, 0xbc, 0x6b, 0xdd, 0x1a, 0xd2, 0x3b, + 0xd2, 0x89, 0xbe, 0x21, 0x1f, 0x10, 0x5e, 0xd5, 0x63, 0x45, 0xae, 0xcc, 0xc7, 0x9c, 0x8c, 0x9e, + 0xf3, 0x68, 0x99, 0x9c, 0xda, 0xad, 0x7b, 0xd9, 0xb0, 0x5e, 0x20, 0x1b, 0x27, 0xb0, 0x92, 0xcf, + 0x08, 0xd7, 0x0b, 0x49, 0x93, 0x6b, 0xf3, 0x31, 0x2b, 0xc2, 0x5f, 0x72, 0x49, 0x3d, 0x83, 0x79, + 0xd5, 0x3d, 0x09, 0xf3, 0x96, 0x3d, 0x01, 0xef, 0x10, 0xae, 0x17, 0x22, 0x5e, 0x84, 0x5d, 0x91, + 0xba, 0xb3, 0x40, 0x31, 0xe3, 0x7b, 0x68, 0xdc, 0xe3, 0xad, 0x45, 0x3d, 0xfe, 0x82, 0xf0, 0x19, + 0x1f, 0x64, 0x9a, 0x89, 0x00, 0xb4, 0xee, 0x17, 0xf5, 0x7a, 0x32, 0x1b, 0xcb, 0xed, 0xb5, 0x76, + 0xeb, 0xde, 0x34, 0xcc, 0x94, 0x5c, 0x9f, 0xcf, 0xec, 0x89, 0x92, 0x77, 0x5b, 0x09, 0x80, 0xbb, + 0x0f, 0xbe, 0x8d, 0x5a, 0xe8, 0xfb, 0xa8, 0x85, 0x7e, 0x8e, 0x5a, 0xe8, 0xf9, 0xed, 0xbf, 0x7b, + 0xbd, 0x82, 0x38, 0x82, 0xc4, 0x7e, 0x2e, 0xf7, 0xea, 0xe6, 0xcd, 0xba, 0xf1, 0x27, 0x00, 0x00, + 0xff, 0xff, 0xd5, 0xe2, 0xa9, 0xbf, 0x5d, 0x07, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -826,6 +835,16 @@ func (m *ApplicationSetCreateRequest) MarshalToSizedBuffer(dAtA []byte) (int, er i -= len(m.XXX_unrecognized) copy(dAtA[i:], m.XXX_unrecognized) } + if m.DryRun { + i-- + if m.DryRun { + dAtA[i] = 1 + } else { + dAtA[i] = 0 + } + i-- + dAtA[i] = 0x18 + } if m.Upsert { i-- if m.Upsert { @@ -1023,6 +1042,9 @@ func (m *ApplicationSetCreateRequest) Size() (n int) { if m.Upsert { n += 2 } + if m.DryRun { + n += 2 + } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } @@ -1541,6 +1563,26 @@ func (m *ApplicationSetCreateRequest) Unmarshal(dAtA []byte) error { } } m.Upsert = bool(v != 0) + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field DryRun", wireType) + } + var v int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowApplicationset + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + v |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + m.DryRun = bool(v != 0) default: iNdEx = preIndex skippy, err := skipApplicationset(dAtA[iNdEx:]) diff --git a/server/applicationset/applicationset.go b/server/applicationset/applicationset.go index 5f2b7508b9a2e..a66173384e8c1 100644 --- a/server/applicationset/applicationset.go +++ b/server/applicationset/applicationset.go @@ -17,18 +17,26 @@ import ( apierr "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/cache" + "sigs.k8s.io/controller-runtime/pkg/client" + appsettemplate "github.com/argoproj/argo-cd/v2/applicationset/controllers/template" + "github.com/argoproj/argo-cd/v2/applicationset/generators" + "github.com/argoproj/argo-cd/v2/applicationset/services" + appsetstatus "github.com/argoproj/argo-cd/v2/applicationset/status" appsetutils "github.com/argoproj/argo-cd/v2/applicationset/utils" "github.com/argoproj/argo-cd/v2/pkg/apiclient/applicationset" "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" appclientset "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned" applisters "github.com/argoproj/argo-cd/v2/pkg/client/listers/application/v1alpha1" + repoapiclient "github.com/argoproj/argo-cd/v2/reposerver/apiclient" "github.com/argoproj/argo-cd/v2/server/rbacpolicy" "github.com/argoproj/argo-cd/v2/util/argo" "github.com/argoproj/argo-cd/v2/util/collections" "github.com/argoproj/argo-cd/v2/util/db" + "github.com/argoproj/argo-cd/v2/util/github_app" "github.com/argoproj/argo-cd/v2/util/rbac" "github.com/argoproj/argo-cd/v2/util/security" "github.com/argoproj/argo-cd/v2/util/session" @@ -36,24 +44,36 @@ import ( ) type Server struct { - ns string - db db.ArgoDB - enf *rbac.Enforcer - appclientset appclientset.Interface - appsetInformer cache.SharedIndexInformer - appsetLister applisters.ApplicationSetLister - projLister applisters.AppProjectNamespaceLister - auditLogger *argo.AuditLogger - settings *settings.SettingsManager - projectLock sync.KeyLock - enabledNamespaces []string + ns string + db db.ArgoDB + enf *rbac.Enforcer + k8sClient kubernetes.Interface + dynamicClient dynamic.Interface + client client.Client + repoClientSet repoapiclient.Clientset + appclientset appclientset.Interface + appsetInformer cache.SharedIndexInformer + appsetLister applisters.ApplicationSetLister + projLister applisters.AppProjectNamespaceLister + auditLogger *argo.AuditLogger + settings *settings.SettingsManager + projectLock sync.KeyLock + enabledNamespaces []string + GitSubmoduleEnabled bool + EnableNewGitFileGlobbing bool + ScmRootCAPath string + AllowedScmProviders []string + EnableScmProviders bool } // NewServer returns a new instance of the ApplicationSet service func NewServer( db db.ArgoDB, kubeclientset kubernetes.Interface, + dynamicClientset dynamic.Interface, + kubeControllerClientset client.Client, enf *rbac.Enforcer, + repoClientSet repoapiclient.Clientset, appclientset appclientset.Interface, appsetInformer cache.SharedIndexInformer, appsetLister applisters.ApplicationSetLister, @@ -62,19 +82,33 @@ func NewServer( namespace string, projectLock sync.KeyLock, enabledNamespaces []string, + gitSubmoduleEnabled bool, + enableNewGitFileGlobbing bool, + scmRootCAPath string, + allowedScmProviders []string, + enableScmProviders bool, ) applicationset.ApplicationSetServiceServer { s := &Server{ - ns: namespace, - db: db, - enf: enf, - appclientset: appclientset, - appsetInformer: appsetInformer, - appsetLister: appsetLister, - projLister: projLister, - settings: settings, - projectLock: projectLock, - auditLogger: argo.NewAuditLogger(namespace, kubeclientset, "argocd-server"), - enabledNamespaces: enabledNamespaces, + ns: namespace, + db: db, + enf: enf, + dynamicClient: dynamicClientset, + client: kubeControllerClientset, + k8sClient: kubeclientset, + repoClientSet: repoClientSet, + appclientset: appclientset, + appsetInformer: appsetInformer, + appsetLister: appsetLister, + projLister: projLister, + settings: settings, + projectLock: projectLock, + auditLogger: argo.NewAuditLogger(namespace, kubeclientset, "argocd-server"), + enabledNamespaces: enabledNamespaces, + GitSubmoduleEnabled: gitSubmoduleEnabled, + EnableNewGitFileGlobbing: enableNewGitFileGlobbing, + ScmRootCAPath: scmRootCAPath, + AllowedScmProviders: allowedScmProviders, + EnableScmProviders: enableScmProviders, } return s } @@ -166,6 +200,23 @@ func (s *Server) Create(ctx context.Context, q *applicationset.ApplicationSetCre return nil, fmt.Errorf("error checking create permissions for ApplicationSets %s : %w", appset.Name, err) } + if q.GetDryRun() { + apps, err := s.generateApplicationSetApps(ctx, *appset, namespace) + if err != nil { + return nil, fmt.Errorf("unable to generate Applications of ApplicationSet: %w", err) + } + + statusMap := appsetstatus.GetResourceStatusMap(appset) + statusMap = appsetstatus.BuildResourceStatus(statusMap, apps) + + statuses := []v1alpha1.ResourceStatus{} + for _, status := range statusMap { + statuses = append(statuses, status) + } + appset.Status.Resources = statuses + return appset, nil + } + s.projectLock.RLock(projectName) defer s.projectLock.RUnlock(projectName) @@ -209,6 +260,30 @@ func (s *Server) Create(ctx context.Context, q *applicationset.ApplicationSetCre return updated, nil } +func (s *Server) generateApplicationSetApps(ctx context.Context, appset v1alpha1.ApplicationSet, namespace string) ([]v1alpha1.Application, error) { + logCtx := log.WithField("applicationset", appset.Name) + + argoCDDB := s.db + + scmConfig := generators.NewSCMConfig(s.ScmRootCAPath, s.AllowedScmProviders, s.EnableScmProviders, github_app.NewAuthCredentials(argoCDDB.(db.RepoCredsDB))) + + getRepository := func(ctx context.Context, url, project string) (*v1alpha1.Repository, error) { + return s.db.GetRepository(ctx, url, project) + } + argoCDService, err := services.NewArgoCDService(getRepository, s.GitSubmoduleEnabled, s.repoClientSet, s.EnableNewGitFileGlobbing) + if err != nil { + return nil, fmt.Errorf("error creating ArgoCDService: %w", err) + } + + appSetGenerators := generators.GetGenerators(ctx, s.client, s.k8sClient, namespace, argoCDService, s.dynamicClient, scmConfig) + + apps, _, err := appsettemplate.GenerateApplications(logCtx, appset, appSetGenerators, &appsetutils.Render{}, s.client) + if err != nil { + return nil, fmt.Errorf("error generating applications: %w", err) + } + return apps, nil +} + func (s *Server) updateAppSet(appset *v1alpha1.ApplicationSet, newAppset *v1alpha1.ApplicationSet, ctx context.Context, merge bool) (*v1alpha1.ApplicationSet, error) { if appset != nil && appset.Spec.Template.Spec.Project != newAppset.Spec.Template.Spec.Project { // When changing projects, caller must have applicationset create and update privileges in new project diff --git a/server/applicationset/applicationset.proto b/server/applicationset/applicationset.proto index 07ed4e2c89384..8feca553c2e5d 100644 --- a/server/applicationset/applicationset.proto +++ b/server/applicationset/applicationset.proto @@ -37,6 +37,7 @@ message ApplicationSetResponse { message ApplicationSetCreateRequest { github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.ApplicationSet applicationset = 1; bool upsert = 2; + bool dryRun = 3; } @@ -54,7 +55,6 @@ message ApplicationSetTreeQuery { // ApplicationSetService service ApplicationSetService { - // Get returns an applicationset by name rpc Get (ApplicationSetGetQuery) returns (github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.ApplicationSet) { option (google.api.http).get = "/api/v1/applicationsets/{name}"; diff --git a/server/applicationset/applicationset_test.go b/server/applicationset/applicationset_test.go index da8c980fdd4e5..bd72ee613e70c 100644 --- a/server/applicationset/applicationset_test.go +++ b/server/applicationset/applicationset_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/fake" @@ -143,7 +144,10 @@ func newTestAppSetServerWithEnforcerConfigure(f func(*rbac.Enforcer), namespace server := NewServer( db, kubeclientset, + nil, + nil, enforcer, + nil, fakeAppsClientset, appInformer, factory.Argoproj().V1alpha1().ApplicationSets().Lister(), @@ -152,6 +156,11 @@ func newTestAppSetServerWithEnforcerConfigure(f func(*rbac.Enforcer), namespace testNamespace, sync.NewKeyLock(), []string{testNamespace, "external-namespace"}, + true, + true, + "", + []string{}, + true, ) return server.(*Server) } @@ -358,6 +367,54 @@ func TestCreateAppSetWrongNamespace(t *testing.T) { assert.Equal(t, "namespace 'NOT-ALLOWED' is not permitted", err.Error()) } +func TestCreateAppSetDryRun(t *testing.T) { + testAppSet := newTestAppSet() + appServer := newTestAppSetServer() + testAppSet.Spec.Template.Name = "{{name}}" + testAppSet.Spec.Generators = []appsv1.ApplicationSetGenerator{ + { + List: &appsv1.ListGenerator{ + Elements: []apiextensionsv1.JSON{{Raw: []byte(`{"name": "a"}`)}, {Raw: []byte(`{"name": "b"}`)}}, + }, + }, + } + createReq := applicationset.ApplicationSetCreateRequest{ + Applicationset: testAppSet, + DryRun: true, + } + result, err := appServer.Create(context.Background(), &createReq) + + require.NoError(t, err) + assert.Len(t, result.Status.Resources, 2) + assert.Equal(t, "a", result.Status.Resources[0].Name) + assert.Equal(t, testAppSet.Namespace, result.Status.Resources[0].Namespace) + assert.Equal(t, "b", result.Status.Resources[1].Name) + assert.Equal(t, testAppSet.Namespace, result.Status.Resources[1].Namespace) +} + +func TestCreateAppSetDryRunWithDuplicate(t *testing.T) { + testAppSet := newTestAppSet() + appServer := newTestAppSetServer() + testAppSet.Spec.Template.Name = "{{name}}" + testAppSet.Spec.Generators = []appsv1.ApplicationSetGenerator{ + { + List: &appsv1.ListGenerator{ + Elements: []apiextensionsv1.JSON{{Raw: []byte(`{"name": "a"}`)}, {Raw: []byte(`{"name": "a"}`)}}, + }, + }, + } + createReq := applicationset.ApplicationSetCreateRequest{ + Applicationset: testAppSet, + DryRun: true, + } + result, err := appServer.Create(context.Background(), &createReq) + + require.NoError(t, err) + assert.Len(t, result.Status.Resources, 1) + assert.Equal(t, "a", result.Status.Resources[0].Name) + assert.Equal(t, testAppSet.Namespace, result.Status.Resources[0].Namespace) +} + func TestGetAppSet(t *testing.T) { appSet1 := newTestAppSet(func(appset *appsv1.ApplicationSet) { appset.Name = "AppSet1" diff --git a/server/server.go b/server/server.go index 862ad4075c4f9..77e058afae959 100644 --- a/server/server.go +++ b/server/server.go @@ -54,8 +54,10 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/cache" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/argoproj/argo-cd/v2/common" "github.com/argoproj/argo-cd/v2/pkg/apiclient" @@ -167,6 +169,7 @@ func init() { // ArgoCDServer is the API server for Argo CD type ArgoCDServer struct { ArgoCDServerOpts + ApplicationSetOpts ssoClientApp *oidc.ClientApp settings *settings_util.ArgoCDSettings @@ -198,31 +201,41 @@ type ArgoCDServer struct { } type ArgoCDServerOpts struct { - DisableAuth bool - ContentTypes []string - EnableGZip bool - Insecure bool - StaticAssetsDir string - ListenPort int - ListenHost string - MetricsPort int - MetricsHost string - Namespace string - DexServerAddr string - DexTLSConfig *dexutil.DexTLSConfig - BaseHRef string - RootPath string - KubeClientset kubernetes.Interface - AppClientset appclientset.Interface - RepoClientset repoapiclient.Clientset - Cache *servercache.Cache - RepoServerCache *repocache.Cache - RedisClient *redis.Client - TLSConfigCustomizer tlsutil.ConfigCustomizer - XFrameOptions string - ContentSecurityPolicy string - ApplicationNamespaces []string - EnableProxyExtension bool + DisableAuth bool + ContentTypes []string + EnableGZip bool + Insecure bool + StaticAssetsDir string + ListenPort int + ListenHost string + MetricsPort int + MetricsHost string + Namespace string + DexServerAddr string + DexTLSConfig *dexutil.DexTLSConfig + BaseHRef string + RootPath string + DynamicClientset dynamic.Interface + KubeControllerClientset client.Client + KubeClientset kubernetes.Interface + AppClientset appclientset.Interface + RepoClientset repoapiclient.Clientset + Cache *servercache.Cache + RepoServerCache *repocache.Cache + RedisClient *redis.Client + TLSConfigCustomizer tlsutil.ConfigCustomizer + XFrameOptions string + ContentSecurityPolicy string + ApplicationNamespaces []string + EnableProxyExtension bool +} + +type ApplicationSetOpts struct { + GitSubmoduleEnabled bool + EnableNewGitFileGlobbing bool + ScmRootCAPath string + AllowedScmProviders []string + EnableScmProviders bool } // HTTPMetricsRegistry exposes operations to update http metrics in the Argo CD @@ -259,7 +272,7 @@ func initializeDefaultProject(opts ArgoCDServerOpts) error { } // NewServer returns a new instance of the Argo CD API server -func NewServer(ctx context.Context, opts ArgoCDServerOpts) *ArgoCDServer { +func NewServer(ctx context.Context, opts ArgoCDServerOpts, appsetOpts ApplicationSetOpts) *ArgoCDServer { settingsMgr := settings_util.NewSettingsManager(ctx, opts.KubeClientset, opts.Namespace) settings, err := settingsMgr.InitializeSettings(opts.Insecure) errorsutil.CheckError(err) @@ -315,26 +328,27 @@ func NewServer(ctx context.Context, opts ArgoCDServerOpts) *ArgoCDServer { em := extension.NewManager(logger, sg, ag, pg, enf) a := &ArgoCDServer{ - ArgoCDServerOpts: opts, - log: logger, - settings: settings, - sessionMgr: sessionMgr, - settingsMgr: settingsMgr, - enf: enf, - projInformer: projInformer, - projLister: projLister, - appInformer: appInformer, - appLister: appLister, - appsetInformer: appsetInformer, - appsetLister: appsetLister, - policyEnforcer: policyEnf, - userStateStorage: userStateStorage, - staticAssets: http.FS(staticFS), - db: dbInstance, - apiFactory: apiFactory, - secretInformer: secretInformer, - configMapInformer: configMapInformer, - extensionManager: em, + ArgoCDServerOpts: opts, + ApplicationSetOpts: appsetOpts, + log: logger, + settings: settings, + sessionMgr: sessionMgr, + settingsMgr: settingsMgr, + enf: enf, + projInformer: projInformer, + projLister: projLister, + appInformer: appInformer, + appLister: appLister, + appsetInformer: appsetInformer, + appsetLister: appsetLister, + policyEnforcer: policyEnf, + userStateStorage: userStateStorage, + staticAssets: http.FS(staticFS), + db: dbInstance, + apiFactory: apiFactory, + secretInformer: secretInformer, + configMapInformer: configMapInformer, + extensionManager: em, } err = a.logInClusterWarnings() @@ -868,7 +882,10 @@ func newArgoCDServiceSet(a *ArgoCDServer) *ArgoCDServiceSet { applicationSetService := applicationset.NewServer( a.db, a.KubeClientset, + a.DynamicClientset, + a.KubeControllerClientset, a.enf, + a.RepoClientset, a.AppClientset, a.appsetInformer, a.appsetLister, @@ -876,7 +893,13 @@ func newArgoCDServiceSet(a *ArgoCDServer) *ArgoCDServiceSet { a.settingsMgr, a.Namespace, projectLock, - a.ApplicationNamespaces) + a.ApplicationNamespaces, + a.GitSubmoduleEnabled, + a.EnableNewGitFileGlobbing, + a.ScmRootCAPath, + a.AllowedScmProviders, + a.EnableScmProviders, + ) projectService := project.NewServer(a.Namespace, a.KubeClientset, a.AppClientset, a.enf, projectLock, a.sessionMgr, a.policyEnforcer, a.projInformer, a.settingsMgr, a.db) appsInAnyNamespaceEnabled := len(a.ArgoCDServerOpts.ApplicationNamespaces) > 0 diff --git a/server/server_test.go b/server/server_test.go index 13bb3d6770753..7923db7f3e9d6 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -19,9 +19,13 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/metadata" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/fake" "sigs.k8s.io/yaml" + dynfake "k8s.io/client-go/dynamic/fake" + clientfake "sigs.k8s.io/controller-runtime/pkg/client/fake" + "github.com/argoproj/argo-cd/v2/common" "github.com/argoproj/argo-cd/v2/pkg/apiclient" "github.com/argoproj/argo-cd/v2/pkg/apiclient/session" @@ -52,10 +56,12 @@ func fakeServer(t *testing.T) (*FakeArgoCDServer, func()) { kubeclientset := fake.NewSimpleClientset(cm, secret) appClientSet := apps.NewSimpleClientset() redis, closer := test.NewInMemoryRedis() - port, err := test.GetFreePort() mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}} tmpAssetsDir := t.TempDir() + dynamicClient := dynfake.NewSimpleDynamicClient(runtime.NewScheme()) + fakeClient := clientfake.NewClientBuilder().Build() + port, err := test.GetFreePort() if err != nil { panic(err) } @@ -78,11 +84,13 @@ func fakeServer(t *testing.T) (*FakeArgoCDServer, func()) { 1*time.Minute, 1*time.Minute, ), - RedisClient: redis, - RepoClientset: mockRepoClient, - StaticAssetsDir: tmpAssetsDir, + RedisClient: redis, + RepoClientset: mockRepoClient, + StaticAssetsDir: tmpAssetsDir, + DynamicClientset: dynamicClient, + KubeControllerClientset: fakeClient, } - srv := NewServer(context.Background(), argoCDOpts) + srv := NewServer(context.Background(), argoCDOpts, ApplicationSetOpts{}) fakeSrv := &FakeArgoCDServer{srv, tmpAssetsDir} return fakeSrv, closer } @@ -118,7 +126,7 @@ func TestEnforceProjectToken(t *testing.T) { mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}} t.Run("TestEnforceProjectTokenSuccessful", func(t *testing.T) { - s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}) + s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}, ApplicationSetOpts{}) cancel := test.StartInformer(s.projInformer) defer cancel() claims := jwt.MapClaims{"sub": defaultSub, "iat": defaultIssuedAt} @@ -127,21 +135,21 @@ func TestEnforceProjectToken(t *testing.T) { }) t.Run("TestEnforceProjectTokenWithDiffCreateAtFailure", func(t *testing.T) { - s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}) + s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}, ApplicationSetOpts{}) diffCreateAt := defaultIssuedAt + 1 claims := jwt.MapClaims{"sub": defaultSub, "iat": diffCreateAt} assert.False(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject)) }) t.Run("TestEnforceProjectTokenIncorrectSubFormatFailure", func(t *testing.T) { - s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}) + s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}, ApplicationSetOpts{}) invalidSub := "proj:test" claims := jwt.MapClaims{"sub": invalidSub, "iat": defaultIssuedAt} assert.False(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject)) }) t.Run("TestEnforceProjectTokenNoTokenFailure", func(t *testing.T) { - s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}) + s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}, ApplicationSetOpts{}) nonExistentToken := "fake-token" invalidSub := fmt.Sprintf(subFormat, projectName, nonExistentToken) claims := jwt.MapClaims{"sub": invalidSub, "iat": defaultIssuedAt} @@ -151,7 +159,7 @@ func TestEnforceProjectToken(t *testing.T) { t.Run("TestEnforceProjectTokenNotJWTTokenFailure", func(t *testing.T) { proj := existingProj.DeepCopy() proj.Spec.Roles[0].JWTTokens = nil - s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(proj), RepoClientset: mockRepoClient}) + s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(proj), RepoClientset: mockRepoClient}, ApplicationSetOpts{}) claims := jwt.MapClaims{"sub": defaultSub, "iat": defaultIssuedAt} assert.False(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject)) }) @@ -164,7 +172,7 @@ func TestEnforceProjectToken(t *testing.T) { proj := existingProj.DeepCopy() proj.Spec.Roles[0] = role - s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(proj), RepoClientset: mockRepoClient}) + s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(proj), RepoClientset: mockRepoClient}, ApplicationSetOpts{}) cancel := test.StartInformer(s.projInformer) defer cancel() claims := jwt.MapClaims{"sub": defaultSub, "iat": defaultIssuedAt} @@ -175,7 +183,7 @@ func TestEnforceProjectToken(t *testing.T) { }) t.Run("TestEnforceProjectTokenWithIdSuccessful", func(t *testing.T) { - s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}) + s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}, ApplicationSetOpts{}) cancel := test.StartInformer(s.projInformer) defer cancel() claims := jwt.MapClaims{"sub": defaultSub, "jti": defaultId} @@ -184,7 +192,7 @@ func TestEnforceProjectToken(t *testing.T) { }) t.Run("TestEnforceProjectTokenWithInvalidIdFailure", func(t *testing.T) { - s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}) + s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}, ApplicationSetOpts{}) invalidId := "invalidId" claims := jwt.MapClaims{"sub": defaultSub, "jti": defaultId} res := s.enf.Enforce(claims, "applications", "get", invalidId) @@ -268,7 +276,7 @@ func TestInitializingExistingDefaultProject(t *testing.T) { RepoClientset: mockRepoClient, } - argocd := NewServer(context.Background(), argoCDOpts) + argocd := NewServer(context.Background(), argoCDOpts, ApplicationSetOpts{}) assert.NotNil(t, argocd) proj, err := appClientSet.ArgoprojV1alpha1().AppProjects(test.FakeArgoCDNamespace).Get(context.Background(), v1alpha1.DefaultAppProjectName, metav1.GetOptions{}) @@ -291,7 +299,7 @@ func TestInitializingNotExistingDefaultProject(t *testing.T) { RepoClientset: mockRepoClient, } - argocd := NewServer(context.Background(), argoCDOpts) + argocd := NewServer(context.Background(), argoCDOpts, ApplicationSetOpts{}) assert.NotNil(t, argocd) proj, err := appClientSet.ArgoprojV1alpha1().AppProjects(test.FakeArgoCDNamespace).Get(context.Background(), v1alpha1.DefaultAppProjectName, metav1.GetOptions{}) @@ -333,7 +341,7 @@ func TestEnforceProjectGroups(t *testing.T) { } mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}} kubeclientset := fake.NewSimpleClientset(test.NewFakeConfigMap(), test.NewFakeSecret()) - s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}) + s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}, ApplicationSetOpts{}) cancel := test.StartInformer(s.projInformer) defer cancel() claims := jwt.MapClaims{ @@ -395,7 +403,7 @@ func TestRevokedToken(t *testing.T) { }, } - s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}) + s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}, ApplicationSetOpts{}) cancel := test.StartInformer(s.projInformer) defer cancel() claims := jwt.MapClaims{"sub": defaultSub, "iat": defaultIssuedAt} @@ -450,7 +458,7 @@ func TestAuthenticate(t *testing.T) { AppClientset: appClientSet, RepoClientset: mockRepoClient, } - argocd := NewServer(context.Background(), argoCDOpts) + argocd := NewServer(context.Background(), argoCDOpts, ApplicationSetOpts{}) ctx := context.Background() if testData.user != "" { token, err := argocd.sessionMgr.Create(testData.user, 0, "abc") @@ -587,7 +595,7 @@ connectors: if withFakeSSO && useDexForSSO { argoCDOpts.DexServerAddr = ts.URL } - argocd = NewServer(context.Background(), argoCDOpts) + argocd = NewServer(context.Background(), argoCDOpts, ApplicationSetOpts{}) var err error argocd.ssoClientApp, err = oidc.NewClientApp(argocd.settings, argocd.DexServerAddr, argocd.DexTLSConfig, argocd.BaseHRef, cache.NewInMemoryCache(24*time.Hour)) require.NoError(t, err) @@ -1071,7 +1079,7 @@ func TestTranslateGrpcCookieHeader(t *testing.T) { AppClientset: apps.NewSimpleClientset(), RepoClientset: &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}, } - argocd := NewServer(context.Background(), argoCDOpts) + argocd := NewServer(context.Background(), argoCDOpts, ApplicationSetOpts{}) t.Run("TokenIsNotEmpty", func(t *testing.T) { recorder := httptest.NewRecorder() diff --git a/test/e2e/app_k8s_events_test.go b/test/e2e/app_k8s_events_test.go index e306e2ddf0d5a..7438adfe7001c 100644 --- a/test/e2e/app_k8s_events_test.go +++ b/test/e2e/app_k8s_events_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" . "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" @@ -31,7 +32,7 @@ func TestLabelsOnAppK8sEvents(t *testing.T) { events, err := KubeClientset.CoreV1().Events(app.Namespace).List(context.Background(), metav1.ListOptions{ FieldSelector: fmt.Sprintf("involvedObject.name=%s,involvedObject.kind=Application", app.Name), }) - assert.NoError(t, err) + require.NoError(t, err) for _, event := range events.Items { for k, v := range event.Labels { ev, found := expectedLabels[k] @@ -56,7 +57,7 @@ func TestNoLabelsOnAppK8sEvents(t *testing.T) { events, err := KubeClientset.CoreV1().Events(app.Namespace).List(context.Background(), metav1.ListOptions{ FieldSelector: fmt.Sprintf("involvedObject.name=%s,involvedObject.kind=Application", app.Name), }) - assert.NoError(t, err) + require.NoError(t, err) for _, event := range events.Items { assert.Nil(t, event.Labels) }