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

Adding slack integration for posting to channel on applies. Fixes #179 #180

Closed
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
12 changes: 12 additions & 0 deletions server/events/apply_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/hootsuite/atlantis/server/events/models"
"github.com/hootsuite/atlantis/server/events/run"
"github.com/hootsuite/atlantis/server/events/terraform"
"github.com/hootsuite/atlantis/server/events/webhooks"
)

type ApplyExecutor struct {
Expand All @@ -21,6 +22,7 @@ type ApplyExecutor struct {
Run *run.Run
Workspace Workspace
ProjectPreExecute *ProjectPreExecute
Webhooks webhooks.WebhooksSender
}

func (a *ApplyExecutor) Execute(ctx *CommandContext) CommandResponse {
Expand Down Expand Up @@ -92,6 +94,16 @@ func (a *ApplyExecutor) apply(ctx *CommandContext, repoDir string, plan models.P
env := ctx.Command.Environment
tfApplyCmd := append(append(append([]string{"apply", "-no-color"}, applyExtraArgs...), ctx.Command.Flags...), plan.LocalPath)
output, err := a.Terraform.RunCommandWithVersion(ctx.Log, absolutePath, tfApplyCmd, terraformVersion, env)

// Send webhooks.
Copy link
Collaborator

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

  1. explain why a piece of code is doing what it is
  2. explain what a large section of code is doing if it's not clear from just reading it

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 comment Send 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() to Queue() 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

Copy link
Contributor Author

@nicholas-wu-hs nicholas-wu-hs Nov 14, 2017

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

a.Webhooks.Send(ctx.Log, webhooks.ApplyResult{
Environment: env,
User: ctx.User,
Repo: ctx.BaseRepo,
Pull: ctx.Pull,
Success: err == nil,
})

if err != nil {
return ProjectResult{Error: fmt.Errorf("%s\n%s", err.Error(), output)}
}
Expand Down
2 changes: 1 addition & 1 deletion server/events/plan_executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import (
rmocks "github.com/hootsuite/atlantis/server/events/run/mocks"
tmocks "github.com/hootsuite/atlantis/server/events/terraform/mocks"
"github.com/hootsuite/atlantis/server/logging"
. "github.com/petergtz/pegomock"
. "github.com/hootsuite/atlantis/testing"
. "github.com/petergtz/pegomock"
)

var planCtx = events.CommandContext{
Expand Down
137 changes: 137 additions & 0 deletions server/events/webhooks/mocks/mock_webhooks.go
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
}
88 changes: 88 additions & 0 deletions server/events/webhooks/slack.go
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.
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is a good comment. I'd add
Make sure the slack channel exists so we can error early.

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"
Copy link
Collaborator

Choose a reason for hiding this comment

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

make these constants at the top of file underneath the import section:

const successColour = "good"
const failureColour = "danger"

(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)
}
4 changes: 4 additions & 0 deletions server/events/webhooks/slack_test.go
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
Copy link
Collaborator

Choose a reason for hiding this comment

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

nice work figuring this out

79 changes: 79 additions & 0 deletions server/events/webhooks/webhooks.go
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)
}
}
}
Loading