-
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 2 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 |
---|---|---|
|
@@ -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 | ||
|
@@ -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 { | ||
|
@@ -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 { | ||
var status string | ||
if success { | ||
status = ":white_check_mark:" | ||
} else { | ||
status = ":x:" | ||
} | ||
|
||
return fmt.Sprintf("%s *%s* %s in <%s|%s>.", | ||
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. can you add a screenshot so we can see what this looks like? 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. 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. 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. |
||
status, | ||
ctx.User.Username, | ||
ctx.Command.Name.String()+" "+ctx.Command.Environment, | ||
ctx.Pull.URL, | ||
ctx.BaseRepo.Name) | ||
} |
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?' | ||
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. I don't think you need this. It says you can just use the name: https://api.slack.com/methods/chat.postMessage#channels 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. With I was thinking of making sure the So to do that, I looked at https://api.slack.com/methods/channels.info which requires an ID. Alternatively, I can do a 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. 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") | ||
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. 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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" | ||
|
@@ -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) { | ||
|
@@ -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 != "" { | ||
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. Only attempt to create a slack client if both 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. In general it's dangerous to use 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") | ||
|
@@ -86,6 +97,7 @@ func NewServer(config Config) (*Server, error) { | |
} | ||
applyExecutor := &events.ApplyExecutor{ | ||
Github: githubClient, | ||
Slack: slackClient, | ||
Terraform: terraformClient, | ||
RequireApproval: config.RequireApproval, | ||
Run: run, | ||
|
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.
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.
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.
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