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 2 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
24 changes: 24 additions & 0 deletions server/events/apply_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ import (
"github.com/hootsuite/atlantis/server/events/github"
"github.com/hootsuite/atlantis/server/events/models"
"github.com/hootsuite/atlantis/server/events/run"
"github.com/hootsuite/atlantis/server/events/slack"
"github.com/hootsuite/atlantis/server/events/terraform"
)

type ApplyExecutor struct {
Github github.Client
Slack slack.Client
Terraform *terraform.Client
RequireApproval bool
Run *run.Run
Expand Down Expand Up @@ -93,8 +95,14 @@ func (a *ApplyExecutor) apply(ctx *CommandContext, repoDir string, plan models.P
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)
if err != nil {
if a.Slack != nil {
a.Slack.PostMessage(createSlackMessage(ctx, false))
}
return ProjectResult{Error: fmt.Errorf("%s\n%s", err.Error(), output)}
}
if a.Slack != nil {
a.Slack.PostMessage(createSlackMessage(ctx, true))
}
ctx.Log.Info("apply succeeded")

if len(config.PostApply) > 0 {
Expand All @@ -106,3 +114,19 @@ func (a *ApplyExecutor) apply(ctx *CommandContext, repoDir string, plan models.P

return ProjectResult{ApplySuccess: output}
}

func createSlackMessage(ctx *CommandContext, success bool) string {
Copy link
Collaborator

Choose a reason for hiding this comment

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

this functionality doesn't belong in the apply executor since it's all about applying, not creating slack messages. Put this in the slack client.

Copy link
Collaborator

Choose a reason for hiding this comment

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

don't use package functions like this, instead add it to the struct.
func (a *ApplyExecutor) createSlackMessage

If you don't do this then anyone can call this method from anywhere in this package which is weird, you really only want it called from the object

var status string
if success {
status = ":white_check_mark:"
} else {
status = ":x:"
}

return fmt.Sprintf("%s *%s* %s in <%s|%s>.",
Copy link
Collaborator

Choose a reason for hiding this comment

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

can you add a screenshot so we can see what this looks like?

Copy link
Contributor Author

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

Choose a reason for hiding this comment

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

Here it is for using the emojis!
screen shot 2017-11-02 at 4 27 40 pm

Copy link
Contributor Author

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

Choose a reason for hiding this comment

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

Also had them as text at one point:
screen shot 2017-11-02 at 4 41 27 pm

Copy link
Contributor Author

Choose a reason for hiding this comment

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

And as attachments but they're chunkier:
screen shot 2017-11-02 at 4 42 09 pm

status,
ctx.User.Username,
ctx.Command.Name.String()+" "+ctx.Command.Environment,
ctx.Pull.URL,
ctx.BaseRepo.Name)
}
52 changes: 52 additions & 0 deletions server/events/slack/slack_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package slack

import (
"errors"

"github.com/nlopes/slack"
)

type Client interface {
PostMessage(text string) (string, error)
}

type ConcreteClient struct {
client *slack.Client
channel string
}

func NewClient(slackToken string, channelName string) (*ConcreteClient, error) {
slackClient := slack.New(slackToken)

if _, err := slackClient.AuthTest(); err != nil {
return nil, err
}

// https://api.slack.com/faq
// 'How do I find a channel's ID if I only have its #name?'
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think you need this. It says you can just use the name: https://api.slack.com/methods/chat.postMessage#channels

Copy link
Contributor Author

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

Choose a reason for hiding this comment

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

With chat.postMessage you can use a channel name, but if the channel doesn't exist, it doesn't send.

I was thinking of making sure the channel from config exists on the creation of the slack client and failing early if it doesn't, rather than failing at chat.postMessage or failing silently.

So to do that, I looked at https://api.slack.com/methods/channels.info which requires an ID. Alternatively, I can do a chat.postMessage and see if it succeeds but that would post a message? Or there might be other ways!

Copy link
Collaborator

Choose a reason for hiding this comment

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

I like the idea of failing when Atlantis server starts if that channel doesn't exist. Failing early is usually better so good call.

// says need to look through all channels and match the name
channels, err := slackClient.GetChannels(true)
if err != nil {
return nil, err
}
for _, c := range channels {
if c.Name == channelName {
// channel exists, no errors
return &ConcreteClient{
client: slackClient,
channel: channelName,
}, nil
}
}

return nil, errors.New("channel_not_found")
Copy link
Collaborator

Choose a reason for hiding this comment

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

errors should be human readable so you could have just had "channel doesn't exist"

}

func (s *ConcreteClient) PostMessage(text string) (string, error) {
params := slack.NewPostMessageParameters()
params.AsUser = true
params.EscapeText = false

_, timestamp, err := s.client.PostMessage(s.channel, text, params)
return timestamp, err
}
12 changes: 12 additions & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/hootsuite/atlantis/server/events/locking"
"github.com/hootsuite/atlantis/server/events/locking/boltdb"
"github.com/hootsuite/atlantis/server/events/run"
"github.com/hootsuite/atlantis/server/events/slack"
"github.com/hootsuite/atlantis/server/events/terraform"
"github.com/hootsuite/atlantis/server/logging"
"github.com/hootsuite/atlantis/server/static"
Expand Down Expand Up @@ -53,6 +54,8 @@ type Config struct {
LogLevel string `mapstructure:"log-level"`
Port int `mapstructure:"port"`
RequireApproval bool `mapstructure:"require-approval"`
SlackToken string `mapstructure:"slack-token"`
SlackChannel string `mapstructure:"slack-channel"`
}

func NewServer(config Config) (*Server, error) {
Expand All @@ -61,6 +64,14 @@ func NewServer(config Config) (*Server, error) {
return nil, err
}
githubStatus := &events.GithubStatus{Client: githubClient}
// nil slackClient unless token and channel was specified
var slackClient slack.Client
if config.SlackToken != "" && config.SlackChannel != "" {
Copy link
Contributor Author

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

Choose a reason for hiding this comment

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

Only attempt to create a slack client if both token and channel are supplied.
If we decide to only use a token and channel, I plan to add a warning for when the user supplies ONLY one item. But the schema will probably be changed after discussion in #179 and this'll be irrelevant.

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 it's dangerous to use nil for something like this because it's not obvious that my slack client will be nil just because somewhere up the chain depending on the config. On the other hand, you can't initialize a slack client if you don't have the config :D.

I would create a NoopSlackClient that implements the interface but doesn't do anything and use it here if there is no slack config.

slackClient, err = slack.NewClient(config.SlackToken, config.SlackChannel)
if err != nil {
return nil, errors.Wrap(err, "initializing slack client")
}
}
terraformClient, err := terraform.NewClient()
if err != nil {
return nil, errors.Wrap(err, "initializing terraform")
Expand All @@ -86,6 +97,7 @@ func NewServer(config Config) (*Server, error) {
}
applyExecutor := &events.ApplyExecutor{
Github: githubClient,
Slack: slackClient,
Terraform: terraformClient,
RequireApproval: config.RequireApproval,
Run: run,
Expand Down