diff --git a/commitserver/commit/hydratorhelper.go b/commitserver/commit/hydratorhelper.go index 8d8c55bd24a37..523b049a13d04 100644 --- a/commitserver/commit/hydratorhelper.go +++ b/commitserver/commit/hydratorhelper.go @@ -5,9 +5,7 @@ import ( "fmt" "os" "path/filepath" - "strings" "text/template" - "time" "github.com/Masterminds/sprig/v3" log "github.com/sirupsen/logrus" @@ -17,7 +15,7 @@ import ( "github.com/argoproj/argo-cd/v3/commitserver/apiclient" "github.com/argoproj/argo-cd/v3/common" appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" - "github.com/argoproj/argo-cd/v3/util/git" + "github.com/argoproj/argo-cd/v3/util/hydrator" "github.com/argoproj/argo-cd/v3/util/io" ) @@ -36,25 +34,13 @@ func init() { // WriteForPaths writes the manifests, hydrator.metadata, and README.md files for each path in the provided paths. It // also writes a root-level hydrator.metadata file containing the repo URL and dry SHA. func WriteForPaths(root *os.Root, repoUrl, drySha string, dryCommitMetadata *appv1.RevisionMetadata, paths []*apiclient.PathDetails) error { //nolint:revive //FIXME(var-naming) - author := "" - message := "" - date := "" - var references []appv1.RevisionReference - if dryCommitMetadata != nil { - author = dryCommitMetadata.Author - message = dryCommitMetadata.Message - if dryCommitMetadata.Date != nil { - date = dryCommitMetadata.Date.Format(time.RFC3339) - } - references = dryCommitMetadata.References + hydratorMetadata, err := hydrator.GetCommitMetadata(repoUrl, drySha, dryCommitMetadata) + if err != nil { + return fmt.Errorf("failed to retrieve hydrator metadata: %w", err) } - subject, body, _ := strings.Cut(message, "\n\n") - - _, bodyMinusTrailers := git.GetReferences(log.WithFields(log.Fields{"repo": repoUrl, "revision": drySha}), body) - // Write the top-level readme. - err := writeMetadata(root, "", hydratorMetadataFile{DrySHA: drySha, RepoURL: repoUrl, Author: author, Subject: subject, Body: bodyMinusTrailers, Date: date, References: references}) + err = writeMetadata(root, "", hydratorMetadata) if err != nil { return fmt.Errorf("failed to write top-level hydrator metadata: %w", err) } @@ -86,7 +72,7 @@ func WriteForPaths(root *os.Root, repoUrl, drySha string, dryCommitMetadata *app } // Write hydrator.metadata containing information about the hydration process. - hydratorMetadata := hydratorMetadataFile{ + hydratorMetadata := hydrator.HydratorCommitMetadata{ Commands: p.Commands, DrySHA: drySha, RepoURL: repoUrl, @@ -106,7 +92,7 @@ func WriteForPaths(root *os.Root, repoUrl, drySha string, dryCommitMetadata *app } // writeMetadata writes the metadata to the hydrator.metadata file. -func writeMetadata(root *os.Root, dirPath string, metadata hydratorMetadataFile) error { +func writeMetadata(root *os.Root, dirPath string, metadata hydrator.HydratorCommitMetadata) error { hydratorMetadataPath := filepath.Join(dirPath, "hydrator.metadata") f, err := root.Create(hydratorMetadataPath) if err != nil { @@ -125,7 +111,7 @@ func writeMetadata(root *os.Root, dirPath string, metadata hydratorMetadataFile) } // writeReadme writes the readme to the README.md file. -func writeReadme(root *os.Root, dirPath string, metadata hydratorMetadataFile) error { +func writeReadme(root *os.Root, dirPath string, metadata hydrator.HydratorCommitMetadata) error { readmeTemplate, err := template.New("readme").Funcs(sprigFuncMap).Parse(manifestHydrationReadmeTemplate) if err != nil { return fmt.Errorf("failed to parse readme template: %w", err) diff --git a/commitserver/commit/hydratorhelper_test.go b/commitserver/commit/hydratorhelper_test.go index 72ebb66779162..5838ee28b0307 100644 --- a/commitserver/commit/hydratorhelper_test.go +++ b/commitserver/commit/hydratorhelper_test.go @@ -18,6 +18,7 @@ import ( "github.com/argoproj/argo-cd/v3/commitserver/apiclient" appsv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" + "github.com/argoproj/argo-cd/v3/util/hydrator" ) // tempRoot creates a temporary directory and returns an os.Root object for it. @@ -144,7 +145,7 @@ Argocd-reference-commit-sha: abc123 func TestWriteMetadata(t *testing.T) { root := tempRoot(t) - metadata := hydratorMetadataFile{ + metadata := hydrator.HydratorCommitMetadata{ RepoURL: "https://github.com/example/repo", DrySHA: "abc123", } @@ -156,7 +157,7 @@ func TestWriteMetadata(t *testing.T) { metadataBytes, err := os.ReadFile(metadataPath) require.NoError(t, err) - var readMetadata hydratorMetadataFile + var readMetadata hydrator.HydratorCommitMetadata err = json.Unmarshal(metadataBytes, &readMetadata) require.NoError(t, err) assert.Equal(t, metadata, readMetadata) @@ -171,7 +172,7 @@ func TestWriteReadme(t *testing.T) { hash := sha256.Sum256(randomData) sha := hex.EncodeToString(hash[:]) - metadata := hydratorMetadataFile{ + metadata := hydrator.HydratorCommitMetadata{ RepoURL: "https://github.com/example/repo", DrySHA: "abc123", References: []appsv1.RevisionReference{ diff --git a/controller/hydrator/hydrator.go b/controller/hydrator/hydrator.go index 2f27c1c76fb48..f9e139f7f5490 100644 --- a/controller/hydrator/hydrator.go +++ b/controller/hydrator/hydrator.go @@ -16,6 +16,7 @@ import ( "github.com/argoproj/argo-cd/v3/reposerver/apiclient" applog "github.com/argoproj/argo-cd/v3/util/app/log" "github.com/argoproj/argo-cd/v3/util/git" + "github.com/argoproj/argo-cd/v3/util/hydrator" utilio "github.com/argoproj/argo-cd/v3/util/io" ) @@ -59,6 +60,9 @@ type Dependencies interface { // AddHydrationQueueItem adds a hydration queue item to the queue. This is used to trigger the hydration process for // a group of applications which are hydrating to the same repo and target branch. AddHydrationQueueItem(key types.HydrationQueueKey) + + // GetHydratorCommitMessageTemplate gets the configured template for rendering commit messages. + GetHydratorCommitMessageTemplate() (string, error) } // Hydrator is the main struct that implements the hydration logic. It uses the Dependencies interface to access the @@ -340,13 +344,22 @@ func (h *Hydrator) hydrate(logCtx *log.Entry, apps []*appv1.Application) (string } logCtx.Warn("no credentials found for repo, continuing without credentials") } + // get the commit message template + commitMessageTemplate, err := h.dependencies.GetHydratorCommitMessageTemplate() + if err != nil { + return "", "", fmt.Errorf("failed to get hydrated commit message template: %w", err) + } + commitMessage, errMsg := getTemplatedCommitMessage(repoURL, targetRevision, commitMessageTemplate, revisionMetadata) + if errMsg != nil { + return "", "", fmt.Errorf("failed to get hydrator commit templated message: %w", errMsg) + } manifestsRequest := commitclient.CommitHydratedManifestsRequest{ Repo: repo, SyncBranch: syncBranch, TargetBranch: targetBranch, DrySha: targetRevision, - CommitMessage: "[Argo CD Bot] hydrate " + targetRevision, + CommitMessage: commitMessage, Paths: paths, DryCommitMetadata: revisionMetadata, } @@ -411,3 +424,18 @@ func appNeedsHydration(app *appv1.Application, statusHydrateTimeout time.Duratio return false, "" } + +// Gets the multi-line commit message based on the template defined in the configmap. It is a two step process: +// 1. Get the metadata template engine would use to render the template +// 2. Pass the output of Step 1 and Step 2 to template Render +func getTemplatedCommitMessage(repoURL, revision, commitMessageTemplate string, dryCommitMetadata *appv1.RevisionMetadata) (string, error) { + hydratorCommitMetadata, err := hydrator.GetCommitMetadata(repoURL, revision, dryCommitMetadata) + if err != nil { + return "", fmt.Errorf("failed to get hydrated commit message: %w", err) + } + templatedCommitMsg, err := hydrator.Render(commitMessageTemplate, hydratorCommitMetadata) + if err != nil { + return "", fmt.Errorf("failed to parse template %s: %w", commitMessageTemplate, err) + } + return templatedCommitMsg, nil +} diff --git a/controller/hydrator/hydrator_test.go b/controller/hydrator/hydrator_test.go index d826eb7196c2c..f7587afdf8d55 100644 --- a/controller/hydrator/hydrator_test.go +++ b/controller/hydrator/hydrator_test.go @@ -13,8 +13,15 @@ import ( "github.com/argoproj/argo-cd/v3/controller/hydrator/mocks" "github.com/argoproj/argo-cd/v3/controller/hydrator/types" "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" + "github.com/argoproj/argo-cd/v3/util/settings" ) +var message = `testn +Argocd-reference-commit-repourl: https://github.com/test/argocd-example-apps +Argocd-reference-commit-author: Argocd-reference-commit-author +Argocd-reference-commit-subject: testhydratormd +Signed-off-by: testUser ` + func Test_appNeedsHydration(t *testing.T) { t.Parallel() @@ -167,3 +174,80 @@ func Test_getRelevantAppsForHydration_RepoURLNormalization(t *testing.T) { require.NoError(t, err) assert.Len(t, relevantApps, 2, "Expected both apps to be considered relevant despite URL differences") } + +func TestHydrator_getTemplatedCommitMessage(t *testing.T) { + references := make([]v1alpha1.RevisionReference, 0) + revReference := v1alpha1.RevisionReference{ + Commit: &v1alpha1.CommitMetadata{ + Author: "testAuthor", + Subject: "test", + RepoURL: "https://github.com/test/argocd-example-apps", + SHA: "3ff41cc5247197a6caf50216c4c76cc29d78a97c", + }, + } + references = append(references, revReference) + type args struct { + repoURL string + revision string + dryCommitMetadata *v1alpha1.RevisionMetadata + template string + } + tests := []struct { + name string + args args + want string + wantErr bool + }{ + { + name: "test template", + args: args{ + repoURL: "https://github.com/test/argocd-example-apps", + revision: "3ff41cc5247197a6caf50216c4c76cc29d78a97d", + dryCommitMetadata: &v1alpha1.RevisionMetadata{ + Author: "test test@test.com", + Date: &metav1.Time{ + Time: metav1.Now().Time, + }, + Message: message, + References: references, + }, + template: settings.CommitMessageTemplate, + }, + want: `3ff41cc: testn +Argocd-reference-commit-repourl: https://github.com/test/argocd-example-apps +Argocd-reference-commit-author: Argocd-reference-commit-author +Argocd-reference-commit-subject: testhydratormd +Signed-off-by: testUser + +Co-authored-by: testAuthor +Co-authored-by: test test@test.com +`, + }, + { + name: "test empty template", + args: args{ + repoURL: "https://github.com/test/argocd-example-apps", + revision: "3ff41cc5247197a6caf50216c4c76cc29d78a97d", + dryCommitMetadata: &v1alpha1.RevisionMetadata{ + Author: "test test@test.com", + Date: &metav1.Time{ + Time: metav1.Now().Time, + }, + Message: message, + References: references, + }, + }, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getTemplatedCommitMessage(tt.args.repoURL, tt.args.revision, tt.args.template, tt.args.dryCommitMetadata) + if (err != nil) != tt.wantErr { + t.Errorf("Hydrator.getHydratorCommitMessage() error = %v, wantErr %v", err, tt.wantErr) + return + } + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/controller/hydrator/mocks/Dependencies.go b/controller/hydrator/mocks/Dependencies.go index 1e6b0aab2dc79..c8c5937d3b955 100644 --- a/controller/hydrator/mocks/Dependencies.go +++ b/controller/hydrator/mocks/Dependencies.go @@ -81,6 +81,59 @@ func (_c *Dependencies_AddHydrationQueueItem_Call) RunAndReturn(run func(key typ return _c } +// GetHydratorCommitMessageTemplate provides a mock function for the type Dependencies +func (_mock *Dependencies) GetHydratorCommitMessageTemplate() (string, error) { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for GetHydratorCommitMessageTemplate") + } + + var r0 string + var r1 error + if returnFunc, ok := ret.Get(0).(func() (string, error)); ok { + return returnFunc() + } + if returnFunc, ok := ret.Get(0).(func() string); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(string) + } + if returnFunc, ok := ret.Get(1).(func() error); ok { + r1 = returnFunc() + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// Dependencies_GetHydratorCommitMessageTemplate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetHydratorCommitMessageTemplate' +type Dependencies_GetHydratorCommitMessageTemplate_Call struct { + *mock.Call +} + +// GetHydratorCommitMessageTemplate is a helper method to define mock.On call +func (_e *Dependencies_Expecter) GetHydratorCommitMessageTemplate() *Dependencies_GetHydratorCommitMessageTemplate_Call { + return &Dependencies_GetHydratorCommitMessageTemplate_Call{Call: _e.mock.On("GetHydratorCommitMessageTemplate")} +} + +func (_c *Dependencies_GetHydratorCommitMessageTemplate_Call) Run(run func()) *Dependencies_GetHydratorCommitMessageTemplate_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Dependencies_GetHydratorCommitMessageTemplate_Call) Return(s string, err error) *Dependencies_GetHydratorCommitMessageTemplate_Call { + _c.Call.Return(s, err) + return _c +} + +func (_c *Dependencies_GetHydratorCommitMessageTemplate_Call) RunAndReturn(run func() (string, error)) *Dependencies_GetHydratorCommitMessageTemplate_Call { + _c.Call.Return(run) + return _c +} + // GetProcessableAppProj provides a mock function for the type Dependencies func (_mock *Dependencies) GetProcessableAppProj(app *v1alpha1.Application) (*v1alpha1.AppProject, error) { ret := _mock.Called(app) diff --git a/controller/hydrator_dependencies.go b/controller/hydrator_dependencies.go index 7ae95d627494d..1e0e63954dc62 100644 --- a/controller/hydrator_dependencies.go +++ b/controller/hydrator_dependencies.go @@ -97,3 +97,12 @@ func (ctrl *ApplicationController) PersistAppHydratorStatus(orig *appv1.Applicat func (ctrl *ApplicationController) AddHydrationQueueItem(key types.HydrationQueueKey) { ctrl.hydrationQueue.AddRateLimited(key) } + +func (ctrl *ApplicationController) GetHydratorCommitMessageTemplate() (string, error) { + sourceHydratorCommitMessageKey, err := ctrl.settingsMgr.GetSourceHydratorCommitMessageTemplate() + if err != nil { + return "", fmt.Errorf("failed to get sourceHydrator commit message template key: %w", err) + } + + return sourceHydratorCommitMessageKey, nil +} diff --git a/controller/hydrator_dependencies_test.go b/controller/hydrator_dependencies_test.go index 39d1872d435d2..0e73ff5237238 100644 --- a/controller/hydrator_dependencies_test.go +++ b/controller/hydrator_dependencies_test.go @@ -14,6 +14,7 @@ import ( "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" "github.com/argoproj/argo-cd/v3/reposerver/apiclient" "github.com/argoproj/argo-cd/v3/test" + "github.com/argoproj/argo-cd/v3/util/settings" ) func TestGetRepoObjs(t *testing.T) { @@ -77,3 +78,46 @@ func TestGetRepoObjs(t *testing.T) { assert.Equal(t, "ConfigMap", objs[0].GetKind()) } + +func TestGetHydratorCommitMessageTemplate_WhenTemplateisNotDefined_FallbackToDefault(t *testing.T) { + cm := test.NewConfigMap() + cmBytes, _ := json.Marshal(cm) + + data := fakeData{ + manifestResponse: &apiclient.ManifestResponse{ + Manifests: []string{string(cmBytes)}, + Namespace: test.FakeDestNamespace, + Server: test.FakeClusterURL, + Revision: "abc123", + }, + } + + ctrl := newFakeControllerWithResync(&data, time.Minute, nil, errors.New("this should not be called")) + + tmpl, err := ctrl.GetHydratorCommitMessageTemplate() + require.NoError(t, err) + assert.NotEmpty(t, tmpl) // should fallback to default + assert.Equal(t, settings.CommitMessageTemplate, tmpl) +} + +func TestGetHydratorCommitMessageTemplate(t *testing.T) { + cm := test.NewFakeConfigMap() + cm.Data["sourceHydrator.commitMessageTemplate"] = settings.CommitMessageTemplate + cmBytes, _ := json.Marshal(cm) + + data := fakeData{ + manifestResponse: &apiclient.ManifestResponse{ + Manifests: []string{string(cmBytes)}, + Namespace: test.FakeDestNamespace, + Server: test.FakeClusterURL, + Revision: "abc123", + }, + configMapData: cm.Data, + } + + ctrl := newFakeControllerWithResync(&data, time.Minute, nil, errors.New("this should not be called")) + + tmpl, err := ctrl.GetHydratorCommitMessageTemplate() + require.NoError(t, err) + assert.NotEmpty(t, tmpl) +} diff --git a/docs/operator-manual/argocd-cm.yaml b/docs/operator-manual/argocd-cm.yaml index 25b5119e73d2a..209a706048b29 100644 --- a/docs/operator-manual/argocd-cm.yaml +++ b/docs/operator-manual/argocd-cm.yaml @@ -439,3 +439,22 @@ data: # application.sync.impersonation.enabled enables application sync to use a custom service account, via impersonation. This allows decoupling sync from control-plane service account. application.sync.impersonation.enabled: "false" + + ### SourceHydrator commit message template. + # This template iterates through the fields in the `.metadata` object, + # and formats them based on their type (map, array, or primitive values). + # This is the default template and targets specific metadata properties + sourceHydrator.commitMessageTemplate: | + {{.metadata.drySha | trunc 7}}: {{ .metadata.subject }} + {{- if .metadata.body }} + + {{ .metadata.body }} + {{- end }} + {{ range $ref := .metadata.references }} + {{- if and $ref.commit $ref.commit.author }} + Co-authored-by: {{ $ref.commit.author }} + {{- end }} + {{- end }} + {{- if .metadata.author }} + Co-authored-by: {{ .metadata.author }} + {{- end }} diff --git a/docs/user-guide/source-hydrator.md b/docs/user-guide/source-hydrator.md index 1a602d833b360..850b36f51d7d3 100644 --- a/docs/user-guide/source-hydrator.md +++ b/docs/user-guide/source-hydrator.md @@ -262,6 +262,34 @@ specified more than once, the last one will be used. All trailers are optional. If a trailer is not specified, the corresponding field in the metadata will be omitted. +## Commit Message Template + +The commit message is generated using a [Go text/template](https://pkg.go.dev/text/template), optionally configured by the user via the argocd-cm ConfigMap. The template is rendered using the values from `hydrator.metadata`. The template can be multi-line, allowing users to define a subject line, body and optional trailers. To define the commit message template, you need to set the `sourceHydrator.commitMessageTemplate` field in argocd-cm ConfigMap. + +The template may functions from the [Sprig function library](https://github.com/Masterminds/sprig). + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: argocd-cm + namespace: argocd +data: + sourceHydrator.commitMessageTemplate: | + {{.metadata.drySha | trunc 7}}: {{ .metadata.subject }} + {{- if .metadata.body }} + + {{ .metadata.body }} + {{- end }} + {{ range $ref := .metadata.references }} + {{- if and $ref.commit $ref.commit.author }} + Co-authored-by: {{ $ref.commit.author }} + {{- end }} + {{- end }} + {{- if .metadata.author }} + Co-authored-by: {{ .metadata.author }} + {{- end }} + ### Credential Templates Credential templates allow a single credential to be used for multiple repositories. The source hydrator supports credential templates. For example, if you setup credential templates for the URL prefix `https://github.com/argoproj`, these credentials will be used for all repositories with this URL as prefix (e.g. `https://github.com/argoproj/argocd-example-apps`) that do not have their own credentials configured. diff --git a/util/hydrator/hydrator.go b/util/hydrator/hydrator.go new file mode 100644 index 0000000000000..014a620add260 --- /dev/null +++ b/util/hydrator/hydrator.go @@ -0,0 +1,60 @@ +package hydrator + +import ( + "strings" + "time" + + log "github.com/sirupsen/logrus" + + appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" + "github.com/argoproj/argo-cd/v3/util/git" +) + +// HydratorCommitMetadata defines the struct used by both Controller and commitServer +// to define the templated commit message and the hydrated manifest +type HydratorCommitMetadata struct { + RepoURL string `json:"repoURL,omitempty"` + DrySHA string `json:"drySha,omitempty"` + Commands []string `json:"commands,omitempty"` + Author string `json:"author,omitempty"` + Date string `json:"date,omitempty"` + // Subject is the subject line of the DRY commit message, i.e. `git show --format=%s`. + Subject string `json:"subject,omitempty"` + // Body is the body of the DRY commit message, excluding the subject line, i.e. `git show --format=%b`. + // Known Argocd- trailers with valid values are removed, but all other trailers are kept. + Body string `json:"body,omitempty"` + References []appv1.RevisionReference `json:"references,omitempty"` +} + +// GetCommitMetadata takes repo, drySha and commitMetadata and returns a HydratorCommitMetadata which is a +// common contract controller and commitServer +func GetCommitMetadata(repoUrl, drySha string, dryCommitMetadata *appv1.RevisionMetadata) (HydratorCommitMetadata, error) { //nolint:revive //FIXME(var-naming) + author := "" + message := "" + date := "" + var references []appv1.RevisionReference + if dryCommitMetadata != nil { + author = dryCommitMetadata.Author + message = dryCommitMetadata.Message + if dryCommitMetadata.Date != nil { + date = dryCommitMetadata.Date.Format(time.RFC3339) + } + references = dryCommitMetadata.References + } + + subject, body, _ := strings.Cut(message, "\n\n") + + _, bodyMinusTrailers := git.GetReferences(log.WithFields(log.Fields{"repo": repoUrl, "revision": drySha}), body) + + hydratorCommitMetadata := HydratorCommitMetadata{ + RepoURL: repoUrl, + DrySHA: drySha, + Author: author, + Subject: subject, + Body: bodyMinusTrailers, + Date: date, + References: references, + } + + return hydratorCommitMetadata, nil +} diff --git a/util/hydrator/hydrator_test.go b/util/hydrator/hydrator_test.go new file mode 100644 index 0000000000000..bda8f48412717 --- /dev/null +++ b/util/hydrator/hydrator_test.go @@ -0,0 +1,74 @@ +package hydrator + +import ( + "reflect" + "testing" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" +) + +func TestGetCommitMetadata(t *testing.T) { + repoURL := "https://github.com/test/argocd-example-apps" + drySHA := "3ff41cc5247197a6caf50216c4c76cc29d78a97d" + date := &metav1.Time{Time: metav1.Now().Time} + revisionAuthor := "test test@test.com" + references := make([]appv1.RevisionReference, 0) + revReference := appv1.RevisionReference{ + Commit: &appv1.CommitMetadata{ + Author: "testAuthor", + Subject: "test", + RepoURL: repoURL, + SHA: "3ff41cc5247197a6caf50216c4c76cc29d78a97c", + }, + } + references = append(references, revReference) + hydratedCommitMetadata := HydratorCommitMetadata{ + RepoURL: repoURL, + DrySHA: drySHA, + Author: revisionAuthor, + Date: date.Format(time.RFC3339), + References: references, + Subject: "testMessage", + } + type args struct { + repoURL string + drySha string + dryCommitMetadata *appv1.RevisionMetadata + } + tests := []struct { + name string + args args + want HydratorCommitMetadata + wantErr bool + }{ + { + name: "test GetHydratorCommitMD", + args: args{ + repoURL: repoURL, + drySha: drySHA, + dryCommitMetadata: &appv1.RevisionMetadata{ + Author: revisionAuthor, + Date: date, + Message: "testMessage", + References: references, + }, + }, + want: hydratedCommitMetadata, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetCommitMetadata(tt.args.repoURL, tt.args.drySha, tt.args.dryCommitMetadata) + if (err != nil) != tt.wantErr { + t.Errorf("GetCommitMetadata() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetCommitMetadata() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/util/hydrator/template.go b/util/hydrator/template.go new file mode 100644 index 0000000000000..f11d46540abb1 --- /dev/null +++ b/util/hydrator/template.go @@ -0,0 +1,61 @@ +package hydrator + +import ( + "bytes" + "encoding/json" + "fmt" + "text/template" + + "github.com/Masterminds/sprig/v3" +) + +var sprigFuncMap = sprig.GenericFuncMap() // a singleton for better performance + +func init() { + // Avoid allowing the user to learn things about the environment. + delete(sprigFuncMap, "env") + delete(sprigFuncMap, "expandenv") + delete(sprigFuncMap, "getHostByName") +} + +// Render use a parsed template and calls the Execute to apply the data. +// currently the method supports struct and a map[string]any as data +func Render(tmpl string, data HydratorCommitMetadata) (string, error) { + var dataMap map[string]any + var err error + // short-circuit if template is not defined + if tmpl == "" { + return "", nil + } + dataMap, err = structToMap(data) + if err != nil { + return "", fmt.Errorf("marshaling failed: %w", err) + } + metadata := map[string]any{ + "metadata": dataMap, + } + template, err := template.New("commit-template").Funcs(sprigFuncMap).Parse(tmpl) + if err != nil { + return "", fmt.Errorf("failed to parse template %s: %w", tmpl, err) + } + var replacedTmplBuffer bytes.Buffer + if err = template.Execute(&replacedTmplBuffer, metadata); err != nil { + return "", fmt.Errorf("failed to execute go template %s: %w", tmpl, err) + } + + return replacedTmplBuffer.String(), nil +} + +func structToMap(s any) (map[string]any, error) { + jsonOut, err := json.Marshal(s) + if err != nil { + return nil, err + } + + var result map[string]any + err = json.Unmarshal(jsonOut, &result) + if err != nil { + return nil, err + } + return result, nil +} diff --git a/util/hydrator/template_test.go b/util/hydrator/template_test.go new file mode 100644 index 0000000000000..1d167288edd4e --- /dev/null +++ b/util/hydrator/template_test.go @@ -0,0 +1,99 @@ +package hydrator + +import ( + "testing" + + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" + "github.com/argoproj/argo-cd/v3/util/settings" +) + +func TestRender(t *testing.T) { + tests := []struct { + name string + metadata HydratorCommitMetadata + want string + wantErr bool + }{ + { + name: "author and multiple references", + metadata: HydratorCommitMetadata{ + RepoURL: "https://github.com/test/argocd-example-apps", + DrySHA: "3ff41cc5247197a6caf50216c4c76cc29d78a97d", + Author: "test ", + Date: metav1.Now().String(), + References: []v1alpha1.RevisionReference{ + { + Commit: &v1alpha1.CommitMetadata{ + Author: "ref test ", + Subject: "test", + RepoURL: "https://github.com/test/argocd-example-apps", + SHA: "3ff41cc5247197a6caf50216c4c76cc29d78a97c", + }, + }, + { + Commit: &v1alpha1.CommitMetadata{ + Author: "ref test 2 ", + Subject: "test 2", + RepoURL: "https://github.com/test/argocd-example-apps", + SHA: "abc12345678912345678912345678912345678912", + }, + }, + }, + Body: "testBody", + Subject: "testSubject", + }, + want: `3ff41cc: testSubject + +testBody + +Co-authored-by: ref test +Co-authored-by: ref test 2 +Co-authored-by: test +`, + }, + { + name: "no references", + metadata: HydratorCommitMetadata{ + RepoURL: "https://github.com/test/argocd-example-apps", + DrySHA: "3ff41cc5247197a6caf50216c4c76cc29d78a97d", + Author: "test ", + Date: metav1.Now().String(), + Body: "testBody", + Subject: "testSubject", + }, + want: `3ff41cc: testSubject + +testBody + +Co-authored-by: test +`, + }, + { + name: "no body", + metadata: HydratorCommitMetadata{ + RepoURL: "https://github.com/test/argocd-example-apps", + DrySHA: "3ff41cc5247197a6caf50216c4c76cc29d78a97d", + Author: "test ", + Date: metav1.Now().String(), + Subject: "testSubject", + }, + want: `3ff41cc: testSubject + +Co-authored-by: test +`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Render(settings.CommitMessageTemplate, tt.metadata) + if (err != nil) != tt.wantErr { + t.Errorf("Render() error = %v, wantErr %v", err, tt.wantErr) + return + } + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/util/settings/settings.go b/util/settings/settings.go index ac74206cfaefc..92526a657431a 100644 --- a/util/settings/settings.go +++ b/util/settings/settings.go @@ -45,6 +45,21 @@ import ( tlsutil "github.com/argoproj/argo-cd/v3/util/tls" ) +var CommitMessageTemplate = `{{.metadata.drySha | trunc 7}}: {{ .metadata.subject }} +{{- if .metadata.body }} + +{{ .metadata.body }} +{{- end }} +{{ range $ref := .metadata.references }} +{{- if and $ref.commit $ref.commit.author }} +Co-authored-by: {{ $ref.commit.author }} +{{- end }} +{{- end }} +{{- if .metadata.author }} +Co-authored-by: {{ .metadata.author }} +{{- end }} +` + // ArgoCDSettings holds in-memory runtime configuration options. type ArgoCDSettings struct { // URL is the externally facing URL users will visit to reach Argo CD. @@ -494,6 +509,8 @@ const ( settingUIBannerPositionKey = "ui.bannerposition" // settingsBinaryUrlsKey designates the key for the argocd binary URLs settingsBinaryUrlsKey = "help.download" + // settingsApplicationInstanceLabelKey is the key to configure injected app instance label key + settingsSourceHydratorCommitMessageTemplateKey = "sourceHydrator.commitMessageTemplate" // globalProjectsKey designates the key for global project settings globalProjectsKey = "globalProjects" // initialPasswordSecretName is the name of the secret that will hold the initial admin password @@ -1005,6 +1022,17 @@ func (mgr *SettingsManager) GetResourceOverrides() (map[string]v1alpha1.Resource return resourceOverrides, nil } +func (mgr *SettingsManager) GetSourceHydratorCommitMessageTemplate() (string, error) { + argoCDCM, err := mgr.getConfigMap() + if err != nil { + return "", err + } + if argoCDCM.Data[settingsSourceHydratorCommitMessageTemplateKey] == "" { + return CommitMessageTemplate, nil // in case template is not defined return default + } + return argoCDCM.Data[settingsSourceHydratorCommitMessageTemplateKey], nil +} + func addStatusOverrideToGK(resourceOverrides map[string]v1alpha1.ResourceOverride, groupKind string) { if val, ok := resourceOverrides[groupKind]; ok { val.IgnoreDifferences.JSONPointers = append(val.IgnoreDifferences.JSONPointers, "/status")