Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Slack notifications matched on base branch name #3644

Merged
merged 5 commits into from
Aug 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions runatlantis.io/docs/using-slack-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ In your Atlantis configuration you can now add the following:
webhooks:
- event: apply
workspace-regex: .*
branch-regex: .*
kind: slack
channel: my-channel
```
Expand All @@ -56,6 +57,7 @@ config: |
webhooks:
- event: apply
workspace-regex: .*
branch-regex: .*
kind: slack
channel: my-channel
```
Expand Down
10 changes: 6 additions & 4 deletions server/events/webhooks/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,26 @@ import (
type SlackWebhook struct {
Client SlackClient
WorkspaceRegex *regexp.Regexp
BranchRegex *regexp.Regexp
Channel string
}

func NewSlack(r *regexp.Regexp, channel string, client SlackClient) (*SlackWebhook, error) {
func NewSlack(wr *regexp.Regexp, br *regexp.Regexp, channel string, client SlackClient) (*SlackWebhook, error) {
if err := client.AuthTest(); err != nil {
return nil, fmt.Errorf("testing slack authentication: %s. Verify your slack-token is valid", err)
}

return &SlackWebhook{
Client: client,
WorkspaceRegex: r,
WorkspaceRegex: wr,
BranchRegex: br,
Channel: channel,
}, nil
}

// Send sends the webhook to Slack if the workspace matches the regex.
// Send sends the webhook to Slack if workspace and branch matches their respective regex.
func (s *SlackWebhook) Send(log logging.SimpleLogging, applyResult ApplyResult) error {
if !s.WorkspaceRegex.MatchString(applyResult.Workspace) {
if !s.WorkspaceRegex.MatchString(applyResult.Workspace) || !s.BranchRegex.MatchString(applyResult.Pull.BaseBranch) {
return nil
}
return s.Client.PostMessage(s.Channel, applyResult)
Expand Down
5 changes: 5 additions & 0 deletions server/events/webhooks/slack_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,11 @@ func (d *DefaultSlackClient) createAttachments(applyResult ApplyResult) []slack.
Value: applyResult.Workspace,
Short: true,
},
{
Title: "Branch",
Value: applyResult.Pull.BaseBranch,
Short: true,
},
{
Title: "User",
Value: applyResult.User.Username,
Expand Down
5 changes: 3 additions & 2 deletions server/events/webhooks/slack_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,9 @@ func setup(t *testing.T) {
FullName: "runatlantis/atlantis",
},
Pull: models.PullRequest{
Num: 1,
URL: "url",
Num: 1,
URL: "url",
BaseBranch: "main",
},
User: models.User{
Username: "lkysow",
Expand Down
9 changes: 9 additions & 0 deletions server/events/webhooks/slack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"testing"

. "github.com/petergtz/pegomock/v4"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/events/webhooks"
"github.com/runatlantis/atlantis/server/events/webhooks/mocks"
"github.com/runatlantis/atlantis/server/logging"
Expand All @@ -35,10 +36,14 @@ func TestSend_PostMessage(t *testing.T) {
hook := webhooks.SlackWebhook{
Client: client,
WorkspaceRegex: regex,
BranchRegex: regex,
Channel: channel,
}
result := webhooks.ApplyResult{
Workspace: "production",
Pull: models.PullRequest{
BaseBranch: "main",
},
}

t.Log("PostMessage should be called, doesn't matter if it errors or not")
Expand All @@ -57,10 +62,14 @@ func TestSend_NoopSuccess(t *testing.T) {
hook := webhooks.SlackWebhook{
Client: client,
WorkspaceRegex: regex,
BranchRegex: regex,
Channel: channel,
}
result := webhooks.ApplyResult{
Workspace: "production",
Pull: models.PullRequest{
BaseBranch: "main",
},
}
err = hook.Send(logging.NewNoopLogger(t), result)
Ok(t, err)
Expand Down
9 changes: 7 additions & 2 deletions server/events/webhooks/webhooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,19 @@ type MultiWebhookSender struct {
type Config struct {
Event string
WorkspaceRegex string
BranchRegex string
Kind string
Channel string
}

func NewMultiWebhookSender(configs []Config, client SlackClient) (*MultiWebhookSender, error) {
var webhooks []Sender
for _, c := range configs {
r, err := regexp.Compile(c.WorkspaceRegex)
wr, err := regexp.Compile(c.WorkspaceRegex)
if err != nil {
return nil, err
}
br, err := regexp.Compile(c.BranchRegex)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this become a breaking change? It might be better to use .* when passing empty string.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I tested a webhooks config without defining either workspace-regex or branch-regex and the Slack notification still went through. For example, regexp.MatchString("", "main") evaluates to true, so looks like this will still function fine if the user does not set the branch-regex in the config.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, thanks for your confirmation 👍

if err != nil {
return nil, err
}
Expand All @@ -77,7 +82,7 @@ func NewMultiWebhookSender(configs []Config, client SlackClient) (*MultiWebhookS
if c.Channel == "" {
return nil, errors.New("must specify \"channel\" if using a webhook of \"kind: slack\"")
}
slack, err := NewSlack(r, c.Channel, client)
slack, err := NewSlack(wr, br, c.Channel, client)
if err != nil {
return nil, err
}
Expand Down
32 changes: 30 additions & 2 deletions server/events/webhooks/webhooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const (
var validConfig = webhooks.Config{
Event: validEvent,
WorkspaceRegex: validRegex,
BranchRegex: validRegex,
Kind: validKind,
Channel: validChannel,
}
Expand All @@ -42,8 +43,8 @@ func validConfigs() []webhooks.Config {
return []webhooks.Config{validConfig}
}

func TestNewWebhooksManager_InvalidRegex(t *testing.T) {
t.Log("When given an invalid regex in a config, an error is returned")
func TestNewWebhooksManager_InvalidWorkspaceRegex(t *testing.T) {
t.Log("When given an invalid workspace regex in a config, an error is returned")
RegisterMockTestingT(t)
client := mocks.NewMockSlackClient()

Expand All @@ -55,6 +56,33 @@ func TestNewWebhooksManager_InvalidRegex(t *testing.T) {
Assert(t, strings.Contains(err.Error(), "error parsing regexp"), "expected regex error")
}

func TestNewWebhooksManager_InvalidBranchRegex(t *testing.T) {
t.Log("When given an invalid branch regex in a config, an error is returned")
RegisterMockTestingT(t)
client := mocks.NewMockSlackClient()

invalidRegex := "("
configs := validConfigs()
configs[0].BranchRegex = invalidRegex
_, err := webhooks.NewMultiWebhookSender(configs, client)
Assert(t, err != nil, "expected error")
Assert(t, strings.Contains(err.Error(), "error parsing regexp"), "expected regex error")
}

func TestNewWebhooksManager_InvalidBranchAndWorkspaceRegex(t *testing.T) {
t.Log("When given an invalid branch and invalid workspace regex in a config, an error is returned")
RegisterMockTestingT(t)
client := mocks.NewMockSlackClient()

invalidRegex := "("
configs := validConfigs()
configs[0].WorkspaceRegex = invalidRegex
configs[0].BranchRegex = invalidRegex
_, err := webhooks.NewMultiWebhookSender(configs, client)
Assert(t, err != nil, "expected error")
Assert(t, strings.Contains(err.Error(), "error parsing regexp"), "expected regex error")
}

func TestNewWebhooksManager_NoEvent(t *testing.T) {
t.Log("When the event key is not specified in a config, an error is returned")
RegisterMockTestingT(t)
Expand Down
5 changes: 5 additions & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ type WebhookConfig struct {
// that is being modified for this event. If the regex matches, we'll
// send the webhook, ex. "production.*".
WorkspaceRegex string `mapstructure:"workspace-regex"`
// BranchRegex is a regex that is used to match against the base branch
// that is being modified for this event. If the regex matches, we'll
// send the webhook, ex. "main.*".
BranchRegex string `mapstructure:"branch-regex"`
// Kind is the type of webhook we should send, ex. slack.
Kind string `mapstructure:"kind"`
// Channel is the channel to send this webhook to. It only applies to
Expand Down Expand Up @@ -344,6 +348,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
for _, c := range userConfig.Webhooks {
config := webhooks.Config{
Channel: c.Channel,
BranchRegex: c.BranchRegex,
Event: c.Event,
Kind: c.Kind,
WorkspaceRegex: c.WorkspaceRegex,
Expand Down