-
Notifications
You must be signed in to change notification settings - Fork 48
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
Adding slack integration for posting to channel on applies. Fixes #179 #180
Changes from 10 commits
d5d8e12
5491a30
3be7f8c
3976d46
8c1dc0f
46d9f44
f94cede
e35fd72
4cd775b
ed1d8cd
4e57157
0d39196
b17eec2
66a6f6f
9b680e5
70ad95f
e1d059e
1ebc62b
54e4207
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
// Automatically generated by pegomock. DO NOT EDIT! | ||
// Source: webhooks.go | ||
|
||
package mocks | ||
|
||
import ( | ||
logging "github.com/hootsuite/atlantis/server/logging" | ||
pegomock "github.com/petergtz/pegomock" | ||
"reflect" | ||
) | ||
|
||
type MockWebhooksSender struct { | ||
fail func(message string, callerSkip ...int) | ||
} | ||
|
||
func NewMockWebhooksSender() *MockWebhooksSender { | ||
return &MockWebhooksSender{fail: pegomock.GlobalFailHandler} | ||
} | ||
|
||
func (mock *MockWebhooksSender) Send(log *logging.SimpleLogger, result ApplyResult) { | ||
params := []pegomock.Param{log, result} | ||
pegomock.GetGenericMockFrom(mock).Invoke("Send", params, []reflect.Type{}) | ||
} | ||
|
||
func (mock *MockWebhooksSender) VerifyWasCalledOnce() *VerifierWebhooksSender { | ||
return &VerifierWebhooksSender{mock, pegomock.Times(1), nil} | ||
} | ||
|
||
func (mock *MockWebhooksSender) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierWebhooksSender { | ||
return &VerifierWebhooksSender{mock, invocationCountMatcher, nil} | ||
} | ||
|
||
func (mock *MockWebhooksSender) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierWebhooksSender { | ||
return &VerifierWebhooksSender{mock, invocationCountMatcher, inOrderContext} | ||
} | ||
|
||
type VerifierWebhooksSender struct { | ||
mock *MockWebhooksSender | ||
invocationCountMatcher pegomock.Matcher | ||
inOrderContext *pegomock.InOrderContext | ||
} | ||
|
||
func (verifier *VerifierWebhooksSender) Send(log *logging.SimpleLogger, result ApplyResult) *WebhooksSender_Send_OngoingVerification { | ||
params := []pegomock.Param{log, result} | ||
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Send", params) | ||
return &WebhooksSender_Send_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} | ||
} | ||
|
||
type WebhooksSender_Send_OngoingVerification struct { | ||
mock *MockWebhooksSender | ||
methodInvocations []pegomock.MethodInvocation | ||
} | ||
|
||
func (c *WebhooksSender_Send_OngoingVerification) GetCapturedArguments() (*logging.SimpleLogger, ApplyResult) { | ||
log, result := c.GetAllCapturedArguments() | ||
return log[len(log)-1], result[len(result)-1] | ||
} | ||
|
||
func (c *WebhooksSender_Send_OngoingVerification) GetAllCapturedArguments() (_param0 []*logging.SimpleLogger, _param1 []ApplyResult) { | ||
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) | ||
if len(params) > 0 { | ||
_param0 = make([]*logging.SimpleLogger, len(params[0])) | ||
for u, param := range params[0] { | ||
_param0[u] = param.(*logging.SimpleLogger) | ||
} | ||
_param1 = make([]ApplyResult, len(params[1])) | ||
for u, param := range params[1] { | ||
_param1[u] = param.(ApplyResult) | ||
} | ||
} | ||
return | ||
} | ||
|
||
type MockWebhookSender struct { | ||
fail func(message string, callerSkip ...int) | ||
} | ||
|
||
func NewMockWebhookSender() *MockWebhookSender { | ||
return &MockWebhookSender{fail: pegomock.GlobalFailHandler} | ||
} | ||
|
||
func (mock *MockWebhookSender) Send(_param0 ApplyResult) error { | ||
params := []pegomock.Param{_param0} | ||
result := pegomock.GetGenericMockFrom(mock).Invoke("Send", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) | ||
var ret0 error | ||
if len(result) != 0 { | ||
if result[0] != nil { | ||
ret0 = result[0].(error) | ||
} | ||
} | ||
return ret0 | ||
} | ||
|
||
func (mock *MockWebhookSender) VerifyWasCalledOnce() *VerifierWebhookSender { | ||
return &VerifierWebhookSender{mock, pegomock.Times(1), nil} | ||
} | ||
|
||
func (mock *MockWebhookSender) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierWebhookSender { | ||
return &VerifierWebhookSender{mock, invocationCountMatcher, nil} | ||
} | ||
|
||
func (mock *MockWebhookSender) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierWebhookSender { | ||
return &VerifierWebhookSender{mock, invocationCountMatcher, inOrderContext} | ||
} | ||
|
||
type VerifierWebhookSender struct { | ||
mock *MockWebhookSender | ||
invocationCountMatcher pegomock.Matcher | ||
inOrderContext *pegomock.InOrderContext | ||
} | ||
|
||
func (verifier *VerifierWebhookSender) Send(_param0 ApplyResult) *WebhookSender_Send_OngoingVerification { | ||
params := []pegomock.Param{_param0} | ||
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Send", params) | ||
return &WebhookSender_Send_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} | ||
} | ||
|
||
type WebhookSender_Send_OngoingVerification struct { | ||
mock *MockWebhookSender | ||
methodInvocations []pegomock.MethodInvocation | ||
} | ||
|
||
func (c *WebhookSender_Send_OngoingVerification) GetCapturedArguments() ApplyResult { | ||
_param0 := c.GetAllCapturedArguments() | ||
return _param0[len(_param0)-1] | ||
} | ||
|
||
func (c *WebhookSender_Send_OngoingVerification) GetAllCapturedArguments() (_param0 []ApplyResult) { | ||
params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) | ||
if len(params) > 0 { | ||
_param0 = make([]ApplyResult, len(params[0])) | ||
for u, param := range params[0] { | ||
_param0[u] = param.(ApplyResult) | ||
} | ||
} | ||
return | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
package webhooks | ||
|
||
import ( | ||
"fmt" | ||
"regexp" | ||
|
||
"github.com/nlopes/slack" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
type SlackWebhook struct { | ||
EnvRegex *regexp.Regexp | ||
Channel string | ||
Token string | ||
Client *slack.Client | ||
} | ||
|
||
func NewSlack(r *regexp.Regexp, channel string, token string) (*SlackWebhook, error) { | ||
slackClient := slack.New(token) | ||
if _, err := slackClient.AuthTest(); err != nil { | ||
return nil, errors.Wrap(err, "testing slack authentication") | ||
} | ||
|
||
// Make sure the slack channel exists. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a good comment. I'd add |
||
channels, err := slackClient.GetChannels(true) | ||
if err != nil { | ||
return nil, err | ||
} | ||
channelExist := false | ||
for _, c := range channels { | ||
if c.Name == channel { | ||
channelExist = true | ||
break | ||
} | ||
} | ||
if !channelExist { | ||
return nil, errors.Errorf("slack channel %q doesn't exist", channel) | ||
} | ||
|
||
return &SlackWebhook{ | ||
Client: slackClient, | ||
EnvRegex: r, | ||
Channel: channel, | ||
Token: token, | ||
}, nil | ||
} | ||
|
||
func (s *SlackWebhook) Send(result ApplyResult) error { | ||
if !s.EnvRegex.MatchString(result.Environment) { | ||
return nil | ||
} | ||
|
||
params := slack.NewPostMessageParameters() | ||
params.Attachments = s.createAttachments(result) | ||
params.AsUser = true | ||
params.EscapeText = false | ||
_, _, err := s.Client.PostMessage(s.Channel, "", params) | ||
return err | ||
} | ||
|
||
func (s *SlackWebhook) createAttachments(result ApplyResult) []slack.Attachment { | ||
var color string | ||
if result.Success { | ||
color = "good" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. make these constants at the top of file underneath the
(also color => colour because we're canadian :D 🇨🇦 ) |
||
} else { | ||
color = "danger" | ||
} | ||
|
||
text := fmt.Sprintf("Applied in <%s|%s>.", result.Pull.URL, result.Repo.FullName) | ||
attachment := slack.Attachment{ | ||
Color: color, | ||
Text: text, | ||
Fields: []slack.AttachmentField{ | ||
slack.AttachmentField{ | ||
Title: "Environment", | ||
Value: result.Environment, | ||
Short: true, | ||
}, | ||
slack.AttachmentField{ | ||
Title: "User", | ||
Value: result.User.Username, | ||
Short: true, | ||
}, | ||
}, | ||
} | ||
var attachments []slack.Attachment | ||
return append(attachments, attachment) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
package webhooks_test | ||
|
||
// todo: actually test | ||
// purposefully empty to trigger coverage report | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nice work figuring this out |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package webhooks | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"regexp" | ||
|
||
"github.com/hootsuite/atlantis/server/events/models" | ||
"github.com/hootsuite/atlantis/server/logging" | ||
) | ||
|
||
const SlackKind = "slack" | ||
const ApplyEvent = "apply" | ||
|
||
//go:generate pegomock generate --package mocks -o mocks/mock_slack.go slack.go | ||
|
||
type WebhooksSender interface { | ||
Send(log *logging.SimpleLogger, result ApplyResult) | ||
} | ||
|
||
type WebhookSender interface { | ||
Send(ApplyResult) error | ||
} | ||
|
||
type ApplyResult struct { | ||
Environment string | ||
Repo models.Repo | ||
Pull models.PullRequest | ||
User models.User | ||
Success bool | ||
} | ||
|
||
type WebhooksManager struct { | ||
Webhooks []WebhookSender | ||
} | ||
|
||
type Config struct { | ||
Event string | ||
WorkspaceRegex string | ||
Kind string | ||
Channel string | ||
} | ||
|
||
func NewWebhooksManager(configs []Config, slackToken string) (*WebhooksManager, error) { | ||
var webhooks []WebhookSender | ||
for _, c := range configs { | ||
r, err := regexp.Compile(c.WorkspaceRegex) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if c.Event != ApplyEvent { | ||
return nil, fmt.Errorf("event: %s not supported. Only event: %s is supported right now", c.Kind, ApplyEvent) | ||
} | ||
if c.Kind != SlackKind { | ||
return nil, fmt.Errorf("kind: %s not supported. Only kind: %s is supported right now", c.Kind, SlackKind) | ||
} | ||
if slackToken == "" { | ||
return nil, errors.New("for slack webhooks, slack-token must be set") | ||
} | ||
|
||
slack, err := NewSlack(r, c.Channel, slackToken) | ||
if err != nil { | ||
return nil, err | ||
} | ||
webhooks = append(webhooks, slack) | ||
} | ||
|
||
return &WebhooksManager{ | ||
Webhooks: webhooks, | ||
}, nil | ||
} | ||
|
||
func (w *WebhooksManager) Send(log *logging.SimpleLogger, result ApplyResult) { | ||
for _, w := range w.Webhooks { | ||
if err := w.Send(result); err != nil { | ||
log.Warn("error sending slack webhook: %s", err) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general comments should be used to
For 2. we should strive to write code that removes the need to comment it because it's so clear.
In this case, you've done that, it's pretty obvious what
a.Webhooks.Send
is doing and so the commentSend webhooks.
doesn't tell me any more information.The problem with comments is that they're a form of duplication and they're harder to keep up to date. If I rename
Send()
toQueue()
then my code won't compile if I don't change the function everywhere, but your comment might get left behind and fall out of date.tl;dr: remove this comment, it adds nothing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the insight, I agree with the view on what comments should be and developers should strive to write self-documenting code -- I'll will remove the comment :P