diff --git a/go.mod b/go.mod index 45f8ba1c..b5a4abc4 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/spf13/cobra v1.6.1 github.com/stretchr/testify v1.8.4 github.com/whilp/git-urls v0.0.0-20191001220047-6db9661140c0 + github.com/xanzy/go-gitlab v0.93.2 golang.org/x/time v0.3.0 gomodules.xyz/notify v0.1.1 google.golang.org/api v0.132.0 @@ -113,7 +114,7 @@ require ( github.com/aws/aws-sdk-go-v2 v1.17.3 github.com/aws/aws-sdk-go-v2/config v1.18.8 github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-retryablehttp v0.5.3 + github.com/hashicorp/go-retryablehttp v0.7.2 ) replace github.com/prometheus/client_golang => github.com/prometheus/client_golang v1.14.0 diff --git a/go.sum b/go.sum index e4a9d71c..90370464 100644 --- a/go.sum +++ b/go.sum @@ -289,9 +289,11 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFb github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-retryablehttp v0.5.1/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.5.3 h1:QlWt0KvWT0lq8MFppF9tsJGF+ynG7ztc2KIPhzRGk7s= -github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0= +github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= @@ -439,6 +441,8 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/whilp/git-urls v0.0.0-20191001220047-6db9661140c0 h1:qqllXPzXh+So+mmANlX/gCJrgo+1kQyshMoQ+NASzm0= github.com/whilp/git-urls v0.0.0-20191001220047-6db9661140c0/go.mod h1:2rx5KE5FLD0HRfkkpyn8JwbVLBdhgeiOb2D2D9LLKM4= +github.com/xanzy/go-gitlab v0.93.2 h1:kNNf3BYNYn/Zkig0B89fma12l36VLcYSGu7OnaRlRDg= +github.com/xanzy/go-gitlab v0.93.2/go.mod h1:5ryv+MnpZStBH8I/77HuQBsMbBGANtVpLWC15qOjWAw= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/pkg/services/github.go b/pkg/services/github.go index c8f5d4cd..3a03f9b0 100644 --- a/pkg/services/github.go +++ b/pkg/services/github.go @@ -5,23 +5,15 @@ import ( "context" "fmt" "net/http" - "regexp" "strings" texttemplate "text/template" - "unicode/utf8" "github.com/bradleyfalzon/ghinstallation/v2" "github.com/google/go-github/v41/github" log "github.com/sirupsen/logrus" "github.com/spf13/cast" - giturls "github.com/whilp/git-urls" httputil "github.com/argoproj/notifications-engine/pkg/util/http" - "github.com/argoproj/notifications-engine/pkg/util/text" -) - -var ( - gitSuffix = regexp.MustCompile(`\.git$`) ) type GitHubOptions struct { @@ -61,16 +53,16 @@ type GitHubPullRequestComment struct { } const ( - repoURLtemplate = "{{.app.spec.source.repoURL}}" - revisionTemplate = "{{.app.status.operationState.syncResult.revision}}" + githubRepoURLtemplate = "{{.app.spec.source.repoURL}}" + githubRevisionTemplate = "{{.app.status.operationState.syncResult.revision}}" ) func (g *GitHubNotification) GetTemplater(name string, f texttemplate.FuncMap) (Templater, error) { if g.RepoURLPath == "" { - g.RepoURLPath = repoURLtemplate + g.RepoURLPath = githubRepoURLtemplate } if g.RevisionPath == "" { - g.RevisionPath = revisionTemplate + g.RevisionPath = githubRevisionTemplate } repoURL, err := texttemplate.New(name).Funcs(f).Parse(g.RepoURLPath) @@ -276,27 +268,6 @@ type gitHubService struct { client *github.Client } -func trunc(message string, n int) string { - if utf8.RuneCountInString(message) > n { - return string([]rune(message)[0:n-3]) + "..." - } - return message -} - -func fullNameByRepoURL(rawURL string) string { - parsed, err := giturls.Parse(rawURL) - if err != nil { - panic(err) - } - - path := gitSuffix.ReplaceAllString(parsed.Path, "") - if pathParts := text.SplitRemoveEmpty(path, "/"); len(pathParts) >= 2 { - return strings.Join(pathParts[:2], "/") - } - - return path -} - func (g gitHubService) Send(notification Notification, _ Destination) error { if notification.GitHub == nil { return fmt.Errorf("config is empty") diff --git a/pkg/services/github_test.go b/pkg/services/github_test.go index aa191d2d..ef742fbd 100644 --- a/pkg/services/github_test.go +++ b/pkg/services/github_test.go @@ -213,7 +213,7 @@ func TestNewGitHubService_GitHubOptions(t *testing.T) { } } -func TestGetTemplater_Github_PullRequestComment(t *testing.T) { +func TestGetTemplater_GitHub_PullRequestComment(t *testing.T) { n := Notification{ GitHub: &GitHubNotification{ RepoURLPath: "{{.sync.spec.git.repo}}", diff --git a/pkg/services/gitlab.go b/pkg/services/gitlab.go new file mode 100644 index 00000000..b5f518b7 --- /dev/null +++ b/pkg/services/gitlab.go @@ -0,0 +1,332 @@ +package services + +import ( + "bytes" + "context" + "fmt" + "net/url" + "strconv" + "strings" + texttemplate "text/template" + + log "github.com/sirupsen/logrus" + "github.com/xanzy/go-gitlab" +) + +type GitLabOptions struct { + BaseURL string `json:"baseURL"` + Token string `json:"token"` +} + +type GitLabNotification struct { + repoURL string + revision string + revisionIsTag bool + Status *GitLabStatus `json:"status,omitempty"` + Deployment *GitLabDeployment `json:"deployment,omitempty"` + MergeRequestComment *GitLabMergeRequestComment `json:"mergeRequestComment,omitempty"` + RepoURLPath string `json:"repoURLPath,omitempty"` + RevisionPath string `json:"revisionPath,omitempty"` + RevisionIsTagPath string `json:"revisionIsTagPath,omitempty"` +} + +type GitLabStatus struct { + State string `json:"state,omitempty"` + Label string `json:"label,omitempty"` + TargetURL string `json:"targetURL,omitempty"` +} + +type GitLabDeployment struct { + State string `json:"state,omitempty"` + Environment string `json:"environment,omitempty"` + EnvironmentURL string `json:"environmentURL,omitempty"` +} + +type GitLabMergeRequestComment struct { + Content string `json:"content,omitempty"` +} + +const ( + gitlabRepoURLtemplate = "{{.app.spec.source.repoURL}}" + gitlabRevisionTemplate = "{{.app.status.operationState.syncResult.revision}}" + gitlabRevisionIsTagTemplate = "{{gt (len (call .repo.GitCommitMetadata .app.status.operationState.syncResult.revision).Tags) 0}}" +) + +func (g *GitLabNotification) GetTemplater(name string, f texttemplate.FuncMap) (Templater, error) { + if g.RepoURLPath == "" { + g.RepoURLPath = gitlabRepoURLtemplate + } + if g.RevisionPath == "" { + g.RevisionPath = gitlabRevisionTemplate + } + if g.RevisionIsTagPath == "" { + g.RevisionIsTagPath = gitlabRevisionIsTagTemplate + } + + repoURL, err := texttemplate.New(name).Funcs(f).Parse(g.RepoURLPath) + if err != nil { + return nil, err + } + + revision, err := texttemplate.New(name).Funcs(f).Parse(g.RevisionPath) + if err != nil { + return nil, err + } + + revisionIsTag, err := texttemplate.New(name).Funcs(f).Parse(g.RevisionIsTagPath) + if err != nil { + return nil, err + } + + var statusState, label, targetURL *texttemplate.Template + if g.Status != nil { + statusState, err = texttemplate.New(name).Funcs(f).Parse(g.Status.State) + if err != nil { + return nil, err + } + + label, err = texttemplate.New(name).Funcs(f).Parse(g.Status.Label) + if err != nil { + return nil, err + } + + targetURL, err = texttemplate.New(name).Funcs(f).Parse(g.Status.TargetURL) + if err != nil { + return nil, err + } + } + + var deploymentState, environment, environmentURL *texttemplate.Template + if g.Deployment != nil { + deploymentState, err = texttemplate.New(name).Funcs(f).Parse(g.Deployment.State) + if err != nil { + return nil, err + } + + environment, err = texttemplate.New(name).Funcs(f).Parse(g.Deployment.Environment) + if err != nil { + return nil, err + } + + environmentURL, err = texttemplate.New(name).Funcs(f).Parse(g.Deployment.EnvironmentURL) + if err != nil { + return nil, err + } + } + + var mergeRequestCommentContent *texttemplate.Template + if g.MergeRequestComment != nil { + mergeRequestCommentContent, err = texttemplate.New(name).Funcs(f).Parse(g.MergeRequestComment.Content) + if err != nil { + return nil, err + } + } + + return func(notification *Notification, vars map[string]interface{}) error { + if notification.GitLab == nil { + notification.GitLab = &GitLabNotification{ + RepoURLPath: g.RepoURLPath, + RevisionPath: g.RevisionPath, + RevisionIsTagPath: g.RevisionIsTagPath, + } + } + + var repoURLData bytes.Buffer + if err := repoURL.Execute(&repoURLData, vars); err != nil { + return err + } + notification.GitLab.repoURL = repoURLData.String() + + var revisionData bytes.Buffer + if err := revision.Execute(&revisionData, vars); err != nil { + return err + } + notification.GitLab.revision = revisionData.String() + + var revisionIsTagData bytes.Buffer + if err := revisionIsTag.Execute(&revisionIsTagData, vars); err != nil { + return err + } + revisionIsTag, err := strconv.ParseBool(revisionIsTagData.String()) + if err != nil { + return err + } + notification.GitLab.revisionIsTag = revisionIsTag + + if g.Status != nil { + if notification.GitLab.Status == nil { + notification.GitLab.Status = &GitLabStatus{} + } + + var stateData bytes.Buffer + if err := statusState.Execute(&stateData, vars); err != nil { + return err + } + notification.GitLab.Status.State = stateData.String() + + var labelData bytes.Buffer + if err := label.Execute(&labelData, vars); err != nil { + return err + } + notification.GitLab.Status.Label = labelData.String() + + var targetData bytes.Buffer + if err := targetURL.Execute(&targetData, vars); err != nil { + return err + } + notification.GitLab.Status.TargetURL = targetData.String() + } + + if g.Deployment != nil { + if notification.GitLab.Deployment == nil { + notification.GitLab.Deployment = &GitLabDeployment{} + } + + var stateData bytes.Buffer + if err := deploymentState.Execute(&stateData, vars); err != nil { + return err + } + notification.GitLab.Deployment.State = stateData.String() + + var environmentData bytes.Buffer + if err := environment.Execute(&environmentData, vars); err != nil { + return err + } + notification.GitLab.Deployment.Environment = environmentData.String() + + var environmentURLData bytes.Buffer + if err := environmentURL.Execute(&environmentURLData, vars); err != nil { + return err + } + notification.GitLab.Deployment.EnvironmentURL = environmentURLData.String() + } + + if g.MergeRequestComment != nil { + if notification.GitLab.MergeRequestComment == nil { + notification.GitLab.MergeRequestComment = &GitLabMergeRequestComment{} + } + + var contentData bytes.Buffer + if err := mergeRequestCommentContent.Execute(&contentData, vars); err != nil { + return err + } + notification.GitLab.MergeRequestComment.Content = contentData.String() + } + + return nil + }, nil +} + +func NewGitLabService(opts GitLabOptions) (NotificationService, error) { + url := "https://gitlab.com/api/v4" + if opts.BaseURL != "" { + url = opts.BaseURL + } + + var client *gitlab.Client + client, err := gitlab.NewClient( + opts.Token, + gitlab.WithBaseURL(url), + gitlab.WithCustomLogger(log.WithField("service", "gitlab")), + ) + if err != nil { + return nil, err + } + return &GitLabService{ + opts: opts, + client: client, + }, nil +} + +type GitLabService struct { + opts GitLabOptions + + client *gitlab.Client +} + +func (g GitLabService) Send(notification Notification, _ Destination) error { + if notification.GitLab == nil { + return fmt.Errorf("config is empty") + } + + repo := fullNameByRepoURL(notification.GitLab.repoURL) + if len(strings.Split(repo, "/")) < 2 { + return fmt.Errorf("GitLab.repoURL (%s) does not have a `/`", notification.GitLab.repoURL) + } + pid := url.QueryEscape(repo) + + if notification.GitLab.Status != nil { + // TODO: find the max length for status to truncate to + // maximum length for status description is 140 characters + // description := trunc(notification.Message, 140) + + _, _, err := g.client.Commits.SetCommitStatus( + pid, + notification.GitLab.revision, + &gitlab.SetCommitStatusOptions{ + State: gitlab.BuildStateValue(notification.GitLab.Status.State), + Ref: gitlab.String(notification.GitLab.revision), + Name: gitlab.String(notification.GitLab.Status.Label), + TargetURL: ¬ification.GitLab.Status.TargetURL, + Description: gitlab.String(notification.Message), + }, + gitlab.WithContext(context.TODO()), + ) + if err != nil { + return err + } + } + + if notification.GitLab.Deployment != nil { + // maximum length for environment name is 255 characters + environmentName := trunc(notification.GitLab.Deployment.Environment, 255) + + // TODO: If deployment already exists, update it + // find how to get deployment ID + + _, _, err := g.client.Deployments.CreateProjectDeployment( + pid, + &gitlab.CreateProjectDeploymentOptions{ + Environment: gitlab.String(environmentName), + Ref: gitlab.String(notification.GitLab.revision), + SHA: gitlab.String(notification.GitLab.revision), + Tag: gitlab.Bool(notification.GitLab.revisionIsTag), + Status: gitlab.DeploymentStatus(gitlab.DeploymentStatusValue(notification.GitLab.Deployment.State)), + }, + gitlab.WithContext(context.TODO()), + ) + if err != nil { + return err + } + } + + if notification.GitLab.MergeRequestComment != nil { + // maximum length for merge request comment body is 1000000 characters + body := trunc(notification.GitLab.MergeRequestComment.Content, 1000000) + + mrs, _, err := g.client.Commits.ListMergeRequestsByCommit( + pid, + notification.GitLab.revision, + gitlab.WithContext(context.TODO()), + ) + if err != nil { + return err + } + + for _, mr := range mrs { + _, _, err := g.client.Notes.CreateMergeRequestNote( + pid, + mr.IID, + &gitlab.CreateMergeRequestNoteOptions{ + Body: gitlab.String(body), + }, + gitlab.WithContext(context.TODO()), + ) + if err != nil { + return err + } + } + } + + return nil +} diff --git a/pkg/services/gitlab_test.go b/pkg/services/gitlab_test.go new file mode 100644 index 00000000..3e6d779b --- /dev/null +++ b/pkg/services/gitlab_test.go @@ -0,0 +1,248 @@ +package services + +import ( + "testing" + "text/template" + + "github.com/stretchr/testify/assert" +) + +func TestGetTemplater_GitLab(t *testing.T) { + n := Notification{ + GitLab: &GitLabNotification{ + Status: &GitLabStatus{ + State: "{{.context.state}}", + Label: "continuous-delivery/{{.app.metadata.name}}", + TargetURL: "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}", + }, + }, + } + templater, err := n.GetTemplater("", template.FuncMap{}) + + if !assert.NoError(t, err) { + return + } + + var notification Notification + err = templater(¬ification, map[string]interface{}{ + "context": map[string]interface{}{ + "argocdUrl": "https://example.com", + "state": "success", + }, + "app": map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "argocd-notifications", + }, + "spec": map[string]interface{}{ + "source": map[string]interface{}{ + "repoURL": "https://gitlab.com/argoproj-labs/argocd-notifications.git", + }, + }, + "status": map[string]interface{}{ + "operationState": map[string]interface{}{ + "syncResult": map[string]interface{}{ + "revision": "0123456789", + }, + }, + }, + }, + }) + + if !assert.NoError(t, err) { + return + } + + assert.Equal(t, "https://gitlab.com/argoproj-labs/argocd-notifications.git", notification.GitLab.repoURL) + assert.Equal(t, "0123456789", notification.GitLab.revision) + assert.Equal(t, "success", notification.GitLab.Status.State) + assert.Equal(t, "continuous-delivery/argocd-notifications", notification.GitLab.Status.Label) + assert.Equal(t, "https://example.com/applications/argocd-notifications", notification.GitLab.Status.TargetURL) +} + +func TestGetTemplater_GitLab_Custom_Resource(t *testing.T) { + n := Notification{ + GitLab: &GitLabNotification{ + RepoURLPath: "{{.sync.spec.git.repo}}", + RevisionPath: "{{.sync.status.lastSyncedCommit}}", + RevisionIsTagPath: "true", + Status: &GitLabStatus{ + State: "synced", + Label: "continuous-delivery/{{.sync.metadata.name}}", + }, + }, + } + templater, err := n.GetTemplater("", template.FuncMap{}) + + if !assert.NoError(t, err) { + return + } + + var notification Notification + err = templater(¬ification, map[string]interface{}{ + "sync": map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "root-sync-test", + }, + "spec": map[string]interface{}{ + "git": map[string]interface{}{ + "repo": "https://gitlab.com/argoproj-labs/argocd-notifications.git", + }, + }, + "status": map[string]interface{}{ + "lastSyncedCommit": "0123456789", + }, + }, + }) + + if !assert.NoError(t, err) { + return + } + + assert.Equal(t, "{{.sync.spec.git.repo}}", notification.GitLab.RepoURLPath) + assert.Equal(t, "{{.sync.status.lastSyncedCommit}}", notification.GitLab.RevisionPath) + assert.Equal(t, "true", notification.GitLab.RevisionIsTagPath) + assert.Equal(t, "https://gitlab.com/argoproj-labs/argocd-notifications.git", notification.GitLab.repoURL) + assert.Equal(t, "0123456789", notification.GitLab.revision) + assert.Equal(t, "synced", notification.GitLab.Status.State) + assert.Equal(t, "continuous-delivery/root-sync-test", notification.GitLab.Status.Label) + assert.Equal(t, "", notification.GitLab.Status.TargetURL) +} + +func TestSend_GitLabService_BadURL(t *testing.T) { + e := gitHubService{}.Send( + Notification{ + GitLab: &GitLabNotification{ + repoURL: "hello", + }, + }, + Destination{ + Service: "", + Recipient: "", + }, + ) + assert.ErrorContains(t, e, "does not have a `/`") +} + +func TestGetTemplater_GitLab_Deployment(t *testing.T) { + n := Notification{ + GitLab: &GitLabNotification{ + RepoURLPath: "{{.sync.spec.git.repo}}", + RevisionPath: "{{.sync.status.lastSyncedCommit}}", + RevisionIsTagPath: "true", + Deployment: &GitLabDeployment{ + State: "success", + Environment: "production", + EnvironmentURL: "https://argoproj.gitlab.io", + }, + }, + } + templater, err := n.GetTemplater("", template.FuncMap{}) + + if !assert.NoError(t, err) { + return + } + + var notification Notification + err = templater(¬ification, map[string]interface{}{ + "sync": map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "root-sync-test", + }, + "spec": map[string]interface{}{ + "git": map[string]interface{}{ + "repo": "https://gitlab.com/argoproj-labs/argocd-notifications.git", + }, + }, + "status": map[string]interface{}{ + "lastSyncedCommit": "0123456789", + }, + }, + }) + + if !assert.NoError(t, err) { + return + } + + assert.Equal(t, "{{.sync.spec.git.repo}}", notification.GitLab.RepoURLPath) + assert.Equal(t, "{{.sync.status.lastSyncedCommit}}", notification.GitLab.RevisionPath) + assert.Equal(t, "true", notification.GitLab.RevisionIsTagPath) + assert.Equal(t, "https://gitlab.com/argoproj-labs/argocd-notifications.git", notification.GitLab.repoURL) + assert.Equal(t, "0123456789", notification.GitLab.revision) + assert.Equal(t, "success", notification.GitLab.Deployment.State) + assert.Equal(t, "production", notification.GitLab.Deployment.Environment) + assert.Equal(t, "https://argoproj.gitlab.io", notification.GitLab.Deployment.EnvironmentURL) +} + +func TestNewGitLabService_GitLabOptions(t *testing.T) { + tests := []struct { + name string + token string + }{ + { + name: "empty", + token: "", + }, + { + name: "string", + token: "123456789", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := NewGitLabService(GitLabOptions{ + Token: tt.token, + }) + + if !assert.NoError(t, err) { + return + } + }) + } +} + +func TestGetTemplater_GitLab_MergeRequestComment(t *testing.T) { + n := Notification{ + GitLab: &GitLabNotification{ + RepoURLPath: "{{.sync.spec.git.repo}}", + RevisionPath: "{{.sync.status.lastSyncedCommit}}", + RevisionIsTagPath: "true", + MergeRequestComment: &GitLabMergeRequestComment{ + Content: "This is a comment", + }, + }, + } + templater, err := n.GetTemplater("", template.FuncMap{}) + + if !assert.NoError(t, err) { + return + } + + var notification Notification + err = templater(¬ification, map[string]interface{}{ + "sync": map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": "root-sync-test", + }, + "spec": map[string]interface{}{ + "git": map[string]interface{}{ + "repo": "https://gitlab.com/argoproj-labs/argocd-notifications.git", + }, + }, + "status": map[string]interface{}{ + "lastSyncedCommit": "0123456789", + }, + }, + }) + + if !assert.NoError(t, err) { + return + } + + assert.Equal(t, "{{.sync.spec.git.repo}}", notification.GitLab.RepoURLPath) + assert.Equal(t, "{{.sync.status.lastSyncedCommit}}", notification.GitLab.RevisionPath) + assert.Equal(t, "true", notification.GitLab.RevisionIsTagPath) + assert.Equal(t, "https://gitlab.com/argoproj-labs/argocd-notifications.git", notification.GitLab.repoURL) + assert.Equal(t, "0123456789", notification.GitLab.revision) + assert.Equal(t, "This is a comment", notification.GitLab.MergeRequestComment.Content) +} diff --git a/pkg/services/newrelic.go b/pkg/services/newrelic.go index f0717be1..bae21da4 100644 --- a/pkg/services/newrelic.go +++ b/pkg/services/newrelic.go @@ -31,8 +31,12 @@ var ( ErrMissingApiKey = errors.New("apiKey is missing") ) +const ( + newrelicRevisionTemplate = "{{.app.status.operationState.syncResult.revision}}" +) + func (n *NewrelicNotification) GetTemplater(name string, f texttemplate.FuncMap) (Templater, error) { - revision, err := texttemplate.New(name).Funcs(f).Parse(revisionTemplate) + revision, err := texttemplate.New(name).Funcs(f).Parse(newrelicRevisionTemplate) if err != nil { return nil, err } diff --git a/pkg/services/services.go b/pkg/services/services.go index ba6376aa..f4bae570 100644 --- a/pkg/services/services.go +++ b/pkg/services/services.go @@ -27,6 +27,7 @@ type Notification struct { Pagerduty *PagerDutyNotification `json:"pagerduty,omitempty"` PagerdutyV2 *PagerDutyV2Notification `json:"pagerdutyv2,omitempty"` Newrelic *NewrelicNotification `json:"newrelic,omitempty"` + GitLab *GitLabNotification `json:"gitlab,omitempty"` } // Destinations holds notification destinations group by trigger @@ -103,6 +104,9 @@ func (n *Notification) GetTemplater(name string, f texttemplate.FuncMap) (Templa if n.Newrelic != nil { sources = append(sources, n.Newrelic) } + if n.GitLab != nil { + sources = append(sources, n.GitLab) + } return n.getTemplater(name, f, sources) } @@ -223,6 +227,12 @@ func NewService(serviceType string, optsData []byte) (NotificationService, error return nil, err } return NewWebexService(opts), nil + case "gitlab": + var opts GitLabOptions + if err := yaml.Unmarshal(optsData, &opts); err != nil { + return nil, err + } + return NewGitLabService(opts) default: return nil, fmt.Errorf("service type '%s' is not supported", serviceType) } diff --git a/pkg/services/utils.go b/pkg/services/utils.go new file mode 100644 index 00000000..2c6ea930 --- /dev/null +++ b/pkg/services/utils.go @@ -0,0 +1,33 @@ +package services + +import ( + "regexp" + "strings" + "unicode/utf8" + + giturls "github.com/whilp/git-urls" + + "github.com/argoproj/notifications-engine/pkg/util/text" +) + +var ( + gitSuffix = regexp.MustCompile(`\.git$`) +) + +func trunc(message string, n int) string { + if utf8.RuneCountInString(message) > n { + return string([]rune(message)[0:n-3]) + "..." + } + return message +} + +func fullNameByRepoURL(rawURL string) string { + parsed, err := giturls.Parse(rawURL) + if err != nil { + panic(err) + } + + path := gitSuffix.ReplaceAllString(parsed.Path, "") + pathParts := text.SplitRemoveEmpty(path, "/") + return strings.Join(pathParts, "/") +}